Skip to content

Commit

Permalink
Add support for finally task results in pipeline results
Browse files Browse the repository at this point in the history
Prior to this commit, finally tasks can emit Results but
results emitted from the finally tasks can not be configured in
the Pipeline Results.

In this commit, we add implementation for supporting finally tasks
in Pipeline results, update documentation to reflect these changes,
and add examples for the users to better understand the new
feature being added.

References [TEP-0116](tektoncd/community#746)

/kind feature
/cc @jerop
  • Loading branch information
vsinghai committed Jul 23, 2022
1 parent a4f9216 commit ed4e11c
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 29 deletions.
31 changes: 15 additions & 16 deletions docs/pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ weight: 400
- [Specifying `Parameters` in `finally` tasks](#specifying-parameters-in-finally-tasks)
- [Specifying `matrix` in `finally` tasks](#specifying-matrix-in-finally-tasks)
- [Consuming `Task` execution results in `finally`](#consuming-task-execution-results-in-finally)
- [Consuming `Pipeline` result with `finally`](#consuming-pipeline-result-with-finally)
- [`PipelineRun` Status with `finally`](#pipelinerun-status-with-finally)
- [Using Execution `Status` of `pipelineTask`](#using-execution-status-of-pipelinetask)
- [Using Aggregate Execution `Status` of All `Tasks`](#using-aggregate-execution-status-of-all-tasks)
Expand All @@ -50,7 +51,6 @@ weight: 400
- [Known Limitations](#known-limitations)
- [Specifying `Resources` in `finally` tasks](#specifying-resources-in-finally-tasks)
- [Cannot configure the `finally` task execution order](#cannot-configure-the-finally-task-execution-order)
- [Cannot configure `Pipeline` result with `finally`](#cannot-configure-pipeline-result-with-finally)
- [Using Custom Tasks](#using-custom-tasks)
- [Specifying the target Custom Task](#specifying-the-target-custom-task)
- [Specifying a Custom Task Spec in-line (or embedded)](#specifying-a-custom-task-spec-in-line-or-embedded)
Expand Down Expand Up @@ -1268,6 +1268,20 @@ uninitialized task result `commit`, the `finally` Task `discover-git-commit` wil
`skippedTasks` and continues executing rest of the `finally` tasks. The pipeline exits with `completion` instead of
`success` if a `finally` task is added to the list of `skippedTasks`.

### Consuming `Pipeline` result with `finally`

`finally` tasks can emit `Results` and these results emitted from the `finally` tasks can be configured in the
[Pipeline Results](#emitting-results-from-a-pipeline). References of `Results` from `finally` will follow the same naming conventions as referencing `Results` from `tasks`: ```$(finally.<finally-pipelinetask-name>.result.<result-name>)```.

```yaml
results:
- name: comment-count-validate
value: $(finally.check-count.results.comment-count-validate)
```

In this example, `pipelineResults` in `status` will show the name-value pair for that result `comment-count-validate`. `finally` support `pipelineResults` was designed and discussed in [TEP-0116](https://github.com/tektoncd/community/pull/746).


### `PipelineRun` Status with `finally`

With `finally`, `PipelineRun` status is calculated based on `PipelineTasks` under `tasks` section and `finally` tasks.
Expand Down Expand Up @@ -1544,21 +1558,6 @@ It's not possible to configure or modify the execution order of the `finally` ta
all `finally` tasks run simultaneously and start executing once all `PipelineTasks` under `tasks` have settled which means
no `runAfter` can be specified in `finally` tasks.

#### Cannot configure `Pipeline` result with `finally`

`finally` tasks can emit `Results` but results emitted from the `finally` tasks can not be configured in the
[Pipeline Results](#emitting-results-from-a-pipeline). We are working on adding support for this
(tracked in issue [#2710](https://github.com/tektoncd/pipeline/issues/2710)).

```yaml
results:
- name: comment-count-validate
value: $(finally.check-count.results.comment-count-validate)
```

In this example, `pipelineResults` in `status` will exclude the name-value pair for that result `comment-count-validate`.


## Using Custom Tasks

**Note: This is only allowed if `enable-custom-tasks` is set to
Expand Down
110 changes: 110 additions & 0 deletions examples/v1beta1/pipelineruns/pipelinerun-with-final-results.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: sum-and-multiply-pipeline
spec:
params:
- name: a
type: string
default: "1"
- name: b
type: string
default: "1"
results:
- name: finally-result
description: "grabbing results from the finally section"
value: $(finally.sum-and-multiply.results.sum)

tasks:
- name: sum-inputs
taskRef:
name: sum
params:
- name: a
value: "$(params.a)"
- name: b
value: "$(params.b)"
- name: multiply-inputs
taskRef:
name: multiply
params:
- name: a
value: "$(params.a)"
- name: b
value: "$(params.b)"
finally:
- name: sum-and-multiply
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/v1beta1
kind: Task
metadata:
name: sum
annotations:
description: |
A simple task that sums the two provided integers
spec:
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 $(( "$(params.a)" + "$(params.b)" )) | tee $(results.sum.path)
---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: multiply
annotations:
description: |
A simple task that multiplies the two provided integers
spec:
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 $(( "$(params.a)" * "$(params.b)" )) | tee $(results.product.path)
---
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
generateName: sum-and-multiply-pipeline-run-
spec:
pipelineRef:
name: sum-and-multiply-pipeline
params:
- name: a
value: "2"
- name: b
value: "10"
19 changes: 13 additions & 6 deletions pkg/apis/pipeline/v1beta1/pipeline_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (ps *PipelineSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
errs = errs.Also(validatePipelineWorkspacesUsage(ps.Workspaces, ps.Tasks).ViaField("tasks"))
errs = errs.Also(validatePipelineWorkspacesUsage(ps.Workspaces, ps.Finally).ViaField("finally"))
// Validate the pipeline's results
errs = errs.Also(validatePipelineResults(ps.Results, ps.Tasks))
errs = errs.Also(validatePipelineResults(ps.Results, ps.Tasks, ps.Finally))
errs = errs.Also(validateTasksAndFinallySection(ps))
errs = errs.Also(validateFinalTasks(ps.Tasks, ps.Finally))
errs = errs.Also(validateWhenExpressions(ps.Tasks, ps.Finally))
Expand Down Expand Up @@ -255,8 +255,9 @@ func filter(arr []string, cond func(string) bool) []string {
}

// validatePipelineResults ensure that pipeline result variables are properly configured
func validatePipelineResults(results []PipelineResult, tasks []PipelineTask) (errs *apis.FieldError) {
func validatePipelineResults(results []PipelineResult, tasks []PipelineTask, finally []PipelineTask) (errs *apis.FieldError) {
pipelineTaskNames := getPipelineTasksNames(tasks)
pipelineFinallyTaskNames := getPipelineTasksNames(finally)
for idx, result := range results {
expressions, ok := GetVarSubstitutionExpressionsForPipelineResult(result)
if !ok {
Expand All @@ -276,7 +277,7 @@ func validatePipelineResults(results []PipelineResult, tasks []PipelineTask) (er
"value").ViaFieldIndex("results", idx))
}

if !taskContainsResult(result.Value.StringVal, pipelineTaskNames) {
if !taskContainsResult(result.Value.StringVal, pipelineTaskNames, pipelineFinallyTaskNames) {
errs = errs.Also(apis.ErrInvalidValue("referencing a nonexistent task",
"value").ViaFieldIndex("results", idx))
}
Expand All @@ -297,16 +298,22 @@ func getPipelineTasksNames(pipelineTasks []PipelineTask) sets.String {

// taskContainsResult ensures the result value is referenced within the
// task names
func taskContainsResult(resultExpression string, pipelineTaskNames sets.String) bool {
func taskContainsResult(resultExpression string, pipelineTaskNames sets.String, pipelineFinallyTaskNames sets.String) bool {
// split incase of multiple resultExpressions in the same result.Value string
// i.e "$(task.<task-name).result.<result-name>) - $(task2.<task2-name).result2.<result2-name>)"
split := strings.Split(resultExpression, "$")
for _, expression := range split {
if expression != "" {
pipelineTaskName, _, _, _, _ := parseExpression(stripVarSubExpression("$" + expression))
if !pipelineTaskNames.Has(pipelineTaskName) {
value := stripVarSubExpression("$" + expression)
pipelineTaskName, _, _, _, _ := parseExpression(value)
if !strings.HasPrefix(value, "tasks") && !strings.HasPrefix(value, "finally") {
return false
} else if strings.HasPrefix(value, "tasks") && !pipelineTaskNames.Has(pipelineTaskName) {
return false
} else if strings.HasPrefix(value, "finally") && !pipelineFinallyTaskNames.Has(pipelineTaskName) {
return false
}

}
}
return true
Expand Down
4 changes: 2 additions & 2 deletions pkg/apis/pipeline/v1beta1/pipeline_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1149,7 +1149,7 @@ func TestValidatePipelineResults_Success(t *testing.T) {
Description: "this is my pipeline result",
Value: *NewArrayOrString("$(tasks.a-task.results.gitrepo.commit)"),
}}
if err := validatePipelineResults(results, []PipelineTask{{Name: "a-task"}}); err != nil {
if err := validatePipelineResults(results, []PipelineTask{{Name: "a-task"}}, []PipelineTask{}); err != nil {
t.Errorf("Pipeline.validatePipelineResults() returned error for valid pipeline: %s: %v", desc, err)
}
}
Expand Down Expand Up @@ -1189,7 +1189,7 @@ func TestValidatePipelineResults_Failure(t *testing.T) {
apis.ErrInvalidValue("referencing a nonexistent task", "results[0].value")),
}}
for _, tt := range tests {
err := validatePipelineResults(tt.results, []PipelineTask{{Name: "a-task"}})
err := validatePipelineResults(tt.results, []PipelineTask{{Name: "a-task"}}, []PipelineTask{})
if err == nil {
t.Errorf("Pipeline.validatePipelineResults() did not return for invalid pipeline: %s", tt.desc)
}
Expand Down
10 changes: 6 additions & 4 deletions pkg/apis/pipeline/v1beta1/resultref.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const (
objectResultExpressionFormat = "tasks.<taskName>.results.<objectResultName>.<individualAttribute>"
// ResultTaskPart Constant used to define the "tasks" part of a pipeline result reference
ResultTaskPart = "tasks"
// ResultFinallyPart Constant used to define the "finally" part of a pipeline result reference
ResultFinallyPart = "finally"
// ResultResultPart Constant used to define the "results" part of a pipeline result reference
ResultResultPart = "results"
// TODO(#2462) use one regex across all substitutions
Expand Down Expand Up @@ -99,7 +101,7 @@ func LooksLikeContainsResultRefs(expressions []string) bool {
// looksLikeResultRef attempts to check if the given string looks like it contains any
// result references. Returns true if it does, false otherwise
func looksLikeResultRef(expression string) bool {
return strings.HasPrefix(expression, "task") && strings.Contains(expression, ".result")
return (strings.HasPrefix(expression, "task") || strings.HasPrefix(expression, "finally")) && strings.Contains(expression, ".result")
}

// GetVarSubstitutionExpressionsForParam extracts all the value between "$(" and ")"" for a parameter
Expand Down Expand Up @@ -172,7 +174,7 @@ func parseExpression(substitutionExpression string) (string, string, int, string

// For string result: tasks.<taskName>.results.<stringResultName>
// For array result: tasks.<taskName>.results.<arrayResultName>[index]
if len(subExpressions) == 4 && subExpressions[0] == ResultTaskPart && subExpressions[2] == ResultResultPart {
if len(subExpressions) == 4 && (subExpressions[0] == ResultTaskPart || subExpressions[0] == ResultFinallyPart) && subExpressions[2] == ResultResultPart {
resultName, stringIdx := ParseResultName(subExpressions[3])
if stringIdx != "" {
intIdx, _ := strconv.Atoi(stringIdx)
Expand All @@ -182,11 +184,11 @@ func parseExpression(substitutionExpression string) (string, string, int, string
}

// For object type result: tasks.<taskName>.results.<objectResultName>.<individualAttribute>
if len(subExpressions) == 5 && subExpressions[0] == ResultTaskPart && subExpressions[2] == ResultResultPart {
if len(subExpressions) == 5 && (subExpressions[0] == ResultTaskPart || subExpressions[0] == ResultFinallyPart) && subExpressions[2] == ResultResultPart {
return subExpressions[1], subExpressions[3], 0, subExpressions[4], nil
}

return "", "", 0, "", fmt.Errorf("Must be one of the form 1). %q; 2). %q", resultExpressionFormat, objectResultExpressionFormat)
return "", "", 0, "", fmt.Errorf("must be one of the form 1). %q; 2). %q", resultExpressionFormat, objectResultExpressionFormat)
}

// ParseResultName parse the input string to extract resultName and result index.
Expand Down
2 changes: 1 addition & 1 deletion pkg/reconciler/pipelinerun/resources/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ func ApplyTaskResultsToPipelineResults(
continue
}
variableParts := strings.Split(variable, ".")
if variableParts[0] != v1beta1.ResultTaskPart || variableParts[2] != v1beta1.ResultResultPart {
if (variableParts[0] != v1beta1.ResultTaskPart && variableParts[0] != v1beta1.ResultFinallyPart) || variableParts[2] != v1beta1.ResultResultPart {
validPipelineResult = false
invalidPipelineResults = append(invalidPipelineResults, pipelineResult.Name)
continue
Expand Down
71 changes: 71 additions & 0 deletions pkg/reconciler/pipelinerun/resources/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2056,6 +2056,77 @@ func TestApplyWorkspaces(t *testing.T) {
}
}

func TestApplyFinallyResultsToPipelineResults(t *testing.T) {
for _, tc := range []struct {
description string
results []v1beta1.PipelineResult
taskResults map[string][]v1beta1.TaskRunResult
runResults map[string][]v1alpha1.RunResult
expected []v1beta1.PipelineRunResult
}{{
description: "single-string-result-single-successful-task",
results: []v1beta1.PipelineResult{{
Name: "pipeline-result-1",
Value: *v1beta1.NewArrayOrString("$(finally.pt1.results.foo)"),
}},
taskResults: map[string][]v1beta1.TaskRunResult{
"pt1": {
{
Name: "foo",
Value: *v1beta1.NewArrayOrString("do"),
},
},
},
expected: []v1beta1.PipelineRunResult{{
Name: "pipeline-result-1",
Value: *v1beta1.NewArrayOrString("do"),
}},
}, {
description: "single-array-result-single-successful-task",
results: []v1beta1.PipelineResult{{
Name: "pipeline-result-1",
Value: *v1beta1.NewArrayOrString("$(finally.pt1.results.foo[*])"),
}},
taskResults: map[string][]v1beta1.TaskRunResult{
"pt1": {
{
Name: "foo",
Value: *v1beta1.NewArrayOrString("do", "rae"),
},
},
},
expected: []v1beta1.PipelineRunResult{{
Name: "pipeline-result-1",
Value: *v1beta1.NewArrayOrString("do", "rae"),
}},
}, {
description: "multiple-results-custom-and-normal-tasks",
results: []v1beta1.PipelineResult{{
Name: "pipeline-result-1",
Value: *v1beta1.NewArrayOrString("$(finally.customtask.results.foo)"),
}},
runResults: map[string][]v1alpha1.RunResult{
"customtask": {
{
Name: "foo",
Value: "do",
},
},
},
expected: []v1beta1.PipelineRunResult{{
Name: "pipeline-result-1",
Value: *v1beta1.NewArrayOrString("do"),
}},
}} {
t.Run(tc.description, func(t *testing.T) {
received, _ := ApplyTaskResultsToPipelineResults(tc.results, tc.taskResults, tc.runResults)
if d := cmp.Diff(tc.expected, received); d != "" {
t.Errorf(diff.PrintWantGot(d))
}
})
}
}

func TestApplyTaskResultsToPipelineResults(t *testing.T) {
for _, tc := range []struct {
description string
Expand Down

0 comments on commit ed4e11c

Please sign in to comment.