Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditions Beta: When Expressions #3135

Merged
merged 1 commit into from
Sep 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/conditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ weight: 11
-->
# Conditions

**Note:** `Conditions` are deprecated, use [WhenExpressions](pipelines.md#guard-task-execution-using-whenexpressions) instead.

- [Overview](#overview)
- [Configuring a `Condition`](#configuring-a-condition)
- [Specifying the condition `check`](#specifying-the-condition-check)
Expand Down
20 changes: 20 additions & 0 deletions docs/pipelineruns.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,26 @@ False|PipelineRunTimeout|Yes|The `PipelineRun` timed out.

When a `PipelineRun` changes status, [events](events.md#pipelineruns) are triggered accordingly.

When a `PipelineRun` has `Tasks` with [WhenExpressions](pipelines.md#guard-task-execution-using-whenexpressions):
- If the `WhenExpressions` evaluate to `true`, the `Task` is executed and the `TaskRun` will be listed in the `Task Runs` section of the `status` of the `PipelineRun`.
- If the `WhenExpressions` evaluate to `false`, the `Task` is skipped and it is listed in the `Skipped Tasks` section of the `status` of the `PipelineRun`.

```yaml
Conditions:
Last Transition Time: 2020-08-27T15:07:34Z
Message: Tasks Completed: 1 (Failed: 0, Cancelled 0), Skipped: 1
jerop marked this conversation as resolved.
Show resolved Hide resolved
Reason: Completed
Status: True
Type: Succeeded
Skipped Tasks:
Name: skip-this-task
Task Runs:
pipelinerun-to-skip-task-run-this-task-r2djj:
Pipeline Task Name: run-this-task
Status:
...
```
jerop marked this conversation as resolved.
Show resolved Hide resolved

## Cancelling a `PipelineRun`

To cancel a `PipelineRun` that's currently executing, update its definition
Expand Down
49 changes: 49 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,8 +317,56 @@ tasks:
name: build-push
```

### Guard `Task` execution using `WhenExpressions`
jerop marked this conversation as resolved.
Show resolved Hide resolved

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 `WhenExpression` which can be static inputs or variables ([`Parameters`](#specifying-parameters) or [`Results`](#using-results)). If the `Input` is not provided, it defaults to an empty string.
- `Operator` represents an `Input`'s relationship to a set of `Values`. A valid `Operator` must be provided, which can be either `in` or `notin`.
- `Values` is an array of string values. The `Values` array must be provided and be non-empty. It can contain static values or variables ([`Parameters`](#specifying-parameters) or [`Results`](#using-results)).

The [`Parameters`](#specifying-parameters) are read from the `Pipeline` and [`Results`](#using-results) are read directly from previous [`Tasks`](#adding-tasks-to-the-pipeline). Using [`Results`](#using-results) in a `WhenExpression` in a guarded `Task` introduces a resource dependency on the previous `Task` that produced the `Result`.

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 `Task` is listed in the [`Skipped Tasks` section of the `PipelineRunStatus`](pipelineruns.md#monitoring-execution-status).
jerop marked this conversation as resolved.
Show resolved Hide resolved

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 specified in the same `Task`. The `Pipeline` will be rejected as invalid if both `WhenExpressions` and `Conditions` are included.

jerop marked this conversation as resolved.
Show resolved Hide resolved
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

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

**Note:** `Conditions` are deprecated, use [`WhenExpressions`](#guard-task-execution-using-whenexpressions) instead.
jerop marked this conversation as resolved.
Show resolved Hide resolved

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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# `Conditions` are deprecated, use `WhenExpressions` instead
# https://github.com/tektoncd/pipeline/blob/master/docs/pipelines.md#guard-task-execution-using-whenexpressions
apiVersion: tekton.dev/v1alpha1
kind: Condition
metadata:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# `Conditions` are deprecated, use `WhenExpressions` instead
# https://github.com/tektoncd/pipeline/blob/master/docs/pipelines.md#guard-task-execution-using-whenexpressions
apiVersion: tekton.dev/v1alpha1
kind: Condition
metadata:
Expand Down
2 changes: 2 additions & 0 deletions examples/v1beta1/pipelineruns/conditional-pipelinerun.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# `Conditions` are deprecated, use `WhenExpressions` instead
# https://github.com/tektoncd/pipeline/blob/master/docs/pipelines.md#guard-task-execution-using-whenexpressions
apiVersion: tekton.dev/v1alpha1
kind: Condition
metadata:
Expand Down
5 changes: 4 additions & 1 deletion examples/v1beta1/pipelineruns/demo-optional-resources.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# `Conditions` are deprecated, use `WhenExpressions` instead
# https://github.com/tektoncd/pipeline/blob/master/docs/pipelines.md#guard-task-execution-using-whenexpressions
apiVersion: tekton.dev/v1alpha1
kind: Condition
metadata:
Expand All @@ -13,7 +15,8 @@ spec:
image: alpine
script: 'test -f $(resources.git-repo.path)/$(params.path)'
---

# `Conditions` are deprecated, use `WhenExpressions` instead
# https://github.com/tektoncd/pipeline/blob/master/docs/pipelines.md#guard-task-execution-using-whenexpressions
apiVersion: tekton.dev/v1alpha1
kind: Condition
metadata:
Expand Down
2 changes: 2 additions & 0 deletions examples/v1beta1/pipelineruns/pipeline-result-conditions.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# `Conditions` are deprecated, use `WhenExpressions` instead
# https://github.com/tektoncd/pipeline/blob/master/docs/pipelines.md#guard-task-execution-using-whenexpressions
apiVersion: tekton.dev/v1alpha1
kind: Condition
metadata:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ spec:
- |
ls -al $(resources.inputs.pipeline-git.path)
---

# `Conditions` are deprecated, use `WhenExpressions` instead
# https://github.com/tektoncd/pipeline/blob/master/docs/pipelines.md#guard-task-execution-using-whenexpressions
jerop marked this conversation as resolved.
Show resolved Hide resolved
apiVersion: tekton.dev/v1alpha1
kind: Condition
metadata:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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
workspaces:
- name: source
description: |
This workspace will receive the cloned git repo and be passed
to the next Task to create a file
tasks:
- name: create-file
when:
- input: "$(params.path)"
operator: in
values: ["README.md"]
workspaces:
- name: source
workspace: source
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
params:
- name: path
value: "$(params.path)"
workspaces:
- name: source
workspace: source
runAfter:
- create-file
taskSpec:
params:
- name: path
workspaces:
- name: source
description: The workspace to check for the file
results:
- name: status
description: indicates whether the file exists or is missing
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
- 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: "$(params.path)"
operator: notin
values: ["README.md"]
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: path
value: README.md
workspaces:
- name: source
volumeClaimTemplate:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
13 changes: 13 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 @@ -332,6 +333,18 @@ func PipelineTaskConditionResource(name, resource string, from ...string) Pipeli
}
}

// PipelineTaskWhenExpression adds a WhenExpression with the specified input, operator and values
// which are used to determine whether the PipelineTask should be executed or skipped.
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
11 changes: 7 additions & 4 deletions internal/builder/v1beta1/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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),
),
Expand Down Expand Up @@ -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{
Expand Down
15 changes: 15 additions & 0 deletions pkg/apis/pipeline/v1beta1/pipeline_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,14 @@ type PipelineTask struct {
TaskSpec *EmbeddedTask `json:"taskSpec,inline,omitempty"`

// Conditions is a list of conditions that need to be true for the task to run
// Conditions are deprecated, use WhenExpressions instead
// +optional
jerop marked this conversation as resolved.
Show resolved Hide resolved
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"`
Expand Down Expand Up @@ -197,6 +202,16 @@ func (pt PipelineTask) Deps() []string {
}
}
}
// Add any dependents from when expressions
jerop marked this conversation as resolved.
Show resolved Hide resolved
for _, whenExpression := range pt.WhenExpressions {
expressions, ok := whenExpression.GetVarSubstitutionExpressions()
if ok {
resultRefs := NewResultRefs(expressions)
for _, resultRef := range resultRefs {
deps = append(deps, resultRef.PipelineTask)
}
}
}
return deps
}

Expand Down
30 changes: 30 additions & 0 deletions pkg/apis/pipeline/v1beta1/pipeline_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -364,6 +368,9 @@ func validatePipelineParametersVariables(tasks []PipelineTask, prefix string, pa
if err := validatePipelineParametersVariablesInTaskParameters(task.Params, prefix, paramNames, arrayParamNames); err != nil {
return err
}
if err := task.WhenExpressions.validatePipelineParametersVariables(prefix, paramNames, arrayParamNames); err != nil {
return err
}
}
return nil
}
Expand Down Expand Up @@ -460,6 +467,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 {
jerop marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down Expand Up @@ -503,3 +513,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
}