Browse Source

feat: make cache generic, refactor feature flags (#1640)

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 3 years ago
parent
commit
5ef3b23a68

+ 12 - 15
cmd/root.go

@@ -39,8 +39,7 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/controllers/externalsecret"
 	"github.com/external-secrets/external-secrets/pkg/controllers/externalsecret"
 	"github.com/external-secrets/external-secrets/pkg/controllers/pushsecret"
 	"github.com/external-secrets/external-secrets/pkg/controllers/pushsecret"
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
-	awsauth "github.com/external-secrets/external-secrets/pkg/provider/aws/auth"
-	"github.com/external-secrets/external-secrets/pkg/provider/vault"
+	"github.com/external-secrets/external-secrets/pkg/feature"
 )
 )
 
 
 var (
 var (
@@ -72,9 +71,6 @@ var (
 	crdRequeueInterval                    time.Duration
 	crdRequeueInterval                    time.Duration
 	certCheckInterval                     time.Duration
 	certCheckInterval                     time.Duration
 	certLookaheadInterval                 time.Duration
 	certLookaheadInterval                 time.Duration
-	enableAWSSession                      bool
-	enableVaultTokenCache                 bool
-	vaultTokenCacheSize                   int
 	tlsCiphers                            string
 	tlsCiphers                            string
 	tlsMinVersion                         string
 	tlsMinVersion                         string
 )
 )
@@ -205,19 +201,19 @@ var rootCmd = &cobra.Command{
 				os.Exit(1)
 				os.Exit(1)
 			}
 			}
 		}
 		}
-		if enableAWSSession {
-			awsauth.EnableCache = true
-		}
-		if enableVaultTokenCache {
-			vault.EnableCache = true
-			vault.VaultClientCache.Size = vaultTokenCacheSize
+
+		fs := feature.Features()
+		for _, f := range fs {
+			if f.Initialize == nil {
+				continue
+			}
+			f.Initialize()
 		}
 		}
 		setupLog.Info("starting manager")
 		setupLog.Info("starting manager")
 		if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
 		if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
 			setupLog.Error(err, "problem running manager")
 			setupLog.Error(err, "problem running manager")
 			os.Exit(1)
 			os.Exit(1)
 		}
 		}
-
 	},
 	},
 }
 }
 
 
@@ -244,7 +240,8 @@ func init() {
 	rootCmd.Flags().BoolVar(&enableConfigMapsCache, "enable-configmaps-caching", false, "Enable secrets caching for external-secrets pod.")
 	rootCmd.Flags().BoolVar(&enableConfigMapsCache, "enable-configmaps-caching", false, "Enable secrets caching for external-secrets pod.")
 	rootCmd.Flags().DurationVar(&storeRequeueInterval, "store-requeue-interval", time.Minute*5, "Default Time duration between reconciling (Cluster)SecretStores")
 	rootCmd.Flags().DurationVar(&storeRequeueInterval, "store-requeue-interval", time.Minute*5, "Default Time duration between reconciling (Cluster)SecretStores")
 	rootCmd.Flags().BoolVar(&enableFloodGate, "enable-flood-gate", true, "Enable flood gate. External secret will be reconciled only if the ClusterStore or Store have an healthy or unknown state.")
 	rootCmd.Flags().BoolVar(&enableFloodGate, "enable-flood-gate", true, "Enable flood gate. External secret will be reconciled only if the ClusterStore or Store have an healthy or unknown state.")
-	rootCmd.Flags().BoolVar(&enableAWSSession, "experimental-enable-aws-session-cache", false, "Enable experimental AWS session cache. External secret will reuse the AWS session without creating a new one on each request.")
-	rootCmd.Flags().BoolVar(&enableVaultTokenCache, "experimental-enable-vault-token-cache", false, "Enable experimental Vault token cache. External secrets will reuse the Vault token without creating a new one on each request.")
-	rootCmd.Flags().IntVar(&vaultTokenCacheSize, "experimental-vault-token-cache-size", 100, "Maximum size of Vault token cache. Only used if --experimental-enable-vault-token-cache is set.")
+	fs := feature.Features()
+	for _, f := range fs {
+		rootCmd.Flags().AddFlagSet(f.Flags)
+	}
 }
 }

