Repository: vmware-tanzu/octant
Pull request #555 (2020-01-21 23:14:24)

Use daysjs instead of moment
This PR removes a library used for timestamp parsing and formatting for a lightweight alternative. It provides a modest reduction of bundle size.

See: https://github.com/you-dont-need/You-Dont-Need-Momentjs

Before:
```
chunk {0} runtime-es2015.85f895af57b038f1e5b4.js (runtime) 2.82 kB [entry] [rendered]
chunk {1} main-es2015.74a311777ed32990fc10.js (main) 4.69 MB [initial] [rendered]
chunk {2} polyfills-es2015.bbb3ae487191bcb3d1d0.js (polyfills) 64 kB [initial] [rendered]
chunk {3} polyfills-es5-es2015.5c49e3c64e17bea52deb.js (polyfills-es5) 222 kB [initial] [rendered]
chunk {4} styles.4fd7d554c62d3defbf67.css (styles) 12.2 kB [initial] [rendered]
chunk {scripts} scripts.30bb7001f84db42c8b29.js (scripts) 635 kB [entry] [rendered]
Date: 2020-01-21T00:02:58.155Z - Hash: 09b1f591a1e87126e66f - Time: 115808ms

WARNING in budgets, maximum exceeded for initial. Budget 3 MB was exceeded by 2.6 MB.
```

After:
```
chunk {0} runtime-es2015.85f895af57b038f1e5b4.js (runtime) 2.82 kB [entry] [rendered]
chunk {1} main-es2015.93007daf9d35dafbf506.js (main) 4.39 MB [initial] [rendered]
chunk {2} polyfills-es2015.bbb3ae487191bcb3d1d0.js (polyfills) 64 kB [initial] [rendered]
chunk {3} polyfills-es5-es2015.5c49e3c64e17bea52deb.js (polyfills-es5) 222 kB [initial] [rendered]
chunk {4} styles.4fd7d554c62d3defbf67.css (styles) 12.2 kB [initial] [rendered]
chunk {scripts} scripts.30bb7001f84db42c8b29.js (scripts) 635 kB [entry] [rendered]
Date: 2020-01-21T23:07:54.782Z - Hash: 443eff9b6e03ebcae6eb - Time: 100070ms

WARNING in budgets, maximum exceeded for initial. Budget 3 MB was exceeded by 2.31 MB.
```


Signed-off-by: GuessWhoSamFoo <foos@vmware.com>


