Przeglądaj źródła

fix: preserve v2 push store kinds when kind is omitted

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 2 miesięcy temu
rodzic
commit
55867bd514

+ 21 - 8
pkg/controllers/pushsecret/pushsecret_controller.go

@@ -1117,10 +1117,7 @@ func storeRefExistsInList(ref *esapi.PushSecretStoreRef, storeRefs []esapi.PushS
 
 func resolvedStoreInfo(ref esapi.PushSecretStoreRef, store interface{}) (storeInfo, bool) {
 	if genericStore, ok := store.(esv1.GenericStore); ok {
-		kind := ref.Kind
-		if kind == "" {
-			kind = genericStore.GetKind()
-		}
+		kind := resolvedPushStoreKind(ref.Kind, genericStore)
 		return storeInfo{
 			Name:   genericStore.GetName(),
 			Kind:   kind,
@@ -1129,10 +1126,7 @@ func resolvedStoreInfo(ref esapi.PushSecretStoreRef, store interface{}) (storeIn
 	}
 
 	if obj, ok := store.(client.Object); ok {
-		kind := ref.Kind
-		if kind == "" {
-			kind = esv1.SecretStoreKind
-		}
+		kind := resolvedPushStoreKind(ref.Kind, obj)
 		return storeInfo{
 			Name:   obj.GetName(),
 			Kind:   kind,
@@ -1143,6 +1137,25 @@ func resolvedStoreInfo(ref esapi.PushSecretStoreRef, store interface{}) (storeIn
 	return storeInfo{}, false
 }
 
+func resolvedPushStoreKind(refKind string, store interface{}) string {
+	if refKind != "" {
+		return refKind
+	}
+
+	if genericStore, ok := store.(esv1.GenericStore); ok {
+		return genericStore.GetKind()
+	}
+
+	switch store.(type) {
+	case *esv1.Provider:
+		return esv1.ProviderKindStr
+	case *esv1.ClusterProvider:
+		return esv1.ClusterProviderKindStr
+	default:
+		return esv1.SecretStoreKind
+	}
+}
+
 // validateDataToMatchesResolvedStores checks that every dataTo entry with a
 // labelSelector actually matches at least one resolved store. Without this,
 // a misconfigured labelSelector silently becomes a no-op.

+ 111 - 53
pkg/controllers/pushsecret/pushsecret_controller_v2.go

@@ -20,38 +20,20 @@ import (
 	"strings"
 
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	"github.com/external-secrets/external-secrets/runtime/clientmanager"
 )
 
 // isV2SecretStore checks if the referenced SecretStore is a v2 API version.
 func (r *Reconciler) isV2SecretStore(ctx context.Context, storeRef esv1alpha1.PushSecretStoreRef, namespace string) bool {
-	if storeRef.Kind == esapi.ProviderKindStr || storeRef.Kind == esapi.ClusterProviderKindStr {
-		if storeRef.APIVersion == "" {
-			return true
-		}
-		return storeRef.APIVersion == esapi.SchemeGroupVersion.String()
-	}
-
-	// Check the apiVersion field first if specified
-	if storeRef.APIVersion != "" {
-		return false
-	}
-
-	if storeRef.Kind == esapi.ClusterProviderKindStr {
-		var store esapi.ClusterProvider
-		err := r.Client.Get(ctx, types.NamespacedName{Name: storeRef.Name}, &store)
-		return err == nil
-	}
-
-	// For backwards compatibility, try to fetch as namespaced v2 Provider.
-	var store esapi.Provider
-	storeKey := types.NamespacedName{Name: storeRef.Name, Namespace: namespace}
-	err := r.Client.Get(ctx, storeKey, &store)
-	return err == nil
+	_, ok, err := r.resolveV2Store(ctx, storeRef, namespace)
+	return err == nil && ok
 }
 
 // GetSecretStoresV2 retrieves both v1 and v2 Providers.
@@ -59,24 +41,21 @@ func (r *Reconciler) GetSecretStoresV2(ctx context.Context, ps esv1alpha1.PushSe
 	stores := make(map[esv1alpha1.PushSecretStoreRef]interface{})
 
 	for _, refStore := range ps.Spec.SecretStoreRefs {
-		// Check if this is a v2 Provider
-		if r.isV2SecretStore(ctx, refStore, ps.Namespace) {
-			if refStore.Kind == esapi.ClusterProviderKindStr {
-				var store esapi.ClusterProvider
-				storeKey := types.NamespacedName{Name: refStore.Name}
-				if err := r.Client.Get(ctx, storeKey, &store); err != nil {
-					return nil, fmt.Errorf("failed to get v2 ClusterProvider %s: %w", refStore.Name, err)
-				}
-				stores[refStore] = &store
-				continue
+		if refStore.LabelSelector != nil {
+			resolvedStores, err := r.getSecretStoresFromSelectorV2(ctx, refStore, ps.Namespace)
+			if err != nil {
+				return nil, err
 			}
-
-			var store esapi.Provider
-			storeKey := types.NamespacedName{Name: refStore.Name, Namespace: ps.Namespace}
-			if err := r.Client.Get(ctx, storeKey, &store); err != nil {
-				return nil, fmt.Errorf("failed to get v2 Provider %s: %w", refStore.Name, err)
+			for resolvedRef, store := range resolvedStores {
+				stores[resolvedRef] = store
 			}
-			stores[refStore] = &store
+			continue
+		}
+
+		if store, ok, err := r.resolveV2Store(ctx, refStore, ps.Namespace); err != nil {
+			return nil, err
+		} else if ok {
+			stores[refStore] = store
 			continue
 		} else {
 			// Get v1 SecretStore (existing implementation)
@@ -91,6 +70,99 @@ func (r *Reconciler) GetSecretStoresV2(ctx context.Context, ps esv1alpha1.PushSe
 	return stores, nil
 }
 
+func (r *Reconciler) getSecretStoresFromSelectorV2(ctx context.Context, storeRef esv1alpha1.PushSecretStoreRef, namespace string) (map[esv1alpha1.PushSecretStoreRef]interface{}, error) {
+	selector, err := metav1.LabelSelectorAsSelector(storeRef.LabelSelector)
+	if err != nil {
+		return nil, fmt.Errorf("could not convert labels: %w", err)
+	}
+
+	listOptions := &client.ListOptions{LabelSelector: selector}
+	stores := make(map[esv1alpha1.PushSecretStoreRef]interface{})
+
+	switch storeRef.Kind {
+	case esapi.ProviderKindStr:
+		listOptions.Namespace = namespace
+		var providerList esv1.ProviderList
+		if err := r.List(ctx, &providerList, listOptions); err != nil {
+			return nil, fmt.Errorf("could not list Providers: %w", err)
+		}
+		for i := range providerList.Items {
+			store := &providerList.Items[i]
+			stores[esv1alpha1.PushSecretStoreRef{Name: store.Name, Kind: esapi.ProviderKindStr}] = store
+		}
+	case esapi.ClusterProviderKindStr:
+		var providerList esv1.ClusterProviderList
+		if err := r.List(ctx, &providerList, listOptions); err != nil {
+			return nil, fmt.Errorf("could not list ClusterProviders: %w", err)
+		}
+		for i := range providerList.Items {
+			store := &providerList.Items[i]
+			stores[esv1alpha1.PushSecretStoreRef{Name: store.Name, Kind: esapi.ClusterProviderKindStr}] = store
+		}
+	case esv1.ClusterSecretStoreKind:
+		var storeList esv1.ClusterSecretStoreList
+		if err := r.List(ctx, &storeList, listOptions); err != nil {
+			return nil, fmt.Errorf("could not list cluster Secret Stores: %w", err)
+		}
+		for i := range storeList.Items {
+			store := &storeList.Items[i]
+			stores[esv1alpha1.PushSecretStoreRef{Name: store.Name, Kind: esv1.ClusterSecretStoreKind}] = store
+		}
+	default:
+		listOptions.Namespace = namespace
+		var storeList esv1.SecretStoreList
+		if err := r.List(ctx, &storeList, listOptions); err != nil {
+			return nil, fmt.Errorf("could not list Secret Stores: %w", err)
+		}
+		for i := range storeList.Items {
+			store := &storeList.Items[i]
+			stores[esv1alpha1.PushSecretStoreRef{Name: store.Name, Kind: esv1.SecretStoreKind}] = store
+		}
+	}
+
+	return stores, nil
+}
+
+func (r *Reconciler) resolveV2Store(ctx context.Context, storeRef esv1alpha1.PushSecretStoreRef, namespace string) (interface{}, bool, error) {
+	if storeRef.APIVersion != "" && storeRef.APIVersion != esapi.SchemeGroupVersion.String() {
+		return nil, false, nil
+	}
+	if storeRef.Name == "" {
+		return nil, false, nil
+	}
+
+	switch storeRef.Kind {
+	case esapi.ClusterProviderKindStr:
+		var store esapi.ClusterProvider
+		storeKey := types.NamespacedName{Name: storeRef.Name}
+		if err := r.Client.Get(ctx, storeKey, &store); err != nil {
+			return nil, true, fmt.Errorf("failed to get v2 ClusterProvider %s: %w", storeRef.Name, err)
+		}
+		return &store, true, nil
+	case esapi.ProviderKindStr:
+		var store esapi.Provider
+		storeKey := types.NamespacedName{Name: storeRef.Name, Namespace: namespace}
+		if err := r.Client.Get(ctx, storeKey, &store); err != nil {
+			return nil, true, fmt.Errorf("failed to get v2 Provider %s: %w", storeRef.Name, err)
+		}
+		return &store, true, nil
+	case "":
+		var provider esapi.Provider
+		providerKey := types.NamespacedName{Name: storeRef.Name, Namespace: namespace}
+		if err := r.Client.Get(ctx, providerKey, &provider); err == nil {
+			return &provider, true, nil
+		}
+
+		var clusterProvider esapi.ClusterProvider
+		clusterProviderKey := types.NamespacedName{Name: storeRef.Name}
+		if err := r.Client.Get(ctx, clusterProviderKey, &clusterProvider); err == nil {
+			return &clusterProvider, true, nil
+		}
+	}
+
+	return nil, false, nil
+}
+
 // PushSecretToProvidersV2 pushes secret data to both v1 stores and v2 providers.
 func (r *Reconciler) PushSecretToProvidersV2(
 	ctx context.Context,
@@ -132,20 +204,6 @@ func (r *Reconciler) DeleteSecretFromProvidersV2(ctx context.Context, ps *esv1al
 		storeKind := parts[0]
 		storeNameOnly := parts[1]
 
-		// Find the matching store
-		var found bool
-		for ref := range stores {
-			if ref.Kind == storeKind && ref.Name == storeNameOnly {
-				found = true
-				break
-			}
-		}
-
-		if !found {
-			// Store no longer referenced, skip deletion
-			continue
-		}
-
 		secretClient, err := mgr.Get(ctx, esapi.SecretStoreRef{
 			Name: storeNameOnly,
 			Kind: storeKind,

+ 398 - 0
pkg/controllers/pushsecret/pushsecret_controller_v2_test.go

@@ -100,6 +100,38 @@ func TestResolvedStoreInfoSupportsProviderKinds(t *testing.T) {
 	}
 }
 
+func TestResolvedStoreInfoInfersOmittedProviderKinds(t *testing.T) {
+	providerInfo, ok := resolvedStoreInfo(esapi.PushSecretStoreRef{
+		Name: "provider",
+	}, &esv1.Provider{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:   "provider",
+			Labels: map[string]string{"team": "a"},
+		},
+	})
+	if !ok {
+		t.Fatal("expected provider store info to resolve")
+	}
+	if providerInfo.Kind != esv1.ProviderKindStr {
+		t.Fatalf("expected kind %q, got %#v", esv1.ProviderKindStr, providerInfo)
+	}
+
+	clusterProviderInfo, ok := resolvedStoreInfo(esapi.PushSecretStoreRef{
+		Name: "cluster-provider",
+	}, &esv1.ClusterProvider{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:   "cluster-provider",
+			Labels: map[string]string{"scope": "cluster"},
+		},
+	})
+	if !ok {
+		t.Fatal("expected cluster provider store info to resolve")
+	}
+	if clusterProviderInfo.Kind != esv1.ClusterProviderKindStr {
+		t.Fatalf("expected kind %q, got %#v", esv1.ClusterProviderKindStr, clusterProviderInfo)
+	}
+}
+
 func TestValidateDataToMatchesResolvedStoresSupportsProviderKinds(t *testing.T) {
 	err := validateDataToMatchesResolvedStores([]esapi.PushSecretDataTo{
 		{
@@ -238,6 +270,92 @@ func TestPushSecretToProvidersV2UsesProviderPath(t *testing.T) {
 	}
 }
 
+func TestPushSecretToProvidersV2UsesProviderPathWhenKindOmitted(t *testing.T) {
+	previous := clientmanager.V2ProvidersEnabled()
+	clientmanager.SetV2ProvidersEnabled(true)
+	t.Cleanup(func() {
+		clientmanager.SetV2ProvidersEnabled(previous)
+	})
+
+	scheme := newPushSecretTestScheme(t)
+	server, address, tlsSecret := newPushSecretProviderServer(t)
+
+	provider := &esv1.Provider{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "provider",
+			Namespace: "tenant-a",
+			Labels:    map[string]string{"team": "a"},
+		},
+		Spec: esv1.ProviderSpec{
+			Config: esv1.ProviderConfig{
+				Address: address,
+				ProviderRef: esv1.ProviderReference{
+					APIVersion: "provider.external-secrets.io/v2alpha1",
+					Kind:       "Kubernetes",
+					Name:       "backend",
+				},
+			},
+		},
+	}
+
+	kubeClient := fakeclient.NewClientBuilder().
+		WithScheme(scheme).
+		WithObjects(provider, &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "external-secrets-provider-tls",
+				Namespace: "tenant-a",
+			},
+			Data: tlsSecret,
+		}).
+		Build()
+
+	r := &Reconciler{Client: kubeClient, Log: logr.Discard()}
+	mgr := clientmanager.NewManager(kubeClient, "", false)
+	defer func() {
+		_ = mgr.Close(context.Background())
+	}()
+
+	ps := esapi.PushSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "pushsecret",
+			Namespace: "tenant-a",
+		},
+		Spec: esapi.PushSecretSpec{
+			SecretStoreRefs: []esapi.PushSecretStoreRef{{
+				Name: "provider",
+			}},
+			Data: []esapi.PushSecretData{{
+				Match: esapi.PushSecretMatch{
+					SecretKey: "token",
+					RemoteRef: esapi.PushSecretRemoteRef{
+						RemoteKey: "remote/path",
+						Property:  "property",
+					},
+				},
+				Metadata: &apiextensionsv1.JSON{Raw: []byte(`{"owner":"eso"}`)},
+			}},
+		},
+	}
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{"token": []byte("value")},
+	}
+
+	synced, err := r.PushSecretToProvidersV2(context.Background(), map[esapi.PushSecretStoreRef]interface{}{
+		{Name: "provider"}: provider,
+	}, ps, secret, mgr)
+	if err != nil {
+		t.Fatalf("PushSecretToProvidersV2() error = %v", err)
+	}
+
+	if server.pushRequest == nil {
+		t.Fatal("expected push request to be recorded")
+	}
+	if synced["Provider/provider"]["remote/path/property"].Match.SecretKey != "token" {
+		t.Fatalf("unexpected synced map: %#v", synced)
+	}
+}
+
 func TestPushSecretToProvidersV2UsesProviderNamespaceAuthScope(t *testing.T) {
 	previous := clientmanager.V2ProvidersEnabled()
 	clientmanager.SetV2ProvidersEnabled(true)
@@ -442,6 +560,286 @@ func TestDeleteSecretFromProvidersV2UsesClusterProviderPath(t *testing.T) {
 	}
 }
 
+func TestDeleteSecretFromProvidersV2UsesClusterProviderPathWhenKindOmitted(t *testing.T) {
+	previous := clientmanager.V2ProvidersEnabled()
+	clientmanager.SetV2ProvidersEnabled(true)
+	t.Cleanup(func() {
+		clientmanager.SetV2ProvidersEnabled(previous)
+	})
+
+	scheme := newPushSecretTestScheme(t)
+	server, address, tlsSecret := newPushSecretProviderServer(t)
+
+	clusterProvider := &esv1.ClusterProvider{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:   "cluster-provider",
+			Labels: map[string]string{"scope": "cluster"},
+		},
+		Spec: esv1.ClusterProviderSpec{
+			Config: esv1.ProviderConfig{
+				Address: address,
+				ProviderRef: esv1.ProviderReference{
+					APIVersion: "provider.external-secrets.io/v2alpha1",
+					Kind:       "Kubernetes",
+					Name:       "backend",
+				},
+			},
+			AuthenticationScope: esv1.AuthenticationScopeManifestNamespace,
+			Conditions: []esv1.ClusterSecretStoreCondition{{
+				Namespaces: []string{"tenant-a"},
+			}},
+		},
+	}
+
+	kubeClient := fakeclient.NewClientBuilder().
+		WithScheme(scheme).
+		WithObjects(
+			clusterProvider,
+			&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
+				Name: "tenant-a",
+				Labels: map[string]string{
+					"kubernetes.io/metadata.name": "tenant-a",
+				},
+			}},
+			&corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "external-secrets-provider-tls",
+					Namespace: "tenant-a",
+				},
+				Data: tlsSecret,
+			},
+		).
+		Build()
+
+	r := &Reconciler{Client: kubeClient, Log: logr.Discard()}
+	ps := &esapi.PushSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "pushsecret",
+			Namespace: "tenant-a",
+		},
+		Status: esapi.PushSecretStatus{
+			SyncedPushSecrets: esapi.SyncedPushSecretsMap{
+				"ClusterProvider/cluster-provider": {
+					"remote/path": {
+						Match: esapi.PushSecretMatch{
+							SecretKey: "token",
+							RemoteRef: esapi.PushSecretRemoteRef{
+								RemoteKey: "remote/path",
+								Property:  "property",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	result, err := r.DeleteSecretFromProvidersV2(context.Background(), ps, esapi.SyncedPushSecretsMap{}, map[esapi.PushSecretStoreRef]interface{}{
+		{Name: "cluster-provider"}: clusterProvider,
+	})
+	if err != nil {
+		t.Fatalf("DeleteSecretFromProvidersV2() error = %v", err)
+	}
+
+	if server.deleteRequest == nil {
+		t.Fatal("expected delete request to be recorded")
+	}
+	if _, ok := result["ClusterProvider/cluster-provider"]; ok {
+		t.Fatalf("expected synced state to be cleaned up, got %#v", result)
+	}
+}
+
+func TestGetSecretStoresV2ResolvesClusterProviderWhenKindOmitted(t *testing.T) {
+	previous := clientmanager.V2ProvidersEnabled()
+	clientmanager.SetV2ProvidersEnabled(true)
+	t.Cleanup(func() {
+		clientmanager.SetV2ProvidersEnabled(previous)
+	})
+
+	scheme := newPushSecretTestScheme(t)
+	clusterProvider := &esv1.ClusterProvider{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "cluster-provider",
+		},
+	}
+
+	kubeClient := fakeclient.NewClientBuilder().
+		WithScheme(scheme).
+		WithObjects(clusterProvider).
+		Build()
+
+	r := &Reconciler{Client: kubeClient, Log: logr.Discard()}
+	ps := esapi.PushSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "pushsecret",
+			Namespace: "tenant-a",
+		},
+		Spec: esapi.PushSecretSpec{
+			SecretStoreRefs: []esapi.PushSecretStoreRef{{
+				Name: "cluster-provider",
+			}},
+		},
+	}
+
+	stores, err := r.GetSecretStoresV2(context.Background(), ps)
+	if err != nil {
+		t.Fatalf("GetSecretStoresV2() error = %v", err)
+	}
+
+	store, ok := stores[esapi.PushSecretStoreRef{Name: "cluster-provider"}]
+	if !ok {
+		t.Fatalf("expected cluster provider store, got %#v", stores)
+	}
+	if _, ok := store.(*esv1.ClusterProvider); !ok {
+		t.Fatalf("expected ClusterProvider, got %T", store)
+	}
+}
+
+func TestGetSecretStoresV2SupportsSecretStoreLabelSelectors(t *testing.T) {
+	scheme := newPushSecretTestScheme(t)
+	selectedStore := &esv1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "selected",
+			Namespace: "tenant-a",
+			Labels:    map[string]string{"env": "test"},
+		},
+	}
+	otherNamespaceStore := &esv1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "other-namespace",
+			Namespace: "tenant-b",
+			Labels:    map[string]string{"env": "test"},
+		},
+	}
+
+	kubeClient := fakeclient.NewClientBuilder().
+		WithScheme(scheme).
+		WithObjects(selectedStore, otherNamespaceStore).
+		Build()
+
+	r := &Reconciler{Client: kubeClient, Log: logr.Discard()}
+	ps := esapi.PushSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "pushsecret",
+			Namespace: "tenant-a",
+		},
+		Spec: esapi.PushSecretSpec{
+			SecretStoreRefs: []esapi.PushSecretStoreRef{{
+				Kind: esv1.SecretStoreKind,
+				LabelSelector: &metav1.LabelSelector{
+					MatchLabels: map[string]string{"env": "test"},
+				},
+			}},
+		},
+	}
+
+	stores, err := r.GetSecretStoresV2(context.Background(), ps)
+	if err != nil {
+		t.Fatalf("GetSecretStoresV2() error = %v", err)
+	}
+
+	if len(stores) != 1 {
+		t.Fatalf("expected one resolved store, got %#v", stores)
+	}
+
+	store, ok := stores[esapi.PushSecretStoreRef{Name: "selected", Kind: esv1.SecretStoreKind}]
+	if !ok {
+		t.Fatalf("expected selected store, got %#v", stores)
+	}
+	if _, ok := store.(*esv1.SecretStore); !ok {
+		t.Fatalf("expected SecretStore, got %T", store)
+	}
+}
+
+func TestDeleteSecretFromProvidersV2DeletesRemovedStoreEvenWhenNoLongerReferenced(t *testing.T) {
+	previous := clientmanager.V2ProvidersEnabled()
+	clientmanager.SetV2ProvidersEnabled(true)
+	t.Cleanup(func() {
+		clientmanager.SetV2ProvidersEnabled(previous)
+	})
+
+	scheme := newPushSecretTestScheme(t)
+	server, address, tlsSecret := newPushSecretProviderServer(t)
+
+	clusterProvider := &esv1.ClusterProvider{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "cluster-provider",
+		},
+		Spec: esv1.ClusterProviderSpec{
+			Config: esv1.ProviderConfig{
+				Address: address,
+				ProviderRef: esv1.ProviderReference{
+					APIVersion: "provider.external-secrets.io/v2alpha1",
+					Kind:       "Kubernetes",
+					Name:       "backend",
+				},
+			},
+			AuthenticationScope: esv1.AuthenticationScopeManifestNamespace,
+			Conditions: []esv1.ClusterSecretStoreCondition{{
+				Namespaces: []string{"tenant-a"},
+			}},
+		},
+	}
+
+	kubeClient := fakeclient.NewClientBuilder().
+		WithScheme(scheme).
+		WithObjects(
+			clusterProvider,
+			&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
+				Name: "tenant-a",
+				Labels: map[string]string{
+					"kubernetes.io/metadata.name": "tenant-a",
+				},
+			}},
+			&corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "external-secrets-provider-tls",
+					Namespace: "tenant-a",
+				},
+				Data: tlsSecret,
+			},
+		).
+		Build()
+
+	r := &Reconciler{Client: kubeClient, Log: logr.Discard()}
+	ps := &esapi.PushSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "pushsecret",
+			Namespace: "tenant-a",
+		},
+		Status: esapi.PushSecretStatus{
+			SyncedPushSecrets: esapi.SyncedPushSecretsMap{
+				"ClusterProvider/cluster-provider": {
+					"remote/path": {
+						Match: esapi.PushSecretMatch{
+							SecretKey: "token",
+							RemoteRef: esapi.PushSecretRemoteRef{
+								RemoteKey: "remote/path",
+								Property:  "property",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	result, err := r.DeleteSecretFromProvidersV2(context.Background(), ps, esapi.SyncedPushSecretsMap{}, map[esapi.PushSecretStoreRef]interface{}{})
+	if err != nil {
+		t.Fatalf("DeleteSecretFromProvidersV2() error = %v", err)
+	}
+
+	if server.deleteRequest == nil {
+		t.Fatal("expected delete request to be recorded")
+	}
+	if server.deleteRequest.RemoteRef == nil || server.deleteRequest.RemoteRef.RemoteKey != "remote/path" {
+		t.Fatalf("unexpected delete ref: %#v", server.deleteRequest.RemoteRef)
+	}
+	if _, ok := result["ClusterProvider/cluster-provider"]; ok {
+		t.Fatalf("expected synced state to be cleaned up, got %#v", result)
+	}
+}
+
 func TestDeleteSecretFromProvidersV2UsesProviderNamespaceAuthScope(t *testing.T) {
 	previous := clientmanager.V2ProvidersEnabled()
 	clientmanager.SetV2ProvidersEnabled(true)