Fix: Prevent app validation errors when traits are used alongside workflow data passing (#7031)
Some checks failed
Webhook Upgrade Validation / webhook-upgrade-check (push) Failing after 51m50s

Signed-off-by: Brian Kane <briankane1@gmail.com>
This commit is contained in:
Brian Kane
2026-01-23 18:03:09 +00:00
committed by GitHub
parent 37fb2a6f49
commit 7c06ee2060
3 changed files with 807 additions and 14 deletions

View File

@@ -75,12 +75,61 @@ generate_certificates() {
# Generate server private key
openssl genrsa -out ${CERT_DIR}/tls.key 2048
# Get host IP for Docker internal network
# NOTE: 192.168.5.2 is the standard k3d host gateway IP that allows containers to reach the host machine
# Auto-detect host IP for Docker/k3d internal network
# This is only for local k3d development environments - DO NOT use this script in production
# With failurePolicy: Fail, an unreachable webhook can block CRD operations cluster-wide
# Try to detect k3d cluster
K3D_CLUSTER=$(kubectl config current-context | grep -o 'k3d-[^@]*' | sed 's/k3d-//' || echo "")
if [ -n "$K3D_CLUSTER" ]; then
echo "Detected k3d cluster: $K3D_CLUSTER"
# Check if k3d is using host network
NETWORK_MODE=$(docker inspect "k3d-${K3D_CLUSTER}-server-0" 2>/dev/null | grep -o '"NetworkMode": "[^"]*"' | cut -d'"' -f4 || echo "")
if [ "$NETWORK_MODE" = "host" ]; then
# Host network mode - detect OS
if [ "$(uname)" = "Darwin" ]; then
# macOS with Docker Desktop - use host.docker.internal
echo "Detected k3d with --network host on macOS, using host.docker.internal"
HOST_IP="host.docker.internal"
else
# Linux - true host networking works
echo "Detected k3d with --network host, using localhost"
HOST_IP="127.0.0.1"
fi
else
# Bridge network mode - get gateway IP
NETWORK_NAME="k3d-${K3D_CLUSTER}"
HOST_IP=$(docker network inspect "$NETWORK_NAME" -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null || echo "")
if [ -z "$HOST_IP" ]; then
# Fallback to common k3d gateway IPs
echo "Could not detect gateway IP, trying common defaults..."
if docker exec "k3d-${K3D_CLUSTER}-server-0" getent hosts host.k3d.internal 2>/dev/null | awk '{print $1}' | grep -q .; then
HOST_IP=$(docker exec "k3d-${K3D_CLUSTER}-server-0" cat /etc/hosts | grep host.k3d.internal | awk '{print $1}')
else
HOST_IP="172.18.0.1"
fi
fi
echo "Detected k3d with bridge network, using gateway IP: $HOST_IP"
fi
else
# Not k3d, use default
echo "Not using k3d, defaulting to 192.168.5.2"
HOST_IP="192.168.5.2"
fi
# Get local machine IP for SANs (optional, for reference)
if command -v ifconfig &> /dev/null; then
LOCAL_IP=$(ifconfig | grep "inet " | grep -v 127.0.0.1 | head -1 | awk '{print $2}')
elif command -v ip &> /dev/null; then
LOCAL_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v 127.0.0.1 | head -1)
else
LOCAL_IP=""
fi
# Create certificate config with SANs
cat > /tmp/webhook.conf << EOF
@@ -98,11 +147,26 @@ DNS.2 = vela-webhook.${NAMESPACE}.svc
DNS.3 = vela-webhook.${NAMESPACE}.svc.cluster.local
DNS.4 = *.${NAMESPACE}.svc
DNS.5 = *.${NAMESPACE}.svc.cluster.local
DNS.6 = host.k3d.internal
DNS.7 = host.docker.internal
DNS.8 = host.lima.internal
IP.1 = 127.0.0.1
IP.2 = ${HOST_IP}
IP.3 = ${LOCAL_IP}
EOF
# Add HOST_IP - check if it's a hostname or IP
if [[ "$HOST_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
# It's an IP address
echo "IP.2 = ${HOST_IP}" >> /tmp/webhook.conf
else
# It's a hostname - already covered by DNS SANs above
echo "# HOST_IP is hostname: ${HOST_IP} (already in DNS SANs)" >> /tmp/webhook.conf
fi
# Add LOCAL_IP to SANs only if detected and is an IP
if [ -n "$LOCAL_IP" ] && [[ "$LOCAL_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "IP.3 = ${LOCAL_IP}" >> /tmp/webhook.conf
fi
# Generate certificate request
openssl req -new -key ${CERT_DIR}/tls.key -out /tmp/server.csr \
-subj "/CN=vela-webhook.${NAMESPACE}.svc" -config /tmp/webhook.conf
@@ -226,19 +290,38 @@ show_next_steps() {
echo "Webhook debugging setup complete!"
echo "========================================="
echo -e "${NC}"
echo "Configuration:"
echo " - Webhook URL: https://${HOST_IP}:9445"
echo " - Certificate directory: ${CERT_DIR}"
if [ -n "$K3D_CLUSTER" ]; then
echo " - k3d cluster: ${K3D_CLUSTER}"
if [ "$NETWORK_MODE" = "host" ]; then
echo " - Network mode: host (using ${HOST_IP})"
else
echo " - Network mode: bridge (using gateway ${HOST_IP})"
fi
fi
echo ""
echo "Next steps:"
echo "1. Open VS Code"
echo "1. Open your IDE (VS Code, GoLand, etc.)"
echo "2. Set breakpoints in webhook validation code:"
echo " - pkg/webhook/utils/utils.go:141"
echo " - pkg/webhook/core.oam.dev/v1beta1/componentdefinition/validating_handler.go:74"
echo "3. Press F5 and select 'Debug Webhook Validation'"
echo "4. Wait for webhook server to start (port 9445)"
echo " - pkg/webhook/core.oam.dev/v1beta1/application/validating_handler.go:66"
echo " - pkg/webhook/core.oam.dev/v1beta1/componentdefinition/component_definition_validating_handler.go:74"
echo "3. Start debugging cmd/core/main.go with arguments:"
echo " --use-webhook=true"
echo " --webhook-port=9445"
echo " --webhook-cert-dir=${CERT_DIR}"
echo " --leader-elect=false"
echo "4. Wait for webhook server to start"
echo "5. Test with kubectl apply commands"
echo ""
echo -e "${YELLOW}Test command (should be rejected):${NC}"
echo 'kubectl apply -f test/webhook-test-invalid.yaml'
echo -e "${YELLOW}Test command:${NC}"
echo 'kubectl apply -f <your-application.yaml>'
echo ""
echo -e "${GREEN}The webhook will reject ComponentDefinitions with non-existent CRDs${NC}"
echo -e "${GREEN}Your breakpoints will hit when kubectl applies resources!${NC}"
}
# Main execution

View File

@@ -57,7 +57,24 @@ func (p *Parser) ValidateCUESchematicAppfile(a *Appfile) error {
}
}
// Collect workflow-supplied params for this component upfront
workflowParams := getWorkflowAndPolicySuppliedParams(a)
// Only augment if component has traits AND workflow supplies params (issue 7022)
originalParams := wl.Params
if len(wl.Traits) > 0 && len(workflowParams) > 0 {
shouldSkip, augmented := p.augmentComponentParamsForValidation(wl, workflowParams, ctxData)
if shouldSkip {
// Component has complex validation that can't be handled, skip trait validation
fmt.Printf("INFO: Skipping trait validation for component %q due to workflow-supplied parameters with complex validation\n", wl.Name)
continue
}
wl.Params = augmented
}
pCtx, err := newValidationProcessContext(wl, ctxData)
wl.Params = originalParams // Restore immediately
if err != nil {
return errors.WithMessagef(err, "cannot create the validation process context of app=%s in namespace=%s", a.Name, a.Namespace)
}
@@ -337,3 +354,200 @@ func validateAuxiliaryNameUnique() process.AuxiliaryHook {
return nil
})
}
// getWorkflowAndPolicySuppliedParams returns a set of parameter keys that will be
// supplied by workflow steps or override policies at runtime.
func getWorkflowAndPolicySuppliedParams(app *Appfile) map[string]bool {
result := make(map[string]bool)
// Collect from workflow step inputs
for _, step := range app.WorkflowSteps {
for _, in := range step.Inputs {
result[in.ParameterKey] = true
}
}
// Collect from override policies
for _, p := range app.Policies {
if p.Type != "override" {
continue
}
var spec overrideSpec
if err := json.Unmarshal(p.Properties.Raw, &spec); err != nil {
continue // Skip if we can't parse
}
for _, c := range spec.Components {
if len(c.Properties) == 0 {
continue
}
flat, err := flatten.Flatten(c.Properties, "", flatten.DotStyle)
if err != nil {
continue // Skip if we can't flatten
}
for k := range flat {
result[k] = true
}
}
}
return result
}
// getDefaultForMissingParameter checks if a parameter can be defaulted for validation
// and returns an appropriate placeholder value.
func getDefaultForMissingParameter(v cue.Value) (bool, any) {
if v.IsConcrete() {
return true, nil
}
if defaultVal, hasDefault := v.Default(); hasDefault {
return true, defaultVal
}
// Use Expr() to inspect the operation tree for complex validation
op, args := v.Expr()
switch op {
case cue.NoOp, cue.SelectorOp:
// No operation or field selector - simple type
// Use IncompleteKind for non-concrete values to get the correct type
return true, getTypeDefault(v.IncompleteKind())
case cue.AndOp:
// Conjunction (e.g., int & >0 & <100)
if len(args) > 1 {
// Check if any arg is NOT just a basic kind (indicates complex validation)
for _, arg := range args {
if arg.Kind() == cue.BottomKind {
return false, nil
}
}
}
return true, getTypeDefault(v.IncompleteKind())
case cue.OrOp:
// Disjunction (e.g., "value1" | "value2" | "value3") - likely an enum
if len(args) > 0 {
firstVal := args[0]
if firstVal.IsConcrete() {
var result any
if err := firstVal.Decode(&result); err == nil {
return true, result
}
}
}
return false, nil
default:
return false, nil
}
}
// getTypeDefault returns a simple default value based on the CUE Kind.
func getTypeDefault(kind cue.Kind) any {
switch kind {
case cue.StringKind:
return "__workflow_supplied__"
case cue.FloatKind:
return 0.0
case cue.IntKind, cue.NumberKind:
return 0
case cue.BoolKind:
return false
case cue.ListKind:
return []any{}
case cue.StructKind:
return map[string]any{}
default:
return "__workflow_supplied__"
}
}
// augmentComponentParamsForValidation checks if workflow-supplied parameters
// need to be augmented for trait validation. Returns (shouldSkip, augmentedParams).
// If shouldSkip=true, the component has complex validation and should skip trait validation.
// If shouldSkip=false, augmentedParams contains the original params plus simple defaults.
func (p *Parser) augmentComponentParamsForValidation(wl *Component, workflowParams map[string]bool, ctxData velaprocess.ContextData) (bool, map[string]any) {
// Build CUE value to inspect the component's parameter schema
ctx := velaprocess.NewContext(ctxData)
baseCtx, err := ctx.BaseContextFile()
if err != nil {
return false, wl.Params // Can't inspect, proceed normally
}
paramSnippet, err := cueParamBlock(wl.Params)
if err != nil {
return false, wl.Params
}
cueSrc := strings.Join([]string{
renderTemplate(wl.FullTemplate.TemplateStr),
paramSnippet,
baseCtx,
}, "\n")
val, err := cuex.DefaultCompiler.Get().CompileString(ctx.GetCtx(), cueSrc)
if err != nil {
return false, wl.Params // Can't compile, proceed normally
}
// Get the parameter schema
paramVal := val.LookupPath(value.FieldPath(velaprocess.ParameterFieldName))
// Collect default values for workflow-supplied params that are missing
workflowParamDefaults := make(map[string]any)
for paramKey := range workflowParams {
// Skip if already provided
if _, exists := wl.Params[paramKey]; exists {
continue
}
// Check the field in the schema
fieldVal := paramVal.LookupPath(cue.ParsePath(paramKey))
if !fieldVal.Exists() {
continue // Not a parameter field
}
canDefault, defaultVal := getDefaultForMissingParameter(fieldVal)
if !canDefault {
// complex validation - skip
return true, nil
}
if defaultVal != nil {
workflowParamDefaults[paramKey] = defaultVal
}
}
if len(workflowParamDefaults) == 0 {
return false, wl.Params
}
// Create augmented params map
augmented := make(map[string]any)
for k, v := range wl.Params {
augmented[k] = v
}
for k, v := range workflowParamDefaults {
augmented[k] = v
}
fmt.Printf("INFO: Augmented component %q with workflow-supplied defaults for trait validation: %v\n",
wl.Name, getMapKeys(workflowParamDefaults))
return false, augmented
}
// getMapKeys returns the keys from a map as a slice
func getMapKeys(m map[string]any) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}

View File

@@ -20,9 +20,12 @@ import (
"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"
@@ -598,3 +601,496 @@ func TestParser_ValidateCUESchematicAppfile(t *testing.T) {
assert.NoError(t, err)
})
}
// TestValidateCUESchematicAppfile_WorkflowSuppliedParams tests validation with workflow-supplied parameters (issue #7022)
func TestValidateCUESchematicAppfile_WorkflowSuppliedParams(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"))
})
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")
})
}