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..b6222c7498 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", 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..0e3cae39cd 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" @@ -261,3 +262,73 @@ 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 + } + filters, err := filters.FromParam(r.Form.Get("filters")) + if err != nil { + return err + } + + secrets, err := sr.backend.GetSecrets(basictypes.SecretListOptions{Filters: filters}) + if err != nil { + 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 { + 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 { + 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 { + 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 errors.NewBadRequestError(err) + } + + rawVersion := r.URL.Query().Get("version") + version, err := strconv.ParseUint(rawVersion, 10, 64) + if err != nil { + return errors.NewBadRequestError(fmt.Errorf("invalid secret version")) + } + + id := vars["id"] + if err := sr.backend.UpdateSecret(id, version, secret); err != nil { + return err + } + + return nil +} diff --git a/components/engine/api/types/client.go b/components/engine/api/types/client.go index 769c836446..b6a313ac66 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 } + +// SecretRequestOption is a type for requesting secrets +type SecretRequestOption struct { + Source string + Target string + UID string + GID string + Mode os.FileMode +} diff --git a/components/engine/api/types/container/secret.go b/components/engine/api/types/container/secret.go new file mode 100644 index 0000000000..a9bbd2f041 --- /dev/null +++ b/components/engine/api/types/container/secret.go @@ -0,0 +1,14 @@ +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 + Data []byte + UID string + GID string + 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..dbed63af8e --- /dev/null +++ b/components/engine/api/types/swarm/secret.go @@ -0,0 +1,33 @@ +package swarm + +import "os" + +// Secret represents a secret. +type Secret struct { + ID string + Meta + Spec SecretSpec + Digest string + SecretSize int64 +} + +// SecretSpec represents a secret specification from a secret in swarm +type SecretSpec struct { + Annotations + Data []byte `json:",omitempty"` +} + +// SecretReferenceFileTarget is a file target in a secret reference +type SecretReferenceFileTarget struct { + Name string + UID string + GID string + Mode os.FileMode +} + +// SecretReference is a reference to a secret in swarm +type SecretReference struct { + SecretID string + SecretName string + Target *SecretReferenceFileTarget +} diff --git a/components/engine/api/types/types.go b/components/engine/api/types/types.go index 5591646b69..a185baf9dc 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" @@ -128,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 @@ -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 { + Filters 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..9800048341 --- /dev/null +++ b/components/engine/cli/command/secret/create.go @@ -0,0 +1,66 @@ +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/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/spf13/cobra" +) + +type createOptions struct { + name string + labels opts.ListOpts +} + +func newSecretCreateCommand(dockerCli *command.DockerCli) *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.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + 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, options 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: options.name, + Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), + }, + 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..25da79f16d --- /dev/null +++ b/components/engine/cli/command/secret/inspect.go @@ -0,0 +1,56 @@ +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() + + // attempt to lookup secret by name + secrets, err := getSecretsByName(client, ctx, []string{opts.name}) + if err != nil { + return err + } + + 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/ls.go b/components/engine/cli/command/secret/ls.go new file mode 100644 index 0000000000..67fc1daff6 --- /dev/null +++ b/components/engine/cli/command/secret/ls.go @@ -0,0 +1,68 @@ +package secret + +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" +) + +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 { + 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) + } + } + + 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..d277eceba2 --- /dev/null +++ b/components/engine/cli/command/secret/remove.go @@ -0,0 +1,66 @@ +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() + + // attempt to lookup secret by name + secrets, err := getSecretsByName(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 + } + + fmt.Fprintln(dockerCli.Out(), id) + } + + return nil +} diff --git a/components/engine/cli/command/secret/utils.go b/components/engine/cli/command/secret/utils.go new file mode 100644 index 0000000000..d1a7d97c44 --- /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 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{ + Filters: args, + }) +} diff --git a/components/engine/cli/command/service/create.go b/components/engine/cli/command/service/create.go index d6c3ebdb9c..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") @@ -58,6 +59,13 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { return err } + // parse and validate secrets + secrets, err := parseSecrets(apiClient, opts.secrets.Value()) + 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..b81998ec09 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,6 +432,7 @@ type serviceOptions struct { logDriver logDriverOptions healthcheck healthCheckOptions + secrets opts.SecretOpt } func newServiceOptions() *serviceOptions { @@ -403,6 +499,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Options: opts.dnsOptions.GetAll(), }, StopGracePeriod: opts.stopGrace.Value(), + Secrets: nil, }, Networks: convertNetworks(opts.networks.GetAll()), Resources: opts.resources.ToResourceRequirements(), @@ -553,4 +650,7 @@ const ( flagHealthRetries = "health-retries" flagHealthTimeout = "health-timeout" flagNoHealthcheck = "no-healthcheck" + flagSecret = "secret" + flagSecretAdd = "secret-add" + flagSecretRemove = "secret-rm" ) diff --git a/components/engine/cli/command/service/parse.go b/components/engine/cli/command/service/parse.go new file mode 100644 index 0000000000..368bc6d449 --- /dev/null +++ b/components/engine/cli/command/service/parse.go @@ -0,0 +1,68 @@ +package service + +import ( + "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 +// them to secret references to use with the spec +func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) { + secretRefs := make(map[string]*swarmtypes.SecretReference) + ctx := context.Background() + + for _, secret := range requestedSecrets { + secretRef := &swarmtypes.SecretReference{ + SecretName: secret.Source, + Target: &swarmtypes.SecretReferenceFileTarget{ + 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) + } + secretRefs[secret.Target] = secretRef + } + + args := filters.NewArgs() + for _, s := range secretRefs { + args.Add("names", s.SecretName) + } + + secrets, err := client.SecretList(ctx, types.SecretListOptions{ + Filters: args, + }) + if err != nil { + return nil, err + } + + foundSecrets := make(map[string]string) + for _, secret := range secrets { + foundSecrets[secret.Spec.Annotations.Name] = secret.ID + } + + addedSecrets := []*swarmtypes.SecretReference{} + + for _, ref := range secretRefs { + id, ok := foundSecrets[ref.SecretName] + if !ok { + 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 + ref.SecretID = id + addedSecrets = append(addedSecrets, ref) + } + + return addedSecrets, nil +} diff --git a/components/engine/cli/command/service/update.go b/components/engine/cli/command/service/update.go index 4a77229497..1bc72a8f19 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.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") @@ -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,27 @@ 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 := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value() + + 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/client/errors.go b/components/engine/client/errors.go index 53e2065332..94c22a728a 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..f92a3d1510 --- /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", 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..b7def89d0e --- /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" + 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..7e9d5ec167 --- /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.Filters.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filters) + 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..1ac11cddb3 --- /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{ + Filters: 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_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_unix.go b/components/engine/container/container_unix.go index c38f750667..f02be89e78 100644 --- a/components/engine/container/container_unix.go +++ b/components/engine/container/container_unix.go @@ -22,8 +22,11 @@ import ( "golang.org/x/sys/unix" ) -// DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container -const DefaultSHMSize int64 = 67108864 +const ( + // DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container + 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,11 @@ 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") +} + // 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 +268,31 @@ func (container *Container) IpcMounts() []Mount { return mounts } +// SecretMount returns the mount for the secret path +func (container *Container) SecretMount() *Mount { + if len(container.Secrets) > 0 { + return &Mount{ + Source: container.SecretMountPath(), + Destination: containerSecretMountPath, + Writable: false, + } + } + + return nil +} + +// 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 + } + return err + } + + 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/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 ( diff --git a/components/engine/daemon/cluster/convert/container.go b/components/engine/daemon/cluster/convert/container.go index 38749dd8b2..a1ecabe92a 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" @@ -23,6 +24,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 +77,49 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec { return containerSpec } +func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference { + refs := make([]*swarmapi.SecretReference, 0, len(sr)) + for _, s := range sr { + refs = append(refs, &swarmapi.SecretReference{ + SecretID: s.SecretID, + SecretName: s.SecretName, + Target: &swarmapi.SecretReference_File{ + File: &swarmapi.SecretReference_FileTarget{ + Name: s.Target.Name, + UID: s.Target.UID, + GID: s.Target.GID, + Mode: s.Target.Mode, + }, + }, + }) + } + + return refs +} +func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference { + refs := make([]*types.SecretReference, 0, len(sr)) + for _, s := range sr { + 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: &types.SecretReferenceFileTarget{ + Name: target.Name, + UID: target.UID, + GID: target.GID, + Mode: target.Mode, + }, + }) + } + + return refs +} + func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) { containerSpec := &swarmapi.ContainerSpec{ Image: c.Image, @@ -87,6 +132,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..f8cb4ff80c --- /dev/null +++ b/components/engine/daemon/cluster/convert/secret.go @@ -0,0 +1,41 @@ +package convert + +import ( + 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 { + 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, + }, + } + + secret.Version.Index = s.Meta.Version.Index + // Meta + secret.CreatedAt, _ = ptypes.Timestamp(s.Meta.CreatedAt) + secret.UpdatedAt, _ = ptypes.Timestamp(s.Meta.UpdatedAt) + + return secret +} + +// SecretSpecToGRPC converts Secret to a grpc Secret. +func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec { + return swarmapi.SecretSpec{ + Annotations: swarmapi.Annotations{ + Name: s.Name, + Labels: s.Labels, + }, + Data: s.Data, + } +} 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..98ea358100 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.SecretGetter } -func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*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 @@ -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,40 @@ func (c *containerAdapter) create(ctx context.Context) error { } } + 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 container.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.GetFile() + if target == nil { + logrus.Warnf("secret target was not a file: secret=%s", s.SecretID) + continue + } + + secrets = append(secrets, &containertypes.ContainerSecret{ + Name: name, + Target: target.Name, + Data: sec.Spec.Data, + UID: target.UID, + GID: target.GID, + Mode: target.Mode, + }) + } + + // 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..e0ee81a8b9 100644 --- a/components/engine/daemon/cluster/executor/container/attachment.go +++ b/components/engine/daemon/cluster/executor/container/attachment.go @@ -2,6 +2,7 @@ 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" ) @@ -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.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 0185e415b5..47fc2bf7a5 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.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 844821b83e..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(), } } @@ -122,10 +125,10 @@ 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) { if t.Spec.GetAttachment() != nil { - return newNetworkAttacherController(e.backend, t) + return newNetworkAttacherController(e.backend, t, e.secrets) } - ctlr, err := newController(e.backend, t) + 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) } 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..27114152f4 --- /dev/null +++ b/components/engine/daemon/cluster/secrets.go @@ -0,0 +1,126 @@ +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.Filters) + 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 := convert.SecretSpecToGRPC(s) + + 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. +// 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() + + if !c.isActiveManager() { + return c.errNoManager() + } + + ctx, cancel := c.getRequestContext() + defer cancel() + + secretSpec := convert.SecretSpecToGRPC(spec) + + 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..5e1da60414 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" @@ -12,10 +13,12 @@ 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" "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/libnetwork" @@ -23,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 } @@ -139,6 +143,79 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error { return nil } + +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) + + defer func() { + if setupErr != nil { + // cleanup + _ = detachMounted(localMountPath) + + if err := os.RemoveAll(localMountPath); err != nil { + log.Errorf("error cleaning up secret mount: %s", err) + } + } + }() + + // retrieve possible remapped range start for root UID, GID + rootUID, rootGID := daemon.GetRemappedUIDGID() + // create tmpfs + if err := idtools.MkdirAllAs(localMountPath, 0700, rootUID, rootGID); err != nil { + return errors.Wrap(err, "error creating secret local mount path") + } + 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") + } + + for _, s := range c.Secrets { + targetPath := filepath.Clean(s.Target) + // ensure that the target is a filename only; no paths allowed + if targetPath != filepath.Base(targetPath) { + return fmt.Errorf("error creating secret: secret must not be a path") + } + + fPath := filepath.Join(localMountPath, targetPath) + if err := idtools.MkdirAllAs(filepath.Dir(fPath), 0700, rootUID, rootGID); err != nil { + return 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 errors.Wrap(err, "error injecting secret") + } + + 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, 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,"+tmpfsOwnership); err != nil { + return errors.Wrap(err, "unable to remount secret dir as readonly") + } + + 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..3ce21dfb93 100644 --- a/components/engine/daemon/oci_linux.go +++ b/components/engine/daemon/oci_linux.go @@ -702,16 +702,27 @@ 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...) + + if m := c.SecretMount(); m != nil { + ms = append(ms, *m) + } + 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..1d13adaaa4 --- /dev/null +++ b/components/engine/daemon/secrets.go @@ -0,0 +1,23 @@ +package daemon + +import ( + "github.com/Sirupsen/logrus" + 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() && len(secrets) > 0 { + 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) } 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` 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..6a124d5383 --- /dev/null +++ b/components/engine/docs/reference/commandline/secret_create.md @@ -0,0 +1,80 @@ +--- +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 +Options: + --help Print usage + -l, --label list Secret labels (default []) +``` + +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 secret.json | docker secret create secret.json +mhv17xfe3gh6xc4rij5orpfds + +$ docker secret ls +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) +* [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..0d427464af --- /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 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 secret.json +[ + { + "ID": "mhv17xfe3gh6xc4rij5orpfds", + "Version": { + "Index": 1198 + }, + "CreatedAt": "2016-10-27T23:25:43.909181089Z", + "UpdatedAt": "2016-10-27T23:25:43.909181089Z", + "Spec": { + "Name": "secret.json", + "Data": null + }, + "Digest": "sha256:8281c6d924520986e3c6af23ed8926710a611c90339db582c2a9ac480ba622b7", + "SecretSize": 1679 + } +] +``` + +### Formatting secret output + +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 %} +$ 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..fa78e66427 --- /dev/null +++ b/components/engine/docs/reference/commandline/secret_ls.md @@ -0,0 +1,43 @@ +--- +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 +``` + +Run this command from a manager to list the secrets in the Swarm. + +On a manager node: + +```bash +$ docker secret ls +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 + +* [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..f504b1ba4f --- /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. + +This example removes a secret: + +```bash +$ docker secret rm secret.json +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..8f33bf8c33 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,33 @@ ID NAME MODE REPLICAS IMAGE 4cdgfyky7ozw redis replicated 5/5 redis:3.0.7 ``` +### Create a service with secrets +Use the `--secret` flag to give a container access to a +[secret](secret_create.md). + +Create a service specifying a secret: + +```bash +$ 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 +``` + +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..261f1c3518 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 source=ssh-2,target=ssh-2 \ + --secret-rm ssh-1 \ + myservice +``` + ## Related information * [service create](service_create.md) diff --git a/components/engine/integration-cli/daemon_swarm.go b/components/engine/integration-cli/daemon_swarm.go index 50e464cdd2..9a8de41d6c 100644 --- a/components/engine/integration-cli/daemon_swarm.go +++ b/components/engine/integration-cli/daemon_swarm.go @@ -284,6 +284,42 @@ 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))) + 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 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 229c7a8e03..39bf721211 100644 --- a/components/engine/integration-cli/docker_api_swarm_test.go +++ b/components/engine/integration-cli/docker_api_swarm_test.go @@ -1263,3 +1263,50 @@ 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) 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))) +} 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") +} 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..7af6a5c6de 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,62 @@ 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) 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" + 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("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) + 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, 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..548f8e64c2 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,45 @@ 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("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) + 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, 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) +} diff --git a/components/engine/opts/secret.go b/components/engine/opts/secret.go new file mode 100644 index 0000000000..b77a33f685 --- /dev/null +++ b/components/engine/opts/secret.go @@ -0,0 +1,107 @@ +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.SecretRequestOption +} + +// 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.SecretRequestOption{ + Source: "", + Target: "", + UID: "0", + GID: "0", + 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]) + + 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: + if len(fields) == 1 && value == "" { + + } else { + 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.SecretRequestOption { + return o.values +} 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)) +} 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} +}