Repository: RamenDR/ramen
Pull request #2011: e2e: Support cancellation
Author: login: nirs
Labels: name: high
description: Issue is of high priority and needs attention
Created: 2025-04-22T20:03:52Z | Merged: 2025-04-24T12:23:29Z

When user run or clean tests in ramenctl, the command can block for 10 minutes
before it fails. Users are likely to interrupt the block command and report
bugs with the test report[1]. However if ramenctl is interrupted, it exits
without writing a test report, and we don't have enough info about the failure.

We cannot educate users to wait until commands completes, so we should handle
interruption gracefully. The best way to do this is to support cancellation in
e2e using context.Context. The k8s client already support cancellation, if we
pass a cancellable context.Context instead of context.TODO() or
context.Background().

Passing context.Context to all functions so we can pass it to the client
function does not make sense. We already pass types.Context() to most of the
code to accessing test related state. This change use our context to make
cancellation possible.

This change renames the types.Context interface to types.TestContext, and add a
new types.Context interface for code running in global context. The
types.Context interface includes Context() function returning the shared
context.Context. This context is used now in the client calls using
context.Context. When we wait during retires we fail early if the context was
canceled.

With these changes, ramenctl can pass a cancellable context to ramen/e2e
functions, and cancel the context when receiving a termination signal. This
avoids making entire ramenctl thread safe which will make it harder to
contribute to.

[1] https://github.com/RamenDR/ramenctl/issues/112

Blocks https://github.com/RamenDR/ramenctl/issues/43


