Procházet zdrojové kódy

e2e: stabilize v2 operational provider coverage

(cherry picked from commit d637054bf0ee34a69f2149fd600434c15a37476b)
Moritz Johner před 2 měsíci
rodič
revize
45feaf6450
38 změnil soubory, kde provedl 3768 přidání a 101 odebrání
  1. 1 1
      .github/actions/e2e/action.yml
  2. 6 0
      .github/workflows/e2e.yml
  3. 6 0
      Makefile
  4. 556 0
      docs/superpowers/plans/2026-04-16-aws-v2-generator-e2e.md
  5. 944 0
      docs/superpowers/plans/2026-04-16-v2-operational-e2e.md
  6. 5 1
      e2e/Makefile
  7. 32 1
      e2e/framework/addon/chart.go
  8. 35 1
      e2e/framework/addon/chart_test.go
  9. 1 1
      e2e/framework/addon/eso.go
  10. 1 21
      e2e/framework/addon/eso_argocd_application.go
  11. 1 21
      e2e/framework/addon/eso_flux_helm.go
  12. 43 7
      e2e/framework/addon/uninstall_eso_crds.go
  13. 75 0
      e2e/framework/addon/uninstall_eso_crds_test.go
  14. 61 0
      e2e/framework/addon/webhook.go
  15. 92 0
      e2e/framework/addon/webhook_test.go
  16. 8 1
      e2e/framework/framework.go
  17. 60 0
      e2e/framework/framework_test.go
  18. 66 4
      e2e/framework/testcase.go
  19. 87 0
      e2e/framework/testcase_test.go
  20. 5 0
      e2e/framework/util/util.go
  21. 26 2
      e2e/framework/util/util_test.go
  22. 30 11
      e2e/framework/v2/helpers.go
  23. 100 12
      e2e/framework/v2/metrics.go
  24. 106 0
      e2e/framework/v2/metrics_test.go
  25. 147 0
      e2e/framework/v2/operational.go
  26. 21 0
      e2e/makefile_test.go
  27. 6 3
      e2e/suites/argocd/suite_test.go
  28. 6 3
      e2e/suites/flux/suite_test.go
  29. 6 3
      e2e/suites/generator/suite_test.go
  30. 393 0
      e2e/suites/provider/cases/common/operational_v2.go
  31. 60 0
      e2e/suites/provider/cases/common/operational_v2_test.go
  32. 272 0
      e2e/suites/provider/cases/fake/operational_v2.go
  33. 129 2
      e2e/suites/provider/cases/fake/provider_v2.go
  34. 107 0
      e2e/suites/provider/cases/fake/provider_v2_test.go
  35. 62 0
      e2e/suites/provider/cases/kubernetes/metrics_v2.go
  36. 167 0
      e2e/suites/provider/cases/kubernetes/operational_v2.go
  37. 33 0
      e2e/suites/provider/cases/kubernetes/operational_v2_test.go
  38. 12 6
      e2e/suites/provider/suite_test.go

+ 1 - 1
.github/actions/e2e/action.yml

@@ -65,7 +65,7 @@ runs:
         MAKE_TARGET: ${{ inputs.make-target }}
       run: |
         case "$MAKE_TARGET" in
-          test.e2e|test.e2e.v2)
+          test.e2e|test.e2e.v2|test.e2e.v2.operational)
             make "$MAKE_TARGET"
             ;;
           *)

+ 6 - 0
.github/workflows/e2e.yml

@@ -31,6 +31,9 @@ jobs:
         - name: v2
           make_target: test.e2e.v2
           allow_failure: true
+        - name: v2-operational
+          make_target: test.e2e.v2.operational
+          allow_failure: true
     continue-on-error: ${{ matrix.suite.allow_failure }}
     permissions:
       id-token: write #for oidc auth with aws/gcp/azure
@@ -96,6 +99,9 @@ jobs:
         - name: v2
           make_target: test.e2e.v2
           allow_failure: true
+        - name: v2-operational
+          make_target: test.e2e.v2.operational
+          allow_failure: true
     continue-on-error: ${{ matrix.suite.allow_failure }}
     permissions:
       id-token: write      #for oidc auth with aws/gcp/azure

+ 6 - 0
Makefile

@@ -162,6 +162,12 @@ test.e2e.v2: generate ## Run V2 E2E tests
 	$(MAKE) -C ./e2e test.v2
 	@$(OK) go test v2 e2e-tests
 
+.PHONY: test.e2e.v2.operational
+test.e2e.v2.operational: generate ## Run focused V2 operational E2E tests
+	@$(INFO) go test v2 operational e2e-tests
+	$(MAKE) -C ./e2e test.v2.operational
+	@$(OK) go test v2 operational e2e-tests
+
 .PHONY: test.crds
 test.crds: cty crds.generate.tests ## Test CRDs for modification and backwards compatibility
 	@$(INFO) $(CTY) test tests

+ 556 - 0
docs/superpowers/plans/2026-04-16-aws-v2-generator-e2e.md

