From 5431f2d3ce91ef47f7bd455e3022613841a436da Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Wed, 26 Oct 2016 09:47:12 -0700 Subject: [PATCH 01/37] secrets: vendor swarmkit Signed-off-by: Evan Hazlett secrets: vendor swarmkit Signed-off-by: Evan Hazlett Upstream-commit: 1310dadf4ab3f38b346d78a6cbc7dae045388013 Component: engine --- .../docker/swarmkit/agent/secrets/secrets.go | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 components/engine/vendor/github.com/docker/swarmkit/agent/secrets/secrets.go diff --git a/components/engine/vendor/github.com/docker/swarmkit/agent/secrets/secrets.go b/components/engine/vendor/github.com/docker/swarmkit/agent/secrets/secrets.go new file mode 100644 index 0000000000..8f2bcf2706 --- /dev/null +++ b/components/engine/vendor/github.com/docker/swarmkit/agent/secrets/secrets.go @@ -0,0 +1,87 @@ +package secrets + +import ( + "sync" + + "github.com/docker/swarmkit/agent/exec" + "github.com/docker/swarmkit/api" +) + +// secrets is a map that keeps all the currenty available secrets to the agent +// mapped by secret ID +type secrets struct { + mu sync.RWMutex + m map[string]*api.Secret +} + +// NewManager returns a place to store secrets. +func NewManager() exec.SecretsManager { + return &secrets{ + m: make(map[string]*api.Secret), + } +} + +// Get returns a secret by ID. If the secret doesn't exist, returns nil. +func (s *secrets) Get(secretID string) *api.Secret { + s.mu.RLock() + defer s.mu.RUnlock() + if s, ok := s.m[secretID]; ok { + return s + } + return nil +} + +// add adds one or more secrets to the secret map +func (s *secrets) Add(secrets ...api.Secret) { + s.mu.Lock() + defer s.mu.Unlock() + for _, secret := range secrets { + s.m[secret.ID] = secret.Copy() + } +} + +// remove removes one or more secrets by ID from the secret map. Succeeds +// whether or not the given IDs are in the map. +func (s *secrets) Remove(secrets []string) { + s.mu.Lock() + defer s.mu.Unlock() + for _, secret := range secrets { + delete(s.m, secret) + } +} + +// reset removes all the secrets +func (s *secrets) Reset() { + s.mu.Lock() + defer s.mu.Unlock() + s.m = make(map[string]*api.Secret) +} + +// taskRestrictedSecretsProvider restricts the ids to the task. +type taskRestrictedSecretsProvider struct { + secrets exec.SecretGetter + secretIDs map[string]struct{} // allow list of secret ids +} + +func (sp *taskRestrictedSecretsProvider) Get(secretID string) *api.Secret { + if _, ok := sp.secretIDs[secretID]; !ok { + return nil + } + + return sp.secrets.Get(secretID) +} + +// Restrict provides a getter that only allows access to the secrets +// referenced by the task. +func Restrict(secrets exec.SecretGetter, t *api.Task) exec.SecretGetter { + sids := map[string]struct{}{} + + container := t.Spec.GetContainer() + if container != nil { + for _, ref := range container.Secrets { + sids[ref.SecretID] = struct{}{} + } + } + + return &taskRestrictedSecretsProvider{secrets: secrets, secretIDs: sids} +} From 9dbbc071d5b442f83d73afeda4bac1202e1853f3 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Wed, 19 Oct 2016 12:22:02 -0400 Subject: [PATCH 02/37] secrets: secret management for swarm Signed-off-by: Evan Hazlett wip: use tmpfs for swarm secrets Signed-off-by: Evan Hazlett wip: inject secrets from swarm secret store Signed-off-by: Evan Hazlett secrets: use secret names in cli for service create Signed-off-by: Evan Hazlett switch to use mounts instead of volumes Signed-off-by: Evan Hazlett vendor: use ehazlett swarmkit Signed-off-by: Evan Hazlett secrets: finish secret update Signed-off-by: Evan Hazlett Upstream-commit: 3716ec25b423d8ff7dfa231a7b3cf0154726ed37 Component: engine --- .../engine/api/server/router/swarm/backend.go | 5 + .../engine/api/server/router/swarm/cluster.go | 5 + .../api/server/router/swarm/cluster_routes.go | 74 ++++++++++ .../engine/api/types/container/secret.go | 12 ++ .../engine/api/types/swarm/container.go | 1 + components/engine/api/types/swarm/secret.go | 30 ++++ components/engine/api/types/types.go | 13 ++ .../engine/cli/command/commands/commands.go | 2 + components/engine/cli/command/secret/cmd.go | 29 ++++ .../engine/cli/command/secret/create.go | 57 ++++++++ .../engine/cli/command/secret/inspect.go | 42 ++++++ components/engine/cli/command/secret/ls.go | 62 +++++++++ .../engine/cli/command/secret/remove.go | 43 ++++++ .../engine/cli/command/service/create.go | 7 + components/engine/cli/command/service/opts.go | 17 +++ .../engine/cli/command/service/parse.go | 92 ++++++++++++ components/engine/client/errors.go | 22 +++ components/engine/client/interface.go | 9 ++ components/engine/client/secret_create.go | 24 ++++ .../engine/client/secret_create_test.go | 57 ++++++++ components/engine/client/secret_inspect.go | 34 +++++ .../engine/client/secret_inspect_test.go | 65 +++++++++ components/engine/client/secret_list.go | 35 +++++ components/engine/client/secret_list_test.go | 94 +++++++++++++ components/engine/client/secret_remove.go | 10 ++ .../engine/client/secret_remove_test.go | 47 +++++++ components/engine/container/container.go | 5 +- components/engine/container/container_unix.go | 29 +++- .../daemon/cluster/convert/container.go | 43 ++++++ .../engine/daemon/cluster/convert/secret.go | 46 ++++++ .../engine/daemon/cluster/executor/backend.go | 1 + .../cluster/executor/container/adapter.go | 34 ++++- .../cluster/executor/container/attachment.go | 5 +- .../cluster/executor/container/controller.go | 4 +- .../cluster/executor/container/executor.go | 10 +- .../cluster/executor/container/health_test.go | 2 +- .../executor/container/validate_test.go | 2 +- components/engine/daemon/cluster/filters.go | 18 +++ components/engine/daemon/cluster/secrets.go | 131 ++++++++++++++++++ .../daemon/container_operations_unix.go | 39 ++++++ components/engine/daemon/daemon.go | 1 + components/engine/daemon/oci_linux.go | 7 + components/engine/daemon/secrets.go | 22 +++ components/engine/daemon/secrets_linux.go | 7 + .../engine/daemon/secrets_unsupported.go | 7 + components/engine/daemon/start.go | 4 + 46 files changed, 1292 insertions(+), 13 deletions(-) create mode 100644 components/engine/api/types/container/secret.go create mode 100644 components/engine/api/types/swarm/secret.go create mode 100644 components/engine/cli/command/secret/cmd.go create mode 100644 components/engine/cli/command/secret/create.go create mode 100644 components/engine/cli/command/secret/inspect.go create mode 100644 components/engine/cli/command/secret/ls.go create mode 100644 components/engine/cli/command/secret/remove.go create mode 100644 components/engine/cli/command/service/parse.go create mode 100644 components/engine/client/secret_create.go create mode 100644 components/engine/client/secret_create_test.go create mode 100644 components/engine/client/secret_inspect.go create mode 100644 components/engine/client/secret_inspect_test.go create mode 100644 components/engine/client/secret_list.go create mode 100644 components/engine/client/secret_list_test.go create mode 100644 components/engine/client/secret_remove.go create mode 100644 components/engine/client/secret_remove_test.go create mode 100644 components/engine/daemon/cluster/convert/secret.go create mode 100644 components/engine/daemon/cluster/secrets.go create mode 100644 components/engine/daemon/secrets.go create mode 100644 components/engine/daemon/secrets_linux.go create mode 100644 components/engine/daemon/secrets_unsupported.go diff --git a/components/engine/api/server/router/swarm/backend.go b/components/engine/api/server/router/swarm/backend.go index a7cc9eef40..1ab09a8f8b 100644 --- a/components/engine/api/server/router/swarm/backend.go +++ b/components/engine/api/server/router/swarm/backend.go @@ -23,4 +23,9 @@ type Backend interface { RemoveNode(string, bool) error GetTasks(basictypes.TaskListOptions) ([]types.Task, error) GetTask(string) (types.Task, error) + GetSecrets(opts basictypes.SecretListOptions) ([]types.Secret, error) + CreateSecret(s types.SecretSpec) (string, error) + RemoveSecret(id string) error + GetSecret(id string) (types.Secret, error) + UpdateSecret(id string, version uint64, spec types.SecretSpec) error } diff --git a/components/engine/api/server/router/swarm/cluster.go b/components/engine/api/server/router/swarm/cluster.go index a67ffa9632..ec92e7aaa0 100644 --- a/components/engine/api/server/router/swarm/cluster.go +++ b/components/engine/api/server/router/swarm/cluster.go @@ -40,5 +40,10 @@ func (sr *swarmRouter) initRoutes() { router.NewPostRoute("/nodes/{id:.*}/update", sr.updateNode), router.NewGetRoute("/tasks", sr.getTasks), router.NewGetRoute("/tasks/{id:.*}", sr.getTask), + router.NewGetRoute("/secrets", sr.getSecrets), + router.NewPostRoute("/secrets/create", sr.createSecret), + router.NewDeleteRoute("/secrets/{id:.*}", sr.removeSecret), + router.NewGetRoute("/secrets/{id:.*}", sr.getSecret), + router.NewPostRoute("/secrets/{id:.*}/update", sr.updateSecret), } } diff --git a/components/engine/api/server/router/swarm/cluster_routes.go b/components/engine/api/server/router/swarm/cluster_routes.go index bc78601692..fc33d27746 100644 --- a/components/engine/api/server/router/swarm/cluster_routes.go +++ b/components/engine/api/server/router/swarm/cluster_routes.go @@ -261,3 +261,77 @@ func (sr *swarmRouter) getTask(ctx context.Context, w http.ResponseWriter, r *ht return httputils.WriteJSON(w, http.StatusOK, task) } + +func (sr *swarmRouter) getSecrets(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + filter, err := filters.FromParam(r.Form.Get("filters")) + if err != nil { + return err + } + + secrets, err := sr.backend.GetSecrets(basictypes.SecretListOptions{Filter: filter}) + if err != nil { + logrus.Errorf("Error getting secrets: %v", err) + return err + } + + return httputils.WriteJSON(w, http.StatusOK, secrets) +} + +func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + var secret types.SecretSpec + if err := json.NewDecoder(r.Body).Decode(&secret); err != nil { + return err + } + + id, err := sr.backend.CreateSecret(secret) + if err != nil { + logrus.Errorf("Error creating secret %s: %v", id, err) + return err + } + + return httputils.WriteJSON(w, http.StatusCreated, &basictypes.SecretCreateResponse{ + ID: id, + }) +} + +func (sr *swarmRouter) removeSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := sr.backend.RemoveSecret(vars["id"]); err != nil { + logrus.Errorf("Error removing secret %s: %v", vars["id"], err) + return err + } + + return nil +} + +func (sr *swarmRouter) getSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + secret, err := sr.backend.GetSecret(vars["id"]) + if err != nil { + logrus.Errorf("Error getting secret %s: %v", vars["id"], err) + return err + } + + return httputils.WriteJSON(w, http.StatusOK, secret) +} + +func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + var secret types.SecretSpec + if err := json.NewDecoder(r.Body).Decode(&secret); err != nil { + return err + } + + rawVersion := r.URL.Query().Get("version") + version, err := strconv.ParseUint(rawVersion, 10, 64) + if err != nil { + return fmt.Errorf("Invalid secret version '%s': %s", rawVersion, err.Error()) + } + + id := vars["id"] + if err := sr.backend.UpdateSecret(id, version, secret); err != nil { + return fmt.Errorf("Error updating secret: %s", err) + } + + return nil +} diff --git a/components/engine/api/types/container/secret.go b/components/engine/api/types/container/secret.go new file mode 100644 index 0000000000..eee5bf89d2 --- /dev/null +++ b/components/engine/api/types/container/secret.go @@ -0,0 +1,12 @@ +package container + +import "os" + +type ContainerSecret struct { + Name string + Target string + Data []byte + Uid int + Gid int + Mode os.FileMode +} diff --git a/components/engine/api/types/swarm/container.go b/components/engine/api/types/swarm/container.go index e1ab81c5f9..39f1d3987c 100644 --- a/components/engine/api/types/swarm/container.go +++ b/components/engine/api/types/swarm/container.go @@ -37,4 +37,5 @@ type ContainerSpec struct { StopGracePeriod *time.Duration `json:",omitempty"` Healthcheck *container.HealthConfig `json:",omitempty"` DNSConfig *DNSConfig `json:",omitempty"` + Secrets []*SecretReference `json:",omitempty"` } diff --git a/components/engine/api/types/swarm/secret.go b/components/engine/api/types/swarm/secret.go new file mode 100644 index 0000000000..3323ba212d --- /dev/null +++ b/components/engine/api/types/swarm/secret.go @@ -0,0 +1,30 @@ +package swarm + +// Secret represents a secret. +type Secret struct { + ID string + Meta + Spec *SecretSpec `json:",omitempty"` + Digest string `json:",omitempty"` + SecretSize int64 `json:",omitempty"` +} + +type SecretSpec struct { + Annotations + Data []byte `json",omitempty"` +} + +type SecretReferenceMode int + +const ( + SecretReferenceSystem SecretReferenceMode = 0 + SecretReferenceFile SecretReferenceMode = 1 + SecretReferenceEnv SecretReferenceMode = 2 +) + +type SecretReference struct { + SecretID string `json:",omitempty"` + Mode SecretReferenceMode `json:",omitempty"` + Target string `json:",omitempty"` + SecretName string `json:",omitempty"` +} diff --git a/components/engine/api/types/types.go b/components/engine/api/types/types.go index 5591646b69..fe60755c84 100644 --- a/components/engine/api/types/types.go +++ b/components/engine/api/types/types.go @@ -6,6 +6,7 @@ import ( "time" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/registry" @@ -509,3 +510,15 @@ type ImagesPruneReport struct { type NetworksPruneReport struct { NetworksDeleted []string } + +// SecretCreateResponse contains the information returned to a client +// on the creation of a new secret. +type SecretCreateResponse struct { + // ID is the id of the created secret. + ID string +} + +// SecretListOptions holds parameters to list secrets +type SecretListOptions struct { + Filter filters.Args +} diff --git a/components/engine/cli/command/commands/commands.go b/components/engine/cli/command/commands/commands.go index fad709bca1..d64d5680cc 100644 --- a/components/engine/cli/command/commands/commands.go +++ b/components/engine/cli/command/commands/commands.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/cli/command/node" "github.com/docker/docker/cli/command/plugin" "github.com/docker/docker/cli/command/registry" + "github.com/docker/docker/cli/command/secret" "github.com/docker/docker/cli/command/service" "github.com/docker/docker/cli/command/stack" "github.com/docker/docker/cli/command/swarm" @@ -25,6 +26,7 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { node.NewNodeCommand(dockerCli), service.NewServiceCommand(dockerCli), swarm.NewSwarmCommand(dockerCli), + secret.NewSecretCommand(dockerCli), container.NewContainerCommand(dockerCli), image.NewImageCommand(dockerCli), system.NewSystemCommand(dockerCli), diff --git a/components/engine/cli/command/secret/cmd.go b/components/engine/cli/command/secret/cmd.go new file mode 100644 index 0000000000..995300ad77 --- /dev/null +++ b/components/engine/cli/command/secret/cmd.go @@ -0,0 +1,29 @@ +package secret + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" +) + +// NewSecretCommand returns a cobra command for `secret` subcommands +func NewSecretCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "secret", + Short: "Manage Docker secrets", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newSecretListCommand(dockerCli), + newSecretCreateCommand(dockerCli), + newSecretInspectCommand(dockerCli), + newSecretRemoveCommand(dockerCli), + ) + return cmd +} diff --git a/components/engine/cli/command/secret/create.go b/components/engine/cli/command/secret/create.go new file mode 100644 index 0000000000..1c0e933f57 --- /dev/null +++ b/components/engine/cli/command/secret/create.go @@ -0,0 +1,57 @@ +package secret + +import ( + "context" + "fmt" + "io/ioutil" + "os" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type createOptions struct { + name string +} + +func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "create [name]", + Short: "Create a secret using stdin as content", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts := createOptions{ + name: args[0], + } + + return runSecretCreate(dockerCli, opts) + }, + } +} + +func runSecretCreate(dockerCli *command.DockerCli, opts createOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + secretData, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("Error reading content from STDIN: %v", err) + } + + spec := swarm.SecretSpec{ + Annotations: swarm.Annotations{ + Name: opts.name, + }, + Data: secretData, + } + + r, err := client.SecretCreate(ctx, spec) + if err != nil { + return err + } + + fmt.Fprintln(dockerCli.Out(), r.ID) + return nil +} diff --git a/components/engine/cli/command/secret/inspect.go b/components/engine/cli/command/secret/inspect.go new file mode 100644 index 0000000000..c8d5cd8f79 --- /dev/null +++ b/components/engine/cli/command/secret/inspect.go @@ -0,0 +1,42 @@ +package secret + +import ( + "context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/inspect" + "github.com/spf13/cobra" +) + +type inspectOptions struct { + name string + format string +} + +func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := inspectOptions{} + cmd := &cobra.Command{ + Use: "inspect [name]", + Short: "Inspect a secret", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.name = args[0] + return runSecretInspect(dockerCli, opts) + }, + } + + cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + return cmd +} + +func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + getRef := func(name string) (interface{}, []byte, error) { + return client.SecretInspectWithRaw(ctx, name) + } + + return inspect.Inspect(dockerCli.Out(), []string{opts.name}, opts.format, getRef) +} diff --git a/components/engine/cli/command/secret/ls.go b/components/engine/cli/command/secret/ls.go new file mode 100644 index 0000000000..1befdad9d0 --- /dev/null +++ b/components/engine/cli/command/secret/ls.go @@ -0,0 +1,62 @@ +package secret + +import ( + "context" + "fmt" + "text/tabwriter" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type listOptions struct { + quiet bool +} + +func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := listOptions{} + + cmd := &cobra.Command{ + Use: "ls", + Short: "List secrets", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runSecretList(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + + return cmd +} + +func runSecretList(dockerCli *command.DockerCli, opts listOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + secrets, err := client.SecretList(ctx, types.SecretListOptions{}) + if err != nil { + return err + } + + w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) + if opts.quiet { + for _, s := range secrets { + fmt.Fprintf(w, "%s\n", s.ID) + } + } else { + fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED\tSIZE") + fmt.Fprintf(w, "\n") + + for _, s := range secrets { + 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) + } + } + + w.Flush() + + return nil +} diff --git a/components/engine/cli/command/secret/remove.go b/components/engine/cli/command/secret/remove.go new file mode 100644 index 0000000000..f336c6161a --- /dev/null +++ b/components/engine/cli/command/secret/remove.go @@ -0,0 +1,43 @@ +package secret + +import ( + "context" + "fmt" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type removeOptions struct { + ids []string +} + +func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "rm [id]", + Short: "Remove a secret", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts := removeOptions{ + ids: args, + } + return runSecretRemove(dockerCli, opts) + }, + } +} + +func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + for _, id := range opts.ids { + if err := client.SecretRemove(ctx, id); err != nil { + return err + } + + fmt.Fprintln(dockerCli.Out(), id) + } + + return nil +} diff --git a/components/engine/cli/command/service/create.go b/components/engine/cli/command/service/create.go index d6c3ebdb9c..8fb9070e67 100644 --- a/components/engine/cli/command/service/create.go +++ b/components/engine/cli/command/service/create.go @@ -58,6 +58,13 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { return err } + // parse and validate secrets + secrets, err := parseSecrets(apiClient, opts.secrets) + if err != nil { + return err + } + service.TaskTemplate.ContainerSpec.Secrets = secrets + ctx := context.Background() // only send auth if flag was set diff --git a/components/engine/cli/command/service/opts.go b/components/engine/cli/command/service/opts.go index 827c4e5cdc..a4fd08881c 100644 --- a/components/engine/cli/command/service/opts.go +++ b/components/engine/cli/command/service/opts.go @@ -191,6 +191,19 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { return nets } +func convertSecrets(secrets []string) []*swarm.SecretReference { + sec := []*swarm.SecretReference{} + for _, s := range secrets { + sec = append(sec, &swarm.SecretReference{ + SecretID: s, + Mode: swarm.SecretReferenceFile, + Target: "", + }) + } + + return sec +} + type endpointOptions struct { mode string ports opts.ListOpts @@ -337,6 +350,7 @@ type serviceOptions struct { logDriver logDriverOptions healthcheck healthCheckOptions + secrets []string } func newServiceOptions() *serviceOptions { @@ -403,6 +417,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Options: opts.dnsOptions.GetAll(), }, StopGracePeriod: opts.stopGrace.Value(), + Secrets: convertSecrets(opts.secrets), }, Networks: convertNetworks(opts.networks.GetAll()), Resources: opts.resources.ToResourceRequirements(), @@ -488,6 +503,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") + flags.StringSliceVar(&opts.secrets, flagSecret, []string{}, "Specify secrets to expose to the service") } const ( @@ -553,4 +569,5 @@ const ( flagHealthRetries = "health-retries" flagHealthTimeout = "health-timeout" flagNoHealthcheck = "no-healthcheck" + flagSecret = "secret" ) diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go new file mode 100644 index 0000000000..41883fb445 --- /dev/null +++ b/components/engine/cli/command/service/parse.go @@ -0,0 +1,92 @@ +package service + +import ( + "context" + "fmt" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + swarmtypes "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" +) + +// parseSecretString parses the requested secret and returns the secret name +// and target. Expects format SECRET_NAME:TARGET +func parseSecretString(secretString string) (string, string, error) { + tokens := strings.Split(secretString, ":") + + secretName := strings.TrimSpace(tokens[0]) + targetName := "" + + if secretName == "" { + return "", "", fmt.Errorf("invalid secret name provided") + } + + if len(tokens) > 1 { + targetName = strings.TrimSpace(tokens[1]) + if targetName == "" { + return "", "", fmt.Errorf("invalid presentation name provided") + } + } else { + targetName = secretName + } + return secretName, targetName, nil +} + +// parseSecrets retrieves the secrets from the requested names and converts +// them to secret references to use with the spec +func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) { + lookupSecretNames := []string{} + needSecrets := make(map[string]*swarmtypes.SecretReference) + ctx := context.Background() + + for _, secret := range requestedSecrets { + n, t, err := parseSecretString(secret) + if err != nil { + return nil, err + } + + secretRef := &swarmtypes.SecretReference{ + SecretName: n, + Mode: swarmtypes.SecretReferenceFile, + Target: t, + } + + lookupSecretNames = append(lookupSecretNames, n) + needSecrets[n] = secretRef + } + + args := filters.NewArgs() + for _, s := range lookupSecretNames { + args.Add("names", s) + } + + secrets, err := client.SecretList(ctx, types.SecretListOptions{ + Filter: args, + }) + if err != nil { + return nil, err + } + + foundSecrets := make(map[string]*swarmtypes.Secret) + for _, secret := range secrets { + foundSecrets[secret.Spec.Annotations.Name] = &secret + } + + addedSecrets := []*swarmtypes.SecretReference{} + + for secretName, secretRef := range needSecrets { + s, ok := foundSecrets[secretName] + if !ok { + return nil, fmt.Errorf("secret not found: %s", secretName) + } + + // set the id for the ref to properly assign in swarm + // since swarm needs the ID instead of the name + secretRef.SecretID = s.ID + addedSecrets = append(addedSecrets, secretRef) + } + + return addedSecrets, nil +} diff --git a/components/engine/client/errors.go b/components/engine/client/errors.go index 53e2065332..db7294daa8 100644 --- a/components/engine/client/errors.go +++ b/components/engine/client/errors.go @@ -217,3 +217,25 @@ func (cli *Client) NewVersionError(APIrequired, feature string) error { } return nil } + +// secretNotFoundError implements an error returned when a secret is not found. +type secretNotFoundError struct { + name string +} + +// Error returns a string representation of a secretNotFoundError +func (e secretNotFoundError) Error() string { + return fmt.Sprintf("Error: No such secret: %s", e.name) +} + +// NoFound indicates that this error type is of NotFound +func (e secretNotFoundError) NotFound() bool { + return true +} + +// IsErrSecretNotFound returns true if the error is caused +// when a secret is not found. +func IsErrSecretNotFound(err error) bool { + _, ok := err.(secretNotFoundError) + return ok +} diff --git a/components/engine/client/interface.go b/components/engine/client/interface.go index 99b06709b5..49b66b1d17 100644 --- a/components/engine/client/interface.go +++ b/components/engine/client/interface.go @@ -23,6 +23,7 @@ type CommonAPIClient interface { NetworkAPIClient ServiceAPIClient SwarmAPIClient + SecretAPIClient SystemAPIClient VolumeAPIClient ClientVersion() string @@ -141,3 +142,11 @@ type VolumeAPIClient interface { VolumeRemove(ctx context.Context, volumeID string, force bool) error VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) } + +// SecretAPIClient defines API client methods for secrets +type SecretAPIClient interface { + SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) + SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) + SecretRemove(ctx context.Context, id string) error + SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) +} diff --git a/components/engine/client/secret_create.go b/components/engine/client/secret_create.go new file mode 100644 index 0000000000..de8b041567 --- /dev/null +++ b/components/engine/client/secret_create.go @@ -0,0 +1,24 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SecretCreate creates a new Secret. +func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) { + var headers map[string][]string + + var response types.SecretCreateResponse + resp, err := cli.post(ctx, "/secrets/create", nil, secret, headers) + if err != nil { + return response, err + } + + err = json.NewDecoder(resp.body).Decode(&response) + ensureReaderClosed(resp) + return response, err +} diff --git a/components/engine/client/secret_create_test.go b/components/engine/client/secret_create_test.go new file mode 100644 index 0000000000..d264eb6692 --- /dev/null +++ b/components/engine/client/secret_create_test.go @@ -0,0 +1,57 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestSecretCreateError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.SecretCreate(context.Background(), swarm.SecretSpec{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSecretCreate(t *testing.T) { + expectedURL := "/secrets/create" + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + b, err := json.Marshal(types.SecretCreateResponse{ + ID: "test_secret", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + r, err := client.SecretCreate(context.Background(), swarm.SecretSpec{}) + if err != nil { + t.Fatal(err) + } + if r.ID != "test_secret" { + t.Fatalf("expected `test_secret`, got %s", r.ID) + } +} diff --git a/components/engine/client/secret_inspect.go b/components/engine/client/secret_inspect.go new file mode 100644 index 0000000000..f774576118 --- /dev/null +++ b/components/engine/client/secret_inspect.go @@ -0,0 +1,34 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SecretInspectWithRaw returns the secret information with raw data +func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) { + resp, err := cli.get(ctx, "/secrets/"+id, nil, nil) + if err != nil { + if resp.statusCode == http.StatusNotFound { + return swarm.Secret{}, nil, secretNotFoundError{id} + } + return swarm.Secret{}, nil, err + } + defer ensureReaderClosed(resp) + + body, err := ioutil.ReadAll(resp.body) + if err != nil { + return swarm.Secret{}, nil, err + } + + var secret swarm.Secret + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&secret) + + return secret, body, err +} diff --git a/components/engine/client/secret_inspect_test.go b/components/engine/client/secret_inspect_test.go new file mode 100644 index 0000000000..423d986968 --- /dev/null +++ b/components/engine/client/secret_inspect_test.go @@ -0,0 +1,65 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestSecretInspectError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + } + + _, _, err := client.SecretInspectWithRaw(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSecretInspectSecretNotFound(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusNotFound, "Server error")), + } + + _, _, err := client.SecretInspectWithRaw(context.Background(), "unknown") + if err == nil || !IsErrSecretNotFound(err) { + t.Fatalf("expected an secretNotFoundError error, got %v", err) + } +} + +func TestSecretInspect(t *testing.T) { + expectedURL := "/secrets/secret_id" + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal(swarm.Secret{ + ID: "secret_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + secretInspect, _, err := client.SecretInspectWithRaw(context.Background(), "secret_id") + if err != nil { + t.Fatal(err) + } + if secretInspect.ID != "secret_id" { + t.Fatalf("expected `secret_id`, got %s", secretInspect.ID) + } +} diff --git a/components/engine/client/secret_list.go b/components/engine/client/secret_list.go new file mode 100644 index 0000000000..5e9d2b5098 --- /dev/null +++ b/components/engine/client/secret_list.go @@ -0,0 +1,35 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SecretList returns the list of secrets. +func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { + query := url.Values{} + + if options.Filter.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filter) + if err != nil { + return nil, err + } + + query.Set("filters", filterJSON) + } + + resp, err := cli.get(ctx, "/secrets", query, nil) + if err != nil { + return nil, err + } + + var secrets []swarm.Secret + err = json.NewDecoder(resp.body).Decode(&secrets) + ensureReaderClosed(resp) + return secrets, err +} diff --git a/components/engine/client/secret_list_test.go b/components/engine/client/secret_list_test.go new file mode 100644 index 0000000000..174963c7ee --- /dev/null +++ b/components/engine/client/secret_list_test.go @@ -0,0 +1,94 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestSecretListError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.SecretList(context.Background(), types.SecretListOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSecretList(t *testing.T) { + expectedURL := "/secrets" + + filters := filters.NewArgs() + filters.Add("label", "label1") + filters.Add("label", "label2") + + listCases := []struct { + options types.SecretListOptions + expectedQueryParams map[string]string + }{ + { + options: types.SecretListOptions{}, + expectedQueryParams: map[string]string{ + "filters": "", + }, + }, + { + options: types.SecretListOptions{ + Filter: filters, + }, + expectedQueryParams: map[string]string{ + "filters": `{"label":{"label1":true,"label2":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + content, err := json.Marshal([]swarm.Secret{ + { + ID: "secret_id1", + }, + { + ID: "secret_id2", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + secrets, err := client.SecretList(context.Background(), listCase.options) + if err != nil { + t.Fatal(err) + } + if len(secrets) != 2 { + t.Fatalf("expected 2 secrets, got %v", secrets) + } + } +} diff --git a/components/engine/client/secret_remove.go b/components/engine/client/secret_remove.go new file mode 100644 index 0000000000..1955b988a9 --- /dev/null +++ b/components/engine/client/secret_remove.go @@ -0,0 +1,10 @@ +package client + +import "golang.org/x/net/context" + +// SecretRemove removes a Secret. +func (cli *Client) SecretRemove(ctx context.Context, id string) error { + resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/client/secret_remove_test.go b/components/engine/client/secret_remove_test.go new file mode 100644 index 0000000000..f269f787d2 --- /dev/null +++ b/components/engine/client/secret_remove_test.go @@ -0,0 +1,47 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestSecretRemoveError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.SecretRemove(context.Background(), "secret_id") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSecretRemove(t *testing.T) { + expectedURL := "/secrets/secret_id" + + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "DELETE" { + return nil, fmt.Errorf("expected DELETE method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + }, nil + }), + } + + err := client.SecretRemove(context.Background(), "secret_id") + if err != nil { + t.Fatal(err) + } +} diff --git a/components/engine/container/container.go b/components/engine/container/container.go index 722271be96..74d080d46c 100644 --- a/components/engine/container/container.go +++ b/components/engine/container/container.go @@ -89,8 +89,9 @@ type CommonContainer struct { HasBeenStartedBefore bool HasBeenManuallyStopped bool // used for unless-stopped restart policy MountPoints map[string]*volume.MountPoint - HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable - ExecCommands *exec.Store `json:"-"` + HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable + ExecCommands *exec.Store `json:"-"` + Secrets []*containertypes.ContainerSecret `json:"-"` // do not serialize // logDriver for closing LogDriver logger.Logger `json:"-"` LogCopier *logger.Copier `json:"-"` diff --git a/components/engine/container/container_unix.go b/components/engine/container/container_unix.go index c38f750667..099073b83e 100644 --- a/components/engine/container/container_unix.go +++ b/components/engine/container/container_unix.go @@ -23,7 +23,10 @@ import ( ) // DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container -const DefaultSHMSize int64 = 67108864 +const ( + DefaultSHMSize int64 = 67108864 + containerSecretMountPath = "/run/secrets" +) // Container holds the fields specific to unixen implementations. // See CommonContainer for standard fields common to all containers. @@ -175,6 +178,10 @@ func (container *Container) NetworkMounts() []Mount { return mounts } +func (container *Container) SecretMountPath() string { + return filepath.Join(container.Root, "secrets") +} + // CopyImagePathContent copies files in destination to the volume. func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error { rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, destination), container.BaseFS) @@ -260,6 +267,26 @@ func (container *Container) IpcMounts() []Mount { return mounts } +// SecretMounts returns the list of Secret mounts +func (container *Container) SecretMounts() []Mount { + var mounts []Mount + + if len(container.Secrets) > 0 { + mounts = append(mounts, Mount{ + Source: container.SecretMountPath(), + Destination: containerSecretMountPath, + Writable: false, + }) + } + + return mounts +} + +// UnmountSecrets unmounts the local tmpfs for secrets +func (container *Container) UnmountSecrets() error { + return detachMounted(container.SecretMountPath()) +} + // UpdateContainer updates configuration of a container. func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error { container.Lock() diff --git a/components/engine/daemon/cluster/convert/container.go b/components/engine/daemon/cluster/convert/container.go index 38749dd8b2..38143c6861 100644 --- a/components/engine/daemon/cluster/convert/container.go +++ b/components/engine/daemon/cluster/convert/container.go @@ -23,6 +23,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec { User: c.User, Groups: c.Groups, TTY: c.TTY, + Secrets: secretReferencesFromGRPC(c.Secrets), } if c.DNSConfig != nil { @@ -75,6 +76,47 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec { return containerSpec } +func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference { + refs := []*swarmapi.SecretReference{} + for _, s := range sr { + var mode swarmapi.SecretReference_Mode + switch s.Mode { + case types.SecretReferenceSystem: + mode = swarmapi.SecretReference_SYSTEM + default: + mode = swarmapi.SecretReference_FILE + } + refs = append(refs, &swarmapi.SecretReference{ + SecretID: s.SecretID, + SecretName: s.SecretName, + Target: s.Target, + Mode: mode, + }) + } + + return refs +} +func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference { + refs := []*types.SecretReference{} + for _, s := range sr { + var mode types.SecretReferenceMode + switch s.Mode { + case swarmapi.SecretReference_SYSTEM: + mode = types.SecretReferenceSystem + default: + mode = types.SecretReferenceFile + } + refs = append(refs, &types.SecretReference{ + SecretID: s.SecretID, + SecretName: s.SecretName, + Target: s.Target, + Mode: mode, + }) + } + + return refs +} + func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) { containerSpec := &swarmapi.ContainerSpec{ Image: c.Image, @@ -87,6 +129,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) { User: c.User, Groups: c.Groups, TTY: c.TTY, + Secrets: secretReferencesToGRPC(c.Secrets), } if c.DNSConfig != nil { diff --git a/components/engine/daemon/cluster/convert/secret.go b/components/engine/daemon/cluster/convert/secret.go new file mode 100644 index 0000000000..e26fe84ce4 --- /dev/null +++ b/components/engine/daemon/cluster/convert/secret.go @@ -0,0 +1,46 @@ +package convert + +import ( + "github.com/Sirupsen/logrus" + swarmtypes "github.com/docker/docker/api/types/swarm" + swarmapi "github.com/docker/swarmkit/api" + "github.com/docker/swarmkit/protobuf/ptypes" +) + +// SecretFromGRPC converts a grpc Secret to a Secret. +func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret { + logrus.Debugf("%+v", s) + secret := swarmtypes.Secret{ + ID: s.ID, + Digest: s.Digest, + SecretSize: s.SecretSize, + } + + // Meta + secret.Version.Index = s.Meta.Version.Index + secret.CreatedAt, _ = ptypes.Timestamp(s.Meta.CreatedAt) + secret.UpdatedAt, _ = ptypes.Timestamp(s.Meta.UpdatedAt) + + secret.Spec = &swarmtypes.SecretSpec{ + Annotations: swarmtypes.Annotations{ + Name: s.Spec.Annotations.Name, + Labels: s.Spec.Annotations.Labels, + }, + Data: s.Spec.Data, + } + + return secret +} + +// SecretSpecToGRPC converts Secret to a grpc Secret. +func SecretSpecToGRPC(s swarmtypes.SecretSpec) (swarmapi.SecretSpec, error) { + spec := swarmapi.SecretSpec{ + Annotations: swarmapi.Annotations{ + Name: s.Name, + Labels: s.Labels, + }, + Data: s.Data, + } + + return spec, nil +} diff --git a/components/engine/daemon/cluster/executor/backend.go b/components/engine/daemon/cluster/executor/backend.go index fb88613c1d..76b9d8888f 100644 --- a/components/engine/daemon/cluster/executor/backend.go +++ b/components/engine/daemon/cluster/executor/backend.go @@ -34,6 +34,7 @@ type Backend interface { ContainerWaitWithContext(ctx context.Context, name string) error ContainerRm(name string, config *types.ContainerRmConfig) error ContainerKill(name string, sig uint64) error + SetContainerSecrets(name string, secrets []*container.ContainerSecret) error SystemInfo() (*types.Info, error) VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error) Containers(config *types.ContainerListOptions) ([]*types.Container, error) diff --git a/components/engine/daemon/cluster/executor/container/adapter.go b/components/engine/daemon/cluster/executor/container/adapter.go index 618f4b22b4..40a08401f4 100644 --- a/components/engine/daemon/cluster/executor/container/adapter.go +++ b/components/engine/daemon/cluster/executor/container/adapter.go @@ -17,6 +17,7 @@ import ( "github.com/docker/docker/api/types/versions" executorpkg "github.com/docker/docker/daemon/cluster/executor" "github.com/docker/libnetwork" + "github.com/docker/swarmkit/agent/exec" "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/log" "golang.org/x/net/context" @@ -29,9 +30,10 @@ import ( type containerAdapter struct { backend executorpkg.Backend container *containerConfig + secrets exec.SecretProvider } -func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapter, error) { +func newContainerAdapter(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*containerAdapter, error) { ctnr, err := newContainerConfig(task) if err != nil { return nil, err @@ -40,6 +42,7 @@ func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapt return &containerAdapter{ container: ctnr, backend: b, + secrets: secrets, }, nil } @@ -215,6 +218,35 @@ func (c *containerAdapter) create(ctx context.Context) error { } } + secrets := []*containertypes.ContainerSecret{} + for _, s := range c.container.task.Spec.GetContainer().Secrets { + sec := c.secrets.Get(s.SecretID) + if sec == nil { + logrus.Warnf("unable to get secret %s from provider", s.SecretID) + continue + } + + name := sec.Spec.Annotations.Name + target := s.Target + if target == "" { + target = name + } + secrets = append(secrets, &containertypes.ContainerSecret{ + Name: name, + Target: target, + Data: sec.Spec.Data, + // TODO (ehazlett): enable configurable uid, gid, mode + Uid: 0, + Gid: 0, + Mode: 0444, + }) + } + + // configure secrets + if err := c.backend.SetContainerSecrets(cr.ID, secrets); err != nil { + return err + } + if err := c.backend.UpdateContainerServiceConfig(cr.ID, c.container.serviceConfig()); err != nil { return err } diff --git a/components/engine/daemon/cluster/executor/container/attachment.go b/components/engine/daemon/cluster/executor/container/attachment.go index f3b738f70b..a897268b26 100644 --- a/components/engine/daemon/cluster/executor/container/attachment.go +++ b/components/engine/daemon/cluster/executor/container/attachment.go @@ -4,6 +4,7 @@ import ( executorpkg "github.com/docker/docker/daemon/cluster/executor" "github.com/docker/swarmkit/api" "golang.org/x/net/context" + "src/github.com/docker/swarmkit/agent/exec" ) // networkAttacherController implements agent.Controller against docker's API. @@ -19,8 +20,8 @@ type networkAttacherController struct { closed chan struct{} } -func newNetworkAttacherController(b executorpkg.Backend, task *api.Task) (*networkAttacherController, error) { - adapter, err := newContainerAdapter(b, task) +func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*networkAttacherController, error) { + adapter, err := newContainerAdapter(b, task, secrets) if err != nil { return nil, err } diff --git a/components/engine/daemon/cluster/executor/container/controller.go b/components/engine/daemon/cluster/executor/container/controller.go index 0185e415b5..d3d80075a4 100644 --- a/components/engine/daemon/cluster/executor/container/controller.go +++ b/components/engine/daemon/cluster/executor/container/controller.go @@ -33,8 +33,8 @@ type controller struct { var _ exec.Controller = &controller{} // NewController returns a docker exec runner for the provided task. -func newController(b executorpkg.Backend, task *api.Task) (*controller, error) { - adapter, err := newContainerAdapter(b, task) +func newController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*controller, error) { + adapter, err := newContainerAdapter(b, task, secrets) if err != nil { return nil, err } diff --git a/components/engine/daemon/cluster/executor/container/executor.go b/components/engine/daemon/cluster/executor/container/executor.go index 844821b83e..fdd270006d 100644 --- a/components/engine/daemon/cluster/executor/container/executor.go +++ b/components/engine/daemon/cluster/executor/container/executor.go @@ -18,6 +18,10 @@ type executor struct { backend executorpkg.Backend } +type secretProvider interface { + Get(secretID string) *api.Secret +} + // NewExecutor returns an executor from the docker client. func NewExecutor(b executorpkg.Backend) exec.Executor { return &executor{ @@ -120,12 +124,12 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error { } // Controller returns a docker container runner. -func (e *executor) Controller(t *api.Task) (exec.Controller, error) { +func (e *executor) Controller(t *api.Task, secrets exec.SecretProvider) (exec.Controller, error) { if t.Spec.GetAttachment() != nil { - return newNetworkAttacherController(e.backend, t) + return newNetworkAttacherController(e.backend, t, secrets) } - ctlr, err := newController(e.backend, t) + ctlr, err := newController(e.backend, t, secrets) if err != nil { return nil, err } diff --git a/components/engine/daemon/cluster/executor/container/health_test.go b/components/engine/daemon/cluster/executor/container/health_test.go index 16a1e0c096..99cf7502af 100644 --- a/components/engine/daemon/cluster/executor/container/health_test.go +++ b/components/engine/daemon/cluster/executor/container/health_test.go @@ -54,7 +54,7 @@ func TestHealthStates(t *testing.T) { EventsService: e, } - controller, err := newController(daemon, task) + controller, err := newController(daemon, task, nil) if err != nil { t.Fatalf("create controller fail %v", err) } diff --git a/components/engine/daemon/cluster/executor/container/validate_test.go b/components/engine/daemon/cluster/executor/container/validate_test.go index d911c1ebec..5f202d5859 100644 --- a/components/engine/daemon/cluster/executor/container/validate_test.go +++ b/components/engine/daemon/cluster/executor/container/validate_test.go @@ -26,7 +26,7 @@ func newTestControllerWithMount(m api.Mount) (*controller, error) { }, }, }, - }) + }, nil) } func TestControllerValidateMountBind(t *testing.T) { diff --git a/components/engine/daemon/cluster/filters.go b/components/engine/daemon/cluster/filters.go index a1d800e56c..88668edaac 100644 --- a/components/engine/daemon/cluster/filters.go +++ b/components/engine/daemon/cluster/filters.go @@ -96,3 +96,21 @@ func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) e return f, nil } + +func newListSecretsFilters(filter filters.Args) (*swarmapi.ListSecretsRequest_Filters, error) { + accepted := map[string]bool{ + "names": true, + "name": true, + "id": true, + "label": true, + } + if err := filter.Validate(accepted); err != nil { + return nil, err + } + return &swarmapi.ListSecretsRequest_Filters{ + Names: filter.Get("names"), + NamePrefixes: filter.Get("name"), + IDPrefixes: filter.Get("id"), + Labels: runconfigopts.ConvertKVStringsToMap(filter.Get("label")), + }, nil +} diff --git a/components/engine/daemon/cluster/secrets.go b/components/engine/daemon/cluster/secrets.go new file mode 100644 index 0000000000..305f2afab5 --- /dev/null +++ b/components/engine/daemon/cluster/secrets.go @@ -0,0 +1,131 @@ +package cluster + +import ( + apitypes "github.com/docker/docker/api/types" + types "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/daemon/cluster/convert" + swarmapi "github.com/docker/swarmkit/api" +) + +// GetSecret returns a secret from a managed swarm cluster +func (c *Cluster) GetSecret(id string) (types.Secret, error) { + ctx, cancel := c.getRequestContext() + defer cancel() + + r, err := c.node.client.GetSecret(ctx, &swarmapi.GetSecretRequest{SecretID: id}) + if err != nil { + return types.Secret{}, err + } + + return convert.SecretFromGRPC(r.Secret), nil +} + +// GetSecrets returns all secrets of a managed swarm cluster. +func (c *Cluster) GetSecrets(options apitypes.SecretListOptions) ([]types.Secret, error) { + c.RLock() + defer c.RUnlock() + + if !c.isActiveManager() { + return nil, c.errNoManager() + } + + filters, err := newListSecretsFilters(options.Filter) + if err != nil { + return nil, err + } + ctx, cancel := c.getRequestContext() + defer cancel() + + r, err := c.node.client.ListSecrets(ctx, + &swarmapi.ListSecretsRequest{Filters: filters}) + if err != nil { + return nil, err + } + + secrets := []types.Secret{} + + for _, secret := range r.Secrets { + secrets = append(secrets, convert.SecretFromGRPC(secret)) + } + + return secrets, nil +} + +// CreateSecret creates a new secret in a managed swarm cluster. +func (c *Cluster) CreateSecret(s types.SecretSpec) (string, error) { + c.RLock() + defer c.RUnlock() + + if !c.isActiveManager() { + return "", c.errNoManager() + } + + ctx, cancel := c.getRequestContext() + defer cancel() + + secretSpec, err := convert.SecretSpecToGRPC(s) + if err != nil { + return "", err + } + + r, err := c.node.client.CreateSecret(ctx, + &swarmapi.CreateSecretRequest{Spec: &secretSpec}) + if err != nil { + return "", err + } + + return r.Secret.ID, nil +} + +// RemoveSecret removes a secret from a managed swarm cluster. +func (c *Cluster) RemoveSecret(id string) error { + c.RLock() + defer c.RUnlock() + + if !c.isActiveManager() { + return c.errNoManager() + } + + ctx, cancel := c.getRequestContext() + defer cancel() + + req := &swarmapi.RemoveSecretRequest{ + SecretID: id, + } + + if _, err := c.node.client.RemoveSecret(ctx, req); err != nil { + return err + } + return nil +} + +// UpdateSecret updates a secret in a managed swarm cluster. +func (c *Cluster) UpdateSecret(id string, version uint64, spec types.SecretSpec) error { + c.RLock() + defer c.RUnlock() + + if !c.isActiveManager() { + return c.errNoManager() + } + + ctx, cancel := c.getRequestContext() + defer cancel() + + secretSpec, err := convert.SecretSpecToGRPC(spec) + if err != nil { + return err + } + + if _, err := c.client.UpdateSecret(ctx, + &swarmapi.UpdateSecretRequest{ + SecretID: id, + SecretVersion: &swarmapi.Version{ + Index: version, + }, + Spec: &secretSpec, + }); err != nil { + return err + } + + return nil +} diff --git a/components/engine/daemon/container_operations_unix.go b/components/engine/daemon/container_operations_unix.go index 66b11bb288..a3031ba3e6 100644 --- a/components/engine/daemon/container_operations_unix.go +++ b/components/engine/daemon/container_operations_unix.go @@ -4,6 +4,7 @@ package daemon import ( "fmt" + "io/ioutil" "os" "path/filepath" "strconv" @@ -18,6 +19,7 @@ import ( "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/runconfig" + "github.com/docker/engine-api/types/mount" "github.com/docker/libnetwork" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/devices" @@ -139,6 +141,43 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error { return nil } + +func (daemon *Daemon) setupSecretDir(c *container.Container) error { + localMountPath := c.SecretMountPath() + logrus.Debugf("secrets: setting up secret dir: %s", localMountPath) + + // create tmpfs + if err := os.MkdirAll(localMountPath, 0700); err != nil { + return fmt.Errorf("error creating secret local mount path: %s", err) + } + if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev"); err != nil { + return fmt.Errorf("unable to setup secret mount: %s", err) + } + + for _, s := range c.Secrets { + fPath := filepath.Join(localMountPath, s.Target) + if err := os.MkdirAll(filepath.Dir(fPath), 0700); err != nil { + return fmt.Errorf("error creating secret mount path: %s", err) + } + + logrus.Debugf("injecting secret: name=%s path=%s", s.Name, fPath) + if err := ioutil.WriteFile(fPath, s.Data, s.Mode); err != nil { + return fmt.Errorf("error injecting secret: %s", err) + } + + if err := os.Chown(fPath, s.Uid, s.Gid); err != nil { + return fmt.Errorf("error setting ownership for secret: %s", err) + } + } + + // remount secrets ro + if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro"); err != nil { + return fmt.Errorf("unable to remount secret dir as readonly: %s", err) + } + + return nil +} + func killProcessDirectly(container *container.Container) error { if _, err := container.WaitStop(10 * time.Second); err != nil { // Ensure that we don't kill ourselves diff --git a/components/engine/daemon/daemon.go b/components/engine/daemon/daemon.go index 8bbc8f42d4..c3efbac2bd 100644 --- a/components/engine/daemon/daemon.go +++ b/components/engine/daemon/daemon.go @@ -854,6 +854,7 @@ func (daemon *Daemon) Unmount(container *container.Container) error { logrus.Errorf("Error unmounting container %s: %s", container.ID, err) return err } + return nil } diff --git a/components/engine/daemon/oci_linux.go b/components/engine/daemon/oci_linux.go index 5cac76843d..d74ddd053b 100644 --- a/components/engine/daemon/oci_linux.go +++ b/components/engine/daemon/oci_linux.go @@ -702,16 +702,23 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { return nil, err } + if err := daemon.setupSecretDir(c); err != nil { + return nil, err + } + ms, err := daemon.setupMounts(c) if err != nil { return nil, err } ms = append(ms, c.IpcMounts()...) + tmpfsMounts, err := c.TmpfsMounts() if err != nil { return nil, err } ms = append(ms, tmpfsMounts...) + + ms = append(ms, c.SecretMounts()...) sort.Sort(mounts(ms)) if err := setMounts(daemon, &s, c, ms); err != nil { return nil, fmt.Errorf("linux mounts: %v", err) diff --git a/components/engine/daemon/secrets.go b/components/engine/daemon/secrets.go new file mode 100644 index 0000000000..342b33c2a3 --- /dev/null +++ b/components/engine/daemon/secrets.go @@ -0,0 +1,22 @@ +package daemon + +import ( + "github.com/Sirupsen/logrus" + containertypes "github.com/docker/docker/api/types/container" +) + +func (daemon *Daemon) SetContainerSecrets(name string, secrets []*containertypes.ContainerSecret) error { + if !secretsSupported() { + logrus.Warn("secrets are not supported on this platform") + return nil + } + + c, err := daemon.GetContainer(name) + if err != nil { + return err + } + + c.Secrets = secrets + + return nil +} diff --git a/components/engine/daemon/secrets_linux.go b/components/engine/daemon/secrets_linux.go new file mode 100644 index 0000000000..fca4e12598 --- /dev/null +++ b/components/engine/daemon/secrets_linux.go @@ -0,0 +1,7 @@ +// +build linux + +package daemon + +func secretsSupported() bool { + return true +} diff --git a/components/engine/daemon/secrets_unsupported.go b/components/engine/daemon/secrets_unsupported.go new file mode 100644 index 0000000000..d6f36fda1e --- /dev/null +++ b/components/engine/daemon/secrets_unsupported.go @@ -0,0 +1,7 @@ +// +build !linux + +package daemon + +func secretsSupported() bool { + return false +} diff --git a/components/engine/daemon/start.go b/components/engine/daemon/start.go index c642ce22a8..af08ccdf39 100644 --- a/components/engine/daemon/start.go +++ b/components/engine/daemon/start.go @@ -212,6 +212,10 @@ func (daemon *Daemon) Cleanup(container *container.Container) { } } + if err := container.UnmountSecrets(); err != nil { + logrus.Warnf("%s cleanup: failed to unmount secrets: %s", container.ID, err) + } + for _, eConfig := range container.ExecCommands.Commands() { daemon.unregisterExecCommand(container, eConfig) } From dde21cf7dc5fbf458cf0e8cec86c4e60ffedf2a6 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Wed, 26 Oct 2016 13:30:53 -0700 Subject: [PATCH 03/37] review changes - fix lint issues - use errors pkg for wrapping errors - cleanup on error when setting up secrets mount - fix erroneous import - remove unneeded switch for secret reference mode - return single mount for secrets instead of slice Signed-off-by: Evan Hazlett Upstream-commit: 857e60c2f943a09e3ec0ac0f236821b797935900 Component: engine --- components/engine/api/types/swarm/secret.go | 2 +- .../engine/cli/command/service/parse.go | 8 ++++ components/engine/container/container_unix.go | 15 ++++--- .../daemon/cluster/convert/container.go | 7 +-- .../engine/daemon/cluster/convert/secret.go | 8 +--- .../cluster/executor/container/attachment.go | 2 +- components/engine/daemon/cluster/secrets.go | 10 +---- .../daemon/container_operations_unix.go | 44 ++++++++++++++----- components/engine/daemon/oci_linux.go | 3 +- components/engine/daemon/secrets.go | 3 +- .../engine/integration-cli/daemon_swarm.go | 10 +++++ .../integration-cli/docker_api_swarm_test.go | 24 ++++++++++ 12 files changed, 96 insertions(+), 40 deletions(-) diff --git a/components/engine/api/types/swarm/secret.go b/components/engine/api/types/swarm/secret.go index 3323ba212d..0700177588 100644 --- a/components/engine/api/types/swarm/secret.go +++ b/components/engine/api/types/swarm/secret.go @@ -11,7 +11,7 @@ type Secret struct { type SecretSpec struct { Annotations - Data []byte `json",omitempty"` + Data []byte `json:",omitempty"` } type SecretReferenceMode int diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go index 41883fb445..596d8e50d8 100644 --- a/components/engine/cli/command/service/parse.go +++ b/components/engine/cli/command/service/parse.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "path/filepath" "strings" "github.com/docker/docker/api/types" @@ -31,6 +32,13 @@ func parseSecretString(secretString string) (string, string, error) { } else { targetName = secretName } + + // ensure target is a filename only; no paths allowed + tDir, _ := filepath.Split(targetName) + if tDir != "" { + return "", "", fmt.Errorf("target must not have a path") + } + return secretName, targetName, nil } diff --git a/components/engine/container/container_unix.go b/components/engine/container/container_unix.go index 099073b83e..6fc6c53c27 100644 --- a/components/engine/container/container_unix.go +++ b/components/engine/container/container_unix.go @@ -22,8 +22,8 @@ import ( "golang.org/x/sys/unix" ) -// DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container const ( + // DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container DefaultSHMSize int64 = 67108864 containerSecretMountPath = "/run/secrets" ) @@ -178,6 +178,7 @@ func (container *Container) NetworkMounts() []Mount { return mounts } +// SecretMountPath returns the path of the secret mount for the container func (container *Container) SecretMountPath() string { return filepath.Join(container.Root, "secrets") } @@ -267,19 +268,19 @@ func (container *Container) IpcMounts() []Mount { return mounts } -// SecretMounts returns the list of Secret mounts -func (container *Container) SecretMounts() []Mount { - var mounts []Mount +// SecretMount returns the list of Secret mounts +func (container *Container) SecretMount() Mount { + var mount Mount if len(container.Secrets) > 0 { - mounts = append(mounts, Mount{ + mount = Mount{ Source: container.SecretMountPath(), Destination: containerSecretMountPath, Writable: false, - }) + } } - return mounts + return mount } // UnmountSecrets unmounts the local tmpfs for secrets diff --git a/components/engine/daemon/cluster/convert/container.go b/components/engine/daemon/cluster/convert/container.go index 38143c6861..6436b01c60 100644 --- a/components/engine/daemon/cluster/convert/container.go +++ b/components/engine/daemon/cluster/convert/container.go @@ -79,12 +79,9 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec { func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference { refs := []*swarmapi.SecretReference{} for _, s := range sr { - var mode swarmapi.SecretReference_Mode - switch s.Mode { - case types.SecretReferenceSystem: + mode := swarmapi.SecretReference_FILE + if s.Mode == types.SecretReferenceSystem { mode = swarmapi.SecretReference_SYSTEM - default: - mode = swarmapi.SecretReference_FILE } refs = append(refs, &swarmapi.SecretReference{ SecretID: s.SecretID, diff --git a/components/engine/daemon/cluster/convert/secret.go b/components/engine/daemon/cluster/convert/secret.go index e26fe84ce4..fc5b43594f 100644 --- a/components/engine/daemon/cluster/convert/secret.go +++ b/components/engine/daemon/cluster/convert/secret.go @@ -1,7 +1,6 @@ package convert import ( - "github.com/Sirupsen/logrus" swarmtypes "github.com/docker/docker/api/types/swarm" swarmapi "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/protobuf/ptypes" @@ -9,7 +8,6 @@ import ( // SecretFromGRPC converts a grpc Secret to a Secret. func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret { - logrus.Debugf("%+v", s) secret := swarmtypes.Secret{ ID: s.ID, Digest: s.Digest, @@ -33,14 +31,12 @@ func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret { } // SecretSpecToGRPC converts Secret to a grpc Secret. -func SecretSpecToGRPC(s swarmtypes.SecretSpec) (swarmapi.SecretSpec, error) { - spec := swarmapi.SecretSpec{ +func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec { + return swarmapi.SecretSpec{ Annotations: swarmapi.Annotations{ Name: s.Name, Labels: s.Labels, }, Data: s.Data, } - - return spec, nil } diff --git a/components/engine/daemon/cluster/executor/container/attachment.go b/components/engine/daemon/cluster/executor/container/attachment.go index a897268b26..4dce78cd46 100644 --- a/components/engine/daemon/cluster/executor/container/attachment.go +++ b/components/engine/daemon/cluster/executor/container/attachment.go @@ -2,9 +2,9 @@ package container import ( executorpkg "github.com/docker/docker/daemon/cluster/executor" + "github.com/docker/swarmkit/agent/exec" "github.com/docker/swarmkit/api" "golang.org/x/net/context" - "src/github.com/docker/swarmkit/agent/exec" ) // networkAttacherController implements agent.Controller against docker's API. diff --git a/components/engine/daemon/cluster/secrets.go b/components/engine/daemon/cluster/secrets.go index 305f2afab5..ca795192be 100644 --- a/components/engine/daemon/cluster/secrets.go +++ b/components/engine/daemon/cluster/secrets.go @@ -63,10 +63,7 @@ func (c *Cluster) CreateSecret(s types.SecretSpec) (string, error) { ctx, cancel := c.getRequestContext() defer cancel() - secretSpec, err := convert.SecretSpecToGRPC(s) - if err != nil { - return "", err - } + secretSpec := convert.SecretSpecToGRPC(s) r, err := c.node.client.CreateSecret(ctx, &swarmapi.CreateSecretRequest{Spec: &secretSpec}) @@ -111,10 +108,7 @@ func (c *Cluster) UpdateSecret(id string, version uint64, spec types.SecretSpec) ctx, cancel := c.getRequestContext() defer cancel() - secretSpec, err := convert.SecretSpecToGRPC(spec) - if err != nil { - return err - } + secretSpec := convert.SecretSpecToGRPC(spec) if _, err := c.client.UpdateSecret(ctx, &swarmapi.UpdateSecretRequest{ diff --git a/components/engine/daemon/container_operations_unix.go b/components/engine/daemon/container_operations_unix.go index a3031ba3e6..e208962030 100644 --- a/components/engine/daemon/container_operations_unix.go +++ b/components/engine/daemon/container_operations_unix.go @@ -13,6 +13,7 @@ import ( "time" "github.com/Sirupsen/logrus" + "github.com/cloudflare/cfssl/log" containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/container" "github.com/docker/docker/daemon/links" @@ -25,6 +26,7 @@ import ( "github.com/opencontainers/runc/libcontainer/devices" "github.com/opencontainers/runc/libcontainer/label" "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" ) func u32Ptr(i int64) *uint32 { u := uint32(i); return &u } @@ -146,36 +148,58 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) error { localMountPath := c.SecretMountPath() logrus.Debugf("secrets: setting up secret dir: %s", localMountPath) + var setupErr error + + defer func(err error) { + if err != nil { + // cleanup + _ = detachMounted(localMountPath) + + if err := os.RemoveAll(localMountPath); err != nil { + log.Errorf("error cleaning up secret mount: %s", err) + } + } + }(setupErr) + // create tmpfs if err := os.MkdirAll(localMountPath, 0700); err != nil { - return fmt.Errorf("error creating secret local mount path: %s", err) + setupErr = errors.Wrap(err, "error creating secret local mount path") } if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev"); err != nil { - return fmt.Errorf("unable to setup secret mount: %s", err) + setupErr = errors.Wrap(err, "unable to setup secret mount") } for _, s := range c.Secrets { - fPath := filepath.Join(localMountPath, s.Target) - if err := os.MkdirAll(filepath.Dir(fPath), 0700); err != nil { - return fmt.Errorf("error creating secret mount path: %s", err) + // ensure that the target is a filename only; no paths allowed + tDir, tPath := filepath.Split(s.Target) + if tDir != "" { + setupErr = fmt.Errorf("error creating secret: secret must not have a path") } - logrus.Debugf("injecting secret: name=%s path=%s", s.Name, fPath) + fPath := filepath.Join(localMountPath, tPath) + if err := os.MkdirAll(filepath.Dir(fPath), 0700); err != nil { + setupErr = errors.Wrap(err, "error creating secret mount path") + } + + logrus.WithFields(logrus.Fields{ + "name": s.Name, + "path": fPath, + }).Debug("injecting secret") if err := ioutil.WriteFile(fPath, s.Data, s.Mode); err != nil { - return fmt.Errorf("error injecting secret: %s", err) + setupErr = errors.Wrap(err, "error injecting secret") } if err := os.Chown(fPath, s.Uid, s.Gid); err != nil { - return fmt.Errorf("error setting ownership for secret: %s", err) + setupErr = errors.Wrap(err, "error setting ownership for secret") } } // remount secrets ro if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro"); err != nil { - return fmt.Errorf("unable to remount secret dir as readonly: %s", err) + setupErr = errors.Wrap(err, "unable to remount secret dir as readonly") } - return nil + return setupErr } func killProcessDirectly(container *container.Container) error { diff --git a/components/engine/daemon/oci_linux.go b/components/engine/daemon/oci_linux.go index d74ddd053b..6889a737c5 100644 --- a/components/engine/daemon/oci_linux.go +++ b/components/engine/daemon/oci_linux.go @@ -718,7 +718,8 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { } ms = append(ms, tmpfsMounts...) - ms = append(ms, c.SecretMounts()...) + ms = append(ms, c.SecretMount()) + sort.Sort(mounts(ms)) if err := setMounts(daemon, &s, c, ms); err != nil { return nil, fmt.Errorf("linux mounts: %v", err) diff --git a/components/engine/daemon/secrets.go b/components/engine/daemon/secrets.go index 342b33c2a3..1d13adaaa4 100644 --- a/components/engine/daemon/secrets.go +++ b/components/engine/daemon/secrets.go @@ -5,8 +5,9 @@ import ( containertypes "github.com/docker/docker/api/types/container" ) +// SetContainerSecrets sets the container secrets needed func (daemon *Daemon) SetContainerSecrets(name string, secrets []*containertypes.ContainerSecret) error { - if !secretsSupported() { + if !secretsSupported() && len(secrets) > 0 { logrus.Warn("secrets are not supported on this platform") return nil } diff --git a/components/engine/integration-cli/daemon_swarm.go b/components/engine/integration-cli/daemon_swarm.go index 50e464cdd2..222fa018e9 100644 --- a/components/engine/integration-cli/daemon_swarm.go +++ b/components/engine/integration-cli/daemon_swarm.go @@ -284,6 +284,16 @@ func (d *SwarmDaemon) listServices(c *check.C) []swarm.Service { return services } +func (d *SwarmDaemon) listSecrets(c *check.C) []swarm.Service { + status, out, err := d.SockRequest("GET", "/secrets", nil) + c.Assert(err, checker.IsNil, check.Commentf(string(out))) + c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out))) + + secrets := []swarm.Secret{} + c.Assert(json.Unmarshal(out, &secrets), checker.IsNil) + return services +} + func (d *SwarmDaemon) getSwarm(c *check.C) swarm.Swarm { var sw swarm.Swarm status, out, err := d.SockRequest("GET", "/swarm", nil) diff --git a/components/engine/integration-cli/docker_api_swarm_test.go b/components/engine/integration-cli/docker_api_swarm_test.go index 229c7a8e03..be6efeed4c 100644 --- a/components/engine/integration-cli/docker_api_swarm_test.go +++ b/components/engine/integration-cli/docker_api_swarm_test.go @@ -1263,3 +1263,27 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesUpdateWithName(c *check.C) { c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out))) waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, instances) } + +func (s *DockerSwarmSuite) TestAPISwarmSecretsEmptyList(c *check.C) { + d := s.AddDaemon(c, true, true) + + secrets := d.listSecrets(c) + c.Assert(secrets, checker.NotNil) + c.Assert(len(secrets), checker.Equals, 0, check.Commentf("secrets: %#v", secrets)) +} + +//func (s *DockerSwarmSuite) TestAPISwarmServicesCreate(c *check.C) { +// d := s.AddDaemon(c, true, true) +// +// instances := 2 +// id := d.createService(c, simpleTestService, setInstances(instances)) +// waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, instances) +// +// service := d.getService(c, id) +// instances = 5 +// d.updateService(c, service, setInstances(instances)) +// waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, instances) +// +// d.removeService(c, service.ID) +// waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 0) +//} From b7893465b093b017b15c09cc2f3c4541371a4c7a Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Wed, 26 Oct 2016 14:57:06 -0700 Subject: [PATCH 04/37] more review changes Signed-off-by: Evan Hazlett Upstream-commit: 669a9dbe7f260f2d9e18d80e8f604012122054f7 Component: engine --- .../engine/api/server/router/swarm/cluster_routes.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/engine/api/server/router/swarm/cluster_routes.go b/components/engine/api/server/router/swarm/cluster_routes.go index fc33d27746..b04d066191 100644 --- a/components/engine/api/server/router/swarm/cluster_routes.go +++ b/components/engine/api/server/router/swarm/cluster_routes.go @@ -7,6 +7,7 @@ import ( "strconv" "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/errors" "github.com/docker/docker/api/server/httputils" basictypes "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -319,18 +320,18 @@ func (sr *swarmRouter) getSecret(ctx context.Context, w http.ResponseWriter, r * func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { var secret types.SecretSpec if err := json.NewDecoder(r.Body).Decode(&secret); err != nil { - return err + return errors.NewBadRequestError(err) } rawVersion := r.URL.Query().Get("version") version, err := strconv.ParseUint(rawVersion, 10, 64) if err != nil { - return fmt.Errorf("Invalid secret version '%s': %s", rawVersion, err.Error()) + return errors.NewBadRequestError(fmt.Errorf("invalid secret version")) } id := vars["id"] if err := sr.backend.UpdateSecret(id, version, secret); err != nil { - return fmt.Errorf("Error updating secret: %s", err) + return errors.NewErrorWithStatusCode(err, http.StatusInternalServerError) } return nil From 848c015ac1ecec6a5c8434070f80e768050816ed Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 00:41:32 -0700 Subject: [PATCH 05/37] more review updates - use /secrets for swarm secret create route - do not specify omitempty for secret and secret reference - simplify lookup for secret ids - do not use pointer for secret grpc conversion Signed-off-by: Evan Hazlett Upstream-commit: 189f89301e0abfee32447f2ca23dacd3a96de06d Component: engine --- .../engine/api/server/router/swarm/cluster.go | 2 +- components/engine/api/types/swarm/secret.go | 16 ++++++++-------- components/engine/cli/command/service/opts.go | 15 +-------------- components/engine/cli/command/service/parse.go | 12 +++++------- components/engine/client/errors.go | 2 +- components/engine/client/secret_create.go | 2 +- components/engine/client/secret_create_test.go | 2 +- components/engine/container/container_unix.go | 8 +++----- .../engine/daemon/cluster/convert/secret.go | 2 +- .../cluster/executor/container/executor.go | 4 ---- components/engine/daemon/oci_linux.go | 5 ++++- .../engine/integration-cli/daemon_swarm.go | 4 ++-- .../integration-cli/docker_api_swarm_test.go | 16 ---------------- 13 files changed, 28 insertions(+), 62 deletions(-) diff --git a/components/engine/api/server/router/swarm/cluster.go b/components/engine/api/server/router/swarm/cluster.go index ec92e7aaa0..b6222c7498 100644 --- a/components/engine/api/server/router/swarm/cluster.go +++ b/components/engine/api/server/router/swarm/cluster.go @@ -41,7 +41,7 @@ func (sr *swarmRouter) initRoutes() { router.NewGetRoute("/tasks", sr.getTasks), router.NewGetRoute("/tasks/{id:.*}", sr.getTask), router.NewGetRoute("/secrets", sr.getSecrets), - router.NewPostRoute("/secrets/create", sr.createSecret), + router.NewPostRoute("/secrets", sr.createSecret), router.NewDeleteRoute("/secrets/{id:.*}", sr.removeSecret), router.NewGetRoute("/secrets/{id:.*}", sr.getSecret), router.NewPostRoute("/secrets/{id:.*}/update", sr.updateSecret), diff --git a/components/engine/api/types/swarm/secret.go b/components/engine/api/types/swarm/secret.go index 0700177588..86a6beafeb 100644 --- a/components/engine/api/types/swarm/secret.go +++ b/components/engine/api/types/swarm/secret.go @@ -4,14 +4,14 @@ package swarm type Secret struct { ID string Meta - Spec *SecretSpec `json:",omitempty"` - Digest string `json:",omitempty"` - SecretSize int64 `json:",omitempty"` + Spec SecretSpec + Digest string + SecretSize int64 } type SecretSpec struct { Annotations - Data []byte `json:",omitempty"` + Data []byte } type SecretReferenceMode int @@ -23,8 +23,8 @@ const ( ) type SecretReference struct { - SecretID string `json:",omitempty"` - Mode SecretReferenceMode `json:",omitempty"` - Target string `json:",omitempty"` - SecretName string `json:",omitempty"` + SecretID string + Mode SecretReferenceMode + Target string + SecretName string } diff --git a/components/engine/cli/command/service/opts.go b/components/engine/cli/command/service/opts.go index a4fd08881c..3ef24ee33c 100644 --- a/components/engine/cli/command/service/opts.go +++ b/components/engine/cli/command/service/opts.go @@ -191,19 +191,6 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { return nets } -func convertSecrets(secrets []string) []*swarm.SecretReference { - sec := []*swarm.SecretReference{} - for _, s := range secrets { - sec = append(sec, &swarm.SecretReference{ - SecretID: s, - Mode: swarm.SecretReferenceFile, - Target: "", - }) - } - - return sec -} - type endpointOptions struct { mode string ports opts.ListOpts @@ -417,7 +404,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Options: opts.dnsOptions.GetAll(), }, StopGracePeriod: opts.stopGrace.Value(), - Secrets: convertSecrets(opts.secrets), + Secrets: nil, }, Networks: convertNetworks(opts.networks.GetAll()), Resources: opts.resources.ToResourceRequirements(), diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go index 596d8e50d8..f3061660a2 100644 --- a/components/engine/cli/command/service/parse.go +++ b/components/engine/cli/command/service/parse.go @@ -18,7 +18,7 @@ func parseSecretString(secretString string) (string, string, error) { tokens := strings.Split(secretString, ":") secretName := strings.TrimSpace(tokens[0]) - targetName := "" + targetName := secretName if secretName == "" { return "", "", fmt.Errorf("invalid secret name provided") @@ -29,8 +29,6 @@ func parseSecretString(secretString string) (string, string, error) { if targetName == "" { return "", "", fmt.Errorf("invalid presentation name provided") } - } else { - targetName = secretName } // ensure target is a filename only; no paths allowed @@ -77,22 +75,22 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt return nil, err } - foundSecrets := make(map[string]*swarmtypes.Secret) + foundSecrets := make(map[string]string) for _, secret := range secrets { - foundSecrets[secret.Spec.Annotations.Name] = &secret + foundSecrets[secret.Spec.Annotations.Name] = secret.ID } addedSecrets := []*swarmtypes.SecretReference{} for secretName, secretRef := range needSecrets { - s, ok := foundSecrets[secretName] + id, ok := foundSecrets[secretName] if !ok { return nil, fmt.Errorf("secret not found: %s", secretName) } // set the id for the ref to properly assign in swarm // since swarm needs the ID instead of the name - secretRef.SecretID = s.ID + secretRef.SecretID = id addedSecrets = append(addedSecrets, secretRef) } diff --git a/components/engine/client/errors.go b/components/engine/client/errors.go index db7294daa8..94c22a728a 100644 --- a/components/engine/client/errors.go +++ b/components/engine/client/errors.go @@ -225,7 +225,7 @@ type secretNotFoundError struct { // Error returns a string representation of a secretNotFoundError func (e secretNotFoundError) Error() string { - return fmt.Sprintf("Error: No such secret: %s", e.name) + return fmt.Sprintf("Error: no such secret: %s", e.name) } // NoFound indicates that this error type is of NotFound diff --git a/components/engine/client/secret_create.go b/components/engine/client/secret_create.go index de8b041567..f92a3d1510 100644 --- a/components/engine/client/secret_create.go +++ b/components/engine/client/secret_create.go @@ -13,7 +13,7 @@ func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (t var headers map[string][]string var response types.SecretCreateResponse - resp, err := cli.post(ctx, "/secrets/create", nil, secret, headers) + resp, err := cli.post(ctx, "/secrets", nil, secret, headers) if err != nil { return response, err } diff --git a/components/engine/client/secret_create_test.go b/components/engine/client/secret_create_test.go index d264eb6692..b7def89d0e 100644 --- a/components/engine/client/secret_create_test.go +++ b/components/engine/client/secret_create_test.go @@ -25,7 +25,7 @@ func TestSecretCreateError(t *testing.T) { } func TestSecretCreate(t *testing.T) { - expectedURL := "/secrets/create" + expectedURL := "/secrets" client := &Client{ client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { diff --git a/components/engine/container/container_unix.go b/components/engine/container/container_unix.go index 6fc6c53c27..3cd0d4c17a 100644 --- a/components/engine/container/container_unix.go +++ b/components/engine/container/container_unix.go @@ -269,18 +269,16 @@ func (container *Container) IpcMounts() []Mount { } // SecretMount returns the list of Secret mounts -func (container *Container) SecretMount() Mount { - var mount Mount - +func (container *Container) SecretMount() *Mount { if len(container.Secrets) > 0 { - mount = Mount{ + return &Mount{ Source: container.SecretMountPath(), Destination: containerSecretMountPath, Writable: false, } } - return mount + return nil } // UnmountSecrets unmounts the local tmpfs for secrets diff --git a/components/engine/daemon/cluster/convert/secret.go b/components/engine/daemon/cluster/convert/secret.go index fc5b43594f..acfc863efa 100644 --- a/components/engine/daemon/cluster/convert/secret.go +++ b/components/engine/daemon/cluster/convert/secret.go @@ -19,7 +19,7 @@ func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret { secret.CreatedAt, _ = ptypes.Timestamp(s.Meta.CreatedAt) secret.UpdatedAt, _ = ptypes.Timestamp(s.Meta.UpdatedAt) - secret.Spec = &swarmtypes.SecretSpec{ + secret.Spec = swarmtypes.SecretSpec{ Annotations: swarmtypes.Annotations{ Name: s.Spec.Annotations.Name, Labels: s.Spec.Annotations.Labels, diff --git a/components/engine/daemon/cluster/executor/container/executor.go b/components/engine/daemon/cluster/executor/container/executor.go index fdd270006d..029a4b9b9c 100644 --- a/components/engine/daemon/cluster/executor/container/executor.go +++ b/components/engine/daemon/cluster/executor/container/executor.go @@ -18,10 +18,6 @@ type executor struct { backend executorpkg.Backend } -type secretProvider interface { - Get(secretID string) *api.Secret -} - // NewExecutor returns an executor from the docker client. func NewExecutor(b executorpkg.Backend) exec.Executor { return &executor{ diff --git a/components/engine/daemon/oci_linux.go b/components/engine/daemon/oci_linux.go index 6889a737c5..3ce21dfb93 100644 --- a/components/engine/daemon/oci_linux.go +++ b/components/engine/daemon/oci_linux.go @@ -710,6 +710,7 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { if err != nil { return nil, err } + ms = append(ms, c.IpcMounts()...) tmpfsMounts, err := c.TmpfsMounts() @@ -718,7 +719,9 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { } ms = append(ms, tmpfsMounts...) - ms = append(ms, c.SecretMount()) + if m := c.SecretMount(); m != nil { + ms = append(ms, *m) + } sort.Sort(mounts(ms)) if err := setMounts(daemon, &s, c, ms); err != nil { diff --git a/components/engine/integration-cli/daemon_swarm.go b/components/engine/integration-cli/daemon_swarm.go index 222fa018e9..ac933bb26f 100644 --- a/components/engine/integration-cli/daemon_swarm.go +++ b/components/engine/integration-cli/daemon_swarm.go @@ -284,14 +284,14 @@ func (d *SwarmDaemon) listServices(c *check.C) []swarm.Service { return services } -func (d *SwarmDaemon) listSecrets(c *check.C) []swarm.Service { +func (d *SwarmDaemon) listSecrets(c *check.C) []swarm.Secret { status, out, err := d.SockRequest("GET", "/secrets", nil) c.Assert(err, checker.IsNil, check.Commentf(string(out))) c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out))) secrets := []swarm.Secret{} c.Assert(json.Unmarshal(out, &secrets), checker.IsNil) - return services + return secrets } func (d *SwarmDaemon) getSwarm(c *check.C) swarm.Swarm { diff --git a/components/engine/integration-cli/docker_api_swarm_test.go b/components/engine/integration-cli/docker_api_swarm_test.go index be6efeed4c..8fc56a1271 100644 --- a/components/engine/integration-cli/docker_api_swarm_test.go +++ b/components/engine/integration-cli/docker_api_swarm_test.go @@ -1271,19 +1271,3 @@ func (s *DockerSwarmSuite) TestAPISwarmSecretsEmptyList(c *check.C) { c.Assert(secrets, checker.NotNil) c.Assert(len(secrets), checker.Equals, 0, check.Commentf("secrets: %#v", secrets)) } - -//func (s *DockerSwarmSuite) TestAPISwarmServicesCreate(c *check.C) { -// d := s.AddDaemon(c, true, true) -// -// instances := 2 -// id := d.createService(c, simpleTestService, setInstances(instances)) -// waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, instances) -// -// service := d.getService(c, id) -// instances = 5 -// d.updateService(c, service, setInstances(instances)) -// waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, instances) -// -// d.removeService(c, service.ID) -// waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 0) -//} From 45bc21e90e07f02cd33c22afd510c2868f04a707 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 10:34:58 -0700 Subject: [PATCH 06/37] update to support latest swarm changes Signed-off-by: Evan Hazlett Upstream-commit: f50a65ff0c28618f798d06838e36b015dd022b45 Component: engine --- .../daemon/cluster/executor/container/adapter.go | 4 ++-- .../daemon/cluster/executor/container/attachment.go | 2 +- .../daemon/cluster/executor/container/controller.go | 2 +- .../daemon/cluster/executor/container/executor.go | 13 ++++++++++--- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/components/engine/daemon/cluster/executor/container/adapter.go b/components/engine/daemon/cluster/executor/container/adapter.go index 40a08401f4..1cdb6429f2 100644 --- a/components/engine/daemon/cluster/executor/container/adapter.go +++ b/components/engine/daemon/cluster/executor/container/adapter.go @@ -30,10 +30,10 @@ import ( type containerAdapter struct { backend executorpkg.Backend container *containerConfig - secrets exec.SecretProvider + secrets exec.SecretGetter } -func newContainerAdapter(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*containerAdapter, error) { +func newContainerAdapter(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*containerAdapter, error) { ctnr, err := newContainerConfig(task) if err != nil { return nil, err diff --git a/components/engine/daemon/cluster/executor/container/attachment.go b/components/engine/daemon/cluster/executor/container/attachment.go index 4dce78cd46..e0ee81a8b9 100644 --- a/components/engine/daemon/cluster/executor/container/attachment.go +++ b/components/engine/daemon/cluster/executor/container/attachment.go @@ -20,7 +20,7 @@ type networkAttacherController struct { closed chan struct{} } -func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*networkAttacherController, error) { +func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*networkAttacherController, error) { adapter, err := newContainerAdapter(b, task, secrets) if err != nil { return nil, err diff --git a/components/engine/daemon/cluster/executor/container/controller.go b/components/engine/daemon/cluster/executor/container/controller.go index d3d80075a4..47fc2bf7a5 100644 --- a/components/engine/daemon/cluster/executor/container/controller.go +++ b/components/engine/daemon/cluster/executor/container/controller.go @@ -33,7 +33,7 @@ type controller struct { var _ exec.Controller = &controller{} // NewController returns a docker exec runner for the provided task. -func newController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*controller, error) { +func newController(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*controller, error) { adapter, err := newContainerAdapter(b, task, secrets) if err != nil { return nil, err diff --git a/components/engine/daemon/cluster/executor/container/executor.go b/components/engine/daemon/cluster/executor/container/executor.go index 029a4b9b9c..b40d9c2954 100644 --- a/components/engine/daemon/cluster/executor/container/executor.go +++ b/components/engine/daemon/cluster/executor/container/executor.go @@ -10,18 +10,21 @@ import ( clustertypes "github.com/docker/docker/daemon/cluster/provider" networktypes "github.com/docker/libnetwork/types" "github.com/docker/swarmkit/agent/exec" + "github.com/docker/swarmkit/agent/secrets" "github.com/docker/swarmkit/api" "golang.org/x/net/context" ) type executor struct { backend executorpkg.Backend + secrets exec.SecretsManager } // NewExecutor returns an executor from the docker client. func NewExecutor(b executorpkg.Backend) exec.Executor { return &executor{ backend: b, + secrets: secrets.NewManager(), } } @@ -120,12 +123,12 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error { } // Controller returns a docker container runner. -func (e *executor) Controller(t *api.Task, secrets exec.SecretProvider) (exec.Controller, error) { +func (e *executor) Controller(t *api.Task) (exec.Controller, error) { if t.Spec.GetAttachment() != nil { - return newNetworkAttacherController(e.backend, t, secrets) + return newNetworkAttacherController(e.backend, t, e.secrets) } - ctlr, err := newController(e.backend, t, secrets) + ctlr, err := newController(e.backend, t, e.secrets) if err != nil { return nil, err } @@ -150,6 +153,10 @@ func (e *executor) SetNetworkBootstrapKeys(keys []*api.EncryptionKey) error { return nil } +func (e *executor) Secrets() exec.SecretsManager { + return e.secrets +} + type sortedPlugins []api.PluginDescription func (sp sortedPlugins) Len() int { return len(sp) } From e2eea356c22e20f492a9f8006fa8ff0ee051fca7 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 11:29:51 -0700 Subject: [PATCH 07/37] proper cleanup upon mount fail Signed-off-by: Evan Hazlett Upstream-commit: 0c170a76c51be67bd6f7c4d38ed28ddcb473eac9 Component: engine --- .../daemon/container_operations_unix.go | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/components/engine/daemon/container_operations_unix.go b/components/engine/daemon/container_operations_unix.go index e208962030..abbe180eb8 100644 --- a/components/engine/daemon/container_operations_unix.go +++ b/components/engine/daemon/container_operations_unix.go @@ -144,12 +144,10 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error { return nil } -func (daemon *Daemon) setupSecretDir(c *container.Container) error { +func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { localMountPath := c.SecretMountPath() logrus.Debugf("secrets: setting up secret dir: %s", localMountPath) - var setupErr error - defer func(err error) { if err != nil { // cleanup @@ -163,22 +161,22 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) error { // create tmpfs if err := os.MkdirAll(localMountPath, 0700); err != nil { - setupErr = errors.Wrap(err, "error creating secret local mount path") + return errors.Wrap(err, "error creating secret local mount path") } if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev"); err != nil { - setupErr = errors.Wrap(err, "unable to setup secret mount") + return errors.Wrap(err, "unable to setup secret mount") } for _, s := range c.Secrets { // ensure that the target is a filename only; no paths allowed tDir, tPath := filepath.Split(s.Target) if tDir != "" { - setupErr = fmt.Errorf("error creating secret: secret must not have a path") + return fmt.Errorf("error creating secret: secret must not have a path") } fPath := filepath.Join(localMountPath, tPath) if err := os.MkdirAll(filepath.Dir(fPath), 0700); err != nil { - setupErr = errors.Wrap(err, "error creating secret mount path") + return errors.Wrap(err, "error creating secret mount path") } logrus.WithFields(logrus.Fields{ @@ -186,20 +184,20 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) error { "path": fPath, }).Debug("injecting secret") if err := ioutil.WriteFile(fPath, s.Data, s.Mode); err != nil { - setupErr = errors.Wrap(err, "error injecting secret") + return errors.Wrap(err, "error injecting secret") } if err := os.Chown(fPath, s.Uid, s.Gid); err != nil { - setupErr = errors.Wrap(err, "error setting ownership for secret") + return errors.Wrap(err, "error setting ownership for secret") } } // remount secrets ro if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro"); err != nil { - setupErr = errors.Wrap(err, "unable to remount secret dir as readonly") + return errors.Wrap(err, "unable to remount secret dir as readonly") } - return setupErr + return nil } func killProcessDirectly(container *container.Container) error { From f9430dc4f0e9ecf3485924a2e502a6686dd70ac1 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 13:51:13 -0700 Subject: [PATCH 08/37] add SecretMount and UnmountSecrets methods for non-Linux Signed-off-by: Evan Hazlett Upstream-commit: 72c1d7f46bbc5e525f9339f6a1015c6a38b2616e Component: engine --- components/engine/container/container_notlinux.go | 10 ++++++++++ components/engine/container/container_windows.go | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/components/engine/container/container_notlinux.go b/components/engine/container/container_notlinux.go index c9774fd5b9..f65653e992 100644 --- a/components/engine/container/container_notlinux.go +++ b/components/engine/container/container_notlinux.go @@ -11,3 +11,13 @@ func detachMounted(path string) error { // Therefore there are separate definitions for this. return unix.Unmount(path, 0) } + +// SecretMount returns the mount for the secret path +func (container *Container) SecretMount() *Mount { + return nil +} + +// UnmountSecrets unmounts the fs for secrets +func (container *Container) UnmountSecrets() error { + return nil +} diff --git a/components/engine/container/container_windows.go b/components/engine/container/container_windows.go index 69d53452df..4dbd02b4a7 100644 --- a/components/engine/container/container_windows.go +++ b/components/engine/container/container_windows.go @@ -44,6 +44,16 @@ func (container *Container) IpcMounts() []Mount { return nil } +// SecretMount returns the mount for the secret path +func (container *Container) SecretMount() *Mount { + return nil +} + +// UnmountSecrets unmounts the fs for secrets +func (container *Container) UnmountSecrets() error { + return nil +} + // UnmountVolumes explicitly unmounts volumes from the container. func (container *Container) UnmountVolumes(forceSyscall bool, volumeEventLog func(name, action string, attributes map[string]string)) error { var ( From a28c9109167084526254595682b4f83017d137c6 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 15:51:02 -0700 Subject: [PATCH 09/37] add secret support for service update - add nosuid and noexec to tmpfs Signed-off-by: Evan Hazlett Upstream-commit: 00237a96249739dbf426d81672a904e9f4c5e702 Component: engine --- components/engine/cli/command/service/opts.go | 2 ++ .../engine/cli/command/service/update.go | 34 +++++++++++++++++++ components/engine/container/container_unix.go | 2 +- .../daemon/container_operations_unix.go | 8 ++--- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/components/engine/cli/command/service/opts.go b/components/engine/cli/command/service/opts.go index 3ef24ee33c..37da5d1145 100644 --- a/components/engine/cli/command/service/opts.go +++ b/components/engine/cli/command/service/opts.go @@ -557,4 +557,6 @@ const ( flagHealthTimeout = "health-timeout" flagNoHealthcheck = "no-healthcheck" flagSecret = "secret" + flagSecretAdd = "secret-add" + flagSecretRemove = "secret-rm" ) diff --git a/components/engine/cli/command/service/update.go b/components/engine/cli/command/service/update.go index 4a77229497..a9f5ac9be6 100644 --- a/components/engine/cli/command/service/update.go +++ b/components/engine/cli/command/service/update.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/client" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" @@ -54,6 +55,8 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label") flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") + flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret") + flags.StringSliceVar(&opts.secrets, flagSecretAdd, []string{}, "Add a secret") flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint") flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") @@ -97,6 +100,13 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str return err } + updatedSecrets, err := getUpdatedSecrets(apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets) + if err != nil { + return err + } + + spec.TaskTemplate.ContainerSpec.Secrets = updatedSecrets + // only send auth if flag was set sendAuth, err := flags.GetBool(flagRegistryAuth) if err != nil { @@ -401,6 +411,30 @@ func updateEnvironment(flags *pflag.FlagSet, field *[]string) { *field = removeItems(*field, toRemove, envKey) } +func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) { + if flags.Changed(flagSecretAdd) { + values, err := flags.GetStringSlice(flagSecretAdd) + if err != nil { + return nil, err + } + + addSecrets, err := parseSecrets(apiClient, values) + if err != nil { + return nil, err + } + secrets = append(secrets, addSecrets...) + } + toRemove := buildToRemoveSet(flags, flagSecretRemove) + newSecrets := []*swarm.SecretReference{} + for _, secret := range secrets { + if _, exists := toRemove[secret.SecretName]; !exists { + newSecrets = append(newSecrets, secret) + } + } + + return newSecrets, nil +} + func envKey(value string) string { kv := strings.SplitN(value, "=", 2) return kv[0] diff --git a/components/engine/container/container_unix.go b/components/engine/container/container_unix.go index 3cd0d4c17a..8d825c7698 100644 --- a/components/engine/container/container_unix.go +++ b/components/engine/container/container_unix.go @@ -268,7 +268,7 @@ func (container *Container) IpcMounts() []Mount { return mounts } -// SecretMount returns the list of Secret mounts +// SecretMount returns the mount for the secret path func (container *Container) SecretMount() *Mount { if len(container.Secrets) > 0 { return &Mount{ diff --git a/components/engine/daemon/container_operations_unix.go b/components/engine/daemon/container_operations_unix.go index abbe180eb8..bdadaa7d7f 100644 --- a/components/engine/daemon/container_operations_unix.go +++ b/components/engine/daemon/container_operations_unix.go @@ -148,8 +148,8 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { localMountPath := c.SecretMountPath() logrus.Debugf("secrets: setting up secret dir: %s", localMountPath) - defer func(err error) { - if err != nil { + defer func() { + if setupErr != nil { // cleanup _ = detachMounted(localMountPath) @@ -157,13 +157,13 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { log.Errorf("error cleaning up secret mount: %s", err) } } - }(setupErr) + }() // create tmpfs if err := os.MkdirAll(localMountPath, 0700); err != nil { return errors.Wrap(err, "error creating secret local mount path") } - if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev"); err != nil { + if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev,nosuid,noexec"); err != nil { return errors.Wrap(err, "unable to setup secret mount") } From 994d3dafef62234ba70c52969d84387f30b906fb Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 17:18:12 -0700 Subject: [PATCH 10/37] support the same secret with different targets on service create Signed-off-by: Evan Hazlett Upstream-commit: 111e497dc6be6a1211e446d6a91bfb6e1c14e4c2 Component: engine --- components/engine/cli/command/service/parse.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go index f3061660a2..1a8e56b8c4 100644 --- a/components/engine/cli/command/service/parse.go +++ b/components/engine/cli/command/service/parse.go @@ -44,9 +44,10 @@ func parseSecretString(secretString string) (string, string, error) { // them to secret references to use with the spec func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) { lookupSecretNames := []string{} - needSecrets := make(map[string]*swarmtypes.SecretReference) + neededSecrets := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() + neededLookup := map[string]string{} for _, secret := range requestedSecrets { n, t, err := parseSecretString(secret) if err != nil { @@ -60,7 +61,8 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt } lookupSecretNames = append(lookupSecretNames, n) - needSecrets[n] = secretRef + neededLookup[t] = n + neededSecrets[t] = secretRef } args := filters.NewArgs() @@ -82,12 +84,17 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt addedSecrets := []*swarmtypes.SecretReference{} - for secretName, secretRef := range needSecrets { + for target, secretName := range neededLookup { id, ok := foundSecrets[secretName] if !ok { return nil, fmt.Errorf("secret not found: %s", secretName) } + secretRef, ok := neededSecrets[target] + if !ok { + return nil, fmt.Errorf("secret reference not found: %s", secretName) + } + // set the id for the ref to properly assign in swarm // since swarm needs the ID instead of the name secretRef.SecretID = id From a0ec36114c3ef2d71cb683ced559685cf1010b92 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 17:30:32 -0700 Subject: [PATCH 11/37] docs: update cli reference for secret usage Signed-off-by: Evan Hazlett docs: minor update to service create usage Signed-off-by: Evan Hazlett Upstream-commit: 958881d209a543565e139354a721d45b33863611 Component: engine --- .../reference/commandline/secret_create.md | 46 ++++++++++ .../reference/commandline/secret_inspect.md | 88 +++++++++++++++++++ .../docs/reference/commandline/secret_ls.md | 44 ++++++++++ .../docs/reference/commandline/secret_rm.md | 48 ++++++++++ .../reference/commandline/service_create.md | 16 ++++ .../reference/commandline/service_update.md | 16 ++++ 6 files changed, 258 insertions(+) create mode 100644 components/engine/docs/reference/commandline/secret_create.md create mode 100644 components/engine/docs/reference/commandline/secret_inspect.md create mode 100644 components/engine/docs/reference/commandline/secret_ls.md create mode 100644 components/engine/docs/reference/commandline/secret_rm.md diff --git a/components/engine/docs/reference/commandline/secret_create.md b/components/engine/docs/reference/commandline/secret_create.md new file mode 100644 index 0000000000..92cee6cde7 --- /dev/null +++ b/components/engine/docs/reference/commandline/secret_create.md @@ -0,0 +1,46 @@ +--- +title: "secret create" +description: "The secret create command description and usage" +keywords: ["secret, create"] +--- + + + +# secret create + +```Markdown +Usage: docker secret create [NAME] + +Create a secret using stdin as content +``` + +Creates a secret using standard input for the secret content. You must run this +command on a manager node. + +## Examples + +### Create a secret + +```bash +$ cat ssh-dev | docker secret create ssh-dev +mhv17xfe3gh6xc4rij5orpfds + +$ docker secret ls +ID NAME CREATED UPDATED SIZE +mhv17xfe3gh6xc4rij5orpfds ssh-dev 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679 +``` + +## Related information + +* [secret inspect](secret_inspect.md) +* [secret ls](secret_ls.md) +* [secret rm](secret_rm.md) + + diff --git a/components/engine/docs/reference/commandline/secret_inspect.md b/components/engine/docs/reference/commandline/secret_inspect.md new file mode 100644 index 0000000000..1572a87b16 --- /dev/null +++ b/components/engine/docs/reference/commandline/secret_inspect.md @@ -0,0 +1,88 @@ +--- +title: "secret inspect" +description: "The secret inspect command description and usage" +keywords: ["secret, inspect"] +--- + + + +# secret inspect + +```Markdown +Usage: docker secret inspect [OPTIONS] SECRET [SECRET...] + +Display detailed information on one or more secrets + +Options: + -f, --format string Format the output using the given Go template + --help Print usage +``` + + +Inspects the specified secret. This command has to be run targeting a manager +node. + +By default, this renders all results in a JSON array. If a format is specified, +the given template will be executed for each result. + +Go's [text/template](http://golang.org/pkg/text/template/) package +describes all the details of the format. + +## Examples + +### Inspecting a secret by name or ID + +You can inspect a secret, either by its *name*, or *ID* + +For example, given the following secret: + +```bash +$ docker secret ls +ID NAME CREATED UPDATED SIZE +mhv17xfe3gh6xc4rij5orpfds ssh-dev 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679 +``` + +```bash +$ docker secret inspect mhv17xfe3gh6xc4rij5orpfds +[ + { + "ID": "mhv17xfe3gh6xc4rij5orpfds", + "Version": { + "Index": 1198 + }, + "CreatedAt": "2016-10-27T23:25:43.909181089Z", + "UpdatedAt": "2016-10-27T23:25:43.909181089Z", + "Spec": { + "Name": "ssh-dev", + "Data": null + }, + "Digest": "sha256:8281c6d924520986e3c6af23ed8926710a611c90339db582c2a9ac480ba622b7", + "SecretSize": 1679 + } +] +``` + +### Formatting secret output + +The `--format` option can be used to obtain specific information about a +secret. For example, the following command outputs the digest of the +secret. + +```bash{% raw %} +$ docker secret inspect --format='{{.Digest}}' mhv17xfe3gh6xc4rij5orpfds +sha256:8281c6d924520986e3c6af23ed8926710a611c90339db582c2a9ac480ba622b7 +{% endraw %}``` + + +## Related information + +* [secret create](secret_create.md) +* [secret ls](secret_ls.md) +* [secret rm](secret_rm.md) diff --git a/components/engine/docs/reference/commandline/secret_ls.md b/components/engine/docs/reference/commandline/secret_ls.md new file mode 100644 index 0000000000..13a8e60454 --- /dev/null +++ b/components/engine/docs/reference/commandline/secret_ls.md @@ -0,0 +1,44 @@ +--- +title: "secret ls" +description: "The secret ls command description and usage" +keywords: ["secret, ls"] +--- + + + +# secret ls + +```Markdown +Usage: docker secret ls [OPTIONS] + +List secrets + +Aliases: + ls, list + +Options: + -q, --quiet Only display IDs +``` + +This command when run targeting a manager, lists secrets in the +swarm. + +On a manager node: + +```bash +$ docker secret ls +ID NAME CREATED UPDATED SIZE +mhv17xfe3gh6xc4rij5orpfds ssh-dev 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679 +``` +## Related information + +* [secret create](secret_create.md) +* [secret inspect](secret_inspect.md) +* [secret rm](secret_rm.md) diff --git a/components/engine/docs/reference/commandline/secret_rm.md b/components/engine/docs/reference/commandline/secret_rm.md new file mode 100644 index 0000000000..5f9ba5f69b --- /dev/null +++ b/components/engine/docs/reference/commandline/secret_rm.md @@ -0,0 +1,48 @@ +--- +title: "secret rm" +description: "The secret rm command description and usage" +keywords: ["secret, rm"] +--- + + + +# secret rm + +```Markdown +Usage: docker secret rm SECRET [SECRET...] + +Remove one or more secrets + +Aliases: + rm, remove + +Options: + --help Print usage +``` + +Removes the specified secrets from the swarm. This command has to be run +targeting a manager node. + +For example, to remove secret: + +```bash +$ docker secret rm sapth4csdo5b6wz2p5uimh5xg +sapth4csdo5b6wz2p5uimh5xg +``` + +> **Warning**: Unlike `docker rm`, this command does not ask for confirmation +> before removing a secret. + + +## Related information + +* [secret create](secret_create.md) +* [secret inspect](secret_inspect.md) +* [secret ls](secret_ls.md) diff --git a/components/engine/docs/reference/commandline/service_create.md b/components/engine/docs/reference/commandline/service_create.md index d659a60d19..9d98600043 100644 --- a/components/engine/docs/reference/commandline/service_create.md +++ b/components/engine/docs/reference/commandline/service_create.md @@ -54,6 +54,7 @@ Options: --restart-delay duration Delay between restart attempts (default none) --restart-max-attempts uint Maximum number of restarts before giving up (default none) --restart-window duration Window used to evaluate the restart policy (default none) + --secret value Specify secrets to expose to the service (default []) --stop-grace-period duration Time to wait before force killing a container (default none) -t, --tty Allocate a pseudo-TTY --update-delay duration Delay between updates (ns|us|ms|s|m|h) (default 0s) @@ -119,6 +120,21 @@ ID NAME MODE REPLICAS IMAGE 4cdgfyky7ozw redis replicated 5/5 redis:3.0.7 ``` +### Create a service with secrets +Use the `--secret` flag to use a [secret](secret_create.md). The following +command will create a service with two secrets named `ssh-key` and `app-key`: + +```bash +$ docker service create --name redis --secret ssh-key:ssh --secret app-key:app redis:3.0.6 +4cdgfyky7ozwh3htjfw0d12qv +``` + +Secrets are located in `/run/secrets` in the container. If no target is +specified, the name of the secret will be used as the in memory file in the +container. If a target is specified, that will be the filename. In the +example above, two files will be created: `/run/secrets/ssh` and +`/run/secrets/app` for each of the secret targets specified. + ### Create a service with a rolling update policy ```bash diff --git a/components/engine/docs/reference/commandline/service_update.md b/components/engine/docs/reference/commandline/service_update.md index f321b31ddc..2cc67996e6 100644 --- a/components/engine/docs/reference/commandline/service_update.md +++ b/components/engine/docs/reference/commandline/service_update.md @@ -63,6 +63,8 @@ Options: --restart-max-attempts uint Maximum number of restarts before giving up (default none) --restart-window duration Window used to evaluate the restart policy (default none) --rollback Rollback to previous specification + --secret-add list Add a secret (default []) + --secret-rm list Remove a secret (default []) --stop-grace-period duration Time to wait before force killing a container (default none) -t, --tty Allocate a pseudo-TTY --update-delay duration Delay between updates (ns|us|ms|s|m|h) (default 0s) @@ -146,6 +148,20 @@ $ docker service update --mount-rm /somewhere myservice myservice ``` +### Adding and removing secrets + +Use the `--secret-add` or `--secret-rm` options add or remove a service's +secrets. + +The following example adds a secret named `ssh-2` and removes `ssh-1`: + +```bash +$ docker service update \ + --secret-add ssh-2 \ + --secret-rm ssh-1 \ + myservice +``` + ## Related information * [service create](service_create.md) From f3ba87649bb40e8da81847a2c0d556b726bf9e7a Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 17:57:38 -0700 Subject: [PATCH 12/37] simplify secret lookup on service create Signed-off-by: Evan Hazlett Upstream-commit: dce2afbd81945056aa955079fac04e28ab96e703 Component: engine --- .../engine/cli/command/service/parse.go | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go index 1a8e56b8c4..71d6fb1958 100644 --- a/components/engine/cli/command/service/parse.go +++ b/components/engine/cli/command/service/parse.go @@ -43,11 +43,9 @@ func parseSecretString(secretString string) (string, string, error) { // parseSecrets retrieves the secrets from the requested names and converts // them to secret references to use with the spec func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) { - lookupSecretNames := []string{} - neededSecrets := make(map[string]*swarmtypes.SecretReference) + secretRefs := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() - neededLookup := map[string]string{} for _, secret := range requestedSecrets { n, t, err := parseSecretString(secret) if err != nil { @@ -60,14 +58,15 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt Target: t, } - lookupSecretNames = append(lookupSecretNames, n) - neededLookup[t] = n - neededSecrets[t] = secretRef + if _, exists := secretRefs[t]; exists { + return nil, fmt.Errorf("duplicate secret target for %s not allowed", n) + } + secretRefs[t] = secretRef } args := filters.NewArgs() - for _, s := range lookupSecretNames { - args.Add("names", s) + for _, s := range secretRefs { + args.Add("names", s.SecretName) } secrets, err := client.SecretList(ctx, types.SecretListOptions{ @@ -84,21 +83,16 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt addedSecrets := []*swarmtypes.SecretReference{} - for target, secretName := range neededLookup { - id, ok := foundSecrets[secretName] + for _, ref := range secretRefs { + id, ok := foundSecrets[ref.SecretName] if !ok { - return nil, fmt.Errorf("secret not found: %s", secretName) - } - - secretRef, ok := neededSecrets[target] - if !ok { - return nil, fmt.Errorf("secret reference not found: %s", secretName) + return nil, fmt.Errorf("secret not found: %s", ref.SecretName) } // set the id for the ref to properly assign in swarm // since swarm needs the ID instead of the name - secretRef.SecretID = id - addedSecrets = append(addedSecrets, secretRef) + ref.SecretID = id + addedSecrets = append(addedSecrets, ref) } return addedSecrets, nil From 854a5e1eb781cd0f4949e1903b1db6ad50ef0345 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 19:49:23 -0700 Subject: [PATCH 13/37] secrets: add secret create and delete integration tests Signed-off-by: Evan Hazlett Upstream-commit: e63dc5cde4c0dd52c3a54bb007259a4b8878b7df Component: engine --- .../engine/integration-cli/daemon_swarm.go | 26 +++++++++++++ .../integration-cli/docker_api_swarm_test.go | 39 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/components/engine/integration-cli/daemon_swarm.go b/components/engine/integration-cli/daemon_swarm.go index ac933bb26f..9a8de41d6c 100644 --- a/components/engine/integration-cli/daemon_swarm.go +++ b/components/engine/integration-cli/daemon_swarm.go @@ -284,6 +284,17 @@ func (d *SwarmDaemon) listServices(c *check.C) []swarm.Service { return services } +func (d *SwarmDaemon) createSecret(c *check.C, secretSpec swarm.SecretSpec) string { + status, out, err := d.SockRequest("POST", "/secrets", secretSpec) + + c.Assert(err, checker.IsNil, check.Commentf(string(out))) + c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf("output: %q", string(out))) + + var scr types.SecretCreateResponse + c.Assert(json.Unmarshal(out, &scr), checker.IsNil) + return scr.ID +} + func (d *SwarmDaemon) listSecrets(c *check.C) []swarm.Secret { status, out, err := d.SockRequest("GET", "/secrets", nil) c.Assert(err, checker.IsNil, check.Commentf(string(out))) @@ -294,6 +305,21 @@ func (d *SwarmDaemon) listSecrets(c *check.C) []swarm.Secret { return secrets } +func (d *SwarmDaemon) getSecret(c *check.C, id string) *swarm.Secret { + var secret swarm.Secret + status, out, err := d.SockRequest("GET", "/secrets/"+id, nil) + c.Assert(err, checker.IsNil, check.Commentf(string(out))) + c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out))) + c.Assert(json.Unmarshal(out, &secret), checker.IsNil) + return &secret +} + +func (d *SwarmDaemon) deleteSecret(c *check.C, id string) { + status, out, err := d.SockRequest("DELETE", "/secrets/"+id, nil) + c.Assert(err, checker.IsNil, check.Commentf(string(out))) + c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out))) +} + func (d *SwarmDaemon) getSwarm(c *check.C) swarm.Swarm { var sw swarm.Swarm status, out, err := d.SockRequest("GET", "/swarm", nil) diff --git a/components/engine/integration-cli/docker_api_swarm_test.go b/components/engine/integration-cli/docker_api_swarm_test.go index 8fc56a1271..39bf721211 100644 --- a/components/engine/integration-cli/docker_api_swarm_test.go +++ b/components/engine/integration-cli/docker_api_swarm_test.go @@ -1271,3 +1271,42 @@ func (s *DockerSwarmSuite) TestAPISwarmSecretsEmptyList(c *check.C) { c.Assert(secrets, checker.NotNil) c.Assert(len(secrets), checker.Equals, 0, check.Commentf("secrets: %#v", secrets)) } + +func (s *DockerSwarmSuite) TestAPISwarmSecretsCreate(c *check.C) { + d := s.AddDaemon(c, true, true) + + testName := "test_secret" + id := d.createSecret(c, swarm.SecretSpec{ + swarm.Annotations{ + Name: testName, + }, + []byte("TESTINGDATA"), + }) + c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id)) + + secrets := d.listSecrets(c) + c.Assert(len(secrets), checker.Equals, 1, check.Commentf("secrets: %#v", secrets)) + name := secrets[0].Spec.Annotations.Name + c.Assert(name, checker.Equals, testName, check.Commentf("secret: %s", name)) +} + +func (s *DockerSwarmSuite) TestAPISwarmSecretsDelete(c *check.C) { + d := s.AddDaemon(c, true, true) + + testName := "test_secret" + id := d.createSecret(c, swarm.SecretSpec{ + swarm.Annotations{ + Name: testName, + }, + []byte("TESTINGDATA"), + }) + c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id)) + + secret := d.getSecret(c, id) + c.Assert(secret.ID, checker.Equals, id, check.Commentf("secret: %v", secret)) + + d.deleteSecret(c, secret.ID) + status, out, err := d.SockRequest("GET", "/secrets/"+id, nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNotFound, check.Commentf("secret delete: %s", string(out))) +} From bc88c956b66d7c89105e4936c68c301bb3c4a0fb Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Fri, 28 Oct 2016 17:16:07 -0700 Subject: [PATCH 14/37] secrets: only setup secret mount if secrets are requested Signed-off-by: Evan Hazlett Upstream-commit: 6d12de53699d6fa03c70493eec63ac5cfa41f2fe Component: engine --- components/engine/daemon/container_operations_unix.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/engine/daemon/container_operations_unix.go b/components/engine/daemon/container_operations_unix.go index bdadaa7d7f..163dede7a1 100644 --- a/components/engine/daemon/container_operations_unix.go +++ b/components/engine/daemon/container_operations_unix.go @@ -145,6 +145,10 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error { } func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { + if len(c.Secrets) == 0 { + return nil + } + localMountPath := c.SecretMountPath() logrus.Debugf("secrets: setting up secret dir: %s", localMountPath) From 3d26adc9bc40f441717de1df5db4b3d9c85fbfab Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Fri, 28 Oct 2016 17:30:22 -0700 Subject: [PATCH 15/37] secrets: only try to unmount if present Signed-off-by: Evan Hazlett Upstream-commit: 643ae8b400492d47a42524124b5d41da3c37e081 Component: engine --- components/engine/container/container_unix.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/engine/container/container_unix.go b/components/engine/container/container_unix.go index 8d825c7698..32325f5639 100644 --- a/components/engine/container/container_unix.go +++ b/components/engine/container/container_unix.go @@ -283,6 +283,14 @@ func (container *Container) SecretMount() *Mount { // UnmountSecrets unmounts the local tmpfs for secrets func (container *Container) UnmountSecrets() error { + if _, err := os.Stat(container.SecretMountPath()); err != nil { + if os.IsNotExist(err) { + return nil + } else { + return err + } + } + return detachMounted(container.SecretMountPath()) } From 5a7b3b358baf780021f92dc38c4190452aa489e1 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Mon, 31 Oct 2016 11:02:10 -0400 Subject: [PATCH 16/37] doc review updates Signed-off-by: Evan Hazlett Upstream-commit: cf6483a1529a7de8939ac5fb85ff94e5f2f4ff60 Component: engine --- .../engine/docs/reference/commandline/secret_inspect.md | 4 ++-- components/engine/docs/reference/commandline/secret_ls.md | 3 +-- components/engine/docs/reference/commandline/secret_rm.md | 2 +- .../engine/docs/reference/commandline/service_create.md | 5 +++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/engine/docs/reference/commandline/secret_inspect.md b/components/engine/docs/reference/commandline/secret_inspect.md index 1572a87b16..0b75bfe385 100644 --- a/components/engine/docs/reference/commandline/secret_inspect.md +++ b/components/engine/docs/reference/commandline/secret_inspect.md @@ -71,8 +71,8 @@ $ docker secret inspect mhv17xfe3gh6xc4rij5orpfds ### Formatting secret output -The `--format` option can be used to obtain specific information about a -secret. For example, the following command outputs the digest of the +You can use the --format option to obtain specific information about a +secret. The following example command outputs the digest of the secret. ```bash{% raw %} diff --git a/components/engine/docs/reference/commandline/secret_ls.md b/components/engine/docs/reference/commandline/secret_ls.md index 13a8e60454..aa1f31d615 100644 --- a/components/engine/docs/reference/commandline/secret_ls.md +++ b/components/engine/docs/reference/commandline/secret_ls.md @@ -27,8 +27,7 @@ Options: -q, --quiet Only display IDs ``` -This command when run targeting a manager, lists secrets in the -swarm. +Run this command from a manager to list the secrets in the Swarm. On a manager node: diff --git a/components/engine/docs/reference/commandline/secret_rm.md b/components/engine/docs/reference/commandline/secret_rm.md index 5f9ba5f69b..86f2df9a5a 100644 --- a/components/engine/docs/reference/commandline/secret_rm.md +++ b/components/engine/docs/reference/commandline/secret_rm.md @@ -30,7 +30,7 @@ Options: Removes the specified secrets from the swarm. This command has to be run targeting a manager node. -For example, to remove secret: +This example removes a secret: ```bash $ docker secret rm sapth4csdo5b6wz2p5uimh5xg diff --git a/components/engine/docs/reference/commandline/service_create.md b/components/engine/docs/reference/commandline/service_create.md index 9d98600043..8e12a88d85 100644 --- a/components/engine/docs/reference/commandline/service_create.md +++ b/components/engine/docs/reference/commandline/service_create.md @@ -121,8 +121,9 @@ ID NAME MODE REPLICAS IMAGE ``` ### Create a service with secrets -Use the `--secret` flag to use a [secret](secret_create.md). The following -command will create a service with two secrets named `ssh-key` and `app-key`: +Use the `--secret` flag to give a container access to a +[secret](secret_create.md). The following command will create a service +with two secrets named `ssh-key` and `app-key`: ```bash $ docker service create --name redis --secret ssh-key:ssh --secret app-key:app redis:3.0.6 From ade83acd05360af0e93877e3baa9069e0108410d Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Mon, 31 Oct 2016 11:53:43 -0400 Subject: [PATCH 17/37] lint fixes Signed-off-by: Evan Hazlett Upstream-commit: baffa793db51e72ef853336fb7c3a11a233ce039 Component: engine --- components/engine/container/container_unix.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/engine/container/container_unix.go b/components/engine/container/container_unix.go index 32325f5639..f02be89e78 100644 --- a/components/engine/container/container_unix.go +++ b/components/engine/container/container_unix.go @@ -286,9 +286,8 @@ func (container *Container) UnmountSecrets() error { if _, err := os.Stat(container.SecretMountPath()); err != nil { if os.IsNotExist(err) { return nil - } else { - return err } + return err } return detachMounted(container.SecretMountPath()) From da6add7ce9b01c04a524ba72dd158e1068778367 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Mon, 31 Oct 2016 15:38:00 -0400 Subject: [PATCH 18/37] secrets: add service create and update integration tests Signed-off-by: Evan Hazlett Upstream-commit: 76b33fdb99a6395670b8b466a5ef65a8b928be94 Component: engine --- .../docker_cli_service_create_test.go | 29 +++++++++++++ .../docker_cli_service_update_test.go | 42 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/components/engine/integration-cli/docker_cli_service_create_test.go b/components/engine/integration-cli/docker_cli_service_create_test.go index 4b71a18b8d..8d632ed237 100644 --- a/components/engine/integration-cli/docker_cli_service_create_test.go +++ b/components/engine/integration-cli/docker_cli_service_create_test.go @@ -4,6 +4,7 @@ package main import ( "encoding/json" + "fmt" "strings" "github.com/docker/docker/api/types" @@ -43,3 +44,31 @@ func (s *DockerSwarmSuite) TestServiceCreateMountVolume(c *check.C) { c.Assert(mounts[0].Destination, checker.Equals, "/foo") c.Assert(mounts[0].RW, checker.Equals, true) } + +func (s *DockerSwarmSuite) TestServiceCreateWithSecret(c *check.C) { + d := s.AddDaemon(c, true, true) + + serviceName := "test-service-secret" + testName := "test_secret" + id := d.createSecret(c, swarm.SecretSpec{ + swarm.Annotations{ + Name: testName, + }, + []byte("TESTINGDATA"), + }) + c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id)) + testTarget := "testing" + + out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", fmt.Sprintf("%s:%s", testName, testTarget), "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName) + c.Assert(err, checker.IsNil) + + var refs []swarm.SecretReference + c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil) + c.Assert(refs, checker.HasLen, 1) + + c.Assert(refs[0].SecretName, checker.Equals, testName) + c.Assert(refs[0].Target, checker.Equals, testTarget) +} diff --git a/components/engine/integration-cli/docker_cli_service_update_test.go b/components/engine/integration-cli/docker_cli_service_update_test.go index dd71cb0e7d..166c5efbaf 100644 --- a/components/engine/integration-cli/docker_cli_service_update_test.go +++ b/components/engine/integration-cli/docker_cli_service_update_test.go @@ -4,6 +4,7 @@ package main import ( "encoding/json" + "fmt" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/pkg/integration/checker" @@ -84,3 +85,44 @@ func (s *DockerSwarmSuite) TestServiceUpdateLabel(c *check.C) { c.Assert(service.Spec.Labels, checker.HasLen, 1) c.Assert(service.Spec.Labels["foo"], checker.Equals, "bar") } + +func (s *DockerSwarmSuite) TestServiceUpdateSecrets(c *check.C) { + d := s.AddDaemon(c, true, true) + testName := "test_secret" + id := d.createSecret(c, swarm.SecretSpec{ + swarm.Annotations{ + Name: testName, + }, + []byte("TESTINGDATA"), + }) + c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id)) + testTarget := "testing" + serviceName := "test" + + out, err := d.Cmd("service", "create", "--name", serviceName, "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + // add secret + out, err = d.Cmd("service", "update", "test", "--secret-add", fmt.Sprintf("%s:%s", testName, testTarget)) + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName) + c.Assert(err, checker.IsNil) + + var refs []swarm.SecretReference + c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil) + c.Assert(refs, checker.HasLen, 1) + + c.Assert(refs[0].SecretName, checker.Equals, testName) + c.Assert(refs[0].Target, checker.Equals, testTarget) + + // remove + out, err = d.Cmd("service", "update", "test", "--secret-rm", testName) + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName) + c.Assert(err, checker.IsNil) + + c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil) + c.Assert(refs, checker.HasLen, 0) +} From e9ea5a5560d02f646b4899a45b4c38af0ae5d796 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 1 Nov 2016 18:11:43 -0400 Subject: [PATCH 19/37] update to support new target in swarmkit Signed-off-by: Evan Hazlett Upstream-commit: 88dea0e06e0e94a8ab4cb5fe852e26bff309261a Component: engine --- .../engine/api/types/container/secret.go | 4 +-- components/engine/api/types/swarm/secret.go | 18 +++++----- .../engine/cli/command/service/parse.go | 9 +++-- .../daemon/cluster/convert/container.go | 34 +++++++++++-------- .../cluster/executor/container/adapter.go | 28 ++++++++++----- .../daemon/container_operations_unix.go | 2 +- 6 files changed, 59 insertions(+), 36 deletions(-) diff --git a/components/engine/api/types/container/secret.go b/components/engine/api/types/container/secret.go index eee5bf89d2..da86577f9c 100644 --- a/components/engine/api/types/container/secret.go +++ b/components/engine/api/types/container/secret.go @@ -6,7 +6,7 @@ type ContainerSecret struct { Name string Target string Data []byte - Uid int - Gid int + UID int + GID int Mode os.FileMode } diff --git a/components/engine/api/types/swarm/secret.go b/components/engine/api/types/swarm/secret.go index 86a6beafeb..1f842c32ca 100644 --- a/components/engine/api/types/swarm/secret.go +++ b/components/engine/api/types/swarm/secret.go @@ -1,5 +1,7 @@ package swarm +import "os" + // Secret represents a secret. type Secret struct { ID string @@ -14,17 +16,15 @@ type SecretSpec struct { Data []byte } -type SecretReferenceMode int - -const ( - SecretReferenceSystem SecretReferenceMode = 0 - SecretReferenceFile SecretReferenceMode = 1 - SecretReferenceEnv SecretReferenceMode = 2 -) +type SecretReferenceFileTarget struct { + Name string + UID string + GID string + Mode os.FileMode +} type SecretReference struct { SecretID string - Mode SecretReferenceMode - Target string SecretName string + Target SecretReferenceFileTarget } diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go index 71d6fb1958..5a22ed352c 100644 --- a/components/engine/cli/command/service/parse.go +++ b/components/engine/cli/command/service/parse.go @@ -54,8 +54,13 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt secretRef := &swarmtypes.SecretReference{ SecretName: n, - Mode: swarmtypes.SecretReferenceFile, - Target: t, + // TODO (ehazlett): parse these from cli request + Target: swarmtypes.SecretReferenceFileTarget{ + Name: t, + UID: "0", + GID: "0", + Mode: 0444, + }, } if _, exists := secretRefs[t]; exists { diff --git a/components/engine/daemon/cluster/convert/container.go b/components/engine/daemon/cluster/convert/container.go index 6436b01c60..1a6121c240 100644 --- a/components/engine/daemon/cluster/convert/container.go +++ b/components/engine/daemon/cluster/convert/container.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/Sirupsen/logrus" container "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" types "github.com/docker/docker/api/types/swarm" @@ -79,15 +80,17 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec { func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference { refs := []*swarmapi.SecretReference{} for _, s := range sr { - mode := swarmapi.SecretReference_FILE - if s.Mode == types.SecretReferenceSystem { - mode = swarmapi.SecretReference_SYSTEM - } refs = append(refs, &swarmapi.SecretReference{ SecretID: s.SecretID, SecretName: s.SecretName, - Target: s.Target, - Mode: mode, + Target: &swarmapi.SecretReference_File{ + File: &swarmapi.SecretReference_FileTarget{ + Name: s.Target.Name, + UID: s.Target.UID, + GID: s.Target.GID, + Mode: s.Target.Mode, + }, + }, }) } @@ -96,18 +99,21 @@ func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretRefer func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference { refs := []*types.SecretReference{} for _, s := range sr { - var mode types.SecretReferenceMode - switch s.Mode { - case swarmapi.SecretReference_SYSTEM: - mode = types.SecretReferenceSystem - default: - mode = types.SecretReferenceFile + target := s.GetFile() + if target == nil { + // not a file target + logrus.Warnf("secret target not a file: secret=%s", s.SecretID) + continue } refs = append(refs, &types.SecretReference{ SecretID: s.SecretID, SecretName: s.SecretName, - Target: s.Target, - Mode: mode, + Target: types.SecretReferenceFileTarget{ + Name: target.Name, + UID: target.UID, + GID: target.GID, + Mode: target.Mode, + }, }) } diff --git a/components/engine/daemon/cluster/executor/container/adapter.go b/components/engine/daemon/cluster/executor/container/adapter.go index 1cdb6429f2..02c327f8a1 100644 --- a/components/engine/daemon/cluster/executor/container/adapter.go +++ b/components/engine/daemon/cluster/executor/container/adapter.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "strconv" "strings" "syscall" "time" @@ -227,18 +228,29 @@ func (c *containerAdapter) create(ctx context.Context) error { } name := sec.Spec.Annotations.Name - target := s.Target - if target == "" { - target = name + target := s.GetFile() + if target == nil { + logrus.Warnf("secret target was not a file: secret=%s", s.SecretID) + continue } + // convert uid / gid string to int + uid, err := strconv.Atoi(target.UID) + if err != nil { + return err + } + + gid, err := strconv.Atoi(target.GID) + if err != nil { + return err + } + secrets = append(secrets, &containertypes.ContainerSecret{ Name: name, - Target: target, + Target: target.Name, Data: sec.Spec.Data, - // TODO (ehazlett): enable configurable uid, gid, mode - Uid: 0, - Gid: 0, - Mode: 0444, + UID: uid, + GID: gid, + Mode: target.Mode, }) } diff --git a/components/engine/daemon/container_operations_unix.go b/components/engine/daemon/container_operations_unix.go index 163dede7a1..ac6b6ad1ad 100644 --- a/components/engine/daemon/container_operations_unix.go +++ b/components/engine/daemon/container_operations_unix.go @@ -191,7 +191,7 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { return errors.Wrap(err, "error injecting secret") } - if err := os.Chown(fPath, s.Uid, s.Gid); err != nil { + if err := os.Chown(fPath, s.UID, s.GID); err != nil { return errors.Wrap(err, "error setting ownership for secret") } } From 6984dffbb514d7df4fe4168370018b7a22ea5b56 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 1 Nov 2016 18:59:16 -0400 Subject: [PATCH 20/37] update tests for service create and update Signed-off-by: Evan Hazlett Upstream-commit: ca6b6535519881357c9e3bba5fe0d8965ce8077c Component: engine --- .../engine/integration-cli/docker_cli_service_create_test.go | 3 ++- .../engine/integration-cli/docker_cli_service_update_test.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/components/engine/integration-cli/docker_cli_service_create_test.go b/components/engine/integration-cli/docker_cli_service_create_test.go index 8d632ed237..85d07489a5 100644 --- a/components/engine/integration-cli/docker_cli_service_create_test.go +++ b/components/engine/integration-cli/docker_cli_service_create_test.go @@ -70,5 +70,6 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecret(c *check.C) { c.Assert(refs, checker.HasLen, 1) c.Assert(refs[0].SecretName, checker.Equals, testName) - c.Assert(refs[0].Target, checker.Equals, testTarget) + c.Assert(refs[0].Target, checker.Not(checker.IsNil)) + c.Assert(refs[0].Target.Name, checker.Equals, testTarget) } diff --git a/components/engine/integration-cli/docker_cli_service_update_test.go b/components/engine/integration-cli/docker_cli_service_update_test.go index 166c5efbaf..7bad23901a 100644 --- a/components/engine/integration-cli/docker_cli_service_update_test.go +++ b/components/engine/integration-cli/docker_cli_service_update_test.go @@ -114,7 +114,8 @@ func (s *DockerSwarmSuite) TestServiceUpdateSecrets(c *check.C) { c.Assert(refs, checker.HasLen, 1) c.Assert(refs[0].SecretName, checker.Equals, testName) - c.Assert(refs[0].Target, checker.Equals, testTarget) + c.Assert(refs[0].Target, checker.Not(checker.IsNil)) + c.Assert(refs[0].Target.Name, checker.Equals, testTarget) // remove out, err = d.Cmd("service", "update", "test", "--secret-rm", testName) From 9c2b1fd34ad180e02a17552b33f2b546aef74651 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 1 Nov 2016 22:28:32 -0400 Subject: [PATCH 21/37] secrets: use explicit format when using secrets Signed-off-by: Evan Hazlett Upstream-commit: 2adbdcdf5a83b45b4e191413d3aa14158535085b Component: engine --- .../engine/cli/command/service/create.go | 3 +- components/engine/cli/command/service/opts.go | 97 ++++++++++++++++++- .../engine/cli/command/service/opts_test.go | 45 +++++++++ .../engine/cli/command/service/parse.go | 54 ++--------- .../engine/cli/command/service/update.go | 7 +- .../reference/commandline/service_create.md | 2 +- .../reference/commandline/service_update.md | 2 +- .../docker_cli_service_create_test.go | 2 +- .../docker_cli_service_update_test.go | 2 +- 9 files changed, 158 insertions(+), 56 deletions(-) diff --git a/components/engine/cli/command/service/create.go b/components/engine/cli/command/service/create.go index 8fb9070e67..e5b728d3e8 100644 --- a/components/engine/cli/command/service/create.go +++ b/components/engine/cli/command/service/create.go @@ -39,6 +39,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service") flags.Var(&opts.constraints, flagConstraint, "Placement constraints") flags.Var(&opts.networks, flagNetwork, "Network attachments") + flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") @@ -59,7 +60,7 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { } // parse and validate secrets - secrets, err := parseSecrets(apiClient, opts.secrets) + secrets, err := parseSecrets(apiClient, opts.secrets.Value()) if err != nil { return err } diff --git a/components/engine/cli/command/service/opts.go b/components/engine/cli/command/service/opts.go index 37da5d1145..00cdecb67d 100644 --- a/components/engine/cli/command/service/opts.go +++ b/components/engine/cli/command/service/opts.go @@ -1,7 +1,10 @@ package service import ( + "encoding/csv" "fmt" + "os" + "path/filepath" "strconv" "strings" "time" @@ -139,6 +142,98 @@ func (f *floatValue) Value() float32 { return float32(*f) } +// SecretRequestSpec is a type for requesting secrets +type SecretRequestSpec struct { + source string + target string + uid string + gid string + mode os.FileMode +} + +// SecretOpt is a Value type for parsing secrets +type SecretOpt struct { + values []*SecretRequestSpec +} + +// Set a new secret value +func (o *SecretOpt) Set(value string) error { + csvReader := csv.NewReader(strings.NewReader(value)) + fields, err := csvReader.Read() + if err != nil { + return err + } + + spec := &SecretRequestSpec{ + source: "", + target: "", + uid: "0", + gid: "0", + mode: 0444, + } + + for _, field := range fields { + parts := strings.SplitN(field, "=", 2) + key := strings.ToLower(parts[0]) + + if len(parts) != 2 { + return fmt.Errorf("invalid field '%s' must be a key=value pair", field) + } + + value := parts[1] + switch key { + case "source": + spec.source = value + case "target": + tDir, _ := filepath.Split(value) + if tDir != "" { + return fmt.Errorf("target must not have a path") + } + spec.target = value + case "uid": + spec.uid = value + case "gid": + spec.gid = value + case "mode": + m, err := strconv.ParseUint(value, 0, 32) + if err != nil { + return fmt.Errorf("invalid mode specified: %v", err) + } + + spec.mode = os.FileMode(m) + default: + return fmt.Errorf("invalid field in secret request: %s", key) + } + } + + if spec.source == "" { + return fmt.Errorf("source is required") + } + + o.values = append(o.values, spec) + return nil +} + +// Type returns the type of this option +func (o *SecretOpt) Type() string { + return "secret" +} + +// String returns a string repr of this option +func (o *SecretOpt) String() string { + secrets := []string{} + for _, secret := range o.values { + repr := fmt.Sprintf("%s -> %s", secret.source, secret.target) + secrets = append(secrets, repr) + } + return strings.Join(secrets, ", ") +} + +// Value returns the secret requests +func (o *SecretOpt) Value() []*SecretRequestSpec { + return o.values +} + type updateOptions struct { parallelism uint64 delay time.Duration @@ -337,7 +432,7 @@ type serviceOptions struct { logDriver logDriverOptions healthcheck healthCheckOptions - secrets []string + secrets SecretOpt } func newServiceOptions() *serviceOptions { diff --git a/components/engine/cli/command/service/opts_test.go b/components/engine/cli/command/service/opts_test.go index aa2d999dcf..551dfc239c 100644 --- a/components/engine/cli/command/service/opts_test.go +++ b/components/engine/cli/command/service/opts_test.go @@ -1,6 +1,7 @@ package service import ( + "os" "reflect" "testing" "time" @@ -105,3 +106,47 @@ func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) { _, err := opt.toHealthConfig() assert.Error(t, err, "--no-healthcheck conflicts with --health-* options") } + +func TestSecretOptionsSimple(t *testing.T) { + var opt SecretOpt + + testCase := "source=/foo,target=testing" + assert.NilError(t, opt.Set(testCase)) + + reqs := opt.Value() + assert.Equal(t, len(reqs), 1) + req := reqs[0] + assert.Equal(t, req.source, "/foo") + assert.Equal(t, req.target, "testing") +} + +func TestSecretOptionsCustomUidGid(t *testing.T) { + var opt SecretOpt + + testCase := "source=/foo,target=testing,uid=1000,gid=1001" + assert.NilError(t, opt.Set(testCase)) + + reqs := opt.Value() + assert.Equal(t, len(reqs), 1) + req := reqs[0] + assert.Equal(t, req.source, "/foo") + assert.Equal(t, req.target, "testing") + assert.Equal(t, req.uid, "1000") + assert.Equal(t, req.gid, "1001") +} + +func TestSecretOptionsCustomMode(t *testing.T) { + var opt SecretOpt + + testCase := "source=/foo,target=testing,uid=1000,gid=1001,mode=0444" + assert.NilError(t, opt.Set(testCase)) + + reqs := opt.Value() + assert.Equal(t, len(reqs), 1) + req := reqs[0] + assert.Equal(t, req.source, "/foo") + assert.Equal(t, req.target, "testing") + assert.Equal(t, req.uid, "1000") + assert.Equal(t, req.gid, "1001") + assert.Equal(t, req.mode, os.FileMode(0444)) +} diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go index 5a22ed352c..73fa8a0cb9 100644 --- a/components/engine/cli/command/service/parse.go +++ b/components/engine/cli/command/service/parse.go @@ -3,8 +3,6 @@ package service import ( "context" "fmt" - "path/filepath" - "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -12,61 +10,27 @@ import ( "github.com/docker/docker/client" ) -// parseSecretString parses the requested secret and returns the secret name -// and target. Expects format SECRET_NAME:TARGET -func parseSecretString(secretString string) (string, string, error) { - tokens := strings.Split(secretString, ":") - - secretName := strings.TrimSpace(tokens[0]) - targetName := secretName - - if secretName == "" { - return "", "", fmt.Errorf("invalid secret name provided") - } - - if len(tokens) > 1 { - targetName = strings.TrimSpace(tokens[1]) - if targetName == "" { - return "", "", fmt.Errorf("invalid presentation name provided") - } - } - - // ensure target is a filename only; no paths allowed - tDir, _ := filepath.Split(targetName) - if tDir != "" { - return "", "", fmt.Errorf("target must not have a path") - } - - return secretName, targetName, nil -} - // parseSecrets retrieves the secrets from the requested names and converts // them to secret references to use with the spec -func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) { +func parseSecrets(client client.APIClient, requestedSecrets []*SecretRequestSpec) ([]*swarmtypes.SecretReference, error) { secretRefs := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() for _, secret := range requestedSecrets { - n, t, err := parseSecretString(secret) - if err != nil { - return nil, err - } - secretRef := &swarmtypes.SecretReference{ - SecretName: n, - // TODO (ehazlett): parse these from cli request + SecretName: secret.source, Target: swarmtypes.SecretReferenceFileTarget{ - Name: t, - UID: "0", - GID: "0", - Mode: 0444, + Name: secret.target, + UID: secret.uid, + GID: secret.gid, + Mode: secret.mode, }, } - if _, exists := secretRefs[t]; exists { - return nil, fmt.Errorf("duplicate secret target for %s not allowed", n) + if _, exists := secretRefs[secret.target]; exists { + return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.source) } - secretRefs[t] = secretRef + secretRefs[secret.target] = secretRef } args := filters.NewArgs() diff --git a/components/engine/cli/command/service/update.go b/components/engine/cli/command/service/update.go index a9f5ac9be6..37f709e230 100644 --- a/components/engine/cli/command/service/update.go +++ b/components/engine/cli/command/service/update.go @@ -56,7 +56,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret") - flags.StringSliceVar(&opts.secrets, flagSecretAdd, []string{}, "Add a secret") + flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service") flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint") flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") @@ -413,10 +413,7 @@ func updateEnvironment(flags *pflag.FlagSet, field *[]string) { func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) { if flags.Changed(flagSecretAdd) { - values, err := flags.GetStringSlice(flagSecretAdd) - if err != nil { - return nil, err - } + values := flags.Lookup(flagSecretAdd).Value.(*SecretOpt).Value() addSecrets, err := parseSecrets(apiClient, values) if err != nil { diff --git a/components/engine/docs/reference/commandline/service_create.md b/components/engine/docs/reference/commandline/service_create.md index 8e12a88d85..e98c2cc8ae 100644 --- a/components/engine/docs/reference/commandline/service_create.md +++ b/components/engine/docs/reference/commandline/service_create.md @@ -126,7 +126,7 @@ Use the `--secret` flag to give a container access to a with two secrets named `ssh-key` and `app-key`: ```bash -$ docker service create --name redis --secret ssh-key:ssh --secret app-key:app redis:3.0.6 +$ 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 4cdgfyky7ozwh3htjfw0d12qv ``` diff --git a/components/engine/docs/reference/commandline/service_update.md b/components/engine/docs/reference/commandline/service_update.md index 2cc67996e6..261f1c3518 100644 --- a/components/engine/docs/reference/commandline/service_update.md +++ b/components/engine/docs/reference/commandline/service_update.md @@ -157,7 +157,7 @@ The following example adds a secret named `ssh-2` and removes `ssh-1`: ```bash $ docker service update \ - --secret-add ssh-2 \ + --secret-add source=ssh-2,target=ssh-2 \ --secret-rm ssh-1 \ myservice ``` diff --git a/components/engine/integration-cli/docker_cli_service_create_test.go b/components/engine/integration-cli/docker_cli_service_create_test.go index 85d07489a5..bac23b0eb1 100644 --- a/components/engine/integration-cli/docker_cli_service_create_test.go +++ b/components/engine/integration-cli/docker_cli_service_create_test.go @@ -59,7 +59,7 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecret(c *check.C) { c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id)) testTarget := "testing" - out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", fmt.Sprintf("%s:%s", testName, testTarget), "busybox", "top") + out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", fmt.Sprintf("source=%s,target=%s", testName, testTarget), "busybox", "top") c.Assert(err, checker.IsNil, check.Commentf(out)) out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName) diff --git a/components/engine/integration-cli/docker_cli_service_update_test.go b/components/engine/integration-cli/docker_cli_service_update_test.go index 7bad23901a..548f8e64c2 100644 --- a/components/engine/integration-cli/docker_cli_service_update_test.go +++ b/components/engine/integration-cli/docker_cli_service_update_test.go @@ -103,7 +103,7 @@ func (s *DockerSwarmSuite) TestServiceUpdateSecrets(c *check.C) { c.Assert(err, checker.IsNil, check.Commentf(out)) // add secret - out, err = d.Cmd("service", "update", "test", "--secret-add", fmt.Sprintf("%s:%s", testName, testTarget)) + out, err = d.Cmd("service", "update", "test", "--secret-add", fmt.Sprintf("source=%s,target=%s", testName, testTarget)) c.Assert(err, checker.IsNil, check.Commentf(out)) out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName) From c28de44f7a1a0dc23f82d058669cdfea1a76e9b1 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 1 Nov 2016 23:32:21 -0400 Subject: [PATCH 22/37] secrets: enable secret inspect and rm by secret name Signed-off-by: Evan Hazlett Upstream-commit: e0e65b9a3baab6bb3f35eddb6ed3d52654184029 Component: engine --- .../engine/cli/command/secret/inspect.go | 20 ++++++++++++--- .../engine/cli/command/secret/remove.go | 25 ++++++++++++++++++- components/engine/cli/command/secret/utils.go | 21 ++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 components/engine/cli/command/secret/utils.go diff --git a/components/engine/cli/command/secret/inspect.go b/components/engine/cli/command/secret/inspect.go index c8d5cd8f79..c5b0aa6a3d 100644 --- a/components/engine/cli/command/secret/inspect.go +++ b/components/engine/cli/command/secret/inspect.go @@ -34,9 +34,23 @@ func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() - getRef := func(name string) (interface{}, []byte, error) { - return client.SecretInspectWithRaw(ctx, name) + // attempt to lookup secret by name + secrets, err := getSecrets(client, ctx, []string{opts.name}) + if err != nil { + return err } - return inspect.Inspect(dockerCli.Out(), []string{opts.name}, opts.format, getRef) + id := opts.name + for _, s := range secrets { + if s.Spec.Annotations.Name == opts.name { + id = s.ID + break + } + } + + getRef := func(name string) (interface{}, []byte, error) { + return client.SecretInspectWithRaw(ctx, id) + } + + return inspect.Inspect(dockerCli.Out(), []string{id}, opts.format, getRef) } diff --git a/components/engine/cli/command/secret/remove.go b/components/engine/cli/command/secret/remove.go index f336c6161a..9396b9b179 100644 --- a/components/engine/cli/command/secret/remove.go +++ b/components/engine/cli/command/secret/remove.go @@ -31,7 +31,30 @@ func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { client := dockerCli.Client() ctx := context.Background() - for _, id := range opts.ids { + // attempt to lookup secret by name + secrets, err := getSecrets(client, ctx, opts.ids) + if err != nil { + return err + } + + ids := opts.ids + + names := make(map[string]int) + for _, id := range ids { + names[id] = 1 + } + + if len(secrets) > 0 { + ids = []string{} + + for _, s := range secrets { + if _, ok := names[s.Spec.Annotations.Name]; ok { + ids = append(ids, s.ID) + } + } + } + + for _, id := range ids { if err := client.SecretRemove(ctx, id); err != nil { return err } diff --git a/components/engine/cli/command/secret/utils.go b/components/engine/cli/command/secret/utils.go new file mode 100644 index 0000000000..40aa4a6d77 --- /dev/null +++ b/components/engine/cli/command/secret/utils.go @@ -0,0 +1,21 @@ +package secret + +import ( + "context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" +) + +func getSecrets(client client.APIClient, ctx context.Context, names []string) ([]swarm.Secret, error) { + args := filters.NewArgs() + for _, n := range names { + args.Add("names", n) + } + + return client.SecretList(ctx, types.SecretListOptions{ + Filter: args, + }) +} From 3e01794690a5544f994e051d54fcbd11e6751f22 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 11:08:22 -0400 Subject: [PATCH 23/37] move secretopt to opts pkg Signed-off-by: Evan Hazlett Upstream-commit: c00138748daeef52d353dda66c7b89322b7708fc Component: engine --- components/engine/api/types/client.go | 10 ++ components/engine/api/types/swarm/secret.go | 9 ++ components/engine/cli/command/service/opts.go | 2 +- .../engine/cli/command/service/parse.go | 18 ++-- .../engine/cli/command/service/update.go | 2 +- components/engine/opts/secret.go | 95 +++++++++++++++++++ 6 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 components/engine/opts/secret.go diff --git a/components/engine/api/types/client.go b/components/engine/api/types/client.go index 769c836446..d79ebd9e55 100644 --- a/components/engine/api/types/client.go +++ b/components/engine/api/types/client.go @@ -4,6 +4,7 @@ import ( "bufio" "io" "net" + "os" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" @@ -339,3 +340,12 @@ type PluginInstallOptions struct { AcceptPermissionsFunc func(PluginPrivileges) (bool, error) Args []string } + +// SecretRequestOptions is a type for requesting secrets +type SecretRequestOptions struct { + Source string + Target string + UID string + GID string + Mode os.FileMode +} diff --git a/components/engine/api/types/swarm/secret.go b/components/engine/api/types/swarm/secret.go index 1f842c32ca..21a9e7cde2 100644 --- a/components/engine/api/types/swarm/secret.go +++ b/components/engine/api/types/swarm/secret.go @@ -28,3 +28,12 @@ type SecretReference struct { SecretName string Target SecretReferenceFileTarget } + +// SecretRequestSpec is a type for requesting secrets +type SecretRequestSpec struct { + Source string + Target string + UID string + GID string + Mode os.FileMode +} diff --git a/components/engine/cli/command/service/opts.go b/components/engine/cli/command/service/opts.go index 00cdecb67d..45adb37672 100644 --- a/components/engine/cli/command/service/opts.go +++ b/components/engine/cli/command/service/opts.go @@ -432,7 +432,7 @@ type serviceOptions struct { logDriver logDriverOptions healthcheck healthCheckOptions - secrets SecretOpt + secrets opts.SecretOpt } func newServiceOptions() *serviceOptions { diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go index 73fa8a0cb9..cbf2745dce 100644 --- a/components/engine/cli/command/service/parse.go +++ b/components/engine/cli/command/service/parse.go @@ -12,25 +12,25 @@ import ( // parseSecrets retrieves the secrets from the requested names and converts // them to secret references to use with the spec -func parseSecrets(client client.APIClient, requestedSecrets []*SecretRequestSpec) ([]*swarmtypes.SecretReference, error) { +func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretRequestOptions) ([]*swarmtypes.SecretReference, error) { secretRefs := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() for _, secret := range requestedSecrets { secretRef := &swarmtypes.SecretReference{ - SecretName: secret.source, + SecretName: secret.Source, Target: swarmtypes.SecretReferenceFileTarget{ - Name: secret.target, - UID: secret.uid, - GID: secret.gid, - Mode: secret.mode, + Name: secret.Target, + UID: secret.UID, + GID: secret.GID, + Mode: secret.Mode, }, } - if _, exists := secretRefs[secret.target]; exists { - return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.source) + if _, exists := secretRefs[secret.Target]; exists { + return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.Source) } - secretRefs[secret.target] = secretRef + secretRefs[secret.Target] = secretRef } args := filters.NewArgs() diff --git a/components/engine/cli/command/service/update.go b/components/engine/cli/command/service/update.go index 37f709e230..1bc72a8f19 100644 --- a/components/engine/cli/command/service/update.go +++ b/components/engine/cli/command/service/update.go @@ -413,7 +413,7 @@ func updateEnvironment(flags *pflag.FlagSet, field *[]string) { func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) { if flags.Changed(flagSecretAdd) { - values := flags.Lookup(flagSecretAdd).Value.(*SecretOpt).Value() + values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value() addSecrets, err := parseSecrets(apiClient, values) if err != nil { diff --git a/components/engine/opts/secret.go b/components/engine/opts/secret.go new file mode 100644 index 0000000000..34ed42a680 --- /dev/null +++ b/components/engine/opts/secret.go @@ -0,0 +1,95 @@ +package opts + +import ( + "encoding/csv" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/docker/docker/api/types" +) + +// SecretOpt is a Value type for parsing secrets +type SecretOpt struct { + values []*types.SecretRequestOptions +} + +// Set a new secret value +func (o *SecretOpt) Set(value string) error { + csvReader := csv.NewReader(strings.NewReader(value)) + fields, err := csvReader.Read() + if err != nil { + return err + } + + options := &types.SecretRequestOptions{ + Source: "", + Target: "", + UID: "0", + GID: "0", + Mode: 0444, + } + + for _, field := range fields { + parts := strings.SplitN(field, "=", 2) + key := strings.ToLower(parts[0]) + + if len(parts) != 2 { + return fmt.Errorf("invalid field '%s' must be a key=value pair", field) + } + + value := parts[1] + switch key { + case "source": + options.Source = value + case "target": + tDir, _ := filepath.Split(value) + if tDir != "" { + return fmt.Errorf("target must not be a path") + } + options.Target = value + case "uid": + options.UID = value + case "gid": + options.GID = value + case "mode": + m, err := strconv.ParseUint(value, 0, 32) + if err != nil { + return fmt.Errorf("invalid mode specified: %v", err) + } + + options.Mode = os.FileMode(m) + default: + return fmt.Errorf("invalid field in secret request: %s", key) + } + } + + if options.Source == "" { + return fmt.Errorf("source is required") + } + + o.values = append(o.values, options) + return nil +} + +// Type returns the type of this option +func (o *SecretOpt) Type() string { + return "secret" +} + +// String returns a string repr of this option +func (o *SecretOpt) String() string { + secrets := []string{} + for _, secret := range o.values { + repr := fmt.Sprintf("%s -> %s", secret.Source, secret.Target) + secrets = append(secrets, repr) + } + return strings.Join(secrets, ", ") +} + +// Value returns the secret requests +func (o *SecretOpt) Value() []*types.SecretRequestOptions { + return o.values +} From e09372844256092b1c168db54391f82b17968742 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 14:09:13 -0400 Subject: [PATCH 24/37] review updates - use Filters instead of Filter for secret list - UID, GID -> string - getSecrets -> getSecretsByName - updated test case for secrets with better source - use golang.org/x/context instead of context - for grpc conversion allocate with make - check for nil with task.Spec.GetContainer() Signed-off-by: Evan Hazlett Upstream-commit: b2e4c7f3b5b30fc6940768ec41836d708a48b463 Component: engine --- .../api/server/router/swarm/cluster_routes.go | 8 ++--- .../engine/api/types/container/secret.go | 4 +-- components/engine/api/types/types.go | 2 +- .../engine/cli/command/secret/inspect.go | 2 +- .../engine/cli/command/secret/remove.go | 2 +- components/engine/cli/command/secret/utils.go | 4 +-- .../engine/cli/command/service/opts_test.go | 34 +++++++++---------- .../engine/cli/command/service/parse.go | 4 +-- components/engine/client/secret_list.go | 4 +-- components/engine/client/secret_list_test.go | 2 +- .../daemon/cluster/convert/container.go | 4 +-- .../cluster/executor/container/adapter.go | 21 ++++-------- components/engine/daemon/cluster/secrets.go | 3 +- .../daemon/container_operations_unix.go | 11 +++++- 14 files changed, 52 insertions(+), 53 deletions(-) diff --git a/components/engine/api/server/router/swarm/cluster_routes.go b/components/engine/api/server/router/swarm/cluster_routes.go index b04d066191..3c98e3ee26 100644 --- a/components/engine/api/server/router/swarm/cluster_routes.go +++ b/components/engine/api/server/router/swarm/cluster_routes.go @@ -267,14 +267,13 @@ func (sr *swarmRouter) getSecrets(ctx context.Context, w http.ResponseWriter, r if err := httputils.ParseForm(r); err != nil { return err } - filter, err := filters.FromParam(r.Form.Get("filters")) + filters, err := filters.FromParam(r.Form.Get("filters")) if err != nil { return err } - secrets, err := sr.backend.GetSecrets(basictypes.SecretListOptions{Filter: filter}) + secrets, err := sr.backend.GetSecrets(basictypes.SecretListOptions{Filters: filters}) if err != nil { - logrus.Errorf("Error getting secrets: %v", err) return err } @@ -289,7 +288,6 @@ func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter, id, err := sr.backend.CreateSecret(secret) if err != nil { - logrus.Errorf("Error creating secret %s: %v", id, err) return err } @@ -300,7 +298,6 @@ func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter, func (sr *swarmRouter) removeSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := sr.backend.RemoveSecret(vars["id"]); err != nil { - logrus.Errorf("Error removing secret %s: %v", vars["id"], err) return err } @@ -310,7 +307,6 @@ func (sr *swarmRouter) removeSecret(ctx context.Context, w http.ResponseWriter, func (sr *swarmRouter) getSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { secret, err := sr.backend.GetSecret(vars["id"]) if err != nil { - logrus.Errorf("Error getting secret %s: %v", vars["id"], err) return err } diff --git a/components/engine/api/types/container/secret.go b/components/engine/api/types/container/secret.go index da86577f9c..13a50d352c 100644 --- a/components/engine/api/types/container/secret.go +++ b/components/engine/api/types/container/secret.go @@ -6,7 +6,7 @@ type ContainerSecret struct { Name string Target string Data []byte - UID int - GID int + UID string + GID string Mode os.FileMode } diff --git a/components/engine/api/types/types.go b/components/engine/api/types/types.go index fe60755c84..ceb18ddd89 100644 --- a/components/engine/api/types/types.go +++ b/components/engine/api/types/types.go @@ -520,5 +520,5 @@ type SecretCreateResponse struct { // SecretListOptions holds parameters to list secrets type SecretListOptions struct { - Filter filters.Args + Filters filters.Args } diff --git a/components/engine/cli/command/secret/inspect.go b/components/engine/cli/command/secret/inspect.go index c5b0aa6a3d..25da79f16d 100644 --- a/components/engine/cli/command/secret/inspect.go +++ b/components/engine/cli/command/secret/inspect.go @@ -35,7 +35,7 @@ func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error { ctx := context.Background() // attempt to lookup secret by name - secrets, err := getSecrets(client, ctx, []string{opts.name}) + secrets, err := getSecretsByName(client, ctx, []string{opts.name}) if err != nil { return err } diff --git a/components/engine/cli/command/secret/remove.go b/components/engine/cli/command/secret/remove.go index 9396b9b179..d277eceba2 100644 --- a/components/engine/cli/command/secret/remove.go +++ b/components/engine/cli/command/secret/remove.go @@ -32,7 +32,7 @@ func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { ctx := context.Background() // attempt to lookup secret by name - secrets, err := getSecrets(client, ctx, opts.ids) + secrets, err := getSecretsByName(client, ctx, opts.ids) if err != nil { return err } diff --git a/components/engine/cli/command/secret/utils.go b/components/engine/cli/command/secret/utils.go index 40aa4a6d77..d1a7d97c44 100644 --- a/components/engine/cli/command/secret/utils.go +++ b/components/engine/cli/command/secret/utils.go @@ -9,13 +9,13 @@ import ( "github.com/docker/docker/client" ) -func getSecrets(client client.APIClient, ctx context.Context, names []string) ([]swarm.Secret, error) { +func getSecretsByName(client client.APIClient, ctx context.Context, names []string) ([]swarm.Secret, error) { args := filters.NewArgs() for _, n := range names { args.Add("names", n) } return client.SecretList(ctx, types.SecretListOptions{ - Filter: args, + Filters: args, }) } diff --git a/components/engine/cli/command/service/opts_test.go b/components/engine/cli/command/service/opts_test.go index 551dfc239c..3df3a4fd5d 100644 --- a/components/engine/cli/command/service/opts_test.go +++ b/components/engine/cli/command/service/opts_test.go @@ -108,45 +108,45 @@ func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) { } func TestSecretOptionsSimple(t *testing.T) { - var opt SecretOpt + var opt opts.SecretOpt - testCase := "source=/foo,target=testing" + testCase := "source=foo,target=testing" assert.NilError(t, opt.Set(testCase)) reqs := opt.Value() assert.Equal(t, len(reqs), 1) req := reqs[0] - assert.Equal(t, req.source, "/foo") - assert.Equal(t, req.target, "testing") + assert.Equal(t, req.Source, "foo") + assert.Equal(t, req.Target, "testing") } func TestSecretOptionsCustomUidGid(t *testing.T) { - var opt SecretOpt + var opt opts.SecretOpt - testCase := "source=/foo,target=testing,uid=1000,gid=1001" + testCase := "source=foo,target=testing,uid=1000,gid=1001" assert.NilError(t, opt.Set(testCase)) reqs := opt.Value() assert.Equal(t, len(reqs), 1) req := reqs[0] - assert.Equal(t, req.source, "/foo") - assert.Equal(t, req.target, "testing") - assert.Equal(t, req.uid, "1000") - assert.Equal(t, req.gid, "1001") + assert.Equal(t, req.Source, "foo") + assert.Equal(t, req.Target, "testing") + assert.Equal(t, req.UID, "1000") + assert.Equal(t, req.GID, "1001") } func TestSecretOptionsCustomMode(t *testing.T) { - var opt SecretOpt + var opt opts.SecretOpt - testCase := "source=/foo,target=testing,uid=1000,gid=1001,mode=0444" + testCase := "source=foo,target=testing,uid=1000,gid=1001,mode=0444" assert.NilError(t, opt.Set(testCase)) reqs := opt.Value() assert.Equal(t, len(reqs), 1) req := reqs[0] - assert.Equal(t, req.source, "/foo") - assert.Equal(t, req.target, "testing") - assert.Equal(t, req.uid, "1000") - assert.Equal(t, req.gid, "1001") - assert.Equal(t, req.mode, os.FileMode(0444)) + assert.Equal(t, req.Source, "foo") + assert.Equal(t, req.Target, "testing") + assert.Equal(t, req.UID, "1000") + assert.Equal(t, req.GID, "1001") + assert.Equal(t, req.Mode, os.FileMode(0444)) } diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go index cbf2745dce..4728c773c4 100644 --- a/components/engine/cli/command/service/parse.go +++ b/components/engine/cli/command/service/parse.go @@ -1,13 +1,13 @@ package service import ( - "context" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" swarmtypes "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" + "golang.org/x/net/context" ) // parseSecrets retrieves the secrets from the requested names and converts @@ -39,7 +39,7 @@ func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretReque } secrets, err := client.SecretList(ctx, types.SecretListOptions{ - Filter: args, + Filters: args, }) if err != nil { return nil, err diff --git a/components/engine/client/secret_list.go b/components/engine/client/secret_list.go index 5e9d2b5098..7e9d5ec167 100644 --- a/components/engine/client/secret_list.go +++ b/components/engine/client/secret_list.go @@ -14,8 +14,8 @@ import ( func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { query := url.Values{} - if options.Filter.Len() > 0 { - filterJSON, err := filters.ToParam(options.Filter) + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filters) if err != nil { return nil, err } diff --git a/components/engine/client/secret_list_test.go b/components/engine/client/secret_list_test.go index 174963c7ee..1ac11cddb3 100644 --- a/components/engine/client/secret_list_test.go +++ b/components/engine/client/secret_list_test.go @@ -45,7 +45,7 @@ func TestSecretList(t *testing.T) { }, { options: types.SecretListOptions{ - Filter: filters, + Filters: filters, }, expectedQueryParams: map[string]string{ "filters": `{"label":{"label1":true,"label2":true}}`, diff --git a/components/engine/daemon/cluster/convert/container.go b/components/engine/daemon/cluster/convert/container.go index 1a6121c240..b5ce27dc61 100644 --- a/components/engine/daemon/cluster/convert/container.go +++ b/components/engine/daemon/cluster/convert/container.go @@ -78,7 +78,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec { } func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference { - refs := []*swarmapi.SecretReference{} + refs := make([]*swarmapi.SecretReference, 0, len(sr)) for _, s := range sr { refs = append(refs, &swarmapi.SecretReference{ SecretID: s.SecretID, @@ -97,7 +97,7 @@ func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretRefer return refs } func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference { - refs := []*types.SecretReference{} + refs := make([]*types.SecretReference, 0, len(sr)) for _, s := range sr { target := s.GetFile() if target == nil { diff --git a/components/engine/daemon/cluster/executor/container/adapter.go b/components/engine/daemon/cluster/executor/container/adapter.go index 02c327f8a1..87ddaac455 100644 --- a/components/engine/daemon/cluster/executor/container/adapter.go +++ b/components/engine/daemon/cluster/executor/container/adapter.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "strconv" "strings" "syscall" "time" @@ -219,7 +218,11 @@ func (c *containerAdapter) create(ctx context.Context) error { } } - secrets := []*containertypes.ContainerSecret{} + container := c.container.task.Spec.GetContainer() + if container == nil { + return fmt.Errorf("unable to get container from task spec") + } + secrets := make([]*containertypes.ContainerSecret, 0, len(container.Secrets)) for _, s := range c.container.task.Spec.GetContainer().Secrets { sec := c.secrets.Get(s.SecretID) if sec == nil { @@ -233,23 +236,13 @@ func (c *containerAdapter) create(ctx context.Context) error { logrus.Warnf("secret target was not a file: secret=%s", s.SecretID) continue } - // convert uid / gid string to int - uid, err := strconv.Atoi(target.UID) - if err != nil { - return err - } - - gid, err := strconv.Atoi(target.GID) - if err != nil { - return err - } secrets = append(secrets, &containertypes.ContainerSecret{ Name: name, Target: target.Name, Data: sec.Spec.Data, - UID: uid, - GID: gid, + UID: target.UID, + GID: target.GID, Mode: target.Mode, }) } diff --git a/components/engine/daemon/cluster/secrets.go b/components/engine/daemon/cluster/secrets.go index ca795192be..27114152f4 100644 --- a/components/engine/daemon/cluster/secrets.go +++ b/components/engine/daemon/cluster/secrets.go @@ -29,7 +29,7 @@ func (c *Cluster) GetSecrets(options apitypes.SecretListOptions) ([]types.Secret return nil, c.errNoManager() } - filters, err := newListSecretsFilters(options.Filter) + filters, err := newListSecretsFilters(options.Filters) if err != nil { return nil, err } @@ -97,6 +97,7 @@ func (c *Cluster) RemoveSecret(id string) error { } // UpdateSecret updates a secret in a managed swarm cluster. +// Note: this is not exposed to the CLI but is available from the API only func (c *Cluster) UpdateSecret(id string, version uint64, spec types.SecretSpec) error { c.RLock() defer c.RUnlock() diff --git a/components/engine/daemon/container_operations_unix.go b/components/engine/daemon/container_operations_unix.go index ac6b6ad1ad..d1b77134be 100644 --- a/components/engine/daemon/container_operations_unix.go +++ b/components/engine/daemon/container_operations_unix.go @@ -191,7 +191,16 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { return errors.Wrap(err, "error injecting secret") } - if err := os.Chown(fPath, s.UID, s.GID); err != nil { + uid, err := strconv.Atoi(s.UID) + if err != nil { + return err + } + gid, err := strconv.Atoi(s.GID) + if err != nil { + return err + } + + if err := os.Chown(fPath, uid, gid); err != nil { return errors.Wrap(err, "error setting ownership for secret") } } From d31e5eafbec6d5a8bd0b581f225629490d9502ba Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 15:56:05 -0400 Subject: [PATCH 25/37] secrets: support simple syntax --secret foo Signed-off-by: Evan Hazlett Upstream-commit: a257f674ba22d325e7ad59541723c3ee4e9adc27 Component: engine --- .../engine/cli/command/service/opts_test.go | 46 ------------- .../reference/commandline/service_create.md | 17 ++++- .../docker_cli_service_create_test.go | 32 ++++++++- components/engine/opts/secret.go | 14 +++- components/engine/opts/secret_test.go | 67 +++++++++++++++++++ 5 files changed, 125 insertions(+), 51 deletions(-) create mode 100644 components/engine/opts/secret_test.go diff --git a/components/engine/cli/command/service/opts_test.go b/components/engine/cli/command/service/opts_test.go index 3df3a4fd5d..85de3ae88a 100644 --- a/components/engine/cli/command/service/opts_test.go +++ b/components/engine/cli/command/service/opts_test.go @@ -1,13 +1,11 @@ package service import ( - "os" "reflect" "testing" "time" "github.com/docker/docker/api/types/container" - "github.com/docker/docker/opts" "github.com/docker/docker/pkg/testutil/assert" ) @@ -106,47 +104,3 @@ func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) { _, err := opt.toHealthConfig() assert.Error(t, err, "--no-healthcheck conflicts with --health-* options") } - -func TestSecretOptionsSimple(t *testing.T) { - var opt opts.SecretOpt - - testCase := "source=foo,target=testing" - assert.NilError(t, opt.Set(testCase)) - - reqs := opt.Value() - assert.Equal(t, len(reqs), 1) - req := reqs[0] - assert.Equal(t, req.Source, "foo") - assert.Equal(t, req.Target, "testing") -} - -func TestSecretOptionsCustomUidGid(t *testing.T) { - var opt opts.SecretOpt - - testCase := "source=foo,target=testing,uid=1000,gid=1001" - assert.NilError(t, opt.Set(testCase)) - - reqs := opt.Value() - assert.Equal(t, len(reqs), 1) - req := reqs[0] - assert.Equal(t, req.Source, "foo") - assert.Equal(t, req.Target, "testing") - assert.Equal(t, req.UID, "1000") - assert.Equal(t, req.GID, "1001") -} - -func TestSecretOptionsCustomMode(t *testing.T) { - var opt opts.SecretOpt - - testCase := "source=foo,target=testing,uid=1000,gid=1001,mode=0444" - assert.NilError(t, opt.Set(testCase)) - - reqs := opt.Value() - assert.Equal(t, len(reqs), 1) - req := reqs[0] - assert.Equal(t, req.Source, "foo") - assert.Equal(t, req.Target, "testing") - assert.Equal(t, req.UID, "1000") - assert.Equal(t, req.GID, "1001") - assert.Equal(t, req.Mode, os.FileMode(0444)) -} diff --git a/components/engine/docs/reference/commandline/service_create.md b/components/engine/docs/reference/commandline/service_create.md index e98c2cc8ae..8f33bf8c33 100644 --- a/components/engine/docs/reference/commandline/service_create.md +++ b/components/engine/docs/reference/commandline/service_create.md @@ -122,11 +122,22 @@ ID NAME MODE REPLICAS IMAGE ### Create a service with secrets Use the `--secret` flag to give a container access to a -[secret](secret_create.md). The following command will create a service -with two secrets named `ssh-key` and `app-key`: +[secret](secret_create.md). + +Create a service specifying a secret: ```bash -$ 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 +$ docker service create --name redis --secret secret.json redis:3.0.6 +4cdgfyky7ozwh3htjfw0d12qv +``` + +Create a service specifying the secret, target, user/group ID and mode: + +```bash +$ 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 4cdgfyky7ozwh3htjfw0d12qv ``` diff --git a/components/engine/integration-cli/docker_cli_service_create_test.go b/components/engine/integration-cli/docker_cli_service_create_test.go index bac23b0eb1..7af6a5c6de 100644 --- a/components/engine/integration-cli/docker_cli_service_create_test.go +++ b/components/engine/integration-cli/docker_cli_service_create_test.go @@ -45,7 +45,37 @@ func (s *DockerSwarmSuite) TestServiceCreateMountVolume(c *check.C) { c.Assert(mounts[0].RW, checker.Equals, true) } -func (s *DockerSwarmSuite) TestServiceCreateWithSecret(c *check.C) { +func (s *DockerSwarmSuite) TestServiceCreateWithSecretSimple(c *check.C) { + d := s.AddDaemon(c, true, true) + + serviceName := "test-service-secret" + testName := "test_secret" + id := d.createSecret(c, swarm.SecretSpec{ + swarm.Annotations{ + Name: testName, + }, + []byte("TESTINGDATA"), + }) + c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id)) + + out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", testName, "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName) + c.Assert(err, checker.IsNil) + + var refs []swarm.SecretReference + c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil) + c.Assert(refs, checker.HasLen, 1) + + c.Assert(refs[0].SecretName, checker.Equals, testName) + c.Assert(refs[0].Target, checker.Not(checker.IsNil)) + c.Assert(refs[0].Target.Name, checker.Equals, testName) + c.Assert(refs[0].Target.UID, checker.Equals, "0") + c.Assert(refs[0].Target.GID, checker.Equals, "0") +} + +func (s *DockerSwarmSuite) TestServiceCreateWithSecretSourceTarget(c *check.C) { d := s.AddDaemon(c, true, true) serviceName := "test-service-secret" diff --git a/components/engine/opts/secret.go b/components/engine/opts/secret.go index 34ed42a680..9475d9f506 100644 --- a/components/engine/opts/secret.go +++ b/components/engine/opts/secret.go @@ -32,6 +32,14 @@ func (o *SecretOpt) Set(value string) error { Mode: 0444, } + // support a simple syntax of --secret foo + if len(fields) == 1 { + options.Source = fields[0] + options.Target = fields[0] + o.values = append(o.values, options) + return nil + } + for _, field := range fields { parts := strings.SplitN(field, "=", 2) key := strings.ToLower(parts[0]) @@ -62,7 +70,11 @@ func (o *SecretOpt) Set(value string) error { options.Mode = os.FileMode(m) default: - return fmt.Errorf("invalid field in secret request: %s", key) + if len(fields) == 1 && value == "" { + + } else { + return fmt.Errorf("invalid field in secret request: %s", key) + } } } diff --git a/components/engine/opts/secret_test.go b/components/engine/opts/secret_test.go new file mode 100644 index 0000000000..ce4418a0bc --- /dev/null +++ b/components/engine/opts/secret_test.go @@ -0,0 +1,67 @@ +package opts + +import ( + "os" + "testing" + + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestSecretOptionsSimple(t *testing.T) { + var opt SecretOpt + + testCase := "app-secret" + assert.NilError(t, opt.Set(testCase)) + + reqs := opt.Value() + assert.Equal(t, len(reqs), 1) + req := reqs[0] + assert.Equal(t, req.Source, "app-secret") + assert.Equal(t, req.Target, "app-secret") + assert.Equal(t, req.UID, "0") + assert.Equal(t, req.GID, "0") +} + +func TestSecretOptionsSourceTarget(t *testing.T) { + var opt SecretOpt + + testCase := "source=foo,target=testing" + assert.NilError(t, opt.Set(testCase)) + + reqs := opt.Value() + assert.Equal(t, len(reqs), 1) + req := reqs[0] + assert.Equal(t, req.Source, "foo") + assert.Equal(t, req.Target, "testing") +} + +func TestSecretOptionsCustomUidGid(t *testing.T) { + var opt SecretOpt + + testCase := "source=foo,target=testing,uid=1000,gid=1001" + assert.NilError(t, opt.Set(testCase)) + + reqs := opt.Value() + assert.Equal(t, len(reqs), 1) + req := reqs[0] + assert.Equal(t, req.Source, "foo") + assert.Equal(t, req.Target, "testing") + assert.Equal(t, req.UID, "1000") + assert.Equal(t, req.GID, "1001") +} + +func TestSecretOptionsCustomMode(t *testing.T) { + var opt SecretOpt + + testCase := "source=foo,target=testing,uid=1000,gid=1001,mode=0444" + assert.NilError(t, opt.Set(testCase)) + + reqs := opt.Value() + assert.Equal(t, len(reqs), 1) + req := reqs[0] + assert.Equal(t, req.Source, "foo") + assert.Equal(t, req.Target, "testing") + assert.Equal(t, req.UID, "1000") + assert.Equal(t, req.GID, "1001") + assert.Equal(t, req.Mode, os.FileMode(0444)) +} From 8c1af1ea769a7c960b399484a1d20499f548a7a7 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 16:03:01 -0400 Subject: [PATCH 26/37] remove SecretRequestSpec Signed-off-by: Evan Hazlett Upstream-commit: 4d1fba0204af2b8cd5ae119a286eaefc84533945 Component: engine --- components/engine/api/types/swarm/secret.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/components/engine/api/types/swarm/secret.go b/components/engine/api/types/swarm/secret.go index 21a9e7cde2..1f842c32ca 100644 --- a/components/engine/api/types/swarm/secret.go +++ b/components/engine/api/types/swarm/secret.go @@ -28,12 +28,3 @@ type SecretReference struct { SecretName string Target SecretReferenceFileTarget } - -// SecretRequestSpec is a type for requesting secrets -type SecretRequestSpec struct { - Source string - Target string - UID string - GID string - Mode os.FileMode -} From 19e0a8c065e0f8adb2b5b135673340b286526608 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 17:01:54 -0400 Subject: [PATCH 27/37] support labels for secrets upon creation; review updates Signed-off-by: Evan Hazlett Upstream-commit: 583c013a8735031f4e5090bb1699effb64daf950 Component: engine --- components/engine/api/types/swarm/secret.go | 2 +- .../engine/cli/command/secret/create.go | 29 +++++++++----- .../engine/cli/command/service/parse.go | 2 +- .../daemon/cluster/convert/container.go | 2 +- .../daemon/container_operations_unix.go | 8 ++-- .../reference/commandline/secret_create.md | 40 +++++++++++++++++-- .../reference/commandline/secret_inspect.md | 10 ++--- .../docs/reference/commandline/secret_ls.md | 4 +- .../docs/reference/commandline/secret_rm.md | 2 +- 9 files changed, 71 insertions(+), 28 deletions(-) diff --git a/components/engine/api/types/swarm/secret.go b/components/engine/api/types/swarm/secret.go index 1f842c32ca..d82534d0bc 100644 --- a/components/engine/api/types/swarm/secret.go +++ b/components/engine/api/types/swarm/secret.go @@ -26,5 +26,5 @@ type SecretReferenceFileTarget struct { type SecretReference struct { SecretID string SecretName string - Target SecretReferenceFileTarget + Target *SecretReferenceFileTarget } diff --git a/components/engine/cli/command/secret/create.go b/components/engine/cli/command/secret/create.go index 1c0e933f57..9800048341 100644 --- a/components/engine/cli/command/secret/create.go +++ b/components/engine/cli/command/secret/create.go @@ -9,29 +9,37 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/spf13/cobra" ) type createOptions struct { - name string + name string + labels opts.ListOpts } func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{ + createOpts := createOptions{ + labels: opts.NewListOpts(runconfigopts.ValidateEnv), + } + + cmd := &cobra.Command{ Use: "create [name]", Short: "Create a secret using stdin as content", - Args: cli.ExactArgs(1), + Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts := createOptions{ - name: args[0], - } - - return runSecretCreate(dockerCli, opts) + createOpts.name = args[0] + return runSecretCreate(dockerCli, createOpts) }, } + flags := cmd.Flags() + flags.VarP(&createOpts.labels, "label", "l", "Secret labels") + + return cmd } -func runSecretCreate(dockerCli *command.DockerCli, opts createOptions) error { +func runSecretCreate(dockerCli *command.DockerCli, options createOptions) error { client := dockerCli.Client() ctx := context.Background() @@ -42,7 +50,8 @@ func runSecretCreate(dockerCli *command.DockerCli, opts createOptions) error { spec := swarm.SecretSpec{ Annotations: swarm.Annotations{ - Name: opts.name, + Name: options.name, + Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), }, Data: secretData, } diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go index 4728c773c4..0e3a229f4e 100644 --- a/components/engine/cli/command/service/parse.go +++ b/components/engine/cli/command/service/parse.go @@ -19,7 +19,7 @@ func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretReque for _, secret := range requestedSecrets { secretRef := &swarmtypes.SecretReference{ SecretName: secret.Source, - Target: swarmtypes.SecretReferenceFileTarget{ + Target: &swarmtypes.SecretReferenceFileTarget{ Name: secret.Target, UID: secret.UID, GID: secret.GID, diff --git a/components/engine/daemon/cluster/convert/container.go b/components/engine/daemon/cluster/convert/container.go index b5ce27dc61..a1ecabe92a 100644 --- a/components/engine/daemon/cluster/convert/container.go +++ b/components/engine/daemon/cluster/convert/container.go @@ -108,7 +108,7 @@ func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretRef refs = append(refs, &types.SecretReference{ SecretID: s.SecretID, SecretName: s.SecretName, - Target: types.SecretReferenceFileTarget{ + Target: &types.SecretReferenceFileTarget{ Name: target.Name, UID: target.UID, GID: target.GID, diff --git a/components/engine/daemon/container_operations_unix.go b/components/engine/daemon/container_operations_unix.go index d1b77134be..a89ef3056b 100644 --- a/components/engine/daemon/container_operations_unix.go +++ b/components/engine/daemon/container_operations_unix.go @@ -172,13 +172,13 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { } for _, s := range c.Secrets { + targetPath := filepath.Clean(s.Target) // ensure that the target is a filename only; no paths allowed - tDir, tPath := filepath.Split(s.Target) - if tDir != "" { - return fmt.Errorf("error creating secret: secret must not have a path") + if targetPath != filepath.Base(targetPath) { + return fmt.Errorf("error creating secret: secret must not be a path") } - fPath := filepath.Join(localMountPath, tPath) + fPath := filepath.Join(localMountPath, targetPath) if err := os.MkdirAll(filepath.Dir(fPath), 0700); err != nil { return errors.Wrap(err, "error creating secret mount path") } diff --git a/components/engine/docs/reference/commandline/secret_create.md b/components/engine/docs/reference/commandline/secret_create.md index 92cee6cde7..6a124d5383 100644 --- a/components/engine/docs/reference/commandline/secret_create.md +++ b/components/engine/docs/reference/commandline/secret_create.md @@ -19,6 +19,9 @@ keywords: ["secret, create"] Usage: docker secret create [NAME] Create a secret using stdin as content +Options: + --help Print usage + -l, --label list Secret labels (default []) ``` Creates a secret using standard input for the secret content. You must run this @@ -29,14 +32,45 @@ command on a manager node. ### Create a secret ```bash -$ cat ssh-dev | docker secret create ssh-dev +$ cat secret.json | docker secret create secret.json mhv17xfe3gh6xc4rij5orpfds $ docker secret ls -ID NAME CREATED UPDATED SIZE -mhv17xfe3gh6xc4rij5orpfds ssh-dev 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679 +ID NAME CREATED UPDATED SIZE +mhv17xfe3gh6xc4rij5orpfds secret.json 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679 ``` +### Create a secret with labels + +```bash +$ cat secret.json | docker secret create secret.json --label env=dev --label rev=20161102 +jtn7g6aukl5ky7nr9gvwafoxh + +$ docker secret inspect secret.json +[ + { + "ID": "jtn7g6aukl5ky7nr9gvwafoxh", + "Version": { + "Index": 541 + }, + "CreatedAt": "2016-11-03T20:54:12.924766548Z", + "UpdatedAt": "2016-11-03T20:54:12.924766548Z", + "Spec": { + "Name": "secret.json", + "Labels": { + "env": "dev", + "rev": "20161102" + }, + "Data": null + }, + "Digest": "sha256:4212a44b14e94154359569333d3fc6a80f6b9959dfdaff26412f4b2796b1f387", + "SecretSize": 1679 + } +] + +``` + + ## Related information * [secret inspect](secret_inspect.md) diff --git a/components/engine/docs/reference/commandline/secret_inspect.md b/components/engine/docs/reference/commandline/secret_inspect.md index 0b75bfe385..0d427464af 100644 --- a/components/engine/docs/reference/commandline/secret_inspect.md +++ b/components/engine/docs/reference/commandline/secret_inspect.md @@ -37,7 +37,7 @@ describes all the details of the format. ## Examples -### Inspecting a secret by name or ID +### Inspecting a secret by name or ID You can inspect a secret, either by its *name*, or *ID* @@ -45,12 +45,12 @@ For example, given the following secret: ```bash $ docker secret ls -ID NAME CREATED UPDATED SIZE -mhv17xfe3gh6xc4rij5orpfds ssh-dev 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679 +ID NAME CREATED UPDATED SIZE +mhv17xfe3gh6xc4rij5orpfds secret.json 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679 ``` ```bash -$ docker secret inspect mhv17xfe3gh6xc4rij5orpfds +$ docker secret inspect secret.json [ { "ID": "mhv17xfe3gh6xc4rij5orpfds", @@ -60,7 +60,7 @@ $ docker secret inspect mhv17xfe3gh6xc4rij5orpfds "CreatedAt": "2016-10-27T23:25:43.909181089Z", "UpdatedAt": "2016-10-27T23:25:43.909181089Z", "Spec": { - "Name": "ssh-dev", + "Name": "secret.json", "Data": null }, "Digest": "sha256:8281c6d924520986e3c6af23ed8926710a611c90339db582c2a9ac480ba622b7", diff --git a/components/engine/docs/reference/commandline/secret_ls.md b/components/engine/docs/reference/commandline/secret_ls.md index aa1f31d615..fa78e66427 100644 --- a/components/engine/docs/reference/commandline/secret_ls.md +++ b/components/engine/docs/reference/commandline/secret_ls.md @@ -33,8 +33,8 @@ On a manager node: ```bash $ docker secret ls -ID NAME CREATED UPDATED SIZE -mhv17xfe3gh6xc4rij5orpfds ssh-dev 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679 +ID NAME CREATED UPDATED SIZE +mhv17xfe3gh6xc4rij5orpfds secret.json 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679 ``` ## Related information diff --git a/components/engine/docs/reference/commandline/secret_rm.md b/components/engine/docs/reference/commandline/secret_rm.md index 86f2df9a5a..f504b1ba4f 100644 --- a/components/engine/docs/reference/commandline/secret_rm.md +++ b/components/engine/docs/reference/commandline/secret_rm.md @@ -33,7 +33,7 @@ targeting a manager node. This example removes a secret: ```bash -$ docker secret rm sapth4csdo5b6wz2p5uimh5xg +$ docker secret rm secret.json sapth4csdo5b6wz2p5uimh5xg ``` From f6d13237263d56f2093282392a8d4e1b68fb984c Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 17:28:56 -0400 Subject: [PATCH 28/37] add integration tests for secret create with labels Signed-off-by: Evan Hazlett Upstream-commit: e077f701dbd0cda8eaf4c96628e145c2cf450995 Component: engine --- .../docker_cli_secret_create_test.go | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 components/engine/integration-cli/docker_cli_secret_create_test.go diff --git a/components/engine/integration-cli/docker_cli_secret_create_test.go b/components/engine/integration-cli/docker_cli_secret_create_test.go new file mode 100644 index 0000000000..3126a0db14 --- /dev/null +++ b/components/engine/integration-cli/docker_cli_secret_create_test.go @@ -0,0 +1,48 @@ +// +build !windows + +package main + +import ( + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSwarmSuite) TestSecretCreate(c *check.C) { + d := s.AddDaemon(c, true, true) + + testName := "test_secret" + id := d.createSecret(c, swarm.SecretSpec{ + swarm.Annotations{ + Name: testName, + }, + []byte("TESTINGDATA"), + }) + c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id)) + + secret := d.getSecret(c, id) + c.Assert(secret.Spec.Name, checker.Equals, testName) +} + +func (s *DockerSwarmSuite) TestSecretCreateWithLabels(c *check.C) { + d := s.AddDaemon(c, true, true) + + testName := "test_secret" + id := d.createSecret(c, swarm.SecretSpec{ + swarm.Annotations{ + Name: testName, + Labels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + []byte("TESTINGDATA"), + }) + c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id)) + + secret := d.getSecret(c, id) + c.Assert(secret.Spec.Name, checker.Equals, testName) + c.Assert(len(secret.Spec.Labels), checker.Equals, 2) + c.Assert(secret.Spec.Labels["key1"], checker.Equals, "value1") + c.Assert(secret.Spec.Labels["key2"], checker.Equals, "value2") +} From 5907fa959f26da5ba520dc4b4c45601ddf8d57d6 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 22:42:30 -0400 Subject: [PATCH 29/37] more review updates Signed-off-by: Evan Hazlett Upstream-commit: eff36b7d669a8d0dbb9d79b70f9cb1f8a6ffdbd8 Component: engine --- components/engine/api/types/swarm/secret.go | 3 +++ components/engine/daemon/cluster/executor/container/adapter.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/components/engine/api/types/swarm/secret.go b/components/engine/api/types/swarm/secret.go index d82534d0bc..72e93c9d26 100644 --- a/components/engine/api/types/swarm/secret.go +++ b/components/engine/api/types/swarm/secret.go @@ -11,11 +11,13 @@ type Secret struct { SecretSize int64 } +// SecretSpec represents a secret specification from a secret in swarm type SecretSpec struct { Annotations Data []byte } +// SecretReferenceFileTarget is a file target in a secret reference type SecretReferenceFileTarget struct { Name string UID string @@ -23,6 +25,7 @@ type SecretReferenceFileTarget struct { Mode os.FileMode } +// SecretReference is a reference to a secret in swarm type SecretReference struct { SecretID string SecretName string diff --git a/components/engine/daemon/cluster/executor/container/adapter.go b/components/engine/daemon/cluster/executor/container/adapter.go index 87ddaac455..98ea358100 100644 --- a/components/engine/daemon/cluster/executor/container/adapter.go +++ b/components/engine/daemon/cluster/executor/container/adapter.go @@ -223,7 +223,7 @@ func (c *containerAdapter) create(ctx context.Context) error { return fmt.Errorf("unable to get container from task spec") } secrets := make([]*containertypes.ContainerSecret, 0, len(container.Secrets)) - for _, s := range c.container.task.Spec.GetContainer().Secrets { + for _, s := range container.Secrets { sec := c.secrets.Get(s.SecretID) if sec == nil { logrus.Warnf("unable to get secret %s from provider", s.SecretID) From da9150e0d2506424d0b68cb73c82198c1211447c Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 22:43:52 -0400 Subject: [PATCH 30/37] review updates Signed-off-by: Evan Hazlett Upstream-commit: e9fd1c5918c1fc49c9c3a024d0ef52b57f73e9f0 Component: engine --- components/engine/api/types/container/secret.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/engine/api/types/container/secret.go b/components/engine/api/types/container/secret.go index 13a50d352c..a9bbd2f041 100644 --- a/components/engine/api/types/container/secret.go +++ b/components/engine/api/types/container/secret.go @@ -2,6 +2,8 @@ package container import "os" +// ContainerSecret represents a secret in a container. This gets realized +// in the container tmpfs type ContainerSecret struct { Name string Target string From 5cd25dda5d914705a73f028e520ec0d3d9dc4ede Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Fri, 4 Nov 2016 14:24:44 -0400 Subject: [PATCH 31/37] SecretRequestOptions -> SecretRequestOption Signed-off-by: Evan Hazlett Upstream-commit: 5b2230a38b357a3e2725fc56be9c07c01fd1203e Component: engine --- components/engine/api/types/client.go | 4 ++-- components/engine/cli/command/service/parse.go | 2 +- components/engine/opts/secret.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/engine/api/types/client.go b/components/engine/api/types/client.go index d79ebd9e55..b6a313ac66 100644 --- a/components/engine/api/types/client.go +++ b/components/engine/api/types/client.go @@ -341,8 +341,8 @@ type PluginInstallOptions struct { Args []string } -// SecretRequestOptions is a type for requesting secrets -type SecretRequestOptions struct { +// SecretRequestOption is a type for requesting secrets +type SecretRequestOption struct { Source string Target string UID string diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go index 0e3a229f4e..368bc6d449 100644 --- a/components/engine/cli/command/service/parse.go +++ b/components/engine/cli/command/service/parse.go @@ -12,7 +12,7 @@ import ( // parseSecrets retrieves the secrets from the requested names and converts // them to secret references to use with the spec -func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretRequestOptions) ([]*swarmtypes.SecretReference, error) { +func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) { secretRefs := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() diff --git a/components/engine/opts/secret.go b/components/engine/opts/secret.go index 9475d9f506..b77a33f685 100644 --- a/components/engine/opts/secret.go +++ b/components/engine/opts/secret.go @@ -13,7 +13,7 @@ import ( // SecretOpt is a Value type for parsing secrets type SecretOpt struct { - values []*types.SecretRequestOptions + values []*types.SecretRequestOption } // Set a new secret value @@ -24,7 +24,7 @@ func (o *SecretOpt) Set(value string) error { return err } - options := &types.SecretRequestOptions{ + options := &types.SecretRequestOption{ Source: "", Target: "", UID: "0", @@ -102,6 +102,6 @@ func (o *SecretOpt) String() string { } // Value returns the secret requests -func (o *SecretOpt) Value() []*types.SecretRequestOptions { +func (o *SecretOpt) Value() []*types.SecretRequestOption { return o.values } From d5d838f9f351b27c867966a71d16ba759632a099 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Mon, 7 Nov 2016 16:18:53 -0500 Subject: [PATCH 32/37] embed spec when converting from grpc Signed-off-by: Evan Hazlett Upstream-commit: a9d41184296c971c650f7d97a67ae5c9b44d4200 Component: engine --- .../engine/daemon/cluster/convert/secret.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/components/engine/daemon/cluster/convert/secret.go b/components/engine/daemon/cluster/convert/secret.go index acfc863efa..f8cb4ff80c 100644 --- a/components/engine/daemon/cluster/convert/secret.go +++ b/components/engine/daemon/cluster/convert/secret.go @@ -12,21 +12,20 @@ func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret { ID: s.ID, Digest: s.Digest, SecretSize: s.SecretSize, + Spec: swarmtypes.SecretSpec{ + Annotations: swarmtypes.Annotations{ + Name: s.Spec.Annotations.Name, + Labels: s.Spec.Annotations.Labels, + }, + Data: s.Spec.Data, + }, } - // Meta secret.Version.Index = s.Meta.Version.Index + // Meta secret.CreatedAt, _ = ptypes.Timestamp(s.Meta.CreatedAt) secret.UpdatedAt, _ = ptypes.Timestamp(s.Meta.UpdatedAt) - secret.Spec = swarmtypes.SecretSpec{ - Annotations: swarmtypes.Annotations{ - Name: s.Spec.Annotations.Name, - Labels: s.Spec.Annotations.Labels, - }, - Data: s.Spec.Data, - } - return secret } From bd20a72add105b22b51030b6454f973c5d4a2263 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 8 Nov 2016 11:34:45 -0500 Subject: [PATCH 33/37] more review updates - return err instead of wrap for update secret - add omitempty for data in secret spec Signed-off-by: Evan Hazlett Upstream-commit: 427c70d97779566b2b3ea3a9141bb9bf3dd05dd8 Component: engine --- components/engine/api/server/router/swarm/cluster_routes.go | 2 +- components/engine/api/types/swarm/secret.go | 2 +- components/engine/cli/command/service/opts.go | 1 - components/engine/cli/command/service/opts_test.go | 1 + components/engine/daemon/container_operations_unix.go | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/engine/api/server/router/swarm/cluster_routes.go b/components/engine/api/server/router/swarm/cluster_routes.go index 3c98e3ee26..0e3cae39cd 100644 --- a/components/engine/api/server/router/swarm/cluster_routes.go +++ b/components/engine/api/server/router/swarm/cluster_routes.go @@ -327,7 +327,7 @@ func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter, id := vars["id"] if err := sr.backend.UpdateSecret(id, version, secret); err != nil { - return errors.NewErrorWithStatusCode(err, http.StatusInternalServerError) + return err } return nil diff --git a/components/engine/api/types/swarm/secret.go b/components/engine/api/types/swarm/secret.go index 72e93c9d26..dbed63af8e 100644 --- a/components/engine/api/types/swarm/secret.go +++ b/components/engine/api/types/swarm/secret.go @@ -14,7 +14,7 @@ type Secret struct { // SecretSpec represents a secret specification from a secret in swarm type SecretSpec struct { Annotations - Data []byte + Data []byte `json:",omitempty"` } // SecretReferenceFileTarget is a file target in a secret reference diff --git a/components/engine/cli/command/service/opts.go b/components/engine/cli/command/service/opts.go index 45adb37672..b81998ec09 100644 --- a/components/engine/cli/command/service/opts.go +++ b/components/engine/cli/command/service/opts.go @@ -585,7 +585,6 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") - flags.StringSliceVar(&opts.secrets, flagSecret, []string{}, "Specify secrets to expose to the service") } const ( diff --git a/components/engine/cli/command/service/opts_test.go b/components/engine/cli/command/service/opts_test.go index 85de3ae88a..aa2d999dcf 100644 --- a/components/engine/cli/command/service/opts_test.go +++ b/components/engine/cli/command/service/opts_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/opts" "github.com/docker/docker/pkg/testutil/assert" ) diff --git a/components/engine/daemon/container_operations_unix.go b/components/engine/daemon/container_operations_unix.go index a89ef3056b..8fbc7bca6f 100644 --- a/components/engine/daemon/container_operations_unix.go +++ b/components/engine/daemon/container_operations_unix.go @@ -18,9 +18,9 @@ import ( "github.com/docker/docker/container" "github.com/docker/docker/daemon/links" "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/runconfig" - "github.com/docker/engine-api/types/mount" "github.com/docker/libnetwork" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/devices" From f16e4b629515ee4b7142f66a43d1b9381965ccbc Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 8 Nov 2016 20:50:48 -0500 Subject: [PATCH 34/37] add api docs for secrets Signed-off-by: Evan Hazlett Upstream-commit: 0bcb65ccbae17bf9370191bb295548d232569b93 Component: engine --- .../reference/api/docker_remote_api_v1.25.md | 130 +++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/components/engine/docs/reference/api/docker_remote_api_v1.25.md b/components/engine/docs/reference/api/docker_remote_api_v1.25.md index e84a38b266..0da2c7aee3 100644 --- a/components/engine/docs/reference/api/docker_remote_api_v1.25.md +++ b/components/engine/docs/reference/api/docker_remote_api_v1.25.md @@ -240,7 +240,7 @@ List containers - `volume`=(`` or ``) - `network`=(`` or ``) - `health`=(`starting`|`healthy`|`unhealthy`|`none`) - + **Status codes**: - **200** – no error @@ -5842,6 +5842,134 @@ Get details on a task - **404** – unknown task - **500** – server error +## 3.11 Secrets + +**Note**: Secret operations require the engine to be part of a swarm. + +### List secrets + +`GET /secrets` + +List secrets + +**Example request**: + + GET /secrets HTTP/1.1 + +**Example response**: + + [ + { + "ID": "ktnbjxoalbkvbvedmg1urrz8h", + "Version": { + "Index": 11 + }, + "CreatedAt": "2016-11-05T01:20:17.327670065Z", + "UpdatedAt": "2016-11-05T01:20:17.327670065Z", + "Spec": { + "Name": "app-dev.crt" + }, + "Digest": "sha256:11d7c6f38253b73e608153c9f662a191ae605e1a3d9b756b0b3426388f91d3fa", + "SecretSize": 31 + } + ] + + +**Query parameters**: + +- **filters** - a JSON encoded value of the filters (a `map[string][]string`) to process on the secrets list. Available filters: + - `names=` + +**Status codes**: + +- **200** – no error + +### Create a secret + +`POST /secrets/create` + +Create a secret + +**Example request**: + + POST /secrets/create HTTP/1.1 + Content-Type: application/json + + { + "Name": "app-key.crt", + "Labels": { + "foo": "bar" + }, + "Data": "VEhJUyBJUyBOT1QgQSBSRUFMIENFUlRJRklDQVRFCg==" + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "ID": "ktnbjxoalbkvbvedmg1urrz8h" + } + +**Status codes**: + +- **201** – no error +- **406** – server error or node is not part of a swarm +- **409** – name conflicts with an existing object + +**JSON Parameters**: + +- **Name** – User-defined name for the secret. +- **Labels** – A map of labels to associate with the secret (e.g., `{"key":"value", "key2":"value2"}`). +- **Data** – Base64-url-safe-encoded secret data + +### Inspect a secret + +`GET /secrets/(secret id)` + +Get details on a secret + +**Example request**: + + GET /secrets/ktnbjxoalbkvbvedmg1urrz8h HTTP/1.1 + +**Example response**: + + { + "ID": "ktnbjxoalbkvbvedmg1urrz8h", + "Version": { + "Index": 11 + }, + "CreatedAt": "2016-11-05T01:20:17.327670065Z", + "UpdatedAt": "2016-11-05T01:20:17.327670065Z", + "Spec": { + "Name": "app-dev.crt" + }, + "Digest": "sha256:11d7c6f38253b73e608153c9f662a191ae605e1a3d9b756b0b3426388f91d3fa", + "SecretSize": 31 + } + +**Status codes**: + +- **200** – no error +- **404** – unknown secret +- **500** – server error + +### Remove a secret + +`DELETE /secrets/(id)` + +Remove the secret `id` from the secret store + +**Example request**: + + DELETE /secrets/ktnbjxoalbkvbvedmg1urrz8h HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + # 4. Going further ## 4.1 Inside `docker run` From 807eb50cc65104ab6d5b0e0c90be19efc4e02d5e Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 8 Nov 2016 22:40:46 -0500 Subject: [PATCH 35/37] use human readable units when listing secrets Signed-off-by: Evan Hazlett Upstream-commit: 880e8be1eea086ee83e5ad22f953696365c5b799 Component: engine --- components/engine/cli/command/secret/ls.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/engine/cli/command/secret/ls.go b/components/engine/cli/command/secret/ls.go index 1befdad9d0..67fc1daff6 100644 --- a/components/engine/cli/command/secret/ls.go +++ b/components/engine/cli/command/secret/ls.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "text/tabwriter" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/go-units" "github.com/spf13/cobra" ) @@ -52,7 +54,11 @@ func runSecretList(dockerCli *command.DockerCli, opts listOptions) error { fmt.Fprintf(w, "\n") for _, s := range secrets { - 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) + created := units.HumanDuration(time.Now().UTC().Sub(s.Meta.CreatedAt)) + " ago" + updated := units.HumanDuration(time.Now().UTC().Sub(s.Meta.UpdatedAt)) + " ago" + size := units.HumanSizeWithPrecision(float64(s.SecretSize), 3) + + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", s.ID, s.Spec.Annotations.Name, created, updated, size) } } From bd2a95acd1bad9c739d752e9ae29ffb25ced7727 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 8 Nov 2016 22:58:23 -0500 Subject: [PATCH 36/37] fix lint issue from rebase Signed-off-by: Evan Hazlett Upstream-commit: 83af60e6237b4227126dad1ef2f8a2016984ee90 Component: engine --- components/engine/api/types/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/engine/api/types/types.go b/components/engine/api/types/types.go index ceb18ddd89..a185baf9dc 100644 --- a/components/engine/api/types/types.go +++ b/components/engine/api/types/types.go @@ -129,7 +129,7 @@ type ContainerProcessList struct { Titles []string } -// Info contains response of Remote API: +// Ping contains response of Remote API: // GET "/_ping" type Ping struct { APIVersion string From 6941a29c56931f17d665900a03184c61d3e62448 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Wed, 9 Nov 2016 16:52:38 +0100 Subject: [PATCH 37/37] Correct secrets permissions when userns enabled Docker-DCO-1.1-Signed-off-by: Phil Estes Upstream-commit: 8119809b68bee9a027bae9d5851b11c743438bd6 Component: engine --- .../engine/daemon/container_operations_unix.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/components/engine/daemon/container_operations_unix.go b/components/engine/daemon/container_operations_unix.go index 8fbc7bca6f..5e1da60414 100644 --- a/components/engine/daemon/container_operations_unix.go +++ b/components/engine/daemon/container_operations_unix.go @@ -163,11 +163,14 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { } }() + // retrieve possible remapped range start for root UID, GID + rootUID, rootGID := daemon.GetRemappedUIDGID() // create tmpfs - if err := os.MkdirAll(localMountPath, 0700); err != nil { + if err := idtools.MkdirAllAs(localMountPath, 0700, rootUID, rootGID); err != nil { return errors.Wrap(err, "error creating secret local mount path") } - if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev,nosuid,noexec"); err != nil { + tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootUID, rootGID) + if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev,nosuid,noexec,"+tmpfsOwnership); err != nil { return errors.Wrap(err, "unable to setup secret mount") } @@ -179,7 +182,7 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { } fPath := filepath.Join(localMountPath, targetPath) - if err := os.MkdirAll(filepath.Dir(fPath), 0700); err != nil { + if err := idtools.MkdirAllAs(filepath.Dir(fPath), 0700, rootUID, rootGID); err != nil { return errors.Wrap(err, "error creating secret mount path") } @@ -200,13 +203,13 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { return err } - if err := os.Chown(fPath, uid, gid); err != nil { + if err := os.Chown(fPath, rootUID+uid, rootGID+gid); err != nil { return errors.Wrap(err, "error setting ownership for secret") } } // remount secrets ro - if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro"); err != nil { + if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro,"+tmpfsOwnership); err != nil { return errors.Wrap(err, "unable to remount secret dir as readonly") }