From 3e5853cf899e4b0d29a9335834804691e77901e2 Mon Sep 17 00:00:00 2001 From: Luke Kysow <1034429+lkysow@users.noreply.github.com> Date: Fri, 1 May 2020 12:27:36 -0700 Subject: [PATCH] Add create-federation-secret command This command will be run as a Kubernetes Job via a Helm hook. It creates a Kubernetes secret that contains data needed by secondary datacenters to federate with the primary. To set up a secondary dc, users will export this secret from their primary and import it into secondaries. They will then reference the secret in their Helm config for secondaries. The command works with ACLs enabled/disabled and with gossip encryption enabled/disabled. The command only works when TLS is enabled because federation requires TLS be enabled. --- commands.go | 5 + go.mod | 8 - go.sum | 16 - subcommand/common/common.go | 13 + subcommand/common/test_util.go | 55 + .../create-federation-secret/command.go | 447 +++++++ .../create-federation-secret/command_test.go | 1105 +++++++++++++++++ .../get-consul-client-ca/command_test.go | 53 +- subcommand/server-acl-init/command.go | 7 +- .../server-acl-init/create_or_update.go | 3 +- subcommand/server-acl-init/servers.go | 3 +- 11 files changed, 1638 insertions(+), 77 deletions(-) create mode 100644 subcommand/common/common.go create mode 100644 subcommand/common/test_util.go create mode 100644 subcommand/create-federation-secret/command.go create mode 100644 subcommand/create-federation-secret/command_test.go diff --git a/commands.go b/commands.go index 51b5238459..cd0f825837 100644 --- a/commands.go +++ b/commands.go @@ -4,6 +4,7 @@ import ( "os" cmdACLInit "github.com/hashicorp/consul-k8s/subcommand/acl-init" + cmdCreateFederationSecret "github.com/hashicorp/consul-k8s/subcommand/create-federation-secret" cmdDeleteCompletedJob "github.com/hashicorp/consul-k8s/subcommand/delete-completed-job" cmdGetConsulClientCA "github.com/hashicorp/consul-k8s/subcommand/get-consul-client-ca" cmdInjectConnect "github.com/hashicorp/consul-k8s/subcommand/inject-connect" @@ -58,6 +59,10 @@ func init() { "version": func() (cli.Command, error) { return &cmdVersion.Command{UI: ui, Version: version.GetHumanVersion()}, nil }, + + "create-federation-secret": func() (cli.Command, error) { + return &cmdCreateFederationSecret.Command{UI: ui}, nil + }, } } diff --git a/go.mod b/go.mod index 5880edc473..6c52d0ce26 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,10 @@ module github.com/hashicorp/consul-k8s require ( - github.com/Microsoft/go-winio v0.4.11 // indirect - github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect github.com/cenkalti/backoff v2.1.1+incompatible - github.com/coredns/coredns v1.2.2 // indirect github.com/deckarep/golang-set v1.7.1 - github.com/docker/go-connections v0.4.0 // indirect - github.com/elazarl/go-bindata-assetfs v1.0.0 // indirect github.com/gogo/protobuf v1.3.1 // indirect github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 // indirect github.com/googleapis/gnostic v0.3.1 // indirect @@ -21,18 +16,15 @@ require ( github.com/hashicorp/go-hclog v0.12.0 github.com/hashicorp/go-multierror v1.0.0 github.com/hashicorp/golang-lru v0.5.3 // indirect - github.com/hashicorp/hil v0.0.0-20170627220502-fa9f258a9250 // indirect github.com/imdario/mergo v0.3.8 // indirect github.com/json-iterator/go v1.1.8 // indirect github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a github.com/mitchellh/cli v1.0.0 github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/hashstructure v1.0.0 // indirect github.com/onsi/ginkgo v1.10.3 // indirect github.com/onsi/gomega v1.7.1 // indirect github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 github.com/radovskyb/watcher v1.0.2 - github.com/shirou/gopsutil v2.17.12+incompatible // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.4.0 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 // indirect diff --git a/go.sum b/go.sum index b25980a1eb..6f48f0c177 100644 --- a/go.sum +++ b/go.sum @@ -11,13 +11,9 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/DataDog/datadog-go v2.2.0+incompatible h1:V5BKkxACZLjzHjSgBbr2gvLA2Ae49yhc6CSY7MLy5k4= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Microsoft/go-winio v0.4.3/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/NYTimes/gziphandler v1.0.1 h1:iLrQrdwjDd52kHDA5op2UBJFjmOb9g+7scBan4RN8F0= github.com/NYTimes/gziphandler v1.0.1/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= -github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f h1:5ZfJxyXo8KyX8DgGXC5B7ILL8y51fci/qYz2B4j8iLY= -github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af h1:DBNMBMuMiWYu0b+8KMJuWmfCkcxl09JwdlqwDZZ6U14= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= @@ -48,8 +44,6 @@ github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coredns/coredns v1.1.2/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0= -github.com/coredns/coredns v1.2.2 h1:SEMmU3wdSQW2iMCL6JaIkENTLDli3L2xZ9v7w2Yqfgw= -github.com/coredns/coredns v1.2.2/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -67,12 +61,8 @@ github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQ github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/elazarl/go-bindata-assetfs v0.0.0-20160803192304-e1a2a7ec64b0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= -github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk= -github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/envoyproxy/go-control-plane v0.8.0 h1:uE6Fp4fOcAJdc1wTQXLJ+SYistkbG1dNoi6Zs1+Ybvk= github.com/envoyproxy/go-control-plane v0.8.0/go.mod h1:GSSbY9P1neVhdY7G4wu+IK1rk/dqhiCC/4ExuWJZVuk= github.com/envoyproxy/protoc-gen-validate v0.0.14 h1:YBW6/cKy9prEGRYLnaGa4IDhzxZhRCtKsax8srGKDnM= @@ -189,8 +179,6 @@ github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hil v0.0.0-20160711231837-1e86c6b523c5/go.mod h1:KHvg/R2/dPtaePb16oW4qIyzkMxXOL38xjRN64adsts= -github.com/hashicorp/hil v0.0.0-20170627220502-fa9f258a9250 h1:fooK5IvDL/KIsi4LxF/JH68nVdrBSiGNPhS2JAQjtjo= -github.com/hashicorp/hil v0.0.0-20170627220502-fa9f258a9250/go.mod h1:KHvg/R2/dPtaePb16oW4qIyzkMxXOL38xjRN64adsts= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -284,8 +272,6 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/hashstructure v0.0.0-20170609045927-2bca23e0e452/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= -github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y= -github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= @@ -345,8 +331,6 @@ github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIH github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shirou/gopsutil v0.0.0-20181107111621-48177ef5f880/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/gopsutil v2.17.12+incompatible h1:FNbznluSK3DQggqiVw3wK/tFKJrKlLPBuQ+V8XkkCOc= -github.com/shirou/gopsutil v2.17.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s= diff --git a/subcommand/common/common.go b/subcommand/common/common.go new file mode 100644 index 0000000000..26f96b3a0f --- /dev/null +++ b/subcommand/common/common.go @@ -0,0 +1,13 @@ +// Package common holds code needed by multiple commands. +package common + +const ( + // ACLReplicationTokenName is the name used for the ACL replication policy and + // Kubernetes secret. It is consumed in both the server-acl-init and + // create-federation-secret commands and so lives in this common package. + ACLReplicationTokenName = "acl-replication" + + // ACLTokenSecretKey is the key that we store the ACL tokens in when we + // create Kubernetes secrets. + ACLTokenSecretKey = "token" +) diff --git a/subcommand/common/test_util.go b/subcommand/common/test_util.go new file mode 100644 index 0000000000..41e9d8b841 --- /dev/null +++ b/subcommand/common/test_util.go @@ -0,0 +1,55 @@ +package common + +import ( + "io/ioutil" + "os" + "testing" + "time" + + "github.com/hashicorp/consul-k8s/helper/cert" + "github.com/stretchr/testify/require" +) + +// GenerateServerCerts generates Consul CA +// and a server certificate and saves them to temp files. +// It returns file names in this order: +// CA certificate, server certificate, and server key. +// Note that it's the responsibility of the caller to +// remove the temporary files created by this function. +func GenerateServerCerts(t *testing.T) (string, string, string, func()) { + require := require.New(t) + + caFile, err := ioutil.TempFile("", "ca") + require.NoError(err) + + certFile, err := ioutil.TempFile("", "cert") + require.NoError(err) + + certKeyFile, err := ioutil.TempFile("", "key") + require.NoError(err) + + // Generate CA + signer, _, caCertPem, caCertTemplate, err := cert.GenerateCA("Consul Agent CA - Test") + require.NoError(err) + + // Generate Server Cert + name := "server.dc1.consul" + hosts := []string{name, "localhost", "127.0.0.1"} + certPem, keyPem, err := cert.GenerateCert(name, 1*time.Hour, caCertTemplate, signer, hosts) + require.NoError(err) + + // Write certs and key to files + _, err = caFile.WriteString(caCertPem) + require.NoError(err) + _, err = certFile.WriteString(certPem) + require.NoError(err) + _, err = certKeyFile.WriteString(keyPem) + require.NoError(err) + + cleanupFunc := func() { + os.Remove(caFile.Name()) + os.Remove(certFile.Name()) + os.Remove(certKeyFile.Name()) + } + return caFile.Name(), certFile.Name(), certKeyFile.Name(), cleanupFunc +} diff --git a/subcommand/create-federation-secret/command.go b/subcommand/create-federation-secret/command.go new file mode 100644 index 0000000000..0ecefc6eb1 --- /dev/null +++ b/subcommand/create-federation-secret/command.go @@ -0,0 +1,447 @@ +package createfederationsecret + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "io/ioutil" + "os" + "strings" + "sync" + "time" + + "github.com/cenkalti/backoff" + "github.com/hashicorp/consul-k8s/subcommand" + "github.com/hashicorp/consul-k8s/subcommand/common" + k8sflags "github.com/hashicorp/consul-k8s/subcommand/flags" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/go-hclog" + "github.com/mitchellh/cli" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +const ( + fedSecretGossipKey = "gossipEncryptionKey" + fedSecretCACertKey = "caCert" + fedSecretCAKeyKey = "caKey" + fedSecretServerConfigKey = "serverConfigJSON" + fedSecretReplicationTokenKey = "replicationToken" +) + +var retryInterval = 1 * time.Second + +type Command struct { + UI cli.Ui + flags *flag.FlagSet + k8s *k8sflags.K8SFlags + http *flags.HTTPFlags + + // flagExportReplicationToken controls whether we include the acl replication + // token in the secret. + flagExportReplicationToken bool + flagGossipKeyFile string + + // flagServerCACertFile is the location of the file containing the CA cert + // for servers. We also accept a -ca-file flag. This will point to a different + // file when auto-encrypt is enabled, otherwise it will point to the same file + // as -server-ca-cert-file. + // When auto-encrypt is enabled, the clients + // use a different CA than the servers (since they piggy-back on the Connect CA) + // and so when talking to our local client we need to use the CA cert passed + // via -ca-file, not the server CA. + flagServerCACertFile string + flagServerCAKeyFile string + flagResourcePrefix string + flagK8sNamespace string + flagLogLevel string + flagMeshGatewayServiceName string + + k8sClient kubernetes.Interface + consulClient *api.Client + + once sync.Once + help string +} + +func (c *Command) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.BoolVar(&c.flagExportReplicationToken, "export-replication-token", false, + "Set to true if the ACL replication token should be contained in the created secret. "+ + "If ACLs are enabled this should be set to true.") + c.flags.StringVar(&c.flagGossipKeyFile, "gossip-key-file", "", + "Location of a file containing the gossip encryption key. If not set, the created secret won't have a gossip encryption key.") + c.flags.StringVar(&c.flagServerCACertFile, "server-ca-cert-file", "", + "Location of a file containing the servers' CA certificate.") + c.flags.StringVar(&c.flagServerCAKeyFile, "server-ca-key-file", "", + "Location of a file containing the servers' CA signing key.") + c.flags.StringVar(&c.flagResourcePrefix, "resource-prefix", "", + "Prefix to use for Kubernetes resources. The created secret will be named '-federation'.") + c.flags.StringVar(&c.flagK8sNamespace, "k8s-namespace", "", + "Name of Kubernetes namespace where Consul is deployed.") + c.flags.StringVar(&c.flagMeshGatewayServiceName, "mesh-gateway-service-name", "", + "Name of the mesh gateway service registered into Consul.") + c.flags.StringVar(&c.flagLogLevel, "log-level", "info", + "Log verbosity level. Supported values (in order of detail) are \"trace\", "+ + "\"debug\", \"info\", \"warn\", and \"error\".") + + c.help = flags.Usage(help, c.flags) + c.http = &flags.HTTPFlags{} + c.k8s = &k8sflags.K8SFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.ServerFlags()) + flags.Merge(c.flags, c.k8s.Flags()) +} + +// Run creates a Kubernetes secret with data needed by secondary datacenters +// in order to federate with the primary. It's assumed this is running in the +// primary datacenter. +func (c *Command) Run(args []string) int { + c.once.Do(c.init) + + if err := c.validateFlags(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + // Create logger. + level := hclog.LevelFromString(c.flagLogLevel) + if level == hclog.NoLevel { + c.UI.Error(fmt.Sprintf("Unknown log level: %s", c.flagLogLevel)) + return 1 + } + logger := hclog.New(&hclog.LoggerOptions{ + Level: level, + Output: os.Stderr, + }) + + // The initial secret struct. We will be filling in its data map + // as we continue. + federationSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-federation", c.flagResourcePrefix), + Namespace: c.flagK8sNamespace, + }, + Type: "Opaque", + Data: make(map[string][]byte), + } + + // Add gossip encryption key if it exists. + if c.flagGossipKeyFile != "" { + logger.Info("Retrieving gossip encryption key data") + gossipKey, err := ioutil.ReadFile(c.flagGossipKeyFile) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading gossip encryption key file: %s", err)) + return 1 + } + if len(gossipKey) == 0 { + c.UI.Error(fmt.Sprintf("gossip key file %q was empty", c.flagGossipKeyFile)) + return 1 + } + federationSecret.Data[fedSecretGossipKey] = gossipKey + logger.Info("Gossip encryption key retrieved successfully") + } + + // Add server CA cert. + logger.Info("Retrieving server CA cert data") + caCert, err := ioutil.ReadFile(c.flagServerCACertFile) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading server CA cert file: %s", err)) + return 1 + } + federationSecret.Data[fedSecretCACertKey] = caCert + logger.Info("Server CA cert retrieved successfully") + + // Add server CA key. + logger.Info("Retrieving server CA key data") + caKey, err := ioutil.ReadFile(c.flagServerCAKeyFile) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading server CA key file: %s", err)) + return 1 + } + federationSecret.Data[fedSecretCAKeyKey] = caKey + logger.Info("Server CA key retrieved successfully") + + // Create the Kubernetes clientset. + if c.k8sClient == nil { + k8sCfg, err := subcommand.K8SConfig(c.k8s.KubeConfig()) + if err != nil { + c.UI.Error(fmt.Sprintf("Error retrieving Kubernetes auth: %s", err)) + return 1 + } + c.k8sClient, err = kubernetes.NewForConfig(k8sCfg) + if err != nil { + c.UI.Error(fmt.Sprintf("Error initializing Kubernetes client: %s", err)) + return 1 + } + } + + // Add replication token. + var replicationToken []byte + if c.flagExportReplicationToken { + var err error + replicationToken, err = c.replicationToken(logger) + if err != nil { + logger.Error("error retrieving replication token", "err", err) + return 1 + } + federationSecret.Data[fedSecretReplicationTokenKey] = replicationToken + } + + // Set up Consul client because we need to make calls to Consul to retrieve + // the datacenter name and mesh gateway addresses. + if c.consulClient == nil { + consulCfg := &api.Config{ + // Use the replication token for our ACL token. If ACLs are disabled, + // this will be empty which won't matter because ACLs are disabled. + Token: string(replicationToken), + } + // Merge our base config containing the optional ACL token with client + // config automatically parsed from the passed flags and environment + // variables. For example, when running in k8s the CONSUL_HTTP_ADDR environment + // variable will be set to the IP of the Consul client pod on the same + // node. + c.http.MergeOntoConfig(consulCfg) + + var err error + c.consulClient, err = api.NewClient(consulCfg) + if err != nil { + logger.Error("Error creating consul client", "err", err) + return 1 + } + } + + // Get the datacenter's name. We assume this is the primary datacenter + // because users should only be running this in the primary datacenter. + logger.Info("Retrieving datacenter name from Consul") + datacenter := c.consulDatacenter(logger) + logger.Info("Successfully retrieved datacenter name") + + // Get the mesh gateway addresses. + logger.Info("Retrieving mesh gateway addresses from Consul") + meshGWAddrs, err := c.meshGatewayAddrs(logger) + if err != nil { + logger.Error("Error looking up mesh gateways", "err", err) + return 1 + } + logger.Info("Found mesh gateway addresses", "addrs", strings.Join(meshGWAddrs, ",")) + + // Generate a JSON config from the datacenter and mesh gateway addresses + // that can be set as a config file by Consul servers in secondary datacenters. + serverCfg, err := c.serverCfg(datacenter, meshGWAddrs) + if err != nil { + logger.Error("Unable to create server config json", "err", err) + return 1 + } + federationSecret.Data[fedSecretServerConfigKey] = serverCfg + + // Now create the Kubernetes secret. + logger.Info("Creating/updating Kubernetes secret", "name", federationSecret.ObjectMeta.Name, "ns", c.flagK8sNamespace) + _, err = c.k8sClient.CoreV1().Secrets(c.flagK8sNamespace).Create(federationSecret) + if k8serrors.IsAlreadyExists(err) { + logger.Info("Secret already exists, updating instead") + _, err = c.k8sClient.CoreV1().Secrets(c.flagK8sNamespace).Update(federationSecret) + } + + if err != nil { + logger.Error("Error creating/updating federation secret", "err", err) + return 1 + } + logger.Info("Successfully created/updated federation secret", "name", federationSecret.ObjectMeta.Name, "ns", c.flagK8sNamespace) + return 0 +} + +func (c *Command) validateFlags(args []string) error { + if err := c.flags.Parse(args); err != nil { + return err + } + if len(c.flags.Args()) > 0 { + return errors.New("should have no non-flag arguments") + } + if c.flagResourcePrefix == "" { + return errors.New("-resource-prefix must be set") + } + if c.flagK8sNamespace == "" { + return errors.New("-k8s-namespace must be set") + } + if c.flagServerCACertFile == "" { + return errors.New("-server-ca-cert-file must be set") + } + if c.flagServerCAKeyFile == "" { + return errors.New("-server-ca-key-file must be set") + } + if c.flagMeshGatewayServiceName == "" { + return errors.New("-mesh-gateway-service-name must be set") + } + if err := c.validateCAFileFlag(); err != nil { + return err + } + return nil +} + +// replicationToken waits for the ACL replication token Kubernetes secret to +// be created and then returns it. +func (c *Command) replicationToken(logger hclog.Logger) ([]byte, error) { + secretName := fmt.Sprintf("%s-%s-acl-token", c.flagResourcePrefix, common.ACLReplicationTokenName) + logger.Info("Retrieving replication token from secret", "secret", secretName, "ns", c.flagK8sNamespace) + + var unrecoverableErr error + var token []byte + + // Run in a retry loop because the replication secret will only exist once + // ACL bootstrapping is complete. This can take some time because it + // requires all servers to be running and a leader elected. + // This will run forever but it's running as a Helm hook so Helm will timeout + // after a configurable time period. + backoff.Retry(func() error { + secret, err := c.k8sClient.CoreV1().Secrets(c.flagK8sNamespace).Get(secretName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + logger.Warn("secret not yet created, retrying", "secret", secretName, "ns", c.flagK8sNamespace) + return errors.New("") + } else if err != nil { + unrecoverableErr = err + return nil + } + var ok bool + token, ok = secret.Data[common.ACLTokenSecretKey] + if !ok { + // If the secret exists but it doesn't have the expected key then + // something must have gone wrong generating the secret and we + // can't recover from that. + unrecoverableErr = fmt.Errorf("expected key '%s' in secret %s not set", common.ACLTokenSecretKey, secretName) + return nil + } + return nil + }, backoff.NewConstantBackOff(retryInterval)) + + if unrecoverableErr != nil { + return nil, unrecoverableErr + } + logger.Info("Replication token retrieved successfully") + return token, nil +} + +// meshGatewayAddrs returns a list of unique WAN addresses for all service +// instances of the mesh-gateway service. +func (c *Command) meshGatewayAddrs(logger hclog.Logger) ([]string, error) { + var meshGWSvcs []*api.CatalogService + + // Run in a retry in case the mesh gateways haven't yet been registered. + backoff.Retry(func() error { + var err error + meshGWSvcs, _, err = c.consulClient.Catalog().Service(c.flagMeshGatewayServiceName, "", nil) + if err != nil { + logger.Error("Error looking up mesh gateways, retrying", "err", err) + return errors.New("") + } + if len(meshGWSvcs) < 1 { + logger.Error("No instances of mesh gateway service found, retrying", "service-name", c.flagMeshGatewayServiceName) + return errors.New("") + } + return nil + }, backoff.NewConstantBackOff(retryInterval)) + + // Use a map to collect the addresses to ensure uniqueness. + meshGatewayAddrs := make(map[string]bool) + for _, svc := range meshGWSvcs { + addr, ok := svc.ServiceTaggedAddresses["wan"] + if !ok { + return nil, fmt.Errorf("no 'wan' key found in tagged addresses for service instance %q", svc.ServiceID) + } + meshGatewayAddrs[fmt.Sprintf("%s:%d", addr.Address, addr.Port)] = true + } + var uniqMeshGatewayAddrs []string + for addr := range meshGatewayAddrs { + uniqMeshGatewayAddrs = append(uniqMeshGatewayAddrs, addr) + } + return uniqMeshGatewayAddrs, nil +} + +// serverCfg returns a JSON consul server config. +func (c *Command) serverCfg(datacenter string, gatewayAddrs []string) ([]byte, error) { + type serverConfig struct { + PrimaryDatacenter string `json:"primary_datacenter"` + PrimaryGateways []string `json:"primary_gateways"` + } + return json.Marshal(serverConfig{ + PrimaryDatacenter: datacenter, + PrimaryGateways: gatewayAddrs, + }) +} + +// consulDatacenter returns the current datacenter. +func (c *Command) consulDatacenter(logger hclog.Logger) string { + // withLog is a helper method we'll use in the retry loop below to ensure + // that errors are logged. + var withLog = func(fn func() error) func() error { + return func() error { + err := fn() + if err != nil { + logger.Error("Error retrieving current datacenter, retrying", "err", err) + } + return err + } + } + + // Run in a retry because the Consul clients may not be running yet. + var dc string + backoff.Retry(withLog(func() error { + agentCfg, err := c.consulClient.Agent().Self() + if err != nil { + return err + } + if _, ok := agentCfg["Config"]; !ok { + return fmt.Errorf("/agent/self response did not contain Config key: %s", agentCfg) + } + if _, ok := agentCfg["Config"]["Datacenter"]; !ok { + return fmt.Errorf("/agent/self response did not contain Config.Datacenter key: %s", agentCfg) + } + var ok bool + dc, ok = agentCfg["Config"]["Datacenter"].(string) + if !ok { + return fmt.Errorf("could not cast Config.Datacenter as string: %s", agentCfg) + } + if dc == "" { + return fmt.Errorf("value of Config.Datacenter was empty string: %s", agentCfg) + } + return nil + }), backoff.NewConstantBackOff(retryInterval)) + + return dc +} + +// validateCAFileFlag returns an error if the -ca-file flag (or its env var +// CONSUL_CACERT) isn't set or the file it points to can't be read. +func (c *Command) validateCAFileFlag() error { + cfg := api.DefaultConfig() + c.http.MergeOntoConfig(cfg) + if cfg.TLSConfig.CAFile == "" { + return errors.New("-ca-file or CONSUL_CACERT must be set") + } + _, err := ioutil.ReadFile(cfg.TLSConfig.CAFile) + if err != nil { + return fmt.Errorf("error reading CA file: %s", err) + } + return nil +} + +func (c *Command) Synopsis() string { return synopsis } +func (c *Command) Help() string { + c.once.Do(c.init) + return c.help +} + +const synopsis = "Create a Kubernetes secret containing data needed for federation" +const help = ` +Usage: consul-k8s create-federation-secret [options] + + Creates a Kubernetes secret that contains all the data required for a secondary + datacenter to federate with the primary. This command should only be run in the + primary datacenter. + +` diff --git a/subcommand/create-federation-secret/command_test.go b/subcommand/create-federation-secret/command_test.go new file mode 100644 index 0000000000..a699e192c0 --- /dev/null +++ b/subcommand/create-federation-secret/command_test.go @@ -0,0 +1,1105 @@ +package createfederationsecret + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/hashicorp/consul-k8s/subcommand/common" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/freeport" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestRun_FlagValidation(t *testing.T) { + t.Parallel() + f, err := ioutil.TempFile("", "") + require.NoError(t, err) + defer os.Remove(f.Name()) + + cases := []struct { + flags []string + expErr string + }{ + { + flags: nil, + expErr: "-resource-prefix must be set", + }, + { + flags: []string{"-resource-prefix=prefix"}, + expErr: "-k8s-namespace must be set", + }, + { + flags: []string{"-resource-prefix=prefix", "-k8s-namespace=default"}, + expErr: "-server-ca-cert-file must be set", + }, + { + flags: []string{"-resource-prefix=prefix", "-k8s-namespace=default", "-server-ca-cert-file=file"}, + expErr: "-server-ca-key-file must be set", + }, + { + flags: []string{"-resource-prefix=prefix", "-k8s-namespace=default", "-server-ca-cert-file=file", "-server-ca-key-file=file"}, + expErr: "-mesh-gateway-service-name must be set", + }, + { + flags: []string{"-resource-prefix=prefix", "-k8s-namespace=default", "-server-ca-cert-file=file", "-server-ca-key-file=file", "-mesh-gateway-service-name=mesh-gateway"}, + expErr: "-ca-file or CONSUL_CACERT must be set", + }, + { + flags: []string{ + "-resource-prefix=prefix", + "-k8s-namespace=default", + "-server-ca-cert-file=file", + "-server-ca-key-file=file", + "-ca-file", f.Name(), + "-mesh-gateway-service-name=name", + "-log-level=invalid", + }, + expErr: "Unknown log level: invalid", + }, + } + + for _, c := range cases { + t.Run(c.expErr, func(tt *testing.T) { + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + } + exitCode := cmd.Run(c.flags) + require.Equal(tt, 1, exitCode, ui.ErrorWriter.String()) + require.Contains(tt, ui.ErrorWriter.String(), c.expErr) + }) + } +} + +func TestRun_CAFileMissing(t *testing.T) { + t.Parallel() + f, err := ioutil.TempFile("", "") + require.NoError(t, err) + defer os.Remove(f.Name()) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + } + exitCode := cmd.Run([]string{ + "-resource-prefix=prefix", + "-k8s-namespace=default", + "-mesh-gateway-service-name=name", + "-server-ca-cert-file", f.Name(), + "-server-ca-key-file", f.Name(), + "-ca-file=/this/does/not/exist", + }) + require.Equal(t, 1, exitCode, ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "error reading CA file") +} + +func TestRun_ServerCACertFileMissing(t *testing.T) { + t.Parallel() + f, err := ioutil.TempFile("", "") + require.NoError(t, err) + defer os.Remove(f.Name()) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + } + exitCode := cmd.Run([]string{ + "-resource-prefix=prefix", + "-k8s-namespace=default", + "-mesh-gateway-service-name=name", + "-ca-file", f.Name(), + "-server-ca-cert-file=/this/does/not/exist", + "-server-ca-key-file", f.Name(), + }) + require.Equal(t, 1, exitCode, ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Error reading server CA cert file") +} + +func TestRun_ServerCAKeyFileMissing(t *testing.T) { + t.Parallel() + f, err := ioutil.TempFile("", "") + require.NoError(t, err) + defer os.Remove(f.Name()) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + } + exitCode := cmd.Run([]string{ + "-resource-prefix=prefix", + "-k8s-namespace=default", + "-mesh-gateway-service-name=name", + "-ca-file", f.Name(), + "-server-ca-cert-file", f.Name(), + "-server-ca-key-file=/this/does/not/exist", + }) + require.Equal(t, 1, exitCode, ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Error reading server CA key file") +} + +func TestRun_GossipEncryptionKeyFileMissing(t *testing.T) { + t.Parallel() + f, err := ioutil.TempFile("", "") + require.NoError(t, err) + defer os.Remove(f.Name()) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + } + exitCode := cmd.Run([]string{ + "-resource-prefix=prefix", + "-k8s-namespace=default", + "-mesh-gateway-service-name=name", + "-ca-file", f.Name(), + "-server-ca-cert-file", f.Name(), + "-server-ca-key-file", f.Name(), + "-gossip-key-file=/this/does/not/exist", + }) + require.Equal(t, 1, exitCode, ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Error reading gossip encryption key file") +} + +func TestRun_GossipEncryptionKeyFileEmpty(t *testing.T) { + t.Parallel() + f, err := ioutil.TempFile("", "") + require.NoError(t, err) + defer os.Remove(f.Name()) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + } + exitCode := cmd.Run([]string{ + "-resource-prefix=prefix", + "-k8s-namespace=default", + "-mesh-gateway-service-name=name", + "-ca-file", f.Name(), + "-server-ca-cert-file", f.Name(), + "-server-ca-key-file", f.Name(), + "-gossip-key-file", f.Name(), + }) + require.Equal(t, 1, exitCode, ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), fmt.Sprintf("gossip key file %q was empty", f.Name())) +} + +// Test when the replication secret exists but it's missing the expected +// token key, we return error. +func TestRun_ReplicationTokenMissingExpectedKey(t *testing.T) { + t.Parallel() + f, err := ioutil.TempFile("", "") + require.NoError(t, err) + defer os.Remove(f.Name()) + + ui := cli.NewMockUi() + k8s := fake.NewSimpleClientset() + k8sNS := "default" + k8s.CoreV1().Secrets(k8sNS).Create(&v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "prefix-" + common.ACLReplicationTokenName + "-acl-token", + }, + }) + cmd := Command{ + UI: ui, + k8sClient: k8s, + } + exitCode := cmd.Run([]string{ + "-resource-prefix=prefix", + "-k8s-namespace=default", + "-mesh-gateway-service-name=name", + "-ca-file", f.Name(), + "-server-ca-cert-file", f.Name(), + "-server-ca-key-file", f.Name(), + "-export-replication-token", + }) + require.Equal(t, 1, exitCode, ui.ErrorWriter.String()) +} + +// Our main test testing most permutations. +// Tests running with ACLs on/off, different kubernetes namespaces, with/without +// gossip key flag, different resource prefixes. +func TestRun_ACLs_K8SNamespaces_ResourcePrefixes(tt *testing.T) { + tt.Parallel() + + cases := map[string]struct { + // aclsEnabled will enable ACLs and also set the -export-replication-token + // flag because the helm chart won't allow this command to be run without + // that flag when ACLs are enabled. + aclsEnabled bool + // k8sNS is the kubernetes namespace. + k8sNS string + // resourcePrefix is passed into -resource-prefix. + resourcePrefix string + // gossipKey controls whether we pass -gossip-key-file flag and expect + // the output to contain the gossip key. + gossipKey bool + }{ + "acls disabled": { + aclsEnabled: false, + k8sNS: "default", + resourcePrefix: "prefix", + gossipKey: false, + }, + "acls disabled, gossip": { + aclsEnabled: false, + k8sNS: "default", + resourcePrefix: "prefix", + gossipKey: true, + }, + "acls enabled, gossip": { + aclsEnabled: true, + k8sNS: "default", + resourcePrefix: "prefix", + gossipKey: true, + }, + "acls disabled, k8sNS=other": { + aclsEnabled: false, + k8sNS: "other", + resourcePrefix: "prefix", + gossipKey: false, + }, + "acls enabled, k8sNS=other, gossip": { + aclsEnabled: true, + k8sNS: "other", + resourcePrefix: "prefix1", + gossipKey: true, + }, + // NOTE: Not testing gossip with different k8sNS because gossip key is + // mounted in as a file. + "acls disabled, resourcePrefix=other": { + aclsEnabled: false, + k8sNS: "default", + resourcePrefix: "other", + gossipKey: false, + }, + "acls enabled, resourcePrefix=other": { + aclsEnabled: true, + k8sNS: "default", + resourcePrefix: "other", + gossipKey: false, + }, + } + for name, c := range cases { + tt.Run(name, func(t *testing.T) { + + // Set up Consul server with TLS. + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) + defer cleanup() + a, err := testutil.NewTestServerConfigT(t, func(cfg *testutil.TestServerConfig) { + cfg.CAFile = caFile + cfg.CertFile = certFile + cfg.KeyFile = keyFile + if c.aclsEnabled { + cfg.ACL.Enabled = true + cfg.ACL.DefaultPolicy = "deny" + } + }) + require.NoError(t, err) + defer a.Stop() + + // Construct Consul client. + client, err := api.NewClient(&api.Config{ + Address: a.HTTPSAddr, + Scheme: "https", + TLSConfig: api.TLSConfig{ + CAFile: caFile, + }, + }) + require.NoError(t, err) + + // Bootstrap ACLs if enabled. + var replicationToken string + if c.aclsEnabled { + var bootstrapResp *api.ACLToken + timer := &retry.Timer{Timeout: 10 * time.Second, Wait: 500 * time.Millisecond} + // May need to retry bootstrapping until server has elected + // leader. + retry.RunWith(timer, t, func(r *retry.R) { + bootstrapResp, _, err = client.ACL().Bootstrap() + require.NoError(r, err) + }) + bootstrapToken := bootstrapResp.SecretID + require.NotEmpty(t, bootstrapToken) + + // Redefine the client with the bootstrap token set so + // subsequent calls will succeed. + client, err = api.NewClient(&api.Config{ + Address: a.HTTPSAddr, + Scheme: "https", + TLSConfig: api.TLSConfig{ + CAFile: caFile, + }, + Token: bootstrapToken, + }) + require.NoError(t, err) + + // Create a token for the replication policy. + _, _, err = client.ACL().PolicyCreate(&api.ACLPolicy{ + Name: "acl-replication-token", + Rules: replicationPolicy, + }, nil) + require.NoError(t, err) + + resp, _, err := client.ACL().TokenCreate(&api.ACLToken{ + Policies: []*api.ACLTokenPolicyLink{ + { + Name: "acl-replication-token", + }, + }, + }, nil) + require.NoError(t, err) + replicationToken = resp.SecretID + } + + // Create mesh gateway. + meshGWIP := "192.168.0.1" + meshGWPort := 443 + err = client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "mesh-gateway", + TaggedAddresses: map[string]api.ServiceAddress{ + "wan": { + Address: meshGWIP, + Port: meshGWPort, + }, + }, + }) + require.NoError(t, err) + + // Create fake k8s. + k8s := fake.NewSimpleClientset() + + // Create replication token secret if expected. + if c.aclsEnabled { + _, err := k8s.CoreV1().Secrets(c.k8sNS).Create(&v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.resourcePrefix + "-acl-replication-acl-token", + }, + Data: map[string][]byte{ + common.ACLTokenSecretKey: []byte(replicationToken), + }, + }) + require.NoError(t, err) + } + + // Create gossip encryption key if expected. + gossipEncryptionKey := "oGaLv60gQ0E+Uvn+Lokz9APjbu5fJaYx7kglOmg4jZc=" + var gossipKeyFile string + if c.gossipKey { + f, err := ioutil.TempFile("", "") + require.NoError(t, err) + err = ioutil.WriteFile(f.Name(), []byte(gossipEncryptionKey), 0644) + require.NoError(t, err) + gossipKeyFile = f.Name() + } + + // Run the command. + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + k8sClient: k8s, + } + flags := []string{ + "-resource-prefix", c.resourcePrefix, + "-k8s-namespace", c.k8sNS, + "-mesh-gateway-service-name=mesh-gateway", + "-ca-file", caFile, + "-server-ca-cert-file", caFile, + "-server-ca-key-file", keyFile, + "-http-addr", fmt.Sprintf("https://%s", a.HTTPSAddr), + } + if c.aclsEnabled { + flags = append(flags, "-export-replication-token") + } + if c.gossipKey { + flags = append(flags, "-gossip-key-file", gossipKeyFile) + } + exitCode := cmd.Run(flags) + require.Equal(t, 0, exitCode, ui.ErrorWriter.String()) + + // Check the secret is as expected. + secret, err := k8s.CoreV1().Secrets(c.k8sNS).Get(c.resourcePrefix+"-federation", metav1.GetOptions{}) + require.NoError(t, err) + + // CA Cert + require.Contains(t, secret.Data, "caCert") + caFileBytes, err := ioutil.ReadFile(caFile) + require.NoError(t, err) + require.Equal(t, string(caFileBytes), string(secret.Data["caCert"])) + + // CA Key + require.Contains(t, secret.Data, "caKey") + keyFileBytes, err := ioutil.ReadFile(keyFile) + require.NoError(t, err) + require.Equal(t, string(keyFileBytes), string(secret.Data["caKey"])) + + // Server Config + require.Contains(t, secret.Data, "serverConfigJSON") + expCfg := fmt.Sprintf(`{"primary_datacenter":"dc1","primary_gateways":["%s:%d"]}`, meshGWIP, meshGWPort) + require.Equal(t, expCfg, string(secret.Data["serverConfigJSON"])) + + // Replication Token + if c.aclsEnabled { + require.Contains(t, secret.Data, "replicationToken") + require.Equal(t, replicationToken, string(secret.Data["replicationToken"])) + } else { + require.NotContains(t, secret.Data, "replicationToken") + } + + // Gossip encryption key. + if c.gossipKey { + require.Contains(t, secret.Data, "gossipEncryptionKey") + require.Equal(t, gossipEncryptionKey, string(secret.Data["gossipEncryptionKey"])) + } else { + require.NotContains(t, secret.Data, "gossipEncryptionKey") + } + }) + } +} + +// Test when mesh gateway instances are delayed. +func TestRun_WaitsForMeshGatewayInstances(t *testing.T) { + // Create fake k8s. + k8s := fake.NewSimpleClientset() + + // Set up Consul server with TLS. + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) + defer cleanup() + a, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { + c.CAFile = caFile + c.CertFile = certFile + c.KeyFile = keyFile + }) + require.NoError(t, err) + defer a.Stop() + + // Create a mesh gateway instance after a delay. + meshGWIP := "192.168.0.1" + meshGWPort := 443 + go func() { + time.Sleep(500 * time.Millisecond) + client, err := api.NewClient(&api.Config{ + Address: a.HTTPSAddr, + Scheme: "https", + TLSConfig: api.TLSConfig{ + CAFile: caFile, + }, + }) + require.NoError(t, err) + err = client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "mesh-gateway", + TaggedAddresses: map[string]api.ServiceAddress{ + "wan": { + Address: meshGWIP, + Port: meshGWPort, + }, + }, + }) + require.NoError(t, err) + }() + + // Run the command. + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + k8sClient: k8s, + } + k8sNS := "default" + resourcePrefix := "prefix" + exitCode := cmd.Run([]string{ + "-resource-prefix", resourcePrefix, + "-k8s-namespace", k8sNS, + "-mesh-gateway-service-name=mesh-gateway", + "-ca-file", caFile, + "-server-ca-cert-file", certFile, + "-server-ca-key-file", keyFile, + "-http-addr", fmt.Sprintf("https://%s", a.HTTPSAddr), + }) + require.Equal(t, 0, exitCode, ui.ErrorWriter.String()) + + // Check the secret is as expected. + secret, err := k8s.CoreV1().Secrets(k8sNS).Get(resourcePrefix+"-federation", metav1.GetOptions{}) + require.NoError(t, err) + + // Test server config. + require.Contains(t, secret.Data, "serverConfigJSON") + expCfg := fmt.Sprintf(`{"primary_datacenter":"dc1","primary_gateways":["%s:%d"]}`, meshGWIP, meshGWPort) + require.Equal(t, expCfg, string(secret.Data["serverConfigJSON"])) +} + +// Test when the mesh gateways don't have a tagged address of name "wan". +func TestRun_MeshGatewayNoWANAddr(t *testing.T) { + t.Parallel() + + // Set up Consul server with TLS. + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) + defer cleanup() + a, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { + c.CAFile = caFile + c.CertFile = certFile + c.KeyFile = keyFile + }) + require.NoError(t, err) + defer a.Stop() + client, err := api.NewClient(&api.Config{ + Address: a.HTTPSAddr, + Scheme: "https", + TLSConfig: api.TLSConfig{ + CAFile: caFile, + }, + }) + require.NoError(t, err) + err = client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "mesh-gateway", + }) + require.NoError(t, err) + + ui := cli.NewMockUi() + k8s := fake.NewSimpleClientset() + cmd := Command{ + UI: ui, + k8sClient: k8s, + } + exitCode := cmd.Run([]string{ + "-resource-prefix=prefix", + "-k8s-namespace=default", + "-mesh-gateway-service-name=mesh-gateway", + "-ca-file", caFile, + "-server-ca-cert-file", caFile, + "-server-ca-key-file", keyFile, + "-http-addr", fmt.Sprintf("https://%s", a.HTTPSAddr), + }) + require.Equal(t, 1, exitCode, ui.ErrorWriter.String()) +} + +// Test that we only return unique addrs for the mesh gateways. +func TestRun_MeshGatewayUniqueAddrs(tt *testing.T) { + tt.Parallel() + + cases := []struct { + addrs []string + expAddrs []string + }{ + { + addrs: []string{"127.0.0.1:443"}, + expAddrs: []string{"127.0.0.1:443"}, + }, + { + addrs: []string{"127.0.0.1:443", "127.0.0.1:443"}, + expAddrs: []string{"127.0.0.1:443"}, + }, + { + addrs: []string{"127.0.0.1:443", "127.0.0.2:443", "127.0.0.1:443"}, + expAddrs: []string{"127.0.0.1:443", "127.0.0.2:443"}, + }, + { + addrs: []string{"127.0.0.1:443", "127.0.0.1:543", "127.0.0.1:443"}, + expAddrs: []string{"127.0.0.1:443", "127.0.0.1:543"}, + }, + } + for _, c := range cases { + tt.Run(strings.Join(c.addrs, ","), func(t *testing.T) { + // Create fake k8s. + k8s := fake.NewSimpleClientset() + + // Set up Consul server with TLS. + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) + defer cleanup() + a, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { + c.CAFile = caFile + c.CertFile = certFile + c.KeyFile = keyFile + }) + require.NoError(t, err) + defer a.Stop() + + // Create mesh gateway instances. + client, err := api.NewClient(&api.Config{ + Address: a.HTTPSAddr, + Scheme: "https", + TLSConfig: api.TLSConfig{ + CAFile: caFile, + }, + }) + require.NoError(t, err) + for i, addr := range c.addrs { + port, err := strconv.Atoi(strings.Split(addr, ":")[1]) + require.NoError(t, err) + err = client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "mesh-gateway", + ID: fmt.Sprintf("mesh-gateway-%d", i), + TaggedAddresses: map[string]api.ServiceAddress{ + "wan": { + Address: strings.Split(addr, ":")[0], + Port: port, + }, + }, + }) + } + require.NoError(t, err) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + k8sClient: k8s, + } + k8sNS := "default" + resourcePrefix := "prefix" + exitCode := cmd.Run([]string{ + "-resource-prefix", resourcePrefix, + "-k8s-namespace", k8sNS, + "-mesh-gateway-service-name=mesh-gateway", + "-ca-file", caFile, + "-server-ca-cert-file", caFile, + "-server-ca-key-file", keyFile, + "-http-addr", fmt.Sprintf("https://%s", a.HTTPSAddr), + }) + require.Equal(t, 0, exitCode, ui.ErrorWriter.String()) + + // Check the secret is as expected. + secret, err := k8s.CoreV1().Secrets(k8sNS).Get(resourcePrefix+"-federation", metav1.GetOptions{}) + require.NoError(t, err) + + // Server Config + require.Contains(t, secret.Data, "serverConfigJSON") + type ServerCfg struct { + PrimaryGateways []string `json:"primary_gateways"` + } + var cfg ServerCfg + err = json.Unmarshal(secret.Data["serverConfigJSON"], &cfg) + require.NoError(t, err) + require.ElementsMatch(t, cfg.PrimaryGateways, c.expAddrs) + }) + } +} + +// Test when the replication secret isn't created immediately. This mimics +// what happens in a regular installation because the replication secret doesn't +// get created until ACL bootstrapping is complete which can take a while since +// it requires the servers to all be up and a leader elected. +func TestRun_ReplicationSecretDelay(t *testing.T) { + t.Parallel() + + // Set up Consul server with TLS. + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) + defer cleanup() + a, err := testutil.NewTestServerConfigT(t, func(cfg *testutil.TestServerConfig) { + cfg.CAFile = caFile + cfg.CertFile = certFile + cfg.KeyFile = keyFile + cfg.ACL.Enabled = true + cfg.ACL.DefaultPolicy = "deny" + }) + require.NoError(t, err) + defer a.Stop() + + // Construct Consul client. + client, err := api.NewClient(&api.Config{ + Address: a.HTTPSAddr, + Scheme: "https", + TLSConfig: api.TLSConfig{ + CAFile: caFile, + }, + }) + require.NoError(t, err) + + // Bootstrap ACLs. We can do this before the command is started because + // the command retrieves the replication token from Kubernetes secret, i.e. + // that's the only thing that needs to be delayed. + var bootstrapResp *api.ACLToken + timer := &retry.Timer{Timeout: 10 * time.Second, Wait: 500 * time.Millisecond} + // May need to retry bootstrapping until server has elected + // leader. + retry.RunWith(timer, t, func(r *retry.R) { + bootstrapResp, _, err = client.ACL().Bootstrap() + require.NoError(r, err) + }) + bootstrapToken := bootstrapResp.SecretID + require.NotEmpty(t, bootstrapToken) + + // Redefine the client with the bootstrap token set so + // subsequent calls will succeed. + client, err = api.NewClient(&api.Config{ + Address: a.HTTPSAddr, + Scheme: "https", + TLSConfig: api.TLSConfig{ + CAFile: caFile, + }, + Token: bootstrapToken, + }) + require.NoError(t, err) + + // Create a token for the replication policy. + _, _, err = client.ACL().PolicyCreate(&api.ACLPolicy{ + Name: "acl-replication-policy", + Rules: replicationPolicy, + }, nil) + require.NoError(t, err) + + resp, _, err := client.ACL().TokenCreate(&api.ACLToken{ + Policies: []*api.ACLTokenPolicyLink{ + { + Name: "acl-replication-policy", + }, + }, + }, nil) + require.NoError(t, err) + replicationToken := resp.SecretID + + // Create mesh gateway. + meshGWIP := "192.168.0.1" + meshGWPort := 443 + err = client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "mesh-gateway", + TaggedAddresses: map[string]api.ServiceAddress{ + "wan": { + Address: meshGWIP, + Port: meshGWPort, + }, + }, + }) + require.NoError(t, err) + + // Create fake k8s. + k8s := fake.NewSimpleClientset() + + // Create replication token secret after a delay. + go func() { + time.Sleep(400 * time.Millisecond) + _, err := k8s.CoreV1().Secrets("default").Create(&v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "prefix-" + common.ACLReplicationTokenName + "-acl-token", + }, + Data: map[string][]byte{ + common.ACLTokenSecretKey: []byte(replicationToken), + }, + }) + require.NoError(t, err) + }() + + // Run the command. + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + k8sClient: k8s, + } + flags := []string{ + "-resource-prefix=prefix", + "-k8s-namespace=default", + "-mesh-gateway-service-name=mesh-gateway", + "-ca-file", caFile, + "-server-ca-cert-file", caFile, + "-server-ca-key-file", keyFile, + "-http-addr", fmt.Sprintf("https://%s", a.HTTPSAddr), + "-export-replication-token", + } + exitCode := cmd.Run(flags) + require.Equal(t, 0, exitCode, ui.ErrorWriter.String()) + + // Check the secret is as expected. + secret, err := k8s.CoreV1().Secrets("default").Get("prefix-federation", metav1.GetOptions{}) + require.NoError(t, err) + require.Contains(t, secret.Data, "replicationToken") + require.Equal(t, replicationToken, string(secret.Data["replicationToken"])) +} + +// Test that re-running the command updates the secret. In this test, we'll +// update the addresses of the mesh gateways. +func TestRun_UpdatesSecret(t *testing.T) { + t.Parallel() + + // Create fake k8s. + k8s := fake.NewSimpleClientset() + + // Set up Consul server with TLS. + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) + defer cleanup() + a, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { + c.CAFile = caFile + c.CertFile = certFile + c.KeyFile = keyFile + }) + require.NoError(t, err) + defer a.Stop() + + // Create a mesh gateway instance. + client, err := api.NewClient(&api.Config{ + Address: a.HTTPSAddr, + Scheme: "https", + TLSConfig: api.TLSConfig{ + CAFile: caFile, + }, + }) + require.NoError(t, err) + meshGWIP := "192.168.0.1" + meshGWPort := 443 + err = client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "mesh-gateway", + TaggedAddresses: map[string]api.ServiceAddress{ + "wan": { + Address: meshGWIP, + Port: meshGWPort, + }, + }, + }) + require.NoError(t, err) + + k8sNS := "default" + resourcePrefix := "prefix" + + // First run. + { + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + k8sClient: k8s, + } + exitCode := cmd.Run([]string{ + "-resource-prefix", resourcePrefix, + "-k8s-namespace", k8sNS, + "-mesh-gateway-service-name=mesh-gateway", + "-ca-file", caFile, + "-server-ca-cert-file", certFile, + "-server-ca-key-file", keyFile, + "-http-addr", fmt.Sprintf("https://%s", a.HTTPSAddr), + }) + require.Equal(t, 0, exitCode, ui.ErrorWriter.String()) + + // Check the secret is as expected. + secret, err := k8s.CoreV1().Secrets(k8sNS).Get(resourcePrefix+"-federation", metav1.GetOptions{}) + require.NoError(t, err) + + // Test server config. + require.Contains(t, secret.Data, "serverConfigJSON") + expCfg := fmt.Sprintf(`{"primary_datacenter":"dc1","primary_gateways":["%s:%d"]}`, meshGWIP, meshGWPort) + require.Equal(t, expCfg, string(secret.Data["serverConfigJSON"])) + } + + // Now re-run the command. + { + // Update the mesh gateway IP. + newMeshGWIP := "127.0.0.1" + err = client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "mesh-gateway", + TaggedAddresses: map[string]api.ServiceAddress{ + "wan": { + Address: newMeshGWIP, + Port: meshGWPort, + }, + }, + }) + require.NoError(t, err) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + k8sClient: k8s, + } + exitCode := cmd.Run([]string{ + "-resource-prefix", resourcePrefix, + "-k8s-namespace", k8sNS, + "-mesh-gateway-service-name=mesh-gateway", + "-ca-file", caFile, + "-server-ca-cert-file", caFile, + "-server-ca-key-file", keyFile, + "-http-addr", fmt.Sprintf("https://%s", a.HTTPSAddr), + }) + require.Equal(t, 0, exitCode, ui.ErrorWriter.String()) + + // Check the secret is as expected. + secret, err := k8s.CoreV1().Secrets(k8sNS).Get(resourcePrefix+"-federation", metav1.GetOptions{}) + require.NoError(t, err) + + // Test server config. The mesh gateway IP should be updated. + require.Contains(t, secret.Data, "serverConfigJSON") + expCfg := fmt.Sprintf(`{"primary_datacenter":"dc1","primary_gateways":["%s:%d"]}`, newMeshGWIP, meshGWPort) + require.Equal(t, expCfg, string(secret.Data["serverConfigJSON"])) + } +} + +// Test that if the Consul client isn't up yet we will retry until it is. +func TestRun_ConsulClientDelay(t *testing.T) { + t.Parallel() + + // We need to reserve all 6 ports to avoid potential + // port collisions with other tests. + randomPorts := freeport.MustTake(6) + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) + defer cleanup() + + // Create fake k8s. + k8s := fake.NewSimpleClientset() + + // Set up Consul server with TLS. Start after a 500ms delay. + var a *testutil.TestServer + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep(500 * time.Millisecond) + var err error + a, err = testutil.NewTestServerConfigT(t, func(cfg *testutil.TestServerConfig) { + cfg.CAFile = caFile + cfg.CertFile = certFile + cfg.KeyFile = keyFile + cfg.Ports = &testutil.TestPortConfig{ + DNS: randomPorts[0], + HTTP: randomPorts[1], + HTTPS: randomPorts[2], + SerfLan: randomPorts[3], + SerfWan: randomPorts[4], + Server: randomPorts[5], + } + }) + require.NoError(t, err) + + // Construct Consul client. + client, err := api.NewClient(&api.Config{ + Address: a.HTTPSAddr, + Scheme: "https", + TLSConfig: api.TLSConfig{ + CAFile: caFile, + }, + }) + require.NoError(t, err) + + // Create mesh gateway. + meshGWIP := "192.168.0.1" + meshGWPort := 443 + err = client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "mesh-gateway", + TaggedAddresses: map[string]api.ServiceAddress{ + "wan": { + Address: meshGWIP, + Port: meshGWPort, + }, + }, + }) + require.NoError(t, err) + }() + defer func() { + if a != nil { + a.Stop() + } + }() + + // Run the command. + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + k8sClient: k8s, + } + flags := []string{ + "-resource-prefix=prefix", + "-k8s-namespace=default", + "-mesh-gateway-service-name=mesh-gateway", + "-ca-file", caFile, + "-server-ca-cert-file", caFile, + "-server-ca-key-file", keyFile, + "-http-addr", fmt.Sprintf("https://127.0.0.1:%d", randomPorts[2]), + } + exitCode := cmd.Run(flags) + require.Equal(t, 0, exitCode, ui.ErrorWriter.String()) + + // Check the secret is as expected. + wg.Wait() + _, err := k8s.CoreV1().Secrets("default").Get("prefix-federation", metav1.GetOptions{}) + require.NoError(t, err) +} + +// Test that we use the -ca-file for our consul client and not the -server-ca-cert-file. +// If autoencrypt is enabled, the server CA won't work. +func TestRun_Autoencrypt(t *testing.T) { + t.Parallel() + + // Create fake k8s. + k8s := fake.NewSimpleClientset() + + // Set up Consul server with TLS. + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) + defer cleanup() + a, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { + c.CAFile = caFile + c.CertFile = certFile + c.KeyFile = keyFile + }) + require.NoError(t, err) + defer a.Stop() + + // Create a mesh gateway instance. + client, err := api.NewClient(&api.Config{ + Address: a.HTTPSAddr, + Scheme: "https", + TLSConfig: api.TLSConfig{ + CAFile: caFile, + }, + }) + require.NoError(t, err) + meshGWIP := "192.168.0.1" + meshGWPort := 443 + err = client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "mesh-gateway", + TaggedAddresses: map[string]api.ServiceAddress{ + "wan": { + Address: meshGWIP, + Port: meshGWPort, + }, + }, + }) + require.NoError(t, err) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + k8sClient: k8s, + } + k8sNS := "default" + resourcePrefix := "prefix" + exitCode := cmd.Run([]string{ + "-resource-prefix", resourcePrefix, + "-k8s-namespace", k8sNS, + "-mesh-gateway-service-name=mesh-gateway", + "-ca-file", caFile, + // Here we're passing in the key file which would fail the test if this + // was being used as the CA (since it's not a CA). + "-server-ca-cert-file", keyFile, + "-server-ca-key-file", keyFile, + "-http-addr", fmt.Sprintf("https://%s", a.HTTPSAddr), + }) + require.Equal(t, 0, exitCode, ui.ErrorWriter.String()) + + // Check the value of the server CA cert is the key file. + secret, err := k8s.CoreV1().Secrets(k8sNS).Get(resourcePrefix+"-federation", metav1.GetOptions{}) + require.NoError(t, err) + + require.Contains(t, secret.Data, "caCert") + keyFileBytes, err := ioutil.ReadFile(keyFile) + require.NoError(t, err) + require.Equal(t, string(keyFileBytes), string(secret.Data["caCert"])) +} + +var replicationPolicy = `acl = "write" +operator = "write" +agent_prefix "" { + policy = "read" +} +node_prefix "" { + policy = "write" +} +service_prefix "" { + policy = "read" + intentions = "read" +} +` diff --git a/subcommand/get-consul-client-ca/command_test.go b/subcommand/get-consul-client-ca/command_test.go index 86681fe891..75c50fb7b4 100644 --- a/subcommand/get-consul-client-ca/command_test.go +++ b/subcommand/get-consul-client-ca/command_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/consul-k8s/helper/cert" "github.com/hashicorp/consul-k8s/helper/go-discover/mocks" + "github.com/hashicorp/consul-k8s/subcommand/common" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/sdk/freeport" "github.com/hashicorp/consul/sdk/testutil" @@ -70,7 +71,7 @@ func TestRun(t *testing.T) { require.NoError(t, err) defer os.Remove(outputFile.Name()) - caFile, certFile, keyFile, cleanup := generateServerCerts(t) + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) defer cleanup() ui := cli.NewMockUi() @@ -132,7 +133,7 @@ func TestRun_ConsulServerAvailableLater(t *testing.T) { require.NoError(t, err) defer os.Remove(outputFile.Name()) - caFile, certFile, keyFile, cleanup := generateServerCerts(t) + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) defer cleanup() ui := cli.NewMockUi() @@ -219,7 +220,7 @@ func TestRun_GetsOnlyActiveRoot(t *testing.T) { require.NoError(t, err) defer os.Remove(outputFile.Name()) - caFile, certFile, keyFile, cleanup := generateServerCerts(t) + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) defer cleanup() ui := cli.NewMockUi() @@ -317,7 +318,7 @@ func TestRun_WithProvider(t *testing.T) { providers: map[string]discover.Provider{"mock": provider}, } - caFile, certFile, keyFile, cleanup := generateServerCerts(t) + caFile, certFile, keyFile, cleanup := common.GenerateServerCerts(t) defer cleanup() // start the test server @@ -378,47 +379,3 @@ func generateCA(t *testing.T) (caPem, keyPem string) { return } - -// generateServerCerts generates Consul CA -// and a server certificate and saves them to temp files. -// It returns file names in this order: -// CA certificate, server certificate, and server key. -// Note that it's the responsibility of the caller to -// remove the temporary files created by this function. -func generateServerCerts(t *testing.T) (string, string, string, func()) { - require := require.New(t) - - caFile, err := ioutil.TempFile("", "ca") - require.NoError(err) - - certFile, err := ioutil.TempFile("", "cert") - require.NoError(err) - - certKeyFile, err := ioutil.TempFile("", "key") - require.NoError(err) - - // Generate CA - signer, _, caCertPem, caCertTemplate, err := cert.GenerateCA("Consul Agent CA - Test") - require.NoError(err) - - // Generate Server Cert - name := "server.dc1.consul" - hosts := []string{name, "localhost", "127.0.0.1"} - certPem, keyPem, err := cert.GenerateCert(name, 1*time.Hour, caCertTemplate, signer, hosts) - require.NoError(err) - - // Write certs and key to files - _, err = caFile.WriteString(caCertPem) - require.NoError(err) - _, err = certFile.WriteString(certPem) - require.NoError(err) - _, err = certKeyFile.WriteString(keyPem) - require.NoError(err) - - cleanupFunc := func() { - os.Remove(caFile.Name()) - os.Remove(certFile.Name()) - os.Remove(certKeyFile.Name()) - } - return caFile.Name(), certFile.Name(), certKeyFile.Name(), cleanupFunc -} diff --git a/subcommand/server-acl-init/command.go b/subcommand/server-acl-init/command.go index 13ff58e5f3..1f8161ebef 100644 --- a/subcommand/server-acl-init/command.go +++ b/subcommand/server-acl-init/command.go @@ -13,6 +13,7 @@ import ( godiscover "github.com/hashicorp/consul-k8s/helper/go-discover" "github.com/hashicorp/consul-k8s/subcommand" + "github.com/hashicorp/consul-k8s/subcommand/common" k8sflags "github.com/hashicorp/consul-k8s/subcommand/flags" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/flags" @@ -94,7 +95,7 @@ type Command struct { func (c *Command) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) c.flags.StringVar(&c.flagResourcePrefix, "resource-prefix", "", - "Prefix to use for Kubernetes resources. If not set, the \"-consul\" prefix is used, where is the value set by the -release-name flag.") + "Prefix to use for Kubernetes resources.") c.flags.StringVar(&c.flagK8sNamespace, "k8s-namespace", "", "Name of Kubernetes namespace where Consul and consul-k8s components are deployed.") @@ -506,7 +507,7 @@ func (c *Command) Run(args []string) int { } // Policy must be global because it replicates from the primary DC // and so the primary DC needs to be able to accept the token. - err = c.createGlobalACL("acl-replication", rules, consulDC, consulClient) + err = c.createGlobalACL(common.ACLReplicationTokenName, rules, consulDC, consulClient) if err != nil { c.log.Error(err.Error()) return 1 @@ -528,7 +529,7 @@ func (c *Command) getBootstrapToken(secretName string) (string, error) { } return "", err } - token, ok := secret.Data["token"] + token, ok := secret.Data[common.ACLTokenSecretKey] if !ok { return "", fmt.Errorf("secret %q does not have data key 'token'", secretName) } diff --git a/subcommand/server-acl-init/create_or_update.go b/subcommand/server-acl-init/create_or_update.go index ae890c7b07..22db47bf6a 100644 --- a/subcommand/server-acl-init/create_or_update.go +++ b/subcommand/server-acl-init/create_or_update.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/consul-k8s/subcommand/common" "github.com/hashicorp/consul/api" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -89,7 +90,7 @@ func (c *Command) createACL(name, rules string, localToken bool, dc string, cons Name: secretName, }, Data: map[string][]byte{ - "token": []byte(token), + common.ACLTokenSecretKey: []byte(token), }, } _, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Create(secret) diff --git a/subcommand/server-acl-init/servers.go b/subcommand/server-acl-init/servers.go index c280225a4c..a536bb029a 100644 --- a/subcommand/server-acl-init/servers.go +++ b/subcommand/server-acl-init/servers.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/consul-k8s/subcommand/common" "github.com/hashicorp/consul/api" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -67,7 +68,7 @@ func (c *Command) bootstrapServers(serverAddresses []string, bootTokenSecretName Name: bootTokenSecretName, }, Data: map[string][]byte{ - "token": bootstrapToken, + common.ACLTokenSecretKey: bootstrapToken, }, } _, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Create(secret)