@@ -0,0 +1,556 @@
+# AWS V2 Generator E2E Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add AWS v2 generator e2e coverage for `ECRAuthorizationToken` and `STSSessionToken`, and make the default `test.v2` loop execute generator suites in v2 mode.
+
+**Architecture:** Keep the existing generator e2e suite structure and make its global ESO bootstrap v2-aware when `E2E_PROVIDER_MODE=v2` is set. Add AWS-focused generator helpers and v2-labeled cases in the generator suite, then extend the `test.v2` target and its dry-run tests so provider and generator suites run together.
+
+**Tech Stack:** Go, Ginkgo/Gomega, Kubernetes e2e framework, Helm-based test installs, Make
+
+---
+
+### Task 1: Make The Generator Suite Bootstrap V2-Aware
+
+**Files:**
+- Modify: `e2e/suites/generator/suite_test.go`
+
+- [ ] **Step 1: Write the failing test expectation as a code diff target**
+
+Add a v2 bootstrap branch in `e2e/suites/generator/suite_test.go` so the generator suite matches the provider suite install path.
+
+```go
+var _ = SynchronizedBeforeSuite(func() []byte {
+	if framework.IsV2ProviderMode() {
+		By("installing eso in generator v2 mode")
+		addon.InstallGlobalAddon(addon.NewESO(
+			addon.WithCRDs(),
+			addon.WithAllowGenericTargets(),
+			addon.WithV2Namespace(),
+			addon.WithV2KubernetesProvider(),
+			addon.WithV2FakeProvider(),
+			addon.WithV2AWSProvider(),
+		))
+		return nil
+	}
+
+	cfg := &addon.Config{}
+	cfg.KubeConfig, cfg.KubeClientSet, cfg.CRClient = util.NewConfig()
+
+	By("installing eso")
+	addon.InstallGlobalAddon(addon.NewESO(addon.WithCRDs(), addon.WithAllowGenericTargets()))
+
+	return nil
+}, func([]byte) {
+	// noop
+})
+```
+
+- [ ] **Step 2: Run the affected generator package build to verify the old code path fails the intended review**
+
+Run:
+
+```bash
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./e2e/suites/generator -run TestE2E -count=1
+```
+
+Expected: PASS before the change, which confirms this task is a bootstrap behavior change rather than a missing-package compile failure. The red check for this task comes from the next task's explicit v2 generator coverage and from `test.v2` wiring assertions.
+
+- [ ] **Step 3: Write the minimal implementation**
+
+Update `e2e/suites/generator/suite_test.go` to:
+
+- import `github.com/external-secrets/external-secrets-e2e/framework`
+- branch on `framework.IsV2ProviderMode()`
+- keep `addon.WithAllowGenericTargets()` in both classic and v2 installs
+- install AWS, fake, and kubernetes v2 providers only in v2 mode
+
+```go
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	"github.com/external-secrets/external-secrets-e2e/framework/addon"
+	"github.com/external-secrets/external-secrets-e2e/framework/util"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+)
+```
+
+- [ ] **Step 4: Run the generator package test again to verify it still passes**
+
+Run:
+
+```bash
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./e2e/suites/generator -run TestE2E -count=1
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/suites/generator/suite_test.go
+git commit -m "Make generator suite bootstrap v2-aware"
+```
+
+### Task 2: Add Shared AWS Generator Helpers
+
+**Files:**
+- Create: `e2e/suites/generator/aws.go`
+
+- [ ] **Step 1: Write the failing helper usage target**
+
+Define a helper file that both AWS generator cases can use for:
+
+- creating the namespaced AWS credential secret
+- building `genv1alpha1.AWSAuth`
+- skipping when static credentials are unavailable
+
+```go
+package generator
+
+const awsCredsSecretName = "aws-creds"
+
+func skipIfAWSGeneratorCredentialsMissing() {
+	if os.Getenv("AWS_REGION") == "" || os.Getenv("AWS_ACCESS_KEY_ID") == "" || os.Getenv("AWS_SECRET_ACCESS_KEY") == "" {
+		Skip("AWS static generator credentials are required")
+	}
+}
+
+func createAWSGeneratorCredentialsSecret(f *framework.Framework) {
+	err := f.CRClient.Create(GinkgoT().Context(), &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      awsCredsSecretName,
+			Namespace: f.Namespace.Name,
+		},
+		Data: map[string][]byte{
+			"akid": []byte(os.Getenv("AWS_ACCESS_KEY_ID")),
+			"sak":  []byte(os.Getenv("AWS_SECRET_ACCESS_KEY")),
+			"st":   []byte(os.Getenv("AWS_SESSION_TOKEN")),
+		},
+	})
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func awsGeneratorAuth() genv1alpha1.AWSAuth {
+	auth := genv1alpha1.AWSAuth{
+		SecretRef: &genv1alpha1.AWSAuthSecretRef{
+			AccessKeyID: esmeta.SecretKeySelector{Name: awsCredsSecretName, Key: "akid"},
+			SecretAccessKey: esmeta.SecretKeySelector{Name: awsCredsSecretName, Key: "sak"},
+		},
+	}
+	if os.Getenv("AWS_SESSION_TOKEN") != "" {
+		auth.SecretRef.SessionToken = &esmeta.SecretKeySelector{Name: awsCredsSecretName, Key: "st"}
+	}
+	return auth
+}
+```
+
+- [ ] **Step 2: Run a targeted package compile to verify the helper file is still absent before implementation**
+
+Run:
+
+```bash
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./e2e/suites/generator -run TestE2E -count=1
+```
+
+Expected: PASS before implementation because no callers exist yet. The helper becomes necessary in Tasks 3 and 4.
+
+- [ ] **Step 3: Write the minimal implementation**
+
+Create `e2e/suites/generator/aws.go` with:
+
+- package-local constant `awsCredsSecretName`
+- `skipIfAWSGeneratorCredentialsMissing()`
+- `createAWSGeneratorCredentialsSecret(f *framework.Framework)`
+- `awsGeneratorAuth() genv1alpha1.AWSAuth`
+
+Imports should include:
+
+```go
+import (
+	"os"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+```
+
+- [ ] **Step 4: Run the package tests to verify the helper file compiles cleanly**
+
+Run:
+
+```bash
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./e2e/suites/generator -run TestE2E -count=1
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/suites/generator/aws.go
+git commit -m "Add shared AWS generator e2e helpers"
+```
+
+### Task 3: Add AWS V2 ECR Generator Coverage
+
+**Files:**
+- Create: `e2e/suites/generator/ecr_v2.go`
+- Read for reference: `e2e/suites/generator/ecr.go`
+- Read for reference: `providers/v2/aws/generator/ecr.go`
+
+- [ ] **Step 1: Write the failing v2 ECR generator test**
+
+Add a new v2-only AWS-labeled test file:
+
+```go
+var _ = Describe("ecr generator v2", Label("aws", "ecr", "v2"), func() {
+	f := framework.New("ecr-v2")
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+		skipIfAWSGeneratorCredentialsMissing()
+	})
+
+	injectGenerator := func(tc *testCase) {
+		createAWSGeneratorCredentialsSecret(f)
+		tc.Generator = &genv1alpha1.ECRAuthorizationToken{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: genv1alpha1.Group + "/" + genv1alpha1.Version,
+				Kind:       genv1alpha1.ECRAuthorizationTokenKind,
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      generatorName,
+				Namespace: f.Namespace.Name,
+			},
+			Spec: genv1alpha1.ECRAuthorizationTokenSpec{
+				Region: os.Getenv("AWS_REGION"),
+				Auth:   awsGeneratorAuth(),
+			},
+		}
+	}
+
+	customResourceGenerator := func(tc *testCase) {
+		tc.ExternalSecret.Spec.DataFrom = []esv1.ExternalSecretDataFromRemoteRef{{
+			SourceRef: &esv1.StoreGeneratorSourceRef{
+				GeneratorRef: &esv1.GeneratorRef{
+					Kind: "ECRAuthorizationToken",
+					Name: generatorName,
+				},
+			},
+		}}
+		tc.AfterSync = func(secret *v1.Secret) {
+			Expect(string(secret.Data["username"])).To(Equal("AWS"))
+			Expect(string(secret.Data["password"])).ToNot(BeEmpty())
+			Expect(string(secret.Data["proxy_endpoint"])).ToNot(BeEmpty())
+			Expect(string(secret.Data["expires_at"])).ToNot(BeEmpty())
+		}
+	}
+
+	DescribeTable("generate ecr auth tokens through the v2 aws provider", generatorTableFunc,
+		Entry("using custom resource generator", f, injectGenerator, customResourceGenerator),
+	)
+})
+```
+
+- [ ] **Step 2: Run the focused v2 generator e2e selection**
+
+Run:
+
+```bash
+cd e2e && TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2 GINKGO_LABELS='aws && v2 && ecr' TEST_SUITES='generator'
+```
+
+Expected: if Task 1 has not landed yet, FAIL because the generator suite still installs classic ESO without the v2 provider sidecars. After Task 1 and Task 2, this command becomes the first green verification for the new ECR v2 coverage.
+
+- [ ] **Step 3: Write the minimal implementation**
+
+Create `e2e/suites/generator/ecr_v2.go` with:
+
+- `Label("aws", "ecr", "v2")`
+- `framework.IsV2ProviderMode()` gate
+- shared AWS helper usage
+- assertions for `username`, `password`, `proxy_endpoint`, and `expires_at`
+
+Imports should include:
+
+```go
+import (
+	"os"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	v1 "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"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+)
+```
+
+- [ ] **Step 4: Run the generator package tests to verify the new case compiles**
+
+Run:
+
+```bash
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./e2e/suites/generator -run TestE2E -count=1
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/suites/generator/ecr_v2.go e2e/suites/generator/aws.go
+git commit -m "Add AWS v2 ECR generator e2e coverage"
+```
+
+### Task 4: Add AWS V2 STS Generator Coverage
+
+**Files:**
+- Create: `e2e/suites/generator/sts_v2.go`
+- Read for reference: `providers/v2/aws/generator/sts.go`
+- Read for reference: `apis/generators/v1alpha1/types_sts.go`
+
+- [ ] **Step 1: Write the failing v2 STS generator test**
+
+Create a new STS generator case that validates the output contract from `providers/v2/aws/generator/sts.go`:
+
+```go
+var _ = Describe("sts generator v2", Label("aws", "sts", "v2"), func() {
+	f := framework.New("sts-v2")
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+		skipIfAWSGeneratorCredentialsMissing()
+	})
+
+	injectGenerator := func(tc *testCase) {
+		createAWSGeneratorCredentialsSecret(f)
+		tc.Generator = &genv1alpha1.STSSessionToken{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: genv1alpha1.Group + "/" + genv1alpha1.Version,
+				Kind:       genv1alpha1.STSSessionTokenKind,
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      generatorName,
+				Namespace: f.Namespace.Name,
+			},
+			Spec: genv1alpha1.STSSessionTokenSpec{
+				Region: os.Getenv("AWS_REGION"),
+				Auth:   awsGeneratorAuth(),
+			},
+		}
+	}
+
+	customResourceGenerator := func(tc *testCase) {
+		tc.ExternalSecret.Spec.DataFrom = []esv1.ExternalSecretDataFromRemoteRef{{
+			SourceRef: &esv1.StoreGeneratorSourceRef{
+				GeneratorRef: &esv1.GeneratorRef{
+					Kind: "STSSessionToken",
+					Name: generatorName,
+				},
+			},
+		}}
+		tc.AfterSync = func(secret *v1.Secret) {
+			Expect(string(secret.Data["access_key_id"])).ToNot(BeEmpty())
+			Expect(string(secret.Data["secret_access_key"])).ToNot(BeEmpty())
+			Expect(string(secret.Data["session_token"])).ToNot(BeEmpty())
+			Expect(string(secret.Data["expiration"])).ToNot(BeEmpty())
+		}
+	}
+
+	DescribeTable("generate sts session tokens through the v2 aws provider", generatorTableFunc,
+		Entry("using custom resource generator", f, injectGenerator, customResourceGenerator),
+	)
+})
+```
+
+- [ ] **Step 2: Run the focused v2 generator e2e selection**
+
+Run:
+
+```bash
+cd e2e && TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2 GINKGO_LABELS='aws && v2 && sts' TEST_SUITES='generator'
+```
+
+Expected: PASS once the new test and shared helpers are in place. If it fails, the failure should come from STS generator wiring or assertion mismatches, not from classic-mode skips.
+
+- [ ] **Step 3: Write the minimal implementation**
+
+Create `e2e/suites/generator/sts_v2.go` with:
+
+- `Label("aws", "sts", "v2")`
+- shared AWS helper usage
+- `framework.IsV2ProviderMode()` guard
+- assertions for the four STS generator output keys
+
+Imports should include:
+
+```go
+import (
+	"os"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	v1 "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"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+)
+```
+
+- [ ] **Step 4: Run the generator package tests to verify the new STS case compiles**
+
+Run:
+
+```bash
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./e2e/suites/generator -run TestE2E -count=1
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/suites/generator/sts_v2.go
+git commit -m "Add AWS v2 STS generator e2e coverage"
+```
+
+### Task 5: Include Generator Suites In The Default V2 Loop
+
+**Files:**
+- Modify: `e2e/Makefile`
+- Modify: `e2e/makefile_test.go`
+
+- [ ] **Step 1: Write the failing dry-run assertions**
+
+Update `e2e/makefile_test.go` so `test.v2` expects the generated run command to include `TEST_SUITES="provider generator"`.
+
+Add assertions like:
+
+```go
+if !strings.Contains(defaultDryRun, `TEST_SUITES="provider generator"`) {
+	t.Fatalf("expected default test.v2 dry-run to run provider and generator suites, output:\n%s", defaultDryRun)
+}
+```
+
+and for the skipped build case:
+
+```go
+if !strings.Contains(skippedDryRun, `TEST_SUITES="provider generator"`) {
+	t.Fatalf("expected skipped test.v2 dry-run to still run provider and generator suites, output:\n%s", skippedDryRun)
+}
+```
+
+- [ ] **Step 2: Run the targeted makefile tests to verify they fail**
+
+Run:
+
+```bash
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./e2e -run 'TestV2MakeTarget' -count=1
+```
+
+Expected: FAIL because `e2e/Makefile` still uses `TEST_SUITES="provider"`
+
+- [ ] **Step 3: Write the minimal implementation**
+
+Change `e2e/Makefile`:
+
+```make
+KUBECTL_CONTEXT="$(KIND_CONTEXT)" GINKGO_LABELS="$(V2_GINKGO_LABELS)" E2E_PROVIDER_MODE="v2" TEST_SUITES="provider generator" E2E_SKIP_HELM_DEPENDENCY_UPDATE="true" ./run.sh
+```
+
+Update `e2e/makefile_test.go` to assert the new suite list in both v2 dry-run paths.
+
+- [ ] **Step 4: Run the makefile tests to verify they pass**
+
+Run:
+
+```bash
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./e2e -run 'TestV2MakeTarget|TestClassicMakeTarget|TestV2MakeTargetPrunesDockerImagesInCI' -count=1
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/Makefile e2e/makefile_test.go
+git commit -m "Run generator suites in the default v2 e2e target"
+```
+
+### Task 6: Verify AWS V2 Generator Coverage End-To-End
+
+**Files:**
+- Verify: `e2e/suites/generator/aws.go`
+- Verify: `e2e/suites/generator/ecr_v2.go`
+- Verify: `e2e/suites/generator/sts_v2.go`
+- Verify: `e2e/suites/generator/suite_test.go`
+- Verify: `e2e/Makefile`
+- Verify: `e2e/makefile_test.go`
+
+- [ ] **Step 1: Run the focused Go verification**
+
+Run:
+
+```bash
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./e2e ./e2e/suites/generator -count=1
+```
+
+Expected: PASS
+
+- [ ] **Step 2: Run the focused v2 generator e2e labels**
+
+Run:
+
+```bash
+cd e2e && TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2 GINKGO_LABELS='aws && v2 && (ecr || sts)' TEST_SUITES='generator'
+```
+
+Expected: PASS
+
+- [ ] **Step 3: Run the default v2 target smoke verification**
+
+Run:
+
+```bash
+cd e2e && TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2
+```
+
+Expected: PASS for provider and generator suites with managed tests excluded by `V2_GINKGO_LABELS`
+
+- [ ] **Step 4: Review the final diff and commit**
+
+Run:
+
+```bash
+git status --short
+git diff --stat
+```
+
+Expected: only the planned generator/bootstrap/makefile files are changed
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/suites/generator/suite_test.go e2e/suites/generator/aws.go e2e/suites/generator/ecr_v2.go e2e/suites/generator/sts_v2.go e2e/Makefile e2e/makefile_test.go
+git commit -m "Add AWS v2 generator e2e coverage"
+```

+ 944 - 0
docs/superpowers/plans/2026-04-16-v2-operational-e2e.md

@@ -0,0 +1,944 @@
+# V2 Operational E2E Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add reusable v2 operational e2e coverage for provider outage, restart, readiness recovery, and connection-scaling behavior across read, push, and generator-backed flows, then wire at least one real operational scenario into CI.
+
+**Architecture:** Extend the existing v2 e2e framework with shared pod-disruption and metrics helpers, then build provider-agnostic operational scenario builders in `e2e/suites/provider/cases/common`. Use provider-specific harnesses for `fake` and `kubernetes` to opt into shared scenarios, add fake-backed v2 generator outage coverage in the generator suite, and finish with a focused `test.e2e.v2.operational` CI slice that runs a deterministic real operational scenario.
+
+**Tech Stack:** Go, Ginkgo/Gomega, Kubernetes kind e2e framework, controller-runtime client, Prometheus metrics scraping, GitHub Actions
+
+---
+
+## File Map
+
+- Create: `e2e/framework/v2/operational.go`
+- Create: `e2e/framework/v2/metrics_test.go`
+- Modify: `e2e/framework/v2/helpers.go`
+- Modify: `e2e/framework/v2/metrics.go`
+- Create: `e2e/suites/provider/cases/common/operational_v2.go`
+- Create: `e2e/suites/provider/cases/common/operational_v2_test.go`
+- Create: `e2e/suites/provider/cases/fake/operational_v2.go`
+- Modify: `e2e/suites/provider/cases/fake/provider_v2.go`
+- Modify: `e2e/suites/provider/cases/fake/provider_v2_test.go`
+- Create: `e2e/suites/provider/cases/kubernetes/operational_v2.go`
+- Modify: `e2e/suites/provider/cases/kubernetes/provider_v2.go`
+- Modify: `e2e/suites/provider/cases/kubernetes/clusterprovider_v2.go`
+- Create: `e2e/suites/generator/operational_v2.go`
+- Modify: `e2e/suites/generator/testcase.go`
+- Modify: `e2e/Makefile`
+- Modify: `e2e/makefile_test.go`
+- Modify: `Makefile`
+- Modify: `.github/actions/e2e/action.yml`
+- Modify: `.github/workflows/e2e.yml`
+
+### Task 1: Add Shared V2 Operational Framework Helpers
+
+**Files:**
+- Create: `e2e/framework/v2/operational.go`
+- Create: `e2e/framework/v2/metrics_test.go`
+- Modify: `e2e/framework/v2/helpers.go`
+- Modify: `e2e/framework/v2/metrics.go`
+
+- [ ] **Step 1: Write the failing metric-helper tests**
+
+Create `e2e/framework/v2/metrics_test.go` with focused tests for the metric helpers the operational suite will need.
+
+```go
+package v2
+
+import "testing"
+
+func TestSumMetricValues(t *testing.T) {
+	metrics := MetricsMap{
+		"grpc_pool_connections_total": {
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-a"}, Value: 1},
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-a"}, Value: 2},
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-b"}, Value: 4},
+		},
+	}
+
+	got := SumMetricValues(metrics, "grpc_pool_connections_total", map[string]string{"address": "provider-a"})
+	if got != 3 {
+		t.Fatalf("expected sum 3, got %v", got)
+	}
+}
+
+func TestCountMetricSamples(t *testing.T) {
+	metrics := MetricsMap{
+		"grpc_pool_connections_total": {
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-a"}, Value: 1},
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-a"}, Value: 2},
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-b"}, Value: 4},
+		},
+	}
+
+	got := CountMetricSamples(metrics, "grpc_pool_connections_total", map[string]string{"address": "provider-a"})
+	if got != 2 {
+		t.Fatalf("expected count 2, got %d", got)
+	}
+}
+```
+
+- [ ] **Step 2: Run the focused framework tests to verify they fail**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./framework/v2 -run 'TestSumMetricValues|TestCountMetricSamples' -count=1
+```
+
+Expected: FAIL with `undefined: SumMetricValues` and `undefined: CountMetricSamples`
+
+- [ ] **Step 3: Write the minimal framework implementation**
+
+Create `e2e/framework/v2/operational.go` with pod and deployment helpers that the provider harnesses can reuse.
+
+```go
+package v2
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/gomega"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	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 BackendTarget struct {
+	Namespace        string
+	DeploymentName   string
+	PodLabelSelector string
+}
+
+func WaitForClusterProviderNotReady(f *framework.Framework, name string, timeout time.Duration) *esv1.ClusterProvider {
+	return WaitForClusterProviderCondition(f, name, metav1.ConditionFalse, timeout)
+}
+
+func WaitForClusterProviderCondition(f *framework.Framework, name string, status metav1.ConditionStatus, timeout time.Duration) *esv1.ClusterProvider {
+	var clusterProvider esv1.ClusterProvider
+	Eventually(func() bool {
+		err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: name}, &clusterProvider)
+		if err != nil {
+			return false
+		}
+		for _, condition := range clusterProvider.Status.Conditions {
+			if condition.Type == "Ready" && condition.Status == status {
+				return true
+			}
+		}
+		return false
+	}, timeout, time.Second).Should(BeTrue())
+	return &clusterProvider
+}
+
+func ScaleDeployment(f *framework.Framework, namespace, name string, replicas int32) {
+	var deployment appsv1.Deployment
+	Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Namespace: namespace, Name: name}, &deployment)).To(Succeed())
+	deployment.Spec.Replicas = &replicas
+	Expect(f.CRClient.Update(context.Background(), &deployment)).To(Succeed())
+}
+
+func DeleteOneProviderPod(f *framework.Framework, namespace, labelSelector string) {
+	var podList corev1.PodList
+	Expect(f.CRClient.List(context.Background(), &podList, &client.ListOptions{
+		Namespace:     namespace,
+		LabelSelector: labels.SelectorFromSet(labels.Set{}),
+	})).To(Succeed())
+	for _, pod := range podList.Items {
+		if pod.Status.Phase == corev1.PodRunning {
+			Expect(f.CRClient.Delete(context.Background(), &pod)).To(Succeed())
+			return
+		}
+	}
+	Fail(fmt.Sprintf("no running pod found for selector %s", labelSelector))
+}
+```
+
+Modify `e2e/framework/v2/metrics.go` to add bounded-scaling helpers.
+
+```go
+func SumMetricValues(metrics MetricsMap, metricName string, matchLabels map[string]string) float64 {
+	samples, exists := metrics[metricName]
+	if !exists {
+		return 0
+	}
+	var total float64
+	for _, sample := range samples {
+		if labelsMatch(sample.Labels, matchLabels) {
+			total += sample.Value
+		}
+	}
+	return total
+}
+
+func CountMetricSamples(metrics MetricsMap, metricName string, matchLabels map[string]string) int {
+	samples, exists := metrics[metricName]
+	if !exists {
+		return 0
+	}
+	count := 0
+	for _, sample := range samples {
+		if labelsMatch(sample.Labels, matchLabels) {
+			count++
+		}
+	}
+	return count
+}
+```
+
+Also add the missing imports in `e2e/framework/v2/operational.go`:
+
+```go
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/gomega"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+```
+
+- [ ] **Step 4: Run the framework tests again to verify they pass**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./framework/v2 -run 'TestSumMetricValues|TestCountMetricSamples|TestCreateKubernetesProviderUsesProvidedCABundle|TestGetClusterCABundleWaitsForRootCAConfigMap' -count=1
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/framework/v2/operational.go e2e/framework/v2/metrics.go e2e/framework/v2/metrics_test.go e2e/framework/v2/helpers.go
+git commit -m "Add v2 operational e2e framework helpers"
+```
+
+### Task 2: Add Shared Operational Scenario Builders
+
+**Files:**
+- Create: `e2e/suites/provider/cases/common/operational_v2.go`
+- Create: `e2e/suites/provider/cases/common/operational_v2_test.go`
+
+- [ ] **Step 1: Write the failing shared-runtime tests**
+
+Create `e2e/suites/provider/cases/common/operational_v2_test.go`.
+
+```go
+package common
+
+import "testing"
+
+func TestOperationalRuntimeSupportsDisruptionLifecycle(t *testing.T) {
+	runtimeWithoutHooks := &OperationalRuntime{}
+	if runtimeWithoutHooks.SupportsDisruptionLifecycle() {
+		t.Fatalf("expected false when all hooks are nil")
+	}
+
+	runtimeWithBreakOnly := &OperationalRuntime{
+		MakeUnavailable: func() {},
+	}
+	if runtimeWithBreakOnly.SupportsDisruptionLifecycle() {
+		t.Fatalf("expected false when Restore is nil")
+	}
+
+	runtimeWithRestoreOnly := &OperationalRuntime{
+		RestoreAvailability: func() {},
+	}
+	if runtimeWithRestoreOnly.SupportsDisruptionLifecycle() {
+		t.Fatalf("expected false when MakeUnavailable is nil")
+	}
+
+	runtimeWithBoth := &OperationalRuntime{
+		MakeUnavailable:     func() {},
+		RestoreAvailability: func() {},
+	}
+	if !runtimeWithBoth.SupportsDisruptionLifecycle() {
+		t.Fatalf("expected true when both hooks exist")
+	}
+}
+
+func TestOperationalRuntimeSupportsRestart(t *testing.T) {
+	runtime := &OperationalRuntime{}
+	if runtime.SupportsRestart() {
+		t.Fatalf("expected false when RestartBackend is nil")
+	}
+
+	runtime.RestartBackend = func() {}
+	if !runtime.SupportsRestart() {
+		t.Fatalf("expected true when RestartBackend is present")
+	}
+}
+```
+
+- [ ] **Step 2: Run the common package tests to verify they fail**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./suites/provider/cases/common -run 'TestOperationalRuntimeSupportsDisruptionLifecycle|TestOperationalRuntimeSupportsRestart' -count=1
+```
+
+Expected: FAIL with `undefined: OperationalRuntime`
+
+- [ ] **Step 3: Write the shared operational scenario builders**
+
+Create `e2e/suites/provider/cases/common/operational_v2.go` with a small runtime contract and reusable cases for namespaced-provider, cluster-provider, and push-secret operational behavior.
+
+```go
+package common
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	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"
+)
+
+type OperationalRuntime struct {
+	ProviderRef          esv1.SecretStoreRef
+	ClusterProviderName  string
+	BackendAddress       string
+	MakeUnavailable      func()
+	RestoreAvailability  func()
+	RestartBackend       func()
+}
+
+func (r *OperationalRuntime) SupportsDisruptionLifecycle() bool {
+	return r != nil && r.MakeUnavailable != nil && r.RestoreAvailability != nil
+}
+
+func (r *OperationalRuntime) SupportsRestart() bool {
+	return r != nil && r.RestartBackend != nil
+}
+
+type OperationalExternalSecretHarness struct {
+	PrepareNamespaced func(tc *framework.TestCase) *OperationalRuntime
+	PrepareCluster    func(tc *framework.TestCase, cfg ClusterProviderConfig) *OperationalRuntime
+}
+
+type OperationalPushSecretHarness struct {
+	PrepareNamespaced func(tc *framework.TestCase) *OperationalRuntime
+	PrepareCluster    func(tc *framework.TestCase, cfg ClusterProviderConfig) *OperationalRuntime
+}
+
+func NamespacedProviderUnavailable(f *framework.Framework, harness OperationalExternalSecretHarness, remoteKey, expectedValue string) (string, func(*framework.TestCase)) {
+	return "[common] should surface Provider unavailability and recover after backend restoration", func(tc *framework.TestCase) {
+		tc.ExternalSecret.ObjectMeta.Name = "operational-unavailable-es"
+		tc.ExternalSecret.Spec.Target.Name = "operational-unavailable-target"
+		tc.Secrets = map[string]framework.SecretEntry{
+			remoteKey: {Value: jsonSecretValue(expectedValue)},
+		}
+		var runtime *OperationalRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.PrepareNamespaced(tc)
+			tc.ProviderOverride = nil
+			tc.ExternalSecret.Spec.SecretStoreRef = runtime.ProviderRef
+			runtime.MakeUnavailable()
+		}
+		tc.AfterSync = func(_ framework.SecretStoreProvider, _ *corev1.Secret) {
+			frameworkv2.WaitForProviderConnectionNotReady(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Spec.SecretStoreRef.Name, time.Minute)
+			waitForExternalSecretStatus(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Name, corev1.ConditionFalse)
+			runtime.RestoreAvailability()
+			frameworkv2.WaitForProviderConnectionReady(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Spec.SecretStoreRef.Name, time.Minute)
+		}
+	}
+}
+```
+
+In the same file, add matching builders for:
+
+- `NamespacedProviderRestart`
+- `ClusterProviderUnavailable`
+- `ClusterProviderRestart`
+- `NamespacedPushSecretUnavailable`
+- `ClusterProviderPushUnavailable`
+
+Use the existing helper style from `clusterprovider.go` and `push_secret.go`: set the resource spec in `tc.Prepare`, use `waitForExternalSecretStatus` or `waitForPushSecretStatus`, and call the shared v2 readiness helpers for the resource-level assertions.
+
+- [ ] **Step 4: Run the common package tests again to verify they pass**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./suites/provider/cases/common -run 'TestOperationalRuntimeSupportsDisruptionLifecycle|TestOperationalRuntimeSupportsRestart|TestClusterProviderExternalSecretRuntimeSupportsAuthLifecycle|TestClusterProviderPushRuntimeSupportsAuthLifecycle' -count=1
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/suites/provider/cases/common/operational_v2.go e2e/suites/provider/cases/common/operational_v2_test.go
+git commit -m "Add shared v2 operational scenario builders"
+```
+
+### Task 3: Add Fake Provider Operational Coverage
+
+**Files:**
+- Create: `e2e/suites/provider/cases/fake/operational_v2.go`
+- Modify: `e2e/suites/provider/cases/fake/provider_v2.go`
+- Modify: `e2e/suites/provider/cases/fake/provider_v2_test.go`
+
+- [ ] **Step 1: Write the failing fake-provider helper test**
+
+Extend `e2e/suites/provider/cases/fake/provider_v2_test.go` with a small pure test for the backend selector helper you will add.
+
+```go
+func TestFakeBackendTargetUsesProviderNamespaceAndSelector(t *testing.T) {
+	target := fakeBackendTarget()
+	if target.Namespace != frameworkv2.ProviderNamespace {
+		t.Fatalf("expected provider namespace %q, got %q", frameworkv2.ProviderNamespace, target.Namespace)
+	}
+	if target.PodLabelSelector != "app.kubernetes.io/name=external-secrets-provider-fake" {
+		t.Fatalf("unexpected selector %q", target.PodLabelSelector)
+	}
+}
+```
+
+- [ ] **Step 2: Run the fake package tests to verify they fail**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./suites/provider/cases/fake -run 'TestFakeBackendTargetUsesProviderNamespaceAndSelector|TestUpsertFakeProviderDataReplacesMatchingEntry' -count=1
+```
+
+Expected: FAIL with `undefined: fakeBackendTarget`
+
+- [ ] **Step 3: Implement fake operational harnesses and specs**
+
+Modify `e2e/suites/provider/cases/fake/provider_v2.go` to expose a backend target helper and operational runtime factories.
+
+```go
+func fakeBackendTarget() frameworkv2.BackendTarget {
+	return frameworkv2.BackendTarget{
+		Namespace:        frameworkv2.ProviderNamespace,
+		PodLabelSelector: "app.kubernetes.io/name=external-secrets-provider-fake",
+	}
+}
+
+func (s *ProviderV2) prepareNamespacedOperationalRuntime() *common.OperationalRuntime {
+	return &common.OperationalRuntime{
+		ProviderRef: esv1.SecretStoreRef{
+			Name: s.framework.Namespace.Name,
+			Kind: esv1.ProviderKindStr,
+		},
+		BackendAddress: frameworkv2.ProviderAddress("fake"),
+		MakeUnavailable: func() {
+			frameworkv2.ScaleDeploymentBySelector(s.framework, fakeBackendTarget(), 0)
+		},
+		RestoreAvailability: func() {
+			frameworkv2.ScaleDeploymentBySelector(s.framework, fakeBackendTarget(), 1)
+		},
+		RestartBackend: func() {
+			frameworkv2.DeleteOneProviderPodBySelector(s.framework, fakeBackendTarget())
+		},
+	}
+}
+```
+
+Create `e2e/suites/provider/cases/fake/operational_v2.go` with operational labels and shared common entries.
+
+```go
+package fake
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+
+	"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"
+)
+
+var _ = Describe("[fake] v2 operational", Label("fake", "v2", "operational"), func() {
+	f := framework.New("eso-fake-v2-operational")
+	prov := NewProviderV2(f)
+
+	DescribeTable("external secret operational behavior",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(common.NamespacedProviderUnavailable(f, newFakeOperationalExternalSecretHarness(f, prov), "fake-operational-unavailable", "recovered")),
+		Entry(common.NamespacedProviderRestart(f, newFakeOperationalExternalSecretHarness(f, prov), "fake-operational-restart", "restarted")),
+		Entry(common.ClusterProviderUnavailable(f, newFakeOperationalExternalSecretHarness(f, prov), "fake-operational-cluster", "cluster-recovered", esv1.AuthenticationScopeManifestNamespace)),
+	)
+
+	DescribeTable("push secret operational behavior",
+		framework.TableFuncWithPushSecret(f, prov, nil),
+		Entry(common.NamespacedPushSecretUnavailable(f, newFakeOperationalPushHarness(f, prov))),
+		Entry(common.ClusterProviderPushUnavailable(f, newFakeOperationalPushHarness(f, prov), esv1.AuthenticationScopeManifestNamespace)),
+	)
+})
+```
+
+- [ ] **Step 4: Run fake unit tests and focused fake operational e2e**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./suites/provider/cases/fake -run 'TestFakeBackendTargetUsesProviderNamespaceAndSelector|TestUpsertFakeProviderDataReplacesMatchingEntry|TestRemoveFakeProviderDataRemovesOnlyExactMatch' -count=1
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2 V2_GINKGO_LABELS='fake && v2 && operational' TEST_SUITES='provider'
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/suites/provider/cases/fake/provider_v2.go e2e/suites/provider/cases/fake/provider_v2_test.go e2e/suites/provider/cases/fake/operational_v2.go
+git commit -m "Add fake v2 operational e2e coverage"
+```
+
+### Task 4: Add Kubernetes Provider Operational Coverage
+
+**Files:**
+- Create: `e2e/suites/provider/cases/kubernetes/operational_v2.go`
+- Modify: `e2e/suites/provider/cases/kubernetes/provider_v2.go`
+- Modify: `e2e/suites/provider/cases/kubernetes/clusterprovider_v2.go`
+
+- [ ] **Step 1: Write the failing Kubernetes helper test**
+
+Add `TestKubernetesBackendTargetUsesProviderNamespaceAndSelector` to `e2e/framework/v2/helpers_test.go` or create `e2e/suites/provider/cases/kubernetes/operational_v2_test.go`.
+
+```go
+func TestKubernetesBackendTargetUsesProviderNamespaceAndSelector(t *testing.T) {
+	target := kubernetesBackendTarget()
+	if target.Namespace != frameworkv2.ProviderNamespace {
+		t.Fatalf("expected provider namespace %q, got %q", frameworkv2.ProviderNamespace, target.Namespace)
+	}
+	if target.PodLabelSelector != "app.kubernetes.io/name=external-secrets-provider-kubernetes" {
+		t.Fatalf("unexpected selector %q", target.PodLabelSelector)
+	}
+}
+```
+
+- [ ] **Step 2: Run the Kubernetes package tests to verify they fail**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./suites/provider/cases/kubernetes -run 'TestKubernetesBackendTargetUsesProviderNamespaceAndSelector' -count=1
+```
+
+Expected: FAIL with `undefined: kubernetesBackendTarget`
+
+- [ ] **Step 3: Implement the Kubernetes operational harness and specs**
+
+Create `e2e/suites/provider/cases/kubernetes/operational_v2.go` and expose backend helpers from the provider files.
+
+```go
+func kubernetesBackendTarget() frameworkv2.BackendTarget {
+	return frameworkv2.BackendTarget{
+		Namespace:        frameworkv2.ProviderNamespace,
+		PodLabelSelector: "app.kubernetes.io/name=external-secrets-provider-kubernetes",
+	}
+}
+
+func newKubernetesOperationalExternalSecretHarness(f *framework.Framework) common.OperationalExternalSecretHarness {
+	return common.OperationalExternalSecretHarness{
+		PrepareNamespaced: func(tc *framework.TestCase) *common.OperationalRuntime {
+			return &common.OperationalRuntime{
+				ProviderRef: esv1.SecretStoreRef{Name: f.Namespace.Name, Kind: esv1.ProviderKindStr},
+				BackendAddress: frameworkv2.ProviderAddress("kubernetes"),
+				MakeUnavailable: func() {
+					frameworkv2.ScaleDeploymentBySelector(f, kubernetesBackendTarget(), 0)
+				},
+				RestoreAvailability: func() {
+					frameworkv2.ScaleDeploymentBySelector(f, kubernetesBackendTarget(), 1)
+				},
+				RestartBackend: func() {
+					frameworkv2.DeleteOneProviderPodBySelector(f, kubernetesBackendTarget())
+				},
+			}
+		},
+	}
+}
+```
+
+Populate the spec file with deterministic namespaced and cluster-provider entries, reusing the existing remote-secret setup in `provider_v2.go` and `clusterprovider_v2.go`.
+
+- [ ] **Step 4: Run Kubernetes unit tests and focused operational e2e**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./suites/provider/cases/kubernetes -run 'TestKubernetesBackendTargetUsesProviderNamespaceAndSelector|TestCreateKubernetesProviderUsesProvidedCABundle' -count=1
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2 V2_GINKGO_LABELS='kubernetes && v2 && operational' TEST_SUITES='provider'
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/suites/provider/cases/kubernetes/operational_v2.go e2e/suites/provider/cases/kubernetes/provider_v2.go e2e/suites/provider/cases/kubernetes/clusterprovider_v2.go
+git commit -m "Add kubernetes v2 operational e2e coverage"
+```
+
+### Task 5: Add Fake V2 Generator Operational Coverage
+
+**Files:**
+- Create: `e2e/suites/generator/operational_v2.go`
+- Modify: `e2e/suites/generator/testcase.go`
+
+- [ ] **Step 1: Write the failing generator status helper test**
+
+Create a small test file `e2e/suites/generator/operational_v2_test.go` or extend an existing generator test file with a helper-level test.
+
+```go
+func TestGetESCondReturnsNilWhenConditionMissing(t *testing.T) {
+	got := getESCond(esv1.ExternalSecretStatus{}, esv1.ExternalSecretReady)
+	if got != nil {
+		t.Fatalf("expected nil condition, got %#v", got)
+	}
+}
+```
+
+This keeps the generator task grounded in the existing helper file before the operational flow adds negative-path waits.
+
+- [ ] **Step 2: Run the focused generator tests**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./suites/generator -run 'TestGetESCondReturnsNilWhenConditionMissing|TestAWSGeneratorAuthSessionTokenHandling' -count=1
+```
+
+Expected: PASS before the helper change, confirming the package is healthy before adding operational behavior
+
+- [ ] **Step 3: Implement a fake generator outage/recovery spec**
+
+Modify `e2e/suites/generator/testcase.go` to add a negative-path wait helper that can observe `ExternalSecretReady=False` before recovery.
+
+```go
+func waitForGeneratorExternalSecretStatus(f *framework.Framework, namespace, name string, expected v1.ConditionStatus) {
+	Eventually(func() v1.ConditionStatus {
+		var es esv1.ExternalSecret
+		if err := f.CRClient.Get(GinkgoT().Context(), types.NamespacedName{Namespace: namespace, Name: name}, &es); err != nil {
+			return ""
+		}
+		cond := getESCond(es.Status, esv1.ExternalSecretReady)
+		if cond == nil {
+			return ""
+		}
+		return cond.Status
+	}).WithTimeout(30 * time.Second).Should(Equal(expected))
+}
+```
+
+Create `e2e/suites/generator/operational_v2.go` with a fake-generator operational case.
+
+```go
+package generator
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	v1 "k8s.io/api/core/v1"
+
+	"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"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var _ = Describe("fake generator operational v2", Label("fake", "v2", "operational", "generator"), func() {
+	f := framework.New("fake-generator-operational")
+
+	It("recovers after the fake provider pod is restarted", func() {
+		tc := &testCase{Framework: f}
+		tc.Generator = &genv1alpha1.Fake{
+			TypeMeta: metav1.TypeMeta{APIVersion: genv1alpha1.Group + "/" + genv1alpha1.Version, Kind: genv1alpha1.FakeKind},
+			ObjectMeta: metav1.ObjectMeta{Name: generatorName, Namespace: f.Namespace.Name},
+			Spec: genv1alpha1.FakeSpec{Data: map[string]string{"value": "recovered"}},
+		}
+		tc.ExternalSecret = &esv1.ExternalSecret{
+			ObjectMeta: metav1.ObjectMeta{Name: "fake-generator-operational-es", Namespace: f.Namespace.Name},
+			Spec: esv1.ExternalSecretSpec{
+				RefreshInterval: &metav1.Duration{Duration: defaultV2RefreshInterval},
+				Target: esv1.ExternalSecretTarget{Name: "fake-generator-operational-target"},
+				DataFrom: []esv1.ExternalSecretDataFromRemoteRef{{
+					SourceRef: &esv1.StoreGeneratorSourceRef{
+						GeneratorRef: &esv1.GeneratorRef{Kind: "Fake", Name: generatorName},
+					},
+				}},
+			},
+		}
+
+		Expect(f.CRClient.Create(GinkgoT().Context(), tc.Generator)).To(Succeed())
+		Expect(f.CRClient.Create(GinkgoT().Context(), tc.ExternalSecret)).To(Succeed())
+		frameworkv2.DeleteOneProviderPodBySelector(f, frameworkv2.BackendTarget{
+			Namespace: frameworkv2.ProviderNamespace,
+			PodLabelSelector: "app.kubernetes.io/name=external-secrets-provider-fake",
+		})
+		waitForGeneratorExternalSecretStatus(f, tc.ExternalSecret.Namespace, tc.ExternalSecret.Name, v1.ConditionFalse)
+		waitForGeneratorExternalSecretStatus(f, tc.ExternalSecret.Namespace, tc.ExternalSecret.Name, v1.ConditionTrue)
+	})
+})
+```
+
+- [ ] **Step 4: Run the generator tests and focused fake operational generator e2e**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./suites/generator -run 'TestGetESCondReturnsNilWhenConditionMissing|TestAWSGeneratorAuthSessionTokenHandling|TestCreateAWSGeneratorCredentialsSecretUpdatesData' -count=1
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2 V2_GINKGO_LABELS='fake && v2 && operational && generator' TEST_SUITES='generator'
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/suites/generator/testcase.go e2e/suites/generator/operational_v2.go
+git commit -m "Add fake v2 generator operational e2e coverage"
+```
+
+### Task 6: Add Connection-Reuse And Provider-Fanout Metrics Scenarios
+
+**Files:**
+- Modify: `e2e/suites/provider/cases/fake/operational_v2.go`
+- Modify: `e2e/suites/provider/cases/kubernetes/metrics_v2.go`
+- Modify: `e2e/framework/v2/metrics.go`
+
+- [ ] **Step 1: Write the failing metrics-scaling expectation as a spec addition**
+
+Add new operational entries that create many `ExternalSecret` resources against one backend and assert pooled connections stay bounded. Use the fake provider for the strictest deterministic check first.
+
+```go
+It("reuses one backend connection across many namespaced fake Provider consumers", func() {
+	const consumerCount = 10
+	// create ten ExternalSecrets using the same Provider backend
+	// scrape controller metrics
+	// assert grpc_pool_connections_total for address=provider-fake stays <= 2
+})
+
+It("does not create one pooled connection per ExternalSecret", func() {
+	// assert the total connection count is less than consumerCount
+})
+```
+
+- [ ] **Step 2: Run a focused fake operational label selection to verify the new scenarios are absent**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2 V2_GINKGO_LABELS='fake && v2 && operational' TEST_SUITES='provider'
+```
+
+Expected: PASS before the scaling cases are added, with no metrics-scaling assertions yet
+
+- [ ] **Step 3: Implement bounded-scaling and fanout assertions**
+
+Extend `e2e/suites/provider/cases/fake/operational_v2.go` with deterministic scaling specs.
+
+```go
+metrics, err := frameworkv2.ScrapeControllerMetrics(context.Background(), f.KubeConfig, f.KubeClientSet, frameworkv2.ProviderNamespace)
+Expect(err).ToNot(HaveOccurred())
+
+total := frameworkv2.SumMetricValues(metrics, "grpc_pool_connections_total", map[string]string{
+	"address": frameworkv2.ProviderAddress("fake"),
+})
+Expect(total).To(BeNumerically("<=", 2), "expected bounded connection reuse for one backend")
+Expect(total).To(BeNumerically("<", consumerCount))
+```
+
+For provider-fanout, create multiple `Provider` CRs that all point at the same fake backend address and assert total connections remain bounded by backend identity rather than consumer count.
+
+Also extend `e2e/suites/provider/cases/kubernetes/metrics_v2.go` with one reuse assertion for the Kubernetes provider using the same helper functions, but keep the bound loose enough to survive restarts.
+
+- [ ] **Step 4: Run focused fake and kubernetes metrics checks**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2 V2_GINKGO_LABELS='(fake || kubernetes) && v2 && operational' TEST_SUITES='provider'
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/suites/provider/cases/fake/operational_v2.go e2e/suites/provider/cases/kubernetes/metrics_v2.go e2e/framework/v2/metrics.go
+git commit -m "Add v2 connection reuse and fanout e2e checks"
+```
+
+### Task 7: Wire A Focused Operational V2 Target Into Make And CI
+
+**Files:**
+- Modify: `e2e/Makefile`
+- Modify: `e2e/makefile_test.go`
+- Modify: `Makefile`
+- Modify: `.github/actions/e2e/action.yml`
+- Modify: `.github/workflows/e2e.yml`
+
+- [ ] **Step 1: Write the failing make-target tests**
+
+Extend `e2e/makefile_test.go` with explicit coverage for a new focused target.
+
+```go
+func TestV2OperationalMakeTarget(t *testing.T) {
+	cmd := renderMakeDryRun(t, "test.v2.operational")
+	if !strings.Contains(cmd, `V2_GINKGO_LABELS="v2 && operational && fake"`) {
+		t.Fatalf("expected operational labels in target, got:\n%s", cmd)
+	}
+	if !strings.Contains(cmd, `TEST_SUITES="provider generator"`) {
+		t.Fatalf("expected provider and generator suites in target, got:\n%s", cmd)
+	}
+}
+```
+
+- [ ] **Step 2: Run the makefile tests to verify they fail**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test . -run 'TestV2OperationalMakeTarget|TestV2MakeTarget|TestClassicMakeTarget' -count=1
+```
+
+Expected: FAIL with `test.v2.operational` missing
+
+- [ ] **Step 3: Implement the focused operational targets and CI wiring**
+
+Modify `e2e/Makefile`:
+
+```make
+test.v2.operational: ## Run focused operational v2 e2e tests
+	$(MAKE) test.v2 V2_GINKGO_LABELS='v2 && operational && fake' TEST_SUITES='provider generator'
+```
+
+Modify the root `Makefile`:
+
+```make
+.PHONY: test.e2e.v2.operational
+test.e2e.v2.operational: generate ## Run focused V2 operational E2E tests
+	@$(INFO) go test v2 operational e2e-tests
+	$(MAKE) -C ./e2e test.v2.operational
+	@$(OK) go test v2 operational e2e-tests
+```
+
+Modify `.github/actions/e2e/action.yml` to allow the new target:
+
+```bash
+case "$MAKE_TARGET" in
+  test.e2e|test.e2e.v2|test.e2e.v2.operational)
+    make "$MAKE_TARGET"
+    ;;
+```
+
+Modify `.github/workflows/e2e.yml` to add a focused operational suite entry that runs a real deterministic test in CI.
+
+```yaml
+        - name: v2-operational
+          make_target: test.e2e.v2.operational
+          allow_failure: true
+```
+
+Keep `allow_failure: true` for the first rollout so the real CI run happens without immediately turning the whole workflow red while the new slice stabilizes.
+
+- [ ] **Step 4: Run the makefile tests and a local focused operational target**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test . -run 'TestV2OperationalMakeTarget|TestV2MakeTarget|TestClassicMakeTarget|TestV2MakeTargetPrunesDockerImagesInCI' -count=1
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2.operational
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add e2e/Makefile e2e/makefile_test.go Makefile .github/actions/e2e/action.yml .github/workflows/e2e.yml
+git commit -m "Wire focused v2 operational e2e into CI"
+```
+
+### Task 8: Final Verification And CI Observation
+
+**Files:**
+- Modify as needed: any files from Tasks 1-7 based on verification failures
+
+- [ ] **Step 1: Run the local verification matrix**
+
+Run:
+
+```bash
+cd e2e
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test ./framework/v2 ./suites/provider/cases/common ./suites/provider/cases/fake ./suites/provider/cases/kubernetes ./suites/generator -count=1
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2 V2_GINKGO_LABELS='(fake || kubernetes) && v2 && operational' TEST_SUITES='provider'
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off make test.v2 V2_GINKGO_LABELS='fake && v2 && operational && generator' TEST_SUITES='generator'
+TMPDIR=/home/moritz/.cache/eso-tmp GOTMPDIR=/home/moritz/.cache/eso-tmp GOCACHE=/home/moritz/.cache/eso-go-build GOMODCACHE=/home/moritz/.cache/eso-go-mod GOWORK=off go test . -run 'TestV2OperationalMakeTarget|TestV2MakeTarget|TestClassicMakeTarget|TestV2MakeTargetPrunesDockerImagesInCI' -count=1
+```
+
+Expected: PASS
+
+- [ ] **Step 2: Push the branch and watch the focused CI slice**
+
+Run:
+
+```bash
+git push
+```
+
+Then observe the `v2-operational` workflow job and record:
+
+- which deterministic operational scenario actually ran
+- whether it passed in CI
+- whether any follow-up stabilization changes were required
+
+- [ ] **Step 3: Fix CI issues one at a time if the real operational run fails**
+
+For each CI failure:
+
+1. reproduce locally with the matching label set
+2. change only the failing helper or scenario
+3. rerun the matching local verification command
+4. push and re-observe the CI job
+
+- [ ] **Step 4: Commit any final stabilization fixes**
+
+```bash
+git add <exact files changed>
+git commit -m "Stabilize v2 operational e2e coverage"
+```

+ 5 - 1
e2e/Makefile

@@ -12,6 +12,7 @@ export E2E_IMAGE_NAME ?= ghcr.io/external-secrets/external-secrets-e2e
 export GINKGO_LABELS ?= !managed && !v2
 export V2_GINKGO_LABELS ?= !managed && v2
 export TEST_SUITES ?= provider generator flux argocd
+export V2_TEST_SUITES ?= provider
 export GOCACHE ?= $(CURDIR)/.cache/go-build
 export GOMODCACHE ?= $(CURDIR)/.cache/go-mod
 
@@ -91,7 +92,10 @@ endif
 ifeq ($(CI),true)
 	docker system prune --all --force --volumes
 endif
-	KUBECTL_CONTEXT="$(KIND_CONTEXT)" GINKGO_LABELS="$(V2_GINKGO_LABELS)" E2E_PROVIDER_MODE="v2" TEST_SUITES="provider" E2E_SKIP_HELM_DEPENDENCY_UPDATE="true" ./run.sh
+	KUBECTL_CONTEXT="$(KIND_CONTEXT)" GINKGO_LABELS="$(V2_GINKGO_LABELS)" E2E_PROVIDER_MODE="v2" TEST_SUITES="$(V2_TEST_SUITES)" E2E_SKIP_HELM_DEPENDENCY_UPDATE="true" ./run.sh
+
+test.v2.operational: ## Run focused operational v2 e2e tests
+	$(MAKE) test.v2 V2_GINKGO_LABELS='v2 && operational && fake' V2_TEST_SUITES='provider'
 
 test.managed: e2e-image ## Run e2e tests against current kube context
 	$(MAKE) -C ../ docker.build \

+ 32 - 1
e2e/framework/addon/chart.go

@@ -144,6 +144,14 @@ func (c *HelmChart) uninstallArgs() []string {
 	return []string{"uninstall", "--namespace", c.Namespace, c.ReleaseName, "--wait", "--ignore-not-found"}
 }
 
+func (c *HelmChart) cleanupUninstallArgs() []string {
+	return []string{"uninstall", "--namespace", c.Namespace, c.ReleaseName, "--ignore-not-found"}
+}
+
+func (c *HelmChart) releaseStatusArgs() []string {
+	return []string{"status", "--namespace", c.Namespace, c.ReleaseName}
+}
+
 func (c *HelmChart) runInstall(args []string) ([]byte, error) {
 	log.Logf("installing chart with args: %+q", args)
 	cmd, err := frameworkutil.Command("helm", args...)
@@ -154,21 +162,44 @@ func (c *HelmChart) runInstall(args []string) ([]byte, error) {
 }
 
 func (c *HelmChart) cleanupExistingRelease() error {
-	cmd, err := frameworkutil.Command("helm", c.uninstallArgs()...)
+	cmd, err := frameworkutil.Command("helm", c.cleanupUninstallArgs()...)
 	if err != nil {
 		return fmt.Errorf("resolve helm executable: %w", err)
 	}
 	output, err := cmd.CombinedOutput()
 	if err != nil && !strings.Contains(string(output), "release: not found") {
+		statusOutput, statusErr := c.releaseStatus()
+		if canIgnoreHelmCleanupError(string(statusOutput)) {
+			return nil
+		}
+		if statusErr != nil {
+			return fmt.Errorf("unable to uninstall stale helm release: %w: %s (status check failed: %v: %s)", err, string(output), statusErr, string(statusOutput))
+		}
 		return fmt.Errorf("unable to uninstall stale helm release: %w: %s", err, string(output))
 	}
 	return nil
 }
 
+func (c *HelmChart) releaseStatus() ([]byte, error) {
+	cmd, err := frameworkutil.Command("helm", c.releaseStatusArgs()...)
+	if err != nil {
+		return nil, fmt.Errorf("resolve helm executable: %w", err)
+	}
+	return cmd.CombinedOutput()
+}
+
 func isHelmReleaseNameInUseError(output string) bool {
 	return strings.Contains(output, "cannot re-use a name that is still in use")
 }
 
+func isHelmReleaseNotFoundError(output string) bool {
+	return strings.Contains(output, "release: not found")
+}
+
+func canIgnoreHelmCleanupError(statusOutput string) bool {
+	return isHelmReleaseNotFoundError(statusOutput)
+}
+
 // Uninstall removes the chart aswell as the repo.
 func (c *HelmChart) Uninstall() error {
 	cmd, err := frameworkutil.Command("helm", c.uninstallArgs()...)

+ 35 - 1
e2e/framework/addon/chart_test.go

@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-
 package addon
 
 import "testing"
@@ -74,6 +73,20 @@ func TestUninstallArgsIncludeIgnoreNotFound(t *testing.T) {
 	}
 }
 
+func TestCleanupUninstallArgsOmitWait(t *testing.T) {
+	args := (&HelmChart{
+		ReleaseName: "external-secrets",
+		Namespace:   "external-secrets-system",
+	}).cleanupUninstallArgs()
+
+	if contains(args, "--wait") {
+		t.Fatalf("expected stale release cleanup uninstall args to omit --wait, got %v", args)
+	}
+	if !contains(args, "--ignore-not-found") {
+		t.Fatalf("expected stale release cleanup uninstall args to include --ignore-not-found, got %v", args)
+	}
+}
+
 func TestIsHelmReleaseNameInUseError(t *testing.T) {
 	if !isHelmReleaseNameInUseError("Error: INSTALLATION FAILED: cannot re-use a name that is still in use") {
 		t.Fatal("expected stale release message to be detected")
@@ -83,6 +96,27 @@ func TestIsHelmReleaseNameInUseError(t *testing.T) {
 	}
 }
 
+func TestIsHelmReleaseNotFoundError(t *testing.T) {
+	if !isHelmReleaseNotFoundError("Error: release: not found") {
+		t.Fatal("expected missing release message to be detected")
+	}
+	if isHelmReleaseNotFoundError("STATUS: deployed") {
+		t.Fatal("did not expect deployed release output to be treated as missing")
+	}
+}
+
+func TestCanIgnoreHelmCleanupErrorWhenReleaseMissingAfterUninstallFailure(t *testing.T) {
+	if !canIgnoreHelmCleanupError("Error: release: not found") {
+		t.Fatal("expected cleanup error to be ignored when release status reports not found")
+	}
+}
+
+func TestCanIgnoreHelmCleanupErrorWhenReleaseStillExists(t *testing.T) {
+	if canIgnoreHelmCleanupError("NAME: external-secrets\nSTATUS: uninstalling") {
+		t.Fatal("did not expect cleanup error to be ignored when release still exists")
+	}
+}
+
 func contains(values []string, want string) bool {
 	for _, value := range values {
 		if value == want {

+ 1 - 1
e2e/framework/addon/eso.go

@@ -214,7 +214,7 @@ func (l *ESO) Install() error {
 		return err
 	}
 
-	return nil
+	return waitForExternalSecretWebhookReady(l.Namespace)
 }
 
 func (l *ESO) Uninstall() error {

+ 1 - 21
e2e/framework/addon/eso_argocd_application.go

@@ -17,11 +17,8 @@ limitations under the License.
 package addon
 
 import (
-	"bytes"
 	"context"
-	"crypto/tls"
 	"fmt"
-	"net/http"
 	"strings"
 	"time"
 
@@ -140,24 +137,7 @@ func (c *ArgoCDApplication) Install() error {
 		return fmt.Errorf("failed waiting for argo app to become ready: %w", err)
 	}
 
-	// we have to wait for the webhook to become ready
-	tr := &http.Transport{
-		// nolint:gosec
-		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-	}
-	client := &http.Client{Transport: tr}
-	return wait.PollUntilContextTimeout(GinkgoT().Context(), time.Second, time.Minute*5, true, func(ctx context.Context) (bool, error) {
-		const payload = `{"apiVersion": "admission.k8s.io/v1","kind": "AdmissionReview","request": {"uid": "test","kind": {"group": "external-secrets.io","version": "v1","kind": "ExternalSecret"}, "resource": {"group": "external-secrets.io","version": "v1","kind": "ExternalSecret"},"dryRun": true, "operation": "CREATE", "userInfo":{"username":"test","uid":"test","groups":[],"extra":{}}}}`
-		res, err := client.Post("https://external-secrets-webhook.external-secrets.svc.cluster.local/validate-external-secrets-io-v1-externalsecret", "application/json", bytes.NewBufferString(payload))
-		if err != nil {
-			return false, nil
-		}
-		defer func() {
-			_ = res.Body.Close()
-		}()
-		GinkgoWriter.Printf("webhook res: %d", res.StatusCode)
-		return res.StatusCode == http.StatusOK, nil
-	})
+	return waitForExternalSecretWebhookReady(c.DestinationNamespace)
 }
 
 // Uninstall removes the chart aswell as the repo.

+ 1 - 21
e2e/framework/addon/eso_flux_helm.go

@@ -17,10 +17,7 @@ limitations under the License.
 package addon
 
 import (
-	"bytes"
 	"context"
-	"crypto/tls"
-	"net/http"
 	"time"
 
 	fluxhelm "github.com/fluxcd/helm-controller/api/v2"
@@ -130,24 +127,7 @@ func (c *FluxHelmRelease) Install() error {
 		return err
 	}
 
-	// we have to wait for the webhook to become ready
-	tr := &http.Transport{
-		// nolint:gosec
-		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-	}
-	client := &http.Client{Transport: tr}
-	return wait.PollUntilContextTimeout(GinkgoT().Context(), time.Second, time.Minute*5, true, func(ctx context.Context) (bool, error) {
-		const payload = `{"apiVersion": "admission.k8s.io/v1","kind": "AdmissionReview","request": {"uid": "test","kind": {"group": "external-secrets.io","version": "v1","kind": "ExternalSecret"}, "resource": "external-secrets.io/v1.externalsecrets","dryRun": true, "operation": "CREATE", "userInfo":{"username":"test","uid":"test","groups":[],"extra":{}}}}`
-		res, err := client.Post("https://external-secrets-webhook.external-secrets.svc.cluster.local/validate-external-secrets-io-v1-externalsecret", "application/json", bytes.NewBufferString(payload))
-		if err != nil {
-			return false, nil
-		}
-		defer func() {
-			_ = res.Body.Close()
-		}()
-		GinkgoWriter.Printf("webhook res: %d", res.StatusCode)
-		return res.StatusCode == http.StatusOK, nil
-	})
+	return waitForExternalSecretWebhookReady(c.TargetNamespace)
 }
 
 // Uninstall removes the chart aswell as the repo.

+ 43 - 7
e2e/framework/addon/uninstall_eso_crds.go

@@ -17,29 +17,65 @@ limitations under the License.
 package addon
 
 import (
+	"context"
 	"strings"
+	"time"
 
 	. "github.com/onsi/ginkgo/v2"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/util/wait"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+var (
+	externalSecretsCRDDeletePollInterval = time.Second
+	externalSecretsCRDDeleteTimeout      = 5 * time.Minute
+)
+
 func uninstallCRDs(cfg *Config) error {
 	By("Uninstalling eso CRDs")
-	var crdList apiextensionsv1.CustomResourceDefinitionList
-	if err := cfg.CRClient.List(GinkgoT().Context(), &crdList); err != nil {
+	crdList, err := listExternalSecretsCRDs(GinkgoT().Context(), cfg)
+	if err != nil {
 		return err
 	}
 
-	for _, crd := range crdList.Items {
-		if !strings.Contains(crd.Spec.Group, "external-secrets.io") {
-			continue
-		}
+	for _, crd := range crdList {
 		err := cfg.CRClient.Delete(GinkgoT().Context(), &crd, &client.DeleteOptions{})
 		if err != nil && !apierrors.IsNotFound(err) {
 			return err
 		}
 	}
-	return nil
+
+	if len(crdList) == 0 {
+		return nil
+	}
+
+	return wait.PollUntilContextTimeout(GinkgoT().Context(), externalSecretsCRDDeletePollInterval, externalSecretsCRDDeleteTimeout, true, func(ctx context.Context) (bool, error) {
+		crds, err := listExternalSecretsCRDs(ctx, cfg)
+		if err != nil {
+			return false, err
+		}
+		return len(crds) == 0, nil
+	})
+}
+
+func listExternalSecretsCRDs(ctx context.Context, cfg *Config) ([]apiextensionsv1.CustomResourceDefinition, error) {
+	var crdList apiextensionsv1.CustomResourceDefinitionList
+	if err := cfg.CRClient.List(ctx, &crdList); err != nil {
+		return nil, err
+	}
+
+	crds := make([]apiextensionsv1.CustomResourceDefinition, 0, len(crdList.Items))
+	for _, crd := range crdList.Items {
+		if !isExternalSecretsCRDGroup(crd.Spec.Group) {
+			continue
+		}
+		crds = append(crds, crd)
+	}
+	return crds, nil
+}
+
+func isExternalSecretsCRDGroup(group string) bool {
+	return strings.Contains(group, "external-secrets.io")
 }

+ 75 - 0
e2e/framework/addon/uninstall_eso_crds_test.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 addon
+
+import (
+	"context"
+	"testing"
+
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func TestIsExternalSecretsCRDGroup(t *testing.T) {
+	t.Helper()
+
+	if !isExternalSecretsCRDGroup("external-secrets.io") {
+		t.Fatal("expected external-secrets.io group to match")
+	}
+	if !isExternalSecretsCRDGroup("provider.external-secrets.io") {
+		t.Fatal("expected provider.external-secrets.io group to match")
+	}
+	if isExternalSecretsCRDGroup("example.com") {
+		t.Fatal("did not expect unrelated group to match")
+	}
+}
+
+func TestListExternalSecretsCRDsFiltersByGroup(t *testing.T) {
+	t.Helper()
+
+	scheme := runtime.NewScheme()
+	if err := apiextensionsv1.AddToScheme(scheme); err != nil {
+		t.Fatalf("add apiextensions scheme: %v", err)
+	}
+
+	cfg := &Config{
+		CRClient: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
+			&apiextensionsv1.CustomResourceDefinition{
+				ObjectMeta: metav1.ObjectMeta{Name: "externalsecrets.external-secrets.io"},
+				Spec: apiextensionsv1.CustomResourceDefinitionSpec{Group: "external-secrets.io"},
+			},
+			&apiextensionsv1.CustomResourceDefinition{
+				ObjectMeta: metav1.ObjectMeta{Name: "providers.provider.external-secrets.io"},
+				Spec: apiextensionsv1.CustomResourceDefinitionSpec{Group: "provider.external-secrets.io"},
+			},
+			&apiextensionsv1.CustomResourceDefinition{
+				ObjectMeta: metav1.ObjectMeta{Name: "widgets.example.com"},
+				Spec: apiextensionsv1.CustomResourceDefinitionSpec{Group: "example.com"},
+			},
+		).Build(),
+	}
+
+	crds, err := listExternalSecretsCRDs(context.Background(), cfg)
+	if err != nil {
+		t.Fatalf("listExternalSecretsCRDs returned error: %v", err)
+	}
+	if len(crds) != 2 {
+		t.Fatalf("expected 2 external-secrets CRDs, got %d", len(crds))
+	}
+}

+ 61 - 0
e2e/framework/addon/webhook.go

@@ -0,0 +1,61 @@
+/*
+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 addon
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"fmt"
+	"net/http"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	"k8s.io/apimachinery/pkg/util/wait"
+)
+
+const externalSecretValidationReview = `{"apiVersion":"admission.k8s.io/v1","kind":"AdmissionReview","request":{"uid":"test","kind":{"group":"external-secrets.io","version":"v1","kind":"ExternalSecret"},"resource":{"group":"external-secrets.io","version":"v1","resource":"externalsecrets"},"dryRun":true,"operation":"CREATE","userInfo":{"username":"test","uid":"test","groups":[],"extra":{}}}}`
+
+var (
+	externalSecretWebhookURL = func(namespace string) string {
+		return fmt.Sprintf("https://external-secrets-webhook.%s.svc.cluster.local/validate-external-secrets-io-v1-externalsecret", namespace)
+	}
+	webhookReadyPollInterval = time.Second
+	webhookReadyTimeout      = 5 * time.Minute
+	webhookReadyContext      = func() context.Context { return GinkgoT().Context() }
+)
+
+func waitForExternalSecretWebhookReady(namespace string) error {
+	tr := &http.Transport{
+		// nolint:gosec
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+	}
+	client := &http.Client{Transport: tr}
+	url := externalSecretWebhookURL(namespace)
+
+	return wait.PollUntilContextTimeout(webhookReadyContext(), webhookReadyPollInterval, webhookReadyTimeout, true, func(ctx context.Context) (bool, error) {
+		res, err := client.Post(url, "application/json", bytes.NewBufferString(externalSecretValidationReview))
+		if err != nil {
+			return false, nil
+		}
+		defer func() {
+			_ = res.Body.Close()
+		}()
+		GinkgoWriter.Printf("webhook res: %d", res.StatusCode)
+		return res.StatusCode == http.StatusOK, nil
+	})
+}

+ 92 - 0
e2e/framework/addon/webhook_test.go

@@ -0,0 +1,92 @@
+/*
+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 addon
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+)
+
+func TestWaitForExternalSecretWebhookReadyRetriesUntilOK(t *testing.T) {
+	t.Helper()
+
+	originalURL := externalSecretWebhookURL
+	originalPollInterval := webhookReadyPollInterval
+	originalTimeout := webhookReadyTimeout
+	originalContext := webhookReadyContext
+	t.Cleanup(func() {
+		externalSecretWebhookURL = originalURL
+		webhookReadyPollInterval = originalPollInterval
+		webhookReadyTimeout = originalTimeout
+		webhookReadyContext = originalContext
+	})
+
+	attempts := 0
+	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		attempts++
+		if attempts < 3 {
+			http.Error(w, "not ready", http.StatusServiceUnavailable)
+			return
+		}
+		w.WriteHeader(http.StatusOK)
+	}))
+	defer server.Close()
+
+	externalSecretWebhookURL = func(string) string { return server.URL }
+	webhookReadyPollInterval = 10 * time.Millisecond
+	webhookReadyTimeout = time.Second
+	webhookReadyContext = context.Background
+
+	if err := waitForExternalSecretWebhookReady("external-secrets-system"); err != nil {
+		t.Fatalf("waitForExternalSecretWebhookReady returned error: %v", err)
+	}
+	if attempts != 3 {
+		t.Fatalf("expected 3 webhook attempts, got %d", attempts)
+	}
+}
+
+func TestWaitForExternalSecretWebhookReadyTimesOut(t *testing.T) {
+	t.Helper()
+
+	originalURL := externalSecretWebhookURL
+	originalPollInterval := webhookReadyPollInterval
+	originalTimeout := webhookReadyTimeout
+	originalContext := webhookReadyContext
+	t.Cleanup(func() {
+		externalSecretWebhookURL = originalURL
+		webhookReadyPollInterval = originalPollInterval
+		webhookReadyTimeout = originalTimeout
+		webhookReadyContext = originalContext
+	})
+
+	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		http.Error(w, "not ready", http.StatusServiceUnavailable)
+	}))
+	defer server.Close()
+
+	externalSecretWebhookURL = func(string) string { return server.URL }
+	webhookReadyPollInterval = 10 * time.Millisecond
+	webhookReadyTimeout = 50 * time.Millisecond
+	webhookReadyContext = context.Background
+
+	if err := waitForExternalSecretWebhookReady("external-secrets-system"); err == nil {
+		t.Fatal("expected waitForExternalSecretWebhookReady to time out")
+	}
+}

+ 8 - 1
e2e/framework/framework.go

@@ -62,6 +62,8 @@ type Framework struct {
 	DefaultPushSecretStoreRefAPIVersion string
 }
 
+var newFrameworkConfig = util.NewConfig
+
 // New returns a new framework instance with defaults.
 func New(baseName string) *Framework {
 	f := &Framework{
@@ -74,7 +76,7 @@ func New(baseName string) *Framework {
 		f.DefaultSecretStoreRefKind = esv1.ProviderKindStr
 		f.DefaultPushSecretStoreRefKind = esv1.ProviderKindStr
 	}
-	f.KubeConfig, f.KubeClientSet, f.CRClient = util.NewConfig()
+	f.refreshClients()
 
 	BeforeEach(f.BeforeEach)
 	AfterEach(f.AfterEach)
@@ -82,9 +84,14 @@ func New(baseName string) *Framework {
 	return f
 }
 
+func (f *Framework) refreshClients() {
+	f.KubeConfig, f.KubeClientSet, f.CRClient = newFrameworkConfig()
+}
+
 // BeforeEach creates a namespace.
 func (f *Framework) BeforeEach() {
 	var err error
+	f.refreshClients()
 	err = util.CleanupTerminatingE2ENamespaces(GinkgoT().Context(), f.CRClient)
 	Expect(err).ToNot(HaveOccurred())
 	f.Namespace, err = util.CreateKubeNamespace(f.BaseName, f.KubeClientSet)

+ 60 - 0
e2e/framework/framework_test.go

@@ -0,0 +1,60 @@
+/*
+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"
+
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+	crclient "sigs.k8s.io/controller-runtime/pkg/client"
+	crfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func TestRefreshClientsReloadsFrameworkClients(t *testing.T) {
+	t.Helper()
+
+	originalNewConfig := newFrameworkConfig
+	t.Cleanup(func() {
+		newFrameworkConfig = originalNewConfig
+	})
+
+	wantConfig := &rest.Config{Host: "https://refreshed.example"}
+	wantClientset := &kubernetes.Clientset{}
+	wantCRClient := crfake.NewClientBuilder().Build()
+	newFrameworkConfig = func() (*rest.Config, *kubernetes.Clientset, crclient.Client) {
+		return wantConfig, wantClientset, wantCRClient
+	}
+
+	f := &Framework{
+		KubeConfig:    &rest.Config{Host: "https://stale.example"},
+		KubeClientSet: &kubernetes.Clientset{},
+		CRClient:      crfake.NewClientBuilder().Build(),
+	}
+
+	f.refreshClients()
+
+	if f.KubeConfig != wantConfig {
+		t.Fatalf("expected refreshed kube config")
+	}
+	if f.KubeClientSet != wantClientset {
+		t.Fatalf("expected refreshed kube clientset")
+	}
+	if f.CRClient != wantCRClient {
+		t.Fatalf("expected refreshed controller-runtime client")
+	}
+}

+ 66 - 4
e2e/framework/testcase.go

@@ -17,21 +17,31 @@ limitations under the License.
 package framework
 
 import (
+	"context"
+	"strings"
 	"time"
 
 	//nolint
 	"github.com/external-secrets/external-secrets-e2e/framework/log"
+	"github.com/external-secrets/external-secrets-e2e/framework/util"
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	v1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
 var TargetSecretName = "target-secret"
 
+const (
+	createObjectRetryPollInterval = 250 * time.Millisecond
+	createObjectRetryTimeout      = 30 * time.Second
+)
+
 // TestCase contains the test infra to run a table driven test.
 type TestCase struct {
 	Framework               *Framework
@@ -134,7 +144,7 @@ func externalSecretTargetName(tc *TestCase) string {
 func generateAdditionalObjects(tc *TestCase) {
 	if tc.AdditionalObjects != nil {
 		for _, obj := range tc.AdditionalObjects {
-			err := tc.Framework.CRClient.Create(GinkgoT().Context(), obj)
+			err := tc.Framework.CreateObjectWithRetry(obj)
 			Expect(err).ToNot(HaveOccurred())
 		}
 	}
@@ -144,7 +154,7 @@ func createProvidedExternalSecret(tc *TestCase) {
 	if tc.ExternalSecret == nil {
 		return
 	}
-	err := tc.Framework.CRClient.Create(GinkgoT().Context(), tc.ExternalSecret)
+	err := tc.Framework.CreateObjectWithRetry(tc.ExternalSecret)
 	Expect(err).ToNot(HaveOccurred())
 }
 
@@ -166,14 +176,14 @@ func TableFuncWithPushSecret(f *Framework, prov SecretStoreProvider, pushClient
 		generateAdditionalObjects(tc)
 
 		if tc.PushSecretSource != nil {
-			err := tc.Framework.CRClient.Create(GinkgoT().Context(), tc.PushSecretSource)
+			err := tc.Framework.CreateObjectWithRetry(tc.PushSecretSource)
 			Expect(err).ToNot(HaveOccurred())
 		}
 
 		// create v1alpha1 push secret, if provided
 		if tc.PushSecret != nil {
 			// create v1beta1 external secret otherwise
-			err = tc.Framework.CRClient.Create(GinkgoT().Context(), tc.PushSecret)
+			err = tc.Framework.CreateObjectWithRetry(tc.PushSecret)
 			Expect(err).ToNot(HaveOccurred())
 		}
 
@@ -241,3 +251,55 @@ func makeDefaultPushSecretTestCase(f *Framework) *TestCase {
 		},
 	}
 }
+
+func (f *Framework) CreateObjectWithRetry(obj client.Object) error {
+	return f.CreateObjectWithRetryContext(GinkgoT().Context(), obj)
+}
+
+func (f *Framework) CreateObjectWithRetryContext(ctx context.Context, obj client.Object) error {
+	return wait.PollUntilContextTimeout(ctx, createObjectRetryPollInterval, createObjectRetryTimeout, true, func(ctx context.Context) (bool, error) {
+		err := f.CRClient.Create(ctx, obj)
+		switch {
+		case err == nil, apierrors.IsAlreadyExists(err):
+			return true, nil
+		case isRetryableCreateObjectError(err):
+			if f.KubeConfig != nil {
+				f.refreshClients()
+			}
+			return false, nil
+		default:
+			return false, err
+		}
+	})
+}
+
+func createObjectWithRetryPolling(ctx context.Context, c client.Client, obj client.Object, pollInterval, timeout time.Duration) error {
+	return wait.PollUntilContextTimeout(ctx, pollInterval, timeout, true, func(ctx context.Context) (bool, error) {
+		err := c.Create(ctx, obj)
+		switch {
+		case err == nil, apierrors.IsAlreadyExists(err):
+			return true, nil
+		case isRetryableCreateObjectError(err):
+			return false, nil
+		default:
+			return false, err
+		}
+	})
+}
+
+func isRetryableCreateObjectError(err error) bool {
+	if util.IsMissingAPIResourceError(err) {
+		return true
+	}
+	if apierrors.IsNotFound(err) && strings.Contains(err.Error(), "could not find the requested resource") {
+		return true
+	}
+
+	if !(apierrors.IsInternalError(err) || apierrors.IsServiceUnavailable(err)) {
+		return false
+	}
+
+	msg := strings.ToLower(err.Error())
+	return strings.Contains(msg, "failed calling webhook") &&
+		(strings.Contains(msg, "connection refused") || strings.Contains(msg, "no endpoints available"))
+}

+ 87 - 0
e2e/framework/testcase_test.go

@@ -17,10 +17,17 @@ limitations under the License.
 package framework
 
 import (
+	"context"
 	"testing"
+	"time"
 
 	. "github.com/onsi/gomega"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1api "k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 )
@@ -115,3 +122,83 @@ func TestExternalSecretTargetNameFallsBackToExternalSecretName(t *testing.T) {
 
 	Expect(externalSecretTargetName(tc)).To(Equal("external-secret-name"))
 }
+
+type flakyCreateClient struct {
+	client.Client
+	createErrs  []error
+	createCalls int
+}
+
+func (c *flakyCreateClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
+	callIndex := c.createCalls
+	c.createCalls++
+	if callIndex < len(c.createErrs) && c.createErrs[callIndex] != nil {
+		return c.createErrs[callIndex]
+	}
+	return c.Client.Create(ctx, obj, opts...)
+}
+
+func TestCreateObjectWithRetryPollingRetriesMissingAPIResourceErrors(t *testing.T) {
+	t.Helper()
+	RegisterTestingT(t)
+
+	scheme := runtime.NewScheme()
+	Expect(esv1.AddToScheme(scheme)).To(Succeed())
+
+	baseClient := fake.NewClientBuilder().WithScheme(scheme).Build()
+	cl := &flakyCreateClient{
+		Client: baseClient,
+		createErrs: []error{
+			&metav1api.NoResourceMatchError{},
+		},
+	}
+
+	es := &esv1.ExternalSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "retry-external-secret",
+			Namespace: "default",
+		},
+	}
+
+	err := createObjectWithRetryPolling(context.Background(), cl, es, 5*time.Millisecond, 100*time.Millisecond)
+	Expect(err).NotTo(HaveOccurred())
+	Expect(cl.createCalls).To(Equal(2))
+
+	var created esv1.ExternalSecret
+	Expect(baseClient.Get(context.Background(), client.ObjectKeyFromObject(es), &created)).To(Succeed())
+}
+
+func TestCreateObjectWithRetryPollingRetriesMissingResourceEndpointErrors(t *testing.T) {
+	t.Helper()
+	RegisterTestingT(t)
+
+	scheme := runtime.NewScheme()
+	Expect(esv1.AddToScheme(scheme)).To(Succeed())
+
+	baseClient := fake.NewClientBuilder().WithScheme(scheme).Build()
+	cl := &flakyCreateClient{
+		Client: baseClient,
+		createErrs: []error{
+			&apierrors.StatusError{ErrStatus: metav1.Status{
+				Status:  metav1.StatusFailure,
+				Message: "the server could not find the requested resource (post externalsecrets.external-secrets.io)",
+				Reason:  metav1.StatusReasonNotFound,
+				Code:    404,
+			}},
+		},
+	}
+
+	es := &esv1.ExternalSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "retry-endpoint-external-secret",
+			Namespace: "default",
+		},
+	}
+
+	err := createObjectWithRetryPolling(context.Background(), cl, es, 5*time.Millisecond, 100*time.Millisecond)
+	Expect(err).NotTo(HaveOccurred())
+	Expect(cl.createCalls).To(Equal(2))
+
+	var created esv1.ExternalSecret
+	Expect(baseClient.Get(context.Background(), client.ObjectKeyFromObject(es), &created)).To(Succeed())
+}

+ 5 - 0
e2e/framework/util/util.go

@@ -31,6 +31,7 @@ import (
 	v1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -104,6 +105,10 @@ func IsE2ETestNamespace(namespace string) bool {
 	return strings.HasPrefix(namespace, e2eNamespacePrefix)
 }
 
+func IsMissingAPIResourceError(err error) bool {
+	return err != nil && meta.IsNoMatchError(err)
+}
+
 func ClearKnownNamespaceFinalizers(ctx context.Context, c crclient.Client, namespace string) error {
 	var secretList v1.SecretList
 	if err := c.List(ctx, &secretList, crclient.InNamespace(namespace)); err != nil {

+ 26 - 2
e2e/framework/util/util_test.go

@@ -21,8 +21,11 @@ import (
 	"testing"
 
 	corev1 "k8s.io/api/core/v1"
+	metav1api "k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@@ -78,8 +81,8 @@ func TestCleanupTerminatingE2ENamespaces(t *testing.T) {
 	cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
 		&corev1.Namespace{
 			ObjectMeta: metav1.ObjectMeta{
-				Name:       "e2e-tests-demo-12345",
-				Finalizers: []string{"kubernetes"},
+				Name:              "e2e-tests-demo-12345",
+				Finalizers:        []string{"kubernetes"},
 				DeletionTimestamp: &now,
 			},
 		},
@@ -124,3 +127,24 @@ func TestCleanupTerminatingE2ENamespaces(t *testing.T) {
 		t.Fatalf("expected non-e2e namespace secret finalizers to remain, got %v", untouched.Finalizers)
 	}
 }
+
+func TestIsMissingAPIResourceError(t *testing.T) {
+	t.Parallel()
+
+	gv := schema.GroupVersion{Group: "generators.external-secrets.io", Version: "v1alpha1"}
+	discoveryErr := apiutil.ErrResourceDiscoveryFailed{
+		gv: &metav1api.NoResourceMatchError{
+			PartialResource: gv.WithResource(""),
+		},
+	}
+
+	if !IsMissingAPIResourceError(&discoveryErr) {
+		t.Fatal("expected missing resource discovery failure to be treated as ignorable")
+	}
+	if !IsMissingAPIResourceError(&metav1api.NoResourceMatchError{}) {
+		t.Fatal("expected no-match error to be treated as ignorable")
+	}
+	if IsMissingAPIResourceError(context.Canceled) {
+		t.Fatal("did not expect unrelated errors to be treated as ignorable")
+	}
+}

+ 30 - 11
e2e/framework/v2/helpers.go

@@ -28,6 +28,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/util/retry"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/external-secrets/external-secrets-e2e/framework"
@@ -219,6 +220,10 @@ func WaitForProviderConnectionCondition(f *framework.Framework, namespace, name
 }
 
 func WaitForClusterProviderReady(f *framework.Framework, name string, timeout time.Duration) *esv1.ClusterProvider {
+	return WaitForClusterProviderCondition(f, name, metav1.ConditionTrue, timeout)
+}
+
+func WaitForClusterProviderCondition(f *framework.Framework, name string, status metav1.ConditionStatus, timeout time.Duration) *esv1.ClusterProvider {
 	var clusterProvider esv1.ClusterProvider
 	Eventually(func() bool {
 		err := f.CRClient.Get(context.Background(),
@@ -230,30 +235,44 @@ func WaitForClusterProviderReady(f *framework.Framework, name string, timeout ti
 		}
 
 		for _, condition := range clusterProvider.Status.Conditions {
-			if condition.Type == "Ready" && condition.Status == metav1.ConditionTrue {
+			if condition.Type == "Ready" && condition.Status == status {
 				return true
 			}
 		}
 		return false
-	}, timeout, time.Second).Should(BeTrue(), "ClusterProvider should become ready")
+	}, timeout, time.Second).Should(BeTrue(), fmt.Sprintf("ClusterProvider should become %s", status))
 
 	return &clusterProvider
 }
 
 func VerifyProviderConnectionCapabilities(f *framework.Framework, namespace, name string, expected esv1.ProviderCapabilities) {
-	var provider esv1.Provider
-	Expect(f.CRClient.Get(context.Background(),
-		types.NamespacedName{Name: name, Namespace: namespace},
-		&provider)).To(Succeed())
-
-	Expect(provider.Status.Capabilities).NotTo(BeEmpty())
-	Expect(provider.Status.Capabilities).To(Equal(expected))
+	Eventually(func(g Gomega) {
+		var provider esv1.Provider
+		g.Expect(f.CRClient.Get(context.Background(),
+			types.NamespacedName{Name: name, Namespace: namespace},
+			&provider)).To(Succeed())
+		g.Expect(provider.Status.Capabilities).NotTo(BeEmpty())
+		g.Expect(provider.Status.Capabilities).To(Equal(expected))
+	}, 30*time.Second, time.Second).Should(Succeed())
 }
 
 func createOrIgnoreAlreadyExists(f *framework.Framework, obj client.Object) error {
 	err := f.CRClient.Create(context.Background(), obj)
-	if err == nil || apierrors.IsAlreadyExists(err) {
+	if err == nil {
 		return nil
 	}
-	return err
+	if !apierrors.IsAlreadyExists(err) {
+		return err
+	}
+
+	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
+		existing := obj.DeepCopyObject().(client.Object)
+		if err := f.CRClient.Get(context.Background(), client.ObjectKeyFromObject(obj), existing); err != nil {
+			return err
+		}
+
+		obj.SetResourceVersion(existing.GetResourceVersion())
+		obj.SetUID(existing.GetUID())
+		return f.CRClient.Update(context.Background(), obj)
+	})
 }

+ 100 - 12
e2e/framework/v2/metrics.go

@@ -23,6 +23,7 @@ import (
 	"io"
 	"net/http"
 	"regexp"
+	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -79,6 +80,38 @@ func GetMetricValue(metrics MetricsMap, metricName string, matchLabels map[strin
 	return 0, false
 }
 
+func SumMetricValues(metrics MetricsMap, metricName string, matchLabels map[string]string) float64 {
+	samples, exists := metrics[metricName]
+	if !exists {
+		return 0
+	}
+
+	var total float64
+	for _, sample := range samples {
+		if labelsMatch(sample.Labels, matchLabels) {
+			total += sample.Value
+		}
+	}
+
+	return total
+}
+
+func CountMetricSamples(metrics MetricsMap, metricName string, matchLabels map[string]string) int {
+	samples, exists := metrics[metricName]
+	if !exists {
+		return 0
+	}
+
+	count := 0
+	for _, sample := range samples {
+		if labelsMatch(sample.Labels, matchLabels) {
+			count++
+		}
+	}
+
+	return count
+}
+
 func ExpectMetricExists(metrics MetricsMap, metricName string) {
 	_, exists := metrics[metricName]
 	if !exists {
@@ -86,6 +119,7 @@ func ExpectMetricExists(metrics MetricsMap, metricName string) {
 		for name := range metrics {
 			availableMetrics = append(availableMetrics, name)
 		}
+		sort.Strings(availableMetrics)
 		GinkgoWriter.Printf("Available metrics: %v\n", availableMetrics)
 	}
 	Expect(exists).To(BeTrue(), "metric %s should exist", metricName)
@@ -104,8 +138,21 @@ func ExpectMetricGreaterThan(metrics MetricsMap, metricName string, matchLabels
 }
 
 func WaitForMetric(ctx context.Context, scraper func() (MetricsMap, error), metricName string, matchLabels map[string]string, minValue float64, timeout time.Duration) error {
-	deadline := time.Now().Add(timeout)
-	for time.Now().Before(deadline) {
+	ticker := time.NewTicker(time.Second)
+	defer ticker.Stop()
+
+	timer := time.NewTimer(timeout)
+	defer timer.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			return fmt.Errorf("waiting for metric %s canceled: %w", metricName, ctx.Err())
+		case <-timer.C:
+			return fmt.Errorf("timeout waiting for metric %s with labels %v to reach %f", metricName, matchLabels, minValue)
+		default:
+		}
+
 		metrics, err := scraper()
 		if err == nil {
 			value, found := GetMetricValue(metrics, metricName, matchLabels)
@@ -113,10 +160,16 @@ func WaitForMetric(ctx context.Context, scraper func() (MetricsMap, error), metr
 				return nil
 			}
 		}
-		time.Sleep(time.Second)
+
+		select {
+		case <-ctx.Done():
+			return fmt.Errorf("waiting for metric %s canceled: %w", metricName, ctx.Err())
+		case <-timer.C:
+			return fmt.Errorf("timeout waiting for metric %s with labels %v to reach %f", metricName, matchLabels, minValue)
+		case <-ticker.C:
+		}
 	}
 
-	return fmt.Errorf("timeout waiting for metric %s with labels %v to reach %f", metricName, matchLabels, minValue)
 }
 
 func scrapePodMetrics(ctx context.Context, config *rest.Config, clientset kubernetes.Interface, namespace, podName string, podPort int) (MetricsMap, error) {
@@ -126,14 +179,12 @@ func scrapePodMetrics(ctx context.Context, config *rest.Config, clientset kubern
 	}
 	defer cleanup()
 
-	time.Sleep(time.Second)
-
-	body, err := scrapeMetrics(ctx, address)
+	body, err := waitForMetricsEndpoint(ctx, address, 10*time.Second)
 	if err != nil {
 		return nil, err
 	}
 
-	return parsePrometheusMetrics(body), nil
+	return parsePrometheusMetrics(body)
 }
 
 func findPod(ctx context.Context, clientset kubernetes.Interface, namespace, labelSelector string) (string, error) {
@@ -210,6 +261,9 @@ func setupPortForward(ctx context.Context, config *rest.Config, clientset kubern
 	case err = <-errChan:
 		close(stopChan)
 		return "", nil, fmt.Errorf("port forward failed: %w", err)
+	case <-ctx.Done():
+		close(stopChan)
+		return "", nil, fmt.Errorf("port forward canceled: %w", ctx.Err())
 	case <-time.After(30 * time.Second):
 		close(stopChan)
 		return "", nil, fmt.Errorf("timeout waiting for port forward to be ready")
@@ -239,11 +293,37 @@ func scrapeMetrics(ctx context.Context, address string) (string, error) {
 	return string(body), nil
 }
 
-func parsePrometheusMetrics(body string) MetricsMap {
+func waitForMetricsEndpoint(ctx context.Context, address string, timeout time.Duration) (string, error) {
+	ticker := time.NewTicker(250 * time.Millisecond)
+	defer ticker.Stop()
+
+	timer := time.NewTimer(timeout)
+	defer timer.Stop()
+
+	var lastErr error
+	for {
+		body, err := scrapeMetrics(ctx, address)
+		if err == nil {
+			return body, nil
+		}
+		lastErr = err
+
+		select {
+		case <-ctx.Done():
+			return "", fmt.Errorf("waiting for metrics endpoint canceled: %w", ctx.Err())
+		case <-timer.C:
+			return "", fmt.Errorf("timed out waiting for metrics endpoint: %w", lastErr)
+		case <-ticker.C:
+		}
+	}
+}
+
+func parsePrometheusMetrics(body string) (MetricsMap, error) {
 	metrics := make(MetricsMap)
 	metricRegex := regexp.MustCompile(`^([a-zA-Z_:][a-zA-Z0-9_:]*?)(?:\{([^}]*)\})?\s+([^\s]+)`)
 
 	scanner := bufio.NewScanner(strings.NewReader(body))
+	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
 	for scanner.Scan() {
 		line := scanner.Text()
 		if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" {
@@ -268,7 +348,11 @@ func parsePrometheusMetrics(body string) MetricsMap {
 		metrics[sample.Name] = append(metrics[sample.Name], sample)
 	}
 
-	return metrics
+	if err := scanner.Err(); err != nil {
+		return nil, fmt.Errorf("failed to scan metrics response: %w", err)
+	}
+
+	return metrics, nil
 }
 
 func parseLabels(labelsStr string) map[string]string {
@@ -277,11 +361,15 @@ func parseLabels(labelsStr string) map[string]string {
 		return labels
 	}
 
-	labelRegex := regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_]*)="([^"]*)"`)
+	labelRegex := regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_]*)="((?:[^"\\]|\\.)*)"`)
 	matches := labelRegex.FindAllStringSubmatch(labelsStr, -1)
 	for _, match := range matches {
 		if len(match) == 3 {
-			labels[match[1]] = match[2]
+			value, err := strconv.Unquote(`"` + match[2] + `"`)
+			if err != nil {
+				value = match[2]
+			}
+			labels[match[1]] = value
 		}
 	}
 	return labels

