Skip to content

Commit cb83a61

Browse files
authored
Helm: Use informer to list helm secrets to improve performance (#6354)
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 Signed-off-by: Luca Berneking <[email protected]>
1 parent f3b3ecd commit cb83a61

File tree

3 files changed

+235
-9
lines changed

3 files changed

+235
-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, the 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: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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+
"k8s.io/apimachinery/pkg/selection"
24+
25+
corev1 "k8s.io/api/core/v1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/labels"
28+
"k8s.io/apimachinery/pkg/types"
29+
"k8s.io/apimachinery/pkg/util/wait"
30+
"k8s.io/apimachinery/pkg/watch"
31+
applyconfv1 "k8s.io/client-go/applyconfigurations/core/v1"
32+
"k8s.io/client-go/informers"
33+
"k8s.io/client-go/kubernetes"
34+
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
35+
listerscorev1 "k8s.io/client-go/listers/core/v1"
36+
logf "sigs.k8s.io/controller-runtime/pkg/log"
37+
)
38+
39+
var log = logf.Log.WithName("helm.watchedsecrets")
40+
41+
const helmSecretsLabelKey = "owner"
42+
const helmSecretsLabelValue = "helm"
43+
44+
// Wraps the kubernetes SecretInterface
45+
// Helm queries its own secrets multiple times per reconciliation. To reduce the number of lists going to the apiserver
46+
// we instead use an informer to watch the changes on secrets.
47+
type WatchedSecrets struct {
48+
inner typedcorev1.SecretInterface
49+
informerFactory informers.SharedInformerFactory
50+
informerLister listerscorev1.SecretNamespaceLister
51+
}
52+
53+
func (w *WatchedSecrets) Create(ctx context.Context, secret *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) {
54+
return w.inner.Create(ctx, secret, opts)
55+
}
56+
57+
func (w *WatchedSecrets) Update(ctx context.Context, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
58+
return w.inner.Update(ctx, secret, opts)
59+
}
60+
61+
func (w *WatchedSecrets) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
62+
return w.inner.Delete(ctx, name, opts)
63+
}
64+
65+
func (w *WatchedSecrets) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
66+
return w.inner.DeleteCollection(ctx, opts, listOpts)
67+
}
68+
69+
func (w *WatchedSecrets) Get(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.Secret, error) {
70+
return w.inner.Get(ctx, name, opts)
71+
}
72+
73+
func (w *WatchedSecrets) List(ctx context.Context, opts metav1.ListOptions) (*corev1.SecretList, error) {
74+
labelSelector, err := labels.Parse(opts.LabelSelector)
75+
if err != nil {
76+
return nil, err
77+
}
78+
ownerLabelSelector, hasOwnerLabelSelector := labelSelector.RequiresExactMatch(helmSecretsLabelKey)
79+
80+
// The informer interface only offers to filter secrets with a labelSelector
81+
// We are only watching secrets with label owner=helm.
82+
// Currently (helm v3.10.3) this List function is only being called in `storage/driver/secrets.go` with a
83+
// labelSelector, meaning this case should never be executed. But we are able to fallback to the normal List
84+
// implementation.
85+
if hasListOptionsOtherThanLabelSelector(opts) || !hasOwnerLabelSelector || ownerLabelSelector != helmSecretsLabelValue {
86+
log.Info("Cannot use informer to list secrets", "listOptions", opts)
87+
return w.inner.List(ctx, opts)
88+
}
89+
90+
secrets, err := w.informerLister.List(labelSelector)
91+
if err != nil {
92+
return nil, err
93+
}
94+
95+
secretList := &corev1.SecretList{
96+
TypeMeta: metav1.TypeMeta{},
97+
ListMeta: metav1.ListMeta{},
98+
Items: make([]corev1.Secret, len(secrets)),
99+
}
100+
for i, sec := range secrets {
101+
secretList.Items[i] = *sec
102+
}
103+
104+
return secretList, nil
105+
}
106+
107+
func hasListOptionsOtherThanLabelSelector(opts metav1.ListOptions) bool {
108+
empty := metav1.ListOptions{}
109+
110+
providedWithoutLabelSelector := opts
111+
providedWithoutLabelSelector.LabelSelector = ""
112+
113+
return empty != providedWithoutLabelSelector
114+
}
115+
116+
func (w *WatchedSecrets) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
117+
return w.inner.Watch(ctx, opts)
118+
}
119+
120+
func (w *WatchedSecrets) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *corev1.Secret, err error) {
121+
return w.inner.Patch(ctx, name, pt, data, opts)
122+
}
123+
124+
func (w *WatchedSecrets) Apply(ctx context.Context, secret *applyconfv1.SecretApplyConfiguration, opts metav1.ApplyOptions) (result *corev1.Secret, err error) {
125+
return w.inner.Apply(ctx, secret, opts)
126+
}
127+
128+
var _ typedcorev1.SecretInterface = &WatchedSecrets{}
129+
130+
func NewWatchedSecrets(clientSet kubernetes.Interface, namespace string) *WatchedSecrets {
131+
log.V(2).Info("Get secrets client", "namespace", namespace)
132+
133+
helmListOptionsTweaker := func(options *metav1.ListOptions) {
134+
labelSelector, err := labels.Parse(options.LabelSelector)
135+
if err != nil {
136+
log.Info("Could not parse labelSelector", "labelSelector", options.LabelSelector)
137+
panic("could not parse labelSelector")
138+
}
139+
140+
ownerLabelSelector, hasOwnerLabelSelector := labelSelector.RequiresExactMatch(helmSecretsLabelKey)
141+
142+
if !hasOwnerLabelSelector || ownerLabelSelector != helmSecretsLabelValue {
143+
helmRequirement, _ := labels.NewRequirement(
144+
"owner", selection.Equals, []string{helmSecretsLabelValue},
145+
)
146+
labelSelectorWithOwner := labelSelector.Add(*helmRequirement)
147+
options.LabelSelector = labelSelectorWithOwner.String()
148+
}
149+
}
150+
151+
informerFactory := informers.NewSharedInformerFactoryWithOptions(clientSet, time.Second*30, informers.WithNamespace(namespace), informers.WithTweakListOptions(helmListOptionsTweaker))
152+
secretsInformer := informerFactory.Core().V1().Secrets()
153+
154+
informerSecretsLister := secretsInformer.Lister().Secrets(namespace)
155+
156+
return &WatchedSecrets{
157+
inner: clientSet.CoreV1().Secrets(namespace),
158+
informerFactory: informerFactory,
159+
informerLister: informerSecretsLister,
160+
}
161+
}
162+
163+
func (w *WatchedSecrets) Run() {
164+
w.informerFactory.Start(wait.NeverStop)
165+
_ = w.informerFactory.WaitForCacheSync(wait.NeverStop)
166+
}

0 commit comments

Comments
 (0)