diff --git a/docs/pipelineruns.md b/docs/pipelineruns.md index ba6daabc9f4..a0ff05679a4 100644 --- a/docs/pipelineruns.md +++ b/docs/pipelineruns.md @@ -258,6 +258,9 @@ set for the target [`namespace`](https://kubernetes.io/docs/concepts/overview/wo For more information, see [`ServiceAccount`](auth.md). +[`Custom tasks`](pipelines.md#using-custom-tasks) may or may not use a service account name. +Consult the documentation of the custom task that you are using to determine whether it supports a service account name. + ### Mapping `ServiceAccount` credentials to `Tasks` If you require more granularity in specifying execution credentials, use the `serviceAccountNames` field to @@ -341,6 +344,9 @@ spec: claimName: my-volume-claim ``` +[`Custom tasks`](pipelines.md#using-custom-tasks) may or may not use a pod template. +Consult the documentation of the custom task that you are using to determine whether it supports a pod template. + ### Specifying taskRunSpecs Specifies a list of `PipelineTaskRunSpec` which contains `TaskServiceAccountName`, `TaskPodTemplate` @@ -385,6 +391,9 @@ For more information, see the following topics: - For a list of supported `Volume` types, see [Specifying `VolumeSources` in `Workspaces`](workspaces.md#specifying-volumesources-in-workspaces). - For an end-to-end example, see [`Workspaces` in a `PipelineRun`](../examples/v1beta1/pipelineruns/workspaces.yaml). +[`Custom tasks`](pipelines.md#using-custom-tasks) may or may not use workspaces. +Consult the documentation of the custom task that you are using to determine whether it supports workspaces. + ### Specifying `LimitRange` values In order to only consume the bare minimum amount of resources needed to execute one `Step` at a diff --git a/docs/pipelines.md b/docs/pipelines.md index 9a15c0c718f..9db48dd7679 100644 --- a/docs/pipelines.md +++ b/docs/pipelines.md @@ -954,13 +954,12 @@ If the `taskRef` specifies a name, the custom task controller should look up the If the `taskRef` does not specify a name, the custom task controller might support some default behavior for executing unnamed tasks. -### Specifying `Parameters` +### Specifying parameters If a custom task supports [`parameters`](tasks.md#parameters), you can use the `params` field to specify their values: ```yaml -spec: spec: tasks: - name: run-custom-task @@ -973,6 +972,24 @@ spec: value: bah ``` +### Specifying workspaces + +If the custom task supports it, you can provide [`Workspaces`](workspaces.md#using-workspaces-in-tasks) to share data with the custom task. + +```yaml +spec: + tasks: + - name: run-custom-task + taskRef: + apiVersion: example.dev/v1alpha1 + kind: Example + name: myexample + workspaces: + - name: my-workspace +``` + +Consult the documentation of the custom task that you are using to determine whether it supports workspaces and how to name them. + ### Using `Results` If the custom task produces results, you can reference them in a Pipeline using the normal syntax, @@ -980,14 +997,11 @@ If the custom task produces results, you can reference them in a Pipeline using ### Limitations -Pipelines do not directly support passing the following items to custom tasks: +Pipelines do not support the following items with custom tasks: * Pipeline Resources -* Workspaces -* Service account name -* Pod templates - -A pipeline task that references a custom task cannot reference `Conditions`. -`Conditions` are deprecated. Use [`WhenExpressions`](#guard-task-execution-using-whenexpressions) instead. +* [`retries`](#using-the-retries-parameter) +* [`timeout`](#configuring-the-failure-timeout) +* Conditions (`Conditions` are deprecated. Use [`WhenExpressions`](#guard-task-execution-using-whenexpressions) instead.) ## Code examples diff --git a/docs/runs.md b/docs/runs.md index 87611eb9ea5..1a307effebb 100644 --- a/docs/runs.md +++ b/docs/runs.md @@ -10,7 +10,8 @@ weight: 2 - [Overview](#overview) - [Configuring a `Run`](#configuring-a-run) - [Specifying the target Custom Task](#specifying-the-target-custom-task) - - [Specifying `Parameters`](#specifying-parameters) + - [Specifying Parameters](#specifying-parameters) + - [Specifying Workspaces, Service Account, and Pod Template](#specifying-workspaces-service-account-and-pod-template) - [Monitoring execution status](#monitoring-execution-status) - [Monitoring `Results`](#monitoring-results) - [Code examples](#code-examples) @@ -51,6 +52,12 @@ A `Run` definition supports the following fields: - Optional: - [`params`](#specifying-parameters) - Specifies the desired execution parameters for the custom task. + - [`serviceAccountName`](#specifying-a-serviceaccount) - Specifies a `ServiceAccount` + object that provides custom credentials for executing the `Run`. + - [`workspaces`](#specifying-workspaces) - Specifies the physical volumes to use for the + [`Workspaces`](workspaces.md) required by a custom task. + - [`podTemplate`](#specifying-a-pod-template) - Specifies a [`Pod` template](podtemplates.md) to use + to configure pods created by the custom task. [kubernetes-overview]: https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#required-fields @@ -108,6 +115,57 @@ If the custom task controller knows how to interpret the parameter value, it will do so. It might enforce that some parameter values must be specified, or reject unknown parameter values. +### Specifying Workspaces, Service Account, and Pod Template + +A `Run` object can specify workspaces, a service account name, or a pod template. +These are intended to be used with custom tasks that create Pods or other resources that embed a Pod specification. +The custom task can use these specifications to construct the Pod specification. +Not all custom tasks will support these values. +Consult the documentation of the custom task that you are using to determine whether these values apply. + +#### Specifying workspaces + +If the custom task supports it, you can provide [`Workspaces`](workspaces.md) to share data with the custom task. + +```yaml +spec: + workspaces: + - name: my-workspace + emptyDir: {} +``` + +Consult the documentation of the custom task that you are using to determine whether it supports workspaces and how to name them. + +#### Specifying a ServiceAccount + +If the custom task supports it, you can execute the `Run` with a specific set of credentials by +specifying a `ServiceAccount` object name in the `serviceAccountName` field in your `Run` +definition. If you do not explicitly specify this, the `Run` executes with the service account +specified in the `configmap-defaults` `ConfigMap`. If this default is not specified, `Runs` +will execute with the [`default` service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server) +set for the target [`namespace`](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/). + +```yaml +spec: + serviceAccountName: my-account +``` + +Consult the documentation of the custom task that you are using to determine whether it supports a service account name. + +#### Specifying a pod template + +If the custom task supports it, you can specify a [`Pod` template](podtemplates.md) configuration that the custom task will +use to configure Pods (or other resources that embed a Pod specification) that it creates. + +```yaml +spec: + podTemplate: + securityContext: + runAsUser: 1001 +``` + +Consult the documentation of the custom task that you are using to determine whether it supports a pod template. + ## Monitoring execution status As your `Run` executes, its `status` field accumulates information on the diff --git a/pkg/apis/pipeline/v1alpha1/run_defaults.go b/pkg/apis/pipeline/v1alpha1/run_defaults.go index 9dcd8627147..133ec5e1609 100644 --- a/pkg/apis/pipeline/v1alpha1/run_defaults.go +++ b/pkg/apis/pipeline/v1alpha1/run_defaults.go @@ -19,6 +19,7 @@ package v1alpha1 import ( "context" + "github.com/tektoncd/pipeline/pkg/apis/config" "knative.dev/pkg/apis" ) @@ -30,5 +31,13 @@ func (r *Run) SetDefaults(ctx context.Context) { } func (rs *RunSpec) SetDefaults(ctx context.Context) { - // No defaults to set. + cfg := config.FromContextOrDefaults(ctx) + defaultSA := cfg.Defaults.DefaultServiceAccount + if rs.ServiceAccountName == "" && defaultSA != "" { + rs.ServiceAccountName = defaultSA + } + defaultPodTemplate := cfg.Defaults.DefaultPodTemplate + if rs.PodTemplate == nil { + rs.PodTemplate = defaultPodTemplate + } } diff --git a/pkg/apis/pipeline/v1alpha1/run_types.go b/pkg/apis/pipeline/v1alpha1/run_types.go index 6cb820ef5b6..aea0cf17259 100644 --- a/pkg/apis/pipeline/v1alpha1/run_types.go +++ b/pkg/apis/pipeline/v1alpha1/run_types.go @@ -48,10 +48,20 @@ type RunSpec struct { // +optional Status RunSpecStatus `json:"status,omitempty"` + // +optional + ServiceAccountName string `json:"serviceAccountName"` + + // PodTemplate holds pod specific configuration + // +optional + PodTemplate *PodTemplate `json:"podTemplate,omitempty"` + + // Workspaces is a list of WorkspaceBindings from volumes to workspaces. + // +optional + Workspaces []v1beta1.WorkspaceBinding `json:"workspaces,omitempty"` + // TODO(https://github.com/tektoncd/community/pull/128) // - timeout // - inline task spec - // - workspaces ? } // RunSpecStatus defines the taskrun spec status the user can provide diff --git a/pkg/apis/pipeline/v1alpha1/run_validation.go b/pkg/apis/pipeline/v1alpha1/run_validation.go index 5fd50f87af0..5a68359f172 100644 --- a/pkg/apis/pipeline/v1alpha1/run_validation.go +++ b/pkg/apis/pipeline/v1alpha1/run_validation.go @@ -54,5 +54,9 @@ func (rs *RunSpec) Validate(ctx context.Context) *apis.FieldError { return err } + if err := validateWorkspaceBindings(ctx, rs.Workspaces); err != nil { + return err + } + return nil } diff --git a/pkg/apis/pipeline/v1alpha1/run_validation_test.go b/pkg/apis/pipeline/v1alpha1/run_validation_test.go index 645b481739f..baa827d7f4c 100644 --- a/pkg/apis/pipeline/v1alpha1/run_validation_test.go +++ b/pkg/apis/pipeline/v1alpha1/run_validation_test.go @@ -24,6 +24,7 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" v1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/test/diff" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/pkg/apis" ) @@ -145,6 +146,20 @@ func TestRun_Valid(t *testing.T) { }}, }, }, + }, { + name: "valid workspace", + run: &v1alpha1.Run{ + Spec: v1alpha1.RunSpec{ + Ref: &v1alpha1.TaskRef{ + APIVersion: "blah", + Kind: "blah", + }, + Workspaces: []v1beta1.WorkspaceBinding{{ + Name: "workspace", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}, + }, + }, }} { t.Run(c.name, func(t *testing.T) { if err := c.run.Validate(context.Background()); err != nil { @@ -153,3 +168,56 @@ func TestRun_Valid(t *testing.T) { }) } } + +func TestRun_Workspaces_Invalid(t *testing.T) { + tests := []struct { + name string + run *v1alpha1.Run + wantErr *apis.FieldError + }{{ + name: "make sure WorkspaceBinding validation invoked", + run: &v1alpha1.Run{ + Spec: v1alpha1.RunSpec{ + Ref: &v1alpha1.TaskRef{ + APIVersion: "blah", + Kind: "blah", + }, + Workspaces: []v1beta1.WorkspaceBinding{{ + Name: "workspace", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "", + }, + }}, + }, + }, + wantErr: apis.ErrMissingField("workspace.persistentvolumeclaim.claimname"), + }, { + name: "bind same workspace twice", + run: &v1alpha1.Run{ + Spec: v1alpha1.RunSpec{ + Ref: &v1alpha1.TaskRef{ + APIVersion: "blah", + Kind: "blah", + }, + Workspaces: []v1beta1.WorkspaceBinding{{ + Name: "workspace", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, { + Name: "workspace", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}, + }, + }, + wantErr: apis.ErrMultipleOneOf("spec.workspaces.name"), + }} + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + err := ts.run.Validate(context.Background()) + if err == nil { + t.Errorf("Expected error for invalid Run but got none") + } else if d := cmp.Diff(ts.wantErr.Error(), err.Error()); d != "" { + t.Errorf("Did not get expected error for %q: %s", ts.name, diff.PrintWantGot(d)) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go index b0b06c95bcf..c5e48955b6d 100644 --- a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go @@ -686,6 +686,18 @@ func (in *RunSpec) DeepCopyInto(out *RunSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.PodTemplate != nil { + in, out := &in.PodTemplate, &out.PodTemplate + *out = new(pod.Template) + (*in).DeepCopyInto(*out) + } + if in.Workspaces != nil { + in, out := &in.Workspaces, &out.Workspaces + *out = make([]v1beta1.WorkspaceBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/pkg/apis/pipeline/v1beta1/pipeline_validation.go b/pkg/apis/pipeline/v1beta1/pipeline_validation.go index 755cae22e5d..8a6351fcda3 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_validation.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_validation.go @@ -156,9 +156,6 @@ func validatePipelineTask(ctx context.Context, t PipelineTask, taskNames sets.St if t.Resources != nil { errs = errs.Also(apis.ErrInvalidValue("custom tasks do not support PipelineResources", "resources")) } - if len(t.Workspaces) > 0 { - errs = errs.Also(apis.ErrInvalidValue("custom tasks do not support Workspaces", "workspaces")) - } if t.Timeout != nil { errs = errs.Also(apis.ErrInvalidValue("custom tasks do not support timeout", "timeout")) } diff --git a/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go b/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go index 47c3b23b3f0..d2fab8280a9 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go @@ -620,18 +620,6 @@ func TestValidatePipelineTasks_Failure(t *testing.T) { Paths: []string{"tasks[0].resources"}, }, wc: enableFeature(t, "enable-custom-tasks"), - }, { - name: "pipelinetask custom task doesn't support workspaces", - tasks: []PipelineTask{{ - Name: "foo", - Workspaces: []WorkspacePipelineTaskBinding{{}}, - TaskRef: &TaskRef{APIVersion: "example.dev/v0", Kind: "Example"}, - }}, - expectedError: apis.FieldError{ - Message: `invalid value: custom tasks do not support Workspaces`, - Paths: []string{"tasks[0].workspaces"}, - }, - wc: enableFeature(t, "enable-custom-tasks"), }, { name: "pipelinetask custom task doesn't support timeout", tasks: []PipelineTask{{ diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index 872ba6bc5cc..0f3b0584253 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -714,20 +714,10 @@ func (c *Reconciler) createTaskRun(ctx context.Context, rprt *resources.Resolved } var pipelinePVCWorkspaceName string - pipelineRunWorkspaces := make(map[string]v1beta1.WorkspaceBinding) - for _, binding := range pr.Spec.Workspaces { - pipelineRunWorkspaces[binding.Name] = binding - } - for _, ws := range rprt.PipelineTask.Workspaces { - taskWorkspaceName, pipelineTaskSubPath, pipelineWorkspaceName := ws.Name, ws.SubPath, ws.Workspace - if b, hasBinding := pipelineRunWorkspaces[pipelineWorkspaceName]; hasBinding { - if b.PersistentVolumeClaim != nil || b.VolumeClaimTemplate != nil { - pipelinePVCWorkspaceName = pipelineWorkspaceName - } - tr.Spec.Workspaces = append(tr.Spec.Workspaces, taskWorkspaceByWorkspaceVolumeSource(b, taskWorkspaceName, pipelineTaskSubPath, pr.GetOwnerReference())) - } else { - return nil, fmt.Errorf("expected workspace %q to be provided by pipelinerun for pipeline task %q", pipelineWorkspaceName, rprt.PipelineTask.Name) - } + var err error + tr.Spec.Workspaces, pipelinePVCWorkspaceName, err = getTaskrunWorkspaces(pr, rprt) + if err != nil { + return nil, err } if !c.isAffinityAssistantDisabled(ctx) && pipelinePVCWorkspaceName != "" { @@ -741,6 +731,7 @@ func (c *Reconciler) createTaskRun(ctx context.Context, rprt *resources.Resolved func (c *Reconciler) createRun(ctx context.Context, rprt *resources.ResolvedPipelineRunTask, pr *v1beta1.PipelineRun) (*v1alpha1.Run, error) { logger := logging.FromContext(ctx) + taskRunSpec := pr.GetTaskRunSpec(rprt.PipelineTask.Name) r := &v1alpha1.Run{ ObjectMeta: metav1.ObjectMeta{ Name: rprt.RunName, @@ -750,14 +741,51 @@ func (c *Reconciler) createRun(ctx context.Context, rprt *resources.ResolvedPipe Annotations: getTaskrunAnnotations(pr), }, Spec: v1alpha1.RunSpec{ - Ref: rprt.PipelineTask.TaskRef, - Params: rprt.PipelineTask.Params, + Ref: rprt.PipelineTask.TaskRef, + Params: rprt.PipelineTask.Params, + ServiceAccountName: taskRunSpec.TaskServiceAccountName, + PodTemplate: taskRunSpec.TaskPodTemplate, }, } + + var pipelinePVCWorkspaceName string + var err error + r.Spec.Workspaces, pipelinePVCWorkspaceName, err = getTaskrunWorkspaces(pr, rprt) + if err != nil { + return nil, err + } + + // Set the affinity assistant annotation in case the custom task creates TaskRuns or Pods + // that can take advantage of it. + if !c.isAffinityAssistantDisabled(ctx) && pipelinePVCWorkspaceName != "" { + r.Annotations[workspace.AnnotationAffinityAssistantName] = getAffinityAssistantName(pipelinePVCWorkspaceName, pr.Name) + } + logger.Infof("Creating a new Run object %s", rprt.RunName) return c.PipelineClientSet.TektonV1alpha1().Runs(pr.Namespace).Create(ctx, r, metav1.CreateOptions{}) } +func getTaskrunWorkspaces(pr *v1beta1.PipelineRun, rprt *resources.ResolvedPipelineRunTask) ([]v1beta1.WorkspaceBinding, string, error) { + var workspaces []v1beta1.WorkspaceBinding + var pipelinePVCWorkspaceName string + pipelineRunWorkspaces := make(map[string]v1beta1.WorkspaceBinding) + for _, binding := range pr.Spec.Workspaces { + pipelineRunWorkspaces[binding.Name] = binding + } + for _, ws := range rprt.PipelineTask.Workspaces { + taskWorkspaceName, pipelineTaskSubPath, pipelineWorkspaceName := ws.Name, ws.SubPath, ws.Workspace + if b, hasBinding := pipelineRunWorkspaces[pipelineWorkspaceName]; hasBinding { + if b.PersistentVolumeClaim != nil || b.VolumeClaimTemplate != nil { + pipelinePVCWorkspaceName = pipelineWorkspaceName + } + workspaces = append(workspaces, taskWorkspaceByWorkspaceVolumeSource(b, taskWorkspaceName, pipelineTaskSubPath, pr.GetOwnerReference())) + } else { + return nil, "", fmt.Errorf("expected workspace %q to be provided by pipelinerun for pipeline task %q", pipelineWorkspaceName, rprt.PipelineTask.Name) + } + } + return workspaces, pipelinePVCWorkspaceName, nil +} + // taskWorkspaceByWorkspaceVolumeSource is returning the WorkspaceBinding with the TaskRun specified name. // If the volume source is a volumeClaimTemplate, the template is applied and passed to TaskRun as a persistentVolumeClaim func taskWorkspaceByWorkspaceVolumeSource(wb v1beta1.WorkspaceBinding, taskWorkspaceName string, pipelineTaskSubPath string, owner metav1.OwnerReference) v1beta1.WorkspaceBinding { diff --git a/pkg/reconciler/pipelinerun/pipelinerun_test.go b/pkg/reconciler/pipelinerun/pipelinerun_test.go index 8ecc4d8513a..779213dfc25 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/pipelinerun/pipelinerun_test.go @@ -463,10 +463,16 @@ func TestReconcile(t *testing.T) { // created, it checks the resulting API actions, status and events. func TestReconcile_CustomTask(t *testing.T) { names.TestingSeed() - const pipelineRunName = "test-pipelinerun-custom-task" + const pipelineRunName = "test-pipelinerun" + const pipelineTaskName = "custom-task" const namespace = "namespace" - prt := NewPipelineRunTest(test.Data{ - PipelineRuns: []*v1beta1.PipelineRun{{ + tcs := []struct { + name string + pr *v1beta1.PipelineRun + wantRun *v1alpha1.Run + }{{ + name: "simple custom task", + pr: &v1beta1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ Name: pipelineRunName, Namespace: namespace, @@ -474,7 +480,7 @@ func TestReconcile_CustomTask(t *testing.T) { Spec: v1beta1.PipelineRunSpec{ PipelineSpec: &v1beta1.PipelineSpec{ Tasks: []v1beta1.PipelineTask{{ - Name: "custom-task", + Name: pipelineTaskName, Params: []v1beta1.Param{{ Name: "param1", Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "value1"}, @@ -486,79 +492,160 @@ func TestReconcile_CustomTask(t *testing.T) { }}, }, }, - }}, - ConfigMaps: []*corev1.ConfigMap{ - { - ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.GetNamespace()}, - Data: map[string]string{ - "enable-custom-tasks": "true", + }, + wantRun: &v1alpha1.Run{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pipelinerun-custom-task-9l9zj", + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "tekton.dev/v1beta1", + Kind: "PipelineRun", + Name: pipelineRunName, + Controller: &trueb, + BlockOwnerDeletion: &trueb, + }}, + Labels: map[string]string{ + "tekton.dev/pipeline": pipelineRunName, + "tekton.dev/pipelineRun": pipelineRunName, + "tekton.dev/pipelineTask": pipelineTaskName, + }, + Annotations: map[string]string{}, + }, + Spec: v1alpha1.RunSpec{ + Params: []v1beta1.Param{{ + Name: "param1", + Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "value1"}, + }}, + Ref: &v1beta1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "Example", }, + ServiceAccountName: "default", }, }, - }, t) - defer prt.Cancel() - - wantEvents := []string{ - "Normal Started", - "Normal Running Tasks Completed: 0", - } - reconciledRun, clients := prt.reconcileRun(namespace, pipelineRunName, wantEvents, false) - - actions := clients.Pipeline.Actions() - if len(actions) < 2 { - t.Fatalf("Expected client to have at least two action implementation but it has %d", len(actions)) - } - - // Check that the expected Run was created. - trueB := true - actual := actions[0].(ktesting.CreateAction).GetObject() - wantRun := &v1alpha1.Run{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-pipelinerun-custom-task-custom-task-9l9zj", - Namespace: namespace, - OwnerReferences: []metav1.OwnerReference{{ - APIVersion: "tekton.dev/v1beta1", - Kind: "PipelineRun", - Name: "test-pipelinerun-custom-task", - Controller: &trueB, - BlockOwnerDeletion: &trueB, - }}, - Labels: map[string]string{ - "tekton.dev/pipeline": "test-pipelinerun-custom-task", - "tekton.dev/pipelineRun": "test-pipelinerun-custom-task", - "tekton.dev/pipelineTask": "custom-task", + }, { + name: "custom task with workspace", + pr: &v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: pipelineRunName, + Namespace: namespace, + }, + Spec: v1beta1.PipelineRunSpec{ + PipelineSpec: &v1beta1.PipelineSpec{ + Workspaces: []v1beta1.PipelineWorkspaceDeclaration{{Name: "pipelinews"}}, + Tasks: []v1beta1.PipelineTask{{ + Name: pipelineTaskName, + TaskRef: &v1beta1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "Example", + }, + Workspaces: []v1beta1.WorkspacePipelineTaskBinding{{ + Name: "taskws", + Workspace: "pipelinews", + SubPath: "bar", + }}, + }}, + }, + Workspaces: []v1beta1.WorkspaceBinding{{ + Name: "pipelinews", + SubPath: "foo", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "myclaim", + ReadOnly: false, + }}, + }, }, - Annotations: map[string]string{}, }, - Spec: v1alpha1.RunSpec{ - Params: []v1beta1.Param{{ - Name: "param1", - Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "value1"}, - }}, - Ref: &v1beta1.TaskRef{ - APIVersion: "example.dev/v0", - Kind: "Example", + wantRun: &v1alpha1.Run{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pipelinerun-custom-task-mz4c7", + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "tekton.dev/v1beta1", + Kind: "PipelineRun", + Name: pipelineRunName, + Controller: &trueb, + BlockOwnerDeletion: &trueb, + }}, + Labels: map[string]string{ + "tekton.dev/pipeline": pipelineRunName, + "tekton.dev/pipelineRun": pipelineRunName, + "tekton.dev/pipelineTask": pipelineTaskName, + }, + Annotations: map[string]string{ + "pipeline.tekton.dev/affinity-assistant": getAffinityAssistantName("pipelinews", pipelineRunName), + }, + }, + Spec: v1alpha1.RunSpec{ + Ref: &v1beta1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "Example", + }, + ServiceAccountName: "default", + Workspaces: []v1beta1.WorkspaceBinding{{ + Name: "taskws", + SubPath: "foo/bar", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "myclaim", + ReadOnly: false, + }}, + }, }, }, - } - if d := cmp.Diff(wantRun, actual); d != "" { - t.Errorf("expected to see Run created: %s", diff.PrintWantGot(d)) - } + }} - // This PipelineRun is in progress now and the status should reflect that - condition := reconciledRun.Status.GetCondition(apis.ConditionSucceeded) - if condition == nil || condition.Status != corev1.ConditionUnknown { - t.Errorf("Expected PipelineRun status to be in progress, but was %v", condition) - } - if condition != nil && condition.Reason != v1beta1.PipelineRunReasonRunning.String() { - t.Errorf("Expected reason %q but was %s", v1beta1.PipelineRunReasonRunning.String(), condition.Reason) + cms := []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.GetNamespace()}, + Data: map[string]string{ + "enable-custom-tasks": "true", + }, + }, } - if len(reconciledRun.Status.Runs) != 1 { - t.Errorf("Expected PipelineRun status to include one Run status, got %d", len(reconciledRun.Status.Runs)) - } - if _, exists := reconciledRun.Status.Runs["test-pipelinerun-custom-task-custom-task-9l9zj"]; !exists { - t.Errorf("Expected PipelineRun status to include Run status but was %v", reconciledRun.Status.Runs) + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + + d := test.Data{ + PipelineRuns: []*v1beta1.PipelineRun{tc.pr}, + ConfigMaps: cms, + } + prt := NewPipelineRunTest(d, t) + defer prt.Cancel() + + wantEvents := []string{ + "Normal Started", + "Normal Running Tasks Completed: 0", + } + reconciledRun, clients := prt.reconcileRun(namespace, pipelineRunName, wantEvents, false) + + actions := clients.Pipeline.Actions() + if len(actions) < 2 { + t.Fatalf("Expected client to have at least two action implementation but it has %d", len(actions)) + } + + // Check that the expected Run was created. + actual := actions[0].(ktesting.CreateAction).GetObject() + if d := cmp.Diff(tc.wantRun, actual); d != "" { + t.Errorf("expected to see Run created: %s", diff.PrintWantGot(d)) + } + + // This PipelineRun is in progress now and the status should reflect that + condition := reconciledRun.Status.GetCondition(apis.ConditionSucceeded) + if condition == nil || condition.Status != corev1.ConditionUnknown { + t.Errorf("Expected PipelineRun status to be in progress, but was %v", condition) + } + if condition != nil && condition.Reason != v1beta1.PipelineRunReasonRunning.String() { + t.Errorf("Expected reason %q but was %s", v1beta1.PipelineRunReasonRunning.String(), condition.Reason) + } + + if len(reconciledRun.Status.Runs) != 1 { + t.Errorf("Expected PipelineRun status to include one Run status, got %d", len(reconciledRun.Status.Runs)) + } + if _, exists := reconciledRun.Status.Runs[tc.wantRun.Name]; !exists { + t.Errorf("Expected PipelineRun status to include Run status but was %v", reconciledRun.Status.Runs) + } + }) } } @@ -1674,6 +1761,58 @@ func TestReconcileWithDifferentServiceAccounts(t *testing.T) { } } +func TestReconcileCustomTasksWithDifferentServiceAccounts(t *testing.T) { + names.TestingSeed() + + ps := []*v1beta1.Pipeline{tb.Pipeline("test-pipeline", tb.PipelineNamespace("foo"), tb.PipelineSpec( + tb.PipelineTask("hello-world-0", ""), + tb.PipelineTask("hello-world-1", ""), + ))} + // Update the pipeline tasks to be custom tasks (builder does not support this case). + ps[0].Spec.Tasks[0].TaskRef = &v1beta1.TaskRef{APIVersion: "example.dev/v0", Kind: "Example"} + ps[0].Spec.Tasks[1].TaskRef = &v1beta1.TaskRef{APIVersion: "example.dev/v0", Kind: "Example"} + + prs := []*v1beta1.PipelineRun{tb.PipelineRun("test-pipeline-run-different-service-accs", tb.PipelineRunNamespace("foo"), + tb.PipelineRunSpec("test-pipeline", + tb.PipelineRunServiceAccountName("test-sa-0"), + tb.PipelineRunServiceAccountNameTask("hello-world-1", "test-sa-1"), + ), + )} + + cms := []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.GetNamespace()}, + Data: map[string]string{ + "enable-custom-tasks": "true", + }, + }, + } + + d := test.Data{ + PipelineRuns: prs, + Pipelines: ps, + ConfigMaps: cms, + } + prt := NewPipelineRunTest(d, t) + defer prt.Cancel() + + _, clients := prt.reconcileRun("foo", "test-pipeline-run-different-service-accs", []string{}, false) + + runNames := []string{"test-pipeline-run-different-service-accs-hello-world-0-9l9zj", "test-pipeline-run-different-service-accs-hello-world-1-mz4c7"} + expectedSANames := []string{"test-sa-0", "test-sa-1"} + + for i := range ps[0].Spec.Tasks { + actual, err := clients.Pipeline.TektonV1alpha1().Runs("foo").Get(prt.TestAssets.Ctx, runNames[i], metav1.GetOptions{}) + if err != nil { + t.Errorf("Expected a Run %s to be created but it wasn't: %s", runNames[i], err) + continue + } + if actual.Spec.ServiceAccountName != expectedSANames[i] { + t.Errorf("Expected Run %s to have service account %s but it was %s", runNames[i], expectedSANames[i], actual.Spec.ServiceAccountName) + } + } +} + // TestReconcileWithTimeoutAndRetry runs "Reconcile" against pipelines with // retries and timeout settings, and status that represents different number of // retries already performed. It verifies the reconciled status and events @@ -1976,6 +2115,66 @@ func TestReconcileAndPropagateCustomPipelineTaskRunSpec(t *testing.T) { } } +func TestReconcileCustomTasksWithTaskRunSpec(t *testing.T) { + names.TestingSeed() + prName := "test-pipeline-run" + ps := []*v1beta1.Pipeline{tb.Pipeline("test-pipeline", tb.PipelineNamespace("foo"), tb.PipelineSpec( + tb.PipelineTask("hello-world-1", ""), + ))} + // Update the pipeline tasks to be custom tasks (builder does not support this case). + ps[0].Spec.Tasks[0].TaskRef = &v1beta1.TaskRef{APIVersion: "example.dev/v0", Kind: "Example"} + + serviceAccount := "custom-sa" + podTemplate := &pod.Template{ + NodeSelector: map[string]string{ + "workloadtype": "tekton", + }, + } + prs := []*v1beta1.PipelineRun{tb.PipelineRun(prName, tb.PipelineRunNamespace("foo"), + tb.PipelineRunSpec("test-pipeline", + tb.PipelineRunServiceAccountName("test-sa"), + tb.PipelineTaskRunSpecs([]v1beta1.PipelineTaskRunSpec{{ + PipelineTaskName: "hello-world-1", + TaskServiceAccountName: serviceAccount, + TaskPodTemplate: podTemplate, + }}), + ), + )} + + cms := []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.GetNamespace()}, + Data: map[string]string{ + "enable-custom-tasks": "true", + }, + }, + } + + d := test.Data{ + PipelineRuns: prs, + Pipelines: ps, + ConfigMaps: cms, + } + prt := NewPipelineRunTest(d, t) + defer prt.Cancel() + + _, clients := prt.reconcileRun("foo", prName, []string{}, false) + + runName := "test-pipeline-run-hello-world-1-9l9zj" + + actual, err := clients.Pipeline.TektonV1alpha1().Runs("foo").Get(prt.TestAssets.Ctx, runName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Expected a Run %s to be created but it wasn't: %s", runName, err) + } + if actual.Spec.ServiceAccountName != serviceAccount { + t.Errorf("Expected Run %s to have service account %s but it was %s", runName, serviceAccount, actual.Spec.ServiceAccountName) + } + if d := cmp.Diff(actual.Spec.PodTemplate, podTemplate); d != "" { + t.Errorf("Incorrect pod template in Run %s. Diff %s", runName, diff.PrintWantGot(d)) + } + +} + func TestReconcileWithConditionChecks(t *testing.T) { // TestReconcileWithConditionChecks runs "Reconcile" on a PipelineRun that has a task with // multiple conditions. It verifies that reconcile is successful, taskruns are created and