diff --git a/docs/pipelineruns.md b/docs/pipelineruns.md index 5a3cab00a0b..fbd865294d8 100644 --- a/docs/pipelineruns.md +++ b/docs/pipelineruns.md @@ -13,6 +13,7 @@ weight: 4 - [Speciying `Parameters`](#specifying-parameters) - [Specifying custom `ServiceAccount` credentials](#specifying-custom-serviceaccount-credentials) - [Mapping `ServiceAccount` credentials to `Tasks`](#mapping-serviceaccount-credentials-to-tasks) + - [Specifying `TaskRunSpecs`](#specifying-task-run-specs) - [Specifying a `Pod` template](#specifying-a-pod-template) - [Specifying `Workspaces`](#specifying-workspaces) - [Specifying `LimitRange` values](#specifying-limitrange-values) @@ -58,6 +59,7 @@ A `PipelineRun` definition supports the following fields: object that supplies specific execution credentials for the `Pipeline`. - [`serviceAccountNames`](#mapping-serviceaccount-credentials-to-tasks) - Maps specific `serviceAccountName` values to `Tasks` in the `Pipeline`. This overrides the credentials set for the entire `Pipeline`. + - [`taskRunSpec`](#specifying-task-run-specs) - Specifies a list of `PipelineRunTaskSpec` which allows for setting `ServiceAccountName` and [`Pod` template](./podtemplates.md) for each task. This overrides the `Pod` template set for the entire `Pipeline`. - [`timeout`](#configuring-a-failure-timeout) - Specifies the timeout before the `PipelineRun` fails. - [`podTemplate`](#pod-template) - Specifies a [`Pod` template](./podtemplates.md) to use as the basis for the configuration of the `Pod` that executes each `Task`. @@ -357,3 +359,24 @@ Except as otherwise noted, the content of this page is licensed under the [Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/), and code samples are licensed under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0). + +## Specifying task run specs + +Specifies a list of `PipelineRunTaskSpec` which contains `TaskServiceAccountName`,`TaskPodTemplate` and `TaskName`. Mapping the specs to the corresponding `Task` based upon the `TaskName` a PipelineTask will run with the configured `TaskServiceAccountName` and `TaskPodTemplate` overwriting the pipeline wide [`ServiceAccountName`](#service-account) and [`podTemplate`](#pod-template) configuration, for example: + +```yaml +spec: + podTemplate: + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + fsGroup: 3000 + taskRunSpecs: + - taskName: build-task + taskServiceAccountName: sa-for-build + taskPodTemplate: + nodeSelector: + disktype: ssd +``` + +If used with this `Pipeline`, `build-task` will use the task specific pod template (where `nodeSelector` has `disktype` equal to `ssd`). diff --git a/examples/v1beta1/pipelineruns/pipelinerun-taskrunspecs.yaml b/examples/v1beta1/pipelineruns/pipelinerun-taskrunspecs.yaml new file mode 100644 index 00000000000..3e0eece8d4f --- /dev/null +++ b/examples/v1beta1/pipelineruns/pipelinerun-taskrunspecs.yaml @@ -0,0 +1,86 @@ +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: add-task-taskspec +spec: + params: + - name: first + description: the first operand + - name: second + description: the second operand + results: + - name: sum + description: the sum of the first and second operand + steps: + - name: add + image: alpine + env: + - name: OP1 + value: $(params.first) + - name: OP2 + value: $(params.second) + command: ["/bin/sh", "-c"] + args: + - echo -n $((${OP1}+${OP2})) | tee $(results.sum.path); +--- +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: add-pipeline-taskspec +spec: + params: + - name: first + description: the first operand + - name: second + description: the second operand + - name: third + description: the third operand + tasks: + - name: first-add-taskspec + taskRef: + name: add-task-taskspec + params: + - name: first + value: $(params.first) + - name: second + value: $(params.second) + - name: second-add-taskspec + taskRef: + name: add-task-taskspec + params: + - name: first + value: $(tasks.first-add-taskspec.results.sum) + - name: second + value: $(params.third) + results: + - name: sum + description: the sum of all three operands + value: $(tasks.second-add-taskspec.results.sum) + - name: partial-sum + description: the sum of first two operands + value: $(tasks.first-add-taskspec.results.sum) + - name: all-sum + description: the sum of everything + value: $(tasks.second-add-taskspec.results.sum)-$(tasks.first-add-taskspec.results.sum) +--- +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + name: task-spec-pipeline +spec: + pipelineRef: + name: add-pipeline-taskspec + taskRunSpecs: + - pipelineTaskName: first-add-taskspec + taskServiceAccountName: 'default' + - pipelineTaskName: second-add-taskspec + taskPodTemplate: + nodeSelector: + disktype: ssd + params: + - name: first + value: "2" + - name: second + value: "10" + - name: third + value: "10" diff --git a/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go b/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go index 7dee51a3461..57b62d25c42 100644 --- a/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go +++ b/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go @@ -91,6 +91,9 @@ type PipelineRunSpec struct { // with those declared in the pipeline. // +optional Workspaces []WorkspaceBinding `json:"workspaces,omitempty"` + // TaskRunSpecs holds a set of task specific specs + // +optional + TaskRunSpecs []PipelineTaskRunSpec `json:"taskRunSpecs,omitempty"` } // PipelineRunSpecStatus defines the pipelinerun spec status the user can provide @@ -217,3 +220,24 @@ func (pr *PipelineRun) HasVolumeClaimTemplate() bool { } return false } + +// PipelineTaskRunSpec holds task specific specs +type PipelineTaskRunSpec struct { + PipelineTaskName string `json:"pipelineTaskName,omitempty"` + TaskServiceAccountName string `json:"taskServiceAccountName,omitempty"` + TaskPodTemplate *PodTemplate `json:"taskPodTemplate,omitempty"` +} + +// GetTaskRunSpecs returns the task specific spec for a given +// PipelineTask if configured, otherwise it returns the PipelineRun's default. +func (pr *PipelineRun) GetTaskRunSpecs(pipelineTaskName string) (string, *PodTemplate) { + serviceAccountName := pr.GetServiceAccountName(pipelineTaskName) + taskPodTemplate := pr.Spec.PodTemplate + for _, task := range pr.Spec.TaskRunSpecs { + if task.PipelineTaskName == pipelineTaskName { + taskPodTemplate = task.TaskPodTemplate + serviceAccountName = task.TaskServiceAccountName + } + } + return serviceAccountName, taskPodTemplate +} diff --git a/pkg/apis/pipeline/v1alpha1/pipelinerun_types_test.go b/pkg/apis/pipeline/v1alpha1/pipelinerun_types_test.go index 3f43b877856..a81e5bfd4c4 100644 --- a/pkg/apis/pipeline/v1alpha1/pipelinerun_types_test.go +++ b/pkg/apis/pipeline/v1alpha1/pipelinerun_types_test.go @@ -282,3 +282,110 @@ func TestPipelineRunGetServiceAccountName(t *testing.T) { } } } + +func TestPipelineRunGetPodSpecSABackcompatibility(t *testing.T) { + for _, tt := range []struct { + name string + pr *v1alpha1.PipelineRun + expectedSAs map[string]string + }{ + { + name: "test backward compatibility", + pr: &v1alpha1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pr"}, + Spec: v1alpha1.PipelineRunSpec{ + PipelineRef: &v1alpha1.PipelineRef{Name: "prs"}, + ServiceAccountName: "defaultSA", + ServiceAccountNames: []v1alpha1.PipelineRunSpecServiceAccountName{{ + TaskName: "taskName", ServiceAccountName: "taskSA", + }}, + TaskRunSpecs: []v1alpha1.PipelineTaskRunSpec{{ + PipelineTaskName: "taskName", + TaskServiceAccountName: "newTaskSA", + }}, + }, + }, + expectedSAs: map[string]string{ + "unknown": "defaultSA", + "taskName": "newTaskSA", + }, + }, + { + name: "mixed default SA backward compatibility", + pr: &v1alpha1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pr"}, + Spec: v1alpha1.PipelineRunSpec{ + PipelineRef: &v1alpha1.PipelineRef{Name: "prs"}, + ServiceAccountName: "defaultSA", + TaskRunSpecs: []v1alpha1.PipelineTaskRunSpec{{ + PipelineTaskName: "taskNameOne", + TaskServiceAccountName: "TaskSAOne", + }, { + PipelineTaskName: "taskNameTwo", + TaskServiceAccountName: "newTaskTwo", + }}, + }, + }, + expectedSAs: map[string]string{ + "unknown": "defaultSA", + "taskNameOne": "TaskSAOne", + "taskNameTwo": "newTaskTwo", + }, + }, + } { + for taskName, expected := range tt.expectedSAs { + t.Run(tt.name, func(t *testing.T) { + sa, _ := tt.pr.GetTaskRunSpecs(taskName) + if expected != sa { + t.Errorf("%s: wrong service account: got: %v, want: %v", tt.name, sa, expected) + } + }) + } + } +} + +func TestPipelineRunGetPodSpec(t *testing.T) { + for _, tt := range []struct { + name string + pr *v1alpha1.PipelineRun + expectedPodTemplates map[string][]string + }{ + { + name: "mix default and none default", + pr: &v1alpha1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pr"}, + Spec: v1alpha1.PipelineRunSpec{ + PodTemplate: &v1alpha1.PodTemplate{SchedulerName: "scheduleTest"}, + PipelineRef: &v1alpha1.PipelineRef{Name: "prs"}, + ServiceAccountName: "defaultSA", + TaskRunSpecs: []v1alpha1.PipelineTaskRunSpec{{ + PipelineTaskName: "taskNameOne", + TaskServiceAccountName: "TaskSAOne", + TaskPodTemplate: &v1alpha1.PodTemplate{SchedulerName: "scheduleTestOne"}, + }, { + PipelineTaskName: "taskNameTwo", + TaskServiceAccountName: "newTaskTwo", + TaskPodTemplate: &v1alpha1.PodTemplate{SchedulerName: "scheduleTestTwo"}, + }}, + }, + }, + expectedPodTemplates: map[string][]string{ + "unknown": {"scheduleTest", "defaultSA"}, + "taskNameOne": {"scheduleTestOne", "TaskSAOne"}, + "taskNameTwo": {"scheduleTestTwo", "newTaskTwo"}, + }, + }, + } { + for taskName, values := range tt.expectedPodTemplates { + t.Run(tt.name, func(t *testing.T) { + sa, taskPodTemplate := tt.pr.GetTaskRunSpecs(taskName) + if values[0] != taskPodTemplate.SchedulerName { + t.Errorf("%s: wrong task podtemplate scheduler name: got: %v, want: %v", tt.name, taskPodTemplate.SchedulerName, values[0]) + } + if values[1] != sa { + t.Errorf("%s: wrong service account: got: %v, want: %v", tt.name, sa, values[1]) + } + }) + } + } +} diff --git a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go index 0b3fe44bc8b..881a01da0e7 100644 --- a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go @@ -426,6 +426,13 @@ func (in *PipelineRunSpec) DeepCopyInto(out *PipelineRunSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.TaskRunSpecs != nil { + in, out := &in.TaskRunSpecs, &out.TaskRunSpecs + *out = make([]PipelineTaskRunSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -582,6 +589,27 @@ func (in PipelineTaskList) DeepCopy() PipelineTaskList { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PipelineTaskRunSpec) DeepCopyInto(out *PipelineTaskRunSpec) { + *out = *in + if in.TaskPodTemplate != nil { + in, out := &in.TaskPodTemplate, &out.TaskPodTemplate + *out = new(pod.Template) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PipelineTaskRunSpec. +func (in *PipelineTaskRunSpec) DeepCopy() *PipelineTaskRunSpec { + if in == nil { + return nil + } + out := new(PipelineTaskRunSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Task) DeepCopyInto(out *Task) { *out = *in diff --git a/pkg/apis/pipeline/v1beta1/pipelinerun_types.go b/pkg/apis/pipeline/v1beta1/pipelinerun_types.go index a850f4f01db..16654e6c39a 100644 --- a/pkg/apis/pipeline/v1beta1/pipelinerun_types.go +++ b/pkg/apis/pipeline/v1beta1/pipelinerun_types.go @@ -168,6 +168,9 @@ type PipelineRunSpec struct { // with those declared in the pipeline. // +optional Workspaces []WorkspaceBinding `json:"workspaces,omitempty"` + // TaskRunSpecs holds a set of runtime specs + // +optional + TaskRunSpecs []PipelineTaskRunSpec `json:"taskRunSpecs,omitempty"` } // PipelineRunSpecStatus defines the pipelinerun spec status the user can provide @@ -313,3 +316,25 @@ type PipelineRunList struct { type PipelineTaskRun struct { Name string `json:"name,omitempty"` } + +// PipelineTaskRunSpec can be used to configure specific +// specs for a concrete Task +type PipelineTaskRunSpec struct { + PipelineTaskName string `json:"pipelineTaskName,omitempty"` + TaskServiceAccountName string `json:"taskServiceAccountName,omitempty"` + TaskPodTemplate *PodTemplate `json:"taskPodTemplate,omitempty"` +} + +// GetTaskRunSpecs returns the task specific spec for a given +// PipelineTask if configured, otherwise it returns the PipelineRun's default. +func (pr *PipelineRun) GetTaskRunSpecs(pipelineTaskName string) (string, *PodTemplate) { + serviceAccountName := pr.GetServiceAccountName(pipelineTaskName) + taskPodTemplate := pr.Spec.PodTemplate + for _, task := range pr.Spec.TaskRunSpecs { + if task.PipelineTaskName == pipelineTaskName { + taskPodTemplate = task.TaskPodTemplate + serviceAccountName = task.TaskServiceAccountName + } + } + return serviceAccountName, taskPodTemplate +} diff --git a/pkg/apis/pipeline/v1beta1/pipelinerun_types_test.go b/pkg/apis/pipeline/v1beta1/pipelinerun_types_test.go index f2844ca3e1e..6b15b0c226f 100644 --- a/pkg/apis/pipeline/v1beta1/pipelinerun_types_test.go +++ b/pkg/apis/pipeline/v1beta1/pipelinerun_types_test.go @@ -295,3 +295,110 @@ func TestPipelineRunGetServiceAccountName(t *testing.T) { } } } + +func TestPipelineRunGetPodSpecSABackcompatibility(t *testing.T) { + for _, tt := range []struct { + name string + pr *v1beta1.PipelineRun + expectedSAs map[string]string + }{ + { + name: "test backward compatibility", + pr: &v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pr"}, + Spec: v1beta1.PipelineRunSpec{ + PipelineRef: &v1beta1.PipelineRef{Name: "prs"}, + ServiceAccountName: "defaultSA", + ServiceAccountNames: []v1beta1.PipelineRunSpecServiceAccountName{{ + TaskName: "taskName", ServiceAccountName: "taskSA", + }}, + TaskRunSpecs: []v1beta1.PipelineTaskRunSpec{{ + PipelineTaskName: "taskName", + TaskServiceAccountName: "newTaskSA", + }}, + }, + }, + expectedSAs: map[string]string{ + "unknown": "defaultSA", + "taskName": "newTaskSA", + }, + }, + { + name: "mixed default SA backward compatibility", + pr: &v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pr"}, + Spec: v1beta1.PipelineRunSpec{ + PipelineRef: &v1beta1.PipelineRef{Name: "prs"}, + ServiceAccountName: "defaultSA", + TaskRunSpecs: []v1beta1.PipelineTaskRunSpec{{ + PipelineTaskName: "taskNameOne", + TaskServiceAccountName: "TaskSAOne", + }, { + PipelineTaskName: "taskNameTwo", + TaskServiceAccountName: "newTaskTwo", + }}, + }, + }, + expectedSAs: map[string]string{ + "unknown": "defaultSA", + "taskNameOne": "TaskSAOne", + "taskNameTwo": "newTaskTwo", + }, + }, + } { + for taskName, expected := range tt.expectedSAs { + t.Run(tt.name, func(t *testing.T) { + sa, _ := tt.pr.GetTaskRunSpecs(taskName) + if expected != sa { + t.Errorf("%s: wrong service account: got: %v, want: %v", tt.name, sa, expected) + } + }) + } + } +} + +func TestPipelineRunGetPodSpec(t *testing.T) { + for _, tt := range []struct { + name string + pr *v1beta1.PipelineRun + expectedPodTemplates map[string][]string + }{ + { + name: "mix default and none default", + pr: &v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pr"}, + Spec: v1beta1.PipelineRunSpec{ + PodTemplate: &v1beta1.PodTemplate{SchedulerName: "scheduleTest"}, + PipelineRef: &v1beta1.PipelineRef{Name: "prs"}, + ServiceAccountName: "defaultSA", + TaskRunSpecs: []v1beta1.PipelineTaskRunSpec{{ + PipelineTaskName: "taskNameOne", + TaskServiceAccountName: "TaskSAOne", + TaskPodTemplate: &v1beta1.PodTemplate{SchedulerName: "scheduleTestOne"}, + }, { + PipelineTaskName: "taskNameTwo", + TaskServiceAccountName: "newTaskTwo", + TaskPodTemplate: &v1beta1.PodTemplate{SchedulerName: "scheduleTestTwo"}, + }}, + }, + }, + expectedPodTemplates: map[string][]string{ + "unknown": {"scheduleTest", "defaultSA"}, + "taskNameOne": {"scheduleTestOne", "TaskSAOne"}, + "taskNameTwo": {"scheduleTestTwo", "newTaskTwo"}, + }, + }, + } { + for taskName, values := range tt.expectedPodTemplates { + t.Run(tt.name, func(t *testing.T) { + sa, taskPodTemplate := tt.pr.GetTaskRunSpecs(taskName) + if values[0] != taskPodTemplate.SchedulerName { + t.Errorf("%s: wrong task podtemplate scheduler name: got: %v, want: %v", tt.name, taskPodTemplate.SchedulerName, values[0]) + } + if values[1] != sa { + t.Errorf("%s: wrong service account: got: %v, want: %v", tt.name, sa, values[1]) + } + }) + } + } +} diff --git a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go index 40fc616861b..67bd84b41d6 100644 --- a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go @@ -594,6 +594,13 @@ func (in *PipelineRunSpec) DeepCopyInto(out *PipelineRunSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.TaskRunSpecs != nil { + in, out := &in.TaskRunSpecs, &out.TaskRunSpecs + *out = make([]PipelineTaskRunSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -980,6 +987,27 @@ 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 *PipelineTaskRunSpec) DeepCopyInto(out *PipelineTaskRunSpec) { + *out = *in + if in.TaskPodTemplate != nil { + in, out := &in.TaskPodTemplate, &out.TaskPodTemplate + *out = new(pod.Template) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PipelineTaskRunSpec. +func (in *PipelineTaskRunSpec) DeepCopy() *PipelineTaskRunSpec { + if in == nil { + return nil + } + out := new(PipelineTaskRunSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResultRef) DeepCopyInto(out *ResultRef) { *out = *in diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index e297cf8d100..6d261a1a5b3 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -910,7 +910,7 @@ func (c *Reconciler) makeConditionCheckContainer(rprt *resources.ResolvedPipelin if err != nil { return nil, fmt.Errorf("failed to get TaskSpec from Condition: %w", err) } - + serviceAccountName, podtemplate := pr.GetTaskRunSpecs(rprt.PipelineTask.Name) tr := &v1alpha1.TaskRun{ ObjectMeta: metav1.ObjectMeta{ Name: rcc.ConditionCheckName, @@ -921,13 +921,13 @@ func (c *Reconciler) makeConditionCheckContainer(rprt *resources.ResolvedPipelin }, Spec: v1alpha1.TaskRunSpec{ TaskSpec: taskSpec, - ServiceAccountName: pr.GetServiceAccountName(rprt.PipelineTask.Name), + ServiceAccountName: serviceAccountName, Params: rcc.PipelineTaskCondition.Params, Resources: &v1beta1.TaskRunResources{ Inputs: rcc.ToTaskResourceBindings(), }, Timeout: getTaskRunTimeout(pr, rprt), - PodTemplate: pr.Spec.PodTemplate, + PodTemplate: podtemplate, }} cctr, err := c.PipelineClientSet.TektonV1alpha1().TaskRuns(pr.Namespace).Create(tr)