mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 18:10:21 +00:00
* Feat: Support Native Cue in HealthPolicy and CustomStatus Signed-off-by: Brian Kane <briankane1@gmail.com> * Feat: Support Native Cue in HealthPolicy and CustomStatus - Fix PR Comments & Bugs Signed-off-by: Brian Kane <briankane1@gmail.com> --------- Signed-off-by: Brian Kane <briankane1@gmail.com>
363 lines
12 KiB
Go
363 lines
12 KiB
Go
/*
|
|
Copyright 2021 The KubeVela Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package definition
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
ast2 "cuelang.org/go/cue/ast"
|
|
"cuelang.org/go/cue/parser"
|
|
|
|
"github.com/oam-dev/kubevela/pkg/definition/ast"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
"sigs.k8s.io/yaml"
|
|
|
|
common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
|
addonutils "github.com/oam-dev/kubevela/pkg/utils/addon"
|
|
"github.com/oam-dev/kubevela/pkg/utils/common"
|
|
"github.com/oam-dev/kubevela/pkg/utils/filters"
|
|
)
|
|
|
|
func TestDefinitionBasicFunctions(t *testing.T) {
|
|
c := fake.NewClientBuilder().WithScheme(common.Scheme).Build()
|
|
def := &Definition{Unstructured: unstructured.Unstructured{}}
|
|
def.SetAnnotations(map[string]string{
|
|
UserPrefix + "annotation": "annotation",
|
|
"other": "other",
|
|
})
|
|
def.SetLabels(map[string]string{
|
|
UserPrefix + "label": "label",
|
|
"other": "other",
|
|
})
|
|
def.SetName("test-trait")
|
|
def.SetGVK("TraitDefinition")
|
|
def.SetOwnerReferences([]v1.OwnerReference{{
|
|
Name: addonutils.Addon2AppName("test-addon"),
|
|
}})
|
|
if _type := def.GetType(); _type != "trait" {
|
|
t.Fatalf("set gvk invalid, expected trait got %s", _type)
|
|
}
|
|
if err := def.SetType("abc"); err == nil {
|
|
t.Fatalf("set type should failed due to invalid type, but got no error")
|
|
}
|
|
def.Object["spec"] = GetDefinitionDefaultSpec("TraitDefinition")
|
|
_ = unstructured.SetNestedField(def.Object, "patch: metadata: labels: \"KubeVela-test\": parameter.tag\nparameter: tag: string\n", "spec", "schematic", "cue", "template")
|
|
cueString, err := def.ToCUEString()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error when getting to cue: %v", err)
|
|
}
|
|
trait := &v1beta1.TraitDefinition{}
|
|
s, _ := json.Marshal(def.Object)
|
|
_ = json.Unmarshal(s, trait)
|
|
if err = c.Create(context.Background(), trait); err != nil {
|
|
t.Fatalf("unexpected error when creating new definition with fake client: %v", err)
|
|
}
|
|
if err = def.FromCUEString("abc:]{xa}", nil); err == nil {
|
|
t.Fatalf("should encounter invalid cue string but not found error")
|
|
}
|
|
if err = def.FromCUEString(cueString+"abc: {xa}", nil); err == nil {
|
|
t.Fatalf("should encounter invalid cue string but not found error")
|
|
}
|
|
parts := strings.Split(cueString, "template: ")
|
|
if err = def.FromCUEString(parts[0], nil); err == nil {
|
|
t.Fatalf("should encounter no template found error but not found error")
|
|
}
|
|
if err = def.FromCUEString("template:"+parts[1], nil); err == nil {
|
|
t.Fatalf("should encounter no metadata found error but not found error")
|
|
}
|
|
if err = def.FromCUEString("import \"strconv\"\n"+cueString, nil); err != nil {
|
|
t.Fatalf("should not encounter cue compile error due to useless import")
|
|
}
|
|
if err = def.FromCUEString("abc: {}\n"+cueString, nil); err == nil {
|
|
t.Fatalf("should encounter duplicated object name error but not found error")
|
|
}
|
|
if err = def.FromCUEString(strings.Replace(cueString, "\"trait\"", "\"tr\"", 1), nil); err == nil {
|
|
t.Fatalf("should encounter invalid type error but not found error")
|
|
}
|
|
if err = def.FromCUEString(cueString, nil); err != nil {
|
|
t.Fatalf("unexpected error when setting from cue: %v", err)
|
|
}
|
|
if _cueString, err := def.ToCUEString(); err != nil {
|
|
t.Fatalf("failed to generate cue string: %v", err)
|
|
} else if _cueString != cueString {
|
|
t.Fatalf("the bidirectional conversion of cue string is not idempotent")
|
|
}
|
|
templateString, _, _ := unstructured.NestedString(def.Object, DefinitionTemplateKeys...)
|
|
_ = unstructured.SetNestedField(def.Object, "import \"strconv\"\n"+templateString, DefinitionTemplateKeys...)
|
|
if s, err := def.ToCUEString(); err != nil {
|
|
t.Fatalf("failed to generate cue string: %v", err)
|
|
} else if !strings.Contains(s, "import \"strconv\"\n") {
|
|
t.Fatalf("definition ToCUEString missed import, val: %v", s)
|
|
}
|
|
def = &Definition{}
|
|
if err = def.FromCUEString(cueString, nil); err != nil {
|
|
t.Fatalf("unexpected error when setting from cue for empty def: %v", err)
|
|
}
|
|
|
|
// test other definition default spec
|
|
_ = GetDefinitionDefaultSpec("ComponentDefinition")
|
|
_ = GetDefinitionDefaultSpec("WorkloadDefinition")
|
|
_ = ValidDefinitionTypes()
|
|
|
|
if _, err = SearchDefinition(c, "", ""); err != nil {
|
|
t.Fatalf("failed to search definition: %v", err)
|
|
}
|
|
if _, err = SearchDefinition(c, "trait", "default"); err != nil {
|
|
t.Fatalf("failed to search definition: %v", err)
|
|
}
|
|
res, err := SearchDefinition(c, "", "", filters.ByOwnerAddon("test-addon"))
|
|
if err != nil {
|
|
t.Fatalf("failed to search definition: %v", err)
|
|
}
|
|
if len(res) < 1 {
|
|
t.Fatalf("failed to search definition with addon filter applied: %s", "no result returned")
|
|
}
|
|
res, err = SearchDefinition(c, "", "", filters.ByName("test-trait"), filters.ByOwnerAddon("test-addon"))
|
|
if err != nil {
|
|
t.Fatalf("failed to search definition: %v", err)
|
|
}
|
|
if len(res) < 1 {
|
|
t.Fatalf("failed to search definition with addon filter applied: %s", "no result returned")
|
|
}
|
|
res, err = SearchDefinition(c, "", "", filters.ByOwnerAddon("this-is-a-non-existent-addon"))
|
|
if err != nil {
|
|
t.Fatalf("failed to search definition: %v", err)
|
|
}
|
|
if len(res) >= 1 {
|
|
t.Fatalf("failed to search definition with addon filter applied: %s", "too many results returned")
|
|
}
|
|
}
|
|
|
|
func TestDefinitionRevisionSearch(t *testing.T) {
|
|
c := fake.NewClientBuilder().WithScheme(common.Scheme).Build()
|
|
|
|
var err error
|
|
|
|
// Load test DefinitionRevisions files into client
|
|
testFiles, err := os.ReadDir("testdata")
|
|
assert.NoError(t, err, "read testdata failed")
|
|
for _, file := range testFiles {
|
|
if !strings.HasSuffix(file.Name(), ".yaml") {
|
|
continue
|
|
}
|
|
content, err := os.ReadFile(filepath.Join("testdata", file.Name()))
|
|
assert.NoError(t, err)
|
|
def := &v1beta1.DefinitionRevision{}
|
|
err = yaml.Unmarshal(content, def)
|
|
assert.NoError(t, err)
|
|
err = c.Create(context.TODO(), def)
|
|
assert.NoError(t, err, "cannot create "+file.Name())
|
|
}
|
|
|
|
var defrevs []v1beta1.DefinitionRevision
|
|
|
|
// Read with no conditions, should at least have 4 defrevs
|
|
defrevs, err = SearchDefinitionRevisions(context.TODO(), c, "", "", "", 0)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, true, len(defrevs) >= 4)
|
|
|
|
// Restrict namespace
|
|
defrevs, err = SearchDefinitionRevisions(context.TODO(), c, "rev-test-custom-ns", "", "", 0)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 1, len(defrevs))
|
|
|
|
// Restrict type
|
|
defrevs, err = SearchDefinitionRevisions(context.TODO(), c, "rev-test-ns", "", common2.ComponentType, 0)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 2, len(defrevs))
|
|
|
|
// Restrict revision
|
|
defrevs, err = SearchDefinitionRevisions(context.TODO(), c, "rev-test-ns", "", "", 1)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 2, len(defrevs))
|
|
|
|
// Restrict name
|
|
defrevs, err = SearchDefinitionRevisions(context.TODO(), c, "rev-test-ns", "webservice", "", 1)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 1, len(defrevs))
|
|
|
|
// Test GetDefinitionFromDefinitionRevision
|
|
defrev := defrevs[0]
|
|
|
|
// Simulate ComponentDefinition
|
|
defrev.Spec.DefinitionType = common2.ComponentType
|
|
_, err = GetDefinitionFromDefinitionRevision(&defrev)
|
|
assert.NoError(t, err)
|
|
|
|
// Simulate TraitDefinition
|
|
defrev.Spec.DefinitionType = common2.TraitType
|
|
_, err = GetDefinitionFromDefinitionRevision(&defrev)
|
|
assert.NoError(t, err)
|
|
|
|
// Simulate PolicyDefinition
|
|
defrev.Spec.DefinitionType = common2.PolicyType
|
|
_, err = GetDefinitionFromDefinitionRevision(&defrev)
|
|
assert.NoError(t, err)
|
|
|
|
// Simulate WorkflowStepDefinition
|
|
defrev.Spec.DefinitionType = common2.WorkflowStepType
|
|
_, err = GetDefinitionFromDefinitionRevision(&defrev)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestValidateSpec(t *testing.T) {
|
|
testcases := map[string]struct {
|
|
Input string
|
|
Type string
|
|
HasErr bool
|
|
}{
|
|
"comp": {
|
|
Input: `{"podSpecPath": "a"}`,
|
|
Type: "component",
|
|
},
|
|
"trait": {
|
|
Input: `{"appliesToWorkloads":["deployments"]}`,
|
|
Type: "trait",
|
|
},
|
|
"workflow-step": {
|
|
Input: `{"definitionRef":{"name":"v"}}`,
|
|
Type: "workflow-step",
|
|
},
|
|
"bad-policy": {
|
|
Input: `{"definitionRef":{"invalid":5}}`,
|
|
Type: "policy",
|
|
HasErr: true,
|
|
},
|
|
"unknown": {
|
|
Input: `{}`,
|
|
Type: "unknown",
|
|
HasErr: false,
|
|
},
|
|
}
|
|
for name, tt := range testcases {
|
|
t.Run(name, func(t *testing.T) {
|
|
spec := map[string]interface{}{}
|
|
require.NoError(t, json.Unmarshal([]byte(tt.Input), &spec))
|
|
err := validateSpec(spec, tt.Type)
|
|
if tt.HasErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCueNativeStatusFromCueString(t *testing.T) {
|
|
cueStr := strings.TrimSpace(`
|
|
"a-component": {
|
|
attributes: {
|
|
workload: {
|
|
definition: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
}
|
|
}
|
|
status: {
|
|
customStatus: {
|
|
message: "\(context.output.status.readyReplicas) / \(context.output.status.replicas) replicas are ready"
|
|
}
|
|
|
|
healthPolicy: {
|
|
isHealth: context.output.status.readyReplicas == context.output.status.replicas
|
|
}
|
|
|
|
details: {
|
|
$temp: context.output.status.replicas
|
|
deploymentReady: *(context.output.status.replicas == context.output.status.readyReplicas) | false
|
|
}
|
|
}
|
|
}
|
|
type: "component"
|
|
}
|
|
|
|
template: output: {}
|
|
`)
|
|
|
|
def := &Definition{}
|
|
err := def.FromCUEString(cueStr, nil)
|
|
require.NoError(t, err, "failed to parse cue string")
|
|
|
|
str, err := def.ToCUEString()
|
|
require.NoError(t, err, "failed to convert definition to CUE")
|
|
|
|
f, err := parser.ParseFile("-", str, parser.ParseComments)
|
|
require.NoError(t, err, "failed to parse resulting CUE string")
|
|
|
|
statusField, ok := ast.GetFieldByPath(f, fmt.Sprintf("%s.attributes.status.details", def.GetName()))
|
|
require.True(t, ok, "status field not found in CUE definition")
|
|
require.IsType(t, &ast2.StructLit{}, statusField.Value, "expected status field to be of type StructLit")
|
|
}
|
|
|
|
func TestStringStatusFromCueString(t *testing.T) {
|
|
cueStr := strings.TrimSpace(`
|
|
"a-component": {
|
|
attributes: {
|
|
workload: {
|
|
definition: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
}
|
|
}
|
|
status: {
|
|
customStatus: #"""
|
|
message: "\(context.output.status.readyReplicas) / \(context.output.status.replicas) replicas are ready"
|
|
"""#
|
|
|
|
healthPolicy: #"""
|
|
isHealth: context.output.status.readyReplicas == context.output.status.replicas
|
|
"""#
|
|
|
|
details: #"""
|
|
$someValue: context.output.status.replicas
|
|
deploymentReady: *(context.output.status.replicas == context.output.status.readyReplicas) | false
|
|
replicas: *context.output.status.replicas | 0
|
|
"""#
|
|
}
|
|
}
|
|
type: "component"
|
|
}
|
|
|
|
template: output: {}
|
|
`)
|
|
def := &Definition{}
|
|
err := def.FromCUEString(cueStr, nil)
|
|
require.NoError(t, err, "failed to parse cue string")
|
|
|
|
str, err := def.ToCUEString()
|
|
require.NoError(t, err, "failed to convert definition to CUE")
|
|
|
|
f, err := parser.ParseFile("-", str, parser.ParseComments)
|
|
require.NoError(t, err, "failed to parse resulting CUE string")
|
|
|
|
statusField, ok := ast.GetFieldByPath(f, fmt.Sprintf("%s.attributes.status.details", def.GetName()))
|
|
require.True(t, ok, "status field not found in CUE definition")
|
|
require.IsType(t, &ast2.StructLit{}, statusField.Value, "expected status field to be of type StructLit")
|
|
}
|