+ 16 - 0
e2e/framework/addon/eso.go

@@ -57,6 +57,22 @@ func NewESO(mutators ...MutationFunc) *ESO {
 					Key:   installCRDsVar,
 					Key:   installCRDsVar,
 					Value: "false",
 					Value: "false",
 				},
 				},
+				{
+					Key:   "concurrent",
+					Value: "100",
+				},
+				{
+					Key:   "extraArgs.experimental-enable-vault-token-cache",
+					Value: "true",
+				},
+				{
+					Key:   "extraArgs.experimental-enable-aws-session-cache",
+					Value: "true",
+				},
+				{
+					Key:   "extraArgs.experimental-vault-token-cache-size",
+					Value: "10",
+				},
 			},
 			},
 		},
 		},
 	}
 	}

+ 55 - 0
e2e/framework/eso.go

@@ -20,11 +20,16 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"time"
 	"time"
 
 
+	//nolint
+	. "github.com/onsi/gomega"
+
 	v1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/apimachinery/pkg/util/wait"
 
 
+	"github.com/external-secrets/external-secrets-e2e/framework/log"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 )
 )
 
 
@@ -45,6 +50,56 @@ func (f *Framework) WaitForSecretValue(namespace, name string, expected *v1.Secr
 	return secret, err
 	return secret, err
 }
 }
 
 
