Files
kubevela/pkg/definition/definition_test.go
Brian Kane a5de74ec1e Feat: Support Native Cue in HealthPolicy and CustomStatus (#6859)
* 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>
2025-08-22 12:24:21 +08:00

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")
}