From 5625a6a7825f0d26ac6063c9af81fc0185e07f0f Mon Sep 17 00:00:00 2001 From: ericzzzzzzz <102683393+ericzzzzzzz@users.noreply.github.com> Date: Mon, 4 Mar 2024 22:31:53 -0500 Subject: [PATCH] feat: introduce when expressions to steps --- cmd/entrypoint/main.go | 9 + docs/pipeline-api.md | 30 +- docs/stepactions.md | 102 +++ .../v1/taskruns/alpha/stepaction-when.yaml | 72 ++ pkg/apis/pipeline/v1/container_types.go | 4 + pkg/apis/pipeline/v1/merge.go | 2 +- pkg/apis/pipeline/v1/merge_test.go | 12 + pkg/apis/pipeline/v1/openapi_generated.go | 16 +- pkg/apis/pipeline/v1/swagger.json | 8 + pkg/apis/pipeline/v1/task_validation.go | 37 +- pkg/apis/pipeline/v1/task_validation_test.go | 83 +++ pkg/apis/pipeline/v1/when_types.go | 2 + pkg/apis/pipeline/v1/when_validation.go | 2 +- pkg/apis/pipeline/v1/zz_generated.deepcopy.go | 7 + .../pipeline/v1beta1/container_conversion.go | 10 + pkg/apis/pipeline/v1beta1/container_types.go | 2 + pkg/apis/pipeline/v1beta1/merge.go | 2 +- pkg/apis/pipeline/v1beta1/merge_test.go | 12 + .../pipeline/v1beta1/openapi_generated.go | 15 +- pkg/apis/pipeline/v1beta1/swagger.json | 7 + .../pipeline/v1beta1/task_conversion_test.go | 23 + pkg/apis/pipeline/v1beta1/task_validation.go | 37 +- .../pipeline/v1beta1/task_validation_test.go | 693 ++++++++++-------- pkg/apis/pipeline/v1beta1/when_types.go | 2 + pkg/apis/pipeline/v1beta1/when_validation.go | 2 +- .../pipeline/v1beta1/zz_generated.deepcopy.go | 7 + pkg/container/step_replacements.go | 1 + pkg/container/step_replacements_test.go | 13 + pkg/entrypoint/entrypointer.go | 123 +++- pkg/entrypoint/entrypointer_test.go | 396 +++++++++- pkg/pod/entrypoint.go | 10 + pkg/pod/entrypoint_test.go | 44 +- test/step_when_test.go | 469 ++++++++++++ 33 files changed, 1910 insertions(+), 344 deletions(-) create mode 100644 examples/v1/taskruns/alpha/stepaction-when.yaml create mode 100644 test/step_when_test.go 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
+ + +WhenExpressions + + + + +(Optional) +

When is a list of when expressions that need to be true for the task to run

+ +

