Skip to content

Commit beef96e

Browse files
armrufcanovai
andauthored
chore: add secrets cache (cloudnative-pg#47)
Signed-off-by: Armando Ruocco <[email protected]> Signed-off-by: Francesco Canovai <[email protected]> Co-authored-by: Francesco Canovai <[email protected]>
1 parent 74bc9e2 commit beef96e

15 files changed

+435
-28
lines changed

Makefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ help: ## Display this help.
4545

4646
.PHONY: manifests
4747
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
48-
$(CONTROLLER_GEN) rbac:roleName=plugin-barman-cloud crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
48+
$(CONTROLLER_GEN) rbac:roleName=plugin-barman-cloud crd webhook paths="./api/..." output:crd:artifacts:config=config/crd/bases
4949

5050
.PHONY: generate
5151
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
52-
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
52+
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./api/..."
5353

5454
.PHONY: fmt
5555
fmt: ## Run go fmt against code.

api/v1/doc.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
Copyright The CloudNativePG Contributors
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 v1 contains API Schema definitions for the barmancloud v1 API group
18+
// +kubebuilder:object:generate=true
19+
// +groupName=barmancloud.cnpg.io
20+
package v1

api/v1/objectstore_types.go

+24-5
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,30 @@ import (
2121
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2222
)
2323

24-
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
25-
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
24+
// InstanceSidecarConfiguration defines the configuration for the sidecar that runs in the instance pods.
25+
type InstanceSidecarConfiguration struct {
26+
// The expiration time of the cache entries not managed by the informers. Expressed in seconds.
27+
// +optional
28+
// +kubebuilder:validation:Minimum=0
29+
// +kubebuilder:validation:Maximum=3600
30+
// +kubebuilder:default=180
31+
CacheTTL *int `json:"cacheTTL,omitempty"`
32+
}
33+
34+
// GetCacheTTL returns the cache TTL value, defaulting to 180 seconds if not set.
35+
func (i InstanceSidecarConfiguration) GetCacheTTL() int {
36+
if i.CacheTTL == nil {
37+
return 180
38+
}
39+
return *i.CacheTTL
40+
}
2641

2742
// ObjectStoreSpec defines the desired state of ObjectStore.
2843
type ObjectStoreSpec struct {
2944
Configuration barmanapi.BarmanObjectStoreConfiguration `json:"configuration"`
3045

31-
// TODO: we add here any exclusive fields for our plugin CRD
46+
// +optional
47+
InstanceSidecarConfiguration InstanceSidecarConfiguration `json:"instanceSidecarConfiguration,omitempty"`
3248
}
3349

3450
// ObjectStoreStatus defines the observed state of ObjectStore.
@@ -39,13 +55,16 @@ type ObjectStoreStatus struct {
3955

4056
// +kubebuilder:object:root=true
4157
// +kubebuilder:subresource:status
58+
// +genclient
59+
// +kubebuilder:storageversion
4260

4361
// ObjectStore is the Schema for the objectstores API.
4462
type ObjectStore struct {
4563
metav1.TypeMeta `json:",inline"`
46-
metav1.ObjectMeta `json:"metadata,omitempty"`
64+
metav1.ObjectMeta `json:"metadata"`
4765

48-
Spec ObjectStoreSpec `json:"spec,omitempty"`
66+
Spec ObjectStoreSpec `json:"spec"`
67+
// +optional
4968
Status ObjectStoreStatus `json:"status,omitempty"`
5069
}
5170

api/v1/zz_generated.deepcopy.go

+21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/barmancloud.cnpg.io_objectstores.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,18 @@ spec:
378378
required:
379379
- destinationPath
380380
type: object
381+
instanceSidecarConfiguration:
382+
description: InstanceSidecarConfiguration defines the configuration
383+
for the sidecar that runs in the instance pods.
384+
properties:
385+
cacheTTL:
386+
default: 180
387+
description: The expiration time of the cache entries not managed
388+
by the informers. Expressed in seconds.
389+
maximum: 3600
390+
minimum: 0
391+
type: integer
392+
type: object
381393
required:
382394
- configuration
383395
type: object

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ require (
99
github.com/cloudnative-pg/barman-cloud v0.0.0-20241105055149-ae6c2408bd14
1010
github.com/cloudnative-pg/cloudnative-pg v1.24.1-0.20241113134512-8608232c2813
1111
github.com/cloudnative-pg/cnpg-i v0.0.0-20241109002750-8abd359df734
12-
github.com/cloudnative-pg/cnpg-i-machinery v0.0.0-20241014090747-e9c2b3738d19
13-
github.com/cloudnative-pg/machinery v0.0.0-20241030141148-670a0f16f836
12+
github.com/cloudnative-pg/cnpg-i-machinery v0.0.0-20241030141108-7e59fc9f4797
13+
github.com/cloudnative-pg/machinery v0.0.0-20241105070525-042a028b767c
1414
github.com/onsi/ginkgo/v2 v2.21.0
1515
github.com/onsi/gomega v1.35.1
1616
github.com/spf13/cobra v1.8.1

go.sum

+4-4
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ github.com/cloudnative-pg/cloudnative-pg v1.24.1-0.20241113134512-8608232c2813 h
2424
github.com/cloudnative-pg/cloudnative-pg v1.24.1-0.20241113134512-8608232c2813/go.mod h1:f4hObdRVoQtMmVtWqZ6VDZBrI6ok9Td/UMhojQ+EPmk=
2525
github.com/cloudnative-pg/cnpg-i v0.0.0-20241109002750-8abd359df734 h1:4jq/FUrlAKxu0Kw9PL5lj5Njq8pAnmUpP/kXKOrJAaE=
2626
github.com/cloudnative-pg/cnpg-i v0.0.0-20241109002750-8abd359df734/go.mod h1:3U7miYasKr2rYCQzrn/IvbSQc0OpYF8ieZt2FKG4nv0=
27-
github.com/cloudnative-pg/cnpg-i-machinery v0.0.0-20241014090747-e9c2b3738d19 h1:qy+LrScvQpIwt4qeg9FfCJuoC9CbX/kpFGLF8vSobXg=
28-
github.com/cloudnative-pg/cnpg-i-machinery v0.0.0-20241014090747-e9c2b3738d19/go.mod h1:X6r1fRuUEIAv4+5SSBY2RmQ201K6GcptOXgnmaX/8tY=
29-
github.com/cloudnative-pg/machinery v0.0.0-20241030141148-670a0f16f836 h1:Hhg+I2QcaPNN5XaSsYb7Xw3PbQlvCA9eDY+SvVf902Q=
30-
github.com/cloudnative-pg/machinery v0.0.0-20241030141148-670a0f16f836/go.mod h1:+mUFdys1IX+qwQUrV+/i56Tey/mYh8ZzWZYttwivRns=
27+
github.com/cloudnative-pg/cnpg-i-machinery v0.0.0-20241030141108-7e59fc9f4797 h1:8iaPgTx16yzx8rrhOi99u+GWGp47kqveF9NShElsYKM=
28+
github.com/cloudnative-pg/cnpg-i-machinery v0.0.0-20241030141108-7e59fc9f4797/go.mod h1:X6r1fRuUEIAv4+5SSBY2RmQ201K6GcptOXgnmaX/8tY=
29+
github.com/cloudnative-pg/machinery v0.0.0-20241105070525-042a028b767c h1:t0RBU2gBiwJQ9XGeXlHPBYpsTscSKHgB5TfcWaiwanc=
30+
github.com/cloudnative-pg/machinery v0.0.0-20241105070525-042a028b767c/go.mod h1:uBHGRIk5rt07mO4zjIC1uvGBWTH6PqIiD1PfpvPGZKU=
3131
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
3232
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3333
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync"
7+
"time"
8+
9+
"github.com/cloudnative-pg/machinery/pkg/log"
10+
corev1 "k8s.io/api/core/v1"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
13+
v1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
14+
)
15+
16+
type cachedSecret struct {
17+
secret *corev1.Secret
18+
fetchUnixTime int64
19+
}
20+
21+
// ExtendedClient is an extended client that is capable of caching multiple secrets without relying on informers
22+
type ExtendedClient struct {
23+
client.Client
24+
barmanObjectKey client.ObjectKey
25+
cachedSecrets []*cachedSecret
26+
mux *sync.Mutex
27+
ttl int
28+
}
29+
30+
// NewExtendedClient returns an extended client capable of caching secrets on the 'Get' operation
31+
func NewExtendedClient(
32+
baseClient client.Client,
33+
objectStoreKey client.ObjectKey,
34+
) client.Client {
35+
return &ExtendedClient{
36+
Client: baseClient,
37+
barmanObjectKey: objectStoreKey,
38+
mux: &sync.Mutex{},
39+
}
40+
}
41+
42+
func (e *ExtendedClient) refreshTTL(ctx context.Context) error {
43+
var object v1.ObjectStore
44+
if err := e.Get(ctx, e.barmanObjectKey, &object); err != nil {
45+
return fmt.Errorf("failed to get the object store while refreshing the TTL parameter: %w", err)
46+
}
47+
48+
e.ttl = object.Spec.InstanceSidecarConfiguration.GetCacheTTL()
49+
50+
return nil
51+
}
52+
53+
// Get behaves like the original Get method, but uses a cache for secrets
54+
func (e *ExtendedClient) Get(
55+
ctx context.Context,
56+
key client.ObjectKey,
57+
obj client.Object,
58+
opts ...client.GetOption,
59+
) error {
60+
contextLogger := log.FromContext(ctx).
61+
WithName("extended_client").
62+
WithValues("name", key.Name, "namespace", key.Namespace)
63+
64+
if _, ok := obj.(*corev1.Secret); !ok {
65+
contextLogger.Trace("not a secret, skipping")
66+
return e.Client.Get(ctx, key, obj, opts...)
67+
}
68+
69+
if err := e.refreshTTL(ctx); err != nil {
70+
return err
71+
}
72+
73+
if e.isCacheDisabled() {
74+
contextLogger.Trace("cache is disabled")
75+
return e.Client.Get(ctx, key, obj, opts...)
76+
}
77+
78+
contextLogger.Trace("locking the cache")
79+
e.mux.Lock()
80+
defer e.mux.Unlock()
81+
82+
expiredSecretIndex := -1
83+
// check if in cache
84+
for idx, cache := range e.cachedSecrets {
85+
if cache.secret.Namespace != key.Namespace || cache.secret.Name != key.Name {
86+
continue
87+
}
88+
if e.isExpired(cache.fetchUnixTime) {
89+
contextLogger.Trace("secret found, but it is expired")
90+
expiredSecretIndex = idx
91+
break
92+
}
93+
contextLogger.Debug("secret found, loading it from cache")
94+
cache.secret.DeepCopyInto(obj.(*corev1.Secret))
95+
return nil
96+
}
97+
98+
if err := e.Client.Get(ctx, key, obj); err != nil {
99+
return err
100+
}
101+
102+
cs := &cachedSecret{
103+
secret: obj.(*corev1.Secret).DeepCopy(),
104+
fetchUnixTime: time.Now().Unix(),
105+
}
106+
107+
contextLogger.Debug("setting secret in the cache")
108+
if expiredSecretIndex != -1 {
109+
e.cachedSecrets[expiredSecretIndex] = cs
110+
} else {
111+
e.cachedSecrets = append(e.cachedSecrets, cs)
112+
}
113+
114+
return nil
115+
}
116+
117+
func (e *ExtendedClient) isExpired(unixTime int64) bool {
118+
return time.Now().Unix()-unixTime > int64(e.ttl)
119+
}
120+
121+
func (e *ExtendedClient) isCacheDisabled() bool {
122+
const noCache = 0
123+
return e.ttl == noCache
124+
}
125+
126+
// RemoveSecret ensures that a secret is not present in the cache
127+
func (e *ExtendedClient) RemoveSecret(key client.ObjectKey) {
128+
if e.isCacheDisabled() {
129+
return
130+
}
131+
132+
e.mux.Lock()
133+
defer e.mux.Unlock()
134+
135+
for i, cache := range e.cachedSecrets {
136+
if cache.secret.Namespace == key.Namespace && cache.secret.Name == key.Name {
137+
e.cachedSecrets = append(e.cachedSecrets[:i], e.cachedSecrets[i+1:]...)
138+
return
139+
}
140+
}
141+
}
142+
143+
// Update behaves like the original Update method, but on secrets it removes the secret from the cache
144+
func (e *ExtendedClient) Update(
145+
ctx context.Context,
146+
obj client.Object,
147+
opts ...client.UpdateOption,
148+
) error {
149+
if e.isCacheDisabled() {
150+
return e.Client.Update(ctx, obj, opts...)
151+
}
152+
153+
if _, ok := obj.(*corev1.Secret); !ok {
154+
return e.Client.Update(ctx, obj, opts...)
155+
}
156+
157+
e.RemoveSecret(client.ObjectKeyFromObject(obj))
158+
159+
return e.Client.Update(ctx, obj, opts...)
160+
}
161+
162+
// Delete behaves like the original Delete method, but on secrets it removes the secret from the cache
163+
func (e *ExtendedClient) Delete(
164+
ctx context.Context,
165+
obj client.Object,
166+
opts ...client.DeleteOption,
167+
) error {
168+
if e.isCacheDisabled() {
169+
return e.Client.Delete(ctx, obj, opts...)
170+
}
171+
172+
if _, ok := obj.(*corev1.Secret); !ok {
173+
return e.Client.Delete(ctx, obj, opts...)
174+
}
175+
176+
e.RemoveSecret(client.ObjectKeyFromObject(obj))
177+
178+
return e.Client.Delete(ctx, obj, opts...)
179+
}
180+
181+
// Patch behaves like the original Patch method, but on secrets it removes the secret from the cache
182+
func (e *ExtendedClient) Patch(
183+
ctx context.Context,
184+
obj client.Object,
185+
patch client.Patch,
186+
opts ...client.PatchOption,
187+
) error {
188+
if e.isCacheDisabled() {
189+
return e.Client.Patch(ctx, obj, patch, opts...)
190+
}
191+
192+
if _, ok := obj.(*corev1.Secret); !ok {
193+
return e.Client.Patch(ctx, obj, patch, opts...)
194+
}
195+
196+
e.RemoveSecret(client.ObjectKeyFromObject(obj))
197+
198+
return e.Client.Patch(ctx, obj, patch, opts...)
199+
}

0 commit comments

Comments
 (0)