From fe42657aee2bc4c966df788c500ee1d3571f5bed Mon Sep 17 00:00:00 2001 From: qingliu Date: Tue, 19 Mar 2024 14:08:06 +0800 Subject: [PATCH] fix: ensure default type for params in remote tasks to prevent pipeline failures fix #7775 In the existing logic, resources used for ConvertTo should have default values set. Otherwise, there could be issues with incorrect parameter types being set (e.g., an array type being treated as a string type). However, resources fetched from remote sources haven't undergone the SetDefaults operation. If we directly invoke the ConvertTo operation, it might result in erroneous outcomes. For instance, a v1beta1 ClusterTask that undergoes a direct ConvertTo to convert the resource into a v1 Task for validation might be mistakenly considered invalid. Additionally, even if a v1beta1 Task passes validation, the process of converting it to a v1 Task could still incorrectly set default parameter types, leading to errors during execution. --- .../pipelinerun/pipelinerun_test.go | 10 +++--- .../pipelinerun/resources/pipelineref.go | 4 +++ .../pipelinerun/resources/pipelineref_test.go | 22 ++++++++++--- pkg/reconciler/taskrun/resources/taskref.go | 5 +++ .../taskrun/resources/taskref_test.go | 16 ++++++--- test/parse/yaml.go | 33 +++++++++++++++++++ 6 files changed, 75 insertions(+), 15 deletions(-) diff --git a/pkg/reconciler/pipelinerun/pipelinerun_test.go b/pkg/reconciler/pipelinerun/pipelinerun_test.go index de293772183..b7c72df80ea 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/pipelinerun/pipelinerun_test.go @@ -15923,7 +15923,7 @@ spec: func TestReconcile_verifyResolved_V1beta1Pipeline_NoError(t *testing.T) { resolverName := "foobar" - ts := parse.MustParseV1beta1Task(t, ` + ts := parse.MustParseV1beta1TaskAndSetDefaults(t, ` metadata: name: test-task namespace: foo @@ -15947,7 +15947,7 @@ spec: t.Fatal("fail to marshal task", err) } - ps := parse.MustParseV1beta1Pipeline(t, fmt.Sprintf(` + ps := parse.MustParseV1beta1PipelineAndSetDefaults(t, fmt.Sprintf(` metadata: name: test-pipeline namespace: foo @@ -16260,7 +16260,7 @@ spec: func TestReconcile_verifyResolved_V1Pipeline_NoError(t *testing.T) { resolverName := "foobar" - ts := parse.MustParseV1Task(t, ` + ts := parse.MustParseV1TaskAndSetDefaults(t, ` metadata: name: test-task namespace: foo @@ -16284,7 +16284,7 @@ spec: t.Fatal("fail to marshal task", err) } - ps := parse.MustParseV1Pipeline(t, fmt.Sprintf(` + ps := parse.MustParseV1PipelineAndSetDefaults(t, fmt.Sprintf(` metadata: name: test-pipeline namespace: foo @@ -16416,7 +16416,7 @@ func TestReconcile_verifyResolved_V1Pipeline_Error(t *testing.T) { resolverName := "foobar" // Case1: unsigned Pipeline refers to unsigned Task - unsignedTask := parse.MustParseV1beta1Task(t, ` + unsignedTask := parse.MustParseV1beta1TaskAndSetDefaults(t, ` metadata: name: test-task namespace: foo diff --git a/pkg/reconciler/pipelinerun/resources/pipelineref.go b/pkg/reconciler/pipelinerun/resources/pipelineref.go index a6674f15483..71fd76669dd 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelineref.go +++ b/pkg/reconciler/pipelinerun/resources/pipelineref.go @@ -136,6 +136,7 @@ func resolvePipeline(ctx context.Context, resolver remote.Resolver, name string, func readRuntimeObjectAsPipeline(ctx context.Context, namespace string, obj runtime.Object, k8s kubernetes.Interface, tekton clientset.Interface, refSource *v1.RefSource, verificationPolicies []*v1alpha1.VerificationPolicy) (*v1.Pipeline, *trustedresources.VerificationResult, error) { switch obj := obj.(type) { case *v1beta1.Pipeline: + obj.SetDefaults(ctx) // Verify the Pipeline once we fetch from the remote resolution, mutating, validation and conversion of the pipeline should happen after the verification, since signatures are based on the remote pipeline contents vr := trustedresources.VerifyResource(ctx, obj, k8s, refSource, verificationPolicies) // Issue a dry-run request to create the remote Pipeline, so that it can undergo validation from validating admission webhooks @@ -154,6 +155,9 @@ func readRuntimeObjectAsPipeline(ctx context.Context, namespace string, obj runt } return p, &vr, nil case *v1.Pipeline: + // This SetDefaults is currently not necessary, but for consistency, it is recommended to add it. + // Avoid forgetting to add it in the future when there is a v2 version, causing similar problems. + obj.SetDefaults(ctx) vr := trustedresources.VerifyResource(ctx, obj, k8s, refSource, verificationPolicies) // Issue a dry-run request to create the remote Pipeline, so that it can undergo validation from validating admission webhooks // without actually creating the Pipeline on the cluster diff --git a/pkg/reconciler/pipelinerun/resources/pipelineref_test.go b/pkg/reconciler/pipelinerun/resources/pipelineref_test.go index 93d01a52ecc..9b173bc49dc 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelineref_test.go +++ b/pkg/reconciler/pipelinerun/resources/pipelineref_test.go @@ -316,7 +316,7 @@ func TestGetPipelineFunc_RemoteResolution(t *testing.T) { "apiVersion: tekton.dev/v1beta1", pipelineYAMLString, }, "\n"), - wantPipeline: parse.MustParseV1Pipeline(t, pipelineYAMLString), + wantPipeline: parse.MustParseV1PipelineAndSetDefaults(t, pipelineYAMLString), }, { name: "v1 pipeline", pipelineYAML: strings.Join([]string{ @@ -324,7 +324,7 @@ func TestGetPipelineFunc_RemoteResolution(t *testing.T) { "apiVersion: tekton.dev/v1", pipelineYAMLString, }, "\n"), - wantPipeline: parse.MustParseV1Pipeline(t, pipelineYAMLString), + wantPipeline: parse.MustParseV1PipelineAndSetDefaults(t, pipelineYAMLString), }, { name: "v1 remote pipeline without defaults", pipelineYAML: strings.Join([]string{ @@ -332,7 +332,7 @@ func TestGetPipelineFunc_RemoteResolution(t *testing.T) { "apiVersion: tekton.dev/v1", pipelineYAMLStringWithoutDefaults, }, "\n"), - wantPipeline: parse.MustParseV1Pipeline(t, pipelineYAMLStringWithoutDefaults), + wantPipeline: parse.MustParseV1PipelineAndSetDefaults(t, pipelineYAMLStringWithoutDefaults), }, { name: "v1 remote pipeline with different namespace", pipelineYAML: strings.Join([]string{ @@ -340,7 +340,7 @@ func TestGetPipelineFunc_RemoteResolution(t *testing.T) { "apiVersion: tekton.dev/v1", pipelineYAMLStringNamespaceFoo, }, "\n"), - wantPipeline: parse.MustParseV1Pipeline(t, pipelineYAMLStringNamespaceFoo), + wantPipeline: parse.MustParseV1PipelineAndSetDefaults(t, pipelineYAMLStringNamespaceFoo), }} for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { @@ -433,7 +433,7 @@ func TestGetPipelineFunc_RemoteResolution_ReplacedParams(t *testing.T) { ctx, clients := getFakePipelineClient(t) cfg := config.FromContextOrDefaults(ctx) ctx = config.ToContext(ctx, cfg) - pipeline := parse.MustParseV1Pipeline(t, pipelineYAMLString) + pipeline := parse.MustParseV1PipelineAndSetDefaults(t, pipelineYAMLString) pipelineRef := &v1.PipelineRef{ ResolverRef: v1.ResolverRef{ Resolver: "git", @@ -1315,9 +1315,21 @@ var pipelineYAMLString = ` metadata: name: foo spec: + params: + - name: array + # type: array + default: + - "bar" + - "bar" tasks: - name: task1 taskSpec: + params: + - name: array + # type: array + default: + - "bar" + - "bar" steps: - name: step1 image: ubuntu diff --git a/pkg/reconciler/taskrun/resources/taskref.go b/pkg/reconciler/taskrun/resources/taskref.go index 368ce8b78c7..7bddf0b0523 100644 --- a/pkg/reconciler/taskrun/resources/taskref.go +++ b/pkg/reconciler/taskrun/resources/taskref.go @@ -233,6 +233,7 @@ func resolveStepAction(ctx context.Context, resolver remote.Resolver, name, name func readRuntimeObjectAsTask(ctx context.Context, namespace string, obj runtime.Object, k8s kubernetes.Interface, tekton clientset.Interface, refSource *v1.RefSource, verificationPolicies []*v1alpha1.VerificationPolicy) (*v1.Task, *trustedresources.VerificationResult, error) { switch obj := obj.(type) { case *v1beta1.Task: + obj.SetDefaults(ctx) // Verify the Task once we fetch from the remote resolution, mutating, validation and conversion of the task should happen after the verification, since signatures are based on the remote task contents vr := trustedresources.VerifyResource(ctx, obj, k8s, refSource, verificationPolicies) // Issue a dry-run request to create the remote Task, so that it can undergo validation from validating admission webhooks @@ -251,6 +252,7 @@ func readRuntimeObjectAsTask(ctx context.Context, namespace string, obj runtime. } return t, &vr, nil case *v1beta1.ClusterTask: + obj.SetDefaults(ctx) t, err := convertClusterTaskToTask(ctx, *obj) // Issue a dry-run request to create the remote Task, so that it can undergo validation from validating admission webhooks // without actually creating the Task on the cluster @@ -259,6 +261,9 @@ func readRuntimeObjectAsTask(ctx context.Context, namespace string, obj runtime. } return t, nil, err case *v1.Task: + // This SetDefaults is currently not necessary, but for consistency, it is recommended to add it. + // Avoid forgetting to add it in the future when there is a v2 version, causing similar problems. + obj.SetDefaults(ctx) vr := trustedresources.VerifyResource(ctx, obj, k8s, refSource, verificationPolicies) // Issue a dry-run request to create the remote Task, so that it can undergo validation from validating admission webhooks // without actually creating the Task on the cluster diff --git a/pkg/reconciler/taskrun/resources/taskref_test.go b/pkg/reconciler/taskrun/resources/taskref_test.go index e2d6ca79e24..1f81947dd95 100644 --- a/pkg/reconciler/taskrun/resources/taskref_test.go +++ b/pkg/reconciler/taskrun/resources/taskref_test.go @@ -1062,7 +1062,7 @@ func TestGetTaskFunc_RemoteResolution(t *testing.T) { "apiVersion: tekton.dev/v1beta1", taskYAMLString, }, "\n"), - wantTask: parse.MustParseV1Task(t, taskYAMLString), + wantTask: parse.MustParseV1TaskAndSetDefaults(t, taskYAMLString), }, { name: "v1beta1 cluster task", taskYAML: strings.Join([]string{ @@ -1070,7 +1070,7 @@ func TestGetTaskFunc_RemoteResolution(t *testing.T) { "apiVersion: tekton.dev/v1beta1", taskYAMLString, }, "\n"), - wantTask: parse.MustParseV1Task(t, taskYAMLString), + wantTask: parse.MustParseV1TaskAndSetDefaults(t, taskYAMLString), }, { name: "v1 task", taskYAML: strings.Join([]string{ @@ -1078,7 +1078,7 @@ func TestGetTaskFunc_RemoteResolution(t *testing.T) { "apiVersion: tekton.dev/v1", taskYAMLString, }, "\n"), - wantTask: parse.MustParseV1Task(t, taskYAMLString), + wantTask: parse.MustParseV1TaskAndSetDefaults(t, taskYAMLString), }, { name: "v1 task without defaults", taskYAML: strings.Join([]string{ @@ -1086,7 +1086,7 @@ func TestGetTaskFunc_RemoteResolution(t *testing.T) { "apiVersion: tekton.dev/v1", remoteTaskYamlWithoutDefaults, }, "\n"), - wantTask: parse.MustParseV1Task(t, remoteTaskYamlWithoutDefaults), + wantTask: parse.MustParseV1TaskAndSetDefaults(t, remoteTaskYamlWithoutDefaults), }} for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { @@ -1189,7 +1189,7 @@ func TestGetTaskFunc_RemoteResolution_ReplacedParams(t *testing.T) { ctx := context.Background() cfg := config.FromContextOrDefaults(ctx) ctx = config.ToContext(ctx, cfg) - task := parse.MustParseV1Task(t, taskYAMLString) + task := parse.MustParseV1TaskAndSetDefaults(t, taskYAMLString) taskRef := &v1.TaskRef{ ResolverRef: v1.ResolverRef{ Resolver: "git", @@ -1899,6 +1899,12 @@ var taskYAMLString = ` metadata: name: foo spec: + params: + - name: array + # type: array + default: + - "bar" + - "bar" steps: - name: step1 image: ubuntu diff --git a/test/parse/yaml.go b/test/parse/yaml.go index 8b0a30e11c7..68bc16e14b5 100644 --- a/test/parse/yaml.go +++ b/test/parse/yaml.go @@ -14,6 +14,7 @@ package parse import ( + "context" "testing" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" @@ -67,6 +68,14 @@ kind: Task return &task } +// MustParseV1beta1TaskAndSetDefaults takes YAML and parses it into a *v1beta1.Task and sets defaults +func MustParseV1beta1TaskAndSetDefaults(t *testing.T, yaml string) *v1beta1.Task { + t.Helper() + task := MustParseV1beta1Task(t, yaml) + task.SetDefaults(context.Background()) + return task +} + // MustParseCustomRun takes YAML and parses it into a *v1beta1.CustomRun func MustParseCustomRun(t *testing.T, yaml string) *v1beta1.CustomRun { t.Helper() @@ -89,6 +98,14 @@ kind: Task return &task } +// MustParseV1TaskAndSetDefaults takes YAML and parses it into a *v1.Task and sets defaults +func MustParseV1TaskAndSetDefaults(t *testing.T, yaml string) *v1.Task { + t.Helper() + task := MustParseV1Task(t, yaml) + task.SetDefaults(context.Background()) + return task +} + // MustParseClusterTask takes YAML and parses it into a *v1beta1.ClusterTask func MustParseClusterTask(t *testing.T, yaml string) *v1beta1.ClusterTask { t.Helper() @@ -133,6 +150,14 @@ kind: Pipeline return &pipeline } +// MustParseV1beta1PipelineAndSetDefaults takes YAML and parses it into a *v1beta1.Pipeline and sets defaults +func MustParseV1beta1PipelineAndSetDefaults(t *testing.T, yaml string) *v1beta1.Pipeline { + t.Helper() + p := MustParseV1beta1Pipeline(t, yaml) + p.SetDefaults(context.Background()) + return p +} + // MustParseV1Pipeline takes YAML and parses it into a *v1.Pipeline func MustParseV1Pipeline(t *testing.T, yaml string) *v1.Pipeline { t.Helper() @@ -144,6 +169,14 @@ kind: Pipeline return &pipeline } +// MustParseV1PipelineAndSetDefaults takes YAML and parses it into a *v1.Pipeline and sets defaults +func MustParseV1PipelineAndSetDefaults(t *testing.T, yaml string) *v1.Pipeline { + t.Helper() + p := MustParseV1Pipeline(t, yaml) + p.SetDefaults(context.Background()) + return p +} + // MustParseVerificationPolicy takes YAML and parses it into a *v1alpha1.VerificationPolicy func MustParseVerificationPolicy(t *testing.T, yaml string) *v1alpha1.VerificationPolicy { t.Helper()