ソースを参照

feat(akeyless): add SecretStore ignoreCache to bypass Gateway cache (#6507)

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Signed-off-by: baraka-akeyless <barak.a@akeyless.io>
baraka_akeyless 6 日 前
コミット
2b4413b703

+ 5 - 0
apis/externalsecrets/v1/secretstore_akeyless_types.go

@@ -26,6 +26,11 @@ type AkeylessProvider struct {
 	// Akeyless GW API Url from which the secrets to be fetched from.
 	AkeylessGWApiURL *string `json:"akeylessGWApiURL"`
 
+	// IgnoreCache bypasses the Gateway cache for secret reads when true.
+	// Only relevant when akeylessGWApiURL points to an Akeyless Gateway.
+	// +optional
+	IgnoreCache *bool `json:"ignoreCache,omitempty"`
+
 	// Auth configures how the operator authenticates with Akeyless.
 	Auth *AkeylessAuth `json:"authSecretRef"`
 

+ 5 - 0
apis/externalsecrets/v1/zz_generated.deepcopy.go

@@ -219,6 +219,11 @@ func (in *AkeylessProvider) DeepCopyInto(out *AkeylessProvider) {
 		*out = new(string)
 		**out = **in
 	}
+	if in.IgnoreCache != nil {
+		in, out := &in.IgnoreCache, &out.IgnoreCache
+		*out = new(bool)
+		**out = **in
+	}
 	if in.Auth != nil {
 		in, out := &in.Auth, &out.Auth
 		*out = new(AkeylessAuth)

+ 5 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -399,6 +399,11 @@ spec:
                         - name
                         - type
                         type: object
+                      ignoreCache:
+                        description: |-
+                          IgnoreCache bypasses the Gateway cache for secret reads when true.
+                          Only relevant when akeylessGWApiURL points to an Akeyless Gateway.
+                        type: boolean
                     required:
                     - akeylessGWApiURL
                     - authSecretRef

+ 5 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -399,6 +399,11 @@ spec:
                         - name
                         - type
                         type: object
+                      ignoreCache:
+                        description: |-
+                          IgnoreCache bypasses the Gateway cache for secret reads when true.
+                          Only relevant when akeylessGWApiURL points to an Akeyless Gateway.
+                        type: boolean
                     required:
                     - akeylessGWApiURL
                     - authSecretRef

+ 10 - 0
deploy/crds/bundle.yaml

@@ -2709,6 +2709,11 @@ spec:
                             - name
                             - type
                           type: object
+                        ignoreCache:
+                          description: |-
+                            IgnoreCache bypasses the Gateway cache for secret reads when true.
+                            Only relevant when akeylessGWApiURL points to an Akeyless Gateway.
+                          type: boolean
                       required:
                         - akeylessGWApiURL
                         - authSecretRef
@@ -15480,6 +15485,11 @@ spec:
                             - name
                             - type
                           type: object
+                        ignoreCache:
+                          description: |-
+                            IgnoreCache bypasses the Gateway cache for secret reads when true.
+                            Only relevant when akeylessGWApiURL points to an Akeyless Gateway.
+                          type: boolean
                       required:
                         - akeylessGWApiURL
                         - authSecretRef

+ 13 - 0
docs/api/spec.md

@@ -591,6 +591,19 @@ string
 </tr>
 <tr>
 <td>
+<code>ignoreCache</code></br>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>IgnoreCache bypasses the Gateway cache for secret reads when true.
+Only relevant when akeylessGWApiURL points to an Akeyless Gateway.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>authSecretRef</code></br>
 <em>
 <a href="#external-secrets.io/v1.AkeylessAuth">

+ 5 - 0
docs/provider/akeyless.md

@@ -9,6 +9,10 @@ SecretStore resource specifies how to access Akeyless. This resource is namespac
 **NOTE:** Make sure the Akeyless provider is listed in the Kind=SecretStore.
 If you use a customer fragment, define the value of akeylessGWApiURL as the URL of your Akeyless Gateway in the following format: https://your.akeyless.gw:8080/v2.
 
+When using an Akeyless Gateway, you can set `ignoreCache: true` on the SecretStore to bypass the Gateway cache and fetch the latest secret value from Akeyless SaaS on every sync. This matches the `ignore_cache` option in the Akeyless Kubernetes Secrets Injector. Use this only when you need fresh values on each reconcile: it increases load on the Gateway and upstream Akeyless API compared to serving cached responses.
+
+**NOTE:** `ignoreCache` affects Gateway-side caching only. ESO still reuses the provider client between reconciles when the SecretStore spec is unchanged.
+
 Akeyless provides several Authentication Methods:
 
 ### Authentication with Kubernetes
@@ -77,6 +81,7 @@ spec:
   provider:
     akeyless:
       akeylessGWApiURL: "https://your.akeyless.gw:8080/v2"
+      ignoreCache: true
 
       # Optional caBundle - PEM/base64 encoded CA certificate
       caBundle: "<base64 encoded cabundle>"

+ 1 - 0
docs/snippets/akeyless-secret-store.yaml

@@ -7,6 +7,7 @@ spec:
     akeyless:
       # URL of your akeyless API
       akeylessGWApiURL: "https://api.akeyless.io"
+      # ignoreCache: true  # bypass Gateway cache when using an Akeyless Gateway URL
       authSecretRef:
         secretRef:
           accessID:

+ 25 - 3
providers/v1/akeyless/akeyless.go

@@ -42,6 +42,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/runtime/cache"
 	"github.com/external-secrets/external-secrets/runtime/esutils"
 	"github.com/external-secrets/external-secrets/runtime/find"
 )
@@ -60,7 +61,9 @@ var _ esv1.SecretsClient = &Akeyless{}
 var _ esv1.Provider = &Provider{}
 
 // Provider satisfies the provider interface.
-type Provider struct{}
+type Provider struct {
+	clientCache *cache.Cache[esv1.SecretsClient]
+}
 
 // akeylessBase satisfies the provider.SecretsClient interface.
 type akeylessBase struct {
@@ -71,6 +74,7 @@ type akeylessBase struct {
 	namespace string
 
 	akeylessGwAPIURL string
+	ignoreCache      bool
 	RestAPI          *akeyless.V2ApiService
 }
 
@@ -104,6 +108,15 @@ func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
 
 // NewClient constructs a new secrets client based on the provided store.
 func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube client.Client, namespace string) (esv1.SecretsClient, error) {
+	key := cache.Key{
+		Name:      store.GetObjectMeta().Name,
+		Namespace: namespace,
+		Kind:      store.GetKind(),
+	}
+	if cachedClient, ok := p.clientCache.Get(store.GetObjectMeta().ResourceVersion, key); ok {
+		return cachedClient, nil
+	}
+
 	// controller-runtime/client does not support TokenRequest or other subresource APIs
 	// so we need to construct our own client and use it to fetch tokens
 	// (for Kubernetes service account token auth)
@@ -116,7 +129,13 @@ func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube
 		return nil, err
 	}
 
-	return newClient(ctx, store, kube, clientset.CoreV1(), namespace)
+	client, err := newClient(ctx, store, kube, clientset.CoreV1(), namespace)
+	if err != nil {
+		return nil, err
+	}
+
+	p.clientCache.Add(store.GetObjectMeta().ResourceVersion, key, client)
+	return client, nil
 }
 
 // ValidateStore validates the configuration of the Akeyless provider in the store.
@@ -232,6 +251,7 @@ func newClient(ctx context.Context, store esv1.GenericStore, kube client.Client,
 	}).V2Api
 
 	akl.akeylessGwAPIURL = akeylessGwAPIURL
