Browse code

secrets: secret management for swarm

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

wip: use tmpfs for swarm secrets

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

wip: inject secrets from swarm secret store

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

secrets: use secret names in cli for service create

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

switch to use mounts instead of volumes

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

vendor: use ehazlett swarmkit

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

secrets: finish secret update

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

Evan Hazlett authored on 2016/10/20 01:22:02
Showing 46 changed files
... ...
@@ -23,4 +23,9 @@ type Backend interface {
23 23
 	RemoveNode(string, bool) error
24 24
 	GetTasks(basictypes.TaskListOptions) ([]types.Task, error)
25 25
 	GetTask(string) (types.Task, error)
26
+	GetSecrets(opts basictypes.SecretListOptions) ([]types.Secret, error)
27
+	CreateSecret(s types.SecretSpec) (string, error)
28
+	RemoveSecret(id string) error
29
+	GetSecret(id string) (types.Secret, error)
30
+	UpdateSecret(id string, version uint64, spec types.SecretSpec) error
26 31
 }
... ...
@@ -40,5 +40,10 @@ func (sr *swarmRouter) initRoutes() {
40 40
 		router.NewPostRoute("/nodes/{id:.*}/update", sr.updateNode),
41 41
 		router.NewGetRoute("/tasks", sr.getTasks),
42 42
 		router.NewGetRoute("/tasks/{id:.*}", sr.getTask),
43
+		router.NewGetRoute("/secrets", sr.getSecrets),
44
+		router.NewPostRoute("/secrets/create", sr.createSecret),
45
+		router.NewDeleteRoute("/secrets/{id:.*}", sr.removeSecret),
46
+		router.NewGetRoute("/secrets/{id:.*}", sr.getSecret),
47
+		router.NewPostRoute("/secrets/{id:.*}/update", sr.updateSecret),
43 48
 	}
44 49
 }
... ...
@@ -261,3 +261,77 @@ func (sr *swarmRouter) getTask(ctx context.Context, w http.ResponseWriter, r *ht
261 261
 
262 262
 	return httputils.WriteJSON(w, http.StatusOK, task)
263 263
 }
264
+
265
+func (sr *swarmRouter) getSecrets(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
266
+	if err := httputils.ParseForm(r); err != nil {
267
+		return err
268
+	}
269
+	filter, err := filters.FromParam(r.Form.Get("filters"))
270
+	if err != nil {
271
+		return err
272
+	}
273
+
274
+	secrets, err := sr.backend.GetSecrets(basictypes.SecretListOptions{Filter: filter})
275
+	if err != nil {
276
+		logrus.Errorf("Error getting secrets: %v", err)
277
+		return err
278
+	}
279
+
280
+	return httputils.WriteJSON(w, http.StatusOK, secrets)
281
+}
282
+
283
+func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
284
+	var secret types.SecretSpec
285
+	if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
286
+		return err
287
+	}
288
+
289
+	id, err := sr.backend.CreateSecret(secret)
290
+	if err != nil {
291
+		logrus.Errorf("Error creating secret %s: %v", id, err)
292
+		return err
293
+	}
294
+
295
+	return httputils.WriteJSON(w, http.StatusCreated, &basictypes.SecretCreateResponse{
296
+		ID: id,
297
+	})
298
+}
299
+
300
+func (sr *swarmRouter) removeSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
301
+	if err := sr.backend.RemoveSecret(vars["id"]); err != nil {
302
+		logrus.Errorf("Error removing secret %s: %v", vars["id"], err)
303
+		return err
304
+	}
305
+
306
+	return nil
307
+}
308
+
309
+func (sr *swarmRouter) getSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
310
+	secret, err := sr.backend.GetSecret(vars["id"])
311
+	if err != nil {
312
+		logrus.Errorf("Error getting secret %s: %v", vars["id"], err)
313
+		return err
314
+	}
315
+
316
+	return httputils.WriteJSON(w, http.StatusOK, secret)
317
+}
318
+
319
+func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
320
+	var secret types.SecretSpec
321
+	if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
322
+		return err
323
+	}
324
+
325
+	rawVersion := r.URL.Query().Get("version")
326
+	version, err := strconv.ParseUint(rawVersion, 10, 64)
327
+	if err != nil {
328
+		return fmt.Errorf("Invalid secret version '%s': %s", rawVersion, err.Error())
329
+	}
330
+
331
+	id := vars["id"]
332
+	if err := sr.backend.UpdateSecret(id, version, secret); err != nil {
333
+		return fmt.Errorf("Error updating secret: %s", err)
334
+	}
335
+
336
+	return nil
337
+}
264 338
new file mode 100644
... ...
@@ -0,0 +1,12 @@
0
+package container
1
+
2
+import "os"
3
+
4
+type ContainerSecret struct {
5
+	Name   string
6
+	Target string
7
+	Data   []byte
8
+	Uid    int
9
+	Gid    int
10
+	Mode   os.FileMode
11
+}
... ...
@@ -37,4 +37,5 @@ type ContainerSpec struct {
37 37
 	StopGracePeriod *time.Duration          `json:",omitempty"`
38 38
 	Healthcheck     *container.HealthConfig `json:",omitempty"`
39 39
 	DNSConfig       *DNSConfig              `json:",omitempty"`
40
+	Secrets         []*SecretReference      `json:",omitempty"`
40 41
 }
41 42
new file mode 100644
... ...
@@ -0,0 +1,30 @@
0
+package swarm
1
+
2
+// Secret represents a secret.
3
+type Secret struct {
4
+	ID string
5
+	Meta
6
+	Spec       *SecretSpec `json:",omitempty"`
7
+	Digest     string      `json:",omitempty"`
8
+	SecretSize int64       `json:",omitempty"`
9
+}
10
+
11
+type SecretSpec struct {
12
+	Annotations
13
+	Data []byte `json",omitempty"`
14
+}
15
+
16
+type SecretReferenceMode int
17
+
18
+const (
19
+	SecretReferenceSystem SecretReferenceMode = 0
20
+	SecretReferenceFile   SecretReferenceMode = 1
21
+	SecretReferenceEnv    SecretReferenceMode = 2
22
+)
23
+
24
+type SecretReference struct {
25
+	SecretID   string              `json:",omitempty"`
26
+	Mode       SecretReferenceMode `json:",omitempty"`
27
+	Target     string              `json:",omitempty"`
28
+	SecretName string              `json:",omitempty"`
29
+}
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"time"
7 7
 
8 8
 	"github.com/docker/docker/api/types/container"
9
+	"github.com/docker/docker/api/types/filters"
9 10
 	"github.com/docker/docker/api/types/mount"
10 11
 	"github.com/docker/docker/api/types/network"
11 12
 	"github.com/docker/docker/api/types/registry"
