diff --git a/docs/pipelines.md b/docs/pipelines.md index 62ce5b81c6f..c7a2ae85339 100644 --- a/docs/pipelines.md +++ b/docs/pipelines.md @@ -377,6 +377,33 @@ tasks: name: echo-hello ``` +When a `Condition` evaluates to `False`, to skip the guarded `Task` only and allow dependent `Tasks` to execute, use the +`continueAfterSkip` field and set it to `true` or `yes`. The `continueAfterSkip` field defaults to `false`, but to explicitly +not execute dependent `Tasks`, set it to `false` or `no`. + +In this example, `Task` `create-file` is executed, `Condition` `echo-when-file-missing` evaluates to `false` so `Task` +`echo-when-file-missing` is skipped, but because `continueAfterSkip` is set to `true`, `Task` `echo-hello` is executed. + +```yaml +tasks: + - name: create-file # executed + taskRef: create-readme-file + - name: echo-when-file-missing # skipped + when: + - name: file-missing + taskRef: + name: file-missing + continueAfterSkip: `true` + taskRef: + name: echo-missing + runAfter: + - create-file + - name: echo-hello # executed + taskRef: echo-hello + runAfter: + - echo-when-file-missing +``` + ### Configuring the failure timeout You can use the `Timeout` field in the `Task` spec within the `Pipeline` to set the timeout diff --git a/examples/v1alpha1/pipelineruns/conditional-pipelinerun-continue-after-skip-ordering.yaml b/examples/v1alpha1/pipelineruns/conditional-pipelinerun-continue-after-skip-ordering.yaml new file mode 100644 index 00000000000..aea83f25001 --- /dev/null +++ b/examples/v1alpha1/pipelineruns/conditional-pipelinerun-continue-after-skip-ordering.yaml @@ -0,0 +1,56 @@ +apiVersion: tekton.dev/v1alpha1 +kind: Condition +metadata: + name: always-false +spec: + check: + image: alpine + script: | + exit 1 +--- +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: echo-expected +spec: + steps: + - name: echo-expected + image: ubuntu + script: 'echo expected' +--- +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: echo-unexpected +spec: + steps: + - name: echo-file-exists + image: ubuntu + script: 'echo UNEXPECTED!' +--- +apiVersion: tekton.dev/v1alpha1 +kind: Pipeline +metadata: + name: conditional-pipeline +spec: + tasks: + - name: task-should-be-skipped # failed + conditions: + - conditionRef: always-false + continueAfterSkip: 'true' # allows executing dependent tasks + taskRef: + name: echo-unexpected + - name: task-should-execute # succeeded + taskRef: + name: echo-expected + runAfter: + - task-should-be-skipped +--- +apiVersion: tekton.dev/v1alpha1 +kind: PipelineRun +metadata: + name: conditional-pr-continue-after-skip-ordering +spec: + pipelineRef: + name: conditional-pipeline + serviceAccountName: 'default' diff --git a/examples/v1alpha1/pipelineruns/conditional-pipelinerun-continue-after-skip-resource.yaml b/examples/v1alpha1/pipelineruns/conditional-pipelinerun-continue-after-skip-resource.yaml new file mode 100644 index 00000000000..65124652999 --- /dev/null +++ b/examples/v1alpha1/pipelineruns/conditional-pipelinerun-continue-after-skip-resource.yaml @@ -0,0 +1,134 @@ +apiVersion: tekton.dev/v1alpha1 +kind: Condition +metadata: + name: is-equal +spec: + params: + - name: left + type: string + - name: right + type: string + check: + image: alpine + script: | + #!/bin/sh + if [ $(params.left) = $(params.right) ]; then + echo "$(params.left) == $(params.right)" + exit 0 + else + echo "$(params.left) != $(params.right)" + exit 1 + fi +--- +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: sum + annotations: + description: | + A simple task that sums the two provided integers +spec: + inputs: + params: + - name: a + type: string + default: "1" + description: The first integer + - name: b + type: string + default: "1" + description: The second integer + results: + - name: sum + description: The sum of the two provided integers + steps: + - name: sum + image: bash:latest + script: | + #!/usr/bin/env bash + echo -n $(( "$(inputs.params.a)" + "$(inputs.params.b)" )) | tee $(results.sum.path) +--- +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: multiply + annotations: + description: | + A simple task that multiplies the two provided integers +spec: + inputs: + params: + - name: a + type: string + default: "1" + description: The first integer + - name: b + type: string + default: "1" + description: The second integer + results: + - name: product + description: The product of the two provided integers + steps: + - name: product + image: bash:latest + script: | + #!/usr/bin/env bash + echo -n $(( "$(inputs.params.a)" * "$(inputs.params.b)" )) | tee $(results.product.path) +--- +apiVersion: tekton.dev/v1alpha1 +kind: Pipeline +metadata: + name: condition-pipeline +spec: + params: + - name: a + type: string + - name: b + type: string + tasks: + - name: sum-inputs # condition evaluates to false, should be skipped (failed) + conditions: + - conditionRef: is-equal + params: + - name: left + value: "1" + - name: right + value: $(params.b) + continueAfterSkip: 'true' + taskRef: + name: sum + params: + - name: a + value: "$(params.a)" + - name: b + value: "$(params.b)" + - name: multiply-inputs # should execute (succeeded) + taskRef: + name: multiply + params: + - name: a + value: "$(params.a)" + - name: b + value: "$(params.b)" + - name: sum-and-multiply # should execute, resolution error (failed) + taskRef: + name: sum + params: + - name: a + value: "$(tasks.multiply-inputs.results.product)$(tasks.sum-inputs.results.sum)" + - name: b + value: "$(tasks.multiply-inputs.results.product)$(tasks.sum-inputs.results.sum)" +--- +apiVersion: tekton.dev/v1alpha1 +kind: PipelineRun +metadata: + name: condition-pipelinerun-resources +spec: + params: + - name: a + value: "1" + - name: b + value: "2" + pipelineRef: + name: condition-pipeline diff --git a/go.mod b/go.mod index 79868528449..ac0d4740146 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/tektoncd/pipeline go 1.13 require ( + cloud.google.com/go/storage v1.6.0 contrib.go.opencensus.io/exporter/stackdriver v0.13.1 // indirect github.com/GoogleCloudPlatform/cloud-builders/gcs-fetcher v0.0.0-20191203181535-308b93ad1f39 github.com/aws/aws-sdk-go v1.30.16 // indirect @@ -26,6 +27,7 @@ require ( golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/text v0.3.3 // indirect gomodules.xyz/jsonpatch/v2 v2.1.0 + google.golang.org/api v0.20.0 google.golang.org/protobuf v1.22.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect k8s.io/api v0.17.6 diff --git a/internal/builder/v1alpha1/pipeline.go b/internal/builder/v1alpha1/pipeline.go index a1c4acc9189..b8ab02a73f9 100644 --- a/internal/builder/v1alpha1/pipeline.go +++ b/internal/builder/v1alpha1/pipeline.go @@ -306,6 +306,13 @@ func PipelineTaskConditionResource(name, resource string, from ...string) Pipeli } } +// ContinueAfterSkip sets the boolean to determine whether dependent tasks will execute upon Condition failure +func ContinueAfterSkip(continueAfterSkip string) PipelineTaskOp { + return func(pt *v1alpha1.PipelineTask) { + pt.ContinueAfterSkip = continueAfterSkip + } +} + // PipelineTaskWorkspaceBinding adds a workspace with the specified name, workspace and subpath on a PipelineTask. func PipelineTaskWorkspaceBinding(name, workspace, subPath string) PipelineTaskOp { return func(pt *v1alpha1.PipelineTask) { diff --git a/pkg/apis/pipeline/v1alpha1/pipeline_conversion.go b/pkg/apis/pipeline/v1alpha1/pipeline_conversion.go index 787fd3a7827..b73f7b871c3 100644 --- a/pkg/apis/pipeline/v1alpha1/pipeline_conversion.go +++ b/pkg/apis/pipeline/v1alpha1/pipeline_conversion.go @@ -67,6 +67,7 @@ func (source *PipelineTask) ConvertTo(ctx context.Context, sink *v1beta1.Pipelin } } sink.Conditions = source.Conditions + sink.ContinueAfterSkip = source.ContinueAfterSkip sink.Retries = source.Retries sink.RunAfter = source.RunAfter sink.Resources = source.Resources @@ -117,6 +118,7 @@ func (sink *PipelineTask) ConvertFrom(ctx context.Context, source v1beta1.Pipeli } } sink.Conditions = source.Conditions + sink.ContinueAfterSkip = source.ContinueAfterSkip sink.Retries = source.Retries sink.RunAfter = source.RunAfter sink.Resources = source.Resources diff --git a/pkg/apis/pipeline/v1alpha1/pipeline_conversion_test.go b/pkg/apis/pipeline/v1alpha1/pipeline_conversion_test.go index ab78a966927..6c8cea43a57 100644 --- a/pkg/apis/pipeline/v1alpha1/pipeline_conversion_test.go +++ b/pkg/apis/pipeline/v1alpha1/pipeline_conversion_test.go @@ -81,6 +81,7 @@ func TestPipelineConversion_Success(t *testing.T) { Conditions: []PipelineTaskCondition{{ ConditionRef: "condition1", }}, + ContinueAfterSkip: "false", Retries: 10, RunAfter: []string{"task1"}, Resources: &PipelineTaskResources{ diff --git a/pkg/apis/pipeline/v1alpha1/pipeline_types.go b/pkg/apis/pipeline/v1alpha1/pipeline_types.go index 0fe65aaae43..1a7432676a8 100644 --- a/pkg/apis/pipeline/v1alpha1/pipeline_types.go +++ b/pkg/apis/pipeline/v1alpha1/pipeline_types.go @@ -121,6 +121,11 @@ type PipelineTask struct { // +optional Conditions []PipelineTaskCondition `json:"conditions,omitempty"` + // ContinueAfterSkip is a string that needs to be true for the dependent Tasks of a Task guarded by Conditions + // to execute even when the Conditions evaluate to False and the Task is skipped + // +optional + ContinueAfterSkip string `json:"continueAfterSkip,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"` @@ -134,6 +139,7 @@ type PipelineTask struct { // outputs. // +optional Resources *PipelineTaskResources `json:"resources,omitempty"` + // Parameters declares parameters passed to this task. // +optional Params []Param `json:"params,omitempty"` diff --git a/pkg/apis/pipeline/v1beta1/pipeline_types.go b/pkg/apis/pipeline/v1beta1/pipeline_types.go index 74cfe3dc5fe..729d8b75735 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_types.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_types.go @@ -110,6 +110,11 @@ type PipelineTask struct { // +optional Conditions []PipelineTaskCondition `json:"conditions,omitempty"` + // ContinueAfterSkip is a string that needs to be true for the dependent Tasks of a Task guarded by Conditions + // to execute even when the Conditions evaluate to False and the Task is skipped + // +optional + ContinueAfterSkip string `json:"continueAfterSkip,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"` diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go index d8f014ef084..8bfa25dc84c 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go @@ -21,6 +21,7 @@ import ( "fmt" "reflect" "strconv" + "strings" "go.uber.org/zap" corev1 "k8s.io/api/core/v1" @@ -141,6 +142,28 @@ func (t ResolvedPipelineRunTask) IsStarted() bool { return true } +func (t ResolvedPipelineRunTask) shouldContinueAfterSkip() bool { + trueMap := map[string]bool {"true": true, "yes": true, "y": true, "on": true} + if _, ok := trueMap[strings.ToLower(t.PipelineTask.ContinueAfterSkip)]; ok { + return true + } + return false +} + +func (t ResolvedPipelineRunTask) skipChildTask(state PipelineRunState, d *dag.Graph) bool { + stateMap := state.ToMap() + 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()].shouldContinueAfterSkip(){ + return true + } + } + } + } + return false +} // IsSkipped returns true if a PipelineTask will not be run because // (1) its Condition Checks failed or // (2) one of the parent task's conditions failed or @@ -164,17 +187,12 @@ func (t ResolvedPipelineRunTask) IsSkipped(state PipelineRunState, d *dag.Graph) return true } - stateMap := state.ToMap() // Recursively look at parent tasks to see if they have been skipped, // if any of the parents have been skipped, skip as well - 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) { - return true - } - } + if t.skipChildTask(state, d) { + return true } + return false } diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go index a1b4dbc0fea..c0cc6e3c945 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go @@ -891,6 +891,162 @@ func TestIsDone(t *testing.T) { } } +func TestShouldContinuteAfterSkip (t *testing.T) { + tcs := []struct { + name string + taskName string + state PipelineRunState + expected bool + }{{ + name: "tasks-parent-condition-false-should-continue-default", + taskName: "mytask7", + state: PipelineRunState{{ + PipelineTask: &pts[6], + }}, + expected: false, + }, { + name: "tasks-parent-condition-false-should-continue-true", + taskName: "mytask10", + state: PipelineRunState{{ + PipelineTask: &v1beta1.PipelineTask{ + Name: "mytask10", + TaskRef: &v1beta1.TaskRef{Name: "taskWithConditionsWithContinueAfterSkip"}, + Conditions: []v1beta1.PipelineTaskCondition{{ + ConditionRef: "always-true", + }}, + ContinueAfterSkip: "true", + }, + }}, + expected: true, + }, { + name: "tasks-parent-condition-false-should-continue-yes", + taskName: "mytask10", + state: PipelineRunState{{ + PipelineTask: &v1beta1.PipelineTask{ + Name: "mytask10", + TaskRef: &v1beta1.TaskRef{Name: "taskWithConditionsWithContinueAfterSkip"}, + Conditions: []v1beta1.PipelineTaskCondition{{ + ConditionRef: "always-true", + }}, + ContinueAfterSkip: "yes", + }, + }}, + expected: true, + }, { + name: "tasks-parent-condition-false-should-continue-false", + taskName: "mytask10", + state: PipelineRunState{{ + PipelineTask: &v1beta1.PipelineTask{ + Name: "mytask10", + TaskRef: &v1beta1.TaskRef{Name: "taskWithConditionsWithContinueAfterSkip"}, + Conditions: []v1beta1.PipelineTaskCondition{{ + ConditionRef: "always-true", + }}, + ContinueAfterSkip: "false", + }, + }}, + expected: false, + }, { + name: "tasks-parent-condition-false-should-continue-no", + taskName: "mytask10", + state: PipelineRunState{{ + PipelineTask: &v1beta1.PipelineTask{ + Name: "mytask10", + TaskRef: &v1beta1.TaskRef{Name: "taskWithConditionsWithContinueAfterSkip"}, + Conditions: []v1beta1.PipelineTaskCondition{{ + ConditionRef: "always-true", + }}, + ContinueAfterSkip: "no", + }, + }}, + expected: false, + }} + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + stateMap := tc.state.ToMap() + rprt := stateMap[tc.taskName] + if rprt == nil { + t.Fatalf("Could not get task %s from the state: %v", tc.taskName, tc.state) + } + shouldContinueAfterSkip := rprt.shouldContinueAfterSkip() + if d := cmp.Diff(shouldContinueAfterSkip, tc.expected); d != "" { + t.Errorf("Didn't get expected shouldContinueAfterSkip %s from task %s", diff.PrintWantGot(d), tc.taskName) + } + }) + } + +} +func TestSkipChildTask(t *testing.T) { + tcs := []struct { + name string + taskName string + state PipelineRunState + expected bool + }{{ + name: "tasks-parent-condition-failed-should-skip-child-task", + taskName: "mytask7", + state: PipelineRunState{{ + PipelineTask: &pts[5], + TaskRunName: "pipelinerun-conditionaltask", + TaskRun: nil, + ResolvedTaskResources: &resources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + ResolvedConditionChecks: failedTaskConditionCheckState, + }, { + PipelineTask: &pts[6], + }}, + expected: true, + }, { + name: "tasks-parent-condition-failed-should-execute-child-task", + taskName: "mytask11", + state: PipelineRunState{{ + PipelineTask: &v1beta1.PipelineTask{ + Name: "mytask10", + TaskRef: &v1beta1.TaskRef{Name: "taskWithConditionsWithContinueAfterSkip"}, + Conditions: []v1beta1.PipelineTaskCondition{{ + ConditionRef: "always-true", + }}, + ContinueAfterSkip: "true", + }, + TaskRunName: "pipelinerun-conditionaltask", + TaskRun: nil, + ResolvedTaskResources: &resources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + ResolvedConditionChecks: failedTaskConditionCheckState, + }, { + PipelineTask: &v1beta1.PipelineTask{ + Name: "mytask11", + TaskRef: &v1beta1.TaskRef{Name: "taskWithOneParentWithContinueAfterSkip"}, + RunAfter: []string{"mytask10"}, + }, + }}, + expected: false, + }} + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + dag, err := DagFromState(tc.state) + if err != nil { + t.Fatalf("Could not get a dag from the TC state %#v: %v", tc.state, err) + } + + stateMap := tc.state.ToMap() + rprt := stateMap[tc.taskName] + if rprt == nil { + t.Fatalf("Could not get task %s from the state: %v", tc.taskName, tc.state) + } + + skipChildTask := rprt.skipChildTask(tc.state, dag) + if d := cmp.Diff(skipChildTask, tc.expected); d != "" { + t.Errorf("Didn't get expected skipChildTask %s from task %s", diff.PrintWantGot(d), tc.taskName) + } + }) + } +} + func TestIsSkipped(t *testing.T) { tcs := []struct {