From b0401a71f7479a52b2670346e0d1e97cb68089a4 Mon Sep 17 00:00:00 2001 From: Ying Li Date: Tue, 25 Apr 2017 15:40:46 -0700 Subject: [PATCH] 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 --- api/swagger.yaml | 3 ++ api/types/swarm/swarm.go | 4 ++ cli/command/swarm/opts.go | 11 ++++++ daemon/cluster/convert/swarm.go | 2 + integration-cli/docker_api_swarm_test.go | 12 ++++-- integration-cli/docker_cli_swarm_test.go | 48 +++++++++++++++++++++--- 6 files changed, 70 insertions(+), 10 deletions(-) diff --git a/api/swagger.yaml b/api/swagger.yaml index 01f9d5d233..12ae9d3720 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1835,6 +1835,9 @@ definitions: type: "object" additionalProperties: type: "string" + CACert: + 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)." + type: "string" EncryptionConfig: description: "Parameters related to encryption-at-rest." type: "object" diff --git a/api/types/swarm/swarm.go b/api/types/swarm/swarm.go index c513274750..c989e15725 100644 --- a/api/types/swarm/swarm.go +++ b/api/types/swarm/swarm.go @@ -126,6 +126,10 @@ type ExternalCA struct { // Options is a set of additional key/value pairs whose interpretation // depends on the specified CA type. Options map[string]string `json:",omitempty"` + + // CACert specifies which root CA is used by this external CA. This certificate must + // be in PEM format. + CACert string } // InitRequest is the request used to init a swarm. diff --git a/cli/command/swarm/opts.go b/cli/command/swarm/opts.go index 6eddddccae..75b92d49c3 100644 --- a/cli/command/swarm/opts.go +++ b/cli/command/swarm/opts.go @@ -2,7 +2,9 @@ package swarm import ( "encoding/csv" + "encoding/pem" "fmt" + "io/ioutil" "strings" "time" @@ -155,6 +157,15 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { case "url": hasURL = true externalCA.URL = value + case "cacert": + cacontents, err := ioutil.ReadFile(value) + if err != nil { + return nil, errors.Wrap(err, "unable to read CA cert for external CA") + } + if pemBlock, _ := pem.Decode(cacontents); pemBlock == nil { + return nil, errors.New("CA cert for external CA must be in PEM format") + } + externalCA.CACert = string(cacontents) default: externalCA.Options[key] = value } diff --git a/daemon/cluster/convert/swarm.go b/daemon/cluster/convert/swarm.go index 98e0ce25e6..09121fc8ff 100644 --- a/daemon/cluster/convert/swarm.go +++ b/daemon/cluster/convert/swarm.go @@ -47,6 +47,7 @@ func SwarmFromGRPC(c swarmapi.Cluster) types.Swarm { Protocol: types.ExternalCAProtocol(strings.ToLower(ca.Protocol.String())), URL: ca.URL, Options: ca.Options, + CACert: string(ca.CACert), }) } @@ -112,6 +113,7 @@ func MergeSwarmSpecToGRPC(s types.Spec, spec swarmapi.ClusterSpec) (swarmapi.Clu Protocol: swarmapi.ExternalCA_CAProtocol(protocol), URL: ca.URL, Options: ca.Options, + CACert: []byte(ca.CACert), }) } diff --git a/integration-cli/docker_api_swarm_test.go b/integration-cli/docker_api_swarm_test.go index 7b131000c7..ac3e1e538e 100644 --- a/integration-cli/docker_api_swarm_test.go +++ b/integration-cli/docker_api_swarm_test.go @@ -146,9 +146,6 @@ func (s *DockerSwarmSuite) TestAPISwarmJoinToken(c *check.C) { } func (s *DockerSwarmSuite) TestUpdateSwarmAddExternalCA(c *check.C) { - // TODO: when root rotation is in, convert to a series of root rotation tests instead. - // currently just makes sure that we don't have to provide a CA certificate when - // providing an external CA d1 := s.AddDaemon(c, false, false) c.Assert(d1.Init(swarm.InitRequest{}), checker.IsNil) d1.UpdateSwarm(c, func(s *swarm.Spec) { @@ -157,11 +154,18 @@ func (s *DockerSwarmSuite) TestUpdateSwarmAddExternalCA(c *check.C) { Protocol: swarm.ExternalCAProtocolCFSSL, URL: "https://thishasnoca.org", }, + { + Protocol: swarm.ExternalCAProtocolCFSSL, + URL: "https://thishasacacert.org", + CACert: "cacert", + }, } }) info, err := d1.SwarmInfo() c.Assert(err, checker.IsNil) - c.Assert(info.Cluster.Spec.CAConfig.ExternalCAs, checker.HasLen, 1) + c.Assert(info.Cluster.Spec.CAConfig.ExternalCAs, checker.HasLen, 2) + c.Assert(info.Cluster.Spec.CAConfig.ExternalCAs[0].CACert, checker.Equals, "") + c.Assert(info.Cluster.Spec.CAConfig.ExternalCAs[1].CACert, checker.Equals, "cacert") } func (s *DockerSwarmSuite) TestAPISwarmCAHash(c *check.C) { diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index 5d79ee9176..c32f9cf52c 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/integration-cli/docker_cli_swarm_test.go @@ -23,6 +23,7 @@ import ( "github.com/docker/docker/integration-cli/daemon" "github.com/docker/docker/pkg/testutil" icmd "github.com/docker/docker/pkg/testutil/cmd" + "github.com/docker/docker/pkg/testutil/tempfile" "github.com/docker/libnetwork/driverapi" "github.com/docker/libnetwork/ipamapi" remoteipam "github.com/docker/libnetwork/ipams/remote/api" @@ -53,11 +54,29 @@ func (s *DockerSwarmSuite) TestSwarmUpdate(c *check.C) { c.Assert(spec.CAConfig.NodeCertExpiry, checker.Equals, 30*time.Hour) // passing an external CA (this is without starting a root rotation) does not fail - out, err = d.Cmd("swarm", "update", "--external-ca", "protocol=cfssl,url=https://something.org") - c.Assert(err, checker.IsNil, check.Commentf("out: %v", out)) + cli.Docker(cli.Args("swarm", "update", "--external-ca", "protocol=cfssl,url=https://something.org", + "--external-ca", "protocol=cfssl,url=https://somethingelse.org,cacert=fixtures/https/ca.pem"), + cli.Daemon(d.Daemon)).Assert(c, icmd.Success) + + expected, err := ioutil.ReadFile("fixtures/https/ca.pem") + c.Assert(err, checker.IsNil) spec = getSpec() - c.Assert(spec.CAConfig.ExternalCAs, checker.HasLen, 1) + c.Assert(spec.CAConfig.ExternalCAs, checker.HasLen, 2) + c.Assert(spec.CAConfig.ExternalCAs[0].CACert, checker.Equals, "") + c.Assert(spec.CAConfig.ExternalCAs[1].CACert, checker.Equals, string(expected)) + + // passing an invalid external CA fails + tempFile := tempfile.NewTempFile(c, "testfile", "fakecert") + defer tempFile.Remove() + + result := cli.Docker(cli.Args("swarm", "update", + "--external-ca", fmt.Sprintf("protocol=cfssl,url=https://something.org,cacert=%s", tempFile.Name())), + cli.Daemon(d.Daemon)) + result.Assert(c, icmd.Expected{ + ExitCode: 125, + Err: "must be in PEM format", + }) } func (s *DockerSwarmSuite) TestSwarmInit(c *check.C) { @@ -68,17 +87,34 @@ func (s *DockerSwarmSuite) TestSwarmInit(c *check.C) { return sw.Spec } + // passing an invalid external CA fails + tempFile := tempfile.NewTempFile(c, "testfile", "fakecert") + defer tempFile.Remove() + + result := cli.Docker(cli.Args("swarm", "init", "--cert-expiry", "30h", "--dispatcher-heartbeat", "11s", + "--external-ca", fmt.Sprintf("protocol=cfssl,url=https://somethingelse.org,cacert=%s", tempFile.Name())), + cli.Daemon(d.Daemon)) + result.Assert(c, icmd.Expected{ + ExitCode: 125, + Err: "must be in PEM format", + }) + cli.Docker(cli.Args("swarm", "init", "--cert-expiry", "30h", "--dispatcher-heartbeat", "11s", - "--external-ca", "protocol=cfssl,url=https://something.org"), + "--external-ca", "protocol=cfssl,url=https://something.org", + "--external-ca", "protocol=cfssl,url=https://somethingelse.org,cacert=fixtures/https/ca.pem"), cli.Daemon(d.Daemon)).Assert(c, icmd.Success) + expected, err := ioutil.ReadFile("fixtures/https/ca.pem") + c.Assert(err, checker.IsNil) + spec := getSpec() c.Assert(spec.CAConfig.NodeCertExpiry, checker.Equals, 30*time.Hour) c.Assert(spec.Dispatcher.HeartbeatPeriod, checker.Equals, 11*time.Second) - c.Assert(spec.CAConfig.ExternalCAs, checker.HasLen, 1) + c.Assert(spec.CAConfig.ExternalCAs, checker.HasLen, 2) + c.Assert(spec.CAConfig.ExternalCAs[0].CACert, checker.Equals, "") + c.Assert(spec.CAConfig.ExternalCAs[1].CACert, checker.Equals, string(expected)) c.Assert(d.Leave(true), checker.IsNil) - time.Sleep(500 * time.Millisecond) // https://github.com/docker/swarmkit/issues/1421 cli.Docker(cli.Args("swarm", "init"), cli.Daemon(d.Daemon)).Assert(c, icmd.Success) spec = getSpec()