diff --git a/subcommand/inject-connect/command_test.go b/subcommand/inject-connect/command_test.go index c0d4edbb3b..5bb04a922e 100644 --- a/subcommand/inject-connect/command_test.go +++ b/subcommand/inject-connect/command_test.go @@ -117,7 +117,7 @@ func TestRun_FlagValidation(t *testing.T) { expErr: "request must be <= limit: -lifecycle-sidecar-cpu-request value of \"50m\" is greater than the -lifecycle-sidecar-cpu-limit value of \"25m\"", }, { - flags: []string{"-consul-k8s-image", "kschoche/consul-k8s-dev", + flags: []string{"-consul-k8s-image", "hashicorpdev/consul-k8s:latest", "-enable-health-checks-controller=true"}, expErr: "CONSUL_HTTP_ADDR is not specified", }, diff --git a/subcommand/server-acl-init/command.go b/subcommand/server-acl-init/command.go index 82d22a000c..b4f514166b 100644 --- a/subcommand/server-acl-init/command.go +++ b/subcommand/server-acl-init/command.go @@ -80,6 +80,9 @@ type Command struct { // Flag to support a custom bootstrap token flagBootstrapTokenFile string + // Flag to indicate that the health checks controller is enabled. + flagEnableHealthChecks bool + flagLogLevel string flagTimeout time.Duration @@ -116,12 +119,19 @@ func (c *Command) init() { "The Consul node name to register for catalog sync. Defaults to k8s-sync. To be discoverable "+ "via DNS, the name should only contain alpha-numerics and dashes.") - c.flags.BoolVar(&c.flagCreateInjectToken, "create-inject-namespace-token", false, - "Toggle for creating a connect injector token. Only required when namespaces are enabled.") - c.flags.BoolVar(&c.flagCreateInjectAuthMethod, "create-inject-auth-method", false, - "Toggle for creating a connect inject auth method.") - c.flags.BoolVar(&c.flagCreateInjectAuthMethod, "create-inject-token", false, - "Toggle for creating a connect inject auth method. Deprecated: use -create-inject-auth-method instead.") + // Previously when this flag was set, -enable-namespaces and -create-inject-auth-method + // were always passed, so now we just look at those flags and ignore + // this flag. We keep the flag here though so there's no error if it's + // passed. + var unused bool + c.flags.BoolVar(&unused, "create-inject-namespace-token", false, + "Toggle for creating a connect injector token. Only required when namespaces are enabled. "+ + "Deprecated: set -enable-namespaces and -create-inject-token instead.") + + c.flags.BoolVar(&c.flagCreateInjectToken, "create-inject-auth-method", false, + "Toggle for creating a connect inject auth method. Deprecated: use -create-inject-token instead.") + c.flags.BoolVar(&c.flagCreateInjectToken, "create-inject-token", false, + "Toggle for creating a connect inject auth method and an ACL token. The ACL token will only be created if either of the -enable-namespaces or -enable-health-checks flags is set.") c.flags.StringVar(&c.flagInjectAuthMethodHost, "inject-auth-method-host", "", "Kubernetes Host config parameter for the auth method."+ "If not provided, the default cluster Kubernetes service will be used.") @@ -185,6 +195,9 @@ func (c *Command) init() { "Path to file containing ACL token for creating policies and tokens. This token must have 'acl:write' permissions."+ "When provided, servers will not be bootstrapped and their policies and tokens will not be updated.") + c.flags.BoolVar(&c.flagEnableHealthChecks, "enable-health-checks", false, + "Toggle for adding ACL rules for the health check controller to the connect ACL token. Requires -create-inject-token to be also be set.") + c.flags.DurationVar(&c.flagTimeout, "timeout", 10*time.Minute, "How long we'll try to bootstrap ACLs for before timing out, e.g. 1ms, 2s, 3m") c.flags.StringVar(&c.flagLogLevel, "log-level", "info", @@ -458,23 +471,33 @@ func (c *Command) Run(args []string) int { } if c.flagCreateInjectToken { - injectRules, err := c.injectRules() + err := c.configureConnectInjectAuthMethod(consulClient) if err != nil { - c.log.Error("Error templating inject rules", "err", err) + c.log.Error(err.Error()) return 1 } - // If namespaces are enabled, the policy and token needs to be global - // to be allowed to create namespaces. - if c.flagEnableNamespaces { - err = c.createGlobalACL("connect-inject", injectRules, consulDC, consulClient) - } else { - err = c.createLocalACL("connect-inject", injectRules, consulDC, consulClient) - } + // If health checks or namespaces are enabled, + // then the connect injector needs an ACL token. + if c.flagEnableNamespaces || c.flagEnableHealthChecks { + injectRules, err := c.injectRules() + if err != nil { + c.log.Error("Error templating inject rules", "err", err) + return 1 + } - if err != nil { - c.log.Error(err.Error()) - return 1 + // If namespaces are enabled, the policy and token need to be global + // to be allowed to create namespaces. + if c.flagEnableNamespaces { + err = c.createGlobalACL("connect-inject", injectRules, consulDC, consulClient) + } else { + err = c.createLocalACL("connect-inject", injectRules, consulDC, consulClient) + } + + if err != nil { + c.log.Error(err.Error()) + return 1 + } } } @@ -622,14 +645,6 @@ func (c *Command) Run(args []string) int { } } - if c.flagCreateInjectAuthMethod { - err := c.configureConnectInject(consulClient) - if err != nil { - c.log.Error(err.Error()) - return 1 - } - } - if c.flagCreateACLReplicationToken { rules, err := c.aclReplicationRules() if err != nil { @@ -763,14 +778,14 @@ func (c *Command) createAnonymousPolicy() bool { // Consul DNS requires the anonymous policy because DNS queries don't // have ACL tokens. (c.flagAllowDNS || - // If the connect auth method and ACL replication token are being + // If connect is enabled and the ACL replication token is being // created then we know we're using multi-dc Connect. // In this case the anonymous policy is required because Connect // services in Kubernetes have local tokens which are stripped // on cross-dc API calls. The cross-dc API calls thus use the anonymous // token. Cross-dc API calls are needed by the Connect proxies to talk // cross-dc. - (c.flagCreateInjectAuthMethod && c.flagCreateACLReplicationToken)) + (c.flagCreateInjectToken && c.flagCreateACLReplicationToken)) } func (c *Command) validateFlags() error { diff --git a/subcommand/server-acl-init/command_ent_test.go b/subcommand/server-acl-init/command_ent_test.go index 000648539e..8b497c1ee6 100644 --- a/subcommand/server-acl-init/command_ent_test.go +++ b/subcommand/server-acl-init/command_ent_test.go @@ -25,7 +25,7 @@ func TestRun_ConnectInject_SingleDestinationNamespace(t *testing.T) { t.Run(consulDestNamespace, func(tt *testing.T) { k8s, testAgent := completeEnterpriseSetup(tt) defer testAgent.Stop() - setUpK8sServiceAccount(tt, k8s) + setUpK8sServiceAccount(tt, k8s, ns) require := require.New(tt) ui := cli.NewMockUi() @@ -39,7 +39,7 @@ func TestRun_ConnectInject_SingleDestinationNamespace(t *testing.T) { "-server-port=" + strings.Split(testAgent.HTTPAddr, ":")[1], "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, - "-create-inject-auth-method", + "-create-inject-token", "-enable-namespaces", "-consul-inject-destination-namespace", consulDestNamespace, "-acl-binding-rule-selector=serviceaccount.name!=default", @@ -143,9 +143,9 @@ func TestRun_ConnectInject_NamespaceMirroring(t *testing.T) { for name, c := range cases { t.Run(name, func(tt *testing.T) { - k8s, testAgent := completeEnterpriseSetup(t) + k8s, testAgent := completeEnterpriseSetup(tt) defer testAgent.Stop() - setUpK8sServiceAccount(tt, k8s) + setUpK8sServiceAccount(tt, k8s, ns) require := require.New(tt) ui := cli.NewMockUi() @@ -159,7 +159,7 @@ func TestRun_ConnectInject_NamespaceMirroring(t *testing.T) { "-server-port=" + strings.Split(testAgent.HTTPAddr, ":")[1], "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, - "-create-inject-auth-method", + "-create-inject-token", "-enable-namespaces", "-enable-inject-k8s-namespace-mirroring", "-inject-k8s-namespace-mirroring-prefix", c.MirroringPrefix, @@ -169,7 +169,7 @@ func TestRun_ConnectInject_NamespaceMirroring(t *testing.T) { responseCode := cmd.Run(args) require.Equal(0, responseCode, ui.ErrorWriter.String()) - bootToken := getBootToken(t, k8s, resourcePrefix, ns) + bootToken := getBootToken(tt, k8s, resourcePrefix, ns) consul, err := api.NewClient(&api.Config{ Address: testAgent.HTTPAddr, Token: bootToken, @@ -211,6 +211,7 @@ func TestRun_ACLPolicyUpdates(t *testing.T) { for _, k8sNamespaceFlag := range k8sNamespaceFlags { t.Run(k8sNamespaceFlag, func(t *testing.T) { k8s, testAgent := completeEnterpriseSetup(t) + setUpK8sServiceAccount(t, k8s, k8sNamespaceFlag) defer testAgent.Stop() require := require.New(t) @@ -224,7 +225,7 @@ func TestRun_ACLPolicyUpdates(t *testing.T) { "-allow-dns", "-create-mesh-gateway-token", "-create-sync-token", - "-create-inject-namespace-token", + "-create-inject-token", "-create-snapshot-agent-token", "-create-enterprise-license-token", "-ingress-gateway-name=gw", @@ -259,7 +260,6 @@ func TestRun_ACLPolicyUpdates(t *testing.T) { "anonymous-token-policy", "client-token", "catalog-sync-token", - "connect-inject-token", "mesh-gateway-token", "client-snapshot-agent-token", "enterprise-license-token", @@ -407,7 +407,6 @@ func TestRun_ConnectInject_Updates(t *testing.T) { "no ns => single dest ns": { FirstRunArgs: nil, SecondRunArgs: []string{ - "-create-inject-auth-method", "-enable-namespaces", "-consul-inject-destination-namespace=dest", }, @@ -517,7 +516,7 @@ func TestRun_ConnectInject_Updates(t *testing.T) { require := require.New(tt) k8s, testAgent := completeEnterpriseSetup(tt) defer testAgent.Stop() - setUpK8sServiceAccount(tt, k8s) + setUpK8sServiceAccount(tt, k8s, ns) ui := cli.NewMockUi() defaultArgs := []string{ @@ -525,7 +524,7 @@ func TestRun_ConnectInject_Updates(t *testing.T) { "-server-port=" + strings.Split(testAgent.HTTPAddr, ":")[1], "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, - "-create-inject-auth-method", + "-create-inject-token", } // First run. NOTE: we don't assert anything here since we've @@ -549,7 +548,7 @@ func TestRun_ConnectInject_Updates(t *testing.T) { require.Equal(0, responseCode, ui.ErrorWriter.String()) // Now check that everything is as expected. - bootToken := getBootToken(t, k8s, resourcePrefix, ns) + bootToken := getBootToken(tt, k8s, resourcePrefix, ns) consul, err := api.NewClient(&api.Config{ Address: testAgent.HTTPAddr, Token: bootToken, @@ -608,8 +607,8 @@ func TestRun_TokensWithNamespacesEnabled(t *testing.T) { SecretNames: []string{resourcePrefix + "-catalog-sync-acl-token"}, LocalToken: false, }, - "connect-inject-namespace token": { - TokenFlags: []string{"-create-inject-namespace-token"}, + "connect-inject-token": { + TokenFlags: []string{"-create-inject-token", "-enable-namespaces"}, PolicyNames: []string{"connect-inject-token"}, PolicyDCs: nil, SecretNames: []string{resourcePrefix + "-connect-inject-acl-token"}, @@ -669,10 +668,25 @@ func TestRun_TokensWithNamespacesEnabled(t *testing.T) { SecretNames: []string{resourcePrefix + "-acl-replication-acl-token"}, LocalToken: false, }, + "inject token with namespaces (deprecated)": { + TokenFlags: []string{"-create-inject-auth-method", "-enable-namespaces", "-create-inject-namespace-token"}, + PolicyNames: []string{"connect-inject-token"}, + PolicyDCs: nil, + SecretNames: []string{resourcePrefix + "-connect-inject-acl-token"}, + LocalToken: false, + }, + "inject token with health checks and namespaces": { + TokenFlags: []string{"-create-inject-token", "-enable-namespaces", "-enable-health-checks"}, + PolicyNames: []string{"connect-inject-token"}, + PolicyDCs: nil, + SecretNames: []string{resourcePrefix + "-connect-inject-acl-token"}, + LocalToken: false, + }, } for testName, c := range cases { t.Run(testName, func(t *testing.T) { k8s, testSvr := completeEnterpriseSetup(t) + setUpK8sServiceAccount(t, k8s, ns) defer testSvr.Stop() require := require.New(t) diff --git a/subcommand/server-acl-init/command_test.go b/subcommand/server-acl-init/command_test.go index c68b57fcbf..fa1d577e63 100644 --- a/subcommand/server-acl-init/command_test.go +++ b/subcommand/server-acl-init/command_test.go @@ -169,14 +169,6 @@ func TestRun_TokensPrimaryDC(t *testing.T) { SecretNames: []string{resourcePrefix + "-catalog-sync-acl-token"}, LocalToken: true, }, - { - TestName: "Inject namespace token", - TokenFlags: []string{"-create-inject-namespace-token"}, - PolicyNames: []string{"connect-inject-token"}, - PolicyDCs: []string{"dc1"}, - SecretNames: []string{resourcePrefix + "-connect-inject-acl-token"}, - LocalToken: true, - }, { TestName: "Enterprise license token", TokenFlags: []string{"-create-enterprise-license-token"}, @@ -245,10 +237,19 @@ func TestRun_TokensPrimaryDC(t *testing.T) { SecretNames: []string{resourcePrefix + "-controller-acl-token"}, LocalToken: true, }, + { + TestName: "Health Checks ACL token", + TokenFlags: []string{"-create-inject-token", "-enable-health-checks"}, + PolicyNames: []string{"connect-inject-token"}, + PolicyDCs: []string{"dc1"}, + SecretNames: []string{resourcePrefix + "-connect-inject-acl-token"}, + LocalToken: true, + }, } for _, c := range cases { t.Run(c.TestName, func(t *testing.T) { k8s, testSvr := completeSetup(t) + setUpK8sServiceAccount(t, k8s, ns) defer testSvr.Stop() require := require.New(t) @@ -339,14 +340,6 @@ func TestRun_TokensReplicatedDC(t *testing.T) { SecretNames: []string{resourcePrefix + "-catalog-sync-acl-token"}, LocalToken: true, }, - { - TestName: "Inject namespace token", - TokenFlags: []string{"-create-inject-namespace-token"}, - PolicyNames: []string{"connect-inject-token-dc2"}, - PolicyDCs: []string{"dc2"}, - SecretNames: []string{resourcePrefix + "-connect-inject-acl-token"}, - LocalToken: true, - }, { TestName: "Enterprise license token", TokenFlags: []string{"-create-enterprise-license-token"}, @@ -399,6 +392,14 @@ func TestRun_TokensReplicatedDC(t *testing.T) { resourcePrefix + "-another-gateway-terminating-gateway-acl-token"}, LocalToken: true, }, + { + TestName: "Health Checks ACL token", + TokenFlags: []string{"-create-inject-token", "-enable-health-checks"}, + PolicyNames: []string{"connect-inject-token-dc2"}, + PolicyDCs: []string{"dc2"}, + SecretNames: []string{resourcePrefix + "-connect-inject-acl-token"}, + LocalToken: true, + }, } for _, c := range cases { t.Run(c.TestName, func(t *testing.T) { @@ -407,6 +408,7 @@ func TestRun_TokensReplicatedDC(t *testing.T) { defer fileCleanup() k8s, consul, secondaryAddr, cleanup := mockReplicatedSetup(t, bootToken) + setUpK8sServiceAccount(t, k8s, ns) defer cleanup() // Run the command. @@ -474,12 +476,6 @@ func TestRun_TokensWithProvidedBootstrapToken(t *testing.T) { PolicyNames: []string{"catalog-sync-token"}, SecretNames: []string{resourcePrefix + "-catalog-sync-acl-token"}, }, - { - TestName: "Inject token", - TokenFlags: []string{"-create-inject-namespace-token"}, - PolicyNames: []string{"connect-inject-token"}, - SecretNames: []string{resourcePrefix + "-connect-inject-acl-token"}, - }, { TestName: "Enterprise license token", TokenFlags: []string{"-create-enterprise-license-token"}, @@ -528,6 +524,12 @@ func TestRun_TokensWithProvidedBootstrapToken(t *testing.T) { PolicyNames: []string{"acl-replication-token"}, SecretNames: []string{resourcePrefix + "-acl-replication-acl-token"}, }, + { + TestName: "Health Checks ACL token", + TokenFlags: []string{"-create-inject-token", "-enable-health-checks"}, + PolicyNames: []string{"connect-inject-token"}, + SecretNames: []string{resourcePrefix + "-connect-inject-acl-token"}, + }, } for _, c := range cases { t.Run(c.TestName, func(t *testing.T) { @@ -536,6 +538,7 @@ func TestRun_TokensWithProvidedBootstrapToken(t *testing.T) { defer fileCleanup() k8s, testAgent := completeBootstrappedSetup(t, bootToken) + setUpK8sServiceAccount(t, k8s, ns) defer testAgent.Stop() // Run the command. @@ -604,21 +607,36 @@ func TestRun_AnonymousTokenPolicy(t *testing.T) { SecondaryDC: true, ExpAnonymousPolicy: false, }, - "auth method, primary dc, no replication": { + "auth method, primary dc, no replication (deprecated)": { Flags: []string{"-create-inject-auth-method"}, SecondaryDC: false, ExpAnonymousPolicy: false, }, - "auth method, primary dc, with replication": { + "auth method, primary dc, with replication (deprecated)": { Flags: []string{"-create-inject-auth-method", "-create-acl-replication-token"}, SecondaryDC: false, ExpAnonymousPolicy: true, }, - "auth method, secondary dc": { + "auth method, secondary dc (deprecated)": { Flags: []string{"-create-inject-auth-method"}, SecondaryDC: true, ExpAnonymousPolicy: false, }, + "auth method, primary dc, no replication": { + Flags: []string{"-create-inject-token"}, + SecondaryDC: false, + ExpAnonymousPolicy: false, + }, + "auth method, primary dc, with replication": { + Flags: []string{"-create-inject-token", "-create-acl-replication-token"}, + SecondaryDC: false, + ExpAnonymousPolicy: true, + }, + "auth method, secondary dc": { + Flags: []string{"-create-inject-token"}, + SecondaryDC: true, + ExpAnonymousPolicy: false, + }, } for name, c := range cases { t.Run(name, func(t *testing.T) { @@ -644,7 +662,7 @@ func TestRun_AnonymousTokenPolicy(t *testing.T) { defer testSvr.Stop() consulHTTPAddr = testSvr.HTTPAddr } - setUpK8sServiceAccount(t, k8s) + setUpK8sServiceAccount(t, k8s, ns) // Run the command. ui := cli.NewMockUi() @@ -724,21 +742,28 @@ func TestRun_ConnectInjectAuthMethod(t *testing.T) { flags: []string{"-create-inject-auth-method"}, expectedHost: "https://kubernetes.default.svc", }, - "-inject-auth-method-host flag": { + "-inject-auth-method-host flag (deprecated)": { flags: []string{ "-create-inject-auth-method", "-inject-auth-method-host=https://my-kube.com", }, expectedHost: "https://my-kube.com", }, + "-inject-auth-method-host flag": { + flags: []string{ + "-create-inject-token", + "-inject-auth-method-host=https://my-kube.com", + }, + expectedHost: "https://my-kube.com", + }, } for testName, c := range cases { - t.Run(testName, func(tt *testing.T) { + t.Run(testName, func(t *testing.T) { - k8s, testSvr := completeSetup(tt) + k8s, testSvr := completeSetup(t) defer testSvr.Stop() - caCert, jwtToken := setUpK8sServiceAccount(tt, k8s) - require := require.New(tt) + caCert, jwtToken := setUpK8sServiceAccount(t, k8s, ns) + require := require.New(t) // Run the command. ui := cli.NewMockUi() @@ -805,165 +830,179 @@ func TestRun_ConnectInjectAuthMethod(t *testing.T) { func TestRun_ConnectInjectAuthMethodUpdates(t *testing.T) { t.Parallel() - k8s, testSvr := completeSetup(t) - defer testSvr.Stop() - caCert, jwtToken := setUpK8sServiceAccount(t, k8s) - require := require.New(t) + // Test with deprecated -create-inject-auth-method flag. + cases := []string{"-create-inject-auth-method", "-create-inject-token"} + for _, flag := range cases { + t.Run(flag, func(t *testing.T) { - ui := cli.NewMockUi() - cmd := Command{ - UI: ui, - clientset: k8s, - } + k8s, testSvr := completeSetup(t) + defer testSvr.Stop() + caCert, jwtToken := setUpK8sServiceAccount(t, k8s, ns) + require := require.New(t) - bindingRuleSelector := "serviceaccount.name!=default" + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + clientset: k8s, + } - // First, create an auth method using the defaults - responseCode := cmd.Run([]string{ - "-timeout=1m", - "-resource-prefix=" + resourcePrefix, - "-k8s-namespace=" + ns, - "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], - "-server-port", strings.Split(testSvr.HTTPAddr, ":")[1], - "-create-inject-auth-method", - "-acl-binding-rule-selector=" + bindingRuleSelector, - }) - require.Equal(0, responseCode, ui.ErrorWriter.String()) + bindingRuleSelector := "serviceaccount.name!=default" - // Check that the auth method was created. - bootToken := getBootToken(t, k8s, resourcePrefix, ns) - consul, err := api.NewClient(&api.Config{ - Address: testSvr.HTTPAddr, - }) - require.NoError(err) - authMethodName := resourcePrefix + "-k8s-auth-method" - authMethod, _, err := consul.ACL().AuthMethodRead(authMethodName, - &api.QueryOptions{Token: bootToken}) - require.NoError(err) - require.NotNil(authMethod) - require.Contains(authMethod.Config, "Host") - require.Equal(authMethod.Config["Host"], defaultKubernetesHost) - require.Contains(authMethod.Config, "CACert") - require.Equal(authMethod.Config["CACert"], caCert) - require.Contains(authMethod.Config, "ServiceAccountJWT") - require.Equal(authMethod.Config["ServiceAccountJWT"], jwtToken) - - // Generate a new CA certificate - _, _, caCertPem, _, err := cert.GenerateCA("kubernetes") - require.NoError(err) + // First, create an auth method using the defaults + responseCode := cmd.Run([]string{ + "-timeout=1m", + "-resource-prefix=" + resourcePrefix, + "-k8s-namespace=" + ns, + "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], + "-server-port", strings.Split(testSvr.HTTPAddr, ":")[1], + flag, + "-acl-binding-rule-selector=" + bindingRuleSelector, + }) + require.Equal(0, responseCode, ui.ErrorWriter.String()) + + // Check that the auth method was created. + bootToken := getBootToken(t, k8s, resourcePrefix, ns) + consul, err := api.NewClient(&api.Config{ + Address: testSvr.HTTPAddr, + }) + require.NoError(err) + authMethodName := resourcePrefix + "-k8s-auth-method" + authMethod, _, err := consul.ACL().AuthMethodRead(authMethodName, + &api.QueryOptions{Token: bootToken}) + require.NoError(err) + require.NotNil(authMethod) + require.Contains(authMethod.Config, "Host") + require.Equal(authMethod.Config["Host"], defaultKubernetesHost) + require.Contains(authMethod.Config, "CACert") + require.Equal(authMethod.Config["CACert"], caCert) + require.Contains(authMethod.Config, "ServiceAccountJWT") + require.Equal(authMethod.Config["ServiceAccountJWT"], jwtToken) - // Overwrite the default kubernetes api, service account token and CA cert - kubernetesHost := "https://kubernetes.example.com" - // This token is the base64 encoded example token from jwt.io - serviceAccountToken = "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnpkV0lpT2lJeE1qTTBOVFkzT0Rrd0lpd2libUZ0WlNJNklrcHZhRzRnUkc5bElpd2lhV0YwSWpveE5URTJNak01TURJeWZRLlNmbEt4d1JKU01lS0tGMlFUNGZ3cE1lSmYzNlBPazZ5SlZfYWRRc3N3NWM=" - serviceAccountCACert = base64.StdEncoding.EncodeToString([]byte(caCertPem)) + // Generate a new CA certificate + _, _, caCertPem, _, err := cert.GenerateCA("kubernetes") + require.NoError(err) - // Create a new service account - updatedCACert, updatedJWTToken := setUpK8sServiceAccount(t, k8s) + // Overwrite the default kubernetes api, service account token and CA cert + kubernetesHost := "https://kubernetes.example.com" + // This token is the base64 encoded example token from jwt.io + serviceAccountToken = "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnpkV0lpT2lJeE1qTTBOVFkzT0Rrd0lpd2libUZ0WlNJNklrcHZhRzRnUkc5bElpd2lhV0YwSWpveE5URTJNak01TURJeWZRLlNmbEt4d1JKU01lS0tGMlFUNGZ3cE1lSmYzNlBPazZ5SlZfYWRRc3N3NWM=" + serviceAccountCACert = base64.StdEncoding.EncodeToString([]byte(caCertPem)) - // Run command again - responseCode = cmd.Run([]string{ - "-timeout=1m", - "-resource-prefix=" + resourcePrefix, - "-k8s-namespace=" + ns, - "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], - "-server-port", strings.Split(testSvr.HTTPAddr, ":")[1], - "-acl-binding-rule-selector=" + bindingRuleSelector, - "-create-inject-auth-method", - "-inject-auth-method-host=" + kubernetesHost, - }) - require.Equal(0, responseCode, ui.ErrorWriter.String()) + // Create a new service account + updatedCACert, updatedJWTToken := setUpK8sServiceAccount(t, k8s, ns) - // Check that the auth method has been updated - authMethod, _, err = consul.ACL().AuthMethodRead(authMethodName, - &api.QueryOptions{Token: bootToken}) - require.NoError(err) - require.NotNil(authMethod) - require.Contains(authMethod.Config, "Host") - require.Equal(authMethod.Config["Host"], kubernetesHost) - require.Contains(authMethod.Config, "CACert") - require.Equal(authMethod.Config["CACert"], updatedCACert) - require.Contains(authMethod.Config, "ServiceAccountJWT") - require.Equal(authMethod.Config["ServiceAccountJWT"], updatedJWTToken) + // Run command again + responseCode = cmd.Run([]string{ + "-timeout=1m", + "-resource-prefix=" + resourcePrefix, + "-k8s-namespace=" + ns, + "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], + "-server-port", strings.Split(testSvr.HTTPAddr, ":")[1], + "-acl-binding-rule-selector=" + bindingRuleSelector, + flag, + "-inject-auth-method-host=" + kubernetesHost, + }) + require.Equal(0, responseCode, ui.ErrorWriter.String()) + + // Check that the auth method has been updated + authMethod, _, err = consul.ACL().AuthMethodRead(authMethodName, + &api.QueryOptions{Token: bootToken}) + require.NoError(err) + require.NotNil(authMethod) + require.Contains(authMethod.Config, "Host") + require.Equal(authMethod.Config["Host"], kubernetesHost) + require.Contains(authMethod.Config, "CACert") + require.Equal(authMethod.Config["CACert"], updatedCACert) + require.Contains(authMethod.Config, "ServiceAccountJWT") + require.Equal(authMethod.Config["ServiceAccountJWT"], updatedJWTToken) + }) + } } // Test that ACL binding rules are updated if the rule selector changes. -func TestRun_BindingRuleUpdates(t *testing.T) { - t.Parallel() - k8s, testSvr := completeSetup(t) - setUpK8sServiceAccount(t, k8s) - defer testSvr.Stop() - require := require.New(t) +func TestRun_BindingRuleUpdates(tt *testing.T) { + tt.Parallel() - consul, err := api.NewClient(&api.Config{ - Address: testSvr.HTTPAddr, - }) - require.NoError(err) + // Test with deprecated -create-inject-auth-method flag. + cases := []string{"-create-inject-auth-method", "-create-inject-token"} + for _, flag := range cases { + tt.Run(flag, func(t *testing.T) { + k8s, testSvr := completeSetup(t) + setUpK8sServiceAccount(t, k8s, ns) + defer testSvr.Stop() + require := require.New(t) - ui := cli.NewMockUi() - commonArgs := []string{ - "-resource-prefix=" + resourcePrefix, - "-k8s-namespace=" + ns, - "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], - "-server-port", strings.Split(testSvr.HTTPAddr, ":")[1], - "-create-inject-auth-method", - } - firstRunArgs := append(commonArgs, - "-acl-binding-rule-selector=serviceaccount.name!=default", - ) - // On the second run, we change the binding rule selector. - secondRunArgs := append(commonArgs, - "-acl-binding-rule-selector=serviceaccount.name!=changed", - ) + consul, err := api.NewClient(&api.Config{ + Address: testSvr.HTTPAddr, + }) + require.NoError(err) - // Run the command first to populate the binding rule. - cmd := Command{ - UI: ui, - clientset: k8s, - } - responseCode := cmd.Run(firstRunArgs) - require.Equal(0, responseCode, ui.ErrorWriter.String()) + ui := cli.NewMockUi() + commonArgs := []string{ + "-resource-prefix=" + resourcePrefix, + "-k8s-namespace=" + ns, + "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], + "-server-port", strings.Split(testSvr.HTTPAddr, ":")[1], + flag, + } + firstRunArgs := append(commonArgs, + "-acl-binding-rule-selector=serviceaccount.name!=default", + ) + // On the second run, we change the binding rule selector. + secondRunArgs := append(commonArgs, + "-acl-binding-rule-selector=serviceaccount.name!=changed", + ) + + // Run the command first to populate the binding rule. + cmd := Command{ + UI: ui, + clientset: k8s, + } + responseCode := cmd.Run(firstRunArgs) + require.Equal(0, responseCode, ui.ErrorWriter.String()) - // Validate the binding rule. - { - queryOpts := &api.QueryOptions{Token: getBootToken(t, k8s, resourcePrefix, ns)} - authMethodName := resourcePrefix + "-k8s-auth-method" - rules, _, err := consul.ACL().BindingRuleList(authMethodName, queryOpts) - require.NoError(err) - require.Len(rules, 1) - actRule, _, err := consul.ACL().BindingRuleRead(rules[0].ID, queryOpts) - require.NoError(err) - require.NotNil(actRule) - require.Equal("Kubernetes binding rule", actRule.Description) - require.Equal(api.BindingRuleBindTypeService, actRule.BindType) - require.Equal("${serviceaccount.name}", actRule.BindName) - require.Equal("serviceaccount.name!=default", actRule.Selector) - } + // Validate the binding rule. + { + queryOpts := &api.QueryOptions{Token: getBootToken(t, k8s, resourcePrefix, ns)} + authMethodName := resourcePrefix + "-k8s-auth-method" + rules, _, err := consul.ACL().BindingRuleList(authMethodName, queryOpts) + require.NoError(err) + require.Len(rules, 1) + actRule, _, err := consul.ACL().BindingRuleRead(rules[0].ID, queryOpts) + require.NoError(err) + require.NotNil(actRule) + require.Equal("Kubernetes binding rule", actRule.Description) + require.Equal(api.BindingRuleBindTypeService, actRule.BindType) + require.Equal("${serviceaccount.name}", actRule.BindName) + require.Equal("serviceaccount.name!=default", actRule.Selector) + } - // Re-run the command with namespace flags. The policies should be updated. - // NOTE: We're redefining the command so that the old flag values are - // reset. - cmd = Command{ - UI: ui, - clientset: k8s, - } - responseCode = cmd.Run(secondRunArgs) - require.Equal(0, responseCode, ui.ErrorWriter.String()) + // Re-run the command with namespace flags. The policies should be updated. + // NOTE: We're redefining the command so that the old flag values are + // reset. + cmd = Command{ + UI: ui, + clientset: k8s, + } + responseCode = cmd.Run(secondRunArgs) + require.Equal(0, responseCode, ui.ErrorWriter.String()) - // Check the binding rule is changed expected. - { - queryOpts := &api.QueryOptions{Token: getBootToken(t, k8s, resourcePrefix, ns)} - authMethodName := resourcePrefix + "-k8s-auth-method" - rules, _, err := consul.ACL().BindingRuleList(authMethodName, queryOpts) - require.NoError(err) - require.Len(rules, 1) - actRule, _, err := consul.ACL().BindingRuleRead(rules[0].ID, queryOpts) - require.NoError(err) - require.NotNil(actRule) - require.Equal("Kubernetes binding rule", actRule.Description) - require.Equal(api.BindingRuleBindTypeService, actRule.BindType) - require.Equal("${serviceaccount.name}", actRule.BindName) - require.Equal("serviceaccount.name!=changed", actRule.Selector) + // Check the binding rule is changed expected. + { + queryOpts := &api.QueryOptions{Token: getBootToken(t, k8s, resourcePrefix, ns)} + authMethodName := resourcePrefix + "-k8s-auth-method" + rules, _, err := consul.ACL().BindingRuleList(authMethodName, queryOpts) + require.NoError(err) + require.Len(rules, 1) + actRule, _, err := consul.ACL().BindingRuleRead(rules[0].ID, queryOpts) + require.NoError(err) + require.NotNil(actRule) + require.Equal("Kubernetes binding rule", actRule.Description) + require.Equal(api.BindingRuleBindTypeService, actRule.BindType) + require.Equal("${serviceaccount.name}", actRule.BindName) + require.Equal("serviceaccount.name!=changed", actRule.Selector) + } + }) } } @@ -1633,7 +1672,7 @@ func TestRun_AnonPolicy_IgnoredWithReplication(t *testing.T) { tokenFile, fileCleanup := writeTempFile(t, bootToken) defer fileCleanup() k8s, consul, serverAddr, cleanup := mockReplicatedSetup(t, bootToken) - setUpK8sServiceAccount(t, k8s) + setUpK8sServiceAccount(t, k8s, ns) defer cleanup() // Run the command. @@ -1986,16 +2025,16 @@ func generateServerCerts(t *testing.T) (string, string, string, func()) { // This Service Account would normally automatically be created by Kubernetes // when the injector deployment is created. It returns the Service Account // CA Cert and JWT token. -func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset) (string, string) { +func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset, namespace string) (string, string) { // Create ServiceAccount for the kubernetes auth method if it doesn't exist, // otherwise, do nothing. serviceAccountName := resourcePrefix + "-connect-injector-authmethod-svc-account" - sa, _ := k8s.CoreV1().ServiceAccounts(ns).Get(context.Background(), serviceAccountName, metav1.GetOptions{}) + sa, _ := k8s.CoreV1().ServiceAccounts(namespace).Get(context.Background(), serviceAccountName, metav1.GetOptions{}) if sa == nil { // Create a service account that references two secrets. // The second secret is mimicking the behavior on Openshift, // where two secrets are injected: one with SA token and one with docker config. - _, err := k8s.CoreV1().ServiceAccounts(ns).Create( + _, err := k8s.CoreV1().ServiceAccounts(namespace).Create( context.Background(), &v1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ @@ -2032,7 +2071,7 @@ func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset) (string, string) }, Type: v1.SecretTypeServiceAccountToken, } - createOrUpdateSecret(t, k8s, secret) + createOrUpdateSecret(t, k8s, secret, namespace) // Create the second secret of a different type otherSecret := &v1.Secret{ @@ -2042,19 +2081,19 @@ func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset) (string, string) Data: map[string][]byte{}, Type: v1.SecretTypeDockercfg, } - createOrUpdateSecret(t, k8s, otherSecret) + createOrUpdateSecret(t, k8s, otherSecret, namespace) return string(caCertBytes), string(tokenBytes) } -func createOrUpdateSecret(t *testing.T, k8s *fake.Clientset, secret *v1.Secret) { - existingSecret, _ := k8s.CoreV1().Secrets(ns).Get(context.Background(), secret.Name, metav1.GetOptions{}) +func createOrUpdateSecret(t *testing.T, k8s *fake.Clientset, secret *v1.Secret, namespace string) { + existingSecret, _ := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secret.Name, metav1.GetOptions{}) var err error if existingSecret == nil { - _, err = k8s.CoreV1().Secrets(ns).Create(context.Background(), secret, metav1.CreateOptions{}) + _, err = k8s.CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{}) require.NoError(t, err) } else { - _, err = k8s.CoreV1().Secrets(ns).Update(context.Background(), secret, metav1.UpdateOptions{}) + _, err = k8s.CoreV1().Secrets(namespace).Update(context.Background(), secret, metav1.UpdateOptions{}) require.NoError(t, err) } } diff --git a/subcommand/server-acl-init/connect_inject.go b/subcommand/server-acl-init/connect_inject.go index 28c500f179..b14224140d 100644 --- a/subcommand/server-acl-init/connect_inject.go +++ b/subcommand/server-acl-init/connect_inject.go @@ -19,7 +19,7 @@ const defaultKubernetesHost = "https://kubernetes.default.svc" // configureConnectInject sets up auth methods so that connect injection will // work. -func (c *Command) configureConnectInject(consulClient *api.Client) error { +func (c *Command) configureConnectInjectAuthMethod(consulClient *api.Client) error { authMethodName := c.withPrefix("k8s-auth-method") diff --git a/subcommand/server-acl-init/rules.go b/subcommand/server-acl-init/rules.go index 89501f2e0b..b68fa84aa9 100644 --- a/subcommand/server-acl-init/rules.go +++ b/subcommand/server-acl-init/rules.go @@ -15,6 +15,7 @@ type rulesData struct { InjectEnableNSMirroring bool InjectNSMirroringPrefix string SyncConsulNodeName string + EnableHealthChecks bool } type gatewayRulesData struct { @@ -204,11 +205,20 @@ namespace "{{ .SyncConsulDestNS }}" { } func (c *Command) injectRules() (string, error) { - // The Connect injector only needs permissions to create namespaces. + // The Connect injector needs permissions to create namespaces when namespaces are enabled + // and also create/update service checks when health checks are enabled. injectRulesTpl := ` {{- if .EnableNamespaces }} operator = "write" {{- end }} +{{- if .EnableHealthChecks }} + node_prefix "" { + policy = "write" + } + service_prefix "" { + policy = "write" + } +{{- end }} ` return c.renderRules(injectRulesTpl) } @@ -275,6 +285,7 @@ func (c *Command) rulesData() rulesData { InjectEnableNSMirroring: c.flagEnableInjectK8SNSMirroring, InjectNSMirroringPrefix: c.flagInjectK8SNSMirroringPrefix, SyncConsulNodeName: c.flagSyncConsulNodeName, + EnableHealthChecks: c.flagEnableHealthChecks, } } diff --git a/subcommand/server-acl-init/rules_test.go b/subcommand/server-acl-init/rules_test.go index 4155f9c5df..5c257ae743 100644 --- a/subcommand/server-acl-init/rules_test.go +++ b/subcommand/server-acl-init/rules_test.go @@ -495,21 +495,49 @@ namespace_prefix "prefix-" { func TestInjectRules(t *testing.T) { cases := []struct { - Name string - EnableNamespaces bool - Expected string + Name string + EnableNamespaces bool + EnableHealthChecks bool + Expected string }{ { - "Namespaces are disabled", + "Namespaces are disabled, health checks controller disabled", + false, false, "", }, { - "Namespaces are enabled", + "Namespaces are enabled, health checks controller disabled", true, + false, ` operator = "write"`, }, + { + "Namespaces are disabled, health checks controller enabled", + false, + true, + ` + node_prefix "" { + policy = "write" + } + service_prefix "" { + policy = "write" + }`, + }, + { + "Namespaces are enabled, health checks controller enabled", + true, + true, + ` +operator = "write" + node_prefix "" { + policy = "write" + } + service_prefix "" { + policy = "write" + }`, + }, } for _, tt := range cases { @@ -517,7 +545,8 @@ operator = "write"`, require := require.New(t) cmd := Command{ - flagEnableNamespaces: tt.EnableNamespaces, + flagEnableNamespaces: tt.EnableNamespaces, + flagEnableHealthChecks: tt.EnableHealthChecks, } injectorRules, err := cmd.injectRules()