From 0d22419a93e2211e46399b20b4556e2dde1f625e Mon Sep 17 00:00:00 2001 From: Jerome Ju Date: Wed, 3 Aug 2022 15:09:10 +0000 Subject: [PATCH] Add V1 TaskRun Golang structs --- pkg/apis/pipeline/v1/pod.go | 112 ++++ pkg/apis/pipeline/v1/taskref_types.go | 2 + pkg/apis/pipeline/v1/taskrun_defaults.go | 72 +++ pkg/apis/pipeline/v1/taskrun_defaults_test.go | 426 +++++++++++++++ pkg/apis/pipeline/v1/taskrun_types.go | 463 ++++++++++++++++ pkg/apis/pipeline/v1/taskrun_types_test.go | 361 +++++++++++++ pkg/apis/pipeline/v1/taskrun_validation.go | 189 +++++++ .../pipeline/v1/taskrun_validation_test.go | 504 ++++++++++++++++++ 8 files changed, 2129 insertions(+) create mode 100644 pkg/apis/pipeline/v1/pod.go create mode 100644 pkg/apis/pipeline/v1/taskrun_defaults.go create mode 100644 pkg/apis/pipeline/v1/taskrun_defaults_test.go create mode 100644 pkg/apis/pipeline/v1/taskrun_types.go create mode 100644 pkg/apis/pipeline/v1/taskrun_types_test.go create mode 100644 pkg/apis/pipeline/v1/taskrun_validation.go create mode 100644 pkg/apis/pipeline/v1/taskrun_validation_test.go diff --git a/pkg/apis/pipeline/v1/pod.go b/pkg/apis/pipeline/v1/pod.go new file mode 100644 index 00000000000..56b11ed5739 --- /dev/null +++ b/pkg/apis/pipeline/v1/pod.go @@ -0,0 +1,112 @@ +/* +Copyright 2022 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 v1 + +import "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" + +// PodTemplate holds pod specific configuration +type PodTemplate = pod.Template + +// MergePodTemplateWithDefault merges 2 PodTemplates together. If the same +// field is set on both templates, the value from tpl will overwrite the value +// from defaultTpl. +func MergePodTemplateWithDefault(tpl, defaultTpl *PodTemplate) *PodTemplate { + switch { + case defaultTpl == nil: + // No configured default, just return the template + return tpl + case tpl == nil: + // No template, just return the default template + return defaultTpl + default: + // Otherwise, merge fields + if tpl.NodeSelector == nil { + tpl.NodeSelector = defaultTpl.NodeSelector + } + if tpl.Tolerations == nil { + tpl.Tolerations = defaultTpl.Tolerations + } + if tpl.Affinity == nil { + tpl.Affinity = defaultTpl.Affinity + } + if tpl.SecurityContext == nil { + tpl.SecurityContext = defaultTpl.SecurityContext + } + if tpl.Volumes == nil { + tpl.Volumes = defaultTpl.Volumes + } + if tpl.RuntimeClassName == nil { + tpl.RuntimeClassName = defaultTpl.RuntimeClassName + } + if tpl.AutomountServiceAccountToken == nil { + tpl.AutomountServiceAccountToken = defaultTpl.AutomountServiceAccountToken + } + if tpl.DNSPolicy == nil { + tpl.DNSPolicy = defaultTpl.DNSPolicy + } + if tpl.DNSConfig == nil { + tpl.DNSConfig = defaultTpl.DNSConfig + } + if tpl.EnableServiceLinks == nil { + tpl.EnableServiceLinks = defaultTpl.EnableServiceLinks + } + if tpl.PriorityClassName == nil { + tpl.PriorityClassName = defaultTpl.PriorityClassName + } + if tpl.SchedulerName == "" { + tpl.SchedulerName = defaultTpl.SchedulerName + } + if tpl.ImagePullSecrets == nil { + tpl.ImagePullSecrets = defaultTpl.ImagePullSecrets + } + if tpl.HostAliases == nil { + tpl.HostAliases = defaultTpl.HostAliases + } + if tpl.HostNetwork == false && defaultTpl.HostNetwork == true { + tpl.HostNetwork = true + } + return tpl + } +} + +// AAPodTemplate holds pod specific configuration for the affinity-assistant +type AAPodTemplate = pod.AffinityAssistantTemplate + +// MergeAAPodTemplateWithDefault is the same as MergePodTemplateWithDefault but +// for AffinityAssistantPodTemplates. +func MergeAAPodTemplateWithDefault(tpl, defaultTpl *AAPodTemplate) *AAPodTemplate { + switch { + case defaultTpl == nil: + // No configured default, just return the template + return tpl + case tpl == nil: + // No template, just return the default template + return defaultTpl + default: + // Otherwise, merge fields + if tpl.NodeSelector == nil { + tpl.NodeSelector = defaultTpl.NodeSelector + } + if tpl.Tolerations == nil { + tpl.Tolerations = defaultTpl.Tolerations + } + if tpl.ImagePullSecrets == nil { + tpl.ImagePullSecrets = defaultTpl.ImagePullSecrets + } + return tpl + } +} diff --git a/pkg/apis/pipeline/v1/taskref_types.go b/pkg/apis/pipeline/v1/taskref_types.go index 74a319dd713..675c5cba6ab 100644 --- a/pkg/apis/pipeline/v1/taskref_types.go +++ b/pkg/apis/pipeline/v1/taskref_types.go @@ -39,4 +39,6 @@ type TaskKind string const ( // NamespacedTaskKind indicates that the task type has a namespaced scope. NamespacedTaskKind TaskKind = "Task" + // ClusterTaskKind indicates that task type has a cluster scope. + ClusterTaskKind TaskKind = "ClusterTask" ) diff --git a/pkg/apis/pipeline/v1/taskrun_defaults.go b/pkg/apis/pipeline/v1/taskrun_defaults.go new file mode 100644 index 00000000000..3314892a3b3 --- /dev/null +++ b/pkg/apis/pipeline/v1/taskrun_defaults.go @@ -0,0 +1,72 @@ +/* +Copyright 2022 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 v1 + +import ( + "context" + "time" + + "github.com/tektoncd/pipeline/pkg/apis/config" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/apis" +) + +var _ apis.Defaultable = (*TaskRun)(nil) + +// ManagedByLabelKey is the label key used to mark what is managing this resource +const ManagedByLabelKey = "app.kubernetes.io/managed-by" + +// SetDefaults implements apis.Defaultable +func (tr *TaskRun) SetDefaults(ctx context.Context) { + ctx = apis.WithinParent(ctx, tr.ObjectMeta) + tr.Spec.SetDefaults(ctx) + + // If the TaskRun doesn't have a managed-by label, apply the default + // specified in the config. + cfg := config.FromContextOrDefaults(ctx) + if tr.ObjectMeta.Labels == nil { + tr.ObjectMeta.Labels = map[string]string{} + } + if _, found := tr.ObjectMeta.Labels[ManagedByLabelKey]; !found { + tr.ObjectMeta.Labels[ManagedByLabelKey] = cfg.Defaults.DefaultManagedByLabelValue + } +} + +// SetDefaults implements apis.Defaultable +func (trs *TaskRunSpec) SetDefaults(ctx context.Context) { + cfg := config.FromContextOrDefaults(ctx) + if trs.TaskRef != nil && trs.TaskRef.Kind == "" { + trs.TaskRef.Kind = NamespacedTaskKind + } + + if trs.Timeout == nil { + trs.Timeout = &metav1.Duration{Duration: time.Duration(cfg.Defaults.DefaultTimeoutMinutes) * time.Minute} + } + + defaultSA := cfg.Defaults.DefaultServiceAccount + if trs.ServiceAccountName == "" && defaultSA != "" { + trs.ServiceAccountName = defaultSA + } + + defaultPodTemplate := cfg.Defaults.DefaultPodTemplate + trs.PodTemplate = MergePodTemplateWithDefault(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/v1/taskrun_defaults_test.go b/pkg/apis/pipeline/v1/taskrun_defaults_test.go new file mode 100644 index 00000000000..4e75f366040 --- /dev/null +++ b/pkg/apis/pipeline/v1/taskrun_defaults_test.go @@ -0,0 +1,426 @@ +/* +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 v1_test + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/tektoncd/pipeline/pkg/apis/config" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" + "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" + logtesting "knative.dev/pkg/logging/testing" +) + +var ( + ignoreUnexportedResources = cmpopts.IgnoreUnexported() + ttrue = true +) + +func TestTaskRunSpec_SetDefaults(t *testing.T) { + cases := []struct { + desc string + trs *v1beta1.TaskRunSpec + want *v1beta1.TaskRunSpec + }{{ + desc: "taskref is nil", + trs: &v1beta1.TaskRunSpec{ + TaskRef: nil, + Timeout: &metav1.Duration{Duration: 500 * time.Millisecond}, + }, + want: &v1beta1.TaskRunSpec{ + TaskRef: nil, + ServiceAccountName: config.DefaultServiceAccountValue, + Timeout: &metav1.Duration{Duration: 500 * time.Millisecond}, + }, + }, { + desc: "taskref kind is empty", + trs: &v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{}, + Timeout: &metav1.Duration{Duration: 500 * time.Millisecond}, + }, + want: &v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Kind: v1beta1.NamespacedTaskKind}, + ServiceAccountName: config.DefaultServiceAccountValue, + Timeout: &metav1.Duration{Duration: 500 * time.Millisecond}, + }, + }, { + desc: "timeout is nil", + trs: &v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Kind: v1beta1.ClusterTaskKind}, + }, + want: &v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Kind: v1beta1.ClusterTaskKind}, + ServiceAccountName: config.DefaultServiceAccountValue, + Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute}, + }, + }, { + desc: "pod template is nil", + trs: &v1beta1.TaskRunSpec{}, + want: &v1beta1.TaskRunSpec{ + ServiceAccountName: config.DefaultServiceAccountValue, + Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute}, + }, + }, { + desc: "pod template is not nil", + trs: &v1beta1.TaskRunSpec{ + PodTemplate: &pod.Template{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + want: &v1beta1.TaskRunSpec{ + ServiceAccountName: config.DefaultServiceAccountValue, + Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute}, + PodTemplate: &pod.Template{ + NodeSelector: map[string]string{ + "label": "value", + }, + }, + }, + }, { + desc: "embedded taskSpec", + trs: &v1beta1.TaskRunSpec{ + TaskSpec: &v1beta1.TaskSpec{ + Params: []v1beta1.ParamSpec{{ + Name: "param-name", + }}, + }, + }, + want: &v1beta1.TaskRunSpec{ + TaskSpec: &v1beta1.TaskSpec{ + Params: []v1beta1.ParamSpec{{ + Name: "param-name", + Type: v1beta1.ParamTypeString, + }}, + }, + ServiceAccountName: config.DefaultServiceAccountValue, + Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute}, + }, + }} + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + ctx := context.Background() + tc.trs.SetDefaults(ctx) + + if d := cmp.Diff(tc.want, tc.trs); d != "" { + t.Errorf("Mismatch of TaskRunSpec: %s", diff.PrintWantGot(d)) + } + }) + } +} + +func TestTaskRunDefaulting(t *testing.T) { + tests := []struct { + name string + in *v1beta1.TaskRun + want *v1beta1.TaskRun + wc func(context.Context) context.Context + }{{ + name: "empty no context", + in: &v1beta1.TaskRun{}, + want: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "tekton-pipelines"}, + }, + Spec: v1beta1.TaskRunSpec{ + ServiceAccountName: config.DefaultServiceAccountValue, + Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute}, + }, + }, + }, { + name: "TaskRef default to namespace kind", + in: &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo"}, + }, + }, + want: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "tekton-pipelines"}, + }, + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo", Kind: v1beta1.NamespacedTaskKind}, + ServiceAccountName: config.DefaultServiceAccountValue, + Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute}, + }, + }, + }, { + name: "TaskRef default config context", + in: &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo"}, + }, + }, + want: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "tekton-pipelines"}, + }, + Spec: v1beta1.TaskRunSpec{ + ServiceAccountName: config.DefaultServiceAccountValue, + TaskRef: &v1beta1.TaskRef{Name: "foo", Kind: v1beta1.NamespacedTaskKind}, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + }, + }, + wc: func(ctx context.Context) context.Context { + s := config.NewStore(logtesting.TestLogger(t)) + s.OnConfigChanged(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.GetDefaultsConfigName(), + }, + Data: map[string]string{ + "default-timeout-minutes": "5", + }, + }) + return s.ToContext(ctx) + }, + }, { + name: "TaskRef default config context with SA", + in: &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo"}, + }, + }, + want: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "tekton-pipelines"}, + }, + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo", Kind: v1beta1.NamespacedTaskKind}, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + ServiceAccountName: "tekton", + }, + }, + wc: func(ctx context.Context) context.Context { + s := config.NewStore(logtesting.TestLogger(t)) + s.OnConfigChanged(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.GetDefaultsConfigName(), + }, + Data: map[string]string{ + "default-timeout-minutes": "5", + "default-service-account": "tekton", + }, + }) + return s.ToContext(ctx) + }, + }, { + name: "TaskRun managed-by set in config", + in: &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo"}, + }, + }, + want: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "something-else"}, + }, + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo", Kind: v1beta1.NamespacedTaskKind}, + ServiceAccountName: config.DefaultServiceAccountValue, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + }, + }, + wc: func(ctx context.Context) context.Context { + s := config.NewStore(logtesting.TestLogger(t)) + s.OnConfigChanged(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.GetDefaultsConfigName(), + }, + Data: map[string]string{ + "default-timeout-minutes": "5", + "default-managed-by-label-value": "something-else", + }, + }) + return s.ToContext(ctx) + }, + }, { + name: "TaskRun managed-by set in request and config (request wins)", + in: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "user-specified"}, + }, + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo"}, + }, + }, + want: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "user-specified"}, + }, + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo", Kind: v1beta1.NamespacedTaskKind}, + ServiceAccountName: config.DefaultServiceAccountValue, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + }, + }, + wc: func(ctx context.Context) context.Context { + s := config.NewStore(logtesting.TestLogger(t)) + s.OnConfigChanged(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.GetDefaultsConfigName(), + }, + Data: map[string]string{ + "default-timeout-minutes": "5", + "default-managed-by-label-value": "something-else", + }, + }) + return s.ToContext(ctx) + }, + }, { + name: "TaskRef pod template is coming from default config pod template", + in: &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo"}, + }, + }, + want: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "tekton-pipelines"}, + }, + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo", Kind: v1beta1.NamespacedTaskKind}, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + ServiceAccountName: "tekton", + PodTemplate: &pod.Template{ + 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.GetDefaultsConfigName(), + }, + 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 NodeSelector takes precedence over default config pod template NodeSelector", + in: &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo"}, + PodTemplate: &pod.Template{ + NodeSelector: map[string]string{ + "label2": "value2", + }, + }, + }, + }, + want: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "tekton-pipelines"}, + }, + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo", Kind: v1beta1.NamespacedTaskKind}, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + ServiceAccountName: "tekton", + PodTemplate: &pod.Template{ + 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.GetDefaultsConfigName(), + }, + 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 merges non competing fields with default config pod template", + in: &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo"}, + PodTemplate: &pod.Template{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &ttrue, + }, + }, + }, + }, + want: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app.kubernetes.io/managed-by": "tekton-pipelines"}, + }, + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "foo", Kind: v1beta1.NamespacedTaskKind}, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + ServiceAccountName: "tekton", + PodTemplate: &pod.Template{ + NodeSelector: map[string]string{ + "label": "value", + }, + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &ttrue, + }, + }, + }, + }, + wc: func(ctx context.Context) context.Context { + s := config.NewStore(logtesting.TestLogger(t)) + s.OnConfigChanged(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.GetDefaultsConfigName(), + }, + 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) { + got := tc.in + ctx := context.Background() + if tc.wc != nil { + ctx = tc.wc(ctx) + } + got.SetDefaults(ctx) + if !cmp.Equal(got, tc.want, ignoreUnexportedResources) { + d := cmp.Diff(got, tc.want, ignoreUnexportedResources) + t.Errorf("SetDefaults %s", diff.PrintWantGot(d)) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1/taskrun_types.go b/pkg/apis/pipeline/v1/taskrun_types.go new file mode 100644 index 00000000000..74c3802ff51 --- /dev/null +++ b/pkg/apis/pipeline/v1/taskrun_types.go @@ -0,0 +1,463 @@ +/* +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 v1 + +import ( + "context" + "fmt" + "time" + + "github.com/tektoncd/pipeline/pkg/apis/config" + apisconfig "github.com/tektoncd/pipeline/pkg/apis/config" + "github.com/tektoncd/pipeline/pkg/apis/pipeline" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/clock" + "knative.dev/pkg/apis" + duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" +) + +// TaskRunSpec defines the desired state of TaskRun +type TaskRunSpec struct { + // +optional + Debug *TaskRunDebug `json:"debug,omitempty"` + // +optional + // +listType=atomic + Params []Param `json:"params,omitempty"` + // +optional + ServiceAccountName string `json:"serviceAccountName"` + // no more than one of the TaskRef and TaskSpec may be specified. + // +optional + TaskRef *TaskRef `json:"taskRef,omitempty"` + // +optional + TaskSpec *TaskSpec `json:"taskSpec,omitempty"` + // Used for cancelling a taskrun (and maybe more later on) + // +optional + Status TaskRunSpecStatus `json:"status,omitempty"` + // Time after which the build times out. Defaults to 1 hour. + // Specified build timeout should be less than 24h. + // 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"` + // Workspaces is a list of WorkspaceBindings from volumes to workspaces. + // +optional + // +listType=atomic + Workspaces []WorkspaceBinding `json:"workspaces,omitempty"` + // Overrides to apply to Steps in this TaskRun. + // If a field is specified in both a Step and a StepOverride, + // the value from the StepOverride will be used. + // This field is only supported when the alpha feature gate is enabled. + // +optional + // +listType=atomic + StepOverrides []TaskRunStepOverride `json:"stepOverrides,omitempty"` + // Overrides to apply to Sidecars in this TaskRun. + // If a field is specified in both a Sidecar and a SidecarOverride, + // the value from the SidecarOverride will be used. + // This field is only supported when the alpha feature gate is enabled. + // +optional + // +listType=atomic + SidecarOverrides []TaskRunSidecarOverride `json:"sidecarOverrides,omitempty"` + // Compute resources to use for this TaskRun + ComputeResources *corev1.ResourceRequirements `json:"computeResources,omitempty"` +} + +// TaskRunSpecStatus defines the taskrun spec status the user can provide +type TaskRunSpecStatus string + +const ( + // TaskRunSpecStatusCancelled indicates that the user wants to cancel the task, + // if not already cancelled or terminated + TaskRunSpecStatusCancelled = "TaskRunCancelled" +) + +// TaskRunDebug defines the breakpoint config for a particular TaskRun +type TaskRunDebug struct { + // +optional + // +listType=atomic + Breakpoint []string `json:"breakpoint,omitempty"` +} + +// TODO task_validation_test + +// TaskRunInputs holds the input values that this task was invoked with. +type TaskRunInputs struct { + // +optional + // +listType=atomic + Params []Param `json:"params,omitempty"` +} + +var taskRunCondSet = apis.NewBatchConditionSet() + +// TaskRunStatus defines the observed state of TaskRun +type TaskRunStatus struct { + duckv1beta1.Status `json:",inline"` + + // TaskRunStatusFields inlines the status fields. + TaskRunStatusFields `json:",inline"` +} + +// TaskRunReason is an enum used to store all TaskRun reason for +// the Succeeded condition that are controlled by the TaskRun itself. Failure +// reasons that emerge from underlying resources are not included here +type TaskRunReason string + +const ( + // TaskRunReasonStarted is the reason set when the TaskRun has just started + TaskRunReasonStarted TaskRunReason = "Started" + // TaskRunReasonRunning is the reason set when the TaskRun is running + TaskRunReasonRunning TaskRunReason = "Running" + // TaskRunReasonSuccessful is the reason set when the TaskRun completed successfully + TaskRunReasonSuccessful TaskRunReason = "Succeeded" + // TaskRunReasonFailed is the reason set when the TaskRun completed with a failure + TaskRunReasonFailed TaskRunReason = "Failed" + // TaskRunReasonCancelled is the reason set when the Taskrun is cancelled by the user + TaskRunReasonCancelled TaskRunReason = "TaskRunCancelled" + // TaskRunReasonTimedOut is the reason set when the Taskrun has timed out + TaskRunReasonTimedOut TaskRunReason = "TaskRunTimeout" + // TaskRunReasonResolvingTaskRef indicates that the TaskRun is waiting for + // its taskRef to be asynchronously resolved. + TaskRunReasonResolvingTaskRef = "ResolvingTaskRef" + // TaskRunReasonImagePullFailed is the reason set when the step of a task fails due to image not being pulled + TaskRunReasonImagePullFailed TaskRunReason = "TaskRunImagePullFailed" +) + +func (t TaskRunReason) String() string { + return string(t) +} + +// GetStartedReason returns the reason set to the "Succeeded" condition when +// InitializeConditions is invoked +func (trs *TaskRunStatus) GetStartedReason() string { + return TaskRunReasonStarted.String() +} + +// GetRunningReason returns the reason set to the "Succeeded" condition when +// the TaskRun starts running. This is used indicate that the resource +// could be validated is starting to perform its job. +func (trs *TaskRunStatus) GetRunningReason() string { + return TaskRunReasonRunning.String() +} + +// MarkResourceOngoing sets the ConditionSucceeded condition to ConditionUnknown +// with the reason and message. +func (trs *TaskRunStatus) MarkResourceOngoing(reason TaskRunReason, message string) { + taskRunCondSet.Manage(trs).SetCondition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionUnknown, + Reason: reason.String(), + Message: message, + }) +} + +// MarkResourceFailed sets the ConditionSucceeded condition to ConditionFalse +// based on an error that occurred and a reason +func (trs *TaskRunStatus) MarkResourceFailed(reason TaskRunReason, err error) { + taskRunCondSet.Manage(trs).SetCondition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + Reason: reason.String(), + Message: err.Error(), + }) + succeeded := trs.GetCondition(apis.ConditionSucceeded) + trs.CompletionTime = &succeeded.LastTransitionTime.Inner +} + +// TaskRunStatusFields holds the fields of TaskRun's status. This is defined +// separately and inlined so that other types can readily consume these fields +// via duck typing. +type TaskRunStatusFields struct { + // PodName is the name of the pod responsible for executing this task's steps. + PodName string `json:"podName"` + + // StartTime is the time the build is actually started. + // +optional + StartTime *metav1.Time `json:"startTime,omitempty"` + + // CompletionTime is the time the build completed. + // +optional + CompletionTime *metav1.Time `json:"completionTime,omitempty"` + + // Steps describes the state of each build step container. + // +optional + // +listType=atomic + Steps []StepState `json:"steps,omitempty"` + + // CloudEvents describe the state of each cloud event requested via a + // CloudEventResource. + // +optional + // +listType=atomic + CloudEvents []CloudEventDelivery `json:"cloudEvents,omitempty"` + + // RetriesStatus contains the history of TaskRunStatus in case of a retry in order to keep record of failures. + // All TaskRunStatus stored in RetriesStatus will have no date within the RetriesStatus as is redundant. + // +optional + // +listType=atomic + RetriesStatus []TaskRunStatus `json:"retriesStatus,omitempty"` + + // TaskRunResults are the list of results written out by the task's containers + // +optional + // +listType=atomic + TaskRunResults []TaskRunResult `json:"taskResults,omitempty"` + + // The list has one entry per sidecar in the manifest. Each entry is + // represents the imageid of the corresponding sidecar. + // +listType=atomic + Sidecars []SidecarState `json:"sidecars,omitempty"` + + // TaskSpec contains the Spec from the dereferenced Task definition used to instantiate this TaskRun. + TaskSpec *TaskSpec `json:"taskSpec,omitempty"` +} + +// TaskRunStepOverride is used to override the values of a Step in the corresponding Task. +type TaskRunStepOverride struct { + // The name of the Step to override. + Name string `json:"name"` + // The resource requirements to apply to the Step. + Resources corev1.ResourceRequirements `json:"resources"` +} + +// TaskRunSidecarOverride is used to override the values of a Sidecar in the corresponding Task. +type TaskRunSidecarOverride struct { + // The name of the Sidecar to override. + Name string `json:"name"` + // The resource requirements to apply to the Sidecar. + Resources corev1.ResourceRequirements `json:"resources"` +} + +// GetGroupVersionKind implements kmeta.OwnerRefable. +func (*TaskRun) GetGroupVersionKind() schema.GroupVersionKind { + return SchemeGroupVersion.WithKind(pipeline.TaskRunControllerName) +} + +// GetStatusCondition returns the task run status as a ConditionAccessor +func (tr *TaskRun) GetStatusCondition() apis.ConditionAccessor { + return &tr.Status +} + +// GetCondition returns the Condition matching the given type. +func (trs *TaskRunStatus) GetCondition(t apis.ConditionType) *apis.Condition { + return taskRunCondSet.Manage(trs).GetCondition(t) +} + +// InitializeConditions will set all conditions in taskRunCondSet to unknown for the TaskRun +// and set the started time to the current time +func (trs *TaskRunStatus) InitializeConditions() { + started := false + if trs.StartTime.IsZero() { + trs.StartTime = &metav1.Time{Time: time.Now()} + started = true + } + conditionManager := taskRunCondSet.Manage(trs) + conditionManager.InitializeConditions() + // Ensure the started reason is set for the "Succeeded" condition + if started { + initialCondition := conditionManager.GetCondition(apis.ConditionSucceeded) + initialCondition.Reason = TaskRunReasonStarted.String() + conditionManager.SetCondition(*initialCondition) + } +} + +// SetCondition sets the condition, unsetting previous conditions with the same +// type as necessary. +func (trs *TaskRunStatus) SetCondition(newCond *apis.Condition) { + if newCond != nil { + taskRunCondSet.Manage(trs).SetCondition(*newCond) + } +} + +// StepState reports the results of running a step in a Task. +type StepState struct { + corev1.ContainerState `json:",inline"` + Name string `json:"name,omitempty"` + ContainerName string `json:"container,omitempty"` + ImageID string `json:"imageID,omitempty"` +} + +// SidecarState reports the results of running a sidecar in a Task. +type SidecarState struct { + corev1.ContainerState `json:",inline"` + Name string `json:"name,omitempty"` + ContainerName string `json:"container,omitempty"` + ImageID string `json:"imageID,omitempty"` +} + +// CloudEventDelivery is the target of a cloud event along with the state of +// delivery. +type CloudEventDelivery struct { + // Target points to an addressable + Target string `json:"target,omitempty"` + Status CloudEventDeliveryState `json:"status,omitempty"` +} + +// CloudEventCondition is a string that represents the condition of the event. +type CloudEventCondition string + +const ( + // CloudEventConditionUnknown means that the condition for the event to be + // triggered was not met yet, or we don't know the state yet. + CloudEventConditionUnknown CloudEventCondition = "Unknown" + // CloudEventConditionSent means that the event was sent successfully + CloudEventConditionSent CloudEventCondition = "Sent" + // CloudEventConditionFailed means that there was one or more attempts to + // send the event, and none was successful so far. + CloudEventConditionFailed CloudEventCondition = "Failed" +) + +// CloudEventDeliveryState reports the state of a cloud event to be sent. +type CloudEventDeliveryState struct { + // Current status + Condition CloudEventCondition `json:"condition,omitempty"` + // SentAt is the time at which the last attempt to send the event was made + // +optional + SentAt *metav1.Time `json:"sentAt,omitempty"` + // Error is the text of error (if any) + Error string `json:"message"` + // RetryCount is the number of attempts of sending the cloud event + RetryCount int32 `json:"retryCount"` +} + +// +genclient +// +genreconciler:krshapedlogic=false +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TaskRun represents a single execution of a Task. TaskRuns are how the steps +// specified in a Task are executed; they specify the parameters and resources +// used to run the steps in a Task. +// +// +k8s:openapi-gen=true +type TaskRun struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +optional + Spec TaskRunSpec `json:"spec,omitempty"` + // +optional + Status TaskRunStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TaskRunList contains a list of TaskRun +type TaskRunList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + Items []TaskRun `json:"items"` +} + +// GetPipelineRunPVCName for taskrun gets pipelinerun +func (tr *TaskRun) GetPipelineRunPVCName() string { + if tr == nil { + return "" + } + for _, ref := range tr.GetOwnerReferences() { + if ref.Kind == pipeline.PipelineRunControllerName { + return fmt.Sprintf("%s-pvc", ref.Name) + } + } + return "" +} + +// HasPipelineRunOwnerReference returns true of TaskRun has +// owner reference of type PipelineRun +func (tr *TaskRun) HasPipelineRunOwnerReference() bool { + for _, ref := range tr.GetOwnerReferences() { + if ref.Kind == pipeline.PipelineRunControllerName { + return true + } + } + return false +} + +// IsDone returns true if the TaskRun's status indicates that it is done. +func (tr *TaskRun) IsDone() bool { + return !tr.Status.GetCondition(apis.ConditionSucceeded).IsUnknown() +} + +// HasStarted function check whether taskrun has valid start time set in its status +func (tr *TaskRun) HasStarted() bool { + return tr.Status.StartTime != nil && !tr.Status.StartTime.IsZero() +} + +// IsSuccessful returns true if the TaskRun's status indicates that it is done. +func (tr *TaskRun) IsSuccessful() bool { + return tr != nil && tr.Status.GetCondition(apis.ConditionSucceeded).IsTrue() +} + +// IsCancelled returns true if the TaskRun's spec status is set to Cancelled state +func (tr *TaskRun) IsCancelled() bool { + return tr.Spec.Status == TaskRunSpecStatusCancelled +} + +// HasTimedOut returns true if the TaskRun runtime is beyond the allowed timeout +func (tr *TaskRun) HasTimedOut(ctx context.Context, c clock.PassiveClock) bool { + if tr.Status.StartTime.IsZero() { + return false + } + timeout := tr.GetTimeout(ctx) + // If timeout is set to 0 or defaulted to 0, there is no timeout. + if timeout == apisconfig.NoTimeoutDuration { + return false + } + runtime := c.Since(tr.Status.StartTime.Time) + return runtime > timeout +} + +// GetTimeout returns the timeout for the TaskRun, or the default if not specified +func (tr *TaskRun) GetTimeout(ctx context.Context) time.Duration { + // Use the platform default is no timeout is set + if tr.Spec.Timeout == nil { + defaultTimeout := time.Duration(config.FromContextOrDefaults(ctx).Defaults.DefaultTimeoutMinutes) + return defaultTimeout * time.Minute + } + return tr.Spec.Timeout.Duration +} + +// GetNamespacedName returns a k8s namespaced name that identifies this TaskRun +func (tr *TaskRun) GetNamespacedName() types.NamespacedName { + return types.NamespacedName{Namespace: tr.Namespace, Name: tr.Name} +} + +// IsPartOfPipeline return true if TaskRun is a part of a Pipeline. +// It also return the name of Pipeline and PipelineRun +func (tr *TaskRun) IsPartOfPipeline() (bool, string, string) { + if tr == nil || len(tr.Labels) == 0 { + return false, "", "" + } + + if pl, ok := tr.Labels[pipeline.PipelineLabelKey]; ok { + return true, pl, tr.Labels[pipeline.PipelineRunLabelKey] + } + + return false, "", "" +} + +// HasVolumeClaimTemplate returns true if TaskRun contains volumeClaimTemplates that is +// used for creating PersistentVolumeClaims with an OwnerReference for each run +func (tr *TaskRun) HasVolumeClaimTemplate() bool { + for _, ws := range tr.Spec.Workspaces { + if ws.VolumeClaimTemplate != nil { + return true + } + } + return false +} diff --git a/pkg/apis/pipeline/v1/taskrun_types_test.go b/pkg/apis/pipeline/v1/taskrun_types_test.go new file mode 100644 index 00000000000..92c2f8b160b --- /dev/null +++ b/pkg/apis/pipeline/v1/taskrun_types_test.go @@ -0,0 +1,361 @@ +/* +Copyright 2022 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 v1_test + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/pipeline" + "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" + "k8s.io/apimachinery/pkg/util/clock" + "knative.dev/pkg/apis" + duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" +) + +// TODO: issues#5111 related, shall be in v1_test +var now = time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) +var testClock = clock.NewFakePassiveClock(now) + +func TestTaskRun_GetPipelineRunPVCName(t *testing.T) { + tests := []struct { + name string + tr *v1beta1.TaskRun + expectedPVCName string + }{{ + name: "invalid owner reference", + tr: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{{ + Kind: "SomeOtherOwner", + Name: "testpr", + }}, + }, + }, + expectedPVCName: "", + }, { + name: "valid pipelinerun owner", + tr: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{{ + Kind: "PipelineRun", + Name: "testpr", + }}, + }, + }, + expectedPVCName: "testpr-pvc", + }, { + name: "nil taskrun", + expectedPVCName: "", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.tr.GetPipelineRunPVCName() != tt.expectedPVCName { + t.Fatalf("taskrun pipeline run pvc name mismatch: got %s ; expected %s", tt.tr.GetPipelineRunPVCName(), tt.expectedPVCName) + } + }) + } +} + +func TestTaskRun_HasPipelineRun(t *testing.T) { + tests := []struct { + name string + tr *v1beta1.TaskRun + want bool + }{{ + name: "invalid owner reference", + tr: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{{ + Kind: "SomeOtherOwner", + Name: "testpr", + }}, + }, + }, + want: false, + }, { + name: "valid pipelinerun owner", + tr: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{{ + Kind: "PipelineRun", + Name: "testpr", + }}, + }, + }, + want: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.tr.HasPipelineRunOwnerReference() != tt.want { + t.Fatalf("taskrun pipeline run pvc name mismatch: got %s ; expected %t", tt.tr.GetPipelineRunPVCName(), tt.want) + } + }) + } +} + +func TestTaskRunIsDone(t *testing.T) { + tr := &v1beta1.TaskRun{ + Status: v1beta1.TaskRunStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }}, + }, + }, + } + if !tr.IsDone() { + t.Fatal("Expected pipelinerun status to be done") + } +} + +func TestTaskRunIsCancelled(t *testing.T) { + tr := &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + Status: v1beta1.TaskRunSpecStatusCancelled, + }, + } + if !tr.IsCancelled() { + t.Fatal("Expected pipelinerun status to be cancelled") + } +} + +func TestTaskRunHasVolumeClaimTemplate(t *testing.T) { + tr := &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + Workspaces: []v1beta1.WorkspaceBinding{{ + Name: "my-workspace", + VolumeClaimTemplate: &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc", + }, + Spec: corev1.PersistentVolumeClaimSpec{}, + }, + }}, + }, + } + if !tr.HasVolumeClaimTemplate() { + t.Fatal("Expected taskrun to have a volumeClaimTemplate workspace") + } +} + +func TestTaskRunKey(t *testing.T) { + tr := &v1beta1.TaskRun{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "trunname"}} + n := tr.GetNamespacedName() + expected := "foo/trunname" + if n.String() != expected { + t.Fatalf("Expected name to be %s but got %s", expected, n.String()) + } +} + +func TestTaskRunHasStarted(t *testing.T) { + params := []struct { + name string + trStatus v1beta1.TaskRunStatus + expectedValue bool + }{{ + name: "trWithNoStartTime", + trStatus: v1beta1.TaskRunStatus{}, + expectedValue: false, + }, { + name: "trWithStartTime", + trStatus: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: now}, + }, + }, + expectedValue: true, + }, { + name: "trWithZeroStartTime", + trStatus: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + StartTime: &metav1.Time{}, + }, + }, + expectedValue: false, + }} + for _, tc := range params { + t.Run(tc.name, func(t *testing.T) { + tr := &v1beta1.TaskRun{} + tr.Status = tc.trStatus + if tr.HasStarted() != tc.expectedValue { + t.Fatalf("Expected taskrun HasStarted() to return %t but got %t", tc.expectedValue, tr.HasStarted()) + } + }) + } +} + +func TestTaskRunIsOfPipelinerun(t *testing.T) { + tests := []struct { + name string + tr *v1beta1.TaskRun + expectedValue bool + expetectedPipeline string + expetectedPipelineRun string + }{{ + name: "yes", + tr: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + pipeline.PipelineLabelKey: "pipeline", + pipeline.PipelineRunLabelKey: "pipelinerun", + }, + }, + }, + expectedValue: true, + expetectedPipeline: "pipeline", + expetectedPipelineRun: "pipelinerun", + }, { + name: "no", + tr: &v1beta1.TaskRun{}, + expectedValue: false, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + value, pipeline, pipelineRun := test.tr.IsPartOfPipeline() + if value != test.expectedValue { + t.Fatalf("Expecting %v got %v", test.expectedValue, value) + } + + if pipeline != test.expetectedPipeline { + t.Fatalf("Mismatch in pipeline: got %s expected %s", pipeline, test.expetectedPipeline) + } + + if pipelineRun != test.expetectedPipelineRun { + t.Fatalf("Mismatch in pipelinerun: got %s expected %s", pipelineRun, test.expetectedPipelineRun) + } + }) + } +} + +func TestHasTimedOut(t *testing.T) { + // IsZero reports whether t represents the zero time instant, January 1, year 1, 00:00:00 UTC + zeroTime := time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC) + testCases := []struct { + name string + taskRun *v1beta1.TaskRun + expectedStatus bool + }{{ + name: "TaskRun not started", + taskRun: &v1beta1.TaskRun{ + Status: v1beta1.TaskRunStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }}, + }, + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: zeroTime}, + }, + }, + }, + expectedStatus: false, + }, { + name: "TaskRun no timeout", + taskRun: &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + Timeout: &metav1.Duration{ + Duration: 0 * time.Minute, + }, + }, + Status: v1beta1.TaskRunStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }}, + }, + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: now.Add(-15 * time.Hour)}, + }, + }, + }, + expectedStatus: false, + }, { + name: "TaskRun timed out", + taskRun: &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + Timeout: &metav1.Duration{ + Duration: 10 * time.Second, + }, + }, + Status: v1beta1.TaskRunStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }}, + }, + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: now.Add(-15 * time.Second)}, + }, + }, + }, + expectedStatus: true, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := tc.taskRun.HasTimedOut(context.Background(), testClock) + if d := cmp.Diff(result, tc.expectedStatus); d != "" { + t.Fatalf(diff.PrintWantGot(d)) + } + }) + } +} + +func TestInitializeTaskRunConditions(t *testing.T) { + tr := &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Namespace: "test-ns", + }, + } + tr.Status.InitializeConditions() + + if tr.Status.StartTime.IsZero() { + t.Fatalf("TaskRun StartTime not initialized correctly") + } + + condition := tr.Status.GetCondition(apis.ConditionSucceeded) + if condition.Reason != v1beta1.TaskRunReasonStarted.String() { + t.Fatalf("TaskRun initialize reason should be %s, got %s instead", v1beta1.TaskRunReasonStarted.String(), condition.Reason) + } + + // Change the reason before we initialize again + tr.Status.SetCondition(&apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionUnknown, + Reason: "not just started", + Message: "hello", + }) + + tr.Status.InitializeConditions() + + newCondition := tr.Status.GetCondition(apis.ConditionSucceeded) + if newCondition.Reason != "not just started" { + t.Fatalf("PipelineRun initialize reset the condition reason to %s", newCondition.Reason) + } +} diff --git a/pkg/apis/pipeline/v1/taskrun_validation.go b/pkg/apis/pipeline/v1/taskrun_validation.go new file mode 100644 index 00000000000..9b09a7ba57e --- /dev/null +++ b/pkg/apis/pipeline/v1/taskrun_validation.go @@ -0,0 +1,189 @@ +/* +Copyright 2022 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 v1 + +import ( + "context" + "fmt" + "strings" + + "github.com/tektoncd/pipeline/pkg/apis/config" + "github.com/tektoncd/pipeline/pkg/apis/validate" + "github.com/tektoncd/pipeline/pkg/apis/version" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/sets" + "knative.dev/pkg/apis" +) + +var _ apis.Validatable = (*TaskRun)(nil) + +// Validate taskrun +func (tr *TaskRun) Validate(ctx context.Context) *apis.FieldError { + if apis.IsInDelete(ctx) { + return nil + } + errs := validate.ObjectMetadata(tr.GetObjectMeta()).ViaField("metadata") + return errs.Also(tr.Spec.Validate(apis.WithinSpec(ctx)).ViaField("spec")) +} + +// Validate taskrun spec +func (ts *TaskRunSpec) Validate(ctx context.Context) (errs *apis.FieldError) { + // Must have exactly one of taskRef and taskSpec. + if ts.TaskRef == nil && ts.TaskSpec == nil { + errs = errs.Also(apis.ErrMissingOneOf("taskRef", "taskSpec")) + } + if ts.TaskRef != nil && ts.TaskSpec != nil { + errs = errs.Also(apis.ErrMultipleOneOf("taskRef", "taskSpec")) + } + // Validate TaskRef if it's present. + if ts.TaskRef != nil { + errs = errs.Also(ts.TaskRef.Validate(ctx).ViaField("taskRef")) + } + // Validate TaskSpec if it's present. + if ts.TaskSpec != nil { + errs = errs.Also(ts.TaskSpec.Validate(ctx).ViaField("taskSpec")) + } + + errs = errs.Also(ValidateParameters(ctx, ts.Params).ViaField("params")) + errs = errs.Also(ValidateWorkspaceBindings(ctx, ts.Workspaces).ViaField("workspaces")) + if ts.Debug != nil { + errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "debug", config.AlphaAPIFields).ViaField("debug")) + errs = errs.Also(validateDebug(ts.Debug).ViaField("debug")) + } + if ts.StepOverrides != nil { + errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "stepOverrides", config.AlphaAPIFields).ViaField("stepOverrides")) + errs = errs.Also(validateStepOverrides(ts.StepOverrides).ViaField("stepOverrides")) + } + if ts.SidecarOverrides != nil { + errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "sidecarOverrides", config.AlphaAPIFields).ViaField("sidecarOverrides")) + errs = errs.Also(validateSidecarOverrides(ts.SidecarOverrides).ViaField("sidecarOverrides")) + } + if ts.ComputeResources != nil { + errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "computeResources", config.AlphaAPIFields).ViaField("computeResources")) + errs = errs.Also(validateTaskRunComputeResources(ts.ComputeResources, ts.StepOverrides)) + } + + if ts.Status != "" { + if ts.Status != TaskRunSpecStatusCancelled { + errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be %s", ts.Status, TaskRunSpecStatusCancelled), "status")) + } + } + if ts.Timeout != nil { + // timeout should be a valid duration of at least 0. + if ts.Timeout.Duration < 0 { + errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be >= 0", ts.Timeout.Duration.String()), "timeout")) + } + } + + return errs +} + +// validateDebug +func validateDebug(db *TaskRunDebug) (errs *apis.FieldError) { + breakpointOnFailure := "onFailure" + validBreakpoints := sets.NewString() + validBreakpoints.Insert(breakpointOnFailure) + + for _, b := range db.Breakpoint { + if !validBreakpoints.Has(b) { + errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s is not a valid breakpoint. Available valid breakpoints include %s", b, validBreakpoints.List()), "breakpoint")) + } + } + return errs +} + +// ValidateWorkspaceBindings makes sure the volumes provided for the Task's declared workspaces make sense. +func ValidateWorkspaceBindings(ctx context.Context, wb []WorkspaceBinding) (errs *apis.FieldError) { + var names []string + for idx, w := range wb { + names = append(names, w.Name) + errs = errs.Also(w.Validate(ctx).ViaIndex(idx)) + } + errs = errs.Also(validateNoDuplicateNames(names, true)) + return errs +} + +// ValidateParameters makes sure the params for the Task are valid. +func ValidateParameters(ctx context.Context, params []Param) (errs *apis.FieldError) { + var names []string + for _, p := range params { + if p.Value.Type == ParamTypeObject { + // Object type parameter is an alpha feature and will fail validation if it's used in a taskrun spec + // when the enable-api-fields feature gate is not "alpha". + errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "object type parameter", config.AlphaAPIFields)) + } + names = append(names, p.Name) + } + return errs.Also(validateNoDuplicateNames(names, false)) +} + +func validateStepOverrides(overrides []TaskRunStepOverride) (errs *apis.FieldError) { + var names []string + for i, o := range overrides { + if o.Name == "" { + errs = errs.Also(apis.ErrMissingField("name").ViaIndex(i)) + } else { + names = append(names, o.Name) + } + } + errs = errs.Also(validateNoDuplicateNames(names, true)) + return errs +} + +// validateTaskRunComputeResources ensures that compute resources are not configured at both the step level and the task level +func validateTaskRunComputeResources(computeResources *corev1.ResourceRequirements, overrides []TaskRunStepOverride) (errs *apis.FieldError) { + for _, override := range overrides { + if override.Resources.Size() != 0 && computeResources != nil { + return apis.ErrMultipleOneOf( + "stepOverrides.resources", + "computeResources", + ) + } + } + return nil +} + +func validateSidecarOverrides(overrides []TaskRunSidecarOverride) (errs *apis.FieldError) { + var names []string + for i, o := range overrides { + if o.Name == "" { + errs = errs.Also(apis.ErrMissingField("name").ViaIndex(i)) + } else { + names = append(names, o.Name) + } + } + errs = errs.Also(validateNoDuplicateNames(names, true)) + return errs +} + +// validateNoDuplicateNames returns an error for each name that is repeated in names. +// Case insensitive. +// If byIndex is true, the error will be reported by index instead of by key. +func validateNoDuplicateNames(names []string, byIndex bool) (errs *apis.FieldError) { + seen := sets.NewString() + for i, n := range names { + if seen.Has(strings.ToLower(n)) { + if byIndex { + errs = errs.Also(apis.ErrMultipleOneOf("name").ViaIndex(i)) + } else { + errs = errs.Also(apis.ErrMultipleOneOf("name").ViaKey(n)) + } + } + seen.Insert(strings.ToLower(n)) + } + return errs +} diff --git a/pkg/apis/pipeline/v1/taskrun_validation_test.go b/pkg/apis/pipeline/v1/taskrun_validation_test.go new file mode 100644 index 00000000000..f431f38f22a --- /dev/null +++ b/pkg/apis/pipeline/v1/taskrun_validation_test.go @@ -0,0 +1,504 @@ +/* +Copyright 2022 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 v1_test + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/config" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/test/diff" + corev1 "k8s.io/api/core/v1" + corev1resources "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/apis" +) + +func TestTaskRun_Invalidate(t *testing.T) { + tests := []struct { + name string + taskRun *v1beta1.TaskRun + want *apis.FieldError + }{{ + name: "invalid taskspec", + taskRun: &v1beta1.TaskRun{}, + want: apis.ErrMissingOneOf("spec.taskRef", "spec.taskSpec").Also( + apis.ErrGeneric(`invalid resource name "": must be a valid DNS label`, "metadata.name")), + }} + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + err := ts.taskRun.Validate(context.Background()) + if d := cmp.Diff(ts.want.Error(), err.Error()); d != "" { + t.Error(diff.PrintWantGot(d)) + } + }) + } +} + +func TestTaskRun_Validate(t *testing.T) { + tests := []struct { + name string + taskRun *v1beta1.TaskRun + wc func(context.Context) context.Context + }{{ + name: "do not validate spec on delete", + taskRun: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: "taskrname"}, + }, + wc: apis.WithinDelete, + }, { + name: "alpha feature: valid step and sidecar overrides", + taskRun: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: "tr"}, + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "task"}, + StepOverrides: []v1beta1.TaskRunStepOverride{{ + Name: "foo", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceMemory: corev1resources.MustParse("1Gi")}, + }, + }}, + SidecarOverrides: []v1beta1.TaskRunSidecarOverride{{ + Name: "bar", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceMemory: corev1resources.MustParse("1Gi")}, + }, + }}, + }, + }, + wc: config.EnableAlphaAPIFields, + }} + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + ctx := context.Background() + if ts.wc != nil { + ctx = ts.wc(ctx) + } + if err := ts.taskRun.Validate(ctx); err != nil { + t.Errorf("TaskRun.Validate() error = %v", err) + } + }) + } +} + +func TestTaskRun_Workspaces_Invalid(t *testing.T) { + tests := []struct { + name string + tr *v1beta1.TaskRun + wantErr *apis.FieldError + }{{ + name: "make sure WorkspaceBinding validation invoked", + tr: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: "taskname"}, + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "task"}, + Workspaces: []v1beta1.WorkspaceBinding{{ + Name: "workspace", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{}, + }}, + }, + }, + wantErr: apis.ErrMissingField("spec.workspaces[0].persistentvolumeclaim.claimname"), + }, { + name: "bind same workspace twice", + tr: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: "taskname"}, + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "task"}, + Workspaces: []v1beta1.WorkspaceBinding{{ + Name: "workspace", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, { + Name: "workspace", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}, + }, + }, + wantErr: apis.ErrMultipleOneOf("spec.workspaces[1].name"), + }} + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + err := ts.tr.Validate(context.Background()) + if err == nil { + t.Errorf("Expected error for invalid TaskRun but got none") + } + if d := cmp.Diff(ts.wantErr.Error(), err.Error()); d != "" { + t.Error(diff.PrintWantGot(d)) + } + }) + } +} + +func TestTaskRunSpec_Invalidate(t *testing.T) { + tests := []struct { + name string + spec v1beta1.TaskRunSpec + wantErr *apis.FieldError + wc func(context.Context) context.Context + }{{ + name: "invalid taskspec", + spec: v1beta1.TaskRunSpec{}, + wantErr: apis.ErrMissingOneOf("taskRef", "taskSpec"), + }, { + name: "invalid taskref and taskspec together", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{ + Name: "taskrefname", + }, + TaskSpec: &v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Name: "mystep", + Image: "myimage", + }}, + }, + }, + wantErr: apis.ErrMultipleOneOf("taskRef", "taskSpec"), + }, { + name: "negative pipeline timeout", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{ + Name: "taskrefname", + }, + Timeout: &metav1.Duration{Duration: -48 * time.Hour}, + }, + wantErr: apis.ErrInvalidValue("-48h0m0s should be >= 0", "timeout"), + }, { + name: "wrong taskrun cancel", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{ + Name: "taskrefname", + }, + Status: "TaskRunCancell", + }, + wantErr: apis.ErrInvalidValue("TaskRunCancell should be TaskRunCancelled", "status"), + }, { + name: "invalid taskspec", + spec: v1beta1.TaskRunSpec{ + TaskSpec: &v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Name: "invalid-name-with-$weird-char/%", + Image: "myimage", + }}, + }, + }, + wantErr: &apis.FieldError{ + Message: `invalid value "invalid-name-with-$weird-char/%"`, + Paths: []string{"taskSpec.steps[0].name"}, + Details: "Task step name must be a valid DNS Label, For more info refer to https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + }, + }, { + name: "invalid params - exactly same names", + spec: v1beta1.TaskRunSpec{ + Params: []v1beta1.Param{{ + Name: "myname", + Value: *v1beta1.NewArrayOrString("value"), + }, { + Name: "myname", + Value: *v1beta1.NewArrayOrString("value"), + }}, + TaskRef: &v1beta1.TaskRef{Name: "mytask"}, + }, + wantErr: apis.ErrMultipleOneOf("params[myname].name"), + }, { + name: "invalid params - same names but different case", + spec: v1beta1.TaskRunSpec{ + Params: []v1beta1.Param{{ + Name: "FOO", + Value: *v1beta1.NewArrayOrString("value"), + }, { + Name: "foo", + Value: *v1beta1.NewArrayOrString("value"), + }}, + TaskRef: &v1beta1.TaskRef{Name: "mytask"}, + }, + wantErr: apis.ErrMultipleOneOf("params[foo].name"), + }, { + name: "invalid params (object type) - same names but different case", + spec: v1beta1.TaskRunSpec{ + Params: []v1beta1.Param{{ + Name: "MYOBJECTPARAM", + Value: *v1beta1.NewObject(map[string]string{"key1": "val1", "key2": "val2"}), + }, { + Name: "myobjectparam", + Value: *v1beta1.NewObject(map[string]string{"key1": "val1", "key2": "val2"}), + }}, + TaskRef: &v1beta1.TaskRef{Name: "mytask"}, + }, + wantErr: apis.ErrMultipleOneOf("params[myobjectparam].name"), + wc: config.EnableAlphaAPIFields, + }, { + name: "using debug when apifields stable", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{ + Name: "my-task", + }, + Debug: &v1beta1.TaskRunDebug{ + Breakpoint: []string{"onFailure"}, + }, + }, + wantErr: apis.ErrGeneric("debug requires \"enable-api-fields\" feature gate to be \"alpha\" but it is \"stable\""), + }, { + name: "invalid breakpoint", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{ + Name: "my-task", + }, + Debug: &v1beta1.TaskRunDebug{ + Breakpoint: []string{"breakito"}, + }, + }, + wantErr: apis.ErrInvalidValue("breakito is not a valid breakpoint. Available valid breakpoints include [onFailure]", "debug.breakpoint"), + wc: config.EnableAlphaAPIFields, + }, { + name: "stepOverride disallowed without alpha feature gate", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{ + Name: "foo", + }, + StepOverrides: []v1beta1.TaskRunStepOverride{{ + Name: "foo", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceMemory: corev1resources.MustParse("1Gi")}, + }, + }}, + }, + wantErr: apis.ErrGeneric("stepOverrides requires \"enable-api-fields\" feature gate to be \"alpha\" but it is \"stable\""), + }, { + name: "sidecarOverride disallowed without alpha feature gate", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{ + Name: "foo", + }, + SidecarOverrides: []v1beta1.TaskRunSidecarOverride{{ + Name: "foo", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceMemory: corev1resources.MustParse("1Gi")}, + }, + }}, + }, + wantErr: apis.ErrGeneric("sidecarOverrides requires \"enable-api-fields\" feature gate to be \"alpha\" but it is \"stable\""), + }, { + name: "duplicate stepOverride names", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "task"}, + StepOverrides: []v1beta1.TaskRunStepOverride{{ + Name: "foo", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceMemory: corev1resources.MustParse("1Gi")}, + }, + }, { + Name: "foo", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceMemory: corev1resources.MustParse("1Gi")}, + }, + }}, + }, + wantErr: apis.ErrMultipleOneOf("stepOverrides[1].name"), + wc: config.EnableAlphaAPIFields, + }, { + name: "missing stepOverride names", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "task"}, + StepOverrides: []v1beta1.TaskRunStepOverride{{ + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceMemory: corev1resources.MustParse("1Gi")}, + }, + }}, + }, + wantErr: apis.ErrMissingField("stepOverrides[0].name"), + wc: config.EnableAlphaAPIFields, + }, { + name: "duplicate sidecarOverride names", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "task"}, + SidecarOverrides: []v1beta1.TaskRunSidecarOverride{{ + Name: "bar", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceMemory: corev1resources.MustParse("1Gi")}, + }, + }, { + Name: "bar", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceMemory: corev1resources.MustParse("1Gi")}, + }, + }}, + }, + wantErr: apis.ErrMultipleOneOf("sidecarOverrides[1].name"), + wc: config.EnableAlphaAPIFields, + }, { + name: "missing sidecarOverride names", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "task"}, + SidecarOverrides: []v1beta1.TaskRunSidecarOverride{{ + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceMemory: corev1resources.MustParse("1Gi")}, + }, + }}, + }, + wantErr: apis.ErrMissingField("sidecarOverrides[0].name"), + wc: config.EnableAlphaAPIFields, + }, { + name: "invalid both step-level (stepOverrides.resources) and task-level (spec.computeResources) resource requirements", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "task"}, + StepOverrides: []v1beta1.TaskRunStepOverride{{ + Name: "stepOverride", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: corev1resources.MustParse("1Gi"), + }, + }, + }}, + ComputeResources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: corev1resources.MustParse("2Gi"), + }, + }, + }, + wantErr: apis.ErrMultipleOneOf( + "stepOverrides.resources", + "computeResources", + ), + wc: config.EnableAlphaAPIFields, + }, { + name: "computeResources disallowed without alpha feature gate", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{ + Name: "foo", + }, + ComputeResources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: corev1resources.MustParse("2Gi"), + }, + }, + }, + wantErr: apis.ErrGeneric("computeResources requires \"enable-api-fields\" feature gate to be \"alpha\" but it is \"stable\""), + }} + + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + ctx := context.Background() + if ts.wc != nil { + ctx = ts.wc(ctx) + } + err := ts.spec.Validate(ctx) + if d := cmp.Diff(ts.wantErr.Error(), err.Error()); d != "" { + t.Error(diff.PrintWantGot(d)) + } + }) + } +} + +func TestTaskRunSpec_Validate(t *testing.T) { + tests := []struct { + name string + spec v1beta1.TaskRunSpec + wc func(context.Context) context.Context + }{{ + name: "taskspec without a taskRef", + spec: v1beta1.TaskRunSpec{ + TaskSpec: &v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Name: "mystep", + Image: "myimage", + }}, + }, + }, + }, { + name: "no timeout", + spec: v1beta1.TaskRunSpec{ + Timeout: &metav1.Duration{Duration: 0}, + TaskSpec: &v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Name: "mystep", + Image: "myimage", + }}, + }, + }, + }, { + name: "parameters", + spec: v1beta1.TaskRunSpec{ + Timeout: &metav1.Duration{Duration: 0}, + Params: []v1beta1.Param{{ + Name: "name", + Value: *v1beta1.NewArrayOrString("value"), + }}, + TaskSpec: &v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Name: "mystep", + Image: "myimage", + }}, + }, + }, + }, { + name: "task spec with credentials.path variable", + spec: v1beta1.TaskRunSpec{ + TaskSpec: &v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Name: "mystep", + Image: "myimage", + Script: `echo "creds-init writes to $(credentials.path)"`, + }}, + }, + }, + }, { + name: "valid task-level (spec.resources) resource requirements", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "task"}, + ComputeResources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: corev1resources.MustParse("2Gi"), + }, + }, + }, + wc: config.EnableAlphaAPIFields, + }, { + name: "valid sidecar and task-level (spec.resources) resource requirements", + spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{Name: "task"}, + ComputeResources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: corev1resources.MustParse("2Gi"), + }, + }, + SidecarOverrides: []v1beta1.TaskRunSidecarOverride{{ + Name: "sidecar", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: corev1resources.MustParse("4Gi"), + }, + }, + }}, + }, + wc: config.EnableAlphaAPIFields, + }} + + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + ctx := context.Background() + if ts.wc != nil { + ctx = ts.wc(ctx) + } + if err := ts.spec.Validate(ctx); err != nil { + t.Error(err) + } + }) + } +}