Files
kubevela/pkg/definition/defkit/patch_container_test.go
Jerrin Francis cf66f20cd9 Feat: introduce Mandatory() API and three-state CUE field markers in defkit (#7068)
* feat(defkit): introduce Mandatory() API and three-state CUE field markers

 Add a new .Mandatory() fluent method to all defkit param types and StructField,
 giving callers explicit control over all three CUE field presence semantics:

   field?: type   — optional, emitted by default (no method call)
   field: type    — non-optional; must have a value, defaults/merging can satisfy (.Mandatory())
   field!: type   — required in input; user must explicitly provide it (.Required())

 Previously, Required() emitted no marker (identical to a mandatory field), making
 it impossible to generate the CUE ! marker at all. Now Required() emits ! and
 Mandatory() emits no marker, matching their intuitive meanings.

 - Add `mandatory bool` field to baseParam and StructField
 - Add IsMandatory() accessor to baseParam, StructField, and Param interface
 - Add .Mandatory() builder method to all 10 concrete param types and StructField
 - Replace the `optional string` variable in cuegen.go with a typed `marker` variable
   backed by named constants (fieldMarkerOptional, fieldMarkerNone, fieldMarkerRequired)
 - Update IsOptional() to return !required && !mandatory
 - Update Optional() to clear mandatory instead of required
 - Update all tests to use .Mandatory() for non-optional fields

Signed-off-by: Jerrin Francis <jerrinfrancis7@gmail.com>

* fix(defkit): correct doc comments after Required/Mandatory semantic swap

  Fix misleading comments that still described the pre-swap behaviour.
  Mandatory() comments incorrectly referenced the "!" marker, and
  Required()/IsRequired() accessors had stale descriptions. Aligned
  all doc comments and field annotations with the actual semantics:
  Required → "!" marker, Mandatory → non-optional (no ? marker).

Signed-off-by: Jerrin Francis <jerrinfrancis7@gmail.com>

* feat(defkit): add BeMandatory matcher and fix BeOptional for three-state semantics

  BeOptional incorrectly returned true for mandatory params by checking
  !IsRequired(). Now uses IsOptional() which correctly excludes both
  required and mandatory params. Adds BeMandatory() matcher and updates
  paramAccessor interface with IsOptional/IsMandatory methods.

Signed-off-by: Jerrin Francis <jerrinfrancis7@gmail.com>

* fix(defkit): clear required flag in Optional() for all param types

  The previous commit only fixed StringParam.Optional(). Apply the same
  fix to Int, Bool, Float, Array, Map, Struct, StructField, Enum, OneOf,
  and StringKeyMap so that Optional() consistently resets both mandatory
  and required flags across all parameter types.

Signed-off-by: Jerrin Francis <jerrinfrancis7@gmail.com>

* fix(defkit): make Required() and Mandatory() mutually exclusive and fix missing optional markers in CUE generation

  Required() and Mandatory() only set their own flag without clearing
  the other, allowing both to be true simultaneously via chaining.
  Each method now clears the opposite flag so the last call wins,
  matching the reset semantics that Optional() already followed.

  Also fix CUE generation for int, bool, float, and enum params where
  the optional marker was dropped from format strings when a default
  value was present, causing those fields to always render without
  the field marker regardless of their optional/mandatory/required state.

Signed-off-by: Jerrin Francis <jerrinfrancis7@gmail.com>

* Empty commit to re-trigger e2e

Signed-off-by: Jerrin Francis <jerrinfrancis7@gmail.com>

---------

Signed-off-by: Jerrin Francis <jerrinfrancis7@gmail.com>
2026-03-12 16:39:34 +00: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").Mandatory()).
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.Inner()
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))
})
})
})