Repository: vmware-tanzu/octant
Pull request #1567 (2020-10-30 20:49:03)

Show ConfigMaps (Volume-based) and PersistentVolumeClaims in Pod's resource viewer
**What this PR does / why we need it**:
This PR improves the Pod's resource viewer to include:
* ConfigMaps that are referenced by a Pod via Volumes
* PersistentVolumeClaims that are referenced by a Pod

**Which issue(s) this PR fixes**
- Addresses #1541

**Special notes for your reviewer**:

There might be other resources that are missing from a Pod's resource viewer, but opening this PR to keep the scope somewhat small.

Keeping that in mind, should we keep #1541 open?

**Release note**:
```
The Pod resource viewer now shows ConfigMaps mounted via Volumes and PersistentVolumeClaims
```



Patch:
diff --git a/changelogs/unreleased/1023-wwitzel3 b/changelogs/unreleased/1023-wwitzel3
deleted file mode 100644
index 233a75ae..00000000
--- a/changelogs/unreleased/1023-wwitzel3
+++ /dev/null
@@ -1,1 +0,0 @@
-Log warning, but don't block page render, when external metrics unavailable
diff --git a/changelogs/unreleased/1563-GuessWhoSamFoo b/changelogs/unreleased/1563-GuessWhoSamFoo
deleted file mode 100644
index 2c79237e..00000000
--- a/changelogs/unreleased/1563-GuessWhoSamFoo
+++ /dev/null
@@ -1,1 +0,0 @@
-Removed formGroup from payload actions and updated to return values for multiple checkboxes
diff --git a/internal/objectvisitor/pod.go b/internal/objectvisitor/pod.go
index a645196e..6012201c 100644
--- a/internal/objectvisitor/pod.go
+++ b/internal/objectvisitor/pod.go
@@ -143,5 +143,25 @@ func (p *Pod) Visit(ctx context.Context, object *unstructured.Unstructured, hand
 		return nil
 	})
 
+	g.Go(func() error {
+		pvcs, err := p.queryer.PersistentVolumeClaimsForPod(ctx, pod)
+		if err != nil {
+			return err
+		}
+		for i := range pvcs {
+			pvc := pvcs[i]
+			g.Go(func() error {
+				m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pvc)
+				if err != nil {
+					return err
+				}
+				u := &unstructured.Unstructured{Object: m}
+				return handler.AddEdge(ctx, object, u)
+			})
+		}
+
+		return nil
+	})
+
 	return g.Wait()
 }
diff --git a/internal/octant/fake/mock_pod_metrics_loader.go b/internal/octant/fake/mock_pod_metrics_loader.go
index 06d7e1d7..f30e3d91 100644
--- a/internal/octant/fake/mock_pod_metrics_loader.go
+++ b/internal/octant/fake/mock_pod_metrics_loader.go
@@ -5,7 +5,6 @@
 package fake
 
 import (
-	context "context"
 	reflect "reflect"
 
 	gomock "github.com/golang/mock/gomock"
@@ -52,16 +51,16 @@ func (mr *MockPodMetricsLoaderMockRecorder) Load(arg0, arg1 interface{}) *gomock
 }
 
 // SupportsMetrics mocks base method
-func (m *MockPodMetricsLoader) SupportsMetrics(arg0 context.Context) (bool, error) {
+func (m *MockPodMetricsLoader) SupportsMetrics() (bool, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "SupportsMetrics", arg0)
+	ret := m.ctrl.Call(m, "SupportsMetrics")
 	ret0, _ := ret[0].(bool)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // SupportsMetrics indicates an expected call of SupportsMetrics
-func (mr *MockPodMetricsLoaderMockRecorder) SupportsMetrics(arg0 interface{}) *gomock.Call {
+func (mr *MockPodMetricsLoaderMockRecorder) SupportsMetrics() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportsMetrics", reflect.TypeOf((*MockPodMetricsLoader)(nil).SupportsMetrics), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportsMetrics", reflect.TypeOf((*MockPodMetricsLoader)(nil).SupportsMetrics))
 }
