From 9e49bc031b8a396483e051d8f6d9658d2ac2af9c Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Wed, 10 Jul 2019 15:54:51 +0200 Subject: [PATCH] Add a PodTemplate field for PipelineRun and TaskRun This allows to group and add `PodSpec` customizations to PipelineRun and TaskRun and share the field with both of them. - We are still putting field by hand compared to PodSpec as PodSpec has a large number of fields, including `InitContainers` and `Containers` and we don't want people to be able to add random containers to the `Task` pod. - This adds SecurityContext in addition to the current `PodSpec` specific fields. - This adds Volumes in addition to the current `PodSpec` specific fields. This is done in a backward compatibility way. The old field are still there, but `PodTemplate` takes precedence over it (depending on which field are present). These fields will be remove when we bump the API to `v1beta1`. Signed-off-by: Vincent Demeester --- docs/taskruns.md | 60 +++++++++++ examples/taskruns/task-volume.yaml | 8 +- .../pipeline/v1alpha1/pipelinerun_types.go | 5 + pkg/apis/pipeline/v1alpha1/pod.go | 62 +++++++++++ pkg/apis/pipeline/v1alpha1/pod_test.go | 102 ++++++++++++++++++ pkg/apis/pipeline/v1alpha1/taskrun_types.go | 5 + .../v1alpha1/zz_generated.deepcopy.go | 57 ++++++++++ .../v1alpha1/pipelinerun/pipelinerun.go | 9 +- .../v1alpha1/taskrun/resources/pod.go | 9 +- test/dag_test.go | 2 +- test/init_test.go | 2 +- 11 files changed, 307 insertions(+), 14 deletions(-) create mode 100644 pkg/apis/pipeline/v1alpha1/pod.go create mode 100644 pkg/apis/pipeline/v1alpha1/pod_test.go diff --git a/docs/taskruns.md b/docs/taskruns.md index 580a7c0bcaa..e665be35bee 100644 --- a/docs/taskruns.md +++ b/docs/taskruns.md @@ -17,6 +17,7 @@ A `TaskRun` runs until all `steps` have completed or until a failure occurs. - [Providing resources](#providing-resources) - [Overriding where resources are copied from](#overriding-where-resources-are-copied-from) - [Service Account](#service-account) + - [Pod Template](#pod-template) - [Steps](#steps) - [Cancelling a TaskRun](#cancelling-a-taskrun) - [Examples](#examples) @@ -159,6 +160,65 @@ of the `TaskRun` resource object. For examples and more information about specifying service accounts, see the [`ServiceAccount`](./auth.md) reference topic. +## Pod Template + +Specifies a subset of +[`PodSpec`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#pod-v1-core) +configuration that will be used as the basis for the `Task` pod. This +allows to customize some Pod specific field per `Task` execution, aka +`TaskRun`. The current field supported are + +- `nodeSelector`: a selector which must be true for the pod to fit on + a node, see [here](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/). +- `tolerations`: allow (but do not require) the pods to schedule onto + nodes with matching taints. +- `affinity`: allow to constrain which nodes your pod is eligible to + be scheduled on, based on labels on the node. +- `securityContext`: pod-level security attributes and common + container settings, like `runAsUser` or `selinux`. +- `volumes`: list of volumes that can be mounted by containers + belonging to the pod. This lets the user of a Task define which type + of volume to use for a Task `volumeMount` + +In the following example, the Task is defined with a `volumeMount` +(`my-cache`), that is provided by the TaskRun, using a +PersistenceVolumeClaim. The Pod will also run as a non-root user. + +```yaml +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: myTask + namespace: default +spec: + steps: + - name: write something + image: ubuntu + command: ["bash", "-c"] + args: ["echo 'foo' > /my-cache/bar"] + volumeMounts: + - name: my-cache + mountPath: /my-cache +``` + +```yaml +apiVersion: tekton.dev/v1alpha1 +kind: TaskRun +metadata: + name: myTaskRun + namespace: default +spec: + taskRef: + name: myTask + podTemplate: + securityContext: + runAsNonRoot: true + volumes: + - name: my-cache + persistentVolumeClaim: + claimName: my-volume-claim +``` + ### Overriding where resources are copied from When specifying input and output `PipelineResources`, you can optionally specify diff --git a/examples/taskruns/task-volume.yaml b/examples/taskruns/task-volume.yaml index b1c1bc0fcf5..3292fdfca5a 100644 --- a/examples/taskruns/task-volume.yaml +++ b/examples/taskruns/task-volume.yaml @@ -19,10 +19,6 @@ spec: volumeMounts: - name: custom mountPath: /short/and/stout - - volumes: - - name: custom - emptyDir: {} --- apiVersion: tekton.dev/v1alpha1 kind: TaskRun @@ -31,3 +27,7 @@ metadata: spec: taskRef: name: task-volume + podTemplate: + volumes: + - name: custom + emptyDir: {} diff --git a/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go b/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go index d4b93726515..f59113cefaf 100644 --- a/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go +++ b/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go @@ -62,6 +62,11 @@ type PipelineRunSpec struct { // Refer to Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration // +optional Timeout *metav1.Duration `json:"timeout,omitempty"` + + // PodTemplate holds pod specific configuration + PodTemplate PodTemplate `json:"podTemplate,omitempty"` + + // FIXME(vdemeester) Deprecated // NodeSelector is a selector which must be true for the pod to fit on a node. // Selector which must match a node's labels for the pod to be scheduled on that node. // More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ diff --git a/pkg/apis/pipeline/v1alpha1/pod.go b/pkg/apis/pipeline/v1alpha1/pod.go new file mode 100644 index 00000000000..3deca076866 --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/pod.go @@ -0,0 +1,62 @@ +/* +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 v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" +) + +// PodTemplate holds pod specific configuration +type PodTemplate struct { + // NodeSelector is a selector which must be true for the pod to fit on a node. + // Selector which must match a node's labels for the pod to be scheduled on that node. + // More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ + // +optional + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + + // If specified, the pod's tolerations. + // +optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + + // If specified, the pod's scheduling constraints + // +optional + Affinity *corev1.Affinity `json:"affinity,omitempty"` + + // SecurityContext holds pod-level security attributes and common container settings. + // Optional: Defaults to empty. See type description for default values of each field. + // +optional + SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` + + // List of volumes that can be mounted by containers belonging to the pod. + // More info: https://kubernetes.io/docs/concepts/storage/volumes + // +optional + Volumes []corev1.Volume `json:"volumes,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name" protobuf:"bytes,1,rep,name=volumes"` +} + +// CombinePodTemplate takes a PodTemplate (either from TaskRun or PipelineRun) and merge it with deprecated field that were inlined. +func CombinedPodTemplate(template PodTemplate, deprecatedNodeSelector map[string]string, deprecatedTolerations []corev1.Toleration, deprecatedAffinity *corev1.Affinity) PodTemplate { + if len(template.NodeSelector) == 0 && len(deprecatedNodeSelector) != 0 { + template.NodeSelector = deprecatedNodeSelector + } + if len(template.Tolerations) == 0 && len(deprecatedTolerations) != 0 { + template.Tolerations = deprecatedTolerations + } + if template.Affinity == nil && deprecatedAffinity != nil { + template.Affinity = deprecatedAffinity + } + return template +} diff --git a/pkg/apis/pipeline/v1alpha1/pod_test.go b/pkg/apis/pipeline/v1alpha1/pod_test.go new file mode 100644 index 00000000000..6fbb05e581c --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/pod_test.go @@ -0,0 +1,102 @@ +/* +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 v1alpha1_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + corev1 "k8s.io/api/core/v1" +) + +func TestCombinedPodTemplate(t *testing.T) { + affinity := &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{}, + } + nodeSelector := map[string]string{ + "banana": "chocolat", + } + tolerations := []corev1.Toleration{{ + Key: "banana", + Value: "chocolat", + }} + + template := v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "foo": "bar", + "bar": "baz", + }, + Tolerations: []corev1.Toleration{{ + Key: "foo", + Value: "bar", + }}, + Affinity: &corev1.Affinity{ + PodAffinity: &corev1.PodAffinity{}, + }, + } + // Same as template above + want := v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "foo": "bar", + "bar": "baz", + }, + Tolerations: []corev1.Toleration{{ + Key: "foo", + Value: "bar", + }}, + Affinity: &corev1.Affinity{ + PodAffinity: &corev1.PodAffinity{}, + }, + } + + got := v1alpha1.CombinedPodTemplate(template, nodeSelector, tolerations, affinity) + if d := cmp.Diff(got, want); d != "" { + t.Errorf("Diff:\n%s", d) + } +} + +func TestCombinedPodTemplateOnlyDeprecated(t *testing.T) { + template := v1alpha1.PodTemplate{} + affinity := &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{}, + } + + nodeSelector := map[string]string{ + "foo": "bar", + } + tolerations := []corev1.Toleration{{ + Key: "foo", + Value: "bar", + }} + + want := v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "foo": "bar", + }, + Tolerations: []corev1.Toleration{{ + Key: "foo", + Value: "bar", + }}, + Affinity: affinity, + } + + got := v1alpha1.CombinedPodTemplate(template, nodeSelector, tolerations, affinity) + if d := cmp.Diff(got, want); d != "" { + t.Errorf("Diff:\n%s", d) + } +} diff --git a/pkg/apis/pipeline/v1alpha1/taskrun_types.go b/pkg/apis/pipeline/v1alpha1/taskrun_types.go index 4f4968bdb30..dc51212ff14 100644 --- a/pkg/apis/pipeline/v1alpha1/taskrun_types.go +++ b/pkg/apis/pipeline/v1alpha1/taskrun_types.go @@ -53,6 +53,11 @@ type TaskRunSpec struct { // Refer Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration // +optional Timeout *metav1.Duration `json:"timeout,omitempty"` + + // PodTemplate holds pod specific configuration + PodTemplate PodTemplate `json:"podTemplate,omitempty"` + + // FIXME(vdemeester) Deprecated // NodeSelector is a selector which must be true for the pod to fit on a node. // Selector which must match a node's labels for the pod to be scheduled on that node. // More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ diff --git a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go index 54e4a008c5a..713414de6be 100644 --- a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go @@ -712,6 +712,7 @@ func (in *PipelineRunSpec) DeepCopyInto(out *PipelineRunSpec) { *out = new(metav1.Duration) **out = **in } + in.PodTemplate.DeepCopyInto(&out.PodTemplate) if in.NodeSelector != nil { in, out := &in.NodeSelector, &out.NodeSelector *out = make(map[string]string, len(*in)) @@ -1004,6 +1005,61 @@ 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 *PodTemplate) DeepCopyInto(out *PodTemplate) { + *out = *in + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + if *in == nil { + *out = nil + } else { + *out = new(v1.Affinity) + (*in).DeepCopyInto(*out) + } + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + if *in == nil { + *out = nil + } else { + *out = new(v1.PodSecurityContext) + (*in).DeepCopyInto(*out) + } + } + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]v1.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodTemplate. +func (in *PodTemplate) DeepCopy() *PodTemplate { + if in == nil { + return nil + } + out := new(PodTemplate) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PullRequestResource) DeepCopyInto(out *PullRequestResource) { *out = *in @@ -1330,6 +1386,7 @@ func (in *TaskRunSpec) DeepCopyInto(out *TaskRunSpec) { *out = new(metav1.Duration) **out = **in } + in.PodTemplate.DeepCopyInto(&out.PodTemplate) if in.NodeSelector != nil { in, out := &in.NodeSelector, &out.NodeSelector *out = make(map[string]string, len(*in)) diff --git a/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go index c48255701e9..4ab425cf587 100644 --- a/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go @@ -448,6 +448,8 @@ func (c *Reconciler) createTaskRun(logger *zap.SugaredLogger, rprt *resources.Re }) return c.PipelineClientSet.TektonV1alpha1().TaskRuns(pr.Namespace).UpdateStatus(tr) } + + podTemplate := v1alpha1.CombinedPodTemplate(pr.Spec.PodTemplate, pr.Spec.NodeSelector, pr.Spec.Tolerations, pr.Spec.Affinity) tr = &v1alpha1.TaskRun{ ObjectMeta: metav1.ObjectMeta{ Name: rprt.TaskRunName, @@ -466,11 +468,8 @@ func (c *Reconciler) createTaskRun(logger *zap.SugaredLogger, rprt *resources.Re }, ServiceAccount: serviceAccount, Timeout: taskRunTimeout, - NodeSelector: pr.Spec.NodeSelector, - Tolerations: pr.Spec.Tolerations, - Affinity: pr.Spec.Affinity, - }, - } + PodTemplate: podTemplate, + }} resources.WrapSteps(&tr.Spec, rprt.PipelineTask, rprt.ResolvedTaskResources.Inputs, rprt.ResolvedTaskResources.Outputs, storageBasePath) diff --git a/pkg/reconciler/v1alpha1/taskrun/resources/pod.go b/pkg/reconciler/v1alpha1/taskrun/resources/pod.go index 27e96302c65..17469a52e7d 100644 --- a/pkg/reconciler/v1alpha1/taskrun/resources/pod.go +++ b/pkg/reconciler/v1alpha1/taskrun/resources/pod.go @@ -333,6 +333,8 @@ func MakePod(taskRun *v1alpha1.TaskRun, taskSpec v1alpha1.TaskSpec, kubeclient k return nil, err } + podTemplate := v1alpha1.CombinedPodTemplate(taskRun.Spec.PodTemplate, taskRun.Spec.NodeSelector, taskRun.Spec.Tolerations, taskRun.Spec.Affinity) + return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ // We execute the build's pod in the same namespace as where the build was @@ -356,9 +358,10 @@ func MakePod(taskRun *v1alpha1.TaskRun, taskSpec v1alpha1.TaskSpec, kubeclient k Containers: mergedPodContainers, ServiceAccountName: taskRun.Spec.ServiceAccount, Volumes: volumes, - NodeSelector: taskRun.Spec.NodeSelector, - Tolerations: taskRun.Spec.Tolerations, - Affinity: taskRun.Spec.Affinity, + NodeSelector: podTemplate.NodeSelector, + Tolerations: podTemplate.Tolerations, + Affinity: podTemplate.Affinity, + SecurityContext: podTemplate.SecurityContext, }, }, nil } diff --git a/test/dag_test.go b/test/dag_test.go index 464612b06cf..1b3f86b05da 100644 --- a/test/dag_test.go +++ b/test/dag_test.go @@ -1,7 +1,7 @@ // +build e2e /* -Copyright 2018 Knative Authors LLC +Copyright 2019 Tekton Authors LLC 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 diff --git a/test/init_test.go b/test/init_test.go index b1ae921da7c..5f9678e24c3 100644 --- a/test/init_test.go +++ b/test/init_test.go @@ -1,7 +1,7 @@ // +build e2e /* -Copyright 2018 Knative Authors LLC +Copyright 2019 Tekton Authors LLC 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