From 3716ec25b423d8ff7dfa231a7b3cf0154726ed37 Mon Sep 17 00:00:00 2001 From: Evan Hazlett <ejhazlett@gmail.com> Date: Wed, 19 Oct 2016 12:22:02 -0400 Subject: [PATCH] secrets: secret management for swarm Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> wip: use tmpfs for swarm secrets Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> wip: inject secrets from swarm secret store Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> secrets: use secret names in cli for service create Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> switch to use mounts instead of volumes Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> vendor: use ehazlett swarmkit Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> secrets: finish secret update Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> --- api/server/router/swarm/backend.go | 5 + api/server/router/swarm/cluster.go | 5 + api/server/router/swarm/cluster_routes.go | 74 ++++++++++ api/types/container/secret.go | 12 ++ api/types/swarm/container.go | 1 + api/types/swarm/secret.go | 30 ++++ api/types/types.go | 13 ++ cli/command/commands/commands.go | 2 + cli/command/secret/cmd.go | 29 ++++ cli/command/secret/create.go | 57 ++++++++ cli/command/secret/inspect.go | 42 ++++++ cli/command/secret/ls.go | 62 +++++++++ cli/command/secret/remove.go | 43 ++++++ cli/command/service/create.go | 7 + cli/command/service/opts.go | 17 +++ cli/command/service/parse.go | 92 ++++++++++++ client/errors.go | 22 +++ client/interface.go | 9 ++ client/secret_create.go | 24 ++++ client/secret_create_test.go | 57 ++++++++ client/secret_inspect.go | 34 +++++ client/secret_inspect_test.go | 65 +++++++++ client/secret_list.go | 35 +++++ client/secret_list_test.go | 94 +++++++++++++ client/secret_remove.go | 10 ++ client/secret_remove_test.go | 47 +++++++ container/container.go | 5 +- container/container_unix.go | 29 +++- daemon/cluster/convert/container.go | 43 ++++++ daemon/cluster/convert/secret.go | 46 ++++++ daemon/cluster/executor/backend.go | 1 + daemon/cluster/executor/container/adapter.go | 34 ++++- .../cluster/executor/container/attachment.go | 5 +- .../cluster/executor/container/controller.go | 4 +- daemon/cluster/executor/container/executor.go | 10 +- .../cluster/executor/container/health_test.go | 2 +- .../executor/container/validate_test.go | 2 +- daemon/cluster/filters.go | 18 +++ daemon/cluster/secrets.go | 131 ++++++++++++++++++ daemon/container_operations_unix.go | 39 ++++++ daemon/daemon.go | 1 + daemon/oci_linux.go | 7 + daemon/secrets.go | 22 +++ daemon/secrets_linux.go | 7 + daemon/secrets_unsupported.go | 7 + daemon/start.go | 4 + 46 files changed, 1292 insertions(+), 13 deletions(-) create mode 100644 api/types/container/secret.go create mode 100644 api/types/swarm/secret.go create mode 100644 cli/command/secret/cmd.go create mode 100644 cli/command/secret/create.go create mode 100644 cli/command/secret/inspect.go create mode 100644 cli/command/secret/ls.go create mode 100644 cli/command/secret/remove.go create mode 100644 cli/command/service/parse.go create mode 100644 client/secret_create.go create mode 100644 client/secret_create_test.go create mode 100644 client/secret_inspect.go create mode 100644 client/secret_inspect_test.go create mode 100644 client/secret_list.go create mode 100644 client/secret_list_test.go create mode 100644 client/secret_remove.go create mode 100644 client/secret_remove_test.go create mode 100644 daemon/cluster/convert/secret.go create mode 100644 daemon/cluster/secrets.go create mode 100644 daemon/secrets.go create mode 100644 daemon/secrets_linux.go create mode 100644 daemon/secrets_unsupported.go diff --git a/api/server/router/swarm/backend.go b/api/server/router/swarm/backend.go index a7cc9eef40..1ab09a8f8b 100644 --- a/api/server/router/swarm/backend.go +++ b/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/api/server/router/swarm/cluster.go b/api/server/router/swarm/cluster.go index a67ffa9632..ec92e7aaa0 100644 --- a/api/server/router/swarm/cluster.go +++ b/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/api/server/router/swarm/cluster_routes.go b/api/server/router/swarm/cluster_routes.go index bc78601692..fc33d27746 100644 --- a/api/server/router/swarm/cluster_routes.go +++ b/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/api/types/container/secret.go b/api/types/container/secret.go new file mode 100644 index 0000000000..eee5bf89d2 --- /dev/null +++ b/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/api/types/swarm/container.go b/api/types/swarm/container.go index e1ab81c5f9..39f1d3987c 100644 --- a/api/types/swarm/container.go +++ b/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/api/types/swarm/secret.go b/api/types/swarm/secret.go new file mode 100644 index 0000000000..3323ba212d --- /dev/null +++ b/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/api/types/types.go b/api/types/types.go index 5591646b69..fe60755c84 100644 --- a/api/types/types.go +++ b/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/cli/command/commands/commands.go b/cli/command/commands/commands.go index fad709bca1..d64d5680cc 100644 --- a/cli/command/commands/commands.go +++ b/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/cli/command/secret/cmd.go b/cli/command/secret/cmd.go new file mode 100644 index 0000000000..995300ad77 --- /dev/null +++ b/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/cli/command/secret/create.go b/cli/command/secret/create.go new file mode 100644 index 0000000000..1c0e933f57 --- /dev/null +++ b/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/cli/command/secret/inspect.go b/cli/command/secret/inspect.go new file mode 100644 index 0000000000..c8d5cd8f79 --- /dev/null +++ b/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/cli/command/secret/ls.go b/cli/command/secret/ls.go new file mode 100644 index 0000000000..1befdad9d0 --- /dev/null +++ b/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/cli/command/secret/remove.go b/cli/command/secret/remove.go new file mode 100644 index 0000000000..f336c6161a --- /dev/null +++ b/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/cli/command/service/create.go b/cli/command/service/create.go index d6c3ebdb9c..8fb9070e67 100644 --- a/cli/command/service/create.go +++ b/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/cli/command/service/opts.go b/cli/command/service/opts.go index 827c4e5cdc..a4fd08881c 100644 --- a/cli/command/service/opts.go +++ b/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/cli/command/service/parse.go b/cli/command/service/parse.go new file mode 100644 index 0000000000..41883fb445 --- /dev/null +++ b/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/client/errors.go b/client/errors.go index 53e2065332..db7294daa8 100644 --- a/client/errors.go +++ b/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/client/interface.go b/client/interface.go index 99b06709b5..49b66b1d17 100644 --- a/client/interface.go +++ b/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/client/secret_create.go b/client/secret_create.go new file mode 100644 index 0000000000..de8b041567 --- /dev/null +++ b/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/client/secret_create_test.go b/client/secret_create_test.go new file mode 100644 index 0000000000..d264eb6692 --- /dev/null +++ b/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/client/secret_inspect.go b/client/secret_inspect.go new file mode 100644 index 0000000000..f774576118 --- /dev/null +++ b/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/client/secret_inspect_test.go b/client/secret_inspect_test.go new file mode 100644 index 0000000000..423d986968 --- /dev/null +++ b/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/client/secret_list.go b/client/secret_list.go new file mode 100644 index 0000000000..5e9d2b5098 --- /dev/null +++ b/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/client/secret_list_test.go b/client/secret_list_test.go new file mode 100644 index 0000000000..174963c7ee --- /dev/null +++ b/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/client/secret_remove.go b/client/secret_remove.go new file mode 100644 index 0000000000..1955b988a9 --- /dev/null +++ b/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/client/secret_remove_test.go b/client/secret_remove_test.go new file mode 100644 index 0000000000..f269f787d2 --- /dev/null +++ b/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/container/container.go b/container/container.go index 722271be96..74d080d46c 100644 --- a/container/container.go +++ b/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/container/container_unix.go b/container/container_unix.go index c38f750667..099073b83e 100644 --- a/container/container_unix.go +++ b/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/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go index 38749dd8b2..38143c6861 100644 --- a/daemon/cluster/convert/container.go +++ b/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/daemon/cluster/convert/secret.go b/daemon/cluster/convert/secret.go new file mode 100644 index 0000000000..e26fe84ce4 --- /dev/null +++ b/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/daemon/cluster/executor/backend.go b/daemon/cluster/executor/backend.go index fb88613c1d..76b9d8888f 100644 --- a/daemon/cluster/executor/backend.go +++ b/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/daemon/cluster/executor/container/adapter.go b/daemon/cluster/executor/container/adapter.go index 618f4b22b4..40a08401f4 100644 --- a/daemon/cluster/executor/container/adapter.go +++ b/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/daemon/cluster/executor/container/attachment.go b/daemon/cluster/executor/container/attachment.go index f3b738f70b..a897268b26 100644 --- a/daemon/cluster/executor/container/attachment.go +++ b/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/daemon/cluster/executor/container/controller.go b/daemon/cluster/executor/container/controller.go index 0185e415b5..d3d80075a4 100644 --- a/daemon/cluster/executor/container/controller.go +++ b/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/daemon/cluster/executor/container/executor.go b/daemon/cluster/executor/container/executor.go index 844821b83e..fdd270006d 100644 --- a/daemon/cluster/executor/container/executor.go +++ b/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/daemon/cluster/executor/container/health_test.go b/daemon/cluster/executor/container/health_test.go index 16a1e0c096..99cf7502af 100644 --- a/daemon/cluster/executor/container/health_test.go +++ b/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/daemon/cluster/executor/container/validate_test.go b/daemon/cluster/executor/container/validate_test.go index d911c1ebec..5f202d5859 100644 --- a/daemon/cluster/executor/container/validate_test.go +++ b/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/daemon/cluster/filters.go b/daemon/cluster/filters.go index a1d800e56c..88668edaac 100644 --- a/daemon/cluster/filters.go +++ b/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/daemon/cluster/secrets.go b/daemon/cluster/secrets.go new file mode 100644 index 0000000000..305f2afab5 --- /dev/null +++ b/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/daemon/container_operations_unix.go b/daemon/container_operations_unix.go index 66b11bb288..a3031ba3e6 100644 --- a/daemon/container_operations_unix.go +++ b/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/daemon/daemon.go b/daemon/daemon.go index 8bbc8f42d4..c3efbac2bd 100644 --- a/daemon/daemon.go +++ b/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/daemon/oci_linux.go b/daemon/oci_linux.go index 5cac76843d..d74ddd053b 100644 --- a/daemon/oci_linux.go +++ b/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/daemon/secrets.go b/daemon/secrets.go new file mode 100644 index 0000000000..342b33c2a3 --- /dev/null +++ b/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/daemon/secrets_linux.go b/daemon/secrets_linux.go new file mode 100644 index 0000000000..fca4e12598 --- /dev/null +++ b/daemon/secrets_linux.go @@ -0,0 +1,7 @@ +// +build linux + +package daemon + +func secretsSupported() bool { + return true +} diff --git a/daemon/secrets_unsupported.go b/daemon/secrets_unsupported.go new file mode 100644 index 0000000000..d6f36fda1e --- /dev/null +++ b/daemon/secrets_unsupported.go @@ -0,0 +1,7 @@ +// +build !linux + +package daemon + +func secretsSupported() bool { + return false +} diff --git a/daemon/start.go b/daemon/start.go index c642ce22a8..af08ccdf39 100644 --- a/daemon/start.go +++ b/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) }