mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-20 04:56:39 +00:00
* fix: expand dotted field paths in ItemBuilder.Set Signed-off-by: Krishnan K M <krishnankaruvattu@gmail.com> * Fix: preserve dots in bracketed SetIf paths Signed-off-by: Krishnan K M <krishnankaruvattu@gmail.com> --------- Signed-off-by: Krishnan K M <krishnankaruvattu@gmail.com>
3323 lines
118 KiB
Go
3323 lines
118 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("CUEGenerator", func() {
|
|
var gen *defkit.CUEGenerator
|
|
|
|
BeforeEach(func() {
|
|
gen = defkit.NewCUEGenerator()
|
|
})
|
|
|
|
Describe("GenerateParameterSchema", func() {
|
|
It("should generate CUE for string parameters", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.String("image").Description("Container image"),
|
|
defkit.String("tag").Default("latest").Description("Image tag"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("// +usage=Container image"))
|
|
Expect(cue).To(ContainSubstring("image: string"))
|
|
Expect(cue).To(ContainSubstring("// +usage=Image tag"))
|
|
Expect(cue).To(ContainSubstring(`tag: *"latest" | string`))
|
|
})
|
|
|
|
It("should generate CUE for integer parameters", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.Int("replicas").Default(1).Description("Number of replicas"),
|
|
defkit.Int("port"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("replicas: *1 | int"))
|
|
Expect(cue).To(ContainSubstring("port: int"))
|
|
})
|
|
|
|
It("should generate CUE for boolean parameters", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.Bool("enabled").Default(true),
|
|
defkit.Bool("debug").Default(false),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("enabled: *true | bool"))
|
|
Expect(cue).To(ContainSubstring("debug: *false | bool"))
|
|
})
|
|
|
|
It("should generate CUE for array parameters", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.StringList("cmd").Optional().Description("Commands"),
|
|
defkit.List("env").Optional().Description("Environment variables"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("cmd?: [...string]"))
|
|
// List() creates an untyped array which generates [..._]
|
|
Expect(cue).To(ContainSubstring("env?: [..._]"))
|
|
})
|
|
|
|
It("should generate CUE for object parameters", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.Object("config").Optional().Description("Configuration object"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("config?: {...}"))
|
|
})
|
|
|
|
It("should generate CUE for string-key map parameters", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.StringKeyMap("labels").Optional().Description("Labels"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("labels?: [string]: string"))
|
|
})
|
|
|
|
It("should emit ! for required parameters", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.String("required").Required(),
|
|
defkit.String("optional").Optional(),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("required!: string"))
|
|
Expect(cue).To(ContainSubstring("optional?: string"))
|
|
})
|
|
|
|
It("should handle two-state field presence markers", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.String("accessKey").Required(), // ! — user must explicitly provide
|
|
defkit.Int("port"), // no marker — bare param
|
|
defkit.Bool("enabled"), // no marker — bare param
|
|
defkit.String("tag").Optional(), // ? — explicitly optional
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
// Required fields get "!" marker
|
|
Expect(cue).To(ContainSubstring("accessKey!: string"))
|
|
// Bare params get no marker (no ? and no !)
|
|
Expect(cue).To(ContainSubstring("port: int"))
|
|
Expect(cue).NotTo(ContainSubstring("port!:"))
|
|
Expect(cue).NotTo(ContainSubstring("port?:"))
|
|
Expect(cue).To(ContainSubstring("enabled: bool"))
|
|
// Optional gets ?
|
|
Expect(cue).To(ContainSubstring("tag?: string"))
|
|
})
|
|
|
|
It("should emit ! for required parameters with defaults", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.String("accessKey").Required().Default("key123"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
// Required with default: "!" wins, default value is still in the CUE type
|
|
Expect(cue).To(ContainSubstring(`accessKey!: *"key123" | string`))
|
|
})
|
|
|
|
It("should emit ! for required non-string parameters with defaults", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.Int("port").Required().Default(8080),
|
|
defkit.Bool("enabled").Required().Default(true),
|
|
defkit.Float("ratio").Required().Default(0.5),
|
|
defkit.Enum("mode").Required().Default("fast").Values("fast", "slow"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring(`port!: *8080 | int`))
|
|
Expect(cue).To(ContainSubstring(`enabled!: *true | bool`))
|
|
Expect(cue).To(ContainSubstring(`ratio!: *0.5 | float`))
|
|
Expect(cue).To(ContainSubstring(`mode!: *"fast" | "slow"`))
|
|
})
|
|
|
|
It("should keep ? for ForceOptional parameters even with defaults", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.String("normalDefault").Default("Honor").Values("Honor", "Ignore"),
|
|
defkit.String("optionalDefault").Default("Honor").Optional().Values("Honor", "Ignore"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
// Normal default: no ? (field is always present)
|
|
Expect(cue).To(ContainSubstring(`normalDefault: *"Honor" | "Ignore"`))
|
|
Expect(cue).NotTo(ContainSubstring(`normalDefault?:`))
|
|
|
|
// ForceOptional with default: has ? (field can be absent, defaults when present)
|
|
Expect(cue).To(ContainSubstring(`optionalDefault?: *"Honor" | "Ignore"`))
|
|
})
|
|
|
|
It("should append string to enum when OpenEnum is set", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.String("verbosity").Default("info").Values("info", "debug", "warn").OpenEnum(),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring(`*"info" | "debug" | "warn" | string`))
|
|
})
|
|
|
|
It("should generate // +ignore directive for ignored parameters", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.String("visible").Description("A visible field"),
|
|
defkit.Enum("hidden").Values("A", "B").Default("A").Ignore().Description("An ignored field"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).NotTo(ContainSubstring("// +ignore\n\t// +usage=A visible field"))
|
|
Expect(cue).To(ContainSubstring("// +ignore\n\t// +usage=An ignored field"))
|
|
})
|
|
|
|
It("should generate // +short directive for params with short flags", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.String("image").Description("Container image").Short("i"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("// +usage=Container image"))
|
|
Expect(cue).To(ContainSubstring("// +short=i"))
|
|
})
|
|
|
|
It("should generate both // +ignore and // +short directives in correct order", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.Int("port").Ignore().Description("Deprecated field, please use ports instead").Short("p"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
// Order should be: // +ignore, // +usage=..., // +short=p
|
|
ignoreIdx := strings.Index(cue, "// +ignore")
|
|
usageIdx := strings.Index(cue, "// +usage=Deprecated field")
|
|
shortIdx := strings.Index(cue, "// +short=p")
|
|
Expect(ignoreIdx).To(BeNumerically(">", 0))
|
|
Expect(usageIdx).To(BeNumerically(">", ignoreIdx))
|
|
Expect(shortIdx).To(BeNumerically(">", usageIdx))
|
|
})
|
|
})
|
|
|
|
Describe("GenerateParameterSchema with complex types", func() {
|
|
It("should generate CUE for struct parameters with nested fields", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.Struct("resources").Optional().WithFields(
|
|
defkit.Field("cpu", defkit.ParamTypeString).Default("100m"),
|
|
defkit.Field("memory", defkit.ParamTypeString).Default("128Mi"),
|
|
).Description("Resource limits"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("// +usage=Resource limits"))
|
|
Expect(cue).To(ContainSubstring("resources?:"))
|
|
Expect(cue).To(ContainSubstring("cpu:"))
|
|
Expect(cue).To(ContainSubstring("memory:"))
|
|
})
|
|
|
|
It("should generate CUE for enum parameters", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.Enum("protocol").Values("TCP", "UDP", "SCTP").Default("TCP"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring(`*"TCP"`))
|
|
Expect(cue).To(ContainSubstring(`"UDP"`))
|
|
Expect(cue).To(ContainSubstring(`"SCTP"`))
|
|
})
|
|
|
|
It("should generate CUE for float parameters", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.Float("ratio").Default(0.5).Description("Scale ratio"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("// +usage=Scale ratio"))
|
|
Expect(cue).To(ContainSubstring("ratio: *0.5 | float"))
|
|
})
|
|
|
|
It("should generate CUE for array parameters with fields", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.List("ports").Optional().WithFields(
|
|
defkit.Int("port"),
|
|
defkit.String("name").Optional(),
|
|
defkit.Enum("protocol").Values("TCP", "UDP").Default("TCP"),
|
|
),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("ports?:"))
|
|
Expect(cue).To(ContainSubstring("port:"))
|
|
Expect(cue).To(ContainSubstring("name?:"))
|
|
Expect(cue).To(ContainSubstring("protocol:"))
|
|
})
|
|
|
|
It("should generate CUE for nested array struct fields", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.Struct("selector").WithFields(
|
|
defkit.Field("matchExpressions", defkit.ParamTypeArray).Optional().
|
|
Nested(defkit.Struct("matchExpression").WithFields(
|
|
defkit.Field("key", defkit.ParamTypeString),
|
|
defkit.Field("operator", defkit.ParamTypeString).Optional(),
|
|
)),
|
|
),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("matchExpressions?: [...{"))
|
|
Expect(cue).To(ContainSubstring("key: string"))
|
|
Expect(cue).To(ContainSubstring("operator?: string"))
|
|
})
|
|
})
|
|
|
|
Describe("GenerateParameterSchema with OneOf parameters", func() {
|
|
It("should generate discriminator field and conditional variant blocks", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.OneOf("type").
|
|
Default("emptyDir").
|
|
Description("Volume type").
|
|
Variants(
|
|
defkit.Variant("pvc").WithFields(
|
|
defkit.Field("claimName", defkit.ParamTypeString),
|
|
),
|
|
defkit.Variant("emptyDir").WithFields(
|
|
defkit.Field("medium", defkit.ParamTypeString).Default("").Values("", "Memory"),
|
|
),
|
|
),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
// Discriminator field with default
|
|
Expect(cue).To(ContainSubstring(`*"emptyDir"`))
|
|
Expect(cue).To(ContainSubstring(`"pvc"`))
|
|
// Conditional blocks
|
|
Expect(cue).To(ContainSubstring(`if type == "pvc"`))
|
|
Expect(cue).To(ContainSubstring("claimName: string"))
|
|
Expect(cue).To(ContainSubstring(`if type == "emptyDir"`))
|
|
Expect(cue).To(ContainSubstring(`medium:`))
|
|
})
|
|
|
|
It("should generate OneOf inside array with shared fields", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.List("volumes").Optional().WithFields(
|
|
defkit.String("name"),
|
|
defkit.OneOf("type").Default("emptyDir").Variants(
|
|
defkit.Variant("pvc").WithFields(
|
|
defkit.Field("claimName", defkit.ParamTypeString),
|
|
),
|
|
defkit.Variant("emptyDir"),
|
|
),
|
|
),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("volumes?:"))
|
|
Expect(cue).To(ContainSubstring("[...{"))
|
|
Expect(cue).To(ContainSubstring("name: string"))
|
|
Expect(cue).To(ContainSubstring(`type: *"emptyDir" | "pvc"`))
|
|
Expect(cue).To(ContainSubstring(`if type == "pvc"`))
|
|
Expect(cue).To(ContainSubstring("claimName: string"))
|
|
})
|
|
|
|
It("should omit conditional block for empty variants", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.OneOf("kind").
|
|
Variants(
|
|
defkit.Variant("simple"), // no fields
|
|
defkit.Variant("complex").WithFields(
|
|
defkit.Field("config", defkit.ParamTypeString),
|
|
),
|
|
),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).NotTo(ContainSubstring(`if kind == "simple"`))
|
|
Expect(cue).To(ContainSubstring(`if kind == "complex"`))
|
|
Expect(cue).To(ContainSubstring("config: string"))
|
|
})
|
|
})
|
|
|
|
Describe("GenerateFullDefinition with MapVariant", func() {
|
|
It("should generate conditional field blocks in comprehension", func() {
|
|
volumes := defkit.List("volumes")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("batch/v1", "Job").
|
|
Params(volumes).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("batch/v1", "Job").
|
|
Set("spec.volumes",
|
|
defkit.Each(volumes).
|
|
Map(defkit.FieldMap{"name": defkit.FieldRef("name")}).
|
|
MapVariant("type", "pvc", defkit.FieldMap{
|
|
"persistentVolumeClaim.claimName": defkit.FieldRef("claimName"),
|
|
}).
|
|
MapVariant("type", "emptyDir", defkit.FieldMap{
|
|
"emptyDir.medium": defkit.FieldRef("medium"),
|
|
}),
|
|
),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("for v in parameter.volumes"))
|
|
Expect(cue).To(ContainSubstring("name: v.name"))
|
|
Expect(cue).To(ContainSubstring(`if v.type == "pvc"`))
|
|
Expect(cue).To(ContainSubstring("persistentVolumeClaim.claimName: v.claimName"))
|
|
Expect(cue).To(ContainSubstring(`if v.type == "emptyDir"`))
|
|
Expect(cue).To(ContainSubstring("emptyDir.medium: v.medium"))
|
|
})
|
|
|
|
It("should generate optional fields inside variant blocks", func() {
|
|
volumes := defkit.List("volumes")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("batch/v1", "Job").
|
|
Params(volumes).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("batch/v1", "Job").
|
|
Set("spec.volumes",
|
|
defkit.Each(volumes).
|
|
Map(defkit.FieldMap{"name": defkit.FieldRef("name")}).
|
|
MapVariant("type", "configMap", defkit.FieldMap{
|
|
"configMap.name": defkit.FieldRef("cmName"),
|
|
"configMap.items": defkit.OptionalFieldRef("items"),
|
|
}),
|
|
),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring(`if v.type == "configMap"`))
|
|
Expect(cue).To(ContainSubstring("configMap.name: v.cmName"))
|
|
Expect(cue).To(ContainSubstring("if v.items != _|_"))
|
|
Expect(cue).To(ContainSubstring("configMap.items: v.items"))
|
|
})
|
|
})
|
|
|
|
Describe("Multi-conditional leaf values (condValues)", func() {
|
|
It("should render both conditional values when same path is set with different conditions", func() {
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
volMounts := defkit.String("volumeMounts")
|
|
volumes := defkit.String("volumes")
|
|
|
|
comp := defkit.NewComponent("test-multi-cond").
|
|
Description("Test multi-conditional values").
|
|
AutodetectWorkload().
|
|
Params(volMounts, volumes).
|
|
Template(func(tpl *defkit.Template) {
|
|
res := defkit.NewResource("batch/v1", "Job")
|
|
res.
|
|
If(volMounts.IsSet()).
|
|
Set("spec.containers[0].volumeMounts", defkit.Lit("mountsArray")).
|
|
EndIf().
|
|
If(volumes.IsSet()).
|
|
Set("spec.containers[0].volumeMounts", defkit.Lit("volumesArray")).
|
|
EndIf()
|
|
tpl.Output(res)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
// Both conditions should appear for the same field
|
|
Expect(cue).To(ContainSubstring(`if parameter["volumeMounts"] != _|_`))
|
|
Expect(cue).To(ContainSubstring(`if parameter["volumes"] != _|_`))
|
|
// The field should appear twice, each in its own if block
|
|
Expect(strings.Count(cue, "volumeMounts:")).To(BeNumerically(">=", 2))
|
|
})
|
|
|
|
It("should not activate condValues when path is set unconditionally then conditionally", func() {
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
optional := defkit.String("opt")
|
|
|
|
comp := defkit.NewComponent("test-uncond-then-cond").
|
|
Description("Test unconditional then conditional").
|
|
AutodetectWorkload().
|
|
Params(optional).
|
|
Template(func(tpl *defkit.Template) {
|
|
res := defkit.NewResource("v1", "Pod")
|
|
res.
|
|
Set("spec.field", defkit.Lit("default")).
|
|
SetIf(optional.IsSet(), "spec.field", optional)
|
|
tpl.Output(res)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
// Conditional should win (overwrites unconditional)
|
|
Expect(cue).To(ContainSubstring("field: parameter.opt"))
|
|
})
|
|
})
|
|
|
|
Describe("Decomposition with condValues (condValues + canDecomposeByCondition)", func() {
|
|
It("should decompose struct when leaf nodes have condValues from SetIf with different And conditions", func() {
|
|
// This is the webservice CPU/memory limit branching pattern:
|
|
// Two SetIf calls with different conditions target the same leaf path,
|
|
// creating condValues. The parent struct must still decompose correctly.
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
cpu := defkit.String("cpu")
|
|
limit := defkit.Object("limit")
|
|
|
|
comp := defkit.NewComponent("test-condval-decompose").
|
|
Description("Test condValues decomposition").
|
|
AutodetectWorkload().
|
|
Params(cpu, limit).
|
|
Template(func(tpl *defkit.Template) {
|
|
res := defkit.NewResource("v1", "Pod")
|
|
// When cpu is set and limit.cpu exists: use limit.cpu for limits, cpu for requests
|
|
res.SetIf(defkit.And(cpu.IsSet(), defkit.PathExists("parameter.limit.cpu")),
|
|
"spec.resources.requests.cpu", cpu)
|
|
res.SetIf(defkit.And(cpu.IsSet(), defkit.PathExists("parameter.limit.cpu")),
|
|
"spec.resources.limits.cpu", defkit.Reference("parameter.limit.cpu"))
|
|
// When cpu is set but limit.cpu does NOT exist: use cpu for both
|
|
res.SetIf(defkit.And(cpu.IsSet(), defkit.Not(defkit.PathExists("parameter.limit.cpu"))),
|
|
"spec.resources.limits.cpu", cpu)
|
|
res.SetIf(defkit.And(cpu.IsSet(), defkit.Not(defkit.PathExists("parameter.limit.cpu"))),
|
|
"spec.resources.requests.cpu", cpu)
|
|
tpl.Output(res)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
// Both compound conditions should appear as separate blocks
|
|
Expect(cue).To(ContainSubstring(`parameter["cpu"] != _|_`))
|
|
Expect(cue).To(ContainSubstring("parameter.limit.cpu"))
|
|
|
|
// resources should be decomposed into per-condition blocks
|
|
// Each block should contain both limits and requests
|
|
Expect(strings.Count(cue, "resources:")).To(BeNumerically(">=", 2))
|
|
Expect(strings.Count(cue, "limits:")).To(BeNumerically(">=", 2))
|
|
Expect(strings.Count(cue, "requests:")).To(BeNumerically(">=", 2))
|
|
|
|
// The block where limit.cpu exists should use parameter.limit.cpu for limits
|
|
Expect(cue).To(ContainSubstring("cpu: parameter.limit.cpu"))
|
|
// The block where limit.cpu does NOT exist should use parameter.cpu for limits
|
|
Expect(strings.Count(cue, "cpu: parameter.cpu")).To(BeNumerically(">=", 2))
|
|
})
|
|
|
|
It("should decompose when two sibling leaves at the same path have different condValues", func() {
|
|
// Pattern: limits.cpu set under condition A (primary) and condition B (condValue),
|
|
// requests.cpu set under condition A (primary) and condition B (condValue).
|
|
// collectLeafConditions must see both A and B from condValues.
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
flagA := defkit.Bool("flagA")
|
|
flagB := defkit.Bool("flagB")
|
|
|
|
comp := defkit.NewComponent("test-condval-siblings").
|
|
Description("Test condValues sibling decomposition").
|
|
AutodetectWorkload().
|
|
Params(flagA, flagB).
|
|
Template(func(tpl *defkit.Template) {
|
|
res := defkit.NewResource("v1", "Pod")
|
|
res.SetIf(flagA.IsSet(), "spec.nested.child1", defkit.Lit("A1"))
|
|
res.SetIf(flagB.IsSet(), "spec.nested.child1", defkit.Lit("B1"))
|
|
res.SetIf(flagA.IsSet(), "spec.nested.child2", defkit.Lit("A2"))
|
|
res.SetIf(flagB.IsSet(), "spec.nested.child2", defkit.Lit("B2"))
|
|
tpl.Output(res)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
// Both conditions should appear
|
|
Expect(cue).To(ContainSubstring(`parameter["flagA"]`))
|
|
Expect(cue).To(ContainSubstring(`parameter["flagB"]`))
|
|
// nested should be decomposed into per-condition blocks
|
|
Expect(strings.Count(cue, "nested:")).To(BeNumerically(">=", 2))
|
|
|
|
// In the flagA block: child1 = "A1", child2 = "A2"
|
|
flagAIdx := strings.Index(cue, `parameter["flagA"]`)
|
|
Expect(flagAIdx).To(BeNumerically(">", 0))
|
|
endA := flagAIdx + 200
|
|
if endA > len(cue) {
|
|
endA = len(cue)
|
|
}
|
|
afterFlagA := cue[flagAIdx:endA]
|
|
Expect(afterFlagA).To(ContainSubstring(`child1: "A1"`))
|
|
Expect(afterFlagA).To(ContainSubstring(`child2: "A2"`))
|
|
|
|
// In the flagB block: child1 = "B1", child2 = "B2"
|
|
flagBIdx := strings.Index(cue, `parameter["flagB"]`)
|
|
Expect(flagBIdx).To(BeNumerically(">", 0))
|
|
endB := flagBIdx + 200
|
|
if endB > len(cue) {
|
|
endB = len(cue)
|
|
}
|
|
afterFlagB := cue[flagBIdx:endB]
|
|
Expect(afterFlagB).To(ContainSubstring(`child1: "B1"`))
|
|
Expect(afterFlagB).To(ContainSubstring(`child2: "B2"`))
|
|
})
|
|
|
|
It("should filter condValues correctly: primary condition returns primary value, condValue condition returns condValue value", func() {
|
|
// Verifies filterNodeByCondition returns the correct value
|
|
// when the target condition matches a condValue rather than the primary.
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
modeA := defkit.String("modeA")
|
|
modeB := defkit.String("modeB")
|
|
|
|
comp := defkit.NewComponent("test-filter-condval").
|
|
Description("Test filterNodeByCondition with condValues").
|
|
AutodetectWorkload().
|
|
Params(modeA, modeB).
|
|
Template(func(tpl *defkit.Template) {
|
|
res := defkit.NewResource("v1", "Pod")
|
|
// Same leaf path, two different conditions, two different values
|
|
res.SetIf(modeA.IsSet(), "spec.wrapper.target", defkit.Lit("value-from-A"))
|
|
res.SetIf(modeB.IsSet(), "spec.wrapper.target", defkit.Lit("value-from-B"))
|
|
// Second leaf to make decomposition viable
|
|
res.SetIf(modeA.IsSet(), "spec.wrapper.other", defkit.Lit("other-A"))
|
|
res.SetIf(modeB.IsSet(), "spec.wrapper.other", defkit.Lit("other-B"))
|
|
tpl.Output(res)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
// Find the modeA block and verify it has value-from-A
|
|
modeAIdx := strings.Index(cue, `parameter["modeA"]`)
|
|
Expect(modeAIdx).To(BeNumerically(">", 0))
|
|
endA := modeAIdx + 250
|
|
if endA > len(cue) {
|
|
endA = len(cue)
|
|
}
|
|
afterA := cue[modeAIdx:endA]
|
|
Expect(afterA).To(ContainSubstring(`target: "value-from-A"`))
|
|
Expect(afterA).To(ContainSubstring(`other: "other-A"`))
|
|
|
|
// Find the modeB block and verify it has value-from-B
|
|
modeBIdx := strings.Index(cue, `parameter["modeB"]`)
|
|
Expect(modeBIdx).To(BeNumerically(">", 0))
|
|
endB := modeBIdx + 250
|
|
if endB > len(cue) {
|
|
endB = len(cue)
|
|
}
|
|
afterB := cue[modeBIdx:endB]
|
|
Expect(afterB).To(ContainSubstring(`target: "value-from-B"`))
|
|
Expect(afterB).To(ContainSubstring(`other: "other-B"`))
|
|
})
|
|
})
|
|
|
|
Describe("Intermediate node decomposition (canDecomposeByCondition)", func() {
|
|
It("should decompose struct into per-condition blocks when all leaves share the same condition set", func() {
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
cpu := defkit.String("cpu")
|
|
memory := defkit.String("memory")
|
|
|
|
comp := defkit.NewComponent("test-decompose").
|
|
Description("Test condition decomposition").
|
|
AutodetectWorkload().
|
|
Params(cpu, memory).
|
|
Template(func(tpl *defkit.Template) {
|
|
res := defkit.NewResource("v1", "Pod")
|
|
res.
|
|
If(cpu.IsSet()).
|
|
Set("spec.resources.limits.cpu", cpu).
|
|
Set("spec.resources.requests.cpu", cpu).
|
|
EndIf().
|
|
If(memory.IsSet()).
|
|
Set("spec.resources.limits.memory", memory).
|
|
Set("spec.resources.requests.memory", memory).
|
|
EndIf()
|
|
tpl.Output(res)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
// resources should NOT appear unconditionally
|
|
// It should appear inside if blocks
|
|
Expect(cue).To(ContainSubstring(`if parameter["cpu"] != _|_ {`))
|
|
Expect(cue).To(ContainSubstring(`if parameter["memory"] != _|_ {`))
|
|
|
|
// Each condition block should contain resources with limits and requests
|
|
// Find the cpu block
|
|
cpuIdx := strings.Index(cue, `if parameter["cpu"] != _|_ {`)
|
|
Expect(cpuIdx).To(BeNumerically(">", 0))
|
|
// After the cpu condition, resources should appear
|
|
afterCpu := cue[cpuIdx:]
|
|
Expect(afterCpu).To(ContainSubstring("resources:"))
|
|
Expect(afterCpu).To(ContainSubstring("limits:"))
|
|
Expect(afterCpu).To(ContainSubstring("requests:"))
|
|
})
|
|
|
|
It("should not decompose when children have unconditional values", func() {
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
cpu := defkit.String("cpu")
|
|
|
|
comp := defkit.NewComponent("test-no-decompose").
|
|
Description("Test no decomposition").
|
|
AutodetectWorkload().
|
|
Params(cpu).
|
|
Template(func(tpl *defkit.Template) {
|
|
res := defkit.NewResource("v1", "Pod")
|
|
res.
|
|
If(cpu.IsSet()).
|
|
Set("spec.resources.limits.cpu", cpu).
|
|
EndIf().
|
|
Set("spec.resources.limits.memory", defkit.Lit("128Mi"))
|
|
tpl.Output(res)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
// resources should appear as a regular struct (not decomposed)
|
|
// because it has a mix of conditional and unconditional children
|
|
Expect(cue).To(ContainSubstring("resources:"))
|
|
Expect(cue).To(ContainSubstring("limits:"))
|
|
Expect(cue).To(ContainSubstring(`memory: "128Mi"`))
|
|
})
|
|
})
|
|
|
|
Describe("GenerateFullDefinition", func() {
|
|
It("should generate complete CUE definition", func() {
|
|
comp := defkit.NewComponent("webservice").
|
|
Description("Web service component").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(
|
|
defkit.String("image"),
|
|
)
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring(`webservice: {`))
|
|
Expect(cue).To(ContainSubstring(`type: "component"`))
|
|
Expect(cue).To(ContainSubstring(`description: "Web service component"`))
|
|
Expect(cue).To(ContainSubstring(`apiVersion: "apps/v1"`))
|
|
Expect(cue).To(ContainSubstring(`kind: "Deployment"`))
|
|
Expect(cue).To(ContainSubstring(`type: "deployments.apps"`))
|
|
Expect(cue).To(ContainSubstring(`parameter: {`))
|
|
})
|
|
|
|
It("should quote component names with special characters", func() {
|
|
comp := defkit.NewComponent("my-service").
|
|
Description("Service with dash").
|
|
Workload("apps/v1", "Deployment")
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring(`"my-service": {`))
|
|
})
|
|
|
|
It("should include status when defined", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
CustomStatus("message: \"Ready\"").
|
|
HealthPolicy("isHealth: true")
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("status: {"))
|
|
Expect(cue).To(ContainSubstring("customStatus:"))
|
|
Expect(cue).To(ContainSubstring("healthPolicy:"))
|
|
})
|
|
|
|
It("should infer correct workload types", func() {
|
|
testCases := []struct {
|
|
apiVersion string
|
|
kind string
|
|
expectedType string
|
|
}{
|
|
{"apps/v1", "Deployment", "deployments.apps"},
|
|
{"apps/v1", "StatefulSet", "statefulsets.apps"},
|
|
{"apps/v1", "DaemonSet", "daemonsets.apps"},
|
|
{"batch/v1", "Job", "jobs.batch"},
|
|
{"batch/v1", "CronJob", "cronjobs.batch"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
comp := defkit.NewComponent("test").
|
|
Workload(tc.apiVersion, tc.kind)
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
Expect(cue).To(ContainSubstring(tc.expectedType),
|
|
"Expected workload type %s for %s/%s", tc.expectedType, tc.apiVersion, tc.kind)
|
|
}
|
|
})
|
|
})
|
|
|
|
Describe("GenerateParameterSchema with Map parameters", func() {
|
|
It("should generate CUE for map parameters", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.Map("labels").Optional().Description("Labels to apply"),
|
|
defkit.Map("annotations").Optional(),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("labels?:"))
|
|
Expect(cue).To(ContainSubstring("annotations?:"))
|
|
Expect(cue).To(ContainSubstring("Labels to apply"))
|
|
})
|
|
})
|
|
|
|
Describe("GenerateFullDefinition with template", func() {
|
|
It("should generate output block with resource", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(defkit.String("image")).
|
|
Template(func(tpl *defkit.Template) {
|
|
image := defkit.String("image")
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", defkit.VelaCtx().Name()).
|
|
Set("spec.template.spec.containers[0].image", image),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("output:"))
|
|
Expect(cue).To(ContainSubstring("apiVersion:"))
|
|
Expect(cue).To(ContainSubstring("kind:"))
|
|
})
|
|
|
|
It("should generate outputs block with auxiliary resources", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", defkit.VelaCtx().Name()),
|
|
)
|
|
tpl.Outputs("service",
|
|
defkit.NewResource("v1", "Service").
|
|
Set("metadata.name", defkit.VelaCtx().Name()),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("output:"))
|
|
Expect(cue).To(ContainSubstring("outputs:"))
|
|
Expect(cue).To(ContainSubstring("service:"))
|
|
})
|
|
|
|
It("should generate conditional fields with SetIf", func() {
|
|
cpu := defkit.String("cpu").Optional()
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(cpu).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
SetIf(cpu.IsSet(), "spec.resources.limits.cpu", cpu),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("output:"))
|
|
Expect(cue).To(ContainSubstring("if"))
|
|
})
|
|
})
|
|
|
|
Describe("GenerateFullDefinition with OutputsGroupIf", func() {
|
|
It("should render a grouped if block with multiple outputs on a component", func() {
|
|
enabled := defkit.Bool("enabled")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(enabled).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", defkit.VelaCtx().Name()),
|
|
)
|
|
tpl.OutputsGroupIf(defkit.Eq(enabled, defkit.Lit(true)), func(g *defkit.OutputGroup) {
|
|
g.Add("service", defkit.NewResource("v1", "Service").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
g.Add("ingress", defkit.NewResource("networking.k8s.io/v1", "Ingress").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
})
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("outputs: {"))
|
|
Expect(cue).To(ContainSubstring("if parameter.enabled == true {"))
|
|
Expect(cue).To(ContainSubstring("service: {"))
|
|
Expect(cue).To(ContainSubstring("ingress: {"))
|
|
|
|
ifIdx := strings.Index(cue, "if parameter.enabled == true {")
|
|
svcIdx := strings.Index(cue, "service: {")
|
|
ingIdx := strings.Index(cue, "ingress: {")
|
|
Expect(ifIdx).To(BeNumerically(">=", 0))
|
|
Expect(ifIdx).To(BeNumerically("<", ingIdx))
|
|
Expect(ifIdx).To(BeNumerically("<", svcIdx))
|
|
Expect(ingIdx).To(BeNumerically("<", svcIdx))
|
|
})
|
|
|
|
It("should render plain Outputs before grouped outputs", func() {
|
|
enabled := defkit.Bool("enabled")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(enabled).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", defkit.VelaCtx().Name()),
|
|
)
|
|
tpl.Outputs("configmap",
|
|
defkit.NewResource("v1", "ConfigMap").
|
|
Set("metadata.name", defkit.VelaCtx().Name()),
|
|
)
|
|
tpl.OutputsGroupIf(defkit.Eq(enabled, defkit.Lit(true)), func(g *defkit.OutputGroup) {
|
|
g.Add("service", defkit.NewResource("v1", "Service").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
})
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
cmIdx := strings.Index(cue, "configmap: {")
|
|
ifIdx := strings.Index(cue, "if parameter.enabled == true {")
|
|
svcIdx := strings.Index(cue, "service: {")
|
|
Expect(cmIdx).To(BeNumerically(">=", 0))
|
|
Expect(ifIdx).To(BeNumerically(">=", 0))
|
|
Expect(svcIdx).To(BeNumerically(">=", 0))
|
|
Expect(cmIdx).To(BeNumerically("<", ifIdx))
|
|
Expect(ifIdx).To(BeNumerically("<", svcIdx))
|
|
})
|
|
|
|
It("should render multiple OutputsGroupIf blocks independently", func() {
|
|
enabled := defkit.Bool("enabled")
|
|
debug := defkit.Bool("debug")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(enabled, debug).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", defkit.VelaCtx().Name()),
|
|
)
|
|
tpl.OutputsGroupIf(defkit.Eq(enabled, defkit.Lit(true)), func(g *defkit.OutputGroup) {
|
|
g.Add("service", defkit.NewResource("v1", "Service").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
})
|
|
tpl.OutputsGroupIf(defkit.Eq(debug, defkit.Lit(true)), func(g *defkit.OutputGroup) {
|
|
g.Add("debug-cm", defkit.NewResource("v1", "ConfigMap").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
})
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("if parameter.enabled == true {"))
|
|
Expect(cue).To(ContainSubstring("if parameter.debug == true {"))
|
|
Expect(cue).To(ContainSubstring("service: {"))
|
|
Expect(cue).To(ContainSubstring("debug-cm"))
|
|
})
|
|
|
|
It("should render an outputs block even with only grouped outputs (no plain Outputs)", func() {
|
|
enabled := defkit.Bool("enabled")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(enabled).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", defkit.VelaCtx().Name()),
|
|
)
|
|
tpl.OutputsGroupIf(defkit.Eq(enabled, defkit.Lit(true)), func(g *defkit.OutputGroup) {
|
|
g.Add("service", defkit.NewResource("v1", "Service").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
g.Add("ingress", defkit.NewResource("networking.k8s.io/v1", "Ingress").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
})
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("outputs: {"))
|
|
Expect(cue).To(ContainSubstring("if parameter.enabled == true {"))
|
|
Expect(cue).To(ContainSubstring("service: {"))
|
|
Expect(cue).To(ContainSubstring("ingress: {"))
|
|
})
|
|
|
|
It("should render grouped output names sorted alphabetically", func() {
|
|
enabled := defkit.Bool("enabled")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(enabled).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", defkit.VelaCtx().Name()),
|
|
)
|
|
tpl.OutputsGroupIf(defkit.Eq(enabled, defkit.Lit(true)), func(g *defkit.OutputGroup) {
|
|
g.Add("z-ingress", defkit.NewResource("networking.k8s.io/v1", "Ingress").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
g.Add("a-svc", defkit.NewResource("v1", "Service").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
})
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
aIdx := strings.Index(cue, "a-svc")
|
|
zIdx := strings.Index(cue, "z-ingress")
|
|
Expect(aIdx).To(BeNumerically(">=", 0))
|
|
Expect(zIdx).To(BeNumerically(">=", 0))
|
|
Expect(aIdx).To(BeNumerically("<", zIdx))
|
|
})
|
|
})
|
|
|
|
Describe("Import detection", func() {
|
|
It("should detect strconv import from FormatInt", func() {
|
|
port := defkit.Int("port")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(port).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.annotations.port", defkit.StrconvFormatInt(port, 10)),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("strconv"))
|
|
})
|
|
|
|
It("should detect strings import from ToLower", func() {
|
|
name := defkit.String("name")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(name).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", defkit.StringsToLower(name)),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("strings"))
|
|
})
|
|
|
|
It("should detect strings import from StringParam.MinLen", func() {
|
|
hostname := defkit.String("hostname").MinLen(1)
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(hostname).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", hostname),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(MatchRegexp(`import\s+\(\s+"strings"\s+\)`))
|
|
Expect(cue).To(ContainSubstring("strings.MinRunes(1)"))
|
|
})
|
|
|
|
It("should detect strings import from StringParam.MaxLen", func() {
|
|
hostname := defkit.String("hostname").MaxLen(63)
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(hostname).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", hostname),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(MatchRegexp(`import\s+\(\s+"strings"\s+\)`))
|
|
Expect(cue).To(ContainSubstring("strings.MaxRunes(63)"))
|
|
})
|
|
|
|
It("should detect strings import when both MinLen and MaxLen are set", func() {
|
|
hostname := defkit.String("hostname").
|
|
Pattern("^[a-z0-9-]+$").
|
|
MinLen(1).
|
|
MaxLen(63)
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(hostname).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", hostname),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(MatchRegexp(`import\s+\(\s+"strings"\s+\)`))
|
|
Expect(cue).To(ContainSubstring("strings.MinRunes(1)"))
|
|
Expect(cue).To(ContainSubstring("strings.MaxRunes(63)"))
|
|
// Only one import of "strings", not duplicated
|
|
Expect(strings.Count(cue, `"strings"`)).To(Equal(1))
|
|
})
|
|
|
|
It("should not emit strings import when no param uses MinLen/MaxLen", func() {
|
|
name := defkit.String("name")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(name).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", name),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).NotTo(MatchRegexp(`import\s+\(\s+"strings"\s+\)`))
|
|
Expect(cue).NotTo(ContainSubstring("strings.MinRunes"))
|
|
Expect(cue).NotTo(ContainSubstring("strings.MaxRunes"))
|
|
})
|
|
|
|
It("should detect strings import even when MinLen param is not referenced in template", func() {
|
|
// The param is declared but the template doesn't reference it — the
|
|
// import must still be emitted because the parameter schema uses it.
|
|
hostname := defkit.String("hostname").MinLen(1).MaxLen(63)
|
|
name := defkit.String("name")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(hostname, name).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", name),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(MatchRegexp(`import\s+\(\s+"strings"\s+\)`))
|
|
})
|
|
})
|
|
|
|
Describe("GenerateFullDefinition with ConditionalOrFieldRef", func() {
|
|
It("should generate if/else pattern for conditional field reference", func() {
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
ports := defkit.List("ports").WithFields(
|
|
defkit.Int("port"),
|
|
defkit.String("name"),
|
|
)
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(ports).
|
|
Template(func(tpl *defkit.Template) {
|
|
containerPorts := defkit.Each(ports).Map(defkit.FieldMap{
|
|
"containerPort": defkit.FieldRef("port"),
|
|
"name": defkit.FieldRef("name").OrConditional(defkit.Format("port-%v", defkit.FieldRef("port"))),
|
|
})
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("spec.template.spec.containers[0].ports", containerPorts),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
// Should generate if/else blocks, NOT default syntax
|
|
Expect(cue).To(ContainSubstring("if v.name != _|_"))
|
|
Expect(cue).To(ContainSubstring("name: v.name"))
|
|
Expect(cue).To(ContainSubstring("if v.name == _|_"))
|
|
Expect(cue).To(ContainSubstring("strconv.FormatInt(v.port, 10)"))
|
|
})
|
|
})
|
|
|
|
Describe("GenerateFullDefinition with Directive", func() {
|
|
It("should render // +patchKey directive before field value", func() {
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
hostAliases := defkit.Object("hostAliases")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "DaemonSet").
|
|
Params(hostAliases).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "DaemonSet").
|
|
SetIf(hostAliases.IsSet(), "spec.template.spec.hostAliases", hostAliases).
|
|
Directive("spec.template.spec.hostAliases", "patchKey=ip"),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
// Directive should appear before the field value
|
|
Expect(cue).To(ContainSubstring("// +patchKey=ip"))
|
|
Expect(cue).To(ContainSubstring("hostAliases: parameter.hostAliases"))
|
|
|
|
// Verify ordering: directive before field
|
|
patchIdx := strings.Index(cue, "// +patchKey=ip")
|
|
hostIdx := strings.Index(cue, "hostAliases: parameter.hostAliases")
|
|
Expect(patchIdx).To(BeNumerically("<", hostIdx))
|
|
})
|
|
})
|
|
|
|
Describe("GenerateFullDefinition with CompoundOptionalField", func() {
|
|
It("should generate compound conditional for OptionalFieldWithCond in collection Map", func() {
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
exposeType := defkit.Enum("exposeType").
|
|
Values("ClusterIP", "NodePort", "LoadBalancer").
|
|
Default("ClusterIP")
|
|
ports := defkit.List("ports").WithFields(
|
|
defkit.Int("port"),
|
|
defkit.String("name"),
|
|
defkit.Int("nodePort"),
|
|
defkit.Bool("expose").Default(false),
|
|
)
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(exposeType, ports).
|
|
Template(func(tpl *defkit.Template) {
|
|
exposePorts := defkit.Each(ports).
|
|
Filter(defkit.FieldEquals("expose", true)).
|
|
Map(defkit.FieldMap{
|
|
"port": defkit.FieldRef("port"),
|
|
"nodePort": defkit.OptionalFieldWithCond("nodePort", defkit.Eq(exposeType, defkit.Lit("NodePort"))),
|
|
})
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("spec.ports", exposePorts),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
// Should generate compound conditional: if v.nodePort != _|_ if <cond> { nodePort: v.nodePort }
|
|
Expect(cue).To(ContainSubstring("v.nodePort != _|_"))
|
|
Expect(cue).To(ContainSubstring(`parameter.exposeType == "NodePort"`))
|
|
Expect(cue).To(ContainSubstring("nodePort: v.nodePort"))
|
|
})
|
|
|
|
It("should generate simple optional conditional alongside compound conditional", func() {
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
exposeType := defkit.Enum("exposeType").
|
|
Values("ClusterIP", "NodePort", "LoadBalancer").
|
|
Default("ClusterIP")
|
|
ports := defkit.List("ports").WithFields(
|
|
defkit.Int("port"),
|
|
defkit.Int("nodePort"),
|
|
defkit.String("protocol"),
|
|
defkit.Bool("expose").Default(false),
|
|
)
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(exposeType, ports).
|
|
Template(func(tpl *defkit.Template) {
|
|
exposePorts := defkit.Each(ports).
|
|
Filter(defkit.FieldEquals("expose", true)).
|
|
Map(defkit.FieldMap{
|
|
"port": defkit.FieldRef("port"),
|
|
"nodePort": defkit.OptionalFieldWithCond("nodePort", defkit.Eq(exposeType, defkit.Lit("NodePort"))),
|
|
"protocol": defkit.OptionalFieldRef("protocol"),
|
|
})
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "Deployment").
|
|
Set("spec.ports", exposePorts),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
// Compound conditional for nodePort
|
|
Expect(cue).To(ContainSubstring("v.nodePort != _|_"))
|
|
Expect(cue).To(ContainSubstring(`parameter.exposeType == "NodePort"`))
|
|
Expect(cue).To(ContainSubstring("nodePort: v.nodePort"))
|
|
|
|
// Simple optional conditional for protocol
|
|
Expect(cue).To(ContainSubstring("v.protocol != _|_"))
|
|
Expect(cue).To(ContainSubstring("protocol: v.protocol"))
|
|
})
|
|
})
|
|
|
|
Describe("GenerateFullDefinition with InlineArray", func() {
|
|
It("should render inline array value as [{field: value}]", func() {
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
port := defkit.Int("port")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "DaemonSet").
|
|
Params(port).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "DaemonSet").
|
|
Set("spec.template.spec.containers[0].ports", defkit.InlineArray(map[string]defkit.Value{
|
|
"containerPort": port,
|
|
})),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("containerPort: parameter.port"))
|
|
})
|
|
|
|
It("should render inline array with multiple fields", func() {
|
|
gen := defkit.NewCUEGenerator()
|
|
|
|
port := defkit.Int("port")
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "DaemonSet").
|
|
Params(port).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(
|
|
defkit.NewResource("apps/v1", "DaemonSet").
|
|
Set("spec.template.spec.containers[0].ports", defkit.InlineArray(map[string]defkit.Value{
|
|
"containerPort": port,
|
|
"protocol": defkit.Lit("TCP"),
|
|
})),
|
|
)
|
|
})
|
|
|
|
cue := gen.GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("containerPort: parameter.port"))
|
|
Expect(cue).To(ContainSubstring(`protocol: "TCP"`))
|
|
})
|
|
})
|
|
|
|
Describe("Struct field enum generation in helper definitions", func() {
|
|
It("should generate enum with default on a helper struct field", func() {
|
|
rule := defkit.Struct("rule").WithFields(
|
|
defkit.Field("strategy", defkit.ParamTypeString).
|
|
Default("onAppUpdate").
|
|
Values("onAppUpdate", "onAppDelete", "never"),
|
|
)
|
|
|
|
p := defkit.NewPolicy("test-enum-default").
|
|
Description("Test").
|
|
Helper("Rule", rule)
|
|
|
|
cue := p.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`strategy: *"onAppUpdate" | "onAppDelete" | "never"`))
|
|
Expect(cue).NotTo(ContainSubstring(`strategy: *"onAppUpdate" | string`))
|
|
})
|
|
|
|
It("should generate enum without default on a helper struct field", func() {
|
|
rule := defkit.Struct("rule").WithFields(
|
|
defkit.Field("propagation", defkit.ParamTypeString).
|
|
Values("orphan", "cascading").
|
|
Optional(),
|
|
)
|
|
|
|
p := defkit.NewPolicy("test-enum-no-default").
|
|
Description("Test").
|
|
Helper("Rule", rule)
|
|
|
|
cue := p.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`propagation?: "orphan" | "cascading"`))
|
|
Expect(cue).NotTo(ContainSubstring("propagation?: string"))
|
|
})
|
|
|
|
It("should generate enum without default on a helper struct field", func() {
|
|
rule := defkit.Struct("rule").WithFields(
|
|
defkit.Field("mode", defkit.ParamTypeString).
|
|
Values("strict", "permissive"),
|
|
)
|
|
|
|
p := defkit.NewPolicy("test-enum-mandatory").
|
|
Description("Test").
|
|
Helper("Rule", rule)
|
|
|
|
cue := p.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`mode: "strict" | "permissive"`))
|
|
Expect(cue).NotTo(ContainSubstring("mode?: "))
|
|
Expect(cue).NotTo(ContainSubstring("mode: string"))
|
|
})
|
|
|
|
It("should generate enum without default on a parameter struct field", func() {
|
|
comp := defkit.NewComponent("test-param-enum").
|
|
Workload("v1", "Pod").
|
|
Params(
|
|
defkit.Struct("config").WithFields(
|
|
defkit.Field("level", defkit.ParamTypeString).
|
|
Values("low", "medium", "high").
|
|
Optional(),
|
|
defkit.Field("mode", defkit.ParamTypeString).
|
|
Values("fast", "safe"),
|
|
),
|
|
).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "Pod"))
|
|
})
|
|
|
|
cue := defkit.NewCUEGenerator().GenerateFullDefinition(comp)
|
|
|
|
Expect(cue).To(ContainSubstring(`level?: "low" | "medium" | "high"`))
|
|
Expect(cue).To(ContainSubstring(`mode: "fast" | "safe"`))
|
|
Expect(cue).NotTo(ContainSubstring("level?: string"))
|
|
Expect(cue).NotTo(ContainSubstring("mode: string"))
|
|
Expect(cue).NotTo(ContainSubstring("mode?: "))
|
|
})
|
|
|
|
It("should handle mixed enum fields: with default, without default, and plain string", func() {
|
|
rule := defkit.Struct("rule").WithFields(
|
|
defkit.Field("strategy", defkit.ParamTypeString).
|
|
Default("always").
|
|
Values("always", "never", "on-failure"),
|
|
defkit.Field("propagation", defkit.ParamTypeString).
|
|
Values("orphan", "cascading").
|
|
Optional(),
|
|
defkit.Field("name", defkit.ParamTypeString).
|
|
Optional(),
|
|
)
|
|
|
|
p := defkit.NewPolicy("test-mixed").
|
|
Description("Test").
|
|
Helper("Rule", rule)
|
|
|
|
cue := p.ToCue()
|
|
|
|
// Enum with default
|
|
Expect(cue).To(ContainSubstring(`strategy: *"always" | "never" | "on-failure"`))
|
|
// Enum without default (optional)
|
|
Expect(cue).To(ContainSubstring(`propagation?: "orphan" | "cascading"`))
|
|
// Plain string (optional)
|
|
Expect(cue).To(ContainSubstring("name?: string"))
|
|
})
|
|
|
|
// Table-driven tests for StringParam enum edge cases via writeStringParam.
|
|
DescribeTable("StringParam enum edge cases",
|
|
func(buildComp func() defkit.Param, contains []string, notContains []string) {
|
|
comp := defkit.NewComponent("test-string-enum-edge").
|
|
Workload("v1", "Pod").
|
|
Params(buildComp()).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "Pod"))
|
|
})
|
|
cue := defkit.NewCUEGenerator().GenerateParameterSchema(comp)
|
|
for _, s := range contains {
|
|
Expect(cue).To(ContainSubstring(s))
|
|
}
|
|
for _, s := range notContains {
|
|
Expect(cue).NotTo(ContainSubstring(s))
|
|
}
|
|
},
|
|
Entry("empty Values and no default produces plain string",
|
|
func() defkit.Param {
|
|
return defkit.String("fieldName")
|
|
},
|
|
[]string{"fieldName: string"},
|
|
[]string{`fieldName: "`, "fieldName?: "},
|
|
),
|
|
Entry("single value no default required produces quoted literal",
|
|
func() defkit.Param {
|
|
return defkit.String("fieldName").Values("v1")
|
|
},
|
|
[]string{`fieldName: "v1"`},
|
|
[]string{"fieldName?: ", "fieldName: string"},
|
|
),
|
|
Entry("single value optional produces optional quoted literal",
|
|
func() defkit.Param {
|
|
return defkit.String("fieldName").Values("v1").Optional()
|
|
},
|
|
[]string{`fieldName?: "v1"`},
|
|
[]string{"fieldName: string"},
|
|
),
|
|
Entry("default not in Values list: default appears first with asterisk",
|
|
func() defkit.Param {
|
|
return defkit.String("fieldName").Default("x").Values("v1", "v2")
|
|
},
|
|
[]string{`fieldName: *"x" | "v1" | "v2"`},
|
|
[]string{"fieldName?: ", "fieldName: string"},
|
|
),
|
|
Entry("OpenEnum with values appends | string",
|
|
func() defkit.Param {
|
|
return defkit.String("fieldName").Values("v1", "v2").OpenEnum()
|
|
},
|
|
[]string{`fieldName: "v1" | "v2" | string`},
|
|
[]string{"fieldName?: "},
|
|
),
|
|
Entry("OpenEnum with no values produces plain string",
|
|
func() defkit.Param {
|
|
return defkit.String("fieldName").OpenEnum()
|
|
},
|
|
[]string{"fieldName: string"},
|
|
[]string{"fieldName?: "},
|
|
),
|
|
)
|
|
|
|
// Table-driven tests for EnumParam edge cases via writeEnumParam.
|
|
DescribeTable("EnumParam edge cases",
|
|
func(buildComp func() defkit.Param, contains []string, notContains []string) {
|
|
comp := defkit.NewComponent("test-enum-param-edge").
|
|
Workload("v1", "Pod").
|
|
Params(buildComp()).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "Pod"))
|
|
})
|
|
cue := defkit.NewCUEGenerator().GenerateParameterSchema(comp)
|
|
for _, s := range contains {
|
|
Expect(cue).To(ContainSubstring(s))
|
|
}
|
|
for _, s := range notContains {
|
|
Expect(cue).NotTo(ContainSubstring(s))
|
|
}
|
|
},
|
|
Entry("required enum with multiple values — ! marker present",
|
|
func() defkit.Param {
|
|
return defkit.Enum("fieldName").Values("a", "b", "c").Required()
|
|
},
|
|
[]string{`fieldName!: "a" | "b" | "c"`},
|
|
[]string{"fieldName?: ", "fieldName: string"},
|
|
),
|
|
Entry("optional enum with multiple values — ? marker present",
|
|
func() defkit.Param {
|
|
return defkit.Enum("fieldName").Values("a", "b", "c").Optional()
|
|
},
|
|
[]string{`fieldName?: "a" | "b" | "c"`},
|
|
[]string{"fieldName: string"},
|
|
),
|
|
Entry("EnumParam with single value only",
|
|
func() defkit.Param {
|
|
return defkit.Enum("fieldName").Values("only")
|
|
},
|
|
[]string{`fieldName: "only"`},
|
|
[]string{"fieldName?: ", "fieldName: string"},
|
|
),
|
|
Entry("EnumParam with no values produces plain string",
|
|
func() defkit.Param {
|
|
return defkit.Enum("fieldName")
|
|
},
|
|
[]string{"fieldName: string"},
|
|
[]string{"fieldName?: "},
|
|
),
|
|
)
|
|
})
|
|
|
|
Describe("GenerateParameterSchema with ClosedUnion parameters", func() {
|
|
It("should generate close() disjunction with simple fields", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.ClosedUnion("url").
|
|
Description("Specify the url").
|
|
Options(
|
|
defkit.ClosedStruct().WithFields(
|
|
defkit.Field("value", defkit.ParamTypeString),
|
|
),
|
|
defkit.ClosedStruct().WithFields(
|
|
defkit.Field("ref", defkit.ParamTypeString),
|
|
),
|
|
),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("// +usage=Specify the url"))
|
|
Expect(cue).To(ContainSubstring("url: close({"))
|
|
Expect(cue).To(ContainSubstring("value: string"))
|
|
Expect(cue).To(ContainSubstring("}) | close({"))
|
|
Expect(cue).To(ContainSubstring("ref: string"))
|
|
})
|
|
|
|
It("should generate close() disjunction with nested structs", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.ClosedUnion("url").
|
|
Options(
|
|
defkit.ClosedStruct().WithFields(
|
|
defkit.Field("value", defkit.ParamTypeString),
|
|
),
|
|
defkit.ClosedStruct().WithFields(
|
|
defkit.Field("secretRef", defkit.ParamTypeStruct).Nested(
|
|
defkit.Struct("secretRef").WithFields(
|
|
defkit.Field("name", defkit.ParamTypeString).Description("name of the secret"),
|
|
defkit.Field("key", defkit.ParamTypeString).Description("key in the secret"),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("url: close({"))
|
|
Expect(cue).To(ContainSubstring("value: string"))
|
|
Expect(cue).To(ContainSubstring("}) | close({"))
|
|
Expect(cue).To(ContainSubstring("secretRef: {"))
|
|
Expect(cue).To(ContainSubstring("// +usage=name of the secret"))
|
|
Expect(cue).To(ContainSubstring("name: string"))
|
|
Expect(cue).To(ContainSubstring("// +usage=key in the secret"))
|
|
Expect(cue).To(ContainSubstring("key: string"))
|
|
})
|
|
|
|
It("should generate optional closed union with ?", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.ClosedUnion("source").
|
|
Optional().
|
|
Options(
|
|
defkit.ClosedStruct().WithFields(
|
|
defkit.Field("hcl", defkit.ParamTypeString),
|
|
),
|
|
),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
Expect(cue).To(ContainSubstring("source?:"))
|
|
})
|
|
|
|
It("should handle close() disjunction field ordering", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.ClosedUnion("url").Options(
|
|
defkit.ClosedStruct().WithFields(
|
|
defkit.Field("value", defkit.ParamTypeString),
|
|
),
|
|
defkit.ClosedStruct().WithFields(
|
|
defkit.Field("secretRef", defkit.ParamTypeStruct),
|
|
),
|
|
),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
// Verify ordering: first option before second
|
|
valueIdx := strings.Index(cue, "value: string")
|
|
secretIdx := strings.Index(cue, "secretRef:")
|
|
Expect(valueIdx).To(BeNumerically(">=", 0), "expected 'value: string' to be present")
|
|
Expect(secretIdx).To(BeNumerically(">=", 0), "expected 'secretRef:' to be present")
|
|
Expect(valueIdx).To(BeNumerically("<", secretIdx))
|
|
})
|
|
|
|
It("should handle empty options gracefully", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.ClosedUnion("empty"),
|
|
)
|
|
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
|
|
// Empty options should produce fallback
|
|
Expect(cue).To(ContainSubstring("empty: _"))
|
|
})
|
|
|
|
It("should generate ClosedUnion in helper definition", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Params(
|
|
defkit.String("name"),
|
|
).
|
|
Helper("URLConfig", defkit.ClosedUnion("urlConfig").
|
|
Options(
|
|
defkit.ClosedStruct().WithFields(
|
|
defkit.Field("value", defkit.ParamTypeString),
|
|
),
|
|
defkit.ClosedStruct().WithFields(
|
|
defkit.Field("secretRef", defkit.ParamTypeString),
|
|
),
|
|
),
|
|
)
|
|
|
|
cue := comp.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("#URLConfig:"))
|
|
Expect(cue).To(ContainSubstring("close({"))
|
|
Expect(cue).To(ContainSubstring("value: string"))
|
|
Expect(cue).To(ContainSubstring("}) | close({"))
|
|
Expect(cue).To(ContainSubstring("secretRef: string"))
|
|
})
|
|
})
|
|
|
|
Describe("ForEachGuarded inner braces", func() {
|
|
It("should wrap each element in inner braces", func() {
|
|
hasStorage := defkit.PathExists("parameter.storage")
|
|
ws := defkit.NewWorkflowStep("test").
|
|
Params(
|
|
defkit.Array("storage").WithFields(
|
|
defkit.String("name").Required(),
|
|
defkit.String("path").Required(),
|
|
),
|
|
).
|
|
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
|
arr := defkit.NewArray().
|
|
ForEachGuarded(
|
|
hasStorage,
|
|
defkit.ParamRef("storage"),
|
|
defkit.NewArrayElement().
|
|
Set("name", defkit.Reference("m.name")).
|
|
Set("path", defkit.Reference("m.path")),
|
|
)
|
|
tpl.Set("items", arr)
|
|
})
|
|
|
|
cue := ws.ToCue()
|
|
|
|
// Should have inner braces around each element
|
|
Expect(cue).To(ContainSubstring("for m in"))
|
|
Expect(cue).To(MatchRegexp(`for m in .+ \{\n[^\n]*\{`))
|
|
})
|
|
|
|
It("should wrap elements with conditional fields in inner braces", func() {
|
|
hasStorage := defkit.PathExists("parameter.storage")
|
|
ws := defkit.NewWorkflowStep("test").
|
|
Params(
|
|
defkit.Array("storage").WithFields(
|
|
defkit.String("name").Required(),
|
|
defkit.String("subPath"),
|
|
),
|
|
).
|
|
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
|
arr := defkit.NewArray().
|
|
ForEachGuarded(
|
|
hasStorage,
|
|
defkit.ParamRef("storage"),
|
|
defkit.NewArrayElement().
|
|
Set("name", defkit.Reference("m.name")).
|
|
SetIf(defkit.PathExists("m.subPath"), "subPath", defkit.Reference("m.subPath")),
|
|
)
|
|
tpl.Set("mounts", arr)
|
|
})
|
|
|
|
cue := ws.ToCue()
|
|
|
|
// Inner braces should contain both the field and the conditional
|
|
Expect(cue).To(ContainSubstring("name: m.name"))
|
|
Expect(cue).To(ContainSubstring("if m.subPath != _|_"))
|
|
})
|
|
})
|
|
|
|
Describe("Dedupe from Reference source", func() {
|
|
It("should generate dedup pattern when source is a Reference", func() {
|
|
ws := defkit.NewWorkflowStep("test").
|
|
Params(defkit.String("name").Required()).
|
|
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
|
tpl.Set("deduped", defkit.From(defkit.Reference("volumesList")).Dedupe("name"))
|
|
})
|
|
|
|
cue := ws.ToCue()
|
|
|
|
// Should generate the full dedup pattern, not simple [for v in ...]
|
|
Expect(cue).To(ContainSubstring("for val in ["))
|
|
Expect(cue).To(ContainSubstring("for i, vi in volumesList"))
|
|
Expect(cue).To(ContainSubstring("for j, vj in volumesList if j < i && vi.name == vj.name"))
|
|
Expect(cue).To(ContainSubstring("_ignore: true"))
|
|
Expect(cue).To(ContainSubstring("if val._ignore == _|_"))
|
|
Expect(cue).NotTo(ContainSubstring("[for v in volumesList { v }]"))
|
|
})
|
|
|
|
It("should generate dedup pattern with different key field", func() {
|
|
ws := defkit.NewWorkflowStep("test").
|
|
Params(defkit.String("name").Required()).
|
|
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
|
tpl.Set("unique", defkit.From(defkit.Reference("items")).Dedupe("id"))
|
|
})
|
|
|
|
cue := ws.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("vi.id == vj.id"))
|
|
Expect(cue).To(ContainSubstring("for i, vi in items"))
|
|
})
|
|
})
|
|
|
|
Describe("TemplateBody with no params", func() {
|
|
It("should skip parameter block when TemplateBody is set and no params exist", func() {
|
|
ws := defkit.NewWorkflowStep("test").
|
|
Description("test step").
|
|
TemplateBody("nop: {}")
|
|
|
|
cue := ws.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("nop: {}"))
|
|
Expect(cue).NotTo(ContainSubstring("parameter:"))
|
|
})
|
|
|
|
It("should still emit parameter block when TemplateBody is set but params exist", func() {
|
|
ws := defkit.NewWorkflowStep("test").
|
|
Description("test step").
|
|
Params(defkit.String("name")).
|
|
TemplateBody("nop: {}")
|
|
|
|
cue := ws.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("nop: {}"))
|
|
Expect(cue).To(ContainSubstring("parameter:"))
|
|
Expect(cue).To(ContainSubstring("name: string"))
|
|
})
|
|
|
|
It("should emit parameter block when no TemplateBody and no params", func() {
|
|
ws := defkit.NewWorkflowStep("test").
|
|
Description("test step")
|
|
|
|
cue := ws.ToCue()
|
|
|
|
// Default behavior: empty parameter block is emitted
|
|
Expect(cue).To(ContainSubstring("parameter:"))
|
|
})
|
|
})
|
|
|
|
Describe("Fluent Builder API Validator Patterns CUE Generation", func() {
|
|
var gen *defkit.CUEGenerator
|
|
|
|
BeforeEach(func() {
|
|
gen = defkit.NewCUEGenerator()
|
|
})
|
|
|
|
Context("OmitWorkloadType CUE Generation", func() {
|
|
It("should suppress workload type field when omitted", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
OmitWorkloadType().
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", defkit.Lit("test")))
|
|
})
|
|
|
|
cue := comp.ToCue()
|
|
Expect(cue).To(ContainSubstring(`kind: "Deployment"`))
|
|
Expect(cue).To(ContainSubstring("workload:"))
|
|
Expect(cue).NotTo(MatchRegexp(`type:\s+"deployments\.apps"`))
|
|
})
|
|
|
|
It("should include workload type when not omitted", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Workload("apps/v1", "Deployment").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", defkit.Lit("test")))
|
|
})
|
|
|
|
cue := comp.ToCue()
|
|
Expect(cue).To(ContainSubstring(`type: "deployments.apps"`))
|
|
})
|
|
})
|
|
|
|
Context("RawHeaderBlock in Component Template", func() {
|
|
It("should emit raw header block before output", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Workload("v1", "PersistentVolumeClaim").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.SetRawHeaderBlock(`let _claimName = parameter.claimName + "-pvc"`)
|
|
tpl.Output(defkit.NewResource("v1", "PersistentVolumeClaim").
|
|
Set("metadata.name", defkit.Reference("_claimName")))
|
|
})
|
|
|
|
cue := gen.GenerateTemplate(comp)
|
|
Expect(cue).To(ContainSubstring(`let _claimName = parameter.claimName + "-pvc"`))
|
|
Expect(cue).To(ContainSubstring("_claimName"))
|
|
})
|
|
|
|
It("should emit multiline raw header block", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Workload("v1", "ConfigMap").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.SetRawHeaderBlock("let _a = parameter.a\nlet _b = parameter.b")
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
Set("metadata.name", defkit.Lit("test")))
|
|
})
|
|
|
|
cue := gen.GenerateTemplate(comp)
|
|
Expect(cue).To(ContainSubstring("let _a = parameter.a"))
|
|
Expect(cue).To(ContainSubstring("let _b = parameter.b"))
|
|
})
|
|
|
|
It("should not emit header block when empty", func() {
|
|
comp := defkit.NewComponent("test").
|
|
Workload("v1", "ConfigMap").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
Set("metadata.name", defkit.Lit("test")))
|
|
})
|
|
|
|
cue := gen.GenerateTemplate(comp)
|
|
Expect(cue).NotTo(ContainSubstring("let _"))
|
|
})
|
|
})
|
|
|
|
Context("ArrayParam NotEmpty Elements CUE Generation", func() {
|
|
It("should generate [...(string & !=\"\")] for NotEmpty string arrays", func() {
|
|
p := defkit.StringList("tags").NotEmpty()
|
|
comp := defkit.NewComponent("test").Params(p)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring(`[...(string & !="")]`))
|
|
})
|
|
|
|
It("should generate normal [...string] without NotEmpty", func() {
|
|
p := defkit.StringList("tags")
|
|
comp := defkit.NewComponent("test").Params(p)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring(`[...string]`))
|
|
Expect(cue).NotTo(ContainSubstring(`!=""`))
|
|
})
|
|
})
|
|
|
|
Context("ArrayParam with Schema and Default CUE Generation", func() {
|
|
It("should generate array with OfEnum and default value", func() {
|
|
p := defkit.Array("methods").
|
|
OfEnum("GET", "POST", "DELETE").
|
|
Default([]any{"GET"})
|
|
|
|
comp := defkit.NewComponent("test").Params(p)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring(`["GET"]`))
|
|
Expect(cue).To(ContainSubstring(`"GET" | "POST" | "DELETE"`))
|
|
})
|
|
|
|
It("should generate array with OfEnum and nil default as empty array", func() {
|
|
p := defkit.Array("methods").
|
|
OfEnum("GET", "POST").
|
|
Default(nil)
|
|
|
|
comp := defkit.NewComponent("test").Params(p)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring("*[]"))
|
|
})
|
|
|
|
It("should generate array with OfEnum and multi-value default", func() {
|
|
p := defkit.Array("methods").
|
|
OfEnum("GET", "POST", "DELETE").
|
|
Default([]any{"GET", "POST"})
|
|
|
|
comp := defkit.NewComponent("test").Params(p)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring(`["GET", "POST"]`))
|
|
})
|
|
})
|
|
|
|
Context("MapParam Closed CUE Generation", func() {
|
|
It("should emit close({...}) for closed struct", func() {
|
|
p := defkit.Object("governance").Closed().WithFields(
|
|
defkit.String("tenantName"),
|
|
defkit.String("departmentCode"),
|
|
)
|
|
comp := defkit.NewComponent("test").Params(p)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring("close({"))
|
|
Expect(cue).To(ContainSubstring("})"))
|
|
Expect(cue).To(ContainSubstring("tenantName: string"))
|
|
})
|
|
|
|
It("should not emit close({}) for non-closed struct", func() {
|
|
p := defkit.Object("config").WithFields(
|
|
defkit.String("name"),
|
|
)
|
|
comp := defkit.NewComponent("test").Params(p)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).NotTo(ContainSubstring("close({"))
|
|
Expect(cue).To(ContainSubstring("config: {"))
|
|
})
|
|
})
|
|
|
|
Context("MapParam with Validators CUE Generation", func() {
|
|
It("should emit validators inside struct", func() {
|
|
v := defkit.Validate("name required").
|
|
WithName("_validateName").
|
|
FailWhen(defkit.LocalField("name").Eq(""))
|
|
|
|
p := defkit.Object("governance").WithFields(
|
|
defkit.String("name"),
|
|
).Validators(v)
|
|
|
|
comp := defkit.NewComponent("test").Params(p)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring("governance: {"))
|
|
Expect(cue).To(ContainSubstring("_validateName:"))
|
|
Expect(cue).To(ContainSubstring(`"name required": true`))
|
|
})
|
|
})
|
|
|
|
Context("MapParam with ConditionalFields CUE Generation", func() {
|
|
It("should emit conditional fields inside struct", func() {
|
|
flag := defkit.Bool("flag").Default(false)
|
|
p := defkit.Object("config").Optional().ConditionalFields(
|
|
defkit.WhenParam(flag.Eq(true)).Params(
|
|
defkit.String("secret").Required(),
|
|
),
|
|
defkit.WhenParam(flag.Eq(false)).Params(
|
|
defkit.String("secret").Optional(),
|
|
),
|
|
)
|
|
|
|
comp := defkit.NewComponent("test").Params(flag, p)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring("config?: {"))
|
|
Expect(cue).To(ContainSubstring("if parameter.flag == true"))
|
|
Expect(cue).To(ContainSubstring("if parameter.flag == false"))
|
|
})
|
|
|
|
It("should emit validators inside conditional field branches", func() {
|
|
flag := defkit.Bool("flag").Default(false)
|
|
v := defkit.Validate("check").WithName("_v").
|
|
FailWhen(defkit.LocalField("secret").Eq(""))
|
|
|
|
p := defkit.Object("config").Optional().ConditionalFields(
|
|
defkit.WhenParam(flag.Eq(true)).
|
|
Params(defkit.String("secret").Required()).
|
|
Validators(v),
|
|
)
|
|
|
|
comp := defkit.NewComponent("test").Params(flag, p)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring("_v:"))
|
|
})
|
|
})
|
|
|
|
Context("Closed MapParam with Validators and ConditionalFields Combined", func() {
|
|
It("should combine all features", func() {
|
|
flag := defkit.Bool("flag").Default(false)
|
|
v := defkit.Validate("name required").
|
|
WithName("_validateName").
|
|
FailWhen(defkit.LocalField("name").Eq(""))
|
|
|
|
p := defkit.Object("governance").Closed().
|
|
WithFields(
|
|
defkit.String("name").NotEmpty(),
|
|
).
|
|
Validators(v).
|
|
ConditionalFields(
|
|
defkit.WhenParam(flag.Eq(true)).Params(
|
|
defkit.String("extra").Required(),
|
|
),
|
|
)
|
|
|
|
comp := defkit.NewComponent("test").Params(flag, p)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring("close({"))
|
|
Expect(cue).To(ContainSubstring(`!=""`))
|
|
Expect(cue).To(ContainSubstring("_validateName:"))
|
|
Expect(cue).To(ContainSubstring("if parameter.flag == true"))
|
|
Expect(cue).To(ContainSubstring("})"))
|
|
})
|
|
})
|
|
|
|
Context("RegexMatch CUE Generation", func() {
|
|
It("should generate regex match for StringParam.Matches", func() {
|
|
p := defkit.String("name")
|
|
comp := defkit.NewComponent("test").
|
|
Params(p).
|
|
Workload("v1", "ConfigMap").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
SetIf(p.Matches("^prod-"), "data.env", defkit.Lit("production")))
|
|
})
|
|
|
|
cue := gen.GenerateTemplate(comp)
|
|
Expect(cue).To(ContainSubstring(`parameter.name =~ "^prod-"`))
|
|
})
|
|
|
|
It("should generate regex match for LocalFieldRef.Matches in validator", func() {
|
|
v := defkit.Validate("bad").
|
|
WithName("_v").
|
|
FailWhen(defkit.LocalField("host").Matches(`\.internal$`))
|
|
|
|
comp := defkit.NewComponent("test").Validators(v)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring(`host =~ "\\.internal$"`))
|
|
})
|
|
})
|
|
|
|
Context("LocalFieldRef NotSet CUE Generation", func() {
|
|
It("should generate == _|_ for LocalFieldRef.NotSet", func() {
|
|
v := defkit.Validate("role required").
|
|
WithName("_v").
|
|
FailWhen(defkit.LocalField("role").NotSet())
|
|
|
|
comp := defkit.NewComponent("test").Validators(v)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring("role == _|_"))
|
|
})
|
|
})
|
|
|
|
Context("LocalFieldRef LenGt CUE Generation", func() {
|
|
It("should generate len(field) > n", func() {
|
|
v := defkit.Validate("too many").
|
|
WithName("_v").
|
|
FailWhen(defkit.LocalField("items").LenGt(10))
|
|
|
|
comp := defkit.NewComponent("test").Validators(v)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring("len(items) > 10"))
|
|
})
|
|
})
|
|
|
|
Context("LenOfExpr CUE Generation", func() {
|
|
It("should generate len(expr) > n for Gt", func() {
|
|
v := defkit.Validate("name too long").
|
|
WithName("_v").
|
|
FailWhen(defkit.LenOf(defkit.LocalField("name")).Gt(63))
|
|
|
|
comp := defkit.NewComponent("test").Validators(v)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring("len(name) > 63"))
|
|
})
|
|
|
|
It("should generate len(expr) >= n for Gte", func() {
|
|
v := defkit.Validate("check").
|
|
WithName("_v").
|
|
FailWhen(defkit.LenOf(defkit.LocalField("data")).Gte(100))
|
|
|
|
comp := defkit.NewComponent("test").Validators(v)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring("len(data) >= 100"))
|
|
})
|
|
|
|
It("should generate len(expr) == n for Eq", func() {
|
|
v := defkit.Validate("check").
|
|
WithName("_v").
|
|
FailWhen(defkit.LenOf(defkit.LocalField("code")).Eq(3))
|
|
|
|
comp := defkit.NewComponent("test").Validators(v)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring("len(code) == 3"))
|
|
})
|
|
})
|
|
|
|
Context("TimeParse CUE Generation", func() {
|
|
It("should generate time.Parse expressions", func() {
|
|
v := defkit.Validate("start before end").
|
|
WithName("_v").
|
|
FailWhen(defkit.TimeParse("2006-01-02T15:04:05Z", defkit.LocalField("start")).
|
|
Gte(defkit.TimeParse("2006-01-02T15:04:05Z", defkit.LocalField("end"))))
|
|
|
|
comp := defkit.NewComponent("test").Validators(v)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring(`time.Parse("2006-01-02T15:04:05Z", start)`))
|
|
Expect(cue).To(ContainSubstring(`time.Parse("2006-01-02T15:04:05Z", end)`))
|
|
})
|
|
})
|
|
|
|
Context("RawCUECondition CUE Generation", func() {
|
|
It("should emit raw expression verbatim", func() {
|
|
v := defkit.Validate("check").
|
|
WithName("_v").
|
|
FailWhen(defkit.CUEExpr(`len("prefix-"+parameter.name) > 63`))
|
|
|
|
comp := defkit.NewComponent("test").Validators(v)
|
|
cue := gen.GenerateParameterSchema(comp)
|
|
Expect(cue).To(ContainSubstring(`len("prefix-"+parameter.name) > 63`))
|
|
})
|
|
})
|
|
|
|
Context("ConditionalStructOp CUE Generation", func() {
|
|
It("should generate conditional struct in template output", func() {
|
|
replConfig := defkit.Object("replicationConfiguration").Optional()
|
|
|
|
comp := defkit.NewComponent("test").
|
|
Params(replConfig).
|
|
Workload("apps/v1", "Deployment").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
Set("metadata.name", defkit.Lit("test")).
|
|
ConditionalStruct(replConfig.IsSet(), "spec.replication", func(b *defkit.OutputStructBuilder) {
|
|
b.Set("role", defkit.Reference("parameter.replicationConfiguration.role"))
|
|
b.SetIf(replConfig.IsSet(), "enabled", defkit.Lit(true))
|
|
}))
|
|
})
|
|
|
|
cue := gen.GenerateTemplate(comp)
|
|
Expect(cue).To(ContainSubstring(`if parameter["replicationConfiguration"] != _|_`))
|
|
Expect(cue).To(ContainSubstring("replication:"))
|
|
Expect(cue).To(ContainSubstring("role: parameter.replicationConfiguration.role"))
|
|
})
|
|
|
|
It("should generate nested path correctly", func() {
|
|
config := defkit.Object("config").Optional()
|
|
|
|
comp := defkit.NewComponent("test").
|
|
Params(config).
|
|
Workload("v1", "ConfigMap").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
Set("metadata.name", defkit.Lit("test")).
|
|
ConditionalStruct(config.IsSet(), "spec.deep.nested.path", func(b *defkit.OutputStructBuilder) {
|
|
b.Set("key", defkit.Reference("parameter.config.key"))
|
|
}))
|
|
})
|
|
|
|
cue := gen.GenerateTemplate(comp)
|
|
Expect(cue).To(ContainSubstring("deep:"))
|
|
Expect(cue).To(ContainSubstring("nested:"))
|
|
Expect(cue).To(ContainSubstring("path:"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Builtin WithDirectFields", func() {
|
|
It("should render fields directly without $params wrapper", func() {
|
|
ws := defkit.NewWorkflowStep("test").
|
|
Params(defkit.String("env").Required()).
|
|
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
|
tpl.Builtin("app", "op.#ShareCloudResource").
|
|
WithDirectFields().
|
|
WithParams(map[string]defkit.Value{
|
|
"env": defkit.Reference("parameter.env"),
|
|
"namespace": defkit.Reference("context.namespace"),
|
|
}).
|
|
Build()
|
|
})
|
|
|
|
cue := ws.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("app: op.#ShareCloudResource & {"))
|
|
Expect(cue).To(ContainSubstring("env: parameter.env"))
|
|
Expect(cue).To(ContainSubstring("namespace: context.namespace"))
|
|
Expect(cue).NotTo(ContainSubstring("$params:"))
|
|
})
|
|
|
|
It("should still use $params when WithDirectFields is not called", func() {
|
|
ws := defkit.NewWorkflowStep("test").
|
|
Params(defkit.String("name").Required()).
|
|
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
|
tpl.Builtin("deploy", "kube.#Apply").
|
|
WithParams(map[string]defkit.Value{
|
|
"value": defkit.Reference("object"),
|
|
}).
|
|
Build()
|
|
})
|
|
|
|
cue := ws.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("deploy: kube.#Apply & {"))
|
|
Expect(cue).To(ContainSubstring("$params:"))
|
|
})
|
|
})
|
|
|
|
// --- OneOf with Default --------------------------------------------------
|
|
//
|
|
// Background: when a OneOfParam has both Optional() and Default(), the
|
|
// generated discriminator block uses sibling-scope `if name == "..."`
|
|
// references that fail CUE strict mode if the field is marked `?`.
|
|
// Default makes the value concrete; the `?` marker must be dropped.
|
|
Context("OneOf with Default", func() {
|
|
It("should drop the ? marker when a default is set", func() {
|
|
vol := defkit.OneOf("volume").Optional().Default("emptyDir").Variants(
|
|
defkit.Variant("emptyDir").WithFields(
|
|
defkit.Field("medium", defkit.ParamTypeString).Optional(),
|
|
),
|
|
defkit.Variant("configMap").WithFields(
|
|
defkit.Field("name", defkit.ParamTypeString).Required(),
|
|
),
|
|
)
|
|
schema := defkit.NewCUEGenerator().GenerateParameterSchema(
|
|
defkit.NewComponent("c").Params(vol))
|
|
Expect(schema).To(ContainSubstring(`volume: *"emptyDir" | "configMap"`))
|
|
Expect(schema).NotTo(ContainSubstring(`volume?:`))
|
|
})
|
|
|
|
It("should keep the ? marker when no default is set", func() {
|
|
vol := defkit.OneOf("volume").Optional().Variants(
|
|
defkit.Variant("a").WithFields(defkit.Field("x", defkit.ParamTypeString).Required()),
|
|
defkit.Variant("b").WithFields(defkit.Field("y", defkit.ParamTypeString).Required()),
|
|
)
|
|
schema := defkit.NewCUEGenerator().GenerateParameterSchema(
|
|
defkit.NewComponent("c").Params(vol))
|
|
Expect(schema).To(ContainSubstring(`volume?:`))
|
|
})
|
|
})
|
|
|
|
// --- Auto-import for ArrayParam list constraints ------------------------
|
|
//
|
|
// Background: ArrayParam.MinItems/MaxItems emit list.MinItems(N) /
|
|
// list.MaxItems(N), which require the CUE "list" stdlib import. The
|
|
// auto-import scanner picks this up via ArrayParam.RequiredImports.
|
|
Context("Auto-import for Array list constraints", func() {
|
|
It("should add the list import when Array.MinItems is set", func() {
|
|
ports := defkit.IntList("ports").Optional().MinItems(1)
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(ports).
|
|
Template(func(tpl *defkit.Template) {}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(`"list"`))
|
|
Expect(cue).To(ContainSubstring(`list.MinItems(1)`))
|
|
})
|
|
|
|
It("should add the list import when Array.MaxItems is set", func() {
|
|
ports := defkit.IntList("ports").Optional().MaxItems(10)
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(ports).
|
|
Template(func(tpl *defkit.Template) {}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(`"list"`))
|
|
Expect(cue).To(ContainSubstring(`list.MaxItems(10)`))
|
|
})
|
|
|
|
It("should NOT add the list import for a plain Array param", func() {
|
|
args := defkit.StringList("args").Optional()
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(args).
|
|
Template(func(tpl *defkit.Template) {}).
|
|
ToCue()
|
|
Expect(cue).NotTo(ContainSubstring(`"list"`))
|
|
})
|
|
|
|
It("should emit the list import only once when both MinItems and MaxItems are set", func() {
|
|
ports := defkit.IntList("ports").Optional().MinItems(1).MaxItems(10)
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(ports).
|
|
Template(func(tpl *defkit.Template) {}).
|
|
ToCue()
|
|
Expect(strings.Count(cue, `"list"`)).To(Equal(1))
|
|
})
|
|
|
|
It("should add the list import when Array.Contains() is used", func() {
|
|
tags := defkit.StringList("tags").Optional()
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(tags).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
SetIf(tags.Contains("gpu"), "data.gpu", defkit.Lit("true")))
|
|
}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(`"list"`))
|
|
Expect(cue).To(ContainSubstring(`list.Contains`))
|
|
})
|
|
})
|
|
|
|
// --- Optional collection rendering (end-to-end) -------------------------
|
|
//
|
|
// Background: in CUE strict mode (and the KubeVela template pipeline
|
|
// specifically), references to optional fields like `parameter.X` (dot)
|
|
// or `len(parameter.X)` trip "cannot reference optional field". The
|
|
// rendering must use the bracket-existence pattern `parameter["X"] != _|_`
|
|
// — the same form every built-in KubeVela component (cron-task.cue,
|
|
// daemon.cue, helmchart.cue) uses.
|
|
// --- Bracket-access conditional rendering --------------------------------
|
|
//
|
|
// Regression: when SetIf targets a bracket-access path (e.g.
|
|
// `data[args-empty]`), the bracket-leaf rendering previously dropped the
|
|
// node's condition and condValues, emitting the field unconditionally.
|
|
// Both single conditions and AbsentOrEmpty's two-branch expansion must
|
|
// produce wrapping if blocks for keys with hyphens / dots / etc.
|
|
Context("Bracket-access conditional rendering", func() {
|
|
It("should wrap a bracket-access SetIf in an if block (single condition)", func() {
|
|
args := defkit.StringList("args").Optional()
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(args).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
SetIf(args.IsNotEmpty(), "data[has-args]", defkit.Lit("yes")))
|
|
}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(`if parameter["args"] != _|_ if len(parameter["args"]) > 0`))
|
|
// The bracket key must be quoted (CUE requires quoting for
|
|
// non-identifier field names) and live inside the if block.
|
|
Expect(cue).To(ContainSubstring(`"has-args": "yes"`))
|
|
})
|
|
|
|
It("should wrap a bracket-access SetIf with AbsentOrEmpty in TWO if blocks", func() {
|
|
args := defkit.StringList("args").Optional()
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(args).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
SetIf(args.IsEmpty(), "data[args-empty]", defkit.Lit("yes")))
|
|
}).
|
|
ToCue()
|
|
// Two branches: absent + set-and-empty, both wrapping the same key.
|
|
Expect(cue).To(ContainSubstring(`if parameter["args"] == _|_`))
|
|
Expect(cue).To(ContainSubstring(`if parameter["args"] != _|_ if len(parameter["args"]) == 0`))
|
|
Expect(strings.Count(cue, `"args-empty": "yes"`)).To(Equal(2))
|
|
})
|
|
|
|
It("should render an unconditional bracket-key Set without an if block", func() {
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
Set("data[my-key]", defkit.Lit("v")))
|
|
}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(`"my-key": "v"`))
|
|
Expect(cue).NotTo(ContainSubstring(`if `))
|
|
})
|
|
|
|
It("should render a bracket-key parent with a nested child as a struct", func() {
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
Set("metadata.annotations[my-key].nested", defkit.Lit("v")))
|
|
}).
|
|
ToCue()
|
|
// Bracket key with a child renders as `"my-key": { nested: ... }`.
|
|
Expect(cue).To(ContainSubstring(`"my-key": {`))
|
|
Expect(cue).To(ContainSubstring(`nested: "v"`))
|
|
})
|
|
|
|
It("should keep per-bracket-key conditions when sibling keys have different conds", func() {
|
|
args := defkit.StringList("args").Optional()
|
|
tags := defkit.StringList("tags").Optional()
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(args, tags).
|
|
Template(func(tpl *defkit.Template) {
|
|
// Two bracket keys with DIFFERENT conditions — liftChildConditions
|
|
// can't merge them, so each bracket leaf keeps its own cond and
|
|
// writeBracketKeyNode hits the per-leaf if-block emission path.
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
SetIf(args.IsNotEmpty(), "data[has-args]", defkit.Lit("yes")).
|
|
SetIf(tags.IsNotEmpty(), "data[has-tags]", defkit.Lit("yes")))
|
|
}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(`"has-args": "yes"`))
|
|
Expect(cue).To(ContainSubstring(`"has-tags": "yes"`))
|
|
Expect(cue).To(ContainSubstring(`len(parameter["args"]) > 0`))
|
|
Expect(cue).To(ContainSubstring(`len(parameter["tags"]) > 0`))
|
|
})
|
|
|
|
It("should keep per-bracket-key condValues when AbsentOrEmpty mixes with other conds", func() {
|
|
args := defkit.StringList("args").Optional()
|
|
tags := defkit.StringList("tags").Optional()
|
|
// Mix AbsentOrEmpty (two condValues) with another condition on a
|
|
// sibling bracket key — prevents liftChildConditions from sharing,
|
|
// so the bracket leaf keeps its condValues and writeBracketKeyNode
|
|
// hits `case len(node.condValues) > 0`.
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(args, tags).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
SetIf(args.IsEmpty(), "data[args-empty]", defkit.Lit("yes")).
|
|
SetIf(tags.IsNotEmpty(), "data[has-tags]", defkit.Lit("yes")))
|
|
}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(`"args-empty": "yes"`))
|
|
Expect(cue).To(ContainSubstring(`"has-tags": "yes"`))
|
|
})
|
|
})
|
|
|
|
// --- Compound condition rendering ---------------------------------------
|
|
//
|
|
// Conditions that use CUE chained-if syntax (LenCondition, ArrayContains)
|
|
// cannot live inside `(...) && (...)` — compound joiners must use ` if `
|
|
// instead. Cover the chained-guard branches of And / LogicalExpr and the
|
|
// LogicalExpr OR pass-through.
|
|
Context("Compound condition rendering", func() {
|
|
It("should join AND with chained-guard LenCondition operand using ` if `", func() {
|
|
args := defkit.StringList("args").Optional()
|
|
flag := defkit.Bool("flag").Default(false)
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(args, flag).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
SetIf(defkit.And(args.LenGt(0), flag.IsTrue()),
|
|
"data[both]", defkit.Lit("y")))
|
|
}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(`len(parameter["args"]) > 0`))
|
|
// The parenthesized && form must NOT wrap a chained-guard operand.
|
|
Expect(cue).NotTo(MatchRegexp(`\([^)]*len\(parameter\["args"\]\)[^)]*\) && `))
|
|
})
|
|
|
|
It("should join AND with chained-guard ArrayContains operands using ` if `", func() {
|
|
tags := defkit.StringList("tags").Optional()
|
|
flag := defkit.Bool("flag").Default(false)
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(tags, flag).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
SetIf(defkit.And(tags.Contains("gpu"), flag.IsTrue(), tags.LenGt(0)),
|
|
"data[ok]", defkit.Lit("y")))
|
|
}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(`list.Contains(parameter["tags"], "gpu")`))
|
|
Expect(cue).To(ContainSubstring(`len(parameter["tags"]) > 0`))
|
|
})
|
|
|
|
It("should join LogicalExpr OR with ` || ` regardless of chained guards", func() {
|
|
flag := defkit.Bool("flag").Default(false)
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(flag).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
SetIf(defkit.Or(flag.IsTrue(), flag.IsFalse()),
|
|
"data[either]", defkit.Lit("y")))
|
|
}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(` || `))
|
|
})
|
|
})
|
|
|
|
Context("Optional collection rendering", func() {
|
|
It("should render IsNotEmpty() on optional Array as chained-if guard with len() > 0", func() {
|
|
args := defkit.StringList("args").Optional()
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(args).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
SetIf(args.IsNotEmpty(), "data.x", defkit.Lit("y")))
|
|
}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(`if parameter["args"] != _|_`))
|
|
Expect(cue).To(ContainSubstring(`len(parameter["args"]) > 0`))
|
|
Expect(cue).NotTo(ContainSubstring(`len(parameter.args)`))
|
|
Expect(cue).NotTo(ContainSubstring(`parameter.args | []`))
|
|
})
|
|
|
|
It("should render IsEmpty() on optional Array as two if blocks (absent OR set-and-empty)", func() {
|
|
args := defkit.StringList("args").Optional()
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(args).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
SetIf(args.IsEmpty(), "data.empty", defkit.Lit("yes")))
|
|
}).
|
|
ToCue()
|
|
// Branch 1: field absent
|
|
Expect(cue).To(ContainSubstring(`if parameter["args"] == _|_`))
|
|
// Branch 2: field set and empty
|
|
Expect(cue).To(ContainSubstring(`if parameter["args"] != _|_ if len(parameter["args"]) == 0`))
|
|
})
|
|
|
|
It("should expand AbsentOrEmpty in SpreadIf into two spread blocks", func() {
|
|
extra := defkit.Map("extra").Of(defkit.ParamTypeString).Optional()
|
|
// SpreadIf renders only when its target node also has at least one
|
|
// regular child (otherwise the leaf-with-only-spreads case is a
|
|
// pre-existing no-op in writeFieldNode). Add a sibling Set under
|
|
// metadata.labels so the spread is exercised.
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(extra).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
Set("metadata.labels.fixed", defkit.Lit("y")).
|
|
SpreadIf(extra.IsEmpty(), "metadata.labels", defkit.Reference("parameter.extra")))
|
|
}).
|
|
ToCue()
|
|
// Both AbsentOrEmpty branches must wrap the spread.
|
|
Expect(cue).To(ContainSubstring(`if parameter["extra"] == _|_`))
|
|
Expect(cue).To(ContainSubstring(`if parameter["extra"] != _|_ if len(parameter["extra"]) == 0`))
|
|
})
|
|
|
|
It("should expand AbsentOrEmpty SetIf inside an IfBlock with combined guards", func() {
|
|
args := defkit.StringList("args").Optional()
|
|
flag := defkit.Bool("flag").Default(false)
|
|
cue := defkit.NewComponent("c").
|
|
Workload("v1", "ConfigMap").
|
|
Params(args, flag).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
If(flag.IsTrue()).
|
|
SetIf(args.IsEmpty(), "data[when-flag]", defkit.Lit("y")).
|
|
EndIf())
|
|
}).
|
|
ToCue()
|
|
// Outer flag guard combined with each inner AbsentOrEmpty branch.
|
|
Expect(cue).To(ContainSubstring(`parameter["args"] == _|_`))
|
|
Expect(cue).To(ContainSubstring(`len(parameter["args"]) == 0`))
|
|
Expect(cue).To(ContainSubstring(`parameter.flag`))
|
|
})
|
|
|
|
It("should render Map.HasKey() with the daemon.cue two-clause guard", func() {
|
|
cfg := defkit.Map("config").Of(defkit.ParamTypeString).Optional()
|
|
schema := defkit.NewCUEGenerator().GenerateParameterSchema(
|
|
defkit.NewComponent("c").Params(cfg).
|
|
Validators(
|
|
defkit.Validate("debug must not be set").
|
|
WithName("_v").
|
|
FailWhen(cfg.HasKey("debug")),
|
|
))
|
|
Expect(schema).To(ContainSubstring(`parameter["config"] != _|_`))
|
|
Expect(schema).To(ContainSubstring(`parameter["config"].debug != _|_`))
|
|
Expect(schema).NotTo(ContainSubstring(`parameter.config.debug != _|_`))
|
|
})
|
|
|
|
It("should render a multi-collection ComponentDefinition without dot-references to optional fields", func() {
|
|
image := defkit.String("image").Required()
|
|
args := defkit.StringList("args").Optional()
|
|
ports := defkit.IntList("ports").Optional().MinItems(1).MaxItems(10)
|
|
labels := defkit.StringKeyMap("labels").Optional()
|
|
anns := defkit.Map("annotations").Of(defkit.ParamTypeString).Optional()
|
|
vol := defkit.OneOf("volume").Optional().Default("emptyDir").Variants(
|
|
defkit.Variant("emptyDir").WithFields(
|
|
defkit.Field("medium", defkit.ParamTypeString).Optional(),
|
|
),
|
|
defkit.Variant("configMap").WithFields(
|
|
defkit.Field("name", defkit.ParamTypeString).Required(),
|
|
),
|
|
)
|
|
|
|
c := defkit.NewComponent("collection-showcase").
|
|
Workload("apps/v1", "Deployment").
|
|
PodSpecPath("spec.template.spec").
|
|
Params(image, args, ports, labels, anns, vol).
|
|
Template(func(tpl *defkit.Template) {
|
|
vela := defkit.VelaCtx()
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
Set("metadata.name", vela.Name()).
|
|
Set("spec.template.spec.containers[0].image", image).
|
|
SetIf(labels.IsNotEmpty(), "metadata.labels", labels).
|
|
SetIf(anns.IsNotEmpty(), "metadata.annotations", anns).
|
|
SetIf(args.IsNotEmpty(), "spec.template.spec.containers[0].args", args).
|
|
SetIf(ports.IsNotEmpty(), "metadata.annotations[showcase/ports-set]", defkit.Lit("true")))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
|
|
// 1. Schema-level: list import present, MinItems/MaxItems intact.
|
|
Expect(cue).To(ContainSubstring(`"list"`))
|
|
Expect(cue).To(ContainSubstring(`list.MinItems(1) & list.MaxItems(10)`))
|
|
|
|
// 2. Every optional-collection guard uses bracket existence.
|
|
Expect(cue).To(ContainSubstring(`if parameter["labels"] != _|_`))
|
|
Expect(cue).To(ContainSubstring(`if parameter["annotations"] != _|_`))
|
|
Expect(cue).To(ContainSubstring(`if parameter["args"] != _|_`))
|
|
Expect(cue).To(ContainSubstring(`if parameter["ports"] != _|_`))
|
|
|
|
// 3. None of the strict-mode-failing forms appear.
|
|
Expect(cue).NotTo(ContainSubstring(`len(parameter.labels)`))
|
|
Expect(cue).NotTo(ContainSubstring(`len(parameter.args)`))
|
|
Expect(cue).NotTo(ContainSubstring(`parameter.labels | {}`))
|
|
Expect(cue).NotTo(ContainSubstring(`parameter.args | []`))
|
|
|
|
// 4. OneOf with Default: discriminator field is concrete, not optional.
|
|
Expect(cue).To(ContainSubstring(`volume: *"emptyDir" | "configMap"`))
|
|
Expect(cue).NotTo(ContainSubstring(`volume?:`))
|
|
|
|
// 5. Inside a guarded if-block, dot syntax for the value reference
|
|
// is still emitted (safe because the guard establishes existence).
|
|
Expect(cue).To(ContainSubstring(`labels: parameter.labels`))
|
|
Expect(cue).To(ContainSubstring(`args: parameter.args`))
|
|
})
|
|
})
|
|
|
|
// ------------------------------------------------------------------
|
|
// Regression coverage for two related defkit-cuegen bugs:
|
|
// 1. predicateToCUE() hardcoded the iteration variable to "v", so
|
|
// ForEachWithGuardedFilteredVar with any other name emitted
|
|
// `for p in source if v.field == ...` — broken CUE.
|
|
// 2. From().Filter().Map().Dedupe() rendered only the dedupe loop
|
|
// and silently dropped the Filter and Map stages.
|
|
// ------------------------------------------------------------------
|
|
|
|
Describe("Filter predicate honours iteration variable name", func() {
|
|
It("ForEachWithGuardedFilteredVar with a custom var renames the predicate", func() {
|
|
ports := defkit.Array("ports").WithFields(
|
|
defkit.Int("port"),
|
|
defkit.Bool("expose").Default(false),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(ports).
|
|
Template(func(tpl *defkit.Template) {
|
|
arr := defkit.NewArray().ForEachWithGuardedFilteredVar(
|
|
"p",
|
|
ports.IsSet(),
|
|
defkit.FieldEquals("expose", true),
|
|
ports,
|
|
func(item *defkit.ItemBuilder) {
|
|
p := item.Var()
|
|
item.Set("port", p.Field("port"))
|
|
})
|
|
tpl.Output(defkit.NewResource("v1", "Service").
|
|
Set("spec.ports", arr))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
// The for-loop must iterate `p`, AND the filter must reference `p`.
|
|
Expect(cue).To(ContainSubstring("for p in parameter.ports if p.expose == true"))
|
|
// The buggy combination — iteration `p` with filter against `v` —
|
|
// must not regress.
|
|
Expect(cue).NotTo(ContainSubstring("for p in parameter.ports if v.expose"))
|
|
})
|
|
|
|
It("ForEachWith with the default 'v' variable still renders v.field", func() {
|
|
ports := defkit.Array("ports").WithFields(
|
|
defkit.Int("port"),
|
|
defkit.Bool("expose").Default(false),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(ports).
|
|
Template(func(tpl *defkit.Template) {
|
|
arr := defkit.NewArray().ForEachWithGuardedFiltered(
|
|
ports.IsSet(),
|
|
defkit.FieldEquals("expose", true),
|
|
ports,
|
|
func(item *defkit.ItemBuilder) {
|
|
v := item.Var()
|
|
item.Set("port", v.Field("port"))
|
|
})
|
|
tpl.Output(defkit.NewResource("v1", "Service").
|
|
Set("spec.ports", arr))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring("for v in parameter.ports if v.expose == true"))
|
|
})
|
|
|
|
It("ForEachWithVar renames the iteration variable in the body", func() {
|
|
env := defkit.Array("env").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("value"),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(env).
|
|
Template(func(tpl *defkit.Template) {
|
|
arr := defkit.NewArray().ForEachWithVar("e", env,
|
|
func(item *defkit.ItemBuilder) {
|
|
e := item.Var()
|
|
item.Set("name", e.Field("name"))
|
|
item.Set("value", e.Field("value"))
|
|
})
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
Set("spec.template.spec.containers[0].env", arr))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring("for e in parameter.env"))
|
|
Expect(cue).To(ContainSubstring("name: e.name"))
|
|
Expect(cue).To(ContainSubstring("value: e.value"))
|
|
})
|
|
|
|
It("ItemBuilder.Set expands a dotted path into nested fields", func() {
|
|
maxRead := defkit.Int("maxReadRequestUnits").Optional()
|
|
indexes := defkit.Array("indexes").WithFields(
|
|
defkit.String("name"),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("table").
|
|
Workload("v1", "ConfigMap").
|
|
Params(indexes, maxRead).
|
|
Template(func(tpl *defkit.Template) {
|
|
arr := defkit.NewArray().ForEachWith(indexes,
|
|
func(item *defkit.ItemBuilder) {
|
|
v := item.Var()
|
|
item.Set("name", v.Field("name"))
|
|
item.Set("onDemandThroughput.maxReadRequestUnits", maxRead)
|
|
})
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
Set("spec.indexes", arr))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring("onDemandThroughput: maxReadRequestUnits: parameter.maxReadRequestUnits"))
|
|
// The unexpanded label is not valid CUE and must not reappear.
|
|
Expect(cue).NotTo(ContainSubstring("onDemandThroughput.maxReadRequestUnits:"))
|
|
// A single-segment field is unaffected.
|
|
Expect(cue).To(ContainSubstring("name: v.name"))
|
|
Expect(parseCUE(cue)).To(Succeed())
|
|
})
|
|
|
|
It("ItemBuilder.Set expands a dotted path inside a conditional block", func() {
|
|
maxRead := defkit.Int("maxReadRequestUnits").Optional()
|
|
maxWrite := defkit.Int("maxWriteRequestUnits").Optional()
|
|
indexes := defkit.Array("indexes").WithFields(
|
|
defkit.String("name"),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("table").
|
|
Workload("v1", "ConfigMap").
|
|
Params(indexes, maxRead, maxWrite).
|
|
Template(func(tpl *defkit.Template) {
|
|
arr := defkit.NewArray().ForEachWith(indexes,
|
|
func(item *defkit.ItemBuilder) {
|
|
item.If(maxRead.IsSet(), func() {
|
|
item.Set("onDemandThroughput.maxReadRequestUnits", maxRead)
|
|
})
|
|
item.If(maxWrite.IsSet(), func() {
|
|
item.Set("onDemandThroughput.maxWriteRequestUnits", maxWrite)
|
|
})
|
|
})
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
Set("spec.indexes", arr))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
// Sibling writes to the same parent unify into one struct.
|
|
Expect(cue).To(ContainSubstring("onDemandThroughput: maxReadRequestUnits: parameter.maxReadRequestUnits"))
|
|
Expect(cue).To(ContainSubstring("onDemandThroughput: maxWriteRequestUnits: parameter.maxWriteRequestUnits"))
|
|
Expect(parseCUE(cue)).To(Succeed())
|
|
})
|
|
|
|
It("ItemBuilder.Set does not split on a dot inside a bracketed key", func() {
|
|
items := defkit.Array("items").WithFields(
|
|
defkit.String("value"),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("annotated").
|
|
Workload("v1", "ConfigMap").
|
|
Params(items).
|
|
Template(func(tpl *defkit.Template) {
|
|
arr := defkit.NewArray().ForEachWith(items,
|
|
func(item *defkit.ItemBuilder) {
|
|
v := item.Var()
|
|
item.Set(`metadata.annotations["example.com/owner"]`, v.Field("value"))
|
|
})
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
Set("spec.items", arr))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
// The dot inside the brackets is part of the key, not a separator.
|
|
// Bracketed keys are not otherwise supported here; this only guards
|
|
// against one being split apart.
|
|
Expect(cue).To(ContainSubstring(`annotations["example.com/owner"]: v.value`))
|
|
Expect(cue).NotTo(ContainSubstring(`example: com/owner`))
|
|
})
|
|
|
|
It("ArrayElement.SetIf preserves dots inside bracketed keys", func() {
|
|
enabled := defkit.Bool("enabled").Optional()
|
|
items := defkit.Array("items").WithFields(
|
|
defkit.String("value"),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("annotated").
|
|
Workload("v1", "ConfigMap").
|
|
Params(enabled, items).
|
|
Template(func(tpl *defkit.Template) {
|
|
const path = `labels["app.kubernetes.io/name"]`
|
|
arr := defkit.NewArray().
|
|
Item(defkit.NewArrayElement().
|
|
SetIf(enabled.IsSet(), path, defkit.Lit("static"))).
|
|
ForEach(items, defkit.NewArrayElement().
|
|
SetIf(enabled.IsSet(), path, defkit.Reference("m.value")))
|
|
tpl.Output(defkit.NewResource("v1", "ConfigMap").
|
|
Set("spec.items", arr))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(strings.Count(cue, `labels["app.kubernetes.io/name"]:`)).To(Equal(2))
|
|
Expect(cue).NotTo(ContainSubstring(`labels["app: kubernetes: io/name"]:`))
|
|
})
|
|
})
|
|
|
|
Describe("From().Filter().Map().Dedupe() pipeline", func() {
|
|
It("renders Filter, Map, and Dedupe stages together", func() {
|
|
env := defkit.Array("env").WithFields(
|
|
defkit.String("name"),
|
|
defkit.Bool("required").Default(false),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(env).
|
|
Template(func(tpl *defkit.Template) {
|
|
chain := defkit.From(env).
|
|
Filter(defkit.FieldEquals("required", true)).
|
|
Map(defkit.FieldMap{
|
|
"secretName": defkit.FieldRef("name"),
|
|
}).
|
|
Dedupe("name")
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
SetIf(env.IsSet(),
|
|
"spec.template.spec.containers[0].envFrom", chain))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
|
|
// Filter must reach the dedupe inner loops, on both vi and vj,
|
|
// or duplicate detection compares across the filter boundary.
|
|
Expect(cue).To(ContainSubstring("for i, vi in parameter.env if vi.required == true"))
|
|
Expect(cue).To(ContainSubstring("vj.required == true && vi.name == vj.name"))
|
|
|
|
// Map output must replace the bare `vi` body that the buggy
|
|
// renderer used to emit; the mapped field reads off "val".
|
|
Expect(cue).To(ContainSubstring("if val._ignore == _|_"))
|
|
Expect(cue).To(ContainSubstring("secretName: val.name"))
|
|
|
|
// Negative: the previous output that silently dropped Filter+Map
|
|
// must not regress. A plain `val,` line in the body would mean
|
|
// the map was not applied.
|
|
Expect(cue).NotTo(MatchRegexp(`if val\._ignore == _\|_ \{\s*val,`))
|
|
})
|
|
|
|
It("Filter+Dedupe without Map keeps the deduped element", func() {
|
|
env := defkit.Array("env").WithFields(
|
|
defkit.String("name"),
|
|
defkit.Bool("required").Default(false),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(env).
|
|
Template(func(tpl *defkit.Template) {
|
|
chain := defkit.From(env).
|
|
Filter(defkit.FieldEquals("required", true)).
|
|
Dedupe("name")
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
SetIf(env.IsSet(),
|
|
"spec.template.spec.containers[0].env", chain))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring("for i, vi in parameter.env if vi.required == true"))
|
|
Expect(cue).To(ContainSubstring("vj.required == true && vi.name == vj.name"))
|
|
// Without a Map, the deduped body is the bare element.
|
|
Expect(cue).To(ContainSubstring("val,"))
|
|
})
|
|
|
|
It("Map+Dedupe without Filter still renders the mapped struct", func() {
|
|
env := defkit.Array("env").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("value"),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(env).
|
|
Template(func(tpl *defkit.Template) {
|
|
chain := defkit.From(env).
|
|
Map(defkit.FieldMap{
|
|
"name": defkit.FieldRef("name"),
|
|
"value": defkit.FieldRef("value"),
|
|
}).
|
|
Dedupe("name")
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
SetIf(env.IsSet(),
|
|
"spec.template.spec.containers[0].env", chain))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
// No filter clause should leak into the i-loop.
|
|
Expect(cue).NotTo(ContainSubstring("for i, vi in parameter.env if"))
|
|
// Map output references "val" (the dedupe outer var), not "v".
|
|
Expect(cue).To(ContainSubstring("name: val.name"))
|
|
Expect(cue).To(ContainSubstring("value: val.value"))
|
|
})
|
|
})
|
|
|
|
// ------------------------------------------------------------------
|
|
// Regression coverage for the defkit audit bugs:
|
|
// Bug 1: SetIf on an array-element path drops the if-guard.
|
|
// Bug 2: tpl.AddLetBinding(...) ignored on components.
|
|
// Bug 3: tpl.Helper(...).Filter(...).Guard(...).Build() drops both.
|
|
// (Bug 4 is in status_expr_test.go.)
|
|
// ------------------------------------------------------------------
|
|
|
|
Describe("TestFix_ArrayElementSetIfGuard (Bug 1)", func() {
|
|
It("keeps the if-guard around an array-element SetIf body", func() {
|
|
cmd := defkit.StringList("cmd").Optional()
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(cmd).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
Set("spec.template.spec.containers[0].image", defkit.Lit("nginx")).
|
|
SetIf(cmd.IsSet(), "spec.template.spec.containers[0].command", cmd))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring(`if parameter["cmd"] != _|_`))
|
|
Expect(cue).To(ContainSubstring("command: parameter.cmd"))
|
|
})
|
|
})
|
|
|
|
Describe("TestFix_ComponentLetBinding (Bug 2)", func() {
|
|
It("emits the let binding above the output block", func() {
|
|
privileges := defkit.Array("privileges").WithFields(
|
|
defkit.String("scope"),
|
|
).Optional()
|
|
c := defkit.NewComponent("rbac").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(privileges).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.AddLetBinding("_clusterPrivileges",
|
|
defkit.From(privileges).
|
|
Filter(defkit.FieldEquals("scope", "cluster")).
|
|
Guard(privileges.IsSet()))
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment"))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring("let _clusterPrivileges = "))
|
|
Expect(cue).To(ContainSubstring(`if parameter["privileges"] != _|_`))
|
|
Expect(cue).To(ContainSubstring(`v.scope == "cluster"`))
|
|
// The let must appear before the output block.
|
|
Expect(strings.Index(cue, "let _clusterPrivileges")).To(BeNumerically("<", strings.Index(cue, "output:")))
|
|
})
|
|
})
|
|
|
|
Describe("TestFix_HelperFilterGuard (Bug 3)", func() {
|
|
It("emits both Filter and Guard inside the helper comprehension", func() {
|
|
privileges := defkit.Array("privileges").WithFields(
|
|
defkit.String("scope"),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("rbac").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(privileges).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Helper("_clusterPrivileges").
|
|
From(privileges).
|
|
Filter(defkit.FieldEquals("scope", "cluster")).
|
|
Guard(privileges.IsSet()).
|
|
Build()
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment"))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring(`if parameter["privileges"] != _|_`))
|
|
Expect(cue).To(ContainSubstring(`for v in parameter.privileges if v.scope == "cluster"`))
|
|
// Negative: the previous broken output silently dropped both.
|
|
Expect(cue).NotTo(MatchRegexp(`_clusterPrivileges: \[for v in parameter\.privileges \{ v \}\]`))
|
|
})
|
|
})
|
|
|
|
// ------------------------------------------------------------------
|
|
// Additional coverage for writeMapBody field variants on the dedupe
|
|
// path: OptionalField, CompoundOptionalField, ConditionalOrFieldRef,
|
|
// and MapVariant. The non-dedupe path is already covered by existing
|
|
// "GenerateFullDefinition with MapVariant" tests; these scenarios
|
|
// exercise the body that writeMapBody emits when a Dedupe stage is in
|
|
// the chain (so the iteration variable is "val", not "v").
|
|
// ------------------------------------------------------------------
|
|
Describe("Dedupe + Map variants exercise writeMapBody scope substitution", func() {
|
|
It("substitutes val.X for FieldRef and emits OptionalField guard", func() {
|
|
env := defkit.Array("env").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("value").Optional(),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(env).
|
|
Template(func(tpl *defkit.Template) {
|
|
chain := defkit.From(env).
|
|
Map(defkit.FieldMap{
|
|
"name": defkit.FieldRef("name"),
|
|
"value": defkit.OptionalFieldRef("value"),
|
|
}).
|
|
Dedupe("name")
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
SetIf(env.IsSet(),
|
|
"spec.template.spec.containers[0].env", chain))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
// OptionalField guard must reference the deduped scope variable.
|
|
Expect(cue).To(ContainSubstring("if val.value != _|_"))
|
|
Expect(cue).To(ContainSubstring("value: val.value"))
|
|
Expect(cue).To(ContainSubstring("name: val.name"))
|
|
})
|
|
|
|
It("emits CompoundOptionalField with an additional condition", func() {
|
|
mode := defkit.Enum("mode").Values("inline", "ref").Default("inline")
|
|
env := defkit.Array("env").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("value").Optional(),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(mode, env).
|
|
Template(func(tpl *defkit.Template) {
|
|
chain := defkit.From(env).
|
|
Map(defkit.FieldMap{
|
|
"name": defkit.FieldRef("name"),
|
|
"value": defkit.OptionalFieldWithCond("value",
|
|
defkit.Eq(mode, defkit.Lit("inline"))),
|
|
}).
|
|
Dedupe("name")
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
SetIf(env.IsSet(),
|
|
"spec.template.spec.containers[0].env", chain))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring("if val.value != _|_"))
|
|
Expect(cue).To(ContainSubstring(`parameter.mode == "inline"`))
|
|
Expect(cue).To(ContainSubstring("value: val.value"))
|
|
})
|
|
|
|
It("emits ConditionalOrFieldRef with primary/fallback branches", func() {
|
|
env := defkit.Array("env").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("override").Optional(),
|
|
defkit.String("defaultValue").Optional(),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(env).
|
|
Template(func(tpl *defkit.Template) {
|
|
chain := defkit.From(env).
|
|
Map(defkit.FieldMap{
|
|
"name": defkit.FieldRef("name"),
|
|
"value": defkit.FieldRef("override").OrConditional(defkit.FieldRef("defaultValue")),
|
|
}).
|
|
Dedupe("name")
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
SetIf(env.IsSet(),
|
|
"spec.template.spec.containers[0].env", chain))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
// Primary present branch.
|
|
Expect(cue).To(ContainSubstring("if val.override != _|_"))
|
|
Expect(cue).To(ContainSubstring("value: val.override"))
|
|
// Primary absent branch — fallback FieldRef("defaultValue") was
|
|
// emitted as "v.defaultValue" by fieldValueToCUE; subVar must
|
|
// rewrite it to "val.defaultValue" in the dedupe scope.
|
|
Expect(cue).To(ContainSubstring("if val.override == _|_"))
|
|
Expect(cue).To(ContainSubstring("value: val.defaultValue"))
|
|
})
|
|
|
|
It("emits MapVariant blocks with val-scoped discriminator and fields", func() {
|
|
volumes := defkit.Array("volumes").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("type"),
|
|
defkit.String("claimName").Optional(),
|
|
defkit.String("medium").Optional(),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(volumes).
|
|
Template(func(tpl *defkit.Template) {
|
|
chain := defkit.From(volumes).
|
|
Map(defkit.FieldMap{
|
|
"name": defkit.FieldRef("name"),
|
|
}).
|
|
MapVariant("type", "pvc", defkit.FieldMap{
|
|
"persistentVolumeClaim.claimName": defkit.FieldRef("claimName"),
|
|
}).
|
|
MapVariant("type", "emptyDir", defkit.FieldMap{
|
|
"emptyDir.medium": defkit.OptionalFieldRef("medium"),
|
|
}).
|
|
Dedupe("name")
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
SetIf(volumes.IsSet(),
|
|
"spec.template.spec.volumes", chain))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
// Discriminator and variant fields read off "val".
|
|
Expect(cue).To(ContainSubstring(`if val.type == "pvc"`))
|
|
Expect(cue).To(ContainSubstring("persistentVolumeClaim.claimName: val.claimName"))
|
|
Expect(cue).To(ContainSubstring(`if val.type == "emptyDir"`))
|
|
// OptionalField nested inside MapVariant guards on val.medium.
|
|
Expect(cue).To(ContainSubstring("if val.medium != _|_"))
|
|
Expect(cue).To(ContainSubstring("emptyDir.medium: val.medium"))
|
|
})
|
|
})
|
|
|
|
// ------------------------------------------------------------------
|
|
// predicateToCUE FieldIsSet branch — exercised when Filter() takes a
|
|
// FieldExists predicate. The dedupe path threads it through the inner
|
|
// loops, so the rendered CUE must contain "v.field != _|_".
|
|
// ------------------------------------------------------------------
|
|
Describe("Filter with FieldExists predicate", func() {
|
|
It("renders v.field != _|_ inside the dedupe loops", func() {
|
|
env := defkit.Array("env").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("optional").Optional(),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(env).
|
|
Template(func(tpl *defkit.Template) {
|
|
chain := defkit.From(env).
|
|
Filter(defkit.FieldExists("optional")).
|
|
Dedupe("name")
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
SetIf(env.IsSet(),
|
|
"spec.template.spec.containers[0].env", chain))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring("vi.optional != _|_"))
|
|
Expect(cue).To(ContainSubstring("vj.optional != _|_"))
|
|
})
|
|
})
|
|
|
|
// ------------------------------------------------------------------
|
|
// collectFilterCondition AND-composes multiple Filter() stages into
|
|
// one CUE expression on each side of the dedupe inner loops.
|
|
// ------------------------------------------------------------------
|
|
Describe("Multiple Filter stages compose via && in dedupe loops", func() {
|
|
It("ANDs two Filter predicates inside the dedupe i/j loops", func() {
|
|
env := defkit.Array("env").WithFields(
|
|
defkit.String("name"),
|
|
defkit.Bool("required").Default(false),
|
|
defkit.Bool("active").Default(true),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(env).
|
|
Template(func(tpl *defkit.Template) {
|
|
chain := defkit.From(env).
|
|
Filter(defkit.FieldEquals("required", true)).
|
|
Filter(defkit.FieldEquals("active", true)).
|
|
Dedupe("name")
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
|
|
SetIf(env.IsSet(),
|
|
"spec.template.spec.containers[0].env", chain))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
// Both filters reach the i-loop via " && ".
|
|
Expect(cue).To(ContainSubstring("vi.required == true && vi.active == true"))
|
|
// And the j-loop precedes the dedupe key compare.
|
|
Expect(cue).To(ContainSubstring("vj.required == true && vj.active == true && vi.name == vj.name"))
|
|
})
|
|
})
|
|
|
|
// ------------------------------------------------------------------
|
|
// writeCollectionOpHelper bare-comprehension default path. With no
|
|
// Filter, Map, Dedupe, or Pick on the chain the helper renders as
|
|
// `[for v in source { v }]` — covers the bare-default branch.
|
|
// ------------------------------------------------------------------
|
|
Describe("Helper with no Filter / no Map / no Dedupe renders bare comprehension", func() {
|
|
It("emits [for v in source { v }]", func() {
|
|
ports := defkit.Array("ports").WithFields(
|
|
defkit.Int("port"),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(ports).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Helper("_passthrough").
|
|
From(ports).
|
|
Build()
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment"))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring(`for v in parameter.ports { v }`))
|
|
})
|
|
})
|
|
|
|
// ------------------------------------------------------------------
|
|
// writeMultiSourceHelper / multiSourceToCUE pick up the renamed
|
|
// predicateToCUE(pred, "v") call site whenever a MultiSource chain
|
|
// includes a Filter. tpl.Helper(...).FromFields(...).Filter(...) wires
|
|
// that path end-to-end.
|
|
// ------------------------------------------------------------------
|
|
Describe("MultiSource helper with Filter exercises renamed predicate path", func() {
|
|
It("emits the filter expression in the multi-source comprehension", func() {
|
|
volumes := defkit.Struct("volumes").WithFields(
|
|
defkit.Field("pvc", defkit.ParamTypeArray).Optional(),
|
|
defkit.Field("configMap", defkit.ParamTypeArray).Optional(),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("svc").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(volumes).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Helper("_exposed").
|
|
FromFields(volumes, "pvc", "configMap").
|
|
Filter(defkit.FieldEquals("expose", true)).
|
|
Build()
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment"))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring("v.expose == true"))
|
|
})
|
|
})
|
|
|
|
// ------------------------------------------------------------------
|
|
// writeCollectionOpHelper Default path with both Filter and Guard.
|
|
// Triggered when a helper has filters and a guard but no Map / Dedupe /
|
|
// MapVariant / Reduce — the renderer must emit the guard-prefixed
|
|
// comprehension with the filter clause inline.
|
|
// ------------------------------------------------------------------
|
|
Describe("Helper with Filter+Guard but no Map renders guard-prefixed comprehension", func() {
|
|
It("emits if-guard followed by for-v-in source if cond { v }", func() {
|
|
privileges := defkit.Array("privileges").WithFields(
|
|
defkit.String("scope"),
|
|
).Optional()
|
|
|
|
c := defkit.NewComponent("rbac").
|
|
Workload("apps/v1", "Deployment").
|
|
Params(privileges).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Helper("_clusterScopes").
|
|
From(privileges).
|
|
Filter(defkit.FieldEquals("scope", "cluster")).
|
|
Guard(privileges.IsSet()).
|
|
Build()
|
|
tpl.Output(defkit.NewResource("apps/v1", "Deployment"))
|
|
})
|
|
|
|
cue := c.ToCue()
|
|
Expect(cue).To(ContainSubstring(`if parameter["privileges"] != _|_`))
|
|
Expect(cue).To(ContainSubstring(`for v in parameter.privileges if v.scope == "cluster"`))
|
|
})
|
|
})
|
|
})
|