mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
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>
This commit is contained in:
parent
769efdc391
commit
5ef3b23a68
12 changed files with 389 additions and 180 deletions
25
cmd/root.go
25
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/pushsecret"
|
||||
"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 (
|
||||
|
@ -72,9 +71,6 @@ var (
|
|||
crdRequeueInterval time.Duration
|
||||
certCheckInterval time.Duration
|
||||
certLookaheadInterval time.Duration
|
||||
enableAWSSession bool
|
||||
enableVaultTokenCache bool
|
||||
vaultTokenCacheSize int
|
||||
tlsCiphers string
|
||||
tlsMinVersion string
|
||||
)
|
||||
|
@ -205,19 +201,19 @@ var rootCmd = &cobra.Command{
|
|||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if enableAWSSession {
|
||||
awsauth.EnableCache = true
|
||||
|
||||
fs := feature.Features()
|
||||
for _, f := range fs {
|
||||
if f.Initialize == nil {
|
||||
continue
|
||||
}
|
||||
if enableVaultTokenCache {
|
||||
vault.EnableCache = true
|
||||
vault.VaultClientCache.Size = vaultTokenCacheSize
|
||||
f.Initialize()
|
||||
}
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
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().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(&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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,22 @@ func NewESO(mutators ...MutationFunc) *ESO {
|
|||
Key: installCRDsVar,
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -20,11 +20,16 @@ import (
|
|||
"encoding/json"
|
||||
"time"
|
||||
|
||||
//nolint
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -45,6 +50,56 @@ func (f *Framework) WaitForSecretValue(namespace, name string, expected *v1.Secr
|
|||
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 {
|
||||
if exp.Type != ts.Type {
|
||||
return false
|
||||
|
|
|
@ -19,5 +19,5 @@ import (
|
|||
|
||||
// Logf logs the format string to ginkgo stdout.
|
||||
func Logf(format string, args ...interface{}) {
|
||||
ginkgo.GinkgoWriter.Printf(format, args...)
|
||||
ginkgo.GinkgoWriter.Printf(format+"\n", args...)
|
||||
}
|
||||
|
|
|
@ -96,6 +96,7 @@ func TableFunc(f *Framework, prov SecretStoreProvider) func(...func(*TestCase))
|
|||
// wait for Kind=Secret to have the expected data
|
||||
secret, err := tc.Framework.WaitForSecretValue(tc.Framework.Namespace.Name, TargetSecretName, tc.ExpectedSecret)
|
||||
if err != nil {
|
||||
f.printESDebugLogs(tc.ExternalSecret.Name, tc.ExternalSecret.Namespace)
|
||||
log.Logf("Did not match. Expected: %+v, Got: %+v", tc.ExpectedSecret, secret)
|
||||
}
|
||||
|
||||
|
|
2
go.mod
2
go.mod
|
@ -98,6 +98,7 @@ require (
|
|||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0
|
||||
github.com/sethvargo/go-password v0.2.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
sigs.k8s.io/yaml v1.3.0
|
||||
)
|
||||
|
||||
|
@ -206,7 +207,6 @@ require (
|
|||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/sony/gobreaker v0.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/pretty v1.2.0 // indirect
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
|
||||
|
|
99
pkg/cache/cache.go
vendored
Normal file
99
pkg/cache/cache.go
vendored
Normal file
|
@ -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
pkg/cache/cache_test.go
vendored
Normal file
99
pkg/cache/cache_test.go
vendored
Normal file
|
@ -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
pkg/feature/feature.go
Normal file
38
pkg/feature/feature.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/aws/aws-sdk-go/service/sts/stsiface"
|
||||
"github.com/spf13/pflag"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
|
@ -34,6 +35,8 @@ import (
|
|||
ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
@ -44,17 +47,10 @@ type Config struct {
|
|||
APIRetries int
|
||||
}
|
||||
|
||||
type SessionCache struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Kind string
|
||||
ResourceVersion string
|
||||
}
|
||||
|
||||
var (
|
||||
log = ctrl.Log.WithName("provider").WithName("aws")
|
||||
sessions = make(map[SessionCache]*session.Session)
|
||||
EnableCache bool
|
||||
enableSessionCache bool
|
||||
sessionCache *cache.Cache[*session.Session]
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -71,6 +67,15 @@ const (
|
|||
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
|
||||
// it uses the following authentication mechanisms in order:
|
||||
// * 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -327,17 +332,16 @@ func DefaultSTSProvider(sess *session.Session) stsiface.STSAPI {
|
|||
// getAWSSession checks if an AWS session should be reused
|
||||
// it returns the aws session or an error.
|
||||
func getAWSSession(config *aws.Config, enableCache bool, name, kind, namespace, resourceVersion string) (*session.Session, error) {
|
||||
tmpSession := SessionCache{
|
||||
key := cache.Key{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Kind: kind,
|
||||
ResourceVersion: resourceVersion,
|
||||
}
|
||||
|
||||
if enableCache {
|
||||
sess, ok := sessions[tmpSession]
|
||||
sess, ok := sessionCache.Get(resourceVersion, key)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -354,7 +358,7 @@ func getAWSSession(config *aws.Config, enableCache bool, name, kind, namespace,
|
|||
}
|
||||
|
||||
if enableCache {
|
||||
sessions[tmpSession] = sess
|
||||
sessionCache.Add(resourceVersion, key, sess)
|
||||
}
|
||||
return sess, nil
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -32,6 +32,7 @@ import (
|
|||
approle "github.com/hashicorp/vault/api/auth/approle"
|
||||
authkubernetes "github.com/hashicorp/vault/api/auth/kubernetes"
|
||||
authldap "github.com/hashicorp/vault/api/auth/ldap"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tidwall/gjson"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
@ -45,6 +46,8 @@ import (
|
|||
|
||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||
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/utils"
|
||||
)
|
||||
|
@ -52,8 +55,9 @@ import (
|
|||
var (
|
||||
_ esv1beta1.Provider = &connector{}
|
||||
_ esv1beta1.SecretsClient = &client{}
|
||||
EnableCache bool
|
||||
VaultClientCache clientCache
|
||||
enableCache bool
|
||||
logger = ctrl.Log.WithName("provider").WithName("vault")
|
||||
clientCache *cache.Cache[Client]
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -198,14 +202,6 @@ type client struct {
|
|||
storeKind string
|
||||
}
|
||||
|
||||
func init() {
|
||||
esv1beta1.Register(&connector{
|
||||
newVaultClient: newVaultClient,
|
||||
}, &esv1beta1.SecretStoreProvider{
|
||||
Vault: &esv1beta1.VaultProvider{},
|
||||
})
|
||||
}
|
||||
|
||||
func newVaultClient(c *vault.Config) (Client, error) {
|
||||
cl, err := vault.NewClient(c)
|
||||
if err != nil {
|
||||
|
@ -227,30 +223,17 @@ func newVaultClient(c *vault.Config) (Client, error) {
|
|||
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
|
||||
useCache := EnableCache && !isStaticToken
|
||||
useCache := enableCache && !isStaticToken
|
||||
|
||||
if useCache {
|
||||
VaultClientCache.lock()
|
||||
defer VaultClientCache.unlock()
|
||||
|
||||
err := VaultClientCache.initialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
key := clientCacheKey{
|
||||
key := cache.Key{
|
||||
Name: store.GetObjectMeta().Name,
|
||||
Namespace: store.GetObjectMeta().Namespace,
|
||||
Kind: store.GetTypeMeta().Kind,
|
||||
}
|
||||
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 {
|
||||
return client, nil
|
||||
}
|
||||
|
@ -261,11 +244,8 @@ func getVaultClient(ctx context.Context, c *connector, store esv1beta1.GenericSt
|
|||
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
|
||||
}
|
||||
|
@ -306,7 +286,7 @@ func (c *connector) newClient(ctx context.Context, store esv1beta1.GenericStore,
|
|||
kube: kube,
|
||||
corev1: corev1,
|
||||
store: vaultSpec,
|
||||
log: ctrl.Log.WithName("provider").WithName("vault"),
|
||||
log: logger,
|
||||
namespace: namespace,
|
||||
storeKind: store.GetObjectKind().GroupVersionKind().Kind,
|
||||
}
|
||||
|
@ -316,7 +296,7 @@ func (c *connector) newClient(ctx context.Context, store esv1beta1.GenericStore,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
client, err := getVaultClient(ctx, c, store, cfg)
|
||||
client, err := getVaultClient(c, store, cfg)
|
||||
if err != nil {
|
||||
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 {
|
||||
// Revoke the token if we have one set, it wasn't sourced from a TokenSecretRef,
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1415,3 +1395,29 @@ func (v *client) requestTokenWithCertAuth(ctx context.Context, certAuth *esv1bet
|
|||
v.client.SetToken(token)
|
||||
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{},
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue