Repository: vmware-tanzu/octant
Pull request #2212 (2021-03-24 00:58:30)

Update component dynamically if view type changes
The recent Resource Viewer [change](https://github.com/vmware-tanzu/octant/pull/2177) uncovered problems with updating the object status component. After Resource Viewer selection has been changed, the status view was not updated when component of different type had to be used, so for example we were missing heptagon indicators in some cases. This change is fixing that by checking the view type and forcing the update if new view has different type.  

Signed-off-by: Milan Klanjsek <mklanjsek@pivotal.io>



Patch:
diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go
index c78f5436..9d3dbd50 100644
--- a/internal/cluster/cluster.go
+++ b/internal/cluster/cluster.go
@@ -43,7 +43,6 @@ import (
 //go:generate mockgen -source=../../pkg/cluster/namespace.go -destination=./fake/mock_namespace_interface.go -package=fake github.com/vmware-tanzu/octant/pkg/cluster NamespaceInterface
 //go:generate mockgen -source=../../pkg/cluster/info.go -destination=./fake/mock_info_interface.go -package=fake github.com/vmware-tanzu/octant/pkg/cluster InfoInterface
 //go:generate mockgen -source=../../vendor/k8s.io/client-go/informers/generic.go -destination=./fake/mock_genericinformer.go -package=fake k8s.io/client-go/informers GenericInformer
-//go:generate mockgen -source=../../vendor/k8s.io/client-go/dynamic/dynamicinformer/interface.go -destination=./fake/mock_dynamicsharedinformerfactory.go -package=fake k8s.io/client-go/dynamic/dynamicinformer DynamicSharedInformerFactory
 //go:generate mockgen -source=../../vendor/k8s.io/client-go/discovery/discovery_client.go -imports=openapi_v2=github.com/googleapis/gnostic/openapiv2 -destination=./fake/mock_discoveryinterface.go -package=fake k8s.io/client-go/discovery DiscoveryInterface
 //go:generate mockgen -source=../../vendor/k8s.io/client-go/kubernetes/clientset.go -destination=./fake/mock_kubernetes_client.go -package=fake -mock_names=Interface=MockKubernetesInterface k8s.io/client-go/kubernetes Interface
 //go:generate mockgen -destination=./fake/mock_sharedindexinformer.go -package=fake k8s.io/client-go/tools/cache SharedIndexInformer
diff --git a/internal/cluster/fake/mock_dynamicsharedinformerfactory.go b/internal/cluster/fake/mock_dynamicsharedinformerfactory.go
deleted file mode 100644
index 92166611..00000000
--- a/internal/cluster/fake/mock_dynamicsharedinformerfactory.go
+++ /dev/null
@@ -1,76 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: ../../vendor/k8s.io/client-go/dynamic/dynamicinformer/interface.go
-
-// Package fake is a generated GoMock package.
-package fake
-
-import (
-	reflect "reflect"
-
-	gomock "github.com/golang/mock/gomock"
-	schema "k8s.io/apimachinery/pkg/runtime/schema"
-	informers "k8s.io/client-go/informers"
-)
-
-// MockDynamicSharedInformerFactory is a mock of DynamicSharedInformerFactory interface
-type MockDynamicSharedInformerFactory struct {
-	ctrl     *gomock.Controller
-	recorder *MockDynamicSharedInformerFactoryMockRecorder
-}
-
-// MockDynamicSharedInformerFactoryMockRecorder is the mock recorder for MockDynamicSharedInformerFactory
-type MockDynamicSharedInformerFactoryMockRecorder struct {
-	mock *MockDynamicSharedInformerFactory
-}
-
-// NewMockDynamicSharedInformerFactory creates a new mock instance
-func NewMockDynamicSharedInformerFactory(ctrl *gomock.Controller) *MockDynamicSharedInformerFactory {
-	mock := &MockDynamicSharedInformerFactory{ctrl: ctrl}
-	mock.recorder = &MockDynamicSharedInformerFactoryMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use
-func (m *MockDynamicSharedInformerFactory) EXPECT() *MockDynamicSharedInformerFactoryMockRecorder {
-	return m.recorder
-}
-
-// Start mocks base method
-func (m *MockDynamicSharedInformerFactory) Start(stopCh <-chan struct{}) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "Start", stopCh)
-}
-
-// Start indicates an expected call of Start
-func (mr *MockDynamicSharedInformerFactoryMockRecorder) Start(stopCh interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockDynamicSharedInformerFactory)(nil).Start), stopCh)
-}
-
-// ForResource mocks base method
-func (m *MockDynamicSharedInformerFactory) ForResource(gvr schema.GroupVersionResource) informers.GenericInformer {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ForResource", gvr)
-	ret0, _ := ret[0].(informers.GenericInformer)
-	return ret0
-}
-
-// ForResource indicates an expected call of ForResource
-func (mr *MockDynamicSharedInformerFactoryMockRecorder) ForResource(gvr interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForResource", reflect.TypeOf((*MockDynamicSharedInformerFactory)(nil).ForResource), gvr)
-}
-
-// WaitForCacheSync mocks base method
-func (m *MockDynamicSharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "WaitForCacheSync", stopCh)
-	ret0, _ := ret[0].(map[schema.GroupVersionResource]bool)
-	return ret0
-}
-
-// WaitForCacheSync indicates an expected call of WaitForCacheSync
-func (mr *MockDynamicSharedInformerFactoryMockRecorder) WaitForCacheSync(stopCh interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForCacheSync", reflect.TypeOf((*MockDynamicSharedInformerFactory)(nil).WaitForCacheSync), stopCh)
-}
diff --git a/internal/config/dash.go b/internal/config/dash.go
index dcaeaa49..cddec8e3 100644
--- a/internal/config/dash.go
+++ b/internal/config/dash.go
@@ -193,6 +193,9 @@ func NewLiveConfig(
 		buildInfo:            buildInfo,
 		contextChosenInUI:    contextChosenInUI,
 	}
+	objectStore.RegisterOnUpdate(func(store store.Store) {
+		l.objectStore = store
+	})
 
 	return l
 }
diff --git a/internal/describer/crd_watcher.go b/internal/describer/crd_watcher.go
index 8acb2e29..fe08afcb 100644
--- a/internal/describer/crd_watcher.go
+++ b/internal/describer/crd_watcher.go
@@ -49,6 +49,16 @@ func NewDefaultCRDWatcher(ctx context.Context, clusterClient cluster.ClientInter
 		watchConfigs:  make(map[string]*config.CRDWatchConfig),
 	}
 
+	objectStore.RegisterOnUpdate(func(newObjectStore store.Store) {
+		cw.mu.Lock()
+		defer cw.mu.Unlock()
+
+		cw.objectStore = newObjectStore
+
+		logger := log.From(ctx)
+		logger.Debugf("default crd watcher updated object store")
+	})
+
 	return cw, nil
 }
 
@@ -69,6 +79,7 @@ func (cw *DefaultCRDWatcher) Watch(ctx context.Context) error {
 			defer cw.mu.Unlock()
 
 			logger := logger.With("crdwatcher", "add")
+
 			u, ok := object.(*unstructured.Unstructured)
 			if !ok {
 				logger.
@@ -90,6 +101,7 @@ func (cw *DefaultCRDWatcher) Watch(ctx context.Context) error {
 			defer cw.mu.Unlock()
 
 			logger := logger.With("crdwatcher", "delete")
+
 			u, ok := object.(*unstructured.Unstructured)
 			if !ok {
 				logger.
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 67ec4729..f50e9988 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -64,21 +64,11 @@ func (g *Generator) Generate(ctx context.Context, contentPath string, opts Optio
 	ctx, span := trace.StartSpan(ctx, "Generate")
 	defer span.End()
 
-	span.AddAttributes(
-		trace.StringAttribute("contentPath", contentPath),
-	)
-
 	pf, err := g.pathMatcher.Find(contentPath)
 	if err != nil {
 		if err == describer.ErrPathNotFound {
-			span.AddAttributes(
-				trace.BoolAttribute("pathNotFound", true),
-			)
 			return component.EmptyContentResponse, api.NewNotFoundError(contentPath)
 		}
-		span.AddAttributes(
-			trace.BoolAttribute("pathMatcherErr", true),
-		)
 		return component.EmptyContentResponse, err
 	}
 
@@ -114,15 +104,8 @@ func (g *Generator) Generate(ctx context.Context, contentPath string, opts Optio
 		LoadObject:  loaderFactory.LoadObject,
 	}
 
-	span.AddAttributes(
-		trace.BoolAttribute("preparedOptions", true),
-	)
-
 	cResponse, err := pf.Describer.Describe(ctx, namespace, options)
 	if err != nil {
-		span.AddAttributes(
-			trace.BoolAttribute("describeError", true),
-		)
 		return component.EmptyContentResponse, err
 	}
 
diff --git a/internal/modules/overview/overview.go b/internal/modules/overview/overview.go
index f7eeac00..e41cfb47 100644
--- a/internal/modules/overview/overview.go
+++ b/internal/modules/overview/overview.go
@@ -22,6 +22,7 @@ import (
 	"github.com/vmware-tanzu/octant/pkg/icon"
 	"github.com/vmware-tanzu/octant/pkg/log"
 	"github.com/vmware-tanzu/octant/pkg/navigation"
+	"github.com/vmware-tanzu/octant/pkg/store"
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
 
@@ -67,6 +68,15 @@ func New(ctx context.Context, options Options) (*Overview, error) {
 		return nil, err
 	}
 
+	logger := internalLog.From(ctx).With("module", "overview")
+
+	co.dashConfig.ObjectStore().RegisterOnUpdate(func(newObjectStore store.Store) {
+		logger.Debugf("object store was updated")
+		if err := co.bootstrap(ctx); err != nil {
+			logger.WithErr(err).Errorf("updating object store")
+		}
+	})
+
 	return co, nil
 }
 
diff --git a/internal/objectstore/access.go b/internal/objectstore/access.go
new file mode 100644
index 00000000..6d53abb0
--- /dev/null
+++ b/internal/objectstore/access.go
@@ -0,0 +1,201 @@
+/*
+Copyright (c) 2019 the Octant contributors. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package objectstore
+
+import (
+	"context"
+	"fmt"
+	"sync"
+
+	"go.opencensus.io/trace"
+	authorizationv1 "k8s.io/api/authorization/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/vmware-tanzu/octant/internal/cluster"
+	oerrors "github.com/vmware-tanzu/octant/internal/errors"
+	"github.com/vmware-tanzu/octant/pkg/store"
+)
+
+type AccessError struct {
+	Key AccessKey
+}
+
+func (ae *AccessError) Error() string {
+	return fmt.Sprintf("access denied: no %s access in %s to %s/%s",
+		ae.Key.Verb, ae.Key.Namespace, ae.Key.Group, ae.Key.Resource)
+}
+
+// AccessKey is used at a key in an access map. It is made up of a Namespace, Group, Resource, and Verb.
+type AccessKey struct {
+	Namespace string
+	Group     string
+	Resource  string
+	Verb      string
+}
+
+type accessMap map[AccessKey]bool
+
+type accessCache struct {
+	access accessMap
+	mu     sync.RWMutex
+}
+
+func newAccessCache() *accessCache {
+	return &accessCache{
+		access: accessMap{},
+	}
+}
+
+func (c *accessCache) reset() {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	c.access = accessMap{}
+}
+
+func (c *accessCache) set(key AccessKey, value bool) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	c.access[key] = value
+}
+
+func (c *accessCache) get(key AccessKey) (v, ok bool) {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
+	v, ok = c.access[key]
+	return v, ok
+}
+
+type ResourceAccess interface {
+	HasAccess(context.Context, store.Key, string) error
+	Reset()
+	Get(AccessKey) (bool, bool)
+	Set(AccessKey, bool)
+	UpdateClient(client cluster.ClientInterface)
+}
+
+type resourceAccess struct {
+	client cluster.ClientInterface
+	cache  *accessCache
+
+	mu sync.RWMutex
+}
+
+var _ ResourceAccess = (*resourceAccess)(nil)
+
+func NewResourceAccess(client cluster.ClientInterface) ResourceAccess {
+	return &resourceAccess{
+		client: client,
+		cache:  newAccessCache(),
+	}
+}
+
+func (r *resourceAccess) UpdateClient(client cluster.ClientInterface) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	r.client = client
+}
+
+// Reset resets the resource access cache.
+func (r *resourceAccess) Reset() {
+	r.cache.reset()
+}
+
+// Get returns the value and if it was found for an AccessKey.
+func (r *resourceAccess) Get(key AccessKey) (value, found bool) {
+	return r.cache.get(key)
+}
+
+// Set will set the value in the map for an AccessKey.
+func (r *resourceAccess) Set(key AccessKey, v bool) {
+	r.cache.set(key, v)
+}
+
+// HasAccess returns an error if the current user does not have access to perform the verb action
+// for the given key.
+func (r *resourceAccess) HasAccess(ctx context.Context, key store.Key, verb string) error {
+	_, span := trace.StartSpan(ctx, "resourceAccessHasAccess")
+	defer span.End()
+
+	aKey, err := r.keyToAccessKey(key, verb)
+	if err != nil {
+		return err
+	}
+
+	access, ok := r.cache.get(aKey)
+
+	if !ok {
+		span.Annotate([]trace.Attribute{}, "fetch access start")
+		val, err := r.fetchAccess(aKey, verb)
+		if err != nil {
+			return fmt.Errorf("fetch access: %+v: %w", aKey, err)
+		}
+
+		r.cache.set(aKey, val)
+		access = val
+		span.Annotate([]trace.Attribute{}, "fetch access finish")
+	}
+
+	if !access {
+		return oerrors.NewAccessError(key, verb, err)
+	}
+
+	return nil
+}
+
+func (r *resourceAccess) keyToAccessKey(key store.Key, verb string) (AccessKey, error) {
+	r.mu.RLock()
+	defer r.mu.RUnlock()
+
+	gvk := key.GroupVersionKind()
+
+	if gvk.GroupKind().Empty() {
+		return AccessKey{}, fmt.Errorf("unable to check access for key %s", key.String())
+	}
+
+	gvr, _, err := r.client.Resource(gvk.GroupKind())
+	if err != nil {
+		return AccessKey{}, fmt.Errorf("unable to get resource for group kind %s: %w", gvk.GroupKind(), err)
+	}
+
+	aKey := AccessKey{
+		Namespace: key.Namespace,
+		Group:     gvr.Group,
+		Resource:  gvr.Resource,
+		Verb:      verb,
+	}
+	return aKey, nil
+}
+
+func (r *resourceAccess) fetchAccess(key AccessKey, verb string) (bool, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	k8sClient, err := r.client.KubernetesClient()
+	if err != nil {
+		return false, fmt.Errorf("client kubernetes: %w", err)
+	}
+
+	authClient := k8sClient.AuthorizationV1()
+	sar := &authorizationv1.SelfSubjectAccessReview{
+		Spec: authorizationv1.SelfSubjectAccessReviewSpec{
+			ResourceAttributes: &authorizationv1.ResourceAttributes{
+				Namespace: key.Namespace,
+				Group:     key.Group,
+				Resource:  key.Resource,
+				Verb:      verb,
+			},
+		},
+	}
+
+	review, err := authClient.SelfSubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{})
+	if err != nil {
+		return false, fmt.Errorf("client auth: %w", err)
+	}
+	return review.Status.Allowed, nil
+}
diff --git a/internal/objectstore/accessors.go b/internal/objectstore/accessors.go
new file mode 100644
index 00000000..b9d02c52
--- /dev/null
+++ b/internal/objectstore/accessors.go
@@ -0,0 +1,190 @@
+package objectstore
+
+import (
+	"sync"
+
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/vmware-tanzu/octant/pkg/store"
+)
+
+type informerSynced struct {
+	status sync.Map
+}
+
+func initInformerSynced() *informerSynced {
+	return &informerSynced{
+		status: sync.Map{},
+	}
+}
+
+func (c *informerSynced) setSynced(key store.Key, value bool) {
+	c.status.Store(key.String(), value)
+}
+
+func (c *informerSynced) hasSynced(key store.Key) bool {
+	v, ok := c.status.Load(key.String())
+	if !ok {
+		return true
+	}
+	return v.(bool)
+}
+
+func (c *informerSynced) hasSeen(key store.Key) bool {
+	_, ok := c.status.Load(key.String())
+	return ok
+}
+
+func (c *informerSynced) reset() {
+	c.status.Range(func(k, v interface{}) bool {
+		c.status.Delete(k)
+		return true
+	})
+}
+
+type factoriesCache struct {
+	factories map[string]InformerFactory
+
+	mu sync.RWMutex
+}
+
+func initFactoriesCache() *factoriesCache {
+	return &factoriesCache{
+		factories: make(map[string]InformerFactory),
+	}
+}
+
+func (c *factoriesCache) set(key string, value InformerFactory) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	c.factories[key] = value
+}
+
+func (c *factoriesCache) keys() []string {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
+	var list []string
+	for k := range c.factories {
+		list = append(list, k)
+	}
+
+	return list
+}
+
+func (c *factoriesCache) get(key string) (InformerFactory, bool) {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
+	v, ok := c.factories[key]
+	return v, ok
+}
+
+func (c *factoriesCache) reset() {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	for name := range c.factories {
+		delete(c.factories, name)
+	}
+}
+
+func (c *factoriesCache) delete(key string) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	delete(c.factories, key)
+}
+
+type seenGVKsCache struct {
+	seenGVKs map[string]map[schema.GroupVersionKind]bool
+
+	mu sync.RWMutex
+}
+
+func initSeenGVKsCache() *seenGVKsCache {
+	return &seenGVKsCache{
+		seenGVKs: make(map[string]map[schema.GroupVersionKind]bool),
+	}
+}
+
+func (c *seenGVKsCache) setSeen(key string, groupVersionKind schema.GroupVersionKind, value bool) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	cur, ok := c.seenGVKs[key]
+	if !ok {
+		cur = make(map[schema.GroupVersionKind]bool)
+	}
+
+	cur[groupVersionKind] = value
+	c.seenGVKs[key] = cur
+}
+
+func (c *seenGVKsCache) hasSeen(key string, groupVersionKind schema.GroupVersionKind) bool {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
+	v, ok := c.seenGVKs[key]
+	if !ok {
+		return false
+	}
+
+	seen, ok := v[groupVersionKind]
+	if !ok {
+		return false
+	}
+
+	return seen
+}
+
+func (c *seenGVKsCache) reset() {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	for k := range c.seenGVKs {
+		delete(c.seenGVKs, k)
+	}
+}
+
+type informerContextCache struct {
+	cache map[schema.GroupVersionKind]chan struct{}
+
+	mu sync.Mutex
+}
+
+func initInformerContextCache() *informerContextCache {
+	return &informerContextCache{
+		cache: make(map[schema.GroupVersionKind]chan struct{}),
+	}
+}
+
+func (c *informerContextCache) addChild(key schema.GroupVersionKind) chan struct{} {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	ch := make(chan struct{}, 1)
+	c.cache[key] = ch
+	return ch
+}
+
+func (c *informerContextCache) delete(key schema.GroupVersionKind) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if stopCh, ok := c.cache[key]; ok {
+		close(stopCh)
+		delete(c.cache, key)
+	}
+}
+
+func (c *informerContextCache) reset() {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	for k, stopCh := range c.cache {
+		close(stopCh)
+		delete(c.cache, k)
+	}
+}
diff --git a/internal/objectstore/backoff.go b/internal/objectstore/backoff.go
new file mode 100644
index 00000000..00d3378f
--- /dev/null
+++ b/internal/objectstore/backoff.go
@@ -0,0 +1,57 @@
+package objectstore
+
+import (
+	"sync"
+	"time"
+
+	"k8s.io/apimachinery/pkg/util/wait"
+
+	"github.com/vmware-tanzu/octant/pkg/store"
+)
+
+var defaultBackoff = wait.Backoff{
+	Duration: 1 * time.Second,
+	Factor:   2.0,
+	Jitter:   0.1,
+	Steps:    50,
+	Cap:      10 * time.Minute,
+}
+
+type backoffer interface {
+	isWaiting() bool
+	setWaiting(bool)
+	wait() time.Duration
+}
+
+var _ backoffer = (*backoffEntry)(nil)
+
+type backoffEntry struct {
+	key     store.Key
+	waiting bool
+	backoff wait.Backoff
+
+	mu sync.RWMutex
+}
+
+func newBackoffEntry(key store.Key, backoff wait.Backoff) *backoffEntry {
+	return &backoffEntry{
+		key:     key,
+		waiting: false,
+		backoff: backoff,
+	}
+}
+func (b *backoffEntry) isWaiting() bool {
+	b.mu.RLock()
+	defer b.mu.RUnlock()
+	return b.waiting
+}
+
+func (b *backoffEntry) setWaiting(waiting bool) {
+	b.mu.Lock()
+	defer b.mu.Unlock()
+	b.waiting = waiting
+}
+
+func (b *backoffEntry) wait() time.Duration {
+	return b.backoff.Step()
+}
diff --git a/internal/objectstore/diagnostics_unix.go b/internal/objectstore/diagnostics_unix.go
new file mode 100644
index 00000000..3e4f3a4e
--- /dev/null
+++ b/internal/objectstore/diagnostics_unix.go
@@ -0,0 +1,28 @@
+// +build darwin linux
+
+package objectstore
+
+import (
+	"os"
+	"os/signal"
+	"syscall"
+
+	"github.com/vmware-tanzu/octant/pkg/log"
+)
+
+func initStatusCheck(stopCh <-chan struct{}, logger log.Logger, factories *factoriesCache) {
+	sigCh := make(chan os.Signal, 1)
+	signal.Notify(sigCh, syscall.SIGUSR2)
+
+	done := false
+	for !done {
+		select {
+		case <-stopCh:
+			done = true
+		case <-sigCh:
+			logger.With("factory-count", len(factories.factories)).Debugf("dynamic cache status")
+		}
+	}
+
+	logger.Debugf("dynamic cache status exiting")
+}
diff --git a/internal/objectstore/diagnostics_windows.go b/internal/objectstore/diagnostics_windows.go
new file mode 100644
index 00000000..fede9c7d
--- /dev/null
+++ b/internal/objectstore/diagnostics_windows.go
@@ -0,0 +1,11 @@
+// +build windows
+
+package objectstore
+
+import (
+	"github.com/vmware-tanzu/octant/pkg/log"
+)
+
+func initStatusCheck(stopCh <-chan struct{}, logger log.Logger, factories *factoriesCache) {
+
+}
diff --git a/internal/objectstore/dynamic_cache.go b/internal/objectstore/dynamic_cache.go
index 18eaf863..4bbecf62 100644
--- a/internal/objectstore/dynamic_cache.go
+++ b/internal/objectstore/dynamic_cache.go
@@ -1,3 +1,8 @@
+/*
+Copyright (c) 2019 the Octant contributors. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+
 package objectstore
 
 import (
@@ -9,19 +14,20 @@ import (
 	"sync"
 	"time"
 
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/yaml"
+
 	"go.opencensus.io/trace"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/labels"
-	"k8s.io/apimachinery/pkg/runtime"
+	kLabels "k8s.io/apimachinery/pkg/labels"
+	kruntime "k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/yaml"
-	"k8s.io/client-go/dynamic/dynamicinformer"
-	"k8s.io/client-go/tools/cache"
-	"k8s.io/client-go/util/retry"
+	"k8s.io/client-go/informers"
+	kcache "k8s.io/client-go/tools/cache"
+	kretry "k8s.io/client-go/util/retry"
 	sigyaml "sigs.k8s.io/yaml"
 
 	"github.com/vmware-tanzu/octant/internal/cluster"
@@ -29,88 +35,254 @@ import (
 	"github.com/vmware-tanzu/octant/pkg/store"
 )
 
-const resyncPeriod = time.Second * 180
+const (
+	// defaultMutableResync is the resync period for informers.
+	defaultInformerResync = time.Second * 180
 
-type lister interface {
-	// List will return all objects in this namespace
-	List(selector labels.Selector) (ret []runtime.Object, err error)
-	// Get will attempt to retrieve by namespace and name
-	Get(name string) (runtime.Object, error)
+	// initialInformerSyncTimeout
+	initialInformerSyncTimeout = time.Second * 10
+)
+
+func initInformerFactory(ctx context.Context, client cluster.ClientInterface, namespace string) (InformerFactory, error) {
+	return newInformerFactory(ctx.Done(), client, defaultInformerResync, namespace), nil
 }
 
+// DynamicCacheOpt is an option for configuration DynamicCache.
+type DynamicCacheOpt func(*DynamicCache)
+
+// Access sets the Resource Access cache for a DynamicCache.
+func Access(resourceAccess ResourceAccess) DynamicCacheOpt {
+	return func(dc *DynamicCache) {
+		dc.setResourceAccess(resourceAccess)
+	}
+}
+
+// DynamicCache is a cache based on the dynamic shared informer factory.
 type DynamicCache struct {
-	ctx    context.Context
-	cancel context.CancelFunc
-	client cluster.ClientInterface
+	initFactoryFunc func(context.Context, cluster.ClientInterface, string) (InformerFactory, error)
+	factories       *factoriesCache
+	informerSynced  *informerSynced
+	backoffMap      sync.Map
+	client          cluster.ClientInterface
+	seenGVKs        *seenGVKsCache
+	access          ResourceAccess
+	updateFns       []store.UpdateFn
+	updateMu        sync.Mutex
+
+	syncTimeoutFunc func(context.Context, store.Key, chan bool)
+	waitForSyncFunc func(context.Context, store.Key, *DynamicCache, informers.GenericInformer, chan bool)
+}
 
-	informerFactory dynamicinformer.DynamicSharedInformerFactory
-	knownInformers  sync.Map // gvr:GenericInformer
-	unwatched       sync.Map // gvr:bool
-	gvrCache        sync.Map // gk:gvr
+func syncTimeout(ctx context.Context, key store.Key, done chan bool) {
+	logger := log.From(ctx).With("key", key)
+	timer := time.NewTimer(initialInformerSyncTimeout)
+	select {
+	case <-timer.C:
+		logger.Debugf("cache has taken more than %s seconds to sync", initialInformerSyncTimeout)
+	case <-done:
+		timer.Stop()
+	}
+}
 
-	removeCh chan schema.GroupVersionResource
-	mu       sync.Mutex
+func waitForSync(ctx context.Context, key store.Key, dc *DynamicCache, informer informers.GenericInformer, done chan bool) {
+	now := time.Now()
+	logger := log.From(ctx).With("key", key)
+	msg := "informer cache has synced"
+	kcache.WaitForCacheSync(ctx.Done(), informer.Informer().HasSynced)
+	<-time.After(100 * time.Millisecond)
+	logger.With("elapsed", time.Since(now)).
+		Debugf(msg)
+	dc.informerSynced.setSynced(key, true)
+	done <- true
 }
 
 var _ store.Store = (*DynamicCache)(nil)
 
-type Option func(*DynamicCache)
+// NewDynamicCache creates an instance of DynamicCache.
+func NewDynamicCache(ctx context.Context, client cluster.ClientInterface, options ...DynamicCacheOpt) (*DynamicCache, error) {
+	c := &DynamicCache{
+		initFactoryFunc: initInformerFactory,
+		syncTimeoutFunc: syncTimeout,
+		waitForSyncFunc: waitForSync,
+		client:          client,
+		seenGVKs:        initSeenGVKsCache(),
+		informerSynced:  initInformerSynced(),
+	}
+
+	for _, option := range options {
+		option(c)
+	}
+
+	logger := log.From(ctx).With("component", "DynamicCache")
+
+	c.factories = initFactoriesCache()
+	go initStatusCheck(ctx.Done(), logger, c.factories)
 
-func WithDynamicSharedInformerFactory(factory dynamicinformer.DynamicSharedInformerFactory) Option {
-	return func(d *DynamicCache) {
-		d.informerFactory = factory
+	factory, err := c.initFactoryFunc(context.Background(), client, "")
+	if err != nil {
+		return nil, fmt.Errorf("initialize dynamic shared informer factory: %w", err)
 	}
+
+	c.factories.set("", factory)
+
+	return c, nil
+}
+
+type lister interface {
+	List(selector kLabels.Selector) ([]kruntime.Object, error)
 }
 
-func NewDynamicCache(ctx context.Context, client cluster.ClientInterface, opts ...Option) (*DynamicCache, error) {
-	ctx, cancel := context.WithCancel(ctx)
-	dynamicClient, err := client.DynamicClient()
+func (dc *DynamicCache) setResourceAccess(resourceAccess ResourceAccess) {
+	dc.access = resourceAccess
+}
+
+func (dc *DynamicCache) currentInformer(ctx context.Context, key store.Key) (informers.GenericInformer, bool, error) {
+	if dc.client == nil {
+		return nil, false, errors.New("cluster client is nil")
+	}
+
+	gvk := key.GroupVersionKind()
+	factory, ok := dc.factories.get(key.Namespace)
+	if !ok {
+		if err := dc.access.HasAccess(ctx, store.Key{Namespace: metav1.NamespaceAll}, "watch"); err != nil {
+			factory, err = dc.initFactoryFunc(ctx, dc.client, key.Namespace)
+			if err != nil {
+				return nil, false, fmt.Errorf("check access watch all namespaces: %w", err)
+			}
+		} else {
+			factory, ok = dc.factories.get("")
+			if !ok {
+				return nil, false, errors.New("no default DynamicInformerFactory found")
+			}
+		}
+
+		dc.factories.set(key.Namespace, factory)
+	}
+
+	informer, err := factory.ForResource(gvk)
 	if err != nil {
-		cancel()
-		return nil, err
+		return nil, false, fmt.Errorf("find informer for %s: %w", gvk, err)
 	}
 
-	dc := &DynamicCache{
-		ctx:            ctx,
-		cancel:         cancel,
-		client:         client,
-		knownInformers: sync.Map{},
-		unwatched:      sync.Map{},
-		gvrCache:       sync.Map{},
-		removeCh:       make(chan schema.GroupVersionResource),
+	dc.checkKeySynced(ctx, informer, key)
+	dc.seenGVKs.setSeen(key.Namespace, gvk, true)
+
+	return informer, dc.informerSynced.hasSynced(key), nil
+}
+
+func (dc *DynamicCache) checkKeySynced(ctx context.Context, informer informers.GenericInformer, key store.Key) {
+	dc.updateMu.Lock()
+	defer dc.updateMu.Unlock()
+
+	if dc.seenGVKs.hasSeen(key.Namespace, key.GroupVersionKind()) ||
+		(dc.informerSynced.hasSeen(key) && dc.informerSynced.hasSynced(key)) {
+		return
 	}
 
-	for _, opt := range opts {
-		opt(dc)
+	done := make(chan bool, 1)
+	go dc.waitForSyncFunc(ctx, key, dc, informer, done)
+	go dc.syncTimeoutFunc(ctx, key, done)
+}
+
+// backoff returns the Duration of the next backoff. A duration of 0 is returned if
+// backoff is already waiting.
+func (dc *DynamicCache) backoff(ctx context.Context, key store.Key) time.Duration {
+	newEntry := newBackoffEntry(key, defaultBackoff)
+	actual, _ := dc.backoffMap.LoadOrStore(key.GroupVersionKind(), newEntry)
+	entry := actual.(backoffer)
+	if entry.isWaiting() {
+		return 0
 	}
 
-	if dc.informerFactory == nil {
-		dc.informerFactory = dynamicinformer.NewDynamicSharedInformerFactory(dynamicClient, resyncPeriod)
+	entry.setWaiting(true)
+	dc.backoffMap.Store(key.GroupVersionKind(), entry)
+	t := entry.wait()
+
+	go func() {
+		select {
+		case <-ctx.Done():
+			return
+		default:
+			logger := log.From(ctx)
+			defer func() {
+				entry.setWaiting(false)
+				dc.backoffMap.Store(key.GroupVersionKind(), entry)
+			}()
+			unwatchErr := dc.Unwatch(ctx, key.GroupVersionKind())
+			if unwatchErr != nil {
+				logger.Errorf("unwatch: %w", unwatchErr)
+			}
+			logger.Infof("backing off for %s (%s)", key.GroupVersionKind(), t)
+			<-time.After(t)
+		}
+	}()
+
+	return t
+}
+
+func (dc *DynamicCache) isBackingOff(ctx context.Context, key store.Key) bool {
+	actual, ok := dc.backoffMap.Load(key.GroupVersionKind())
+	if !ok {
+		return false
 	}
+	entry := actual.(backoffer)
+	return entry.isWaiting()
+}
 
-	go dc.worker()
+// List lists objects.
+func (dc *DynamicCache) List(ctx context.Context, key store.Key) (*unstructured.UnstructuredList, bool, error) {
+	ctx, span := trace.StartSpan(ctx, "dynamicCache:list")
+	defer span.End()
 
-	return dc, nil
+	if dc.isBackingOff(ctx, key) {
+		return &unstructured.UnstructuredList{}, false, nil
+	}
+
+	if err := dc.access.HasAccess(ctx, key, "list"); err != nil {
+		if meta.IsNoMatchError(err) {
+			return &unstructured.UnstructuredList{}, false, nil
+		}
+		if !dc.isBackingOff(ctx, key) {
+			dc.backoff(ctx, key)
+		}
+		return nil, false, fmt.Errorf("check access to list %s: %w", key, err)
+	}
+
+	span.Annotate([]trace.Attribute{
+		trace.StringAttribute("namespace", key.Namespace),
+		trace.StringAttribute("apiVersion", key.APIVersion),
+		trace.StringAttribute("kind", key.Kind),
+	}, "list key")
+
+	return dc.listFromInformer(ctx, key)
 }
 
-func (d *DynamicCache) List(ctx context.Context, key store.Key) (list *unstructured.UnstructuredList, loading bool, err error) {
-	_, span := trace.StartSpan(ctx, "dynamicCache:List")
+func (dc *DynamicCache) listFromInformer(ctx context.Context, key store.Key) (*unstructured.UnstructuredList, bool, error) {
+	ctx, span := trace.StartSpan(ctx, "dynamicCache:list:informer")
 	defer span.End()
 
-	resourceLister, err := d.listerForResource(ctx, key)
+	informer, hasSynced, err := dc.currentInformer(ctx, key)
 	if err != nil {
-		return nil, false, err
+		return nil, false, fmt.Errorf("retrieving informer for %+v: %w", key, err)
+	}
+
+	if !hasSynced {
+		list, err := dc.listFromDynamicClient(ctx, key)
+		return list, false, err
 	}
 
-	if resourceLister == nil {
-		return nil, false, fmt.Errorf("resourceLister is nil")
+	var l lister
+	if key.Namespace == "" {
+		l = informer.Lister()
+	} else {
+		l = informer.Lister().ByNamespace(key.Namespace)
 	}
 
 	if key.Selector != nil && key.LabelSelector != nil {
 		return nil, false, fmt.Errorf("must provide only one of Key.Selector and Key.LabelSelector")
 	}
 
-	var selector = labels.Everything()
+	var selector = kLabels.Everything()
 	if key.Selector != nil {
 		selector = key.Selector.AsSelector()
 	} else if key.LabelSelector != nil {
@@ -120,376 +292,357 @@ func (d *DynamicCache) List(ctx context.Context, key store.Key) (list *unstructu
 		}
 	}
 
-	span.AddAttributes(
-		trace.StringAttribute("key", fmt.Sprintf("%s", key)),
-		trace.StringAttribute("selector", fmt.Sprintf("%s", selector)),
-	)
-
-	objs, err := resourceLister.List(selector)
+	objects, err := l.List(selector)
 	if err != nil {
-		return nil, false, err
+		return nil, false, fmt.Errorf("listing %v: %w", key, err)
 	}
 
-	objectCount := len(objs)
-
-	span.AddAttributes(
-		trace.Int64Attribute("objectCount", int64(objectCount)),
-	)
-	ul := &unstructured.UnstructuredList{}
-	ul.Items = make([]unstructured.Unstructured, len(objs))
-
-	for i := 0; i < objectCount; i++ {
-		u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(objs[i])
-		if err != nil {
-			return nil, false, err
-		}
-		ul.Items[i].Object = u
+	list := &unstructured.UnstructuredList{}
+	for i := range objects {
+		list.Items = append(list.Items, *objects[i].(*unstructured.Unstructured))
 	}
 
-	return ul, false, err
+	return list, !dc.informerSynced.hasSynced(key), nil
 }
 
-func (d *DynamicCache) Get(ctx context.Context, key store.Key) (object *unstructured.Unstructured, err error) {
-	ctx, span := trace.StartSpan(ctx, "dynamicCache:Get")
+func (dc *DynamicCache) listFromDynamicClient(ctx context.Context, key store.Key) (*unstructured.UnstructuredList, error) {
+	_, span := trace.StartSpan(ctx, "dynamicCache:list:informer")
 	defer span.End()
 
-	resourceLister, err := d.listerForResource(ctx, key)
-	if err != nil {
-		return nil, err
+	var selector = kLabels.Everything()
+	if key.Selector != nil {
+		selector = key.Selector.AsSelector()
+	} else if key.LabelSelector != nil {
+		var err error
+		selector, err = metav1.LabelSelectorAsSelector(key.LabelSelector)
+		if err != nil {
+			return nil, err
+		}
 	}
 
-	span.AddAttributes(
-		trace.StringAttribute("key", fmt.Sprintf("%s", key)),
-	)
-
-	obj, err := resourceLister.Get(key.Name)
+	dynamicClient, err := dc.client.DynamicClient()
 	if err != nil {
 		return nil, err
 	}
-	return obj.(*unstructured.Unstructured), nil
-}
-
-func (d *DynamicCache) Delete(ctx context.Context, key store.Key) error {
-	_, span := trace.StartSpan(ctx, "dynamicCache:delete")
-	defer span.End()
 
-	dynamicClient, err := d.client.DynamicClient()
+	gvr, _, err := dc.client.Resource(key.GroupVersionKind().GroupKind())
 	if err != nil {
-		return err
-	}
-
-	gvr, err := d.gvrFromKey(ctx, key)
-	if err != nil {
-		return err
+		return nil, err
 	}
 
-	deletePolicy := metav1.DeletePropagationForeground
-	deleteOptions := metav1.DeleteOptions{
-		PropagationPolicy: &deletePolicy,
+	listOptions := metav1.ListOptions{
+		LabelSelector: selector.String(),
 	}
-
 	if key.Namespace == "" {
-		return dynamicClient.Resource(gvr).Delete(ctx, key.Name, deleteOptions)
+		return dynamicClient.Resource(gvr).List(ctx, listOptions)
 	}
 
-	return dynamicClient.Resource(gvr).Namespace(key.Namespace).Delete(ctx, key.Name, deleteOptions)
+	return dynamicClient.Resource(gvr).Namespace(key.Namespace).List(ctx, listOptions)
 }
 
-func (d *DynamicCache) UpdateClusterClient(ctx context.Context, client cluster.ClientInterface) error {
-	_, span := trace.StartSpan(ctx, "dynamicCache:UpdateClusterClient")
-	defer span.End()
-
-	d.mu.Lock()
-	defer d.mu.Unlock()
-
-	d.stopAllInformers()
-
-	d.client = client
-	dynamicClient, err := client.DynamicClient()
-	if err != nil {
-		return err
-	}
-
-	d.informerFactory = dynamicinformer.NewDynamicSharedInformerFactory(dynamicClient, resyncPeriod)
-	d.knownInformers = sync.Map{}
-	d.unwatched = sync.Map{}
-
-	return nil
+type getter interface {
+	Get(string) (kruntime.Object, error)
 }
 
-func (d *DynamicCache) Update(ctx context.Context, key store.Key, updater func(*unstructured.Unstructured) error) error {
-	ctx, span := trace.StartSpan(ctx, "dynamicCache:Update")
+// Get retrieves a single object.
+func (dc *DynamicCache) Get(ctx context.Context, key store.Key) (*unstructured.Unstructured, error) {
+	ctx, span := trace.StartSpan(ctx, "dynamicCacheGet")
 	defer span.End()
 
-	if updater == nil {
-		return fmt.Errorf("can't update object")
+	if dc.isBackingOff(ctx, key) {
+		return &unstructured.Unstructured{}, nil
 	}
 
-	err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
-		object, err := d.Get(ctx, key)
-		if err != nil {
-			return err
+	if err := dc.access.HasAccess(ctx, key, "get"); err != nil {
+		if meta.IsNoMatchError(err) {
+			return &unstructured.Unstructured{}, nil
 		}
-
-		if object == nil {
-			return errors.New("object not found")
+		if !dc.isBackingOff(ctx, key) {
+			dc.backoff(ctx, key)
 		}
+		return nil, fmt.Errorf("check access for get to %s: %w", key, err)
+	}
 
-		gvr, err := d.gvrFromKey(ctx, key)
-		if err != nil {
-			return err
-		}
+	span.Annotate([]trace.Attribute{
+		trace.StringAttribute("namespace", key.Namespace),
+		trace.StringAttribute("apiVersion", key.APIVersion),
+		trace.StringAttribute("kind", key.Kind),
+		trace.StringAttribute("name", key.Name),
+	}, "get key")
 
-		dynamicClient, err := d.client.DynamicClient()
-		if err != nil {
-			return err
-		}
-
-		if err := updater(object); err != nil {
-			return fmt.Errorf("unable to update object: %w", err)
+	object, err := dc.getFromInformer(ctx, key)
+	if err != nil {
+		if kerrors.IsNotFound(err) {
+			object, err := dc.getFromDynamicClient(ctx, key)
+			if err != nil {
+				return nil, err
+			}
+			return object, nil
 		}
 
-		client := dynamicClient.Resource(gvr).Namespace(object.GetNamespace())
-
-		_, err = client.Update(ctx, object, metav1.UpdateOptions{})
-		return err
-	})
+		return nil, err
+	}
 
-	return err
+	return object, nil
 
 }
 
-func (d *DynamicCache) IsLoading(ctx context.Context, key store.Key) bool {
-	_, span := trace.StartSpan(ctx, "dynamicCache:IsLoading")
+func (dc *DynamicCache) getFromInformer(ctx context.Context, key store.Key) (*unstructured.Unstructured, error) {
+	ctx, span := trace.StartSpan(ctx, "dynamicCache:get:informer")
 	defer span.End()
-	return false
+
+	informer, hasSynced, err := dc.currentInformer(ctx, key)
+	if err != nil {
+		return nil, fmt.Errorf("retrieving informer for %v: %w", key, err)
+	}
+
+	if !hasSynced {
+		return dc.getFromDynamicClient(ctx, key)
+	}
+
+	var g getter
+	if key.Namespace == "" {
+		g = informer.Lister()
+	} else {
+		g = informer.Lister().ByNamespace(key.Namespace)
+	}
+
+	object, err := g.Get(key.Name)
+	if err != nil {
+		return nil, err
+	}
+	return object.(*unstructured.Unstructured), nil
 }
 
-func (d *DynamicCache) Create(ctx context.Context, object *unstructured.Unstructured) error {
-	ctx, span := trace.StartSpan(ctx, "dynamicCache:Create")
+func (dc *DynamicCache) getFromDynamicClient(ctx context.Context, key store.Key) (*unstructured.Unstructured, error) {
+	_, span := trace.StartSpan(ctx, "dynamicCache:get:dynamicClient")
 	defer span.End()
 
-	key, err := store.KeyFromObject(object)
+	dynamicClient, err := dc.client.DynamicClient()
 	if err != nil {
-		return fmt.Errorf("key from object: %w", err)
+		return nil, err
 	}
 
-	gvr, err := d.gvrFromKey(ctx, key)
+	gvr, _, err := dc.client.Resource(key.GroupVersionKind().GroupKind())
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	createOptions := metav1.CreateOptions{}
+	if key.Namespace == "" {
+		return dynamicClient.Resource(gvr).Get(ctx, key.Name, metav1.GetOptions{})
+	}
+	return dynamicClient.Resource(gvr).Namespace(key.Namespace).Get(ctx, key.Name, metav1.GetOptions{})
+}
 
-	dynamicClient, err := d.client.DynamicClient()
-	if err != nil {
-		return err
+// Watch watches the cluster for an event and performs actions with the
+// supplied handler.
+func (dc *DynamicCache) Watch(ctx context.Context, key store.Key, handler kcache.ResourceEventHandler) error {
+	if dc.isBackingOff(ctx, key) {
+		return nil
 	}
 
-	if key.Namespace == "" {
-		_, err := dynamicClient.Resource(gvr).Create(ctx, object, createOptions)
-		return err
+	if err := dc.access.HasAccess(ctx, key, "watch"); err != nil {
+		if meta.IsNoMatchError(err) {
+			return nil
+		}
+		if !dc.isBackingOff(ctx, key) {
+			dc.backoff(ctx, key)
+		}
+		return fmt.Errorf("check access to watch %s: %w", key, err)
 	}
 
-	_, err = dynamicClient.Resource(gvr).Namespace(key.Namespace).Create(ctx, object, createOptions)
-	return err
+	informer, _, err := dc.currentInformer(ctx, key)
+	if err != nil {
+		return fmt.Errorf("retrieving informer for %s: %w", key, err)
+	}
+
+	informer.Informer().AddEventHandler(handler)
+	return nil
 }
 
-func (d *DynamicCache) WaitForCacheSync(ctx context.Context) bool {
-	_, span := trace.StartSpan(ctx, "dynamicCache:WaitForCacheSync")
-	defer span.End()
+// Unwatch un-watches a key by stopping it's informer.
+func (dc *DynamicCache) Unwatch(ctx context.Context, groupVersionKinds ...schema.GroupVersionKind) error {
+	for _, namespace := range dc.factories.keys() {
+		factory, ok := dc.factories.get(namespace)
+		if ok {
+			for _, groupVersionKind := range groupVersionKinds {
+				factory.Delete(groupVersionKind)
+			}
+		}
+	}
 
-	var ret bool
-	d.knownInformers.Range(func(k, v interface{}) bool {
-		ii := v.(interuptibleInformer)
-		ret = cache.WaitForCacheSync(ii.stopCh, ii.informer.Informer().HasSynced)
-		return ret
-	})
-	return ret
+	return nil
 }
 
-func (d *DynamicCache) Watch(ctx context.Context, key store.Key, handler cache.ResourceEventHandler) error {
-	ctx, span := trace.StartSpan(ctx, "dynamicCache:Watch")
+// Delete deletes an object from the cluster using a key.
+func (dc *DynamicCache) Delete(ctx context.Context, key store.Key) error {
+	_, span := trace.StartSpan(ctx, "dynamicCache:delete")
 	defer span.End()
 
-	logger := log.From(ctx)
-	logger.With("dynamicCache", "watch")
-	logger.Debugf("creating watch for %s", key)
+	if dc.isBackingOff(ctx, key) {
+		return nil
+	}
+
+	if err := dc.access.HasAccess(ctx, key, "delete"); err != nil {
+		if meta.IsNoMatchError(err) {
+			return nil
+		}
+		if !dc.isBackingOff(ctx, key) {
+			dc.backoff(ctx, key)
+		}
+		return fmt.Errorf("check access to delete %s: %w", key, err)
+	}
 
-	gvr, err := d.gvrFromKey(ctx, key)
+	dynamicClient, err := dc.client.DynamicClient()
 	if err != nil {
 		return err
 	}
 
-	if d.isUnwatched(ctx, gvr) {
-		return fmt.Errorf("watcher was unable to start for %s", gvr)
+	gvr, _, err := dc.client.Resource(key.GroupVersionKind().GroupKind())
+	if err != nil {
+		return err
 	}
 
-	span.AddAttributes(trace.StringAttribute("key", fmt.Sprintf("%s", key)))
+	deletePolicy := metav1.DeletePropagationForeground
+	deleteOptions := metav1.DeleteOptions{
+		PropagationPolicy: &deletePolicy,
+	}
 
-	d.forResource(ctx, gvr, handler)
+	if key.Namespace == "" {
+		return dynamicClient.Resource(gvr).Delete(ctx, key.Name, deleteOptions)
+	}
 
-	return err
+	return dynamicClient.Resource(gvr).Namespace(key.Namespace).Delete(ctx, key.Name, deleteOptions)
 }
 
-func (d *DynamicCache) Unwatch(ctx context.Context, groupVersionKinds ...schema.GroupVersionKind) error {
-	return nil
-}
+// UpdateClusterClient updates the cluster client.
+func (dc *DynamicCache) UpdateClusterClient(ctx context.Context, client cluster.ClientInterface) error {
+	logger := log.From(ctx)
+	logger.Debugf("updating its cluster client")
 
-func (d *DynamicCache) worker() {
-	for {
-		select {
-		case <-d.ctx.Done():
-			d.stopAllInformers()
-			return
-		case gvr := <-d.removeCh:
-			d.unwatched.Store(gvr, true)
-			v, ok := d.knownInformers.LoadAndDelete(gvr)
-			if ok {
-				ii := v.(interuptibleInformer)
-				ii.Stop()
-			}
-		case <-time.After(time.Millisecond * 500):
-			continue
-		}
+	dc.updateMu.Lock()
+	dc.client = client
+	dc.factories.reset()
+	dc.seenGVKs.reset()
+	dc.informerSynced.reset()
+	dc.access = NewResourceAccess(client)
+	dc.updateMu.Unlock()
+
+	for _, fn := range dc.updateFns {
+		fn(dc)
 	}
-}
 
-func (d *DynamicCache) stopAllInformers() {
-	d.knownInformers.Range(func(k, v interface{}) bool {
-		ii := v.(interuptibleInformer)
-		ii.Stop()
-		return true
-	})
+	return nil
 }
 
-func (d *DynamicCache) isUnwatched(ctx context.Context, gvr schema.GroupVersionResource) bool {
-	_, ok := d.unwatched.Load(gvr)
-	return ok
+// RegisterOnUpdate registers a function that will be called when the store updates it's client.
+// TODO: investigate if this needed since object store isn't replaced, it's client is. (GH#496)
+func (dc *DynamicCache) RegisterOnUpdate(fn store.UpdateFn) {
+	dc.updateFns = append(dc.updateFns, fn)
 }
 
-func (d *DynamicCache) forResource(ctx context.Context, gvr schema.GroupVersionResource, handler cache.ResourceEventHandler) interuptibleInformer {
-	_, span := trace.StartSpan(ctx, "dynamicCache:forResource")
-	defer span.End()
+func (dc *DynamicCache) Update(ctx context.Context, key store.Key, updater func(*unstructured.Unstructured) error) error {
+	if updater == nil {
+		return errors.New("can't update object")
+	}
 
-	logger := log.From(ctx)
-	logger = logger.With("dynamicCache", "forResource")
+	if dc.isBackingOff(ctx, key) {
+		return nil
+	}
 
-	v, ok := d.knownInformers.Load(gvr)
-	if !ok {
-		i := d.informerFactory.ForResource(gvr)
-		stopCh := make(chan struct{})
-		i.Informer().SetWatchErrorHandler(d.watchErrorHandler(ctx, gvr, stopCh))
-		if handler != nil {
-			i.Informer().AddEventHandlerWithResyncPeriod(handler, resyncPeriod)
+	if err := dc.access.HasAccess(ctx, key, "update"); err != nil {
+		if meta.IsNoMatchError(err) {
+			return nil
 		}
-
-		go func() {
-			logger.Debugf("starting informer for %s", gvr)
-			i.Informer().Run(stopCh)
-			logger.Debugf("stopping informer for %s", gvr)
-		}()
-
-		ii := interuptibleInformer{
-			stopCh,
-			i,
-			gvr,
+		if !dc.isBackingOff(ctx, key) {
+			dc.backoff(ctx, key)
 		}
-		d.knownInformers.Store(gvr, ii)
-		return ii
-	}
-	ii := v.(interuptibleInformer)
-	if handler != nil {
-		ii.informer.Informer().AddEventHandlerWithResyncPeriod(handler, resyncPeriod)
+		return fmt.Errorf("check access to update %s: %w", key, err)
 	}
-	return ii
-}
-
-func (d *DynamicCache) listerForResource(ctx context.Context, key store.Key) (lister, error) {
-	ctx, span := trace.StartSpan(ctx, "dynamicCache:ListerForResource")
-	defer span.End()
 
-	gvr, err := d.gvrFromKey(ctx, key)
-	if err != nil {
-		return nil, err
-	}
+	err := kretry.RetryOnConflict(kretry.DefaultRetry, func() error {
+		object, err := dc.Get(ctx, key)
+		if err != nil {
+			return err
+		}
 
-	span.AddAttributes(
-		trace.StringAttribute("key", fmt.Sprintf("%s", key)),
-		trace.StringAttribute("gvr", fmt.Sprintf("%s", gvr)),
-	)
+		if object == nil {
+			return errors.New("object not found")
+		}
 
-	if d.isUnwatched(ctx, gvr) {
-		return nil, fmt.Errorf("unable to get Lister for %s, watcher was unable to start", gvr)
-	}
+		gvk := object.GroupVersionKind()
 
-	ii := d.forResource(ctx, gvr, nil)
+		gvr, _, err := dc.client.Resource(gvk.GroupKind())
+		if err != nil {
+			return err
+		}
 
-	var l lister
-	if key.Namespace == "" {
-		l = ii.informer.Lister()
-	} else {
-		l = ii.informer.Lister().ByNamespace(key.Namespace)
-	}
+		dynamicClient, err := dc.client.DynamicClient()
+		if err != nil {
+			return err
+		}
 
-	return l, nil
-}
+		if err := updater(object); err != nil {
+			return fmt.Errorf("unable to update object: %w", err)
+		}
 
-func (d *DynamicCache) watchErrorHandler(ctx context.Context, gvr schema.GroupVersionResource, stopCh chan struct{}) func(*cache.Reflector, error) {
-	return func(r *cache.Reflector, err error) {
-		_, span := trace.StartSpan(ctx, "dynamicCache:watchErrorHandler")
-		defer span.End()
+		client := dynamicClient.Resource(gvr).Namespace(object.GetNamespace())
 
-		span.AddAttributes(trace.StringAttribute("gvr", fmt.Sprintf("%s", gvr)))
+		_, err = client.Update(ctx, object, metav1.UpdateOptions{})
+		return err
+	})
 
-		logger := log.From(ctx)
-		logger.Warnf("unable to start watcher ", err.Error())
+	return err
+}
 
-		d.removeCh <- gvr
-	}
+func (dc *DynamicCache) IsLoading(ctx context.Context, key store.Key) bool {
+	return !dc.informerSynced.hasSynced(key)
 }
 
-func (d *DynamicCache) gvrFromKey(ctx context.Context, key store.Key) (schema.GroupVersionResource, error) {
-	_, span := trace.StartSpan(ctx, "dynamicCache:gvrFromKey")
+// Create creates an object in the cluster.
+// Note: test coverage of DynamicCache is slim.
+func (dc *DynamicCache) Create(ctx context.Context, object *unstructured.Unstructured) error {
+	_, span := trace.StartSpan(ctx, "dynamicCache:create")
 	defer span.End()
 
-	// short circuit for CRD type
-	if key.APIVersion == "apiextensions.k8s.io/v1" && key.Kind == "CustomResourceDefinition" {
-		return schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}, nil
+	key, err := store.KeyFromObject(object)
+	if err != nil {
+		return fmt.Errorf("key from object: %w", err)
 	}
 
-	gk := key.GroupVersionKind().GroupKind()
-	var gvr schema.GroupVersionResource
+	if dc.isBackingOff(ctx, key) {
+		return nil
+	}
 
-	v, ok := d.gvrCache.Load(gk)
-	if !ok {
-		gvr, _, err := d.client.Resource(gk)
-		if err != nil {
-			return schema.GroupVersionResource{}, err
+	if err := dc.access.HasAccess(ctx, key, "create"); err != nil {
+		if meta.IsNoMatchError(err) {
+			return nil
 		}
-
-		if gvr.Resource == "" {
-			_, gvr = meta.UnsafeGuessKindToResource(key.GroupVersionKind())
+		if !dc.isBackingOff(ctx, key) {
+			dc.backoff(ctx, key)
 		}
+		return fmt.Errorf("check access to create %s: %w", key, err)
+	}
 
-		// do not store an empty GVR
-		if gvr.Version == "" && gvr.Resource == "" {
-			return schema.GroupVersionResource{}, fmt.Errorf("unable to locate GVR for GVK: %s", key.GroupVersionKind())
-		}
+	dynamicClient, err := dc.client.DynamicClient()
+	if err != nil {
+		return err
+	}
 
-		d.gvrCache.Store(gk, gvr)
-	} else {
-		gvr, _ = v.(schema.GroupVersionResource)
+	gvr, _, err := dc.client.Resource(key.GroupVersionKind().GroupKind())
+	if err != nil {
+		return err
 	}
 
-	span.AddAttributes(
-		trace.StringAttribute("live", "true"),
-		trace.StringAttribute("key", fmt.Sprintf("%s", key)),
-		trace.StringAttribute("gvr", fmt.Sprintf("%s %s %s", gvr.Group, gvr.Version, gvr.Resource)),
-		trace.StringAttribute("gk", fmt.Sprintf("%s", gk)),
-	)
+	createOptions := metav1.CreateOptions{}
+
+	if key.Namespace == "" {
+		_, err := dynamicClient.Resource(gvr).Create(ctx, object, createOptions)
+		return err
+	}
 
-	return gvr, nil
+	_, err = dynamicClient.Resource(gvr).Namespace(key.Namespace).Create(ctx, object, createOptions)
+	return err
 }
 
 func CreateOrUpdateFromHandler(
diff --git a/internal/objectstore/fake/mock_informer_factory.go b/internal/objectstore/fake/mock_informer_factory.go
new file mode 100644
index 00000000..f14983ee
--- /dev/null
+++ b/internal/objectstore/fake/mock_informer_factory.go
@@ -0,0 +1,77 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/vmware-tanzu/octant/internal/objectstore (interfaces: InformerFactory)
+
+// Package fake is a generated GoMock package.
+package fake
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	informers "k8s.io/client-go/informers"
+)
+
+// MockInformerFactory is a mock of InformerFactory interface
+type MockInformerFactory struct {
+	ctrl     *gomock.Controller
+	recorder *MockInformerFactoryMockRecorder
+}
+
+// MockInformerFactoryMockRecorder is the mock recorder for MockInformerFactory
+type MockInformerFactoryMockRecorder struct {
+	mock *MockInformerFactory
+}
+
+// NewMockInformerFactory creates a new mock instance
+func NewMockInformerFactory(ctrl *gomock.Controller) *MockInformerFactory {
+	mock := &MockInformerFactory{ctrl: ctrl}
+	mock.recorder = &MockInformerFactoryMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockInformerFactory) EXPECT() *MockInformerFactoryMockRecorder {
+	return m.recorder
+}
+
+// Delete mocks base method
+func (m *MockInformerFactory) Delete(arg0 schema.GroupVersionKind) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Delete", arg0)
+}
+
+// Delete indicates an expected call of Delete
+func (mr *MockInformerFactoryMockRecorder) Delete(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockInformerFactory)(nil).Delete), arg0)
+}
+
+// ForResource mocks base method
+func (m *MockInformerFactory) ForResource(arg0 schema.GroupVersionKind) (informers.GenericInformer, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ForResource", arg0)
+	ret0, _ := ret[0].(informers.GenericInformer)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ForResource indicates an expected call of ForResource
+func (mr *MockInformerFactoryMockRecorder) ForResource(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForResource", reflect.TypeOf((*MockInformerFactory)(nil).ForResource), arg0)
+}
+
+// WaitForCacheSync mocks base method
+func (m *MockInformerFactory) WaitForCacheSync(arg0 <-chan struct{}) map[schema.GroupVersionKind]bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WaitForCacheSync", arg0)
+	ret0, _ := ret[0].(map[schema.GroupVersionKind]bool)
+	return ret0
+}
+
+// WaitForCacheSync indicates an expected call of WaitForCacheSync
+func (mr *MockInformerFactoryMockRecorder) WaitForCacheSync(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForCacheSync", reflect.TypeOf((*MockInformerFactory)(nil).WaitForCacheSync), arg0)
+}
diff --git a/internal/objectstore/informer.go b/internal/objectstore/informer.go
index ac9c83c7..4cf13657 100644
--- a/internal/objectstore/informer.go
+++ b/internal/objectstore/informer.go
@@ -1,16 +1,135 @@
 package objectstore
 
 import (
+	"fmt"
+	"sync"
+	"time"
+
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic/dynamicinformer"
 	"k8s.io/client-go/informers"
+	"k8s.io/client-go/tools/cache"
+
+	"github.com/vmware-tanzu/octant/internal/cluster"
 )
 
-type interuptibleInformer struct {
-	stopCh   chan struct{}
-	informer informers.GenericInformer
-	gvr      schema.GroupVersionResource
+//go:generate mockgen -destination=./fake/mock_informer_factory.go -package=fake github.com/vmware-tanzu/octant/internal/objectstore InformerFactory
+
+// InformerFactory creates informers.
+type InformerFactory interface {
+	ForResource(gvr schema.GroupVersionKind) (informers.GenericInformer, error)
+	Delete(gvr schema.GroupVersionKind)
+	WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionKind]bool
+}
+
+type informerFactory struct {
+	client        cluster.ClientInterface
+	defaultResync time.Duration
+	namespace     string
+
+	lock                 sync.Mutex
+	informers            map[schema.GroupVersionKind]informers.GenericInformer
+	informerErrors       map[schema.GroupVersionKind]error
+	tweakListOptions     dynamicinformer.TweakListOptionsFunc
+	stopCh               <-chan struct{}
+	informerContextCache *informerContextCache
+}
+
+var _ InformerFactory = (*informerFactory)(nil)
+
+func newInformerFactory(stopCh <-chan struct{}, client cluster.ClientInterface, defaultResync time.Duration, namespace string) *informerFactory {
+	return &informerFactory{
+		stopCh:               stopCh,
+		client:               client,
+		defaultResync:        defaultResync,
+		namespace:            namespace,
+		informers:            make(map[schema.GroupVersionKind]informers.GenericInformer),
+		informerErrors:       make(map[schema.GroupVersionKind]error),
+		informerContextCache: initInformerContextCache(),
+	}
+}
+
+func (f *informerFactory) watchErrorHandler(gvk schema.GroupVersionKind, stopCh chan struct{}) cache.WatchErrorHandler {
+	return func(r *cache.Reflector, err error) {
+		f.lock.Lock()
+		defer f.lock.Unlock()
+		f.informerErrors[gvk] = err
+		close(stopCh)
+	}
 }
 
-func (i interuptibleInformer) Stop() {
-	close(i.stopCh)
+// ForResource creates an informer and starts it given a group/version/resource. Informers
+// are cached and will not be re-created if they already exist.
+func (f *informerFactory) ForResource(groupVersionKind schema.GroupVersionKind) (informers.GenericInformer, error) {
+	f.lock.Lock()
+	defer f.lock.Unlock()
+
+	informer, exists := f.informers[groupVersionKind]
+	if exists && informer != nil {
+		return informer, nil
+	}
+
+	stopCh := f.informerContextCache.addChild(groupVersionKind)
+
+	gvr, _, err := f.client.Resource(groupVersionKind.GroupKind())
+	if err != nil {
+		return nil, fmt.Errorf("unable to find group version resource for group kind %s: %w",
+			groupVersionKind.GroupKind(), err)
+	}
+
+	gvr.Version = groupVersionKind.Version
+
+	dynamicClient, err := f.client.DynamicClient()
+	if err != nil {
+		return nil, fmt.Errorf("get dynamic client: %w", err)
+	}
+
+	genericInformer := dynamicinformer.NewFilteredDynamicInformer(
+		dynamicClient,
+		gvr,
+		f.namespace,
+		f.defaultResync,
+		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
+		f.tweakListOptions)
+	f.informers[groupVersionKind] = genericInformer
+
+	genericInformer.Informer().SetWatchErrorHandler(f.watchErrorHandler(groupVersionKind, stopCh))
+	go genericInformer.Informer().Run(stopCh)
+
+	err, exists = f.informerErrors[groupVersionKind]
+	if exists && err != nil {
+		return genericInformer, err
+	}
+
+	return genericInformer, nil
+}
+
+// Delete deletes an informer given a a group/version/resource.
+func (f *informerFactory) Delete(groupVersionKind schema.GroupVersionKind) {
+	f.lock.Lock()
+	defer f.lock.Unlock()
+
+	f.informerContextCache.delete(groupVersionKind)
+	delete(f.informers, groupVersionKind)
+	f.informers[groupVersionKind] = nil
+}
+
+// WaitForCacheSync waits for all started informers' cache were synced.
+func (f *informerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionKind]bool {
+	list := func() map[schema.GroupVersionKind]cache.SharedIndexInformer {
+		f.lock.Lock()
+		defer f.lock.Unlock()
+
+		shared := map[schema.GroupVersionKind]cache.SharedIndexInformer{}
+		for informerType, informer := range f.informers {
+			shared[informerType] = informer.Informer()
+		}
+		return shared
+	}()
+
+	res := map[schema.GroupVersionKind]bool{}
+	for informerType, informer := range list {
+		res[informerType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
+	}
+	return res
 }
diff --git a/pkg/dash/dash.go b/pkg/dash/dash.go
index 59464564..4817043a 100644
--- a/pkg/dash/dash.go
+++ b/pkg/dash/dash.go
@@ -17,8 +17,6 @@ import (
 	"strings"
 	"time"
 
-	"k8s.io/client-go/dynamic/dynamicinformer"
-
 	"github.com/vmware-tanzu/octant/internal/util/path_util"
 
 	"contrib.go.opencensus.io/exporter/jaeger"
@@ -70,7 +68,6 @@ type Options struct {
 	BuildInfo              config.BuildInfo
 	Listener               net.Listener
 	clusterClient          cluster.ClientInterface
-	factory                dynamicinformer.DynamicSharedInformerFactory
 }
 
 type RunnerOption struct {
@@ -78,15 +75,6 @@ type RunnerOption struct {
 	nonClusterOption func(*Options)
 }
 
-func WithDynamicSharedInformerFactory(factory dynamicinformer.DynamicSharedInformerFactory) RunnerOption {
-	return RunnerOption{
-		kubeConfigOption: kubeconfig.Noop(),
-		nonClusterOption: func(o *Options) {
-			o.factory = factory
-		},
-	}
-}
-
 func WithOpenCensus() RunnerOption {
 	return RunnerOption{
 		kubeConfigOption: kubeconfig.Noop(),
@@ -394,8 +382,7 @@ func (r *Runner) initAPI(ctx context.Context, logger log.Logger, opts ...RunnerO
 
 	logger.Debugf("initial namespace for dashboard is %s", options.Namespace)
 
-	factoryOption := objectstore.WithDynamicSharedInformerFactory(options.factory)
-	appObjectStore, err := initObjectStore(ctx, clusterClient, factoryOption)
+	appObjectStore, err := initObjectStore(ctx, clusterClient)
 	if err != nil {
 		return nil, nil, fmt.Errorf("initializing store: %w", err)
 	}
@@ -508,12 +495,13 @@ func (r *Runner) initAPI(ctx context.Context, logger log.Logger, opts ...RunnerO
 }
 
 // initObjectStore initializes the cluster object store interface
-func initObjectStore(ctx context.Context, client cluster.ClientInterface, opts ...objectstore.Option) (store.Store, error) {
+func initObjectStore(ctx context.Context, client cluster.ClientInterface) (store.Store, error) {
 	if client == nil {
 		return nil, fmt.Errorf("nil cluster client")
 	}
 
-	appObjectStore, err := objectstore.NewDynamicCache(ctx, client, opts...)
+	resourceAccess := objectstore.NewResourceAccess(client)
+	appObjectStore, err := objectstore.NewDynamicCache(ctx, client, objectstore.Access(resourceAccess))
 
 	if err != nil {
 		return nil, fmt.Errorf("creating object store for app: %w", err)
diff --git a/pkg/navigation/navigation.go b/pkg/navigation/navigation.go
index de47b52a..d54e5de7 100644
--- a/pkg/navigation/navigation.go
+++ b/pkg/navigation/navigation.go
@@ -11,8 +11,6 @@ import (
 	"path"
 	"sort"
 
-	"go.opencensus.io/trace"
-
 	"github.com/vmware-tanzu/octant/internal/util/json"
 
 	"github.com/pkg/errors"
@@ -76,9 +74,6 @@ func New(title, navigationPath string, options ...Option) (*Navigation, error) {
 
 // CRDEntries generates navigation entries for CRDs.
 func CRDEntries(ctx context.Context, prefix, namespace string, objectStore store.Store, wantsClusterScoped bool) ([]Navigation, bool, error) {
-	ctx, span := trace.StartSpan(ctx, "navigation:CRDEntries")
-	defer span.End()
-
 	var list = []Navigation{}
 
 	loading := false
@@ -124,9 +119,6 @@ func CRDEntries(ctx context.Context, prefix, namespace string, objectStore store
 }
 
 func CustomResourceDefinitions(ctx context.Context, o store.Store) ([]*apiextv1.CustomResourceDefinition, bool, error) {
-	ctx, span := trace.StartSpan(ctx, "navigation:CustomResourceDefinitions")
-	defer span.End()
-
 	key := store.Key{
 		APIVersion: "apiextensions.k8s.io/v1",
 		Kind:       "CustomResourceDefinition",
diff --git a/pkg/plugin/javascript.go b/pkg/plugin/javascript.go
index c1a71582..7c0f7a7d 100644
--- a/pkg/plugin/javascript.go
+++ b/pkg/plugin/javascript.go
@@ -371,39 +371,29 @@ func (t *jsPlugin) PrintTabs(ctx context.Context, object runtime.Object) ([]TabR
 		return []TabResponse{}, err
 	}
 
-	responses, ok := tabResponse.Export().([]interface{})
-	if ok {
-		cTabs := []TabResponse{}
-		for _, response := range responses {
-			cTab := &component.Tab{}
-			tab, ok := response.(map[string]interface{})["tab"]
-			if !ok {
-				return []TabResponse{}, fmt.Errorf("tab property not found")
-			}
-			contents, ok := tab.(map[string]interface{})
-			if !ok {
-				return []TabResponse{}, fmt.Errorf("unable to get tab contents")
-			}
-			if name, ok := contents["name"]; ok {
-				cTab.Contents = *component.NewFlexLayout(name.(string))
-				cTab.Name = name.(string)
-			}
-			if contents, ok := contents["contents"]; ok {
-				realComponent, err := javascript.ConvertToComponent("tab contents", contents)
-				if err != nil {
-					return []TabResponse{}, fmt.Errorf("unable to extract component: %w", err)
-				}
-				realFlexLayout := *realComponent.(*component.FlexLayout)
-				cTab.Contents.Config = realFlexLayout.Config
-			}
-			cTabs = append(cTabs, TabResponse{Tab: cTab})
-		}
-		return cTabs, nil
+	tab := tabResponse.Get("tab")
+	if tab == goja.Undefined() {
+		return []TabResponse{}, fmt.Errorf("tab property not found")
 	}
 
-	cTab, err := generateTabFromResponse(tabResponse)
-	if err != nil {
-		return []TabResponse{}, err
+	contents, ok := tab.Export().(map[string]interface{})
+	if !ok {
+		return []TabResponse{}, fmt.Errorf("unable to export tab contents")
+	}
+
+	cTab := &component.Tab{}
+	if name, ok := contents["name"]; ok {
+		cTab.Contents = *component.NewFlexLayout(name.(string))
+		cTab.Name = name.(string)
+	}
+
+	if contents, ok := contents["contents"]; ok {
+		realComponent, err := javascript.ConvertToComponent("tab contents", contents)
+		if err != nil {
+			return []TabResponse{}, fmt.Errorf("unable to extract component: %w", err)
+		}
+		realFlexLayout := *realComponent.(*component.FlexLayout)
+		cTab.Contents.Config = realFlexLayout.Config
 	}
 
 	return []TabResponse{
@@ -682,31 +672,3 @@ func extractMetadata(vm *goja.Runtime, pluginValue goja.Value) (*Metadata, error
 
 	return metadata, nil
 }
-
-func generateTabFromResponse(response *goja.Object) (*component.Tab, error) {
-	tab := response.Get("tab")
-	if tab == goja.Undefined() || tab == nil {
-		return nil, fmt.Errorf("tab property not found")
-	}
-
-	contents, ok := tab.Export().(map[string]interface{})
-	if !ok {
-		return nil, fmt.Errorf("unable to export tab contents")
-	}
-
-	cTab := &component.Tab{}
-	if name, ok := contents["name"]; ok {
-		cTab.Contents = *component.NewFlexLayout(name.(string))
-		cTab.Name = name.(string)
-	}
-
-	if contents, ok := contents["contents"]; ok {
-		realComponent, err := javascript.ConvertToComponent("tab contents", contents)
-		if err != nil {
-			return nil, fmt.Errorf("unable to extract component: %w", err)
-		}
-		realFlexLayout := *realComponent.(*component.FlexLayout)
-		cTab.Contents.Config = realFlexLayout.Config
-	}
-	return cTab, nil
-}
diff --git a/pkg/store/fake/mock_store.go b/pkg/store/fake/mock_store.go
index ad4697a3..d04b3d9f 100644
--- a/pkg/store/fake/mock_store.go
+++ b/pkg/store/fake/mock_store.go
@@ -128,6 +128,18 @@ func (mr *MockStoreMockRecorder) List(arg0, arg1 interface{}) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockStore)(nil).List), arg0, arg1)
 }
 
+// RegisterOnUpdate mocks base method
+func (m *MockStore) RegisterOnUpdate(arg0 store.UpdateFn) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "RegisterOnUpdate", arg0)
+}
+
+// RegisterOnUpdate indicates an expected call of RegisterOnUpdate
+func (mr *MockStoreMockRecorder) RegisterOnUpdate(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterOnUpdate", reflect.TypeOf((*MockStore)(nil).RegisterOnUpdate), arg0)
+}
+
 // Unwatch mocks base method
 func (m *MockStore) Unwatch(arg0 context.Context, arg1 ...schema.GroupVersionKind) error {
 	m.ctrl.T.Helper()
diff --git a/pkg/store/store.go b/pkg/store/store.go
index 3637937f..b6621bc6 100644
--- a/pkg/store/store.go
+++ b/pkg/store/store.go
@@ -39,6 +39,7 @@ type Store interface {
 	Watch(ctx context.Context, key Key, handler cache.ResourceEventHandler) error
 	Unwatch(ctx context.Context, groupVersionKinds ...schema.GroupVersionKind) error
 	UpdateClusterClient(ctx context.Context, client cluster.ClientInterface) error
+	RegisterOnUpdate(fn UpdateFn)
 	Update(ctx context.Context, key Key, updater func(*unstructured.Unstructured) error) error
 	IsLoading(ctx context.Context, key Key) bool
 	Create(ctx context.Context, object *unstructured.Unstructured) error
diff --git a/web/src/app/modules/shared/components/view/view-container.component.ts b/web/src/app/modules/shared/components/view/view-container.component.ts
index 1961afd5..252da1d0 100644
--- a/web/src/app/modules/shared/components/view/view-container.component.ts
+++ b/web/src/app/modules/shared/components/view/view-container.component.ts
@@ -38,6 +38,8 @@ interface Viewer {
 })
 export class ViewContainerComponent implements OnInit, AfterViewInit {
   @ViewChild(ViewHostDirective, { static: true }) appView: ViewHostDirective;
+  lastViewType: string;
+
   @Input() set view(v: View) {
     if (v && v.metadata) {
       const cur = JSON.stringify(v);
@@ -79,7 +81,7 @@ export class ViewContainerComponent implements OnInit, AfterViewInit {
   }
 
   loadView(view: View) {
-    if (!this.componentRef) {
+    if (!this.componentRef || this.lastViewType !== view.metadata.type) {
       const viewType = view.metadata.type;
       let component: Type<any> = this.componentMappings[viewType];
       if (!component) {


Test patch:
diff --git a/internal/config/dash_test.go b/internal/config/dash_test.go
index 15632ce2..d124305f 100644
--- a/internal/config/dash_test.go
+++ b/internal/config/dash_test.go
@@ -91,6 +91,9 @@ func TestLiveConfig(t *testing.T) {
 	portForwarder := portForwardFake.NewMockPortForwarder(controller)
 	buildInfo := BuildInfo{}
 
+	objectStore.EXPECT().
+		RegisterOnUpdate(gomock.Any())
+
 	restConfigOptions := cluster.RESTConfigOptions{}
 
 	config := NewLiveConfig(
@@ -137,6 +140,9 @@ func TestLiveConfig_UseContext_WithContextChosenByUISetToTrue(t *testing.T) {
 	portForwarder := portForwardFake.NewMockPortForwarder(controller)
 	buildInfo := BuildInfo{}
 
+	objectStore.EXPECT().
+		RegisterOnUpdate(gomock.Any())
+
 	restConfigOptions := cluster.RESTConfigOptions{}
 
 	contextDecorator := configFake.NewMockKubeContextDecorator(controller)
@@ -194,6 +200,9 @@ func TestLiveConfig_UseContext_WithContextChosenByUISetToFalse(t *testing.T) {
 	portForwarder := portForwardFake.NewMockPortForwarder(controller)
 	buildInfo := BuildInfo{}
 
+	objectStore.EXPECT().
+		RegisterOnUpdate(gomock.Any())
+
 	restConfigOptions := cluster.RESTConfigOptions{}
 
 	contextDecorator := configFake.NewMockKubeContextDecorator(controller)
diff --git a/internal/describer/crd_watcher_test.go b/internal/describer/crd_watcher_test.go
index 936536ce..a0eb625b 100644
--- a/internal/describer/crd_watcher_test.go
+++ b/internal/describer/crd_watcher_test.go
@@ -50,6 +50,8 @@ func TestDefaultCRDWatcher_Watch(t *testing.T) {
 			assert.NotNil(t, c.DeleteFunc)
 			return nil
 		})
+	objectStore.EXPECT().
+		RegisterOnUpdate(gomock.Any())
 	errorStore, err := internalErr.NewErrorStore()
 	require.NoError(t, err)
 
@@ -80,6 +82,8 @@ func TestDefaultCRDWatcher_Watch_failure(t *testing.T) {
 		DoAndReturn(func(_ context.Context, _ store.Key, c *cache.ResourceEventHandlerFuncs) error {
 			return errors.New("failure")
 		})
+	objectStore.EXPECT().
+		RegisterOnUpdate(gomock.Any())
 	errorStore, err := internalErr.NewErrorStore()
 	require.NoError(t, err)
 
diff --git a/internal/objectstore/access_test.go b/internal/objectstore/access_test.go
new file mode 100644
index 00000000..d88d4614
--- /dev/null
+++ b/internal/objectstore/access_test.go
@@ -0,0 +1,131 @@
+package objectstore
+
+import (
+	"context"
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	clusterfake "github.com/vmware-tanzu/octant/internal/cluster/fake"
+	"github.com/vmware-tanzu/octant/pkg/store"
+)
+
+func Test_accessCache(t *testing.T) {
+	c := newAccessCache()
+
+	key := AccessKey{
+		Namespace: "test",
+		Group:     "group",
+		Resource:  "resource",
+		Verb:      "list",
+	}
+
+	c.set(key, true)
+
+	got, isFound := c.get(key)
+	require.True(t, isFound)
+	require.True(t, got)
+}
+
+func Test_ResourceAccess_HasAccess(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	controller := gomock.NewController(t)
+	defer controller.Finish()
+
+	client := clusterfake.NewMockClientInterface(controller)
+	namespaceClient := clusterfake.NewMockNamespaceInterface(controller)
+
+	client.EXPECT().NamespaceClient().Return(namespaceClient, nil).MaxTimes(3)
+	namespaces := []string{"test", ""}
+	namespaceClient.EXPECT().Names().Return(namespaces, nil).MaxTimes(3)
+
+	r := NewResourceAccess(client)
+
+	scenarios := []struct {
+		name        string
+		resource    string
+		key         store.Key
+		setupAccess func()
+		expectErr   bool
+	}{
+		{
+			name:     "pods",
+			resource: "pods",
+			key: store.Key{
+				APIVersion: "apps/v1",
+				Kind:       "Pod",
+			},
+			setupAccess: func() {
+				aKey := AccessKey{
+					Namespace: "",
+					Group:     "apps",
+					Resource:  "pods",
+					Verb:      "get",
+				}
+				r.Set(aKey, true)
+			},
+			expectErr: false,
+		},
+		{
+			name:     "crds",
+			resource: "customresourcedefinitions",
+			key: store.Key{
+				APIVersion: "apiextensions.k8s.io/v1",
+				Kind:       "CustomResourceDefinition",
+			},
+			setupAccess: func() {
+				aKey := AccessKey{
+					Namespace: "",
+					Group:     "apiextensions.k8s.io",
+					Resource:  "customresourcedefinitions",
+					Verb:      "get",
+				}
+				r.Set(aKey, true)
+			},
+			expectErr: false,
+		},
+		{
+			name:     "no access crds",
+			resource: "customresourcedefinitions",
+			key: store.Key{
+				APIVersion: "apiextensions.k8s.io/v1",
+				Kind:       "CustomResourceDefinition",
+			},
+			setupAccess: func() {
+				aKey := AccessKey{
+					Namespace: "",
+					Group:     "apiextensions.k8s.io",
+					Resource:  "customresourcedefinitions",
+					Verb:      "get",
+				}
+				r.Set(aKey, false)
+			},
+			expectErr: true,
+		},
+	}
+
+	for i := range scenarios {
+		ts := scenarios[i]
+		t.Run(ts.name, func(t *testing.T) {
+			ts.setupAccess()
+
+			gvk := ts.key.GroupVersionKind()
+			podGVR := schema.GroupVersionResource{
+				Group:    gvk.Group,
+				Version:  gvk.Version,
+				Resource: ts.resource,
+			}
+			client.EXPECT().Resource(gomock.Eq(gvk.GroupKind())).Return(podGVR, true, nil)
+
+			if ts.expectErr {
+				require.Error(t, r.HasAccess(ctx, ts.key, "get"))
+			} else {
+				require.NoError(t, r.HasAccess(ctx, ts.key, "get"))
+			}
+		})
+	}
+}
diff --git a/internal/objectstore/accessors_test.go b/internal/objectstore/accessors_test.go
new file mode 100644
index 00000000..59fa4135
--- /dev/null
+++ b/internal/objectstore/accessors_test.go
@@ -0,0 +1,119 @@
+package objectstore
+
+import (
+	"context"
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/vmware-tanzu/octant/internal/cluster/fake"
+	"github.com/vmware-tanzu/octant/internal/gvk"
+	"github.com/vmware-tanzu/octant/pkg/store"
+)
+
+func Test_informerSynced(t *testing.T) {
+	c := initInformerSynced()
+	key := store.Key{APIVersion: "apiVersion"}
+	otherKey := store.Key{APIVersion: "apiVersion2"}
+	require.True(t, c.hasSynced(key))
+
+	c.setSynced(key, true)
+	require.True(t, c.hasSynced(key))
+	require.True(t, c.hasSynced(otherKey))
+
+	c.setSynced(key, false)
+	require.False(t, c.hasSynced(key))
+}
+
+func Test_factoriesCache(t *testing.T) {
+	const namespaceName = "test"
+
+	controller := gomock.NewController(t)
+	defer controller.Finish()
+
+	client := fake.NewMockClientInterface(controller)
+
+	c := initFactoriesCache()
+
+	ctx := context.Background()
+	factory, err := initInformerFactory(ctx, client, namespaceName)
+	require.NoError(t, err)
+
+	c.set(namespaceName, factory)
+
+	got, isFound := c.get(namespaceName)
+	require.True(t, isFound)
+	require.Equal(t, factory, got)
+
+	c.delete(namespaceName)
+	_, isFound = c.get(namespaceName)
+	require.False(t, isFound)
+}
+
+func Test_seenGVKsCache(t *testing.T) {
+	c := initSeenGVKsCache()
+	c.setSeen("test", gvk.Pod, true)
+
+	tests := []struct {
+		name      string
+		namespace string
+		gvk       schema.GroupVersionKind
+		expected  bool
+	}{
+		{
+			name:      "gvk that has been seen",
+			namespace: "test",
+			gvk:       gvk.Pod,
+			expected:  true,
+		},
+		{
+			name:      "namespace that has not been seen",
+			namespace: "other",
+			gvk:       gvk.Pod,
+			expected:  false,
+		},
+		{
+			name:      "gvk that has not been seen",
+			namespace: "test",
+			gvk:       gvk.Deployment,
+			expected:  false,
+		},
+	}
+
+	for i := range tests {
+		test := tests[i]
+
+		t.Run(test.name, func(t *testing.T) {
+			got := c.hasSeen(test.namespace, test.gvk)
+			require.Equal(t, test.expected, got)
+		})
+	}
+}
+
+func Test_informerContextCache(t *testing.T) {
+	c := initInformerContextCache()
+
+	groupVersionKind1 := schema.GroupVersionKind{
+		Group:   "group",
+		Version: "version",
+		Kind:    "resource1",
+	}
+	groupVersionKind2 := schema.GroupVersionKind{
+		Group:   "group",
+		Version: "version",
+		Kind:    "resource2",
+	}
+	_ = c.addChild(groupVersionKind1)
+	assert.Len(t, c.cache, 1)
+	_ = c.addChild(groupVersionKind1)
+	assert.Len(t, c.cache, 1)
+	_ = c.addChild(groupVersionKind2)
+	assert.Len(t, c.cache, 2)
+	c.delete(groupVersionKind1)
+	assert.Len(t, c.cache, 1)
+	c.reset()
+	assert.Len(t, c.cache, 0)
+}
diff --git a/internal/objectstore/backoff_test.go b/internal/objectstore/backoff_test.go
new file mode 100644
index 00000000..cb5128a3
--- /dev/null
+++ b/internal/objectstore/backoff_test.go
@@ -0,0 +1,31 @@
+package objectstore
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/vmware-tanzu/octant/pkg/store"
+)
+
+func Test_waiting(t *testing.T) {
+	key := store.Key{APIVersion: "apiVersion"}
+	entry := newBackoffEntry(key, defaultBackoff)
+
+	assert.False(t, entry.isWaiting())
+	entry.setWaiting(true)
+	assert.True(t, entry.isWaiting())
+}
+
+func Test_wait(t *testing.T) {
+	key := store.Key{APIVersion: "apiVersion"}
+	entry := newBackoffEntry(key, defaultBackoff)
+	d := entry.wait()
+
+	assert.True(t, d > time.Second)
+	d = entry.wait()
+	assert.True(t, d > (time.Second*2))
+	d = entry.wait()
+	assert.True(t, d > (time.Second*4))
+}
diff --git a/internal/objectstore/dynamic_cache_test.go b/internal/objectstore/dynamic_cache_test.go
index 8de0196e..d5bc9a78 100644
--- a/internal/objectstore/dynamic_cache_test.go
+++ b/internal/objectstore/dynamic_cache_test.go
@@ -1,1 +1,27 @@
 package objectstore
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/vmware-tanzu/octant/internal/gvk"
+	"github.com/vmware-tanzu/octant/pkg/store"
+)
+
+func TestDynamicCache_backoff(t *testing.T) {
+	d := &DynamicCache{
+		factories: initFactoriesCache(),
+	}
+
+	ctx := context.TODO()
+	key := store.Key{APIVersion: gvk.Pod.Version, Kind: gvk.Pod.Kind}
+
+	tD := d.backoff(ctx, key)
+	require.True(t, d.isBackingOff(ctx, key))
+	<-time.After(tD + (time.Millisecond * 250))
+	assert.False(t, d.isBackingOff(ctx, key))
+}
diff --git a/pkg/dash/dash_test.go b/pkg/dash/dash_test.go
index ab393943..0f006585 100644
--- a/pkg/dash/dash_test.go
+++ b/pkg/dash/dash_test.go
@@ -8,7 +8,6 @@ package dash
 
 import (
 	"context"
-	"encoding/base64"
 	"fmt"
 	"io/ioutil"
 	"net"
@@ -21,6 +20,8 @@ import (
 	"testing"
 	"time"
 
+	v1 "k8s.io/api/authorization/v1"
+
 	"github.com/golang/mock/gomock"
 	"github.com/gorilla/websocket"
 	"github.com/spf13/afero"
@@ -28,13 +29,13 @@ import (
 	"github.com/stretchr/testify/require"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 
-	"github.com/vmware-tanzu/octant/internal/cluster"
 	"github.com/vmware-tanzu/octant/internal/util/json"
 
-	clusterFake "github.com/vmware-tanzu/octant/internal/cluster/fake"
+	"github.com/vmware-tanzu/octant/internal/cluster"
 	internalLog "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"
 )
 
@@ -180,25 +181,11 @@ func TestNewRunnerUsesClusterClient(t *testing.T) {
 	clusterClient := mockClusterClientReturningNamespace(controller, namespace)
 	listener := NewInMemoryListener()
 
-	sharedIndexInformer := clusterFake.NewMockSharedIndexInformer(controller)
-	sharedIndexInformer.EXPECT().SetWatchErrorHandler(gomock.Any())
-	sharedIndexInformer.EXPECT().AddEventHandlerWithResyncPeriod(gomock.Any(), gomock.Any())
-	sharedIndexInformer.EXPECT().Run(gomock.Any()).AnyTimes()
-
-	genericInformer := clusterFake.NewMockGenericInformer(controller)
-	genericInformer.EXPECT().Informer().Return(sharedIndexInformer).AnyTimes()
-	genericInformer.EXPECT().Lister().AnyTimes()
-
-	crds := schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}
-	dynamicInformerFactory := clusterFake.NewMockDynamicSharedInformerFactory(controller)
-	dynamicInformerFactory.EXPECT().ForResource(crds).Return(genericInformer)
-
 	logger := internalLog.NopLogger()
 	cancel, err := makeRunner(
 		logger,
 		WithClusterClient(clusterClient),
 		WithListener(listener),
-		WithDynamicSharedInformerFactory(dynamicInformerFactory),
 	)
 	require.NoError(t, err)
 	defer cancel()
@@ -211,50 +198,54 @@ func TestNewRunnerUsesClusterClient(t *testing.T) {
 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})
-	nsClient.EXPECT().Names().AnyTimes()
-	nsClient.EXPECT().HasNamespace(namespace).Return(true)
-
-	dynamicClient := clusterFake.NewMockDynamicInterface(controller)
-
+	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().DynamicClient().Return(dynamicClient, nil)
-	clusterClient.EXPECT().RESTClient()
-	clusterClient.EXPECT().RESTConfig()
+	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) {
-	srv := fakeK8sAPIThatForbidsWatchingCRDs()
-	defer srv.Close()
-	stubRiceBox("dist/octant")
-
-	listener := NewInMemoryListener()
-	cancel, err := makeRunner(internalLog.NopLogger(), WithListener(listener))
-	require.NoError(t, err)
-	defer cancel()
-	kubeConfig := makeKubeConfig("test-context", srv.URL)
-	websocketWrite(
-		fmt.Sprintf(`{
-	"type": "action.octant.dev/uploadKubeConfig",
-	"payload": {"kubeConfig": "%s"}
-}`, base64.StdEncoding.EncodeToString(kubeConfig)),
-		listener,
-	)
-	// wait for API to reload
-	for {
-		if websocketReadTimeout(listener, 10*time.Millisecond) {
-			break
-		}
-	}
-	kubeConfigEvent, err := waitForEventOfType(listener, event.EventTypeKubeConfig)
-	require.NoError(t, err)
-
-	require.Equal(t, "test-context", kubeConfigEvent.Data["currentContext"].(string))
-}
+//func TestNewRunnerRunsLoadingAPIWhenStartedWithoutKubeConfig(t *testing.T) {
+//	srv := fakeK8sAPIThatForbidsWatchingCRDs()
+//	defer srv.Close()
+//	stubRiceBox("dist/octant")
+//
+//	listener := NewInMemoryListener()
+//	cancel, err := makeRunner(internalLog.NopLogger(), WithListener(listener))
+//	require.NoError(t, err)
+//	defer cancel()
+//	kubeConfig := makeKubeConfig("test-context", srv.URL)
+//	websocketWrite(
+//		fmt.Sprintf(`{
+//	"type": "action.octant.dev/uploadKubeConfig",
+//	"payload": {"kubeConfig": "%s"}
+//}`, base64.StdEncoding.EncodeToString(kubeConfig)),
+//		listener,
+//	)
+//	// wait for API to reload
+//	for {
+//		if websocketReadTimeout(listener, 10*time.Millisecond) {
+//			break
+//		}
+//	}
+//	kubeConfigEvent, err := waitForEventOfType(listener, event.EventTypeKubeConfig)
+//	require.NoError(t, err)
+//
+//	require.Equal(t, "test-context", kubeConfigEvent.Data["currentContext"].(string))
+//}
 
 func TestNewRunnerShutsDownPluginsWhenStoppedBeforeReceivingKubeConfig(t *testing.T) {
 	stubRiceBox("dist/octant")