Files
kubevela/pkg/definition/defkit/workflow_step.go
Vishal Kumar 732b49d236 Feat: defkit api completeness (#7064)
* docs: map existing codebase

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* docs: initialize project

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* Fix: add Short() and Ignore() methods to FloatParam for API completeness

FloatParam was the only param type missing Short(string) and Ignore()
fluent methods; the underlying baseParam fields already existed. Adds
matching test cases following the existing BoolParam/IntParam pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(defkit): add StatusDetails() to all 4 definition types via baseDefinition

Adds statusDetails string field to baseDefinition with setStatusDetails()
setter and GetStatusDetails() getter. Exposes StatusDetails(string) fluent
method on ComponentDefinition, TraitDefinition, WorkflowStepDefinition, and
PolicyDefinition. Updates writeStatus in cuegen.go and the inline status
render block in trait.go to render statusDetails as a #"""..."""# CUE block
alongside customStatus and healthPolicy when set.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(01-02): add ForceOptional() to IntParam, FloatParam, EnumParam

- IntParam.ForceOptional() *IntParam
- FloatParam.ForceOptional() *FloatParam
- EnumParam.ForceOptional() *EnumParam
- Test cases for all three types following BoolParam/StringParam patterns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(02-01): add Labels()/GetLabels() to PolicyDefinition and fix hardcoded labels CUE

- Add labels map[string]string field to PolicyDefinition struct
- Add Labels() fluent setter and GetLabels() getter
- Replace hardcoded labels: {} in PolicyCUEGenerator with nil-conditional sorted output
- Add sort import to policy.go

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* test(02-01): add Ginkgo tests for PolicyDefinition.Labels

- Test store/return labels
- Test sorted key output in CUE
- Test labels block omitted when Labels() never called
- Test empty labels block when Labels called with empty map

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* docs(02-01): complete Labels on PolicyDefinition plan

- SUMMARY.md: plan 02-01 execution results
- STATE.md: phase 2 in progress, B1 satisfied, key decisions recorded
- ROADMAP.md: phase 2 progress updated (1/3 plans)
- REQUIREMENTS.md: B1 marked complete

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(02-02): add annotations field to baseDefinition with getter/setter

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(02-03): wire status block rendering into WorkflowStep and Policy CUE generators

- Add status: { customStatus, healthPolicy, statusDetails } rendering to WorkflowStepCUEGenerator.GenerateTemplate
- Add same status block rendering to PolicyCUEGenerator.GenerateTemplate
- Block only emitted when at least one status field is non-empty, matching trait.go pattern

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(02-02): add Annotations() fluent method to all 4 definition types

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(02-02): conditional sorted annotations CUE block for component, trait, policy

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* test(02-03): add Ginkgo status block CUE render tests for WorkflowStep and Policy

- 5 new tests in WorkflowStepDefinition/Status Block CUE Render context
- 5 new tests in PolicyDefinition/Status Block CUE Render context
- Rule 3 fix: restore sort import in trait.go (used by annotations render, spuriously flagged)

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(02-02): merge user annotations before category in workflow step CUE block

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(02-02): add sorted annotations CUE block to WorkflowStepCUEGenerator

- Wire sorted user annotations rendering into WorkflowStep.GenerateFullDefinition
- Matches pattern already added to Component, Trait, and Policy generators

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(02-02): merge user annotations into metadata.annotations in ToYAML for all 4 types

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* docs(02-03): complete Wire Status Block Rendering plan

- Add 02-03-SUMMARY.md with full execution record
- Update STATE.md: phase 2 complete, new decision recorded
- Update ROADMAP.md progress for phase 2
- Mark B4 complete in REQUIREMENTS.md

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* test(02-02): add Annotations Ginkgo tests for all 4 definition types

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* docs(02-02): complete Annotations plan - SUMMARY, STATE, ROADMAP updated

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* docs(03): create phase 3 plans for missing CRD spec fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(03-01): add Version() fluent method and GetVersion() to all 4 definition types

- version string field added to baseDefinition struct
- setVersion/GetVersion methods on baseDefinition following existing pattern
- Version(string) fluent setter on TraitDefinition, ComponentDefinition, PolicyDefinition, WorkflowStepDefinition
- TDD RED+GREEN: tests for round-trip pass; CUE/YAML render tests added (fail, to be fixed in next task)

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(03-02): add ManageWorkload/ControlPlaneOnly/RevisionEnabled to TraitDefinition

- Add manageWorkload, controlPlaneOnly, revisionEnabled bool fields to TraitDefinition struct
- Add ManageWorkload(), ControlPlaneOnly(), RevisionEnabled() fluent setters
- Add IsManageWorkload(), IsControlPlaneOnly(), IsRevisionEnabled() getters
- Emit conditionally in ToYAML only when true; ToCue() unaffected
- Add 11 round-trip tests covering defaults, setters, and YAML emission

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* test(03-03): add Ginkgo tests for ChildResourceKind accumulator on ComponentDefinition

- 7 specs covering nil default, single entry, multi-entry accumulation,
  selector preservation, ToYAML emit/omit, and chaining

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(03-02): add ManageHealthCheck to PolicyDefinition

- Add manageHealthCheck bool field to PolicyDefinition struct
- Add ManageHealthCheck() fluent setter and IsManageHealthCheck() getter
- Emit conditionally in ToYAML only when true; ToCue() unaffected
- Add 5 round-trip tests covering default, setter, YAML emission, and chaining

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* test(03-03): add Ginkgo tests for PodSpecPath on ComponentDefinition

- 5 specs covering empty default, set/get round-trip, ToYAML emit/omit,
  and chaining; both fields satisfy C5 (childResourceKinds) and C6 (podSpecPath)

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(03-01): render version in CUE output and spec.version in ToYAML for all 4 definition types

- cuegen.go: conditional version emit in ComponentDefinition GenerateFullDefinition (after description)
- trait.go: conditional version emit in TraitCUEGenerator.GenerateFullDefinition; spec.version in ToYAML
- policy.go: conditional version emit in PolicyCUEGenerator.GenerateFullDefinition; spec.version in ToYAML
- workflow_step.go: conditional version emit in WorkflowStepCUEGenerator.GenerateFullDefinition; spec.version in ToYAML
- version omitted entirely when not set; TDD GREEN phase complete

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* docs(03-03): complete ChildResourceKind+PodSpecPath plan - SUMMARY, STATE, ROADMAP updated

- C5 (childResourceKinds accumulator) and C6 (podSpecPath) requirements satisfied
- 12 Ginkgo specs added covering both fields

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* docs(03-02): complete boolean CRD spec fields plan — manageWorkload/controlPlaneOnly/revisionEnabled/manageHealthCheck

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* docs(03-01): complete Version() plan — SUMMARY, STATE, ROADMAP updated

- 03-01-SUMMARY.md created documenting Version() on all 4 definition types
- STATE.md: 03-01 session log entry added; phase 3 marked complete; decision recorded
- ROADMAP.md: phase 3 updated to 3/3 plans executed; status Complete
- REQUIREMENTS.md: C1 marked complete

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* docs(04): add gap closure plan 04-03 for vela-go-definitions call sites

Closes 17 ArrayOf→Of and 1 FilterPred→Filter call sites broken by the phase 04 renames.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(phase-4): low-risk renames — SetField→Set, ArrayOf→Of, FilterPred→Filter

A1: PolicyTemplate.SetField() renamed to Set()
A3: StructField.ArrayOf() renamed to Of()
A5: HelperBuilder.FilterPred(Predicate) renamed to Filter(Predicate);
    HelperBuilder.Filter(Condition) renamed to FilterCond(Condition)

All callers within defkit updated. Verification: 894 Ginkgo specs pass,
go build exits 0, zero occurrences of old names in pkg/definition/defkit/.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* docs(05-high-impact-renames): create phase plan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat(phase-5): high-impact renames — Values→Enum, Fields→WithFields

A4: EnumParam.Values() renamed to Enum() — aligns with StringParam.Enum()
    and StructField.Enum(); 10 call sites updated in defkit tests

A2: StructParam.Fields() and OneOfVariant.Fields() renamed to WithFields()
    — aligns with ArrayParam and MapParam; ~50 call sites updated in defkit tests

Both repos build clean. go test ./pkg/definition/defkit/... passes.
Non-target .Fields() methods (InCondition, StructBuilder, ArrayElement etc.)
correctly preserved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* chore: exclude .planning/ and .claude/ from version control

Local development artifacts only — not for upstream.

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* refactor: adjust formatting for consistency in component and policy definitions

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat: add FilterCond method for filtering items by Condition expression

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat: enhance filter condition handling with AND-composition for multiple filters

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat: rename Enum method to Values for consistency in parameter definitions

Signed-off-by: Jerrin Francis <jfo@>

* feat: simplify labels handling in policy and trait definitions

Signed-off-by: Jerrin Francis <jerrinfrancis7@gmail.com>

---------

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Jerrin Francis <jfo@>
Signed-off-by: Jerrin Francis <jerrinfrancis7@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-10 11:28:23 +00:00

687 lines
23 KiB
Go

/*
Copyright 2025 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package defkit
import (
"fmt"
"sort"
"strings"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/pkg/definition/defkit/placement"
)
// WorkflowStepDefinition represents a KubeVela WorkflowStepDefinition.
// Workflow steps define operations in an application's deployment workflow,
// such as deploy, suspend, notification, approval, etc.
type WorkflowStepDefinition struct {
baseDefinition // embedded common fields and methods
category string // e.g., "Application Delivery", "Notification"
scope string // e.g., "Application", "Workflow"
labels map[string]string // arbitrary metadata labels beyond scope
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)
rawTemplateBody string // raw CUE embedded inside template: {} before the parameter block
}
// WorkflowStepTemplate provides the building context for workflow step templates.
// Workflow steps typically use vela builtins and may have conditional logic.
type WorkflowStepTemplate struct {
actions []WorkflowAction
suspendMsg string // for suspend steps
}
// WorkflowAction represents an action in a workflow step.
type WorkflowAction interface {
isWorkflowAction()
}
// BuiltinAction represents a call to a vela builtin.
type BuiltinAction struct {
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
useFullParam bool // if true, generates $params: parameter instead of $params: { key: value }
}
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
action WorkflowAction
}
func (c *ConditionalAction) isWorkflowAction() {}
// NewWorkflowStep creates a new WorkflowStepDefinition builder.
func NewWorkflowStep(name string) *WorkflowStepDefinition {
return &WorkflowStepDefinition{
baseDefinition: baseDefinition{
name: name,
params: make([]Param, 0),
},
}
}
// Description sets the workflow step description.
func (w *WorkflowStepDefinition) Description(desc string) *WorkflowStepDefinition {
w.setDescription(desc)
return w
}
// Category sets the workflow step category (shown in annotations).
// Common values: "Application Delivery", "Notification", "Approval"
func (w *WorkflowStepDefinition) Category(category string) *WorkflowStepDefinition {
w.category = category
return w
}
// Scope sets the workflow step scope (shown in labels).
// Common values: "Application", "Workflow"
func (w *WorkflowStepDefinition) Scope(scope string) *WorkflowStepDefinition {
w.scope = scope
return w
}
// Labels sets arbitrary metadata labels for the workflow step definition.
// These labels appear in the definition's labels block alongside scope.
func (w *WorkflowStepDefinition) Labels(labels map[string]string) *WorkflowStepDefinition {
w.labels = labels
return w
}
// GetLabels returns the workflow step's metadata labels.
func (w *WorkflowStepDefinition) GetLabels() map[string]string { return w.labels }
// 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...)
return w
}
// Param adds a single parameter definition to the workflow step.
// This provides a more fluent API when adding parameters one at a time.
func (w *WorkflowStepDefinition) Param(param Param) *WorkflowStepDefinition {
w.addParams(param)
return w
}
// Template sets the template function for the workflow step.
func (w *WorkflowStepDefinition) Template(fn func(tpl *WorkflowStepTemplate)) *WorkflowStepDefinition {
w.stepTemplate = fn
return w
}
// RawCUE sets raw CUE for complex workflow step definitions that don't fit the builder pattern.
func (w *WorkflowStepDefinition) RawCUE(cue string) *WorkflowStepDefinition {
w.setRawCUE(cue)
return w
}
// TemplateBody sets raw CUE that is embedded inside the template: { ... } block,
// between any builder-generated actions and the parameter block.
// This allows complex workflow logic (array comprehensions, context variables, etc.)
// while still using the builder API for metadata (description, category, scope, imports)
// and parameter schema definitions.
// The body should be provided at zero indentation; one tab indent is added per line when embedded.
func (w *WorkflowStepDefinition) TemplateBody(body string) *WorkflowStepDefinition {
w.rawTemplateBody = body
return w
}
// GetRawTemplateBody returns the raw CUE template body.
func (w *WorkflowStepDefinition) GetRawTemplateBody() string { return w.rawTemplateBody }
// HasRawTemplateBody returns true if a raw template body is set.
func (w *WorkflowStepDefinition) HasRawTemplateBody() bool { return w.rawTemplateBody != "" }
// Helper adds a helper type definition using fluent API.
// The param defines the schema for the helper type.
// Example:
//
// Helper("Placement", defkit.Struct("placement").Fields(...))
func (w *WorkflowStepDefinition) Helper(name string, param Param) *WorkflowStepDefinition {
w.addHelper(name, param)
return w
}
// Note: GetHelperDefinitions() is inherited from baseDefinition
// WithImports adds CUE imports to the workflow step definition.
// Common imports: "vela/multicluster", "vela/builtin"
func (w *WorkflowStepDefinition) WithImports(imports ...string) *WorkflowStepDefinition {
w.addImports(imports...)
return w
}
// CustomStatus sets the custom status CUE expression for the workflow step.
// This provides status visibility in the workflow execution.
func (w *WorkflowStepDefinition) CustomStatus(expr string) *WorkflowStepDefinition {
w.setCustomStatus(expr)
return w
}
// HealthPolicy sets the health policy CUE expression for the workflow step.
// This defines how the step's health is determined.
func (w *WorkflowStepDefinition) HealthPolicy(expr string) *WorkflowStepDefinition {
w.setHealthPolicy(expr)
return w
}
// HealthPolicyExpr sets the health policy using a composable HealthExpression.
func (w *WorkflowStepDefinition) HealthPolicyExpr(expr HealthExpression) *WorkflowStepDefinition {
w.setHealthPolicyExpr(expr)
return w
}
// StatusDetails sets the status details CUE expression for the workflow step.
func (w *WorkflowStepDefinition) StatusDetails(details string) *WorkflowStepDefinition {
w.setStatusDetails(details)
return w
}
// Annotations sets metadata annotations on the workflow step definition.
func (w *WorkflowStepDefinition) Annotations(annotations map[string]string) *WorkflowStepDefinition {
w.setAnnotations(annotations)
return w
}
// Version sets the version string for the workflow step definition.
func (w *WorkflowStepDefinition) Version(v string) *WorkflowStepDefinition {
w.setVersion(v)
return w
}
// RunOn adds placement conditions specifying which clusters this workflow step should run on.
// Use the placement package's fluent API to build conditions.
//
// Example:
//
// defkit.NewWorkflowStep("eks-deploy").
// RunOn(placement.Label("provider").Eq("aws"))
//
// Multiple RunOn calls are combined with AND semantics (all conditions must match).
func (w *WorkflowStepDefinition) RunOn(conditions ...placement.Condition) *WorkflowStepDefinition {
w.addRunOn(conditions...)
return w
}
// NotRunOn adds placement conditions specifying which clusters this workflow step should NOT run on.
// Use the placement package's fluent API to build conditions.
//
// Example:
//
// defkit.NewWorkflowStep("no-vclusters").
// NotRunOn(placement.Label("cluster-type").Eq("vcluster"))
//
// If any NotRunOn condition matches, the workflow step is ineligible for that cluster.
func (w *WorkflowStepDefinition) NotRunOn(conditions ...placement.Condition) *WorkflowStepDefinition {
w.addNotRunOn(conditions...)
return w
}
// DefName implements Definition.DefName.
func (w *WorkflowStepDefinition) DefName() string { return w.GetName() }
// DefType implements Definition.DefType.
func (w *WorkflowStepDefinition) DefType() DefinitionType { return DefinitionTypeWorkflowStep }
// Note: GetName(), GetDescription(), GetParams(), GetHelperDefinitions(),
// GetRawCUE(), GetImports(), GetCustomStatus(), GetHealthPolicy()
// are all inherited from baseDefinition
// GetCategory returns the workflow step category.
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
if w.HasRawCUE() {
return w.GetRawCUEWithName()
}
gen := NewWorkflowStepCUEGenerator()
if len(w.GetImports()) > 0 {
gen.WithImports(w.GetImports()...)
}
return gen.GenerateFullDefinition(w)
}
// ToYAML generates the Kubernetes YAML representation of the WorkflowStepDefinition.
func (w *WorkflowStepDefinition) ToYAML() ([]byte, error) {
cueStr := w.ToCue()
// Build the WorkflowStepDefinition CR structure
cr := map[string]any{
"apiVersion": "core.oam.dev/v1beta1",
"kind": "WorkflowStepDefinition",
"metadata": map[string]any{
"name": w.GetName(),
"annotations": func() map[string]any {
a := map[string]any{}
for k, v := range w.GetAnnotations() {
a[k] = v
}
a["definition.oam.dev/description"] = w.GetDescription()
return a
}(),
},
"spec": map[string]any{
"schematic": map[string]any{
"cue": map[string]any{
"template": cueStr,
},
},
},
}
if w.GetVersion() != "" {
cr["spec"].(map[string]any)["version"] = w.GetVersion()
}
return yaml.Marshal(cr)
}
// --- WorkflowStepTemplate methods ---
// NewWorkflowStepTemplate creates a new workflow step template.
func NewWorkflowStepTemplate() *WorkflowStepTemplate {
return &WorkflowStepTemplate{
actions: make([]WorkflowAction, 0),
}
}
// Builtin adds a call to a vela builtin.
// Example: tpl.Builtin("deploy", "multicluster.#Deploy").WithParams(...)
func (wt *WorkflowStepTemplate) Builtin(name, builtinRef string) *BuiltinActionBuilder {
action := &BuiltinAction{
varName: name,
name: builtinRef,
params: make(map[string]Value),
}
return &BuiltinActionBuilder{
template: wt,
action: action,
varName: name,
}
}
// 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 {
wt.suspendMsg = message
return wt
}
// SuspendIf adds a conditional suspend action.
// Example: tpl.SuspendIf(param.Eq(false), "Waiting for approval")
func (wt *WorkflowStepTemplate) SuspendIf(cond Condition, message string) *WorkflowStepTemplate {
wt.actions = append(wt.actions, &ConditionalAction{
cond: cond,
action: &BuiltinAction{
name: "builtin.#Suspend",
params: map[string]Value{
"message": Lit(message),
},
},
})
return wt
}
// GetActions returns all actions.
func (wt *WorkflowStepTemplate) GetActions() []WorkflowAction { return wt.actions }
// GetSuspendMessage returns the suspend message.
func (wt *WorkflowStepTemplate) GetSuspendMessage() string { return wt.suspendMsg }
// BuiltinActionBuilder builds a builtin action.
type BuiltinActionBuilder struct {
template *WorkflowStepTemplate
action *BuiltinAction
varName string
}
// WithParams sets parameters for the builtin.
func (b *BuiltinActionBuilder) WithParams(params map[string]Value) *BuiltinActionBuilder {
for k, v := range params {
b.action.params[k] = v
}
return b
}
// WithFullParameter passes the entire parameter object as $params.
// This generates: $params: parameter
// Useful for builtins (e.g., builtin.#Suspend) that accept all step parameters directly.
func (b *BuiltinActionBuilder) WithFullParameter() *BuiltinActionBuilder {
b.action.useFullParam = 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)
return b.template
}
// If makes this action conditional.
func (b *BuiltinActionBuilder) If(cond Condition) *BuiltinActionBuilder {
// Replace action with conditional version
condAction := &ConditionalAction{
cond: cond,
action: b.action,
}
b.template.actions = append(b.template.actions, condAction)
return b
}
// --- WorkflowStepCUEGenerator ---
// WorkflowStepCUEGenerator generates CUE definitions for workflow steps.
type WorkflowStepCUEGenerator struct {
indent string
imports []string
}
// NewWorkflowStepCUEGenerator creates a new workflow step CUE generator.
func NewWorkflowStepCUEGenerator() *WorkflowStepCUEGenerator {
return &WorkflowStepCUEGenerator{
indent: "\t",
imports: []string{},
}
}
// WithImports adds CUE imports.
func (g *WorkflowStepCUEGenerator) WithImports(imports ...string) *WorkflowStepCUEGenerator {
g.imports = append(g.imports, imports...)
return g
}
// GenerateFullDefinition generates the complete CUE definition for a workflow step.
func (g *WorkflowStepCUEGenerator) GenerateFullDefinition(w *WorkflowStepDefinition) string {
var sb strings.Builder
// Write imports if any
if len(g.imports) > 0 {
sb.WriteString("import (\n")
for _, imp := range g.imports {
sb.WriteString(fmt.Sprintf("\t%q\n", imp))
}
sb.WriteString(")\n\n")
}
// Write workflow step header - quote names with special characters
name := w.GetName()
if strings.ContainsAny(name, "-./") {
name = fmt.Sprintf("%q", name)
}
sb.WriteString(fmt.Sprintf("%s: {\n", name))
sb.WriteString(fmt.Sprintf("%stype: \"workflow-step\"\n", g.indent))
// Write annotations (user annotations + category)
sb.WriteString(fmt.Sprintf("%sannotations: {\n", g.indent))
if annots := w.GetAnnotations(); len(annots) > 0 {
keys := make([]string, 0, len(annots))
for k := range annots {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if k == "category" && w.GetCategory() != "" {
continue
}
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, annots[k]))
}
}
if w.GetCategory() != "" {
sb.WriteString(fmt.Sprintf("%s\t\"category\": %q\n", g.indent, w.GetCategory()))
}
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
// Write labels (scope + custom labels)
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
if labels := w.GetLabels(); len(labels) > 0 {
keys := make([]string, 0, len(labels))
for k := range labels {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, labels[k]))
}
}
if w.GetScope() != "" {
sb.WriteString(fmt.Sprintf("%s\t\"scope\": %q\n", g.indent, w.GetScope()))
}
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()))
if w.GetVersion() != "" {
sb.WriteString(fmt.Sprintf("%sversion: %q\n", g.indent, w.GetVersion()))
}
sb.WriteString("}\n")
// Write template section
sb.WriteString(g.GenerateTemplate(w))
return sb.String()
}
// GenerateTemplate generates the template block for a workflow step.
func (g *WorkflowStepCUEGenerator) GenerateTemplate(w *WorkflowStepDefinition) string {
var sb strings.Builder
sb.WriteString("template: {\n")
gen := NewCUEGenerator()
// Generate helper type definitions
for _, helperDef := range w.GetHelperDefinitions() {
gen.WriteHelperDefinition(&sb, helperDef, 1)
}
// Execute template function if provided
if w.stepTemplate != nil {
wt := NewWorkflowStepTemplate()
w.stepTemplate(wt)
// Write actions
g.writeActions(&sb, wt, 1)
}
// Embed raw template body if set (for complex logic not expressible via builder API).
// Each line of the body is prefixed with one tab to sit inside the template: {} block.
if w.HasRawTemplateBody() {
body := strings.TrimRight(w.rawTemplateBody, "\n")
lines := strings.Split(body, "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "" {
sb.WriteString("\n")
} else {
sb.WriteString(fmt.Sprintf("%s%s\n", g.indent, line))
}
}
sb.WriteString("\n")
}
// Generate parameter section
sb.WriteString(g.generateParameterBlock(w, 1))
if w.GetCustomStatus() != "" || w.GetHealthPolicy() != "" || w.GetStatusDetails() != "" {
indent := g.indent
innerIndent := g.indent + g.indent
sb.WriteString(fmt.Sprintf("%sstatus: {\n", indent))
if w.GetCustomStatus() != "" {
sb.WriteString(fmt.Sprintf("%scustomStatus: #\"\"\"\n", innerIndent))
for _, line := range strings.Split(w.GetCustomStatus(), "\n") {
sb.WriteString(fmt.Sprintf("%s\t%s\n", innerIndent, line))
}
sb.WriteString(fmt.Sprintf("%s\t\"\"\"#\n", innerIndent))
}
if w.GetHealthPolicy() != "" {
sb.WriteString(fmt.Sprintf("%shealthPolicy: #\"\"\"\n", innerIndent))
for _, line := range strings.Split(w.GetHealthPolicy(), "\n") {
sb.WriteString(fmt.Sprintf("%s\t%s\n", innerIndent, line))
}
sb.WriteString(fmt.Sprintf("%s\t\"\"\"#\n", innerIndent))
}
if w.GetStatusDetails() != "" {
sb.WriteString(fmt.Sprintf("%sstatusDetails: #\"\"\"\n", innerIndent))
for _, line := range strings.Split(w.GetStatusDetails(), "\n") {
sb.WriteString(fmt.Sprintf("%s\t%s\n", innerIndent, line))
}
sb.WriteString(fmt.Sprintf("%s\t\"\"\"#\n", innerIndent))
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
sb.WriteString("}\n")
return sb.String()
}
// writeActions writes the workflow actions.
func (g *WorkflowStepCUEGenerator) writeActions(sb *strings.Builder, wt *WorkflowStepTemplate, depth int) {
indent := strings.Repeat(g.indent, depth)
gen := NewCUEGenerator()
for _, action := range wt.GetActions() {
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))
}
}
}
// writeBuiltinAction writes a builtin action.
func (g *WorkflowStepCUEGenerator) writeBuiltinAction(sb *strings.Builder, a *BuiltinAction, extraIndent, indent string, gen *CUEGenerator) {
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 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 {
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)))
}
sb.WriteString(fmt.Sprintf("%s%s\t}\n", indent, extraIndent))
}
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"
// "builtin.#Suspend" -> "suspend"
parts := strings.Split(builtinRef, "#")
if len(parts) == 2 {
return strings.ToLower(parts[1])
}
return strings.ToLower(builtinRef)
}
// generateParameterBlock generates the parameter schema for the workflow step.
func (g *WorkflowStepCUEGenerator) generateParameterBlock(w *WorkflowStepDefinition, depth int) string {
var sb strings.Builder
indent := strings.Repeat(g.indent, depth)
sb.WriteString(fmt.Sprintf("%sparameter: {\n", indent))
gen := NewCUEGenerator()
for _, param := range w.GetParams() {
gen.writeParam(&sb, param, depth+1)
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
return sb.String()
}