Browse code

Add the CACert parameter to the ExternalCA object in order to match swarmkit's API type. Make sure this parameter gets propagated to swarmkit, and also add an extra option to the CLI when providing external CAs to parse the CA cert from a file.

Signed-off-by: Ying Li <ying.li@docker.com>

Ying Li authored on 2017/04/26 07:40:46
Showing 6 changed files
... ...
@@ -1835,6 +1835,9 @@ definitions:
1835 1835
                   type: "object"
1836 1836
                   additionalProperties:
1837 1837
                     type: "string"
1838
+                CACert:
1839
+                  description: "The root CA certificate (in PEM format) this external CA uses to issue TLS certificates (assumed to be to the current swarm root CA certificate if not provided)."
1840
+                  type: "string"
1838 1841
       EncryptionConfig:
1839 1842
         description: "Parameters related to encryption-at-rest."
1840 1843
         type: "object"
... ...
@@ -126,6 +126,10 @@ type ExternalCA struct {
126 126
 	// Options is a set of additional key/value pairs whose interpretation
127 127
 	// depends on the specified CA type.
128 128
 	Options map[string]string `json:",omitempty"`
129
+
130
+	// CACert specifies which root CA is used by this external CA.  This certificate must
131
+	// be in PEM format.
132
+	CACert string
129 133
 }
130 134
 
131 135
 // InitRequest is the request used to init a swarm.
... ...
@@ -2,7 +2,9 @@ package swarm
2 2
 
3 3
 import (
4 4
 	"encoding/csv"
5
+	"encoding/pem"
5 6
 	"fmt"
7
+	"io/ioutil"
6 8
 	"strings"
7 9
 	"time"
8 10
 
... ...
@@ -155,6 +157,15 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) {
155 155
 		case "url":
156 156
 			hasURL = true
157 157
 			externalCA.URL = value
158
+		case "cacert":
159
+			cacontents, err := ioutil.ReadFile(value)
160
+			if err != nil {
161
+				return nil, errors.Wrap(err, "unable to read CA cert for external CA")
162
+			}
163
+			if pemBlock, _ := pem.Decode(cacontents); pemBlock == nil {
164
+				return nil, errors.New("CA cert for external CA must be in PEM format")
165
+			}
166
+			externalCA.CACert = string(cacontents)
158 167
 		default:
159 168
 			externalCA.Options[key] = value
160 169
 		}
... ...
@@ -47,6 +47,7 @@ func SwarmFromGRPC(c swarmapi.Cluster) types.Swarm {
47 47
 			Protocol: types.ExternalCAProtocol(strings.ToLower(ca.Protocol.String())),
48 48
 			URL:      ca.URL,
49 49
 			Options:  ca.Options,
50
+			CACert:   string(ca.CACert),
50 51
 		})
51 52
 	}
52 53
 
... ...
@@ -112,6 +113,7 @@ func MergeSwarmSpecToGRPC(s types.Spec, spec swarmapi.ClusterSpec) (swarmapi.Clu
112 112
 			Protocol: swarmapi.ExternalCA_CAProtocol(protocol),
113 113
 			URL:      ca.URL,
114 114
 			Options:  ca.Options,
115
+			CACert:   []byte(ca.CACert),
115 116
 		})
116 117
 	}
117 118
 
... ...
@@ -146,9 +146,6 @@ func (s *DockerSwarmSuite) TestAPISwarmJoinToken(c *check.C) {
146 146
 }
147 147
 
148 148
 func (s *DockerSwarmSuite) TestUpdateSwarmAddExternalCA(c *check.C) {
149
-	// TODO: when root rotation is in, convert to a series of root rotation tests instead.
150
-	// currently just makes sure that we don't have to provide a CA certificate when
151
-	// providing an external CA
152 149
 	d1 := s.AddDaemon(c, false, false)
153 150
 	c.Assert(d1.Init(swarm.InitRequest{}), checker.IsNil)
154 151
 	d1.UpdateSwarm(c, func(s *swarm.Spec) {
... ...
@@ -157,11 +154,18 @@ func (s *DockerSwarmSuite) TestUpdateSwarmAddExternalCA(c *check.C) {
157 157
 				Protocol: swarm.ExternalCAProtocolCFSSL,
158 158
 				URL:      "https://thishasnoca.org",
159 159
 			},
160
+			{
161
+				Protocol: swarm.ExternalCAProtocolCFSSL,
162
+				URL:      "https://thishasacacert.org",
163
+				CACert:   "cacert",
164
+			},
160 165
 		}
161 166
 	})
162 167
 	info, err := d1.SwarmInfo()
163 168
 	c.Assert(err, checker.IsNil)
164
-	c.Assert(info.Cluster.Spec.CAConfig.ExternalCAs, checker.HasLen, 1)
169
+	c.Assert(info.Cluster.Spec.CAConfig.ExternalCAs, checker.HasLen, 2)
170
+	c.Assert(info.Cluster.Spec.CAConfig.ExternalCAs[0].CACert, checker.Equals, "")
171
+	c.Assert(info.Cluster.Spec.CAConfig.ExternalCAs[1].CACert, checker.Equals, "cacert")
165 172
 }
166 173
 
