Skip to content

Commit

Permalink
Merge pull request #480 from fluxcd/imprv-kube
Browse files Browse the repository at this point in the history
  • Loading branch information
hiddeco committed May 12, 2022
2 parents e78a6f0 + 1bed542 commit 6254549
Show file tree
Hide file tree
Showing 11 changed files with 798 additions and 83 deletions.
55 changes: 12 additions & 43 deletions controllers/helmrelease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type HelmReleaseReconciler struct {
MetricsRecorder *metrics.Recorder
DefaultServiceAccount string
NoCrossNamespaceRef bool
ClientOpts fluxClient.Options
KubeConfigOpts fluxClient.KubeConfigOptions
}

Expand Down Expand Up @@ -295,7 +296,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
}
Expand Down Expand Up @@ -472,23 +473,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) {
opts := []kube.ClientGetterOption{kube.WithClientOptions(r.ClientOpts)}
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(),
Expand All @@ -498,33 +487,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
}

if r.DefaultServiceAccount != "" || hr.Spec.ServiceAccountName != "" {
return kube.NewInClusterRESTClientGetter(&config, hr.GetReleaseNamespace()), nil
opts = append(opts, kube.WithKubeConfig(kubeConfig, r.KubeConfigOpts))
}

return kube.NewInClusterRESTClientGetter(r.Config, hr.GetReleaseNamespace()), nil

return kube.BuildClientGetter(hr.GetReleaseNamespace(), opts...)
}

// composeValues attempts to resolve all v2beta1.ValuesReference resources
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions internal/kube/builder.go
Original file line number Diff line number Diff line change
@@ -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 (
"github.com/fluxcd/pkg/runtime/client"
"k8s.io/cli-runtime/pkg/genericclioptions"
)

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 {
namespace string
kubeConfig []byte
impersonateAccount string
clientOptions client.Options
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, opts client.KubeConfigOptions) func(o *clientGetterOptions) {
return func(o *clientGetterOptions) {
o.kubeConfig = kubeConfig
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) {
return func(o *clientGetterOptions) {
o.impersonateAccount = accountName
}
}

// BuildClientGetter builds a genericclioptions.RESTClientGetter based on the
// 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{
namespace: namespace,
}
for _, opt := range opts {
opt(o)
}
if len(o.kubeConfig) > 0 {
return NewMemoryRESTClientGetter(o.kubeConfig, namespace, o.impersonateAccount, o.clientOptions, o.kubeConfigOptions), nil
}
return NewInClusterRESTClientGetter(namespace, o.impersonateAccount, &o.clientOptions)
}
119 changes: 119 additions & 0 deletions internal/kube/builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
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/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"
)

func TestBuildClientGetter(t *testing.T) {
t.Run("with namespace and retrieved config", func(t *testing.T) {
g := NewWithT(t)
cfg := &rest.Config{Host: "https://example.com"}
ctrl.GetConfig = func() (*rest.Config, error) {
return cfg, nil
}

namespace := "a-namespace"
getter, err := BuildClientGetter(namespace)
g.Expect(err).ToNot(HaveOccurred())
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.APIServer).ToNot(BeNil())
g.Expect(*flags.APIServer).To(Equal(cfg.Host))
})

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
clusters:
- cluster:
server: https://example.com
name: example-cluster
contexts:
- context:
cluster: example-cluster
namespace: flux-system
kind: Config
preferences: {}
users:`)
clientOpts := client.Options{QPS: 600, Burst: 1000}
cfgOpts := client.KubeConfigOptions{InsecureTLS: true}
impersonate := "jane"

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.clientOpts).To(Equal(clientOpts))
g.Expect(got.kubeConfigOpts).To(Equal(cfgOpts))
g.Expect(got.impersonateAccount).To(Equal(impersonate))
})

t.Run("with impersonate account", func(t *testing.T) {
g := NewWithT(t)
ctrl.GetConfig = mockGetConfig

namespace := "a-namespace"
impersonate := "frank"
getter, err := BuildClientGetter(namespace, WithImpersonate(impersonate))
g.Expect(err).ToNot(HaveOccurred())
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 DefaultServiceAccount", func(t *testing.T) {
g := NewWithT(t)
ctrl.GetConfig = mockGetConfig

namespace := "a-namespace"
DefaultServiceAccountName = "frank"
getter, err := BuildClientGetter(namespace)
g.Expect(err).ToNot(HaveOccurred())
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"))
})
}

func mockGetConfig() (*rest.Config, error) {
return &rest.Config{}, nil
}
Loading

0 comments on commit 6254549

Please sign in to comment.