Skip to content

Commit

Permalink
Allow pipeline results to use custom task results
Browse files Browse the repository at this point in the history
This PR enables pipeline results to refer to pipeline tasks that run custom tasks and produce results.
  • Loading branch information
GregDritschler authored and tekton-robot committed Jan 19, 2021
1 parent 7e5ab1a commit f09f963
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 33 deletions.
2 changes: 1 addition & 1 deletion pkg/reconciler/pipelinerun/pipelinerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1beta1.PipelineRun, get
pr.Status.SkippedTasks = pipelineRunFacts.GetSkippedTasks()

if after.Status == corev1.ConditionTrue {
pr.Status.PipelineResults = resources.ApplyTaskResultsToPipelineResults(pipelineSpec.Results, pr.Status.TaskRuns)
pr.Status.PipelineResults = resources.ApplyTaskResultsToPipelineResults(pipelineSpec.Results, pr.Status.TaskRuns, pr.Status.Runs)
}

logger.Infof("PipelineRun %s status is being set to %s", pr.Name, after)
Expand Down
60 changes: 46 additions & 14 deletions pkg/reconciler/pipelinerun/resources/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,24 @@ func replaceParamValues(params []v1beta1.Param, stringReplacements map[string]st
return params
}

// ApplyTaskResultsToPipelineResults applies the results of completed TasksRuns to a Pipeline's
// ApplyTaskResultsToPipelineResults applies the results of completed TasksRuns and Runs to a Pipeline's
// list of PipelineResults, returning the computed set of PipelineRunResults. References to
// non-existent TaskResults or failed TaskRuns result in a PipelineResult being considered invalid
// non-existent TaskResults or failed TaskRuns or Runs result in a PipelineResult being considered invalid
// and omitted from the returned slice. A nil slice is returned if no results are passed in or all
// results are invalid.
func ApplyTaskResultsToPipelineResults(results []v1beta1.PipelineResult, taskRunStatuses map[string]*v1beta1.PipelineRunTaskRunStatus) []v1beta1.PipelineRunResult {
func ApplyTaskResultsToPipelineResults(
results []v1beta1.PipelineResult,
taskRunStatuses map[string]*v1beta1.PipelineRunTaskRunStatus,
runStatuses map[string]*v1beta1.PipelineRunRunStatus) []v1beta1.PipelineRunResult {

taskStatuses := map[string]*v1beta1.PipelineRunTaskRunStatus{}
for _, trStatus := range taskRunStatuses {
taskStatuses[trStatus.PipelineTaskName] = trStatus
}
customTaskStatuses := map[string]*v1beta1.PipelineRunRunStatus{}
for _, runStatus := range runStatuses {
customTaskStatuses[runStatus.PipelineTaskName] = runStatus
}

var runResults []v1beta1.PipelineRunResult = nil
stringReplacements := map[string]string{}
Expand All @@ -161,8 +169,16 @@ func ApplyTaskResultsToPipelineResults(results []v1beta1.PipelineResult, taskRun
if _, isMemoized := stringReplacements[variable]; isMemoized {
continue
}
if resultValue := taskResultValue(variable, taskStatuses); resultValue != nil {
stringReplacements[variable] = *resultValue
variableParts := strings.Split(variable, ".")
if len(variableParts) == 4 && variableParts[0] == "tasks" && variableParts[2] == "results" {
taskName, resultName := variableParts[1], variableParts[3]
if resultValue := taskResultValue(taskName, resultName, taskStatuses); resultValue != nil {
stringReplacements[variable] = *resultValue
} else if resultValue := runResultValue(taskName, resultName, customTaskStatuses); resultValue != nil {
stringReplacements[variable] = *resultValue
} else {
validPipelineResult = false
}
} else {
validPipelineResult = false
}
Expand All @@ -183,15 +199,9 @@ func ApplyTaskResultsToPipelineResults(results []v1beta1.PipelineResult, taskRun
return runResults
}

// taskResultValue returns a pointer to the result value for a given task result variable. A nil
// pointer is returned if the variable is invalid for any reason.
func taskResultValue(variable string, taskStatuses map[string]*v1beta1.PipelineRunTaskRunStatus) *string {
variableParts := strings.Split(variable, ".")
if len(variableParts) != 4 || variableParts[0] != "tasks" || variableParts[2] != "results" {
return nil
}

taskName, resultName := variableParts[1], variableParts[3]
// taskResultValue checks if a TaskRun result exists for a given pipeline task and result name.
// A nil pointer is returned if the variable is invalid for any reason.
func taskResultValue(taskName string, resultName string, taskStatuses map[string]*v1beta1.PipelineRunTaskRunStatus) *string {

status, taskExists := taskStatuses[taskName]
if !taskExists || status.Status == nil {
Expand All @@ -210,3 +220,25 @@ func taskResultValue(variable string, taskStatuses map[string]*v1beta1.PipelineR
}
return nil
}

// runResultValue checks if a Run result exists for a given pipeline task and result name.
// A nil pointer is returned if the variable is invalid for any reason.
func runResultValue(taskName string, resultName string, runStatuses map[string]*v1beta1.PipelineRunRunStatus) *string {

status, runExists := runStatuses[taskName]
if !runExists || status.Status == nil {
return nil
}

cond := status.Status.GetCondition(apis.ConditionSucceeded)
if cond == nil || cond.Status != corev1.ConditionTrue {
return nil
}

for _, runResult := range status.Status.Results {
if runResult.Name == resultName {
return &runResult.Value
}
}
return nil
}
163 changes: 162 additions & 1 deletion pkg/reconciler/pipelinerun/resources/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ import (
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
resourcev1alpha1 "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1"
runv1alpha1 "github.com/tektoncd/pipeline/pkg/apis/run/v1alpha1"
"github.com/tektoncd/pipeline/test/diff"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/selection"
"knative.dev/pkg/apis"
duckv1 "knative.dev/pkg/apis/duck/v1"
duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1"
)

Expand Down Expand Up @@ -619,6 +621,7 @@ func TestApplyTaskResultsToPipelineResults(t *testing.T) {
description string
results []v1beta1.PipelineResult
statuses map[string]*v1beta1.PipelineRunTaskRunStatus
runStatuses map[string]*v1beta1.PipelineRunRunStatus
expected []v1beta1.PipelineRunResult
}{{
description: "no-pipeline-results-no-returned-results",
Expand Down Expand Up @@ -874,9 +877,167 @@ func TestApplyTaskResultsToPipelineResults(t *testing.T) {
Name: "pipeline-result-2",
Value: "do, rae, mi, rae, do",
}},
}, {
description: "no-run-results-no-returned-results",
results: []v1beta1.PipelineResult{{
Name: "foo",
Value: "$(tasks.customtask.results.foo)",
}},
runStatuses: map[string]*v1beta1.PipelineRunRunStatus{
"task1": {
PipelineTaskName: "customtask",
Status: &runv1alpha1.RunStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: apis.ConditionSucceeded,
Status: corev1.ConditionTrue,
}},
},
RunStatusFields: runv1alpha1.RunStatusFields{
Results: []runv1alpha1.RunResult{},
},
},
},
},
expected: nil,
}, {
description: "wrong-customtask-name-no-returned-result",
results: []v1beta1.PipelineResult{{
Name: "foo",
Value: "$(tasks.customtask.results.foo)",
}},
runStatuses: map[string]*v1beta1.PipelineRunRunStatus{
"task1": {
PipelineTaskName: "differentcustomtask",
Status: &runv1alpha1.RunStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: apis.ConditionSucceeded,
Status: corev1.ConditionTrue,
}},
},
RunStatusFields: runv1alpha1.RunStatusFields{
Results: []runv1alpha1.RunResult{{
Name: "foo",
Value: "bar",
}},
},
},
},
},
expected: nil,
}, {
description: "right-customtask-name-wrong-result-name-no-returned-result",
results: []v1beta1.PipelineResult{{
Name: "foo",
Value: "$(tasks.customtask.results.foo)",
}},
runStatuses: map[string]*v1beta1.PipelineRunRunStatus{
"task1": {
PipelineTaskName: "customtask",
Status: &runv1alpha1.RunStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: apis.ConditionSucceeded,
Status: corev1.ConditionTrue,
}},
},
RunStatusFields: runv1alpha1.RunStatusFields{
Results: []runv1alpha1.RunResult{{
Name: "notfoo",
Value: "bar",
}},
},
},
},
},
expected: nil,
}, {
description: "unsuccessful-run-no-returned-result",
results: []v1beta1.PipelineResult{{
Name: "foo",
Value: "$(tasks.customtask.results.foo)",
}},
runStatuses: map[string]*v1beta1.PipelineRunRunStatus{
"task1": {
PipelineTaskName: "customtask",
Status: &runv1alpha1.RunStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: apis.ConditionSucceeded,
Status: corev1.ConditionFalse,
}},
},
RunStatusFields: runv1alpha1.RunStatusFields{
Results: []runv1alpha1.RunResult{{
Name: "foo",
Value: "bar",
}},
},
},
},
},
expected: nil,
}, {
description: "multiple-results-custom-and-normal-tasks",
results: []v1beta1.PipelineResult{{
Name: "pipeline-result-1",
Value: "$(tasks.customtask.results.foo)",
}, {
Name: "pipeline-result-2",
Value: "$(tasks.customtask.results.foo), $(tasks.normaltask.results.baz), $(tasks.customtask.results.bar), $(tasks.normaltask.results.baz), $(tasks.customtask.results.foo)",
}},
runStatuses: map[string]*v1beta1.PipelineRunRunStatus{
"task1": {
PipelineTaskName: "customtask",
Status: &runv1alpha1.RunStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: apis.ConditionSucceeded,
Status: corev1.ConditionTrue,
}},
},
RunStatusFields: runv1alpha1.RunStatusFields{
Results: []runv1alpha1.RunResult{{
Name: "foo",
Value: "do",
}, {
Name: "bar",
Value: "mi",
}},
},
},
},
},
statuses: map[string]*v1beta1.PipelineRunTaskRunStatus{
"task2": {
PipelineTaskName: "normaltask",
Status: &v1beta1.TaskRunStatus{
Status: duckv1beta1.Status{
Conditions: []apis.Condition{{
Type: apis.ConditionSucceeded,
Status: corev1.ConditionTrue,
}},
},
TaskRunStatusFields: v1beta1.TaskRunStatusFields{
TaskRunResults: []v1beta1.TaskRunResult{{
Name: "baz",
Value: "rae",
}},
},
},
},
},
expected: []v1beta1.PipelineRunResult{{
Name: "pipeline-result-1",
Value: "do",
}, {
Name: "pipeline-result-2",
Value: "do, rae, mi, rae, do",
}},
}} {
t.Run(tc.description, func(t *testing.T) {
received := ApplyTaskResultsToPipelineResults(tc.results, tc.statuses)
received := ApplyTaskResultsToPipelineResults(tc.results, tc.statuses, tc.runStatuses)
if d := cmp.Diff(tc.expected, received); d != "" {
t.Errorf(diff.PrintWantGot(d))
}
Expand Down
29 changes: 12 additions & 17 deletions test/custom_task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,23 +186,18 @@ func TestCustomTask(t *testing.T) {
}

// Validate that the pipeline's result reference to the custom task's result was resolved.
// This validation has been commented out because of a race condition.
// The pipelinerun reconciler updates the pipelinerun results AFTER the pipelinerun is marked DONE.
// So when the test case sees the pipelinerun is done, the results may or may not have been
// updated yet. The FUBAR logic in the pipelinerun reconciler needs to be fixed to not mark
// the PR done before all the status updates are made.

// expectedPipelineResults := []v1beta1.PipelineRunResult{{
// Name: "prResult",
// Value: "aResultValue",
// }}

// if len(pr.Status.PipelineResults) == 0 {
// t.Fatalf("Expected PipelineResults but there are none")
// }
// if d := cmp.Diff(expectedPipelineResults, pr.Status.PipelineResults); d != "" {
// t.Fatalf("Unexpected PipelineResults: %s", diff.PrintWantGot(d))
// }

expectedPipelineResults := []v1beta1.PipelineRunResult{{
Name: "prResult",
Value: "aResultValue",
}}

if len(pr.Status.PipelineResults) == 0 {
t.Fatalf("Expected PipelineResults but there are none")
}
if d := cmp.Diff(expectedPipelineResults, pr.Status.PipelineResults); d != "" {
t.Fatalf("Unexpected PipelineResults: %s", diff.PrintWantGot(d))
}
}

func skipIfCustomTasksDisabled(ctx context.Context, t *testing.T, c *clients, namespace string) {
Expand Down

0 comments on commit f09f963

Please sign in to comment.