Browse code

Add support for external CAs

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
(cherry picked from commit 11085b2260a78b3248f3e98e0a1e3203431fae22)

Tonis Tiigi authored on 2016/07/01 06:39:39
Showing 3 changed files
... ...
@@ -1,6 +1,8 @@
1 1
 package swarm
2 2
 
3 3
 import (
4
+	"encoding/csv"
5
+	"errors"
4 6
 	"fmt"
5 7
 	"strings"
6 8
 	"time"
... ...
@@ -23,6 +25,7 @@ const (
23 23
 	flagListenAddr          = "listen-addr"
24 24
 	flagSecret              = "secret"
25 25
 	flagTaskHistoryLimit    = "task-history-limit"
26
+	flagExternalCA          = "external-ca"
26 27
 )
27 28
 
28 29
 var (
... ...
@@ -38,6 +41,7 @@ type swarmOptions struct {
38 38
 	taskHistoryLimit    int64
39 39
 	dispatcherHeartbeat time.Duration
40 40
 	nodeCertExpiry      time.Duration
41
+	externalCA          ExternalCAOption
41 42
 }
42 43
 
43 44
 // NodeAddrOption is a pflag.Value for listen and remote addresses
... ...
@@ -142,12 +146,102 @@ func NewAutoAcceptOption() AutoAcceptOption {
142 142
 	return AutoAcceptOption{values: make(map[string]bool)}
143 143
 }
144 144
 
145
+// ExternalCAOption is a Value type for parsing external CA specifications.
146
+type ExternalCAOption struct {
147
+	values []*swarm.ExternalCA
148
+}
149
+
150
+// Set parses an external CA option.
151
+func (m *ExternalCAOption) Set(value string) error {
152
+	parsed, err := parseExternalCA(value)
153
+	if err != nil {
154
+		return err
155
+	}
156
+
157
+	m.values = append(m.values, parsed)
158
+	return nil
159
+}
160
+
161
+// Type returns the type of this option.
162
+func (m *ExternalCAOption) Type() string {
163
+	return "external-ca"
164
+}
165
+
166
+// String returns a string repr of this option.
167
+func (m *ExternalCAOption) String() string {
168
+	externalCAs := []string{}
169
+	for _, externalCA := range m.values {
170
+		repr := fmt.Sprintf("%s: %s", externalCA.Protocol, externalCA.URL)
171
+		externalCAs = append(externalCAs, repr)
172
+	}
173
+	return strings.Join(externalCAs, ", ")
174
+}
175
+
176
+// Value returns the external CAs
177
+func (m *ExternalCAOption) Value() []*swarm.ExternalCA {
178
+	return m.values
179
+}
180
+
181
+// parseExternalCA parses an external CA specification from the command line,
182
+// such as protocol=cfssl,url=https://example.com.
183
+func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) {
184
+	csvReader := csv.NewReader(strings.NewReader(caSpec))
185
+	fields, err := csvReader.Read()
186
+	if err != nil {
187
+		return nil, err
188
+	}
189
+
190
+	externalCA := swarm.ExternalCA{
191
+		Options: make(map[string]string),
192
+	}
193
+
194
+	var (
195
+		hasProtocol bool
196
+		hasURL      bool
197
+	)
198
+
199
+	for _, field := range fields {
200
+		parts := strings.SplitN(field, "=", 2)
201
+
202
+		if len(parts) != 2 {
203
+			return nil, fmt.Errorf("invalid field '%s' must be a key=value pair", field)
204
+		}
205
+
206
+		key, value := parts[0], parts[1]
207
+
208
+		switch strings.ToLower(key) {
209
+		case "protocol":
210
+			hasProtocol = true
211
+			if strings.ToLower(value) == string(swarm.ExternalCAProtocolCFSSL) {
212
+				externalCA.Protocol = swarm.ExternalCAProtocolCFSSL
213
+			} else {
214
+				return nil, fmt.Errorf("unrecognized external CA protocol %s", value)
215
+			}
216
+		case "url":
217
+			hasURL = true
218
+			externalCA.URL = value
219
+		default:
220
+			externalCA.Options[key] = value
221
+		}
222
+	}
223
+
224
+	if !hasProtocol {
225
+		return nil, errors.New("the external-ca option needs a protocol= parameter")
226
+	}
227
+	if !hasURL {
228
+		return nil, errors.New("the external-ca option needs a url= parameter")
229
+	}
230
+
231
+	return &externalCA, nil
232
+}
233
+
145 234
 func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) {
146 235
 	flags.Var(&opts.autoAccept, flagAutoAccept, "Auto acceptance policy (worker, manager or none)")
147 236
 	flags.StringVar(&opts.secret, flagSecret, "", "Set secret value needed to accept nodes into cluster")
148 237
 	flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 10, "Task history retention limit")
