mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-19 04:26:39 +00:00
Feat: add ClosedUnion and ClosedStruct support in defkit (#7069)
* feat: add ClosedUnion and ClosedStruct support in defkit - Introduced ClosedUnionParam and ClosedStructOption to handle closed struct disjunctions in parameters. - Implemented methods for creating closed structs and adding fields. - Enhanced CUE generation to support closed unions, allowing for more complex parameter definitions. - Added tests for ClosedUnion and ClosedStruct to ensure correct functionality and integration. - Updated existing tests to cover new features and ensure backward compatibility. Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: enhance CUE generation with deduplication and inner braces for array elements Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: enhance WorkflowStep parameter handling in CUE generation Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: add WithDirectFields method to BuiltinActionBuilder for direct field rendering Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: streamline string formatting in CUE generation for improved readability Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: enhance CUE generation with deduplication support and improve test assertions Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: add support for ClosedUnion and condition rendering in CUE generation Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * ci: retrigger checks Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: refactor value rendering in CUE generation to improve builder support Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: add GuardedBlockAction and corresponding CUE generation support Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: update writeOneOfParam and writeStructField to use marker instead of optional Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: add ClosedUnionParam support and update related tests for optionality Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: update parameter definitions to remove required constraints in example code Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: enhance CUE generation with multiline value indentation and improved block handling Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: add baseContainer definition to CUE generation for container patterns Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * fix: update test to expect _baseContainer singular field in PatchContainer output The _baseContainer: *_|_ | {...} field was intentionally added to CUE generation but the test was still asserting it should not exist. Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * ci: retrigger checks Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> --------- Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
This commit is contained in:
+4
-4
@@ -802,7 +802,7 @@ require github.com/oam-dev/kubevela v1.10.0
|
||||
// func myComponent() *defkit.ComponentDefinition {
|
||||
// return defkit.NewComponent("my-component").
|
||||
// Description("My component description").
|
||||
// WithParameter("image", defkit.String().Required()).
|
||||
// WithParameter("image", defkit.String()).
|
||||
// Template(func(tpl *defkit.Template) {
|
||||
// // Generate Kubernetes resources
|
||||
// })
|
||||
@@ -823,7 +823,7 @@ package components
|
||||
// return defkit.NewTrait("my-trait").
|
||||
// Description("My trait description").
|
||||
// AppliesToWorkloads("deployments.apps").
|
||||
// WithParameter("replicas", defkit.Int().Required()).
|
||||
// WithParameter("replicas", defkit.Int()).
|
||||
// PatchTemplate(func(tpl *defkit.PatchTemplate) {
|
||||
// // Patch the component output
|
||||
// })
|
||||
@@ -843,7 +843,7 @@ package traits
|
||||
// func myPolicy() *defkit.PolicyDefinition {
|
||||
// return defkit.NewPolicy("my-policy").
|
||||
// Description("My policy description").
|
||||
// WithParameter("clusters", defkit.StringArray().Required())
|
||||
// WithParameter("clusters", defkit.StringArray())
|
||||
// }
|
||||
package policies
|
||||
`
|
||||
@@ -860,7 +860,7 @@ package policies
|
||||
// func myStep() *defkit.WorkflowStepDefinition {
|
||||
// return defkit.NewWorkflowStep("my-step").
|
||||
// Description("My workflow step description").
|
||||
// WithParameter("message", defkit.String().Required())
|
||||
// WithParameter("message", defkit.String())
|
||||
// }
|
||||
package workflowsteps
|
||||
`
|
||||
|
||||
@@ -480,6 +480,25 @@ func (g *CUEGenerator) writeHelperDefFromParam(sb *strings.Builder, param Param,
|
||||
} else {
|
||||
sb.WriteString("[...]\n")
|
||||
}
|
||||
case *ClosedUnionParam:
|
||||
// For closed unions, write as: close({...}) | close({...})
|
||||
options := p.GetOptions()
|
||||
if len(options) == 0 {
|
||||
sb.WriteString("_\n")
|
||||
} else {
|
||||
indent := strings.Repeat(g.indent, depth)
|
||||
for i, opt := range options {
|
||||
if i > 0 {
|
||||
sb.WriteString(" | ")
|
||||
}
|
||||
sb.WriteString("close({\n")
|
||||
for _, field := range opt.GetFields() {
|
||||
g.writeStructFieldForHelper(sb, field, depth+1)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s})", indent))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
case *IntParam:
|
||||
// For int types with optional constraints: int & >=1 & <=65535
|
||||
var constraints []string
|
||||
@@ -925,7 +944,13 @@ func (g *CUEGenerator) writeCollectionOpHelper(sb *strings.Builder, col *Collect
|
||||
if dedup, ok := op.(*dedupeOp); ok {
|
||||
// For dedupe, use the elegant CUE pattern with _ignore marker
|
||||
// This pattern checks if any earlier item has the same key
|
||||
var sourceName string
|
||||
if helperRef, ok := col.Source().(*HelperVar); ok {
|
||||
sourceName = helperRef.Name()
|
||||
} else if ref, ok := col.Source().(*Ref); ok {
|
||||
sourceName = ref.Path()
|
||||
}
|
||||
if sourceName != "" {
|
||||
keyField := dedup.keyField
|
||||
sb.WriteString(fmt.Sprintf(`[
|
||||
for val in [
|
||||
@@ -938,7 +963,7 @@ func (g *CUEGenerator) writeCollectionOpHelper(sb *strings.Builder, col *Collect
|
||||
] if val._ignore == _|_ {
|
||||
val
|
||||
},
|
||||
]`, helperRef.Name(), helperRef.Name(), keyField, keyField))
|
||||
]`, sourceName, sourceName, keyField, keyField))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1807,8 +1832,25 @@ func (g *CUEGenerator) filterNodeByCondition(node *fieldNode, condStr string) *f
|
||||
return filtered
|
||||
}
|
||||
|
||||
// tryRenderBuilder checks if a Value implements CUERenderer or CUEConditionRenderer
|
||||
// and renders it. Returns empty string if the value is not a builder.
|
||||
func (g *CUEGenerator) tryRenderBuilder(v Value) string {
|
||||
if ccr, ok := v.(CUEConditionRenderer); ok {
|
||||
return ccr.RenderCUEWithCondition(g.valueToCUE, g.conditionToCUE)
|
||||
}
|
||||
if cr, ok := v.(CUERenderer); ok {
|
||||
return cr.RenderCUE(g.valueToCUE)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// valueToCUE converts a Value to CUE syntax.
|
||||
func (g *CUEGenerator) valueToCUE(v Value) string {
|
||||
// Check if the value can render itself (used by fluent builders).
|
||||
if s := g.tryRenderBuilder(v); s != "" {
|
||||
return s
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case *Literal:
|
||||
return formatCUEValue(val.Val())
|
||||
@@ -1982,14 +2024,14 @@ func (g *CUEGenerator) arrayElementToCUEWithDepth(elem *ArrayElement, depth int)
|
||||
|
||||
sb.WriteString("{\n")
|
||||
for key, val := range elem.Fields() {
|
||||
valStr := g.valueToCUE(val)
|
||||
valStr := indentMultilineValue(g.valueToCUE(val), innerIndent)
|
||||
sb.WriteString(fmt.Sprintf("%s%s: %s\n", innerIndent, key, valStr))
|
||||
}
|
||||
// Write conditional operations
|
||||
for _, op := range elem.Ops() {
|
||||
if setIf, ok := op.(*SetIfOp); ok {
|
||||
condStr := g.conditionToCUE(setIf.Cond())
|
||||
valStr := g.valueToCUE(setIf.Value())
|
||||
valStr := indentMultilineValue(g.valueToCUE(setIf.Value()), innerIndent+"\t")
|
||||
// Convert dot-separated path to CUE shorthand syntax: "a.b.c" -> "a: b: c"
|
||||
cuePath := strings.ReplaceAll(setIf.Path(), ".", ": ")
|
||||
sb.WriteString(fmt.Sprintf("%sif %s {\n", innerIndent, condStr))
|
||||
@@ -2000,13 +2042,30 @@ func (g *CUEGenerator) arrayElementToCUEWithDepth(elem *ArrayElement, depth int)
|
||||
// Write patchKey-annotated fields (nested patchKey inside array elements)
|
||||
for _, pkf := range elem.PatchKeyFields() {
|
||||
sb.WriteString(fmt.Sprintf("%s// +patchKey=%s\n", innerIndent, pkf.key))
|
||||
valStr := g.valueToCUEAtDepth(pkf.value, depth+1)
|
||||
valStr := indentMultilineValue(g.valueToCUEAtDepth(pkf.value, depth+1), innerIndent)
|
||||
sb.WriteString(fmt.Sprintf("%s%s: %s\n", innerIndent, pkf.field, valStr))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}", indent))
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// indentMultilineValue prepends indent to every line of s after the first.
|
||||
// This is needed when embedding a multi-line valueToCUE result into an
|
||||
// already-indented context: the first line sits on the same line as the
|
||||
// key, but subsequent lines need the surrounding indentation added.
|
||||
func indentMultilineValue(s, indent string) string {
|
||||
if !strings.Contains(s, "\n") {
|
||||
return s
|
||||
}
|
||||
lines := strings.Split(s, "\n")
|
||||
for i := 1; i < len(lines); i++ {
|
||||
if lines[i] != "" {
|
||||
lines[i] = indent + lines[i]
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// arrayBuilderToCUE converts an ArrayBuilder to CUE syntax.
|
||||
// Generates: [{static}, if cond {{conditional}}, if guard for m in source {iterated}]
|
||||
func (g *CUEGenerator) arrayBuilderToCUE(ab *ArrayBuilder, depth int) string {
|
||||
@@ -2040,10 +2099,13 @@ func (g *CUEGenerator) arrayBuilderToCUE(ab *ArrayBuilder, depth int) string {
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%sfor m in %s {\n", innerIndent, sourceStr))
|
||||
}
|
||||
// Wrap each element in inner braces for explicit struct boundary
|
||||
extraIndent := deepIndent + "\t"
|
||||
sb.WriteString(fmt.Sprintf("%s{\n", deepIndent))
|
||||
// Write each field from the element template
|
||||
for key, val := range entry.element.Fields() {
|
||||
valStr := g.valueToCUE(val)
|
||||
sb.WriteString(fmt.Sprintf("%s%s: %s\n", deepIndent, key, valStr))
|
||||
sb.WriteString(fmt.Sprintf("%s%s: %s\n", extraIndent, key, valStr))
|
||||
}
|
||||
// Write conditional operations
|
||||
for _, op := range entry.element.Ops() {
|
||||
@@ -2051,11 +2113,12 @@ func (g *CUEGenerator) arrayBuilderToCUE(ab *ArrayBuilder, depth int) string {
|
||||
condStr := g.conditionToCUE(setIf.Cond())
|
||||
valStr := g.valueToCUE(setIf.Value())
|
||||
cuePath := strings.ReplaceAll(setIf.Path(), ".", ": ")
|
||||
sb.WriteString(fmt.Sprintf("%sif %s {\n", deepIndent, condStr))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", deepIndent, cuePath, valStr))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", deepIndent))
|
||||
sb.WriteString(fmt.Sprintf("%sif %s {\n", extraIndent, condStr))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", extraIndent, cuePath, valStr))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", extraIndent))
|
||||
}
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", deepIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s},\n", innerIndent))
|
||||
|
||||
case entryForEachWith:
|
||||
@@ -2117,6 +2180,15 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for deduplication
|
||||
var dedupeKeyField string
|
||||
for _, op := range ops {
|
||||
if dedup, ok := op.(*dedupeOp); ok {
|
||||
dedupeKeyField = dedup.keyField
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Build filter condition if present; AND-compose multiple filters
|
||||
filterCondition := ""
|
||||
for _, op := range ops {
|
||||
@@ -2147,6 +2219,30 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Dedupe: render the nested-comprehension pattern.
|
||||
// This is placed after all op detection so that guard/filter/map are not bypassed.
|
||||
if dedupeKeyField != "" {
|
||||
var sb strings.Builder
|
||||
// Apply guard if present
|
||||
if guard := col.GetGuard(); guard != nil {
|
||||
guardStr := g.conditionToCUE(guard)
|
||||
sb.WriteString(fmt.Sprintf("if %s ", guardStr))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(`[
|
||||
for val in [
|
||||
for i, vi in %s {
|
||||
for j, vj in %s if j < i && vi.%s == vj.%s {
|
||||
_ignore: true
|
||||
}
|
||||
vi
|
||||
},
|
||||
] if val._ignore == _|_ {
|
||||
val
|
||||
},
|
||||
]`, sourceStr, sourceStr, dedupeKeyField, dedupeKeyField))
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Build the list comprehension
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[")
|
||||
@@ -2791,6 +2887,8 @@ func (g *CUEGenerator) writeParam(sb *strings.Builder, param Param, depth int) {
|
||||
g.writeEnumParam(sb, p, indent, name, marker)
|
||||
case *OneOfParam:
|
||||
g.writeOneOfParam(sb, p, indent, name, marker, depth)
|
||||
case *ClosedUnionParam:
|
||||
g.writeClosedUnionParam(sb, p, indent, name, marker, depth)
|
||||
default:
|
||||
// Generic fallback
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: _\n", indent, name, marker))
|
||||
@@ -3058,6 +3156,16 @@ func (g *CUEGenerator) writeStructField(sb *strings.Builder, f *StructField, dep
|
||||
|
||||
fieldType := g.cueTypeForParamType(f.FieldType())
|
||||
|
||||
// Check schemaRef first — references a helper definition like #HealthProbe
|
||||
if schemaRef := f.GetSchemaRef(); schemaRef != "" {
|
||||
if f.FieldType() == ParamTypeArray {
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: [...#%s]\n", indent, name, marker, schemaRef))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: #%s\n", indent, name, marker, schemaRef))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
nested := f.GetNested()
|
||||
switch {
|
||||
case nested != nil:
|
||||
@@ -3181,6 +3289,31 @@ func (g *CUEGenerator) writeOneOfParam(sb *strings.Builder, p *OneOfParam, inden
|
||||
}
|
||||
}
|
||||
|
||||
// writeClosedUnionParam writes a closed struct disjunction parameter.
|
||||
// It generates CUE of the form: name: close({...}) | close({...})
|
||||
func (g *CUEGenerator) writeClosedUnionParam(sb *strings.Builder, p *ClosedUnionParam, indent, name, optional string, depth int) {
|
||||
options := p.GetOptions()
|
||||
if len(options) == 0 {
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: _\n", indent, name, optional))
|
||||
return
|
||||
}
|
||||
|
||||
// Open the field assignment
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: ", indent, name, optional))
|
||||
|
||||
for i, opt := range options {
|
||||
if i > 0 {
|
||||
sb.WriteString(" | ")
|
||||
}
|
||||
sb.WriteString("close({\n")
|
||||
for _, field := range opt.GetFields() {
|
||||
g.writeStructField(sb, field, depth+1)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s})", indent))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// cueTypeStr converts a ParamType to its CUE type string.
|
||||
func cueTypeStr(pt ParamType) string {
|
||||
switch pt {
|
||||
@@ -3194,7 +3327,7 @@ func cueTypeStr(pt ParamType) string {
|
||||
return "float"
|
||||
case ParamTypeArray:
|
||||
return "[...]"
|
||||
case ParamTypeMap, ParamTypeStruct, ParamTypeOneOf:
|
||||
case ParamTypeMap, ParamTypeStruct, ParamTypeOneOf, ParamTypeClosedUnion:
|
||||
return cueOpenStruct
|
||||
default:
|
||||
return "_"
|
||||
|
||||
@@ -1201,4 +1201,305 @@ var _ = Describe("CUEGenerator", func() {
|
||||
Expect(cue).To(ContainSubstring("name?: string"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GenerateParameterSchema with ClosedUnion parameters", func() {
|
||||
It("should generate close() disjunction with simple fields", func() {
|
||||
comp := defkit.NewComponent("test").
|
||||
Params(
|
||||
defkit.ClosedUnion("url").
|
||||
Description("Specify the url").
|
||||
Options(
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("value", defkit.ParamTypeString),
|
||||
),
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("ref", defkit.ParamTypeString),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
cue := gen.GenerateParameterSchema(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring("// +usage=Specify the url"))
|
||||
Expect(cue).To(ContainSubstring("url: close({"))
|
||||
Expect(cue).To(ContainSubstring("value: string"))
|
||||
Expect(cue).To(ContainSubstring("}) | close({"))
|
||||
Expect(cue).To(ContainSubstring("ref: string"))
|
||||
})
|
||||
|
||||
It("should generate close() disjunction with nested structs", func() {
|
||||
comp := defkit.NewComponent("test").
|
||||
Params(
|
||||
defkit.ClosedUnion("url").
|
||||
Options(
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("value", defkit.ParamTypeString),
|
||||
),
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("secretRef", defkit.ParamTypeStruct).Nested(
|
||||
defkit.Struct("secretRef").WithFields(
|
||||
defkit.Field("name", defkit.ParamTypeString).Description("name of the secret"),
|
||||
defkit.Field("key", defkit.ParamTypeString).Description("key in the secret"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
cue := gen.GenerateParameterSchema(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring("url: close({"))
|
||||
Expect(cue).To(ContainSubstring("value: string"))
|
||||
Expect(cue).To(ContainSubstring("}) | close({"))
|
||||
Expect(cue).To(ContainSubstring("secretRef: {"))
|
||||
Expect(cue).To(ContainSubstring("// +usage=name of the secret"))
|
||||
Expect(cue).To(ContainSubstring("name: string"))
|
||||
Expect(cue).To(ContainSubstring("// +usage=key in the secret"))
|
||||
Expect(cue).To(ContainSubstring("key: string"))
|
||||
})
|
||||
|
||||
It("should generate optional closed union with ?", func() {
|
||||
comp := defkit.NewComponent("test").
|
||||
Params(
|
||||
defkit.ClosedUnion("source").
|
||||
Optional().
|
||||
Options(
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("hcl", defkit.ParamTypeString),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
cue := gen.GenerateParameterSchema(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring("source?:"))
|
||||
})
|
||||
|
||||
It("should handle close() disjunction field ordering", func() {
|
||||
comp := defkit.NewComponent("test").
|
||||
Params(
|
||||
defkit.ClosedUnion("url").Options(
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("value", defkit.ParamTypeString),
|
||||
),
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("secretRef", defkit.ParamTypeStruct),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
cue := gen.GenerateParameterSchema(comp)
|
||||
|
||||
// Verify ordering: first option before second
|
||||
valueIdx := strings.Index(cue, "value: string")
|
||||
secretIdx := strings.Index(cue, "secretRef:")
|
||||
Expect(valueIdx).To(BeNumerically(">=", 0), "expected 'value: string' to be present")
|
||||
Expect(secretIdx).To(BeNumerically(">=", 0), "expected 'secretRef:' to be present")
|
||||
Expect(valueIdx).To(BeNumerically("<", secretIdx))
|
||||
})
|
||||
|
||||
It("should handle empty options gracefully", func() {
|
||||
comp := defkit.NewComponent("test").
|
||||
Params(
|
||||
defkit.ClosedUnion("empty"),
|
||||
)
|
||||
|
||||
cue := gen.GenerateParameterSchema(comp)
|
||||
|
||||
// Empty options should produce fallback
|
||||
Expect(cue).To(ContainSubstring("empty: _"))
|
||||
})
|
||||
|
||||
It("should generate ClosedUnion in helper definition", func() {
|
||||
comp := defkit.NewComponent("test").
|
||||
Params(
|
||||
defkit.String("name"),
|
||||
).
|
||||
Helper("URLConfig", defkit.ClosedUnion("urlConfig").
|
||||
Options(
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("value", defkit.ParamTypeString),
|
||||
),
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("secretRef", defkit.ParamTypeString),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
cue := comp.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("#URLConfig:"))
|
||||
Expect(cue).To(ContainSubstring("close({"))
|
||||
Expect(cue).To(ContainSubstring("value: string"))
|
||||
Expect(cue).To(ContainSubstring("}) | close({"))
|
||||
Expect(cue).To(ContainSubstring("secretRef: string"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ForEachGuarded inner braces", func() {
|
||||
It("should wrap each element in inner braces", func() {
|
||||
hasStorage := defkit.PathExists("parameter.storage")
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(
|
||||
defkit.Array("storage").WithFields(
|
||||
defkit.String("name").Required(),
|
||||
defkit.String("path").Required(),
|
||||
),
|
||||
).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
arr := defkit.NewArray().
|
||||
ForEachGuarded(
|
||||
hasStorage,
|
||||
defkit.ParamRef("storage"),
|
||||
defkit.NewArrayElement().
|
||||
Set("name", defkit.Reference("m.name")).
|
||||
Set("path", defkit.Reference("m.path")),
|
||||
)
|
||||
tpl.Set("items", arr)
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
// Should have inner braces around each element
|
||||
Expect(cue).To(ContainSubstring("for m in"))
|
||||
Expect(cue).To(MatchRegexp(`for m in .+ \{\n[^\n]*\{`))
|
||||
})
|
||||
|
||||
It("should wrap elements with conditional fields in inner braces", func() {
|
||||
hasStorage := defkit.PathExists("parameter.storage")
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(
|
||||
defkit.Array("storage").WithFields(
|
||||
defkit.String("name").Required(),
|
||||
defkit.String("subPath"),
|
||||
),
|
||||
).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
arr := defkit.NewArray().
|
||||
ForEachGuarded(
|
||||
hasStorage,
|
||||
defkit.ParamRef("storage"),
|
||||
defkit.NewArrayElement().
|
||||
Set("name", defkit.Reference("m.name")).
|
||||
SetIf(defkit.PathExists("m.subPath"), "subPath", defkit.Reference("m.subPath")),
|
||||
)
|
||||
tpl.Set("mounts", arr)
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
// Inner braces should contain both the field and the conditional
|
||||
Expect(cue).To(ContainSubstring("name: m.name"))
|
||||
Expect(cue).To(ContainSubstring("if m.subPath != _|_"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Dedupe from Reference source", func() {
|
||||
It("should generate dedup pattern when source is a Reference", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("name").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Set("deduped", defkit.From(defkit.Reference("volumesList")).Dedupe("name"))
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
// Should generate the full dedup pattern, not simple [for v in ...]
|
||||
Expect(cue).To(ContainSubstring("for val in ["))
|
||||
Expect(cue).To(ContainSubstring("for i, vi in volumesList"))
|
||||
Expect(cue).To(ContainSubstring("for j, vj in volumesList if j < i && vi.name == vj.name"))
|
||||
Expect(cue).To(ContainSubstring("_ignore: true"))
|
||||
Expect(cue).To(ContainSubstring("if val._ignore == _|_"))
|
||||
Expect(cue).NotTo(ContainSubstring("[for v in volumesList { v }]"))
|
||||
})
|
||||
|
||||
It("should generate dedup pattern with different key field", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("name").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Set("unique", defkit.From(defkit.Reference("items")).Dedupe("id"))
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("vi.id == vj.id"))
|
||||
Expect(cue).To(ContainSubstring("for i, vi in items"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("TemplateBody with no params", func() {
|
||||
It("should skip parameter block when TemplateBody is set and no params exist", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Description("test step").
|
||||
TemplateBody("nop: {}")
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("nop: {}"))
|
||||
Expect(cue).NotTo(ContainSubstring("parameter:"))
|
||||
})
|
||||
|
||||
It("should still emit parameter block when TemplateBody is set but params exist", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Description("test step").
|
||||
Params(defkit.String("name")).
|
||||
TemplateBody("nop: {}")
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("nop: {}"))
|
||||
Expect(cue).To(ContainSubstring("parameter:"))
|
||||
Expect(cue).To(ContainSubstring("name: string"))
|
||||
})
|
||||
|
||||
It("should emit parameter block when no TemplateBody and no params", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Description("test step")
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
// Default behavior: empty parameter block is emitted
|
||||
Expect(cue).To(ContainSubstring("parameter:"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Builtin WithDirectFields", func() {
|
||||
It("should render fields directly without $params wrapper", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("env").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Builtin("app", "op.#ShareCloudResource").
|
||||
WithDirectFields().
|
||||
WithParams(map[string]defkit.Value{
|
||||
"env": defkit.Reference("parameter.env"),
|
||||
"namespace": defkit.Reference("context.namespace"),
|
||||
}).
|
||||
Build()
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("app: op.#ShareCloudResource & {"))
|
||||
Expect(cue).To(ContainSubstring("env: parameter.env"))
|
||||
Expect(cue).To(ContainSubstring("namespace: context.namespace"))
|
||||
Expect(cue).NotTo(ContainSubstring("$params:"))
|
||||
})
|
||||
|
||||
It("should still use $params when WithDirectFields is not called", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("name").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Builtin("deploy", "kube.#Apply").
|
||||
WithParams(map[string]defkit.Value{
|
||||
"value": defkit.Reference("object"),
|
||||
}).
|
||||
Build()
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("deploy: kube.#Apply & {"))
|
||||
Expect(cue).To(ContainSubstring("$params:"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -713,4 +713,86 @@ var _ = Describe("Helper", func() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("HelperBuilder Filter and FilterCond on MultiSource", func() {
|
||||
var tpl *defkit.Template
|
||||
|
||||
BeforeEach(func() {
|
||||
tpl = defkit.NewTemplate()
|
||||
})
|
||||
|
||||
It("should propagate Filter to MultiSource", func() {
|
||||
volumeMounts := defkit.Object("volumeMounts")
|
||||
helper := tpl.Helper("filteredMounts").
|
||||
FromFields(volumeMounts, "pvc", "configMap").
|
||||
Filter(defkit.FieldEquals("readOnly", false)).
|
||||
Build()
|
||||
|
||||
ms, ok := helper.Collection().(*defkit.MultiSource)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(ms.Operations()).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should propagate FilterCond to MultiSource", func() {
|
||||
volumeMounts := defkit.Object("volumeMounts")
|
||||
cond := defkit.Eq(defkit.Item().Get("enabled"), defkit.Lit(true))
|
||||
helper := tpl.Helper("filteredMounts").
|
||||
FromFields(volumeMounts, "pvc", "configMap").
|
||||
FilterCond(cond).
|
||||
Build()
|
||||
|
||||
ms, ok := helper.Collection().(*defkit.MultiSource)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(ms.Operations()).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should propagate FilterCond to CollectionOp via From", func() {
|
||||
ports := defkit.List("ports")
|
||||
cond := defkit.Eq(defkit.Item().Get("enabled"), defkit.Lit(true))
|
||||
helper := tpl.Helper("filteredPorts").
|
||||
From(ports).
|
||||
FilterCond(cond).
|
||||
Build()
|
||||
|
||||
col, ok := helper.Collection().(*defkit.CollectionOp)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(col.Operations()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("CollectionOp FilterCond", func() {
|
||||
It("should add FilterCond operation to collection", func() {
|
||||
ports := defkit.List("ports")
|
||||
cond := defkit.Eq(defkit.Item().Get("enabled"), defkit.Lit(true))
|
||||
col := defkit.Each(ports).FilterCond(cond)
|
||||
Expect(col.Operations()).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should compose with other operations", func() {
|
||||
ports := defkit.List("ports")
|
||||
cond := defkit.Eq(defkit.Item().Get("enabled"), defkit.Lit(true))
|
||||
col := defkit.Each(ports).
|
||||
Filter(defkit.FieldEquals("expose", true)).
|
||||
FilterCond(cond).
|
||||
Pick("name", "port")
|
||||
Expect(col.Operations()).To(HaveLen(3))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("MultiSource Filter and FilterCond", func() {
|
||||
It("should add Filter operation to multi-source", func() {
|
||||
volumeMounts := defkit.Object("volumeMounts")
|
||||
ms := defkit.FromFields(volumeMounts, "pvc", "configMap")
|
||||
ms.Filter(defkit.FieldEquals("readOnly", false))
|
||||
Expect(ms.Operations()).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should add FilterCond operation to multi-source", func() {
|
||||
volumeMounts := defkit.Object("volumeMounts")
|
||||
ms := defkit.FromFields(volumeMounts, "pvc", "configMap")
|
||||
cond := defkit.Eq(defkit.Item().Get("enabled"), defkit.Lit(true))
|
||||
ms.FilterCond(cond)
|
||||
Expect(ms.Operations()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CUERenderer is implemented by builder types that can render themselves to CUE.
|
||||
// The renderValue function is provided by the CUE generator to render nested Value types.
|
||||
type CUERenderer interface {
|
||||
RenderCUE(renderValue func(Value) string) string
|
||||
}
|
||||
|
||||
// CUEConditionRenderer extends CUERenderer with condition rendering support.
|
||||
// Builders that need to render Condition values should implement this interface.
|
||||
// The CUE generator will prefer this over CUERenderer when available.
|
||||
type CUEConditionRenderer interface {
|
||||
RenderCUEWithCondition(renderValue func(Value) string, renderCondition func(Condition) string) string
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// KubeRead builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// KubeReadBuilder builds a kube.#Read operation fluently.
|
||||
type KubeReadBuilder struct {
|
||||
apiVersion string
|
||||
kind string
|
||||
name Value
|
||||
namespace Value
|
||||
cluster Value
|
||||
nsCond Condition // for NamespaceIf
|
||||
}
|
||||
|
||||
func (b *KubeReadBuilder) expr() {}
|
||||
func (b *KubeReadBuilder) value() {}
|
||||
func (b *KubeReadBuilder) condition() {}
|
||||
|
||||
// KubeRead creates a new kube.#Read builder for the given apiVersion and kind.
|
||||
func KubeRead(apiVersion, kind string) *KubeReadBuilder {
|
||||
return &KubeReadBuilder{
|
||||
apiVersion: apiVersion,
|
||||
kind: kind,
|
||||
}
|
||||
}
|
||||
|
||||
// Name sets the metadata.name value.
|
||||
func (b *KubeReadBuilder) Name(v Value) *KubeReadBuilder {
|
||||
b.name = v
|
||||
return b
|
||||
}
|
||||
|
||||
// Namespace sets the metadata.namespace value.
|
||||
func (b *KubeReadBuilder) Namespace(v Value) *KubeReadBuilder {
|
||||
b.namespace = v
|
||||
return b
|
||||
}
|
||||
|
||||
// NamespaceIf sets the metadata.namespace conditionally.
|
||||
func (b *KubeReadBuilder) NamespaceIf(cond Condition, v Value) *KubeReadBuilder {
|
||||
b.nsCond = cond
|
||||
b.namespace = v
|
||||
return b
|
||||
}
|
||||
|
||||
// Cluster sets the optional cluster parameter for multi-cluster reads.
|
||||
func (b *KubeReadBuilder) Cluster(v Value) *KubeReadBuilder {
|
||||
b.cluster = v
|
||||
return b
|
||||
}
|
||||
|
||||
// RenderCUE renders the builder to a CUE string.
|
||||
func (b *KubeReadBuilder) RenderCUE(rv func(Value) string) string {
|
||||
return b.RenderCUEWithCondition(rv, nil)
|
||||
}
|
||||
|
||||
// RenderCUEWithCondition renders the builder to a CUE string with condition support.
|
||||
func (b *KubeReadBuilder) RenderCUEWithCondition(rv func(Value) string, rc func(Condition) string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("kube.#Read & {\n")
|
||||
|
||||
// cluster param sits alongside value in $params
|
||||
if b.cluster != nil {
|
||||
sb.WriteString("\t$params: {\n")
|
||||
sb.WriteString(fmt.Sprintf("\t\tcluster: %s\n", rv(b.cluster)))
|
||||
sb.WriteString("\t\tvalue: {\n")
|
||||
b.writeValueBody(&sb, rv, rc, "\t\t\t")
|
||||
sb.WriteString("\t\t}\n")
|
||||
sb.WriteString("\t}\n")
|
||||
} else {
|
||||
sb.WriteString("\t$params: value: {\n")
|
||||
b.writeValueBody(&sb, rv, rc, "\t\t")
|
||||
sb.WriteString("\t}\n")
|
||||
}
|
||||
|
||||
sb.WriteString("}")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (b *KubeReadBuilder) writeValueBody(sb *strings.Builder, rv func(Value) string, rc func(Condition) string, indent string) {
|
||||
sb.WriteString(fmt.Sprintf("%sapiVersion: %q\n", indent, b.apiVersion))
|
||||
sb.WriteString(fmt.Sprintf("%skind: %q\n", indent, b.kind))
|
||||
sb.WriteString(fmt.Sprintf("%smetadata: {\n", indent))
|
||||
if b.name != nil {
|
||||
sb.WriteString(fmt.Sprintf("%s\tname: %s\n", indent, rv(b.name)))
|
||||
}
|
||||
if b.namespace != nil {
|
||||
if b.nsCond != nil && rc != nil {
|
||||
condStr := rc(b.nsCond)
|
||||
sb.WriteString(fmt.Sprintf("%s\tif %s {\n", indent, condStr))
|
||||
sb.WriteString(fmt.Sprintf("%s\t\tnamespace: %s\n", indent, rv(b.namespace)))
|
||||
sb.WriteString(fmt.Sprintf("%s\t}\n", indent))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s\tnamespace: %s\n", indent, rv(b.namespace)))
|
||||
}
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// KubeApply builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// KubeApplyBuilder builds a kube.#Apply operation fluently.
|
||||
type KubeApplyBuilder struct {
|
||||
objectValue Value
|
||||
cluster Value
|
||||
}
|
||||
|
||||
func (b *KubeApplyBuilder) expr() {}
|
||||
func (b *KubeApplyBuilder) value() {}
|
||||
func (b *KubeApplyBuilder) condition() {}
|
||||
|
||||
// KubeApply creates a new kube.#Apply builder with the given object value.
|
||||
func KubeApply(objectValue Value) *KubeApplyBuilder {
|
||||
return &KubeApplyBuilder{objectValue: objectValue}
|
||||
}
|
||||
|
||||
// Cluster sets the optional cluster parameter.
|
||||
func (b *KubeApplyBuilder) Cluster(v Value) *KubeApplyBuilder {
|
||||
b.cluster = v
|
||||
return b
|
||||
}
|
||||
|
||||
// RenderCUE renders the builder to a CUE string.
|
||||
func (b *KubeApplyBuilder) RenderCUE(rv func(Value) string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("kube.#Apply & {\n")
|
||||
sb.WriteString("\t$params: {\n")
|
||||
sb.WriteString(fmt.Sprintf("\t\tvalue: %s\n", rv(b.objectValue)))
|
||||
if b.cluster != nil {
|
||||
sb.WriteString(fmt.Sprintf("\t\tcluster: %s\n", rv(b.cluster)))
|
||||
}
|
||||
sb.WriteString("\t}\n")
|
||||
sb.WriteString("}")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTTPPost builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// HTTPPostBuilder builds an http.#HTTPDo POST operation fluently.
|
||||
type HTTPPostBuilder struct {
|
||||
url Value
|
||||
body Value
|
||||
headers map[string]string
|
||||
}
|
||||
|
||||
func (b *HTTPPostBuilder) expr() {}
|
||||
func (b *HTTPPostBuilder) value() {}
|
||||
func (b *HTTPPostBuilder) condition() {}
|
||||
|
||||
// HTTPPost creates a new http.#HTTPDo builder for a POST request.
|
||||
func HTTPPost(url Value) *HTTPPostBuilder {
|
||||
return &HTTPPostBuilder{
|
||||
url: url,
|
||||
headers: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Body sets the request body.
|
||||
func (b *HTTPPostBuilder) Body(v Value) *HTTPPostBuilder {
|
||||
b.body = v
|
||||
return b
|
||||
}
|
||||
|
||||
// Header adds a request header.
|
||||
func (b *HTTPPostBuilder) Header(key, value string) *HTTPPostBuilder {
|
||||
b.headers[key] = value
|
||||
return b
|
||||
}
|
||||
|
||||
// RenderCUE renders the builder to a CUE string.
|
||||
func (b *HTTPPostBuilder) RenderCUE(rv func(Value) string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("http.#HTTPDo & {\n")
|
||||
sb.WriteString("\t$params: {\n")
|
||||
sb.WriteString("\t\tmethod: \"POST\"\n")
|
||||
sb.WriteString(fmt.Sprintf("\t\turl: %s\n", rv(b.url)))
|
||||
sb.WriteString("\t\trequest: {\n")
|
||||
if b.body != nil {
|
||||
sb.WriteString(fmt.Sprintf("\t\t\tbody: %s\n", rv(b.body)))
|
||||
}
|
||||
for k, v := range b.headers {
|
||||
sb.WriteString(fmt.Sprintf("\t\t\theader: %q: %q\n", k, v))
|
||||
}
|
||||
sb.WriteString("\t\t}\n")
|
||||
sb.WriteString("\t}\n")
|
||||
sb.WriteString("}")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ConvertString builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ConvertStringBuilder builds a util.#ConvertString operation fluently.
|
||||
type ConvertStringBuilder struct {
|
||||
input Value
|
||||
}
|
||||
|
||||
func (b *ConvertStringBuilder) expr() {}
|
||||
func (b *ConvertStringBuilder) value() {}
|
||||
func (b *ConvertStringBuilder) condition() {}
|
||||
|
||||
// ConvertString creates a new util.#ConvertString builder.
|
||||
func ConvertString(input Value) *ConvertStringBuilder {
|
||||
return &ConvertStringBuilder{input: input}
|
||||
}
|
||||
|
||||
// RenderCUE renders the builder to a CUE string.
|
||||
func (b *ConvertStringBuilder) RenderCUE(rv func(Value) string) string {
|
||||
return fmt.Sprintf("util.#ConvertString & {\n\t$params: bt: %s\n}", rv(b.input))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WaitUntil builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// WaitUntilBuilder builds a builtin.#ConditionalWait operation fluently.
|
||||
type WaitUntilBuilder struct {
|
||||
continueExpr Value
|
||||
guards []Value
|
||||
messageCond Condition
|
||||
messageVal Value
|
||||
}
|
||||
|
||||
func (b *WaitUntilBuilder) expr() {}
|
||||
func (b *WaitUntilBuilder) value() {}
|
||||
func (b *WaitUntilBuilder) condition() {}
|
||||
|
||||
// WaitUntil creates a new builtin.#ConditionalWait builder.
|
||||
func WaitUntil(continueExpr Value) *WaitUntilBuilder {
|
||||
return &WaitUntilBuilder{continueExpr: continueExpr}
|
||||
}
|
||||
|
||||
// Guard adds guard conditions that must be true (not _|_) before the continue
|
||||
// check is evaluated. Multiple guards are nested as chained if statements.
|
||||
func (b *WaitUntilBuilder) Guard(guards ...Value) *WaitUntilBuilder {
|
||||
b.guards = append(b.guards, guards...)
|
||||
return b
|
||||
}
|
||||
|
||||
// MessageIf conditionally sets the message parameter.
|
||||
func (b *WaitUntilBuilder) MessageIf(cond Condition, val Value) *WaitUntilBuilder {
|
||||
b.messageCond = cond
|
||||
b.messageVal = val
|
||||
return b
|
||||
}
|
||||
|
||||
// RenderCUE renders the builder to a CUE string.
|
||||
func (b *WaitUntilBuilder) RenderCUE(rv func(Value) string) string {
|
||||
return b.RenderCUEWithCondition(rv, nil)
|
||||
}
|
||||
|
||||
// RenderCUEWithCondition renders the builder to a CUE string with condition support.
|
||||
func (b *WaitUntilBuilder) RenderCUEWithCondition(rv func(Value) string, rc func(Condition) string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("builtin.#ConditionalWait & {\n")
|
||||
|
||||
if len(b.guards) > 0 {
|
||||
// Build chained guard: if g1 if g2 { ... }
|
||||
var guardParts []string
|
||||
for _, g := range b.guards {
|
||||
guardParts = append(guardParts, fmt.Sprintf("if %s != _|_", rv(g)))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("\t%s {\n", strings.Join(guardParts, " ")))
|
||||
sb.WriteString(fmt.Sprintf("\t\t$params: continue: %s\n", rv(b.continueExpr)))
|
||||
sb.WriteString("\t}\n")
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("\t$params: continue: %s\n", rv(b.continueExpr)))
|
||||
}
|
||||
|
||||
if b.messageCond != nil && b.messageVal != nil {
|
||||
if rc != nil {
|
||||
condStr := rc(b.messageCond)
|
||||
sb.WriteString(fmt.Sprintf("\tif %s {\n", condStr))
|
||||
} else {
|
||||
// Fallback when condition renderer is not available
|
||||
sb.WriteString(fmt.Sprintf("\tif %s != _|_ {\n", rv(b.messageVal)))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("\t\t$params: message: %s\n", rv(b.messageVal)))
|
||||
sb.WriteString("\t}\n")
|
||||
}
|
||||
|
||||
sb.WriteString("}")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fail builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// FailBuilder builds a builtin.#Fail operation fluently.
|
||||
type FailBuilder struct {
|
||||
message Value
|
||||
}
|
||||
|
||||
func (b *FailBuilder) expr() {}
|
||||
func (b *FailBuilder) value() {}
|
||||
func (b *FailBuilder) condition() {}
|
||||
|
||||
// Fail creates a new builtin.#Fail builder with the given message.
|
||||
func Fail(message Value) *FailBuilder {
|
||||
return &FailBuilder{message: message}
|
||||
}
|
||||
|
||||
// RenderCUE renders the builder to a CUE string.
|
||||
func (b *FailBuilder) RenderCUE(rv func(Value) string) string {
|
||||
return fmt.Sprintf("builtin.#Fail & {\n\t$params: message: %s\n}", rv(b.message))
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
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"
|
||||
)
|
||||
|
||||
// simpleRenderValue is a test helper that renders Values the same way cuegen does for Ref types.
|
||||
func simpleRenderValue(v defkit.Value) string {
|
||||
if cr, ok := v.(defkit.CUERenderer); ok {
|
||||
return cr.RenderCUE(simpleRenderValue)
|
||||
}
|
||||
// For *Ref, use the Path() method via the interface trick
|
||||
type pather interface{ Path() string }
|
||||
if p, ok := v.(pather); ok {
|
||||
return p.Path()
|
||||
}
|
||||
return "_"
|
||||
}
|
||||
|
||||
var _ = Describe("Operation Builders", func() {
|
||||
|
||||
Describe("KubeRead", func() {
|
||||
It("should render a basic kube.#Read with name and namespace", func() {
|
||||
b := defkit.KubeRead("v1", "Secret").
|
||||
Name(defkit.Reference("parameter.name")).
|
||||
Namespace(defkit.Reference("context.namespace"))
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("kube.#Read & {"))
|
||||
Expect(cue).To(ContainSubstring(`apiVersion: "v1"`))
|
||||
Expect(cue).To(ContainSubstring(`kind: "Secret"`))
|
||||
Expect(cue).To(ContainSubstring("name: parameter.name"))
|
||||
Expect(cue).To(ContainSubstring("namespace: context.namespace"))
|
||||
Expect(cue).To(ContainSubstring("$params: value: {"))
|
||||
})
|
||||
|
||||
It("should render kube.#Read with cluster parameter", func() {
|
||||
b := defkit.KubeRead("v1", "ConfigMap").
|
||||
Name(defkit.Reference("parameter.name")).
|
||||
Namespace(defkit.Reference("context.namespace")).
|
||||
Cluster(defkit.Reference("parameter.cluster"))
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("kube.#Read & {"))
|
||||
Expect(cue).To(ContainSubstring("cluster: parameter.cluster"))
|
||||
Expect(cue).To(ContainSubstring(`apiVersion: "v1"`))
|
||||
Expect(cue).To(ContainSubstring(`kind: "ConfigMap"`))
|
||||
// When cluster is set, $params uses expanded form
|
||||
Expect(cue).NotTo(ContainSubstring("$params: value: {"))
|
||||
Expect(cue).To(ContainSubstring("$params: {"))
|
||||
})
|
||||
|
||||
It("should render without namespace when not set", func() {
|
||||
b := defkit.KubeRead("apps/v1", "Deployment").
|
||||
Name(defkit.Reference("parameter.name"))
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("name: parameter.name"))
|
||||
Expect(cue).NotTo(ContainSubstring("namespace:"))
|
||||
})
|
||||
|
||||
It("should render with different apiVersion and kind", func() {
|
||||
b := defkit.KubeRead("core.oam.dev/v1beta1", "Application").
|
||||
Name(defkit.Reference("context.name")).
|
||||
Namespace(defkit.Reference("context.namespace"))
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring(`apiVersion: "core.oam.dev/v1beta1"`))
|
||||
Expect(cue).To(ContainSubstring(`kind: "Application"`))
|
||||
})
|
||||
|
||||
It("should render NamespaceIf with condition through CUE generator", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("name").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Set("read", defkit.KubeRead("v1", "Secret").
|
||||
Name(defkit.Reference("parameter.name")).
|
||||
NamespaceIf(
|
||||
defkit.PathExists("parameter.namespace"),
|
||||
defkit.Reference("parameter.namespace"),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("if parameter.namespace != _|_ {"))
|
||||
Expect(cue).To(ContainSubstring("namespace: parameter.namespace"))
|
||||
})
|
||||
|
||||
It("should render NamespaceIf without condition renderer as unconditional", func() {
|
||||
b := defkit.KubeRead("v1", "Secret").
|
||||
Name(defkit.Reference("parameter.name")).
|
||||
NamespaceIf(
|
||||
defkit.PathExists("parameter.namespace"),
|
||||
defkit.Reference("parameter.namespace"),
|
||||
)
|
||||
|
||||
// RenderCUE without condition renderer falls back to unconditional
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("namespace: parameter.namespace"))
|
||||
})
|
||||
|
||||
It("should implement Value interface", func() {
|
||||
var v defkit.Value = defkit.KubeRead("v1", "Pod").
|
||||
Name(defkit.Reference("name"))
|
||||
Expect(v).NotTo(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("KubeApply", func() {
|
||||
It("should render a basic kube.#Apply", func() {
|
||||
b := defkit.KubeApply(defkit.Reference("deployment"))
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("kube.#Apply & {"))
|
||||
Expect(cue).To(ContainSubstring("value: deployment"))
|
||||
Expect(cue).To(ContainSubstring("$params: {"))
|
||||
})
|
||||
|
||||
It("should render kube.#Apply with cluster", func() {
|
||||
b := defkit.KubeApply(defkit.Reference("configMap")).
|
||||
Cluster(defkit.Reference("parameter.cluster"))
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("value: configMap"))
|
||||
Expect(cue).To(ContainSubstring("cluster: parameter.cluster"))
|
||||
})
|
||||
|
||||
It("should render kube.#Apply without cluster", func() {
|
||||
b := defkit.KubeApply(defkit.Reference("jobValue"))
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).NotTo(ContainSubstring("cluster:"))
|
||||
})
|
||||
|
||||
It("should implement Value interface", func() {
|
||||
var v defkit.Value = defkit.KubeApply(defkit.Reference("obj"))
|
||||
Expect(v).NotTo(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("HTTPPost", func() {
|
||||
It("should render a basic POST request", func() {
|
||||
b := defkit.HTTPPost(defkit.Reference("parameter.url")).
|
||||
Body(defkit.Reference("data.value")).
|
||||
Header("Content-Type", "application/json")
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("http.#HTTPDo & {"))
|
||||
Expect(cue).To(ContainSubstring(`method: "POST"`))
|
||||
Expect(cue).To(ContainSubstring("url: parameter.url"))
|
||||
Expect(cue).To(ContainSubstring("body: data.value"))
|
||||
Expect(cue).To(ContainSubstring(`header: "Content-Type": "application/json"`))
|
||||
})
|
||||
|
||||
It("should render without body when not set", func() {
|
||||
b := defkit.HTTPPost(defkit.Reference("parameter.url")).
|
||||
Header("Content-Type", "application/json")
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).NotTo(ContainSubstring("body:"))
|
||||
Expect(cue).To(ContainSubstring("request: {"))
|
||||
})
|
||||
|
||||
It("should render with computed URL", func() {
|
||||
b := defkit.HTTPPost(defkit.Reference("stringValue.$returns.str")).
|
||||
Body(defkit.Reference("data.value")).
|
||||
Header("Content-Type", "application/json")
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("url: stringValue.$returns.str"))
|
||||
})
|
||||
|
||||
It("should implement Value interface", func() {
|
||||
var v defkit.Value = defkit.HTTPPost(defkit.Reference("url"))
|
||||
Expect(v).NotTo(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ConvertString", func() {
|
||||
It("should render util.#ConvertString", func() {
|
||||
b := defkit.ConvertString(
|
||||
defkit.Reference("base64.Decode(null, read.$returns.value.data[key])"),
|
||||
)
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("util.#ConvertString & {"))
|
||||
Expect(cue).To(ContainSubstring("$params: bt: base64.Decode(null, read.$returns.value.data[key])"))
|
||||
})
|
||||
|
||||
It("should implement Value interface", func() {
|
||||
var v defkit.Value = defkit.ConvertString(defkit.Reference("input"))
|
||||
Expect(v).NotTo(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("WaitUntil", func() {
|
||||
It("should render simple conditional wait", func() {
|
||||
b := defkit.WaitUntil(
|
||||
defkit.Reference("output.$returns.value.status.readyReplicas == parameter.replicas"),
|
||||
)
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("builtin.#ConditionalWait & {"))
|
||||
Expect(cue).To(ContainSubstring("$params: continue: output.$returns.value.status.readyReplicas == parameter.replicas"))
|
||||
Expect(cue).NotTo(ContainSubstring("if "))
|
||||
})
|
||||
|
||||
It("should render with single guard", func() {
|
||||
b := defkit.WaitUntil(
|
||||
defkit.Reference(`apply.$returns.value.status.apply.state == "Available"`),
|
||||
).Guard(
|
||||
defkit.Reference("apply.$returns.value.status"),
|
||||
)
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("if apply.$returns.value.status != _|_"))
|
||||
Expect(cue).To(ContainSubstring(`$params: continue: apply.$returns.value.status.apply.state == "Available"`))
|
||||
})
|
||||
|
||||
It("should render with chained guards", func() {
|
||||
b := defkit.WaitUntil(
|
||||
defkit.Reference(`apply.$returns.value.status.apply.state == "Available"`),
|
||||
).Guard(
|
||||
defkit.Reference("apply.$returns.value.status"),
|
||||
defkit.Reference("apply.$returns.value.status.apply"),
|
||||
)
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("if apply.$returns.value.status != _|_ if apply.$returns.value.status.apply != _|_"))
|
||||
})
|
||||
|
||||
It("should render MessageIf with condition through CUE generator", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("name").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Set("wait", defkit.WaitUntil(
|
||||
defkit.Reference("job.$returns.value.status.succeeded > 0"),
|
||||
).MessageIf(
|
||||
defkit.PathExists("job.$returns.value.status.conditions"),
|
||||
defkit.Reference("job.$returns.value.status.conditions[0].message"),
|
||||
))
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("if job.$returns.value.status.conditions != _|_ {"))
|
||||
Expect(cue).To(ContainSubstring("$params: message: job.$returns.value.status.conditions[0].message"))
|
||||
})
|
||||
|
||||
It("should implement Value interface", func() {
|
||||
var v defkit.Value = defkit.WaitUntil(defkit.Reference("true"))
|
||||
Expect(v).NotTo(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Fail", func() {
|
||||
It("should render builtin.#Fail with message", func() {
|
||||
b := defkit.Fail(defkit.Reference("check.$returns.message"))
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring("builtin.#Fail & {"))
|
||||
Expect(cue).To(ContainSubstring("$params: message: check.$returns.message"))
|
||||
})
|
||||
|
||||
It("should render with literal message", func() {
|
||||
b := defkit.Fail(defkit.Reference(`"failed to execute command"`))
|
||||
|
||||
cue := b.RenderCUE(simpleRenderValue)
|
||||
|
||||
Expect(cue).To(ContainSubstring(`$params: message: "failed to execute command"`))
|
||||
})
|
||||
|
||||
It("should implement Value interface", func() {
|
||||
var v defkit.Value = defkit.Fail(defkit.Reference("msg"))
|
||||
Expect(v).NotTo(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("CUERenderer integration via valueToCUE", func() {
|
||||
It("should render KubeRead through CUE generator", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("name").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Set("output", defkit.KubeRead("v1", "Pod").
|
||||
Name(defkit.Reference("parameter.name")).
|
||||
Namespace(defkit.Reference("context.namespace")),
|
||||
)
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("kube.#Read & {"))
|
||||
Expect(cue).To(ContainSubstring(`apiVersion: "v1"`))
|
||||
Expect(cue).To(ContainSubstring(`kind: "Pod"`))
|
||||
Expect(cue).To(ContainSubstring("name: parameter.name"))
|
||||
Expect(cue).To(ContainSubstring("namespace: context.namespace"))
|
||||
})
|
||||
|
||||
It("should render HTTPPost through CUE generator", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("url").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Set("req", defkit.HTTPPost(defkit.Reference("parameter.url")).
|
||||
Body(defkit.Reference("data.value")).
|
||||
Header("Content-Type", "application/json"),
|
||||
)
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("http.#HTTPDo & {"))
|
||||
Expect(cue).To(ContainSubstring(`method: "POST"`))
|
||||
Expect(cue).To(ContainSubstring("url: parameter.url"))
|
||||
})
|
||||
|
||||
It("should render ConvertString through CUE generator", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("name").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Set("convert", defkit.ConvertString(
|
||||
defkit.Reference("base64.Decode(null, data)"),
|
||||
))
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("util.#ConvertString & {"))
|
||||
Expect(cue).To(ContainSubstring("$params: bt: base64.Decode(null, data)"))
|
||||
})
|
||||
|
||||
It("should render WaitUntil with Guard through CUE generator", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("name").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Set("wait", defkit.WaitUntil(
|
||||
defkit.Reference("job.$returns.value.status.succeeded > 0"),
|
||||
).Guard(
|
||||
defkit.Reference("job.$returns.value.status"),
|
||||
defkit.Reference("job.$returns.value.status.succeeded"),
|
||||
))
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("builtin.#ConditionalWait & {"))
|
||||
Expect(cue).To(ContainSubstring("if job.$returns.value.status != _|_ if job.$returns.value.status.succeeded != _|_"))
|
||||
Expect(cue).To(ContainSubstring("$params: continue: job.$returns.value.status.succeeded > 0"))
|
||||
})
|
||||
|
||||
It("should render WaitUntil without Guard through CUE generator", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("name").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Set("wait", defkit.WaitUntil(
|
||||
defkit.Reference("output.$returns.value.status.ready == true"),
|
||||
))
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("builtin.#ConditionalWait & {"))
|
||||
Expect(cue).To(ContainSubstring("$params: continue: output.$returns.value.status.ready == true"))
|
||||
Expect(cue).NotTo(ContainSubstring("if "))
|
||||
})
|
||||
|
||||
It("should render Fail through CUE generator", func() {
|
||||
ws := defkit.NewWorkflowStep("test").
|
||||
Params(defkit.String("name").Required()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Set("error", defkit.Fail(
|
||||
defkit.Reference(`"operation failed"`),
|
||||
))
|
||||
})
|
||||
|
||||
cue := ws.ToCue()
|
||||
|
||||
Expect(cue).To(ContainSubstring("builtin.#Fail & {"))
|
||||
Expect(cue).To(ContainSubstring(`$params: message: "operation failed"`))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1245,6 +1245,78 @@ func (p *OneOfParam) GetVariant(name string) *OneOfVariant {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClosedStructOption represents one option in a closed struct disjunction.
|
||||
// It holds a set of struct fields that will be wrapped in close({...}).
|
||||
type ClosedStructOption struct {
|
||||
fields []*StructField
|
||||
}
|
||||
|
||||
// ClosedStruct creates a new closed struct option for use in ClosedUnion.
|
||||
func ClosedStruct() *ClosedStructOption {
|
||||
return &ClosedStructOption{
|
||||
fields: make([]*StructField, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// WithFields adds fields to the closed struct option.
|
||||
func (c *ClosedStructOption) WithFields(fields ...*StructField) *ClosedStructOption {
|
||||
c.fields = append(c.fields, fields...)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetFields returns the fields of this closed struct option.
|
||||
func (c *ClosedStructOption) GetFields() []*StructField {
|
||||
return c.fields
|
||||
}
|
||||
|
||||
// ClosedUnionParam represents a closed struct disjunction parameter.
|
||||
// It generates CUE of the form: close({...}) | close({...})
|
||||
type ClosedUnionParam struct {
|
||||
baseParam
|
||||
options []*ClosedStructOption
|
||||
}
|
||||
|
||||
// ClosedUnion creates a new closed struct disjunction parameter.
|
||||
func ClosedUnion(name string) *ClosedUnionParam {
|
||||
return &ClosedUnionParam{
|
||||
baseParam: baseParam{
|
||||
name: name,
|
||||
paramType: ParamTypeClosedUnion,
|
||||
},
|
||||
options: make([]*ClosedStructOption, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Options sets the closed struct alternatives for the disjunction.
|
||||
func (p *ClosedUnionParam) Options(options ...*ClosedStructOption) *ClosedUnionParam {
|
||||
p.options = append(p.options, options...)
|
||||
return p
|
||||
}
|
||||
|
||||
// Required marks the parameter as required.
|
||||
func (p *ClosedUnionParam) Required() *ClosedUnionParam {
|
||||
p.required = true
|
||||
return p
|
||||
}
|
||||
|
||||
// Optional marks the parameter as optional.
|
||||
func (p *ClosedUnionParam) Optional() *ClosedUnionParam {
|
||||
p.optional = true
|
||||
p.required = false
|
||||
return p
|
||||
}
|
||||
|
||||
// Description sets the parameter description.
|
||||
func (p *ClosedUnionParam) Description(desc string) *ClosedUnionParam {
|
||||
p.description = desc
|
||||
return p
|
||||
}
|
||||
|
||||
// GetOptions returns the closed struct options.
|
||||
func (p *ClosedUnionParam) GetOptions() []*ClosedStructOption {
|
||||
return p.options
|
||||
}
|
||||
|
||||
// Convenience functions for common parameter patterns
|
||||
|
||||
// StringList creates a string array parameter.
|
||||
|
||||
@@ -1001,4 +1001,98 @@ var _ = Describe("Parameters", func() {
|
||||
Expect(p.GetDescription()).To(Equal("Deprecated field"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("ClosedUnionParam", func() {
|
||||
It("should create a closed union parameter with name", func() {
|
||||
p := defkit.ClosedUnion("url")
|
||||
Expect(p.Name()).To(Equal("url"))
|
||||
Expect(p.IsRequired()).To(BeFalse())
|
||||
Expect(p.IsOptional()).To(BeFalse())
|
||||
Expect(p.HasDefault()).To(BeFalse())
|
||||
Expect(p.GetOptions()).To(HaveLen(0))
|
||||
})
|
||||
|
||||
It("should support required modifier", func() {
|
||||
p := defkit.ClosedUnion("url").Required()
|
||||
Expect(p.IsRequired()).To(BeTrue())
|
||||
Expect(p.IsOptional()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should support optional modifier", func() {
|
||||
p := defkit.ClosedUnion("url").Required().Optional()
|
||||
Expect(p.IsRequired()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should support description", func() {
|
||||
p := defkit.ClosedUnion("url").Description("the url")
|
||||
Expect(p.GetDescription()).To(Equal("the url"))
|
||||
})
|
||||
|
||||
It("should support options with fields", func() {
|
||||
p := defkit.ClosedUnion("url").Options(
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("value", defkit.ParamTypeString).Required(),
|
||||
),
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("secretRef", defkit.ParamTypeStruct).Required(),
|
||||
),
|
||||
)
|
||||
Expect(p.GetOptions()).To(HaveLen(2))
|
||||
Expect(p.GetOptions()[0].GetFields()).To(HaveLen(1))
|
||||
Expect(p.GetOptions()[0].GetFields()[0].Name()).To(Equal("value"))
|
||||
Expect(p.GetOptions()[1].GetFields()).To(HaveLen(1))
|
||||
Expect(p.GetOptions()[1].GetFields()[0].Name()).To(Equal("secretRef"))
|
||||
})
|
||||
|
||||
It("should support fluent chaining", func() {
|
||||
p := defkit.ClosedUnion("source").
|
||||
Required().
|
||||
Description("the source").
|
||||
Options(
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("hcl", defkit.ParamTypeString).Required(),
|
||||
),
|
||||
defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("remote", defkit.ParamTypeString).Required(),
|
||||
defkit.Field("path", defkit.ParamTypeString),
|
||||
),
|
||||
)
|
||||
Expect(p.Name()).To(Equal("source"))
|
||||
Expect(p.IsRequired()).To(BeTrue())
|
||||
Expect(p.GetDescription()).To(Equal("the source"))
|
||||
Expect(p.GetOptions()).To(HaveLen(2))
|
||||
Expect(p.GetOptions()[1].GetFields()).To(HaveLen(2))
|
||||
})
|
||||
})
|
||||
|
||||
Context("ClosedStructOption", func() {
|
||||
It("should create an empty closed struct", func() {
|
||||
cs := defkit.ClosedStruct()
|
||||
Expect(cs.GetFields()).To(HaveLen(0))
|
||||
})
|
||||
|
||||
It("should add fields with WithFields", func() {
|
||||
cs := defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("name", defkit.ParamTypeString).Required(),
|
||||
defkit.Field("key", defkit.ParamTypeString).Required(),
|
||||
)
|
||||
Expect(cs.GetFields()).To(HaveLen(2))
|
||||
Expect(cs.GetFields()[0].Name()).To(Equal("name"))
|
||||
Expect(cs.GetFields()[1].Name()).To(Equal("key"))
|
||||
})
|
||||
|
||||
It("should support nested struct fields", func() {
|
||||
cs := defkit.ClosedStruct().WithFields(
|
||||
defkit.Field("secretRef", defkit.ParamTypeStruct).Nested(
|
||||
defkit.Struct("secretRef").WithFields(
|
||||
defkit.Field("name", defkit.ParamTypeString).Required(),
|
||||
defkit.Field("key", defkit.ParamTypeString).Required(),
|
||||
),
|
||||
),
|
||||
)
|
||||
Expect(cs.GetFields()).To(HaveLen(1))
|
||||
Expect(cs.GetFields()[0].GetNested()).NotTo(BeNil())
|
||||
Expect(cs.GetFields()[0].GetNested().GetFields()).To(HaveLen(2))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1299,6 +1299,7 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf
|
||||
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))
|
||||
|
||||
@@ -1597,8 +1597,8 @@ template: {
|
||||
})
|
||||
})
|
||||
|
||||
Context("PatchContainer no _baseContainer singular", func() {
|
||||
It("should use _baseContainers (plural) not _baseContainer (singular)", func() {
|
||||
Context("PatchContainer base container definitions", func() {
|
||||
It("should generate _baseContainers (plural) and _baseContainer (singular) fields", func() {
|
||||
trait := defkit.NewTrait("base-containers-test").
|
||||
Description("Test base containers plural").
|
||||
AppliesTo("deployments.apps").
|
||||
@@ -1615,7 +1615,7 @@ template: {
|
||||
|
||||
cue := trait.ToCue()
|
||||
Expect(cue).To(ContainSubstring("_baseContainers: context.output.spec.template.spec.containers"))
|
||||
Expect(cue).NotTo(ContainSubstring("_baseContainer:"))
|
||||
Expect(cue).To(ContainSubstring("_baseContainer: *_|_ | {...}"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1723,5 +1723,85 @@ template: {
|
||||
Expect(trait.IsControlPlaneOnly()).To(BeTrue())
|
||||
Expect(trait.IsRevisionEnabled()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should NOT emit controlPlaneOnly in ToYAML when not set", func() {
|
||||
yamlBytes, err := defkit.NewTrait("t").ToYAML()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(yamlBytes)).NotTo(ContainSubstring("controlPlaneOnly"))
|
||||
})
|
||||
|
||||
It("should NOT emit revisionEnabled in ToYAML when not set", func() {
|
||||
yamlBytes, err := defkit.NewTrait("t").ToYAML()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(yamlBytes)).NotTo(ContainSubstring("revisionEnabled"))
|
||||
})
|
||||
|
||||
It("should emit manageWorkload: true in CUE attributes when set", func() {
|
||||
cue := defkit.NewTrait("t").ManageWorkload().ToCue()
|
||||
Expect(cue).To(ContainSubstring("manageWorkload: true"))
|
||||
})
|
||||
|
||||
It("should NOT emit manageWorkload in CUE attributes when not set", func() {
|
||||
cue := defkit.NewTrait("t").ToCue()
|
||||
Expect(cue).NotTo(ContainSubstring("manageWorkload"))
|
||||
})
|
||||
|
||||
It("should emit controlPlaneOnly: true in CUE attributes when set", func() {
|
||||
cue := defkit.NewTrait("t").ControlPlaneOnly().ToCue()
|
||||
Expect(cue).To(ContainSubstring("controlPlaneOnly: true"))
|
||||
})
|
||||
|
||||
It("should NOT emit controlPlaneOnly in CUE attributes when not set", func() {
|
||||
cue := defkit.NewTrait("t").ToCue()
|
||||
Expect(cue).NotTo(ContainSubstring("controlPlaneOnly"))
|
||||
})
|
||||
|
||||
It("should emit revisionEnabled: true in CUE attributes when set", func() {
|
||||
cue := defkit.NewTrait("t").RevisionEnabled().ToCue()
|
||||
Expect(cue).To(ContainSubstring("revisionEnabled: true"))
|
||||
})
|
||||
|
||||
It("should NOT emit revisionEnabled in CUE attributes when not set", func() {
|
||||
cue := defkit.NewTrait("t").ToCue()
|
||||
Expect(cue).NotTo(ContainSubstring("revisionEnabled"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Status Block CUE Render", func() {
|
||||
It("should render statusDetails in trait CUE", func() {
|
||||
cue := defkit.NewTrait("t").StatusDetails("phase: context.output.status.phase").ToCue()
|
||||
Expect(cue).To(ContainSubstring("status:"))
|
||||
Expect(cue).To(ContainSubstring("statusDetails:"))
|
||||
Expect(cue).To(ContainSubstring("phase: context.output.status.phase"))
|
||||
})
|
||||
|
||||
It("should render customStatus in trait CUE", func() {
|
||||
cue := defkit.NewTrait("t").CustomStatus("message: \"Running\"").ToCue()
|
||||
Expect(cue).To(ContainSubstring("status:"))
|
||||
Expect(cue).To(ContainSubstring("customStatus:"))
|
||||
})
|
||||
|
||||
It("should render healthPolicy in trait CUE", func() {
|
||||
cue := defkit.NewTrait("t").HealthPolicy("isHealth: true").ToCue()
|
||||
Expect(cue).To(ContainSubstring("status:"))
|
||||
Expect(cue).To(ContainSubstring("healthPolicy:"))
|
||||
})
|
||||
|
||||
It("should omit status block when none set", func() {
|
||||
cue := defkit.NewTrait("t").ToCue()
|
||||
Expect(cue).NotTo(ContainSubstring("status:"))
|
||||
})
|
||||
|
||||
It("should render all three status fields together", func() {
|
||||
cue := defkit.NewTrait("t").
|
||||
CustomStatus("message: \"Running\"").
|
||||
HealthPolicy("isHealth: true").
|
||||
StatusDetails("phase: context.output.status.phase").
|
||||
ToCue()
|
||||
Expect(cue).To(ContainSubstring("status:"))
|
||||
Expect(cue).To(ContainSubstring("customStatus:"))
|
||||
Expect(cue).To(ContainSubstring("healthPolicy:"))
|
||||
Expect(cue).To(ContainSubstring("statusDetails:"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -91,4 +91,6 @@ const (
|
||||
ParamTypeEnum ParamType = "enum"
|
||||
// ParamTypeOneOf represents a discriminated union parameter
|
||||
ParamTypeOneOf ParamType = "oneof"
|
||||
// ParamTypeClosedUnion represents a closed struct disjunction parameter
|
||||
ParamTypeClosedUnion ParamType = "closedunion"
|
||||
)
|
||||
|
||||
@@ -58,6 +58,7 @@ type BuiltinAction struct {
|
||||
name string // e.g., "multicluster.#Deploy", "builtin.#Suspend"
|
||||
params map[string]Value // parameters to pass
|
||||
useFullParam bool // if true, generates $params: parameter instead of $params: { key: value }
|
||||
directFields bool // if true, renders fields directly without $params wrapper (for op.# operations)
|
||||
}
|
||||
|
||||
func (b *BuiltinAction) isWorkflowAction() {}
|
||||
@@ -78,6 +79,17 @@ type ConditionalAction struct {
|
||||
|
||||
func (c *ConditionalAction) isWorkflowAction() {}
|
||||
|
||||
// GuardedBlockAction represents a field that always exists with a guard condition inside.
|
||||
// Generates: name: { if cond { ...contents... } }
|
||||
// Unlike ConditionalAction which generates: if cond { name: { ...contents... } }
|
||||
type GuardedBlockAction struct {
|
||||
cond Condition
|
||||
name string
|
||||
value Value
|
||||
}
|
||||
|
||||
func (g *GuardedBlockAction) isWorkflowAction() {}
|
||||
|
||||
// NewWorkflowStep creates a new WorkflowStepDefinition builder.
|
||||
func NewWorkflowStep(name string) *WorkflowStepDefinition {
|
||||
return &WorkflowStepDefinition{
|
||||
@@ -365,6 +377,19 @@ func (wt *WorkflowStepTemplate) SetIf(cond Condition, name string, value Value)
|
||||
return wt
|
||||
}
|
||||
|
||||
// SetGuardedBlock assigns a value to a field with the guard condition placed inside
|
||||
// the field block, so the field always exists (possibly empty).
|
||||
// Generates: name: { if cond { ...contents... } }
|
||||
// Unlike SetIf which generates: if cond { name: { ...contents... } }
|
||||
func (wt *WorkflowStepTemplate) SetGuardedBlock(cond Condition, name string, value Value) *WorkflowStepTemplate {
|
||||
wt.actions = append(wt.actions, &GuardedBlockAction{
|
||||
cond: cond,
|
||||
name: name,
|
||||
value: value,
|
||||
})
|
||||
return wt
|
||||
}
|
||||
|
||||
// Suspend adds a suspend action.
|
||||
// Example: tpl.Suspend("Waiting for approval")
|
||||
func (wt *WorkflowStepTemplate) Suspend(message string) *WorkflowStepTemplate {
|
||||
@@ -416,6 +441,14 @@ func (b *BuiltinActionBuilder) WithFullParameter() *BuiltinActionBuilder {
|
||||
return b
|
||||
}
|
||||
|
||||
// WithDirectFields renders fields directly on the struct without the $params wrapper.
|
||||
// This is used for op.# operations (e.g., op.#ShareCloudResource, op.#DeployCloudResource)
|
||||
// that take fields as direct struct members rather than inside $params.
|
||||
func (b *BuiltinActionBuilder) WithDirectFields() *BuiltinActionBuilder {
|
||||
b.action.directFields = true
|
||||
return b
|
||||
}
|
||||
|
||||
// Build finalizes the action and adds it to the template.
|
||||
func (b *BuiltinActionBuilder) Build() *WorkflowStepTemplate {
|
||||
b.template.actions = append(b.template.actions, b.action)
|
||||
@@ -566,8 +599,11 @@ func (g *WorkflowStepCUEGenerator) GenerateTemplate(w *WorkflowStepDefinition) s
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Generate parameter section
|
||||
sb.WriteString(g.generateParameterBlock(w, 1))
|
||||
// Generate parameter section (skip if no params and raw body is set,
|
||||
// e.g. step-group which uses TemplateBody with no parameter references)
|
||||
if len(w.GetParams()) > 0 || !w.HasRawTemplateBody() {
|
||||
sb.WriteString(g.generateParameterBlock(w, 1))
|
||||
}
|
||||
|
||||
if w.GetCustomStatus() != "" || w.GetHealthPolicy() != "" || w.GetStatusDetails() != "" {
|
||||
indent := g.indent
|
||||
@@ -622,6 +658,8 @@ func (g *WorkflowStepCUEGenerator) writeActions(sb *strings.Builder, wt *Workflo
|
||||
g.writeValueAction(sb, value, "\t", indent, gen)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
case *GuardedBlockAction:
|
||||
g.writeGuardedBlockAction(sb, a, indent, gen)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -636,10 +674,16 @@ func (g *WorkflowStepCUEGenerator) writeBuiltinAction(sb *strings.Builder, a *Bu
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: %s & {\n", indent, extraIndent, actionName, a.name))
|
||||
if a.useFullParam {
|
||||
switch {
|
||||
case a.useFullParam:
|
||||
// Pass the entire parameter object as $params (e.g., builtin.#Suspend)
|
||||
sb.WriteString(fmt.Sprintf("%s%s\t$params: parameter\n", indent, extraIndent))
|
||||
} else if len(a.params) > 0 {
|
||||
case a.directFields && len(a.params) > 0:
|
||||
// Render fields directly without $params wrapper (for op.# operations)
|
||||
for paramName, paramVal := range a.params {
|
||||
sb.WriteString(fmt.Sprintf("%s%s\t%s: %s\n", indent, extraIndent, paramName, gen.valueToCUE(paramVal)))
|
||||
}
|
||||
case len(a.params) > 0:
|
||||
sb.WriteString(fmt.Sprintf("%s%s\t$params: {\n", indent, extraIndent))
|
||||
for paramName, paramVal := range a.params {
|
||||
sb.WriteString(fmt.Sprintf("%s%s\t\t%s: %s\n", indent, extraIndent, paramName, gen.valueToCUE(paramVal)))
|
||||
@@ -658,6 +702,63 @@ func (g *WorkflowStepCUEGenerator) writeValueAction(sb *strings.Builder, a *Valu
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, extraIndent, name, gen.valueToCUE(a.value)))
|
||||
}
|
||||
|
||||
// writeGuardedBlockAction writes a field that always exists with a guard condition inside.
|
||||
// Output: name: { if cond { ...value contents... } }
|
||||
func (g *WorkflowStepCUEGenerator) writeGuardedBlockAction(sb *strings.Builder, a *GuardedBlockAction, indent string, gen *CUEGenerator) {
|
||||
name := a.name
|
||||
if strings.ContainsAny(name, "-./") {
|
||||
name = fmt.Sprintf("%q", name)
|
||||
}
|
||||
innerIndent := indent + g.indent
|
||||
condStr := gen.conditionToCUE(a.cond)
|
||||
valStr := gen.valueToCUE(a.value)
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s%s: {\n", indent, name))
|
||||
sb.WriteString(fmt.Sprintf("%sif %s {\n", innerIndent, condStr))
|
||||
|
||||
// If the value is a block (starts with { and ends with }), strip the outer braces
|
||||
// and re-indent the inner content, preserving relative indentation.
|
||||
trimmed := strings.TrimSpace(valStr)
|
||||
if strings.HasPrefix(trimmed, "{") && strings.HasSuffix(trimmed, "}") {
|
||||
// Strip outer braces
|
||||
inner := trimmed[1 : len(trimmed)-1]
|
||||
lines := strings.Split(inner, "\n")
|
||||
|
||||
// Find minimum indentation of non-empty lines to dedent by
|
||||
minIndent := -1
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
indent := len(line) - len(strings.TrimLeft(line, " \t"))
|
||||
if minIndent < 0 || indent < minIndent {
|
||||
minIndent = indent
|
||||
}
|
||||
}
|
||||
if minIndent < 0 {
|
||||
minIndent = 0
|
||||
}
|
||||
|
||||
// Re-emit lines with relative indentation preserved
|
||||
targetIndent := innerIndent + "\t"
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
// Strip common leading indent, then prepend target indent
|
||||
if len(line) >= minIndent {
|
||||
line = line[minIndent:]
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s%s\n", targetIndent, line))
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s\n", innerIndent, trimmed))
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", innerIndent))
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
}
|
||||
|
||||
// extractActionName extracts a simple action name from a builtin reference.
|
||||
func extractActionName(builtinRef string) string {
|
||||
// "multicluster.#Deploy" -> "deploy"
|
||||
|
||||
@@ -506,4 +506,123 @@ template: {
|
||||
Expect(yaml).To(ContainSubstring("Actual Description"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("SetGuardedBlock", func() {
|
||||
It("should preserve nested indentation for multi-line values", func() {
|
||||
step := defkit.NewWorkflowStep("notifier").
|
||||
Description("test").
|
||||
WithImports("vela/http", "vela/kube").
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.SetGuardedBlock(
|
||||
defkit.PathExists("parameter.channel"),
|
||||
"action",
|
||||
defkit.NewArrayElement().
|
||||
SetIf(defkit.PathExists("parameter.channel.url"), "post1",
|
||||
defkit.HTTPPost(defkit.Reference("parameter.channel.url")).
|
||||
Body(defkit.Reference("json.Marshal(parameter.channel.msg)")).
|
||||
Header("Content-Type", "application/json"),
|
||||
).
|
||||
SetIf(defkit.PathExists("parameter.channel.secret"), "read",
|
||||
defkit.KubeRead("v1", "Secret").
|
||||
Name(defkit.Reference("parameter.channel.secret.name")).
|
||||
Namespace(defkit.Reference("context.namespace")),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
cue := step.ToCue()
|
||||
|
||||
// The HTTPDo block should be indented deeper than the if block
|
||||
Expect(cue).To(ContainSubstring("action: {"))
|
||||
Expect(cue).To(ContainSubstring("if parameter.channel != _|_ {"))
|
||||
|
||||
// Verify nested indentation: $params must be indented inside http.#HTTPDo
|
||||
lines := strings.Split(cue, "\n")
|
||||
var httpDoLine, paramsLine, methodLine int
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "post1: http.#HTTPDo") {
|
||||
httpDoLine = i
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "$params: {") && httpDoLine > 0 && paramsLine == 0 {
|
||||
paramsLine = i
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "method: \"POST\"") && paramsLine > 0 && methodLine == 0 {
|
||||
methodLine = i
|
||||
}
|
||||
}
|
||||
Expect(httpDoLine).To(BeNumerically(">", 0), "should find post1: http.#HTTPDo line")
|
||||
Expect(paramsLine).To(BeNumerically(">", httpDoLine), "should find $params after http.#HTTPDo")
|
||||
Expect(methodLine).To(BeNumerically(">", paramsLine), "should find method after $params")
|
||||
|
||||
// Verify each level is indented one more tab than its parent
|
||||
httpDoIndent := len(lines[httpDoLine]) - len(strings.TrimLeft(lines[httpDoLine], "\t"))
|
||||
paramsIndent := len(lines[paramsLine]) - len(strings.TrimLeft(lines[paramsLine], "\t"))
|
||||
methodIndent := len(lines[methodLine]) - len(strings.TrimLeft(lines[methodLine], "\t"))
|
||||
Expect(paramsIndent).To(Equal(httpDoIndent + 1))
|
||||
Expect(methodIndent).To(Equal(paramsIndent + 1))
|
||||
})
|
||||
|
||||
It("should handle simple non-block values", func() {
|
||||
step := defkit.NewWorkflowStep("simple").
|
||||
Description("test").
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.SetGuardedBlock(
|
||||
defkit.PathExists("parameter.x"),
|
||||
"result",
|
||||
defkit.Reference("parameter.x"),
|
||||
)
|
||||
})
|
||||
|
||||
cue := step.ToCue()
|
||||
Expect(cue).To(ContainSubstring("result: {"))
|
||||
Expect(cue).To(ContainSubstring("if parameter.x != _|_ {"))
|
||||
Expect(cue).To(ContainSubstring("parameter.x"))
|
||||
})
|
||||
|
||||
It("should produce correct kube.#Read indentation inside guarded block", func() {
|
||||
step := defkit.NewWorkflowStep("reader").
|
||||
Description("test").
|
||||
WithImports("vela/kube").
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.SetGuardedBlock(
|
||||
defkit.PathExists("parameter.target"),
|
||||
"fetch",
|
||||
defkit.NewArrayElement().
|
||||
Set("read", defkit.KubeRead("v1", "Secret").
|
||||
Name(defkit.Reference("parameter.target.name")).
|
||||
Namespace(defkit.Reference("context.namespace")),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
cue := step.ToCue()
|
||||
lines := strings.Split(cue, "\n")
|
||||
|
||||
// Find the kube.#Read line and metadata line
|
||||
var readLine, metadataLine, nameLine int
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "read: kube.#Read") {
|
||||
readLine = i
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "metadata: {") && readLine > 0 && metadataLine == 0 {
|
||||
metadataLine = i
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "name:") && metadataLine > 0 && nameLine == 0 {
|
||||
nameLine = i
|
||||
}
|
||||
}
|
||||
Expect(readLine).To(BeNumerically(">", 0))
|
||||
Expect(metadataLine).To(BeNumerically(">", readLine))
|
||||
Expect(nameLine).To(BeNumerically(">", metadataLine))
|
||||
|
||||
// Verify progressive indentation
|
||||
readIndent := len(lines[readLine]) - len(strings.TrimLeft(lines[readLine], "\t"))
|
||||
metaIndent := len(lines[metadataLine]) - len(strings.TrimLeft(lines[metadataLine], "\t"))
|
||||
nameIndent := len(lines[nameLine]) - len(strings.TrimLeft(lines[nameLine], "\t"))
|
||||
Expect(metaIndent).To(BeNumerically(">", readIndent))
|
||||
Expect(nameIndent).To(BeNumerically(">", metaIndent))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user