Skip to content

Commit

Permalink
access execution status of pipelineTask
Browse files Browse the repository at this point in the history
Introducing a variable which can be used to access the execution
status of any pipelineTask in a pipeline. Use
$(context.pipelineRun.Tasks.<pipelineTask>) as param value which
contains the status, one of, Succeeded, Failed, TaskRunCancelled, and
Unknown.
  • Loading branch information
pritidesai committed Oct 15, 2020
1 parent da7a789 commit ebdb86d
Show file tree
Hide file tree
Showing 10 changed files with 327 additions and 0 deletions.
36 changes: 36 additions & 0 deletions docs/pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,42 @@ Overall, `PipelineRun` state transitioning is explained below for respective sce
Please refer to the [table](pipelineruns.md#monitoring-execution-status) under Monitoring Execution Status to learn about
what kind of events are triggered based on the `Pipelinerun` status.


### Using Execution `Status` of `pipelineTask`

Finally Task can utilize execution status of any of the `pipelineTasks` under `tasks` section using param:

```yaml
finally:
- name: finaltask
params:
- name: pipelineRun-pipelineTask-task1
value: "$(context.pipelineRun.Tasks.task1)"
taskSpec:
params:
- name: pipelineRun-pipelineTask-task1
steps:
- image: ubuntu
name: print-task-status
script: |
if [ $(params.pipelineRun-pipelineTask-task1) == "Failed" ]
then
echo "Task1 has failed, continue processing the failure"
fi
```

This kind of variable can have any one of the values from the following table:

| Status | Description |
| ------- | -----------|
| Succeeded | `taskRun` for that `pipelineTask` completed successfully |
| Failed | `taskRun` for that `pipelineTask` completed with a failure |
| TaskRunCancelled | `taskRun` for that `pipelineTask` is cancelled by the user |
| Skipped | that `pipelineTask` has been skipped |
| Unknown | no information available for that `pipelineTask` execution |

For an end-to-end example, see [`status` in a `PipelineRun`](../examples/v1beta1/pipelineruns/pipelinerun-task-execution-status.yaml).

### Known Limitations

### Specifying `Resources` in Final Tasks
Expand Down
1 change: 1 addition & 0 deletions docs/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ This page documents the variable substitutions supported by `Tasks` and `Pipelin
| `context.taskRun.namespace` | The namespace of the `TaskRun` that this `Task` is running in. |
| `context.taskRun.uid` | The uid of the `TaskRun` that this `Task` is running in. |
| `context.task.name` | The name of this `Task`. |
| `context.pipelineRun.Tasks.<pipelineTaskName>` | The execution status of the specified `pipelineTask`. |

### `PipelineResource` variables available in a `Task`

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
kind: PipelineRun
apiVersion: tekton.dev/v1beta1
metadata:
generateName: pr-execution-status-
spec:
serviceAccountName: 'default'
pipelineSpec:
tasks:
- name: task1 # successful task
params:
taskSpec:
steps:
- image: ubuntu
name: hello
script: |
echo "Hello World!"
- name: task2 # skipped task
when:
- input: "true"
operator: "notin"
values: [ "true" ]
taskSpec:
steps:
- image: ubuntu
name: success
script: |
exit 0
- name: task3 # failed task
taskSpec:
steps:
- image: ubuntu
name: fail
script: |
exit 1
finally:
- name: task4
params:
- name: pipelineRun-pipelineTask-task1
value: "$(context.pipelineRun.Tasks.task1)"
- name: pipelineRun-pipelineTask-task2
value: "$(context.pipelineRun.Tasks.task2)"
- name: pipelineRun-pipelineTask-task3
value: "$(context.pipelineRun.Tasks.task3)"
taskSpec:
params:
- name: pipelineRun-pipelineTask-task1
- name: pipelineRun-pipelineTask-task2
- name: pipelineRun-pipelineTask-task3
steps:
- image: ubuntu
name: print-failed-task-status
script: |
echo "Pipeline Task Status: Task1: $(params.pipelineRun-pipelineTask-task1)"
echo "Pipeline Task Status: Task2: $(params.pipelineRun-pipelineTask-task2)"
echo "Pipeline Task Status: Task3: $(params.pipelineRun-pipelineTask-task3)"
15 changes: 15 additions & 0 deletions pkg/apis/pipeline/v1beta1/pipeline_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func (ps *PipelineSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
errs = errs.Also(validatePipelineParameterVariables(ps.Tasks, ps.Params).ViaField("tasks"))
errs = errs.Also(validatePipelineParameterVariables(ps.Finally, ps.Params).ViaField("finally"))
errs = errs.Also(validatePipelineContextVariables(ps.Tasks))
errs = errs.Also(validatePipelineTasksContextVariables(ps.Tasks))
// Validate the pipeline's workspaces.
errs = errs.Also(validatePipelineWorkspaces(ps.Workspaces, ps.Tasks, ps.Finally))
// Validate the pipeline's results
Expand Down Expand Up @@ -232,6 +233,20 @@ func validatePipelineContextVariables(tasks []PipelineTask) *apis.FieldError {
return errs.Also(validatePipelineContextVariablesInParamValues(paramValues, "context\\.pipeline", pipelineContextNames))
}

func validatePipelineTasksContextVariables(tasks []PipelineTask) (errs *apis.FieldError) {
// creating a list of pipelineTask names to validate context.pipelineRun.Tasks.<name>
pipelineRunTasksContextNames := sets.String{}
var paramValues []string
for _, t := range tasks {
pipelineRunTasksContextNames.Insert(t.Name)
for _, param := range t.Params {
paramValues = append(paramValues, param.Value.StringVal)
paramValues = append(paramValues, param.Value.ArrayVal...)
}
}
return errs.Also(validatePipelineContextVariablesInParamValues(paramValues, "context\\.pipelineRun\\.Tasks", pipelineRunTasksContextNames))
}

func validatePipelineContextVariablesInParamValues(paramValues []string, prefix string, contextNames sets.String) (errs *apis.FieldError) {
for _, paramValue := range paramValues {
errs = errs.Also(substitution.ValidateVariableP(paramValue, prefix, contextNames).ViaField("value"))
Expand Down
44 changes: 44 additions & 0 deletions pkg/apis/pipeline/v1beta1/pipeline_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1995,6 +1995,50 @@ func TestContextInvalid(t *testing.T) {
}
}

func TestPipelineTasksContext(t *testing.T) {
tests := []struct {
name string
tasks []PipelineTask
expectedError apis.FieldError
}{{
name: "valid string context variable in pipelineTask status",
tasks: []PipelineTask{{
Name: "bar",
TaskRef: &TaskRef{Name: "bar-task"},
Params: []Param{{
Name: "bar-status", Value: ArrayOrString{StringVal: "$(context.pipelineRun.Tasks.bar)"},
}},
}},
}, {
name: "valid string context variable in pipelineTask status",
tasks: []PipelineTask{{
Name: "bar",
TaskRef: &TaskRef{Name: "bar-task"},
Params: []Param{{
Name: "bar-status", Value: ArrayOrString{StringVal: "$(context.pipelineRun.Tasks.notask)"},
}},
}},
expectedError: *apis.ErrGeneric(`non-existent variable in "$(context.pipelineRun.Tasks.notask)"`, "value"),
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validatePipelineTasksContextVariables(tt.tasks)
if len(tt.expectedError.Error()) == 0 {
if err != nil {
t.Errorf("Pipeline.validatePipelineTasksContextVariables() returned error for valid pipeline context variables: %s: %v", tt.name, err)
}
} else {
if err == nil {
t.Errorf("Pipeline.validatePipelineTasksContextVariables() did not return error for invalid pipeline parameters: %s, %s", tt.name, tt.tasks[0].Params)
}
if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
t.Errorf("PipelineSpec.Validate() errors diff %s", diff.PrintWantGot(d))
}
}
})
}
}

func getTaskSpec() TaskSpec {
return TaskSpec{
Steps: []Step{{
Expand Down
7 changes: 7 additions & 0 deletions pkg/reconciler/pipelinerun/pipelinerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,9 +520,16 @@ func (c *Reconciler) runNextSchedulableTask(ctx context.Context, pr *v1beta1.Pip

resources.ApplyTaskResults(nextRprts, resolvedResultRefs)

tStatus := make(map[string]string)
for _, t := range pipelineRunFacts.State {
tStatus["context.pipelineRun.Tasks."+t.PipelineTask.Name] = t.GetTaskRunStatus(t.PipelineTask.Name, pipelineRunFacts)
}

// GetFinalTasks only returns tasks when a DAG is complete
nextRprts = append(nextRprts, pipelineRunFacts.GetFinalTasks()...)

resources.ApplyPipelineTaskContext(nextRprts, tStatus)

for _, rprt := range nextRprts {
if rprt == nil || rprt.Skip(pipelineRunFacts) {
continue
Expand Down
97 changes: 97 additions & 0 deletions pkg/reconciler/pipelinerun/pipelinerun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3973,6 +3973,103 @@ func TestReconcilePipeline_TaskSpecMetadata(t *testing.T) {
}
}

func TestReconciler_ReconcileKind_PipelineTaskContext(t *testing.T) {
names.TestingSeed()

pipelineName := "p-pipelinetask-status"
pipelineRunName := "pr-pipelinetask-status"

ps := []*v1beta1.Pipeline{tb.Pipeline(pipelineName, tb.PipelineNamespace("foo"), tb.PipelineSpec(
tb.PipelineTask("task1", "mytask"),
tb.FinalPipelineTask("finaltask", "finaltask",
tb.PipelineTaskParam("pipelineRun-Tasks-task1", "$(context.pipelineRun.Tasks.task1)"),
),
))}

prs := []*v1beta1.PipelineRun{tb.PipelineRun(pipelineRunName, tb.PipelineRunNamespace("foo"),
tb.PipelineRunSpec(pipelineName, tb.PipelineRunServiceAccountName("test-sa")),
)}

ts := []*v1beta1.Task{
tb.Task("mytask", tb.TaskNamespace("foo")),
tb.Task("finaltask", tb.TaskNamespace("foo"),
tb.TaskSpec(
tb.TaskParam("pipelineRun-Tasks-task1", v1beta1.ParamTypeString),
),
),
}

trs := []*v1beta1.TaskRun{
tb.TaskRun(pipelineRunName+"-task1-xxyyy",
tb.TaskRunNamespace("foo"),
tb.TaskRunOwnerReference("PipelineRun", pipelineRunName,
tb.OwnerReferenceAPIVersion("tekton.dev/v1beta1"),
tb.Controller, tb.BlockOwnerDeletion,
),
tb.TaskRunLabel("tekton.dev/pipeline", pipelineName),
tb.TaskRunLabel("tekton.dev/pipelineRun", pipelineRunName),
tb.TaskRunLabel("tekton.dev/pipelineTask", "task1"),
tb.TaskRunSpec(
tb.TaskRunTaskRef("mytask"),
tb.TaskRunServiceAccountName("test-sa"),
),
tb.TaskRunStatus(
tb.StatusCondition(
apis.Condition{
Type: apis.ConditionSucceeded,
Status: corev1.ConditionFalse,
Reason: v1beta1.TaskRunReasonFailed.String(),
},
),
),
),
}

d := test.Data{
PipelineRuns: prs,
Pipelines: ps,
Tasks: ts,
TaskRuns: trs,
}
prt := NewPipelineRunTest(d, t)
defer prt.Cancel()

_, clients := prt.reconcileRun("foo", pipelineRunName, []string{}, false)

expectedTaskRunName := pipelineRunName + "-finaltask-9l9zj"
expectedTaskRun := tb.TaskRun(expectedTaskRunName,
tb.TaskRunNamespace("foo"),
tb.TaskRunOwnerReference("PipelineRun", pipelineRunName,
tb.OwnerReferenceAPIVersion("tekton.dev/v1beta1"),
tb.Controller, tb.BlockOwnerDeletion,
),
tb.TaskRunLabel("tekton.dev/pipeline", pipelineName),
tb.TaskRunLabel("tekton.dev/pipelineRun", pipelineRunName),
tb.TaskRunLabel("tekton.dev/pipelineTask", "finaltask"),
tb.TaskRunSpec(
tb.TaskRunTaskRef("finaltask"),
tb.TaskRunServiceAccountName("test-sa"),
tb.TaskRunParam("pipelineRun-Tasks-task1", "Failed"),
),
)
// Check that the expected TaskRun was created
actual, err := clients.Pipeline.TektonV1beta1().TaskRuns("foo").List(prt.TestAssets.Ctx, metav1.ListOptions{
LabelSelector: "tekton.dev/pipelineTask=finaltask,tekton.dev/pipelineRun=" + pipelineRunName,
Limit: 1,
})

if err != nil {
t.Fatalf("Failure to list TaskRun's %s", err)
}
if len(actual.Items) != 1 {
t.Fatalf("Expected 1 TaskRuns got %d", len(actual.Items))
}
actualTaskRun := actual.Items[0]
if d := cmp.Diff(&actualTaskRun, expectedTaskRun, ignoreResourceVersion); d != "" {
t.Errorf("expected to see TaskRun %v created. Diff %s", expectedTaskRunName, diff.PrintWantGot(d))
}
}

// NewPipelineRunTest returns PipelineRunTest with a new PipelineRun controller created with specified state through data
// This PipelineRunTest can be reused for multiple PipelineRuns by calling reconcileRun for each pipelineRun
func NewPipelineRunTest(data test.Data, t *testing.T) *PipelineRunTest {
Expand Down
10 changes: 10 additions & 0 deletions pkg/reconciler/pipelinerun/resources/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ func ApplyTaskResults(targets PipelineRunState, resolvedResultRefs ResolvedResul
}
}

func ApplyPipelineTaskContext(state PipelineRunState, replacements map[string]string) {
for _, resolvedPipelineRunTask := range state {
if resolvedPipelineRunTask.PipelineTask != nil {
pipelineTask := resolvedPipelineRunTask.PipelineTask.DeepCopy()
pipelineTask.Params = replaceParamValues(pipelineTask.Params, replacements, nil)
resolvedPipelineRunTask.PipelineTask = pipelineTask
}
}
}

func ApplyWorkspaces(p *v1beta1.PipelineSpec, pr *v1beta1.PipelineRun) *v1beta1.PipelineSpec {
p = p.DeepCopy()
replacements := map[string]string{}
Expand Down
38 changes: 38 additions & 0 deletions pkg/reconciler/pipelinerun/resources/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,3 +609,41 @@ func TestApplyWorkspaces(t *testing.T) {
})
}
}

func TestApplyTaskRunContext(t *testing.T) {
r := map[string]string{
"context.pipelineRun.Tasks.task1": "Succeeded",
"context.pipelineRun.Tasks.task3": "Unknown",
}
state := PipelineRunState{{
PipelineTask: &v1beta1.PipelineTask{
Name: "task4",
TaskRef: &v1beta1.TaskRef{Name: "task"},
Params: []v1beta1.Param{{
Name: "task1",
Value: *v1beta1.NewArrayOrString("$(context.pipelineRun.Tasks.task1)"),
}, {
Name: "task3",
Value: *v1beta1.NewArrayOrString("$(context.pipelineRun.Tasks.task3)"),
}},
},
}}
expectedState := PipelineRunState{{
PipelineTask: &v1beta1.PipelineTask{
Name: "task4",
TaskRef: &v1beta1.TaskRef{Name: "task"},
Params: []v1beta1.Param{{
Name: "task1",
Value: *v1beta1.NewArrayOrString("Succeeded"),
}, {
Name: "task3",
Value: *v1beta1.NewArrayOrString("Unknown"),
}},
},
}}
ApplyPipelineTaskContext(state, r)
if d := cmp.Diff(expectedState, state); d != "" {
t.Fatalf("AppluTaskRunContext() %s", diff.PrintWantGot(d))
}

}
Loading

0 comments on commit ebdb86d

Please sign in to comment.