Skip to content

Commit 2e18c5b

Browse files
authored
Add support for a chunked release storage driver (#350)
This commit adds a custom helm release storage driver that overcomes limitations in the size of a single value that can be stored in etcd. In order to remain backward-compatible and also make this storage driver available, this commit also refactors the ActionConfigGetter options so that a custom function can be provided to the ActionConfigGetter that can create the desired storage driver. This commit also separates the rest config mapping into two separate options. One for interactions with the storage backend, and the other for managing release content. Signed-off-by: Joe Lanford <[email protected]>
1 parent d6fdc05 commit 2e18c5b

File tree

7 files changed

+1043
-47
lines changed

7 files changed

+1043
-47
lines changed

pkg/client/actionconfig.go

+89-47
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"helm.sh/helm/v3/pkg/kube"
2626
"helm.sh/helm/v3/pkg/storage"
2727
"helm.sh/helm/v3/pkg/storage/driver"
28-
corev1 "k8s.io/api/core/v1"
2928
"k8s.io/apimachinery/pkg/api/meta"
3029
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3130
"k8s.io/client-go/discovery"
@@ -57,14 +56,25 @@ func NewActionConfigGetter(baseRestConfig *rest.Config, rm meta.RESTMapper, opts
5756
if acg.objectToClientNamespace == nil {
5857
acg.objectToClientNamespace = getObjectNamespace
5958
}
60-
if acg.objectToStorageNamespace == nil {
61-
acg.objectToStorageNamespace = getObjectNamespace
59+
if acg.objectToClientRestConfig == nil {
60+
acg.objectToClientRestConfig = func(_ context.Context, _ client.Object, baseRestConfig *rest.Config) (*rest.Config, error) {
61+
return rest.CopyConfig(baseRestConfig), nil
62+
}
6263
}
63-
if acg.objectToRestConfig == nil {
64-
acg.objectToRestConfig = func(_ context.Context, _ client.Object, baseRestConfig *rest.Config) (*rest.Config, error) {
64+
if acg.objectToStorageRestConfig == nil {
65+
acg.objectToStorageRestConfig = func(_ context.Context, _ client.Object, baseRestConfig *rest.Config) (*rest.Config, error) {
6566
return rest.CopyConfig(baseRestConfig), nil
6667
}
6768
}
69+
if acg.objectToStorageDriver == nil {
70+
if acg.objectToStorageNamespace == nil {
71+
acg.objectToStorageNamespace = getObjectNamespace
72+
}
73+
acg.objectToStorageDriver = DefaultSecretsStorageDriver(SecretsStorageDriverOpts{
74+
DisableOwnerRefInjection: acg.disableStorageOwnerRefInjection,
75+
StorageNamespaceMapper: acg.objectToStorageNamespace,
76+
})
77+
}
6878
return acg, nil
6979
}
7080

@@ -73,28 +83,52 @@ var _ ActionConfigGetter = &actionConfigGetter{}
7383
type ActionConfigGetterOption func(getter *actionConfigGetter)
7484

7585
type ObjectToStringMapper func(client.Object) (string, error)
86+
type ObjectToRestConfigMapper func(context.Context, client.Object, *rest.Config) (*rest.Config, error)
87+
type ObjectToStorageDriverMapper func(context.Context, client.Object, *rest.Config) (driver.Driver, error)
88+
89+
func ClientRestConfigMapper(f ObjectToRestConfigMapper) ActionConfigGetterOption { // nolint:revive
90+
return func(getter *actionConfigGetter) {
91+
getter.objectToClientRestConfig = f
92+
}
93+
}
7694

7795
func ClientNamespaceMapper(m ObjectToStringMapper) ActionConfigGetterOption { // nolint:revive
7896
return func(getter *actionConfigGetter) {
7997
getter.objectToClientNamespace = m
8098
}
8199
}
82100

101+
func StorageRestConfigMapper(f ObjectToRestConfigMapper) ActionConfigGetterOption {
102+
return func(getter *actionConfigGetter) {
103+
getter.objectToStorageRestConfig = f
104+
}
105+
}
106+
107+
func StorageDriverMapper(f ObjectToStorageDriverMapper) ActionConfigGetterOption {
108+
return func(getter *actionConfigGetter) {
109+
getter.objectToStorageDriver = f
110+
}
111+
}
112+
113+
// Deprecated: use StorageDriverMapper(DefaultSecretsStorageDriver(SecretsStorageDriverOpts)) instead.
83114
func StorageNamespaceMapper(m ObjectToStringMapper) ActionConfigGetterOption {
84115
return func(getter *actionConfigGetter) {
85116
getter.objectToStorageNamespace = m
86117
}
87118
}
88119

120+
// Deprecated: use StorageDriverMapper(DefaultSecretsStorageDriver(SecretsStorageDriverOpts)) instead.
89121
func DisableStorageOwnerRefInjection(v bool) ActionConfigGetterOption {
90122
return func(getter *actionConfigGetter) {
91123
getter.disableStorageOwnerRefInjection = v
92124
}
93125
}
94126

127+
// Deprecated: use ClientRestConfigMapper and StorageRestConfigMapper instead.
95128
func RestConfigMapper(f func(context.Context, client.Object, *rest.Config) (*rest.Config, error)) ActionConfigGetterOption {
96129
return func(getter *actionConfigGetter) {
97-
getter.objectToRestConfig = f
130+
getter.objectToClientRestConfig = f
131+
getter.objectToStorageRestConfig = f
98132
}
99133
}
100134

@@ -107,58 +141,53 @@ type actionConfigGetter struct {
107141
restMapper meta.RESTMapper
108142
discoveryClient discovery.CachedDiscoveryInterface
109143

110-
objectToClientNamespace ObjectToStringMapper
111-
objectToStorageNamespace ObjectToStringMapper
112-
objectToRestConfig func(context.Context, client.Object, *rest.Config) (*rest.Config, error)
144+
objectToClientRestConfig ObjectToRestConfigMapper
145+
objectToClientNamespace ObjectToStringMapper
146+
147+
objectToStorageRestConfig ObjectToRestConfigMapper
148+
objectToStorageDriver ObjectToStorageDriverMapper
149+
150+
// Deprecated: only keep around for backward compatibility with StorageNamespaceMapper option.
151+
objectToStorageNamespace ObjectToStringMapper
152+
// Deprecated: only keep around for backward compatibility with DisableStorageOwnerRefInjection option.
113153
disableStorageOwnerRefInjection bool
114154
}
115155

116156
func (acg *actionConfigGetter) ActionConfigFor(ctx context.Context, obj client.Object) (*action.Configuration, error) {
117-
storageNs, err := acg.objectToStorageNamespace(obj)
157+
clientRestConfig, err := acg.objectToClientRestConfig(ctx, obj, acg.baseRestConfig)
118158
if err != nil {
119-
return nil, fmt.Errorf("get storage namespace for object: %v", err)
120-
}
121-
122-
restConfig, err := acg.objectToRestConfig(ctx, obj, acg.baseRestConfig)
123-
if err != nil {
124-
return nil, fmt.Errorf("get rest config for object: %v", err)
159+
return nil, fmt.Errorf("get client rest config for object: %v", err)
125160
}
126161

127162
clientNamespace, err := acg.objectToClientNamespace(obj)
128163
if err != nil {
129164
return nil, fmt.Errorf("get client namespace for object: %v", err)
130165
}
131166

132-
rcg := newRESTClientGetter(restConfig, acg.restMapper, acg.discoveryClient, clientNamespace)
133-
kc := kube.New(rcg)
134-
kc.Namespace = clientNamespace
135-
136-
kcs, err := kc.Factory.KubernetesClientSet()
137-
if err != nil {
138-
return nil, fmt.Errorf("create kubernetes clientset: %v", err)
139-
}
167+
clientRCG := newRESTClientGetter(clientRestConfig, acg.restMapper, acg.discoveryClient, clientNamespace)
168+
clientKC := kube.New(clientRCG)
169+
clientKC.Namespace = clientNamespace
140170

141171
// Setup the debug log function that Helm will use
142172
debugLog := getDebugLogger(ctx)
143173

144-
secretClient := kcs.CoreV1().Secrets(storageNs)
145-
if !acg.disableStorageOwnerRefInjection {
146-
ownerRef := metav1.NewControllerRef(obj, obj.GetObjectKind().GroupVersionKind())
147-
secretClient = &ownerRefSecretClient{
148-
SecretInterface: secretClient,
149-
refs: []metav1.OwnerReference{*ownerRef},
150-
}
174+
storageRestConfig, err := acg.objectToStorageRestConfig(ctx, obj, acg.baseRestConfig)
175+
if err != nil {
176+
return nil, fmt.Errorf("get storage rest config for object: %v", err)
177+
}
178+
179+
d, err := acg.objectToStorageDriver(ctx, obj, storageRestConfig)
180+
if err != nil {
181+
return nil, fmt.Errorf("get storage driver for object: %v", err)
151182
}
152-
d := driver.NewSecrets(secretClient)
153-
d.Log = debugLog
154183

155184
// Initialize the storage backend
156185
s := storage.Init(d)
157186

158187
return &action.Configuration{
159-
RESTClientGetter: rcg,
188+
RESTClientGetter: clientRCG,
160189
Releases: s,
161-
KubeClient: kc,
190+
KubeClient: clientKC,
162191
Log: debugLog,
163192
}, nil
164193
}
@@ -173,19 +202,32 @@ func getDebugLogger(ctx context.Context) func(format string, v ...interface{}) {
173202
}
174203
}
175204

176-
var _ v1.SecretInterface = &ownerRefSecretClient{}
177-
178-
type ownerRefSecretClient struct {
179-
v1.SecretInterface
180-
refs []metav1.OwnerReference
205+
type SecretsStorageDriverOpts struct {
206+
DisableOwnerRefInjection bool
207+
StorageNamespaceMapper ObjectToStringMapper
181208
}
182209

183-
func (c *ownerRefSecretClient) Create(ctx context.Context, in *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) {
184-
in.OwnerReferences = append(in.OwnerReferences, c.refs...)
185-
return c.SecretInterface.Create(ctx, in, opts)
186-
}
210+
func DefaultSecretsStorageDriver(opts SecretsStorageDriverOpts) ObjectToStorageDriverMapper {
211+
if opts.StorageNamespaceMapper == nil {
212+
opts.StorageNamespaceMapper = getObjectNamespace
213+
}
214+
return func(ctx context.Context, obj client.Object, restConfig *rest.Config) (driver.Driver, error) {
215+
storageNamespace, err := opts.StorageNamespaceMapper(obj)
216+
if err != nil {
217+
return nil, fmt.Errorf("get storage namespace for object: %v", err)
218+
}
219+
secretsInterface, err := v1.NewForConfig(restConfig)
220+
if err != nil {
221+
return nil, fmt.Errorf("create secrets client for storage: %v", err)
222+
}
187223

188-
func (c *ownerRefSecretClient) Update(ctx context.Context, in *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
189-
in.OwnerReferences = append(in.OwnerReferences, c.refs...)
190-
return c.SecretInterface.Update(ctx, in, opts)
224+
secretClient := secretsInterface.Secrets(storageNamespace)
225+
if !opts.DisableOwnerRefInjection {
226+
ownerRef := metav1.NewControllerRef(obj, obj.GetObjectKind().GroupVersionKind())
227+
secretClient = NewOwnerRefSecretClient(secretClient, []metav1.OwnerReference{*ownerRef}, MatchAllSecrets)
228+
}
229+
d := driver.NewSecrets(secretClient)
230+
d.Log = getDebugLogger(ctx)
231+
return d, nil
232+
}
191233
}

pkg/client/actionconfig_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ import (
2323

2424
. "github.com/onsi/ginkgo/v2"
2525
. "github.com/onsi/gomega"
26+
2627
"helm.sh/helm/v3/pkg/action"
2728
"helm.sh/helm/v3/pkg/kube"
29+
"helm.sh/helm/v3/pkg/release"
30+
"helm.sh/helm/v3/pkg/storage/driver"
2831
corev1 "k8s.io/api/core/v1"
2932
"k8s.io/apimachinery/pkg/api/meta"
3033
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -185,6 +188,32 @@ metadata:
185188
Expect(err).ToNot(HaveOccurred())
186189
Expect(ac2.RESTClientGetter.ToRESTConfig()).To(WithTransform(func(c *rest.Config) string { return c.BearerToken }, Equal("test2")))
187190
})
191+
192+
It("should use a custom storage driver", func() {
193+
storageDriver := driver.NewMemory()
194+
195+
storageDriverMapper := func(ctx context.Context, obj client.Object, cfg *rest.Config) (driver.Driver, error) {
196+
return storageDriver, nil
197+
}
198+
acg, err := NewActionConfigGetter(cfg, rm, StorageDriverMapper(storageDriverMapper))
199+
Expect(err).ToNot(HaveOccurred())
200+
201+
testObject := func(name string) client.Object {
202+
u := unstructured.Unstructured{}
203+
u.SetName(name)
204+
return &u
205+
}
206+
207+
ac, err := acg.ActionConfigFor(context.Background(), testObject("test1"))
208+
Expect(err).ToNot(HaveOccurred())
209+
210+
expected := &release.Release{Name: "test1", Version: 2, Info: &release.Info{Status: release.StatusDeployed}}
211+
Expect(ac.Releases.Create(expected)).To(Succeed())
212+
actual, err := storageDriver.List(func(r *release.Release) bool { return true })
213+
Expect(err).ToNot(HaveOccurred())
214+
Expect(actual).To(HaveLen(1))
215+
Expect(actual[0]).To(Equal(expected))
216+
})
188217
})
189218
})
190219

pkg/client/ownerrefclient.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package client
2+
3+
import (
4+
"context"
5+
6+
corev1 "k8s.io/api/core/v1"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
9+
)
10+
11+
var _ clientcorev1.SecretInterface = &ownerRefSecretClient{}
12+
13+
// NewOwnerRefSecretClient returns a SecretInterface that injects the provided owner references
14+
// to all created or updated secrets that match the provided match function. If match is nil, all
15+
// secrets are matched.
16+
func NewOwnerRefSecretClient(client clientcorev1.SecretInterface, refs []metav1.OwnerReference, match func(*corev1.Secret) bool) clientcorev1.SecretInterface {
17+
if match == nil {
18+
match = MatchAllSecrets
19+
}
20+
return &ownerRefSecretClient{
21+
SecretInterface: client,
22+
match: match,
23+
refs: refs,
24+
}
25+
}
26+
27+
func MatchAllSecrets(_ *corev1.Secret) bool {
28+
return true
29+
}
30+
31+
type ownerRefSecretClient struct {
32+
clientcorev1.SecretInterface
33+
match func(secret *corev1.Secret) bool
34+
refs []metav1.OwnerReference
35+
}
36+
37+
func (c *ownerRefSecretClient) appendMissingOwnerRefs(secret *corev1.Secret) {
38+
hasOwnerRef := func(secret *corev1.Secret, ref metav1.OwnerReference) bool {
39+
for _, r := range secret.OwnerReferences {
40+
if r.UID == ref.UID {
41+
return true
42+
}
43+
}
44+
return false
45+
}
46+
for i := range c.refs {
47+
if !hasOwnerRef(secret, c.refs[i]) {
48+
secret.OwnerReferences = append(secret.OwnerReferences, c.refs[i])
49+
}
50+
}
51+
}
52+
53+
func (c *ownerRefSecretClient) Create(ctx context.Context, in *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) {
54+
if c.match == nil || c.match(in) {
55+
c.appendMissingOwnerRefs(in)
56+
}
57+
return c.SecretInterface.Create(ctx, in, opts)
58+
}
59+
60+
func (c *ownerRefSecretClient) Update(ctx context.Context, in *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
61+
if c.match == nil || c.match(in) {
62+
c.appendMissingOwnerRefs(in)
63+
}
64+
return c.SecretInterface.Update(ctx, in, opts)
65+
}

0 commit comments

Comments
 (0)