mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
Feat: defkit comp def discrepancies (#7048)
* Feat: add MapVariant operation and support for OneOf parameters with default variant Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: enhance string parameter output to include optional prefix in CUE generation Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: add ConditionalOrFieldRef for fallback handling and support inline array values Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: update CUE generation to support inline arrays with conditional wrapping Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: update CUE generation to support inline arrays with conditional wrapping Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: add support for compound optional fields and enhance array builder with guarded filtering Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: add comprehensive tests for ArrayBuilder functionality Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: implement field grouping in StatusBuilder for consolidated CUE output Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: enhance CUE decomposition to support condValues and improve filtering logic Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: add metadata labels to ComponentDefinition and update CUE generation Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: clean up comments and formatting in cuegen and param files Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: enhance CUE generation, trait definitions, and PatchContainer logic CUE Generation: - Simplify condition decomposition logic and rename GetDirective method - Add condition decomposition and lifting logic to improve generated CUE output - Refactor cueTypeForParamType to use a standalone cueTypeStr function for reusability Collections: - Enhance MapVariant operation to merge variant mappings and preserve non-matching items Trait Definitions: - Enhance TraitDefinition and PatchContainerConfig with new attributes (MultiContainerCheckField, MultiContainerErrMsg) - Update emission logic in TraitCUEGenerator for multi-container support PatchContainer: - Update error messages to use camelCase for consistency with KubeVela conventions Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: enhance test descriptions for clarity and accuracy in array_builder, collections, and status tests Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: enhance handling of optional fields in collections and improve test descriptions for clarity Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: improve ConditionalOrFieldRef tests for clarity and accuracy in handling primary and fallback fields Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: enhance test descriptions for clarity and accuracy in array_builder and expr tests Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * Feat: improve formatting of test data in collections tests for better readability Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> --------- Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
This commit is contained in:
@@ -32,6 +32,7 @@ type arrayEntry struct {
|
||||
cond Condition // for conditional entries
|
||||
source Value // for forEach entries (iteration source)
|
||||
guard Condition // for forEach entries (optional guard: if source != _|_)
|
||||
filter Predicate // for forEach entries (optional filter: if v.field == value)
|
||||
itemBuilder *ItemBuilder // for forEachWith entries (complex per-item logic)
|
||||
}
|
||||
|
||||
@@ -129,6 +130,27 @@ func (a *ArrayBuilder) ForEachWithVar(varName string, source Value, fn func(item
|
||||
return a
|
||||
}
|
||||
|
||||
// ForEachWithGuardedFiltered adds a guarded and filtered complex iterated item to the array.
|
||||
// The guard condition wraps the for loop, and the filter predicate filters iteration items.
|
||||
// Generates: if guard for v in source if filter { ... }
|
||||
func (a *ArrayBuilder) ForEachWithGuardedFiltered(guard Condition, filter Predicate, source Value, fn func(item *ItemBuilder)) *ArrayBuilder {
|
||||
return a.ForEachWithGuardedFilteredVar("v", guard, filter, source, fn)
|
||||
}
|
||||
|
||||
// ForEachWithGuardedFilteredVar is like ForEachWithGuardedFiltered but allows specifying the iteration variable name.
|
||||
func (a *ArrayBuilder) ForEachWithGuardedFilteredVar(varName string, guard Condition, filter Predicate, source Value, fn func(item *ItemBuilder)) *ArrayBuilder {
|
||||
ib := &ItemBuilder{varName: varName, ops: make([]itemOp, 0)}
|
||||
fn(ib)
|
||||
a.entries = append(a.entries, arrayEntry{
|
||||
kind: entryForEachWith,
|
||||
source: source,
|
||||
guard: guard,
|
||||
filter: filter,
|
||||
itemBuilder: ib,
|
||||
})
|
||||
return a
|
||||
}
|
||||
|
||||
// --- ItemBuilder ---
|
||||
|
||||
// itemOp is a single operation recorded by the ItemBuilder.
|
||||
|
||||
@@ -0,0 +1,744 @@
|
||||
/*
|
||||
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 (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/definition/defkit"
|
||||
)
|
||||
|
||||
var _ = Describe("ArrayBuilder", func() {
|
||||
|
||||
Describe("NewArray", func() {
|
||||
It("should create an empty array builder", func() {
|
||||
ab := defkit.NewArray()
|
||||
Expect(ab).NotTo(BeNil())
|
||||
Expect(ab.Entries()).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should implement the Value interface and retain builder behavior", func() {
|
||||
ab := defkit.NewArray()
|
||||
var v defkit.Value = ab // compile-time interface check
|
||||
Expect(v).NotTo(BeNil())
|
||||
// Verify the builder still works through the interface
|
||||
ab.Item(defkit.NewArrayElement().Set("name", defkit.Lit("test")))
|
||||
Expect(ab.Entries()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Item", func() {
|
||||
It("should add a static entry", func() {
|
||||
elem := defkit.NewArrayElement().Set("name", defkit.Lit("cpu"))
|
||||
ab := defkit.NewArray().Item(elem)
|
||||
Expect(ab.Entries()).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should chain multiple static items", func() {
|
||||
elem1 := defkit.NewArrayElement().Set("name", defkit.Lit("cpu"))
|
||||
elem2 := defkit.NewArrayElement().Set("name", defkit.Lit("memory"))
|
||||
ab := defkit.NewArray().Item(elem1).Item(elem2)
|
||||
Expect(ab.Entries()).To(HaveLen(2))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ItemIf", func() {
|
||||
It("should add a conditional entry", func() {
|
||||
mem := defkit.String("memory")
|
||||
elem := defkit.NewArrayElement().Set("name", defkit.Lit("memory"))
|
||||
ab := defkit.NewArray().ItemIf(mem.IsSet(), elem)
|
||||
Expect(ab.Entries()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ForEach", func() {
|
||||
It("should add a forEach entry", func() {
|
||||
ports := defkit.List("ports")
|
||||
elem := defkit.NewArrayElement().Set("port", defkit.Reference("m.port"))
|
||||
ab := defkit.NewArray().ForEach(ports, elem)
|
||||
Expect(ab.Entries()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ForEachGuarded", func() {
|
||||
It("should add a guarded forEach entry", func() {
|
||||
ports := defkit.List("ports")
|
||||
elem := defkit.NewArrayElement().Set("port", defkit.Reference("m.port"))
|
||||
ab := defkit.NewArray().ForEachGuarded(ports.IsSet(), ports, elem)
|
||||
Expect(ab.Entries()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ForEachWith", func() {
|
||||
It("should add a forEachWith entry with ItemBuilder", func() {
|
||||
ports := defkit.List("ports")
|
||||
ab := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.Set("port", v.Field("port"))
|
||||
})
|
||||
entries := ab.Entries()
|
||||
Expect(entries).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should use 'v' as default variable name", func() {
|
||||
ports := defkit.List("ports")
|
||||
ab := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
Expect(item.VarName()).To(Equal("v"))
|
||||
})
|
||||
Expect(ab.Entries()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ForEachWithVar", func() {
|
||||
It("should use custom variable name", func() {
|
||||
ports := defkit.List("ports")
|
||||
ab := defkit.NewArray().ForEachWithVar("p", ports, func(item *defkit.ItemBuilder) {
|
||||
Expect(item.VarName()).To(Equal("p"))
|
||||
v := item.Var()
|
||||
item.Set("port", v.Field("port"))
|
||||
})
|
||||
Expect(ab.Entries()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ForEachWithGuardedFiltered", func() {
|
||||
It("should add entry with guard and filter", func() {
|
||||
ports := defkit.List("ports")
|
||||
ab := defkit.NewArray().ForEachWithGuardedFiltered(
|
||||
ports.IsSet(),
|
||||
defkit.FieldEquals("expose", true),
|
||||
ports,
|
||||
func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.Set("port", v.Field("port"))
|
||||
},
|
||||
)
|
||||
Expect(ab.Entries()).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should use 'v' as default variable name", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWithGuardedFiltered(
|
||||
ports.IsSet(),
|
||||
defkit.FieldEquals("expose", true),
|
||||
ports,
|
||||
func(item *defkit.ItemBuilder) {
|
||||
Expect(item.VarName()).To(Equal("v"))
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ForEachWithGuardedFilteredVar", func() {
|
||||
It("should use custom variable name with guard and filter", func() {
|
||||
ports := defkit.List("ports")
|
||||
ab := defkit.NewArray().ForEachWithGuardedFilteredVar(
|
||||
"p",
|
||||
ports.IsSet(),
|
||||
defkit.FieldEquals("expose", true),
|
||||
ports,
|
||||
func(item *defkit.ItemBuilder) {
|
||||
Expect(item.VarName()).To(Equal("p"))
|
||||
v := item.Var()
|
||||
item.Set("port", v.Field("port"))
|
||||
},
|
||||
)
|
||||
Expect(ab.Entries()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("mixed entries", func() {
|
||||
It("should support mixing static, conditional, forEach, and forEachWith entries", func() {
|
||||
mem := defkit.String("memory")
|
||||
ports := defkit.List("ports")
|
||||
metrics := defkit.List("metrics")
|
||||
|
||||
staticElem := defkit.NewArrayElement().Set("name", defkit.Lit("cpu"))
|
||||
condElem := defkit.NewArrayElement().Set("name", defkit.Lit("memory"))
|
||||
forEachElem := defkit.NewArrayElement().Set("name", defkit.Reference("m.name"))
|
||||
|
||||
ab := defkit.NewArray().
|
||||
Item(staticElem).
|
||||
ItemIf(mem.IsSet(), condElem).
|
||||
ForEach(metrics, forEachElem).
|
||||
ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.Set("port", v.Field("port"))
|
||||
})
|
||||
|
||||
Expect(ab.Entries()).To(HaveLen(4))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("fluent chaining", func() {
|
||||
It("should return the same builder for all methods", func() {
|
||||
ab := defkit.NewArray()
|
||||
elem := defkit.NewArrayElement().Set("name", defkit.Lit("test"))
|
||||
ports := defkit.List("ports")
|
||||
|
||||
result := ab.Item(elem)
|
||||
Expect(result).To(BeIdenticalTo(ab))
|
||||
|
||||
result = ab.ItemIf(ports.IsSet(), elem)
|
||||
Expect(result).To(BeIdenticalTo(ab))
|
||||
|
||||
result = ab.ForEach(ports, elem)
|
||||
Expect(result).To(BeIdenticalTo(ab))
|
||||
|
||||
result = ab.ForEachGuarded(ports.IsSet(), ports, elem)
|
||||
Expect(result).To(BeIdenticalTo(ab))
|
||||
|
||||
result = ab.ForEachWith(ports, func(item *defkit.ItemBuilder) {})
|
||||
Expect(result).To(BeIdenticalTo(ab))
|
||||
|
||||
result = ab.ForEachWithGuardedFiltered(
|
||||
ports.IsSet(), defkit.FieldEquals("expose", true), ports,
|
||||
func(item *defkit.ItemBuilder) {},
|
||||
)
|
||||
Expect(result).To(BeIdenticalTo(ab))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("ItemBuilder", func() {
|
||||
|
||||
Describe("Var", func() {
|
||||
It("should return an IterVarBuilder with the correct variable name", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
Expect(v).NotTo(BeNil())
|
||||
|
||||
// v.Field returns an IterFieldRef which implements Value
|
||||
fieldRef := v.Field("port")
|
||||
Expect(fieldRef).NotTo(BeNil())
|
||||
Expect(fieldRef.VarName()).To(Equal("v"))
|
||||
Expect(fieldRef.FieldName()).To(Equal("port"))
|
||||
|
||||
// v.Ref returns an IterVarRef for the whole variable
|
||||
varRef := v.Ref()
|
||||
Expect(varRef).NotTo(BeNil())
|
||||
Expect(varRef.VarName()).To(Equal("v"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Set", func() {
|
||||
It("should record a setOp", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.Set("port", v.Field("port"))
|
||||
item.Set("name", v.Field("name"))
|
||||
Expect(item.Ops()).To(HaveLen(2))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("If", func() {
|
||||
It("should record an ifBlockOp with nested operations", func() {
|
||||
ports := defkit.List("ports")
|
||||
exposeType := defkit.String("exposeType")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.If(defkit.Eq(exposeType, defkit.Lit("NodePort")), func() {
|
||||
item.Set("nodePort", v.Field("nodePort"))
|
||||
})
|
||||
Expect(item.Ops()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("IfSet", func() {
|
||||
It("should record a conditional for field existence", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.IfSet("name", func() {
|
||||
item.Set("name", v.Field("name"))
|
||||
})
|
||||
Expect(item.Ops()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("IfNotSet", func() {
|
||||
It("should record a conditional for field non-existence", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
_ = item.Var()
|
||||
item.IfNotSet("name", func() {
|
||||
item.Set("name", defkit.Lit("default"))
|
||||
})
|
||||
Expect(item.Ops()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("IfSet and IfNotSet together", func() {
|
||||
It("should form an if/else pattern", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.IfSet("containerPort", func() {
|
||||
item.Set("containerPort", v.Field("containerPort"))
|
||||
})
|
||||
item.IfNotSet("containerPort", func() {
|
||||
item.Set("containerPort", v.Field("port"))
|
||||
})
|
||||
Expect(item.Ops()).To(HaveLen(2))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Let", func() {
|
||||
It("should record a letOp and return a reference with correct name", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
ref := item.Let("_name",
|
||||
defkit.Plus(defkit.Lit("port-"), defkit.StrconvFormatInt(v.Field("port"), 10)))
|
||||
letRef, ok := ref.(*defkit.IterLetRef)
|
||||
Expect(ok).To(BeTrue(), "expected *IterLetRef")
|
||||
Expect(letRef.RefName()).To(Equal("_name"))
|
||||
Expect(item.Ops()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
It("should allow using the let reference in subsequent operations", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
nameRef := item.Let("_name",
|
||||
defkit.Plus(defkit.Lit("port-"), defkit.StrconvFormatInt(v.Field("port"), 10)))
|
||||
item.SetDefault("name", nameRef, "string")
|
||||
Expect(item.Ops()).To(HaveLen(2))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("SetDefault", func() {
|
||||
It("should record a setDefaultOp", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
item.SetDefault("name", defkit.Lit("default"), "string")
|
||||
Expect(item.Ops()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("FieldExists", func() {
|
||||
It("should return a Condition for field existence with correct var and field name", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
cond := item.FieldExists("name")
|
||||
Expect(cond).NotTo(BeNil())
|
||||
iterCond, ok := cond.(*defkit.IterFieldExistsCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *IterFieldExistsCondition")
|
||||
Expect(iterCond.VarName()).To(Equal("v"))
|
||||
Expect(iterCond.FieldName()).To(Equal("name"))
|
||||
Expect(iterCond.IsNegated()).To(BeFalse())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("FieldNotExists", func() {
|
||||
It("should return a negated Condition for field non-existence", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
cond := item.FieldNotExists("name")
|
||||
Expect(cond).NotTo(BeNil())
|
||||
iterCond, ok := cond.(*defkit.IterFieldExistsCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *IterFieldExistsCondition")
|
||||
Expect(iterCond.VarName()).To(Equal("v"))
|
||||
Expect(iterCond.FieldName()).To(Equal("name"))
|
||||
Expect(iterCond.IsNegated()).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("complex ItemBuilder pattern", func() {
|
||||
It("should support nested conditionals with let bindings and defaults", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
|
||||
// Unconditional field
|
||||
item.Set("port", v.Field("port"))
|
||||
|
||||
// If/else for containerPort
|
||||
item.IfSet("containerPort", func() {
|
||||
item.Set("containerPort", v.Field("containerPort"))
|
||||
})
|
||||
item.IfNotSet("containerPort", func() {
|
||||
item.Set("containerPort", v.Field("port"))
|
||||
})
|
||||
|
||||
// Nested: name with let binding, default, and protocol suffix
|
||||
item.IfSet("name", func() {
|
||||
item.Set("name", v.Field("name"))
|
||||
})
|
||||
item.IfNotSet("name", func() {
|
||||
nameRef := item.Let("_name",
|
||||
defkit.Plus(defkit.Lit("port-"), defkit.StrconvFormatInt(v.Field("port"), 10)))
|
||||
item.SetDefault("name", nameRef, "string")
|
||||
item.If(defkit.Ne(v.Field("protocol"), defkit.Lit("TCP")), func() {
|
||||
item.Set("name", defkit.Plus(nameRef, defkit.Lit("-"), defkit.StringsToLower(v.Field("protocol"))))
|
||||
})
|
||||
})
|
||||
|
||||
// set=1, ifSet(containerPort)=1, ifNotSet(containerPort)=1, ifSet(name)=1, ifNotSet(name)=1
|
||||
Expect(item.Ops()).To(HaveLen(5))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("IterVarBuilder", func() {
|
||||
It("should return field references with correct variable and field names", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
portRef := v.Field("port")
|
||||
Expect(portRef.VarName()).To(Equal("v"))
|
||||
Expect(portRef.FieldName()).To(Equal("port"))
|
||||
|
||||
nameRef := v.Field("name")
|
||||
Expect(nameRef.VarName()).To(Equal("v"))
|
||||
Expect(nameRef.FieldName()).To(Equal("name"))
|
||||
})
|
||||
})
|
||||
|
||||
It("should return a whole-variable reference with correct variable name", func() {
|
||||
items := defkit.StringList("items")
|
||||
defkit.NewArray().ForEachWith(items, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
ref := v.Ref()
|
||||
Expect(ref.VarName()).To(Equal("v"))
|
||||
})
|
||||
})
|
||||
|
||||
It("should use custom variable name for field references", func() {
|
||||
ports := defkit.List("ports")
|
||||
defkit.NewArray().ForEachWithVar("p", ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
fieldRef := v.Field("port")
|
||||
Expect(fieldRef.VarName()).To(Equal("p"))
|
||||
Expect(fieldRef.FieldName()).To(Equal("port"))
|
||||
|
||||
ref := v.Ref()
|
||||
Expect(ref.VarName()).To(Equal("p"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("ArrayConcat", func() {
|
||||
It("should create an array concatenation value", func() {
|
||||
left := defkit.NewArray()
|
||||
right := defkit.List("extra")
|
||||
concat := defkit.ArrayConcat(left, right)
|
||||
Expect(concat).NotTo(BeNil())
|
||||
Expect(concat.Left()).To(BeIdenticalTo(left))
|
||||
Expect(concat.Right()).To(BeIdenticalTo(right))
|
||||
})
|
||||
|
||||
It("should implement the Value interface and preserve operands", func() {
|
||||
left := defkit.NewArray()
|
||||
right := defkit.List("extra")
|
||||
var v defkit.Value = defkit.ArrayConcat(left, right) // compile-time interface check
|
||||
Expect(v).NotTo(BeNil())
|
||||
concat := v.(*defkit.ArrayConcatValue)
|
||||
Expect(concat.Left()).To(BeIdenticalTo(left))
|
||||
Expect(concat.Right()).To(BeIdenticalTo(right))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("ArrayBuilder CUE Generation", func() {
|
||||
var gen *defkit.CUEGenerator
|
||||
|
||||
BeforeEach(func() {
|
||||
gen = defkit.NewCUEGenerator()
|
||||
})
|
||||
|
||||
It("should generate CUE for ForEachWith with simple field assignments", func() {
|
||||
ports := defkit.List("ports").WithFields(
|
||||
defkit.Int("port").Required(),
|
||||
defkit.String("name"),
|
||||
)
|
||||
comp := defkit.NewComponent("test").
|
||||
Workload("apps/v1", "Deployment").
|
||||
Params(ports).
|
||||
Template(func(tpl *defkit.Template) {
|
||||
containerPorts := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.Set("containerPort", v.Field("port"))
|
||||
item.Set("name", v.Field("name"))
|
||||
})
|
||||
tpl.Output(
|
||||
defkit.NewResource("apps/v1", "Deployment").
|
||||
SetIf(ports.IsSet(), "spec.template.spec.containers[0].ports", containerPorts),
|
||||
)
|
||||
})
|
||||
|
||||
cue := gen.GenerateFullDefinition(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring("for v in parameter.ports"))
|
||||
Expect(cue).To(ContainSubstring("containerPort: v.port"))
|
||||
Expect(cue).To(ContainSubstring("name: v.name"))
|
||||
})
|
||||
|
||||
It("should generate CUE for ForEachWith with IfSet/IfNotSet conditionals", func() {
|
||||
ports := defkit.List("ports").WithFields(
|
||||
defkit.Int("port").Required(),
|
||||
defkit.Int("containerPort"),
|
||||
)
|
||||
comp := defkit.NewComponent("test").
|
||||
Workload("apps/v1", "Deployment").
|
||||
Params(ports).
|
||||
Template(func(tpl *defkit.Template) {
|
||||
containerPorts := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.IfSet("containerPort", func() {
|
||||
item.Set("containerPort", v.Field("containerPort"))
|
||||
})
|
||||
item.IfNotSet("containerPort", func() {
|
||||
item.Set("containerPort", v.Field("port"))
|
||||
})
|
||||
})
|
||||
tpl.Output(
|
||||
defkit.NewResource("apps/v1", "Deployment").
|
||||
SetIf(ports.IsSet(), "spec.template.spec.containers[0].ports", containerPorts),
|
||||
)
|
||||
})
|
||||
|
||||
cue := gen.GenerateFullDefinition(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring("if v.containerPort != _|_"))
|
||||
Expect(cue).To(ContainSubstring("containerPort: v.containerPort"))
|
||||
Expect(cue).To(ContainSubstring("if v.containerPort == _|_"))
|
||||
Expect(cue).To(ContainSubstring("containerPort: v.port"))
|
||||
})
|
||||
|
||||
It("should generate CUE for ForEachWith with let bindings and defaults", func() {
|
||||
ports := defkit.List("ports").WithFields(
|
||||
defkit.Int("port").Required(),
|
||||
)
|
||||
comp := defkit.NewComponent("test").
|
||||
Workload("apps/v1", "Deployment").
|
||||
Params(ports).
|
||||
Template(func(tpl *defkit.Template) {
|
||||
containerPorts := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
nameRef := item.Let("_name",
|
||||
defkit.Plus(defkit.Lit("port-"), defkit.StrconvFormatInt(v.Field("port"), 10)))
|
||||
item.SetDefault("name", nameRef, "string")
|
||||
})
|
||||
tpl.Output(
|
||||
defkit.NewResource("apps/v1", "Deployment").
|
||||
SetIf(ports.IsSet(), "spec.template.spec.containers[0].ports", containerPorts),
|
||||
)
|
||||
})
|
||||
|
||||
cue := gen.GenerateFullDefinition(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring(`_name: "port-" + strconv.FormatInt(v.port, 10)`))
|
||||
Expect(cue).To(ContainSubstring("name: *_name | string"))
|
||||
})
|
||||
|
||||
It("should generate CUE for ForEachWithVar with custom variable name", func() {
|
||||
ports := defkit.List("ports").WithFields(
|
||||
defkit.Int("port").Required(),
|
||||
)
|
||||
comp := defkit.NewComponent("test").
|
||||
Workload("apps/v1", "Deployment").
|
||||
Params(ports).
|
||||
Template(func(tpl *defkit.Template) {
|
||||
containerPorts := defkit.NewArray().ForEachWithVar("p", ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.Set("containerPort", v.Field("port"))
|
||||
})
|
||||
tpl.Output(
|
||||
defkit.NewResource("apps/v1", "Deployment").
|
||||
SetIf(ports.IsSet(), "spec.template.spec.containers[0].ports", containerPorts),
|
||||
)
|
||||
})
|
||||
|
||||
cue := gen.GenerateFullDefinition(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring("for p in parameter.ports"))
|
||||
Expect(cue).To(ContainSubstring("containerPort: p.port"))
|
||||
})
|
||||
|
||||
It("should generate CUE for ForEachWithGuardedFiltered with guard and filter", func() {
|
||||
ports := defkit.List("ports").WithFields(
|
||||
defkit.Int("port").Required(),
|
||||
defkit.Bool("expose").Default(false),
|
||||
)
|
||||
comp := defkit.NewComponent("test").
|
||||
Workload("apps/v1", "Deployment").
|
||||
Params(ports).
|
||||
Template(func(tpl *defkit.Template) {
|
||||
exposePorts := 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("apps/v1", "Deployment").
|
||||
Set("spec.ports", exposePorts),
|
||||
)
|
||||
})
|
||||
|
||||
cue := gen.GenerateFullDefinition(comp)
|
||||
|
||||
// Guard condition
|
||||
Expect(cue).To(ContainSubstring(`parameter["ports"] != _|_`))
|
||||
// Filter predicate
|
||||
Expect(cue).To(ContainSubstring("v.expose == true"))
|
||||
// Field assignment
|
||||
Expect(cue).To(ContainSubstring("port: v.port"))
|
||||
})
|
||||
|
||||
It("should generate CUE for ForEachWith with nested If conditions", func() {
|
||||
ports := defkit.List("ports").WithFields(
|
||||
defkit.Int("port").Required(),
|
||||
defkit.String("protocol"),
|
||||
)
|
||||
exposeType := defkit.String("exposeType")
|
||||
comp := defkit.NewComponent("test").
|
||||
Workload("apps/v1", "Deployment").
|
||||
Params(ports, exposeType).
|
||||
Template(func(tpl *defkit.Template) {
|
||||
result := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.Set("port", v.Field("port"))
|
||||
item.If(defkit.Ne(v.Field("protocol"), defkit.Lit("TCP")), func() {
|
||||
item.Set("protocol", v.Field("protocol"))
|
||||
})
|
||||
})
|
||||
tpl.Output(
|
||||
defkit.NewResource("apps/v1", "Deployment").
|
||||
SetIf(ports.IsSet(), "spec.ports", result),
|
||||
)
|
||||
})
|
||||
|
||||
cue := gen.GenerateFullDefinition(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring("port: v.port"))
|
||||
Expect(cue).To(ContainSubstring(`v.protocol != "TCP"`))
|
||||
Expect(cue).To(ContainSubstring("protocol: v.protocol"))
|
||||
})
|
||||
|
||||
It("should auto-detect strconv import from ForEachWith ItemBuilder ops", func() {
|
||||
ports := defkit.List("ports").WithFields(
|
||||
defkit.Int("port").Required(),
|
||||
)
|
||||
comp := defkit.NewComponent("test").
|
||||
Workload("apps/v1", "Deployment").
|
||||
Params(ports).
|
||||
Template(func(tpl *defkit.Template) {
|
||||
result := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.Let("_name",
|
||||
defkit.Plus(defkit.Lit("port-"), defkit.StrconvFormatInt(v.Field("port"), 10)))
|
||||
})
|
||||
tpl.Output(
|
||||
defkit.NewResource("apps/v1", "Deployment").
|
||||
Set("spec.ports", result),
|
||||
)
|
||||
})
|
||||
|
||||
cue := gen.GenerateFullDefinition(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring(`"strconv"`))
|
||||
})
|
||||
|
||||
It("should auto-detect strings import from ForEachWith ItemBuilder ops", func() {
|
||||
ports := defkit.List("ports").WithFields(
|
||||
defkit.Int("port").Required(),
|
||||
defkit.String("protocol"),
|
||||
)
|
||||
comp := defkit.NewComponent("test").
|
||||
Workload("apps/v1", "Deployment").
|
||||
WithImports().
|
||||
Params(ports).
|
||||
Template(func(tpl *defkit.Template) {
|
||||
result := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.Set("name", defkit.StringsToLower(v.Field("protocol")))
|
||||
})
|
||||
tpl.Output(
|
||||
defkit.NewResource("apps/v1", "Deployment").
|
||||
Set("spec.ports", result),
|
||||
)
|
||||
})
|
||||
|
||||
cue := gen.GenerateFullDefinition(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring(`"strings"`))
|
||||
})
|
||||
|
||||
It("should generate CUE for helper backed by FromArray with ArrayBuilder", func() {
|
||||
ports := defkit.List("ports").WithFields(
|
||||
defkit.Int("port").Required(),
|
||||
defkit.Bool("expose").Default(false),
|
||||
)
|
||||
comp := defkit.NewComponent("test").
|
||||
Workload("apps/v1", "Deployment").
|
||||
Params(ports).
|
||||
Template(func(tpl *defkit.Template) {
|
||||
exposePortsArray := defkit.NewArray().ForEachWithGuardedFiltered(
|
||||
ports.IsSet(),
|
||||
defkit.FieldEquals("expose", true),
|
||||
ports,
|
||||
func(item *defkit.ItemBuilder) {
|
||||
v := item.Var()
|
||||
item.Set("port", v.Field("port"))
|
||||
},
|
||||
)
|
||||
exposePorts := tpl.Helper("exposePorts").
|
||||
FromArray(exposePortsArray).
|
||||
AfterOutput().
|
||||
Build()
|
||||
|
||||
tpl.Output(
|
||||
defkit.NewResource("apps/v1", "Deployment").
|
||||
Set("metadata.name", defkit.VelaCtx().Name()),
|
||||
)
|
||||
tpl.OutputsIf(exposePorts.NotEmpty(), "svc",
|
||||
defkit.NewResource("v1", "Service").
|
||||
Set("spec.ports", exposePorts),
|
||||
)
|
||||
})
|
||||
|
||||
cue := gen.GenerateFullDefinition(comp)
|
||||
|
||||
// Helper should be named exposePorts
|
||||
Expect(cue).To(ContainSubstring("exposePorts:"))
|
||||
// Guard + filter + iteration
|
||||
Expect(cue).To(ContainSubstring(`parameter["ports"] != _|_`))
|
||||
Expect(cue).To(ContainSubstring("v.expose == true"))
|
||||
Expect(cue).To(ContainSubstring("port: v.port"))
|
||||
// Outputs should reference exposePorts
|
||||
Expect(cue).To(ContainSubstring("len(exposePorts) != 0"))
|
||||
})
|
||||
})
|
||||
@@ -90,6 +90,19 @@ func (c *CollectionOp) Map(mappings FieldMap) *CollectionOp {
|
||||
return c
|
||||
}
|
||||
|
||||
// MapVariant adds a conditional field mapping that only applies when the iteration
|
||||
// variable's discriminator field equals the given variant name.
|
||||
// Generates: if v.discriminator == "variantName" { ...mappings... }
|
||||
// Usage: .MapVariant("type", "pvc", FieldMap{"persistentVolumeClaim.claimName": FieldRef("claimName")})
|
||||
func (c *CollectionOp) MapVariant(discriminator, variantName string, mappings FieldMap) *CollectionOp {
|
||||
c.ops = append(c.ops, &mapVariantOp{
|
||||
discriminator: discriminator,
|
||||
variantName: variantName,
|
||||
mappings: mappings,
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
// Pick selects only the specified fields from each item.
|
||||
// Usage: .Pick("name", "mountPath")
|
||||
func (c *CollectionOp) Pick(fields ...string) *CollectionOp {
|
||||
@@ -236,10 +249,36 @@ func (f FieldRef) resolve(item map[string]any) any {
|
||||
}
|
||||
|
||||
// Or provides a fallback if the field is nil or empty.
|
||||
// Generates CUE like: *v.field | fallback
|
||||
func (f FieldRef) Or(fallback FieldValue) *OrFieldRef {
|
||||
return &OrFieldRef{primary: f, fallback: fallback}
|
||||
}
|
||||
|
||||
// OrConditional provides a fallback using if/else blocks instead of default syntax.
|
||||
// Generates CUE like:
|
||||
//
|
||||
// if v.field != _|_ { name: v.field }
|
||||
// if v.field == _|_ { name: fallbackExpr }
|
||||
func (f FieldRef) OrConditional(fallback FieldValue) *ConditionalOrFieldRef {
|
||||
return &ConditionalOrFieldRef{primary: f, fallback: fallback}
|
||||
}
|
||||
|
||||
// ConditionalOrFieldRef represents a field reference with a conditional fallback.
|
||||
// Instead of generating CUE default syntax (*v.field | fallback), it generates
|
||||
// two if/else blocks for the field.
|
||||
type ConditionalOrFieldRef struct {
|
||||
primary FieldRef
|
||||
fallback FieldValue
|
||||
}
|
||||
|
||||
func (c *ConditionalOrFieldRef) resolve(item map[string]any) any {
|
||||
val := item[string(c.primary)]
|
||||
if val == nil || val == "" {
|
||||
return c.fallback.resolve(item)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// OrFieldRef represents a field reference with a fallback value.
|
||||
type OrFieldRef struct {
|
||||
primary FieldRef
|
||||
@@ -368,7 +407,18 @@ func (m *mapOp) apply(items []any) []any {
|
||||
if itemMap, ok := item.(map[string]any); ok {
|
||||
newItem := make(map[string]any)
|
||||
for newKey, fieldVal := range m.mappings {
|
||||
newItem[newKey] = fieldVal.resolve(itemMap)
|
||||
resolved := fieldVal.resolve(itemMap)
|
||||
// Skip nil values from optional field types — they should be
|
||||
// omitted from the output entirely, not included as nil.
|
||||
if resolved == nil {
|
||||
if _, isOpt := fieldVal.(*OptionalField); isOpt {
|
||||
continue
|
||||
}
|
||||
if _, isCompOpt := fieldVal.(*CompoundOptionalField); isCompOpt {
|
||||
continue
|
||||
}
|
||||
}
|
||||
newItem[newKey] = resolved
|
||||
}
|
||||
result = append(result, newItem)
|
||||
}
|
||||
@@ -376,6 +426,47 @@ func (m *mapOp) apply(items []any) []any {
|
||||
return result
|
||||
}
|
||||
|
||||
// mapVariantOp represents a conditional map operation based on a discriminator field value.
|
||||
// When the iteration variable's discriminator field equals the variant name,
|
||||
// the variant's field mappings are included.
|
||||
// Generates: if v.discriminator == "variantName" { ...mappings... }
|
||||
type mapVariantOp struct {
|
||||
discriminator string
|
||||
variantName string
|
||||
mappings FieldMap
|
||||
}
|
||||
|
||||
func (m *mapVariantOp) apply(items []any) []any {
|
||||
// Runtime apply: merge variant mappings into matching items, pass others through.
|
||||
// This mirrors CUE semantics where each variant condition is evaluated for every
|
||||
// item in the loop body — non-matching items are kept so later MapVariant ops
|
||||
// can process them.
|
||||
result := make([]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
itemMap, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
result = append(result, item)
|
||||
continue
|
||||
}
|
||||
disc, exists := itemMap[m.discriminator]
|
||||
if !exists || fmt.Sprintf("%v", disc) != m.variantName {
|
||||
// Non-matching: pass through unchanged
|
||||
result = append(result, item)
|
||||
continue
|
||||
}
|
||||
// Matching: copy existing fields and merge variant mappings
|
||||
newItem := make(map[string]any, len(itemMap)+len(m.mappings))
|
||||
for k, v := range itemMap {
|
||||
newItem[k] = v
|
||||
}
|
||||
for newKey, fieldVal := range m.mappings {
|
||||
newItem[newKey] = fieldVal.resolve(itemMap)
|
||||
}
|
||||
result = append(result, newItem)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type pickOp struct {
|
||||
fields []string
|
||||
}
|
||||
@@ -698,6 +789,28 @@ func OptionalFieldRef(field string) *OptionalField {
|
||||
return &OptionalField{field: field}
|
||||
}
|
||||
|
||||
// CompoundOptionalField includes a field value only if the field exists AND an additional condition is met.
|
||||
// Generates CUE: if v.field != _|_ if additionalCond { fieldName: v.field }
|
||||
type CompoundOptionalField struct {
|
||||
field string
|
||||
additionalCond Condition
|
||||
}
|
||||
|
||||
func (o *CompoundOptionalField) resolve(item map[string]any) any {
|
||||
val, exists := item[o.field]
|
||||
if !exists || val == nil {
|
||||
return nil
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// OptionalFieldWithCond creates a field reference that includes the field only when
|
||||
// both the field exists and the additional condition is satisfied.
|
||||
// Generates CUE: if v.field != _|_ if cond { fieldName: v.field }
|
||||
func OptionalFieldWithCond(field string, cond Condition) *CompoundOptionalField {
|
||||
return &CompoundOptionalField{field: field, additionalCond: cond}
|
||||
}
|
||||
|
||||
// NestedFieldMap creates a nested object from field mappings.
|
||||
// This is an alias for Nested, providing a clearer name for struct array helpers.
|
||||
// Usage: defkit.NestedFieldMap(defkit.FieldMap{"claimName": defkit.FieldRef("claimName")})
|
||||
|
||||
@@ -81,6 +81,19 @@ var _ = Describe("Collections", func() {
|
||||
Expect(col.Operations()).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should chain MapVariant operation", func() {
|
||||
volumes := defkit.List("volumes")
|
||||
col := 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"),
|
||||
})
|
||||
Expect(col.Operations()).To(HaveLen(3))
|
||||
})
|
||||
|
||||
It("should chain Rename operation", func() {
|
||||
ports := defkit.List("ports")
|
||||
col := defkit.Each(ports).Rename("port", "containerPort")
|
||||
@@ -101,42 +114,85 @@ var _ = Describe("Collections", func() {
|
||||
})
|
||||
|
||||
Context("FieldRef", func() {
|
||||
It("should resolve field from item", func() {
|
||||
It("should store field name and resolve correctly from item", func() {
|
||||
ref := defkit.FieldRef("port")
|
||||
Expect(ref).NotTo(BeNil())
|
||||
Expect(string(ref)).To(Equal("port"))
|
||||
|
||||
// Verify it resolves correctly through a collection
|
||||
ports := defkit.List("ports")
|
||||
col := defkit.Each(ports).Map(defkit.FieldMap{"p": ref})
|
||||
items := []any{map[string]any{"port": 8080}}
|
||||
results := col.Collect(items)
|
||||
Expect(results).To(HaveLen(1))
|
||||
Expect(results[0]["p"]).To(Equal(8080))
|
||||
})
|
||||
|
||||
It("should support Or fallback", func() {
|
||||
ref := defkit.FieldRef("name").Or(defkit.Format("port-%v", defkit.FieldRef("port")))
|
||||
Expect(ref).NotTo(BeNil())
|
||||
It("should support Or fallback and use fallback when primary is nil", func() {
|
||||
ports := defkit.List("ports")
|
||||
col := defkit.Each(ports).Map(defkit.FieldMap{
|
||||
"display": defkit.FieldRef("name").Or(defkit.Format("port-%v", defkit.FieldRef("port"))),
|
||||
})
|
||||
// Primary field present — should use primary
|
||||
items := []any{map[string]any{"port": 80, "name": "http"}}
|
||||
results := col.Collect(items)
|
||||
Expect(results[0]["display"]).To(Equal("http"))
|
||||
|
||||
// Primary field missing — should use fallback
|
||||
items = []any{map[string]any{"port": 443}}
|
||||
results = col.Collect(items)
|
||||
Expect(results[0]["display"]).To(Equal("port-443"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("FieldEquals", func() {
|
||||
It("should create equality predicate", func() {
|
||||
It("should filter items matching the field equality predicate", func() {
|
||||
pred := defkit.FieldEquals("expose", true)
|
||||
Expect(pred).NotTo(BeNil())
|
||||
ports := defkit.List("ports")
|
||||
col := defkit.Each(ports).Filter(pred)
|
||||
items := []any{
|
||||
map[string]any{"port": 80, "expose": true},
|
||||
map[string]any{"port": 443, "expose": false},
|
||||
}
|
||||
results := col.Collect(items)
|
||||
Expect(results).To(HaveLen(1))
|
||||
Expect(results[0]["port"]).To(Equal(80))
|
||||
})
|
||||
})
|
||||
|
||||
Context("FieldExists", func() {
|
||||
It("should create existence predicate", func() {
|
||||
It("should filter items where field is present", func() {
|
||||
pred := defkit.FieldExists("items")
|
||||
Expect(pred).NotTo(BeNil())
|
||||
ports := defkit.List("data")
|
||||
col := defkit.Each(ports).Filter(pred)
|
||||
items := []any{
|
||||
map[string]any{"name": "a", "items": []string{"x"}},
|
||||
map[string]any{"name": "b"},
|
||||
}
|
||||
results := col.Collect(items)
|
||||
Expect(results).To(HaveLen(1))
|
||||
Expect(results[0]["name"]).To(Equal("a"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Format", func() {
|
||||
It("should create format field value", func() {
|
||||
It("should create format field value with required imports", func() {
|
||||
f := defkit.Format("port-%v", defkit.FieldRef("port"))
|
||||
Expect(f).NotTo(BeNil())
|
||||
Expect(f.RequiredImports()).To(ContainElement("strconv"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("LitField", func() {
|
||||
It("should create literal field value", func() {
|
||||
It("should resolve literal field value from item", func() {
|
||||
lit := defkit.LitField("TCP")
|
||||
Expect(lit).NotTo(BeNil())
|
||||
// LitField should resolve to the literal value regardless of item content
|
||||
ports := defkit.List("ports")
|
||||
col := defkit.Each(ports).Map(defkit.FieldMap{"protocol": lit})
|
||||
items := []any{map[string]any{"port": 80}}
|
||||
results := col.Collect(items)
|
||||
Expect(results).To(HaveLen(1))
|
||||
Expect(results[0]["protocol"]).To(Equal("TCP"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -181,32 +237,98 @@ var _ = Describe("Collections", func() {
|
||||
})
|
||||
|
||||
Context("Nested", func() {
|
||||
It("should create nested field mapping", func() {
|
||||
It("should resolve nested field mapping from item", func() {
|
||||
nested := defkit.Nested(defkit.FieldMap{
|
||||
"claimName": defkit.FieldRef("claimName"),
|
||||
})
|
||||
Expect(nested).NotTo(BeNil())
|
||||
// Verify nested resolves correctly through a collection
|
||||
vols := defkit.List("volumes")
|
||||
col := defkit.Each(vols).Map(defkit.FieldMap{
|
||||
"pvc": nested,
|
||||
})
|
||||
items := []any{map[string]any{"claimName": "my-pvc"}}
|
||||
results := col.Collect(items)
|
||||
Expect(results).To(HaveLen(1))
|
||||
Expect(results[0]["pvc"]).To(Equal(map[string]any{"claimName": "my-pvc"}))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Optional and OptionalFieldRef", func() {
|
||||
It("should create optional field reference", func() {
|
||||
opt := defkit.Optional("items")
|
||||
Expect(opt).NotTo(BeNil())
|
||||
It("should include optional field when present and omit when absent", func() {
|
||||
vols := defkit.List("volumes")
|
||||
col := defkit.Each(vols).Map(defkit.FieldMap{
|
||||
"name": defkit.FieldRef("name"),
|
||||
"items": defkit.Optional("items"),
|
||||
})
|
||||
items := []any{
|
||||
map[string]any{"name": "vol1", "items": []string{"a"}},
|
||||
map[string]any{"name": "vol2"},
|
||||
}
|
||||
results := col.Collect(items)
|
||||
Expect(results).To(HaveLen(2))
|
||||
Expect(results[0]).To(HaveKey("items"))
|
||||
Expect(results[1]).NotTo(HaveKey("items"))
|
||||
})
|
||||
|
||||
It("should create optional field reference via OptionalFieldRef alias", func() {
|
||||
opt := defkit.OptionalFieldRef("subPath")
|
||||
Expect(opt).NotTo(BeNil())
|
||||
vols := defkit.List("volumes")
|
||||
col := defkit.Each(vols).Map(defkit.FieldMap{
|
||||
"name": defkit.FieldRef("name"),
|
||||
"subPath": defkit.OptionalFieldRef("subPath"),
|
||||
})
|
||||
items := []any{
|
||||
map[string]any{"name": "vol1", "subPath": "/data"},
|
||||
map[string]any{"name": "vol2"},
|
||||
}
|
||||
results := col.Collect(items)
|
||||
Expect(results).To(HaveLen(2))
|
||||
Expect(results[0]["subPath"]).To(Equal("/data"))
|
||||
Expect(results[1]).NotTo(HaveKey("subPath"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("CompoundOptionalField", func() {
|
||||
It("should create compound optional field with OptionalFieldWithCond", func() {
|
||||
cond := defkit.Eq(defkit.String("exposeType"), defkit.Lit("NodePort"))
|
||||
compOpt := defkit.OptionalFieldWithCond("nodePort", cond)
|
||||
Expect(compOpt).NotTo(BeNil())
|
||||
// Verify it can be used in a FieldMap and the CUE generation picks it up
|
||||
ports := defkit.List("ports").WithFields(
|
||||
defkit.Int("port").Required(),
|
||||
defkit.Int("nodePort"),
|
||||
)
|
||||
col := defkit.Each(ports).Map(defkit.FieldMap{
|
||||
"port": defkit.FieldRef("port"),
|
||||
"nodePort": compOpt,
|
||||
})
|
||||
Expect(col.Operations()).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should be usable as a FieldValue in FieldMap alongside regular fields", func() {
|
||||
cond := defkit.Eq(defkit.String("exposeType"), defkit.Lit("NodePort"))
|
||||
fm := defkit.FieldMap{
|
||||
"port": defkit.FieldRef("port"),
|
||||
"nodePort": defkit.OptionalFieldWithCond("nodePort", cond),
|
||||
}
|
||||
Expect(fm).To(HaveLen(2))
|
||||
Expect(fm).To(HaveKey("port"))
|
||||
Expect(fm).To(HaveKey("nodePort"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("NestedFieldMap", func() {
|
||||
It("should create nested field mapping (alias for Nested)", func() {
|
||||
It("should resolve nested field mapping identically to Nested", func() {
|
||||
nested := defkit.NestedFieldMap(defkit.FieldMap{
|
||||
"claimName": defkit.FieldRef("claimName"),
|
||||
})
|
||||
Expect(nested).NotTo(BeNil())
|
||||
vols := defkit.List("volumes")
|
||||
col := defkit.Each(vols).Map(defkit.FieldMap{"pvc": nested})
|
||||
items := []any{map[string]any{"claimName": "my-pvc"}}
|
||||
results := col.Collect(items)
|
||||
Expect(results).To(HaveLen(1))
|
||||
Expect(results[0]["pvc"]).To(Equal(map[string]any{"claimName": "my-pvc"}))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -817,6 +939,65 @@ var _ = Describe("Collections", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Context("MapVariant chained operation behavior", func() {
|
||||
It("should apply chained variant mappings to matching items and pass others through", func() {
|
||||
volumes := defkit.List("volumes")
|
||||
col := defkit.Each(volumes).
|
||||
MapVariant("type", "pvc", defkit.FieldMap{
|
||||
"pvcClaim": defkit.FieldRef("claimName"),
|
||||
}).
|
||||
MapVariant("type", "configMap", defkit.FieldMap{
|
||||
"cmRef": defkit.FieldRef("cmName"),
|
||||
}).
|
||||
MapVariant("type", "emptyDir", defkit.FieldMap{
|
||||
"medium": defkit.FieldRef("medium"),
|
||||
})
|
||||
|
||||
items := []any{
|
||||
map[string]any{"name": "data", "type": "pvc", "claimName": "data-pvc"},
|
||||
map[string]any{"name": "config", "type": "configMap", "cmName": "app-config"},
|
||||
map[string]any{"name": "cache", "type": "emptyDir", "medium": "Memory"},
|
||||
}
|
||||
|
||||
results := col.Collect(items)
|
||||
Expect(results).To(HaveLen(3))
|
||||
|
||||
// PVC item should have variant field merged, original fields preserved
|
||||
Expect(results[0]["name"]).To(Equal("data"))
|
||||
Expect(results[0]["pvcClaim"]).To(Equal("data-pvc"))
|
||||
Expect(results[0]).NotTo(HaveKey("cmRef"))
|
||||
|
||||
// ConfigMap item should have variant field merged
|
||||
Expect(results[1]["name"]).To(Equal("config"))
|
||||
Expect(results[1]["cmRef"]).To(Equal("app-config"))
|
||||
Expect(results[1]).NotTo(HaveKey("pvcClaim"))
|
||||
|
||||
// EmptyDir item should have variant field merged
|
||||
Expect(results[2]["name"]).To(Equal("cache"))
|
||||
Expect(results[2]["medium"]).To(Equal("Memory"))
|
||||
})
|
||||
|
||||
It("should preserve items with no matching variant", func() {
|
||||
volumes := defkit.List("volumes")
|
||||
col := defkit.Each(volumes).
|
||||
MapVariant("type", "pvc", defkit.FieldMap{
|
||||
"pvcClaim": defkit.FieldRef("claimName"),
|
||||
})
|
||||
|
||||
items := []any{
|
||||
map[string]any{"name": "data", "type": "pvc", "claimName": "data-pvc"},
|
||||
map[string]any{"name": "config", "type": "configMap", "cmName": "app-config"},
|
||||
}
|
||||
|
||||
results := col.Collect(items)
|
||||
Expect(results).To(HaveLen(2))
|
||||
Expect(results[0]["pvcClaim"]).To(Equal("data-pvc"))
|
||||
// Non-matching item passes through unchanged
|
||||
Expect(results[1]["name"]).To(Equal("config"))
|
||||
Expect(results[1]["type"]).To(Equal("configMap"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Flatten operation behavior", func() {
|
||||
It("should flatten nested arrays", func() {
|
||||
volumes := defkit.List("volumes")
|
||||
@@ -877,6 +1058,51 @@ var _ = Describe("Collections", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Context("OrConditional (ConditionalOrFieldRef)", func() {
|
||||
It("should create ConditionalOrFieldRef that resolves primary field", func() {
|
||||
ref := defkit.FieldRef("name").OrConditional(defkit.Format("port-%v", defkit.FieldRef("port")))
|
||||
col := defkit.Each(defkit.List("items")).Map(defkit.FieldMap{"result": ref})
|
||||
results := col.Collect([]any{map[string]any{"name": "web", "port": 80}})
|
||||
Expect(results).To(HaveLen(1))
|
||||
Expect(results[0]["result"]).To(Equal("web"))
|
||||
})
|
||||
|
||||
It("should resolve primary vs fallback across multiple items with full structure", func() {
|
||||
ports := defkit.List("ports")
|
||||
col := defkit.Each(ports).Map(defkit.FieldMap{
|
||||
"displayName": defkit.FieldRef("name").OrConditional(defkit.Format("port-%v", defkit.FieldRef("port"))),
|
||||
"portNumber": defkit.FieldRef("port"),
|
||||
"protocol": defkit.FieldRef("proto").OrConditional(defkit.LitField("TCP")),
|
||||
})
|
||||
|
||||
items := []any{
|
||||
map[string]any{"port": 80, "name": "http", "proto": "HTTP"}, // all primary fields present
|
||||
map[string]any{"port": 443}, // name and proto nil → fallback
|
||||
map[string]any{"port": 8080, "name": "", "proto": ""}, // name and proto empty → fallback
|
||||
map[string]any{"port": 9090, "name": "grpc"}, // name present, proto nil → mixed
|
||||
}
|
||||
|
||||
results := col.Collect(items)
|
||||
Expect(results).To(HaveLen(4))
|
||||
|
||||
Expect(results[0]["displayName"]).To(Equal("http"))
|
||||
Expect(results[0]["portNumber"]).To(Equal(80))
|
||||
Expect(results[0]["protocol"]).To(Equal("HTTP"))
|
||||
|
||||
Expect(results[1]["displayName"]).To(Equal("port-443"))
|
||||
Expect(results[1]["portNumber"]).To(Equal(443))
|
||||
Expect(results[1]["protocol"]).To(Equal("TCP"))
|
||||
|
||||
Expect(results[2]["displayName"]).To(Equal("port-8080"))
|
||||
Expect(results[2]["portNumber"]).To(Equal(8080))
|
||||
Expect(results[2]["protocol"]).To(Equal("TCP"))
|
||||
|
||||
Expect(results[3]["displayName"]).To(Equal("grpc"))
|
||||
Expect(results[3]["portNumber"]).To(Equal(9090))
|
||||
Expect(results[3]["protocol"]).To(Equal("TCP"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("FormatField RequiredImports", func() {
|
||||
It("should require strconv for numeric formatting", func() {
|
||||
f := defkit.Format("port-%v", defkit.FieldRef("port"))
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
type ComponentDefinition struct {
|
||||
baseDefinition // embedded common fields (name, description, params, template, etc.)
|
||||
workload WorkloadType
|
||||
labels map[string]string // metadata labels for the component definition
|
||||
}
|
||||
|
||||
// WorkloadType represents the workload type for a component.
|
||||
@@ -140,6 +141,16 @@ func (c *ComponentDefinition) Helper(name string, param Param) *ComponentDefinit
|
||||
return c
|
||||
}
|
||||
|
||||
// Labels sets metadata labels on the component definition.
|
||||
// Usage: component.Labels(map[string]string{"ui-hidden": "true"})
|
||||
func (c *ComponentDefinition) Labels(labels map[string]string) *ComponentDefinition {
|
||||
c.labels = labels
|
||||
return c
|
||||
}
|
||||
|
||||
// GetLabels returns the component's metadata labels.
|
||||
func (c *ComponentDefinition) GetLabels() map[string]string { return c.labels }
|
||||
|
||||
// RawCUE sets raw CUE for complex component definitions that don't fit the builder pattern.
|
||||
// When set, this bypasses all other template settings and outputs the raw CUE directly.
|
||||
func (c *ComponentDefinition) RawCUE(cue string) *ComponentDefinition {
|
||||
|
||||
+497
-64
@@ -18,9 +18,13 @@ package defkit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// cueOpenStruct is the CUE literal for an open struct type.
|
||||
const cueOpenStruct = "{...}"
|
||||
|
||||
// cueLabel quotes a CUE field label if it contains characters that are not
|
||||
// valid in a CUE identifier (letters, digits, underscore, $).
|
||||
func cueLabel(name string) string {
|
||||
@@ -172,6 +176,16 @@ func (g *CUEGenerator) collectImportsFromValue(v interface{}) {
|
||||
g.collectImportsFromFieldValue(fv)
|
||||
}
|
||||
}
|
||||
case *ArrayBuilder:
|
||||
for _, entry := range val.Entries() {
|
||||
if entry.itemBuilder != nil {
|
||||
g.collectImportsFromItemOps(entry.itemBuilder.Ops())
|
||||
}
|
||||
}
|
||||
case *PlusExpr:
|
||||
for _, part := range val.Parts() {
|
||||
g.collectImportsFromValue(part)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +206,8 @@ func (g *CUEGenerator) collectImportsFromFieldValue(fv FieldValue) {
|
||||
switch val := fv.(type) {
|
||||
case *OrFieldRef:
|
||||
g.collectImportsFromFieldValue(val.fallback)
|
||||
case *ConditionalOrFieldRef:
|
||||
g.collectImportsFromFieldValue(val.fallback)
|
||||
case *NestedField:
|
||||
for _, nested := range val.mapping {
|
||||
g.collectImportsFromFieldValue(nested)
|
||||
@@ -199,6 +215,22 @@ func (g *CUEGenerator) collectImportsFromFieldValue(fv FieldValue) {
|
||||
}
|
||||
}
|
||||
|
||||
// collectImportsFromItemOps recursively scans ItemBuilder operations for import requirements.
|
||||
func (g *CUEGenerator) collectImportsFromItemOps(ops []itemOp) {
|
||||
for _, op := range ops {
|
||||
switch o := op.(type) {
|
||||
case setOp:
|
||||
g.collectImportsFromValue(o.value)
|
||||
case letOp:
|
||||
g.collectImportsFromValue(o.value)
|
||||
case setDefaultOp:
|
||||
g.collectImportsFromValue(o.defValue)
|
||||
case ifBlockOp:
|
||||
g.collectImportsFromItemOps(o.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collectImportsFromResource checks all operations in a resource for import requirements.
|
||||
func (g *CUEGenerator) collectImportsFromResource(res *Resource) {
|
||||
if res == nil {
|
||||
@@ -260,7 +292,15 @@ func (g *CUEGenerator) GenerateFullDefinition(c *ComponentDefinition) string {
|
||||
sb.WriteString(fmt.Sprintf("%s: {\n", cueLabel(c.GetName())))
|
||||
sb.WriteString(fmt.Sprintf("%stype: \"component\"\n", g.indent))
|
||||
sb.WriteString(fmt.Sprintf("%sannotations: {}\n", g.indent))
|
||||
sb.WriteString(fmt.Sprintf("%slabels: {}\n", g.indent))
|
||||
if len(c.GetLabels()) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
|
||||
for k, v := range c.GetLabels() {
|
||||
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, v))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%slabels: {}\n", g.indent))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%sdescription: %q\n", g.indent, c.GetDescription()))
|
||||
|
||||
// Write attributes
|
||||
@@ -674,6 +714,8 @@ func (g *CUEGenerator) writeHelper(sb *strings.Builder, helper *HelperVar, depth
|
||||
g.writeMultiSourceHelper(sb, col, depth)
|
||||
case *CollectionOp:
|
||||
g.writeCollectionOpHelper(sb, col, depth, guard)
|
||||
case *ArrayBuilder:
|
||||
sb.WriteString(g.arrayBuilderToCUE(col, depth))
|
||||
default:
|
||||
sb.WriteString("[]")
|
||||
}
|
||||
@@ -787,8 +829,29 @@ func (g *CUEGenerator) writeCollectionOpHelper(sb *strings.Builder, col *Collect
|
||||
}
|
||||
|
||||
for fieldName, fieldVal := range mOp.mappings {
|
||||
valStr := g.fieldValueToCUE(fieldVal)
|
||||
sb.WriteString(fmt.Sprintf("%s%s: %s\n", fieldIndent, fieldName, valStr))
|
||||
if condRef, isConditional := fieldVal.(*ConditionalOrFieldRef); isConditional {
|
||||
// Emit if/else pattern for conditional field reference
|
||||
primaryField := string(condRef.primary)
|
||||
fallbackStr := g.fieldValueToCUE(condRef.fallback)
|
||||
sb.WriteString(fmt.Sprintf("%sif v.%s != _|_ {\n", fieldIndent, primaryField))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: v.%s\n", fieldIndent, fieldName, primaryField))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", fieldIndent))
|
||||
sb.WriteString(fmt.Sprintf("%sif v.%s == _|_ {\n", fieldIndent, primaryField))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", fieldIndent, fieldName, fallbackStr))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", fieldIndent))
|
||||
} else if optField, isOptional := fieldVal.(*OptionalField); isOptional {
|
||||
sb.WriteString(fmt.Sprintf("%sif v.%s != _|_ {\n", fieldIndent, optField.field))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: v.%s\n", fieldIndent, fieldName, optField.field))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", fieldIndent))
|
||||
} else if compOpt, isCompound := fieldVal.(*CompoundOptionalField); isCompound {
|
||||
condStr := g.conditionToCUE(compOpt.additionalCond)
|
||||
sb.WriteString(fmt.Sprintf("%sif v.%s != _|_ if %s {\n", fieldIndent, compOpt.field, condStr))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: v.%s\n", fieldIndent, fieldName, compOpt.field))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", fieldIndent))
|
||||
} else {
|
||||
valStr := g.fieldValueToCUE(fieldVal)
|
||||
sb.WriteString(fmt.Sprintf("%s%s: %s\n", fieldIndent, fieldName, valStr))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s},\n%s]", innerIndent, strings.Repeat(g.indent, depth)))
|
||||
@@ -837,6 +900,11 @@ func (g *CUEGenerator) writeFieldMapAsHelper(sb *strings.Builder, mapping FieldM
|
||||
sb.WriteString(fmt.Sprintf("%sif v.%s != _|_ {\n", indent, fieldVal.(*OptionalField).field))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: v.%s\n", indent, fieldName, fieldVal.(*OptionalField).field))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
} else if compOpt, isCompound := fieldVal.(*CompoundOptionalField); isCompound {
|
||||
condStr := g.conditionToCUE(compOpt.additionalCond)
|
||||
sb.WriteString(fmt.Sprintf("%sif v.%s != _|_ if %s {\n", indent, compOpt.field, condStr))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: v.%s\n", indent, fieldName, compOpt.field))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, fieldName, valStr))
|
||||
}
|
||||
@@ -919,6 +987,15 @@ func (g *CUEGenerator) writeResourceOutput(sb *strings.Builder, name string, res
|
||||
}
|
||||
}
|
||||
|
||||
// condValueEntry represents an additional conditional value at a field node.
|
||||
// When the same path is written with multiple different conditions (e.g.,
|
||||
// volumeMounts set under both "volumeMounts is set" and "volumes is set"),
|
||||
// the first write uses value/cond and additional writes append here.
|
||||
type condValueEntry struct {
|
||||
value Value
|
||||
cond Condition
|
||||
}
|
||||
|
||||
// fieldNode represents a node in the field tree being built.
|
||||
type fieldNode struct {
|
||||
value Value // Direct value (if leaf)
|
||||
@@ -927,11 +1004,13 @@ type fieldNode struct {
|
||||
childOrder []string // Track insertion order
|
||||
isArray bool
|
||||
arrayIndex int
|
||||
spreads []spreadEntry // Spread operations at this node level
|
||||
forEach *ForEachOp // ForEach operation (for trait patches)
|
||||
patchKey *PatchKeyOp // PatchKey operation (for array patches with merge key)
|
||||
spreadAll *SpreadAllOp // SpreadAll operation (for array constraint patches)
|
||||
patchStrategy string // e.g. "retainKeys" → generates // +patchStrategy=retainKeys
|
||||
spreads []spreadEntry // Spread operations at this node level
|
||||
forEach *ForEachOp // ForEach operation (for trait patches)
|
||||
patchKey *PatchKeyOp // PatchKey operation (for array patches with merge key)
|
||||
spreadAll *SpreadAllOp // SpreadAll operation (for array constraint patches)
|
||||
patchStrategy string // e.g. "retainKeys" → generates // +patchStrategy=retainKeys
|
||||
directives []string // e.g. ["patchKey=ip"] → generates // +patchKey=ip
|
||||
condValues []condValueEntry // additional conditional values at same path
|
||||
}
|
||||
|
||||
// spreadEntry represents a conditional spread operation.
|
||||
@@ -970,6 +1049,8 @@ func (g *CUEGenerator) buildFieldTree(ops []ResourceOp) *fieldNode {
|
||||
g.insertSpreadAllIntoTree(root, o, nil)
|
||||
case *PatchStrategyAnnotationOp:
|
||||
g.insertAnnotationIntoTree(root, o.Path(), o.Strategy())
|
||||
case *DirectiveOp:
|
||||
g.insertDirectiveIntoTree(root, o.Path(), o.GetDirective())
|
||||
case *IfBlock:
|
||||
// For if blocks, process inner ops with the block's condition
|
||||
for _, innerOp := range o.Ops() {
|
||||
@@ -995,6 +1076,8 @@ func (g *CUEGenerator) buildFieldTree(ops []ResourceOp) *fieldNode {
|
||||
g.insertSpreadAllIntoTree(root, inner, o.Cond())
|
||||
case *PatchStrategyAnnotationOp:
|
||||
g.insertAnnotationIntoTree(root, inner.Path(), inner.Strategy())
|
||||
case *DirectiveOp:
|
||||
g.insertDirectiveIntoTree(root, inner.Path(), inner.GetDirective())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1003,6 +1086,20 @@ func (g *CUEGenerator) buildFieldTree(ops []ResourceOp) *fieldNode {
|
||||
return root
|
||||
}
|
||||
|
||||
// insertDirectiveIntoTree navigates to a node by path and adds a directive annotation.
|
||||
func (g *CUEGenerator) insertDirectiveIntoTree(root *fieldNode, path string, directive string) {
|
||||
parts := splitPath(path)
|
||||
current := root
|
||||
for _, part := range parts {
|
||||
if _, exists := current.children[part]; !exists {
|
||||
current.children[part] = newFieldNode()
|
||||
current.childOrder = append(current.childOrder, part)
|
||||
}
|
||||
current = current.children[part]
|
||||
}
|
||||
current.directives = append(current.directives, directive)
|
||||
}
|
||||
|
||||
// insertAnnotationIntoTree navigates to a node by path and sets its patchStrategy annotation.
|
||||
func (g *CUEGenerator) insertAnnotationIntoTree(root *fieldNode, path string, strategy string) {
|
||||
parts := splitPath(path)
|
||||
@@ -1058,8 +1155,25 @@ func (g *CUEGenerator) insertIntoTree(root *fieldNode, path string, value Value,
|
||||
|
||||
// If this is the last part, set the value
|
||||
if i == len(parts)-1 {
|
||||
current.value = value
|
||||
current.cond = cond
|
||||
if current.value != nil && cond != nil {
|
||||
// This path already has a value. If the existing value also
|
||||
// has a condition, append this as an additional conditional
|
||||
// value instead of overwriting.
|
||||
if current.cond != nil {
|
||||
current.condValues = append(current.condValues, condValueEntry{
|
||||
value: value,
|
||||
cond: cond,
|
||||
})
|
||||
} else {
|
||||
// Existing value is unconditional; the new conditional
|
||||
// value takes precedence (shouldn't normally happen).
|
||||
current.value = value
|
||||
current.cond = cond
|
||||
}
|
||||
} else {
|
||||
current.value = value
|
||||
current.cond = cond
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1267,10 +1381,15 @@ func (g *CUEGenerator) writeFieldTree(sb *strings.Builder, node *fieldNode, dept
|
||||
|
||||
for _, name := range node.childOrder {
|
||||
child := node.children[name]
|
||||
if child.cond != nil {
|
||||
switch {
|
||||
case len(child.condValues) > 0:
|
||||
// Multi-conditional nodes render their own if blocks internally,
|
||||
// so they must be treated as unconditional at the parent level.
|
||||
unconditional = append(unconditional, name)
|
||||
case child.cond != nil:
|
||||
condStr := g.conditionToCUE(child.cond)
|
||||
conditional[condStr] = append(conditional[condStr], name)
|
||||
} else {
|
||||
default:
|
||||
unconditional = append(unconditional, name)
|
||||
}
|
||||
}
|
||||
@@ -1325,6 +1444,12 @@ func (g *CUEGenerator) liftChildConditions(node *fieldNode) {
|
||||
canLift = false
|
||||
break
|
||||
}
|
||||
// Don't lift if any grandchild has multiple conditional values,
|
||||
// since those nodes manage their own condition rendering.
|
||||
if len(grand.condValues) > 0 {
|
||||
canLift = false
|
||||
break
|
||||
}
|
||||
condStr := g.conditionToCUE(grand.cond)
|
||||
if sharedCondStr == "" {
|
||||
sharedCondStr = condStr
|
||||
@@ -1346,6 +1471,78 @@ func (g *CUEGenerator) liftChildConditions(node *fieldNode) {
|
||||
}
|
||||
}
|
||||
|
||||
// tryDecomposeOrLift attempts to decompose a struct node into per-condition
|
||||
// blocks or lift a shared child condition to the parent. Returns true if the
|
||||
// node was handled, false if normal rendering should proceed.
|
||||
func (g *CUEGenerator) tryDecomposeOrLift(sb *strings.Builder, name string, node *fieldNode, indent string, depth int) bool {
|
||||
if node.value != nil || len(node.children) == 0 || len(node.spreads) > 0 || node.forEach != nil || node.patchKey != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Decompose a struct node into per-condition blocks when every child
|
||||
// subtree shares the same uniform set of leaf conditions.
|
||||
condGroups := g.canDecomposeByCondition(node)
|
||||
if condGroups != nil {
|
||||
condStrs := make([]string, 0, len(condGroups))
|
||||
for cs := range condGroups {
|
||||
condStrs = append(condStrs, cs)
|
||||
}
|
||||
sort.Strings(condStrs)
|
||||
|
||||
for _, condStr := range condStrs {
|
||||
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: {\n", indent, name))
|
||||
for _, childName := range node.childOrder {
|
||||
child := node.children[childName]
|
||||
filteredChild := g.filterNodeByCondition(child, condStr)
|
||||
if filteredChild != nil {
|
||||
g.writeFieldNode(sb, childName, filteredChild, depth+2)
|
||||
}
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s\t}\n", indent))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// If all children share the same condition, lift it to avoid empty parent structs.
|
||||
condStr := ""
|
||||
canLift := true
|
||||
for _, childName := range node.childOrder {
|
||||
child := node.children[childName]
|
||||
if child.cond == nil {
|
||||
canLift = false
|
||||
break
|
||||
}
|
||||
childCondStr := g.conditionToCUE(child.cond)
|
||||
if condStr == "" {
|
||||
condStr = childCondStr
|
||||
} else if condStr != childCondStr {
|
||||
canLift = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if canLift && condStr != "" {
|
||||
clone := &fieldNode{
|
||||
children: make(map[string]*fieldNode, len(node.children)),
|
||||
childOrder: append([]string(nil), node.childOrder...),
|
||||
}
|
||||
for childName, child := range node.children {
|
||||
childCopy := *child
|
||||
childCopy.cond = nil
|
||||
clone.children[childName] = &childCopy
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: {\n", indent, name))
|
||||
g.writeFieldTree(sb, clone, depth+2)
|
||||
sb.WriteString(fmt.Sprintf("%s\t}\n", indent))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// writeFieldNode writes a single field node as CUE.
|
||||
func (g *CUEGenerator) writeFieldNode(sb *strings.Builder, name string, node *fieldNode, depth int) {
|
||||
indent := strings.Repeat(g.indent, depth)
|
||||
@@ -1376,47 +1573,40 @@ func (g *CUEGenerator) writeFieldNode(sb *strings.Builder, name string, node *fi
|
||||
return
|
||||
}
|
||||
|
||||
// If all children share the same condition and there are no unconditional parts,
|
||||
// lift the condition to avoid emitting empty parent structs.
|
||||
if node.value == nil && len(node.children) > 0 && len(node.spreads) == 0 && node.forEach == nil && node.patchKey == nil {
|
||||
condStr := ""
|
||||
canLift := true
|
||||
for _, childName := range node.childOrder {
|
||||
child := node.children[childName]
|
||||
if child.cond == nil {
|
||||
canLift = false
|
||||
break
|
||||
}
|
||||
childCondStr := g.conditionToCUE(child.cond)
|
||||
if condStr == "" {
|
||||
condStr = childCondStr
|
||||
} else if condStr != childCondStr {
|
||||
canLift = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if canLift && condStr != "" {
|
||||
// Clone node with cleared child conditions for rendering.
|
||||
clone := &fieldNode{
|
||||
children: make(map[string]*fieldNode, len(node.children)),
|
||||
childOrder: append([]string(nil), node.childOrder...),
|
||||
}
|
||||
for childName, child := range node.children {
|
||||
childCopy := *child
|
||||
childCopy.cond = nil
|
||||
clone.children[childName] = &childCopy
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: {\n", indent, name))
|
||||
g.writeFieldTree(sb, clone, depth+2)
|
||||
sb.WriteString(fmt.Sprintf("%s\t}\n", indent))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
return
|
||||
}
|
||||
// Try to decompose or lift conditions for cleaner CUE output.
|
||||
if g.tryDecomposeOrLift(sb, name, node, indent, depth) {
|
||||
return
|
||||
}
|
||||
|
||||
// Emit directive annotations before the field
|
||||
for _, directive := range node.directives {
|
||||
sb.WriteString(fmt.Sprintf("%s// +%s\n", indent, directive))
|
||||
}
|
||||
|
||||
// Regular field
|
||||
if node.value != nil && len(node.children) == 0 {
|
||||
if len(node.condValues) > 0 {
|
||||
// Multiple conditional values at the same path — render each
|
||||
// inside its own if block.
|
||||
if node.cond != nil {
|
||||
condStr := g.conditionToCUE(node.cond)
|
||||
valStr := g.valueToCUE(node.value)
|
||||
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", indent, name, valStr))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
} else {
|
||||
valStr := g.valueToCUE(node.value)
|
||||
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, name, valStr))
|
||||
}
|
||||
for _, cv := range node.condValues {
|
||||
condStr := g.conditionToCUE(cv.cond)
|
||||
valStr := g.valueToCUE(cv.value)
|
||||
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", indent, name, valStr))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
}
|
||||
return
|
||||
}
|
||||
// Leaf node with value
|
||||
valStr := g.valueToCUE(node.value)
|
||||
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, name, valStr))
|
||||
@@ -1428,6 +1618,134 @@ func (g *CUEGenerator) writeFieldNode(sb *strings.Builder, name string, node *fi
|
||||
}
|
||||
}
|
||||
|
||||
// canDecomposeByCondition checks if a struct node's children can be split into
|
||||
// separate conditional blocks. This is possible when every child subtree has
|
||||
// the same uniform set of leaf conditions (e.g., every leaf is guarded by either
|
||||
// condition A or condition B). Returns a map of conditionString -> childNames,
|
||||
// or nil if decomposition is not possible.
|
||||
func (g *CUEGenerator) canDecomposeByCondition(node *fieldNode) map[string][]string {
|
||||
if node.value != nil || len(node.spreads) > 0 || node.forEach != nil || node.patchKey != nil {
|
||||
return nil
|
||||
}
|
||||
if len(node.children) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect the set of leaf conditions from each child subtree
|
||||
childCondSets := make(map[string]map[string]bool)
|
||||
for _, childName := range node.childOrder {
|
||||
child := node.children[childName]
|
||||
condSet := make(map[string]bool)
|
||||
g.collectLeafConditions(child, condSet)
|
||||
if len(condSet) == 0 {
|
||||
return nil
|
||||
}
|
||||
// If any leaf is unconditional (empty string), can't decompose
|
||||
if condSet[""] {
|
||||
return nil
|
||||
}
|
||||
childCondSets[childName] = condSet
|
||||
}
|
||||
|
||||
// All children must have the same condition set
|
||||
var referenceSet map[string]bool
|
||||
for _, condSet := range childCondSets {
|
||||
if referenceSet == nil {
|
||||
referenceSet = condSet
|
||||
} else {
|
||||
if len(condSet) != len(referenceSet) {
|
||||
return nil
|
||||
}
|
||||
for k := range referenceSet {
|
||||
if !condSet[k] {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need at least 2 different conditions to warrant decomposition
|
||||
if len(referenceSet) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(map[string][]string)
|
||||
for condStr := range referenceSet {
|
||||
result[condStr] = append(result[condStr], node.childOrder...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// collectLeafConditions traverses a subtree and collects the CUE string
|
||||
// representations of all leaf conditions. An unconditional leaf adds "".
|
||||
func (g *CUEGenerator) collectLeafConditions(node *fieldNode, condSet map[string]bool) {
|
||||
if node.cond != nil && node.value != nil {
|
||||
condSet[g.conditionToCUE(node.cond)] = true
|
||||
// Also collect conditions from condValues (additional conditional
|
||||
// values at the same path).
|
||||
for _, cv := range node.condValues {
|
||||
condSet[g.conditionToCUE(cv.cond)] = true
|
||||
}
|
||||
return
|
||||
}
|
||||
if node.cond != nil && len(node.children) > 0 {
|
||||
// Intermediate node with a condition (already lifted)
|
||||
condSet[g.conditionToCUE(node.cond)] = true
|
||||
return
|
||||
}
|
||||
if node.value != nil && node.cond == nil {
|
||||
condSet[""] = true
|
||||
return
|
||||
}
|
||||
for _, childName := range node.childOrder {
|
||||
child := node.children[childName]
|
||||
g.collectLeafConditions(child, condSet)
|
||||
}
|
||||
}
|
||||
|
||||
// filterNodeByCondition returns a copy of the subtree containing only leaves
|
||||
// that match the given condition string. Intermediate nodes have their
|
||||
// conditions cleared since the caller wraps the result in the condition block.
|
||||
func (g *CUEGenerator) filterNodeByCondition(node *fieldNode, condStr string) *fieldNode {
|
||||
if node.cond != nil {
|
||||
if g.conditionToCUE(node.cond) == condStr {
|
||||
nodeCopy := *node
|
||||
nodeCopy.cond = nil
|
||||
nodeCopy.condValues = nil // Strip condValues; other conditions handled by their own block
|
||||
return &nodeCopy
|
||||
}
|
||||
// Check if the target condition is in condValues instead of the primary
|
||||
for _, cv := range node.condValues {
|
||||
if g.conditionToCUE(cv.cond) == condStr {
|
||||
nodeCopy := *node
|
||||
nodeCopy.value = cv.value
|
||||
nodeCopy.cond = nil
|
||||
nodeCopy.condValues = nil
|
||||
return &nodeCopy
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
filtered := &fieldNode{
|
||||
children: make(map[string]*fieldNode),
|
||||
childOrder: make([]string, 0),
|
||||
value: node.value,
|
||||
}
|
||||
for _, childName := range node.childOrder {
|
||||
child := node.children[childName]
|
||||
filteredChild := g.filterNodeByCondition(child, condStr)
|
||||
if filteredChild != nil {
|
||||
filtered.children[childName] = filteredChild
|
||||
filtered.childOrder = append(filtered.childOrder, childName)
|
||||
}
|
||||
}
|
||||
if len(filtered.children) == 0 && filtered.value == nil {
|
||||
return nil
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// valueToCUE converts a Value to CUE syntax.
|
||||
func (g *CUEGenerator) valueToCUE(v Value) string {
|
||||
switch val := v.(type) {
|
||||
@@ -1442,7 +1760,7 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
|
||||
case *HelperVar:
|
||||
// Return reference to the helper by name
|
||||
return val.Name()
|
||||
case *StringParam, *IntParam, *BoolParam, *FloatParam, *ArrayParam, *MapParam, *StringKeyMapParam, *EnumParam:
|
||||
case *StringParam, *IntParam, *BoolParam, *FloatParam, *ArrayParam, *MapParam, *StringKeyMapParam, *EnumParam, *OneOfParam:
|
||||
return "parameter." + v.(Param).Name()
|
||||
case *DynamicMapParam:
|
||||
// Dynamic map parameters reference just "parameter"
|
||||
@@ -1454,6 +1772,8 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
|
||||
return g.collectionOpToCUE(val)
|
||||
case *MultiSource:
|
||||
return g.multiSourceToCUE(val)
|
||||
case *InlineArrayValue:
|
||||
return g.inlineArrayToCUE(val)
|
||||
case *ConcatExprValue:
|
||||
return g.concatExprToCUE(val)
|
||||
case *CUEFunc:
|
||||
@@ -1679,7 +1999,15 @@ func (g *CUEGenerator) arrayBuilderToCUE(ab *ArrayBuilder, depth int) string {
|
||||
|
||||
case entryForEachWith:
|
||||
sourceStr := g.valueToCUE(entry.source)
|
||||
sb.WriteString(fmt.Sprintf("%sfor %s in %s {\n", innerIndent, entry.itemBuilder.VarName(), sourceStr))
|
||||
guardPrefix := ""
|
||||
if entry.guard != nil {
|
||||
guardPrefix = "if " + g.conditionToCUE(entry.guard) + " "
|
||||
}
|
||||
filterSuffix := ""
|
||||
if entry.filter != nil {
|
||||
filterSuffix = " if " + g.predicateToCUE(entry.filter)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s%sfor %s in %s%s {\n", innerIndent, guardPrefix, entry.itemBuilder.VarName(), sourceStr, filterSuffix))
|
||||
g.writeItemBuilderOps(&sb, entry.itemBuilder.Ops(), depth+2)
|
||||
sb.WriteString(fmt.Sprintf("%s},\n", innerIndent))
|
||||
}
|
||||
@@ -1736,12 +2064,15 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's a Map operation
|
||||
// Check if there's a Map or MapVariant operation
|
||||
hasMap := false
|
||||
hasVariant := false
|
||||
for _, op := range ops {
|
||||
if _, ok := op.(*mapOp); ok {
|
||||
hasMap = true
|
||||
break
|
||||
}
|
||||
if _, ok := op.(*mapVariantOp); ok {
|
||||
hasVariant = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1764,7 +2095,7 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
|
||||
sb.WriteString(filterCondition)
|
||||
}
|
||||
|
||||
if hasMap {
|
||||
if hasMap || hasVariant {
|
||||
// Map operations: render mapped fields in a struct
|
||||
sb.WriteString(" {\n")
|
||||
sb.WriteString("\t\t\t\t{\n")
|
||||
@@ -1775,6 +2106,21 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\tif v.%s != _|_ {\n", optField.field))
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t%s: v.%s\n", fieldName, optField.field))
|
||||
sb.WriteString("\t\t\t\t\t}\n")
|
||||
} else if compOpt, isCompound := fieldVal.(*CompoundOptionalField); isCompound {
|
||||
condStr := g.conditionToCUE(compOpt.additionalCond)
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\tif v.%s != _|_ if %s {\n", compOpt.field, condStr))
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t%s: v.%s\n", fieldName, compOpt.field))
|
||||
sb.WriteString("\t\t\t\t\t}\n")
|
||||
} else if condRef, isConditional := fieldVal.(*ConditionalOrFieldRef); isConditional {
|
||||
// Emit if/else pattern for conditional field reference
|
||||
primaryField := string(condRef.primary)
|
||||
fallbackStr := g.fieldValueToCUE(condRef.fallback)
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\tif v.%s != _|_ {\n", primaryField))
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t%s: v.%s\n", fieldName, primaryField))
|
||||
sb.WriteString("\t\t\t\t\t}\n")
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\tif v.%s == _|_ {\n", primaryField))
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t%s: %s\n", fieldName, fallbackStr))
|
||||
sb.WriteString("\t\t\t\t\t}\n")
|
||||
} else {
|
||||
valStr := g.fieldValueToCUE(fieldVal)
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\t%s: %s\n", fieldName, valStr))
|
||||
@@ -1782,6 +2128,23 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
|
||||
}
|
||||
}
|
||||
}
|
||||
// MapVariant operations: render conditional field blocks
|
||||
for _, op := range ops {
|
||||
if mvOp, ok := op.(*mapVariantOp); ok {
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\tif v.%s == %q {\n", mvOp.discriminator, mvOp.variantName))
|
||||
for fieldName, fieldVal := range mvOp.mappings {
|
||||
if optField, isOptional := fieldVal.(*OptionalField); isOptional {
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\tif v.%s != _|_ {\n", optField.field))
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t\t%s: v.%s\n", fieldName, optField.field))
|
||||
sb.WriteString("\t\t\t\t\t\t}\n")
|
||||
} else {
|
||||
valStr := g.fieldValueToCUE(fieldVal)
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t%s: %s\n", fieldName, valStr))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\t\t\t\t\t}\n")
|
||||
}
|
||||
}
|
||||
sb.WriteString("\t\t\t\t}\n")
|
||||
sb.WriteString("\t\t\t}]")
|
||||
} else {
|
||||
@@ -2052,6 +2415,19 @@ func (g *CUEGenerator) concatExprToCUE(ce *ConcatExprValue) string {
|
||||
return strings.Join(parts, " + ")
|
||||
}
|
||||
|
||||
// inlineArrayToCUE converts an InlineArrayValue to CUE syntax.
|
||||
// Generates: [{field1: value1, field2: value2}]
|
||||
func (g *CUEGenerator) inlineArrayToCUE(arr *InlineArrayValue) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[{\n")
|
||||
for fieldName, fieldVal := range arr.Fields() {
|
||||
valStr := g.valueToCUE(fieldVal)
|
||||
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t\t%s: %s\n", fieldName, valStr))
|
||||
}
|
||||
sb.WriteString("\t\t\t\t\t\t}]")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// conditionToCUE converts a Condition to CUE syntax.
|
||||
func (g *CUEGenerator) conditionToCUE(cond Condition) string {
|
||||
switch c := cond.(type) {
|
||||
@@ -2262,11 +2638,21 @@ func (g *CUEGenerator) writeStatus(sb *strings.Builder, c *ComponentDefinition,
|
||||
func (g *CUEGenerator) writeParam(sb *strings.Builder, param Param, depth int) {
|
||||
indent := strings.Repeat(g.indent, depth)
|
||||
|
||||
// Write // +ignore directive if set (before +usage)
|
||||
if ip, ok := param.(interface{ IsIgnore() bool }); ok && ip.IsIgnore() {
|
||||
sb.WriteString(fmt.Sprintf("%s// +ignore\n", indent))
|
||||
}
|
||||
|
||||
// Write description as comment if present
|
||||
if desc := param.GetDescription(); desc != "" {
|
||||
sb.WriteString(fmt.Sprintf("%s// +usage=%s\n", indent, desc))
|
||||
}
|
||||
|
||||
// Write // +short=X directive if set (after +usage)
|
||||
if sp, ok := param.(interface{ GetShort() string }); ok && sp.GetShort() != "" {
|
||||
sb.WriteString(fmt.Sprintf("%s// +short=%s\n", indent, sp.GetShort()))
|
||||
}
|
||||
|
||||
name := param.Name()
|
||||
optional := "?"
|
||||
forceOptional := false
|
||||
@@ -2299,6 +2685,8 @@ func (g *CUEGenerator) writeParam(sb *strings.Builder, param Param, depth int) {
|
||||
g.writeStructParam(sb, p, indent, name, optional, depth)
|
||||
case *EnumParam:
|
||||
g.writeEnumParam(sb, p, indent, name, optional)
|
||||
case *OneOfParam:
|
||||
g.writeOneOfParam(sb, p, indent, name, optional, depth)
|
||||
default:
|
||||
// Generic fallback
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: _\n", indent, name, optional))
|
||||
@@ -2356,9 +2744,9 @@ func (g *CUEGenerator) writeStringParam(sb *strings.Builder, p *StringParam, ind
|
||||
|
||||
if p.HasDefault() {
|
||||
if len(constraints) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("%s%s: *%q | string & %s\n", indent, name, p.GetDefault(), strings.Join(constraints, " & ")))
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: *%q | string & %s\n", indent, name, optional, p.GetDefault(), strings.Join(constraints, " & ")))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s%s: *%q | string\n", indent, name, p.GetDefault()))
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: *%q | string\n", indent, name, optional, p.GetDefault()))
|
||||
}
|
||||
} else {
|
||||
if len(constraints) > 0 {
|
||||
@@ -2521,11 +2909,11 @@ func (g *CUEGenerator) writeMapParam(sb *strings.Builder, p *MapParam, indent, n
|
||||
if cueType != "" {
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: [string]: %s\n", indent, name, optional, cueType))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: {...}\n", indent, name, optional))
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, cueOpenStruct))
|
||||
}
|
||||
} else {
|
||||
// Generic object
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: {...}\n", indent, name, optional))
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, cueOpenStruct))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2635,8 +3023,50 @@ func (g *CUEGenerator) writeEnumParam(sb *strings.Builder, p *EnumParam, indent,
|
||||
}
|
||||
}
|
||||
|
||||
// cueTypeForParamType converts a ParamType to its CUE type string.
|
||||
func (g *CUEGenerator) cueTypeForParamType(pt ParamType) string {
|
||||
// writeOneOfParam writes a discriminated union parameter.
|
||||
// The param name is used as the discriminator field name (e.g., OneOf("type")).
|
||||
// Generates:
|
||||
//
|
||||
// type: *"default" | "variant1" | "variant2"
|
||||
// if type == "variant1" { field1: string }
|
||||
// if type == "variant2" { field2: int }
|
||||
func (g *CUEGenerator) writeOneOfParam(sb *strings.Builder, p *OneOfParam, indent, name, optional string, depth int) {
|
||||
variants := p.GetVariants()
|
||||
|
||||
// Build discriminator field: type: *"default" | "variant1" | "variant2"
|
||||
var enumParts []string
|
||||
if p.HasDefault() {
|
||||
defaultStr := fmt.Sprintf("%v", p.GetDefault())
|
||||
enumParts = append(enumParts, fmt.Sprintf("*%s", formatCUEValue(p.GetDefault())))
|
||||
for _, v := range variants {
|
||||
if v.Name() != defaultStr {
|
||||
enumParts = append(enumParts, fmt.Sprintf("%q", v.Name()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, v := range variants {
|
||||
enumParts = append(enumParts, fmt.Sprintf("%q", v.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, strings.Join(enumParts, " | ")))
|
||||
|
||||
// Write conditional blocks for each variant
|
||||
for _, variant := range variants {
|
||||
fields := variant.GetFields()
|
||||
if len(fields) == 0 {
|
||||
continue
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%sif %s == %q {\n", indent, name, variant.Name()))
|
||||
for _, field := range fields {
|
||||
g.writeStructField(sb, field, depth+1)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
}
|
||||
}
|
||||
|
||||
// cueTypeStr converts a ParamType to its CUE type string.
|
||||
func cueTypeStr(pt ParamType) string {
|
||||
switch pt {
|
||||
case ParamTypeString:
|
||||
return string(ParamTypeString)
|
||||
@@ -2648,15 +3078,18 @@ func (g *CUEGenerator) cueTypeForParamType(pt ParamType) string {
|
||||
return "float"
|
||||
case ParamTypeArray:
|
||||
return "[...]"
|
||||
case ParamTypeMap:
|
||||
return "{...}"
|
||||
case ParamTypeStruct:
|
||||
return "{...}"
|
||||
case ParamTypeMap, ParamTypeStruct, ParamTypeOneOf:
|
||||
return cueOpenStruct
|
||||
default:
|
||||
return "_"
|
||||
}
|
||||
}
|
||||
|
||||
// cueTypeForParamType converts a ParamType to its CUE type string.
|
||||
func (g *CUEGenerator) cueTypeForParamType(pt ParamType) string {
|
||||
return cueTypeStr(pt)
|
||||
}
|
||||
|
||||
// formatCUEValue formats a Go value as a CUE literal.
|
||||
func formatCUEValue(v any) string {
|
||||
switch val := v.(type) {
|
||||
|
||||
@@ -17,6 +17,8 @@ limitations under the License.
|
||||
package defkit_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
@@ -137,6 +139,48 @@ var _ = Describe("CUEGenerator", func() {
|
||||
// ForceOptional with default: has ? (field can be absent, defaults when present)
|
||||
Expect(cue).To(ContainSubstring(`optionalDefault?: *"Honor" | "Ignore"`))
|
||||
})
|
||||
|
||||
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").Required().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() {
|
||||
@@ -220,6 +264,412 @@ var _ = Describe("CUEGenerator", func() {
|
||||
})
|
||||
})
|
||||
|
||||
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").Fields(
|
||||
defkit.Field("claimName", defkit.ParamTypeString).Required(),
|
||||
),
|
||||
defkit.Variant("emptyDir").Fields(
|
||||
defkit.Field("medium", defkit.ParamTypeString).Default("").Enum("", "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").WithFields(
|
||||
defkit.String("name").Required(),
|
||||
defkit.OneOf("type").Default("emptyDir").Variants(
|
||||
defkit.Variant("pvc").Fields(
|
||||
defkit.Field("claimName", defkit.ParamTypeString).Required(),
|
||||
),
|
||||
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").Fields(
|
||||
defkit.Field("config", defkit.ParamTypeString).Required(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
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").
|
||||
@@ -399,4 +849,190 @@ var _ = Describe("CUEGenerator", func() {
|
||||
Expect(cue).To(ContainSubstring("strings"))
|
||||
})
|
||||
})
|
||||
|
||||
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").Required(),
|
||||
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").Required(),
|
||||
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").Required(),
|
||||
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"`))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -355,6 +355,14 @@ type CUEFunc struct {
|
||||
func (c *CUEFunc) expr() {}
|
||||
func (c *CUEFunc) value() {}
|
||||
|
||||
// RequiredImports returns the CUE imports required by this function call.
|
||||
func (c *CUEFunc) RequiredImports() []string {
|
||||
if c.pkg != "" {
|
||||
return []string{c.pkg}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Package returns the CUE package name.
|
||||
func (c *CUEFunc) Package() string { return c.pkg }
|
||||
|
||||
@@ -798,3 +806,23 @@ func (c *IterFieldExistsCondition) FieldName() string { return c.field }
|
||||
|
||||
// IsNegated returns true if this is a "not exists" check.
|
||||
func (c *IterFieldExistsCondition) IsNegated() bool { return c.negate }
|
||||
|
||||
// InlineArrayValue represents an inline array literal containing struct elements.
|
||||
// This generates CUE like: [{field1: value1, field2: value2}]
|
||||
// Used for deprecated parameter fallbacks that create a single-element array.
|
||||
type InlineArrayValue struct {
|
||||
fields map[string]Value
|
||||
}
|
||||
|
||||
func (a *InlineArrayValue) expr() {}
|
||||
func (a *InlineArrayValue) value() {}
|
||||
|
||||
// Fields returns the field mappings.
|
||||
func (a *InlineArrayValue) Fields() map[string]Value { return a.fields }
|
||||
|
||||
// InlineArray creates an inline array value with a single struct element.
|
||||
// Example: InlineArray(map[string]Value{"containerPort": port})
|
||||
// Generates: [{containerPort: parameter.port}]
|
||||
func InlineArray(fields map[string]Value) *InlineArrayValue {
|
||||
return &InlineArrayValue{fields: fields}
|
||||
}
|
||||
|
||||
@@ -194,59 +194,63 @@ var _ = Describe("Expressions", func() {
|
||||
})
|
||||
|
||||
Context("CueFunc expressions", func() {
|
||||
It("should create StrconvFormatInt function", func() {
|
||||
It("should create StrconvFormatInt function with correct args", func() {
|
||||
num := defkit.Int("port")
|
||||
fn := defkit.StrconvFormatInt(num, 10)
|
||||
Expect(fn).NotTo(BeNil())
|
||||
Expect(fn.Package()).To(Equal("strconv"))
|
||||
Expect(fn.Function()).To(Equal("FormatInt"))
|
||||
Expect(fn.Args()).To(HaveLen(2))
|
||||
Expect(fn.Args()[0]).To(Equal(num))
|
||||
Expect(fn.Args()[1]).To(Equal(defkit.Lit(10)))
|
||||
})
|
||||
|
||||
It("should create StringsToLower function", func() {
|
||||
It("should create StringsToLower function with correct arg", func() {
|
||||
str := defkit.String("name")
|
||||
fn := defkit.StringsToLower(str)
|
||||
Expect(fn).NotTo(BeNil())
|
||||
Expect(fn.Package()).To(Equal("strings"))
|
||||
Expect(fn.Function()).To(Equal("ToLower"))
|
||||
Expect(fn.Args()).To(HaveLen(1))
|
||||
Expect(fn.Args()[0]).To(Equal(str))
|
||||
})
|
||||
|
||||
It("should create StringsToUpper function", func() {
|
||||
It("should create StringsToUpper function with correct arg", func() {
|
||||
str := defkit.String("name")
|
||||
fn := defkit.StringsToUpper(str)
|
||||
Expect(fn).NotTo(BeNil())
|
||||
Expect(fn.Package()).To(Equal("strings"))
|
||||
Expect(fn.Function()).To(Equal("ToUpper"))
|
||||
Expect(fn.Args()).To(HaveLen(1))
|
||||
Expect(fn.Args()[0]).To(Equal(str))
|
||||
})
|
||||
|
||||
It("should create StringsHasPrefix function", func() {
|
||||
It("should create StringsHasPrefix function with correct args", func() {
|
||||
str := defkit.String("path")
|
||||
fn := defkit.StringsHasPrefix(str, "/api")
|
||||
Expect(fn).NotTo(BeNil())
|
||||
Expect(fn.Package()).To(Equal("strings"))
|
||||
Expect(fn.Function()).To(Equal("HasPrefix"))
|
||||
Expect(fn.Args()).To(HaveLen(2))
|
||||
Expect(fn.Args()[0]).To(Equal(str))
|
||||
Expect(fn.Args()[1]).To(Equal(defkit.Lit("/api")))
|
||||
})
|
||||
|
||||
It("should create StringsHasSuffix function", func() {
|
||||
It("should create StringsHasSuffix function with correct args", func() {
|
||||
str := defkit.String("file")
|
||||
fn := defkit.StringsHasSuffix(str, ".yaml")
|
||||
Expect(fn).NotTo(BeNil())
|
||||
Expect(fn.Package()).To(Equal("strings"))
|
||||
Expect(fn.Function()).To(Equal("HasSuffix"))
|
||||
Expect(fn.Args()).To(HaveLen(2))
|
||||
Expect(fn.Args()[0]).To(Equal(str))
|
||||
Expect(fn.Args()[1]).To(Equal(defkit.Lit(".yaml")))
|
||||
})
|
||||
|
||||
It("should create ListConcat function", func() {
|
||||
It("should create ListConcat function with correct args", func() {
|
||||
list1 := defkit.List("list1")
|
||||
list2 := defkit.List("list2")
|
||||
fn := defkit.ListConcat(list1, list2)
|
||||
Expect(fn).NotTo(BeNil())
|
||||
Expect(fn.Package()).To(Equal("list"))
|
||||
Expect(fn.Function()).To(Equal("Concat"))
|
||||
Expect(fn.Args()).To(HaveLen(2))
|
||||
Expect(fn.Args()[0]).To(Equal(list1))
|
||||
Expect(fn.Args()[1]).To(Equal(list2))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -259,12 +263,14 @@ var _ = Describe("Expressions", func() {
|
||||
})
|
||||
|
||||
It("should set fields on array element", func() {
|
||||
nameLit := defkit.Lit("test")
|
||||
portLit := defkit.Lit(8080)
|
||||
elem := defkit.NewArrayElement().
|
||||
Set("name", defkit.Lit("test")).
|
||||
Set("port", defkit.Lit(8080))
|
||||
Set("name", nameLit).
|
||||
Set("port", portLit)
|
||||
Expect(elem.Fields()).To(HaveLen(2))
|
||||
Expect(elem.Fields()["name"]).NotTo(BeNil())
|
||||
Expect(elem.Fields()["port"]).NotTo(BeNil())
|
||||
Expect(elem.Fields()["name"]).To(Equal(nameLit))
|
||||
Expect(elem.Fields()["port"]).To(Equal(portLit))
|
||||
})
|
||||
|
||||
It("should set conditional fields on array element", func() {
|
||||
@@ -286,6 +292,11 @@ var _ = Describe("Expressions", func() {
|
||||
It("should create ForEachMap expression", func() {
|
||||
forEach := defkit.ForEachMap()
|
||||
Expect(forEach).NotTo(BeNil())
|
||||
Expect(forEach.Source()).To(Equal("parameter"))
|
||||
Expect(forEach.KeyVar()).To(Equal("k"))
|
||||
Expect(forEach.ValVar()).To(Equal("v"))
|
||||
Expect(forEach.KeyExpr()).To(BeEmpty())
|
||||
Expect(forEach.ValExpr()).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should set source and variable names", func() {
|
||||
@@ -316,11 +327,47 @@ var _ = Describe("Expressions", func() {
|
||||
It("should create parameter reference", func() {
|
||||
ref := defkit.ParamRef("image")
|
||||
Expect(ref).NotTo(BeNil())
|
||||
Expect(ref.Path()).To(Equal("parameter.image"))
|
||||
})
|
||||
|
||||
It("should reference nested parameter", func() {
|
||||
ref := defkit.ParamRef("config.port")
|
||||
Expect(ref).NotTo(BeNil())
|
||||
Expect(ref.Path()).To(Equal("parameter.config.port"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("InlineArrayValue", func() {
|
||||
It("should create inline array with fields and store correct values", func() {
|
||||
port := defkit.Int("port")
|
||||
arr := defkit.InlineArray(map[string]defkit.Value{
|
||||
"containerPort": port,
|
||||
})
|
||||
Expect(arr.Fields()).To(HaveLen(1))
|
||||
Expect(arr.Fields()).To(HaveKey("containerPort"))
|
||||
Expect(arr.Fields()["containerPort"]).To(Equal(port))
|
||||
})
|
||||
|
||||
It("should create inline array with multiple fields and store correct values", func() {
|
||||
port := defkit.Int("port")
|
||||
protocol := defkit.Lit("TCP")
|
||||
arr := defkit.InlineArray(map[string]defkit.Value{
|
||||
"containerPort": port,
|
||||
"protocol": protocol,
|
||||
})
|
||||
Expect(arr.Fields()).To(HaveLen(2))
|
||||
Expect(arr.Fields()["containerPort"]).To(Equal(port))
|
||||
Expect(arr.Fields()["protocol"]).To(Equal(protocol))
|
||||
})
|
||||
|
||||
It("should implement Value interface", func() {
|
||||
port := defkit.Int("port")
|
||||
arr := defkit.InlineArray(map[string]defkit.Value{
|
||||
"containerPort": port,
|
||||
})
|
||||
var v defkit.Value = arr // compile-time check
|
||||
Expect(v).NotTo(BeNil())
|
||||
Expect(arr.Fields()["containerPort"]).To(Equal(port))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -252,6 +252,14 @@ func (hb *HelperBuilder) FromFields(source Value, fields ...string) *HelperBuild
|
||||
return hb
|
||||
}
|
||||
|
||||
// FromArray uses a pre-built ArrayBuilder as the helper source.
|
||||
// This enables complex iteration patterns (ForEachWithGuardedFiltered)
|
||||
// that can't be expressed through the standard From/Filter/Map pipeline.
|
||||
func (hb *HelperBuilder) FromArray(ab *ArrayBuilder) *HelperBuilder {
|
||||
hb.source = &arrayBuilderSource{builder: ab}
|
||||
return hb
|
||||
}
|
||||
|
||||
// FromHelper references another helper as the source.
|
||||
// This enables helper chaining for patterns like deduplication.
|
||||
//
|
||||
@@ -273,6 +281,13 @@ type helperRefSource struct {
|
||||
|
||||
func (h *helperRefSource) isHelperSource() {}
|
||||
|
||||
// arrayBuilderSource wraps an ArrayBuilder as a helper source.
|
||||
type arrayBuilderSource struct {
|
||||
builder *ArrayBuilder
|
||||
}
|
||||
|
||||
func (a *arrayBuilderSource) isHelperSource() {}
|
||||
|
||||
// Each applies a transformation function to each element.
|
||||
// The function receives a Value representing the current item and returns
|
||||
// a transformed Value.
|
||||
@@ -474,6 +489,9 @@ func (hb *HelperBuilder) buildCollection() Value {
|
||||
hb.applyOpsToCollection(col)
|
||||
return col
|
||||
|
||||
case *arrayBuilderSource:
|
||||
return src.builder
|
||||
|
||||
default:
|
||||
// Default: empty collection
|
||||
return Each(Lit([]any{}))
|
||||
|
||||
@@ -23,7 +23,9 @@ type baseParam struct {
|
||||
required bool
|
||||
defaultValue any
|
||||
description string
|
||||
forceOptional bool // when true, field stays optional even with a default value
|
||||
forceOptional bool // when true, field stays optional even with a default value
|
||||
short string // short flag alias (e.g. "i" → // +short=i)
|
||||
ignore bool // when true, emits // +ignore directive
|
||||
}
|
||||
|
||||
func (p *baseParam) expr() {}
|
||||
@@ -37,6 +39,8 @@ func (p *baseParam) HasDefault() bool { return p.defaultValue != nil }
|
||||
func (p *baseParam) GetDefault() any { return p.defaultValue }
|
||||
func (p *baseParam) GetDescription() string { return p.description }
|
||||
func (p *baseParam) IsForceOptional() bool { return p.forceOptional }
|
||||
func (p *baseParam) GetShort() string { return p.short }
|
||||
func (p *baseParam) IsIgnore() bool { return p.ignore }
|
||||
|
||||
// IsSet returns a condition that checks if the parameter has a value.
|
||||
// This is used with SetIf for conditional field assignment.
|
||||
@@ -142,6 +146,20 @@ func (p *StringParam) ForceOptional() *StringParam {
|
||||
return p
|
||||
}
|
||||
|
||||
// Short sets a short flag alias for the parameter.
|
||||
// This generates a // +short=X directive in the CUE output.
|
||||
func (p *StringParam) Short(s string) *StringParam {
|
||||
p.short = s
|
||||
return p
|
||||
}
|
||||
|
||||
// Ignore marks the parameter as ignored by the UI.
|
||||
// This generates a // +ignore directive in the CUE output.
|
||||
func (p *StringParam) Ignore() *StringParam {
|
||||
p.ignore = true
|
||||
return p
|
||||
}
|
||||
|
||||
// Default sets a default value for the parameter.
|
||||
func (p *StringParam) Default(value string) *StringParam {
|
||||
p.defaultValue = value
|
||||
@@ -320,6 +338,20 @@ func (p *IntParam) Description(desc string) *IntParam {
|
||||
return p
|
||||
}
|
||||
|
||||
// Short sets a short flag alias for the parameter.
|
||||
// This generates a // +short=X directive in the CUE output.
|
||||
func (p *IntParam) Short(s string) *IntParam {
|
||||
p.short = s
|
||||
return p
|
||||
}
|
||||
|
||||
// Ignore marks the parameter as ignored by the UI.
|
||||
// This generates a // +ignore directive in the CUE output.
|
||||
func (p *IntParam) Ignore() *IntParam {
|
||||
p.ignore = true
|
||||
return p
|
||||
}
|
||||
|
||||
// Min sets the minimum value constraint for the parameter.
|
||||
// This generates CUE like: int & >=n
|
||||
func (p *IntParam) Min(n int) *IntParam {
|
||||
@@ -417,6 +449,20 @@ func (p *BoolParam) Description(desc string) *BoolParam {
|
||||
return p
|
||||
}
|
||||
|
||||
// Short sets a short flag alias for the parameter.
|
||||
// This generates a // +short=X directive in the CUE output.
|
||||
func (p *BoolParam) Short(s string) *BoolParam {
|
||||
p.short = s
|
||||
return p
|
||||
}
|
||||
|
||||
// Ignore marks the parameter as ignored by the UI.
|
||||
// This generates a // +ignore directive in the CUE output.
|
||||
func (p *BoolParam) Ignore() *BoolParam {
|
||||
p.ignore = true
|
||||
return p
|
||||
}
|
||||
|
||||
// IsTrue returns a condition that checks if the bool parameter is truthy.
|
||||
// In CUE, this generates `if parameter.name` instead of `if parameter.name == true`.
|
||||
func (p *BoolParam) IsTrue() Condition {
|
||||
@@ -1033,6 +1079,20 @@ func (p *EnumParam) Description(desc string) *EnumParam {
|
||||
return p
|
||||
}
|
||||
|
||||
// Short sets a short flag alias for the parameter.
|
||||
// This generates a // +short=X directive in the CUE output.
|
||||
func (p *EnumParam) Short(s string) *EnumParam {
|
||||
p.short = s
|
||||
return p
|
||||
}
|
||||
|
||||
// Ignore marks the parameter as ignored by the UI.
|
||||
// This generates a // +ignore directive in the CUE output.
|
||||
func (p *EnumParam) Ignore() *EnumParam {
|
||||
p.ignore = true
|
||||
return p
|
||||
}
|
||||
|
||||
// GetValues returns the allowed enum values.
|
||||
func (p *EnumParam) GetValues() []string {
|
||||
return p.values
|
||||
@@ -1095,6 +1155,12 @@ func (p *OneOfParam) Variants(variants ...*OneOfVariant) *OneOfParam {
|
||||
return p
|
||||
}
|
||||
|
||||
// Default sets the default variant name for the discriminator.
|
||||
func (p *OneOfParam) Default(value string) *OneOfParam {
|
||||
p.defaultValue = value
|
||||
return p
|
||||
}
|
||||
|
||||
// Required marks the parameter as required.
|
||||
func (p *OneOfParam) Required() *OneOfParam {
|
||||
p.required = true
|
||||
@@ -1197,6 +1263,9 @@ func (p *StringKeyMapParam) Description(desc string) *StringKeyMapParam {
|
||||
return p
|
||||
}
|
||||
|
||||
// GetType returns the parameter type.
|
||||
func (p *StringKeyMapParam) GetType() ParamType { return p.paramType }
|
||||
|
||||
// DynamicMapParam represents a parameter where the parameter itself is a dynamic map.
|
||||
// In CUE: parameter: [string]: T (where T is the value type)
|
||||
// This is used for traits like labels where all user values become map keys.
|
||||
|
||||
@@ -362,42 +362,51 @@ var _ = Describe("Parameters", func() {
|
||||
It("should create Eq condition from IntParam", func() {
|
||||
replicas := defkit.Int("replicas")
|
||||
cond := replicas.Eq(3)
|
||||
Expect(cond).NotTo(BeNil())
|
||||
// Check it implements Condition interface
|
||||
_, ok := cond.(defkit.Condition) //lint:ignore S1040 intentional interface check for documentation
|
||||
Expect(ok).To(BeTrue())
|
||||
pcc, ok := cond.(*defkit.ParamCompareCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *ParamCompareCondition")
|
||||
Expect(pcc.ParamName()).To(Equal("replicas"))
|
||||
Expect(pcc.Op()).To(Equal("=="))
|
||||
Expect(pcc.CompareValue()).To(Equal(3))
|
||||
})
|
||||
|
||||
It("should create comparison conditions from StringParam", func() {
|
||||
status := defkit.String("status")
|
||||
|
||||
// Eq
|
||||
eqCond := status.Eq("running")
|
||||
Expect(eqCond).NotTo(BeNil())
|
||||
eqCond, ok := status.Eq("running").(*defkit.ParamCompareCondition)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(eqCond.ParamName()).To(Equal("status"))
|
||||
Expect(eqCond.Op()).To(Equal("=="))
|
||||
Expect(eqCond.CompareValue()).To(Equal("running"))
|
||||
|
||||
// Ne
|
||||
neCond := status.Ne("error")
|
||||
Expect(neCond).NotTo(BeNil())
|
||||
neCond, ok := status.Ne("error").(*defkit.ParamCompareCondition)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(neCond.ParamName()).To(Equal("status"))
|
||||
Expect(neCond.Op()).To(Equal("!="))
|
||||
Expect(neCond.CompareValue()).To(Equal("error"))
|
||||
})
|
||||
|
||||
It("should create numeric comparison conditions", func() {
|
||||
replicas := defkit.Int("replicas")
|
||||
|
||||
// Gt
|
||||
gtCond := replicas.Gt(1)
|
||||
Expect(gtCond).NotTo(BeNil())
|
||||
gtCond, ok := replicas.Gt(1).(*defkit.ParamCompareCondition)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(gtCond.Op()).To(Equal(">"))
|
||||
Expect(gtCond.CompareValue()).To(Equal(1))
|
||||
|
||||
// Gte
|
||||
gteCond := replicas.Gte(1)
|
||||
Expect(gteCond).NotTo(BeNil())
|
||||
gteCond, ok := replicas.Gte(1).(*defkit.ParamCompareCondition)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(gteCond.Op()).To(Equal(">="))
|
||||
Expect(gteCond.CompareValue()).To(Equal(1))
|
||||
|
||||
// Lt
|
||||
ltCond := replicas.Lt(10)
|
||||
Expect(ltCond).NotTo(BeNil())
|
||||
ltCond, ok := replicas.Lt(10).(*defkit.ParamCompareCondition)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(ltCond.Op()).To(Equal("<"))
|
||||
Expect(ltCond.CompareValue()).To(Equal(10))
|
||||
|
||||
// Lte
|
||||
lteCond := replicas.Lte(10)
|
||||
Expect(lteCond).NotTo(BeNil())
|
||||
lteCond, ok := replicas.Lte(10).(*defkit.ParamCompareCondition)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(lteCond.Op()).To(Equal("<="))
|
||||
Expect(lteCond.CompareValue()).To(Equal(10))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -405,30 +414,35 @@ var _ = Describe("Parameters", func() {
|
||||
It("should create Add expression from IntParam", func() {
|
||||
replicas := defkit.Int("replicas")
|
||||
expr := replicas.Add(1)
|
||||
Expect(expr).NotTo(BeNil())
|
||||
// Check it implements Value interface
|
||||
_, ok := expr.(defkit.Value) //lint:ignore S1040 intentional interface check for documentation
|
||||
Expect(ok).To(BeTrue())
|
||||
arith, ok := expr.(*defkit.ParamArithExpr)
|
||||
Expect(ok).To(BeTrue(), "expected *ParamArithExpr")
|
||||
Expect(arith.ParamName()).To(Equal("replicas"))
|
||||
Expect(arith.Op()).To(Equal("+"))
|
||||
Expect(arith.ArithValue()).To(Equal(1))
|
||||
})
|
||||
|
||||
It("should create arithmetic expressions from IntParam", func() {
|
||||
replicas := defkit.Int("replicas")
|
||||
|
||||
// Add
|
||||
addExpr := replicas.Add(1)
|
||||
Expect(addExpr).NotTo(BeNil())
|
||||
addExpr, ok := replicas.Add(1).(*defkit.ParamArithExpr)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(addExpr.Op()).To(Equal("+"))
|
||||
Expect(addExpr.ArithValue()).To(Equal(1))
|
||||
|
||||
// Sub
|
||||
subExpr := replicas.Sub(1)
|
||||
Expect(subExpr).NotTo(BeNil())
|
||||
subExpr, ok := replicas.Sub(1).(*defkit.ParamArithExpr)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(subExpr.Op()).To(Equal("-"))
|
||||
Expect(subExpr.ArithValue()).To(Equal(1))
|
||||
|
||||
// Mul
|
||||
mulExpr := replicas.Mul(2)
|
||||
Expect(mulExpr).NotTo(BeNil())
|
||||
mulExpr, ok := replicas.Mul(2).(*defkit.ParamArithExpr)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(mulExpr.Op()).To(Equal("*"))
|
||||
Expect(mulExpr.ArithValue()).To(Equal(2))
|
||||
|
||||
// Div
|
||||
divExpr := replicas.Div(2)
|
||||
Expect(divExpr).NotTo(BeNil())
|
||||
divExpr, ok := replicas.Div(2).(*defkit.ParamArithExpr)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(divExpr.Op()).To(Equal("/"))
|
||||
Expect(divExpr.ArithValue()).To(Equal(2))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -436,19 +450,21 @@ var _ = Describe("Parameters", func() {
|
||||
It("should create Concat expression from StringParam", func() {
|
||||
name := defkit.String("name")
|
||||
expr := name.Concat("-suffix")
|
||||
Expect(expr).NotTo(BeNil())
|
||||
// Check it implements Value interface
|
||||
_, ok := expr.(defkit.Value) //lint:ignore S1040 intentional interface check for documentation
|
||||
Expect(ok).To(BeTrue())
|
||||
concat, ok := expr.(*defkit.ParamConcatExpr)
|
||||
Expect(ok).To(BeTrue(), "expected *ParamConcatExpr")
|
||||
Expect(concat.ParamName()).To(Equal("name"))
|
||||
Expect(concat.Suffix()).To(Equal("-suffix"))
|
||||
Expect(concat.Prefix()).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should create Prepend expression from StringParam", func() {
|
||||
name := defkit.String("name")
|
||||
expr := name.Prepend("prefix-")
|
||||
Expect(expr).NotTo(BeNil())
|
||||
// Check it implements Value interface
|
||||
_, ok := expr.(defkit.Value) //lint:ignore S1040 intentional interface check for documentation
|
||||
Expect(ok).To(BeTrue())
|
||||
concat, ok := expr.(*defkit.ParamConcatExpr)
|
||||
Expect(ok).To(BeTrue(), "expected *ParamConcatExpr")
|
||||
Expect(concat.ParamName()).To(Equal("name"))
|
||||
Expect(concat.Prefix()).To(Equal("prefix-"))
|
||||
Expect(concat.Suffix()).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -476,14 +492,20 @@ var _ = Describe("Parameters", func() {
|
||||
config := defkit.Struct("config")
|
||||
fieldRef := config.Field("port")
|
||||
cond := fieldRef.IsSet()
|
||||
Expect(cond).NotTo(BeNil())
|
||||
isSet, ok := cond.(*defkit.ParamPathIsSetCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *ParamPathIsSetCondition")
|
||||
Expect(isSet.Path()).To(Equal("config.port"))
|
||||
})
|
||||
|
||||
It("should create Eq condition from field ref", func() {
|
||||
config := defkit.Struct("config")
|
||||
fieldRef := config.Field("enabled")
|
||||
cond := fieldRef.Eq(true)
|
||||
Expect(cond).NotTo(BeNil())
|
||||
pcc, ok := cond.(*defkit.ParamCompareCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *ParamCompareCondition")
|
||||
Expect(pcc.ParamName()).To(Equal("config.enabled"))
|
||||
Expect(pcc.Op()).To(Equal("=="))
|
||||
Expect(pcc.CompareValue()).To(Equal(true))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -492,26 +514,33 @@ var _ = Describe("Parameters", func() {
|
||||
It("should create StringList parameter", func() {
|
||||
p := defkit.StringList("tags")
|
||||
Expect(p.Name()).To(Equal("tags"))
|
||||
Expect(p.ElementType()).To(Equal(defkit.ParamTypeString))
|
||||
Expect(p.IsRequired()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should create IntList parameter", func() {
|
||||
p := defkit.IntList("ports")
|
||||
Expect(p.Name()).To(Equal("ports"))
|
||||
Expect(p.ElementType()).To(Equal(defkit.ParamTypeInt))
|
||||
Expect(p.IsRequired()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should create StringKeyMap parameter", func() {
|
||||
p := defkit.StringKeyMap("labels")
|
||||
Expect(p.Name()).To(Equal("labels"))
|
||||
Expect(p.GetType()).To(Equal(defkit.ParamTypeMap))
|
||||
})
|
||||
|
||||
It("should create List parameter", func() {
|
||||
p := defkit.List("items")
|
||||
Expect(p.Name()).To(Equal("items"))
|
||||
Expect(p.IsRequired()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should create Object parameter", func() {
|
||||
p := defkit.Object("config")
|
||||
Expect(p.Name()).To(Equal("config"))
|
||||
Expect(p.IsRequired()).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -519,13 +548,19 @@ var _ = Describe("Parameters", func() {
|
||||
It("should create IsSet condition from param", func() {
|
||||
replicas := defkit.Int("replicas")
|
||||
cond := replicas.IsSet()
|
||||
Expect(cond).NotTo(BeNil())
|
||||
isSet, ok := cond.(*defkit.IsSetCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *IsSetCondition")
|
||||
Expect(isSet.ParamName()).To(Equal("replicas"))
|
||||
})
|
||||
|
||||
It("should create NotSet condition from param", func() {
|
||||
replicas := defkit.Int("replicas")
|
||||
cond := replicas.NotSet()
|
||||
Expect(cond).NotTo(BeNil())
|
||||
notCond, ok := cond.(*defkit.NotCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *NotCondition")
|
||||
inner, ok := notCond.Inner().(*defkit.IsSetCondition)
|
||||
Expect(ok).To(BeTrue(), "expected inner *IsSetCondition")
|
||||
Expect(inner.ParamName()).To(Equal("replicas"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -533,13 +568,17 @@ var _ = Describe("Parameters", func() {
|
||||
It("should create IsTrue condition", func() {
|
||||
enabled := defkit.Bool("enabled")
|
||||
cond := enabled.IsTrue()
|
||||
Expect(cond).NotTo(BeNil())
|
||||
truthy, ok := cond.(*defkit.TruthyCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *TruthyCondition")
|
||||
Expect(truthy.ParamName()).To(Equal("enabled"))
|
||||
})
|
||||
|
||||
It("should create IsFalse condition", func() {
|
||||
enabled := defkit.Bool("enabled")
|
||||
cond := enabled.IsFalse()
|
||||
Expect(cond).NotTo(BeNil())
|
||||
falsy, ok := cond.(*defkit.FalsyCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *FalsyCondition")
|
||||
Expect(falsy.ParamName()).To(Equal("enabled"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -556,7 +595,11 @@ var _ = Describe("Parameters", func() {
|
||||
It("should create length constraint conditions", func() {
|
||||
arr := defkit.Array("items")
|
||||
gteCond := arr.LenGte(1)
|
||||
Expect(gteCond).NotTo(BeNil())
|
||||
lenCond, ok := gteCond.(*defkit.LenCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *LenCondition")
|
||||
Expect(lenCond.ParamName()).To(Equal("items"))
|
||||
Expect(lenCond.Op()).To(Equal(">="))
|
||||
Expect(lenCond.Length()).To(Equal(1))
|
||||
})
|
||||
|
||||
It("should set WithFields for array items", func() {
|
||||
@@ -603,6 +646,30 @@ var _ = Describe("Parameters", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Context("OneOfParam Default method", func() {
|
||||
It("should set default variant name", func() {
|
||||
p := defkit.OneOf("type").Default("emptyDir")
|
||||
Expect(p.HasDefault()).To(BeTrue())
|
||||
Expect(p.GetDefault()).To(Equal("emptyDir"))
|
||||
})
|
||||
|
||||
It("should support fluent chaining with Default", func() {
|
||||
p := defkit.OneOf("type").
|
||||
Default("emptyDir").
|
||||
Description("Volume type").
|
||||
Variants(
|
||||
defkit.Variant("pvc").Fields(
|
||||
defkit.Field("claimName", defkit.ParamTypeString).Required(),
|
||||
),
|
||||
defkit.Variant("emptyDir"),
|
||||
)
|
||||
Expect(p.HasDefault()).To(BeTrue())
|
||||
Expect(p.GetDefault()).To(Equal("emptyDir"))
|
||||
Expect(p.GetDescription()).To(Equal("Volume type"))
|
||||
Expect(p.GetVariants()).To(HaveLen(2))
|
||||
})
|
||||
})
|
||||
|
||||
Context("IntParam Optional method", func() {
|
||||
It("should set int as optional", func() {
|
||||
p := defkit.Int("replicas").Optional()
|
||||
@@ -628,13 +695,21 @@ var _ = Describe("Parameters", func() {
|
||||
It("should create LenLt condition", func() {
|
||||
arr := defkit.Array("items")
|
||||
cond := arr.LenLt(10)
|
||||
Expect(cond).NotTo(BeNil())
|
||||
lenCond, ok := cond.(*defkit.LenCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *LenCondition")
|
||||
Expect(lenCond.ParamName()).To(Equal("items"))
|
||||
Expect(lenCond.Op()).To(Equal("<"))
|
||||
Expect(lenCond.Length()).To(Equal(10))
|
||||
})
|
||||
|
||||
It("should create LenLte condition", func() {
|
||||
arr := defkit.Array("items")
|
||||
cond := arr.LenLte(10)
|
||||
Expect(cond).NotTo(BeNil())
|
||||
lenCond, ok := cond.(*defkit.LenCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *LenCondition")
|
||||
Expect(lenCond.ParamName()).To(Equal("items"))
|
||||
Expect(lenCond.Op()).To(Equal("<="))
|
||||
Expect(lenCond.Length()).To(Equal(10))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -756,7 +831,9 @@ var _ = Describe("Parameters", func() {
|
||||
It("should create IsSet condition", func() {
|
||||
path := defkit.ParamPath("config.port")
|
||||
cond := path.IsSet()
|
||||
Expect(cond).NotTo(BeNil())
|
||||
isSet, ok := cond.(*defkit.ParamPathIsSetCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *ParamPathIsSetCondition")
|
||||
Expect(isSet.Path()).To(Equal("config.port"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -789,7 +866,70 @@ var _ = Describe("Parameters", func() {
|
||||
It("should create Ne condition", func() {
|
||||
p := defkit.Int("count")
|
||||
cond := p.Ne(0)
|
||||
Expect(cond).NotTo(BeNil())
|
||||
pcc, ok := cond.(*defkit.ParamCompareCondition)
|
||||
Expect(ok).To(BeTrue(), "expected *ParamCompareCondition")
|
||||
Expect(pcc.ParamName()).To(Equal("count"))
|
||||
Expect(pcc.Op()).To(Equal("!="))
|
||||
Expect(pcc.CompareValue()).To(Equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Short method", func() {
|
||||
It("should set short flag on StringParam", func() {
|
||||
p := defkit.String("image").Short("i")
|
||||
Expect(p.GetShort()).To(Equal("i"))
|
||||
})
|
||||
It("should set short flag on IntParam", func() {
|
||||
p := defkit.Int("port").Short("p")
|
||||
Expect(p.GetShort()).To(Equal("p"))
|
||||
})
|
||||
It("should set short flag on BoolParam", func() {
|
||||
p := defkit.Bool("debug").Short("d")
|
||||
Expect(p.GetShort()).To(Equal("d"))
|
||||
})
|
||||
It("should set short flag on EnumParam", func() {
|
||||
p := defkit.Enum("protocol").Values("TCP", "UDP").Short("p")
|
||||
Expect(p.GetShort()).To(Equal("p"))
|
||||
})
|
||||
It("should return empty string when not set", func() {
|
||||
p := defkit.String("image")
|
||||
Expect(p.GetShort()).To(BeEmpty())
|
||||
})
|
||||
It("should support fluent chaining with other methods", func() {
|
||||
p := defkit.String("image").Required().Description("Container image").Short("i")
|
||||
Expect(p.Name()).To(Equal("image"))
|
||||
Expect(p.IsRequired()).To(BeTrue())
|
||||
Expect(p.GetDescription()).To(Equal("Container image"))
|
||||
Expect(p.GetShort()).To(Equal("i"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Ignore method", func() {
|
||||
It("should mark StringParam as ignored", func() {
|
||||
p := defkit.String("port").Ignore()
|
||||
Expect(p.IsIgnore()).To(BeTrue())
|
||||
})
|
||||
It("should mark IntParam as ignored", func() {
|
||||
p := defkit.Int("port").Ignore()
|
||||
Expect(p.IsIgnore()).To(BeTrue())
|
||||
})
|
||||
It("should mark BoolParam as ignored", func() {
|
||||
p := defkit.Bool("debug").Ignore()
|
||||
Expect(p.IsIgnore()).To(BeTrue())
|
||||
})
|
||||
It("should mark EnumParam as ignored", func() {
|
||||
p := defkit.Enum("type").Values("A", "B").Ignore()
|
||||
Expect(p.IsIgnore()).To(BeTrue())
|
||||
})
|
||||
It("should not be ignored by default", func() {
|
||||
p := defkit.String("image")
|
||||
Expect(p.IsIgnore()).To(BeFalse())
|
||||
})
|
||||
It("should support fluent chaining with Short and other methods", func() {
|
||||
p := defkit.Int("port").Ignore().Description("Deprecated field").Short("p")
|
||||
Expect(p.IsIgnore()).To(BeTrue())
|
||||
Expect(p.GetShort()).To(Equal("p"))
|
||||
Expect(p.GetDescription()).To(Equal("Deprecated field"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -223,10 +223,14 @@ type PatchContainerConfig struct {
|
||||
ContainersDescription string // +usage description for the containers param (auto-generated if empty)
|
||||
CustomParamsBlock string // custom CUE block for #PatchParams (for complex types)
|
||||
MultiContainerParam string // alternate name for multi-container param (default: "probes" for probes, "containers" for others)
|
||||
MultiContainerCheckField string // field name for multi-container error check (default: "containerName")
|
||||
MultiContainerErrMsg string // custom error message for multi-container mode (default: "container name must be set for %s")
|
||||
CustomPatchContainerBlock string // custom CUE block for PatchContainer body (for complex merge logic)
|
||||
CustomPatchBlock string // custom CUE block for the patch: spec: template: spec: { ... } body
|
||||
CustomParameterBlock string // custom CUE block for the parameter definition
|
||||
PatchStrategy string // if set, emitted as // +patchStrategy=<value> before the patch block (e.g., "open")
|
||||
ParamsTypeName string // custom name for the #PatchParams helper (default: "PatchParams")
|
||||
NoDefaultDisjunction bool // if true, omit the * default marker on parameter disjunction
|
||||
}
|
||||
|
||||
// --- Let Binding Support ---
|
||||
|
||||
@@ -628,6 +628,202 @@ var _ = Describe("PatchContainer", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Context("MultiContainerCheckField and MultiContainerErrMsg", func() {
|
||||
It("should use default check field 'containerName' and default error message with camelCase", func() {
|
||||
trait := defkit.NewTrait("default-multi-err-test").
|
||||
Description("Test default multi-container error").
|
||||
AppliesTo("deployments.apps").
|
||||
Template(func(tpl *defkit.Template) {
|
||||
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
||||
ContainerNameParam: "containerName",
|
||||
DefaultToContextName: true,
|
||||
AllowMultiple: true,
|
||||
ContainersParam: "containers",
|
||||
PatchFields: []defkit.PatchContainerField{
|
||||
{ParamName: "image", TargetField: "image"},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
cue := trait.ToCue()
|
||||
|
||||
// Default check field is "containerName"
|
||||
Expect(cue).To(ContainSubstring(`if c.containerName == ""`))
|
||||
Expect(cue).To(ContainSubstring(`if c.containerName != ""`))
|
||||
// Default error message uses "containerName" (camelCase, matching the field name)
|
||||
Expect(cue).To(ContainSubstring(`err: "containerName must be set for containers"`))
|
||||
})
|
||||
|
||||
It("should use custom MultiContainerCheckField when set", func() {
|
||||
trait := defkit.NewTrait("custom-check-field-test").
|
||||
Description("Test custom check field").
|
||||
AppliesTo("deployments.apps").
|
||||
Template(func(tpl *defkit.Template) {
|
||||
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
||||
ContainerNameParam: "containerName",
|
||||
DefaultToContextName: true,
|
||||
AllowMultiple: true,
|
||||
MultiContainerParam: "probes",
|
||||
MultiContainerCheckField: "name",
|
||||
PatchFields: []defkit.PatchContainerField{
|
||||
{ParamName: "image", TargetField: "image"},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
cue := trait.ToCue()
|
||||
|
||||
// Custom check field "name" instead of default "containerName"
|
||||
Expect(cue).To(ContainSubstring(`if c.name == ""`))
|
||||
Expect(cue).To(ContainSubstring(`if c.name != ""`))
|
||||
Expect(cue).NotTo(ContainSubstring(`c.containerName == ""`))
|
||||
})
|
||||
|
||||
It("should use custom MultiContainerErrMsg when set", func() {
|
||||
trait := defkit.NewTrait("custom-err-msg-test").
|
||||
Description("Test custom error message").
|
||||
AppliesTo("deployments.apps").
|
||||
Template(func(tpl *defkit.Template) {
|
||||
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
||||
ContainerNameParam: "containerName",
|
||||
DefaultToContextName: true,
|
||||
AllowMultiple: true,
|
||||
MultiContainerParam: "probes",
|
||||
MultiContainerCheckField: "name",
|
||||
MultiContainerErrMsg: "containerName must be set when specifying startup probe for multiple containers",
|
||||
PatchFields: []defkit.PatchContainerField{
|
||||
{ParamName: "image", TargetField: "image"},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
cue := trait.ToCue()
|
||||
|
||||
// Custom error message overrides the default
|
||||
Expect(cue).To(ContainSubstring(`err: "containerName must be set when specifying startup probe for multiple containers"`))
|
||||
// Default error message should NOT appear
|
||||
Expect(cue).NotTo(ContainSubstring(`err: "containerName must be set for probes"`))
|
||||
})
|
||||
|
||||
It("should use default error with MultiContainerParam name", func() {
|
||||
trait := defkit.NewTrait("multi-param-err-test").
|
||||
Description("Test error message includes multi param name").
|
||||
AppliesTo("deployments.apps").
|
||||
Template(func(tpl *defkit.Template) {
|
||||
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
||||
ContainerNameParam: "containerName",
|
||||
DefaultToContextName: true,
|
||||
AllowMultiple: true,
|
||||
MultiContainerParam: "probes",
|
||||
PatchFields: []defkit.PatchContainerField{
|
||||
{ParamName: "image", TargetField: "image"},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
cue := trait.ToCue()
|
||||
|
||||
// Default error message should include the multi-container param name
|
||||
Expect(cue).To(ContainSubstring(`err: "containerName must be set for probes"`))
|
||||
})
|
||||
})
|
||||
|
||||
Context("writePatchParamMapping typed scalar fields", func() {
|
||||
It("should map typed scalar fields unconditionally in _params block even with IsSet and no default", func() {
|
||||
trait := defkit.NewTrait("typed-scalar-unconditional-test").
|
||||
Description("Test typed scalar fields pass through unconditionally").
|
||||
AppliesTo("deployments.apps").
|
||||
Template(func(tpl *defkit.Template) {
|
||||
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
||||
ContainerNameParam: "containerName",
|
||||
DefaultToContextName: true,
|
||||
AllowMultiple: true,
|
||||
ContainersParam: "containers",
|
||||
Groups: []defkit.PatchContainerGroup{
|
||||
{
|
||||
TargetField: "startupProbe",
|
||||
Fields: defkit.PatchFields(
|
||||
defkit.PatchField("terminationGracePeriodSeconds").Int().IsSet(),
|
||||
defkit.PatchField("exec").IsSet(),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
cue := trait.ToCue()
|
||||
|
||||
// Int().IsSet() with no default: typed scalar should be unconditional in _params block
|
||||
Expect(cue).To(ContainSubstring("terminationGracePeriodSeconds: parameter.terminationGracePeriodSeconds"))
|
||||
// But untyped IsSet() with no default (e.g., exec) should remain conditional in _params block
|
||||
Expect(cue).To(MatchRegexp(`if parameter\.exec != _\|_\s*\{[^}]*exec:\s*parameter\.exec`))
|
||||
|
||||
// Both should still be conditional in the PatchContainer body
|
||||
Expect(cue).To(ContainSubstring("if _params.terminationGracePeriodSeconds != _|_"))
|
||||
Expect(cue).To(ContainSubstring("if _params.exec != _|_"))
|
||||
})
|
||||
|
||||
It("should keep untyped IsSet fields conditional in _params block", func() {
|
||||
trait := defkit.NewTrait("untyped-conditional-test").
|
||||
Description("Test untyped fields remain conditional").
|
||||
AppliesTo("deployments.apps").
|
||||
Template(func(tpl *defkit.Template) {
|
||||
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
||||
ContainerNameParam: "containerName",
|
||||
DefaultToContextName: true,
|
||||
AllowMultiple: true,
|
||||
ContainersParam: "containers",
|
||||
Groups: []defkit.PatchContainerGroup{
|
||||
{
|
||||
TargetField: "startupProbe",
|
||||
Fields: defkit.PatchFields(
|
||||
defkit.PatchField("exec").IsSet(),
|
||||
defkit.PatchField("httpGet").IsSet(),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
cue := trait.ToCue()
|
||||
|
||||
// Untyped IsSet() fields should still be conditional in _params block
|
||||
Expect(cue).To(MatchRegexp(`if parameter\.exec != _\|_\s*\{[^}]*exec:\s*parameter\.exec`))
|
||||
Expect(cue).To(MatchRegexp(`if parameter\.httpGet != _\|_\s*\{[^}]*httpGet:\s*parameter\.httpGet`))
|
||||
})
|
||||
|
||||
It("should map typed scalar fields with default unconditionally regardless of IsSet", func() {
|
||||
trait := defkit.NewTrait("typed-with-default-test").
|
||||
Description("Test typed fields with default are unconditional").
|
||||
AppliesTo("deployments.apps").
|
||||
Template(func(tpl *defkit.Template) {
|
||||
tpl.UsePatchContainer(defkit.PatchContainerConfig{
|
||||
ContainerNameParam: "containerName",
|
||||
DefaultToContextName: true,
|
||||
AllowMultiple: true,
|
||||
ContainersParam: "containers",
|
||||
Groups: []defkit.PatchContainerGroup{
|
||||
{
|
||||
TargetField: "startupProbe",
|
||||
Fields: defkit.PatchFields(
|
||||
defkit.PatchField("initialDelaySeconds").Int().IsSet().Default("0"),
|
||||
defkit.PatchField("periodSeconds").Int().IsSet().Default("10"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
cue := trait.ToCue()
|
||||
|
||||
// Typed fields with defaults are always unconditional in _params block
|
||||
Expect(cue).To(ContainSubstring("initialDelaySeconds: parameter.initialDelaySeconds"))
|
||||
Expect(cue).To(ContainSubstring("periodSeconds: parameter.periodSeconds"))
|
||||
// Should NOT be wrapped in conditionals in _params block
|
||||
Expect(cue).NotTo(MatchRegexp(`if parameter\.initialDelaySeconds[^{]*\{[^}]*initialDelaySeconds:\s*parameter\.initialDelaySeconds`))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Template Let Bindings", func() {
|
||||
It("should accumulate bindings in order for CUE generation", func() {
|
||||
tpl := defkit.NewTemplate()
|
||||
|
||||
@@ -226,3 +226,30 @@ func (r *Resource) EndIf() *Resource {
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Directive records a CUE directive annotation on a field path.
|
||||
// The directive string should be like "patchKey=ip" and will be rendered as // +patchKey=ip.
|
||||
func (r *Resource) Directive(path string, directive string) *Resource {
|
||||
op := &DirectiveOp{path: path, directive: directive}
|
||||
if r.currentIf != nil {
|
||||
r.currentIf.ops = append(r.currentIf.ops, op)
|
||||
} else {
|
||||
r.ops = append(r.ops, op)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// DirectiveOp records a CUE directive annotation on a field path.
|
||||
// The directive string (e.g. "patchKey=ip") is rendered as // +patchKey=ip.
|
||||
type DirectiveOp struct {
|
||||
path string
|
||||
directive string
|
||||
}
|
||||
|
||||
func (d *DirectiveOp) resourceOp() {}
|
||||
|
||||
// Path returns the field path.
|
||||
func (d *DirectiveOp) Path() string { return d.path }
|
||||
|
||||
// GetDirective returns the directive string.
|
||||
func (d *DirectiveOp) GetDirective() string { return d.directive }
|
||||
|
||||
@@ -202,4 +202,43 @@ var _ = Describe("Resource", func() {
|
||||
Expect(isSpreadIf).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Context("Directive", func() {
|
||||
It("should record a Directive operation", func() {
|
||||
r := defkit.NewResource("apps/v1", "DaemonSet").
|
||||
Directive("spec.template.spec.hostAliases", "patchKey=ip")
|
||||
Expect(r.Ops()).To(HaveLen(1))
|
||||
dirOp, ok := r.Ops()[0].(*defkit.DirectiveOp)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(dirOp.Path()).To(Equal("spec.template.spec.hostAliases"))
|
||||
Expect(dirOp.GetDirective()).To(Equal("patchKey=ip"))
|
||||
})
|
||||
|
||||
It("should record Directive within If block", func() {
|
||||
hostAliases := defkit.Object("hostAliases")
|
||||
r := defkit.NewResource("apps/v1", "DaemonSet").
|
||||
If(hostAliases.IsSet()).
|
||||
Set("spec.template.spec.hostAliases", hostAliases).
|
||||
Directive("spec.template.spec.hostAliases", "patchKey=ip").
|
||||
EndIf()
|
||||
Expect(r.Ops()).To(HaveLen(1))
|
||||
ifBlock, ok := r.Ops()[0].(*defkit.IfBlock)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(ifBlock.Ops()).To(HaveLen(2))
|
||||
_, isDirOp := ifBlock.Ops()[1].(*defkit.DirectiveOp)
|
||||
Expect(isDirOp).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should combine with Set operations", func() {
|
||||
hostAliases := defkit.Object("hostAliases")
|
||||
r := defkit.NewResource("apps/v1", "DaemonSet").
|
||||
SetIf(hostAliases.IsSet(), "spec.template.spec.hostAliases", hostAliases).
|
||||
Directive("spec.template.spec.hostAliases", "patchKey=ip")
|
||||
Expect(r.Ops()).To(HaveLen(2))
|
||||
_, isSetIf := r.Ops()[0].(*defkit.SetIfOp)
|
||||
_, isDirOp := r.Ops()[1].(*defkit.DirectiveOp)
|
||||
Expect(isSetIf).To(BeTrue())
|
||||
Expect(isDirOp).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,9 +98,7 @@ func (s *StatusBuilder) Build() string {
|
||||
|
||||
var parts []string
|
||||
|
||||
for _, f := range s.fields {
|
||||
parts = append(parts, s.buildField(f))
|
||||
}
|
||||
parts = append(parts, s.buildGroupedFields()...)
|
||||
|
||||
if s.message != "" {
|
||||
// Use simple quotes around message - don't use %q which escapes backslashes,
|
||||
@@ -111,6 +109,79 @@ func (s *StatusBuilder) Build() string {
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// buildGroupedFields groups fields by parent prefix and generates consolidated CUE blocks.
|
||||
// Fields with the same parent (e.g., "status.active", "status.failed") are consolidated
|
||||
// into a single block with column-aligned defaults.
|
||||
func (s *StatusBuilder) buildGroupedFields() []string {
|
||||
type fieldGroup struct {
|
||||
parent string
|
||||
fields []*StatusField
|
||||
}
|
||||
|
||||
groups := make([]fieldGroup, 0)
|
||||
groupIndex := make(map[string]int)
|
||||
var simpleFields []*StatusField
|
||||
|
||||
for _, f := range s.fields {
|
||||
parts := strings.Split(f.name, ".")
|
||||
if len(parts) == 2 {
|
||||
parent := parts[0]
|
||||
if idx, ok := groupIndex[parent]; ok {
|
||||
groups[idx].fields = append(groups[idx].fields, f)
|
||||
} else {
|
||||
groupIndex[parent] = len(groups)
|
||||
groups = append(groups, fieldGroup{parent: parent, fields: []*StatusField{f}})
|
||||
}
|
||||
} else {
|
||||
simpleFields = append(simpleFields, f)
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
|
||||
for _, g := range groups {
|
||||
result = append(result, s.buildConsolidatedGroup(g.parent, g.fields))
|
||||
}
|
||||
|
||||
for _, f := range simpleFields {
|
||||
result = append(result, s.buildField(f))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// buildConsolidatedGroup generates a single CUE block for fields sharing a parent prefix.
|
||||
// Field values are column-aligned within the block.
|
||||
func (s *StatusBuilder) buildConsolidatedGroup(parent string, fields []*StatusField) string {
|
||||
var defaults []string
|
||||
var conditionals []string
|
||||
|
||||
// Calculate max child field name length for column alignment
|
||||
maxLen := 0
|
||||
for _, f := range fields {
|
||||
parts := strings.Split(f.name, ".")
|
||||
childName := parts[1]
|
||||
if len(childName) > maxLen {
|
||||
maxLen = len(childName)
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range fields {
|
||||
parts := strings.Split(f.name, ".")
|
||||
childName := parts[1]
|
||||
padding := strings.Repeat(" ", maxLen-len(childName))
|
||||
|
||||
defaults = append(defaults, fmt.Sprintf("\t%s:%s %s", childName, padding, f.defaultExpr()))
|
||||
conditionals = append(conditionals, fmt.Sprintf("\tif context.output.%s != _|_ {\n\t\t%s: context.output.%s\n\t}", f.sourcePath, childName, f.sourcePath))
|
||||
}
|
||||
|
||||
if len(conditionals) == 0 {
|
||||
return fmt.Sprintf("%s: {\n%s\n}", parent, strings.Join(defaults, "\n"))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s: {\n%s\n} & {\n%s\n}", parent, strings.Join(defaults, "\n"), strings.Join(conditionals, "\n"))
|
||||
}
|
||||
|
||||
// RawCUE sets raw CUE for complex status expressions that don't fit the builder pattern.
|
||||
func (s *StatusBuilder) RawCUE(cue string) *StatusBuilder {
|
||||
s.rawCUE = cue
|
||||
@@ -236,7 +307,18 @@ func (h *HealthBuilder) Build() string {
|
||||
parts = append(parts, h.buildGroupedFields()...)
|
||||
|
||||
if len(h.conditions) > 0 {
|
||||
healthExpr := strings.Join(h.conditions, " && ")
|
||||
// When multiple conditions are joined with &&, auto-parenthesize each
|
||||
// for correct precedence and readability. Skip already-parenthesized
|
||||
// conditions (e.g. from StatusOr). Single conditions are left as-is.
|
||||
conditions := h.conditions
|
||||
if len(conditions) > 1 {
|
||||
wrapped := make([]string, len(conditions))
|
||||
for i, c := range conditions {
|
||||
wrapped[i] = parenthesizeCondition(c)
|
||||
}
|
||||
conditions = wrapped
|
||||
}
|
||||
healthExpr := strings.Join(conditions, " && ")
|
||||
if h.useDefault {
|
||||
parts = append(parts, fmt.Sprintf("_isHealth: %s", healthExpr), "isHealth: *_isHealth | bool")
|
||||
} else {
|
||||
@@ -251,6 +333,39 @@ func (h *HealthBuilder) Build() string {
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// parenthesizeCondition wraps a condition string in parentheses if it isn't already
|
||||
// fully enclosed in a matching pair. This prevents double-wrapping conditions that
|
||||
// are already parenthesized (e.g. from StatusOr).
|
||||
func parenthesizeCondition(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if isFullyParenthesized(s) {
|
||||
return s
|
||||
}
|
||||
return "(" + s + ")"
|
||||
}
|
||||
|
||||
// isFullyParenthesized checks whether the string is enclosed by a single matching
|
||||
// pair of parentheses. For example "(a == b)" and "(a || b)" return true, but
|
||||
// "(a) && (b)" returns false because the first closing paren appears before the end.
|
||||
func isFullyParenthesized(s string) bool {
|
||||
if len(s) < 2 || s[0] != '(' || s[len(s)-1] != ')' {
|
||||
return false
|
||||
}
|
||||
depth := 0
|
||||
for i, ch := range s {
|
||||
if ch == '(' {
|
||||
depth++
|
||||
} else if ch == ')' {
|
||||
depth--
|
||||
}
|
||||
// If depth reaches 0 before the last character, the outer parens don't enclose everything
|
||||
if depth == 0 && i < len(s)-1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return depth == 0
|
||||
}
|
||||
|
||||
// buildGroupedFields groups fields by parent prefix and generates consolidated CUE blocks.
|
||||
// Fields with the same parent (e.g., "ready.replicas", "ready.updatedReplicas") are
|
||||
// consolidated into a single block with column-aligned defaults.
|
||||
@@ -402,9 +517,9 @@ func DeploymentHealth() *HealthBuilder {
|
||||
IntField("ready.replicas", "status.replicas", 0).
|
||||
IntField("ready.observedGeneration", "status.observedGeneration", 0).
|
||||
HealthyWhen(
|
||||
"("+StatusEq("context.output.spec.replicas", "ready.readyReplicas")+")",
|
||||
"("+StatusEq("context.output.spec.replicas", "ready.updatedReplicas")+")",
|
||||
"("+StatusEq("context.output.spec.replicas", "ready.replicas")+")",
|
||||
StatusEq("context.output.spec.replicas", "ready.readyReplicas"),
|
||||
StatusEq("context.output.spec.replicas", "ready.updatedReplicas"),
|
||||
StatusEq("context.output.spec.replicas", "ready.replicas"),
|
||||
StatusOr(StatusEq("ready.observedGeneration", "context.output.metadata.generation"), "ready.observedGeneration > context.output.metadata.generation"),
|
||||
).
|
||||
WithDefault().
|
||||
|
||||
@@ -28,9 +28,11 @@ import (
|
||||
var _ = Describe("Status", func() {
|
||||
|
||||
Context("StatusBuilder", func() {
|
||||
It("should create a status builder", func() {
|
||||
It("should create a status builder that produces empty CUE without fields", func() {
|
||||
s := defkit.Status()
|
||||
Expect(s).NotTo(BeNil())
|
||||
cue := s.Build()
|
||||
Expect(cue).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should add IntField to status", func() {
|
||||
@@ -71,9 +73,11 @@ var _ = Describe("Status", func() {
|
||||
})
|
||||
|
||||
Context("HealthBuilder", func() {
|
||||
It("should create a health builder", func() {
|
||||
It("should create a health builder that produces empty CUE without conditions", func() {
|
||||
h := defkit.Health()
|
||||
Expect(h).NotTo(BeNil())
|
||||
cue := h.Build()
|
||||
Expect(cue).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should add IntField through HealthBuilder", func() {
|
||||
@@ -110,11 +114,11 @@ var _ = Describe("Status", func() {
|
||||
Expect(cue).To(ContainSubstring("isHealth: ready.replicas == desired.replicas"))
|
||||
})
|
||||
|
||||
It("should add multiple health conditions", func() {
|
||||
It("should add multiple health conditions with auto-parenthesization", func() {
|
||||
h := defkit.Health().
|
||||
HealthyWhen("condition1", "condition2", "condition3")
|
||||
cue := h.Build()
|
||||
Expect(cue).To(ContainSubstring("isHealth: condition1 && condition2 && condition3"))
|
||||
Expect(cue).To(ContainSubstring("isHealth: (condition1) && (condition2) && (condition3)"))
|
||||
})
|
||||
|
||||
It("should support RawCUE override on health builder", func() {
|
||||
@@ -229,6 +233,49 @@ var _ = Describe("Status", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Context("StatusBuilder field grouping", func() {
|
||||
It("should consolidate multiple fields with same parent into one block", func() {
|
||||
s := defkit.Status().
|
||||
IntField("status.active", "status.active", 0).
|
||||
IntField("status.failed", "status.failed", 0).
|
||||
IntField("status.succeeded", "status.succeeded", 0).
|
||||
Message(`Active/Failed/Succeeded:\(status.active)/\(status.failed)/\(status.succeeded)`)
|
||||
cue := s.Build()
|
||||
Expect(strings.Count(cue, "status: {")).To(Equal(1))
|
||||
Expect(cue).To(ContainSubstring("active:"))
|
||||
Expect(cue).To(ContainSubstring("failed:"))
|
||||
Expect(cue).To(ContainSubstring("succeeded:"))
|
||||
})
|
||||
|
||||
It("should keep different parent prefixes as separate blocks", func() {
|
||||
s := defkit.Status().
|
||||
IntField("ready.replicas", "status.numberReady", 0).
|
||||
IntField("desired.replicas", "status.desiredNumberScheduled", 0).
|
||||
Message(`Ready:\(ready.replicas)/\(desired.replicas)`)
|
||||
cue := s.Build()
|
||||
Expect(strings.Count(cue, "ready: {")).To(Equal(1))
|
||||
Expect(strings.Count(cue, "desired: {")).To(Equal(1))
|
||||
})
|
||||
|
||||
It("should column-align fields within consolidated block", func() {
|
||||
s := defkit.Status().
|
||||
IntField("status.active", "status.active", 0).
|
||||
IntField("status.succeeded", "status.succeeded", 0)
|
||||
cue := s.Build()
|
||||
// "active" is shorter than "succeeded", so it should be padded
|
||||
Expect(cue).To(ContainSubstring("active: *0 | int"))
|
||||
Expect(cue).To(ContainSubstring("succeeded: *0 | int"))
|
||||
})
|
||||
|
||||
It("should handle simple (non-nested) fields without grouping", func() {
|
||||
s := defkit.Status().
|
||||
IntField("succeeded", "status.succeeded", 0)
|
||||
cue := s.Build()
|
||||
Expect(cue).To(ContainSubstring("succeeded: *0 | int"))
|
||||
Expect(cue).To(ContainSubstring("context.output.status.succeeded"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("HealthBuilder field grouping", func() {
|
||||
It("should consolidate multiple fields with same parent into one block", func() {
|
||||
h := defkit.Health().
|
||||
@@ -286,104 +333,202 @@ var _ = Describe("Status", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Context("HealthBuilder auto-parenthesization", func() {
|
||||
It("should NOT parenthesize a single condition", func() {
|
||||
h := defkit.Health().
|
||||
HealthyWhen("ready.replicas == desired.replicas")
|
||||
cue := h.Build()
|
||||
Expect(cue).To(ContainSubstring("isHealth: ready.replicas == desired.replicas"))
|
||||
})
|
||||
|
||||
It("should auto-parenthesize multiple conditions", func() {
|
||||
h := defkit.Health().
|
||||
HealthyWhen("a == b", "c == d")
|
||||
cue := h.Build()
|
||||
Expect(cue).To(ContainSubstring("isHealth: (a == b) && (c == d)"))
|
||||
})
|
||||
|
||||
It("should NOT double-wrap already-parenthesized conditions", func() {
|
||||
h := defkit.Health().
|
||||
HealthyWhen("a == b", defkit.StatusOr("x == y", "x > y"))
|
||||
cue := h.Build()
|
||||
// StatusOr returns "(x == y || x > y)" which is already fully parenthesized
|
||||
Expect(cue).To(ContainSubstring("(a == b) && (x == y || x > y)"))
|
||||
})
|
||||
|
||||
It("should NOT treat partially-parenthesized strings as fully wrapped", func() {
|
||||
// "(a) && (b)" starts with ( and ends with ) but is not fully enclosed
|
||||
h := defkit.Health().
|
||||
HealthyWhen("first", "(a) && (b)")
|
||||
cue := h.Build()
|
||||
Expect(cue).To(ContainSubstring("(first) && ((a) && (b))"))
|
||||
})
|
||||
|
||||
It("should work with StatusEq without manual parens", func() {
|
||||
h := defkit.Health().
|
||||
HealthyWhen(
|
||||
defkit.StatusEq("spec.replicas", "ready.replicas"),
|
||||
defkit.StatusEq("spec.replicas", "ready.updated"),
|
||||
)
|
||||
cue := h.Build()
|
||||
Expect(cue).To(ContainSubstring("(spec.replicas == ready.replicas) && (spec.replicas == ready.updated)"))
|
||||
})
|
||||
|
||||
It("should work with WithDefault and StatusEq", func() {
|
||||
h := defkit.Health().
|
||||
HealthyWhen(
|
||||
defkit.StatusEq("a", "b"),
|
||||
defkit.StatusEq("c", "d"),
|
||||
).
|
||||
WithDefault()
|
||||
cue := h.Build()
|
||||
Expect(cue).To(ContainSubstring("_isHealth: (a == b) && (c == d)"))
|
||||
Expect(cue).To(ContainSubstring("isHealth: *_isHealth | bool"))
|
||||
})
|
||||
|
||||
It("should produce correct CUE for DaemonSetHealth with auto-parens", func() {
|
||||
h := defkit.DaemonSetHealth()
|
||||
cue := h.Build()
|
||||
// Each equality condition should be parenthesized
|
||||
Expect(cue).To(ContainSubstring("(desired.replicas == ready.replicas)"))
|
||||
Expect(cue).To(ContainSubstring("(desired.replicas == updated.replicas)"))
|
||||
Expect(cue).To(ContainSubstring("(desired.replicas == current.replicas)"))
|
||||
// StatusOr is already wrapped, should not be double-wrapped
|
||||
Expect(cue).To(ContainSubstring("(generation.observed == generation.metadata || generation.observed > generation.metadata)"))
|
||||
})
|
||||
|
||||
It("should produce correct CUE for DeploymentHealth with auto-parens", func() {
|
||||
h := defkit.DeploymentHealth()
|
||||
cue := h.Build()
|
||||
Expect(cue).To(ContainSubstring("(context.output.spec.replicas == ready.readyReplicas)"))
|
||||
Expect(cue).To(ContainSubstring("(context.output.spec.replicas == ready.updatedReplicas)"))
|
||||
Expect(cue).To(ContainSubstring("(context.output.spec.replicas == ready.replicas)"))
|
||||
Expect(cue).To(ContainSubstring("_isHealth:"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("HealthBuilder Expressions", func() {
|
||||
It("should create Condition expression", func() {
|
||||
It("should generate Condition expression that checks condition status", func() {
|
||||
h := defkit.Health()
|
||||
cond := h.Condition("Ready")
|
||||
Expect(cond).NotTo(BeNil())
|
||||
expr := h.Condition("Ready").IsTrue()
|
||||
policy := h.Policy(expr)
|
||||
Expect(policy).To(ContainSubstring("Ready"))
|
||||
Expect(policy).To(ContainSubstring("isHealth:"))
|
||||
Expect(policy).To(ContainSubstring(`"True"`))
|
||||
})
|
||||
|
||||
It("should create Field expression", func() {
|
||||
h := defkit.Health()
|
||||
field := h.Field("status.replicas")
|
||||
Expect(field).NotTo(BeNil())
|
||||
})
|
||||
|
||||
It("should create FieldRef expression", func() {
|
||||
h := defkit.Health()
|
||||
ref := h.FieldRef("spec.replicas")
|
||||
Expect(ref).NotTo(BeNil())
|
||||
})
|
||||
|
||||
It("should create Phase expression", func() {
|
||||
It("should generate Phase expression that checks status.phase", func() {
|
||||
h := defkit.Health()
|
||||
expr := h.Phase("Running", "Succeeded")
|
||||
Expect(expr).NotTo(BeNil())
|
||||
policy := h.Policy(expr)
|
||||
Expect(policy).To(ContainSubstring("isHealth:"))
|
||||
Expect(policy).To(ContainSubstring("Running"))
|
||||
Expect(policy).To(ContainSubstring("Succeeded"))
|
||||
Expect(policy).To(ContainSubstring("context.output.status.phase"))
|
||||
})
|
||||
|
||||
It("should create PhaseField expression", func() {
|
||||
It("should generate PhaseField expression with custom field path", func() {
|
||||
h := defkit.Health()
|
||||
expr := h.PhaseField("status.currentPhase", "Active", "Ready")
|
||||
Expect(expr).NotTo(BeNil())
|
||||
policy := h.Policy(expr)
|
||||
Expect(policy).To(ContainSubstring("isHealth:"))
|
||||
Expect(policy).To(ContainSubstring("context.output.status.currentPhase"))
|
||||
Expect(policy).To(ContainSubstring("Active"))
|
||||
Expect(policy).To(ContainSubstring("Ready"))
|
||||
})
|
||||
|
||||
It("should create Exists expression", func() {
|
||||
It("should generate Exists expression that checks field != _|_", func() {
|
||||
h := defkit.Health()
|
||||
expr := h.Exists("status.loadBalancer.ingress")
|
||||
Expect(expr).NotTo(BeNil())
|
||||
policy := h.Policy(expr)
|
||||
Expect(policy).To(ContainSubstring("isHealth:"))
|
||||
Expect(policy).To(ContainSubstring("status.loadBalancer.ingress"))
|
||||
Expect(policy).To(ContainSubstring("!= _|_"))
|
||||
})
|
||||
|
||||
It("should create NotExists expression", func() {
|
||||
It("should generate NotExists expression that checks field == _|_", func() {
|
||||
h := defkit.Health()
|
||||
expr := h.NotExists("status.error")
|
||||
Expect(expr).NotTo(BeNil())
|
||||
policy := h.Policy(expr)
|
||||
Expect(policy).To(ContainSubstring("isHealth:"))
|
||||
Expect(policy).To(ContainSubstring("status.error"))
|
||||
Expect(policy).To(ContainSubstring("== _|_"))
|
||||
})
|
||||
|
||||
It("should create And expression", func() {
|
||||
It("should generate And expression combining multiple conditions", func() {
|
||||
h := defkit.Health()
|
||||
expr1 := h.Condition("Ready").IsTrue()
|
||||
expr2 := h.Condition("Synced").IsTrue()
|
||||
and := h.And(expr1, expr2)
|
||||
Expect(and).NotTo(BeNil())
|
||||
policy := h.Policy(and)
|
||||
Expect(policy).To(ContainSubstring("Ready"))
|
||||
Expect(policy).To(ContainSubstring("Synced"))
|
||||
Expect(policy).To(ContainSubstring("&&"))
|
||||
})
|
||||
|
||||
It("should create Or expression", func() {
|
||||
It("should generate Or expression combining multiple conditions", func() {
|
||||
h := defkit.Health()
|
||||
expr1 := h.Phase("Running")
|
||||
expr2 := h.Phase("Succeeded")
|
||||
or := h.Or(expr1, expr2)
|
||||
Expect(or).NotTo(BeNil())
|
||||
policy := h.Policy(or)
|
||||
Expect(policy).To(ContainSubstring("Running"))
|
||||
Expect(policy).To(ContainSubstring("Succeeded"))
|
||||
Expect(policy).To(ContainSubstring("||"))
|
||||
})
|
||||
|
||||
It("should create Not expression", func() {
|
||||
It("should generate Not expression negating a condition", func() {
|
||||
h := defkit.Health()
|
||||
expr := h.Condition("Stalled").IsTrue()
|
||||
not := h.Not(expr)
|
||||
Expect(not).NotTo(BeNil())
|
||||
policy := h.Policy(not)
|
||||
Expect(policy).To(ContainSubstring("Stalled"))
|
||||
Expect(policy).To(ContainSubstring("!"))
|
||||
})
|
||||
|
||||
It("should create Always expression", func() {
|
||||
It("should generate Always expression as isHealth: true", func() {
|
||||
h := defkit.Health()
|
||||
expr := h.Always()
|
||||
Expect(expr).NotTo(BeNil())
|
||||
policy := h.Policy(expr)
|
||||
Expect(policy).To(ContainSubstring("isHealth: true"))
|
||||
})
|
||||
|
||||
It("should create AllTrue expression", func() {
|
||||
It("should generate AllTrue expression checking multiple conditions are True", func() {
|
||||
h := defkit.Health()
|
||||
expr := h.AllTrue("Ready", "Synced", "Available")
|
||||
Expect(expr).NotTo(BeNil())
|
||||
policy := h.Policy(expr)
|
||||
Expect(policy).To(ContainSubstring("Ready"))
|
||||
Expect(policy).To(ContainSubstring("Synced"))
|
||||
Expect(policy).To(ContainSubstring("Available"))
|
||||
Expect(policy).To(ContainSubstring("&&"))
|
||||
})
|
||||
|
||||
It("should create AnyTrue expression", func() {
|
||||
It("should generate AnyTrue expression checking any condition is True", func() {
|
||||
h := defkit.Health()
|
||||
expr := h.AnyTrue("Ready", "Available")
|
||||
Expect(expr).NotTo(BeNil())
|
||||
policy := h.Policy(expr)
|
||||
Expect(policy).To(ContainSubstring("Ready"))
|
||||
Expect(policy).To(ContainSubstring("Available"))
|
||||
Expect(policy).To(ContainSubstring("||"))
|
||||
})
|
||||
|
||||
It("should set health condition with HealthyWhenExpr", func() {
|
||||
It("should set health condition with HealthyWhenExpr and generate correct CUE", func() {
|
||||
h := defkit.Health()
|
||||
expr := h.Condition("Ready").IsTrue()
|
||||
h.HealthyWhenExpr(expr)
|
||||
cue := h.Build()
|
||||
Expect(cue).NotTo(BeEmpty())
|
||||
Expect(cue).To(ContainSubstring("isHealth:"))
|
||||
Expect(cue).To(ContainSubstring("Ready"))
|
||||
Expect(cue).To(ContainSubstring(`"True"`))
|
||||
})
|
||||
|
||||
It("should generate policy from expression", func() {
|
||||
It("should generate policy with correct isHealth expression from Condition", func() {
|
||||
h := defkit.Health()
|
||||
expr := h.Condition("Ready").IsTrue()
|
||||
policy := h.Policy(expr)
|
||||
Expect(policy).NotTo(BeEmpty())
|
||||
Expect(policy).To(ContainSubstring("isHealth:"))
|
||||
Expect(policy).To(ContainSubstring("Ready"))
|
||||
Expect(policy).To(ContainSubstring(`"True"`))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -42,10 +42,12 @@ type TraitDefinition struct {
|
||||
baseDefinition // embedded common fields (name, description, params, template, etc.)
|
||||
appliesToWorkloads []string // e.g., ["deployments.apps", "statefulsets.apps"]
|
||||
conflictsWith []string // traits that conflict with this one
|
||||
conflictsWithSet bool // true when ConflictsWith() was explicitly called
|
||||
podDisruptive bool // whether applying this trait causes pod restart
|
||||
stage string // "PreDispatch" or "PostDispatch" (default: "")
|
||||
templateBlock string // raw CUE for template: block only (uses fluent API for header)
|
||||
labels map[string]string // metadata labels for the trait definition
|
||||
workloadRefPath *string // workloadRefPath attribute (nil = omit, pointer to distinguish from empty)
|
||||
}
|
||||
|
||||
// NewTrait creates a new TraitDefinition builder.
|
||||
@@ -75,6 +77,7 @@ func (t *TraitDefinition) AppliesTo(workloads ...string) *TraitDefinition {
|
||||
|
||||
// ConflictsWith specifies traits that cannot be used together with this trait.
|
||||
func (t *TraitDefinition) ConflictsWith(traits ...string) *TraitDefinition {
|
||||
t.conflictsWithSet = true
|
||||
t.conflictsWith = append(t.conflictsWith, traits...)
|
||||
return t
|
||||
}
|
||||
@@ -85,6 +88,12 @@ func (t *TraitDefinition) PodDisruptive(disruptive bool) *TraitDefinition {
|
||||
return t
|
||||
}
|
||||
|
||||
// WorkloadRefPath sets the workloadRefPath attribute for the trait.
|
||||
func (t *TraitDefinition) WorkloadRefPath(path string) *TraitDefinition {
|
||||
t.workloadRefPath = &path
|
||||
return t
|
||||
}
|
||||
|
||||
// Stage sets the trait application stage.
|
||||
// Use "PreDispatch" for traits that must run before dispatch,
|
||||
// "PostDispatch" for traits that run after (e.g., creating Services).
|
||||
@@ -412,13 +421,17 @@ func (g *TraitCUEGenerator) GenerateFullDefinition(t *TraitDefinition) string {
|
||||
sb.WriteString(fmt.Sprintf("%stype: \"trait\"\n", g.indent))
|
||||
sb.WriteString(fmt.Sprintf("%sannotations: {}\n", g.indent))
|
||||
|
||||
// Write labels block only if there are labels (omit empty labels to match original CUE format)
|
||||
if len(t.GetLabels()) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
|
||||
for k, v := range t.GetLabels() {
|
||||
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, v))
|
||||
// Write labels block when Labels() was explicitly called (nil check distinguishes unset from empty)
|
||||
if t.labels != nil {
|
||||
if len(t.labels) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
|
||||
for k, v := range t.labels {
|
||||
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, v))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%slabels: {}\n", g.indent))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%sdescription: %q\n", g.indent, t.GetDescription()))
|
||||
|
||||
@@ -439,10 +452,8 @@ func (g *TraitCUEGenerator) GenerateFullDefinition(t *TraitDefinition) string {
|
||||
func (g *TraitCUEGenerator) writeAttributes(sb *strings.Builder, t *TraitDefinition, depth int) {
|
||||
indent := strings.Repeat(g.indent, depth)
|
||||
|
||||
// podDisruptive (only emit when true, false is the default)
|
||||
if t.IsPodDisruptive() {
|
||||
sb.WriteString(fmt.Sprintf("%spodDisruptive: %v\n", indent, t.IsPodDisruptive()))
|
||||
}
|
||||
// podDisruptive (always emit — vela CUE always includes this attribute)
|
||||
sb.WriteString(fmt.Sprintf("%spodDisruptive: %v\n", indent, t.IsPodDisruptive()))
|
||||
|
||||
// stage (if set)
|
||||
if t.GetStage() != "" {
|
||||
@@ -458,13 +469,17 @@ func (g *TraitCUEGenerator) writeAttributes(sb *strings.Builder, t *TraitDefinit
|
||||
sb.WriteString(fmt.Sprintf("%sappliesToWorkloads: [%s]\n", indent, strings.Join(workloads, ", ")))
|
||||
}
|
||||
|
||||
// conflictsWith (if set)
|
||||
if len(t.GetConflictsWith()) > 0 {
|
||||
conflicts := make([]string, len(t.GetConflictsWith()))
|
||||
for i, c := range t.GetConflictsWith() {
|
||||
conflicts[i] = fmt.Sprintf("%q", c)
|
||||
// conflictsWith (emit when explicitly set, even if empty)
|
||||
if t.conflictsWithSet {
|
||||
if len(t.GetConflictsWith()) > 0 {
|
||||
conflicts := make([]string, len(t.GetConflictsWith()))
|
||||
for i, c := range t.GetConflictsWith() {
|
||||
conflicts[i] = fmt.Sprintf("%q", c)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%sconflictsWith: [%s]\n", indent, strings.Join(conflicts, ", ")))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%sconflictsWith: []\n", indent))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%sconflictsWith: [%s]\n", indent, strings.Join(conflicts, ", ")))
|
||||
}
|
||||
|
||||
// status (customStatus and healthPolicy)
|
||||
@@ -490,6 +505,11 @@ func (g *TraitCUEGenerator) writeAttributes(sb *strings.Builder, t *TraitDefinit
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
}
|
||||
|
||||
// workloadRefPath (if explicitly set)
|
||||
if t.workloadRefPath != nil {
|
||||
sb.WriteString(fmt.Sprintf("%sworkloadRefPath: %q\n", indent, *t.workloadRefPath))
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateTemplate generates the template block for a trait.
|
||||
@@ -1098,7 +1118,7 @@ func (g *TraitCUEGenerator) generateParameterBlock(t *TraitDefinition, depth int
|
||||
if dynMap.GetValueTypeUnion() != "" {
|
||||
typeStr = dynMap.GetValueTypeUnion()
|
||||
} else {
|
||||
typeStr = cueTypeForParamType(dynMap.GetValueType())
|
||||
typeStr = cueTypeStr(dynMap.GetValueType())
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%sparameter: [string]: %s\n", indent, typeStr))
|
||||
return sb.String()
|
||||
@@ -1123,28 +1143,6 @@ func (g *TraitCUEGenerator) generateParameterBlock(t *TraitDefinition, depth int
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// cueTypeForParamType converts a ParamType to its CUE type string (standalone function).
|
||||
func cueTypeForParamType(pt ParamType) string {
|
||||
switch pt {
|
||||
case ParamTypeString:
|
||||
return string(ParamTypeString)
|
||||
case ParamTypeInt:
|
||||
return "int"
|
||||
case ParamTypeBool:
|
||||
return "bool"
|
||||
case ParamTypeFloat:
|
||||
return "float"
|
||||
case ParamTypeArray:
|
||||
return "[...]"
|
||||
case ParamTypeMap:
|
||||
return "{...}"
|
||||
case ParamTypeStruct:
|
||||
return "{...}"
|
||||
default:
|
||||
return "_"
|
||||
}
|
||||
}
|
||||
|
||||
// writePatchContainerPattern generates the complete PatchContainer pattern in CUE.
|
||||
// This generates the #PatchParams helper, PatchContainer definition, patch block,
|
||||
// parameter schema, and errs aggregation.
|
||||
@@ -1153,10 +1151,16 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf
|
||||
innerIndent := strings.Repeat(g.indent, depth+1)
|
||||
deepIndent := strings.Repeat(g.indent, depth+2)
|
||||
|
||||
// Generate #PatchParams helper definition
|
||||
// Determine the helper type name (default: "PatchParams")
|
||||
paramsTypeName := config.ParamsTypeName
|
||||
if paramsTypeName == "" {
|
||||
paramsTypeName = "PatchParams"
|
||||
}
|
||||
|
||||
// Generate helper definition
|
||||
if config.CustomParamsBlock != "" {
|
||||
// Use custom params block (for complex schemas like startup-probe)
|
||||
sb.WriteString(fmt.Sprintf("%s#PatchParams: {\n", indent))
|
||||
sb.WriteString(fmt.Sprintf("%s#%s: {\n", indent, paramsTypeName))
|
||||
sb.WriteString(fmt.Sprintf("%s// +usage=Specify the name of the target container, if not set, use the component name\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%scontainerName: *\"\" | string\n", innerIndent))
|
||||
// Write each line of custom params block with proper indentation
|
||||
@@ -1165,7 +1169,7 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s#PatchParams: {\n", indent))
|
||||
sb.WriteString(fmt.Sprintf("%s#%s: {\n", indent, paramsTypeName))
|
||||
sb.WriteString(fmt.Sprintf("%s// +usage=Specify the name of the target container, if not set, use the component name\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%scontainerName: *\"\" | string\n", innerIndent))
|
||||
|
||||
@@ -1192,11 +1196,10 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%sPatchContainer: {\n", indent))
|
||||
sb.WriteString(fmt.Sprintf("%s_params: #PatchParams\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s_params: #%s\n", innerIndent, paramsTypeName))
|
||||
sb.WriteString(fmt.Sprintf("%sname: _params.containerName\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s_baseContainers: context.output.spec.template.spec.containers\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s_matchContainers_: [for _container_ in _baseContainers if _container_.name == name {_container_}]\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s_baseContainer: *_|_ | {...}\n", innerIndent))
|
||||
|
||||
// Container not found error
|
||||
sb.WriteString(fmt.Sprintf("%sif len(_matchContainers_) == 0 {\n", innerIndent))
|
||||
@@ -1276,10 +1279,18 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf
|
||||
sb.WriteString(fmt.Sprintf("%sif parameter.%s != _|_ {\n", innerIndent, multiParam))
|
||||
sb.WriteString(fmt.Sprintf("%s\t// +patchKey=name\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s\tcontainers: [for c in parameter.%s {\n", innerIndent, multiParam))
|
||||
sb.WriteString(fmt.Sprintf("%s\t\tif c.containerName == \"\" {\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s\t\t\terr: \"containerName must be set for %s\"\n", innerIndent, multiParam))
|
||||
checkField := "containerName"
|
||||
if config.MultiContainerCheckField != "" {
|
||||
checkField = config.MultiContainerCheckField
|
||||
}
|
||||
errMsg := fmt.Sprintf("containerName must be set for %s", multiParam)
|
||||
if config.MultiContainerErrMsg != "" {
|
||||
errMsg = config.MultiContainerErrMsg
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s\t\tif c.%s == \"\" {\n", innerIndent, checkField))
|
||||
sb.WriteString(fmt.Sprintf("%s\t\t\terr: \"%s\"\n", innerIndent, errMsg))
|
||||
sb.WriteString(fmt.Sprintf("%s\t\t}\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s\t\tif c.containerName != \"\" {\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s\t\tif c.%s != \"\" {\n", innerIndent, checkField))
|
||||
sb.WriteString(fmt.Sprintf("%s\t\t\tPatchContainer & {_params: c}\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s\t\t}\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s\t}]\n", innerIndent))
|
||||
@@ -1336,16 +1347,20 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf
|
||||
}
|
||||
}
|
||||
case config.AllowMultiple && multiParam != "":
|
||||
sb.WriteString(fmt.Sprintf("%sparameter: *#PatchParams | close({\n", indent))
|
||||
defaultMarker := "*"
|
||||
if config.NoDefaultDisjunction {
|
||||
defaultMarker = ""
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%sparameter: %s#%s | close({\n", indent, defaultMarker, paramsTypeName))
|
||||
containersDesc := config.ContainersDescription
|
||||
if containersDesc == "" {
|
||||
containersDesc = "Specify the settings for multiple containers"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s// +usage=%s\n", innerIndent, containersDesc))
|
||||
sb.WriteString(fmt.Sprintf("%s%s: [...#PatchParams]\n", innerIndent, multiParam))
|
||||
sb.WriteString(fmt.Sprintf("%s%s: [...#%s]\n", innerIndent, multiParam, paramsTypeName))
|
||||
sb.WriteString(fmt.Sprintf("%s})\n", indent))
|
||||
default:
|
||||
sb.WriteString(fmt.Sprintf("%sparameter: #PatchParams\n", indent))
|
||||
sb.WriteString(fmt.Sprintf("%sparameter: #%s\n", indent, paramsTypeName))
|
||||
}
|
||||
|
||||
// Generate errs aggregation
|
||||
@@ -1457,7 +1472,7 @@ func (g *TraitCUEGenerator) writePatchContainerGroup(sb *strings.Builder, group
|
||||
// propagating _|_ when the parameter is unset. Fields with defaults or value-based
|
||||
// conditions (like '!= ""') always have a value and can be mapped unconditionally.
|
||||
func (g *TraitCUEGenerator) writePatchParamMapping(sb *strings.Builder, field PatchContainerField, indent string, prefix string) {
|
||||
if field.Condition == "!= _|_" && field.ParamDefault == "" {
|
||||
if field.Condition == "!= _|_" && field.ParamDefault == "" && field.ParamType == "" {
|
||||
sb.WriteString(fmt.Sprintf("%sif %s%s %s {\n", indent, prefix, field.ParamName, field.Condition))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: %s%s\n", indent, field.ParamName, prefix, field.ParamName))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
@@ -1497,13 +1512,17 @@ func (g *TraitCUEGenerator) GenerateDefinitionWithRawTemplate(t *TraitDefinition
|
||||
sb.WriteString(fmt.Sprintf("%stype: \"trait\"\n", g.indent))
|
||||
sb.WriteString(fmt.Sprintf("%sannotations: {}\n", g.indent))
|
||||
|
||||
// Write labels block only if there are labels (omit empty labels to match original CUE format)
|
||||
if len(t.GetLabels()) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
|
||||
for k, v := range t.GetLabels() {
|
||||
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, v))
|
||||
// Write labels block when Labels() was explicitly called (nil check distinguishes unset from empty)
|
||||
if t.labels != nil {
|
||||
if len(t.labels) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
|
||||
for k, v := range t.labels {
|
||||
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, v))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%slabels: {}\n", g.indent))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%sdescription: %q\n", g.indent, t.GetDescription()))
|
||||
|
||||
|
||||
@@ -187,8 +187,8 @@ parameter: #PatchParams
|
||||
Expect(cue).To(ContainSubstring(`scaler: {`))
|
||||
Expect(cue).To(ContainSubstring(`type: "trait"`))
|
||||
Expect(cue).To(ContainSubstring(`description: "Scale workloads"`))
|
||||
// podDisruptive: false is not emitted (it's the default)
|
||||
Expect(cue).NotTo(ContainSubstring(`podDisruptive: false`))
|
||||
// podDisruptive is always emitted
|
||||
Expect(cue).To(ContainSubstring(`podDisruptive: false`))
|
||||
Expect(cue).To(ContainSubstring(`appliesToWorkloads: ["deployments.apps"]`))
|
||||
})
|
||||
|
||||
@@ -1375,4 +1375,247 @@ template: {
|
||||
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 not emit labels when never called", func() {
|
||||
trait := defkit.NewTrait("no-labels").
|
||||
Description("Trait without labels").
|
||||
AppliesTo("deployments.apps")
|
||||
|
||||
cue := trait.ToCue()
|
||||
Expect(cue).NotTo(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 no _baseContainer singular", func() {
|
||||
It("should use _baseContainers (plural) not _baseContainer (singular)", 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).NotTo(ContainSubstring("_baseContainer:"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user