mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 20:17:04 +00:00
* Fix: update predicateToCUE to honor custom iteration variable and ensure Filter, Map, and Dedupe stages are rendered correctly Signed-off-by: Vishal Kumar <vishal210893@gmail.com> * Chore: remove accidentally committed .claude-session-head Signed-off-by: Vishal Kumar <vishal210893@gmail.com> * Fix: ensure let bindings are emitted before output block and handle guard conditions in list comprehensions Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com> Signed-off-by: Vishal Kumar <vishal210893@gmail.com> * Fix: update ArrayConcat to use list.Concat for CUE v0.11 compatibility and auto-detect required imports Signed-off-by: Vishal Kumar <vishal210893@gmail.com> * Fix: enhance test coverage for writeMapBody field variants and import detection in trait templates Signed-off-by: Vishal Kumar <vishal210893@gmail.com> * Fix: ensure required imports are detected for traits without a Template() function Signed-off-by: Vishal Kumar <vishal210893@gmail.com> --------- Signed-off-by: Vishal Kumar <vishal210893@gmail.com> Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com> Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
2141 lines
74 KiB
Go
2141 lines
74 KiB
Go
/*
|
|
Copyright 2025 The KubeVela Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package defkit_test
|
|
|
|
import (
|
|
"strings"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
|
|
"github.com/oam-dev/kubevela/pkg/definition/defkit"
|
|
)
|
|
|
|
var _ = Describe("TraitDefinition", func() {
|
|
|
|
Context("Basic Builder Methods", func() {
|
|
It("should set name, type, and description correctly", func() {
|
|
trait := defkit.NewTrait("scaler").
|
|
Description("Manually scale K8s pod for your workload.").
|
|
AppliesTo("deployments.apps", "statefulsets.apps").
|
|
PodDisruptive(false)
|
|
|
|
Expect(trait.DefName()).To(Equal("scaler"))
|
|
Expect(trait.GetName()).To(Equal("scaler"))
|
|
Expect(trait.DefType()).To(Equal(defkit.DefinitionTypeTrait))
|
|
Expect(trait.GetDescription()).To(Equal("Manually scale K8s pod for your workload."))
|
|
Expect(trait.GetAppliesToWorkloads()).To(Equal([]string{"deployments.apps", "statefulsets.apps"}))
|
|
Expect(trait.IsPodDisruptive()).To(BeFalse())
|
|
})
|
|
|
|
It("should set ConflictsWith correctly", func() {
|
|
trait := defkit.NewTrait("hpa").
|
|
Description("HPA scaler trait.").
|
|
AppliesTo("deployments.apps").
|
|
ConflictsWith("scaler", "cpuscaler")
|
|
|
|
Expect(trait.GetConflictsWith()).To(Equal([]string{"scaler", "cpuscaler"}))
|
|
})
|
|
|
|
It("should set Stage correctly", func() {
|
|
trait := defkit.NewTrait("expose").
|
|
Description("Expose service.").
|
|
Stage("PostDispatch").
|
|
AppliesTo("deployments.apps")
|
|
|
|
Expect(trait.GetStage()).To(Equal("PostDispatch"))
|
|
})
|
|
|
|
It("should add parameters with Params and Param methods", func() {
|
|
trait := defkit.NewTrait("test").
|
|
AppliesTo("deployments.apps").
|
|
Params(
|
|
defkit.Int("replicas").Default(1),
|
|
defkit.String("image"),
|
|
).
|
|
Param(defkit.Bool("enabled").Default(true))
|
|
|
|
params := trait.GetParams()
|
|
Expect(params).To(HaveLen(3))
|
|
Expect(params[0].Name()).To(Equal("replicas"))
|
|
Expect(params[1].Name()).To(Equal("image"))
|
|
Expect(params[2].Name()).To(Equal("enabled"))
|
|
})
|
|
|
|
It("should set Labels correctly", func() {
|
|
labels := map[string]string{
|
|
"ui-hidden": "true",
|
|
"custom": "value",
|
|
}
|
|
trait := defkit.NewTrait("labeled").
|
|
Description("Trait with labels").
|
|
AppliesTo("deployments.apps").
|
|
Labels(labels)
|
|
|
|
gotLabels := trait.GetLabels()
|
|
Expect(gotLabels["ui-hidden"]).To(Equal("true"))
|
|
Expect(gotLabels["custom"]).To(Equal("value"))
|
|
})
|
|
|
|
It("should set imports correctly", func() {
|
|
trait := defkit.NewTrait("import-trait").
|
|
WithImports("strconv", "strings").
|
|
AppliesTo("deployments.apps")
|
|
|
|
Expect(trait.GetImports()).To(Equal([]string{"strconv", "strings"}))
|
|
})
|
|
})
|
|
|
|
Context("Status and Health Methods", func() {
|
|
It("should set custom status and health policy", func() {
|
|
trait := defkit.NewTrait("status-trait").
|
|
AppliesTo("deployments.apps").
|
|
CustomStatus("message: \"Ready\"").
|
|
HealthPolicy("isHealth: true")
|
|
|
|
Expect(trait.GetCustomStatus()).To(Equal("message: \"Ready\""))
|
|
Expect(trait.GetHealthPolicy()).To(Equal("isHealth: true"))
|
|
})
|
|
|
|
It("should set health policy from expression", func() {
|
|
trait := defkit.NewTrait("health").
|
|
AppliesTo("deployments.apps").
|
|
HealthPolicyExpr(defkit.Health().Condition("Ready").IsTrue())
|
|
|
|
policy := trait.GetHealthPolicy()
|
|
Expect(policy).NotTo(BeEmpty())
|
|
Expect(policy).To(ContainSubstring("isHealth"))
|
|
})
|
|
})
|
|
|
|
Context("RawCUE Methods", func() {
|
|
It("should set raw CUE with complete definition", func() {
|
|
rawCUE := `scaler: {
|
|
type: "trait"
|
|
description: "Raw CUE trait"
|
|
}
|
|
template: {
|
|
patch: spec: replicas: parameter.replicas
|
|
parameter: replicas: *1 | int
|
|
}`
|
|
trait := defkit.NewTrait("scaler").RawCUE(rawCUE)
|
|
|
|
Expect(trait.HasRawCUE()).To(BeTrue())
|
|
Expect(trait.GetRawCUE()).To(Equal(rawCUE))
|
|
})
|
|
|
|
It("should set template block correctly", func() {
|
|
templateBlock := `
|
|
#PatchParams: {
|
|
containerName: *"" | string
|
|
command: *null | [...string]
|
|
}
|
|
patch: spec: template: spec: containers: [{name: parameter.containerName}]
|
|
parameter: #PatchParams
|
|
`
|
|
trait := defkit.NewTrait("command").
|
|
Description("Add command").
|
|
AppliesTo("deployments.apps").
|
|
TemplateBlock(templateBlock)
|
|
|
|
Expect(trait.HasTemplateBlock()).To(BeTrue())
|
|
Expect(trait.GetTemplateBlock()).To(Equal(templateBlock))
|
|
})
|
|
})
|
|
|
|
Context("Helper Methods", func() {
|
|
It("should add helper definitions", func() {
|
|
probeSchema := defkit.Struct("probe").WithFields(
|
|
defkit.Field("path", defkit.ParamTypeString).Default("/health"),
|
|
defkit.Field("port", defkit.ParamTypeInt).Default(8080),
|
|
)
|
|
|
|
trait := defkit.NewTrait("health-probe").
|
|
Description("Add health probes").
|
|
AppliesTo("deployments.apps").
|
|
Helper("HealthProbe", probeSchema)
|
|
|
|
helpers := trait.GetHelperDefinitions()
|
|
Expect(helpers).To(HaveLen(1))
|
|
Expect(helpers[0].GetName()).To(Equal("HealthProbe"))
|
|
})
|
|
})
|
|
|
|
Context("ToCue Generation - Metadata", func() {
|
|
It("should generate complete CUE definition with header", func() {
|
|
trait := defkit.NewTrait("scaler").
|
|
Description("Scale workloads").
|
|
AppliesTo("deployments.apps").
|
|
Params(defkit.Int("replicas").Default(1).Description("Number of replicas"))
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`scaler: {`))
|
|
Expect(cue).To(ContainSubstring(`type: "trait"`))
|
|
Expect(cue).To(ContainSubstring(`description: "Scale workloads"`))
|
|
// podDisruptive is always emitted
|
|
Expect(cue).To(ContainSubstring(`podDisruptive: false`))
|
|
Expect(cue).To(ContainSubstring(`appliesToWorkloads: ["deployments.apps"]`))
|
|
})
|
|
|
|
It("should quote trait names with special characters", func() {
|
|
trait := defkit.NewTrait("my-trait-v1.0").
|
|
Description("Trait with special characters").
|
|
AppliesTo("deployments.apps")
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`"my-trait-v1.0": {`))
|
|
})
|
|
|
|
It("should include labels in CUE output", func() {
|
|
trait := defkit.NewTrait("labeled").
|
|
Description("Trait with labels").
|
|
AppliesTo("deployments.apps").
|
|
Labels(map[string]string{"ui-hidden": "true"})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// CUE formatter may output labels inline or as block
|
|
Expect(cue).To(ContainSubstring(`labels:`))
|
|
Expect(cue).To(ContainSubstring(`"ui-hidden": "true"`))
|
|
})
|
|
|
|
It("should include stage in CUE output", func() {
|
|
trait := defkit.NewTrait("staged").
|
|
Description("Trait with stage").
|
|
AppliesTo("deployments.apps").
|
|
Stage("PostDispatch")
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`stage:`))
|
|
Expect(cue).To(ContainSubstring(`"PostDispatch"`))
|
|
})
|
|
|
|
It("should include conflictsWith in CUE output", func() {
|
|
trait := defkit.NewTrait("exclusive").
|
|
Description("Exclusive trait").
|
|
AppliesTo("deployments.apps").
|
|
ConflictsWith("other-trait", "incompatible-trait")
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`conflictsWith: ["other-trait", "incompatible-trait"]`))
|
|
})
|
|
|
|
It("should include imports in CUE output", func() {
|
|
trait := defkit.NewTrait("with-imports").
|
|
Description("Trait with imports").
|
|
AppliesTo("deployments.apps").
|
|
WithImports("strconv", "strings")
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`import (`))
|
|
Expect(cue).To(ContainSubstring(`"strconv"`))
|
|
Expect(cue).To(ContainSubstring(`"strings"`))
|
|
})
|
|
|
|
It("should still auto-detect param imports when the trait has no Template() func", func() {
|
|
// Reproduces the P1 review finding: a trait that declares
|
|
// constraint-bearing params but no Template(func(tpl)) body
|
|
// (raw TemplateBlock / status-only trait) used to short-circuit
|
|
// out of detectRequiredImports and skip the params loop, so
|
|
// "strings" was never added even though MinLen rendered
|
|
// strings.MinRunes(...) into the parameter block.
|
|
name := defkit.String("name").MinLen(3)
|
|
|
|
trait := defkit.NewTrait("params-only").
|
|
Description("Trait with constraint params but no Template func").
|
|
AppliesTo("deployments.apps").
|
|
Params(name).
|
|
TemplateBlock(`patch: metadata: name: parameter.name`)
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`import (`))
|
|
Expect(cue).To(ContainSubstring(`"strings"`))
|
|
})
|
|
|
|
It("should auto-detect the strings import for StringParam with MinLen via ImportRequirer", func() {
|
|
name := defkit.String("name").MinLen(3)
|
|
|
|
trait := defkit.NewTrait("named-trait").
|
|
Description("Trait whose StringParam requires strings import").
|
|
AppliesTo("deployments.apps").
|
|
Params(name).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().Set("metadata.name", name)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`import (`))
|
|
Expect(cue).To(ContainSubstring(`"strings"`))
|
|
})
|
|
|
|
It("should auto-detect the list import via Template helpers (Helper, ConcatHelper, DedupeHelper, StructArrayHelper)", func() {
|
|
volumes := defkit.Array("volumes").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("type"),
|
|
).Optional()
|
|
extras := defkit.Array("extras").WithFields(
|
|
defkit.String("name"),
|
|
).Optional()
|
|
|
|
trait := defkit.NewTrait("helper-imports").
|
|
Description("Helpers on a trait template must pull their imports").
|
|
AppliesTo("deployments.apps").
|
|
Params(volumes, extras).
|
|
Template(func(tpl *defkit.Template) {
|
|
// HelperVar — exercises GetHelpersBeforeOutput on a trait.
|
|
tpl.Helper("_namedVolumes").
|
|
From(volumes).
|
|
Map(defkit.FieldMap{"name": defkit.FieldRef("name")}).
|
|
Build()
|
|
// StructArrayHelper + ConcatHelper — both declared
|
|
// at template scope, both ImportRequirers.
|
|
byType := tpl.StructArrayHelper("_byType", volumes).
|
|
Field("pvc", defkit.FieldMap{"name": defkit.FieldRef("name")}).
|
|
Build()
|
|
tpl.ConcatHelper("_allTypes", byType).Fields("pvc").Build()
|
|
// DedupeHelper — also an ImportRequirer.
|
|
tpl.DedupeHelper("_uniqExtras", extras).
|
|
ByKey("name").
|
|
Build()
|
|
tpl.Outputs("dep", defkit.NewResource("apps/v1", "Deployment").
|
|
SetIf(extras.IsSet(),
|
|
"metadata.annotations.uniq", defkit.Lit("set")))
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Helpers above ought to surface "list" because both
|
|
// ConcatHelper and DedupeHelper render via list.Concat /
|
|
// list-based dedupe operations.
|
|
Expect(cue).To(ContainSubstring(`import (`))
|
|
Expect(cue).To(ContainSubstring(`"list"`))
|
|
})
|
|
|
|
It("should not duplicate imports when WithImports already declares the same package", func() {
|
|
extras := defkit.Array("extraVolumeMounts").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("mountPath"),
|
|
)
|
|
|
|
trait := defkit.NewTrait("dedup-imports").
|
|
Description("WithImports + auto-detect should converge on one list import").
|
|
AppliesTo("deployments.apps").
|
|
WithImports("list").
|
|
Params(extras).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().PatchKey("spec.template.spec.containers", "name",
|
|
defkit.NewArrayElement().
|
|
PatchKeyField("volumeMounts", "name", defkit.ArrayConcat(
|
|
defkit.NewArray().Item(
|
|
defkit.NewArrayElement().
|
|
Set("name", defkit.Lit("work")).
|
|
Set("mountPath", defkit.Lit("/work")),
|
|
),
|
|
extras,
|
|
)),
|
|
)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
// Exactly one "list" import in the imports block.
|
|
imp := strings.Count(cue, `"list"`)
|
|
Expect(imp).To(Equal(1))
|
|
})
|
|
|
|
It("should auto-detect the list import when an ArrayConcat appears in a patch block", func() {
|
|
extras := defkit.Array("extraVolumeMounts").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("mountPath"),
|
|
)
|
|
|
|
trait := defkit.NewTrait("auto-import-from-array-concat").
|
|
Description("ArrayConcat inside patch must pull the list import without WithImports").
|
|
AppliesTo("deployments.apps").
|
|
Params(extras).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().PatchKey("spec.template.spec.containers", "name",
|
|
defkit.NewArrayElement().
|
|
PatchKeyField("volumeMounts", "name", defkit.ArrayConcat(
|
|
defkit.NewArray().Item(
|
|
defkit.NewArrayElement().
|
|
Set("name", defkit.Lit("work")).
|
|
Set("mountPath", defkit.Lit("/work")),
|
|
),
|
|
extras,
|
|
)),
|
|
)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`import (`))
|
|
Expect(cue).To(ContainSubstring(`"list"`))
|
|
Expect(cue).To(ContainSubstring(`list.Concat([`))
|
|
Expect(cue).NotTo(ContainSubstring(`] + parameter.extraVolumeMounts`))
|
|
})
|
|
})
|
|
|
|
Context("ToCue Generation - Status", func() {
|
|
It("should include status block with customStatus and healthPolicy", func() {
|
|
trait := defkit.NewTrait("health-aware").
|
|
Description("Health aware trait").
|
|
AppliesTo("deployments.apps").
|
|
CustomStatus("message: \"Running\"").
|
|
HealthPolicy("isHealth: output.status.ready == true")
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`status: {`))
|
|
Expect(cue).To(ContainSubstring(`customStatus:`))
|
|
Expect(cue).To(ContainSubstring(`message: "Running"`))
|
|
Expect(cue).To(ContainSubstring(`healthPolicy:`))
|
|
Expect(cue).To(ContainSubstring(`isHealth: output.status.ready == true`))
|
|
})
|
|
})
|
|
|
|
Context("ToCue Generation - Template with Patch", func() {
|
|
It("should generate patch block from Template API", func() {
|
|
replicas := defkit.Int("replicas").Default(1)
|
|
|
|
trait := defkit.NewTrait("scaler").
|
|
Description("Manually scale K8s pod.").
|
|
AppliesTo("deployments.apps", "statefulsets.apps").
|
|
PodDisruptive(false).
|
|
Params(replicas).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.PatchStrategy("retainKeys").
|
|
Patch().Set("spec.replicas", replicas)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`// +patchStrategy=retainKeys`))
|
|
Expect(cue).To(ContainSubstring(`patch:`))
|
|
Expect(cue).To(ContainSubstring(`spec:`))
|
|
Expect(cue).To(ContainSubstring(`replicas:`))
|
|
})
|
|
})
|
|
|
|
Context("ToCue Generation - Template with Outputs", func() {
|
|
It("should generate outputs block from Template API", func() {
|
|
trait := defkit.NewTrait("expose").
|
|
Description("Expose workload via Service").
|
|
AppliesTo("deployments.apps").
|
|
Stage("PostDispatch").
|
|
Params(
|
|
defkit.Int("port").Default(80).Description("Service port"),
|
|
defkit.String("type").Default("ClusterIP").Description("Service type"),
|
|
).
|
|
Template(func(tpl *defkit.Template) {
|
|
service := defkit.NewResource("v1", "Service").
|
|
Set("metadata.name", defkit.VelaCtx().Name()).
|
|
Set("spec.type", defkit.ParamRef("type")).
|
|
Set("spec.ports[0].port", defkit.ParamRef("port"))
|
|
tpl.Outputs("service", service)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// CUE formatter may inline single outputs
|
|
Expect(cue).To(ContainSubstring(`outputs:`))
|
|
Expect(cue).To(ContainSubstring(`service:`))
|
|
Expect(cue).To(ContainSubstring(`apiVersion: "v1"`))
|
|
Expect(cue).To(ContainSubstring(`kind:`))
|
|
Expect(cue).To(ContainSubstring(`"Service"`))
|
|
Expect(cue).To(ContainSubstring(`metadata: name: context.name`))
|
|
})
|
|
})
|
|
|
|
Context("ToCue Generation - RawCUE", func() {
|
|
It("should handle raw CUE with top-level template block", func() {
|
|
rawCUE := `scaler: {
|
|
type: "trait"
|
|
description: "Raw CUE trait"
|
|
}
|
|
template: {
|
|
patch: spec: replicas: parameter.replicas
|
|
parameter: replicas: *1 | int
|
|
}`
|
|
trait := defkit.NewTrait("scaler").RawCUE(rawCUE)
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`scaler:`))
|
|
Expect(cue).To(ContainSubstring(`template:`))
|
|
Expect(cue).To(ContainSubstring(`patch: spec: replicas: parameter.replicas`))
|
|
})
|
|
|
|
It("should handle raw CUE without top-level template (partial)", func() {
|
|
rawCUE := `patch: spec: replicas: parameter.replicas
|
|
parameter: replicas: *1 | int`
|
|
|
|
trait := defkit.NewTrait("partial").
|
|
Description("Partial raw CUE trait").
|
|
AppliesTo("deployments.apps").
|
|
RawCUE(rawCUE)
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`partial:`))
|
|
Expect(cue).To(ContainSubstring(`type:`))
|
|
Expect(cue).To(ContainSubstring(`"trait"`))
|
|
Expect(cue).To(ContainSubstring(`template:`))
|
|
})
|
|
|
|
It("should handle TemplateBlock", func() {
|
|
templateBlock := `#PatchParams: {
|
|
containerName: *"" | string
|
|
}
|
|
patch: spec: template: spec: containers: [{name: parameter.containerName}]
|
|
parameter: #PatchParams`
|
|
|
|
trait := defkit.NewTrait("command").
|
|
Description("Add command").
|
|
AppliesTo("deployments.apps").
|
|
TemplateBlock(templateBlock)
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`command: {`))
|
|
Expect(cue).To(ContainSubstring(`template: {`))
|
|
Expect(cue).To(ContainSubstring(`#PatchParams:`))
|
|
})
|
|
|
|
It("should preserve raw CUE content when it has template block", func() {
|
|
rawCUE := `myname: {
|
|
type: "trait"
|
|
}
|
|
template: {
|
|
patch: spec: replicas: 1
|
|
}`
|
|
trait := defkit.NewTrait("myname").RawCUE(rawCUE)
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Raw CUE with top-level template block is returned as-is (formatted)
|
|
Expect(cue).To(ContainSubstring(`myname:`))
|
|
Expect(cue).To(ContainSubstring(`template:`))
|
|
Expect(cue).To(ContainSubstring(`patch:`))
|
|
})
|
|
})
|
|
|
|
Context("ToCue Generation - Parameters", func() {
|
|
It("should generate parameter block", func() {
|
|
trait := defkit.NewTrait("params").
|
|
AppliesTo("deployments.apps").
|
|
Params(
|
|
defkit.Int("replicas").Default(1).Description("Number of replicas"),
|
|
defkit.String("image").Description("Container image"),
|
|
)
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring(`parameter: {`))
|
|
Expect(cue).To(ContainSubstring(`// +usage=Number of replicas`))
|
|
Expect(cue).To(ContainSubstring(`replicas: *1 | int`))
|
|
Expect(cue).To(ContainSubstring(`// +usage=Container image`))
|
|
Expect(cue).To(ContainSubstring(`image: string`))
|
|
})
|
|
})
|
|
|
|
Context("ToYAML Generation", func() {
|
|
It("should generate valid Kubernetes YAML", func() {
|
|
trait := defkit.NewTrait("scaler").
|
|
Description("Scale workload.").
|
|
AppliesTo("deployments.apps").
|
|
PodDisruptive(false).
|
|
Params(defkit.Int("replicas").Default(1))
|
|
|
|
yamlBytes, err := trait.ToYAML()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
yamlStr := string(yamlBytes)
|
|
Expect(yamlStr).To(ContainSubstring("kind: TraitDefinition"))
|
|
Expect(yamlStr).To(ContainSubstring("name: scaler"))
|
|
Expect(yamlStr).To(ContainSubstring("appliesToWorkloads:"))
|
|
})
|
|
|
|
It("should include conflictsWith and stage in YAML", func() {
|
|
trait := defkit.NewTrait("exclusive").
|
|
Description("Exclusive trait").
|
|
AppliesTo("deployments.apps").
|
|
ConflictsWith("other-trait").
|
|
Stage("PreDispatch").
|
|
PodDisruptive(true)
|
|
|
|
yamlBytes, err := trait.ToYAML()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
yamlStr := string(yamlBytes)
|
|
Expect(yamlStr).To(ContainSubstring("conflictsWith:"))
|
|
Expect(yamlStr).To(ContainSubstring("stage: PreDispatch"))
|
|
Expect(yamlStr).To(ContainSubstring("podDisruptive: true"))
|
|
})
|
|
})
|
|
|
|
Context("Registry Integration", func() {
|
|
It("should register and retrieve traits", func() {
|
|
defkit.Clear() // Reset registry
|
|
|
|
trait1 := defkit.NewTrait("scaler").Description("Scale").AppliesTo("deployments.apps")
|
|
trait2 := defkit.NewTrait("expose").Description("Expose").AppliesTo("deployments.apps")
|
|
comp := defkit.NewComponent("webservice").Description("Component")
|
|
|
|
defkit.Register(trait1)
|
|
defkit.Register(trait2)
|
|
defkit.Register(comp)
|
|
|
|
Expect(defkit.Count()).To(Equal(3))
|
|
Expect(defkit.Traits()).To(HaveLen(2))
|
|
Expect(defkit.Components()).To(HaveLen(1))
|
|
|
|
defkit.Clear() // Clean up
|
|
})
|
|
})
|
|
|
|
Context("ToCue with Patch operations", func() {
|
|
It("should generate patch with SpreadIf", func() {
|
|
labels := defkit.Object("labels")
|
|
trait := defkit.NewTrait("label").
|
|
Description("Add labels").
|
|
AppliesTo("deployments.apps").
|
|
Params(labels).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().
|
|
SpreadIf(labels.IsSet(), "spec.template.metadata.labels", labels)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("patch:"))
|
|
Expect(cue).To(ContainSubstring("labels"))
|
|
})
|
|
|
|
It("should generate patch with ForEach", func() {
|
|
labels := defkit.Object("labels")
|
|
trait := defkit.NewTrait("label").
|
|
Description("Add labels").
|
|
AppliesTo("deployments.apps").
|
|
Params(labels).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().ForEach(labels, "spec.template.metadata.labels")
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("patch:"))
|
|
})
|
|
|
|
It("should generate patch with If/EndIf block", func() {
|
|
enabled := defkit.Bool("enabled")
|
|
replicas := defkit.Int("replicas")
|
|
trait := defkit.NewTrait("conditional").
|
|
Description("Conditional scaling").
|
|
AppliesTo("deployments.apps").
|
|
Params(enabled, replicas).
|
|
Template(func(tpl *defkit.Template) {
|
|
cond := defkit.Eq(enabled, defkit.Lit(true))
|
|
tpl.Patch().
|
|
If(cond).
|
|
Set("spec.replicas", replicas).
|
|
EndIf()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("patch:"))
|
|
Expect(cue).To(ContainSubstring("if"))
|
|
})
|
|
|
|
It("should generate patch with PatchKey", func() {
|
|
containerName := defkit.String("containerName")
|
|
image := defkit.String("image")
|
|
trait := defkit.NewTrait("sidecar").
|
|
Description("Add sidecar container").
|
|
AppliesTo("deployments.apps").
|
|
Params(containerName, image).
|
|
Template(func(tpl *defkit.Template) {
|
|
container := defkit.NewArrayElement().
|
|
Set("name", containerName).
|
|
Set("image", image)
|
|
tpl.Patch().PatchKey("spec.template.spec.containers", "name", container)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("patch:"))
|
|
Expect(cue).To(ContainSubstring("patchKey"))
|
|
})
|
|
|
|
It("should generate patch with Passthrough", func() {
|
|
trait := defkit.NewTrait("json-patch").
|
|
Description("Apply JSON patch").
|
|
AppliesTo("*").
|
|
Params(defkit.OpenStruct()).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().Passthrough()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("patch:"))
|
|
})
|
|
|
|
It("should generate Optional field guards in Map comprehension", func() {
|
|
items := defkit.Array("items").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("label"),
|
|
defkit.Int("priority"),
|
|
)
|
|
trait := defkit.NewTrait("optional-test").
|
|
Description("Test optional fields").
|
|
AppliesTo("deployments.apps").
|
|
Params(items).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().
|
|
SetIf(items.IsSet(), "spec.template.spec.items",
|
|
defkit.From(items).Map(defkit.FieldMap{
|
|
"name": defkit.F("name"),
|
|
"label": defkit.Optional("label"),
|
|
"priority": defkit.Optional("priority"),
|
|
}))
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Required field should use direct access
|
|
Expect(cue).To(ContainSubstring("name: v.name"))
|
|
// Optional fields should have if guards
|
|
Expect(cue).To(ContainSubstring("if v.label != _|_"))
|
|
Expect(cue).To(ContainSubstring("label: v.label"))
|
|
Expect(cue).To(ContainSubstring("if v.priority != _|_"))
|
|
Expect(cue).To(ContainSubstring("priority: v.priority"))
|
|
// Should NOT contain top-level underscore for optional fields
|
|
Expect(cue).NotTo(ContainSubstring("label: _"))
|
|
Expect(cue).NotTo(ContainSubstring("priority: _"))
|
|
})
|
|
|
|
It("should generate If/EndIf with SetIf using sub-field conditions", func() {
|
|
parent := defkit.Map("parent").WithFields(
|
|
defkit.Array("required").WithFields(
|
|
defkit.String("key"),
|
|
),
|
|
defkit.Array("preferred").WithFields(
|
|
defkit.Int("weight"),
|
|
),
|
|
)
|
|
trait := defkit.NewTrait("if-subfield-test").
|
|
Description("Test If/EndIf with sub-field conditions").
|
|
AppliesTo("deployments.apps").
|
|
Params(parent).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().
|
|
If(parent.IsSet()).
|
|
SetIf(parent.Field("required").IsSet(),
|
|
"spec.requiredItems",
|
|
defkit.From(defkit.ParamPath("parent.required")).Map(defkit.FieldMap{
|
|
"key": defkit.F("key"),
|
|
})).
|
|
SetIf(parent.Field("preferred").IsSet(),
|
|
"spec.preferredItems",
|
|
defkit.From(defkit.ParamPath("parent.preferred")).Map(defkit.FieldMap{
|
|
"weight": defkit.F("weight"),
|
|
})).
|
|
EndIf()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Both conditions should appear (AND-combined by the field tree)
|
|
Expect(cue).To(ContainSubstring(`parameter["parent"] != _|_`))
|
|
Expect(cue).To(ContainSubstring("parameter.parent.required != _|_"))
|
|
Expect(cue).To(ContainSubstring("parameter.parent.preferred != _|_"))
|
|
// Verify the For comprehension sources
|
|
Expect(cue).To(ContainSubstring("for v in parameter.parent.required"))
|
|
Expect(cue).To(ContainSubstring("for v in parameter.parent.preferred"))
|
|
Expect(cue).To(ContainSubstring("key: v.key"))
|
|
Expect(cue).To(ContainSubstring("weight: v.weight"))
|
|
})
|
|
})
|
|
|
|
Context("Let Bindings with ForEachMap", func() {
|
|
It("should generate let binding with ForEachMap and LetVariable references", func() {
|
|
trait := defkit.NewTrait("let-foreach-test").
|
|
Description("Test let binding with ForEachMap").
|
|
AppliesTo("*").
|
|
PodDisruptive(true).
|
|
Param(defkit.DynamicMap().ValueTypeUnion("string | null")).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.PatchStrategy("jsonMergePatch")
|
|
tpl.AddLetBinding("content", defkit.ForEachMap())
|
|
tpl.Patch().
|
|
Set("metadata.annotations", defkit.LetVariable("content"))
|
|
tpl.Patch().
|
|
If(defkit.And(
|
|
defkit.ContextOutput().HasPath("spec"),
|
|
defkit.ContextOutput().HasPath("spec.template"),
|
|
)).
|
|
Set("spec.template.metadata.annotations", defkit.LetVariable("content")).
|
|
EndIf()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Let binding should appear before the patch block
|
|
Expect(cue).To(ContainSubstring("let content ="))
|
|
// ForEachMap should render as a struct comprehension
|
|
Expect(cue).To(ContainSubstring("for k, v in parameter"))
|
|
Expect(cue).To(ContainSubstring("(k): v"))
|
|
// Let variable references should appear in the patch
|
|
Expect(cue).To(ContainSubstring("metadata: annotations: content"))
|
|
// Conditional block should reference the let variable
|
|
Expect(cue).To(ContainSubstring("annotations: content"))
|
|
Expect(cue).To(ContainSubstring("context.output.spec != _|_"))
|
|
Expect(cue).To(ContainSubstring("context.output.spec.template != _|_"))
|
|
// The for-each comprehension should NOT be inlined at each usage site
|
|
// It should only appear once in the let binding
|
|
Expect(strings.Count(cue, "for k, v in parameter")).To(Equal(1))
|
|
// Patch strategy should be present
|
|
Expect(cue).To(ContainSubstring("// +patchStrategy=jsonMergePatch"))
|
|
})
|
|
|
|
It("should render ForEachMap with custom source and vars via valueToCUE", func() {
|
|
trait := defkit.NewTrait("custom-foreach-test").
|
|
Description("Test custom ForEachMap rendering").
|
|
AppliesTo("deployments.apps").
|
|
Param(defkit.DynamicMap().ValueTypeUnion("string | null")).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.AddLetBinding("labelContent",
|
|
defkit.ForEachMap().Over("parameter.labels").WithVars("key", "val"))
|
|
tpl.Patch().
|
|
Set("metadata.labels", defkit.LetVariable("labelContent"))
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Custom variable names and source
|
|
Expect(cue).To(ContainSubstring("let labelContent ="))
|
|
Expect(cue).To(ContainSubstring("for key, val in parameter.labels"))
|
|
Expect(cue).To(ContainSubstring("(key): val"))
|
|
Expect(cue).To(ContainSubstring("metadata: labels: labelContent"))
|
|
})
|
|
})
|
|
|
|
Context("PatchKey with ArrayParam (no array wrapping)", func() {
|
|
It("should emit direct assignment when single element is an ArrayParam", func() {
|
|
items := defkit.Array("items").WithFields(
|
|
defkit.String("name"),
|
|
defkit.String("value"),
|
|
)
|
|
|
|
trait := defkit.NewTrait("patchkey-array-test").
|
|
Description("Test PatchKey with ArrayParam").
|
|
AppliesTo("deployments.apps").
|
|
Params(items).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().
|
|
PatchKey("spec.template.spec.items", "name", items)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Should emit patchKey annotation
|
|
Expect(cue).To(ContainSubstring("// +patchKey=name"))
|
|
// Should assign parameter directly, NOT wrapped in [...]
|
|
Expect(cue).To(ContainSubstring("items: parameter.items"))
|
|
// Should NOT have array wrapping around the parameter
|
|
Expect(cue).NotTo(ContainSubstring("[parameter.items]"))
|
|
})
|
|
|
|
It("should still wrap individual ArrayElements in array brackets", func() {
|
|
elem := defkit.NewArrayElement().
|
|
Set("name", defkit.Lit("test")).
|
|
Set("value", defkit.Lit("foo"))
|
|
|
|
trait := defkit.NewTrait("patchkey-elem-test").
|
|
Description("Test PatchKey with ArrayElement").
|
|
AppliesTo("deployments.apps").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().
|
|
PatchKey("spec.items", "name", elem)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// ArrayElement should still be wrapped in [...]
|
|
Expect(cue).To(ContainSubstring("// +patchKey=name"))
|
|
Expect(cue).To(ContainSubstring("items: [{"))
|
|
})
|
|
})
|
|
|
|
Context("IsSet bracket notation for optional field checks", func() {
|
|
It("should generate bracket notation for IsSet conditions", func() {
|
|
optParam := defkit.Array("env").Of(defkit.ParamTypeString).Optional()
|
|
|
|
trait := defkit.NewTrait("isset-bracket-test").
|
|
Description("Test bracket notation for IsSet").
|
|
AppliesTo("deployments.apps").
|
|
Params(optParam).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().
|
|
SetIf(optParam.IsSet(), "spec.env", optParam)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Should use bracket notation: parameter["env"] != _|_
|
|
Expect(cue).To(ContainSubstring(`parameter["env"] != _|_`))
|
|
// Should NOT use dot notation for the condition
|
|
Expect(cue).NotTo(ContainSubstring("parameter.env != _|_"))
|
|
})
|
|
|
|
It("should generate bracket notation for NotSet (negated IsSet) conditions", func() {
|
|
optParam := defkit.String("debug").Optional()
|
|
|
|
trait := defkit.NewTrait("notset-bracket-test").
|
|
Description("Test bracket notation for NotSet").
|
|
AppliesTo("deployments.apps").
|
|
Params(optParam).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().
|
|
SetIf(optParam.NotSet(), "spec.debug", defkit.Lit(false))
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Should use bracket notation: parameter["debug"] == _|_
|
|
Expect(cue).To(ContainSubstring(`parameter["debug"] == _|_`))
|
|
// Should NOT use dot notation
|
|
Expect(cue).NotTo(ContainSubstring("parameter.debug == _|_"))
|
|
})
|
|
|
|
It("should use bracket notation in compound conditions", func() {
|
|
cpu := defkit.String("cpu").Optional()
|
|
memory := defkit.String("memory").Optional()
|
|
|
|
trait := defkit.NewTrait("compound-bracket-test").
|
|
Description("Test bracket notation in compound conditions").
|
|
AppliesTo("deployments.apps").
|
|
Params(cpu, memory).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().
|
|
SetIf(defkit.And(cpu.IsSet(), memory.IsSet()),
|
|
"spec.resources", defkit.Lit("configured"))
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Both parts of the compound condition should use bracket notation
|
|
Expect(cue).To(ContainSubstring(`parameter["cpu"] != _|_`))
|
|
Expect(cue).To(ContainSubstring(`parameter["memory"] != _|_`))
|
|
})
|
|
})
|
|
|
|
Context("PatchStrategyAnnotation", func() {
|
|
It("should record PatchStrategyAnnotation ops on PatchResource", func() {
|
|
p := defkit.NewPatchResource()
|
|
p.PatchStrategyAnnotation("spec.strategy", "retainKeys")
|
|
|
|
ops := p.Ops()
|
|
Expect(ops).To(HaveLen(1))
|
|
ann, ok := ops[0].(*defkit.PatchStrategyAnnotationOp)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(ann.Path()).To(Equal("spec.strategy"))
|
|
Expect(ann.Strategy()).To(Equal("retainKeys"))
|
|
})
|
|
|
|
It("should record PatchStrategyAnnotation inside If block", func() {
|
|
p := defkit.NewPatchResource()
|
|
cond := defkit.Eq(defkit.ParameterField("kind"), defkit.Lit("Deployment"))
|
|
p.If(cond).
|
|
PatchStrategyAnnotation("spec.strategy", "retainKeys").
|
|
Set("spec.strategy.type", defkit.ParameterField("strategyType")).
|
|
EndIf()
|
|
|
|
ops := p.Ops()
|
|
Expect(ops).To(HaveLen(1))
|
|
ifBlock, ok := ops[0].(*defkit.IfBlock)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(ifBlock.Ops()).To(HaveLen(2)) // PatchStrategyAnnotation + Set
|
|
})
|
|
|
|
It("should emit patchStrategy annotation in CUE output for unconditional field", func() {
|
|
strategy := defkit.Struct("strategy").WithFields(
|
|
defkit.Field("type", defkit.ParamTypeString).Default("RollingUpdate"),
|
|
)
|
|
|
|
trait := defkit.NewTrait("annotation-test").
|
|
Description("Test patchStrategy annotation").
|
|
AppliesTo("deployments.apps").
|
|
Params(strategy).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().
|
|
PatchStrategyAnnotation("spec.strategy", "retainKeys").
|
|
Set("spec.strategy.type", defkit.ParameterField("strategy.type"))
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Should contain the annotation comment
|
|
Expect(cue).To(ContainSubstring("// +patchStrategy=retainKeys"))
|
|
// Annotation should appear before the field
|
|
Expect(cue).To(ContainSubstring("strategy: type: parameter.strategy.type"))
|
|
})
|
|
|
|
It("should emit patchStrategy annotation inside conditional block", func() {
|
|
kind := defkit.String("kind").Default("Deployment").Values("Deployment", "StatefulSet")
|
|
strategy := defkit.Struct("strategy").WithFields(
|
|
defkit.Field("type", defkit.ParamTypeString).Default("RollingUpdate"),
|
|
)
|
|
|
|
trait := defkit.NewTrait("cond-annotation-test").
|
|
Description("Test patchStrategy in conditional block").
|
|
AppliesTo("deployments.apps").
|
|
Params(kind, strategy).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().
|
|
If(defkit.Eq(kind, defkit.Lit("Deployment"))).
|
|
PatchStrategyAnnotation("spec.strategy", "retainKeys").
|
|
Set("spec.strategy.type", defkit.ParameterField("strategy.type")).
|
|
EndIf()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Should contain the annotation inside the if block
|
|
Expect(cue).To(ContainSubstring("// +patchStrategy=retainKeys"))
|
|
Expect(cue).To(ContainSubstring(`parameter.kind == "Deployment"`))
|
|
})
|
|
})
|
|
|
|
Context("Condition hoisting", func() {
|
|
It("should hoist matching conditions from children to parent", func() {
|
|
enabled := defkit.Bool("enabled")
|
|
replicas := defkit.Int("replicas")
|
|
image := defkit.String("image")
|
|
|
|
trait := defkit.NewTrait("hoist-test").
|
|
Description("Test condition hoisting").
|
|
AppliesTo("deployments.apps").
|
|
Params(enabled, replicas, image).
|
|
Template(func(tpl *defkit.Template) {
|
|
cond := defkit.Eq(enabled, defkit.Lit(true))
|
|
tpl.Patch().
|
|
If(cond).
|
|
Set("spec.replicas", replicas).
|
|
Set("spec.template.spec.image", image).
|
|
EndIf()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// The If condition should appear
|
|
Expect(cue).To(ContainSubstring(`parameter.enabled == true`))
|
|
// Both fields should be set inside the condition
|
|
Expect(cue).To(ContainSubstring("replicas: parameter.replicas"))
|
|
Expect(cue).To(ContainSubstring("image: parameter.image"))
|
|
})
|
|
|
|
It("should hoist common condition from AND combinations", func() {
|
|
kind := defkit.String("kind").Default("Deployment")
|
|
replicas := defkit.Int("replicas")
|
|
image := defkit.String("image")
|
|
|
|
trait := defkit.NewTrait("hoist-and-test").
|
|
Description("Test AND condition hoisting").
|
|
AppliesTo("deployments.apps").
|
|
Params(kind, replicas, image).
|
|
Template(func(tpl *defkit.Template) {
|
|
isDeployment := defkit.Eq(kind, defkit.Lit("Deployment"))
|
|
replicasSet := replicas.IsSet()
|
|
imageSet := image.IsSet()
|
|
tpl.Patch().
|
|
If(isDeployment).
|
|
SetIf(replicasSet, "spec.replicas", replicas).
|
|
SetIf(imageSet, "spec.template.spec.image", image).
|
|
EndIf()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// The outer condition should appear at the parent level
|
|
Expect(cue).To(ContainSubstring(`parameter.kind == "Deployment"`))
|
|
// The inner conditions should remain on the children
|
|
Expect(cue).To(ContainSubstring(`parameter["replicas"] != _|_`))
|
|
Expect(cue).To(ContainSubstring(`parameter["image"] != _|_`))
|
|
})
|
|
})
|
|
|
|
Context("Multiple IfBlocks with overlapping paths", func() {
|
|
It("should render multiple IfBlocks as separate conditional blocks", func() {
|
|
kind := defkit.String("kind").Default("Deployment").Values("Deployment", "StatefulSet", "DaemonSet")
|
|
strategyType := defkit.String("strategyType").Default("RollingUpdate")
|
|
maxSurge := defkit.String("maxSurge").Default("25%")
|
|
partition := defkit.Int("partition").Default(0)
|
|
|
|
trait := defkit.NewTrait("multi-ifblock-test").
|
|
Description("Test multiple IfBlocks").
|
|
AppliesTo("deployments.apps", "statefulsets.apps", "daemonsets.apps").
|
|
Params(kind, strategyType, maxSurge, partition).
|
|
Template(func(tpl *defkit.Template) {
|
|
isDeployment := defkit.Eq(kind, defkit.Lit("Deployment"))
|
|
isStatefulSet := defkit.Eq(kind, defkit.Lit("StatefulSet"))
|
|
|
|
tpl.Patch().
|
|
If(isDeployment).
|
|
PatchStrategyAnnotation("spec.strategy", "retainKeys").
|
|
Set("spec.strategy.type", strategyType).
|
|
Set("spec.strategy.rollingUpdate.maxSurge", maxSurge).
|
|
EndIf().
|
|
If(isStatefulSet).
|
|
PatchStrategyAnnotation("spec.updateStrategy", "retainKeys").
|
|
Set("spec.updateStrategy.type", strategyType).
|
|
Set("spec.updateStrategy.rollingUpdate.partition", partition).
|
|
EndIf()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Should have two separate if blocks (not merged)
|
|
Expect(cue).To(ContainSubstring(`parameter.kind == "Deployment"`))
|
|
Expect(cue).To(ContainSubstring(`parameter.kind == "StatefulSet"`))
|
|
// Each should have its own patchStrategy annotation
|
|
Expect(strings.Count(cue, "// +patchStrategy=retainKeys")).To(Equal(2))
|
|
// Deployment uses "strategy", StatefulSet uses "updateStrategy"
|
|
Expect(cue).To(ContainSubstring("strategy: {"))
|
|
Expect(cue).To(ContainSubstring("updateStrategy: {"))
|
|
// Fields should be present
|
|
Expect(cue).To(ContainSubstring("maxSurge: parameter.maxSurge"))
|
|
Expect(cue).To(ContainSubstring("partition: parameter.partition"))
|
|
})
|
|
|
|
It("should produce correct k8s-update-strategy-like pattern", func() {
|
|
targetKind := defkit.String("targetKind").Default("Deployment").Values("Deployment", "StatefulSet", "DaemonSet")
|
|
strategy := defkit.Struct("strategy").WithFields(
|
|
defkit.Field("type", defkit.ParamTypeString).Default("RollingUpdate").Values("RollingUpdate", "Recreate", "OnDelete"),
|
|
defkit.Field("rollingStrategy", defkit.ParamTypeStruct).
|
|
Nested(defkit.Struct("rollingStrategy").WithFields(
|
|
defkit.Field("maxSurge", defkit.ParamTypeString).Default("25%"),
|
|
defkit.Field("maxUnavailable", defkit.ParamTypeString).Default("25%"),
|
|
defkit.Field("partition", defkit.ParamTypeInt).Default(0),
|
|
)),
|
|
)
|
|
|
|
trait := defkit.NewTrait("k8s-update-strategy-test").
|
|
Description("Test k8s-update-strategy pattern").
|
|
AppliesTo("deployments.apps", "statefulsets.apps", "daemonsets.apps").
|
|
PodDisruptive(false).
|
|
Params(targetKind, strategy).
|
|
Template(func(tpl *defkit.Template) {
|
|
strategyType := defkit.ParameterField("strategy.type")
|
|
maxSurge := defkit.ParameterField("strategy.rollingStrategy.maxSurge")
|
|
maxUnavailable := defkit.ParameterField("strategy.rollingStrategy.maxUnavailable")
|
|
partition := defkit.ParameterField("strategy.rollingStrategy.partition")
|
|
|
|
isDeployment := defkit.Eq(defkit.ParameterField("targetKind"), defkit.Lit("Deployment"))
|
|
isStatefulSet := defkit.Eq(defkit.ParameterField("targetKind"), defkit.Lit("StatefulSet"))
|
|
isDaemonSet := defkit.Eq(defkit.ParameterField("targetKind"), defkit.Lit("DaemonSet"))
|
|
isNotOnDelete := defkit.Ne(strategyType, defkit.Lit("OnDelete"))
|
|
isNotRecreate := defkit.Ne(strategyType, defkit.Lit("Recreate"))
|
|
isRollingUpdate := defkit.Eq(strategyType, defkit.Lit("RollingUpdate"))
|
|
|
|
tpl.Patch().
|
|
If(defkit.And(isDeployment, isNotOnDelete)).
|
|
PatchStrategyAnnotation("spec.strategy", "retainKeys").
|
|
Set("spec.strategy.type", strategyType).
|
|
SetIf(isRollingUpdate, "spec.strategy.rollingUpdate.maxSurge", maxSurge).
|
|
SetIf(isRollingUpdate, "spec.strategy.rollingUpdate.maxUnavailable", maxUnavailable).
|
|
EndIf().
|
|
If(defkit.And(isStatefulSet, isNotRecreate)).
|
|
PatchStrategyAnnotation("spec.updateStrategy", "retainKeys").
|
|
Set("spec.updateStrategy.type", strategyType).
|
|
SetIf(isRollingUpdate, "spec.updateStrategy.rollingUpdate.partition", partition).
|
|
EndIf().
|
|
If(defkit.And(isDaemonSet, isNotRecreate)).
|
|
PatchStrategyAnnotation("spec.updateStrategy", "retainKeys").
|
|
Set("spec.updateStrategy.type", strategyType).
|
|
SetIf(isRollingUpdate, "spec.updateStrategy.rollingUpdate.maxSurge", maxSurge).
|
|
SetIf(isRollingUpdate, "spec.updateStrategy.rollingUpdate.maxUnavailable", maxUnavailable).
|
|
EndIf()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Three separate if blocks
|
|
Expect(cue).To(ContainSubstring(`parameter.targetKind == "Deployment" && parameter.strategy.type != "OnDelete"`))
|
|
Expect(cue).To(ContainSubstring(`parameter.targetKind == "StatefulSet" && parameter.strategy.type != "Recreate"`))
|
|
Expect(cue).To(ContainSubstring(`parameter.targetKind == "DaemonSet" && parameter.strategy.type != "Recreate"`))
|
|
// Three patchStrategy annotations
|
|
Expect(strings.Count(cue, "// +patchStrategy=retainKeys")).To(Equal(3))
|
|
// RollingUpdate inner conditions
|
|
Expect(cue).To(ContainSubstring(`parameter.strategy.type == "RollingUpdate"`))
|
|
// Deployment uses "strategy", others use "updateStrategy"
|
|
Expect(cue).To(ContainSubstring("strategy: {"))
|
|
Expect(cue).To(ContainSubstring("updateStrategy: {"))
|
|
// Correct field assignments
|
|
Expect(cue).To(ContainSubstring("maxSurge: parameter.strategy.rollingStrategy.maxSurge"))
|
|
Expect(cue).To(ContainSubstring("maxUnavailable: parameter.strategy.rollingStrategy.maxUnavailable"))
|
|
Expect(cue).To(ContainSubstring("partition: parameter.strategy.rollingStrategy.partition"))
|
|
})
|
|
})
|
|
|
|
Context("Raw patch block with fluent params and helpers", func() {
|
|
It("should render raw patch block followed by fluent parameter and helper definitions", func() {
|
|
postStart := defkit.Map("postStart").Optional().WithSchemaRef("Handler")
|
|
preStop := defkit.Map("preStop").Optional().WithSchemaRef("Handler")
|
|
|
|
trait := defkit.NewTrait("lifecycle").
|
|
Description("test").
|
|
AppliesTo("deployments.apps").
|
|
PodDisruptive(true).
|
|
Params(postStart, preStop).
|
|
Helper("Handler", defkit.Struct("Handler").WithFields(
|
|
defkit.Field("exec", defkit.ParamTypeStruct).
|
|
Nested(defkit.Struct("exec").WithFields(
|
|
defkit.Field("command", defkit.ParamTypeArray).Of(defkit.ParamTypeString),
|
|
)),
|
|
)).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.SetRawPatchBlock(`patch: spec: containers: [...{
|
|
lifecycle: {
|
|
if parameter.postStart != _|_ {
|
|
postStart: parameter.postStart
|
|
}
|
|
}
|
|
}]`)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Raw patch block is rendered
|
|
Expect(cue).To(ContainSubstring("containers: [...{"))
|
|
Expect(cue).To(ContainSubstring("lifecycle: {"))
|
|
Expect(cue).To(ContainSubstring("if parameter.postStart != _|_"))
|
|
|
|
// Fluent parameter block is rendered (not skipped)
|
|
Expect(cue).To(ContainSubstring("parameter: {"))
|
|
Expect(cue).To(ContainSubstring("postStart?: #Handler"))
|
|
// CUE formatter may add alignment spaces: preStop?: #Handler
|
|
Expect(cue).To(ContainSubstring("preStop?:"))
|
|
Expect(cue).To(MatchRegexp(`preStop\?:\s+#Handler`))
|
|
|
|
// Helper definition is rendered (not skipped)
|
|
// CUE formatter collapses single-field struct to inline form
|
|
Expect(cue).To(ContainSubstring("#Handler:"))
|
|
Expect(cue).To(ContainSubstring("command: [...string]"))
|
|
})
|
|
|
|
It("should still use full raw mode when raw parameter block is set", func() {
|
|
trait := defkit.NewTrait("raw-all").
|
|
Description("test").
|
|
AppliesTo("deployments.apps").
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.SetRawPatchBlock(`patch: spec: replicas: parameter.replicas`)
|
|
tpl.SetRawParameterBlock(`parameter: {
|
|
replicas: *1 | int
|
|
}`)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("replicas: parameter.replicas"))
|
|
Expect(cue).To(ContainSubstring("replicas: *1 | int"))
|
|
// Should NOT have a duplicate parameter block
|
|
Expect(strings.Count(cue, "parameter:")).To(Equal(2)) // one in patch ref, one in param block
|
|
})
|
|
})
|
|
|
|
Context("IntParam helper definition rendering", func() {
|
|
It("should render constrained int helper with min and max", func() {
|
|
trait := defkit.NewTrait("int-helper-test").
|
|
Description("test").
|
|
AppliesTo("deployments.apps").
|
|
Helper("Port", defkit.Int("Port").Min(1).Max(65535)).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.SetRawPatchBlock(`patch: spec: port: parameter.port`)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("#Port: int & >=1 & <=65535"))
|
|
})
|
|
|
|
It("should render int helper with only min constraint", func() {
|
|
trait := defkit.NewTrait("int-min-test").
|
|
Description("test").
|
|
AppliesTo("deployments.apps").
|
|
Helper("Positive", defkit.Int("Positive").Min(0)).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.SetRawPatchBlock(`patch: spec: count: parameter.count`)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("#Positive: int & >=0"))
|
|
Expect(cue).NotTo(ContainSubstring("<="))
|
|
})
|
|
|
|
It("should render int helper without constraints", func() {
|
|
trait := defkit.NewTrait("int-bare-test").
|
|
Description("test").
|
|
AppliesTo("deployments.apps").
|
|
Helper("Count", defkit.Int("Count")).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.SetRawPatchBlock(`patch: spec: count: parameter.count`)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("#Count: int"))
|
|
Expect(cue).NotTo(ContainSubstring(">="))
|
|
Expect(cue).NotTo(ContainSubstring("<="))
|
|
})
|
|
})
|
|
|
|
Context("SpreadAll operation", func() {
|
|
It("should record SpreadAll ops on PatchResource", func() {
|
|
p := defkit.NewPatchResource()
|
|
elem := defkit.NewArrayElement().Set("name", defkit.Lit("test"))
|
|
p.SpreadAll("spec.containers", elem)
|
|
|
|
ops := p.Ops()
|
|
Expect(ops).To(HaveLen(1))
|
|
sa, ok := ops[0].(*defkit.SpreadAllOp)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(sa.Path()).To(Equal("spec.containers"))
|
|
Expect(sa.Elements()).To(HaveLen(1))
|
|
})
|
|
|
|
It("should record SpreadAll inside If block", func() {
|
|
p := defkit.NewPatchResource()
|
|
cond := defkit.Eq(defkit.ParameterField("enabled"), defkit.Lit(true))
|
|
elem := defkit.NewArrayElement().Set("name", defkit.Lit("test"))
|
|
p.If(cond).
|
|
SpreadAll("spec.containers", elem).
|
|
EndIf()
|
|
|
|
ops := p.Ops()
|
|
Expect(ops).To(HaveLen(1))
|
|
ifBlock, ok := ops[0].(*defkit.IfBlock)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(ifBlock.Ops()).To(HaveLen(1))
|
|
_, ok = ifBlock.Ops()[0].(*defkit.SpreadAllOp)
|
|
Expect(ok).To(BeTrue())
|
|
})
|
|
|
|
It("should render unconditional SpreadAll with simple value", func() {
|
|
image := defkit.String("image")
|
|
|
|
trait := defkit.NewTrait("spreadall-simple-test").
|
|
Description("Test SpreadAll with simple value").
|
|
AppliesTo("deployments.apps").
|
|
Params(image).
|
|
Template(func(tpl *defkit.Template) {
|
|
elem := defkit.NewArrayElement().
|
|
Set("image", image)
|
|
tpl.Patch().SpreadAll("spec.template.spec.containers", elem)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("containers: [...{"))
|
|
Expect(cue).To(ContainSubstring("image: parameter.image"))
|
|
})
|
|
|
|
It("should render conditional SpreadAll with ArrayElement SetIf", func() {
|
|
postStart := defkit.String("postStart")
|
|
preStop := defkit.String("preStop")
|
|
|
|
trait := defkit.NewTrait("spreadall-conditional-test").
|
|
Description("Test SpreadAll with conditional fields").
|
|
AppliesTo("deployments.apps").
|
|
Params(postStart, preStop).
|
|
Template(func(tpl *defkit.Template) {
|
|
elem := defkit.NewArrayElement().
|
|
SetIf(postStart.IsSet(), "lifecycle.postStart.exec.command", postStart).
|
|
SetIf(preStop.IsSet(), "lifecycle.preStop.exec.command", preStop)
|
|
tpl.Patch().SpreadAll("spec.template.spec.containers", elem)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("containers: [...{"))
|
|
Expect(cue).To(ContainSubstring(`parameter["postStart"] != _|_`))
|
|
Expect(cue).To(ContainSubstring(`parameter["preStop"] != _|_`))
|
|
})
|
|
|
|
It("should render SpreadAll inside an IfBlock", func() {
|
|
enabled := defkit.Bool("enabled").Default(false)
|
|
image := defkit.String("image")
|
|
|
|
trait := defkit.NewTrait("spreadall-ifblock-test").
|
|
Description("Test SpreadAll inside IfBlock").
|
|
AppliesTo("deployments.apps").
|
|
Params(enabled, image).
|
|
Template(func(tpl *defkit.Template) {
|
|
elem := defkit.NewArrayElement().
|
|
Set("image", image)
|
|
tpl.Patch().
|
|
If(defkit.Eq(enabled, defkit.Lit(true))).
|
|
SpreadAll("spec.template.spec.containers", elem).
|
|
EndIf()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("parameter.enabled == true"))
|
|
Expect(cue).To(ContainSubstring("containers: [...{"))
|
|
Expect(cue).To(ContainSubstring("image: parameter.image"))
|
|
})
|
|
})
|
|
|
|
Context("Multiple IfBlocks with bare ops", func() {
|
|
It("should render bare ops before conditional IfBlocks", func() {
|
|
kind := defkit.String("kind").Default("Deployment").Values("Deployment", "StatefulSet")
|
|
replicas := defkit.Int("replicas").Default(1)
|
|
strategyType := defkit.String("strategyType").Default("RollingUpdate")
|
|
|
|
trait := defkit.NewTrait("bare-ops-ifblock-test").
|
|
Description("Test bare ops with multiple IfBlocks").
|
|
AppliesTo("deployments.apps", "statefulsets.apps").
|
|
Params(kind, replicas, strategyType).
|
|
Template(func(tpl *defkit.Template) {
|
|
isDeployment := defkit.Eq(kind, defkit.Lit("Deployment"))
|
|
isStatefulSet := defkit.Eq(kind, defkit.Lit("StatefulSet"))
|
|
|
|
tpl.Patch().
|
|
Set("spec.replicas", replicas).
|
|
If(isDeployment).
|
|
Set("spec.strategy.type", strategyType).
|
|
EndIf().
|
|
If(isStatefulSet).
|
|
Set("spec.updateStrategy.type", strategyType).
|
|
EndIf()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Bare op should be present
|
|
Expect(cue).To(ContainSubstring("replicas: parameter.replicas"))
|
|
// Both conditional blocks
|
|
Expect(cue).To(ContainSubstring(`parameter.kind == "Deployment"`))
|
|
Expect(cue).To(ContainSubstring(`parameter.kind == "StatefulSet"`))
|
|
Expect(cue).To(ContainSubstring("strategy: type:"))
|
|
Expect(cue).To(ContainSubstring("updateStrategy: type:"))
|
|
})
|
|
})
|
|
|
|
Context("findIfBlockCommonPrefix edge cases", func() {
|
|
It("should handle IfBlocks with no common prefix", func() {
|
|
kind := defkit.String("kind").Default("a").Values("a", "b")
|
|
|
|
trait := defkit.NewTrait("no-common-prefix-test").
|
|
Description("Test no common prefix").
|
|
AppliesTo("deployments.apps").
|
|
Params(kind).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().
|
|
If(defkit.Eq(kind, defkit.Lit("a"))).
|
|
Set("spec.strategy.type", defkit.Lit("RollingUpdate")).
|
|
EndIf().
|
|
If(defkit.Eq(kind, defkit.Lit("b"))).
|
|
Set("metadata.labels.version", defkit.Lit("v2")).
|
|
EndIf()
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// Both conditions should be rendered
|
|
Expect(cue).To(ContainSubstring(`parameter.kind == "a"`))
|
|
Expect(cue).To(ContainSubstring(`parameter.kind == "b"`))
|
|
Expect(cue).To(ContainSubstring("strategy: type:"))
|
|
Expect(cue).To(ContainSubstring("labels: version:"))
|
|
})
|
|
})
|
|
|
|
Context("Nested array struct in helper definitions", func() {
|
|
It("should render array field with nested struct as [...{fields}]", func() {
|
|
trait := defkit.NewTrait("nested-array-test").
|
|
Description("test").
|
|
AppliesTo("deployments.apps").
|
|
Helper("Config", defkit.Struct("Config").WithFields(
|
|
defkit.Field("headers", defkit.ParamTypeArray).Optional().
|
|
Nested(defkit.Struct("headers").WithFields(
|
|
defkit.Field("name", defkit.ParamTypeString),
|
|
defkit.Field("value", defkit.ParamTypeString),
|
|
)),
|
|
)).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.SetRawPatchBlock(`patch: spec: config: parameter.config`)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
// CUE formatter collapses single-field struct to inline form
|
|
Expect(cue).To(ContainSubstring("#Config: headers?: [...{"))
|
|
Expect(cue).To(ContainSubstring("name: string"))
|
|
Expect(cue).To(ContainSubstring("value: string"))
|
|
})
|
|
|
|
It("should render schema ref on fields within helper structs", func() {
|
|
trait := defkit.NewTrait("schema-ref-test").
|
|
Description("test").
|
|
AppliesTo("deployments.apps").
|
|
Helper("Port", defkit.Int("Port").Min(1).Max(65535)).
|
|
Helper("Endpoint", defkit.Struct("Endpoint").WithFields(
|
|
defkit.Field("port", defkit.ParamTypeInt).WithSchemaRef("Port"),
|
|
defkit.Field("host", defkit.ParamTypeString).Optional(),
|
|
)).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.SetRawPatchBlock(`patch: spec: endpoint: parameter.endpoint`)
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("#Port: int & >=1 & <=65535"))
|
|
Expect(cue).To(ContainSubstring("#Endpoint: {"))
|
|
Expect(cue).To(MatchRegexp(`port:\s+#Port`))
|
|
Expect(cue).To(ContainSubstring("host?: string"))
|
|
})
|
|
})
|
|
|
|
Context("podDisruptive always emitted", func() {
|
|
It("should emit podDisruptive: true when set to true", func() {
|
|
trait := defkit.NewTrait("disruptive").
|
|
Description("Disruptive trait").
|
|
AppliesTo("deployments.apps").
|
|
PodDisruptive(true)
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("podDisruptive: true"))
|
|
})
|
|
|
|
It("should emit podDisruptive: false when set to false", func() {
|
|
trait := defkit.NewTrait("nondisruptive").
|
|
Description("Non-disruptive trait").
|
|
AppliesTo("deployments.apps").
|
|
PodDisruptive(false)
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("podDisruptive: false"))
|
|
})
|
|
|
|
It("should emit podDisruptive: false when not explicitly set", func() {
|
|
trait := defkit.NewTrait("default-disruptive").
|
|
Description("Default trait").
|
|
AppliesTo("deployments.apps")
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("podDisruptive: false"))
|
|
})
|
|
})
|
|
|
|
Context("ConflictsWith empty list emission", func() {
|
|
It("should emit conflictsWith with values when set", func() {
|
|
trait := defkit.NewTrait("conflicts-with-values").
|
|
Description("Trait with conflicts").
|
|
AppliesTo("deployments.apps").
|
|
ConflictsWith("scaler", "hpa")
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring(`conflictsWith: ["scaler", "hpa"]`))
|
|
})
|
|
|
|
It("should emit conflictsWith: [] when explicitly set with no values", func() {
|
|
trait := defkit.NewTrait("conflicts-empty").
|
|
Description("Trait with empty conflicts").
|
|
AppliesTo("deployments.apps").
|
|
ConflictsWith()
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("conflictsWith: []"))
|
|
})
|
|
|
|
It("should not emit conflictsWith when never called", func() {
|
|
trait := defkit.NewTrait("no-conflicts").
|
|
Description("Trait without conflicts").
|
|
AppliesTo("deployments.apps")
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).NotTo(ContainSubstring("conflictsWith"))
|
|
})
|
|
})
|
|
|
|
Context("Labels nil vs empty emission", func() {
|
|
It("should emit labels with values when set", func() {
|
|
trait := defkit.NewTrait("labels-values").
|
|
Description("Trait with labels").
|
|
AppliesTo("deployments.apps").
|
|
Labels(map[string]string{"ui-hidden": "true"})
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("labels:"))
|
|
Expect(cue).To(ContainSubstring(`"ui-hidden": "true"`))
|
|
})
|
|
|
|
It("should emit labels: {} when explicitly set to empty map", func() {
|
|
trait := defkit.NewTrait("labels-empty").
|
|
Description("Trait with empty labels").
|
|
AppliesTo("deployments.apps").
|
|
Labels(map[string]string{})
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("labels: {}"))
|
|
})
|
|
|
|
It("should emit empty labels block when Labels never called", func() {
|
|
trait := defkit.NewTrait("no-labels").
|
|
Description("Trait without labels").
|
|
AppliesTo("deployments.apps")
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("labels: {}"))
|
|
})
|
|
})
|
|
|
|
Context("WorkloadRefPath attribute", func() {
|
|
It("should emit workloadRefPath when set to empty string", func() {
|
|
trait := defkit.NewTrait("wlref-empty").
|
|
Description("Trait with empty workloadRefPath").
|
|
AppliesTo("deployments.apps").
|
|
WorkloadRefPath("")
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring(`workloadRefPath: ""`))
|
|
})
|
|
|
|
It("should emit workloadRefPath when set to a path", func() {
|
|
trait := defkit.NewTrait("wlref-path").
|
|
Description("Trait with workloadRefPath").
|
|
AppliesTo("deployments.apps").
|
|
WorkloadRefPath("spec.workloadRef")
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring(`workloadRefPath: "spec.workloadRef"`))
|
|
})
|
|
|
|
It("should not emit workloadRefPath when never called", func() {
|
|
trait := defkit.NewTrait("no-wlref").
|
|
Description("Trait without wlref path").
|
|
AppliesTo("deployments.apps")
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).NotTo(ContainSubstring("workloadRefPath"))
|
|
})
|
|
})
|
|
|
|
Context("PatchContainer ParamsTypeName", func() {
|
|
It("should use default PatchParams when ParamsTypeName is empty", func() {
|
|
trait := defkit.NewTrait("default-params-name").
|
|
Description("Test default params type name").
|
|
AppliesTo("deployments.apps").
|
|
PodDisruptive(true).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
|
ContainerNameParam: "containerName",
|
|
DefaultToContextName: true,
|
|
PatchFields: defkit.PatchFields(
|
|
defkit.PatchField("image").Strategy("retainKeys"),
|
|
),
|
|
})
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("#PatchParams: {"))
|
|
Expect(cue).To(ContainSubstring("_params: #PatchParams"))
|
|
Expect(cue).NotTo(ContainSubstring("#StartupProbeParams"))
|
|
})
|
|
|
|
It("should use custom ParamsTypeName when set", func() {
|
|
trait := defkit.NewTrait("custom-params-name").
|
|
Description("Test custom params type name").
|
|
AppliesTo("deployments.apps").
|
|
PodDisruptive(true).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
|
ContainerNameParam: "containerName",
|
|
DefaultToContextName: true,
|
|
AllowMultiple: true,
|
|
MultiContainerParam: "probes",
|
|
ParamsTypeName: "StartupProbeParams",
|
|
CustomParamsBlock: "initialDelaySeconds: *0 | int",
|
|
PatchFields: defkit.PatchFields(
|
|
defkit.PatchField("initialDelaySeconds").Int().IsSet().Default("0"),
|
|
),
|
|
})
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("#StartupProbeParams: {"))
|
|
Expect(cue).To(ContainSubstring("_params: #StartupProbeParams"))
|
|
Expect(cue).To(ContainSubstring("parameter: *#StartupProbeParams | close({"))
|
|
Expect(cue).To(ContainSubstring("probes: [...#StartupProbeParams]"))
|
|
Expect(cue).NotTo(ContainSubstring("#PatchParams"))
|
|
})
|
|
})
|
|
|
|
Context("PatchContainer NoDefaultDisjunction", func() {
|
|
It("should include * default marker by default", func() {
|
|
trait := defkit.NewTrait("default-disjunction").
|
|
Description("Test default disjunction").
|
|
AppliesTo("deployments.apps").
|
|
PodDisruptive(true).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
|
ContainerNameParam: "containerName",
|
|
DefaultToContextName: true,
|
|
AllowMultiple: true,
|
|
ContainersParam: "containers",
|
|
PatchFields: defkit.PatchFields(
|
|
defkit.PatchField("image").Strategy("retainKeys"),
|
|
),
|
|
})
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("parameter: *#PatchParams | close({"))
|
|
})
|
|
|
|
It("should omit * default marker when NoDefaultDisjunction is true", func() {
|
|
trait := defkit.NewTrait("no-default-disjunction").
|
|
Description("Test no default disjunction").
|
|
AppliesTo("deployments.apps").
|
|
PodDisruptive(true).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
|
ContainerNameParam: "containerName",
|
|
DefaultToContextName: true,
|
|
AllowMultiple: true,
|
|
ContainersParam: "containers",
|
|
NoDefaultDisjunction: true,
|
|
PatchFields: defkit.PatchFields(
|
|
defkit.PatchField("image").Strategy("retainKeys"),
|
|
),
|
|
})
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("parameter: #PatchParams | close({"))
|
|
Expect(cue).NotTo(ContainSubstring("parameter: *#PatchParams"))
|
|
})
|
|
})
|
|
|
|
Context("PatchContainer base container definitions", func() {
|
|
It("should generate _baseContainers (plural) and _baseContainer (singular) fields", func() {
|
|
trait := defkit.NewTrait("base-containers-test").
|
|
Description("Test base containers plural").
|
|
AppliesTo("deployments.apps").
|
|
PodDisruptive(true).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
|
ContainerNameParam: "containerName",
|
|
DefaultToContextName: true,
|
|
PatchFields: defkit.PatchFields(
|
|
defkit.PatchField("image"),
|
|
),
|
|
})
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
Expect(cue).To(ContainSubstring("_baseContainers: context.output.spec.template.spec.containers"))
|
|
Expect(cue).To(ContainSubstring("_baseContainer: *_|_ | {...}"))
|
|
})
|
|
})
|
|
|
|
Context("Annotations", func() {
|
|
It("should store and return annotations", func() {
|
|
t := defkit.NewTrait("scaler").
|
|
Annotations(map[string]string{"owner": "team-a", "env": "prod"})
|
|
Expect(t.GetAnnotations()).To(HaveKeyWithValue("owner", "team-a"))
|
|
Expect(t.GetAnnotations()).To(HaveKeyWithValue("env", "prod"))
|
|
})
|
|
|
|
It("should render sorted annotation keys in CUE", func() {
|
|
cue := defkit.NewTrait("scaler").
|
|
Annotations(map[string]string{"b": "2", "a": "1"}).
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring(`annotations: {`))
|
|
aIdx := strings.Index(cue, `"1"`)
|
|
bIdx := strings.Index(cue, `"2"`)
|
|
Expect(aIdx).To(BeNumerically("<", bIdx))
|
|
})
|
|
|
|
It("should keep annotations: {} in CUE when not set", func() {
|
|
cue := defkit.NewTrait("scaler").ToCue()
|
|
Expect(cue).To(ContainSubstring("annotations: {}"))
|
|
})
|
|
|
|
It("should merge user annotations in ToYAML without overriding description", func() {
|
|
t := defkit.NewTrait("scaler").
|
|
Description("My Trait").
|
|
Annotations(map[string]string{
|
|
"owner": "team-a",
|
|
})
|
|
yamlBytes, err := t.ToYAML()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
yaml := string(yamlBytes)
|
|
Expect(yaml).To(ContainSubstring("owner: team-a"))
|
|
Expect(yaml).To(ContainSubstring("My Trait"))
|
|
})
|
|
|
|
It("should not allow user annotation to override description in ToYAML", func() {
|
|
t := defkit.NewTrait("scaler").
|
|
Description("Actual Description").
|
|
Annotations(map[string]string{
|
|
"definition.oam.dev/description": "Not This",
|
|
})
|
|
yamlBytes, err := t.ToYAML()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
yaml := string(yamlBytes)
|
|
Expect(yaml).To(ContainSubstring("Actual Description"))
|
|
})
|
|
})
|
|
|
|
Context("Boolean CRD Spec Fields", func() {
|
|
It("should default ManageWorkload to false", func() {
|
|
Expect(defkit.NewTrait("t").IsManageWorkload()).To(BeFalse())
|
|
})
|
|
|
|
It("should set ManageWorkload to true after calling ManageWorkload()", func() {
|
|
Expect(defkit.NewTrait("t").ManageWorkload().IsManageWorkload()).To(BeTrue())
|
|
})
|
|
|
|
It("should default ControlPlaneOnly to false", func() {
|
|
Expect(defkit.NewTrait("t").IsControlPlaneOnly()).To(BeFalse())
|
|
})
|
|
|
|
It("should set ControlPlaneOnly to true after calling ControlPlaneOnly()", func() {
|
|
Expect(defkit.NewTrait("t").ControlPlaneOnly().IsControlPlaneOnly()).To(BeTrue())
|
|
})
|
|
|
|
It("should default RevisionEnabled to false", func() {
|
|
Expect(defkit.NewTrait("t").IsRevisionEnabled()).To(BeFalse())
|
|
})
|
|
|
|
It("should set RevisionEnabled to true after calling RevisionEnabled()", func() {
|
|
Expect(defkit.NewTrait("t").RevisionEnabled().IsRevisionEnabled()).To(BeTrue())
|
|
})
|
|
|
|
It("should emit manageWorkload: true in ToYAML when set", func() {
|
|
yamlBytes, err := defkit.NewTrait("t").ManageWorkload().ToYAML()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(string(yamlBytes)).To(ContainSubstring("manageWorkload: true"))
|
|
})
|
|
|
|
It("should NOT emit manageWorkload in ToYAML when not set", func() {
|
|
yamlBytes, err := defkit.NewTrait("t").ToYAML()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(string(yamlBytes)).NotTo(ContainSubstring("manageWorkload"))
|
|
})
|
|
|
|
It("should emit controlPlaneOnly: true in ToYAML when set", func() {
|
|
yamlBytes, err := defkit.NewTrait("t").ControlPlaneOnly().ToYAML()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(string(yamlBytes)).To(ContainSubstring("controlPlaneOnly: true"))
|
|
})
|
|
|
|
It("should emit revisionEnabled: true in ToYAML when set", func() {
|
|
yamlBytes, err := defkit.NewTrait("t").RevisionEnabled().ToYAML()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(string(yamlBytes)).To(ContainSubstring("revisionEnabled: true"))
|
|
})
|
|
|
|
It("should support chaining and return *TraitDefinition", func() {
|
|
trait := defkit.NewTrait("t").ManageWorkload().ControlPlaneOnly().RevisionEnabled()
|
|
Expect(trait.IsManageWorkload()).To(BeTrue())
|
|
Expect(trait.IsControlPlaneOnly()).To(BeTrue())
|
|
Expect(trait.IsRevisionEnabled()).To(BeTrue())
|
|
})
|
|
|
|
It("should NOT emit controlPlaneOnly in ToYAML when not set", func() {
|
|
yamlBytes, err := defkit.NewTrait("t").ToYAML()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(string(yamlBytes)).NotTo(ContainSubstring("controlPlaneOnly"))
|
|
})
|
|
|
|
It("should NOT emit revisionEnabled in ToYAML when not set", func() {
|
|
yamlBytes, err := defkit.NewTrait("t").ToYAML()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(string(yamlBytes)).NotTo(ContainSubstring("revisionEnabled"))
|
|
})
|
|
|
|
It("should emit manageWorkload: true in CUE attributes when set", func() {
|
|
cue := defkit.NewTrait("t").ManageWorkload().ToCue()
|
|
Expect(cue).To(ContainSubstring("manageWorkload: true"))
|
|
})
|
|
|
|
It("should NOT emit manageWorkload in CUE attributes when not set", func() {
|
|
cue := defkit.NewTrait("t").ToCue()
|
|
Expect(cue).NotTo(ContainSubstring("manageWorkload"))
|
|
})
|
|
|
|
It("should emit controlPlaneOnly: true in CUE attributes when set", func() {
|
|
cue := defkit.NewTrait("t").ControlPlaneOnly().ToCue()
|
|
Expect(cue).To(ContainSubstring("controlPlaneOnly: true"))
|
|
})
|
|
|
|
It("should NOT emit controlPlaneOnly in CUE attributes when not set", func() {
|
|
cue := defkit.NewTrait("t").ToCue()
|
|
Expect(cue).NotTo(ContainSubstring("controlPlaneOnly"))
|
|
})
|
|
|
|
It("should emit revisionEnabled: true in CUE attributes when set", func() {
|
|
cue := defkit.NewTrait("t").RevisionEnabled().ToCue()
|
|
Expect(cue).To(ContainSubstring("revisionEnabled: true"))
|
|
})
|
|
|
|
It("should NOT emit revisionEnabled in CUE attributes when not set", func() {
|
|
cue := defkit.NewTrait("t").ToCue()
|
|
Expect(cue).NotTo(ContainSubstring("revisionEnabled"))
|
|
})
|
|
})
|
|
|
|
Context("Status Block CUE Render", func() {
|
|
It("should render statusDetails in trait CUE", func() {
|
|
cue := defkit.NewTrait("t").StatusDetails("phase: context.output.status.phase").ToCue()
|
|
Expect(cue).To(ContainSubstring("status:"))
|
|
Expect(cue).To(ContainSubstring("details:"))
|
|
Expect(cue).To(ContainSubstring("phase: context.output.status.phase"))
|
|
})
|
|
|
|
It("should render customStatus in trait CUE", func() {
|
|
cue := defkit.NewTrait("t").CustomStatus("message: \"Running\"").ToCue()
|
|
Expect(cue).To(ContainSubstring("status:"))
|
|
Expect(cue).To(ContainSubstring("customStatus:"))
|
|
})
|
|
|
|
It("should render healthPolicy in trait CUE", func() {
|
|
cue := defkit.NewTrait("t").HealthPolicy("isHealth: true").ToCue()
|
|
Expect(cue).To(ContainSubstring("status:"))
|
|
Expect(cue).To(ContainSubstring("healthPolicy:"))
|
|
})
|
|
|
|
It("should omit status block when none set", func() {
|
|
cue := defkit.NewTrait("t").ToCue()
|
|
Expect(cue).NotTo(ContainSubstring("status:"))
|
|
})
|
|
|
|
It("should render all three status fields together", func() {
|
|
cue := defkit.NewTrait("t").
|
|
CustomStatus("message: \"Running\"").
|
|
HealthPolicy("isHealth: true").
|
|
StatusDetails("phase: context.output.status.phase").
|
|
ToCue()
|
|
Expect(cue).To(ContainSubstring("status:"))
|
|
Expect(cue).To(ContainSubstring("customStatus:"))
|
|
Expect(cue).To(ContainSubstring("healthPolicy:"))
|
|
Expect(cue).To(ContainSubstring("details:"))
|
|
})
|
|
})
|
|
|
|
Context("Labels ordering", func() {
|
|
It("should render label keys sorted alphabetically in CUE", func() {
|
|
cue := defkit.NewTrait("scaler").
|
|
Labels(map[string]string{"z-team": "infra", "a-env": "prod", "m-region": "us"}).
|
|
ToCue()
|
|
|
|
aIdx := strings.Index(cue, `"a-env"`)
|
|
mIdx := strings.Index(cue, `"m-region"`)
|
|
zIdx := strings.Index(cue, `"z-team"`)
|
|
Expect(aIdx).To(BeNumerically(">=", 0))
|
|
Expect(mIdx).To(BeNumerically(">=", 0))
|
|
Expect(zIdx).To(BeNumerically(">=", 0))
|
|
Expect(aIdx).To(BeNumerically("<", mIdx))
|
|
Expect(mIdx).To(BeNumerically("<", zIdx))
|
|
})
|
|
})
|
|
|
|
Context("Output groups ordering", func() {
|
|
It("should render grouped output names sorted alphabetically in CUE", func() {
|
|
trait := defkit.NewTrait("expose").
|
|
Description("Expose ports").
|
|
Params(defkit.Int("port").Default(80)).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().Set("spec.replicas", defkit.Lit(1))
|
|
tpl.Outputs("base-svc", defkit.NewResource("v1", "Service").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
tpl.OutputsGroupIf(defkit.PathExists("parameter.port"), 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 := trait.ToCue()
|
|
|
|
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))
|
|
})
|
|
})
|
|
|
|
Context("OutputsGroupIf with no plain Outputs sibling", func() {
|
|
It("should render an outputs block for a trait with only grouped outputs", func() {
|
|
trait := defkit.NewTrait("tls-companions").
|
|
Description("Attach TLS companion resources").
|
|
Params(defkit.Bool("tlsEnabled").Default(false)).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().Set("spec.replicas", defkit.Lit(1))
|
|
tpl.OutputsGroupIf(defkit.PathExists("parameter.tlsEnabled"), func(g *defkit.OutputGroup) {
|
|
g.Add("tls-secret", defkit.NewResource("v1", "Secret").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
g.Add("tls-cm", defkit.NewResource("v1", "ConfigMap").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
})
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("outputs: {"))
|
|
Expect(cue).To(ContainSubstring("if parameter.tlsEnabled != _|_ {"))
|
|
Expect(cue).To(ContainSubstring("tls-cm"))
|
|
Expect(cue).To(ContainSubstring("tls-secret"))
|
|
|
|
cmIdx := strings.Index(cue, "tls-cm")
|
|
secIdx := strings.Index(cue, "tls-secret")
|
|
Expect(cmIdx).To(BeNumerically(">=", 0))
|
|
Expect(secIdx).To(BeNumerically(">=", 0))
|
|
Expect(cmIdx).To(BeNumerically("<", secIdx))
|
|
})
|
|
|
|
It("should render multiple independent OutputsGroupIf blocks with no plain outputs", func() {
|
|
trait := defkit.NewTrait("multi-group").
|
|
Description("Multiple grouped outputs").
|
|
Params(
|
|
defkit.Bool("tlsEnabled").Default(false),
|
|
defkit.Bool("tracingEnabled").Default(false),
|
|
).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().Set("spec.replicas", defkit.Lit(1))
|
|
tpl.OutputsGroupIf(defkit.PathExists("parameter.tlsEnabled"), func(g *defkit.OutputGroup) {
|
|
g.Add("tls-secret", defkit.NewResource("v1", "Secret").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
})
|
|
tpl.OutputsGroupIf(defkit.PathExists("parameter.tracingEnabled"), func(g *defkit.OutputGroup) {
|
|
g.Add("tracing-cm", defkit.NewResource("v1", "ConfigMap").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
})
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
Expect(cue).To(ContainSubstring("outputs: {"))
|
|
Expect(cue).To(ContainSubstring("if parameter.tlsEnabled != _|_ {"))
|
|
Expect(cue).To(ContainSubstring("if parameter.tracingEnabled != _|_ {"))
|
|
Expect(cue).To(ContainSubstring("tls-secret"))
|
|
Expect(cue).To(ContainSubstring("tracing-cm"))
|
|
})
|
|
|
|
It("should render plain Outputs before grouped outputs on a trait", func() {
|
|
trait := defkit.NewTrait("mixed").
|
|
Description("Mix plain and grouped outputs").
|
|
Params(defkit.Bool("extras").Default(false)).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Patch().Set("spec.replicas", defkit.Lit(1))
|
|
tpl.Outputs("always-cm", defkit.NewResource("v1", "ConfigMap").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
tpl.OutputsGroupIf(defkit.PathExists("parameter.extras"), func(g *defkit.OutputGroup) {
|
|
g.Add("extra-secret", defkit.NewResource("v1", "Secret").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
})
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
alwaysIdx := strings.Index(cue, "always-cm")
|
|
ifIdx := strings.Index(cue, "if parameter.extras != _|_ {")
|
|
extraIdx := strings.Index(cue, "extra-secret")
|
|
Expect(alwaysIdx).To(BeNumerically(">=", 0))
|
|
Expect(ifIdx).To(BeNumerically(">=", 0))
|
|
Expect(extraIdx).To(BeNumerically(">=", 0))
|
|
Expect(alwaysIdx).To(BeNumerically("<", ifIdx))
|
|
Expect(ifIdx).To(BeNumerically("<", extraIdx))
|
|
})
|
|
})
|
|
|
|
Context("TemplateBlock with labels", func() {
|
|
It("should render sorted labels when using TemplateBlock", func() {
|
|
result := defkit.NewTrait("scaler").
|
|
Labels(map[string]string{"z-team": "infra", "a-env": "prod"}).
|
|
Params(defkit.Int("replicas").Default(1)).
|
|
TemplateBlock("patch: spec: replicas: parameter.replicas").
|
|
ToCue()
|
|
|
|
aIdx := strings.Index(result, `"a-env"`)
|
|
zIdx := strings.Index(result, `"z-team"`)
|
|
Expect(aIdx).To(BeNumerically(">=", 0))
|
|
Expect(zIdx).To(BeNumerically(">=", 0))
|
|
Expect(aIdx).To(BeNumerically("<", zIdx))
|
|
})
|
|
})
|
|
|
|
Context("Outputs ordering", func() {
|
|
It("should render output names sorted alphabetically in CUE", func() {
|
|
trait := defkit.NewTrait("expose").
|
|
Description("Expose ports").
|
|
Params(defkit.Int("port").Default(80)).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Outputs("zebra-ingress", defkit.NewResource("networking.k8s.io/v1", "Ingress").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
tpl.Outputs("alpha-svc", defkit.NewResource("v1", "Service").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
})
|
|
|
|
cue := trait.ToCue()
|
|
|
|
alphaIdx := strings.Index(cue, "alpha-svc")
|
|
zebraIdx := strings.Index(cue, "zebra-ingress")
|
|
Expect(alphaIdx).To(BeNumerically(">=", 0))
|
|
Expect(zebraIdx).To(BeNumerically(">=", 0))
|
|
Expect(alphaIdx).To(BeNumerically("<", zebraIdx))
|
|
})
|
|
})
|
|
|
|
Context("Repeated generation stability", func() {
|
|
It("should produce identical CUE across 20 runs", func() {
|
|
build := func() string {
|
|
return defkit.NewTrait("scaler").
|
|
Labels(map[string]string{"z": "3", "a": "1", "m": "2"}).
|
|
Description("scale pods").
|
|
Params(defkit.Int("replicas").Default(1)).
|
|
Template(func(tpl *defkit.Template) {
|
|
tpl.Outputs("z-hpa", defkit.NewResource("autoscaling/v2", "HorizontalPodAutoscaler").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
tpl.Outputs("a-pdb", defkit.NewResource("policy/v1", "PodDisruptionBudget").
|
|
Set("metadata.name", defkit.VelaCtx().Name()))
|
|
}).
|
|
ToCue()
|
|
}
|
|
|
|
first := build()
|
|
for i := 0; i < 20; i++ {
|
|
Expect(build()).To(Equal(first), "CUE output differed on iteration %d", i)
|
|
}
|
|
})
|
|
})
|
|
})
|