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:
parent
e78a6f0973
commit
4371610e4b
|
@ -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
2
go.mod
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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"))
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
})
|
||||
}
|
|
@ -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 ""
|
||||
}
|
|
@ -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
19
main.go
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue