Repository: kudobuilder/kuttl (go)

Issue:
Allow specifying Kubeconfig contexts in the TestSteps
<!-- Please only use this template for submitting enhancement requests.
Implementing your enhancement will follow the KEP process: https://github.com/kudobuilder/kuttl/blob/main/keps/0001-kep-process.md
-->

**What would you like to be added**:
Specification of Kubeconfig context in `TestSteps`.

**Why is this needed**:
Currently, Kuttl loads the default context from the Kubeconfig specified in the `TestStep`. In case of shared environments, there maybe multiple clusters and multiple contexts in a single Kubeconfig. In such scenarios, it would be very helpful if we could specify the context in the `TestStep`.

The desired behaviour is similar to `kubectl --context <xyz> ...`.

Pull request description:
feat: add context switching in TestStep
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://github.com/kudobuilder/kuttl/blob/main/CONTRIBUTING.md
2. Make sure you have added and ran the tests before submitting your PR
3. If the PR is unfinished, start it as a Draft PR: https://github.blog/2019-02-14-introducing-draft-pull-requests/
-->

**What this PR does / why we need it**:


<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #558 

**Testing**
Files

```zsh
.
├── e2e
│   └── case
│       ├── 00-teststep.yaml
│       └── ns.yaml
├── kubeconfig
└── kuttl-test.yaml
```

```yaml
# 00-teststep.yaml

apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- ns.yaml
kubeconfig: ~/.kube/config
context: kind-data-plane-aws
```

```yaml
# ns.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: test-ns
```

```yaml
# kuttl-test.yaml

apiVersion: kuttl.dev/v1beta1
kind: TestSuite
testDirs:
- ./e2e
timeout: 2400
startControlPlane: false
```

Contexts
```zsh
k config get-contexts
CURRENT   NAME                    CLUSTER                 AUTHINFO                NAMESPACE
          cluster                 cluster                 user
          kind-data-plane-aws     kind-data-plane-aws     kind-data-plane-aws
          kind-data-plane-aws-2   kind-data-plane-aws-2   kind-data-plane-aws-2
*         kind-data-plane-gcp     kind-data-plane-gcp     kind-data-plane-gcp
```

Namespace Created in the specified (non-default) context
```zsh
$ k --context kind-data-plane-aws get ns -w
NAME                 STATUS   AGE
default              Active   20h
kube-node-lease      Active   20h
kube-public          Active   20h
kube-system          Active   20h
local-path-storage   Active   20h
pd-hcomp-1           Active   20h
kuttl-test-pleasant-duckling   Active   0s
test-ns                        Active   0s
test-ns                        Terminating   0s
kuttl-test-pleasant-duckling   Terminating   0s
test-ns                        Terminating   5s
kuttl-test-pleasant-duckling   Terminating   5s
test-ns                        Terminating   5s
kuttl-test-pleasant-duckling   Terminating   5s
```

Patch:
diff --git a/pkg/k8s/main.go b/pkg/k8s/main.go
index 4c8a38c..a2923d6 100644
--- a/pkg/k8s/main.go
+++ b/pkg/k8s/main.go
@@ -2,6 +2,7 @@ package k8s
 
 import (
 	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
 )
 
@@ -19,3 +20,14 @@ func GetConfig() (*rest.Config, error) {
 
 	return cfg, nil
 }
+
+func BuildConfigWithContext(kubeconfig, context string) (*rest.Config, error) {
+	if context == "" {
+		// Use default context
+		return clientcmd.BuildConfigFromFlags("", kubeconfig)
+	}
+
+	return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
+		&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig},
+		&clientcmd.ConfigOverrides{CurrentContext: context}).ClientConfig()
+}


Test patch:
diff --git a/docs/testing/reference.md b/docs/testing/reference.md
index 0498039..f3d6e3f 100644
--- a/docs/testing/reference.md
+++ b/docs/testing/reference.md
@@ -64,17 +64,18 @@ kubeconfig: foo.kubeconfig
 
 Supported settings:
 
