diff --git a/docs/taskruns.md b/docs/taskruns.md index d55535c7109..b368dfb6ba9 100644 --- a/docs/taskruns.md +++ b/docs/taskruns.md @@ -231,6 +231,7 @@ at runtime you need to map the `workspaces` to actual physical volumes with * [`emptyDir`](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) * [`persistentVolumeClaim`](https://kubernetes.io/docs/concepts/storage/volumes/#persistentvolumeclaim) +* [`configMap`](https://kubernetes.io/docs/concepts/storage/volumes/#configmap) _If you need support for a `VolumeSource` not listed here [please open an issue](https://github.com/tektoncd/pipeline/issues) or feel free to @@ -260,6 +261,24 @@ workspaces: emptyDir: {} ``` +A ConfigMap can also be used as a workspace with the following caveats: + +1. ConfigMap volume sources are always mounted as read-only inside a task's +containers - tasks cannot write content to them and a step may error out +and fail the task if a write is attempted. +2. The ConfigMap you want to use as a workspace must already exist prior +to the TaskRun being submitted. + +To use a [`configMap`](https://kubernetes.io/docs/concepts/storage/volumes/#configmap) +as a `workspace`: + +```yaml +workspaces: +- name: myworkspace + configmap: + name: my-configmap +``` + _For a complete example see [workspace.yaml](../examples/taskruns/workspace.yaml)._ ## Status diff --git a/examples/taskruns/workspace.yaml b/examples/taskruns/workspace.yaml index 2ff0ae0f7bf..345f5c6d079 100644 --- a/examples/taskruns/workspace.yaml +++ b/examples/taskruns/workspace.yaml @@ -10,6 +10,13 @@ spec: accessModes: - ReadWriteOnce --- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap +data: + message: hello world +--- apiVersion: tekton.dev/v1alpha1 kind: TaskRun metadata: @@ -26,43 +33,39 @@ spec: - name: custom3 emptyDir: {} subPath: testing + - name: custom4 + configMap: + name: my-configmap + items: + - key: message + path: my-message.txt taskSpec: steps: - name: write image: ubuntu - script: | - #!/usr/bin/env bash - set -xe - echo $(workspaces.custom.volume) > $(workspaces.custom.path)/foo + script: echo $(workspaces.custom.volume) > $(workspaces.custom.path)/foo - name: read image: ubuntu - script: | - #!/usr/bin/env bash - set -xe - cat $(workspaces.custom.path)/foo | grep $(workspaces.custom.volume) + script: cat $(workspaces.custom.path)/foo | grep $(workspaces.custom.volume) - name: write2 image: ubuntu - script: | - #!/usr/bin/env bash - set -xe - echo $(workspaces.custom2.path) > $(workspaces.custom2.path)/foo + script: echo $(workspaces.custom2.path) > $(workspaces.custom2.path)/foo - name: read2 image: ubuntu - script: | - #!/usr/bin/env bash - cat $(workspaces.custom2.path)/foo | grep $(workspaces.custom2.path) + script: cat $(workspaces.custom2.path)/foo | grep $(workspaces.custom2.path) - name: write3 image: ubuntu - script: | - #!/usr/bin/env bash - echo $(workspaces.custom3.path) > $(workspaces.custom3.path)/foo + script: echo $(workspaces.custom3.path) > $(workspaces.custom3.path)/foo - name: read3 image: ubuntu - script: | - #!/usr/bin/env bash - cat $(workspaces.custom3.path)/foo | grep $(workspaces.custom3.path) + script: cat $(workspaces.custom3.path)/foo | grep $(workspaces.custom3.path) + - name: readconfigmap + image: ubuntu + script: cat $(workspaces.custom4.path)/my-message.txt | grep "hello world" workspaces: - name: custom - name: custom2 mountPath: /foo/bar/baz - - name: custom3 \ No newline at end of file + - name: custom3 + - name: custom4 + mountPath: /baz/bar/quux diff --git a/pkg/apis/pipeline/v1alpha1/workspace_types.go b/pkg/apis/pipeline/v1alpha1/workspace_types.go index 01b80bcc0cc..8b54695e501 100644 --- a/pkg/apis/pipeline/v1alpha1/workspace_types.go +++ b/pkg/apis/pipeline/v1alpha1/workspace_types.go @@ -24,5 +24,5 @@ import ( type WorkspaceDeclaration = v1alpha2.WorkspaceDeclaration // WorkspaceBinding maps a Task's declared workspace to a Volume. -// Currently we only support PersistentVolumeClaims and EmptyDir. +// Currently we only support PersistentVolumeClaims, EmptyDir and ConfigMap. type WorkspaceBinding = v1alpha2.WorkspaceBinding diff --git a/pkg/apis/pipeline/v1alpha1/workspace_validation_test.go b/pkg/apis/pipeline/v1alpha1/workspace_validation_test.go index 5866305784a..345506822a5 100644 --- a/pkg/apis/pipeline/v1alpha1/workspace_validation_test.go +++ b/pkg/apis/pipeline/v1alpha1/workspace_validation_test.go @@ -41,6 +41,16 @@ func TestWorkspaceBindingValidateValid(t *testing.T) { Name: "beth", EmptyDir: &corev1.EmptyDirVolumeSource{}, }, + }, { + name: "Valid configMap", + binding: &WorkspaceBinding{ + Name: "beth", + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "a-configmap-name", + }, + }, + }, }} { t.Run(tc.name, func(t *testing.T) { if err := tc.binding.Validate(context.Background()); err != nil { @@ -78,6 +88,12 @@ func TestWorkspaceBindingValidateInvalid(t *testing.T) { Name: "beth", PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{}, }, + }, { + name: "Provide configmap without a name", + binding: &WorkspaceBinding{ + Name: "beth", + ConfigMap: &corev1.ConfigMapVolumeSource{}, + }, }} { t.Run(tc.name, func(t *testing.T) { if err := tc.binding.Validate(context.Background()); err == nil { diff --git a/pkg/apis/pipeline/v1alpha2/workspace_types.go b/pkg/apis/pipeline/v1alpha2/workspace_types.go index b211a7f3034..5acd43598f4 100644 --- a/pkg/apis/pipeline/v1alpha2/workspace_types.go +++ b/pkg/apis/pipeline/v1alpha2/workspace_types.go @@ -65,4 +65,7 @@ type WorkspaceBinding struct { // Either this OR PersistentVolumeClaim can be used. // +optional EmptyDir *corev1.EmptyDirVolumeSource `json:"emptyDir,omitempty"` + // ConfigMap represents a configMap that should populate this workspace. + // +optional + ConfigMap *corev1.ConfigMapVolumeSource `json:"configMap,omitempty"` } diff --git a/pkg/apis/pipeline/v1alpha2/workspace_validation.go b/pkg/apis/pipeline/v1alpha2/workspace_validation.go index c90a9e6f0fc..1e1452890c0 100644 --- a/pkg/apis/pipeline/v1alpha2/workspace_validation.go +++ b/pkg/apis/pipeline/v1alpha2/workspace_validation.go @@ -23,6 +23,14 @@ import ( "knative.dev/pkg/apis" ) +// allVolumeSourceFields is a list of all the volume source field paths that a +// WorkspaceBinding may include. +var allVolumeSourceFields []string = []string{ + "workspace.persistentvolumeclaim", + "workspace.emptydir", + "workspace.configmap", +} + // 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. @@ -31,19 +39,41 @@ func (b *WorkspaceBinding) Validate(ctx context.Context) *apis.FieldError { 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") + numSources := b.numSources() + + if numSources > 1 { + return apis.ErrMultipleOneOf(allVolumeSourceFields...) } - // Users must provide at least one supported VolumeSource. - if b.PersistentVolumeClaim == nil && b.EmptyDir == nil { - return apis.ErrMissingOneOf("workspace.persistentvolumeclaim", "workspace.emptydir") + if numSources == 0 { + return apis.ErrMissingOneOf(allVolumeSourceFields...) } // 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") } + + // For a ConfigMap to work, you must provide the name of the ConfigMap to use. + if b.ConfigMap != nil && b.ConfigMap.LocalObjectReference.Name == "" { + return apis.ErrMissingField("workspace.configmap.name") + } + return nil } + +// numSources returns the total number of volume sources that this WorkspaceBinding +// has been configured with. +func (b *WorkspaceBinding) numSources() int { + n := 0 + if b.PersistentVolumeClaim != nil { + n++ + } + if b.EmptyDir != nil { + n++ + } + if b.ConfigMap != nil { + n++ + } + return n +} diff --git a/pkg/apis/pipeline/v1alpha2/workspace_validation_test.go b/pkg/apis/pipeline/v1alpha2/workspace_validation_test.go index e25638bf775..8c790c0f54d 100644 --- a/pkg/apis/pipeline/v1alpha2/workspace_validation_test.go +++ b/pkg/apis/pipeline/v1alpha2/workspace_validation_test.go @@ -41,6 +41,16 @@ func TestWorkspaceBindingValidateValid(t *testing.T) { Name: "beth", EmptyDir: &corev1.EmptyDirVolumeSource{}, }, + }, { + name: "Valid configMap", + binding: &WorkspaceBinding{ + Name: "beth", + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "a-configmap-name", + }, + }, + }, }} { t.Run(tc.name, func(t *testing.T) { if err := tc.binding.Validate(context.Background()); err != nil { @@ -78,6 +88,12 @@ func TestWorkspaceBindingValidateInvalid(t *testing.T) { Name: "beth", PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{}, }, + }, { + name: "Provide configmap without a name", + binding: &WorkspaceBinding{ + Name: "beth", + ConfigMap: &corev1.ConfigMapVolumeSource{}, + }, }} { t.Run(tc.name, func(t *testing.T) { if err := tc.binding.Validate(context.Background()); err == nil { diff --git a/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go index 38bb0f812e9..670ef011e47 100644 --- a/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go @@ -313,6 +313,11 @@ func (in *WorkspaceBinding) DeepCopyInto(out *WorkspaceBinding) { *out = new(v1.EmptyDirVolumeSource) (*in).DeepCopyInto(*out) } + if in.ConfigMap != nil { + in, out := &in.ConfigMap, &out.ConfigMap + *out = new(v1.ConfigMapVolumeSource) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/workspace/apply.go b/pkg/workspace/apply.go index 1755c0dab27..17393ae64fc 100644 --- a/pkg/workspace/apply.go +++ b/pkg/workspace/apply.go @@ -20,7 +20,8 @@ func GetVolumes(wb []v1alpha1.WorkspaceBinding) map[string]corev1.Volume { v := map[string]corev1.Volume{} for _, w := range wb { name := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(volumeNameBase) - if w.PersistentVolumeClaim != nil { + switch { + case w.PersistentVolumeClaim != nil: // If it's a PVC, we need to check if we've encountered it before so we avoid mounting it twice if vv, ok := pvcs[w.PersistentVolumeClaim.ClaimName]; ok { v[w.Name] = vv @@ -34,7 +35,7 @@ func GetVolumes(wb []v1alpha1.WorkspaceBinding) map[string]corev1.Volume { } pvcs[pvc.ClaimName] = v[w.Name] } - } else if w.EmptyDir != nil { + case w.EmptyDir != nil: ed := *w.EmptyDir v[w.Name] = corev1.Volume{ Name: name, @@ -42,6 +43,14 @@ func GetVolumes(wb []v1alpha1.WorkspaceBinding) map[string]corev1.Volume { EmptyDir: &ed, }, } + case w.ConfigMap != nil: + cm := *w.ConfigMap + v[w.Name] = corev1.Volume{ + Name: name, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &cm, + }, + } } } return v diff --git a/pkg/workspace/apply_test.go b/pkg/workspace/apply_test.go index 12c34614fea..487cadaf788 100644 --- a/pkg/workspace/apply_test.go +++ b/pkg/workspace/apply_test.go @@ -54,6 +54,37 @@ func TestGetVolumes(t *testing.T) { }, }, }, + }, { + name: "binding a single workspace with configMap", + workspaces: []v1alpha1.WorkspaceBinding{{ + Name: "custom", + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "foobarconfigmap", + }, + Items: []corev1.KeyToPath{{ + Key: "foobar", + Path: "foobar.txt", + }}, + }, + SubPath: "/foo/bar/baz", + }}, + expectedVolumes: map[string]corev1.Volume{ + "custom": { + Name: "ws-mssqb", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "foobarconfigmap", + }, + Items: []corev1.KeyToPath{{ + Key: "foobar", + Path: "foobar.txt", + }}, + }, + }, + }, + }, }, { name: "0 workspace bindings", workspaces: []v1alpha1.WorkspaceBinding{}, @@ -74,7 +105,7 @@ func TestGetVolumes(t *testing.T) { }}, expectedVolumes: map[string]corev1.Volume{ "custom": { - Name: "ws-mssqb", + Name: "ws-78c5n", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: "mypvc", @@ -82,7 +113,7 @@ func TestGetVolumes(t *testing.T) { }, }, "even-more-custom": { - Name: "ws-78c5n", + Name: "ws-6nl7g", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: "myotherpvc", @@ -107,7 +138,7 @@ func TestGetVolumes(t *testing.T) { }}, expectedVolumes: map[string]corev1.Volume{ "custom": { - Name: "ws-6nl7g", + Name: "ws-j2tds", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: "mypvc", @@ -116,7 +147,7 @@ func TestGetVolumes(t *testing.T) { }, "custom2": { // Since it is the same PVC source, it can't be added twice with two different names - Name: "ws-6nl7g", + Name: "ws-j2tds", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: "mypvc",