Skip to content

Commit 4be2ead

Browse files
committed
Helm: Use informer to list helm secrets to improve performance
Helm stores its state in secrets inside the cluster. Instead of listing these secrets before every reconciliation of every release, we use an informer to query a local secrets list. This significantly reduced the load on the kubernetes apiserver and etcd
1 parent 9d2f672 commit 4be2ead

File tree

3 files changed

+209
-9
lines changed

3 files changed

+209
-9
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# entries is a list of entries to include in
2+
# release notes and/or the migration guide
3+
entries:
4+
- description: >
5+
(helm): Use informer to list helm secrets to improve performance
6+
7+
# kind is one of:
8+
# - addition
9+
# - change
10+
# - deprecation
11+
# - removal
12+
# - bugfix
13+
kind: "change"
14+
15+
# Is this a breaking change?
16+
breaking: false
17+
18+
# NOTE: ONLY USE `pull_request_override` WHEN ADDING THIS
19+
# FILE FOR A PREVIOUSLY MERGED PULL_REQUEST!
20+
#
21+
# The generator auto-detects the PR number from the commit
22+
# message in which this file was originally added.
23+
#
24+
# What is the pull request number (without the "#")?
25+
# pull_request_override: 0
26+
27+
28+
# Migration can be defined to automatically add a section to
29+
# the migration guide. This is required for breaking changes.
30+
migration:
31+
header: Require `watch` on `secrets`
32+
body: |
33+
The operator now requires the watch operation on secrets.
34+
When using a custom ServiceAccount for deployment, add following additional role is now required:
35+
```
36+
rules:
37+
- apiGroups:
38+
- ""
39+
resources:
40+
- secrets
41+
verbs:
42+
- watch
43+
```

internal/helm/client/actionconfig.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package client
1919
import (
2020
"context"
2121
"fmt"
22+
"sync"
2223

2324
"k8s.io/client-go/kubernetes"
2425

@@ -57,26 +58,42 @@ func NewActionConfigGetter(cfg *rest.Config, rm meta.RESTMapper, log logr.Logger
5758
}
5859

5960
return &actionConfigGetter{
60-
kubeClient: kc,
61-
kubeClientSet: kcs,
62-
debugLog: debugLog,
63-
restClientGetter: rcg.restClientGetter,
61+
kubeClient: kc,
62+
kubeClientSet: kcs,
63+
debugLog: debugLog,
64+
restClientGetter: rcg.restClientGetter,
65+
watchedSecrets: map[string]*WatchedSecrets{},
66+
watchedSecretsMutex: &sync.Mutex{},
6467
}, nil
6568
}
6669

6770
var _ ActionConfigGetter = &actionConfigGetter{}
6871

6972
type actionConfigGetter struct {
70-
kubeClient *kube.Client
71-
kubeClientSet kubernetes.Interface
72-
debugLog func(string, ...interface{})
73-
restClientGetter *restClientGetter
73+
kubeClient *kube.Client
74+
kubeClientSet kubernetes.Interface
75+
debugLog func(string, ...interface{})
76+
restClientGetter *restClientGetter
77+
watchedSecrets map[string]*WatchedSecrets
78+
watchedSecretsMutex *sync.Mutex
79+
}
80+
81+
// Creates a new watcher for each namespace to not require cluster-wide secret access
82+
func (acg *actionConfigGetter) getWatchedSecretsForNamespace(namespace string) *WatchedSecrets {
83+
acg.watchedSecretsMutex.Lock()
84+
if _, found := acg.watchedSecrets[namespace]; !found {
85+
acg.watchedSecrets[namespace] = NewWatchedSecrets(acg.kubeClientSet, namespace)
86+
acg.watchedSecrets[namespace].Run()
87+
}
88+
acg.watchedSecretsMutex.Unlock()
89+
return acg.watchedSecrets[namespace]
7490
}
7591

