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)