diff --git a/cmd/helm.go b/cmd/helm.go index b8cd399..124d21c 100644 --- a/cmd/helm.go +++ b/cmd/helm.go @@ -350,6 +350,7 @@ At least one of --%s, --%s, or --%s are required.`, RbacUserFlag, RbacGroupFlag, tlsAlgorithmFlag, tlsECDSACurveFlag, tlsRSABitsFlag, + tillerDeploymentNameFlag, helmKubectlContextNameFlag, helmKubeconfigFlag, helmKubectlServerFlag, @@ -618,13 +619,14 @@ func grantHelmAccess(cliContext *cli.Context) error { if err != nil { return err } + tillerDeploymentName := cliContext.String(tillerDeploymentNameFlag.Name) rbacGroups := cliContext.StringSlice(grantedRbacGroupsFlag.Name) rbacUsers := cliContext.StringSlice(grantedRbacUsersFlag.Name) serviceAccounts := cliContext.StringSlice(grantedServiceAccountsFlag.Name) if len(rbacGroups) == 0 && len(rbacUsers) == 0 && len(serviceAccounts) == 0 { return entrypoint.NewRequiredArgsError(fmt.Sprintf("At least one --%s, --%s, or --%s is required", RbacUserFlag, RbacGroupFlag, RbacServiceAccountFlag)) } - return helm.GrantAccess(kubectlOptions, tlsOptions, tillerNamespace, rbacGroups, rbacUsers, serviceAccounts) + return helm.GrantAccess(kubectlOptions, tlsOptions, tillerDeploymentName, tillerNamespace, rbacGroups, rbacUsers, serviceAccounts) } // revokeHelmAccess is the action function for the helm revoke command. diff --git a/helm/deploy.go b/helm/deploy.go index ac69328..47b6b9d 100644 --- a/helm/deploy.go +++ b/helm/deploy.go @@ -269,7 +269,7 @@ func grantAndConfigureLocalClient( return errors.WithStackTrace(UnknownRBACEntityType{localClientRBACEntity.EntityType()}) } - err := GrantAccess(kubectlOptions, tlsOptions, tillerNamespace, rbacGroups, rbacUsers, rbacServiceAccounts) + err := GrantAccess(kubectlOptions, tlsOptions, TillerDeploymentName, tillerNamespace, rbacGroups, rbacUsers, rbacServiceAccounts) if err != nil { return err } diff --git a/helm/deploy_test.go b/helm/deploy_test.go index 13d989c..103d9c9 100644 --- a/helm/deploy_test.go +++ b/helm/deploy_test.go @@ -71,7 +71,7 @@ func TestValidateRequiredResourcesForDeploy(t *testing.T) { func TestHelmDeployConfigureUndeploy(t *testing.T) { t.Parallel() - imageSpec := "gcr.io/kubernetes-helm/tiller:v2.14.0" + imageSpec := "gcr.io/kubernetes-helm/tiller:v2.14.3" kubectlOptions := kubectl.GetTestKubectlOptions(t) terratestKubectlOptions := k8s.NewKubectlOptions("", "") @@ -80,32 +80,40 @@ func TestHelmDeployConfigureUndeploy(t *testing.T) { tlsOptions := tls.SampleTlsOptions(tls.ECDSAAlgorithm) clientTLSOptions := tls.SampleTlsOptions(tls.ECDSAAlgorithm) clientTLSOptions.DistinguishedName.CommonName = "client" - namespaceName := strings.ToLower(random.UniqueId()) - serviceAccountName := fmt.Sprintf("%s-service-account", namespaceName) + tillerNamespaceName := strings.ToLower(random.UniqueId()) + resourceNamespaceName := strings.ToLower(random.UniqueId()) + serviceAccountName := fmt.Sprintf("%s-service-account", tillerNamespaceName) - defer k8s.DeleteNamespace(t, terratestKubectlOptions, namespaceName) - k8s.CreateNamespace(t, terratestKubectlOptions, namespaceName) - terratestKubectlOptions.Namespace = namespaceName + defer k8s.DeleteNamespace(t, terratestKubectlOptions, tillerNamespaceName) + k8s.CreateNamespace(t, terratestKubectlOptions, tillerNamespaceName) + defer k8s.DeleteNamespace(t, terratestKubectlOptions, resourceNamespaceName) + k8s.CreateNamespace(t, terratestKubectlOptions, resourceNamespaceName) - // Create a test service account we can use for auth + // Create a test service account we can use for auth in the resource namespace + terratestKubectlOptions.Namespace = resourceNamespaceName testServiceAccountName, testServiceAccountKubectlOptions := createServiceAccountForAuth(t, terratestKubectlOptions) defer k8s.DeleteConfigContextE(t, testServiceAccountKubectlOptions.ContextName) testServiceAccountInfo := ServiceAccountInfo{Name: testServiceAccountName, Namespace: terratestKubectlOptions.Namespace} // Create a service account for Tiller + terratestKubectlOptions.Namespace = tillerNamespaceName k8s.CreateServiceAccount(t, terratestKubectlOptions, serviceAccountName) - bindNamespaceAdminRole(t, terratestKubectlOptions, serviceAccountName) + bindNamespaceAdminRole(t, terratestKubectlOptions, tillerNamespaceName, serviceAccountName) + + // Also make sure to bind admin roles for resource namespace + terratestKubectlOptions.Namespace = resourceNamespaceName + bindNamespaceAdminRole(t, terratestKubectlOptions, tillerNamespaceName, serviceAccountName) defer func() { // Make sure to undeploy all helm releases before undeploying the server. However, don't force undeploy the // server so that it crashes should the release removal fail. - assert.NoError(t, Undeploy(kubectlOptions, namespaceName, "", false, true)) + assert.NoError(t, Undeploy(kubectlOptions, tillerNamespaceName, "", false, true)) }() // Deploy, Grant, and Configure assert.NoError(t, Deploy( kubectlOptions, - namespaceName, - namespaceName, + tillerNamespaceName, + resourceNamespaceName, serviceAccountName, tlsOptions, clientTLSOptions, @@ -115,10 +123,11 @@ func TestHelmDeployConfigureUndeploy(t *testing.T) { )) // Check tiller pod is in chosen namespace + terratestKubectlOptions.Namespace = tillerNamespaceName tillerPodName := validateTillerPodDeployedInNamespace(t, terratestKubectlOptions) // Check tiller pod is using the right image - validateTillerPodImage(t, terratestKubectlOptions, namespaceName, imageSpec) + validateTillerPodImage(t, terratestKubectlOptions, tillerNamespaceName, imageSpec) // Check tiller pod is launched with the right service account validateTillerPodServiceAccount(t, terratestKubectlOptions, tillerPodName, serviceAccountName) @@ -133,29 +142,32 @@ func TestHelmDeployConfigureUndeploy(t *testing.T) { validateTillerAndClientTLSDifferent(t, terratestKubectlOptions, testServiceAccountInfo) // Check that we can deploy a helm chart - validateHelmChartDeploy(t, testServiceAccountKubectlOptions, namespaceName) + validateHelmChartDeploy(t, testServiceAccountKubectlOptions, tillerNamespaceName, resourceNamespaceName) + + // Check that the test service account can get the tiller deployment resource + validateGetTillerDeployment(t, testServiceAccountKubectlOptions, tillerNamespaceName) // Check that the rendered helm env file works validateHelmEnvFile(t, testServiceAccountKubectlOptions) - // Revoke the tiller service account + // Revoke the test service account rbacGroups := []string{} rbacUsers := []string{} - serviceAccounts := []string{fmt.Sprintf("%s/%s", namespaceName, testServiceAccountName)} - require.NoError(t, RevokeAccess(kubectlOptions, namespaceName, rbacGroups, rbacUsers, serviceAccounts)) + serviceAccounts := []string{fmt.Sprintf("%s/%s", resourceNamespaceName, testServiceAccountName)} + require.NoError(t, RevokeAccess(kubectlOptions, tillerNamespaceName, rbacGroups, rbacUsers, serviceAccounts)) // ServiceAccount role has been removed - err = validateNoRole(t, kubeClient, namespaceName, testServiceAccountName) - roleName := getTillerAccessRoleName(testServiceAccountName, namespaceName) + err = validateNoRole(t, kubeClient, tillerNamespaceName, testServiceAccountName) + roleName := getTillerAccessRoleName(testServiceAccountName, tillerNamespaceName) assert.Equal(t, err.Error(), fmt.Sprintf("roles.rbac.authorization.k8s.io \"%s\" not found", roleName)) // ServiceAccount role binding has been removed - err = validateNoRoleBinding(t, kubeClient, namespaceName, testServiceAccountName) + err = validateNoRoleBinding(t, kubeClient, tillerNamespaceName, testServiceAccountName) roleBindingName := getTillerAccessRoleBindingName(testServiceAccountName, roleName) assert.Equal(t, err.Error(), fmt.Sprintf("rolebindings.rbac.authorization.k8s.io \"%s\" not found", roleBindingName)) // ServiceAccount TLS secret has been removed - err = validateNoTLSSecret(t, kubeClient, namespaceName, serviceAccountName) + err = validateNoTLSSecret(t, kubeClient, tillerNamespaceName, serviceAccountName) secretName := getTillerClientCertSecretName(serviceAccountName) assert.Equal(t, err.Error(), fmt.Sprintf("secrets \"%s\" not found", secretName)) } @@ -237,6 +249,7 @@ func validateGenerateCertificateKeyPair(t *testing.T, algorithm string) { algorithm, tmpDir, ) + require.NoError(t, err) // Make sure the keys are compatible with cert validateKeyCompatibility(t, caCertificateKeyPair) @@ -279,7 +292,12 @@ func validateKeyCompatibility(t *testing.T, certKeyPair tls.CertificateKeyPairPa } // validateHelmChartDeploy checks if we can deploy a simple helm chart to the server. -func validateHelmChartDeploy(t *testing.T, kubectlOptions *kubectl.KubectlOptions, namespace string) { +func validateHelmChartDeploy( + t *testing.T, + kubectlOptions *kubectl.KubectlOptions, + tillerNamespace string, + resourceNamespace string, +) { require.NoError( t, RunHelm( @@ -290,13 +308,22 @@ func validateHelmChartDeploy(t *testing.T, kubectlOptions *kubectl.KubectlOption "--tls", "--tls-verify", "--tiller-namespace", - namespace, + tillerNamespace, "--namespace", - namespace, + resourceNamespace, ), ) } +// validateGetTillerDeployment verifies that the provided account has access to read the tiller deployment resource. +func validateGetTillerDeployment(t *testing.T, options *kubectl.KubectlOptions, tillerNamespace string) { + tillerDeploymentName := "tiller-deploy" + client, err := kubectl.GetKubernetesClientFromOptions(options) + require.NoError(t, err) + _, err = client.AppsV1().Deployments(tillerNamespace).Get(tillerDeploymentName, metav1.GetOptions{}) + require.NoError(t, err) +} + // validateHelmEnvFile sources the generated helm env file and verifies it sets the necessary and sufficient // environment variables for helm to talk to the deployed Tiller instance. func validateHelmEnvFile(t *testing.T, options *kubectl.KubectlOptions) { @@ -323,7 +350,7 @@ func validateHelmEnvFile(t *testing.T, options *kubectl.KubectlOptions) { } // validateServiceAccountRoleRemoved -func validateNoRole(t *testing.T, client *kubernetes.Clientset, namespace, serviceAccountName string) error { +func validateNoRole(t *testing.T, client *kubernetes.Clientset, namespace string, serviceAccountName string) error { roleName := getTillerAccessRoleName(serviceAccountName, namespace) _, err := client.RbacV1().Roles(namespace).Get(roleName, metav1.GetOptions{}) return err diff --git a/helm/grant.go b/helm/grant.go index cb5f6d5..5650f1b 100644 --- a/helm/grant.go +++ b/helm/grant.go @@ -28,6 +28,7 @@ import ( func GrantAccess( kubectlOptions *kubectl.KubectlOptions, tlsOptions tls.TLSOptions, + tillerDeploymentName string, tillerNamespace string, rbacGroups []string, rbacUsers []string, @@ -63,14 +64,14 @@ func GrantAccess( logger.Infof("Successfully downloaded CA TLS certificates for Tiller deployed in namespace %s.", tillerNamespace) logger.Infof("Granting access to deployed Tiller in namespace %s to RBAC groups", tillerNamespace) - if err := grantAccessToRBACEntities(kubectlOptions, tlsOptions, caKeyPairPath, tillerNamespace, convertToGroupInfos(rbacGroups)); err != nil { + if err := grantAccessToRBACEntities(kubectlOptions, tlsOptions, caKeyPairPath, tillerDeploymentName, tillerNamespace, convertToGroupInfos(rbacGroups)); err != nil { logger.Errorf("Error granting access to deployed Tiller in namespace %s to RBAC groups: %s", tillerNamespace, err) return err } logger.Infof("Successfully granted access to deployed Tiller in namespace %s to RBAC groups", tillerNamespace) logger.Infof("Granting access to deployed Tiller in namespace %s to RBAC users", tillerNamespace) - if err := grantAccessToRBACEntities(kubectlOptions, tlsOptions, caKeyPairPath, tillerNamespace, convertToUserInfos(rbacUsers)); err != nil { + if err := grantAccessToRBACEntities(kubectlOptions, tlsOptions, caKeyPairPath, tillerDeploymentName, tillerNamespace, convertToUserInfos(rbacUsers)); err != nil { logger.Errorf("Error granting access to deployed Tiller in namespace %s to RBAC users: %s", tillerNamespace, err) return err } @@ -81,7 +82,7 @@ func GrantAccess( if err != nil { return err } - if err := grantAccessToRBACEntities(kubectlOptions, tlsOptions, caKeyPairPath, tillerNamespace, serviceAccountInfos); err != nil { + if err := grantAccessToRBACEntities(kubectlOptions, tlsOptions, caKeyPairPath, tillerDeploymentName, tillerNamespace, serviceAccountInfos); err != nil { logger.Errorf("Error granting access to deployed Tiller in namespace %s to Service Accounts: %s", tillerNamespace, err) return err } @@ -136,6 +137,7 @@ func grantAccessToRBACEntities( kubectlOptions *kubectl.KubectlOptions, tlsOptions tls.TLSOptions, caKeyPairPath tls.CertificateKeyPairPath, + tillerDeploymentName string, tillerNamespace string, rbacEntities []RBACEntity, ) error { @@ -152,7 +154,7 @@ func grantAccessToRBACEntities( logger.Infof("Successfully generated and stored certificate key pair for %s", rbacEntity) logger.Infof("Creating and binding RBAC roles to %s", rbacEntity) - err = createAndBindRBACRolesForTillerAccess(kubectlOptions, tillerNamespace, clientSecretName, rbacEntity) + err = createAndBindRBACRolesForTillerAccess(kubectlOptions, tillerDeploymentName, tillerNamespace, clientSecretName, rbacEntity) if err != nil { logger.Errorf("Error creating and binding RBAC roles to %s", rbacEntity) return err @@ -254,6 +256,7 @@ func generateAndStoreSignedCertificateKeyPair( // - Get the client TLS certificate Secret resource in the tiller namespace. func createAndBindRBACRolesForTillerAccess( kubectlOptions *kubectl.KubectlOptions, + tillerDeploymentName string, tillerNamespace string, clientSecretName string, rbacEntity RBACEntity, @@ -262,7 +265,7 @@ func createAndBindRBACRolesForTillerAccess( roleName := getTillerAccessRoleName(rbacEntity.EntityID(), tillerNamespace) logger.Infof("Creating RBAC role to grant access to Tiller in namespace %s to %s", tillerNamespace, rbacEntity) - err := createTillerRBACRole(kubectlOptions, tillerNamespace, clientSecretName, roleName, rbacEntity) + err := createTillerRBACRole(kubectlOptions, tillerDeploymentName, tillerNamespace, clientSecretName, roleName, rbacEntity) if err != nil { logger.Errorf("Error creating RBAC role to grant access to Tiller: %s", err) return err @@ -282,6 +285,7 @@ func createAndBindRBACRolesForTillerAccess( func createTillerRBACRole( kubectlOptions *kubectl.KubectlOptions, + tillerDeploymentName string, tillerNamespace string, clientSecretName string, roleName string, @@ -294,6 +298,12 @@ func createTillerRBACRole( APIGroups: []string{""}, Resources: []string{"pods"}, }, + rbacv1.PolicyRule{ + Verbs: []string{"get"}, + APIGroups: []string{"apps"}, + Resources: []string{"deployments"}, + ResourceNames: []string{tillerDeploymentName}, + }, rbacv1.PolicyRule{ Verbs: []string{"get"}, APIGroups: []string{""}, diff --git a/helm/test_helpers.go b/helm/test_helpers.go index 5475612..731c643 100644 --- a/helm/test_helpers.go +++ b/helm/test_helpers.go @@ -26,7 +26,7 @@ func getHelmHome(t *testing.T) string { return helmHome } -func bindNamespaceAdminRole(t *testing.T, ttKubectlOptions *k8s.KubectlOptions, serviceAccountName string) { +func bindNamespaceAdminRole(t *testing.T, ttKubectlOptions *k8s.KubectlOptions, serviceAccountNamespace string, serviceAccountName string) { clientset, err := k8s.GetKubernetesClientFromOptionsE(t, ttKubectlOptions) require.NoError(t, err) @@ -52,7 +52,7 @@ func bindNamespaceAdminRole(t *testing.T, ttKubectlOptions *k8s.KubectlOptions, Kind: "ServiceAccount", APIGroup: "", Name: serviceAccountName, - Namespace: ttKubectlOptions.Namespace, + Namespace: serviceAccountNamespace, }, }, RoleRef: rbacv1.RoleRef{