7692
func (acg *actionConfigGetter) ActionConfigFor(obj client.Object) (*action.Configuration, error) {
93+
watchedSecrets := acg.getWatchedSecretsForNamespace(obj.GetNamespace())
7794
ownerRef := metav1.NewControllerRef(obj, obj.GetObjectKind().GroupVersionKind())
7895
d := driver.NewSecrets(&ownerRefSecretClient{
79-
SecretInterface: acg.kubeClientSet.CoreV1().Secrets(obj.GetNamespace()),
96+
SecretInterface: watchedSecrets,
8097
refs: []metav1.OwnerReference{*ownerRef},
8198
})
8299

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
Copyright 2020 The Operator-SDK Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package client
18+
19+
import (
20+
"context"
21+
"time"
22+
23+
corev1 "k8s.io/api/core/v1"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/labels"
26+
"k8s.io/apimachinery/pkg/types"
27+
"k8s.io/apimachinery/pkg/util/wait"
28+
"k8s.io/apimachinery/pkg/watch"
29+
applyconfv1 "k8s.io/client-go/applyconfigurations/core/v1"
30+
"k8s.io/client-go/informers"
31+
"k8s.io/client-go/kubernetes"
32+
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
33+
v12 "k8s.io/client-go/listers/core/v1"
34+
logf "sigs.k8s.io/controller-runtime/pkg/log"
35+
)
36+
37+
var log = logf.Log.WithName("helm.watchedsecrets")
38+
39+
// Wraps the kubernetes SecretInterface
40+
// Helm queries its own secrets multiple times per reconciliation. To reduce the number of lists going to the apiserver
41+
// we instead use an informer to watch the changes on secrets.
42+
type WatchedSecrets struct {
43+
inner v1.SecretInterface
44+
informerFactory informers.SharedInformerFactory
45+
informerLister v12.SecretNamespaceLister
46+
}
47+
48+
func (w *WatchedSecrets) Create(ctx context.Context, secret *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) {
49+
return w.inner.Create(ctx, secret, opts)
50+
}
51+
52+
func (w *WatchedSecrets) Update(ctx context.Context, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
53+
return w.inner.Update(ctx, secret, opts)
54+
}
55+
56+
func (w *WatchedSecrets) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
57+
return w.inner.Delete(ctx, name, opts)
58+
}
59+
60+
func (w *WatchedSecrets) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
61+
return w.inner.DeleteCollection(ctx, opts, listOpts)
62+
}
63+
64+
func (w *WatchedSecrets) Get(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.Secret, error) {
65+
return w.inner.Get(ctx, name, opts)
66+
}
67+
68+
func (w *WatchedSecrets) List(ctx context.Context, opts metav1.ListOptions) (*corev1.SecretList, error) {
69+
70+
// The informer interface only offers to filter secrets with a labelSelector
71+
// Currently (helm v3.10.3) this List function is only being called in `storage/driver/secrets.go` with a
72+
// labelSelector, meaning this case should never be executed. But we are able to fallback to the normal List
73+
// implementation.
74+
if hasListOptionsOtherThanLabelSelector(opts) {
75+
log.Info("Cannot use informer to list secrets", "listOptions", opts)
76+
return w.inner.List(ctx, opts)
77+
}
78+
79+
labelSelector, err := labels.Parse(opts.LabelSelector)
80+
if err != nil {
81+
return nil, err
82+
}
83+
secrets, err := w.informerLister.List(labelSelector)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
secretList := &corev1.SecretList{
89+
TypeMeta: metav1.TypeMeta{},
90+
ListMeta: metav1.ListMeta{},
91+
Items: make([]corev1.Secret, len(secrets)),
92+
}
93+
for i, sec := range secrets {
94+
secretList.Items[i] = *sec
95+
}
96+
97+
return secretList, nil
98+
}
99+
100+
func hasListOptionsOtherThanLabelSelector(opts metav1.ListOptions) bool {
101+
empty := metav1.ListOptions{}
102+
103+
providedWithoutLabelSelector := opts
104+
providedWithoutLabelSelector.LabelSelector = ""
105+
106+
return empty != providedWithoutLabelSelector
107+
}
108+
109+
func (w *WatchedSecrets) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
110+
return w.inner.Watch(ctx, opts)
111+
}
112+
113+
func (w *WatchedSecrets) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *corev1.Secret, err error) {
114+
return w.inner.Patch(ctx, name, pt, data, opts)
115+
}
116+
117+
func (w *WatchedSecrets) Apply(ctx context.Context, secret *applyconfv1.SecretApplyConfiguration, opts metav1.ApplyOptions) (result *corev1.Secret, err error) {
118+
return w.inner.Apply(ctx, secret, opts)
119+
}
120+
121+
var _ v1.SecretInterface = &WatchedSecrets{}
122+
123+
func NewWatchedSecrets(clientSet kubernetes.Interface, namespace string) *WatchedSecrets {
124+
log.V(2).Info("Get secrets client", "namespace", namespace)
125+
informerFactory := informers.NewSharedInformerFactoryWithOptions(clientSet, time.Second*30, informers.WithNamespace(namespace))
126+
secretsInformer := informerFactory.Core().V1().Secrets()
127+
128+
informerSecretsLister := secretsInformer.Lister().Secrets(namespace)
129+
130+
return &WatchedSecrets{
131+
inner: clientSet.CoreV1().Secrets(namespace),
132+
informerFactory: informerFactory,
133+
informerLister: informerSecretsLister,
134+
}
135+
}
136+
137+
func (w *WatchedSecrets) Run() {
138+
w.informerFactory.Start(wait.NeverStop)
139+
_ = w.informerFactory.WaitForCacheSync(wait.NeverStop)
140+
}

0 commit comments

Comments
 (0)