... ...
@@ -509,3 +510,15 @@ type ImagesPruneReport struct {
509 509
 type NetworksPruneReport struct {
510 510
 	NetworksDeleted []string
511 511
 }
512
+
513
+// SecretCreateResponse contains the information returned to a client
514
+// on the creation of a new secret.
515
+type SecretCreateResponse struct {
516
+	// ID is the id of the created secret.
517
+	ID string
518
+}
519
+
520
+// SecretListOptions holds parameters to list secrets
521
+type SecretListOptions struct {
522
+	Filter filters.Args
523
+}
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	"github.com/docker/docker/cli/command/node"
12 12
 	"github.com/docker/docker/cli/command/plugin"
13 13
 	"github.com/docker/docker/cli/command/registry"
14
+	"github.com/docker/docker/cli/command/secret"
14 15
 	"github.com/docker/docker/cli/command/service"
15 16
 	"github.com/docker/docker/cli/command/stack"
16 17
 	"github.com/docker/docker/cli/command/swarm"
... ...
@@ -25,6 +26,7 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
25 25
 		node.NewNodeCommand(dockerCli),
26 26
 		service.NewServiceCommand(dockerCli),
27 27
 		swarm.NewSwarmCommand(dockerCli),
28
+		secret.NewSecretCommand(dockerCli),
28 29
 		container.NewContainerCommand(dockerCli),
29 30
 		image.NewImageCommand(dockerCli),
30 31
 		system.NewSystemCommand(dockerCli),
31 32
new file mode 100644
... ...
@@ -0,0 +1,29 @@
0
+package secret
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/spf13/cobra"
6
+
7
+	"github.com/docker/docker/cli"
8
+	"github.com/docker/docker/cli/command"
9
+)
10
+
11
+// NewSecretCommand returns a cobra command for `secret` subcommands
12
+func NewSecretCommand(dockerCli *command.DockerCli) *cobra.Command {
13
+	cmd := &cobra.Command{
14
+		Use:   "secret",
15
+		Short: "Manage Docker secrets",
16
+		Args:  cli.NoArgs,
17
+		Run: func(cmd *cobra.Command, args []string) {
18
+			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
19
+		},
20
+	}
21
+	cmd.AddCommand(
22
+		newSecretListCommand(dockerCli),
23
+		newSecretCreateCommand(dockerCli),
24
+		newSecretInspectCommand(dockerCli),
25
+		newSecretRemoveCommand(dockerCli),
26
+	)
27
+	return cmd
28
+}
0 29
new file mode 100644
... ...
@@ -0,0 +1,57 @@
0
+package secret
1
+
2
+import (
3
+	"context"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"os"
7
+
8
+	"github.com/docker/docker/api/types/swarm"
9
+	"github.com/docker/docker/cli"
10
+	"github.com/docker/docker/cli/command"
11
+	"github.com/spf13/cobra"
12
+)
13
+
14
+type createOptions struct {
15
+	name string
16
+}
17
+
18
+func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
19
+	return &cobra.Command{
20
+		Use:   "create [name]",
21
+		Short: "Create a secret using stdin as content",
22
+		Args:  cli.ExactArgs(1),
23
+		RunE: func(cmd *cobra.Command, args []string) error {
24
+			opts := createOptions{
25
+				name: args[0],
26
+			}
27
+
28
+			return runSecretCreate(dockerCli, opts)
29
+		},
30
+	}
31
+}
32
+
33
+func runSecretCreate(dockerCli *command.DockerCli, opts createOptions) error {
34
+	client := dockerCli.Client()
35
+	ctx := context.Background()
36
+
37
+	secretData, err := ioutil.ReadAll(os.Stdin)
38
+	if err != nil {
39
+		return fmt.Errorf("Error reading content from STDIN: %v", err)
40
+	}
41
+
42
+	spec := swarm.SecretSpec{
43
+		Annotations: swarm.Annotations{
44
+			Name: opts.name,
45
+		},
46
+		Data: secretData,
47
+	}
48
+
49
+	r, err := client.SecretCreate(ctx, spec)
50
+	if err != nil {
51
+		return err
52
+	}
53
+
54
+	fmt.Fprintln(dockerCli.Out(), r.ID)
55
+	return nil
56
+}
0 57
new file mode 100644
... ...
@@ -0,0 +1,42 @@
0
+package secret
1
+
2
+import (
3
+	"context"
4
+
5
+	"github.com/docker/docker/cli"
6
+	"github.com/docker/docker/cli/command"
7
+	"github.com/docker/docker/cli/command/inspect"
8
+	"github.com/spf13/cobra"
9
+)
10
+
11
+type inspectOptions struct {
12
+	name   string
13
+	format string
14
+}
15
+
16
+func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
17
+	opts := inspectOptions{}
18
+	cmd := &cobra.Command{
19
+		Use:   "inspect [name]",
20
+		Short: "Inspect a secret",
21
+		Args:  cli.ExactArgs(1),
22
+		RunE: func(cmd *cobra.Command, args []string) error {
23
+			opts.name = args[0]
24
+			return runSecretInspect(dockerCli, opts)
25
+		},
26
+	}
27
+
28
+	cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
29
+	return cmd
30
+}
31
+
32
+func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
33
+	client := dockerCli.Client()
34
+	ctx := context.Background()
35
+
36
+	getRef := func(name string) (interface{}, []byte, error) {
37
+		return client.SecretInspectWithRaw(ctx, name)
38
+	}
39
+
40
+	return inspect.Inspect(dockerCli.Out(), []string{opts.name}, opts.format, getRef)
41
+}
0 42
new file mode 100644
... ...
@@ -0,0 +1,62 @@
0
+package secret
1
+
2
+import (
3
+	"context"
4
+	"fmt"
5
+	"text/tabwriter"
6
+
7
+	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/cli"
9
+	"github.com/docker/docker/cli/command"
10
+	"github.com/spf13/cobra"
11
+)
12
+
13
+type listOptions struct {
14
+	quiet bool
15
+}
16
+
17
+func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command {
18
+	opts := listOptions{}
19
+
20
+	cmd := &cobra.Command{
21
+		Use:   "ls",
22
+		Short: "List secrets",
23
+		Args:  cli.NoArgs,
24
+		RunE: func(cmd *cobra.Command, args []string) error {
25
+			return runSecretList(dockerCli, opts)
26
+		},
27
+	}
28
+
29
+	flags := cmd.Flags()
30
+	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
31
+
32
+	return cmd
33
+}
34
+
35
+func runSecretList(dockerCli *command.DockerCli, opts listOptions) error {
36
+	client := dockerCli.Client()
37
+	ctx := context.Background()
38
+
39
+	secrets, err := client.SecretList(ctx, types.SecretListOptions{})
40
+	if err != nil {
41
+		return err
42
+	}
43
+
44
+	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
45
+	if opts.quiet {
46
+		for _, s := range secrets {
47
+			fmt.Fprintf(w, "%s\n", s.ID)
48
+		}
49
+	} else {
50
+		fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED\tSIZE")
51
+		fmt.Fprintf(w, "\n")
52
+
53
+		for _, s := range secrets {
54
+			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", s.ID, s.Spec.Annotations.Name, s.Meta.CreatedAt, s.Meta.UpdatedAt, s.SecretSize)
55
+		}
56
+	}
57
+
58
+	w.Flush()
59
+
60
+	return nil
61
+}
0 62
new file mode 100644
... ...
@@ -0,0 +1,43 @@
0
+package secret
1
+
2
+import (
3
+	"context"
4
+	"fmt"
5
+
6
+	"github.com/docker/docker/cli"
7
+	"github.com/docker/docker/cli/command"
8
+	"github.com/spf13/cobra"
9
+)
10
+
11
+type removeOptions struct {
12
+	ids []string
13
+}
14
+
15
+func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
16
+	return &cobra.Command{
17
+		Use:   "rm [id]",
18
+		Short: "Remove a secret",
19
+		Args:  cli.RequiresMinArgs(1),
20
+		RunE: func(cmd *cobra.Command, args []string) error {
21
+			opts := removeOptions{
22
+				ids: args,
23
+			}
24
+			return runSecretRemove(dockerCli, opts)
25
+		},
26
+	}
27
+}
28
+
29
+func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error {
30
+	client := dockerCli.Client()
31
+	ctx := context.Background()
32
+
33
+	for _, id := range opts.ids {
34
+		if err := client.SecretRemove(ctx, id); err != nil {
35
+			return err
36
+		}
37
+
38
+		fmt.Fprintln(dockerCli.Out(), id)
39
+	}
40
+
41
+	return nil
42
+}
... ...
@@ -58,6 +58,13 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
58 58
 		return err
