diff --git a/command/swarm/cmd.go b/command/swarm/cmd.go index 5ea973bb78..6c70459df0 100644 --- a/command/swarm/cmd.go +++ b/command/swarm/cmd.go @@ -22,6 +22,7 @@ func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command { newInitCommand(dockerCli), newJoinCommand(dockerCli), newJoinTokenCommand(dockerCli), + newUnlockKeyCommand(dockerCli), newUpdateCommand(dockerCli), newLeaveCommand(dockerCli), newUnlockCommand(dockerCli), diff --git a/command/swarm/init.go b/command/swarm/init.go index b2590e1568..93c97c3a74 100644 --- a/command/swarm/init.go +++ b/command/swarm/init.go @@ -1,20 +1,15 @@ package swarm import ( - "bufio" - "crypto/rand" - "errors" "fmt" - "io" - "math/big" "strings" - "golang.org/x/crypto/ssh/terminal" "golang.org/x/net/context" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -25,7 +20,6 @@ type initOptions struct { // Not a NodeAddrOption because it has no default port. advertiseAddr string forceNewCluster bool - lockKey bool } func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -45,7 +39,6 @@ func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") - flags.BoolVar(&opts.lockKey, flagLockKey, false, "Encrypt swarm with optionally provided key from stdin") flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state") addSwarmFlags(flags, &opts.swarmOptions) return cmd @@ -55,31 +48,12 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption client := dockerCli.Client() ctx := context.Background() - var lockKey string - if opts.lockKey { - var err error - lockKey, err = readKey(dockerCli.In(), "Please enter key for encrypting swarm(leave empty to generate): ") - if err != nil { - return err - } - if len(lockKey) == 0 { - randBytes := make([]byte, 16) - if _, err := rand.Read(randBytes[:]); err != nil { - panic(fmt.Errorf("failed to general random lock key: %v", err)) - } - - var n big.Int - n.SetBytes(randBytes[:]) - lockKey = n.Text(36) - } - } - req := swarm.InitRequest{ - ListenAddr: opts.listenAddr.String(), - AdvertiseAddr: opts.advertiseAddr, - ForceNewCluster: opts.forceNewCluster, - Spec: opts.swarmOptions.ToSpec(flags), - LockKey: lockKey, + ListenAddr: opts.listenAddr.String(), + AdvertiseAddr: opts.advertiseAddr, + ForceNewCluster: opts.forceNewCluster, + Spec: opts.swarmOptions.ToSpec(flags), + AutoLockManagers: opts.swarmOptions.autolock, } nodeID, err := client.SwarmInit(ctx, req) @@ -92,29 +66,19 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption fmt.Fprintf(dockerCli.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", nodeID) - if len(lockKey) > 0 { - fmt.Fprintf(dockerCli.Out(), "Swarm is encrypted. When a node is restarted it needs to be unlocked by running command:\n\n echo '%s' | docker swarm unlock\n\n", lockKey) - } - if err := printJoinCommand(ctx, dockerCli, nodeID, true, false); err != nil { return err } fmt.Fprint(dockerCli.Out(), "To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.\n\n") + + if req.AutoLockManagers { + unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) + if err != nil { + return errors.Wrap(err, "could not fetch unlock key") + } + printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) + } + return nil } - -func readKey(in *command.InStream, prompt string) (string, error) { - if in.IsTerminal() { - fmt.Print(prompt) - dt, err := terminal.ReadPassword(int(in.FD())) - fmt.Println() - return string(dt), err - } else { - key, err := bufio.NewReader(in).ReadString('\n') - if err == io.EOF { - err = nil - } - return strings.TrimSpace(key), err - } -} diff --git a/command/swarm/opts.go b/command/swarm/opts.go index a08c761a6d..8682375b15 100644 --- a/command/swarm/opts.go +++ b/command/swarm/opts.go @@ -27,6 +27,7 @@ const ( flagMaxSnapshots = "max-snapshots" flagSnapshotInterval = "snapshot-interval" flagLockKey = "lock-key" + flagAutolock = "autolock" ) type swarmOptions struct { @@ -36,6 +37,7 @@ type swarmOptions struct { externalCA ExternalCAOption maxSnapshots uint64 snapshotInterval uint64 + autolock bool } // NodeAddrOption is a pflag.Value for listening addresses @@ -174,6 +176,7 @@ func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain") flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") + flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable or disable manager autolocking (requiring an unlock key to start a stopped manager)") } func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) { @@ -195,6 +198,9 @@ func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) if flags.Changed(flagSnapshotInterval) { spec.Raft.SnapshotInterval = opts.snapshotInterval } + if flags.Changed(flagAutolock) { + spec.EncryptionConfig.AutoLockManagers = opts.autolock + } } func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec { diff --git a/command/swarm/unlock.go b/command/swarm/unlock.go index 03a11da556..51b06d6267 100644 --- a/command/swarm/unlock.go +++ b/command/swarm/unlock.go @@ -1,9 +1,14 @@ package swarm import ( + "bufio" "context" + "fmt" + "io" + "strings" "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" @@ -24,7 +29,7 @@ func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { return err } req := swarm.UnlockRequest{ - LockKey: string(key), + UnlockKey: key, } return client.SwarmUnlock(ctx, req) @@ -33,3 +38,17 @@ func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } + +func readKey(in *command.InStream, prompt string) (string, error) { + if in.IsTerminal() { + fmt.Print(prompt) + dt, err := terminal.ReadPassword(int(in.FD())) + fmt.Println() + return string(dt), err + } + key, err := bufio.NewReader(in).ReadString('\n') + if err == io.EOF { + err = nil + } + return strings.TrimSpace(key), err +} diff --git a/command/swarm/unlock_key.go b/command/swarm/unlock_key.go new file mode 100644 index 0000000000..19caa0cc2b --- /dev/null +++ b/command/swarm/unlock_key.go @@ -0,0 +1,57 @@ +package swarm + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +func newUnlockKeyCommand(dockerCli *command.DockerCli) *cobra.Command { + var rotate, quiet bool + + cmd := &cobra.Command{ + Use: "unlock-key [OPTIONS]", + Short: "Manage the unlock key", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client := dockerCli.Client() + ctx := context.Background() + + if rotate { + // FIXME(aaronl) + } + + unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) + if err != nil { + return errors.Wrap(err, "could not fetch unlock key") + } + + if quiet { + fmt.Fprintln(dockerCli.Out(), unlockKeyResp.UnlockKey) + } else { + printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) + } + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&rotate, flagRotate, false, "Rotate unlock key") + flags.BoolVarP(&quiet, flagQuiet, "q", false, "Only display token") + + return cmd +} + +func printUnlockCommand(ctx context.Context, dockerCli *command.DockerCli, unlockKey string) { + if len(unlockKey) == 0 { + return + } + + fmt.Fprintf(dockerCli.Out(), "To unlock a swarm manager after it restarts, run the `docker swarm unlock`\ncommand and provide the following key:\n\n %s\n\nPlease remember to store this key in a password manager, since without it you\nwill not be able to restart the manager.\n", unlockKey) + return +} diff --git a/command/swarm/update.go b/command/swarm/update.go index a39f34c881..7c88760492 100644 --- a/command/swarm/update.go +++ b/command/swarm/update.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -39,8 +40,12 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts swarmOpt return err } + prevAutoLock := swarm.Spec.EncryptionConfig.AutoLockManagers + opts.mergeSwarmSpec(&swarm.Spec, flags) + curAutoLock := swarm.Spec.EncryptionConfig.AutoLockManagers + err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec, updateFlags) if err != nil { return err @@ -48,5 +53,13 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts swarmOpt fmt.Fprintln(dockerCli.Out(), "Swarm updated.") + if curAutoLock && !prevAutoLock { + unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) + if err != nil { + return errors.Wrap(err, "could not fetch unlock key") + } + printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) + } + return nil }