Patch:
diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go
index bf021654..3685a054 100644
--- a/internal/cluster/cluster.go
+++ b/internal/cluster/cluster.go
@@ -48,7 +48,6 @@ type ClientInterface interface {
 	DefaultNamespace() string
 	ResourceExists(schema.GroupVersionResource) bool
 	Resource(schema.GroupKind) (schema.GroupVersionResource, error)
-	ResetMapper()
 	KubernetesClient() (kubernetes.Interface, error)
 	DynamicClient() (dynamic.Interface, error)
 	DiscoveryClient() (discovery.DiscoveryInterface, error)
@@ -170,10 +169,6 @@ func (c *Cluster) Resource(gk schema.GroupKind) (schema.GroupVersionResource, er
 	return restMapping.Resource, nil
 }
 
-func (c *Cluster) ResetMapper() {
-	c.restMapper.Reset()
-}
-
 // KubernetesClient returns a Kubernetes client.
 func (c *Cluster) KubernetesClient() (kubernetes.Interface, error) {
 	return c.kubernetesClient, nil
diff --git a/internal/cluster/fake/mock_client_interface.go b/internal/cluster/fake/mock_client_interface.go
index 36589633..d31b0976 100644
--- a/internal/cluster/fake/mock_client_interface.go
+++ b/internal/cluster/fake/mock_client_interface.go
@@ -81,18 +81,6 @@ func (mr *MockClientInterfaceMockRecorder) Resource(arg0 interface{}) *gomock.Ca
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resource", reflect.TypeOf((*MockClientInterface)(nil).Resource), arg0)
 }
 
-// ResetMapper mocks base method
-func (m *MockClientInterface) ResetMapper() {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "ResetMapper")
-}
-
-// ResetMapper indicates an expected call of ResetMapper
-func (mr *MockClientInterfaceMockRecorder) ResetMapper() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetMapper", reflect.TypeOf((*MockClientInterface)(nil).ResetMapper))
-}
-
 // KubernetesClient mocks base method
 func (m *MockClientInterface) KubernetesClient() (kubernetes.Interface, error) {
 	m.ctrl.T.Helper()
diff --git a/internal/config/dash.go b/internal/config/dash.go
index 8b1c701b..7476643c 100644
--- a/internal/config/dash.go
+++ b/internal/config/dash.go
@@ -26,8 +26,7 @@ import (
 
 // CRDWatcher watches for CRDs.
 type CRDWatcher interface {
-	Watch(ctx context.Context) error
-	AddConfig(config *CRDWatchConfig) error
+	Watch(ctx context.Context, config *CRDWatchConfig) error
 }
 
 // ObjectHandler is a function that is run when a new object is available.
diff --git a/internal/describer/crd_watcher.go b/internal/describer/crd_watcher.go
index b6aa366f..7fb8b9e2 100644
--- a/internal/describer/crd_watcher.go
+++ b/internal/describer/crd_watcher.go
@@ -10,26 +10,20 @@ import (
 	"fmt"
 	"sync"
 
-	"github.com/google/uuid"
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	kcache "k8s.io/client-go/tools/cache"
 
-	"github.com/vmware-tanzu/octant/internal/cluster"
 	"github.com/vmware-tanzu/octant/internal/config"
 	internalErr "github.com/vmware-tanzu/octant/internal/errors"
 	"github.com/vmware-tanzu/octant/internal/log"
-	"github.com/vmware-tanzu/octant/internal/util/kubernetes"
 	"github.com/vmware-tanzu/octant/pkg/store"
 )
 
 // DefaultCRDWatcher is the default CRD watcher.
 type DefaultCRDWatcher struct {
-	objectStore   store.Store
-	clusterClient cluster.ClientInterface
-	errorStore    internalErr.ErrorStore
-
-	watchConfigs map[string]*config.CRDWatchConfig
+	objectStore store.Store
+	errorStore  internalErr.ErrorStore
 
 	accessError sync.Once
 	mu          sync.Mutex
@@ -38,16 +32,14 @@ type DefaultCRDWatcher struct {
 var _ config.CRDWatcher = (*DefaultCRDWatcher)(nil)
 
 // NewDefaultCRDWatcher creates an instance of DefaultCRDWatcher.
-func NewDefaultCRDWatcher(ctx context.Context, clusterClient cluster.ClientInterface, objectStore store.Store, errorStore internalErr.ErrorStore) (*DefaultCRDWatcher, error) {
+func NewDefaultCRDWatcher(ctx context.Context, objectStore store.Store, errorStore internalErr.ErrorStore) (*DefaultCRDWatcher, error) {
 	if objectStore == nil {
 		return nil, errors.New("object store is nil")
 	}
 
 	cw := &DefaultCRDWatcher{
-		objectStore:   objectStore,
-		clusterClient: clusterClient,
-		errorStore:    errorStore,
-		watchConfigs:  make(map[string]*config.CRDWatchConfig),
+		objectStore: objectStore,
+		errorStore:  errorStore,
 	}
 
 	objectStore.RegisterOnUpdate(func(newObjectStore store.Store) {
@@ -71,66 +63,22 @@ var (
 )
 
 // Watch watches for CRDs given a configuration.
-func (cw *DefaultCRDWatcher) Watch(ctx context.Context) error {
-	logger := log.From(ctx)
-
-	handler := &kcache.ResourceEventHandlerFuncs{
-		AddFunc: func(object interface{}) {
-			cw.mu.Lock()
-			defer cw.mu.Unlock()
-
-			logger := logger.With("crdwatcher", "add")
-
-			u, ok := object.(*unstructured.Unstructured)
-			if !ok {
-				logger.
-					With("object-type", fmt.Sprintf("%T", object)).
-					Warnf("crd watcher received a non dynamic object")
-				return
-			}
-
-			cw.clusterClient.ResetMapper()
-
-			for _, watchConfig := range cw.watchConfigs {
-				if watchConfig.CanPerform(u) {
-					watchConfig.Add(ctx, u)
-				}
-			}
-		},
-		DeleteFunc: func(object interface{}) {
-			cw.mu.Lock()
-			defer cw.mu.Unlock()
-
-			logger := logger.With("crdwatcher", "delete")
-
-			u, ok := object.(*unstructured.Unstructured)
-			if !ok {
-				logger.
-					With("object-type", fmt.Sprintf("%T", object)).
-					Warnf("crd watcher received a non dynamic object")
-				return
-			}
-
-			cw.clusterClient.ResetMapper()
-
-			for _, watchConfig := range cw.watchConfigs {
-				if watchConfig.CanPerform(u) {
-					watchConfig.Delete(ctx, u)
-				}
-			}
+func (cw *DefaultCRDWatcher) Watch(ctx context.Context, watchConfig *config.CRDWatchConfig) error {
+	if watchConfig == nil {
+		return errors.New("watch config is nil")
+	}
 
-			list, err := kubernetes.CRDResources(u)
-			if err != nil {
-				logger.WithErr(err).Errorf("unable to get group/version/kinds for CRD")
+	cw.mu.Lock()
+	defer cw.mu.Unlock()
 
-			}
+	handler := &kcache.ResourceEventHandlerFuncs{}
 
-			if err := cw.objectStore.Unwatch(ctx, list...); err != nil {
-				logger.WithErr(err).Errorf("unable to unwatch CRD")
-				return
-			}
+	if watchConfig.Add != nil {
+		handler.AddFunc = performWatch(ctx, watchConfig.CanPerform, watchConfig.Add)
+	}
 
-		},
+	if watchConfig.Delete != nil {
+		handler.DeleteFunc = performWatch(ctx, watchConfig.CanPerform, watchConfig.Delete)
 	}
 
 	err := cw.objectStore.Watch(ctx, crdKey, handler)
@@ -140,6 +88,7 @@ func (cw *DefaultCRDWatcher) Watch(ctx context.Context) error {
 			found := cw.errorStore.Add(aErr)
 			// Log if we have not seen this access error before.
 			if !found {
+				logger := log.From(ctx)
 				logger.WithErr(aErr).Errorf("access denied")
 			}
 			return nil
@@ -150,21 +99,6 @@ func (cw *DefaultCRDWatcher) Watch(ctx context.Context) error {
 	return nil
 }
 
-// AddConfig adds watch config to the watcher.
-func (cw *DefaultCRDWatcher) AddConfig(watchConfig *config.CRDWatchConfig) error {
-	if watchConfig == nil {
-		return fmt.Errorf("watch config is nil")
-	}
-
-	cw.mu.Lock()
-	defer cw.mu.Unlock()
-
-	id := uuid.New().String()
-	cw.watchConfigs[id] = watchConfig
-
-	return nil
-}
-
 func performWatch(ctx context.Context, canPerform func(*unstructured.Unstructured) bool, handler config.ObjectHandler) func(object interface{}) {
 	return func(object interface{}) {
 		u, ok := object.(*unstructured.Unstructured)
diff --git a/internal/describer/customresource.go b/internal/describer/customresource.go
index 937c6793..69e90c68 100644
--- a/internal/describer/customresource.go
+++ b/internal/describer/customresource.go
@@ -15,6 +15,7 @@ import (
 	"github.com/vmware-tanzu/octant/internal/gvk"
 	"github.com/vmware-tanzu/octant/internal/log"
 	"github.com/vmware-tanzu/octant/internal/module"
+	"github.com/vmware-tanzu/octant/internal/util/kubernetes"
 	"github.com/vmware-tanzu/octant/pkg/store"
 )
 
@@ -55,7 +56,7 @@ func AddCRD(ctx context.Context, crd *unstructured.Unstructured, pm *PathMatcher
 	}
 }
 
-func DeleteCRD(ctx context.Context, crd *unstructured.Unstructured, pm *PathMatcher, crdSection *CRDSection, m module.Module) {
+func DeleteCRD(ctx context.Context, crd *unstructured.Unstructured, pm *PathMatcher, crdSection *CRDSection, m module.Module, s store.Store) {
 	name := crd.GetName()
 
 	logger := log.From(ctx).With("crd-name", name, "module", m.Name())
@@ -70,6 +71,16 @@ func DeleteCRD(ctx context.Context, crd *unstructured.Unstructured, pm *PathMatc
 		logger.WithErr(err).Errorf("unable to remove CRD")
 	}
 
+	list, err := kubernetes.CRDResources(crd)
+	if err != nil {
+		logger.WithErr(err).Errorf("unable to get group/version/kinds for CRD")
+
+	}
+
+	if err := s.Unwatch(ctx, list...); err != nil {
+		logger.WithErr(err).Errorf("unable to unwatch CRD")
+		return
+	}
 }
 
 func crdListPath(name string) string {
diff --git a/internal/modules/clusteroverview/clusteroverview.go b/internal/modules/clusteroverview/clusteroverview.go
index 25259932..e641d526 100644
--- a/internal/modules/clusteroverview/clusteroverview.go
+++ b/internal/modules/clusteroverview/clusteroverview.go
@@ -71,6 +71,7 @@ func New(ctx context.Context, options Options) (*ClusterOverview, error) {
 	}
 
 	crdWatcher := options.DashConfig.CRDWatcher()
+	objectStore := co.DashConfig.ObjectStore()
 	watchConfig := &config.CRDWatchConfig{
 		Add: func(_ *describer.PathMatcher, sectionDescriber *describer.CRDSection) config.ObjectHandler {
 			return func(ctx context.Context, object *unstructured.Unstructured) {
@@ -92,7 +93,7 @@ func New(ctx context.Context, options Options) (*ClusterOverview, error) {
 				if object == nil {
 					return
 				}
-				describer.DeleteCRD(ctx, object, pathMatcher, customResourcesDescriber, co)
+				describer.DeleteCRD(ctx, object, pathMatcher, customResourcesDescriber, co, objectStore)
 				var list []*unstructured.Unstructured
 				for i := range co.watchedCRDs {
 					if co.watchedCRDs[i].GetUID() == object.GetUID() {
@@ -106,8 +107,8 @@ func New(ctx context.Context, options Options) (*ClusterOverview, error) {
 		IsNamespaced: false,
 	}
 
-	if err := crdWatcher.AddConfig(watchConfig); err != nil {
-		return nil, errors.Wrap(err, "create cluster scoped CRD watcher for cluster overview")
+	if err := crdWatcher.Watch(ctx, watchConfig); err != nil {
+		return nil, errors.Wrap(err, "create namespaced CRD watcher for overview")
 	}
 
 	return co, nil
@@ -251,7 +252,7 @@ func (co *ClusterOverview) SetContext(ctx context.Context, contextName string) e
 	defer co.mu.Unlock()
 
 	for i := range co.watchedCRDs {
-		describer.DeleteCRD(ctx, co.watchedCRDs[i], co.pathMatcher, customResourcesDescriber, co)
+		describer.DeleteCRD(ctx, co.watchedCRDs[i], co.pathMatcher, customResourcesDescriber, co, co.DashConfig.ObjectStore())
 	}
 
 	co.watchedCRDs = []*unstructured.Unstructured{}
diff --git a/internal/modules/overview/overview.go b/internal/modules/overview/overview.go
index d3df8ac8..0ac67dc7 100644
--- a/internal/modules/overview/overview.go
+++ b/internal/modules/overview/overview.go
@@ -90,7 +90,7 @@ func (co *Overview) SetContext(ctx context.Context, contextName string) error {
 	customResourcesDescriber := describer.NamespacedCRD()
 	co.contextName = contextName
 	for i := range co.watchedCRDs {
-		describer.DeleteCRD(ctx, co.watchedCRDs[i], co.pathMatcher, customResourcesDescriber, co)
+		describer.DeleteCRD(ctx, co.watchedCRDs[i], co.pathMatcher, customResourcesDescriber, co, co.dashConfig.ObjectStore())
 	}
 
 	co.watchedCRDs = []*unstructured.Unstructured{}
@@ -131,6 +131,8 @@ func (co *Overview) bootstrap(ctx context.Context) error {
 
 	crdWatcher := co.dashConfig.CRDWatcher()
 
+	objectStore := co.dashConfig.ObjectStore()
+
 	customResourcesDescriber := describer.NamespacedCRD()
 
 	watchConfig := &config.CRDWatchConfig{
@@ -154,7 +156,7 @@ func (co *Overview) bootstrap(ctx context.Context) error {
 				if object == nil {
 					return
 				}
-				describer.DeleteCRD(ctx, object, pathMatcher, customResourcesDescriber, co)
+				describer.DeleteCRD(ctx, object, pathMatcher, customResourcesDescriber, co, objectStore)
 				var list []*unstructured.Unstructured
 				for i := range co.watchedCRDs {
 					if co.watchedCRDs[i].GetUID() == object.GetUID() {
@@ -168,7 +170,7 @@ func (co *Overview) bootstrap(ctx context.Context) error {
 		IsNamespaced: true,
 	}
 
-	if err := crdWatcher.AddConfig(watchConfig); err != nil {
+	if err := crdWatcher.Watch(ctx, watchConfig); err != nil {
 		return errors.Wrap(err, "create namespaced CRD watcher for overview")
 	}
 
diff --git a/internal/objectstore/access.go b/internal/objectstore/access.go
index dff0ecb7..2cea5695 100644
--- a/internal/objectstore/access.go
+++ b/internal/objectstore/access.go
@@ -160,7 +160,7 @@ func (r *resourceAccess) keyToAccessKey(key store.Key, verb string) (AccessKey,
 
 	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)
+		return AccessKey{}, errors.Wrap(err, "client resource")
 	}
 
 	aKey := AccessKey{
diff --git a/internal/objectstore/accessors.go b/internal/objectstore/accessors.go
index d942ec5d..be2e2654 100644
--- a/internal/objectstore/accessors.go
+++ b/internal/objectstore/accessors.go
@@ -160,18 +160,18 @@ func (c *seenGVKsCache) reset() {
 }
 
 type informerContextCache struct {
-	cache map[schema.GroupVersionKind]chan struct{}
+	cache map[schema.GroupVersionResource]chan struct{}
 
 	mu sync.Mutex
 }
 
 func initInformerContextCache() *informerContextCache {
 	return &informerContextCache{
-		cache: make(map[schema.GroupVersionKind]chan struct{}),
+		cache: make(map[schema.GroupVersionResource]chan struct{}),
 	}
 }
 
-func (c *informerContextCache) addChild(key schema.GroupVersionKind) chan struct{} {
+func (c *informerContextCache) addChild(key schema.GroupVersionResource) <-chan struct{} {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
@@ -180,7 +180,7 @@ func (c *informerContextCache) addChild(key schema.GroupVersionKind) chan struct
 	return ch
 }
 
-func (c *informerContextCache) delete(key schema.GroupVersionKind) {
+func (c *informerContextCache) delete(key schema.GroupVersionResource) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
diff --git a/internal/objectstore/dynamic_cache.go b/internal/objectstore/dynamic_cache.go
index 75b26f67..9bf134f2 100644
--- a/internal/objectstore/dynamic_cache.go
+++ b/internal/objectstore/dynamic_cache.go
@@ -7,7 +7,6 @@ package objectstore
 
 import (
 	"context"
-	"fmt"
 	"sync"
 	"time"
 
@@ -38,7 +37,11 @@ const (
 )
 
 func initInformerFactory(ctx context.Context, client cluster.ClientInterface, namespace string) (InformerFactory, error) {
-	return newInformerFactory(ctx.Done(), client, defaultInformerResync, namespace), nil
+	dynamicClient, err := client.DynamicClient()
+	if err != nil {
+		return nil, err
+	}
+	return newInformerFactory(ctx.Done(), dynamicClient, defaultInformerResync, namespace), nil
 }
 
 // DynamicCacheOpt is an option for configuration DynamicCache.
@@ -135,12 +138,17 @@ func (dc *DynamicCache) currentInformer(ctx context.Context, key store.Key) (inf
 	}
 
 	gvk := key.GroupVersionKind()
+	gvr, err := dc.client.Resource(gvk.GroupKind())
+	if err != nil {
+		return nil, false, errors.Wrap(err, "client resource")
+	}
+
 	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)
+				return nil, false, err
 			}
 		} else {
 			factory, ok = dc.factories.get("")
@@ -152,10 +160,7 @@ func (dc *DynamicCache) currentInformer(ctx context.Context, key store.Key) (inf
 		dc.factories.set(key.Namespace, factory)
 	}
 
-	informer, err := factory.ForResource(gvk)
-	if err != nil {
-		return nil, false, fmt.Errorf("find informer for %s: %w", gvk, err)
-	}
+	informer := factory.ForResource(gvr)
 
 	dc.checkKeySynced(ctx, informer, key)
 	dc.seenGVKs.setSeen(key.Namespace, gvk, true)
@@ -186,7 +191,7 @@ func (dc *DynamicCache) List(ctx context.Context, key store.Key) (*unstructured.
 		if meta.IsNoMatchError(err) {
 			return &unstructured.UnstructuredList{}, false, nil
 		}
-		return nil, false, fmt.Errorf("check access to list %s: %w", key, err)
+		return nil, false, err
 	}
 
 	span.Annotate([]trace.Attribute{
@@ -276,7 +281,7 @@ func (dc *DynamicCache) Get(ctx context.Context, key store.Key) (*unstructured.U
 	defer span.End()
 
 	if err := dc.access.HasAccess(ctx, key, "get"); err != nil {
-		return nil, false, fmt.Errorf("check access for get to %s: %w", key, err)
+		return nil, false, err
 	}
 
 	span.Annotate([]trace.Attribute{
@@ -351,7 +356,7 @@ func (dc *DynamicCache) getFromDynamicClient(ctx context.Context, key store.Key)
 // supplied handler.
 func (dc *DynamicCache) Watch(ctx context.Context, key store.Key, handler kcache.ResourceEventHandler) error {
 	if err := dc.access.HasAccess(ctx, key, "watch"); err != nil {
-		return fmt.Errorf("check access to watch %s: %w", key, err)
+		return err
 	}
 
 	informer, _, err := dc.currentInformer(ctx, key)
@@ -369,9 +374,14 @@ func (dc *DynamicCache) Unwatch(ctx context.Context, groupVersionKinds ...schema
 		factory, ok := dc.factories.get(namespace)
 		if ok {
 			for _, groupVersionKind := range groupVersionKinds {
-				factory.Delete(groupVersionKind)
+				gvr, err := dc.client.Resource(groupVersionKind.GroupKind())
+				if err != nil {
+					return errors.Wrap(err, "get resource for key")
+				}
+				factory.Delete(gvr)
 			}
 		}
+
 	}
 
 	return nil
@@ -383,7 +393,7 @@ func (dc *DynamicCache) Delete(ctx context.Context, key store.Key) error {
 	defer span.End()
 
 	if err := dc.access.HasAccess(ctx, key, "delete"); err != nil {
-		return fmt.Errorf("check access to delete %s: %w", key, err)
+		return err
 	}
 
 	dynamicClient, err := dc.client.DynamicClient()
diff --git a/internal/objectstore/fake/mock_informer_factory.go b/internal/objectstore/fake/mock_informer_factory.go
index 1f3e4243..3d93b3e4 100644
--- a/internal/objectstore/fake/mock_informer_factory.go
+++ b/internal/objectstore/fake/mock_informer_factory.go
@@ -35,7 +35,7 @@ func (m *MockInformerFactory) EXPECT() *MockInformerFactoryMockRecorder {
 }
 
 // Delete mocks base method
-func (m *MockInformerFactory) Delete(arg0 schema.GroupVersionKind) {
+func (m *MockInformerFactory) Delete(arg0 schema.GroupVersionResource) {
 	m.ctrl.T.Helper()
 	m.ctrl.Call(m, "Delete", arg0)
 }
@@ -47,12 +47,11 @@ func (mr *MockInformerFactoryMockRecorder) Delete(arg0 interface{}) *gomock.Call
 }
 
 // ForResource mocks base method
-func (m *MockInformerFactory) ForResource(arg0 schema.GroupVersionKind) (informers.GenericInformer, error) {
+func (m *MockInformerFactory) ForResource(arg0 schema.GroupVersionResource) informers.GenericInformer {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "ForResource", arg0)
 	ret0, _ := ret[0].(informers.GenericInformer)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
+	return ret0
 }
 
 // ForResource indicates an expected call of ForResource
@@ -62,10 +61,10 @@ func (mr *MockInformerFactoryMockRecorder) ForResource(arg0 interface{}) *gomock
 }
 
 // WaitForCacheSync mocks base method
-func (m *MockInformerFactory) WaitForCacheSync(arg0 <-chan struct{}) map[schema.GroupVersionKind]bool {
+func (m *MockInformerFactory) WaitForCacheSync(arg0 <-chan struct{}) map[schema.GroupVersionResource]bool {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "WaitForCacheSync", arg0)
-	ret0, _ := ret[0].(map[schema.GroupVersionKind]bool)
+	ret0, _ := ret[0].(map[schema.GroupVersionResource]bool)
 	return ret0
 }
 
diff --git a/internal/objectstore/informer.go b/internal/objectstore/informer.go
index 701d5e59..2aa96271 100644
--- a/internal/objectstore/informer.go
+++ b/internal/objectstore/informer.go
@@ -1,34 +1,32 @@
 package objectstore
 
 import (
-	"fmt"
 	"sync"
 	"time"
 
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/dynamic/dynamicinformer"
 	"k8s.io/client-go/informers"
 	"k8s.io/client-go/tools/cache"
-
-	"github.com/vmware-tanzu/octant/internal/cluster"
 )
 
 //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
+	ForResource(gvr schema.GroupVersionResource) informers.GenericInformer
+	Delete(gvr schema.GroupVersionResource)
+	WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool
 }
 
 type informerFactory struct {
-	client        cluster.ClientInterface
+	client        dynamic.Interface
 	defaultResync time.Duration
 	namespace     string
 
 	lock                 sync.Mutex
-	informers            map[schema.GroupVersionKind]informers.GenericInformer
+	informers            map[schema.GroupVersionResource]informers.GenericInformer
 	tweakListOptions     dynamicinformer.TweakListOptionsFunc
 	stopCh               <-chan struct{}
 	informerContextCache *informerContextCache
@@ -36,80 +34,65 @@ type informerFactory struct {
 
 var _ InformerFactory = (*informerFactory)(nil)
 
-func newInformerFactory(stopCh <-chan struct{}, client cluster.ClientInterface, defaultResync time.Duration, namespace string) *informerFactory {
+func newInformerFactory(stopCh <-chan struct{}, client dynamic.Interface, defaultResync time.Duration, namespace string) *informerFactory {
 	return &informerFactory{
 		stopCh:               stopCh,
 		client:               client,
 		defaultResync:        defaultResync,
 		namespace:            namespace,
-		informers:            make(map[schema.GroupVersionKind]informers.GenericInformer),
+		informers:            map[schema.GroupVersionResource]informers.GenericInformer{},
 		informerContextCache: initInformerContextCache(),
 	}
 }
 
 // ForResource creates an informer and starts it given a group/version/resource.
-func (f *informerFactory) ForResource(groupVersionKind schema.GroupVersionKind) (informers.GenericInformer, error) {
+func (f *informerFactory) ForResource(gvr schema.GroupVersionResource) informers.GenericInformer {
 	f.lock.Lock()
 	defer f.lock.Unlock()
 
-	informer, exists := f.informers[groupVersionKind]
+	key := gvr
+	informer, exists := f.informers[key]
 	if exists && informer != nil {
-		return informer, nil
+		return informer
 	}
 
-	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)
-	}
+	informer = dynamicinformer.NewFilteredDynamicInformer(f.client, gvr, f.namespace, f.defaultResync, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+	f.informers[key] = informer
 
-	dynamicClient, err := f.client.DynamicClient()
-	if err != nil {
-		return nil, fmt.Errorf("get dynamic client: %w", err)
-	}
+	stopCh := f.informerContextCache.addChild(gvr)
+	go informer.Informer().Run(stopCh)
 
-	genericInformer := dynamicinformer.NewFilteredDynamicInformer(
-		dynamicClient,
-		gvr,
-		f.namespace,
-		f.defaultResync,
-		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
-		f.tweakListOptions)
-	f.informers[groupVersionKind] = genericInformer
-
-	go genericInformer.Informer().Run(stopCh)
-
-	return genericInformer, nil
+	return informer
 }
 
 // Delete deletes an informer given a a group/version/resource.
-func (f *informerFactory) Delete(groupVersionKind schema.GroupVersionKind) {
+func (f *informerFactory) Delete(gvr schema.GroupVersionResource) {
 	f.lock.Lock()
 	defer f.lock.Unlock()
 
-	f.informerContextCache.delete(groupVersionKind)
-	delete(f.informers, groupVersionKind)
-	f.informers[groupVersionKind] = nil
+	if _, ok := f.informers[gvr]; ok {
+		f.informerContextCache.delete(gvr)
+		delete(f.informers, gvr)
+		f.informers[gvr] = 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 {
+func (f *informerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool {
+	list := func() map[schema.GroupVersionResource]cache.SharedIndexInformer {
 		f.lock.Lock()
 		defer f.lock.Unlock()
 
-		shared := map[schema.GroupVersionKind]cache.SharedIndexInformer{}
+		shared := map[schema.GroupVersionResource]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)
+	res := map[schema.GroupVersionResource]bool{}
+	for informType, informer := range list {
+		res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
 	}
 	return res
 }
diff --git a/internal/octant/object_path.go b/internal/octant/object_path.go
index 743dde0e..27e5c85e 100644
--- a/internal/octant/object_path.go
+++ b/internal/octant/object_path.go
@@ -110,8 +110,7 @@ func (op *ObjectPath) RemoveCRD(ctx context.Context, crd *unstructured.Unstructu
 	defer op.mu.Unlock()
 
 	if crd == nil {
-		// nothing to do if crd is nil
-		return nil
+		return errors.New("unable to remove nil crd")
 	}
 
 	delete(op.crds, crd.GetName())
diff --git a/pkg/dash/dash.go b/pkg/dash/dash.go
index f9097e91..897a554b 100644
--- a/pkg/dash/dash.go
+++ b/pkg/dash/dash.go
@@ -106,15 +106,11 @@ func Run(ctx context.Context, logger log.Logger, shutdownCh chan bool, options O
 		return errors.Wrap(err, "initializing error store")
 	}
 
-	crdWatcher, err := describer.NewDefaultCRDWatcher(ctx, clusterClient, appObjectStore, errorStore)
+	crdWatcher, err := describer.NewDefaultCRDWatcher(ctx, appObjectStore, errorStore)
 	if err != nil {
 		return errors.Wrap(err, "initializing CRD watcher")
 	}
 
-	if err := crdWatcher.Watch(ctx); err != nil {
-		return fmt.Errorf("unable to start CRD watcher")
-	}
-
 	portForwarder, err := initPortForwarder(ctx, clusterClient, appObjectStore)
 	if err != nil {
 		return errors.Wrap(err, "initializing port forwarder")
diff --git a/pkg/navigation/navigation.go b/pkg/navigation/navigation.go
index c63b80f1..9c703c46 100644
--- a/pkg/navigation/navigation.go
+++ b/pkg/navigation/navigation.go
@@ -163,41 +163,30 @@ func ListCustomResources(
 	if crd == nil {
 		return nil, false, errors.New("crd is nil")
 	}
+	gvk := schema.GroupVersionKind{
+		Group:   crd.Spec.Group,
+		Version: crd.Spec.Version,
+		Kind:    crd.Spec.Names.Kind,
+	}
 
-	list := new(unstructured.UnstructuredList)
-
-	for _, version := range crd.Spec.Versions {
-		if !version.Served {
-			continue
-		}
-
-		gvk := schema.GroupVersionKind{
-			Group:   crd.Spec.Group,
-			Version: version.Name,
-			Kind:    crd.Spec.Names.Kind,
-		}
-
-		apiVersion, kind := gvk.ToAPIVersionAndKind()
-
-		key := store.Key{
-			APIVersion: apiVersion,
-			Kind:       kind,
-			Selector:   selector,
-		}
+	apiVersion, kind := gvk.ToAPIVersionAndKind()
 
-		if crd.Spec.Scope == apiextv1beta1.NamespaceScoped {
-			key.Namespace = namespace
-		}
+	key := store.Key{
+		APIVersion: apiVersion,
+		Kind:       kind,
+		Selector:   selector,
+	}
 
-		objects, _, err := o.List(ctx, key)
-		if err != nil {
-			return nil, false, errors.Wrapf(err, "listing custom resources for %q", crd.Name)
-		}
+	if crd.Spec.Scope == apiextv1beta1.NamespaceScoped {
+		key.Namespace = namespace
+	}
 
-		list.Items = append(list.Items, objects.Items...)
+	objects, hasSynced, err := o.List(ctx, key)
+	if err != nil {
+		return nil, false, errors.Wrapf(err, "listing custom resources for %q", crd.Name)
 	}
 
-	return list, false, nil
+	return objects, hasSynced, nil
 }
 
 type navConfig struct {
diff --git a/web/package-lock.json b/web/package-lock.json
index c2ce05e5..6ad7e43e 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -5026,6 +5026,12 @@
       "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
       "dev": true
     },
+    "dayjs": {
+      "version": "1.8.19",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.19.tgz",
+      "integrity": "sha512-7kqOoj3oQSmqbvtvGFLU5iYqies+SqUiEGNT0UtUPPxcPYgY1BrkXR0Cq2R9HYSimBXN+xHkEN4Hi399W+Ovlg==",
+      "dev": true
+    },
     "debug": {
       "version": "2.6.9",
       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -9437,7 +9443,8 @@
     "moment": {
       "version": "2.24.0",
       "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
-      "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
+      "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
+      "dev": true
     },
     "move-concurrently": {
       "version": "1.0.1",
diff --git a/web/package.json b/web/package.json
index e0c1b57c..5aafcce4 100644
--- a/web/package.json
+++ b/web/package.json
@@ -45,7 +45,6 @@
     "default-passive-events": "^1.0.10",
     "highlight.js": "^9.15.6",
     "lodash": "^4.17.15",
-    "moment": "^2.24.0",
     "ng-terminal": "^2.2.1",
     "ngx-highlightjs": "^3.0.3",
     "ngx-markdown": "^8.1.0",
@@ -71,6 +70,7 @@
     "codelyzer": "^5.0.1",
     "cypress": "^3.4.1",
     "cypress-plugin-retries": "^1.3.0",
+    "dayjs": "^1.8.19",
     "jasmine-core": "~2.99.1",
     "jasmine-spec-reporter": "~4.2.1",
     "karma": "^4.0.1",
diff --git a/web/src/app/modules/overview/components/timestamp/timestamp.component.ts b/web/src/app/modules/overview/components/timestamp/timestamp.component.ts
index b42e15fd..c4ee56bb 100644
--- a/web/src/app/modules/overview/components/timestamp/timestamp.component.ts
+++ b/web/src/app/modules/overview/components/timestamp/timestamp.component.ts
@@ -3,7 +3,9 @@
 //
 
 import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
-import moment from 'moment';
+import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc';
+import LocalizedFormat from 'dayjs/plugin/localizedFormat';
 import { TimestampView, View } from 'src/app/models/content';
 
 @Component({
@@ -24,16 +26,20 @@ export class TimestampComponent implements OnChanges {
   humanReadable: string;
   age: string;
 
-  constructor() {}
+  constructor() {
+    dayjs.extend(utc);
+    dayjs.extend(LocalizedFormat);
+  }
 
   ngOnChanges(changes: SimpleChanges): void {
     if (changes.view.currentValue) {
       const view = changes.view.currentValue as TimestampView;
 
       const timestamp = view.config.timestamp;
-      this.humanReadable = moment(timestamp * 1000)
-        .utcOffset('+0000')
-        .format('LLLL z');
+      this.humanReadable =
+        dayjs(timestamp * 1000)
+          .utc()
+          .format('LLLL') + ' UTC';
       this.age = this.summarizeTimestamp(timestamp);
     }
   }


Test patch:
diff --git a/internal/config/dash_test.go b/internal/config/dash_test.go
index ae661422..d03518ad 100644
--- a/internal/config/dash_test.go
+++ b/internal/config/dash_test.go
@@ -112,10 +112,6 @@ type stubCRDWatcher struct{}
 
 var _ CRDWatcher = (*stubCRDWatcher)(nil)
 
-func (w stubCRDWatcher) AddConfig(config *CRDWatchConfig) error {
-	return nil
-}
-
-func (stubCRDWatcher) Watch(_ context.Context) error {
+func (stubCRDWatcher) Watch(_ context.Context, config *CRDWatchConfig) error {
 	return nil
 }
diff --git a/internal/describer/crd_watcher_test.go b/internal/describer/crd_watcher_test.go
index a0eb625b..1e2c2bbf 100644
--- a/internal/describer/crd_watcher_test.go
+++ b/internal/describer/crd_watcher_test.go
@@ -16,7 +16,6 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/client-go/tools/cache"
 
-	clusterFake "github.com/vmware-tanzu/octant/internal/cluster/fake"
 	"github.com/vmware-tanzu/octant/internal/config"
 	internalErr "github.com/vmware-tanzu/octant/internal/errors"
 	"github.com/vmware-tanzu/octant/pkg/store"
@@ -24,13 +23,8 @@ import (
 )
 
 func TestNewDefaultCRDWatcher_requires_object_store(t *testing.T) {
-	controller := gomock.NewController(t)
-	defer controller.Finish()
-
-	client := clusterFake.NewMockClientInterface(controller)
-
 	ctx := context.Background()
-	_, err := NewDefaultCRDWatcher(ctx, client, nil, nil)
+	_, err := NewDefaultCRDWatcher(ctx, nil, nil)
 	require.Error(t, err)
 }
 
@@ -40,8 +34,6 @@ func TestDefaultCRDWatcher_Watch(t *testing.T) {
 
 	ctx := context.Background()
 
-	client := clusterFake.NewMockClientInterface(controller)
-
 	objectStore := objectStoreFake.NewMockStore(controller)
 	objectStore.EXPECT().
 		Watch(ctx, crdKey, gomock.Any()).
@@ -55,7 +47,7 @@ func TestDefaultCRDWatcher_Watch(t *testing.T) {
 	errorStore, err := internalErr.NewErrorStore()
 	require.NoError(t, err)
 
-	watcher, err := NewDefaultCRDWatcher(ctx, client, objectStore, errorStore)
+	watcher, err := NewDefaultCRDWatcher(ctx, objectStore, errorStore)
 	require.NoError(t, err)
 
 	watchConfig := &config.CRDWatchConfig{
@@ -64,8 +56,8 @@ func TestDefaultCRDWatcher_Watch(t *testing.T) {
 		IsNamespaced: false,
 	}
 
-	require.NoError(t, watcher.AddConfig(watchConfig))
-	require.NoError(t, watcher.Watch(ctx))
+	err = watcher.Watch(ctx, watchConfig)
+	require.NoError(t, err)
 }
 
 func TestDefaultCRDWatcher_Watch_failure(t *testing.T) {
@@ -74,8 +66,6 @@ func TestDefaultCRDWatcher_Watch_failure(t *testing.T) {
 
 	ctx := context.Background()
 
-	client := clusterFake.NewMockClientInterface(controller)
-
 	objectStore := objectStoreFake.NewMockStore(controller)
 	objectStore.EXPECT().
 		Watch(ctx, crdKey, gomock.Any()).
@@ -86,8 +76,8 @@ func TestDefaultCRDWatcher_Watch_failure(t *testing.T) {
 		RegisterOnUpdate(gomock.Any())
 	errorStore, err := internalErr.NewErrorStore()
 	require.NoError(t, err)
-
-	watcher, err := NewDefaultCRDWatcher(ctx, client, objectStore, errorStore)
+	
+	watcher, err := NewDefaultCRDWatcher(ctx, objectStore, errorStore)
 	require.NoError(t, err)
 
 	watchConfig := &config.CRDWatchConfig{
@@ -96,8 +86,7 @@ func TestDefaultCRDWatcher_Watch_failure(t *testing.T) {
 		IsNamespaced: false,
 	}
 
-	require.NoError(t, watcher.AddConfig(watchConfig))
-	err = watcher.Watch(ctx)
+	err = watcher.Watch(ctx, watchConfig)
 	require.Error(t, err)
 }
 
diff --git a/internal/objectstore/accessors_test.go b/internal/objectstore/accessors_test.go
index 59fa4135..676e67e2 100644
--- a/internal/objectstore/accessors_test.go
+++ b/internal/objectstore/accessors_test.go
@@ -34,7 +34,12 @@ func Test_factoriesCache(t *testing.T) {
 	controller := gomock.NewController(t)
 	defer controller.Finish()
 
+	dynamicClient := fake.NewMockDynamicInterface(controller)
+
 	client := fake.NewMockClientInterface(controller)
+	client.EXPECT().
+		DynamicClient().
+		Return(dynamicClient, nil)
 
 	c := initFactoriesCache()
 
@@ -96,23 +101,23 @@ func Test_seenGVKsCache(t *testing.T) {
 func Test_informerContextCache(t *testing.T) {
 	c := initInformerContextCache()
 
-	groupVersionKind1 := schema.GroupVersionKind{
-		Group:   "group",
-		Version: "version",
-		Kind:    "resource1",
+	gvr1 := schema.GroupVersionResource{
+		Group:    "group",
+		Version:  "version",
+		Resource: "resource1",
 	}
-	groupVersionKind2 := schema.GroupVersionKind{
-		Group:   "group",
-		Version: "version",
-		Kind:    "resource2",
+	gvr2 := schema.GroupVersionResource{
+		Group:    "group",
+		Version:  "version",
+		Resource: "resource2",
 	}
-	_ = c.addChild(groupVersionKind1)
+	_ = c.addChild(gvr1)
 	assert.Len(t, c.cache, 1)
-	_ = c.addChild(groupVersionKind1)
+	_ = c.addChild(gvr1)
 	assert.Len(t, c.cache, 1)
-	_ = c.addChild(groupVersionKind2)
+	_ = c.addChild(gvr2)
 	assert.Len(t, c.cache, 2)
-	c.delete(groupVersionKind1)
+	c.delete(gvr1)
 	assert.Len(t, c.cache, 1)
 	c.reset()
 	assert.Len(t, c.cache, 0)
diff --git a/internal/objectstore/dynamic_cache_test.go b/internal/objectstore/dynamic_cache_test.go
new file mode 100644
index 00000000..00713c27
--- /dev/null
+++ b/internal/objectstore/dynamic_cache_test.go
@@ -0,0 +1,322 @@
+/*
+Copyright (c) 2019 the Octant contributors. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package objectstore
+
+import (
+	"context"
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	authorizationv1 "k8s.io/api/authorization/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	kLabels "k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	dynamicFake "k8s.io/client-go/dynamic/fake"
+	"k8s.io/client-go/informers"
+	clientGoTesting "k8s.io/client-go/testing"
+	"k8s.io/client-go/tools/cache"
+
+	"github.com/vmware-tanzu/octant/internal/cluster"
+	clusterFake "github.com/vmware-tanzu/octant/internal/cluster/fake"
+	"github.com/vmware-tanzu/octant/internal/objectstore/fake"
+	"github.com/vmware-tanzu/octant/internal/testutil"
+	"github.com/vmware-tanzu/octant/pkg/store"
+)
+
+var (
+	podGVR = schema.GroupVersionResource{Version: "v1", Resource: "pods"}
+)
+
+type fakeLister struct {
+	listObjects []runtime.Object
+	listErr     error
+
+	getObject runtime.Object
+	getErr    error
+}
+
+var _ cache.GenericLister = (*fakeLister)(nil)
+
+func (l fakeLister) List(selector kLabels.Selector) ([]runtime.Object, error) {
+	return l.listObjects, l.listErr
+}
+
+func (l fakeLister) Get(name string) (runtime.Object, error) {
+	return l.getObject, l.getErr
+}
+
+func (l fakeLister) ByNamespace(namespace string) cache.GenericNamespaceLister {
+	return l
+}
+
+func expectNamespaceAccess(
+	accessClient *clusterFake.MockSelfSubjectAccessReviewInterface,
+	authClient *clusterFake.MockAuthorizationV1Interface,
+	namespaceCount int,
+) {
+	authClient.EXPECT().SelfSubjectAccessReviews().Return(accessClient).MaxTimes(namespaceCount)
+	accessResp := &authorizationv1.SelfSubjectAccessReview{
+		Status: authorizationv1.SubjectAccessReviewStatus{
+			Allowed: true,
+		},
+	}
+	accessClient.EXPECT().Create(gomock.Any()).Return(accessResp, nil).MaxTimes(namespaceCount)
+}
+
+func Test_DynamicCache_List(t *testing.T) {
+	h := initDynamicCacheTestHarness(t)
+	defer h.finish()
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	pod := testutil.CreatePod("pod")
+
+	objects := []runtime.Object{testutil.ToUnstructured(t, pod)}
+
+	l := &fakeLister{listObjects: objects}
+	h.setupLister(podGVR, l)
+
+	h.mapResources(pod.GroupVersionKind(), podGVR)
+
+	c, err := h.factory(ctx)
+	require.NoError(t, err)
+
+	h.setSynced(t, c, pod)
+
+	key := h.keyFromObject(t, pod)
+
+	got, isLoading, err := c.List(ctx, key)
+	require.NoError(t, err)
+	require.False(t, isLoading)
+
+	expected := testutil.ToUnstructuredList(t, pod)
+	assert.Equal(t, expected, got)
+}
+
+func Test_DynamicCache_Get(t *testing.T) {
+	h := initDynamicCacheTestHarness(t)
+	defer h.finish()
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	pod := testutil.CreatePod("pod")
+
+	l := &fakeLister{getObject: testutil.ToUnstructured(t, pod)}
+	h.setupLister(podGVR, l)
+	h.mapResources(pod.GroupVersionKind(), podGVR)
+
+	c, err := h.factory(ctx)
+	require.NoError(t, err)
+
+	h.setSynced(t, c, pod)
+	key := h.keyFromObject(t, pod)
+
+	got, found, err := c.Get(ctx, key)
+	require.NoError(t, err)
+	require.True(t, found)
+
+	expected := testutil.ToUnstructured(t, pod)
+
+	assert.Equal(t, expected, got)
+}
+
+func TestDynamicCache_Update(t *testing.T) {
+	h := initDynamicCacheTestHarness(t)
+	defer h.finish()
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	pod := testutil.ToUnstructured(t, testutil.CreatePod("pod"))
+
+	podInformer := h.informerFor(podGVR)
+	h.mapResources(pod.GroupVersionKind(), podGVR)
+
+	l := &fakeLister{getObject: pod}
+	podInformer.EXPECT().Lister().Return(l)
+
+	scheme := runtime.NewScheme()
+
+	dc := dynamicFake.NewSimpleDynamicClient(scheme, pod)
+
+	h.client.EXPECT().DynamicClient().Return(dc, nil)
+
+	c, err := h.factory(ctx)
+	require.NoError(t, err)
+
+	key := h.keyFromObject(t, pod)
+
+	err = c.Update(ctx, key, func(*unstructured.Unstructured) error {
+		return nil
+	})
+	require.NoError(t, err)
+
+	assert.Len(t, dc.Actions(), 1)
+
+	action := dc.Actions()[0]
+	assert.Equal(t, "update", action.GetVerb())
+}
+
+func TestDynamicCache_Delete(t *testing.T) {
+	h := initDynamicCacheTestHarness(t)
+	defer h.finish()
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	pod := testutil.ToUnstructured(t, testutil.CreatePod("pod"))
+	h.mapResources(pod.GroupVersionKind(), podGVR)
+
+	scheme := runtime.NewScheme()
+
+	dc := dynamicFake.NewSimpleDynamicClient(scheme, pod)
+	h.client.EXPECT().DynamicClient().Return(dc, nil)
+
+	c, err := h.factory(ctx)
+	require.NoError(t, err)
+
+	key := h.keyFromObject(t, pod)
+
+	err = c.Delete(ctx, key)
+	require.NoError(t, err)
+
+	assert.Len(t, dc.Actions(), 1)
+
+	expected := clientGoTesting.DeleteActionImpl{
+		ActionImpl: clientGoTesting.ActionImpl{
+			Namespace: pod.GetNamespace(),
+			Verb:      "delete",
+			Resource:  podGVR,
+		},
+		Name: pod.GetName(),
+	}
+
+	got := dc.Actions()[0]
+	assert.Equal(t, expected, got)
+}
+
+func TestDynamicCache_Unwatch(t *testing.T) {
+	h := initDynamicCacheTestHarness(t)
+	defer h.finish()
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	pod := testutil.CreatePod("pod")
+	h.mapResources(pod.GroupVersionKind(), podGVR)
+
+	c, err := h.factory(ctx)
+	require.NoError(t, err)
+
+	h.informerFactory.EXPECT().Delete(podGVR)
+
+	err = c.Unwatch(ctx, pod.GroupVersionKind())
+	require.NoError(t, err)
+}
+
+type dynamicCacheTestHarness struct {
+	controller       *gomock.Controller
+	client           *clusterFake.MockClientInterface
+	informerFactory  *fake.MockInformerFactory
+	kubernetesClient *clusterFake.MockKubernetesInterface
+	authClient       *clusterFake.MockAuthorizationV1Interface
+	namespaceClient  *clusterFake.MockNamespaceInterface
+	accessClient     *clusterFake.MockSelfSubjectAccessReviewInterface
+}
+
+func initDynamicCacheTestHarness(t *testing.T) *dynamicCacheTestHarness {
+	controller := gomock.NewController(t)
+	client := clusterFake.NewMockClientInterface(controller)
+	informerFactory := fake.NewMockInformerFactory(controller)
+	kubernetesClient := clusterFake.NewMockKubernetesInterface(controller)
+	authClient := clusterFake.NewMockAuthorizationV1Interface(controller)
+	accessClient := clusterFake.NewMockSelfSubjectAccessReviewInterface(controller)
+	namespaceClient := clusterFake.NewMockNamespaceInterface(controller)
+
+	namespaces := []string{"test", ""}
+	client.EXPECT().NamespaceClient().Return(namespaceClient, nil).AnyTimes()
+	namespaceClient.EXPECT().Names().Return(namespaces, nil).MaxTimes(2)
+	expectNamespaceAccess(accessClient, authClient, len(namespaces))
+
+	client.EXPECT().KubernetesClient().Return(kubernetesClient, nil).AnyTimes()
+	kubernetesClient.EXPECT().AuthorizationV1().Return(authClient).AnyTimes()
+
+	return &dynamicCacheTestHarness{
+		controller:       controller,
+		client:           client,
+		informerFactory:  informerFactory,
+		kubernetesClient: kubernetesClient,
+		authClient:       authClient,
+		accessClient:     accessClient,
+		namespaceClient:  namespaceClient,
+	}
+}
+
+func (h *dynamicCacheTestHarness) finish() {
+	h.controller.Finish()
+}
+
+func (h *dynamicCacheTestHarness) factory(ctx context.Context) (*DynamicCache, error) {
+	factoryFunc := func(c *DynamicCache) {
+		c.initFactoryFunc = func(i context.Context, clientInterface cluster.ClientInterface, s string) (factory InformerFactory, e error) {
+			return h.informerFactory, nil
+
+		}
+		c.waitForSyncFunc = func(context.Context, store.Key, *DynamicCache, informers.GenericInformer, chan bool) {
+			return
+		}
+	}
+
+	resourceAccess := NewResourceAccess(h.client)
+	return NewDynamicCache(ctx, h.client, factoryFunc, Access(resourceAccess))
+}
+
+func (h *dynamicCacheTestHarness) informerFor(gvr schema.GroupVersionResource) *clusterFake.MockGenericInformer {
+	informer := clusterFake.NewMockGenericInformer(h.controller)
+	h.informerFactory.EXPECT().
+		ForResource(gvr).
+		Return(informer)
+
+	return informer
+}
+
+func (h *dynamicCacheTestHarness) setupLister(gvr schema.GroupVersionResource, l cache.GenericLister) {
+	informer := h.informerFor(gvr)
+	informer.EXPECT().Lister().Return(l)
+}
+
+func (h *dynamicCacheTestHarness) mapResources(groupVersionKind schema.GroupVersionKind, resource schema.GroupVersionResource) {
+	h.client.EXPECT().
+		Resource(groupVersionKind.GroupKind()).
+		Return(resource, nil).
+		AnyTimes()
+}
+
+func (h *dynamicCacheTestHarness) setSynced(t *testing.T, c *DynamicCache, object runtime.Object) {
+	accessor, err := meta.Accessor(object)
+	require.NoError(t, err)
+
+	apiVersion, kind := object.GetObjectKind().GroupVersionKind().ToAPIVersionAndKind()
+
+	key := store.Key{
+		Namespace:  accessor.GetNamespace(),
+		APIVersion: apiVersion,
+		Kind:       kind,
+	}
+	c.informerSynced.setSynced(key, true)
+}
+
+func (h *dynamicCacheTestHarness) keyFromObject(t *testing.T, object runtime.Object) store.Key {
+	key, err := store.KeyFromObject(object)
+	require.NoError(t, err)
+	return key
+}