Skip to content

Commit

Permalink
Skipping when Guard evaluates as False -- ContinueAfterSkip
Browse files Browse the repository at this point in the history
When a `Condition` fails, the guarded `Task` and its branch
(dependent `Tasks`) are skipped. A `Task` is dependent on and in the
branch of another `Task` as specified by ordering using `runAfter` or by
resources using `Results`, `Workspaces` and `Resources`.

In some use cases of `Conditions`, when a `Condition` evaluates to
`False`, users need to skip the guarded `Task` only and allow dependent
`Tasks` to execute. An example use case is when there’s a particular
`Task` that a Pipeline wants to execute when the git branch is
dev/staging/qa, but not when it’s the main/master/prod branch.
Another use case is when a user wants to send out a notification whether
or not a parent guarded `Task` was executed, as described in
[this issue](tektoncd#2937).

In this PR, we add a `continueAfterSkip` field to specify whether to
terminate the whole `Task` branch, or to skip the guarded `Task` only
and continue with the rest of the `Task` branch.

When a `Condition` evaluates to `False`, to skip the guarded `Task`
only and allow dependent `Tasks` to execute, users can pass in the
`continueAfterSkip` field and set it to `true` or `yes` (case insensitive).
The field defaults to `false` -- the rest of the `Task` branch is skipped
by default as it was previously.

When `continueAfterSkip` is set to `True`, subsequent `Tasks` based on
ordering dependencies should execute and the subsequent `Tasks` based
resource dependencies will have resource validation errors.
  • Loading branch information
jerop committed Jul 22, 2020
1 parent 1b28720 commit f9a8633
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 8 deletions.
27 changes: 27 additions & 0 deletions docs/pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions internal/builder/v1alpha1/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/pipeline/v1alpha1/pipeline_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/pipeline/v1alpha1/pipeline_conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func TestPipelineConversion_Success(t *testing.T) {
Conditions: []PipelineTaskCondition{{
ConditionRef: "condition1",
}},
ContinueAfterSkip: "false",
Retries: 10,
RunAfter: []string{"task1"},
Resources: &PipelineTaskResources{
Expand Down
6 changes: 6 additions & 0 deletions pkg/apis/pipeline/v1alpha1/pipeline_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/pipeline/v1beta1/pipeline_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
34 changes: 26 additions & 8 deletions pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"reflect"
"strconv"
"strings"

"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down
Loading

0 comments on commit f9a8633

Please sign in to comment.