internal/kube: get REST config from runtime

Signed-off-by: Hidde Beydals <hello@hidde.co>
This commit is contained in:
Hidde Beydals 2022-05-10 15:24:13 +02:00
parent 5784f0644a
commit 1bed542fe4
6 changed files with 112 additions and 78 deletions

View File

@ -83,6 +83,7 @@ type HelmReleaseReconciler struct {
MetricsRecorder *metrics.Recorder
DefaultServiceAccount string
NoCrossNamespaceRef bool
ClientOpts fluxClient.Options
KubeConfigOpts fluxClient.KubeConfigOptions
}
@ -473,7 +474,7 @@ func (r *HelmReleaseReconciler) checkDependencies(hr v2.HelmRelease) error {
}
func (r *HelmReleaseReconciler) buildRESTClientGetter(ctx context.Context, hr v2.HelmRelease) (genericclioptions.RESTClientGetter, error) {
var opts []kube.ClientGetterOption
opts := []kube.ClientGetterOption{kube.WithClientOptions(r.ClientOpts)}
if hr.Spec.ServiceAccountName != "" {
opts = append(opts, kube.WithImpersonate(hr.Spec.ServiceAccountName))
}
@ -490,9 +491,9 @@ func (r *HelmReleaseReconciler) buildRESTClientGetter(ctx context.Context, hr v2
if err != nil {
return nil, err
}
opts = append(opts, kube.WithKubeConfig(kubeConfig, r.Config.QPS, r.Config.Burst, r.KubeConfigOpts))
opts = append(opts, kube.WithKubeConfig(kubeConfig, r.KubeConfigOpts))
}
return kube.BuildClientGetter(r.Config, hr.GetReleaseNamespace(), opts...), nil
return kube.BuildClientGetter(hr.GetReleaseNamespace(), opts...)
}
// composeValues attempts to resolve all v2beta1.ValuesReference resources

View File

