mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
Fix: Update duration format handling and enhance StringParam with enum string support (#7053)
* Fix: update duration format handling and enhance StringParam with enum string support Signed-off-by: Anaswara Suresh M K <anaswarasuresh2212@gmail.com> Signed-off-by: vishal210893 <vishal210893@gmail.com> * date build-push-image workflow and improve image handlinggp Signed-off-by: Anaswara Suresh M K <anaswarasuresh2212@gmail.com> * chore: commit to re run pipeline Signed-off-by: Anaswara Suresh M K <anaswarasuresh2212@gmail.com> * Feat: add deduplication and field order tracking in collections and array elements Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com> * Fix: update test expectation for parameter data count in workflow step Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com> * Feat: add tests for label handling and template body in workflow steps Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com> * Feat: add tests for label handling and template body in workflow steps Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com> * Refactor: rename AllowString to OpenEnum for clarity in enum handling Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com> * Chore: format StringParam struct for improved readability Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com> --------- Signed-off-by: Anaswara Suresh M K <anaswarasuresh2212@gmail.com> Signed-off-by: vishal210893 <vishal210893@gmail.com> Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com>
This commit is contained in:
@@ -22,7 +22,6 @@ spec:
|
||||
query: parameter.query
|
||||
metricEndpoint: parameter.metricEndpoint
|
||||
condition: parameter.condition
|
||||
stepID: context.stepSessionID
|
||||
duration: parameter.duration
|
||||
failDuration: parameter.failDuration
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ spec:
|
||||
ding: {
|
||||
if parameter.dingding != _|_ {
|
||||
if parameter.dingding.url.value != _|_ {
|
||||
ding1: http.#Do & {
|
||||
ding1: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: parameter.dingding.url.value
|
||||
@@ -238,7 +238,7 @@ spec:
|
||||
}
|
||||
|
||||
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.dingding.url.secretRef.key])}
|
||||
ding2: http.#Do & {
|
||||
ding2: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: stringValue.$returns.str
|
||||
@@ -255,10 +255,10 @@ spec:
|
||||
lark: {
|
||||
if parameter.lark != _|_ {
|
||||
if parameter.lark.url.value != _|_ {
|
||||
lark1: http.#Do & {
|
||||
lark1: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: parameter.lark.message
|
||||
url: parameter.lark.url.value
|
||||
request: {
|
||||
body: json.Marshal(parameter.lark.message)
|
||||
header: "Content-Type": "application/json"
|
||||
@@ -279,7 +279,7 @@ spec:
|
||||
}
|
||||
|
||||
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.lark.url.secretRef.key])}
|
||||
lark2: http.#Do & {
|
||||
lark2: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: stringValue.$returns.str
|
||||
@@ -297,7 +297,7 @@ spec:
|
||||
slack: {
|
||||
if parameter.slack != _|_ {
|
||||
if parameter.slack.url.value != _|_ {
|
||||
slack1: http.#Do & {
|
||||
slack1: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: parameter.slack.url.value
|
||||
@@ -321,7 +321,7 @@ spec:
|
||||
}
|
||||
|
||||
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.slack.url.secretRef.key])}
|
||||
slack2: http.#Do & {
|
||||
slack2: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: stringValue.$returns.str
|
||||
|
||||
@@ -48,14 +48,16 @@ spec:
|
||||
|
||||
# Convert duration to seconds
|
||||
SECONDS=0
|
||||
if [[ "$DURATION" =~ ^([0-9]+)m$ ]]; then
|
||||
if [[ "$DURATION" =~ ^([0-9]+)s$ ]]; then
|
||||
SECONDS=${BASH_REMATCH[1]}
|
||||
elif [[ "$DURATION" =~ ^([0-9]+)m$ ]]; then
|
||||
SECONDS=$((${BASH_REMATCH[1]} * 60))
|
||||
elif [[ "$DURATION" =~ ^([0-9]+)h$ ]]; then
|
||||
SECONDS=$((${BASH_REMATCH[1]} * 3600))
|
||||
elif [[ "$DURATION" =~ ^([0-9]+)d$ ]]; then
|
||||
SECONDS=$((${BASH_REMATCH[1]} * 86400))
|
||||
else
|
||||
echo "ERROR: Invalid duration format: $DURATION (expected format: 5m, 1h, or 2d)"
|
||||
echo "ERROR: Invalid duration format: $DURATION (expected format: 30s, 5m, 1h, or 2d)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -108,8 +110,8 @@ spec:
|
||||
// +usage=Schedule restart at a specific RFC3339 timestamp (e.g., "2025-01-15T14:30:00Z")
|
||||
at?: string & =~"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$"
|
||||
// +usage=Schedule restart after a relative duration from now (e.g., "5m", "1h", "2d")
|
||||
after?: string & =~"^[0-9]+(m|h|d)$"
|
||||
after?: string & =~"^[0-9]+(s|m|h|d)$"
|
||||
// +usage=Schedule recurring restarts every specified duration (e.g., "5m", "1h", "24h")
|
||||
every?: string & =~"^[0-9]+(m|h|d)$"
|
||||
every?: string & =~"^[0-9]+(s|m|h|d)$"
|
||||
}
|
||||
|
||||
|
||||
@@ -38,9 +38,10 @@ spec:
|
||||
}
|
||||
webhook: {
|
||||
if parameter.url.value != _|_ {
|
||||
req: http.#HTTPPost & {
|
||||
req: http.#HTTPDo & {
|
||||
$params: {
|
||||
url: parameter.url.value
|
||||
method: "POST"
|
||||
url: parameter.url.value
|
||||
request: {
|
||||
body: data.value
|
||||
header: "Content-Type": "application/json"
|
||||
@@ -61,9 +62,10 @@ spec:
|
||||
}
|
||||
|
||||
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.url.secretRef.key])}
|
||||
req: http.#HTTPPost & {
|
||||
req: http.#HTTPDo & {
|
||||
$params: {
|
||||
url: stringValue.$returns.str
|
||||
method: "POST"
|
||||
url: stringValue.$returns.str
|
||||
request: {
|
||||
body: data.value
|
||||
header: "Content-Type": "application/json"
|
||||
|
||||
@@ -358,5 +358,5 @@ func TestStringStatusFromCueString(t *testing.T) {
|
||||
|
||||
statusField, ok := ast.GetFieldByPath(f, fmt.Sprintf("%s.attributes.status.details", def.GetName()))
|
||||
require.True(t, ok, "status field not found in CUE definition")
|
||||
require.IsType(t, &ast2.StructLit{}, statusField.Value, "expected status field to be of type StructLit")
|
||||
require.IsType(t, &ast2.StructLit{}, statusField.Value, "expected status field to be of type StructLit ")
|
||||
}
|
||||
|
||||
@@ -138,6 +138,12 @@ func (c *CollectionOp) Flatten() *CollectionOp {
|
||||
return c
|
||||
}
|
||||
|
||||
// Dedupe removes duplicate items by a key field, keeping the first occurrence.
|
||||
func (c *CollectionOp) Dedupe(keyField string) *CollectionOp {
|
||||
c.ops = append(c.ops, &dedupeOp{keyField: keyField})
|
||||
return c
|
||||
}
|
||||
|
||||
// Source returns the source value.
|
||||
func (c *CollectionOp) Source() Value { return c.source }
|
||||
|
||||
|
||||
@@ -111,6 +111,12 @@ var _ = Describe("Collections", func() {
|
||||
col := defkit.Each(ports).DefaultField("name", defkit.Format("port-%v", defkit.FieldRef("port")))
|
||||
Expect(col.Operations()).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should chain Dedupe operation on CollectionOp", func() {
|
||||
ports := defkit.List("ports")
|
||||
col := defkit.Each(ports).Dedupe("name")
|
||||
Expect(col.Operations()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
Context("FieldRef", func() {
|
||||
|
||||
@@ -2722,6 +2722,9 @@ func (g *CUEGenerator) writeStringParam(sb *strings.Builder, p *StringParam, ind
|
||||
enumParts = append(enumParts, fmt.Sprintf("%q", v))
|
||||
}
|
||||
}
|
||||
if p.IsOpenEnum() {
|
||||
enumParts = append(enumParts, "string")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, strings.Join(enumParts, " | ")))
|
||||
} else {
|
||||
// Build constraint parts
|
||||
|
||||
@@ -140,6 +140,17 @@ var _ = Describe("CUEGenerator", func() {
|
||||
Expect(cue).To(ContainSubstring(`optionalDefault?: *"Honor" | "Ignore"`))
|
||||
})
|
||||
|
||||
It("should append string to enum when OpenEnum is set", func() {
|
||||
comp := defkit.NewComponent("test").
|
||||
Params(
|
||||
defkit.String("verbosity").Default("info").Enum("info", "debug", "warn").OpenEnum(),
|
||||
)
|
||||
|
||||
cue := gen.GenerateParameterSchema(comp)
|
||||
|
||||
Expect(cue).To(ContainSubstring(`*"info" | "debug" | "warn" | string`))
|
||||
})
|
||||
|
||||
It("should generate // +ignore directive for ignored parameters", func() {
|
||||
comp := defkit.NewComponent("test").
|
||||
Params(
|
||||
|
||||
@@ -501,6 +501,7 @@ type patchKeyField struct {
|
||||
// Used for building array values with struct elements.
|
||||
type ArrayElement struct {
|
||||
fields map[string]Value
|
||||
fieldOrder []string
|
||||
ops []ResourceOp // nested operations for complex structs
|
||||
patchKeyFields []patchKeyField // nested patchKey-annotated array fields
|
||||
}
|
||||
@@ -511,13 +512,17 @@ func (a *ArrayElement) value() {}
|
||||
// NewArrayElement creates a new array element builder.
|
||||
func NewArrayElement() *ArrayElement {
|
||||
return &ArrayElement{
|
||||
fields: make(map[string]Value),
|
||||
ops: make([]ResourceOp, 0),
|
||||
fields: make(map[string]Value),
|
||||
fieldOrder: make([]string, 0),
|
||||
ops: make([]ResourceOp, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets a field on the array element.
|
||||
func (a *ArrayElement) Set(key string, value Value) *ArrayElement {
|
||||
if _, exists := a.fields[key]; !exists {
|
||||
a.fieldOrder = append(a.fieldOrder, key)
|
||||
}
|
||||
a.fields[key] = value
|
||||
return a
|
||||
}
|
||||
@@ -544,6 +549,9 @@ func (a *ArrayElement) PatchKeyField(field string, key string, value Value) *Arr
|
||||
// Fields returns all fields set on this element.
|
||||
func (a *ArrayElement) Fields() map[string]Value { return a.fields }
|
||||
|
||||
// FieldOrder returns field names in insertion order.
|
||||
func (a *ArrayElement) FieldOrder() []string { return a.fieldOrder }
|
||||
|
||||
// Ops returns any conditional operations.
|
||||
func (a *ArrayElement) Ops() []ResourceOp { return a.ops }
|
||||
|
||||
|
||||
@@ -286,6 +286,23 @@ var _ = Describe("Expressions", func() {
|
||||
fields := elem.Fields()
|
||||
Expect(fields).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("should track field insertion order", func() {
|
||||
elem := defkit.NewArrayElement().
|
||||
Set("charlie", defkit.Lit(3)).
|
||||
Set("alpha", defkit.Lit(1)).
|
||||
Set("bravo", defkit.Lit(2))
|
||||
Expect(elem.FieldOrder()).To(Equal([]string{"charlie", "alpha", "bravo"}))
|
||||
})
|
||||
|
||||
It("should not duplicate field order on overwrite", func() {
|
||||
elem := defkit.NewArrayElement().
|
||||
Set("name", defkit.Lit("old")).
|
||||
Set("port", defkit.Lit(80)).
|
||||
Set("name", defkit.Lit("new"))
|
||||
Expect(elem.FieldOrder()).To(Equal([]string{"name", "port"}))
|
||||
Expect(elem.Fields()["name"]).To(Equal(defkit.Lit("new")))
|
||||
})
|
||||
})
|
||||
|
||||
Context("ForEachMap", func() {
|
||||
|
||||
@@ -111,6 +111,7 @@ func (c *ParamCompareCondition) CompareValue() any { return c.value }
|
||||
type StringParam struct {
|
||||
baseParam
|
||||
enumValues []string // allowed enum values
|
||||
openEnum bool // when true, appends | string to enum disjunction (open enum)
|
||||
pattern string // regex pattern constraint
|
||||
minLen *int // minimum length constraint
|
||||
maxLen *int // maximum length constraint
|
||||
@@ -183,6 +184,18 @@ func (p *StringParam) GetEnumValues() []string {
|
||||
return p.enumValues
|
||||
}
|
||||
|
||||
// OpenEnum marks the enum as open, allowing any string in addition to the
|
||||
// enumerated values. This generates CUE like: "value1" | "value2" | string
|
||||
func (p *StringParam) OpenEnum() *StringParam {
|
||||
p.openEnum = true
|
||||
return p
|
||||
}
|
||||
|
||||
// IsOpenEnum returns whether the enum allows arbitrary strings beyond the listed values.
|
||||
func (p *StringParam) IsOpenEnum() bool {
|
||||
return p.openEnum
|
||||
}
|
||||
|
||||
// Pattern sets a regex pattern constraint for the parameter.
|
||||
// This generates CUE like: string & =~"pattern"
|
||||
func (p *StringParam) Pattern(regex string) *StringParam {
|
||||
@@ -437,6 +450,14 @@ func (p *BoolParam) Optional() *BoolParam {
|
||||
return p
|
||||
}
|
||||
|
||||
// ForceOptional makes the field optional even when it has a default value.
|
||||
// Normally, fields with defaults are treated as always-present (no ? in CUE).
|
||||
// This generates field?: *default | type instead of field: *default | type.
|
||||
func (p *BoolParam) ForceOptional() *BoolParam {
|
||||
p.forceOptional = true
|
||||
return p
|
||||
}
|
||||
|
||||
// Default sets a default value for the parameter.
|
||||
func (p *BoolParam) Default(value bool) *BoolParam {
|
||||
p.defaultValue = value
|
||||
|
||||
@@ -74,6 +74,17 @@ var _ = Describe("Parameters", func() {
|
||||
p := defkit.String("policy").Default("Honor")
|
||||
Expect(p.IsForceOptional()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should support OpenEnum on enum", func() {
|
||||
p := defkit.String("verbosity").Enum("info", "debug").OpenEnum()
|
||||
Expect(p.IsOpenEnum()).To(BeTrue())
|
||||
Expect(p.GetEnumValues()).To(ConsistOf("info", "debug"))
|
||||
})
|
||||
|
||||
It("should not allow string by default on enum", func() {
|
||||
p := defkit.String("verbosity").Enum("info", "debug")
|
||||
Expect(p.IsOpenEnum()).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
Context("IntParam", func() {
|
||||
@@ -122,6 +133,17 @@ var _ = Describe("Parameters", func() {
|
||||
Expect(p.IsOptional()).To(BeTrue())
|
||||
Expect(p.GetDefault()).To(Equal(false))
|
||||
})
|
||||
|
||||
It("should support ForceOptional", func() {
|
||||
p := defkit.Bool("auto").Default(true).ForceOptional()
|
||||
Expect(p.IsForceOptional()).To(BeTrue())
|
||||
Expect(p.HasDefault()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should not be force-optional by default", func() {
|
||||
p := defkit.Bool("auto").Default(true)
|
||||
Expect(p.IsForceOptional()).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
Context("FloatParam", func() {
|
||||
|
||||
@@ -18,6 +18,7 @@ package defkit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
@@ -29,12 +30,14 @@ import (
|
||||
// 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"
|
||||
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)
|
||||
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.
|
||||
@@ -51,9 +54,10 @@ type WorkflowAction interface {
|
||||
|
||||
// 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
|
||||
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() {}
|
||||
@@ -104,6 +108,16 @@ func (w *WorkflowStepDefinition) Scope(scope string) *WorkflowStepDefinition {
|
||||
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 {
|
||||
@@ -137,6 +151,23 @@ func (w *WorkflowStepDefinition) RawCUE(cue string) *WorkflowStepDefinition {
|
||||
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:
|
||||
@@ -350,6 +381,14 @@ func (b *BuiltinActionBuilder) WithParams(params map[string]Value) *BuiltinActio
|
||||
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)
|
||||
@@ -417,8 +456,18 @@ func (g *WorkflowStepCUEGenerator) GenerateFullDefinition(w *WorkflowStepDefinit
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
|
||||
|
||||
// Write labels (scope)
|
||||
// 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()))
|
||||
}
|
||||
@@ -459,6 +508,21 @@ func (g *WorkflowStepCUEGenerator) GenerateTemplate(w *WorkflowStepDefinition) s
|
||||
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))
|
||||
|
||||
@@ -501,7 +565,10 @@ func (g *WorkflowStepCUEGenerator) writeBuiltinAction(sb *strings.Builder, a *Bu
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s: %s & {\n", indent, extraIndent, actionName, a.name))
|
||||
if len(a.params) > 0 {
|
||||
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)))
|
||||
|
||||
@@ -17,6 +17,8 @@ limitations under the License.
|
||||
package defkit_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
@@ -258,6 +260,131 @@ template: {
|
||||
Expect(cue).NotTo(ContainSubstring(`apply: kube.#Apply & {`))
|
||||
Expect(cue).NotTo(ContainSubstring(`conditionalwait: builtin.#ConditionalWait & {`))
|
||||
})
|
||||
|
||||
It("should generate separate if blocks for each SetIf operation", func() {
|
||||
data := defkit.Object("data")
|
||||
noData := defkit.Eq(defkit.ParamRef("data"), defkit.Reference("_|_"))
|
||||
hasData := defkit.PathExists("parameter.data")
|
||||
|
||||
step := defkit.NewWorkflowStep("webhook").
|
||||
WithImports("vela/kube", "encoding/json").
|
||||
Params(data).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
dataValue := defkit.NewArrayElement().
|
||||
SetIf(noData, "read", defkit.Reference("kube.#Read & {}")).
|
||||
SetIf(noData, "value", defkit.Reference("json.Marshal(read.$returns.value)")).
|
||||
SetIf(hasData, "value", defkit.Reference("json.Marshal(parameter.data)"))
|
||||
tpl.Set("data", dataValue)
|
||||
})
|
||||
|
||||
cue := step.ToCue()
|
||||
Expect(strings.Count(cue, "if parameter.data == _|_ {")).To(Equal(2))
|
||||
Expect(cue).To(ContainSubstring("read: kube.#Read & {}"))
|
||||
Expect(cue).To(ContainSubstring("value: json.Marshal(read.$returns.value)"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Labels", func() {
|
||||
It("should set and get labels", func() {
|
||||
step := defkit.NewWorkflowStep("check-metrics").
|
||||
Labels(map[string]string{"catalog": "Delivery"})
|
||||
Expect(step.GetLabels()).To(HaveKeyWithValue("catalog", "Delivery"))
|
||||
})
|
||||
|
||||
It("should render labels in CUE output", func() {
|
||||
step := defkit.NewWorkflowStep("check-metrics").
|
||||
Description("Verify metrics").
|
||||
Category("Application Delivery").
|
||||
Labels(map[string]string{"catalog": "Delivery"})
|
||||
|
||||
cue := step.ToCue()
|
||||
Expect(cue).To(ContainSubstring(`"catalog": "Delivery"`))
|
||||
})
|
||||
|
||||
It("should render multiple labels sorted alphabetically", func() {
|
||||
step := defkit.NewWorkflowStep("test").
|
||||
Description("Test").
|
||||
Labels(map[string]string{"z-label": "last", "a-label": "first"})
|
||||
|
||||
cue := step.ToCue()
|
||||
aIdx := strings.Index(cue, `"a-label"`)
|
||||
zIdx := strings.Index(cue, `"z-label"`)
|
||||
Expect(aIdx).To(BeNumerically("<", zIdx))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TemplateBody", func() {
|
||||
It("should set and get raw template body", func() {
|
||||
step := defkit.NewWorkflowStep("test").
|
||||
TemplateBody(`check: metrics.#PromCheck & {}`)
|
||||
Expect(step.HasRawTemplateBody()).To(BeTrue())
|
||||
Expect(step.GetRawTemplateBody()).To(Equal(`check: metrics.#PromCheck & {}`))
|
||||
})
|
||||
|
||||
It("should return false when no template body set", func() {
|
||||
step := defkit.NewWorkflowStep("test")
|
||||
Expect(step.HasRawTemplateBody()).To(BeFalse())
|
||||
Expect(step.GetRawTemplateBody()).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should embed raw template body in generated CUE", func() {
|
||||
step := defkit.NewWorkflowStep("check-metrics").
|
||||
Description("Verify metrics").
|
||||
WithImports("vela/metrics").
|
||||
Params(defkit.String("query").Required()).
|
||||
TemplateBody("check: metrics.#PromCheck & {\n\t$params: query: parameter.query\n}")
|
||||
|
||||
cue := step.ToCue()
|
||||
Expect(cue).To(ContainSubstring("check: metrics.#PromCheck & {"))
|
||||
Expect(cue).To(ContainSubstring("$params: query: parameter.query"))
|
||||
Expect(cue).To(ContainSubstring("parameter:"))
|
||||
Expect(cue).To(ContainSubstring("query: string"))
|
||||
})
|
||||
|
||||
It("should handle empty lines in template body", func() {
|
||||
step := defkit.NewWorkflowStep("test").
|
||||
Description("Test").
|
||||
TemplateBody("line1: true\n\nline2: false")
|
||||
|
||||
cue := step.ToCue()
|
||||
Expect(cue).To(ContainSubstring("line1: true"))
|
||||
Expect(cue).To(ContainSubstring("line2: false"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("WithFullParameter", func() {
|
||||
It("should generate $params: parameter for builtin", func() {
|
||||
step := defkit.NewWorkflowStep("suspend").
|
||||
Description("Suspend workflow").
|
||||
Params(defkit.String("message").Optional()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Builtin("suspend", "builtin.#Suspend").
|
||||
WithFullParameter().
|
||||
Build()
|
||||
})
|
||||
|
||||
cue := step.ToCue()
|
||||
Expect(cue).To(ContainSubstring("suspend: builtin.#Suspend & {"))
|
||||
Expect(cue).To(ContainSubstring("$params: parameter"))
|
||||
})
|
||||
|
||||
It("should use WithFullParameter over WithParams when both are set", func() {
|
||||
step := defkit.NewWorkflowStep("suspend").
|
||||
Description("Suspend workflow").
|
||||
Params(defkit.String("message").Optional()).
|
||||
Template(func(tpl *defkit.WorkflowStepTemplate) {
|
||||
tpl.Builtin("suspend", "builtin.#Suspend").
|
||||
WithFullParameter().
|
||||
WithParams(map[string]defkit.Value{
|
||||
"message": defkit.Reference("parameter.message"),
|
||||
}).
|
||||
Build()
|
||||
})
|
||||
|
||||
cue := step.ToCue()
|
||||
Expect(cue).To(ContainSubstring("$params: parameter"))
|
||||
Expect(cue).NotTo(ContainSubstring("$params: {"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Registry", func() {
|
||||
|
||||
@@ -19,7 +19,6 @@ template: {
|
||||
query: parameter.query
|
||||
metricEndpoint: parameter.metricEndpoint
|
||||
condition: parameter.condition
|
||||
stepID: context.stepSessionID
|
||||
duration: parameter.duration
|
||||
failDuration: parameter.failDuration
|
||||
}
|
||||
|
||||
@@ -189,9 +189,7 @@ template: {
|
||||
multiline?: bool
|
||||
min_length?: int
|
||||
max_length?: int
|
||||
dispatch_action_config?: {
|
||||
trigger_actions_on?: [...string]
|
||||
}
|
||||
dispatch_action_config?: trigger_actions_on?: [...string]
|
||||
initial_time?: string
|
||||
}]
|
||||
}
|
||||
@@ -214,7 +212,7 @@ template: {
|
||||
ding: {
|
||||
if parameter.dingding != _|_ {
|
||||
if parameter.dingding.url.value != _|_ {
|
||||
ding1: http.#Do & {
|
||||
ding1: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: parameter.dingding.url.value
|
||||
@@ -240,7 +238,7 @@ template: {
|
||||
}
|
||||
|
||||
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.dingding.url.secretRef.key])}
|
||||
ding2: http.#Do & {
|
||||
ding2: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: stringValue.$returns.str
|
||||
@@ -257,10 +255,10 @@ template: {
|
||||
lark: {
|
||||
if parameter.lark != _|_ {
|
||||
if parameter.lark.url.value != _|_ {
|
||||
lark1: http.#Do & {
|
||||
lark1: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: parameter.lark.message
|
||||
url: parameter.lark.url.value
|
||||
request: {
|
||||
body: json.Marshal(parameter.lark.message)
|
||||
header: "Content-Type": "application/json"
|
||||
@@ -283,7 +281,7 @@ template: {
|
||||
}
|
||||
|
||||
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.lark.url.secretRef.key])}
|
||||
lark2: http.#Do & {
|
||||
lark2: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: stringValue.$returns.str
|
||||
@@ -301,7 +299,7 @@ template: {
|
||||
slack: {
|
||||
if parameter.slack != _|_ {
|
||||
if parameter.slack.url.value != _|_ {
|
||||
slack1: http.#Do & {
|
||||
slack1: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: parameter.slack.url.value
|
||||
@@ -327,7 +325,7 @@ template: {
|
||||
}
|
||||
|
||||
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.slack.url.secretRef.key])}
|
||||
slack2: http.#Do & {
|
||||
slack2: http.#HTTPDo & {
|
||||
$params: {
|
||||
method: "POST"
|
||||
url: stringValue.$returns.str
|
||||
|
||||
@@ -47,14 +47,16 @@ template: {
|
||||
|
||||
# Convert duration to seconds
|
||||
SECONDS=0
|
||||
if [[ "$DURATION" =~ ^([0-9]+)m$ ]]; then
|
||||
if [[ "$DURATION" =~ ^([0-9]+)s$ ]]; then
|
||||
SECONDS=${BASH_REMATCH[1]}
|
||||
elif [[ "$DURATION" =~ ^([0-9]+)m$ ]]; then
|
||||
SECONDS=$((${BASH_REMATCH[1]} * 60))
|
||||
elif [[ "$DURATION" =~ ^([0-9]+)h$ ]]; then
|
||||
SECONDS=$((${BASH_REMATCH[1]} * 3600))
|
||||
elif [[ "$DURATION" =~ ^([0-9]+)d$ ]]; then
|
||||
SECONDS=$((${BASH_REMATCH[1]} * 86400))
|
||||
else
|
||||
echo "ERROR: Invalid duration format: $DURATION (expected format: 5m, 1h, or 2d)"
|
||||
echo "ERROR: Invalid duration format: $DURATION (expected format: 30s, 5m, 1h, or 2d)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -111,8 +113,8 @@ template: {
|
||||
// +usage=Schedule restart at a specific RFC3339 timestamp (e.g., "2025-01-15T14:30:00Z")
|
||||
at?: string & =~"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$"
|
||||
// +usage=Schedule restart after a relative duration from now (e.g., "5m", "1h", "2d")
|
||||
after?: string & =~"^[0-9]+(m|h|d)$"
|
||||
after?: string & =~"^[0-9]+(s|m|h|d)$"
|
||||
// +usage=Schedule recurring restarts every specified duration (e.g., "5m", "1h", "24h")
|
||||
every?: string & =~"^[0-9]+(m|h|d)$"
|
||||
every?: string & =~"^[0-9]+(s|m|h|d)$"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,10 @@ template: {
|
||||
}
|
||||
webhook: {
|
||||
if parameter.url.value != _|_ {
|
||||
req: http.#HTTPPost & {
|
||||
req: http.#HTTPDo & {
|
||||
$params: {
|
||||
url: parameter.url.value
|
||||
method: "POST"
|
||||
url: parameter.url.value
|
||||
request: {
|
||||
body: data.value
|
||||
header: "Content-Type": "application/json"
|
||||
@@ -62,9 +63,10 @@ template: {
|
||||
}
|
||||
|
||||
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.url.secretRef.key])}
|
||||
req: http.#HTTPPost & {
|
||||
req: http.#HTTPDo & {
|
||||
$params: {
|
||||
url: stringValue.$returns.str
|
||||
method: "POST"
|
||||
url: stringValue.$returns.str
|
||||
request: {
|
||||
body: data.value
|
||||
header: "Content-Type": "application/json"
|
||||
|
||||
Reference in New Issue
Block a user