Allow setting a default service account for impersonation
Introduce the flag `--default-service-account` for allowing cluster admins to enforce impersonation for resources reconciliation. Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
This commit is contained in:
parent
5a293d2dc4
commit
0173eaa0df
|
@ -80,6 +80,7 @@ type HelmReleaseReconciler struct {
|
|||
EventRecorder kuberecorder.EventRecorder
|
||||
ExternalEventRecorder *events.Recorder
|
||||
MetricsRecorder *metrics.Recorder
|
||||
DefaultServiceAccount string
|
||||
NoCrossNamespaceRef bool
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
if name != "" {
|
||||
username := fmt.Sprintf("system:serviceaccount:%s:%s", hr.GetNamespace(), name)
|
||||
restConfig.Impersonate = rest.ImpersonationConfig{UserName: username}
|
||||
return username
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
secretName := types.NamespacedName{
|
||||
Namespace: hr.Namespace,
|
||||
Name: hr.Spec.ServiceAccountName,
|
||||
}
|
||||
|
||||
for _, secret := range serviceAccount.Secrets {
|
||||
if strings.HasPrefix(secret.Name, fmt.Sprintf("%s-token", serviceAccount.Name)) {
|
||||
secretName.Name = secret.Name
|
||||
break
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
@ -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/)
|
||||
|
|
1
go.mod
1
go.mod
|
@ -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
|
||||
|
|
1
go.sum
1
go.sum
|
@ -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=
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
@ -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
main.go
3
main.go
|
@ -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.")
|
||||
|
@ -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)
|
||||
|
@ -143,6 +145,7 @@ func main() {
|
|||
ExternalEventRecorder: eventRecorder,
|
||||
MetricsRecorder: metricsRecorder,
|
||||
NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs,
|
||||
DefaultServiceAccount: defaultServiceAccount,
|
||||
}).SetupWithManager(mgr, controllers.HelmReleaseReconcilerOptions{
|
||||
MaxConcurrentReconciles: concurrent,
|
||||
DependencyRequeueInterval: requeueDependency,
|
||||
|
|
Loading…
Reference in New Issue