@ -17,10 +17,8 @@ limitations under the License.
package kube
import (
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
"github.com/fluxcd/pkg/runtime/client"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
const (
@ -35,12 +33,10 @@ const (
// clientGetterOptions used to BuildClientGetter.
type clientGetterOptions struct {
config *rest.Config
namespace string
kubeConfig []byte
burst int
qps float32
impersonateAccount string
clientOptions client.Options
kubeConfigOptions client.KubeConfigOptions
}
@ -49,15 +45,21 @@ 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) {
func WithKubeConfig(kubeConfig []byte, opts client.KubeConfigOptions) func(o *clientGetterOptions) {
return func(o *clientGetterOptions) {
o.kubeConfig = kubeConfig
o.qps = qps
o.burst = burst
o.kubeConfigOptions = opts
}
}
// WithClientOptions configures the genericclioptions.RESTClientGetter with
// provided options.
func WithClientOptions(opts client.Options) func(o *clientGetterOptions) {
return func(o *clientGetterOptions) {
o.clientOptions = opts
}
}
// WithImpersonate configures the genericclioptions.RESTClientGetter to
// impersonate the provided account name.
func WithImpersonate(accountName string) func(o *clientGetterOptions) {
@ -67,20 +69,18 @@ func WithImpersonate(accountName string) func(o *clientGetterOptions) {
}
// 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 {
// provided options and returns the result. Namespace is not expected to be
// empty. In case it fails to construct using NewInClusterRESTClientGetter, it
// returns an error.
func BuildClientGetter(namespace string, opts ...ClientGetterOption) (genericclioptions.RESTClientGetter, error) {
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)
return NewMemoryRESTClientGetter(o.kubeConfig, namespace, o.impersonateAccount, o.clientOptions, o.kubeConfigOptions), nil
}
cfg := *config
SetImpersonationConfig(&cfg, namespace, o.impersonateAccount)
return NewInClusterRESTClientGetter(&cfg, namespace)
return NewInClusterRESTClientGetter(namespace, o.impersonateAccount, &o.clientOptions)
}

View File

@ -17,34 +17,38 @@ limitations under the License.
package kube
import (
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"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) {
t.Run("with namespace and retrieved config", func(t *testing.T) {
g := NewWithT(t)
cfg := &rest.Config{
BearerToken: "a-token",
cfg := &rest.Config{Host: "https://example.com"}
ctrl.GetConfig = func() (*rest.Config, error) {
return cfg, nil
}
namespace := "a-namespace"
getter := BuildClientGetter(cfg, namespace)
getter, err := BuildClientGetter(namespace)
g.Expect(err).ToNot(HaveOccurred())
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))
g.Expect(flags.APIServer).ToNot(BeNil())
g.Expect(*flags.APIServer).To(Equal(cfg.Host))
})
t.Run("with kubeconfig and impersonate", func(t *testing.T) {
t.Run("with kubeconfig, impersonate and client options", func(t *testing.T) {
g := NewWithT(t)
ctrl.GetConfig = mockGetConfig
namespace := "a-namespace"
cfg := []byte(`apiVersion: v1
@ -59,30 +63,30 @@ contexts:
kind: Config
preferences: {}
users:`)
qps := float32(600)
burst := 1000
clientOpts := client.Options{QPS: 600, Burst: 1000}
cfgOpts := client.KubeConfigOptions{InsecureTLS: true}
impersonate := "jane"
getter := BuildClientGetter(&rest.Config{}, namespace, WithKubeConfig(cfg, qps, burst, cfgOpts), WithImpersonate(impersonate))
getter, err := BuildClientGetter(namespace, WithClientOptions(clientOpts), WithKubeConfig(cfg, cfgOpts), WithImpersonate(impersonate))
g.Expect(err).ToNot(HaveOccurred())
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.clientOpts).To(Equal(clientOpts))
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) {
t.Run("with impersonate account", func(t *testing.T) {
g := NewWithT(t)
ctrl.GetConfig = mockGetConfig
namespace := "a-namespace"
impersonate := "frank"
getter := BuildClientGetter(&rest.Config{}, namespace, WithImpersonate(impersonate))
getter, err := BuildClientGetter(namespace, WithImpersonate(impersonate))
g.Expect(err).ToNot(HaveOccurred())
g.Expect(getter).To(BeAssignableToTypeOf(&genericclioptions.ConfigFlags{}))
flags := getter.(*genericclioptions.ConfigFlags)
@ -92,12 +96,14 @@ users:`)
g.Expect(*flags.Impersonate).To(Equal("system:serviceaccount:a-namespace:frank"))
})
t.Run("with config and DefaultServiceAccount", func(t *testing.T) {
t.Run("with DefaultServiceAccount", func(t *testing.T) {
g := NewWithT(t)
ctrl.GetConfig = mockGetConfig
namespace := "a-namespace"
DefaultServiceAccountName = "frank"
getter := BuildClientGetter(&rest.Config{}, namespace)
getter, err := BuildClientGetter(namespace)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(getter).To(BeAssignableToTypeOf(&genericclioptions.ConfigFlags{}))
flags := getter.(*genericclioptions.ConfigFlags)
@ -107,3 +113,7 @@ users:`)
g.Expect(*flags.Impersonate).To(Equal("system:serviceaccount:a-namespace:frank"))
})
}
func mockGetConfig() (*rest.Config, error) {
return &rest.Config{}, nil
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package kube
import (
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/discovery"
@ -25,29 +26,38 @@ import (
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/utils/pointer"
controllerruntime "sigs.k8s.io/controller-runtime"
"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 {
// authentication, impersonation, client options, and the provided namespace.
// It returns an error if it fails to retrieve a rest.Config.
func NewInClusterRESTClientGetter(namespace, impersonateAccount string, opts *client.Options) (genericclioptions.RESTClientGetter, error) {
cfg, err := controllerruntime.GetConfig()
if err != nil {
return nil, fmt.Errorf("failed to get config for in-cluster REST client: %w", err)
}
SetImpersonationConfig(cfg, namespace, impersonateAccount)
flags := genericclioptions.NewConfigFlags(false)
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 opts != nil {
flags.WithDiscoveryBurst(opts.Burst)
flags.WithDiscoveryQPS(opts.QPS)
}
if sa := cfg.Impersonate.UserName; sa != "" {
flags.Impersonate = pointer.String(sa)
}
// In a container, we are not expected to be able to write to the
// home dir default. However, explicitly disabling this is better.
flags.CacheDir = nil
return flags
return flags, nil
}
// MemoryRESTClientGetter is an implementation of the genericclioptions.RESTClientGetter,
@ -55,16 +65,14 @@ func NewInClusterRESTClientGetter(cfg *rest.Config, namespace string) genericcli
type MemoryRESTClientGetter struct {
// kubeConfig used to load a rest.Config, after being sanitized.
kubeConfig []byte
// kubeConfigOpts control the sanitization of the kubeConfig.
// kubeConfigOpts controls the sanitization of the kubeConfig.
kubeConfigOpts client.KubeConfigOptions
// clientOpts controls the kube client configuration.
clientOpts client.Options
// namespace specifies the namespace the client is configured to.
namespace string
// impersonateAccount configures the rest.ImpersonationConfig account name.
impersonateAccount string
// 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
@ -74,15 +82,13 @@ func NewMemoryRESTClientGetter(
kubeConfig []byte,
namespace string,
impersonate string,
qps float32,
burst int,
clientOpts client.Options,
kubeConfigOpts client.KubeConfigOptions) genericclioptions.RESTClientGetter {
return &MemoryRESTClientGetter{
kubeConfig: kubeConfig,
namespace: namespace,
impersonateAccount: impersonate,
qps: qps,
burst: burst,
clientOpts: clientOpts,
kubeConfigOpts: kubeConfigOpts,
}
}
@ -110,8 +116,8 @@ func (c *MemoryRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryI
return nil, err
}
config.QPS = c.qps
config.Burst = c.burst
config.QPS = c.clientOpts.QPS
config.Burst = c.clientOpts.Burst
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
@ -147,6 +153,5 @@ func (c *MemoryRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig
if c.impersonateAccount != "" {
overrides.AuthInfo.Impersonate = c.impersonateAccount
}
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
}

View File

@ -17,12 +17,14 @@ limitations under the License.
package kube
import (
"fmt"
"testing"
"github.com/fluxcd/pkg/runtime/client"
. "github.com/onsi/gomega"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
)
var cfg = []byte(`current-context: federal-context
@ -45,7 +47,7 @@ users:
token: foo`)
func TestNewInClusterRESTClientGetter(t *testing.T) {
t.Run("api server config", func(t *testing.T) {
t.Run("discover config", func(t *testing.T) {
g := NewWithT(t)
cfg := &rest.Config{
@ -55,8 +57,11 @@ func TestNewInClusterRESTClientGetter(t *testing.T) {
CAFile: "afile",
},
}
got := NewInClusterRESTClientGetter(cfg, "")
ctrl.GetConfig = func() (*rest.Config, error) {
return cfg, nil
}
got, err := NewInClusterRESTClientGetter("", "", nil)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(BeAssignableToTypeOf(&genericclioptions.ConfigFlags{}))
flags := got.(*genericclioptions.ConfigFlags)
@ -72,40 +77,52 @@ func TestNewInClusterRESTClientGetter(t *testing.T) {
}
})
t.Run("config retrieval error", func(t *testing.T) {
g := NewWithT(t)
ctrl.GetConfig = func() (*rest.Config, error) {
return nil, fmt.Errorf("error")
}
got, err := NewInClusterRESTClientGetter("", "", nil)
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring("failed to get config for in-cluster REST client"))
g.Expect(got).To(BeNil())
})
t.Run("namespace", func(t *testing.T) {
g := NewWithT(t)
got := NewInClusterRESTClientGetter(&rest.Config{}, "a-space")
ctrl.GetConfig = mockGetConfig
namespace := "a-space"
got, err := NewInClusterRESTClientGetter(namespace, "", nil)
g.Expect(err).ToNot(HaveOccurred())
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"))
g.Expect(*flags.Namespace).To(Equal(namespace))
})
t.Run("impersonation", func(t *testing.T) {
g := NewWithT(t)
cfg := &rest.Config{
Impersonate: rest.ImpersonationConfig{
UserName: "system:serviceaccount:namespace:foo",
},
}
got := NewInClusterRESTClientGetter(cfg, "")
ctrl.GetConfig = mockGetConfig
ns := "a-namespace"
accountName := "foo"
got, err := NewInClusterRESTClientGetter(ns, accountName, nil)
g.Expect(err).ToNot(HaveOccurred())
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))
g.Expect(*flags.Impersonate).To(Equal(fmt.Sprintf("system:serviceaccount:%s:%s", ns, accountName)))
})
}
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{})
getter := NewMemoryRESTClientGetter(cfg, "", "", client.Options{}, client.KubeConfigOptions{})
got, err := getter.ToRESTConfig()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got.Host).To(Equal("http://cow.org:8080"))
@ -114,7 +131,7 @@ func TestMemoryRESTClientGetter_ToRESTConfig(t *testing.T) {
t.Run("sets ImpersonationConfig", func(t *testing.T) {
g := NewWithT(t)
getter := NewMemoryRESTClientGetter(cfg, "", "someone", 0, 0, client.KubeConfigOptions{})
getter := NewMemoryRESTClientGetter(cfg, "", "someone", client.Options{}, client.KubeConfigOptions{})
got, err := getter.ToRESTConfig()
g.Expect(err).ToNot(HaveOccurred())
@ -126,7 +143,7 @@ func TestMemoryRESTClientGetter_ToRESTConfig(t *testing.T) {
agent := "a static string forever," +
"but static strings can have dreams and hope too"
getter := NewMemoryRESTClientGetter(cfg, "", "someone", 0, 0, client.KubeConfigOptions{
getter := NewMemoryRESTClientGetter(cfg, "", "someone", client.Options{QPS: 400, Burst: 800}, client.KubeConfigOptions{
UserAgent: agent,
})
@ -138,7 +155,7 @@ func TestMemoryRESTClientGetter_ToRESTConfig(t *testing.T) {
t.Run("invalid config", func(t *testing.T) {
g := NewWithT(t)
getter := NewMemoryRESTClientGetter([]byte(`invalid`), "", "", 0, 0, client.KubeConfigOptions{})
getter := NewMemoryRESTClientGetter([]byte("invalid"), "", "", client.Options{QPS: 400, Burst: 800}, client.KubeConfigOptions{})
got, err := getter.ToRESTConfig()
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
@ -148,7 +165,7 @@ func TestMemoryRESTClientGetter_ToRESTConfig(t *testing.T) {
func TestMemoryRESTClientGetter_ToDiscoveryClient(t *testing.T) {
g := NewWithT(t)
getter := NewMemoryRESTClientGetter(cfg, "", "", 400, 800, client.KubeConfigOptions{})
getter := NewMemoryRESTClientGetter(cfg, "", "", client.Options{QPS: 400, Burst: 800}, client.KubeConfigOptions{})
got, err := getter.ToDiscoveryClient()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).ToNot(BeNil())
@ -157,7 +174,7 @@ func TestMemoryRESTClientGetter_ToDiscoveryClient(t *testing.T) {
func TestMemoryRESTClientGetter_ToRESTMapper(t *testing.T) {
g := NewWithT(t)
getter := NewMemoryRESTClientGetter(cfg, "", "", 400, 800, client.KubeConfigOptions{})
getter := NewMemoryRESTClientGetter(cfg, "", "", client.Options{QPS: 400, Burst: 800}, client.KubeConfigOptions{})
got, err := getter.ToRESTMapper()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).ToNot(BeNil())
@ -166,7 +183,7 @@ func TestMemoryRESTClientGetter_ToRESTMapper(t *testing.T) {
func TestMemoryRESTClientGetter_ToRawKubeConfigLoader(t *testing.T) {
g := NewWithT(t)
getter := NewMemoryRESTClientGetter(cfg, "a-namespace", "impersonate", 0, 0, client.KubeConfigOptions{})
getter := NewMemoryRESTClientGetter(cfg, "a-namespace", "impersonate", client.Options{QPS: 400, Burst: 800}, client.KubeConfigOptions{})
got := getter.ToRawKubeConfigLoader()
g.Expect(got).ToNot(BeNil())

View File

@ -145,6 +145,7 @@ func main() {
EventRecorder: eventRecorder,
MetricsRecorder: metricsRecorder,
NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs,
ClientOpts: clientOptions,
KubeConfigOpts: kubeConfigOpts,
}).SetupWithManager(mgr, controllers.HelmReleaseReconcilerOptions{
MaxConcurrentReconciles: concurrent,