Files
kubevela/pkg/definition/defkit/trait_test.go
T
4010da6765 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>
2026-02-20 14:09:11 -08:00

1379 lines
48 KiB
Go

/*
Copyright 2025 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package defkit_test
import (
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
var _ = Describe("TraitDefinition", func() {
Context("Basic Builder Methods", func() {
It("should set name, type, and description correctly", func() {
trait := defkit.NewTrait("scaler").
Description("Manually scale K8s pod for your workload.").
AppliesTo("deployments.apps", "statefulsets.apps").
PodDisruptive(false)
Expect(trait.DefName()).To(Equal("scaler"))
Expect(trait.GetName()).To(Equal("scaler"))
Expect(trait.DefType()).To(Equal(defkit.DefinitionTypeTrait))
Expect(trait.GetDescription()).To(Equal("Manually scale K8s pod for your workload."))
Expect(trait.GetAppliesToWorkloads()).To(Equal([]string{"deployments.apps", "statefulsets.apps"}))
Expect(trait.IsPodDisruptive()).To(BeFalse())
})
It("should set ConflictsWith correctly", func() {
trait := defkit.NewTrait("hpa").
Description("HPA scaler trait.").
AppliesTo("deployments.apps").
ConflictsWith("scaler", "cpuscaler")
Expect(trait.GetConflictsWith()).To(Equal([]string{"scaler", "cpuscaler"}))
})
It("should set Stage correctly", func() {
trait := defkit.NewTrait("expose").
Description("Expose service.").
Stage("PostDispatch").
AppliesTo("deployments.apps")
Expect(trait.GetStage()).To(Equal("PostDispatch"))
})
It("should add parameters with Params and Param methods", func() {
trait := defkit.NewTrait("test").
AppliesTo("deployments.apps").
Params(
defkit.Int("replicas").Default(1),
defkit.String("image"),
).
Param(defkit.Bool("enabled").Default(true))
params := trait.GetParams()
Expect(params).To(HaveLen(3))
Expect(params[0].Name()).To(Equal("replicas"))
Expect(params[1].Name()).To(Equal("image"))
Expect(params[2].Name()).To(Equal("enabled"))
})
It("should set Labels correctly", func() {
labels := map[string]string{
"ui-hidden": "true",
"custom": "value",
}
trait := defkit.NewTrait("labeled").
Description("Trait with labels").
AppliesTo("deployments.apps").
Labels(labels)
gotLabels := trait.GetLabels()
Expect(gotLabels["ui-hidden"]).To(Equal("true"))
Expect(gotLabels["custom"]).To(Equal("value"))
})
It("should set imports correctly", func() {
trait := defkit.NewTrait("import-trait").
WithImports("strconv", "strings").
AppliesTo("deployments.apps")
Expect(trait.GetImports()).To(Equal([]string{"strconv", "strings"}))
})
})
Context("Status and Health Methods", func() {
It("should set custom status and health policy", func() {
trait := defkit.NewTrait("status-trait").
AppliesTo("deployments.apps").
CustomStatus("message: \"Ready\"").
HealthPolicy("isHealth: true")
Expect(trait.GetCustomStatus()).To(Equal("message: \"Ready\""))
Expect(trait.GetHealthPolicy()).To(Equal("isHealth: true"))
})
It("should set health policy from expression", func() {
trait := defkit.NewTrait("health").
AppliesTo("deployments.apps").
HealthPolicyExpr(defkit.Health().Condition("Ready").IsTrue())
policy := trait.GetHealthPolicy()
Expect(policy).NotTo(BeEmpty())
Expect(policy).To(ContainSubstring("isHealth"))
})
})
Context("RawCUE Methods", func() {
It("should set raw CUE with complete definition", func() {
rawCUE := `scaler: {
type: "trait"
description: "Raw CUE trait"
}
template: {
patch: spec: replicas: parameter.replicas
parameter: replicas: *1 | int
}`
trait := defkit.NewTrait("scaler").RawCUE(rawCUE)
Expect(trait.HasRawCUE()).To(BeTrue())
Expect(trait.GetRawCUE()).To(Equal(rawCUE))
})
It("should set template block correctly", func() {
templateBlock := `
#PatchParams: {
containerName: *"" | string
command: *null | [...string]
}
patch: spec: template: spec: containers: [{name: parameter.containerName}]
parameter: #PatchParams
`
trait := defkit.NewTrait("command").
Description("Add command").
AppliesTo("deployments.apps").
TemplateBlock(templateBlock)
Expect(trait.HasTemplateBlock()).To(BeTrue())
Expect(trait.GetTemplateBlock()).To(Equal(templateBlock))
})
})
Context("Helper Methods", func() {
It("should add helper definitions", func() {
probeSchema := defkit.Struct("probe").Fields(
defkit.Field("path", defkit.ParamTypeString).Default("/health"),
defkit.Field("port", defkit.ParamTypeInt).Default(8080),
)
trait := defkit.NewTrait("health-probe").
Description("Add health probes").
AppliesTo("deployments.apps").
Helper("HealthProbe", probeSchema)
helpers := trait.GetHelperDefinitions()
Expect(helpers).To(HaveLen(1))
Expect(helpers[0].GetName()).To(Equal("HealthProbe"))
})
})
Context("ToCue Generation - Metadata", func() {
It("should generate complete CUE definition with header", func() {
trait := defkit.NewTrait("scaler").
Description("Scale workloads").
AppliesTo("deployments.apps").
Params(defkit.Int("replicas").Default(1).Required().Description("Number of replicas"))
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`scaler: {`))
Expect(cue).To(ContainSubstring(`type: "trait"`))
Expect(cue).To(ContainSubstring(`description: "Scale workloads"`))
// podDisruptive: false is not emitted (it's the default)
Expect(cue).NotTo(ContainSubstring(`podDisruptive: false`))
Expect(cue).To(ContainSubstring(`appliesToWorkloads: ["deployments.apps"]`))
})
It("should quote trait names with special characters", func() {
trait := defkit.NewTrait("my-trait-v1.0").
Description("Trait with special characters").
AppliesTo("deployments.apps")
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`"my-trait-v1.0": {`))
})
It("should include labels in CUE output", func() {
trait := defkit.NewTrait("labeled").
Description("Trait with labels").
AppliesTo("deployments.apps").
Labels(map[string]string{"ui-hidden": "true"})
cue := trait.ToCue()
// CUE formatter may output labels inline or as block
Expect(cue).To(ContainSubstring(`labels:`))
Expect(cue).To(ContainSubstring(`"ui-hidden": "true"`))
})
It("should include stage in CUE output", func() {
trait := defkit.NewTrait("staged").
Description("Trait with stage").
AppliesTo("deployments.apps").
Stage("PostDispatch")
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`stage:`))
Expect(cue).To(ContainSubstring(`"PostDispatch"`))
})
It("should include conflictsWith in CUE output", func() {
trait := defkit.NewTrait("exclusive").
Description("Exclusive trait").
AppliesTo("deployments.apps").
ConflictsWith("other-trait", "incompatible-trait")
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`conflictsWith: ["other-trait", "incompatible-trait"]`))
})
It("should include imports in CUE output", func() {
trait := defkit.NewTrait("with-imports").
Description("Trait with imports").
AppliesTo("deployments.apps").
WithImports("strconv", "strings")
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`import (`))
Expect(cue).To(ContainSubstring(`"strconv"`))
Expect(cue).To(ContainSubstring(`"strings"`))
})
})
Context("ToCue Generation - Status", func() {
It("should include status block with customStatus and healthPolicy", func() {
trait := defkit.NewTrait("health-aware").
Description("Health aware trait").
AppliesTo("deployments.apps").
CustomStatus("message: \"Running\"").
HealthPolicy("isHealth: output.status.ready == true")
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`status: {`))
Expect(cue).To(ContainSubstring(`customStatus:`))
Expect(cue).To(ContainSubstring(`message: "Running"`))
Expect(cue).To(ContainSubstring(`healthPolicy:`))
Expect(cue).To(ContainSubstring(`isHealth: output.status.ready == true`))
})
})
Context("ToCue Generation - Template with Patch", func() {
It("should generate patch block from Template API", func() {
replicas := defkit.Int("replicas").Default(1)
trait := defkit.NewTrait("scaler").
Description("Manually scale K8s pod.").
AppliesTo("deployments.apps", "statefulsets.apps").
PodDisruptive(false).
Params(replicas).
Template(func(tpl *defkit.Template) {
tpl.PatchStrategy("retainKeys").
Patch().Set("spec.replicas", replicas)
})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`// +patchStrategy=retainKeys`))
Expect(cue).To(ContainSubstring(`patch:`))
Expect(cue).To(ContainSubstring(`spec:`))
Expect(cue).To(ContainSubstring(`replicas:`))
})
})
Context("ToCue Generation - Template with Outputs", func() {
It("should generate outputs block from Template API", func() {
trait := defkit.NewTrait("expose").
Description("Expose workload via Service").
AppliesTo("deployments.apps").
Stage("PostDispatch").
Params(
defkit.Int("port").Default(80).Description("Service port"),
defkit.String("type").Default("ClusterIP").Description("Service type"),
).
Template(func(tpl *defkit.Template) {
service := defkit.NewResource("v1", "Service").
Set("metadata.name", defkit.VelaCtx().Name()).
Set("spec.type", defkit.ParamRef("type")).
Set("spec.ports[0].port", defkit.ParamRef("port"))
tpl.Outputs("service", service)
})
cue := trait.ToCue()
// CUE formatter may inline single outputs
Expect(cue).To(ContainSubstring(`outputs:`))
Expect(cue).To(ContainSubstring(`service:`))
Expect(cue).To(ContainSubstring(`apiVersion: "v1"`))
Expect(cue).To(ContainSubstring(`kind:`))
Expect(cue).To(ContainSubstring(`"Service"`))
Expect(cue).To(ContainSubstring(`metadata: name: context.name`))
})
})
Context("ToCue Generation - RawCUE", func() {
It("should handle raw CUE with top-level template block", func() {
rawCUE := `scaler: {
type: "trait"
description: "Raw CUE trait"
}
template: {
patch: spec: replicas: parameter.replicas
parameter: replicas: *1 | int
}`
trait := defkit.NewTrait("scaler").RawCUE(rawCUE)
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`scaler:`))
Expect(cue).To(ContainSubstring(`template:`))
Expect(cue).To(ContainSubstring(`patch: spec: replicas: parameter.replicas`))
})
It("should handle raw CUE without top-level template (partial)", func() {
rawCUE := `patch: spec: replicas: parameter.replicas
parameter: replicas: *1 | int`
trait := defkit.NewTrait("partial").
Description("Partial raw CUE trait").
AppliesTo("deployments.apps").
RawCUE(rawCUE)
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`partial:`))
Expect(cue).To(ContainSubstring(`type:`))
Expect(cue).To(ContainSubstring(`"trait"`))
Expect(cue).To(ContainSubstring(`template:`))
})
It("should handle TemplateBlock", func() {
templateBlock := `#PatchParams: {
containerName: *"" | string
}
patch: spec: template: spec: containers: [{name: parameter.containerName}]
parameter: #PatchParams`
trait := defkit.NewTrait("command").
Description("Add command").
AppliesTo("deployments.apps").
TemplateBlock(templateBlock)
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`command: {`))
Expect(cue).To(ContainSubstring(`template: {`))
Expect(cue).To(ContainSubstring(`#PatchParams:`))
})
It("should preserve raw CUE content when it has template block", func() {
rawCUE := `myname: {
type: "trait"
}
template: {
patch: spec: replicas: 1
}`
trait := defkit.NewTrait("myname").RawCUE(rawCUE)
cue := trait.ToCue()
// Raw CUE with top-level template block is returned as-is (formatted)
Expect(cue).To(ContainSubstring(`myname:`))
Expect(cue).To(ContainSubstring(`template:`))
Expect(cue).To(ContainSubstring(`patch:`))
})
})
Context("ToCue Generation - Parameters", func() {
It("should generate parameter block", func() {
trait := defkit.NewTrait("params").
AppliesTo("deployments.apps").
Params(
defkit.Int("replicas").Default(1).Description("Number of replicas"),
defkit.String("image").Required().Description("Container image"),
)
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`parameter: {`))
Expect(cue).To(ContainSubstring(`// +usage=Number of replicas`))
Expect(cue).To(ContainSubstring(`replicas: *1 | int`))
Expect(cue).To(ContainSubstring(`// +usage=Container image`))
Expect(cue).To(ContainSubstring(`image: string`))
})
})
Context("ToYAML Generation", func() {
It("should generate valid Kubernetes YAML", func() {
trait := defkit.NewTrait("scaler").
Description("Scale workload.").
AppliesTo("deployments.apps").
PodDisruptive(false).
Params(defkit.Int("replicas").Default(1))
yamlBytes, err := trait.ToYAML()
Expect(err).NotTo(HaveOccurred())
yamlStr := string(yamlBytes)
Expect(yamlStr).To(ContainSubstring("kind: TraitDefinition"))
Expect(yamlStr).To(ContainSubstring("name: scaler"))
Expect(yamlStr).To(ContainSubstring("appliesToWorkloads:"))
})
It("should include conflictsWith and stage in YAML", func() {
trait := defkit.NewTrait("exclusive").
Description("Exclusive trait").
AppliesTo("deployments.apps").
ConflictsWith("other-trait").
Stage("PreDispatch").
PodDisruptive(true)
yamlBytes, err := trait.ToYAML()
Expect(err).NotTo(HaveOccurred())
yamlStr := string(yamlBytes)
Expect(yamlStr).To(ContainSubstring("conflictsWith:"))
Expect(yamlStr).To(ContainSubstring("stage: PreDispatch"))
Expect(yamlStr).To(ContainSubstring("podDisruptive: true"))
})
})
Context("Registry Integration", func() {
It("should register and retrieve traits", func() {
defkit.Clear() // Reset registry
trait1 := defkit.NewTrait("scaler").Description("Scale").AppliesTo("deployments.apps")
trait2 := defkit.NewTrait("expose").Description("Expose").AppliesTo("deployments.apps")
comp := defkit.NewComponent("webservice").Description("Component")
defkit.Register(trait1)
defkit.Register(trait2)
defkit.Register(comp)
Expect(defkit.Count()).To(Equal(3))
Expect(defkit.Traits()).To(HaveLen(2))
Expect(defkit.Components()).To(HaveLen(1))
defkit.Clear() // Clean up
})
})
Context("ToCue with Patch operations", func() {
It("should generate patch with SpreadIf", func() {
labels := defkit.Object("labels")
trait := defkit.NewTrait("label").
Description("Add labels").
AppliesTo("deployments.apps").
Params(labels).
Template(func(tpl *defkit.Template) {
tpl.Patch().
SpreadIf(labels.IsSet(), "spec.template.metadata.labels", labels)
})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("patch:"))
Expect(cue).To(ContainSubstring("labels"))
})
It("should generate patch with ForEach", func() {
labels := defkit.Object("labels")
trait := defkit.NewTrait("label").
Description("Add labels").
AppliesTo("deployments.apps").
Params(labels).
Template(func(tpl *defkit.Template) {
tpl.Patch().ForEach(labels, "spec.template.metadata.labels")
})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("patch:"))
})
It("should generate patch with If/EndIf block", func() {
enabled := defkit.Bool("enabled")
replicas := defkit.Int("replicas")
trait := defkit.NewTrait("conditional").
Description("Conditional scaling").
AppliesTo("deployments.apps").
Params(enabled, replicas).
Template(func(tpl *defkit.Template) {
cond := defkit.Eq(enabled, defkit.Lit(true))
tpl.Patch().
If(cond).
Set("spec.replicas", replicas).
EndIf()
})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("patch:"))
Expect(cue).To(ContainSubstring("if"))
})
It("should generate patch with PatchKey", func() {
containerName := defkit.String("containerName")
image := defkit.String("image")
trait := defkit.NewTrait("sidecar").
Description("Add sidecar container").
AppliesTo("deployments.apps").
Params(containerName, image).
Template(func(tpl *defkit.Template) {
container := defkit.NewArrayElement().
Set("name", containerName).
Set("image", image)
tpl.Patch().PatchKey("spec.template.spec.containers", "name", container)
})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("patch:"))
Expect(cue).To(ContainSubstring("patchKey"))
})
It("should generate patch with Passthrough", func() {
trait := defkit.NewTrait("json-patch").
Description("Apply JSON patch").
AppliesTo("*").
Params(defkit.OpenStruct()).
Template(func(tpl *defkit.Template) {
tpl.Patch().Passthrough()
})
cue := trait.ToCue()
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"))
})
})
})