Files
kubevela/pkg/definition/defkit/patch_container_test.go
Ayush Kumar 012a134829 Feat: extend fluent builder API validator patterns (#7092)
* Feat: Add NotEmpty and NegativePattern constraints to StringParam; implement Closed for MapParam

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

* feat: add validation support for array and map parameters

- Introduced validators for ArrayParam and MapParam, allowing for cross-field validation within structured parameters.
- Added NonEmpty validation for ArrayParam to ensure arrays are not empty.
- Implemented ConditionalStructOp for conditional struct generation based on specified conditions.
- Created a new Validator type for defining validation rules with optional guard conditions.
- Added tests for various validation scenarios, including mutual exclusion and conditional parameters.
- Enhanced the CUE generation logic to incorporate new validation features and conditional struct handling.

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

* feat: extend fluent API with new scoped field conditions and improve validation checks

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

* feat: enhance ArrayParam with NotEmpty constraint and update ScopedField documentation

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

* feat: rename ScopedField to LocalField for improved clarity in condition building

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

* feat: refactor local field conditions to use RegexMatch and streamline condition building

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

* feat: simplify condition handling by removing unused comparison types and refactoring NotCondition usage

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

* refactor: remove unused raw CUE block handling from baseDefinition and ComponentDefinition

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

* test: update condition handling in parameter tests to use NotExpr and Cond methods

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

* refactor: remove negative pattern handling from StringParam and related tests

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

* feat: add support for emitting raw header blocks in template generation

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

* refactor: remove non-empty check from ArrayParam and update related tests

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

* refactor: convert parameter constraint tests to use Ginkgo and Gomega for improved readability and maintainability

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

* feat: extend fluent APIs for OAM with new CUE generation tests and condition evaluations

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

* refactor: clean up whitespace in component, cuegen, expr, param, and resource files

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

* feat: enhance CUE generation by adding support for new expression types and iterator references

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

* refactor: remove unnecessary whitespace in cuegen.go

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

* refactor: rename LenOf to LenOfExpr for clarity in comparison methods

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

* feat: enhance CUE generation and validation for string arrays in ArrayParam

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

* ci: retrigger checks

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

---------

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Signed-off-by: Ayush Kumar <aykumar@guidewire.com>
Co-authored-by: Ayush Kumar <aykumar@guidewire.com>
2026-04-09 14:25:06 +01:00

965 lines
37 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("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("")
command := defkit.StringList("command")
args := defkit.StringList("args")
trait := defkit.NewTrait("command").
Description("Override container command and args").
AppliesTo("deployments.apps", "statefulsets.apps").
Params(containerName, command, args).
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
PatchFields: []defkit.PatchContainerField{
{ParamName: "command", TargetField: "command", PatchStrategy: "replace"},
{ParamName: "args", TargetField: "args", PatchStrategy: "replace"},
},
})
})
cue := trait.ToCue()
// Verify trait metadata block
Expect(cue).To(ContainSubstring(`command: {`))
Expect(cue).To(ContainSubstring(`type: "trait"`))
Expect(cue).To(ContainSubstring(`description: "Override container command and args"`))
Expect(cue).To(ContainSubstring(`appliesToWorkloads: ["deployments.apps", "statefulsets.apps"]`))
// Verify #PatchParams definition with all fields
Expect(cue).To(ContainSubstring(`#PatchParams: {`))
Expect(cue).To(ContainSubstring(`containerName: *"" | string`))
Expect(cue).To(ContainSubstring(`command: [...string]`))
Expect(cue).To(ContainSubstring(`args: [...string]`))
// Verify PatchContainer helper structure
Expect(cue).To(ContainSubstring(`PatchContainer: {`))
Expect(cue).To(ContainSubstring(`_params: #PatchParams`))
Expect(cue).To(ContainSubstring(`name: _params.containerName`))
Expect(cue).To(ContainSubstring(`_baseContainers: context.output.spec.template.spec.containers`))
Expect(cue).To(ContainSubstring(`_matchContainers_: [for _container_ in _baseContainers if _container_.name == name {_container_}]`))
// Verify error handling for container not found
Expect(cue).To(ContainSubstring(`if len(_matchContainers_) == 0 {`))
Expect(cue).To(ContainSubstring(`err: "container \(name) not found"`))
// Verify patch fields with strategy comments
Expect(cue).To(ContainSubstring(`// +patchStrategy=replace`))
Expect(cue).To(ContainSubstring(`command: _params.command`))
Expect(cue).To(ContainSubstring(`args: _params.args`))
// Verify patch structure with patchKey annotation
Expect(cue).To(ContainSubstring(`patch: spec: template: spec: {`))
Expect(cue).To(ContainSubstring(`// +patchKey=name`))
Expect(cue).To(ContainSubstring(`containers: [{`))
// Verify default-to-context-name logic
Expect(cue).To(ContainSubstring(`if parameter.containerName == "" {`))
Expect(cue).To(ContainSubstring(`containerName: context.name`))
Expect(cue).To(ContainSubstring(`if parameter.containerName != "" {`))
Expect(cue).To(ContainSubstring(`containerName: parameter.containerName`))
// Verify error collection
Expect(cue).To(ContainSubstring(`errs: [for c in patch.spec.template.spec.containers if c.err != _|_ {c.err}]`))
// 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")).
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: {}"))
})
})
Context("MultiContainerCheckField and MultiContainerErrMsg", func() {
It("should use default check field 'containerName' and default error message with camelCase", func() {
trait := defkit.NewTrait("default-multi-err-test").
Description("Test default multi-container error").
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()
// Default check field is "containerName"
Expect(cue).To(ContainSubstring(`if c.containerName == ""`))
Expect(cue).To(ContainSubstring(`if c.containerName != ""`))
// Default error message uses "containerName" (camelCase, matching the field name)
Expect(cue).To(ContainSubstring(`err: "containerName must be set for containers"`))
})
It("should use custom MultiContainerCheckField when set", func() {
trait := defkit.NewTrait("custom-check-field-test").
Description("Test custom check field").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
MultiContainerParam: "probes",
MultiContainerCheckField: "name",
PatchFields: []defkit.PatchContainerField{
{ParamName: "image", TargetField: "image"},
},
})
})
cue := trait.ToCue()
// Custom check field "name" instead of default "containerName"
Expect(cue).To(ContainSubstring(`if c.name == ""`))
Expect(cue).To(ContainSubstring(`if c.name != ""`))
Expect(cue).NotTo(ContainSubstring(`c.containerName == ""`))
})
It("should use custom MultiContainerErrMsg when set", func() {
trait := defkit.NewTrait("custom-err-msg-test").
Description("Test custom error message").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
MultiContainerParam: "probes",
MultiContainerCheckField: "name",
MultiContainerErrMsg: "containerName must be set when specifying startup probe for multiple containers",
PatchFields: []defkit.PatchContainerField{
{ParamName: "image", TargetField: "image"},
},
})
})
cue := trait.ToCue()
// Custom error message overrides the default
Expect(cue).To(ContainSubstring(`err: "containerName must be set when specifying startup probe for multiple containers"`))
// Default error message should NOT appear
Expect(cue).NotTo(ContainSubstring(`err: "containerName must be set for probes"`))
})
It("should use default error with MultiContainerParam name", func() {
trait := defkit.NewTrait("multi-param-err-test").
Description("Test error message includes multi param name").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
MultiContainerParam: "probes",
PatchFields: []defkit.PatchContainerField{
{ParamName: "image", TargetField: "image"},
},
})
})
cue := trait.ToCue()
// Default error message should include the multi-container param name
Expect(cue).To(ContainSubstring(`err: "containerName must be set for probes"`))
})
})
Context("writePatchParamMapping typed scalar fields", func() {
It("should map typed scalar fields unconditionally in _params block even with IsSet and no default", func() {
trait := defkit.NewTrait("typed-scalar-unconditional-test").
Description("Test typed scalar fields pass through unconditionally").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
ContainersParam: "containers",
Groups: []defkit.PatchContainerGroup{
{
TargetField: "startupProbe",
Fields: defkit.PatchFields(
defkit.PatchField("terminationGracePeriodSeconds").Int().IsSet(),
defkit.PatchField("exec").IsSet(),
),
},
},
})
})
cue := trait.ToCue()
// Int().IsSet() with no default: typed scalar should be unconditional in _params block
Expect(cue).To(ContainSubstring("terminationGracePeriodSeconds: parameter.terminationGracePeriodSeconds"))
// But untyped IsSet() with no default (e.g., exec) should remain conditional in _params block
Expect(cue).To(MatchRegexp(`if parameter\.exec != _\|_\s*\{[^}]*exec:\s*parameter\.exec`))
// Both should still be conditional in the PatchContainer body
Expect(cue).To(ContainSubstring("if _params.terminationGracePeriodSeconds != _|_"))
Expect(cue).To(ContainSubstring("if _params.exec != _|_"))
})
It("should keep untyped IsSet fields conditional in _params block", func() {
trait := defkit.NewTrait("untyped-conditional-test").
Description("Test untyped fields remain conditional").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
ContainersParam: "containers",
Groups: []defkit.PatchContainerGroup{
{
TargetField: "startupProbe",
Fields: defkit.PatchFields(
defkit.PatchField("exec").IsSet(),
defkit.PatchField("httpGet").IsSet(),
),
},
},
})
})
cue := trait.ToCue()
// Untyped IsSet() fields should still be conditional in _params block
Expect(cue).To(MatchRegexp(`if parameter\.exec != _\|_\s*\{[^}]*exec:\s*parameter\.exec`))
Expect(cue).To(MatchRegexp(`if parameter\.httpGet != _\|_\s*\{[^}]*httpGet:\s*parameter\.httpGet`))
})
It("should map typed scalar fields with default unconditionally regardless of IsSet", func() {
trait := defkit.NewTrait("typed-with-default-test").
Description("Test typed fields with default are unconditional").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
ContainersParam: "containers",
Groups: []defkit.PatchContainerGroup{
{
TargetField: "startupProbe",
Fields: defkit.PatchFields(
defkit.PatchField("initialDelaySeconds").Int().IsSet().Default("0"),
defkit.PatchField("periodSeconds").Int().IsSet().Default("10"),
),
},
},
})
})
cue := trait.ToCue()
// Typed fields with defaults are always unconditional in _params block
Expect(cue).To(ContainSubstring("initialDelaySeconds: parameter.initialDelaySeconds"))
Expect(cue).To(ContainSubstring("periodSeconds: parameter.periodSeconds"))
// Should NOT be wrapped in conditionals in _params block
Expect(cue).NotTo(MatchRegexp(`if parameter\.initialDelaySeconds[^{]*\{[^}]*initialDelaySeconds:\s*parameter\.initialDelaySeconds`))
})
})
Context("Template Let Bindings", func() {
It("should accumulate bindings in order for CUE generation", func() {
tpl := defkit.NewTemplate()
expr1 := defkit.Lit(100)
expr2 := defkit.Struct("config")
expr3 := defkit.List("items")
tpl.AddLetBinding("count", expr1)
tpl.AddLetBinding("cfg", expr2)
tpl.AddLetBinding("items", expr3)
bindings := tpl.GetLetBindings()
Expect(bindings).To(HaveLen(3))
Expect(bindings[0].Name()).To(Equal("count"))
Expect(bindings[0].Expr()).To(Equal(expr1))
Expect(bindings[1].Name()).To(Equal("cfg"))
Expect(bindings[1].Expr()).To(Equal(expr2))
Expect(bindings[2].Name()).To(Equal("items"))
Expect(bindings[2].Expr()).To(Equal(expr3))
})
It("should return nil when no bindings added", func() {
tpl := defkit.NewTemplate()
Expect(tpl.GetLetBindings()).To(BeNil())
})
})
Context("LetVariable for CUE let binding references", func() {
It("should create referenceable variable usable as Value", func() {
ref := defkit.LetVariable("resourceContent")
// Name should match the let binding name
Expect(ref.Name()).To(Equal("resourceContent"))
// Should be usable as a Value in template operations
var v defkit.Value = ref
Expect(v).NotTo(BeNil())
})
})
Context("ListComprehension for CUE for-each generation", func() {
It("should capture all components needed for CUE comprehension syntax", func() {
source := defkit.ParamRef("constraints")
filter := defkit.ListFieldExists("enabled")
mappings := defkit.FieldMap{
"maxSkew": defkit.FieldRef("maxSkew"),
"minDomains": defkit.Optional("minDomains"),
}
comp := defkit.ForEachIn(source).
WithFilter(filter).
MapFields(mappings).
WithOptionalFields("labelSelector", "topologyKey")
// All components should be captured for CUE generation
Expect(comp.Source()).To(Equal(source))
Expect(comp.FilterCondition()).To(Equal(filter))
Expect(comp.Mappings()).To(Equal(mappings))
Expect(comp.ConditionalFields()).To(Equal([]string{"labelSelector", "topologyKey"}))
})
})
Context("ListFieldExists predicate", func() {
It("should store field name for CUE != _|_ check generation", func() {
pred := defkit.ListFieldExists("optionalField")
Expect(pred.GetField()).To(Equal("optionalField"))
})
})
Context("Template Raw CUE Blocks for complex patterns", func() {
It("should store raw blocks for direct CUE injection", func() {
tpl := defkit.NewTemplate()
headerBlock := `let _containers = context.output.spec.template.spec.containers`
patchBlock := `spec: template: spec: containers: [for c in _containers { c & _patch }]`
paramBlock := `parameter: { debug: *false | bool }`
outputsBlock := `outputs: service: { apiVersion: "v1", kind: "Service" }`
tpl.SetRawHeaderBlock(headerBlock)
tpl.SetRawPatchBlock(patchBlock)
tpl.SetRawParameterBlock(paramBlock)
tpl.SetRawOutputsBlock(outputsBlock)
Expect(tpl.GetRawHeaderBlock()).To(Equal(headerBlock))
Expect(tpl.GetRawPatchBlock()).To(Equal(patchBlock))
Expect(tpl.GetRawParameterBlock()).To(Equal(paramBlock))
Expect(tpl.GetRawOutputsBlock()).To(Equal(outputsBlock))
})
It("should return empty strings when blocks not set", func() {
tpl := defkit.NewTemplate()
Expect(tpl.GetRawHeaderBlock()).To(BeEmpty())
Expect(tpl.GetRawPatchBlock()).To(BeEmpty())
Expect(tpl.GetRawParameterBlock()).To(BeEmpty())
Expect(tpl.GetRawOutputsBlock()).To(BeEmpty())
})
})
Context("Condition types for CUE conditional generation", func() {
It("ParamIsSet should store param name for CUE != _|_ generation", func() {
cond := defkit.ParamIsSet("replicas")
Expect(cond.ParamName()).To(Equal("replicas"))
})
It("ParamNotSet should store param name for CUE == _|_ generation", func() {
cond := defkit.ParamNotSet("defaults")
inner := cond.Cond()
isSet, ok := inner.(*defkit.IsSetCondition)
Expect(ok).To(BeTrue())
Expect(isSet.ParamName()).To(Equal("defaults"))
})
It("ContextOutputExists should store path for context.output check", func() {
cond := defkit.ContextOutputExists("spec.template.spec.containers")
Expect(cond.Path()).To(Equal("spec.template.spec.containers"))
})
It("AllConditions should store all conditions for nested CUE if generation", func() {
cond1 := defkit.ParamIsSet("cpu")
cond2 := defkit.ParamIsSet("memory")
cond3 := defkit.ParamNotSet("defaults")
compound := defkit.AllConditions(cond1, cond2, cond3)
conditions := compound.Conditions()
Expect(conditions).To(HaveLen(3))
Expect(conditions[0]).To(Equal(cond1))
Expect(conditions[1]).To(Equal(cond2))
Expect(conditions[2]).To(Equal(cond3))
})
})
})