diff --git a/docs/pipelines.md b/docs/pipelines.md index d4b32bce308..72d9eca664a 100644 --- a/docs/pipelines.md +++ b/docs/pipelines.md @@ -15,6 +15,7 @@ weight: 3 - [Using the `from` parameter](#using-the-from-parameter) - [Using the `runAfter` parameter](#using-the-runafter-parameter) - [Using the `retries` parameter](#using-the-retries-parameter) + - [Guard `Task` execution using `When Expressions`](#guard-task-execution-using-whenexpressions) - [Guard `Task` execution using `Conditions`](#guard-task-execution-using-conditions) - [Configuring the failure timeout](#configuring-the-failure-timeout) - [Using `Results`](#using-results) @@ -316,8 +317,60 @@ tasks: name: build-push ``` +### Guard `Task` execution using `WhenExpressions` + +To run a `Task` only when certain conditions are met, it is possible to _guard_ task execution using +the `when` field. The `when` field allows you to list a series of references to `WhenExpressions`. + +There are a lot of scenarios where `WhenExpressions` can be really useful. Some of these are: +- Checking if the name of a git branch matches +- Checking if the `Result` of a previous `Task` is as expected +- Checking if a git file has changed in the previous commits +- Checking if an image exists in the registry +- Checking if the name of a CI job matches + +The components of `WhenExpressions` are `Input`, `Operator` and `Values`: + +- `Input` is the input for the `WhenExpression` which can be static inputs or variables (`Parameters` or `Results`). +- `Operator` represents an `Input`'s relationship to a set of `Values`. `Operators` we will use in `WhenExpressions` are `in` and `notin`. +- `Values` is an array of string values. The `Values` array must be non-empty. It can contain static values or variables (`Parameters` or `Results`. + +The declared `WhenExpressions` are evaluated before the `Task` is run. If all the `WhenExpressions` +evaluate to `True`, the `Task` is run. If any of the `WhenExpressions` evaluate to `False`, the `Task` is +not run and the `TaskRun` status field `ConditionSucceeded` is set to `False` with the reason set to +`WhenExpressionsEvaluatedToFalse`. + +In these examples, `create-readme-file` task will only be executed if the `path` parameter is `README.md` and `echo-file-exists` task will only be executed if the `status` result from `check-file` task is `exists`. + +```yaml +tasks: + - name: first-create-file + when: + - input: "$(params.path)" + operator: in + values: ["README.md"] + taskRef: + name: create-readme-file +--- +tasks: + - name: echo-file-exists + when: + - input: "$(tasks.check-file.results.status)" + operator: in + values: ["exists"] + taskRef: + name: echo-file-exists +``` + +For an end-to-end example, see [PipelineRun with WhenExpressions](../examples/v1beta1/pipelineruns/pipelinerun-with-when-expressions.yaml). + +When `WhenExpressions` are specified in a `Task`, [`Conditions`](#guard-task-execution-using-conditions) should not be speficied in the same `Task`. +The `Pipeline` will be rejected as invalid if both `WhenExpressions` and `Conditions` are included. + ### Guard `Task` execution using `Conditions` +**Note:** `Conditions` are deprecated, use [`WhenExpressions`](#guard-task-execution-using-whenexpressions) instead. + To run a `Task` only when certain conditions are met, it is possible to _guard_ task execution using the `conditions` field. The `conditions` field allows you to list a series of references to [`Condition`](./conditions.md) resources. The declared `Conditions` are run before the `Task` is run. diff --git a/examples/v1beta1/pipelineruns/pipelinerun-with-when-expressions.yaml b/examples/v1beta1/pipelineruns/pipelinerun-with-when-expressions.yaml new file mode 100644 index 00000000000..8b3f6a78a52 --- /dev/null +++ b/examples/v1beta1/pipelineruns/pipelinerun-with-when-expressions.yaml @@ -0,0 +1,239 @@ +# Copied from https://github.com/tektoncd/catalog/blob/v1beta1/git/git-clone.yaml :( +# This can be deleted after we add support to refer to the remote Task in a registry (Issue #1839) or +# add support for referencing task in git directly (issue #2298) +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: git-clone-from-catalog +spec: + workspaces: + - name: output + description: The git repo will be cloned onto the volume backing this workspace + params: + - name: url + description: git url to clone + type: string + - name: revision + description: git revision to checkout (branch, tag, sha, ref…) + type: string + default: master + - name: refspec + description: (optional) git refspec to fetch before checking out revision + default: "" + - name: submodules + description: defines if the resource should initialize and fetch the submodules + type: string + default: "true" + - name: depth + description: performs a shallow clone where only the most recent commit(s) will be fetched + type: string + default: "1" + - name: sslVerify + description: defines if http.sslVerify should be set to true or false in the global git config + type: string + default: "true" + - name: subdirectory + description: subdirectory inside the "output" workspace to clone the git repo into + type: string + default: "" + - name: deleteExisting + description: clean out the contents of the repo's destination directory (if it already exists) before trying to clone the repo there + type: string + default: "false" + - name: httpProxy + description: git HTTP proxy server for non-SSL requests + type: string + default: "" + - name: httpsProxy + description: git HTTPS proxy server for SSL requests + type: string + default: "" + - name: noProxy + description: git no proxy - opt out of proxying HTTP/HTTPS requests + type: string + default: "" + results: + - name: commit + description: The precise commit SHA that was fetched by this Task + steps: + - name: clone + image: gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init:v0.12.1 + script: | + CHECKOUT_DIR="$(workspaces.output.path)/$(params.subdirectory)" + + cleandir() { + # Delete any existing contents of the repo directory if it exists. + # + # We don't just "rm -rf $CHECKOUT_DIR" because $CHECKOUT_DIR might be "/" + # or the root of a mounted volume. + if [[ -d "$CHECKOUT_DIR" ]] ; then + # Delete non-hidden files and directories + rm -rf "$CHECKOUT_DIR"/* + # Delete files and directories starting with . but excluding .. + rm -rf "$CHECKOUT_DIR"/.[!.]* + # Delete files and directories starting with .. plus any other character + rm -rf "$CHECKOUT_DIR"/..?* + fi + } + + if [[ "$(params.deleteExisting)" == "true" ]] ; then + cleandir + fi + + test -z "$(params.httpProxy)" || export HTTP_PROXY=$(params.httpProxy) + test -z "$(params.httpsProxy)" || export HTTPS_PROXY=$(params.httpsProxy) + test -z "$(params.noProxy)" || export NO_PROXY=$(params.noProxy) + + /ko-app/git-init \ + -url "$(params.url)" \ + -revision "$(params.revision)" \ + -refspec "$(params.refspec)" \ + -path "$CHECKOUT_DIR" \ + -sslVerify="$(params.sslVerify)" \ + -submodules="$(params.submodules)" \ + -depth "$(params.depth)" + cd "$CHECKOUT_DIR" + RESULT_SHA="$(git rev-parse HEAD | tr -d '\n')" + EXIT_CODE="$?" + if [ "$EXIT_CODE" != 0 ] + then + exit $EXIT_CODE + fi + # Make sure we don't add a trailing newline to the result! + echo -n "$RESULT_SHA" > $(results.commit.path) +--- +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: check-file +spec: + params: + - name: path + workspaces: + - name: source + description: The workspace to check for the file. + results: + - name: status + description: indicating whether the file exists + steps: + - name: check-file + image: alpine + script: | + if test -f $(workspaces.source.path)/$(params.path); then + printf exists | tee /tekton/results/status + else + printf missing | tee /tekton/results/status + fi +--- +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: guarded-pipeline +spec: + params: + - name: path + type: string + description: The path of the file to be created. + default: "README.md" + - name: repo-url + type: string + description: The git repository URL to clone from. + - name: branch-name + type: string + description: The git branch to clone. + workspaces: + - name: source-repo + description: | + This workspace will receive the cloned git repo and be passed + to the next Task to create a file. + tasks: + - name: fetch-repo + taskRef: + name: git-clone-from-catalog + params: + - name: url + value: $(params.repo-url) + - name: revision + value: $(params.branch-name) + workspaces: + - name: output + workspace: source-repo + - name: create-file + when: + - input: "$(params.path)" + operator: in + values: ["README.md"] + workspaces: + - name: source + workspace: source-repo + runAfter: + - fetch-repo + taskSpec: + workspaces: + - name: source + description: The workspace to create the readme file in. + steps: + - name: write-new-stuff + image: ubuntu + script: 'touch $(workspaces.source.path)/README.md' + - name: check-file + when: + - input: "foo" + operator: in + values: ["foo", "bar"] + params: + - name: path + value: "$(params.path)" + workspaces: + - name: source + workspace: source-repo + taskRef: + name: check-file + runAfter: + - create-file + - name: echo-file-exists + when: + - input: "$(tasks.check-file.results.status)" + operator: in + values: ["exists"] + taskSpec: + steps: + - name: echo + image: ubuntu + script: 'echo file exists' + - name: task-should-be-skipped + when: + - input: "foo" + operator: notin + values: ["foo"] + - input: "foo" + operator: in + values: ["bar"] + taskSpec: + steps: + - name: echo + image: ubuntu + script: exit 1 +--- +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + name: guarded-pr +spec: + serviceAccountName: 'default' + pipelineRef: + name: guarded-pipeline + params: + - name: repo-url + value: https://github.com/tektoncd/pipeline.git + - name: branch-name + value: master + workspaces: + - name: source-repo + volumeClaimTemplate: + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/internal/builder/v1beta1/pipeline.go b/internal/builder/v1beta1/pipeline.go index 68a764a120c..93af5973d66 100644 --- a/internal/builder/v1beta1/pipeline.go +++ b/internal/builder/v1beta1/pipeline.go @@ -24,6 +24,7 @@ import ( resource "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/selection" "knative.dev/pkg/apis" ) @@ -54,6 +55,9 @@ type PipelineRunStatusOp func(*v1beta1.PipelineRunStatus) // PipelineTaskConditionOp is an operation which modifies a PipelineTaskCondition type PipelineTaskConditionOp func(condition *v1beta1.PipelineTaskCondition) +// PipelineTaskWhenExpressionOp is an operation which modifies a WhenExpression. +type PipelineTaskWhenExpressionOp func(*v1beta1.WhenExpression) + // Pipeline creates a Pipeline with default values. // Any number of Pipeline modifier can be passed to transform it. func Pipeline(name string, ops ...PipelineOp) *v1beta1.Pipeline { @@ -332,6 +336,17 @@ func PipelineTaskConditionResource(name, resource string, from ...string) Pipeli } } +// PipelineTaskWhenExpression adds a WhenExpression with the specified input, operator and values. +func PipelineTaskWhenExpression(input string, operator selection.Operator, values []string) PipelineTaskOp { + return func(pt *v1beta1.PipelineTask) { + pt.WhenExpressions = append(pt.WhenExpressions, v1beta1.WhenExpression{ + Input: input, + Operator: operator, + Values: values, + }) + } +} + // PipelineTaskWorkspaceBinding adds a workspace with the specified name, workspace and subpath on a PipelineTask. func PipelineTaskWorkspaceBinding(name, workspace, subPath string) PipelineTaskOp { return func(pt *v1beta1.PipelineTask) { diff --git a/internal/builder/v1beta1/pipeline_test.go b/internal/builder/v1beta1/pipeline_test.go index cc40e365b5b..3647ec36ad6 100644 --- a/internal/builder/v1beta1/pipeline_test.go +++ b/internal/builder/v1beta1/pipeline_test.go @@ -23,6 +23,7 @@ import ( "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/selection" "knative.dev/pkg/apis" duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" @@ -54,6 +55,7 @@ func TestPipeline(t *testing.T) { tb.PipelineTaskOutputResource("some-image", "my-only-image-resource"), ), tb.PipelineTask("never-gonna", "give-you-up", + tb.PipelineTaskWhenExpression("foo", selection.In, []string{"foo", "bar"}), tb.RunAfter("foo"), tb.PipelineTaskTimeout(5*time.Second), ), @@ -133,10 +135,11 @@ func TestPipeline(t *testing.T) { }}, }, }, { - Name: "never-gonna", - TaskRef: &v1beta1.TaskRef{Name: "give-you-up"}, - RunAfter: []string{"foo"}, - Timeout: &metav1.Duration{Duration: 5 * time.Second}, + Name: "never-gonna", + TaskRef: &v1beta1.TaskRef{Name: "give-you-up"}, + WhenExpressions: []v1beta1.WhenExpression{{Input: "foo", Operator: selection.In, Values: []string{"foo", "bar"}}}, + RunAfter: []string{"foo"}, + Timeout: &metav1.Duration{Duration: 5 * time.Second}, }, { Name: "foo", TaskSpec: &v1beta1.EmbeddedTask{ diff --git a/pkg/apis/pipeline/v1beta1/pipeline_types.go b/pkg/apis/pipeline/v1beta1/pipeline_types.go index 002e8b45896..0013fd7e583 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_types.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_types.go @@ -127,6 +127,10 @@ type PipelineTask struct { // +optional Conditions []PipelineTaskCondition `json:"conditions,omitempty"` + // WhenExpressions is a list of when expressions that need to be true for the task to run + // +optional + WhenExpressions WhenExpressions `json:"when,omitempty"` + // Retries represents how many times this task should be retried in case of task failure: ConditionSucceeded set to False // +optional Retries int `json:"retries,omitempty"` @@ -197,6 +201,16 @@ func (pt PipelineTask) Deps() []string { } } } + // Add any dependents from when expressions + for _, whenExpression := range pt.WhenExpressions { + expressions, ok := whenExpression.GetVarSubstitutionExpressionsForWhenExpression() + if ok { + resultRefs := NewResultRefs(expressions) + for _, resultRef := range resultRefs { + deps = append(deps, resultRef.PipelineTask) + } + } + } return deps } diff --git a/pkg/apis/pipeline/v1beta1/pipeline_validation.go b/pkg/apis/pipeline/v1beta1/pipeline_validation.go index a82544ec1cc..62391082aa9 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_validation.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_validation.go @@ -212,6 +212,10 @@ func (ps *PipelineSpec) Validate(ctx context.Context) *apis.FieldError { return err } + if err := validateWhenExpressions(ps.Tasks); err != nil { + return err + } + return nil } @@ -356,30 +360,47 @@ func validatePipelineParameterVariables(tasks []PipelineTask, params []ParamSpec } } - return validatePipelineVariables(tasks, "params", parameterNames, arrayParameterNames) + return validatePipelineParametersVariables(tasks, "params", parameterNames, arrayParameterNames) } -func validatePipelineVariables(tasks []PipelineTask, prefix string, paramNames sets.String, arrayParamNames sets.String) *apis.FieldError { +func validatePipelineParametersVariables(tasks []PipelineTask, prefix string, paramNames sets.String, arrayParamNames sets.String) *apis.FieldError { for _, task := range tasks { for _, param := range task.Params { if param.Value.Type == ParamTypeString { - if err := validatePipelineVariable(fmt.Sprintf("param[%s]", param.Name), param.Value.StringVal, prefix, paramNames); err != nil { - return err - } - if err := validatePipelineNoArrayReferenced(fmt.Sprintf("param[%s]", param.Name), param.Value.StringVal, prefix, arrayParamNames); err != nil { + if err := validatePipelineStringVariable(fmt.Sprintf("param[%s]", param.Name), param.Value.StringVal, prefix, paramNames, arrayParamNames); err != nil { return err } } else { for _, arrayElement := range param.Value.ArrayVal { - if err := validatePipelineVariable(fmt.Sprintf("param[%s]", param.Name), arrayElement, prefix, paramNames); err != nil { - return err - } - if err := validatePipelineArraysIsolated(fmt.Sprintf("param[%s]", param.Name), arrayElement, prefix, arrayParamNames); err != nil { + if err := validatePipelineArrayVariable(fmt.Sprintf("param[%s]", param.Name), arrayElement, prefix, paramNames, arrayParamNames); err != nil { return err } } } } + if err := task.WhenExpressions.validateParametersVariables(prefix, paramNames, arrayParamNames); err != nil { + return err + } + } + return nil +} + +func validatePipelineStringVariable(name, value, prefix string, stringVars sets.String, arrayVars sets.String) *apis.FieldError { + if err := validatePipelineVariable(name, value, prefix, stringVars); err != nil { + return err + } + if err := validatePipelineNoArrayReferenced(name, value, prefix, arrayVars); err != nil { + return err + } + return nil +} + +func validatePipelineArrayVariable(name, value, prefix string, stringVars sets.String, arrayVars sets.String) *apis.FieldError { + if err := validatePipelineVariable(name, value, prefix, stringVars); err != nil { + return err + } + if err := validatePipelineArraysIsolated(name, value, prefix, arrayVars); err != nil { + return err } return nil } @@ -488,6 +509,9 @@ func validateFinalTasks(finalTasks []PipelineTask) *apis.FieldError { if len(f.Conditions) != 0 { return apis.ErrInvalidValue(fmt.Sprintf("no conditions allowed under spec.finally, final task %s has conditions specified", f.Name), "spec.finally") } + if len(f.WhenExpressions) != 0 { + return apis.ErrInvalidValue(fmt.Sprintf("no when expressions allowed under spec.finally, final task %s has when expressions specified", f.Name), "spec.finally") + } } if err := validateTaskResultReferenceNotUsed(finalTasks); err != nil { @@ -531,3 +555,23 @@ func validateTasksInputFrom(tasks []PipelineTask) *apis.FieldError { } return nil } + +func validateWhenExpressions(tasks []PipelineTask) *apis.FieldError { + for i, t := range tasks { + if err := validateOneOfWhenExpressionsOrConditions(i, t); err != nil { + return err + } + if err := t.WhenExpressions.validate(); err != nil { + return err + } + } + return nil +} + +func validateOneOfWhenExpressionsOrConditions(i int, t PipelineTask) *apis.FieldError { + prefix := "spec.tasks" + if t.WhenExpressions != nil && t.Conditions != nil { + return apis.ErrMultipleOneOf(fmt.Sprintf(fmt.Sprintf(prefix+"[%d].when", i), fmt.Sprintf(prefix+"[%d].conditions", i))) + } + return nil +} diff --git a/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go b/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go index 9615a56a07a..becd842c458 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go @@ -23,6 +23,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/selection" ) func TestPipeline_Validate_Success(t *testing.T) { @@ -157,6 +158,43 @@ func TestPipelineSpec_Validate_Failure(t *testing.T) { TaskSpec: &EmbeddedTask{TaskSpec: getTaskSpec()}, }}, }, + }, { + name: "invalid pipeline with one pipeline task having both conditions and when expressions", + ps: &PipelineSpec{ + Description: "this is an invalid pipeline with invalid pipeline task", + Tasks: []PipelineTask{{ + Name: "valid-pipeline-task", + TaskRef: &TaskRef{Name: "foo-task"}, + WhenExpressions: []WhenExpression{{ + Input: "$(tasks.a-task.resultTypo.bResult)", + Operator: selection.In, + Values: []string{"bar"}, + }}, + Conditions: []PipelineTaskCondition{{ + ConditionRef: "some-condition", + Resources: []PipelineTaskInputResource{{ + Name: "some-workspace", Resource: "missing-great-resource", + }}, + }}, + }}, + }, + }, { + name: "invalid pipeline with one pipeline task having when expression with misconfigured result reference", + ps: &PipelineSpec{ + Description: "this is an invalid pipeline with invalid pipeline task", + Tasks: []PipelineTask{{ + Name: "valid-pipeline-task", + TaskRef: &TaskRef{Name: "foo-task"}, + }, { + Name: "invalid-pipeline-task", + TaskRef: &TaskRef{Name: "foo-task"}, + WhenExpressions: []WhenExpression{{ + Input: "$(tasks.a-task.resultTypo.bResult)", + Operator: selection.In, + Values: []string{"bar"}, + }}, + }}, + }, }, { name: "invalid pipeline with pipeline task having reference to resources which does not exist", ps: &PipelineSpec{ @@ -738,6 +776,26 @@ func TestValidatePipelineParameterVariables_Success(t *testing.T) { Name: "a-param", Value: ArrayOrString{StringVal: "$(baz) and $(foo-is-baz)"}, }}, }}, + }, { + name: "valid string parameter variables in when expression", + params: []ParamSpec{{ + Name: "baz", Type: ParamTypeString, + }, { + Name: "foo-is-baz", Type: ParamTypeString, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "$(params.baz)", + Operator: selection.In, + Values: []string{"foo"}, + }, { + Input: "$(params.foo-is-baz)", + Operator: selection.In, + Values: []string{"baz"}, + }}, + }}, }, { name: "valid array parameter variables", params: []ParamSpec{{ @@ -803,6 +861,56 @@ func TestValidatePipelineParameterVariables_Failure(t *testing.T) { Name: "a-param", Value: ArrayOrString{Type: ParamTypeString, StringVal: "$(params.does-not-exist)"}, }}, }}, + }, { + name: "invalid string parameter variables in when expression, missing input param from the param declarations", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "$(params.baz)", + Operator: selection.In, + Values: []string{"foo"}, + }}, + }}, + }, { + name: "invalid string parameter variables in when expression, missing values param from the param declarations", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "bax", + Operator: selection.In, + Values: []string{"$(params.foo-is-baz)"}, + }}, + }}, + }, { + name: "invalid string parameter variables in when expression, array reference in input", + params: []ParamSpec{{ + Name: "foo", Type: ParamTypeArray, Default: &ArrayOrString{Type: ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "$(params.foo)", + Operator: selection.In, + Values: []string{"foo"}, + }}, + }}, + }, { + name: "invalid string parameter variables in when expression, array reference in values", + params: []ParamSpec{{ + Name: "foo", Type: ParamTypeArray, Default: &ArrayOrString{Type: ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "bax", + Operator: selection.In, + Values: []string{"$(params.foo)"}, + }}, + }}, }, { name: "invalid pipeline task with a parameter combined with missing param from the param declarations", params: []ParamSpec{{ @@ -1361,6 +1469,17 @@ func TestValidateFinalTasks_Failure(t *testing.T) { Name: "param1", Value: ArrayOrString{Type: ParamTypeString, StringVal: "$(tasks.a-task.results.output)"}, }}, }}, + }, { + name: "invalid pipeline with final task specifying when expressions", + finalTasks: []PipelineTask{{ + Name: "final-task", + TaskRef: &TaskRef{Name: "final-task"}, + WhenExpressions: []WhenExpression{{ + Input: "foo", + Operator: selection.In, + Values: []string{"foo", "bar"}, + }}, + }}, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1479,3 +1598,133 @@ func getTaskSpec() *TaskSpec { }}, } } + +func TestWhenExpressionsValid(t *testing.T) { + tests := []struct { + name string + tasks []PipelineTask + }{{ + name: "valid operator - In - and values", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "foo", + Operator: selection.In, + Values: []string{"foo"}, + }}, + }}, + }, { + name: "valid operator - NotIn - and values", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "foo", + Operator: selection.NotIn, + Values: []string{"bar"}, + }}, + }}, + }, { + name: "valid variable", + tasks: []PipelineTask{{ + Name: "a-task", + TaskSpec: &EmbeddedTask{TaskSpec: getTaskSpec()}, + }, { + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + WhenExpressions: []WhenExpression{{ + Input: "$(tasks.a-task.results.output)", Operator: selection.In, Values: []string{"bar"}, + }}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateWhenExpressions(tt.tasks); err != nil { + t.Errorf("Pipeline.validateWhenExpressions() returned an error for valid when expressions: %s, %s", tt.name, tt.tasks[0].WhenExpressions) + } + }) + } +} + +func TestWhenExpressionsInvalid(t *testing.T) { + tests := []struct { + name string + tasks []PipelineTask + }{{ + name: "invalid operator - exists", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "foo", + Operator: selection.Exists, + Values: []string{"foo"}, + }}, + }}, + }, { + name: "invalid values - empty", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "foo", + Operator: selection.In, + Values: []string{}, + }}, + }}, + }, { + name: "missing Operator", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "foo", + Values: []string{"foo"}, + }}, + }}, + }, { + name: "missing Values", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "foo", + Operator: selection.NotIn, + }}, + }}, + }, { + name: "contains both when expressions and conditions", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + WhenExpressions: []WhenExpression{{ + Input: "foo", + Operator: selection.In, + Values: []string{"bar"}, + }}, + Conditions: []PipelineTaskCondition{{ + ConditionRef: "some-condition", + }}, + }}, + }, { + name: "invalid variable", + tasks: []PipelineTask{{ + Name: "a-task", + TaskSpec: &EmbeddedTask{TaskSpec: getTaskSpec()}, + }, { + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + WhenExpressions: []WhenExpression{{ + Input: "$(tasks.a-task.resultsTypo.output)", Operator: selection.In, Values: []string{"bar"}, + }}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateWhenExpressions(tt.tasks); err == nil { + t.Errorf("Pipeline.validateWhenExpressions() did not return error for invalid when expressions: %s, %s, %s", tt.name, tt.tasks[0].WhenExpressions, err) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1beta1/pipelinerun_types.go b/pkg/apis/pipeline/v1beta1/pipelinerun_types.go index 267f5e19491..43676e9a090 100644 --- a/pkg/apis/pipeline/v1beta1/pipelinerun_types.go +++ b/pkg/apis/pipeline/v1beta1/pipelinerun_types.go @@ -352,6 +352,9 @@ type PipelineRunTaskRunStatus struct { // ConditionChecks maps the name of a condition check to its Status // +optional ConditionChecks map[string]*PipelineRunConditionCheckStatus `json:"conditionChecks,omitempty"` + // WhenExpressionsStatus is the results for the PipelineTask's when expressions + // +optional + WhenExpressionsStatus *WhenExpressionsEvaluationStatus `json:"whenExpressionsStatus,omitempty"` } // PipelineRunConditionCheckStatus returns the condition check status diff --git a/pkg/apis/pipeline/v1beta1/resultref_test.go b/pkg/apis/pipeline/v1beta1/resultref_test.go index a01a3b1f6b4..88d53fac309 100644 --- a/pkg/apis/pipeline/v1beta1/resultref_test.go +++ b/pkg/apis/pipeline/v1beta1/resultref_test.go @@ -23,6 +23,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/test/diff" + "k8s.io/apimachinery/pkg/selection" ) func TestNewResultReference(t *testing.T) { @@ -433,3 +434,343 @@ func TestLooksLikeResultRef(t *testing.T) { }) } } + +func TestNewResultReferenceWhenExpressions(t *testing.T) { + type args struct { + whenExpression v1beta1.WhenExpression + } + tests := []struct { + name string + args args + want []*v1beta1.ResultRef + }{ + { + name: "Test valid expression", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(tasks.sumTask.results.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: []*v1beta1.ResultRef{ + { + PipelineTask: "sumTask", + Result: "sumResult", + }, + }, + }, { + name: "substitution within string", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "sum-will-go-here -> $(tasks.sumTask.results.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: []*v1beta1.ResultRef{ + { + PipelineTask: "sumTask", + Result: "sumResult", + }, + }, + }, { + name: "multiple substitution", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(tasks.sumTask1.results.sumResult) and another $(tasks.sumTask2.results.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: []*v1beta1.ResultRef{ + { + PipelineTask: "sumTask1", + Result: "sumResult", + }, { + PipelineTask: "sumTask2", + Result: "sumResult", + }, + }, + }, { + name: "multiple substitution with param", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(params.param) $(tasks.sumTask1.results.sumResult) and another $(tasks.sumTask2.results.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: []*v1beta1.ResultRef{ + { + PipelineTask: "sumTask1", + Result: "sumResult", + }, { + PipelineTask: "sumTask2", + Result: "sumResult", + }, + }, + }, { + name: "first separator typo", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(task.sumTasks.results.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: nil, + }, { + name: "third separator typo", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(tasks.sumTasks.result.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: nil, + }, { + name: "param substitution shouldn't be considered result ref", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(params.paramName)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: nil, + }, { + name: "One bad and good result substitution", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "good -> $(tasks.sumTask1.results.sumResult) bad-> $(task.sumTask2.results.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: []*v1beta1.ResultRef{ + { + PipelineTask: "sumTask1", + Result: "sumResult", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expressions, ok := tt.args.whenExpression.GetVarSubstitutionExpressionsForWhenExpression() + if !ok && tt.want != nil { + t.Fatalf("expected to find expressions but didn't find any") + } else { + got := v1beta1.NewResultRefs(expressions) + if d := cmp.Diff(tt.want, got); d != "" { + t.Errorf("TestNewResultReference/%s %s", tt.name, diff.PrintWantGot(d)) + } + } + }) + } +} + +func TestHasResultReferenceWhenExpression(t *testing.T) { + type args struct { + whenExpression v1beta1.WhenExpression + } + tests := []struct { + name string + args args + wantRef []*v1beta1.ResultRef + }{ + { + name: "Test valid expression", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "sumResult", + Operator: selection.In, + Values: []string{"$(tasks.sumTask.results.sumResult)"}, + }, + }, + wantRef: []*v1beta1.ResultRef{ + { + PipelineTask: "sumTask", + Result: "sumResult", + }, + }, + }, { + name: "Test valid expression with dashes", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(tasks.sum-task.results.sum-result)", + Operator: selection.In, + Values: []string{"sum-result"}, + }, + }, + wantRef: []*v1beta1.ResultRef{ + { + PipelineTask: "sum-task", + Result: "sum-result", + }, + }, + }, { + name: "Test valid expression with underscores", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(tasks.sum-task.results.sum_result)", + Operator: selection.In, + Values: []string{"sum-result"}, + }, + }, + wantRef: []*v1beta1.ResultRef{ + { + PipelineTask: "sum-task", + Result: "sum_result", + }, + }, + }, { + name: "Test invalid expression: param substitution shouldn't be considered result ref", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(params.paramName)", + Operator: selection.In, + Values: []string{"sum-result"}, + }, + }, + wantRef: nil, + }, { + name: "Test valid expression in array", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$sumResult", + Operator: selection.In, + Values: []string{"$(tasks.sumTask.results.sumResult)", "$(tasks.sumTask2.results.sumResult2)"}, + }, + }, + wantRef: []*v1beta1.ResultRef{ + { + PipelineTask: "sumTask", + Result: "sumResult", + }, + { + PipelineTask: "sumTask2", + Result: "sumResult2", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expressions, ok := tt.args.whenExpression.GetVarSubstitutionExpressionsForWhenExpression() + if !ok { + t.Fatalf("expected to find expressions but didn't find any") + } + got := v1beta1.NewResultRefs(expressions) + sort.Slice(got, func(i, j int) bool { + if got[i].PipelineTask > got[j].PipelineTask { + return false + } + if got[i].Result > got[j].Result { + return false + } + return true + }) + if d := cmp.Diff(tt.wantRef, got); d != "" { + t.Errorf("TestHasResultReference/%s %s", tt.name, diff.PrintWantGot(d)) + } + }) + } +} + +func TestLooksLikeResultRefWhenExpression(t *testing.T) { + type args struct { + whenExpression v1beta1.WhenExpression + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "test expression that is a result ref", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(tasks.sumTasks.results.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: true, + }, { + name: "test expression: looks like result ref, but typo in 'task' separator", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(task.sumTasks.results.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: true, + }, { + name: "test expression: looks like result ref, but typo in 'results' separator", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(tasks.sumTasks.result.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: true, + }, { + name: "test expression: missing 'task' separator", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(sumTasks.results.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: false, + }, { + name: "test expression: missing variable substitution", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "tasks.sumTasks.results.sumResult", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: false, + }, { + name: "test expression: param substitution shouldn't be considered result ref", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(params.someParam)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: false, + }, { + name: "test expression: one good ref, one bad one should return true", + args: args{ + whenExpression: v1beta1.WhenExpression{ + Input: "$(tasks.sumTasks.results.sumResult) $(task.sumTasks.results.sumResult)", + Operator: selection.In, + Values: []string{"foo"}, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expressions, ok := tt.args.whenExpression.GetVarSubstitutionExpressionsForWhenExpression() + if ok { + if got := v1beta1.LooksLikeContainsResultRefs(expressions); got != tt.want { + t.Errorf("LooksLikeContainsResultRefs() = %v, want %v", got, tt.want) + } + } else if tt.want { + t.Errorf("LooksLikeContainsResultRefs() = %v, want %v", false, tt.want) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1beta1/when_types.go b/pkg/apis/pipeline/v1beta1/when_types.go new file mode 100644 index 00000000000..e49c5a0861a --- /dev/null +++ b/pkg/apis/pipeline/v1beta1/when_types.go @@ -0,0 +1,139 @@ +/* +Copyright 2020 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 v1beta1 + +import ( + "k8s.io/apimachinery/pkg/selection" +) + +// WhenExpression allows a PipelineTask to declare expressions to be evaluated before the Task is run +// to determine whether the Task should be executed or skipped +type WhenExpression struct { + // Input is the string for guard checking which can be a static input or an output from a parent Task + Input string `json:"input,omitempty"` + // Operator that represents an Input's relationship to the values + Operator selection.Operator `json:"operator,omitempty"` + // Values is an array of strings, which is compared against the input, for guard checking + // It must be non-empty + Values []string `json:"values,omitempty"` +} + +// GetVarSubstitutionExpressionsForWhenExpression extracts all the values between "$(" and ")"" for a when expression +func (we *WhenExpression) GetVarSubstitutionExpressionsForWhenExpression() ([]string, bool) { + var allExpressions []string + allExpressions = append(allExpressions, validateString(we.Input)...) + for _, value := range we.Values { + allExpressions = append(allExpressions, validateString(value)...) + } + return allExpressions, len(allExpressions) != 0 +} + +func (we *WhenExpression) valuesContainsInput() bool { + for i := range we.Values { + if we.Values[i] == we.Input { + return true + } + } + return false +} + +func (we *WhenExpression) isTrue() bool { + if we.Operator == selection.In { + return we.valuesContainsInput() + } + return !we.valuesContainsInput() +} + +func (we *WhenExpression) hasVariable() bool { + if _, hasVariable := we.GetVarSubstitutionExpressionsForWhenExpression(); hasVariable { + return true + } + return false +} + +func (we *WhenExpression) applyReplacements(replacements map[string]string) WhenExpression { + replacedInput := ApplyReplacements(we.Input, replacements) + + var replacedValues []string + for _, val := range we.Values { + replacedValues = append(replacedValues, ApplyReplacements(val, replacements)) + } + + return WhenExpression{Input: replacedInput, Operator: we.Operator, Values: replacedValues} +} + +// WhenExpressions is an array of When Expressions which are used to specify whether a Task should be executed or skipped. +type WhenExpressions []WhenExpression + +// AreTrue indicates whether the When Expression are true. +func (wes WhenExpressions) AreTrue() bool { + for _, we := range wes { + if isTrue := we.isTrue(); !isTrue { + return false + } + } + return true +} + +// HaveVariables indicates whether When Expressions have variables in the Inputs or Values. +func (wes WhenExpressions) HaveVariables() bool { + for _, we := range wes { + if hasVariable := we.hasVariable(); !hasVariable { + return false + } + } + return true +} + +// ReplaceWhenExpressionsVariables interpolates variables in When Expressions. +func (wes WhenExpressions) ReplaceWhenExpressionsVariables(replacements map[string]string) WhenExpressions { + var replaced []WhenExpression + for _, we := range wes { + replaced = append(replaced, we.applyReplacements(replacements)) + } + return replaced +} + +// WhenExpressionEvaluationResult is the result from evaluating a When Expression. +type WhenExpressionEvaluationResult struct { + // Expression is the expression, made up of Input, Operator and Values. + Expression *WhenExpression `json:"expression,omitempty"` + // IsTrue is a boolean from evaluating the Expression. + IsTrue bool `json:"result,omitempty"` +} + +// WhenExpressionsEvaluationStatus are the results from evaluating When Expressions. +type WhenExpressionsEvaluationStatus struct { + // EvaluationResults are the results from evaluating When Expressions. + EvaluationResults []*WhenExpressionEvaluationResult `json:"evaluationResults,omitempty"` + // Executed is a boolean that represents an AND of all results from evaluating When Expressions. + Executed bool `json:"execute,omitempty"` +} + +// GetWhenExpressionsStatus evaluates When Expressions and produces Evaluation Results. +func (wes WhenExpressions) GetWhenExpressionsStatus() WhenExpressionsEvaluationStatus { + var evaluationResults []*WhenExpressionEvaluationResult + + executed := true + for i := range wes { + isTrue := wes[i].isTrue() + evaluationResults = append(evaluationResults, &WhenExpressionEvaluationResult{Expression: &wes[i], IsTrue: isTrue}) + executed = executed && isTrue + } + + return WhenExpressionsEvaluationStatus{EvaluationResults: evaluationResults, Executed: executed} +} diff --git a/pkg/apis/pipeline/v1beta1/when_types_test.go b/pkg/apis/pipeline/v1beta1/when_types_test.go new file mode 100644 index 00000000000..e0186684b64 --- /dev/null +++ b/pkg/apis/pipeline/v1beta1/when_types_test.go @@ -0,0 +1,323 @@ +/* +Copyright 2020 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 v1beta1 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/test/diff" + "k8s.io/apimachinery/pkg/selection" +) + +func TestAreTrue(t *testing.T) { + tests := []struct { + name string + whenExpressions WhenExpressions + expected bool + }{{ + name: "in expression", + whenExpressions: WhenExpressions{ + { + Input: "foo", + Operator: selection.In, + Values: []string{"foo", "bar"}, + }, + }, + expected: true, + }, { + name: "notin expression", + whenExpressions: WhenExpressions{ + { + Input: "foobar", + Operator: selection.NotIn, + Values: []string{"foobar"}, + }, + }, + expected: false, + }, { + name: "multiple expressions", + whenExpressions: WhenExpressions{ + { + Input: "foobar", + Operator: selection.In, + Values: []string{"foobar"}, + }, { + Input: "foo", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + expected: false, + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := tc.whenExpressions.AreTrue() + if d := cmp.Diff(tc.expected, got); d != "" { + t.Errorf("Error evaluating AreTrue() for When Expressions in test case %s: %s", tc.name, diff.PrintWantGot(d)) + } + }) + } +} + +func TestHaveVariables(t *testing.T) { + tests := []struct { + name string + whenExpressions WhenExpressions + expected bool + }{{ + name: "doesn't have variable", + whenExpressions: WhenExpressions{ + { + Input: "foo", + Operator: selection.In, + Values: []string{"foo", "bar"}, + }, + }, + expected: false, + }, { + name: "have variable - input", + whenExpressions: WhenExpressions{ + { + Input: "$(params.foobar)", + Operator: selection.NotIn, + Values: []string{"foobar"}, + }, + }, + expected: true, + }, { + name: "have variable - values", + whenExpressions: WhenExpressions{ + { + Input: "foobar", + Operator: selection.In, + Values: []string{"$(params.foobar)"}, + }, + }, + expected: true, + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := tc.whenExpressions.HaveVariables() + if d := cmp.Diff(tc.expected, got); d != "" { + t.Errorf("Error evaluating HaveVariables() for When Expressions in test case %s: %s", tc.name, diff.PrintWantGot(d)) + } + }) + } +} + +func TestGetWhenExpressionsStatus(t *testing.T) { + tests := []struct { + name string + whenExpressions WhenExpressions + expected WhenExpressionsEvaluationStatus + }{{ + name: "in expression", + whenExpressions: WhenExpressions{ + { + Input: "foo", + Operator: selection.In, + Values: []string{"foo", "bar"}, + }, + }, + expected: WhenExpressionsEvaluationStatus{ + EvaluationResults: []*WhenExpressionEvaluationResult{ + { + Expression: &WhenExpression{ + Input: "foo", + Operator: selection.In, + Values: []string{"foo", "bar"}, + }, + IsTrue: true, + }, + }, + Executed: true, + }, + }, { + name: "notin expression", + whenExpressions: WhenExpressions{ + { + Input: "foobar", + Operator: selection.NotIn, + Values: []string{"foobar"}, + }, + }, + expected: WhenExpressionsEvaluationStatus{ + EvaluationResults: []*WhenExpressionEvaluationResult{ + { + Expression: &WhenExpression{ + Input: "foobar", + Operator: selection.NotIn, + Values: []string{"foobar"}, + }, + IsTrue: false, + }, + }, + Executed: false, + }, + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := tc.whenExpressions.GetWhenExpressionsStatus() + if d := cmp.Diff(tc.expected, got); d != "" { + t.Errorf("Error evaluating When Expressions in test case %s: %s", tc.name, diff.PrintWantGot(d)) + } + }) + } +} + +func TestReplaceWhenExpressionsVariables(t *testing.T) { + tests := []struct { + name string + whenExpressions WhenExpressions + replacements map[string]string + expected WhenExpressions + }{{ + name: "params replacement in input", + whenExpressions: WhenExpressions{ + { + Input: "$(params.foo)", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + replacements: map[string]string{ + "params.foo": "bar", + }, + expected: WhenExpressions{ + { + Input: "bar", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + }, { + name: "params replacement in values", + whenExpressions: WhenExpressions{ + { + Input: "bar", + Operator: selection.In, + Values: []string{"$(params.foo)"}, + }, + }, + replacements: map[string]string{ + "params.foo": "bar", + }, + expected: WhenExpressions{ + { + Input: "bar", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + }, { + name: "results replacement in input", + whenExpressions: WhenExpressions{ + { + Input: "$(tasks.aTask.results.foo)", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + replacements: map[string]string{ + "tasks.aTask.results.foo": "bar", + }, + expected: WhenExpressions{ + { + Input: "bar", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + }, { + name: "results replacement in values", + whenExpressions: WhenExpressions{ + { + Input: "bar", + Operator: selection.In, + Values: []string{"$(tasks.aTask.results.foo)"}, + }, + }, + replacements: map[string]string{ + "tasks.aTask.results.foo": "bar", + }, + expected: WhenExpressions{ + { + Input: "bar", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := tc.whenExpressions.ReplaceWhenExpressionsVariables(tc.replacements) + if d := cmp.Diff(tc.expected, got); d != "" { + t.Errorf("Error evaluating When Expressions in test case %s: %s", tc.name, diff.PrintWantGot(d)) + } + }) + } +} + +func TestApplyReplacements(t *testing.T) { + tests := []struct { + name string + original *WhenExpression + replacements map[string]string + expected *WhenExpression + }{{ + name: "replace parameters variables", + original: &WhenExpression{ + Input: "$(params.path)", + Operator: selection.In, + Values: []string{"$(params.branch)"}, + }, + replacements: map[string]string{ + "params.path": "readme.md", + "params.branch": "staging", + }, + expected: &WhenExpression{ + Input: "readme.md", + Operator: selection.In, + Values: []string{"staging"}, + }, + }, { + name: "replace results variables", + original: &WhenExpression{ + Input: "$(tasks.foo.results.bar)", + Operator: selection.In, + Values: []string{"$(tasks.aTask.results.aResult)"}, + }, + replacements: map[string]string{ + "tasks.foo.results.bar": "foobar", + "tasks.aTask.results.aResult": "barfoo", + }, + expected: &WhenExpression{ + Input: "foobar", + Operator: selection.In, + Values: []string{"barfoo"}, + }, + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := tc.original.applyReplacements(tc.replacements) + if d := cmp.Diff(tc.expected, &got); d != "" { + t.Errorf("Error applying replacements for When Expressions: %s", diff.PrintWantGot(d)) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1beta1/when_validation.go b/pkg/apis/pipeline/v1beta1/when_validation.go new file mode 100644 index 00000000000..1b9a9c4fa2d --- /dev/null +++ b/pkg/apis/pipeline/v1beta1/when_validation.go @@ -0,0 +1,110 @@ +/* +Copyright 2020 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 v1beta1 + +import ( + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/util/sets" + "knative.dev/pkg/apis" +) + +var validWhenOperators = []string{ + string(selection.In), + string(selection.NotIn), +} + +func (wes WhenExpressions) validate() *apis.FieldError { + if err := wes.validateWhenExpressionsFields(); err != nil { + return err + } + if err := wes.validateTaskResultsVariables(); err != nil { + return err + } + return nil +} + +func (wes WhenExpressions) validateWhenExpressionsFields() *apis.FieldError { + for _, we := range wes { + if err := we.validateWhenExpressionFields(); err != nil { + return err + } + } + return nil +} + +func (we *WhenExpression) validateWhenExpressionFields() *apis.FieldError { + if equality.Semantic.DeepEqual(we, &WhenExpression{}) || we == nil { + return apis.ErrMissingField(apis.CurrentField) + } + if err := validateWhenExpressionOperator(&we.Operator); err != nil { + return err + } + if err := validateWhenExpressionValues(&we.Values); err != nil { + return err + } + return nil +} + +func validateWhenExpressionOperator(operator *selection.Operator) *apis.FieldError { + if !sets.NewString(validWhenOperators...).Has(string(*operator)) { + message := fmt.Sprintf("operator %q is not recognized. valid operators: %s", *operator, strings.Join(validWhenOperators, ",")) + return apis.ErrInvalidValue(message, "spec.task.when") + } + return nil +} + +func validateWhenExpressionValues(values *[]string) *apis.FieldError { + if len(*values) == 0 { + return apis.ErrInvalidValue("expecting non-empty values field", "spec.task.when") + } + return nil +} + +func (wes WhenExpressions) validateTaskResultsVariables() *apis.FieldError { + for _, we := range wes { + expressions, ok := we.GetVarSubstitutionExpressionsForWhenExpression() + if ok { + if LooksLikeContainsResultRefs(expressions) { + expressions = filter(expressions, looksLikeResultRef) + resultRefs := NewResultRefs(expressions) + if len(expressions) != len(resultRefs) { + message := fmt.Sprintf("expected all of the expressions %v to be result expressions but only %v were", expressions, resultRefs) + return apis.ErrInvalidValue(message, "spec.tasks.when") + } + } + } + } + return nil +} + +func (wes WhenExpressions) validateParametersVariables(prefix string, paramNames sets.String, arrayParamNames sets.String) *apis.FieldError { + for _, whenExpression := range wes { + if err := validatePipelineStringVariable(fmt.Sprintf("whenInput[%s]", whenExpression.Input), whenExpression.Input, prefix, paramNames, arrayParamNames); err != nil { + return err + } + for _, whenExpressionValue := range whenExpression.Values { + if err := validatePipelineStringVariable(fmt.Sprintf("whenValue[%s]", whenExpressionValue), whenExpressionValue, prefix, paramNames, arrayParamNames); err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/apis/pipeline/v1beta1/when_validation_test.go b/pkg/apis/pipeline/v1beta1/when_validation_test.go new file mode 100644 index 00000000000..6c3c1fa8325 --- /dev/null +++ b/pkg/apis/pipeline/v1beta1/when_validation_test.go @@ -0,0 +1,107 @@ +/* +Copyright 2020 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 v1beta1 + +import ( + "testing" + + "k8s.io/apimachinery/pkg/selection" +) + +func TestWhenExpressions_Valid(t *testing.T) { + tests := []struct { + name string + wes WhenExpressions + }{{ + name: "valid operator - In - and values", + wes: []WhenExpression{{ + Input: "foo", + Operator: selection.In, + Values: []string{"foo"}, + }}, + }, { + name: "valid operator - NotIn - and values", + wes: []WhenExpression{{ + Input: "foo", + Operator: selection.NotIn, + Values: []string{"bar"}, + }}, + }, { + wes: []WhenExpression{{ + Input: "$(tasks.a-task.results.output)", + Operator: selection.In, + Values: []string{"bar"}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.wes.validate(); err != nil { + t.Errorf("WhenExpressions.validate() returned an error for valid when expressions: %s, %s", tt.name, tt.wes) + } + }) + } +} + +func TestWhenExpressions_Invalid(t *testing.T) { + tests := []struct { + name string + wes WhenExpressions + }{{ + name: "invalid operator - exists", + wes: []WhenExpression{{ + Input: "foo", + Operator: selection.Exists, + Values: []string{"foo"}, + }}, + }, { + name: "invalid values - empty", + wes: []WhenExpression{{ + Input: "foo", + Operator: selection.In, + Values: []string{}, + }}, + }, { + name: "missing Operator", + wes: []WhenExpression{{ + Input: "foo", + Values: []string{"foo"}, + }}, + }, { + name: "missing Values", + wes: []WhenExpression{{ + Input: "foo", + Operator: selection.NotIn, + }}, + }, { + name: "invalid result", + wes: []WhenExpression{{ + Input: "$(tasks.a-task.resultsTypo.output)", + Operator: selection.In, + Values: []string{"bar"}, + }}, + }, { + name: "missing when expression", + wes: []WhenExpression{{}}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.wes.validate(); err == nil { + t.Errorf("WhenExpressions.validate() did not return error for invalid when expressions: %s, %s, %s", tt.name, tt.wes, err) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go index 37733e74976..f645c9ccd4d 100644 --- a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go @@ -762,6 +762,11 @@ func (in *PipelineRunTaskRunStatus) DeepCopyInto(out *PipelineRunTaskRunStatus) (*out)[key] = outVal } } + if in.WhenExpressionsStatus != nil { + in, out := &in.WhenExpressionsStatus, &out.WhenExpressionsStatus + *out = new(WhenExpressionsEvaluationStatus) + (*in).DeepCopyInto(*out) + } return } @@ -847,6 +852,13 @@ func (in *PipelineTask) DeepCopyInto(out *PipelineTask) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.WhenExpressions != nil { + in, out := &in.WhenExpressions, &out.WhenExpressions + *out = make(WhenExpressions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.RunAfter != nil { in, out := &in.RunAfter, &out.RunAfter *out = make([]string, len(*in)) @@ -1708,6 +1720,97 @@ func (in *TaskSpec) DeepCopy() *TaskSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhenExpression) DeepCopyInto(out *WhenExpression) { + *out = *in + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhenExpression. +func (in *WhenExpression) DeepCopy() *WhenExpression { + if in == nil { + return nil + } + out := new(WhenExpression) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhenExpressionEvaluationResult) DeepCopyInto(out *WhenExpressionEvaluationResult) { + *out = *in + if in.Expression != nil { + in, out := &in.Expression, &out.Expression + *out = new(WhenExpression) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhenExpressionEvaluationResult. +func (in *WhenExpressionEvaluationResult) DeepCopy() *WhenExpressionEvaluationResult { + if in == nil { + return nil + } + out := new(WhenExpressionEvaluationResult) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in WhenExpressions) DeepCopyInto(out *WhenExpressions) { + { + in := &in + *out = make(WhenExpressions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhenExpressions. +func (in WhenExpressions) DeepCopy() WhenExpressions { + if in == nil { + return nil + } + out := new(WhenExpressions) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhenExpressionsEvaluationStatus) DeepCopyInto(out *WhenExpressionsEvaluationStatus) { + *out = *in + if in.EvaluationResults != nil { + in, out := &in.EvaluationResults, &out.EvaluationResults + *out = make([]*WhenExpressionEvaluationResult, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(WhenExpressionEvaluationResult) + (*in).DeepCopyInto(*out) + } + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhenExpressionsEvaluationStatus. +func (in *WhenExpressionsEvaluationStatus) DeepCopy() *WhenExpressionsEvaluationStatus { + if in == nil { + return nil + } + out := new(WhenExpressionsEvaluationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkspaceBinding) DeepCopyInto(out *WorkspaceBinding) { *out = *in diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index 47d14c68273..0946d5083a0 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -567,7 +567,7 @@ func getPipelineRunResults(pipelineSpec *v1beta1.PipelineSpec, resolvedResultRef func getTaskRunsStatus(pr *v1beta1.PipelineRun, state []*resources.ResolvedPipelineRunTask) map[string]*v1beta1.PipelineRunTaskRunStatus { status := make(map[string]*v1beta1.PipelineRunTaskRunStatus) for _, rprt := range state { - if rprt.TaskRun == nil && rprt.ResolvedConditionChecks == nil { + if rprt.TaskRun == nil && rprt.ResolvedConditionChecks == nil && rprt.PipelineTask.WhenExpressions == nil { continue } @@ -585,6 +585,22 @@ func getTaskRunsStatus(pr *v1beta1.PipelineRun, state []*resources.ResolvedPipel prtrs.Status = &rprt.TaskRun.Status } + if len(rprt.PipelineTask.WhenExpressions) > 0 { + if prtrs.Status == nil { + prtrs.Status = &v1beta1.TaskRunStatus{} + } + status := rprt.PipelineTask.WhenExpressions.GetWhenExpressionsStatus() + prtrs.WhenExpressionsStatus = &status + if !status.Executed { + prtrs.Status.SetCondition(&apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + Reason: resources.ReasonWhenExpressionsEvaluatedToFalse, + Message: fmt.Sprintf("When Expressions for Task %s in PipelineRun %s evaluated to False", rprt.TaskRunName, pr.Name), + }) + } + } + if len(rprt.ResolvedConditionChecks) > 0 { cStatus := make(map[string]*v1beta1.PipelineRunConditionCheckStatus) for _, c := range rprt.ResolvedConditionChecks { diff --git a/pkg/reconciler/pipelinerun/pipelinerun_test.go b/pkg/reconciler/pipelinerun/pipelinerun_test.go index 4f9246cbd4d..0d1fb26cd69 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/pipelinerun/pipelinerun_test.go @@ -47,6 +47,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/types" ktesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/record" @@ -835,6 +836,72 @@ func TestUpdateTaskRunsState(t *testing.T) { } +func TestUpdateTaskRunsWithWhenExpressions(t *testing.T) { + // TestUpdateTaskRunsWithWhenExpressions runs "getTaskRunsStatus" and verifies how it updates a PipelineRun status + // from a TaskRun with When Expressionsassociated to the PipelineRun + pr := tb.PipelineRun("test-pipeline-run", tb.PipelineRunNamespace("foo"), tb.PipelineRunSpec("test-pipeline")) + pipelineTask := v1beta1.PipelineTask{ + Name: "unit-test-1", + TaskRef: &v1beta1.TaskRef{Name: "unit-test-task"}, + WhenExpressions: []v1beta1.WhenExpression{{ + Input: "foo", + Operator: selection.NotIn, + Values: []string{"foo", "bar"}, + }}, + } + task := tb.Task("unit-test-task", tb.TaskSpec( + tb.TaskResources(tb.TaskResourcesInput("workspace", resourcev1alpha1.PipelineResourceTypeGit)), + ), tb.TaskNamespace("foo")) + + expectedWhenExpressionsResults := &v1beta1.WhenExpressionsEvaluationStatus{ + EvaluationResults: []*v1beta1.WhenExpressionEvaluationResult{{ + Expression: &v1beta1.WhenExpression{ + Input: "foo", + Operator: selection.NotIn, + Values: []string{"foo", "bar"}, + }, + IsTrue: false, + }}, + Executed: false, + } + + expectedTaskRunsStatus := make(map[string]*v1beta1.PipelineRunTaskRunStatus) + expectedTaskRunsStatus["test-pipeline-run-success-unit-test-1"] = &v1beta1.PipelineRunTaskRunStatus{ + PipelineTaskName: "unit-test-1", + Status: &v1beta1.TaskRunStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: "False", + Reason: "WhenExpressionsEvaluatedToFalse", + Message: "When Expressions for Task test-pipeline-run-success-unit-test-1 in PipelineRun test-pipeline-run evaluated to False", + }}, + }, + }, + WhenExpressionsStatus: expectedWhenExpressionsResults, + } + + expectedPipelineRunStatus := v1beta1.PipelineRunStatus{ + PipelineRunStatusFields: v1beta1.PipelineRunStatusFields{ + TaskRuns: expectedTaskRunsStatus, + }, + } + + pipelineRunState := resources.PipelineRunState{{ + PipelineTask: &pipelineTask, + TaskRunName: "test-pipeline-run-success-unit-test-1", + TaskRun: nil, + ResolvedTaskResources: &taskrunresources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + }} + pr.Status.InitializeConditions() + status := getTaskRunsStatus(pr, pipelineRunState) + if d := cmp.Diff(expectedPipelineRunStatus.TaskRuns, status, ignoreLastTransitionTime); d != "" { + t.Fatalf("Expected PipelineRun status to match TaskRun(s) status, but got a mismatch: %s", diff.PrintWantGot(d)) + } +} + func TestUpdateTaskRunStateWithConditionChecks(t *testing.T) { // TestUpdateTaskRunsState runs "getTaskRunsStatus" and verifies how it updates a PipelineRun status // from several different TaskRun with Conditions associated to the PipelineRun diff --git a/pkg/reconciler/pipelinerun/resources/apply.go b/pkg/reconciler/pipelinerun/resources/apply.go index dd12c092151..f6821b767d4 100644 --- a/pkg/reconciler/pipelinerun/resources/apply.go +++ b/pkg/reconciler/pipelinerun/resources/apply.go @@ -65,7 +65,7 @@ func ApplyContexts(spec *v1beta1.PipelineSpec, pipelineName string, pr *v1beta1. return ApplyReplacements(spec, replacements, map[string][]string{}) } -// ApplyTaskResults applies the ResolvedResultRef to each PipelineTask.Params in targets +// ApplyTaskResults applies the ResolvedResultRef to each PipelineTask.Params and Pipeline.WhenExpressions in targets func ApplyTaskResults(targets PipelineRunState, resolvedResultRefs ResolvedResultRefs) { stringReplacements := map[string]string{} @@ -84,6 +84,7 @@ func ApplyTaskResults(targets PipelineRunState, resolvedResultRefs ResolvedResul if resolvedPipelineRunTask.PipelineTask != nil { pipelineTask := resolvedPipelineRunTask.PipelineTask.DeepCopy() pipelineTask.Params = replaceParamValues(pipelineTask.Params, stringReplacements, nil) + pipelineTask.WhenExpressions = pipelineTask.WhenExpressions.ReplaceWhenExpressionsVariables(stringReplacements) resolvedPipelineRunTask.PipelineTask = pipelineTask } } @@ -102,6 +103,7 @@ func ApplyReplacements(p *v1beta1.PipelineSpec, replacements map[string]string, c := tasks[i].Conditions[j] c.Params = replaceParamValues(c.Params, replacements, arrayReplacements) } + tasks[i].WhenExpressions = tasks[i].WhenExpressions.ReplaceWhenExpressionsVariables(replacements) } return p diff --git a/pkg/reconciler/pipelinerun/resources/apply_test.go b/pkg/reconciler/pipelinerun/resources/apply_test.go index 3be1f59fa05..eb1a6a36226 100644 --- a/pkg/reconciler/pipelinerun/resources/apply_test.go +++ b/pkg/reconciler/pipelinerun/resources/apply_test.go @@ -22,6 +22,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/selection" tb "github.com/tektoncd/pipeline/internal/builder/v1beta1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" @@ -59,6 +60,25 @@ func TestApplyParameters(t *testing.T) { tb.PipelineTaskParam("first-task-second-param", "second-value"), tb.PipelineTaskParam("first-task-third-param", "static value"), ))), + }, { + name: "single parameter with when expression", + original: tb.Pipeline("test-pipeline", + tb.PipelineSpec( + tb.PipelineParamSpec("first-param", v1beta1.ParamTypeString, tb.ParamSpecDefault("default-value")), + tb.PipelineParamSpec("second-param", v1beta1.ParamTypeString), + tb.PipelineTask("first-task-1", "first-task", + tb.PipelineTaskWhenExpression("$(params.first-param)", selection.In, []string{"$(params.second-param)"}), + ))), + run: tb.PipelineRun("test-pipeline-run", + tb.PipelineRunSpec("test-pipeline", + tb.PipelineRunParam("second-param", "second-value"))), + expected: tb.Pipeline("test-pipeline", + tb.PipelineSpec( + tb.PipelineParamSpec("first-param", v1beta1.ParamTypeString, tb.ParamSpecDefault("default-value")), + tb.PipelineParamSpec("second-param", v1beta1.ParamTypeString), + tb.PipelineTask("first-task-1", "first-task", + tb.PipelineTaskWhenExpression("default-value", selection.In, []string{"second-value"}), + ))), }, { name: "pipeline parameter nested inside task parameter", original: tb.Pipeline("test-pipeline", @@ -177,7 +197,7 @@ func TestApplyTaskResults_MinimalExpression(t *testing.T) { want PipelineRunState }{ { - name: "Test result substitution on minimal variable substitution expression", + name: "Test result substitution on minimal variable substitution expression - params", args: args{ resolvedResultRefs: ResolvedResultRefs{ { @@ -227,6 +247,53 @@ func TestApplyTaskResults_MinimalExpression(t *testing.T) { }, }, }, + }, { + name: "Test result substitution on minimal variable substitution expression - when expressions", + args: args{ + resolvedResultRefs: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, + }, + targets: PipelineRunState{ + { + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + WhenExpressions: []v1beta1.WhenExpression{ + { + Input: "$(tasks.aTask.results.aResult)", + Operator: selection.In, + Values: []string{"$(tasks.aTask.results.aResult)"}, + }, + }, + }, + }, + }, + }, + want: PipelineRunState{ + { + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + WhenExpressions: []v1beta1.WhenExpression{ + { + Input: "aResultValue", + Operator: selection.In, + Values: []string{"aResultValue"}, + }, + }, + }, + }, + }, }, } for _, tt := range tests { @@ -250,7 +317,7 @@ func TestApplyTaskResults_EmbeddedExpression(t *testing.T) { want PipelineRunState }{ { - name: "Test result substitution on embedded variable substitution expression", + name: "Test result substitution on embedded variable substitution expression - params", args: args{ resolvedResultRefs: ResolvedResultRefs{ { @@ -300,6 +367,53 @@ func TestApplyTaskResults_EmbeddedExpression(t *testing.T) { }, }, }, + }, { + name: "Test result substitution on embedded variable substitution expression - when expressions", + args: args{ + resolvedResultRefs: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, + }, + targets: PipelineRunState{ + { + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + WhenExpressions: []v1beta1.WhenExpression{ + { + Input: "Result value --> $(tasks.aTask.results.aResult)", + Operator: selection.In, + Values: []string{"Result value --> $(tasks.aTask.results.aResult)"}, + }, + }, + }, + }, + }, + }, + want: PipelineRunState{ + { + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + WhenExpressions: []v1beta1.WhenExpression{ + { + Input: "Result value --> aResultValue", + Operator: selection.In, + Values: []string{"Result value --> aResultValue"}, + }, + }, + }, + }, + }, }, } for _, tt := range tests { diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go index f528d05103e..99ec3022898 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go @@ -41,6 +41,8 @@ const ( // ReasonConditionCheckFailed indicates that the reason for the failure status is that the // condition check associated to the pipeline task evaluated to false ReasonConditionCheckFailed = "ConditionCheckFailed" + + ReasonWhenExpressionsEvaluatedToFalse = "WhenExpressionsEvaluatedToFalse" ) // TaskNotFoundError indicates that the resolution failed because a referenced Task couldn't be retrieved @@ -141,12 +143,12 @@ func (t ResolvedPipelineRunTask) IsStarted() bool { return true } -// IsSkipped returns true if a PipelineTask will not be run because -// (1) its Condition Checks failed or +// Skip returns true if a PipelineTask will not be run because +// (1) its Condition Checks failed or its When Expressions evaluated to False // (2) one of the parent task's conditions failed or // (3) Pipeline is in stopping state (one of the PipelineTasks failed) -// Note that this means IsSkipped returns false if a conditionCheck is in progress -func (t ResolvedPipelineRunTask) IsSkipped(state PipelineRunState, d *dag.Graph) bool { +// Note that this means Skip returns false if a conditionCheck is in progress +func (t ResolvedPipelineRunTask) Skip(state PipelineRunState, d *dag.Graph) bool { // it already has TaskRun associated with it - PipelineTask not skipped if t.IsStarted() { return false @@ -159,6 +161,15 @@ func (t ResolvedPipelineRunTask) IsSkipped(state PipelineRunState, d *dag.Graph) } } + // Check if the when expressions have been + if len(t.PipelineTask.WhenExpressions) > 0 { + if haveVariables := t.PipelineTask.WhenExpressions.HaveVariables(); !haveVariables { + if areTrue := t.PipelineTask.WhenExpressions.AreTrue(); !areTrue { + return true + } + } + } + // Skip the PipelineTask if pipeline is in stopping state if isTaskInGraph(t.PipelineTask.Name, d) && state.IsStopping(d) { return true @@ -170,7 +181,7 @@ func (t ResolvedPipelineRunTask) IsSkipped(state PipelineRunState, d *dag.Graph) node := d.Nodes[t.PipelineTask.Name] if isTaskInGraph(t.PipelineTask.Name, d) { for _, p := range node.Prev { - if stateMap[p.Task.HashKey()].IsSkipped(state, d) { + if stateMap[p.Task.HashKey()].Skip(state, d) { return true } } @@ -253,7 +264,7 @@ func (state PipelineRunState) SuccessfulOrSkippedDAGTasks(d *dag.Graph) []string tasks := []string{} for _, t := range state { if isTaskInGraph(t.PipelineTask.Name, d) { - if t.IsSuccessful() || t.IsSkipped(state, d) { + if t.IsSuccessful() || t.Skip(state, d) { tasks = append(tasks, t.PipelineTask.Name) } } @@ -270,7 +281,7 @@ func (state PipelineRunState) checkTasksDone(d *dag.Graph) bool { // this task might have skipped if taskRun is nil // continue and ignore if this task was skipped // skipped task is considered part of done - if t.IsSkipped(state, d) { + if t.Skip(state, d) { continue } return false @@ -554,7 +565,7 @@ func GetPipelineConditionStatus(pr *v1beta1.PipelineRun, state PipelineRunState, switch { case rprt.IsSuccessful(): withStatusTasks = append(withStatusTasks, rprt.PipelineTask.Name) - case rprt.IsSkipped(state, dag): + case rprt.Skip(state, dag): skipTasks++ withStatusTasks = append(withStatusTasks, rprt.PipelineTask.Name) // At least one is skipped and no failure yet, mark as completed diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go index a1b4dbc0fea..86c4928ade0 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go @@ -38,6 +38,7 @@ import ( corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/util/sets" "knative.dev/pkg/apis" duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" @@ -78,6 +79,22 @@ var pts = []v1beta1.PipelineTask{{ Name: "mytask9", TaskRef: &v1beta1.TaskRef{Name: "taskHasParentWithRunAfter"}, RunAfter: []string{"mytask8"}, +}, { + Name: "mytask10", + TaskRef: &v1beta1.TaskRef{Name: "taskWithWhenExpressions"}, + WhenExpressions: []v1beta1.WhenExpression{{ + Input: "foo", + Operator: selection.In, + Values: []string{"foo", "bar"}, + }}, +}, { + Name: "mytask11", + TaskRef: &v1beta1.TaskRef{Name: "taskWithWhenExpressions"}, + WhenExpressions: []v1beta1.WhenExpression{{ + Input: "foo", + Operator: selection.NotIn, + Values: []string{"foo", "bar"}, + }}, }} var p = &v1beta1.Pipeline{ @@ -1134,6 +1151,30 @@ func TestIsSkipped(t *testing.T) { }, }}, expected: true, + }, { + name: "tasks-when-expressions-passed", + taskName: "mytask10", + state: PipelineRunState{{ + PipelineTask: &pts[9], + TaskRunName: "pipelinerun-guardedtask", + TaskRun: nil, + ResolvedTaskResources: &resources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + }}, + expected: false, + }, { + name: "tasks-when-expression-failed", + taskName: "mytask11", + state: PipelineRunState{{ + PipelineTask: &pts[10], + TaskRunName: "pipelinerun-guardedtask", + TaskRun: nil, + ResolvedTaskResources: &resources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + }}, + expected: true, }} for _, tc := range tcs { @@ -1147,7 +1188,7 @@ func TestIsSkipped(t *testing.T) { if rprt == nil { t.Fatalf("Could not get task %s from the state: %v", tc.taskName, tc.state) } - isSkipped := rprt.IsSkipped(tc.state, dag) + isSkipped := rprt.Skip(tc.state, dag) if d := cmp.Diff(isSkipped, tc.expected); d != "" { t.Errorf("Didn't get expected isSkipped %s", diff.PrintWantGot(d)) } @@ -2660,3 +2701,79 @@ func TestPipelineRunState_GetFinalTasks(t *testing.T) { }) } } + +func TestResolvePipeline_WhenExpressions(t *testing.T) { + names.TestingSeed() + tName1 := "pipelinerun-mytask1-9l9zj-always-true-mz4c7" + tName2 := "pipelinerun-mytask1-9l9zj-always-true-mssqb" + + t1 := &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: tName1, + }, + } + + t2 := &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: tName2, + }, + } + + ptwe1 := v1beta1.WhenExpression{ + Input: "foo", + Operator: selection.In, + Values: []string{"foo"}, + } + + ptwe2 := v1beta1.WhenExpression{ + Input: "foo", + Operator: selection.NotIn, + Values: []string{"bar"}, + } + + pts := []v1beta1.PipelineTask{{ + Name: "mytask1", + TaskRef: &v1beta1.TaskRef{Name: "task"}, + WhenExpressions: []v1beta1.WhenExpression{ptwe1, ptwe2}, + }} + + providedResources := map[string]*resourcev1alpha1.PipelineResource{} + + getTask := func(name string) (v1beta1.TaskInterface, error) { return task, nil } + getClusterTask := func(name string) (v1beta1.TaskInterface, error) { return nil, errors.New("should not get called") } + getCondition := func(name string) (*v1alpha1.Condition, error) { return &condition, nil } + pr := v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipelinerun", + }, + } + + tcs := []struct { + name string + getTaskRun resources.GetTaskRun + }{ + { + name: "When Expressions exist", + getTaskRun: func(name string) (*v1beta1.TaskRun, error) { + switch name { + case "pipelinerun-mytask1-9l9zj-always-true-0-mz4c7": + return t1, nil + case "pipelinerun-mytask1-9l9zj": + return &trs[0], nil + case "pipelinerun-mytask1-9l9zj-always-true-1-mssqb": + return t2, nil + } + return nil, fmt.Errorf("getTaskRun called with unexpected name %s", name) + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + _, err := ResolvePipelineRun(context.Background(), pr, getTask, tc.getTaskRun, getClusterTask, getCondition, pts, providedResources) + if err != nil { + t.Fatalf("Did not expect error when resolving PipelineRun: %v", err) + } + }) + } +} diff --git a/pkg/reconciler/pipelinerun/resources/resultrefresolution.go b/pkg/reconciler/pipelinerun/resources/resultrefresolution.go index c2062eca4cb..9c4130e261f 100644 --- a/pkg/reconciler/pipelinerun/resources/resultrefresolution.go +++ b/pkg/reconciler/pipelinerun/resources/resultrefresolution.go @@ -40,7 +40,7 @@ type ResolvedResultRef struct { func ResolveResultRefs(pipelineRunState PipelineRunState, targets PipelineRunState) (ResolvedResultRefs, error) { var allResolvedResultRefs ResolvedResultRefs for _, target := range targets { - resolvedResultRefs, err := convertParamsToResultRefs(pipelineRunState, target) + resolvedResultRefs, err := convertToResultRefs(pipelineRunState, target) if err != nil { return nil, err } @@ -72,6 +72,14 @@ func extractResultRefsForParam(pipelineRunState PipelineRunState, param v1beta1. return nil, nil } +func extractResultRefsForWhenExpression(pipelineRunState PipelineRunState, whenExpression v1beta1.WhenExpression) (ResolvedResultRefs, error) { + expressions, ok := whenExpression.GetVarSubstitutionExpressionsForWhenExpression() + if ok { + return extractResultRefs(expressions, pipelineRunState) + } + return nil, nil +} + // extractResultRefs resolves any ResultReference that are found in param or pipeline result // Returns nil if none are found func extractResultRefsForPipelineResult(pipelineStatus v1beta1.PipelineRunStatus, result v1beta1.PipelineResult) (ResolvedResultRefs, error) { @@ -139,24 +147,30 @@ func removeDup(refs ResolvedResultRefs) ResolvedResultRefs { return deduped } -// convertParamsToResultRefs converts all params of the resolved pipeline run task -func convertParamsToResultRefs(pipelineRunState PipelineRunState, target *ResolvedPipelineRunTask) (ResolvedResultRefs, error) { - var resolvedParams ResolvedResultRefs +// convertToResultRefs converts all params and when expressions of the resolved pipeline run task +func convertToResultRefs(pipelineRunState PipelineRunState, target *ResolvedPipelineRunTask) (ResolvedResultRefs, error) { + var resolvedResultRefs ResolvedResultRefs for _, condition := range target.PipelineTask.Conditions { condRefs, err := convertParams(condition.Params, pipelineRunState, condition.ConditionRef) if err != nil { return nil, err } - resolvedParams = append(resolvedParams, condRefs...) + resolvedResultRefs = append(resolvedResultRefs, condRefs...) } taskParamsRefs, err := convertParams(target.PipelineTask.Params, pipelineRunState, target.PipelineTask.Name) if err != nil { return nil, err } - resolvedParams = append(resolvedParams, taskParamsRefs...) + resolvedResultRefs = append(resolvedResultRefs, taskParamsRefs...) - return resolvedParams, nil + taskWhenExpressionsRefs, err := convertWhenExpressions(target.PipelineTask.WhenExpressions, pipelineRunState, target.PipelineTask.Name) + if err != nil { + return nil, err + } + resolvedResultRefs = append(resolvedResultRefs, taskWhenExpressionsRefs...) + + return resolvedResultRefs, nil } func convertParams(params []v1beta1.Param, pipelineRunState PipelineRunState, name string) (ResolvedResultRefs, error) { @@ -173,6 +187,21 @@ func convertParams(params []v1beta1.Param, pipelineRunState PipelineRunState, na return resolvedParams, nil } +// convertWhenExpressions converts all When Expressions of the resolved pipeline run task +func convertWhenExpressions(whenExpressions []v1beta1.WhenExpression, pipelineRunState PipelineRunState, name string) (ResolvedResultRefs, error) { + var resolvedWhenExpressions ResolvedResultRefs + for _, whenExpression := range whenExpressions { + resolvedResultRefs, err := extractResultRefsForWhenExpression(pipelineRunState, whenExpression) + if err != nil { + return nil, fmt.Errorf("unable to find result referenced by when expression with input %q in task %q: %w", whenExpression.Input, name, err) + } + if resolvedResultRefs != nil { + resolvedWhenExpressions = append(resolvedWhenExpressions, resolvedResultRefs...) + } + } + return resolvedWhenExpressions, nil +} + // convertPipelineResultToResultRefs converts all params of the resolved pipeline run task func convertPipelineResultToResultRefs(pipelineStatus v1beta1.PipelineRunStatus, pipelineResult v1beta1.PipelineResult) ResolvedResultRefs { resolvedResultRefs, err := extractResultRefsForPipelineResult(pipelineStatus, pipelineResult) diff --git a/pkg/reconciler/pipelinerun/resources/resultrefresolution_test.go b/pkg/reconciler/pipelinerun/resources/resultrefresolution_test.go index 518379d9962..c5edc3cec01 100644 --- a/pkg/reconciler/pipelinerun/resources/resultrefresolution_test.go +++ b/pkg/reconciler/pipelinerun/resources/resultrefresolution_test.go @@ -11,6 +11,7 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/test/diff" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/selection" "knative.dev/pkg/apis" duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" ) @@ -294,6 +295,263 @@ func resolvedSliceAsString(rs []*ResolvedResultRef) string { return fmt.Sprintf("[\n%s\n]", strings.Join(s, ",\n")) } +func TestTaskWhenExpressionsResolver_ResolveResultRefs(t *testing.T) { + type fields struct { + pipelineRunState PipelineRunState + } + type args struct { + we v1beta1.WhenExpression + } + tests := []struct { + name string + fields fields + args args + want ResolvedResultRefs + wantErr bool + }{ + { + name: "successful resolution: when expression not using result reference", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + TaskRunName: "aTaskRun", + TaskRun: tb.TaskRun("aTaskRun"), + PipelineTask: &v1beta1.PipelineTask{ + Name: "aTask", + TaskRef: &v1beta1.TaskRef{Name: "aTask"}, + }, + }, + }, + }, + args: args{ + we: v1beta1.WhenExpression{ + Input: "explicitValueNoResultReference", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + want: nil, + wantErr: false, + }, + { + name: "successful resolution: using result reference", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + TaskRunName: "aTaskRun", + TaskRun: tb.TaskRun("aTaskRun", tb.TaskRunStatus( + tb.TaskRunResult("aResult", "aResultValue"), + )), + PipelineTask: &v1beta1.PipelineTask{ + Name: "aTask", + TaskRef: &v1beta1.TaskRef{Name: "aTask"}, + }, + }, + }, + }, + args: args{ + we: v1beta1.WhenExpression{ + Input: "$(tasks.aTask.results.aResult)", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + want: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, + }, + wantErr: false, + }, { + name: "successful resolution: using multiple result reference", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + TaskRunName: "aTaskRun", + TaskRun: tb.TaskRun("aTaskRun", tb.TaskRunStatus( + tb.TaskRunResult("aResult", "aResultValue"), + )), + PipelineTask: &v1beta1.PipelineTask{ + Name: "aTask", + TaskRef: &v1beta1.TaskRef{Name: "aTask"}, + }, + }, { + TaskRunName: "bTaskRun", + TaskRun: tb.TaskRun("bTaskRun", tb.TaskRunStatus( + tb.TaskRunResult("bResult", "bResultValue"), + )), + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + }, + }, + }, + }, + args: args{ + we: v1beta1.WhenExpression{ + Input: "$(tasks.aTask.results.aResult) $(tasks.bTask.results.bResult)", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + want: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "bResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "bTask", + Result: "bResult", + }, + FromTaskRun: "bTaskRun", + }, + }, + wantErr: false, + }, { + name: "successful resolution: duplicate result references", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + TaskRunName: "aTaskRun", + TaskRun: tb.TaskRun("aTaskRun", tb.TaskRunStatus( + tb.TaskRunResult("aResult", "aResultValue"), + )), + PipelineTask: &v1beta1.PipelineTask{ + Name: "aTask", + TaskRef: &v1beta1.TaskRef{Name: "aTask"}, + }, + }, + }, + }, + args: args{ + we: v1beta1.WhenExpression{ + Input: "$(tasks.aTask.results.aResult) $(tasks.aTask.results.aResult)", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + want: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, + }, + wantErr: false, + }, { + name: "unsuccessful resolution: referenced result doesn't exist in referenced task", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + TaskRunName: "aTaskRun", + TaskRun: tb.TaskRun("aTaskRun"), + PipelineTask: &v1beta1.PipelineTask{ + Name: "aTask", + TaskRef: &v1beta1.TaskRef{Name: "aTask"}, + }, + }, + }, + }, + args: args{ + we: v1beta1.WhenExpression{ + Input: "$(tasks.aTask.results.aResult)", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + want: nil, + wantErr: true, + }, { + name: "unsuccessful resolution: pipeline task missing", + fields: fields{ + pipelineRunState: PipelineRunState{}, + }, + args: args{ + we: v1beta1.WhenExpression{ + Input: "$(tasks.aTask.results.aResult)", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + want: nil, + wantErr: true, + }, { + name: "unsuccessful resolution: task run missing", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + PipelineTask: &v1beta1.PipelineTask{ + Name: "aTask", + TaskRef: &v1beta1.TaskRef{Name: "aTask"}, + }, + }, + }, + }, + args: args{ + we: v1beta1.WhenExpression{ + Input: "$(tasks.aTask.results.aResult)", + Operator: selection.In, + Values: []string{"bar"}, + }, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Logf("test name: %s\n", tt.name) + got, err := extractResultRefsForWhenExpression(tt.fields.pipelineRunState, tt.args.we) + // sort result ref based on task name to guarantee an certain order + sort.SliceStable(got, func(i, j int) bool { + return strings.Compare(got[i].FromTaskRun, got[j].FromTaskRun) < 0 + }) + if (err != nil) != tt.wantErr { + t.Fatalf("ResolveResultRef() error = %v, wantErr %v", err, tt.wantErr) + } + if len(tt.want) != len(got) { + t.Fatalf("incorrect number of refs, want %d, got %d", len(tt.want), len(got)) + } + for _, rGot := range got { + foundMatch := false + for _, rWant := range tt.want { + if d := cmp.Diff(rGot, rWant); d == "" { + foundMatch = true + } + } + if !foundMatch { + t.Fatalf("Expected resolved refs:\n%s\n\nbut received:\n%s\n", resolvedSliceAsString(tt.want), resolvedSliceAsString(got)) + } + } + }) + } +} + func TestResolveResultRefs(t *testing.T) { type args struct { pipelineRunState PipelineRunState @@ -323,6 +581,44 @@ func TestResolveResultRefs(t *testing.T) { }, }, }, + }, { + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + WhenExpressions: []v1beta1.WhenExpression{ + { + Input: "$(tasks.aTask.results.aResult)", + Operator: selection.In, + Values: []string{"$(tasks.aTask.results.aResult)"}, + }, + }, + }, + }, { + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + WhenExpressions: []v1beta1.WhenExpression{ + { + Input: "$(tasks.aTask.results.missingResult)", + Operator: selection.In, + Values: []string{"$(tasks.aTask.results.missingResult)"}, + }, + }, + }, + }, { + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + Params: []v1beta1.Param{ + { + Name: "bParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.aTask.results.missingResult)", + }, + }, + }, + }, }, } @@ -333,7 +629,7 @@ func TestResolveResultRefs(t *testing.T) { wantErr bool }{ { - name: "Test successful result references resolution", + name: "Test successful result references resolution - params", args: args{ pipelineRunState: pipelineRunState, targets: PipelineRunState{ @@ -354,8 +650,29 @@ func TestResolveResultRefs(t *testing.T) { }, }, wantErr: false, - }, - { + }, { + name: "Test successful result references resolution - when expressions", + args: args{ + pipelineRunState: pipelineRunState, + targets: PipelineRunState{ + pipelineRunState[2], + }, + }, + want: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, + }, + wantErr: false, + }, { name: "Test successful result references resolution non result references", args: args{ pipelineRunState: pipelineRunState, @@ -365,6 +682,26 @@ func TestResolveResultRefs(t *testing.T) { }, want: nil, wantErr: false, + }, { + name: "Test unsuccessful result references resolution - when expression", + args: args{ + pipelineRunState: pipelineRunState, + targets: PipelineRunState{ + pipelineRunState[3], + }, + }, + want: nil, + wantErr: true, + }, { + name: "Test unsuccessful result references resolution - params", + args: args{ + pipelineRunState: pipelineRunState, + targets: PipelineRunState{ + pipelineRunState[4], + }, + }, + want: nil, + wantErr: true, }, } for _, tt := range tests {