From f9b302ede35fd7feca2c502e87126bc484975e5f Mon Sep 17 00:00:00 2001 From: Christie Wilson Date: Tue, 26 Nov 2019 14:55:00 -0500 Subject: [PATCH] Add workspace types for Task and TaskRun with validation This allows users to use Volumes with Tasks such that: - The actual volumes to use (or subdirectories on those volumes) are provided at runtime, not at Task authoring time - At Task authoring time you can declare that you expect a volume to be provided and control what path that volume should end up at - Validation will be provided that the volumes (workspaces) are actually provided at runtime Before this change, there were two ways to use Volumes with Tasks: - VolumeMounts were explicitly declared at the level of a step - Volumes were declared in Tasks, meaning the Task author controlled the name of the volume being used and it wasn't possible at runtime to use a subdir of the volume - Or the Volume could be provided via the podTemplate, if the user realized this was possible None of this was validated and could cause unexpected and hard to diagnose errors at runtime. We have also limited (at least initially) the types of volume source being supported instead of expanding to all volume sources, tho we can expand it later if we want to and if users need it. This would reduce the API surface that a Tekton compliant system would need to conform to (once we actually define what conformance means!). Part of #1438 In future commits we will add support for workspaces to Pipelines and PipelineRuns as well; for now if a user tries to use a Pipeline with a Task that requires a Workspace, it will fail at runtime because it is not (yet) possible for the Pipeline and PipelineRun to provide workspaces. Co-authored-by: Scott --- docs/pipelineruns.md | 16 +- docs/pipelines.md | 7 + docs/taskruns.md | 41 ++- docs/tasks.md | 64 +++- examples/taskruns/custom-volume.yaml | 7 +- examples/taskruns/workspace.yaml | 35 ++ pkg/apis/pipeline/v1alpha1/task_types.go | 3 + pkg/apis/pipeline/v1alpha1/taskrun_types.go | 5 + pkg/apis/pipeline/v1alpha1/workspace_types.go | 53 +++ .../pipeline/v1alpha1/workspace_validation.go | 48 +++ .../v1alpha1/workspace_validation_test.go | 88 +++++ .../v1alpha1/zz_generated.deepcopy.go | 54 +++ pkg/list/diff.go | 4 +- pkg/pod/pod.go | 13 +- pkg/pod/workingdir_init.go | 2 +- pkg/reconciler/taskrun/taskrun.go | 25 +- pkg/workspace/apply.go | 69 ++++ pkg/workspace/apply_test.go | 309 ++++++++++++++++++ pkg/workspace/validate.go | 50 +++ pkg/workspace/validate_test.go | 133 ++++++++ 20 files changed, 1003 insertions(+), 23 deletions(-) create mode 100644 examples/taskruns/workspace.yaml create mode 100644 pkg/apis/pipeline/v1alpha1/workspace_types.go create mode 100644 pkg/apis/pipeline/v1alpha1/workspace_validation.go create mode 100644 pkg/apis/pipeline/v1alpha1/workspace_validation_test.go create mode 100644 pkg/workspace/apply.go create mode 100644 pkg/workspace/apply_test.go create mode 100644 pkg/workspace/validate.go create mode 100644 pkg/workspace/validate_test.go diff --git a/docs/pipelineruns.md b/docs/pipelineruns.md index 573e461d09f..755a8d40de4 100644 --- a/docs/pipelineruns.md +++ b/docs/pipelineruns.md @@ -16,6 +16,7 @@ Creation of a `PipelineRun` will trigger the creation of - [Service account](#service-account) - [Service accounts](#service-accounts) - [Pod Template](#pod-template) + - [Workspaces](#workspaces) - [Cancelling a PipelineRun](#cancelling-a-pipelinerun) - [Examples](https://github.com/tektoncd/pipeline/tree/master/examples/pipelineruns) - [Logs](logs.md) @@ -27,7 +28,14 @@ following fields: - Required: - [`apiVersion`][kubernetes-overview] - Specifies the API version, for example - `tekton.dev/v1alpha1`. + `tekton.dev/v1alpha1`#### Workspace Substitution + +Paths to a `Task's` declared [workspaces](#workspaces) can be substituted with: + +``` +$(workspaces.myworkspace.path) +``` +. - [`kind`][kubernetes-overview] - Specify the `PipelineRun` resource object. - [`metadata`][kubernetes-overview] - Specifies data to uniquely identify the `PipelineRun` resource object, for example a `name`. @@ -265,6 +273,12 @@ spec: claimName: my-volume-claim ``` +## Workspaces + +It is not yet possible to specify [workspaces](tasks.md#workspaces) via `Pipelines` +or `PipelineRuns`, so `Tasks` requiring `workspaces` cannot be used with them until +[#1438](https://github.com/tektoncd/pipeline/issues/1438) is completed. + ## Cancelling a PipelineRun In order to cancel a running pipeline (`PipelineRun`), you need to update its diff --git a/docs/pipelines.md b/docs/pipelines.md index 62e2f9694aa..0807f9416c2 100644 --- a/docs/pipelines.md +++ b/docs/pipelines.md @@ -6,6 +6,7 @@ This document defines `Pipelines` and their capabilities. - [Syntax](#syntax) - [Declared resources](#declared-resources) + - [Workspaces][#declared-workspaces] - [Parameters](#parameters) - [Pipeline Tasks](#pipeline-tasks) - [From](#from) @@ -72,6 +73,12 @@ spec: type: image ``` +### Declared Workspaces + +It is not yet possible to specify [workspaces](tasks.md#workspaces) via `Pipelines` +or `PipelineRuns`, so `Tasks` requiring `workspaces` cannot be used with them until +[#1438](https://github.com/tektoncd/pipeline/issues/1438) is completed. + ### Parameters `Pipeline`s can declare input parameters that must be supplied to the `Pipeline` diff --git a/docs/taskruns.md b/docs/taskruns.md index 8896f23cc1a..38c7134cc17 100644 --- a/docs/taskruns.md +++ b/docs/taskruns.md @@ -18,6 +18,7 @@ A `TaskRun` runs until all `steps` have completed or until a failure occurs. - [Overriding where resources are copied from](#overriding-where-resources-are-copied-from) - [Service Account](#service-account) - [Pod Template](#pod-template) + - [Workspaces](#workspaces) - [Status](#status) - [Steps](#steps) - [Cancelling a TaskRun](#cancelling-a-taskrun) @@ -57,7 +58,9 @@ following fields: to configure the default timeout. - [`podTemplate`](#pod-template) - Specifies a subset of [`PodSpec`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#pod-v1-core) - configuration that will be used as the basis for the `Task` pod. + configuration that will be used as the basis for the `Task` pod. + - [`workspaces`](#workspaces) - Specify the actual volumes to use for the + [workspaces](tasks.md#workspaces) declared by a `Task` [kubernetes-overview]: https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#required-fields @@ -227,7 +230,43 @@ spec: claimName: my-volume-claim ``` +## Workspaces +For a `TaskRun` to execute [a `Task` that declares `workspaces`](tasks.md#workspaces), +at runtime you need to map the `workspaces` to actual physical volumes with +`workspaces`. Values in `workspaces` are +[`Volumes`](https://kubernetes.io/docs/tasks/configure-pod-container/configure-volume-storage/), however currently we only support a subset of `VolumeSources`: + +* [`emptyDir`](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) +* [`persistentVolumeClaim`](https://kubernetes.io/docs/concepts/storage/volumes/#persistentvolumeclaim) + +_If you need support for a `VolumeSource` not listed here +[please open an issue](https://github.com/tektoncd/pipeline/issues) or feel free to +[contribute a PR](https://github.com/tektoncd/pipeline/blob/master/CONTRIBUTING.md)._ + + +If the declared `workspaces` are not provided at runtime, the `TaskRun` will fail +with an error. + +For example to provide an existing PVC called `mypvc` for a `workspace` called +`myworkspace` declared by the `Pipeline`, using the `my-subdir` folder which already exists +on the PVC (there will be an error if it does not exist): + +```yaml +workspaces: +- name: myworkspace + persistentVolumeClaim: + claimName: mypvc + volumeSubPath: my-subdir +``` + +Or to use [`emptyDir`](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) for the same `workspace`: + +```yaml +workspaces: +- name: myworkspace + emptyDir: {} +``` ## Status diff --git a/docs/tasks.md b/docs/tasks.md index bf1bf5254da..d8e39dfdabb 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -19,11 +19,12 @@ entire Kubernetes cluster. - [Syntax](#syntax) - [Steps](#steps) - [Step script](#step-script) + - [Workspaces](#workspaces) - [Inputs](#inputs) - [Outputs](#outputs) - [Controlling where resources are mounted](#controlling-where-resources-are-mounted) - [Volumes](#volumes) - - [Container Template **deprecated**](#step-template) + - [Workspaces](#workspaces) - [Step Template](#step-template) - [Variable Substitution](#variable-substitution) - [Examples](#examples) @@ -77,6 +78,8 @@ following fields: created by your `Task` - [`volumes`](#volumes) - Specifies one or more volumes that you want to make available to your `Task`'s steps. + - [`workspaces`](#workspaces) - Specifies paths at which you expect volumes to + be mounted and available - [`stepTemplate`](#step-template) - Specifies a `Container` step definition to use as the basis for all steps within your `Task`. - [`sidecars`](#sidecars) - Specifies sidecar containers to run alongside @@ -132,7 +135,6 @@ the body of a `Task`. If multiple `steps` are defined, they will be executed in the same order as they are defined, if the `Task` is invoked by a [`TaskRun`](taskruns.md). - Each `steps` in a `Task` must specify a container image that adheres to the [container contract](./container-contract.md). For each of the `steps` fields, or container images that you define: @@ -182,7 +184,7 @@ steps: ...or to execute a Node script, if the image includes `node`: ```yaml -steps: +steps:- [Workspaces](#workspaces) - image: node # contains node script: | #!/usr/bin/env node @@ -369,6 +371,40 @@ For example, use volumes to accomplish one of the following common tasks: unsafe_. Use [kaniko](https://github.com/GoogleContainerTools/kaniko) instead. This is used only for the purposes of demonstration. +### Workspaces + +`workspaces` are a way of declaring volumes you expect to be made available to your +executing `Task` and the path to make them available at. They are similar to +[`volumes`](#volumes) but allow you to enforce at runtime that the volumes have +been attached and [allow you to specify subpaths](taskruns.md#workspaces) in the volumes +to attach. + +The volume will be made available at `/workspace/myworkspace`, or you can or override +this with `mountPath`. The value at `mountPath` can be anywhere on your pod's filesystem. +The path will be available via [variable substitution](#variable-substituation) with +`$(workspaces.myworkspace.path)`. + +The actual volumes must be provided at runtime +[in the `TaskRun`](taskruns.md#workspaces). +In a future iteration ([#1438](https://github.com/tektoncd/pipeline/issues/1438)) +it [will be possible to specify these in the `PipelineRun`](pipelineruns.md#workspaces) +as well. + +For example: + +```yaml +spec: + steps: + - name: write-message + image: ubuntu + command: ['bash'] + args: ['-c', 'echo hello! > $(workspaces.messages.path)/message'] + workspaces: + - name: messages + description: The folder where we write the message to + mountPath: /custom/path/relative/to/root +``` + ### Step Template Specifies a [`Container`](https://kubernetes.io/docs/concepts/containers/) @@ -459,9 +495,17 @@ has been created to track this bug. ### Variable Substitution -`Tasks` support string replacement using values from all [`inputs`](#inputs) and -[`outputs`](#outputs). +`Tasks` support string replacement using values from: + +* [Inputs and Outputs](#input-and-output-substitution) + * [Array params](#variable-substitution-with-parameters-of-type-array) +* [`workspaces`](#workspaces) +* [`volumes`](#variable-substitution-with-volumes) + +#### Input and Output substitution +[`inputs`](#inputs) and [outputs](#outputs) attributes can be used in replacements, +including [`params`](#params) and [resources](./resources.md#variable-substitution). Input parameters can be referenced in the `Task` spec using the variable substitution syntax below, where `` is the name of the parameter: @@ -472,7 +516,7 @@ $(inputs.params.) Param values from resources can also be accessed using [variable substitution](./resources.md#variable-substitution) -#### Variable Substitution with Parameters of Type `Array` +##### Variable Substitution with Parameters of Type `Array` Referenced parameters of type `array` will expand to insert the array elements in the reference string's spot. @@ -515,6 +559,14 @@ A valid reference to the `build-args` parameter is isolated and in an eligible f args: ["build", "$(inputs.params.build-args)", "additonalArg"] ``` +#### Workspace Substitution + +Paths to a `Task's` declared [workspaces](#workspaces) can be substituted with: + +``` +$(workspaces.myworkspace.path) +``` + #### Variable Substitution within Volumes Task volume names and different diff --git a/examples/taskruns/custom-volume.yaml b/examples/taskruns/custom-volume.yaml index 51092f29d0a..3bffd4bc8a4 100644 --- a/examples/taskruns/custom-volume.yaml +++ b/examples/taskruns/custom-volume.yaml @@ -7,15 +7,16 @@ spec: steps: - name: write image: ubuntu - command: ["/bin/bash"] - args: ["-c", "echo some stuff > /im/a/custom/mount/path/file"] + script: | + #!/usr/bin/env bash + echo some stuff > /im/a/custom/mount/path/file volumeMounts: - name: custom mountPath: /im/a/custom/mount/path - name: read image: ubuntu command: ["/bin/bash"] - args: ["-c", "cat /short/and/stout/file"] + args: ["-c", "cat /short/and/stout/file | grep stuff"] volumeMounts: - name: custom mountPath: /short/and/stout diff --git a/examples/taskruns/workspace.yaml b/examples/taskruns/workspace.yaml new file mode 100644 index 00000000000..ce003554dcf --- /dev/null +++ b/examples/taskruns/workspace.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: my-pvc +spec: + resources: + requests: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce +--- +apiVersion: tekton.dev/v1alpha1 +kind: TaskRun +metadata: + generateName: custom-volume- +spec: + workspaces: + - name: custom + emptyDir: {} + persistentVolumeClaim: + claimName: my-pvc + subPath: my-subdir + taskSpec: + steps: + - name: write + image: ubuntu + command: ["/bin/bash"] + args: ["-c", "echo stuff > /workspace/custom/foo"] + - name: read + image: ubuntu + command: ["/bin/bash"] + args: ["-c", "cat /workspace/custom/foo | grep stuff"] + workspaces: + - name: custom \ No newline at end of file diff --git a/pkg/apis/pipeline/v1alpha1/task_types.go b/pkg/apis/pipeline/v1alpha1/task_types.go index b0975c4369e..b183a593d22 100644 --- a/pkg/apis/pipeline/v1alpha1/task_types.go +++ b/pkg/apis/pipeline/v1alpha1/task_types.go @@ -59,6 +59,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/v1alpha1/taskrun_types.go b/pkg/apis/pipeline/v1alpha1/taskrun_types.go index 1a676a7cbaf..d7fd5fdc7a7 100644 --- a/pkg/apis/pipeline/v1alpha1/taskrun_types.go +++ b/pkg/apis/pipeline/v1alpha1/taskrun_types.go @@ -50,7 +50,12 @@ type TaskRunSpec struct { Timeout *metav1.Duration `json:"timeout,omitempty"` // PodTemplate holds pod specific configuration + // +optional PodTemplate PodTemplate `json:"podTemplate,omitempty"` + + // Workspaces is a list of WorkspaceBindings from volumes to workspaces. + // +optional + Workspaces []WorkspaceBinding `json:"workspaces,omitempty"` } // TaskRunSpecStatus defines the taskrun spec status the user can provide diff --git a/pkg/apis/pipeline/v1alpha1/workspace_types.go b/pkg/apis/pipeline/v1alpha1/workspace_types.go new file mode 100644 index 00000000000..7cfafd12795 --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/workspace_types.go @@ -0,0 +1,53 @@ +/* +Copyright 2019 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" +) + +// 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"` +} + +// 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/v1alpha1/workspace_validation.go b/pkg/apis/pipeline/v1alpha1/workspace_validation.go new file mode 100644 index 00000000000..68b6917bd92 --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/workspace_validation.go @@ -0,0 +1,48 @@ +/* +Copyright 2019 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + + "knative.dev/pkg/apis" +) + +// Validate looks at the Volume provided in wb and makes sure that it is valid. +// This means that the 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 b == nil { + return nil + } + + // Users should only provide one supported VolumeSource. + if b.PersistentVolumeClaim != nil && b.EmptyDir != nil { + return apis.ErrDisallowedFields("workspace.persistentvolumeclaim", "workspace.emptydir") + } + + // Users must provide at least one supported VolumeSource. + if b.PersistentVolumeClaim == nil && b.EmptyDir == nil { + return apis.ErrMissingField("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/workspace_validation_test.go b/pkg/apis/pipeline/v1alpha1/workspace_validation_test.go new file mode 100644 index 00000000000..17e9fc151c9 --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/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 v1alpha1 + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" +) + +func TestWorkspaceBindingValidateValid(t *testing.T) { + for _, tc := range []struct { + name string + binding *WorkspaceBinding + }{{ + name: "no binding provided", + binding: nil, + }, { + 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: "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/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go index 2d57103828e..679ad3df6ef 100644 --- a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go @@ -1717,6 +1717,13 @@ func (in *TaskRunSpec) DeepCopyInto(out *TaskRunSpec) { **out = **in } in.PodTemplate.DeepCopyInto(&out.PodTemplate) + if in.Workspaces != nil { + in, out := &in.Workspaces, &out.Workspaces + *out = make([]WorkspaceBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -1825,6 +1832,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 } @@ -1853,3 +1865,45 @@ 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/list/diff.go b/pkg/list/diff.go index 049fd91c315..ded6da2c06c 100644 --- a/pkg/list/diff.go +++ b/pkg/list/diff.go @@ -24,11 +24,11 @@ import "golang.org/x/xerrors" func IsSame(required, provided []string) error { missing := DiffLeft(required, provided) if len(missing) > 0 { - return xerrors.Errorf("Didn't provide required values: %s", missing) + return xerrors.Errorf("didn't provide required values: %s", missing) } extra := DiffLeft(provided, required) if len(extra) > 0 { - return xerrors.Errorf("Provided extra values: %s", extra) + return xerrors.Errorf("provided extra values: %s", extra) } return nil } diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index 4bba5c95af2..4ba4b9904b4 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -35,12 +35,15 @@ import ( ) const ( - workspaceDir = "/workspace" - homeDir = "/builder/home" + homeDir = "/builder/home" + + taskRunLabelKey = pipeline.GroupName + pipeline.TaskRunLabelKey - taskRunLabelKey = pipeline.GroupName + pipeline.TaskRunLabelKey ManagedByLabelKey = "app.kubernetes.io/managed-by" ManagedByLabelValue = "tekton-pipelines" + + // WorkspaceDir is the root directory used for PipelineResources and (by default) Workspaces + WorkspaceDir = "/workspace" ) // These are effectively const, but Go doesn't have such an annotation. @@ -57,7 +60,7 @@ var ( }} implicitVolumeMounts = []corev1.VolumeMount{{ Name: "workspace", - MountPath: workspaceDir, + MountPath: WorkspaceDir, }, { Name: "home", MountPath: homeDir, @@ -168,7 +171,7 @@ func MakePod(images pipeline.Images, taskRun *v1alpha1.TaskRun, taskSpec v1alpha // isolation. for i, s := range stepContainers { if s.WorkingDir == "" { - stepContainers[i].WorkingDir = workspaceDir + stepContainers[i].WorkingDir = WorkspaceDir } if s.Name == "" { stepContainers[i].Name = names.SimpleNameGenerator.RestrictLength(fmt.Sprintf("%sunnamed-%d", stepPrefix, i)) diff --git a/pkg/pod/workingdir_init.go b/pkg/pod/workingdir_init.go index 5df27265759..e78886dab52 100644 --- a/pkg/pod/workingdir_init.go +++ b/pkg/pod/workingdir_init.go @@ -74,7 +74,7 @@ func workingDirInit(shellImage string, steps []v1alpha1.Step, volumeMounts []cor Image: shellImage, Command: []string{"sh"}, Args: []string{"-c", "mkdir -p " + strings.Join(relativeDirs, " ")}, - WorkingDir: workspaceDir, + WorkingDir: WorkspaceDir, VolumeMounts: volumeMounts, } } diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index 8b866cf8c7b..58808a1d9ad 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -34,6 +34,7 @@ import ( "github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources" "github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources/cloudevent" "github.com/tektoncd/pipeline/pkg/status" + "github.com/tektoncd/pipeline/pkg/workspace" "go.uber.org/zap" "golang.org/x/xerrors" corev1 "k8s.io/api/core/v1" @@ -293,7 +294,7 @@ func (c *Reconciler) reconcile(ctx context.Context, tr *v1alpha1.TaskRun) error } if err := ValidateResolvedTaskResources(tr.Spec.Inputs.Params, rtr); err != nil { - c.Logger.Errorf("Failed to validate taskrun %q: %v", tr.Name, err) + c.Logger.Errorf("TaskRun %q resources are invalid: %v", tr.Name, err) tr.Status.SetCondition(&apis.Condition{ Type: apis.ConditionSucceeded, Status: corev1.ConditionFalse, @@ -303,6 +304,16 @@ func (c *Reconciler) reconcile(ctx context.Context, tr *v1alpha1.TaskRun) error return nil } + if err := workspace.ValidateBindings(taskSpec.Workspaces, tr.Spec.Workspaces); err != nil { + c.Logger.Errorf("TaskRun %q workspaces are invalid: %v", tr.Name, err) + tr.Status.SetCondition(&apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + Reason: status.ReasonFailedValidation, + Message: err.Error(), + }) + } + // Initialize the cloud events if at least a CloudEventResource is defined // and they have not been initialized yet. // FIXME(afrittoli) This resource specific logic will have to be replaced @@ -463,19 +474,19 @@ func (c *Reconciler) createPod(tr *v1alpha1.TaskRun, rtr *resources.ResolvedTask err = resources.AddOutputImageDigestExporter(c.Images.ImageDigestExporterImage, tr, ts, c.resourceLister.PipelineResources(tr.Namespace).Get) if err != nil { - c.Logger.Errorf("Failed to create a build for taskrun: %s due to output image resource error %v", tr.Name, err) + c.Logger.Errorf("Failed to create a pod for taskrun: %s due to output image resource error %v", tr.Name, err) return nil, err } ts, err = resources.AddInputResource(c.KubeClientSet, c.Images, rtr.TaskName, ts, tr, inputResources, c.Logger) if err != nil { - c.Logger.Errorf("Failed to create a build for taskrun: %s due to input resource error %v", tr.Name, err) + c.Logger.Errorf("Failed to create a pod for taskrun: %s due to input resource error %v", tr.Name, err) return nil, err } ts, err = resources.AddOutputResources(c.KubeClientSet, c.Images, rtr.TaskName, ts, tr, outputResources, c.Logger) if err != nil { - c.Logger.Errorf("Failed to create a build for taskrun: %s due to output resource error %v", tr.Name, err) + c.Logger.Errorf("Failed to create a pod for taskrun: %s due to output resource error %v", tr.Name, err) return nil, err } @@ -490,6 +501,12 @@ func (c *Reconciler) createPod(tr *v1alpha1.TaskRun, rtr *resources.ResolvedTask ts = resources.ApplyResources(ts, inputResources, "inputs") ts = resources.ApplyResources(ts, outputResources, "outputs") + ts, err = workspace.Apply(*ts, tr.Spec.Workspaces) + if err != nil { + c.Logger.Errorf("Failed to create a pod for taskrun: %s due to workspace error %v", tr.Name, err) + return nil, err + } + pod, err := podconvert.MakePod(c.Images, tr, *ts, c.KubeClientSet, c.entrypointCache) if err != nil { return nil, xerrors.Errorf("translating Build to Pod: %w", err) diff --git a/pkg/workspace/apply.go b/pkg/workspace/apply.go new file mode 100644 index 00000000000..009cd425552 --- /dev/null +++ b/pkg/workspace/apply.go @@ -0,0 +1,69 @@ +package workspace + +import ( + "fmt" + "path/filepath" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/pod" + corev1 "k8s.io/api/core/v1" +) + +func getDeclaredWorkspace(name string, w []v1alpha1.WorkspaceDeclaration) (*v1alpha1.WorkspaceDeclaration, error) { + for _, workspace := range w { + if workspace.Name == name { + return &workspace, nil + } + } + // Trusting validation to ensure + return nil, fmt.Errorf("even though validation should have caught it, bound workspace %s did not exist in declared workspaces", name) +} + +// Apply will update the StepTemplate and Volumes declaration in ts so that the workspaces +// specified through wb combined with the declared workspaces in ts will be available for +// all containers in the resulting pod. +func Apply(ts v1alpha1.TaskSpec, wb []v1alpha1.WorkspaceBinding) (*v1alpha1.TaskSpec, error) { + // If there are no bound workspaces, we don't need to do anything + if len(wb) == 0 { + return &ts, nil + } + + // Initialize StepTemplate if it hasn't been already + if ts.StepTemplate == nil { + ts.StepTemplate = &corev1.Container{} + } + + for i := range wb { + w, err := getDeclaredWorkspace(wb[i].Name, ts.Workspaces) + if err != nil { + return nil, err + } + mountPath := filepath.Join(pod.WorkspaceDir, wb[i].Name) + if w.MountPath != "" { + mountPath = w.MountPath + } + ts.StepTemplate.VolumeMounts = append(ts.StepTemplate.VolumeMounts, corev1.VolumeMount{ + Name: wb[i].Name, + MountPath: mountPath, + SubPath: wb[i].SubPath, + }) + + source := corev1.VolumeSource{} + if wb[i].PersistentVolumeClaim != nil { + pvc := *wb[i].PersistentVolumeClaim + source = corev1.VolumeSource{ + PersistentVolumeClaim: &pvc, + } + } else if wb[i].EmptyDir != nil { + ed := *wb[i].EmptyDir + source = corev1.VolumeSource{ + EmptyDir: &ed, + } + } + ts.Volumes = append(ts.Volumes, corev1.Volume{ + Name: wb[i].Name, + VolumeSource: source, + }) + } + return &ts, nil +} diff --git a/pkg/workspace/apply_test.go b/pkg/workspace/apply_test.go new file mode 100644 index 00000000000..a8fd932f3bf --- /dev/null +++ b/pkg/workspace/apply_test.go @@ -0,0 +1,309 @@ +package workspace_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/workspace" + corev1 "k8s.io/api/core/v1" +) + +func TestApply(t *testing.T) { + for _, tc := range []struct { + name string + ts v1alpha1.TaskSpec + workspaces []v1alpha1.WorkspaceBinding + expectedTaskSpec v1alpha1.TaskSpec + }{{ + name: "binding a single workspace with a PVC", + ts: v1alpha1.TaskSpec{ + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom", + }}, + }, + workspaces: []v1alpha1.WorkspaceBinding{{ + Name: "custom", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + SubPath: "/foo/bar/baz", + }}, + expectedTaskSpec: v1alpha1.TaskSpec{ + StepTemplate: &corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "custom", + MountPath: "/workspace/custom", + SubPath: "/foo/bar/baz", + }}, + }, + Volumes: []corev1.Volume{{ + Name: "custom", // TODO: randomize names so that there aren't collisions? + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + }, + }}, + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom", + }}, + }, + }, { + name: "binding a single workspace with emptyDir", + ts: v1alpha1.TaskSpec{ + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom", + }}, + }, + workspaces: []v1alpha1.WorkspaceBinding{{ + Name: "custom", + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + }, + SubPath: "/foo/bar/baz", + }}, + expectedTaskSpec: v1alpha1.TaskSpec{ + StepTemplate: &corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "custom", + MountPath: "/workspace/custom", + SubPath: "/foo/bar/baz", // TODO: what happens when you use subPath with emptyDir + }}, + }, + Volumes: []corev1.Volume{{ + Name: "custom", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + }, + }, + }}, + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom", + }}, + }, + }, { + name: "task spec already has volumes and stepTemplate", + ts: v1alpha1.TaskSpec{ + StepTemplate: &corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "awesome-volume", + MountPath: "/", + }}, + }, + Volumes: []corev1.Volume{{ + Name: "awesome-volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }}, + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom", + }}, + }, + workspaces: []v1alpha1.WorkspaceBinding{{ + Name: "custom", + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + }, + SubPath: "/foo/bar/baz", + }}, + expectedTaskSpec: v1alpha1.TaskSpec{ + StepTemplate: &corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "awesome-volume", + MountPath: "/", + }, { + Name: "custom", + MountPath: "/workspace/custom", + SubPath: "/foo/bar/baz", // TODO: what happens when you use subPath with emptyDir + }}, + }, + Volumes: []corev1.Volume{{ + Name: "awesome-volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, { + Name: "custom", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + }, + }, + }}, + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom", + }}, + }, + }, { + name: "0 workspace bindings", + ts: v1alpha1.TaskSpec{ + Steps: []v1alpha1.Step{{ + Container: corev1.Container{ + Name: "foo", + }}}, + }, + workspaces: []v1alpha1.WorkspaceBinding{}, + expectedTaskSpec: v1alpha1.TaskSpec{ + Steps: []v1alpha1.Step{{ + Container: corev1.Container{ + Name: "foo", + }}}, + }, + }, { + name: "binding multiple workspaces", + ts: v1alpha1.TaskSpec{ + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom", + }, { + Name: "even-more-custom", + }}, + }, + workspaces: []v1alpha1.WorkspaceBinding{{ + Name: "custom", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + SubPath: "/foo/bar/baz", + }, { + Name: "even-more-custom", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "myotherpvc", + }, + }}, + expectedTaskSpec: v1alpha1.TaskSpec{ + StepTemplate: &corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "custom", + MountPath: "/workspace/custom", + SubPath: "/foo/bar/baz", + }, { + Name: "even-more-custom", + MountPath: "/workspace/even-more-custom", + SubPath: "", + }}, + }, + Volumes: []corev1.Volume{{ + Name: "custom", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + }, + }, { + Name: "even-more-custom", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "myotherpvc", + }, + }, + }}, + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom"}, { + Name: "even-more-custom", + }}, + }, + }, { + name: "multiple workspaces binding to the same volume with diff subpaths", + ts: v1alpha1.TaskSpec{ + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom", + }, { + Name: "custom2", + }}, + }, + workspaces: []v1alpha1.WorkspaceBinding{{ + Name: "custom", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + SubPath: "/foo/bar/baz", + }, { + Name: "custom2", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + SubPath: "/very/professional/work/space", + }}, + expectedTaskSpec: v1alpha1.TaskSpec{ + StepTemplate: &corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "custom", + MountPath: "/workspace/custom", + SubPath: "/foo/bar/baz", + }, { + Name: "custom2", + MountPath: "/workspace/custom2", + SubPath: "/very/professional/work/space", + }}, + }, + Volumes: []corev1.Volume{{ + Name: "custom", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + }, + }, { + Name: "custom2", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + }, + }}, + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom", + }, { + Name: "custom2", + }}, + }, + }, { + name: "non default mount path", + ts: v1alpha1.TaskSpec{ + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom", + MountPath: "/my/fancy/mount/path", + }}, + }, + workspaces: []v1alpha1.WorkspaceBinding{{ + Name: "custom", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + }}, + expectedTaskSpec: v1alpha1.TaskSpec{ + StepTemplate: &corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "custom", + MountPath: "/my/fancy/mount/path", + }}, + }, + Volumes: []corev1.Volume{{ + Name: "custom", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + }, + }}, + Workspaces: []v1alpha1.WorkspaceDeclaration{{ + Name: "custom", + MountPath: "/my/fancy/mount/path", + }}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ts, err := workspace.Apply(tc.ts, tc.workspaces) + if err != nil { + t.Fatalf("Did not expect error but got %v", err) + } + if d := cmp.Diff(tc.expectedTaskSpec, *ts); d != "" { + t.Errorf("Didn't get expected TaskSpec modifications (-want, +got): %s", d) + } + }) + } +} diff --git a/pkg/workspace/validate.go b/pkg/workspace/validate.go new file mode 100644 index 00000000000..d59542bbbbc --- /dev/null +++ b/pkg/workspace/validate.go @@ -0,0 +1,50 @@ +/* +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 workspace + +import ( + "context" + "fmt" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/list" +) + +// ValidateBindings will return an error if the bound workspaces in wb satisfy the declared +// workspaces in w. +func ValidateBindings(w []v1alpha1.WorkspaceDeclaration, wb []v1alpha1.WorkspaceBinding) error { + // This will also be validated at webhook time but in case the webhook isn't invoked for some + // reason we'll invoke the same validation here. + for _, b := range wb { + if err := b.Validate(context.Background()); err != nil { + return fmt.Errorf("binding %q is invalid: %v", b.Name, err) + } + } + + declNames := make([]string, len(w)) + for i := range w { + declNames[i] = w[i].Name + } + bindNames := make([]string, len(wb)) + for i := range wb { + bindNames[i] = wb[i].Name + } + if err := list.IsSame(declNames, bindNames); err != nil { + return fmt.Errorf("bound workspaces did not match declared workspaces: %v", err) + } + return nil +} diff --git a/pkg/workspace/validate_test.go b/pkg/workspace/validate_test.go new file mode 100644 index 00000000000..e79d104a509 --- /dev/null +++ b/pkg/workspace/validate_test.go @@ -0,0 +1,133 @@ +/* +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 workspace + +import ( + "testing" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + corev1 "k8s.io/api/core/v1" +) + +func TestValidateBindingsValid(t *testing.T) { + for _, tc := range []struct { + name string + declarations []v1alpha1.WorkspaceDeclaration + bindings []v1alpha1.WorkspaceBinding + }{{ + name: "no bindings provided or required", + declarations: nil, + bindings: nil, + }, { + name: "empty list of bindings provided and required", + declarations: []v1alpha1.WorkspaceDeclaration{}, + bindings: []v1alpha1.WorkspaceBinding{}, + }, { + name: "Successfully bound with PVC", + declarations: []v1alpha1.WorkspaceDeclaration{{ + Name: "beth", + }}, + bindings: []v1alpha1.WorkspaceBinding{{ + Name: "beth", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "pool-party", + }, + }}, + }, { + name: "Successfully bound with emptyDir", + declarations: []v1alpha1.WorkspaceDeclaration{{ + Name: "beth", + }}, + bindings: []v1alpha1.WorkspaceBinding{{ + Name: "beth", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}, + }} { + t.Run(tc.name, func(t *testing.T) { + if err := ValidateBindings(tc.declarations, tc.bindings); err != nil { + t.Errorf("didnt expect error for valid bindings but got: %v", err) + } + }) + } + +} + +func TestValidateBindingsInvalid(t *testing.T) { + for _, tc := range []struct { + name string + declarations []v1alpha1.WorkspaceDeclaration + bindings []v1alpha1.WorkspaceBinding + }{{ + name: "Didn't provide binding matching declared workspace", + declarations: []v1alpha1.WorkspaceDeclaration{{ + Name: "beth", + }}, + bindings: []v1alpha1.WorkspaceBinding{{ + Name: "kate", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, { + Name: "beth", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}, + }, { + name: "Provided a binding that wasn't needed", + declarations: []v1alpha1.WorkspaceDeclaration{{ + Name: "randall", + }, { + Name: "beth", + }}, + bindings: []v1alpha1.WorkspaceBinding{{ + Name: "beth", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}, + }, { + name: "Provided both pvc and emptydir", + declarations: []v1alpha1.WorkspaceDeclaration{{ + Name: "beth", + }}, + bindings: []v1alpha1.WorkspaceBinding{{ + Name: "beth", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "pool-party", + }, + }}, + }, { + name: "Provided neither pvc nor emptydir", + declarations: []v1alpha1.WorkspaceDeclaration{{ + Name: "beth", + }}, + bindings: []v1alpha1.WorkspaceBinding{{ + Name: "beth", + }}, + }, { + name: "Provided pvc without claim name", + declarations: []v1alpha1.WorkspaceDeclaration{{ + Name: "beth", + }}, + bindings: []v1alpha1.WorkspaceBinding{{ + Name: "beth", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{}, + }}, + }} { + t.Run(tc.name, func(t *testing.T) { + if err := ValidateBindings(tc.declarations, tc.bindings); err == nil { + t.Errorf("expected error for invalid bindings but didn't get any!") + } + }) + } +}