diff --git a/subcommand/server-acl-init/command_test.go b/subcommand/server-acl-init/command_test.go index a26748e08e..7ec955eb2f 100644 --- a/subcommand/server-acl-init/command_test.go +++ b/subcommand/server-acl-init/command_test.go @@ -88,6 +88,7 @@ func TestRun_Defaults(t *testing.T) { clientset: k8s, } args := []string{ + "-timeout=1m", "-k8s-namespace=" + ns, "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], "-server-port", strings.Split(testSvr.HTTPAddr, ":")[1], @@ -233,6 +234,7 @@ func TestRun_TokensPrimaryDC(t *testing.T) { } cmd.init() cmdArgs := append([]string{ + "-timeout=1m", "-k8s-namespace=" + ns, "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], "-server-port", strings.Split(testSvr.HTTPAddr, ":")[1], @@ -389,6 +391,7 @@ func TestRun_TokensReplicatedDC(t *testing.T) { } cmd.init() cmdArgs := append([]string{ + "-timeout=1m", "-k8s-namespace=" + ns, "-acl-replication-token-file", tokenFile, "-server-address", strings.Split(secondaryAddr, ":")[0], @@ -516,6 +519,7 @@ func TestRun_TokensWithProvidedBootstrapToken(t *testing.T) { clientset: k8s, } cmdArgs := append([]string{ + "-timeout=1m", "-k8s-namespace", ns, "-bootstrap-token-file", tokenFile, "-server-address", strings.Split(testAgent.HTTPAddr, ":")[0], @@ -624,6 +628,7 @@ func TestRun_AnonymousTokenPolicy(t *testing.T) { } cmd.init() cmdArgs := append([]string{ + "-timeout=1m", "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-server-address", strings.Split(consulHTTPAddr, ":")[0], @@ -718,6 +723,7 @@ func TestRun_ConnectInjectAuthMethod(t *testing.T) { cmd.init() bindingRuleSelector := "serviceaccount.name!=default" cmdArgs := []string{ + "-timeout=1m", "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], @@ -788,6 +794,7 @@ func TestRun_ConnectInjectAuthMethodUpdates(t *testing.T) { // 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], @@ -830,6 +837,7 @@ func TestRun_ConnectInjectAuthMethodUpdates(t *testing.T) { // Run command again responseCode = cmd.Run([]string{ + "-timeout=1m", "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], @@ -954,6 +962,7 @@ func TestRun_DelayedServers(t *testing.T) { var responseCode int go func() { responseCode = cmd.Run([]string{ + "-timeout=1m", "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-server-address=127.0.0.1", @@ -1075,6 +1084,7 @@ func TestRun_NoLeader(t *testing.T) { var responseCode int go func() { responseCode = cmd.Run([]string{ + "-timeout=1m", "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-server-address=" + serverURL.Hostname(), @@ -1185,6 +1195,7 @@ func TestRun_ClientTokensRetry(t *testing.T) { clientset: k8s, } responseCode := cmd.Run([]string{ + "-timeout=1m", "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-server-address=" + serverURL.Hostname(), @@ -1285,6 +1296,7 @@ func TestRun_AlreadyBootstrapped(t *testing.T) { } responseCode := cmd.Run([]string{ + "-timeout=500ms", "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-server-address=" + serverURL.Hostname(), @@ -1366,6 +1378,7 @@ func TestRun_SkipBootstrapping_WhenBootstrapTokenIsProvided(t *testing.T) { } responseCode := cmd.Run([]string{ + "-timeout=500ms", "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-server-address=" + serverURL.Hostname(), @@ -1398,10 +1411,10 @@ func TestRun_Timeout(t *testing.T) { } responseCode := cmd.Run([]string{ + "-timeout=500ms", "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-server-address=foo", - "-timeout=500ms", }) require.Equal(1, responseCode, ui.ErrorWriter.String()) } @@ -1434,6 +1447,7 @@ func TestRun_HTTPS(t *testing.T) { } responseCode := cmd.Run([]string{ + "-timeout=1m", "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-use-https", @@ -1472,6 +1486,7 @@ func TestRun_ACLReplicationTokenValid(t *testing.T) { } secondaryCmd.init() secondaryCmdArgs := []string{ + "-timeout=1m", "-k8s-namespace=" + ns, "-server-address", strings.Split(secondaryAddr, ":")[0], "-server-port", strings.Split(secondaryAddr, ":")[1], @@ -1526,6 +1541,7 @@ func TestRun_AnonPolicy_IgnoredWithReplication(t *testing.T) { } cmd.init() cmdArgs := append([]string{ + "-timeout=1m", "-k8s-namespace=" + ns, "-acl-replication-token-file", tokenFile, "-server-address", strings.Split(serverAddr, ":")[0], @@ -1572,6 +1588,7 @@ func TestRun_CloudAutoJoin(t *testing.T) { providers: map[string]discover.Provider{"mock": provider}, } args := []string{ + "-timeout=1m", "-k8s-namespace=" + ns, "-resource-prefix=" + resourcePrefix, "-server-address", "provider=mock", @@ -1641,6 +1658,7 @@ func TestRun_GatewayErrors(t *testing.T) { clientset: k8s, } cmdArgs := []string{ + "-timeout=500ms", "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], @@ -1871,6 +1889,9 @@ func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset) (string, string) serviceAccountName := resourcePrefix + "-connect-injector-authmethod-svc-account" sa, _ := k8s.CoreV1().ServiceAccounts(ns).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( context.Background(), &v1.ServiceAccount{ @@ -1878,6 +1899,9 @@ func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset) (string, string) Name: serviceAccountName, }, Secrets: []v1.ObjectReference{ + { + Name: resourcePrefix + "-some-other-secret", + }, { Name: resourcePrefix + "-connect-injector-authmethod-svc-account", }, @@ -1903,8 +1927,26 @@ func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset) (string, string) "ca.crt": caCertBytes, "token": tokenBytes, }, + Type: v1.SecretTypeServiceAccountToken, + } + createOrUpdateSecret(t, k8s, secret) + + // Create the second secret of a different type + otherSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourcePrefix + "-some-other-secret", + }, + Data: map[string][]byte{}, + Type: v1.SecretTypeDockercfg, } - existingSecret, _ := k8s.CoreV1().Secrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) + createOrUpdateSecret(t, k8s, otherSecret) + + 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{}) + var err error if existingSecret == nil { _, err = k8s.CoreV1().Secrets(ns).Create(context.Background(), secret, metav1.CreateOptions{}) require.NoError(t, err) @@ -1912,8 +1954,6 @@ func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset) (string, string) _, err = k8s.CoreV1().Secrets(ns).Update(context.Background(), secret, metav1.UpdateOptions{}) require.NoError(t, err) } - - return string(caCertBytes), string(tokenBytes) } // policyExists asserts that policy with name exists. Returns the policy diff --git a/subcommand/server-acl-init/connect_inject.go b/subcommand/server-acl-init/connect_inject.go index 586e3ae3e7..3b7b3a193b 100644 --- a/subcommand/server-acl-init/connect_inject.go +++ b/subcommand/server-acl-init/connect_inject.go @@ -117,7 +117,7 @@ func (c *Command) configureConnectInject(consulClient *api.Client) error { // for this auth method, but none that match the binding // rule set up here in the bootstrap method. if abr.ID == "" { - return errors.New("Unable to find a matching ACL binding rule to update") + return errors.New("unable to find a matching ACL binding rule to update") } err = c.untilSucceeds(fmt.Sprintf("updating acl binding rule for %s", authMethodName), @@ -152,19 +152,31 @@ func (c *Command) createAuthMethodTmpl(authMethodName string) (api.ACLAuthMethod // ServiceAccounts always have a secret name. The secret // contains the JWT token. - saSecretName := authMethodServiceAccount.Secrets[0].Name - - // Get the secret that will contain the ServiceAccount JWT token. + // Because there could be multiple secrets attached to the service account, + // we need pick the first one of type "kubernetes.io/service-account-token". var saSecret *apiv1.Secret - err = c.untilSucceeds(fmt.Sprintf("getting %s Secret", saSecretName), - func() error { - var err error - saSecret, err = c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Get(context.TODO(), saSecretName, metav1.GetOptions{}) - return err - }) + for _, secretRef := range authMethodServiceAccount.Secrets { + var secret *apiv1.Secret + err = c.untilSucceeds(fmt.Sprintf("getting %s Secret", secretRef.Name), + func() error { + var err error + secret, err = c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Get(context.TODO(), secretRef.Name, metav1.GetOptions{}) + return err + }) + if secret != nil && secret.Type == apiv1.SecretTypeServiceAccountToken { + saSecret = secret + break + } + } if err != nil { return api.ACLAuthMethod{}, err } + // This is very unlikely to happen because Kubernetes ensure that there is always + // a secret of type ServiceAccountToken. + if saSecret == nil { + return api.ACLAuthMethod{}, + fmt.Errorf("found no secret of type 'kubernetes.io/service-account-token' associated with the %s service account", saName) + } kubernetesHost := defaultKubernetesHost diff --git a/subcommand/server-acl-init/connect_inject_test.go b/subcommand/server-acl-init/connect_inject_test.go new file mode 100644 index 0000000000..cad81d8b3c --- /dev/null +++ b/subcommand/server-acl-init/connect_inject_test.go @@ -0,0 +1,66 @@ +package serveraclinit + +import ( + "context" + "testing" + + "github.com/hashicorp/go-hclog" + "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" +) + +// Test that createAuthMethodTmpl returns an error when +// it cannot find a secret of type kubernetes.io/service-account-token +// associated with the service account. +// Note we are only testing this special error case here because we cannot assert on log output +// from the command. +// Also note that the remainder of this function is tested in the command_test.go. +func TestCommand_createAuthMethodTmpl_SecretNotFound(t *testing.T) { + k8s := fake.NewSimpleClientset() + + cmd := &Command{ + flagK8sNamespace: ns, + flagResourcePrefix: resourcePrefix, + clientset: k8s, + log: hclog.New(nil), + cmdTimeout: context.TODO(), + } + + serviceAccountName := resourcePrefix + "-connect-injector-authmethod-svc-account" + secretName := resourcePrefix + "-connect-injector-authmethod-svc-account" + + // Create a service account referencing secretName + sa, _ := k8s.CoreV1().ServiceAccounts(ns).Get(context.Background(), serviceAccountName, metav1.GetOptions{}) + if sa == nil { + _, err := k8s.CoreV1().ServiceAccounts(ns).Create( + context.Background(), + &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccountName, + }, + Secrets: []v1.ObjectReference{ + { + Name: secretName, + }, + }, + }, + metav1.CreateOptions{}) + require.NoError(t, err) + } + + // Create a secret of non service-account-token type (we're using the opaque type). + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + }, + Data: map[string][]byte{}, + Type: v1.SecretTypeOpaque, + } + _, err := k8s.CoreV1().Secrets(ns).Create(context.TODO(), secret, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = cmd.createAuthMethodTmpl("test") + require.EqualError(t, err, "found no secret of type 'kubernetes.io/service-account-token' associated with the release-name-consul-connect-injector-authmethod-svc-account service account") +}