Repository: vmware-tanzu/octant
Pull request #1873 (2021-01-25 21:24:42)

Fix mock visitor test race condition
The order appended to `visited` does not matter since it will be sorted later. However, visited still needs to be locked when appending otherwise not all objects are reliably added to the array.

xref: https://github.com/vmware-tanzu/octant/pull/1867/checks?check_run_id=1764506539#step:6:775

This can be reproduced by going to `/internal/objectvisitor` and running `go test -run TestService_Visit -race -count 300`

Signed-off-by: GuessWhoSamFoo <foos@vmware.com>



Patch:
diff --git a/build.go b/build.go
index 35cd2f84..3c353dcc 100644
--- a/build.go
+++ b/build.go
@@ -455,7 +455,7 @@ func removeFakes() {
 				return nil
 			}
 			if info.Name() == "fake" {
-				fakePaths = append(fakePaths, path)
+				fakePaths = append(fakePaths, filepath.Join(path, info.Name()))
 			}
 			return nil
 		})
diff --git a/cmd/octant-electron/main.go b/cmd/octant-electron/main.go
index 90492aa2..f6dc8d83 100644
--- a/cmd/octant-electron/main.go
+++ b/cmd/octant-electron/main.go
@@ -77,9 +77,11 @@ func run(ctx context.Context) error {
 		return fmt.Errorf("failed to create net listener: %w", err)
 	}
 
-	dashOptions := []dash.RunnerOption{
-		dash.WithClientUserAgent("octant-electron"), // TODO: create proper user agent
-		dash.WithListener(listener),
+	dashOptions := dash.Options{
+		DisableClusterOverview: false,
+		EnableOpenCensus:       false,
+		UserAgent:              fmt.Sprintf("octant-electron"), // TODO: create proper user agent
+		Listener:               listener,
 	}
 	shutdownCh := make(chan bool, 1)
 	startupCh := make(chan bool, 1)
@@ -87,13 +89,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(startupCh, shutdownCh, dashOptions...)
+		runner.Start(dashOptions, startupCh, shutdownCh)
 		runCh <- true
 	}()
 
diff --git a/internal/api/websocket_state.go b/internal/api/websocket_state.go
index 32f21ca6..4e662824 100644
--- a/internal/api/websocket_state.go
+++ b/internal/api/websocket_state.go
@@ -348,8 +348,6 @@ 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 fa3e5c07..a1c962ce 100644
--- a/internal/cluster/cluster.go
+++ b/internal/cluster/cluster.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"path/filepath"
 	"time"
 
 	"github.com/pkg/errors"
@@ -30,6 +31,7 @@ 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
@@ -239,85 +241,41 @@ func (c *Cluster) Version() (string, error) {
 	return fmt.Sprint(serverVersion), nil
 }
 
-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
+// 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,
 	}
-}
 
-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)
-		}
+	overrides := &clientcmd.ConfigOverrides{}
+	if contextName != "" {
+		overrides.CurrentContext = contextName
 	}
-	restConfig, err := clientConfig.ClientConfig()
+	cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
+	config, err := cc.ClientConfig()
 	if err != nil {
 		return nil, err
 	}
 
 	var defaultNamespace string
 
-	if options.InitialNamespace == "" {
-		defaultNamespace, _, err = clientConfig.Namespace()
+	if initialNamespace == "" {
+		defaultNamespace, _, err = cc.Namespace()
 		if err != nil {
 			return nil, err
 		}
 	} else {
-		defaultNamespace = options.InitialNamespace
+		defaultNamespace = initialNamespace
 	}
 
 	logger := internalLog.From(ctx)
-	logger.With("client-qps", options.RESTConfigOptions.QPS, "client-burst", options.RESTConfigOptions.Burst).
+	logger.With("client-qps", options.QPS, "client-burst", options.Burst).
 		Debugf("initializing REST client configuration")
 
-	restConfig = withConfigDefaults(restConfig, options.RESTConfigOptions)
+	config = withConfigDefaults(config, options)
 
-	return newCluster(ctx, clientConfig, restConfig, defaultNamespace, options.ProvidedNamespaces)
+	return newCluster(ctx, cc, config, defaultNamespace, 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 31f947a8..60fa2aa9 100644
--- a/internal/commands/dash.go
+++ b/internal/commands/dash.go
@@ -80,24 +80,20 @@ func newOctantCmd(version string, gitCommit string, buildTime string) *cobra.Com
 					Time:    buildTime,
 				}
 
-				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())
+				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,
 				}
 
 				klogVerbosity := viper.GetString("klog-verbosity")
