Skip to content

Commit 001e6c7

Browse files
authored
Fix memory leak due to misuse of K8s clients (#198)
1 parent 3e32ba3 commit 001e6c7

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
@@ -25,7 +25,6 @@ require (
2525
k8s.io/apimachinery v0.24.2
2626
k8s.io/cli-runtime v0.24.2
2727
k8s.io/client-go v0.24.2
28-
k8s.io/kubectl v0.24.2
2928
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
3029
sigs.k8s.io/controller-runtime v0.12.2
3130
sigs.k8s.io/kubebuilder/v3 v3.6.0
@@ -178,6 +177,7 @@ require (
178177
k8s.io/component-base v0.24.2 // indirect
179178
k8s.io/klog/v2 v2.60.1 // indirect
180179
k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect
180+
k8s.io/kubectl v0.24.2 // indirect
181181
oras.land/oras-go v1.2.0 // indirect
182182
sigs.k8s.io/controller-tools v0.9.2 // indirect
183183
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect

pkg/client/actionclient_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ var _ = Describe("ActionClient", func() {
5959
})
6060
var _ = Describe("NewActionClientGetter", func() {
6161
It("should return a valid ActionConfigGetter", func() {
62-
actionConfigGetter := NewActionConfigGetter(cfg, rm, logr.Discard())
62+
actionConfigGetter, err := NewActionConfigGetter(cfg, rm, logr.Discard())
63+
Expect(err).ShouldNot(HaveOccurred())
6364
Expect(NewActionClientGetter(actionConfigGetter)).NotTo(BeNil())
6465
})
6566
})
@@ -85,7 +86,9 @@ var _ = Describe("ActionClient", func() {
8586
obj = testutil.BuildTestCR(gvk)
8687
})
8788
It("should return a valid ActionClient", func() {
88-
acg := NewActionClientGetter(NewActionConfigGetter(cfg, rm, logr.Discard()))
89+
actionConfGetter, err := NewActionConfigGetter(cfg, rm, logr.Discard())
90+
Expect(err).ShouldNot(HaveOccurred())
91+
acg := NewActionClientGetter(actionConfGetter)
8992
ac, err := acg.ActionClientFor(obj)
9093
Expect(err).To(BeNil())
9194
Expect(ac).NotTo(BeNil())
@@ -102,8 +105,8 @@ var _ = Describe("ActionClient", func() {
102105
BeforeEach(func() {
103106
obj = testutil.BuildTestCR(gvk)
104107

105-
var err error
106-
actionConfigGetter := NewActionConfigGetter(cfg, rm, logr.Discard())
108+
actionConfigGetter, err := NewActionConfigGetter(cfg, rm, logr.Discard())
109+
Expect(err).ShouldNot(HaveOccurred())
107110
acg := NewActionClientGetter(actionConfigGetter)
108111
ac, err = acg.ActionClientFor(obj)
109112
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
@@ -132,7 +132,10 @@ func (r *Reconciler) setupAnnotationMaps() {
132132
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
133133
controllerName := fmt.Sprintf("%v-controller", strings.ToLower(r.gvk.Kind))
134134

135-
r.addDefaults(mgr, controllerName)
135+
if err := r.addDefaults(mgr, controllerName); err != nil {
136+
return err
137+
}
138+
136139
if !r.skipPrimaryGVKSchemeRegistration {
137140
r.setupScheme(mgr)
138141
}
@@ -270,33 +273,33 @@ func SkipDependentWatches(skip bool) Option {
270273
//
271274
// Example for using a custom type for the GVK scheme instead of unstructured.Unstructured:
272275
//
273-
// // Define custom type for GVK scheme.
274-
// //+kubebuilder:object:root=true
275-
// type Custom struct {
276-
// // [...]
277-
// }
276+
// // Define custom type for GVK scheme.
277+
// //+kubebuilder:object:root=true
278+
// type Custom struct {
279+
// // [...]
280+
// }
278281
//
279-
// // Register custom type along with common meta types in scheme.
280-
// scheme := runtime.NewScheme()
281-
// scheme.AddKnownTypes(SchemeGroupVersion, &Custom{})
282-
// metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
282+
// // Register custom type along with common meta types in scheme.
283+
// scheme := runtime.NewScheme()
284+
// scheme.AddKnownTypes(SchemeGroupVersion, &Custom{})
285+
// metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
283286
//
284-
// // Create new manager using the controller-runtime, injecting above scheme.
285-
// options := ctrl.Options{
286-
// Scheme = scheme,
287-
// // [...]
288-
// }
289-
// mgr, err := ctrl.NewManager(config, options)
287+
// // Create new manager using the controller-runtime, injecting above scheme.
288+
// options := ctrl.Options{
289+
// Scheme = scheme,
290+
// // [...]
291+
// }
292+
// mgr, err := ctrl.NewManager(config, options)
290293
//
291-
// // Create reconciler with generic scheme registration being disabled.
292-
// r, err := reconciler.New(
293-
// reconciler.WithChart(chart),
294-
// reconciler.SkipPrimaryGVKSchemeRegistration(true),
295-
// // [...]
296-
// )
294+
// // Create reconciler with generic scheme registration being disabled.
295+
// r, err := reconciler.New(
296+
// reconciler.WithChart(chart),
297+
// reconciler.SkipPrimaryGVKSchemeRegistration(true),
298+
// // [...]
299+
// )
297300
//
298-
// // Setup reconciler with above manager.
299-
// err = r.SetupWithManager(mgr)
301+
// // Setup reconciler with above manager.
302+
// err = r.SetupWithManager(mgr)
300303
//
301304
// By default, skipping of the generic scheme setup is disabled, which means that
302305
// unstructured.Unstructured is used for the GVK scheme.
@@ -435,16 +438,16 @@ func WithPostHook(h hook.PostHook) Option {
435438
// If you wish to, you can convert the Unstructured that is passed to your Translator to your own
436439
// Custom Resource struct like this:
437440
//
438-
// import "k8s.io/apimachinery/pkg/runtime"
439-
// foo := your.Foo{}
440-
// if err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &foo); err != nil {
441-
// return nil, err
442-
// }
443-
// // work with the type-safe foo
441+
// import "k8s.io/apimachinery/pkg/runtime"
442+
// foo := your.Foo{}
443+
// if err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &foo); err != nil {
444+
// return nil, err
445+
// }
446+
// // work with the type-safe foo
444447
//
445448
// Alternatively, your translator can also work similarly to a Mapper, by accessing the spec with:
446449
//
447-
// u.Object["spec"].(map[string]interface{})
450+
// u.Object["spec"].(map[string]interface{})
448451
func WithValueTranslator(t values.Translator) Option {
449452
return func(r *Reconciler) error {
450453
r.valueTranslator = t
@@ -866,15 +869,18 @@ func (r *Reconciler) validate() error {
866869
return nil
867870
}
868871

869-
func (r *Reconciler) addDefaults(mgr ctrl.Manager, controllerName string) {
872+
func (r *Reconciler) addDefaults(mgr ctrl.Manager, controllerName string) error {
870873
if r.client == nil {
871874
r.client = mgr.GetClient()
872875
}
873876
if r.log.GetSink() == nil {
874877
r.log = ctrl.Log.WithName("controllers").WithName("Helm")
875878
}
876879
if r.actionClientGetter == nil {
877-
actionConfigGetter := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), r.log)
880+
actionConfigGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), r.log)
881+
if err != nil {
882+
return fmt.Errorf("creating action config getter: %w", err)
883+
}
878884
r.actionClientGetter = helmclient.NewActionClientGetter(actionConfigGetter)
879885
}
880886
if r.eventRecorder == nil {
@@ -886,6 +892,7 @@ func (r *Reconciler) addDefaults(mgr ctrl.Manager, controllerName string) {
886892
if r.valueMapper == nil {
887893
r.valueMapper = internalvalues.DefaultMapper
888894
}
895+
return nil
889896
}
890897

891898
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() {
@@ -974,8 +973,8 @@ var _ = Describe("Reconciler", func() {
974973
var actionConf *action.Configuration
975974
BeforeEach(func() {
976975
By("getting the current release and config", func() {
977-
var err error
978-
acg := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), logr.Discard())
976+
acg, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), logr.Discard())
977+
Expect(err).ShouldNot(HaveOccurred())
979978
actionConf, err = acg.ActionConfigFor(obj)
980979
Expect(err).To(BeNil())
981980
})

0 commit comments

Comments
 (0)