title | authors | creation-date | last-updated | status | |||
---|---|---|---|---|---|---|---|
Task Results without Results |
|
2020-10-20 |
2022-08-09 |
implementable |
A task
in a pipeline
can produce a result and that result can be consumed in many ways within that pipeline
:
params
mapping in a consumerpipelineTask
kind: Pipeline
spec:
tasks:
- name: format-result
taskRef:
name: format-result
params:
- name: result
value: "$(tasks.sum-inputs.results.result)"
WhenExpressions
kind: Pipeline
spec:
tasks:
- name: echo-file-exists
when:
- input: "$(tasks.check-file.results.exists)"
operator: in
values: ["yes"]
- Pipeline Results
kind: Pipeline
spec:
tasks:
...
results:
- name: sum
value: $(tasks.second-add.results.sum)
Today, pipeline
is declared failure
and stops executing further if the result resolution fails because of
a missing task result. There are many reasons for a missing task result:
- a
task
producing task result failed, no result available - a
task
producing result was skipped/disabled and no result generated - a
task
producing result did not generate that result even without any failure. We have a bug report open to declare such a task as failure. This reason might not hold true after issue #3497 is fixed.
Here are the major motivations for pipeline
authors to design their pipelines with the missing task results:
-
Implementing the TEP-0059: Skipping Strategies proposal to limit the scope of
WhenExpressions
to only that task and continue executing the dependencies.Let's revisit an example of sending a Slack notification when someone manually approves the PR. This is done by sending the
approver
's name to theslack-msg
task as the result ofmanual-approval
task.Further, extending the same use case, when someone approves the PR, the
approver
would be set to an appropriate name. At the same time, set the task resultapprover
to None in case themanual-approval
task is skipped and theapprover
is not initialized. It is still possible to send a notification that no one approved the PR.lint unit-tests | | v v report-linter-output integration-tests | v manual-approval | | v (approver) build-image | | v v slack-msg deploy-image
Let's look at one more simple use case of conditional task.
clone-repo | v check-PR-content | (image changed) | v build-image | (image) | ______________ | | v v deploy-image update-list-of-builds
Here, the
pipeline
checks the changes being proposed in a PR. If the changes include updating an image,build-image
is executed to build a new image and publish it to a container registry.deploy-image
deploys this newly built image after resolving the result frombuild-image
. Ifbuild-image
was skipped and did not create any new image,deploy-image
need to deploy an already existing latest image which could be set as the default by the pipeline.This is not possible today without setting any default for the results.
deploy-image
will fail as the result resolution fails whenbuild-image
is not executed. -
Initialize
pipeline
results using the results of one of the two conditional tasks. Thepipeline
has two conditional tasks,build-trusted
andbuild-untrusted
. Thepipeline
executes one of the tasks based on the type of the builder. Now, irrespective of how the image was built, propagate the name of the image which was built to the pipeline results. This is not possible today. The task result resolution fails to resolve the missing result and declares the consolidating task as a failure along with thepipeline
.git-clone trusted | | untrusted v v build-trusted build-untrusted | | (image) (image) | | ______________ | v propogate APP_IMAGE to pipeline results
Missing the task results do not have to be fatal. Provide an option to the pipeline
author to build pipeline
that can continue executing even when a task result is missing.
-
Enable a
pipeline
to execute thepipelineTask
when that task is consuming the results of conditional tasks. -
Enable a
pipeline
to producepipeline results
produced by the conditional tasks.
Producing the task result in case of a failed task is out of the scope of this TEP.
deploy-image
requires a default image name to deploy on a cluster when build-image
is skipped because the
PR had no changes to a docker file.
spec:
tasks:
# Clone runtime repo
- name: git-clone
taskRef:
name: git-clone
# check the content of the PR i.e. the changes proposed
# does any of those changes contain changing a dockerfile
# if so, build a new image, otherwise, skip building an image
- name: check-pr-content
runAfter: [ "git-clone" ]
taskRef:
name: check-pr-content
# build an image if the platform developer is committing changes to a dockerfile or any other file which is part of
# the image
- name: build-image
runAfter: [ "check-pr-content" ]
when:
- input: "$(tasks.check-pr-content.results.image-change)"
operator: in
values: ["yes"]
taskRef:
name: build-image
# deploy a newly built image if build-image was successful and produced an image name
# deploy a latest platform by default if there are no changes in this PR
- name: deploy-image
runAfter: [ "build-image" ]
params:
- name: image-name
value: "$(tasks.build-image.results.image-name.path)"
taskRef:
name: deploy-image
# update the page where a list of builds is maintained with this new image
- name: update-list-of-builds
runAfter: [ "build-image" ]
params:
- name: image-name
value: "$(tasks.build-image.results.image-name.path)"
when:
- input: "$(tasks.build-image.status)"
operator: in
values: ["succeeded"]
taskRef:
name: update-list-of-builds
Produce the name of the image as the pipeline result depending on how the image was built.
spec:
tasks:
# Clone application repo
- name: git-clone
taskRef:
name: git-clone
# TRUST_BUILDER is set to true at the pipelineRun level if the builder image is trusted
# if the builder image is trusted, executed build-trusted and produce an image name as a result
- name: build-trusted
runAfter: [ "git-clone" ]
when:
- input: "$(params.TRUST_BUILDER)"
operator: in
values: ["true"]
taskRef:
name: build-trusted
# TRUST_BUILDER is set to false at the pipelineRun level if the builder image is not trusted
# and needs to run in isolation
# if the builder image is not trusted, executed build-un trusted and produce an image name as a result
- name: build-untrusted
runAfter: [ "git-clone" ]
when:
- input: "$(params.trusted)"
operator: in
values: ["false"]
taskRef:
name: build-untrusted
# read result of both build-trusted and build-untrusted and propagate the one which is initialized as a pipeline result
- name: propagate-image-name
runAfter: [ "build-image" ]
params:
- name: trusted-image-name
value: "$(tasks.build-trusted.results.image)"
- name: untrusted-image-name
value: "$(tasks.build-untrusted.results.image)"
taskRef:
name: propagate-image-name
# pipeline result
results:
- name: APP_IMAGE
value: $(tasks.propagate-image-name.results.image)
Today, a Task
can declare a Result
but not produce it and it's
execution will still be successful whereas if a subsequent Task
in
a Pipeline
attempts to use that Result
it will fail. This behaviour
can be confusing to the users as the Task
which didn't produced
the Result
passed and the follow-up Task
fails. Moreover, the user
of the Task
will loose faith in it as it failed to produce what it's
claiming.
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: check-name-matches
annotations:
description: |
Returns "passed" in the "check" result if the name of the CI Job
(GitHub Check) matches the regular expression specified. This is used
for the "/test" command in GitHub. The regular expression cannot contain spaces.
spec:
params:
- name: gitHubCommand
description: The whole comment left on GitHub
- name: checkName
description: The name of the check
results:
- name: check
description: The result of the check, "passed" or "failed"
steps:
- name: check-name
image: alpine
script: |
#!/bin/sh
set -ex
set -o pipefail
# If no command was specified, the check is successful
[[ "$(params.gitHubCommand)" == "" ]] && exit 0
# If a command was specified, the regex should match the checkName
REGEX="$(echo $(params.gitHubCommand) | awk '{ print $2}')"
[[ "$REGEX" == "" ]] && REGEX='.*'
(echo "$(params.checkName)" | grep -E "$REGEX") \
&& printf "passed" > $(results.check.path) \
|| printf "failed" > $(results.check.path)
---
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: catlin-linter
spec:
workspaces:
- name: source
description: Workspace where the git repo is prepared for linting.
params:
- name: gitCloneDepth
description: Number of commits in the change + 1
- name: gitHubCommand
description: The command that was used to trigger testing
- name: checkName
description: The name of the GitHub check that this pipeline is used for
- name: pullRequestUrl
description: The HTML URL for the pull request
- name: gitRepository
description: The git repository that hosts context and Dockerfile
- name: pullRequestBaseRef
description: The pull request base branch
- name: pullRequestNumber
description: The pullRequestNumber
tasks:
- name: check-name-match
taskRef:
name: check-name-matches
params:
- name: gitHubCommand
value: $(params.gitHubCommand)
- name: checkName
value: $(params.checkName)
- name: clone-repo
when:
- input: $(tasks.check-name-match.results.check)
operator: in
values: ["passed"]
taskRef:
name: git-batch-merge
bundle: gcr.io/tekton-releases/catalog/upstream/git-batch-merge:0.2
workspaces:
- name: output
workspace: source
params:
- name: url
value: $(params.gitRepository)
- name: mode
value: "merge"
- name: revision
value: $(params.pullRequestBaseRef)
- name: refspec
value: refs/heads/$(params.pullRequestBaseRef):refs/heads/$(params.pullRequestBaseRef)
- name: batchedRefs
value: "refs/pull/$(params.pullRequestNumber)/head"
- name: lint-catalog
runAfter:
- "clone-repo"
taskRef:
name: catlin-lint
workspaces:
- name: source
workspace: source
params:
- name: gitCloneDepth
value: $(params.gitCloneDepth)
finally:
- name: post-comment
when:
- input: $(tasks.check-name-match.results.check)
operator: in
values: ["passed"]
taskRef:
name: github-add-comment
bundle: gcr.io/tekton-releases/catalog/upstream/github-add-comment:0.3
params:
- name: COMMENT_OR_FILE
value: "catlin.txt"
- name: GITHUB_TOKEN_SECRET_NAME
value: bot-token-github
- name: GITHUB_TOKEN_SECRET_KEY
value: bot-token
- name: REQUEST_URL
value: $(params.pullRequestUrl)
workspaces:
- name: comment-file
workspace: source
The above Pipeline
is a part of Catlin
CI with a bit modifications
done in check-name-matches
Task
. The above Pipeline
should run
with the git events such as when a pull request is created
, etc but it
doesn't runs. Having default results defined in check-name-matches
Task
will fix the Pipeline
.
We propose adding an optional field - default
- to the Result
specification in Task
.
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: task
spec:
results:
- name: merge_status
description: whether to rebase or squash
type: string
default: rebase
- name: branches
description: branches in the repository
type: array
default:
- main
- v1alpha1
- v1beta1
- v1
- name: images
type: object
properties:
node:
type: string
default: "node:latest"
gcloud:
type: string
default: "gcloud:latest"
steps:
...
Adding a default value to Results
is optional; validation of a Task
doesn't
fail if the default value isn't provided.
Adding a default value to a Result
will guarantee that it will hold a value even
if the Task
fails to produce it. If a Task
does not produce a Result
that does not
have a default value, then the Task
should fail - see tektoncd/pipeline#3497 for further details.
The proposed solution can be used to solve the above use cases as follows:
spec:
tasks:
# Clone runtime repo
- name: git-clone
taskRef:
name: git-clone
# check the content of the PR i.e. the changes proposed
# does any of those changes contain changing a dockerfile
# if so, build a new image, otherwise, skip building an image
- name: check-pr-content
runAfter: [ "git-clone" ]
taskRef:
name: check-pr-content
# build an image if the platform developer is committing changes to a dockerfile or any other file which is part of
# the image
- name: build-image
runAfter: [ "check-pr-content" ]
when:
- input: "$(tasks.check-pr-content.results.image-change)"
operator: in
values: ["yes"]
taskRef:
name: build-image
# deploy a newly built image if build-image was successful and produced an image name
# deploy a latest platform by default if there are no changes in this PR
- name: deploy-image
runAfter: [ "build-image" ]
params:
- name: image-name
value: "$(tasks.build-image.results.image-name.path)"
taskRef:
name: deploy-image
# update the page where a list of builds is maintained with this new image
- name: update-list-of-builds
runAfter: [ "build-image" ]
params:
- name: image-name
value: "$(tasks.build-image.results.image-name.path)"
when:
- input: "$(tasks.build-image.status)"
operator: in
values: ["succeeded"]
taskRef:
name: update-list-of-builds
spec:
tasks:
# Clone application repo
- name: git-clone
taskRef:
name: git-clone
# TRUST_BUILDER is set to true at the pipelineRun level if the builder image is trusted
# if the builder image is trusted, executed build-trusted and produce an image name as a result
- name: build-trusted
runAfter: [ "git-clone" ]
when:
- input: "$(params.TRUST_BUILDER)"
operator: in
values: ["true"]
taskRef:
name: build-trusted
# TRUST_BUILDER is set to false at the pipelineRun level if the builder image is not trusted
# and needs to run in isolation
# if the builder image is not trusted, executed build-un trusted and produce an image name as a result
- name: build-untrusted
runAfter: [ "git-clone" ]
when:
- input: "$(params.trusted)"
operator: in
values: ["false"]
taskRef:
name: build-untrusted
# read result of both build-trusted and build-untrusted and propagate the one which is initialized as a pipeline result
- name: propagate-image-name
runAfter: [ "build-image" ]
params:
- name: trusted-image-name
value: "$(tasks.build-trusted.results.image)"
- name: untrusted-image-name
value: "$(tasks.build-untrusted.results.image)"
taskRef:
name: propagate-image-name
# pipeline result
results:
- name: APP_IMAGE
value: $(tasks.propagate-image-name.results.image)
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: check-name-matches
annotations:
description: |
Returns "passed" in the "check" result if the name of the CI Job
(GitHub Check) matches the regular expression specified. This is used
for the "/test" command in GitHub. The regular expression cannot contain spaces.
spec:
params:
- name: gitHubCommand
description: The whole comment left on GitHub
- name: checkName
description: The name of the check
results:
- name: check
description: The result of the check, "passed" or "failed"
default: "passed"
steps:
- name: check-name
image: alpine
script: |
#!/bin/sh
set -ex
set -o pipefail
# If no command was specified, the check is successful
[[ "$(params.gitHubCommand)" == "" ]] && exit 0
# If a command was specified, the regex should match the checkName
REGEX="$(echo $(params.gitHubCommand) | awk '{ print $2}')"
[[ "$REGEX" == "" ]] && REGEX='.*'
(echo "$(params.checkName)" | grep -E "$REGEX") \
&& printf "passed" > $(results.check.path) \
|| printf "failed" > $(results.check.path)
---
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: catlin-linter
spec:
workspaces:
- name: source
description: Workspace where the git repo is prepared for linting.
params:
- name: gitCloneDepth
description: Number of commits in the change + 1
- name: gitHubCommand
description: The command that was used to trigger testing
- name: checkName
description: The name of the GitHub check that this pipeline is used for
default: ""
- name: pullRequestUrl
description: The HTML URL for the pull request
- name: gitRepository
description: The git repository that hosts context and Dockerfile
- name: pullRequestBaseRef
description: The pull request base branch
- name: pullRequestNumber
description: The pullRequestNumber
tasks:
- name: check-name-match
taskRef:
name: check-name-matches
params:
- name: gitHubCommand
value: $(params.gitHubCommand)
- name: checkName
value: $(params.checkName)
- name: clone-repo
when:
- input: $(tasks.check-name-match.results.check)
operator: in
values: ["passed"]
taskRef:
name: git-batch-merge
bundle: gcr.io/tekton-releases/catalog/upstream/git-batch-merge:0.2
workspaces:
- name: output
workspace: source
...
...
finally:
...
In the above example, even if the value of parameter checkName
is not passed,
default value of Result
will be produced passed
and Pipeline's execution will
be continued. The only case when the execution of Pipeline
is stopped when
the wrong value of parameter checkName
is passed.
Having default results defined in check-name-matches Task will fix the Pipeline.
- Unit tests for the behaviour of
Results
when default value is present and none of the steps produce any result - Unit tests for the behaviour of
Results
when no default value is present and none of the steps produce any result - E2E tests can take the form of YAML examples that demonstrate the syntax for declaring a result with default value and results without defaults
Allow Results
to declare an optional field as optional
. When a Task
fails to
produce a Result
that's optional, then the Task
does not fail. When a Task
fails
to produce a Result
that's not optional, then then the Task
will fail.
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: task
spec:
results:
- name: merge_status
description: whether to rebase or squash
type: string
optional: true
- name: branches
description: branches in the repository
type: array
optional: true
- name: images
type: object
properties:
node:
type: string
optional: true
gcloud:
type: string
steps:
...
However, this solution only solves the issue of Tasks
not failing when they don't
produce Results
as discussed in tektoncd/pipeline#3497.
It does not address the use cases for providing default Results
that can be consumed in subsequent Tasks
.
Determine if we need default Results
declared at runtime in the future, and how we can support that.