mirror of
https://github.com/kubevela/kubevela.git
synced 2026-05-20 16:23:24 +00:00
* Proof Concept Signed-off-by: Brian Kane <briankane1@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Cleanup dispatch stages and remove dry run namespaces Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Single Health Condition Working Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Multiple Health Conditions Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Support Empty Health Status Signed-off-by: Brian Kane <briankane1@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Caching Signed-off-by: Brian Kane <briankane1@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * includes changes from make-reviewable Signed-off-by: Amit Singh <singhamitch@outlook.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * minor formatting fixes Signed-off-by: Amit Singh <singhamitch@outlook.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * fix: handles cases where resource is nil Signed-off-by: Amit Singh <singhamitch@outlook.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: creates chart release before returning rendered resources to kubevela Signed-off-by: Amit Singh <singhamitch@outlook.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: helm release record via dry-run for helm list visibility The previous createChartRelease did a real helm install/upgrade which deployed resources directly, conflicting with KubeVela's dispatch ("exists but not managed by any application" errors). Also, Helm's lazy client via cli.New().RESTClientGetter() silently wrote to in-memory storage instead of Kubernetes secrets, so helm list never showed releases. Fix: use dry-run install to build a release object without deploying resources, and bypass Helm's lazy client by creating a direct Kubernetes clientset wired to helmdriver.NewSecrets. Set KubeVersion from the real cluster version to pass chart constraints. Prevent duplicate revisions on repeated reconciles via mutex serialization and in-memory fingerprint tracking (chartVersion + values). New revisions are only created when chart version or values actually change. Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * fix: override Kubernetes version for kubeVersion validation in chart release Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Enhance Helm chart component definition and CUE provider, update API types, and add new examples and documentation. Signed-off-by: Chaitanya Reddy Onteddu <co@guidewire.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Add Helm uninstall action and optimize install/upgrade with release fingerprinting and KubeVela ownership labels. Signed-off-by: Chaitanyareddy0702 <chaitanyareddy0702@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Introduce to prevent resources from being applied while ensuring they are recorded for garbage collection, and apply it to Helm release secrets. Signed-off-by: Chaitanyareddy0702 <chaitanyareddy0702@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Enhance velaLabelPostRenderer to include Helm ownership annotations and improve cache invalidation logic for Helm releases Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Enhance Helm release secret tracking for garbage collection during application deletion and upgrades Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> Co-Authored-By: Jerrin Francis <jerrinfrancis7@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Add Helm chart test and application definition for podinfo deployment Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Refactor Helm release secret handling and improve metadata filtering for KubeVela adoption Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Label existing Helm release secrets with KubeVela ownership for better tracking and cleanup Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Remove outdated documentation files for Helm integration and revision management Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Add various Helm chart application test and improve test coverage for Helmchart component Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Update Helm chart tests to validate deployment list and ensure label presence Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Enhance Helm chart tests for resource verification and application health checks Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Update Helm chart test scenarios for multi-component health checks and resource configurations Signed-off-by: Anaswara Suresh <anaswarasuresh2212@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Implement dry-run mode for Helm provider to enhance validation without cluster interaction Signed-off-by: Anaswara Suresh <anaswarasuresh2212@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Refactor import statements and enhance Helmchart edge case tests for better error handling and validation Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Refactor Helm provider methods for improved error handling and retry logic during installation Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Improve Helm provider tests and documentation with enhanced assertions and example configurations Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Register helm package with cuex.DefaultCompiler for improved import resolution Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Remove crossplane application configuration from crossplane-app.yaml Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Enhance workload compiler with external package loading and improve Helm chart health checks Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Remove unused cuex import and initialization function from helm provider Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Replace cuex.DefaultCompiler with localcuex.WorkloadCompiler for improved CUE compilation Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: Implement dry-run mode for Helm provider functions during validation Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * test: add unit tests for Helm provider and workflow provider - Created suite tests for the Helm provider in pkg/cue/cuex/providers/helm/suite_test.go. - Implemented unit tests for Helm workflow provider in pkg/workflow/providers/helm/helm_test.go. - Added suite tests for Helm workflow provider in pkg/workflow/providers/helm/suite_test.go. Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> * feat: Add additional health check tests for error scenarios Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> * feat: Enhance health check logic and improve documentation in helmchart definition Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> * feat: Refactor helmchart template for improved readability and structure Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> * docs: remove redundant struct-only tests from helm_test.go Signed-off-by: Chaitanyareddy0702 <chaitanyareddy0702@gmail.com> * Feat: Implement CUE sanitization for OpenAPI schema generation Signed-off-by: Vishal Kumar <vishal210893@gmail.com> * feat: Add tests for rendering and fetching Helm charts with dry-run support Signed-off-by: vaibhav0096 <vaibhav.agrawal0096@gmail.com> * feat: Enhance required parameter validation tests with retry logic for webhook synchronization Signed-off-by: vaibhav0096 <vaibhav.agrawal0096@gmail.com> * feat: Improve chart version retrieval by sorting entries and enhancing error messages Signed-off-by: vaibhav0096 <vaibhav.agrawal0096@gmail.com> * feat: Enhance primary output handling for Helm charts with stable ConfigMap metadata Signed-off-by: vaibhav0096 <vaibhav.agrawal0096@gmail.com> * chore: Fix CUE formatting alignment in helmchart definition Signed-off-by: vaibhav0096 <vaibhav.agrawal0096@gmail.com> * feat: Extend timeout for self-healing Helmchart tests to improve reliability Signed-off-by: vaibhav0096 <vaibhav.agrawal0096@gmail.com> * feat: Increase timeout for golangci-lint to enhance linting reliability Signed-off-by: vaibhav0096 <vaibhav.agrawal0096@gmail.com> --------- Signed-off-by: Brian Kane <briankane1@gmail.com> Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> Signed-off-by: Amit Singh <singhamitch@outlook.com> Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: Chaitanya Reddy Onteddu <co@guidewire.com> Signed-off-by: Chaitanyareddy0702 <chaitanyareddy0702@gmail.com> Signed-off-by: Anaswara Suresh <anaswarasuresh2212@gmail.com> Signed-off-by: Vishal Kumar <vishal210893@gmail.com> Signed-off-by: vaibhav0096 <vaibhav.agrawal0096@gmail.com> Co-authored-by: Brian Kane <briankane1@gmail.com> Co-authored-by: Amit Singh <singhamitch@outlook.com> Co-authored-by: Jerrin Francis <jerrinfrancis7@gmail.com> Co-authored-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Co-authored-by: Chaitanya Reddy Onteddu <co@guidewire.com> Co-authored-by: Chaitanyareddy0702 <chaitanyareddy0702@gmail.com> Co-authored-by: Vishal Kumar <vishal210893@gmail.com> Co-authored-by: Anaswara Suresh <anaswarasuresh2212@gmail.com> Co-authored-by: vaibhav0096 <vaibhav.agrawal0096@gmail.com>
1490 lines
38 KiB
Go
1490 lines
38 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 appfile
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"cuelang.org/go/cue"
|
|
wfTypesv1alpha1 "github.com/kubevela/pkg/apis/oam/v1alpha1"
|
|
"github.com/stretchr/testify/assert"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
|
"github.com/oam-dev/kubevela/apis/types"
|
|
"github.com/oam-dev/kubevela/pkg/cue/definition"
|
|
"github.com/oam-dev/kubevela/pkg/features"
|
|
)
|
|
|
|
func TestTrait_EvalContext_OutputNameUniqueness(t *testing.T) {
|
|
type SubTestCase struct {
|
|
name string
|
|
compDefTmpl string
|
|
traitDefTmpl1 string
|
|
traitDefTmpl2 string
|
|
wantErrMsg string
|
|
}
|
|
|
|
testCases := []SubTestCase{
|
|
{
|
|
name: "Succeed",
|
|
compDefTmpl: `
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
}
|
|
outputs: mysvc: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
}
|
|
`,
|
|
traitDefTmpl1: `
|
|
outputs: mysvc1: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
}
|
|
`,
|
|
traitDefTmpl2: `
|
|
outputs: mysvc2: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
}
|
|
`,
|
|
wantErrMsg: "",
|
|
},
|
|
{
|
|
name: "CompDef and TraitDef have same outputs",
|
|
compDefTmpl: `
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
}
|
|
outputs: mysvc1: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
}
|
|
`,
|
|
traitDefTmpl1: `
|
|
outputs: mysvc1: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
}
|
|
`,
|
|
traitDefTmpl2: `
|
|
outputs: mysvc2: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
}
|
|
`,
|
|
wantErrMsg: `auxiliary "mysvc1" already exits`,
|
|
},
|
|
{
|
|
name: "TraitDefs have same outputs",
|
|
compDefTmpl: `
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
}
|
|
outputs: mysvc: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
}
|
|
`,
|
|
traitDefTmpl1: `
|
|
outputs: mysvc1: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
}
|
|
`,
|
|
traitDefTmpl2: `
|
|
outputs: mysvc1: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
}
|
|
`,
|
|
wantErrMsg: `auxiliary "mysvc1" already exits`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
wl := &Component{
|
|
Name: "myweb",
|
|
Type: "worker",
|
|
CapabilityCategory: types.CUECategory,
|
|
Traits: []*Trait{
|
|
{
|
|
Name: "myscaler",
|
|
CapabilityCategory: types.CUECategory,
|
|
Template: tc.traitDefTmpl1,
|
|
engine: definition.NewTraitAbstractEngine("myscaler"),
|
|
},
|
|
{
|
|
Name: "myingress",
|
|
CapabilityCategory: types.CUECategory,
|
|
Template: tc.traitDefTmpl2,
|
|
engine: definition.NewTraitAbstractEngine("myingress"),
|
|
},
|
|
},
|
|
FullTemplate: &Template{
|
|
TemplateStr: tc.compDefTmpl,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("myweb"),
|
|
}
|
|
|
|
ctxData := GenerateContextDataFromAppFile(&Appfile{
|
|
Name: "myapp",
|
|
Namespace: "test-ns",
|
|
AppRevisionName: "myapp-v1",
|
|
}, wl.Name)
|
|
pCtx, err := newValidationProcessContext(wl, ctxData)
|
|
assert.NoError(t, err)
|
|
|
|
var evalErr error
|
|
for _, tr := range wl.Traits {
|
|
if err := tr.EvalContext(pCtx); err != nil {
|
|
evalErr = err
|
|
break
|
|
}
|
|
}
|
|
|
|
if tc.wantErrMsg != "" {
|
|
assert.Error(t, evalErr)
|
|
assert.Contains(t, evalErr.Error(), tc.wantErrMsg)
|
|
} else {
|
|
assert.NoError(t, evalErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParser_ValidateComponentParams(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
compName string
|
|
template string
|
|
params map[string]interface{}
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "valid params and template",
|
|
compName: "valid",
|
|
template: `
|
|
parameter: {
|
|
replicas: int | *1
|
|
}
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
}
|
|
`,
|
|
params: map[string]interface{}{
|
|
"replicas": 2,
|
|
},
|
|
wantErr: "",
|
|
},
|
|
{
|
|
name: "invalid CUE in template",
|
|
compName: "invalid-cue",
|
|
template: `
|
|
parameter: {
|
|
replicas: int | *1
|
|
}
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
invalidField: {
|
|
}
|
|
`,
|
|
params: map[string]interface{}{
|
|
"replicas": 2,
|
|
},
|
|
wantErr: "CUE compile error",
|
|
},
|
|
{
|
|
name: "missing required parameter",
|
|
compName: "missing-required",
|
|
template: `
|
|
parameter: {
|
|
replicas: int
|
|
}
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
}
|
|
`,
|
|
params: map[string]interface{}{},
|
|
wantErr: "component \"missing-required\": missing parameters: replicas",
|
|
},
|
|
{
|
|
name: "parameter constraint violation",
|
|
compName: "constraint-violation",
|
|
template: `
|
|
parameter: {
|
|
replicas: int & >0
|
|
}
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
}
|
|
`,
|
|
params: map[string]interface{}{
|
|
"replicas": -1,
|
|
},
|
|
wantErr: "parameter constraint violation",
|
|
},
|
|
{
|
|
name: "invalid parameter block",
|
|
compName: "invalid-param-block",
|
|
template: `
|
|
parameter: {
|
|
replicas: int | *1
|
|
}
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
}
|
|
`,
|
|
params: map[string]interface{}{
|
|
"replicas": "not-an-int",
|
|
},
|
|
wantErr: "parameter constraint violation",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
wl := &Component{
|
|
Name: tc.compName,
|
|
Type: "worker",
|
|
FullTemplate: &Template{TemplateStr: tc.template},
|
|
Params: tc.params,
|
|
}
|
|
app := &Appfile{
|
|
Name: "myapp",
|
|
Namespace: "test-ns",
|
|
}
|
|
ctxData := GenerateContextDataFromAppFile(app, wl.Name)
|
|
parser := &Parser{}
|
|
err := parser.ValidateComponentParams(ctxData, wl, app)
|
|
if tc.wantErr == "" {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidationHelpers(t *testing.T) {
|
|
t.Run("renderTemplate", func(t *testing.T) {
|
|
tmpl := "output: {}"
|
|
expected := "output: {}\ncontext: _\nparameter: _\n"
|
|
assert.Equal(t, expected, renderTemplate(tmpl))
|
|
})
|
|
|
|
t.Run("cueParamBlock", func(t *testing.T) {
|
|
t.Run("should handle empty params", func(t *testing.T) {
|
|
out, err := cueParamBlock(map[string]any{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "parameter: {}", out)
|
|
})
|
|
|
|
t.Run("should handle valid params", func(t *testing.T) {
|
|
params := map[string]any{"key": "value"}
|
|
out, err := cueParamBlock(params)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, `parameter: {"key":"value"}`, out)
|
|
})
|
|
|
|
t.Run("should return error for unmarshallable params", func(t *testing.T) {
|
|
params := map[string]any{"key": make(chan int)}
|
|
_, err := cueParamBlock(params)
|
|
assert.Error(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("filterMissing", func(t *testing.T) {
|
|
t.Run("should filter missing keys", func(t *testing.T) {
|
|
keys := []string{"a", "b.c", "d"}
|
|
provided := map[string]any{
|
|
"a": 1,
|
|
"b": map[string]any{
|
|
"c": 2,
|
|
},
|
|
}
|
|
out, err := filterMissing(keys, provided)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"d"}, out)
|
|
})
|
|
|
|
t.Run("should handle no missing keys", func(t *testing.T) {
|
|
keys := []string{"a"}
|
|
provided := map[string]any{"a": 1}
|
|
out, err := filterMissing(keys, provided)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, out)
|
|
})
|
|
})
|
|
|
|
t.Run("requiredFields", func(t *testing.T) {
|
|
t.Run("should identify required fields", func(t *testing.T) {
|
|
cueStr := `
|
|
parameter: {
|
|
name: string
|
|
age: int
|
|
nested: {
|
|
field1: string
|
|
field2: bool
|
|
}
|
|
}
|
|
`
|
|
var r cue.Runtime
|
|
inst, err := r.Compile("", cueStr)
|
|
assert.NoError(t, err)
|
|
val := inst.Value()
|
|
paramVal := val.LookupPath(cue.ParsePath("parameter"))
|
|
|
|
fields, err := requiredFields(paramVal)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, []string{"name", "age", "nested.field1", "nested.field2"}, fields)
|
|
})
|
|
|
|
t.Run("should ignore optional and default fields", func(t *testing.T) {
|
|
cueStr := `
|
|
parameter: {
|
|
name: string
|
|
age?: int
|
|
location: string | *"unknown"
|
|
nested: {
|
|
field1: string
|
|
field2?: bool
|
|
}
|
|
}
|
|
`
|
|
var r cue.Runtime
|
|
inst, err := r.Compile("", cueStr)
|
|
assert.NoError(t, err)
|
|
val := inst.Value()
|
|
paramVal := val.LookupPath(cue.ParsePath("parameter"))
|
|
|
|
fields, err := requiredFields(paramVal)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, []string{"name", "nested.field1"}, fields)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestEnforceRequiredParams(t *testing.T) {
|
|
var r cue.Runtime
|
|
cueStr := `
|
|
parameter: {
|
|
image: string
|
|
replicas: int
|
|
port: int
|
|
data: {
|
|
key: string
|
|
value: string
|
|
}
|
|
}
|
|
`
|
|
inst, err := r.Compile("", cueStr)
|
|
assert.NoError(t, err)
|
|
root := inst.Value()
|
|
|
|
t.Run("should pass if all params are provided directly", func(t *testing.T) {
|
|
params := map[string]any{
|
|
"image": "nginx",
|
|
"replicas": 2,
|
|
"port": 80,
|
|
"data": map[string]any{
|
|
"key": "k",
|
|
"value": "v",
|
|
},
|
|
}
|
|
app := &Appfile{}
|
|
err := enforceRequiredParams(root, params, app)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should fail if params are missing", func(t *testing.T) {
|
|
params := map[string]any{
|
|
"image": "nginx",
|
|
}
|
|
app := &Appfile{}
|
|
err := enforceRequiredParams(root, params, app)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "missing parameters: replicas,port,data.key,data.value")
|
|
})
|
|
}
|
|
|
|
func TestParser_ValidateCUESchematicAppfile(t *testing.T) {
|
|
assert.NoError(t, utilfeature.DefaultMutableFeatureGate.Set(string(features.EnableCueValidation)+"=true"))
|
|
t.Cleanup(func() {
|
|
assert.NoError(t, utilfeature.DefaultMutableFeatureGate.Set(string(features.EnableCueValidation)+"=false"))
|
|
})
|
|
|
|
t.Run("should validate a valid CUE schematic appfile", func(t *testing.T) {
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-comp",
|
|
Type: "worker",
|
|
CapabilityCategory: types.CUECategory,
|
|
Params: map[string]any{
|
|
"image": "nginx",
|
|
},
|
|
FullTemplate: &Template{
|
|
TemplateStr: `
|
|
parameter: {
|
|
image: string
|
|
}
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
spec: {
|
|
template: {
|
|
spec: {
|
|
containers: [{
|
|
name: "my-container"
|
|
image: parameter.image
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("my-comp"),
|
|
Traits: []*Trait{
|
|
{
|
|
Name: "my-trait",
|
|
CapabilityCategory: types.CUECategory,
|
|
Template: `
|
|
parameter: {
|
|
domain: string
|
|
}
|
|
patch: {}
|
|
`,
|
|
Params: map[string]any{
|
|
"domain": "example.com",
|
|
},
|
|
engine: definition.NewTraitAbstractEngine("my-trait"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should return error for invalid trait evaluation", func(t *testing.T) {
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-comp",
|
|
Type: "worker",
|
|
CapabilityCategory: types.CUECategory,
|
|
Params: map[string]any{
|
|
"image": "nginx",
|
|
},
|
|
FullTemplate: &Template{
|
|
TemplateStr: `
|
|
parameter: {
|
|
image: string
|
|
}
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
}
|
|
`,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("my-comp"),
|
|
Traits: []*Trait{
|
|
{
|
|
Name: "my-trait",
|
|
CapabilityCategory: types.CUECategory,
|
|
Template: `
|
|
// invalid CUE template
|
|
parameter: {
|
|
domain: string
|
|
}
|
|
patch: {
|
|
invalid: {
|
|
}
|
|
`,
|
|
Params: map[string]any{
|
|
"domain": "example.com",
|
|
},
|
|
engine: definition.NewTraitAbstractEngine("my-trait"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "cannot evaluate trait \"my-trait\"")
|
|
})
|
|
|
|
t.Run("should return error for missing parameters", func(t *testing.T) {
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-comp",
|
|
Type: "worker",
|
|
CapabilityCategory: types.CUECategory,
|
|
Params: map[string]any{}, // no params provided
|
|
FullTemplate: &Template{
|
|
TemplateStr: `
|
|
parameter: {
|
|
image: string
|
|
}
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
}
|
|
`,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("my-comp"),
|
|
},
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "missing parameters: image")
|
|
})
|
|
|
|
t.Run("should skip non-CUE components", func(t *testing.T) {
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-comp",
|
|
Type: "helm",
|
|
CapabilityCategory: types.TerraformCategory,
|
|
},
|
|
},
|
|
}
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestCheckUndeclaredParams(t *testing.T) {
|
|
var r cue.Runtime
|
|
|
|
t.Run("undeclared top-level parameter", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { name: string }`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
err = checkUndeclaredParams(schema, map[string]any{
|
|
"name": "test",
|
|
"otherUndeclared": "bad",
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "undeclared parameters")
|
|
assert.Contains(t, err.Error(), "otherUndeclared")
|
|
})
|
|
|
|
t.Run("undeclared nested parameter", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { data: { key: string } }`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
err = checkUndeclaredParams(schema, map[string]any{
|
|
"data": map[string]any{
|
|
"key": "ok",
|
|
"extra": "bad",
|
|
},
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "data.extra")
|
|
})
|
|
|
|
t.Run("all valid parameters", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { name: string, count: int }`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
err = checkUndeclaredParams(schema, map[string]any{
|
|
"name": "test",
|
|
"count": 1,
|
|
})
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("empty parameters", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { name: string }`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
err = checkUndeclaredParams(schema, map[string]any{})
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("optional parameters omitted", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { name: string, label?: string }`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
err = checkUndeclaredParams(schema, map[string]any{
|
|
"name": "test",
|
|
})
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("optional parameter provided is valid", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { name: string, label?: string }`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
err = checkUndeclaredParams(schema, map[string]any{
|
|
"name": "test",
|
|
"label": "ok",
|
|
})
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("pattern constraint at top level allows any keys", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: [string]: string`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
err = checkUndeclaredParams(schema, map[string]any{
|
|
"anyKey": "value1",
|
|
"anotherKey": "value2",
|
|
})
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("nested pattern constraint allows any keys", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { name: string, labels?: [string]: string }`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
err = checkUndeclaredParams(schema, map[string]any{
|
|
"name": "test",
|
|
"labels": map[string]any{
|
|
"app": "myapp",
|
|
"version": "v1",
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("webservice-like schema with labels and annotations", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: {
|
|
image: string
|
|
labels?: [string]: string
|
|
annotations?: [string]: string
|
|
}`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
err = checkUndeclaredParams(schema, map[string]any{
|
|
"image": "nginx",
|
|
"labels": map[string]any{
|
|
"app": "myapp",
|
|
},
|
|
"annotations": map[string]any{
|
|
"note": "test",
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("undeclared fields sorted in error message", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { name: string }`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
err = checkUndeclaredParams(schema, map[string]any{
|
|
"name": "test",
|
|
"zebra": "z",
|
|
"alpha": "a",
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "alpha,zebra")
|
|
})
|
|
}
|
|
|
|
func TestValidateUndeclaredParams_FeatureGate(t *testing.T) {
|
|
prevCue := utilfeature.DefaultMutableFeatureGate.Enabled(features.EnableCueValidation)
|
|
prevUndeclared := utilfeature.DefaultMutableFeatureGate.Enabled(features.ValidateUndeclaredParameters)
|
|
t.Cleanup(func() {
|
|
assert.NoError(t, utilfeature.DefaultMutableFeatureGate.Set(
|
|
fmt.Sprintf("%s=%t,%s=%t",
|
|
string(features.EnableCueValidation), prevCue,
|
|
string(features.ValidateUndeclaredParameters), prevUndeclared)))
|
|
})
|
|
|
|
t.Run("gate disabled - undeclared params allowed", func(t *testing.T) {
|
|
assert.NoError(t, utilfeature.DefaultMutableFeatureGate.Set(
|
|
string(features.EnableCueValidation)+"=true,"+
|
|
string(features.ValidateUndeclaredParameters)+"=false"))
|
|
|
|
wl := &Component{
|
|
Name: "my-comp",
|
|
Type: "worker",
|
|
FullTemplate: &Template{TemplateStr: `
|
|
parameter: { name: string }
|
|
output: { apiVersion: "v1", kind: "ConfigMap" }
|
|
`},
|
|
Params: map[string]any{"name": "test", "extra": "bad"},
|
|
}
|
|
app := &Appfile{Name: "myapp", Namespace: "test-ns"}
|
|
ctxData := GenerateContextDataFromAppFile(app, wl.Name)
|
|
err := (&Parser{}).ValidateComponentParams(ctxData, wl, app)
|
|
assert.NoError(t, err, "Should allow undeclared params when gate is disabled")
|
|
})
|
|
|
|
t.Run("gate enabled - undeclared params rejected", func(t *testing.T) {
|
|
assert.NoError(t, utilfeature.DefaultMutableFeatureGate.Set(
|
|
string(features.EnableCueValidation)+"=true,"+
|
|
string(features.ValidateUndeclaredParameters)+"=true"))
|
|
|
|
wl := &Component{
|
|
Name: "my-comp",
|
|
Type: "worker",
|
|
FullTemplate: &Template{TemplateStr: `
|
|
parameter: { name: string }
|
|
output: { apiVersion: "v1", kind: "ConfigMap" }
|
|
`},
|
|
Params: map[string]any{"name": "test", "extra": "bad"},
|
|
}
|
|
app := &Appfile{Name: "myapp", Namespace: "test-ns"}
|
|
ctxData := GenerateContextDataFromAppFile(app, wl.Name)
|
|
err := (&Parser{}).ValidateComponentParams(ctxData, wl, app)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "undeclared parameters")
|
|
assert.Contains(t, err.Error(), "extra")
|
|
})
|
|
}
|
|
|
|
// TestValidateCUESchematicAppfile_WorkflowSuppliedParams tests validation with workflow-supplied parameters (issue #7022)
|
|
func TestValidateCUESchematicAppfile_WorkflowSuppliedParams(t *testing.T) {
|
|
prevCue := utilfeature.DefaultMutableFeatureGate.Enabled(features.EnableCueValidation)
|
|
assert.NoError(t, utilfeature.DefaultMutableFeatureGate.Set(string(features.EnableCueValidation)+"=true"))
|
|
t.Cleanup(func() {
|
|
assert.NoError(t, utilfeature.DefaultMutableFeatureGate.Set(
|
|
fmt.Sprintf("%s=%t", string(features.EnableCueValidation), prevCue)))
|
|
})
|
|
|
|
componentTemplate := `
|
|
parameter: {
|
|
image: string
|
|
port: int | *80
|
|
}
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
spec: {
|
|
template: {
|
|
spec: {
|
|
containers: [{
|
|
name: "main"
|
|
image: parameter.image
|
|
ports: [{
|
|
containerPort: parameter.port
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
traitTemplate := `
|
|
parameter: {
|
|
key: string
|
|
value: string
|
|
}
|
|
patch: {
|
|
metadata: {
|
|
labels: {
|
|
(parameter.key): parameter.value
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
t.Run("workflow supplies param - NO traits - should PASS", func(t *testing.T) {
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-webservice",
|
|
Type: "webservice",
|
|
CapabilityCategory: types.CUECategory,
|
|
Params: map[string]any{
|
|
"port": 80,
|
|
},
|
|
FullTemplate: &Template{
|
|
TemplateStr: componentTemplate,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("my-webservice"),
|
|
},
|
|
},
|
|
WorkflowSteps: []wfTypesv1alpha1.WorkflowStep{
|
|
{
|
|
WorkflowStepBase: wfTypesv1alpha1.WorkflowStepBase{
|
|
Name: "apply-microservice",
|
|
Type: "apply-component",
|
|
Inputs: wfTypesv1alpha1.StepInputs{
|
|
{
|
|
From: "dynamicValue",
|
|
ParameterKey: "image",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
assert.NoError(t, err, "Should pass when workflow supplies missing param and NO traits present")
|
|
})
|
|
|
|
t.Run("workflow supplies param - WITH traits - should PASS", func(t *testing.T) {
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-webservice",
|
|
Type: "webservice",
|
|
CapabilityCategory: types.CUECategory,
|
|
Params: map[string]any{
|
|
"port": 80,
|
|
},
|
|
FullTemplate: &Template{
|
|
TemplateStr: componentTemplate,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("my-webservice"),
|
|
Traits: []*Trait{
|
|
{
|
|
Name: "labels",
|
|
CapabilityCategory: types.CUECategory,
|
|
Template: traitTemplate,
|
|
Params: map[string]any{
|
|
"key": "release",
|
|
"value": "stable",
|
|
},
|
|
engine: definition.NewTraitAbstractEngine("labels"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
WorkflowSteps: []wfTypesv1alpha1.WorkflowStep{
|
|
{
|
|
WorkflowStepBase: wfTypesv1alpha1.WorkflowStepBase{
|
|
Name: "apply-microservice",
|
|
Type: "apply-component",
|
|
Inputs: wfTypesv1alpha1.StepInputs{
|
|
{
|
|
From: "dynamicValue",
|
|
ParameterKey: "image",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
assert.NoError(t, err, "Should pass when workflow supplies missing param even WITH traits")
|
|
})
|
|
|
|
t.Run("workflow supplies param with ENUM - should use first enum value", func(t *testing.T) {
|
|
enumComponentTemplate := `
|
|
parameter: {
|
|
image: "nginx:latest" | "apache:latest" | "httpd:latest"
|
|
port: int | *80
|
|
}
|
|
output: {
|
|
apiVersion: "apps/v1"
|
|
kind: "Deployment"
|
|
spec: {
|
|
template: {
|
|
spec: {
|
|
containers: [{
|
|
name: "main"
|
|
image: parameter.image
|
|
ports: [{
|
|
containerPort: parameter.port
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-webservice",
|
|
Type: "webservice",
|
|
CapabilityCategory: types.CUECategory,
|
|
Params: map[string]any{
|
|
"port": 80,
|
|
},
|
|
FullTemplate: &Template{
|
|
TemplateStr: enumComponentTemplate,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("my-webservice"),
|
|
Traits: []*Trait{
|
|
{
|
|
Name: "labels",
|
|
CapabilityCategory: types.CUECategory,
|
|
Template: traitTemplate,
|
|
Params: map[string]any{
|
|
"key": "release",
|
|
"value": "stable",
|
|
},
|
|
engine: definition.NewTraitAbstractEngine("labels"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
WorkflowSteps: []wfTypesv1alpha1.WorkflowStep{
|
|
{
|
|
WorkflowStepBase: wfTypesv1alpha1.WorkflowStepBase{
|
|
Name: "apply-microservice",
|
|
Type: "apply-component",
|
|
Inputs: wfTypesv1alpha1.StepInputs{
|
|
{
|
|
From: "dynamicValue",
|
|
ParameterKey: "image",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
assert.NoError(t, err, "Should use first enum value as default")
|
|
})
|
|
|
|
t.Run("param missing everywhere - should FAIL", func(t *testing.T) {
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-webservice",
|
|
Type: "webservice",
|
|
CapabilityCategory: types.CUECategory,
|
|
Params: map[string]any{
|
|
"port": 80,
|
|
},
|
|
FullTemplate: &Template{
|
|
TemplateStr: componentTemplate,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("my-webservice"),
|
|
Traits: []*Trait{
|
|
{
|
|
Name: "labels",
|
|
CapabilityCategory: types.CUECategory,
|
|
Template: traitTemplate,
|
|
Params: map[string]any{
|
|
"key": "release",
|
|
"value": "stable",
|
|
},
|
|
engine: definition.NewTraitAbstractEngine("labels"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
assert.Error(t, err, "Should fail when param is missing everywhere")
|
|
assert.Contains(t, err.Error(), "missing parameters: image")
|
|
})
|
|
|
|
t.Run("override policy supplies param - WITH traits - should PASS", func(t *testing.T) {
|
|
policyJSON := `{
|
|
"components": [{
|
|
"properties": {
|
|
"image": "nginx:1.20"
|
|
}
|
|
}]
|
|
}`
|
|
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-webservice",
|
|
Type: "webservice",
|
|
CapabilityCategory: types.CUECategory,
|
|
Params: map[string]any{
|
|
"port": 80,
|
|
},
|
|
FullTemplate: &Template{
|
|
TemplateStr: componentTemplate,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("my-webservice"),
|
|
Traits: []*Trait{
|
|
{
|
|
Name: "labels",
|
|
CapabilityCategory: types.CUECategory,
|
|
Template: traitTemplate,
|
|
Params: map[string]any{
|
|
"key": "release",
|
|
"value": "stable",
|
|
},
|
|
engine: definition.NewTraitAbstractEngine("labels"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Policies: []v1beta1.AppPolicy{
|
|
{
|
|
Name: "override-policy",
|
|
Type: "override",
|
|
Properties: &runtime.RawExtension{
|
|
Raw: []byte(policyJSON),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
assert.NoError(t, err, "Should pass when override policy supplies missing param")
|
|
})
|
|
|
|
t.Run("workflow supplies different param types - should use correct defaults", func(t *testing.T) {
|
|
multiTypeTemplate := `
|
|
parameter: {
|
|
count: int
|
|
enabled: bool
|
|
tags: [...string]
|
|
port: int | *80
|
|
}
|
|
output: {
|
|
apiVersion: "v1"
|
|
kind: "ConfigMap"
|
|
data: {
|
|
count: "\(parameter.count)"
|
|
enabled: "\(parameter.enabled)"
|
|
port: "\(parameter.port)"
|
|
}
|
|
metadata: {
|
|
labels: {
|
|
for i, tag in parameter.tags {
|
|
"tag-\(i)": tag
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-config",
|
|
Type: "raw",
|
|
CapabilityCategory: types.CUECategory,
|
|
Params: map[string]any{
|
|
"port": 80,
|
|
},
|
|
FullTemplate: &Template{
|
|
TemplateStr: multiTypeTemplate,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("my-config"),
|
|
Traits: []*Trait{
|
|
{
|
|
Name: "labels",
|
|
CapabilityCategory: types.CUECategory,
|
|
Template: traitTemplate,
|
|
Params: map[string]any{
|
|
"key": "env",
|
|
"value": "test",
|
|
},
|
|
engine: definition.NewTraitAbstractEngine("labels"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
WorkflowSteps: []wfTypesv1alpha1.WorkflowStep{
|
|
{
|
|
WorkflowStepBase: wfTypesv1alpha1.WorkflowStepBase{
|
|
Name: "apply-config",
|
|
Type: "apply-component",
|
|
Inputs: wfTypesv1alpha1.StepInputs{
|
|
{From: "dynamicCount", ParameterKey: "count"},
|
|
{From: "dynamicEnabled", ParameterKey: "enabled"},
|
|
{From: "dynamicTags", ParameterKey: "tags"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
assert.NoError(t, err, "Should handle int, bool, list types with correct defaults")
|
|
})
|
|
|
|
t.Run("workflow supplies param with numeric bounds - should skip validation", func(t *testing.T) {
|
|
// Component with complex validation that can't be easily defaulted
|
|
complexTemplate := `
|
|
parameter: {
|
|
port: int & >1024 & <65535
|
|
image: string
|
|
}
|
|
output: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
spec: {
|
|
ports: [{
|
|
port: parameter.port
|
|
}]
|
|
}
|
|
}
|
|
`
|
|
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-service",
|
|
Type: "service",
|
|
CapabilityCategory: types.CUECategory,
|
|
Params: map[string]any{
|
|
"image": "nginx:latest",
|
|
},
|
|
FullTemplate: &Template{
|
|
TemplateStr: complexTemplate,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("my-service"),
|
|
Traits: []*Trait{
|
|
{
|
|
Name: "labels",
|
|
CapabilityCategory: types.CUECategory,
|
|
Template: traitTemplate,
|
|
Params: map[string]any{
|
|
"key": "version",
|
|
"value": "v1",
|
|
},
|
|
engine: definition.NewTraitAbstractEngine("labels"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
WorkflowSteps: []wfTypesv1alpha1.WorkflowStep{
|
|
{
|
|
WorkflowStepBase: wfTypesv1alpha1.WorkflowStepBase{
|
|
Name: "apply-service",
|
|
Type: "apply-component",
|
|
Inputs: wfTypesv1alpha1.StepInputs{
|
|
{From: "dynamicPort", ParameterKey: "port"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
// Should pass by skipping validation due to complex constraints
|
|
assert.NoError(t, err, "Should skip validation when complex constraints cannot be satisfied")
|
|
})
|
|
|
|
t.Run("workflow param already provided in component - should not augment", func(t *testing.T) {
|
|
appfile := &Appfile{
|
|
Name: "test-app",
|
|
Namespace: "test-ns",
|
|
ParsedComponents: []*Component{
|
|
{
|
|
Name: "my-webservice",
|
|
Type: "webservice",
|
|
CapabilityCategory: types.CUECategory,
|
|
Params: map[string]any{
|
|
"image": "custom-image:v1.0",
|
|
"port": 8080,
|
|
},
|
|
FullTemplate: &Template{
|
|
TemplateStr: componentTemplate,
|
|
},
|
|
engine: definition.NewWorkloadAbstractEngine("my-webservice"),
|
|
Traits: []*Trait{
|
|
{
|
|
Name: "labels",
|
|
CapabilityCategory: types.CUECategory,
|
|
Template: traitTemplate,
|
|
Params: map[string]any{
|
|
"key": "app",
|
|
"value": "myapp",
|
|
},
|
|
engine: definition.NewTraitAbstractEngine("labels"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
WorkflowSteps: []wfTypesv1alpha1.WorkflowStep{
|
|
{
|
|
WorkflowStepBase: wfTypesv1alpha1.WorkflowStepBase{
|
|
Name: "apply-webservice",
|
|
Type: "apply-component",
|
|
Inputs: wfTypesv1alpha1.StepInputs{
|
|
{From: "dynamicImage", ParameterKey: "image"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
err := p.ValidateCUESchematicAppfile(appfile)
|
|
assert.NoError(t, err, "Should use existing param value, not augment from workflow")
|
|
})
|
|
}
|
|
|
|
func TestGetTypeDefault(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
kind cue.Kind
|
|
expected interface{}
|
|
}{
|
|
{"string", cue.StringKind, "__workflow_supplied__"},
|
|
{"float", cue.FloatKind, 0.0},
|
|
{"int", cue.IntKind, 0},
|
|
{"number", cue.NumberKind, 0},
|
|
{"bool", cue.BoolKind, false},
|
|
{"list", cue.ListKind, []interface{}{}},
|
|
{"struct", cue.StructKind, map[string]interface{}{}},
|
|
{"bottom (unknown)", cue.BottomKind, "__workflow_supplied__"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := getTypeDefault(tc.kind)
|
|
assert.Equal(t, tc.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetDefaultForMissingParameter(t *testing.T) {
|
|
var r cue.Runtime
|
|
|
|
t.Run("simple string type should return string default", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { name: string }`)
|
|
assert.NoError(t, err)
|
|
val := inst.Value().LookupPath(cue.ParsePath("parameter.name"))
|
|
canDefault, defaultVal := getDefaultForMissingParameter(val)
|
|
assert.True(t, canDefault)
|
|
assert.Equal(t, "__workflow_supplied__", defaultVal)
|
|
})
|
|
|
|
t.Run("simple int type should return 0", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { count: int }`)
|
|
assert.NoError(t, err)
|
|
val := inst.Value().LookupPath(cue.ParsePath("parameter.count"))
|
|
canDefault, defaultVal := getDefaultForMissingParameter(val)
|
|
assert.True(t, canDefault)
|
|
assert.Equal(t, 0, defaultVal)
|
|
})
|
|
|
|
t.Run("simple bool type should return false", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { enabled: bool }`)
|
|
assert.NoError(t, err)
|
|
val := inst.Value().LookupPath(cue.ParsePath("parameter.enabled"))
|
|
canDefault, defaultVal := getDefaultForMissingParameter(val)
|
|
assert.True(t, canDefault)
|
|
assert.Equal(t, false, defaultVal)
|
|
})
|
|
|
|
t.Run("field with default should use default", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { port: int | *80 }`)
|
|
assert.NoError(t, err)
|
|
val := inst.Value().LookupPath(cue.ParsePath("parameter.port"))
|
|
canDefault, _ := getDefaultForMissingParameter(val)
|
|
assert.True(t, canDefault)
|
|
})
|
|
|
|
t.Run("concrete value should return true with nil default", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { port: 80 }`)
|
|
assert.NoError(t, err)
|
|
val := inst.Value().LookupPath(cue.ParsePath("parameter.port"))
|
|
canDefault, defaultVal := getDefaultForMissingParameter(val)
|
|
assert.True(t, canDefault)
|
|
assert.Nil(t, defaultVal)
|
|
})
|
|
|
|
t.Run("enum disjunction should return first value", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { image: "nginx:latest" | "apache:latest" }`)
|
|
assert.NoError(t, err)
|
|
val := inst.Value().LookupPath(cue.ParsePath("parameter.image"))
|
|
canDefault, defaultVal := getDefaultForMissingParameter(val)
|
|
assert.True(t, canDefault)
|
|
assert.Equal(t, "nginx:latest", defaultVal)
|
|
})
|
|
|
|
t.Run("list type should be defaultable", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { tags: [...string] }`)
|
|
assert.NoError(t, err)
|
|
val := inst.Value().LookupPath(cue.ParsePath("parameter.tags"))
|
|
canDefault, _ := getDefaultForMissingParameter(val)
|
|
assert.True(t, canDefault)
|
|
})
|
|
}
|
|
|
|
func TestGetMapKeys(t *testing.T) {
|
|
t.Run("should return all keys", func(t *testing.T) {
|
|
m := map[string]interface{}{
|
|
"a": 1,
|
|
"b": 2,
|
|
"c": 3,
|
|
}
|
|
keys := getMapKeys(m)
|
|
assert.Len(t, keys, 3)
|
|
assert.ElementsMatch(t, []string{"a", "b", "c"}, keys)
|
|
})
|
|
|
|
t.Run("should return empty slice for empty map", func(t *testing.T) {
|
|
keys := getMapKeys(map[string]interface{}{})
|
|
assert.Empty(t, keys)
|
|
})
|
|
}
|
|
|
|
func TestGetWorkflowAndPolicySuppliedParams(t *testing.T) {
|
|
t.Run("should collect from workflow step inputs", func(t *testing.T) {
|
|
app := &Appfile{
|
|
WorkflowSteps: []wfTypesv1alpha1.WorkflowStep{
|
|
{
|
|
WorkflowStepBase: wfTypesv1alpha1.WorkflowStepBase{
|
|
Name: "step1",
|
|
Type: "apply-component",
|
|
Inputs: wfTypesv1alpha1.StepInputs{
|
|
{From: "dynamic", ParameterKey: "image"},
|
|
{From: "dynamic", ParameterKey: "port"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result := getWorkflowAndPolicySuppliedParams(app)
|
|
assert.True(t, result["image"])
|
|
assert.True(t, result["port"])
|
|
assert.False(t, result["other"])
|
|
})
|
|
|
|
t.Run("should collect from override policies", func(t *testing.T) {
|
|
policyJSON := `{"components": [{"properties": {"image": "nginx:1.20", "replicas": 3}}]}`
|
|
app := &Appfile{
|
|
Policies: []v1beta1.AppPolicy{
|
|
{
|
|
Name: "override",
|
|
Type: "override",
|
|
Properties: &runtime.RawExtension{
|
|
Raw: []byte(policyJSON),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result := getWorkflowAndPolicySuppliedParams(app)
|
|
assert.True(t, result["image"])
|
|
assert.True(t, result["replicas"])
|
|
})
|
|
|
|
t.Run("should skip non-override policies", func(t *testing.T) {
|
|
app := &Appfile{
|
|
Policies: []v1beta1.AppPolicy{
|
|
{
|
|
Name: "topology",
|
|
Type: "topology",
|
|
Properties: &runtime.RawExtension{
|
|
Raw: []byte(`{"clusters": ["local"]}`),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result := getWorkflowAndPolicySuppliedParams(app)
|
|
assert.Empty(t, result)
|
|
})
|
|
|
|
t.Run("should return empty for empty appfile", func(t *testing.T) {
|
|
result := getWorkflowAndPolicySuppliedParams(&Appfile{})
|
|
assert.Empty(t, result)
|
|
})
|
|
|
|
t.Run("should skip malformed policy JSON gracefully", func(t *testing.T) {
|
|
app := &Appfile{
|
|
Policies: []v1beta1.AppPolicy{
|
|
{
|
|
Name: "bad-override",
|
|
Type: "override",
|
|
Properties: &runtime.RawExtension{
|
|
Raw: []byte(`{invalid json`),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result := getWorkflowAndPolicySuppliedParams(app)
|
|
assert.Empty(t, result)
|
|
})
|
|
}
|
|
|
|
func TestGetDeclaredFieldNames(t *testing.T) {
|
|
var r cue.Runtime
|
|
|
|
t.Run("should return all declared field names including optional", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: { name: string, label?: string, count: int }`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
declared := getDeclaredFieldNames(schema)
|
|
assert.True(t, declared["name"])
|
|
assert.True(t, declared["label"])
|
|
assert.True(t, declared["count"])
|
|
assert.False(t, declared["nonexistent"])
|
|
})
|
|
|
|
t.Run("should return empty map for non-struct", func(t *testing.T) {
|
|
inst, err := r.Compile("", `parameter: string`)
|
|
assert.NoError(t, err)
|
|
schema := inst.Value().LookupPath(cue.ParsePath("parameter"))
|
|
declared := getDeclaredFieldNames(schema)
|
|
assert.Empty(t, declared)
|
|
})
|
|
}
|