-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Fix bug where PipelineRun emits task results that were never produced #3472
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,8 +18,11 @@ package resources | |
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" | ||
corev1 "k8s.io/api/core/v1" | ||
"knative.dev/pkg/apis" | ||
) | ||
|
||
// ApplyParameters applies the params from a PipelineRun.Params to a PipelineSpec. | ||
|
@@ -84,6 +87,8 @@ func ApplyTaskResults(targets PipelineRunState, resolvedResultRefs ResolvedResul | |
} | ||
} | ||
|
||
// ApplyWorkspaces replaces workspace variables in the given pipeline spec with their | ||
// concrete values. | ||
func ApplyWorkspaces(p *v1beta1.PipelineSpec, pr *v1beta1.PipelineRun) *v1beta1.PipelineSpec { | ||
p = p.DeepCopy() | ||
replacements := map[string]string{} | ||
|
@@ -124,3 +129,73 @@ func replaceParamValues(params []v1beta1.Param, stringReplacements map[string]st | |
} | ||
return params | ||
} | ||
|
||
// ApplyTaskResultsToPipelineResults applies the results of completed TasksRuns 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 | ||
// and omitted from the returned slice. A nil slice is returned if no results are passed in or all | ||
// results are invalid. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i apologize for probably having already asked this, but it seems weird to me that we just ignore the invalid values, feels like we should be returning an error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure it's quite right to say that we ignore the invalid values. We return a I don't totally agree that this is an error state but I don't feel strongly enough to dispute it. What should we do with that error when it's returned? In the old code it was silently ignored. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Returning an error might break There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should eventually move the execution of this logic to before the pipelinerun state is set. |
||
func ApplyTaskResultsToPipelineResults(results []v1beta1.PipelineResult, taskRunStatuses map[string]*v1beta1.PipelineRunTaskRunStatus) []v1beta1.PipelineRunResult { | ||
taskStatuses := map[string]*v1beta1.PipelineRunTaskRunStatus{} | ||
for _, trStatus := range taskRunStatuses { | ||
taskStatuses[trStatus.PipelineTaskName] = trStatus | ||
} | ||
|
||
var runResults []v1beta1.PipelineRunResult = nil | ||
stringReplacements := map[string]string{} | ||
for _, pipelineResult := range results { | ||
variablesInPipelineResult, _ := v1beta1.GetVarSubstitutionExpressionsForPipelineResult(pipelineResult) | ||
validPipelineResult := true | ||
for _, variable := range variablesInPipelineResult { | ||
if _, isMemoized := stringReplacements[variable]; isMemoized { | ||
continue | ||
} | ||
if resultValue := taskResultValue(variable, taskStatuses); resultValue != nil { | ||
stringReplacements[variable] = *resultValue | ||
} else { | ||
validPipelineResult = false | ||
} | ||
} | ||
if validPipelineResult { | ||
finalValue := pipelineResult.Value | ||
for variable, value := range stringReplacements { | ||
v := fmt.Sprintf("$(%s)", variable) | ||
finalValue = strings.Replace(finalValue, v, value, -1) | ||
} | ||
runResults = append(runResults, v1beta1.PipelineRunResult{ | ||
Name: pipelineResult.Name, | ||
Value: finalValue, | ||
}) | ||
} | ||
} | ||
|
||
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" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT: |
||
return nil | ||
} | ||
|
||
taskName, resultName := variableParts[1], variableParts[3] | ||
|
||
status, taskExists := taskStatuses[taskName] | ||
if !taskExists || status.Status == nil { | ||
return nil | ||
} | ||
|
||
cond := status.Status.GetCondition(apis.ConditionSucceeded) | ||
if cond == nil || cond.Status != corev1.ConditionTrue { | ||
return nil | ||
} | ||
|
||
for _, trResult := range status.Status.TaskRunResults { | ||
if trResult.Name == resultName { | ||
return &trResult.Value | ||
} | ||
} | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think as a user i might find it hard to reason about what will happen based on this description, maybe it would be clearer to flip it around a bit, and i think it might be more clear to a user to say something like "task" or "pipeline task" instead of "task run":
PipelineRun execution will fail if a pipeline result requires any of the following:
Task
and may fail aTaskRun
in future.I didn't include these ones b/c i'm not sure if they WILL have that same error:
TaskRun
referenced by thePipeline Result
failed. ThePipelineRun
will also have failed. <-- since execution will stop in this case, i dont think there would be an error related to the result?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I've updated the section to reference "Pipeline Task" throughout instead of "TaskRun".
Inre: your comments on PipelineRun execution behaviour, I think there are at least two discrete questions we're trying to answer in this secion of the doc:
I think that (1) is answered by the existing PR. Personally I think that (2) is independent of the results and shouldn't be documented in a section titled
Emitting Results from a Pipeline
since it's orthogonal: The Pipeline Results don't have any bearing on the execution status of the PipelineRun. That doesn't mean we shouldn't document it but it does seem to me a little out-of-scope to try and document PipelineRun execution state in a section dedicated to result data.PipelineRun execution doesn't fail when a Task is skipped and a result references it. This is kinda an example of why I'm a tad hesitant to include documentation about pipelinerun execution state in a section dedicated to result data. I think this will come back to bite us if we try and document it here. One day we'll change the behaviour of PipelineRuns and we'll have to remember that we documented pipelinerun execution in multiple places that aren't
pipelineruns.md
and then comb back through to make sure all those places reflect the changed behaviour. Personally I'd prefer that we keep the documentation inpipelines.md
as tightly focused as possible on how Pipeline Results behave.