Repository: vmware-tanzu/octant
Pull request #1721 (2020-12-10 18:42:14)

Custom clientconfig
**What this PR does / why we need it**: 
- Separates functionality of client interface and context switching
- Allows one to use client interfaces not backed by kubeconfigs

**Which issue(s) this PR fixes**
- Fixes no issues

**Special notes for your reviewer**:

**Release note**:
```
release-note
```

cc @dimitrybesson @zoetian

mstergianis: The "Verify generated code" check appears to be failing because of [removing the Loader interface](https://github.com/vmware-tanzu/octant/pull/1721/files?file-filters%5B%5D=.go&hide-deleted-files=true#diff-2416f88d535bd940665a6d183c33508a77f267716ac70dd9526ca7bbf2c38b9dL17) as the changes we made caused the interface to become unecessary.
GuessWhoSamFoo: Has the mock file been regenerated with `go run build.go go-install && go run build.go generate`?
mstergianis: Good catch @GuessWhoSamFoo. I didn't pay enough attention to the diffs and didn't notice that the file was not deleted. I fixed the underlying issue: `removeFakes` in `build.go` was trying to remove `{pkg,internal}/<some package>/fake/fake`

Patch:
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 3367a000..e6973ed9 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -26,7 +26,7 @@ jobs:
   eslint:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/setup-node@v2.1.3
+      - uses: actions/setup-node@v2.1.2
         with:
           node-version: '14'
       - uses: actions/checkout@v2
diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml
index a08b208e..95fee17d 100644
--- a/.github/workflows/nightly.yaml
+++ b/.github/workflows/nightly.yaml
@@ -7,7 +7,7 @@ jobs:
     name: Build nightly
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/setup-node@v2.1.3
+      - uses: actions/setup-node@v2.1.2
         with:
           node-version: '14'
       - uses: actions/setup-go@v2
diff --git a/.github/workflows/preflight-checks.yaml b/.github/workflows/preflight-checks.yaml
index 4fcd3cd7..8159b202 100644
--- a/.github/workflows/preflight-checks.yaml
+++ b/.github/workflows/preflight-checks.yaml
@@ -21,7 +21,7 @@ jobs:
     runs-on: ${{ matrix.platform }}
     name: Golang tests on ${{ matrix.platform }}
     steps:
-      - uses: actions/setup-node@v2.1.3
+      - uses: actions/setup-node@v2.1.2
         with:
           node-version: '14'
       - uses: actions/setup-go@v2
@@ -59,7 +59,7 @@ jobs:
     runs-on: ${{ matrix.platform }}
     name: Karma tests on ${{ matrix.platform }}
     steps:
-      - uses: actions/setup-node@v2.1.3
+      - uses: actions/setup-node@v2.1.2
         with:
           node-version: '14'
       - uses: actions/checkout@v2
@@ -79,7 +79,7 @@ jobs:
     runs-on: ${{ matrix.platform }}
     name: Build on ${{ matrix.platform }}
     steps:
-      - uses: actions/setup-node@v2.1.3
+      - uses: actions/setup-node@v2.1.2
         with:
           node-version: '14'
       - uses: actions/setup-go@v2
diff --git a/build.go b/build.go
index 7b67bd93..62bcf131 100644
--- a/build.go
+++ b/build.go
@@ -437,7 +437,7 @@ func removeFakes() {
 				return nil
 			}
 			if info.Name() == "fake" {
-				fakePaths = append(fakePaths, filepath.Join(path, info.Name()))
+				fakePaths = append(fakePaths, path)
 			}
 			return nil
 		})
diff --git a/choco/octant.nuspec b/choco/octant.nuspec
index 436606d5..e114db6d 100644
--- a/choco/octant.nuspec
+++ b/choco/octant.nuspec
@@ -3,7 +3,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
   <metadata>
     <id>octant</id>
-    <version>0.16.3</version>
+    <version>0.16.1</version>
     <owners>VMware</owners>
     <title>Octant</title>
     <authors>VMware Tanzu</authors>
diff --git a/choco/tools/chocolateyinstall.ps1 b/choco/tools/chocolateyinstall.ps1
index 9bf6ce2e..8f8186df 100644
--- a/choco/tools/chocolateyinstall.ps1
+++ b/choco/tools/chocolateyinstall.ps1
@@ -2,8 +2,8 @@
 $ErrorActionPreference = 'Stop';
 $packageName = 'octant'
 $toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