59 59
 	}
60 60
 
61
+	// parse and validate secrets
62
+	secrets, err := parseSecrets(apiClient, opts.secrets)
63
+	if err != nil {
64
+		return err
65
+	}
66
+	service.TaskTemplate.ContainerSpec.Secrets = secrets
67
+
61 68
 	ctx := context.Background()
62 69
 
63 70
 	// only send auth if flag was set
... ...
@@ -191,6 +191,19 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
191 191
 	return nets
192 192
 }
193 193
 
194
+func convertSecrets(secrets []string) []*swarm.SecretReference {
195
+	sec := []*swarm.SecretReference{}
196
+	for _, s := range secrets {
197
+		sec = append(sec, &swarm.SecretReference{
198
+			SecretID: s,
199
+			Mode:     swarm.SecretReferenceFile,
200
+			Target:   "",
201
+		})
202
+	}
203
+
204
+	return sec
205
+}
206
+
194 207
 type endpointOptions struct {
195 208
 	mode  string
196 209
 	ports opts.ListOpts
... ...
@@ -337,6 +350,7 @@ type serviceOptions struct {
337 337
 	logDriver logDriverOptions
338 338
 
339 339
 	healthcheck healthCheckOptions
340
+	secrets     []string
340 341
 }
341 342
 
342 343
 func newServiceOptions() *serviceOptions {
... ...
@@ -403,6 +417,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
403 403
 					Options:     opts.dnsOptions.GetAll(),
404 404
 				},
405 405
 				StopGracePeriod: opts.stopGrace.Value(),
406
+				Secrets:         convertSecrets(opts.secrets),
406 407
 			},
407 408
 			Networks:      convertNetworks(opts.networks.GetAll()),
408 409
 			Resources:     opts.resources.ToResourceRequirements(),
... ...
@@ -488,6 +503,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
488 488
 	flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK")
489 489
 
490 490
 	flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY")
491
+	flags.StringSliceVar(&opts.secrets, flagSecret, []string{}, "Specify secrets to expose to the service")
491 492
 }
492 493
 
493 494
 const (
... ...
@@ -553,4 +569,5 @@ const (
553 553
 	flagHealthRetries         = "health-retries"
554 554
 	flagHealthTimeout         = "health-timeout"
555 555
 	flagNoHealthcheck         = "no-healthcheck"
556
+	flagSecret                = "secret"
556 557
 )
557 558
new file mode 100644
... ...
@@ -0,0 +1,92 @@
0
+package service
1
+
2
+import (
3
+	"context"
4
+	"fmt"
5
+	"strings"
6
+
7
+	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
9
+	swarmtypes "github.com/docker/docker/api/types/swarm"
10
+	"github.com/docker/docker/client"
11
+)
12
+
13
+// parseSecretString parses the requested secret and returns the secret name
14
+// and target.  Expects format SECRET_NAME:TARGET
15
+func parseSecretString(secretString string) (string, string, error) {
16
+	tokens := strings.Split(secretString, ":")
17
+
18
+	secretName := strings.TrimSpace(tokens[0])
19
+	targetName := ""
20
+
21
+	if secretName == "" {
22
+		return "", "", fmt.Errorf("invalid secret name provided")
23
+	}
24
+
25
+	if len(tokens) > 1 {
26
+		targetName = strings.TrimSpace(tokens[1])
27
+		if targetName == "" {
28
+			return "", "", fmt.Errorf("invalid presentation name provided")
29
+		}
30
+	} else {
31
+		targetName = secretName
32
+	}
33
+	return secretName, targetName, nil
34
+}
35
+
36
+// parseSecrets retrieves the secrets from the requested names and converts
37
+// them to secret references to use with the spec
38
+func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) {
39
+	lookupSecretNames := []string{}
40
+	needSecrets := make(map[string]*swarmtypes.SecretReference)
41
+	ctx := context.Background()
42
+
43
+	for _, secret := range requestedSecrets {
44
+		n, t, err := parseSecretString(secret)
45
+		if err != nil {
46
+			return nil, err
47
+		}
48
+
49
+		secretRef := &swarmtypes.SecretReference{
50
+			SecretName: n,
51
+			Mode:       swarmtypes.SecretReferenceFile,
52
+			Target:     t,
53
+		}
54
+
55
+		lookupSecretNames = append(lookupSecretNames, n)
56
+		needSecrets[n] = secretRef
57
+	}
58
+
59
+	args := filters.NewArgs()
60
+	for _, s := range lookupSecretNames {
61
+		args.Add("names", s)
62
+	}
63
+
64
+	secrets, err := client.SecretList(ctx, types.SecretListOptions{
65
+		Filter: args,
66
+	})
67
+	if err != nil {
68
+		return nil, err
69
+	}
70
+
71
+	foundSecrets := make(map[string]*swarmtypes.Secret)
72
+	for _, secret := range secrets {
73
+		foundSecrets[secret.Spec.Annotations.Name] = &secret
74
+	}
75
+
76
+	addedSecrets := []*swarmtypes.SecretReference{}
77
+
78
+	for secretName, secretRef := range needSecrets {
79
+		s, ok := foundSecrets[secretName]
80
+		if !ok {
81
+			return nil, fmt.Errorf("secret not found: %s", secretName)
82
+		}
83
+
84
+		// set the id for the ref to properly assign in swarm
85
+		// since swarm needs the ID instead of the name
86
+		secretRef.SecretID = s.ID
87
+		addedSecrets = append(addedSecrets, secretRef)
88
+	}
89
+
90
+	return addedSecrets, nil
91
+}
... ...
@@ -217,3 +217,25 @@ func (cli *Client) NewVersionError(APIrequired, feature string) error {
217 217
 	}
