Merge pull request #213 from dnephin/improve-swarm-ca-cmd

Refactor and UI changes to `swarm ca` command
This commit is contained in:
Vincent Demeester 2017-07-03 17:02:45 +02:00 committed by GitHub
commit 85b41c3e71
4 changed files with 148 additions and 50 deletions

View File

@ -3,23 +3,22 @@ package swarm
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"golang.org/x/net/context"
"io/ioutil" "io/ioutil"
"strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/swarm/progress" "github.com/docker/cli/cli/command/swarm/progress"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"golang.org/x/net/context"
) )
type caOptions struct { type caOptions struct {
swarmOptions swarmCAOptions
rootCACert PEMFile rootCACert PEMFile
rootCAKey PEMFile rootCAKey PEMFile
rotate bool rotate bool
@ -27,21 +26,21 @@ type caOptions struct {
quiet bool quiet bool
} }
func newRotateCACommand(dockerCli command.Cli) *cobra.Command { func newCACommand(dockerCli command.Cli) *cobra.Command {
opts := caOptions{} opts := caOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "ca [OPTIONS]", Use: "ca [OPTIONS]",
Short: "Manage root CA", Short: "Display and rotate the root CA",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runRotateCA(dockerCli, cmd.Flags(), opts) return runCA(dockerCli, cmd.Flags(), opts)
}, },
Tags: map[string]string{"version": "1.30"}, Tags: map[string]string{"version": "1.30"},
} }
flags := cmd.Flags() flags := cmd.Flags()
addSwarmCAFlags(flags, &opts.swarmOptions) addSwarmCAFlags(flags, &opts.swarmCAOptions)
flags.BoolVar(&opts.rotate, flagRotate, false, "Rotate the swarm CA - if no certificate or key are provided, new ones will be generated") flags.BoolVar(&opts.rotate, flagRotate, false, "Rotate the swarm CA - if no certificate or key are provided, new ones will be generated")
flags.Var(&opts.rootCACert, flagCACert, "Path to the PEM-formatted root CA certificate to use for the new cluster") flags.Var(&opts.rootCACert, flagCACert, "Path to the PEM-formatted root CA certificate to use for the new cluster")
flags.Var(&opts.rootCAKey, flagCAKey, "Path to the PEM-formatted root CA key to use for the new cluster") flags.Var(&opts.rootCAKey, flagCAKey, "Path to the PEM-formatted root CA key to use for the new cluster")
@ -51,7 +50,7 @@ func newRotateCACommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) error { func runCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) error {
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background() ctx := context.Background()
@ -66,31 +65,10 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er
return fmt.Errorf("`--%s` flag requires the `--rotate` flag to update the CA", f) return fmt.Errorf("`--%s` flag requires the `--rotate` flag to update the CA", f)
} }
} }
if swarmInspect.ClusterInfo.TLSInfo.TrustRoot == "" { return displayTrustRoot(dockerCli.Out(), swarmInspect)
fmt.Fprintln(dockerCli.Out(), "No CA information available")
} else {
fmt.Fprintln(dockerCli.Out(), strings.TrimSpace(swarmInspect.ClusterInfo.TLSInfo.TrustRoot))
}
return nil
}
genRootCA := true
spec := &swarmInspect.Spec
opts.mergeSwarmSpec(spec, flags) // updates the spec given the cert expiry or external CA flag
if flags.Changed(flagCACert) {
spec.CAConfig.SigningCACert = opts.rootCACert.Contents()
genRootCA = false
}
if flags.Changed(flagCAKey) {
spec.CAConfig.SigningCAKey = opts.rootCAKey.Contents()
genRootCA = false
}
if genRootCA {
spec.CAConfig.ForceRotate++
spec.CAConfig.SigningCACert = ""
spec.CAConfig.SigningCAKey = ""
} }
updateSwarmSpec(&swarmInspect.Spec, flags, opts)
if err := client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, swarm.UpdateFlags{}); err != nil { if err := client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, swarm.UpdateFlags{}); err != nil {
return err return err
} }
@ -98,7 +76,29 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er
if opts.detach { if opts.detach {
return nil return nil
} }
return attach(ctx, dockerCli, opts)
}
func updateSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet, opts caOptions) {
opts.mergeSwarmSpecCAFlags(spec, flags)
caCert := opts.rootCACert.Contents()
caKey := opts.rootCAKey.Contents()
if caCert != "" {
spec.CAConfig.SigningCACert = caCert
}
if caKey != "" {
spec.CAConfig.SigningCAKey = caKey
}
if caKey == "" && caCert == "" {
spec.CAConfig.ForceRotate++
spec.CAConfig.SigningCACert = ""
spec.CAConfig.SigningCAKey = ""
}
}
func attach(ctx context.Context, dockerCli command.Cli, opts caOptions) error {
client := dockerCli.Client()
errChan := make(chan error, 1) errChan := make(chan error, 1)
pipeReader, pipeWriter := io.Pipe() pipeReader, pipeWriter := io.Pipe()
@ -111,7 +111,7 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er
return <-errChan return <-errChan
} }
err = jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil) err := jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil)
if err == nil { if err == nil {
err = <-errChan err = <-errChan
} }
@ -119,15 +119,17 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er
return err return err
} }
swarmInspect, err = client.SwarmInspect(ctx) swarmInspect, err := client.SwarmInspect(ctx)
if err != nil { if err != nil {
return err return err
} }
return displayTrustRoot(dockerCli.Out(), swarmInspect)
}
if swarmInspect.ClusterInfo.TLSInfo.TrustRoot == "" { func displayTrustRoot(out io.Writer, info swarm.Swarm) error {
fmt.Fprintln(dockerCli.Out(), "No CA information available") if info.ClusterInfo.TLSInfo.TrustRoot == "" {
} else { return errors.New("No CA information available")
fmt.Fprintln(dockerCli.Out(), strings.TrimSpace(swarmInspect.ClusterInfo.TLSInfo.TrustRoot))
} }
fmt.Fprintln(out, strings.TrimSpace(info.ClusterInfo.TLSInfo.TrustRoot))
return nil return nil
} }