-$url64 = 'https://github.com/vmware-tanzu/octant/releases/download/v0.16.3/octant_0.16.3_Windows-64bit.zip'
-$checksum64 = '15bafbb6354257102c51478dda889eefa7676c60ea291b2756911a5fcb2e1f71'
+$url64 = 'https://github.com/vmware-tanzu/octant/releases/download/v0.16.1/octant_0.16.1_Windows-64bit.zip'
+$checksum64 = '07f276d1f1396a634881598fd8cd6532ed25325048081defccdb54512b6037f4'
 $checksumType64= 'sha256'
 
 $packageArgs = @{
diff --git a/cmd/octant-electron/main.go b/cmd/octant-electron/main.go
index f6dc8d83..90492aa2 100644
--- a/cmd/octant-electron/main.go
+++ b/cmd/octant-electron/main.go
@@ -77,11 +77,9 @@ func run(ctx context.Context) error {
 		return fmt.Errorf("failed to create net listener: %w", err)
 	}
 
-	dashOptions := dash.Options{
-		DisableClusterOverview: false,
-		EnableOpenCensus:       false,
-		UserAgent:              fmt.Sprintf("octant-electron"), // TODO: create proper user agent
-		Listener:               listener,
+	dashOptions := []dash.RunnerOption{
+		dash.WithClientUserAgent("octant-electron"), // TODO: create proper user agent
+		dash.WithListener(listener),
 	}
 	shutdownCh := make(chan bool, 1)
 	startupCh := make(chan bool, 1)
@@ -89,13 +87,13 @@ func run(ctx context.Context) error {
 
 	ctx, cancel := context.WithCancel(ctx)
 
-	runner, err := dash.NewRunner(ctx, logger, dashOptions)
+	runner, err := dash.NewRunner(ctx, logger, dashOptions...)
 	if err != nil {
 		return fmt.Errorf("create octant runner: %w", err)
 	}
 
 	go func() {
-		runner.Start(dashOptions, startupCh, shutdownCh)
+		runner.Start(startupCh, shutdownCh, dashOptions...)
 		runCh <- true
 	}()
 
diff --git a/internal/api/websocket_state.go b/internal/api/websocket_state.go
index 17e90c5d..f3773879 100644
--- a/internal/api/websocket_state.go
+++ b/internal/api/websocket_state.go
@@ -345,6 +345,8 @@ func (c *WebsocketState) SetFilters(filters []octant.Filter) {
 
 // SetContext sets the Kubernetes context.
 func (c *WebsocketState) SetContext(requestedContext string) {
+	c.dashConfig.SetContextChosenInUI(true)
+
 	if err := c.dashConfig.UseContext(context.TODO(), requestedContext); err != nil {
 		c.dashConfig.Logger().WithErr(err).Errorf("update context")
 	}
diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go
index 23b1c918..47a4a338 100644
--- a/internal/cluster/cluster.go
+++ b/internal/cluster/cluster.go
@@ -10,7 +10,6 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
-	"path/filepath"
 	"time"
 
 	"github.com/pkg/errors"
@@ -31,7 +30,6 @@ import (
 	apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
 
 	internalLog "github.com/vmware-tanzu/octant/internal/log"
-	"github.com/vmware-tanzu/octant/internal/util/strings"
 	"github.com/vmware-tanzu/octant/pkg/log"
 
 	// auth plugins
@@ -241,41 +239,85 @@ func (c *Cluster) Version() (string, error) {
 	return fmt.Sprint(serverVersion), nil
 }
 
-// FromKubeConfig creates a Cluster from a kubeConfig chain.
-func FromKubeConfig(ctx context.Context, kubeConfigList, contextName string, initialNamespace string, providedNamespaces []string, options RESTConfigOptions) (*Cluster, error) {
-	chain := strings.Deduplicate(filepath.SplitList(kubeConfigList))
-	rules := &clientcmd.ClientConfigLoadingRules{
-		Precedence: chain,
+type clusterOptions struct {
+	InitialNamespace   string
+	ProvidedNamespaces []string
+	RESTConfigOptions  RESTConfigOptions
+}
+
+type ClusterOption func(*clusterOptions)
+
+func WithInitialNamespace(initialNamespace string) ClusterOption {
+	return func(clusterOptions *clusterOptions) {
+		clusterOptions.InitialNamespace = initialNamespace
+	}
+}
+
+func WithProvidedNamespaces(providedNamespaces []string) ClusterOption {
+	return func(clusterOptions *clusterOptions) {
+		clusterOptions.ProvidedNamespaces = providedNamespaces
+	}
+}
+
+func WithClientQPS(qps float32) ClusterOption {
+	return func(clusterOptions *clusterOptions) {
+		clusterOptions.RESTConfigOptions.QPS = qps
+	}
+}
+
+func WithClientBurst(burst int) ClusterOption {
+	return func(clusterOptions *clusterOptions) {
+		clusterOptions.RESTConfigOptions.Burst = burst
+	}
+}
+
+func WithClientUserAgent(userAgent string) ClusterOption {
+	return func(clusterOptions *clusterOptions) {
+		clusterOptions.RESTConfigOptions.UserAgent = userAgent
 	}
+}
 
-	overrides := &clientcmd.ConfigOverrides{}
-	if contextName != "" {
-		overrides.CurrentContext = contextName
+func WithRESTConfigOptions(restConfigOptions RESTConfigOptions) ClusterOption {
+	return func(clusterOptions *clusterOptions) {
+		clusterOptions.RESTConfigOptions = restConfigOptions
+	}
+}
+
+// FromClientConfig creates a Cluster from a k8s.io/client-go ClientConfig
+func FromClientConfig(
+	ctx context.Context,
+	clientConfig clientcmd.ClientConfig,
+	opts ...ClusterOption,
+) (*Cluster, error) {
+	options := clusterOptions{}
+	for _, opt := range opts {
+		if opt != nil {
+			opt(&options)
+		}
 	}
-	cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
-	config, err := cc.ClientConfig()
+	restConfig, err := clientConfig.ClientConfig()
 	if err != nil {
 		return nil, err
 	}
 
 	var defaultNamespace string
 
-	if initialNamespace == "" {
-		defaultNamespace, _, err = cc.Namespace()
+	if options.InitialNamespace == "" {
+		defaultNamespace, _, err = clientConfig.Namespace()
 		if err != nil {
 			return nil, err
 		}
 	} else {
-		defaultNamespace = initialNamespace
+		defaultNamespace = options.InitialNamespace
 	}
 
 	logger := internalLog.From(ctx)
-	logger.With("client-qps", options.QPS, "client-burst", options.Burst).
+	logger.With("client-qps", options.RESTConfigOptions.QPS, "client-burst", options.RESTConfigOptions.Burst).
 		Debugf("initializing REST client configuration")
 
-	config = withConfigDefaults(config, options)
+	restConfig = withConfigDefaults(restConfig, options.RESTConfigOptions)
 
-	return newCluster(ctx, cc, config, defaultNamespace, providedNamespaces)
+	return newCluster(ctx, clientConfig, restConfig, defaultNamespace, options.ProvidedNamespaces)
 }
 
 // withConfigDefaults returns an extended rest.Config object with additional defaults applied
diff --git a/internal/commands/dash.go b/internal/commands/dash.go
index 60fa2aa9..31f947a8 100644
--- a/internal/commands/dash.go
+++ b/internal/commands/dash.go
@@ -80,20 +80,24 @@ func newOctantCmd(version string, gitCommit string, buildTime string) *cobra.Com
 					Time:    buildTime,
 				}
 
-				options := dash.Options{
-					DisableClusterOverview: viper.GetBool("disable-cluster-overview"),
-					EnableOpenCensus:       viper.GetBool("enable-opencensus"),
-					KubeConfig:             viper.GetString("kubeconfig"),
-					Namespace:              viper.GetString("namespace"),
-					Namespaces:             viper.GetStringSlice("namespace-list"),
-					FrontendURL:            viper.GetString("ui-url"),
-					BrowserPath:            viper.GetString("browser-path"),
-					Context:                viper.GetString("context"),
-					ClientQPS:              float32(viper.GetFloat64("client-qps")),
-					ClientBurst:            viper.GetInt("client-burst"),
-					UserAgent:              fmt.Sprintf("octant/%s", version),
-					BuildInfo:              buildInfo,
-					Listener:               listener,
+				options := []dash.RunnerOption{
+					dash.WithKubeConfig(viper.GetString("kubeconfig")),
+					dash.WithNamespace(viper.GetString("namespace")),
+					dash.WithNamespaces(viper.GetStringSlice("namespace-list")),
+					dash.WithFrontendURL(viper.GetString("ui-url")),
+					dash.WithBrowserPath(viper.GetString("browser-path")),
+					dash.WithContext(viper.GetString("context")),
+					dash.WithClientQPS(float32(viper.GetFloat64("client-qps"))),
+					dash.WithClientBurst(viper.GetInt("client-burst")),
+					dash.WithClientUserAgent(fmt.Sprintf("octant/%s", version)),
+					dash.WithBuildInfo(buildInfo),
+					dash.WithListener(listener),
+				}
+				if viper.GetBool("disable-cluster-overview") {
+					options = append(options, dash.WithoutClusterOverview())
+				}
+				if viper.GetBool("enable-opencensus") {
+					options = append(options, dash.WithOpenCensus())
 				}
 
 				klogVerbosity := viper.GetString("klog-verbosity")
@@ -118,13 +122,13 @@ func newOctantCmd(version string, gitCommit string, buildTime string) *cobra.Com
 
 				_ = klogFlagSet.Parse(klogOpts)
 
-				runner, err := dash.NewRunner(ctx, logger, options)
+				runner, err := dash.NewRunner(ctx, logger, options...)
 				if err != nil {
 					golog.Printf("unable to start runner: %v", err)
 					os.Exit(1)
 				}
 
-				runner.Start(options, nil, shutdownCh)
+				runner.Start(nil, shutdownCh, options...)
 
 				runCh <- true
 			}()
diff --git a/internal/config/dash.go b/internal/config/dash.go
index 1ba41eea..93c6a80d 100644
--- a/internal/config/dash.go
+++ b/internal/config/dash.go
@@ -8,6 +8,7 @@ package config
 import (
 	"context"
 
+	"github.com/vmware-tanzu/octant/internal/kubeconfig"
 	"github.com/vmware-tanzu/octant/internal/octant"
 	"github.com/vmware-tanzu/octant/pkg/store"
 
@@ -24,6 +25,7 @@ import (
 )
 
 //go:generate mockgen -destination=./fake/mock_dash.go -package=fake github.com/vmware-tanzu/octant/internal/config Dash
+//go:generate mockgen -destination=./fake/mock_kubecontextdecorator.go -package=fake github.com/vmware-tanzu/octant/internal/config KubeContextDecorator
 
 // CRDWatcher watches for CRDs.
 type CRDWatcher interface {
@@ -31,6 +33,38 @@ type CRDWatcher interface {
 	AddConfig(config *CRDWatchConfig) error
 }
 
+// KubeContextDecorator handles context changes
+type KubeContextDecorator interface {
+	SwitchContext(context.Context, string) error
+	ClusterClient() cluster.ClientInterface
+	CurrentContext() string
+	Contexts() []kubeconfig.Context
+}
+
+func StaticClusterClient(client cluster.ClientInterface) *staticClusterClient {
+	return &staticClusterClient{client}
+}
+
+type staticClusterClient struct {
+	cluster.ClientInterface
+}
+
+func (scc *staticClusterClient) SwitchContext(
+	ctx context.Context,
+	contextName string,
+) error {
+	return nil
+}
+func (scc *staticClusterClient) ClusterClient() cluster.ClientInterface {
+	return scc.ClientInterface
+}
+func (scc *staticClusterClient) CurrentContext() string {
+	return ""
+}
+func (scc *staticClusterClient) Contexts() []kubeconfig.Context {
+	return nil
+}
+
 // ObjectHandler is a function that is run when a new object is available.
 type ObjectHandler func(ctx context.Context, object *unstructured.Unstructured)
 
@@ -93,11 +127,15 @@ type Dash interface {
 
 	PortForwarder() portforward.PortForwarder
 
-	KubeConfigPath() string
+	SetContextChosenInUI(contextChosen bool)
+
+	UseFSContext(ctx context.Context) error
 
 	UseContext(ctx context.Context, contextName string) error
 
-	ContextName() string
+	CurrentContext() string
+
+	Contexts() []kubeconfig.Context
 
 	DefaultNamespace() string
 
@@ -108,52 +146,52 @@ type Dash interface {
 	BuildInfo() (string, string, string)
 }
 
+// UseFSContext is used to indicate a context switch to the file system Kubeconfig context
+const UseFSContext = ""
+
 // Live is a live version of dash config.
 type Live struct {
-	clusterClient      cluster.ClientInterface
-	crdWatcher         CRDWatcher
-	logger             log.Logger
-	moduleManager      module.ManagerInterface
-	objectStore        store.Store
-	errorStore         internalErr.ErrorStore
-	pluginManager      plugin.ManagerInterface
-	portForwarder      portforward.PortForwarder
-	kubeConfigPath     string
-	currentContextName string
-	restConfigOptions  cluster.RESTConfigOptions
-	buildInfo          BuildInfo
+	kubeContextDecorator KubeContextDecorator
+	crdWatcher           CRDWatcher
+	logger               log.Logger
+	moduleManager        module.ManagerInterface
+	objectStore          store.Store
+	errorStore           internalErr.ErrorStore
+	pluginManager        plugin.ManagerInterface
+	portForwarder        portforward.PortForwarder
+	restConfigOptions    cluster.RESTConfigOptions
+	buildInfo            BuildInfo
+	contextChosenInUI    bool
 }
 
 var _ Dash = (*Live)(nil)
 
 // NewLiveConfig creates an instance of Live.
 func NewLiveConfig(
-	clusterClient cluster.ClientInterface,
+	kubeContextDecorator KubeContextDecorator,
 	crdWatcher CRDWatcher,
-	kubeConfigPath string,
 	logger log.Logger,
 	moduleManager module.ManagerInterface,
 	objectStore store.Store,
 	errorStore internalErr.ErrorStore,
 	pluginManager plugin.ManagerInterface,
 	portForwarder portforward.PortForwarder,
-	currentContextName string,
 	restConfigOptions cluster.RESTConfigOptions,
 	buildInfo BuildInfo,
+	contextChosenInUI bool,
 ) *Live {
 	l := &Live{
-		clusterClient:      clusterClient,
-		crdWatcher:         crdWatcher,
-		kubeConfigPath:     kubeConfigPath,
-		logger:             logger,
-		moduleManager:      moduleManager,
-		objectStore:        objectStore,
-		errorStore:         errorStore,
-		pluginManager:      pluginManager,
-		portForwarder:      portForwarder,
-		currentContextName: currentContextName,
-		restConfigOptions:  restConfigOptions,
-		buildInfo:          buildInfo,
+		kubeContextDecorator: kubeContextDecorator,
+		crdWatcher:           crdWatcher,
+		logger:               logger,
+		moduleManager:        moduleManager,
+		objectStore:          objectStore,
+		errorStore:           errorStore,
+		pluginManager:        pluginManager,
+		portForwarder:        portForwarder,
+		restConfigOptions:    restConfigOptions,
+		buildInfo:            buildInfo,
+		contextChosenInUI:    contextChosenInUI,
 	}
 	objectStore.RegisterOnUpdate(func(store store.Store) {
 		l.objectStore = store
@@ -169,7 +207,7 @@ func (l *Live) ObjectPath(namespace, apiVersion, kind, name string) (string, err
 
 // ClusterClient returns a cluster client.
 func (l *Live) ClusterClient() cluster.ClientInterface {
-	return l.clusterClient
+	return l.kubeContextDecorator.ClusterClient()
 }
 
 // CRDWatcher returns a CRD watcher.
@@ -187,11 +225,6 @@ func (l *Live) ErrorStore() internalErr.ErrorStore {
 	return l.errorStore
 }
 
-// KubeConfigPath returns the kube config path.
-func (l *Live) KubeConfigPath() string {
-	return l.kubeConfigPath
-}
-
 // Logger returns a logger.
 func (l *Live) Logger() log.Logger {
 	return l.logger
@@ -207,17 +240,26 @@ func (l *Live) PortForwarder() portforward.PortForwarder {
 	return l.portForwarder
 }
 
+func (l *Live) SetContextChosenInUI(contextChosen bool) {
+	l.contextChosenInUI = contextChosen
+}
+
+func (l *Live) UseFSContext(ctx context.Context) error {
+	return l.UseContext(ctx, UseFSContext)
+}
+
 // UseContext switches context name. This process should have synchronously.
 func (l *Live) UseContext(ctx context.Context, contextName string) error {
-	// TODO: (GuessWhoSamFoo) FromKubeConfig needs a refactor. Initial ns is not needed when changing contexts (GH#362)
-	client, err := cluster.FromKubeConfig(ctx, l.kubeConfigPath, contextName, "", []string{}, l.restConfigOptions)
+	if l.contextChosenInUI && contextName == UseFSContext {
+		contextName = l.CurrentContext()
+	}
+
+	err := l.kubeContextDecorator.SwitchContext(ctx, contextName)
 	if err != nil {
 		return err
 	}
 
-	l.ClusterClient().Close()
-	l.clusterClient = client
-
+	client := l.kubeContextDecorator.ClusterClient()
 	if err := l.objectStore.UpdateClusterClient(ctx, client); err != nil {
 		return err
 	}
@@ -226,7 +268,6 @@ func (l *Live) UseContext(ctx context.Context, contextName string) error {
 		return err
 	}
 
-	l.currentContextName = contextName
 	l.Logger().With("new-kube-context", contextName).Infof("updated kube config context")
 
 	for _, m := range l.moduleManager.Modules() {
@@ -240,9 +281,14 @@ func (l *Live) UseContext(ctx context.Context, contextName string) error {
 	return nil
 }
 
-// ContextName returns the current context name
-func (l *Live) ContextName() string {
-	return l.currentContextName
+// CurrentContext returns the current context name
+func (l *Live) CurrentContext() string {
+	return l.kubeContextDecorator.CurrentContext()
+}
+
+// Contexts returns the set of all contexts
+func (l *Live) Contexts() []kubeconfig.Context {
+	return l.kubeContextDecorator.Contexts()
 }
 
 // DefaultNamespace returns the default namespace for the current cluster..
@@ -252,7 +298,7 @@ func (l *Live) DefaultNamespace() string {
 
 // Validate validates the configuration and returns an error if there is an issue.
 func (l *Live) Validate() error {
-	if l.clusterClient == nil {
+	if l.ClusterClient() == nil {
 		return errors.New("cluster client is nil")
 	}
 
diff --git a/internal/config/fake/mock_dash.go b/internal/config/fake/mock_dash.go
index e7e86345..2edbbdb7 100644
--- a/internal/config/fake/mock_dash.go
+++ b/internal/config/fake/mock_dash.go
@@ -13,6 +13,7 @@ import (
 	cluster "github.com/vmware-tanzu/octant/internal/cluster"
 	config "github.com/vmware-tanzu/octant/internal/config"
 	errors "github.com/vmware-tanzu/octant/internal/errors"
+	kubeconfig "github.com/vmware-tanzu/octant/internal/kubeconfig"
 	module "github.com/vmware-tanzu/octant/internal/module"
 	portforward "github.com/vmware-tanzu/octant/internal/portforward"
 	log "github.com/vmware-tanzu/octant/pkg/log"
@@ -87,18 +88,32 @@ func (mr *MockDashMockRecorder) ClusterClient() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterClient", reflect.TypeOf((*MockDash)(nil).ClusterClient))
 }
 
-// ContextName mocks base method
-func (m *MockDash) ContextName() string {
+// Contexts mocks base method
+func (m *MockDash) Contexts() []kubeconfig.Context {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ContextName")
+	ret := m.ctrl.Call(m, "Contexts")
+	ret0, _ := ret[0].([]kubeconfig.Context)
+	return ret0
+}
+
+// Contexts indicates an expected call of Contexts
+func (mr *MockDashMockRecorder) Contexts() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Contexts", reflect.TypeOf((*MockDash)(nil).Contexts))
+}
+
+// CurrentContext mocks base method
+func (m *MockDash) CurrentContext() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CurrentContext")
 	ret0, _ := ret[0].(string)
 	return ret0
 }
 
-// ContextName indicates an expected call of ContextName
-func (mr *MockDashMockRecorder) ContextName() *gomock.Call {
+// CurrentContext indicates an expected call of CurrentContext
+func (mr *MockDashMockRecorder) CurrentContext() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContextName", reflect.TypeOf((*MockDash)(nil).ContextName))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentContext", reflect.TypeOf((*MockDash)(nil).CurrentContext))
 }
 
 // DefaultNamespace mocks base method
@@ -129,20 +144,6 @@ func (mr *MockDashMockRecorder) ErrorStore() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ErrorStore", reflect.TypeOf((*MockDash)(nil).ErrorStore))
 }
 
-// KubeConfigPath mocks base method
-func (m *MockDash) KubeConfigPath() string {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "KubeConfigPath")
-	ret0, _ := ret[0].(string)
-	return ret0
-}
-
-// KubeConfigPath indicates an expected call of KubeConfigPath
-func (mr *MockDashMockRecorder) KubeConfigPath() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KubeConfigPath", reflect.TypeOf((*MockDash)(nil).KubeConfigPath))
-}
-
 // Logger mocks base method
 func (m *MockDash) Logger() log.Logger {
 	m.ctrl.T.Helper()
@@ -228,6 +229,18 @@ func (mr *MockDashMockRecorder) PortForwarder() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PortForwarder", reflect.TypeOf((*MockDash)(nil).PortForwarder))
 }
 
+// SetContextChosenInUI mocks base method
+func (m *MockDash) SetContextChosenInUI(arg0 bool) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetContextChosenInUI", arg0)
+}
+
+// SetContextChosenInUI indicates an expected call of SetContextChosenInUI
+func (mr *MockDashMockRecorder) SetContextChosenInUI(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetContextChosenInUI", reflect.TypeOf((*MockDash)(nil).SetContextChosenInUI), arg0)
+}
+
 // UseContext mocks base method
 func (m *MockDash) UseContext(arg0 context.Context, arg1 string) error {
 	m.ctrl.T.Helper()
@@ -242,6 +255,20 @@ func (mr *MockDashMockRecorder) UseContext(arg0, arg1 interface{}) *gomock.Call
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseContext", reflect.TypeOf((*MockDash)(nil).UseContext), arg0, arg1)
 }
 
+// UseFSContext mocks base method
+func (m *MockDash) UseFSContext(arg0 context.Context) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UseFSContext", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// UseFSContext indicates an expected call of UseFSContext
+func (mr *MockDashMockRecorder) UseFSContext(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseFSContext", reflect.TypeOf((*MockDash)(nil).UseFSContext), arg0)
+}
+
 // Validate mocks base method
 func (m *MockDash) Validate() error {
 	m.ctrl.T.Helper()
diff --git a/internal/config/fake/mock_kubecontextdecorator.go b/internal/config/fake/mock_kubecontextdecorator.go
new file mode 100644
index 00000000..76a635a9
--- /dev/null
+++ b/internal/config/fake/mock_kubecontextdecorator.go
@@ -0,0 +1,94 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/vmware-tanzu/octant/internal/config (interfaces: KubeContextDecorator)
+
+// Package fake is a generated GoMock package.
+package fake
+
+import (
+	context "context"
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+
+	cluster "github.com/vmware-tanzu/octant/internal/cluster"
+	kubeconfig "github.com/vmware-tanzu/octant/internal/kubeconfig"
+)
+
+// MockKubeContextDecorator is a mock of KubeContextDecorator interface
+type MockKubeContextDecorator struct {
+	ctrl     *gomock.Controller
+	recorder *MockKubeContextDecoratorMockRecorder
+}
+
+// MockKubeContextDecoratorMockRecorder is the mock recorder for MockKubeContextDecorator
+type MockKubeContextDecoratorMockRecorder struct {
+	mock *MockKubeContextDecorator
+}
+
+// NewMockKubeContextDecorator creates a new mock instance
+func NewMockKubeContextDecorator(ctrl *gomock.Controller) *MockKubeContextDecorator {
+	mock := &MockKubeContextDecorator{ctrl: ctrl}
+	mock.recorder = &MockKubeContextDecoratorMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockKubeContextDecorator) EXPECT() *MockKubeContextDecoratorMockRecorder {
+	return m.recorder
+}
+
+// ClusterClient mocks base method
+func (m *MockKubeContextDecorator) ClusterClient() cluster.ClientInterface {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ClusterClient")
+	ret0, _ := ret[0].(cluster.ClientInterface)
+	return ret0
+}
+
+// ClusterClient indicates an expected call of ClusterClient
+func (mr *MockKubeContextDecoratorMockRecorder) ClusterClient() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterClient", reflect.TypeOf((*MockKubeContextDecorator)(nil).ClusterClient))
+}
+
+// Contexts mocks base method
+func (m *MockKubeContextDecorator) Contexts() []kubeconfig.Context {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Contexts")
+	ret0, _ := ret[0].([]kubeconfig.Context)
+	return ret0
+}
+
+// Contexts indicates an expected call of Contexts
+func (mr *MockKubeContextDecoratorMockRecorder) Contexts() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Contexts", reflect.TypeOf((*MockKubeContextDecorator)(nil).Contexts))
+}
+
+// CurrentContext mocks base method
+func (m *MockKubeContextDecorator) CurrentContext() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CurrentContext")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// CurrentContext indicates an expected call of CurrentContext
+func (mr *MockKubeContextDecoratorMockRecorder) CurrentContext() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentContext", reflect.TypeOf((*MockKubeContextDecorator)(nil).CurrentContext))
+}
+
+// SwitchContext mocks base method
+func (m *MockKubeContextDecorator) SwitchContext(arg0 context.Context, arg1 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SwitchContext", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// SwitchContext indicates an expected call of SwitchContext
+func (mr *MockKubeContextDecoratorMockRecorder) SwitchContext(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwitchContext", reflect.TypeOf((*MockKubeContextDecorator)(nil).SwitchContext), arg0, arg1)
+}
diff --git a/internal/event/context.go b/internal/event/context.go
index 9c9419d7..98ad62c5 100644
--- a/internal/event/context.go
+++ b/internal/event/context.go
@@ -12,9 +12,6 @@ import (
 
 	"github.com/vmware-tanzu/octant/pkg/event"
 
-	"github.com/pkg/errors"
-
-	"github.com/vmware-tanzu/octant/internal/config"
 	"github.com/vmware-tanzu/octant/internal/kubeconfig"
 	"github.com/vmware-tanzu/octant/internal/octant"
 )
@@ -27,18 +24,21 @@ type kubeContextsResponse struct {
 
 type ContextGeneratorOption func(generator *ContextsGenerator)
 
+type KubeContextStore interface {
+	CurrentContext() string
+	Contexts() []kubeconfig.Context
+}
+
 // ContextsGenerator generates kube contexts for the front end.
 type ContextsGenerator struct {
-	ConfigLoader kubeconfig.Loader
-	DashConfig   config.Dash
+	KubeContextStore KubeContextStore
 }
 
 var _ octant.Generator = (*ContextsGenerator)(nil)
 
-func NewContextsGenerator(dashConfig config.Dash, options ...ContextGeneratorOption) *ContextsGenerator {
+func NewContextsGenerator(kubeContextStore KubeContextStore, options ...ContextGeneratorOption) *ContextsGenerator {
 	kcg := &ContextsGenerator{
-		ConfigLoader: kubeconfig.NewFSLoader(),
-		DashConfig:   dashConfig,
+		KubeContextStore: kubeContextStore,
 	}
 
 	for _, option := range options {
@@ -49,21 +49,9 @@ func NewContextsGenerator(dashConfig config.Dash, options ...ContextGeneratorOpt
 }
 
 func (g *ContextsGenerator) Event(ctx context.Context) (event.Event, error) {
-	configPath := g.DashConfig.KubeConfigPath()
-
-	kubeConfig, err := g.ConfigLoader.Load(configPath)
-	if err != nil {
-		return event.Event{}, errors.Wrap(err, "unable to load kube config")
-	}
-
-	currentContext := g.DashConfig.ContextName()
-	if currentContext == "" {
-		currentContext = kubeConfig.CurrentContext
-	}
-
 	resp := kubeContextsResponse{
-		CurrentContext: currentContext,
-		Contexts:       kubeConfig.Contexts,
+		CurrentContext: g.KubeContextStore.CurrentContext(),
+		Contexts:       g.KubeContextStore.Contexts(),
 	}
 
 	sort.Slice(resp.Contexts, func(i, j int) bool {
diff --git a/internal/kubeconfig/fake/mock_loader.go b/internal/kubeconfig/fake/mock_loader.go
deleted file mode 100644
index f3726713..00000000
--- a/internal/kubeconfig/fake/mock_loader.go
+++ /dev/null
@@ -1,51 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/vmware-tanzu/octant/internal/kubeconfig (interfaces: Loader)
-
-// Package fake is a generated GoMock package.
-package fake
-
-import (
-	reflect "reflect"
-
-	gomock "github.com/golang/mock/gomock"
-
-	kubeconfig "github.com/vmware-tanzu/octant/internal/kubeconfig"
-)
-
-// MockLoader is a mock of Loader interface
-type MockLoader struct {
-	ctrl     *gomock.Controller
-	recorder *MockLoaderMockRecorder
-}
-
-// MockLoaderMockRecorder is the mock recorder for MockLoader
-type MockLoaderMockRecorder struct {
-	mock *MockLoader
-}
-
-// NewMockLoader creates a new mock instance
-func NewMockLoader(ctrl *gomock.Controller) *MockLoader {
-	mock := &MockLoader{ctrl: ctrl}
-	mock.recorder = &MockLoaderMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use
-func (m *MockLoader) EXPECT() *MockLoaderMockRecorder {
-	return m.recorder
-}
-
-// Load mocks base method
-func (m *MockLoader) Load(arg0 string) (*kubeconfig.KubeConfig, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Load", arg0)
-	ret0, _ := ret[0].(*kubeconfig.KubeConfig)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// Load indicates an expected call of Load
-func (mr *MockLoaderMockRecorder) Load(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Load", reflect.TypeOf((*MockLoader)(nil).Load), arg0)
-}
diff --git a/internal/kubeconfig/kubeconfig.go b/internal/kubeconfig/kubeconfig.go
index b3263797..16730486 100644
--- a/internal/kubeconfig/kubeconfig.go
+++ b/internal/kubeconfig/kubeconfig.go
@@ -6,77 +6,161 @@ SPDX-License-Identifier: Apache-2.0
 package kubeconfig
 
 import (
+	"context"
 	"path/filepath"
 	"sort"
 
 	"k8s.io/client-go/tools/clientcmd"
 
+	"github.com/pkg/errors"
+
+	"github.com/vmware-tanzu/octant/internal/cluster"
 	"github.com/vmware-tanzu/octant/internal/util/strings"
 )
 
-//go:generate mockgen -destination=./fake/mock_loader.go -package=fake github.com/vmware-tanzu/octant/internal/kubeconfig Loader
-
-// KubeConfig describes a kube config for dash.
-type KubeConfig struct {
-	Contexts       []Context
-	CurrentContext string
+type KubeConfigOption struct {
+	kubeConfigOption func(*kubeConfigOptions)
+	clusterOption    cluster.ClusterOption
 }
 
-// Context describes a kube config context.
-type Context struct {
-	Name string `json:"name"`
+type kubeConfigOptions struct {
+	KubeConfigList string
+	ContextName    string
 }
 
-// Loader is an interface for loading kube config.
-type Loader interface {
-	Load(filename string) (*KubeConfig, error)
+func Noop() KubeConfigOption {
+	return KubeConfigOption{
+		kubeConfigOption: func(*kubeConfigOptions) {},
+	}
 }
 
-// FSLoaderOpt is an option for configuring FSLoader.
-type FSLoaderOpt func(loader *FSLoader)
-
-// FSLoader loads kube configs from the file system.
-type FSLoader struct {
+func WithKubeConfigList(kubeConfigList string) KubeConfigOption {
+	return KubeConfigOption{
+		kubeConfigOption: func(kubeConfigOptions *kubeConfigOptions) {
+			kubeConfigOptions.KubeConfigList = kubeConfigList
+		},
+	}
 }
 
-var _ Loader = (*FSLoader)(nil)
-
-// NewFSLoader creates an instance of FSLoader.
-func NewFSLoader(options ...FSLoaderOpt) *FSLoader {
-	l := &FSLoader{}
-
-	for _, option := range options {
-		option(l)
+func WithContextName(contextName string) KubeConfigOption {
+	return KubeConfigOption{
+		kubeConfigOption: func(kubeConfigOptions *kubeConfigOptions) {
+			kubeConfigOptions.ContextName = contextName
+		},
 	}
-
-	return l
 }
 
-// Load loads a kube config contexts from a list of files.
-func (l *FSLoader) Load(fileList string) (*KubeConfig, error) {
-	chain := strings.Deduplicate(filepath.SplitList(fileList))
+func FromClusterOption(clusterOption cluster.ClusterOption) KubeConfigOption {
+	return KubeConfigOption{
+		kubeConfigOption: func(*kubeConfigOptions) {},
+		clusterOption:    clusterOption,
+	}
+}
 
-	loadingRules := &clientcmd.ClientConfigLoadingRules{
+func NewKubeConfigContextManager(ctx context.Context, opts ...KubeConfigOption) (*KubeConfigContextManager, error) {
+	options := kubeConfigOptions{}
+	clusterOptions := []cluster.ClusterOption{}
+	for _, opt := range opts {
+		opt.kubeConfigOption(&options)
+		clusterOptions = append(clusterOptions, opt.clusterOption)
+	}
+	chain := strings.Deduplicate(filepath.SplitList(options.KubeConfigList))
+	rules := &clientcmd.ClientConfigLoadingRules{
 		Precedence: chain,
 	}
 
-	config, err := loadingRules.Load()
+	overrides := &clientcmd.ConfigOverrides{}
+	if options.ContextName != "" {
+		overrides.CurrentContext = options.ContextName
+	}
+	clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
+	clusterClient, err := cluster.FromClientConfig(ctx, clientConfig, clusterOptions...)
 	if err != nil {
-		return nil, err
+		return nil, errors.Wrap(err, "unable to create cluster client")
 	}
 
-	var list []Context
+	config, err := clientConfig.RawConfig()
+	if err != nil {
+		return nil, errors.Wrap(err, "unable to load kube config")
+	}
+	var contextList []Context
 
 	for name := range config.Contexts {
-		list = append(list, Context{Name: name})
+		contextList = append(contextList, Context{Name: name})
 	}
 
-	sort.Slice(list, func(i, j int) bool {
-		return list[i].Name < list[j].Name
+	sort.Slice(contextList, func(i, j int) bool {
+		return contextList[i].Name < contextList[j].Name
 	})
 
-	return &KubeConfig{
-		Contexts:       list,
-		CurrentContext: config.CurrentContext,
+	contextName := options.ContextName
+	if contextName == "" {
+		contextName = config.CurrentContext
+	}
+
+	return &KubeConfigContextManager{
+		configLoadingRules: &clientcmd.ClientConfigLoadingRules{
+			Precedence: chain,
+		},
+		currentContext: contextName,
+		contexts:       contextList,
+		clusterClient:  clusterClient,
+		clusterOptions: clusterOptions,
 	}, nil
 }
+
+type KubeConfigContextManager struct {
+	configLoadingRules *clientcmd.ClientConfigLoadingRules
+	currentContext     string
+	contexts           []Context
+	clusterClient      cluster.ClientInterface
+	clusterOptions     []cluster.ClusterOption
+}
+
+// Context describes a kube config context.
+type Context struct {
+	Name string `json:"name"`
+}
+
+// UseFSContext is used to indicate a context switch to the file system Kubeconfig context
+const UseFSContext = ""
+
+func (k *KubeConfigContextManager) CurrentContext() string {
+	return k.currentContext
+}
+
+func (k *KubeConfigContextManager) Contexts() []Context {
+	return k.contexts
+}
+
+func (k *KubeConfigContextManager) SwitchContext(ctx context.Context, contextName string) error {
+	if k.clusterClient != nil {
+		k.clusterClient.Close()
+	}
+
+	clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
+		k.configLoadingRules,
+		&clientcmd.ConfigOverrides{CurrentContext: contextName},
+	)
+
+	var err error
+	k.clusterClient, err = cluster.FromClientConfig(ctx, clientConfig, k.clusterOptions...)
+	if err != nil {
+		return errors.Wrap(err, "unable to create cluster client")
+	}
+
+	if contextName == UseFSContext {
+		rawConfig, err := clientConfig.RawConfig()
+		if err != nil {
+			return errors.Wrap(err, "unable to infer context name from kube config")
+		}
+		contextName = rawConfig.CurrentContext
+	}
+
+	k.currentContext = contextName
+	return nil
+}
+
+func (k *KubeConfigContextManager) ClusterClient() cluster.ClientInterface {
+	return k.clusterClient
+}
diff --git a/pkg/dash/dash.go b/pkg/dash/dash.go
index 381f406a..f67f281e 100644
--- a/pkg/dash/dash.go
+++ b/pkg/dash/dash.go
@@ -29,6 +29,7 @@ import (
 	ocontext "github.com/vmware-tanzu/octant/internal/context"
 	"github.com/vmware-tanzu/octant/internal/describer"
 	oerrors "github.com/vmware-tanzu/octant/internal/errors"
+	"github.com/vmware-tanzu/octant/internal/kubeconfig"
 	internalLog "github.com/vmware-tanzu/octant/internal/log"
 	"github.com/vmware-tanzu/octant/internal/module"
 	"github.com/vmware-tanzu/octant/internal/modules/applications"
@@ -62,6 +63,129 @@ type Options struct {
 	UserAgent              string
 	BuildInfo              config.BuildInfo
 	Listener               net.Listener
+	clusterClient          cluster.ClientInterface
+}
+
+type RunnerOption struct {
+	kubeConfigOption kubeconfig.KubeConfigOption
+	nonClusterOption func(*Options)
+}
+
+func WithOpenCensus() RunnerOption {
+	return RunnerOption{
+		nonClusterOption: func(o *Options) {
+			o.EnableOpenCensus = true
+		},
+	}
+}
+
+func WithoutClusterOverview() RunnerOption {
+	return RunnerOption{
+		nonClusterOption: func(o *Options) {
+			o.DisableClusterOverview = true
+		},
+	}
+}
+
+func WithKubeConfig(kubeConfig string) RunnerOption {
+	return RunnerOption{
+		kubeConfigOption: kubeconfig.WithKubeConfigList(kubeConfig),
+		nonClusterOption: func(o *Options) {
+			o.KubeConfig = kubeConfig
+		},
+	}
+}
+
+func WithNamespace(namespace string) RunnerOption {
+	return RunnerOption{
+		kubeConfigOption: kubeconfig.FromClusterOption(cluster.WithInitialNamespace(namespace)),
+		nonClusterOption: func(o *Options) {
+			o.Namespace = namespace
+		},
+	}
+}
+
+func WithNamespaces(namespaces []string) RunnerOption {
+	return RunnerOption{
+		kubeConfigOption: kubeconfig.FromClusterOption(cluster.WithProvidedNamespaces(namespaces)),
+		nonClusterOption: func(o *Options) {
+			o.Namespaces = namespaces
+		},
+	}
+}
+
+func WithFrontendURL(frontendURL string) RunnerOption {
+	return RunnerOption{
+		kubeConfigOption: kubeconfig.Noop(),
+		nonClusterOption: func(o *Options) {
+			o.FrontendURL = frontendURL
+		},
+	}
+}
+
+func WithBrowserPath(browserPath string) RunnerOption {
+	return RunnerOption{
+		kubeConfigOption: kubeconfig.Noop(),
+		nonClusterOption: func(o *Options) {
+			o.BrowserPath = browserPath
+		},
+	}
+}
+
+func WithContext(context string) RunnerOption {
+	return RunnerOption{
+		kubeConfigOption: kubeconfig.WithContextName(context),
+		nonClusterOption: func(o *Options) {
+			o.Context = context
+		},
+	}
+}
+
+func WithClientQPS(qps float32) RunnerOption {
+	return RunnerOption{
+		kubeConfigOption: kubeconfig.FromClusterOption(cluster.WithClientQPS(qps)),
+		nonClusterOption: func(o *Options) {},
+	}
+}
+
+func WithClientBurst(burst int) RunnerOption {
+	return RunnerOption{
+		kubeConfigOption: kubeconfig.FromClusterOption(cluster.WithClientBurst(burst)),
+		nonClusterOption: func(o *Options) {},
+	}
+}
+
+func WithClientUserAgent(userAgent string) RunnerOption {
+	return RunnerOption{
+		kubeConfigOption: kubeconfig.FromClusterOption(cluster.WithClientUserAgent(userAgent)),
+		nonClusterOption: func(o *Options) {},
+	}
+}
+
+func WithBuildInfo(buildInfo config.BuildInfo) RunnerOption {
+	return RunnerOption{
+		kubeConfigOption: kubeconfig.Noop(),
+		nonClusterOption: func(o *Options) {
+			o.BuildInfo = buildInfo
+		},
+	}
+}
+
+func WithListener(listener net.Listener) RunnerOption {
+	return RunnerOption{
+		kubeConfigOption: kubeconfig.Noop(),
+		nonClusterOption: func(o *Options) {
+			o.Listener = listener
+		},
+	}
+}
+
+func WithClusterClient(client cluster.ClientInterface) RunnerOption {
+	return RunnerOption{
+		nonClusterOption: func(o *Options) {
+			o.clusterClient = client
+		},
+	}
 }
 
 type Runner struct {
@@ -75,7 +199,12 @@ type Runner struct {
 	fs                     afero.Fs
 }
 
-func NewRunner(ctx context.Context, logger log.Logger, options Options) (*Runner, error) {
+func NewRunner(ctx context.Context, logger log.Logger, opts ...RunnerOption) (*Runner, error) {
+	options := Options{}
+	for _, opt := range opts {
+		opt.nonClusterOption(&options)
+	}
+
 	r := Runner{}
 	ctx = internalLog.WithLoggerContext(ctx, logger)
 	ctx = ocontext.WithKubeConfigCh(ctx)
@@ -100,15 +229,13 @@ func NewRunner(ctx context.Context, logger log.Logger, options Options) (*Runner
 
 	r.fs = afero.NewOsFs()
 
-	if options.KubeConfig, err = ValidateKubeConfig(logger, options.KubeConfig, r.fs); err == nil {
-		apiService, pluginService, apiErr = r.initAPI(ctx, logger, options)
-		if apiErr != nil {
-			return nil, fmt.Errorf("failed to start service api: %w", apiErr)
-		}
+	if options.clusterClient != nil {
+		apiService, pluginService, apiErr = r.initAPI(ctx, logger, opts...)
 	} else {
-		logger.Infof("no valid kube config found, initializing loading API")
-		// Initialize the API
-		apiService = api.NewLoadingAPI(ctx, api.PathPrefix, r.actionManager, r.websocketClientManager, logger)
+		apiService, pluginService, apiErr = r.apiFromKubeConfig(options.KubeConfig, opts...)
+	}
+	if apiErr != nil {
+		return nil, fmt.Errorf("failed to start service api: %w", apiErr)
 	}
 
 	d, err := newDash(options.Listener, options.Namespace, options.FrontendURL, options.BrowserPath, apiService, pluginService, logger)
@@ -125,7 +252,24 @@ func NewRunner(ctx context.Context, logger log.Logger, options Options) (*Runner
 	return &r, nil
 }
 
-func (r *Runner) Start(options Options, startupCh, shutdownCh chan bool) error {
+func (r *Runner) apiFromKubeConfig(kubeConfig string, opts ...RunnerOption) (api.Service, *pluginAPI.GRPCService, error) {
+	logger := internalLog.From(r.ctx)
+	validKubeConfig, err := ValidateKubeConfig(logger, kubeConfig, r.fs)
+	if err == nil {
+		opts = append(opts, WithKubeConfig(validKubeConfig))
+		return r.initAPI(r.ctx, logger, opts...)
+	} else {
+		logger.Infof("no valid kube config found, initializing loading API")
+		return api.NewLoadingAPI(r.ctx, api.PathPrefix, r.actionManager, r.websocketClientManager, logger), nil, nil
+	}
+}
+
+func (r *Runner) Start(startupCh, shutdownCh chan bool, opts ...RunnerOption) error {
+	options := Options{}
+	for _, opt := range opts {
+		opt.nonClusterOption(&options)
+	}
+
 	logger := internalLog.From(r.ctx)
 	go func() {
 		if err := r.dash.Run(r.ctx, startupCh); err != nil {
@@ -137,13 +281,14 @@ func (r *Runner) Start(options Options, startupCh, shutdownCh chan bool) error {
 		go func() {
 			logger.Infof("waiting for kube config ...")
 			options.KubeConfig = <-ocontext.KubeConfigChFrom(r.ctx)
+			opts = append(opts, WithKubeConfig(options.KubeConfig))
 
 			if options.KubeConfig == "" {
 				logger.Errorf("unexpected empty kube config")
 				return
 			}
 			logger.Debugf("Loading configuration: %v", options.KubeConfig)
-			apiService, pluginService, err := r.initAPI(r.ctx, logger, options)
+			apiService, pluginService, err := r.initAPI(r.ctx, logger, opts...)
 			if err != nil {
 				logger.Errorf("cannot create api: %v", err)
 			}
@@ -175,18 +320,26 @@ func (r *Runner) Start(options Options, startupCh, shutdownCh chan bool) error {
 	return nil
 }
 
-func (r *Runner) initAPI(ctx context.Context, logger log.Logger, options Options) (*api.API, *pluginAPI.GRPCService, error) {
+func (r *Runner) initAPI(ctx context.Context, logger log.Logger, opts ...RunnerOption) (*api.API, *pluginAPI.GRPCService, error) {
+	kubeConfigOptions := []kubeconfig.KubeConfigOption{}
+	options := Options{}
+	for _, opt := range opts {
+		kubeConfigOptions = append(kubeConfigOptions, opt.kubeConfigOption)
+		opt.nonClusterOption(&options)
+	}
 	frontendProxy := pluginAPI.FrontendProxy{}
 
-	restConfigOptions := cluster.RESTConfigOptions{
-		QPS:       options.ClientQPS,
-		Burst:     options.ClientBurst,
-		UserAgent: options.UserAgent,
-	}
-	clusterClient, err := cluster.FromKubeConfig(ctx, options.KubeConfig, options.Context, options.Namespace, options.Namespaces, restConfigOptions)
-	if err != nil {
-		return nil, nil, fmt.Errorf("failed to init cluster client, does your kube config have a current-context set?: %w", err)
+	var kubeContextDecorator config.KubeContextDecorator
+	if options.clusterClient != nil {
+		kubeContextDecorator = config.StaticClusterClient(options.clusterClient)
+	} else {
+		var err error
+		kubeContextDecorator, err = kubeconfig.NewKubeConfigContextManager(ctx, kubeConfigOptions...)
+		if err != nil {
+			return nil, nil, fmt.Errorf("failed to init cluster client, does your kube config have a current-context set?: %w", err)
+		}
 	}
+	clusterClient := kubeContextDecorator.ClusterClient()
 
 	if options.EnableOpenCensus {
 		if err := enableOpenCensus(); err != nil {
@@ -267,19 +420,24 @@ func (r *Runner) initAPI(ctx context.Context, logger log.Logger, options Options
 		Time:    options.BuildInfo.Time,
 	}
 
+	restConfigOptions := cluster.RESTConfigOptions{
+		QPS:       options.ClientQPS,
+		Burst:     options.ClientBurst,
+		UserAgent: options.UserAgent,
+	}
 	dashConfig := config.NewLiveConfig(
-		clusterClient,
+		kubeContextDecorator,
 		crdWatcher,
-		options.KubeConfig,
 		logger,
 		moduleManager,
 		appObjectStore,
 		errorStore,
 		pluginManager,
 		portForwarder,
-		options.Context,
 		restConfigOptions,
-		buildInfo)
+		buildInfo,
+		false,
+	)
 
 	pluginManager.SetOctantClient(dashConfig)
 
@@ -335,7 +493,7 @@ func initPortForwarder(ctx context.Context, client cluster.ClientInterface, appO
 }
 
 type moduleOptions struct {
-	clusterClient  *cluster.Cluster
+	clusterClient  cluster.ClientInterface
 	crdWatcher     config.CRDWatcher
 	namespace      string
 	logger         log.Logger
@@ -390,8 +548,7 @@ func initModules(ctx context.Context, dashConfig config.Dash, namespace string,
 	}
 
 	configurationOptions := configuration.Options{
-		DashConfig:     dashConfig,
-		KubeConfigPath: dashConfig.KubeConfigPath(),
+		DashConfig: dashConfig,
 	}
 	configurationModule := configuration.New(ctx, configurationOptions)
 
diff --git a/pkg/dash/fake/mock_watcher_config.go b/pkg/dash/fake/mock_watcher_config.go
index 2e9dcdaa..3eb3780d 100644
--- a/pkg/dash/fake/mock_watcher_config.go
+++ b/pkg/dash/fake/mock_watcher_config.go
@@ -34,18 +34,18 @@ func (m *MockWatcherConfig) EXPECT() *MockWatcherConfigMockRecorder {
 	return m.recorder
 }
 
-// ContextName mocks base method
-func (m *MockWatcherConfig) ContextName() string {
+// CurrentContext mocks base method
+func (m *MockWatcherConfig) CurrentContext() string {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ContextName")
+	ret := m.ctrl.Call(m, "CurrentContext")
 	ret0, _ := ret[0].(string)
 	return ret0
 }
 
-// ContextName indicates an expected call of ContextName
-func (mr *MockWatcherConfigMockRecorder) ContextName() *gomock.Call {
+// CurrentContext indicates an expected call of CurrentContext
+func (mr *MockWatcherConfigMockRecorder) CurrentContext() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContextName", reflect.TypeOf((*MockWatcherConfig)(nil).ContextName))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentContext", reflect.TypeOf((*MockWatcherConfig)(nil).CurrentContext))
 }
 
 // UseContext mocks base method
@@ -61,3 +61,17 @@ func (mr *MockWatcherConfigMockRecorder) UseContext(arg0, arg1 interface{}) *gom
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseContext", reflect.TypeOf((*MockWatcherConfig)(nil).UseContext), arg0, arg1)
 }
+
+// UseFSContext mocks base method
+func (m *MockWatcherConfig) UseFSContext(arg0 context.Context) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UseFSContext", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// UseFSContext indicates an expected call of UseFSContext
+func (mr *MockWatcherConfigMockRecorder) UseFSContext(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseFSContext", reflect.TypeOf((*MockWatcherConfig)(nil).UseFSContext), arg0)
+}
diff --git a/pkg/dash/watcher.go b/pkg/dash/watcher.go
index 00e032b4..979593b1 100644
--- a/pkg/dash/watcher.go
+++ b/pkg/dash/watcher.go
@@ -77,7 +77,8 @@ func ConfigWatcherFileWatcher(fw FileWatcher) ConfigWatcherOption {
 
 // WatcherConfig is an interface with configuration for ConfigWatcher.
 type WatcherConfig interface {
-	ContextName() string
+	CurrentContext() string
+	UseFSContext(ctx context.Context) error
 	UseContext(ctx context.Context, name string) error
 }
 
@@ -132,7 +133,7 @@ func (cw *ConfigWatcher) Watch(ctx context.Context) {
 			done = true
 			logger.Infof("shutting down config watcher")
 		case _ = <-cw.FileWatcher.Events():
-			if err := cw.watcherConfig.UseContext(ctx, cw.watcherConfig.ContextName()); err != nil {
+			if err := cw.watcherConfig.UseFSContext(ctx); err != nil {
 				logger.WithErr(err).Errorf("reload config")
 			}
 		case err := <-cw.FileWatcher.Errors():
diff --git a/web/package-lock.json b/web/package-lock.json
index ab00294c..c6db8c09 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -97,62 +97,6 @@
         "worker-plugin": "5.0.0"
       },
       "dependencies": {
-        "@babel/code-frame": {
-          "version": "7.10.4",
-          "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
-          "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
-          "dev": true,
-          "requires": {
-            "@babel/highlight": "^7.10.4"
-          }
-        },
-        "@babel/core": {
-          "version": "7.12.3",
-          "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.3.tgz",
-          "integrity": "sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g==",
-          "dev": true,
-          "requires": {
-            "@babel/code-frame": "^7.10.4",
-            "@babel/generator": "^7.12.1",
-            "@babel/helper-module-transforms": "^7.12.1",
-            "@babel/helpers": "^7.12.1",
-            "@babel/parser": "^7.12.3",
-            "@babel/template": "^7.10.4",
-            "@babel/traverse": "^7.12.1",
-            "@babel/types": "^7.12.1",
-            "convert-source-map": "^1.7.0",
-            "debug": "^4.1.0",
-            "gensync": "^1.0.0-beta.1",
-            "json5": "^2.1.2",
-            "lodash": "^4.17.19",
-            "resolve": "^1.3.2",
-            "semver": "^5.4.1",
-            "source-map": "^0.5.0"
-          },
-          "dependencies": {
-            "json5": {
-              "version": "2.1.3",
-              "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
-              "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
-              "dev": true,
-              "requires": {
-                "minimist": "^1.2.5"
-              }
-            },
-            "semver": {
-              "version": "5.7.1",
-              "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-              "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-              "dev": true
-            },
-            "source-map": {
-              "version": "0.5.7",
-              "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
-              "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
-              "dev": true
-            }
-          }
-        },
         "@babel/generator": {
           "version": "7.12.1",
           "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.1.tgz",
@@ -172,17 +116,6 @@
             }
           }
         },
-        "@babel/highlight": {
-          "version": "7.10.4",
-          "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
-          "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
-          "dev": true,
-          "requires": {
-            "@babel/helper-validator-identifier": "^7.10.4",
-            "chalk": "^2.0.0",
-            "js-tokens": "^4.0.0"
-          }
-        },
         "@babel/runtime": {
           "version": "7.12.1",
           "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.1.tgz",
@@ -229,12 +162,6 @@
             }
           }
         },
-        "core-js": {
-          "version": "3.6.5",
-          "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz",
-          "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==",
-          "dev": true
-        },
         "glob": {
           "version": "7.1.6",
           "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
@@ -653,24 +580,25 @@
       "dev": true
     },
     "@babel/core": {
-      "version": "7.12.10",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz",
-      "integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==",
+      "version": "7.12.3",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.3.tgz",
+      "integrity": "sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.10.4",
-        "@babel/generator": "^7.12.10",
+        "@babel/generator": "^7.12.1",
         "@babel/helper-module-transforms": "^7.12.1",
-        "@babel/helpers": "^7.12.5",
-        "@babel/parser": "^7.12.10",
-        "@babel/template": "^7.12.7",
-        "@babel/traverse": "^7.12.10",
-        "@babel/types": "^7.12.10",
+        "@babel/helpers": "^7.12.1",
+        "@babel/parser": "^7.12.3",
+        "@babel/template": "^7.10.4",
+        "@babel/traverse": "^7.12.1",
+        "@babel/types": "^7.12.1",
         "convert-source-map": "^1.7.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.1",
         "json5": "^2.1.2",
         "lodash": "^4.17.19",
+        "resolve": "^1.3.2",
         "semver": "^5.4.1",
         "source-map": "^0.5.0"
       },
@@ -684,17 +612,6 @@
             "@babel/highlight": "^7.10.4"
           }
         },
-        "@babel/generator": {
-          "version": "7.12.10",
-          "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.10.tgz",
-          "integrity": "sha512-6mCdfhWgmqLdtTkhXjnIz0LcdVCd26wS2JXRtj2XY0u5klDsXBREA/pG5NVOuVnF2LUrBGNFtQkIqqTbblg0ww==",
-          "dev": true,
-          "requires": {
-            "@babel/types": "^7.12.10",
-            "jsesc": "^2.5.1",
-            "source-map": "^0.5.0"
-          }
-        },
         "@babel/highlight": {
           "version": "7.10.4",
           "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
@@ -706,51 +623,6 @@
             "js-tokens": "^4.0.0"
           }
         },
-        "@babel/parser": {
-          "version": "7.12.10",
-          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.10.tgz",
-          "integrity": "sha512-PJdRPwyoOqFAWfLytxrWwGrAxghCgh/yTNCYciOz8QgjflA7aZhECPZAa2VUedKg2+QMWkI0L9lynh2SNmNEgA==",
-          "dev": true
-        },
-        "@babel/template": {
-          "version": "7.12.7",
-          "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz",
-          "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==",
-          "dev": true,
-          "requires": {
-            "@babel/code-frame": "^7.10.4",
-            "@babel/parser": "^7.12.7",
-            "@babel/types": "^7.12.7"
-          }
-        },
-        "@babel/traverse": {
-          "version": "7.12.10",
-          "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz",
-          "integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==",
-          "dev": true,
-          "requires": {
-            "@babel/code-frame": "^7.10.4",
-            "@babel/generator": "^7.12.10",
-            "@babel/helper-function-name": "^7.10.4",
-            "@babel/helper-split-export-declaration": "^7.11.0",
-            "@babel/parser": "^7.12.10",
-            "@babel/types": "^7.12.10",
-            "debug": "^4.1.0",
-            "globals": "^11.1.0",
-            "lodash": "^4.17.19"
-          }
-        },
-        "@babel/types": {
-          "version": "7.12.10",
-          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.10.tgz",
-          "integrity": "sha512-sf6wboJV5mGyip2hIpDSKsr80RszPinEFjsHTalMxZAZkoQ2/2yQzxlcFN52SJqsyPfLtPmenL4g2KB3KJXPDw==",
-          "dev": true,
-          "requires": {
-            "@babel/helper-validator-identifier": "^7.10.4",
-            "lodash": "^4.17.19",
-            "to-fast-properties": "^2.0.0"
-          }
-        },
         "semver": {
           "version": "5.7.1",
           "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -9647,9 +9519,9 @@
       }
     },
     "core-js": {
-      "version": "3.8.1",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.1.tgz",
-      "integrity": "sha512-9Id2xHY1W7m8hCl8NkhQn5CufmF/WuR30BTRewvCXc1aZd3kMECwNZ69ndLbekKfakw9Rf2Xyc+QR6E7Gg+obg=="
+      "version": "3.6.5",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz",
+      "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA=="
     },
     "core-js-compat": {
       "version": "3.8.0",
@@ -16697,9 +16569,9 @@
       }
     },
     "marked": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmjs.org/marked/-/marked-1.2.6.tgz",
-      "integrity": "sha512-7vVuSEZ8g/HH3hK/BH/+7u/NJj7x9VY4EHzujLDcqAQLiOUeFJYAsfSAyoWtR17lKrx7b08qyIno4lffwrzTaA=="
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-1.2.4.tgz",
+      "integrity": "sha512-6x5TFGCTKSQBLTZtOburGxCxFEBJEGYVLwCMTBCxzvyuisGcC20UNzDSJhCr/cJ/Kmh6ulfJm10g6WWEAJ3kvg=="
     },
     "matcher": {
       "version": "3.0.0",


Test patch:
diff --git a/internal/api/websocket_state_test.go b/internal/api/websocket_state_test.go
index 6d4f2b89..739f4a95 100644
--- a/internal/api/websocket_state_test.go
+++ b/internal/api/websocket_state_test.go
@@ -252,6 +252,20 @@ func TestWebsocketState_AddFilter(t *testing.T) {
 	assert.Equal(t, expected, got)
 }
 
+func TestWebSocketState_SetContext(t *testing.T) {
+	mocks := newWebsocketStateMocks(t, "default")
+	defer mocks.finish()
+
+	contextName := "fakeContext"
+	mocks.dashConfig.EXPECT().SetContextChosenInUI(gomock.Eq(true))
+	mocks.dashConfig.EXPECT().UseContext(context.TODO(), contextName)
+	mocks.dashConfig.EXPECT().DefaultNamespace().Return("defaultNamespace")
+	mocks.wsClient.EXPECT().Send(gomock.Any()).AnyTimes()
+
+	s := mocks.factory()
+	s.SetContext(contextName)
+}
+
 type websocketStateMocks struct {
 	controller       *gomock.Controller
 	module           *moduleFake.MockModule
diff --git a/internal/cluster/cluster_test.go b/internal/cluster/cluster_test.go
deleted file mode 100644
index ecb9398a..00000000
--- a/internal/cluster/cluster_test.go
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
-Copyright (c) 2019 the Octant contributors. All Rights Reserved.
-SPDX-License-Identifier: Apache-2.0
-*/
-
-package cluster
-
-import (
-	"context"
-	"path/filepath"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func Test_FromKubeConfig(t *testing.T) {
-	kubeConfig := filepath.Join("testdata", "kubeconfig.yaml")
-	config := RESTConfigOptions{}
-
-	_, err := FromKubeConfig(context.TODO(), kubeConfig, "", "", []string{}, config)
-	require.NoError(t, err)
-}
diff --git a/internal/config/dash_test.go b/internal/config/dash_test.go
index ee5b0364..d124305f 100644
--- a/internal/config/dash_test.go
+++ b/internal/config/dash_test.go
@@ -3,7 +3,7 @@ Copyright (c) 2019 the Octant contributors. All Rights Reserved.
 SPDX-License-Identifier: Apache-2.0
 */
 
-package config
+package config_test
 
 import (
 	"context"
@@ -14,10 +14,14 @@ import (
 	"github.com/stretchr/testify/require"
 	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 
+	. "github.com/vmware-tanzu/octant/internal/config"
+
 	"github.com/vmware-tanzu/octant/internal/cluster"
 	clusterFake "github.com/vmware-tanzu/octant/internal/cluster/fake"
+	configFake "github.com/vmware-tanzu/octant/internal/config/fake"
 	internalErr "github.com/vmware-tanzu/octant/internal/errors"
 	"github.com/vmware-tanzu/octant/internal/log"
+	"github.com/vmware-tanzu/octant/internal/module"
 	moduleFake "github.com/vmware-tanzu/octant/internal/module/fake"
 	portForwardFake "github.com/vmware-tanzu/octant/internal/portforward/fake"
 	"github.com/vmware-tanzu/octant/internal/testutil"
@@ -85,18 +89,26 @@ func TestLiveConfig(t *testing.T) {
 	assert.NoError(t, err)
 	pluginManager := pluginFake.NewMockManagerInterface(controller)
 	portForwarder := portForwardFake.NewMockPortForwarder(controller)
-	kubeConfigPath := "/path"
 	buildInfo := BuildInfo{}
 
 	objectStore.EXPECT().
 		RegisterOnUpdate(gomock.Any())
 
-	contextName := "context-name"
 	restConfigOptions := cluster.RESTConfigOptions{}
 
-	config := NewLiveConfig(clusterClient, crdWatcher, kubeConfigPath, logger, moduleManager, objectStore,
-		errorStore, pluginManager, portForwarder,
-		contextName, restConfigOptions, buildInfo)
+	config := NewLiveConfig(
+		StaticClusterClient(clusterClient),
+		crdWatcher,
+		logger,
+		moduleManager,
+		objectStore,
+		errorStore,
+		pluginManager,
+		portForwarder,
+		restConfigOptions,
+		buildInfo,
+		false,
+	)
 
 	assert.NoError(t, config.Validate())
 	assert.Equal(t, clusterClient, config.ClusterClient())
@@ -111,6 +123,126 @@ func TestLiveConfig(t *testing.T) {
 	assert.Equal(t, "/pod", objectPath)
 }
 
+func TestLiveConfig_UseContext_WithContextChosenByUISetToTrue(t *testing.T) {
+	controller := gomock.NewController(t)
+	defer controller.Finish()
+
+	logger := log.NopLogger()
+
+	crdWatcher := stubCRDWatcher{}
+
+	moduleManager := moduleFake.NewMockManagerInterface(controller)
+
+	objectStore := objectStoreFake.NewMockStore(controller)
+	errorStore, err := internalErr.NewErrorStore()
+	assert.NoError(t, err)
+	pluginManager := pluginFake.NewMockManagerInterface(controller)
+	portForwarder := portForwardFake.NewMockPortForwarder(controller)
+	buildInfo := BuildInfo{}
+
+	objectStore.EXPECT().
+		RegisterOnUpdate(gomock.Any())
+
+	restConfigOptions := cluster.RESTConfigOptions{}
+
+	contextDecorator := configFake.NewMockKubeContextDecorator(controller)
+
+	config := NewLiveConfig(
+		contextDecorator,
+		crdWatcher,
+		logger,
+		moduleManager,
+		objectStore,
+		errorStore,
+		pluginManager,
+		portForwarder,
+		restConfigOptions,
+		buildInfo,
+		true, // contextChosenInUI
+	)
+
+	objectStore.EXPECT().UpdateClusterClient(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
+	moduleManager.EXPECT().Modules().Return(make([]module.Module, 0)).AnyTimes()
+	pluginManager.EXPECT().SetOctantClient(gomock.Eq(config)).AnyTimes()
+	contextDecorator.EXPECT().ClusterClient().AnyTimes()
+
+	newContext := ""
+	currentContext := "socketContext"
+	// Since UseContext is called with "" with contextChosenInUI = true, our context should stay as currentContext
+	moduleManager.EXPECT().UpdateContext(gomock.Any(), currentContext).Return(nil)
+	contextDecorator.EXPECT().CurrentContext().Return(currentContext).AnyTimes()
+	contextDecorator.EXPECT().SwitchContext(gomock.Any(), gomock.Eq(currentContext)).Return(nil)
+
+	config.UseContext(context.TODO(), newContext)
+
+	newContext = "newContext"
+	// Since UseContext is called with "newContext" with contextChosenInUI = true, our context should change to newContext
+	moduleManager.EXPECT().UpdateContext(gomock.Any(), newContext).Return(nil)
+	contextDecorator.EXPECT().SwitchContext(gomock.Any(), gomock.Eq(newContext)).Return(nil)
+
+	config.UseContext(context.TODO(), newContext)
+}
+
+func TestLiveConfig_UseContext_WithContextChosenByUISetToFalse(t *testing.T) {
+	controller := gomock.NewController(t)
+	defer controller.Finish()
+
+	logger := log.NopLogger()
+
+	crdWatcher := stubCRDWatcher{}
+
+	moduleManager := moduleFake.NewMockManagerInterface(controller)
+
+	objectStore := objectStoreFake.NewMockStore(controller)
+	errorStore, err := internalErr.NewErrorStore()
+	assert.NoError(t, err)
+	pluginManager := pluginFake.NewMockManagerInterface(controller)
+	portForwarder := portForwardFake.NewMockPortForwarder(controller)
+	buildInfo := BuildInfo{}
+
+	objectStore.EXPECT().
+		RegisterOnUpdate(gomock.Any())
+
+	restConfigOptions := cluster.RESTConfigOptions{}
+
+	contextDecorator := configFake.NewMockKubeContextDecorator(controller)
+
+	config := NewLiveConfig(
+		contextDecorator,
+		crdWatcher,
+		logger,
+		moduleManager,
+		objectStore,
+		errorStore,
+		pluginManager,
+		portForwarder,
+		restConfigOptions,
+		buildInfo,
+		false, // contextChosenInUI
+	)
+
+	objectStore.EXPECT().UpdateClusterClient(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
+	moduleManager.EXPECT().Modules().Return(make([]module.Module, 0)).AnyTimes()
+	pluginManager.EXPECT().SetOctantClient(gomock.Eq(config)).AnyTimes()
+	contextDecorator.EXPECT().ClusterClient().AnyTimes()
+
+	newContext := ""
+	currentContext := "socketContext"
+	// Since UseContext is called with "" with contextChosenInUI = false, our context should change to newContext
+	moduleManager.EXPECT().UpdateContext(gomock.Any(), newContext).Return(nil)
+	contextDecorator.EXPECT().CurrentContext().Return(currentContext).AnyTimes()
+	contextDecorator.EXPECT().SwitchContext(gomock.Any(), gomock.Eq(newContext)).Return(nil)
+
+	config.UseContext(context.TODO(), newContext)
+
+	newContext = "newContext"
+	// Since UseContext is called with "newContext" with contextChosenInUI = false, our context should change to newContext
+	moduleManager.EXPECT().UpdateContext(gomock.Any(), newContext).Return(nil)
+	contextDecorator.EXPECT().SwitchContext(gomock.Any(), gomock.Eq(newContext)).Return(nil)
+
+	config.UseContext(context.TODO(), newContext)
+}
+
 type stubCRDWatcher struct{}
 
 var _ CRDWatcher = (*stubCRDWatcher)(nil)
diff --git a/internal/event/context_test.go b/internal/event/context_test.go
index 46c590c2..07deea27 100644
--- a/internal/event/context_test.go
+++ b/internal/event/context_test.go
@@ -16,32 +16,17 @@ import (
 	"github.com/stretchr/testify/require"
 
 	dashConfigFake "github.com/vmware-tanzu/octant/internal/config/fake"
-	"github.com/vmware-tanzu/octant/internal/kubeconfig"
-	"github.com/vmware-tanzu/octant/internal/kubeconfig/fake"
 )
 
 func Test_kubeContextGenerator(t *testing.T) {
+	currentContext := "current-context"
 	controller := gomock.NewController(t)
 	defer controller.Finish()
-
-	kc := &kubeconfig.KubeConfig{
-		CurrentContext: "current-context",
-	}
-
-	loader := fake.NewMockLoader(controller)
-	loader.EXPECT().
-		Load("/path").
-		Return(kc, nil)
-
-	configLoaderFuncOpt := func(x *ContextsGenerator) {
-		x.ConfigLoader = loader
-	}
-
 	dashConfig := dashConfigFake.NewMockDash(controller)
-	dashConfig.EXPECT().KubeConfigPath().Return("/path")
-	dashConfig.EXPECT().ContextName().Return("")
+	dashConfig.EXPECT().CurrentContext().Return(currentContext)
+	dashConfig.EXPECT().Contexts().Return(nil)
 
-	kgc := NewContextsGenerator(dashConfig, configLoaderFuncOpt)
+	kgc := NewContextsGenerator(dashConfig)
 
 	assert.Equal(t, "kubeConfig", kgc.Name())
 
@@ -52,7 +37,7 @@ func Test_kubeContextGenerator(t *testing.T) {
 	assert.Equal(t, event.EventTypeKubeConfig, e.Type)
 
 	resp := kubeContextsResponse{
-		CurrentContext: kc.CurrentContext,
+		CurrentContext: currentContext,
 	}
 
 	assert.Equal(t, resp, e.Data)
diff --git a/internal/kubeconfig/kubeconfig_test.go b/internal/kubeconfig/kubeconfig_test.go
index 9595df9e..85051b6d 100644
--- a/internal/kubeconfig/kubeconfig_test.go
+++ b/internal/kubeconfig/kubeconfig_test.go
@@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0
 package kubeconfig
 
 import (
-	"fmt"
+	"context"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -15,8 +15,58 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+
+	"github.com/vmware-tanzu/octant/internal/cluster"
 )
 
+func Test_NewKubeConfigs(t *testing.T) {
+	kubeConfig := filepath.Join("testdata", "kubeconfig.yaml")
+	config := cluster.RESTConfigOptions{}
+
+	_, err := NewKubeConfigContextManager(
+		context.TODO(),
+		WithKubeConfigList(kubeConfig),
+		FromClusterOption(cluster.WithRESTConfigOptions(config)),
+	)
+	require.NoError(t, err)
+}
+
+func Test_SwitchContextUpdatesCurrentContext(t *testing.T) {
+	kubeConfigs, err := NewKubeConfigContextManager(
+		context.TODO(),
+		WithKubeConfigList(filepath.Join("testdata", "kubeconfig.yaml")),
+	)
+	require.NoError(t, err)
+
+	kubeConfigs.SwitchContext(context.TODO(), "other-context")
+
+	require.Equal(t, "other-context", kubeConfigs.CurrentContext())
+}
+
+func Test_SwitchContextToEmptyUpdatesCurrentContextFromFileSystem(t *testing.T) {
+	kubeConfigs, err := NewKubeConfigContextManager(
+		context.TODO(),
+		WithKubeConfigList(filepath.Join("testdata", "kubeconfig.yaml")),
+	)
+	require.NoError(t, err)
+
+	kubeConfigs.SwitchContext(context.TODO(), "")
+
+	require.Equal(t, "my-cluster", kubeConfigs.CurrentContext())
+}
+
+func Test_SwitchContextUpdatesClientNamespace(t *testing.T) {
+	kubeConfigs, err := NewKubeConfigContextManager(
+		context.TODO(),
+		WithKubeConfigList(filepath.Join("testdata", "kubeconfig.yaml")),
+	)
+	require.NoError(t, err)
+
+	kubeConfigs.SwitchContext(context.TODO(), "other-context")
+
+	require.Equal(t, "non-default", kubeConfigs.ClusterClient().DefaultNamespace())
+}
+
 func TestFSLoader_Load(t *testing.T) {
 	dir, err := ioutil.TempDir("", "loader-test")
 	require.NoError(t, err)
@@ -34,19 +84,16 @@ func TestFSLoader_Load(t *testing.T) {
 		paths = append(paths, kubeConfigPath)
 	}
 
-	l := NewFSLoader()
-
-	kc, err := l.Load(strings.Join(paths, string(os.PathListSeparator)))
+	kc, err := NewKubeConfigContextManager(
+		context.TODO(),
+		WithKubeConfigList(strings.Join(paths, string(os.PathListSeparator))),
+	)
 	require.NoError(t, err)
 
-	expected := &KubeConfig{
-		Contexts: []Context{
-			{Name: "dev-frontend"},
-			{Name: "dev-storage"},
-			{Name: "exp-scratch"},
-		},
-		CurrentContext: "dev-frontend",
-	}
-
-	assert.Equal(t, fmt.Sprint(*expected), fmt.Sprint(*kc))
+	assert.Equal(t, "dev-frontend", kc.CurrentContext())
+	assert.Equal(t, []Context{
+		{Name: "dev-frontend"},
+		{Name: "dev-storage"},
+		{Name: "exp-scratch"},
+	}, kc.Contexts())
 }
diff --git a/internal/kubeconfig/testdata/kubeconfig-1.yaml b/internal/kubeconfig/testdata/kubeconfig-1.yaml
index f396bf3d..13f7d82c 100644
--- a/internal/kubeconfig/testdata/kubeconfig-1.yaml
+++ b/internal/kubeconfig/testdata/kubeconfig-1.yaml
@@ -4,8 +4,12 @@ preferences: {}
 
 clusters:
   - cluster:
+      api-version: v1
+      server: https://cluster:4443
     name: development
   - cluster:
+      api-version: v1
+      server: https://cluster:4443
     name: scratch
 
 users:
@@ -14,7 +18,9 @@ users:
 
 contexts:
   - context:
+      cluster: development
     name: dev-frontend
   - context:
+      cluster: development
     name: dev-storage
 current-context: dev-frontend
diff --git a/internal/kubeconfig/testdata/kubeconfig-2.yaml b/internal/kubeconfig/testdata/kubeconfig-2.yaml
index 0f0796b7..649cf793 100644
--- a/internal/kubeconfig/testdata/kubeconfig-2.yaml
+++ b/internal/kubeconfig/testdata/kubeconfig-2.yaml
@@ -4,8 +4,12 @@ preferences: {}
 
 clusters:
   - cluster:
+      api-version: v1
+      server: https://cluster:4443
     name: development
   - cluster:
+      api-version: v1
+      server: https://cluster:4443
     name: scratch
 
 users:
@@ -14,5 +18,6 @@ users:
 
 contexts:
   - context:
+      cluster: scratch
     name: exp-scratch
 current-context: exp-scratch
diff --git a/internal/cluster/testdata/kubeconfig.yaml b/internal/kubeconfig/testdata/kubeconfig.yaml
similarity index 70%
rename from internal/cluster/testdata/kubeconfig.yaml
rename to internal/kubeconfig/testdata/kubeconfig.yaml
index 77ac54ff..9201c4ee 100644
--- a/internal/cluster/testdata/kubeconfig.yaml
+++ b/internal/kubeconfig/testdata/kubeconfig.yaml
@@ -12,8 +12,13 @@ contexts:
     namespace: default
     user: user
   name: my-cluster
+- context:
+    cluster: my-cluster
+    namespace: non-default
+    user: user
+  name: other-context
 kind: Config
 users:
 - name: user
   user:
-    token: my-token
\ No newline at end of file
+    token: my-token
diff --git a/pkg/dash/dash_test.go b/pkg/dash/dash_test.go
index dcd9e4c8..1d31e579 100644
--- a/pkg/dash/dash_test.go
+++ b/pkg/dash/dash_test.go
@@ -22,15 +22,21 @@ import (
 	"testing"
 	"time"
 
+	v1 "k8s.io/api/authorization/v1"
+
+	"github.com/golang/mock/gomock"
 	"github.com/gorilla/websocket"
 	"github.com/spf13/afero"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 
-	"github.com/vmware-tanzu/octant/internal/log"
+	"github.com/vmware-tanzu/octant/internal/cluster"
+	internalLog "github.com/vmware-tanzu/octant/internal/log"
 	"github.com/vmware-tanzu/octant/pkg/event"
 
-	pkglog "github.com/vmware-tanzu/octant/pkg/log"
+	clusterFake "github.com/vmware-tanzu/octant/internal/cluster/fake"
+	"github.com/vmware-tanzu/octant/pkg/log"
 )
 
 func TestRunner_ValidateKubeconfig(t *testing.T) {
@@ -86,7 +92,7 @@ func TestRunner_ValidateKubeconfig(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			logger := log.NopLogger()
+			logger := internalLog.NopLogger()
 			path, err := ValidateKubeConfig(logger, test.fileList, fs)
 			if test.isErr {
 				require.Error(t, err)
@@ -147,25 +153,69 @@ func TestNewRunnerLoadsValidKubeConfigFilteringNonexistent(t *testing.T) {
 	stubRiceBox("dist/dash-frontend")
 	kubeConfig := tempFile(makeKubeConfig("test-context", srv.URL))
 	defer os.Remove(kubeConfig.Name())
+	listener := NewInMemoryListener()
 
+	cancel, err := makeRunner(
+		internalLog.NopLogger(),
+		WithKubeConfig(strings.Join(
+			[]string{
+				"/non/existent/kubeconfig",
+				kubeConfig.Name(),
+			},
+			string(filepath.ListSeparator),
+		)),
+		WithListener(listener),
+	)
+	require.NoError(t, err)
+	defer cancel()
+	kubeConfigEvent, err := waitForEventOfType(listener, event.EventTypeKubeConfig)
+	require.NoError(t, err)
+
+	require.Equal(t, "test-context", kubeConfigEvent.Data["currentContext"].(string))
+}
+
+func TestNewRunnerUsesClusterClient(t *testing.T) {
+	namespace := "foobar-banana"
+	controller := gomock.NewController(t)
+	defer controller.Finish()
+	clusterClient := mockClusterClientReturningNamespace(controller, namespace)
 	listener := NewInMemoryListener()
-	cancel, _ := makeRunner(
-		Options{
-			KubeConfig: strings.Join(
-				[]string{
-					"/non/existent/kubeconfig",
-					kubeConfig.Name(),
-				},
-				string(filepath.ListSeparator),
-			),
-			Listener: listener,
-		},
-		log.NopLogger(),
+
+	logger := internalLog.NopLogger()
+	cancel, err := makeRunner(
+		logger,
+		WithClusterClient(clusterClient),
+		WithListener(listener),
 	)
+	require.NoError(t, err)
 	defer cancel()
-	kubeConfigEvent := waitForKubeConfigEvent(listener)
+	namespacesEvent, err := waitForEventOfType(listener, event.EventTypeNamespaces)
+	require.NoError(t, err)
 
-	require.Equal(t, "test-context", kubeConfigEvent.Data.CurrentContext)
+	require.Equal(t, []interface{}{namespace}, namespacesEvent.Data["namespaces"].([]interface{}))
+}
+
+func mockClusterClientReturningNamespace(controller *gomock.Controller, namespace string) cluster.ClientInterface {
+	nsClient := clusterFake.NewMockNamespaceInterface(controller)
+	nsClient.EXPECT().InitialNamespace().Return(namespace)
+	nsClient.EXPECT().Names().Return([]string{namespace}, nil)
+	nsClient.EXPECT().ProvidedNamespaces().Return([]string{namespace})
+	ssar := clusterFake.NewMockSelfSubjectAccessReviewInterface(controller)
+	ssar.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(&v1.SelfSubjectAccessReview{}, nil)
+	authClient := clusterFake.NewMockAuthorizationV1Interface(controller)
+	authClient.EXPECT().SelfSubjectAccessReviews().Return(ssar).MinTimes(1)
+	k8sClient := clusterFake.NewMockKubernetesInterface(controller)
+	k8sClient.EXPECT().AuthorizationV1().Return(authClient).MinTimes(1)
+	clusterClient := clusterFake.NewMockClientInterface(controller)
+	clusterClient.EXPECT().NamespaceClient().Return(nsClient, nil).MinTimes(1)
+	clusterClient.EXPECT().RESTClient().Return(nil, nil)
+	clusterClient.EXPECT().RESTConfig().Return(nil)
+	clusterClient.EXPECT().Resource(gomock.Any()).
+		Return(schema.GroupVersionResource{}, false, nil).
+		MinTimes(1)
+	clusterClient.EXPECT().KubernetesClient().Return(k8sClient, nil).MinTimes(1)
+	clusterClient.EXPECT().DefaultNamespace().Return(namespace)
+	return clusterClient
 }
 
 func TestNewRunnerRunsLoadingAPIWhenStartedWithoutKubeConfig(t *testing.T) {
@@ -174,7 +224,8 @@ func TestNewRunnerRunsLoadingAPIWhenStartedWithoutKubeConfig(t *testing.T) {
 	stubRiceBox("dist/dash-frontend")
 
 	listener := NewInMemoryListener()
-	cancel, _ := makeRunner(Options{Listener: listener}, log.NopLogger())
+	cancel, err := makeRunner(internalLog.NopLogger(), WithListener(listener))
+	require.NoError(t, err)
 	defer cancel()
 	kubeConfig := makeKubeConfig("test-context", srv.URL)
 	websocketWrite(
@@ -190,24 +241,21 @@ func TestNewRunnerRunsLoadingAPIWhenStartedWithoutKubeConfig(t *testing.T) {
 			break
 		}
 	}
-	kubeConfigEvent := waitForKubeConfigEvent(listener)
+	kubeConfigEvent, err := waitForEventOfType(listener, event.EventTypeKubeConfig)
+	require.NoError(t, err)
 
-	require.Equal(t, "test-context", kubeConfigEvent.Data.CurrentContext)
+	require.Equal(t, "test-context", kubeConfigEvent.Data["currentContext"].(string))
 }
 
 func TestNewRunnerShutsDownPluginsWhenStoppedBeforeReceivingKubeConfig(t *testing.T) {
 	stubRiceBox("dist/dash-frontend")
 	listener := NewInMemoryListener()
 	shutdownCh := make(chan bool)
-	options := Options{
-		Listener: listener,
-	}
-	logger := log.NopLogger()
 	ctx, cancel := context.WithCancel(context.Background())
-	runner, err := NewRunner(ctx, logger, options)
+	runner, err := NewRunner(ctx, internalLog.NopLogger(), WithListener(listener))
 	require.NoError(t, err)
 
-	go runner.Start(options, make(chan bool), shutdownCh)
+	go runner.Start(make(chan bool), shutdownCh)
 	cancel()
 
 	select {
@@ -266,31 +314,34 @@ current-context: %s
 `, currentContext, serverAddr, currentContext))
 }
 
-func waitForKubeConfigEvent(listener *inMemoryListener) kubeConfigEvent {
-	var message kubeConfigEvent
+func waitForEventOfType(listener *inMemoryListener, eventType event.EventType) (streamingEvent, error) {
+	var message streamingEvent
 	dialer := websocket.DefaultDialer
 	dialer.NetDial = listener.Dial
-	wsConn, resp, err := dialer.Dial("ws://127.0.0.1:7777/api/v1/stream", nil)
+	wsConn, _, err := dialer.Dial("ws://127.0.0.1:7777/api/v1/stream", nil)
 	if err != nil {
-		fmt.Println(resp)
-		panic(err)
+		return message, err
 	}
 	defer wsConn.Close()
 	for {
-		msgBytes, _ := readNextMessage(wsConn)
-		json.Unmarshal(msgBytes, &message)
-		if message.Type == event.EventTypeKubeConfig {
+		msgBytes, err := readNextMessage(wsConn)
+		if err != nil {
+			return message, err
+		}
+		err = json.Unmarshal(msgBytes, &message)
+		if err != nil {
+			return message, err
+		}
+		if message.Type == eventType {
 			break
 		}
 	}
-	return message
+	return message, nil
 }
 
-type kubeConfigEvent struct {
-	Type event.EventType `json:"type"`
-	Data struct {
-		CurrentContext string `json:"currentContext"`
-	} `json:"data"`
+type streamingEvent struct {
+	Type event.EventType        `json:"type"`
+	Data map[string]interface{} `json:"data"`
 }
 
 func tempFile(contents []byte) *os.File {
@@ -300,13 +351,13 @@ func tempFile(contents []byte) *os.File {
 	return tmpFile
 }
 
-func makeRunner(options Options, logger pkglog.Logger) (context.CancelFunc, error) {
+func makeRunner(logger log.Logger, opts ...RunnerOption) (context.CancelFunc, error) {
 	ctx, cancel := context.WithCancel(context.Background())
-	runner, err := NewRunner(ctx, logger, options)
+	runner, err := NewRunner(ctx, logger, opts...)
 	if err != nil {
 		return cancel, err
 	}
-	go runner.Start(options, make(chan bool), make(chan bool))
+	go runner.Start(make(chan bool), make(chan bool), opts...)
 	return cancel, nil
 }
 
diff --git a/pkg/dash/watcher_test.go b/pkg/dash/watcher_test.go
index eaa23205..536dcdf6 100644
--- a/pkg/dash/watcher_test.go
+++ b/pkg/dash/watcher_test.go
@@ -70,11 +70,9 @@ func TestConfigWatcher_Watch(t *testing.T) {
 
 	watcherConfig := fake.NewMockWatcherConfig(controller)
 
-	watcherConfig.EXPECT().ContextName().Return("name")
-
 	ch := make(chan bool, 1)
-	watcherConfig.EXPECT().UseContext(gomock.Any(), "name").
-		DoAndReturn(func(_ context.Context, _ string) error {
+	watcherConfig.EXPECT().UseFSContext(gomock.Any()).
+		DoAndReturn(func(_ context.Context) error {
 			ch <- true
 			return nil
 		})