218 218
 	return nil
219 219
 }
220
+
221
+// secretNotFoundError implements an error returned when a secret is not found.
222
+type secretNotFoundError struct {
223
+	name string
224
+}
225
+
226
+// Error returns a string representation of a secretNotFoundError
227
+func (e secretNotFoundError) Error() string {
228
+	return fmt.Sprintf("Error: No such secret: %s", e.name)
229
+}
230
+
231
+// NoFound indicates that this error type is of NotFound
232
+func (e secretNotFoundError) NotFound() bool {
233
+	return true
234
+}
235
+
236
+// IsErrSecretNotFound returns true if the error is caused
237
+// when a secret is not found.
238
+func IsErrSecretNotFound(err error) bool {
239
+	_, ok := err.(secretNotFoundError)
240
+	return ok
241
+}
... ...
@@ -23,6 +23,7 @@ type CommonAPIClient interface {
23 23
 	NetworkAPIClient
24 24
 	ServiceAPIClient
25 25
 	SwarmAPIClient
26
+	SecretAPIClient
26 27
 	SystemAPIClient
27 28
 	VolumeAPIClient
28 29
 	ClientVersion() string
... ...
@@ -141,3 +142,11 @@ type VolumeAPIClient interface {
141 141
 	VolumeRemove(ctx context.Context, volumeID string, force bool) error
142 142
 	VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error)
143 143
 }