+ 106 - 0
e2e/framework/v2/metrics_test.go

@@ -0,0 +1,106 @@
+/*
+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 v2
+
+import (
+	"context"
+	"strings"
+	"testing"
+	"time"
+)
+
+func TestSumMetricValues(t *testing.T) {
+	metrics := MetricsMap{
+		"grpc_pool_connections_total": {
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-a"}, Value: 1},
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-a"}, Value: 2},
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-b"}, Value: 4},
+		},
+	}
+
+	got := SumMetricValues(metrics, "grpc_pool_connections_total", map[string]string{"address": "provider-a"})
+	if got != 3 {
+		t.Fatalf("expected sum 3, got %v", got)
+	}
+}
+
+func TestCountMetricSamples(t *testing.T) {
+	metrics := MetricsMap{
+		"grpc_pool_connections_total": {
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-a"}, Value: 1},
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-a"}, Value: 2},
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-b"}, Value: 4},
+		},
+	}
+
+	got := CountMetricSamples(metrics, "grpc_pool_connections_total", map[string]string{"address": "provider-a"})
+	if got != 2 {
+		t.Fatalf("expected count 2, got %d", got)
+	}
+}
+
+func TestGetMetricValueMatchesSubsetOfLabels(t *testing.T) {
+	metrics := MetricsMap{
+		"grpc_pool_connections_total": {
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-a", "state": "idle"}, Value: 2},
+			{Name: "grpc_pool_connections_total", Labels: map[string]string{"address": "provider-b", "state": "active"}, Value: 4},
+		},
+	}
+
+	got, found := GetMetricValue(metrics, "grpc_pool_connections_total", map[string]string{"address": "provider-a"})
+	if !found {
+		t.Fatalf("expected matching metric sample")
+	}
+	if got != 2 {
+		t.Fatalf("expected value 2, got %v", got)
+	}
+}
+
+func TestWaitForMetricHonorsContextCancellation(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	cancel()
+
+	err := WaitForMetric(ctx, func() (MetricsMap, error) {
+		return MetricsMap{}, nil
+	}, "grpc_pool_connections_total", map[string]string{"address": "provider-a"}, 1, 10*time.Second)
+	if err == nil {
+		t.Fatalf("expected cancellation error")
+	}
+	if !strings.Contains(err.Error(), "context canceled") {
+		t.Fatalf("expected context cancellation error, got %v", err)
+	}
+}
+
+func TestParsePrometheusMetricsParsesEscapedLabels(t *testing.T) {
+	body := "grpc_pool_connections_total{address=\"provider-\\\"a\\\"\",state=\"idle\\nstate\"} 2\n"
+
+	metrics, err := parsePrometheusMetrics(body)
+	if err != nil {
+		t.Fatalf("unexpected parse error: %v", err)
+	}
+
+	got, found := GetMetricValue(metrics, "grpc_pool_connections_total", map[string]string{
+		"address": "provider-\"a\"",
+		"state":   "idle\nstate",
+	})
+	if !found {
+		t.Fatalf("expected escaped metric labels to match parsed sample, got metrics %#v", metrics)
+	}
+	if got != 2 {
+		t.Fatalf("expected value 2, got %v", got)
+	}
+}

+ 147 - 0
e2e/framework/v2/operational.go

@@ -0,0 +1,147 @@
+/*
+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 v2
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/gomega"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/util/retry"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+type BackendTarget struct {
+	Namespace        string
+	DeploymentName   string
+	PodLabelSelector string
+}
+
+func ScaleDeploymentBySelector(f *framework.Framework, target BackendTarget, replicas int32) {
+	deploymentName := target.DeploymentName
+	if deploymentName == "" {
+		deploymentName = findDeploymentNameBySelector(f, target)
+	}
+	scaleDeployment(f, target.Namespace, deploymentName, replicas)
+}
+
+func ScaleDeploymentBySelectorAndWait(f *framework.Framework, target BackendTarget, replicas int32, timeout time.Duration) {
+	ScaleDeploymentBySelector(f, target, replicas)
+	WaitForBackendTargetRunningReplicas(f, target, int(replicas), timeout)
+}
+
+func DeleteOneProviderPodBySelector(f *framework.Framework, target BackendTarget) {
+	Expect(target.Namespace).NotTo(BeEmpty(), "backend target namespace must be set")
+	Expect(target.PodLabelSelector).NotTo(BeEmpty(), "backend target pod label selector must be set")
+
+	selector, err := labels.Parse(target.PodLabelSelector)
+	Expect(err).NotTo(HaveOccurred())
+
+	var podList corev1.PodList
+	Expect(f.CRClient.List(context.Background(), &podList, &client.ListOptions{
+		Namespace:     target.Namespace,
+		LabelSelector: selector,
+	})).To(Succeed())
+
+	foundRunningPod := false
+	for i := range podList.Items {
+		pod := &podList.Items[i]
+		if pod.Status.Phase == corev1.PodRunning {
+			foundRunningPod = true
+			Expect(f.CRClient.Delete(context.Background(), pod)).To(Succeed())
+			return
+		}
+	}
+
+	Expect(foundRunningPod).To(BeTrue(), fmt.Sprintf("no running pod found for selector %s in namespace %s", target.PodLabelSelector, target.Namespace))
+}
+
+func findDeploymentNameBySelector(f *framework.Framework, target BackendTarget) string {
+	Expect(target.Namespace).NotTo(BeEmpty(), "backend target namespace must be set")
+	Expect(target.PodLabelSelector).NotTo(BeEmpty(), "backend target pod label selector must be set")
+
+	selector, err := labels.Parse(target.PodLabelSelector)
+	Expect(err).NotTo(HaveOccurred())
+
+	var deploymentList appsv1.DeploymentList
+	Expect(f.CRClient.List(context.Background(), &deploymentList, &client.ListOptions{
+		Namespace: target.Namespace,
+	})).To(Succeed())
+
+	matches := make([]appsv1.Deployment, 0, len(deploymentList.Items))
+	for _, deployment := range deploymentList.Items {
+		if selector.Matches(labels.Set(deployment.Spec.Template.GetLabels())) {
+			matches = append(matches, deployment)
+		}
+	}
+
+	Expect(matches).NotTo(BeEmpty(), "no deployment found for selector %s", target.PodLabelSelector)
+	Expect(matches).To(HaveLen(1), "expected one deployment for selector %s", target.PodLabelSelector)
+
+	return matches[0].Name
+}
+
+func WaitForBackendTargetRunningReplicas(f *framework.Framework, target BackendTarget, expectedReplicas int, timeout time.Duration) {
+	Expect(target.Namespace).NotTo(BeEmpty(), "backend target namespace must be set")
+	Expect(target.PodLabelSelector).NotTo(BeEmpty(), "backend target pod label selector must be set")
+
+	selector, err := labels.Parse(target.PodLabelSelector)
+	Expect(err).NotTo(HaveOccurred())
+
+	Eventually(func(g Gomega) {
+		var podList corev1.PodList
+		g.Expect(f.CRClient.List(context.Background(), &podList, &client.ListOptions{
+			Namespace:     target.Namespace,
+			LabelSelector: selector,
+		})).To(Succeed())
+
+		running := 0
+		for i := range podList.Items {
+			if podList.Items[i].Status.Phase == corev1.PodRunning {
+				running++
+			}
+		}
+		g.Expect(running).To(Equal(expectedReplicas))
+	}, timeout, time.Second).Should(Succeed())
+}
+
+func scaleDeployment(f *framework.Framework, namespace, name string, replicas int32) {
+	Expect(namespace).NotTo(BeEmpty(), "deployment namespace must be set")
+	Expect(name).NotTo(BeEmpty(), "deployment name must be set")
+
+	Expect(retry.RetryOnConflict(retry.DefaultRetry, func() error {
+		var deployment appsv1.Deployment
+		if err := f.CRClient.Get(context.Background(), types.NamespacedName{Namespace: namespace, Name: name}, &deployment); err != nil {
+			return err
+		}
+		deployment.Spec.Replicas = &replicas
+		return f.CRClient.Update(context.Background(), &deployment)
+	})).To(Succeed())
+}
+
+func WaitForClusterProviderNotReady(f *framework.Framework, name string, timeout time.Duration) *esv1.ClusterProvider {
+	return WaitForClusterProviderCondition(f, name, metav1.ConditionFalse, timeout)
+}

+ 21 - 0
e2e/makefile_test.go

@@ -93,6 +93,9 @@ func TestV2MakeTargetCanSkipKubernetesProviderBuild(t *testing.T) {
 	if !strings.Contains(defaultDryRun, helmDependencyEnsureCmd) {
 		t.Fatalf("expected default test.v2 dry-run to ensure helm dependencies before copying the chart, output:\n%s", defaultDryRun)
 	}
+	if !strings.Contains(defaultDryRun, `TEST_SUITES="provider"`) {
+		t.Fatalf("expected default test.v2 dry-run to run the provider suite, output:\n%s", defaultDryRun)
+	}
 	if strings.Contains(defaultDryRun, dockerCleanupCmd) {
 		t.Fatalf("expected default test.v2 dry-run to avoid CI-only docker cleanup, output:\n%s", defaultDryRun)
 	}
@@ -122,6 +125,9 @@ func TestV2MakeTargetCanSkipKubernetesProviderBuild(t *testing.T) {
 	if !strings.Contains(skippedDryRun, helmDependencyEnsureCmd) {
 		t.Fatalf("expected skipped test.v2 dry-run to ensure helm dependencies before copying the chart, output:\n%s", skippedDryRun)
 	}
+	if !strings.Contains(skippedDryRun, `TEST_SUITES="provider"`) {
+		t.Fatalf("expected skipped test.v2 dry-run to still run the provider suite, output:\n%s", skippedDryRun)
+	}
 }
 
 func TestV2MakeTargetPrunesDockerImagesInCI(t *testing.T) {
@@ -133,6 +139,21 @@ func TestV2MakeTargetPrunesDockerImagesInCI(t *testing.T) {
 	}
 }
 
+func TestV2OperationalMakeTarget(t *testing.T) {
+	t.Parallel()
+
+	dryRun := runMakeDryRun(t, "test.v2.operational", testVersionArg)
+	if !strings.Contains(dryRun, `V2_GINKGO_LABELS='v2 && operational && fake'`) {
+		t.Fatalf("expected operational labels in target, got:\n%s", dryRun)
+	}
+	if !strings.Contains(dryRun, `V2_TEST_SUITES='provider'`) {
+		t.Fatalf("expected provider suite in target, got:\n%s", dryRun)
+	}
+	if !strings.Contains(dryRun, `TEST_SUITES="provider"`) {
+		t.Fatalf("expected operational target to render provider-only v2 suites, got:\n%s", dryRun)
+	}
+}
+
 func runMakeDryRun(t *testing.T, target string, extraArgs ...string) string {
 	t.Helper()
 	return runMakeDryRunWithEnv(t, nil, target, extraArgs...)

+ 6 - 3
e2e/suites/argocd/suite_test.go

@@ -45,9 +45,12 @@ var _ = SynchronizedAfterSuite(func() {
 	By("Deleting any pending generator states")
 	generatorStates := &genv1alpha1.GeneratorStateList{}
 	err := cl.List(GinkgoT().Context(), generatorStates)
-	Expect(err).ToNot(HaveOccurred())
-	for _, generatorState := range generatorStates.Items {
-		err = cl.Delete(GinkgoT().Context(), &generatorState)
+	if err == nil {
+		for _, generatorState := range generatorStates.Items {
+			err = cl.Delete(GinkgoT().Context(), &generatorState)
+			Expect(err).ToNot(HaveOccurred())
+		}
+	} else if !util.IsMissingAPIResourceError(err) {
 		Expect(err).ToNot(HaveOccurred())
 	}
 

+ 6 - 3
e2e/suites/flux/suite_test.go

@@ -46,9 +46,12 @@ var _ = SynchronizedAfterSuite(func() {
 	By("Deleting any pending generator states")
 	generatorStates := &genv1alpha1.GeneratorStateList{}
 	err := cfg.CRClient.List(GinkgoT().Context(), generatorStates)
-	Expect(err).ToNot(HaveOccurred())
-	for _, generatorState := range generatorStates.Items {
-		err = cfg.CRClient.Delete(GinkgoT().Context(), &generatorState)
+	if err == nil {
+		for _, generatorState := range generatorStates.Items {
+			err = cfg.CRClient.Delete(GinkgoT().Context(), &generatorState)
+			Expect(err).ToNot(HaveOccurred())
+		}
+	} else if !util.IsMissingAPIResourceError(err) {
 		Expect(err).ToNot(HaveOccurred())
 	}
 

+ 6 - 3
e2e/suites/generator/suite_test.go

@@ -49,9 +49,12 @@ var _ = SynchronizedAfterSuite(func() {
 	By("Deleting any pending generator states")
 	generatorStates := &genv1alpha1.GeneratorStateList{}
 	err := cfg.CRClient.List(GinkgoT().Context(), generatorStates)
-	Expect(err).ToNot(HaveOccurred())
-	for _, generatorState := range generatorStates.Items {
-		err = cfg.CRClient.Delete(GinkgoT().Context(), &generatorState)
+	if err == nil {
+		for _, generatorState := range generatorStates.Items {
+			err = cfg.CRClient.Delete(GinkgoT().Context(), &generatorState)
+			Expect(err).ToNot(HaveOccurred())
+		}
+	} else if !util.IsMissingAPIResourceError(err) {
 		Expect(err).ToNot(HaveOccurred())
 	}
 	By("Cleaning up global addons")

+ 393 - 0
e2e/suites/provider/cases/common/operational_v2.go

@@ -0,0 +1,393 @@
+/*
+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"
+	"strings"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	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"
+)
+
+const (
+	operationalPollInterval = 5 * time.Second
+	operationalTimeout      = 3 * time.Minute
+)
+
+type OperationalRuntime struct {
+	Provider               framework.SecretStoreProvider
+	ProviderRef            esv1.SecretStoreRef
+	DefaultRemoteNamespace string
+	WaitForRemoteSecret    func(namespace, name, key, expectedValue string)
+	ExpectNoRemoteSecret   func(namespace, name string)
+	MakeUnavailable        func()
+	RestoreAvailability    func()
+	RestartBackend         func()
+}
+
+func (r *OperationalRuntime) SupportsDisruptionLifecycle() bool {
+	return r != nil && r.MakeUnavailable != nil && r.RestoreAvailability != nil
+}
+
+func (r *OperationalRuntime) SupportsRestart() bool {
+	return r != nil && r.RestartBackend != nil
+}
+
+type OperationalExternalSecretHarness struct {
+	PrepareNamespaced func(tc *framework.TestCase) *OperationalRuntime
+	PrepareCluster    func(tc *framework.TestCase, cfg ClusterProviderConfig) *OperationalRuntime
+}
+
+type OperationalPushSecretHarness struct {
+	PrepareNamespaced func(tc *framework.TestCase) *OperationalRuntime
+	PrepareCluster    func(tc *framework.TestCase, cfg ClusterProviderConfig) *OperationalRuntime
+}
+
+func NamespacedProviderUnavailable(f *framework.Framework, harness OperationalExternalSecretHarness, remoteKey, expectedValue string) (string, func(*framework.TestCase)) {
+	return "[common] should surface namespaced Provider unavailability and recover after backend restoration", func(tc *framework.TestCase) {
+		tc.ExternalSecret.ObjectMeta.Name = "operational-unavailable-es"
+		tc.ExternalSecret.Spec.Target.Name = "operational-unavailable-target"
+		tc.ExternalSecret.Spec.Data = []esv1.ExternalSecretData{{
+			SecretKey: "value",
+			RemoteRef: esv1.ExternalSecretDataRemoteRef{
+				Key:      remoteKey,
+				Property: "value",
+			},
+		}}
+		tc.Secrets = map[string]framework.SecretEntry{
+			remoteKey: {Value: jsonSecretValue(expectedValue)},
+		}
+		tc.ExpectedSecret = opaqueValueSecret(expectedValue)
+
+		var runtime *OperationalRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.PrepareNamespaced(tc)
+			applyOperationalExternalSecret(tc, runtime)
+		}
+		tc.AfterSync = func(_ framework.SecretStoreProvider, _ *corev1.Secret) {
+			skipIfOperationalRuntimeMissingDisruptionLifecycle(runtime)
+			DeferCleanup(func() {
+				runtime.RestoreAvailability()
+				waitForProviderRefCondition(tc.Framework, tc.ExternalSecret.Namespace, runtime.ProviderRef, metav1.ConditionTrue)
+			})
+			runtime.MakeUnavailable()
+			waitForExternalSecretStatus(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Name, corev1.ConditionFalse)
+
+			runtime.RestoreAvailability()
+			waitForProviderRefCondition(tc.Framework, tc.ExternalSecret.Namespace, runtime.ProviderRef, metav1.ConditionTrue)
+			waitForExternalSecretStatus(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Name, corev1.ConditionTrue)
+			waitForSecretData(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Spec.Target.Name, map[string][]byte{
+				"value": []byte(expectedValue),
+			}, operationalTimeout)
+		}
+	}
+}
+
+func NamespacedProviderRestart(f *framework.Framework, harness OperationalExternalSecretHarness, remoteKey, expectedValue string) (string, func(*framework.TestCase)) {
+	return "[common] should recover namespaced Provider reads after backend restart", func(tc *framework.TestCase) {
+		tc.ExternalSecret.ObjectMeta.Name = "operational-restart-es"
+		tc.ExternalSecret.Spec.Target.Name = "operational-restart-target"
+		tc.ExternalSecret.Spec.Data = []esv1.ExternalSecretData{{
+			SecretKey: "value",
+			RemoteRef: esv1.ExternalSecretDataRemoteRef{
+				Key:      remoteKey,
+				Property: "value",
+			},
+		}}
+		tc.Secrets = map[string]framework.SecretEntry{
+			remoteKey: {Value: jsonSecretValue("before-restart")},
+		}
+		tc.ExpectedSecret = opaqueValueSecret("before-restart")
+
+		var runtime *OperationalRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.PrepareNamespaced(tc)
+			applyOperationalExternalSecret(tc, runtime)
+		}
+		tc.AfterSync = func(prov framework.SecretStoreProvider, _ *corev1.Secret) {
+			skipIfOperationalRuntimeMissingRestart(runtime)
+			runtime.RestartBackend()
+			waitForProviderRefCondition(tc.Framework, tc.ExternalSecret.Namespace, runtime.ProviderRef, metav1.ConditionTrue)
+
+			prov.DeleteSecret(remoteKey)
+			prov.CreateSecret(remoteKey, framework.SecretEntry{Value: jsonSecretValue(expectedValue)})
+
+			waitForExternalSecretStatus(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Name, corev1.ConditionTrue)
+			waitForSecretData(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Spec.Target.Name, map[string][]byte{
+				"value": []byte(expectedValue),
+			}, operationalTimeout)
+		}
+	}
+}
+
+func ClusterProviderUnavailable(f *framework.Framework, harness OperationalExternalSecretHarness, remoteKey, expectedValue string, authScope esv1.AuthenticationScope) (string, func(*framework.TestCase)) {
+	return fmt.Sprintf("[common] should surface ClusterProvider unavailability and recover with %s auth", authScope), func(tc *framework.TestCase) {
+		scopeSuffix := operationalScopeSuffix(authScope)
+		tc.ExternalSecret.ObjectMeta.Name = fmt.Sprintf("operational-cluster-unavailable-%s", scopeSuffix)
+		tc.ExternalSecret.Spec.Target.Name = fmt.Sprintf("operational-cluster-unavailable-%s-target", scopeSuffix)
+		tc.ExternalSecret.Spec.Data = []esv1.ExternalSecretData{{
+			SecretKey: "value",
+			RemoteRef: esv1.ExternalSecretDataRemoteRef{
+				Key:      remoteKey,
+				Property: "value",
+			},
+		}}
+		tc.Secrets = map[string]framework.SecretEntry{
+			remoteKey: {Value: jsonSecretValue(expectedValue)},
+		}
+		tc.ExpectedSecret = opaqueValueSecret(expectedValue)
+
+		var runtime *OperationalRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.PrepareCluster(tc, ClusterProviderConfig{
+				Name:      "operational-unavailable",
+				AuthScope: authScope,
+			})
+			applyOperationalExternalSecret(tc, runtime)
+		}
+		tc.AfterSync = func(_ framework.SecretStoreProvider, _ *corev1.Secret) {
+			skipIfOperationalRuntimeMissingDisruptionLifecycle(runtime)
+			DeferCleanup(func() {
+				runtime.RestoreAvailability()
+				waitForProviderRefCondition(tc.Framework, tc.ExternalSecret.Namespace, runtime.ProviderRef, metav1.ConditionTrue)
+			})
+			runtime.MakeUnavailable()
+			waitForExternalSecretStatus(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Name, corev1.ConditionFalse)
+
+			runtime.RestoreAvailability()
+			waitForProviderRefCondition(tc.Framework, tc.ExternalSecret.Namespace, runtime.ProviderRef, metav1.ConditionTrue)
+			waitForExternalSecretStatus(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Name, corev1.ConditionTrue)
+			waitForSecretData(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Spec.Target.Name, map[string][]byte{
+				"value": []byte(expectedValue),
+			}, operationalTimeout)
+		}
+	}
+}
+
+func ClusterProviderRestart(f *framework.Framework, harness OperationalExternalSecretHarness, remoteKey, expectedValue string, authScope esv1.AuthenticationScope) (string, func(*framework.TestCase)) {
+	return fmt.Sprintf("[common] should recover ClusterProvider reads after backend restart with %s auth", authScope), func(tc *framework.TestCase) {
+		scopeSuffix := operationalScopeSuffix(authScope)
+		tc.ExternalSecret.ObjectMeta.Name = fmt.Sprintf("operational-cluster-restart-%s", scopeSuffix)
+		tc.ExternalSecret.Spec.Target.Name = fmt.Sprintf("operational-cluster-restart-%s-target", scopeSuffix)
+		tc.ExternalSecret.Spec.Data = []esv1.ExternalSecretData{{
+			SecretKey: "value",
+			RemoteRef: esv1.ExternalSecretDataRemoteRef{
+				Key:      remoteKey,
+				Property: "value",
+			},
+		}}
+		tc.Secrets = map[string]framework.SecretEntry{
+			remoteKey: {Value: jsonSecretValue("before-restart")},
+		}
+		tc.ExpectedSecret = opaqueValueSecret("before-restart")
+
+		var runtime *OperationalRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.PrepareCluster(tc, ClusterProviderConfig{
+				Name:      "operational-restart",
+				AuthScope: authScope,
+			})
+			applyOperationalExternalSecret(tc, runtime)
+		}
+		tc.AfterSync = func(prov framework.SecretStoreProvider, _ *corev1.Secret) {
+			skipIfOperationalRuntimeMissingRestart(runtime)
+			runtime.RestartBackend()
+			waitForProviderRefCondition(tc.Framework, tc.ExternalSecret.Namespace, runtime.ProviderRef, metav1.ConditionTrue)
+
+			prov.DeleteSecret(remoteKey)
+			prov.CreateSecret(remoteKey, framework.SecretEntry{Value: jsonSecretValue(expectedValue)})
+
+			waitForExternalSecretStatus(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Name, corev1.ConditionTrue)
+			waitForSecretData(tc.Framework, tc.ExternalSecret.Namespace, tc.ExternalSecret.Spec.Target.Name, map[string][]byte{
+				"value": []byte(expectedValue),
+			}, operationalTimeout)
+		}
+	}
+}
+
+func NamespacedPushSecretUnavailable(f *framework.Framework, harness OperationalPushSecretHarness) (string, func(*framework.TestCase)) {
+	return "[common] should surface namespaced Provider push unavailability and recover after backend restoration", func(tc *framework.TestCase) {
+		tc.PushSecretSource = operationalPushSourceSecret(f, "operational-push-namespaced-source", "before-outage")
+
+		var runtime *OperationalRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.PrepareNamespaced(tc)
+			remoteSecretName := f.MakeRemoteRefKey("operational-push-namespaced-remote")
+			applyOperationalPushSecret(tc, runtime, remoteSecretName)
+			tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+				runtime.WaitForRemoteSecret(runtime.DefaultRemoteNamespace, remoteSecretName, "value", "before-outage")
+
+				skipIfOperationalRuntimeMissingDisruptionLifecycle(runtime)
+				DeferCleanup(func() {
+					runtime.RestoreAvailability()
+					waitForProviderRefCondition(tc.Framework, ps.Namespace, runtime.ProviderRef, metav1.ConditionTrue)
+				})
+				runtime.MakeUnavailable()
+				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
+
+				runtime.RestoreAvailability()
+				waitForProviderRefCondition(tc.Framework, ps.Namespace, runtime.ProviderRef, metav1.ConditionTrue)
+				updatePushSecretSource(tc.Framework, tc.PushSecretSource.Namespace, tc.PushSecretSource.Name, "after-outage")
+				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+				runtime.WaitForRemoteSecret(runtime.DefaultRemoteNamespace, remoteSecretName, "value", "after-outage")
+			}
+		}
+	}
+}
+
+func ClusterProviderPushUnavailable(f *framework.Framework, harness OperationalPushSecretHarness, authScope esv1.AuthenticationScope) (string, func(*framework.TestCase)) {
+	return fmt.Sprintf("[common] should surface ClusterProvider push unavailability and recover with %s auth", authScope), func(tc *framework.TestCase) {
+		scopeSuffix := operationalScopeSuffix(authScope)
+		tc.PushSecretSource = operationalPushSourceSecret(f, fmt.Sprintf("operational-push-cluster-source-%s", scopeSuffix), "before-outage")
+
+		var runtime *OperationalRuntime
+		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			runtime = harness.PrepareCluster(tc, ClusterProviderConfig{
+				Name:      "operational-push-unavailable",
+				AuthScope: authScope,
+			})
+			remoteSecretName := f.MakeRemoteRefKey(fmt.Sprintf("operational-push-cluster-remote-%s", scopeSuffix))
+			applyOperationalPushSecret(tc, runtime, remoteSecretName)
+			tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+				runtime.WaitForRemoteSecret(runtime.DefaultRemoteNamespace, remoteSecretName, "value", "before-outage")
+
+				skipIfOperationalRuntimeMissingDisruptionLifecycle(runtime)
+				DeferCleanup(func() {
+					runtime.RestoreAvailability()
+					waitForProviderRefCondition(tc.Framework, ps.Namespace, runtime.ProviderRef, metav1.ConditionTrue)
+				})
+				runtime.MakeUnavailable()
+				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
+
+				runtime.RestoreAvailability()
+				waitForProviderRefCondition(tc.Framework, ps.Namespace, runtime.ProviderRef, metav1.ConditionTrue)
+				updatePushSecretSource(tc.Framework, tc.PushSecretSource.Namespace, tc.PushSecretSource.Name, "after-outage")
+				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+				runtime.WaitForRemoteSecret(runtime.DefaultRemoteNamespace, remoteSecretName, "value", "after-outage")
+			}
+		}
+	}
+}
+
+func applyOperationalExternalSecret(tc *framework.TestCase, runtime *OperationalRuntime) {
+	Expect(runtime).NotTo(BeNil(), "operational harness returned nil runtime")
+	tc.ExternalSecret.Spec.SecretStoreRef = runtime.ProviderRef
+	if runtime.Provider != nil {
+		tc.ProviderOverride = runtime.Provider
+	}
+}
+
+func applyOperationalPushSecret(tc *framework.TestCase, runtime *OperationalRuntime, remoteSecretName string) {
+	Expect(runtime).NotTo(BeNil(), "operational harness returned nil runtime")
+	Expect(runtime.ProviderRef.Name).NotTo(BeEmpty(), "operational runtime provider ref name must be set")
+	Expect(runtime.WaitForRemoteSecret).NotTo(BeNil(), "operational runtime WaitForRemoteSecret hook must be set")
+
+	tc.PushSecret.ObjectMeta.Name = fmt.Sprintf("%s-push-secret", tc.PushSecretSource.Name)
+	tc.PushSecret.Spec.SecretStoreRefs = []esv1alpha1.PushSecretStoreRef{{
+		Name:       runtime.ProviderRef.Name,
+		Kind:       runtime.ProviderRef.Kind,
+		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 waitForProviderRefCondition(f *framework.Framework, namespace string, ref esv1.SecretStoreRef, status metav1.ConditionStatus) {
+	switch ref.Kind {
+	case esv1.ClusterProviderKindStr:
+		frameworkv2.WaitForClusterProviderCondition(f, ref.Name, status, operationalTimeout)
+	default:
+		frameworkv2.WaitForProviderConnectionCondition(f, namespace, ref.Name, status, operationalTimeout)
+	}
+}
+
+func skipIfOperationalRuntimeMissingDisruptionLifecycle(runtime *OperationalRuntime) {
+	Expect(runtime).NotTo(BeNil(), "operational harness returned nil runtime")
+	if !runtime.SupportsDisruptionLifecycle() {
+		Skip(fmt.Sprintf("provider ref %q does not support disruption lifecycle hooks", runtime.ProviderRef.Name))
+	}
+}
+
+func skipIfOperationalRuntimeMissingRestart(runtime *OperationalRuntime) {
+	Expect(runtime).NotTo(BeNil(), "operational harness returned nil runtime")
+	if !runtime.SupportsRestart() {
+		Skip(fmt.Sprintf("provider ref %q does not support restart hooks", runtime.ProviderRef.Name))
+	}
+}
+
+func operationalPushSourceSecret(f *framework.Framework, name, value string) *corev1.Secret {
+	return &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: f.Namespace.Name,
+		},
+		Data: map[string][]byte{
+			"value": []byte(value),
+		},
+	}
+}
+
+func updatePushSecretSource(f *framework.Framework, namespace, name, value string) {
+	Eventually(func(g Gomega) {
+		var secret corev1.Secret
+		g.Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: name, Namespace: namespace}, &secret)).To(Succeed())
+		secret.Data["value"] = []byte(value)
+		g.Expect(f.CRClient.Update(context.Background(), &secret)).To(Succeed())
+	}, operationalTimeout, operationalPollInterval).Should(Succeed())
+}
+
+func opaqueValueSecret(value string) *corev1.Secret {
+	return &corev1.Secret{
+		Type: corev1.SecretTypeOpaque,
+		Data: map[string][]byte{
+			"value": []byte(value),
+		},
+	}
+}
+
+func operationalScopeSuffix(authScope esv1.AuthenticationScope) string {
+	replacer := strings.NewReplacer(
+		"ManifestNamespace", "manifest-namespace",
+		"ProviderNamespace", "provider-namespace",
+	)
+	return replacer.Replace(string(authScope))
+}

+ 60 - 0
e2e/suites/provider/cases/common/operational_v2_test.go

@@ -0,0 +1,60 @@
+/*
+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 "testing"
+
+func TestOperationalRuntimeSupportsDisruptionLifecycle(t *testing.T) {
+	runtimeWithoutHooks := &OperationalRuntime{}
+	if runtimeWithoutHooks.SupportsDisruptionLifecycle() {
+		t.Fatalf("expected false when all hooks are nil")
+	}
+
+	runtimeWithBreakOnly := &OperationalRuntime{
+		MakeUnavailable: func() {},
+	}
+	if runtimeWithBreakOnly.SupportsDisruptionLifecycle() {
+		t.Fatalf("expected false when RestoreAvailability is nil")
+	}
+
+	runtimeWithRestoreOnly := &OperationalRuntime{
+		RestoreAvailability: func() {},
+	}
+	if runtimeWithRestoreOnly.SupportsDisruptionLifecycle() {
+		t.Fatalf("expected false when MakeUnavailable is nil")
+	}
+
+	runtimeWithBoth := &OperationalRuntime{
+		MakeUnavailable:     func() {},
+		RestoreAvailability: func() {},
+	}
+	if !runtimeWithBoth.SupportsDisruptionLifecycle() {
+		t.Fatalf("expected true when both hooks exist")
+	}
+}
+
+func TestOperationalRuntimeSupportsRestart(t *testing.T) {
+	runtime := &OperationalRuntime{}
+	if runtime.SupportsRestart() {
+		t.Fatalf("expected false when RestartBackend is nil")
+	}
+
+	runtime.RestartBackend = func() {}
+	if !runtime.SupportsRestart() {
+		t.Fatalf("expected true when RestartBackend is present")
+	}
+}

+ 272 - 0
e2e/suites/provider/cases/fake/operational_v2.go

@@ -0,0 +1,272 @@
+/*
+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"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	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"
+	"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"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+)
+
+var _ = Describe("[fake] v2 operational", Serial, Label("fake", "v2", "operational"), func() {
+	f := framework.New("eso-fake-v2-operational")
+	prov := NewProviderV2(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("external secret operational behavior",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(common.NamespacedProviderUnavailable(f, newFakeOperationalExternalSecretHarness(f, prov), "fake-operational-unavailable", "recovered")),
+		Entry(common.NamespacedProviderRestart(f, newFakeOperationalExternalSecretHarness(f, prov), "fake-operational-restart", "restarted")),
+		Entry(common.ClusterProviderUnavailable(f, newFakeOperationalExternalSecretHarness(f, prov), "fake-operational-cluster-unavailable", "cluster-recovered", esv1.AuthenticationScopeManifestNamespace)),
+		Entry(common.ClusterProviderRestart(f, newFakeOperationalExternalSecretHarness(f, prov), "fake-operational-cluster-restart", "cluster-restarted", esv1.AuthenticationScopeManifestNamespace)),
+	)
+
+	DescribeTable("push secret operational behavior",
+		framework.TableFuncWithPushSecret(f, prov, nil),
+		Entry(common.NamespacedPushSecretUnavailable(f, newFakeOperationalPushHarness(f, prov))),
+		Entry(common.ClusterProviderPushUnavailable(f, newFakeOperationalPushHarness(f, prov), esv1.AuthenticationScopeManifestNamespace)),
+	)
+
+	It("reuses one backend connection across many namespaced fake Provider consumers", func() {
+		const consumerCount = 10
+
+		for i := 0; i < consumerCount; i++ {
+			remoteKey := fmt.Sprintf("fake-operational-consumer-%d", i)
+			targetName := fmt.Sprintf("fake-operational-consumer-target-%d", i)
+			expectedValue := fmt.Sprintf("value-%d", i)
+
+			prov.CreateSecret(remoteKey, framework.SecretEntry{
+				Value: fmt.Sprintf(`{"value":"%s"}`, expectedValue),
+			})
+
+			Expect(f.CreateObjectWithRetry(&esv1.ExternalSecret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      fmt.Sprintf("fake-operational-consumer-es-%d", i),
+					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: targetName,
+					},
+					Data: []esv1.ExternalSecretData{{
+						SecretKey: "value",
+						RemoteRef: esv1.ExternalSecretDataRemoteRef{
+							Key:      remoteKey,
+							Property: "value",
+						},
+					}},
+				},
+			})).To(Succeed())
+
+			_, err := f.WaitForSecretValue(f.Namespace.Name, targetName, &corev1.Secret{
+				Type: corev1.SecretTypeOpaque,
+				Data: map[string][]byte{
+					"value": []byte(expectedValue),
+				},
+			})
+			Expect(err).NotTo(HaveOccurred())
+		}
+
+		Eventually(func(g Gomega) {
+			metrics, err := frameworkv2.ScrapeControllerMetrics(context.Background(), f.KubeConfig, f.KubeClientSet, frameworkv2.ProviderNamespace)
+			g.Expect(err).NotTo(HaveOccurred())
+
+			total := frameworkv2.SumMetricValues(metrics, "grpc_pool_connections_total", map[string]string{
+				"address": frameworkv2.ProviderAddress("fake"),
+			})
+			g.Expect(total).To(BeNumerically(">=", 1))
+			g.Expect(total).To(BeNumerically("<=", 2), "expected bounded connection reuse for one backend")
+			g.Expect(total).To(BeNumerically("<", consumerCount), "expected fewer pooled connections than consumers")
+		}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
+	})
+
+	It("reuses backend connections across multiple fake Provider resources that share one backend", func() {
+		const providerCount = 4
+
+		for i := 0; i < providerCount; i++ {
+			providerName := fmt.Sprintf("fake-fanout-provider-%d", i)
+			remoteKey := fmt.Sprintf("fake-fanout-remote-%d", i)
+			expectedValue := fmt.Sprintf("fanout-%d", i)
+			targetName := fmt.Sprintf("fake-fanout-target-%d", i)
+
+			frameworkv2.CreateProviderConnection(
+				f,
+				f.Namespace.Name,
+				providerName,
+				frameworkv2.ProviderAddress("fake"),
+				fakeProviderAPIVersion,
+				fakeProviderKind,
+				f.Namespace.Name,
+				"",
+			)
+			frameworkv2.WaitForProviderConnectionReady(f, f.Namespace.Name, providerName, defaultV2WaitTimeout)
+
+			prov.CreateSecret(remoteKey, framework.SecretEntry{
+				Value: fmt.Sprintf(`{"value":"%s"}`, expectedValue),
+			})
+
+			Expect(f.CreateObjectWithRetry(&esv1.ExternalSecret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      fmt.Sprintf("fake-fanout-es-%d", i),
+					Namespace: f.Namespace.Name,
+				},
+				Spec: esv1.ExternalSecretSpec{
+					RefreshInterval: &metav1.Duration{Duration: defaultV2RefreshInterval},
+					SecretStoreRef: esv1.SecretStoreRef{
+						Name: providerName,
+						Kind: esv1.ProviderKindStr,
+					},
+					Target: esv1.ExternalSecretTarget{
+						Name: targetName,
+					},
+					Data: []esv1.ExternalSecretData{{
+						SecretKey: "value",
+						RemoteRef: esv1.ExternalSecretDataRemoteRef{
+							Key:      remoteKey,
+							Property: "value",
+						},
+					}},
+				},
+			})).To(Succeed())
+
+			_, err := f.WaitForSecretValue(f.Namespace.Name, targetName, &corev1.Secret{
+				Type: corev1.SecretTypeOpaque,
+				Data: map[string][]byte{
+					"value": []byte(expectedValue),
+				},
+			})
+			Expect(err).NotTo(HaveOccurred())
+		}
+
+		Eventually(func(g Gomega) {
+			metrics, err := frameworkv2.ScrapeControllerMetrics(context.Background(), f.KubeConfig, f.KubeClientSet, frameworkv2.ProviderNamespace)
+			g.Expect(err).NotTo(HaveOccurred())
+
+			total := frameworkv2.SumMetricValues(metrics, "grpc_pool_connections_total", map[string]string{
+				"address": frameworkv2.ProviderAddress("fake"),
+			})
+			g.Expect(total).To(BeNumerically(">=", 1))
+			g.Expect(total).To(BeNumerically("<=", 2), "expected bounded connection reuse across shared backend fanout")
+			g.Expect(total).To(BeNumerically("<", providerCount), "expected fewer pooled connections than Provider resources")
+		}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
+	})
+
+	It("recovers generator-backed push traffic after fake provider outage", func() {
+		const (
+			generatorName   = "fake-operational-generator"
+			pushSecretName  = "fake-operational-generator-push"
+			remoteSecretKey = "fake-operational-generator-remote"
+		)
+
+		generator := &genv1alpha1.Fake{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: genv1alpha1.Group + "/" + genv1alpha1.Version,
+				Kind:       genv1alpha1.FakeKind,
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      generatorName,
+				Namespace: f.Namespace.Name,
+			},
+			Spec: genv1alpha1.FakeSpec{
+				Data: map[string]string{
+					"value": "before-outage",
+				},
+			},
+		}
+		Expect(f.CreateObjectWithRetry(generator)).To(Succeed())
+
+		pushSecret := &esv1alpha1.PushSecret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      pushSecretName,
+				Namespace: f.Namespace.Name,
+			},
+			Spec: esv1alpha1.PushSecretSpec{
+				RefreshInterval: &metav1.Duration{Duration: defaultV2RefreshInterval},
+				SecretStoreRefs: []esv1alpha1.PushSecretStoreRef{{
+					Name:       f.Namespace.Name,
+					Kind:       esv1.ProviderKindStr,
+					APIVersion: esv1.SchemeGroupVersion.String(),
+				}},
+				Selector: esv1alpha1.PushSecretSelector{
+					GeneratorRef: &esv1.GeneratorRef{
+						APIVersion: genv1alpha1.Group + "/" + genv1alpha1.Version,
+						Kind:       genv1alpha1.FakeKind,
+						Name:       generatorName,
+					},
+				},
+				Data: []esv1alpha1.PushSecretData{{
+					Match: esv1alpha1.PushSecretMatch{
+						SecretKey: "value",
+						RemoteRef: esv1alpha1.PushSecretRemoteRef{
+							RemoteKey: remoteSecretKey,
+							Property:  "value",
+						},
+					},
+				}},
+			},
+		}
+		Expect(f.CreateObjectWithRetry(pushSecret)).To(Succeed())
+
+		commonWaitForPushSecretReady(f, f.Namespace.Name, pushSecretName, corev1.ConditionTrue)
+		waitForPushedValueViaExternalSecret(f, esv1.SecretStoreRef{
+			Name: f.Namespace.Name,
+			Kind: esv1.ProviderKindStr,
+		}, remoteSecretKey, "before-outage")
+
+		DeferCleanup(func() {
+			frameworkv2.ScaleDeploymentBySelectorAndWait(f, fakeBackendTarget(), 1, defaultV2WaitTimeout)
+			frameworkv2.WaitForProviderConnectionReady(f, f.Namespace.Name, f.Namespace.Name, defaultV2WaitTimeout)
+		})
+		frameworkv2.ScaleDeploymentBySelectorAndWait(f, fakeBackendTarget(), 0, defaultV2WaitTimeout)
+		commonWaitForPushSecretReady(f, f.Namespace.Name, pushSecretName, corev1.ConditionFalse)
+
+		var updated genv1alpha1.Fake
+		Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: generatorName, Namespace: f.Namespace.Name}, &updated)).To(Succeed())
+		updated.Spec.Data["value"] = "after-outage"
+		Expect(f.CRClient.Update(context.Background(), &updated)).To(Succeed())
+
+		frameworkv2.ScaleDeploymentBySelectorAndWait(f, fakeBackendTarget(), 1, defaultV2WaitTimeout)
+		frameworkv2.WaitForProviderConnectionReady(f, f.Namespace.Name, f.Namespace.Name, defaultV2WaitTimeout)
+		commonWaitForPushSecretReady(f, f.Namespace.Name, pushSecretName, corev1.ConditionTrue)
+		waitForPushedValueViaExternalSecret(f, esv1.SecretStoreRef{
+			Name: f.Namespace.Name,
+			Kind: esv1.ProviderKindStr,
+		}, remoteSecretKey, "after-outage")
+	})
+})

+ 129 - 2
e2e/suites/provider/cases/fake/provider_v2.go

@@ -19,6 +19,7 @@ package fake
 import (
 	"context"
 	"fmt"
+	"reflect"
 	"time"
 
 	. "github.com/onsi/ginkgo/v2"
@@ -40,7 +41,7 @@ import (
 const (
 	fakeProviderAPIVersion   = "provider.external-secrets.io/v2alpha1"
 	fakeProviderKind         = "Fake"
-	defaultV2WaitTimeout     = 60 * time.Second
+	defaultV2WaitTimeout     = 3 * time.Minute
 	defaultV2PollInterval    = 2 * time.Second
 	defaultV2RefreshInterval = 10 * time.Second
 )
@@ -123,6 +124,7 @@ func (s *ProviderV2) BeforeEach() {
 		return
 	}
 
+	frameworkv2.ScaleDeploymentBySelectorAndWait(s.framework, fakeBackendTarget(), 1, defaultV2WaitTimeout)
 	s.createStore()
 	frameworkv2.WaitForProviderConnectionReady(s.framework, s.framework.Namespace.Name, s.framework.Namespace.Name, defaultV2WaitTimeout)
 }
@@ -160,6 +162,39 @@ func (s *ProviderV2) updateStore(mutate func(*fakev2alpha1.Fake)) {
 	updateFakeProviderConfig(s.framework, s.framework.Namespace.Name, s.framework.Namespace.Name, mutate)
 }
 
+func fakeBackendTarget() frameworkv2.BackendTarget {
+	return frameworkv2.BackendTarget{
+		Namespace:        frameworkv2.ProviderNamespace,
+		PodLabelSelector: "app.kubernetes.io/name=external-secrets-provider-fake",
+	}
+}
+
+func (s *ProviderV2) prepareNamespacedOperationalRuntime() *common.OperationalRuntime {
+	return &common.OperationalRuntime{
+		Provider: s,
+		ProviderRef: esv1.SecretStoreRef{
+			Name: s.framework.Namespace.Name,
+			Kind: esv1.ProviderKindStr,
+		},
+		DefaultRemoteNamespace: s.framework.Namespace.Name,
+		WaitForRemoteSecret: func(_, name, _ string, expectedValue string) {
+			waitForPushedValueViaExternalSecret(s.framework, esv1.SecretStoreRef{
+				Name: s.framework.Namespace.Name,
+				Kind: esv1.ProviderKindStr,
+			}, name, expectedValue)
+		},
+		MakeUnavailable: func() {
+			frameworkv2.ScaleDeploymentBySelector(s.framework, fakeBackendTarget(), 0)
+		},
+		RestoreAvailability: func() {
+			frameworkv2.ScaleDeploymentBySelector(s.framework, fakeBackendTarget(), 1)
+		},
+		RestartBackend: func() {
+			frameworkv2.DeleteOneProviderPodBySelector(s.framework, fakeBackendTarget())
+		},
+	}
+}
+
 type fakeClusterProviderScenario struct {
 	f                    *framework.Framework
 	namePrefix           string
@@ -254,6 +289,80 @@ func newFakeClusterProviderPushHarness(f *framework.Framework) common.ClusterPro
 	}
 }
 
+func newFakeOperationalExternalSecretHarness(f *framework.Framework, prov *ProviderV2) common.OperationalExternalSecretHarness {
+	return common.OperationalExternalSecretHarness{
+		PrepareNamespaced: func(_ *framework.TestCase) *common.OperationalRuntime {
+			return prov.prepareNamespacedOperationalRuntime()
+		},
+		PrepareCluster: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.OperationalRuntime {
+			s := newFakeClusterProviderScenario(f, cfg.Name, cfg.AuthScope)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.OperationalRuntime{
+				Provider: s,
+				ProviderRef: esv1.SecretStoreRef{
+					Name: clusterProviderName,
+					Kind: esv1.ClusterProviderKindStr,
+				},
+				DefaultRemoteNamespace: s.fakeConfigNamespace,
+				WaitForRemoteSecret: func(_, name, _ string, expectedValue string) {
+					waitForPushedValueViaExternalSecret(f, esv1.SecretStoreRef{
+						Name: clusterProviderName,
+						Kind: esv1.ClusterProviderKindStr,
+					}, name, expectedValue)
+				},
+				MakeUnavailable: func() {
+					frameworkv2.ScaleDeploymentBySelectorAndWait(f, fakeBackendTarget(), 0, defaultV2WaitTimeout)
+				},
+				RestoreAvailability: func() {
+					frameworkv2.ScaleDeploymentBySelectorAndWait(f, fakeBackendTarget(), 1, defaultV2WaitTimeout)
+				},
+				RestartBackend: func() {
+					frameworkv2.DeleteOneProviderPodBySelector(f, fakeBackendTarget())
+				},
+			}
+		},
+	}
+}
+
+func newFakeOperationalPushHarness(f *framework.Framework, prov *ProviderV2) common.OperationalPushSecretHarness {
+	return common.OperationalPushSecretHarness{
+		PrepareNamespaced: func(_ *framework.TestCase) *common.OperationalRuntime {
+			return prov.prepareNamespacedOperationalRuntime()
+		},
+		PrepareCluster: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.OperationalRuntime {
+			s := newFakeClusterProviderScenario(f, cfg.Name, cfg.AuthScope)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.OperationalRuntime{
+				Provider: s,
+				ProviderRef: esv1.SecretStoreRef{
+					Name: clusterProviderName,
+					Kind: esv1.ClusterProviderKindStr,
+				},
+				DefaultRemoteNamespace: s.fakeConfigNamespace,
+				WaitForRemoteSecret: func(_, name, _ string, expectedValue string) {
+					waitForPushedValueViaExternalSecret(f, esv1.SecretStoreRef{
+						Name: clusterProviderName,
+						Kind: esv1.ClusterProviderKindStr,
+					}, name, expectedValue)
+				},
+				MakeUnavailable: func() {
+					frameworkv2.ScaleDeploymentBySelectorAndWait(f, fakeBackendTarget(), 0, defaultV2WaitTimeout)
+				},
+				RestoreAvailability: func() {
+					frameworkv2.ScaleDeploymentBySelectorAndWait(f, fakeBackendTarget(), 1, defaultV2WaitTimeout)
+				},
+				RestartBackend: func() {
+					frameworkv2.DeleteOneProviderPodBySelector(f, fakeBackendTarget())
+				},
+			}
+		},
+	}
+}
+
 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{
@@ -331,7 +440,7 @@ func waitForPushedValueViaExternalSecret(f *framework.Framework, storeRef esv1.S
 			}},
 		},
 	}
-	Expect(f.CRClient.Create(context.Background(), externalSecret)).To(Succeed())
+	Expect(createOrUpdateReadbackExternalSecret(context.Background(), f, externalSecret)).To(Succeed())
 
 	DeferCleanup(func() {
 		err := f.CRClient.Delete(context.Background(), externalSecret)
@@ -349,6 +458,24 @@ func waitForPushedValueViaExternalSecret(f *framework.Framework, storeRef esv1.S
 	Expect(err).NotTo(HaveOccurred())
 }
 
+func createOrUpdateReadbackExternalSecret(ctx context.Context, f *framework.Framework, externalSecret *esv1.ExternalSecret) error {
+	if err := f.CreateObjectWithRetryContext(ctx, externalSecret); err != nil {
+		return err
+	}
+
+	var existing esv1.ExternalSecret
+	if err := f.CRClient.Get(ctx, client.ObjectKeyFromObject(externalSecret), &existing); err != nil {
+		return err
+	}
+	if reflect.DeepEqual(existing.Spec, externalSecret.Spec) {
+		return nil
+	}
+
+	externalSecret.SetResourceVersion(existing.GetResourceVersion())
+	externalSecret.SetUID(existing.GetUID())
+	return f.CRClient.Update(ctx, externalSecret)
+}
+
 func createFakeProviderConfig(f *framework.Framework, namespace, name string) {
 	Expect(f.CRClient.Create(context.Background(), &fakev2alpha1.Fake{
 		TypeMeta: metav1.TypeMeta{

+ 107 - 0
e2e/suites/provider/cases/fake/provider_v2_test.go

@@ -17,13 +17,31 @@ limitations under the License.
 package fake
 
 import (
+	"context"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	metav1api "k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
+	"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"
 )
 
+func TestFakeBackendTargetUsesProviderNamespaceAndSelector(t *testing.T) {
+	target := fakeBackendTarget()
+	if target.Namespace != frameworkv2.ProviderNamespace {
+		t.Fatalf("expected provider namespace %q, got %q", frameworkv2.ProviderNamespace, target.Namespace)
+	}
+	if target.PodLabelSelector != "app.kubernetes.io/name=external-secrets-provider-fake" {
+		t.Fatalf("unexpected selector %q", target.PodLabelSelector)
+	}
+}
+
 func TestUpsertFakeProviderDataReplacesMatchingEntry(t *testing.T) {
 	input := []esv1.FakeProviderData{
 		{Key: "other", Value: "untouched"},
@@ -80,3 +98,92 @@ func TestFakeConfigNamespaceForAuthScope(t *testing.T) {
 		t.Fatalf("expected provider namespace for provider scope, got %q", got)
 	}
 }
+
+func TestCreateOrUpdateReadbackExternalSecretUpdatesExistingObject(t *testing.T) {
+	scheme := runtime.NewScheme()
+	if err := esv1.AddToScheme(scheme); err != nil {
+		t.Fatalf("add external secrets scheme: %v", err)
+	}
+
+	cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&esv1.ExternalSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "readback",
+			Namespace: "default",
+		},
+		Spec: esv1.ExternalSecretSpec{
+			Target: esv1.ExternalSecretTarget{Name: "old-target"},
+		},
+	}).Build()
+
+	updated := &esv1.ExternalSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "readback",
+			Namespace: "default",
+		},
+		Spec: esv1.ExternalSecretSpec{
+			Target: esv1.ExternalSecretTarget{Name: "new-target"},
+		},
+	}
+	f := &framework.Framework{CRClient: cl}
+	if err := createOrUpdateReadbackExternalSecret(context.Background(), f, updated); err != nil {
+		t.Fatalf("createOrUpdateReadbackExternalSecret returned error: %v", err)
+	}
+
+	var got esv1.ExternalSecret
+	if err := cl.Get(context.Background(), client.ObjectKeyFromObject(updated), &got); err != nil {
+		t.Fatalf("get external secret: %v", err)
+	}
+	if got.Spec.Target.Name != "new-target" {
+		t.Fatalf("expected updated target name, got %q", got.Spec.Target.Name)
+	}
+}
+
+type flakyReadbackCreateClient struct {
+	client.Client
+	createErrs  []error
+	createCalls int
+}
+
+func (c *flakyReadbackCreateClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
+	callIndex := c.createCalls
+	c.createCalls++
+	if callIndex < len(c.createErrs) && c.createErrs[callIndex] != nil {
+		return c.createErrs[callIndex]
+	}
+	return c.Client.Create(ctx, obj, opts...)
+}
+
+func TestCreateOrUpdateReadbackExternalSecretRetriesMissingAPIResourceErrors(t *testing.T) {
+	scheme := runtime.NewScheme()
+	if err := esv1.AddToScheme(scheme); err != nil {
+		t.Fatalf("add external secrets scheme: %v", err)
+	}
+
+	baseClient := fake.NewClientBuilder().WithScheme(scheme).Build()
+	cl := &flakyReadbackCreateClient{
+		Client: baseClient,
+		createErrs: []error{
+			&metav1api.NoResourceMatchError{},
+		},
+	}
+	f := &framework.Framework{CRClient: cl}
+
+	externalSecret := &esv1.ExternalSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "readback",
+			Namespace: "default",
+		},
+	}
+
+	if err := createOrUpdateReadbackExternalSecret(context.Background(), f, externalSecret); err != nil {
+		t.Fatalf("createOrUpdateReadbackExternalSecret returned error: %v", err)
+	}
+	if cl.createCalls != 2 {
+		t.Fatalf("expected 2 create calls, got %d", cl.createCalls)
+	}
+
+	var got esv1.ExternalSecret
+	if err := baseClient.Get(context.Background(), client.ObjectKeyFromObject(externalSecret), &got); err != nil {
+		t.Fatalf("get external secret: %v", err)
+	}
+}

+ 62 - 0
e2e/suites/provider/cases/kubernetes/metrics_v2.go

@@ -197,4 +197,66 @@ var _ = Describe("[kubernetes] v2 metrics", Label("kubernetes", "v2", "metrics")
 			return found && value >= 1.0
 		}, defaultV2WaitTimeout, defaultV2PollInterval).Should(BeTrue())
 	})
+
+	It("reuses one backend connection across many namespaced kubernetes Provider consumers", func() {
+		const consumerCount = 6
+
+		for i := 0; i < consumerCount; i++ {
+			remoteKey := fmt.Sprintf("%s-operational-metric-%d", f.Namespace.Name, i)
+			targetName := fmt.Sprintf("kubernetes-operational-metric-target-%d", i)
+			expectedValue := fmt.Sprintf("metric-value-%d", i)
+
+			Expect(f.CRClient.Create(context.Background(), &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      remoteKey,
+					Namespace: f.Namespace.Name,
+				},
+				Data: map[string][]byte{
+					"value": []byte(expectedValue),
+				},
+			})).To(Succeed())
+
+			Expect(f.CRClient.Create(context.Background(), &esv1.ExternalSecret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      fmt.Sprintf("kubernetes-operational-metric-es-%d", i),
+					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: targetName,
+					},
+					Data: []esv1.ExternalSecretData{{
+						SecretKey: "value",
+						RemoteRef: esv1.ExternalSecretDataRemoteRef{
+							Key:      remoteKey,
+							Property: "value",
+						},
+					}},
+				},
+			})).To(Succeed())
+
+			Eventually(func(g Gomega) {
+				var secret corev1.Secret
+				g.Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: targetName, Namespace: f.Namespace.Name}, &secret)).To(Succeed())
+				g.Expect(secret.Data["value"]).To(Equal([]byte(expectedValue)))
+			}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
+		}
+
+		Eventually(func(g Gomega) {
+			metrics, err := frameworkv2.ScrapeControllerMetrics(context.Background(), f.KubeConfig, f.KubeClientSet, frameworkv2.ProviderNamespace)
+			g.Expect(err).NotTo(HaveOccurred())
+
+			total := frameworkv2.SumMetricValues(metrics, "grpc_pool_connections_total", map[string]string{
+				"address": frameworkv2.ProviderAddress("kubernetes"),
+			})
+			g.Expect(total).To(BeNumerically(">=", 1))
+			g.Expect(total).To(BeNumerically("<=", 4), "expected bounded connection reuse for kubernetes backend")
+			g.Expect(total).To(BeNumerically("<", consumerCount), "expected fewer pooled connections than consumers")
+		}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
+	})
 })

+ 167 - 0
e2e/suites/provider/cases/kubernetes/operational_v2.go

@@ -0,0 +1,167 @@
+/*
+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 kubernetes
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+
+	"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 operational", Serial, Label("kubernetes", "v2", "operational"), func() {
+	f := framework.New("eso-kubernetes-v2-operational")
+	prov := NewProvider(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+		frameworkv2.WaitForProviderConnectionReady(f, f.Namespace.Name, f.Namespace.Name, defaultV2WaitTimeout)
+	})
+
+	DescribeTable("external secret operational behavior",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(common.NamespacedProviderUnavailable(f, newKubernetesOperationalExternalSecretHarness(f, prov), "kubernetes-operational-unavailable", "recovered")),
+		Entry(common.NamespacedProviderRestart(f, newKubernetesOperationalExternalSecretHarness(f, prov), "kubernetes-operational-restart", "restarted")),
+		Entry(common.ClusterProviderUnavailable(f, newKubernetesOperationalExternalSecretHarness(f, prov), "kubernetes-operational-cluster-unavailable", "cluster-recovered", esv1.AuthenticationScopeManifestNamespace)),
+		Entry(common.ClusterProviderRestart(f, newKubernetesOperationalExternalSecretHarness(f, prov), "kubernetes-operational-cluster-restart", "cluster-restarted", esv1.AuthenticationScopeManifestNamespace)),
+	)
+
+	DescribeTable("push secret operational behavior",
+		framework.TableFuncWithPushSecret(f, prov, nil),
+		Entry(common.NamespacedPushSecretUnavailable(f, newKubernetesOperationalPushHarness(f, prov))),
+		Entry(common.ClusterProviderPushUnavailable(f, newKubernetesOperationalPushHarness(f, prov), esv1.AuthenticationScopeManifestNamespace)),
+	)
+})
+
+func kubernetesBackendTarget() frameworkv2.BackendTarget {
+	return frameworkv2.BackendTarget{
+		Namespace:        frameworkv2.ProviderNamespace,
+		PodLabelSelector: "app.kubernetes.io/name=external-secrets-provider-kubernetes",
+	}
+}
+
+func newKubernetesOperationalExternalSecretHarness(f *framework.Framework, prov *Provider) common.OperationalExternalSecretHarness {
+	return common.OperationalExternalSecretHarness{
+		PrepareNamespaced: func(_ *framework.TestCase) *common.OperationalRuntime {
+			return &common.OperationalRuntime{
+				Provider: prov,
+				ProviderRef: esv1.SecretStoreRef{
+					Name: f.Namespace.Name,
+					Kind: esv1.ProviderKindStr,
+				},
+				MakeUnavailable: func() {
+					frameworkv2.ScaleDeploymentBySelector(f, kubernetesBackendTarget(), 0)
+				},
+				RestoreAvailability: func() {
+					frameworkv2.ScaleDeploymentBySelector(f, kubernetesBackendTarget(), 1)
+				},
+				RestartBackend: func() {
+					frameworkv2.DeleteOneProviderPodBySelector(f, kubernetesBackendTarget())
+				},
+			}
+		},
+		PrepareCluster: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.OperationalRuntime {
+			s := newClusterProviderV2Scenario(f, cfg.Name)
+			s.allowRemoteAccessForScope(cfg.AuthScope, cfg.Name)
+
+			clusterProviderName := s.createClusterProvider(cfg.Name, cfg.AuthScope, cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.OperationalRuntime{
+				Provider: s,
+				ProviderRef: esv1.SecretStoreRef{
+					Name: clusterProviderName,
+					Kind: esv1.ClusterProviderKindStr,
+				},
+				MakeUnavailable: func() {
+					frameworkv2.ScaleDeploymentBySelector(f, kubernetesBackendTarget(), 0)
+				},
+				RestoreAvailability: func() {
+					frameworkv2.ScaleDeploymentBySelector(f, kubernetesBackendTarget(), 1)
+				},
+				RestartBackend: func() {
+					frameworkv2.DeleteOneProviderPodBySelector(f, kubernetesBackendTarget())
+				},
+			}
+		},
+	}
+}
+
+func newKubernetesOperationalPushHarness(f *framework.Framework, prov *Provider) common.OperationalPushSecretHarness {
+	return common.OperationalPushSecretHarness{
+		PrepareNamespaced: func(_ *framework.TestCase) *common.OperationalRuntime {
+			return &common.OperationalRuntime{
+				Provider: prov,
+				ProviderRef: esv1.SecretStoreRef{
+					Name: f.Namespace.Name,
+					Kind: esv1.ProviderKindStr,
+				},
+				DefaultRemoteNamespace: f.Namespace.Name,
+				WaitForRemoteSecret: func(namespace, name, key, expectedValue string) {
+					waitForSecretValueInNamespace(f, namespace, name, key, expectedValue)
+				},
+				ExpectNoRemoteSecret: func(namespace, name string) {
+					expectNoSecretInNamespace(f, namespace, name)
+				},
+				MakeUnavailable: func() {
+					frameworkv2.ScaleDeploymentBySelector(f, kubernetesBackendTarget(), 0)
+				},
+				RestoreAvailability: func() {
+					frameworkv2.ScaleDeploymentBySelector(f, kubernetesBackendTarget(), 1)
+				},
+				RestartBackend: func() {
+					frameworkv2.DeleteOneProviderPodBySelector(f, kubernetesBackendTarget())
+				},
+			}
+		},
+		PrepareCluster: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.OperationalRuntime {
+			s := newClusterProviderV2Scenario(f, cfg.Name)
+			s.allowRemoteAccessForScope(cfg.AuthScope, cfg.Name)
+
+			clusterProviderName := s.createClusterProvider(cfg.Name, cfg.AuthScope, cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.OperationalRuntime{
+				Provider: s,
+				ProviderRef: esv1.SecretStoreRef{
+					Name: clusterProviderName,
+					Kind: esv1.ClusterProviderKindStr,
+				},
+				DefaultRemoteNamespace: s.remoteNamespace,
+				WaitForRemoteSecret: func(namespace, name, key, expectedValue string) {
+					waitForSecretValueInNamespace(f, namespace, name, key, expectedValue)
+				},
+				ExpectNoRemoteSecret: func(namespace, name string) {
+					expectNoSecretInNamespace(f, namespace, name)
+				},
+				MakeUnavailable: func() {
+					frameworkv2.ScaleDeploymentBySelector(f, kubernetesBackendTarget(), 0)
+				},
+				RestoreAvailability: func() {
+					frameworkv2.ScaleDeploymentBySelector(f, kubernetesBackendTarget(), 1)
+				},
+				RestartBackend: func() {
+					frameworkv2.DeleteOneProviderPodBySelector(f, kubernetesBackendTarget())
+				},
+			}
+		},
+	}
+}

+ 33 - 0
e2e/suites/provider/cases/kubernetes/operational_v2_test.go

@@ -0,0 +1,33 @@
+/*
+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 kubernetes
+
+import (
+	"testing"
+
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+)
+
+func TestKubernetesBackendTargetUsesProviderNamespaceAndSelector(t *testing.T) {
+	target := kubernetesBackendTarget()
+	if target.Namespace != frameworkv2.ProviderNamespace {
+		t.Fatalf("expected provider namespace %q, got %q", frameworkv2.ProviderNamespace, target.Namespace)
+	}
+	if target.PodLabelSelector != "app.kubernetes.io/name=external-secrets-provider-kubernetes" {
+		t.Fatalf("unexpected selector %q", target.PodLabelSelector)
+	}
+}

+ 12 - 6
e2e/suites/provider/suite_test.go

@@ -63,18 +63,24 @@ var _ = SynchronizedAfterSuite(func() {
 	By("Deleting any pending generator states")
 	generatorStates := &genv1alpha1.GeneratorStateList{}
 	err := cfg.CRClient.List(GinkgoT().Context(), generatorStates)
-	Expect(err).ToNot(HaveOccurred())
-	for _, generatorState := range generatorStates.Items {
-		err = cfg.CRClient.Delete(GinkgoT().Context(), &generatorState)
+	if err == nil {
+		for _, generatorState := range generatorStates.Items {
+			err = cfg.CRClient.Delete(GinkgoT().Context(), &generatorState)
+			Expect(err).ToNot(HaveOccurred())
+		}
+	} else if !util.IsMissingAPIResourceError(err) {
 		Expect(err).ToNot(HaveOccurred())
 	}
 
 	By("Deleting all ClusterExternalSecrets")
 	externalSecretsList := &v1.ClusterExternalSecretList{}
 	err = cfg.CRClient.List(GinkgoT().Context(), externalSecretsList)
-	Expect(err).ToNot(HaveOccurred())
-	for _, externalSecret := range externalSecretsList.Items {
-		err = cfg.CRClient.Delete(GinkgoT().Context(), &externalSecret)
+	if err == nil {
+		for _, externalSecret := range externalSecretsList.Items {
+			err = cfg.CRClient.Delete(GinkgoT().Context(), &externalSecret)
+			Expect(err).ToNot(HaveOccurred())
+		}
+	} else if !util.IsMissingAPIResourceError(err) {
 		Expect(err).ToNot(HaveOccurred())
 	}