View File

@ -0,0 +1,88 @@
package swarm
import (
"bytes"
"testing"
"time"
"github.com/docker/docker/api/types/swarm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func swarmSpecWithFullCAConfig() *swarm.Spec {
return &swarm.Spec{
CAConfig: swarm.CAConfig{
SigningCACert: "cacert",
SigningCAKey: "cakey",
ForceRotate: 1,
NodeCertExpiry: time.Duration(200),
ExternalCAs: []*swarm.ExternalCA{
{
URL: "https://example.com/ca",
Protocol: swarm.ExternalCAProtocolCFSSL,
CACert: "excacert",
},
},
},
}
}
func TestDisplayTrustRootNoRoot(t *testing.T) {
buffer := new(bytes.Buffer)
err := displayTrustRoot(buffer, swarm.Swarm{})
assert.EqualError(t, err, "No CA information available")
}
func TestDisplayTrustRoot(t *testing.T) {
buffer := new(bytes.Buffer)
trustRoot := "trustme"
err := displayTrustRoot(buffer, swarm.Swarm{
ClusterInfo: swarm.ClusterInfo{
TLSInfo: swarm.TLSInfo{TrustRoot: trustRoot},
},
})
require.NoError(t, err)
assert.Equal(t, trustRoot+"\n", buffer.String())
}
func TestUpdateSwarmSpecDefaultRotate(t *testing.T) {
spec := swarmSpecWithFullCAConfig()
flags := newCACommand(nil).Flags()
updateSwarmSpec(spec, flags, caOptions{})
expected := swarmSpecWithFullCAConfig()
expected.CAConfig.ForceRotate = 2
expected.CAConfig.SigningCACert = ""
expected.CAConfig.SigningCAKey = ""
assert.Equal(t, expected, spec)
}
func TestUpdateSwarmSpecPartial(t *testing.T) {
spec := swarmSpecWithFullCAConfig()
flags := newCACommand(nil).Flags()
updateSwarmSpec(spec, flags, caOptions{
rootCACert: PEMFile{contents: "cacert"},
})
expected := swarmSpecWithFullCAConfig()
expected.CAConfig.SigningCACert = "cacert"
assert.Equal(t, expected, spec)
}
func TestUpdateSwarmSpecFullFlags(t *testing.T) {
flags := newCACommand(nil).Flags()
flags.Lookup(flagCertExpiry).Changed = true
spec := swarmSpecWithFullCAConfig()
updateSwarmSpec(spec, flags, caOptions{
rootCACert: PEMFile{contents: "cacert"},
rootCAKey: PEMFile{contents: "cakey"},
swarmCAOptions: swarmCAOptions{nodeCertExpiry: 3 * time.Minute},
})
expected := swarmSpecWithFullCAConfig()
expected.CAConfig.SigningCACert = "cacert"
expected.CAConfig.SigningCAKey = "cakey"
expected.CAConfig.NodeCertExpiry = 3 * time.Minute
assert.Equal(t, expected, spec)
}