@@ -122,13 +118,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(nil, shutdownCh, options...)
+				runner.Start(options, nil, shutdownCh)
 
 				runCh <- true
 			}()
diff --git a/internal/config/dash.go b/internal/config/dash.go
index 93c6a80d..1ba41eea 100644
--- a/internal/config/dash.go
+++ b/internal/config/dash.go
@@ -8,7 +8,6 @@ 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"
 
@@ -25,7 +24,6 @@ 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 {
@@ -33,38 +31,6 @@ 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)
 
@@ -127,15 +93,11 @@ type Dash interface {
 
 	PortForwarder() portforward.PortForwarder
 
-	SetContextChosenInUI(contextChosen bool)
-
-	UseFSContext(ctx context.Context) error
+	KubeConfigPath() string
 
 	UseContext(ctx context.Context, contextName string) error
 
-	CurrentContext() string
-
-	Contexts() []kubeconfig.Context
+	ContextName() string
 
 	DefaultNamespace() string
 
@@ -146,52 +108,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 {
-	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
+	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
 }
 
 var _ Dash = (*Live)(nil)
 
 // NewLiveConfig creates an instance of Live.
 func NewLiveConfig(
-	kubeContextDecorator KubeContextDecorator,
+	clusterClient cluster.ClientInterface,
 	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{
-		kubeContextDecorator: kubeContextDecorator,
-		crdWatcher:           crdWatcher,
-		logger:               logger,
-		moduleManager:        moduleManager,
-		objectStore:          objectStore,
-		errorStore:           errorStore,
-		pluginManager:        pluginManager,
-		portForwarder:        portForwarder,
-		restConfigOptions:    restConfigOptions,
-		buildInfo:            buildInfo,
-		contextChosenInUI:    contextChosenInUI,
+		clusterClient:      clusterClient,
+		crdWatcher:         crdWatcher,
+		kubeConfigPath:     kubeConfigPath,
+		logger:             logger,
+		moduleManager:      moduleManager,
+		objectStore:        objectStore,
+		errorStore:         errorStore,
+		pluginManager:      pluginManager,
+		portForwarder:      portForwarder,
+		currentContextName: currentContextName,
+		restConfigOptions:  restConfigOptions,
+		buildInfo:          buildInfo,
 	}
 	objectStore.RegisterOnUpdate(func(store store.Store) {
 		l.objectStore = store
@@ -207,7 +169,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.kubeContextDecorator.ClusterClient()
+	return l.clusterClient
 }
 
 // CRDWatcher returns a CRD watcher.
@@ -225,6 +187,11 @@ 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
@@ -240,26 +207,17 @@ 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 {
-	if l.contextChosenInUI && contextName == UseFSContext {
-		contextName = l.CurrentContext()
-	}
-
-	err := l.kubeContextDecorator.SwitchContext(ctx, contextName)
+	// 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 err != nil {
 		return err
 	}
 
-	client := l.kubeContextDecorator.ClusterClient()
+	l.ClusterClient().Close()
+	l.clusterClient = client
+
 	if err := l.objectStore.UpdateClusterClient(ctx, client); err != nil {
 		return err
 	}
@@ -268,6 +226,7 @@ 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() {
@@ -281,14 +240,9 @@ func (l *Live) UseContext(ctx context.Context, contextName string) error {
 	return nil
 }
 
-// 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()
+// ContextName returns the current context name
+func (l *Live) ContextName() string {
+	return l.currentContextName
 }
 
 // DefaultNamespace returns the default namespace for the current cluster..
@@ -298,7 +252,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 2edbbdb7..e7e86345 100644
--- a/internal/config/fake/mock_dash.go
+++ b/internal/config/fake/mock_dash.go
@@ -13,7 +13,6 @@ 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"
@@ -88,32 +87,18 @@ func (mr *MockDashMockRecorder) ClusterClient() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterClient", reflect.TypeOf((*MockDash)(nil).ClusterClient))
 }
 
-// Contexts mocks base method
-func (m *MockDash) Contexts() []kubeconfig.Context {
+// ContextName mocks base method
+func (m *MockDash) ContextName() string {
 	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 *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")
+	ret := m.ctrl.Call(m, "ContextName")
 	ret0, _ := ret[0].(string)
 	return ret0
 }
 
-// CurrentContext indicates an expected call of CurrentContext
-func (mr *MockDashMockRecorder) CurrentContext() *gomock.Call {
+// ContextName indicates an expected call of ContextName
+func (mr *MockDashMockRecorder) ContextName() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentContext", reflect.TypeOf((*MockDash)(nil).CurrentContext))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContextName", reflect.TypeOf((*MockDash)(nil).ContextName))
 }
 
 // DefaultNamespace mocks base method
@@ -144,6 +129,20 @@ 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()
@@ -229,18 +228,6 @@ 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()
@@ -255,20 +242,6 @@ 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
deleted file mode 100644
index 76a635a9..00000000
--- a/internal/config/fake/mock_kubecontextdecorator.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// 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 98ad62c5..9c9419d7 100644
--- a/internal/event/context.go
+++ b/internal/event/context.go
@@ -12,6 +12,9 @@ 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"
 )
@@ -24,21 +27,18 @@ 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 {
-	KubeContextStore KubeContextStore
+	ConfigLoader kubeconfig.Loader
+	DashConfig   config.Dash
 }
 
 var _ octant.Generator = (*ContextsGenerator)(nil)
 
-func NewContextsGenerator(kubeContextStore KubeContextStore, options ...ContextGeneratorOption) *ContextsGenerator {
+func NewContextsGenerator(dashConfig config.Dash, options ...ContextGeneratorOption) *ContextsGenerator {
 	kcg := &ContextsGenerator{
-		KubeContextStore: kubeContextStore,
+		ConfigLoader: kubeconfig.NewFSLoader(),
+		DashConfig:   dashConfig,
 	}
 
 	for _, option := range options {
@@ -49,9 +49,21 @@ func NewContextsGenerator(kubeContextStore KubeContextStore, options ...ContextG
 }
 
 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: g.KubeContextStore.CurrentContext(),
-		Contexts:       g.KubeContextStore.Contexts(),
+		CurrentContext: currentContext,
+		Contexts:       kubeConfig.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
new file mode 100644
index 00000000..f3726713
--- /dev/null
+++ b/internal/kubeconfig/fake/mock_loader.go
@@ -0,0 +1,51 @@
+// 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 16730486..b3263797 100644
--- a/internal/kubeconfig/kubeconfig.go
+++ b/internal/kubeconfig/kubeconfig.go
@@ -6,161 +6,77 @@ 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"
 )
 
-type KubeConfigOption struct {
-	kubeConfigOption func(*kubeConfigOptions)
-	clusterOption    cluster.ClusterOption
-}
+//go:generate mockgen -destination=./fake/mock_loader.go -package=fake github.com/vmware-tanzu/octant/internal/kubeconfig Loader
 
-type kubeConfigOptions struct {
-	KubeConfigList string
-	ContextName    string
+// KubeConfig describes a kube config for dash.
+type KubeConfig struct {
+	Contexts       []Context
+	CurrentContext string
 }
 
-func Noop() KubeConfigOption {
-	return KubeConfigOption{
-		kubeConfigOption: func(*kubeConfigOptions) {},
-	}
+// Context describes a kube config context.
+type Context struct {
+	Name string `json:"name"`
 }
 
-func WithKubeConfigList(kubeConfigList string) KubeConfigOption {
-	return KubeConfigOption{
-		kubeConfigOption: func(kubeConfigOptions *kubeConfigOptions) {
-			kubeConfigOptions.KubeConfigList = kubeConfigList
-		},
-	}
+// Loader is an interface for loading kube config.
+type Loader interface {
+	Load(filename string) (*KubeConfig, error)
 }
 
-func WithContextName(contextName string) KubeConfigOption {
-	return KubeConfigOption{
-		kubeConfigOption: func(kubeConfigOptions *kubeConfigOptions) {
-			kubeConfigOptions.ContextName = contextName
-		},
-	}
-}
+// FSLoaderOpt is an option for configuring FSLoader.
+type FSLoaderOpt func(loader *FSLoader)
 
-func FromClusterOption(clusterOption cluster.ClusterOption) KubeConfigOption {
-	return KubeConfigOption{
-		kubeConfigOption: func(*kubeConfigOptions) {},
-		clusterOption:    clusterOption,
-	}
+// FSLoader loads kube configs from the file system.
+type FSLoader struct {
 }
 
-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,
-	}
-
-	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, errors.Wrap(err, "unable to create cluster client")
-	}
+var _ Loader = (*FSLoader)(nil)
 
-	config, err := clientConfig.RawConfig()
-	if err != nil {
-		return nil, errors.Wrap(err, "unable to load kube config")
-	}
-	var contextList []Context
+// NewFSLoader creates an instance of FSLoader.
+func NewFSLoader(options ...FSLoaderOpt) *FSLoader {
+	l := &FSLoader{}
 
-	for name := range config.Contexts {
-		contextList = append(contextList, Context{Name: name})
+	for _, option := range options {
+		option(l)
 	}
 
-	sort.Slice(contextList, func(i, j int) bool {
-		return contextList[i].Name < contextList[j].Name
-	})
-
-	contextName := options.ContextName
-	if contextName == "" {
-		contextName = config.CurrentContext
-	}
-
-	return &KubeConfigContextManager{
-		configLoadingRules: &clientcmd.ClientConfigLoadingRules{
-			Precedence: chain,
-		},
-		currentContext: contextName,
-		contexts:       contextList,
-		clusterClient:  clusterClient,
-		clusterOptions: clusterOptions,
-	}, nil
+	return l
 }
 
-type KubeConfigContextManager struct {
-	configLoadingRules *clientcmd.ClientConfigLoadingRules
-	currentContext     string
-	contexts           []Context
-	clusterClient      cluster.ClientInterface
-	clusterOptions     []cluster.ClusterOption
-}
+// 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))
 
-// 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()
+	loadingRules := &clientcmd.ClientConfigLoadingRules{
+		Precedence: chain,
 	}
 
-	clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
-		k.configLoadingRules,
-		&clientcmd.ConfigOverrides{CurrentContext: contextName},
-	)
-
-	var err error
-	k.clusterClient, err = cluster.FromClientConfig(ctx, clientConfig, k.clusterOptions...)
+	config, err := loadingRules.Load()
 	if err != nil {
-		return errors.Wrap(err, "unable to create cluster client")
+		return nil, err
 	}
 
-	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
+	var list []Context
+
+	for name := range config.Contexts {
+		list = append(list, Context{Name: name})
 	}
 
-	k.currentContext = contextName
-	return nil
-}
+	sort.Slice(list, func(i, j int) bool {
+		return list[i].Name < list[j].Name
+	})
 
-func (k *KubeConfigContextManager) ClusterClient() cluster.ClientInterface {
-	return k.clusterClient
+	return &KubeConfig{
+		Contexts:       list,
+		CurrentContext: config.CurrentContext,
+	}, nil
 }
diff --git a/pkg/dash/dash.go b/pkg/dash/dash.go
index 61d0dfc5..1ec7b44c 100644
--- a/pkg/dash/dash.go
+++ b/pkg/dash/dash.go
@@ -31,7 +31,6 @@ 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"
@@ -65,129 +64,6 @@ 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 {
@@ -201,12 +77,7 @@ type Runner struct {
 	fs                     afero.Fs
 }
 
-func NewRunner(ctx context.Context, logger log.Logger, opts ...RunnerOption) (*Runner, error) {
-	options := Options{}
-	for _, opt := range opts {
-		opt.nonClusterOption(&options)
-	}
-
+func NewRunner(ctx context.Context, logger log.Logger, options Options) (*Runner, error) {
 	r := Runner{}
 	ctx = internalLog.WithLoggerContext(ctx, logger)
 	ctx = ocontext.WithKubeConfigCh(ctx)
@@ -231,13 +102,15 @@ func NewRunner(ctx context.Context, logger log.Logger, opts ...RunnerOption) (*R
 
 	r.fs = afero.NewOsFs()
 
-	if options.clusterClient != nil {
-		apiService, pluginService, apiErr = r.initAPI(ctx, logger, opts...)
+	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)
+		}
 	} else {
-		apiService, pluginService, apiErr = r.apiFromKubeConfig(options.KubeConfig, opts...)
-	}
-	if apiErr != nil {
-		return nil, fmt.Errorf("failed to start service api: %w", apiErr)
+		logger.Infof("no valid kube config found, initializing loading API")
+		// Initialize the API
+		apiService = api.NewLoadingAPI(ctx, api.PathPrefix, r.actionManager, r.websocketClientManager, logger)
 	}
 
 	d, err := newDash(options.Listener, options.Namespace, options.FrontendURL, options.BrowserPath, apiService, pluginService, logger)
@@ -254,24 +127,7 @@ func NewRunner(ctx context.Context, logger log.Logger, opts ...RunnerOption) (*R
 	return &r, nil
 }
 
-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)
-	}
-
+func (r *Runner) Start(options Options, startupCh, shutdownCh chan bool) error {
 	logger := internalLog.From(r.ctx)
 	go func() {
 		if err := r.dash.Run(r.ctx, startupCh); err != nil {
@@ -283,14 +139,13 @@ func (r *Runner) Start(startupCh, shutdownCh chan bool, opts ...RunnerOption) er
 		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, opts...)
+			apiService, pluginService, err := r.initAPI(r.ctx, logger, options)
 			if err != nil {
 				logger.Errorf("cannot create api: %v", err)
 			}
@@ -322,26 +177,18 @@ func (r *Runner) Start(startupCh, shutdownCh chan bool, opts ...RunnerOption) er
 	return nil
 }
 
-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)
-	}
+func (r *Runner) initAPI(ctx context.Context, logger log.Logger, options Options) (*api.API, *pluginAPI.GRPCService, error) {
 	frontendProxy := pluginAPI.FrontendProxy{}
 
-	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)
-		}
+	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)
 	}
-	clusterClient := kubeContextDecorator.ClusterClient()
 
 	if options.EnableOpenCensus {
 		if err := enableOpenCensus(); err != nil {
@@ -422,24 +269,19 @@ func (r *Runner) initAPI(ctx context.Context, logger log.Logger, opts ...RunnerO
 		Time:    options.BuildInfo.Time,
 	}
 
-	restConfigOptions := cluster.RESTConfigOptions{
-		QPS:       options.ClientQPS,
-		Burst:     options.ClientBurst,
-		UserAgent: options.UserAgent,
-	}
 	dashConfig := config.NewLiveConfig(
-		kubeContextDecorator,
+		clusterClient,
 		crdWatcher,
+		options.KubeConfig,
 		logger,
 		moduleManager,
 		appObjectStore,
 		errorStore,
 		pluginManager,
 		portForwarder,
+		options.Context,
 		restConfigOptions,
-		buildInfo,
-		false,
-	)
+		buildInfo)
 
 	pluginManager.SetOctantClient(dashConfig)
 
@@ -495,7 +337,7 @@ func initPortForwarder(ctx context.Context, client cluster.ClientInterface, appO
 }
 
 type moduleOptions struct {
-	clusterClient  cluster.ClientInterface
+	clusterClient  *cluster.Cluster
 	crdWatcher     config.CRDWatcher
 	namespace      string
 	logger         log.Logger
@@ -550,7 +392,8 @@ func initModules(ctx context.Context, dashConfig config.Dash, namespace string,
 	}
 
 	configurationOptions := configuration.Options{
-		DashConfig: dashConfig,
+		DashConfig:     dashConfig,
+		KubeConfigPath: dashConfig.KubeConfigPath(),
 	}
 	configurationModule := configuration.New(ctx, configurationOptions)
 
diff --git a/pkg/dash/fake/mock_watcher_config.go b/pkg/dash/fake/mock_watcher_config.go
index 3eb3780d..2e9dcdaa 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
 }
 
-// CurrentContext mocks base method
-func (m *MockWatcherConfig) CurrentContext() string {
+// ContextName mocks base method
+func (m *MockWatcherConfig) ContextName() string {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CurrentContext")
+	ret := m.ctrl.Call(m, "ContextName")
 	ret0, _ := ret[0].(string)
 	return ret0
 }
 
-// CurrentContext indicates an expected call of CurrentContext
-func (mr *MockWatcherConfigMockRecorder) CurrentContext() *gomock.Call {
+// ContextName indicates an expected call of ContextName
+func (mr *MockWatcherConfigMockRecorder) ContextName() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentContext", reflect.TypeOf((*MockWatcherConfig)(nil).CurrentContext))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContextName", reflect.TypeOf((*MockWatcherConfig)(nil).ContextName))
 }
 
 // UseContext mocks base method
@@ -61,17 +61,3 @@ 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 979593b1..00e032b4 100644
--- a/pkg/dash/watcher.go
+++ b/pkg/dash/watcher.go
@@ -77,8 +77,7 @@ func ConfigWatcherFileWatcher(fw FileWatcher) ConfigWatcherOption {
 
 // WatcherConfig is an interface with configuration for ConfigWatcher.
 type WatcherConfig interface {
-	CurrentContext() string
-	UseFSContext(ctx context.Context) error
+	ContextName() string
 	UseContext(ctx context.Context, name string) error
 }
 
@@ -133,7 +132,7 @@ func (cw *ConfigWatcher) Watch(ctx context.Context) {
 			done = true
 			logger.Infof("shutting down config watcher")
 		case _ = <-cw.FileWatcher.Events():
-			if err := cw.watcherConfig.UseFSContext(ctx); err != nil {
+			if err := cw.watcherConfig.UseContext(ctx, cw.watcherConfig.ContextName()); err != nil {
 				logger.WithErr(err).Errorf("reload config")
 			}
 		case err := <-cw.FileWatcher.Errors():


Test patch:
diff --git a/internal/api/websocket_state_test.go b/internal/api/websocket_state_test.go
index 739f4a95..6d4f2b89 100644
--- a/internal/api/websocket_state_test.go
+++ b/internal/api/websocket_state_test.go
@@ -252,20 +252,6 @@ 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
new file mode 100644
index 00000000..ecb9398a
--- /dev/null
+++ b/internal/cluster/cluster_test.go
@@ -0,0 +1,22 @@
+/*
+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/kubeconfig/testdata/kubeconfig.yaml b/internal/cluster/testdata/kubeconfig.yaml
similarity index 70%
rename from internal/kubeconfig/testdata/kubeconfig.yaml
rename to internal/cluster/testdata/kubeconfig.yaml
index 9201c4ee..77ac54ff 100644
--- a/internal/kubeconfig/testdata/kubeconfig.yaml
+++ b/internal/cluster/testdata/kubeconfig.yaml
@@ -12,13 +12,8 @@ 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
+    token: my-token
\ No newline at end of file
diff --git a/internal/config/dash_test.go b/internal/config/dash_test.go
index d124305f..ee5b0364 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_test
+package config
 
 import (
 	"context"
@@ -14,14 +14,10 @@ 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"
@@ -89,26 +85,18 @@ 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(
-		StaticClusterClient(clusterClient),
-		crdWatcher,
-		logger,
-		moduleManager,
-		objectStore,
-		errorStore,
-		pluginManager,
-		portForwarder,
-		restConfigOptions,
-		buildInfo,
-		false,
-	)
+	config := NewLiveConfig(clusterClient, crdWatcher, kubeConfigPath, logger, moduleManager, objectStore,
+		errorStore, pluginManager, portForwarder,
+		contextName, restConfigOptions, buildInfo)
 
 	assert.NoError(t, config.Validate())
 	assert.Equal(t, clusterClient, config.ClusterClient())
@@ -123,126 +111,6 @@ 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 07deea27..46c590c2 100644
--- a/internal/event/context_test.go
+++ b/internal/event/context_test.go
@@ -16,17 +16,32 @@ 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().CurrentContext().Return(currentContext)
-	dashConfig.EXPECT().Contexts().Return(nil)
+	dashConfig.EXPECT().KubeConfigPath().Return("/path")
+	dashConfig.EXPECT().ContextName().Return("")
 
-	kgc := NewContextsGenerator(dashConfig)
+	kgc := NewContextsGenerator(dashConfig, configLoaderFuncOpt)
 
 	assert.Equal(t, "kubeConfig", kgc.Name())
 
@@ -37,7 +52,7 @@ func Test_kubeContextGenerator(t *testing.T) {
 	assert.Equal(t, event.EventTypeKubeConfig, e.Type)
 
 	resp := kubeContextsResponse{
-		CurrentContext: currentContext,
+		CurrentContext: kc.CurrentContext,
 	}
 
 	assert.Equal(t, resp, e.Data)
diff --git a/internal/kubeconfig/kubeconfig_test.go b/internal/kubeconfig/kubeconfig_test.go
index 85051b6d..9595df9e 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 (
-	"context"
+	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -15,58 +15,8 @@ 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)
@@ -84,16 +34,19 @@ func TestFSLoader_Load(t *testing.T) {
 		paths = append(paths, kubeConfigPath)
 	}
 
-	kc, err := NewKubeConfigContextManager(
-		context.TODO(),
-		WithKubeConfigList(strings.Join(paths, string(os.PathListSeparator))),
-	)
+	l := NewFSLoader()
+
+	kc, err := l.Load(strings.Join(paths, string(os.PathListSeparator)))
 	require.NoError(t, err)
 
-	assert.Equal(t, "dev-frontend", kc.CurrentContext())
-	assert.Equal(t, []Context{
-		{Name: "dev-frontend"},
-		{Name: "dev-storage"},
-		{Name: "exp-scratch"},
-	}, kc.Contexts())
+	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))
 }
diff --git a/internal/kubeconfig/testdata/kubeconfig-1.yaml b/internal/kubeconfig/testdata/kubeconfig-1.yaml
index 13f7d82c..f396bf3d 100644
--- a/internal/kubeconfig/testdata/kubeconfig-1.yaml
+++ b/internal/kubeconfig/testdata/kubeconfig-1.yaml
@@ -4,12 +4,8 @@ preferences: {}
 
 clusters:
   - cluster:
-      api-version: v1
-      server: https://cluster:4443
     name: development
   - cluster:
-      api-version: v1
-      server: https://cluster:4443
     name: scratch
 
 users:
@@ -18,9 +14,7 @@ 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 649cf793..0f0796b7 100644
--- a/internal/kubeconfig/testdata/kubeconfig-2.yaml
+++ b/internal/kubeconfig/testdata/kubeconfig-2.yaml
@@ -4,12 +4,8 @@ preferences: {}
 
 clusters:
   - cluster:
-      api-version: v1
-      server: https://cluster:4443
     name: development
   - cluster:
-      api-version: v1
-      server: https://cluster:4443
     name: scratch
 
 users:
@@ -18,6 +14,5 @@ users:
 
 contexts:
   - context:
-      cluster: scratch
     name: exp-scratch
 current-context: exp-scratch
diff --git a/internal/objectvisitor/service_test.go b/internal/objectvisitor/service_test.go
index 94de9104..5139f86f 100644
--- a/internal/objectvisitor/service_test.go
+++ b/internal/objectvisitor/service_test.go
@@ -2,6 +2,7 @@ package objectvisitor_test
 
 import (
 	"context"
+	"sync"
 	"testing"
 
 	"github.com/golang/mock/gomock"
@@ -65,10 +66,13 @@ func TestService_Visit(t *testing.T) {
 		Return(nil)
 
 	var visited []unstructured.Unstructured
+	var m sync.Mutex
 	visitor := fake.NewMockVisitor(controller)
 	visitor.EXPECT().
 		Visit(gomock.Any(), gomock.Any(), handler, gomock.Any()).
 		DoAndReturn(func(ctx context.Context, object *unstructured.Unstructured, handler objectvisitor.ObjectHandler, _ bool) error {
+			m.Lock()
+			defer m.Unlock()
 			visited = append(visited, *object)
 			return nil
 		}).
diff --git a/pkg/dash/dash_test.go b/pkg/dash/dash_test.go
index 1d31e579..dcd9e4c8 100644
--- a/pkg/dash/dash_test.go
+++ b/pkg/dash/dash_test.go
@@ -22,21 +22,15 @@ 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/cluster"
-	internalLog "github.com/vmware-tanzu/octant/internal/log"
+	"github.com/vmware-tanzu/octant/internal/log"
 	"github.com/vmware-tanzu/octant/pkg/event"
 
-	clusterFake "github.com/vmware-tanzu/octant/internal/cluster/fake"
-	"github.com/vmware-tanzu/octant/pkg/log"
+	pkglog "github.com/vmware-tanzu/octant/pkg/log"
 )
 
 func TestRunner_ValidateKubeconfig(t *testing.T) {
@@ -92,7 +86,7 @@ func TestRunner_ValidateKubeconfig(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			logger := internalLog.NopLogger()
+			logger := log.NopLogger()
 			path, err := ValidateKubeConfig(logger, test.fileList, fs)
 			if test.isErr {
 				require.Error(t, err)
@@ -153,69 +147,25 @@ 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()
-
-	logger := internalLog.NopLogger()
-	cancel, err := makeRunner(
-		logger,
-		WithClusterClient(clusterClient),
-		WithListener(listener),
+	cancel, _ := makeRunner(
+		Options{
+			KubeConfig: strings.Join(
+				[]string{
+					"/non/existent/kubeconfig",
+					kubeConfig.Name(),
+				},
+				string(filepath.ListSeparator),
+			),
+			Listener: listener,
+		},
+		log.NopLogger(),
 	)
-	require.NoError(t, err)
 	defer cancel()
-	namespacesEvent, err := waitForEventOfType(listener, event.EventTypeNamespaces)
-	require.NoError(t, err)
+	kubeConfigEvent := waitForKubeConfigEvent(listener)
 
-	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
+	require.Equal(t, "test-context", kubeConfigEvent.Data.CurrentContext)
 }
 
 func TestNewRunnerRunsLoadingAPIWhenStartedWithoutKubeConfig(t *testing.T) {
@@ -224,8 +174,7 @@ func TestNewRunnerRunsLoadingAPIWhenStartedWithoutKubeConfig(t *testing.T) {
 	stubRiceBox("dist/dash-frontend")
 
 	listener := NewInMemoryListener()
-	cancel, err := makeRunner(internalLog.NopLogger(), WithListener(listener))
-	require.NoError(t, err)
+	cancel, _ := makeRunner(Options{Listener: listener}, log.NopLogger())
 	defer cancel()
 	kubeConfig := makeKubeConfig("test-context", srv.URL)
 	websocketWrite(
@@ -241,21 +190,24 @@ func TestNewRunnerRunsLoadingAPIWhenStartedWithoutKubeConfig(t *testing.T) {
 			break
 		}
 	}
-	kubeConfigEvent, err := waitForEventOfType(listener, event.EventTypeKubeConfig)
-	require.NoError(t, err)
+	kubeConfigEvent := waitForKubeConfigEvent(listener)
 
-	require.Equal(t, "test-context", kubeConfigEvent.Data["currentContext"].(string))
+	require.Equal(t, "test-context", kubeConfigEvent.Data.CurrentContext)
 }
 
 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, internalLog.NopLogger(), WithListener(listener))
+	runner, err := NewRunner(ctx, logger, options)
 	require.NoError(t, err)
 
-	go runner.Start(make(chan bool), shutdownCh)
+	go runner.Start(options, make(chan bool), shutdownCh)
 	cancel()
 
 	select {
@@ -314,34 +266,31 @@ current-context: %s
 `, currentContext, serverAddr, currentContext))
 }
 
-func waitForEventOfType(listener *inMemoryListener, eventType event.EventType) (streamingEvent, error) {
-	var message streamingEvent
+func waitForKubeConfigEvent(listener *inMemoryListener) kubeConfigEvent {
+	var message kubeConfigEvent
 	dialer := websocket.DefaultDialer
 	dialer.NetDial = listener.Dial
-	wsConn, _, err := dialer.Dial("ws://127.0.0.1:7777/api/v1/stream", nil)
+	wsConn, resp, err := dialer.Dial("ws://127.0.0.1:7777/api/v1/stream", nil)
 	if err != nil {
-		return message, err
+		fmt.Println(resp)
+		panic(err)
 	}
 	defer wsConn.Close()
 	for {
-		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 {
+		msgBytes, _ := readNextMessage(wsConn)
+		json.Unmarshal(msgBytes, &message)
+		if message.Type == event.EventTypeKubeConfig {
 			break
 		}
 	}
-	return message, nil
+	return message
 }
 
-type streamingEvent struct {
-	Type event.EventType        `json:"type"`
-	Data map[string]interface{} `json:"data"`
+type kubeConfigEvent struct {
+	Type event.EventType `json:"type"`
+	Data struct {
+		CurrentContext string `json:"currentContext"`
+	} `json:"data"`
 }
 
 func tempFile(contents []byte) *os.File {
@@ -351,13 +300,13 @@ func tempFile(contents []byte) *os.File {
 	return tmpFile
 }
 
-func makeRunner(logger log.Logger, opts ...RunnerOption) (context.CancelFunc, error) {
+func makeRunner(options Options, logger pkglog.Logger) (context.CancelFunc, error) {
 	ctx, cancel := context.WithCancel(context.Background())
-	runner, err := NewRunner(ctx, logger, opts...)
+	runner, err := NewRunner(ctx, logger, options)
 	if err != nil {
 		return cancel, err
 	}
-	go runner.Start(make(chan bool), make(chan bool), opts...)
+	go runner.Start(options, make(chan bool), make(chan bool))
 	return cancel, nil
 }
 
diff --git a/pkg/dash/watcher_test.go b/pkg/dash/watcher_test.go
index 536dcdf6..eaa23205 100644
--- a/pkg/dash/watcher_test.go
+++ b/pkg/dash/watcher_test.go
@@ -70,9 +70,11 @@ func TestConfigWatcher_Watch(t *testing.T) {
 
 	watcherConfig := fake.NewMockWatcherConfig(controller)
 
+	watcherConfig.EXPECT().ContextName().Return("name")
+
 	ch := make(chan bool, 1)
-	watcherConfig.EXPECT().UseFSContext(gomock.Any()).
-		DoAndReturn(func(_ context.Context) error {
+	watcherConfig.EXPECT().UseContext(gomock.Any(), "name").
+		DoAndReturn(func(_ context.Context, _ string) error {
 			ch <- true
 			return nil
 		})