Repository: gardener/gardener
Pull request #11618: e2e: `Refactor Create Rotate Delete Garden` test to ordered containers
Author: login: Wieneo
Labels: name: kind/enhancement
description: Enhancement, improvement, extension
name: area/dev-productivity
description: Developer productivity related (how to improve development)
name: size/L
description: Denotes a PR that changes 100-499 lines, ignoring generated files.
name: cla: yes
description: Indicates the PR's author has signed the cla-assistant.io CLA.
name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
Created: 2025-03-07T11:11:19Z | Merged: 2025-04-16T11:45:58Z

**How to categorize this PR?**
<!--
Please select area, kind, and priority for this pull request. This helps the community categorizing it.
Replace below TODOs or exchange the existing identifiers with those that fit best in your opinion.
If multiple identifiers make sense you can also state the commands multiple times, e.g.
  /area control-plane
  /area auto-scaling
  ...

"/area" identifiers:     audit-logging|auto-scaling|backup|compliance|control-plane-migration|control-plane|cost|delivery|dev-productivity|disaster-recovery|documentation|high-availability|logging|metering|monitoring|networking|open-source|ops-productivity|os|performance|quality|robustness|scalability|security|storage|testing|usability|user-management
"/kind" identifiers:     api-change|bug|cleanup|discussion|enhancement|epic|flake|impediment|poc|post-mortem|question|regression|task|technical-debt|test

For Gardener Enhancement Proposals (GEPs), please check the following [documentation](https://github.com/gardener/gardener/tree/master/docs/proposals/README.md) before submitting this pull request.
-->
/area dev-productivity
/kind enhancement

**What this PR does / why we need it**:
This PR adapts the mentioned test to the ordered containers approach.
See https://github.com/gardener/gardener/issues/11379

I have deliberately not yet refactored the verifiers and their interface in this step, as the PR would then become too large / confusing.

I will submit this work in a separate PR.

**Which issue(s) this PR fixes**:
Part of https://github.com/gardener/gardener/issues/11379

**Special notes for your reviewer**:

**Release note**:
<!--
Write your release note:
1. Enter your release note in the below block.
2. If no release note is required, just write "NONE" within the block.

Format of block header: <category> <target_group>
Possible values:
- category:       breaking|noteworthy|feature|bugfix|doc|other
- target_group:   user|operator|developer|dependency
-->
```other operator
NONE
```



Diff:
diff --git a/test/e2e/operator/garden/common.go b/test/e2e/operator/garden/common.go
index 0b42e82b0ee..81ec3eedb50 100644
--- a/test/e2e/operator/garden/common.go
+++ b/test/e2e/operator/garden/common.go
@@ -5,57 +5,22 @@
 package garden
 
 import (
-	"context"
-	"fmt"
-	"os"
-	"time"
-
-	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	componentbaseconfigv1alpha1 "k8s.io/component-base/config/v1alpha1"
 	"k8s.io/utils/ptr"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-	logf "sigs.k8s.io/controller-runtime/pkg/log"
-	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 
 	gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
+	v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
 	operatorv1alpha1 "github.com/gardener/gardener/pkg/apis/operator/v1alpha1"
-	"github.com/gardener/gardener/pkg/client/kubernetes"
-	"github.com/gardener/gardener/pkg/logger"
-	operatorclient "github.com/gardener/gardener/pkg/operator/client"
 	"github.com/gardener/gardener/pkg/utils"
-	. "github.com/gardener/gardener/pkg/utils/test"
-	. "github.com/gardener/gardener/pkg/utils/test/matchers"
 )
 
-const namespace = "garden"
-
-var (
-	parentCtx     context.Context
-	runtimeClient client.Client
-)
-
-var _ = BeforeSuite(func() {
-	logf.SetLogger(logger.MustNewZapLogger(logger.InfoLevel, logger.FormatJSON, zap.WriteTo(GinkgoWriter)).WithName("garden-test"))
-
-	restConfig, err := kubernetes.RESTConfigFromClientConnectionConfiguration(&componentbaseconfigv1alpha1.ClientConnectionConfiguration{Kubeconfig: os.Getenv("KUBECONFIG")}, nil, kubernetes.AuthTokenFile)
-	Expect(err).NotTo(HaveOccurred())
-
-	runtimeClient, err = client.New(restConfig, client.Options{Scheme: operatorclient.RuntimeScheme})
-	Expect(err).NotTo(HaveOccurred())
-})
-
-var _ = BeforeEach(func() {
-	parentCtx = context.Background()
-})
-
 func defaultBackupSecret() *corev1.Secret {
 	return &corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "virtual-garden-etcd-main-backup",
-			Namespace: namespace,
+			Namespace: v1beta1constants.GardenNamespace,
 		},
 		Type: corev1.SecretTypeOpaque,
 		Data: map[string][]byte{"hostPath": []byte("/etc/gardener/local-backupbuckets")},
@@ -146,81 +111,3 @@ func defaultGarden(backupSecret *corev1.Secret, specifyBackupBucket bool) *opera
 		},
 	}
 }