149 238
 	flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period")
150 239
 	flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates")
240
+	flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints")
151 241
 }
152 242
 
153 243
 func (opts *swarmOptions) ToSpec() swarm.Spec {
... ...
@@ -160,5 +254,6 @@ func (opts *swarmOptions) ToSpec() swarm.Spec {
160 160
 	spec.Orchestration.TaskHistoryRetentionLimit = opts.taskHistoryLimit
161 161
 	spec.Dispatcher.HeartbeatPeriod = uint64(opts.dispatcherHeartbeat.Nanoseconds())
162 162
 	spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry
163
+	spec.CAConfig.ExternalCAs = opts.externalCA.Value()
163 164
 	return spec
164 165
 }
... ...
@@ -85,5 +85,10 @@ func mergeSwarm(swarm *swarm.Swarm, flags *pflag.FlagSet) error {
85 85
 		}
86 86
 	}
87 87
 
88
+	if flags.Changed(flagExternalCA) {
89
+		value := flags.Lookup(flagExternalCA).Value.(*ExternalCAOption)
90
+		spec.CAConfig.ExternalCAs = value.Value()
91
+	}
92
+
88 93
 	return nil
89 94
 }
... ...
@@ -35,6 +35,14 @@ func SwarmFromGRPC(c swarmapi.Cluster) types.Swarm {
35 35
 
36 36
 	swarm.Spec.CAConfig.NodeCertExpiry, _ = ptypes.Duration(c.Spec.CAConfig.NodeCertExpiry)
37 37
 
38
+	for _, ca := range c.Spec.CAConfig.ExternalCAs {
39
+		swarm.Spec.CAConfig.ExternalCAs = append(swarm.Spec.CAConfig.ExternalCAs, &types.ExternalCA{
40
+			Protocol: types.ExternalCAProtocol(strings.ToLower(ca.Protocol.String())),
41
+			URL:      ca.URL,
42
+			Options:  ca.Options,
43
+		})
44
+	}
45
+
38 46
 	// Meta
39 47
 	swarm.Version.Index = c.Meta.Version.Index
40 48
 	swarm.CreatedAt, _ = ptypes.Timestamp(c.Meta.CreatedAt)
... ...
@@ -84,6 +92,18 @@ func SwarmSpecToGRPCandMerge(s types.Spec, existingSpec *swarmapi.ClusterSpec) (
84 84
 		},
85 85
 	}
86 86
 
87
+	for _, ca := range s.CAConfig.ExternalCAs {
88
+		protocol, ok := swarmapi.ExternalCA_CAProtocol_value[strings.ToUpper(string(ca.Protocol))]
89
+		if !ok {
90
+			return swarmapi.ClusterSpec{}, fmt.Errorf("invalid protocol: %q", ca.Protocol)
91
+		}
92
+		spec.CAConfig.ExternalCAs = append(spec.CAConfig.ExternalCAs, &swarmapi.ExternalCA{
93
+			Protocol: swarmapi.ExternalCA_CAProtocol(protocol),
94
+			URL:      ca.URL,
95
+			Options:  ca.Options,
96
+		})
97
+	}
98
+
87 99
 	if err := SwarmSpecUpdateAcceptancePolicy(&spec, s.AcceptancePolicy, existingSpec); err != nil {
88 100
 		return swarmapi.ClusterSpec{}, err
89 101
 	}