Просмотр исходного кода

test(e2e): refactor kubernetes provider cases into reusable tables

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 2 месяцев назад
Родитель
Сommit
d4fc4b9822

+ 40 - 11
e2e/framework/testcase.go

@@ -41,6 +41,8 @@ type TestCase struct {
 	AdditionalObjects       []client.Object
 	Secrets                 map[string]SecretEntry
 	ExpectedSecret          *v1.Secret
+	Prepare                 func(*TestCase, SecretStoreProvider)
+	ProviderOverride        SecretStoreProvider
 	AfterSync               func(SecretStoreProvider, *v1.Secret)
 	VerifyPushSecretOutcome func(ps *esv1alpha1.PushSecret, pushClient esv1.SecretsClient)
 }
@@ -67,6 +69,8 @@ func TableFuncWithExternalSecret(f *Framework, prov SecretStoreProvider) func(..
 			tweak(tc)
 		}
 
+		prov = prepareTestCase(tc, prov)
+
 		// create secrets & defer delete
 		var deferRemoveKeys []string
 		for k, v := range tc.Secrets {
@@ -81,16 +85,11 @@ func TableFuncWithExternalSecret(f *Framework, prov SecretStoreProvider) func(..
 			}
 		}()
 
-		// create v1alpha1 external secret, if provided
-		createProvidedExternalSecret(tc)
-
 		// create additional objects
 		generateAdditionalObjects(tc)
 
-		// in case target name is empty
-		if tc.ExternalSecret != nil && tc.ExternalSecret.Spec.Target.Name == "" {
-			TargetSecretName = tc.ExternalSecret.ObjectMeta.Name
-		}
+		// create v1alpha1 external secret, if provided
+		createProvidedExternalSecret(tc)
 
 		// wait for Kind=Secret to have the expected data
 		executeAfterSync(tc, f, prov)
