Skip to content

Commit

Permalink
Merge pull request #406 from fluxcd/default-service-account
Browse files Browse the repository at this point in the history
  • Loading branch information
hiddeco committed Jan 31, 2022
2 parents 5a293d2 + 0173eaa commit 05a1e00
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 72 deletions.
99 changes: 35 additions & 64 deletions controllers/helmrelease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type HelmReleaseReconciler struct {
EventRecorder kuberecorder.EventRecorder
ExternalEventRecorder *events.Recorder
MetricsRecorder *metrics.Recorder
DefaultServiceAccount string
NoCrossNamespaceRef bool
}

Expand Down Expand Up @@ -456,83 +457,53 @@ func (r *HelmReleaseReconciler) checkDependencies(hr v2.HelmRelease) error {
return nil
}

func (r *HelmReleaseReconciler) getRESTClientGetter(ctx context.Context, hr v2.HelmRelease) (genericclioptions.RESTClientGetter, error) {
if hr.Spec.KubeConfig == nil {
// impersonate service account if specified
if hr.Spec.ServiceAccountName != "" {
token, err := r.getServiceAccountToken(ctx, hr)
if err != nil {
return nil, fmt.Errorf("could not impersonate ServiceAccount '%s': %w", hr.Spec.ServiceAccountName, err)
}

config := *r.Config
config.BearerToken = token
return kube.NewInClusterRESTClientGetter(&config, hr.GetReleaseNamespace()), nil
}

return kube.NewInClusterRESTClientGetter(r.Config, hr.GetReleaseNamespace()), nil
func (r *HelmReleaseReconciler) setImpersonationConfig(restConfig *rest.Config, hr v2.HelmRelease) string {
name := r.DefaultServiceAccount
if sa := hr.Spec.ServiceAccountName; sa != "" {
name = sa
}
secretName := types.NamespacedName{
Namespace: hr.GetNamespace(),
Name: hr.Spec.KubeConfig.SecretRef.Name,
}
var secret corev1.Secret
if err := r.Get(ctx, secretName, &secret); err != nil {
return nil, fmt.Errorf("could not find KubeConfig secret '%s': %w", secretName, err)
}

var kubeConfig []byte
for k, _ := range secret.Data {
if k == "value" || k == "value.yaml" {
kubeConfig = secret.Data[k]
break
}
}

if len(kubeConfig) == 0 {
return nil, fmt.Errorf("KubeConfig secret '%s' does not contain a 'value' key", secretName)
if name != "" {
username := fmt.Sprintf("system:serviceaccount:%s:%s", hr.GetNamespace(), name)
restConfig.Impersonate = rest.ImpersonationConfig{UserName: username}
return username
}
return kube.NewMemoryRESTClientGetter(kubeConfig, hr.GetReleaseNamespace()), nil
return ""
}

func (r *HelmReleaseReconciler) getServiceAccountToken(ctx context.Context, hr v2.HelmRelease) (string, error) {
namespacedName := types.NamespacedName{
Namespace: hr.Namespace,
Name: hr.Spec.ServiceAccountName,
}
func (r *HelmReleaseReconciler) getRESTClientGetter(ctx context.Context, hr v2.HelmRelease) (genericclioptions.RESTClientGetter, error) {
config := *r.Config
impersonateAccount := r.setImpersonationConfig(&config, hr)

var serviceAccount corev1.ServiceAccount
err := r.Client.Get(ctx, namespacedName, &serviceAccount)
if err != nil {
return "", err
}
if hr.Spec.KubeConfig != nil {
secretName := types.NamespacedName{
Namespace: hr.GetNamespace(),
Name: hr.Spec.KubeConfig.SecretRef.Name,
}
var secret corev1.Secret
if err := r.Get(ctx, secretName, &secret); err != nil {
return nil, fmt.Errorf("could not find KubeConfig secret '%s': %w", secretName, err)
}

secretName := types.NamespacedName{
Namespace: hr.Namespace,
Name: hr.Spec.ServiceAccountName,
}
var kubeConfig []byte
for k, _ := range secret.Data {
if k == "value" || k == "value.yaml" {
kubeConfig = secret.Data[k]
break
}
}

for _, secret := range serviceAccount.Secrets {
if strings.HasPrefix(secret.Name, fmt.Sprintf("%s-token", serviceAccount.Name)) {
secretName.Name = secret.Name
break
if len(kubeConfig) == 0 {
return nil, fmt.Errorf("KubeConfig secret '%s' does not contain a 'value' key", secretName)
}
return kube.NewMemoryRESTClientGetter(kubeConfig, hr.GetReleaseNamespace(), impersonateAccount), nil
}

var secret corev1.Secret
err = r.Client.Get(ctx, secretName, &secret)
if err != nil {
return "", err
if r.DefaultServiceAccount != "" || hr.Spec.ServiceAccountName != "" {
return kube.NewInClusterRESTClientGetter(&config, hr.GetReleaseNamespace()), nil
}

var token string
if data, ok := secret.Data["token"]; ok {
token = string(data)
} else {
return "", fmt.Errorf("the service account secret '%s' does not containt a token", secretName.String())
}
return kube.NewInClusterRESTClientGetter(r.Config, hr.GetReleaseNamespace()), nil

return token, nil
}

// composeValues attempts to resolve all v2beta1.ValuesReference resources
Expand Down
12 changes: 12 additions & 0 deletions docs/spec/v2beta1/helmreleases.md
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,15 @@ When the controller reconciles the `podinfo` release, it will impersonate the `w
account. If the chart contains cluster level objects like CRDs, the reconciliation will fail since
the account it runs under has no permissions to alter objects outside of the `webapp` namespace.

### Enforce impersonation

On multi-tenant clusters, platform admins can enforce impersonation with the
`--default-service-account` flag.

When the flag is set, all HelmReleases which don't have `spec.serviceAccountName` specified
will use the service account name provided by `--default-service-account=<SA Name>`
in the namespace of the object.

## Remote Clusters / Cluster-API

If the `spec.kubeConfig` field is set, Helm actions will run against the default cluster specified
Expand Down Expand Up @@ -1126,6 +1135,9 @@ kubectl -n default create secret generic prod-kubeconfig \
> from current Cluster API providers. KubeConfigs with cmd-path in them likely won't work without
> a custom, per-provider installation of helm-controller.

When both `spec.kubeConfig` and `spec.ServiceAccountName` are specified,
the controller will impersonate the service account on the target cluster.

## Post Renderers

HelmRelease resources has a built-in [Kustomize](https://kubectl.docs.kubernetes.io/references/kustomize/)
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ require (
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fluxcd/pkg/apis/acl v0.0.3 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-logr/zapr v1.2.0 // indirect
Expand Down
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,6 @@ github.com/fluxcd/pkg/apis/kustomize v0.3.1 h1:wmb5D9e1+Rr3/5O3235ERuj+h2VKUArVf
github.com/fluxcd/pkg/apis/kustomize v0.3.1/go.mod h1:k2HSRd68UwgNmOYBPOd6WbX6a2MH2X/Jeh7e3s3PFPc=
github.com/fluxcd/pkg/apis/meta v0.10.2 h1:pnDBBEvfs4HaKiVAYgz+e/AQ8dLvcgmVfSeBroZ/KKI=
github.com/fluxcd/pkg/apis/meta v0.10.2/go.mod h1:KQ2er9xa6koy7uoPMZjIjNudB5p4tXs+w0GO6fRcy7I=
github.com/fluxcd/pkg/runtime v0.12.3 h1:h21AZ3YG5MAP7DxFF9hfKrP+vFzys2L7CkUbPFjbP/0=
github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5fY0js8mCE4SM=
github.com/fluxcd/pkg/runtime v0.12.4 h1:gA27RG/+adN2/7Qe03PB46Iwmye/MusPCpuS4zQ2fW0=
github.com/fluxcd/pkg/runtime v0.12.4/go.mod h1:gspNvhAqodZgSmK1ZhMtvARBf/NGAlxmaZaIOHkJYsc=
Expand Down
33 changes: 27 additions & 6 deletions internal/kube/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,38 @@ func NewInClusterRESTClientGetter(cfg *rest.Config, namespace string) genericcli
flags.BearerToken = &cfg.BearerToken
flags.CAFile = &cfg.CAFile
flags.Namespace = &namespace
if sa := cfg.Impersonate.UserName; sa != "" {
flags.Impersonate = &sa
}

return flags
}

// MemoryRESTClientGetter is an implementation of the genericclioptions.RESTClientGetter,
// capable of working with an in-memory kubeconfig file.
type MemoryRESTClientGetter struct {
kubeConfig []byte
namespace string
kubeConfig []byte
namespace string
impersonateAccount string
}

func NewMemoryRESTClientGetter(kubeConfig []byte, namespace string) genericclioptions.RESTClientGetter {
func NewMemoryRESTClientGetter(kubeConfig []byte, namespace string, impersonateAccount string) genericclioptions.RESTClientGetter {
return &MemoryRESTClientGetter{
kubeConfig: kubeConfig,
namespace: namespace,
kubeConfig: kubeConfig,
namespace: namespace,
impersonateAccount: impersonateAccount,
}
}

func (c *MemoryRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
return clientcmd.RESTConfigFromKubeConfig(c.kubeConfig)
cfg, err := clientcmd.RESTConfigFromKubeConfig(c.kubeConfig)
if err != nil {
return nil, err
}
if c.impersonateAccount != "" {
cfg.Impersonate = rest.ImpersonationConfig{UserName: c.impersonateAccount}
}
return cfg, nil
}

func (c *MemoryRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
Expand All @@ -59,6 +72,10 @@ func (c *MemoryRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryI
return nil, err
}

if c.impersonateAccount != "" {
config.Impersonate = rest.ImpersonationConfig{UserName: c.impersonateAccount}
}

// The more groups you have, the more discovery requests you need to make.
// given 25 groups (our groups + a few custom resources) with one-ish version each, discovery needs to make 50 requests
// double it just so we don't end up here again for a while. This config is only used for discovery.
Expand Down Expand Up @@ -88,5 +105,9 @@ func (c *MemoryRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig
overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults}
overrides.Context.Namespace = c.namespace

if c.impersonateAccount != "" {
overrides.AuthInfo.Impersonate = c.impersonateAccount
}

return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
}
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func main() {
logOptions logger.Options
aclOptions acl.Options
leaderElectionOptions leaderelection.Options
defaultServiceAccount string
)

flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
Expand All @@ -83,6 +84,7 @@ func main() {
flag.BoolVar(&watchAllNamespaces, "watch-all-namespaces", true,
"Watch for custom resources in all namespaces, if set to false it will only watch the runtime namespace.")
flag.IntVar(&httpRetry, "http-retry", 9, "The maximum number of retries when failing to fetch artifacts over HTTP.")
flag.StringVar(&defaultServiceAccount, "default-service-account", "", "Default service account used for impersonation.")
clientOptions.BindFlags(flag.CommandLine)
logOptions.BindFlags(flag.CommandLine)
aclOptions.BindFlags(flag.CommandLine)
Expand Down Expand Up @@ -143,6 +145,7 @@ func main() {
ExternalEventRecorder: eventRecorder,
MetricsRecorder: metricsRecorder,
NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs,
DefaultServiceAccount: defaultServiceAccount,
}).SetupWithManager(mgr, controllers.HelmReleaseReconcilerOptions{
MaxConcurrentReconciles: concurrent,
DependencyRequeueInterval: requeueDependency,
Expand Down

0 comments on commit 05a1e00

Please sign in to comment.