View File

@ -25,7 +25,7 @@ func NewSwarmCommand(dockerCli command.Cli) *cobra.Command {
newUpdateCommand(dockerCli), newUpdateCommand(dockerCli),
newLeaveCommand(dockerCli), newLeaveCommand(dockerCli),
newUnlockCommand(dockerCli), newUnlockCommand(dockerCli),
newRotateCACommand(dockerCli), newCACommand(dockerCli),
) )
return cmd return cmd
} }

View File

@ -36,10 +36,9 @@ const (
) )
type swarmOptions struct { type swarmOptions struct {
swarmCAOptions
taskHistoryLimit int64 taskHistoryLimit int64
dispatcherHeartbeat time.Duration dispatcherHeartbeat time.Duration
nodeCertExpiry time.Duration
externalCA ExternalCAOption
maxSnapshots uint64 maxSnapshots uint64
snapshotInterval uint64 snapshotInterval uint64
autolock bool autolock bool
@ -216,7 +215,7 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) {
return &externalCA, nil return &externalCA, nil
} }
func addSwarmCAFlags(flags *pflag.FlagSet, opts *swarmOptions) { func addSwarmCAFlags(flags *pflag.FlagSet, opts *swarmCAOptions) {
flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, 90*24*time.Hour, "Validity period for node certificates (ns|us|ms|s|m|h)") flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, 90*24*time.Hour, "Validity period for node certificates (ns|us|ms|s|m|h)")
flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints")
} }
@ -228,7 +227,7 @@ func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) {
flags.SetAnnotation(flagMaxSnapshots, "version", []string{"1.25"}) flags.SetAnnotation(flagMaxSnapshots, "version", []string{"1.25"})
flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots")
flags.SetAnnotation(flagSnapshotInterval, "version", []string{"1.25"}) flags.SetAnnotation(flagSnapshotInterval, "version", []string{"1.25"})
addSwarmCAFlags(flags, opts) addSwarmCAFlags(flags, &opts.swarmCAOptions)
} }
func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) { func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) {
@ -238,12 +237,6 @@ func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet)
if flags.Changed(flagDispatcherHeartbeat) { if flags.Changed(flagDispatcherHeartbeat) {
spec.Dispatcher.HeartbeatPeriod = opts.dispatcherHeartbeat spec.Dispatcher.HeartbeatPeriod = opts.dispatcherHeartbeat
} }
if flags.Changed(flagCertExpiry) {
spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry
}
if flags.Changed(flagExternalCA) {
spec.CAConfig.ExternalCAs = opts.externalCA.Value()
}
if flags.Changed(flagMaxSnapshots) { if flags.Changed(flagMaxSnapshots) {
spec.Raft.KeepOldSnapshots = &opts.maxSnapshots spec.Raft.KeepOldSnapshots = &opts.maxSnapshots
} }
@ -253,6 +246,21 @@ func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet)
if flags.Changed(flagAutolock) { if flags.Changed(flagAutolock) {
spec.EncryptionConfig.AutoLockManagers = opts.autolock spec.EncryptionConfig.AutoLockManagers = opts.autolock
} }
opts.mergeSwarmSpecCAFlags(spec, flags)
}
type swarmCAOptions struct {
nodeCertExpiry time.Duration
externalCA ExternalCAOption
}
func (opts *swarmCAOptions) mergeSwarmSpecCAFlags(spec *swarm.Spec, flags *pflag.FlagSet) {
if flags.Changed(flagCertExpiry) {
spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry
}
if flags.Changed(flagExternalCA) {
spec.CAConfig.ExternalCAs = opts.externalCA.Value()
}
} }
func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec { func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec {