diff --git a/go.mod b/go.mod index 531c04d0a94..35381bb4612 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.13 require ( cloud.google.com/go v0.47.0 // indirect - cloud.google.com/go/storage v1.0.0 contrib.go.opencensus.io/exporter/prometheus v0.1.0 // indirect contrib.go.opencensus.io/exporter/stackdriver v0.12.8 // indirect github.com/Azure/azure-sdk-for-go v36.1.0+incompatible // indirect @@ -52,7 +51,7 @@ require ( golang.org/x/sys v0.0.0-20191119060738-e882bf8e40c2 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect golang.org/x/tools v0.0.0-20191118222007-07fc4c7f2b98 // indirect - google.golang.org/api v0.10.0 + google.golang.org/api v0.10.0 // indirect google.golang.org/appengine v1.6.5 // indirect google.golang.org/grpc v1.24.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/apis/pipeline/v1alpha1/workspace_types.go b/pkg/apis/pipeline/v1alpha1/workspace_types.go index a10fa6483b4..01b80bcc0cc 100644 --- a/pkg/apis/pipeline/v1alpha1/workspace_types.go +++ b/pkg/apis/pipeline/v1alpha1/workspace_types.go @@ -17,52 +17,12 @@ limitations under the License. package v1alpha1 import ( - "path/filepath" - - "github.com/tektoncd/pipeline/pkg/apis/pipeline" - corev1 "k8s.io/api/core/v1" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha2" ) // WorkspaceDeclaration is a declaration of a volume that a Task requires. -type WorkspaceDeclaration struct { - // Name is the name by which you can bind the volume at runtime. - Name string `json:"name"` - // Description is an optional human readable description of this volume. - // +optional - Description string `json:"description,omitempty"` - // MountPath overrides the directory that the volume will be made available at. - // +optional - MountPath string `json:"mountPath,omitempty"` - // ReadOnly dictates whether a mounted volume is writable. By default this - // field is false and so mounted volumes are writable. - ReadOnly bool `json:"readOnly,omitempty"` -} - -// GetMountPath returns the mountPath for w which is the MountPath if provided or the -// default if not. -func (w *WorkspaceDeclaration) GetMountPath() string { - if w.MountPath != "" { - return w.MountPath - } - return filepath.Join(pipeline.WorkspaceDir, w.Name) -} +type WorkspaceDeclaration = v1alpha2.WorkspaceDeclaration // WorkspaceBinding maps a Task's declared workspace to a Volume. // Currently we only support PersistentVolumeClaims and EmptyDir. -type WorkspaceBinding struct { - // Name is the name of the workspace populated by the volume. - Name string `json:"name"` - // SubPath is optionally a directory on the volume which should be used - // for this binding (i.e. the volume will be mounted at this sub directory). - // +optional - SubPath string `json:"subPath,omitempty"` - // PersistentVolumeClaimVolumeSource represents a reference to a - // PersistentVolumeClaim in the same namespace. Either this OR EmptyDir can be used. - // +optional - PersistentVolumeClaim *corev1.PersistentVolumeClaimVolumeSource `json:"persistentVolumeClaim,omitempty"` - // EmptyDir represents a temporary directory that shares a Task's lifetime. - // More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir - // Either this OR PersistentVolumeClaim can be used. - // +optional - EmptyDir *corev1.EmptyDirVolumeSource `json:"emptyDir,omitempty"` -} +type WorkspaceBinding = v1alpha2.WorkspaceBinding diff --git a/pkg/apis/pipeline/v1alpha1/workspace_validation.go b/pkg/apis/pipeline/v1alpha1/workspace_validation.go index 1ca6fe0527d..7536360d31d 100644 --- a/pkg/apis/pipeline/v1alpha1/workspace_validation.go +++ b/pkg/apis/pipeline/v1alpha1/workspace_validation.go @@ -15,35 +15,3 @@ limitations under the License. */ package v1alpha1 - -import ( - "context" - - "k8s.io/apimachinery/pkg/api/equality" - "knative.dev/pkg/apis" -) - -// Validate looks at the Volume provided in wb and makes sure that it is valid. -// This means that only one VolumeSource can be specified, and also that the -// supported VolumeSource is itself valid. -func (b *WorkspaceBinding) Validate(ctx context.Context) *apis.FieldError { - if equality.Semantic.DeepEqual(b, &WorkspaceBinding{}) || b == nil { - return apis.ErrMissingField(apis.CurrentField) - } - - // Users should only provide one supported VolumeSource. - if b.PersistentVolumeClaim != nil && b.EmptyDir != nil { - return apis.ErrMultipleOneOf("workspace.persistentvolumeclaim", "workspace.emptydir") - } - - // Users must provide at least one supported VolumeSource. - if b.PersistentVolumeClaim == nil && b.EmptyDir == nil { - return apis.ErrMissingOneOf("workspace.persistentvolumeclaim", "workspace.emptydir") - } - - // For a PersistentVolumeClaim to work, you must at least provide the name of the PVC to use. - if b.PersistentVolumeClaim != nil && b.PersistentVolumeClaim.ClaimName == "" { - return apis.ErrMissingField("workspace.persistentvolumeclaim.claimname") - } - return nil -} diff --git a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go index d1398113c77..5cbbeeea111 100644 --- a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go @@ -1520,7 +1520,7 @@ func (in *TaskRunSpec) DeepCopyInto(out *TaskRunSpec) { in.PodTemplate.DeepCopyInto(&out.PodTemplate) if in.Workspaces != nil { in, out := &in.Workspaces, &out.Workspaces - *out = make([]WorkspaceBinding, len(*in)) + *out = make([]v1alpha2.WorkspaceBinding, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1652,7 +1652,7 @@ func (in *TaskSpec) DeepCopyInto(out *TaskSpec) { } if in.Workspaces != nil { in, out := &in.Workspaces, &out.Workspaces - *out = make([]WorkspaceDeclaration, len(*in)) + *out = make([]v1alpha2.WorkspaceDeclaration, len(*in)) copy(*out, *in) } return @@ -1683,45 +1683,3 @@ func (in *TestResult) DeepCopy() *TestResult { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WorkspaceBinding) DeepCopyInto(out *WorkspaceBinding) { - *out = *in - if in.PersistentVolumeClaim != nil { - in, out := &in.PersistentVolumeClaim, &out.PersistentVolumeClaim - *out = new(v1.PersistentVolumeClaimVolumeSource) - **out = **in - } - if in.EmptyDir != nil { - in, out := &in.EmptyDir, &out.EmptyDir - *out = new(v1.EmptyDirVolumeSource) - (*in).DeepCopyInto(*out) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceBinding. -func (in *WorkspaceBinding) DeepCopy() *WorkspaceBinding { - if in == nil { - return nil - } - out := new(WorkspaceBinding) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WorkspaceDeclaration) DeepCopyInto(out *WorkspaceDeclaration) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceDeclaration. -func (in *WorkspaceDeclaration) DeepCopy() *WorkspaceDeclaration { - if in == nil { - return nil - } - out := new(WorkspaceDeclaration) - in.DeepCopyInto(out) - return out -} diff --git a/pkg/apis/pipeline/v1alpha2/task_types.go b/pkg/apis/pipeline/v1alpha2/task_types.go index e5d96efd56a..022aff5741a 100644 --- a/pkg/apis/pipeline/v1alpha2/task_types.go +++ b/pkg/apis/pipeline/v1alpha2/task_types.go @@ -82,6 +82,9 @@ type TaskSpec struct { // Sidecars are run alongside the Task's step containers. They begin before // the steps start and end after the steps complete. Sidecars []corev1.Container `json:"sidecars,omitempty"` + + // Workspaces are the volumes that this Task requires. + Workspaces []WorkspaceDeclaration } // Step embeds the Container type, which allows it to include fields not diff --git a/pkg/apis/pipeline/v1alpha2/task_validation.go b/pkg/apis/pipeline/v1alpha2/task_validation.go index 9be36140852..f916fa6eeb9 100644 --- a/pkg/apis/pipeline/v1alpha2/task_validation.go +++ b/pkg/apis/pipeline/v1alpha2/task_validation.go @@ -19,6 +19,7 @@ package v1alpha2 import ( "context" "fmt" + "path/filepath" "strings" "github.com/tektoncd/pipeline/pkg/apis/validate" @@ -49,6 +50,9 @@ func (ts *TaskSpec) Validate(ctx context.Context) *apis.FieldError { if err := ValidateVolumes(ts.Volumes).ViaField("volumes"); err != nil { return err } + if err := ValidateDeclaredWorkspaces(ts.Workspaces, ts.Steps, ts.StepTemplate); err != nil { + return err + } mergedSteps, err := MergeStepsWithStepTemplate(ts.StepTemplate, ts.Steps) if err != nil { return &apis.FieldError{ @@ -90,6 +94,44 @@ func (ts *TaskSpec) Validate(ctx context.Context) *apis.FieldError { return nil } +// a mount path which conflicts with any other declared workspaces, with the explicitly +// declared volume mounts, or with the stepTemplate. The names must also be unique. +func ValidateDeclaredWorkspaces(workspaces []WorkspaceDeclaration, steps []Step, stepTemplate *corev1.Container) *apis.FieldError { + mountPaths := map[string]struct{}{} + for _, step := range steps { + for _, vm := range step.VolumeMounts { + mountPaths[filepath.Clean(vm.MountPath)] = struct{}{} + } + } + if stepTemplate != nil { + for _, vm := range stepTemplate.VolumeMounts { + mountPaths[filepath.Clean(vm.MountPath)] = struct{}{} + } + } + + wsNames := map[string]struct{}{} + for _, w := range workspaces { + // Workspace names must be unique + if _, ok := wsNames[w.Name]; ok { + return &apis.FieldError{ + Message: fmt.Sprintf("workspace name %q must be unique", w.Name), + Paths: []string{"workspaces.name"}, + } + } + wsNames[w.Name] = struct{}{} + // Workspaces must not try to use mount paths that are already used + mountPath := filepath.Clean(w.GetMountPath()) + if _, ok := mountPaths[mountPath]; ok { + return &apis.FieldError{ + Message: fmt.Sprintf("workspace mount path %q must be unique", mountPath), + Paths: []string{"workspaces.mountpath"}, + } + } + mountPaths[mountPath] = struct{}{} + } + return nil +} + func ValidateVolumes(volumes []corev1.Volume) *apis.FieldError { // Task must not have duplicate volume names. vols := map[string]struct{}{} @@ -108,33 +150,42 @@ func ValidateVolumes(volumes []corev1.Volume) *apis.FieldError { func validateSteps(steps []Step) *apis.FieldError { // Task must not have duplicate step names. names := map[string]struct{}{} - for _, s := range steps { + for idx, s := range steps { if s.Image == "" { return apis.ErrMissingField("Image") } if s.Script != "" { - if len(s.Args) > 0 || len(s.Command) > 0 { - return &apis.FieldError{ - Message: "script cannot be used with args or command", - Paths: []string{"script"}, - } - } - if !strings.HasPrefix(strings.TrimSpace(s.Script), "#!") { + if len(s.Command) > 0 { return &apis.FieldError{ - Message: "script must start with a shebang (#!)", + Message: fmt.Sprintf("step %d script cannot be used with command", idx), Paths: []string{"script"}, } } } - if s.Name == "" { - continue + if s.Name != "" { + if _, ok := names[s.Name]; ok { + return apis.ErrInvalidValue(s.Name, "name") + } + names[s.Name] = struct{}{} } - if _, ok := names[s.Name]; ok { - return apis.ErrInvalidValue(s.Name, "name") + + for _, vm := range s.VolumeMounts { + if strings.HasPrefix(vm.MountPath, "/tekton/") && + !strings.HasPrefix(vm.MountPath, "/tekton/home") { + return &apis.FieldError{ + Message: fmt.Sprintf("step %d volumeMount cannot be mounted under /tekton/ (volumeMount %q mounted at %q)", idx, vm.Name, vm.MountPath), + Paths: []string{"volumeMounts.mountPath"}, + } + } + if strings.HasPrefix(vm.Name, "tekton-internal-") { + return &apis.FieldError{ + Message: fmt.Sprintf(`step %d volumeMount name %q cannot start with "tekton-internal-"`, idx, vm.Name), + Paths: []string{"volumeMounts.name"}, + } + } } - names[s.Name] = struct{}{} } return nil } diff --git a/pkg/apis/pipeline/v1alpha2/task_validation_test.go b/pkg/apis/pipeline/v1alpha2/task_validation_test.go index 5bc9e58c582..9273d2969d6 100644 --- a/pkg/apis/pipeline/v1alpha2/task_validation_test.go +++ b/pkg/apis/pipeline/v1alpha2/task_validation_test.go @@ -58,6 +58,7 @@ func TestTaskSpecValidate(t *testing.T) { Resources *v1alpha2.TaskResources Steps []v1alpha2.Step StepTemplate *corev1.Container + Workspaces []v1alpha2.WorkspaceDeclaration } tests := []struct { name string @@ -165,6 +166,45 @@ func TestTaskSpecValidate(t *testing.T) { hello world`, }}, }, + }, { + name: "valid step with script and args", + fields: fields{ + Steps: []v1alpha2.Step{{ + Container: corev1.Container{ + Image: "my-image", + Args: []string{"arg"}, + }, + Script: ` + #!/usr/bin/env bash + hello $1`, + }}, + }, + }, { + name: "valid step with volumeMount under /tekton/home", + fields: fields{ + Steps: []v1alpha2.Step{{Container: corev1.Container{ + Image: "myimage", + VolumeMounts: []corev1.VolumeMount{{ + Name: "foo", + MountPath: "/tekton/home", + }}, + }}}, + }, + }, { + name: "valid workspace", + fields: fields{ + Steps: []v1alpha2.Step{{ + Container: corev1.Container{ + Image: "my-image", + Args: []string{"arg"}, + }, + }}, + Workspaces: []v1alpha2.WorkspaceDeclaration{{ + Name: "foo-workspace", + Description: "my great workspace", + MountPath: "some/path", + }}, + }, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -173,6 +213,7 @@ func TestTaskSpecValidate(t *testing.T) { Resources: tt.fields.Resources, Steps: tt.fields.Steps, StepTemplate: tt.fields.StepTemplate, + Workspaces: tt.fields.Workspaces, } ctx := context.Background() ts.SetDefaults(ctx) @@ -185,10 +226,12 @@ func TestTaskSpecValidate(t *testing.T) { func TestTaskSpecValidateError(t *testing.T) { type fields struct { - Params []v1alpha2.ParamSpec - Resources *v1alpha2.TaskResources - Steps []v1alpha2.Step - Volumes []corev1.Volume + Params []v1alpha2.ParamSpec + Resources *v1alpha2.TaskResources + Steps []v1alpha2.Step + Volumes []corev1.Volume + StepTemplate *corev1.Container + Workspaces []v1alpha2.WorkspaceDeclaration } tests := []struct { name string @@ -505,57 +548,170 @@ func TestTaskSpecValidateError(t *testing.T) { Paths: []string{"volumes.name"}, }, }, { - name: "step with script and args", + name: "step with script and command", fields: fields{ Steps: []v1alpha2.Step{{ Container: corev1.Container{ - Image: "myimage", - Args: []string{"arg"}, + Image: "myimage", + Command: []string{"command"}, }, Script: "script", }}, }, expectedError: apis.FieldError{ - Message: "script cannot be used with args or command", + Message: "step 0 script cannot be used with command", Paths: []string{"steps.script"}, }, }, { - name: "step with script without shebang", + name: "step volume mounts under /tekton/", + fields: fields{ + Steps: []v1alpha2.Step{{Container: corev1.Container{ + Image: "myimage", + VolumeMounts: []corev1.VolumeMount{{ + Name: "foo", + MountPath: "/tekton/foo", + }}, + }}}, + }, + expectedError: apis.FieldError{ + Message: `step 0 volumeMount cannot be mounted under /tekton/ (volumeMount "foo" mounted at "/tekton/foo")`, + Paths: []string{"steps.volumeMounts.mountPath"}, + }, + }, { + name: "step volume mount name starts with tekton-internal-", + fields: fields{ + Steps: []v1alpha2.Step{{Container: corev1.Container{ + Image: "myimage", + VolumeMounts: []corev1.VolumeMount{{ + Name: "tekton-internal-foo", + MountPath: "/this/is/fine", + }}, + }}}, + }, + expectedError: apis.FieldError{ + Message: `step 0 volumeMount name "tekton-internal-foo" cannot start with "tekton-internal-"`, + Paths: []string{"steps.volumeMounts.name"}, + }, + }, { + name: "declared workspaces names are not unique", + fields: fields{ + Steps: validSteps, + Workspaces: []v1alpha2.WorkspaceDeclaration{{ + Name: "same-workspace", + }, { + Name: "same-workspace", + }}, + }, + expectedError: apis.FieldError{ + Message: "workspace name \"same-workspace\" must be unique", + Paths: []string{"workspaces.name"}, + }, + }, { + name: "declared workspaces clash with each other", + fields: fields{ + Steps: validSteps, + Workspaces: []v1alpha2.WorkspaceDeclaration{{ + Name: "some-workspace", + MountPath: "/foo", + }, { + Name: "another-workspace", + MountPath: "/foo", + }}, + }, + expectedError: apis.FieldError{ + Message: "workspace mount path \"/foo\" must be unique", + Paths: []string{"workspaces.mountpath"}, + }, + }, { + name: "workspace mount path already in volumeMounts", fields: fields{ Steps: []v1alpha2.Step{{ Container: corev1.Container{ - Image: "my-image", + Image: "myimage", + Command: []string{"command"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "my-mount", + MountPath: "/foo", + }}, }, - Script: "does not begin with shebang", + }}, + Workspaces: []v1alpha2.WorkspaceDeclaration{{ + Name: "some-workspace", + MountPath: "/foo", }}, }, expectedError: apis.FieldError{ - Message: "script must start with a shebang (#!)", - Paths: []string{"steps.script"}, + Message: "workspace mount path \"/foo\" must be unique", + Paths: []string{"workspaces.mountpath"}, }, }, { - name: "step with script and command", + name: "workspace default mount path already in volumeMounts", fields: fields{ Steps: []v1alpha2.Step{{ Container: corev1.Container{ Image: "myimage", Command: []string{"command"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "my-mount", + MountPath: "/workspace/some-workspace/", + }}, }, - Script: "script", + }}, + Workspaces: []v1alpha2.WorkspaceDeclaration{{ + Name: "some-workspace", }}, }, expectedError: apis.FieldError{ - Message: "script cannot be used with args or command", - Paths: []string{"steps.script"}, + Message: "workspace mount path \"/workspace/some-workspace\" must be unique", + Paths: []string{"workspaces.mountpath"}, + }, + }, { + name: "workspace mount path already in stepTemplate", + fields: fields{ + StepTemplate: &corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "my-mount", + MountPath: "/foo", + }}, + }, + Steps: validSteps, + Workspaces: []v1alpha2.WorkspaceDeclaration{{ + Name: "some-workspace", + MountPath: "/foo", + }}, + }, + expectedError: apis.FieldError{ + Message: "workspace mount path \"/foo\" must be unique", + Paths: []string{"workspaces.mountpath"}, + }, + }, { + name: "workspace default mount path already in stepTemplate", + fields: fields{ + StepTemplate: &corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "my-mount", + MountPath: "/workspace/some-workspace", + }}, + }, + Steps: validSteps, + Workspaces: []v1alpha2.WorkspaceDeclaration{{ + Name: "some-workspace", + }}, + }, + expectedError: apis.FieldError{ + Message: "workspace mount path \"/workspace/some-workspace\" must be unique", + Paths: []string{"workspaces.mountpath"}, }, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ts := &v1alpha2.TaskSpec{ - Params: tt.fields.Params, - Resources: tt.fields.Resources, - Steps: tt.fields.Steps, - Volumes: tt.fields.Volumes, + Params: tt.fields.Params, + Resources: tt.fields.Resources, + Steps: tt.fields.Steps, + Volumes: tt.fields.Volumes, + StepTemplate: tt.fields.StepTemplate, + Workspaces: tt.fields.Workspaces, } ctx := context.Background() ts.SetDefaults(ctx) diff --git a/pkg/apis/pipeline/v1alpha2/workspace_types.go b/pkg/apis/pipeline/v1alpha2/workspace_types.go new file mode 100644 index 00000000000..b211a7f3034 --- /dev/null +++ b/pkg/apis/pipeline/v1alpha2/workspace_types.go @@ -0,0 +1,68 @@ +/* +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 v1alpha2 + +import ( + "path/filepath" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline" + corev1 "k8s.io/api/core/v1" +) + +// WorkspaceDeclaration is a declaration of a volume that a Task requires. +type WorkspaceDeclaration struct { + // Name is the name by which you can bind the volume at runtime. + Name string `json:"name"` + // Description is an optional human readable description of this volume. + // +optional + Description string `json:"description,omitempty"` + // MountPath overrides the directory that the volume will be made available at. + // +optional + MountPath string `json:"mountPath,omitempty"` + // ReadOnly dictates whether a mounted volume is writable. By default this + // field is false and so mounted volumes are writable. + ReadOnly bool `json:"readOnly,omitempty"` +} + +// GetMountPath returns the mountPath for w which is the MountPath if provided or the +// default if not. +func (w *WorkspaceDeclaration) GetMountPath() string { + if w.MountPath != "" { + return w.MountPath + } + return filepath.Join(pipeline.WorkspaceDir, w.Name) +} + +// WorkspaceBinding maps a Task's declared workspace to a Volume. +// Currently we only support PersistentVolumeClaims and EmptyDir. +type WorkspaceBinding struct { + // Name is the name of the workspace populated by the volume. + Name string `json:"name"` + // SubPath is optionally a directory on the volume which should be used + // for this binding (i.e. the volume will be mounted at this sub directory). + // +optional + SubPath string `json:"subPath,omitempty"` + // PersistentVolumeClaimVolumeSource represents a reference to a + // PersistentVolumeClaim in the same namespace. Either this OR EmptyDir can be used. + // +optional + PersistentVolumeClaim *corev1.PersistentVolumeClaimVolumeSource `json:"persistentVolumeClaim,omitempty"` + // EmptyDir represents a temporary directory that shares a Task's lifetime. + // More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + // Either this OR PersistentVolumeClaim can be used. + // +optional + EmptyDir *corev1.EmptyDirVolumeSource `json:"emptyDir,omitempty"` +} diff --git a/pkg/apis/pipeline/v1alpha2/workspace_validation.go b/pkg/apis/pipeline/v1alpha2/workspace_validation.go new file mode 100644 index 00000000000..c90a9e6f0fc --- /dev/null +++ b/pkg/apis/pipeline/v1alpha2/workspace_validation.go @@ -0,0 +1,49 @@ +/* +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 v1alpha2 + +import ( + "context" + + "k8s.io/apimachinery/pkg/api/equality" + "knative.dev/pkg/apis" +) + +// Validate looks at the Volume provided in wb and makes sure that it is valid. +// This means that only one VolumeSource can be specified, and also that the +// supported VolumeSource is itself valid. +func (b *WorkspaceBinding) Validate(ctx context.Context) *apis.FieldError { + if equality.Semantic.DeepEqual(b, &WorkspaceBinding{}) || b == nil { + return apis.ErrMissingField(apis.CurrentField) + } + + // Users should only provide one supported VolumeSource. + if b.PersistentVolumeClaim != nil && b.EmptyDir != nil { + return apis.ErrMultipleOneOf("workspace.persistentvolumeclaim", "workspace.emptydir") + } + + // Users must provide at least one supported VolumeSource. + if b.PersistentVolumeClaim == nil && b.EmptyDir == nil { + return apis.ErrMissingOneOf("workspace.persistentvolumeclaim", "workspace.emptydir") + } + + // For a PersistentVolumeClaim to work, you must at least provide the name of the PVC to use. + if b.PersistentVolumeClaim != nil && b.PersistentVolumeClaim.ClaimName == "" { + return apis.ErrMissingField("workspace.persistentvolumeclaim.claimname") + } + return nil +} diff --git a/pkg/apis/pipeline/v1alpha2/workspace_validation_test.go b/pkg/apis/pipeline/v1alpha2/workspace_validation_test.go new file mode 100644 index 00000000000..e25638bf775 --- /dev/null +++ b/pkg/apis/pipeline/v1alpha2/workspace_validation_test.go @@ -0,0 +1,88 @@ +/* +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 v1alpha2 + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" +) + +func TestWorkspaceBindingValidateValid(t *testing.T) { + for _, tc := range []struct { + name string + binding *WorkspaceBinding + }{{ + name: "Valid PVC", + binding: &WorkspaceBinding{ + Name: "beth", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "pool-party", + }, + }, + }, { + name: "Valid emptyDir", + binding: &WorkspaceBinding{ + Name: "beth", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }} { + t.Run(tc.name, func(t *testing.T) { + if err := tc.binding.Validate(context.Background()); err != nil { + t.Errorf("didnt expect error for valid binding but got: %v", err) + } + }) + } + +} + +func TestWorkspaceBindingValidateInvalid(t *testing.T) { + for _, tc := range []struct { + name string + binding *WorkspaceBinding + }{{ + name: "no binding provided", + binding: nil, + }, { + name: "Provided both pvc and emptydir", + binding: &WorkspaceBinding{ + Name: "beth", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "pool-party", + }, + }, + }, { + name: "Provided neither pvc nor emptydir", + binding: &WorkspaceBinding{ + Name: "beth", + }, + }, { + name: "Provided pvc without claim name", + binding: &WorkspaceBinding{ + Name: "beth", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{}, + }, + }} { + t.Run(tc.name, func(t *testing.T) { + if err := tc.binding.Validate(context.Background()); err == nil { + t.Errorf("expected error for invalid binding but didn't get any!") + } + }) + } +} diff --git a/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go index cde39100423..38bb0f812e9 100644 --- a/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go @@ -282,6 +282,11 @@ func (in *TaskSpec) DeepCopyInto(out *TaskSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Workspaces != nil { + in, out := &in.Workspaces, &out.Workspaces + *out = make([]WorkspaceDeclaration, len(*in)) + copy(*out, *in) + } return } @@ -294,3 +299,45 @@ func (in *TaskSpec) DeepCopy() *TaskSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkspaceBinding) DeepCopyInto(out *WorkspaceBinding) { + *out = *in + if in.PersistentVolumeClaim != nil { + in, out := &in.PersistentVolumeClaim, &out.PersistentVolumeClaim + *out = new(v1.PersistentVolumeClaimVolumeSource) + **out = **in + } + if in.EmptyDir != nil { + in, out := &in.EmptyDir, &out.EmptyDir + *out = new(v1.EmptyDirVolumeSource) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceBinding. +func (in *WorkspaceBinding) DeepCopy() *WorkspaceBinding { + if in == nil { + return nil + } + out := new(WorkspaceBinding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkspaceDeclaration) DeepCopyInto(out *WorkspaceDeclaration) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceDeclaration. +func (in *WorkspaceDeclaration) DeepCopy() *WorkspaceDeclaration { + if in == nil { + return nil + } + out := new(WorkspaceDeclaration) + in.DeepCopyInto(out) + return out +}