Skip to content

Commit

Permalink
Implement WhenExpressions
Browse files Browse the repository at this point in the history
Adding `WhenExpressions` used to efficiently specify guarded execution
of `Tasks`, without spinning up new pods. We use `WhenExpressions` to
avoid adding an opinionated andcomplex expression language to the Tekton
API to ensure Tekton can be supported by as many systems as possible.

The components of `WhenExpressions` are `Input`, `Operator`
and `Values`:
- `Input` is the input for the `Guard` checking which can be static
inputs or variables, such as `Parameters` or `Results`.
- `Values` is an array of string values. The `Values` array must be
non-empty. It can contain static values 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`.

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
skipped.

Further details in [Conditions Beta TEP](https://github.com/tektoncd/community/blob/master/teps/0007-conditions-beta.md).
  • Loading branch information
jerop committed Aug 20, 2020
1 parent 7d7119a commit e669fec
Show file tree
Hide file tree
Showing 21 changed files with 1,988 additions and 25 deletions.
42 changes: 42 additions & 0 deletions docs/pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -316,6 +317,47 @@ 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`.

The components of `WhenExpressions` are `Input`, `Operator` and `Values`:

- `Input` is the input for the `Guard` checking which can be static inputs or variables (`Parameters` or `Results`).
- `Values` is an array of string values. The `Values` array must be non-empty. It can contain static values
or variables (`Parameters` or `Results`).
- `Operator` represents an `Input`'s relationship to a set of `Values`. `Operators` we will use in `Guards`
are `In` and `NotIn`.

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
skipped.

When `WhenExpressions` are specified in a `Task`, `Conditions` should not be speficied in the same `Task`.

```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).

### Guard `Task` execution using `Conditions`

To run a `Task` only when certain conditions are met, it is possible to _guard_ task execution using
Expand Down
123 changes: 123 additions & 0 deletions examples/v1beta1/pipelineruns/pipelinerun-with-when-expressions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: create-readme-file
spec:
resources:
outputs:
- name: workspace
type: git
steps:
- name: write-new-stuff
image: ubuntu
script: 'touch $(resources.outputs.workspace.path)/README.md'
---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: check-file
spec:
params:
- name: path
resources:
inputs:
- name: workspace
type: git
results:
- name: status
description: indicating whether the file exists
steps:
- name: check-file
image: alpine
script: |
if test -f $(resources.inputs.workspace.path)/$(params.path); then
printf exists | tee /tekton/results/status
else
printf missing | tee /tekton/results/status
fi
---
apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: pipeline-git
spec:
type: git
params:
- name: revision
value: master
- name: url
value: https://github.com/tektoncd/pipeline
---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: echo-file-status
spec:
params:
- name: status
steps:
- name: echo
image: ubuntu
script: 'echo file $(params.status)'
---
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: guarded-pipeline
spec:
resources:
- name: source-repo
type: git
params:
- name: path
default: "README.md"
tasks:
- name: first-create-file
when:
- input: "$(params.path)"
operator: in
values: ["README.md"]
taskRef:
name: create-readme-file
resources:
outputs:
- name: workspace
resource: source-repo
- name: check-file
when:
- input: "foo"
operator: in
values: ["foo", "bar"]
taskRef:
name: check-file
params:
- name: path
value: "$(params.path)"
resources:
inputs:
- name: workspace
resource: source-repo
from: [first-create-file]
- name: echo-file-exists
params:
- name: status
value: "$(tasks.check-file.results.status)"
when:
- input: "$(tasks.check-file.results.status)"
operator: in
values: ["exists"]
taskRef:
name: echo-file-status
---
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: guarded-pr
spec:
pipelineRef:
name: guarded-pipeline
serviceAccountName: 'default'
resources:
- name: source-repo
resourceRef:
name: pipeline-git
14 changes: 14 additions & 0 deletions internal/builder/v1beta1/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -54,6 +55,8 @@ type PipelineRunStatusOp func(*v1beta1.PipelineRunStatus)
// PipelineTaskConditionOp is an operation which modifies a PipelineTaskCondition
type PipelineTaskConditionOp func(condition *v1beta1.PipelineTaskCondition)

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 {
Expand Down Expand Up @@ -319,6 +322,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) {
Expand Down
10 changes: 6 additions & 4 deletions internal/builder/v1beta1/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func TestPipeline(t *testing.T) {
tb.PipelineTaskOutputResource("some-image", "my-only-image-resource"),
),
tb.PipelineTask("never-gonna", "give-you-up",
tb.PipelineTaskWhenExpression("foo", "in", []string{"foo", "bar"}),
tb.RunAfter("foo"),
tb.PipelineTaskTimeout(5*time.Second),
),
Expand Down Expand Up @@ -131,10 +132,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: "in", Values: []string{"foo", "bar"}}},
RunAfter: []string{"foo"},
Timeout: &metav1.Duration{Duration: 5 * time.Second},
}, {
Name: "foo",
TaskSpec: &v1beta1.TaskSpec{
Expand Down
37 changes: 37 additions & 0 deletions pkg/apis/pipeline/v1beta1/pipeline_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package v1beta1
import (
"github.com/tektoncd/pipeline/pkg/reconciler/pipeline/dag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/selection"
)

// +genclient
Expand Down Expand Up @@ -110,6 +111,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 []WhenExpression `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"`
Expand Down Expand Up @@ -176,6 +181,16 @@ func (pt PipelineTask) Deps() []string {
}
}
}
// Add any dependents from when expressions
for _, whenExpression := range pt.WhenExpressions {
expressions, ok := GetVarSubstitutionExpressionsForWhenExpression(whenExpression)
if ok {
resultRefs := NewResultRefs(expressions)
for _, resultRef := range resultRefs {
deps = append(deps, resultRef.PipelineTask)
}
}
}
return deps
}

Expand All @@ -189,6 +204,16 @@ func (l PipelineTaskList) Items() []dag.Task {
return tasks
}

// ApplyReplacements applies replacements for variables in When Expressions in a Task.
func (whenExpression *WhenExpression) ApplyReplacements(stringReplacements map[string]string) {
whenExpression.Input = ApplyReplacements(whenExpression.Input, stringReplacements)
var newValues []string
for _, val := range whenExpression.Values {
newValues = append(newValues, ApplyReplacements(val, stringReplacements))
}
whenExpression.Values = newValues
}

// PipelineTaskParam is used to provide arbitrary string parameters to a Task.
type PipelineTaskParam struct {
Name string `json:"name"`
Expand All @@ -209,6 +234,18 @@ type PipelineTaskCondition struct {
Resources []PipelineTaskInputResource `json:"resources,omitempty"`
}

// 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"`
}

// PipelineDeclaredResource is used by a Pipeline to declare the types of the
// PipelineResources that it will required to run and names which can be used to
// refer to these PipelineResources in PipelineTaskResourceBindings.
Expand Down
Loading

0 comments on commit e669fec

Please sign in to comment.