diff --git a/internal/octant/pod_metrics_loader.go b/internal/octant/pod_metrics_loader.go
index ad56683f..8e6b2add 100644
--- a/internal/octant/pod_metrics_loader.go
+++ b/internal/octant/pod_metrics_loader.go
@@ -14,11 +14,9 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/client-go/discovery"
 
 	"github.com/vmware-tanzu/octant/internal/cluster"
 	"github.com/vmware-tanzu/octant/internal/gvk"
-	"github.com/vmware-tanzu/octant/internal/log"
 )
 
 //go:generate mockgen -destination=./fake/mock_pod_metrics_loader.go -package=fake github.com/vmware-tanzu/octant/internal/octant PodMetricsLoader
@@ -93,7 +91,7 @@ type PodMetricsLoader interface {
 	// object is not found.
 	Load(namespace, name string) (object *unstructured.Unstructured, isFound bool, err error)
 	// SupportsMetrics returns true if the cluster has metrics support.
-	SupportsMetrics(ctx context.Context) (bool, error)
+	SupportsMetrics() (bool, error)
 }
 
 // ClusterPodMetricsLoaderOption is an option for configuring ClusterPodMetricsLoader.
@@ -147,22 +145,17 @@ func (ml *ClusterPodMetricsLoader) Load(namespace, name string) (*unstructured.U
 	return ml.PodMetricsCRUD.Get(namespace, name)
 }
 
