diff --git a/config/config-defaults.yaml b/config/config-defaults.yaml index 490a84ef1cc..4f51e0d76b4 100644 --- a/config/config-defaults.yaml +++ b/config/config-defaults.yaml @@ -47,3 +47,8 @@ data: # TaskRuns. If a user's requested TaskRun specifies another value for this # label, the user's request supercedes. default-managed-by-label-value: "tekton-pipelines" + + # default-pod-template contains the default pod template to use + # TaskRun and PipelineRun, if none is specified. If a pod template + # is specified, the default pod template is ignored. + # default-pod-template: \ No newline at end of file diff --git a/docs/install.md b/docs/install.md index c9fe1197081..b35e54cf9f0 100644 --- a/docs/install.md +++ b/docs/install.md @@ -164,7 +164,6 @@ stringData: https_validate_certificates = True --- apiVersion: v1 -data: null kind: ConfigMap metadata: name: config-artifact-pvc @@ -181,24 +180,33 @@ creation of a persistent volume could be slower than uploading/downloading files to a bucket, or if the the cluster is running in multiple zones, the access to the persistent volume can fail. -### Overriding default ServiceAccount used for TaskRun and PipelineRun +### Overriding default ServiceAccount, Timeout or PodTemplate used for TaskRun and PipelineRun -The ConfigMap `config-defaults` can be used to override default service account -e.g. to override the default service account (`default`) to `tekton` apply the -following +The ConfigMap `config-defaults` can be used to override default values. +You can override the default service account, the default timeout, and the +default pod template applied to `TaskRun` and `PipelineRun`. -```yaml +The example below overrides the following : +- the default service account (`default`) to `tekton` +- the default timeout (60 minutes) to 20 minutes +- the default pod template to include an annotation preventing clusterautoscaler to evict a running task pod +(see [here](./taskruns.md#pod-template) or [here](./pipelineruns.md#pod-template) for more infos on pod templates) -### config-defaults.yaml +```yaml apiVersion: v1 kind: ConfigMap +metadata: + name: config-defaults data: default-service-account: "tekton" - + default-timeout-minutes: "20" + default-pod-template: | + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: 'false' ``` -*NOTE:* The `_example` key contains of the keys that can be overriden and their -default values. +*NOTE:* The `_example` key in the provided [config-defaults.yaml](./../config/config-defaults.yaml) +file contains the keys that can be overriden and their default values. ## Custom Releases diff --git a/docs/podtemplates.md b/docs/podtemplates.md index 35111b82966..90dc2ef7a65 100644 --- a/docs/podtemplates.md +++ b/docs/podtemplates.md @@ -6,6 +6,10 @@ 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`. +Alternatively, you can also define a default pod template in tekton config, see [here](./install.md) +When a pod template is specified for a `PipelineRun` or `TaskRun`, the default pod template is ignored, ie +both templates are **NOT** merged, it's always one or the other. + --- The current fields supported are: diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 167151dbb6e..b160e267a52 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -46,6 +46,11 @@ ${GOPATH}/bin/deepcopy-gen \ --go-header-file ${REPO_ROOT_DIR}/hack/boilerplate/boilerplate.go.txt \ -i github.com/tektoncd/pipeline/pkg/apis/config +${GOPATH}/bin/deepcopy-gen \ + -O zz_generated.deepcopy \ + --go-header-file ${REPO_ROOT_DIR}/hack/boilerplate/boilerplate.go.txt \ + -i github.com/tektoncd/pipeline/pkg/apis/pipeline/pod + # Knative Injection # This generates the knative injection packages for the resource package (v1alpha1). # This is separate from the pipeline package for the same reason as client and all (see above). diff --git a/pkg/apis/config/default.go b/pkg/apis/config/default.go index b56c28313ac..b82843531db 100644 --- a/pkg/apis/config/default.go +++ b/pkg/apis/config/default.go @@ -21,6 +21,8 @@ import ( "strconv" "time" + "github.com/ghodss/yaml" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" corev1 "k8s.io/api/core/v1" ) @@ -33,6 +35,7 @@ const ( defaultServiceAccountKey = "default-service-account" defaultManagedByLabelValueKey = "default-managed-by-label-value" DefaultManagedByLabelValue = "tekton-pipelines" + defaultPodTemplateKey = "default-pod-template" ) // Defaults holds the default configurations @@ -41,13 +44,23 @@ type Defaults struct { DefaultTimeoutMinutes int DefaultServiceAccount string DefaultManagedByLabelValue string + DefaultPodTemplate *pod.Template } // Equals returns true if two Configs are identical func (cfg *Defaults) Equals(other *Defaults) bool { + if cfg == nil && other == nil { + return true + } + + if cfg == nil || other == nil { + return false + } + return other.DefaultTimeoutMinutes == cfg.DefaultTimeoutMinutes && other.DefaultServiceAccount == cfg.DefaultServiceAccount && - other.DefaultManagedByLabelValue == cfg.DefaultManagedByLabelValue + other.DefaultManagedByLabelValue == cfg.DefaultManagedByLabelValue && + other.DefaultPodTemplate.Equals(cfg.DefaultPodTemplate) } // NewDefaultsFromMap returns a Config given a map corresponding to a ConfigMap @@ -56,6 +69,7 @@ func NewDefaultsFromMap(cfgMap map[string]string) (*Defaults, error) { DefaultTimeoutMinutes: DefaultTimeoutMinutes, DefaultManagedByLabelValue: DefaultManagedByLabelValue, } + if defaultTimeoutMin, ok := cfgMap[defaultTimeoutMinutesKey]; ok { timeout, err := strconv.ParseInt(defaultTimeoutMin, 10, 0) if err != nil { @@ -67,10 +81,19 @@ func NewDefaultsFromMap(cfgMap map[string]string) (*Defaults, error) { if defaultServiceAccount, ok := cfgMap[defaultServiceAccountKey]; ok { tc.DefaultServiceAccount = defaultServiceAccount } + if defaultManagedByLabelValue, ok := cfgMap[defaultManagedByLabelValueKey]; ok { tc.DefaultManagedByLabelValue = defaultManagedByLabelValue } + if defaultPodTemplate, ok := cfgMap[defaultPodTemplateKey]; ok { + var podTemplate pod.Template + if err := yaml.Unmarshal([]byte(defaultPodTemplate), &podTemplate); err != nil { + return nil, fmt.Errorf("failed to unmarshal %v", defaultPodTemplate) + } + tc.DefaultPodTemplate = &podTemplate + } + return &tc, nil } diff --git a/pkg/apis/config/default_test.go b/pkg/apis/config/default_test.go index b5df6695ccc..2b8ffb5c6e7 100644 --- a/pkg/apis/config/default_test.go +++ b/pkg/apis/config/default_test.go @@ -20,16 +20,58 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" test "github.com/tektoncd/pipeline/pkg/reconciler/testing" ) func TestNewDefaultsFromConfigMap(t *testing.T) { - expectedConfig := &Defaults{ - DefaultTimeoutMinutes: 50, - DefaultServiceAccount: "tekton", - DefaultManagedByLabelValue: "something-else", + type testCase struct { + expectedConfig *Defaults + expectedError bool + fileName string + } + + testCases := []testCase{ + { + expectedConfig: &Defaults{ + DefaultTimeoutMinutes: 50, + DefaultServiceAccount: "tekton", + DefaultManagedByLabelValue: "something-else", + }, + fileName: DefaultsConfigName, + }, + { + expectedConfig: &Defaults{ + DefaultTimeoutMinutes: 50, + DefaultServiceAccount: "tekton", + DefaultManagedByLabelValue: DefaultManagedByLabelValue, + DefaultPodTemplate: &pod.Template{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + fileName: "config-defaults-with-pod-template", + }, + // the github.com/ghodss/yaml package in the vendor directory does not support UnmarshalStrict + // update it, switch to UnmarshalStrict in defaults.go, then uncomment these tests + // { + // expectedError: true, + // fileName: "config-defaults-timeout-err", + // }, + // { + // expectedError: true, + // fileName: "config-defaults-pod-template-err", + // }, + } + + for _, tc := range testCases { + if tc.expectedError { + verifyConfigFileWithExpectedError(t, tc.fileName) + } else { + verifyConfigFileWithExpectedConfig(t, tc.fileName, tc.expectedConfig) + } } - verifyConfigFileWithExpectedConfig(t, DefaultsConfigName, expectedConfig) } func TestNewDefaultsFromEmptyConfigMap(t *testing.T) { @@ -41,6 +83,105 @@ func TestNewDefaultsFromEmptyConfigMap(t *testing.T) { verifyConfigFileWithExpectedConfig(t, DefaultsConfigEmptyName, expectedConfig) } +func TestEquals(t *testing.T) { + testCases := []struct { + name string + left *Defaults + right *Defaults + expected bool + }{ + { + name: "left and right nil", + left: nil, + right: nil, + expected: true, + }, + { + name: "left nil", + left: nil, + right: &Defaults{}, + expected: false, + }, + { + name: "right nil", + left: &Defaults{}, + right: nil, + expected: false, + }, + { + name: "right and right default", + left: &Defaults{}, + right: &Defaults{}, + expected: true, + }, + { + name: "different default timeout", + left: &Defaults{ + DefaultTimeoutMinutes: 10, + }, + right: &Defaults{ + DefaultTimeoutMinutes: 20, + }, + expected: false, + }, + { + name: "same default timeout", + left: &Defaults{ + DefaultTimeoutMinutes: 20, + }, + right: &Defaults{ + DefaultTimeoutMinutes: 20, + }, + expected: true, + }, + { + name: "different default pod template", + left: &Defaults{ + DefaultPodTemplate: &pod.Template{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + right: &Defaults{ + DefaultPodTemplate: &pod.Template{ + NodeSelector: map[string]string{ + "label2": "value", + }, + }, + }, + expected: false, + }, + { + name: "same default pod template", + left: &Defaults{ + DefaultPodTemplate: &pod.Template{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + right: &Defaults{ + DefaultPodTemplate: &pod.Template{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := tc.left.Equals(tc.right) + if actual != tc.expected { + t.Errorf("Comparison failed expected: %t, actual: %t", tc.expected, actual) + } + }) + } +} + func verifyConfigFileWithExpectedConfig(t *testing.T, fileName string, expectedConfig *Defaults) { cm := test.ConfigMapFromTestFile(t, fileName) if Defaults, err := NewDefaultsFromConfigMap(cm); err == nil { @@ -51,3 +192,10 @@ func verifyConfigFileWithExpectedConfig(t *testing.T, fileName string, expectedC t.Errorf("NewDefaultsFromConfigMap(actual) = %v", err) } } + +func verifyConfigFileWithExpectedError(t *testing.T, fileName string) { + cm := test.ConfigMapFromTestFile(t, fileName) + if _, err := NewDefaultsFromConfigMap(cm); err == nil { + t.Errorf("NewDefaultsFromConfigMap(actual) was expected to return an error") + } +} diff --git a/pkg/apis/config/testdata/config-defaults-pod-template-err.yaml b/pkg/apis/config/testdata/config-defaults-pod-template-err.yaml new file mode 100644 index 00000000000..f29d0d9d026 --- /dev/null +++ b/pkg/apis/config/testdata/config-defaults-pod-template-err.yaml @@ -0,0 +1,24 @@ +# 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 +# +# https://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. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-defaults + namespace: tekton-pipelines +data: + default-timeout-minutes: "50" + default-service-account: "tekton" + default-pod-template: | + badKey: 42 diff --git a/pkg/apis/config/testdata/config-defaults-timeout-err.yaml b/pkg/apis/config/testdata/config-defaults-timeout-err.yaml new file mode 100644 index 00000000000..84ff5a67dd1 --- /dev/null +++ b/pkg/apis/config/testdata/config-defaults-timeout-err.yaml @@ -0,0 +1,22 @@ +# 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 +# +# https://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. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-defaults + namespace: tekton-pipelines +data: + default-timeout-minutes: "abc" + default-service-account: "tekton" diff --git a/pkg/apis/config/testdata/config-defaults-with-pod-template.yaml b/pkg/apis/config/testdata/config-defaults-with-pod-template.yaml new file mode 100644 index 00000000000..36cb376e30e --- /dev/null +++ b/pkg/apis/config/testdata/config-defaults-with-pod-template.yaml @@ -0,0 +1,25 @@ +# 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 +# +# https://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. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-defaults + namespace: tekton-pipelines +data: + default-timeout-minutes: "50" + default-service-account: "tekton" + default-pod-template: | + nodeSelector: + label: value diff --git a/pkg/apis/config/zz_generated.deepcopy.go b/pkg/apis/config/zz_generated.deepcopy.go index 8abb6b77762..d96e9e04756 100644 --- a/pkg/apis/config/zz_generated.deepcopy.go +++ b/pkg/apis/config/zz_generated.deepcopy.go @@ -20,9 +20,18 @@ limitations under the License. package config +import ( + pod "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" +) + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Defaults) DeepCopyInto(out *Defaults) { *out = *in + if in.DefaultPodTemplate != nil { + in, out := &in.DefaultPodTemplate, &out.DefaultPodTemplate + *out = new(pod.Template) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/apis/pipeline/pod/doc.go b/pkg/apis/pipeline/pod/doc.go new file mode 100644 index 00000000000..5f22dbb9027 --- /dev/null +++ b/pkg/apis/pipeline/pod/doc.go @@ -0,0 +1,17 @@ +/* +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 pod diff --git a/pkg/apis/pipeline/pod/template.go b/pkg/apis/pipeline/pod/template.go new file mode 100644 index 00000000000..01ce09fb739 --- /dev/null +++ b/pkg/apis/pipeline/pod/template.go @@ -0,0 +1,105 @@ +/* +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 pod + +import ( + "reflect" + + corev1 "k8s.io/api/core/v1" +) + +// PodTemplate holds pod specific configuration +// +k8s:deepcopy-gen=true +type Template 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"` + + // RuntimeClassName refers to a RuntimeClass object in the node.k8s.io + // group, which should be used to run this pod. If no RuntimeClass resource + // matches the named class, the pod will not be run. If unset or empty, the + // "legacy" RuntimeClass will be used, which is an implicit class with an + // empty definition that uses the default runtime handler. + // More info: https://git.k8s.io/enhancements/keps/sig-node/runtime-class.md + // This is a beta feature as of Kubernetes v1.14. + // +optional + RuntimeClassName *string `json:"runtimeClassName,omitempty" protobuf:"bytes,2,opt,name=runtimeClassName"` + + // AutomountServiceAccountToken indicates whether pods running as this + // service account should have an API token automatically mounted. + // +optional + AutomountServiceAccountToken *bool `json:"automountServiceAccountToken,omitempty" protobuf:"varint,3,opt,name=automountServiceAccountToken"` + + // Set DNS policy for the pod. Defaults to "ClusterFirst". Valid values are + // 'ClusterFirst', 'Default' or 'None'. DNS parameters given in DNSConfig + // will be merged with the policy selected with DNSPolicy. + // +optional + DNSPolicy *corev1.DNSPolicy `json:"dnsPolicy,omitempty" protobuf:"bytes,4,opt,name=dnsPolicy,casttype=k8s.io/api/core/v1.DNSPolicy"` + + // Specifies the DNS parameters of a pod. + // Parameters specified here will be merged to the generated DNS + // configuration based on DNSPolicy. + // +optional + DNSConfig *corev1.PodDNSConfig `json:"dnsConfig,omitempty" protobuf:"bytes,5,opt,name=dnsConfig"` + + // EnableServiceLinks indicates whether information about services should be injected into pod's + // environment variables, matching the syntax of Docker links. + // Optional: Defaults to true. + // +optional + EnableServiceLinks *bool `json:"enableServiceLinks,omitempty" protobuf:"varint,6,opt,name=enableServiceLinks"` + + // If specified, indicates the pod's priority. "system-node-critical" and + // "system-cluster-critical" are two special keywords which indicate the + // highest priorities with the former being the highest priority. Any other + // name must be defined by creating a PriorityClass object with that name. + // If not specified, the pod priority will be default or zero if there is no + // default. + // +optional + PriorityClassName *string `json:"priorityClassName,omitempty" protobuf:"bytes,7,opt,name=priorityClassName"` +} + +func (tpl *Template) Equals(other *Template) bool { + if tpl == nil && other == nil { + return true + } + + if tpl == nil || other == nil { + return false + } + + return reflect.DeepEqual(tpl, other) +} diff --git a/pkg/apis/pipeline/pod/zz_generated.deepcopy.go b/pkg/apis/pipeline/pod/zz_generated.deepcopy.go new file mode 100644 index 00000000000..fa4226a30c2 --- /dev/null +++ b/pkg/apis/pipeline/pod/zz_generated.deepcopy.go @@ -0,0 +1,102 @@ +// +build !ignore_autogenerated + +/* +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. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package pod + +import ( + v1 "k8s.io/api/core/v1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Template) DeepCopyInto(out *Template) { + *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 + *out = new(v1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *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]) + } + } + if in.RuntimeClassName != nil { + in, out := &in.RuntimeClassName, &out.RuntimeClassName + *out = new(string) + **out = **in + } + if in.AutomountServiceAccountToken != nil { + in, out := &in.AutomountServiceAccountToken, &out.AutomountServiceAccountToken + *out = new(bool) + **out = **in + } + if in.DNSPolicy != nil { + in, out := &in.DNSPolicy, &out.DNSPolicy + *out = new(v1.DNSPolicy) + **out = **in + } + if in.DNSConfig != nil { + in, out := &in.DNSConfig, &out.DNSConfig + *out = new(v1.PodDNSConfig) + (*in).DeepCopyInto(*out) + } + if in.EnableServiceLinks != nil { + in, out := &in.EnableServiceLinks, &out.EnableServiceLinks + *out = new(bool) + **out = **in + } + if in.PriorityClassName != nil { + in, out := &in.PriorityClassName, &out.PriorityClassName + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Template. +func (in *Template) DeepCopy() *Template { + if in == nil { + return nil + } + out := new(Template) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/apis/pipeline/v1alpha1/pipelinerun_defaults.go b/pkg/apis/pipeline/v1alpha1/pipelinerun_defaults.go index eced242decc..e9fbd381159 100644 --- a/pkg/apis/pipeline/v1alpha1/pipelinerun_defaults.go +++ b/pkg/apis/pipeline/v1alpha1/pipelinerun_defaults.go @@ -51,4 +51,9 @@ func (prs *PipelineRunSpec) SetDefaults(ctx context.Context) { if prs.ServiceAccountName == "" && defaultSA != "" { prs.ServiceAccountName = defaultSA } + + defaultPodTemplate := cfg.Defaults.DefaultPodTemplate + if prs.PodTemplate == nil { + prs.PodTemplate = defaultPodTemplate + } } diff --git a/pkg/apis/pipeline/v1alpha1/pipelinerun_defaults_test.go b/pkg/apis/pipeline/v1alpha1/pipelinerun_defaults_test.go index 4232d658175..f687670a33e 100644 --- a/pkg/apis/pipeline/v1alpha1/pipelinerun_defaults_test.go +++ b/pkg/apis/pipeline/v1alpha1/pipelinerun_defaults_test.go @@ -57,6 +57,31 @@ func TestPipelineRunSpec_SetDefaults(t *testing.T) { Timeout: &metav1.Duration{Duration: 500 * time.Millisecond}, }, }, + { + desc: "pod template is nil", + prs: &v1alpha1.PipelineRunSpec{}, + want: &v1alpha1.PipelineRunSpec{ + Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute}, + }, + }, + { + desc: "pod template is not nil", + prs: &v1alpha1.PipelineRunSpec{ + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + want: &v1alpha1.PipelineRunSpec{ + Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute}, + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { @@ -151,6 +176,77 @@ func TestPipelineRunDefaulting(t *testing.T) { }) return s.ToContext(ctx) }, + }, { + name: "PipelineRef pod template is coming from default config pod template", + in: &v1alpha1.PipelineRun{ + Spec: v1alpha1.PipelineRunSpec{ + PipelineRef: &v1alpha1.PipelineRef{Name: "foo"}, + }, + }, + want: &v1alpha1.PipelineRun{ + Spec: v1alpha1.PipelineRunSpec{ + PipelineRef: &v1alpha1.PipelineRef{Name: "foo"}, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + ServiceAccountName: "tekton", + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + }, + wc: func(ctx context.Context) context.Context { + s := config.NewStore(logtesting.TestLogger(t)) + s.OnConfigChanged(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.DefaultsConfigName, + }, + Data: map[string]string{ + "default-timeout-minutes": "5", + "default-service-account": "tekton", + "default-pod-template": "nodeSelector: { 'label': 'value' }", + }, + }) + return s.ToContext(ctx) + }, + }, { + name: "PipelineRef pod template takes precedence over default config pod template", + in: &v1alpha1.PipelineRun{ + Spec: v1alpha1.PipelineRunSpec{ + PipelineRef: &v1alpha1.PipelineRef{Name: "foo"}, + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label2": "value2", + }, + }, + }, + }, + want: &v1alpha1.PipelineRun{ + Spec: v1alpha1.PipelineRunSpec{ + PipelineRef: &v1alpha1.PipelineRef{Name: "foo"}, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + ServiceAccountName: "tekton", + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label2": "value2", + }, + }, + }, + }, + wc: func(ctx context.Context) context.Context { + s := config.NewStore(logtesting.TestLogger(t)) + s.OnConfigChanged(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.DefaultsConfigName, + }, + Data: map[string]string{ + "default-timeout-minutes": "5", + "default-service-account": "tekton", + "default-pod-template": "nodeSelector: { 'label': 'value' }", + }, + }) + return s.ToContext(ctx) + }, }} for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go b/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go index b9fb97b6a84..28288537333 100644 --- a/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go +++ b/pkg/apis/pipeline/v1alpha1/pipelinerun_types.go @@ -62,7 +62,7 @@ type PipelineRunSpec struct { Timeout *metav1.Duration `json:"timeout,omitempty"` // PodTemplate holds pod specific configuration - PodTemplate PodTemplate `json:"podTemplate,omitempty"` + PodTemplate *PodTemplate `json:"podTemplate,omitempty"` } // PipelineRunSpecStatus defines the pipelinerun spec status the user can provide diff --git a/pkg/apis/pipeline/v1alpha1/pod.go b/pkg/apis/pipeline/v1alpha1/pod.go index 84afae9d3e7..b46d9fd3b25 100644 --- a/pkg/apis/pipeline/v1alpha1/pod.go +++ b/pkg/apis/pipeline/v1alpha1/pod.go @@ -1,90 +1,7 @@ -/* -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" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" ) -// 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"` - - // RuntimeClassName refers to a RuntimeClass object in the node.k8s.io - // group, which should be used to run this pod. If no RuntimeClass resource - // matches the named class, the pod will not be run. If unset or empty, the - // "legacy" RuntimeClass will be used, which is an implicit class with an - // empty definition that uses the default runtime handler. - // More info: https://git.k8s.io/enhancements/keps/sig-node/runtime-class.md - // This is a beta feature as of Kubernetes v1.14. - // +optional - RuntimeClassName *string `json:"runtimeClassName,omitempty" protobuf:"bytes,2,opt,name=runtimeClassName"` - - // AutomountServiceAccountToken indicates whether pods running as this - // service account should have an API token automatically mounted. - // +optional - AutomountServiceAccountToken *bool `json:"automountServiceAccountToken,omitempty" protobuf:"varint,3,opt,name=automountServiceAccountToken"` - - // Set DNS policy for the pod. Defaults to "ClusterFirst". Valid values are - // 'ClusterFirst', 'Default' or 'None'. DNS parameters given in DNSConfig - // will be merged with the policy selected with DNSPolicy. - // +optional - DNSPolicy *corev1.DNSPolicy `json:"dnsPolicy,omitempty" protobuf:"bytes,4,opt,name=dnsPolicy,casttype=k8s.io/api/core/v1.DNSPolicy"` - - // Specifies the DNS parameters of a pod. - // Parameters specified here will be merged to the generated DNS - // configuration based on DNSPolicy. - // +optional - DNSConfig *corev1.PodDNSConfig `json:"dnsConfig,omitempty" protobuf:"bytes,5,opt,name=dnsConfig"` - - // EnableServiceLinks indicates whether information about services should be injected into pod's - // environment variables, matching the syntax of Docker links. - // Optional: Defaults to true. - // +optional - EnableServiceLinks *bool `json:"enableServiceLinks,omitempty" protobuf:"varint,6,opt,name=enableServiceLinks"` - - // If specified, indicates the pod's priority. "system-node-critical" and - // "system-cluster-critical" are two special keywords which indicate the - // highest priorities with the former being the highest priority. Any other - // name must be defined by creating a PriorityClass object with that name. - // If not specified, the pod priority will be default or zero if there is no - // default. - // +optional - PriorityClassName *string `json:"priorityClassName,omitempty" protobuf:"bytes,7,opt,name=priorityClassName"` -} +type PodTemplate = pod.Template diff --git a/pkg/apis/pipeline/v1alpha1/taskrun_defaults.go b/pkg/apis/pipeline/v1alpha1/taskrun_defaults.go index dab7ddb3088..cd6310228f3 100644 --- a/pkg/apis/pipeline/v1alpha1/taskrun_defaults.go +++ b/pkg/apis/pipeline/v1alpha1/taskrun_defaults.go @@ -68,6 +68,11 @@ func (trs *TaskRunSpec) SetDefaults(ctx context.Context) { trs.ServiceAccountName = defaultSA } + defaultPodTemplate := cfg.Defaults.DefaultPodTemplate + if trs.PodTemplate == nil { + trs.PodTemplate = defaultPodTemplate + } + // If this taskrun has an embedded task, apply the usual task defaults if trs.TaskSpec != nil { trs.TaskSpec.SetDefaults(ctx) diff --git a/pkg/apis/pipeline/v1alpha1/taskrun_defaults_test.go b/pkg/apis/pipeline/v1alpha1/taskrun_defaults_test.go index 4a9e03e5f7f..153d41ec88c 100644 --- a/pkg/apis/pipeline/v1alpha1/taskrun_defaults_test.go +++ b/pkg/apis/pipeline/v1alpha1/taskrun_defaults_test.go @@ -64,6 +64,29 @@ func TestTaskRunSpec_SetDefaults(t *testing.T) { TaskRef: &v1alpha1.TaskRef{Kind: v1alpha1.ClusterTaskKind}, Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute}, }, + }, { + desc: "pod template is nil", + trs: &v1alpha1.TaskRunSpec{}, + want: &v1alpha1.TaskRunSpec{ + Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute}, + }, + }, { + desc: "pod template is not nil", + trs: &v1alpha1.TaskRunSpec{ + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + want: &v1alpha1.TaskRunSpec{ + Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute}, + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, }, { desc: "embedded taskSpec", trs: &v1alpha1.TaskRunSpec{ @@ -268,6 +291,83 @@ func TestTaskRunDefaulting(t *testing.T) { }) return s.ToContext(ctx) }, + }, { + name: "TaskRef pod template is coming from default config pod template", + in: &v1alpha1.TaskRun{ + Spec: v1alpha1.TaskRunSpec{ + TaskRef: &v1alpha1.TaskRef{Name: "foo"}, + }, + }, + want: &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "tekton-pipelines"}, + }, + Spec: v1alpha1.TaskRunSpec{ + TaskRef: &v1alpha1.TaskRef{Name: "foo", Kind: v1alpha1.NamespacedTaskKind}, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + ServiceAccountName: "tekton", + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + }, + wc: func(ctx context.Context) context.Context { + s := config.NewStore(logtesting.TestLogger(t)) + s.OnConfigChanged(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.DefaultsConfigName, + }, + Data: map[string]string{ + "default-timeout-minutes": "5", + "default-service-account": "tekton", + "default-pod-template": "nodeSelector: { 'label': 'value' }", + }, + }) + return s.ToContext(ctx) + }, + }, { + name: "TaskRef pod template takes precedence over default config pod template", + in: &v1alpha1.TaskRun{ + Spec: v1alpha1.TaskRunSpec{ + TaskRef: &v1alpha1.TaskRef{Name: "foo"}, + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label2": "value2", + }, + }, + }, + }, + want: &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "tekton-pipelines"}, + }, + Spec: v1alpha1.TaskRunSpec{ + TaskRef: &v1alpha1.TaskRef{Name: "foo", Kind: v1alpha1.NamespacedTaskKind}, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + ServiceAccountName: "tekton", + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label2": "value2", + }, + }, + }, + }, + wc: func(ctx context.Context) context.Context { + s := config.NewStore(logtesting.TestLogger(t)) + s.OnConfigChanged(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.DefaultsConfigName, + }, + Data: map[string]string{ + "default-timeout-minutes": "5", + "default-service-account": "tekton", + "default-pod-template": "nodeSelector: { 'label': 'value' }", + }, + }) + return s.ToContext(ctx) + }, }} for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/apis/pipeline/v1alpha1/taskrun_types.go b/pkg/apis/pipeline/v1alpha1/taskrun_types.go index 2915b77d9dd..a753e887478 100644 --- a/pkg/apis/pipeline/v1alpha1/taskrun_types.go +++ b/pkg/apis/pipeline/v1alpha1/taskrun_types.go @@ -51,7 +51,7 @@ type TaskRunSpec struct { // PodTemplate holds pod specific configuration // +optional - PodTemplate PodTemplate `json:"podTemplate,omitempty"` + PodTemplate *PodTemplate `json:"podTemplate,omitempty"` // Workspaces is a list of WorkspaceBindings from volumes to workspaces. // +optional diff --git a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go index 42aa8ba2a60..33ea95b96b8 100644 --- a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1alpha1 import ( + pod "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" v1alpha2 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha2" resourcev1alpha1 "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1" v1 "k8s.io/api/core/v1" @@ -759,7 +760,11 @@ func (in *PipelineRunSpec) DeepCopyInto(out *PipelineRunSpec) { *out = new(metav1.Duration) **out = **in } - in.PodTemplate.DeepCopyInto(&out.PodTemplate) + if in.PodTemplate != nil { + in, out := &in.PodTemplate, &out.PodTemplate + *out = new(pod.Template) + (*in).DeepCopyInto(*out) + } return } @@ -1132,83 +1137,6 @@ 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 - *out = new(v1.Affinity) - (*in).DeepCopyInto(*out) - } - if in.SecurityContext != nil { - in, out := &in.SecurityContext, &out.SecurityContext - *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]) - } - } - if in.RuntimeClassName != nil { - in, out := &in.RuntimeClassName, &out.RuntimeClassName - *out = new(string) - **out = **in - } - if in.AutomountServiceAccountToken != nil { - in, out := &in.AutomountServiceAccountToken, &out.AutomountServiceAccountToken - *out = new(bool) - **out = **in - } - if in.DNSPolicy != nil { - in, out := &in.DNSPolicy, &out.DNSPolicy - *out = new(v1.DNSPolicy) - **out = **in - } - if in.DNSConfig != nil { - in, out := &in.DNSConfig, &out.DNSConfig - *out = new(v1.PodDNSConfig) - (*in).DeepCopyInto(*out) - } - if in.EnableServiceLinks != nil { - in, out := &in.EnableServiceLinks, &out.EnableServiceLinks - *out = new(bool) - **out = **in - } - if in.PriorityClassName != nil { - in, out := &in.PriorityClassName, &out.PriorityClassName - *out = new(string) - **out = **in - } - 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 @@ -1512,7 +1440,11 @@ func (in *TaskRunSpec) DeepCopyInto(out *TaskRunSpec) { *out = new(metav1.Duration) **out = **in } - in.PodTemplate.DeepCopyInto(&out.PodTemplate) + 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([]v1alpha2.WorkspaceBinding, len(*in)) diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index bd3d058a3bc..0cd35f39521 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -163,9 +163,16 @@ func MakePod(images pipeline.Images, taskRun *v1alpha1.TaskRun, taskSpec v1alpha } } + // By default, use an empty pod template and take the one defined in the task run spec if any + podTemplate := v1alpha1.PodTemplate{} + + if taskRun.Spec.PodTemplate != nil { + podTemplate = *taskRun.Spec.PodTemplate + } + // Add podTemplate Volumes to the explicitly declared use volumes volumes = append(volumes, taskSpec.Volumes...) - volumes = append(volumes, taskRun.Spec.PodTemplate.Volumes...) + volumes = append(volumes, podTemplate.Volumes...) if err := v1alpha1.ValidateVolumes(volumes); err != nil { return nil, err @@ -179,13 +186,13 @@ func MakePod(images pipeline.Images, taskRun *v1alpha1.TaskRun, taskSpec v1alpha } var dnsPolicy corev1.DNSPolicy - if taskRun.Spec.PodTemplate.DNSPolicy != nil { - dnsPolicy = *taskRun.Spec.PodTemplate.DNSPolicy + if podTemplate.DNSPolicy != nil { + dnsPolicy = *podTemplate.DNSPolicy } var priorityClassName string - if taskRun.Spec.PodTemplate.PriorityClassName != nil { - priorityClassName = *taskRun.Spec.PodTemplate.PriorityClassName + if podTemplate.PriorityClassName != nil { + priorityClassName = *podTemplate.PriorityClassName } podAnnotations := taskRun.Annotations @@ -214,15 +221,15 @@ func MakePod(images pipeline.Images, taskRun *v1alpha1.TaskRun, taskSpec v1alpha Containers: mergedPodContainers, ServiceAccountName: taskRun.Spec.ServiceAccountName, Volumes: volumes, - NodeSelector: taskRun.Spec.PodTemplate.NodeSelector, - Tolerations: taskRun.Spec.PodTemplate.Tolerations, - Affinity: taskRun.Spec.PodTemplate.Affinity, - SecurityContext: taskRun.Spec.PodTemplate.SecurityContext, - RuntimeClassName: taskRun.Spec.PodTemplate.RuntimeClassName, - AutomountServiceAccountToken: taskRun.Spec.PodTemplate.AutomountServiceAccountToken, + NodeSelector: podTemplate.NodeSelector, + Tolerations: podTemplate.Tolerations, + Affinity: podTemplate.Affinity, + SecurityContext: podTemplate.SecurityContext, + RuntimeClassName: podTemplate.RuntimeClassName, + AutomountServiceAccountToken: podTemplate.AutomountServiceAccountToken, DNSPolicy: dnsPolicy, - DNSConfig: taskRun.Spec.PodTemplate.DNSConfig, - EnableServiceLinks: taskRun.Spec.PodTemplate.EnableServiceLinks, + DNSConfig: podTemplate.DNSConfig, + EnableServiceLinks: podTemplate.EnableServiceLinks, PriorityClassName: priorityClassName, }, }, nil diff --git a/pkg/pod/pod_test.go b/pkg/pod/pod_test.go index 80be50298d9..a30b34d75b9 100644 --- a/pkg/pod/pod_test.go +++ b/pkg/pod/pod_test.go @@ -171,7 +171,7 @@ func TestMakePod(t *testing.T) { }}}, }, trs: v1alpha1.TaskRunSpec{ - PodTemplate: v1alpha1.PodTemplate{ + PodTemplate: &v1alpha1.PodTemplate{ SecurityContext: &corev1.PodSecurityContext{ Sysctls: []corev1.Sysctl{ {Name: "net.ipv4.tcp_syncookies", Value: "1"}, @@ -645,7 +645,7 @@ script-heredoc-randomly-generated-78c5n } if d := cmp.Diff(c.want, &got.Spec, resourceQuantityCmp); d != "" { - t.Errorf("Diff(-want,+got): %s", d) + t.Errorf("Diff(-want, +got):\n%s", d) } }) } @@ -667,7 +667,7 @@ func TestMakeLabels(t *testing.T) { }, }, }) - if d := cmp.Diff(want, got); d != "" { - t.Errorf("Diff(-want,+got): %s", d) + if d := cmp.Diff(got, want); d != "" { + t.Errorf("Diff labels:\n%s", d) } } diff --git a/test/builder/pipeline.go b/test/builder/pipeline.go index 2616bbd6252..164ae5e1e3a 100644 --- a/test/builder/pipeline.go +++ b/test/builder/pipeline.go @@ -397,6 +397,9 @@ func PipelineRunNilTimeout(prs *v1alpha1.PipelineRunSpec) { // PipelineRunNodeSelector sets the Node selector to the PipelineRunSpec. func PipelineRunNodeSelector(values map[string]string) PipelineRunSpecOp { return func(prs *v1alpha1.PipelineRunSpec) { + if prs.PodTemplate == nil { + prs.PodTemplate = &v1alpha1.PodTemplate{} + } prs.PodTemplate.NodeSelector = values } } @@ -404,6 +407,9 @@ func PipelineRunNodeSelector(values map[string]string) PipelineRunSpecOp { // PipelineRunTolerations sets the Node selector to the PipelineRunSpec. func PipelineRunTolerations(values []corev1.Toleration) PipelineRunSpecOp { return func(prs *v1alpha1.PipelineRunSpec) { + if prs.PodTemplate == nil { + prs.PodTemplate = &v1alpha1.PodTemplate{} + } prs.PodTemplate.Tolerations = values } } @@ -411,6 +417,9 @@ func PipelineRunTolerations(values []corev1.Toleration) PipelineRunSpecOp { // PipelineRunAffinity sets the affinity to the PipelineRunSpec. func PipelineRunAffinity(affinity *corev1.Affinity) PipelineRunSpecOp { return func(prs *v1alpha1.PipelineRunSpec) { + if prs.PodTemplate == nil { + prs.PodTemplate = &v1alpha1.PodTemplate{} + } prs.PodTemplate.Affinity = affinity } } diff --git a/test/builder/pipeline_test.go b/test/builder/pipeline_test.go index ea26527fd4a..f4e6f293524 100644 --- a/test/builder/pipeline_test.go +++ b/test/builder/pipeline_test.go @@ -205,6 +205,78 @@ func TestPipelineRun(t *testing.T) { } } +func TestPipelineRunWithPodTemplate(t *testing.T) { + startTime := time.Now() + completedTime := startTime.Add(5 * time.Minute) + + pipelineRun := tb.PipelineRun("pear", "foo", tb.PipelineRunSpec( + "tomatoes", tb.PipelineRunServiceAccountName("sa"), + tb.PipelineRunParam("first-param-string", "first-value"), + tb.PipelineRunParam("second-param-array", "some", "array"), + tb.PipelineRunTimeout(1*time.Hour), + tb.PipelineRunResourceBinding("some-resource", tb.PipelineResourceBindingRef("my-special-resource")), + tb.PipelineRunServiceAccountNameTask("foo", "sa-2"), + tb.PipelineRunNodeSelector(map[string]string{ + "label": "value", + }), + ), tb.PipelineRunStatus(tb.PipelineRunStatusCondition( + apis.Condition{Type: apis.ConditionSucceeded}), + tb.PipelineRunStartTime(startTime), + tb.PipelineRunCompletionTime(completedTime), + tb.PipelineRunTaskRunsStatus("trname", &v1alpha1.PipelineRunTaskRunStatus{ + PipelineTaskName: "task-1", + }), + ), tb.PipelineRunLabel("label-key", "label-value")) + expectedPipelineRun := &v1alpha1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pear", + Namespace: "foo", + Labels: map[string]string{ + "label-key": "label-value", + }, + }, + Spec: v1alpha1.PipelineRunSpec{ + PipelineRef: &v1alpha1.PipelineRef{Name: "tomatoes"}, + ServiceAccountName: "sa", + ServiceAccountNames: []v1alpha1.PipelineRunSpecServiceAccountName{{TaskName: "foo", ServiceAccountName: "sa-2"}}, + Params: []v1alpha1.Param{{ + Name: "first-param-string", + Value: *tb.ArrayOrString("first-value"), + }, { + Name: "second-param-array", + Value: *tb.ArrayOrString("some", "array"), + }}, + Timeout: &metav1.Duration{Duration: 1 * time.Hour}, + Resources: []v1alpha1.PipelineResourceBinding{{ + Name: "some-resource", + ResourceRef: &v1alpha1.PipelineResourceRef{ + Name: "my-special-resource", + }, + }}, + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + Status: v1alpha1.PipelineRunStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{Type: apis.ConditionSucceeded}}, + }, + PipelineRunStatusFields: v1alpha1.PipelineRunStatusFields{ + StartTime: &metav1.Time{Time: startTime}, + CompletionTime: &metav1.Time{Time: completedTime}, + TaskRuns: map[string]*v1alpha1.PipelineRunTaskRunStatus{ + "trname": {PipelineTaskName: "task-1"}, + }, + }, + }, + } + if d := cmp.Diff(expectedPipelineRun, pipelineRun); d != "" { + t.Fatalf("PipelineRun diff -want, +got: %v", d) + } +} + func TestPipelineRunWithResourceSpec(t *testing.T) { startTime := time.Now() completedTime := startTime.Add(5 * time.Minute) diff --git a/test/builder/task.go b/test/builder/task.go index 61ac4986ed5..f12a4525b05 100644 --- a/test/builder/task.go +++ b/test/builder/task.go @@ -396,6 +396,9 @@ func TaskRunNilTimeout(spec *v1alpha1.TaskRunSpec) { // TaskRunNodeSelector sets the NodeSelector to the TaskRunSpec. func TaskRunNodeSelector(values map[string]string) TaskRunSpecOp { return func(spec *v1alpha1.TaskRunSpec) { + if spec.PodTemplate == nil { + spec.PodTemplate = &v1alpha1.PodTemplate{} + } spec.PodTemplate.NodeSelector = values } } @@ -403,6 +406,9 @@ func TaskRunNodeSelector(values map[string]string) TaskRunSpecOp { // TaskRunTolerations sets the Tolerations to the TaskRunSpec. func TaskRunTolerations(values []corev1.Toleration) TaskRunSpecOp { return func(spec *v1alpha1.TaskRunSpec) { + if spec.PodTemplate == nil { + spec.PodTemplate = &v1alpha1.PodTemplate{} + } spec.PodTemplate.Tolerations = values } } @@ -410,6 +416,9 @@ func TaskRunTolerations(values []corev1.Toleration) TaskRunSpecOp { // TaskRunAffinity sets the Affinity to the TaskRunSpec. func TaskRunAffinity(affinity *corev1.Affinity) TaskRunSpecOp { return func(spec *v1alpha1.TaskRunSpec) { + if spec.PodTemplate == nil { + spec.PodTemplate = &v1alpha1.PodTemplate{} + } spec.PodTemplate.Affinity = affinity } } @@ -417,6 +426,9 @@ func TaskRunAffinity(affinity *corev1.Affinity) TaskRunSpecOp { // TaskRunPodSecurityContext sets the SecurityContext to the TaskRunSpec (through PodTemplate). func TaskRunPodSecurityContext(context *corev1.PodSecurityContext) TaskRunSpecOp { return func(spec *v1alpha1.TaskRunSpec) { + if spec.PodTemplate == nil { + spec.PodTemplate = &v1alpha1.PodTemplate{} + } spec.PodTemplate.SecurityContext = context } } diff --git a/test/builder/task_test.go b/test/builder/task_test.go index 60970bdf739..11a5cd96a59 100644 --- a/test/builder/task_test.go +++ b/test/builder/task_test.go @@ -331,6 +331,55 @@ func TestTaskRunWithTaskSpec(t *testing.T) { } } +func TestTaskRunWithPodTemplate(t *testing.T) { + taskRun := tb.TaskRun("test-taskrun", "foo", tb.TaskRunSpec( + tb.TaskRunTaskSpec( + tb.Step("image", tb.StepCommand("/mycmd")), + tb.TaskInputs(tb.InputsResource("workspace", v1alpha1.PipelineResourceTypeGit, tb.ResourceOptional(true))), + ), + tb.TaskRunServiceAccountName("sa"), + tb.TaskRunTimeout(2*time.Minute), + tb.TaskRunSpecStatus(v1alpha1.TaskRunSpecStatusCancelled), + tb.TaskRunNodeSelector(map[string]string{ + "label": "value", + }), + )) + expectedTaskRun := &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-taskrun", Namespace: "foo", + Annotations: map[string]string{}, + }, + Spec: v1alpha1.TaskRunSpec{ + TaskSpec: &v1alpha1.TaskSpec{ + Steps: []v1alpha1.Step{{Container: corev1.Container{ + Image: "image", + Command: []string{"/mycmd"}, + }}}, + Inputs: &v1alpha1.Inputs{ + Resources: []v1alpha1.TaskResource{{ + ResourceDeclaration: v1alpha1.ResourceDeclaration{ + Name: "workspace", + Type: v1alpha1.PipelineResourceTypeGit, + Optional: true, + }}}, + Params: nil, + }, + }, + PodTemplate: &v1alpha1.PodTemplate{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + ServiceAccountName: "sa", + Status: v1alpha1.TaskRunSpecStatusCancelled, + Timeout: &metav1.Duration{Duration: 2 * time.Minute}, + }, + } + if d := cmp.Diff(expectedTaskRun, taskRun); d != "" { + t.Fatalf("TaskRun diff -want, +got: %v", d) + } +} + func TestResolvedTaskResources(t *testing.T) { resolvedTaskResources := tb.ResolvedTaskResources( tb.ResolvedTaskResourcesTaskSpec(