@@ -99,7 +98,7 @@ func TableFuncWithExternalSecret(f *Framework, prov SecretStoreProvider) func(..
 
 func executeAfterSync(tc *TestCase, f *Framework, prov SecretStoreProvider) {
 	if tc.ExpectedSecret != nil {
-		secret, err := tc.Framework.WaitForSecretValue(tc.Framework.Namespace.Name, TargetSecretName, tc.ExpectedSecret)
+		secret, err := tc.Framework.WaitForSecretValue(tc.Framework.Namespace.Name, externalSecretTargetName(tc), 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)
@@ -112,6 +111,19 @@ func executeAfterSync(tc *TestCase, f *Framework, prov SecretStoreProvider) {
 	}
 }
 
+func externalSecretTargetName(tc *TestCase) string {
+	if tc == nil || tc.ExternalSecret == nil {
+		return TargetSecretName
+	}
+	if tc.ExternalSecret.Spec.Target.Name != "" {
+		return tc.ExternalSecret.Spec.Target.Name
+	}
+	if tc.ExternalSecret.Name != "" {
+		return tc.ExternalSecret.Name
+	}
+	return TargetSecretName
+}
+
 func generateAdditionalObjects(tc *TestCase) {
 	if tc.AdditionalObjects != nil {
 		for _, obj := range tc.AdditionalObjects {
@@ -141,6 +153,11 @@ func TableFuncWithPushSecret(f *Framework, prov SecretStoreProvider, pushClient
 			tweak(tc)
 		}
 
+		prov = prepareTestCase(tc, prov)
+
+		// additional objects
+		generateAdditionalObjects(tc)
+
 		if tc.PushSecretSource != nil {
 			err := tc.Framework.CRClient.Create(GinkgoT().Context(), tc.PushSecretSource)
 			Expect(err).ToNot(HaveOccurred())
@@ -153,14 +170,26 @@ func TableFuncWithPushSecret(f *Framework, prov SecretStoreProvider, pushClient
 			Expect(err).ToNot(HaveOccurred())
 		}
 
-		// additional objects
-		generateAdditionalObjects(tc)
-
 		// Run verification on the secret that push secret created or not.
 		tc.VerifyPushSecretOutcome(tc.PushSecret, pushClient)
 	}
 }
 
+func prepareTestCase(tc *TestCase, prov SecretStoreProvider) SecretStoreProvider {
+	prov = effectiveTestCaseProvider(tc, prov)
+	if tc.Prepare != nil {
+		tc.Prepare(tc, prov)
+	}
+	return effectiveTestCaseProvider(tc, prov)
+}
+
+func effectiveTestCaseProvider(tc *TestCase, prov SecretStoreProvider) SecretStoreProvider {
+	if tc.ProviderOverride != nil {
+		return tc.ProviderOverride
+	}
+	return prov
+}
+
 func makeDefaultExternalSecretTestCase(f *Framework) *TestCase {
 	return &TestCase{
 		AfterSync: func(ssp SecretStoreProvider, s *v1.Secret) {},

+ 117 - 0
e2e/framework/testcase_test.go

@@ -0,0 +1,117 @@
+/*
+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 framework
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+type testcaseProviderStub struct {
+	name string
+}
+
+func (s *testcaseProviderStub) CreateSecret(string, SecretEntry) {}
+
+func (s *testcaseProviderStub) DeleteSecret(string) {}
+
+func TestPrepareTestCaseUsesDefaultProviderWithoutOverride(t *testing.T) {
+	t.Helper()
+	RegisterTestingT(t)
+
+	defaultProvider := &testcaseProviderStub{name: "default"}
+	tc := &TestCase{}
+
+	provider := prepareTestCase(tc, defaultProvider)
+
+	Expect(provider).To(BeIdenticalTo(defaultProvider))
+}
+
+func TestPrepareTestCaseLetsPrepareInstallProviderOverride(t *testing.T) {
+	t.Helper()
+	RegisterTestingT(t)
+
+	defaultProvider := &testcaseProviderStub{name: "default"}
+	overrideProvider := &testcaseProviderStub{name: "override"}
+	tc := &TestCase{
+		Prepare: func(tc *TestCase, provider SecretStoreProvider) {
+			Expect(provider).To(BeIdenticalTo(defaultProvider))
+			tc.ProviderOverride = overrideProvider
+		},
+	}
+
+	provider := prepareTestCase(tc, defaultProvider)
+
+	Expect(provider).To(BeIdenticalTo(overrideProvider))
+}
+
+func TestPrepareTestCaseUsesExistingOverrideDuringPrepare(t *testing.T) {
+	t.Helper()
+	RegisterTestingT(t)
+
+	defaultProvider := &testcaseProviderStub{name: "default"}
+	overrideProvider := &testcaseProviderStub{name: "override"}
+	tc := &TestCase{
+		ProviderOverride: overrideProvider,
+		Prepare: func(tc *TestCase, provider SecretStoreProvider) {
+			Expect(provider).To(BeIdenticalTo(overrideProvider))
+		},
+	}
+
+	provider := prepareTestCase(tc, defaultProvider)
+
+	Expect(provider).To(BeIdenticalTo(overrideProvider))
+}
+
+func TestExternalSecretTargetNameUsesExplicitTargetName(t *testing.T) {
+	t.Helper()
+	RegisterTestingT(t)
+
+	tc := &TestCase{
+		ExternalSecret: &esv1.ExternalSecret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "external-secret-name",
+			},
+			Spec: esv1.ExternalSecretSpec{
+				Target: esv1.ExternalSecretTarget{
+					Name: "explicit-target-name",
+				},
+			},
+		},
+	}
+
+	Expect(externalSecretTargetName(tc)).To(Equal("explicit-target-name"))
+}
+
+func TestExternalSecretTargetNameFallsBackToExternalSecretName(t *testing.T) {
+	t.Helper()
+	RegisterTestingT(t)
+
+	tc := &TestCase{
+		ExternalSecret: &esv1.ExternalSecret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "external-secret-name",
+			},
+		},
+	}
+
+	Expect(externalSecretTargetName(tc)).To(Equal("external-secret-name"))
+}

+ 242 - 0
e2e/suites/provider/cases/common/clusterprovider.go

@@ -0,0 +1,242 @@
+/*
+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 common
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/gomega"
+	corev1 "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"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+type ClusterProviderConfig struct {
+	Name       string
+	AuthScope  esv1.AuthenticationScope
+	Conditions []esv1.ClusterSecretStoreCondition
+}
+
+type ClusterProviderExternalSecretHarness struct {
+	Prepare func(tc *framework.TestCase, cfg ClusterProviderConfig) *ClusterProviderExternalSecretRuntime
+}
+
+type ClusterProviderExternalSecretRuntime struct {
+	ClusterProviderName string
+	Provider            framework.SecretStoreProvider
+	BreakAuth           func()
+	RepairAuth          func()
+}
+
+func ClusterProviderManifestNamespace(f *framework.Framework, harness ClusterProviderExternalSecretHarness) (string, func(*framework.TestCase)) {
+	return clusterProviderSyncCase(f, harness, "manifest", "manifest-value", esv1.AuthenticationScopeManifestNamespace)
+}
+
+func ClusterProviderProviderNamespace(f *framework.Framework, harness ClusterProviderExternalSecretHarness) (string, func(*framework.TestCase)) {
+	return clusterProviderSyncCase(f, harness, "provider", "provider-value", esv1.AuthenticationScopeProviderNamespace)
+}
+
+func ClusterProviderManifestNamespaceRecovery(f *framework.Framework, harness ClusterProviderExternalSecretHarness) (string, func(*framework.TestCase)) {
+	return clusterProviderRecoveryCase(f, harness, "manifest-recovery", "manifest-recovered", esv1.AuthenticationScopeManifestNamespace)
+}
+
+func ClusterProviderProviderNamespaceRecovery(f *framework.Framework, harness ClusterProviderExternalSecretHarness) (string, func(*framework.TestCase)) {
+	return clusterProviderRecoveryCase(f, harness, "provider-recovery", "provider-recovered", esv1.AuthenticationScopeProviderNamespace)
+}
+
+func ClusterProviderDeniedByConditions(f *framework.Framework, harness ClusterProviderExternalSecretHarness) (string, func(*framework.TestCase)) {
+	return "[common] should deny workload namespaces that do not match ClusterProvider conditions", func(tc *framework.TestCase) {
+		targetSecretName := "denied-target"
+		remoteSecretName := "denied-source"
+		expectedMessage := "should-not-sync"
+
+		tc.ExpectedSecret = nil
+		tc.ExternalSecret.ObjectMeta.Name = "denied-external-secret"
+		tc.ExternalSecret.Spec.Target.Name = targetSecretName
+		tc.ExternalSecret.Spec.Data = []esv1.ExternalSecretData{{
+			SecretKey: "value",
+			RemoteRef: esv1.ExternalSecretDataRemoteRef{
+				Key:      remoteSecretName,
+				Property: "value",
+			},
+		}}
+		tc.Secrets = map[string]framework.SecretEntry{
+			remoteSecretName: {Value: jsonSecretValue(expectedMessage)},
+		}
+
+		var runtime *ClusterProviderExternalSecretRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.Prepare(tc, ClusterProviderConfig{
+				Name:      "deny",
+				AuthScope: esv1.AuthenticationScopeManifestNamespace,
+				Conditions: []esv1.ClusterSecretStoreCondition{{
+					Namespaces: []string{"not-" + f.Namespace.Name},
+				}},
+			})
+			applyClusterProviderExternalSecret(tc, runtime)
+		}
+		tc.AfterSync = func(_ framework.SecretStoreProvider, _ *corev1.Secret) {
+			waitForExternalSecretStatus(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Name, corev1.ConditionFalse)
+			expectNoSecretInNamespace(tc.Framework, tc.ExternalSecret.Namespace, targetSecretName)
+			expectEventMessage(
+				tc.Framework,
+				tc.ExternalSecret.Namespace,
+				tc.ExternalSecret.Name,
+				"ExternalSecret",
+				fmt.Sprintf("using ClusterProvider %q is not allowed from namespace %q: denied by spec.conditions", runtime.ClusterProviderName, f.Namespace.Name),
+			)
+		}
+	}
+}
+
+func clusterProviderSyncCase(f *framework.Framework, harness ClusterProviderExternalSecretHarness, name, expectedValue string, authScope esv1.AuthenticationScope) (string, func(*framework.TestCase)) {
+	return fmt.Sprintf("[common] should use %s auth with ClusterProvider", authScope), func(tc *framework.TestCase) {
+		targetSecretName := fmt.Sprintf("%s-target", name)
+		remoteSecretName := fmt.Sprintf("%s-source", name)
+
+		tc.ExternalSecret.ObjectMeta.Name = fmt.Sprintf("%s-external-secret", name)
+		tc.ExternalSecret.Spec.Target.Name = targetSecretName
+		tc.ExternalSecret.Spec.Data = []esv1.ExternalSecretData{{
+			SecretKey: "value",
+			RemoteRef: esv1.ExternalSecretDataRemoteRef{
+				Key:      remoteSecretName,
+				Property: "value",
+			},
+		}}
+		tc.Secrets = map[string]framework.SecretEntry{
+			remoteSecretName: {Value: jsonSecretValue(expectedValue)},
+		}
+		tc.ExpectedSecret = &corev1.Secret{
+			Type: corev1.SecretTypeOpaque,
+			Data: map[string][]byte{
+				"value": []byte(expectedValue),
+			},
+		}
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime := harness.Prepare(tc, ClusterProviderConfig{
+				Name:      name,
+				AuthScope: authScope,
+			})
+			applyClusterProviderExternalSecret(tc, runtime)
+		}
+	}
+}
+
+func clusterProviderRecoveryCase(f *framework.Framework, harness ClusterProviderExternalSecretHarness, name, expectedValue string, authScope esv1.AuthenticationScope) (string, func(*framework.TestCase)) {
+	return fmt.Sprintf("[common] should recover after repairing ClusterProvider auth with %s scope", authScope), func(tc *framework.TestCase) {
+		targetSecretName := fmt.Sprintf("%s-target", name)
+		remoteSecretName := fmt.Sprintf("%s-source", name)
+
+		tc.ExpectedSecret = nil
+		tc.ExternalSecret.ObjectMeta.Name = fmt.Sprintf("%s-external-secret", name)
+		tc.ExternalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Hour}
+		tc.ExternalSecret.Spec.Target.Name = targetSecretName
+		tc.ExternalSecret.Spec.Data = []esv1.ExternalSecretData{{
+			SecretKey: "value",
+			RemoteRef: esv1.ExternalSecretDataRemoteRef{
+				Key:      remoteSecretName,
+				Property: "value",
+			},
+		}}
+		tc.Secrets = map[string]framework.SecretEntry{
+			remoteSecretName: {Value: jsonSecretValue(expectedValue)},
+		}
+
+		var runtime *ClusterProviderExternalSecretRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.Prepare(tc, ClusterProviderConfig{
+				Name:      name,
+				AuthScope: authScope,
+			})
+			applyClusterProviderExternalSecret(tc, runtime)
+			runtime.BreakAuth()
+		}
+		tc.AfterSync = func(_ framework.SecretStoreProvider, _ *corev1.Secret) {
+			waitForExternalSecretStatus(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Name, corev1.ConditionFalse)
+			expectNoSecretInNamespace(tc.Framework, tc.ExternalSecret.Namespace, targetSecretName)
+			runtime.RepairAuth()
+			waitForSecretData(tc.Framework, tc.ExternalSecret.Namespace, targetSecretName, map[string][]byte{
+				"value": []byte(expectedValue),
+			}, 30*time.Second)
+		}
+	}
+}
+
+func applyClusterProviderExternalSecret(tc *framework.TestCase, runtime *ClusterProviderExternalSecretRuntime) {
+	tc.ProviderOverride = runtime.Provider
+	tc.ExternalSecret.Spec.SecretStoreRef.Name = runtime.ClusterProviderName
+	tc.ExternalSecret.Spec.SecretStoreRef.Kind = esv1.ClusterProviderKindStr
+}
+
+func waitForExternalSecretStatus(f *framework.Framework, namespace, name string, status corev1.ConditionStatus) {
+	Eventually(func(g Gomega) {
+		var externalSecret esv1.ExternalSecret
+		g.Expect(f.CRClient.Get(context.Background(), types.NamespacedName{
+			Name:      name,
+			Namespace: namespace,
+		}, &externalSecret)).To(Succeed())
+		condition := esv1.GetExternalSecretCondition(externalSecret.Status, esv1.ExternalSecretReady)
+		g.Expect(condition).NotTo(BeNil())
+		g.Expect(condition.Status).To(Equal(status))
+	}, time.Minute, 5*time.Second).Should(Succeed())
+}
+
+func waitForSecretData(f *framework.Framework, namespace, name string, expected map[string][]byte, timeout time.Duration) {
+	Eventually(func(g Gomega) {
+		var syncedSecret corev1.Secret
+		g.Expect(f.CRClient.Get(context.Background(), types.NamespacedName{
+			Name:      name,
+			Namespace: namespace,
+		}, &syncedSecret)).To(Succeed())
+		g.Expect(syncedSecret.Type).To(Equal(corev1.SecretTypeOpaque))
+		g.Expect(syncedSecret.Data).To(Equal(expected))
+	}, timeout, 5*time.Second).Should(Succeed())
+}
+
+func expectNoSecretInNamespace(f *framework.Framework, namespace, name string) {
+	Consistently(func() bool {
+		var secret corev1.Secret
+		err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: name, Namespace: namespace}, &secret)
+		return apierrors.IsNotFound(err)
+	}, 10*time.Second, 5*time.Second).Should(BeTrue())
+}
+
+func expectEventMessage(f *framework.Framework, namespace, objectName, objectKind, expectedMessage string) {
+	Eventually(func() string {
+		events, err := f.KubeClientSet.CoreV1().Events(namespace).List(context.Background(), metav1.ListOptions{
+			FieldSelector: "involvedObject.name=" + objectName + ",involvedObject.kind=" + objectKind,
+		})
+		Expect(err).NotTo(HaveOccurred())
+		messages := make([]string, 0, len(events.Items))
+		for _, event := range events.Items {
+			if event.Message != "" {
+				messages = append(messages, event.Message)
+			}
+		}
+		return fmt.Sprintf("%v", messages)
+	}, time.Minute, 5*time.Second).Should(ContainSubstring(expectedMessage))
+}
+
+func jsonSecretValue(value string) string {
+	return fmt.Sprintf(`{"value":%q}`, value)
+}

+ 402 - 0
e2e/suites/provider/cases/common/push_secret.go

@@ -0,0 +1,402 @@
+/*
+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 common
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/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"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+type ClusterProviderPushHarness struct {
+	Prepare func(tc *framework.TestCase, cfg ClusterProviderConfig) *ClusterProviderPushRuntime
+}
+
+type ClusterProviderPushRuntime struct {
+	ClusterProviderName       string
+	DefaultRemoteNamespace    string
+	BreakAuth                 func()
+	RepairAuth                func()
+	WaitForRemoteSecretValue  func(namespace, name, key, expectedValue string)
+	ExpectNoRemoteSecret      func(namespace, name string)
+	CreateWritableRemoteScope func(prefix string) string
+}
+
+func PushSecretPreservesSourceMetadata(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[common] should preserve source secret type, labels, and annotations when pushing to the namespaced Provider", func(tc *framework.TestCase) {
+		tc.PushSecretSource = &corev1.Secret{
+			Type: corev1.SecretTypeDockerConfigJson,
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "source-secret-metadata",
+				Namespace: f.Namespace.Name,
+				Labels: map[string]string{
+					"team": "platform",
+				},
+				Annotations: map[string]string{
+					"owner": "eso",
+				},
+			},
+			Data: map[string][]byte{
+				corev1.DockerConfigJsonKey: []byte(`{"auths":{"registry.example.com":{"auth":"ZXNvOnNlY3JldA=="}}}`),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "test-pushsecret-metadata"
+		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+			Secret: &esv1alpha1.PushSecretSecret{
+				Name: tc.PushSecretSource.Name,
+			},
+		}
+		tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+			Match: esv1alpha1.PushSecretMatch{
+				SecretKey: corev1.DockerConfigJsonKey,
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: "pushed-docker-secret",
+					Property:  corev1.DockerConfigJsonKey,
+				},
+			},
+		}}
+		tc.VerifyPushSecretOutcome = func(_ *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			waitForPushSecretStatus(tc.Framework, tc.PushSecret.Namespace, tc.PushSecret.Name, corev1.ConditionTrue)
+
+			var pushedSecret corev1.Secret
+			Eventually(func(g Gomega) {
+				g.Expect(tc.Framework.CRClient.Get(context.Background(), types.NamespacedName{
+					Name:      "pushed-docker-secret",
+					Namespace: f.Namespace.Name,
+				}, &pushedSecret)).To(Succeed())
+			}, time.Minute, 5*time.Second).Should(Succeed())
+
+			Expect(pushedSecret.Type).To(Equal(tc.PushSecretSource.Type))
+			Expect(pushedSecret.Labels).To(Equal(tc.PushSecretSource.Labels))
+			Expect(pushedSecret.Annotations).To(Equal(tc.PushSecretSource.Annotations))
+			Expect(pushedSecret.Data).To(Equal(tc.PushSecretSource.Data))
+		}
+	}
+}
+
+func PushSecretImplicitProviderKind(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[common] should support namespaced Provider refs when kind is omitted", func(tc *framework.TestCase) {
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "source-secret-implicit-kind",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"key1": []byte("value1"),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "test-pushsecret-implicit-kind"
+		tc.PushSecret.Spec.DeletionPolicy = esv1alpha1.PushSecretDeletionPolicyDelete
+		tc.PushSecret.Spec.SecretStoreRefs[0].Kind = ""
+		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+			Secret: &esv1alpha1.PushSecretSecret{
+				Name: tc.PushSecretSource.Name,
+			},
+		}
+		tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+			Match: esv1alpha1.PushSecretMatch{
+				SecretKey: "key1",
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: "pushed-secret-implicit-kind",
+					Property:  "key1",
+				},
+			},
+		}}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+			Eventually(func(g Gomega) {
+				var pushedSecret corev1.Secret
+				g.Expect(tc.Framework.CRClient.Get(context.Background(), types.NamespacedName{
+					Name:      "pushed-secret-implicit-kind",
+					Namespace: f.Namespace.Name,
+				}, &pushedSecret)).To(Succeed())
+				g.Expect(string(pushedSecret.Data["key1"])).To(Equal("value1"))
+			}, time.Minute, 5*time.Second).Should(Succeed())
+
+			Expect(tc.Framework.CRClient.Delete(context.Background(), ps)).To(Succeed())
+			Eventually(func() bool {
+				var pushedSecret corev1.Secret
+				err := tc.Framework.CRClient.Get(context.Background(), types.NamespacedName{
+					Name:      "pushed-secret-implicit-kind",
+					Namespace: f.Namespace.Name,
+				}, &pushedSecret)
+				return apierrors.IsNotFound(err)
+			}, time.Minute, 5*time.Second).Should(BeTrue())
+		}
+	}
+}
+
+func PushSecretRejectsNamespacedRemoteNamespaceOverride(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[common] should reject remote namespace overrides when pushing through a namespaced Provider", func(tc *framework.TestCase) {
+		overrideNamespace := createE2ENamespace(tc.Framework, "push-provider-override")
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "source-secret-provider-override",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("should-not-push"),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "test-pushsecret-provider-override"
+		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+			Secret: &esv1alpha1.PushSecretSecret{
+				Name: tc.PushSecretSource.Name,
+			},
+		}
+		tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+			Match: esv1alpha1.PushSecretMatch{
+				SecretKey: "value",
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: "pushed-secret-provider-override",
+					Property:  "value",
+				},
+			},
+			Metadata: pushSecretMetadataWithRemoteNamespace(overrideNamespace),
+		}}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
+			expectNoSecretInNamespace(tc.Framework, f.Namespace.Name, "pushed-secret-provider-override")
+			expectNoSecretInNamespace(tc.Framework, overrideNamespace, "pushed-secret-provider-override")
+			expectEventMessage(tc.Framework, ps.Namespace, ps.Name, "PushSecret", "remoteNamespace override is only supported with ClusterSecretStore")
+		}
+	}
+}
+
+func ClusterProviderPushManifestNamespace(f *framework.Framework, harness ClusterProviderPushHarness) (string, func(*framework.TestCase)) {
+	return clusterProviderPushSyncCase(f, harness, "push-manifest", "manifest-push-value", esv1.AuthenticationScopeManifestNamespace)
+}
+
+func ClusterProviderPushProviderNamespace(f *framework.Framework, harness ClusterProviderPushHarness) (string, func(*framework.TestCase)) {
+	return clusterProviderPushSyncCase(f, harness, "push-provider", "provider-push-value", esv1.AuthenticationScopeProviderNamespace)
+}
+
+func ClusterProviderPushManifestNamespaceRecovery(f *framework.Framework, harness ClusterProviderPushHarness) (string, func(*framework.TestCase)) {
+	return clusterProviderPushRecoveryCase(f, harness, "push-manifest-recovery", "manifest-push-recovered", esv1.AuthenticationScopeManifestNamespace)
+}
+
+func ClusterProviderPushProviderNamespaceRecovery(f *framework.Framework, harness ClusterProviderPushHarness) (string, func(*framework.TestCase)) {
+	return clusterProviderPushRecoveryCase(f, harness, "push-provider-recovery", "provider-push-recovered", esv1.AuthenticationScopeProviderNamespace)
+}
+
+func ClusterProviderPushAllowsRemoteNamespaceOverride(f *framework.Framework, harness ClusterProviderPushHarness) (string, func(*framework.TestCase)) {
+	return "[common] should allow ClusterProvider pushes to override the target remote namespace via metadata", func(tc *framework.TestCase) {
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "push-remote-override-source",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("override-push-value"),
+			},
+		}
+
+		var runtime *ClusterProviderPushRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.Prepare(tc, ClusterProviderConfig{
+				Name:      "push-remote-override",
+				AuthScope: esv1.AuthenticationScopeManifestNamespace,
+			})
+			overrideNamespace := runtime.CreateWritableRemoteScope("push-remote-override-target")
+			applyClusterProviderPushSecret(tc, runtime, "push-remote-override-remote")
+			tc.PushSecret.Spec.Data[0].Metadata = pushSecretMetadataWithRemoteNamespace(overrideNamespace)
+			tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+				runtime.WaitForRemoteSecretValue(overrideNamespace, "push-remote-override-remote", "value", "override-push-value")
+				runtime.ExpectNoRemoteSecret(runtime.DefaultRemoteNamespace, "push-remote-override-remote")
+			}
+		}
+	}
+}
+
+func ClusterProviderPushDeniedByConditions(f *framework.Framework, harness ClusterProviderPushHarness) (string, func(*framework.TestCase)) {
+	return "[common] should deny PushSecrets from namespaces that do not match ClusterProvider conditions", func(tc *framework.TestCase) {
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "push-deny-source",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("should-not-push"),
+			},
+		}
+
+		var runtime *ClusterProviderPushRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.Prepare(tc, ClusterProviderConfig{
+				Name:      "push-deny",
+				AuthScope: esv1.AuthenticationScopeManifestNamespace,
+				Conditions: []esv1.ClusterSecretStoreCondition{{
+					Namespaces: []string{"not-" + f.Namespace.Name},
+				}},
+			})
+			applyClusterProviderPushSecret(tc, runtime, "push-deny-remote")
+		}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
+			runtime.ExpectNoRemoteSecret(runtime.DefaultRemoteNamespace, "push-deny-remote")
+			expectEventMessage(tc.Framework, ps.Namespace, ps.Name, "PushSecret", fmt.Sprintf("using ClusterProvider %q is not allowed from namespace %q: denied by spec.conditions", runtime.ClusterProviderName, f.Namespace.Name))
+		}
+	}
+}
+
+func clusterProviderPushSyncCase(f *framework.Framework, harness ClusterProviderPushHarness, name, expectedValue string, authScope esv1.AuthenticationScope) (string, func(*framework.TestCase)) {
+	return fmt.Sprintf("[common] should push through a ClusterProvider with %s auth", authScope), func(tc *framework.TestCase) {
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      fmt.Sprintf("%s-source", name),
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte(expectedValue),
+			},
+		}
+
+		var runtime *ClusterProviderPushRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.Prepare(tc, ClusterProviderConfig{
+				Name:      name,
+				AuthScope: authScope,
+			})
+			applyClusterProviderPushSecret(tc, runtime, fmt.Sprintf("%s-remote", name))
+		}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+			runtime.WaitForRemoteSecretValue(runtime.DefaultRemoteNamespace, fmt.Sprintf("%s-remote", name), "value", expectedValue)
+		}
+	}
+}
+
+func clusterProviderPushRecoveryCase(f *framework.Framework, harness ClusterProviderPushHarness, name, expectedValue string, authScope esv1.AuthenticationScope) (string, func(*framework.TestCase)) {
+	return fmt.Sprintf("[common] should recover after repairing ClusterProvider push auth with %s scope", authScope), func(tc *framework.TestCase) {
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      fmt.Sprintf("%s-source", name),
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte(expectedValue),
+			},
+		}
+
+		var runtime *ClusterProviderPushRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.Prepare(tc, ClusterProviderConfig{
+				Name:      name,
+				AuthScope: authScope,
+			})
+			applyClusterProviderPushSecret(tc, runtime, fmt.Sprintf("%s-remote", name))
+			tc.PushSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Hour}
+			runtime.BreakAuth()
+		}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
+			runtime.ExpectNoRemoteSecret(runtime.DefaultRemoteNamespace, fmt.Sprintf("%s-remote", name))
+			runtime.RepairAuth()
+			waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+			runtime.WaitForRemoteSecretValue(runtime.DefaultRemoteNamespace, fmt.Sprintf("%s-remote", name), "value", expectedValue)
+		}
+	}
+}
+
+func applyClusterProviderPushSecret(tc *framework.TestCase, runtime *ClusterProviderPushRuntime, remoteSecretName string) {
+	tc.PushSecret.ObjectMeta.Name = fmt.Sprintf("%s-push-secret", remoteSecretName)
+	tc.PushSecret.Spec.SecretStoreRefs = []esv1alpha1.PushSecretStoreRef{{
+		Name:       runtime.ClusterProviderName,
+		Kind:       esv1.ClusterProviderKindStr,
+		APIVersion: esv1.SchemeGroupVersion.String(),
+	}}
+	tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+		Secret: &esv1alpha1.PushSecretSecret{
+			Name: tc.PushSecretSource.Name,
+		},
+	}
+	tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+		Match: esv1alpha1.PushSecretMatch{
+			SecretKey: "value",
+			RemoteRef: esv1alpha1.PushSecretRemoteRef{
+				RemoteKey: remoteSecretName,
+				Property:  "value",
+			},
+		},
+	}}
+}
+
+func waitForPushSecretStatus(f *framework.Framework, namespace, name string, status corev1.ConditionStatus) {
+	Eventually(func(g Gomega) {
+		var ps esv1alpha1.PushSecret
+		g.Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: name, Namespace: namespace}, &ps)).To(Succeed())
+		g.Expect(ps.Status.Conditions).NotTo(BeEmpty())
+		ready := false
+		for _, condition := range ps.Status.Conditions {
+			if condition.Type == esv1alpha1.PushSecretReady && condition.Status == status {
+				ready = true
+			}
+		}
+		g.Expect(ready).To(BeTrue())
+	}, time.Minute, 5*time.Second).Should(Succeed())
+}
+
+func pushSecretMetadataWithRemoteNamespace(namespace string) *apiextensionsv1.JSON {
+	return &apiextensionsv1.JSON{Raw: []byte(fmt.Sprintf(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"remoteNamespace":"%s"}}`, namespace))}
+}
+
+func createE2ENamespace(f *framework.Framework, prefix string) string {
+	namespace := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			GenerateName: fmt.Sprintf("e2e-tests-%s-", prefix),
+		},
+	}
+	Expect(f.CRClient.Create(context.Background(), namespace)).To(Succeed())
+
+	DeferCleanup(func() {
+		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+		defer cancel()
+
+		err := f.CRClient.Delete(ctx, namespace)
+		if err != nil && !apierrors.IsNotFound(err) {
+			Expect(err).ToNot(HaveOccurred())
+		}
+
+		err = wait.PollUntilContextTimeout(ctx, 5*time.Second, 2*time.Minute, true, func(ctx context.Context) (bool, error) {
+			_, err := f.KubeClientSet.CoreV1().Namespaces().Get(ctx, namespace.Name, metav1.GetOptions{})
+			if apierrors.IsNotFound(err) {
+				return true, nil
+			}
+			if err != nil {
+				return false, err
+			}
+			return false, nil
+		})
+		Expect(err).To(Succeed())
+	})
+
+	return namespace.Name
+}

+ 62 - 207
e2e/suites/provider/cases/kubernetes/clusterprovider_v2.go

@@ -18,8 +18,8 @@ package kubernetes
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
-	"strings"
 	"time"
 
 	. "github.com/onsi/ginkgo/v2"
@@ -33,11 +33,14 @@ import (
 
 	"github.com/external-secrets/external-secrets-e2e/framework"
 	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 )
 
 var _ = Describe("[kubernetes] v2 cluster provider", Label("kubernetes", "v2", "cluster-provider"), func() {
 	f := framework.New("eso-kubernetes-v2-clusterprovider")
+	prov := NewProvider(f)
+	harness := newKubernetesClusterProviderExternalSecretHarness(f)
 
 	BeforeEach(func() {
 		if !framework.IsV2ProviderMode() {
@@ -45,99 +48,38 @@ var _ = Describe("[kubernetes] v2 cluster provider", Label("kubernetes", "v2", "
 		}
 	})
 
-	It("uses the manifest namespace for auth when authenticationScope=ManifestNamespace", func() {
-		s := newClusterProviderV2Scenario(f, "manifest")
-		s.allowRemoteAccessFrom(s.workloadNamespace, "manifest")
-
-		remoteSecretName := fmt.Sprintf("%s-source", s.namePrefix)
-		targetSecretName := fmt.Sprintf("%s-target", s.namePrefix)
-		s.createRemoteSecret(remoteSecretName, "manifest-value")
-
-		clusterProviderName := s.createClusterProvider("manifest", esv1.AuthenticationScopeManifestNamespace, nil)
-		frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
-
-		externalSecretName := s.createExternalSecret(clusterProviderName, targetSecretName, remoteSecretName)
-		s.waitForExternalSecretValue(externalSecretName, targetSecretName, "manifest-value")
-	})
-
-	It("uses the providerRef namespace for auth when authenticationScope=ProviderNamespace", func() {
-		s := newClusterProviderV2Scenario(f, "provider")
-		s.allowRemoteAccessFrom(s.providerNamespace, "provider")
-
-		remoteSecretName := fmt.Sprintf("%s-source", s.namePrefix)
-		targetSecretName := fmt.Sprintf("%s-target", s.namePrefix)
-		s.createRemoteSecret(remoteSecretName, "provider-value")
-
-		clusterProviderName := s.createClusterProvider("provider", esv1.AuthenticationScopeProviderNamespace, nil)
-		frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
-
-		externalSecretName := s.createExternalSecret(clusterProviderName, targetSecretName, remoteSecretName)
-		s.waitForExternalSecretValue(externalSecretName, targetSecretName, "provider-value")
-	})
-
-	It("recovers after repairing cluster provider auth when authenticationScope=ManifestNamespace", func() {
-		s := newClusterProviderV2Scenario(f, "manifest-recovery")
-		s.allowRemoteAccessFrom(s.workloadNamespace, "manifest-recovery")
-
-		remoteSecretName := fmt.Sprintf("%s-source", s.namePrefix)
-		targetSecretName := fmt.Sprintf("%s-target", s.namePrefix)
-		s.createRemoteSecret(remoteSecretName, "manifest-recovered")
-
-		clusterProviderName := s.createClusterProvider("manifest-recovery", esv1.AuthenticationScopeManifestNamespace, nil)
-		frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
-
-		updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName("manifest-recovery"), "missing-service-account")
-
-		externalSecretName := s.createExternalSecretWithRefresh(clusterProviderName, targetSecretName, remoteSecretName, time.Hour)
-		s.waitForExternalSecretFailure(externalSecretName)
-		s.expectNoTargetSecret(targetSecretName)
-
-		updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName("manifest-recovery"), s.serviceAccount)
-
-		s.waitForExternalSecretValueWithin(externalSecretName, targetSecretName, "manifest-recovered", 30*time.Second)
-	})
-
-	It("recovers after repairing cluster provider auth when authenticationScope=ProviderNamespace", func() {
-		s := newClusterProviderV2Scenario(f, "provider-recovery")
-		s.allowRemoteAccessFrom(s.providerNamespace, "provider-recovery")
-
-		remoteSecretName := fmt.Sprintf("%s-source", s.namePrefix)
-		targetSecretName := fmt.Sprintf("%s-target", s.namePrefix)
-		s.createRemoteSecret(remoteSecretName, "provider-recovered")
-
-		clusterProviderName := s.createClusterProvider("provider-recovery", esv1.AuthenticationScopeProviderNamespace, nil)
-		frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
-
-		updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName("provider-recovery"), "missing-service-account")
-
-		externalSecretName := s.createExternalSecretWithRefresh(clusterProviderName, targetSecretName, remoteSecretName, time.Hour)
-		s.waitForExternalSecretFailure(externalSecretName)
-		s.expectNoTargetSecret(targetSecretName)
-
-		updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName("provider-recovery"), s.serviceAccount)
-
-		s.waitForExternalSecretValueWithin(externalSecretName, targetSecretName, "provider-recovered", 30*time.Second)
-	})
-
-	It("denies workload namespaces that do not match ClusterProvider conditions", func() {
-		s := newClusterProviderV2Scenario(f, "deny")
-		s.allowRemoteAccessFrom(s.workloadNamespace, "deny")
+	DescribeTable("cluster provider external secrets",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(common.ClusterProviderManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderManifestNamespaceRecovery(f, harness)),
+		Entry(common.ClusterProviderProviderNamespaceRecovery(f, harness)),
+		Entry(common.ClusterProviderDeniedByConditions(f, harness)),
+	)
+})
 
-		remoteSecretName := fmt.Sprintf("%s-source", s.namePrefix)
-		targetSecretName := fmt.Sprintf("%s-target", s.namePrefix)
-		s.createRemoteSecret(remoteSecretName, "should-not-sync")
+func newKubernetesClusterProviderExternalSecretHarness(f *framework.Framework) common.ClusterProviderExternalSecretHarness {
+	return common.ClusterProviderExternalSecretHarness{
+		Prepare: func(tc *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderExternalSecretRuntime {
+			s := newClusterProviderV2Scenario(f, cfg.Name)
+			s.allowRemoteAccessForScope(cfg.AuthScope, cfg.Name)
 
-		clusterProviderName := s.createClusterProvider("deny", esv1.AuthenticationScopeManifestNamespace, []esv1.ClusterSecretStoreCondition{{
-			Namespaces: []string{"not-" + s.workloadNamespace},
-		}})
-		frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+			clusterProviderName := s.createClusterProvider(cfg.Name, cfg.AuthScope, cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
 
-		externalSecretName := s.createExternalSecret(clusterProviderName, targetSecretName, remoteSecretName)
-		s.waitForExternalSecretFailure(externalSecretName)
-		s.expectNoTargetSecret(targetSecretName)
-		s.expectExternalSecretEvent(externalSecretName, fmt.Sprintf("using ClusterProvider %q is not allowed from namespace %q: denied by spec.conditions", clusterProviderName, s.workloadNamespace))
-	})
-})
+			return &common.ClusterProviderExternalSecretRuntime{
+				ClusterProviderName: clusterProviderName,
+				Provider:            s,
+				BreakAuth: func() {
+					updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName(cfg.Name), "missing-service-account")
+				},
+				RepairAuth: func() {
+					updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName(cfg.Name), s.serviceAccount)
+				},
+			}
+		},
+	}
+}
 
 type clusterProviderV2Scenario struct {
 	f                 *framework.Framework
@@ -186,6 +128,14 @@ func (s *clusterProviderV2Scenario) allowRemoteAccessFrom(serviceAccountNamespac
 	)
 }
 
+func (s *clusterProviderV2Scenario) allowRemoteAccessForScope(authScope esv1.AuthenticationScope, suffix string) {
+	serviceAccountNamespace := s.workloadNamespace
+	if authScope == esv1.AuthenticationScopeProviderNamespace {
+		serviceAccountNamespace = s.providerNamespace
+	}
+	s.allowRemoteAccessFrom(serviceAccountNamespace, suffix)
+}
+
 func (s *clusterProviderV2Scenario) createClusterProvider(suffix string, authScope esv1.AuthenticationScope, conditions []esv1.ClusterSecretStoreCondition) string {
 	providerConfigName := s.providerConfigName(suffix)
 	frameworkv2.CreateKubernetesProvider(
@@ -217,105 +167,33 @@ func (s *clusterProviderV2Scenario) providerConfigName(suffix string) string {
 	return fmt.Sprintf("%s-config-%s", s.namePrefix, suffix)
 }
 
-func (s *clusterProviderV2Scenario) createRemoteSecret(name, value string) {
-	Expect(s.f.CRClient.Create(context.Background(), &corev1.Secret{
+func (s *clusterProviderV2Scenario) CreateSecret(key string, val framework.SecretEntry) {
+	secret := &corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      name,
+			Name:      key,
 			Namespace: s.remoteNamespace,
+			Labels:    val.Tags,
 		},
-		Data: map[string][]byte{
-			"value": []byte(value),
-		},
-	})).To(Succeed())
-}
+		Data: make(map[string][]byte),
+	}
+	stringMap := make(map[string]string)
+	err := json.Unmarshal([]byte(val.Value), &stringMap)
+	Expect(err).ToNot(HaveOccurred())
 
-func (s *clusterProviderV2Scenario) createExternalSecret(clusterProviderName, targetSecretName, remoteSecretName string) string {
-	return s.createExternalSecretWithRefresh(clusterProviderName, targetSecretName, remoteSecretName, defaultV2RefreshInterval)
+	for k, v := range stringMap {
+		secret.Data[k] = []byte(v)
+	}
+	Expect(s.f.CRClient.Create(GinkgoT().Context(), secret)).To(Succeed())
 }
 
-func (s *clusterProviderV2Scenario) createExternalSecretWithRefresh(clusterProviderName, targetSecretName, remoteSecretName string, refreshInterval time.Duration) string {
-	externalSecretName := fmt.Sprintf("%s-external-secret", s.namePrefix)
-	Expect(s.f.CRClient.Create(context.Background(), &esv1.ExternalSecret{
+func (s *clusterProviderV2Scenario) DeleteSecret(key string) {
+	secret := &corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      externalSecretName,
-			Namespace: s.workloadNamespace,
-		},
-		Spec: esv1.ExternalSecretSpec{
-			RefreshInterval: &metav1.Duration{Duration: refreshInterval},
-			SecretStoreRef: esv1.SecretStoreRef{
-				Name: clusterProviderName,
-				Kind: esv1.ClusterProviderKindStr,
-			},
-			Target: esv1.ExternalSecretTarget{
-				Name: targetSecretName,
-			},
-			Data: []esv1.ExternalSecretData{{
-				SecretKey: "value",
-				RemoteRef: esv1.ExternalSecretDataRemoteRef{
-					Key:      remoteSecretName,
-					Property: "value",
-				},
-			}},
+			Name:      key,
+			Namespace: s.remoteNamespace,
 		},
-	})).To(Succeed())
-
-	DeferCleanup(func() {
-		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
-		defer cancel()
-
-		err := deleteExternalSecretAndWait(ctx, s.f.CRClient, types.NamespacedName{
-			Name:      externalSecretName,
-			Namespace: s.workloadNamespace,
-		})
-		Expect(err).NotTo(HaveOccurred())
-	})
-
-	return externalSecretName
-}
-
-func (s *clusterProviderV2Scenario) waitForExternalSecretValue(externalSecretName, targetSecretName, expectedValue string) {
-	s.waitForExternalSecretValueWithin(externalSecretName, targetSecretName, expectedValue, defaultV2WaitTimeout)
-}
-
-func (s *clusterProviderV2Scenario) waitForExternalSecretValueWithin(externalSecretName, targetSecretName, expectedValue string, timeout time.Duration) {
-	Eventually(func(g Gomega) {
-		var externalSecret esv1.ExternalSecret
-		g.Expect(s.f.CRClient.Get(context.Background(), types.NamespacedName{
-			Name:      externalSecretName,
-			Namespace: s.workloadNamespace,
-		}, &externalSecret)).To(Succeed())
-		condition := esv1.GetExternalSecretCondition(externalSecret.Status, esv1.ExternalSecretReady)
-		g.Expect(condition).NotTo(BeNil())
-		g.Expect(externalSecretConditionHasStatus(condition, corev1.ConditionTrue)).To(BeTrue())
-
-		var syncedSecret corev1.Secret
-		g.Expect(s.f.CRClient.Get(context.Background(), types.NamespacedName{
-			Name:      targetSecretName,
-			Namespace: s.workloadNamespace,
-		}, &syncedSecret)).To(Succeed())
-		g.Expect(syncedSecret.Type).To(Equal(corev1.SecretTypeOpaque))
-		g.Expect(syncedSecret.Data).To(Equal(map[string][]byte{
-			"value": []byte(expectedValue),
-		}))
-	}, timeout, defaultV2PollInterval).Should(Succeed())
-}
-
-func (s *clusterProviderV2Scenario) waitForExternalSecretFailure(externalSecretName string) {
-	Eventually(func(g Gomega) {
-		var externalSecret esv1.ExternalSecret
-		g.Expect(s.f.CRClient.Get(context.Background(), types.NamespacedName{
-			Name:      externalSecretName,
-			Namespace: s.workloadNamespace,
-		}, &externalSecret)).To(Succeed())
-		condition := esv1.GetExternalSecretCondition(externalSecret.Status, esv1.ExternalSecretReady)
-		g.Expect(condition).NotTo(BeNil())
-		g.Expect(externalSecretConditionHasStatus(condition, corev1.ConditionFalse)).To(BeTrue())
-		g.Expect(condition.Reason).To(Equal(esv1.ConditionReasonSecretSyncedError))
-	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
-}
-
-func externalSecretConditionHasStatus(condition *esv1.ExternalSecretStatusCondition, want corev1.ConditionStatus) bool {
-	return condition != nil && condition.Status == want
+	}
+	Expect(s.f.CRClient.Delete(GinkgoT().Context(), secret)).To(Succeed())
 }
 
 func deleteExternalSecretAndWait(ctx context.Context, kubeClient client.Client, key types.NamespacedName) error {
@@ -344,31 +222,8 @@ func deleteExternalSecretAndWait(ctx context.Context, kubeClient client.Client,
 	})
 }
 
-func (s *clusterProviderV2Scenario) expectNoTargetSecret(targetSecretName string) {
-	Consistently(func() bool {
-		var syncedSecret corev1.Secret
-		err := s.f.CRClient.Get(context.Background(), types.NamespacedName{
-			Name:      targetSecretName,
-			Namespace: s.workloadNamespace,
-		}, &syncedSecret)
-		return apierrors.IsNotFound(err)
-	}, 10*time.Second, defaultV2PollInterval).Should(BeTrue())
-}
-
-func (s *clusterProviderV2Scenario) expectExternalSecretEvent(externalSecretName, expectedMessage string) {
-	Eventually(func() string {
-		events, err := s.f.KubeClientSet.CoreV1().Events(s.workloadNamespace).List(context.Background(), metav1.ListOptions{
-			FieldSelector: "involvedObject.name=" + externalSecretName + ",involvedObject.kind=ExternalSecret",
-		})
-		Expect(err).NotTo(HaveOccurred())
-		messages := make([]string, 0, len(events.Items))
-		for _, event := range events.Items {
-			if event.Message != "" {
-				messages = append(messages, event.Message)
-			}
-		}
-		return strings.Join(messages, "\n")
-	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(ContainSubstring(expectedMessage))
+func externalSecretConditionHasStatus(condition *esv1.ExternalSecretStatusCondition, want corev1.ConditionStatus) bool {
+	return condition != nil && condition.Status == want
 }
 
 func createE2ENamespace(f *framework.Framework, prefix string) string {

+ 46 - 444
e2e/suites/provider/cases/kubernetes/push_v2.go

@@ -19,26 +19,23 @@ package kubernetes
 import (
 	"context"
 	"fmt"
-	"strings"
 	"time"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
-	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 
 	"github.com/external-secrets/external-secrets-e2e/framework"
 	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
-	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-e2e/suites/provider/cases/common"
 )
 
 var _ = Describe("[kubernetes] v2 push secret", Label("kubernetes", "v2", "push-secret"), func() {
 	f := framework.New("eso-kubernetes-v2-push")
-	NewProvider(f)
+	prov := NewProvider(f)
+	harness := newKubernetesClusterProviderPushHarness(f)
 
 	BeforeEach(func() {
 		if !framework.IsV2ProviderMode() {
@@ -47,437 +44,58 @@ var _ = Describe("[kubernetes] v2 push secret", Label("kubernetes", "v2", "push-
 		frameworkv2.WaitForProviderConnectionReady(f, f.Namespace.Name, f.Namespace.Name, defaultV2WaitTimeout)
 	})
 
-	It("preserves source secret type, labels, and annotations when pushing to the namespaced Provider", func() {
-		sourceSecret := &corev1.Secret{
-			Type: corev1.SecretTypeDockerConfigJson,
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      "source-secret-metadata",
-				Namespace: f.Namespace.Name,
-				Labels: map[string]string{
-					"team": "platform",
-				},
-				Annotations: map[string]string{
-					"owner": "eso",
-				},
-			},
-			Data: map[string][]byte{
-				corev1.DockerConfigJsonKey: []byte(`{"auths":{"registry.example.com":{"auth":"ZXNvOnNlY3JldA=="}}}`),
-			},
-		}
-		Expect(f.CRClient.Create(context.Background(), sourceSecret)).To(Succeed())
-
-		pushSecret := &esv1alpha1.PushSecret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      "test-pushsecret-metadata",
-				Namespace: f.Namespace.Name,
-			},
-			Spec: esv1alpha1.PushSecretSpec{
-				RefreshInterval: &metav1.Duration{Duration: defaultV2RefreshInterval},
-				SecretStoreRefs: []esv1alpha1.PushSecretStoreRef{
-					{
-						Name:       f.Namespace.Name,
-						Kind:       f.DefaultPushSecretStoreRefKind,
-						APIVersion: f.DefaultPushSecretStoreRefAPIVersion,
-					},
-				},
-				Selector: esv1alpha1.PushSecretSelector{
-					Secret: &esv1alpha1.PushSecretSecret{
-						Name: sourceSecret.Name,
-					},
-				},
-				Data: []esv1alpha1.PushSecretData{{
-					Match: esv1alpha1.PushSecretMatch{
-						SecretKey: corev1.DockerConfigJsonKey,
-						RemoteRef: esv1alpha1.PushSecretRemoteRef{
-							RemoteKey: "pushed-docker-secret",
-							Property:  corev1.DockerConfigJsonKey,
-						},
-					},
-				}},
-			},
-		}
-		Expect(f.CRClient.Create(context.Background(), pushSecret)).To(Succeed())
-
-		waitForPushSecretReady(f, pushSecret.Name)
-
-		var pushedSecret corev1.Secret
-		Eventually(func(g Gomega) {
-			g.Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: "pushed-docker-secret", Namespace: f.Namespace.Name}, &pushedSecret)).To(Succeed())
-		}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
+	DescribeTable("push secret",
+		framework.TableFuncWithPushSecret(f, prov, nil),
+		Entry(common.PushSecretPreservesSourceMetadata(f)),
+		Entry(common.PushSecretImplicitProviderKind(f)),
+		Entry(common.PushSecretRejectsNamespacedRemoteNamespaceOverride(f)),
+		Entry(common.ClusterProviderPushManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderPushProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderPushManifestNamespaceRecovery(f, harness)),
+		Entry(common.ClusterProviderPushProviderNamespaceRecovery(f, harness)),
+		Entry(common.ClusterProviderPushAllowsRemoteNamespaceOverride(f, harness)),
+		Entry(common.ClusterProviderPushDeniedByConditions(f, harness)),
+	)
+})
 
-		Expect(pushedSecret.Type).To(Equal(corev1.SecretTypeDockerConfigJson))
-		Expect(pushedSecret.Labels).To(Equal(sourceSecret.Labels))
-		Expect(pushedSecret.Annotations).To(Equal(sourceSecret.Annotations))
-		Expect(pushedSecret.Data).To(Equal(sourceSecret.Data))
-	})
+func newKubernetesClusterProviderPushHarness(f *framework.Framework) common.ClusterProviderPushHarness {
+	return common.ClusterProviderPushHarness{
+		Prepare: func(tc *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderPushRuntime {
+			s := newClusterProviderV2Scenario(f, cfg.Name)
+			s.allowRemoteAccessForScope(cfg.AuthScope, cfg.Name)
 
-	It("supports namespaced Provider refs when kind is omitted", func() {
-		sourceSecret := &corev1.Secret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      "source-secret-implicit-kind",
-				Namespace: f.Namespace.Name,
-			},
-			Data: map[string][]byte{
-				"key1": []byte("value1"),
-			},
-		}
-		Expect(f.CRClient.Create(context.Background(), sourceSecret)).To(Succeed())
+			clusterProviderName := s.createClusterProvider(cfg.Name, cfg.AuthScope, cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
 
-		pushSecret := &esv1alpha1.PushSecret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      "test-pushsecret-implicit-kind",
-				Namespace: f.Namespace.Name,
-			},
-			Spec: esv1alpha1.PushSecretSpec{
-				RefreshInterval: &metav1.Duration{Duration: defaultV2RefreshInterval},
-				DeletionPolicy:  esv1alpha1.PushSecretDeletionPolicyDelete,
-				SecretStoreRefs: []esv1alpha1.PushSecretStoreRef{
-					{
-						Name:       f.Namespace.Name,
-						APIVersion: f.DefaultPushSecretStoreRefAPIVersion,
-					},
-				},
-				Selector: esv1alpha1.PushSecretSelector{
-					Secret: &esv1alpha1.PushSecretSecret{
-						Name: sourceSecret.Name,
-					},
-				},
-				Data: []esv1alpha1.PushSecretData{
-					{
-						Match: esv1alpha1.PushSecretMatch{
-							SecretKey: "key1",
-							RemoteRef: esv1alpha1.PushSecretRemoteRef{
-								RemoteKey: "pushed-secret-implicit-kind",
-								Property:  "key1",
-							},
-						},
-					},
+			return &common.ClusterProviderPushRuntime{
+				ClusterProviderName:    clusterProviderName,
+				DefaultRemoteNamespace: s.remoteNamespace,
+				BreakAuth: func() {
+					updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName(cfg.Name), "missing-service-account")
 				},
-			},
-		}
-		Expect(f.CRClient.Create(context.Background(), pushSecret)).To(Succeed())
-		waitForPushSecretReady(f, pushSecret.Name)
-
-		Eventually(func(g Gomega) {
-			var pushedSecret corev1.Secret
-			g.Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: "pushed-secret-implicit-kind", Namespace: f.Namespace.Name}, &pushedSecret)).To(Succeed())
-			g.Expect(string(pushedSecret.Data["key1"])).To(Equal("value1"))
-		}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
-
-		Expect(f.CRClient.Delete(context.Background(), pushSecret)).To(Succeed())
-
-		Eventually(func() bool {
-			var pushedSecret corev1.Secret
-			err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: "pushed-secret-implicit-kind", Namespace: f.Namespace.Name}, &pushedSecret)
-			return apierrors.IsNotFound(err)
-		}, defaultV2WaitTimeout, defaultV2PollInterval).Should(BeTrue())
-	})
-
-	It("rejects remote namespace overrides when pushing through a namespaced Provider", func() {
-		overrideNamespace := createE2ENamespace(f, "push-provider-override")
-		sourceSecret := &corev1.Secret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      "source-secret-provider-override",
-				Namespace: f.Namespace.Name,
-			},
-			Data: map[string][]byte{
-				"value": []byte("should-not-push"),
-			},
-		}
-		Expect(f.CRClient.Create(context.Background(), sourceSecret)).To(Succeed())
-
-		pushSecret := &esv1alpha1.PushSecret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      "test-pushsecret-provider-override",
-				Namespace: f.Namespace.Name,
-			},
-			Spec: esv1alpha1.PushSecretSpec{
-				RefreshInterval: &metav1.Duration{Duration: defaultV2RefreshInterval},
-				SecretStoreRefs: []esv1alpha1.PushSecretStoreRef{
-					{
-						Name:       f.Namespace.Name,
-						Kind:       f.DefaultPushSecretStoreRefKind,
-						APIVersion: f.DefaultPushSecretStoreRefAPIVersion,
-					},
+				RepairAuth: func() {
+					updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName(cfg.Name), s.serviceAccount)
 				},
-				Selector: esv1alpha1.PushSecretSelector{
-					Secret: &esv1alpha1.PushSecretSecret{
-						Name: sourceSecret.Name,
-					},
+				WaitForRemoteSecretValue: func(namespace, name, key, expectedValue string) {
+					waitForSecretValueInNamespace(f, namespace, name, key, expectedValue)
 				},
-				Data: []esv1alpha1.PushSecretData{{
-					Match: esv1alpha1.PushSecretMatch{
-						SecretKey: "value",
-						RemoteRef: esv1alpha1.PushSecretRemoteRef{
-							RemoteKey: "pushed-secret-provider-override",
-							Property:  "value",
-						},
-					},
-					Metadata: &apiextensionsv1.JSON{Raw: []byte(fmt.Sprintf(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"remoteNamespace":"%s"}}`, overrideNamespace))},
-				}},
-			},
-		}
-		Expect(f.CRClient.Create(context.Background(), pushSecret)).To(Succeed())
-
-		waitForPushSecretErrored(f, pushSecret.Name)
-		expectNoSecretInNamespace(f, f.Namespace.Name, "pushed-secret-provider-override")
-		expectNoSecretInNamespace(f, overrideNamespace, "pushed-secret-provider-override")
-		expectPushSecretEvent(f, f.Namespace.Name, pushSecret.Name, "remoteNamespace override is only supported with ClusterSecretStore")
-	})
-
-	It("pushes through a ClusterProvider when authenticationScope=ManifestNamespace", func() {
-		s := newClusterProviderV2Scenario(f, "push-manifest")
-		s.allowRemoteAccessFrom(s.workloadNamespace, "push-manifest")
-
-		sourceSecretName := fmt.Sprintf("%s-source", s.namePrefix)
-		remoteSecretName := fmt.Sprintf("%s-remote", s.namePrefix)
-		Expect(f.CRClient.Create(context.Background(), &corev1.Secret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      sourceSecretName,
-				Namespace: s.workloadNamespace,
-			},
-			Data: map[string][]byte{
-				"value": []byte("manifest-push-value"),
-			},
-		})).To(Succeed())
-
-		clusterProviderName := s.createClusterProvider("push-manifest", esv1.AuthenticationScopeManifestNamespace, nil)
-		frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
-
-		pushSecretName := createClusterProviderPushSecret(f, s.workloadNamespace, clusterProviderName, sourceSecretName, remoteSecretName)
-		waitForPushSecretReady(f, pushSecretName)
-		waitForSecretValueInNamespace(f, s.remoteNamespace, remoteSecretName, "value", "manifest-push-value")
-	})
-
-	It("pushes through a ClusterProvider when authenticationScope=ProviderNamespace", func() {
-		s := newClusterProviderV2Scenario(f, "push-provider")
-		s.allowRemoteAccessFrom(s.providerNamespace, "push-provider")
-
-		sourceSecretName := fmt.Sprintf("%s-source", s.namePrefix)
-		remoteSecretName := fmt.Sprintf("%s-remote", s.namePrefix)
-		Expect(f.CRClient.Create(context.Background(), &corev1.Secret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      sourceSecretName,
-				Namespace: s.workloadNamespace,
-			},
-			Data: map[string][]byte{
-				"value": []byte("provider-push-value"),
-			},
-		})).To(Succeed())
-
-		clusterProviderName := s.createClusterProvider("push-provider", esv1.AuthenticationScopeProviderNamespace, nil)
-		frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
-
-		pushSecretName := createClusterProviderPushSecret(f, s.workloadNamespace, clusterProviderName, sourceSecretName, remoteSecretName)
-		waitForPushSecretReady(f, pushSecretName)
-		waitForSecretValueInNamespace(f, s.remoteNamespace, remoteSecretName, "value", "provider-push-value")
-	})
-
-	It("recovers after repairing cluster provider auth for pushes when authenticationScope=ManifestNamespace", func() {
-		s := newClusterProviderV2Scenario(f, "push-manifest-recovery")
-		s.allowRemoteAccessFrom(s.workloadNamespace, "push-manifest-recovery")
-
-		sourceSecretName := fmt.Sprintf("%s-source", s.namePrefix)
-		remoteSecretName := fmt.Sprintf("%s-remote", s.namePrefix)
-		Expect(f.CRClient.Create(context.Background(), &corev1.Secret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      sourceSecretName,
-				Namespace: s.workloadNamespace,
-			},
-			Data: map[string][]byte{
-				"value": []byte("manifest-push-recovered"),
-			},
-		})).To(Succeed())
-
-		clusterProviderName := s.createClusterProvider("push-manifest-recovery", esv1.AuthenticationScopeManifestNamespace, nil)
-		frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
-
-		updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName("push-manifest-recovery"), "missing-service-account")
-
-		pushSecretName := createClusterProviderPushSecretWithRefresh(f, s.workloadNamespace, clusterProviderName, sourceSecretName, remoteSecretName, time.Hour)
-		waitForPushSecretErrored(f, pushSecretName)
-		expectNoSecretInNamespace(f, s.remoteNamespace, remoteSecretName)
-
-		updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName("push-manifest-recovery"), s.serviceAccount)
-
-		waitForPushSecretReady(f, pushSecretName)
-		waitForSecretValueInNamespace(f, s.remoteNamespace, remoteSecretName, "value", "manifest-push-recovered")
-	})
-
-	It("recovers after repairing cluster provider auth for pushes when authenticationScope=ProviderNamespace", func() {
-		s := newClusterProviderV2Scenario(f, "push-provider-recovery")
-		s.allowRemoteAccessFrom(s.providerNamespace, "push-provider-recovery")
-
-		sourceSecretName := fmt.Sprintf("%s-source", s.namePrefix)
-		remoteSecretName := fmt.Sprintf("%s-remote", s.namePrefix)
-		Expect(f.CRClient.Create(context.Background(), &corev1.Secret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      sourceSecretName,
-				Namespace: s.workloadNamespace,
-			},
-			Data: map[string][]byte{
-				"value": []byte("provider-push-recovered"),
-			},
-		})).To(Succeed())
-
-		clusterProviderName := s.createClusterProvider("push-provider-recovery", esv1.AuthenticationScopeProviderNamespace, nil)
-		frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
-
-		updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName("push-provider-recovery"), "missing-service-account")
-
-		pushSecretName := createClusterProviderPushSecretWithRefresh(f, s.workloadNamespace, clusterProviderName, sourceSecretName, remoteSecretName, time.Hour)
-		waitForPushSecretErrored(f, pushSecretName)
-		expectNoSecretInNamespace(f, s.remoteNamespace, remoteSecretName)
-
-		updateKubernetesProviderServiceAccount(f, s.providerNamespace, s.providerConfigName("push-provider-recovery"), s.serviceAccount)
-
-		waitForPushSecretReady(f, pushSecretName)
-		waitForSecretValueInNamespace(f, s.remoteNamespace, remoteSecretName, "value", "provider-push-recovered")
-	})
-
-	It("allows ClusterProvider pushes to override the target remote namespace via metadata", func() {
-		s := newClusterProviderV2Scenario(f, "push-remote-override")
-		s.allowRemoteAccessFrom(s.workloadNamespace, "push-remote-override-default")
-
-		overrideNamespace := createE2ENamespace(f, "push-remote-override-target")
-		frameworkv2.CreateKubernetesAccessRole(
-			f,
-			fmt.Sprintf("%s-access-%s", s.namePrefix, "push-remote-override-target"),
-			s.serviceAccount,
-			s.workloadNamespace,
-			overrideNamespace,
-		)
-
-		sourceSecretName := fmt.Sprintf("%s-source", s.namePrefix)
-		remoteSecretName := fmt.Sprintf("%s-remote", s.namePrefix)
-		Expect(f.CRClient.Create(context.Background(), &corev1.Secret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      sourceSecretName,
-				Namespace: s.workloadNamespace,
-			},
-			Data: map[string][]byte{
-				"value": []byte("override-push-value"),
-			},
-		})).To(Succeed())
-
-		clusterProviderName := s.createClusterProvider("push-remote-override", esv1.AuthenticationScopeManifestNamespace, nil)
-		frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
-
-		pushSecretName := createClusterProviderPushSecretWithMetadata(
-			f,
-			s.workloadNamespace,
-			clusterProviderName,
-			sourceSecretName,
-			remoteSecretName,
-			&apiextensionsv1.JSON{Raw: []byte(fmt.Sprintf(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"remoteNamespace":"%s"}}`, overrideNamespace))},
-		)
-		waitForPushSecretReady(f, pushSecretName)
-		waitForSecretValueInNamespace(f, overrideNamespace, remoteSecretName, "value", "override-push-value")
-		expectNoSecretInNamespace(f, s.remoteNamespace, remoteSecretName)
-	})
-
-	It("denies PushSecrets from namespaces that do not match ClusterProvider conditions", func() {
-		s := newClusterProviderV2Scenario(f, "push-deny")
-		s.allowRemoteAccessFrom(s.workloadNamespace, "push-deny")
-
-		sourceSecretName := fmt.Sprintf("%s-source", s.namePrefix)
-		remoteSecretName := fmt.Sprintf("%s-remote", s.namePrefix)
-		Expect(f.CRClient.Create(context.Background(), &corev1.Secret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      sourceSecretName,
-				Namespace: s.workloadNamespace,
-			},
-			Data: map[string][]byte{
-				"value": []byte("should-not-push"),
-			},
-		})).To(Succeed())
-
-		clusterProviderName := s.createClusterProvider("push-deny", esv1.AuthenticationScopeManifestNamespace, []esv1.ClusterSecretStoreCondition{{
-			Namespaces: []string{"not-" + s.workloadNamespace},
-		}})
-		frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
-
-		pushSecretName := createClusterProviderPushSecret(f, s.workloadNamespace, clusterProviderName, sourceSecretName, remoteSecretName)
-		waitForPushSecretErrored(f, pushSecretName)
-		expectNoSecretInNamespace(f, s.remoteNamespace, remoteSecretName)
-		expectPushSecretEvent(f, s.workloadNamespace, pushSecretName, fmt.Sprintf("using ClusterProvider %q is not allowed from namespace %q: denied by spec.conditions", clusterProviderName, s.workloadNamespace))
-	})
-})
-
-func createClusterProviderPushSecret(f *framework.Framework, namespace, clusterProviderName, sourceSecretName, remoteSecretName string) string {
-	return createClusterProviderPushSecretWithRefreshAndMetadata(f, namespace, clusterProviderName, sourceSecretName, remoteSecretName, defaultV2RefreshInterval, nil)
-}
-
-func createClusterProviderPushSecretWithRefresh(f *framework.Framework, namespace, clusterProviderName, sourceSecretName, remoteSecretName string, refreshInterval time.Duration) string {
-	return createClusterProviderPushSecretWithRefreshAndMetadata(f, namespace, clusterProviderName, sourceSecretName, remoteSecretName, refreshInterval, nil)
-}
-
-func createClusterProviderPushSecretWithMetadata(f *framework.Framework, namespace, clusterProviderName, sourceSecretName, remoteSecretName string, metadata *apiextensionsv1.JSON) string {
-	return createClusterProviderPushSecretWithRefreshAndMetadata(f, namespace, clusterProviderName, sourceSecretName, remoteSecretName, defaultV2RefreshInterval, metadata)
-}
-
-func createClusterProviderPushSecretWithRefreshAndMetadata(f *framework.Framework, namespace, clusterProviderName, sourceSecretName, remoteSecretName string, refreshInterval time.Duration, metadata *apiextensionsv1.JSON) string {
-	pushSecretName := fmt.Sprintf("%s-push-secret", remoteSecretName)
-	Expect(f.CRClient.Create(context.Background(), &esv1alpha1.PushSecret{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      pushSecretName,
-			Namespace: namespace,
-		},
-		Spec: esv1alpha1.PushSecretSpec{
-			RefreshInterval: &metav1.Duration{Duration: refreshInterval},
-			SecretStoreRefs: []esv1alpha1.PushSecretStoreRef{{
-				Name:       clusterProviderName,
-				Kind:       esv1.ClusterProviderKindStr,
-				APIVersion: esv1.SchemeGroupVersion.String(),
-			}},
-			Selector: esv1alpha1.PushSecretSelector{
-				Secret: &esv1alpha1.PushSecretSecret{
-					Name: sourceSecretName,
+				ExpectNoRemoteSecret: func(namespace, name string) {
+					expectNoSecretInNamespace(f, namespace, name)
 				},
-			},
-			Data: []esv1alpha1.PushSecretData{{
-				Match: esv1alpha1.PushSecretMatch{
-					SecretKey: "value",
-					RemoteRef: esv1alpha1.PushSecretRemoteRef{
-						RemoteKey: remoteSecretName,
-						Property:  "value",
-					},
+				CreateWritableRemoteScope: func(prefix string) string {
+					namespace := createE2ENamespace(f, prefix)
+					frameworkv2.CreateKubernetesAccessRole(
+						f,
+						fmt.Sprintf("%s-access-%s", s.namePrefix, prefix),
+						s.serviceAccount,
+						s.workloadNamespace,
+						namespace,
+					)
+					return namespace
 				},
-				Metadata: metadata,
-			}},
-		},
-	})).To(Succeed())
-	return pushSecretName
-}
-
-func waitForPushSecretReady(f *framework.Framework, name string) {
-	Eventually(func(g Gomega) {
-		var ps esv1alpha1.PushSecret
-		g.Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: name, Namespace: f.Namespace.Name}, &ps)).To(Succeed())
-		g.Expect(ps.Status.Conditions).NotTo(BeEmpty())
-		ready := false
-		for _, condition := range ps.Status.Conditions {
-			if condition.Type == esv1alpha1.PushSecretReady && condition.Status == corev1.ConditionTrue {
-				ready = true
-			}
-		}
-		g.Expect(ready).To(BeTrue())
-	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
-}
-
-func waitForPushSecretErrored(f *framework.Framework, name string) {
-	Eventually(func(g Gomega) {
-		var ps esv1alpha1.PushSecret
-		g.Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: name, Namespace: f.Namespace.Name}, &ps)).To(Succeed())
-		g.Expect(ps.Status.Conditions).NotTo(BeEmpty())
-		errored := false
-		for _, condition := range ps.Status.Conditions {
-			if condition.Type == esv1alpha1.PushSecretReady && condition.Status == corev1.ConditionFalse {
-				errored = true
 			}
-		}
-		g.Expect(errored).To(BeTrue())
-	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
+		},
+	}
 }
 
 func waitForSecretValueInNamespace(f *framework.Framework, namespace, name, key, expectedValue string) {
@@ -495,19 +113,3 @@ func expectNoSecretInNamespace(f *framework.Framework, namespace, name string) {
 		return apierrors.IsNotFound(err)
 	}, 10*time.Second, defaultV2PollInterval).Should(BeTrue())
 }
-
-func expectPushSecretEvent(f *framework.Framework, namespace, pushSecretName, expectedMessage string) {
-	Eventually(func() string {
-		events, err := f.KubeClientSet.CoreV1().Events(namespace).List(context.Background(), metav1.ListOptions{
-			FieldSelector: "involvedObject.name=" + pushSecretName + ",involvedObject.kind=PushSecret",
-		})
-		Expect(err).NotTo(HaveOccurred())
-		messages := make([]string, 0, len(events.Items))
-		for _, event := range events.Items {
-			if event.Message != "" {
-				messages = append(messages, event.Message)
-			}
-		}
-		return strings.Join(messages, "\n")
-	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(ContainSubstring(expectedMessage))
-}