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:
Evan Hazlett 2016-10-19 12:22:02 -04:00
parent a11f7b1577
commit 1be644fbcf
9 changed files with 351 additions and 0 deletions

View File

@ -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),

29
command/secret/cmd.go Normal file
View File

@ -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
}

57
command/secret/create.go Normal file
View File

@ -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
}

42
command/secret/inspect.go Normal file
View File

@ -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)
}

62
command/secret/ls.go Normal file
View File

@ -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
}

43
command/secret/remove.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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"
) )

92
command/service/parse.go Normal file
View File

@ -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
}