diff --git a/cmd/entrypoint/main.go b/cmd/entrypoint/main.go index adf7a0a9d4a..5352bf3dfac 100644 --- a/cmd/entrypoint/main.go +++ b/cmd/entrypoint/main.go @@ -33,6 +33,7 @@ import ( "github.com/tektoncd/pipeline/cmd/entrypoint/subcommands" featureFlags "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/credentials" "github.com/tektoncd/pipeline/pkg/credentials/dockercreds" "github.com/tektoncd/pipeline/pkg/credentials/gitcreds" @@ -50,6 +51,7 @@ var ( terminationPath = flag.String("termination_path", "/tekton/termination", "If specified, file to write upon termination") results = flag.String("results", "", "If specified, list of file names that might contain task results") stepResults = flag.String("step_results", "", "step results if specified") + whenExpressions = flag.String("when_expressions", "", "when expressions if specified") timeout = flag.Duration("timeout", time.Duration(0), "If specified, sets timeout for step") stdoutPath = flag.String("stdout_path", "", "If specified, file to copy stdout to") stderrPath = flag.String("stderr_path", "", "If specified, file to copy stderr to") @@ -138,6 +140,12 @@ func main() { log.Fatal(err) } } + var when v1.StepWhenExpressions + if len(*whenExpressions) > 0 { + if err := json.Unmarshal([]byte(*whenExpressions), &when); err != nil { + log.Fatal(err) + } + } var spireWorkloadAPI spire.EntrypointerAPIClient if enableSpire != nil && *enableSpire && socketPath != nil && *socketPath != "" { @@ -162,6 +170,7 @@ func main() { Results: strings.Split(*results, ","), StepResults: strings.Split(*stepResults, ","), Timeout: timeout, + StepWhenExpressions: when, BreakpointOnFailure: *breakpointOnFailure, OnError: *onError, StepMetadataDir: *stepMetadataDir, diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md index 1539352104e..bbe53542e1d 100644 --- a/docs/pipeline-api.md +++ b/docs/pipeline-api.md @@ -4647,6 +4647,20 @@ It cannot be used when referencing StepActions using [v1.Step.Ref]. The Results declared by the StepActions will be stored here instead.
+when
When is a list of when expressions that need to be true for the task to run
+[]github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.WhenExpression
alias)-(Appears on:PipelineTask) +(Appears on:PipelineTask, Step)
WhenExpressions are used to specify whether a Task should be executed or skipped @@ -14069,6 +14083,18 @@ It cannot be used when referencing StepActions using [v1beta1.Step.Ref]. The Results declared by the StepActions will be stored here instead.
+when
[]github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WhenExpression
alias)-(Appears on:PipelineTask) +(Appears on:PipelineTask, Step)
WhenExpressions are used to specify whether a Task should be executed or skipped
diff --git a/docs/stepactions.md b/docs/stepactions.md
index c7d5a334035..d78932fc283 100644
--- a/docs/stepactions.md
+++ b/docs/stepactions.md
@@ -18,6 +18,7 @@ weight: 201
- [Declaring VolumeMounts](#declaring-volumemounts)
- [Referencing a StepAction](#referencing-a-stepaction)
- [Specifying Remote StepActions](#specifying-remote-stepactions)
+ - [Controlling Step Execution with when Expressions](#controlling-step-execution-with-when-expressions)
- [Known Limitations](#known-limitations)
- [Cannot pass Step Results between Steps](#cannot-pass-step-results-between-steps)
@@ -521,3 +522,104 @@ spec:
```
The default resolver type can be configured by the `default-resolver-type` field in the `config-defaults` ConfigMap (`alpha` feature). See [additional-configs.md](./additional-configs.md) for details.
+
+### Controlling Step Execution with when Expressions
+
+You can define `when` in a `step` to control its execution.
+
+The components of `when` expressions are `input`, `operator`, `values`, `cel`:
+
+| Component | Description | Syntax |
+|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `input` | Input for the `when` expression, defaults to an empty string if not provided. | * Static values e.g. `"ubuntu"`
* Variables (parameters or results) e.g. `"$(params.image)"` or `"$(tasks.task1.results.image)"` or `"$(tasks.task1.results.array-results[1])"` |
+| `operator` | `operator` represents an `input`'s relationship to a set of `values`, a valid `operator` must be provided. | `in` or `notin` |
+| `values` | An array of string values, the `values` array must be provided and has to be non-empty. | * An array param e.g. `["$(params.images[*])"]`
* An array result of a task `["$(tasks.task1.results.array-results[*])"]`
* An array result of a step`["(steps.step1.results.array-results[*])"]`
* `values` can contain static values e.g. `"ubuntu"`
* `values` can contain variables (parameters or results) or a Workspaces's `bound` state e.g. `["$(params.image)"]` or `["$(steps.step1.results.image)"]` or `["$(tasks.task1.results.array-results[1])"]` or `["$(steps.step1.results.array-results[1])"]` |
+| `cel` | The Common Expression Language (CEL) implements common semantics for expression evaluation, enabling different applications to more easily interoperate. This is an `alpha` feature, `enable-cel-in-whenexpression` needs to be set to true to use this feature. | [cel-syntax](https://github.com/google/cel-spec/blob/master/doc/langdef.md#syntax)
+
+The below example shows how to use when expressions to control step executions:
+
+```yaml
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: my-pvc-2
+spec:
+ resources:
+ requests:
+ storage: 5Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+---
+apiVersion: tekton.dev/v1
+kind: TaskRun
+metadata:
+ generateName: step-when-example
+spec:
+ workspaces:
+ - name: custom
+ persistentVolumeClaim:
+ claimName: my-pvc-2
+ taskSpec:
+ description: |
+ A simple task that shows how to use when determine if a step should be executed
+ steps:
+ - name: should-execute
+ image: bash:latest
+ script: |
+ #!/usr/bin/env bash
+ echo "executed..."
+ when:
+ - input: "$(workspaces.custom.bound)"
+ operator: in
+ values: [ "true" ]
+ - name: should-skip
+ image: bash:latest
+ script: |
+ #!/usr/bin/env bash
+ echo skipskipskip
+ when:
+ - input: "$(workspaces.custom2.bound)"
+ operator: in
+ values: [ "true" ]
+ - name: should-continue
+ image: bash:latest
+ script: |
+ #!/usr/bin/env bash
+ echo blabalbaba
+ - name: produce-step
+ image: alpine
+ results:
+ - name: result2
+ type: string
+ script: |
+ echo -n "foo" | tee $(step.results.result2.path)
+ - name: run-based-on-step-results
+ image: alpine
+ script: |
+ echo "wooooooo"
+ when:
+ - input: "$(steps.produce-step.results.result2)"
+ operator: in
+ values: [ "bar" ]
+ workspaces:
+ - name: custom
+```
+
+The StepState for a skipped step looks like something similar to the below:
+```yaml
+ {
+ "container": "step-run-based-on-step-results",
+ "imageID": "docker.io/library/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b",
+ "name": "run-based-on-step-results",
+ "terminated": {
+ "containerID": "containerd://bf81162e79cf66a2bbc03e3654942d3464db06ff368c0be263a8a70f363a899b",
+ "exitCode": 0,
+ "finishedAt": "2024-03-26T03:57:47Z",
+ "reason": "Completed",
+ "startedAt": "2024-03-26T03:57:47Z"
+ },
+ "terminationReason": "Skipped"
+ }
+```
+Where `terminated.exitCode` is `0` and `terminationReason` is `Skipped` to indicate the Step exited successfully and was skipped.
\ No newline at end of file
diff --git a/examples/v1/taskruns/alpha/stepaction-when.yaml b/examples/v1/taskruns/alpha/stepaction-when.yaml
new file mode 100644
index 00000000000..17ce0508adb
--- /dev/null
+++ b/examples/v1/taskruns/alpha/stepaction-when.yaml
@@ -0,0 +1,72 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: my-pvc-2
+spec:
+ resources:
+ requests:
+ storage: 5Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+---
+apiVersion: tekton.dev/v1alpha1
+kind: StepAction
+metadata:
+ name: step-action-when
+spec:
+ image: alpine
+ script: |
+ echo "I am a Step Action!!!"
+---
+apiVersion: tekton.dev/v1
+kind: TaskRun
+metadata:
+ generateName: when-in-steps-
+spec:
+ workspaces:
+ - name: custom
+ persistentVolumeClaim:
+ claimName: my-pvc-2
+ taskSpec:
+ description: |
+ A simple task to demotrate how when expressions work in steps.
+ steps:
+ - name: should-execute
+ ref:
+ name: "step-action-when"
+ when:
+ - input: "$(workspaces.custom.bound)"
+ operator: in
+ values: ["true"]
+ - name: should-skip
+ image: bash:latest
+ script: |
+ #!/usr/bin/env bash
+ echo skipskipskip
+ when:
+ - input: "$(workspaces.custom2.bound)"
+ operator: in
+ values: ["true"]
+ - name: should-continue
+ image: bash:latest
+ script: |
+ #!/usr/bin/env bash
+ echo blabalbaba
+ - name: produce-step
+ image: alpine
+ results:
+ - name: result2
+ type: string
+ script: |
+ echo -n "foo" | tee $(step.results.result2.path)
+ - name: run-based-on-step-results
+ image: alpine
+ script: |
+ echo "wooooooo"
+ when:
+ - input: "$(steps.produce-step.results.result2)"
+ operator: in
+ values: ["bar"]
+ workspaces:
+ - name: custom
\ No newline at end of file
diff --git a/pkg/apis/pipeline/v1/container_types.go b/pkg/apis/pipeline/v1/container_types.go
index 9f0c48ae9af..51e9f18717d 100644
--- a/pkg/apis/pipeline/v1/container_types.go
+++ b/pkg/apis/pipeline/v1/container_types.go
@@ -152,6 +152,10 @@ type Step struct {
// +optional
// +listType=atomic
Results []StepResult `json:"results,omitempty"`
+
+ // When is a list of when expressions that need to be true for the task to run
+ // +optional
+ When StepWhenExpressions `json:"when,omitempty"`
}
// Ref can be used to refer to a specific instance of a StepAction.
diff --git a/pkg/apis/pipeline/v1/merge.go b/pkg/apis/pipeline/v1/merge.go
index 296eaf4b145..df413a5456a 100644
--- a/pkg/apis/pipeline/v1/merge.go
+++ b/pkg/apis/pipeline/v1/merge.go
@@ -65,7 +65,7 @@ func MergeStepsWithStepTemplate(template *StepTemplate, steps []Step) ([]Step, e
amendConflictingContainerFields(&merged, s)
// Pass through original step Script, for later conversion.
- newStep := Step{Script: s.Script, OnError: s.OnError, Timeout: s.Timeout, StdoutConfig: s.StdoutConfig, StderrConfig: s.StderrConfig, Results: s.Results, Params: s.Params, Ref: s.Ref}
+ newStep := Step{Script: s.Script, OnError: s.OnError, Timeout: s.Timeout, StdoutConfig: s.StdoutConfig, StderrConfig: s.StderrConfig, Results: s.Results, Params: s.Params, Ref: s.Ref, When: s.When}
newStep.SetContainerFields(merged)
steps[i] = newStep
}
diff --git a/pkg/apis/pipeline/v1/merge_test.go b/pkg/apis/pipeline/v1/merge_test.go
index 8877945aa57..ab3d598742a 100644
--- a/pkg/apis/pipeline/v1/merge_test.go
+++ b/pkg/apis/pipeline/v1/merge_test.go
@@ -26,6 +26,7 @@ import (
"github.com/tektoncd/pipeline/test/diff"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
+ "k8s.io/apimachinery/pkg/selection"
)
func TestMergeStepsWithStepTemplate(t *testing.T) {
@@ -257,6 +258,17 @@ func TestMergeStepsWithStepTemplate(t *testing.T) {
},
}},
}},
+ }, {
+ name: "when",
+ template: nil,
+ steps: []v1.Step{{
+ Image: "some-image",
+ When: v1.StepWhenExpressions{{Input: "foo", Operator: selection.In, Values: []string{"foo", "bar"}}},
+ }},
+ expected: []v1.Step{{
+ Image: "some-image",
+ When: v1.StepWhenExpressions{{Input: "foo", Operator: selection.In, Values: []string{"foo", "bar"}}},
+ }},
}} {
t.Run(tc.name, func(t *testing.T) {
result, err := v1.MergeStepsWithStepTemplate(tc.template, tc.steps)
diff --git a/pkg/apis/pipeline/v1/openapi_generated.go b/pkg/apis/pipeline/v1/openapi_generated.go
index 82896087af0..e9664a7d32e 100644
--- a/pkg/apis/pipeline/v1/openapi_generated.go
+++ b/pkg/apis/pipeline/v1/openapi_generated.go
@@ -3122,12 +3122,26 @@ func schema_pkg_apis_pipeline_v1_Step(ref common.ReferenceCallback) common.OpenA
},
},
},
+ "when": {
+ SchemaProps: spec.SchemaProps{
+ Description: "When is a list of when expressions that need to be true for the task to run",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.WhenExpression"),
+ },
+ },
+ },
+ },
+ },
},
Required: []string{"name"},
},
},
Dependencies: []string{
- "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Param", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Ref", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepOutputConfig", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepResult", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.WorkspaceUsage", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"},
+ "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Param", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Ref", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepOutputConfig", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepResult", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.WhenExpression", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.WorkspaceUsage", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"},
}
}
diff --git a/pkg/apis/pipeline/v1/swagger.json b/pkg/apis/pipeline/v1/swagger.json
index be288cc97b9..f4abdedd2ba 100644
--- a/pkg/apis/pipeline/v1/swagger.json
+++ b/pkg/apis/pipeline/v1/swagger.json
@@ -1581,6 +1581,14 @@
"x-kubernetes-patch-merge-key": "mountPath",
"x-kubernetes-patch-strategy": "merge"
},
+ "when": {
+ "description": "When is a list of when expressions that need to be true for the task to run",
+ "type": "array",
+ "items": {
+ "default": {},
+ "$ref": "#/definitions/v1.WhenExpression"
+ }
+ },
"workingDir": {
"description": "Step's working directory. If not specified, the container runtime's default will be used, which might be configured in the container image. Cannot be updated.",
"type": "string"
diff --git a/pkg/apis/pipeline/v1/task_validation.go b/pkg/apis/pipeline/v1/task_validation.go
index 95708574409..fa364d13b54 100644
--- a/pkg/apis/pipeline/v1/task_validation.go
+++ b/pkg/apis/pipeline/v1/task_validation.go
@@ -260,6 +260,9 @@ func validateSteps(ctx context.Context, steps []Step) (errs *apis.FieldError) {
errs = errs.Also(ValidateStepResultsVariables(ctx, s.Results, s.Script).ViaIndex(idx))
errs = errs.Also(ValidateStepResults(ctx, s.Results).ViaIndex(idx).ViaField("results"))
}
+ if len(s.When) > 0 {
+ errs = errs.Also(s.When.validate(ctx).ViaIndex(idx))
+ }
}
return errs
}
@@ -378,17 +381,8 @@ func validateStepResultReference(s Step) (errs *apis.FieldError) {
}
func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.FieldError) {
- if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableArtifacts {
- var t []string
- t = append(t, s.Script)
- t = append(t, s.Command...)
- t = append(t, s.Args...)
- for _, e := range s.Env {
- t = append(t, e.Value)
- }
- if slices.ContainsFunc(t, stepArtifactReferenceExists) {
- return errs.Also(apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), ""))
- }
+ if err := validateStepArtifactsReferences(ctx, s); err != nil {
+ return err
}
if s.Ref != nil {
@@ -456,6 +450,11 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi
return apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true in order to use Results in Steps.", config.EnableStepActions), "")
}
}
+ if len(s.When) > 0 {
+ if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableStepActions && isCreateOrUpdateAndDiverged(ctx, s) {
+ return apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true in order to use When in Steps.", config.EnableStepActions), "")
+ }
+ }
if s.Image == "" {
errs = errs.Also(apis.ErrMissingField("Image"))
}
@@ -538,6 +537,22 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi
return errs
}
+func validateStepArtifactsReferences(ctx context.Context, s Step) *apis.FieldError {
+ if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableArtifacts {
+ var t []string
+ t = append(t, s.Script)
+ t = append(t, s.Command...)
+ t = append(t, s.Args...)
+ for _, e := range s.Env {
+ t = append(t, e.Value)
+ }
+ if slices.ContainsFunc(t, stepArtifactReferenceExists) {
+ return apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), "")
+ }
+ }
+ return nil
+}
+
// ValidateParameterTypes validates all the types within a slice of ParamSpecs
func ValidateParameterTypes(ctx context.Context, params []ParamSpec) (errs *apis.FieldError) {
for _, p := range params {
diff --git a/pkg/apis/pipeline/v1/task_validation_test.go b/pkg/apis/pipeline/v1/task_validation_test.go
index 5540b7e744b..be4ffc40ec1 100644
--- a/pkg/apis/pipeline/v1/task_validation_test.go
+++ b/pkg/apis/pipeline/v1/task_validation_test.go
@@ -29,6 +29,7 @@ import (
"github.com/tektoncd/pipeline/test/diff"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/utils/pointer"
"knative.dev/pkg/apis"
@@ -3127,3 +3128,85 @@ func TestTaskSpecValidate_StepResults_Error(t *testing.T) {
})
}
}
+
+func TestTaskSpecValidate_StepWhen_Error(t *testing.T) {
+ tests := []struct {
+ name string
+ ts *v1.TaskSpec
+ isCreate bool
+ Results []v1.StepResult
+ isUpdate bool
+ baselineTaskRun *v1.TaskRun
+ expectedError apis.FieldError
+ EnableStepAction bool
+ EnableCEL bool
+ }{
+ {
+ name: "step when not allowed without enable step actions - create event",
+ ts: &v1.TaskSpec{Steps: []v1.Step{{
+ Image: "my-image",
+ When: v1.StepWhenExpressions{{Input: "foo", Operator: selection.In, Values: []string{"foo"}}},
+ }}},
+ isCreate: true,
+ expectedError: apis.FieldError{
+ Message: "feature flag enable-step-actions should be set to true in order to use When in Steps.",
+ Paths: []string{"steps[0]"},
+ },
+ },
+ {
+ name: "step when not allowed without enable step actions - update and diverged event",
+ ts: &v1.TaskSpec{Steps: []v1.Step{{
+ Image: "my-image",
+ When: v1.StepWhenExpressions{{Input: "foo", Operator: selection.In, Values: []string{"foo"}}},
+ }}},
+ isUpdate: true,
+ baselineTaskRun: &v1.TaskRun{
+ Spec: v1.TaskRunSpec{
+ TaskSpec: &v1.TaskSpec{
+ Steps: []v1.Step{{
+ Image: "my-image",
+ Results: []v1.StepResult{{Name: "a-result"}},
+ }},
+ },
+ },
+ },
+ expectedError: apis.FieldError{
+ Message: "feature flag enable-step-actions should be set to true in order to use When in Steps.",
+ Paths: []string{"steps[0]"},
+ },
+ },
+ {
+ name: "cel not allowed if EnableCELInWhenExpression is false",
+ ts: &v1.TaskSpec{Steps: []v1.Step{{
+ Image: "my-image",
+ When: v1.StepWhenExpressions{{CEL: "'d'=='d'"}},
+ }}},
+ EnableStepAction: true,
+ expectedError: apis.FieldError{
+ Message: `feature flag enable-cel-in-whenexpression should be set to true to use CEL: 'd'=='d' in WhenExpression`,
+ Paths: []string{"steps[0].when[0]"},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := config.ToContext(context.Background(), &config.Config{
+ FeatureFlags: &config.FeatureFlags{
+ EnableStepActions: tt.EnableStepAction,
+ EnableCELInWhenExpression: tt.EnableCEL,
+ },
+ })
+ if tt.isCreate {
+ ctx = apis.WithinCreate(ctx)
+ }
+ if tt.isUpdate {
+ ctx = apis.WithinUpdate(ctx, tt.baselineTaskRun)
+ }
+ tt.ts.SetDefaults(ctx)
+ err := tt.ts.Validate(ctx)
+ if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
+ t.Errorf("StepActionSpec.Validate() errors diff %s", diff.PrintWantGot(d))
+ }
+ })
+ }
+}
diff --git a/pkg/apis/pipeline/v1/when_types.go b/pkg/apis/pipeline/v1/when_types.go
index 66e7164a779..0a128d8f9e5 100644
--- a/pkg/apis/pipeline/v1/when_types.go
+++ b/pkg/apis/pipeline/v1/when_types.go
@@ -98,6 +98,8 @@ func (we *WhenExpression) GetVarSubstitutionExpressions() ([]string, bool) {
// All of them need to evaluate to True for a guarded Task to be executed.
type WhenExpressions []WhenExpression
+type StepWhenExpressions = WhenExpressions
+
// AllowsExecution evaluates an Input's relationship to an array of Values, based on the Operator,
// to determine whether all the When Expressions are True. If they are all True, the guarded Task is
// executed, otherwise it is skipped.
diff --git a/pkg/apis/pipeline/v1/when_validation.go b/pkg/apis/pipeline/v1/when_validation.go
index 2a058299a7d..a62621a69ff 100644
--- a/pkg/apis/pipeline/v1/when_validation.go
+++ b/pkg/apis/pipeline/v1/when_validation.go
@@ -48,7 +48,7 @@ func (wes WhenExpressions) validateWhenExpressionsFields(ctx context.Context) (e
func (we *WhenExpression) validateWhenExpressionFields(ctx context.Context) *apis.FieldError {
if we.CEL != "" {
if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableCELInWhenExpression {
- return apis.ErrGeneric("feature flag %s should be set to true to use CEL: %s in WhenExpression", config.EnableCELInWhenExpression, we.CEL)
+ return apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true to use CEL: %s in WhenExpression", config.EnableCELInWhenExpression, we.CEL), "")
}
if we.Input != "" || we.Operator != "" || len(we.Values) != 0 {
return apis.ErrGeneric(fmt.Sprintf("cel and input+operator+values cannot be set in one WhenExpression: %v", we))
diff --git a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go
index 16e483fcec1..506579fe024 100644
--- a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go
+++ b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go
@@ -1385,6 +1385,13 @@ func (in *Step) DeepCopyInto(out *Step) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.When != nil {
+ in, out := &in.When, &out.When
+ *out = make(WhenExpressions, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
return
}
diff --git a/pkg/apis/pipeline/v1beta1/container_conversion.go b/pkg/apis/pipeline/v1beta1/container_conversion.go
index 2e828bc5add..5b61377bcc8 100644
--- a/pkg/apis/pipeline/v1beta1/container_conversion.go
+++ b/pkg/apis/pipeline/v1beta1/container_conversion.go
@@ -72,6 +72,11 @@ func (s Step) convertTo(ctx context.Context, sink *v1.Step) {
sink.Params = append(sink.Params, new)
}
sink.Results = s.Results
+ for _, w := range s.When {
+ new := v1.WhenExpression{}
+ w.convertTo(ctx, &new)
+ sink.When = append(sink.When, new)
+ }
}
func (s *Step) convertFrom(ctx context.Context, source v1.Step) {
@@ -111,6 +116,11 @@ func (s *Step) convertFrom(ctx context.Context, source v1.Step) {
s.Params = append(s.Params, new)
}
s.Results = source.Results
+ for _, w := range source.When {
+ new := WhenExpression{}
+ new.convertFrom(ctx, w)
+ s.When = append(s.When, new)
+ }
}
func (s StepTemplate) convertTo(ctx context.Context, sink *v1.StepTemplate) {
diff --git a/pkg/apis/pipeline/v1beta1/container_types.go b/pkg/apis/pipeline/v1beta1/container_types.go
index 4494184d7aa..26e08e69fe9 100644
--- a/pkg/apis/pipeline/v1beta1/container_types.go
+++ b/pkg/apis/pipeline/v1beta1/container_types.go
@@ -247,6 +247,8 @@ type Step struct {
// +optional
// +listType=atomic
Results []v1.StepResult `json:"results,omitempty"`
+
+ When StepWhenExpressions `json:"when,omitempty"`
}
// Ref can be used to refer to a specific instance of a StepAction.
diff --git a/pkg/apis/pipeline/v1beta1/merge.go b/pkg/apis/pipeline/v1beta1/merge.go
index 6d1432d46db..62111ee7ccb 100644
--- a/pkg/apis/pipeline/v1beta1/merge.go
+++ b/pkg/apis/pipeline/v1beta1/merge.go
@@ -66,7 +66,7 @@ func MergeStepsWithStepTemplate(template *StepTemplate, steps []Step) ([]Step, e
amendConflictingContainerFields(&merged, s)
// Pass through original step Script, for later conversion.
- newStep := Step{Script: s.Script, OnError: s.OnError, Timeout: s.Timeout, StdoutConfig: s.StdoutConfig, StderrConfig: s.StderrConfig}
+ newStep := Step{Script: s.Script, OnError: s.OnError, Timeout: s.Timeout, StdoutConfig: s.StdoutConfig, StderrConfig: s.StderrConfig, When: s.When}
newStep.SetContainerFields(merged)
steps[i] = newStep
}
diff --git a/pkg/apis/pipeline/v1beta1/merge_test.go b/pkg/apis/pipeline/v1beta1/merge_test.go
index 778ccf6869e..fe177e431b2 100644
--- a/pkg/apis/pipeline/v1beta1/merge_test.go
+++ b/pkg/apis/pipeline/v1beta1/merge_test.go
@@ -25,6 +25,7 @@ import (
"github.com/tektoncd/pipeline/test/diff"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
+ "k8s.io/apimachinery/pkg/selection"
"k8s.io/utils/pointer"
)
@@ -231,6 +232,17 @@ func TestMergeStepsWithStepTemplate(t *testing.T) {
},
}},
}},
+ }, {
+ name: "when",
+ template: nil,
+ steps: []v1beta1.Step{{
+ Image: "some-image",
+ When: v1beta1.StepWhenExpressions{{Input: "foo", Operator: selection.In, Values: []string{"foo", "bar"}}},
+ }},
+ expected: []v1beta1.Step{{
+ Image: "some-image",
+ When: v1beta1.StepWhenExpressions{{Input: "foo", Operator: selection.In, Values: []string{"foo", "bar"}}},
+ }},
}} {
t.Run(tc.name, func(t *testing.T) {
result, err := v1beta1.MergeStepsWithStepTemplate(tc.template, tc.steps)
diff --git a/pkg/apis/pipeline/v1beta1/openapi_generated.go b/pkg/apis/pipeline/v1beta1/openapi_generated.go
index d2c52a3790a..dde2863832c 100644
--- a/pkg/apis/pipeline/v1beta1/openapi_generated.go
+++ b/pkg/apis/pipeline/v1beta1/openapi_generated.go
@@ -4077,12 +4077,25 @@ func schema_pkg_apis_pipeline_v1beta1_Step(ref common.ReferenceCallback) common.
},
},
},
+ "when": {
+ SchemaProps: spec.SchemaProps{
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WhenExpression"),
+ },
+ },
+ },
+ },
+ },
},
Required: []string{"name"},
},
},
Dependencies: []string{
- "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepResult", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Param", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Ref", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.StepOutputConfig", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspaceUsage", "k8s.io/api/core/v1.ContainerPort", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.Lifecycle", "k8s.io/api/core/v1.Probe", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"},
+ "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepResult", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Param", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Ref", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.StepOutputConfig", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WhenExpression", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspaceUsage", "k8s.io/api/core/v1.ContainerPort", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.Lifecycle", "k8s.io/api/core/v1.Probe", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"},
}
}
diff --git a/pkg/apis/pipeline/v1beta1/swagger.json b/pkg/apis/pipeline/v1beta1/swagger.json
index 690ef2115e3..e17ab8e0f33 100644
--- a/pkg/apis/pipeline/v1beta1/swagger.json
+++ b/pkg/apis/pipeline/v1beta1/swagger.json
@@ -2239,6 +2239,13 @@
"x-kubernetes-patch-merge-key": "mountPath",
"x-kubernetes-patch-strategy": "merge"
},
+ "when": {
+ "type": "array",
+ "items": {
+ "default": {},
+ "$ref": "#/definitions/v1beta1.WhenExpression"
+ }
+ },
"workingDir": {
"description": "Step's working directory. If not specified, the container runtime's default will be used, which might be configured in the container image. Cannot be updated.",
"type": "string"
diff --git a/pkg/apis/pipeline/v1beta1/task_conversion_test.go b/pkg/apis/pipeline/v1beta1/task_conversion_test.go
index ddf97617f8c..64aaca8ab10 100644
--- a/pkg/apis/pipeline/v1beta1/task_conversion_test.go
+++ b/pkg/apis/pipeline/v1beta1/task_conversion_test.go
@@ -95,6 +95,22 @@ spec:
properties:
key:
type: string
+`
+ stepWhenTaskYAML := `
+metadata:
+ name: foo
+ namespace: bar
+spec:
+ displayName: "task-step-when"
+ description: test
+ steps:
+ - image: foo
+ name: should-execute
+ image: bash:latest
+ when:
+ - input: "$(workspaces.custom.bound)"
+ operator: in
+ values: ["true"]
`
stepActionTaskYAML := `
metadata:
@@ -330,6 +346,9 @@ spec:
stepResultTaskV1beta1 := parse.MustParseV1beta1Task(t, stepResultTaskYAML)
stepResultTaskV1 := parse.MustParseV1Task(t, stepResultTaskYAML)
+ stepWhenTaskV1beta1 := parse.MustParseV1beta1Task(t, stepWhenTaskYAML)
+ stepWhenTaskV1 := parse.MustParseV1Task(t, stepWhenTaskYAML)
+
stepActionTaskV1beta1 := parse.MustParseV1beta1Task(t, stepActionTaskYAML)
stepActionTaskV1 := parse.MustParseV1Task(t, stepActionTaskYAML)
@@ -375,6 +394,10 @@ spec:
name: "step results in task",
v1beta1Task: stepResultTaskV1beta1,
v1Task: stepResultTaskV1,
+ }, {
+ name: "step when in task",
+ v1beta1Task: stepWhenTaskV1beta1,
+ v1Task: stepWhenTaskV1,
}, {
name: "step action in task",
v1beta1Task: stepActionTaskV1beta1,
diff --git a/pkg/apis/pipeline/v1beta1/task_validation.go b/pkg/apis/pipeline/v1beta1/task_validation.go
index 9ef0db6909a..ebff081b2b4 100644
--- a/pkg/apis/pipeline/v1beta1/task_validation.go
+++ b/pkg/apis/pipeline/v1beta1/task_validation.go
@@ -249,6 +249,9 @@ func validateSteps(ctx context.Context, steps []Step) (errs *apis.FieldError) {
errs = errs.Also(v1.ValidateStepResultsVariables(ctx, s.Results, s.Script).ViaIndex(idx))
errs = errs.Also(v1.ValidateStepResults(ctx, s.Results).ViaIndex(idx).ViaField("results"))
}
+ if len(s.When) > 0 {
+ errs = errs.Also(s.When.validate(ctx).ViaIndex(idx))
+ }
}
return errs
}
@@ -368,17 +371,8 @@ func validateStepResultReference(s Step) (errs *apis.FieldError) {
}
func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.FieldError) {
- if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableArtifacts {
- var t []string
- t = append(t, s.Script)
- t = append(t, s.Command...)
- t = append(t, s.Args...)
- for _, e := range s.Env {
- t = append(t, e.Value)
- }
- if slices.ContainsFunc(t, stepArtifactReferenceExists) {
- return errs.Also(apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), ""))
- }
+ if err := validateStepArtifactsReferences(ctx, s); err != nil {
+ return err
}
if s.Ref != nil {
@@ -446,6 +440,11 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi
return apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true in order to use Results in Steps.", config.EnableStepActions), "")
}
}
+ if len(s.When) > 0 {
+ if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableStepActions && isCreateOrUpdateAndDiverged(ctx, s) {
+ return apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true in order to use When in Steps.", config.EnableStepActions), "")
+ }
+ }
if s.Image == "" {
errs = errs.Also(apis.ErrMissingField("Image"))
}
@@ -529,6 +528,22 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi
return errs
}
+func validateStepArtifactsReferences(ctx context.Context, s Step) *apis.FieldError {
+ if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableArtifacts {
+ var t []string
+ t = append(t, s.Script)
+ t = append(t, s.Command...)
+ t = append(t, s.Args...)
+ for _, e := range s.Env {
+ t = append(t, e.Value)
+ }
+ if slices.ContainsFunc(t, stepArtifactReferenceExists) {
+ return apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), "")
+ }
+ }
+ return nil
+}
+
// ValidateParameterTypes validates all the types within a slice of ParamSpecs
func ValidateParameterTypes(ctx context.Context, params []ParamSpec) (errs *apis.FieldError) {
for _, p := range params {
diff --git a/pkg/apis/pipeline/v1beta1/task_validation_test.go b/pkg/apis/pipeline/v1beta1/task_validation_test.go
index 87e32fa904e..181b444edc7 100644
--- a/pkg/apis/pipeline/v1beta1/task_validation_test.go
+++ b/pkg/apis/pipeline/v1beta1/task_validation_test.go
@@ -33,6 +33,7 @@ import (
"github.com/tektoncd/pipeline/test/diff"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/utils/pointer"
"knative.dev/pkg/apis"
@@ -796,8 +797,7 @@ func TestTaskValidateError(t *testing.T) {
Spec: v1beta1.TaskSpec{
Params: tt.fields.Params,
Steps: tt.fields.Steps,
- },
- }
+ }}
ctx := cfgtesting.EnableAlphaAPIFields(context.Background())
task.SetDefaults(ctx)
err := task.Validate(ctx)
@@ -1117,8 +1117,7 @@ func TestTaskSpecValidateError(t *testing.T) {
Name: "mystep",
Image: "my-image",
WorkingDir: "/foo/bar/src/",
- },
- },
+ }},
},
expectedError: apis.FieldError{
Message: `variable type invalid in "$(params.baz[*])"`,
@@ -1471,44 +1470,43 @@ func TestTaskSpecValidateErrorWithStepActionRef_CreateUpdateEvent(t *testing.T)
isCreate bool
isUpdate bool
expectedError apis.FieldError
- }{
- {
- name: "is create ctx",
- Steps: []v1beta1.Step{{
- Ref: &v1beta1.Ref{
- Name: "stepAction",
- },
- }},
- isCreate: true,
- isUpdate: false,
- expectedError: apis.FieldError{
- Message: "feature flag enable-step-actions should be set to true to reference StepActions in Steps.",
- Paths: []string{"steps[0]"},
+ }{{
+ name: "is create ctx",
+ Steps: []v1beta1.Step{{
+ Ref: &v1beta1.Ref{
+ Name: "stepAction",
},
- }, {
- name: "is update ctx",
- Steps: []v1beta1.Step{{
- Ref: &v1beta1.Ref{
- Name: "stepAction",
- },
- }},
- isCreate: false,
- isUpdate: true,
- expectedError: apis.FieldError{
- Message: "feature flag enable-step-actions should be set to true to reference StepActions in Steps.",
- Paths: []string{"steps[0]"},
+ }},
+ isCreate: true,
+ isUpdate: false,
+ expectedError: apis.FieldError{
+ Message: "feature flag enable-step-actions should be set to true to reference StepActions in Steps.",
+ Paths: []string{"steps[0]"},
+ },
+ }, {
+ name: "is update ctx",
+ Steps: []v1beta1.Step{{
+ Ref: &v1beta1.Ref{
+ Name: "stepAction",
},
- }, {
- name: "ctx is not create or update",
- Steps: []v1beta1.Step{{
- Ref: &v1beta1.Ref{
- Name: "stepAction",
- },
- }},
- isCreate: false,
- isUpdate: false,
- expectedError: apis.FieldError{},
+ }},
+ isCreate: false,
+ isUpdate: true,
+ expectedError: apis.FieldError{
+ Message: "feature flag enable-step-actions should be set to true to reference StepActions in Steps.",
+ Paths: []string{"steps[0]"},
},
+ }, {
+ name: "ctx is not create or update",
+ Steps: []v1beta1.Step{{
+ Ref: &v1beta1.Ref{
+ Name: "stepAction",
+ },
+ }},
+ isCreate: false,
+ isUpdate: false,
+ expectedError: apis.FieldError{},
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -1684,86 +1682,85 @@ func TestTaskSpecValidateErrorWithStepResultRef(t *testing.T) {
name string
Steps []v1beta1.Step
expectedError apis.FieldError
- }{
- {
- name: "Cannot reference step results in image",
- Steps: []v1beta1.Step{{
- Image: "$(steps.prevStep.results.resultName)",
- }},
- expectedError: apis.FieldError{
- Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
- Paths: []string{"steps[0].image"},
- },
- }, {
- name: "Cannot reference step results in script",
- Steps: []v1beta1.Step{{
- Image: "my-img",
- Script: "echo $(steps.prevStep.results.resultName)",
- }},
- expectedError: apis.FieldError{
- Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
- Paths: []string{"steps[0].script"},
- },
- }, {
- name: "Cannot reference step results in workingDir",
- Steps: []v1beta1.Step{{
- Image: "my-img",
- WorkingDir: "$(steps.prevStep.results.resultName)",
- }},
- expectedError: apis.FieldError{
- Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
- Paths: []string{"steps[0].workingDir"},
- },
- }, {
- name: "Cannot reference step results in envFrom",
- Steps: []v1beta1.Step{{
- Image: "my-img",
- EnvFrom: []corev1.EnvFromSource{{
- Prefix: "$(steps.prevStep.results.resultName)",
- ConfigMapRef: &corev1.ConfigMapEnvSource{
- LocalObjectReference: corev1.LocalObjectReference{
- Name: "$(steps.prevStep.results.resultName)",
- },
+ }{{
+ name: "Cannot reference step results in image",
+ Steps: []v1beta1.Step{{
+ Image: "$(steps.prevStep.results.resultName)",
+ }},
+ expectedError: apis.FieldError{
+ Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
+ Paths: []string{"steps[0].image"},
+ },
+ }, {
+ name: "Cannot reference step results in script",
+ Steps: []v1beta1.Step{{
+ Image: "my-img",
+ Script: "echo $(steps.prevStep.results.resultName)",
+ }},
+ expectedError: apis.FieldError{
+ Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
+ Paths: []string{"steps[0].script"},
+ },
+ }, {
+ name: "Cannot reference step results in workingDir",
+ Steps: []v1beta1.Step{{
+ Image: "my-img",
+ WorkingDir: "$(steps.prevStep.results.resultName)",
+ }},
+ expectedError: apis.FieldError{
+ Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
+ Paths: []string{"steps[0].workingDir"},
+ },
+ }, {
+ name: "Cannot reference step results in envFrom",
+ Steps: []v1beta1.Step{{
+ Image: "my-img",
+ EnvFrom: []corev1.EnvFromSource{{
+ Prefix: "$(steps.prevStep.results.resultName)",
+ ConfigMapRef: &corev1.ConfigMapEnvSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "$(steps.prevStep.results.resultName)",
},
- SecretRef: &corev1.SecretEnvSource{
- LocalObjectReference: corev1.LocalObjectReference{
- Name: "$(steps.prevStep.results.resultName)",
- },
+ },
+ SecretRef: &corev1.SecretEnvSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "$(steps.prevStep.results.resultName)",
},
- }},
+ },
}},
- expectedError: apis.FieldError{
- Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
- Paths: []string{"steps[0].envFrom.configMapRef", "steps[0].envFrom.prefix", "steps[0].envFrom.secretRef"},
- },
- }, {
- name: "Cannot reference step results in VolumeMounts",
- Steps: []v1beta1.Step{{
- Image: "my-img",
- VolumeMounts: []corev1.VolumeMount{{
- Name: "$(steps.prevStep.results.resultName)",
- MountPath: "$(steps.prevStep.results.resultName)",
- SubPath: "$(steps.prevStep.results.resultName)",
- }},
+ }},
+ expectedError: apis.FieldError{
+ Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
+ Paths: []string{"steps[0].envFrom.configMapRef", "steps[0].envFrom.prefix", "steps[0].envFrom.secretRef"},
+ },
+ }, {
+ name: "Cannot reference step results in VolumeMounts",
+ Steps: []v1beta1.Step{{
+ Image: "my-img",
+ VolumeMounts: []corev1.VolumeMount{{
+ Name: "$(steps.prevStep.results.resultName)",
+ MountPath: "$(steps.prevStep.results.resultName)",
+ SubPath: "$(steps.prevStep.results.resultName)",
}},
- expectedError: apis.FieldError{
- Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
- Paths: []string{"steps[0].volumeMounts.name", "steps[0].volumeMounts.mountPath", "steps[0].volumeMounts.subPath"},
- },
- }, {
- name: "Cannot reference step results in VolumeDevices",
- Steps: []v1beta1.Step{{
- Image: "my-img",
- VolumeDevices: []corev1.VolumeDevice{{
- Name: "$(steps.prevStep.results.resultName)",
- DevicePath: "$(steps.prevStep.results.resultName)",
- }},
+ }},
+ expectedError: apis.FieldError{
+ Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
+ Paths: []string{"steps[0].volumeMounts.name", "steps[0].volumeMounts.mountPath", "steps[0].volumeMounts.subPath"},
+ },
+ }, {
+ name: "Cannot reference step results in VolumeDevices",
+ Steps: []v1beta1.Step{{
+ Image: "my-img",
+ VolumeDevices: []corev1.VolumeDevice{{
+ Name: "$(steps.prevStep.results.resultName)",
+ DevicePath: "$(steps.prevStep.results.resultName)",
}},
- expectedError: apis.FieldError{
- Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
- Paths: []string{"steps[0].volumeDevices.name", "steps[0].volumeDevices.devicePath"},
- },
+ }},
+ expectedError: apis.FieldError{
+ Message: "stepResult substitutions are only allowed in env, command and args. Found usage in",
+ Paths: []string{"steps[0].volumeDevices.name", "steps[0].volumeDevices.devicePath"},
},
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -2017,72 +2014,70 @@ func TestIncompatibleAPIVersions(t *testing.T) {
name string
requiredVersion string
spec v1beta1.TaskSpec
- }{
- {
- name: "step workspace requires beta",
- requiredVersion: "beta",
- spec: v1beta1.TaskSpec{
- Workspaces: []v1beta1.WorkspaceDeclaration{{
+ }{{
+ name: "step workspace requires beta",
+ requiredVersion: "beta",
+ spec: v1beta1.TaskSpec{
+ Workspaces: []v1beta1.WorkspaceDeclaration{{
+ Name: "foo",
+ }},
+ Steps: []v1beta1.Step{{
+ Image: "foo",
+ Workspaces: []v1beta1.WorkspaceUsage{{
Name: "foo",
}},
- Steps: []v1beta1.Step{{
- Image: "foo",
- Workspaces: []v1beta1.WorkspaceUsage{{
- Name: "foo",
- }},
- }},
- },
- }, {
- name: "sidecar workspace requires beta",
- requiredVersion: "beta",
- spec: v1beta1.TaskSpec{
- Workspaces: []v1beta1.WorkspaceDeclaration{{
+ }},
+ },
+ }, {
+ name: "sidecar workspace requires beta",
+ requiredVersion: "beta",
+ spec: v1beta1.TaskSpec{
+ Workspaces: []v1beta1.WorkspaceDeclaration{{
+ Name: "foo",
+ }},
+ Steps: []v1beta1.Step{{
+ Image: "foo",
+ }},
+ Sidecars: []v1beta1.Sidecar{{
+ Image: "foo",
+ Workspaces: []v1beta1.WorkspaceUsage{{
Name: "foo",
}},
- Steps: []v1beta1.Step{{
- Image: "foo",
- }},
- Sidecars: []v1beta1.Sidecar{{
- Image: "foo",
- Workspaces: []v1beta1.WorkspaceUsage{{
- Name: "foo",
- }},
- }},
- },
- }, {
- name: "windows script support requires alpha",
- requiredVersion: "alpha",
- spec: v1beta1.TaskSpec{
- Steps: []v1beta1.Step{{
- Image: "my-image",
- Script: `
+ }},
+ },
+ }, {
+ name: "windows script support requires alpha",
+ requiredVersion: "alpha",
+ spec: v1beta1.TaskSpec{
+ Steps: []v1beta1.Step{{
+ Image: "my-image",
+ Script: `
#!win powershell -File
script-1`,
- }},
- },
- }, {
- name: "stdout stream support requires alpha",
- requiredVersion: "alpha",
- spec: v1beta1.TaskSpec{
- Steps: []v1beta1.Step{{
- Image: "foo",
- StdoutConfig: &v1beta1.StepOutputConfig{
- Path: "/tmp/stdout.txt",
- },
- }},
- },
- }, {
- name: "stderr stream support requires alpha",
- requiredVersion: "alpha",
- spec: v1beta1.TaskSpec{
- Steps: []v1beta1.Step{{
- Image: "foo",
- StderrConfig: &v1beta1.StepOutputConfig{
- Path: "/tmp/stderr.txt",
- },
- }},
- },
+ }},
},
+ }, {
+ name: "stdout stream support requires alpha",
+ requiredVersion: "alpha",
+ spec: v1beta1.TaskSpec{
+ Steps: []v1beta1.Step{{
+ Image: "foo",
+ StdoutConfig: &v1beta1.StepOutputConfig{
+ Path: "/tmp/stdout.txt",
+ },
+ }},
+ },
+ }, {
+ name: "stderr stream support requires alpha",
+ requiredVersion: "alpha",
+ spec: v1beta1.TaskSpec{
+ Steps: []v1beta1.Step{{
+ Image: "foo",
+ StderrConfig: &v1beta1.StepOutputConfig{
+ Path: "/tmp/stderr.txt",
+ },
+ }},
+ }},
} {
for _, version := range versions {
testName := fmt.Sprintf("(using %s) %s", version, tt.name)
@@ -2120,165 +2115,161 @@ func TestGetArrayIndexParamRefs(t *testing.T) {
name string
taskspec *v1beta1.TaskSpec
want sets.String
- }{
- {
- name: "steps reference",
- taskspec: &v1beta1.TaskSpec{
- Params: []v1beta1.ParamSpec{{
- Name: "array-params",
- Default: v1beta1.NewStructuredValues("bar", "foo"),
- }},
- Steps: []v1beta1.Step{{
- Name: "$(params.array-params[10])",
- Image: "$(params.array-params[11])",
- Command: []string{"$(params.array-params[12])"},
- Args: []string{"$(params.array-params[13])"},
- Script: "echo $(params.array-params[14])",
- Env: []corev1.EnvVar{{
- Value: "$(params.array-params[15])",
- ValueFrom: &corev1.EnvVarSource{
- SecretKeyRef: &corev1.SecretKeySelector{
- Key: "$(params.array-params[16])",
- LocalObjectReference: corev1.LocalObjectReference{
- Name: "$(params.array-params[17])",
- },
- },
- ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
- Key: "$(params.array-params[18])",
- LocalObjectReference: corev1.LocalObjectReference{
- Name: "$(params.array-params[19])",
- },
- },
- },
- }},
- EnvFrom: []corev1.EnvFromSource{{
- Prefix: "$(params.array-params[20])",
- ConfigMapRef: &corev1.ConfigMapEnvSource{
+ }{{
+ name: "steps reference",
+ taskspec: &v1beta1.TaskSpec{
+ Params: []v1beta1.ParamSpec{{
+ Name: "array-params",
+ Default: v1beta1.NewStructuredValues("bar", "foo"),
+ }},
+ Steps: []v1beta1.Step{{
+ Name: "$(params.array-params[10])",
+ Image: "$(params.array-params[11])",
+ Command: []string{"$(params.array-params[12])"},
+ Args: []string{"$(params.array-params[13])"},
+ Script: "echo $(params.array-params[14])",
+ Env: []corev1.EnvVar{{
+ Value: "$(params.array-params[15])",
+ ValueFrom: &corev1.EnvVarSource{
+ SecretKeyRef: &corev1.SecretKeySelector{
+ Key: "$(params.array-params[16])",
LocalObjectReference: corev1.LocalObjectReference{
- Name: "$(params.array-params[21])",
+ Name: "$(params.array-params[17])",
},
},
- SecretRef: &corev1.SecretEnvSource{
+ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
+ Key: "$(params.array-params[18])",
LocalObjectReference: corev1.LocalObjectReference{
- Name: "$(params.array-params[22])",
+ Name: "$(params.array-params[19])",
},
},
- }},
- WorkingDir: "$(params.array-params[23])",
- VolumeMounts: []corev1.VolumeMount{{
- Name: "$(params.array-params[24])",
- MountPath: "$(params.array-params[25])",
- SubPath: "$(params.array-params[26])",
- }},
+ },
}},
- StepTemplate: &v1beta1.StepTemplate{
- Image: "$(params.array-params[27])",
- },
- },
- want: sets.NewString("$(params.array-params[10])", "$(params.array-params[11])", "$(params.array-params[12])", "$(params.array-params[13])", "$(params.array-params[14])",
- "$(params.array-params[15])", "$(params.array-params[16])", "$(params.array-params[17])", "$(params.array-params[18])", "$(params.array-params[19])", "$(params.array-params[20])",
- "$(params.array-params[21])", "$(params.array-params[22])", "$(params.array-params[23])", "$(params.array-params[24])", "$(params.array-params[25])", "$(params.array-params[26])", "$(params.array-params[27])"),
- }, {
- name: "stepTemplate reference",
- taskspec: &v1beta1.TaskSpec{
- Params: []v1beta1.ParamSpec{{
- Name: "array-params",
- Default: v1beta1.NewStructuredValues("bar", "foo"),
+ EnvFrom: []corev1.EnvFromSource{{
+ Prefix: "$(params.array-params[20])",
+ ConfigMapRef: &corev1.ConfigMapEnvSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "$(params.array-params[21])",
+ },
+ },
+ SecretRef: &corev1.SecretEnvSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "$(params.array-params[22])",
+ },
+ },
}},
- StepTemplate: &v1beta1.StepTemplate{
- Image: "$(params.array-params[3])",
- },
- },
- want: sets.NewString("$(params.array-params[3])"),
- }, {
- name: "volumes references",
- taskspec: &v1beta1.TaskSpec{
- Params: []v1beta1.ParamSpec{{
- Name: "array-params",
- Default: v1beta1.NewStructuredValues("bar", "foo"),
+ WorkingDir: "$(params.array-params[23])",
+ VolumeMounts: []corev1.VolumeMount{{
+ Name: "$(params.array-params[24])",
+ MountPath: "$(params.array-params[25])",
+ SubPath: "$(params.array-params[26])",
}},
- Volumes: []corev1.Volume{
- {
- Name: "$(params.array-params[10])",
- VolumeSource: corev1.VolumeSource{
- ConfigMap: &corev1.ConfigMapVolumeSource{
+ }},
+ StepTemplate: &v1beta1.StepTemplate{
+ Image: "$(params.array-params[27])",
+ },
+ },
+ want: sets.NewString("$(params.array-params[10])", "$(params.array-params[11])", "$(params.array-params[12])", "$(params.array-params[13])", "$(params.array-params[14])",
+ "$(params.array-params[15])", "$(params.array-params[16])", "$(params.array-params[17])", "$(params.array-params[18])", "$(params.array-params[19])", "$(params.array-params[20])",
+ "$(params.array-params[21])", "$(params.array-params[22])", "$(params.array-params[23])", "$(params.array-params[24])", "$(params.array-params[25])", "$(params.array-params[26])", "$(params.array-params[27])"),
+ }, {
+ name: "stepTemplate reference",
+ taskspec: &v1beta1.TaskSpec{
+ Params: []v1beta1.ParamSpec{{
+ Name: "array-params",
+ Default: v1beta1.NewStructuredValues("bar", "foo"),
+ }},
+ StepTemplate: &v1beta1.StepTemplate{
+ Image: "$(params.array-params[3])",
+ },
+ },
+ want: sets.NewString("$(params.array-params[3])"),
+ }, {
+ name: "volumes references",
+ taskspec: &v1beta1.TaskSpec{
+ Params: []v1beta1.ParamSpec{{
+ Name: "array-params",
+ Default: v1beta1.NewStructuredValues("bar", "foo"),
+ }},
+ Volumes: []corev1.Volume{{
+ Name: "$(params.array-params[10])",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "$(params.array-params[11])",
+ },
+ Items: []corev1.KeyToPath{{
+ Key: "$(params.array-params[12])",
+ Path: "$(params.array-params[13])",
+ },
+ },
+ },
+ Secret: &corev1.SecretVolumeSource{
+ SecretName: "$(params.array-params[14])",
+ Items: []corev1.KeyToPath{{
+ Key: "$(params.array-params[15])",
+ Path: "$(params.array-params[16])",
+ }},
+ },
+ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+ ClaimName: "$(params.array-params[17])",
+ },
+ Projected: &corev1.ProjectedVolumeSource{
+ Sources: []corev1.VolumeProjection{{
+ ConfigMap: &corev1.ConfigMapProjection{
LocalObjectReference: corev1.LocalObjectReference{
- Name: "$(params.array-params[11])",
+ Name: "$(params.array-params[18])",
},
- Items: []corev1.KeyToPath{
- {
- Key: "$(params.array-params[12])",
- Path: "$(params.array-params[13])",
- },
- },
- },
- Secret: &corev1.SecretVolumeSource{
- SecretName: "$(params.array-params[14])",
- Items: []corev1.KeyToPath{{
- Key: "$(params.array-params[15])",
- Path: "$(params.array-params[16])",
- }},
- },
- PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
- ClaimName: "$(params.array-params[17])",
},
- Projected: &corev1.ProjectedVolumeSource{
- Sources: []corev1.VolumeProjection{{
- ConfigMap: &corev1.ConfigMapProjection{
- LocalObjectReference: corev1.LocalObjectReference{
- Name: "$(params.array-params[18])",
- },
- },
- Secret: &corev1.SecretProjection{
- LocalObjectReference: corev1.LocalObjectReference{
- Name: "$(params.array-params[19])",
- },
- },
- ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
- Audience: "$(params.array-params[20])",
- },
- }},
- },
- CSI: &corev1.CSIVolumeSource{
- NodePublishSecretRef: &corev1.LocalObjectReference{
- Name: "$(params.array-params[21])",
+ Secret: &corev1.SecretProjection{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "$(params.array-params[19])",
},
- VolumeAttributes: map[string]string{"key": "$(params.array-params[22])"},
},
+ ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
+ Audience: "$(params.array-params[20])",
+ },
+ }},
+ },
+ CSI: &corev1.CSIVolumeSource{
+ NodePublishSecretRef: &corev1.LocalObjectReference{
+ Name: "$(params.array-params[21])",
},
+ VolumeAttributes: map[string]string{"key": "$(params.array-params[22])"},
},
},
},
- want: sets.NewString("$(params.array-params[10])", "$(params.array-params[11])", "$(params.array-params[12])", "$(params.array-params[13])", "$(params.array-params[14])",
- "$(params.array-params[15])", "$(params.array-params[16])", "$(params.array-params[17])", "$(params.array-params[18])", "$(params.array-params[19])", "$(params.array-params[20])",
- "$(params.array-params[21])", "$(params.array-params[22])"),
- }, {
- name: "workspaces references",
- taskspec: &v1beta1.TaskSpec{
- Params: []v1beta1.ParamSpec{{
- Name: "array-params",
- Default: v1beta1.NewStructuredValues("bar", "foo"),
- }},
- Workspaces: []v1beta1.WorkspaceDeclaration{{
- MountPath: "$(params.array-params[3])",
- }},
},
- want: sets.NewString("$(params.array-params[3])"),
- }, {
- name: "sidecar references",
- taskspec: &v1beta1.TaskSpec{
- Params: []v1beta1.ParamSpec{{
- Name: "array-params",
- Default: v1beta1.NewStructuredValues("bar", "foo"),
- }},
- Sidecars: []v1beta1.Sidecar{
- {
- Script: "$(params.array-params[3])",
- },
- },
+ },
+ want: sets.NewString("$(params.array-params[10])", "$(params.array-params[11])", "$(params.array-params[12])", "$(params.array-params[13])", "$(params.array-params[14])",
+ "$(params.array-params[15])", "$(params.array-params[16])", "$(params.array-params[17])", "$(params.array-params[18])", "$(params.array-params[19])", "$(params.array-params[20])",
+ "$(params.array-params[21])", "$(params.array-params[22])"),
+ }, {
+ name: "workspaces references",
+ taskspec: &v1beta1.TaskSpec{
+ Params: []v1beta1.ParamSpec{{
+ Name: "array-params",
+ Default: v1beta1.NewStructuredValues("bar", "foo"),
+ }},
+ Workspaces: []v1beta1.WorkspaceDeclaration{{
+ MountPath: "$(params.array-params[3])",
+ }},
+ },
+ want: sets.NewString("$(params.array-params[3])"),
+ }, {
+ name: "sidecar references",
+ taskspec: &v1beta1.TaskSpec{
+ Params: []v1beta1.ParamSpec{{
+ Name: "array-params",
+ Default: v1beta1.NewStructuredValues("bar", "foo"),
+ }},
+ Sidecars: []v1beta1.Sidecar{{
+ Script: "$(params.array-params[3])",
+ },
},
- want: sets.NewString("$(params.array-params[3])"),
},
+ want: sets.NewString("$(params.array-params[3])"),
+ },
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -2914,3 +2905,85 @@ func TestTaskSpecValidateErrorWithArtifactsRef(t *testing.T) {
})
}
}
+
+func TestTaskSpecValidate_StepWhen_Error(t *testing.T) {
+ tests := []struct {
+ name string
+ ts *v1beta1.TaskSpec
+ isCreate bool
+ Results []v1.StepResult
+ isUpdate bool
+ baselineTaskRun *v1beta1.TaskRun
+ expectedError apis.FieldError
+ EnableStepAction bool
+ EnableCEL bool
+ }{
+ {
+ name: "step when not allowed without enable step actions - create event",
+ ts: &v1beta1.TaskSpec{Steps: []v1beta1.Step{{
+ Image: "my-image",
+ When: v1beta1.StepWhenExpressions{{Input: "foo", Operator: selection.In, Values: []string{"foo"}}},
+ }}},
+ isCreate: true,
+ expectedError: apis.FieldError{
+ Message: "feature flag enable-step-actions should be set to true in order to use When in Steps.",
+ Paths: []string{"steps[0]"},
+ },
+ },
+ {
+ name: "step when not allowed without enable step actions - update and diverged event",
+ ts: &v1beta1.TaskSpec{Steps: []v1beta1.Step{{
+ Image: "my-image",
+ When: v1beta1.StepWhenExpressions{{Input: "foo", Operator: selection.In, Values: []string{"foo"}}},
+ }}},
+ isUpdate: true,
+ baselineTaskRun: &v1beta1.TaskRun{
+ Spec: v1beta1.TaskRunSpec{
+ TaskSpec: &v1beta1.TaskSpec{
+ Steps: []v1beta1.Step{{
+ Image: "my-image",
+ Results: []v1.StepResult{{Name: "a-result"}},
+ }},
+ },
+ },
+ },
+ expectedError: apis.FieldError{
+ Message: "feature flag enable-step-actions should be set to true in order to use When in Steps.",
+ Paths: []string{"steps[0]"},
+ },
+ },
+ {
+ name: "cel not allowed if EnableCELInWhenExpression is false",
+ ts: &v1beta1.TaskSpec{Steps: []v1beta1.Step{{
+ Image: "my-image",
+ When: v1beta1.StepWhenExpressions{{CEL: "'d'=='d'"}},
+ }}},
+ EnableStepAction: true,
+ expectedError: apis.FieldError{
+ Message: `feature flag enable-cel-in-whenexpression should be set to true to use CEL: 'd'=='d' in WhenExpression`,
+ Paths: []string{"steps[0].when[0]"},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := config.ToContext(context.Background(), &config.Config{
+ FeatureFlags: &config.FeatureFlags{
+ EnableStepActions: tt.EnableStepAction,
+ EnableCELInWhenExpression: tt.EnableCEL,
+ },
+ })
+ if tt.isCreate {
+ ctx = apis.WithinCreate(ctx)
+ }
+ if tt.isUpdate {
+ ctx = apis.WithinUpdate(ctx, tt.baselineTaskRun)
+ }
+ tt.ts.SetDefaults(ctx)
+ err := tt.ts.Validate(ctx)
+ if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
+ t.Errorf("StepActionSpec.Validate() errors diff %s", diff.PrintWantGot(d))
+ }
+ })
+ }
+}
diff --git a/pkg/apis/pipeline/v1beta1/when_types.go b/pkg/apis/pipeline/v1beta1/when_types.go
index f792ec199c8..ad24f8e62e2 100644
--- a/pkg/apis/pipeline/v1beta1/when_types.go
+++ b/pkg/apis/pipeline/v1beta1/when_types.go
@@ -98,6 +98,8 @@ func (we *WhenExpression) GetVarSubstitutionExpressions() ([]string, bool) {
// All of them need to evaluate to True for a guarded Task to be executed.
type WhenExpressions []WhenExpression
+type StepWhenExpressions = WhenExpressions
+
// AllowsExecution evaluates an Input's relationship to an array of Values, based on the Operator,
// to determine whether all the When Expressions are True. If they are all True, the guarded Task is
// executed, otherwise it is skipped.
diff --git a/pkg/apis/pipeline/v1beta1/when_validation.go b/pkg/apis/pipeline/v1beta1/when_validation.go
index 33855040b2b..aa6b4b4cbd7 100644
--- a/pkg/apis/pipeline/v1beta1/when_validation.go
+++ b/pkg/apis/pipeline/v1beta1/when_validation.go
@@ -48,7 +48,7 @@ func (wes WhenExpressions) validateWhenExpressionsFields(ctx context.Context) (e
func (we *WhenExpression) validateWhenExpressionFields(ctx context.Context) *apis.FieldError {
if we.CEL != "" {
if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableCELInWhenExpression {
- return apis.ErrGeneric("feature flag %s should be set to true to use CEL: %s in WhenExpression", config.EnableCELInWhenExpression, we.CEL)
+ return apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true to use CEL: %s in WhenExpression", config.EnableCELInWhenExpression, we.CEL), "")
}
if we.Input != "" || we.Operator != "" || len(we.Values) != 0 {
return apis.ErrGeneric(fmt.Sprintf("cel and input+operator+values cannot be set in one WhenExpression: %v", we))
diff --git a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go
index 04d3a09ad2e..e2e55e8f8d9 100644
--- a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go
+++ b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go
@@ -1858,6 +1858,13 @@ func (in *Step) DeepCopyInto(out *Step) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.When != nil {
+ in, out := &in.When, &out.When
+ *out = make(WhenExpressions, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
return
}
diff --git a/pkg/container/step_replacements.go b/pkg/container/step_replacements.go
index 921995157a2..e30c3335bac 100644
--- a/pkg/container/step_replacements.go
+++ b/pkg/container/step_replacements.go
@@ -31,6 +31,7 @@ func ApplyStepReplacements(step *v1.Step, stringReplacements map[string]string,
if step.StderrConfig != nil {
step.StderrConfig.Path = substitution.ApplyReplacements(step.StderrConfig.Path, stringReplacements)
}
+ step.When = step.When.ReplaceVariables(stringReplacements, arrayReplacements)
applyStepReplacements(step, stringReplacements, arrayReplacements)
}
diff --git a/pkg/container/step_replacements_test.go b/pkg/container/step_replacements_test.go
index 4da6e4acf70..ba45fee8901 100644
--- a/pkg/container/step_replacements_test.go
+++ b/pkg/container/step_replacements_test.go
@@ -23,6 +23,7 @@ import (
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/pipeline/pkg/container"
corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/selection"
)
func TestApplyStepReplacements(t *testing.T) {
@@ -43,6 +44,12 @@ func TestApplyStepReplacements(t *testing.T) {
Args: []string{"$(array.replace.me)"},
WorkingDir: "$(replace.me)",
OnError: "$(replace.me)",
+ When: v1.StepWhenExpressions{{
+ Input: "$(replace.me)",
+ Operator: selection.In,
+ Values: []string{"$(array.replace.me)"},
+ CEL: "'$(replace.me)=bar'",
+ }},
EnvFrom: []corev1.EnvFromSource{{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
@@ -94,6 +101,12 @@ func TestApplyStepReplacements(t *testing.T) {
Args: []string{"val1", "val2"},
WorkingDir: "replaced!",
OnError: "replaced!",
+ When: v1.StepWhenExpressions{{
+ Input: "replaced!",
+ Operator: selection.In,
+ Values: []string{"val1", "val2"},
+ CEL: "'replaced!=bar'",
+ }},
EnvFrom: []corev1.EnvFromSource{{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
diff --git a/pkg/entrypoint/entrypointer.go b/pkg/entrypoint/entrypointer.go
index 792811b19f7..58665dd3d7d 100644
--- a/pkg/entrypoint/entrypointer.go
+++ b/pkg/entrypoint/entrypointer.go
@@ -40,6 +40,8 @@ import (
"github.com/tektoncd/pipeline/pkg/result"
"github.com/tektoncd/pipeline/pkg/spire"
"github.com/tektoncd/pipeline/pkg/termination"
+
+ "github.com/google/cel-go/cel"
"go.uber.org/zap"
)
@@ -132,6 +134,9 @@ type Entrypointer struct {
ResultsDirectory string
// ResultExtractionMethod is the method using which the controller extracts the results from the task pod.
ResultExtractionMethod string
+
+ // StepWhenExpressions a list of when expression to decide if the step should be skipped
+ StepWhenExpressions v1.StepWhenExpressions
}
// Waiter encapsulates waiting for files to exist.
@@ -224,7 +229,20 @@ func (e Entrypointer) Go() error {
logger.Error("Error while waiting for cancellation", zap.Error(err))
}
}()
- err = e.Runner.Run(ctx, e.Command...)
+ allowExec, err1 := e.allowExec()
+
+ switch {
+ case err1 != nil:
+ err = err1
+ case allowExec:
+ err = e.Runner.Run(ctx, e.Command...)
+ default:
+ logger.Info("Step was skipped due to when expressions were evaluated to false.")
+ output = append(output, e.outputRunResult(pod.TerminationReasonSkipped))
+ e.WritePostFile(e.PostFile, nil)
+ e.WriteExitCodeFile(e.StepMetadataDir, "0")
+ return nil
+ }
}
var ee *exec.ExitError
@@ -303,6 +321,50 @@ func readArtifacts(fp string) ([]result.RunResult, error) {
return []result.RunResult{{Key: fp, Value: string(file), ResultType: result.StepArtifactsResultType}}, nil
}
+func (e Entrypointer) allowExec() (bool, error) {
+ when := e.StepWhenExpressions
+ m := map[string]bool{}
+
+ for _, we := range when {
+ if we.CEL == "" {
+ continue
+ }
+ b, ok := m[we.CEL]
+ if ok && !b {
+ return false, nil
+ }
+
+ env, err := cel.NewEnv()
+ if err != nil {
+ return false, err
+ }
+ ast, iss := env.Compile(we.CEL)
+ if iss.Err() != nil {
+ return false, iss.Err()
+ }
+ // Generate an evaluable instance of the Ast within the environment
+ prg, err := env.Program(ast)
+ if err != nil {
+ return false, err
+ }
+ // Evaluate the CEL expression
+ out, _, err := prg.Eval(map[string]interface{}{})
+ if err != nil {
+ return false, err
+ }
+
+ b, ok = out.Value().(bool)
+ if !ok {
+ return false, fmt.Errorf("the CEL expression %s is not evaluated to a boolean", we.CEL)
+ }
+ if !b {
+ return false, err
+ }
+ m[we.CEL] = true
+ }
+ return when.AllowsExecution(m), nil
+}
+
func (e Entrypointer) readResultsFromDisk(ctx context.Context, resultDir string, resultType result.ResultType) error {
output := []result.RunResult{}
results := e.Results
@@ -485,6 +547,13 @@ func (e *Entrypointer) applyStepResultSubstitutions(stepDir string) error {
if err := replaceEnv(stepDir); err != nil {
return err
}
+
+ // replace when
+ newWhen, err := replaceWhen(stepDir, e.StepWhenExpressions)
+ if err != nil {
+ return err
+ }
+ e.StepWhenExpressions = newWhen
// command + args
newCommand, err := replaceCommandAndArgs(e.Command, stepDir)
if err != nil {
@@ -494,6 +563,58 @@ func (e *Entrypointer) applyStepResultSubstitutions(stepDir string) error {
return nil
}
+func replaceWhen(stepDir string, when v1.StepWhenExpressions) (v1.StepWhenExpressions, error) {
+ for i, w := range when {
+ var newValues []string
+ flag:
+ for _, v := range when[i].Values {
+ matches := resultref.StepResultRegex.FindAllStringSubmatch(v, -1)
+ newV := v
+ for _, m := range matches {
+ replaceWithString, replaceWithArray, err := findReplacement(stepDir, m[0])
+ if err != nil {
+ return v1.WhenExpressions{}, err
+ }
+ // replaceWithString and replaceWithArray are mutually exclusive
+ if len(replaceWithArray) > 0 {
+ if v != m[0] {
+ // it has to be exact in "$(steps.