Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
(cherry picked from commit 11085b2260a78b3248f3e98e0a1e3203431fae22)
| ... | ... |
@@ -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 |
} |