diff --git a/go.mod b/go.mod index 903d2f368b6..9b7fbe4a7a8 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( cloud.google.com/go v0.47.0 // indirect + cloud.google.com/go/storage v1.0.0 contrib.go.opencensus.io/exporter/stackdriver v0.12.8 // indirect github.com/GoogleCloudPlatform/cloud-builders/gcs-fetcher v0.0.0-20191203181535-308b93ad1f39 github.com/cloudevents/sdk-go v1.0.0 @@ -38,6 +39,7 @@ require ( golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect + google.golang.org/api v0.15.0 google.golang.org/appengine v1.6.5 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.2.5 // indirect diff --git a/pkg/apis/pipeline/v1alpha1/pipeline_types.go b/pkg/apis/pipeline/v1alpha1/pipeline_types.go index 03b6888f4a3..f220d3c5d89 100644 --- a/pkg/apis/pipeline/v1alpha1/pipeline_types.go +++ b/pkg/apis/pipeline/v1alpha1/pipeline_types.go @@ -158,6 +158,12 @@ func (pt PipelineTask) Deps() []string { deps = append(deps, rd.From...) } } + // Add any dependents from task results + for _, param := range pt.Params { + if resultRef, ok := v1alpha2.HasResultReference(param); ok { + 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 0fbcbed6a7b..bb9e3684d89 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/v1alpha2" "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 v1alpha2.LooksLikeResultRef(param) { + if _, err := v1alpha2.NewResultReference(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 { @@ -185,6 +200,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 4ccb37b37a0..407a7bf550c 100644 --- a/pkg/apis/pipeline/v1alpha1/pipeline_validation_test.go +++ b/pkg/apis/pipeline/v1alpha1/pipeline_validation_test.go @@ -325,6 +325,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/v1alpha2/param_types.go b/pkg/apis/pipeline/v1alpha2/param_types.go index 01249a2101a..01c5fb1f989 100644 --- a/pkg/apis/pipeline/v1alpha2/param_types.go +++ b/pkg/apis/pipeline/v1alpha2/param_types.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "strings" resource "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1" ) @@ -124,3 +125,77 @@ func (arrayOrString *ArrayOrString) ApplyReplacements(stringReplacements map[str arrayOrString.ArrayVal = newArrayVal } } + +// ResultReference is a type that represents a reference to a task run result +type ResultReference struct { + PipelineTask string + Result string +} + +const ( + resultExpressionFormat = "tasks..results." + resultTaskPart = "tasks" + resultResultPart = "results" +) + +// NewResultReference extracts a ResultReference form param. +// If the ResultReference cab be extracted, it is returned. Otherwise an error is returned +func NewResultReference(param Param) (*ResultReference, error) { + substitutionExpression, ok := getVarSubstitutionExpression(param) + if !ok { + return nil, fmt.Errorf("Invalid result reference expression: must contain variable substitution %q", resultExpressionFormat) + } + pipelineTask, result, err := parseExpression(substitutionExpression) + if err != nil { + return nil, fmt.Errorf("Invalid result reference expression: %v", err) + } + return &ResultReference{ + PipelineTask: pipelineTask, + Result: result, + }, nil +} + +// HasResultReference is similar to NewResultReference, but it is used +// when the error is not relevant. i.e. we simply want to check if param contains +// a result reference +func HasResultReference(param Param) (*ResultReference, bool) { + if resultRef, err := NewResultReference(param); err == nil { + return resultRef, true + } + return nil, false +} + +// LooksLikeResultRef attempts to check if param looks like a result reference. +// This is useful if we want to make sure the param looks like a ResultReference before +// performing strict validation +func LooksLikeResultRef(param Param) bool { + if param.Value.Type != ParamTypeString { + return false + } + extractedExpression, ok := getVarSubstitutionExpression(param) + if !ok { + return false + } + + return strings.HasPrefix(extractedExpression, "task") && strings.Contains(extractedExpression, ".result") +} + +// getVarSubstitutionExpression extracts the value between "$(" and ")"" +func getVarSubstitutionExpression(param Param) (string, bool) { + if param.Value.Type != ParamTypeString { + return "", false + } + value := param.Value.StringVal + if !strings.HasPrefix(value, "$(") || !strings.HasSuffix(value, ")") { + return "", false + } + return strings.TrimSuffix(strings.TrimPrefix(value, "$("), ")"), true +} + +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/v1alpha2/param_types_test.go b/pkg/apis/pipeline/v1alpha2/param_types_test.go index b64cc2739c8..492815f9d08 100644 --- a/pkg/apis/pipeline/v1alpha2/param_types_test.go +++ b/pkg/apis/pipeline/v1alpha2/param_types_test.go @@ -187,3 +187,230 @@ func TestArrayOrString_MarshalJSON(t *testing.T) { } } } + +func TestNewResultReference(t *testing.T) { + type args struct { + param v1alpha2.Param + } + tests := []struct { + name string + args args + want *v1alpha2.ResultReference + wantErr bool + }{ + { + name: "Test valid expression", + args: args{ + param: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(tasks.sumTask.results.sumResult)", + }, + }, + }, + want: &v1alpha2.ResultReference{ + PipelineTask: "sumTask", + Result: "sumResult", + }, + wantErr: false, + }, { + name: "Test invalid expression: first separator typo", + args: args{ + param: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(task.sumTasks.results.sumResult)", + }, + }, + }, + want: nil, + wantErr: true, + }, { + name: "Test invalid expression: third separator typo", + args: args{ + param: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.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: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(params.paramName)", + }, + }, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := v1alpha2.NewResultReference(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 v1alpha2.Param + } + tests := []struct { + name string + args args + wantRef *v1alpha2.ResultReference + wantBool bool + }{ + { + name: "Test valid expression", + args: args{ + param: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(tasks.sumTask.results.sumResult)", + }, + }, + }, + wantRef: &v1alpha2.ResultReference{ + PipelineTask: "sumTask", + Result: "sumResult", + }, + wantBool: true, + }, { + name: "Test invalid expression: param substitution shouldn't be considered result ref", + args: args{ + param: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(params.paramName)", + }, + }, + }, + wantRef: nil, + wantBool: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := v1alpha2.HasResultReference(tt.args.param) + if !reflect.DeepEqual(got, tt.wantRef) { + t.Errorf("HasResultReference() got = %v, want %v", got, tt.wantRef) + } + if got1 != tt.wantBool { + t.Errorf("HasResultReference() got1 = %v, want %v", got1, tt.wantBool) + } + }) + } +} + +func TestLooksLikeResultRef(t *testing.T) { + type args struct { + param v1alpha2.Param + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "test expression that is a result ref", + args: args{ + param: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(tasks.sumTasks.results.sumResult)", + }, + }, + }, + want: true, + }, { + name: "test expression: looks like result ref, but typo in 'task' separator", + args: args{ + param: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(task.sumTasks.results.sumResult)", + }, + }, + }, + want: true, + }, { + name: "test expression: looks like result ref, but typo in 'results' separator", + args: args{ + param: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(tasks.sumTasks.result.sumResult)", + }, + }, + }, + want: true, + }, { + name: "test expression: missing 'task' separator", + args: args{ + param: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(sumTasks.results.sumResult)", + }, + }, + }, + want: false, + }, { + name: "test expression: missing variable substitution", + args: args{ + param: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "tasks.sumTasks.results.sumResult", + }, + }, + }, + want: false, + }, { + name: "test expression: param substitution shouldn't be considered result ref", + args: args{ + param: v1alpha2.Param{ + Name: "param", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(params.someParam)", + }, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := v1alpha2.LooksLikeResultRef(tt.args.param); got != tt.want { + t.Errorf("LooksLikeResultRef() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go index 3e79cb420bf..5fab143c8d0 100644 --- a/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go @@ -917,6 +917,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 *ResultReference) DeepCopyInto(out *ResultReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResultReference. +func (in *ResultReference) DeepCopy() *ResultReference { + if in == nil { + return nil + } + out := new(ResultReference) + 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..6900d824f1b 100644 --- a/pkg/reconciler/pipeline/dag/dag_test.go +++ b/pkg/reconciler/pipeline/dag/dag_test.go @@ -19,6 +19,8 @@ package dag_test import ( "testing" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha2" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" @@ -571,3 +573,70 @@ 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"} + xDependsOnA := v1alpha1.PipelineTask{ + Name: "x", + Params: []v1alpha1.Param{ + { + Name: "paramX", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha1.ParamTypeString, + StringVal: "$(tasks.a.results.resultA)", + }, + }, + }, + } + yDependsOnBRunsAfterC := v1alpha1.PipelineTask{ + Name: "y", + RunAfter: []string{"c"}, + Params: []v1alpha1.Param{ + { + Name: "paramB", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha1.ParamTypeString, + StringVal: "$(tasks.b.results.resultB)", + }, + }, + }, + } + + // a b c + // | \ / + // x y + nodeA := &dag.Node{Task: a} + nodeB := &dag.Node{Task: b} + nodeC := &dag.Node{Task: c} + nodeX := &dag.Node{Task: xDependsOnA} + nodeY := &dag.Node{Task: yDependsOnBRunsAfterC} + + nodeA.Next = []*dag.Node{nodeX} + nodeB.Next = []*dag.Node{nodeY} + nodeC.Next = []*dag.Node{nodeY} + nodeX.Prev = []*dag.Node{nodeA} + nodeY.Prev = []*dag.Node{nodeB, nodeC} + + expectedDAG := &dag.Graph{ + Nodes: map[string]*dag.Node{ + "a": nodeA, + "b": nodeB, + "c": nodeC, + "x": nodeX, + "y": nodeY, + }, + } + p := &v1alpha1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, + Spec: v1alpha1.PipelineSpec{ + Tasks: []v1alpha1.PipelineTask{a, b, c, xDependsOnA, yDependsOnBRunsAfterC}, + }, + } + 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 3bae8402058..db6fee9c315 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -428,7 +428,17 @@ 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) + if err := resources.ResolveParams(pipelineState, nextRprts); 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 + } var as artifacts.ArtifactStorageInterface @@ -437,7 +447,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 } @@ -557,7 +567,7 @@ func (c *Reconciler) createTaskRun(rprt *resources.ResolvedPipelineRunTask, pr * }, Spec: v1alpha1.TaskRunSpec{ Inputs: v1alpha1.TaskRunInputs{ - Params: rprt.PipelineTask.Params, + Params: rprt.ResolvedTaskParams.ToTaskRunParams(), }, ServiceAccountName: pr.GetServiceAccountName(rprt.PipelineTask.Name), Timeout: getTaskRunTimeout(pr, rprt), diff --git a/pkg/reconciler/pipelinerun/pipelinerun_test.go b/pkg/reconciler/pipelinerun/pipelinerun_test.go index 27dfd774736..2a9ca930da1 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/pipelinerun/pipelinerun_test.go @@ -1680,3 +1680,92 @@ 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("aTask", "aTask"), + tb.PipelineTask("bTask", "bTask", + tb.PipelineTaskParam("bParam", "$(tasks.aTask.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("aTask", "foo"), + tb.Task("bTask", "foo", + tb.TaskSpec( + tb.TaskInputs(tb.InputsParamSpec("bParam", v1alpha1.ParamTypeString)), + ), + ), + } + trs := []*v1alpha1.TaskRun{ + tb.TaskRun("test-pipeline-run-different-service-accs-aTask-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", "bTask"), + 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-bTask-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", "bTask"), + tb.TaskRunSpec( + tb.TaskRunTaskRef("bTask"), + tb.TaskRunServiceAccountName("test-sa-0"), + tb.TaskRunInputs(tb.TaskRunInputsParam("bParam", "aResultValue")), + ), + ) + // Check that the expected TaskRun was created + actual, err := clients.Pipeline.TektonV1alpha1().TaskRuns("foo").Get(expectedTaskRunName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Expected a TaskRun to be created, but it wasn't: %s", err) + } + if d := cmp.Diff(actual, expectedTaskRun); d != "" { + t.Errorf("expected to see TaskRun %v created. Diff %s", expectedTaskRunName, d) + } +} diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerun.go b/pkg/reconciler/pipelinerun/resources/pipelinerun.go new file mode 100644 index 00000000000..e1baae72a32 --- /dev/null +++ b/pkg/reconciler/pipelinerun/resources/pipelinerun.go @@ -0,0 +1,132 @@ +package resources + +import ( + "fmt" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha2" +) + +// ResolvedTaskParams represents all of the ResolvedPipelineTaskParam for a pipeline task +type ResolvedTaskParams []*ResolvedPipelineTaskParam + +// ResolvedPipelineTaskParam represents a task param 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 ResolvedPipelineTaskParam struct { + Name string + Value v1alpha2.ArrayOrString + ResultReference v1alpha2.ResultReference + FromTaskRun string +} + +type taskParamResolver struct { + resolvedTaskByName map[string]*ResolvedPipelineRunTask +} + +// ToTaskRunParams converts ResolvedTaskParams to v1alpha2.Param +func (resolvedParams ResolvedTaskParams) ToTaskRunParams() []v1alpha2.Param { + var params []v1alpha2.Param + for _, resolvedParam := range resolvedParams { + params = append(params, v1alpha2.Param{ + Name: resolvedParam.Name, + Value: resolvedParam.Value, + }) + } + return params +} + +func newTaskParamResolver(pipelineRunState PipelineRunState) *taskParamResolver { + resolvePipelineRunTaskByName := make(map[string]*ResolvedPipelineRunTask) + for _, resolvedPipelineRunTask := range pipelineRunState { + resolvePipelineRunTaskByName[resolvedPipelineRunTask.PipelineTask.Name] = resolvedPipelineRunTask + } + return &taskParamResolver{ + resolvedTaskByName: resolvePipelineRunTaskByName, + } +} + +// ResolveParams performs the parameter substitution on all ResolvedPipelineRunTask in targets +// using the current PipelineRunState to fetch results if needed +func ResolveParams(pipelineRunState PipelineRunState, targets PipelineRunState) error { + resolver := newTaskParamResolver(pipelineRunState) + for _, target := range targets { + resolvedParams, err := resolver.ResolveParams(target) + if err != nil { + return err + } + target.ResolvedTaskParams = resolvedParams + } + return nil +} + +// ResolveParam resolves task results to their actual value +func (resolver *taskParamResolver) ResolveParam(param v1alpha2.Param) (*ResolvedPipelineTaskParam, error) { + if resultRef, ok := v1alpha2.HasResultReference(param); ok { + resolvedParam, err := resolver.resolveParamFromResultRef(param.Name, resultRef) + if err != nil { + return nil, err + } + return resolvedParam, nil + } + return &ResolvedPipelineTaskParam{ + Name: param.Name, + Value: param.Value, + }, nil + +} + +// ResolveParams converts all params of the resolved pipeline run task +func (resolver *taskParamResolver) ResolveParams(target *ResolvedPipelineRunTask) (ResolvedTaskParams, error) { + var resolvedParams []*ResolvedPipelineTaskParam + for _, param := range target.PipelineTask.Params { + resolvedParam, err := resolver.ResolveParam(param) + if err != nil { + return nil, err + } + resolvedParams = append(resolvedParams, resolvedParam) + } + return resolvedParams, nil +} + +func (resolver *taskParamResolver) resolveParamFromResultRef(paramName string, resultRef *v1alpha2.ResultReference) (*ResolvedPipelineTaskParam, error) { + referencedTaskRun, err := resolver.getReferencedTaskRun(resultRef) + if err != nil { + return nil, err + } + result, err := findTaskResultForParam(referencedTaskRun, resultRef) + if err != nil { + return nil, err + } + return &ResolvedPipelineTaskParam{ + Name: paramName, + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: result.Value, + }, + FromTaskRun: referencedTaskRun.Name, + ResultReference: *resultRef, + }, nil +} + +func (resolver *taskParamResolver) getReferencedTaskRun(reference *v1alpha2.ResultReference) (*v1alpha1.TaskRun, error) { + referencedPipelineTask := resolver.resolvedTaskByName[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 *v1alpha2.ResultReference) (*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/pipelinerunresolution.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go index a1d44457cf5..b08ab88a398 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go @@ -69,6 +69,9 @@ type ResolvedPipelineRunTask struct { ResolvedTaskResources *resources.ResolvedTaskResources // ConditionChecks ~~TaskRuns but for evaling conditions ResolvedConditionChecks TaskConditionCheckState // Could also be a TaskRun or maybe just a Pod? + // After param resolution ResolvedTaskParams will contain ResolvedPipelineTaskParam with values + // populated from Task results if any + ResolvedTaskParams ResolvedTaskParams } // PipelineRunState is a slice of ResolvedPipelineRunTasks the represents the current execution diff --git a/pkg/reconciler/pipelinerun/resources/taskparamresolution_test.go b/pkg/reconciler/pipelinerun/resources/taskparamresolution_test.go new file mode 100644 index 00000000000..4d5cd747271 --- /dev/null +++ b/pkg/reconciler/pipelinerun/resources/taskparamresolution_test.go @@ -0,0 +1,292 @@ +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/v1alpha2" + tb "github.com/tektoncd/pipeline/test/builder" +) + +func TestTaskParamResolver_ResolveParm_ExplicitValue(t *testing.T) { + type fields struct { + pipelineRunState PipelineRunState + } + type args struct { + param v1alpha2.Param + } + tests := []struct { + name string + fields fields + args args + want *ResolvedPipelineTaskParam + wantErr bool + }{ + { + name: "successful resolution: 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: v1alpha2.Param{ + Name: "targetParam", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "explicitValue", + }, + }, + }, + want: &ResolvedPipelineTaskParam{ + Name: "targetParam", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "explicitValue", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resolver := newTaskParamResolver(tt.fields.pipelineRunState) + got, err := resolver.ResolveParam(tt.args.param) + if (err != nil) != tt.wantErr { + t.Errorf("ResolveParam() error = %v, wantErr %v", err, tt.wantErr) + return + } + if d := cmp.Diff(tt.want, got); d != "" { + t.Fatalf("ResolveParam -want, +got: %v", d) + } + }) + } +} + +func TestTaskParamResolver_ResolveParm_ResultSubstitution(t *testing.T) { + type fields struct { + pipelineRunState PipelineRunState + } + type args struct { + param v1alpha2.Param + } + tests := []struct { + name string + fields fields + args args + want *ResolvedPipelineTaskParam + wantErr bool + }{ + { + 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"}, + }, + ResolvedTaskParams: nil, + }, + }, + }, + args: args{ + param: v1alpha2.Param{ + Name: "targetParam", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(tasks.aTask.results.aResult)", + }, + }, + }, + want: &ResolvedPipelineTaskParam{ + Name: "targetParam", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1alpha2.ResultReference{ + 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"}, + }, + ResolvedTaskParams: nil, + }, + }, + }, + args: args{ + param: v1alpha2.Param{ + Name: "targetParam", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(tasks.aTask.results.aResult)", + }, + }, + }, + want: nil, + wantErr: true, + }, { + name: "unsuccessful resolution: pipeline task missing", + fields: fields{ + pipelineRunState: PipelineRunState{}, + }, + args: args{ + param: v1alpha2.Param{ + Name: "targetParam", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.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"}, + }, + ResolvedTaskParams: nil, + }, + }, + }, + args: args{ + param: v1alpha2.Param{ + Name: "targetParam", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.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) + resolver := newTaskParamResolver(tt.fields.pipelineRunState) + got, err := resolver.ResolveParam(tt.args.param) + if (err != nil) != tt.wantErr { + t.Errorf("ResolveParam() error = %v, wantErr %v", err, tt.wantErr) + return + } + if d := cmp.Diff(tt.want, got); d != "" { + t.Fatalf("ResolveParam -want, +got: %v", d) + } + }) + } +} + +func TestResolveParams(t *testing.T) { + type args struct { + pipelineRunState PipelineRunState + targets PipelineRunState + expected 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"}, + }, + ResolvedTaskParams: nil, + }, { + PipelineTask: &v1alpha1.PipelineTask{ + Name: "bTask", + TaskRef: &v1alpha1.TaskRef{Name: "bTask"}, + Params: []v1alpha2.Param{ + { + Name: "bParam", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "$(tasks.aTask.results.aResult)", + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test successful param resolution", + args: args{ + pipelineRunState: pipelineRunState, + // Only resolve second pipeline run task + targets: PipelineRunState{ + pipelineRunState[1], + }, + // Expect something but with param resolved + expected: PipelineRunState{ + pipelineRunState[0], + { + PipelineTask: pipelineRunState[1].PipelineTask, + ResolvedTaskParams: ResolvedTaskParams{ + { + Name: "bParam", + Value: v1alpha2.ArrayOrString{ + Type: v1alpha2.ParamTypeString, + StringVal: "aResultValue", + }, + ResultReference: v1alpha2.ResultReference{ + PipelineTask: "aTask", + Result: "aResult", + }, + FromTaskRun: "aTaskRun", + }, + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ResolveParams(tt.args.pipelineRunState, tt.args.targets); (err != nil) != tt.wantErr { + t.Errorf("ResolveParams() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/test/builder/task.go b/test/builder/task.go index 73dedad26d5..fb5884ecf97 100644 --- a/test/builder/task.go +++ b/test/builder/task.go @@ -332,6 +332,15 @@ func StatusCondition(condition apis.Condition) TaskRunStatusOp { } } +func TaskRunResult(name, value string) TaskRunStatusOp { + return func(s *v1alpha1.TaskRunStatus) { + s.TaskRunResults = append(s.TaskRunResults, v1alpha1.TaskRunResult{ + Name: name, + Value: value, + }) + } +} + func Retry(retry v1alpha1.TaskRunStatus) TaskRunStatusOp { return func(s *v1alpha1.TaskRunStatus) { s.RetriesStatus = append(s.RetriesStatus, retry)