diff --git a/docs/developers/README.md b/docs/developers/README.md index 8eb4fcb1253..3fb5e7a2d82 100644 --- a/docs/developers/README.md +++ b/docs/developers/README.md @@ -280,3 +280,52 @@ status: ``` Instead of hardcoding the path to the result file, the user can also use a variable. So `/tekton/results/current-date-unix-timestamp` can be replaced with: `$(results.current-date-unix-timestamp.path)`. This is more flexible if the path to result files ever changes. + +## How task results can be used in pipeline's tasks + +Now that we have tasks that can return a result, the user can refer to a task result in a pipeline by using the syntax +`$(tasks..results.)`. This will substitute the task result at the location of the variable. + +```yaml +apiVersion: tekton.dev/v1alpha1 +kind: Pipeline +metadata: + name: sum-and-multiply-pipeline + #... + tasks: + - name: sum-inputs + #... + - name: multiply-inputs + #... +- 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)" +``` + +This results in: + +```shell +tkn pipeline start sum-and-multiply-pipeline +? Value for param `a` of type `string`? (Default is `1`) 10 +? Value for param `b` of type `string`? (Default is `1`) 15 +Pipelinerun started: sum-and-multiply-pipeline-run-rgd9j + +In order to track the pipelinerun progress run: +tkn pipelinerun logs sum-and-multiply-pipeline-run-rgd9j -f -n default +``` + +```shell +tkn pipelinerun logs sum-and-multiply-pipeline-run-rgd9j -f -n default +[multiply-inputs : product] 150 + +[sum-inputs : sum] 25 + +[sum-and-multiply : sum] 30050 +``` + +As you can see, you can define multiple tasks in the same pipeline and use the result of more than one task inside another task parameter. The substitution is only done inside `pipeline.spec.tasks[].params[]`. For a complete example demonstrating Task Results in a Pipeline, see the [pipelinerun example](../examples/pipelineruns/task_results_example.yaml). diff --git a/docs/pipelines.md b/docs/pipelines.md index 79c5d6c6f44..0fa2170ab5b 100644 --- a/docs/pipelines.md +++ b/docs/pipelines.md @@ -403,6 +403,22 @@ tasks: name: echo-hello ``` +### Results + +Tasks can declare [results](./tasks.md#results) that they will emit during their execution. These results can be used as values for params in subsequent tasks of a Pipeline. Tekton will infer the ordering of these Tasks to ensure that the Task emitting the results runs before the Task consuming those results in its parameters. + +Using a Task result as a value for another Task's parameter is done with variable substitution. Here is what a Pipeline Task's param looks like with a result wired into it: + +```yaml +params: + - name: foo + value: "$(tasks.previous-task-name.results.bar-result)" +``` + +In this example the previous pipeline task has name "previous-task-name" and its result is declared in the Task definition as having name "bar-result". + +For a complete example demonstrating Task Results in a Pipeline see the [pipelinerun example](../examples/pipelineruns/task_results_example.yaml). + ## Ordering The [Pipeline Tasks](#pipeline-tasks) in a `Pipeline` can be connected and run diff --git a/examples/pipelineruns/task_results_example.yaml b/examples/pipelineruns/task_results_example.yaml new file mode 100644 index 00000000000..d635de86df6 --- /dev/null +++ b/examples/pipelineruns/task_results_example.yaml @@ -0,0 +1,93 @@ +apiVersion: tekton.dev/v1alpha1 +kind: Pipeline +metadata: + name: sum-and-multiply-pipeline +spec: + params: + - name: a + type: string + default: "1" + - name: b + type: string + default: "1" + 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)" + - 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/v1alpha1 +kind: Task +metadata: + name: sum + annotations: + description: | + A simple task that sums the two provided integers +spec: + inputs: + 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 $(( "$(inputs.params.a)" + "$(inputs.params.b)" )) | tee $(results.sum.path) +--- +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: multiply + annotations: + description: | + A simple task that multiplies the two provided integers +spec: + inputs: + 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 $(( "$(inputs.params.a)" * "$(inputs.params.b)" )) | tee $(results.product.path) diff --git a/pkg/apis/pipeline/v1alpha1/param_types_test.go b/pkg/apis/pipeline/v1alpha1/param_types_test.go index 4d875fe8820..344617b3847 100644 --- a/pkg/apis/pipeline/v1alpha1/param_types_test.go +++ b/pkg/apis/pipeline/v1alpha1/param_types_test.go @@ -24,6 +24,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/test/builder" ) @@ -187,3 +188,309 @@ func TestArrayOrString_MarshalJSON(t *testing.T) { } } } + +func TestNewResultReference(t *testing.T) { + type args struct { + param v1beta1.Param + } + tests := []struct { + name string + args args + want []*v1beta1.ResultRef + wantErr bool + }{ + { + name: "Test valid expression", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.sumTask.results.sumResult)", + }, + }, + }, + want: []*v1beta1.ResultRef{ + { + PipelineTask: "sumTask", + Result: "sumResult", + }, + }, + wantErr: false, + }, { + name: "Test valid expression: substitution within string", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "sum-will-go-here -> $(tasks.sumTask.results.sumResult)", + }, + }, + }, + want: []*v1beta1.ResultRef{ + { + PipelineTask: "sumTask", + Result: "sumResult", + }, + }, + wantErr: false, + }, { + name: "Test valid expression: multiple substitution", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.sumTask1.results.sumResult) and another $(tasks.sumTask2.results.sumResult)", + }, + }, + }, + want: []*v1beta1.ResultRef{ + { + PipelineTask: "sumTask1", + Result: "sumResult", + }, { + PipelineTask: "sumTask2", + Result: "sumResult", + }, + }, + wantErr: false, + }, { + name: "Test invalid expression: first separator typo", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(task.sumTasks.results.sumResult)", + }, + }, + }, + want: nil, + wantErr: true, + }, { + name: "Test invalid expression: third separator typo", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.sumTasks.result.sumResult)", + }, + }, + }, + want: nil, + wantErr: true, + }, { + name: "Test invalid expression: param substitution shouldn't be considered result ref", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(params.paramName)", + }, + }, + }, + want: nil, + wantErr: true, + }, { + name: "Test invalid expression: One bad and good result substitution", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "good -> $(tasks.sumTask1.results.sumResult) bad-> $(task.sumTask2.results.sumResult)", + }, + }, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := v1beta1.NewResultRefs(tt.args.param) + if tt.wantErr != (err != nil) { + t.Errorf("TestNewResultReference/%s wantErr %v, error = %v", tt.name, tt.wantErr, err) + return + } + if d := cmp.Diff(tt.want, got); d != "" { + t.Errorf("TestNewResultReference/%s (-want, +got) = %v", tt.name, d) + } + }) + } +} + +func TestHasResultReference(t *testing.T) { + type args struct { + param v1beta1.Param + } + tests := []struct { + name string + args args + wantRef []*v1beta1.ResultRef + }{ + { + name: "Test valid expression", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.sumTask.results.sumResult)", + }, + }, + }, + wantRef: []*v1beta1.ResultRef{ + { + PipelineTask: "sumTask", + Result: "sumResult", + }, + }, + }, { + name: "Test valid expression with dashes", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.sum-task.results.sum-result)", + }, + }, + }, + wantRef: []*v1beta1.ResultRef{ + { + PipelineTask: "sum-task", + Result: "sum-result", + }, + }, + }, { + name: "Test invalid expression: param substitution shouldn't be considered result ref", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(params.paramName)", + }, + }, + }, + wantRef: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _ := v1beta1.NewResultRefs(tt.args.param) + if d := cmp.Diff(tt.wantRef, got); d != "" { + t.Errorf("TestHasResultReference/%s (-want, +got) = %v", tt.name, d) + } + }) + } +} + +func TestLooksLikeResultRef(t *testing.T) { + type args struct { + param v1beta1.Param + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "test expression that is a result ref", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.sumTasks.results.sumResult)", + }, + }, + }, + want: true, + }, { + name: "test expression: looks like result ref, but typo in 'task' separator", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(task.sumTasks.results.sumResult)", + }, + }, + }, + want: true, + }, { + name: "test expression: looks like result ref, but typo in 'results' separator", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.sumTasks.result.sumResult)", + }, + }, + }, + want: true, + }, { + name: "test expression: missing 'task' separator", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(sumTasks.results.sumResult)", + }, + }, + }, + want: false, + }, { + name: "test expression: missing variable substitution", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "tasks.sumTasks.results.sumResult", + }, + }, + }, + want: false, + }, { + name: "test expression: param substitution shouldn't be considered result ref", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(params.someParam)", + }, + }, + }, + want: false, + }, { + name: "test expression: one good ref, one bad one should return true", + args: args{ + param: v1beta1.Param{ + Name: "param", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.sumTasks.results.sumResult) $(task.sumTasks.results.sumResult)", + }, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := v1beta1.LooksLikeContainsResultRefs(tt.args.param); got != tt.want { + t.Errorf("LooksLikeContainsResultRefs() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1alpha1/pipeline_types.go b/pkg/apis/pipeline/v1alpha1/pipeline_types.go index dae4d65dba4..c0261d65579 100644 --- a/pkg/apis/pipeline/v1alpha1/pipeline_types.go +++ b/pkg/apis/pipeline/v1alpha1/pipeline_types.go @@ -158,6 +158,14 @@ func (pt PipelineTask) Deps() []string { deps = append(deps, rd.From...) } } + // Add any dependents from task results + for _, param := range pt.Params { + if resultRefs, err := v1beta1.NewResultRefs(param); err == nil { + for _, resultRef := range resultRefs { + deps = append(deps, resultRef.PipelineTask) + } + } + } return deps } diff --git a/pkg/apis/pipeline/v1alpha1/pipeline_validation.go b/pkg/apis/pipeline/v1alpha1/pipeline_validation.go index 5b5cb0efbe4..fa6b170c084 100644 --- a/pkg/apis/pipeline/v1alpha1/pipeline_validation.go +++ b/pkg/apis/pipeline/v1alpha1/pipeline_validation.go @@ -21,6 +21,7 @@ import ( "fmt" "strings" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/apis/validate" "github.com/tektoncd/pipeline/pkg/list" "github.com/tektoncd/pipeline/pkg/reconciler/pipeline/dag" @@ -128,6 +129,20 @@ func validateGraph(tasks []PipelineTask) error { return nil } +// validateParamResults ensure that task result variables are properly configured +func validateParamResults(tasks []PipelineTask) error { + for _, task := range tasks { + for _, param := range task.Params { + if v1beta1.LooksLikeContainsResultRefs(param) { + if _, err := v1beta1.NewResultRefs(param); err != nil { + return err + } + } + } + } + return nil +} + // Validate checks that taskNames in the Pipeline are valid and that the graph // of Tasks expressed in the Pipeline makes sense. func (ps *PipelineSpec) Validate(ctx context.Context) *apis.FieldError { @@ -192,6 +207,10 @@ func (ps *PipelineSpec) Validate(ctx context.Context) *apis.FieldError { return apis.ErrInvalidValue(err.Error(), "spec.tasks") } + if err := validateParamResults(ps.Tasks); err != nil { + return apis.ErrInvalidValue(err.Error(), "spec.tasks.params.value") + } + // The parameter variables should be valid if err := validatePipelineParameterVariables(ps.Tasks, ps.Params); err != nil { return err diff --git a/pkg/apis/pipeline/v1alpha1/pipeline_validation_test.go b/pkg/apis/pipeline/v1alpha1/pipeline_validation_test.go index 167be62aaff..1b3188d0141 100644 --- a/pkg/apis/pipeline/v1alpha1/pipeline_validation_test.go +++ b/pkg/apis/pipeline/v1alpha1/pipeline_validation_test.go @@ -356,6 +356,15 @@ func TestPipeline_Validate(t *testing.T) { tb.PipelineWorkspaceDeclaration("foo"), )), failureExpected: true, + }, { + name: "task params results malformed variable substitution expression", + p: tb.Pipeline("name", "namespace", tb.PipelineSpec( + tb.PipelineTask("a-task", "a-task"), + tb.PipelineTask("b-task", "b-task", + tb.PipelineTaskParam("b-param", "$(tasks.a-task.resultTypo.bResult)"), + ), + )), + failureExpected: true, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/apis/pipeline/v1beta1/param_types.go b/pkg/apis/pipeline/v1beta1/param_types.go index 0ec2c2774f9..2f436e44cf4 100644 --- a/pkg/apis/pipeline/v1beta1/param_types.go +++ b/pkg/apis/pipeline/v1beta1/param_types.go @@ -20,6 +20,8 @@ import ( "context" "encoding/json" "fmt" + "regexp" + "strings" resource "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1" ) @@ -124,3 +126,93 @@ func (arrayOrString *ArrayOrString) ApplyReplacements(stringReplacements map[str arrayOrString.ArrayVal = newArrayVal } } + +// ResultRef is a type that represents a reference to a task run result +type ResultRef struct { + PipelineTask string + Result string +} + +const ( + resultExpressionFormat = "tasks..results." + // ResultTaskPart Constant used to define the "tasks" part of a pipeline result reference + ResultTaskPart = "tasks" + // ResultResultPart Constant used to define the "results" part of a pipeline result reference + ResultResultPart = "results" + variableSubstitutionFormat = `\$\([A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\)` +) + +var variableSubstitutionRegex = regexp.MustCompile(variableSubstitutionFormat) + +// NewResultRefs extracts all ResultReferences from param. +// If the ResultReference can be extracted, they are returned. Otherwise an error is returned +func NewResultRefs(param Param) ([]*ResultRef, error) { + substitutionExpressions, ok := getVarSubstitutionExpressions(param) + if !ok { + return nil, fmt.Errorf("Invalid result reference expression: must contain variable substitution %q", resultExpressionFormat) + } + var resultRefs []*ResultRef + for _, expression := range substitutionExpressions { + pipelineTask, result, err := parseExpression(expression) + if err != nil { + return nil, fmt.Errorf("Invalid result reference expression: %v", err) + } + resultRefs = append(resultRefs, &ResultRef{ + PipelineTask: pipelineTask, + Result: result, + }) + } + return resultRefs, nil +} + +// LooksLikeContainsResultRefs attempts to check if param looks like it contains any +// result references. +// This is useful if we want to make sure the param looks like a ResultReference before +// performing strict validation +func LooksLikeContainsResultRefs(param Param) bool { + if param.Value.Type != ParamTypeString { + return false + } + extractedExpressions, ok := getVarSubstitutionExpressions(param) + if !ok { + return false + } + for _, expression := range extractedExpressions { + if looksLikeResultRef(expression) { + return true + } + } + return false +} + +func looksLikeResultRef(expression string) bool { + return strings.HasPrefix(expression, "task") && strings.Contains(expression, ".result") +} + +// getVarSubstitutionExpressions extracts all the value between "$(" and ")"" +func getVarSubstitutionExpressions(param Param) ([]string, bool) { + if param.Value.Type != ParamTypeString { + return nil, false + } + expressions := variableSubstitutionRegex.FindAllString(param.Value.StringVal, -1) + if expressions == nil { + return nil, false + } + var allExpressions []string + for _, expression := range expressions { + allExpressions = append(allExpressions, stripVarSubExpression(expression)) + } + return allExpressions, true +} + +func stripVarSubExpression(expression string) string { + return strings.TrimSuffix(strings.TrimPrefix(expression, "$("), ")") +} + +func parseExpression(substitutionExpression string) (string, string, error) { + subExpressions := strings.Split(substitutionExpression, ".") + if len(subExpressions) != 4 || subExpressions[0] != ResultTaskPart || subExpressions[2] != ResultResultPart { + return "", "", fmt.Errorf("Must be of the form %q", resultExpressionFormat) + } + return subExpressions[1], subExpressions[3], nil +} diff --git a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go index 9fec10b10e2..e629bd49efb 100644 --- a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go @@ -933,6 +933,22 @@ func (in *PipelineTaskRun) DeepCopy() *PipelineTaskRun { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResultRef) DeepCopyInto(out *ResultRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResultRef. +func (in *ResultRef) DeepCopy() *ResultRef { + if in == nil { + return nil + } + out := new(ResultRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SidecarState) DeepCopyInto(out *SidecarState) { *out = *in diff --git a/pkg/reconciler/pipeline/dag/dag_test.go b/pkg/reconciler/pipeline/dag/dag_test.go index 18572332614..b58cadc9732 100644 --- a/pkg/reconciler/pipeline/dag/dag_test.go +++ b/pkg/reconciler/pipeline/dag/dag_test.go @@ -22,6 +22,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/list" "github.com/tektoncd/pipeline/pkg/reconciler/pipeline/dag" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -571,3 +572,93 @@ func TestBuild_ConditionResources(t *testing.T) { } assertSameDAG(t, expectedDAG, g) } + +func TestBuild_TaskParamsFromTaskResults(t *testing.T) { + a := v1alpha1.PipelineTask{Name: "a"} + b := v1alpha1.PipelineTask{Name: "b"} + c := v1alpha1.PipelineTask{Name: "c"} + d := v1alpha1.PipelineTask{Name: "d"} + e := v1alpha1.PipelineTask{Name: "e"} + xDependsOnA := v1alpha1.PipelineTask{ + Name: "x", + Params: []v1alpha1.Param{ + { + Name: "paramX", + Value: v1beta1.ArrayOrString{ + Type: v1alpha1.ParamTypeString, + StringVal: "$(tasks.a.results.resultA)", + }, + }, + }, + } + yDependsOnBRunsAfterC := v1alpha1.PipelineTask{ + Name: "y", + RunAfter: []string{"c"}, + Params: []v1alpha1.Param{ + { + Name: "paramB", + Value: v1beta1.ArrayOrString{ + Type: v1alpha1.ParamTypeString, + StringVal: "$(tasks.b.results.resultB)", + }, + }, + }, + } + zDependsOnDAndE := v1alpha1.PipelineTask{ + Name: "z", + Params: []v1alpha1.Param{ + { + Name: "paramZ", + Value: v1beta1.ArrayOrString{ + Type: v1alpha1.ParamTypeString, + StringVal: "$(tasks.d.results.resultD) $(tasks.e.results.resultE)", + }, + }, + }, + } + + // a b c d e + // | \ / \ / + // x y z + nodeA := &dag.Node{Task: a} + nodeB := &dag.Node{Task: b} + nodeC := &dag.Node{Task: c} + nodeD := &dag.Node{Task: d} + nodeE := &dag.Node{Task: e} + nodeX := &dag.Node{Task: xDependsOnA} + nodeY := &dag.Node{Task: yDependsOnBRunsAfterC} + nodeZ := &dag.Node{Task: zDependsOnDAndE} + + nodeA.Next = []*dag.Node{nodeX} + nodeB.Next = []*dag.Node{nodeY} + nodeC.Next = []*dag.Node{nodeY} + nodeD.Next = []*dag.Node{nodeZ} + nodeE.Next = []*dag.Node{nodeZ} + nodeX.Prev = []*dag.Node{nodeA} + nodeY.Prev = []*dag.Node{nodeB, nodeC} + nodeZ.Prev = []*dag.Node{nodeD, nodeE} + + expectedDAG := &dag.Graph{ + Nodes: map[string]*dag.Node{ + "a": nodeA, + "b": nodeB, + "c": nodeC, + "d": nodeD, + "e": nodeE, + "x": nodeX, + "y": nodeY, + "z": nodeZ, + }, + } + p := &v1alpha1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, + Spec: v1alpha1.PipelineSpec{ + Tasks: []v1alpha1.PipelineTask{a, b, c, d, e, xDependsOnA, yDependsOnBRunsAfterC, zDependsOnDAndE}, + }, + } + g, err := dag.Build(v1alpha1.PipelineTaskList(p.Spec.Tasks)) + if err != nil { + t.Fatalf("didn't expect error creating valid Pipeline %v but got %v", p, err) + } + assertSameDAG(t, expectedDAG, g) +} diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index cbd410d0e09..e52481e7ec4 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -441,7 +441,19 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1alpha1.PipelineRun) er c.Logger.Errorf("Error getting potential next tasks for valid pipelinerun %s: %v", pr.Name, err) } - rprts := pipelineState.GetNextTasks(candidateTasks) + nextRprts := pipelineState.GetNextTasks(candidateTasks) + resolvedResultRefs, err := resources.ResolveResultRefs(pipelineState, nextRprts) + if err != nil { + c.Logger.Infof("Failed to resolve all task params for %q with error %v", pr.Name, err) + pr.Status.SetCondition(&apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + Reason: ReasonFailedValidation, + Message: err.Error(), + }) + return nil + } + resources.ApplyTaskResults(nextRprts, resolvedResultRefs) var as artifacts.ArtifactStorageInterface @@ -450,7 +462,7 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1alpha1.PipelineRun) er return err } - for _, rprt := range rprts { + for _, rprt := range nextRprts { if rprt == nil { continue } diff --git a/pkg/reconciler/pipelinerun/pipelinerun_test.go b/pkg/reconciler/pipelinerun/pipelinerun_test.go index 01da170c50b..52bd6e0bbfa 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/pipelinerun/pipelinerun_test.go @@ -1679,3 +1679,100 @@ func ensurePVCCreated(t *testing.T, clients test.Clients, name, namespace string t.Errorf("Expected to see volume resource PVC created but didn't") } } + +func TestReconcileWithTaskResults(t *testing.T) { + names.TestingSeed() + ps := []*v1alpha1.Pipeline{tb.Pipeline("test-pipeline", "foo", tb.PipelineSpec( + tb.PipelineTask("a-task", "a-task"), + tb.PipelineTask("b-task", "b-task", + tb.PipelineTaskParam("bParam", "$(tasks.a-task.results.aResult)"), + ), + ))} + prs := []*v1alpha1.PipelineRun{tb.PipelineRun("test-pipeline-run-different-service-accs", "foo", + tb.PipelineRunSpec("test-pipeline", + tb.PipelineRunServiceAccountName("test-sa-0"), + ), + )} + ts := []*v1alpha1.Task{ + tb.Task("a-task", "foo"), + tb.Task("b-task", "foo", + tb.TaskSpec( + tb.TaskInputs(tb.InputsParamSpec("bParam", v1alpha1.ParamTypeString)), + ), + ), + } + trs := []*v1alpha1.TaskRun{ + tb.TaskRun("test-pipeline-run-different-service-accs-a-task-9l9zj", "foo", + tb.TaskRunOwnerReference("PipelineRun", "test-pipeline-run-different-service-accs", + tb.OwnerReferenceAPIVersion("tekton.dev/v1alpha1"), + tb.Controller, tb.BlockOwnerDeletion, + ), + tb.TaskRunLabel("tekton.dev/pipeline", "test-pipeline"), + tb.TaskRunLabel("tekton.dev/pipelineRun", "test-pipeline-run-different-service-accs"), + tb.TaskRunLabel("tekton.dev/pipelineTask", "a-task"), + tb.TaskRunSpec( + tb.TaskRunTaskRef("hello-world"), + tb.TaskRunServiceAccountName("test-sa"), + ), + tb.TaskRunStatus( + tb.StatusCondition( + apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + ), + tb.TaskRunResult("aResult", "aResultValue"), + ), + ), + } + d := test.Data{ + PipelineRuns: prs, + Pipelines: ps, + Tasks: ts, + TaskRuns: trs, + } + testAssets, cancel := getPipelineRunController(t, d) + defer cancel() + c := testAssets.Controller + clients := testAssets.Clients + err := c.Reconciler.Reconcile(context.Background(), "foo/test-pipeline-run-different-service-accs") + if err != nil { + t.Errorf("Did not expect to see error when reconciling completed PipelineRun but saw %s", err) + } + // Check that the PipelineRun was reconciled correctly + _, err = clients.Pipeline.TektonV1alpha1().PipelineRuns("foo").Get("test-pipeline-run-different-service-accs", metav1.GetOptions{}) + if err != nil { + t.Fatalf("Somehow had error getting completed reconciled run out of fake client: %s", err) + } + expectedTaskRunName := "test-pipeline-run-different-service-accs-b-task-mz4c7" + expectedTaskRun := tb.TaskRun(expectedTaskRunName, "foo", + tb.TaskRunOwnerReference("PipelineRun", "test-pipeline-run-different-service-accs", + tb.OwnerReferenceAPIVersion("tekton.dev/v1alpha1"), + tb.Controller, tb.BlockOwnerDeletion, + ), + tb.TaskRunLabel("tekton.dev/pipeline", "test-pipeline"), + tb.TaskRunLabel("tekton.dev/pipelineRun", "test-pipeline-run-different-service-accs"), + tb.TaskRunLabel("tekton.dev/pipelineTask", "b-task"), + tb.TaskRunSpec( + tb.TaskRunTaskRef("b-task"), + tb.TaskRunServiceAccountName("test-sa-0"), + tb.TaskRunParam("bParam", "aResultValue"), + ), + ) + // Check that the expected TaskRun was created + actual, err := clients.Pipeline.TektonV1alpha1().TaskRuns("foo").List(metav1.ListOptions{ + LabelSelector: "tekton.dev/pipelineTask=b-task,tekton.dev/pipelineRun=test-pipeline-run-different-service-accs", + 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); d != "" { + t.Errorf("expected to see TaskRun %v created. Diff %s", expectedTaskRunName, d) + } +} diff --git a/pkg/reconciler/pipelinerun/resources/apply.go b/pkg/reconciler/pipelinerun/resources/apply.go index 45e7c853f11..3b7a6a2a287 100644 --- a/pkg/reconciler/pipelinerun/resources/apply.go +++ b/pkg/reconciler/pipelinerun/resources/apply.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" ) // ApplyParameters applies the params from a PipelineRun.Params to a PipelineSpec. @@ -53,6 +54,22 @@ func ApplyParameters(p *v1alpha1.PipelineSpec, pr *v1alpha1.PipelineRun) *v1alph return ApplyReplacements(p, stringReplacements, arrayReplacements) } +// ApplyTaskResults applies the ResolvedResultRef to each PipelineTask.Params in targets +func ApplyTaskResults(targets PipelineRunState, resolvedResultRefs ResolvedResultRefs) { + stringReplacements := map[string]string{} + + for _, resolvedResultRef := range resolvedResultRefs { + replaceTarget := fmt.Sprintf("%s.%s.%s.%s", v1beta1.ResultTaskPart, resolvedResultRef.ResultReference.PipelineTask, v1beta1.ResultResultPart, resolvedResultRef.ResultReference.Result) + stringReplacements[replaceTarget] = resolvedResultRef.Value.StringVal + } + + for _, resolvedPipelineRunTask := range targets { + pipelineTask := resolvedPipelineRunTask.PipelineTask.DeepCopy() + pipelineTask.Params = replaceParamValues(pipelineTask.Params, stringReplacements, nil) + resolvedPipelineRunTask.PipelineTask = pipelineTask + } +} + // ApplyReplacements replaces placeholders for declared parameters with the specified replacements. func ApplyReplacements(p *v1alpha1.PipelineSpec, replacements map[string]string, arrayReplacements map[string][]string) *v1alpha1.PipelineSpec { p = p.DeepCopy() diff --git a/pkg/reconciler/pipelinerun/resources/apply_test.go b/pkg/reconciler/pipelinerun/resources/apply_test.go index d2f1a9fc7f7..72e2f5bac63 100644 --- a/pkg/reconciler/pipelinerun/resources/apply_test.go +++ b/pkg/reconciler/pipelinerun/resources/apply_test.go @@ -22,6 +22,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" tb "github.com/tektoncd/pipeline/test/builder" ) @@ -139,3 +140,149 @@ func TestApplyParameters(t *testing.T) { }) } } + +func TestApplyTaskResults_MinimalExpression(t *testing.T) { + type args struct { + targets PipelineRunState + resolvedResultRefs ResolvedResultRefs + } + tests := []struct { + name string + args args + want PipelineRunState + }{ + { + name: "Test result substitution on minimal variable substitution expression", + args: args{ + resolvedResultRefs: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, + }, + targets: PipelineRunState{ + { + PipelineTask: &v1alpha1.PipelineTask{ + Name: "bTask", + TaskRef: &v1alpha1.TaskRef{Name: "bTask"}, + Params: []v1beta1.Param{ + { + Name: "bParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.aTask.results.aResult)", + }, + }, + }, + }, + }, + }, + }, + want: PipelineRunState{ + { + PipelineTask: &v1alpha1.PipelineTask{ + Name: "bTask", + TaskRef: &v1alpha1.TaskRef{Name: "bTask"}, + Params: []v1beta1.Param{ + { + Name: "bParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ApplyTaskResults(tt.args.targets, tt.args.resolvedResultRefs) + if d := cmp.Diff(tt.args.targets, tt.want); d != "" { + t.Fatalf("ApplyTaskResults() -want, +got: %v", d) + } + }) + } +} + +func TestApplyTaskResults_EmbeddedExpression(t *testing.T) { + type args struct { + targets PipelineRunState + resolvedResultRefs ResolvedResultRefs + } + tests := []struct { + name string + args args + want PipelineRunState + }{ + { + name: "Test result substitution on embedded variable substitution expression", + args: args{ + resolvedResultRefs: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, + }, + targets: PipelineRunState{ + { + PipelineTask: &v1alpha1.PipelineTask{ + Name: "bTask", + TaskRef: &v1alpha1.TaskRef{Name: "bTask"}, + Params: []v1beta1.Param{ + { + Name: "bParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "Result value --> $(tasks.aTask.results.aResult)", + }, + }, + }, + }, + }, + }, + }, + want: PipelineRunState{ + { + PipelineTask: &v1alpha1.PipelineTask{ + Name: "bTask", + TaskRef: &v1alpha1.TaskRef{Name: "bTask"}, + Params: []v1beta1.Param{ + { + Name: "bParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "Result value --> aResultValue", + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ApplyTaskResults(tt.args.targets, tt.args.resolvedResultRefs) + if d := cmp.Diff(tt.args.targets, tt.want); d != "" { + t.Fatalf("ApplyTaskResults() -want, +got: %v", d) + } + }) + } +} diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go index f3025f20541..fe20f4e29d0 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go @@ -128,7 +128,8 @@ func (t ResolvedPipelineRunTask) IsCancelled() bool { return c.IsFalse() && c.Reason == v1alpha1.TaskRunSpecStatusCancelled } -func (state PipelineRunState) toMap() map[string]*ResolvedPipelineRunTask { +// ToMap returns a map that maps pipeline task name to the resolved pipeline run task +func (state PipelineRunState) ToMap() map[string]*ResolvedPipelineRunTask { m := make(map[string]*ResolvedPipelineRunTask) for _, rprt := range state { m[rprt.PipelineTask.Name] = rprt @@ -431,7 +432,7 @@ func GetPipelineConditionStatus(pr *v1alpha1.PipelineRun, state PipelineRunState if rprt.IsSuccessful() { successOrSkipTasks = append(successOrSkipTasks, rprt.PipelineTask.Name) } - if isSkipped(rprt, state.toMap(), dag) { + if isSkipped(rprt, state.ToMap(), dag) { skipTasks++ successOrSkipTasks = append(successOrSkipTasks, rprt.PipelineTask.Name) } diff --git a/pkg/reconciler/pipelinerun/resources/resultrefresolution.go b/pkg/reconciler/pipelinerun/resources/resultrefresolution.go new file mode 100644 index 00000000000..e7054b4a203 --- /dev/null +++ b/pkg/reconciler/pipelinerun/resources/resultrefresolution.go @@ -0,0 +1,138 @@ +/* +Copyright 2019 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "fmt" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" +) + +// ResolvedResultRefs represents all of the ResolvedResultRef for a pipeline task +type ResolvedResultRefs []*ResolvedResultRef + +// ResolvedResultRef represents a result ref reference that has been fully resolved (value has been populated). +// If the value is from a Result, then the ResultReference will be populated to point to the ResultReference +// which resulted in the value +type ResolvedResultRef struct { + Value v1beta1.ArrayOrString + ResultReference v1beta1.ResultRef + FromTaskRun string +} + +// ResolveResultRefs resolves any ResultReference that are found in the target ResolvedPipelineRunTask +func ResolveResultRefs(pipelineRunState PipelineRunState, targets PipelineRunState) (ResolvedResultRefs, error) { + var allResolvedResultRefs ResolvedResultRefs + for _, target := range targets { + resolvedResultRefs, err := convertParamsToResultRefs(pipelineRunState, target) + if err != nil { + return nil, err + } + allResolvedResultRefs = append(allResolvedResultRefs, resolvedResultRefs...) + } + return removeDup(allResolvedResultRefs), nil +} + +// extractResultRefsFromParam resolves any ResultReference that are found in param +// Returns nil if none are found +func extractResultRefsFromParam(pipelineRunState PipelineRunState, param v1beta1.Param) (ResolvedResultRefs, error) { + if resultRefs, err := v1beta1.NewResultRefs(param); err == nil { + var resolvedResultRefs ResolvedResultRefs + for _, resultRef := range resultRefs { + resolvedResultRef, err := resolveResultRef(pipelineRunState, resultRef) + if err != nil { + return nil, err + } + resolvedResultRefs = append(resolvedResultRefs, resolvedResultRef) + } + return removeDup(resolvedResultRefs), nil + } + return nil, nil +} + +func removeDup(refs ResolvedResultRefs) ResolvedResultRefs { + if refs == nil { + return nil + } + resolvedResultRefByRef := make(map[v1beta1.ResultRef]*ResolvedResultRef) + for _, resolvedResultRef := range refs { + resolvedResultRefByRef[resolvedResultRef.ResultReference] = resolvedResultRef + } + deduped := make([]*ResolvedResultRef, 0, len(resolvedResultRefByRef)) + + for _, ressolvedResultRef := range resolvedResultRefByRef { + deduped = append(deduped, ressolvedResultRef) + } + return deduped +} + +// convertParamsToResultRefs converts all params of the resolved pipeline run task +func convertParamsToResultRefs(pipelineRunState PipelineRunState, target *ResolvedPipelineRunTask) (ResolvedResultRefs, error) { + var resolvedParams ResolvedResultRefs + for _, param := range target.PipelineTask.Params { + resolvedResultRefs, err := extractResultRefsFromParam(pipelineRunState, param) + if err != nil { + return nil, fmt.Errorf("unable to find result referenced by param %q in pipeline task %q: %w", param.Name, target.PipelineTask.Name, err) + } + if resolvedResultRefs != nil { + resolvedParams = append(resolvedParams, resolvedResultRefs...) + } + } + return resolvedParams, nil +} + +func resolveResultRef(pipelineState PipelineRunState, resultRef *v1beta1.ResultRef) (*ResolvedResultRef, error) { + referencedTaskRun, err := getReferencedTaskRun(pipelineState, resultRef) + if err != nil { + return nil, err + } + result, err := findTaskResultForParam(referencedTaskRun, resultRef) + if err != nil { + return nil, err + } + return &ResolvedResultRef{ + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: result.Value, + }, + FromTaskRun: referencedTaskRun.Name, + ResultReference: *resultRef, + }, nil +} + +func getReferencedTaskRun(pipelineState PipelineRunState, reference *v1beta1.ResultRef) (*v1alpha1.TaskRun, error) { + referencedPipelineTask := pipelineState.ToMap()[reference.PipelineTask] + + if referencedPipelineTask == nil { + return nil, fmt.Errorf("could not find task %q referenced by result", reference.PipelineTask) + } + if referencedPipelineTask.TaskRun == nil || referencedPipelineTask.IsFailure() { + return nil, fmt.Errorf("could not find successful taskrun for task %q", referencedPipelineTask.PipelineTask.Name) + } + return referencedPipelineTask.TaskRun, nil +} + +func findTaskResultForParam(taskRun *v1alpha1.TaskRun, reference *v1beta1.ResultRef) (*v1alpha1.TaskRunResult, error) { + results := taskRun.Status.TaskRunStatusFields.TaskRunResults + for _, result := range results { + if result.Name == reference.Result { + return &result, nil + } + } + return nil, fmt.Errorf("Could not find result with name %s for task run %s", reference.Result, reference.PipelineTask) +} diff --git a/pkg/reconciler/pipelinerun/resources/resultrefresolution_test.go b/pkg/reconciler/pipelinerun/resources/resultrefresolution_test.go new file mode 100644 index 00000000000..16d6d02aca7 --- /dev/null +++ b/pkg/reconciler/pipelinerun/resources/resultrefresolution_test.go @@ -0,0 +1,354 @@ +package resources + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + tb "github.com/tektoncd/pipeline/test/builder" +) + +func TestTaskParamResolver_ResolveResultRefs(t *testing.T) { + type fields struct { + pipelineRunState PipelineRunState + } + type args struct { + param v1beta1.Param + } + tests := []struct { + name string + fields fields + args args + want ResolvedResultRefs + wantErr bool + }{ + { + name: "successful resolution: param not using result reference", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + TaskRunName: "aTaskRun", + TaskRun: tb.TaskRun("aTaskRun", "namespace"), + PipelineTask: &v1alpha1.PipelineTask{ + Name: "aTask", + TaskRef: &v1alpha1.TaskRef{Name: "aTask"}, + }, + }, + }, + }, + args: args{ + param: v1beta1.Param{ + Name: "targetParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "explicitValueNoResultReference", + }, + }, + }, + want: nil, + wantErr: false, + }, + { + name: "successful resolution: using result reference", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + TaskRunName: "aTaskRun", + TaskRun: tb.TaskRun("aTaskRun", "namespace", tb.TaskRunStatus( + tb.TaskRunResult("aResult", "aResultValue"), + )), + PipelineTask: &v1alpha1.PipelineTask{ + Name: "aTask", + TaskRef: &v1alpha1.TaskRef{Name: "aTask"}, + }, + }, + }, + }, + args: args{ + param: v1beta1.Param{ + Name: "targetParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.aTask.results.aResult)", + }, + }, + }, + want: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, + }, + wantErr: false, + }, { + name: "successful resolution: using multiple result reference", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + TaskRunName: "aTaskRun", + TaskRun: tb.TaskRun("aTaskRun", "namespace", tb.TaskRunStatus( + tb.TaskRunResult("aResult", "aResultValue"), + )), + PipelineTask: &v1alpha1.PipelineTask{ + Name: "aTask", + TaskRef: &v1alpha1.TaskRef{Name: "aTask"}, + }, + }, { + TaskRunName: "bTaskRun", + TaskRun: tb.TaskRun("bTaskRun", "namespace", tb.TaskRunStatus( + tb.TaskRunResult("bResult", "bResultValue"), + )), + PipelineTask: &v1alpha1.PipelineTask{ + Name: "bTask", + TaskRef: &v1alpha1.TaskRef{Name: "bTask"}, + }, + }, + }, + }, + args: args{ + param: v1beta1.Param{ + Name: "targetParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.aTask.results.aResult) $(tasks.bTask.results.bResult)", + }, + }, + }, + want: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "bResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "bTask", + Result: "bResult", + }, + FromTaskRun: "bTaskRun", + }, + }, + wantErr: false, + }, { + name: "successful resolution: duplicate result references", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + TaskRunName: "aTaskRun", + TaskRun: tb.TaskRun("aTaskRun", "namespace", tb.TaskRunStatus( + tb.TaskRunResult("aResult", "aResultValue"), + )), + PipelineTask: &v1alpha1.PipelineTask{ + Name: "aTask", + TaskRef: &v1alpha1.TaskRef{Name: "aTask"}, + }, + }, + }, + }, + args: args{ + param: v1beta1.Param{ + Name: "targetParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.aTask.results.aResult) $(tasks.aTask.results.aResult)", + }, + }, + }, + want: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, + }, + wantErr: false, + }, { + name: "unsuccessful resolution: referenced result doesn't exist in referenced task", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + TaskRunName: "aTaskRun", + TaskRun: tb.TaskRun("aTaskRun", "namespace"), + PipelineTask: &v1alpha1.PipelineTask{ + Name: "aTask", + TaskRef: &v1alpha1.TaskRef{Name: "aTask"}, + }, + }, + }, + }, + args: args{ + param: v1beta1.Param{ + Name: "targetParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.aTask.results.aResult)", + }, + }, + }, + want: nil, + wantErr: true, + }, { + name: "unsuccessful resolution: pipeline task missing", + fields: fields{ + pipelineRunState: PipelineRunState{}, + }, + args: args{ + param: v1beta1.Param{ + Name: "targetParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.aTask.results.aResult)", + }, + }, + }, + want: nil, + wantErr: true, + }, { + name: "unsuccessful resolution: task run missing", + fields: fields{ + pipelineRunState: PipelineRunState{ + { + PipelineTask: &v1alpha1.PipelineTask{ + Name: "aTask", + TaskRef: &v1alpha1.TaskRef{Name: "aTask"}, + }, + }, + }, + }, + args: args{ + param: v1beta1.Param{ + Name: "targetParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.aTask.results.aResult)", + }, + }, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Logf("test name: %s\n", tt.name) + got, err := extractResultRefsFromParam(tt.fields.pipelineRunState, tt.args.param) + if (err != nil) != tt.wantErr { + t.Errorf("ResolveResultRef() error = %v, wantErr %v", err, tt.wantErr) + return + } + if d := cmp.Diff(tt.want, got); d != "" { + t.Fatalf("ResolveResultRef -want, +got: %v", d) + } + }) + } +} + +func TestResolveResultRefs(t *testing.T) { + type args struct { + pipelineRunState PipelineRunState + targets PipelineRunState + } + pipelineRunState := PipelineRunState{ + { + TaskRunName: "aTaskRun", + TaskRun: tb.TaskRun("aTaskRun", "namespace", tb.TaskRunStatus( + tb.TaskRunResult("aResult", "aResultValue"), + )), + PipelineTask: &v1alpha1.PipelineTask{ + Name: "aTask", + TaskRef: &v1alpha1.TaskRef{Name: "aTask"}, + }, + }, { + PipelineTask: &v1alpha1.PipelineTask{ + Name: "bTask", + TaskRef: &v1alpha1.TaskRef{Name: "bTask"}, + Params: []v1beta1.Param{ + { + Name: "bParam", + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "$(tasks.aTask.results.aResult)", + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + args args + want ResolvedResultRefs + wantErr bool + }{ + { + name: "Test successful result references resolution", + args: args{ + pipelineRunState: pipelineRunState, + targets: PipelineRunState{ + pipelineRunState[1], + }, + }, + want: ResolvedResultRefs{ + { + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, + }, + wantErr: false, + }, + { + name: "Test successful result references resolution non result references", + args: args{ + pipelineRunState: pipelineRunState, + targets: PipelineRunState{ + pipelineRunState[0], + }, + }, + want: nil, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ResolveResultRefs(tt.args.pipelineRunState, tt.args.targets) + if (err != nil) != tt.wantErr { + t.Errorf("ResolveResultRefs() error = %v, wantErr %v", err, tt.wantErr) + return + } + if d := cmp.Diff(tt.want, got); d != "" { + t.Fatalf("ResolveResultRef -want, +got: %v", d) + } + }) + } +} diff --git a/test/builder/task.go b/test/builder/task.go index 87d2f23aac5..7e9ebdf4244 100644 --- a/test/builder/task.go +++ b/test/builder/task.go @@ -396,6 +396,15 @@ func StatusCondition(condition apis.Condition) TaskRunStatusOp { } } +func TaskRunResult(name, value string) TaskRunStatusOp { + return func(s *v1alpha1.TaskRunStatus) { + s.TaskRunResults = append(s.TaskRunResults, v1beta1.TaskRunResult{ + Name: name, + Value: value, + }) + } +} + func Retry(retry v1alpha1.TaskRunStatus) TaskRunStatusOp { return func(s *v1alpha1.TaskRunStatus) { s.RetriesStatus = append(s.RetriesStatus, retry)