Chore: fix trait definition translation discrepancies (#7044)

* Fix: Enhance CUE generation for optional fields in collection operations

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: Add tests for optional fields and conditional logic in trait definitions

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Add namespace field to podAffinityTerm in affinity trait definition

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Add support for ForEachMap operation in CUE generation

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Add tests for ForEachMap let bindings and custom rendering in trait definitions

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Add optional description fields for PatchContainer and enhance parameter generation logic

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Update parameter access to bracket notation in CUE generation and tests

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Implement PatchStrategyAnnotation for CUE generation and enhance condition hoisting logic

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Add SpreadAll operation for array patches and enhance IntParam constraints handling

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Refactor writeSpreadAllOp to improve condition handling and element processing

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Correct nodeSelector spelling and references in affinity trait definition

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Add optional namespace field to podAffinityTerm in affinity trait definition

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* test: re-trigger test pipelines

Signed-off-by: Chaitanya Reddy Onteddu <chaitanyareddy0702@gmail.com>
Co-authored-by: Chaitanya Reddy Onteddu <chaitanyareddy0702@gmail.com>

* ci: retrigger checks

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Chaitanya Reddy Onteddu <chaitanyareddy0702@gmail.com>

* Fix: Update workflow API import paths to use the correct package location

Signed-off-by: Chaitanya Reddy Onteddu <chaitanyareddy0702@gmail.com>
Co-authored-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Update dependency versions in go.mod for improved compatibility

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Chaitanya Reddy Onteddu <chaitanyareddy0702@gmail.com>

* ci: retrigger checks

Signed-off-by: Chaitanya Reddy Onteddu <chaitanyareddy0702@gmail.com>
Co-authored-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Add tests for SpreadAll operation and conditional handling in trait definitions

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Enhance optional field handling for non-string conditions in PatchContainer

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Implement ForceOptional parameter handling to retain optionality with defaults

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Add PatchStrategy field to PatchContainerConfig and update related logic in trait definitions

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Implement PatchFieldBuilder for fluent API construction of PatchContainerField

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Fix: Update test descriptions and enhance CUE generation for PatchFields with NotEmpty and IsSet conditions

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

---------

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Signed-off-by: Chaitanya Reddy Onteddu <chaitanyareddy0702@gmail.com>
Co-authored-by: Chaitanya Reddy Onteddu <chaitanyareddy0702@gmail.com>
This commit is contained in:
Ayush Kumar
2026-02-20 14:09:11 -08:00
committed by GitHub
co-authored by Chaitanya Reddy Onteddu Ayush Kumar
parent 3cd345c243
commit 4010da6765
19 changed files with 2183 additions and 121 deletions
@@ -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: {
+111 -21
View File
@@ -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))
}
+17
View File
@@ -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() {
+16
View File
@@ -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 != _|_).
@@ -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"))
})
})
})
+17 -5
View File
@@ -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 {
+13
View File
@@ -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() {
+50
View File
@@ -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 {
+169
View File
@@ -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=<value> before the patch block (e.g., "open")
}
// --- Let Binding Support ---
+535 -4
View File
@@ -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: {}"))
})
})
+268 -20
View File
@@ -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))
+821 -2
View File
@@ -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"))
})
})
})
+16 -14
View File
@@ -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
+36 -37
View File
@@ -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=
@@ -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"
@@ -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"
@@ -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"
+9 -9
View File
@@ -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)
@@ -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: {