-
-func waitForGardenToBeReconciledAndHealthy(ctx context.Context, garden *operatorv1alpha1.Garden) {
-	CEventually(ctx, func(g Gomega) bool {
-		g.Expect(runtimeClient.Get(ctx, client.ObjectKeyFromObject(garden), garden)).To(Succeed())
-
-		completed, reason := gardenReconciliationSuccessful(garden)
-		if !completed {
-			logf.Log.Info("Waiting for reconciliation and healthiness", "lastOperation", garden.Status.LastOperation, "reason", reason)
-		}
-		return completed
-	}).WithPolling(10 * time.Second).Should(BeTrue())
-}
-
-func waitForGardenToBeDeleted(ctx context.Context, garden *operatorv1alpha1.Garden) {
-	CEventually(ctx, func() error {
-		return runtimeClient.Get(ctx, client.ObjectKeyFromObject(garden), garden)
-	}).WithPolling(2 * time.Second).Should(BeNotFoundError())
-}
-
-func waitForExtensionToReportDeletion(ctx context.Context, name string) {
-	extension := &operatorv1alpha1.Extension{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: name,
-		},
-	}
-
-	CEventually(ctx, func(g Gomega) {
-		g.Expect(runtimeClient.Get(ctx, client.ObjectKeyFromObject(extension), extension)).To(Succeed())
-		g.Expect(extension.Status.Conditions).Should(ContainCondition(
-			OfType(operatorv1alpha1.ExtensionInstalled),
-			WithStatus(gardencorev1beta1.ConditionFalse),
-			WithReason("DeleteSuccessful"),
-		))
-	}).WithPolling(2 * time.Second).Should(Succeed())
-}
-
-func cleanupVolumes(ctx context.Context) {
-	Expect(runtimeClient.DeleteAllOf(ctx, &corev1.PersistentVolumeClaim{}, client.InNamespace(namespace))).To(Succeed())
-
-	CEventually(ctx, func(g Gomega) bool {
-		pvList := &corev1.PersistentVolumeList{}
-		g.Expect(runtimeClient.List(ctx, pvList)).To(Succeed())
-
-		for _, pv := range pvList.Items {
-			if pv.Spec.ClaimRef != nil &&
-				pv.Spec.ClaimRef.APIVersion == "v1" &&
-				pv.Spec.ClaimRef.Kind == "PersistentVolumeClaim" &&
-				pv.Spec.ClaimRef.Namespace == namespace {
-				return false
-			}
-		}
-
-		return true
-	}).WithPolling(2 * time.Second).Should(BeTrue())
-}
-
-func gardenReconciliationSuccessful(garden *operatorv1alpha1.Garden) (bool, string) {
-	if garden.Generation != garden.Status.ObservedGeneration {
-		return false, "garden generation did not equal observed generation"
-	}
-	if len(garden.Status.Conditions) == 0 && garden.Status.LastOperation == nil {
-		return false, "no conditions and last operation present yet"
-	}
-
-	for _, condition := range garden.Status.Conditions {
-		if condition.Status != gardencorev1beta1.ConditionTrue {
-			return false, fmt.Sprintf("condition type %s is not true yet, had message %s with reason %s", condition.Type, condition.Message, condition.Reason)
-		}
-	}
-
-	if garden.Status.LastOperation != nil {
-		if garden.Status.LastOperation.State != gardencorev1beta1.LastOperationStateSucceeded {
-			return false, "last operation state is not succeeded"
-		}
-	}
-
-	return true, ""
-}
diff --git a/test/e2e/operator/garden/create_rotate_delete.go b/test/e2e/operator/garden/create_rotate_delete.go
index d3c35b1b10b..4fbaea02051 100644
--- a/test/e2e/operator/garden/create_rotate_delete.go
+++ b/test/e2e/operator/garden/create_rotate_delete.go
@@ -21,50 +21,32 @@ import (
 	"github.com/gardener/gardener/pkg/client/kubernetes"
 	operatorclient "github.com/gardener/gardener/pkg/operator/client"
 	"github.com/gardener/gardener/pkg/utils"
-	gardenerutils "github.com/gardener/gardener/pkg/utils/gardener"
+	. "github.com/gardener/gardener/test/e2e"
+	. "github.com/gardener/gardener/test/e2e/gardener"
+	. "github.com/gardener/gardener/test/e2e/operator/garden/internal"
 	"github.com/gardener/gardener/test/e2e/operator/garden/internal/rotation"
 	rotationutils "github.com/gardener/gardener/test/utils/rotation"
 )
 
 var _ = Describe("Garden Tests", Label("Garden", "default"), func() {
-	var (
-		backupSecret = defaultBackupSecret()
-		garden       = defaultGarden(backupSecret, false)
-	)
-
-	It("Create Garden, Rotate Credentials and Delete Garden", Label("credentials-rotation"), func() {
-		ctx, cancel := context.WithTimeout(parentCtx, 20*time.Minute)
-		defer cancel()
-
-		By("Create Garden")
-		Expect(runtimeClient.Create(ctx, backupSecret)).To(Succeed())
-		Expect(runtimeClient.Create(ctx, garden)).To(Succeed())
-		waitForGardenToBeReconciledAndHealthy(ctx, garden)
-
-		DeferCleanup(func() {
-			ctx, cancel = context.WithTimeout(parentCtx, 5*time.Minute)
-			defer cancel()
-
-			By("Delete Garden")
-			Expect(gardenerutils.ConfirmDeletion(ctx, runtimeClient, garden)).To(Succeed())
-			Expect(runtimeClient.Delete(ctx, garden)).To(Succeed())
-			Expect(runtimeClient.Delete(ctx, backupSecret)).To(Succeed())
-			waitForGardenToBeDeleted(ctx, garden)
-			cleanupVolumes(ctx)
-			Expect(runtimeClient.DeleteAllOf(ctx, &corev1.Secret{}, client.InNamespace(namespace), client.MatchingLabels{"role": "kube-apiserver-etcd-encryption-configuration"})).To(Succeed())
-			Expect(runtimeClient.DeleteAllOf(ctx, &corev1.Secret{}, client.InNamespace(namespace), client.MatchingLabels{"role": "gardener-apiserver-etcd-encryption-configuration"})).To(Succeed())
-
-			By("Wait until extension reports a successful uninstallation")
-			waitForExtensionToReportDeletion(ctx, "provider-local")
+	Describe("Create Garden, Rotate Credentials and Delete Garden", Ordered, Label("credentials-rotation"), func() {
+		var s *GardenContext
+
+		BeforeTestSetup(func() {
+			backupSecret := defaultBackupSecret()
+			s = NewTestContext().ForGarden(defaultGarden(backupSecret, false), backupSecret)
 		})
 
+		ItShouldCreateGarden(s)
+		ItShouldWaitForGardenToBeReconciledAndHealthy(s)
+
 		v := rotationutils.Verifiers{
 			// basic verifiers checking secrets
-			&rotation.CAVerifier{RuntimeClient: runtimeClient, Garden: garden},
+			&rotation.CAVerifier{RuntimeClient: s.GardenClient, Garden: s.Garden},
 			&rotationutils.ObservabilityVerifier{
 				GetObservabilitySecretFunc: func(ctx context.Context) (*corev1.Secret, error) {
 					secretList := &corev1.SecretList{}
-					if err := runtimeClient.List(ctx, secretList, client.InNamespace(v1beta1constants.GardenNamespace), client.MatchingLabels{
+					if err := s.GardenClient.List(ctx, secretList, client.InNamespace(v1beta1constants.GardenNamespace), client.MatchingLabels{
 						"managed-by":       "secrets-manager",
 						"manager-identity": "gardener-operator",
 						"name":             "observability-ingress",
@@ -79,57 +61,57 @@ var _ = Describe("Garden Tests", Label("Garden", "default"), func() {
 					return &secretList.Items[0], nil
 				},
 				GetObservabilityEndpoint: func(_ *corev1.Secret) string {
-					return "https://plutono-garden." + garden.Spec.RuntimeCluster.Ingress.Domains[0].Name
+					return "https://plutono-garden." + s.Garden.Spec.RuntimeCluster.Ingress.Domains[0].Name
 				},
 				GetObservabilityRotation: func() *gardencorev1beta1.ObservabilityRotation {
-					return garden.Status.Credentials.Rotation.Observability
+					return s.Garden.Status.Credentials.Rotation.Observability
 				},
 			},
 			&rotationutils.ETCDEncryptionKeyVerifier{
 				GetETCDSecretNamespace: func() string {
-					return namespace
+					return v1beta1constants.GardenNamespace
 				},
 				GetRuntimeClient: func() client.Client {
-					return runtimeClient
+					return s.GardenClient
 				},
 				SecretsManagerLabelSelector: rotation.ManagedByGardenerOperatorSecretsManager,
 				GetETCDEncryptionKeyRotation: func() *gardencorev1beta1.ETCDEncryptionKeyRotation {
-					return garden.Status.Credentials.Rotation.ETCDEncryptionKey
+					return s.Garden.Status.Credentials.Rotation.ETCDEncryptionKey
 				},
 				EncryptionKey:  v1beta1constants.SecretNameETCDEncryptionKey,
 				RoleLabelValue: v1beta1constants.SecretNamePrefixETCDEncryptionConfiguration,
 			},
 			&rotationutils.ETCDEncryptionKeyVerifier{
 				GetETCDSecretNamespace: func() string {
-					return namespace
+					return v1beta1constants.GardenNamespace
 				},
 				GetRuntimeClient: func() client.Client {
-					return runtimeClient
+					return s.GardenClient
 				},
 				SecretsManagerLabelSelector: rotation.ManagedByGardenerOperatorSecretsManager,
 				GetETCDEncryptionKeyRotation: func() *gardencorev1beta1.ETCDEncryptionKeyRotation {
-					return garden.Status.Credentials.Rotation.ETCDEncryptionKey
+					return s.Garden.Status.Credentials.Rotation.ETCDEncryptionKey
 				},
 				EncryptionKey:  v1beta1constants.SecretNameGardenerETCDEncryptionKey,
 				RoleLabelValue: v1beta1constants.SecretNamePrefixGardenerETCDEncryptionConfiguration,
 			},
 			&rotationutils.ServiceAccountKeyVerifier{
 				GetServiceAccountKeySecretNamespace: func() string {
-					return namespace
+					return v1beta1constants.GardenNamespace
 				},
 				GetRuntimeClient: func() client.Client {
-					return runtimeClient
+					return s.GardenClient
 				},
 				SecretsManagerLabelSelector: rotation.ManagedByGardenerOperatorSecretsManager,
 				GetServiceAccountKeyRotation: func() *gardencorev1beta1.ServiceAccountKeyRotation {
-					return garden.Status.Credentials.Rotation.ServiceAccountKey
+					return s.Garden.Status.Credentials.Rotation.ServiceAccountKey
 				},
 			},
 
 			// advanced verifiers testing things from the user's perspective
 			&rotationutils.EncryptedDataVerifier{
 				NewTargetClientFunc: func(ctx context.Context) (kubernetes.Interface, error) {
-					return kubernetes.NewClientFromSecret(ctx, runtimeClient, namespace, "gardener",
+					return kubernetes.NewClientFromSecret(ctx, s.GardenClient, v1beta1constants.GardenNamespace, "gardener",
 						kubernetes.WithDisabledCachedClient(),
 						kubernetes.WithClientOptions(client.Options{Scheme: operatorclient.VirtualScheme}),
 					)
@@ -186,64 +168,73 @@ var _ = Describe("Garden Tests", Label("Garden", "default"), func() {
 					},
 				},
 			},
-			&rotation.VirtualGardenAccessVerifier{RuntimeClient: runtimeClient, Namespace: namespace},
+			&rotation.VirtualGardenAccessVerifier{RuntimeClient: s.GardenClient, Namespace: v1beta1constants.GardenNamespace},
 		}
 
-		DeferCleanup(func() {
-			ctx, cancel := context.WithTimeout(parentCtx, 2*time.Minute)
-			defer cancel()
+		// the verifiers used in this test still use separate "By" statements for structuring tests and expect to be executed within an "It" statement
+		// This is a problem as we removed the "top-level" "It" statements during the refactoring of this test
+		// Until all verifiers are refactored, we need to instantiate separate "It" statements for all shared verifiers to allow for assertions
+		// Also see test/e2e/gardener/shoot/create_rotate_delete.go, where some of the verifiers already got refactored to use separate "It" statements
+		// TODO(Wieneo): Refactor and consolidate verifiers and verifier interface
+		for _, vv := range v {
+			It(fmt.Sprintf("Verify before for %T", vv), func(ctx SpecContext) {
+				vv.Before(ctx)
+			}, SpecTimeout(5*time.Minute))
+		}
 
-			v.Cleanup(ctx)
+		ItShouldAnnotateGarden(s, map[string]string{
+			v1beta1constants.GardenerOperation: v1beta1constants.OperationRotateCredentialsStart,
 		})
 
-		v.Before(ctx)
-
-		By("Start credentials rotation")
-		ctx, cancel = context.WithTimeout(parentCtx, 20*time.Minute)
-		defer cancel()
-
-		patch := client.MergeFrom(garden.DeepCopy())
-		metav1.SetMetaDataAnnotation(&garden.ObjectMeta, v1beta1constants.GardenerOperation, v1beta1constants.OperationRotateCredentialsStart)
-		Eventually(func() error {
-			return runtimeClient.Patch(ctx, garden, patch)
-		}).Should(Succeed())
+		ItShouldEventuallyNotHaveOperationAnnotation(s.GardenKomega, s.Garden)
 
-		Eventually(func(g Gomega) {
-			g.Expect(runtimeClient.Get(ctx, client.ObjectKeyFromObject(garden), garden)).To(Succeed())
-			g.Expect(garden.Annotations).NotTo(HaveKey(v1beta1constants.GardenerOperation))
-			v.ExpectPreparingStatus(g)
-		}).Should(Succeed())
+		It("Rotation in Preparing status", func(ctx SpecContext) {
+			Eventually(ctx, func(g Gomega) {
+				g.Expect(s.GardenKomega.Get(s.Garden)()).To(Succeed())
+				v.ExpectPreparingStatus(g)
+			}).Should(Succeed())
+		}, SpecTimeout(time.Minute))
 
-		waitForGardenToBeReconciledAndHealthy(ctx, garden)
+		ItShouldWaitForGardenToBeReconciledAndHealthy(s)
 
-		Eventually(func() error {
-			return runtimeClient.Get(ctx, client.ObjectKeyFromObject(garden), garden)
-		}).Should(Succeed())
+		for _, vv := range v {
+			It(fmt.Sprintf("Verify after prepared for %T", vv), func(ctx SpecContext) {
+				vv.AfterPrepared(ctx)
+			}, SpecTimeout(5*time.Minute))
+		}
 
-		v.AfterPrepared(ctx)
+		ItShouldAnnotateGarden(s, map[string]string{
+			v1beta1constants.GardenerOperation: v1beta1constants.OperationRotateCredentialsComplete,
+		})
 
-		By("Complete credentials rotation")
-		ctx, cancel = context.WithTimeout(parentCtx, 20*time.Minute)
-		defer cancel()
+		ItShouldEventuallyNotHaveOperationAnnotation(s.GardenKomega, s.Garden)
 
-		patch = client.MergeFrom(garden.DeepCopy())
-		metav1.SetMetaDataAnnotation(&garden.ObjectMeta, v1beta1constants.GardenerOperation, v1beta1constants.OperationRotateCredentialsComplete)
-		Eventually(func() error {
-			return runtimeClient.Patch(ctx, garden, patch)
-		}).Should(Succeed())
+		It("Rotation in Completing status", func(ctx SpecContext) {
+			Eventually(ctx, func(g Gomega) {
+				g.Expect(s.GardenKomega.Get(s.Garden)()).To(Succeed())
+				v.ExpectCompletingStatus(g)
+			}).Should(Succeed())
+		}, SpecTimeout(time.Minute))
 
-		Eventually(func(g Gomega) {
-			g.Expect(runtimeClient.Get(ctx, client.ObjectKeyFromObject(garden), garden)).To(Succeed())
-			g.Expect(garden.Annotations).NotTo(HaveKey(v1beta1constants.GardenerOperation))
-			v.ExpectCompletingStatus(g)
-		}).Should(Succeed())
+		ItShouldWaitForGardenToBeReconciledAndHealthy(s)
 
-		waitForGardenToBeReconciledAndHealthy(ctx, garden)
+		for _, vv := range v {
+			It(fmt.Sprintf("Verify after completed for %T", vv), func(ctx SpecContext) {
+				vv.AfterCompleted(ctx)
+			}, SpecTimeout(5*time.Minute))
+		}
 
-		Eventually(func(g Gomega) {
-			g.Expect(runtimeClient.Get(ctx, client.ObjectKeyFromObject(garden), garden)).To(Succeed())
-		}).Should(Succeed())
+		for _, vv := range v {
+			if cleanup, ok := vv.(rotationutils.CleanupVerifier); ok {
+				It(fmt.Sprintf("Cleanup for %T", vv), func(ctx SpecContext) {
+					cleanup.Cleanup(ctx)
+				})
+			}
+		}
 
-		v.AfterCompleted(ctx)
+		ItShouldDeleteGarden(s)
+		ItShouldWaitForGardenToBeDeleted(s)
+		ItShouldCleanUp(s)
+		ItShouldWaitForExtensionToReportDeletion(s, "provider-local")
 	})
 })
diff --git a/test/e2e/operator/garden/internal/garden.go b/test/e2e/operator/garden/internal/garden.go
index 0efbc5874d9..b8cdbdda3ff 100644
--- a/test/e2e/operator/garden/internal/garden.go
+++ b/test/e2e/operator/garden/internal/garden.go
@@ -120,6 +120,24 @@ func ItShouldWaitForGardenToBeReconciledAndHealthy(s *GardenContext) {
 	}, SpecTimeout(15*time.Minute))
 }
 
+// ItShouldAnnotateGarden sets the given annotation within the garden metadata to the specified value and patches the garden object
+func ItShouldAnnotateGarden(s *GardenContext, annotations map[string]string) {
+	GinkgoHelper()
+
+	It("Annotate Garden", func(ctx SpecContext) {
+		patch := client.MergeFrom(s.Garden.DeepCopy())
+
+		for key, value := range annotations {
+			s.Log.Info("Setting annotation", "annotation", key, "value", value)
+			metav1.SetMetaDataAnnotation(&s.Garden.ObjectMeta, key, value)
+		}
+
+		Eventually(ctx, func() error {
+			return s.GardenClient.Patch(ctx, s.Garden, patch)
+		}).Should(Succeed())
+	}, SpecTimeout(time.Minute))
+}
+
 // ItShouldInitializeVirtualClusterClient initialized the contexts virtual cluster client from the "gardener" secret in the garden namespace
 func ItShouldInitializeVirtualClusterClient(s *GardenContext) {
 	GinkgoHelper()


Reviews:
user: login: marc1404
body: /lgtm

Thanks for taking care of the ordered-it refactoring of this e2e test and sorry for coming back to your PR so late!
Good idea to split the refactoring of the verifier interfaces 👍 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T11:43:46Z
user: login: rfranzke
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T07:36:55Z

Review comments:
diff_hunk: @@ -120,6 +120,24 @@ func ItShouldWaitForGardenToBeReconciledAndHealthy(s *GardenContext) {
 	}, SpecTimeout(15*time.Minute))
 }
 
+// ItShouldAnnotateGarden sets the given annotation within the garden metadata to the specified value and patches the garden object
+func ItShouldAnnotateGarden(s *GardenContext, annotations map[string]string) {
+	GinkgoHelper()
+
+	It("Annotate Garden", func(ctx SpecContext) {
+		patch := client.MergeFrom(s.Garden.DeepCopy())
+
+		for annotationKey, annotationValue := range annotations {
path: test/e2e/operator/garden/internal/garden.go
user: login: rfranzke
body: Nit:

```suggestion
		for key, value := range annotations {
```
created_at: 2025-04-16T07:34:12Z
author_association: MEMBER
diff_hunk: @@ -21,50 +21,31 @@ import (
 	"github.com/gardener/gardener/pkg/client/kubernetes"
 	operatorclient "github.com/gardener/gardener/pkg/operator/client"
 	"github.com/gardener/gardener/pkg/utils"
-	gardenerutils "github.com/gardener/gardener/pkg/utils/gardener"
+	. "github.com/gardener/gardener/test/e2e"
+	. "github.com/gardener/gardener/test/e2e/gardener"
+	. "github.com/gardener/gardener/test/e2e/operator/garden/internal"
 	"github.com/gardener/gardener/test/e2e/operator/garden/internal/rotation"
 	rotationutils "github.com/gardener/gardener/test/utils/rotation"
 )
 
 var _ = Describe("Garden Tests", Label("Garden", "default"), func() {
-	var (
-		backupSecret = defaultBackupSecret()
-		garden       = defaultGarden(backupSecret, false)
-	)
-
-	It("Create Garden, Rotate Credentials and Delete Garden", Label("credentials-rotation"), func() {
-		ctx, cancel := context.WithTimeout(parentCtx, 20*time.Minute)
-		defer cancel()
-
-		By("Create Garden")
-		Expect(runtimeClient.Create(ctx, backupSecret)).To(Succeed())
-		Expect(runtimeClient.Create(ctx, garden)).To(Succeed())
-		waitForGardenToBeReconciledAndHealthy(ctx, garden)
-
-		DeferCleanup(func() {
-			ctx, cancel = context.WithTimeout(parentCtx, 5*time.Minute)
-			defer cancel()
-
-			By("Delete Garden")
-			Expect(gardenerutils.ConfirmDeletion(ctx, runtimeClient, garden)).To(Succeed())
-			Expect(runtimeClient.Delete(ctx, garden)).To(Succeed())
-			Expect(runtimeClient.Delete(ctx, backupSecret)).To(Succeed())
-			waitForGardenToBeDeleted(ctx, garden)
-			cleanupVolumes(ctx)
-			Expect(runtimeClient.DeleteAllOf(ctx, &corev1.Secret{}, client.InNamespace(namespace), client.MatchingLabels{"role": "kube-apiserver-etcd-encryption-configuration"})).To(Succeed())
-			Expect(runtimeClient.DeleteAllOf(ctx, &corev1.Secret{}, client.InNamespace(namespace), client.MatchingLabels{"role": "gardener-apiserver-etcd-encryption-configuration"})).To(Succeed())
-
-			By("Wait until extension reports a successful uninstallation")
-			waitForExtensionToReportDeletion(ctx, "provider-local")
+	Describe("Create Garden, Rotate Credentials and Delete Garden", Ordered, Label("credentials-rotation"), func() {
+		var s *GardenContext
+		BeforeTestSetup(func() {
path: test/e2e/operator/garden/create_rotate_delete.go
user: login: rfranzke
body: ```suggestion

		BeforeTestSetup(func() {
```
created_at: 2025-04-16T07:34:47Z
author_association: MEMBER
diff_hunk: @@ -186,64 +167,73 @@ var _ = Describe("Garden Tests", Label("Garden", "default"), func() {
 					},
 				},
 			},
-			&rotation.VirtualGardenAccessVerifier{RuntimeClient: runtimeClient, Namespace: namespace},
+			&rotation.VirtualGardenAccessVerifier{RuntimeClient: s.GardenClient, Namespace: v1beta1constants.GardenNamespace},
 		}
 
-		DeferCleanup(func() {
-			ctx, cancel := context.WithTimeout(parentCtx, 2*time.Minute)
-			defer cancel()
+		// the verifiers used in this test still use separate "By" statements for structuring tests and expect to be executed within an "It" statement
+		// This is a problem as we removed the "top-level" "It" statements during the refactoring of this test
+		// Until all verifiers are refactored, we need to instantiate separate "It" statements for all shared verifiers to allow for assertions
+		// Also see test/e2e/gardener/shoot/create_rotate_delete.go, where some of the verifiers already got refactored to use separate "It" statements
+		// TODO(Wieneo): Refactor and consolidate verifiers and verifier interface
+		for _, vv := range v {
+			It(fmt.Sprintf("Verify before for %T", vv), func(ctx SpecContext) {
+				vv.Before(ctx)
+			}, SpecTimeout(5*time.Minute))
+		}
 
-			v.Cleanup(ctx)
+		ItShouldAnnotateGarden(s, map[string]string{
+			v1beta1constants.GardenerOperation: v1beta1constants.OperationRotateCredentialsStart,
 		})
 
-		v.Before(ctx)
-
-		By("Start credentials rotation")
-		ctx, cancel = context.WithTimeout(parentCtx, 20*time.Minute)
-		defer cancel()
-
-		patch := client.MergeFrom(garden.DeepCopy())
-		metav1.SetMetaDataAnnotation(&garden.ObjectMeta, v1beta1constants.GardenerOperation, v1beta1constants.OperationRotateCredentialsStart)
-		Eventually(func() error {
-			return runtimeClient.Patch(ctx, garden, patch)
-		}).Should(Succeed())
+		ItShouldEventuallyNotHaveOperationAnnotation(s.GardenKomega, s.Garden)
 
-		Eventually(func(g Gomega) {
-			g.Expect(runtimeClient.Get(ctx, client.ObjectKeyFromObject(garden), garden)).To(Succeed())
-			g.Expect(garden.Annotations).NotTo(HaveKey(v1beta1constants.GardenerOperation))
-			v.ExpectPreparingStatus(g)
-		}).Should(Succeed())
+		It("Rotation in preparing status", func(ctx SpecContext) {
path: test/e2e/operator/garden/create_rotate_delete.go
user: login: rfranzke
body: ```suggestion
		It("Rotation in Preparing status", func(ctx SpecContext) {
```
created_at: 2025-04-16T07:36:18Z
author_association: MEMBER
diff_hunk: @@ -186,64 +167,73 @@ var _ = Describe("Garden Tests", Label("Garden", "default"), func() {
 					},
 				},
 			},
-			&rotation.VirtualGardenAccessVerifier{RuntimeClient: runtimeClient, Namespace: namespace},
+			&rotation.VirtualGardenAccessVerifier{RuntimeClient: s.GardenClient, Namespace: v1beta1constants.GardenNamespace},
 		}
 
-		DeferCleanup(func() {
-			ctx, cancel := context.WithTimeout(parentCtx, 2*time.Minute)
-			defer cancel()
+		// the verifiers used in this test still use separate "By" statements for structuring tests and expect to be executed within an "It" statement
+		// This is a problem as we removed the "top-level" "It" statements during the refactoring of this test
+		// Until all verifiers are refactored, we need to instantiate separate "It" statements for all shared verifiers to allow for assertions
+		// Also see test/e2e/gardener/shoot/create_rotate_delete.go, where some of the verifiers already got refactored to use separate "It" statements
+		// TODO(Wieneo): Refactor and consolidate verifiers and verifier interface
+		for _, vv := range v {
+			It(fmt.Sprintf("Verify before for %T", vv), func(ctx SpecContext) {
+				vv.Before(ctx)
+			}, SpecTimeout(5*time.Minute))
+		}
 
-			v.Cleanup(ctx)
+		ItShouldAnnotateGarden(s, map[string]string{
+			v1beta1constants.GardenerOperation: v1beta1constants.OperationRotateCredentialsStart,
 		})
 
-		v.Before(ctx)
-
-		By("Start credentials rotation")
-		ctx, cancel = context.WithTimeout(parentCtx, 20*time.Minute)
-		defer cancel()
-
-		patch := client.MergeFrom(garden.DeepCopy())
-		metav1.SetMetaDataAnnotation(&garden.ObjectMeta, v1beta1constants.GardenerOperation, v1beta1constants.OperationRotateCredentialsStart)
-		Eventually(func() error {
-			return runtimeClient.Patch(ctx, garden, patch)
-		}).Should(Succeed())
+		ItShouldEventuallyNotHaveOperationAnnotation(s.GardenKomega, s.Garden)
 
-		Eventually(func(g Gomega) {
-			g.Expect(runtimeClient.Get(ctx, client.ObjectKeyFromObject(garden), garden)).To(Succeed())
-			g.Expect(garden.Annotations).NotTo(HaveKey(v1beta1constants.GardenerOperation))
-			v.ExpectPreparingStatus(g)
-		}).Should(Succeed())
+		It("Rotation in preparing status", func(ctx SpecContext) {
+			Eventually(ctx, func(g Gomega) {
+				g.Expect(s.GardenKomega.Get(s.Garden)()).To(Succeed())
+				v.ExpectPreparingStatus(g)
+			}).Should(Succeed())
+		}, SpecTimeout(time.Minute))
 
-		waitForGardenToBeReconciledAndHealthy(ctx, garden)
+		ItShouldWaitForGardenToBeReconciledAndHealthy(s)
 
-		Eventually(func() error {
-			return runtimeClient.Get(ctx, client.ObjectKeyFromObject(garden), garden)
-		}).Should(Succeed())
+		for _, vv := range v {
+			It(fmt.Sprintf("Verify after prepared for %T", vv), func(ctx SpecContext) {
+				vv.AfterPrepared(ctx)
+			}, SpecTimeout(5*time.Minute))
+		}
 
-		v.AfterPrepared(ctx)
+		ItShouldAnnotateGarden(s, map[string]string{
+			v1beta1constants.GardenerOperation: v1beta1constants.OperationRotateCredentialsComplete,
+		})
 
-		By("Complete credentials rotation")
-		ctx, cancel = context.WithTimeout(parentCtx, 20*time.Minute)
-		defer cancel()
+		ItShouldEventuallyNotHaveOperationAnnotation(s.GardenKomega, s.Garden)
 
-		patch = client.MergeFrom(garden.DeepCopy())
-		metav1.SetMetaDataAnnotation(&garden.ObjectMeta, v1beta1constants.GardenerOperation, v1beta1constants.OperationRotateCredentialsComplete)
-		Eventually(func() error {
-			return runtimeClient.Patch(ctx, garden, patch)
-		}).Should(Succeed())
+		It("Rotation in completing status", func(ctx SpecContext) {
path: test/e2e/operator/garden/create_rotate_delete.go
user: login: rfranzke
body: ```suggestion
		It("Rotation in Completing status", func(ctx SpecContext) {
```
created_at: 2025-04-16T07:36:39Z
author_association: MEMBER

PR comments:
user: login: Wieneo
created_at: 2025-03-07T11:12:01Z
body: /hold

Wait for https://github.com/gardener/gardener/pull/11562 and https://github.com/gardener/gardener/pull/11603 to be merged.
Will drop duplicate commits after PRs were merged.
author_association: MEMBER
user: login: gardener-ci-robot
created_at: 2025-03-28T15:24:13Z
body: The Gardener project currently lacks enough active contributors to adequately respond to all PRs.
This bot triages PRs according to the following rules:
- After 15d of inactivity, `lifecycle/stale` is applied
- After 15d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 7d of inactivity since `lifecycle/rotten` was applied, the PR is closed

You can:
- Mark this PR as fresh with `/remove-lifecycle stale`
- Mark this PR as rotten with `/lifecycle rotten`
- Close this PR with `/close`

/lifecycle stale
author_association: CONTRIBUTOR
user: login: gardener-ci-robot
created_at: 2025-04-12T15:40:02Z
body: The Gardener project currently lacks enough active contributors to adequately respond to all PRs.
This bot triages PRs according to the following rules:
- After 15d of inactivity, `lifecycle/stale` is applied
- After 15d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 7d of inactivity since `lifecycle/rotten` was applied, the PR is closed

You can:
- Mark this PR as fresh with `/remove-lifecycle rotten`
- Close this PR with `/close`

/lifecycle rotten
author_association: CONTRIBUTOR
user: login: Wieneo
created_at: 2025-04-14T07:03:13Z
body: /unhold
author_association: MEMBER
user: login: Wieneo
created_at: 2025-04-14T07:11:33Z
body: /remove-lifecycle rotten
author_association: MEMBER
user: login: marc1404
created_at: 2025-04-14T07:14:17Z
body: /assign
author_association: MEMBER
user: login: Wieneo
created_at: 2025-04-14T08:42:33Z
body: /retest
author_association: MEMBER
user: login: gardener-prow[bot]
created_at: 2025-04-15T11:43:51Z
body: LGTM label has been added.  <details>Git tree hash: 5572831150b31d4998e688289f8c490d2ca49b34</details>
author_association: CONTRIBUTOR
user: login: rfranzke
created_at: 2025-04-16T07:41:22Z
body: Awesome, thanks!

/lgtm
/approve
author_association: MEMBER
user: login: gardener-prow[bot]
created_at: 2025-04-16T07:41:28Z
body: LGTM label has been added.  <details>Git tree hash: 6b7ca944264542e604180300f26362254da8050c</details>
author_association: CONTRIBUTOR
user: login: gardener-prow[bot]
created_at: 2025-04-16T07:41:30Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/gardener/gardener/pull/11618#issuecomment-2808686922" title="Approved">rfranzke</a>*

The full list of commands accepted by this bot can be found [here](https://prow.gardener.cloud/command-help?repo=gardener%2Fgardener).

The pull request process is described [here](https://gardener.cloud/docs/contribute/#pull-request-checklist)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/gardener/gardener/blob/master/OWNERS)~~ [rfranzke]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR