From 9be16522c6ee58bf5427148ab6c254e88716cce6 Mon Sep 17 00:00:00 2001 From: joey Date: Mon, 11 Sep 2023 16:13:23 +0800 Subject: [PATCH] 1. add container type substitution expresions to pipeline task result reference 2. propagate results to embedded task spec Part of work on issue #7086 Signed-off-by: chengjoey --- docs/pipelineruns.md | 86 +++ ...ropagating_results_implicit_resultref.yaml | 34 ++ pkg/apis/pipeline/v1/container_types.go | 60 ++ pkg/apis/pipeline/v1/container_types_test.go | 122 ++++ pkg/apis/pipeline/v1/pipeline_validation.go | 16 + pkg/apis/pipeline/v1/resultref.go | 4 + pkg/apis/pipeline/v1/resultref_test.go | 254 ++++++++ pkg/reconciler/pipelinerun/pipelinerun.go | 3 + .../pipelinerun/pipelinerun_test.go | 542 ++++++++++++++++++ pkg/reconciler/pipelinerun/resources/apply.go | 24 + .../pipelinerun/resources/apply_test.go | 320 +++++++++++ test/propagated_results_test.go | 482 ++++++++++++++++ 12 files changed, 1947 insertions(+) create mode 100644 examples/v1/pipelineruns/propagating_results_implicit_resultref.yaml create mode 100644 pkg/apis/pipeline/v1/container_types_test.go create mode 100644 test/propagated_results_test.go diff --git a/docs/pipelineruns.md b/docs/pipelineruns.md index 482c9e40913..a8ab9cf2187 100644 --- a/docs/pipelineruns.md +++ b/docs/pipelineruns.md @@ -744,6 +744,92 @@ spec: then `test-task` will execute using the `sa-1` account while `build-task` will execute with `sa-for-build`. +#### Propagated Results + +When using an embedded spec, `Results` from the parent `PipelineRun` will be +propagated to any inlined specs without needing to be explicitly defined. This +allows authors to simplify specs by automatically propagating top-level +results down to other inlined resources. +**`Result` substitutions will only be made for `name`, `commands`, `args`, `env` and `script` fields of `steps`, `sidecars`.** + +```yaml +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + name: uid-pipeline-run +spec: + pipelineSpec: + tasks: + - name: add-uid + taskSpec: + results: + - name: uid + type: string + steps: + - name: add-uid + image: busybox + command: ["/bin/sh", "-c"] + args: + - echo "1001" | tee $(results.uid.path) + - name: show-uid + # params: + # - name: uid + # value: $(tasks.add-uid.results.uid) + taskSpec: + steps: + - name: show-uid + image: busybox + command: ["/bin/sh", "-c"] + args: + - echo + # - $(params.uid) + - $(tasks.add-uid.results.uid) +``` + +On executing the `PipelineRun`, the `Results` will be interpolated during resolution. + +```yaml +name: uid-pipeline-run-show-uid +apiVersion: tekton.dev/v1 +kind: TaskRun +metadata: + ... +spec: + taskSpec: + steps: + args: + echo + 1001 + command: + - /bin/sh + - -c + image: busybox + name: show-uid +status: + completionTime: 2023-09-11T07:34:28Z + conditions: + lastTransitionTime: 2023-09-11T07:34:28Z + message: All Steps have completed executing + reason: Succeeded + status: True + type: Succeeded + podName: uid-pipeline-run-show-uid-pod + steps: + container: step-show-uid + name: show-uid + taskSpec: + steps: + args: + echo + 1001 + command: + /bin/sh + -c + computeResources: + image: busybox + name: show-uid +``` + ### Specifying a `Pod` template You can specify a [`Pod` template](podtemplates.md) configuration that will serve as the configuration starting diff --git a/examples/v1/pipelineruns/propagating_results_implicit_resultref.yaml b/examples/v1/pipelineruns/propagating_results_implicit_resultref.yaml new file mode 100644 index 00000000000..35c086393c6 --- /dev/null +++ b/examples/v1/pipelineruns/propagating_results_implicit_resultref.yaml @@ -0,0 +1,34 @@ +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: uid-task +spec: + results: + - name: uid + type: string + steps: + - name: uid + image: busybox + command: ["/bin/sh", "-c"] + args: + - echo "1001" | tee $(results.uid.path) +--- +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + name: uid-pipeline-run +spec: + pipelineSpec: + tasks: + - name: add-uid + taskRef: + name: uid-task + - name: show-uid + taskSpec: + steps: + - name: show-uid + image: busybox + command: ["/bin/sh", "-c"] + args: + - echo + - $(tasks.add-uid.results.uid) \ No newline at end of file diff --git a/pkg/apis/pipeline/v1/container_types.go b/pkg/apis/pipeline/v1/container_types.go index 79c9922f462..ccef95cf767 100644 --- a/pkg/apis/pipeline/v1/container_types.go +++ b/pkg/apis/pipeline/v1/container_types.go @@ -188,6 +188,36 @@ func (s *Step) SetContainerFields(c corev1.Container) { s.SecurityContext = c.SecurityContext } +// GetVarSubstitutionExpressions walks all the places a substitution reference can be used +func (s *Step) GetVarSubstitutionExpressions() []string { + var allExpressions []string + allExpressions = append(allExpressions, validateString(s.Name)...) + allExpressions = append(allExpressions, validateString(s.Image)...) + allExpressions = append(allExpressions, validateString(string(s.ImagePullPolicy))...) + allExpressions = append(allExpressions, validateString(s.Script)...) + allExpressions = append(allExpressions, validateString(s.WorkingDir)...) + for _, cmd := range s.Command { + allExpressions = append(allExpressions, validateString(cmd)...) + } + for _, arg := range s.Args { + allExpressions = append(allExpressions, validateString(arg)...) + } + for _, env := range s.Env { + allExpressions = append(allExpressions, validateString(env.Value)...) + if env.ValueFrom != nil { + if env.ValueFrom.SecretKeyRef != nil { + allExpressions = append(allExpressions, validateString(env.ValueFrom.SecretKeyRef.Key)...) + allExpressions = append(allExpressions, validateString(env.ValueFrom.SecretKeyRef.LocalObjectReference.Name)...) + } + if env.ValueFrom.ConfigMapKeyRef != nil { + allExpressions = append(allExpressions, validateString(env.ValueFrom.ConfigMapKeyRef.Key)...) + allExpressions = append(allExpressions, validateString(env.ValueFrom.ConfigMapKeyRef.LocalObjectReference.Name)...) + } + } + } + return allExpressions +} + // StepTemplate is a template for a Step type StepTemplate struct { // Image reference name. @@ -541,3 +571,33 @@ func (s *Sidecar) SetContainerFields(c corev1.Container) { s.StdinOnce = c.StdinOnce s.TTY = c.TTY } + +// GetVarSubstitutionExpressions walks all the places a substitution reference can be used +func (s *Sidecar) GetVarSubstitutionExpressions() []string { + var allExpressions []string + allExpressions = append(allExpressions, validateString(s.Name)...) + allExpressions = append(allExpressions, validateString(s.Image)...) + allExpressions = append(allExpressions, validateString(string(s.ImagePullPolicy))...) + allExpressions = append(allExpressions, validateString(s.Script)...) + allExpressions = append(allExpressions, validateString(s.WorkingDir)...) + for _, cmd := range s.Command { + allExpressions = append(allExpressions, validateString(cmd)...) + } + for _, arg := range s.Args { + allExpressions = append(allExpressions, validateString(arg)...) + } + for _, env := range s.Env { + allExpressions = append(allExpressions, validateString(env.Value)...) + if env.ValueFrom != nil { + if env.ValueFrom.SecretKeyRef != nil { + allExpressions = append(allExpressions, validateString(env.ValueFrom.SecretKeyRef.Key)...) + allExpressions = append(allExpressions, validateString(env.ValueFrom.SecretKeyRef.LocalObjectReference.Name)...) + } + if env.ValueFrom.ConfigMapKeyRef != nil { + allExpressions = append(allExpressions, validateString(env.ValueFrom.ConfigMapKeyRef.Key)...) + allExpressions = append(allExpressions, validateString(env.ValueFrom.ConfigMapKeyRef.LocalObjectReference.Name)...) + } + } + } + return allExpressions +} diff --git a/pkg/apis/pipeline/v1/container_types_test.go b/pkg/apis/pipeline/v1/container_types_test.go new file mode 100644 index 00000000000..06948f9acaa --- /dev/null +++ b/pkg/apis/pipeline/v1/container_types_test.go @@ -0,0 +1,122 @@ +/* + * + * 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 v1 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" +) + +func TestStepGetVarSubstitutionExpressions(t *testing.T) { + s := Step{ + Name: "$(tasks.task1.results.stepName)", + Image: "$(tasks.task1.results.imageResult)", + ImagePullPolicy: corev1.PullPolicy("$(tasks.task1.results.imagePullPolicy)"), + Script: "substitution within string $(tasks.task1.results.scriptResult)", + WorkingDir: "$(tasks.task1.results.workingDir)", + Command: []string{ + "$(tasks.task2.results.command[*])", + }, + Args: []string{ + "$(tasks.task2.results.args[*])", + }, + Env: []corev1.EnvVar{ + { + Name: "env1", + Value: "$(tasks.task2.results.env1)", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + Key: "$(tasks.task2.results.secretKeyRef)", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(tasks.task2.results.secretNameRef)", + }, + }, + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "$(tasks.task2.results.configMapKeyRef)", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(tasks.task2.results.configMapNameRef)", + }, + }, + }, + }, + }, + } + subRefExpressions := s.GetVarSubstitutionExpressions() + wantRefExpressions := []string{ + "tasks.task1.results.stepName", + "tasks.task1.results.imageResult", + "tasks.task1.results.imagePullPolicy", + "tasks.task1.results.scriptResult", + "tasks.task1.results.workingDir", + "tasks.task2.results.command[*]", + "tasks.task2.results.args[*]", + "tasks.task2.results.env1", + "tasks.task2.results.secretKeyRef", + "tasks.task2.results.secretNameRef", + "tasks.task2.results.configMapKeyRef", + "tasks.task2.results.configMapNameRef", + } + if d := cmp.Diff(wantRefExpressions, subRefExpressions); d != "" { + t.Fatalf("Unexpected result (-want, +got): %s", d) + } +} + +func TestSidecarGetVarSubstitutionExpressions(t *testing.T) { + s := Sidecar{ + Name: "$(tasks.task1.results.sidecarName)", + Image: "$(tasks.task1.results.sidecarImage)", + ImagePullPolicy: corev1.PullPolicy("$(tasks.task1.results.sidecarImagePullPolicy)"), + Env: []corev1.EnvVar{ + { + Name: "env1", + Value: "$(tasks.task2.results.sidecarEnv1)", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + Key: "$(tasks.task2.results.sidecarSecretKeyRef)", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(tasks.task2.results.sidecarSecretNameRef)", + }, + }, + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "$(tasks.task2.results.sidecarConfigMapKeyRef)", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(tasks.task2.results.sidecarConfigMapNameRef)", + }, + }, + }, + }, + }, + } + subRefExpressions := s.GetVarSubstitutionExpressions() + wantRefExpressions := []string{ + "tasks.task1.results.sidecarName", + "tasks.task1.results.sidecarImage", + "tasks.task1.results.sidecarImagePullPolicy", + "tasks.task2.results.sidecarEnv1", + "tasks.task2.results.sidecarSecretKeyRef", + "tasks.task2.results.sidecarSecretNameRef", + "tasks.task2.results.sidecarConfigMapKeyRef", + "tasks.task2.results.sidecarConfigMapNameRef", + } + if d := cmp.Diff(wantRefExpressions, subRefExpressions); d != "" { + t.Fatalf("Unexpected result (-want, +got): %s", d) + } +} diff --git a/pkg/apis/pipeline/v1/pipeline_validation.go b/pkg/apis/pipeline/v1/pipeline_validation.go index 7b2de9f77ff..a5cea7d8760 100644 --- a/pkg/apis/pipeline/v1/pipeline_validation.go +++ b/pkg/apis/pipeline/v1/pipeline_validation.go @@ -497,6 +497,22 @@ func (pt *PipelineTask) extractAllParams() Params { return allParams } +// GetVarSubstitutionExpressions extract all values between the parameters "$(" and ")" of steps and sidecars +func (pt *PipelineTask) GetVarSubstitutionExpressions() []string { + var allExpressions []string + if pt.TaskSpec != nil { + for _, step := range pt.TaskSpec.Steps { + stepExpressions := step.GetVarSubstitutionExpressions() + allExpressions = append(allExpressions, stepExpressions...) + } + for _, sidecar := range pt.TaskSpec.Sidecars { + sidecarExpressions := sidecar.GetVarSubstitutionExpressions() + allExpressions = append(allExpressions, sidecarExpressions...) + } + } + return allExpressions +} + func containsExecutionStatusRef(p string) bool { if strings.HasPrefix(p, "tasks.") && strings.HasSuffix(p, ".status") { return true diff --git a/pkg/apis/pipeline/v1/resultref.go b/pkg/apis/pipeline/v1/resultref.go index 14b0de17fdb..2de6bb80804 100644 --- a/pkg/apis/pipeline/v1/resultref.go +++ b/pkg/apis/pipeline/v1/resultref.go @@ -172,6 +172,8 @@ func ParseResultName(resultName string) (string, string) { // in a PipelineTask and returns a list of any references that are found. func PipelineTaskResultRefs(pt *PipelineTask) []*ResultRef { refs := []*ResultRef{} + // TODO move the whenExpression.GetVarSubstitutionExpressions() and GetVarSubstitutionExpressionsForParam(p) as well + // separate cleanup, reference https://github.com/tektoncd/pipeline/pull/7121 for _, p := range pt.extractAllParams() { expressions, _ := p.GetVarSubstitutionExpressions() refs = append(refs, NewResultRefs(expressions)...) @@ -180,5 +182,7 @@ func PipelineTaskResultRefs(pt *PipelineTask) []*ResultRef { expressions, _ := whenExpression.GetVarSubstitutionExpressions() refs = append(refs, NewResultRefs(expressions)...) } + taskSubExpressions := pt.GetVarSubstitutionExpressions() + refs = append(refs, NewResultRefs(taskSubExpressions)...) return refs } diff --git a/pkg/apis/pipeline/v1/resultref_test.go b/pkg/apis/pipeline/v1/resultref_test.go index a471418edec..0ec0c37a79c 100644 --- a/pkg/apis/pipeline/v1/resultref_test.go +++ b/pkg/apis/pipeline/v1/resultref_test.go @@ -23,6 +23,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/test/diff" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/selection" ) @@ -472,6 +473,224 @@ func TestNewResultReferenceWhenExpressions(t *testing.T) { } } +func TestNewResultReferenceTaskSpec(t *testing.T) { + for _, tt := range []struct { + name string + pt *v1.PipelineTask + wantRef []*v1.ResultRef + }{ + { + name: "Test step expression references", + pt: &v1.PipelineTask{ + TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{ + Steps: []v1.Step{ + { + Name: "$(tasks.task1.results.stepName)", + Image: "$(tasks.task1.results.imageResult)", + ImagePullPolicy: corev1.PullPolicy("$(tasks.task1.results.imagePullPolicy)"), + Script: "substitution within string $(tasks.task1.results.scriptResult)", + WorkingDir: "$(tasks.task1.results.workingDir)", + Command: []string{ + "$(tasks.task2.results.command[*])", + }, + Args: []string{ + "$(tasks.task2.results.args[*])", + }, + Env: []corev1.EnvVar{ + { + Name: "env1", + Value: "$(tasks.task2.results.env1)", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + Key: "$(tasks.task2.results.secretKeyRef)", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(tasks.task2.results.secretNameRef)", + }, + }, + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "$(tasks.task2.results.configMapKeyRef)", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(tasks.task2.results.configMapNameRef)", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantRef: []*v1.ResultRef{{ + PipelineTask: "task1", + Result: "stepName", + }, { + PipelineTask: "task1", + Result: "imageResult", + }, { + PipelineTask: "task1", + Result: "imagePullPolicy", + }, { + PipelineTask: "task1", + Result: "scriptResult", + }, { + PipelineTask: "task1", + Result: "workingDir", + }, { + PipelineTask: "task2", + Result: "command", + }, { + PipelineTask: "task2", + Result: "args", + }, { + PipelineTask: "task2", + Result: "env1", + }, { + PipelineTask: "task2", + Result: "secretKeyRef", + }, { + PipelineTask: "task2", + Result: "secretNameRef", + }, { + PipelineTask: "task2", + Result: "configMapKeyRef", + }, { + PipelineTask: "task2", + Result: "configMapNameRef", + }}, + }, { + name: "Test sidecar expression references", + pt: &v1.PipelineTask{ + TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{ + Sidecars: []v1.Sidecar{ + { + Name: "$(tasks.task1.results.stepName)", + Image: "$(tasks.task1.results.imageResult)", + ImagePullPolicy: corev1.PullPolicy("$(tasks.task1.results.imagePullPolicy)"), + Script: "substitution within string $(tasks.task1.results.scriptResult)", + WorkingDir: "$(tasks.task1.results.workingDir)", + Command: []string{ + "$(tasks.task2.results.command[*])", + }, + Args: []string{ + "$(tasks.task2.results.args[*])", + }, + Env: []corev1.EnvVar{ + { + Name: "env1", + Value: "$(tasks.task2.results.env1)", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + Key: "$(tasks.task2.results.secretKeyRef)", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(tasks.task2.results.secretNameRef)", + }, + }, + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "$(tasks.task2.results.configMapKeyRef)", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(tasks.task2.results.configMapNameRef)", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantRef: []*v1.ResultRef{{ + PipelineTask: "task1", + Result: "stepName", + }, { + PipelineTask: "task1", + Result: "imageResult", + }, { + PipelineTask: "task1", + Result: "imagePullPolicy", + }, { + PipelineTask: "task1", + Result: "scriptResult", + }, { + PipelineTask: "task1", + Result: "workingDir", + }, { + PipelineTask: "task2", + Result: "command", + }, { + PipelineTask: "task2", + Result: "args", + }, { + PipelineTask: "task2", + Result: "env1", + }, { + PipelineTask: "task2", + Result: "secretKeyRef", + }, { + PipelineTask: "task2", + Result: "secretNameRef", + }, { + PipelineTask: "task2", + Result: "configMapKeyRef", + }, { + PipelineTask: "task2", + Result: "configMapNameRef", + }}, + }, { + name: "Test both step and sidecar expression references", + pt: &v1.PipelineTask{ + TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{ + Steps: []v1.Step{ + { + Name: "$(tasks.task1.results.stepName)", + Image: "$(tasks.task1.results.stepImage)", + ImagePullPolicy: corev1.PullPolicy("$(tasks.task1.results.stepImagePullPolicy)"), + }, + }, + Sidecars: []v1.Sidecar{ + { + Name: "$(tasks.task2.results.sidecarName)", + Image: "$(tasks.task2.results.sidecarImage)", + ImagePullPolicy: corev1.PullPolicy("$(tasks.task2.results.sidecarImagePullPolicy)"), + }, + }, + }, + }, + }, + wantRef: []*v1.ResultRef{{ + PipelineTask: "task1", + Result: "stepName", + }, { + PipelineTask: "task1", + Result: "stepImage", + }, { + PipelineTask: "task1", + Result: "stepImagePullPolicy", + }, { + PipelineTask: "task2", + Result: "sidecarName", + }, { + PipelineTask: "task2", + Result: "sidecarImage", + }, { + PipelineTask: "task2", + Result: "sidecarImagePullPolicy", + }}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + got := v1.PipelineTaskResultRefs(tt.pt) + if d := cmp.Diff(tt.wantRef, got); d != "" { + t.Error(diff.PrintWantGot(d)) + } + }) + } +} + func TestHasResultReferenceWhenExpression(t *testing.T) { for _, tt := range []struct { name string @@ -650,6 +869,20 @@ func TestPipelineTaskResultRefs(t *testing.T) { }, { Value: *v1.NewStructuredValues("$(tasks.pt7.results.r7)", "$(tasks.pt8.results.r8)"), }}}, + TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{ + Steps: []v1.Step{ + { + Name: "$(tasks.pt8.results.r8)", + Image: "$(tasks.pt9.results.r9)", + ImagePullPolicy: corev1.PullPolicy("$(tasks.pt10.results.r10)"), + Command: []string{"$(tasks.pt11.results.r11)"}, + Args: []string{"$(tasks.pt12.results.r12)", "$(tasks.pt13.results.r13)"}, + Script: "$(tasks.pt14.results.r14)", + }, + }, + }, + }, } refs := v1.PipelineTaskResultRefs(&pt) expectedRefs := []*v1.ResultRef{{ @@ -679,6 +912,27 @@ func TestPipelineTaskResultRefs(t *testing.T) { }, { PipelineTask: "pt9", Result: "r9", + }, { + PipelineTask: "pt8", + Result: "r8", + }, { + PipelineTask: "pt9", + Result: "r9", + }, { + PipelineTask: "pt10", + Result: "r10", + }, { + PipelineTask: "pt11", + Result: "r11", + }, { + PipelineTask: "pt12", + Result: "r12", + }, { + PipelineTask: "pt13", + Result: "r13", + }, { + PipelineTask: "pt14", + Result: "r14", }} if d := cmp.Diff(refs, expectedRefs, cmpopts.SortSlices(lessResultRef)); d != "" { t.Errorf("%v", d) diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index b06da51b21e..90a61b73420 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -808,6 +808,9 @@ func (c *Reconciler) runNextSchedulableTask(ctx context.Context, pr *v1.Pipeline continue } + // propagate previous task results + resources.PropagateResults(rpt, pipelineRunFacts.State) + // Validate parameter types in matrix after apply substitutions from Task Results if rpt.PipelineTask.IsMatrixed() { if err := resources.ValidateParameterTypesInMatrix(pipelineRunFacts.State); err != nil { diff --git a/pkg/reconciler/pipelinerun/pipelinerun_test.go b/pkg/reconciler/pipelinerun/pipelinerun_test.go index 57cdebd4965..e118173020c 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/pipelinerun/pipelinerun_test.go @@ -43,11 +43,14 @@ import ( resolutionutil "github.com/tektoncd/pipeline/pkg/internal/resolution" "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent" "github.com/tektoncd/pipeline/pkg/reconciler/events/k8sevent" + "github.com/tektoncd/pipeline/pkg/reconciler/pipeline/dag" "github.com/tektoncd/pipeline/pkg/reconciler/pipelinerun/resources" + taskresources "github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources" ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" "github.com/tektoncd/pipeline/pkg/reconciler/volumeclaim" resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" remoteresource "github.com/tektoncd/pipeline/pkg/resolution/resource" + "github.com/tektoncd/pipeline/pkg/tracing" "github.com/tektoncd/pipeline/pkg/trustedresources" "github.com/tektoncd/pipeline/pkg/trustedresources/verifier" "github.com/tektoncd/pipeline/test" @@ -15485,6 +15488,545 @@ spec: } } +func Test_runNextSchedulableTask(t *testing.T) { + for _, tc := range []struct { + name string + pr *v1.PipelineRun + pipelineRunFacts *resources.PipelineRunFacts + want *resources.PipelineRunFacts + wantErr bool + }{ + { + name: "propagate strings, array, object results to next task", + pr: &v1.PipelineRun{ + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionUnknown, + }, + }, + }, + }, + Spec: v1.PipelineRunSpec{ + Status: v1.PipelineRunSpecStatusPending, + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + { + Name: "task1", + }, + { + Name: "task2", + }, + }, + }, + }, + }, + pipelineRunFacts: &resources.PipelineRunFacts{ + State: resources.PipelineRunState{ + { + TaskRunNames: []string{"task1"}, + ResolvedTask: &taskresources.ResolvedTask{ + TaskName: "task1", + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + Name: "step1", + }, + }, + }, + }, + PipelineTask: &v1.PipelineTask{ + Name: "task1", + }, + TaskRuns: []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "task1", + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + // string, array, object results generated by task1 + Results: []v1.TaskRunResult{ + { + Name: "foo", + Type: v1.ResultsTypeString, + Value: v1.ResultValue{ + StringVal: "bar", + }, + }, + { + Name: "commands", + Type: v1.ResultsTypeArray, + Value: v1.ResultValue{ + ArrayVal: []string{"bash", "-c"}, + }, + }, + { + Name: "objects", + Type: v1.ResultsTypeObject, + Value: v1.ResultValue{ + ObjectVal: map[string]string{ + "image": "alpine:latest", + }, + }, + }, + }, + }, + }, + Spec: v1.TaskRunSpec{TaskSpec: &v1.TaskSpec{}}, + }, + }, + }, + { + TaskRunNames: []string{"task2"}, + ResolvedTask: &taskresources.ResolvedTask{ + TaskName: "task2", + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + // The results from task1 should be propagated to task2 + Name: "$(tasks.task1.results.foo)", + Command: []string{"$(tasks.task1.results.commands[*])"}, + Image: "$(tasks.task1.results.objects.image)", + }, + }, + }, + }, + PipelineTask: &v1.PipelineTask{ + Name: "task2", + }, + }, + }, + TasksGraph: &dag.Graph{ + Nodes: map[string]*dag.Node{ + "task1": { + Key: "task1", + Next: []*dag.Node{ + { + Key: "task2", + }, + }, + }, + "task2": { + Key: "task2", + Prev: []*dag.Node{ + { + Key: "task1", + }, + }, + }, + }, + }, + FinalTasksGraph: &dag.Graph{ + Nodes: map[string]*dag.Node{}, + }, + }, + want: &resources.PipelineRunFacts{ + State: resources.PipelineRunState{ + { + TaskRunNames: []string{"task1"}, + ResolvedTask: &taskresources.ResolvedTask{ + TaskName: "task1", + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{{Name: "step1"}}, + }, + }, + PipelineTask: &v1.PipelineTask{ + Name: "task1", + }, + TaskRuns: []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "task1", + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + { + Name: "foo", + Type: v1.ResultsTypeString, + Value: v1.ResultValue{ + StringVal: "bar", + }, + }, + { + Name: "commands", + Type: v1.ResultsTypeArray, + Value: v1.ResultValue{ + ArrayVal: []string{"bash", "-c"}, + }, + }, + { + Name: "objects", + Type: v1.ResultsTypeObject, + Value: v1.ResultValue{ + ObjectVal: map[string]string{ + "image": "alpine:latest", + }, + }, + }, + }, + }, + }, + Spec: v1.TaskRunSpec{TaskSpec: &v1.TaskSpec{}}, + }, + }, + }, + { + TaskRunNames: []string{"task2"}, + ResolvedTask: &taskresources.ResolvedTask{ + TaskName: "task2", + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + // here we expect the results from task1 to be propagated to task2 + Name: "bar", + Command: []string{"bash", "-c"}, + Image: "alpine:latest", + }, + }, + }, + }, + PipelineTask: &v1.PipelineTask{ + Name: "task2", + }, + TaskRuns: []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "task2", + ResourceVersion: "00002", + Labels: map[string]string{"tekton.dev/pipelineRun": "", "tekton.dev/pipelineTask": "task2"}, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "tekton.dev/v1", + Kind: "PipelineRun", + Controller: &[]bool{true}[0], + BlockOwnerDeletion: &[]bool{true}[0], + }, + }, + Annotations: map[string]string{}, + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{}, + }, + Spec: v1.TaskRunSpec{}, + }, + }, + }, + }, + TasksGraph: &dag.Graph{ + Nodes: map[string]*dag.Node{ + "task1": { + Key: "task1", + Next: []*dag.Node{ + { + Key: "task2", + }, + }, + }, + "task2": { + Key: "task2", + Prev: []*dag.Node{ + { + Key: "task1", + }, + }, + }, + }, + }, + FinalTasksGraph: &dag.Graph{ + Nodes: map[string]*dag.Node{}, + }, + SkipCache: map[string]resources.TaskSkipStatus{ + "task1": {SkippingReason: "None"}, + "task2": {SkippingReason: "None"}, + }, + }, + wantErr: false, + }, + { + name: "don't propagate results to next task if task is done", + pr: &v1.PipelineRun{ + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionUnknown, + }, + }, + }, + }, + Spec: v1.PipelineRunSpec{ + Status: v1.PipelineRunSpecStatusPending, + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + { + Name: "task1", + }, + { + Name: "task2", + }, + }, + }, + }, + }, + pipelineRunFacts: &resources.PipelineRunFacts{ + State: resources.PipelineRunState{ + { + TaskRunNames: []string{"task1"}, + ResolvedTask: &taskresources.ResolvedTask{ + TaskName: "task1", + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + Name: "step1", + }, + }, + }, + }, + PipelineTask: &v1.PipelineTask{ + Name: "task1", + }, + TaskRuns: []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "task1", + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + { + Name: "foo", + Type: v1.ResultsTypeString, + Value: v1.ResultValue{ + StringVal: "bar", + }, + }, + }, + }, + }, + Spec: v1.TaskRunSpec{TaskSpec: &v1.TaskSpec{}}, + }, + }, + }, + { + TaskRunNames: []string{"task2"}, + ResolvedTask: &taskresources.ResolvedTask{ + TaskName: "task2", + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + Name: "$(tasks.task1.results.foo)", + }, + }, + }, + }, + PipelineTask: &v1.PipelineTask{ + Name: "task2", + }, + TaskRuns: []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "task2", + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }, + }, + }, + }, + Spec: v1.TaskRunSpec{TaskSpec: &v1.TaskSpec{}}, + }, + }, + }, + }, + TasksGraph: &dag.Graph{ + Nodes: map[string]*dag.Node{ + "task1": { + Key: "task1", + Next: []*dag.Node{ + { + Key: "task2", + }, + }, + }, + "task2": { + Key: "task2", + Prev: []*dag.Node{ + { + Key: "task1", + }, + }, + }, + }, + }, + FinalTasksGraph: &dag.Graph{ + Nodes: map[string]*dag.Node{}, + }, + }, + want: &resources.PipelineRunFacts{ + State: resources.PipelineRunState{ + { + TaskRunNames: []string{"task1"}, + ResolvedTask: &taskresources.ResolvedTask{ + TaskName: "task1", + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{{Name: "step1"}}, + }, + }, + PipelineTask: &v1.PipelineTask{ + Name: "task1", + }, + TaskRuns: []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "task1", + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + { + Name: "foo", + Type: v1.ResultsTypeString, + Value: v1.ResultValue{ + StringVal: "bar", + }, + }, + }, + }, + }, + Spec: v1.TaskRunSpec{TaskSpec: &v1.TaskSpec{}}, + }, + }, + }, + { + TaskRunNames: []string{"task2"}, + ResolvedTask: &taskresources.ResolvedTask{ + TaskName: "task2", + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + Name: "$(tasks.task1.results.foo)", + }, + }, + }, + }, + PipelineTask: &v1.PipelineTask{ + Name: "task2", + }, + TaskRuns: []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "task2", + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }, + }, + }, + }, + Spec: v1.TaskRunSpec{TaskSpec: &v1.TaskSpec{}}, + }, + }, + }, + }, + TasksGraph: &dag.Graph{ + Nodes: map[string]*dag.Node{ + "task1": { + Key: "task1", + Next: []*dag.Node{ + { + Key: "task2", + }, + }, + }, + "task2": { + Key: "task2", + Prev: []*dag.Node{ + { + Key: "task1", + }, + }, + }, + }, + }, + FinalTasksGraph: &dag.Graph{ + Nodes: map[string]*dag.Node{}, + }, + SkipCache: map[string]resources.TaskSkipStatus{ + "task1": {SkippingReason: "None"}, + "task2": {SkippingReason: "None"}, + }, + }, + wantErr: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx, _ := ttesting.SetupFakeContext(t) + testAssets, cancel := getPipelineRunController(t, test.Data{ + PipelineRuns: []*v1.PipelineRun{tc.pr}, + }) + defer cancel() + c := &Reconciler{ + Clock: clock.NewFakePassiveClock(time.Now()), + KubeClientSet: testAssets.Clients.Kube, + PipelineClientSet: testAssets.Clients.Pipeline, + tracerProvider: tracing.New("pipelinerun"), + } + err := c.runNextSchedulableTask(ctx, tc.pr, tc.pipelineRunFacts) + if (err != nil) != tc.wantErr { + t.Errorf("runNextSchedulableTask() error = %v, wantErr %v", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, tc.pipelineRunFacts); diff != "" { + t.Errorf("runNextSchedulableTask() got unexpected pipelineRunFacts diff %s", diff) + } + }) + } +} + func getSignedV1Pipeline(unsigned *pipelinev1.Pipeline, signer signature.Signer, name string) (*pipelinev1.Pipeline, error) { signed := unsigned.DeepCopy() signed.Name = name diff --git a/pkg/reconciler/pipelinerun/resources/apply.go b/pkg/reconciler/pipelinerun/resources/apply.go index 7f1a9057900..936a28939be 100644 --- a/pkg/reconciler/pipelinerun/resources/apply.go +++ b/pkg/reconciler/pipelinerun/resources/apply.go @@ -369,6 +369,30 @@ func propagateParams(t v1.PipelineTask, stringReplacements map[string]string, ar return t } +// PropagateResults propagate the result of the completed task to the unfinished task that is not explicitly specify in the params +func PropagateResults(rpt *ResolvedPipelineTask, runStates PipelineRunState) { + if rpt.ResolvedTask == nil || rpt.ResolvedTask.TaskSpec == nil { + return + } + stringReplacements := map[string]string{} + arrayReplacements := map[string][]string{} + for taskName, taskResults := range runStates.GetTaskRunsResults() { + for _, res := range taskResults { + switch res.Type { + case v1.ResultsTypeString: + stringReplacements[fmt.Sprintf("tasks.%s.results.%s", taskName, res.Name)] = res.Value.StringVal + case v1.ResultsTypeArray: + arrayReplacements[fmt.Sprintf("tasks.%s.results.%s", taskName, res.Name)] = res.Value.ArrayVal + case v1.ResultsTypeObject: + for k, v := range res.Value.ObjectVal { + stringReplacements[fmt.Sprintf("tasks.%s.results.%s.%s", taskName, res.Name, k)] = v + } + } + } + } + rpt.ResolvedTask.TaskSpec = resources.ApplyReplacements(rpt.ResolvedTask.TaskSpec, stringReplacements, arrayReplacements) +} + // 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 or Runs result in a PipelineResult being considered invalid diff --git a/pkg/reconciler/pipelinerun/resources/apply_test.go b/pkg/reconciler/pipelinerun/resources/apply_test.go index 2e0d57dc62a..995404fd01b 100644 --- a/pkg/reconciler/pipelinerun/resources/apply_test.go +++ b/pkg/reconciler/pipelinerun/resources/apply_test.go @@ -26,9 +26,13 @@ import ( v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" resources "github.com/tektoncd/pipeline/pkg/reconciler/pipelinerun/resources" + taskresources "github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources" "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" ) func TestApplyParameters(t *testing.T) { @@ -4251,3 +4255,319 @@ func TestApplyTaskRunContext(t *testing.T) { t.Fatalf("ApplyTaskRunContext() %s", diff.PrintWantGot(d)) } } + +func TestPropagateResults(t *testing.T) { + for _, tt := range []struct { + name string + resolvedTask *resources.ResolvedPipelineTask + runStates resources.PipelineRunState + expectedResolvedTask *resources.ResolvedPipelineTask + }{ + { + name: "propagate string result", + resolvedTask: &resources.ResolvedPipelineTask{ + ResolvedTask: &taskresources.ResolvedTask{ + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + Name: "$(tasks.pt1.results.r1)", + Command: []string{"$(tasks.pt1.results.r2)"}, + Args: []string{"$(tasks.pt2.results.r1)"}, + }, + }, + }, + }, + }, + runStates: resources.PipelineRunState{ + { + PipelineTask: &v1.PipelineTask{ + Name: "pt1", + }, + TaskRuns: []*v1.TaskRun{ + { + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + { + Name: "r1", + Type: v1.ResultsTypeString, + Value: v1.ResultValue{ + StringVal: "step1", + }, + }, + { + Name: "r2", + Type: v1.ResultsTypeString, + Value: v1.ResultValue{ + StringVal: "echo", + }, + }, + }, + }, + }, + }, + }, + }, { + PipelineTask: &v1.PipelineTask{ + Name: "pt2", + }, + TaskRuns: []*v1.TaskRun{ + { + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + { + Name: "r1", + Type: v1.ResultsTypeString, + Value: v1.ResultValue{ + StringVal: "arg1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedResolvedTask: &resources.ResolvedPipelineTask{ + ResolvedTask: &taskresources.ResolvedTask{ + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + Name: "step1", + Command: []string{"echo"}, + Args: []string{"arg1"}, + }, + }, + }, + }, + }, + }, { + name: "propagate array result", + resolvedTask: &resources.ResolvedPipelineTask{ + ResolvedTask: &taskresources.ResolvedTask{ + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + Command: []string{"$(tasks.pt1.results.r1[*])"}, + Args: []string{"$(tasks.pt2.results.r1[*])"}, + }, + }, + }, + }, + }, + runStates: resources.PipelineRunState{ + { + PipelineTask: &v1.PipelineTask{ + Name: "pt1", + }, + TaskRuns: []*v1.TaskRun{ + { + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + { + Name: "r1", + Type: v1.ResultsTypeArray, + Value: v1.ResultValue{ + ArrayVal: []string{"bash", "-c"}, + }, + }, + }, + }, + }, + }, + }, + }, { + PipelineTask: &v1.PipelineTask{ + Name: "pt2", + }, + TaskRuns: []*v1.TaskRun{ + { + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + { + Name: "r1", + Type: v1.ResultsTypeArray, + Value: v1.ResultValue{ + ArrayVal: []string{"echo", "arg1"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedResolvedTask: &resources.ResolvedPipelineTask{ + ResolvedTask: &taskresources.ResolvedTask{ + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + Command: []string{"bash", "-c"}, + Args: []string{"echo", "arg1"}, + }, + }, + }, + }, + }, + }, { + name: "propagate object result", + resolvedTask: &resources.ResolvedPipelineTask{ + ResolvedTask: &taskresources.ResolvedTask{ + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + Command: []string{"$(tasks.pt1.results.r1.command1)", "$(tasks.pt1.results.r1.command2)"}, + Args: []string{"$(tasks.pt2.results.r1.arg1)", "$(tasks.pt2.results.r1.arg2)"}, + }, + }, + }, + }, + }, + runStates: resources.PipelineRunState{ + { + PipelineTask: &v1.PipelineTask{ + Name: "pt1", + }, + TaskRuns: []*v1.TaskRun{ + { + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + { + Name: "r1", + Type: v1.ResultsTypeObject, + Value: v1.ResultValue{ + ObjectVal: map[string]string{ + "command1": "bash", + "command2": "-c", + }, + }, + }, + }, + }, + }, + }, + }, + }, { + PipelineTask: &v1.PipelineTask{ + Name: "pt2", + }, + TaskRuns: []*v1.TaskRun{ + { + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + { + Name: "r1", + Type: v1.ResultsTypeObject, + Value: v1.ResultValue{ + ObjectVal: map[string]string{ + "arg1": "echo", + "arg2": "arg1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedResolvedTask: &resources.ResolvedPipelineTask{ + ResolvedTask: &taskresources.ResolvedTask{ + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{ + { + Command: []string{"bash", "-c"}, + Args: []string{"echo", "arg1"}, + }, + }, + }, + }, + }, + }, { + name: "not propagate result when resolved task is nil", + resolvedTask: &resources.ResolvedPipelineTask{ + ResolvedTask: nil, + }, + runStates: resources.PipelineRunState{}, + expectedResolvedTask: &resources.ResolvedPipelineTask{ + ResolvedTask: nil, + }, + }, { + name: "not propagate result when taskSpec is nil", + resolvedTask: &resources.ResolvedPipelineTask{ + ResolvedTask: &taskresources.ResolvedTask{ + TaskSpec: nil, + }, + }, + runStates: resources.PipelineRunState{}, + expectedResolvedTask: &resources.ResolvedPipelineTask{ + ResolvedTask: &taskresources.ResolvedTask{ + TaskSpec: nil, + }, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + resources.PropagateResults(tt.resolvedTask, tt.runStates) + if d := cmp.Diff(tt.expectedResolvedTask, tt.resolvedTask); d != "" { + t.Fatalf("PropagateResults() %s", diff.PrintWantGot(d)) + } + }) + } +} diff --git a/test/propagated_results_test.go b/test/propagated_results_test.go new file mode 100644 index 00000000000..6af07542851 --- /dev/null +++ b/test/propagated_results_test.go @@ -0,0 +1,482 @@ +//go:build e2e +// +build e2e + +/* +Copyright 2023 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 test + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/test/parse" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + knativetest "knative.dev/pkg/test" +) + +func TestPropagatedResults(t *testing.T) { + t.Parallel() + + ignorePipelineRunStatusFields := cmpopts.IgnoreFields(v1.PipelineRunStatusFields{}, "Provenance") + ignoreTaskRunStatus := cmpopts.IgnoreFields(v1.TaskRunStatusFields{}, "StartTime", "CompletionTime", "Sidecars", "Provenance") + requireAlphaFeatureFlag = requireAnyGate(map[string]string{ + "enable-api-fields": "alpha"}) + type tests struct { + name string + pipelineName string + pipelineRunFunc func(*testing.T, string) (*v1.PipelineRun, *v1.PipelineRun, []*v1.TaskRun) + } + + tds := []tests{{ + name: "propagated all type results", + pipelineName: "propagated-all-type-results", + pipelineRunFunc: getPropagatedResultPipelineRun, + }} + + for _, td := range tds { + td := td + t.Run(td.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + c, namespace := setup(ctx, t, requireAlphaFeatureFlag) + + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer tearDown(ctx, t, c, namespace) + + t.Logf("Setting up test resources for %q test in namespace %s", td.name, namespace) + pipelineRun, expectedResolvedPipelineRun, expectedTaskRuns := td.pipelineRunFunc(t, namespace) + + prName := pipelineRun.Name + _, err := c.V1PipelineRunClient.Create(ctx, pipelineRun, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create PipelineRun `%s`: %s", prName, err) + } + + t.Logf("Waiting for PipelineRun %s in namespace %s to complete", prName, namespace) + if err := WaitForPipelineRunState(ctx, c, prName, timeout, PipelineRunSucceed(prName), "PipelineRunSuccess", v1Version); err != nil { + t.Fatalf("Error waiting for PipelineRun %s to finish: %s", prName, err) + } + cl, _ := c.V1PipelineRunClient.Get(ctx, prName, metav1.GetOptions{}) + d := cmp.Diff(expectedResolvedPipelineRun, cl, + ignoreTypeMeta, + ignoreObjectMeta, + ignoreCondition, + ignorePipelineRunStatus, + ignoreTaskRunStatus, + ignoreConditions, + ignoreContainerStates, + ignoreStepState, + ignoreSAPipelineRunSpec, + ignorePipelineRunStatusFields, + ) + if d != "" { + t.Fatalf(`The resolved spec does not match the expected spec. Here is the diff: %v`, d) + } + for _, tr := range expectedTaskRuns { + t.Logf("Checking Taskrun %s", tr.Name) + taskrun, _ := c.V1TaskRunClient.Get(ctx, tr.Name, metav1.GetOptions{}) + d = cmp.Diff(tr, taskrun, + ignoreTypeMeta, + ignoreObjectMeta, + ignoreCondition, + ignoreTaskRunStatus, + ignoreConditions, + ignoreContainerStates, + ignoreStepState, + ignoreSATaskRunSpec, + ) + if d != "" { + t.Fatalf(`The expected taskrun does not match created taskrun. Here is the diff: %v`, d) + } + } + t.Logf("Successfully finished test %q", td.name) + }) + } +} + +func getPropagatedResultPipelineRun(t *testing.T, namespace string) (*v1.PipelineRun, *v1.PipelineRun, []*v1.TaskRun) { + t.Helper() + pipelineRun := parse.MustParseV1PipelineRun(t, fmt.Sprintf(` +metadata: + name: propagated-all-type-results + namespace: %s +spec: + pipelineSpec: + tasks: + - name: make-uid + taskSpec: + results: + - name: strUid + type: string + - name: arrayUid + type: array + - name: mapUid + type: object + properties: + uid: { + type: string + } + steps: + - name: add-str-uid + image: busybox + command: ["/bin/sh", "-c"] + args: + - echo "1001" | tee $(results.strUid.path) + - name: add-array-uid + image: busybox + command: ["/bin/sh", "-c"] + args: + - echo "[\"1002\", \"1003\"]" | tee $(results.arrayUid.path) + - name: add-map-uid + image: busybox + command: ["/bin/sh", "-c"] + args: + - echo -n "{\"uid\":\"1004\"}" | tee $(results.mapUid.path) + - name: show-uid + taskSpec: + steps: + - name: show-str-uid + image: busybox + command: ["/bin/sh", "-c"] + args: + - echo + - $(tasks.make-uid.results.strUid) + - name: show-array-uid + image: busybox + command: ["/bin/sh", "-c"] + args: + - echo + - $(tasks.make-uid.results.arrayUid[*]) + - name: show-map-uid + image: busybox + command: ["/bin/sh", "-c"] + args: + - echo + - $(tasks.make-uid.results.mapUid.uid) +`, namespace)) + expectedPipelineRun := parse.MustParseV1PipelineRun(t, fmt.Sprintf(` +metadata: + name: propagated-all-type-results + namespace: %s +spec: + pipelineSpec: + tasks: + - name: make-uid + taskSpec: + results: + - name: strUid + type: string + - name: arrayUid + type: array + - name: mapUid + properties: + uid: + type: string + type: object + steps: + - args: + - echo "1001" | tee $(results.strUid.path) + command: + - /bin/sh + - -c + image: busybox + name: add-str-uid + - args: + - echo "[\"1002\", \"1003\"]" | tee $(results.arrayUid.path) + command: + - /bin/sh + - -c + image: busybox + name: add-array-uid + - args: + - echo -n "{\"uid\":\"1004\"}" | tee $(results.mapUid.path) + command: + - /bin/sh + - -c + image: busybox + name: add-map-uid + - name: show-uid + taskSpec: + steps: + - args: + - echo + - $(tasks.make-uid.results.strUid) + command: + - /bin/sh + - -c + image: busybox + name: show-str-uid + - args: + - echo + - $(tasks.make-uid.results.arrayUid[*]) + command: + - /bin/sh + - -c + image: busybox + name: show-array-uid + - args: + - echo + - $(tasks.make-uid.results.mapUid.uid) + command: + - /bin/sh + - -c + image: busybox + name: show-map-uid + timeouts: + pipeline: 1h0m0s +status: + pipelineSpec: + tasks: + - name: make-uid + taskSpec: + results: + - name: strUid + type: string + - name: arrayUid + type: array + - name: mapUid + properties: + uid: + type: string + type: object + steps: + - args: + - echo "1001" | tee $(results.strUid.path) + command: + - /bin/sh + - -c + image: busybox + name: add-str-uid + - args: + - echo "[\"1002\", \"1003\"]" | tee $(results.arrayUid.path) + command: + - /bin/sh + - -c + image: busybox + name: add-array-uid + - args: + - echo -n "{\"uid\":\"1004\"}" | tee $(results.mapUid.path) + command: + - /bin/sh + - -c + image: busybox + name: add-map-uid + - name: show-uid + taskSpec: + steps: + - args: + - echo + - $(tasks.make-uid.results.strUid) + command: + - /bin/sh + - -c + image: busybox + name: show-str-uid + - args: + - echo + - $(tasks.make-uid.results.arrayUid[*]) + command: + - /bin/sh + - -c + image: busybox + name: show-array-uid + - args: + - echo + - $(tasks.make-uid.results.mapUid.uid) + command: + - /bin/sh + - -c + image: busybox + name: show-map-uid +`, namespace)) + makeUidTaskRun := parse.MustParseV1TaskRun(t, fmt.Sprintf(` +metadata: + name: propagated-all-type-results-make-uid + namespace: %s +spec: + taskSpec: + results: + - name: strUid + type: string + - name: arrayUid + type: array + - name: mapUid + properties: + uid: + type: string + type: object + steps: + - args: + - echo "1001" | tee $(results.strUid.path) + command: + - /bin/sh + - -c + image: busybox + name: add-str-uid + - args: + - echo "[\"1002\", \"1003\"]" | tee $(results.arrayUid.path) + command: + - /bin/sh + - -c + image: busybox + name: add-array-uid + - args: + - echo -n "{\"uid\":\"1004\"}" | tee $(results.mapUid.path) + command: + - /bin/sh + - -c + image: busybox + name: add-map-uid + timeout: 1h0m0s +status: + podName: propagated-all-type-results-make-uid-pod + results: + - name: strUid + type: string + value: | + 1001 + - name: arrayUid + type: array + value: + - "1002" + - "1003" + - name: mapUid + type: object + value: + uid: "1004" + steps: + - container: step-add-str-uid + name: add-str-uid + - container: step-add-array-uid + name: add-array-uid + - container: step-add-map-uid + name: add-map-uid + taskSpec: + results: + - name: strUid + type: string + - name: arrayUid + type: array + - name: mapUid + properties: + uid: + type: string + type: object + steps: + - args: + - echo "1001" | tee /tekton/results/strUid + command: + - /bin/sh + - -c + image: busybox + name: add-str-uid + - args: + - echo "[\"1002\", \"1003\"]" | tee /tekton/results/arrayUid + command: + - /bin/sh + - -c + image: busybox + name: add-array-uid + - args: + - echo -n "{\"uid\":\"1004\"}" | tee /tekton/results/mapUid + command: + - /bin/sh + - -c + image: busybox + name: add-map-uid +`, namespace)) + showUidTaskRun := parse.MustParseV1TaskRun(t, fmt.Sprintf(` +metadata: + name: propagated-all-type-results-show-uid + namespace: %s +spec: + taskSpec: + steps: + - args: + - echo + - | + 1001 + command: + - /bin/sh + - -c + image: busybox + name: show-str-uid + - args: + - echo + - "1002" + - "1003" + command: + - /bin/sh + - -c + image: busybox + name: show-array-uid + - args: + - echo + - "1004" + command: + - /bin/sh + - -c + image: busybox + name: show-map-uid + timeout: 1h0m0s +status: + podName: propagated-all-type-results-show-uid-pod + steps: + - container: step-show-str-uid + name: show-str-uid + - container: step-show-array-uid + name: show-array-uid + - container: step-show-map-uid + name: show-map-uid + taskSpec: + steps: + - args: + - echo + - | + 1001 + command: + - /bin/sh + - -c + image: busybox + name: show-str-uid + - args: + - echo + - "1002" + - "1003" + command: + - /bin/sh + - -c + image: busybox + name: show-array-uid + - args: + - echo + - "1004" + command: + - /bin/sh + - -c + image: busybox + name: show-map-uid +`, namespace)) + return pipelineRun, expectedPipelineRun, []*v1.TaskRun{makeUidTaskRun, showUidTaskRun} +}