Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

helm configure #7

Merged
merged 15 commits into from
Jan 31, 2019
Prev Previous commit
Next Next commit
Implement the TLS section of the configure command
  • Loading branch information
yorinasub17 committed Jan 29, 2019
commit cab460a72a84d6efc97614045a46a0edbf5a430c
14 changes: 5 additions & 9 deletions cmd/errors.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package main

import (
"fmt"
)

// InvalidServiceAccountInfo error is returned when the encoded service account is not encoded correctly.
type InvalidServiceAccountInfo struct {
EncodedServiceAccount string
// MutualExclusiveFlagError is returned when there is a violation of a mutually exclusive flag set.
type MutuallyExclusiveFlagError struct {
Message string
}

func (err InvalidServiceAccountInfo) Error() string {
return fmt.Sprintf("Invalid encoding for ServiceAccount string %s. Expected NAMESPACE/NAME.", err.EncodedServiceAccount)
func (err MutuallyExclusiveFlagError) Error() string {
return err.Message
}
77 changes: 49 additions & 28 deletions cmd/helm.go
Original file line number Diff line number Diff line change
@@ -106,6 +106,10 @@ var (
Name: "rbac-group",
Usage: "The name of the RBAC group that should be granted access to tiller. Pass in multiple times for multiple groups.",
}
grantedRbacUsersFlag = cli.StringSliceFlag{
Name: "rbac-user",
Usage: "The name of the RBAC user that should be granted access to Tiller. Pass in multiple times for multiple users.",
}
grantedServiceAccountsFlag = cli.StringSliceFlag{
Name: "service-account",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not rbac-service-account for consistency? It's a little awkward to type, but namespacing all 3 under rbac makes the grouping more clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, ServiceAccounts are technically not a part of the RBAC system. They are grouped under core API.

But I think that is a minor detail and I agree with you about the namespace grouping so will do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adjusted to --rbac-service-account

Usage: "The name and namespace of the ServiceAccount (encoded as NAMESPACE/NAME) that should be granted access to tiller. Pass in multiple times for multiple accounts.",
@@ -131,6 +135,18 @@ var (
Name: "set-kubectl-namespace",
Usage: "Set the kubectl context default namespace to match the namespace that Tiller deploys resources into.",
}
configuringRBACUserFlag = cli.StringFlag{
Name: "rbac-user",
Usage: "Name of RBAC user that configuration is for. Only one of --rbac-user, --rbac-group, or --service-account can be specified.",
}
configuringRBACGroupFlag = cli.StringFlag{
Name: "rbac-group",
Usage: "Name of RBAC group that configuration is for. Only one of --rbac-user, --rbac-group, or --service-account can be specified.",
}
configuringServiceAccountFlag = cli.StringFlag{
Name: "service-account",
Usage: "Name of the Service Account that configuration is for. Only one of --rbac-user, --rbac-group, or --service-account can be specified.",
}
)

// SetupHelmCommand creates the cli.Command entry for the helm subcommand of kubergrunt
@@ -191,10 +207,15 @@ Note: By default, this will not undeploy the Helm server if there are any deploy
- Download the client TLS certificate key pair that you have access to.
- Install the TLS certificate key pair in the helm home directory. The helm home directory can be modified with the --helm-home option.
- Install an environment file compatible with your platform that can be sourced to setup variables to configure default parameters for the helm client to access the Tiller install.
- Optionally set the kubectl context default namespace to be the one that Tiller manages.`,
- Optionally set the kubectl context default namespace to be the one that Tiller manages.

You must pass in an identifier for your account. This is either the name of the RBAC user (--rbac-user), RBAC group (--rbac-group), or ServiceAccount (--service-account) that you are authenticating as.`,
Action: configureHelmClient,
Flags: []cli.Flag{
helmHomeFlag,
configuringRBACUserFlag,
configuringRBACGroupFlag,
configuringServiceAccountFlag,
tillerNamespaceFlag,
resourceNamespaceFlag,
setKubectlNamespaceFlag,
@@ -210,6 +231,7 @@ Note: By default, this will not undeploy the Helm server if there are any deploy
Flags: []cli.Flag{
tillerNamespaceFlag,
grantedRbacGroupsFlag,
grantedRbacUsersFlag,
grantedServiceAccountsFlag,
tlsCommonNameFlag,
tlsOrgFlag,
@@ -330,18 +352,37 @@ func configureHelmClient(cliContext *cli.Context) error {
if err != nil {
return err
}
resourceNamespace, err := entrypoint.StringFlagRequiredE(cliContext, resourceNamespaceFlag.Name)
kubectlOptions, err := parseKubectlOptions(cliContext)
if err != nil {
return err
}

// Get mutexed info (entity name)
configuringRBACUser := cliContext.String(configuringRBACUserFlag)
configuringRBACGroup := cliContext.String(configuringRBACGroupFlag)
configuringServiceAccount := cliContext.String(configuringServiceAccountFlag)
setEntities := 0
var entityName string
if configuringRBACUser != "" {
setEntities += 1
entityName = configuringRBACUser
}
if configuringRBACGroup != "" {
setEntities += 1
entityName = configuringRBACGroup
}
if configuringServiceAccount != "" {
setEntities += 1
entityName = configuringServiceAccount
}
if setEntities != 1 {
return MutuallyExclusiveFlagError("Exactly one of --rbac-user, --rbac-group, or --service-account must be set")
}

// Get optional info
setKubectlNamespace := cliContext.Bool(setKubectlNamespaceFlag.Name)
resourceNamespace := cliContext.String(resourceNamespaceFlag.Name)
if resourceNamespace == "" {
logger.Warnf("Did not get a specific resource namespace. Defaulting to the provided Tiller namespace.")
resourceNamespace = tillerNamespace
}

return helm.ConfigureClient(kubectlOptions, helmHome, tillerNamespace, resourceNamespace, setKubectlNamespace)
}
@@ -361,15 +402,12 @@ func grantHelmAccess(cliContext *cli.Context) error {
return err
}
rbacGroups := cliContext.StringSlice(grantedRbacGroupsFlag.Name)
rbacUsers := cliContext.StringSlice(grantedRbacUsersFlag.Name)
serviceAccounts := cliContext.StringSlice(grantedServiceAccountsFlag.Name)
if len(rbacGroups) == 0 && len(serviceAccounts) == 0 {
if len(rbacGroups) == 0 && len(rbacUsers) && len(serviceAccounts) == 0 {
return entrypoint.NewRequiredArgsError("At least one --rbac-group or --service-account is required")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But not --rbac-user?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks forgot that when I added in rbac user support

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

}
serviceAccountInfo, err := serviceAccountsToServiceAccountInfo(serviceAccounts)
if err != nil {
return err
}
return helm.GrantAccess(kubectlOptions, tlsOptions, tillerNamespace, rbacGroups, serviceAccountInfo)
return helm.GrantAccess(kubectlOptions, tlsOptions, tillerNamespace, rbacGroups, rbacUsers, serviceAccounts)
}

// revokeHelmAccess is the action function for the helm revoke command.
@@ -442,20 +480,3 @@ func tlsDistinguishedNameFlagsAsPkixName(cliContext *cli.Context) (pkix.Name, er
}
return distinguishedName, nil
}

// serviceAccountsToServiceAccountInfo takes string encoded service account information and converts them to the
// ServiceAccountInfo struct.
func serviceAccountsToServiceAccountInfo(serviceAccounts []string) ([]helm.ServiceAccountInfo, error) {
serviceAccountInfo := []helm.ServiceAccountInfo{}
for _, serviceAccount := range serviceAccounts {
splitServiceAccount := strings.Split(serviceAccount, "/")
if len(splitServiceAccount) != 2 {
return nil, InvalidServiceAccountInfo{serviceAccount}
}
serviceAccountInfo = append(serviceAccountInfo, helm.ServiceAccountInfo{
Namespace: splitServiceAccount[0],
Name: splitServiceAccount[1],
})
}
return serviceAccountInfo, nil
}
70 changes: 67 additions & 3 deletions helm/configure.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package helm

import (
"fmt"
"io/ioutil"
"path/filepath"

"github.com/gruntwork-io/gruntwork-cli/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/gruntwork-io/kubergrunt/kubectl"
"github.com/gruntwork-io/kubergrunt/logging"
)
@@ -15,19 +23,31 @@ func ConfigureClient(
tillerNamespace string,
resourceNamespace string,
setKubectlNamespace bool,
rbacEntityName string,
) error {
logger := logging.GetProjectLogger()
logger.Infof("Setting up local helm client to access Tiller server deployed in namespace %s.", tillerNamespace)

logger.Info("Checking if authorized to access specified Tiller server.")
// TODO: Check for
// - Access to TLS certs. If unavailable, mention they need to be granted access.
// Check for
// - Access to Tiller pod. If unavailable, mention they need to be granted access, pod should be deployed, or change
// namespace.
if err := verifyAccessToTillerPod(kubectlOptions, tillerNamespace); err != nil {
logger.Errorf("You do not have permissions to access the Tiller endpoint in namespace %s, or Tiller does not exist.", tillerNamespace)
return err
}
// - Access to TLS certs. If unavailable, mention they need to be granted access.
secret, err := getClientCertsSecret(kubectlOptions, tillerNamespace, rbacEntityName)
if err != nil {
logger.Errorf("You do not have permissions to access the client certs for Tiller deployed in namespace %s, or they do not exist.", tillerNamespace)
return err
}
logger.Info("Confirmed authorized to access specified Tiller server.")

logger.Info("Downloading TLS certificates to access specified Tiller server.")
// TODO
if err := downloadTLSCertificatesToHelmHome(helmHome, secret); err != nil {
return err
}
logger.Info("Successfully downloaded TLS certificates.")

logger.Info("Generating environment file to setup helm client.")
@@ -43,3 +63,47 @@ func ConfigureClient(
logger.Infof("Successfully set up local helm client to access Tiller server deployed in namespace %s. Be sure to source the environment file (%s/env) before using the helm client.", tillerNamespace, helmHome)
return nil
}

// verifyAccessToTillerPod checks if the authenticated client has access to the Tiller pod endpoint.
func verifyAccessToTillerPod(kubectlOptions *kubectl.KubectlOptions, tillerNamespace string) error {
filters := metav1.ListOptions{LabelSelector: "app=helm,name=tiller"}
pods, err := kubectl.ListPods(kubectlOptions, tillerNamespace, filters)
if err != nil {
return err
}
if len(pods) == 0 {
msg := fmt.Sprintf("Could not find Tiller pod in namespace %s.", tillerNamespace)
return errors.WithStackTrace(HelmValidationError{msg})
}
return nil
}

// getClientCertsSecret gets the Kubernetes Secret resource corresponding to be a client certificate key pair for
// authenticating with the Tiller instance deployed in the provided namespace.
func getClientCertsSecret(
kubectlOptions *kubectl.KubectlOptions,
tillerNamespace string,
rbacEntityName string,
) (*corev1.Secret, error) {
clientSecretName := getTillerClientCertSecretName(rbacEntityName)
return kubectl.GetSecret(kubectlOptions, tillerNamespace, clientSecretName)
}

func downloadTLSCertificatesToHelmHome(helmHome string, secret *corev1.Secret) error {
decodedCACertData := secret.Data["ca.crt"]
if err := ioutil.WriteFile(filepath.Join(helmHome, "ca.pem"), decodedCACertData, 0644); err != nil {
return errors.WithStackTrace(err)
}

decodedClientPrivateKeyData := secret.Data["client.pem"]
if err := ioutil.WriteFile(filepath.Join(helmHome, "key.pem"), decodedClientPrivateKeyData, 0644); err != nil {
return errors.WithStackTrace(err)
}

decodedClientCertData := secret.Data["client.crt"]
if err := ioutil.WriteFile(filepath.Join(helmHome, "cert.pem"), decodedClientCertData, 0644); err != nil {
return errors.WithStackTrace(err)
}

return nil
}
10 changes: 10 additions & 0 deletions helm/credentials.go
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ func StoreCertificateKeyPairAsKubernetesSecret(
annotations map[string]string,
nameBase string,
certificateKeyPairPath tls.CertificateKeyPairPath,
caCertPath string,
) error {
secret := kubectl.PrepareSecret(secretNamespace, secretName, labels, annotations)
err := kubectl.AddToSecretFromFile(secret, fmt.Sprintf("%s.crt", nameBase), certificateKeyPairPath.CertificatePath)
@@ -29,5 +30,14 @@ func StoreCertificateKeyPairAsKubernetesSecret(
if err != nil {
return err
}

// If we also want to store the CA certificate that can be used to validate server or client
if caCertPath != "" {
err = kubectl.AddToSecretFromFile(secret, "ca.crt", caCertPath)
if err != nil {
return err
}
}

return kubectl.CreateSecret(kubectlOptions, secret)
}
5 changes: 5 additions & 0 deletions helm/credentials_test.go
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ func TestStoreCertificateKeyPairAsKubernetesSecretStoresAllFiles(t *testing.T) {
map[string]string{},
baseName,
certificateKeyPairPath,
"",
)
require.NoError(t, err)

@@ -52,6 +53,10 @@ func TestStoreCertificateKeyPairAsKubernetesSecretStoresAllFiles(t *testing.T) {
assert.Equal(t, secret.Data[fmt.Sprintf("%s.pub", baseName)], mustReadFile(t, certificateKeyPairPath.PublicKeyPath))
}

func TestStoreCertificateKeyPairAsKubernetesSecretStoresCACert(t *testing.T) {
t.Fatalf("Not implemented")
}

func mustReadFile(t *testing.T, path string) []byte {
data, err := ioutil.ReadFile(path)
require.NoError(t, err)
1 change: 1 addition & 0 deletions helm/deploy.go
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@ func Deploy(
map[string]string{},
"ca",
caKeyPairPath,
"",
)
if err != nil {
logger.Errorf("Error uploading CA certificate key pair as a secret: %s", err)
2 changes: 1 addition & 1 deletion helm/deploy_test.go
Original file line number Diff line number Diff line change
@@ -60,7 +60,7 @@ func TestValidateRequiredResourcesForDeploy(t *testing.T) {
// 2. Upload certificate key pairs to Kubernetes secrets
// 3. Deploy Helm with TLS enabled in the specified namespace
// 4. Grant access to helm
// 5. [TODO] Configure helm client
// 5. Configure helm client
// 6. Deploy a helm chart
// 7. Undeploy helm
func TestHelmDeployConfigureUndeploy(t *testing.T) {
9 changes: 9 additions & 0 deletions helm/errors.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,15 @@ import (
"fmt"
)

// InvalidServiceAccountInfo error is returned when the encoded service account is not encoded correctly.
type InvalidServiceAccountInfo struct {
EncodedServiceAccount string
}

func (err InvalidServiceAccountInfo) Error() string {
return fmt.Sprintf("Invalid encoding for ServiceAccount string %s. Expected NAMESPACE/NAME.", err.EncodedServiceAccount)
}

// HelmValidationError is returned when a command validation fails.
type HelmValidationError struct {
Message string
Loading