-Field    |          Type             | Description
----------|---------------------------|---------------------------------------------------------------------
-apply    | list of files             | A list of files to apply as part of this step. Specified path is relative to that in which the step occurs.
-assert   | list of files             | A list of files to assert as part of this step. See documentation for [asserts and errors](asserts-errors.md) for more information. Specified path is relative to that in which the step occurs.
-error    | list of files             | A list of files to error as part of this step. See documentation for [asserts and errors](asserts-errors.md) for more information. Specified path is relative to that in which the step occurs.
-delete   | list of object references | A list of objects to delete, if they do not already exist, at the beginning of the test step. The test harness will wait for the objects to be successfully deleted before applying the objects in the step.
-index    | int                       | Override the test step's index.
+Field    | Type                          | Description
+---------|-------------------------------|---------------------------------------------------------------------
+apply    | list of files                 | A list of files to apply as part of this step. Specified path is relative to that in which the step occurs.
+assert   | list of files                 | A list of files to assert as part of this step. See documentation for [asserts and errors](asserts-errors.md) for more information. Specified path is relative to that in which the step occurs.
+error    | list of files                 | A list of files to error as part of this step. See documentation for [asserts and errors](asserts-errors.md) for more information. Specified path is relative to that in which the step occurs.
+delete   | list of object references     | A list of objects to delete, if they do not already exist, at the beginning of the test step. The test harness will wait for the objects to be successfully deleted before applying the objects in the step.
+index    | int                           | Override the test step's index.
 commands | list of [Commands](#commands) | Commands to run prior at the beginning of the test step.
-kubeconfig    | string                       | The Kubeconfig file to use to run the included steps(s).
-kubeconfigLoading    | string                | Specifies the mode for loading Kubeconfig and making a cluster connection: `Eager` (when loading the test definition) or `Lazy` (right before executing the step, makes it possible to generate the Kubeconfig in a preceding step). Defaults to `Eager`.
-unitTest    | bool                       | Indicates if the step is a unit test, safe to run without a real Kubernetes cluster.
+kubeconfig    | string                        | The Kubeconfig file to use to run the included steps(s).
+kubeconfigLoading    | string                        | Specifies the mode for loading Kubeconfig and making a cluster connection: `Eager` (when loading the test definition) or `Lazy` (right before executing the step, makes it possible to generate the Kubeconfig in a preceding step). Defaults to `Eager`.
+context     | string                        | Specifies the context to use from the Kubeconfig.
+unitTest    | bool                          | Indicates if the step is a unit test, safe to run without a real Kubernetes cluster.
 
 
 Object Reference:
diff --git a/pkg/apis/testharness/v1beta1/test_types.go b/pkg/apis/testharness/v1beta1/test_types.go
index 45aef4d..28d3c51 100644
--- a/pkg/apis/testharness/v1beta1/test_types.go
+++ b/pkg/apis/testharness/v1beta1/test_types.go
@@ -133,6 +133,9 @@ type TestStep struct {
 	// +kubebuilder:default=Eager
 	// +kubebuilder:validation:Enum=Eager;Lazy
 	KubeconfigLoading string `json:"kubeconfigLoading,omitempty"`
+
+	// Specifies the context to use from the Kubeconfig.
+	Context string `json:"context,omitempty"`
 }
 
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
diff --git a/pkg/test/case.go b/pkg/test/case.go
index d8a2b45..9a2c895 100644
--- a/pkg/test/case.go
+++ b/pkg/test/case.go
@@ -23,9 +23,8 @@ import (
 	"k8s.io/client-go/discovery"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	"k8s.io/client-go/tools/clientcmd"
-
 	"github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1"
+	"github.com/kudobuilder/kuttl/pkg/k8s"
 	"github.com/kudobuilder/kuttl/pkg/report"
 	testutils "github.com/kudobuilder/kuttl/pkg/test/utils"
 )
@@ -338,7 +337,7 @@ func (t *Case) Run(test *testing.T, ts *report.Testsuite) {
 			continue
 		}
 
-		cl, err = newClient(testStep.Kubeconfig)(false)
+		cl, err = newClient(testStep.Kubeconfig, testStep.Context)(false)
 		if err != nil {
 			setupReport.Failure = report.NewFailure(err.Error(), nil)
 			ts.AddTestcase(setupReport)
@@ -363,11 +362,11 @@ func (t *Case) Run(test *testing.T, ts *report.Testsuite) {
 		tc := report.NewCase("step " + testStep.String())
 		testStep.Client = t.Client
 		if testStep.Kubeconfig != "" {
-			testStep.Client = newClient(testStep.Kubeconfig)
+			testStep.Client = newClient(testStep.Kubeconfig, testStep.Context)
 		}
 		testStep.DiscoveryClient = t.DiscoveryClient
 		if testStep.Kubeconfig != "" {
-			testStep.DiscoveryClient = newDiscoveryClient(testStep.Kubeconfig)
+			testStep.DiscoveryClient = newDiscoveryClient(testStep.Kubeconfig, testStep.Context)
 		}
 		testStep.Logger = t.Logger.WithPrefix(testStep.String())
 		tc.Assertions += len(testStep.Asserts)
@@ -531,9 +530,9 @@ func (t *Case) LoadTestSteps() error {
 	return nil
 }
 
-func newClient(kubeconfig string) func(bool) (client.Client, error) {
+func newClient(kubeconfig, context string) func(bool) (client.Client, error) {
 	return func(bool) (client.Client, error) {
-		config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
+		config, err := k8s.BuildConfigWithContext(kubeconfig, context)
 		if err != nil {
 			return nil, err
 		}
@@ -544,9 +543,9 @@ func newClient(kubeconfig string) func(bool) (client.Client, error) {
 	}
 }
 
-func newDiscoveryClient(kubeconfig string) func() (discovery.DiscoveryInterface, error) {
+func newDiscoveryClient(kubeconfig, context string) func() (discovery.DiscoveryInterface, error) {
 	return func() (discovery.DiscoveryInterface, error) {
-		config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
+		config, err := k8s.BuildConfigWithContext(kubeconfig, context)
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/test/step.go b/pkg/test/step.go
index 522df6c..2e722f3 100644
--- a/pkg/test/step.go
+++ b/pkg/test/step.go
@@ -53,8 +53,10 @@ type Step struct {
 
 	Kubeconfig        string
 	KubeconfigLoading string
-	Client            func(forceNew bool) (client.Client, error)
-	DiscoveryClient   func() (discovery.DiscoveryInterface, error)
+	Context           string
+
+	Client          func(forceNew bool) (client.Client, error)
+	DiscoveryClient func() (discovery.DiscoveryInterface, error)
 
 	Logger testutils.Logger
 }
@@ -556,6 +558,7 @@ func (s *Step) LoadYAML(file string) error {
 				exKubeconfig := env.Expand(s.Step.Kubeconfig)
 				s.Kubeconfig = cleanPath(exKubeconfig, s.Dir)
 			}
+			s.Context = s.Step.Context
 
 			switch s.Step.KubeconfigLoading {
 			case "", harness.KubeconfigLoadingEager, harness.KubeconfigLoadingLazy: