diff --git a/charts/vela-core/templates/defwithtemplate/affinity.yaml b/charts/vela-core/templates/defwithtemplate/affinity.yaml index 71b49d63f..2f546f522 100644 --- a/charts/vela-core/templates/defwithtemplate/affinity.yaml +++ b/charts/vela-core/templates/defwithtemplate/affinity.yaml @@ -131,20 +131,21 @@ spec: #podAffinityTerm: { labelSelector?: #labelSelector + namespace?: string namespaces?: [...string] topologyKey: string namespaceSelector?: #labelSelector } - #nodeSelecor: { + #nodeSelector: { key: string operator: *"In" | "NotIn" | "Exists" | "DoesNotExist" | "Gt" | "Lt" values?: [...string] } #nodeSelectorTerm: { - matchExpressions?: [...#nodeSelecor] - matchFields?: [...#nodeSelecor] + matchExpressions?: [...#nodeSelector] + matchFields?: [...#nodeSelector] } parameter: { diff --git a/pkg/definition/defkit/cuegen.go b/pkg/definition/defkit/cuegen.go index 039a3ebcb..4fd4b3dd2 100644 --- a/pkg/definition/defkit/cuegen.go +++ b/pkg/definition/defkit/cuegen.go @@ -417,6 +417,20 @@ func (g *CUEGenerator) writeHelperDefFromParam(sb *strings.Builder, param Param, } else { sb.WriteString("[...]\n") } + case *IntParam: + // For int types with optional constraints: int & >=1 & <=65535 + var constraints []string + if minVal := p.GetMin(); minVal != nil { + constraints = append(constraints, fmt.Sprintf(">=%d", *minVal)) + } + if maxVal := p.GetMax(); maxVal != nil { + constraints = append(constraints, fmt.Sprintf("<=%d", *maxVal)) + } + if len(constraints) > 0 { + sb.WriteString(fmt.Sprintf("int & %s\n", strings.Join(constraints, " & "))) + } else { + sb.WriteString("int\n") + } default: // Fallback for other types sb.WriteString("_\n") @@ -459,9 +473,10 @@ func (g *CUEGenerator) writeStructFieldForHelper(sb *strings.Builder, f *StructF return } - // Check for nested struct + // Check for nested struct (or array of structs) if nested := f.GetNested(); nested != nil { if f.FieldType() == ParamTypeArray { + // Array of structs: [...{fields}] sb.WriteString(fmt.Sprintf("%s%s%s: [...{\n", indent, name, optional)) for _, nestedField := range nested.GetFields() { g.writeStructFieldForHelper(sb, nestedField, depth+1) @@ -906,15 +921,17 @@ func (g *CUEGenerator) writeResourceOutput(sb *strings.Builder, name string, res // fieldNode represents a node in the field tree being built. type fieldNode struct { - value Value // Direct value (if leaf) - cond Condition // Condition for this field - children map[string]*fieldNode - childOrder []string // Track insertion order - isArray bool - arrayIndex int - spreads []spreadEntry // Spread operations at this node level - forEach *ForEachOp // ForEach operation (for trait patches) - patchKey *PatchKeyOp // PatchKey operation (for array patches with merge key) + value Value // Direct value (if leaf) + cond Condition // Condition for this field + children map[string]*fieldNode + childOrder []string // Track insertion order + isArray bool + arrayIndex int + spreads []spreadEntry // Spread operations at this node level + forEach *ForEachOp // ForEach operation (for trait patches) + patchKey *PatchKeyOp // PatchKey operation (for array patches with merge key) + spreadAll *SpreadAllOp // SpreadAll operation (for array constraint patches) + patchStrategy string // e.g. "retainKeys" → generates // +patchStrategy=retainKeys } // spreadEntry represents a conditional spread operation. @@ -948,6 +965,11 @@ func (g *CUEGenerator) buildFieldTree(ops []ResourceOp) *fieldNode { case *PatchKeyOp: // PatchKeyOp creates an array patch with merge key annotation g.insertPatchKeyIntoTree(root, o, nil) + case *SpreadAllOp: + // SpreadAllOp constrains all array elements + g.insertSpreadAllIntoTree(root, o, nil) + case *PatchStrategyAnnotationOp: + g.insertAnnotationIntoTree(root, o.Path(), o.Strategy()) case *IfBlock: // For if blocks, process inner ops with the block's condition for _, innerOp := range o.Ops() { @@ -968,6 +990,11 @@ func (g *CUEGenerator) buildFieldTree(ops []ResourceOp) *fieldNode { case *PatchKeyOp: // PatchKey inside an if block - pass the block's condition g.insertPatchKeyIntoTree(root, inner, o.Cond()) + case *SpreadAllOp: + // SpreadAll inside an if block - pass the block's condition + g.insertSpreadAllIntoTree(root, inner, o.Cond()) + case *PatchStrategyAnnotationOp: + g.insertAnnotationIntoTree(root, inner.Path(), inner.Strategy()) } } } @@ -976,6 +1003,20 @@ func (g *CUEGenerator) buildFieldTree(ops []ResourceOp) *fieldNode { return root } +// insertAnnotationIntoTree navigates to a node by path and sets its patchStrategy annotation. +func (g *CUEGenerator) insertAnnotationIntoTree(root *fieldNode, path string, strategy string) { + parts := splitPath(path) + current := root + for _, part := range parts { + if _, exists := current.children[part]; !exists { + current.children[part] = newFieldNode() + current.childOrder = append(current.childOrder, part) + } + current = current.children[part] + } + current.patchStrategy = strategy +} + // insertIntoTree inserts a value at a path into the field tree. func (g *CUEGenerator) insertIntoTree(root *fieldNode, path string, value Value, cond Condition) { parts := splitPath(path) @@ -1159,6 +1200,47 @@ func (g *CUEGenerator) insertPatchKeyIntoTree(root *fieldNode, op *PatchKeyOp, c current.cond = cond } +// insertSpreadAllIntoTree inserts a SpreadAllOp into the field tree. +// This navigates to the target path and sets the spreadAll field. +func (g *CUEGenerator) insertSpreadAllIntoTree(root *fieldNode, op *SpreadAllOp, cond Condition) { + parts := splitPath(op.Path()) + current := root + + for _, part := range parts { + name, key, index := parseBracketAccess(part) + + if _, exists := current.children[name]; !exists { + current.children[name] = newFieldNode() + current.childOrder = append(current.childOrder, name) + } + node := current.children[name] + + switch { + case index >= 0: + node.isArray = true + idxKey := fmt.Sprintf("[%d]", index) + if _, exists := node.children[idxKey]; !exists { + node.children[idxKey] = newFieldNode() + node.children[idxKey].arrayIndex = index + node.childOrder = append(node.childOrder, idxKey) + } + current = node.children[idxKey] + case key != "": + keyNode := fmt.Sprintf("[%s]", key) + if _, exists := node.children[keyNode]; !exists { + node.children[keyNode] = newFieldNode() + node.childOrder = append(node.childOrder, keyNode) + } + current = node.children[keyNode] + default: + current = node + } + } + + current.spreadAll = op + current.cond = cond +} + // writeFieldTree writes the field tree as CUE syntax. func (g *CUEGenerator) writeFieldTree(sb *strings.Builder, node *fieldNode, depth int) { indent := strings.Repeat(g.indent, depth) @@ -1689,8 +1771,14 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string { for _, op := range ops { if mOp, ok := op.(*mapOp); ok { for fieldName, fieldVal := range mOp.mappings { - valStr := g.fieldValueToCUE(fieldVal) - sb.WriteString(fmt.Sprintf("\t\t\t\t\t%s: %s\n", fieldName, valStr)) + if optField, isOptional := fieldVal.(*OptionalField); isOptional { + sb.WriteString(fmt.Sprintf("\t\t\t\t\tif v.%s != _|_ {\n", optField.field)) + sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t%s: v.%s\n", fieldName, optField.field)) + sb.WriteString("\t\t\t\t\t}\n") + } else { + valStr := g.fieldValueToCUE(fieldVal) + sb.WriteString(fmt.Sprintf("\t\t\t\t\t%s: %s\n", fieldName, valStr)) + } } } } @@ -1968,7 +2056,7 @@ func (g *CUEGenerator) concatExprToCUE(ce *ConcatExprValue) string { func (g *CUEGenerator) conditionToCUE(cond Condition) string { switch c := cond.(type) { case *IsSetCondition: - return fmt.Sprintf("parameter.%s != _|_", c.ParamName()) + return fmt.Sprintf("parameter[%q] != _|_", c.ParamName()) case *ParamPathIsSetCondition: // Check if a nested parameter path is set: parameter.path != _|_ return fmt.Sprintf("parameter.%s != _|_", c.Path()) @@ -2012,9 +2100,9 @@ func (g *CUEGenerator) conditionToCUE(cond Condition) string { right := g.conditionToCUE(c.right) return fmt.Sprintf("(%s) || (%s)", left, right) case *NotCondition: - // Special case: Not(IsSet("x")) -> parameter.x == _|_ (cleaner than !(parameter.x != _|_)) + // Special case: Not(IsSet("x")) -> parameter["x"] == _|_ (cleaner than !(parameter["x"] != _|_)) if isSet, ok := c.Inner().(*IsSetCondition); ok { - return fmt.Sprintf("parameter.%s == _|_", isSet.ParamName()) + return fmt.Sprintf("parameter[%q] == _|_", isSet.ParamName()) } inner := g.conditionToCUE(c.Inner()) return fmt.Sprintf("!(%s)", inner) @@ -2181,8 +2269,13 @@ func (g *CUEGenerator) writeParam(sb *strings.Builder, param Param, depth int) { name := param.Name() optional := "?" - if param.IsRequired() || param.HasDefault() { + forceOptional := false + if bp, ok := param.(interface{ IsForceOptional() bool }); ok { + forceOptional = bp.IsForceOptional() + } + if param.IsRequired() || (param.HasDefault() && !forceOptional) { // No ? for required fields or fields with defaults (defaults make them effectively present) + // Unless forceOptional is set, which keeps ? even with a default optional = "" } @@ -2437,11 +2530,8 @@ func (g *CUEGenerator) writeMapParam(sb *strings.Builder, p *MapParam, indent, n } // writeStringKeyMapParam writes a string-to-string map parameter. -func (g *CUEGenerator) writeStringKeyMapParam(sb *strings.Builder, p *StringKeyMapParam, indent, name, optional string) { - // Write description as comment if present - if desc := p.GetDescription(); desc != "" { - sb.WriteString(fmt.Sprintf("%s// +usage=%s\n", indent, desc)) - } +// Note: description is already written by writeParam, so we don't write it here. +func (g *CUEGenerator) writeStringKeyMapParam(sb *strings.Builder, _ *StringKeyMapParam, indent, name, optional string) { sb.WriteString(fmt.Sprintf("%s%s%s: [string]: string\n", indent, name, optional)) } diff --git a/pkg/definition/defkit/cuegen_test.go b/pkg/definition/defkit/cuegen_test.go index b444c08cb..822ae764e 100644 --- a/pkg/definition/defkit/cuegen_test.go +++ b/pkg/definition/defkit/cuegen_test.go @@ -120,6 +120,23 @@ var _ = Describe("CUEGenerator", func() { Expect(cue).To(ContainSubstring("required: string")) Expect(cue).To(ContainSubstring("optional?: string")) }) + + It("should keep ? for ForceOptional parameters even with defaults", func() { + comp := defkit.NewComponent("test"). + Params( + defkit.String("normalDefault").Default("Honor").Enum("Honor", "Ignore"), + defkit.String("optionalDefault").Default("Honor").ForceOptional().Enum("Honor", "Ignore"), + ) + + cue := gen.GenerateParameterSchema(comp) + + // Normal default: no ? (field is always present) + Expect(cue).To(ContainSubstring(`normalDefault: *"Honor" | "Ignore"`)) + Expect(cue).NotTo(ContainSubstring(`normalDefault?:`)) + + // ForceOptional with default: has ? (field can be absent, defaults when present) + Expect(cue).To(ContainSubstring(`optionalDefault?: *"Honor" | "Ignore"`)) + }) }) Describe("GenerateParameterSchema with complex types", func() { diff --git a/pkg/definition/defkit/expr.go b/pkg/definition/defkit/expr.go index 02390595f..d485141e6 100644 --- a/pkg/definition/defkit/expr.go +++ b/pkg/definition/defkit/expr.go @@ -445,6 +445,22 @@ func (p *PatchKeyOp) Key() string { return p.key } // Elements returns the array elements to patch. func (p *PatchKeyOp) Elements() []Value { return p.elements } +// SpreadAllOp represents a patch operation that constrains all array elements. +// This generates: path: [...{element}] +// Used for applying the same patch to every element in an array (e.g., all containers). +type SpreadAllOp struct { + path string + elements []Value +} + +func (s *SpreadAllOp) resourceOp() {} + +// Path returns the path being patched. +func (s *SpreadAllOp) Path() string { return s.path } + +// Elements returns the array elements to constrain. +func (s *SpreadAllOp) Elements() []Value { return s.elements } + // --- Context Path Exists Check --- // PathExistsCondition checks if a path exists in CUE (path != _|_). diff --git a/pkg/definition/defkit/helper_definition_test.go b/pkg/definition/defkit/helper_definition_test.go index 7035b7cdc..944d6d849 100644 --- a/pkg/definition/defkit/helper_definition_test.go +++ b/pkg/definition/defkit/helper_definition_test.go @@ -96,4 +96,98 @@ var _ = Describe("HelperDefinition", func() { Expect(helpers).To(BeEmpty()) }) }) + + Context("Map-based helper with StringKeyMap and typed arrays", func() { + It("should render StringKeyMap as [string]: string in helper definition", func() { + helper := defkit.Map("labelSelector").WithFields( + defkit.StringKeyMap("matchLabels").Description("A map of {key,value} pairs"), + defkit.Array("matchExpressions").Description("Label selector requirements").WithFields( + defkit.String("key").Required(), + defkit.String("operator").Default("In").Enum("In", "NotIn", "Exists", "DoesNotExist"), + defkit.Array("values").Of(defkit.ParamTypeString), + ), + ) + + trait := defkit.NewTrait("helper-test"). + Description("Test helper rendering"). + AppliesTo("deployments.apps"). + Helper("labelSelector", helper). + Template(func(tpl *defkit.Template) { + tpl.Patch().Set("spec.selector", defkit.Lit("test")) + }) + + cue := trait.ToCue() + + // StringKeyMap renders as [string]: string + Expect(cue).To(ContainSubstring("matchLabels?: [string]: string")) + // Should NOT have duplicate description comments + Expect(cue).NotTo(ContainSubstring("// +usage=A map of {key,value} pairs\n\t// +usage=A map of {key,value} pairs")) + // Array with fields renders as [...{...}] + Expect(cue).To(ContainSubstring("matchExpressions?:")) + Expect(cue).To(ContainSubstring("[...{")) + // Typed array renders as [...string] + Expect(cue).To(ContainSubstring("values?: [...string]")) + // Enum with default renders correctly + Expect(cue).To(ContainSubstring(`*"In"`)) + }) + + It("should render ArrayOf(ParamTypeString) in Struct-based helper fields", func() { + helper := defkit.Struct("nodeSelector").Fields( + defkit.Field("key", defkit.ParamTypeString).Required(), + defkit.Field("operator", defkit.ParamTypeString).Default("In").Enum("In", "NotIn"), + defkit.Field("values", defkit.ParamTypeArray).ArrayOf(defkit.ParamTypeString), + ) + + trait := defkit.NewTrait("typed-array-test"). + Description("Test typed arrays in helpers"). + AppliesTo("deployments.apps"). + Helper("nodeSelector", helper). + Template(func(tpl *defkit.Template) { + tpl.Patch().Set("spec.selector", defkit.Lit("test")) + }) + + cue := trait.ToCue() + + Expect(cue).To(ContainSubstring("#nodeSelector")) + // key is required (no ?) - CUE formatter may add tab alignment + Expect(cue).To(MatchRegexp(`key:\s+string`)) + Expect(cue).To(ContainSubstring("values?: [...string]")) + // Untyped array should NOT appear + Expect(cue).NotTo(MatchRegexp(`values\?\: \[\.\.\.\]\s*\n`)) + }) + + It("should render schemaRef with ArrayOf correctly in Struct helper", func() { + selectorHelper := defkit.Struct("nodeSelectorTerm").Fields( + defkit.Field("matchExpressions", defkit.ParamTypeArray).WithSchemaRef("nodeSelector"), + defkit.Field("matchFields", defkit.ParamTypeArray).WithSchemaRef("nodeSelector"), + ) + + affinityHelper := defkit.Struct("podAffinityTerm").Fields( + defkit.Field("labelSelector", defkit.ParamTypeStruct).WithSchemaRef("labelSelector"), + defkit.Field("namespaces", defkit.ParamTypeArray).ArrayOf(defkit.ParamTypeString), + defkit.Field("topologyKey", defkit.ParamTypeString).Required(), + ) + + trait := defkit.NewTrait("schemaref-test"). + Description("Test schemaRef in helpers"). + AppliesTo("deployments.apps"). + Helper("nodeSelectorTerm", selectorHelper). + Helper("podAffinityTerm", affinityHelper). + Template(func(tpl *defkit.Template) { + tpl.Patch().Set("spec.selector", defkit.Lit("test")) + }) + + cue := trait.ToCue() + + // SchemaRef on array field renders as [...#name] + Expect(cue).To(ContainSubstring("matchExpressions?: [...#nodeSelector]")) + Expect(cue).To(ContainSubstring("matchFields?: [...#nodeSelector]")) + // SchemaRef on struct field renders as #name + Expect(cue).To(ContainSubstring("labelSelector?: #labelSelector")) + // ArrayOf(ParamTypeString) renders as [...string] + Expect(cue).To(ContainSubstring("namespaces?: [...string]")) + // Required field has no ? + Expect(cue).To(ContainSubstring("topologyKey: string")) + }) + }) }) diff --git a/pkg/definition/defkit/param.go b/pkg/definition/defkit/param.go index fdf10fda7..9fad940d3 100644 --- a/pkg/definition/defkit/param.go +++ b/pkg/definition/defkit/param.go @@ -18,11 +18,12 @@ package defkit // baseParam provides common parameter functionality. type baseParam struct { - name string - paramType ParamType - required bool - defaultValue any - description string + name string + paramType ParamType + required bool + defaultValue any + description string + forceOptional bool // when true, field stays optional even with a default value } func (p *baseParam) expr() {} @@ -35,6 +36,7 @@ func (p *baseParam) IsOptional() bool { return !p.required } func (p *baseParam) HasDefault() bool { return p.defaultValue != nil } func (p *baseParam) GetDefault() any { return p.defaultValue } func (p *baseParam) GetDescription() string { return p.description } +func (p *baseParam) IsForceOptional() bool { return p.forceOptional } // IsSet returns a condition that checks if the parameter has a value. // This is used with SetIf for conditional field assignment. @@ -132,6 +134,14 @@ func (p *StringParam) Optional() *StringParam { return p } +// ForceOptional makes the field optional even when it has a default value. +// Normally, fields with defaults are treated as always-present (no ? in CUE). +// This generates field?: *default | type instead of field: *default | type. +func (p *StringParam) ForceOptional() *StringParam { + p.forceOptional = true + return p +} + // Default sets a default value for the parameter. func (p *StringParam) Default(value string) *StringParam { p.defaultValue = value @@ -837,6 +847,8 @@ func (f *StructField) Description(desc string) *StructField { } // Nested sets a nested struct definition for this field. +// For ParamTypeStruct fields, this defines the struct's shape. +// For ParamTypeArray fields, this defines the array element struct shape (generates [...{fields}]). func (f *StructField) Nested(s *StructParam) *StructField { f.nested = s if f.fieldType == ParamTypeArray { diff --git a/pkg/definition/defkit/param_test.go b/pkg/definition/defkit/param_test.go index 90f7402ea..4ed706e8f 100644 --- a/pkg/definition/defkit/param_test.go +++ b/pkg/definition/defkit/param_test.go @@ -61,6 +61,19 @@ var _ = Describe("Parameters", func() { Expect(p.GetDefault()).To(Equal("nginx:latest")) Expect(p.GetDescription()).To(Equal("Container image")) }) + + It("should support ForceOptional to stay optional even with a default", func() { + p := defkit.String("policy").Default("Honor").ForceOptional().Enum("Honor", "Ignore") + Expect(p.HasDefault()).To(BeTrue()) + Expect(p.GetDefault()).To(Equal("Honor")) + Expect(p.IsForceOptional()).To(BeTrue()) + Expect(p.IsRequired()).To(BeFalse()) + }) + + It("should not be force-optional by default", func() { + p := defkit.String("policy").Default("Honor") + Expect(p.IsForceOptional()).To(BeFalse()) + }) }) Context("IntParam", func() { diff --git a/pkg/definition/defkit/patch.go b/pkg/definition/defkit/patch.go index 9ad636a34..b07fad755 100644 --- a/pkg/definition/defkit/patch.go +++ b/pkg/definition/defkit/patch.go @@ -122,6 +122,42 @@ func (p *PatchResource) PatchKey(path string, key string, elements ...Value) *Pa return p } +// SpreadAll adds a spread constraint that applies to all array elements. +// This generates: path: [...{element1}, ...{element2}] +// Used for applying the same patch to every element in an array. +// +// Example: +// +// lifecycleObj := defkit.NewArrayElement(). +// SetIf(postStart.IsSet(), "lifecycle.postStart", postStart) +// tpl.Patch().SpreadAll("spec.template.spec.containers", lifecycleObj) +// // Generates: containers: [...{lifecycle: { if ... { postStart: ... } }}] +func (p *PatchResource) SpreadAll(path string, elements ...Value) *PatchResource { + op := &SpreadAllOp{path: path, elements: elements} + if p.currentIf != nil { + p.currentIf.ops = append(p.currentIf.ops, op) + } else { + p.ops = append(p.ops, op) + } + return p +} + +// PatchStrategyAnnotation annotates a specific field path with // +patchStrategy=strategy. +// This generates a CUE comment annotation before the field. +// Example: p.PatchStrategyAnnotation("spec.strategy", "retainKeys") +// Generates: // +patchStrategy=retainKeys +// +// strategy: { ... } +func (p *PatchResource) PatchStrategyAnnotation(path string, strategy string) *PatchResource { + op := &PatchStrategyAnnotationOp{path: path, strategy: strategy} + if p.currentIf != nil { + p.currentIf.ops = append(p.currentIf.ops, op) + } else { + p.ops = append(p.ops, op) + } + return p +} + // Ops returns all recorded operations. func (p *PatchResource) Ops() []ResourceOp { return p.ops } @@ -138,6 +174,20 @@ type PassthroughOp struct{} func (p *PassthroughOp) resourceOp() {} +// PatchStrategyAnnotationOp records a patchStrategy annotation on a field path. +type PatchStrategyAnnotationOp struct { + path string + strategy string +} + +func (p *PatchStrategyAnnotationOp) resourceOp() {} + +// Path returns the path being annotated. +func (p *PatchStrategyAnnotationOp) Path() string { return p.path } + +// Strategy returns the patch strategy value. +func (p *PatchStrategyAnnotationOp) Strategy() string { return p.strategy } + // ForEachOp represents a for-each spread operation in a patch. // This generates CUE like: for k, v in source { (k): v } type ForEachOp struct { diff --git a/pkg/definition/defkit/patch_container.go b/pkg/definition/defkit/patch_container.go index 16e52f80d..053512666 100644 --- a/pkg/definition/defkit/patch_container.go +++ b/pkg/definition/defkit/patch_container.go @@ -22,6 +22,172 @@ package defkit // - ListComprehension: for CUE list comprehensions with conditional fields // - ParamIsSet/ParamNotSet: convenience constructors for parameter existence conditions +// PatchFieldBuilder provides a fluent API for constructing PatchContainerField values. +// Use PatchField() to start building. +// +// Example: +// +// defkit.PatchField("exec").IsSet().Build() +// defkit.PatchField("initialDelaySeconds").Int().IsSet().Default("0").Build() +// defkit.PatchField("image").Strategy("retainKeys").Description("Specify the image").Build() +type PatchFieldBuilder struct { + paramName string + targetField string + patchStrategy string + condition string + paramType string + paramDefault string + description string +} + +// PatchField starts building a PatchContainerField with the given parameter name. +// The TargetField defaults to the same as the parameter name. +func PatchField(name string) *PatchFieldBuilder { + return &PatchFieldBuilder{ + paramName: name, + targetField: name, + } +} + +// Target sets the container field to patch, if different from the parameter name. +func (b *PatchFieldBuilder) Target(t string) *PatchFieldBuilder { + b.targetField = t + return b +} + +// Default sets an explicit default value for the parameter. +func (b *PatchFieldBuilder) Default(val string) *PatchFieldBuilder { + b.paramDefault = val + return b +} + +// Type sets an explicit CUE type string (e.g., "string", "[...string]", "{...}"). +func (b *PatchFieldBuilder) Type(t string) *PatchFieldBuilder { + b.paramType = t + return b +} + +// Int is shorthand for Type("int"). +func (b *PatchFieldBuilder) Int() *PatchFieldBuilder { + return b.Type("int") +} + +// Bool is shorthand for Type("bool"). +func (b *PatchFieldBuilder) Bool() *PatchFieldBuilder { + return b.Type("bool") +} + +// Str is shorthand for Type("string"). +func (b *PatchFieldBuilder) Str() *PatchFieldBuilder { + return b.Type("string") +} + +// StringArray is shorthand for Type("[...string]"). +func (b *PatchFieldBuilder) StringArray() *PatchFieldBuilder { + return b.Type("[...string]") +} + +// Strategy sets the patch strategy (e.g., "replace", "retainKeys"). +func (b *PatchFieldBuilder) Strategy(s string) *PatchFieldBuilder { + b.patchStrategy = s + return b +} + +// --- Condition methods (following param.go / health_expr.go patterns) --- + +// IsSet guards the field with an existence check (CUE: != _|_). +// Use this for optional fields that should only be patched when provided. +func (b *PatchFieldBuilder) IsSet() *PatchFieldBuilder { + b.condition = "!= _|_" + return b +} + +// NotEmpty guards the field with a non-empty string check (CUE: != ""). +// Use this for string fields that should only be patched when non-empty. +func (b *PatchFieldBuilder) NotEmpty() *PatchFieldBuilder { + b.condition = `!= ""` + return b +} + +// Eq sets a condition that checks the field equals the given value. +func (b *PatchFieldBuilder) Eq(val string) *PatchFieldBuilder { + b.condition = "== " + val + return b +} + +// Ne sets a condition that checks the field is not equal to the given value. +func (b *PatchFieldBuilder) Ne(val string) *PatchFieldBuilder { + b.condition = "!= " + val + return b +} + +// Gt sets a condition that checks the field is greater than the given value. +func (b *PatchFieldBuilder) Gt(val string) *PatchFieldBuilder { + b.condition = "> " + val + return b +} + +// Gte sets a condition that checks the field is greater than or equal to the given value. +func (b *PatchFieldBuilder) Gte(val string) *PatchFieldBuilder { + b.condition = ">= " + val + return b +} + +// Lt sets a condition that checks the field is less than the given value. +func (b *PatchFieldBuilder) Lt(val string) *PatchFieldBuilder { + b.condition = "< " + val + return b +} + +// Lte sets a condition that checks the field is less than or equal to the given value. +func (b *PatchFieldBuilder) Lte(val string) *PatchFieldBuilder { + b.condition = "<= " + val + return b +} + +// RawCondition sets a raw CUE condition string. +// Use this as an escape hatch for non-standard conditions not covered by the typed API. +func (b *PatchFieldBuilder) RawCondition(c string) *PatchFieldBuilder { + b.condition = c + return b +} + +// Description sets the +usage description for this field. +func (b *PatchFieldBuilder) Description(d string) *PatchFieldBuilder { + b.description = d + return b +} + +// Build returns the constructed PatchContainerField. +func (b *PatchFieldBuilder) Build() PatchContainerField { + return PatchContainerField{ + ParamName: b.paramName, + TargetField: b.targetField, + PatchStrategy: b.patchStrategy, + Condition: b.condition, + ParamType: b.paramType, + ParamDefault: b.paramDefault, + Description: b.description, + } +} + +// PatchFields builds a slice of PatchContainerField from builders. +// This eliminates the need to call .Build() on each field individually. +// +// Example: +// +// Fields: defkit.PatchFields( +// defkit.PatchField("exec").IsSet(), +// defkit.PatchField("initialDelaySeconds").Int().IsSet().Default("0"), +// ) +func PatchFields(builders ...*PatchFieldBuilder) []PatchContainerField { + fields := make([]PatchContainerField, len(builders)) + for i, b := range builders { + fields[i] = b.Build() + } + return fields +} + // PatchContainerField defines a field to be patched in the container. type PatchContainerField struct { ParamName string // the parameter name (e.g., "command", "args") @@ -30,6 +196,7 @@ type PatchContainerField struct { Condition string // optional CUE condition (e.g., "!= null") ParamType string // explicit CUE type (e.g., "string", "[...string]", "{...}") ParamDefault string // explicit default value (e.g., "0", "\"\"", "false") + Description string // optional +usage description (auto-generated if empty) } // PatchContainerGroup defines a group of fields under a common parent field. @@ -53,11 +220,13 @@ type PatchContainerConfig struct { Groups []PatchContainerGroup // grouped fields (e.g., startupProbe: { ... }) AllowMultiple bool // if true, allow patching multiple containers ContainersParam string // for multi-container mode, the array param name + ContainersDescription string // +usage description for the containers param (auto-generated if empty) CustomParamsBlock string // custom CUE block for #PatchParams (for complex types) MultiContainerParam string // alternate name for multi-container param (default: "probes" for probes, "containers" for others) CustomPatchContainerBlock string // custom CUE block for PatchContainer body (for complex merge logic) CustomPatchBlock string // custom CUE block for the patch: spec: template: spec: { ... } body CustomParameterBlock string // custom CUE block for the parameter definition + PatchStrategy string // if set, emitted as // +patchStrategy= before the patch block (e.g., "open") } // --- Let Binding Support --- diff --git a/pkg/definition/defkit/patch_container_test.go b/pkg/definition/defkit/patch_container_test.go index 4a5fdd329..b063856da 100644 --- a/pkg/definition/defkit/patch_container_test.go +++ b/pkg/definition/defkit/patch_container_test.go @@ -17,6 +17,8 @@ limitations under the License. package defkit_test import ( + "strings" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -25,6 +27,359 @@ import ( var _ = Describe("PatchContainer", func() { + Context("PatchFieldBuilder", func() { + It("should set ParamName and default TargetField to the same name", func() { + f := defkit.PatchField("exec").Build() + Expect(f.ParamName).To(Equal("exec")) + Expect(f.TargetField).To(Equal("exec")) + Expect(f.Condition).To(BeEmpty()) + Expect(f.ParamType).To(BeEmpty()) + Expect(f.ParamDefault).To(BeEmpty()) + Expect(f.PatchStrategy).To(BeEmpty()) + Expect(f.Description).To(BeEmpty()) + }) + + It("should override TargetField with Target()", func() { + f := defkit.PatchField("addCapabilities").Target("add").Build() + Expect(f.ParamName).To(Equal("addCapabilities")) + Expect(f.TargetField).To(Equal("add")) + }) + + It("should set Condition via IsSet()", func() { + f := defkit.PatchField("exec").IsSet().Build() + Expect(f.Condition).To(Equal("!= _|_")) + }) + + It("should set ParamDefault via Default()", func() { + f := defkit.PatchField("initialDelaySeconds").Default("0").Build() + Expect(f.ParamDefault).To(Equal("0")) + }) + + It("should set ParamType via Int(), Bool(), Str(), StringArray()", func() { + Expect(defkit.PatchField("x").Int().Build().ParamType).To(Equal("int")) + Expect(defkit.PatchField("x").Bool().Build().ParamType).To(Equal("bool")) + Expect(defkit.PatchField("x").Str().Build().ParamType).To(Equal("string")) + Expect(defkit.PatchField("x").StringArray().Build().ParamType).To(Equal("[...string]")) + }) + + It("should set PatchStrategy via Strategy()", func() { + f := defkit.PatchField("image").Strategy("retainKeys").Build() + Expect(f.PatchStrategy).To(Equal("retainKeys")) + }) + + It("should set Condition via NotEmpty()", func() { + f := defkit.PatchField("imagePullPolicy").NotEmpty().Build() + Expect(f.Condition).To(Equal(`!= ""`)) + }) + + It("should set Condition via comparison methods", func() { + Expect(defkit.PatchField("x").Gt("0").Build().Condition).To(Equal("> 0")) + Expect(defkit.PatchField("x").Gte("1").Build().Condition).To(Equal(">= 1")) + Expect(defkit.PatchField("x").Lt("100").Build().Condition).To(Equal("< 100")) + Expect(defkit.PatchField("x").Lte("99").Build().Condition).To(Equal("<= 99")) + Expect(defkit.PatchField("x").Eq("42").Build().Condition).To(Equal("== 42")) + Expect(defkit.PatchField("x").Ne("0").Build().Condition).To(Equal("!= 0")) + }) + + It("should set Condition via RawCondition escape hatch", func() { + f := defkit.PatchField("x").RawCondition(`!= "custom"`).Build() + Expect(f.Condition).To(Equal(`!= "custom"`)) + }) + + It("should set Description via Description()", func() { + f := defkit.PatchField("image").Description("Specify the image").Build() + Expect(f.Description).To(Equal("Specify the image")) + }) + + It("should chain all methods together", func() { + f := defkit.PatchField("initialDelaySeconds"). + Int(). + IsSet(). + Default("0"). + Description("Seconds before probe starts"). + Build() + Expect(f.ParamName).To(Equal("initialDelaySeconds")) + Expect(f.TargetField).To(Equal("initialDelaySeconds")) + Expect(f.ParamType).To(Equal("int")) + Expect(f.Condition).To(Equal("!= _|_")) + Expect(f.ParamDefault).To(Equal("0")) + Expect(f.Description).To(Equal("Seconds before probe starts")) + }) + + It("should produce struct equivalent to manual construction", func() { + builder := defkit.PatchField("addCapabilities"). + Target("add"). + StringArray(). + IsSet(). + Description("Specify the addCapabilities of the container"). + Build() + + manual := defkit.PatchContainerField{ + ParamName: "addCapabilities", + TargetField: "add", + ParamType: "[...string]", + Condition: "!= _|_", + Description: "Specify the addCapabilities of the container", + } + + Expect(builder).To(Equal(manual)) + }) + + It("PatchFields should batch-build without .Build()", func() { + fields := defkit.PatchFields( + defkit.PatchField("exec").IsSet(), + defkit.PatchField("delay").Int().IsSet().Default("0"), + ) + Expect(fields).To(HaveLen(2)) + Expect(fields[0].ParamName).To(Equal("exec")) + Expect(fields[0].Condition).To(Equal("!= _|_")) + Expect(fields[1].ParamName).To(Equal("delay")) + Expect(fields[1].ParamType).To(Equal("int")) + Expect(fields[1].ParamDefault).To(Equal("0")) + }) + + It("PatchFields should return empty slice for zero builders", func() { + fields := defkit.PatchFields() + Expect(fields).To(HaveLen(0)) + Expect(fields).NotTo(BeNil()) + }) + + It("should set ParamType via Type() for custom CUE types", func() { + f := defkit.PatchField("metadata").Type("{...}").Build() + Expect(f.ParamType).To(Equal("{...}")) + }) + + It("last condition method call should win", func() { + f := defkit.PatchField("x").IsSet().NotEmpty().Build() + Expect(f.Condition).To(Equal(`!= ""`)) + + f2 := defkit.PatchField("x").NotEmpty().IsSet().Build() + Expect(f2.Condition).To(Equal("!= _|_")) + }) + }) + + Context("CUE generation with PatchFieldBuilder", func() { + It("should produce identical CUE from builder and manual struct construction", func() { + // Build the same trait using builder API + builderTrait := defkit.NewTrait("builder-cue-test"). + Description("Test builder produces same CUE as manual"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + Groups: []defkit.PatchContainerGroup{ + { + TargetField: "securityContext", + Fields: defkit.PatchFields( + defkit.PatchField("privileged").Bool().Default("false"), + defkit.PatchField("runAsUser").Int().IsSet(), + ), + SubGroups: []defkit.PatchContainerGroup{ + { + TargetField: "capabilities", + Fields: defkit.PatchFields( + defkit.PatchField("addCapabilities").Target("add").StringArray().IsSet(), + ), + }, + }, + }, + }, + }) + }) + + // Build the same trait using manual struct construction + manualTrait := defkit.NewTrait("builder-cue-test"). + Description("Test builder produces same CUE as manual"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + Groups: []defkit.PatchContainerGroup{ + { + TargetField: "securityContext", + Fields: []defkit.PatchContainerField{ + {ParamName: "privileged", TargetField: "privileged", ParamType: "bool", ParamDefault: "false"}, + {ParamName: "runAsUser", TargetField: "runAsUser", ParamType: "int", Condition: "!= _|_"}, + }, + SubGroups: []defkit.PatchContainerGroup{ + { + TargetField: "capabilities", + Fields: []defkit.PatchContainerField{ + {ParamName: "addCapabilities", TargetField: "add", ParamType: "[...string]", Condition: "!= _|_"}, + }, + }, + }, + }, + }, + }) + }) + + Expect(builderTrait.ToCue()).To(Equal(manualTrait.ToCue())) + }) + + It("should generate correct CUE for PatchFields with Strategy and NotEmpty", func() { + trait := defkit.NewTrait("builder-strategy-test"). + Description("Test Strategy and NotEmpty via builder"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + PatchFields: defkit.PatchFields( + defkit.PatchField("image").Strategy("retainKeys"), + defkit.PatchField("imagePullPolicy").Strategy("retainKeys").NotEmpty(), + ), + }) + }) + + cue := trait.ToCue() + + // Strategy should produce patchStrategy comments + Expect(cue).To(ContainSubstring(`// +patchStrategy=retainKeys`)) + // NotEmpty() should produce conditional block in PatchContainer body + Expect(cue).To(ContainSubstring(`if _params.imagePullPolicy != ""`)) + // Unconditional field should be assigned directly + Expect(cue).To(ContainSubstring(`image: _params.image`)) + }) + + It("should generate correct CUE for IsSet fields in groups", func() { + trait := defkit.NewTrait("builder-isset-group-test"). + Description("Test IsSet in groups via builder"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + Groups: []defkit.PatchContainerGroup{ + { + TargetField: "startupProbe", + Fields: defkit.PatchFields( + defkit.PatchField("exec").Str().IsSet(), + defkit.PatchField("initialDelaySeconds").Int().IsSet().Default("0"), + defkit.PatchField("terminationGracePeriodSeconds").Int().IsSet(), + ), + }, + }, + }) + }) + + cue := trait.ToCue() + + // Str().IsSet() → optional string param and conditional in PatchContainer body + Expect(cue).To(ContainSubstring(`exec?: string`)) + Expect(cue).To(ContainSubstring(`if _params.exec != _|_`)) + + // Int().IsSet() → optional int in param schema and conditional in body + Expect(cue).To(ContainSubstring(`terminationGracePeriodSeconds?: int`)) + Expect(cue).To(ContainSubstring(`if _params.terminationGracePeriodSeconds != _|_`)) + + // Int().IsSet().Default("0") → default in param schema but still conditional in body + Expect(cue).To(ContainSubstring(`initialDelaySeconds: *0 | int`)) + Expect(cue).To(ContainSubstring(`if _params.initialDelaySeconds != _|_`)) + + // startupProbe group wrapper + Expect(cue).To(ContainSubstring(`startupProbe: {`)) + }) + + It("should generate correct CUE for Default without IsSet (unconditional)", func() { + trait := defkit.NewTrait("builder-default-only-test"). + Description("Test Default without IsSet via builder"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + Groups: []defkit.PatchContainerGroup{ + { + TargetField: "securityContext", + Fields: defkit.PatchFields( + defkit.PatchField("privileged").Bool().Default("false"), + defkit.PatchField("runAsUser").Int().IsSet(), + ), + }, + }, + }) + }) + + cue := trait.ToCue() + + // Bool().Default("false") → default in param, unconditional in PatchContainer body + Expect(cue).To(ContainSubstring(`privileged: *false | bool`)) + Expect(cue).To(ContainSubstring(`privileged: _params.privileged`)) + Expect(cue).NotTo(ContainSubstring(`if _params.privileged`)) + + // Int().IsSet() → optional param, conditional in PatchContainer body + Expect(cue).To(ContainSubstring(`runAsUser?: int`)) + Expect(cue).To(ContainSubstring(`if _params.runAsUser != _|_`)) + }) + + It("should generate correct CUE for Target remapping in subgroups", func() { + trait := defkit.NewTrait("builder-target-test"). + Description("Test Target remapping via builder"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + Groups: []defkit.PatchContainerGroup{ + { + TargetField: "securityContext", + Fields: defkit.PatchFields( + defkit.PatchField("privileged").Bool().Default("false"), + ), + SubGroups: []defkit.PatchContainerGroup{ + { + TargetField: "capabilities", + Fields: defkit.PatchFields( + defkit.PatchField("addCapabilities").Target("add").StringArray().IsSet(), + defkit.PatchField("dropCapabilities").Target("drop").StringArray().IsSet(), + ), + }, + }, + }, + }, + }) + }) + + cue := trait.ToCue() + + // Target("add") remaps param name to different container field + Expect(cue).To(ContainSubstring(`addCapabilities?: [...string]`)) + Expect(cue).To(ContainSubstring(`add: _params.addCapabilities`)) + + // Target("drop") remaps param name to different container field + Expect(cue).To(ContainSubstring(`dropCapabilities?: [...string]`)) + Expect(cue).To(ContainSubstring(`drop: _params.dropCapabilities`)) + + // Nested group structure + Expect(cue).To(ContainSubstring(`securityContext: {`)) + Expect(cue).To(ContainSubstring(`capabilities: {`)) + }) + + It("should generate correct CUE for Description on builder fields", func() { + trait := defkit.NewTrait("builder-desc-test"). + Description("Test Description via builder"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + PatchFields: defkit.PatchFields( + defkit.PatchField("image").Strategy("retainKeys").Description("Specify the image of the container"), + defkit.PatchField("imagePullPolicy").Strategy("retainKeys").NotEmpty().Description("Specify the image pull policy"), + ), + }) + }) + + cue := trait.ToCue() + + Expect(cue).To(ContainSubstring("// +usage=Specify the image of the container")) + Expect(cue).To(ContainSubstring("// +usage=Specify the image pull policy")) + }) + }) + Context("Trait with PatchContainer CUE Generation", func() { It("should generate complete PatchContainer trait CUE structure", func() { containerName := defkit.String("containerName").Default("") @@ -90,10 +445,186 @@ var _ = Describe("PatchContainer", func() { // Verify error collection Expect(cue).To(ContainSubstring(`errs: [for c in patch.spec.template.spec.containers if c.err != _|_ {c.err}]`)) - // Verify parameter block with optional fields - Expect(cue).To(ContainSubstring(`parameter: {`)) - Expect(cue).To(ContainSubstring(`command?: [...string]`)) - Expect(cue).To(ContainSubstring(`args?: [...string]`)) + // Verify parameter block comes from PatchContainer (no duplicate from regular params) + Expect(cue).To(ContainSubstring(`parameter: #PatchParams`)) + // The extra parameter: {} from regular params should NOT appear + Expect(strings.Count(cue, "parameter:")).To(Equal(1)) + }) + + It("should use optional field syntax for non-string conditions like != _|_", func() { + trait := defkit.NewTrait("optional-field-test"). + Description("Test optional fields for non-string conditions"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + Groups: []defkit.PatchContainerGroup{ + { + TargetField: "securityContext", + Fields: []defkit.PatchContainerField{ + {ParamName: "privileged", TargetField: "privileged", ParamType: "bool", ParamDefault: "false"}, + {ParamName: "runAsUser", TargetField: "runAsUser", ParamType: "int", Condition: "!= _|_"}, + {ParamName: "runAsGroup", TargetField: "runAsGroup", ParamType: "int", Condition: "!= _|_"}, + }, + SubGroups: []defkit.PatchContainerGroup{ + { + TargetField: "capabilities", + Fields: []defkit.PatchContainerField{ + {ParamName: "addCapabilities", TargetField: "add", ParamType: "[...string]", Condition: "!= _|_"}, + {ParamName: "dropCapabilities", TargetField: "drop", ParamType: "[...string]", Condition: "!= _|_"}, + }, + }, + }, + }, + }, + }) + }) + + cue := trait.ToCue() + + // Fields with != _|_ condition should use optional syntax (field?: type), not *null | type + Expect(cue).To(ContainSubstring(`runAsUser?: int`)) + Expect(cue).To(ContainSubstring(`runAsGroup?: int`)) + Expect(cue).To(ContainSubstring(`addCapabilities?: [...string]`)) + Expect(cue).To(ContainSubstring(`dropCapabilities?: [...string]`)) + + // Must NOT have *null | type for these fields + Expect(cue).NotTo(ContainSubstring(`runAsUser: *null | int`)) + Expect(cue).NotTo(ContainSubstring(`runAsGroup: *null | int`)) + Expect(cue).NotTo(ContainSubstring(`addCapabilities: *null | [...string]`)) + Expect(cue).NotTo(ContainSubstring(`dropCapabilities: *null | [...string]`)) + + // Fields with explicit defaults should still use default syntax + Expect(cue).To(ContainSubstring(`privileged: *false | bool`)) + + // The PatchContainer body should still have conditional blocks for these fields + Expect(cue).To(ContainSubstring(`if _params.runAsUser != _|_`)) + Expect(cue).To(ContainSubstring(`if _params.runAsGroup != _|_`)) + Expect(cue).To(ContainSubstring(`if _params.addCapabilities != _|_`)) + Expect(cue).To(ContainSubstring(`if _params.dropCapabilities != _|_`)) + }) + + It("should use *empty-string default for string-equality conditions", func() { + trait := defkit.NewTrait("image-test"). + Description("Test string-equality condition default"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + PatchFields: []defkit.PatchContainerField{ + {ParamName: "image", TargetField: "image", PatchStrategy: "retainKeys"}, + {ParamName: "imagePullPolicy", TargetField: "imagePullPolicy", PatchStrategy: "retainKeys", Condition: `!= ""`}, + }, + }) + }) + + cue := trait.ToCue() + + // String-equality condition should default to empty string, not null + Expect(cue).To(ContainSubstring(`imagePullPolicy: *"" |`)) + Expect(cue).NotTo(ContainSubstring(`imagePullPolicy: *null |`)) + }) + + It("should map params unconditionally in single-container _params block", func() { + trait := defkit.NewTrait("unconditional-test"). + Description("Test unconditional param mapping"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + AllowMultiple: true, + ContainersParam: "containers", + PatchFields: []defkit.PatchContainerField{ + {ParamName: "image", TargetField: "image"}, + {ParamName: "imagePullPolicy", TargetField: "imagePullPolicy", Condition: `!= ""`}, + }, + }) + }) + + cue := trait.ToCue() + + // In the single-container _params block, all fields should be mapped unconditionally + Expect(cue).To(ContainSubstring("image: parameter.image")) + Expect(cue).To(ContainSubstring("imagePullPolicy: parameter.imagePullPolicy")) + // The conditional should NOT wrap the param mapping in the _params block + Expect(cue).NotTo(MatchRegexp(`if parameter\.imagePullPolicy != ""[^}]*\n[^}]*imagePullPolicy: parameter\.imagePullPolicy`)) + // But the PatchContainer body should still have the condition + Expect(cue).To(ContainSubstring(`if _params.imagePullPolicy != ""`)) + }) + + It("should emit *#PatchParams with star default marker in multi-container parameter block", func() { + trait := defkit.NewTrait("star-test"). + Description("Test star in parameter"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + AllowMultiple: true, + ContainersParam: "containers", + PatchFields: []defkit.PatchContainerField{ + {ParamName: "image", TargetField: "image"}, + }, + }) + }) + + cue := trait.ToCue() + + // Should be *#PatchParams with star (marks single-container as default branch) + Expect(cue).To(ContainSubstring("parameter: *#PatchParams | close({")) + }) + + It("should use custom Description and ContainersDescription", func() { + trait := defkit.NewTrait("desc-test"). + Description("Test custom descriptions"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + AllowMultiple: true, + ContainersParam: "containers", + ContainersDescription: "Specify the container image for multiple containers", + PatchFields: []defkit.PatchContainerField{ + {ParamName: "image", TargetField: "image", Description: "Specify the image of the container"}, + {ParamName: "imagePullPolicy", TargetField: "imagePullPolicy", Condition: `!= ""`, Description: "Specify the image pull policy of the container"}, + }, + }) + }) + + cue := trait.ToCue() + + // Custom field descriptions + Expect(cue).To(ContainSubstring("// +usage=Specify the image of the container")) + Expect(cue).To(ContainSubstring("// +usage=Specify the image pull policy of the container")) + // Custom containers description + Expect(cue).To(ContainSubstring("// +usage=Specify the container image for multiple containers")) + }) + + It("should not emit duplicate parameter: {} when using PatchContainer", func() { + trait := defkit.NewTrait("no-dup-param-test"). + Description("Test no duplicate parameter block"). + AppliesTo("deployments.apps"). + Params(defkit.String("image").Required()). + Template(func(tpl *defkit.Template) { + tpl.UsePatchContainer(defkit.PatchContainerConfig{ + ContainerNameParam: "containerName", + DefaultToContextName: true, + PatchFields: []defkit.PatchContainerField{ + {ParamName: "image", TargetField: "image"}, + }, + }) + }) + + cue := trait.ToCue() + + // Only one parameter: line should appear (from PatchContainer) + Expect(strings.Count(cue, "parameter:")).To(Equal(1)) + // Should not have parameter: {} + Expect(cue).NotTo(ContainSubstring("parameter: {}")) }) }) diff --git a/pkg/definition/defkit/trait.go b/pkg/definition/defkit/trait.go index 0d1f11e89..a617b9a7a 100644 --- a/pkg/definition/defkit/trait.go +++ b/pkg/definition/defkit/trait.go @@ -439,8 +439,10 @@ func (g *TraitCUEGenerator) GenerateFullDefinition(t *TraitDefinition) string { func (g *TraitCUEGenerator) writeAttributes(sb *strings.Builder, t *TraitDefinition, depth int) { indent := strings.Repeat(g.indent, depth) - // podDisruptive - sb.WriteString(fmt.Sprintf("%spodDisruptive: %v\n", indent, t.IsPodDisruptive())) + // podDisruptive (only emit when true, false is the default) + if t.IsPodDisruptive() { + sb.WriteString(fmt.Sprintf("%spodDisruptive: %v\n", indent, t.IsPodDisruptive())) + } // stage (if set) if t.GetStage() != "" { @@ -508,12 +510,20 @@ func (g *TraitCUEGenerator) GenerateTemplate(t *TraitDefinition) string { sb.WriteString("template: {\n") // Check if using Template API + usedPatchContainer := false if t.HasTemplate() { + // Check if PatchContainer is configured (it generates its own parameter block) + tpl := NewTemplate() + t.GetTemplate()(tpl) + usedPatchContainer = tpl.GetPatchContainerConfig() != nil + g.writeUnifiedTemplate(&sb, t, 1) } - // Generate parameter section - sb.WriteString(g.generateParameterBlock(t, 1)) + // Generate parameter section (skip if PatchContainer already generated it) + if !usedPatchContainer { + sb.WriteString(g.generateParameterBlock(t, 1)) + } // Generate helper type definitions (like #HealthProbe, #labelSelector) gen := NewCUEGenerator() @@ -640,11 +650,164 @@ func (g *TraitCUEGenerator) writePatchResourceOps(sb *strings.Builder, gen *CUEG } } - // Build a tree structure from operations (similar to component resources) - tree := gen.buildFieldTree(ops) - // Normalize conditional nodes to avoid empty parent structs in patches - gen.liftChildConditions(tree) - g.writePatchFieldTree(sb, gen, tree, depth) + // Separate IfBlocks from other ops + var bareOps []ResourceOp + var ifBlocks []*IfBlock + for _, op := range ops { + if ib, ok := op.(*IfBlock); ok { + ifBlocks = append(ifBlocks, ib) + } else { + bareOps = append(bareOps, op) + } + } + + // If 0 or 1 IfBlock, use existing merged approach (no path conflicts possible) + if len(ifBlocks) <= 1 { + tree := gen.buildFieldTree(ops) + gen.liftChildConditions(tree) + g.writePatchFieldTree(sb, gen, tree, depth) + return + } + + // Multiple IfBlocks: process each separately to avoid path conflicts + // when different IfBlocks target the same field paths with different conditions. + prefix := g.findIfBlockCommonPrefix(ifBlocks, bareOps) + var prefixParts []string + if prefix != "" { + prefixParts = strings.Split(prefix, ".") + } + + // Write the common prefix inline (e.g., "spec: ") + for _, p := range prefixParts { + sb.WriteString(fmt.Sprintf("%s: ", p)) + } + + innerDepth := depth + len(prefixParts) + + // Open block for the children + sb.WriteString("{\n") + + indent := strings.Repeat(g.indent, innerDepth+1) + first := true + + // Render bare ops first (if any) + if len(bareOps) > 0 { + bareTree := gen.buildFieldTree(bareOps) + // Navigate to subtree below common prefix + subtree := bareTree + for _, p := range prefixParts { + if child, ok := subtree.children[p]; ok { + subtree = child + } + } + gen.liftChildConditions(subtree) + for _, key := range subtree.childOrder { + node := subtree.children[key] + sb.WriteString(indent) + g.writePatchFieldNode(sb, gen, key, node, innerDepth+1) + sb.WriteString("\n") + } + first = false + } + + // Render each IfBlock as a separate subtree + for _, ib := range ifBlocks { + if !first { + sb.WriteString("\n") + } + + // Build tree from inner ops only (without the outer If condition) + innerTree := gen.buildFieldTree(ib.Ops()) + + // Navigate to subtree below common prefix + subtree := innerTree + for _, p := range prefixParts { + if child, ok := subtree.children[p]; ok { + subtree = child + } + } + + // Normalize conditional nodes in the subtree + gen.liftChildConditions(subtree) + + // Emit: if condition { ... } + condStr := gen.conditionToCUE(ib.Cond()) + sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr)) + + // Render subtree children inside the if block + innerBlockIndent := strings.Repeat(g.indent, innerDepth+2) + for _, key := range subtree.childOrder { + node := subtree.children[key] + sb.WriteString(innerBlockIndent) + g.writePatchFieldNode(sb, gen, key, node, innerDepth+2) + sb.WriteString("\n") + } + + sb.WriteString(fmt.Sprintf("%s}", indent)) + first = false + } + + // Close outer block + sb.WriteString(fmt.Sprintf("\n%s}", strings.Repeat(g.indent, innerDepth))) +} + +// findIfBlockCommonPrefix finds the longest common path prefix across all IfBlocks and bare ops. +// For example, if all paths start with "spec.", the prefix is "spec". +func (g *TraitCUEGenerator) findIfBlockCommonPrefix(ifBlocks []*IfBlock, bareOps []ResourceOp) string { + var allPaths []string + + for _, ib := range ifBlocks { + for _, op := range ib.Ops() { + switch o := op.(type) { + case *SetOp: + allPaths = append(allPaths, o.Path()) + case *SetIfOp: + allPaths = append(allPaths, o.Path()) + case *PatchStrategyAnnotationOp: + allPaths = append(allPaths, o.Path()) + } + } + } + + for _, op := range bareOps { + switch o := op.(type) { + case *SetOp: + allPaths = append(allPaths, o.Path()) + case *SetIfOp: + allPaths = append(allPaths, o.Path()) + } + } + + if len(allPaths) == 0 { + return "" + } + + firstParts := strings.Split(allPaths[0], ".") + commonLen := len(firstParts) + + for _, p := range allPaths[1:] { + parts := strings.Split(p, ".") + minLen := commonLen + if len(parts) < minLen { + minLen = len(parts) + } + matched := 0 + for i := 0; i < minLen; i++ { + if parts[i] != firstParts[i] { + break + } + matched++ + } + commonLen = matched + } + + if commonLen == 0 { + return "" + } + + // Don't include leaf-level parts — only include the common ancestor path + // (at least one level must remain below the prefix for each IfBlock) + return strings.Join(firstParts[:commonLen], ".") } // writePatchFieldTree writes a field tree as CUE patch syntax. @@ -684,11 +847,21 @@ func (g *TraitCUEGenerator) writePatchFieldNode(sb *strings.Builder, gen *CUEGen return } + // Handle SpreadAll operations (array constraint patches) + if node.spreadAll != nil { + g.writeSpreadAllOp(sb, gen, key, node.spreadAll, node.cond, depth) + return + } + // Handle conditional fields if node.cond != nil { condStr := gen.conditionToCUE(node.cond) sb.WriteString(fmt.Sprintf("if %s {\n", condStr)) innerIndent := strings.Repeat(g.indent, depth+1) + // Emit patchStrategy annotation inside the if block + if node.patchStrategy != "" { + sb.WriteString(fmt.Sprintf("%s// +patchStrategy=%s\n", innerIndent, node.patchStrategy)) + } sb.WriteString(fmt.Sprintf("%s%s: ", innerIndent, key)) if len(node.children) > 0 { g.writePatchFieldTreeFromChildren(sb, gen, node, depth+1) @@ -701,6 +874,9 @@ func (g *TraitCUEGenerator) writePatchFieldNode(sb *strings.Builder, gen *CUEGen } // Regular field + if node.patchStrategy != "" { + sb.WriteString(fmt.Sprintf("// +patchStrategy=%s\n%s", node.patchStrategy, strings.Repeat(g.indent, depth))) + } sb.WriteString(fmt.Sprintf("%s: ", key)) if len(node.children) > 0 { g.writePatchFieldTreeFromChildren(sb, gen, node, depth) @@ -741,9 +917,9 @@ func (g *TraitCUEGenerator) hasConditionalDescendant(node *fieldNode) bool { if node.cond != nil { return true } - // PatchKey and ForEach operations require block format because they need - // to write comments or special syntax that can't appear inline - if node.patchKey != nil || node.forEach != nil { + // PatchKey, ForEach, and patchStrategy annotations require block format + // because they need to write comments or special syntax that can't appear inline + if node.patchKey != nil || node.forEach != nil || node.patchStrategy != "" { return true } for _, child := range node.children { @@ -845,6 +1021,57 @@ func (g *TraitCUEGenerator) writePatchKeyOp(sb *strings.Builder, gen *CUEGenerat } } +// writeSpreadAllOp writes a SpreadAll operation as CUE. +// Generates: path: [...{element}] +// If cond is provided, wraps in: if cond { ... } +func (g *TraitCUEGenerator) writeSpreadAllOp(sb *strings.Builder, gen *CUEGenerator, key string, op *SpreadAllOp, cond Condition, depth int) { + indent := strings.Repeat(g.indent, depth) + + writeElements := func(sb *strings.Builder, elemDepth int) { + for i, elem := range op.Elements() { + if i > 0 { + sb.WriteString(", ") + } + if arrElem, ok := elem.(*ArrayElement); ok { + // Build a field tree from the element's ops for proper nesting + tree := gen.buildFieldTree(arrElem.Ops()) + gen.liftChildConditions(tree) + // Also add direct field assignments + for fk, fv := range arrElem.Fields() { + gen.insertIntoTree(tree, fk, fv, nil) + } + sb.WriteString("...{\n") + // Write tree children with explicit indentation + // (avoid writePatchFieldTree's single-child inline optimization) + innerIndent := strings.Repeat(g.indent, elemDepth+1) + for _, tk := range tree.childOrder { + tn := tree.children[tk] + sb.WriteString(innerIndent) + g.writePatchFieldNode(sb, gen, tk, tn, elemDepth+1) + sb.WriteString("\n") + } + sb.WriteString(fmt.Sprintf("%s}", strings.Repeat(g.indent, elemDepth))) + } else { + sb.WriteString("...") + sb.WriteString(gen.valueToCUE(elem)) + } + } + } + + if cond != nil { + condStr := gen.conditionToCUE(cond) + sb.WriteString(fmt.Sprintf("if %s {\n", condStr)) + sb.WriteString(fmt.Sprintf("%s\t%s: [", indent, key)) + writeElements(sb, depth+1) + sb.WriteString("]\n") + sb.WriteString(fmt.Sprintf("%s}", indent)) + } else { + sb.WriteString(fmt.Sprintf("%s: [", key)) + writeElements(sb, depth) + sb.WriteString("]") + } +} + // writeTraitResourceOutput writes a resource as CUE for trait outputs. // This handles OutputsIf conditions and VersionConditionals, which the old // writeResourceOutput method did not support. @@ -1003,6 +1230,9 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf } sb.WriteString(fmt.Sprintf("%s}\n", indent)) } else { + if config.PatchStrategy != "" { + sb.WriteString(fmt.Sprintf("%s// +patchStrategy=%s\n", indent, config.PatchStrategy)) + } sb.WriteString(fmt.Sprintf("%spatch: spec: template: spec: {\n", indent)) // Determine the multi-container parameter name @@ -1107,7 +1337,11 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf } case config.AllowMultiple && multiParam != "": sb.WriteString(fmt.Sprintf("%sparameter: *#PatchParams | close({\n", indent)) - sb.WriteString(fmt.Sprintf("%s// +usage=Specify the settings for multiple containers\n", innerIndent)) + containersDesc := config.ContainersDescription + if containersDesc == "" { + containersDesc = "Specify the settings for multiple containers" + } + sb.WriteString(fmt.Sprintf("%s// +usage=%s\n", innerIndent, containersDesc)) sb.WriteString(fmt.Sprintf("%s%s: [...#PatchParams]\n", innerIndent, multiParam)) sb.WriteString(fmt.Sprintf("%s})\n", indent)) default: @@ -1120,7 +1354,11 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf // writePatchParamField writes a single field in the #PatchParams schema. func (g *TraitCUEGenerator) writePatchParamField(sb *strings.Builder, field PatchContainerField, indent string) { - sb.WriteString(fmt.Sprintf("%s// +usage=Specify the %s for the container\n", indent, field.ParamName)) + desc := field.Description + if desc == "" { + desc = fmt.Sprintf("Specify the %s of the container", field.ParamName) + } + sb.WriteString(fmt.Sprintf("%s// +usage=%s\n", indent, desc)) // Determine the type string typeStr := field.ParamType @@ -1138,17 +1376,24 @@ func (g *TraitCUEGenerator) writePatchParamField(sb *strings.Builder, field Patc } } - // Determine default value + // Determine default value and optionality defaultVal := field.ParamDefault + optional := "" if defaultVal == "" && field.Condition != "" { - // Has condition, likely optional - default to null - defaultVal = "null" + // Has condition, likely optional - choose appropriate default + if field.Condition == "!= \"\"" { + // String-equality condition: default to empty string + defaultVal = "\"\"" + } else { + // Non-string condition (e.g. != _|_): make field optional + optional = "?" + } } if defaultVal != "" { sb.WriteString(fmt.Sprintf("%s%s: *%s | %s\n", indent, field.ParamName, defaultVal, typeStr)) } else { - sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, field.ParamName, typeStr)) + sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, field.ParamName, optional, typeStr)) } } @@ -1207,9 +1452,12 @@ func (g *TraitCUEGenerator) writePatchContainerGroup(sb *strings.Builder, group } // writePatchParamMapping writes a parameter mapping in the patch block. +// Fields with a "!= _|_" existence condition and no ParamDefault are truly optional +// (defined as field?: type in the schema) and need conditional guards to avoid +// propagating _|_ when the parameter is unset. Fields with defaults or value-based +// conditions (like '!= ""') always have a value and can be mapped unconditionally. func (g *TraitCUEGenerator) writePatchParamMapping(sb *strings.Builder, field PatchContainerField, indent string, prefix string) { - if field.Condition != "" { - // Optional field - map conditionally + if field.Condition == "!= _|_" && field.ParamDefault == "" { sb.WriteString(fmt.Sprintf("%sif %s%s %s {\n", indent, prefix, field.ParamName, field.Condition)) sb.WriteString(fmt.Sprintf("%s\t%s: %s%s\n", indent, field.ParamName, prefix, field.ParamName)) sb.WriteString(fmt.Sprintf("%s}\n", indent)) diff --git a/pkg/definition/defkit/trait_test.go b/pkg/definition/defkit/trait_test.go index 84b6c299e..4bc185855 100644 --- a/pkg/definition/defkit/trait_test.go +++ b/pkg/definition/defkit/trait_test.go @@ -17,6 +17,8 @@ limitations under the License. package defkit_test import ( + "strings" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -185,8 +187,8 @@ parameter: #PatchParams Expect(cue).To(ContainSubstring(`scaler: {`)) Expect(cue).To(ContainSubstring(`type: "trait"`)) Expect(cue).To(ContainSubstring(`description: "Scale workloads"`)) - Expect(cue).To(ContainSubstring(`attributes: {`)) - Expect(cue).To(ContainSubstring(`podDisruptive: false`)) + // podDisruptive: false is not emitted (it's the default) + Expect(cue).NotTo(ContainSubstring(`podDisruptive: false`)) Expect(cue).To(ContainSubstring(`appliesToWorkloads: ["deployments.apps"]`)) }) @@ -555,5 +557,822 @@ template: { Expect(cue).To(ContainSubstring("patch:")) }) + + It("should generate Optional field guards in Map comprehension", func() { + items := defkit.Array("items").WithFields( + defkit.String("name").Required(), + defkit.String("label"), + defkit.Int("priority"), + ) + trait := defkit.NewTrait("optional-test"). + Description("Test optional fields"). + AppliesTo("deployments.apps"). + Params(items). + Template(func(tpl *defkit.Template) { + tpl.Patch(). + SetIf(items.IsSet(), "spec.template.spec.items", + defkit.From(items).Map(defkit.FieldMap{ + "name": defkit.F("name"), + "label": defkit.Optional("label"), + "priority": defkit.Optional("priority"), + })) + }) + + cue := trait.ToCue() + + // Required field should use direct access + Expect(cue).To(ContainSubstring("name: v.name")) + // Optional fields should have if guards + Expect(cue).To(ContainSubstring("if v.label != _|_")) + Expect(cue).To(ContainSubstring("label: v.label")) + Expect(cue).To(ContainSubstring("if v.priority != _|_")) + Expect(cue).To(ContainSubstring("priority: v.priority")) + // Should NOT contain top-level underscore for optional fields + Expect(cue).NotTo(ContainSubstring("label: _")) + Expect(cue).NotTo(ContainSubstring("priority: _")) + }) + + It("should generate If/EndIf with SetIf using sub-field conditions", func() { + parent := defkit.Map("parent").WithFields( + defkit.Array("required").WithFields( + defkit.String("key").Required(), + ), + defkit.Array("preferred").WithFields( + defkit.Int("weight").Required(), + ), + ) + trait := defkit.NewTrait("if-subfield-test"). + Description("Test If/EndIf with sub-field conditions"). + AppliesTo("deployments.apps"). + Params(parent). + Template(func(tpl *defkit.Template) { + tpl.Patch(). + If(parent.IsSet()). + SetIf(parent.Field("required").IsSet(), + "spec.requiredItems", + defkit.From(defkit.ParamPath("parent.required")).Map(defkit.FieldMap{ + "key": defkit.F("key"), + })). + SetIf(parent.Field("preferred").IsSet(), + "spec.preferredItems", + defkit.From(defkit.ParamPath("parent.preferred")).Map(defkit.FieldMap{ + "weight": defkit.F("weight"), + })). + EndIf() + }) + + cue := trait.ToCue() + + // Both conditions should appear (AND-combined by the field tree) + Expect(cue).To(ContainSubstring(`parameter["parent"] != _|_`)) + Expect(cue).To(ContainSubstring("parameter.parent.required != _|_")) + Expect(cue).To(ContainSubstring("parameter.parent.preferred != _|_")) + // Verify the For comprehension sources + Expect(cue).To(ContainSubstring("for v in parameter.parent.required")) + Expect(cue).To(ContainSubstring("for v in parameter.parent.preferred")) + Expect(cue).To(ContainSubstring("key: v.key")) + Expect(cue).To(ContainSubstring("weight: v.weight")) + }) + }) + + Context("Let Bindings with ForEachMap", func() { + It("should generate let binding with ForEachMap and LetVariable references", func() { + trait := defkit.NewTrait("let-foreach-test"). + Description("Test let binding with ForEachMap"). + AppliesTo("*"). + PodDisruptive(true). + Param(defkit.DynamicMap().ValueTypeUnion("string | null")). + Template(func(tpl *defkit.Template) { + tpl.PatchStrategy("jsonMergePatch") + tpl.AddLetBinding("content", defkit.ForEachMap()) + tpl.Patch(). + Set("metadata.annotations", defkit.LetVariable("content")) + tpl.Patch(). + If(defkit.And( + defkit.ContextOutput().HasPath("spec"), + defkit.ContextOutput().HasPath("spec.template"), + )). + Set("spec.template.metadata.annotations", defkit.LetVariable("content")). + EndIf() + }) + + cue := trait.ToCue() + + // Let binding should appear before the patch block + Expect(cue).To(ContainSubstring("let content =")) + // ForEachMap should render as a struct comprehension + Expect(cue).To(ContainSubstring("for k, v in parameter")) + Expect(cue).To(ContainSubstring("(k): v")) + // Let variable references should appear in the patch + Expect(cue).To(ContainSubstring("metadata: annotations: content")) + // Conditional block should reference the let variable + Expect(cue).To(ContainSubstring("annotations: content")) + Expect(cue).To(ContainSubstring("context.output.spec != _|_")) + Expect(cue).To(ContainSubstring("context.output.spec.template != _|_")) + // The for-each comprehension should NOT be inlined at each usage site + // It should only appear once in the let binding + Expect(strings.Count(cue, "for k, v in parameter")).To(Equal(1)) + // Patch strategy should be present + Expect(cue).To(ContainSubstring("// +patchStrategy=jsonMergePatch")) + }) + + It("should render ForEachMap with custom source and vars via valueToCUE", func() { + trait := defkit.NewTrait("custom-foreach-test"). + Description("Test custom ForEachMap rendering"). + AppliesTo("deployments.apps"). + Param(defkit.DynamicMap().ValueTypeUnion("string | null")). + Template(func(tpl *defkit.Template) { + tpl.AddLetBinding("labelContent", + defkit.ForEachMap().Over("parameter.labels").WithVars("key", "val")) + tpl.Patch(). + Set("metadata.labels", defkit.LetVariable("labelContent")) + }) + + cue := trait.ToCue() + + // Custom variable names and source + Expect(cue).To(ContainSubstring("let labelContent =")) + Expect(cue).To(ContainSubstring("for key, val in parameter.labels")) + Expect(cue).To(ContainSubstring("(key): val")) + Expect(cue).To(ContainSubstring("metadata: labels: labelContent")) + }) + }) + + Context("PatchKey with ArrayParam (no array wrapping)", func() { + It("should emit direct assignment when single element is an ArrayParam", func() { + items := defkit.Array("items").WithFields( + defkit.String("name").Required(), + defkit.String("value").Required(), + ).Required() + + trait := defkit.NewTrait("patchkey-array-test"). + Description("Test PatchKey with ArrayParam"). + AppliesTo("deployments.apps"). + Params(items). + Template(func(tpl *defkit.Template) { + tpl.Patch(). + PatchKey("spec.template.spec.items", "name", items) + }) + + cue := trait.ToCue() + + // Should emit patchKey annotation + Expect(cue).To(ContainSubstring("// +patchKey=name")) + // Should assign parameter directly, NOT wrapped in [...] + Expect(cue).To(ContainSubstring("items: parameter.items")) + // Should NOT have array wrapping around the parameter + Expect(cue).NotTo(ContainSubstring("[parameter.items]")) + }) + + It("should still wrap individual ArrayElements in array brackets", func() { + elem := defkit.NewArrayElement(). + Set("name", defkit.Lit("test")). + Set("value", defkit.Lit("foo")) + + trait := defkit.NewTrait("patchkey-elem-test"). + Description("Test PatchKey with ArrayElement"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.Patch(). + PatchKey("spec.items", "name", elem) + }) + + cue := trait.ToCue() + + // ArrayElement should still be wrapped in [...] + Expect(cue).To(ContainSubstring("// +patchKey=name")) + Expect(cue).To(ContainSubstring("items: [{")) + }) + }) + + Context("IsSet bracket notation for optional field checks", func() { + It("should generate bracket notation for IsSet conditions", func() { + optParam := defkit.Array("env").Of(defkit.ParamTypeString).Optional() + + trait := defkit.NewTrait("isset-bracket-test"). + Description("Test bracket notation for IsSet"). + AppliesTo("deployments.apps"). + Params(optParam). + Template(func(tpl *defkit.Template) { + tpl.Patch(). + SetIf(optParam.IsSet(), "spec.env", optParam) + }) + + cue := trait.ToCue() + + // Should use bracket notation: parameter["env"] != _|_ + Expect(cue).To(ContainSubstring(`parameter["env"] != _|_`)) + // Should NOT use dot notation for the condition + Expect(cue).NotTo(ContainSubstring("parameter.env != _|_")) + }) + + It("should generate bracket notation for NotSet (negated IsSet) conditions", func() { + optParam := defkit.String("debug").Optional() + + trait := defkit.NewTrait("notset-bracket-test"). + Description("Test bracket notation for NotSet"). + AppliesTo("deployments.apps"). + Params(optParam). + Template(func(tpl *defkit.Template) { + tpl.Patch(). + SetIf(optParam.NotSet(), "spec.debug", defkit.Lit(false)) + }) + + cue := trait.ToCue() + + // Should use bracket notation: parameter["debug"] == _|_ + Expect(cue).To(ContainSubstring(`parameter["debug"] == _|_`)) + // Should NOT use dot notation + Expect(cue).NotTo(ContainSubstring("parameter.debug == _|_")) + }) + + It("should use bracket notation in compound conditions", func() { + cpu := defkit.String("cpu").Optional() + memory := defkit.String("memory").Optional() + + trait := defkit.NewTrait("compound-bracket-test"). + Description("Test bracket notation in compound conditions"). + AppliesTo("deployments.apps"). + Params(cpu, memory). + Template(func(tpl *defkit.Template) { + tpl.Patch(). + SetIf(defkit.And(cpu.IsSet(), memory.IsSet()), + "spec.resources", defkit.Lit("configured")) + }) + + cue := trait.ToCue() + + // Both parts of the compound condition should use bracket notation + Expect(cue).To(ContainSubstring(`parameter["cpu"] != _|_`)) + Expect(cue).To(ContainSubstring(`parameter["memory"] != _|_`)) + }) + }) + + Context("PatchStrategyAnnotation", func() { + It("should record PatchStrategyAnnotation ops on PatchResource", func() { + p := defkit.NewPatchResource() + p.PatchStrategyAnnotation("spec.strategy", "retainKeys") + + ops := p.Ops() + Expect(ops).To(HaveLen(1)) + ann, ok := ops[0].(*defkit.PatchStrategyAnnotationOp) + Expect(ok).To(BeTrue()) + Expect(ann.Path()).To(Equal("spec.strategy")) + Expect(ann.Strategy()).To(Equal("retainKeys")) + }) + + It("should record PatchStrategyAnnotation inside If block", func() { + p := defkit.NewPatchResource() + cond := defkit.Eq(defkit.ParameterField("kind"), defkit.Lit("Deployment")) + p.If(cond). + PatchStrategyAnnotation("spec.strategy", "retainKeys"). + Set("spec.strategy.type", defkit.ParameterField("strategyType")). + EndIf() + + ops := p.Ops() + Expect(ops).To(HaveLen(1)) + ifBlock, ok := ops[0].(*defkit.IfBlock) + Expect(ok).To(BeTrue()) + Expect(ifBlock.Ops()).To(HaveLen(2)) // PatchStrategyAnnotation + Set + }) + + It("should emit patchStrategy annotation in CUE output for unconditional field", func() { + strategy := defkit.Struct("strategy").Required().Fields( + defkit.Field("type", defkit.ParamTypeString).Default("RollingUpdate"), + ) + + trait := defkit.NewTrait("annotation-test"). + Description("Test patchStrategy annotation"). + AppliesTo("deployments.apps"). + Params(strategy). + Template(func(tpl *defkit.Template) { + tpl.Patch(). + PatchStrategyAnnotation("spec.strategy", "retainKeys"). + Set("spec.strategy.type", defkit.ParameterField("strategy.type")) + }) + + cue := trait.ToCue() + + // Should contain the annotation comment + Expect(cue).To(ContainSubstring("// +patchStrategy=retainKeys")) + // Annotation should appear before the field + Expect(cue).To(ContainSubstring("strategy: type: parameter.strategy.type")) + }) + + It("should emit patchStrategy annotation inside conditional block", func() { + kind := defkit.String("kind").Default("Deployment").Enum("Deployment", "StatefulSet") + strategy := defkit.Struct("strategy").Required().Fields( + defkit.Field("type", defkit.ParamTypeString).Default("RollingUpdate"), + ) + + trait := defkit.NewTrait("cond-annotation-test"). + Description("Test patchStrategy in conditional block"). + AppliesTo("deployments.apps"). + Params(kind, strategy). + Template(func(tpl *defkit.Template) { + tpl.Patch(). + If(defkit.Eq(kind, defkit.Lit("Deployment"))). + PatchStrategyAnnotation("spec.strategy", "retainKeys"). + Set("spec.strategy.type", defkit.ParameterField("strategy.type")). + EndIf() + }) + + cue := trait.ToCue() + + // Should contain the annotation inside the if block + Expect(cue).To(ContainSubstring("// +patchStrategy=retainKeys")) + Expect(cue).To(ContainSubstring(`parameter.kind == "Deployment"`)) + }) + }) + + Context("Condition hoisting", func() { + It("should hoist matching conditions from children to parent", func() { + enabled := defkit.Bool("enabled") + replicas := defkit.Int("replicas") + image := defkit.String("image") + + trait := defkit.NewTrait("hoist-test"). + Description("Test condition hoisting"). + AppliesTo("deployments.apps"). + Params(enabled, replicas, image). + Template(func(tpl *defkit.Template) { + cond := defkit.Eq(enabled, defkit.Lit(true)) + tpl.Patch(). + If(cond). + Set("spec.replicas", replicas). + Set("spec.template.spec.image", image). + EndIf() + }) + + cue := trait.ToCue() + + // The If condition should appear + Expect(cue).To(ContainSubstring(`parameter.enabled == true`)) + // Both fields should be set inside the condition + Expect(cue).To(ContainSubstring("replicas: parameter.replicas")) + Expect(cue).To(ContainSubstring("image: parameter.image")) + }) + + It("should hoist common condition from AND combinations", func() { + kind := defkit.String("kind").Default("Deployment") + replicas := defkit.Int("replicas") + image := defkit.String("image") + + trait := defkit.NewTrait("hoist-and-test"). + Description("Test AND condition hoisting"). + AppliesTo("deployments.apps"). + Params(kind, replicas, image). + Template(func(tpl *defkit.Template) { + isDeployment := defkit.Eq(kind, defkit.Lit("Deployment")) + replicasSet := replicas.IsSet() + imageSet := image.IsSet() + tpl.Patch(). + If(isDeployment). + SetIf(replicasSet, "spec.replicas", replicas). + SetIf(imageSet, "spec.template.spec.image", image). + EndIf() + }) + + cue := trait.ToCue() + + // The outer condition should appear at the parent level + Expect(cue).To(ContainSubstring(`parameter.kind == "Deployment"`)) + // The inner conditions should remain on the children + Expect(cue).To(ContainSubstring(`parameter["replicas"] != _|_`)) + Expect(cue).To(ContainSubstring(`parameter["image"] != _|_`)) + }) + }) + + Context("Multiple IfBlocks with overlapping paths", func() { + It("should render multiple IfBlocks as separate conditional blocks", func() { + kind := defkit.String("kind").Default("Deployment").Enum("Deployment", "StatefulSet", "DaemonSet") + strategyType := defkit.String("strategyType").Default("RollingUpdate") + maxSurge := defkit.String("maxSurge").Default("25%") + partition := defkit.Int("partition").Default(0) + + trait := defkit.NewTrait("multi-ifblock-test"). + Description("Test multiple IfBlocks"). + AppliesTo("deployments.apps", "statefulsets.apps", "daemonsets.apps"). + Params(kind, strategyType, maxSurge, partition). + Template(func(tpl *defkit.Template) { + isDeployment := defkit.Eq(kind, defkit.Lit("Deployment")) + isStatefulSet := defkit.Eq(kind, defkit.Lit("StatefulSet")) + + tpl.Patch(). + If(isDeployment). + PatchStrategyAnnotation("spec.strategy", "retainKeys"). + Set("spec.strategy.type", strategyType). + Set("spec.strategy.rollingUpdate.maxSurge", maxSurge). + EndIf(). + If(isStatefulSet). + PatchStrategyAnnotation("spec.updateStrategy", "retainKeys"). + Set("spec.updateStrategy.type", strategyType). + Set("spec.updateStrategy.rollingUpdate.partition", partition). + EndIf() + }) + + cue := trait.ToCue() + + // Should have two separate if blocks (not merged) + Expect(cue).To(ContainSubstring(`parameter.kind == "Deployment"`)) + Expect(cue).To(ContainSubstring(`parameter.kind == "StatefulSet"`)) + // Each should have its own patchStrategy annotation + Expect(strings.Count(cue, "// +patchStrategy=retainKeys")).To(Equal(2)) + // Deployment uses "strategy", StatefulSet uses "updateStrategy" + Expect(cue).To(ContainSubstring("strategy: {")) + Expect(cue).To(ContainSubstring("updateStrategy: {")) + // Fields should be present + Expect(cue).To(ContainSubstring("maxSurge: parameter.maxSurge")) + Expect(cue).To(ContainSubstring("partition: parameter.partition")) + }) + + It("should produce correct k8s-update-strategy-like pattern", func() { + targetKind := defkit.String("targetKind").Default("Deployment").Enum("Deployment", "StatefulSet", "DaemonSet") + strategy := defkit.Struct("strategy").Required().Fields( + defkit.Field("type", defkit.ParamTypeString).Default("RollingUpdate").Enum("RollingUpdate", "Recreate", "OnDelete"), + defkit.Field("rollingStrategy", defkit.ParamTypeStruct). + Nested(defkit.Struct("rollingStrategy").Fields( + defkit.Field("maxSurge", defkit.ParamTypeString).Default("25%"), + defkit.Field("maxUnavailable", defkit.ParamTypeString).Default("25%"), + defkit.Field("partition", defkit.ParamTypeInt).Default(0), + )), + ) + + trait := defkit.NewTrait("k8s-update-strategy-test"). + Description("Test k8s-update-strategy pattern"). + AppliesTo("deployments.apps", "statefulsets.apps", "daemonsets.apps"). + PodDisruptive(false). + Params(targetKind, strategy). + Template(func(tpl *defkit.Template) { + strategyType := defkit.ParameterField("strategy.type") + maxSurge := defkit.ParameterField("strategy.rollingStrategy.maxSurge") + maxUnavailable := defkit.ParameterField("strategy.rollingStrategy.maxUnavailable") + partition := defkit.ParameterField("strategy.rollingStrategy.partition") + + isDeployment := defkit.Eq(defkit.ParameterField("targetKind"), defkit.Lit("Deployment")) + isStatefulSet := defkit.Eq(defkit.ParameterField("targetKind"), defkit.Lit("StatefulSet")) + isDaemonSet := defkit.Eq(defkit.ParameterField("targetKind"), defkit.Lit("DaemonSet")) + isNotOnDelete := defkit.Ne(strategyType, defkit.Lit("OnDelete")) + isNotRecreate := defkit.Ne(strategyType, defkit.Lit("Recreate")) + isRollingUpdate := defkit.Eq(strategyType, defkit.Lit("RollingUpdate")) + + tpl.Patch(). + If(defkit.And(isDeployment, isNotOnDelete)). + PatchStrategyAnnotation("spec.strategy", "retainKeys"). + Set("spec.strategy.type", strategyType). + SetIf(isRollingUpdate, "spec.strategy.rollingUpdate.maxSurge", maxSurge). + SetIf(isRollingUpdate, "spec.strategy.rollingUpdate.maxUnavailable", maxUnavailable). + EndIf(). + If(defkit.And(isStatefulSet, isNotRecreate)). + PatchStrategyAnnotation("spec.updateStrategy", "retainKeys"). + Set("spec.updateStrategy.type", strategyType). + SetIf(isRollingUpdate, "spec.updateStrategy.rollingUpdate.partition", partition). + EndIf(). + If(defkit.And(isDaemonSet, isNotRecreate)). + PatchStrategyAnnotation("spec.updateStrategy", "retainKeys"). + Set("spec.updateStrategy.type", strategyType). + SetIf(isRollingUpdate, "spec.updateStrategy.rollingUpdate.maxSurge", maxSurge). + SetIf(isRollingUpdate, "spec.updateStrategy.rollingUpdate.maxUnavailable", maxUnavailable). + EndIf() + }) + + cue := trait.ToCue() + + // Three separate if blocks + Expect(cue).To(ContainSubstring(`parameter.targetKind == "Deployment" && parameter.strategy.type != "OnDelete"`)) + Expect(cue).To(ContainSubstring(`parameter.targetKind == "StatefulSet" && parameter.strategy.type != "Recreate"`)) + Expect(cue).To(ContainSubstring(`parameter.targetKind == "DaemonSet" && parameter.strategy.type != "Recreate"`)) + // Three patchStrategy annotations + Expect(strings.Count(cue, "// +patchStrategy=retainKeys")).To(Equal(3)) + // RollingUpdate inner conditions + Expect(cue).To(ContainSubstring(`parameter.strategy.type == "RollingUpdate"`)) + // Deployment uses "strategy", others use "updateStrategy" + Expect(cue).To(ContainSubstring("strategy: {")) + Expect(cue).To(ContainSubstring("updateStrategy: {")) + // Correct field assignments + Expect(cue).To(ContainSubstring("maxSurge: parameter.strategy.rollingStrategy.maxSurge")) + Expect(cue).To(ContainSubstring("maxUnavailable: parameter.strategy.rollingStrategy.maxUnavailable")) + Expect(cue).To(ContainSubstring("partition: parameter.strategy.rollingStrategy.partition")) + }) + }) + + Context("Raw patch block with fluent params and helpers", func() { + It("should render raw patch block followed by fluent parameter and helper definitions", func() { + postStart := defkit.Map("postStart").WithSchemaRef("Handler") + preStop := defkit.Map("preStop").WithSchemaRef("Handler") + + trait := defkit.NewTrait("lifecycle"). + Description("test"). + AppliesTo("deployments.apps"). + PodDisruptive(true). + Params(postStart, preStop). + Helper("Handler", defkit.Struct("Handler").Fields( + defkit.Field("exec", defkit.ParamTypeStruct). + Nested(defkit.Struct("exec").Fields( + defkit.Field("command", defkit.ParamTypeArray).ArrayOf(defkit.ParamTypeString).Required(), + )), + )). + Template(func(tpl *defkit.Template) { + tpl.SetRawPatchBlock(`patch: spec: containers: [...{ + lifecycle: { + if parameter.postStart != _|_ { + postStart: parameter.postStart + } + } +}]`) + }) + + cue := trait.ToCue() + + // Raw patch block is rendered + Expect(cue).To(ContainSubstring("containers: [...{")) + Expect(cue).To(ContainSubstring("lifecycle: {")) + Expect(cue).To(ContainSubstring("if parameter.postStart != _|_")) + + // Fluent parameter block is rendered (not skipped) + Expect(cue).To(ContainSubstring("parameter: {")) + Expect(cue).To(ContainSubstring("postStart?: #Handler")) + // CUE formatter may add alignment spaces: preStop?: #Handler + Expect(cue).To(ContainSubstring("preStop?:")) + Expect(cue).To(MatchRegexp(`preStop\?:\s+#Handler`)) + + // Helper definition is rendered (not skipped) + // CUE formatter collapses single-field struct to inline form + Expect(cue).To(ContainSubstring("#Handler:")) + Expect(cue).To(ContainSubstring("command: [...string]")) + }) + + It("should still use full raw mode when raw parameter block is set", func() { + trait := defkit.NewTrait("raw-all"). + Description("test"). + AppliesTo("deployments.apps"). + Template(func(tpl *defkit.Template) { + tpl.SetRawPatchBlock(`patch: spec: replicas: parameter.replicas`) + tpl.SetRawParameterBlock(`parameter: { + replicas: *1 | int +}`) + }) + + cue := trait.ToCue() + + Expect(cue).To(ContainSubstring("replicas: parameter.replicas")) + Expect(cue).To(ContainSubstring("replicas: *1 | int")) + // Should NOT have a duplicate parameter block + Expect(strings.Count(cue, "parameter:")).To(Equal(2)) // one in patch ref, one in param block + }) + }) + + Context("IntParam helper definition rendering", func() { + It("should render constrained int helper with min and max", func() { + trait := defkit.NewTrait("int-helper-test"). + Description("test"). + AppliesTo("deployments.apps"). + Helper("Port", defkit.Int("Port").Min(1).Max(65535)). + Template(func(tpl *defkit.Template) { + tpl.SetRawPatchBlock(`patch: spec: port: parameter.port`) + }) + + cue := trait.ToCue() + + Expect(cue).To(ContainSubstring("#Port: int & >=1 & <=65535")) + }) + + It("should render int helper with only min constraint", func() { + trait := defkit.NewTrait("int-min-test"). + Description("test"). + AppliesTo("deployments.apps"). + Helper("Positive", defkit.Int("Positive").Min(0)). + Template(func(tpl *defkit.Template) { + tpl.SetRawPatchBlock(`patch: spec: count: parameter.count`) + }) + + cue := trait.ToCue() + + Expect(cue).To(ContainSubstring("#Positive: int & >=0")) + Expect(cue).NotTo(ContainSubstring("<=")) + }) + + It("should render int helper without constraints", func() { + trait := defkit.NewTrait("int-bare-test"). + Description("test"). + AppliesTo("deployments.apps"). + Helper("Count", defkit.Int("Count")). + Template(func(tpl *defkit.Template) { + tpl.SetRawPatchBlock(`patch: spec: count: parameter.count`) + }) + + cue := trait.ToCue() + + Expect(cue).To(ContainSubstring("#Count: int")) + Expect(cue).NotTo(ContainSubstring(">=")) + Expect(cue).NotTo(ContainSubstring("<=")) + }) + }) + + Context("SpreadAll operation", func() { + It("should record SpreadAll ops on PatchResource", func() { + p := defkit.NewPatchResource() + elem := defkit.NewArrayElement().Set("name", defkit.Lit("test")) + p.SpreadAll("spec.containers", elem) + + ops := p.Ops() + Expect(ops).To(HaveLen(1)) + sa, ok := ops[0].(*defkit.SpreadAllOp) + Expect(ok).To(BeTrue()) + Expect(sa.Path()).To(Equal("spec.containers")) + Expect(sa.Elements()).To(HaveLen(1)) + }) + + It("should record SpreadAll inside If block", func() { + p := defkit.NewPatchResource() + cond := defkit.Eq(defkit.ParameterField("enabled"), defkit.Lit(true)) + elem := defkit.NewArrayElement().Set("name", defkit.Lit("test")) + p.If(cond). + SpreadAll("spec.containers", elem). + EndIf() + + ops := p.Ops() + Expect(ops).To(HaveLen(1)) + ifBlock, ok := ops[0].(*defkit.IfBlock) + Expect(ok).To(BeTrue()) + Expect(ifBlock.Ops()).To(HaveLen(1)) + _, ok = ifBlock.Ops()[0].(*defkit.SpreadAllOp) + Expect(ok).To(BeTrue()) + }) + + It("should render unconditional SpreadAll with simple value", func() { + image := defkit.String("image").Required() + + trait := defkit.NewTrait("spreadall-simple-test"). + Description("Test SpreadAll with simple value"). + AppliesTo("deployments.apps"). + Params(image). + Template(func(tpl *defkit.Template) { + elem := defkit.NewArrayElement(). + Set("image", image) + tpl.Patch().SpreadAll("spec.template.spec.containers", elem) + }) + + cue := trait.ToCue() + + Expect(cue).To(ContainSubstring("containers: [...{")) + Expect(cue).To(ContainSubstring("image: parameter.image")) + }) + + It("should render conditional SpreadAll with ArrayElement SetIf", func() { + postStart := defkit.String("postStart") + preStop := defkit.String("preStop") + + trait := defkit.NewTrait("spreadall-conditional-test"). + Description("Test SpreadAll with conditional fields"). + AppliesTo("deployments.apps"). + Params(postStart, preStop). + Template(func(tpl *defkit.Template) { + elem := defkit.NewArrayElement(). + SetIf(postStart.IsSet(), "lifecycle.postStart.exec.command", postStart). + SetIf(preStop.IsSet(), "lifecycle.preStop.exec.command", preStop) + tpl.Patch().SpreadAll("spec.template.spec.containers", elem) + }) + + cue := trait.ToCue() + + Expect(cue).To(ContainSubstring("containers: [...{")) + Expect(cue).To(ContainSubstring(`parameter["postStart"] != _|_`)) + Expect(cue).To(ContainSubstring(`parameter["preStop"] != _|_`)) + }) + + It("should render SpreadAll inside an IfBlock", func() { + enabled := defkit.Bool("enabled").Default(false) + image := defkit.String("image").Required() + + trait := defkit.NewTrait("spreadall-ifblock-test"). + Description("Test SpreadAll inside IfBlock"). + AppliesTo("deployments.apps"). + Params(enabled, image). + Template(func(tpl *defkit.Template) { + elem := defkit.NewArrayElement(). + Set("image", image) + tpl.Patch(). + If(defkit.Eq(enabled, defkit.Lit(true))). + SpreadAll("spec.template.spec.containers", elem). + EndIf() + }) + + cue := trait.ToCue() + + Expect(cue).To(ContainSubstring("parameter.enabled == true")) + Expect(cue).To(ContainSubstring("containers: [...{")) + Expect(cue).To(ContainSubstring("image: parameter.image")) + }) + }) + + Context("Multiple IfBlocks with bare ops", func() { + It("should render bare ops before conditional IfBlocks", func() { + kind := defkit.String("kind").Default("Deployment").Enum("Deployment", "StatefulSet") + replicas := defkit.Int("replicas").Default(1) + strategyType := defkit.String("strategyType").Default("RollingUpdate") + + trait := defkit.NewTrait("bare-ops-ifblock-test"). + Description("Test bare ops with multiple IfBlocks"). + AppliesTo("deployments.apps", "statefulsets.apps"). + Params(kind, replicas, strategyType). + Template(func(tpl *defkit.Template) { + isDeployment := defkit.Eq(kind, defkit.Lit("Deployment")) + isStatefulSet := defkit.Eq(kind, defkit.Lit("StatefulSet")) + + tpl.Patch(). + Set("spec.replicas", replicas). + If(isDeployment). + Set("spec.strategy.type", strategyType). + EndIf(). + If(isStatefulSet). + Set("spec.updateStrategy.type", strategyType). + EndIf() + }) + + cue := trait.ToCue() + + // Bare op should be present + Expect(cue).To(ContainSubstring("replicas: parameter.replicas")) + // Both conditional blocks + Expect(cue).To(ContainSubstring(`parameter.kind == "Deployment"`)) + Expect(cue).To(ContainSubstring(`parameter.kind == "StatefulSet"`)) + Expect(cue).To(ContainSubstring("strategy: type:")) + Expect(cue).To(ContainSubstring("updateStrategy: type:")) + }) + }) + + Context("findIfBlockCommonPrefix edge cases", func() { + It("should handle IfBlocks with no common prefix", func() { + kind := defkit.String("kind").Default("a").Enum("a", "b") + + trait := defkit.NewTrait("no-common-prefix-test"). + Description("Test no common prefix"). + AppliesTo("deployments.apps"). + Params(kind). + Template(func(tpl *defkit.Template) { + tpl.Patch(). + If(defkit.Eq(kind, defkit.Lit("a"))). + Set("spec.strategy.type", defkit.Lit("RollingUpdate")). + EndIf(). + If(defkit.Eq(kind, defkit.Lit("b"))). + Set("metadata.labels.version", defkit.Lit("v2")). + EndIf() + }) + + cue := trait.ToCue() + + // Both conditions should be rendered + Expect(cue).To(ContainSubstring(`parameter.kind == "a"`)) + Expect(cue).To(ContainSubstring(`parameter.kind == "b"`)) + Expect(cue).To(ContainSubstring("strategy: type:")) + Expect(cue).To(ContainSubstring("labels: version:")) + }) + }) + + Context("Nested array struct in helper definitions", func() { + It("should render array field with nested struct as [...{fields}]", func() { + trait := defkit.NewTrait("nested-array-test"). + Description("test"). + AppliesTo("deployments.apps"). + Helper("Config", defkit.Struct("Config").Fields( + defkit.Field("headers", defkit.ParamTypeArray). + Nested(defkit.Struct("headers").Fields( + defkit.Field("name", defkit.ParamTypeString).Required(), + defkit.Field("value", defkit.ParamTypeString).Required(), + )), + )). + Template(func(tpl *defkit.Template) { + tpl.SetRawPatchBlock(`patch: spec: config: parameter.config`) + }) + + cue := trait.ToCue() + + // CUE formatter collapses single-field struct to inline form + Expect(cue).To(ContainSubstring("#Config: headers?: [...{")) + Expect(cue).To(ContainSubstring("name: string")) + Expect(cue).To(ContainSubstring("value: string")) + }) + + It("should render schema ref on fields within helper structs", func() { + trait := defkit.NewTrait("schema-ref-test"). + Description("test"). + AppliesTo("deployments.apps"). + Helper("Port", defkit.Int("Port").Min(1).Max(65535)). + Helper("Endpoint", defkit.Struct("Endpoint").Fields( + defkit.Field("port", defkit.ParamTypeInt).WithSchemaRef("Port").Required(), + defkit.Field("host", defkit.ParamTypeString), + )). + Template(func(tpl *defkit.Template) { + tpl.SetRawPatchBlock(`patch: spec: endpoint: parameter.endpoint`) + }) + + cue := trait.ToCue() + + Expect(cue).To(ContainSubstring("#Port: int & >=1 & <=65535")) + Expect(cue).To(ContainSubstring("#Endpoint: {")) + Expect(cue).To(ContainSubstring("port: #Port")) + Expect(cue).To(ContainSubstring("host?: string")) + }) }) }) diff --git a/pkg/definition/gen_sdk/_scaffold/go/go.mod_ b/pkg/definition/gen_sdk/_scaffold/go/go.mod_ index da3b83c1a..8b442c9b8 100644 --- a/pkg/definition/gen_sdk/_scaffold/go/go.mod_ +++ b/pkg/definition/gen_sdk/_scaffold/go/go.mod_ @@ -3,7 +3,7 @@ module github.com/kubevela/vela-go-sdk go 1.23.8 require ( - github.com/oam-dev/kubevela-core-api v1.7.8-0.20250930174210-fb3adce5e9f6 + github.com/oam-dev/kubevela-core-api v1.7.8-0.20260116042113-3461775e5e3f // for main module github.com/pkg/errors v0.9.1 @@ -16,10 +16,13 @@ require ( // for sub-module // require github.com/kubevela/vela-go-sdk v0.0.0-20230309022604-cd431bb25a9a -require github.com/kubevela/workflow v0.6.3-0.20250717221743-56b80cee4121 +require ( + github.com/kubevela/pkg v1.9.3-0.20251028181209-ef6824214171 + github.com/kubevela/workflow v0.6.3-0.20251125110424-924e73add777 // indirect +) require ( - cuelang.org/go v0.9.2 // indirect + cuelang.org/go v0.14.1 // indirect dario.cat/mergo v1.0.0 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect @@ -58,7 +61,6 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.10 // indirect - github.com/kubevela/pkg v1.9.3-0.20250625225831-a2894a62a307 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -67,14 +69,14 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/oam-dev/cluster-gateway v1.9.2-0.20250629203450-2b04dd452b7a // indirect - github.com/oam-dev/terraform-controller v0.8.0 // indirect + github.com/oam-dev/terraform-controller v0.8.1-0.20250707044258-c0557127de25 // indirect github.com/openshift/library-go v0.0.0-20230327085348-8477ec72b725 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.7 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.etcd.io/etcd/api/v3 v3.5.16 // indirect @@ -92,14 +94,14 @@ require ( go.uber.org/automaxprocs v1.5.3 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.37.0 // indirect + golang.org/x/crypto v0.40.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/oauth2 v0.29.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/term v0.31.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.10.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect diff --git a/pkg/definition/gen_sdk/_scaffold/go/go.sum b/pkg/definition/gen_sdk/_scaffold/go/go.sum index a84111a23..60156872c 100644 --- a/pkg/definition/gen_sdk/_scaffold/go/go.sum +++ b/pkg/definition/gen_sdk/_scaffold/go/go.sum @@ -20,7 +20,7 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crossplane/crossplane-runtime v1.16.0 h1:lz+l0wEB3qowdTmN7t0PZkfuNSvfOoEhQrEYFbYqMow= github.com/crossplane/crossplane-runtime v1.16.0/go.mod h1:Pz2tdGVMF6KDGzHZOkvKro0nKc8EzK0sb/nSA7pH4Dc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -31,8 +31,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw= -github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I= +github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -62,8 +62,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= -github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -114,10 +112,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kubevela/cue v0.4.4-0.20221107123854-a976b0e340be h1:0xj/Rh4yVy54mUD2nLmAuN1AYgBkkHxBh4PoLGbIg5g= github.com/kubevela/cue v0.4.4-0.20221107123854-a976b0e340be/go.mod h1:Ya12qn7FZc+LSN0qgEhzEpnzQsvnGHVgoDrqe9i3eNg= -github.com/kubevela/pkg v1.9.3-0.20250625225831-a2894a62a307 h1:6vebFO0h5vU/0gSol3l/9KlgZeuZzYhl3/DlDr0jI6E= -github.com/kubevela/pkg v1.9.3-0.20250625225831-a2894a62a307/go.mod h1:P1yK32LmSs+NRjGu3Wu45VeCeKgIXiRg4qItN1MbgA8= -github.com/kubevela/workflow v0.6.3-0.20250717221743-56b80cee4121 h1:clU2P7FyrhLm1l/xviiLO1Cen00ZI01oOfPxAOoMi0w= -github.com/kubevela/workflow v0.6.3-0.20250717221743-56b80cee4121/go.mod h1:79KSLzfgBnJboWgxy5P/1GCc2ZUOLEYlF+vS4xQ3FNo= +github.com/kubevela/pkg v1.9.3-0.20251028181209-ef6824214171 h1:Ts3UWI0GNxuGtLlIVy/VHtkzKqSM8JC7seuJs58OzjI= +github.com/kubevela/pkg v1.9.3-0.20251028181209-ef6824214171/go.mod h1:EmM4VIyU7KxDmPBq9hG4GpSZbGwiM76/W/8paLBk8wY= +github.com/kubevela/workflow v0.6.3-0.20251125110424-924e73add777 h1:WM97lR7pW+ZMdlOxaEq/dhJfKTh65EXvsYq8pYsGnFo= +github.com/kubevela/workflow v0.6.3-0.20251125110424-924e73add777/go.mod h1:sBIs7uzPGZgJDMdWpFAYhMFW6mPaxIPd1jW6VWB5QRw= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -139,10 +137,10 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/oam-dev/cluster-gateway v1.9.2-0.20250629203450-2b04dd452b7a h1:DRcSDrLv1en8j5ESR+LR0feGLuyT9M15HJgiMn3sFZs= github.com/oam-dev/cluster-gateway v1.9.2-0.20250629203450-2b04dd452b7a/go.mod h1:ZIYRoiy4He22db8XiWTAh7QecIt5QWiQNacCZg1zbbY= -github.com/oam-dev/kubevela-core-api v1.7.8-0.20250930174210-fb3adce5e9f6 h1:nZVplulZS9P7v6FyELWfLMZv9v6sMWu9+80DHixq7jg= -github.com/oam-dev/kubevela-core-api v1.7.8-0.20250930174210-fb3adce5e9f6/go.mod h1:PaR1ddEgEx7gYh3pzlyuIXWOKDEp11fjb+JYvyyg6mQ= -github.com/oam-dev/terraform-controller v0.8.0 h1:/881bAELCsceSj+Zh3Nsu8Ym5N7D5Ho0HsWzO+TDc1w= -github.com/oam-dev/terraform-controller v0.8.0/go.mod h1:ydc9iHjgLzwuWB+MlE2vRA8gOZsif0T4E2FL34K8CZ4= +github.com/oam-dev/kubevela-core-api v1.7.8-0.20260116042113-3461775e5e3f h1:6FT3HZf2x6EOHUnrCb7d3TcOcS1+Ea8wPYA7rW2deDw= +github.com/oam-dev/kubevela-core-api v1.7.8-0.20260116042113-3461775e5e3f/go.mod h1:tKPhhyuCK7EADtf4f8GGAB4mtxZYSxgWmdMD6a0p7d8= +github.com/oam-dev/terraform-controller v0.8.1-0.20250707044258-c0557127de25 h1:J5byd7UtaHZWDMcFA1VoMos+kbEpe3LcitWVmpcJmdI= +github.com/oam-dev/terraform-controller v0.8.1-0.20250707044258-c0557127de25/go.mod h1:+QfZ/EBzdrGZcxGIt/I9OTDwq4kH/+aCD6Mh0yrrGr8= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= @@ -166,19 +164,20 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b h1:zd/2RNzIRkoGGMjE+YIsZ85CnDIz672JK2F3Zl4vux4= -github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b/go.mod h1:KjY0wibdYKc4DYkerHSbguaf3JeIPGhNJBp2BNiFH78= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/protocolbuffers/txtpbfmt v0.0.0-20250627152318-f293424e46b5 h1:WWs1ZFnGobK5ZXNu+N9If+8PDNVB9xAqrib/stUXsV4= +github.com/protocolbuffers/txtpbfmt v0.0.0-20250627152318-f293424e46b5/go.mod h1:BnHogPTyzYAReeQLZrOxyxzS739DaTNtTvohVdbENmA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -239,8 +238,8 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -249,34 +248,34 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/application.go b/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/application.go index 0f0b323d2..7119a8202 100644 --- a/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/application.go +++ b/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/application.go @@ -22,7 +22,7 @@ import ( "k8s.io/apimachinery/pkg/util/json" "sigs.k8s.io/yaml" - workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1" + workflowv1alpha1 "github.com/kubevela/pkg/apis/oam/v1alpha1" "github.com/oam-dev/kubevela-core-api/apis/core.oam.dev/common" "github.com/oam-dev/kubevela-core-api/apis/core.oam.dev/v1beta1" diff --git a/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/catalog.go b/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/catalog.go index 30ef61115..72076014b 100644 --- a/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/catalog.go +++ b/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/catalog.go @@ -19,7 +19,7 @@ package common import ( "github.com/kubevela/vela-go-sdk/pkg/apis" - workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1" + workflowv1alpha1 "github.com/kubevela/pkg/apis/oam/v1alpha1" "github.com/oam-dev/kubevela-core-api/apis/core.oam.dev/common" "github.com/oam-dev/kubevela-core-api/apis/core.oam.dev/v1beta1" diff --git a/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/types.go b/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/types.go index 6c1bceceb..4af0e23b6 100644 --- a/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/types.go +++ b/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/types.go @@ -17,7 +17,7 @@ limitations under the License. package apis import ( - workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1" + workflowv1alpha1 "github.com/kubevela/pkg/apis/oam/v1alpha1" "github.com/oam-dev/kubevela-core-api/apis/core.oam.dev/common" "github.com/oam-dev/kubevela-core-api/apis/core.oam.dev/v1beta1" diff --git a/pkg/definition/gen_sdk/go.go b/pkg/definition/gen_sdk/go.go index bcca71190..d3aba449d 100644 --- a/pkg/definition/gen_sdk/go.go +++ b/pkg/definition/gen_sdk/go.go @@ -85,7 +85,7 @@ var ( DefinitionKindToStatement = map[string]*j.Statement{ v1beta1.ComponentDefinitionKind: j.Qual("common", "ApplicationComponent"), v1beta1.TraitDefinitionKind: j.Qual("common", "ApplicationTrait"), - v1beta1.WorkflowStepDefinitionKind: j.Qual("github.com/kubevela/workflow/api/v1alpha1", "WorkflowStep"), + v1beta1.WorkflowStepDefinitionKind: j.Qual("github.com/kubevela/pkg/apis/oam/v1alpha1", "WorkflowStep"), v1beta1.PolicyDefinitionKind: j.Qual("v1beta1", "AppPolicy"), } ) @@ -447,7 +447,7 @@ func (m *GoDefModifier) genCommonFunc() []*j.Statement { j.Id("Timeout"): j.Id(m.defFuncReceiver).Dot("Base").Dot("Timeout"), j.Id("Meta"): j.Id(m.defFuncReceiver).Dot("Base").Dot("Meta"), } - builderDict[j.Id("WorkflowStepBase")] = j.Qual("github.com/kubevela/workflow/api/v1alpha1", "WorkflowStepBase").Values(workflowStepBaseDict) + builderDict[j.Id("WorkflowStepBase")] = j.Qual("github.com/kubevela/pkg/apis/oam/v1alpha1", "WorkflowStepBase").Values(workflowStepBaseDict) builderDict[j.Id("SubSteps")] = j.Id("subSteps") } else { builderDict[j.Id("Type")] = j.Add(typeName) @@ -479,7 +479,7 @@ func (m *GoDefModifier) genCommonFunc() []*j.Statement { g.Add(j.For(j.List(j.Id("_"), j.Id("subStep")).Op(":=").Range().Id(m.defFuncReceiver).Dot("Base").Dot("SubSteps")).Block( j.Id("_subSteps").Op("=").Append(j.Id("_subSteps"), j.Id("subStep").Dot("Build").Call()), )) - g.Add(j.Id("subSteps").Op(":=").Make(j.Index().Qual("github.com/kubevela/workflow/api/v1alpha1", "WorkflowStepBase"), j.Lit(0))) + g.Add(j.Id("subSteps").Op(":=").Make(j.Index().Qual("github.com/kubevela/pkg/apis/oam/v1alpha1", "WorkflowStepBase"), j.Lit(0))) g.Add(j.For(j.List(j.Id("_"), j.Id("_s").Op(":=").Range().Id("_subSteps"))).Block( j.Id("subSteps").Op("=").Append(j.Id("subSteps"), j.Id("_s").Dot("WorkflowStepBase")), )) @@ -533,7 +533,7 @@ func (m *GoDefModifier) genFromFunc() []*j.Statement { params := DefinitionKindToStatement[kind] if sub { funcName = "FromWorkflowSubStep" - params = j.Qual("github.com/kubevela/workflow/api/v1alpha1", "WorkflowStepBase") + params = j.Qual("github.com/kubevela/pkg/apis/oam/v1alpha1", "WorkflowStepBase") } return j.Func(). Params(j.Id(m.defFuncReceiver).Add(m.defStructPointer)). @@ -581,7 +581,7 @@ func (m *GoDefModifier) genFromFunc() []*j.Statement { j.Return(j.Id(m.defFuncReceiver).Dot("From"+DefinitionKindToPascal[kind]).Call(j.Id("from"))), ) fromSubFunc := j.Func().Id("FromWorkflowSubStep"). - Params(j.Id("from").Qual("github.com/kubevela/workflow/api/v1alpha1", "WorkflowStepBase")).Params(j.Qual("apis", DefinitionKindToPascal[kind]), j.Error()). + Params(j.Id("from").Qual("github.com/kubevela/pkg/apis/oam/v1alpha1", "WorkflowStepBase")).Params(j.Qual("apis", DefinitionKindToPascal[kind]), j.Error()). Block( j.Id(m.defFuncReceiver).Op(":=").Op("&").Id(m.defStructName).Values(j.Dict{}), j.Return(j.Id(m.defFuncReceiver).Dot("FromWorkflowSubStep").Call(j.Id("from"))), @@ -677,8 +677,8 @@ func (m *GoDefModifier) genBaseSetterFunc() []*j.Statement { }{ v1beta1.ComponentDefinitionKind: { {funcName: "DependsOn", argName: "dependsOn", argType: j.Index().String()}, - {funcName: "Inputs", argName: "input", argType: j.Qual("github.com/kubevela/workflow/api/v1alpha1", "StepInputs")}, - {funcName: "Outputs", argName: "output", argType: j.Qual("github.com/kubevela/workflow/api/v1alpha1", "StepOutputs")}, + {funcName: "Inputs", argName: "input", argType: j.Qual("github.com/kubevela/pkg/apis/oam/v1alpha1", "StepInputs")}, + {funcName: "Outputs", argName: "output", argType: j.Qual("github.com/kubevela/pkg/apis/oam/v1alpha1", "StepOutputs")}, {funcName: "AddDependsOn", argName: "dependsOn", argType: j.String(), isAppend: true, dst: j.Dot("DependsOn")}, }, v1beta1.WorkflowStepDefinitionKind: { @@ -686,8 +686,8 @@ func (m *GoDefModifier) genBaseSetterFunc() []*j.Statement { {funcName: "Alias", argName: "alias", argType: j.String(), dst: j.Dot("Meta").Dot("Alias")}, {funcName: "Timeout", argName: "timeout", argType: j.String()}, {funcName: "DependsOn", argName: "dependsOn", argType: j.Index().String()}, - {funcName: "Inputs", argName: "input", argType: j.Qual("github.com/kubevela/workflow/api/v1alpha1", "StepInputs")}, - {funcName: "Outputs", argName: "output", argType: j.Qual("github.com/kubevela/workflow/api/v1alpha1", "StepOutputs")}, + {funcName: "Inputs", argName: "input", argType: j.Qual("github.com/kubevela/pkg/apis/oam/v1alpha1", "StepInputs")}, + {funcName: "Outputs", argName: "output", argType: j.Qual("github.com/kubevela/pkg/apis/oam/v1alpha1", "StepOutputs")}, }, } baseFuncs := make([]*j.Statement, 0) diff --git a/vela-templates/definitions/internal/trait/affinity.cue b/vela-templates/definitions/internal/trait/affinity.cue index 4437f0f26..69158237a 100644 --- a/vela-templates/definitions/internal/trait/affinity.cue +++ b/vela-templates/definitions/internal/trait/affinity.cue @@ -123,20 +123,21 @@ template: { #podAffinityTerm: { labelSelector?: #labelSelector + namespace?: string namespaces?: [...string] topologyKey: string namespaceSelector?: #labelSelector } - #nodeSelecor: { + #nodeSelector: { key: string operator: *"In" | "NotIn" | "Exists" | "DoesNotExist" | "Gt" | "Lt" values?: [...string] } #nodeSelectorTerm: { - matchExpressions?: [...#nodeSelecor] - matchFields?: [...#nodeSelecor] + matchExpressions?: [...#nodeSelector] + matchFields?: [...#nodeSelector] } parameter: {