mirror of https://github.com/docker/cli.git
secrets: secret management for swarm
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> wip: use tmpfs for swarm secrets Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> wip: inject secrets from swarm secret store Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> secrets: use secret names in cli for service create Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> switch to use mounts instead of volumes Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> vendor: use ehazlett swarmkit Signed-off-by: Evan Hazlett <ejhazlett@gmail.com> secrets: finish secret update Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
This commit is contained in:
parent
a11f7b1577
commit
1be644fbcf
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/docker/docker/cli/command/node"
|
"github.com/docker/docker/cli/command/node"
|
||||||
"github.com/docker/docker/cli/command/plugin"
|
"github.com/docker/docker/cli/command/plugin"
|
||||||
"github.com/docker/docker/cli/command/registry"
|
"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/service"
|
||||||
"github.com/docker/docker/cli/command/stack"
|
"github.com/docker/docker/cli/command/stack"
|
||||||
"github.com/docker/docker/cli/command/swarm"
|
"github.com/docker/docker/cli/command/swarm"
|
||||||
|
@ -25,6 +26,7 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||||
node.NewNodeCommand(dockerCli),
|
node.NewNodeCommand(dockerCli),
|
||||||
service.NewServiceCommand(dockerCli),
|
service.NewServiceCommand(dockerCli),
|
||||||
swarm.NewSwarmCommand(dockerCli),
|
swarm.NewSwarmCommand(dockerCli),
|
||||||
|
secret.NewSecretCommand(dockerCli),
|
||||||
container.NewContainerCommand(dockerCli),
|
container.NewContainerCommand(dockerCli),
|
||||||
image.NewImageCommand(dockerCli),
|
image.NewImageCommand(dockerCli),
|
||||||
system.NewSystemCommand(dockerCli),
|
system.NewSystemCommand(dockerCli),
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -58,6 +58,13 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
|
||||||
return err
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
// only send auth if flag was set
|
// only send auth if flag was set
|
||||||
|
|
|
@ -191,6 +191,19 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
|
||||||
return nets
|
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 {
|
type endpointOptions struct {
|
||||||
mode string
|
mode string
|
||||||
ports opts.ListOpts
|
ports opts.ListOpts
|
||||||
|
@ -337,6 +350,7 @@ type serviceOptions struct {
|
||||||
logDriver logDriverOptions
|
logDriver logDriverOptions
|
||||||
|
|
||||||
healthcheck healthCheckOptions
|
healthcheck healthCheckOptions
|
||||||
|
secrets []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServiceOptions() *serviceOptions {
|
func newServiceOptions() *serviceOptions {
|
||||||
|
@ -403,6 +417,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
|
||||||
Options: opts.dnsOptions.GetAll(),
|
Options: opts.dnsOptions.GetAll(),
|
||||||
},
|
},
|
||||||
StopGracePeriod: opts.stopGrace.Value(),
|
StopGracePeriod: opts.stopGrace.Value(),
|
||||||
|
Secrets: convertSecrets(opts.secrets),
|
||||||
},
|
},
|
||||||
Networks: convertNetworks(opts.networks.GetAll()),
|
Networks: convertNetworks(opts.networks.GetAll()),
|
||||||
Resources: opts.resources.ToResourceRequirements(),
|
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.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK")
|
||||||
|
|
||||||
flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY")
|
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 (
|
const (
|
||||||
|
@ -553,4 +569,5 @@ const (
|
||||||
flagHealthRetries = "health-retries"
|
flagHealthRetries = "health-retries"
|
||||||
flagHealthTimeout = "health-timeout"
|
flagHealthTimeout = "health-timeout"
|
||||||
flagNoHealthcheck = "no-healthcheck"
|
flagNoHealthcheck = "no-healthcheck"
|
||||||
|
flagSecret = "secret"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue