/* 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 ") } // TestFromCUEString_PreservesLegacySyntax verifies that FromCUEString stores the // original user-supplied CUE verbatim. Validation runs against the upgraded form, // but the stored template retains the legacy syntax for the compat layer to handle at render time. func TestFromCUEString_PreservesLegacySyntax(t *testing.T) { legacyCUE := strings.TrimSpace(` "legacy-worker": { type: "component" annotations: {} } template: { envWithDefaults: parameter.env + [{name: "MANAGED_BY", value: "kubevela"}] output: { apiVersion: "apps/v1" kind: "Deployment" spec: containers: [{env: envWithDefaults}] } parameter: { env: *[] | [...{name: string, value?: string}] } } `) def := &Definition{} require.NoError(t, def.FromCUEString(legacyCUE, nil)) storedTemplate, found, err := unstructured.NestedString(def.Object, DefinitionTemplateKeys...) require.NoError(t, err) require.True(t, found) assert.Contains(t, storedTemplate, "parameter.env +") assert.NotContains(t, storedTemplate, "list.Concat") }