Skip to content

Commit 9d1614a

Browse files
authored
Fix memory leak due to misuse of K8s clients (#30)
1 parent be98f83 commit 9d1614a

File tree

7 files changed

+116
-93
lines changed

7 files changed

+116
-93
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ require (
2424
k8s.io/apimachinery v0.23.1
2525
k8s.io/cli-runtime v0.23.1
2626
k8s.io/client-go v0.23.1
27-
k8s.io/kubectl v0.23.1
2827
sigs.k8s.io/controller-runtime v0.11.0
2928
sigs.k8s.io/kubebuilder/v3 v3.3.0
3029
sigs.k8s.io/yaml v1.3.0
@@ -171,6 +170,7 @@ require (
171170
k8s.io/component-base v0.23.1 // indirect
172171
k8s.io/klog/v2 v2.30.0 // indirect
173172
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
173+
k8s.io/kubectl v0.23.1 // indirect
174174
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
175175
sigs.k8s.io/controller-tools v0.7.0 // indirect
176176
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect

pkg/client/actionclient_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ var _ = Describe("ActionClient", func() {
6060
})
6161
var _ = Describe("NewActionClientGetter", func() {
6262
It("should return a valid ActionConfigGetter", func() {
63-
actionConfigGetter := NewActionConfigGetter(cfg, rm, logr.Discard())
63+
actionConfigGetter, err := NewActionConfigGetter(cfg, rm, logr.Discard())
64+
Expect(err).ShouldNot(HaveOccurred())
6465
Expect(NewActionClientGetter(actionConfigGetter)).NotTo(BeNil())
6566
})
6667
})
@@ -86,7 +87,9 @@ var _ = Describe("ActionClient", func() {
8687
obj = testutil.BuildTestCR(gvk)
8788
})
8889
It("should return a valid ActionClient", func() {
89-
acg := NewActionClientGetter(NewActionConfigGetter(cfg, rm, logr.Discard()))
90+
actionConfGetter, err := NewActionConfigGetter(cfg, rm, logr.Discard())
91+
Expect(err).ShouldNot(HaveOccurred())
92+
acg := NewActionClientGetter(actionConfGetter)
9093
ac, err := acg.ActionClientFor(obj)
9194
Expect(err).To(BeNil())
9295
Expect(ac).NotTo(BeNil())
@@ -103,8 +106,8 @@ var _ = Describe("ActionClient", func() {
103106
BeforeEach(func() {
104107
obj = testutil.BuildTestCR(gvk)
105108

106-
var err error
107-
actionConfigGetter := NewActionConfigGetter(cfg, rm, logr.Discard())
109+
actionConfigGetter, err := NewActionConfigGetter(cfg, rm, logr.Discard())
110+
Expect(err).ShouldNot(HaveOccurred())
108111
acg := NewActionClientGetter(actionConfigGetter)
109112
ac, err = acg.ActionClientFor(obj)
110113
Expect(err).To(BeNil())

pkg/client/actionconfig.go

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"context"
2121
"fmt"
2222

23+
"k8s.io/client-go/kubernetes"
24+
2325
"github.com/go-logr/logr"
2426
"helm.sh/helm/v3/pkg/action"
2527
"helm.sh/helm/v3/pkg/kube"
@@ -30,72 +32,68 @@ import (
3032
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3133
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
3234
"k8s.io/client-go/rest"
33-
cmdutil "k8s.io/kubectl/pkg/cmd/util"
3435
"sigs.k8s.io/controller-runtime/pkg/client"
3536
)
3637

3738
type ActionConfigGetter interface {
3839
ActionConfigFor(obj client.Object) (*action.Configuration, error)
3940
}
4041

41-
func NewActionConfigGetter(cfg *rest.Config, rm meta.RESTMapper, log logr.Logger) ActionConfigGetter {
42-
return &actionConfigGetter{
43-
cfg: cfg,
44-
restMapper: rm,
45-
log: log,
46-
}
47-
}
48-
49-
var _ ActionConfigGetter = &actionConfigGetter{}
50-
51-
type actionConfigGetter struct {
52-
cfg *rest.Config
53-
restMapper meta.RESTMapper
54-
log logr.Logger
55-
}
56-
57-
func (acg *actionConfigGetter) ActionConfigFor(obj client.Object) (*action.Configuration, error) {
58-
// Create a RESTClientGetter
59-
rcg := newRESTClientGetter(acg.cfg, acg.restMapper, obj.GetNamespace())
60-
42+
func NewActionConfigGetter(cfg *rest.Config, rm meta.RESTMapper, log logr.Logger) (ActionConfigGetter, error) {
43+
rcg := newRESTClientGetter(cfg, rm, "")
6144
// Setup the debug log function that Helm will use
6245
debugLog := func(format string, v ...interface{}) {
63-
if acg.log.GetSink() != nil {
64-
acg.log.V(1).Info(fmt.Sprintf(format, v...))
46+
if log.GetSink() != nil {
47+
log.V(1).Info(fmt.Sprintf(format, v...))
6548
}
6649
}
6750

68-
// Create a client that helm will use to manage release resources.
69-
// The passed object is used as an owner reference on every
70-
// object the client creates.
7151
kc := kube.New(rcg)
7252
kc.Log = debugLog
7353

74-
// Create the Kubernetes Secrets client. The passed object is
75-
// also used as an owner reference in the release secrets
76-
// created by this client.
77-
kcs, err := cmdutil.NewFactory(rcg).KubernetesClientSet()
54+
kcs, err := kc.Factory.KubernetesClientSet()
7855
if err != nil {
79-
return nil, err
56+
return nil, fmt.Errorf("creating kubernetes client set: %w", err)
8057
}
8158

59+
return &actionConfigGetter{
60+
kubeClient: kc,
61+
kubeClientSet: kcs,
62+
debugLog: debugLog,
63+
restClientGetter: rcg.restClientGetter,
64+
}, nil
65+
}
66+
67+
var _ ActionConfigGetter = &actionConfigGetter{}
68+
69+
type actionConfigGetter struct {
70+
kubeClient *kube.Client
71+
kubeClientSet kubernetes.Interface
72+
debugLog func(string, ...interface{})
73+
restClientGetter *restClientGetter
74+
}
75+
76+
func (acg *actionConfigGetter) ActionConfigFor(obj client.Object) (*action.Configuration, error) {
8277
ownerRef := metav1.NewControllerRef(obj, obj.GetObjectKind().GroupVersionKind())
8378
d := driver.NewSecrets(&ownerRefSecretClient{
84-
SecretInterface: kcs.CoreV1().Secrets(obj.GetNamespace()),
79+
SecretInterface: acg.kubeClientSet.CoreV1().Secrets(obj.GetNamespace()),
8580
refs: []metav1.OwnerReference{*ownerRef},
8681
})
8782

8883
// Also, use the debug log for the storage driver
89-
d.Log = debugLog
84+
d.Log = acg.debugLog
9085

9186
// Initialize the storage backend
9287
s := storage.Init(d)
9388

89+
kubeClient := *acg.kubeClient
90+
kubeClient.Namespace = obj.GetNamespace()
91+
9492
return &action.Configuration{
95-
RESTClientGetter: rcg,
93+
RESTClientGetter: acg.restClientGetter.ForNamespace(obj.GetNamespace()),
9694
Releases: s,
97-
KubeClient: kc,
98-
Log: debugLog,
95+
KubeClient: &kubeClient,
96+
Log: acg.debugLog,
9997
}, nil
10098
}
10199

pkg/client/actionconfig_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import (
2929
var _ = Describe("ActionConfig", func() {
3030
var _ = Describe("NewActionConfigGetter", func() {
3131
It("should return a valid ActionConfigGetter", func() {
32-
Expect(NewActionConfigGetter(nil, nil, logr.Discard())).NotTo(BeNil())
32+
acg, err := NewActionConfigGetter(cfg, nil, logr.Discard())
33+
Expect(err).ShouldNot(HaveOccurred())
34+
Expect(acg).NotTo(BeNil())
3335
})
3436
})
3537

@@ -42,7 +44,8 @@ var _ = Describe("ActionConfig", func() {
4244
rm, err := apiutil.NewDiscoveryRESTMapper(cfg)
4345
Expect(err).To(BeNil())
4446

45-
acg := NewActionConfigGetter(cfg, rm, logr.Discard())
47+
acg, err := NewActionConfigGetter(cfg, rm, logr.Discard())
48+
Expect(err).ShouldNot(HaveOccurred())
4649
ac, err := acg.ActionConfigFor(obj)
4750
Expect(err).To(BeNil())
4851
Expect(ac).NotTo(BeNil())

pkg/client/restclientgetter.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,19 @@ import (
2828
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
2929
)
3030

31-
var _ genericclioptions.RESTClientGetter = &restClientGetter{}
32-
33-
func newRESTClientGetter(cfg *rest.Config, rm meta.RESTMapper, ns string) genericclioptions.RESTClientGetter {
34-
return &restClientGetter{
35-
restConfig: cfg,
36-
restMapper: rm,
37-
namespaceConfig: &namespaceClientConfig{ns},
31+
func newRESTClientGetter(cfg *rest.Config, rm meta.RESTMapper, ns string) *namespacedRCG {
32+
return &namespacedRCG{
33+
restClientGetter: &restClientGetter{
34+
restConfig: cfg,
35+
restMapper: rm,
36+
},
37+
namespaceConfig: namespaceClientConfig{ns},
3838
}
3939
}
4040

4141
type restClientGetter struct {
42-
restConfig *rest.Config
43-
restMapper meta.RESTMapper
44-
namespaceConfig clientcmd.ClientConfig
42+
restConfig *rest.Config
43+
restMapper meta.RESTMapper
4544

4645
setupDiscoveryClient sync.Once
4746
cachedDiscoveryClient discovery.CachedDiscoveryInterface
@@ -73,7 +72,21 @@ func (c *restClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
7372
return c.restMapper, nil
7473
}
7574

76-
func (c *restClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
75+
func (c *restClientGetter) ForNamespace(ns string) genericclioptions.RESTClientGetter {
76+
return &namespacedRCG{
77+
restClientGetter: c,
78+
namespaceConfig: namespaceClientConfig{namespace: ns},
79+
}
80+
}
81+
82+
var _ genericclioptions.RESTClientGetter = &namespacedRCG{}
83+
84+
type namespacedRCG struct {
85+
*restClientGetter
86+
namespaceConfig namespaceClientConfig
87+
}
88+
89+
func (c *namespacedRCG) ToRawKubeConfigLoader() clientcmd.ClientConfig {
7790
return c.namespaceConfig
7891
}
7992

pkg/reconciler/reconciler.go

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ func (r *Reconciler) setupAnnotationMaps() {
145145
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
146146
controllerName := fmt.Sprintf("%v-controller", strings.ToLower(r.gvk.Kind))
147147

148-
r.addDefaults(mgr, controllerName)
148+
if err := r.addDefaults(mgr, controllerName); err != nil {
149+
return err
150+
}
151+
149152
if !r.skipPrimaryGVKSchemeRegistration {
150153
r.setupScheme(mgr)
151154
}
@@ -294,33 +297,33 @@ func StripManifestFromStatus(strip bool) Option {
294297
//
295298
// Example for using a custom type for the GVK scheme instead of unstructured.Unstructured:
296299
//
297-
// // Define custom type for GVK scheme.
298-
// //+kubebuilder:object:root=true
299-
// type Custom struct {
300-
// // [...]
301-
// }
300+
// // Define custom type for GVK scheme.
301+
// //+kubebuilder:object:root=true
302+
// type Custom struct {
303+
// // [...]
304+
// }
302305
//
303-
// // Register custom type along with common meta types in scheme.
304-
// scheme := runtime.NewScheme()
305-
// scheme.AddKnownTypes(SchemeGroupVersion, &Custom{})
306-
// metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
306+
// // Register custom type along with common meta types in scheme.
307+
// scheme := runtime.NewScheme()
308+
// scheme.AddKnownTypes(SchemeGroupVersion, &Custom{})
309+
// metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
307310
//
308-
// // Create new manager using the controller-runtime, injecting above scheme.
309-
// options := ctrl.Options{
310-
// Scheme = scheme,
311-
// // [...]
312-
// }
313-
// mgr, err := ctrl.NewManager(config, options)
311+
// // Create new manager using the controller-runtime, injecting above scheme.
312+
// options := ctrl.Options{
313+
// Scheme = scheme,
314+
// // [...]
315+
// }
316+
// mgr, err := ctrl.NewManager(config, options)
314317
//
315-
// // Create reconciler with generic scheme registration being disabled.
316-
// r, err := reconciler.New(
317-
// reconciler.WithChart(chart),
318-
// reconciler.SkipPrimaryGVKSchemeRegistration(true),
319-
// // [...]
320-
// )
318+
// // Create reconciler with generic scheme registration being disabled.
319+
// r, err := reconciler.New(
320+
// reconciler.WithChart(chart),
321+
// reconciler.SkipPrimaryGVKSchemeRegistration(true),
322+
// // [...]
323+
// )
321324
//
322-
// // Setup reconciler with above manager.
323-
// err = r.SetupWithManager(mgr)
325+
// // Setup reconciler with above manager.
326+
// err = r.SetupWithManager(mgr)
324327
//
325328
// By default, skipping of the generic scheme setup is disabled, which means that
326329
// unstructured.Unstructured is used for the GVK scheme.
@@ -501,16 +504,16 @@ func WithPostExtension(e extensions.ReconcileExtension) Option {
501504
// If you wish to, you can convert the Unstructured that is passed to your Translator to your own
502505
// Custom Resource struct like this:
503506
//
504-
// import "k8s.io/apimachinery/pkg/runtime"
505-
// foo := your.Foo{}
506-
// if err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &foo); err != nil {
507-
// return nil, err
508-
// }
509-
// // work with the type-safe foo
507+
// import "k8s.io/apimachinery/pkg/runtime"
508+
// foo := your.Foo{}
509+
// if err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &foo); err != nil {
510+
// return nil, err
511+
// }
512+
// // work with the type-safe foo
510513
//
511514
// Alternatively, your translator can also work similarly to a Mapper, by accessing the spec with:
512515
//
513-
// u.Object["spec"].(map[string]interface{})
516+
// u.Object["spec"].(map[string]interface{})
514517
func WithValueTranslator(t values.Translator) Option {
515518
return func(r *Reconciler) error {
516519
r.valueTranslator = t
@@ -995,15 +998,18 @@ func (r *Reconciler) validate() error {
995998
return nil
996999
}
9971000

998-
func (r *Reconciler) addDefaults(mgr ctrl.Manager, controllerName string) {
1001+
func (r *Reconciler) addDefaults(mgr ctrl.Manager, controllerName string) error {
9991002
if r.client == nil {
10001003
r.client = mgr.GetClient()
10011004
}
10021005
if r.log.GetSink() == nil {
10031006
r.log = ctrl.Log.WithName("controllers").WithName("Helm")
10041007
}
10051008
if r.actionClientGetter == nil {
1006-
actionConfigGetter := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), r.log)
1009+
actionConfigGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), r.log)
1010+
if err != nil {
1011+
return fmt.Errorf("creating action config getter: %w", err)
1012+
}
10071013
r.actionClientGetter = helmclient.NewActionClientGetter(actionConfigGetter)
10081014
}
10091015
if r.eventRecorder == nil {
@@ -1015,6 +1021,7 @@ func (r *Reconciler) addDefaults(mgr ctrl.Manager, controllerName string) {
10151021
if r.valueMapper == nil {
10161022
r.valueMapper = internalvalues.DefaultMapper
10171023
}
1024+
return nil
10181025
}
10191026

10201027
func (r *Reconciler) setupScheme(mgr ctrl.Manager) {

pkg/reconciler/reconciler_test.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,9 @@ var _ = Describe("Reconciler", func() {
135135
})
136136
var _ = Describe("WithActionClientGetter", func() {
137137
It("should set the reconciler action client getter", func() {
138-
cfgGetter := helmclient.NewActionConfigGetter(nil, nil, logr.Discard())
139-
acg := helmclient.NewActionClientGetter(cfgGetter)
140-
Expect(WithActionClientGetter(acg)(r)).To(Succeed())
141-
Expect(r.actionClientGetter).To(Equal(acg))
138+
fakeActionClientGetter := helmfake.NewActionClientGetter(nil, nil)
139+
Expect(WithActionClientGetter(fakeActionClientGetter)(r)).To(Succeed())
140+
Expect(r.actionClientGetter).To(Equal(fakeActionClientGetter))
142141
})
143142
})
144143
var _ = Describe("WithEventRecorder", func() {
@@ -984,8 +983,8 @@ var _ = Describe("Reconciler", func() {
984983
var actionConf *action.Configuration
985984
BeforeEach(func() {
986985
By("getting the current release and config", func() {
987-
var err error
988-
acg := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), logr.Discard())
986+
acg, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), logr.Discard())
987+
Expect(err).ShouldNot(HaveOccurred())
989988
actionConf, err = acg.ActionConfigFor(obj)
990989
Expect(err).To(BeNil())
991990
})

0 commit comments

Comments
 (0)