-func (ml *ClusterPodMetricsLoader) SupportsMetrics(ctx context.Context) (bool, error) {
+func (ml *ClusterPodMetricsLoader) SupportsMetrics() (bool, error) {
 	var sErr error
 	ml.supportsOnce.Do(func() {
-		discoveryClient, err := ml.clusterClient.DiscoveryClient()
+		discovery, err := ml.clusterClient.DiscoveryClient()
 		if err != nil {
 			sErr = fmt.Errorf("get discovery cluster: %w", err)
 			return
 		}
 
-		lists, err := discoveryClient.ServerPreferredNamespacedResources()
+		lists, err := discovery.ServerPreferredNamespacedResources()
 		if err != nil {
-			if discovery.IsGroupDiscoveryFailedError(err) {
-				logger := log.From(ctx)
-				logger.Warnf("metrics failed error: %w", err)
-				return
-			}
 			sErr = fmt.Errorf("get preferred namespaced resources: %w", err)
 			return
 		}
diff --git a/internal/octant/workload.go b/internal/octant/workload.go
index 7a1a3c99..863b69ca 100644
--- a/internal/octant/workload.go
+++ b/internal/octant/workload.go
@@ -391,11 +391,12 @@ func (wl *ClusterWorkloadLoader) Load(ctx context.Context, namespace string) ([]
 		workload := workloadTracker[uid]
 		workload.Owner = owner
 
-		resourceList, err := wl.podMetrics(ctx, object)
+		resourceList, err := wl.podMetrics(object)
 		if err != nil {
 			if IsPodMetricsNotSupported(err) {
 				workload.SetPodMetricsDisabled()
 				workload.AddPodStatus(status.Status(), object, corev1.ResourceList{})
+
 				continue
 			} else {
 				return nil, fmt.Errorf("get metrics for pod '%s': %w", object.GetName(), err)
@@ -415,12 +416,12 @@ func (wl *ClusterWorkloadLoader) Load(ctx context.Context, namespace string) ([]
 	return workloads, nil
 }
 
-func (wl *ClusterWorkloadLoader) podMetrics(ctx context.Context, pod *unstructured.Unstructured) (*corev1.ResourceList, error) {
+func (wl *ClusterWorkloadLoader) podMetrics(pod *unstructured.Unstructured) (*corev1.ResourceList, error) {
 	if pod == nil {
 		return nil, fmt.Errorf("pod is nil")
 	}
 
-	supportsPodMetrics, err := wl.PodMetricsLoader.SupportsMetrics(ctx)
+	supportsPodMetrics, err := wl.PodMetricsLoader.SupportsMetrics()
 	if err != nil {
 		return nil, fmt.Errorf("check for pod metrics support: %w", err)
 	}
diff --git a/internal/queryer/fake/mock_queryer.go b/internal/queryer/fake/mock_queryer.go
index e6afe43c..365e0394 100644
--- a/internal/queryer/fake/mock_queryer.go
+++ b/internal/queryer/fake/mock_queryer.go
@@ -147,6 +147,21 @@ func (mr *MockQueryerMockRecorder) OwnerReference(arg0, arg1 interface{}) *gomoc
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OwnerReference", reflect.TypeOf((*MockQueryer)(nil).OwnerReference), arg0, arg1)
 }
 
+// PersistentVolumeClaimsForPod mocks base method
+func (m *MockQueryer) PersistentVolumeClaimsForPod(arg0 context.Context, arg1 *v10.Pod) ([]*v10.PersistentVolumeClaim, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PersistentVolumeClaimsForPod", arg0, arg1)
+	ret0, _ := ret[0].([]*v10.PersistentVolumeClaim)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// PersistentVolumeClaimsForPod indicates an expected call of PersistentVolumeClaimsForPod
+func (mr *MockQueryerMockRecorder) PersistentVolumeClaimsForPod(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PersistentVolumeClaimsForPod", reflect.TypeOf((*MockQueryer)(nil).PersistentVolumeClaimsForPod), arg0, arg1)
+}
+
 // PodsForService mocks base method
 func (m *MockQueryer) PodsForService(arg0 context.Context, arg1 *v10.Service) ([]*v10.Pod, error) {
 	m.ctrl.T.Helper()
diff --git a/internal/queryer/queryer.go b/internal/queryer/queryer.go
index 55e5ef68..954a5e03 100644
--- a/internal/queryer/queryer.go
+++ b/internal/queryer/queryer.go
@@ -56,6 +56,7 @@ type Queryer interface {
 	ServiceAccountForPod(ctx context.Context, pod *corev1.Pod) (*corev1.ServiceAccount, error)
 	ConfigMapsForPod(ctx context.Context, pod *corev1.Pod) ([]*corev1.ConfigMap, error)
 	SecretsForPod(ctx context.Context, pod *corev1.Pod) ([]*corev1.Secret, error)
+	PersistentVolumeClaimsForPod(ctx context.Context, pod *corev1.Pod) ([]*corev1.PersistentVolumeClaim, error)
 }
 
 type childrenCache struct {
@@ -839,6 +840,12 @@ func (osq *ObjectStoreQueryer) ConfigMapsForPod(ctx context.Context, pod *corev1
 			return nil, errors.Wrap(err, "converting unstructured configmap")
 		}
 
+		for _, v := range pod.Spec.Volumes {
+			if v.ConfigMap != nil && v.ConfigMap.Name == configMap.Name {
+				configMaps = append(configMaps, configMap)
+			}
+		}
+
 		for ci := range pod.Spec.Containers {
 			c := &pod.Spec.Containers[ci]
 			for _, e := range c.Env {
@@ -918,6 +925,39 @@ func (osq *ObjectStoreQueryer) SecretsForPod(ctx context.Context, pod *corev1.Po
 	return secrets, nil
 }
 
+func (osq *ObjectStoreQueryer) PersistentVolumeClaimsForPod(ctx context.Context, pod *corev1.Pod) ([]*corev1.PersistentVolumeClaim, error) {
+	if pod == nil {
+		return nil, errors.New("pod is nil")
+	}
+
+	var persistentVolumeClaims []*corev1.PersistentVolumeClaim
+	key := store.Key{
+		Namespace:  pod.Namespace,
+		APIVersion: "v1",
+		Kind:       "PersistentVolumeClaim",
+	}
+	ul, _, err := osq.objectStore.List(ctx, key)
+	if err != nil {
+		return nil, errors.Wrap(err, "retrieving persistentVolumeClaims")
+	}
+
+	for i := range ul.Items {
+		pvc := &corev1.PersistentVolumeClaim{}
+		err := kubernetes.FromUnstructured(&ul.Items[i], pvc)
+		if err != nil {
+			return nil, errors.Wrap(err, "converting unstructured persistentVolumeClaim")
+		}
+
+		for _, v := range pod.Spec.Volumes {
+			if v.PersistentVolumeClaim != nil && v.PersistentVolumeClaim.ClaimName == pvc.Name {
+				persistentVolumeClaims = append(persistentVolumeClaims, pvc)
+			}
+		}
+	}
+
+	return persistentVolumeClaims, nil
+}
+
 func (osq *ObjectStoreQueryer) getSelector(object runtime.Object) (*metav1.LabelSelector, error) {
 	switch t := object.(type) {
 	case *appsv1.DaemonSet:
diff --git a/pkg/view/component/form.go b/pkg/view/component/form.go
index 0b1eb103..2d50cb45 100644
--- a/pkg/view/component/form.go
+++ b/pkg/view/component/form.go
@@ -620,7 +620,6 @@ type Form struct {
 func (f *Form) MarshalJSON() ([]byte, error) {
 	t := struct {
 		Fields []map[string]interface{} `json:"fields"`
-		Action string                   `json:"action,omitempty"`
 	}{}
 
 	for _, field := range f.Fields {
@@ -637,7 +636,6 @@ func (f *Form) MarshalJSON() ([]byte, error) {
 
 		t.Fields = append(t.Fields, m)
 	}
-	t.Action = f.Action
 
 	return json.Marshal(t)
 }
diff --git a/web/src/app/modules/shared/components/presentation/form/form.component.html b/web/src/app/modules/shared/components/presentation/form/form.component.html
index 61a02c75..8d12a22a 100644
--- a/web/src/app/modules/shared/components/presentation/form/form.component.html
+++ b/web/src/app/modules/shared/components/presentation/form/form.component.html
@@ -10,10 +10,9 @@
             <input
               type="checkbox"
               clrCheckbox
-              [formArraylName]="field.name"
+              [formControlName]="field.name"
               [value]="opt.value"
               [checked]="opt.checked"
-              (change)="onCheck($event, field.name)"
             />
             <label>{{ opt.label }}</label>
           </clr-checkbox-wrapper>
diff --git a/web/src/app/modules/shared/components/presentation/form/form.component.ts b/web/src/app/modules/shared/components/presentation/form/form.component.ts
index ec389328..f0cb7053 100644
--- a/web/src/app/modules/shared/components/presentation/form/form.component.ts
+++ b/web/src/app/modules/shared/components/presentation/form/form.component.ts
@@ -5,9 +5,7 @@
 import { Component, Input, OnInit, Output } from '@angular/core';
 import { ActionField, ActionForm } from '../../../models/content';
 import {
-  FormArray,
   FormBuilder,
-  FormControl,
   FormGroup,
   ValidatorFn,
   Validators,
@@ -29,7 +27,6 @@ export class FormComponent implements OnInit {
   form: ActionForm;
 
   formGroup: FormGroup;
-  formArray: FormArray;
 
   constructor(private formBuilder: FormBuilder) {}
 
@@ -41,30 +38,9 @@ export class FormComponent implements OnInit {
           field.value,
           this.getValidators(field.validators),
         ];
-        if (field.configuration?.choices && field.type === 'checkbox') {
-          const choices: Choice[] = field.configuration.choices;
-          controls[field.name] = new FormArray([]);
-          choices.forEach((choice: Choice) => {
-            if (choice.checked) {
-              controls[field.name].push(new FormControl(choice.value));
-            }
-          });
-        }
       });
-      this.formGroup = this.formBuilder.group(controls);
-    }
-  }
 
-  onCheck(event, field: string) {
-    this.formArray = this.formGroup.get(field) as FormArray;
-    if (event.target.checked) {
-      this.formArray.push(new FormControl(event.target.value));
-    } else {
-      this.formArray.controls.forEach((fc: FormControl, index: number) => {
-        if (fc.value === event.target.value) {
-          this.formArray.removeAt(index);
-        }
-      });
+      this.formGroup = this.formBuilder.group(controls);
     }
   }
 
diff --git a/web/src/app/modules/shared/components/presentation/heptagon-label/heptagon-label.component.spec.ts b/web/src/app/modules/shared/components/presentation/heptagon-label/heptagon-label.component.spec.ts
index 9e3f148f..540d0da7 100644
--- a/web/src/app/modules/shared/components/presentation/heptagon-label/heptagon-label.component.spec.ts
+++ b/web/src/app/modules/shared/components/presentation/heptagon-label/heptagon-label.component.spec.ts
@@ -2,7 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 import { Point } from '../../../models/point';
 import { HeptagonLabelComponent } from './heptagon-label.component';
 
@@ -10,13 +10,11 @@ describe('HeptagonLabelComponent', () => {
   let component: HeptagonLabelComponent;
   let fixture: ComponentFixture<HeptagonLabelComponent>;
 
-  beforeEach(
-    waitForAsync(() => {
-      TestBed.configureTestingModule({
-        declarations: [HeptagonLabelComponent],
-      }).compileComponents();
-    })
-  );
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [HeptagonLabelComponent],
+    }).compileComponents();
+  }));
 
   beforeEach(() => {
     fixture = TestBed.createComponent(HeptagonLabelComponent);
diff --git a/web/src/app/modules/shared/components/presentation/modal/modal.component.ts b/web/src/app/modules/shared/components/presentation/modal/modal.component.ts
index 7aafc229..3377a301 100644
--- a/web/src/app/modules/shared/components/presentation/modal/modal.component.ts
+++ b/web/src/app/modules/shared/components/presentation/modal/modal.component.ts
@@ -10,6 +10,7 @@ import {
 import { FormComponent } from '../form/form.component';
 import { ModalService } from '../../../services/modal/modal.service';
 import { Subscription } from 'rxjs';
+import { WebsocketService } from '../../../../../data/services/websocket/websocket.service';
 import { ActionService } from '../../../services/action/action.service';
 
 interface Choice {
@@ -40,7 +41,8 @@ export class ModalComponent
 
   constructor(
     private actionService: ActionService,
-    private modalService: ModalService
+    private modalService: ModalService,
+    private websocketService: WebsocketService
   ) {
     super();
   }
@@ -70,9 +72,9 @@ export class ModalComponent
 
   onFormSubmit() {
     if (this.modalAppForm && this.modalAppForm.formGroup.valid) {
-      this.actionService.perform({
+      this.websocketService.sendMessage('action.octant.dev/performAction', {
         action: this.action,
-        ...this.modalAppForm.formGroup.value,
+        formGroup: this.modalAppForm.formGroup.value,
       });
       this.opened = false;
     }
diff --git a/web/src/app/modules/shared/components/presentation/stepper/stepper.component.spec.ts b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.spec.ts
index 395527e9..3fb0280e 100644
--- a/web/src/app/modules/shared/components/presentation/stepper/stepper.component.spec.ts
+++ b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.spec.ts
@@ -13,14 +13,13 @@ import {
 } from '@angular/platform-browser/animations';
 import { WebsocketService } from '../../../../../data/services/websocket/websocket.service';
 import { anything, deepEqual, instance, mock, verify } from 'ts-mockito';
-import { ActionService } from '../../../services/action/action.service';
 
 describe('StepperComponent', () => {
   let component: StepperComponent;
   let fixture: ComponentFixture<StepperComponent>;
   const formBuilder: FormBuilder = new FormBuilder();
 
-  const mockActionService: ActionService = mock(ActionService);
+  const mockWebsocketService: WebsocketService = mock(WebsocketService);
 
   const action = 'action.octant.dev/test';
   const view: StepperView = {
@@ -57,8 +56,8 @@ describe('StepperComponent', () => {
         providers: [
           { provide: FormBuilder, useValue: formBuilder },
           {
-            provide: ActionService,
-            useValue: instance(mockActionService),
+            provide: WebsocketService,
+            useValue: instance(mockWebsocketService),
           },
         ],
       }).compileComponents();
@@ -91,8 +90,12 @@ describe('StepperComponent', () => {
         fixture.detectChanges();
 
         verify(
-          mockActionService.perform(
-            deepEqual({ action, 'step 1': {}, 'confirmation step': {} })
+          mockWebsocketService.sendMessage(
+            'action.octant.dev/performAction',
+            deepEqual({
+              action,
+              formGroup: anything(),
+            })
           )
         ).once();
       });
diff --git a/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts
index 4ccae10a..f7256d17 100644
--- a/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts
+++ b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts
@@ -6,8 +6,8 @@ import {
   ValidatorFn,
   Validators,
 } from '@angular/forms';
+import { WebsocketService } from '../../../../../data/services/websocket/websocket.service';
 import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
-import { ActionService } from '../../../services/action/action.service';
 
 interface Choice {
   label: string;
@@ -33,7 +33,7 @@ export class StepperComponent extends AbstractViewComponent<StepperView> {
 
   constructor(
     private formBuilder: FormBuilder,
-    private actionService: ActionService
+    private websocketService: WebsocketService
   ) {
     super();
   }
@@ -71,10 +71,13 @@ export class StepperComponent extends AbstractViewComponent<StepperView> {
   }
 
   onFormSubmit() {
-    this.actionService.perform({
+    this.websocketService.sendMessage('action.octant.dev/performAction', {
       action: this.action,
-      ...this.formGroup.value,
+      formGroup: this.formGroup.value,
     });
+    if (isDevMode()) {
+      console.log('stepper form: ' + this.formGroup.value);
+    }
   }
 
   onFormCancel() {
diff --git a/web/src/app/modules/shared/components/presentation/tabs/tabs.component.ts b/web/src/app/modules/shared/components/presentation/tabs/tabs.component.ts
index 9a792b2e..d64d817f 100644
--- a/web/src/app/modules/shared/components/presentation/tabs/tabs.component.ts
+++ b/web/src/app/modules/shared/components/presentation/tabs/tabs.component.ts
@@ -17,7 +17,7 @@ import {
 } from 'src/app/modules/shared/models/content';
 import { SliderService } from 'src/app/modules/shared/slider/slider.service';
 import { ViewService } from '../../../services/view/view.service';
-import { ActionService } from '../../../services/action/action.service';
+import { WebsocketService } from '../../../../../data/services/websocket/websocket.service';
 
 interface Tab {
   name: string;
@@ -51,7 +51,7 @@ export class TabsComponent implements OnChanges, OnInit {
     private activatedRoute: ActivatedRoute,
     private viewService: ViewService,
     private sliderService: SliderService,
-    private actionService: ActionService
+    private wss: WebsocketService
   ) {}
 
   ngOnInit() {
@@ -133,7 +133,7 @@ export class TabsComponent implements OnChanges, OnInit {
     if (tabIndex > -1) {
       if (this.payloads[tabIndex]) {
         const payload = this.payloads[tabIndex];
-        this.actionService.perform(payload);
+        this.wss.sendMessage('action.octant.dev/performAction', payload);
       }
 
       this.tabs = [


Test patch:
diff --git a/internal/objectvisitor/pod_test.go b/internal/objectvisitor/pod_test.go
index d77b99a0..7a8ae067 100644
--- a/internal/objectvisitor/pod_test.go
+++ b/internal/objectvisitor/pod_test.go
@@ -22,6 +22,7 @@ func TestPod_Visit(t *testing.T) {
 	serviceAccount := testutil.CreateServiceAccount("service-account")
 	configMap := testutil.CreateConfigMap("configmap")
 	secret := testutil.CreateSecret("secret")
+	pvc := testutil.CreatePersistentVolumeClaim("pvc")
 
 	object := testutil.CreatePod("pod")
 	object.Spec.ServiceAccountName = serviceAccount.Name
@@ -41,6 +42,9 @@ func TestPod_Visit(t *testing.T) {
 	q.EXPECT().
 		SecretsForPod(gomock.Any(), object).
 		Return([]*corev1.Secret{secret}, nil)
+	q.EXPECT().
+		PersistentVolumeClaimsForPod(gomock.Any(), object).
+		Return([]*corev1.PersistentVolumeClaim{pvc}, nil)
 
 	handler := fake.NewMockObjectHandler(controller)
 	handler.EXPECT().
@@ -54,6 +58,8 @@ func TestPod_Visit(t *testing.T) {
 		Return(nil)
 	handler.EXPECT().AddEdge(gomock.Any(), u, testutil.ToUnstructured(t, secret)).
 		Return(nil)
+	handler.EXPECT().AddEdge(gomock.Any(), u, testutil.ToUnstructured(t, pvc)).
+		Return(nil)
 
 	var visited []unstructured.Unstructured
 	visitor := fake.NewMockVisitor(controller)
diff --git a/internal/octant/pod_metrics_loader_test.go b/internal/octant/pod_metrics_loader_test.go
index 430bdefb..e0e2e30b 100644
--- a/internal/octant/pod_metrics_loader_test.go
+++ b/internal/octant/pod_metrics_loader_test.go
@@ -6,16 +6,12 @@
 package octant_test
 
 import (
-	"context"
-	"errors"
 	"testing"
 
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/require"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/client-go/discovery"
 
 	"github.com/vmware-tanzu/octant/internal/cluster"
 	clusterFake "github.com/vmware-tanzu/octant/internal/cluster/fake"
@@ -144,18 +140,13 @@ func TestClusterPodMetricsLoader_SupportsMetrics(t *testing.T) {
 			clusterClient: initClusterClientWithoutPodMetrics(controller),
 			want:          false,
 		},
-		{
-			name:          "cluster supports pod metrics but currently unavailable",
-			clusterClient: initClusterClientWithFailingExternalMetrics(controller),
-			want:          false,
-		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			pml, err := octant.NewClusterPodMetricsLoader(tt.clusterClient)
 			require.NoError(t, err)
 
-			got, err := pml.SupportsMetrics(context.TODO())
+			got, err := pml.SupportsMetrics()
 			if tt.wantErr {
 				require.Error(t, err)
 				return
@@ -199,30 +190,3 @@ func initClusterClientWithoutPodMetrics(controller *gomock.Controller) *clusterF
 
 	return clusterClient
 }
-
-func initClusterClientWithFailingExternalMetrics(controller *gomock.Controller) *clusterFake.MockClientInterface {
-	apiResourceLists := []*metav1.APIResourceList{
-		{
-			GroupVersion: gvk.PodMetrics.GroupVersion().String(),
-			APIResources: []metav1.APIResource{
-				{
-					Kind: gvk.PodMetrics.Kind,
-				},
-			},
-		},
-	}
-
-	errGroup := &discovery.ErrGroupDiscoveryFailed{
-		Groups: map[schema.GroupVersion]error{
-			gvk.PodMetrics.GroupVersion(): errors.New("server currently unavailable"),
-		},
-	}
-
-	discoveryClient := clusterFake.NewMockDiscoveryInterface(controller)
-	discoveryClient.EXPECT().ServerPreferredNamespacedResources().Return(apiResourceLists, errGroup)
-
-	clusterClient := clusterFake.NewMockClientInterface(controller)
-	clusterClient.EXPECT().DiscoveryClient().Return(discoveryClient, nil)
-
-	return clusterClient
-}
diff --git a/internal/octant/workload_test.go b/internal/octant/workload_test.go
index 44797117..1e41dea6 100644
--- a/internal/octant/workload_test.go
+++ b/internal/octant/workload_test.go
@@ -37,7 +37,7 @@ func podMetricsLoader(controller *gomock.Controller, pm *unstructured.Unstructur
 		err = fmt.Errorf("failed")
 	}
 
-	pml.EXPECT().SupportsMetrics(context.TODO()).Return(supportsMetrics, err).AnyTimes()
+	pml.EXPECT().SupportsMetrics().Return(supportsMetrics, err).AnyTimes()
 
 	if pm != nil {
 		pml.EXPECT().
diff --git a/internal/queryer/queryer_test.go b/internal/queryer/queryer_test.go
index f2cfab41..5ebb57e3 100644
--- a/internal/queryer/queryer_test.go
+++ b/internal/queryer/queryer_test.go
@@ -1222,8 +1222,12 @@ func TestObjectStoreQueryer_ServiceAccountForPod(t *testing.T) {
 func TestObjectStoreQueryer_ConfigMapsForPod(t *testing.T) {
 	configMapKeyRef := testutil.CreateConfigMap("configmap1")
 	configMapEnv := testutil.CreateConfigMap("configmap2")
+	configMapVolume := testutil.CreateConfigMap("configmap3")
+	configMapVolume2 := testutil.CreateConfigMap("configmap4")
+	unusedConfigMap := testutil.CreateConfigMap("unused")
 
 	pod := testutil.CreatePod("pod")
+
 	pod.Spec.Containers = []corev1.Container{
 		{
 			EnvFrom: []corev1.EnvFromSource{
@@ -1237,8 +1241,8 @@ func TestObjectStoreQueryer_ConfigMapsForPod(t *testing.T) {
 			},
 			Env: []corev1.EnvVar{
 				{
-					Name:  "configmap3",
-					Value: "configmap3_value",
+					Name:  "env_var_name",
+					Value: "env_var_value",
 				},
 				{
 					ValueFrom: &corev1.EnvVarSource{
@@ -1253,6 +1257,30 @@ func TestObjectStoreQueryer_ConfigMapsForPod(t *testing.T) {
 		},
 	}
 
+	// ConfigMap consumed via Volume
+	pod.Spec.Volumes = []corev1.Volume{
+		{
+			Name: "configmap-volume",
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: "configmap3",
+					},
+				},
+			},
+		},
+		{
+			Name: "another-configmap-volume",
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: "configmap4",
+					},
+				},
+			},
+		},
+	}
+
 	controller := gomock.NewController(t)
 	defer controller.Finish()
 
@@ -1271,7 +1299,7 @@ func TestObjectStoreQueryer_ConfigMapsForPod(t *testing.T) {
 
 	o.EXPECT().
 		List(gomock.Any(), gomock.Eq(key)).
-		Return(testutil.ToUnstructuredList(t, configMapKeyRef, configMapEnv), false, nil)
+		Return(testutil.ToUnstructuredList(t, configMapKeyRef, configMapEnv, configMapVolume, configMapVolume2, unusedConfigMap), false, nil)
 	configMaps, err := q.ConfigMapsForPod(ctx, pod)
 	require.NoError(t, err)
 
@@ -1281,7 +1309,7 @@ func TestObjectStoreQueryer_ConfigMapsForPod(t *testing.T) {
 	}
 	sort.Strings(got)
 
-	assert.Equal(t, []string([]string{configMapKeyRef.Name, configMapEnv.Name}), got)
+	assert.Equal(t, []string([]string{configMapKeyRef.Name, configMapEnv.Name, configMapVolume.Name, configMapVolume2.Name}), got)
 }
 
 func TestObjectStoreQueryer_SecretsForPod(t *testing.T) {
@@ -1373,6 +1401,61 @@ func TestObjectStoreQueryer_SecretsForPod(t *testing.T) {
 	assert.Equal(t, []string([]string{secretInVolume.Name, secretEnv.Name, secretEnvFrom.Name}), got)
 }
 
+func TestObjectStoreQueryer_PersistentVolumeClaimsForPod(t *testing.T) {
+	unusedPVC := testutil.CreatePersistentVolumeClaim("unused")
+	pvc1 := testutil.CreatePersistentVolumeClaim("pvc1")
+	pvc2 := testutil.CreatePersistentVolumeClaim("pvc2")
+
+	pod := testutil.CreatePod("pod")
+	pod.Spec.Volumes = []corev1.Volume{
+		{
+			Name: "vol1",
+			VolumeSource: corev1.VolumeSource{
+				PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+					ClaimName: "pvc1",
+				},
+			},
+		},
+		{
+			Name: "vol2",
+			VolumeSource: corev1.VolumeSource{
+				PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+					ClaimName: "pvc2",
+				},
+			},
+		},
+	}
+
+	controller := gomock.NewController(t)
+	defer controller.Finish()
+
+	o := storeFake.NewMockStore(controller)
+	key := store.Key{
+		Namespace:  "namespace",
+		APIVersion: "v1",
+		Kind:       "PersistentVolumeClaim",
+	}
+
+	discovery := queryerFake.NewMockDiscoveryInterface(controller)
+	q := New(o, discovery)
+	ctx := context.Background()
+
+	o.EXPECT().
+		List(gomock.Any(), gomock.Eq(key)).
+		Return(testutil.ToUnstructuredList(t, pvc1, pvc2, unusedPVC), false, nil)
+
+	pvcs, err := q.PersistentVolumeClaimsForPod(ctx, pod)
+	require.NoError(t, err)
+
+	var got []string
+	for _, pvc := range pvcs {
+		got = append(got, pvc.Name)
+	}
+	sort.Strings(got)
+
+	assert.Equal(t, []string{pvc1.Name, pvc2.Name}, got)
+}
+
 func TestObjectStoreQueryer_ScaleTarget(t *testing.T) {
 	deployment := testutil.CreateDeployment("deployment")