+func (f *Framework) printESDebugLogs(esName, esNamespace string) {
+	// fetch es and print status condition
+	var es esv1beta1.ExternalSecret
+	err := f.CRClient.Get(context.Background(), types.NamespacedName{
+		Name:      esName,
+		Namespace: esNamespace,
+	}, &es)
+	Expect(err).ToNot(HaveOccurred())
+	log.Logf("resourceVersion=%s", es.Status.SyncedResourceVersion)
+	for _, cond := range es.Status.Conditions {
+		log.Logf("condition: status=%s type=%s reason=%s message=%s", cond.Status, cond.Type, cond.Reason, cond.Message)
+	}
+	// list events for given
+	evs, err := f.KubeClientSet.CoreV1().Events(esNamespace).List(context.Background(), metav1.ListOptions{
+		FieldSelector: "involvedObject.name=" + esName + ",involvedObject.kind=ExternalSecret",
+	})
+	Expect(err).ToNot(HaveOccurred())
+	for _, ev := range evs.Items {
+		log.Logf("ev reason=%s message=%s", ev.Reason, ev.Message)
+	}
+
+	// print most recent logs of default eso installation
+	podList, err := f.KubeClientSet.CoreV1().Pods("default").List(
+		context.Background(),
+		metav1.ListOptions{LabelSelector: "app.kubernetes.io/instance=eso,app.kubernetes.io/name=external-secrets"})
+	Expect(err).ToNot(HaveOccurred())
+	numLines := int64(60)
+	for i := range podList.Items {
+		pod := podList.Items[i]
+		for _, con := range pod.Spec.Containers {
+			for _, b := range []bool{true, false} {
+				resp := f.KubeClientSet.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &v1.PodLogOptions{
+					Container: con.Name,
+					Previous:  b,
+					TailLines: &numLines,
+				}).Do(context.TODO())
+				err := resp.Error()
+				if err != nil {
+					continue
+				}
+				logs, err := resp.Raw()
+				if err != nil {
+					continue
+				}
+				log.Logf("[%s]: %s", "eso", string(logs))
+			}
+		}
+	}
+}
+
 func equalSecrets(exp, ts *v1.Secret) bool {
 func equalSecrets(exp, ts *v1.Secret) bool {
 	if exp.Type != ts.Type {
 	if exp.Type != ts.Type {
 		return false
 		return false

+ 2 - 2
e2e/framework/log/log.go

@@ -3,7 +3,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 You may obtain a copy of the License at
 
 
-    http://www.apache.org/licenses/LICENSE-2.0
+	http://www.apache.org/licenses/LICENSE-2.0
 
 
 Unless required by applicable law or agreed to in writing, software
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,5 +19,5 @@ import (
 
 
 // Logf logs the format string to ginkgo stdout.
 // Logf logs the format string to ginkgo stdout.
 func Logf(format string, args ...interface{}) {
 func Logf(format string, args ...interface{}) {
-	ginkgo.GinkgoWriter.Printf(format, args...)
+	ginkgo.GinkgoWriter.Printf(format+"\n", args...)
 }
 }

+ 1 - 0
e2e/framework/testcase.go

@@ -96,6 +96,7 @@ func TableFunc(f *Framework, prov SecretStoreProvider) func(...func(*TestCase))
 		// wait for Kind=Secret to have the expected data
 		// wait for Kind=Secret to have the expected data
 		secret, err := tc.Framework.WaitForSecretValue(tc.Framework.Namespace.Name, TargetSecretName, tc.ExpectedSecret)
 		secret, err := tc.Framework.WaitForSecretValue(tc.Framework.Namespace.Name, TargetSecretName, tc.ExpectedSecret)
 		if err != nil {
 		if err != nil {
+			f.printESDebugLogs(tc.ExternalSecret.Name, tc.ExternalSecret.Namespace)
 			log.Logf("Did not match. Expected: %+v, Got: %+v", tc.ExpectedSecret, secret)
 			log.Logf("Did not match. Expected: %+v, Got: %+v", tc.ExpectedSecret, secret)
 		}
 		}
 
 

+ 1 - 1
go.mod

@@ -98,6 +98,7 @@ require (
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0
 	github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0
 	github.com/sethvargo/go-password v0.2.0
 	github.com/sethvargo/go-password v0.2.0
+	github.com/spf13/pflag v1.0.5
 	sigs.k8s.io/yaml v1.3.0
 	sigs.k8s.io/yaml v1.3.0
 )
 )
 
 
@@ -206,7 +207,6 @@ require (
 	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/sony/gobreaker v0.5.0 // indirect
 	github.com/sony/gobreaker v0.5.0 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
 	github.com/tidwall/pretty v1.2.0 // indirect
 	github.com/tidwall/pretty v1.2.0 // indirect
 	github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
 	github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect

+ 99 - 0
pkg/cache/cache.go

@@ -0,0 +1,99 @@
+/*
+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
+
+    http://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 cache
+
+import (
+	"fmt"
+
+	lru "github.com/hashicorp/golang-lru"
+)
+
+// Cache is a generic lru cache that allows you to
+// lookup values using a key and a version.
+// By design, this cache allows access to only a single version of a given key.
+// A version mismatch is considered a cache miss and the key gets evicted if it exists.
+// When a key is evicted a optional cleanup function is called.
+type Cache[T any] struct {
+	lru         *lru.Cache
+	size        int
+	cleanupFunc cleanupFunc[T]
+}
+
+// Key is the cache lookup key.
+type Key struct {
+	Name      string
+	Namespace string
+	Kind      string
+}
+
+type value[T any] struct {
+	Version string
+	Client  T
+}
+
+type cleanupFunc[T any] func(client T)
+
+// New constructs a new lru cache with the desired size and cleanup func.
+func New[T any](size int, cleanup cleanupFunc[T]) (*Cache[T], error) {
+	lruCache, err := lru.NewWithEvict(size, func(_, val any) {
+		if cleanup == nil {
+			return
+		}
+		cleanup(val.(value[T]).Client)
+	})
+	if err != nil {
+		return nil, fmt.Errorf("unable to create lru: %w", err)
+	}
+	return &Cache[T]{
+		lru:         lruCache,
+		size:        size,
+		cleanupFunc: cleanup,
+	}, nil
+}
+
+// Must creates a new lru cache with the desired size and cleanup func
+// This function panics if a error occurrs.
+func Must[T any](size int, cleanup cleanupFunc[T]) *Cache[T] {
+	c, err := New(size, cleanup)
+	if err != nil {
+		panic(err)
+	}
+	return c
+}
+
+// Get retrieves the desired value using the key and
+// compares the version. If there is a mismatch
+// it is considered a cache miss and the existing key is purged.
+func (c *Cache[T]) Get(version string, key Key) (T, bool) {
+	val, ok := c.lru.Get(key)
+	if ok {
+		cachedClient := val.(value[T])
+		if cachedClient.Version == version {
+			return cachedClient.Client, true
+		}
+		c.lru.Remove(key)
+	}
+	return value[T]{}.Client, false
+}
+
+// Add adds a new value for the given key/version.
+func (c *Cache[T]) Add(version string, key Key, client T) {
+	c.lru.Add(key, value[T]{Version: version, Client: client})
+}
+
+// Contains returns true if a value with the given key exists.
+func (c *Cache[T]) Contains(key Key) bool {
+	return c.lru.Contains(key)
+}

+ 99 - 0
pkg/cache/cache_test.go

@@ -0,0 +1,99 @@
+/*
+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
+
+	http://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 cache
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type client struct{}
+
+var cacheKey = Key{Name: "foo"}
+
+func TestCacheAdd(t *testing.T) {
+	c, err := New[client](1, nil)
+	if err != nil {
+		t.Fail()
+	}
+
+	cl := client{}
+	c.Add("", cacheKey, cl)
+	cachedVal, _ := c.Get("", cacheKey)
+
+	assert.EqualValues(t, cl, cachedVal)
+}
+
+func TestCacheContains(t *testing.T) {
+	c, err := New[client](1, nil)
+	if err != nil {
+		t.Fail()
+	}
+
+	cl := client{}
+	c.Add("", cacheKey, cl)
+	exists := c.Contains(cacheKey)
+	notExists := c.Contains(Key{Name: "does not exist"})
+
+	assert.True(t, exists)
+	assert.False(t, notExists)
+	assert.Nil(t, err)
+}
+
+func TestCacheGet(t *testing.T) {
+	c, err := New[*client](1, nil)
+	if err != nil {
+		t.Fail()
+	}
+	cachedVal, ok := c.Get("", cacheKey)
+
+	assert.Nil(t, cachedVal)
+	assert.False(t, ok)
+}
+
+func TestCacheGetInvalidVersion(t *testing.T) {
+	var cleanupCalled bool
+	c, err := New(1, func(client *client) {
+		cleanupCalled = true
+	})
+	if err != nil {
+		t.Fail()
+	}
+	cl := &client{}
+	c.Add("", cacheKey, cl)
+	cachedVal, ok := c.Get("invalid", cacheKey)
+
+	assert.Nil(t, cachedVal)
+	assert.False(t, ok)
+	assert.True(t, cleanupCalled)
+}
+
+func TestCacheEvict(t *testing.T) {
+	var cleanupCalled bool
+	c, err := New(1, func(client client) {
+		cleanupCalled = true
+	})
+	if err != nil {
+		t.Fail()
+	}
+
+	// add first version
+	c.Add("", Key{Name: "foo"}, client{})
+	assert.False(t, cleanupCalled)
+
+	// adding a second version should evict old one
+	c.Add("", Key{Name: "bar"}, client{})
+	assert.True(t, cleanupCalled)
+}

+ 38 - 0
pkg/feature/feature.go

@@ -0,0 +1,38 @@
+/*
+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
+
+	http://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 feature
+
+import (
+	"github.com/spf13/pflag"
+)
+
+// Feature contains the CLI flags that a provider exposes to a user.
+// A optional Initialize func is called once the flags have been parsed.
+// A provider can use this to do late-initialization using the defined cli args.
+type Feature struct {
+	Flags      *pflag.FlagSet
+	Initialize func()
+}
+
+var features = make([]Feature, 0)
+
+// Features returns all registered features.
+func Features() []Feature {
+	return features
+}
+
+// Register registers a new feature.
+func Register(f Feature) {
+	features = append(features, f)
+}

+ 23 - 19
pkg/provider/aws/auth/auth.go

@@ -26,6 +26,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/sts"
 	"github.com/aws/aws-sdk-go/service/sts"
 	"github.com/aws/aws-sdk-go/service/sts/stsiface"
 	"github.com/aws/aws-sdk-go/service/sts/stsiface"
+	"github.com/spf13/pflag"
 	v1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/kubernetes"
@@ -34,6 +35,8 @@ import (
 	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
 	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
 
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/cache"
+	"github.com/external-secrets/external-secrets/pkg/feature"
 	"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
 	"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
 )
 )
 
 
@@ -44,17 +47,10 @@ type Config struct {
 	APIRetries int
 	APIRetries int
 }
 }
 
 
-type SessionCache struct {
-	Name            string
-	Namespace       string
-	Kind            string
-	ResourceVersion string
-}
-
 var (
 var (
-	log         = ctrl.Log.WithName("provider").WithName("aws")
-	sessions    = make(map[SessionCache]*session.Session)
-	EnableCache bool
+	log                = ctrl.Log.WithName("provider").WithName("aws")
+	enableSessionCache bool
+	sessionCache       *cache.Cache[*session.Session]
 )
 )
 
 
 const (
 const (
@@ -71,6 +67,15 @@ const (
 	errMissingAKID                             = "missing AccessKeyID"
 	errMissingAKID                             = "missing AccessKeyID"
 )
 )
 
 
+func init() {
+	fs := pflag.NewFlagSet("aws-auth", pflag.ExitOnError)
+	fs.BoolVar(&enableSessionCache, "experimental-enable-aws-session-cache", false, "Enable experimental AWS session cache. External secret will reuse the AWS session without creating a new one on each request.")
+	feature.Register(feature.Feature{
+		Flags: fs,
+	})
+	sessionCache = cache.Must[*session.Session](1024, nil)
+}
+
 // New creates a new aws session based on the provided store
 // New creates a new aws session based on the provided store
 // it uses the following authentication mechanisms in order:
 // it uses the following authentication mechanisms in order:
 // * service-account token authentication via AssumeRoleWithWebIdentity
 // * service-account token authentication via AssumeRoleWithWebIdentity
@@ -111,7 +116,7 @@ func New(ctx context.Context, store esv1beta1.GenericStore, kube client.Client,
 		config.WithRegion(prov.Region)
 		config.WithRegion(prov.Region)
 	}
 	}
 
 
-	sess, err := getAWSSession(config, EnableCache, store.GetName(), store.GetTypeMeta().Kind, namespace, store.GetObjectMeta().ResourceVersion)
+	sess, err := getAWSSession(config, enableSessionCache, store.GetName(), store.GetTypeMeta().Kind, namespace, store.GetObjectMeta().ResourceVersion)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -327,17 +332,16 @@ func DefaultSTSProvider(sess *session.Session) stsiface.STSAPI {
 // getAWSSession checks if an AWS session should be reused
 // getAWSSession checks if an AWS session should be reused
 // it returns the aws session or an error.
 // it returns the aws session or an error.
 func getAWSSession(config *aws.Config, enableCache bool, name, kind, namespace, resourceVersion string) (*session.Session, error) {
 func getAWSSession(config *aws.Config, enableCache bool, name, kind, namespace, resourceVersion string) (*session.Session, error) {
-	tmpSession := SessionCache{
-		Name:            name,
-		Namespace:       namespace,
-		Kind:            kind,
-		ResourceVersion: resourceVersion,
+	key := cache.Key{
+		Name:      name,
+		Namespace: namespace,
+		Kind:      kind,
 	}
 	}
 
 
 	if enableCache {
 	if enableCache {
-		sess, ok := sessions[tmpSession]
+		sess, ok := sessionCache.Get(resourceVersion, key)
 		if ok {
 		if ok {
-			log.Info("reusing aws session", "SecretStore", tmpSession.Name, "namespace", tmpSession.Namespace, "kind", tmpSession.Kind, "resourceversion", tmpSession.ResourceVersion)
+			log.Info("reusing aws session", "SecretStore", key.Name, "namespace", key.Namespace, "kind", key.Kind, "resourceversion", resourceVersion)
 			return sess, nil
 			return sess, nil
 		}
 		}
 	}
 	}
@@ -354,7 +358,7 @@ func getAWSSession(config *aws.Config, enableCache bool, name, kind, namespace,
 	}
 	}
 
 
 	if enableCache {
 	if enableCache {
-		sessions[tmpSession] = sess
+		sessionCache.Add(resourceVersion, key, sess)
 	}
 	}
 	return sess, nil
 	return sess, nil
 }
 }

+ 0 - 106
pkg/provider/vault/cache.go

@@ -1,106 +0,0 @@
-/*
-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
-
-    http://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 vault
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"sync"
-
-	lru "github.com/hashicorp/golang-lru"
-
-	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-)
-
-type clientCache struct {
-	cache       *lru.Cache
-	Size        int
-	initialized bool
-	mu          sync.Mutex
-}
-
-type clientCacheKey struct {
-	Name      string
-	Namespace string
-	Kind      string
-}
-
-type clientCacheValue struct {
-	ResourceVersion string
-	Client          Client
-}
-
-func (c *clientCache) initialize() error {
-	if !c.initialized {
-		var err error
-		c.cache, err = lru.New(c.Size)
-		if err != nil {
-			return fmt.Errorf(errVaultCacheCreate, err)
-		}
-		c.initialized = true
-	}
-	return nil
-}
-
-func (c *clientCache) get(ctx context.Context, store esv1beta1.GenericStore, key clientCacheKey) (Client, bool, error) {
-	value, ok := c.cache.Get(key)
-	if ok {
-		cachedClient := value.(clientCacheValue)
-		if cachedClient.ResourceVersion == store.GetObjectMeta().ResourceVersion {
-			return cachedClient.Client, true, nil
-		}
-		// revoke token and clear old item from cache if resource has been updated
-		err := revokeTokenIfValid(ctx, cachedClient.Client)
-		if err != nil {
-			return nil, false, err
-		}
-		c.cache.Remove(key)
-	}
-	return nil, false, nil
-}
-
-func (c *clientCache) add(ctx context.Context, store esv1beta1.GenericStore, key clientCacheKey, client Client) error {
-	// don't let the LRU cache evict items
-	// remove the oldest item manually when needed so we can do some cleanup
-	for c.cache.Len() >= c.Size {
-		_, value, ok := c.cache.RemoveOldest()
-		if !ok {
-			return errors.New(errVaultCacheRemove)
-		}
-		cachedClient := value.(clientCacheValue)
-		err := revokeTokenIfValid(ctx, cachedClient.Client)
-		if err != nil {
-			return fmt.Errorf(errVaultRevokeToken, err)
-		}
-	}
-	evicted := c.cache.Add(key, clientCacheValue{ResourceVersion: store.GetObjectMeta().ResourceVersion, Client: client})
-	if evicted {
-		return errors.New(errVaultCacheEviction)
-	}
-	return nil
-}
-
-func (c *clientCache) contains(key clientCacheKey) bool {
-	return c.cache.Contains(key)
-}
-
-func (c *clientCache) lock() {
-	c.mu.Lock()
-}
-
-func (c *clientCache) unlock() {
-	c.mu.Unlock()
-}

+ 43 - 37
pkg/provider/vault/vault.go

@@ -32,6 +32,7 @@ import (
 	approle "github.com/hashicorp/vault/api/auth/approle"
 	approle "github.com/hashicorp/vault/api/auth/approle"
 	authkubernetes "github.com/hashicorp/vault/api/auth/kubernetes"
 	authkubernetes "github.com/hashicorp/vault/api/auth/kubernetes"
 	authldap "github.com/hashicorp/vault/api/auth/ldap"
 	authldap "github.com/hashicorp/vault/api/auth/ldap"
+	"github.com/spf13/pflag"
 	"github.com/tidwall/gjson"
 	"github.com/tidwall/gjson"
 	authenticationv1 "k8s.io/api/authentication/v1"
 	authenticationv1 "k8s.io/api/authentication/v1"
 	corev1 "k8s.io/api/core/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -45,15 +46,18 @@ import (
 
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/cache"
+	"github.com/external-secrets/external-secrets/pkg/feature"
 	"github.com/external-secrets/external-secrets/pkg/find"
 	"github.com/external-secrets/external-secrets/pkg/find"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 )
 
 
 var (
 var (
-	_                esv1beta1.Provider      = &connector{}
-	_                esv1beta1.SecretsClient = &client{}
-	EnableCache      bool
-	VaultClientCache clientCache
+	_           esv1beta1.Provider      = &connector{}
+	_           esv1beta1.SecretsClient = &client{}
+	enableCache bool
+	logger      = ctrl.Log.WithName("provider").WithName("vault")
+	clientCache *cache.Cache[Client]
 )
 )
 
 
 const (
 const (
@@ -198,14 +202,6 @@ type client struct {
 	storeKind string
 	storeKind string
 }
 }
 
 
-func init() {
-	esv1beta1.Register(&connector{
-		newVaultClient: newVaultClient,
-	}, &esv1beta1.SecretStoreProvider{
-		Vault: &esv1beta1.VaultProvider{},
-	})
-}
-
 func newVaultClient(c *vault.Config) (Client, error) {
 func newVaultClient(c *vault.Config) (Client, error) {
 	cl, err := vault.NewClient(c)
 	cl, err := vault.NewClient(c)
 	if err != nil {
 	if err != nil {
@@ -227,30 +223,17 @@ func newVaultClient(c *vault.Config) (Client, error) {
 	return out, nil
 	return out, nil
 }
 }
 
 
-func getVaultClient(ctx context.Context, c *connector, store esv1beta1.GenericStore, cfg *vault.Config) (Client, error) {
+func getVaultClient(c *connector, store esv1beta1.GenericStore, cfg *vault.Config) (Client, error) {
 	isStaticToken := store.GetSpec().Provider.Vault.Auth.TokenSecretRef != nil
 	isStaticToken := store.GetSpec().Provider.Vault.Auth.TokenSecretRef != nil
-	useCache := EnableCache && !isStaticToken
-
-	if useCache {
-		VaultClientCache.lock()
-		defer VaultClientCache.unlock()
+	useCache := enableCache && !isStaticToken
 
 
-		err := VaultClientCache.initialize()
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	key := clientCacheKey{
+	key := cache.Key{
 		Name:      store.GetObjectMeta().Name,
 		Name:      store.GetObjectMeta().Name,
 		Namespace: store.GetObjectMeta().Namespace,
 		Namespace: store.GetObjectMeta().Namespace,
 		Kind:      store.GetTypeMeta().Kind,
 		Kind:      store.GetTypeMeta().Kind,
 	}
 	}
 	if useCache {
 	if useCache {
-		client, ok, err := VaultClientCache.get(ctx, store, key)
-		if err != nil {
-			return nil, err
-		}
+		client, ok := clientCache.Get(store.GetObjectMeta().ResourceVersion, key)
 		if ok {
 		if ok {
 			return client, nil
 			return client, nil
 		}
 		}
@@ -261,11 +244,8 @@ func getVaultClient(ctx context.Context, c *connector, store esv1beta1.GenericSt
 		return nil, fmt.Errorf(errVaultClient, err)
 		return nil, fmt.Errorf(errVaultClient, err)
 	}
 	}
 
 
-	if useCache && !VaultClientCache.contains(key) {
-		err = VaultClientCache.add(ctx, store, key, client)
-		if err != nil {
-			return nil, err
-		}
+	if useCache && !clientCache.Contains(key) {
+		clientCache.Add(store.GetObjectMeta().ResourceVersion, key, client)
 	}
 	}
 	return client, nil
 	return client, nil
 }
 }
@@ -306,7 +286,7 @@ func (c *connector) newClient(ctx context.Context, store esv1beta1.GenericStore,
 		kube:      kube,
 		kube:      kube,
 		corev1:    corev1,
 		corev1:    corev1,
 		store:     vaultSpec,
 		store:     vaultSpec,
-		log:       ctrl.Log.WithName("provider").WithName("vault"),
+		log:       logger,
 		namespace: namespace,
 		namespace: namespace,
 		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
 		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
 	}
 	}
@@ -316,7 +296,7 @@ func (c *connector) newClient(ctx context.Context, store esv1beta1.GenericStore,
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	client, err := getVaultClient(ctx, c, store, cfg)
+	client, err := getVaultClient(c, store, cfg)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf(errVaultClient, err)
 		return nil, fmt.Errorf(errVaultClient, err)
 	}
 	}
@@ -723,7 +703,7 @@ func getTypedKey(data map[string]interface{}, key string) ([]byte, error) {
 func (v *client) Close(ctx context.Context) error {
 func (v *client) Close(ctx context.Context) error {
 	// Revoke the token if we have one set, it wasn't sourced from a TokenSecretRef,
 	// Revoke the token if we have one set, it wasn't sourced from a TokenSecretRef,
 	// and token caching isn't enabled
 	// and token caching isn't enabled
-	if !EnableCache && v.client.Token() != "" && v.store.Auth.TokenSecretRef == nil {
+	if !enableCache && v.client.Token() != "" && v.store.Auth.TokenSecretRef == nil {
 		err := revokeTokenIfValid(ctx, v.client)
 		err := revokeTokenIfValid(ctx, v.client)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -1415,3 +1395,29 @@ func (v *client) requestTokenWithCertAuth(ctx context.Context, certAuth *esv1bet
 	v.client.SetToken(token)
 	v.client.SetToken(token)
 	return nil
 	return nil
 }
 }
+
+func init() {
+	var vaultTokenCacheSize int
+	fs := pflag.NewFlagSet("vault", pflag.ExitOnError)
+	fs.BoolVar(&enableCache, "experimental-enable-vault-token-cache", false, "Enable experimental Vault token cache. External secrets will reuse the Vault token without creating a new one on each request.")
+	fs.IntVar(&vaultTokenCacheSize, "experimental-vault-token-cache-size", 100, "Maximum size of Vault token cache. Only used if --experimental-enable-vault-token-cache is set.")
+	lateInit := func() {
+		logger.Info("initializing vault cache with size=%d", vaultTokenCacheSize)
+		clientCache = cache.Must(vaultTokenCacheSize, func(client Client) {
+			err := revokeTokenIfValid(context.Background(), client)
+			if err != nil {
+				logger.Error(err, "unable to revoke cached token on eviction")
+			}
+		})
+	}
+	feature.Register(feature.Feature{
+		Flags:      fs,
+		Initialize: lateInit,
+	})
+
+	esv1beta1.Register(&connector{
+		newVaultClient: newVaultClient,
+	}, &esv1beta1.SecretStoreProvider{
+		Vault: &esv1beta1.VaultProvider{},
+	})
+}