From be55cf8f33a0a5c2c5ea0fc0532cbfe3a65c9d0c Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 3 Jan 2020 11:29:53 -0500 Subject: [PATCH] Add Secret volume source support to workspaces Fixes https://github.com/tektoncd/pipeline/issues/1438 The final of the original feature requests related to workspaces was to include support for Secrets as the source of a volume mounted into Task containers. This PR introduces support for Secrets as workspaces in a TaskRun definition. --- docs/taskruns.md | 18 ++++++++ examples/taskruns/workspace.yaml | 22 ++++++++++ pkg/apis/pipeline/v1alpha1/workspace_types.go | 1 - .../v1alpha1/workspace_validation_test.go | 38 +++++++++++++++++ pkg/apis/pipeline/v1alpha2/workspace_types.go | 4 +- .../pipeline/v1alpha2/workspace_validation.go | 9 ++++ .../v1alpha2/workspace_validation_test.go | 14 +++++++ .../v1alpha2/zz_generated.deepcopy.go | 5 +++ pkg/workspace/apply.go | 41 +++++++++---------- pkg/workspace/apply_test.go | 35 ++++++++++++++-- 10 files changed, 160 insertions(+), 27 deletions(-) diff --git a/docs/taskruns.md b/docs/taskruns.md index b368dfb6ba9..b6424465b44 100644 --- a/docs/taskruns.md +++ b/docs/taskruns.md @@ -279,6 +279,24 @@ workspaces: name: my-configmap ``` +A Secret can also be used as a workspace with the following caveats: + +1. Secret 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 Secret you want to use as a workspace must already exist prior +to the TaskRun being submitted. + +To use a [`secret`](https://kubernetes.io/docs/concepts/storage/volumes/#secret) +as a `workspace`: + +```yaml +workspaces: +- name: myworkspace + secret: + secretName: my-secret +``` + _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 345f5c6d079..8ac77cf38d1 100644 --- a/examples/taskruns/workspace.yaml +++ b/examples/taskruns/workspace.yaml @@ -17,6 +17,16 @@ metadata: data: message: hello world --- +apiVersion: v1 +kind: Secret +metadata: + name: my-secret +type: Opaque +stringData: + username: user +data: + message: aGVsbG8gc2VjcmV0 +--- apiVersion: tekton.dev/v1alpha1 kind: TaskRun metadata: @@ -39,6 +49,9 @@ spec: items: - key: message path: my-message.txt + - name: custom5 + secret: + secretName: my-secret taskSpec: steps: - name: write @@ -62,6 +75,13 @@ spec: - name: readconfigmap image: ubuntu script: cat $(workspaces.custom4.path)/my-message.txt | grep "hello world" + - name: readsecret + image: ubuntu + script: | + #!/usr/bin/env bash + set -xe + cat $(workspaces.custom5.path)/username | grep "user" + cat $(workspaces.custom5.path)/message | grep "hello secret" workspaces: - name: custom - name: custom2 @@ -69,3 +89,5 @@ spec: - name: custom3 - name: custom4 mountPath: /baz/bar/quux + - name: custom5 + mountPath: /my/secret/volume diff --git a/pkg/apis/pipeline/v1alpha1/workspace_types.go b/pkg/apis/pipeline/v1alpha1/workspace_types.go index 8b54695e501..5c1a0242c66 100644 --- a/pkg/apis/pipeline/v1alpha1/workspace_types.go +++ b/pkg/apis/pipeline/v1alpha1/workspace_types.go @@ -24,5 +24,4 @@ import ( type WorkspaceDeclaration = v1alpha2.WorkspaceDeclaration // WorkspaceBinding maps a Task's declared workspace to a Volume. -// 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 345506822a5..02fed2bbe1a 100644 --- a/pkg/apis/pipeline/v1alpha1/workspace_validation_test.go +++ b/pkg/apis/pipeline/v1alpha1/workspace_validation_test.go @@ -51,6 +51,14 @@ func TestWorkspaceBindingValidateValid(t *testing.T) { }, }, }, + }, { + name: "Valid secret", + binding: &WorkspaceBinding{ + Name: "beth", + Secret: &corev1.SecretVolumeSource{ + SecretName: "my-secret", + }, + }, }} { t.Run(tc.name, func(t *testing.T) { if err := tc.binding.Validate(context.Background()); err != nil { @@ -77,6 +85,30 @@ func TestWorkspaceBindingValidateInvalid(t *testing.T) { ClaimName: "pool-party", }, }, + }, { + name: "Provided both emptydir and configmap", + binding: &WorkspaceBinding{ + Name: "beth", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "foo-configmap", + }, + }, + }, + }, { + name: "Provided both configmap and secret", + binding: &WorkspaceBinding{ + Name: "beth", + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "my-configmap", + }, + }, + Secret: &corev1.SecretVolumeSource{ + SecretName: "my-secret", + }, + }, }, { name: "Provided neither pvc nor emptydir", binding: &WorkspaceBinding{ @@ -94,6 +126,12 @@ func TestWorkspaceBindingValidateInvalid(t *testing.T) { Name: "beth", ConfigMap: &corev1.ConfigMapVolumeSource{}, }, + }, { + name: "Provide secret without a secretName", + binding: &WorkspaceBinding{ + Name: "beth", + Secret: &corev1.SecretVolumeSource{}, + }, }} { 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 5acd43598f4..175649b414a 100644 --- a/pkg/apis/pipeline/v1alpha2/workspace_types.go +++ b/pkg/apis/pipeline/v1alpha2/workspace_types.go @@ -48,7 +48,6 @@ func (w *WorkspaceDeclaration) GetMountPath() string { } // 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"` @@ -68,4 +67,7 @@ type WorkspaceBinding struct { // ConfigMap represents a configMap that should populate this workspace. // +optional ConfigMap *corev1.ConfigMapVolumeSource `json:"configMap,omitempty"` + // Secret represents a secret that should populate this workspace. + // +optional + Secret *corev1.SecretVolumeSource `json:"secret,omitempty"` } diff --git a/pkg/apis/pipeline/v1alpha2/workspace_validation.go b/pkg/apis/pipeline/v1alpha2/workspace_validation.go index 1e1452890c0..61778dc0dc0 100644 --- a/pkg/apis/pipeline/v1alpha2/workspace_validation.go +++ b/pkg/apis/pipeline/v1alpha2/workspace_validation.go @@ -29,6 +29,7 @@ var allVolumeSourceFields []string = []string{ "workspace.persistentvolumeclaim", "workspace.emptydir", "workspace.configmap", + "workspace.secret", } // Validate looks at the Volume provided in wb and makes sure that it is valid. @@ -59,6 +60,11 @@ func (b *WorkspaceBinding) Validate(ctx context.Context) *apis.FieldError { return apis.ErrMissingField("workspace.configmap.name") } + // For a Secret to work, you must provide the name of the Secret to use. + if b.Secret != nil && b.Secret.SecretName == "" { + return apis.ErrMissingField("workspace.secret.secretName") + } + return nil } @@ -75,5 +81,8 @@ func (b *WorkspaceBinding) numSources() int { if b.ConfigMap != nil { n++ } + if b.Secret != 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 8c790c0f54d..f8ee5c1162b 100644 --- a/pkg/apis/pipeline/v1alpha2/workspace_validation_test.go +++ b/pkg/apis/pipeline/v1alpha2/workspace_validation_test.go @@ -51,6 +51,14 @@ func TestWorkspaceBindingValidateValid(t *testing.T) { }, }, }, + }, { + name: "Valid secret", + binding: &WorkspaceBinding{ + Name: "beth", + Secret: &corev1.SecretVolumeSource{ + SecretName: "my-secret", + }, + }, }} { t.Run(tc.name, func(t *testing.T) { if err := tc.binding.Validate(context.Background()); err != nil { @@ -94,6 +102,12 @@ func TestWorkspaceBindingValidateInvalid(t *testing.T) { Name: "beth", ConfigMap: &corev1.ConfigMapVolumeSource{}, }, + }, { + name: "Provide secret without a secretName", + binding: &WorkspaceBinding{ + Name: "beth", + Secret: &corev1.SecretVolumeSource{}, + }, }} { 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 670ef011e47..79b4271ec58 100644 --- a/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go @@ -318,6 +318,11 @@ func (in *WorkspaceBinding) DeepCopyInto(out *WorkspaceBinding) { *out = new(v1.ConfigMapVolumeSource) (*in).DeepCopyInto(*out) } + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(v1.SecretVolumeSource) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/workspace/apply.go b/pkg/workspace/apply.go index 17393ae64fc..a6188695b8b 100644 --- a/pkg/workspace/apply.go +++ b/pkg/workspace/apply.go @@ -12,12 +12,23 @@ const ( volumeNameBase = "ws" ) -// GetVolumes will return a dictionary where the keys are the names fo the workspaces bound in +// nameVolumeMap is a map from a workspace's name to its Volume. +type nameVolumeMap map[string]corev1.Volume + +// setVolumeSource assigns a volume to a workspace's name. +func (nvm nameVolumeMap) setVolumeSource(workspaceName string, volumeName string, source corev1.VolumeSource) { + nvm[workspaceName] = corev1.Volume{ + Name: volumeName, + VolumeSource: source, + } +} + +// GetVolumes will return a dictionary where the keys are the names of the workspaces bound in // wb and the value is the Volume to use. If the same Volume is bound twice, the resulting volumes -// will both have the same name to prevent the same Volume from being attached to pod twice. +// will both have the same name to prevent the same Volume from being attached to a pod twice. func GetVolumes(wb []v1alpha1.WorkspaceBinding) map[string]corev1.Volume { pvcs := map[string]corev1.Volume{} - v := map[string]corev1.Volume{} + v := make(nameVolumeMap) for _, w := range wb { name := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(volumeNameBase) switch { @@ -27,30 +38,18 @@ func GetVolumes(wb []v1alpha1.WorkspaceBinding) map[string]corev1.Volume { v[w.Name] = vv } else { pvc := *w.PersistentVolumeClaim - v[w.Name] = corev1.Volume{ - Name: name, - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &pvc, - }, - } + v.setVolumeSource(w.Name, name, corev1.VolumeSource{PersistentVolumeClaim: &pvc}) pvcs[pvc.ClaimName] = v[w.Name] } case w.EmptyDir != nil: ed := *w.EmptyDir - v[w.Name] = corev1.Volume{ - Name: name, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &ed, - }, - } + v.setVolumeSource(w.Name, name, corev1.VolumeSource{EmptyDir: &ed}) case w.ConfigMap != nil: cm := *w.ConfigMap - v[w.Name] = corev1.Volume{ - Name: name, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &cm, - }, - } + v.setVolumeSource(w.Name, name, corev1.VolumeSource{ConfigMap: &cm}) + case w.Secret != nil: + s := *w.Secret + v.setVolumeSource(w.Name, name, corev1.VolumeSource{Secret: &s}) } } return v diff --git a/pkg/workspace/apply_test.go b/pkg/workspace/apply_test.go index 487cadaf788..983f0f4a8c2 100644 --- a/pkg/workspace/apply_test.go +++ b/pkg/workspace/apply_test.go @@ -85,6 +85,33 @@ func TestGetVolumes(t *testing.T) { }, }, }, + }, { + name: "binding a single workspace with secret", + workspaces: []v1alpha1.WorkspaceBinding{{ + Name: "custom", + Secret: &corev1.SecretVolumeSource{ + SecretName: "foobarsecret", + Items: []corev1.KeyToPath{{ + Key: "foobar", + Path: "foobar.txt", + }}, + }, + SubPath: "/foo/bar/baz", + }}, + expectedVolumes: map[string]corev1.Volume{ + "custom": { + Name: "ws-78c5n", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "foobarsecret", + Items: []corev1.KeyToPath{{ + Key: "foobar", + Path: "foobar.txt", + }}, + }, + }, + }, + }, }, { name: "0 workspace bindings", workspaces: []v1alpha1.WorkspaceBinding{}, @@ -105,7 +132,7 @@ func TestGetVolumes(t *testing.T) { }}, expectedVolumes: map[string]corev1.Volume{ "custom": { - Name: "ws-78c5n", + Name: "ws-6nl7g", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: "mypvc", @@ -113,7 +140,7 @@ func TestGetVolumes(t *testing.T) { }, }, "even-more-custom": { - Name: "ws-6nl7g", + Name: "ws-j2tds", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: "myotherpvc", @@ -138,7 +165,7 @@ func TestGetVolumes(t *testing.T) { }}, expectedVolumes: map[string]corev1.Volume{ "custom": { - Name: "ws-j2tds", + Name: "ws-vr6ds", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: "mypvc", @@ -147,7 +174,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-j2tds", + Name: "ws-vr6ds", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: "mypvc",