DockerCLI/cli/command/swarm/ca_test.go

303 lines
8.2 KiB
Go

package swarm
import (
"bytes"
"io"
"os"
"testing"
"time"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/swarm"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
const (
cert = `
-----BEGIN CERTIFICATE-----
MIIBuDCCAV4CCQDOqUYOWdqMdjAKBggqhkjOPQQDAzBjMQswCQYDVQQGEwJVUzEL
MAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNVBAoMBkRv
Y2tlcjEPMA0GA1UECwwGRG9ja2VyMQ0wCwYDVQQDDARUZXN0MCAXDTE4MDcwMjIx
MjkxOFoYDzMwMTcxMTAyMjEyOTE4WjBjMQswCQYDVQQGEwJVUzELMAkGA1UECAwC
Q0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNVBAoMBkRvY2tlcjEPMA0G
A1UECwwGRG9ja2VyMQ0wCwYDVQQDDARUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
AQcDQgAEgvvZl5Vqpr1e+g5IhoU6TZHgRau+BZETVFTmqyWYajA/mooRQ1MZTozu
s9ZZZA8tzUhIqS36gsFuyIZ4YiAlyjAKBggqhkjOPQQDAwNIADBFAiBQ7pCPQrj8
8zaItMf0pk8j1NU5XrFqFEZICzvjzUJQBAIhAKq2gFwoTn8KH+cAAXZpAGJPmOsT
zsBT8gBAOHhNA6/2
-----END CERTIFICATE-----`
key = `
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEICyheZpw70pbgO4hEuwhZTETWyTpNJmJ3TyFaWT6WTRkoAoGCCqGSM49
AwEHoUQDQgAEgvvZl5Vqpr1e+g5IhoU6TZHgRau+BZETVFTmqyWYajA/mooRQ1MZ
Tozus9ZZZA8tzUhIqS36gsFuyIZ4YiAlyg==
-----END EC PRIVATE KEY-----`
)
func swarmSpecWithFullCAConfig() *swarm.Spec {
return &swarm.Spec{
CAConfig: swarm.CAConfig{
SigningCACert: "cacert",
SigningCAKey: "cakey",
ForceRotate: 1,
NodeCertExpiry: time.Duration(200),
ExternalCAs: []*swarm.ExternalCA{
{
URL: "https://example.com/ca",
Protocol: swarm.ExternalCAProtocolCFSSL,
CACert: "excacert",
},
},
},
}
}
func TestDisplayTrustRootNoRoot(t *testing.T) {
buffer := new(bytes.Buffer)
err := displayTrustRoot(buffer, swarm.Swarm{})
assert.Error(t, err, "No CA information available")
}
type invalidCATestCases struct {
args []string
errorMsg string
}
func writeFile(data string) (string, error) {
tmpfile, err := os.CreateTemp("", "testfile")
if err != nil {
return "", err
}
_, err = tmpfile.Write([]byte(data))
if err != nil {
return "", err
}
return tmpfile.Name(), tmpfile.Close()
}
func TestDisplayTrustRootInvalidFlags(t *testing.T) {
// we need an actual PEMfile to test
tmpfile, err := writeFile(cert)
assert.NilError(t, err)
t.Cleanup(func() { _ = os.Remove(tmpfile) })
errorTestCases := []invalidCATestCases{
{
args: []string{"--ca-cert=" + tmpfile},
errorMsg: "flag requires the `--rotate` flag to update the CA",
},
{
args: []string{"--ca-key=" + tmpfile},
errorMsg: "flag requires the `--rotate` flag to update the CA",
},
{ // to make sure we're not erroring because we didn't provide a CA key along with the CA cert
args: []string{
"--ca-cert=" + tmpfile,
"--ca-key=" + tmpfile,
},
errorMsg: "flag requires the `--rotate` flag to update the CA",
},
{
args: []string{"--cert-expiry=2160h0m0s"},
errorMsg: "flag requires the `--rotate` flag to update the CA",
},
{
args: []string{"--external-ca=protocol=cfssl,url=https://some.example.com/https/url"},
errorMsg: "flag requires the `--rotate` flag to update the CA",
},
{ // to make sure we're not erroring because we didn't provide a CA cert and external CA
args: []string{
"--ca-cert=" + tmpfile,
"--external-ca=protocol=cfssl,url=https://some.example.com/https/url",
},
errorMsg: "flag requires the `--rotate` flag to update the CA",
},
{
args: []string{
"--rotate",
"--external-ca=protocol=cfssl,url=https://some.example.com/https/url",
},
errorMsg: "rotating to an external CA requires the `--ca-cert` flag to specify the external CA's cert - " +
"to add an external CA with the current root CA certificate, use the `update` command instead",
},
{
args: []string{
"--rotate",
"--ca-cert=" + tmpfile,
},
errorMsg: "the --ca-cert flag requires that a --ca-key flag and/or --external-ca flag be provided as well",
},
}
for _, testCase := range errorTestCases {
cmd := newCACommand(
test.NewFakeCli(&fakeClient{
swarmInspectFunc: func() (swarm.Swarm, error) {
return swarm.Swarm{
ClusterInfo: swarm.ClusterInfo{
TLSInfo: swarm.TLSInfo{
TrustRoot: "root",
},
},
}, nil
},
}))
assert.Check(t, cmd.Flags().Parse(testCase.args))
cmd.SetOut(io.Discard)
assert.ErrorContains(t, cmd.Execute(), testCase.errorMsg)
}
}
func TestDisplayTrustRoot(t *testing.T) {
buffer := new(bytes.Buffer)
trustRoot := "trustme"
err := displayTrustRoot(buffer, swarm.Swarm{
ClusterInfo: swarm.ClusterInfo{
TLSInfo: swarm.TLSInfo{TrustRoot: trustRoot},
},
})
assert.NilError(t, err)
assert.Check(t, is.Equal(trustRoot+"\n", buffer.String()))
}
type swarmUpdateRecorder struct {
spec swarm.Spec
}
func (s *swarmUpdateRecorder) swarmUpdate(sp swarm.Spec, _ swarm.UpdateFlags) error {
s.spec = sp
return nil
}
func swarmInspectFuncWithFullCAConfig() (swarm.Swarm, error) {
return swarm.Swarm{
ClusterInfo: swarm.ClusterInfo{
Spec: *swarmSpecWithFullCAConfig(),
},
}, nil
}
func TestUpdateSwarmSpecDefaultRotate(t *testing.T) {
s := &swarmUpdateRecorder{}
cli := test.NewFakeCli(&fakeClient{
swarmInspectFunc: swarmInspectFuncWithFullCAConfig,
swarmUpdateFunc: s.swarmUpdate,
})
cmd := newCACommand(cli)
cmd.SetArgs([]string{"--rotate", "--detach"})
cmd.SetOut(cli.OutBuffer())
assert.NilError(t, cmd.Execute())
expected := swarmSpecWithFullCAConfig()
expected.CAConfig.ForceRotate = 2
expected.CAConfig.SigningCACert = ""
expected.CAConfig.SigningCAKey = ""
assert.Check(t, is.DeepEqual(*expected, s.spec))
}
func TestUpdateSwarmSpecCertAndKey(t *testing.T) {
certfile, err := writeFile(cert)
assert.NilError(t, err)
defer os.Remove(certfile)
keyfile, err := writeFile(key)
assert.NilError(t, err)
defer os.Remove(keyfile)
s := &swarmUpdateRecorder{}
cli := test.NewFakeCli(&fakeClient{
swarmInspectFunc: swarmInspectFuncWithFullCAConfig,
swarmUpdateFunc: s.swarmUpdate,
})
cmd := newCACommand(cli)
cmd.SetArgs([]string{
"--rotate",
"--detach",
"--ca-cert=" + certfile,
"--ca-key=" + keyfile,
"--cert-expiry=3m",
})
cmd.SetOut(cli.OutBuffer())
assert.NilError(t, cmd.Execute())
expected := swarmSpecWithFullCAConfig()
expected.CAConfig.SigningCACert = cert
expected.CAConfig.SigningCAKey = key
expected.CAConfig.NodeCertExpiry = 3 * time.Minute
assert.Check(t, is.DeepEqual(*expected, s.spec))
}
func TestUpdateSwarmSpecCertAndExternalCA(t *testing.T) {
certfile, err := writeFile(cert)
assert.NilError(t, err)
defer os.Remove(certfile)
s := &swarmUpdateRecorder{}
cli := test.NewFakeCli(&fakeClient{
swarmInspectFunc: swarmInspectFuncWithFullCAConfig,
swarmUpdateFunc: s.swarmUpdate,
})
cmd := newCACommand(cli)
cmd.SetArgs([]string{
"--rotate",
"--detach",
"--ca-cert=" + certfile,
"--external-ca=protocol=cfssl,url=https://some.external.ca.example.com",
})
cmd.SetOut(cli.OutBuffer())
assert.NilError(t, cmd.Execute())
expected := swarmSpecWithFullCAConfig()
expected.CAConfig.SigningCACert = cert
expected.CAConfig.SigningCAKey = ""
expected.CAConfig.ExternalCAs = []*swarm.ExternalCA{
{
Protocol: swarm.ExternalCAProtocolCFSSL,
URL: "https://some.external.ca.example.com",
CACert: cert,
Options: make(map[string]string),
},
}
assert.Check(t, is.DeepEqual(*expected, s.spec))
}
func TestUpdateSwarmSpecCertAndKeyAndExternalCA(t *testing.T) {
certfile, err := writeFile(cert)
assert.NilError(t, err)
defer os.Remove(certfile)
keyfile, err := writeFile(key)
assert.NilError(t, err)
defer os.Remove(keyfile)
s := &swarmUpdateRecorder{}
cli := test.NewFakeCli(&fakeClient{
swarmInspectFunc: swarmInspectFuncWithFullCAConfig,
swarmUpdateFunc: s.swarmUpdate,
})
cmd := newCACommand(cli)
cmd.SetArgs([]string{
"--rotate",
"--detach",
"--ca-cert=" + certfile,
"--ca-key=" + keyfile,
"--external-ca=protocol=cfssl,url=https://some.external.ca.example.com",
})
cmd.SetOut(cli.OutBuffer())
assert.NilError(t, cmd.Execute())
expected := swarmSpecWithFullCAConfig()
expected.CAConfig.SigningCACert = cert
expected.CAConfig.SigningCAKey = key
expected.CAConfig.ExternalCAs = []*swarm.ExternalCA{
{
Protocol: swarm.ExternalCAProtocolCFSSL,
URL: "https://some.external.ca.example.com",
CACert: cert,
Options: make(map[string]string),
},
}
assert.Check(t, is.DeepEqual(*expected, s.spec))
}