Sfoglia il codice sorgente

test(e2e): share namespaced provider v2 cases

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 2 mesi fa
parent
commit
629bd845fd

+ 75 - 0
e2e/suites/provider/cases/common/fake_provider.go

@@ -0,0 +1,75 @@
+/*
+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 (
+	"fmt"
+	"time"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+)
+
+func FakeProviderSync(f *framework.Framework) (string, func(*framework.TestCase)) {
+	remoteKey := fmt.Sprintf("fake-sync-%s", f.Namespace.Name)
+	return NamespacedProviderSync(f, NamespacedProviderSyncConfig{
+		Description:        "[fake] should sync a namespaced secret",
+		ExternalSecretName: "fake-sync-es",
+		TargetSecretName:   "fake-sync-target",
+		RemoteKey:          remoteKey,
+		RemoteSecretValue:  `{"value":"fake-sync-value"}`,
+		RemoteProperty:     "value",
+		SecretKey:          "value",
+		ExpectedValue:      "fake-sync-value",
+	})
+}
+
+func FakeProviderRefresh(f *framework.Framework) (string, func(*framework.TestCase)) {
+	remoteKey := fmt.Sprintf("fake-refresh-%s", f.Namespace.Name)
+	return NamespacedProviderRefresh(f, NamespacedProviderRefreshConfig{
+		Description:         "[fake] should refresh after the provider data changes",
+		ExternalSecretName:  "fake-refresh-es",
+		TargetSecretName:    "fake-refresh-target",
+		RemoteKey:           remoteKey,
+		InitialSecretValue:  `{"value":"fake-initial-value"}`,
+		UpdatedSecretValue:  `{"value":"fake-updated-value"}`,
+		RemoteProperty:      "value",
+		SecretKey:           "value",
+		InitialExpectedData: "fake-initial-value",
+		UpdatedExpectedData: "fake-updated-value",
+		RefreshInterval:     10 * time.Second,
+		WaitTimeout:         30 * time.Second,
+	})
+}
+
+func FakeProviderFind(f *framework.Framework) (string, func(*framework.TestCase)) {
+	remoteKeyOne := fmt.Sprintf("fake-find-%s-one", f.Namespace.Name)
+	remoteKeyTwo := fmt.Sprintf("fake-find-%s-two", f.Namespace.Name)
+	remoteKeyThree := fmt.Sprintf("fake-find-ignore-%s", f.Namespace.Name)
+	return NamespacedProviderFind(f, NamespacedProviderFindConfig{
+		Description:        "[fake] should sync dataFrom.find matches",
+		ExternalSecretName: "fake-find-es",
+		TargetSecretName:   "fake-find-target",
+		MatchRegExp:        fmt.Sprintf("fake-find-%s-(one|two)", f.Namespace.Name),
+		MatchingSecrets: map[string]string{
+			remoteKeyOne: `{"value":"fake-find-one"}`,
+			remoteKeyTwo: `{"value":"fake-find-two"}`,
+		},
+		IgnoredSecrets: map[string]string{
+			remoteKeyThree: `{"value":"fake-ignore"}`,
+		},
+	})
+}

+ 154 - 0
e2e/suites/provider/cases/common/namespaced_provider.go

@@ -0,0 +1,154 @@
+/*
+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 (
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+type NamespacedProviderSyncConfig struct {
+	Description        string
+	ExternalSecretName string
+	TargetSecretName   string
+	RemoteKey          string
+	RemoteSecretValue  string
+	RemoteProperty     string
+	SecretKey          string
+	ExpectedValue      string
+}
+
+func NamespacedProviderSync(_ *framework.Framework, cfg NamespacedProviderSyncConfig) (string, func(*framework.TestCase)) {
+	return cfg.Description, func(tc *framework.TestCase) {
+		tc.ExternalSecret.ObjectMeta.Name = cfg.ExternalSecretName
+		tc.ExternalSecret.Spec.Target.Name = cfg.TargetSecretName
+		tc.Secrets = map[string]framework.SecretEntry{
+			cfg.RemoteKey: {Value: cfg.RemoteSecretValue},
+		}
+		tc.ExpectedSecret = &corev1.Secret{
+			Type: corev1.SecretTypeOpaque,
+			Data: map[string][]byte{
+				cfg.SecretKey: []byte(cfg.ExpectedValue),
+			},
+		}
+		tc.ExternalSecret.Spec.Data = []esv1.ExternalSecretData{{
+			SecretKey: cfg.SecretKey,
+			RemoteRef: esv1.ExternalSecretDataRemoteRef{
+				Key:      cfg.RemoteKey,
+				Property: cfg.RemoteProperty,
+			},
+		}}
+	}
+}
+
+type NamespacedProviderRefreshConfig struct {
+	Description         string
+	ExternalSecretName  string
+	TargetSecretName    string
+	RemoteKey           string
+	InitialSecretValue  string
+	UpdatedSecretValue  string
+	RemoteProperty      string
+	SecretKey           string
+	InitialExpectedData string
+	UpdatedExpectedData string
+	RefreshInterval     time.Duration
+	WaitTimeout         time.Duration
+}
+
+func NamespacedProviderRefresh(_ *framework.Framework, cfg NamespacedProviderRefreshConfig) (string, func(*framework.TestCase)) {
+	return cfg.Description, func(tc *framework.TestCase) {
+		waitTimeout := cfg.WaitTimeout
+		if waitTimeout == 0 {
+			waitTimeout = 30 * time.Second
+		}
+
+		tc.ExternalSecret.ObjectMeta.Name = cfg.ExternalSecretName
+		tc.ExternalSecret.Spec.Target.Name = cfg.TargetSecretName
+		tc.ExternalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: cfg.RefreshInterval}
+		tc.Secrets = map[string]framework.SecretEntry{
+			cfg.RemoteKey: {Value: cfg.InitialSecretValue},
+		}
+		tc.ExpectedSecret = &corev1.Secret{
+			Type: corev1.SecretTypeOpaque,
+			Data: map[string][]byte{
+				cfg.SecretKey: []byte(cfg.InitialExpectedData),
+			},
+		}
+		tc.ExternalSecret.Spec.Data = []esv1.ExternalSecretData{{
+			SecretKey: cfg.SecretKey,
+			RemoteRef: esv1.ExternalSecretDataRemoteRef{
+				Key:      cfg.RemoteKey,
+				Property: cfg.RemoteProperty,
+			},
+		}}
+		tc.AfterSync = func(prov framework.SecretStoreProvider, _ *corev1.Secret) {
+			prov.CreateSecret(cfg.RemoteKey, framework.SecretEntry{
+				Value: cfg.UpdatedSecretValue,
+			})
+			waitForSecretData(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Spec.Target.Name, map[string][]byte{
+				cfg.SecretKey: []byte(cfg.UpdatedExpectedData),
+			}, waitTimeout)
+		}
+	}
+}
+
+type NamespacedProviderFindConfig struct {
+	Description        string
+	ExternalSecretName string
+	TargetSecretName   string
+	MatchRegExp        string
+	MatchingSecrets    map[string]string
+	IgnoredSecrets     map[string]string
+}
+
+func NamespacedProviderFind(_ *framework.Framework, cfg NamespacedProviderFindConfig) (string, func(*framework.TestCase)) {
+	return cfg.Description, func(tc *framework.TestCase) {
+		secrets := make(map[string]framework.SecretEntry, len(cfg.MatchingSecrets)+len(cfg.IgnoredSecrets))
+		for key, value := range cfg.MatchingSecrets {
+			secrets[key] = framework.SecretEntry{Value: value}
+		}
+		for key, value := range cfg.IgnoredSecrets {
+			secrets[key] = framework.SecretEntry{Value: value}
+		}
+
+		expectedData := make(map[string][]byte, len(cfg.MatchingSecrets))
+		for key, value := range cfg.MatchingSecrets {
+			expectedData[key] = []byte(value)
+		}
+
+		tc.ExternalSecret.ObjectMeta.Name = cfg.ExternalSecretName
+		tc.ExternalSecret.Spec.Target.Name = cfg.TargetSecretName
+		tc.Secrets = secrets
+		tc.ExpectedSecret = &corev1.Secret{
+			Type: corev1.SecretTypeOpaque,
+			Data: expectedData,
+		}
+		tc.ExternalSecret.Spec.DataFrom = []esv1.ExternalSecretDataFromRemoteRef{{
+			Find: &esv1.ExternalSecretFind{
+				Name: &esv1.FindName{
+					RegExp: cfg.MatchRegExp,
+				},
+			},
+		}}
+	}
+}

+ 72 - 11
e2e/suites/provider/cases/fake/provider.go

@@ -17,7 +17,8 @@ limitations under the License.
 package fake
 
 import (
-	"encoding/json"
+	"fmt"
+	"time"
 
 	// nolint
 	. "github.com/onsi/ginkgo/v2"
@@ -29,6 +30,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/external-secrets/external-secrets-e2e/framework"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 )
 
@@ -53,9 +55,7 @@ func (s *Provider) CreateSecret(key string, val framework.SecretEntry) {
 	Expect(err).ToNot(HaveOccurred())
 	base := store.DeepCopy()
 
-	mapData := make(map[string]string)
-	_ = json.Unmarshal([]byte(val.Value), &mapData)
-	store.Spec.Provider.Fake.Data = append(store.Spec.Provider.Fake.Data, esv1.FakeProviderData{
+	store.Spec.Provider.Fake.Data = upsertFakeProviderData(store.Spec.Provider.Fake.Data, esv1.FakeProviderData{
 		Key:   key,
 		Value: val.Value,
 	})
@@ -75,13 +75,7 @@ func (s *Provider) DeleteSecret(key string) {
 	}, &store)
 	Expect(err).ToNot(HaveOccurred())
 	base := store.DeepCopy()
-	data := make([]esv1.FakeProviderData, 0)
-	for _, v := range store.Spec.Provider.Fake.Data {
-		if v.Key != key {
-			data = append(data, v)
-		}
-	}
-	store.Spec.Provider.Fake.Data = data
+	store.Spec.Provider.Fake.Data = removeFakeProviderData(store.Spec.Provider.Fake.Data, key, "")
 	err = s.framework.CRClient.Patch(GinkgoT().Context(), &store, client.MergeFrom(base))
 	Expect(err).ToNot(HaveOccurred())
 }
@@ -105,3 +99,70 @@ func (s *Provider) CreateStore() {
 	err := s.framework.CRClient.Create(GinkgoT().Context(), fakeStore)
 	Expect(err).ToNot(HaveOccurred())
 }
+
+func upsertFakeProviderData(data []esv1.FakeProviderData, entry esv1.FakeProviderData) []esv1.FakeProviderData {
+	for i := range data {
+		if data[i].Key == entry.Key && data[i].Version == entry.Version {
+			data[i] = entry
+			return data
+		}
+	}
+	return append(data, entry)
+}
+
+func removeFakeProviderData(data []esv1.FakeProviderData, key, version string) []esv1.FakeProviderData {
+	filtered := make([]esv1.FakeProviderData, 0, len(data))
+	for _, entry := range data {
+		if entry.Key == key && entry.Version == version {
+			continue
+		}
+		filtered = append(filtered, entry)
+	}
+	return filtered
+}
+
+func namespacedProviderSyncCase(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return common.NamespacedProviderSync(f, common.NamespacedProviderSyncConfig{
+		Description:        "[fake] should sync a namespaced secret",
+		ExternalSecretName: "fake-sync-es",
+		TargetSecretName:   "fake-sync-target",
+		RemoteKey:          fmt.Sprintf("fake-sync-%s", f.Namespace.Name),
+		RemoteSecretValue:  `{"value":"fake-sync-value"}`,
+		RemoteProperty:     "value",
+		SecretKey:          "value",
+		ExpectedValue:      "fake-sync-value",
+	})
+}
+
+func namespacedProviderRefreshCase(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return common.NamespacedProviderRefresh(f, common.NamespacedProviderRefreshConfig{
+		Description:         "[fake] should refresh after the provider data changes",
+		ExternalSecretName:  "fake-refresh-es",
+		TargetSecretName:    "fake-refresh-target",
+		RemoteKey:           fmt.Sprintf("fake-refresh-%s", f.Namespace.Name),
+		InitialSecretValue:  `{"value":"fake-initial-value"}`,
+		UpdatedSecretValue:  `{"value":"fake-updated-value"}`,
+		RemoteProperty:      "value",
+		SecretKey:           "value",
+		InitialExpectedData: "fake-initial-value",
+		UpdatedExpectedData: "fake-updated-value",
+		RefreshInterval:     defaultV2RefreshInterval,
+		WaitTimeout:         30 * time.Second,
+	})
+}
+
+func namespacedProviderFindCase(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return common.NamespacedProviderFind(f, common.NamespacedProviderFindConfig{
+		Description:        "[fake] should sync dataFrom.find matches",
+		ExternalSecretName: "fake-find-es",
+		TargetSecretName:   "fake-find-target",
+		MatchRegExp:        fmt.Sprintf("fake-find-%s-(one|two)", f.Namespace.Name),
+		MatchingSecrets: map[string]string{
+			fmt.Sprintf("fake-find-%s-one", f.Namespace.Name): `{"value":"fake-find-one"}`,
+			fmt.Sprintf("fake-find-%s-two", f.Namespace.Name): `{"value":"fake-find-two"}`,
+		},
+		IgnoredSecrets: map[string]string{
+			fmt.Sprintf("fake-find-ignore-%s", f.Namespace.Name): `{"value":"fake-ignore"}`,
+		},
+	})
+}

+ 431 - 0
e2e/suites/provider/cases/fake/provider_v2.go

@@ -0,0 +1,431 @@
+/*
+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 fake
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "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"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"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"
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	fakev2alpha1 "github.com/external-secrets/external-secrets/apis/provider/fake/v2alpha1"
+)
+
+const (
+	fakeProviderAPIVersion   = "provider.external-secrets.io/v2alpha1"
+	fakeProviderKind         = "Fake"
+	defaultV2WaitTimeout     = 60 * time.Second
+	defaultV2PollInterval    = 2 * time.Second
+	defaultV2RefreshInterval = 10 * time.Second
+)
+
+var _ = Describe("[fake] v2 namespaced provider", Label("fake", "v2", "namespaced-provider"), func() {
+	f := framework.New("eso-fake-v2-provider")
+	prov := NewProviderV2(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("namespaced provider",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(namespacedProviderSyncCase(f)),
+		Entry(namespacedProviderRefreshCase(f)),
+		Entry(namespacedProviderFindCase(f)),
+		Entry(common.StatusNotUpdatedAfterSuccessfulSync(f)),
+	)
+})
+
+var _ = Describe("[fake] v2 cluster provider", Label("fake", "v2", "cluster-provider"), func() {
+	f := framework.New("eso-fake-v2-clusterprovider")
+	prov := NewProviderV2(f)
+	harness := newFakeClusterProviderExternalSecretHarness(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("cluster provider external secrets",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(common.ClusterProviderManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderDeniedByConditions(f, harness)),
+	)
+})
+
+var _ = Describe("[fake] v2 push secret", Label("fake", "v2", "push-secret"), func() {
+	f := framework.New("eso-fake-v2-push")
+	prov := NewProviderV2(f)
+	harness := newFakeClusterProviderPushHarness(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("push secret",
+		framework.TableFuncWithPushSecret(f, prov, nil),
+		// The fake backend stores raw pushed values in provider memory, so provider-
+		// specific metadata validation such as namespaced remoteNamespace rejection
+		// is not meaningful here.
+		Entry(fakePushSecretImplicitProviderKind(f)),
+		Entry(common.ClusterProviderPushManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderPushProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderPushDeniedByConditions(f, harness)),
+	)
+})
+
+type ProviderV2 struct {
+	framework *framework.Framework
+}
+
+func NewProviderV2(f *framework.Framework) *ProviderV2 {
+	prov := &ProviderV2{
+		framework: f,
+	}
+	BeforeEach(prov.BeforeEach)
+	return prov
+}
+
+func (s *ProviderV2) BeforeEach() {
+	if !framework.IsV2ProviderMode() {
+		return
+	}
+
+	s.createStore()
+	frameworkv2.WaitForProviderConnectionReady(s.framework, s.framework.Namespace.Name, s.framework.Namespace.Name, defaultV2WaitTimeout)
+}
+
+func (s *ProviderV2) CreateSecret(key string, val framework.SecretEntry) {
+	s.updateStore(func(fake *fakev2alpha1.Fake) {
+		fake.Spec.Data = upsertFakeProviderData(fake.Spec.Data, esv1.FakeProviderData{
+			Key:   key,
+			Value: val.Value,
+		})
+	})
+}
+
+func (s *ProviderV2) DeleteSecret(key string) {
+	s.updateStore(func(fake *fakev2alpha1.Fake) {
+		fake.Spec.Data = removeFakeProviderData(fake.Spec.Data, key, "")
+	})
+}
+
+func (s *ProviderV2) createStore() {
+	createFakeProviderConfig(s.framework, s.framework.Namespace.Name, s.framework.Namespace.Name)
+	frameworkv2.CreateProviderConnection(
+		s.framework,
+		s.framework.Namespace.Name,
+		s.framework.Namespace.Name,
+		frameworkv2.ProviderAddress("fake"),
+		fakeProviderAPIVersion,
+		fakeProviderKind,
+		s.framework.Namespace.Name,
+		"",
+	)
+}
+
+func (s *ProviderV2) updateStore(mutate func(*fakev2alpha1.Fake)) {
+	updateFakeProviderConfig(s.framework, s.framework.Namespace.Name, s.framework.Namespace.Name, mutate)
+}
+
+type fakeClusterProviderScenario struct {
+	f                    *framework.Framework
+	namePrefix           string
+	authScope            esv1.AuthenticationScope
+	fakeConfigNamespace  string
+	providerRefNamespace string
+	configName           string
+}
+
+func newFakeClusterProviderScenario(f *framework.Framework, prefix string, authScope esv1.AuthenticationScope) *fakeClusterProviderScenario {
+	providerNamespace := f.Namespace.Name
+	if authScope == esv1.AuthenticationScopeProviderNamespace {
+		providerNamespace = createE2ENamespace(f, prefix+"-provider")
+	}
+
+	s := &fakeClusterProviderScenario{
+		f:                    f,
+		namePrefix:           fmt.Sprintf("%s-%s", f.Namespace.Name, prefix),
+		authScope:            authScope,
+		fakeConfigNamespace:  fakeConfigNamespaceForAuthScope(authScope, f.Namespace.Name, providerNamespace),
+		providerRefNamespace: providerReferenceNamespace(authScope, providerNamespace),
+		configName:           fmt.Sprintf("%s-config", prefix),
+	}
+	createFakeProviderConfig(s.f, s.fakeConfigNamespace, s.configName)
+	return s
+}
+
+func (s *fakeClusterProviderScenario) createClusterProvider(conditions []esv1.ClusterSecretStoreCondition) string {
+	clusterProviderName := fmt.Sprintf("%s-cluster-provider", s.namePrefix)
+	frameworkv2.CreateClusterProviderConnection(
+		s.f,
+		clusterProviderName,
+		frameworkv2.ProviderAddress("fake"),
+		fakeProviderAPIVersion,
+		fakeProviderKind,
+		s.configName,
+		s.providerRefNamespace,
+		s.authScope,
+		conditions,
+	)
+	return clusterProviderName
+}
+
+func (s *fakeClusterProviderScenario) CreateSecret(key string, val framework.SecretEntry) {
+	updateFakeProviderConfig(s.f, s.fakeConfigNamespace, s.configName, func(fake *fakev2alpha1.Fake) {
+		fake.Spec.Data = upsertFakeProviderData(fake.Spec.Data, esv1.FakeProviderData{
+			Key:   key,
+			Value: val.Value,
+		})
+	})
+}
+
+func (s *fakeClusterProviderScenario) DeleteSecret(key string) {
+	updateFakeProviderConfig(s.f, s.fakeConfigNamespace, s.configName, func(fake *fakev2alpha1.Fake) {
+		fake.Spec.Data = removeFakeProviderData(fake.Spec.Data, key, "")
+	})
+}
+
+func newFakeClusterProviderExternalSecretHarness(f *framework.Framework) common.ClusterProviderExternalSecretHarness {
+	return common.ClusterProviderExternalSecretHarness{
+		Prepare: func(tc *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderExternalSecretRuntime {
+			s := newFakeClusterProviderScenario(f, cfg.Name, cfg.AuthScope)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.ClusterProviderExternalSecretRuntime{
+				ClusterProviderName: clusterProviderName,
+				Provider:            s,
+				BreakAuth:           func() {},
+				RepairAuth:          func() {},
+			}
+		},
+	}
+}
+
+func newFakeClusterProviderPushHarness(f *framework.Framework) common.ClusterProviderPushHarness {
+	return common.ClusterProviderPushHarness{
+		Prepare: func(tc *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderPushRuntime {
+			s := newFakeClusterProviderScenario(f, cfg.Name, cfg.AuthScope)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.ClusterProviderPushRuntime{
+				ClusterProviderName:    clusterProviderName,
+				DefaultRemoteNamespace: s.fakeConfigNamespace,
+				BreakAuth:              func() {},
+				RepairAuth:             func() {},
+				WaitForRemoteSecretValue: func(_, name, _ string, expectedValue string) {
+					waitForPushedValueViaExternalSecret(f, esv1.SecretStoreRef{
+						Name: clusterProviderName,
+						Kind: esv1.ClusterProviderKindStr,
+					}, name, expectedValue)
+				},
+				ExpectNoRemoteSecret:      func(string, string) {},
+				CreateWritableRemoteScope: func(prefix string) string { return createE2ENamespace(f, prefix) },
+			}
+		},
+	}
+}
+
+func fakePushSecretImplicitProviderKind(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[fake] should support namespaced Provider refs when push kind is omitted", func(tc *framework.TestCase) {
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "fake-push-implicit-kind-source",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("implicit-kind-value"),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "fake-push-implicit-kind"
+		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: "value",
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: "fake-push-implicit-kind-remote",
+					Property:  "value",
+				},
+			},
+		}}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			commonWaitForPushSecretReady(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+			waitForPushedValueViaExternalSecret(tc.Framework, esv1.SecretStoreRef{
+				Name: f.Namespace.Name,
+				Kind: esv1.ProviderKindStr,
+			}, "fake-push-implicit-kind-remote", "implicit-kind-value")
+		}
+	}
+}
+
+func commonWaitForPushSecretReady(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())
+
+		for _, condition := range ps.Status.Conditions {
+			if condition.Type == esv1alpha1.PushSecretReady && condition.Status == status {
+				return
+			}
+		}
+		g.Expect(false).To(BeTrue())
+	}, time.Minute, 5*time.Second).Should(Succeed())
+}
+
+func waitForPushedValueViaExternalSecret(f *framework.Framework, storeRef esv1.SecretStoreRef, remoteKey, expectedValue string) {
+	externalSecretName := fmt.Sprintf("fake-readback-%s", remoteKey)
+	targetName := fmt.Sprintf("%s-target", externalSecretName)
+
+	externalSecret := &esv1.ExternalSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      externalSecretName,
+			Namespace: f.Namespace.Name,
+		},
+		Spec: esv1.ExternalSecretSpec{
+			RefreshInterval: &metav1.Duration{Duration: defaultV2RefreshInterval},
+			SecretStoreRef:  storeRef,
+			Target: esv1.ExternalSecretTarget{
+				Name: targetName,
+			},
+			Data: []esv1.ExternalSecretData{{
+				SecretKey: "value",
+				RemoteRef: esv1.ExternalSecretDataRemoteRef{
+					Key: remoteKey,
+				},
+			}},
+		},
+	}
+	Expect(f.CRClient.Create(context.Background(), externalSecret)).To(Succeed())
+
+	DeferCleanup(func() {
+		err := f.CRClient.Delete(context.Background(), externalSecret)
+		if err != nil && !apierrors.IsNotFound(err) {
+			Expect(err).ToNot(HaveOccurred())
+		}
+	})
+
+	_, err := f.WaitForSecretValue(f.Namespace.Name, targetName, &corev1.Secret{
+		Type: corev1.SecretTypeOpaque,
+		Data: map[string][]byte{
+			"value": []byte(expectedValue),
+		},
+	})
+	Expect(err).NotTo(HaveOccurred())
+}
+
+func createFakeProviderConfig(f *framework.Framework, namespace, name string) {
+	Expect(f.CRClient.Create(context.Background(), &fakev2alpha1.Fake{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       fakeProviderKind,
+			APIVersion: fakeProviderAPIVersion,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Spec: esv1.FakeProvider{
+			Data: []esv1.FakeProviderData{},
+		},
+	})).To(Succeed())
+}
+
+func updateFakeProviderConfig(f *framework.Framework, namespace, name string, mutate func(*fakev2alpha1.Fake)) {
+	var fake fakev2alpha1.Fake
+	Expect(f.CRClient.Get(context.Background(), types.NamespacedName{
+		Name:      name,
+		Namespace: namespace,
+	}, &fake)).To(Succeed())
+	base := fake.DeepCopy()
+	mutate(&fake)
+	Expect(f.CRClient.Patch(context.Background(), &fake, client.MergeFrom(base))).To(Succeed())
+}
+
+func providerReferenceNamespace(authScope esv1.AuthenticationScope, providerNamespace string) string {
+	if authScope == esv1.AuthenticationScopeProviderNamespace {
+		return providerNamespace
+	}
+	return ""
+}
+
+func fakeConfigNamespaceForAuthScope(authScope esv1.AuthenticationScope, manifestNamespace, providerNamespace string) string {
+	if authScope == esv1.AuthenticationScopeProviderNamespace {
+		return providerNamespace
+	}
+	return manifestNamespace
+}
+
+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, defaultV2PollInterval, 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
+}

+ 39 - 139
e2e/suites/provider/cases/kubernetes/provider_v2.go

@@ -30,6 +30,7 @@ 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"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 	k8sv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/kubernetes/v2alpha1"
@@ -46,147 +47,46 @@ var _ = Describe("[kubernetes] v2 namespaced provider", Label("kubernetes", "v2"
 		frameworkv2.WaitForProviderConnectionReady(f, f.Namespace.Name, f.Namespace.Name, defaultV2WaitTimeout)
 	})
 
-	It("syncs an ExternalSecret through a namespaced Provider", func() {
-		prov.CreateSecret("provider-v2-remote", framework.SecretEntry{
-			Value: `{"value":"provider-v2-value"}`,
-		})
-
-		externalSecret := &esv1.ExternalSecret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      "provider-v2-es",
-				Namespace: f.Namespace.Name,
-			},
-			Spec: esv1.ExternalSecretSpec{
-				RefreshInterval: &metav1.Duration{Duration: defaultV2RefreshInterval},
-				SecretStoreRef: esv1.SecretStoreRef{
-					Name: f.Namespace.Name,
-					Kind: esv1.ProviderKindStr,
-				},
-				Target: esv1.ExternalSecretTarget{
-					Name: "provider-v2-target",
-				},
-				Data: []esv1.ExternalSecretData{{
-					SecretKey: "value",
-					RemoteRef: esv1.ExternalSecretDataRemoteRef{
-						Key:      "provider-v2-remote",
-						Property: "value",
-					},
-				}},
-			},
-		}
-		Expect(f.CRClient.Create(context.Background(), externalSecret)).To(Succeed())
-
-		expected := &corev1.Secret{
-			Type: corev1.SecretTypeOpaque,
-			Data: map[string][]byte{
-				"value": []byte("provider-v2-value"),
+	DescribeTable("namespaced provider read paths",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(common.NamespacedProviderSync(f, common.NamespacedProviderSyncConfig{
+			Description:        "[kubernetes] should sync an ExternalSecret through a namespaced Provider",
+			ExternalSecretName: "provider-v2-es",
+			TargetSecretName:   "provider-v2-target",
+			RemoteKey:          "provider-v2-remote",
+			RemoteSecretValue:  `{"value":"provider-v2-value"}`,
+			RemoteProperty:     "value",
+			SecretKey:          "value",
+			ExpectedValue:      "provider-v2-value",
+		})),
+		Entry(common.NamespacedProviderRefresh(f, common.NamespacedProviderRefreshConfig{
+			Description:         "[kubernetes] should refresh synced secrets after the remote Kubernetes secret changes",
+			ExternalSecretName:  "provider-v2-refresh-es",
+			TargetSecretName:    "provider-v2-refresh-target",
+			RemoteKey:           "provider-v2-refresh-remote",
+			InitialSecretValue:  `{"value":"provider-v2-initial"}`,
+			UpdatedSecretValue:  `{"value":"provider-v2-updated"}`,
+			RemoteProperty:      "value",
+			SecretKey:           "value",
+			InitialExpectedData: "provider-v2-initial",
+			UpdatedExpectedData: "provider-v2-updated",
+			RefreshInterval:     defaultV2RefreshInterval,
+			WaitTimeout:         30 * time.Second,
+		})),
+		Entry(common.NamespacedProviderFind(f, common.NamespacedProviderFindConfig{
+			Description:        "[kubernetes] should sync ExternalSecret dataFrom.find through a namespaced Provider",
+			ExternalSecretName: "provider-v2-find-es",
+			TargetSecretName:   "provider-v2-find-target",
+			MatchRegExp:        "provider-v2-find-(one|two)",
+			MatchingSecrets: map[string]string{
+				"provider-v2-find-one": `{"value":"provider-v2-one"}`,
+				"provider-v2-find-two": `{"value":"provider-v2-two"}`,
 			},
-		}
-
-		_, err := f.WaitForSecretValue(f.Namespace.Name, "provider-v2-target", expected)
-		Expect(err).NotTo(HaveOccurred())
-	})
-
-	It("refreshes synced secrets after the remote Kubernetes secret changes", func() {
-		prov.CreateSecret("provider-v2-refresh-remote", framework.SecretEntry{
-			Value: `{"value":"provider-v2-initial"}`,
-		})
-
-		externalSecret := &esv1.ExternalSecret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      "provider-v2-refresh-es",
-				Namespace: f.Namespace.Name,
+			IgnoredSecrets: map[string]string{
+				"provider-v2-ignore": `{"value":"provider-v2-ignore"}`,
 			},
-			Spec: esv1.ExternalSecretSpec{
-				RefreshInterval: &metav1.Duration{Duration: defaultV2RefreshInterval},
-				SecretStoreRef: esv1.SecretStoreRef{
-					Name: f.Namespace.Name,
-					Kind: esv1.ProviderKindStr,
-				},
-				Target: esv1.ExternalSecretTarget{
-					Name: "provider-v2-refresh-target",
-				},
-				Data: []esv1.ExternalSecretData{{
-					SecretKey: "value",
-					RemoteRef: esv1.ExternalSecretDataRemoteRef{
-						Key:      "provider-v2-refresh-remote",
-						Property: "value",
-					},
-				}},
-			},
-		}
-		Expect(f.CRClient.Create(context.Background(), externalSecret)).To(Succeed())
-
-		_, err := f.WaitForSecretValue(f.Namespace.Name, "provider-v2-refresh-target", &corev1.Secret{
-			Type: corev1.SecretTypeOpaque,
-			Data: map[string][]byte{
-				"value": []byte("provider-v2-initial"),
-			},
-		})
-		Expect(err).NotTo(HaveOccurred())
-
-		updateRemoteSecretValue(f, f.Namespace.Name, "provider-v2-refresh-remote", "provider-v2-updated")
-
-		_, err = f.WaitForSecretValue(f.Namespace.Name, "provider-v2-refresh-target", &corev1.Secret{
-			Type: corev1.SecretTypeOpaque,
-			Data: map[string][]byte{
-				"value": []byte("provider-v2-updated"),
-			},
-		})
-		Expect(err).NotTo(HaveOccurred())
-	})
-
-	It("syncs ExternalSecret dataFrom.find through a namespaced Provider", func() {
-		secretOne := "provider-v2-find-one"
-		secretTwo := "provider-v2-find-two"
-		secretThree := "provider-v2-ignore"
-
-		prov.CreateSecret(secretOne, framework.SecretEntry{
-			Value: `{"value":"provider-v2-one"}`,
-		})
-		prov.CreateSecret(secretTwo, framework.SecretEntry{
-			Value: `{"value":"provider-v2-two"}`,
-		})
-		prov.CreateSecret(secretThree, framework.SecretEntry{
-			Value: `{"value":"provider-v2-ignore"}`,
-		})
-
-		externalSecret := &esv1.ExternalSecret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      "provider-v2-find-es",
-				Namespace: f.Namespace.Name,
-			},
-			Spec: esv1.ExternalSecretSpec{
-				RefreshInterval: &metav1.Duration{Duration: defaultV2RefreshInterval},
-				SecretStoreRef: esv1.SecretStoreRef{
-					Name: f.Namespace.Name,
-					Kind: esv1.ProviderKindStr,
-				},
-				Target: esv1.ExternalSecretTarget{
-					Name: "provider-v2-find-target",
-				},
-				DataFrom: []esv1.ExternalSecretDataFromRemoteRef{{
-					Find: &esv1.ExternalSecretFind{
-						Name: &esv1.FindName{
-							RegExp: "provider-v2-find-(one|two)",
-						},
-					},
-				}},
-			},
-		}
-		Expect(f.CRClient.Create(context.Background(), externalSecret)).To(Succeed())
-
-		expected := &corev1.Secret{
-			Type: corev1.SecretTypeOpaque,
-			Data: map[string][]byte{
-				secretOne: []byte(`{"value":"provider-v2-one"}`),
-				secretTwo: []byte(`{"value":"provider-v2-two"}`),
-			},
-		}
-
-		_, err := f.WaitForSecretValue(f.Namespace.Name, "provider-v2-find-target", expected)
-		Expect(err).NotTo(HaveOccurred())
-	})
+		})),
+	)
 
 	It("recovers after repairing namespaced Provider auth", func() {
 		prov.CreateSecret("provider-v2-recovery-remote", framework.SecretEntry{