Repository: vmware-tanzu/octant
Pull request #583 (2020-01-27 03:49:05)

Added codeblock component for long configmap values
ref: #527 

This also adds a copy to clipboard button as sometimes multi-line configmap values are configuration files.

Signed-off-by: GuessWhoSamFoo <foos@vmware.com>


Patch:
diff --git a/changelogs/unreleased/562-wwitzel3 b/changelogs/unreleased/562-wwitzel3
deleted file mode 100644
index 7f9124c8..00000000
--- a/changelogs/unreleased/562-wwitzel3
+++ /dev/null
@@ -1,1 +0,0 @@
-Fix error type assertion for AccessError.
diff --git a/changelogs/unreleased/572-bryanl b/changelogs/unreleased/572-bryanl
deleted file mode 100644
index 2224fd83..00000000
--- a/changelogs/unreleased/572-bryanl
+++ /dev/null
@@ -1,1 +0,0 @@
-bug fix: don't fail if pod metrics are not available for a pod (they are not available instantly)
diff --git a/changelogs/unreleased/583-GuessWhoSamFoo b/changelogs/unreleased/583-GuessWhoSamFoo
new file mode 100644
index 00000000..8e50c2fb
--- /dev/null
+++ b/changelogs/unreleased/583-GuessWhoSamFoo
@@ -0,0 +1,1 @@
+Added new component for multi-line text to handle long ConfigMap values
diff --git a/internal/describer/crd_watcher.go b/internal/describer/crd_watcher.go
index 01053d4f..b6aa366f 100644
--- a/internal/describer/crd_watcher.go
+++ b/internal/describer/crd_watcher.go
@@ -7,11 +7,11 @@ package describer
 
 import (
 	"context"
-	"errors"
 	"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"
 
@@ -31,6 +31,7 @@ type DefaultCRDWatcher struct {
 
 	watchConfigs map[string]*config.CRDWatchConfig
 
+	accessError sync.Once
 	mu          sync.Mutex
 }
 
@@ -134,16 +135,16 @@ func (cw *DefaultCRDWatcher) Watch(ctx context.Context) error {
 
 	err := cw.objectStore.Watch(ctx, crdKey, handler)
 	if err != nil {
-		var e *internalErr.AccessError
-		if errors.As(err, &e) {
-			found := cw.errorStore.Add(e)
+		aErr, ok := err.(*internalErr.AccessError)
+		if ok {
+			found := cw.errorStore.Add(aErr)
 			// Log if we have not seen this access error before.
 			if !found {
-				logger.WithErr(e).Errorf("access denied")
+				logger.WithErr(aErr).Errorf("access denied")
 			}
 			return nil
 		}
-		return fmt.Errorf("crd watcher has failed: %w", err)
+		return errors.WithMessage(err, "crd watcher has failed")
 	}
 
 	return nil
diff --git a/internal/errors/access_error.go b/internal/errors/access_error.go
index 57e25c4e..681e8994 100644
--- a/internal/errors/access_error.go
+++ b/internal/errors/access_error.go
@@ -44,11 +44,7 @@ func (o *AccessError) Timestamp() time.Time {
 
 // Error returns an error string.
 func (o *AccessError) Error() string {
-	e := fmt.Sprintf("%s: %s", o.verb, o.key)
-	if o.err != nil {
-		e = fmt.Sprintf("%s (error: %s)", e, o.err.Error())
-	}
-	return e
+	return fmt.Sprintf("%s: %s: %s", o.verb, o.key, o.err)
 }
 
 // Key returns the key for the error.
diff --git a/internal/objectstore/access.go b/internal/objectstore/access.go
index af79b9e2..dff0ecb7 100644
--- a/internal/objectstore/access.go
+++ b/internal/objectstore/access.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"sync"
 
+	"github.com/pkg/errors"
 	"go.opencensus.io/trace"
 	authorizationv1 "k8s.io/api/authorization/v1"
 
@@ -132,7 +133,7 @@ func (r *resourceAccess) HasAccess(ctx context.Context, key store.Key, verb stri
 		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)
+			return errors.Wrapf(err, "fetch access: %+v", aKey)
 		}
 
 		r.cache.set(aKey, val)
@@ -154,7 +155,7 @@ func (r *resourceAccess) keyToAccessKey(key store.Key, verb string) (AccessKey,
 	gvk := key.GroupVersionKind()
 
 	if gvk.GroupKind().Empty() {
-		return AccessKey{}, fmt.Errorf("unable to check access for key %s", key.String())
+		return AccessKey{}, errors.Errorf("unable to check access for key %s", key.String())
 	}
 
 	gvr, err := r.client.Resource(gvk.GroupKind())
@@ -177,7 +178,7 @@ func (r *resourceAccess) fetchAccess(key AccessKey, verb string) (bool, error) {
 
 	k8sClient, err := r.client.KubernetesClient()
 	if err != nil {
-		return false, fmt.Errorf("client kubernetes: %w", err)
+		return false, errors.Wrap(err, "client kubernetes")
 	}
 
 	authClient := k8sClient.AuthorizationV1()
@@ -194,7 +195,7 @@ func (r *resourceAccess) fetchAccess(key AccessKey, verb string) (bool, error) {
 
 	review, err := authClient.SelfSubjectAccessReviews().Create(sar)
 	if err != nil {
-		return false, fmt.Errorf("client auth: %w", err)
+		return false, errors.Wrap(err, "client auth")
 	}
 	return review.Status.Allowed, nil
 }
diff --git a/internal/objectstore/dynamic_cache.go b/internal/objectstore/dynamic_cache.go
index b6d1a36d..75b26f67 100644
--- a/internal/objectstore/dynamic_cache.go
+++ b/internal/objectstore/dynamic_cache.go
@@ -7,11 +7,11 @@ package objectstore
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"sync"
 	"time"
 
+	"github.com/pkg/errors"
 	"go.opencensus.io/trace"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
@@ -113,7 +113,7 @@ func NewDynamicCache(ctx context.Context, client cluster.ClientInterface, option
 
 	factory, err := c.initFactoryFunc(context.Background(), client, "")
 	if err != nil {
-		return nil, fmt.Errorf("initialize dynamic shared informer factory: %w", err)
+		return nil, errors.Wrap(err, "initialize dynamic shared informer factory")
 	}
 
 	c.factories.set("", factory)
@@ -204,7 +204,7 @@ func (dc *DynamicCache) listFromInformer(ctx context.Context, key store.Key) (*u
 
 	informer, hasSynced, err := dc.currentInformer(ctx, key)
 	if err != nil {
-		return nil, false, fmt.Errorf("retrieving informer for %+v: %w", key, err)
+		return nil, false, errors.Wrapf(err, "retrieving informer for %+v", key)
 	}
 
 	if !hasSynced {
@@ -226,7 +226,7 @@ func (dc *DynamicCache) listFromInformer(ctx context.Context, key store.Key) (*u
 
 	objects, err := l.List(selector)
 	if err != nil {
-		return nil, false, fmt.Errorf("listing %v: %w", key, err)
+		return nil, false, errors.Wrapf(err, "listing %v", key)
 	}
 
 	list := &unstructured.UnstructuredList{}
@@ -306,7 +306,7 @@ func (dc *DynamicCache) getFromInformer(ctx context.Context, key store.Key) (*un
 
 	informer, hasSynced, err := dc.currentInformer(ctx, key)
 	if err != nil {
-		return nil, fmt.Errorf("retrieving informer for %v: %w", key, err)
+		return nil, errors.Wrapf(err, "retrieving informer for %v", key)
 	}
 
 	if !hasSynced {
@@ -356,7 +356,7 @@ func (dc *DynamicCache) Watch(ctx context.Context, key store.Key, handler kcache
 
 	informer, _, err := dc.currentInformer(ctx, key)
 	if err != nil {
-		return fmt.Errorf("retrieving informer for %s: %w", key, err)
+		return errors.Wrapf(err, "retrieving informer for %s", key)
 	}
 
 	informer.Informer().AddEventHandler(handler)
@@ -447,7 +447,7 @@ func (dc *DynamicCache) Update(ctx context.Context, key store.Key, updater func(
 		}
 
 		if !found {
-			return errors.New("object not found")
+			return errors.Errorf("object not found")
 		}
 
 		gvk := object.GroupVersionKind()
@@ -463,7 +463,7 @@ func (dc *DynamicCache) Update(ctx context.Context, key store.Key, updater func(
 		}
 
 		if err := updater(object); err != nil {
-			return fmt.Errorf("unable to update object: %w", err)
+			return errors.Wrap(err, "unable to update object")
 		}
 
 		client := dynamicClient.Resource(gvr).Namespace(object.GetNamespace())
diff --git a/internal/octant/fake/mock_pod_metrics_crud.go b/internal/octant/fake/mock_pod_metrics_crud.go
index 98598813..22d2caba 100644
--- a/internal/octant/fake/mock_pod_metrics_crud.go
+++ b/internal/octant/fake/mock_pod_metrics_crud.go
@@ -34,13 +34,12 @@ func (m *MockPodMetricsCRUD) EXPECT() *MockPodMetricsCRUDMockRecorder {
 }
 
 // Get mocks base method
-func (m *MockPodMetricsCRUD) Get(arg0, arg1 string) (*unstructured.Unstructured, bool, error) {
+func (m *MockPodMetricsCRUD) Get(arg0, arg1 string) (*unstructured.Unstructured, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Get", arg0, arg1)
 	ret0, _ := ret[0].(*unstructured.Unstructured)
-	ret1, _ := ret[1].(bool)
-	ret2, _ := ret[2].(error)
-	return ret0, ret1, ret2
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
 // Get indicates an expected call of Get
diff --git a/internal/octant/fake/mock_pod_metrics_loader.go b/internal/octant/fake/mock_pod_metrics_loader.go
index 361f5924..ada76772 100644
--- a/internal/octant/fake/mock_pod_metrics_loader.go
+++ b/internal/octant/fake/mock_pod_metrics_loader.go
@@ -34,13 +34,12 @@ func (m *MockPodMetricsLoader) EXPECT() *MockPodMetricsLoaderMockRecorder {
 }
 
 // Load mocks base method
-func (m *MockPodMetricsLoader) Load(arg0, arg1 string) (*unstructured.Unstructured, bool, error) {
+func (m *MockPodMetricsLoader) Load(arg0, arg1 string) (*unstructured.Unstructured, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Load", arg0, arg1)
 	ret0, _ := ret[0].(*unstructured.Unstructured)
-	ret1, _ := ret[1].(bool)
-	ret2, _ := ret[2].(error)
-	return ret0, ret1, ret2
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
 // Load indicates an expected call of Load
diff --git a/internal/octant/pod_metrics_loader.go b/internal/octant/pod_metrics_loader.go
index 4e6f5b6e..d6b4f178 100644
--- a/internal/octant/pod_metrics_loader.go
+++ b/internal/octant/pod_metrics_loader.go
@@ -9,7 +9,6 @@ import (
 	"fmt"
 	"sync"
 
-	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -23,8 +22,8 @@ import (
 
 // PodMetricsCRUD contains CRUD methods for accessing pod metrics.
 type PodMetricsCRUD interface {
-	// Get returns pod metrics for a pod. If pod is not found, isFound will be false.
-	Get(namespace, name string) (pod *unstructured.Unstructured, isFound bool, err error)
+	// Get returns pod metrics for a pod.
+	Get(namespace, name string) (*unstructured.Unstructured, error)
 }
 
 type clusterPodMetricsCRUD struct {
@@ -41,23 +40,19 @@ func newClusterPodMetricsCRUD(clusterClient cluster.ClientInterface) (*clusterPo
 	return &clusterPodMetricsCRUD{clusterClient: clusterClient}, nil
 }
 
-func (c *clusterPodMetricsCRUD) Get(namespace, name string) (*unstructured.Unstructured, bool, error) {
+func (c *clusterPodMetricsCRUD) Get(namespace, name string) (*unstructured.Unstructured, error) {
 	client, err := c.clusterClient.DynamicClient()
 	if err != nil {
-		return nil, false, fmt.Errorf("get dynamic client: %w", err)
+		return nil, fmt.Errorf("get dynamic client: %w", err)
 	}
 
 	options := metav1.GetOptions{}
 	object, err := client.Resource(PodMetricsResource).Namespace(namespace).Get(name, options)
 	if err != nil {
-		if errors.IsNotFound(err) {
-			return nil, false, nil
-		}
-
-		return nil, false, fmt.Errorf("get pod metrics: %w", err)
+		return nil, fmt.Errorf("get pod metrics: %w", err)
 	}
 
-	return object, true, nil
+	return object, nil
 }
 
 type noPodMetricsSupport interface {
@@ -86,9 +81,8 @@ func IsPodMetricsNotSupported(err error) bool {
 
 // PodMetricsLoader loads metrics for a pod.
 type PodMetricsLoader interface {
-	// Load loads metrics for a pod given namespace and a name. It returns false if the
-	// object is not found.
-	Load(namespace, name string) (object *unstructured.Unstructured, isFound bool, err error)
+	// Load loads metrics for a pod given namespace and a name.
+	Load(namespace, name string) (*unstructured.Unstructured, error)
 	// SupportsMetrics returns true if the cluster has metrics support.
 	SupportsMetrics() (bool, error)
 }
@@ -140,7 +134,7 @@ var (
 )
 
 // Load loads metrics for a pod given namespace and a name.
-func (ml *ClusterPodMetricsLoader) Load(namespace, name string) (*unstructured.Unstructured, bool, error) {
+func (ml *ClusterPodMetricsLoader) Load(namespace, name string) (*unstructured.Unstructured, error) {
 	return ml.PodMetricsCRUD.Get(namespace, name)
 }
 
diff --git a/internal/octant/workload.go b/internal/octant/workload.go
index 53487abd..87f13f12 100644
--- a/internal/octant/workload.go
+++ b/internal/octant/workload.go
@@ -429,15 +429,11 @@ func (wl *ClusterWorkloadLoader) podMetrics(pod *unstructured.Unstructured) (*co
 	namespace := pod.GetNamespace()
 	name := pod.GetName()
 
-	object, found, err := wl.PodMetricsLoader.Load(namespace, name)
+	object, err := wl.PodMetricsLoader.Load(namespace, name)
 	if err != nil {
 		return nil, fmt.Errorf("get pod metrics: %w", err)
 	}
 
-	if !found {
-		return nil, &NoPodMetricsErr{}
-	}
-
 	containersRaw, found, err := unstructured.NestedSlice(object.Object, "containers")
 	if err != nil {
 		return nil, fmt.Errorf("get containers in pod metrics")
diff --git a/internal/printer/configmap.go b/internal/printer/configmap.go
index 49fc16bd..b12e4f86 100644
--- a/internal/printer/configmap.go
+++ b/internal/printer/configmap.go
@@ -9,6 +9,7 @@ import (
 	"context"
 	"fmt"
 	"sort"
+	"strings"
 
 	"github.com/pkg/errors"
 
@@ -146,7 +147,11 @@ func describeConfigMapDataRows(cm *corev1.ConfigMap) ([]component.TableRow, erro
 
 		row["Key"] = component.NewText(k)
 
-		row["Value"] = component.NewText(data[k])
+		if strings.Contains(data[k], "\n") {
+			row["Value"] = component.NewCodeBlock(data[k])
+		} else {
+			row["Value"] = component.NewText(data[k])
+		}
 	}
 
 	return rows, nil
diff --git a/pkg/view/component/base.go b/pkg/view/component/base.go
index de7758e0..67fd5509 100644
--- a/pkg/view/component/base.go
+++ b/pkg/view/component/base.go
@@ -10,6 +10,7 @@ const (
 	typeButtonGroup         = "buttonGroup"
 	typeCard                = "card"
 	typeCardList            = "cardList"
+	typeCodeBlock           = "codeBlock"
 	typeContainers          = "containers"
 	typeDonutChart          = "donutChart"
 	typeError               = "error"
diff --git a/pkg/view/component/code.go b/pkg/view/component/code.go
new file mode 100644
index 00000000..afd2a1ae
--- /dev/null
+++ b/pkg/view/component/code.go
@@ -0,0 +1,38 @@
+/*
+Copyright (c) 2020 the Octant contributors. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package component
+
+import "encoding/json"
+
+// Code is a component for code
+type Code struct {
+	base
+	Config CodeConfig `json:"config"`
+}
+
+// CodeConfig is the contents of Code
+type CodeConfig struct {
+	Code string `json:"value"`
+}
+
+// NewCodeBlock creates a code component
+func NewCodeBlock(s string) *Code {
+	return &Code{
+		base: newBase(typeCodeBlock, nil),
+		Config: CodeConfig{
+			Code: s,
+		},
+	}
+}
+
+type codeMarshal Code
+
+// MarshalJSON implements json.Marshaler
+func (c *Code) MarshalJSON() ([]byte, error) {
+	m := codeMarshal(*c)
+	m.Metadata.Type = typeCodeBlock
+	return json.Marshal(&m)
+}
diff --git a/web/src/app/models/content.ts b/web/src/app/models/content.ts
index 346164e8..3852c475 100644
--- a/web/src/app/models/content.ts
+++ b/web/src/app/models/content.ts
@@ -455,3 +455,9 @@ export interface ExtensionView extends View {
     tabs: ExtensionTab[];
   };
 }
+
+export interface CodeView extends View {
+  config: {
+    value: string;
+  };
+}
diff --git a/web/src/app/modules/overview/components/code/code.component.html b/web/src/app/modules/overview/components/code/code.component.html
new file mode 100644
index 00000000..0dd48202
--- /dev/null
+++ b/web/src/app/modules/overview/components/code/code.component.html
@@ -0,0 +1,8 @@
+<div class="code-button-wrapper">
+    <button type="button" class="btn btn-icon btn-link" title="Copy to clipboard" (click)="copyToClipboard(value)">
+        <clr-icon shape="copy-to-clipboard" size="32"></clr-icon>
+    </button>
+</div>
+<pre>
+<code>{{value}}</code>
+</pre>
\ No newline at end of file
diff --git a/web/src/app/modules/overview/components/code/code.component.scss b/web/src/app/modules/overview/components/code/code.component.scss
new file mode 100644
index 00000000..e5b6a5b8
--- /dev/null
+++ b/web/src/app/modules/overview/components/code/code.component.scss
@@ -0,0 +1,9 @@
+/* Copyright (c) 2020 the Octant contributors. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+ .code-button-wrapper {
+     position: absolute;
+     margin-top: 20px;
+     right: 30px;
+ }
\ No newline at end of file
diff --git a/web/src/app/modules/overview/components/code/code.component.spec.ts b/web/src/app/modules/overview/components/code/code.component.spec.ts
new file mode 100644
index 00000000..12e0a864
--- /dev/null
+++ b/web/src/app/modules/overview/components/code/code.component.spec.ts
@@ -0,0 +1,38 @@
+/* Copyright (c) 2020 the Octant contributors. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { CodeComponent } from './code.component';
+
+describe('CodeComponent', () => {
+  let component: CodeComponent;
+  let fixture: ComponentFixture<CodeComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [CodeComponent],
+    }).compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CodeComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('copy button copies text', async(() => {
+    spyOn(component, 'copyToClipboard');
+
+    const button = fixture.debugElement.nativeElement.querySelector('button');
+    button.click();
+
+    fixture.whenStable().then(() => {
+      expect(component.copyToClipboard).toHaveBeenCalled();
+    });
+  }));
+});
diff --git a/web/src/app/modules/overview/components/code/code.component.ts b/web/src/app/modules/overview/components/code/code.component.ts
new file mode 100644
index 00000000..5d8441f0
--- /dev/null
+++ b/web/src/app/modules/overview/components/code/code.component.ts
@@ -0,0 +1,44 @@
+/*
+Copyright (c) 2020 the Octant contributors. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { CodeView, View } from 'src/app/models/content';
+
+@Component({
+  selector: 'app-view-code',
+  templateUrl: './code.component.html',
+  styleUrls: ['./code.component.scss'],
+})
+export class CodeComponent implements OnChanges {
+  private v: CodeView;
+
+  @Input() set view(v: View) {
+    this.v = v as CodeView;
+  }
+
+  get view() {
+    return this.v;
+  }
+
+  value: string;
+
+  constructor() {}
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as CodeView;
+      this.value = view.config.value;
+    }
+  }
+
+  copyToClipboard(text) {
+    document.addEventListener('copy', (e: ClipboardEvent) => {
+      e.clipboardData.setData('text/plain', text);
+      e.preventDefault();
+      document.removeEventListener('copy', null);
+    });
+    document.execCommand('copy');
+  }
+}
diff --git a/web/src/app/modules/overview/components/content-switcher/content-switcher.component.html b/web/src/app/modules/overview/components/content-switcher/content-switcher.component.html
index 347767c9..177d4500 100644
--- a/web/src/app/modules/overview/components/content-switcher/content-switcher.component.html
+++ b/web/src/app/modules/overview/components/content-switcher/content-switcher.component.html
@@ -15,6 +15,9 @@
     <ng-container *ngSwitchCase="'containers'">
       <app-view-containers [view]="view"></app-view-containers>
     </ng-container>
+    <ng-container *ngSwitchCase="'codeBlock'">
+      <app-view-code [view]="view"></app-view-code>
+    </ng-container>
     <ng-container *ngSwitchCase="'donutChart'">
         <app-view-donut-chart [view]="view"></app-view-donut-chart>
     </ng-container>
diff --git a/web/src/app/modules/overview/overview.module.ts b/web/src/app/modules/overview/overview.module.ts
index 395f807d..d0a8b473 100644
--- a/web/src/app/modules/overview/overview.module.ts
+++ b/web/src/app/modules/overview/overview.module.ts
@@ -18,6 +18,7 @@ import { CardComponent } from './components/card/card.component';
 import { ContainersComponent } from './components/containers/containers.component';
 import { ContentSwitcherComponent } from './components/content-switcher/content-switcher.component';
 import { ContextSelectorComponent } from './components/context-selector/context-selector.component';
+import { CodeComponent } from './components/code/code.component';
 import { CytoscapeComponent } from './components/cytoscape/cytoscape.component';
 import { DatagridComponent } from './components/datagrid/datagrid.component';
 import { ErrorComponent } from './components/error/error.component';
@@ -74,6 +75,7 @@ export function hljsLanguages() {
   declarations: [
     AnnotationsComponent,
     ContainersComponent,
+    CodeComponent,
     DatagridComponent,
     ExpressionSelectorComponent,
     ErrorComponent,


Test patch:
diff --git a/internal/errors/access_error_test.go b/internal/errors/access_error_test.go
index b7facddf..73fa2d10 100644
--- a/internal/errors/access_error_test.go
+++ b/internal/errors/access_error_test.go
@@ -6,7 +6,6 @@
 package errors
 
 import (
-	goerrors "errors"
 	"fmt"
 	"testing"
 
@@ -26,36 +25,8 @@ func TestNewAccessError(t *testing.T) {
 	intErr := NewAccessError(key, verb, err)
 	assert.Equal(t, key, intErr.Key())
 	assert.Equal(t, verb, intErr.Verb())
-	assert.Equal(t, fmt.Sprintf("%s: %s (error: %s)", verb, key, err.Error()), intErr.Error())
+	assert.Equal(t, fmt.Sprintf("%s: %s: %s", verb, key, err), intErr.Error())
 	assert.EqualError(t, err, "access denied")
 	assert.NotEmpty(t, intErr.Timestamp())
 	assert.NotZero(t, intErr.ID())
 }
-
-func TestFormattedAccessError(t *testing.T) {
-	key := store.Key{
-		Namespace:  "default",
-		APIVersion: "v1",
-		Kind:       "Pod",
-	}
-	verb := "watch"
-	err := fmt.Errorf("access denied")
-
-	intErr := NewAccessError(key, verb, err)
-	newErr := fmt.Errorf("%w", intErr)
-
-	var e *AccessError
-	assert.True(t, goerrors.As(newErr, &e))
-}
-
-func TestNilErrAccessError(t *testing.T) {
-	key := store.Key{
-		Namespace:  "default",
-		APIVersion: "v1",
-		Kind:       "Pod",
-	}
-	verb := "watch"
-
-	intErr := NewAccessError(key, verb, nil)
-	assert.Equal(t, fmt.Sprintf("%s: %s", verb, key), intErr.Error())
-}
diff --git a/internal/octant/pod_metrics_loader_test.go b/internal/octant/pod_metrics_loader_test.go
index e0e2e30b..25105592 100644
--- a/internal/octant/pod_metrics_loader_test.go
+++ b/internal/octant/pod_metrics_loader_test.go
@@ -67,7 +67,6 @@ func TestClusterPodMetricsLoader_Load(t *testing.T) {
 		clusterClient cluster.ClientInterface
 		options       []octant.ClusterPodMetricsLoaderOption
 		want          *unstructured.Unstructured
-		wantFound     bool
 		wantErr       bool
 	}{
 		{
@@ -77,28 +76,12 @@ func TestClusterPodMetricsLoader_Load(t *testing.T) {
 					crud := octantFake.NewMockPodMetricsCRUD(controller)
 					crud.EXPECT().
 						Get("test", "pod").
-						Return(m, true, nil)
+						Return(m, nil)
 					loader.PodMetricsCRUD = crud
 				},
 			},
 			clusterClient: clusterClient,
 			want:          m,
-			wantFound:     true,
-		},
-		{
-			name: "object not found",
-			options: []octant.ClusterPodMetricsLoaderOption{
-				func(loader *octant.ClusterPodMetricsLoader) {
-					crud := octantFake.NewMockPodMetricsCRUD(controller)
-					crud.EXPECT().
-						Get("test", "pod").
-						Return(m, false, nil)
-					loader.PodMetricsCRUD = crud
-				},
-			},
-			clusterClient: clusterClient,
-			want:          m,
-			wantFound:     false,
 		},
 	}
 	for _, tt := range tests {
@@ -106,15 +89,13 @@ func TestClusterPodMetricsLoader_Load(t *testing.T) {
 			pml, err := octant.NewClusterPodMetricsLoader(tt.clusterClient, tt.options...)
 			require.NoError(t, err)
 
-			got, gotFound, err := pml.Load("test", "pod")
+			got, err := pml.Load("test", "pod")
 
 			if tt.wantErr {
 				require.Error(t, err)
-				require.False(t, gotFound)
 				return
 			}
 			require.NoError(t, err)
-			require.Equal(t, tt.wantFound, gotFound)
 			require.Equal(t, tt.want, got)
 		})
 	}
diff --git a/internal/octant/workload_test.go b/internal/octant/workload_test.go
index de30273d..68236b3c 100644
--- a/internal/octant/workload_test.go
+++ b/internal/octant/workload_test.go
@@ -42,7 +42,7 @@ func podMetricsLoader(controller *gomock.Controller, pm *unstructured.Unstructur
 	if pm != nil {
 		pml.EXPECT().
 			Load("namespace", gomock.Any()).
-			Return(pm, true, nil).
+			Return(pm, nil).
 			AnyTimes()
 	}
 
diff --git a/pkg/view/component/code_test.go b/pkg/view/component/code_test.go
new file mode 100644
index 00000000..713e29df
--- /dev/null
+++ b/pkg/view/component/code_test.go
@@ -0,0 +1,53 @@
+/*
+Copyright (c) 2020 the Octant contributors. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package component
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_Code_Marshal(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    Component
+		expected string
+		isErr    bool
+	}{
+		{
+			name: "general",
+			input: &Code{
+				Config: CodeConfig{
+					Code: "hello world\nthis is a newline",
+				},
+			},
+			expected: `
+			{
+				"metadata": {
+								"type": "codeBlock"
+				},
+				"config": {
+					"value": "hello world\nthis is a newline"
+				}
+			}
+`,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			actual, err := json.Marshal(tc.input)
+			isErr := (err != nil)
+			if isErr != tc.isErr {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+
+			assert.JSONEq(t, tc.expected, string(actual))
+		})
+	}
+}