Feat: $( ) expressions in workflow step properties, resolved when each step runs
Merged
BR
7d ago+3258+3258+0123456789012345678901234567890123456789-34-34-01234567890123456789
Description of your changes
Lets a step's properties hold $( ) expressions, evaluated on the kubevela/pkg CEL engine when the step starts rather than before the run, so a step can read what earlier steps produced.
steps:
- name: list
type: read-config
properties: {name: tenants}
outputs:
- {name: tenants, valueFrom: output.config.tenants}
- name: per-tenant
type: step-group
forEach:
items: '$(steps.list.outputs.tenants)'
subSteps:
- name: save
type: create-config
properties:
name: 'greeting-$(each.item.name)'
config: {message: 'hi $(each.item.name) #$(each.index) in $(context.name)'}
- Roots:
steps.<name>.outputs.<output>reads an output that step declares;each.itemandeach.indexread the current forEach iteration, by the namesinputs[].fromuses;contextreads the step's context, as a CUE template does. Inside an iteration a body step's outputs are that iteration's; outside the loop they are the lists it collected. - When: at step start, every reconcile until the step finishes. A read whose step is still running waits (
waiting for step list); a read of a step that failed fails. A skipped step's outputs read as absent, as an input reads one as null: a guarded read (has(steps.a.outputs.o) ? ... : default) takes its default, an unguarded one fails, and the implieddependsOndoes not skip the reader as a written one would.forEach.itemsmay be an expression, resolved once when the loop starts. - Ordering: a read of
steps.Xis an implieddependsOn: X, by the rules KubeVela uses for component reads: not to the reader itself, a later step in a workflow run in order, or one that closes a cycle. - Status:
status.dependencieslists what each step waits on and whetherdependsOnor an expression said so, in the shape the Application uses for components:{step, dependsOn, source}. - Hosts:
StepGeneratorOptions.Expressionsadds a host's roots with a resolver for each (KubeVela addssource). A resolver is told the run and step it answers for (expressions.RunFrom), so it can hold a value for the whole run in the workflow context. A step's own properties are resolved before aStepConvertorruns, so text a convertor adds, such as a component's properties, is left for whatever it belongs to. - Opt-in: the
EnableCelExpressionsfeature gate (chartworkflow.enableCelExpressions, off by default) and the annotationworkflowrun.oam.dev/cel-expressions: "true". Without both, properties are left as written. - Admission: for a run that opts in, every expression is checked without evaluating it: it compiles; it reads only
stepsoutputs that exist and that only one step declares;eachsits in a forEach body; and no read could wait forever (the reader itself, a later step in order, a cycle).
Part of kubevela/kubevela#7414.
I have:
- Read and followed KubeVela's contribution process.
- Related Docs updated properly: to follow.
- Run
make reviewableto ensure this PR is ready for review. Ran gofmt, golangci-lint and staticcheck, and regenerated the CRD and chart README; not the full target. - Added
backport release-x.ylabels to auto-backport this PR if necessary.
How has this code been tested
- Unit tests for the resolvers (waits, failures, guarded reads, forEach scoping, self reads), implied
dependsOn, admission, and the run scope a host resolver gets. - Step-run tests: resolve, wait, fail, a skip winning over a wait, the
$$(escape, and resolution before a convertor. - Generator and executor specs through the real engine: a sub-step reading an earlier step, a forEach whose items and body read steps and
each, and a DAG ordered only by its reads. - A controller spec reconciling a WorkflowRun that opts in, and one that does not.
- An e2e spec (
e2e/expressions_test.go) on k3d; the e2e install turns the gate on. - End to end through KubeVela on k3d, including source values held for a run across a controller restart (kubevela/kubevela PR to follow).
TestSendEmail (and its legacy copy) and the "request step resolving headers from a Kubernetes Secret" e2e spec fail locally for reasons unrelated to this change: the first needs network access the sandbox denies, the second calls a mock server by cluster DNS, which a controller run on the host cannot resolve.
Special notes for your reviewer
- The WorkflowRun CRD gains
status.dependencies. KubeVela carries it into the Application'sstatus.workflowin the companion PR. - Pinning a value for the run is left to the host: KubeVela's
sourcevalues are typed against their schema in a way the workflow cannot store faithfully, so it keeps them itself throughRunFrom.
Summary by cubic
Lets workflow step properties and forEach.items hold $( ) CEL expressions, resolved when the step starts instead of before the run, so a step can read what earlier steps produced.
Behavior
steps.<name>.outputs.<output>reads a declared output;each.item,each.index, andcontextare also available, and$(written as$\u0028is still treated as an expression.- Reading
steps.XimpliesdependsOn: X; reads wait for running steps and fail if the step failed. A skipped step's outputs read as absent, so a guarded read takes its default, and the implied dependsOn holds the reader pending without skipping it as a written one would. A forEach body's reads see that iteration's values. - Hosts add their own roots with per-run resolvers via
StepGeneratorOptions.Expressions; a step's properties resolve before aStepConvertorruns, which receives a copy so in-place edits can't alter the step as written. - A run is known by its start time (stored to the second), so a host resolver is told the same run across reconciles and never the zero time; the instance is initialized before its runners are generated.
- Restarting a step also restarts steps whose expressions read it, and
status.dependencieslists what each step waits on and why.
Migration
- Opt in per run with the
EnableCelExpressionsfeature gate (workflow.enableCelExpressions, off by default) and theworkflowrun.oam.dev/cel-expressions: "true"annotation; without both, properties are left as written. - Admission rejects expressions that don't compile, read missing or ambiguous outputs, use
eachoutside a forEach body, or could wait forever, including an in-order read of an earlier step that waits on the reader. AforEach.itemsmust be a list or one$( )expression that resolves to one; text around an expression is refused, while whitespace around it is still treated as one. A referenced workflow is checked with its own steps, mode, and roots only, since nothing else resolves its WorkflowRun.