diff --git a/docs/cmd/tkn_pipeline_start.md b/docs/cmd/tkn_pipeline_start.md index 59024a7bf..0446f7123 100644 --- a/docs/cmd/tkn_pipeline_start.md +++ b/docs/cmd/tkn_pipeline_start.md @@ -78,6 +78,7 @@ my-secret, my-empty-dir and my-volume-claim-template) --showlog show logs right after starting the Pipeline --skip-optional-workspace skips the prompt for optional workspaces --task-serviceaccount strings pass the service account corresponding to the task + --taskrun-spec string local or remote file containing a TaskRunSpec definition --tasks-timeout string timeout for Pipeline TaskRuns --use-param-defaults use default parameter values without prompting for input --use-pipelinerun string use this pipelinerun values to re-run the pipeline. diff --git a/docs/man/man1/tkn-pipeline-start.1 b/docs/man/man1/tkn-pipeline-start.1 index 5633195b0..162df3dc7 100644 --- a/docs/man/man1/tkn-pipeline-start.1 +++ b/docs/man/man1/tkn-pipeline-start.1 @@ -87,6 +87,10 @@ Parameters, at least those that have no default value \fB\-\-task\-serviceaccount\fP=[] pass the service account corresponding to the task +.PP +\fB\-\-taskrun\-spec\fP="" + local or remote file containing a TaskRunSpec definition + .PP \fB\-\-tasks\-timeout\fP="" timeout for Pipeline TaskRuns diff --git a/pkg/cmd/pipeline/start.go b/pkg/cmd/pipeline/start.go index 2052ae490..b909e03a7 100644 --- a/pkg/cmd/pipeline/start.go +++ b/pkg/cmd/pipeline/start.go @@ -85,6 +85,7 @@ type startOptions struct { UseParamDefaults bool TektonOptions flags.TektonOptions PodTemplate string + TaskRunSpec string SkipOptionalWorkspace bool } @@ -206,6 +207,7 @@ For passing the workspaces via flags: c.Flags().StringVarP(&opt.Filename, "filename", "f", "", "local or remote file name containing a Pipeline definition to start a PipelineRun") c.Flags().BoolVarP(&opt.UseParamDefaults, "use-param-defaults", "", false, "use default parameter values without prompting for input") c.Flags().StringVar(&opt.PodTemplate, "pod-template", "", "local or remote file containing a PodTemplate definition") + c.Flags().StringVar(&opt.TaskRunSpec, "taskrun-spec", "", "local or remote file containing a TaskRunSpec definition") c.Flags().BoolVarP(&opt.SkipOptionalWorkspace, "skip-optional-workspace", "", false, "skips the prompt for optional workspaces") c.Flags().StringVarP(&opt.ServiceAccountName, "serviceaccount", "s", "", "pass the serviceaccount name") @@ -355,6 +357,15 @@ func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error { pr.Spec.PodTemplate = &podTemplate } + taskRunSpecLocation := opt.TaskRunSpec + if taskRunSpecLocation != "" { + taskRunSpec, err := pods.ParseTaskRunSpec(cs.HTTPClient, taskRunSpecLocation, file.IsYamlFile(), fmt.Errorf("invalid file format for %s: .yaml or .yml file extension and format required", taskRunSpecLocation)) + if err != nil { + return err + } + pr.Spec.TaskRunSpecs = taskRunSpec + } + if opt.DryRun { format := strings.ToLower(opt.Output) if format == "name" { diff --git a/pkg/cmd/pipeline/start_test.go b/pkg/cmd/pipeline/start_test.go index f731b60cf..e6bfaa6c5 100644 --- a/pkg/cmd/pipeline/start_test.go +++ b/pkg/cmd/pipeline/start_test.go @@ -1010,6 +1010,25 @@ func TestPipelineStart_ExecuteCommand_v1beta1(t *testing.T) { wantError: false, goldenFile: true, }, + + { + name: "Dry Run with TaskRunSpec", + command: []string{ + "start", "test-pipeline", + "-s=svc1", + "-r=source=scaffold-git", + "-p=pipeline-param=value1", + "-p=rev-param=value2", + "-l=jemange=desfrites", + "-n", "ns", + "--dry-run", + "--taskrun-spec", "./testdata/taskrunspec.yaml", + }, + namespace: "", + input: c2, + wantError: false, + goldenFile: true, + }, } for _, tp := range testParams { diff --git a/pkg/cmd/pipeline/start_v1_test.go b/pkg/cmd/pipeline/start_v1_test.go index 6eb48ffba..c49803e56 100644 --- a/pkg/cmd/pipeline/start_v1_test.go +++ b/pkg/cmd/pipeline/start_v1_test.go @@ -855,6 +855,24 @@ func TestPipelineStart_ExecuteCommand(t *testing.T) { wantError: false, goldenFile: true, }, + + { + name: "Dry Run with TaskRunSpec", + command: []string{ + "start", "test-pipeline", + "-s=svc1", + "-p=pipeline-param=value1", + "-p=rev-param=value2", + "-l=jemange=desfrites", + "-n", "ns", + "--dry-run", + "--taskrun-spec", "./testdata/taskrunspec.yaml", + }, + namespace: "", + input: c2, + wantError: false, + goldenFile: true, + }, } for _, tp := range testParams { diff --git a/pkg/cmd/pipeline/testdata/TestPipelineStart_ExecuteCommand-Dry_Run_with_TaskRunSpec.golden b/pkg/cmd/pipeline/testdata/TestPipelineStart_ExecuteCommand-Dry_Run_with_TaskRunSpec.golden new file mode 100644 index 000000000..b7da2b30d --- /dev/null +++ b/pkg/cmd/pipeline/testdata/TestPipelineStart_ExecuteCommand-Dry_Run_with_TaskRunSpec.golden @@ -0,0 +1,26 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + creationTimestamp: null + generateName: test-pipeline-run- + labels: + jemange: desfrites + namespace: ns +spec: + params: + - name: pipeline-param + value: value1 + - name: rev-param + value: value2 + pipelineRef: + name: test-pipeline + taskRunSpecs: + - pipelineTaskName: unit-test-task + podTemplate: + schedulerName: SchedulerName + securityContext: + runAsNonRoot: true + runAsUser: 1001 + taskRunTemplate: + serviceAccountName: svc1 +status: {} diff --git a/pkg/cmd/pipeline/testdata/TestPipelineStart_ExecuteCommand_v1beta1-Dry_Run_with_TaskRunSpec.golden b/pkg/cmd/pipeline/testdata/TestPipelineStart_ExecuteCommand_v1beta1-Dry_Run_with_TaskRunSpec.golden new file mode 100644 index 000000000..89d650fb2 --- /dev/null +++ b/pkg/cmd/pipeline/testdata/TestPipelineStart_ExecuteCommand_v1beta1-Dry_Run_with_TaskRunSpec.golden @@ -0,0 +1,30 @@ +Flag --resource has been deprecated, pipelineresources have been deprecated, this flag will be removed soon +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + creationTimestamp: null + generateName: test-pipeline-run- + labels: + jemange: desfrites + namespace: ns +spec: + params: + - name: pipeline-param + value: value1 + - name: rev-param + value: value2 + pipelineRef: + name: test-pipeline + resources: + - name: source + resourceRef: + name: scaffold-git + serviceAccountName: svc1 + taskRunSpecs: + - pipelineTaskName: unit-test-task + taskPodTemplate: + schedulerName: SchedulerName + securityContext: + runAsNonRoot: true + runAsUser: 1001 +status: {} diff --git a/pkg/cmd/pipeline/testdata/taskrunspec.yaml b/pkg/cmd/pipeline/testdata/taskrunspec.yaml new file mode 100644 index 000000000..83c33bade --- /dev/null +++ b/pkg/cmd/pipeline/testdata/taskrunspec.yaml @@ -0,0 +1,20 @@ +# 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. + +- pipelineTaskName: unit-test-task + taskPodTemplate: + schedulerName: SchedulerName + securityContext: + runAsNonRoot: true + runAsUser: 1001 diff --git a/pkg/pods/task_run_spec.go b/pkg/pods/task_run_spec.go new file mode 100644 index 000000000..7a9fbc7b4 --- /dev/null +++ b/pkg/pods/task_run_spec.go @@ -0,0 +1,39 @@ +// 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 pods + +import ( + "net/http" + + "github.com/tektoncd/cli/pkg/file" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "sigs.k8s.io/yaml" +) + +type TaskRunSpec = []v1beta1.PipelineTaskRunSpec + +func ParseTaskRunSpec(httpClient http.Client, taskRunSpecLocation string, validate file.TypeValidator, errorMsg error) (TaskRunSpec, error) { + taskRunSpec := TaskRunSpec{} + b, err := file.LoadFileContent(httpClient, taskRunSpecLocation, validate, errorMsg) + if err != nil { + return taskRunSpec, err + } + + if err := yaml.UnmarshalStrict(b, &taskRunSpec); err != nil { + return taskRunSpec, err + } + + return taskRunSpec, nil +} diff --git a/pkg/pods/task_run_spec_test.go b/pkg/pods/task_run_spec_test.go new file mode 100644 index 000000000..1abdc0768 --- /dev/null +++ b/pkg/pods/task_run_spec_test.go @@ -0,0 +1,98 @@ +// 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 pods + +import ( + "fmt" + "net/http" + "testing" + + "github.com/tektoncd/cli/pkg/file" + "github.com/tektoncd/cli/pkg/test" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + corev1 "k8s.io/api/core/v1" +) + +func getTestTaskRunSpec() TaskRunSpec { + runAsNonRoot := true + runAsUser := int64(1001) + + return TaskRunSpec{ + v1beta1.PipelineTaskRunSpec{ + PipelineTaskName: "first-create-file", + TaskPodTemplate: &pod.PodTemplate{ + ImagePullSecrets: nil, + HostNetwork: false, + SchedulerName: "SchedulerName", + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &runAsNonRoot, + RunAsUser: &runAsUser, + }, + }, + }, + } +} + +func TestTaskRunSpec_Local_File(t *testing.T) { + httpClient := *http.DefaultClient + podTemplateLocation := "./testdata/taskrunspec.yaml" + + podTemplate, err := ParseTaskRunSpec(httpClient, podTemplateLocation, file.IsYamlFile(), fmt.Errorf("invalid file format for %s: .yaml or .yml file extension and format required", podTemplateLocation)) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + test.AssertOutput(t, getTestTaskRunSpec(), podTemplate) +} + +func TestTaskRunSpec_Local_File_Typo(t *testing.T) { + httpClient := *http.DefaultClient + podTemplateLocation := "./testdata/taskrunspec-typo.yaml" + + _, err := ParseTaskRunSpec(httpClient, podTemplateLocation, file.IsYamlFile(), fmt.Errorf("invalid file format for %s: .yaml or .yml file extension and format required", podTemplateLocation)) + if err == nil { + t.Fatalf("Expected error for local file typo, but error was nil") + } + + expected := `error unmarshaling JSON: while decoding JSON: json: unknown field "ecurityContext"` + test.AssertOutput(t, expected, err.Error()) +} + +func TestTaskRunSpec_Local_File_Not_YAML(t *testing.T) { + httpClient := *http.DefaultClient + podTemplateLocation := "./testdata/taskrunspec-not-yaml" + + _, err := ParseTaskRunSpec(httpClient, podTemplateLocation, file.IsYamlFile(), fmt.Errorf("invalid file format for %s: .yaml or .yml file extension and format required", podTemplateLocation)) + if err == nil { + t.Fatalf("Expected error for local file typo, but error was nil") + } + + expected := "invalid file format for ./testdata/taskrunspec-not-yaml: .yaml or .yml file extension and format required" + test.AssertOutput(t, expected, err.Error()) +} + +func TestTaskRunSpec_Local_File_Not_Found(t *testing.T) { + httpClient := *http.DefaultClient + podTemplateLocation := "./testdata/not-exist.yaml" + + _, err := ParseTaskRunSpec(httpClient, podTemplateLocation, file.IsYamlFile(), fmt.Errorf("invalid file format for %s: .yaml or .yml file extension and format required", podTemplateLocation)) + if err == nil { + t.Fatalf("Expected error for local file typo, but error was nil") + } + + expected := "open ./testdata/not-exist.yaml: no such file or directory" + test.AssertOutput(t, expected, err.Error()) +} diff --git a/pkg/pods/testdata/taskrunspec-not-yaml b/pkg/pods/testdata/taskrunspec-not-yaml new file mode 100644 index 000000000..02a20aa17 --- /dev/null +++ b/pkg/pods/testdata/taskrunspec-not-yaml @@ -0,0 +1,20 @@ +# 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. + +- pipelineTaskName: first-create-file + taskPodTemplate: + schedulerName: SchedulerName + securityContext: + runAsNonRoot: true + runAsUser: 1001 diff --git a/pkg/pods/testdata/taskrunspec-typo.yaml b/pkg/pods/testdata/taskrunspec-typo.yaml new file mode 100644 index 000000000..93ca070c2 --- /dev/null +++ b/pkg/pods/testdata/taskrunspec-typo.yaml @@ -0,0 +1,20 @@ +# 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. + +- pipelineTaskName: first-create-file + taskPodTemplate: + schedulerName: SchedulerName + ecurityContext: + runAsNonRoot: true + runAsUser: 1001 diff --git a/pkg/pods/testdata/taskrunspec.yaml b/pkg/pods/testdata/taskrunspec.yaml new file mode 100644 index 000000000..02a20aa17 --- /dev/null +++ b/pkg/pods/testdata/taskrunspec.yaml @@ -0,0 +1,20 @@ +# 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. + +- pipelineTaskName: first-create-file + taskPodTemplate: + schedulerName: SchedulerName + securityContext: + runAsNonRoot: true + runAsUser: 1001 diff --git a/test/e2e/pipeline/pipeline_test.go b/test/e2e/pipeline/pipeline_test.go index 802024aa6..b8e01bcc9 100644 --- a/test/e2e/pipeline/pipeline_test.go +++ b/test/e2e/pipeline/pipeline_test.go @@ -305,6 +305,23 @@ Waiting for logs to be available... } }) + t.Run("Start PipelineRun with --taskrun-spec", func(t *testing.T) { + tkn.MustSucceed(t, "pipeline", "start", tePipelineName, + "-p=filename=output", + "-w=name=shared-data,emptyDir=", + "--taskrun-spec="+helper.GetResourcePath("/taskrunspec.yaml"), + "--use-param-defaults", + "--showlog") + + time.Sleep(1 * time.Second) + + pipelineRunGeneratedName := builder.GetPipelineRunListWithName(c, tePipelineName, true).Items[0].Name + timeout := 5 * time.Minute + if err := wait.ForPipelineRunState(c, pipelineRunGeneratedName, timeout, wait.PipelineRunSucceed(pipelineRunGeneratedName), "PipelineRunSucceeded"); err != nil { + t.Errorf("Error waiting for PipelineRun to Succeed: %s", err) + } + }) + t.Run("Cancel finished PipelineRun with tkn pipelinerun cancel", func(t *testing.T) { // Get last PipelineRun for pipeline-with-workspace pipelineRunLast := builder.GetPipelineRunListWithName(c, tePipelineName, true).Items[0] diff --git a/test/resources/taskrunspec.yaml b/test/resources/taskrunspec.yaml new file mode 100644 index 000000000..9a6d14d1e --- /dev/null +++ b/test/resources/taskrunspec.yaml @@ -0,0 +1,6 @@ +- pipelineTaskName: first-create-file + taskPodTemplate: + schedulerName: SchedulerName + securityContext: + runAsNonRoot: true + runAsUser: 1001