Cherry-pick kube changes from dev

This is a partial cherry-pick of commit ae4f499e87, including
changes around `kube`. This to include some of the changes around the
construction of the ConfigFlags RESTClientGetter, as an attempt to
solve token refresh issues.

Signed-off-by: Hidde Beydals <hello@hidde.co>
This commit is contained in:
Hidde Beydals 2022-05-10 12:33:34 +02:00
parent e78a6f0973
commit 4371610e4b
11 changed files with 750 additions and 71 deletions

View File

@ -295,7 +295,7 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context,
log := ctrl.LoggerFrom(ctx)
// Initialize Helm action runner
getter, err := r.getRESTClientGetter(ctx, hr)
getter, err := r.buildRESTClientGetter(ctx, hr)
if err != nil {
return v2.HelmReleaseNotReady(hr, v2.InitFailedReason, err.Error()), err
}
@ -472,23 +472,11 @@ func (r *HelmReleaseReconciler) checkDependencies(hr v2.HelmRelease) error {
return nil
}
func (r *HelmReleaseReconciler) setImpersonationConfig(restConfig *rest.Config, hr v2.HelmRelease) string {
name := r.DefaultServiceAccount
if sa := hr.Spec.ServiceAccountName; sa != "" {
name = sa
func (r *HelmReleaseReconciler) buildRESTClientGetter(ctx context.Context, hr v2.HelmRelease) (genericclioptions.RESTClientGetter, error) {
var opts []kube.ClientGetterOption
if hr.Spec.ServiceAccountName != "" {
opts = append(opts, kube.WithImpersonate(hr.Spec.ServiceAccountName))
}
if name != "" {
username := fmt.Sprintf("system:serviceaccount:%s:%s", hr.GetNamespace(), name)
restConfig.Impersonate = rest.ImpersonationConfig{UserName: username}
return username
}
return ""
}
func (r *HelmReleaseReconciler) getRESTClientGetter(ctx context.Context, hr v2.HelmRelease) (genericclioptions.RESTClientGetter, error) {
config := *r.Config
impersonateAccount := r.setImpersonationConfig(&config, hr)
if hr.Spec.KubeConfig != nil {
secretName := types.NamespacedName{
Namespace: hr.GetNamespace(),
@ -498,32 +486,13 @@ func (r *HelmReleaseReconciler) getRESTClientGetter(ctx context.Context, hr v2.H
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
switch {
case hr.Spec.KubeConfig.SecretRef.Key != "":
key := hr.Spec.KubeConfig.SecretRef.Key
kubeConfig = secret.Data[key]
if kubeConfig == nil {
return nil, fmt.Errorf("KubeConfig secret '%s' does not contain a '%s' key with a kubeconfig", secretName, key)
}
case secret.Data["value"] != nil:
kubeConfig = secret.Data["value"]
case secret.Data["value.yaml"] != nil:
kubeConfig = secret.Data["value.yaml"]
default:
// User did not specify a key, and the 'value' key was not defined.
return nil, fmt.Errorf("KubeConfig secret '%s' does not contain a 'value' key with a kubeconfig", secretName)
kubeConfig, err := kube.ConfigFromSecret(&secret, hr.Spec.KubeConfig.SecretRef.Key)
if err != nil {
return nil, err
}
return kube.NewMemoryRESTClientGetter(kubeConfig, hr.GetReleaseNamespace(), impersonateAccount, r.Config.QPS, r.Config.Burst, r.KubeConfigOpts), nil
opts = append(opts, kube.WithKubeConfig(kubeConfig, r.Config.QPS, r.Config.Burst, r.KubeConfigOpts))
}
if r.DefaultServiceAccount != "" || hr.Spec.ServiceAccountName != "" {
return kube.NewInClusterRESTClientGetter(&config, hr.GetReleaseNamespace()), nil
}
return kube.NewInClusterRESTClientGetter(r.Config, hr.GetReleaseNamespace()), nil
return kube.BuildClientGetter(r.Config, hr.GetReleaseNamespace(), opts...), nil
}
@ -653,7 +622,7 @@ func (r *HelmReleaseReconciler) reconcileDelete(ctx context.Context, hr v2.HelmR
// Only uninstall the Helm Release if the resource is not suspended.
if !hr.Spec.Suspend {
getter, err := r.getRESTClientGetter(ctx, hr)
getter, err := r.buildRESTClientGetter(ctx, hr)
if err != nil {
return ctrl.Result{}, err
}

2
go.mod
View File

@ -22,6 +22,7 @@ require (
k8s.io/apimachinery v0.23.6
k8s.io/cli-runtime v0.23.6
k8s.io/client-go v0.23.6
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
sigs.k8s.io/controller-runtime v0.11.2
sigs.k8s.io/kustomize/api v0.11.4
sigs.k8s.io/yaml v1.3.0
@ -165,7 +166,6 @@ require (
k8s.io/klog/v2 v2.50.0 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/kubectl v0.23.5 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
oras.land/oras-go v1.1.1 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect

86
internal/kube/builder.go Normal file
View File

@ -0,0 +1,86 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kube
import (
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
"github.com/fluxcd/pkg/runtime/client"
)
const (
// DefaultKubeConfigSecretKey is the default data key ConfigFromSecret
// looks at when no data key is provided.
DefaultKubeConfigSecretKey = "value"
// DefaultKubeConfigSecretKeyExt is the default data key ConfigFromSecret
// looks at when no data key is provided, and DefaultKubeConfigSecretKey
// does not exist.
DefaultKubeConfigSecretKeyExt = DefaultKubeConfigSecretKey + ".yaml"
)
// clientGetterOptions used to BuildClientGetter.
type clientGetterOptions struct {
config *rest.Config
namespace string
kubeConfig []byte
burst int
qps float32
impersonateAccount string
kubeConfigOptions client.KubeConfigOptions
}
// ClientGetterOption configures a genericclioptions.RESTClientGetter.
type ClientGetterOption func(o *clientGetterOptions)
// WithKubeConfig creates a MemoryRESTClientGetter configured with the provided
// KubeConfig and other values.
func WithKubeConfig(kubeConfig []byte, qps float32, burst int, opts client.KubeConfigOptions) func(o *clientGetterOptions) {
return func(o *clientGetterOptions) {
o.kubeConfig = kubeConfig
o.qps = qps
o.burst = burst
o.kubeConfigOptions = opts
}
}
// WithImpersonate configures the genericclioptions.RESTClientGetter to
// impersonate the provided account name.
func WithImpersonate(accountName string) func(o *clientGetterOptions) {
return func(o *clientGetterOptions) {
o.impersonateAccount = accountName
}
}
// BuildClientGetter builds a genericclioptions.RESTClientGetter based on the
// provided options and returns the result. config and namespace are mandatory,
// and not expected to be nil or empty.
func BuildClientGetter(config *rest.Config, namespace string, opts ...ClientGetterOption) genericclioptions.RESTClientGetter {
o := &clientGetterOptions{
config: config,
namespace: namespace,
}
for _, opt := range opts {
opt(o)
}
if len(o.kubeConfig) > 0 {
return NewMemoryRESTClientGetter(o.kubeConfig, namespace, o.impersonateAccount, o.qps, o.burst, o.kubeConfigOptions)
}
cfg := *config
SetImpersonationConfig(&cfg, namespace, o.impersonateAccount)
return NewInClusterRESTClientGetter(&cfg, namespace)
}

View File

@ -0,0 +1,109 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kube
import (
"testing"
"github.com/fluxcd/pkg/runtime/client"
. "github.com/onsi/gomega"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
)
func TestBuildClientGetter(t *testing.T) {
t.Run("with config and namespace", func(t *testing.T) {
g := NewWithT(t)
cfg := &rest.Config{
BearerToken: "a-token",
}
namespace := "a-namespace"
getter := BuildClientGetter(cfg, namespace)
g.Expect(getter).To(BeAssignableToTypeOf(&genericclioptions.ConfigFlags{}))
flags := getter.(*genericclioptions.ConfigFlags)
g.Expect(flags.BearerToken).ToNot(BeNil())
g.Expect(*flags.BearerToken).To(Equal(cfg.BearerToken))
g.Expect(flags.Namespace).ToNot(BeNil())
g.Expect(*flags.Namespace).To(Equal(namespace))
})
t.Run("with kubeconfig and impersonate", func(t *testing.T) {
g := NewWithT(t)
namespace := "a-namespace"
cfg := []byte(`apiVersion: v1
clusters:
- cluster:
server: https://example.com
name: example-cluster
contexts:
- context:
cluster: example-cluster
namespace: flux-system
kind: Config
preferences: {}
users:`)
qps := float32(600)
burst := 1000
cfgOpts := client.KubeConfigOptions{InsecureTLS: true}
impersonate := "jane"
getter := BuildClientGetter(&rest.Config{}, namespace, WithKubeConfig(cfg, qps, burst, cfgOpts), WithImpersonate(impersonate))
g.Expect(getter).To(BeAssignableToTypeOf(&MemoryRESTClientGetter{}))
got := getter.(*MemoryRESTClientGetter)
g.Expect(got.namespace).To(Equal(namespace))
g.Expect(got.kubeConfig).To(Equal(cfg))
g.Expect(got.qps).To(Equal(qps))
g.Expect(got.burst).To(Equal(burst))
g.Expect(got.kubeConfigOpts).To(Equal(cfgOpts))
g.Expect(got.impersonateAccount).To(Equal(impersonate))
})
t.Run("with config and impersonate account", func(t *testing.T) {
g := NewWithT(t)
namespace := "a-namespace"
impersonate := "frank"
getter := BuildClientGetter(&rest.Config{}, namespace, WithImpersonate(impersonate))
g.Expect(getter).To(BeAssignableToTypeOf(&genericclioptions.ConfigFlags{}))
flags := getter.(*genericclioptions.ConfigFlags)
g.Expect(flags.Namespace).ToNot(BeNil())
g.Expect(*flags.Namespace).To(Equal(namespace))
g.Expect(flags.Impersonate).ToNot(BeNil())
g.Expect(*flags.Impersonate).To(Equal("system:serviceaccount:a-namespace:frank"))
})
t.Run("with config and DefaultServiceAccount", func(t *testing.T) {
g := NewWithT(t)
namespace := "a-namespace"
DefaultServiceAccountName = "frank"
getter := BuildClientGetter(&rest.Config{}, namespace)
g.Expect(getter).To(BeAssignableToTypeOf(&genericclioptions.ConfigFlags{}))
flags := getter.(*genericclioptions.ConfigFlags)
g.Expect(flags.Namespace).ToNot(BeNil())
g.Expect(*flags.Namespace).To(Equal(namespace))
g.Expect(flags.Impersonate).ToNot(BeNil())
g.Expect(*flags.Impersonate).To(Equal("system:serviceaccount:a-namespace:frank"))
})
}

View File

@ -24,53 +24,69 @@ import (
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/utils/pointer"
"github.com/fluxcd/pkg/runtime/client"
)
// NewInClusterRESTClientGetter creates a new genericclioptions.RESTClientGetter
// using genericclioptions.NewConfigFlags, and configures it with the server,
// authentication, impersonation, and burst and QPS settings, and the provided
// namespace.
func NewInClusterRESTClientGetter(cfg *rest.Config, namespace string) genericclioptions.RESTClientGetter {
flags := genericclioptions.NewConfigFlags(false)
flags.APIServer = &cfg.Host
flags.BearerToken = &cfg.BearerToken
flags.CAFile = &cfg.CAFile
flags.Namespace = &namespace
flags.APIServer = pointer.String(cfg.Host)
flags.BearerToken = pointer.String(cfg.BearerToken)
flags.CAFile = pointer.String(cfg.CAFile)
flags.Namespace = pointer.String(namespace)
flags.WithDiscoveryBurst(cfg.Burst)
flags.WithDiscoveryQPS(cfg.QPS)
if sa := cfg.Impersonate.UserName; sa != "" {
flags.Impersonate = &sa
flags.Impersonate = pointer.String(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 used to load a rest.Config, after being sanitized.
kubeConfig []byte
// kubeConfigOpts control the sanitization of the kubeConfig.
kubeConfigOpts client.KubeConfigOptions
// namespace specifies the namespace the client is configured to.
namespace string
// impersonateAccount configures the rest.ImpersonationConfig account name.
impersonateAccount string
qps float32
burst int
kubeConfigOpts client.KubeConfigOptions
// qps configures the QPS on the discovery.DiscoveryClient.
qps float32
// burst configures the burst on the discovery.DiscoveryClient.
burst int
}
// NewMemoryRESTClientGetter returns a MemoryRESTClientGetter configured with
// the provided values and client.KubeConfigOptions. The provided KubeConfig is
// sanitized, configure the settings for this using client.KubeConfigOptions.
func NewMemoryRESTClientGetter(
kubeConfig []byte,
namespace string,
impersonateAccount string,
impersonate string,
qps float32,
burst int,
kubeConfigOpts client.KubeConfigOptions) genericclioptions.RESTClientGetter {
return &MemoryRESTClientGetter{
kubeConfig: kubeConfig,
namespace: namespace,
impersonateAccount: impersonateAccount,
impersonateAccount: impersonate,
qps: qps,
burst: burst,
kubeConfigOpts: kubeConfigOpts,
}
}
// ToRESTConfig creates a rest.Config with the rest.ImpersonationConfig configured
// with to the impersonation account. It loads the config the KubeConfig bytes and
// sanitizes it using the client.KubeConfigOptions.
func (c *MemoryRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
cfg, err := clientcmd.RESTConfigFromKubeConfig(c.kubeConfig)
if err != nil {
@ -83,23 +99,25 @@ func (c *MemoryRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
return cfg, nil
}
// ToDiscoveryClient returns a discovery.CachedDiscoveryInterface configured
// with ToRESTConfig, and the QPS and Burst settings.
func (c *MemoryRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
config, err := c.ToRESTConfig()
if err != nil {
return nil, err
}
if c.impersonateAccount != "" {
config.Impersonate = rest.ImpersonationConfig{UserName: c.impersonateAccount}
}
config.QPS = c.qps
config.Burst = c.burst
discoveryClient, _ := discovery.NewDiscoveryClientForConfig(config)
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, err
}
return memory.NewMemCacheClient(discoveryClient), nil
}
// ToRESTMapper returns a RESTMapper constructed from ToDiscoveryClient.
func (c *MemoryRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
discoveryClient, err := c.ToDiscoveryClient()
if err != nil {
@ -111,6 +129,9 @@ func (c *MemoryRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
return expander, nil
}
// ToRawKubeConfigLoader returns a clientcmd.ClientConfig using
// clientcmd.DefaultClientConfig. With clientcmd.ClusterDefaults, namespace, and
// impersonate configured as overwrites.
func (c *MemoryRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
// use the standard defaults for this client command

View File

@ -0,0 +1,179 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kube
import (
"testing"
"github.com/fluxcd/pkg/runtime/client"
. "github.com/onsi/gomega"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
)
var cfg = []byte(`current-context: federal-context
apiVersion: v1
clusters:
- cluster:
api-version: v1
server: http://cow.org:8080
insecure-skip-tls-verify: true
name: cow-cluster
contexts:
- context:
cluster: cow-cluster
user: blue-user
name: federal-context
kind: Config
users:
- name: blue-user
user:
token: foo`)
func TestNewInClusterRESTClientGetter(t *testing.T) {
t.Run("api server config", func(t *testing.T) {
g := NewWithT(t)
cfg := &rest.Config{
Host: "https://example.com",
BearerToken: "chase-the-honey",
TLSClientConfig: rest.TLSClientConfig{
CAFile: "afile",
},
}
got := NewInClusterRESTClientGetter(cfg, "")
g.Expect(got).To(BeAssignableToTypeOf(&genericclioptions.ConfigFlags{}))
flags := got.(*genericclioptions.ConfigFlags)
fields := map[*string]*string{
flags.APIServer: &cfg.Host,
flags.BearerToken: &cfg.BearerToken,
flags.CAFile: &cfg.CAFile,
}
for f, ff := range fields {
g.Expect(f).ToNot(BeNil())
g.Expect(f).To(Equal(ff))
g.Expect(f).ToNot(BeIdenticalTo(ff))
}
})
t.Run("namespace", func(t *testing.T) {
g := NewWithT(t)
got := NewInClusterRESTClientGetter(&rest.Config{}, "a-space")
g.Expect(got).To(BeAssignableToTypeOf(&genericclioptions.ConfigFlags{}))
flags := got.(*genericclioptions.ConfigFlags)
g.Expect(flags.Namespace).ToNot(BeNil())
g.Expect(*flags.Namespace).To(Equal("a-space"))
})
t.Run("impersonation", func(t *testing.T) {
g := NewWithT(t)
cfg := &rest.Config{
Impersonate: rest.ImpersonationConfig{
UserName: "system:serviceaccount:namespace:foo",
},
}
got := NewInClusterRESTClientGetter(cfg, "")
g.Expect(got).To(BeAssignableToTypeOf(&genericclioptions.ConfigFlags{}))
flags := got.(*genericclioptions.ConfigFlags)
g.Expect(flags.Impersonate).ToNot(BeNil())
g.Expect(*flags.Impersonate).To(Equal(cfg.Impersonate.UserName))
})
}
func TestMemoryRESTClientGetter_ToRESTConfig(t *testing.T) {
t.Run("loads REST config from KubeConfig", func(t *testing.T) {
g := NewWithT(t)
getter := NewMemoryRESTClientGetter(cfg, "", "", 0, 0, client.KubeConfigOptions{})
got, err := getter.ToRESTConfig()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got.Host).To(Equal("http://cow.org:8080"))
g.Expect(got.TLSClientConfig.Insecure).To(BeFalse())
})
t.Run("sets ImpersonationConfig", func(t *testing.T) {
g := NewWithT(t)
getter := NewMemoryRESTClientGetter(cfg, "", "someone", 0, 0, client.KubeConfigOptions{})
got, err := getter.ToRESTConfig()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got.Impersonate.UserName).To(Equal("someone"))
})
t.Run("uses KubeConfigOptions", func(t *testing.T) {
g := NewWithT(t)
agent := "a static string forever," +
"but static strings can have dreams and hope too"
getter := NewMemoryRESTClientGetter(cfg, "", "someone", 0, 0, client.KubeConfigOptions{
UserAgent: agent,
})
got, err := getter.ToRESTConfig()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got.UserAgent).To(Equal(agent))
})
t.Run("invalid config", func(t *testing.T) {
g := NewWithT(t)
getter := NewMemoryRESTClientGetter([]byte(`invalid`), "", "", 0, 0, client.KubeConfigOptions{})
got, err := getter.ToRESTConfig()
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
})
}
func TestMemoryRESTClientGetter_ToDiscoveryClient(t *testing.T) {
g := NewWithT(t)
getter := NewMemoryRESTClientGetter(cfg, "", "", 400, 800, client.KubeConfigOptions{})
got, err := getter.ToDiscoveryClient()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).ToNot(BeNil())
}
func TestMemoryRESTClientGetter_ToRESTMapper(t *testing.T) {
g := NewWithT(t)
getter := NewMemoryRESTClientGetter(cfg, "", "", 400, 800, client.KubeConfigOptions{})
got, err := getter.ToRESTMapper()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).ToNot(BeNil())
}
func TestMemoryRESTClientGetter_ToRawKubeConfigLoader(t *testing.T) {
g := NewWithT(t)
getter := NewMemoryRESTClientGetter(cfg, "a-namespace", "impersonate", 0, 0, client.KubeConfigOptions{})
got := getter.ToRawKubeConfigLoader()
g.Expect(got).ToNot(BeNil())
cfg, err := got.ClientConfig()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(cfg.Impersonate.UserName).To(Equal("impersonate"))
ns, _, err := got.Namespace()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(ns).To(Equal("a-namespace"))
}

51
internal/kube/config.go Normal file
View File

@ -0,0 +1,51 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kube
import (
"fmt"
corev1 "k8s.io/api/core/v1"
)
// ConfigFromSecret returns the KubeConfig data from the provided key in the
// given Secret, or attempts to load the data from the default `value` and
// `value.yaml` keys. If a Secret is provided but no key with data can be
// found, an error is returned. The secret may be nil, in which case no bytes
// nor error are returned. Validation of the data is expected to happen while
// decoding the bytes.
func ConfigFromSecret(secret *corev1.Secret, key string) ([]byte, error) {
var kubeConfig []byte
if secret != nil {
secretName := fmt.Sprintf("%s/%s", secret.Namespace, secret.Name)
switch {
case key != "":
kubeConfig = secret.Data[key]
if kubeConfig == nil {
return nil, fmt.Errorf("KubeConfig secret '%s' does not contain a '%s' key with data", secretName, key)
}
case secret.Data[DefaultKubeConfigSecretKey] != nil:
kubeConfig = secret.Data[DefaultKubeConfigSecretKey]
case secret.Data[DefaultKubeConfigSecretKeyExt] != nil:
kubeConfig = secret.Data[DefaultKubeConfigSecretKeyExt]
default:
// User did not specify a key, and the 'value' key was not defined.
return nil, fmt.Errorf("KubeConfig secret '%s' does not contain a '%s' or '%s' key with data", secretName, DefaultKubeConfigSecretKey, DefaultKubeConfigSecretKeyExt)
}
}
return kubeConfig, nil
}

View File

@ -0,0 +1,143 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kube
import (
"testing"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestConfigFromSecret(t *testing.T) {
t.Run("with default key", func(t *testing.T) {
g := NewWithT(t)
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "super-secret",
Namespace: "vault",
},
Data: map[string][]byte{
DefaultKubeConfigSecretKey: []byte("good"),
// Also confirm priority.
DefaultKubeConfigSecretKeyExt: []byte("bad"),
},
}
got, err := ConfigFromSecret(secret, "")
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(Equal(secret.Data[DefaultKubeConfigSecretKey]))
})
t.Run("with default key with ext", func(t *testing.T) {
g := NewWithT(t)
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "super-secret",
Namespace: "vault",
},
Data: map[string][]byte{
DefaultKubeConfigSecretKeyExt: []byte("good"),
},
}
got, err := ConfigFromSecret(secret, "")
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(Equal(secret.Data[DefaultKubeConfigSecretKeyExt]))
})
t.Run("with key", func(t *testing.T) {
g := NewWithT(t)
key := "cola.recipe"
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "super-secret",
Namespace: "vault",
},
Data: map[string][]byte{
key: []byte("snow"),
},
}
got, err := ConfigFromSecret(secret, key)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(Equal(secret.Data[key]))
})
t.Run("invalid key", func(t *testing.T) {
g := NewWithT(t)
key := "black-hole"
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "super-secret",
Namespace: "vault",
},
Data: map[string][]byte{},
}
got, err := ConfigFromSecret(secret, key)
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
g.Expect(err.Error()).To(ContainSubstring("secret 'vault/super-secret' does not contain a 'black-hole' key "))
g.Expect(got).To(Equal(secret.Data[key]))
})
t.Run("key without data", func(t *testing.T) {
g := NewWithT(t)
key := "void"
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "super-secret",
Namespace: "vault",
},
Data: map[string][]byte{
key: nil,
},
}
got, err := ConfigFromSecret(secret, key)
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
g.Expect(err.Error()).To(ContainSubstring("does not contain a 'void' key with data"))
})
t.Run("no keys", func(t *testing.T) {
g := NewWithT(t)
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "super-secret",
Namespace: "vault",
},
Data: map[string][]byte{},
}
got, err := ConfigFromSecret(secret, "")
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
g.Expect(err.Error()).To(ContainSubstring("does not contain a 'value' or 'value.yaml'"))
})
t.Run("nil secret", func(t *testing.T) {
g := NewWithT(t)
got, err := ConfigFromSecret(nil, "")
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(BeNil())
})
}