144
+
145
+// SecretAPIClient defines API client methods for secrets
146
+type SecretAPIClient interface {
147
+	SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error)
148
+	SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error)
149
+	SecretRemove(ctx context.Context, id string) error
150
+	SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error)
151
+}
144 152
new file mode 100644
... ...
@@ -0,0 +1,24 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"github.com/docker/docker/api/types/swarm"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// SecretCreate creates a new Secret.
11
+func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) {
12
+	var headers map[string][]string
13
+
14
+	var response types.SecretCreateResponse
15
+	resp, err := cli.post(ctx, "/secrets/create", nil, secret, headers)
16
+	if err != nil {
17
+		return response, err
18
+	}
19
+
20
+	err = json.NewDecoder(resp.body).Decode(&response)
21
+	ensureReaderClosed(resp)
22
+	return response, err
23
+}
0 24
new file mode 100644
... ...
@@ -0,0 +1,57 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/docker/docker/api/types"
12
+	"github.com/docker/docker/api/types/swarm"
13
+	"golang.org/x/net/context"
14
+)
15
+
16
+func TestSecretCreateError(t *testing.T) {
17
+	client := &Client{
18
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+	_, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
21
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
22
+		t.Fatalf("expected a Server Error, got %v", err)
23
+	}
24
+}
25
+
26
+func TestSecretCreate(t *testing.T) {
27
+	expectedURL := "/secrets/create"
28
+	client := &Client{
29
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
30
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
31
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
32
+			}
33
+			if req.Method != "POST" {
34
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
35
+			}
36
+			b, err := json.Marshal(types.SecretCreateResponse{
37
+				ID: "test_secret",
38
+			})
39
+			if err != nil {
40
+				return nil, err
41
+			}
42
+			return &http.Response{
43
+				StatusCode: http.StatusOK,
44
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
45
+			}, nil
46
+		}),
47
+	}
48
+
49
+	r, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
50
+	if err != nil {
51
+		t.Fatal(err)
52
+	}
53
+	if r.ID != "test_secret" {
54
+		t.Fatalf("expected `test_secret`, got %s", r.ID)
55
+	}
56
+}
0 57
new file mode 100644
... ...
@@ -0,0 +1,34 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"io/ioutil"
6
+	"net/http"
7
+
8
+	"github.com/docker/docker/api/types/swarm"
9
+	"golang.org/x/net/context"
10
+)
11
+
12
+// SecretInspectWithRaw returns the secret information with raw data
13
+func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) {
14
+	resp, err := cli.get(ctx, "/secrets/"+id, nil, nil)
15
+	if err != nil {
16
+		if resp.statusCode == http.StatusNotFound {
17
+			return swarm.Secret{}, nil, secretNotFoundError{id}
18
+		}
19
+		return swarm.Secret{}, nil, err
20
+	}
21
+	defer ensureReaderClosed(resp)
22
+
23
+	body, err := ioutil.ReadAll(resp.body)
24
+	if err != nil {
25
+		return swarm.Secret{}, nil, err
26
+	}
27
+
28
+	var secret swarm.Secret
29
+	rdr := bytes.NewReader(body)
30
+	err = json.NewDecoder(rdr).Decode(&secret)
31
+
32
+	return secret, body, err
33
+}
0 34
new file mode 100644
... ...
@@ -0,0 +1,65 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/docker/docker/api/types/swarm"
12
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestSecretInspectError(t *testing.T) {
16
+	client := &Client{
17
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, _, err := client.SecretInspectWithRaw(context.Background(), "nothing")
21
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
22
+		t.Fatalf("expected a Server Error, got %v", err)
23
+	}
24
+}
25
+
26
+func TestSecretInspectSecretNotFound(t *testing.T) {
27
+	client := &Client{
28
+		client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
29
+	}
30
+
31
+	_, _, err := client.SecretInspectWithRaw(context.Background(), "unknown")
32
+	if err == nil || !IsErrSecretNotFound(err) {
33
+		t.Fatalf("expected an secretNotFoundError error, got %v", err)
34
+	}
35
+}
36
+
37
+func TestSecretInspect(t *testing.T) {
38
+	expectedURL := "/secrets/secret_id"
39
+	client := &Client{
40
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
41
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
42
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
43
+			}
44
+			content, err := json.Marshal(swarm.Secret{
45
+				ID: "secret_id",
46
+			})
47
+			if err != nil {
48
+				return nil, err
49
+			}
50
+			return &http.Response{
51
+				StatusCode: http.StatusOK,
52
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
53
+			}, nil
54
+		}),
55
+	}
56
+
57
+	secretInspect, _, err := client.SecretInspectWithRaw(context.Background(), "secret_id")
58
+	if err != nil {
59
+		t.Fatal(err)
60
+	}
61
+	if secretInspect.ID != "secret_id" {
62
+		t.Fatalf("expected `secret_id`, got %s", secretInspect.ID)
63
+	}
64
+}
0 65
new file mode 100644
... ...
@@ -0,0 +1,35 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/url"
5
+
6
+	"github.com/docker/docker/api/types"
7
+	"github.com/docker/docker/api/types/filters"
8
+	"github.com/docker/docker/api/types/swarm"
9
+	"golang.org/x/net/context"
10
+)
11
+
12
+// SecretList returns the list of secrets.
13
+func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
14
+	query := url.Values{}
15
+
16
+	if options.Filter.Len() > 0 {
17
+		filterJSON, err := filters.ToParam(options.Filter)
18
+		if err != nil {
19
+			return nil, err
20
+		}
21
+
22
+		query.Set("filters", filterJSON)
23
+	}
24
+
25
+	resp, err := cli.get(ctx, "/secrets", query, nil)
26
+	if err != nil {
27
+		return nil, err
28
+	}
29
+
30
+	var secrets []swarm.Secret
31
+	err = json.NewDecoder(resp.body).Decode(&secrets)
32
+	ensureReaderClosed(resp)
33
+	return secrets, err
34
+}
0 35
new file mode 100644
... ...
@@ -0,0 +1,94 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/docker/docker/api/types"
12
+	"github.com/docker/docker/api/types/filters"
13
+	"github.com/docker/docker/api/types/swarm"
14
+	"golang.org/x/net/context"
15
+)
16
+
17
+func TestSecretListError(t *testing.T) {
18
+	client := &Client{
19
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
20
+	}
21
+
22
+	_, err := client.SecretList(context.Background(), types.SecretListOptions{})
23
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
24
+		t.Fatalf("expected a Server Error, got %v", err)
25
+	}
26
+}
27
+
28
+func TestSecretList(t *testing.T) {
29
+	expectedURL := "/secrets"
30
+
31
+	filters := filters.NewArgs()
32
+	filters.Add("label", "label1")
33
+	filters.Add("label", "label2")
34
+
35
+	listCases := []struct {
36
+		options             types.SecretListOptions
37
+		expectedQueryParams map[string]string
38
+	}{
39
+		{
40
+			options: types.SecretListOptions{},
41
+			expectedQueryParams: map[string]string{
42
+				"filters": "",
43
+			},
44
+		},
45
+		{
46
+			options: types.SecretListOptions{
47
+				Filter: filters,
48
+			},
49
+			expectedQueryParams: map[string]string{
50
+				"filters": `{"label":{"label1":true,"label2":true}}`,
51
+			},
52
+		},
53
+	}
54
+	for _, listCase := range listCases {
55
+		client := &Client{
56
+			client: newMockClient(func(req *http.Request) (*http.Response, error) {
57
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
58
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
59
+				}
60
+				query := req.URL.Query()
61
+				for key, expected := range listCase.expectedQueryParams {
62
+					actual := query.Get(key)
63
+					if actual != expected {
64
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
65
+					}
66
+				}
67
+				content, err := json.Marshal([]swarm.Secret{
68
+					{
69
+						ID: "secret_id1",
70
+					},
71
+					{
72
+						ID: "secret_id2",
73
+					},
74
+				})
75
+				if err != nil {
76
+					return nil, err
77
+				}
78
+				return &http.Response{
79
+					StatusCode: http.StatusOK,
80
+					Body:       ioutil.NopCloser(bytes.NewReader(content)),
81
+				}, nil
82
+			}),
83
+		}
84
+
85
+		secrets, err := client.SecretList(context.Background(), listCase.options)
86
+		if err != nil {
87
+			t.Fatal(err)
88
+		}
89
+		if len(secrets) != 2 {
90
+			t.Fatalf("expected 2 secrets, got %v", secrets)
91
+		}
92
+	}
93
+}
0 94
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+package client
1
+
2
+import "golang.org/x/net/context"
3
+
4
+// SecretRemove removes a Secret.
5
+func (cli *Client) SecretRemove(ctx context.Context, id string) error {
6
+	resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil)
7
+	ensureReaderClosed(resp)
8
+	return err
9
+}
0 10
new file mode 100644
... ...
@@ -0,0 +1,47 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"strings"
8
+	"testing"
9
+
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+func TestSecretRemoveError(t *testing.T) {
14
+	client := &Client{
15
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+
18
+	err := client.SecretRemove(context.Background(), "secret_id")
19
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
20
+		t.Fatalf("expected a Server Error, got %v", err)
21
+	}
22
+}
23
+
24
+func TestSecretRemove(t *testing.T) {
25
+	expectedURL := "/secrets/secret_id"
26
+
27
+	client := &Client{
28
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
29
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
30
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
31
+			}
32
+			if req.Method != "DELETE" {
33
+				return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
34
+			}
35
+			return &http.Response{
36
+				StatusCode: http.StatusOK,
37
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
38
+			}, nil
39
+		}),
40
+	}
41
+
42
+	err := client.SecretRemove(context.Background(), "secret_id")
43
+	if err != nil {
44
+		t.Fatal(err)
45
+	}
46
+}
... ...
@@ -89,8 +89,9 @@ type CommonContainer struct {
89 89
 	HasBeenStartedBefore   bool
90 90
 	HasBeenManuallyStopped bool // used for unless-stopped restart policy
91 91
 	MountPoints            map[string]*volume.MountPoint
92
-	HostConfig             *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable
93
-	ExecCommands           *exec.Store                `json:"-"`
92
+	HostConfig             *containertypes.HostConfig        `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable
93
+	ExecCommands           *exec.Store                       `json:"-"`
94
+	Secrets                []*containertypes.ContainerSecret `json:"-"` // do not serialize
94 95
 	// logDriver for closing
95 96
 	LogDriver      logger.Logger  `json:"-"`
96 97
 	LogCopier      *logger.Copier `json:"-"`
... ...
@@ -23,7 +23,10 @@ import (
23 23
 )
24 24
 
25 25
 // DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container
26
-const DefaultSHMSize int64 = 67108864
26
+const (
27
+	DefaultSHMSize           int64 = 67108864
28
+	containerSecretMountPath       = "/run/secrets"
29
+)
27 30
 
28 31
 // Container holds the fields specific to unixen implementations.
29 32
 // See CommonContainer for standard fields common to all containers.
... ...
@@ -175,6 +178,10 @@ func (container *Container) NetworkMounts() []Mount {
175 175
 	return mounts
176 176
 }
177 177
 
178
+func (container *Container) SecretMountPath() string {
179
+	return filepath.Join(container.Root, "secrets")
180
+}
181
+
178 182
 // CopyImagePathContent copies files in destination to the volume.
179 183
 func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error {
180 184
 	rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, destination), container.BaseFS)
... ...
@@ -260,6 +267,26 @@ func (container *Container) IpcMounts() []Mount {
260 260
 	return mounts
261 261
 }
262 262
 
263
+// SecretMounts returns the list of Secret mounts
264
+func (container *Container) SecretMounts() []Mount {
265
+	var mounts []Mount
266
+
267
+	if len(container.Secrets) > 0 {
268
+		mounts = append(mounts, Mount{
269
+			Source:      container.SecretMountPath(),
270
+			Destination: containerSecretMountPath,
271
+			Writable:    false,
272
+		})
273
+	}
274
+
275
+	return mounts
276
+}
277
+
278
+// UnmountSecrets unmounts the local tmpfs for secrets
279
+func (container *Container) UnmountSecrets() error {
280
+	return detachMounted(container.SecretMountPath())
281
+}
282
+
263 283
 // UpdateContainer updates configuration of a container.
264 284
 func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error {
265 285
 	container.Lock()
... ...
@@ -23,6 +23,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
23 23
 		User:     c.User,
24 24
 		Groups:   c.Groups,
25 25
 		TTY:      c.TTY,
26
+		Secrets:  secretReferencesFromGRPC(c.Secrets),
26 27
 	}
27 28
 
28 29
 	if c.DNSConfig != nil {
... ...
@@ -75,6 +76,47 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
75 75
 	return containerSpec
76 76
 }
77 77
 
78
+func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference {
79
+	refs := []*swarmapi.SecretReference{}
80
+	for _, s := range sr {
81
+		var mode swarmapi.SecretReference_Mode
82
+		switch s.Mode {
83
+		case types.SecretReferenceSystem:
84
+			mode = swarmapi.SecretReference_SYSTEM
85
+		default:
86
+			mode = swarmapi.SecretReference_FILE
87
+		}
88
+		refs = append(refs, &swarmapi.SecretReference{
89
+			SecretID:   s.SecretID,
90
+			SecretName: s.SecretName,
91
+			Target:     s.Target,
92
+			Mode:       mode,
93
+		})
94
+	}
95
+
96
+	return refs
97
+}
98
+func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference {
99
+	refs := []*types.SecretReference{}
100
+	for _, s := range sr {
101
+		var mode types.SecretReferenceMode
102
+		switch s.Mode {
103
+		case swarmapi.SecretReference_SYSTEM:
104
+			mode = types.SecretReferenceSystem
105
+		default:
106
+			mode = types.SecretReferenceFile
107
+		}
108
+		refs = append(refs, &types.SecretReference{
109
+			SecretID:   s.SecretID,
110
+			SecretName: s.SecretName,
111
+			Target:     s.Target,
112
+			Mode:       mode,
113
+		})
114
+	}
115
+
116
+	return refs
117
+}
118
+
78 119
 func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
79 120
 	containerSpec := &swarmapi.ContainerSpec{
80 121
 		Image:    c.Image,
... ...
@@ -87,6 +129,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
87 87
 		User:     c.User,
88 88
 		Groups:   c.Groups,
89 89
 		TTY:      c.TTY,
90
+		Secrets:  secretReferencesToGRPC(c.Secrets),
90 91
 	}
91 92
 
92 93
 	if c.DNSConfig != nil {
93 94
new file mode 100644
... ...
@@ -0,0 +1,46 @@
0
+package convert
1
+
2
+import (
3
+	"github.com/Sirupsen/logrus"
4
+	swarmtypes "github.com/docker/docker/api/types/swarm"
5
+	swarmapi "github.com/docker/swarmkit/api"
6
+	"github.com/docker/swarmkit/protobuf/ptypes"
7
+)
8
+
9
+// SecretFromGRPC converts a grpc Secret to a Secret.
10
+func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret {
11
+	logrus.Debugf("%+v", s)
12
+	secret := swarmtypes.Secret{
13
+		ID:         s.ID,
14
+		Digest:     s.Digest,
15
+		SecretSize: s.SecretSize,
16
+	}
17
+
18
+	// Meta
19
+	secret.Version.Index = s.Meta.Version.Index
20
+	secret.CreatedAt, _ = ptypes.Timestamp(s.Meta.CreatedAt)
21
+	secret.UpdatedAt, _ = ptypes.Timestamp(s.Meta.UpdatedAt)
22
+
23
+	secret.Spec = &swarmtypes.SecretSpec{
24
+		Annotations: swarmtypes.Annotations{
25
+			Name:   s.Spec.Annotations.Name,
26
+			Labels: s.Spec.Annotations.Labels,
27
+		},
28
+		Data: s.Spec.Data,
29
+	}
30
+
31
+	return secret
32
+}
33
+
34
+// SecretSpecToGRPC converts Secret to a grpc Secret.
35
+func SecretSpecToGRPC(s swarmtypes.SecretSpec) (swarmapi.SecretSpec, error) {
36
+	spec := swarmapi.SecretSpec{
37
+		Annotations: swarmapi.Annotations{
38
+			Name:   s.Name,
39
+			Labels: s.Labels,
40
+		},
41
+		Data: s.Data,
42
+	}
43
+
44
+	return spec, nil
45
+}
... ...
@@ -34,6 +34,7 @@ type Backend interface {
34 34
 	ContainerWaitWithContext(ctx context.Context, name string) error
35 35
 	ContainerRm(name string, config *types.ContainerRmConfig) error
36 36
 	ContainerKill(name string, sig uint64) error
37
+	SetContainerSecrets(name string, secrets []*container.ContainerSecret) error
37 38
 	SystemInfo() (*types.Info, error)
38 39
 	VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
39 40
 	Containers(config *types.ContainerListOptions) ([]*types.Container, error)
... ...
@@ -17,6 +17,7 @@ import (
17 17
 	"github.com/docker/docker/api/types/versions"
18 18
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
19 19
 	"github.com/docker/libnetwork"
20
+	"github.com/docker/swarmkit/agent/exec"
20 21
 	"github.com/docker/swarmkit/api"
21 22
 	"github.com/docker/swarmkit/log"
22 23
 	"golang.org/x/net/context"
... ...
@@ -29,9 +30,10 @@ import (
29 29
 type containerAdapter struct {
30 30
 	backend   executorpkg.Backend
31 31
 	container *containerConfig
32
+	secrets   exec.SecretProvider
32 33
 }
33 34
 
34
-func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapter, error) {
35
+func newContainerAdapter(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*containerAdapter, error) {
35 36
 	ctnr, err := newContainerConfig(task)
36 37
 	if err != nil {
37 38
 		return nil, err
... ...
@@ -40,6 +42,7 @@ func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapt
40 40
 	return &containerAdapter{
41 41
 		container: ctnr,
42 42
 		backend:   b,
43
+		secrets:   secrets,
43 44
 	}, nil
44 45
 }
45 46
 
... ...
@@ -215,6 +218,35 @@ func (c *containerAdapter) create(ctx context.Context) error {
215 215
 		}
216 216
 	}
217 217
 
218
+	secrets := []*containertypes.ContainerSecret{}
219
+	for _, s := range c.container.task.Spec.GetContainer().Secrets {
220
+		sec := c.secrets.Get(s.SecretID)
221
+		if sec == nil {
222
+			logrus.Warnf("unable to get secret %s from provider", s.SecretID)
223
+			continue
224
+		}
225
+
226
+		name := sec.Spec.Annotations.Name
227
+		target := s.Target
228
+		if target == "" {
229
+			target = name
230
+		}
231
+		secrets = append(secrets, &containertypes.ContainerSecret{
232
+			Name:   name,
233
+			Target: target,
234
+			Data:   sec.Spec.Data,
235
+			// TODO (ehazlett): enable configurable uid, gid, mode
236
+			Uid:  0,
237
+			Gid:  0,
238
+			Mode: 0444,
239
+		})
240
+	}
241
+
242
+	// configure secrets
243
+	if err := c.backend.SetContainerSecrets(cr.ID, secrets); err != nil {
244
+		return err
245
+	}
246
+
218 247
 	if err := c.backend.UpdateContainerServiceConfig(cr.ID, c.container.serviceConfig()); err != nil {
219 248
 		return err
220 249
 	}
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
5 5
 	"github.com/docker/swarmkit/api"
6 6
 	"golang.org/x/net/context"
7
+	"src/github.com/docker/swarmkit/agent/exec"
7 8
 )
8 9
 
9 10
 // networkAttacherController implements agent.Controller against docker's API.
... ...
@@ -19,8 +20,8 @@ type networkAttacherController struct {
19 19
 	closed  chan struct{}
20 20
 }
21 21
 
22
-func newNetworkAttacherController(b executorpkg.Backend, task *api.Task) (*networkAttacherController, error) {
23
-	adapter, err := newContainerAdapter(b, task)
22
+func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*networkAttacherController, error) {
23
+	adapter, err := newContainerAdapter(b, task, secrets)
24 24
 	if err != nil {
25 25
 		return nil, err
26 26
 	}
... ...
@@ -33,8 +33,8 @@ type controller struct {
33 33
 var _ exec.Controller = &controller{}
34 34
 
35 35
 // NewController returns a docker exec runner for the provided task.
36
-func newController(b executorpkg.Backend, task *api.Task) (*controller, error) {
37
-	adapter, err := newContainerAdapter(b, task)
36
+func newController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*controller, error) {
37
+	adapter, err := newContainerAdapter(b, task, secrets)
38 38
 	if err != nil {
39 39
 		return nil, err
40 40
 	}
... ...
@@ -18,6 +18,10 @@ type executor struct {
18 18
 	backend executorpkg.Backend
19 19
 }
20 20
 
21
+type secretProvider interface {
22
+	Get(secretID string) *api.Secret
23
+}
24
+
21 25
 // NewExecutor returns an executor from the docker client.
22 26
 func NewExecutor(b executorpkg.Backend) exec.Executor {
23 27
 	return &executor{
... ...
@@ -120,12 +124,12 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error {
120 120
 }
121 121
 
122 122
 // Controller returns a docker container runner.
123
-func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
123
+func (e *executor) Controller(t *api.Task, secrets exec.SecretProvider) (exec.Controller, error) {
124 124
 	if t.Spec.GetAttachment() != nil {
125
-		return newNetworkAttacherController(e.backend, t)
125
+		return newNetworkAttacherController(e.backend, t, secrets)
126 126
 	}
127 127
 
128
-	ctlr, err := newController(e.backend, t)
128
+	ctlr, err := newController(e.backend, t, secrets)
129 129
 	if err != nil {
130 130
 		return nil, err
131 131
 	}
... ...
@@ -54,7 +54,7 @@ func TestHealthStates(t *testing.T) {
54 54
 		EventsService: e,
55 55
 	}
56 56
 
57
-	controller, err := newController(daemon, task)
57
+	controller, err := newController(daemon, task, nil)
58 58
 	if err != nil {
59 59
 		t.Fatalf("create controller fail %v", err)
60 60
 	}
... ...
@@ -26,7 +26,7 @@ func newTestControllerWithMount(m api.Mount) (*controller, error) {
26 26
 				},
27 27
 			},
28 28
 		},
29
-	})
29
+	}, nil)
30 30
 }
31 31
 
32 32
 func TestControllerValidateMountBind(t *testing.T) {
... ...
@@ -96,3 +96,21 @@ func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) e
96 96
 
97 97
 	return f, nil
98 98
 }
99
+
100
+func newListSecretsFilters(filter filters.Args) (*swarmapi.ListSecretsRequest_Filters, error) {
101
+	accepted := map[string]bool{
102
+		"names": true,
103
+		"name":  true,
104
+		"id":    true,
105
+		"label": true,
106
+	}
107
+	if err := filter.Validate(accepted); err != nil {
108
+		return nil, err
109
+	}
110
+	return &swarmapi.ListSecretsRequest_Filters{
111
+		Names:        filter.Get("names"),
112
+		NamePrefixes: filter.Get("name"),
113
+		IDPrefixes:   filter.Get("id"),
114
+		Labels:       runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
115
+	}, nil
116
+}
99 117
new file mode 100644
... ...
@@ -0,0 +1,131 @@
0
+package cluster
1
+
2
+import (
3
+	apitypes "github.com/docker/docker/api/types"
4
+	types "github.com/docker/docker/api/types/swarm"
5
+	"github.com/docker/docker/daemon/cluster/convert"
6
+	swarmapi "github.com/docker/swarmkit/api"
7
+)
8
+
9
+// GetSecret returns a secret from a managed swarm cluster
10
+func (c *Cluster) GetSecret(id string) (types.Secret, error) {
11
+	ctx, cancel := c.getRequestContext()
12
+	defer cancel()
13
+
14
+	r, err := c.node.client.GetSecret(ctx, &swarmapi.GetSecretRequest{SecretID: id})
15
+	if err != nil {
16
+		return types.Secret{}, err
17
+	}
18
+
19
+	return convert.SecretFromGRPC(r.Secret), nil
20
+}
21
+
22
+// GetSecrets returns all secrets of a managed swarm cluster.
23
+func (c *Cluster) GetSecrets(options apitypes.SecretListOptions) ([]types.Secret, error) {
24
+	c.RLock()
25
+	defer c.RUnlock()
26
+
27
+	if !c.isActiveManager() {
28
+		return nil, c.errNoManager()
29
+	}
30
+
31
+	filters, err := newListSecretsFilters(options.Filter)
32
+	if err != nil {
33
+		return nil, err
34
+	}
35
+	ctx, cancel := c.getRequestContext()
36
+	defer cancel()
37
+
38
+	r, err := c.node.client.ListSecrets(ctx,
39
+		&swarmapi.ListSecretsRequest{Filters: filters})
40
+	if err != nil {
41
+		return nil, err
42
+	}
43
+
44
+	secrets := []types.Secret{}
45
+
46
+	for _, secret := range r.Secrets {
47
+		secrets = append(secrets, convert.SecretFromGRPC(secret))
48
+	}
49
+
50
+	return secrets, nil
51
+}
52
+
53
+// CreateSecret creates a new secret in a managed swarm cluster.
54
+func (c *Cluster) CreateSecret(s types.SecretSpec) (string, error) {
55
+	c.RLock()
56
+	defer c.RUnlock()
57
+
58
+	if !c.isActiveManager() {
59
+		return "", c.errNoManager()
60
+	}
61
+
62
+	ctx, cancel := c.getRequestContext()
63
+	defer cancel()
64
+
65
+	secretSpec, err := convert.SecretSpecToGRPC(s)
66
+	if err != nil {
67
+		return "", err
68
+	}
69
+
70
+	r, err := c.node.client.CreateSecret(ctx,
71
+		&swarmapi.CreateSecretRequest{Spec: &secretSpec})
72
+	if err != nil {
73
+		return "", err
74
+	}
75
+
76
+	return r.Secret.ID, nil
77
+}
78
+
79
+// RemoveSecret removes a secret from a managed swarm cluster.
80
+func (c *Cluster) RemoveSecret(id string) error {
81
+	c.RLock()
82
+	defer c.RUnlock()
83
+
84
+	if !c.isActiveManager() {
85
+		return c.errNoManager()
86
+	}
87
+
88
+	ctx, cancel := c.getRequestContext()
89
+	defer cancel()
90
+
91
+	req := &swarmapi.RemoveSecretRequest{
92
+		SecretID: id,
93
+	}
94
+
95
+	if _, err := c.node.client.RemoveSecret(ctx, req); err != nil {
96
+		return err
97
+	}
98
+	return nil
99
+}
100
+
101
+// UpdateSecret updates a secret in a managed swarm cluster.
102
+func (c *Cluster) UpdateSecret(id string, version uint64, spec types.SecretSpec) error {
103
+	c.RLock()
104
+	defer c.RUnlock()
105
+
106
+	if !c.isActiveManager() {
107
+		return c.errNoManager()
108
+	}
109
+
110
+	ctx, cancel := c.getRequestContext()
111
+	defer cancel()
112
+
113
+	secretSpec, err := convert.SecretSpecToGRPC(spec)
114
+	if err != nil {
115
+		return err
116
+	}
117
+
118
+	if _, err := c.client.UpdateSecret(ctx,
119
+		&swarmapi.UpdateSecretRequest{
120
+			SecretID: id,
121
+			SecretVersion: &swarmapi.Version{
122
+				Index: version,
123
+			},
124
+			Spec: &secretSpec,
125
+		}); err != nil {
126
+		return err
127
+	}
128
+
129
+	return nil
130
+}
... ...
@@ -4,6 +4,7 @@ package daemon
4 4
 
5 5
 import (
6 6
 	"fmt"
7
+	"io/ioutil"
7 8
 	"os"
8 9
 	"path/filepath"
9 10
 	"strconv"
... ...
@@ -18,6 +19,7 @@ import (
18 18
 	"github.com/docker/docker/pkg/idtools"
19 19
 	"github.com/docker/docker/pkg/stringid"
20 20
 	"github.com/docker/docker/runconfig"
21
+	"github.com/docker/engine-api/types/mount"
21 22
 	"github.com/docker/libnetwork"
22 23
 	"github.com/opencontainers/runc/libcontainer/configs"
23 24
 	"github.com/opencontainers/runc/libcontainer/devices"
... ...
@@ -139,6 +141,43 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
139 139
 
140 140
 	return nil
141 141
 }
142
+
143
+func (daemon *Daemon) setupSecretDir(c *container.Container) error {
144
+	localMountPath := c.SecretMountPath()
145
+	logrus.Debugf("secrets: setting up secret dir: %s", localMountPath)
146
+
147
+	// create tmpfs
148
+	if err := os.MkdirAll(localMountPath, 0700); err != nil {
149
+		return fmt.Errorf("error creating secret local mount path: %s", err)
150
+	}
151
+	if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev"); err != nil {
152
+		return fmt.Errorf("unable to setup secret mount: %s", err)
153
+	}
154
+
155
+	for _, s := range c.Secrets {
156
+		fPath := filepath.Join(localMountPath, s.Target)
157
+		if err := os.MkdirAll(filepath.Dir(fPath), 0700); err != nil {
158
+			return fmt.Errorf("error creating secret mount path: %s", err)
159
+		}
160
+
161
+		logrus.Debugf("injecting secret: name=%s path=%s", s.Name, fPath)
162
+		if err := ioutil.WriteFile(fPath, s.Data, s.Mode); err != nil {
163
+			return fmt.Errorf("error injecting secret: %s", err)
164
+		}
165
+
166
+		if err := os.Chown(fPath, s.Uid, s.Gid); err != nil {
167
+			return fmt.Errorf("error setting ownership for secret: %s", err)
168
+		}
169
+	}
170
+
171
+	// remount secrets ro
172
+	if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro"); err != nil {
173
+		return fmt.Errorf("unable to remount secret dir as readonly: %s", err)
174
+	}
175
+
176
+	return nil
177
+}
178
+
142 179
 func killProcessDirectly(container *container.Container) error {
143 180
 	if _, err := container.WaitStop(10 * time.Second); err != nil {
144 181
 		// Ensure that we don't kill ourselves
... ...
@@ -854,6 +854,7 @@ func (daemon *Daemon) Unmount(container *container.Container) error {
854 854
 		logrus.Errorf("Error unmounting container %s: %s", container.ID, err)
855 855
 		return err
856 856
 	}
857
+
857 858
 	return nil
858 859
 }
859 860
 
... ...
@@ -702,16 +702,23 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
702 702
 		return nil, err
703 703
 	}
704 704
 
705
+	if err := daemon.setupSecretDir(c); err != nil {
706
+		return nil, err
707
+	}
708
+
705 709
 	ms, err := daemon.setupMounts(c)
706 710
 	if err != nil {
707 711
 		return nil, err
708 712
 	}
709 713
 	ms = append(ms, c.IpcMounts()...)
714
+
710 715
 	tmpfsMounts, err := c.TmpfsMounts()
711 716
 	if err != nil {
712 717
 		return nil, err
713 718
 	}
714 719
 	ms = append(ms, tmpfsMounts...)
720
+
721
+	ms = append(ms, c.SecretMounts()...)
715 722
 	sort.Sort(mounts(ms))
716 723
 	if err := setMounts(daemon, &s, c, ms); err != nil {
717 724
 		return nil, fmt.Errorf("linux mounts: %v", err)
718 725
new file mode 100644
... ...
@@ -0,0 +1,22 @@
0
+package daemon
1
+
2
+import (
3
+	"github.com/Sirupsen/logrus"
4
+	containertypes "github.com/docker/docker/api/types/container"
5
+)
6
+
7
+func (daemon *Daemon) SetContainerSecrets(name string, secrets []*containertypes.ContainerSecret) error {
8
+	if !secretsSupported() {
9
+		logrus.Warn("secrets are not supported on this platform")
10
+		return nil
11
+	}
12
+
13
+	c, err := daemon.GetContainer(name)
14
+	if err != nil {
15
+		return err
16
+	}
17
+
18
+	c.Secrets = secrets
19
+
20
+	return nil
21
+}
0 22
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+// +build linux
1
+
2
+package daemon
3
+
4
+func secretsSupported() bool {
5
+	return true
6
+}
0 7
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+// +build !linux
1
+
2
+package daemon
3
+
4
+func secretsSupported() bool {
5
+	return false
6
+}
... ...
@@ -212,6 +212,10 @@ func (daemon *Daemon) Cleanup(container *container.Container) {
212 212
 		}
213 213
 	}
214 214
 
215
+	if err := container.UnmountSecrets(); err != nil {
216
+		logrus.Warnf("%s cleanup: failed to unmount secrets: %s", container.ID, err)
217
+	}
218
+
215 219
 	for _, eConfig := range container.ExecCommands.Commands() {
216 220
 		daemon.unregisterExecCommand(container, eConfig)
217 221
 	}