mirror of
https://github.com/kubevela/kubevela.git
synced 2026-05-06 01:17:09 +00:00
Fix: defkit CUE generation for task health, nested array schemas, and patch traits (#7041)
* Fix: Update JobHealth logic to reflect correct health status based on parallelism Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: vishal210893 <vishal210893@gmail.com> * Fix: Correct formatting in status output for better readability Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: vishal210893 <vishal210893@gmail.com> * Fix: Enhance CUE generation for nested array structs and lift shared conditions to parent nodes Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: vishal210893 <vishal210893@gmail.com> * Fix: Improve handling of array values in CUE generation for patchKey operations Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: vishal210893 <vishal210893@gmail.com> * Feat: Defkit Refactor and Clean-Up (#7042) * feat: Enhance status and health policy CUE generation with field grouping, column alignment, `_isHealth` pattern, and annotation-based health disable. Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * feat: introduce defkit package for a structured Go API to define KubeVela component and trait templates with outputs, patches, and helpers. Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> * refactor: Consolidate two `append` calls into one for health expression parts. Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> --------- Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com> update status.go Signed-off-by: vishal210893 <vishal210893@gmail.com> * Fix: Correct formatting in status output for better readability Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> # Conflicts: # pkg/definition/defkit/status.go Signed-off-by: vishal210893 <vishal210893@gmail.com> * removed unitended commited file Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: vishal210893 <vishal210893@gmail.com> * Fix: Preserve explicit action variable names in BuiltinAction and update CUE generation Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: vishal210893 <vishal210893@gmail.com> * Fix: Update default WebhookConfig settings for improved configuration Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: vishal210893 <vishal210893@gmail.com> * Fix: Enhance CUE generation with ForEachMapOp support and add alias handling in workflow steps Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com> Signed-off-by: vishal210893 <vishal210893@gmail.com> --------- Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com> Signed-off-by: vishal210893 <vishal210893@gmail.com> Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com> Co-authored-by: Ayush Kumar <65535504+roguepikachu@users.noreply.github.com>
This commit is contained in:
@@ -461,11 +461,19 @@ func (g *CUEGenerator) writeStructFieldForHelper(sb *strings.Builder, f *StructF
|
||||
|
||||
// Check for nested struct
|
||||
if nested := f.GetNested(); nested != nil {
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional))
|
||||
for _, nestedField := range nested.GetFields() {
|
||||
g.writeStructFieldForHelper(sb, nestedField, depth+1)
|
||||
if f.FieldType() == ParamTypeArray {
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: [...{\n", indent, name, optional))
|
||||
for _, nestedField := range nested.GetFields() {
|
||||
g.writeStructFieldForHelper(sb, nestedField, depth+1)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}]\n", indent))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional))
|
||||
for _, nestedField := range nested.GetFields() {
|
||||
g.writeStructFieldForHelper(sb, nestedField, depth+1)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1155,6 +1163,11 @@ func (g *CUEGenerator) insertPatchKeyIntoTree(root *fieldNode, op *PatchKeyOp, c
|
||||
func (g *CUEGenerator) writeFieldTree(sb *strings.Builder, node *fieldNode, depth int) {
|
||||
indent := strings.Repeat(g.indent, depth)
|
||||
|
||||
// Lift child conditions to the parent field when all children share the same
|
||||
// condition and the parent has no own value/spread/foreach/patchKey. This avoids
|
||||
// emitting empty parent structs like `foo: { if cond { bar: ... } }`.
|
||||
g.liftChildConditions(node)
|
||||
|
||||
// Write spread entries FIRST (user labels spread before OAM labels)
|
||||
// This matches the KubeVela convention of spreading user-provided values
|
||||
// before adding fixed OAM labels, so user values can be overridden if needed.
|
||||
@@ -1200,6 +1213,57 @@ func (g *CUEGenerator) writeFieldTree(sb *strings.Builder, node *fieldNode, dept
|
||||
}
|
||||
}
|
||||
|
||||
// liftChildConditions promotes a shared child condition to the parent node.
|
||||
// It recursively processes the tree so inner nodes are normalized before parent rendering.
|
||||
func (g *CUEGenerator) liftChildConditions(node *fieldNode) {
|
||||
for _, name := range node.childOrder {
|
||||
child := node.children[name]
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
// Recurse first so deeper nodes are normalized
|
||||
g.liftChildConditions(child)
|
||||
|
||||
if child.cond != nil {
|
||||
continue
|
||||
}
|
||||
if child.value != nil || child.isArray || len(child.spreads) > 0 || child.forEach != nil || child.patchKey != nil {
|
||||
continue
|
||||
}
|
||||
if len(child.children) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var sharedCond Condition
|
||||
var sharedCondStr string
|
||||
canLift := true
|
||||
for _, grandName := range child.childOrder {
|
||||
grand := child.children[grandName]
|
||||
if grand == nil || grand.cond == nil {
|
||||
canLift = false
|
||||
break
|
||||
}
|
||||
condStr := g.conditionToCUE(grand.cond)
|
||||
if sharedCondStr == "" {
|
||||
sharedCondStr = condStr
|
||||
sharedCond = grand.cond
|
||||
} else if sharedCondStr != condStr {
|
||||
canLift = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if canLift && sharedCond != nil {
|
||||
child.cond = sharedCond
|
||||
for _, grandName := range child.childOrder {
|
||||
grand := child.children[grandName]
|
||||
if grand != nil {
|
||||
grand.cond = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -1230,6 +1294,45 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// Regular field
|
||||
if node.value != nil && len(node.children) == 0 {
|
||||
// Leaf node with value
|
||||
@@ -1320,6 +1423,8 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
|
||||
return fmt.Sprintf("%s.%s", val.VarName(), val.FieldName())
|
||||
case *IterLetRef:
|
||||
return val.RefName()
|
||||
case *ForEachMapOp:
|
||||
return g.forEachMapOpToCUE(val)
|
||||
default:
|
||||
// Try to get name from Param interface
|
||||
if p, ok := v.(Param); ok {
|
||||
@@ -1329,6 +1434,32 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
|
||||
}
|
||||
}
|
||||
|
||||
// forEachMapOpToCUE converts a ForEachMapOp to CUE map comprehension syntax.
|
||||
// Generates: {for k, v in source { (keyExpr): valExpr }}.
|
||||
func (g *CUEGenerator) forEachMapOpToCUE(op *ForEachMapOp) string {
|
||||
keyVar := op.KeyVar()
|
||||
if keyVar == "" {
|
||||
keyVar = "k"
|
||||
}
|
||||
|
||||
valVar := op.ValVar()
|
||||
if valVar == "" {
|
||||
valVar = "v"
|
||||
}
|
||||
|
||||
keyExpr := op.KeyExpr()
|
||||
if keyExpr == "" {
|
||||
keyExpr = keyVar
|
||||
}
|
||||
|
||||
valExpr := op.ValExpr()
|
||||
if valExpr == "" {
|
||||
valExpr = valVar
|
||||
}
|
||||
|
||||
return fmt.Sprintf("{for %s, %s in %s { (%s): %s }}", keyVar, valVar, op.Source(), keyExpr, valExpr)
|
||||
}
|
||||
|
||||
// cueFuncToCUE converts a CUE function call to CUE syntax.
|
||||
func (g *CUEGenerator) cueFuncToCUE(fn *CUEFunc) string {
|
||||
args := make([]string, len(fn.Args()))
|
||||
@@ -2343,11 +2474,19 @@ func (g *CUEGenerator) writeStructField(sb *strings.Builder, f *StructField, dep
|
||||
nested := f.GetNested()
|
||||
switch {
|
||||
case nested != nil:
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional))
|
||||
for _, nestedField := range nested.GetFields() {
|
||||
g.writeStructField(sb, nestedField, depth+1)
|
||||
if f.FieldType() == ParamTypeArray {
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: [...{\n", indent, name, optional))
|
||||
for _, nestedField := range nested.GetFields() {
|
||||
g.writeStructField(sb, nestedField, depth+1)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}]\n", indent))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional))
|
||||
for _, nestedField := range nested.GetFields() {
|
||||
g.writeStructField(sb, nestedField, depth+1)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
case f.HasDefault():
|
||||
enumValues := f.GetEnumValues()
|
||||
switch {
|
||||
|
||||
@@ -182,6 +182,25 @@ var _ = Describe("CUEGenerator", func() {
|
||||
Expect(cue).To(ContainSubstring("name?:"))
|
||||
Expect(cue).To(ContainSubstring("protocol:"))
|
||||
})
|
||||
|
||||
It("should generate CUE for nested array struct fields", func() {
|
||||
comp := defkit.NewComponent("test").
|
||||
Params(
|
||||
defkit.Struct("selector").Fields(
|
||||
defkit.Field("matchExpressions", defkit.ParamTypeArray).
|
||||
Nested(defkit.Struct("matchExpression").Fields(
|
||||
defkit.Field("key", defkit.ParamTypeString).Required(),
|
||||
defkit.Field("operator", defkit.ParamTypeString),
|
||||
)),
|
||||
),
|
||||
)
|
||||
|
||||
cue := gen.GenerateParameterSchema(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring("matchExpressions?: [...{"))
|
||||
Expect(cue).To(ContainSubstring("key: string"))
|
||||
Expect(cue).To(ContainSubstring("operator?: string"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GenerateFullDefinition", func() {
|
||||
|
||||
@@ -839,7 +839,14 @@ func (f *StructField) Description(desc string) *StructField {
|
||||
// Nested sets a nested struct definition for this field.
|
||||
func (f *StructField) Nested(s *StructParam) *StructField {
|
||||
f.nested = s
|
||||
f.fieldType = ParamTypeStruct
|
||||
if f.fieldType == ParamTypeArray {
|
||||
// Preserve array type; this indicates an array of nested structs.
|
||||
if f.elementType == "" {
|
||||
f.elementType = ParamTypeStruct
|
||||
}
|
||||
} else {
|
||||
f.fieldType = ParamTypeStruct
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
|
||||
@@ -134,10 +134,9 @@ func (s *StatusBuilder) buildField(f *StatusField) string {
|
||||
}
|
||||
|
||||
// Simple field
|
||||
return fmt.Sprintf(`%s: %s & {
|
||||
if context.output.%s != _|_ {
|
||||
%s: context.output.%s
|
||||
}
|
||||
return fmt.Sprintf(`%s: %s
|
||||
if context.output.%s != _|_ {
|
||||
%s: context.output.%s
|
||||
}`, f.name, defExpr, f.sourcePath, f.name, f.sourcePath)
|
||||
}
|
||||
|
||||
@@ -439,8 +438,7 @@ func StatefulSetHealth() *HealthBuilder {
|
||||
func JobHealth() *HealthBuilder {
|
||||
return Health().
|
||||
IntField("succeeded", "status.succeeded", 0).
|
||||
IntField("failed", "status.failed", 0).
|
||||
HealthyWhen("succeeded >= 1 || failed >= 1")
|
||||
HealthyWhen("succeeded == context.output.spec.parallelism")
|
||||
}
|
||||
|
||||
// CronJobHealth returns a pre-configured health builder for CronJob.
|
||||
|
||||
@@ -218,10 +218,8 @@ var _ = Describe("Status", func() {
|
||||
h := defkit.JobHealth()
|
||||
cue := h.Build()
|
||||
Expect(cue).To(ContainSubstring("succeeded:"))
|
||||
Expect(cue).To(ContainSubstring("failed:"))
|
||||
Expect(cue).To(ContainSubstring("status.succeeded"))
|
||||
Expect(cue).To(ContainSubstring("status.failed"))
|
||||
Expect(cue).To(ContainSubstring("isHealth: succeeded >= 1 || failed >= 1"))
|
||||
Expect(cue).To(ContainSubstring("isHealth: succeeded == context.output.spec.parallelism"))
|
||||
})
|
||||
|
||||
It("should create CronJobHealth builder", func() {
|
||||
|
||||
@@ -642,6 +642,8 @@ func (g *TraitCUEGenerator) writePatchResourceOps(sb *strings.Builder, gen *CUEG
|
||||
|
||||
// Build a tree structure from operations (similar to component resources)
|
||||
tree := gen.buildFieldTree(ops)
|
||||
// Normalize conditional nodes to avoid empty parent structs in patches
|
||||
gen.liftChildConditions(tree)
|
||||
g.writePatchFieldTree(sb, gen, tree, depth)
|
||||
}
|
||||
|
||||
@@ -787,44 +789,59 @@ func (g *TraitCUEGenerator) writeForEachOp(sb *strings.Builder, gen *CUEGenerato
|
||||
// If cond is provided, wraps in: if cond { ... }
|
||||
func (g *TraitCUEGenerator) writePatchKeyOp(sb *strings.Builder, gen *CUEGenerator, key string, op *PatchKeyOp, cond Condition, depth int) {
|
||||
indent := strings.Repeat(g.indent, depth)
|
||||
elements := op.Elements()
|
||||
useArrayValue := len(elements) == 1
|
||||
if useArrayValue {
|
||||
_, useArrayValue = elements[0].(*ArrayParam)
|
||||
}
|
||||
|
||||
// Wrap in condition if present
|
||||
if cond != nil {
|
||||
condStr := gen.conditionToCUE(cond)
|
||||
sb.WriteString(fmt.Sprintf("if %s {\n", condStr))
|
||||
sb.WriteString(fmt.Sprintf("%s\t// +patchKey=%s\n", indent, op.Key()))
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: [", indent, key))
|
||||
for i, elem := range op.Elements() {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
// Use depth-aware formatting for ArrayElement
|
||||
if arrElem, ok := elem.(*ArrayElement); ok {
|
||||
sb.WriteString(gen.arrayElementToCUEWithDepth(arrElem, depth+1))
|
||||
} else {
|
||||
sb.WriteString(gen.valueToCUE(elem))
|
||||
if useArrayValue {
|
||||
valStr := gen.valueToCUEAtDepth(elements[0], depth+1)
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", indent, key, valStr))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s\t%s: [", indent, key))
|
||||
for i, elem := range elements {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
// Use depth-aware formatting for ArrayElement
|
||||
if arrElem, ok := elem.(*ArrayElement); ok {
|
||||
sb.WriteString(gen.arrayElementToCUEWithDepth(arrElem, depth+1))
|
||||
} else {
|
||||
sb.WriteString(gen.valueToCUE(elem))
|
||||
}
|
||||
}
|
||||
sb.WriteString("]\n")
|
||||
}
|
||||
sb.WriteString("]\n")
|
||||
sb.WriteString(fmt.Sprintf("%s}", indent))
|
||||
} else {
|
||||
// Write the patchKey annotation comment
|
||||
sb.WriteString(fmt.Sprintf("// +patchKey=%s\n", op.Key()))
|
||||
sb.WriteString(fmt.Sprintf("%s%s: [", indent, key))
|
||||
if useArrayValue {
|
||||
valStr := gen.valueToCUEAtDepth(elements[0], depth)
|
||||
sb.WriteString(fmt.Sprintf("%s%s: %s", indent, key, valStr))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s%s: [", indent, key))
|
||||
|
||||
// Write elements
|
||||
for i, elem := range op.Elements() {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
// Use depth-aware formatting for ArrayElement
|
||||
if arrElem, ok := elem.(*ArrayElement); ok {
|
||||
sb.WriteString(gen.arrayElementToCUEWithDepth(arrElem, depth))
|
||||
} else {
|
||||
sb.WriteString(gen.valueToCUE(elem))
|
||||
// Write elements
|
||||
for i, elem := range elements {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
// Use depth-aware formatting for ArrayElement
|
||||
if arrElem, ok := elem.(*ArrayElement); ok {
|
||||
sb.WriteString(gen.arrayElementToCUEWithDepth(arrElem, depth))
|
||||
} else {
|
||||
sb.WriteString(gen.valueToCUE(elem))
|
||||
}
|
||||
}
|
||||
sb.WriteString("]")
|
||||
}
|
||||
sb.WriteString("]")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ type WorkflowStepDefinition struct {
|
||||
baseDefinition // embedded common fields and methods
|
||||
category string // e.g., "Application Delivery", "Notification"
|
||||
scope string // e.g., "Application", "Workflow"
|
||||
alias string // optional alias for definition metadata annotation
|
||||
hasAlias bool // tracks whether alias was explicitly set (including empty string)
|
||||
stepTemplate func(tpl *WorkflowStepTemplate) // template function for step logic (type-specific)
|
||||
}
|
||||
|
||||
@@ -49,12 +51,21 @@ type WorkflowAction interface {
|
||||
|
||||
// BuiltinAction represents a call to a vela builtin.
|
||||
type BuiltinAction struct {
|
||||
name string // e.g., "multicluster.#Deploy", "builtin.#Suspend"
|
||||
params map[string]Value // parameters to pass
|
||||
varName string // explicit action variable name in template (e.g., "deploy", "wait")
|
||||
name string // e.g., "multicluster.#Deploy", "builtin.#Suspend"
|
||||
params map[string]Value // parameters to pass
|
||||
}
|
||||
|
||||
func (b *BuiltinAction) isWorkflowAction() {}
|
||||
|
||||
// ValueAction represents assigning a value to a template field.
|
||||
type ValueAction struct {
|
||||
name string
|
||||
value Value
|
||||
}
|
||||
|
||||
func (v *ValueAction) isWorkflowAction() {}
|
||||
|
||||
// ConditionalAction represents a conditional workflow action.
|
||||
type ConditionalAction struct {
|
||||
cond Condition
|
||||
@@ -93,6 +104,14 @@ func (w *WorkflowStepDefinition) Scope(scope string) *WorkflowStepDefinition {
|
||||
return w
|
||||
}
|
||||
|
||||
// Alias sets an optional alias for the workflow step definition.
|
||||
// This maps to metadata annotation `definition.oam.dev/alias` in generated YAML.
|
||||
func (w *WorkflowStepDefinition) Alias(alias string) *WorkflowStepDefinition {
|
||||
w.alias = alias
|
||||
w.hasAlias = true
|
||||
return w
|
||||
}
|
||||
|
||||
// Params adds multiple parameter definitions to the workflow step.
|
||||
func (w *WorkflowStepDefinition) Params(params ...Param) *WorkflowStepDefinition {
|
||||
w.addParams(params...)
|
||||
@@ -201,6 +220,12 @@ func (w *WorkflowStepDefinition) GetCategory() string { return w.category }
|
||||
// GetScope returns the workflow step scope.
|
||||
func (w *WorkflowStepDefinition) GetScope() string { return w.scope }
|
||||
|
||||
// GetAlias returns the workflow step alias.
|
||||
func (w *WorkflowStepDefinition) GetAlias() string { return w.alias }
|
||||
|
||||
// HasAlias returns true if alias was explicitly set.
|
||||
func (w *WorkflowStepDefinition) HasAlias() bool { return w.hasAlias }
|
||||
|
||||
// ToCue generates the complete CUE definition string for this workflow step.
|
||||
func (w *WorkflowStepDefinition) ToCue() string {
|
||||
// If raw CUE is set, use it with the name from NewWorkflowStep() taking precedence
|
||||
@@ -254,8 +279,9 @@ func NewWorkflowStepTemplate() *WorkflowStepTemplate {
|
||||
// Example: tpl.Builtin("deploy", "multicluster.#Deploy").WithParams(...)
|
||||
func (wt *WorkflowStepTemplate) Builtin(name, builtinRef string) *BuiltinActionBuilder {
|
||||
action := &BuiltinAction{
|
||||
name: builtinRef,
|
||||
params: make(map[string]Value),
|
||||
varName: name,
|
||||
name: builtinRef,
|
||||
params: make(map[string]Value),
|
||||
}
|
||||
return &BuiltinActionBuilder{
|
||||
template: wt,
|
||||
@@ -264,6 +290,23 @@ func (wt *WorkflowStepTemplate) Builtin(name, builtinRef string) *BuiltinActionB
|
||||
}
|
||||
}
|
||||
|
||||
// Set assigns a value to a top-level field in the workflow template.
|
||||
// Example: tpl.Set("object", someValue)
|
||||
func (wt *WorkflowStepTemplate) Set(name string, value Value) *WorkflowStepTemplate {
|
||||
wt.actions = append(wt.actions, &ValueAction{name: name, value: value})
|
||||
return wt
|
||||
}
|
||||
|
||||
// SetIf conditionally assigns a value to a top-level field in the workflow template.
|
||||
// Example: tpl.SetIf(param.IsSet(), "object", someValue)
|
||||
func (wt *WorkflowStepTemplate) SetIf(cond Condition, name string, value Value) *WorkflowStepTemplate {
|
||||
wt.actions = append(wt.actions, &ConditionalAction{
|
||||
cond: cond,
|
||||
action: &ValueAction{name: name, value: value},
|
||||
})
|
||||
return wt
|
||||
}
|
||||
|
||||
// Suspend adds a suspend action.
|
||||
// Example: tpl.Suspend("Waiting for approval")
|
||||
func (wt *WorkflowStepTemplate) Suspend(message string) *WorkflowStepTemplate {
|
||||
@@ -297,7 +340,6 @@ type BuiltinActionBuilder struct {
|
||||
template *WorkflowStepTemplate
|
||||
action *BuiltinAction
|
||||
varName string
|
||||
cond Condition // optional condition set by If()
|
||||
}
|
||||
|
||||
// WithParams sets parameters for the builtin.
|
||||
@@ -310,21 +352,18 @@ func (b *BuiltinActionBuilder) WithParams(params map[string]Value) *BuiltinActio
|
||||
|
||||
// Build finalizes the action and adds it to the template.
|
||||
func (b *BuiltinActionBuilder) Build() *WorkflowStepTemplate {
|
||||
if b.cond != nil {
|
||||
b.template.actions = append(b.template.actions, &ConditionalAction{
|
||||
cond: b.cond,
|
||||
action: b.action,
|
||||
})
|
||||
} else {
|
||||
b.template.actions = append(b.template.actions, b.action)
|
||||
}
|
||||
b.template.actions = append(b.template.actions, b.action)
|
||||
return b.template
|
||||
}
|
||||
|
||||
// If makes this action conditional.
|
||||
// The condition is applied when Build() is called.
|
||||
func (b *BuiltinActionBuilder) If(cond Condition) *BuiltinActionBuilder {
|
||||
b.cond = cond
|
||||
// Replace action with conditional version
|
||||
condAction := &ConditionalAction{
|
||||
cond: cond,
|
||||
action: b.action,
|
||||
}
|
||||
b.template.actions = append(b.template.actions, condAction)
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -385,6 +424,11 @@ func (g *WorkflowStepCUEGenerator) GenerateFullDefinition(w *WorkflowStepDefinit
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
|
||||
|
||||
// Write alias when explicitly set (including empty string).
|
||||
if w.HasAlias() {
|
||||
sb.WriteString(fmt.Sprintf("%salias: %q\n", g.indent, w.GetAlias()))
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%sdescription: %q\n", g.indent, w.GetDescription()))
|
||||
sb.WriteString("}\n")
|
||||
|
||||
@@ -431,12 +475,17 @@ func (g *WorkflowStepCUEGenerator) writeActions(sb *strings.Builder, wt *Workflo
|
||||
switch a := action.(type) {
|
||||
case *BuiltinAction:
|
||||
g.writeBuiltinAction(sb, a, "", indent, gen)
|
||||
case *ValueAction:
|
||||
g.writeValueAction(sb, a, "", indent, gen)
|
||||
case *ConditionalAction:
|
||||
condStr := gen.conditionToCUE(a.cond)
|
||||
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
|
||||
if builtin, ok := a.action.(*BuiltinAction); ok {
|
||||
g.writeBuiltinAction(sb, builtin, "\t", indent, gen)
|
||||
}
|
||||
if value, ok := a.action.(*ValueAction); ok {
|
||||
g.writeValueAction(sb, value, "\t", indent, gen)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
}
|
||||
}
|
||||
@@ -444,9 +493,12 @@ func (g *WorkflowStepCUEGenerator) writeActions(sb *strings.Builder, wt *Workflo
|
||||
|
||||
// writeBuiltinAction writes a builtin action.
|
||||
func (g *WorkflowStepCUEGenerator) writeBuiltinAction(sb *strings.Builder, a *BuiltinAction, extraIndent, indent string, gen *CUEGenerator) {
|
||||
// Extract the action name from the builtin reference
|
||||
// e.g., "multicluster.#Deploy" -> "deploy"
|
||||
actionName := extractActionName(a.name)
|
||||
actionName := a.varName
|
||||
if actionName == "" {
|
||||
// Backward-compatible fallback when no explicit name is set.
|
||||
// e.g., "multicluster.#Deploy" -> "deploy"
|
||||
actionName = extractActionName(a.name)
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: %s & {\n", indent, extraIndent, actionName, a.name))
|
||||
if len(a.params) > 0 {
|
||||
@@ -459,6 +511,15 @@ func (g *WorkflowStepCUEGenerator) writeBuiltinAction(sb *strings.Builder, a *Bu
|
||||
sb.WriteString(fmt.Sprintf("%s%s}\n", indent, extraIndent))
|
||||
}
|
||||
|
||||
// writeValueAction writes a value assignment action.
|
||||
func (g *WorkflowStepCUEGenerator) writeValueAction(sb *strings.Builder, a *ValueAction, extraIndent, indent string, gen *CUEGenerator) {
|
||||
name := a.name
|
||||
if strings.ContainsAny(name, "-./") {
|
||||
name = fmt.Sprintf("%q", name)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, extraIndent, name, gen.valueToCUE(a.value)))
|
||||
}
|
||||
|
||||
// extractActionName extracts a simple action name from a builtin reference.
|
||||
func extractActionName(builtinRef string) string {
|
||||
// "multicluster.#Deploy" -> "deploy"
|
||||
|
||||
@@ -234,6 +234,30 @@ template: {
|
||||
cue := step.ToCue()
|
||||
Expect(cue).To(ContainSubstring(`template:`))
|
||||
})
|
||||
|
||||
It("should preserve explicit builtin action names", func() {
|
||||
step := defkit.NewWorkflowStep("apply-deployment").
|
||||
Description("Apply deployment with specified image and cmd.").
|
||||
WithImports("vela/kube", "vela/builtin").
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Builtin("output", "kube.#Apply").
|
||||
WithParams(map[string]defkit.Value{
|
||||
"value": defkit.Reference("parameter.value"),
|
||||
}).
|
||||
Build()
|
||||
tpl.Builtin("wait", "builtin.#ConditionalWait").
|
||||
WithParams(map[string]defkit.Value{
|
||||
"continue": defkit.Reference("output.$returns.value.status.readyReplicas == parameter.replicas"),
|
||||
}).
|
||||
Build()
|
||||
})
|
||||
|
||||
cue := step.ToCue()
|
||||
Expect(cue).To(ContainSubstring(`output: kube.#Apply & {`))
|
||||
Expect(cue).To(ContainSubstring(`wait: builtin.#ConditionalWait & {`))
|
||||
Expect(cue).NotTo(ContainSubstring(`apply: kube.#Apply & {`))
|
||||
Expect(cue).NotTo(ContainSubstring(`conditionalwait: builtin.#ConditionalWait & {`))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Registry", func() {
|
||||
|
||||
Reference in New Issue
Block a user