Diff:
diff --git a/e2e/deployers/appset.go b/e2e/deployers/appset.go
index 13d0b25df4..09e1a4ba04 100644
--- a/e2e/deployers/appset.go
+++ b/e2e/deployers/appset.go
@@ -12,7 +12,7 @@ import (
 type ApplicationSet struct{}
 
 // Deploy creates an ApplicationSet on the hub cluster, creating the workload on one of the managed clusters.
-func (a ApplicationSet) Deploy(ctx types.Context) error {
+func (a ApplicationSet) Deploy(ctx types.TestContext) error {
 	name := ctx.Name()
 	log := ctx.Logger()
 	managementNamespace := ctx.ManagementNamespace()
@@ -44,12 +44,12 @@ func (a ApplicationSet) Deploy(ctx types.Context) error {
 }
 
 // Undeploy deletes an ApplicationSet from the hub cluster, deleting the workload from the managed clusters.
-func (a ApplicationSet) Undeploy(ctx types.Context) error {
+func (a ApplicationSet) Undeploy(ctx types.TestContext) error {
 	name := ctx.Name()
 	log := ctx.Logger()
 	managementNamespace := ctx.ManagementNamespace()
 
-	clusterName, err := util.GetCurrentCluster(ctx.Env().Hub, managementNamespace, name)
+	clusterName, err := util.GetCurrentCluster(ctx, managementNamespace, name)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return err
@@ -86,7 +86,7 @@ func (a ApplicationSet) GetName() string {
 	return "appset"
 }
 
-func (a ApplicationSet) GetNamespace(ctx types.Context) string {
+func (a ApplicationSet) GetNamespace(ctx types.TestContext) string {
 	return ctx.Config().Namespaces.ArgocdNamespace
 }
 
diff --git a/e2e/deployers/crud.go b/e2e/deployers/crud.go
index 01d5f5684b..869a6c3187 100644
--- a/e2e/deployers/crud.go
+++ b/e2e/deployers/crud.go
@@ -4,7 +4,6 @@
 package deployers
 
 import (
-	"context"
 	"encoding/json"
 	"fmt"
 	"os"
@@ -31,7 +30,7 @@ const (
 	fMode = 0o600
 )
 
-func CreateManagedClusterSetBinding(ctx types.Context, name, namespace string) error {
+func CreateManagedClusterSetBinding(ctx types.TestContext, name, namespace string) error {
 	log := ctx.Logger()
 	config := ctx.Config()
 	labels := make(map[string]string)
@@ -47,7 +46,7 @@ func CreateManagedClusterSetBinding(ctx types.Context, name, namespace string) e
 		},
 	}
 
-	err := ctx.Env().Hub.Client.Create(context.Background(), mcsb)
+	err := ctx.Env().Hub.Client.Create(ctx.Context(), mcsb)
 	if err != nil {
 		if !k8serrors.IsAlreadyExists(err) {
 			return err
@@ -61,7 +60,7 @@ func CreateManagedClusterSetBinding(ctx types.Context, name, namespace string) e
 	return nil
 }
 
-func DeleteManagedClusterSetBinding(ctx types.Context, name, namespace string) error {
+func DeleteManagedClusterSetBinding(ctx types.TestContext, name, namespace string) error {
 	log := ctx.Logger()
 	mcsb := &ocmv1b2.ManagedClusterSetBinding{
 		ObjectMeta: metav1.ObjectMeta{
@@ -70,7 +69,7 @@ func DeleteManagedClusterSetBinding(ctx types.Context, name, namespace string) e
 		},
 	}
 
-	err := ctx.Env().Hub.Client.Delete(context.Background(), mcsb)
+	err := ctx.Env().Hub.Client.Delete(ctx.Context(), mcsb)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return err
@@ -84,7 +83,7 @@ func DeleteManagedClusterSetBinding(ctx types.Context, name, namespace string) e
 	return nil
 }
 
-func CreatePlacement(ctx types.Context, name, namespace string, clusterName string) error {
+func CreatePlacement(ctx types.TestContext, name, namespace string, clusterName string) error {
 	log := ctx.Logger()
 	config := ctx.Config()
 	labels := make(map[string]string)
@@ -120,7 +119,7 @@ func CreatePlacement(ctx types.Context, name, namespace string, clusterName stri
 		},
 	}
 
-	err := ctx.Env().Hub.Client.Create(context.Background(), placement)
+	err := ctx.Env().Hub.Client.Create(ctx.Context(), placement)
 	if err != nil {
 		if !k8serrors.IsAlreadyExists(err) {
 			return err
@@ -134,7 +133,7 @@ func CreatePlacement(ctx types.Context, name, namespace string, clusterName stri
 	return nil
 }
 
-func DeletePlacement(ctx types.Context, name, namespace string) error {
+func DeletePlacement(ctx types.TestContext, name, namespace string) error {
 	log := ctx.Logger()
 	placement := &ocmv1b1.Placement{
 		ObjectMeta: metav1.ObjectMeta{
@@ -143,7 +142,7 @@ func DeletePlacement(ctx types.Context, name, namespace string) error {
 		},
 	}
 
-	err := ctx.Env().Hub.Client.Delete(context.Background(), placement)
+	err := ctx.Env().Hub.Client.Delete(ctx.Context(), placement)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return err
@@ -157,7 +156,7 @@ func DeletePlacement(ctx types.Context, name, namespace string) error {
 	return nil
 }
 
-func CreateSubscription(ctx types.Context, s Subscription) error {
+func CreateSubscription(ctx types.TestContext, s Subscription) error {
 	name := ctx.Name()
 	log := ctx.Logger()
 	config := ctx.Config()
@@ -202,7 +201,7 @@ func CreateSubscription(ctx types.Context, s Subscription) error {
 		})
 	}
 
-	err := ctx.Env().Hub.Client.Create(context.Background(), subscription)
+	err := ctx.Env().Hub.Client.Create(ctx.Context(), subscription)
 	if err != nil {
 		if !k8serrors.IsAlreadyExists(err) {
 			return err
@@ -216,7 +215,7 @@ func CreateSubscription(ctx types.Context, s Subscription) error {
 	return nil
 }
 
-func DeleteSubscription(ctx types.Context, s Subscription) error {
+func DeleteSubscription(ctx types.TestContext, s Subscription) error {
 	name := ctx.Name()
 	log := ctx.Logger()
 	managementNamespace := ctx.ManagementNamespace()
@@ -228,7 +227,7 @@ func DeleteSubscription(ctx types.Context, s Subscription) error {
 		},
 	}
 
-	err := ctx.Env().Hub.Client.Delete(context.Background(), subscription)
+	err := ctx.Env().Hub.Client.Delete(ctx.Context(), subscription)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return err
@@ -242,11 +241,13 @@ func DeleteSubscription(ctx types.Context, s Subscription) error {
 	return nil
 }
 
-func getSubscription(cluster types.Cluster, namespace, name string) (*subscriptionv1.Subscription, error) {
+func getSubscription(ctx types.TestContext, namespace, name string) (*subscriptionv1.Subscription, error) {
+	hub := ctx.Env().Hub
+
 	subscription := &subscriptionv1.Subscription{}
 	key := k8stypes.NamespacedName{Name: name, Namespace: namespace}
 
-	err := cluster.Client.Get(context.Background(), key, subscription)
+	err := hub.Client.Get(ctx.Context(), key, subscription)
 	if err != nil {
 		return nil, err
 	}
@@ -254,7 +255,7 @@ func getSubscription(cluster types.Cluster, namespace, name string) (*subscripti
 	return subscription, nil
 }
 
-func CreatePlacementDecisionConfigMap(ctx types.Context, cmName string, cmNamespace string) error {
+func CreatePlacementDecisionConfigMap(ctx types.TestContext, cmName string, cmNamespace string) error {
 	log := ctx.Logger()
 	object := metav1.ObjectMeta{Name: cmName, Namespace: cmNamespace}
 
@@ -267,7 +268,7 @@ func CreatePlacementDecisionConfigMap(ctx types.Context, cmName string, cmNamesp
 
 	configMap := &corev1.ConfigMap{ObjectMeta: object, Data: data}
 
-	err := ctx.Env().Hub.Client.Create(context.Background(), configMap)
+	err := ctx.Env().Hub.Client.Create(ctx.Context(), configMap)
 	if err != nil {
 		if !k8serrors.IsAlreadyExists(err) {
 			return fmt.Errorf("could not create configMap %q", cmName)
@@ -281,7 +282,7 @@ func CreatePlacementDecisionConfigMap(ctx types.Context, cmName string, cmNamesp
 	return nil
 }
 
-func DeleteConfigMap(ctx types.Context, cmName string, cmNamespace string) error {
+func DeleteConfigMap(ctx types.TestContext, cmName string, cmNamespace string) error {
 	log := ctx.Logger()
 	object := metav1.ObjectMeta{Name: cmName, Namespace: cmNamespace}
 
@@ -289,7 +290,7 @@ func DeleteConfigMap(ctx types.Context, cmName string, cmNamespace string) error
 		ObjectMeta: object,
 	}
 
-	err := ctx.Env().Hub.Client.Delete(context.Background(), configMap)
+	err := ctx.Env().Hub.Client.Delete(ctx.Context(), configMap)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return fmt.Errorf("could not delete configMap %q in cluster %q", cmName, ctx.Env().Hub.Name)
@@ -304,7 +305,7 @@ func DeleteConfigMap(ctx types.Context, cmName string, cmNamespace string) error
 }
 
 // nolint:funlen
-func CreateApplicationSet(ctx types.Context, a ApplicationSet) error {
+func CreateApplicationSet(ctx types.TestContext, a ApplicationSet) error {
 	var requeueSeconds int64 = 180
 
 	name := ctx.Name()
@@ -374,7 +375,7 @@ func CreateApplicationSet(ctx types.Context, a ApplicationSet) error {
 		appset.Spec.Template.Spec.Source.Kustomize = patches
 	}
 
-	err := ctx.Env().Hub.Client.Create(context.Background(), appset)
+	err := ctx.Env().Hub.Client.Create(ctx.Context(), appset)
 	if err != nil {
 		if !k8serrors.IsAlreadyExists(err) {
 			return err
@@ -388,7 +389,7 @@ func CreateApplicationSet(ctx types.Context, a ApplicationSet) error {
 	return nil
 }
 
-func DeleteApplicationSet(ctx types.Context, a ApplicationSet) error {
+func DeleteApplicationSet(ctx types.TestContext, a ApplicationSet) error {
 	name := ctx.Name()
 	log := ctx.Logger()
 	managementNamespace := ctx.ManagementNamespace()
@@ -400,7 +401,7 @@ func DeleteApplicationSet(ctx types.Context, a ApplicationSet) error {
 		},
 	}
 
-	err := ctx.Env().Hub.Client.Delete(context.Background(), appset)
+	err := ctx.Env().Hub.Client.Delete(ctx.Context(), appset)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return err
@@ -414,7 +415,7 @@ func DeleteApplicationSet(ctx types.Context, a ApplicationSet) error {
 	return nil
 }
 
-func DeleteDiscoveredApps(ctx types.Context, cluster types.Cluster, namespace string) error {
+func DeleteDiscoveredApps(ctx types.TestContext, cluster types.Cluster, namespace string) error {
 	log := ctx.Logger()
 
 	tempDir, err := os.MkdirTemp("", "ramen-")
@@ -448,7 +449,7 @@ func DeleteDiscoveredApps(ctx types.Context, cluster types.Cluster, namespace st
 
 type CombinedData map[string]interface{}
 
-func CreateKustomizationFile(ctx types.Context, dir string) error {
+func CreateKustomizationFile(ctx types.TestContext, dir string) error {
 	w := ctx.Workload()
 	config := ctx.Config()
 	yamlData := `resources:
diff --git a/e2e/deployers/disapp.go b/e2e/deployers/disapp.go
index 82a32c3f31..eeb36fe255 100644
--- a/e2e/deployers/disapp.go
+++ b/e2e/deployers/disapp.go
@@ -18,12 +18,12 @@ func (d DiscoveredApp) GetName() string {
 	return "disapp"
 }
 
-func (d DiscoveredApp) GetNamespace(ctx types.Context) string {
+func (d DiscoveredApp) GetNamespace(ctx types.TestContext) string {
 	return ctx.Config().Namespaces.RamenOpsNamespace
 }
 
 // Deploy creates a workload on the first managed cluster.
-func (d DiscoveredApp) Deploy(ctx types.Context) error {
+func (d DiscoveredApp) Deploy(ctx types.TestContext) error {
 	log := ctx.Logger()
 	appNamespace := ctx.AppNamespace()
 
@@ -31,7 +31,7 @@ func (d DiscoveredApp) Deploy(ctx types.Context) error {
 	cluster := ctx.Env().C1
 
 	// Create namespace on the first DR cluster (c1)
-	err := util.CreateNamespace(cluster, appNamespace, log)
+	err := util.CreateNamespace(ctx, cluster, appNamespace)
 	if err != nil {
 		return err
 	}
@@ -72,7 +72,7 @@ func (d DiscoveredApp) Deploy(ctx types.Context) error {
 }
 
 // Undeploy deletes the workload from the managed clusters.
-func (d DiscoveredApp) Undeploy(ctx types.Context) error {
+func (d DiscoveredApp) Undeploy(ctx types.TestContext) error {
 	log := ctx.Logger()
 	appNamespace := ctx.AppNamespace()
 
@@ -89,11 +89,11 @@ func (d DiscoveredApp) Undeploy(ctx types.Context) error {
 	}
 
 	// delete namespace on both clusters
-	if err := util.DeleteNamespace(ctx.Env().C1, appNamespace, log); err != nil {
+	if err := util.DeleteNamespace(ctx, ctx.Env().C1, appNamespace); err != nil {
 		return err
 	}
 
-	if err := util.DeleteNamespace(ctx.Env().C2, appNamespace, log); err != nil {
+	if err := util.DeleteNamespace(ctx, ctx.Env().C2, appNamespace); err != nil {
 		return err
 	}
 
diff --git a/e2e/deployers/retry.go b/e2e/deployers/retry.go
index 3598d2c953..530fa129e6 100644
--- a/e2e/deployers/retry.go
+++ b/e2e/deployers/retry.go
@@ -13,12 +13,16 @@ import (
 	"github.com/ramendr/ramen/e2e/util"
 )
 
-func waitSubscriptionPhase(ctx types.Context, namespace, name string, phase subscriptionv1.SubscriptionPhase) error {
+func waitSubscriptionPhase(
+	ctx types.TestContext,
+	namespace, name string,
+	phase subscriptionv1.SubscriptionPhase,
+) error {
 	log := ctx.Logger()
 	startTime := time.Now()
 
 	for {
-		sub, err := getSubscription(ctx.Env().Hub, namespace, name)
+		sub, err := getSubscription(ctx, namespace, name)
 		if err != nil {
 			return err
 		}
@@ -35,11 +39,13 @@ func waitSubscriptionPhase(ctx types.Context, namespace, name string, phase subs
 				name, phase, ctx.Env().Hub.Name)
 		}
 
-		time.Sleep(util.RetryInterval)
+		if err := util.Sleep(ctx.Context(), util.RetryInterval); err != nil {
+			return err
+		}
 	}
 }
 
-func WaitWorkloadHealth(ctx types.Context, cluster types.Cluster, namespace string) error {
+func WaitWorkloadHealth(ctx types.TestContext, cluster types.Cluster, namespace string) error {
 	log := ctx.Logger()
 	w := ctx.Workload()
 	startTime := time.Now()
@@ -57,6 +63,8 @@ func WaitWorkloadHealth(ctx types.Context, cluster types.Cluster, namespace stri
 				w.GetName(), util.Timeout, cluster.Name)
 		}
 
-		time.Sleep(util.RetryInterval)
+		if err := util.Sleep(ctx.Context(), util.RetryInterval); err != nil {
+			return err
+		}
 	}
 }
diff --git a/e2e/deployers/subscr.go b/e2e/deployers/subscr.go
index 3c8eff1651..a8dbe92643 100644
--- a/e2e/deployers/subscr.go
+++ b/e2e/deployers/subscr.go
@@ -17,13 +17,13 @@ func (s Subscription) GetName() string {
 	return "subscr"
 }
 
-func (s Subscription) GetNamespace(_ types.Context) string {
+func (s Subscription) GetNamespace(_ types.TestContext) string {
 	// No special namespaces.
 	return ""
 }
 
 // Deploy creates a Subscription on the hub cluster, creating the workload on one of the managed clusters.
-func (s Subscription) Deploy(ctx types.Context) error {
+func (s Subscription) Deploy(ctx types.TestContext) error {
 	// Generate a Placement for the Workload
 	// Use the global Channel
 	// Generate a Binding for the namespace (does this need clusters?)
@@ -42,7 +42,7 @@ func (s Subscription) Deploy(ctx types.Context) error {
 		ctx.AppNamespace(), ctx.Workload().GetAppName(), cluster.Name)
 
 	// create subscription namespace
-	err := util.CreateNamespace(ctx.Env().Hub, managementNamespace, log)
+	err := util.CreateNamespace(ctx, ctx.Env().Hub, managementNamespace)
 	if err != nil {
 		return err
 	}
@@ -73,13 +73,13 @@ func (s Subscription) Deploy(ctx types.Context) error {
 }
 
 // Undeploy deletes a subscription from the hub cluster, deleting the workload from the managed clusters.
-func (s Subscription) Undeploy(ctx types.Context) error {
+func (s Subscription) Undeploy(ctx types.TestContext) error {
 	name := ctx.Name()
 	log := ctx.Logger()
 	config := ctx.Config()
 	managementNamespace := ctx.ManagementNamespace()
 
-	clusterName, err := util.GetCurrentCluster(ctx.Env().Hub, managementNamespace, name)
+	clusterName, err := util.GetCurrentCluster(ctx, managementNamespace, name)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return err
@@ -107,7 +107,7 @@ func (s Subscription) Undeploy(ctx types.Context) error {
 		return err
 	}
 
-	err = util.DeleteNamespace(ctx.Env().Hub, managementNamespace, log)
+	err = util.DeleteNamespace(ctx, ctx.Env().Hub, managementNamespace)
 	if err != nil {
 		return err
 	}
diff --git a/e2e/dr_test.go b/e2e/dr_test.go
index bd53bfd06d..2513c7eed9 100644
--- a/e2e/dr_test.go
+++ b/e2e/dr_test.go
@@ -22,16 +22,16 @@ func TestDR(dt *testing.T) {
 		t.Fatal("No tests found in the configuration file")
 	}
 
-	if err := validate.TestConfig(Ctx.env, Ctx.config, Ctx.log); err != nil {
+	if err := validate.TestConfig(&Ctx); err != nil {
 		t.Fatal(err.Error())
 	}
 
-	if err := util.EnsureChannel(Ctx.env.Hub, Ctx.config, Ctx.log); err != nil {
+	if err := util.EnsureChannel(&Ctx); err != nil {
 		t.Fatalf("Failed to ensure channel: %s", err)
 	}
 
 	t.Cleanup(func() {
-		if err := util.EnsureChannelDeleted(Ctx.env.Hub, Ctx.config, Ctx.log); err != nil {
+		if err := util.EnsureChannelDeleted(&Ctx); err != nil {
 			t.Fatalf("Failed to ensure channel deleted: %s", err)
 		}
 	})
@@ -54,7 +54,7 @@ func TestDR(dt *testing.T) {
 			panic(err)
 		}
 
-		ctx := test.NewContext(workload, deployer, Ctx.env, Ctx.config, Ctx.log)
+		ctx := test.NewContext(&Ctx, workload, deployer)
 		t.Run(ctx.Name(), func(dt *testing.T) {
 			t := test.WithLog(dt, ctx.Logger())
 			t.Parallel()
diff --git a/e2e/dractions/actions.go b/e2e/dractions/actions.go
index 24712f2c88..1f108b136c 100644
--- a/e2e/dractions/actions.go
+++ b/e2e/dractions/actions.go
@@ -5,7 +5,6 @@ package dractions
 
 import (
 	ramen "github.com/ramendr/ramen/api/v1alpha1"
-	"go.uber.org/zap"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/client-go/util/retry"
 
@@ -25,7 +24,7 @@ const (
 // Determine KubeObjectProtection requirements if Imperative (?)
 // Create DRPC, in desired namespace
 // nolint:funlen
-func EnableProtection(ctx types.Context) error {
+func EnableProtection(ctx types.TestContext) error {
 	d := ctx.Deployer()
 	if d.IsDiscovered() {
 		return EnableProtectionDiscoveredApps(ctx)
@@ -43,7 +42,7 @@ func EnableProtection(ctx types.Context) error {
 	placementName := name
 	drpcName := name
 
-	clusterName, err := util.GetCurrentCluster(ctx.Env().Hub, managementNamespace, placementName)
+	clusterName, err := util.GetCurrentCluster(ctx, managementNamespace, placementName)
 	if err != nil {
 		return err
 	}
@@ -51,7 +50,7 @@ func EnableProtection(ctx types.Context) error {
 	log.Infof("Protecting workload \"%s/%s\" in cluster %q", appNamespace, appname, clusterName)
 
 	err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
-		placement, err := util.GetPlacement(ctx.Env().Hub, managementNamespace, placementName)
+		placement, err := util.GetPlacement(ctx, managementNamespace, placementName)
 		if err != nil {
 			return err
 		}
@@ -62,7 +61,7 @@ func EnableProtection(ctx types.Context) error {
 
 		placement.Annotations[OcmSchedulingDisable] = "true"
 
-		if err := updatePlacement(ctx.Env().Hub, placement); err != nil {
+		if err := updatePlacement(ctx, placement); err != nil {
 			return err
 		}
 
@@ -81,7 +80,7 @@ func EnableProtection(ctx types.Context) error {
 		return err
 	}
 
-	if err := createNamespaces(ctx, appNamespace, log); err != nil {
+	if err := createNamespaces(ctx, appNamespace); err != nil {
 		return err
 	}
 
@@ -97,7 +96,7 @@ func EnableProtection(ctx types.Context) error {
 
 // remove DRPC
 // update placement annotation
-func DisableProtection(ctx types.Context) error {
+func DisableProtection(ctx types.TestContext) error {
 	d := ctx.Deployer()
 	if d.IsDiscovered() {
 		return DisableProtectionDiscoveredApps(ctx)
@@ -109,7 +108,7 @@ func DisableProtection(ctx types.Context) error {
 	placementName := name
 	log := ctx.Logger()
 
-	clusterName, err := util.GetCurrentCluster(ctx.Env().Hub, managementNamespace, placementName)
+	clusterName, err := util.GetCurrentCluster(ctx, managementNamespace, placementName)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return err
@@ -138,18 +137,18 @@ func DisableProtection(ctx types.Context) error {
 	return nil
 }
 
-func Failover(ctx types.Context) error {
+func Failover(ctx types.TestContext) error {
 	managementNamespace := ctx.ManagementNamespace()
 	log := ctx.Logger()
 	name := ctx.Name()
 	config := ctx.Config()
 
-	currentCluster, err := util.GetCurrentCluster(ctx.Env().Hub, managementNamespace, name)
+	currentCluster, err := util.GetCurrentCluster(ctx, managementNamespace, name)
 	if err != nil {
 		return err
 	}
 
-	targetCluster, err := getTargetCluster(ctx.Env().Hub, config.DRPolicy, currentCluster)
+	targetCluster, err := getTargetCluster(ctx, ctx.Env().Hub, config.DRPolicy, currentCluster)
 	if err != nil {
 		return err
 	}
@@ -171,18 +170,18 @@ func Failover(ctx types.Context) error {
 // Check Placement
 // Relocate to Primary in DRPolicy as the PrimaryCluster
 // Update DRPC
-func Relocate(ctx types.Context) error {
+func Relocate(ctx types.TestContext) error {
 	managementNamespace := ctx.ManagementNamespace()
 	log := ctx.Logger()
 	config := ctx.Config()
 	name := ctx.Name()
 
-	currentCluster, err := util.GetCurrentCluster(ctx.Env().Hub, managementNamespace, name)
+	currentCluster, err := util.GetCurrentCluster(ctx, managementNamespace, name)
 	if err != nil {
 		return err
 	}
 
-	targetCluster, err := getTargetCluster(ctx.Env().Hub, config.DRPolicy, currentCluster)
+	targetCluster, err := getTargetCluster(ctx, ctx.Env().Hub, config.DRPolicy, currentCluster)
 	if err != nil {
 		return err
 	}
@@ -200,7 +199,8 @@ func Relocate(ctx types.Context) error {
 	return nil
 }
 
-func failoverRelocate(ctx types.Context,
+func failoverRelocate(
+	ctx types.TestContext,
 	action ramen.DRAction,
 	state ramen.DRState,
 	currentCluster string,
@@ -226,13 +226,12 @@ func failoverRelocate(ctx types.Context,
 }
 
 func waitAndUpdateDRPC(
-	ctx types.Context,
+	ctx types.TestContext,
 	namespace, drpcName string,
 	action ramen.DRAction,
 	targetCluster string,
 ) error {
 	log := ctx.Logger()
-	hub := ctx.Env().Hub
 
 	// here we expect drpc should be ready before action
 	if err := waitDRPCReady(ctx, namespace, drpcName); err != nil {
@@ -240,7 +239,7 @@ func waitAndUpdateDRPC(
 	}
 
 	return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
-		drpc, err := getDRPC(hub, namespace, drpcName)
+		drpc, err := getDRPC(ctx, namespace, drpcName)
 		if err != nil {
 			return err
 		}
@@ -252,7 +251,7 @@ func waitAndUpdateDRPC(
 			drpc.Spec.PreferredCluster = targetCluster
 		}
 
-		if err := updateDRPC(hub, drpc); err != nil {
+		if err := updateDRPC(ctx, drpc); err != nil {
 			return err
 		}
 
@@ -266,9 +265,9 @@ func waitAndUpdateDRPC(
 // createNamespaces creates namespaces and annotations for managed app protection on
 // both DR clusters for Volsync based replication if the distribution is Kubernetes.
 // Returns an error if namespace creation or annotation fails.
-func createNamespaces(ctx types.Context, appNamespace string, log *zap.SugaredLogger) error {
+func createNamespaces(ctx types.TestContext, appNamespace string) error {
 	if ctx.Config().Distro == config.DistroK8s {
-		return util.CreateNamespaceAndAddAnnotation(ctx.Env(), appNamespace, log)
+		return util.CreateNamespaceAndAddAnnotation(ctx, appNamespace)
 	}
 
 	return nil
diff --git a/e2e/dractions/crud.go b/e2e/dractions/crud.go
index 114c7f74e4..455c00ac32 100644
--- a/e2e/dractions/crud.go
+++ b/e2e/dractions/crud.go
@@ -4,8 +4,6 @@
 package dractions
 
 import (
-	"context"
-
 	ramen "github.com/ramendr/ramen/api/v1alpha1"
 	v1 "k8s.io/api/core/v1"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
@@ -18,15 +16,19 @@ import (
 	"github.com/ramendr/ramen/e2e/types"
 )
 
-func updatePlacement(cluster types.Cluster, placement *clusterv1beta1.Placement) error {
-	return cluster.Client.Update(context.Background(), placement)
+func updatePlacement(ctx types.TestContext, placement *clusterv1beta1.Placement) error {
+	hub := ctx.Env().Hub
+
+	return hub.Client.Update(ctx.Context(), placement)
 }
 
-func getDRPC(cluster types.Cluster, namespace, name string) (*ramen.DRPlacementControl, error) {
+func getDRPC(ctx types.TestContext, namespace, name string) (*ramen.DRPlacementControl, error) {
+	hub := ctx.Env().Hub
+
 	drpc := &ramen.DRPlacementControl{}
 	key := k8stypes.NamespacedName{Namespace: namespace, Name: name}
 
-	err := cluster.Client.Get(context.Background(), key, drpc)
+	err := hub.Client.Get(ctx.Context(), key, drpc)
 	if err != nil {
 		return nil, err
 	}
@@ -34,11 +36,11 @@ func getDRPC(cluster types.Cluster, namespace, name string) (*ramen.DRPlacementC
 	return drpc, nil
 }
 
-func createDRPC(ctx types.Context, drpc *ramen.DRPlacementControl) error {
+func createDRPC(ctx types.TestContext, drpc *ramen.DRPlacementControl) error {
 	log := ctx.Logger()
 	hub := ctx.Env().Hub
 
-	err := hub.Client.Create(context.Background(), drpc)
+	err := hub.Client.Create(ctx.Context(), drpc)
 	if err != nil {
 		if !k8serrors.IsAlreadyExists(err) {
 			return err
@@ -58,18 +60,20 @@ func createDRPC(ctx types.Context, drpc *ramen.DRPlacementControl) error {
 	return nil
 }
 
-func updateDRPC(cluster types.Cluster, drpc *ramen.DRPlacementControl) error {
-	return cluster.Client.Update(context.Background(), drpc)
+func updateDRPC(ctx types.TestContext, drpc *ramen.DRPlacementControl) error {
+	hub := ctx.Env().Hub
+
+	return hub.Client.Update(ctx.Context(), drpc)
 }
 
-func deleteDRPC(ctx types.Context, namespace, name string) error {
+func deleteDRPC(ctx types.TestContext, namespace, name string) error {
 	log := ctx.Logger()
 	hub := ctx.Env().Hub
 
 	objDrpc := &ramen.DRPlacementControl{}
 	key := k8stypes.NamespacedName{Namespace: namespace, Name: name}
 
-	err := hub.Client.Get(context.Background(), key, objDrpc)
+	err := hub.Client.Get(ctx.Context(), key, objDrpc)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return err
@@ -80,7 +84,7 @@ func deleteDRPC(ctx types.Context, namespace, name string) error {
 		return nil
 	}
 
-	if err := hub.Client.Delete(context.Background(), objDrpc); err != nil {
+	if err := hub.Client.Delete(ctx.Context(), objDrpc); err != nil {
 		return err
 	}
 
@@ -119,7 +123,7 @@ func generateDRPC(name, namespace, clusterName, drPolicyName, placementName, app
 	return drpc
 }
 
-func createPlacementManagedByRamen(ctx types.Context, name, namespace string) error {
+func createPlacementManagedByRamen(ctx types.TestContext, name, namespace string) error {
 	log := ctx.Logger()
 	labels := make(map[string]string)
 	labels[deployers.AppLabelKey] = name
@@ -141,7 +145,7 @@ func createPlacementManagedByRamen(ctx types.Context, name, namespace string) er
 		},
 	}
 
-	err := ctx.Env().Hub.Client.Create(context.Background(), placement)
+	err := ctx.Env().Hub.Client.Create(ctx.Context(), placement)
 	if err != nil {
 		if !k8serrors.IsAlreadyExists(err) {
 			return err
diff --git a/e2e/dractions/disapp.go b/e2e/dractions/disapp.go
index 43faf31413..4be89663f4 100644
--- a/e2e/dractions/disapp.go
+++ b/e2e/dractions/disapp.go
@@ -7,7 +7,6 @@ import (
 	"fmt"
 
 	ramen "github.com/ramendr/ramen/api/v1alpha1"
-	"go.uber.org/zap"
 
 	"github.com/ramendr/ramen/e2e/config"
 	"github.com/ramendr/ramen/e2e/deployers"
@@ -17,7 +16,7 @@ import (
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 )
 
-func EnableProtectionDiscoveredApps(ctx types.Context) error {
+func EnableProtectionDiscoveredApps(ctx types.TestContext) error {
 	w := ctx.Workload()
 	name := ctx.Name()
 	log := ctx.Logger()
@@ -53,7 +52,7 @@ func EnableProtectionDiscoveredApps(ctx types.Context) error {
 		return err
 	}
 
-	if err := createNamespacesDiscoveredApps(ctx, appNamespace, log); err != nil {
+	if err := createNamespacesDiscoveredApps(ctx, appNamespace); err != nil {
 		return err
 	}
 
@@ -70,7 +69,7 @@ func EnableProtectionDiscoveredApps(ctx types.Context) error {
 
 // remove DRPC
 // update placement annotation
-func DisableProtectionDiscoveredApps(ctx types.Context) error {
+func DisableProtectionDiscoveredApps(ctx types.TestContext) error {
 	name := ctx.Name()
 	log := ctx.Logger()
 	config := ctx.Config()
@@ -80,7 +79,7 @@ func DisableProtectionDiscoveredApps(ctx types.Context) error {
 	placementName := name
 	drpcName := name
 
-	clusterName, err := util.GetCurrentCluster(ctx.Env().Hub, managementNamespace, placementName)
+	clusterName, err := util.GetCurrentCluster(ctx, managementNamespace, placementName)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return err
@@ -118,7 +117,7 @@ func DisableProtectionDiscoveredApps(ctx types.Context) error {
 
 // nolint:funlen,cyclop
 func failoverRelocateDiscoveredApps(
-	ctx types.Context,
+	ctx types.TestContext,
 	action ramen.DRAction,
 	state ramen.DRState,
 	currentClusterName string,
@@ -170,12 +169,12 @@ func failoverRelocateDiscoveredApps(
 // For Kubernetes, creates namespaces and adds annotation on both DR clusters for volsync based replication.
 // For OpenShift, creates namespace only on the target DR cluster (c2) before failover.
 // Returns an error if the distribution is unknown, and if namespace creation or annotation fails.
-func createNamespacesDiscoveredApps(ctx types.Context, namespace string, log *zap.SugaredLogger) error {
+func createNamespacesDiscoveredApps(ctx types.TestContext, namespace string) error {
 	switch ctx.Config().Distro {
 	case config.DistroK8s:
-		return util.CreateNamespaceAndAddAnnotation(ctx.Env(), namespace, log)
+		return util.CreateNamespaceAndAddAnnotation(ctx, namespace)
 	case config.DistroOcp:
-		return util.CreateNamespace(ctx.Env().C2, namespace, log)
+		return util.CreateNamespace(ctx, ctx.Env().C2, namespace)
 	default:
 		return fmt.Errorf("unknown distro: %s", ctx.Config().Distro)
 	}
diff --git a/e2e/dractions/retry.go b/e2e/dractions/retry.go
index 8144f356de..eab6a89341 100644
--- a/e2e/dractions/retry.go
+++ b/e2e/dractions/retry.go
@@ -16,7 +16,7 @@ import (
 	"github.com/ramendr/ramen/e2e/util"
 )
 
-func waitDRPCReady(ctx types.Context, namespace string, drpcName string) error {
+func waitDRPCReady(ctx types.TestContext, namespace string, drpcName string) error {
 	log := ctx.Logger()
 	hub := ctx.Env().Hub
 	startTime := time.Now()
@@ -24,7 +24,7 @@ func waitDRPCReady(ctx types.Context, namespace string, drpcName string) error {
 	log.Debugf("Waiting until drpc \"%s/%s\" is ready in cluster %q", namespace, drpcName, hub.Name)
 
 	for {
-		drpc, err := getDRPC(hub, namespace, drpcName)
+		drpc, err := getDRPC(ctx, namespace, drpcName)
 		if err != nil {
 			return err
 		}
@@ -52,7 +52,9 @@ func waitDRPCReady(ctx types.Context, namespace string, drpcName string) error {
 				hub.Name, available, peerReady, progressionCompleted, drpc.Status.LastGroupSyncTime)
 		}
 
-		time.Sleep(util.RetryInterval)
+		if err := util.Sleep(ctx.Context(), util.RetryInterval); err != nil {
+			return err
+		}
 	}
 }
 
@@ -62,7 +64,7 @@ func conditionMet(conditions []metav1.Condition, conditionType string) bool {
 	return condition != nil && condition.Status == "True"
 }
 
-func waitDRPCPhase(ctx types.Context, namespace, name string, phase ramen.DRState) error {
+func waitDRPCPhase(ctx types.TestContext, namespace, name string, phase ramen.DRState) error {
 	log := ctx.Logger()
 	hub := ctx.Env().Hub
 	startTime := time.Now()
@@ -70,7 +72,7 @@ func waitDRPCPhase(ctx types.Context, namespace, name string, phase ramen.DRStat
 	log.Debugf("Waiting until drpc \"%s/%s\" reach phase %q in cluster %q", namespace, name, phase, hub.Name)
 
 	for {
-		drpc, err := getDRPC(hub, namespace, name)
+		drpc, err := getDRPC(ctx, namespace, name)
 		if err != nil {
 			return err
 		}
@@ -86,12 +88,18 @@ func waitDRPCPhase(ctx types.Context, namespace, name string, phase ramen.DRStat
 			return fmt.Errorf("drpc %q status is not %q yet before timeout in cluster %q, fail", name, phase, hub.Name)
 		}
 
-		time.Sleep(util.RetryInterval)
+		if err := util.Sleep(ctx.Context(), util.RetryInterval); err != nil {
+			return err
+		}
 	}
 }
 
-func getTargetCluster(cluster types.Cluster, drPolicyName, currentCluster string) (string, error) {
-	drpolicy, err := util.GetDRPolicy(cluster, drPolicyName)
+func getTargetCluster(
+	ctx types.TestContext,
+	cluster types.Cluster,
+	drPolicyName, currentCluster string,
+) (string, error) {
+	drpolicy, err := util.GetDRPolicy(ctx, cluster, drPolicyName)
 	if err != nil {
 		return "", err
 	}
@@ -106,7 +114,7 @@ func getTargetCluster(cluster types.Cluster, drPolicyName, currentCluster string
 	return targetCluster, nil
 }
 
-func waitDRPCDeleted(ctx types.Context, namespace string, name string) error {
+func waitDRPCDeleted(ctx types.TestContext, namespace string, name string) error {
 	log := ctx.Logger()
 	hub := ctx.Env().Hub
 	startTime := time.Now()
@@ -114,7 +122,7 @@ func waitDRPCDeleted(ctx types.Context, namespace string, name string) error {
 	log.Debugf("Waiting until drpc \"%s/%s\" is deleted in cluster %q", namespace, name, hub.Name)
 
 	for {
-		_, err := getDRPC(hub, namespace, name)
+		_, err := getDRPC(ctx, namespace, name)
 		if err != nil {
 			if k8serrors.IsNotFound(err) {
 				log.Debugf("drpc \"%s/%s\" is deleted in cluster %q", namespace, name, hub.Name)
@@ -129,13 +137,15 @@ func waitDRPCDeleted(ctx types.Context, namespace string, name string) error {
 			return fmt.Errorf("drpc %q is not deleted yet before timeout in cluster %q, fail", name, hub.Name)
 		}
 
-		time.Sleep(util.RetryInterval)
+		if err := util.Sleep(ctx.Context(), util.RetryInterval); err != nil {
+			return err
+		}
 	}
 }
 
 // nolint:unparam
 func waitDRPCProgression(
-	ctx types.Context,
+	ctx types.TestContext,
 	namespace, name string,
 	progression ramen.ProgressionStatus,
 ) error {
@@ -147,7 +157,7 @@ func waitDRPCProgression(
 		namespace, name, progression, hub.Name)
 
 	for {
-		drpc, err := getDRPC(hub, namespace, name)
+		drpc, err := getDRPC(ctx, namespace, name)
 		if err != nil {
 			return err
 		}
@@ -164,6 +174,8 @@ func waitDRPCProgression(
 				name, progression, util.Timeout, hub.Name)
 		}
 
-		time.Sleep(util.RetryInterval)
+		if err := util.Sleep(ctx.Context(), util.RetryInterval); err != nil {
+			return err
+		}
 	}
 }
diff --git a/e2e/env/env.go b/e2e/env/env.go
index a861df91f5..e926731473 100644
--- a/e2e/env/env.go
+++ b/e2e/env/env.go
@@ -87,6 +87,7 @@ func setupClient(kubeconfigPath string) (client.Client, error) {
 }
 
 func setupCluster(
+	ctx context.Context,
 	cluster *types.Cluster,
 	key string,
 	clusterConfig types.ClusterConfig,
@@ -107,7 +108,7 @@ func setupCluster(
 		log.Infof("Using %q cluster name: %q", key, cluster.Name)
 	case "c1", "c2":
 		// For c1 and c2 clusters, get the cluster name from ClusterClaim
-		cluster.Name, err = getClusterClaimName(cluster)
+		cluster.Name, err = getClusterClaimName(ctx, cluster)
 		if err != nil {
 			return fmt.Errorf("failed to get ClusterClaim name for the %q managed cluster: %w", key, err)
 		}
@@ -123,11 +124,11 @@ func setupCluster(
 
 // getClusterClaimName gets the cluster name using clusterclaims/name resource.
 // Returns an error if the resource is not found or retrieval fails.
-func getClusterClaimName(cluster *types.Cluster) (string, error) {
+func getClusterClaimName(ctx context.Context, cluster *types.Cluster) (string, error) {
 	clusterClaim := &ocmv1a1.ClusterClaim{}
 	key := client.ObjectKey{Name: "name"}
 
-	err := cluster.Client.Get(context.TODO(), key, clusterClaim)
+	err := cluster.Client.Get(ctx, key, clusterClaim)
 	if err != nil {
 		return "", fmt.Errorf("failed to get ClusterClaim name: %w", err)
 	}
@@ -135,18 +136,18 @@ func getClusterClaimName(cluster *types.Cluster) (string, error) {
 	return clusterClaim.Spec.Value, nil
 }
 
-func New(config *types.Config, log *zap.SugaredLogger) (*types.Env, error) {
+func New(ctx context.Context, config *types.Config, log *zap.SugaredLogger) (*types.Env, error) {
 	env := &types.Env{}
 
-	if err := setupCluster(&env.Hub, "hub", config.Clusters["hub"], log); err != nil {
+	if err := setupCluster(ctx, &env.Hub, "hub", config.Clusters["hub"], log); err != nil {
 		return nil, fmt.Errorf("failed to create env: %w", err)
 	}
 
-	if err := setupCluster(&env.C1, "c1", config.Clusters["c1"], log); err != nil {
+	if err := setupCluster(ctx, &env.C1, "c1", config.Clusters["c1"], log); err != nil {
 		return nil, fmt.Errorf("failed to create env: %w", err)
 	}
 
-	if err := setupCluster(&env.C2, "c2", config.Clusters["c2"], log); err != nil {
+	if err := setupCluster(ctx, &env.C2, "c2", config.Clusters["c2"], log); err != nil {
 		return nil, fmt.Errorf("failed to create env: %w", err)
 	}
 
diff --git a/e2e/main_test.go b/e2e/main_test.go
index f7e8d6f324..07a44d82bc 100644
--- a/e2e/main_test.go
+++ b/e2e/main_test.go
@@ -4,6 +4,7 @@
 package e2e_test
 
 import (
+	"context"
 	"flag"
 	"os"
 	"testing"
@@ -19,13 +20,30 @@ import (
 	"github.com/ramendr/ramen/e2e/workloads"
 )
 
+// Context implements types.Context for sharing the log, env, and config with all code.
 type Context struct {
 	log    *zap.SugaredLogger
 	env    *types.Env
 	config *types.Config
 }
 
-// The global test context
+func (c *Context) Logger() *zap.SugaredLogger {
+	return c.log
+}
+
+func (c *Context) Config() *types.Config {
+	return c.config
+}
+
+func (c *Context) Env() *types.Env {
+	return c.env
+}
+
+func (c *Context) Context() context.Context {
+	return context.Background()
+}
+
+// The global test context.
 var Ctx Context
 
 func TestMain(m *testing.M) {
@@ -60,7 +78,7 @@ func TestMain(m *testing.M) {
 		log.Fatalf("Failed to read config: %s", err)
 	}
 
-	Ctx.env, err = env.New(Ctx.config, Ctx.log)
+	Ctx.env, err = env.New(Ctx.Context(), Ctx.config, Ctx.log)
 	if err != nil {
 		log.Fatalf("Failed to create testing context: %s", err)
 	}
diff --git a/e2e/test/context.go b/e2e/test/context.go
index 493d88ff96..62170004d4 100644
--- a/e2e/test/context.go
+++ b/e2e/test/context.go
@@ -5,6 +5,7 @@
 package test
 
 import (
+	"context"
 	"strings"
 	"testing"
 
@@ -18,30 +19,26 @@ import (
 const appNamespacePrefix = "e2e-"
 
 type Context struct {
+	ctx      types.Context
 	workload types.Workload
 	deployer types.Deployer
 	name     string
-	env      *types.Env
-	config   *types.Config
 	logger   *zap.SugaredLogger
 }
 
 func NewContext(
+	ctx types.Context,
 	w types.Workload,
 	d types.Deployer,
-	env *types.Env,
-	config *types.Config,
-	log *zap.SugaredLogger,
 ) Context {
 	name := strings.ToLower(d.GetName() + "-" + w.GetName() + "-" + w.GetAppName())
 
 	return Context{
+		ctx:      ctx,
 		workload: w,
 		deployer: d,
 		name:     name,
-		env:      env,
-		config:   config,
-		logger:   log.Named(name),
+		logger:   ctx.Logger().Named(name),
 	}
 }
 
@@ -74,11 +71,15 @@ func (c *Context) Logger() *zap.SugaredLogger {
 }
 
 func (c *Context) Env() *types.Env {
-	return c.env
+	return c.ctx.Env()
 }
 
 func (c *Context) Config() *types.Config {
-	return c.config
+	return c.ctx.Config()
+}
+
+func (c *Context) Context() context.Context {
+	return c.ctx.Context()
 }
 
 // Validated return an error if the combination of deployer and workload is not supported.
diff --git a/e2e/types/types.go b/e2e/types/types.go
index 47f8ce0839..c321e248be 100644
--- a/e2e/types/types.go
+++ b/e2e/types/types.go
@@ -4,6 +4,8 @@
 package types
 
 import (
+	"context"
+
 	"go.uber.org/zap"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
@@ -76,11 +78,11 @@ type Env struct {
 
 // Deployer interface has methods to deploy a workload to a cluster
 type Deployer interface {
-	Deploy(Context) error
-	Undeploy(Context) error
+	Deploy(TestContext) error
+	Undeploy(TestContext) error
 	GetName() string
 	// GetNamespace return the namespace for the ramen resources, or empty string if not using a special namespace.
-	GetNamespace(Context) string
+	GetNamespace(TestContext) string
 	// Return true for OCM discovered application, false for OCM managed applications.
 	IsDiscovered() bool
 }
@@ -93,13 +95,21 @@ type Workload interface {
 	GetAppName() string
 	GetPath() string
 	GetBranch() string
-
-	Health(ctx Context, cluster Cluster, namespace string) error
+	Health(ctx TestContext, cluster Cluster, namespace string) error
 }
 
-// Context combines workload, deployer and logger used in the content of one test.
-// The context name is used for logging and resource names.
+// Context keeps the Logger, Env, Config, and Context shared by all code in the e2e package.
 type Context interface {
+	Logger() *zap.SugaredLogger
+	Env() *Env
+	Config() *Config
+	Context() context.Context
+}
+
+// TestContext is a more specific Context for a single test; a combination of Deployer, Workload, and namespaces. A test
+// has a unique Name and Logger, and it shares the global Env, Config and Context.
+type TestContext interface {
+	Context
 	Deployer() Deployer
 	Workload() Workload
 	Name() string
@@ -110,8 +120,4 @@ type Context interface {
 
 	// Namespace for application resources on the managed clusters.
 	AppNamespace() string
-
-	Logger() *zap.SugaredLogger
-	Env() *Env
-	Config() *Config
 }
diff --git a/e2e/util/channel.go b/e2e/util/channel.go
index ff60e778c1..dbf21f860a 100644
--- a/e2e/util/channel.go
+++ b/e2e/util/channel.go
@@ -4,9 +4,6 @@
 package util
 
 import (
-	"context"
-
-	"go.uber.org/zap"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	channelv1 "open-cluster-management.io/multicloud-operators-channel/pkg/apis/apps/v1"
@@ -14,25 +11,29 @@ import (
 	"github.com/ramendr/ramen/e2e/types"
 )
 
-func EnsureChannel(hub types.Cluster, config *types.Config, log *zap.SugaredLogger) error {
+func EnsureChannel(ctx types.Context) error {
 	// create channel namespace
-	err := CreateNamespace(hub, config.Channel.Namespace, log)
+	err := CreateNamespace(ctx, ctx.Env().Hub, ctx.Config().Channel.Namespace)
 	if err != nil {
 		return err
 	}
 
-	return createChannel(hub, config, log)
+	return createChannel(ctx)
 }
 
-func EnsureChannelDeleted(hub types.Cluster, config *types.Config, log *zap.SugaredLogger) error {
-	if err := deleteChannel(hub, config, log); err != nil {
+func EnsureChannelDeleted(ctx types.Context) error {
+	if err := deleteChannel(ctx); err != nil {
 		return err
 	}
 
-	return DeleteNamespace(hub, config.Channel.Namespace, log)
+	return DeleteNamespace(ctx, ctx.Env().Hub, ctx.Config().Channel.Namespace)
 }
 
-func createChannel(hub types.Cluster, config *types.Config, log *zap.SugaredLogger) error {
+func createChannel(ctx types.Context) error {
+	hub := ctx.Env().Hub
+	config := ctx.Config()
+	log := ctx.Logger()
+
 	objChannel := &channelv1.Channel{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      config.Channel.Name,
@@ -44,7 +45,7 @@ func createChannel(hub types.Cluster, config *types.Config, log *zap.SugaredLogg
 		},
 	}
 
-	err := hub.Client.Create(context.Background(), objChannel)
+	err := hub.Client.Create(ctx.Context(), objChannel)
 	if err != nil {
 		if !k8serrors.IsAlreadyExists(err) {
 			return err
@@ -60,7 +61,11 @@ func createChannel(hub types.Cluster, config *types.Config, log *zap.SugaredLogg
 	return nil
 }
 
-func deleteChannel(hub types.Cluster, config *types.Config, log *zap.SugaredLogger) error {
+func deleteChannel(ctx types.Context) error {
+	hub := ctx.Env().Hub
+	config := ctx.Config()
+	log := ctx.Logger()
+
 	channel := &channelv1.Channel{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      config.Channel.Name,
@@ -68,7 +73,7 @@ func deleteChannel(hub types.Cluster, config *types.Config, log *zap.SugaredLogg
 		},
 	}
 
-	err := hub.Client.Delete(context.Background(), channel)
+	err := hub.Client.Delete(ctx.Context(), channel)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return err
diff --git a/e2e/util/drpolicy.go b/e2e/util/drpolicy.go
index 68532c39e9..afb8fca578 100644
--- a/e2e/util/drpolicy.go
+++ b/e2e/util/drpolicy.go
@@ -4,8 +4,6 @@
 package util
 
 import (
-	"context"
-
 	ramen "github.com/ramendr/ramen/api/v1alpha1"
 	k8stypes "k8s.io/apimachinery/pkg/types"
 
@@ -13,11 +11,11 @@ import (
 )
 
 // nolint:unparam
-func GetDRPolicy(cluster types.Cluster, name string) (*ramen.DRPolicy, error) {
+func GetDRPolicy(ctx types.Context, cluster types.Cluster, name string) (*ramen.DRPolicy, error) {
 	drpolicy := &ramen.DRPolicy{}
 	key := k8stypes.NamespacedName{Name: name}
 
-	err := cluster.Client.Get(context.Background(), key, drpolicy)
+	err := cluster.Client.Get(ctx.Context(), key, drpolicy)
 	if err != nil {
 		return nil, err
 	}
diff --git a/e2e/util/namespace.go b/e2e/util/namespace.go
index 4a6c96716f..d7e409128e 100644
--- a/e2e/util/namespace.go
+++ b/e2e/util/namespace.go
@@ -4,11 +4,9 @@
 package util
 
 import (
-	"context"
 	"fmt"
 	"time"
 
-	"go.uber.org/zap"
 	corev1 "k8s.io/api/core/v1"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -22,14 +20,16 @@ import (
 // More info: https://volsync.readthedocs.io/en/stable/usage/permissionmodel.html#controlling-mover-permissions
 const volsyncPrivilegedMovers = "volsync.backube/privileged-movers"
 
-func CreateNamespace(cluster types.Cluster, namespace string, log *zap.SugaredLogger) error {
+func CreateNamespace(ctx types.Context, cluster types.Cluster, namespace string) error {
+	log := ctx.Logger()
+
 	ns := &corev1.Namespace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: namespace,
 		},
 	}
 
-	err := cluster.Client.Create(context.Background(), ns)
+	err := cluster.Client.Create(ctx.Context(), ns)
 	if err != nil {
 		if !k8serrors.IsAlreadyExists(err) {
 			return err
@@ -43,14 +43,16 @@ func CreateNamespace(cluster types.Cluster, namespace string, log *zap.SugaredLo
 	return nil
 }
 
-func DeleteNamespace(cluster types.Cluster, namespace string, log *zap.SugaredLogger) error {
+func DeleteNamespace(ctx types.Context, cluster types.Cluster, namespace string) error {
+	log := ctx.Logger()
+
 	ns := &corev1.Namespace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: namespace,
 		},
 	}
 
-	err := cluster.Client.Delete(context.Background(), ns)
+	err := cluster.Client.Delete(ctx.Context(), ns)
 	if err != nil {
 		if !k8serrors.IsNotFound(err) {
 			return err
@@ -67,7 +69,7 @@ func DeleteNamespace(cluster types.Cluster, namespace string, log *zap.SugaredLo
 	key := k8stypes.NamespacedName{Name: namespace}
 
 	for {
-		if err := cluster.Client.Get(context.Background(), key, ns); err != nil {
+		if err := cluster.Client.Get(ctx.Context(), key, ns); err != nil {
 			if !k8serrors.IsNotFound(err) {
 				return err
 			}
@@ -81,36 +83,42 @@ func DeleteNamespace(cluster types.Cluster, namespace string, log *zap.SugaredLo
 			return fmt.Errorf("timeout deleting namespace %q in cluster %q", namespace, cluster.Name)
 		}
 
-		time.Sleep(time.Second)
+		if err := Sleep(ctx.Context(), time.Second); err != nil {
+			return err
+		}
 	}
 }
 
 // Problem: currently we must manually add an annotation to application’s namespace to make volsync work.
 // See this link https://volsync.readthedocs.io/en/stable/usage/permissionmodel.html#controlling-mover-permissions
 // Workaround: create ns in both drclusters and add annotation
-func CreateNamespaceAndAddAnnotation(env *types.Env, namespace string, log *zap.SugaredLogger) error {
-	if err := CreateNamespace(env.C1, namespace, log); err != nil {
+func CreateNamespaceAndAddAnnotation(ctx types.Context, namespace string) error {
+	env := ctx.Env()
+
+	if err := CreateNamespace(ctx, env.C1, namespace); err != nil {
 		return err
 	}
 
-	if err := addNamespaceAnnotationForVolSync(env.C1, namespace, log); err != nil {
+	if err := addNamespaceAnnotationForVolSync(ctx, env.C1, namespace); err != nil {
 		return err
 	}
 
-	if err := CreateNamespace(env.C2, namespace, log); err != nil {
+	if err := CreateNamespace(ctx, env.C2, namespace); err != nil {
 		return err
 	}
 
-	return addNamespaceAnnotationForVolSync(env.C2, namespace, log)
+	return addNamespaceAnnotationForVolSync(ctx, env.C2, namespace)
 }
 
-func addNamespaceAnnotationForVolSync(cluster types.Cluster, namespace string, log *zap.SugaredLogger) error {
+func addNamespaceAnnotationForVolSync(ctx types.Context, cluster types.Cluster, namespace string) error {
+	log := ctx.Logger()
+
 	key := k8stypes.NamespacedName{Name: namespace}
 
 	return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
 		objNs := &corev1.Namespace{}
 
-		if err := cluster.Client.Get(context.Background(), key, objNs); err != nil {
+		if err := cluster.Client.Get(ctx.Context(), key, objNs); err != nil {
 			return err
 		}
 
@@ -122,7 +130,7 @@ func addNamespaceAnnotationForVolSync(cluster types.Cluster, namespace string, l
 		annotations[volsyncPrivilegedMovers] = "true"
 		objNs.SetAnnotations(annotations)
 
-		if err := cluster.Client.Update(context.Background(), objNs); err != nil {
+		if err := cluster.Client.Update(ctx.Context(), objNs); err != nil {
 			return err
 		}
 
diff --git a/e2e/util/placement.go b/e2e/util/placement.go
index fd0042e2af..36eb14be64 100644
--- a/e2e/util/placement.go
+++ b/e2e/util/placement.go
@@ -4,7 +4,6 @@
 package util
 
 import (
-	"context"
 	"fmt"
 	"time"
 
@@ -19,8 +18,8 @@ import (
 // based on the PlacementDecision for the given Placement resource.
 // Assumes the PlacementDecision exists with a Decision.
 // Not applicable for discovered apps before enabling protection, as no Placement exists.
-func GetCurrentCluster(cluster types.Cluster, namespace string, placementName string) (string, error) {
-	placementDecision, err := waitPlacementDecision(cluster, namespace, placementName)
+func GetCurrentCluster(ctx types.Context, namespace string, placementName string) (string, error) {
+	placementDecision, err := waitPlacementDecision(ctx, namespace, placementName)
 	if err != nil {
 		return "", err
 	}
@@ -28,11 +27,13 @@ func GetCurrentCluster(cluster types.Cluster, namespace string, placementName st
 	return placementDecision.Status.Decisions[0].ClusterName, nil
 }
 
-func GetPlacement(cluster types.Cluster, namespace, name string) (*v1beta1.Placement, error) {
+func GetPlacement(ctx types.Context, namespace, name string) (*v1beta1.Placement, error) {
+	hub := ctx.Env().Hub
+
 	placement := &v1beta1.Placement{}
 	key := k8stypes.NamespacedName{Namespace: namespace, Name: name}
 
-	err := cluster.Client.Get(context.Background(), key, placement)
+	err := hub.Client.Get(ctx.Context(), key, placement)
 	if err != nil {
 		return nil, err
 	}
@@ -41,17 +42,18 @@ func GetPlacement(cluster types.Cluster, namespace, name string) (*v1beta1.Place
 }
 
 // waitPlacementDecision waits until we have a placement decision and returns the placement decision object.
-func waitPlacementDecision(cluster types.Cluster, namespace string, placementName string,
+func waitPlacementDecision(ctx types.Context, namespace string, placementName string,
 ) (*v1beta1.PlacementDecision, error) {
+	cluster := ctx.Env().Hub
 	startTime := time.Now()
 
 	for {
-		placement, err := GetPlacement(cluster, namespace, placementName)
+		placement, err := GetPlacement(ctx, namespace, placementName)
 		if err != nil {
 			return nil, err
 		}
 
-		placementDecision, err := getPlacementDecisionFromPlacement(cluster, placement)
+		placementDecision, err := getPlacementDecisionFromPlacement(ctx, placement)
 		if err != nil {
 			return nil, err
 		}
@@ -64,12 +66,16 @@ func waitPlacementDecision(cluster types.Cluster, namespace string, placementNam
 			return nil, fmt.Errorf("timeout waiting for placement decisions for %q in cluster %q", placementName, cluster.Name)
 		}
 
-		time.Sleep(RetryInterval)
+		if err := Sleep(ctx.Context(), RetryInterval); err != nil {
+			return nil, err
+		}
 	}
 }
 
-func getPlacementDecisionFromPlacement(cluster types.Cluster, placement *v1beta1.Placement,
+func getPlacementDecisionFromPlacement(ctx types.Context, placement *v1beta1.Placement,
 ) (*v1beta1.PlacementDecision, error) {
+	hub := ctx.Env().Hub
+
 	matchLabels := map[string]string{
 		v1beta1.PlacementLabel: placement.GetName(),
 	}
@@ -80,9 +86,9 @@ func getPlacementDecisionFromPlacement(cluster types.Cluster, placement *v1beta1
 	}
 
 	plDecisions := &v1beta1.PlacementDecisionList{}
-	if err := cluster.Client.List(context.Background(), plDecisions, listOptions...); err != nil {
+	if err := hub.Client.List(ctx.Context(), plDecisions, listOptions...); err != nil {
 		return nil, fmt.Errorf("failed to list PlacementDecisions (placement: %s) in cluster %q",
-			placement.GetNamespace()+"/"+placement.GetName(), cluster.Name)
+			placement.GetNamespace()+"/"+placement.GetName(), hub.Name)
 	}
 
 	if len(plDecisions.Items) == 0 {
@@ -91,7 +97,7 @@ func getPlacementDecisionFromPlacement(cluster types.Cluster, placement *v1beta1
 
 	if len(plDecisions.Items) > 1 {
 		return nil, fmt.Errorf("multiple PlacementDecisions found for Placement (count: %d, placement: %s) in cluster %q",
-			len(plDecisions.Items), placement.GetNamespace()+"/"+placement.GetName(), cluster.Name)
+			len(plDecisions.Items), placement.GetNamespace()+"/"+placement.GetName(), hub.Name)
 	}
 
 	plDecision := plDecisions.Items[0]
@@ -102,7 +108,7 @@ func getPlacementDecisionFromPlacement(cluster types.Cluster, placement *v1beta1
 			" (count: %d, Placement: %s, PlacementDecision: %s) in cluster %q",
 			len(plDecision.Status.Decisions),
 			placement.GetNamespace()+"/"+placement.GetName(),
-			plDecision.GetName()+"/"+plDecision.GetNamespace(), cluster.Name)
+			plDecision.GetName()+"/"+plDecision.GetNamespace(), hub.Name)
 	}
 
 	return &plDecision, nil
diff --git a/e2e/util/timeout.go b/e2e/util/timeout.go
index 38edfb55bf..8a929519af 100644
--- a/e2e/util/timeout.go
+++ b/e2e/util/timeout.go
@@ -3,9 +3,23 @@
 
 package util
 
-import "time"
+import (
+	"context"
+	"time"
+)
 
 const (
 	Timeout       = 600 * time.Second
 	RetryInterval = 5 * time.Second
 )
+
+// Sleep pauses the current goroutine for at least the duration d. If the context was canceled or its deadline has
+// exceeded it will return early with a context.Canceled or a context.DeadlineExceeded error.
+func Sleep(ctx context.Context, d time.Duration) error {
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	case <-time.After(d):
+		return nil
+	}
+}
diff --git a/e2e/validate/clusterset.go b/e2e/validate/clusterset.go
index 7a64ff8eef..da7cff158c 100644
--- a/e2e/validate/clusterset.go
+++ b/e2e/validate/clusterset.go
@@ -4,7 +4,6 @@
 package validate
 
 import (
-	"context"
 	"fmt"
 
 	ocmv1 "open-cluster-management.io/api/cluster/v1"
@@ -14,22 +13,26 @@ import (
 	"github.com/ramendr/ramen/e2e/types"
 )
 
-func getClusterSet(hub types.Cluster, clusterSetName string) (*ocmv1b2.ManagedClusterSet, error) {
+func getClusterSet(ctx types.Context, clusterSetName string) (*ocmv1b2.ManagedClusterSet, error) {
+	hub := ctx.Env().Hub
+
 	clusterSet := &ocmv1b2.ManagedClusterSet{}
 	key := client.ObjectKey{Name: clusterSetName}
 
-	if err := hub.Client.Get(context.TODO(), key, clusterSet); err != nil {
+	if err := hub.Client.Get(ctx.Context(), key, clusterSet); err != nil {
 		return nil, fmt.Errorf("failed to get ClusterSet %q: %w", clusterSetName, err)
 	}
 
 	return clusterSet, nil
 }
 
-func getManagedClustersFromClusterSet(hub types.Cluster, clusterSetName string) ([]string, error) {
+func getManagedClustersFromClusterSet(ctx types.Context, clusterSetName string) ([]string, error) {
+	hub := ctx.Env().Hub
+
 	clusterList := &ocmv1.ManagedClusterList{}
 	labelSelector := client.MatchingLabels{"cluster.open-cluster-management.io/clusterset": clusterSetName}
 
-	if err := hub.Client.List(context.TODO(), clusterList, labelSelector); err != nil {
+	if err := hub.Client.List(ctx.Context(), clusterList, labelSelector); err != nil {
 		return nil, fmt.Errorf("failed to list ManagedClusters for ClusterSet %q: %w", clusterSetName, err)
 	}
 
diff --git a/e2e/validate/storageclass.go b/e2e/validate/storageclass.go
index d98eea6d5e..48935a7ffb 100644
--- a/e2e/validate/storageclass.go
+++ b/e2e/validate/storageclass.go
@@ -1,7 +1,6 @@
 package validate
 
 import (
-	"context"
 	"fmt"
 
 	storagev1 "k8s.io/api/storage/v1"
@@ -13,12 +12,15 @@ import (
 
 // validateStorageClasses verifies that each storageClass referenced in PVC specs
 // exists in both managed clusters.
-func validateStorageClasses(env *types.Env, config *types.Config) error {
+func validateStorageClasses(ctx types.Context) error {
+	env := ctx.Env()
+	config := ctx.Config()
+
 	clusters := []*types.Cluster{&env.C1, &env.C2}
 
 	for _, spec := range config.PVCSpecs {
 		for _, cluster := range clusters {
-			_, err := getStorageClass(cluster, spec.StorageClassName)
+			_, err := getStorageClass(ctx, cluster, spec.StorageClassName)
 			if err != nil {
 				return fmt.Errorf("failed to validate pvcSpec %q storage class: %w", spec.Name, err)
 			}
@@ -29,12 +31,12 @@ func validateStorageClasses(env *types.Env, config *types.Config) error {
 }
 
 // getStorageClass retrieves a specific StorageClass from the given cluster
-func getStorageClass(cluster *types.Cluster, scName string) (*storagev1.StorageClass, error) {
+func getStorageClass(ctx types.Context, cluster *types.Cluster, scName string) (*storagev1.StorageClass, error) {
 	var sc storagev1.StorageClass
 
 	key := client.ObjectKey{Name: scName}
 
-	err := cluster.Client.Get(context.Background(), key, &sc)
+	err := cluster.Client.Get(ctx.Context(), key, &sc)
 	if err != nil {
 		if k8serrors.IsNotFound(err) {
 			return nil, fmt.Errorf("storageClass %q not found in cluster %q", scName, cluster.Name)
diff --git a/e2e/validate/validate.go b/e2e/validate/validate.go
index 311bc49518..50b7d5bfd6 100644
--- a/e2e/validate/validate.go
+++ b/e2e/validate/validate.go
@@ -4,12 +4,10 @@
 package validate
 
 import (
-	"context"
 	"fmt"
 	"slices"
 	"strings"
 
-	"go.uber.org/zap"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -22,11 +20,14 @@ import (
 	"github.com/ramendr/ramen/e2e/util"
 )
 
-func RamenHubOperator(cluster types.Cluster, config *types.Config, log *zap.SugaredLogger) error {
+func RamenHubOperator(ctx types.Context, cluster types.Cluster) error {
+	config := ctx.Config()
+	log := ctx.Logger()
+
 	labelSelector := "app=ramen-hub"
 	podIdentifier := "ramen-hub-operator"
 
-	pod, err := FindPod(cluster, config.Namespaces.RamenHubNamespace, labelSelector, podIdentifier)
+	pod, err := FindPod(ctx, cluster, config.Namespaces.RamenHubNamespace, labelSelector, podIdentifier)
 	if err != nil {
 		return err
 	}
@@ -41,11 +42,14 @@ func RamenHubOperator(cluster types.Cluster, config *types.Config, log *zap.Suga
 	return nil
 }
 
-func RamenDRClusterOperator(cluster types.Cluster, config *types.Config, log *zap.SugaredLogger) error {
+func RamenDRClusterOperator(ctx types.Context, cluster types.Cluster) error {
+	config := ctx.Config()
+	log := ctx.Logger()
+
 	labelSelector := "app=ramen-dr-cluster"
 	podIdentifier := "ramen-dr-cluster-operator"
 
-	pod, err := FindPod(cluster, config.Namespaces.RamenDRClusterNamespace, labelSelector, podIdentifier)
+	pod, err := FindPod(ctx, cluster, config.Namespaces.RamenDRClusterNamespace, labelSelector, podIdentifier)
 	if err != nil {
 		return err
 	}
@@ -61,7 +65,7 @@ func RamenDRClusterOperator(cluster types.Cluster, config *types.Config, log *za
 }
 
 // FindPod returns the first pod matching the label selector including the pod identifier in the namespace.
-func FindPod(cluster types.Cluster, namespace, labelSelector, podIdentifier string) (
+func FindPod(ctx types.Context, cluster types.Cluster, namespace, labelSelector, podIdentifier string) (
 	*v1.Pod, error,
 ) {
 	ls, err := labels.Parse(labelSelector)
@@ -77,7 +81,7 @@ func FindPod(cluster types.Cluster, namespace, labelSelector, podIdentifier stri
 		},
 	}
 
-	err = cluster.Client.List(context.Background(), pods, listOptions...)
+	err = cluster.Client.List(ctx.Context(), pods, listOptions...)
 	if err != nil {
 		return nil, fmt.Errorf("failed to list pods in namespace %s in cluster %q: %v", namespace, cluster.Name, err)
 	}
@@ -96,20 +100,20 @@ func FindPod(cluster types.Cluster, namespace, labelSelector, podIdentifier stri
 // TestConfig is a wrapper function which performs validation checks on
 // the test environment configurations with the DR resources on the clusters.
 // Returns an error if any validation fails.
-func TestConfig(env *types.Env, config *types.Config, log *zap.SugaredLogger) error {
-	if err := detectDistro(env, config, log); err != nil {
+func TestConfig(ctx types.Context) error {
+	if err := detectDistro(ctx); err != nil {
 		return fmt.Errorf("failed to validate test config: %w", err)
 	}
 
-	if err := validateStorageClasses(env, config); err != nil {
+	if err := validateStorageClasses(ctx); err != nil {
 		return fmt.Errorf("failed to validate test config: %w", err)
 	}
 
-	if err := clustersInDRPolicy(env, config, log); err != nil {
+	if err := clustersInDRPolicy(ctx); err != nil {
 		return fmt.Errorf("failed to validate test config: %w", err)
 	}
 
-	if err := clustersInClusterSet(env, config, log); err != nil {
+	if err := clustersInClusterSet(ctx); err != nil {
 		return fmt.Errorf("failed to validate test config: %w", err)
 	}
 
@@ -120,8 +124,12 @@ func TestConfig(env *types.Env, config *types.Config, log *zap.SugaredLogger) er
 // drpolicy. Returns an error if cluster names are not the same as drpolicy
 // drclusters. The reason for a failure may be wrong cluster name or wrong
 // drpolicy.
-func clustersInDRPolicy(env *types.Env, config *types.Config, log *zap.SugaredLogger) error {
-	drpolicy, err := util.GetDRPolicy(env.Hub, config.DRPolicy)
+func clustersInDRPolicy(ctx types.Context) error {
+	env := ctx.Env()
+	config := ctx.Config()
+	log := ctx.Logger()
+
+	drpolicy, err := util.GetDRPolicy(ctx, env.Hub, config.DRPolicy)
 	if err != nil {
 		return fmt.Errorf("failed to get DRPolicy %q: %w", config.DRPolicy, err)
 	}
@@ -142,12 +150,16 @@ func clustersInDRPolicy(env *types.Env, config *types.Config, log *zap.SugaredLo
 // clustersInClusterSet checks if configured clusters exists in configured clusterset.
 // Returns an error if provided cluster names are not the same as managedclusters in clusterset.
 // The reason for a failure may be wrong cluster name or wrong clusterset.
-func clustersInClusterSet(env *types.Env, config *types.Config, log *zap.SugaredLogger) error {
-	if _, err := getClusterSet(env.Hub, config.ClusterSet); err != nil {
+func clustersInClusterSet(ctx types.Context) error {
+	env := ctx.Env()
+	config := ctx.Config()
+	log := ctx.Logger()
+
+	if _, err := getClusterSet(ctx, config.ClusterSet); err != nil {
 		return err
 	}
 
-	clusterNames, err := getManagedClustersFromClusterSet(env.Hub, config.ClusterSet)
+	clusterNames, err := getManagedClustersFromClusterSet(ctx, config.ClusterSet)
 	if err != nil {
 		return err
 	}
@@ -168,7 +180,11 @@ func clustersInClusterSet(env *types.Env, config *types.Config, log *zap.Sugared
 // detectDistro detects the cluster distribution. If distro is set in config, it uses the user
 // provided distro. Otherwise, it determines the distro, sets config.Distro and config.Namespaces.
 // Returns an error if distro detection fails or if clusters have inconsistent distributions.
-func detectDistro(env *types.Env, cfg *types.Config, log *zap.SugaredLogger) error {
+func detectDistro(ctx types.Context) error {
+	env := ctx.Env()
+	cfg := ctx.Config()
+	log := ctx.Logger()
+
 	if cfg.Distro != "" {
 		log.Infof("Using user selected kubernetes distribution: %q", cfg.Distro)
 
@@ -179,7 +195,7 @@ func detectDistro(env *types.Env, cfg *types.Config, log *zap.SugaredLogger) err
 
 	clusters := []*types.Cluster{&env.Hub, &env.C1, &env.C2}
 	for _, cluster := range clusters {
-		distro, err := probeDistro(cluster)
+		distro, err := probeDistro(ctx, cluster)
 		if err != nil {
 			return fmt.Errorf("failed to determine distro for cluster %q: %w", cluster.Name, err)
 		}
@@ -211,7 +227,7 @@ func detectDistro(env *types.Env, cfg *types.Config, log *zap.SugaredLogger) err
 
 // probeDistro determines the distribution of the given Kubernetes cluster.
 // Returns the detected distribution name ("ocp" or "k8s") or an error if detection fails.
-func probeDistro(cluster *types.Cluster) (string, error) {
+func probeDistro(ctx types.Context, cluster *types.Cluster) (string, error) {
 	configList := &unstructured.Unstructured{}
 	configList.SetGroupVersionKind(schema.GroupVersionKind{
 		Group:   "config.openshift.io",
@@ -219,7 +235,7 @@ func probeDistro(cluster *types.Cluster) (string, error) {
 		Kind:    "ClusterVersion",
 	})
 
-	err := cluster.Client.List(context.TODO(), configList)
+	err := cluster.Client.List(ctx.Context(), configList)
 	if err == nil {
 		// found OpenShift only resource type, it is OpenShift
 		return config.DistroOcp, nil
diff --git a/e2e/validation_test.go b/e2e/validation_test.go
index 5cf9ae15b8..88befcda91 100644
--- a/e2e/validation_test.go
+++ b/e2e/validation_test.go
@@ -11,38 +11,40 @@ import (
 )
 
 func TestValidation(dt *testing.T) {
-	t := test.WithLog(dt, Ctx.log)
+	t := test.WithLog(dt, Ctx.Logger())
 	t.Parallel()
 
-	if err := validate.TestConfig(Ctx.env, Ctx.config, Ctx.log); err != nil {
+	if err := validate.TestConfig(&Ctx); err != nil {
 		t.Fatal(err.Error())
 	}
 
+	env := Ctx.Env()
+
 	t.Run("hub", func(dt *testing.T) {
-		t := test.WithLog(dt, Ctx.log)
+		t := test.WithLog(dt, Ctx.Logger())
 		t.Parallel()
 
-		err := validate.RamenHubOperator(Ctx.env.Hub, Ctx.config, Ctx.log)
+		err := validate.RamenHubOperator(&Ctx, env.Hub)
 		if err != nil {
-			t.Fatalf("Failed to validate hub cluster %q: %s", Ctx.env.Hub.Name, err)
+			t.Fatalf("Failed to validate hub cluster %q: %s", env.Hub.Name, err)
 		}
 	})
 	t.Run("c1", func(dt *testing.T) {
-		t := test.WithLog(dt, Ctx.log)
+		t := test.WithLog(dt, Ctx.Logger())
 		t.Parallel()
 
-		err := validate.RamenDRClusterOperator(Ctx.env.C1, Ctx.config, Ctx.log)
+		err := validate.RamenDRClusterOperator(&Ctx, env.C1)
 		if err != nil {
-			t.Fatalf("Failed to validate dr cluster %q: %s", Ctx.env.C1.Name, err)
+			t.Fatalf("Failed to validate dr cluster %q: %s", env.C1.Name, err)
 		}
 	})
 	t.Run("c2", func(dt *testing.T) {
-		t := test.WithLog(dt, Ctx.log)
+		t := test.WithLog(dt, Ctx.Logger())
 		t.Parallel()
 
-		err := validate.RamenDRClusterOperator(Ctx.env.C2, Ctx.config, Ctx.log)
+		err := validate.RamenDRClusterOperator(&Ctx, env.C2)
 		if err != nil {
-			t.Fatalf("Failed to validate dr cluster %q: %s", Ctx.env.C2.Name, err)
+			t.Fatalf("Failed to validate dr cluster %q: %s", env.C2.Name, err)
 		}
 	})
 }
diff --git a/e2e/workloads/deploy.go b/e2e/workloads/deploy.go
index c8c4648814..c4266d8dfa 100644
--- a/e2e/workloads/deploy.go
+++ b/e2e/workloads/deploy.go
@@ -4,7 +4,6 @@
 package workloads
 
 import (
-	"context"
 	"fmt"
 
 	appsv1 "k8s.io/api/apps/v1"
@@ -84,10 +83,10 @@ func (w Deployment) GetResources() error {
 }
 
 // Check the workload health deployed in a cluster namespace
-func (w Deployment) Health(ctx types.Context, cluster types.Cluster, namespace string) error {
+func (w Deployment) Health(ctx types.TestContext, cluster types.Cluster, namespace string) error {
 	log := ctx.Logger()
 
-	deploy, err := getDeployment(cluster, namespace, w.GetAppName())
+	deploy, err := getDeployment(ctx, cluster, namespace, w.GetAppName())
 	if err != nil {
 		return err
 	}
@@ -101,11 +100,11 @@ func (w Deployment) Health(ctx types.Context, cluster types.Cluster, namespace s
 	return nil
 }
 
-func getDeployment(cluster types.Cluster, namespace, name string) (*appsv1.Deployment, error) {
+func getDeployment(ctx types.TestContext, cluster types.Cluster, namespace, name string) (*appsv1.Deployment, error) {
 	deploy := &appsv1.Deployment{}
 	key := k8stypes.NamespacedName{Name: name, Namespace: namespace}
 
-	err := cluster.Client.Get(context.Background(), key, deploy)
+	err := cluster.Client.Get(ctx.Context(), key, deploy)
 	if err != nil {
 		return nil, err
 	}


Reviews:
user: login: parikshithb
body: Looks great! added minor comments.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T11:52:37Z
user: login: parikshithb
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T13:46:40Z
user: login: ELENAGER
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T15:12:51Z
user: login: rakeshgm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T17:38:37Z

Review comments:
diff_hunk: @@ -96,9 +96,17 @@ type Workload interface {
 	Health(ctx TestContext, cluster Cluster, namespace string) error
 }
 
-// TestContext combines workload, deployer and logger used in the content of one test.
+// TestContext keeps the Logger, Env, and Config shared by all code in the e2e package.
path: e2e/types/types.go
user: login: parikshithb
body: should be Context instead of TestContext
created_at: 2025-04-23T11:32:42Z
author_association: MEMBER
diff_hunk: @@ -3,9 +3,23 @@
 
 package util
 
-import "time"
+import (
+	"context"
+	"time"
+)
 
 const (
 	Timeout       = 600 * time.Second
 	RetryInterval = 5 * time.Second
 )
+
+// Sleep pauses the current goroutine for at least the duration d, unless the context is done before the requested
+// durration. It returns an error explaining why the context was done.
path: e2e/util/timeout.go
user: login: parikshithb
body: typo "duration"
created_at: 2025-04-23T11:49:35Z
author_association: MEMBER

PR comments:
user: login: nirs
created_at: 2025-04-23T13:38:10Z
body: @parikshithb Comments addressed and the relevant comments refined.

https://github.com/RamenDR/ramen/compare/26f20c6fdd613262ca7f18ead9068eba00d00104..961f33a2b6b414b9858cad9c7042470a9f987f15
author_association: MEMBER
user: login: nirs
created_at: 2025-04-24T12:23:19Z
body: Thanks for reviewing! the random failed unit test is not related.
author_association: MEMBER