Browse code

secrets: use explicit format when using secrets

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

Evan Hazlett authored on 2016/11/02 11:28:32
Showing 9 changed files
... ...
@@ -39,6 +39,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
39 39
 	flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service")
40 40
 	flags.Var(&opts.constraints, flagConstraint, "Placement constraints")
41 41
 	flags.Var(&opts.networks, flagNetwork, "Network attachments")
42
+	flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service")
42 43
 	flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port")
43 44
 	flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container")
44 45
 	flags.Var(&opts.dns, flagDNS, "Set custom DNS servers")
... ...
@@ -59,7 +60,7 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
59 59
 	}
60 60
 
61 61
 	// parse and validate secrets
62
-	secrets, err := parseSecrets(apiClient, opts.secrets)
62
+	secrets, err := parseSecrets(apiClient, opts.secrets.Value())
63 63
 	if err != nil {
64 64
 		return err
65 65
 	}
... ...
@@ -1,7 +1,10 @@
1 1
 package service
2 2
 
3 3
 import (
4
+	"encoding/csv"
4 5
 	"fmt"
6
+	"os"
7
+	"path/filepath"
5 8
 	"strconv"
6 9
 	"strings"
7 10
 	"time"
... ...
@@ -139,6 +142,98 @@ func (f *floatValue) Value() float32 {
139 139
 	return float32(*f)
140 140
 }
141 141
 
142
+// SecretRequestSpec is a type for requesting secrets
143
+type SecretRequestSpec struct {
144
+	source string
145
+	target string
146
+	uid    string
147
+	gid    string
148
+	mode   os.FileMode
149
+}
150
+
151
+// SecretOpt is a Value type for parsing secrets
152
+type SecretOpt struct {
153
+	values []*SecretRequestSpec
154
+}
155
+
156
+// Set a new secret value
157
+func (o *SecretOpt) Set(value string) error {
158
+	csvReader := csv.NewReader(strings.NewReader(value))
159
+	fields, err := csvReader.Read()
160
+	if err != nil {
161
+		return err
162
+	}
163
+
164
+	spec := &SecretRequestSpec{
165
+		source: "",
166
+		target: "",
167
+		uid:    "0",
168
+		gid:    "0",
169
+		mode:   0444,
170
+	}
171
+
172
+	for _, field := range fields {
173
+		parts := strings.SplitN(field, "=", 2)
174
+		key := strings.ToLower(parts[0])
175
+
176
+		if len(parts) != 2 {
177
+			return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
178
+		}
179
+
180
+		value := parts[1]
181
+		switch key {
182
+		case "source":
183
+			spec.source = value
184
+		case "target":
185
+			tDir, _ := filepath.Split(value)
186
+			if tDir != "" {
187
+				return fmt.Errorf("target must not have a path")
188
+			}
189
+			spec.target = value
190
+		case "uid":
191
+			spec.uid = value
192
+		case "gid":
193
+			spec.gid = value
194
+		case "mode":
195
+			m, err := strconv.ParseUint(value, 0, 32)
196
+			if err != nil {
197
+				return fmt.Errorf("invalid mode specified: %v", err)
198
+			}
199
+
200
+			spec.mode = os.FileMode(m)
201
+		default:
202
+			return fmt.Errorf("invalid field in secret request: %s", key)
203
+		}
204
+	}
205
+
206
+	if spec.source == "" {
207
+		return fmt.Errorf("source is required")
208
+	}
209
+
210
+	o.values = append(o.values, spec)
211
+	return nil
212
+}
213
+
214
+// Type returns the type of this option
215
+func (o *SecretOpt) Type() string {
216
+	return "secret"
217
+}
218
+
219
+// String returns a string repr of this option
220
+func (o *SecretOpt) String() string {
221
+	secrets := []string{}
222
+	for _, secret := range o.values {
223
+		repr := fmt.Sprintf("%s -> %s", secret.source, secret.target)
224
+		secrets = append(secrets, repr)
225
+	}
226
+	return strings.Join(secrets, ", ")
227
+}
228
+
229
+// Value returns the secret requests
230
+func (o *SecretOpt) Value() []*SecretRequestSpec {
231
+	return o.values
232
+}
233
+
142 234
 type updateOptions struct {
143 235
 	parallelism     uint64
144 236
 	delay           time.Duration
... ...
@@ -337,7 +432,7 @@ type serviceOptions struct {
337 337
 	logDriver logDriverOptions
338 338
 
339 339
 	healthcheck healthCheckOptions
340
-	secrets     []string
340
+	secrets     SecretOpt
341 341
 }
342 342
 
343 343
 func newServiceOptions() *serviceOptions {
... ...
@@ -1,6 +1,7 @@
1 1
 package service
2 2
 
3 3
 import (
4
+	"os"
4 5
 	"reflect"
5 6
 	"testing"
6 7
 	"time"
... ...
@@ -105,3 +106,47 @@ func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) {
105 105
 	_, err := opt.toHealthConfig()
106 106
 	assert.Error(t, err, "--no-healthcheck conflicts with --health-* options")
107 107
 }
108
+
109
+func TestSecretOptionsSimple(t *testing.T) {
110
+	var opt SecretOpt
111
+
112
+	testCase := "source=/foo,target=testing"
113
+	assert.NilError(t, opt.Set(testCase))
114
+
115
+	reqs := opt.Value()
116
+	assert.Equal(t, len(reqs), 1)
117
+	req := reqs[0]
118
+	assert.Equal(t, req.source, "/foo")
119
+	assert.Equal(t, req.target, "testing")
120
+}
121
+
122
+func TestSecretOptionsCustomUidGid(t *testing.T) {
123
+	var opt SecretOpt
124
+
125
+	testCase := "source=/foo,target=testing,uid=1000,gid=1001"
126
+	assert.NilError(t, opt.Set(testCase))
127
+
128
+	reqs := opt.Value()
129
+	assert.Equal(t, len(reqs), 1)
130
+	req := reqs[0]
131
+	assert.Equal(t, req.source, "/foo")
132
+	assert.Equal(t, req.target, "testing")
133
+	assert.Equal(t, req.uid, "1000")
134
+	assert.Equal(t, req.gid, "1001")
135
+}
136
+
137
+func TestSecretOptionsCustomMode(t *testing.T) {
138
+	var opt SecretOpt
139
+
140
+	testCase := "source=/foo,target=testing,uid=1000,gid=1001,mode=0444"
141
+	assert.NilError(t, opt.Set(testCase))
142
+
143
+	reqs := opt.Value()
144
+	assert.Equal(t, len(reqs), 1)
145
+	req := reqs[0]
146
+	assert.Equal(t, req.source, "/foo")
147
+	assert.Equal(t, req.target, "testing")
148
+	assert.Equal(t, req.uid, "1000")
149
+	assert.Equal(t, req.gid, "1001")
150
+	assert.Equal(t, req.mode, os.FileMode(0444))
151
+}
... ...
@@ -3,8 +3,6 @@ package service
3 3
 import (
4 4
 	"context"
5 5
 	"fmt"
6
-	"path/filepath"
7
-	"strings"
8 6
 
9 7
 	"github.com/docker/docker/api/types"
10 8
 	"github.com/docker/docker/api/types/filters"
... ...
@@ -12,61 +10,27 @@ import (
12 12
 	"github.com/docker/docker/client"
13 13
 )
14 14
 
15
-// parseSecretString parses the requested secret and returns the secret name
16
-// and target.  Expects format SECRET_NAME:TARGET
17
-func parseSecretString(secretString string) (string, string, error) {
18
-	tokens := strings.Split(secretString, ":")
19
-
20
-	secretName := strings.TrimSpace(tokens[0])
21
-	targetName := secretName
22
-
23
-	if secretName == "" {
24
-		return "", "", fmt.Errorf("invalid secret name provided")
25
-	}
26
-
27
-	if len(tokens) > 1 {
28
-		targetName = strings.TrimSpace(tokens[1])
29
-		if targetName == "" {
30
-			return "", "", fmt.Errorf("invalid presentation name provided")
31
-		}
32
-	}
33
-
34
-	// ensure target is a filename only; no paths allowed
35
-	tDir, _ := filepath.Split(targetName)
36
-	if tDir != "" {
37
-		return "", "", fmt.Errorf("target must not have a path")
38
-	}
39
-
40
-	return secretName, targetName, nil
41
-}
42
-
43 15
 // parseSecrets retrieves the secrets from the requested names and converts
44 16
 // them to secret references to use with the spec
45
-func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) {
17
+func parseSecrets(client client.APIClient, requestedSecrets []*SecretRequestSpec) ([]*swarmtypes.SecretReference, error) {
46 18
 	secretRefs := make(map[string]*swarmtypes.SecretReference)
47 19
 	ctx := context.Background()
48 20
 
49 21
 	for _, secret := range requestedSecrets {
50
-		n, t, err := parseSecretString(secret)
51
-		if err != nil {
52
-			return nil, err
53
-		}
54
-
55 22
 		secretRef := &swarmtypes.SecretReference{
56
-			SecretName: n,
57
-			// TODO (ehazlett): parse these from cli request
23
+			SecretName: secret.source,
58 24
 			Target: swarmtypes.SecretReferenceFileTarget{
59
-				Name: t,
60
-				UID:  "0",
61
-				GID:  "0",
62
-				Mode: 0444,
25
+				Name: secret.target,
26
+				UID:  secret.uid,
27
+				GID:  secret.gid,
28
+				Mode: secret.mode,
63 29
 			},
64 30
 		}
65 31
 
66
-		if _, exists := secretRefs[t]; exists {
67
-			return nil, fmt.Errorf("duplicate secret target for %s not allowed", n)
32
+		if _, exists := secretRefs[secret.target]; exists {
33
+			return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.source)
68 34
 		}
69
-		secretRefs[t] = secretRef
35
+		secretRefs[secret.target] = secretRef
70 36
 	}
71 37
 
72 38
 	args := filters.NewArgs()
... ...
@@ -56,7 +56,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
56 56
 	flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label")
57 57
 	flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable")
58 58
 	flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret")
59
-	flags.StringSliceVar(&opts.secrets, flagSecretAdd, []string{}, "Add a secret")
59
+	flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service")
60 60
 	flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service")
61 61
 	flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint")
62 62
 	flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port")
... ...
@@ -413,10 +413,7 @@ func updateEnvironment(flags *pflag.FlagSet, field *[]string) {
413 413
 
414 414
 func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) {
415 415
 	if flags.Changed(flagSecretAdd) {
416
-		values, err := flags.GetStringSlice(flagSecretAdd)
417
-		if err != nil {
418
-			return nil, err
419
-		}
416
+		values := flags.Lookup(flagSecretAdd).Value.(*SecretOpt).Value()
420 417
 
421 418
 		addSecrets, err := parseSecrets(apiClient, values)
422 419
 		if err != nil {
... ...
@@ -126,7 +126,7 @@ Use the `--secret` flag to give a container access to a
126 126
 with two secrets named `ssh-key` and `app-key`:
127 127
 
128 128
 ```bash
129
-$ docker service create --name redis --secret ssh-key:ssh --secret app-key:app redis:3.0.6
129
+$ docker service create --name redis --secret source=ssh-key,target=ssh --secret source=app-key,target=app,uid=1000,gid=1001,mode=0400 redis:3.0.6
130 130
 4cdgfyky7ozwh3htjfw0d12qv
131 131
 ```
132 132
 
... ...
@@ -157,7 +157,7 @@ The following example adds a secret named `ssh-2` and removes `ssh-1`:
157 157
 
158 158
 ```bash
159 159
 $ docker service update \
160
-    --secret-add ssh-2 \
160
+    --secret-add source=ssh-2,target=ssh-2 \
161 161
     --secret-rm ssh-1 \
162 162
     myservice
163 163
 ```
... ...
@@ -59,7 +59,7 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecret(c *check.C) {
59 59
 	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
60 60
 	testTarget := "testing"
61 61
 
62
-	out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", fmt.Sprintf("%s:%s", testName, testTarget), "busybox", "top")
62
+	out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", fmt.Sprintf("source=%s,target=%s", testName, testTarget), "busybox", "top")
63 63
 	c.Assert(err, checker.IsNil, check.Commentf(out))
64 64
 
65 65
 	out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)
... ...
@@ -103,7 +103,7 @@ func (s *DockerSwarmSuite) TestServiceUpdateSecrets(c *check.C) {
103 103
 	c.Assert(err, checker.IsNil, check.Commentf(out))
104 104
 
105 105
 	// add secret
106
-	out, err = d.Cmd("service", "update", "test", "--secret-add", fmt.Sprintf("%s:%s", testName, testTarget))
106
+	out, err = d.Cmd("service", "update", "test", "--secret-add", fmt.Sprintf("source=%s,target=%s", testName, testTarget))
107 107
 	c.Assert(err, checker.IsNil, check.Commentf(out))
108 108
 
109 109
 	out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)