167 174
 func (s *DockerSwarmSuite) TestAPISwarmCAHash(c *check.C) {
... ...
@@ -23,6 +23,7 @@ import (
23 23
 	"github.com/docker/docker/integration-cli/daemon"
24 24
 	"github.com/docker/docker/pkg/testutil"
25 25
 	icmd "github.com/docker/docker/pkg/testutil/cmd"
26
+	"github.com/docker/docker/pkg/testutil/tempfile"
26 27
 	"github.com/docker/libnetwork/driverapi"
27 28
 	"github.com/docker/libnetwork/ipamapi"
28 29
 	remoteipam "github.com/docker/libnetwork/ipams/remote/api"
... ...
@@ -53,11 +54,29 @@ func (s *DockerSwarmSuite) TestSwarmUpdate(c *check.C) {
53 53
 	c.Assert(spec.CAConfig.NodeCertExpiry, checker.Equals, 30*time.Hour)
54 54
 
55 55
 	// passing an external CA (this is without starting a root rotation) does not fail
56
-	out, err = d.Cmd("swarm", "update", "--external-ca", "protocol=cfssl,url=https://something.org")
57
-	c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
56
+	cli.Docker(cli.Args("swarm", "update", "--external-ca", "protocol=cfssl,url=https://something.org",
57
+		"--external-ca", "protocol=cfssl,url=https://somethingelse.org,cacert=fixtures/https/ca.pem"),
58
+		cli.Daemon(d.Daemon)).Assert(c, icmd.Success)
59
+
60
+	expected, err := ioutil.ReadFile("fixtures/https/ca.pem")
61
+	c.Assert(err, checker.IsNil)
58 62
 
59 63
 	spec = getSpec()
60
-	c.Assert(spec.CAConfig.ExternalCAs, checker.HasLen, 1)
64
+	c.Assert(spec.CAConfig.ExternalCAs, checker.HasLen, 2)
65
+	c.Assert(spec.CAConfig.ExternalCAs[0].CACert, checker.Equals, "")
66
+	c.Assert(spec.CAConfig.ExternalCAs[1].CACert, checker.Equals, string(expected))
67
+
68
+	// passing an invalid external CA fails
69
+	tempFile := tempfile.NewTempFile(c, "testfile", "fakecert")
70
+	defer tempFile.Remove()
71
+
72
+	result := cli.Docker(cli.Args("swarm", "update",
73
+		"--external-ca", fmt.Sprintf("protocol=cfssl,url=https://something.org,cacert=%s", tempFile.Name())),
74
+		cli.Daemon(d.Daemon))
75
+	result.Assert(c, icmd.Expected{
76
+		ExitCode: 125,
77
+		Err:      "must be in PEM format",
78
+	})
61 79
 }
62 80
 
63 81
 func (s *DockerSwarmSuite) TestSwarmInit(c *check.C) {
... ...
@@ -68,17 +87,34 @@ func (s *DockerSwarmSuite) TestSwarmInit(c *check.C) {
68 68
 		return sw.Spec
69 69
 	}
70 70
 
71
+	// passing an invalid external CA fails
72
+	tempFile := tempfile.NewTempFile(c, "testfile", "fakecert")
73
+	defer tempFile.Remove()
74
+
75
+	result := cli.Docker(cli.Args("swarm", "init", "--cert-expiry", "30h", "--dispatcher-heartbeat", "11s",
76
+		"--external-ca", fmt.Sprintf("protocol=cfssl,url=https://somethingelse.org,cacert=%s", tempFile.Name())),
77
+		cli.Daemon(d.Daemon))
78
+	result.Assert(c, icmd.Expected{
79
+		ExitCode: 125,
80
+		Err:      "must be in PEM format",
81
+	})
82
+
71 83
 	cli.Docker(cli.Args("swarm", "init", "--cert-expiry", "30h", "--dispatcher-heartbeat", "11s",
72
-		"--external-ca", "protocol=cfssl,url=https://something.org"),
84
+		"--external-ca", "protocol=cfssl,url=https://something.org",
85
+		"--external-ca", "protocol=cfssl,url=https://somethingelse.org,cacert=fixtures/https/ca.pem"),
73 86
 		cli.Daemon(d.Daemon)).Assert(c, icmd.Success)
74 87
 
88
+	expected, err := ioutil.ReadFile("fixtures/https/ca.pem")
89
+	c.Assert(err, checker.IsNil)
90
+
75 91
 	spec := getSpec()
76 92
 	c.Assert(spec.CAConfig.NodeCertExpiry, checker.Equals, 30*time.Hour)
77 93
 	c.Assert(spec.Dispatcher.HeartbeatPeriod, checker.Equals, 11*time.Second)
78
-	c.Assert(spec.CAConfig.ExternalCAs, checker.HasLen, 1)
94
+	c.Assert(spec.CAConfig.ExternalCAs, checker.HasLen, 2)
95
+	c.Assert(spec.CAConfig.ExternalCAs[0].CACert, checker.Equals, "")
96
+	c.Assert(spec.CAConfig.ExternalCAs[1].CACert, checker.Equals, string(expected))
79 97
 
80 98
 	c.Assert(d.Leave(true), checker.IsNil)
81
-	time.Sleep(500 * time.Millisecond) // https://github.com/docker/swarmkit/issues/1421
82 99
 	cli.Docker(cli.Args("swarm", "init"), cli.Daemon(d.Daemon)).Assert(c, icmd.Success)
83 100
 
84 101
 	spec = getSpec()