diff --git a/api/client/swarm/opts.go b/api/client/swarm/opts.go index 017ad69efe..72d6a7a827 100644 --- a/api/client/swarm/opts.go +++ b/api/client/swarm/opts.go @@ -1,6 +1,8 @@ package swarm import ( + "encoding/csv" + "errors" "fmt" "strings" "time" @@ -23,6 +25,7 @@ const ( flagListenAddr = "listen-addr" flagSecret = "secret" flagTaskHistoryLimit = "task-history-limit" + flagExternalCA = "external-ca" ) var ( @@ -38,6 +41,7 @@ type swarmOptions struct { taskHistoryLimit int64 dispatcherHeartbeat time.Duration nodeCertExpiry time.Duration + externalCA ExternalCAOption } // NodeAddrOption is a pflag.Value for listen and remote addresses @@ -142,12 +146,102 @@ func NewAutoAcceptOption() AutoAcceptOption { return AutoAcceptOption{values: make(map[string]bool)} } +// ExternalCAOption is a Value type for parsing external CA specifications. +type ExternalCAOption struct { + values []*swarm.ExternalCA +} + +// Set parses an external CA option. +func (m *ExternalCAOption) Set(value string) error { + parsed, err := parseExternalCA(value) + if err != nil { + return err + } + + m.values = append(m.values, parsed) + return nil +} + +// Type returns the type of this option. +func (m *ExternalCAOption) Type() string { + return "external-ca" +} + +// String returns a string repr of this option. +func (m *ExternalCAOption) String() string { + externalCAs := []string{} + for _, externalCA := range m.values { + repr := fmt.Sprintf("%s: %s", externalCA.Protocol, externalCA.URL) + externalCAs = append(externalCAs, repr) + } + return strings.Join(externalCAs, ", ") +} + +// Value returns the external CAs +func (m *ExternalCAOption) Value() []*swarm.ExternalCA { + return m.values +} + +// parseExternalCA parses an external CA specification from the command line, +// such as protocol=cfssl,url=https://example.com. +func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { + csvReader := csv.NewReader(strings.NewReader(caSpec)) + fields, err := csvReader.Read() + if err != nil { + return nil, err + } + + externalCA := swarm.ExternalCA{ + Options: make(map[string]string), + } + + var ( + hasProtocol bool + hasURL bool + ) + + for _, field := range fields { + parts := strings.SplitN(field, "=", 2) + + if len(parts) != 2 { + return nil, fmt.Errorf("invalid field '%s' must be a key=value pair", field) + } + + key, value := parts[0], parts[1] + + switch strings.ToLower(key) { + case "protocol": + hasProtocol = true + if strings.ToLower(value) == string(swarm.ExternalCAProtocolCFSSL) { + externalCA.Protocol = swarm.ExternalCAProtocolCFSSL + } else { + return nil, fmt.Errorf("unrecognized external CA protocol %s", value) + } + case "url": + hasURL = true + externalCA.URL = value + default: + externalCA.Options[key] = value + } + } + + if !hasProtocol { + return nil, errors.New("the external-ca option needs a protocol= parameter") + } + if !hasURL { + return nil, errors.New("the external-ca option needs a url= parameter") + } + + return &externalCA, nil +} + func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { flags.Var(&opts.autoAccept, flagAutoAccept, "Auto acceptance policy (worker, manager or none)") flags.StringVar(&opts.secret, flagSecret, "", "Set secret value needed to accept nodes into cluster") flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 10, "Task history retention limit") flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period") flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates") + flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") } func (opts *swarmOptions) ToSpec() swarm.Spec { @@ -160,5 +254,6 @@ func (opts *swarmOptions) ToSpec() swarm.Spec { spec.Orchestration.TaskHistoryRetentionLimit = opts.taskHistoryLimit spec.Dispatcher.HeartbeatPeriod = uint64(opts.dispatcherHeartbeat.Nanoseconds()) spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry + spec.CAConfig.ExternalCAs = opts.externalCA.Value() return spec } diff --git a/api/client/swarm/update.go b/api/client/swarm/update.go index 1cf4880e97..cc7bcaa167 100644 --- a/api/client/swarm/update.go +++ b/api/client/swarm/update.go @@ -85,5 +85,10 @@ func mergeSwarm(swarm *swarm.Swarm, flags *pflag.FlagSet) error { } } + if flags.Changed(flagExternalCA) { + value := flags.Lookup(flagExternalCA).Value.(*ExternalCAOption) + spec.CAConfig.ExternalCAs = value.Value() + } + return nil } diff --git a/daemon/cluster/convert/swarm.go b/daemon/cluster/convert/swarm.go index ab7a4df65c..e9131a3f24 100644 --- a/daemon/cluster/convert/swarm.go +++ b/daemon/cluster/convert/swarm.go @@ -35,6 +35,14 @@ func SwarmFromGRPC(c swarmapi.Cluster) types.Swarm { swarm.Spec.CAConfig.NodeCertExpiry, _ = ptypes.Duration(c.Spec.CAConfig.NodeCertExpiry) + for _, ca := range c.Spec.CAConfig.ExternalCAs { + swarm.Spec.CAConfig.ExternalCAs = append(swarm.Spec.CAConfig.ExternalCAs, &types.ExternalCA{ + Protocol: types.ExternalCAProtocol(strings.ToLower(ca.Protocol.String())), + URL: ca.URL, + Options: ca.Options, + }) + } + // Meta swarm.Version.Index = c.Meta.Version.Index swarm.CreatedAt, _ = ptypes.Timestamp(c.Meta.CreatedAt) @@ -84,6 +92,18 @@ func SwarmSpecToGRPCandMerge(s types.Spec, existingSpec *swarmapi.ClusterSpec) ( }, } + for _, ca := range s.CAConfig.ExternalCAs { + protocol, ok := swarmapi.ExternalCA_CAProtocol_value[strings.ToUpper(string(ca.Protocol))] + if !ok { + return swarmapi.ClusterSpec{}, fmt.Errorf("invalid protocol: %q", ca.Protocol) + } + spec.CAConfig.ExternalCAs = append(spec.CAConfig.ExternalCAs, &swarmapi.ExternalCA{ + Protocol: swarmapi.ExternalCA_CAProtocol(protocol), + URL: ca.URL, + Options: ca.Options, + }) + } + if err := SwarmSpecUpdateAcceptancePolicy(&spec, s.AcceptancePolicy, existingSpec); err != nil { return swarmapi.ClusterSpec{}, err }