StepOutputConfig @@ -6251,7 +6265,7 @@ More info about CEL syntax: WhenExpressions ([]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
+ + +WhenExpressions + + + + + +

StepActionObject @@ -16190,7 +16216,7 @@ More info about CEL syntax: WhenExpressions ([]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..results.[*])" format, without anything else in the original string + return nil, errors.New("value must be in \"$(steps..results.[*])\" format, when using array results") + } + newValues = append(newValues, replaceWithArray...) + continue flag + } + newV = strings.ReplaceAll(newV, m[0], replaceWithString) + } + newValues = append(newValues, newV) + } + when[i].Values = newValues + + matches := resultref.StepResultRegex.FindAllStringSubmatch(w.Input, -1) + v := when[i].Input + for _, m := range matches { + replaceWith, _, err := findReplacement(stepDir, m[0]) + if err != nil { + return v1.StepWhenExpressions{}, err + } + v = strings.ReplaceAll(v, m[0], replaceWith) + } + when[i].Input = v + + matches = resultref.StepResultRegex.FindAllStringSubmatch(w.CEL, -1) + c := when[i].CEL + for _, m := range matches { + replaceWith, _, err := findReplacement(stepDir, m[0]) + if err != nil { + return v1.StepWhenExpressions{}, err + } + c = strings.ReplaceAll(c, m[0], replaceWith) + } + when[i].CEL = c + } + return when, nil +} + // outputRunResult returns the run reason for a termination func (e Entrypointer) outputRunResult(terminationReason string) result.RunResult { return result.RunResult{ diff --git a/pkg/entrypoint/entrypointer_test.go b/pkg/entrypoint/entrypointer_test.go index b15398f32f2..48afc2e2873 100644 --- a/pkg/entrypoint/entrypointer_test.go +++ b/pkg/entrypoint/entrypointer_test.go @@ -32,7 +32,6 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" "github.com/tektoncd/pipeline/pkg/apis/config" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" @@ -41,7 +40,10 @@ import ( "github.com/tektoncd/pipeline/pkg/spire" "github.com/tektoncd/pipeline/pkg/termination" "github.com/tektoncd/pipeline/test/diff" + + "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/selection" "knative.dev/pkg/logging" ) @@ -946,6 +948,377 @@ func TestApplyStepResultSubstitutions_Command(t *testing.T) { } } +func TestApplyStepWhenSubstitutions_Input(t *testing.T) { + testCases := []struct { + name string + stepName string + resultName string + result string + want v1.StepWhenExpressions + when v1.StepWhenExpressions + wantErr bool + }{{ + name: "string param", + stepName: "foo", + resultName: "res", + result: "Hello", + when: v1.StepWhenExpressions{{Input: "$(steps.foo.results.res)"}}, + want: v1.StepWhenExpressions{{Input: "Hello"}}, + wantErr: false, + }, { + name: "array param", + stepName: "foo", + resultName: "res", + result: "[\"Hello\",\"World\"]", + when: v1.StepWhenExpressions{{Input: "$(steps.foo.results.res[1])"}}, + want: v1.StepWhenExpressions{{Input: "World"}}, + wantErr: false, + }, { + name: "object param", + stepName: "foo", + resultName: "res", + result: "{\"hello\":\"World\"}", + when: v1.StepWhenExpressions{{Input: "$(steps.foo.results.res.hello)"}}, + want: v1.StepWhenExpressions{{Input: "World"}}, + wantErr: false, + }, { + name: "bad-result-format", + stepName: "foo", + resultName: "res", + result: "{\"hello\":\"World\"}", + when: v1.StepWhenExpressions{{Input: "$(steps.foo.results.res.hello.bar)"}}, + want: v1.StepWhenExpressions{{Input: "$(steps.foo.results.res.hello.bar)"}}, + wantErr: true, + }} + stepDir := createTmpDir(t, "when-input") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resultPath := filepath.Join(stepDir, pod.GetContainerName(tc.stepName), "results") + err := os.MkdirAll(resultPath, 0750) + if err != nil { + log.Fatal(err) + } + resultFile := filepath.Join(resultPath, tc.resultName) + err = os.WriteFile(resultFile, []byte(tc.result), 0666) + if err != nil { + log.Fatal(err) + } + e := Entrypointer{ + Command: []string{}, + StepWhenExpressions: tc.when, + } + err = e.applyStepResultSubstitutions(stepDir) + if tc.wantErr == false && err != nil { + t.Fatalf("Did not expect and error but got: %v", err) + } else if tc.wantErr == true && err == nil { + t.Fatalf("Expected and error but did not get any.") + } + got := e.StepWhenExpressions + if d := cmp.Diff(got, tc.want); d != "" { + t.Errorf("applyStepResultSubstitutions(): got %v; want %v", got, tc.want) + } + }) + } +} + +func TestApplyStepWhenSubstitutions_CEL(t *testing.T) { + testCases := []struct { + name string + stepName string + resultName string + result string + want v1.StepWhenExpressions + when v1.StepWhenExpressions + wantErr bool + }{{ + name: "string param", + stepName: "foo", + resultName: "res", + result: "Hello", + when: v1.StepWhenExpressions{{CEL: "$(steps.foo.results.res)"}}, + want: v1.StepWhenExpressions{{CEL: "Hello"}}, + wantErr: false, + }, { + name: "array param", + stepName: "foo", + resultName: "res", + result: "[\"Hello\",\"World\"]", + when: v1.StepWhenExpressions{{CEL: "$(steps.foo.results.res[1])"}}, + want: v1.StepWhenExpressions{{CEL: "World"}}, + wantErr: false, + }, { + name: "object param", + stepName: "foo", + resultName: "res", + result: "{\"hello\":\"World\"}", + when: v1.StepWhenExpressions{{CEL: "$(steps.foo.results.res.hello)"}}, + want: v1.StepWhenExpressions{{CEL: "World"}}, + wantErr: false, + }, { + name: "bad-result-format", + stepName: "foo", + resultName: "res", + result: "{\"hello\":\"World\"}", + when: v1.StepWhenExpressions{{CEL: "$(steps.foo.results.res.hello.bar)"}}, + want: v1.StepWhenExpressions{{CEL: "$(steps.foo.results.res.hello.bar)"}}, + wantErr: true, + }} + stepDir := createTmpDir(t, "when-CEL") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resultPath := filepath.Join(stepDir, pod.GetContainerName(tc.stepName), "results") + err := os.MkdirAll(resultPath, 0750) + if err != nil { + log.Fatal(err) + } + resultFile := filepath.Join(resultPath, tc.resultName) + err = os.WriteFile(resultFile, []byte(tc.result), 0666) + if err != nil { + log.Fatal(err) + } + e := Entrypointer{ + Command: []string{}, + StepWhenExpressions: tc.when, + } + err = e.applyStepResultSubstitutions(stepDir) + if tc.wantErr == false && err != nil { + t.Fatalf("Did not expect and error but got: %v", err) + } else if tc.wantErr == true && err == nil { + t.Fatalf("Expected and error but did not get any.") + } + got := e.StepWhenExpressions + if d := cmp.Diff(got, tc.want); d != "" { + t.Errorf("applyStepResultSubstitutions(): got %v; want %v", got, tc.want) + } + }) + } +} + +func TestApplyStepWhenSubstitutions_Values(t *testing.T) { + testCases := []struct { + name string + stepName string + resultName string + result string + want v1.StepWhenExpressions + when v1.StepWhenExpressions + wantErr bool + }{{ + name: "string param", + stepName: "foo", + resultName: "res", + result: "Hello", + when: v1.StepWhenExpressions{{Values: []string{"$(steps.foo.results.res)"}}}, + want: v1.StepWhenExpressions{{Values: []string{"Hello"}}}, + wantErr: false, + }, { + name: "array param, reference an element", + stepName: "foo", + resultName: "res", + result: "[\"Hello\",\"World\"]", + when: v1.StepWhenExpressions{{Values: []string{"$(steps.foo.results.res[1])"}}}, + want: v1.StepWhenExpressions{{Values: []string{"World"}}}, + wantErr: false, + }, { + name: "array param, reference whole array", + stepName: "foo", + resultName: "res", + result: "[\"Hello\",\"World\"]", + when: v1.StepWhenExpressions{{Values: []string{"$(steps.foo.results.res[*])"}}}, + want: v1.StepWhenExpressions{{Values: []string{"Hello", "World"}}}, + wantErr: false, + }, + { + name: "array param, reference whole array with concatenation, error", + stepName: "foo", + resultName: "res", + result: "[\"Hello\",\"World\"]", + when: v1.StepWhenExpressions{{Values: []string{"$(steps.foo.results.res[*])1"}}}, + want: v1.StepWhenExpressions{{Values: []string{"$(steps.foo.results.res[*])1"}}}, + wantErr: true, + }, + { + name: "object param", + stepName: "foo", + resultName: "res", + result: "{\"hello\":\"World\"}", + when: v1.StepWhenExpressions{{Values: []string{"$(steps.foo.results.res.hello)"}}}, + want: v1.StepWhenExpressions{{Values: []string{"World"}}}, + wantErr: false, + }, { + name: "bad-result-format", + stepName: "foo", + resultName: "res", + result: "{\"hello\":\"World\"}", + when: v1.StepWhenExpressions{{Values: []string{"$(steps.foo.results.res.hello.bar)"}}}, + want: v1.StepWhenExpressions{{Values: []string{"$(steps.foo.results.res.hello.bar)"}}}, + wantErr: true, + }} + stepDir := createTmpDir(t, "when-values") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resultPath := filepath.Join(stepDir, pod.GetContainerName(tc.stepName), "results") + err := os.MkdirAll(resultPath, 0750) + if err != nil { + log.Fatal(err) + } + resultFile := filepath.Join(resultPath, tc.resultName) + err = os.WriteFile(resultFile, []byte(tc.result), 0666) + if err != nil { + log.Fatal(err) + } + e := Entrypointer{ + Command: []string{}, + StepWhenExpressions: tc.when, + } + err = e.applyStepResultSubstitutions(stepDir) + if tc.wantErr == false && err != nil { + t.Fatalf("Did not expect and error but got: %v", err) + } else if tc.wantErr == true && err == nil { + t.Fatalf("Expected and error but did not get any.") + } + got := e.StepWhenExpressions + if d := cmp.Diff(got, tc.want); d != "" { + t.Errorf("applyStepResultSubstitutions(): got %v; want %v", got, tc.want) + } + }) + } +} + +func TestAllowExec(t *testing.T) { + tests := []struct { + name string + whenExpressions v1.StepWhenExpressions + expected bool + wantErr bool + }{{ + name: "in expression", + whenExpressions: v1.StepWhenExpressions{ + { + Input: "foo", + Operator: selection.In, + Values: []string{"foo", "bar"}, + }, + }, + expected: true, + }, { + name: "notin expression", + whenExpressions: v1.StepWhenExpressions{ + { + Input: "foobar", + Operator: selection.NotIn, + Values: []string{"foobar"}, + }, + }, + expected: false, + }, { + name: "multiple expressions - false", + whenExpressions: v1.StepWhenExpressions{ + { + Input: "foobar", + Operator: selection.In, + Values: []string{"foobar"}, + }, { + Input: "foo", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + expected: false, + }, { + name: "multiple expressions - true", + whenExpressions: v1.StepWhenExpressions{ + { + Input: "foobar", + Operator: selection.In, + Values: []string{"foobar"}, + }, { + Input: "foo", + Operator: selection.NotIn, + Values: []string{"bar"}, + }, + }, + expected: true, + }, { + name: "CEL is true", + whenExpressions: v1.StepWhenExpressions{ + { + CEL: "'foo'=='foo'", + }, + }, + expected: true, + }, { + name: "CEL is false", + whenExpressions: v1.StepWhenExpressions{ + { + CEL: "'foo'!='foo'", + }, + }, + expected: false, + }, + { + name: "multiple expressions - 1. CEL is true 2. In Op is false, expect false", + whenExpressions: v1.StepWhenExpressions{ + { + CEL: "'foo'=='foo'", + }, + { + Input: "foo", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + expected: false, + }, + { + name: "multiple expressions - 1. CEL is true 2. CEL is false, expect false", + whenExpressions: v1.StepWhenExpressions{ + { + CEL: "'foo'=='foo'", + }, + { + CEL: "'xxx'!='xxx'", + }, + }, + expected: false, + }, + { + name: "CEL is not evaluated to bool", + whenExpressions: v1.StepWhenExpressions{ + { + CEL: "'foo'", + }, + }, + expected: false, + wantErr: true, + }, + { + name: "CEL cannot be compiled", + whenExpressions: v1.StepWhenExpressions{ + { + CEL: "foo==foo", + }, + }, + expected: false, + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + e := Entrypointer{ + StepWhenExpressions: tc.whenExpressions, + } + allowExec, err := e.allowExec() + if d := cmp.Diff(allowExec, tc.expected); d != "" { + t.Errorf("expected equlity of execution evalution, but got: %t, want: %t", allowExec, tc.expected) + } + if (err != nil) != tc.wantErr { + t.Errorf("error checking failed, err %v", err) + } + }) + } +} func TestIsContextDeadlineError(t *testing.T) { ctxErr := ContextError(context.DeadlineExceeded.Error()) if !IsContextDeadlineError(ctxErr) { @@ -978,6 +1351,7 @@ func TestTerminationReason(t *testing.T) { expectedExitCode *string expectedWrotefile *string expectedStatus []result.RunResult + when v1.WhenExpressions }{ { desc: "reason completed", @@ -1039,7 +1413,7 @@ func TestTerminationReason(t *testing.T) { }, }, { - desc: "reason skipped", + desc: "reason skipped due to previous step error", waitFiles: []string{"file"}, expectedRunErr: ErrSkipPreviousStepFailed, expectedWrotefile: ptr("postfile.err"), @@ -1055,6 +1429,23 @@ func TestTerminationReason(t *testing.T) { }, }, }, + { + desc: "reason skipped due to when expressions evaluation", + expectedExitCode: ptr("0"), + expectedWrotefile: ptr("postfile"), + when: v1.StepWhenExpressions{{Input: "foo", Operator: selection.In, Values: []string{"bar"}}}, + expectedStatus: []result.RunResult{ + { + Key: "Reason", + Value: pod.TerminationReasonSkipped, + ResultType: result.InternalTektonResultType, + }, + { + Key: "StartedAt", + ResultType: result.InternalTektonResultType, + }, + }, + }, } for _, test := range tests { @@ -1084,6 +1475,7 @@ func TestTerminationReason(t *testing.T) { BreakpointOnFailure: false, StepMetadataDir: tmpFolder, OnError: test.onError, + StepWhenExpressions: test.when, } err = e.Go() diff --git a/pkg/pod/entrypoint.go b/pkg/pod/entrypoint.go index 3addcd1df00..e6117c0defc 100644 --- a/pkg/pod/entrypoint.go +++ b/pkg/pod/entrypoint.go @@ -171,7 +171,17 @@ func orderContainers(ctx context.Context, commonExtraEntrypointArgs []string, st } // add step results stepResultArgs := stepResultArgument(taskSpec.Steps[i].Results) + argsForEntrypoint = append(argsForEntrypoint, stepResultArgs...) + if len(taskSpec.Steps[i].When) > 0 { + // marshal and pass to the entrypoint and unmarshal it there. + marshal, err := json.Marshal(taskSpec.Steps[i].When) + + if err != nil { + return nil, fmt.Errorf("faile to resolve when %w", err) + } + argsForEntrypoint = append(argsForEntrypoint, "--when_expressions", string(marshal)) + } } argsForEntrypoint = append(argsForEntrypoint, resultArgument(steps, taskSpec.Results)...) } diff --git a/pkg/pod/entrypoint_test.go b/pkg/pod/entrypoint_test.go index 296bedc3cd8..21926649062 100644 --- a/pkg/pod/entrypoint_test.go +++ b/pkg/pod/entrypoint_test.go @@ -22,14 +22,16 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" "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/test/diff" + + "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/selection" fakek8s "k8s.io/client-go/kubernetes/fake" k8stesting "k8s.io/client-go/testing" ) @@ -361,6 +363,46 @@ func TestEntryPointStepActionResults(t *testing.T) { t.Errorf("Diff %s", diff.PrintWantGot(d)) } } +func TestEntryPointStepWhen(t *testing.T) { + containers := []corev1.Container{{ + Image: "step-1", + Command: []string{"cmd"}, + Args: []string{"arg1", "arg2"}, + }} + ts := v1.TaskSpec{Steps: []v1.Step{ + { + Name: "Test-When", + Image: "step-1", + Command: []string{"cmd"}, + Args: []string{"arg1", "arg2"}, + When: v1.StepWhenExpressions{{Input: "foo", Operator: selection.In, Values: []string{"foo", "bar"}}}, + }, + }} + got, err := orderContainers(context.Background(), []string{}, containers, &ts, nil, true, false) + if err != nil { + t.Fatalf("orderContainers: %v", err) + } + want := []corev1.Container{{ + Image: "step-1", + Command: []string{"/tekton/bin/entrypoint"}, + Args: []string{ + "-wait_file", "/tekton/downward/ready", + "-wait_file_content", + "-post_file", "/tekton/run/0/out", + "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/run/0/status", + "--when_expressions", + `[{"input":"foo","operator":"in","values":["foo","bar"]}]`, + "-entrypoint", "cmd", "--", + "arg1", "arg2", + }, + VolumeMounts: []corev1.VolumeMount{downwardMount}, + TerminationMessagePath: "/tekton/termination", + }} + if d := cmp.Diff(want, got); d != "" { + t.Errorf("Diff %s", diff.PrintWantGot(d)) + } +} func TestEntryPointResults(t *testing.T) { taskSpec := v1.TaskSpec{ diff --git a/test/step_when_test.go b/test/step_when_test.go new file mode 100644 index 00000000000..adc0e8df01f --- /dev/null +++ b/test/step_when_test.go @@ -0,0 +1,469 @@ +//go:build e2e +// +build e2e + +// /* +// Copyright 2024 The Tekton Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +package test + +import ( + "context" + "fmt" + "strconv" + "testing" + + "github.com/tektoncd/pipeline/pkg/apis/config" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/test/parse" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/system" + knativetest "knative.dev/pkg/test" + "knative.dev/pkg/test/helpers" +) + +func TestWhenExpressionsInStep(t *testing.T) { + tests := []struct { + desc string + expected []v1.StepState + taskTemplate string + }{ + { + desc: "single step, when is false, skipped", + expected: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Skipped", + Name: "foo", + Container: "step-foo", + }}, + taskTemplate: ` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: foo + image: busybox + command: ['/bin/sh'] + args: ['-c', 'echo hello'] + when: + - input: "foo" + operator: in + values: [ "bar" ] +`, + }, + { + desc: "single step, when is true, completed", + expected: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Completed", + Name: "foo", + Container: "step-foo", + }}, + taskTemplate: ` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: foo + image: busybox + command: ['/bin/sh'] + args: ['-c', 'echo hello'] + when: + - input: "foo" + operator: in + values: [ "foo" ] +`, + }, + { + desc: "two steps, first when is false, skipped and second step complete", + expected: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Skipped", + Name: "foo", + Container: "step-foo", + }, { + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Completed", + Name: "bar", + Container: "step-bar", + }}, + taskTemplate: ` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: foo + image: busybox + command: ['/bin/sh'] + args: ['-c', 'echo hello'] + when: + - input: "foo" + operator: in + values: [ "bar" ] + - name: bar + image: busybox + command: ['/bin/sh'] + args: ['-c', 'echo hello'] +`, + }, + { + desc: "two steps, when is based on step-results", + expected: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Completed", + Name: "foo", + Container: "step-foo", + Results: []v1.TaskRunStepResult{ + { + Name: "result1", + Type: "string", + Value: v1.ParamValue{Type: "string", StringVal: "bar"}, + }, + }, + }, { + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Completed", + Name: "bar", + Container: "step-bar", + }}, + taskTemplate: ` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: foo + image: busybox + results: + - name: result1 + command: ['/bin/sh'] + args: ['-c', 'echo -n bar >> $(step.results.result1.path)'] + - name: bar + image: busybox + command: ['/bin/sh'] + args: ['-c', 'echo hello'] + when: + - input: "$(steps.foo.results.result1)" + operator: in + values: [ "bar" ] +`, + }, + } + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + checkFlagsEnabled := requireAllGates(requireEnableStepActionsGate) + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + c, namespace := setup(ctx, t) + checkFlagsEnabled(ctx, t, c, "") + + knativetest.CleanupOnInterrupt(func() { + tearDown(ctx, t, c, namespace) + }, t.Logf) + + defer tearDown(ctx, t, c, namespace) + + taskRunName := helpers.ObjectNameForTest(t) + + t.Logf("Creating Task and TaskRun in namespace %s", namespace) + task := parse.MustParseV1Task(t, fmt.Sprintf(tc.taskTemplate, taskRunName, namespace)) + if _, err := c.V1TaskClient.Create(ctx, task, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create Task: %s", err) + } + taskRun := parse.MustParseV1TaskRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + taskRef: + name: %s +`, taskRunName, namespace, task.Name)) + if _, err := c.V1TaskRunClient.Create(ctx, taskRun, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create TaskRun: %s", err) + } + + if err := WaitForTaskRunState(ctx, c, taskRunName, TaskRunSucceed(taskRunName), "TaskRunSucceeded", v1Version); err != nil { + t.Errorf("Error waiting for TaskRun to finish: %s", err) + } + + taskrun, err := c.V1TaskRunClient.Get(ctx, taskRunName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected TaskRun %s: %s", taskRunName, err) + } + var ops cmp.Options + ops = append(ops, cmpopts.IgnoreFields(corev1.ContainerStateTerminated{}, "StartedAt", "FinishedAt", "ContainerID", "Message")) + ops = append(ops, cmpopts.IgnoreFields(v1.StepState{}, "ImageID")) + if d := cmp.Diff(taskrun.Status.Steps, tc.expected, ops); d != "" { + t.Fatalf("-got, +want: %v", d) + } + }) + } +} + +func TestWhenExpressionsCELInStep(t *testing.T) { + tests := []struct { + desc string + expected []v1.StepState + taskTemplate string + }{ + { + desc: "single step, when is false, skipped", + expected: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Skipped", + Name: "foo", + Container: "step-foo", + }}, + taskTemplate: ` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: foo + image: busybox + command: ['/bin/sh'] + args: ['-c', 'echo hello'] + when: + - cel: "'foo'=='bar'" +`, + }, + { + desc: "single step, when CEL is true, completed", + expected: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Completed", + Name: "foo", + Container: "step-foo", + }}, + taskTemplate: ` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: foo + image: busybox + command: ['/bin/sh'] + args: ['-c', 'echo hello'] + when: + - cel: "'foo'=='foo'" +`, + }, + { + desc: "two steps, first when CEL is false, skipped and second step complete", + expected: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Skipped", + Name: "foo", + Container: "step-foo", + }, { + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Completed", + Name: "bar", + Container: "step-bar", + }}, + taskTemplate: ` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: foo + image: busybox + command: ['/bin/sh'] + args: ['-c', 'echo hello'] + when: + - cel: "'foo'=='bar'" + - name: bar + image: busybox + command: ['/bin/sh'] + args: ['-c', 'echo hello'] +`, + }, + { + desc: "two steps, when cel is based on step-results", + expected: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Completed", + Name: "foo", + Container: "step-foo", + Results: []v1.TaskRunStepResult{ + { + Name: "result1", + Type: "string", + Value: v1.ParamValue{Type: "string", StringVal: "bar"}, + }, + }, + }, { + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + TerminationReason: "Completed", + Name: "bar", + Container: "step-bar", + }}, + taskTemplate: ` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: foo + image: busybox + results: + - name: result1 + command: ['/bin/sh'] + args: ['-c', 'echo -n bar >> $(step.results.result1.path)'] + - name: bar + image: busybox + command: ['/bin/sh'] + args: ['-c', 'echo hello'] + when: + - cel: "'$(steps.foo.results.result1)'=='bar'" +`, + }, + } + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + featureFlags := getFeatureFlagsBaseOnAPIFlag(t) + checkFlagsEnabled := requireAllGates(requireEnableStepActionsGate) + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + c, namespace := setup(ctx, t) + checkFlagsEnabled(ctx, t, c, "") + + previous := featureFlags.EnableCELInWhenExpression + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + config.EnableCELInWhenExpression: "true", + }) + + knativetest.CleanupOnInterrupt(func() { + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + config.EnableCELInWhenExpression: strconv.FormatBool(previous), + }) + tearDown(ctx, t, c, namespace) + }, t.Logf) + defer func() { + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + config.EnableCELInWhenExpression: strconv.FormatBool(previous), + }) + tearDown(ctx, t, c, namespace) + }() + + taskRunName := helpers.ObjectNameForTest(t) + + t.Logf("Creating Task and TaskRun in namespace %s", namespace) + task := parse.MustParseV1Task(t, fmt.Sprintf(tc.taskTemplate, taskRunName, namespace)) + if _, err := c.V1TaskClient.Create(ctx, task, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create Task: %s", err) + } + taskRun := parse.MustParseV1TaskRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + taskRef: + name: %s +`, taskRunName, namespace, task.Name)) + if _, err := c.V1TaskRunClient.Create(ctx, taskRun, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create TaskRun: %s", err) + } + + if err := WaitForTaskRunState(ctx, c, taskRunName, TaskRunSucceed(taskRunName), "TaskRunSucceeded", v1Version); err != nil { + t.Errorf("Error waiting for TaskRun to finish: %s", err) + } + + taskrun, err := c.V1TaskRunClient.Get(ctx, taskRunName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected TaskRun %s: %s", taskRunName, err) + } + var ops cmp.Options + ops = append(ops, cmpopts.IgnoreFields(corev1.ContainerStateTerminated{}, "StartedAt", "FinishedAt", "ContainerID", "Message")) + ops = append(ops, cmpopts.IgnoreFields(v1.StepState{}, "ImageID")) + if d := cmp.Diff(taskrun.Status.Steps, tc.expected, ops); d != "" { + t.Fatalf("-got, +want: %v", d) + } + }) + } +}