+	akl.ignoreCache = ignoreCacheEnabled(spec)
 	akl.RestAPI = RestAPIClient
 	return &Akeyless{Client: akl, url: akeylessGwAPIURL}, nil
 }
@@ -578,7 +598,9 @@ func (a *akeylessBase) getAkeylessHTTPClient(ctx context.Context, provider *esv1
 
 // NewProvider creates a new Provider instance.
 func NewProvider() esv1.Provider {
-	return &Provider{}
+	return &Provider{
+		clientCache: cache.Must[esv1.SecretsClient](100, nil),
+	}
 }
 
 // ProviderSpec returns the provider specification for registration.

+ 9 - 0
providers/v1/akeyless/akeyless_api.go

@@ -172,6 +172,9 @@ func (a *akeylessBase) GetCertificate(ctx context.Context, certificateName strin
 		Name:    certificateName,
 		Version: &version,
 	}
+	if a.ignoreCache {
+		body.SetIgnoreCache("true")
+	}
 	if err := SetBodyToken(ctx, &body); err != nil {
 		return "", err
 	}
@@ -200,6 +203,9 @@ func (a *akeylessBase) GetRotatedSecrets(ctx context.Context, secretName string,
 		Names:   secretName,
 		Version: &version,
 	}
+	if a.ignoreCache {
+		body.SetIgnoreCache("true")
+	}
 	if err := SetBodyToken(ctx, &body); err != nil {
 		return "", err
 	}
@@ -267,6 +273,9 @@ func (a *akeylessBase) GetStaticSecret(ctx context.Context, secretName string, v
 		Names:   []string{secretName},
 		Version: &version,
 	}
+	if a.ignoreCache {
+		body.SetIgnoreCache("true")
+	}
 	if err := SetBodyToken(ctx, &body); err != nil {
 		return "", err
 	}

+ 1 - 0
providers/v1/akeyless/go.mod

@@ -61,6 +61,7 @@ require (
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
 	github.com/googleapis/gax-go/v2 v2.15.0 // indirect
+	github.com/hashicorp/golang-lru v1.0.2 // indirect
 	github.com/huandu/xstrings v1.5.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect

+ 2 - 0
providers/v1/akeyless/go.sum

@@ -132,6 +132,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU
 github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
 github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
 github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
+github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
+github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
 github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=

+ 4 - 0
providers/v1/akeyless/utils.go

@@ -66,6 +66,10 @@ func GetAKeylessProvider(store esv1.GenericStore) (*esv1.AkeylessProvider, error
 	return prov, nil
 }
 
+func ignoreCacheEnabled(prov *esv1.AkeylessProvider) bool {
+	return prov != nil && prov.IgnoreCache != nil && *prov.IgnoreCache
+}
+
 func getV2Url(path string) string {
 	// add check if not v2
 	rebody := sendReq(path)

+ 66 - 0
providers/v1/akeyless/utils_test.go

@@ -0,0 +1,66 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package akeyless
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func TestIgnoreCacheEnabled(t *testing.T) {
+	t.Parallel()
+
+	trueVal := true
+	falseVal := false
+
+	tests := []struct {
+		name string
+		prov *esv1.AkeylessProvider
+		want bool
+	}{
+		{
+			name: "nil provider",
+			prov: nil,
+			want: false,
+		},
+		{
+			name: "unset",
+			prov: &esv1.AkeylessProvider{},
+			want: false,
+		},
+		{
+			name: "explicit false",
+			prov: &esv1.AkeylessProvider{IgnoreCache: &falseVal},
+			want: false,
+		},
+		{
+			name: "explicit true",
+			prov: &esv1.AkeylessProvider{IgnoreCache: &trueVal},
+			want: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			require.Equal(t, tt.want, ignoreCacheEnabled(tt.prov))
+		})
+	}
+}

+ 1 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -50,6 +50,7 @@ spec:
         name: string
         namespace: string
         type: "Secret" # "Secret", "ConfigMap"
+      ignoreCache: true
     aws:
       additionalRoles: [] # minItems 0 of type string
       auth:

+ 1 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -50,6 +50,7 @@ spec:
         name: string
         namespace: string
         type: "Secret" # "Secret", "ConfigMap"
+      ignoreCache: true
     aws:
       additionalRoles: [] # minItems 0 of type string
       auth: