From a0307eb205ef5330ab67a1fc26d4436d83779c32 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Wed, 19 Oct 2016 12:22:02 -0400 Subject: [PATCH] secrets: secret management for swarm Signed-off-by: Evan Hazlett wip: use tmpfs for swarm secrets Signed-off-by: Evan Hazlett wip: inject secrets from swarm secret store Signed-off-by: Evan Hazlett secrets: use secret names in cli for service create Signed-off-by: Evan Hazlett switch to use mounts instead of volumes Signed-off-by: Evan Hazlett vendor: use ehazlett swarmkit Signed-off-by: Evan Hazlett secrets: finish secret update Signed-off-by: Evan Hazlett Upstream-commit: 1be644fbcf68872433e56914e6c4357920d084ca Component: cli --- components/cli/command/commands/commands.go | 2 + components/cli/command/secret/cmd.go | 29 +++++++ components/cli/command/secret/create.go | 57 +++++++++++++ components/cli/command/secret/inspect.go | 42 ++++++++++ components/cli/command/secret/ls.go | 62 ++++++++++++++ components/cli/command/secret/remove.go | 43 ++++++++++ components/cli/command/service/create.go | 7 ++ components/cli/command/service/opts.go | 17 ++++ components/cli/command/service/parse.go | 92 +++++++++++++++++++++ 9 files changed, 351 insertions(+) create mode 100644 components/cli/command/secret/cmd.go create mode 100644 components/cli/command/secret/create.go create mode 100644 components/cli/command/secret/inspect.go create mode 100644 components/cli/command/secret/ls.go create mode 100644 components/cli/command/secret/remove.go create mode 100644 components/cli/command/service/parse.go diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index fad709bca1..d64d5680cc 100644 --- a/components/cli/command/commands/commands.go +++ b/components/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/cli/command/secret/cmd.go b/components/cli/command/secret/cmd.go new file mode 100644 index 0000000000..995300ad77 --- /dev/null +++ b/components/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/cli/command/secret/create.go b/components/cli/command/secret/create.go new file mode 100644 index 0000000000..1c0e933f57 --- /dev/null +++ b/components/cli/command/secret/create.go @@ -0,0 +1,57 @@ +package secret + +import ( + "context" + "fmt" + "io/ioutil" + "os" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type createOptions struct { + name string +} + +func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "create [name]", + Short: "Create a secret using stdin as content", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts := createOptions{ + name: args[0], + } + + return runSecretCreate(dockerCli, opts) + }, + } +} + +func runSecretCreate(dockerCli *command.DockerCli, opts createOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + secretData, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("Error reading content from STDIN: %v", err) + } + + spec := swarm.SecretSpec{ + Annotations: swarm.Annotations{ + Name: opts.name, + }, + Data: secretData, + } + + r, err := client.SecretCreate(ctx, spec) + if err != nil { + return err + } + + fmt.Fprintln(dockerCli.Out(), r.ID) + return nil +} diff --git a/components/cli/command/secret/inspect.go b/components/cli/command/secret/inspect.go new file mode 100644 index 0000000000..c8d5cd8f79 --- /dev/null +++ b/components/cli/command/secret/inspect.go @@ -0,0 +1,42 @@ +package secret + +import ( + "context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/inspect" + "github.com/spf13/cobra" +) + +type inspectOptions struct { + name string + format string +} + +func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := inspectOptions{} + cmd := &cobra.Command{ + Use: "inspect [name]", + Short: "Inspect a secret", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.name = args[0] + return runSecretInspect(dockerCli, opts) + }, + } + + cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + return cmd +} + +func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + getRef := func(name string) (interface{}, []byte, error) { + return client.SecretInspectWithRaw(ctx, name) + } + + return inspect.Inspect(dockerCli.Out(), []string{opts.name}, opts.format, getRef) +} diff --git a/components/cli/command/secret/ls.go b/components/cli/command/secret/ls.go new file mode 100644 index 0000000000..1befdad9d0 --- /dev/null +++ b/components/cli/command/secret/ls.go @@ -0,0 +1,62 @@ +package secret + +import ( + "context" + "fmt" + "text/tabwriter" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type listOptions struct { + quiet bool +} + +func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := listOptions{} + + cmd := &cobra.Command{ + Use: "ls", + Short: "List secrets", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runSecretList(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + + return cmd +} + +func runSecretList(dockerCli *command.DockerCli, opts listOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + secrets, err := client.SecretList(ctx, types.SecretListOptions{}) + if err != nil { + return err + } + + w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) + if opts.quiet { + for _, s := range secrets { + fmt.Fprintf(w, "%s\n", s.ID) + } + } else { + fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED\tSIZE") + fmt.Fprintf(w, "\n") + + for _, s := range secrets { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", s.ID, s.Spec.Annotations.Name, s.Meta.CreatedAt, s.Meta.UpdatedAt, s.SecretSize) + } + } + + w.Flush() + + return nil +} diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go new file mode 100644 index 0000000000..f336c6161a --- /dev/null +++ b/components/cli/command/secret/remove.go @@ -0,0 +1,43 @@ +package secret + +import ( + "context" + "fmt" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type removeOptions struct { + ids []string +} + +func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "rm [id]", + Short: "Remove a secret", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts := removeOptions{ + ids: args, + } + return runSecretRemove(dockerCli, opts) + }, + } +} + +func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + for _, id := range opts.ids { + if err := client.SecretRemove(ctx, id); err != nil { + return err + } + + fmt.Fprintln(dockerCli.Out(), id) + } + + return nil +} diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index d6c3ebdb9c..8fb9070e67 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -58,6 +58,13 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { return err } + // parse and validate secrets + secrets, err := parseSecrets(apiClient, opts.secrets) + if err != nil { + return err + } + service.TaskTemplate.ContainerSpec.Secrets = secrets + ctx := context.Background() // only send auth if flag was set diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 827c4e5cdc..a4fd08881c 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -191,6 +191,19 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { return nets } +func convertSecrets(secrets []string) []*swarm.SecretReference { + sec := []*swarm.SecretReference{} + for _, s := range secrets { + sec = append(sec, &swarm.SecretReference{ + SecretID: s, + Mode: swarm.SecretReferenceFile, + Target: "", + }) + } + + return sec +} + type endpointOptions struct { mode string ports opts.ListOpts @@ -337,6 +350,7 @@ type serviceOptions struct { logDriver logDriverOptions healthcheck healthCheckOptions + secrets []string } func newServiceOptions() *serviceOptions { @@ -403,6 +417,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Options: opts.dnsOptions.GetAll(), }, StopGracePeriod: opts.stopGrace.Value(), + Secrets: convertSecrets(opts.secrets), }, Networks: convertNetworks(opts.networks.GetAll()), Resources: opts.resources.ToResourceRequirements(), @@ -488,6 +503,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") + flags.StringSliceVar(&opts.secrets, flagSecret, []string{}, "Specify secrets to expose to the service") } const ( @@ -553,4 +569,5 @@ const ( flagHealthRetries = "health-retries" flagHealthTimeout = "health-timeout" flagNoHealthcheck = "no-healthcheck" + flagSecret = "secret" ) diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go new file mode 100644 index 0000000000..41883fb445 --- /dev/null +++ b/components/cli/command/service/parse.go @@ -0,0 +1,92 @@ +package service + +import ( + "context" + "fmt" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + swarmtypes "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" +) + +// parseSecretString parses the requested secret and returns the secret name +// and target. Expects format SECRET_NAME:TARGET +func parseSecretString(secretString string) (string, string, error) { + tokens := strings.Split(secretString, ":") + + secretName := strings.TrimSpace(tokens[0]) + targetName := "" + + if secretName == "" { + return "", "", fmt.Errorf("invalid secret name provided") + } + + if len(tokens) > 1 { + targetName = strings.TrimSpace(tokens[1]) + if targetName == "" { + return "", "", fmt.Errorf("invalid presentation name provided") + } + } else { + targetName = secretName + } + return secretName, targetName, nil +} + +// parseSecrets retrieves the secrets from the requested names and converts +// them to secret references to use with the spec +func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) { + lookupSecretNames := []string{} + needSecrets := make(map[string]*swarmtypes.SecretReference) + ctx := context.Background() + + for _, secret := range requestedSecrets { + n, t, err := parseSecretString(secret) + if err != nil { + return nil, err + } + + secretRef := &swarmtypes.SecretReference{ + SecretName: n, + Mode: swarmtypes.SecretReferenceFile, + Target: t, + } + + lookupSecretNames = append(lookupSecretNames, n) + needSecrets[n] = secretRef + } + + args := filters.NewArgs() + for _, s := range lookupSecretNames { + args.Add("names", s) + } + + secrets, err := client.SecretList(ctx, types.SecretListOptions{ + Filter: args, + }) + if err != nil { + return nil, err + } + + foundSecrets := make(map[string]*swarmtypes.Secret) + for _, secret := range secrets { + foundSecrets[secret.Spec.Annotations.Name] = &secret + } + + addedSecrets := []*swarmtypes.SecretReference{} + + for secretName, secretRef := range needSecrets { + s, ok := foundSecrets[secretName] + if !ok { + return nil, fmt.Errorf("secret not found: %s", secretName) + } + + // set the id for the ref to properly assign in swarm + // since swarm needs the ID instead of the name + secretRef.SecretID = s.ID + addedSecrets = append(addedSecrets, secretRef) + } + + return addedSecrets, nil +}