Quellcode durchsuchen

feat: add clientmanager compatibility package

Moritz Johner vor 1 Monat
Ursprung
Commit
4e17657c69
2 geänderte Dateien mit 810 neuen und 0 gelöschten Zeilen
  1. 334 0
      pkg/clientmanager/manager.go
  2. 476 0
      pkg/clientmanager/manager_test.go

+ 334 - 0
pkg/clientmanager/manager.go

@@ -0,0 +1,334 @@
+/*
+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 clientmanager provides a Manager for provider clients
+package clientmanager
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"regexp"
+	"strings"
+	"sync"
+
+	"github.com/go-logr/logr"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/types"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore/storeutil"
+	"github.com/external-secrets/external-secrets/providers/v2/common/grpc"
+)
+
+const (
+	errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
+	errGetSecretStore        = "could not get SecretStore %q, %w"
+	errClusterStoreMismatch  = "using cluster store %q is not allowed from namespace %q: denied by spec.condition"
+)
+
+var (
+	// globalV2ConnectionPool is a singleton connection pool for v2 gRPC providers.
+	// It persists across all reconciles and Manager instances to enable connection reuse.
+	// Initialized once on first use and shared globally.
+	globalV2ConnectionPool     *grpc.ConnectionPool
+	globalV2ConnectionPoolOnce sync.Once
+	globalV2ConnectionPoolLog  logr.Logger
+)
+
+// initGlobalV2ConnectionPool initializes the global connection pool for v2 providers.
+// This is called once on first use via sync.Once.
+func initGlobalV2ConnectionPool() {
+	globalV2ConnectionPoolLog = ctrl.Log.WithName("v2-connection-pool")
+	poolConfig := grpc.DefaultPoolConfig()
+	globalV2ConnectionPool = grpc.NewConnectionPool(poolConfig)
+	globalV2ConnectionPoolLog.Info("global v2 connection pool initialized",
+		"maxIdleTime", poolConfig.MaxIdleTime.String(),
+		"maxLifetime", poolConfig.MaxLifetime.String(),
+		"healthCheckInterval", poolConfig.HealthCheckInterval.String())
+}
+
+// getGlobalV2ConnectionPool returns the global connection pool, initializing it if needed.
+func getGlobalV2ConnectionPool() *grpc.ConnectionPool {
+	globalV2ConnectionPoolOnce.Do(initGlobalV2ConnectionPool)
+	return globalV2ConnectionPool
+}
+
+// v2PooledConnection tracks connection info needed to release connections back to the pool.
+type v2PooledConnection struct {
+	address   string
+	tlsConfig *grpc.TLSConfig
+}
+
+// Manager stores instances of provider clients
+// At any given time we must have no more than one instance
+// of a client (due to limitations in GCP / see mutexlock there)
+// If the controller requests another instance of a given client
+// we will close the old client first and then construct a new one.
+type Manager struct {
+	log             logr.Logger
+	client          client.Client
+	controllerClass string
+	enableFloodgate bool
+
+	// store clients by provider type
+	clientMap map[clientKey]*clientVal
+
+	// Track v2 provider connections for release back to pool
+	v2PooledConnections []v2PooledConnection
+}
+
+type clientKey struct {
+	providerType string
+}
+
+type clientVal struct {
+	client esv1.SecretsClient
+	store  esv1.GenericStore
+}
+
+// NewManager constructs a new manager with defaults.
+func NewManager(ctrlClient client.Client, controllerClass string, enableFloodgate bool) *Manager {
+	log := ctrl.Log.WithName("clientmanager")
+	return &Manager{
+		log:             log,
+		client:          ctrlClient,
+		controllerClass: controllerClass,
+		enableFloodgate: enableFloodgate,
+		clientMap:       make(map[clientKey]*clientVal),
+	}
+}
+
+// GetFromStore returns a provider client from the given store.
+// Do not close the client returned from this func, instead close
+// the manager once you're done with reconciling the external secret.
+func (m *Manager) GetFromStore(ctx context.Context, store esv1.GenericStore, namespace string) (esv1.SecretsClient, error) {
+	storeProvider, err := esv1.GetProvider(store)
+	if err != nil {
+		return nil, err
+	}
+	secretClient := m.getStoredClient(ctx, storeProvider, store)
+	if secretClient != nil {
+		return secretClient, nil
+	}
+	m.log.V(1).Info("creating new client",
+		"provider", fmt.Sprintf("%T", storeProvider),
+		"store", fmt.Sprintf("%s/%s", store.GetNamespace(), store.GetName()))
+	// secret client is created only if we are going to refresh
+	// this skip an unnecessary check/request in the case we are not going to do anything
+	secretClient, err = storeProvider.NewClient(ctx, store, m.client, namespace)
+	if err != nil {
+		return nil, err
+	}
+	idx := storeKey(storeProvider)
+	m.clientMap[idx] = &clientVal{
+		client: secretClient,
+		store:  store,
+	}
+	return secretClient, nil
+}
+
+// Get returns a provider client from the given storeRef or sourceRef.secretStoreRef
+// while sourceRef.SecretStoreRef takes precedence over storeRef.
+// Do not close the client returned from this func, instead close
+// the manager once you're done with recinciling the external secret.
+func (m *Manager) Get(ctx context.Context, storeRef esv1.SecretStoreRef, namespace string, sourceRef *esv1.StoreGeneratorSourceRef) (esv1.SecretsClient, error) {
+	if sourceRef != nil && sourceRef.SecretStoreRef != nil {
+		storeRef = *sourceRef.SecretStoreRef
+	}
+	store, err := m.getStore(ctx, &storeRef, namespace)
+	if err != nil {
+		return nil, err
+	}
+	// check if store should be handled by this controller instance
+	if !storeutil.ShouldProcessStore(store, m.controllerClass) {
+		return nil, errors.New("can not reference unmanaged store")
+	}
+	// when using ClusterSecretStore, validate the ClusterSecretStore namespace conditions
+	shouldProcess, err := m.shouldProcessSecret(store, namespace)
+	if err != nil {
+		return nil, err
+	}
+	if !shouldProcess {
+		return nil, fmt.Errorf(errClusterStoreMismatch, store.GetName(), namespace)
+	}
+
+	if m.enableFloodgate {
+		err := storeutil.AssertStoreIsUsable(store)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return m.GetFromStore(ctx, store, namespace)
+}
+
+// returns a previously stored client from the cache if store and store-version match
+// if a client exists for the same provider which points to a different store or store version
+// it will be cleaned up.
+func (m *Manager) getStoredClient(ctx context.Context, storeProvider esv1.ProviderInterface, store esv1.GenericStore) esv1.SecretsClient {
+	idx := storeKey(storeProvider)
+	val, ok := m.clientMap[idx]
+	if !ok {
+		return nil
+	}
+	valGVK, err := m.client.GroupVersionKindFor(val.store)
+	if err != nil {
+		return nil
+	}
+	storeGVK, err := m.client.GroupVersionKindFor(store)
+	if err != nil {
+		return nil
+	}
+	storeName := fmt.Sprintf("%s/%s", store.GetNamespace(), store.GetName())
+	// return client if it points to the very same store
+	if val.store.GetObjectMeta().Generation == store.GetGeneration() &&
+		valGVK == storeGVK &&
+		val.store.GetName() == store.GetName() &&
+		val.store.GetNamespace() == store.GetNamespace() {
+		m.log.V(1).Info("reusing stored client",
+			"provider", fmt.Sprintf("%T", storeProvider),
+			"store", storeName)
+		return val.client
+	}
+	m.log.V(1).Info("cleaning up client",
+		"provider", fmt.Sprintf("%T", storeProvider),
+		"store", storeName)
+	// if we have a client, but it points to a different store
+	// we must clean it up
+	_ = val.client.Close(ctx)
+	delete(m.clientMap, idx)
+	return nil
+}
+
+func storeKey(storeProvider esv1.ProviderInterface) clientKey {
+	return clientKey{
+		providerType: fmt.Sprintf("%T", storeProvider),
+	}
+}
+
+// getStore fetches the (Cluster)SecretStore from the kube-apiserver
+// and returns a GenericStore representing it.
+func (m *Manager) getStore(ctx context.Context, storeRef *esv1.SecretStoreRef, namespace string) (esv1.GenericStore, error) {
+	ref := types.NamespacedName{
+		Name: storeRef.Name,
+	}
+	if storeRef.Kind == esv1.ClusterSecretStoreKind {
+		var store esv1.ClusterSecretStore
+		err := m.client.Get(ctx, ref, &store)
+		if err != nil {
+			return nil, fmt.Errorf(errGetClusterSecretStore, ref.Name, err)
+		}
+		return &store, nil
+	}
+	ref.Namespace = namespace
+	var store esv1.SecretStore
+	err := m.client.Get(ctx, ref, &store)
+	if err != nil {
+		return nil, fmt.Errorf(errGetSecretStore, ref.Name, err)
+	}
+	return &store, nil
+}
+
+// Close cleans up all clients.
+// For v1 providers, it closes the clients directly.
+// For v2 providers, it releases connections back to the pool for reuse.
+func (m *Manager) Close(ctx context.Context) error {
+	var errs []string
+
+	// Release v2 pooled connections back to the pool
+	pool := getGlobalV2ConnectionPool()
+	for _, pooledConn := range m.v2PooledConnections {
+		pool.Release(pooledConn.address, pooledConn.tlsConfig)
+		m.log.V(1).Info("released v2 connection back to pool",
+			"address", pooledConn.address)
+	}
+	m.v2PooledConnections = nil
+
+	// Close v1 provider clients (they don't use the pool)
+	for key, val := range m.clientMap {
+		// Only close v1 clients; v2 clients are managed by the pool
+		if key.providerType != "v2-provider" && key.providerType != "v2-cluster-provider" {
+			err := val.client.Close(ctx)
+			if err != nil {
+				errs = append(errs, err.Error())
+			}
+		}
+		delete(m.clientMap, key)
+	}
+
+	if len(errs) != 0 {
+		return fmt.Errorf("errors while closing clients: %s", strings.Join(errs, ", "))
+	}
+	return nil
+}
+
+func (m *Manager) shouldProcessSecret(store esv1.GenericStore, ns string) (bool, error) {
+	if store.GetKind() != esv1.ClusterSecretStoreKind {
+		return true, nil
+	}
+
+	if len(store.GetSpec().Conditions) == 0 {
+		return true, nil
+	}
+
+	namespace := v1.Namespace{}
+	if err := m.client.Get(context.Background(), client.ObjectKey{Name: ns}, &namespace); err != nil {
+		return false, fmt.Errorf("failed to get a namespace %q: %w", ns, err)
+	}
+
+	nsLabels := labels.Set(namespace.GetLabels())
+	for _, condition := range store.GetSpec().Conditions {
+		var labelSelectors []*metav1.LabelSelector
+		if condition.NamespaceSelector != nil {
+			labelSelectors = append(labelSelectors, condition.NamespaceSelector)
+		}
+		for _, n := range condition.Namespaces {
+			labelSelectors = append(labelSelectors, &metav1.LabelSelector{
+				MatchLabels: map[string]string{
+					"kubernetes.io/metadata.name": n,
+				},
+			})
+		}
+
+		for _, ls := range labelSelectors {
+			selector, err := metav1.LabelSelectorAsSelector(ls)
+			if err != nil {
+				return false, fmt.Errorf("failed to convert label selector into selector %v: %w", ls, err)
+			}
+			if selector.Matches(nsLabels) {
+				return true, nil
+			}
+		}
+
+		for _, reg := range condition.NamespaceRegexes {
+			match, err := regexp.MatchString(reg, ns)
+			if err != nil {
+				// Should not happen since store validation already verified the regexes.
+				return false, fmt.Errorf("failed to compile regex %v: %w", reg, err)
+			}
+
+			if match {
+				return true, nil
+			}
+		}
+	}
+
+	return false, nil
+}

+ 476 - 0
pkg/clientmanager/manager_test.go

@@ -0,0 +1,476 @@
+/*
+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 clientmanager
+
+import (
+	"context"
+	"testing"
+
+	"github.com/go-logr/logr"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func TestManagerGet(t *testing.T) {
+	scheme := runtime.NewScheme()
+
+	// add kubernetes schemes
+	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
+	utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
+
+	// add external-secrets schemes
+	utilruntime.Must(esv1.AddToScheme(scheme))
+
+	// We have a test provider to control
+	// the behavior of the NewClient func.
+	fakeProvider := &WrapProvider{}
+	esv1.ForceRegister(fakeProvider, &esv1.SecretStoreProvider{
+		AWS: &esv1.AWSProvider{},
+	}, esv1.MaintenanceStatusMaintained)
+
+	// fake clients are re-used to compare the
+	// in-memory reference
+	clientA := &MockFakeClient{id: "1"}
+	clientB := &MockFakeClient{id: "2"}
+
+	const testNamespace = "foo"
+
+	readyStatus := esv1.SecretStoreStatus{
+		Conditions: []esv1.SecretStoreStatusCondition{
+			{
+				Type:   esv1.SecretStoreReady,
+				Status: corev1.ConditionTrue,
+			},
+		},
+	}
+
+	fakeSpec := esv1.SecretStoreSpec{
+		Provider: &esv1.SecretStoreProvider{
+			AWS: &esv1.AWSProvider{},
+		},
+	}
+
+	defaultStore := &esv1.SecretStore{
+		TypeMeta: metav1.TypeMeta{Kind: esv1.SecretStoreKind},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "foo",
+			Namespace: testNamespace,
+		},
+		Spec:   fakeSpec,
+		Status: readyStatus,
+	}
+
+	otherStore := &esv1.SecretStore{
+		TypeMeta: metav1.TypeMeta{Kind: esv1.SecretStoreKind},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "other",
+			Namespace: testNamespace,
+		},
+		Spec:   fakeSpec,
+		Status: readyStatus,
+	}
+
+	var mgr *Manager
+
+	provKey := storeKey(fakeProvider)
+
+	type fields struct {
+		client    client.Client
+		clientMap map[clientKey]*clientVal
+	}
+	type args struct {
+		storeRef  esv1.SecretStoreRef
+		namespace string
+		sourceRef *esv1.StoreGeneratorSourceRef
+	}
+	tests := []struct {
+		name              string
+		fields            fields
+		args              args
+		clientConstructor func(
+			ctx context.Context,
+			store esv1.GenericStore,
+			kube client.Client,
+			namespace string) (esv1.SecretsClient, error)
+		verify     func(esv1.SecretsClient)
+		afterClose func()
+		want       esv1.SecretsClient
+		wantErr    bool
+	}{
+		{
+			name:    "creates a new client from storeRef and stores it",
+			wantErr: false,
+			fields: fields{
+				client: fakeclient.NewClientBuilder().
+					WithScheme(scheme).
+					WithObjects(defaultStore).
+					Build(),
+				clientMap: make(map[clientKey]*clientVal),
+			},
+			args: args{
+				storeRef: esv1.SecretStoreRef{
+					Name: defaultStore.Name,
+					Kind: esv1.SecretStoreKind,
+				},
+				namespace: defaultStore.Namespace,
+				sourceRef: nil,
+			},
+			clientConstructor: func(_ context.Context, _ esv1.GenericStore, _ client.Client, _ string) (esv1.SecretsClient, error) {
+				return clientA, nil
+			},
+			verify: func(sc esv1.SecretsClient) {
+				// we now must have this provider in the clientMap
+				// and it mustbe the client defined in clientConstructor
+				assert.NotNil(t, sc)
+				c, ok := mgr.clientMap[provKey]
+				require.True(t, ok)
+				assert.Same(t, c.client, clientA)
+			},
+
+			afterClose: func() {
+				v, ok := mgr.clientMap[provKey]
+				assert.False(t, ok)
+				assert.Nil(t, v)
+			},
+		},
+		{
+			name:    "creates a new client using both storeRef and sourceRef",
+			wantErr: false,
+			fields: fields{
+				client: fakeclient.NewClientBuilder().
+					WithScheme(scheme).
+					WithObjects(otherStore).
+					Build(),
+				clientMap: make(map[clientKey]*clientVal),
+			},
+			args: args{
+				storeRef: esv1.SecretStoreRef{
+					Name: defaultStore.Name,
+					Kind: esv1.SecretStoreKind,
+				},
+				// this should take precedence
+				sourceRef: &esv1.StoreGeneratorSourceRef{
+					SecretStoreRef: &esv1.SecretStoreRef{
+						Name: otherStore.Name,
+						Kind: esv1.SecretStoreKind,
+					},
+				},
+				namespace: defaultStore.Namespace,
+			},
+			clientConstructor: func(_ context.Context, _ esv1.GenericStore, _ client.Client, _ string) (esv1.SecretsClient, error) {
+				return clientB, nil
+			},
+			verify: func(sc esv1.SecretsClient) {
+				// we now must have this provider in the clientMap
+				// and it mustbe the client defined in clientConstructor
+				assert.NotNil(t, sc)
+				c, ok := mgr.clientMap[provKey]
+				assert.True(t, ok)
+				assert.Same(t, c.client, clientB)
+			},
+
+			afterClose: func() {
+				v, ok := mgr.clientMap[provKey]
+				assert.False(t, ok)
+				assert.True(t, clientB.closeCalled)
+				assert.Nil(t, v)
+			},
+		},
+		{
+			name:    "retrieve cached client when store matches",
+			wantErr: false,
+			fields: fields{
+				client: fakeclient.NewClientBuilder().
+					WithScheme(scheme).
+					WithObjects(defaultStore).
+					Build(),
+				clientMap: map[clientKey]*clientVal{
+					provKey: {
+						client: clientA,
+						store:  defaultStore,
+					},
+				},
+			},
+			args: args{
+				storeRef: esv1.SecretStoreRef{
+					Name: defaultStore.Name,
+					Kind: esv1.SecretStoreKind,
+				},
+				namespace: defaultStore.Namespace,
+				sourceRef: nil,
+			},
+			clientConstructor: func(_ context.Context, _ esv1.GenericStore, _ client.Client, _ string) (esv1.SecretsClient, error) {
+				// constructor should not be called,
+				// the client from the cache should be returned instead
+				t.Fail()
+				return nil, nil
+			},
+			verify: func(sc esv1.SecretsClient) {
+				// verify that the secretsClient is the one from cache
+				assert.NotNil(t, sc)
+				c, ok := mgr.clientMap[provKey]
+				assert.True(t, ok)
+				assert.Same(t, c.client, clientA)
+				assert.Same(t, sc, clientA)
+			},
+
+			afterClose: func() {
+				v, ok := mgr.clientMap[provKey]
+				assert.False(t, ok)
+				assert.True(t, clientA.closeCalled)
+				assert.Nil(t, v)
+			},
+		},
+		{
+			name:    "create new client when store doesn't match",
+			wantErr: false,
+			fields: fields{
+				client: fakeclient.NewClientBuilder().
+					WithScheme(scheme).
+					WithObjects(otherStore).
+					Build(),
+				clientMap: map[clientKey]*clientVal{
+					provKey: {
+						// we have clientA in cache pointing at defaultStore
+						client: clientA,
+						store:  defaultStore,
+					},
+				},
+			},
+			args: args{
+				storeRef: esv1.SecretStoreRef{
+					Name: otherStore.Name,
+					Kind: esv1.SecretStoreKind,
+				},
+				namespace: otherStore.Namespace,
+				sourceRef: nil,
+			},
+			clientConstructor: func(_ context.Context, _ esv1.GenericStore, _ client.Client, _ string) (esv1.SecretsClient, error) {
+				// because there is a store mismatch
+				// we create a new client
+				return clientB, nil
+			},
+			verify: func(sc esv1.SecretsClient) {
+				// verify that SecretsClient is NOT the one from cache
+				assert.NotNil(t, sc)
+				c, ok := mgr.clientMap[provKey]
+				assert.True(t, ok)
+				assert.Same(t, c.client, clientB)
+				assert.Same(t, sc, clientB)
+				assert.True(t, clientA.closeCalled)
+			},
+			afterClose: func() {
+				v, ok := mgr.clientMap[provKey]
+				assert.False(t, ok)
+				assert.True(t, clientB.closeCalled)
+				assert.Nil(t, v)
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mgr = &Manager{
+				log:             logr.Discard(),
+				client:          tt.fields.client,
+				enableFloodgate: true,
+				clientMap:       tt.fields.clientMap,
+			}
+			fakeProvider.newClientFunc = tt.clientConstructor
+			clientA.closeCalled = false
+			clientB.closeCalled = false
+			got, err := mgr.Get(context.Background(), tt.args.storeRef, tt.args.namespace, tt.args.sourceRef)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Manager.Get() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			tt.verify(got)
+			mgr.Close(context.Background())
+			tt.afterClose()
+		})
+	}
+}
+
+func TestShouldProcessSecret(t *testing.T) {
+	scheme := runtime.NewScheme()
+
+	// add kubernetes schemes
+	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
+	utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
+
+	// add external-secrets schemes
+	utilruntime.Must(esv1.AddToScheme(scheme))
+
+	testNamespace := "test-a"
+	testCases := []struct {
+		name       string
+		conditions []esv1.ClusterSecretStoreCondition
+		namespace  *corev1.Namespace
+		wantErr    string
+		want       bool
+	}{
+		{
+			name: "processes a regex condition",
+			conditions: []esv1.ClusterSecretStoreCondition{
+				{
+					NamespaceRegexes: []string{`test-*`},
+				},
+			},
+			namespace: &corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testNamespace,
+				},
+			},
+			want: true,
+		},
+		{
+			name: "process multiple regexes",
+			conditions: []esv1.ClusterSecretStoreCondition{
+				{
+					NamespaceRegexes: []string{`nope`, `test-*`},
+				},
+			},
+			namespace: &corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testNamespace,
+				},
+			},
+			want: true,
+		},
+		{
+			name: "shouldn't process if nothing matches",
+			conditions: []esv1.ClusterSecretStoreCondition{
+				{
+					NamespaceRegexes: []string{`nope`},
+				},
+			},
+			namespace: &corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testNamespace,
+				},
+			},
+			want: false,
+		},
+	}
+
+	for _, tt := range testCases {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeSpec := esv1.SecretStoreSpec{
+				Conditions: tt.conditions,
+			}
+
+			defaultStore := &esv1.ClusterSecretStore{
+				TypeMeta: metav1.TypeMeta{Kind: esv1.ClusterSecretStoreKind},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "foo",
+					Namespace: tt.namespace.Name,
+				},
+				Spec: fakeSpec,
+			}
+
+			client := fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(defaultStore, tt.namespace).Build()
+			clientMap := make(map[clientKey]*clientVal)
+			mgr := &Manager{
+				log:             logr.Discard(),
+				client:          client,
+				enableFloodgate: true,
+				clientMap:       clientMap,
+			}
+
+			got, err := mgr.shouldProcessSecret(defaultStore, tt.namespace.Name)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+type WrapProvider struct {
+	newClientFunc func(
+		context.Context,
+		esv1.GenericStore,
+		client.Client,
+		string) (esv1.SecretsClient, error)
+}
+
+// NewClient constructs a SecretsManager Provider.
+func (f *WrapProvider) NewClient(
+	ctx context.Context,
+	store esv1.GenericStore,
+	kube client.Client,
+	namespace string) (esv1.SecretsClient, error) {
+	return f.newClientFunc(ctx, store, kube, namespace)
+}
+
+func (f *WrapProvider) Capabilities() esv1.SecretStoreCapabilities {
+	return esv1.SecretStoreReadOnly
+}
+
+// ValidateStore checks if the provided store is valid.
+func (f *WrapProvider) ValidateStore(_ esv1.GenericStore) (admission.Warnings, error) {
+	return nil, nil
+}
+
+type MockFakeClient struct {
+	id          string
+	closeCalled bool
+}
+
+func (c *MockFakeClient) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1.PushSecretData) error {
+	return nil
+}
+
+func (c *MockFakeClient) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
+	return nil
+}
+
+func (c *MockFakeClient) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
+	return false, nil
+}
+
+func (c *MockFakeClient) GetSecret(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	return nil, nil
+}
+
+func (c *MockFakeClient) Validate() (esv1.ValidationResult, error) {
+	return esv1.ValidationResultReady, nil
+}
+
+// GetSecretMap returns multiple k/v pairs from the provider.
+func (c *MockFakeClient) GetSecretMap(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	return nil, nil
+}
+
+// GetAllSecrets returns multiple k/v pairs from the provider.
+func (c *MockFakeClient) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
+	return nil, nil
+}
+
+func (c *MockFakeClient) Close(_ context.Context) error {
+	c.closeCalled = true
+	return nil
+}