View File

@ -0,0 +1,47 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kube
import (
"fmt"
"k8s.io/client-go/rest"
)
// DefaultServiceAccountName can be set at runtime to enable a fallback account
// name when no service account name is provided to SetImpersonationConfig.
var DefaultServiceAccountName string
// userNameFormat is the format of a system service account user name string.
// It formats into `system:serviceaccount:namespace:name`.
const userNameFormat = "system:serviceaccount:%s:%s"
// SetImpersonationConfig configures the provided service account name if
// given, or the DefaultServiceAccountName as a fallback if set. It returns
// the configured impersonation username, or an empty string.
func SetImpersonationConfig(cfg *rest.Config, namespace, serviceAccount string) string {
name := DefaultServiceAccountName
if serviceAccount != "" {
name = serviceAccount
}
if name != "" && namespace != "" {
username := fmt.Sprintf(userNameFormat, namespace, name)
cfg.Impersonate = rest.ImpersonationConfig{UserName: username}
return username
}
return ""
}

View File

@ -0,0 +1,75 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kube
import (
"testing"
. "github.com/onsi/gomega"
"k8s.io/client-go/rest"
)
func TestSetImpersonationConfig(t *testing.T) {
t.Run("DefaultServiceAccountName", func(t *testing.T) {
g := NewWithT(t)
DefaultServiceAccountName = "default"
namespace := "test"
expect := "system:serviceaccount:" + namespace + ":" + DefaultServiceAccountName
cfg := &rest.Config{}
name := SetImpersonationConfig(cfg, namespace, "")
g.Expect(name).To(Equal(expect))
g.Expect(cfg.Impersonate.UserName).ToNot(BeEmpty())
g.Expect(cfg.Impersonate.UserName).To(Equal(name))
})
t.Run("overwrite DefaultServiceAccountName", func(t *testing.T) {
g := NewWithT(t)
DefaultServiceAccountName = "default"
namespace := "test"
serviceAccount := "different"
expect := "system:serviceaccount:" + namespace + ":" + serviceAccount
cfg := &rest.Config{}
name := SetImpersonationConfig(cfg, namespace, serviceAccount)
g.Expect(name).To(Equal(expect))
g.Expect(cfg.Impersonate.UserName).ToNot(BeEmpty())
g.Expect(cfg.Impersonate.UserName).To(Equal(name))
})
t.Run("without namespace", func(t *testing.T) {
g := NewWithT(t)
serviceAccount := "account"
cfg := &rest.Config{}
name := SetImpersonationConfig(cfg, "", serviceAccount)
g.Expect(name).To(BeEmpty())
g.Expect(cfg.Impersonate.UserName).To(BeEmpty())
})
t.Run("no arguments", func(t *testing.T) {
g := NewWithT(t)
cfg := &rest.Config{}
name := SetImpersonationConfig(cfg, "", "")
g.Expect(name).To(BeEmpty())
g.Expect(cfg.Impersonate.UserName).To(BeEmpty())
})
}

19
main.go
View File

@ -43,6 +43,7 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2beta1"
"github.com/fluxcd/helm-controller/controllers"
intkube "github.com/fluxcd/helm-controller/internal/kube"
// +kubebuilder:scaffold:imports
)
@ -76,7 +77,6 @@ func main() {
aclOptions acl.Options
leaderElectionOptions leaderelection.Options
rateLimiterOptions helper.RateLimiterOptions
defaultServiceAccount string
)
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
@ -87,7 +87,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.")
flag.StringVar(&intkube.DefaultServiceAccountName, "default-service-account", "", "Default service account used for impersonation.")
clientOptions.BindFlags(flag.CommandLine)
logOptions.BindFlags(flag.CommandLine)
aclOptions.BindFlags(flag.CommandLine)
@ -139,14 +139,13 @@ func main() {
}
if err = (&controllers.HelmReleaseReconciler{
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
Scheme: mgr.GetScheme(),
EventRecorder: eventRecorder,
MetricsRecorder: metricsRecorder,
NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs,
DefaultServiceAccount: defaultServiceAccount,
KubeConfigOpts: kubeConfigOpts,
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
Scheme: mgr.GetScheme(),
EventRecorder: eventRecorder,
MetricsRecorder: metricsRecorder,
NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs,
KubeConfigOpts: kubeConfigOpts,
}).SetupWithManager(mgr, controllers.HelmReleaseReconcilerOptions{
MaxConcurrentReconciles: concurrent,
DependencyRequeueInterval: requeueDependency,