Skip to content

Commit

Permalink
Add the CACert parameter to the ExternalCA object in order to match
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
cyli committed Apr 26, 2017
1 parent e1101b1 commit b0401a7
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 10 deletions.
3 changes: 3 additions & 0 deletions api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions api/types/swarm/swarm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions cli/command/swarm/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package swarm

import (
"encoding/csv"
"encoding/pem"
"fmt"
"io/ioutil"
"strings"
"time"

Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 2 additions & 0 deletions daemon/cluster/convert/swarm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
})
}

Expand Down Expand Up @@ -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),
})
}

Expand Down
12 changes: 8 additions & 4 deletions integration-cli/docker_api_swarm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
48 changes: 42 additions & 6 deletions integration-cli/docker_cli_swarm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand All @@ -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()
Expand Down

0 comments on commit b0401a7

Please sign in to comment.