Просмотр исходного кода

Sth/kubemodel 2 pr fixes (#3889)

Signed-off-by: Sean Holcomb <seanholcomb@gmail.com>
Sean Holcomb 10 часов назад
Родитель
Сommit
f53137e112
48 измененных файлов с 7000 добавлено и 349 удалено
  1. 13 0
      core/pkg/clusters/mock.go
  2. 116 0
      core/pkg/compute/kubemodel/cluster_test.go
  3. 2 2
      core/pkg/compute/kubemodel/container.go
  4. 205 0
      core/pkg/compute/kubemodel/container_test.go
  5. 117 0
      core/pkg/compute/kubemodel/cronjob_test.go
  6. 138 0
      core/pkg/compute/kubemodel/daemonset_test.go
  7. 170 0
      core/pkg/compute/kubemodel/dcgmdevice_test.go
  8. 162 0
      core/pkg/compute/kubemodel/deployment_test.go
  9. 138 0
      core/pkg/compute/kubemodel/job_test.go
  10. 63 102
      core/pkg/compute/kubemodel/kubemodel.go
  11. 213 0
      core/pkg/compute/kubemodel/kubemodel_test.go
  12. 154 0
      core/pkg/compute/kubemodel/namespace_test.go
  13. 325 0
      core/pkg/compute/kubemodel/node_test.go
  14. 142 0
      core/pkg/compute/kubemodel/persistentvolume_test.go
  15. 155 0
      core/pkg/compute/kubemodel/persistentvolumeclaim_test.go
  16. 309 0
      core/pkg/compute/kubemodel/pod_test.go
  17. 166 0
      core/pkg/compute/kubemodel/replicaset_test.go
  18. 362 0
      core/pkg/compute/kubemodel/resourcequota_test.go
  19. 159 0
      core/pkg/compute/kubemodel/service_test.go
  20. 141 0
      core/pkg/compute/kubemodel/statefulset_test.go
  21. 11 3
      core/pkg/env/core.go
  22. 102 0
      core/pkg/env/metrics.go
  23. 12 12
      core/pkg/model/kubemodel/container.go
  24. 1 1
      core/pkg/model/kubemodel/daemonset.go
  25. 1 1
      core/pkg/model/kubemodel/kubemodel.go
  26. 7 160
      core/pkg/model/kubemodel/kubemodel_codecs.go
  27. 2 1
      core/pkg/model/kubemodel/kubemodel_helpers_test.go
  28. 8 8
      core/pkg/model/kubemodel/mock.go
  29. 213 0
      core/pkg/source/datasource.go
  30. 16 0
      core/pkg/source/decoders.go
  31. 1084 0
      core/pkg/source/mock.go
  32. 716 0
      core/pkg/source/noop.go
  33. 856 0
      core/pkg/source/record.go
  34. 329 0
      core/pkg/source/record_test.go
  35. 20 0
      modules/collector-source/pkg/collector/collector.go
  36. 4 0
      modules/collector-source/pkg/collector/metricsquerier.go
  37. 2 1
      modules/collector-source/pkg/metric/collector.go
  38. 4 0
      modules/collector-source/pkg/scrape/clusterinfo.go
  39. 60 6
      modules/collector-source/pkg/scrape/scrapecontroller.go
  40. 30 0
      modules/collector-source/pkg/scrape/scraper.go
  41. 226 0
      modules/collector-source/pkg/scrape/scraper_test.go
  42. 18 0
      modules/prometheus-source/pkg/prom/metricsquerier.go
  43. 2 1
      pkg/costmodel/costmodel.go
  44. 6 5
      pkg/costmodel/metrics.go
  45. 11 7
      pkg/costmodel/router.go
  46. 0 33
      pkg/env/costmodel.go
  47. 1 1
      pkg/kubemodel/pipeline.go
  48. 8 5
      pkg/metrics/kubemodel.go

+ 13 - 0
core/pkg/clusters/mock.go

@@ -0,0 +1,13 @@
+package clusters
+
+type MockClusterInfoProvider struct {
+	clusterInfo map[string]string
+}
+
+func NewMockClusterInfoProvider(clusterInfo map[string]string) *MockClusterInfoProvider {
+	return &MockClusterInfoProvider{clusterInfo: clusterInfo}
+}
+
+func (m *MockClusterInfoProvider) GetClusterInfo() map[string]string {
+	return m.clusterInfo
+}

+ 116 - 0
core/pkg/compute/kubemodel/cluster_test.go

@@ -0,0 +1,116 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/model/shared"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeCluster(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      *kubemodel.Cluster
+		wantErr   bool
+	}{
+		{
+			name:      "no data returns error",
+			overrides: map[string]any{},
+			wantErr:   true,
+		},
+		{
+			name: "basic cluster info and uptime",
+			overrides: map[string]any{
+				source.QueryClusterInfo: []*source.ClusterInfoResult{
+					{UID: testClusterUID, Cluster: "my-cluster"},
+				},
+				source.QueryClusterUptime: []*source.UptimeResult{
+					{UID: testClusterUID, First: start, Last: end},
+				},
+			},
+			want: &kubemodel.Cluster{
+				UID:   testClusterUID,
+				Name:  "my-cluster",
+				Start: start,
+				End:   end,
+			},
+		},
+		{
+			name: "cluster with provider, account, and region",
+			overrides: map[string]any{
+				source.QueryClusterInfo: []*source.ClusterInfoResult{
+					{UID: testClusterUID, Cluster: "prod-cluster", Provider: "aws", AccountID: "123456789", Region: "us-east-1"},
+				},
+				source.QueryClusterUptime: []*source.UptimeResult{
+					{UID: testClusterUID, First: start, Last: end},
+				},
+			},
+			want: &kubemodel.Cluster{
+				UID:      testClusterUID,
+				Name:     "prod-cluster",
+				Provider: shared.ProviderAWS,
+				Account:  "123456789",
+				Region:   "us-east-1",
+				Start:    start,
+				End:      end,
+			},
+		},
+		{
+			name: "cluster without uptime is registered with zero window but fails validation",
+			overrides: map[string]any{
+				source.QueryClusterInfo: []*source.ClusterInfoResult{
+					{UID: testClusterUID, Cluster: "my-cluster"},
+				},
+			},
+			want: nil,
+		},
+		{
+			name: "uptime for unknown cluster is ignored",
+			overrides: map[string]any{
+				source.QueryClusterInfo: []*source.ClusterInfoResult{
+					{UID: testClusterUID, Cluster: "my-cluster"},
+				},
+				source.QueryClusterUptime: []*source.UptimeResult{
+					{UID: testClusterUID, First: start, Last: end},
+					{UID: "unknown-cluster", First: start, Last: end},
+				},
+			},
+			want: &kubemodel.Cluster{
+				UID:   testClusterUID,
+				Name:  "my-cluster",
+				Start: start,
+				End:   end,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			assert.Equal(t, tt.want, kms.Cluster)
+		})
+	}
+}

+ 2 - 2
core/pkg/compute/kubemodel/container.go

@@ -92,7 +92,7 @@ func (km *KubeModel) computeContainers(kms *kubemodel.KubeModelSet, start, end t
 			continue
 		}
 		if len(res.Data) > 0 {
-			container.CPUCoresAllocated = res.Data[0].Value
+			container.CPUCoreAllocationAvg = res.Data[0].Value
 		}
 	}
 
@@ -105,7 +105,7 @@ func (km *KubeModel) computeContainers(kms *kubemodel.KubeModelSet, start, end t
 			continue
 		}
 		if len(res.Data) > 0 {
-			container.RAMBytesAllocated = res.Data[0].Value
+			container.RAMBytesAllocationAvg = res.Data[0].Value
 		}
 	}
 

+ 205 - 0
core/pkg/compute/kubemodel/container_test.go

@@ -0,0 +1,205 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+	"github.com/opencost/opencost/core/pkg/util"
+)
+
+func TestComputeContainers(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.Container
+	}{
+		{
+			name:      "no data returns empty container map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.Container{},
+		},
+		{
+			name: "basic container uptime",
+			overrides: map[string]any{
+				source.QueryContainerUptime: []*source.ContainerUptimeResult{
+					{UptimeResult: source.UptimeResult{UID: "pod-1", First: start, Last: end}, Container: "main"},
+				},
+			},
+			want: map[string]*kubemodel.Container{
+				"pod-1/main": {
+					PodUID:           "pod-1",
+					Name:             "main",
+					Start:            start,
+					End:              end,
+					ResourceRequests: kubemodel.ResourceQuantities{},
+					ResourceLimits:   kubemodel.ResourceQuantities{},
+				},
+			},
+		},
+		{
+			name: "multiple containers on same pod",
+			overrides: map[string]any{
+				source.QueryContainerUptime: []*source.ContainerUptimeResult{
+					{UptimeResult: source.UptimeResult{UID: "pod-1", First: start, Last: end}, Container: "main"},
+					{UptimeResult: source.UptimeResult{UID: "pod-1", First: start, Last: end}, Container: "sidecar"},
+				},
+			},
+			want: map[string]*kubemodel.Container{
+				"pod-1/main": {
+					PodUID:           "pod-1",
+					Name:             "main",
+					Start:            start,
+					End:              end,
+					ResourceRequests: kubemodel.ResourceQuantities{},
+					ResourceLimits:   kubemodel.ResourceQuantities{},
+				},
+				"pod-1/sidecar": {
+					PodUID:           "pod-1",
+					Name:             "sidecar",
+					Start:            start,
+					End:              end,
+					ResourceRequests: kubemodel.ResourceQuantities{},
+					ResourceLimits:   kubemodel.ResourceQuantities{},
+				},
+			},
+		},
+		{
+			name: "resource requests and limits are populated",
+			overrides: map[string]any{
+				source.QueryContainerUptime: []*source.ContainerUptimeResult{
+					{UptimeResult: source.UptimeResult{UID: "pod-1", First: start, Last: end}, Container: "main"},
+				},
+				source.QueryContainerResourceRequests: []*source.ContainerResourceResult{
+					{ResourceResult: source.ResourceResult{UID: "pod-1", Resource: "cpu", Unit: "cores", Value: 0.5}, Container: "main"},
+					{ResourceResult: source.ResourceResult{UID: "pod-1", Resource: "memory", Unit: "bytes", Value: 512 * 1024 * 1024}, Container: "main"},
+				},
+				source.QueryContainerResourceLimits: []*source.ContainerResourceResult{
+					{ResourceResult: source.ResourceResult{UID: "pod-1", Resource: "cpu", Unit: "cores", Value: 1.0}, Container: "main"},
+					{ResourceResult: source.ResourceResult{UID: "pod-1", Resource: "memory", Unit: "bytes", Value: 1024 * 1024 * 1024}, Container: "main"},
+				},
+			},
+			want: map[string]*kubemodel.Container{
+				"pod-1/main": {
+					PodUID: "pod-1",
+					Name:   "main",
+					Start:  start,
+					End:    end,
+					ResourceRequests: kubemodel.ResourceQuantities{
+						kubemodel.ResourceCPU: {
+							Resource: kubemodel.ResourceCPU,
+							Unit:     kubemodel.UnitCore,
+							Values:   kubemodel.Stats{kubemodel.StatAvg: 0.5},
+						},
+						kubemodel.ResourceMemory: {
+							Resource: kubemodel.ResourceMemory,
+							Unit:     kubemodel.UnitByte,
+							Values:   kubemodel.Stats{kubemodel.StatAvg: 512 * 1024 * 1024},
+						},
+					},
+					ResourceLimits: kubemodel.ResourceQuantities{
+						kubemodel.ResourceCPU: {
+							Resource: kubemodel.ResourceCPU,
+							Unit:     kubemodel.UnitCore,
+							Values:   kubemodel.Stats{kubemodel.StatAvg: 1.0},
+						},
+						kubemodel.ResourceMemory: {
+							Resource: kubemodel.ResourceMemory,
+							Unit:     kubemodel.UnitByte,
+							Values:   kubemodel.Stats{kubemodel.StatAvg: 1024 * 1024 * 1024},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "cpu and ram allocation and usage are populated",
+			overrides: map[string]any{
+				source.QueryContainerUptime: []*source.ContainerUptimeResult{
+					{UptimeResult: source.UptimeResult{UID: "pod-1", First: start, Last: end}, Container: "main"},
+				},
+				source.QueryCPUCoresAllocated: []*source.CPUCoresAllocatedResult{
+					{UID: "pod-1", Container: "main", Data: []*util.Vector{{Value: 0.25}}},
+				},
+				source.QueryRAMBytesAllocated: []*source.RAMBytesAllocatedResult{
+					{UID: "pod-1", Container: "main", Data: []*util.Vector{{Value: 256 * 1024 * 1024}}},
+				},
+				source.QueryCPUUsageAvg: []*source.CPUUsageAvgResult{
+					{UID: "pod-1", Container: "main", Data: []*util.Vector{{Value: 0.1}}},
+				},
+				source.QueryCPUUsageMax: []*source.CPUUsageMaxResult{
+					{UID: "pod-1", Container: "main", Data: []*util.Vector{{Value: 0.2}}},
+				},
+				source.QueryRAMUsageAvg: []*source.RAMUsageAvgResult{
+					{UID: "pod-1", Container: "main", Data: []*util.Vector{{Value: 128 * 1024 * 1024}}},
+				},
+				source.QueryRAMUsageMax: []*source.RAMUsageMaxResult{
+					{UID: "pod-1", Container: "main", Data: []*util.Vector{{Value: 200 * 1024 * 1024}}},
+				},
+			},
+			want: map[string]*kubemodel.Container{
+				"pod-1/main": {
+					PodUID:                "pod-1",
+					Name:                  "main",
+					Start:                 start,
+					End:                   end,
+					ResourceRequests:      kubemodel.ResourceQuantities{},
+					ResourceLimits:        kubemodel.ResourceQuantities{},
+					CPUCoreAllocationAvg:  0.25,
+					RAMBytesAllocationAvg: 256 * 1024 * 1024,
+					CPUCoreUsageAvg:       0.1,
+					CPUCoreUsageMax:       0.2,
+					RAMBytesUsageAvg:      128 * 1024 * 1024,
+					RAMBytesUsageMax:      200 * 1024 * 1024,
+				},
+			},
+		},
+		{
+			name: "resource requests for unknown container are ignored",
+			overrides: map[string]any{
+				source.QueryContainerUptime: []*source.ContainerUptimeResult{
+					{UptimeResult: source.UptimeResult{UID: "pod-1", First: start, Last: end}, Container: "main"},
+				},
+				source.QueryContainerResourceRequests: []*source.ContainerResourceResult{
+					{ResourceResult: source.ResourceResult{UID: "pod-1", Resource: "cpu", Unit: "cores", Value: 0.5}, Container: "unknown-container"},
+				},
+			},
+			want: map[string]*kubemodel.Container{
+				"pod-1/main": {
+					PodUID:           "pod-1",
+					Name:             "main",
+					Start:            start,
+					End:              end,
+					ResourceRequests: kubemodel.ResourceQuantities{},
+					ResourceLimits:   kubemodel.ResourceQuantities{},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.Containers)
+		})
+	}
+}

+ 117 - 0
core/pkg/compute/kubemodel/cronjob_test.go

@@ -0,0 +1,117 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeCronJobs(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.CronJob
+	}{
+		{
+			name:      "no data returns empty cronjob map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.CronJob{},
+		},
+		{
+			name: "basic cronjob info and uptime",
+			overrides: map[string]any{
+				source.QueryCronJobInfo: []*source.CronJobInfoResult{
+					{UID: "cj-1", CronJob: "nightly-backup", NamespaceUID: "ns-1"},
+				},
+				source.QueryCronJobUptime: []*source.UptimeResult{
+					{UID: "cj-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.CronJob{
+				"cj-1": {
+					UID:          "cj-1",
+					Name:         "nightly-backup",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "cronjob without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryCronJobInfo: []*source.CronJobInfoResult{
+					{UID: "cj-1", CronJob: "nightly-backup", NamespaceUID: "ns-1"},
+				},
+			},
+			want: map[string]*kubemodel.CronJob{},
+		},
+		{
+			name: "cronjob without namespace uid is not registered",
+			overrides: map[string]any{
+				source.QueryCronJobInfo: []*source.CronJobInfoResult{
+					{UID: "cj-1", CronJob: "nightly-backup"},
+				},
+				source.QueryCronJobUptime: []*source.UptimeResult{
+					{UID: "cj-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.CronJob{},
+		},
+		{
+			name: "cronjob labels and annotations are attached",
+			overrides: map[string]any{
+				source.QueryCronJobInfo: []*source.CronJobInfoResult{
+					{UID: "cj-1", CronJob: "nightly-backup", NamespaceUID: "ns-1"},
+				},
+				source.QueryCronJobUptime: []*source.UptimeResult{
+					{UID: "cj-1", First: start, Last: end},
+				},
+				source.QueryCronJobLabels: []*source.LabelsResult{
+					{UID: "cj-1", Labels: map[string]string{"schedule": "nightly"}},
+				},
+				source.QueryCronJobAnnotations: []*source.AnnotationsResult{
+					{UID: "cj-1", Annotations: map[string]string{"owner": "ops"}},
+				},
+			},
+			want: map[string]*kubemodel.CronJob{
+				"cj-1": {
+					UID:          "cj-1",
+					Name:         "nightly-backup",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Labels:       map[string]string{"schedule": "nightly"},
+					Annotations:  map[string]string{"owner": "ops"},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.CronJobs)
+		})
+	}
+}

+ 138 - 0
core/pkg/compute/kubemodel/daemonset_test.go

@@ -0,0 +1,138 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeDaemonSets(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.DaemonSet
+	}{
+		{
+			name:      "no data returns empty daemonset map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.DaemonSet{},
+		},
+		{
+			name: "basic daemonset info and uptime",
+			overrides: map[string]any{
+				source.QueryDaemonSetInfo: []*source.DaemonSetInfoResult{
+					{UID: "ds-1", DaemonSet: "fluentd", NamespaceUID: "ns-1"},
+				},
+				source.QueryDaemonSetUptime: []*source.UptimeResult{
+					{UID: "ds-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.DaemonSet{
+				"ds-1": {
+					UID:          "ds-1",
+					Name:         "fluentd",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "daemonset without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryDaemonSetInfo: []*source.DaemonSetInfoResult{
+					{UID: "ds-1", DaemonSet: "fluentd", NamespaceUID: "ns-1"},
+				},
+			},
+			want: map[string]*kubemodel.DaemonSet{},
+		},
+		{
+			name: "daemonset without namespace uid is not registered",
+			overrides: map[string]any{
+				source.QueryDaemonSetInfo: []*source.DaemonSetInfoResult{
+					{UID: "ds-1", DaemonSet: "fluentd"},
+				},
+				source.QueryDaemonSetUptime: []*source.UptimeResult{
+					{UID: "ds-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.DaemonSet{},
+		},
+		{
+			name: "daemonset labels and annotations are attached",
+			overrides: map[string]any{
+				source.QueryDaemonSetInfo: []*source.DaemonSetInfoResult{
+					{UID: "ds-1", DaemonSet: "fluentd", NamespaceUID: "ns-1"},
+				},
+				source.QueryDaemonSetUptime: []*source.UptimeResult{
+					{UID: "ds-1", First: start, Last: end},
+				},
+				source.QueryDaemonSetLabels: []*source.LabelsResult{
+					{UID: "ds-1", Labels: map[string]string{"component": "logging"}},
+				},
+				source.QueryDaemonSetAnnotations: []*source.AnnotationsResult{
+					{UID: "ds-1", Annotations: map[string]string{"managed-by": "helm"}},
+				},
+			},
+			want: map[string]*kubemodel.DaemonSet{
+				"ds-1": {
+					UID:          "ds-1",
+					Name:         "fluentd",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Labels:       map[string]string{"component": "logging"},
+					Annotations:  map[string]string{"managed-by": "helm"},
+				},
+			},
+		},
+		{
+			name: "uptime for unknown daemonset is ignored",
+			overrides: map[string]any{
+				source.QueryDaemonSetInfo: []*source.DaemonSetInfoResult{
+					{UID: "ds-1", DaemonSet: "fluentd", NamespaceUID: "ns-1"},
+				},
+				source.QueryDaemonSetUptime: []*source.UptimeResult{
+					{UID: "ds-1", First: start, Last: end},
+					{UID: "unknown-ds", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.DaemonSet{
+				"ds-1": {
+					UID:          "ds-1",
+					Name:         "fluentd",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.DaemonSets)
+		})
+	}
+}

+ 170 - 0
core/pkg/compute/kubemodel/dcgmdevice_test.go

@@ -0,0 +1,170 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeDCGMDevices(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.DCGMDevice
+	}{
+		{
+			name:      "no data returns empty dcgm device map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.DCGMDevice{},
+		},
+		{
+			name: "basic dcgm device info and uptime",
+			overrides: map[string]any{
+				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
+					{UUID: "GPU-abc123", Device: "nvidia0", ModelName: "A100"},
+				},
+				source.QueryDCGMDeviceUptime: []*source.DCGMDeviceUptimeResult{
+					{UUID: "GPU-abc123", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.DCGMDevice{
+				"GPU-abc123": {
+					UUID:      "GPU-abc123",
+					Device:    "nvidia0",
+					ModelName: "A100",
+					Start:     start,
+					End:       end,
+					PodUsages: map[string]kubemodel.DCGMPod{},
+				},
+			},
+		},
+		{
+			name: "dcgm device without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
+					{UUID: "GPU-abc123", Device: "nvidia0", ModelName: "A100"},
+				},
+			},
+			want: map[string]*kubemodel.DCGMDevice{},
+		},
+		{
+			name: "dcgm device with empty uuid is skipped",
+			overrides: map[string]any{
+				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
+					{UUID: "", Device: "nvidia0", ModelName: "A100"},
+				},
+				source.QueryDCGMDeviceUptime: []*source.DCGMDeviceUptimeResult{
+					{UUID: "GPU-abc123", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.DCGMDevice{},
+		},
+		{
+			name: "dcgm container usage avg and max are populated",
+			overrides: map[string]any{
+				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
+					{UUID: "GPU-abc123", Device: "nvidia0", ModelName: "A100"},
+				},
+				source.QueryDCGMDeviceUptime: []*source.DCGMDeviceUptimeResult{
+					{UUID: "GPU-abc123", First: start, Last: end},
+				},
+				source.QueryDCGMContainerUsageAvg: []*source.DCGMDeviceContainerUsageResult{
+					{UUID: "GPU-abc123", PodUID: "pod-1", Container: "training", Value: 0.75},
+				},
+				source.QueryDCGMContainerUsageMax: []*source.DCGMDeviceContainerUsageResult{
+					{UUID: "GPU-abc123", PodUID: "pod-1", Container: "training", Value: 0.95},
+				},
+			},
+			want: map[string]*kubemodel.DCGMDevice{
+				"GPU-abc123": {
+					UUID:      "GPU-abc123",
+					Device:    "nvidia0",
+					ModelName: "A100",
+					Start:     start,
+					End:       end,
+					PodUsages: map[string]kubemodel.DCGMPod{
+						"pod-1": {
+							ContainerUsages: map[string]kubemodel.DCGMContainer{
+								"training": {UsageAvg: 0.75, UsageMax: 0.95},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "usage with empty pod uid or container is ignored",
+			overrides: map[string]any{
+				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
+					{UUID: "GPU-abc123", Device: "nvidia0", ModelName: "A100"},
+				},
+				source.QueryDCGMDeviceUptime: []*source.DCGMDeviceUptimeResult{
+					{UUID: "GPU-abc123", First: start, Last: end},
+				},
+				source.QueryDCGMContainerUsageAvg: []*source.DCGMDeviceContainerUsageResult{
+					{UUID: "GPU-abc123", PodUID: "", Container: "training", Value: 0.5},
+					{UUID: "GPU-abc123", PodUID: "pod-1", Container: "", Value: 0.5},
+				},
+			},
+			want: map[string]*kubemodel.DCGMDevice{
+				"GPU-abc123": {
+					UUID:      "GPU-abc123",
+					Device:    "nvidia0",
+					ModelName: "A100",
+					Start:     start,
+					End:       end,
+					PodUsages: map[string]kubemodel.DCGMPod{},
+				},
+			},
+		},
+		{
+			name: "duplicate device info entries use first occurrence",
+			overrides: map[string]any{
+				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
+					{UUID: "GPU-abc123", Device: "nvidia0", ModelName: "A100"},
+					{UUID: "GPU-abc123", Device: "nvidia0-dup", ModelName: "A100-dup"},
+				},
+				source.QueryDCGMDeviceUptime: []*source.DCGMDeviceUptimeResult{
+					{UUID: "GPU-abc123", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.DCGMDevice{
+				"GPU-abc123": {
+					UUID:      "GPU-abc123",
+					Device:    "nvidia0",
+					ModelName: "A100",
+					Start:     start,
+					End:       end,
+					PodUsages: map[string]kubemodel.DCGMPod{},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.DCGMDevices)
+		})
+	}
+}

+ 162 - 0
core/pkg/compute/kubemodel/deployment_test.go

@@ -0,0 +1,162 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeDeployments(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.Deployment
+	}{
+		{
+			name:      "no data returns empty deployment map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.Deployment{},
+		},
+		{
+			name: "basic deployment info and uptime",
+			overrides: map[string]any{
+				source.QueryDeploymentInfo: []*source.DeploymentInfoResult{
+					{UID: "dep-1", Deployment: "my-app", NamespaceUID: "ns-1"},
+				},
+				source.QueryDeploymentUptime: []*source.UptimeResult{
+					{UID: "dep-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Deployment{
+				"dep-1": {
+					UID:          "dep-1",
+					Name:         "my-app",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "deployment without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryDeploymentInfo: []*source.DeploymentInfoResult{
+					{UID: "dep-1", Deployment: "my-app", NamespaceUID: "ns-1"},
+				},
+			},
+			want: map[string]*kubemodel.Deployment{},
+		},
+		{
+			name: "deployment without namespace uid is not registered",
+			overrides: map[string]any{
+				source.QueryDeploymentInfo: []*source.DeploymentInfoResult{
+					{UID: "dep-1", Deployment: "my-app"},
+				},
+				source.QueryDeploymentUptime: []*source.UptimeResult{
+					{UID: "dep-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Deployment{},
+		},
+		{
+			name: "deployment labels and annotations are attached",
+			overrides: map[string]any{
+				source.QueryDeploymentInfo: []*source.DeploymentInfoResult{
+					{UID: "dep-1", Deployment: "my-app", NamespaceUID: "ns-1"},
+				},
+				source.QueryDeploymentUptime: []*source.UptimeResult{
+					{UID: "dep-1", First: start, Last: end},
+				},
+				source.QueryDeploymentLabels: []*source.LabelsResult{
+					{UID: "dep-1", Labels: map[string]string{"app": "web"}},
+				},
+				source.QueryDeploymentAnnotations: []*source.AnnotationsResult{
+					{UID: "dep-1", Annotations: map[string]string{"team": "platform"}},
+				},
+			},
+			want: map[string]*kubemodel.Deployment{
+				"dep-1": {
+					UID:          "dep-1",
+					Name:         "my-app",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Labels:       map[string]string{"app": "web"},
+					Annotations:  map[string]string{"team": "platform"},
+				},
+			},
+		},
+		{
+			name: "deployment match labels are attached",
+			overrides: map[string]any{
+				source.QueryDeploymentInfo: []*source.DeploymentInfoResult{
+					{UID: "dep-1", Deployment: "my-app", NamespaceUID: "ns-1"},
+				},
+				source.QueryDeploymentUptime: []*source.UptimeResult{
+					{UID: "dep-1", First: start, Last: end},
+				},
+				source.QueryDeploymentMatchLabels: []*source.DeploymentLabelsResult{
+					{UID: "dep-1", Labels: map[string]string{"app": "web", "tier": "frontend"}},
+				},
+			},
+			want: map[string]*kubemodel.Deployment{
+				"dep-1": {
+					UID:          "dep-1",
+					Name:         "my-app",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					MatchLabels:  map[string]string{"app": "web", "tier": "frontend"},
+				},
+			},
+		},
+		{
+			name: "uptime for unknown deployment is ignored",
+			overrides: map[string]any{
+				source.QueryDeploymentInfo: []*source.DeploymentInfoResult{
+					{UID: "dep-1", Deployment: "my-app", NamespaceUID: "ns-1"},
+				},
+				source.QueryDeploymentUptime: []*source.UptimeResult{
+					{UID: "dep-1", First: start, Last: end},
+					{UID: "unknown-dep", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Deployment{
+				"dep-1": {
+					UID:          "dep-1",
+					Name:         "my-app",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.Deployments)
+		})
+	}
+}

+ 138 - 0
core/pkg/compute/kubemodel/job_test.go

@@ -0,0 +1,138 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeJobs(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.Job
+	}{
+		{
+			name:      "no data returns empty job map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.Job{},
+		},
+		{
+			name: "basic job info and uptime",
+			overrides: map[string]any{
+				source.QueryJobInfo: []*source.JobInfoResult{
+					{UID: "job-1", Job: "batch-processor", NamespaceUID: "ns-1"},
+				},
+				source.QueryJobUptime: []*source.UptimeResult{
+					{UID: "job-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Job{
+				"job-1": {
+					UID:          "job-1",
+					Name:         "batch-processor",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "job without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryJobInfo: []*source.JobInfoResult{
+					{UID: "job-1", Job: "batch-processor", NamespaceUID: "ns-1"},
+				},
+			},
+			want: map[string]*kubemodel.Job{},
+		},
+		{
+			name: "job without namespace uid is not registered",
+			overrides: map[string]any{
+				source.QueryJobInfo: []*source.JobInfoResult{
+					{UID: "job-1", Job: "batch-processor"},
+				},
+				source.QueryJobUptime: []*source.UptimeResult{
+					{UID: "job-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Job{},
+		},
+		{
+			name: "job labels and annotations are attached",
+			overrides: map[string]any{
+				source.QueryJobInfo: []*source.JobInfoResult{
+					{UID: "job-1", Job: "etl-job", NamespaceUID: "ns-1"},
+				},
+				source.QueryJobUptime: []*source.UptimeResult{
+					{UID: "job-1", First: start, Last: end},
+				},
+				source.QueryJobLabels: []*source.LabelsResult{
+					{UID: "job-1", Labels: map[string]string{"batch": "nightly"}},
+				},
+				source.QueryJobAnnotations: []*source.AnnotationsResult{
+					{UID: "job-1", Annotations: map[string]string{"schedule": "0 2 * * *"}},
+				},
+			},
+			want: map[string]*kubemodel.Job{
+				"job-1": {
+					UID:          "job-1",
+					Name:         "etl-job",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Labels:       map[string]string{"batch": "nightly"},
+					Annotations:  map[string]string{"schedule": "0 2 * * *"},
+				},
+			},
+		},
+		{
+			name: "uptime for unknown job is ignored",
+			overrides: map[string]any{
+				source.QueryJobInfo: []*source.JobInfoResult{
+					{UID: "job-1", Job: "batch-processor", NamespaceUID: "ns-1"},
+				},
+				source.QueryJobUptime: []*source.UptimeResult{
+					{UID: "job-1", First: start, Last: end},
+					{UID: "unknown-job", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Job{
+				"job-1": {
+					UID:          "job-1",
+					Name:         "batch-processor",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.Jobs)
+		})
+	}
+}

+ 63 - 102
core/pkg/compute/kubemodel/kubemodel.go

@@ -10,20 +10,20 @@ import (
 	"github.com/opencost/opencost/core/pkg/source"
 )
 
-const logTimeFmt string = "2006-01-02T15:04:05"
-
 type KubeModel struct {
 	ds         source.OpenCostDataSource
+	forceV1    bool
 	clusterUID string
 }
 
-func NewKubeModel(clusterUID string, dataSource source.OpenCostDataSource) (*KubeModel, error) {
+func NewKubeModel(clusterUID string, forceV1 bool, dataSource source.OpenCostDataSource) (*KubeModel, error) {
 	if dataSource == nil {
 		return nil, errors.New("OpenCostDataSource cannot be nil")
 	}
 
 	km := &KubeModel{
 		ds:         dataSource,
+		forceV1:    forceV1,
 		clusterUID: clusterUID,
 	}
 
@@ -34,115 +34,76 @@ func NewKubeModel(clusterUID string, dataSource source.OpenCostDataSource) (*Kub
 	return km, nil
 }
 
+type computeFunc func(*kubemodel.KubeModelSet, time.Time, time.Time) error
+
 // ComputeKubeModel uses the CostModel instance to compute an KubeModelSet
 // for the window defined by the given start and end times. The KubeModels
 // returned are unaggregated (i.e. down to the container level).
 func (km *KubeModel) ComputeKubeModelSet(start, end time.Time) (*kubemodel.KubeModelSet, error) {
-	// 1. Initialize new KubeModelSet for requested Window
 	kms := kubemodel.NewKubeModelSet(start, end)
 
-	// 2. Query CostModel for each set of objects
-	var err error
-
-	// 2.1 Compute Cluster
-	err = km.computeCluster(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-		return kms, fmt.Errorf("error computing kubemodel.Cluster for (%s, %s): %w", start.Format(logTimeFmt), end.Format(logTimeFmt), err)
-	}
-
-	// 2.2 Compute Nodes
-	err = km.computeNodes(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.3 Compute Namespaces
-	err = km.computeNamespaces(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.5 Compute Pods
-	err = km.computePods(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.6 Compute Deployments
-	err = km.computeDeployments(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
+	computeFuncs := km.computeFuncs(start, end)
 
-	// 2.7 Compute StatefulSets
-	err = km.computeStatefulSets(kms, start, end)
-	if err != nil {
-		kms.Error(err)
+	for _, f := range computeFuncs {
+		if err := f(kms, start, end); err != nil {
+			kms.Error(err)
+			return kms, fmt.Errorf("error computing kubemodel for (%s, %s): %w", start.Format(time.DateTime), end.Format(time.DateTime), err)
+		}
 	}
 
-	// 2.8 Compute DaemonSets
-	err = km.computeDaemonSets(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.9 Compute Jobs
-	err = km.computeJobs(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.10 Compute CronJobs
-	err = km.computeCronJobs(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.11 Compute ReplicaSets
-	err = km.computeReplicaSets(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.12 Compute Containers
-	err = km.computeContainers(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.13 Compute ResourceQuotas
-	err = km.computeResourceQuotas(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.14 Compute Services
-	err = km.computeServices(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.15 Compute PersistentVolumes
-	err = km.computePersistentVolumes(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.16 Compute PersistentVolumeClaims
-	err = km.computePersistentVolumeClaims(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 2.17 Compute DCGM Devices
-	err = km.computeDCGMDevices(kms, start, end)
-	if err != nil {
-		kms.Error(err)
-	}
-
-	// 3. Mark KubeModelSet as completed
 	kms.Metadata.CompletedAt = time.Now().UTC()
 
 	return kms, nil
 }
+
+// computeFuncs returns the set of compute functions to run for the window,
+// in struct field order. If cluster_info is not yet reporting the
+// complete_kubemodel label for any cluster in the result, the source has not
+// been upgraded to emit a full kubemodel, so only the minimal set of
+// resources (cluster, namespaces, resource quotas) is computed. The
+// FORCE_KUBEMODEL_V1 env var skips the check entirely and always returns the
+// minimal set.
+func (km *KubeModel) computeFuncs(start, end time.Time) []computeFunc {
+	KubeModelV1ComputeFucs := []computeFunc{
+		km.computeCluster,
+		km.computeNamespaces,
+		km.computeResourceQuotas,
+	}
+
+	if km.forceV1 {
+		return KubeModelV1ComputeFucs
+	}
+
+	results, err := km.ds.Metrics().QueryClusterKubeModelVersion(start, end).Await()
+	if err != nil {
+		log.Errorf("computeFuncs: querying cluster complete kubemodel: %s", err)
+		return KubeModelV1ComputeFucs
+	}
+
+	// If the window contains a result which is missing the version number then the window will produce incomplete KubeModel data
+	// and should not export
+	for _, res := range results {
+		if res.Version == "" {
+			return KubeModelV1ComputeFucs
+		}
+	}
+
+	return []computeFunc{
+		km.computeCluster,
+		km.computeNamespaces,
+		km.computeResourceQuotas,
+		km.computeServices,
+		km.computeDeployments,
+		km.computeStatefulSets,
+		km.computeDaemonSets,
+		km.computeJobs,
+		km.computeCronJobs,
+		km.computeReplicaSets,
+		km.computeNodes,
+		km.computePersistentVolumes,
+		km.computePersistentVolumeClaims,
+		km.computePods,
+		km.computeContainers,
+		km.computeDCGMDevices,
+	}
+}

+ 213 - 0
core/pkg/compute/kubemodel/kubemodel_test.go

@@ -0,0 +1,213 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+const testClusterUID = "cluster-uid-1"
+
+func newTestWindow() (time.Time, time.Time) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	return start, start.Add(time.Hour)
+}
+
+// seedCluster sets the minimum overrides needed for computeCluster to succeed.
+func seedCluster(ds *source.MockOpenCostDataSource, start, end time.Time) {
+	ds.Querier.SetOverride(source.QueryClusterInfo, []*source.ClusterInfoResult{
+		{UID: testClusterUID, Cluster: "my-cluster", Provider: "aws"},
+	})
+	ds.Querier.SetOverride(source.QueryClusterUptime, []*source.UptimeResult{
+		{UID: testClusterUID, First: start, Last: end},
+	})
+}
+
+// ---- NewKubeModel ----
+
+func TestNewKubeModel_NilDataSource(t *testing.T) {
+	_, err := NewKubeModel(testClusterUID, false, nil)
+	require.Error(t, err)
+}
+
+// ---- ComputeKubeModelSet orchestration ----
+
+func TestComputeKubeModelSet(t *testing.T) {
+	start, end := newTestWindow()
+
+	tests := []struct {
+		name    string
+		setup   func(ds *source.MockOpenCostDataSource)
+		wantErr bool
+		check   func(t *testing.T, kms *kubemodel.KubeModelSet)
+	}{
+		{
+			name: "cluster UID not found returns error",
+			setup: func(ds *source.MockOpenCostDataSource) {
+				ds.Querier.SetOverride(source.QueryClusterInfo, []*source.ClusterInfoResult{
+					{UID: "wrong-uid", Cluster: "other"},
+				})
+			},
+			wantErr: true,
+		},
+		{
+			name: "cluster uptime missing: cluster nil but other compute functions still run",
+			setup: func(ds *source.MockOpenCostDataSource) {
+				// cluster info present but no uptime — cluster won't register, but no error
+				ds.Querier.SetOverride(source.QueryClusterInfo, []*source.ClusterInfoResult{
+					{UID: testClusterUID, Cluster: "my-cluster"},
+				})
+				ds.Querier.SetOverride(source.QueryNamespaceInfo, []*source.NamespaceInfoResult{
+					{UID: "ns-1", Namespace: "default"},
+				})
+				ds.Querier.SetOverride(source.QueryNamespaceUptime, []*source.UptimeResult{
+					{UID: "ns-1", First: start, Last: end},
+				})
+			},
+			check: func(t *testing.T, kms *kubemodel.KubeModelSet) {
+				assert.Nil(t, kms.Cluster, "cluster with no uptime should not be registered")
+				assert.NotEmpty(t, kms.Namespaces, "namespace compute should still run after cluster registration fails")
+			},
+		},
+		{
+			name: "all compute functions produce results when given data",
+			setup: func(ds *source.MockOpenCostDataSource) {
+				seedCluster(ds, start, end)
+				ds.Querier.SetOverride(source.QueryNodeInfo, []*source.NodeInfoResult{
+					{UID: "node-1", Node: "node-a"},
+				})
+				ds.Querier.SetOverride(source.QueryNodeUptime, []*source.UptimeResult{
+					{UID: "node-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryNamespaceInfo, []*source.NamespaceInfoResult{
+					{UID: "ns-1", Namespace: "default"},
+				})
+				ds.Querier.SetOverride(source.QueryNamespaceUptime, []*source.UptimeResult{
+					{UID: "ns-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryPodInfo, []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod", NamespaceUID: "ns-1"},
+				})
+				ds.Querier.SetOverride(source.QueryPodUptime, []*source.UptimeResult{
+					{UID: "pod-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryContainerUptime, []*source.ContainerUptimeResult{
+					{UptimeResult: source.UptimeResult{UID: "pod-1", First: start, Last: end}, Container: "app"},
+				})
+				ds.Querier.SetOverride(source.QueryDeploymentInfo, []*source.DeploymentInfoResult{
+					{UID: "dep-1", Deployment: "my-dep", NamespaceUID: "ns-1"},
+				})
+				ds.Querier.SetOverride(source.QueryDeploymentUptime, []*source.UptimeResult{
+					{UID: "dep-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryStatefulSetInfo, []*source.StatefulSetInfoResult{
+					{UID: "sts-1", StatefulSet: "my-sts", NamespaceUID: "ns-1"},
+				})
+				ds.Querier.SetOverride(source.QueryStatefulSetUptime, []*source.UptimeResult{
+					{UID: "sts-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryDaemonSetInfo, []*source.DaemonSetInfoResult{
+					{UID: "ds-1", DaemonSet: "my-ds", NamespaceUID: "ns-1"},
+				})
+				ds.Querier.SetOverride(source.QueryDaemonSetUptime, []*source.UptimeResult{
+					{UID: "ds-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryJobInfo, []*source.JobInfoResult{
+					{UID: "job-1", Job: "my-job", NamespaceUID: "ns-1"},
+				})
+				ds.Querier.SetOverride(source.QueryJobUptime, []*source.UptimeResult{
+					{UID: "job-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryCronJobInfo, []*source.CronJobInfoResult{
+					{UID: "cj-1", CronJob: "my-cj", NamespaceUID: "ns-1"},
+				})
+				ds.Querier.SetOverride(source.QueryCronJobUptime, []*source.UptimeResult{
+					{UID: "cj-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryReplicaSetInfo, []*source.ReplicaSetInfoResult{
+					{UID: "rs-1", ReplicaSet: "my-rs", NamespaceUID: "ns-1"},
+				})
+				ds.Querier.SetOverride(source.QueryReplicaSetUptime, []*source.UptimeResult{
+					{UID: "rs-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryResourceQuotaInfo, []*source.ResourceQuotaInfoResult{
+					{UID: "rq-1", ResourceQuota: "default-quota", NamespaceUID: "ns-1"},
+				})
+				ds.Querier.SetOverride(source.QueryResourceQuotaUptime, []*source.UptimeResult{
+					{UID: "rq-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryServiceInfo, []*source.ServiceInfoResult{
+					{UID: "svc-1", Service: "my-svc", NamespaceUID: "ns-1"},
+				})
+				ds.Querier.SetOverride(source.QueryServiceUptime, []*source.UptimeResult{
+					{UID: "svc-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryKMPVInfo, []*source.PVInfoResult{
+					{UID: "pv-1", PersistentVolume: "my-pv"},
+				})
+				ds.Querier.SetOverride(source.QueryPVUptime, []*source.UptimeResult{
+					{UID: "pv-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryPVBytes, []*source.PVBytesResult{
+					{UID: "pv-1", Value: 10 * 1024 * 1024 * 1024},
+				})
+				ds.Querier.SetOverride(source.QueryKMPVCInfo, []*source.PVCInfoResult{
+					{UID: "pvc-1", PersistentVolumeClaim: "data-claim", NamespaceUID: "ns-1"},
+				})
+				ds.Querier.SetOverride(source.QueryPVCUptime, []*source.UptimeResult{
+					{UID: "pvc-1", First: start, Last: end},
+				})
+				ds.Querier.SetOverride(source.QueryDCGMDeviceInfo, []*source.DCGMDeviceInfoResult{
+					{UUID: "GPU-abc123", Device: "nvidia0", ModelName: "A100"},
+				})
+				ds.Querier.SetOverride(source.QueryDCGMDeviceUptime, []*source.DCGMDeviceUptimeResult{
+					{UUID: "GPU-abc123", First: start, Last: end},
+				})
+			},
+			check: func(t *testing.T, kms *kubemodel.KubeModelSet) {
+				assert.NotNil(t, kms.Cluster)
+				assert.NotEmpty(t, kms.Nodes)
+				assert.NotEmpty(t, kms.Namespaces)
+				assert.NotEmpty(t, kms.Pods)
+				assert.NotEmpty(t, kms.Containers)
+				assert.NotEmpty(t, kms.Deployments)
+				assert.NotEmpty(t, kms.StatefulSets)
+				assert.NotEmpty(t, kms.DaemonSets)
+				assert.NotEmpty(t, kms.Jobs)
+				assert.NotEmpty(t, kms.CronJobs)
+				assert.NotEmpty(t, kms.ReplicaSets)
+				assert.NotEmpty(t, kms.ResourceQuotas)
+				assert.NotEmpty(t, kms.Services)
+				assert.NotEmpty(t, kms.PersistentVolumes)
+				assert.NotEmpty(t, kms.PersistentVolumeClaims)
+				assert.NotEmpty(t, kms.DCGMDevices)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			tt.setup(ds)
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			if tt.check != nil {
+				tt.check(t, kms)
+			}
+		})
+	}
+}

+ 154 - 0
core/pkg/compute/kubemodel/namespace_test.go

@@ -0,0 +1,154 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeNamespaces(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.Namespace
+	}{
+		{
+			name:      "no data returns empty namespace map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.Namespace{},
+		},
+		{
+			name: "basic namespace info and uptime",
+			overrides: map[string]any{
+				source.QueryNamespaceInfo: []*source.NamespaceInfoResult{
+					{UID: "ns-1", Namespace: "default"},
+				},
+				source.QueryNamespaceUptime: []*source.UptimeResult{
+					{UID: "ns-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Namespace{
+				"ns-1": {
+					UID:   "ns-1",
+					Name:  "default",
+					Start: start,
+					End:   end,
+				},
+			},
+		},
+		{
+			name: "namespace without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryNamespaceInfo: []*source.NamespaceInfoResult{
+					{UID: "ns-1", Namespace: "default"},
+				},
+			},
+			want: map[string]*kubemodel.Namespace{},
+		},
+		{
+			name: "namespace labels are attached",
+			overrides: map[string]any{
+				source.QueryNamespaceInfo: []*source.NamespaceInfoResult{
+					{UID: "ns-1", Namespace: "production"},
+				},
+				source.QueryNamespaceUptime: []*source.UptimeResult{
+					{UID: "ns-1", First: start, Last: end},
+				},
+				source.QueryNamespaceLabels: []*source.NamespaceLabelsResult{
+					{UID: "ns-1", Labels: map[string]string{"env": "prod", "team": "platform"}},
+				},
+			},
+			want: map[string]*kubemodel.Namespace{
+				"ns-1": {
+					UID:    "ns-1",
+					Name:   "production",
+					Start:  start,
+					End:    end,
+					Labels: map[string]string{"env": "prod", "team": "platform"},
+				},
+			},
+		},
+		{
+			name: "namespace annotations are attached",
+			overrides: map[string]any{
+				source.QueryNamespaceInfo: []*source.NamespaceInfoResult{
+					{UID: "ns-1", Namespace: "staging"},
+				},
+				source.QueryNamespaceUptime: []*source.UptimeResult{
+					{UID: "ns-1", First: start, Last: end},
+				},
+				source.QueryNamespaceAnnotations: []*source.NamespaceAnnotationsResult{
+					{UID: "ns-1", Annotations: map[string]string{"owner": "team-a"}},
+				},
+			},
+			want: map[string]*kubemodel.Namespace{
+				"ns-1": {
+					UID:         "ns-1",
+					Name:        "staging",
+					Start:       start,
+					End:         end,
+					Annotations: map[string]string{"owner": "team-a"},
+				},
+			},
+		},
+		{
+			name: "multiple namespaces are registered",
+			overrides: map[string]any{
+				source.QueryNamespaceInfo: []*source.NamespaceInfoResult{
+					{UID: "ns-1", Namespace: "default"},
+					{UID: "ns-2", Namespace: "kube-system"},
+				},
+				source.QueryNamespaceUptime: []*source.UptimeResult{
+					{UID: "ns-1", First: start, Last: end},
+					{UID: "ns-2", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Namespace{
+				"ns-1": {UID: "ns-1", Name: "default", Start: start, End: end},
+				"ns-2": {UID: "ns-2", Name: "kube-system", Start: start, End: end},
+			},
+		},
+		{
+			name: "uptime for unknown namespace is ignored",
+			overrides: map[string]any{
+				source.QueryNamespaceInfo: []*source.NamespaceInfoResult{
+					{UID: "ns-1", Namespace: "default"},
+				},
+				source.QueryNamespaceUptime: []*source.UptimeResult{
+					{UID: "ns-1", First: start, Last: end},
+					{UID: "unknown-ns", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Namespace{
+				"ns-1": {UID: "ns-1", Name: "default", Start: start, End: end},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.Namespaces)
+		})
+	}
+}

+ 325 - 0
core/pkg/compute/kubemodel/node_test.go

@@ -0,0 +1,325 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeNodes(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.Node
+	}{
+		{
+			name:      "no data returns empty node map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.Node{},
+		},
+		{
+			name: "basic node info and uptime",
+			overrides: map[string]any{
+				source.QueryNodeInfo: []*source.NodeInfoResult{
+					{UID: "node-1", Node: "node-a", ProviderID: "aws:///us-east-1a/i-abc"},
+					{UID: "node-2", Node: "node-b", ProviderID: "aws:///us-east-1b/i-def"},
+				},
+				source.QueryNodeUptime: []*source.UptimeResult{
+					{UID: "node-1", First: start, Last: end},
+					{UID: "node-2", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Node{
+				"node-1": {
+					UID: "node-1", Name: "node-a", ProviderID: "aws:///us-east-1a/i-abc",
+					Start:                start,
+					End:                  end,
+					ResourceCapacities:   kubemodel.ResourceQuantities{},
+					ResourcesAllocatable: kubemodel.ResourceQuantities{},
+				},
+				"node-2": {
+					UID: "node-2", Name: "node-b", ProviderID: "aws:///us-east-1b/i-def",
+					Start:                start,
+					End:                  end,
+					ResourceCapacities:   kubemodel.ResourceQuantities{},
+					ResourcesAllocatable: kubemodel.ResourceQuantities{},
+				},
+			},
+		},
+		{
+			name: "node without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryNodeInfo: []*source.NodeInfoResult{
+					{UID: "node-1", Node: "node-a"},
+				},
+				// QueryNodeUptime intentionally absent
+			},
+			want: map[string]*kubemodel.Node{},
+		},
+		{
+			name: "labels are attached to node",
+			overrides: map[string]any{
+				source.QueryNodeInfo: []*source.NodeInfoResult{
+					{UID: "node-1", Node: "node-a"},
+				},
+				source.QueryNodeUptime: []*source.UptimeResult{
+					{UID: "node-1", First: start, Last: end},
+				},
+				source.QueryNodeLabels: []*source.NodeLabelsResult{
+					{UID: "node-1", Labels: map[string]string{"zone": "us-east-1a", "role": "worker"}},
+				},
+			},
+			want: map[string]*kubemodel.Node{
+				"node-1": {
+					UID: "node-1", Name: "node-a",
+					Start:                start,
+					End:                  end,
+					Labels:               map[string]string{"zone": "us-east-1a", "role": "worker"},
+					ResourceCapacities:   kubemodel.ResourceQuantities{},
+					ResourcesAllocatable: kubemodel.ResourceQuantities{},
+				},
+			},
+		},
+		{
+			name: "resource capacities and allocatable are populated",
+			overrides: map[string]any{
+				source.QueryNodeInfo: []*source.NodeInfoResult{
+					{UID: "node-1", Node: "node-a"},
+				},
+				source.QueryNodeUptime: []*source.UptimeResult{
+					{UID: "node-1", First: start, Last: end},
+				},
+				source.QueryNodeResourceCapacities: []*source.ResourceResult{
+					{UID: "node-1", Resource: "cpu", Unit: "cores", Value: 4.0},
+					{UID: "node-1", Resource: "memory", Unit: "bytes", Value: 8 * 1024 * 1024 * 1024},
+				},
+				source.QueryNodeResourcesAllocatable: []*source.ResourceResult{
+					{UID: "node-1", Resource: "cpu", Unit: "cores", Value: 3.9},
+					{UID: "node-1", Resource: "memory", Unit: "bytes", Value: 7 * 1024 * 1024 * 1024},
+				},
+			},
+			want: map[string]*kubemodel.Node{
+				"node-1": {
+					UID: "node-1", Name: "node-a",
+					Start: start,
+					End:   end,
+					ResourceCapacities: kubemodel.ResourceQuantities{
+						kubemodel.ResourceCPU: {
+							Resource: kubemodel.ResourceCPU,
+							Unit:     kubemodel.UnitCore,
+							Values:   kubemodel.Stats{kubemodel.StatAvg: 4.0},
+						},
+						kubemodel.ResourceMemory: {
+							Resource: kubemodel.ResourceMemory,
+							Unit:     kubemodel.UnitByte,
+							Values:   kubemodel.Stats{kubemodel.StatAvg: 8 * 1024 * 1024 * 1024},
+						},
+					},
+					ResourcesAllocatable: kubemodel.ResourceQuantities{
+						kubemodel.ResourceCPU: {
+							Resource: kubemodel.ResourceCPU,
+							Unit:     kubemodel.UnitCore,
+							Values:   kubemodel.Stats{kubemodel.StatAvg: 3.9},
+						},
+						kubemodel.ResourceMemory: {
+							Resource: kubemodel.ResourceMemory,
+							Unit:     kubemodel.UnitByte,
+							Values:   kubemodel.Stats{kubemodel.StatAvg: 7 * 1024 * 1024 * 1024},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "local storage bytes are populated",
+			overrides: map[string]any{
+				source.QueryNodeInfo: []*source.NodeInfoResult{
+					{UID: "node-1", Node: "node-a"},
+				},
+				source.QueryNodeUptime: []*source.UptimeResult{
+					{UID: "node-1", First: start, Last: end},
+				},
+				source.QueryKMLocalStorageBytes: []*source.UIDValueResult{
+					{UID: "node-1", Value: 500 * 1024 * 1024 * 1024},
+				},
+				source.QueryKMLocalStorageUsedAvg: []*source.NodeUIDValueResult{
+					{UID: "node-1", Value: 100 * 1024 * 1024 * 1024},
+				},
+				source.QueryKMLocalStorageUsedMax: []*source.NodeUIDValueResult{
+					{UID: "node-1", Value: 200 * 1024 * 1024 * 1024},
+				},
+			},
+			want: map[string]*kubemodel.Node{
+				"node-1": {
+					UID: "node-1", Name: "node-a",
+					Start: start,
+					End:   end,
+					FileSystem: kubemodel.FileSystem{
+						CapacityBytes: 500 * 1024 * 1024 * 1024,
+						UsageByteAvg:  100 * 1024 * 1024 * 1024,
+						UsageByteMax:  200 * 1024 * 1024 * 1024,
+					},
+					ResourceCapacities:   kubemodel.ResourceQuantities{},
+					ResourcesAllocatable: kubemodel.ResourceQuantities{},
+				},
+			},
+		},
+		{
+			name: "uptime for unknown node is ignored",
+			overrides: map[string]any{
+				source.QueryNodeInfo: []*source.NodeInfoResult{
+					{UID: "node-1", Node: "node-a"},
+				},
+				source.QueryNodeUptime: []*source.UptimeResult{
+					{UID: "node-1", First: start, Last: end},
+					{UID: "unknown-node", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Node{
+				"node-1": {
+					UID: "node-1", Name: "node-a",
+					Start:                start,
+					End:                  end,
+					ResourceCapacities:   kubemodel.ResourceQuantities{},
+					ResourcesAllocatable: kubemodel.ResourceQuantities{},
+				},
+			},
+		},
+		{
+			name: "local storage for unknown node is ignored",
+			overrides: map[string]any{
+				source.QueryNodeInfo: []*source.NodeInfoResult{
+					{UID: "node-1", Node: "node-a"},
+				},
+				source.QueryNodeUptime: []*source.UptimeResult{
+					{UID: "node-1", First: start, Last: end},
+				},
+				source.QueryKMLocalStorageBytes: []*source.UIDValueResult{
+					{UID: "unknown-node", Value: 999},
+				},
+			},
+			want: map[string]*kubemodel.Node{
+				"node-1": {
+					UID: "node-1", Name: "node-a",
+					Start:                start,
+					End:                  end,
+					ResourceCapacities:   kubemodel.ResourceQuantities{},
+					ResourcesAllocatable: kubemodel.ResourceQuantities{},
+				},
+			},
+		},
+		{
+			name: "resource capacities for unknown node are ignored",
+			overrides: map[string]any{
+				source.QueryNodeInfo: []*source.NodeInfoResult{
+					{UID: "node-1", Node: "node-a"},
+				},
+				source.QueryNodeUptime: []*source.UptimeResult{
+					{UID: "node-1", First: start, Last: end},
+				},
+				source.QueryNodeResourceCapacities: []*source.ResourceResult{
+					{UID: "unknown-node", Resource: "cpu", Unit: "cores", Value: 4.0},
+				},
+			},
+			want: map[string]*kubemodel.Node{
+				"node-1": {
+					UID: "node-1", Name: "node-a",
+					Start:                start,
+					End:                  end,
+					ResourceCapacities:   kubemodel.ResourceQuantities{},
+					ResourcesAllocatable: kubemodel.ResourceQuantities{},
+				},
+			},
+		},
+		{
+			name: "cpu usage data is populated via resource capacities",
+			overrides: map[string]any{
+				source.QueryNodeInfo: []*source.NodeInfoResult{
+					{UID: "node-1", Node: "node-a"},
+					{UID: "node-2", Node: "node-b"},
+				},
+				source.QueryNodeUptime: []*source.UptimeResult{
+					{UID: "node-1", First: start, Last: end},
+					{UID: "node-2", First: start, Last: end},
+				},
+				source.QueryKMLocalStorageUsedAvg: []*source.NodeUIDValueResult{
+					{UID: "node-1", Value: 50 * 1024 * 1024 * 1024},
+				},
+				source.QueryKMLocalStorageUsedMax: []*source.NodeUIDValueResult{
+					{UID: "node-2", Value: 75 * 1024 * 1024 * 1024},
+				},
+			},
+			want: map[string]*kubemodel.Node{
+				"node-1": {
+					UID: "node-1", Name: "node-a",
+					Start: start, End: end,
+					FileSystem:           kubemodel.FileSystem{UsageByteAvg: 50 * 1024 * 1024 * 1024},
+					ResourceCapacities:   kubemodel.ResourceQuantities{},
+					ResourcesAllocatable: kubemodel.ResourceQuantities{},
+				},
+				"node-2": {
+					UID: "node-2", Name: "node-b",
+					Start: start, End: end,
+					FileSystem:           kubemodel.FileSystem{UsageByteMax: 75 * 1024 * 1024 * 1024},
+					ResourceCapacities:   kubemodel.ResourceQuantities{},
+					ResourcesAllocatable: kubemodel.ResourceQuantities{},
+				},
+			},
+		},
+		{
+			name: "gpu count via resource capacities",
+			overrides: map[string]any{
+				source.QueryNodeInfo: []*source.NodeInfoResult{
+					{UID: "node-1", Node: "gpu-node"},
+				},
+				source.QueryNodeUptime: []*source.UptimeResult{
+					{UID: "node-1", First: start, Last: end},
+				},
+				source.QueryNodeResourceCapacities: []*source.ResourceResult{
+					{UID: "node-1", Resource: "nvidia.com/gpu", Unit: "count", Value: 8},
+				},
+			},
+			want: map[string]*kubemodel.Node{
+				"node-1": {
+					UID: "node-1", Name: "gpu-node",
+					Start: start, End: end,
+					ResourceCapacities: kubemodel.ResourceQuantities{
+						kubemodel.ResourceNvidia: {
+							Resource: kubemodel.ResourceNvidia,
+							Unit:     "count",
+							Values:   kubemodel.Stats{kubemodel.StatAvg: 8},
+						},
+					},
+					ResourcesAllocatable: kubemodel.ResourceQuantities{},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.Nodes)
+		})
+	}
+}

+ 142 - 0
core/pkg/compute/kubemodel/persistentvolume_test.go

@@ -0,0 +1,142 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputePersistentVolumes(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.PersistentVolume
+	}{
+		{
+			name:      "no data returns empty pv map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.PersistentVolume{},
+		},
+		{
+			name: "basic pv info and uptime",
+			overrides: map[string]any{
+				source.QueryKMPVInfo: []*source.PVInfoResult{
+					{UID: "pv-1", PersistentVolume: "pvc-data-0"},
+				},
+				source.QueryPVUptime: []*source.UptimeResult{
+					{UID: "pv-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.PersistentVolume{
+				"pv-1": {
+					UID:   "pv-1",
+					Name:  "pvc-data-0",
+					Start: start,
+					End:   end,
+				},
+			},
+		},
+		{
+			name: "pv without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryKMPVInfo: []*source.PVInfoResult{
+					{UID: "pv-1", PersistentVolume: "pvc-data-0"},
+				},
+			},
+			want: map[string]*kubemodel.PersistentVolume{},
+		},
+		{
+			name: "pv with storage class and csi volume handle",
+			overrides: map[string]any{
+				source.QueryKMPVInfo: []*source.PVInfoResult{
+					{UID: "pv-1", PersistentVolume: "pvc-data-0", StorageClass: "gp2", CSIVolumeHandle: "vol-abc123"},
+				},
+				source.QueryPVUptime: []*source.UptimeResult{
+					{UID: "pv-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.PersistentVolume{
+				"pv-1": {
+					UID:             "pv-1",
+					Name:            "pvc-data-0",
+					StorageClass:    "gp2",
+					CSIVolumeHandle: "vol-abc123",
+					Start:           start,
+					End:             end,
+				},
+			},
+		},
+		{
+			name: "pv size bytes is populated",
+			overrides: map[string]any{
+				source.QueryKMPVInfo: []*source.PVInfoResult{
+					{UID: "pv-1", PersistentVolume: "pvc-data-0"},
+				},
+				source.QueryPVUptime: []*source.UptimeResult{
+					{UID: "pv-1", First: start, Last: end},
+				},
+				source.QueryPVBytes: []*source.PVBytesResult{
+					{UID: "pv-1", Value: 10 * 1024 * 1024 * 1024},
+				},
+			},
+			want: map[string]*kubemodel.PersistentVolume{
+				"pv-1": {
+					UID:       "pv-1",
+					Name:      "pvc-data-0",
+					Start:     start,
+					End:       end,
+					SizeBytes: 10 * 1024 * 1024 * 1024,
+				},
+			},
+		},
+		{
+			name: "pv bytes for unknown pv is ignored",
+			overrides: map[string]any{
+				source.QueryKMPVInfo: []*source.PVInfoResult{
+					{UID: "pv-1", PersistentVolume: "pvc-data-0"},
+				},
+				source.QueryPVUptime: []*source.UptimeResult{
+					{UID: "pv-1", First: start, Last: end},
+				},
+				source.QueryPVBytes: []*source.PVBytesResult{
+					{UID: "unknown-pv", Value: 999},
+				},
+			},
+			want: map[string]*kubemodel.PersistentVolume{
+				"pv-1": {
+					UID:   "pv-1",
+					Name:  "pvc-data-0",
+					Start: start,
+					End:   end,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.PersistentVolumes)
+		})
+	}
+}

+ 155 - 0
core/pkg/compute/kubemodel/persistentvolumeclaim_test.go

@@ -0,0 +1,155 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+	"github.com/opencost/opencost/core/pkg/util"
+)
+
+func TestComputePersistentVolumeClaims(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.PersistentVolumeClaim
+	}{
+		{
+			name:      "no data returns empty pvc map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.PersistentVolumeClaim{},
+		},
+		{
+			name: "basic pvc info and uptime",
+			overrides: map[string]any{
+				source.QueryKMPVCInfo: []*source.PVCInfoResult{
+					{UID: "pvc-1", PersistentVolumeClaim: "data-claim", NamespaceUID: "ns-1"},
+				},
+				source.QueryPVCUptime: []*source.UptimeResult{
+					{UID: "pvc-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.PersistentVolumeClaim{
+				"pvc-1": {
+					UID:          "pvc-1",
+					Name:         "data-claim",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "pvc without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryKMPVCInfo: []*source.PVCInfoResult{
+					{UID: "pvc-1", PersistentVolumeClaim: "data-claim", NamespaceUID: "ns-1"},
+				},
+			},
+			want: map[string]*kubemodel.PersistentVolumeClaim{},
+		},
+		{
+			name: "pvc with pv uid and storage class",
+			overrides: map[string]any{
+				source.QueryKMPVCInfo: []*source.PVCInfoResult{
+					{UID: "pvc-1", PersistentVolumeClaim: "data-claim", NamespaceUID: "ns-1", PVUID: "pv-1", StorageClass: "gp2"},
+				},
+				source.QueryPVCUptime: []*source.UptimeResult{
+					{UID: "pvc-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.PersistentVolumeClaim{
+				"pvc-1": {
+					UID:                 "pvc-1",
+					Name:                "data-claim",
+					NamespaceUID:        "ns-1",
+					PersistentVolumeUID: "pv-1",
+					StorageClass:        "gp2",
+					Start:               start,
+					End:                 end,
+				},
+			},
+		},
+		{
+			name: "pvc requested bytes and usage are populated",
+			overrides: map[string]any{
+				source.QueryKMPVCInfo: []*source.PVCInfoResult{
+					{UID: "pvc-1", PersistentVolumeClaim: "data-claim", NamespaceUID: "ns-1"},
+				},
+				source.QueryPVCUptime: []*source.UptimeResult{
+					{UID: "pvc-1", First: start, Last: end},
+				},
+				source.QueryPVCBytesRequested: []*source.PVCBytesRequestedResult{
+					{UID: "pvc-1", Data: []*util.Vector{{Value: 5 * 1024 * 1024 * 1024}}},
+				},
+				source.QueryPVCBytesUsedAverage: []*source.PVCUIDValueResult{
+					{UID: "pvc-1", Value: 2 * 1024 * 1024 * 1024},
+				},
+				source.QueryPVCBytesUsedMax: []*source.PVCUIDValueResult{
+					{UID: "pvc-1", Value: 4 * 1024 * 1024 * 1024},
+				},
+			},
+			want: map[string]*kubemodel.PersistentVolumeClaim{
+				"pvc-1": {
+					UID:            "pvc-1",
+					Name:           "data-claim",
+					NamespaceUID:   "ns-1",
+					Start:          start,
+					End:            end,
+					RequestedBytes: 5 * 1024 * 1024 * 1024,
+					UsageBytesAvg:  2 * 1024 * 1024 * 1024,
+					UsageBytesMax:  4 * 1024 * 1024 * 1024,
+				},
+			},
+		},
+		{
+			name: "bytes for unknown pvc are ignored",
+			overrides: map[string]any{
+				source.QueryKMPVCInfo: []*source.PVCInfoResult{
+					{UID: "pvc-1", PersistentVolumeClaim: "data-claim", NamespaceUID: "ns-1"},
+				},
+				source.QueryPVCUptime: []*source.UptimeResult{
+					{UID: "pvc-1", First: start, Last: end},
+				},
+				source.QueryPVCBytesUsedAverage: []*source.PVCUIDValueResult{
+					{UID: "unknown-pvc", Value: 999},
+				},
+			},
+			want: map[string]*kubemodel.PersistentVolumeClaim{
+				"pvc-1": {
+					UID:          "pvc-1",
+					Name:         "data-claim",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.PersistentVolumeClaims)
+		})
+	}
+}

+ 309 - 0
core/pkg/compute/kubemodel/pod_test.go

@@ -0,0 +1,309 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputePods(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.Pod
+	}{
+		{
+			name:      "no data returns empty pod map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.Pod{},
+		},
+		{
+			name: "basic pod info and uptime",
+			overrides: map[string]any{
+				source.QueryPodInfo: []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod", NamespaceUID: "ns-1"},
+				},
+				source.QueryPodUptime: []*source.UptimeResult{
+					{UID: "pod-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Pod{
+				"pod-1": {
+					UID:          "pod-1",
+					Name:         "my-pod",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "pod without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryPodInfo: []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod", NamespaceUID: "ns-1"},
+				},
+			},
+			want: map[string]*kubemodel.Pod{},
+		},
+		{
+			name: "pod without namespace uid is not registered",
+			overrides: map[string]any{
+				source.QueryPodInfo: []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod"},
+				},
+				source.QueryPodUptime: []*source.UptimeResult{
+					{UID: "pod-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Pod{},
+		},
+		{
+			name: "pod with node uid",
+			overrides: map[string]any{
+				source.QueryPodInfo: []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod", NamespaceUID: "ns-1", NodeUID: "node-1"},
+				},
+				source.QueryPodUptime: []*source.UptimeResult{
+					{UID: "pod-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Pod{
+				"pod-1": {
+					UID:          "pod-1",
+					Name:         "my-pod",
+					NamespaceUID: "ns-1",
+					NodeUID:      "node-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "pod owners are attached",
+			overrides: map[string]any{
+				source.QueryPodInfo: []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod", NamespaceUID: "ns-1"},
+				},
+				source.QueryPodUptime: []*source.UptimeResult{
+					{UID: "pod-1", First: start, Last: end},
+				},
+				source.QueryPodOwners: []*source.OwnerResult{
+					{UID: "pod-1", OwnerUID: "rs-1", OwnerKind: "ReplicaSet", Controller: true},
+				},
+			},
+			want: map[string]*kubemodel.Pod{
+				"pod-1": {
+					UID:          "pod-1",
+					Name:         "my-pod",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Owners: []kubemodel.Owner{
+						{UID: "rs-1", Kind: kubemodel.OwnerKindReplicaSet, Controller: true},
+					},
+				},
+			},
+		},
+		{
+			name: "pod pvc volumes are attached",
+			overrides: map[string]any{
+				source.QueryPodInfo: []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod", NamespaceUID: "ns-1"},
+				},
+				source.QueryPodUptime: []*source.UptimeResult{
+					{UID: "pod-1", First: start, Last: end},
+				},
+				source.QueryPodPVCVolumes: []*source.PodPVCVolumeResult{
+					{UID: "pod-1", PVCUID: "pvc-1", PodVolumeName: "data"},
+				},
+			},
+			want: map[string]*kubemodel.Pod{
+				"pod-1": {
+					UID:          "pod-1",
+					Name:         "my-pod",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					PVCVolumes: []kubemodel.PodPVCVolume{
+						{Name: "data", PersistentVolumeClaimUID: "pvc-1"},
+					},
+				},
+			},
+		},
+		{
+			name: "pod labels and annotations are attached",
+			overrides: map[string]any{
+				source.QueryPodInfo: []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod", NamespaceUID: "ns-1"},
+				},
+				source.QueryPodUptime: []*source.UptimeResult{
+					{UID: "pod-1", First: start, Last: end},
+				},
+				source.QueryPodLabels: []*source.PodLabelsResult{
+					{UID: "pod-1", Labels: map[string]string{"app": "web"}},
+				},
+				source.QueryPodAnnotations: []*source.PodAnnotationsResult{
+					{UID: "pod-1", Annotations: map[string]string{"team": "platform"}},
+				},
+			},
+			want: map[string]*kubemodel.Pod{
+				"pod-1": {
+					UID:          "pod-1",
+					Name:         "my-pod",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Labels:       map[string]string{"app": "web"},
+					Annotations:  map[string]string{"team": "platform"},
+				},
+			},
+		},
+		{
+			name: "uptime for unknown pod is ignored",
+			overrides: map[string]any{
+				source.QueryPodInfo: []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod", NamespaceUID: "ns-1"},
+				},
+				source.QueryPodUptime: []*source.UptimeResult{
+					{UID: "pod-1", First: start, Last: end},
+					{UID: "unknown-pod", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Pod{
+				"pod-1": {
+					UID:          "pod-1",
+					Name:         "my-pod",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "network egress: internet, cross-region, cross-zone traffic types",
+			overrides: map[string]any{
+				source.QueryPodInfo: []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod", NamespaceUID: "ns-1"},
+				},
+				source.QueryPodUptime: []*source.UptimeResult{
+					{UID: "pod-1", First: start, Last: end},
+				},
+				source.QueryPodNetworkEgressBytes: []*source.PodNetworkBytesResult{
+					{UID: "pod-1", Internet: true, Service: "svc-a", Value: 100},
+					{UID: "pod-1", Internet: false, SameRegion: false, Service: "svc-b", Value: 200},
+					{UID: "pod-1", Internet: false, SameRegion: true, SameZone: false, Service: "svc-c", Value: 300},
+					{UID: "pod-1", Internet: false, SameRegion: true, SameZone: true, Service: "svc-d", Value: 400}, // no traffic type → skipped
+					{UID: "pod-1", Internet: true, Service: "svc-e", Value: 0},                                      // zero bytes → skipped
+					{UID: "unknown-pod", Internet: true, Service: "svc-f", Value: 500},                              // unknown pod → skipped
+				},
+			},
+			want: map[string]*kubemodel.Pod{
+				"pod-1": {
+					UID:          "pod-1",
+					Name:         "my-pod",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					NetworkTrafficDetails: []kubemodel.NetworkTrafficDetail{
+						{PodUID: "pod-1", TrafficDirection: kubemodel.TrafficDirectionEgress, TrafficType: kubemodel.TrafficTypeInternet, Endpoint: "svc-a", Bytes: 100},
+						{PodUID: "pod-1", TrafficDirection: kubemodel.TrafficDirectionEgress, TrafficType: kubemodel.TrafficTypeCrossRegion, Endpoint: "svc-b", Bytes: 200},
+						{PodUID: "pod-1", TrafficDirection: kubemodel.TrafficDirectionEgress, TrafficType: kubemodel.TrafficTypeCrossZone, Endpoint: "svc-c", Bytes: 300},
+					},
+				},
+			},
+		},
+		{
+			name: "unknown uid in supplementary data is ignored",
+			overrides: map[string]any{
+				source.QueryPodInfo: []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod", NamespaceUID: "ns-1"},
+				},
+				source.QueryPodUptime: []*source.UptimeResult{
+					{UID: "pod-1", First: start, Last: end},
+				},
+				// all of these carry an unknown UID — hit the !ok warn paths
+				source.QueryPodOwners: []*source.OwnerResult{
+					{UID: "unknown-pod", OwnerUID: "rs-1", OwnerKind: "ReplicaSet", Controller: true},
+				},
+				source.QueryPodPVCVolumes: []*source.PodPVCVolumeResult{
+					{UID: "unknown-pod", PVCUID: "pvc-1", PodVolumeName: "data"},
+				},
+				source.QueryPodLabels: []*source.PodLabelsResult{
+					{UID: "unknown-pod", Labels: map[string]string{"app": "web"}},
+				},
+				source.QueryPodAnnotations: []*source.PodAnnotationsResult{
+					{UID: "unknown-pod", Annotations: map[string]string{"team": "platform"}},
+				},
+				// same-zone+same-region ingress: triggers !ok continue in ingress loop
+				source.QueryPodNetworkIngressBytes: []*source.PodNetworkBytesResult{
+					{UID: "pod-1", Internet: false, SameRegion: true, SameZone: true, Value: 100},
+				},
+			},
+			want: map[string]*kubemodel.Pod{
+				"pod-1": {
+					UID:          "pod-1",
+					Name:         "my-pod",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "network ingress traffic is recorded",
+			overrides: map[string]any{
+				source.QueryPodInfo: []*source.PodInfoResult{
+					{UID: "pod-1", Pod: "my-pod", NamespaceUID: "ns-1"},
+				},
+				source.QueryPodUptime: []*source.UptimeResult{
+					{UID: "pod-1", First: start, Last: end},
+				},
+				source.QueryPodNetworkIngressBytes: []*source.PodNetworkBytesResult{
+					{UID: "pod-1", Internet: true, NatGateway: true, Service: "svc-a", Value: 512},
+					{UID: "pod-1", Internet: false, SameRegion: false, Service: "svc-b", Value: 256},
+				},
+			},
+			want: map[string]*kubemodel.Pod{
+				"pod-1": {
+					UID:          "pod-1",
+					Name:         "my-pod",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					NetworkTrafficDetails: []kubemodel.NetworkTrafficDetail{
+						{PodUID: "pod-1", TrafficDirection: kubemodel.TrafficDirectionIngress, TrafficType: kubemodel.TrafficTypeInternet, IsNatGateway: true, Endpoint: "svc-a", Bytes: 512},
+						{PodUID: "pod-1", TrafficDirection: kubemodel.TrafficDirectionIngress, TrafficType: kubemodel.TrafficTypeCrossRegion, Endpoint: "svc-b", Bytes: 256},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.Pods)
+		})
+	}
+}

+ 166 - 0
core/pkg/compute/kubemodel/replicaset_test.go

@@ -0,0 +1,166 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeReplicaSets(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.ReplicaSet
+	}{
+		{
+			name:      "no data returns empty replicaset map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.ReplicaSet{},
+		},
+		{
+			name: "basic replicaset info and uptime",
+			overrides: map[string]any{
+				source.QueryReplicaSetInfo: []*source.ReplicaSetInfoResult{
+					{UID: "rs-1", ReplicaSet: "my-app-v1", NamespaceUID: "ns-1"},
+				},
+				source.QueryReplicaSetUptime: []*source.UptimeResult{
+					{UID: "rs-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.ReplicaSet{
+				"rs-1": {
+					UID:          "rs-1",
+					Name:         "my-app-v1",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "replicaset without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryReplicaSetInfo: []*source.ReplicaSetInfoResult{
+					{UID: "rs-1", ReplicaSet: "my-app-v1", NamespaceUID: "ns-1"},
+				},
+			},
+			want: map[string]*kubemodel.ReplicaSet{},
+		},
+		{
+			name: "replicaset without namespace uid is not registered",
+			overrides: map[string]any{
+				source.QueryReplicaSetInfo: []*source.ReplicaSetInfoResult{
+					{UID: "rs-1", ReplicaSet: "my-app-v1"},
+				},
+				source.QueryReplicaSetUptime: []*source.UptimeResult{
+					{UID: "rs-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.ReplicaSet{},
+		},
+		{
+			name: "replicaset owner is attached",
+			overrides: map[string]any{
+				source.QueryReplicaSetInfo: []*source.ReplicaSetInfoResult{
+					{UID: "rs-1", ReplicaSet: "my-app-v1", NamespaceUID: "ns-1"},
+				},
+				source.QueryReplicaSetUptime: []*source.UptimeResult{
+					{UID: "rs-1", First: start, Last: end},
+				},
+				source.QueryReplicaSetOwners: []*source.OwnerResult{
+					{UID: "rs-1", OwnerUID: "dep-1", OwnerKind: "Deployment", Controller: true},
+				},
+			},
+			want: map[string]*kubemodel.ReplicaSet{
+				"rs-1": {
+					UID:          "rs-1",
+					Name:         "my-app-v1",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Owners: []kubemodel.Owner{
+						{UID: "dep-1", Kind: kubemodel.OwnerKindDeployment, Controller: true},
+					},
+				},
+			},
+		},
+		{
+			name: "replicaset labels and annotations are attached",
+			overrides: map[string]any{
+				source.QueryReplicaSetInfo: []*source.ReplicaSetInfoResult{
+					{UID: "rs-1", ReplicaSet: "my-app-v1", NamespaceUID: "ns-1"},
+				},
+				source.QueryReplicaSetUptime: []*source.UptimeResult{
+					{UID: "rs-1", First: start, Last: end},
+				},
+				source.QueryReplicaSetLabels: []*source.LabelsResult{
+					{UID: "rs-1", Labels: map[string]string{"app": "my-app", "version": "v1"}},
+				},
+				source.QueryReplicaSetAnnotations: []*source.AnnotationsResult{
+					{UID: "rs-1", Annotations: map[string]string{"rollout": "stable"}},
+				},
+			},
+			want: map[string]*kubemodel.ReplicaSet{
+				"rs-1": {
+					UID:          "rs-1",
+					Name:         "my-app-v1",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Labels:       map[string]string{"app": "my-app", "version": "v1"},
+					Annotations:  map[string]string{"rollout": "stable"},
+				},
+			},
+		},
+		{
+			name: "owner for unknown replicaset is ignored",
+			overrides: map[string]any{
+				source.QueryReplicaSetInfo: []*source.ReplicaSetInfoResult{
+					{UID: "rs-1", ReplicaSet: "my-app-v1", NamespaceUID: "ns-1"},
+				},
+				source.QueryReplicaSetUptime: []*source.UptimeResult{
+					{UID: "rs-1", First: start, Last: end},
+				},
+				source.QueryReplicaSetOwners: []*source.OwnerResult{
+					{UID: "unknown-rs", OwnerUID: "dep-1", OwnerKind: "Deployment", Controller: true},
+				},
+			},
+			want: map[string]*kubemodel.ReplicaSet{
+				"rs-1": {
+					UID:          "rs-1",
+					Name:         "my-app-v1",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.ReplicaSets)
+		})
+	}
+}

+ 362 - 0
core/pkg/compute/kubemodel/resourcequota_test.go

@@ -0,0 +1,362 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeResourceQuotas(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	emptyRQ := func(uid, name, nsUID string) *kubemodel.ResourceQuota {
+		return &kubemodel.ResourceQuota{
+			UID:          uid,
+			Name:         name,
+			NamespaceUID: nsUID,
+			Start:        start,
+			End:          end,
+			Spec:         &kubemodel.ResourceQuotaSpec{Hard: &kubemodel.ResourceQuotaSpecHard{}},
+			Status:       &kubemodel.ResourceQuotaStatus{Used: &kubemodel.ResourceQuotaStatusUsed{}},
+		}
+	}
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.ResourceQuota
+	}{
+		{
+			name:      "no data returns empty resource quota map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.ResourceQuota{},
+		},
+		{
+			name: "basic resource quota info and uptime",
+			overrides: map[string]any{
+				source.QueryResourceQuotaInfo: []*source.ResourceQuotaInfoResult{
+					{UID: "rq-1", ResourceQuota: "compute-quota", NamespaceUID: "ns-1"},
+				},
+				source.QueryResourceQuotaUptime: []*source.UptimeResult{
+					{UID: "rq-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.ResourceQuota{
+				"rq-1": emptyRQ("rq-1", "compute-quota", "ns-1"),
+			},
+		},
+		{
+			name: "resource quota without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryResourceQuotaInfo: []*source.ResourceQuotaInfoResult{
+					{UID: "rq-1", ResourceQuota: "compute-quota", NamespaceUID: "ns-1"},
+				},
+			},
+			want: map[string]*kubemodel.ResourceQuota{},
+		},
+		{
+			name: "spec cpu and ram request limits are populated",
+			overrides: map[string]any{
+				source.QueryResourceQuotaInfo: []*source.ResourceQuotaInfoResult{
+					{UID: "rq-1", ResourceQuota: "compute-quota", NamespaceUID: "ns-1"},
+				},
+				source.QueryResourceQuotaUptime: []*source.UptimeResult{
+					{UID: "rq-1", First: start, Last: end},
+				},
+				source.QueryResourceQuotaSpecCPURequestAverage: []*source.ResourceResult{
+					{UID: "rq-1", Value: 2.0},
+				},
+				source.QueryResourceQuotaSpecCPURequestMax: []*source.ResourceResult{
+					{UID: "rq-1", Value: 4.0},
+				},
+				source.QueryResourceQuotaSpecRAMRequestAverage: []*source.ResourceResult{
+					{UID: "rq-1", Value: 4 * 1024 * 1024 * 1024},
+				},
+				source.QueryResourceQuotaSpecRAMRequestMax: []*source.ResourceResult{
+					{UID: "rq-1", Value: 8 * 1024 * 1024 * 1024},
+				},
+			},
+			want: map[string]*kubemodel.ResourceQuota{
+				"rq-1": {
+					UID:          "rq-1",
+					Name:         "compute-quota",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Spec: &kubemodel.ResourceQuotaSpec{
+						Hard: &kubemodel.ResourceQuotaSpecHard{
+							Requests: kubemodel.ResourceQuantities{
+								kubemodel.ResourceCPU: {
+									Resource: kubemodel.ResourceCPU,
+									Unit:     kubemodel.UnitMillicore,
+									Values:   kubemodel.Stats{kubemodel.StatAvg: 2000, kubemodel.StatMax: 4000},
+								},
+								kubemodel.ResourceMemory: {
+									Resource: kubemodel.ResourceMemory,
+									Unit:     kubemodel.UnitByte,
+									Values:   kubemodel.Stats{kubemodel.StatAvg: 4 * 1024 * 1024 * 1024, kubemodel.StatMax: 8 * 1024 * 1024 * 1024},
+								},
+							},
+						},
+					},
+					Status: &kubemodel.ResourceQuotaStatus{Used: &kubemodel.ResourceQuotaStatusUsed{}},
+				},
+			},
+		},
+		{
+			name: "status used cpu and ram are populated",
+			overrides: map[string]any{
+				source.QueryResourceQuotaInfo: []*source.ResourceQuotaInfoResult{
+					{UID: "rq-1", ResourceQuota: "compute-quota", NamespaceUID: "ns-1"},
+				},
+				source.QueryResourceQuotaUptime: []*source.UptimeResult{
+					{UID: "rq-1", First: start, Last: end},
+				},
+				source.QueryResourceQuotaStatusUsedCPURequestAverage: []*source.ResourceResult{
+					{UID: "rq-1", Value: 1.0},
+				},
+				source.QueryResourceQuotaStatusUsedRAMRequestAverage: []*source.ResourceResult{
+					{UID: "rq-1", Value: 2 * 1024 * 1024 * 1024},
+				},
+			},
+			want: map[string]*kubemodel.ResourceQuota{
+				"rq-1": {
+					UID:          "rq-1",
+					Name:         "compute-quota",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Spec:         &kubemodel.ResourceQuotaSpec{Hard: &kubemodel.ResourceQuotaSpecHard{}},
+					Status: &kubemodel.ResourceQuotaStatus{
+						Used: &kubemodel.ResourceQuotaStatusUsed{
+							Requests: kubemodel.ResourceQuantities{
+								kubemodel.ResourceCPU: {
+									Resource: kubemodel.ResourceCPU,
+									Unit:     kubemodel.UnitMillicore,
+									Values:   kubemodel.Stats{kubemodel.StatAvg: 1000},
+								},
+								kubemodel.ResourceMemory: {
+									Resource: kubemodel.ResourceMemory,
+									Unit:     kubemodel.UnitByte,
+									Values:   kubemodel.Stats{kubemodel.StatAvg: 2 * 1024 * 1024 * 1024},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "spec cpu and ram limits are populated",
+			overrides: map[string]any{
+				source.QueryResourceQuotaInfo: []*source.ResourceQuotaInfoResult{
+					{UID: "rq-1", ResourceQuota: "compute-quota", NamespaceUID: "ns-1"},
+				},
+				source.QueryResourceQuotaUptime: []*source.UptimeResult{
+					{UID: "rq-1", First: start, Last: end},
+				},
+				source.QueryResourceQuotaSpecCPULimitAverage: []*source.ResourceResult{
+					{UID: "rq-1", Value: 4.0},
+				},
+				source.QueryResourceQuotaSpecCPULimitMax: []*source.ResourceResult{
+					{UID: "rq-1", Value: 8.0},
+				},
+				source.QueryResourceQuotaSpecRAMLimitAverage: []*source.ResourceResult{
+					{UID: "rq-1", Value: 8 * 1024 * 1024 * 1024},
+				},
+				source.QueryResourceQuotaSpecRAMLimitMax: []*source.ResourceResult{
+					{UID: "rq-1", Value: 16 * 1024 * 1024 * 1024},
+				},
+			},
+			want: map[string]*kubemodel.ResourceQuota{
+				"rq-1": {
+					UID:          "rq-1",
+					Name:         "compute-quota",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Spec: &kubemodel.ResourceQuotaSpec{
+						Hard: &kubemodel.ResourceQuotaSpecHard{
+							Limits: kubemodel.ResourceQuantities{
+								kubemodel.ResourceCPU: {
+									Resource: kubemodel.ResourceCPU,
+									Unit:     kubemodel.UnitMillicore,
+									Values:   kubemodel.Stats{kubemodel.StatAvg: 4000, kubemodel.StatMax: 8000},
+								},
+								kubemodel.ResourceMemory: {
+									Resource: kubemodel.ResourceMemory,
+									Unit:     kubemodel.UnitByte,
+									Values:   kubemodel.Stats{kubemodel.StatAvg: 8 * 1024 * 1024 * 1024, kubemodel.StatMax: 16 * 1024 * 1024 * 1024},
+								},
+							},
+						},
+					},
+					Status: &kubemodel.ResourceQuotaStatus{Used: &kubemodel.ResourceQuotaStatusUsed{}},
+				},
+			},
+		},
+		{
+			name: "status used cpu and ram request max are populated",
+			overrides: map[string]any{
+				source.QueryResourceQuotaInfo: []*source.ResourceQuotaInfoResult{
+					{UID: "rq-1", ResourceQuota: "compute-quota", NamespaceUID: "ns-1"},
+				},
+				source.QueryResourceQuotaUptime: []*source.UptimeResult{
+					{UID: "rq-1", First: start, Last: end},
+				},
+				source.QueryResourceQuotaStatusUsedCPURequestMax: []*source.ResourceResult{
+					{UID: "rq-1", Value: 3.0},
+				},
+				source.QueryResourceQuotaStatusUsedRAMRequestMax: []*source.ResourceResult{
+					{UID: "rq-1", Value: 6 * 1024 * 1024 * 1024},
+				},
+			},
+			want: map[string]*kubemodel.ResourceQuota{
+				"rq-1": {
+					UID:          "rq-1",
+					Name:         "compute-quota",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Spec:         &kubemodel.ResourceQuotaSpec{Hard: &kubemodel.ResourceQuotaSpecHard{}},
+					Status: &kubemodel.ResourceQuotaStatus{
+						Used: &kubemodel.ResourceQuotaStatusUsed{
+							Requests: kubemodel.ResourceQuantities{
+								kubemodel.ResourceCPU: {
+									Resource: kubemodel.ResourceCPU,
+									Unit:     kubemodel.UnitMillicore,
+									Values:   kubemodel.Stats{kubemodel.StatMax: 3000},
+								},
+								kubemodel.ResourceMemory: {
+									Resource: kubemodel.ResourceMemory,
+									Unit:     kubemodel.UnitByte,
+									Values:   kubemodel.Stats{kubemodel.StatMax: 6 * 1024 * 1024 * 1024},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "status used cpu and ram limits are populated",
+			overrides: map[string]any{
+				source.QueryResourceQuotaInfo: []*source.ResourceQuotaInfoResult{
+					{UID: "rq-1", ResourceQuota: "compute-quota", NamespaceUID: "ns-1"},
+				},
+				source.QueryResourceQuotaUptime: []*source.UptimeResult{
+					{UID: "rq-1", First: start, Last: end},
+				},
+				source.QueryResourceQuotaStatusUsedCPULimitAverage: []*source.ResourceResult{
+					{UID: "rq-1", Value: 2.0},
+				},
+				source.QueryResourceQuotaStatusUsedCPULimitMax: []*source.ResourceResult{
+					{UID: "rq-1", Value: 4.0},
+				},
+				source.QueryResourceQuotaStatusUsedRAMLimitAverage: []*source.ResourceResult{
+					{UID: "rq-1", Value: 4 * 1024 * 1024 * 1024},
+				},
+				source.QueryResourceQuotaStatusUsedRAMLimitMax: []*source.ResourceResult{
+					{UID: "rq-1", Value: 8 * 1024 * 1024 * 1024},
+				},
+			},
+			want: map[string]*kubemodel.ResourceQuota{
+				"rq-1": {
+					UID:          "rq-1",
+					Name:         "compute-quota",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Spec:         &kubemodel.ResourceQuotaSpec{Hard: &kubemodel.ResourceQuotaSpecHard{}},
+					Status: &kubemodel.ResourceQuotaStatus{
+						Used: &kubemodel.ResourceQuotaStatusUsed{
+							Limits: kubemodel.ResourceQuantities{
+								kubemodel.ResourceCPU: {
+									Resource: kubemodel.ResourceCPU,
+									Unit:     kubemodel.UnitMillicore,
+									Values:   kubemodel.Stats{kubemodel.StatAvg: 2000, kubemodel.StatMax: 4000},
+								},
+								kubemodel.ResourceMemory: {
+									Resource: kubemodel.ResourceMemory,
+									Unit:     kubemodel.UnitByte,
+									Values:   kubemodel.Stats{kubemodel.StatAvg: 4 * 1024 * 1024 * 1024, kubemodel.StatMax: 8 * 1024 * 1024 * 1024},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "metric data for unknown resource quota is ignored",
+			overrides: map[string]any{
+				source.QueryResourceQuotaInfo: []*source.ResourceQuotaInfoResult{
+					{UID: "rq-1", ResourceQuota: "compute-quota", NamespaceUID: "ns-1"},
+				},
+				source.QueryResourceQuotaUptime: []*source.UptimeResult{
+					{UID: "rq-1", First: start, Last: end},
+				},
+				// all 16 metric queries carry an unknown UID — hit every !ok warn path
+				source.QueryResourceQuotaSpecCPURequestAverage:       []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaSpecCPURequestMax:           []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaSpecRAMRequestAverage:       []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaSpecRAMRequestMax:           []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaSpecCPULimitAverage:         []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaSpecCPULimitMax:             []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaSpecRAMLimitAverage:         []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaSpecRAMLimitMax:             []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaStatusUsedCPURequestAverage: []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaStatusUsedCPURequestMax:     []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaStatusUsedRAMRequestAverage: []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaStatusUsedRAMRequestMax:     []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaStatusUsedCPULimitAverage:   []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaStatusUsedCPULimitMax:       []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaStatusUsedRAMLimitAverage:   []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+				source.QueryResourceQuotaStatusUsedRAMLimitMax:       []*source.ResourceResult{{UID: "unknown-rq", Value: 1.0}},
+			},
+			want: map[string]*kubemodel.ResourceQuota{
+				"rq-1": emptyRQ("rq-1", "compute-quota", "ns-1"),
+			},
+		},
+		{
+			name: "uptime for unknown resource quota is ignored",
+			overrides: map[string]any{
+				source.QueryResourceQuotaInfo: []*source.ResourceQuotaInfoResult{
+					{UID: "rq-1", ResourceQuota: "compute-quota", NamespaceUID: "ns-1"},
+				},
+				source.QueryResourceQuotaUptime: []*source.UptimeResult{
+					{UID: "rq-1", First: start, Last: end},
+					{UID: "unknown-rq", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.ResourceQuota{
+				"rq-1": emptyRQ("rq-1", "compute-quota", "ns-1"),
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.ResourceQuotas)
+		})
+	}
+}

+ 159 - 0
core/pkg/compute/kubemodel/service_test.go

@@ -0,0 +1,159 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeServices(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.Service
+	}{
+		{
+			name:      "no data returns empty service map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.Service{},
+		},
+		{
+			name: "basic service info and uptime",
+			overrides: map[string]any{
+				source.QueryServiceInfo: []*source.ServiceInfoResult{
+					{UID: "svc-1", Service: "my-service", NamespaceUID: "ns-1", ServiceType: "ClusterIP"},
+				},
+				source.QueryServiceUptime: []*source.UptimeResult{
+					{UID: "svc-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Service{
+				"svc-1": {
+					UID:          "svc-1",
+					Name:         "my-service",
+					NamespaceUID: "ns-1",
+					Type:         kubemodel.ServiceTypeClusterIP,
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "service without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryServiceInfo: []*source.ServiceInfoResult{
+					{UID: "svc-1", Service: "my-service", NamespaceUID: "ns-1"},
+				},
+			},
+			want: map[string]*kubemodel.Service{},
+		},
+		{
+			name: "service without namespace uid is not registered",
+			overrides: map[string]any{
+				source.QueryServiceInfo: []*source.ServiceInfoResult{
+					{UID: "svc-1", Service: "my-service"},
+				},
+				source.QueryServiceUptime: []*source.UptimeResult{
+					{UID: "svc-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Service{},
+		},
+		{
+			name: "load balancer service with ingress address",
+			overrides: map[string]any{
+				source.QueryServiceInfo: []*source.ServiceInfoResult{
+					{UID: "svc-1", Service: "my-lb", NamespaceUID: "ns-1", ServiceType: "LoadBalancer", LBIngressAddress: "1.2.3.4"},
+				},
+				source.QueryServiceUptime: []*source.UptimeResult{
+					{UID: "svc-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Service{
+				"svc-1": {
+					UID:              "svc-1",
+					Name:             "my-lb",
+					NamespaceUID:     "ns-1",
+					Type:             kubemodel.ServiceTypeLoadBalancer,
+					LBIngressAddress: "1.2.3.4",
+					Start:            start,
+					End:              end,
+				},
+			},
+		},
+		{
+			name: "service selector labels are attached",
+			overrides: map[string]any{
+				source.QueryServiceInfo: []*source.ServiceInfoResult{
+					{UID: "svc-1", Service: "my-service", NamespaceUID: "ns-1", ServiceType: "ClusterIP"},
+				},
+				source.QueryServiceUptime: []*source.UptimeResult{
+					{UID: "svc-1", First: start, Last: end},
+				},
+				source.QueryServiceSelectorLabels: []*source.ServiceLabelsResult{
+					{UID: "svc-1", Labels: map[string]string{"app": "web", "tier": "frontend"}},
+				},
+			},
+			want: map[string]*kubemodel.Service{
+				"svc-1": {
+					UID:          "svc-1",
+					Name:         "my-service",
+					NamespaceUID: "ns-1",
+					Type:         kubemodel.ServiceTypeClusterIP,
+					Start:        start,
+					End:          end,
+					Selector:     map[string]string{"app": "web", "tier": "frontend"},
+				},
+			},
+		},
+		{
+			name: "uptime for unknown service is ignored",
+			overrides: map[string]any{
+				source.QueryServiceInfo: []*source.ServiceInfoResult{
+					{UID: "svc-1", Service: "my-service", NamespaceUID: "ns-1"},
+				},
+				source.QueryServiceUptime: []*source.UptimeResult{
+					{UID: "svc-1", First: start, Last: end},
+					{UID: "unknown-svc", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.Service{
+				"svc-1": {
+					UID:          "svc-1",
+					Name:         "my-service",
+					NamespaceUID: "ns-1",
+					Type:         kubemodel.ServiceTypeClusterIP,
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.Services)
+		})
+	}
+}

+ 141 - 0
core/pkg/compute/kubemodel/statefulset_test.go

@@ -0,0 +1,141 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func TestComputeStatefulSets(t *testing.T) {
+	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := start.Add(time.Hour)
+
+	tests := []struct {
+		name      string
+		overrides map[string]any
+		want      map[string]*kubemodel.StatefulSet
+	}{
+		{
+			name:      "no data returns empty statefulset map",
+			overrides: map[string]any{},
+			want:      map[string]*kubemodel.StatefulSet{},
+		},
+		{
+			name: "basic statefulset info and uptime",
+			overrides: map[string]any{
+				source.QueryStatefulSetInfo: []*source.StatefulSetInfoResult{
+					{UID: "sts-1", StatefulSet: "my-db", NamespaceUID: "ns-1"},
+				},
+				source.QueryStatefulSetUptime: []*source.UptimeResult{
+					{UID: "sts-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.StatefulSet{
+				"sts-1": {
+					UID:          "sts-1",
+					Name:         "my-db",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+				},
+			},
+		},
+		{
+			name: "statefulset without uptime is not registered",
+			overrides: map[string]any{
+				source.QueryStatefulSetInfo: []*source.StatefulSetInfoResult{
+					{UID: "sts-1", StatefulSet: "my-db", NamespaceUID: "ns-1"},
+				},
+			},
+			want: map[string]*kubemodel.StatefulSet{},
+		},
+		{
+			name: "statefulset without namespace uid is not registered",
+			overrides: map[string]any{
+				source.QueryStatefulSetInfo: []*source.StatefulSetInfoResult{
+					{UID: "sts-1", StatefulSet: "my-db"},
+				},
+				source.QueryStatefulSetUptime: []*source.UptimeResult{
+					{UID: "sts-1", First: start, Last: end},
+				},
+			},
+			want: map[string]*kubemodel.StatefulSet{},
+		},
+		{
+			name: "statefulset labels and annotations are attached",
+			overrides: map[string]any{
+				source.QueryStatefulSetInfo: []*source.StatefulSetInfoResult{
+					{UID: "sts-1", StatefulSet: "my-db", NamespaceUID: "ns-1"},
+				},
+				source.QueryStatefulSetUptime: []*source.UptimeResult{
+					{UID: "sts-1", First: start, Last: end},
+				},
+				source.QueryStatefulSetLabels: []*source.LabelsResult{
+					{UID: "sts-1", Labels: map[string]string{"app": "postgres"}},
+				},
+				source.QueryStatefulSetAnnotations: []*source.AnnotationsResult{
+					{UID: "sts-1", Annotations: map[string]string{"version": "14"}},
+				},
+			},
+			want: map[string]*kubemodel.StatefulSet{
+				"sts-1": {
+					UID:          "sts-1",
+					Name:         "my-db",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					Labels:       map[string]string{"app": "postgres"},
+					Annotations:  map[string]string{"version": "14"},
+				},
+			},
+		},
+		{
+			name: "statefulset match labels are attached",
+			overrides: map[string]any{
+				source.QueryStatefulSetInfo: []*source.StatefulSetInfoResult{
+					{UID: "sts-1", StatefulSet: "my-db", NamespaceUID: "ns-1"},
+				},
+				source.QueryStatefulSetUptime: []*source.UptimeResult{
+					{UID: "sts-1", First: start, Last: end},
+				},
+				source.QueryStatefulSetMatchLabels: []*source.StatefulSetLabelsResult{
+					{UID: "sts-1", Labels: map[string]string{"app": "postgres"}},
+				},
+			},
+			want: map[string]*kubemodel.StatefulSet{
+				"sts-1": {
+					UID:          "sts-1",
+					Name:         "my-db",
+					NamespaceUID: "ns-1",
+					Start:        start,
+					End:          end,
+					MatchLabels:  map[string]string{"app": "postgres"},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := source.NewMockOpenCostDataSource()
+			ds.ResolutionValue = 5 * time.Minute
+			seedCluster(ds, start, end)
+			for method, result := range tt.overrides {
+				ds.Querier.SetOverride(method, result)
+			}
+
+			km, err := NewKubeModel(testClusterUID, false, ds)
+			require.NoError(t, err)
+
+			kms, err := km.ComputeKubeModelSet(start, end)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, kms.StatefulSets)
+		})
+	}
+}

+ 11 - 3
core/pkg/env/core.go

@@ -23,6 +23,7 @@ const (
 
 	ExportLegacyDataModelEnvVar = "EXPORT_LEGACY_DATA_MODEL"
 	ExportKubeModelEnvVar       = "EXPORT_KUBEMODEL"
+	ForceKubeModelV1EnvVar      = "FORCE_KUBEMODEL_V1"
 )
 
 // GetAPIPort returns the environment variable value for APIPortEnvVar which
@@ -65,10 +66,17 @@ func GetInstallNamespace(def string) string {
 	return Get(InstallNamespaceEnvVar, def)
 }
 
-func GetExportLegacyDataModel() bool {
+func IsLegacyDataModelExported() bool {
 	return GetBool(ExportLegacyDataModelEnvVar, true)
 }
 
-func GetExportKubeModel() bool {
-	return GetBool(ExportKubeModelEnvVar, true)
+func IsKubeModelExported() bool {
+	return GetBool(ExportKubeModelEnvVar, false)
+}
+
+// IsKubeModelV1Forced returns true if the kubemodel pipeline should always
+// export the legacy v1 (cluster, namespaces, resource quotas only) shape,
+// regardless of whether the source reports a complete kubemodel.
+func IsKubeModelV1Forced() bool {
+	return GetBool(ForceKubeModelV1EnvVar, true)
 }

+ 102 - 0
core/pkg/env/metrics.go

@@ -0,0 +1,102 @@
+package env
+
+const (
+	EmitPodAnnotationsMetricEnvVar       = "EMIT_POD_ANNOTATIONS_METRIC"
+	EmitNamespaceAnnotationsMetricEnvVar = "EMIT_NAMESPACE_ANNOTATIONS_METRIC"
+	EmitDeprecatedMetrics                = "EMIT_DEPRECATED_METRICS"
+
+	EmitKsmV1MetricsEnvVar = "EMIT_KSM_V1_METRICS"
+	EmitKsmV1MetricsOnly   = "EMIT_KSM_V1_METRICS_ONLY"
+
+	EmitDeploymentLabelsMetricEnvVar      = "EMIT_DEPLOYMENT_LABELS_METRIC"
+	EmitDeploymentAnnotationsMetricEnvVar = "EMIT_DEPLOYMENT_ANNOTATIONS_METRIC"
+
+	EmitStatefulSetLabelsMetricEnvVar      = "EMIT_STATEFULSET_LABELS_METRIC"
+	EmitStatefulSetAnnotationsMetricEnvVar = "EMIT_STATEFULSET_ANNOTATIONS_METRIC"
+
+	EmitDaemonSetLabelsMetricEnvVar      = "EMIT_DAEMONSET_LABELS_METRIC"
+	EmitDaemonSetAnnotationsMetricEnvVar = "EMIT_DAEMONSET_ANNOTATIONS_METRIC"
+
+	EmitJobLabelsMetricEnvVar      = "EMIT_JOB_LABELS_METRIC"
+	EmitJobAnnotationsMetricEnvVar = "EMIT_JOB_ANNOTATIONS_METRIC"
+
+	EmitCronJobLabelsMetricEnvVar      = "EMIT_CRONJOB_LABELS_METRIC"
+	EmitCronJobAnnotationsMetricEnvVar = "EMIT_CRONJOB_ANNOTATIONS_METRIC"
+
+	EmitReplicaSetLabelsMetricEnvVar      = "EMIT_REPLICASET_LABELS_METRIC"
+	EmitReplicaSetAnnotationsMetricEnvVar = "EMIT_REPLICASET_ANNOTATIONS_METRIC"
+)
+
+// IsEmitNamespaceAnnotationsMetric returns true if cost-model is configured to emit the kube_namespace_annotations metric
+// containing the namespace annotations
+func IsEmitNamespaceAnnotationsMetric() bool {
+	return GetBool(EmitNamespaceAnnotationsMetricEnvVar, false)
+}
+
+// IsEmitPodAnnotationsMetric returns true if cost-model is configured to emit the kube_pod_annotations metric containing
+// pod annotations.
+func IsEmitPodAnnotationsMetric() bool {
+	return GetBool(EmitPodAnnotationsMetricEnvVar, false)
+}
+
+// IsEmitKsmV1Metrics returns true if cost-model is configured to emit all necessary KSM v1
+// metrics that were removed in KSM v2
+func IsEmitKsmV1Metrics() bool {
+	return GetBool(EmitKsmV1MetricsEnvVar, true)
+}
+
+func IsEmitKsmV1MetricsOnly() bool {
+	return GetBool(EmitKsmV1MetricsOnly, false)
+}
+
+func IsEmitDeprecatedMetrics() bool {
+	return GetBool(EmitDeprecatedMetrics, false)
+}
+
+func IsEmitDeploymentLabelsMetric() bool {
+	return GetBool(EmitDeploymentLabelsMetricEnvVar, true)
+}
+
+func IsEmitDeploymentAnnotationsMetric() bool {
+	return GetBool(EmitDeploymentAnnotationsMetricEnvVar, false)
+}
+
+func IsEmitStatefulSetLabelsMetric() bool {
+	return GetBool(EmitStatefulSetLabelsMetricEnvVar, true)
+}
+
+func IsEmitStatefulSetAnnotationsMetric() bool {
+	return GetBool(EmitStatefulSetAnnotationsMetricEnvVar, false)
+}
+
+func IsEmitDaemonSetLabelsMetric() bool {
+	return GetBool(EmitDaemonSetLabelsMetricEnvVar, true)
+}
+
+func IsEmitDaemonSetAnnotationsMetric() bool {
+	return GetBool(EmitDaemonSetAnnotationsMetricEnvVar, false)
+}
+
+func IsEmitJobLabelsMetric() bool {
+	return GetBool(EmitJobLabelsMetricEnvVar, true)
+}
+
+func IsEmitJobAnnotationsMetric() bool {
+	return GetBool(EmitJobAnnotationsMetricEnvVar, false)
+}
+
+func IsEmitCronJobLabelsMetric() bool {
+	return GetBool(EmitCronJobLabelsMetricEnvVar, true)
+}
+
+func IsEmitCronJobAnnotationsMetric() bool {
+	return GetBool(EmitCronJobAnnotationsMetricEnvVar, false)
+}
+
+func IsEmitReplicaSetLabelsMetric() bool {
+	return GetBool(EmitReplicaSetLabelsMetricEnvVar, true)
+}
+
+func IsEmitReplicaSetAnnotationsMetric() bool {
+	return GetBool(EmitReplicaSetAnnotationsMetricEnvVar, false)
+}

+ 12 - 12
core/pkg/model/kubemodel/container.go

@@ -7,18 +7,18 @@ import (
 
 // @bingen:generate:Container
 type Container struct {
-	PodUID            string             `json:"podUid"`
-	Name              string             `json:"name"`
-	ResourceRequests  ResourceQuantities `json:"resourceRequests"`
-	ResourceLimits    ResourceQuantities `json:"resourceLimits"`
-	CPUCoresAllocated float64            `json:"cpuCoresAllocated"`
-	CPUCoreUsageAvg   float64            `json:"cpuCoreUsageAvg"`
-	CPUCoreUsageMax   float64            `json:"cpuCoreUsageMax"`
-	RAMBytesAllocated float64            `json:"ramBytesAllocated"`
-	RAMBytesUsageAvg  float64            `json:"ramBytesUsageAvg"`
-	RAMBytesUsageMax  float64            `json:"ramBytesUsageMax"`
-	Start             time.Time          `json:"start"`
-	End               time.Time          `json:"end"`
+	PodUID                string             `json:"podUid"`
+	Name                  string             `json:"name"`
+	ResourceRequests      ResourceQuantities `json:"resourceRequests"`
+	ResourceLimits        ResourceQuantities `json:"resourceLimits"`
+	CPUCoreAllocationAvg  float64            `json:"cpuCoreAllocationAvg"`
+	CPUCoreUsageAvg       float64            `json:"cpuCoreUsageAvg"`
+	CPUCoreUsageMax       float64            `json:"cpuCoreUsageMax"`
+	RAMBytesAllocationAvg float64            `json:"ramBytesAllocationAvg"`
+	RAMBytesUsageAvg      float64            `json:"ramBytesUsageAvg"`
+	RAMBytesUsageMax      float64            `json:"ramBytesUsageMax"`
+	Start                 time.Time          `json:"start"`
+	End                   time.Time          `json:"end"`
 }
 
 func (c *Container) GetKey() string {

+ 1 - 1
core/pkg/model/kubemodel/daemonset.go

@@ -13,7 +13,7 @@ type DaemonSet struct {
 	Name             string            `json:"name"`
 	Labels           map[string]string `json:"labels,omitempty"`
 	Annotations      map[string]string `json:"annotations,omitempty"`
-	DevicePluginInfo map[string]string `json:"devicePluginInfo"`
+	DevicePluginInfo map[string]string `json:"devicePluginInfo"` // bingen:field[ignore]
 	Start            time.Time         `json:"start,omitempty"`
 	End              time.Time         `json:"end,omitempty"`
 }

+ 1 - 1
core/pkg/model/kubemodel/kubemodel.go

@@ -24,7 +24,7 @@ type KubeModelSet struct {
 	PersistentVolumeClaims map[string]*PersistentVolumeClaim `json:"pvcs"`              // @bingen:field[version=2]
 	Pods                   map[string]*Pod                   `json:"pods"`              // @bingen:field[version=2]
 	Containers             map[string]*Container             `json:"containers"`        // @bingen:field[version=2]
-	DCGMDevices            map[string]*DCGMDevice            `json:"dcgmDevices"`       // @bingen:field[version=2]
+	DCGMDevices            map[string]*DCGMDevice            `json:"dcgmDevices"`       // @bingen:field[ignore]
 }
 
 func NewKubeModelSet(start time.Time, end time.Time) *KubeModelSet {

+ 7 - 160
core/pkg/model/kubemodel/kubemodel_codecs.go

@@ -14,6 +14,7 @@ package kubemodel
 import (
 	"cmp"
 	"fmt"
+	"github.com/opencost/opencost/core/pkg/model/shared"
 	"io"
 	"iter"
 	"os"
@@ -24,9 +25,7 @@ import (
 	"time"
 	"unsafe"
 
-	"github.com/opencost/opencost/core/pkg/model/shared"
-
-	"github.com/opencost/opencost/core/pkg/util"
+	util "github.com/opencost/opencost/core/pkg/util"
 )
 
 const (
@@ -1207,13 +1206,13 @@ func (target *Container) MarshalBinaryWithContext(ctx *EncodingContext) (err err
 	}
 	// --- [end][write][alias](ResourceQuantities) ---
 
-	buff.WriteFloat64(target.CPUCoresAllocated) // write float64
+	buff.WriteFloat64(target.CPUCoreAllocationAvg) // write float64
 
 	buff.WriteFloat64(target.CPUCoreUsageAvg) // write float64
 
 	buff.WriteFloat64(target.CPUCoreUsageMax) // write float64
 
-	buff.WriteFloat64(target.RAMBytesAllocated) // write float64
+	buff.WriteFloat64(target.RAMBytesAllocationAvg) // write float64
 
 	buff.WriteFloat64(target.RAMBytesUsageAvg) // write float64
 
@@ -1400,7 +1399,7 @@ func (target *Container) UnmarshalBinaryWithContext(ctx *DecodingContext) (err e
 	// --- [end][read][alias](ResourceQuantities) ---
 
 	bb := buff.ReadFloat64() // read float64
-	target.CPUCoresAllocated = bb
+	target.CPUCoreAllocationAvg = bb
 
 	cc := buff.ReadFloat64() // read float64
 	target.CPUCoreUsageAvg = cc
@@ -1409,7 +1408,7 @@ func (target *Container) UnmarshalBinaryWithContext(ctx *DecodingContext) (err e
 	target.CPUCoreUsageMax = dd
 
 	ee := buff.ReadFloat64() // read float64
-	target.RAMBytesAllocated = ee
+	target.RAMBytesAllocationAvg = ee
 
 	ff := buff.ReadFloat64() // read float64
 	target.RAMBytesUsageAvg = ff
@@ -4286,38 +4285,6 @@ func (target *KubeModelSet) MarshalBinaryWithContext(ctx *EncodingContext) (err
 		}
 		// --- [end][write][map](map[string]*Container) ---
 
-	}
-	if target.DCGMDevices == nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1)) // write non-nil byte
-
-		// --- [begin][write][map](map[string]*DCGMDevice) ---
-		buff.WriteInt(len(target.DCGMDevices)) // map length
-		for vvvvvvvvvvvvvvv, zzzzzzzzzzzzzzz := range target.DCGMDevices {
-			if ctx.IsStringTable() {
-				r := ctx.Table.AddOrGet(vvvvvvvvvvvvvvv)
-				buff.WriteInt(r) // write table index
-			} else {
-				buff.WriteString(vvvvvvvvvvvvvvv) // write string
-			}
-			if zzzzzzzzzzzzzzz == nil {
-				buff.WriteUInt8(uint8(0)) // write nil byte
-			} else {
-				buff.WriteUInt8(uint8(1)) // write non-nil byte
-
-				// --- [begin][write][struct](DCGMDevice) ---
-				buff.WriteInt(0) // [compatibility, unused]
-				errR := zzzzzzzzzzzzzzz.MarshalBinaryWithContext(ctx)
-				if errR != nil {
-					return errR
-				}
-				// --- [end][write][struct](DCGMDevice) ---
-
-			}
-		}
-		// --- [end][write][map](map[string]*DCGMDevice) ---
-
 	}
 
 	return nil
@@ -4396,7 +4363,6 @@ func (target *KubeModelSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (er
 	}
 	// field version check
 	if uint8(1) <= version {
-
 		// --- [begin][read][struct](Window) ---
 		b := new(Window)
 		buff.ReadInt() // [compatibility, unused]
@@ -4678,7 +4644,6 @@ func (target *KubeModelSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (er
 				if buff.ReadUInt8() == uint8(0) {
 					zzzzzz = nil
 				} else {
-
 					// --- [begin][read][struct](DaemonSet) ---
 					uu := new(DaemonSet)
 					buff.ReadInt() // [compatibility, unused]
@@ -4904,7 +4869,6 @@ func (target *KubeModelSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (er
 				if buff.ReadUInt8() == uint8(0) {
 					zzzzzzzzzzz = nil
 				} else {
-
 					// --- [begin][read][struct](PersistentVolume) ---
 					ffff := new(PersistentVolume)
 					buff.ReadInt() // [compatibility, unused]
@@ -5040,7 +5004,6 @@ func (target *KubeModelSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (er
 				if buff.ReadUInt8() == uint8(0) {
 					zzzzzzzzzzzzzz = nil
 				} else {
-
 					// --- [begin][read][struct](Container) ---
 					ccccc := new(Container)
 					buff.ReadInt() // [compatibility, unused]
@@ -5062,52 +5025,6 @@ func (target *KubeModelSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (er
 	} else {
 		target.Containers = nil
 	}
-	// field version check
-	if uint8(2) <= version {
-		if buff.ReadUInt8() == uint8(0) {
-			target.DCGMDevices = nil
-		} else {
-			// --- [begin][read][map](map[string]*DCGMDevice) ---
-			eeeee := buff.ReadInt() // map len
-			ddddd := make(map[string]*DCGMDevice, eeeee)
-			for range eeeee {
-				var vvvvvvvvvvvvvvv string
-				var ggggg string
-				if ctx.IsStringTable() {
-					hhhhh := buff.ReadInt() // read string index
-					ggggg = ctx.Table.At(hhhhh)
-				} else {
-					ggggg = buff.ReadString() // read string
-				}
-				fffff := ggggg
-				vvvvvvvvvvvvvvv = fffff
-
-				var zzzzzzzzzzzzzzz *DCGMDevice
-				if buff.ReadUInt8() == uint8(0) {
-					zzzzzzzzzzzzzzz = nil
-				} else {
-
-					// --- [begin][read][struct](DCGMDevice) ---
-					lllll := new(DCGMDevice)
-					buff.ReadInt() // [compatibility, unused]
-					errR := lllll.UnmarshalBinaryWithContext(ctx)
-					if errR != nil {
-						return errR
-					}
-					zzzzzzzzzzzzzzz = lllll
-					// --- [end][read][struct](DCGMDevice) ---
-
-				}
-				ddddd[vvvvvvvvvvvvvvv] = zzzzzzzzzzzzzzz
-			}
-			target.DCGMDevices = ddddd
-			// --- [end][read][map](map[string]*DCGMDevice) ---
-
-		}
-
-	} else {
-		target.DCGMDevices = nil
-	}
 
 	return nil
 }
@@ -5589,7 +5506,6 @@ func (stream *KubeModelSetStream) Stream() iter.Seq2[BingenFieldInfo, *BingenVal
 					if buff.ReadUInt8() == uint8(0) {
 						zzzzzz = nil
 					} else {
-
 						// --- [begin][read][struct](DaemonSet) ---
 						rr := new(DaemonSet)
 						buff.ReadInt() // [compatibility, unused]
@@ -5885,7 +5801,6 @@ func (stream *KubeModelSetStream) Stream() iter.Seq2[BingenFieldInfo, *BingenVal
 					if buff.ReadUInt8() == uint8(0) {
 						zzzzzzzzzzz = nil
 					} else {
-
 						// --- [begin][read][struct](PersistentVolume) ---
 						www := new(PersistentVolume)
 						buff.ReadInt() // [compatibility, unused]
@@ -6063,7 +5978,6 @@ func (stream *KubeModelSetStream) Stream() iter.Seq2[BingenFieldInfo, *BingenVal
 					if buff.ReadUInt8() == uint8(0) {
 						zzzzzzzzzzzzzz = nil
 					} else {
-
 						// --- [begin][read][struct](Container) ---
 						pppp := new(Container)
 						buff.ReadInt() // [compatibility, unused]
@@ -6092,66 +6006,6 @@ func (stream *KubeModelSetStream) Stream() iter.Seq2[BingenFieldInfo, *BingenVal
 				return
 			}
 		}
-
-		fi = BingenFieldInfo{
-			Type: reflect.TypeFor[map[string]*DCGMDevice](),
-			Name: "DCGMDevices",
-		}
-		// field version check
-		if uint8(2) <= version {
-
-			if buff.ReadUInt8() == uint8(0) {
-				if !yield(fi, nil) {
-					return
-				}
-			} else {
-				// --- [begin][read][streaming-map](map[string]*DCGMDevice) ---
-				qqqq := buff.ReadInt() // map len
-				for range qqqq {
-					var vvvvvvvvvvvvvvv string
-					var ssss string
-					if ctx.IsStringTable() {
-						tttt := buff.ReadInt() // read string index
-						ssss = ctx.Table.At(tttt)
-					} else {
-						ssss = buff.ReadString() // read string
-					}
-					rrrr := ssss
-					vvvvvvvvvvvvvvv = rrrr
-
-					var zzzzzzzzzzzzzzz *DCGMDevice
-					if buff.ReadUInt8() == uint8(0) {
-						zzzzzzzzzzzzzzz = nil
-					} else {
-
-						// --- [begin][read][struct](DCGMDevice) ---
-						uuuu := new(DCGMDevice)
-						buff.ReadInt() // [compatibility, unused]
-						errR := uuuu.UnmarshalBinaryWithContext(ctx)
-						if errR != nil {
-							stream.err = errR
-							return
-
-						}
-						zzzzzzzzzzzzzzz = uuuu
-						// --- [end][read][struct](DCGMDevice) ---
-
-					}
-
-					if !yield(fi, pairV(vvvvvvvvvvvvvvv, zzzzzzzzzzzzzzz)) {
-						return
-					}
-				}
-				// --- [end][read][streaming-map](map[string]*DCGMDevice) ---
-
-			}
-
-		} else {
-
-			if !yield(fi, nil) {
-				return
-			}
-		}
 	}
 }
 
@@ -6230,7 +6084,6 @@ func (target *Metadata) MarshalBinaryWithContext(ctx *EncodingContext) (err erro
 		// --- [begin][write][slice]([]Diagnostic) ---
 		buff.WriteInt(len(target.Diagnostics)) // slice length
 		for i := range target.Diagnostics {
-
 			// --- [begin][write][struct](Diagnostic) ---
 			buff.WriteInt(0) // [compatibility, unused]
 			errC := target.Diagnostics[i].MarshalBinaryWithContext(ctx)
@@ -6354,7 +6207,6 @@ func (target *Metadata) UnmarshalBinaryWithContext(ctx *DecodingContext) (err er
 			l := buff.ReadInt() // slice len
 			h := make([]Diagnostic, l)
 			for i := range l {
-
 				// --- [begin][read][struct](Diagnostic) ---
 				n := new(Diagnostic)
 				buff.ReadInt() // [compatibility, unused]
@@ -6377,7 +6229,6 @@ func (target *Metadata) UnmarshalBinaryWithContext(ctx *DecodingContext) (err er
 	}
 	// field version check
 	if uint8(1) <= version {
-
 		// --- [begin][read][alias](DiagnosticLevel) ---
 		var o int
 		p := buff.ReadInt() // read int
@@ -8169,7 +8020,6 @@ func (target *Pod) MarshalBinaryWithContext(ctx *EncodingContext) (err error) {
 		// --- [begin][write][slice]([]NetworkTrafficDetail) ---
 		buff.WriteInt(len(target.NetworkTrafficDetails)) // slice length
 		for ii := range target.NetworkTrafficDetails {
-
 			// --- [begin][write][struct](NetworkTrafficDetail) ---
 			buff.WriteInt(0) // [compatibility, unused]
 			errC := target.NetworkTrafficDetails[ii].MarshalBinaryWithContext(ctx)
@@ -8422,7 +8272,6 @@ func (target *Pod) UnmarshalBinaryWithContext(ctx *DecodingContext) (err error)
 		tt := buff.ReadInt() // slice len
 		ss := make([]NetworkTrafficDetail, tt)
 		for ii := range tt {
-
 			// --- [begin][read][struct](NetworkTrafficDetail) ---
 			ww := new(NetworkTrafficDetail)
 			buff.ReadInt() // [compatibility, unused]
@@ -8679,7 +8528,6 @@ func (target *ReplicaSet) MarshalBinaryWithContext(ctx *EncodingContext) (err er
 		// --- [begin][write][slice]([]Owner) ---
 		buff.WriteInt(len(target.Owners)) // slice length
 		for i := range target.Owners {
-
 			// --- [begin][write][struct](Owner) ---
 			buff.WriteInt(0) // [compatibility, unused]
 			errA := target.Owners[i].MarshalBinaryWithContext(ctx)
@@ -8854,7 +8702,6 @@ func (target *ReplicaSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (err
 		n := buff.ReadInt() // slice len
 		m := make([]Owner, n)
 		for i := range n {
-
 			// --- [begin][read][struct](Owner) ---
 			p := new(Owner)
 			buff.ReadInt() // [compatibility, unused]
@@ -9140,7 +8987,6 @@ func (target *ResourceQuantity) UnmarshalBinaryWithContext(ctx *DecodingContext)
 	}
 	// field version check
 	if uint8(1) <= version {
-
 		// --- [begin][read][alias](Unit) ---
 		var e string
 		var g string
@@ -9160,6 +9006,7 @@ func (target *ResourceQuantity) UnmarshalBinaryWithContext(ctx *DecodingContext)
 	}
 	// field version check
 	if uint8(1) <= version {
+
 		// --- [begin][read][alias](Stats) ---
 		var l map[StatType]float64
 		if buff.ReadUInt8() == uint8(0) {

+ 2 - 1
core/pkg/model/kubemodel/kubemodel_helpers_test.go

@@ -59,5 +59,6 @@ func KubeModelSetEquals(t *testing.T, this, that *KubeModelSet) {
 	require.Equal(t, this.PersistentVolumeClaims, that.PersistentVolumeClaims)
 	require.Equal(t, this.Services, that.Services)
 	require.Equal(t, this.PersistentVolumes, that.PersistentVolumes)
-	require.Equal(t, this.DCGMDevices, that.DCGMDevices)
+	// DCGM is ignored by bingen
+	// require.Equal(t, this.DCGMDevices, that.DCGMDevices)
 }

+ 8 - 8
core/pkg/model/kubemodel/mock.go

@@ -121,14 +121,14 @@ func NewMockKubeModelSet(start, end time.Time) *KubeModelSet {
 			ResourceCPU:    {Resource: ResourceCPU, Unit: UnitMillicore, Values: Stats{StatAvg: 500, StatMax: 500}},
 			ResourceMemory: {Resource: ResourceMemory, Unit: UnitByte, Values: Stats{StatAvg: 1e9, StatMax: 1e9}},
 		},
-		CPUCoresAllocated: 0.25,
-		CPUCoreUsageAvg:   0.18,
-		CPUCoreUsageMax:   0.42,
-		RAMBytesAllocated: 512e6,
-		RAMBytesUsageAvg:  300e6,
-		RAMBytesUsageMax:  480e6,
-		Start:             start,
-		End:               end,
+		CPUCoreAllocationAvg:  0.25,
+		CPUCoreUsageAvg:       0.18,
+		CPUCoreUsageMax:       0.42,
+		RAMBytesAllocationAvg: 512e6,
+		RAMBytesUsageAvg:      300e6,
+		RAMBytesUsageMax:      480e6,
+		Start:                 start,
+		End:                   end,
 	})
 
 	// --- Deployment ---

+ 213 - 0
core/pkg/source/datasource.go

@@ -8,6 +8,216 @@ import (
 	"github.com/opencost/opencost/core/pkg/diagnostics"
 )
 
+// Query name constants for use with MockMetricsQuerier.SetOverride.
+const (
+	// Local Cluster Disks
+	QueryLocalStorageActiveMinutes = "QueryLocalStorageActiveMinutes"
+	QueryLocalStorageUsedAvg       = "QueryLocalStorageUsedAvg"
+	QueryLocalStorageUsedMax       = "QueryLocalStorageUsedMax"
+	QueryLocalStorageBytes         = "QueryLocalStorageBytes"
+	QueryKMLocalStorageUsedAvg     = "QueryKMLocalStorageUsedAvg"
+	QueryKMLocalStorageUsedMax     = "QueryKMLocalStorageUsedMax"
+	QueryKMLocalStorageBytes       = "QueryKMLocalStorageBytes"
+
+	// Nodes
+	QueryNodeInfo                 = "QueryNodeInfo"
+	QueryNodeUptime               = "QueryNodeUptime"
+	QueryNodeActiveMinutes        = "QueryNodeActiveMinutes"
+	QueryNodeCPUCoresCapacity     = "QueryNodeCPUCoresCapacity"
+	QueryNodeCPUCoresAllocatable  = "QueryNodeCPUCoresAllocatable"
+	QueryNodeRAMBytesCapacity     = "QueryNodeRAMBytesCapacity"
+	QueryNodeRAMBytesAllocatable  = "QueryNodeRAMBytesAllocatable"
+	QueryNodeGPUCount             = "QueryNodeGPUCount"
+	QueryNodeCPUModeTotal         = "QueryNodeCPUModeTotal"
+	QueryNodeIsSpot               = "QueryNodeIsSpot"
+	QueryNodeRAMSystemPercent     = "QueryNodeRAMSystemPercent"
+	QueryNodeRAMUserPercent       = "QueryNodeRAMUserPercent"
+	QueryNodeResourceCapacities   = "QueryNodeResourceCapacities"
+	QueryNodeResourcesAllocatable = "QueryNodeResourcesAllocatable"
+
+	// Load Balancers
+	QueryLBActiveMinutes = "QueryLBActiveMinutes"
+	QueryLBPricePerHr    = "QueryLBPricePerHr"
+
+	// Cluster Management
+	QueryClusterInfo                 = "QueryClusterInfo"
+	QueryClusterKubeModelVersion     = "QueryClusterKubeModelVersion"
+	QueryClusterUptime               = "QueryClusterUptime"
+	QueryClusterManagementDuration   = "QueryClusterManagementDuration"
+	QueryClusterManagementPricePerHr = "QueryClusterManagementPricePerHr"
+
+	// Pods
+	QueryPods                   = "QueryPods"
+	QueryPodsUID                = "QueryPodsUID"
+	QueryPodInfo                = "QueryPodInfo"
+	QueryPodUptime              = "QueryPodUptime"
+	QueryPodOwners              = "QueryPodOwners"
+	QueryPodPVCVolumes          = "QueryPodPVCVolumes"
+	QueryPodNetworkEgressBytes  = "QueryPodNetworkEgressBytes"
+	QueryPodNetworkIngressBytes = "QueryPodNetworkIngressBytes"
+
+	// Container
+	QueryContainerUptime           = "QueryContainerUptime"
+	QueryContainerResourceRequests = "QueryContainerResourceRequests"
+	QueryContainerResourceLimits   = "QueryContainerResourceLimits"
+
+	// RAM
+	QueryRAMBytesAllocated    = "QueryRAMBytesAllocated"
+	QueryRAMRequests          = "QueryRAMRequests"
+	QueryRAMLimits            = "QueryRAMLimits"
+	QueryRAMUsageAvg          = "QueryRAMUsageAvg"
+	QueryRAMUsageMax          = "QueryRAMUsageMax"
+	QueryNodeRAMPricePerGiBHr = "QueryNodeRAMPricePerGiBHr"
+
+	// CPU
+	QueryCPUCoresAllocated = "QueryCPUCoresAllocated"
+	QueryCPURequests       = "QueryCPURequests"
+	QueryCPULimits         = "QueryCPULimits"
+	QueryCPUUsageAvg       = "QueryCPUUsageAvg"
+	QueryCPUUsageMax       = "QueryCPUUsageMax"
+	QueryNodeCPUPricePerHr = "QueryNodeCPUPricePerHr"
+
+	// GPU
+	QueryGPUsAllocated     = "QueryGPUsAllocated"
+	QueryGPUsRequested     = "QueryGPUsRequested"
+	QueryGPUsUsageAvg      = "QueryGPUsUsageAvg"
+	QueryGPUsUsageMax      = "QueryGPUsUsageMax"
+	QueryNodeGPUPricePerHr = "QueryNodeGPUPricePerHr"
+	QueryGPUInfo           = "QueryGPUInfo"
+	QueryIsGPUShared       = "QueryIsGPUShared"
+
+	// Device
+	QueryDCGMDeviceInfo        = "QueryDCGMDeviceInfo"
+	QueryDCGMDeviceUptime      = "QueryDCGMDeviceUptime"
+	QueryDCGMContainerUsageAvg = "QueryDCGMContainerUsageAvg"
+	QueryDCGMContainerUsageMax = "QueryDCGMContainerUsageMax"
+
+	// PVC
+	QueryPodPVCAllocation    = "QueryPodPVCAllocation"
+	QueryPVCBytesRequested   = "QueryPVCBytesRequested"
+	QueryPVCInfo             = "QueryPVCInfo"
+	QueryKMPVCInfo           = "QueryKMPVCInfo"
+	QueryPVCUptime           = "QueryPVCUptime"
+	QueryPVCBytesUsedAverage = "QueryPVCBytesUsedAverage"
+	QueryPVCBytesUsedMax     = "QueryPVCBytesUsedMax"
+
+	// PV
+	QueryPVBytes           = "QueryPVBytes"
+	QueryPVPricePerGiBHour = "QueryPVPricePerGiBHour"
+	QueryPVInfo            = "QueryPVInfo"
+	QueryPVActiveMinutes   = "QueryPVActiveMinutes"
+	QueryPVUsedAverage     = "QueryPVUsedAverage"
+	QueryPVUsedMax         = "QueryPVUsedMax"
+	QueryKMPVInfo          = "QueryKMPVInfo"
+	QueryPVUptime          = "QueryPVUptime"
+
+	// Deployment
+	QueryDeploymentInfo        = "QueryDeploymentInfo"
+	QueryDeploymentUptime      = "QueryDeploymentUptime"
+	QueryDeploymentLabels      = "QueryDeploymentLabels"
+	QueryDeploymentAnnotations = "QueryDeploymentAnnotations"
+	QueryDeploymentMatchLabels = "QueryDeploymentMatchLabels"
+
+	// StatefulSet
+	QueryStatefulSetInfo        = "QueryStatefulSetInfo"
+	QueryStatefulSetUptime      = "QueryStatefulSetUptime"
+	QueryStatefulSetLabels      = "QueryStatefulSetLabels"
+	QueryStatefulSetAnnotations = "QueryStatefulSetAnnotations"
+	QueryStatefulSetMatchLabels = "QueryStatefulSetMatchLabels"
+
+	// DaemonSet
+	QueryDaemonSetInfo        = "QueryDaemonSetInfo"
+	QueryDaemonSetUptime      = "QueryDaemonSetUptime"
+	QueryDaemonSetLabels      = "QueryDaemonSetLabels"
+	QueryDaemonSetAnnotations = "QueryDaemonSetAnnotations"
+
+	// Job
+	QueryJobInfo        = "QueryJobInfo"
+	QueryJobUptime      = "QueryJobUptime"
+	QueryJobLabels      = "QueryJobLabels"
+	QueryJobAnnotations = "QueryJobAnnotations"
+
+	// CronJob
+	QueryCronJobInfo        = "QueryCronJobInfo"
+	QueryCronJobUptime      = "QueryCronJobUptime"
+	QueryCronJobLabels      = "QueryCronJobLabels"
+	QueryCronJobAnnotations = "QueryCronJobAnnotations"
+
+	// ReplicaSet
+	QueryReplicaSetInfo           = "QueryReplicaSetInfo"
+	QueryReplicaSetUptime         = "QueryReplicaSetUptime"
+	QueryReplicaSetLabels         = "QueryReplicaSetLabels"
+	QueryReplicaSetAnnotations    = "QueryReplicaSetAnnotations"
+	QueryReplicaSetOwners         = "QueryReplicaSetOwners"
+	QueryPodsWithReplicaSetOwner  = "QueryPodsWithReplicaSetOwner"
+	QueryReplicaSetsWithoutOwners = "QueryReplicaSetsWithoutOwners"
+	QueryReplicaSetsWithRollout   = "QueryReplicaSetsWithRollout"
+
+	// Namespace
+	QueryNamespaceInfo        = "QueryNamespaceInfo"
+	QueryNamespaceUptime      = "QueryNamespaceUptime"
+	QueryNamespaceAnnotations = "QueryNamespaceAnnotations"
+	QueryNamespaceLabels      = "QueryNamespaceLabels"
+
+	// Service
+	QueryServiceInfo           = "QueryServiceInfo"
+	QueryServiceUptime         = "QueryServiceUptime"
+	QueryServiceSelectorLabels = "QueryServiceSelectorLabels"
+
+	// Network Egress
+	QueryNetZoneGiB               = "QueryNetZoneGiB"
+	QueryNetZonePricePerGiB       = "QueryNetZonePricePerGiB"
+	QueryNetRegionGiB             = "QueryNetRegionGiB"
+	QueryNetRegionPricePerGiB     = "QueryNetRegionPricePerGiB"
+	QueryNetInternetGiB           = "QueryNetInternetGiB"
+	QueryNetInternetPricePerGiB   = "QueryNetInternetPricePerGiB"
+	QueryNetInternetServiceGiB    = "QueryNetInternetServiceGiB"
+	QueryNetNatGatewayPricePerGiB = "QueryNetNatGatewayPricePerGiB"
+	QueryNetNatGatewayGiB         = "QueryNetNatGatewayGiB"
+	QueryNetTransferBytes         = "QueryNetTransferBytes"
+
+	// Network Ingress
+	QueryNetZoneIngressGiB               = "QueryNetZoneIngressGiB"
+	QueryNetRegionIngressGiB             = "QueryNetRegionIngressGiB"
+	QueryNetInternetIngressGiB           = "QueryNetInternetIngressGiB"
+	QueryNetInternetServiceIngressGiB    = "QueryNetInternetServiceIngressGiB"
+	QueryNetNatGatewayIngressPricePerGiB = "QueryNetNatGatewayIngressPricePerGiB"
+	QueryNetNatGatewayIngressGiB         = "QueryNetNatGatewayIngressGiB"
+	QueryNetReceiveBytes                 = "QueryNetReceiveBytes"
+
+	// Labels
+	QueryNodeLabels = "QueryNodeLabels"
+	QueryPodLabels  = "QueryPodLabels"
+
+	// Pod ownership
+	QueryPodAnnotations         = "QueryPodAnnotations"
+	QueryPodsWithDaemonSetOwner = "QueryPodsWithDaemonSetOwner"
+	QueryPodsWithJobOwner       = "QueryPodsWithJobOwner"
+
+	// ResourceQuotas
+	QueryResourceQuotaInfo                        = "QueryResourceQuotaInfo"
+	QueryResourceQuotaUptime                      = "QueryResourceQuotaUptime"
+	QueryResourceQuotaSpecCPURequestAverage       = "QueryResourceQuotaSpecCPURequestAverage"
+	QueryResourceQuotaSpecCPURequestMax           = "QueryResourceQuotaSpecCPURequestMax"
+	QueryResourceQuotaSpecRAMRequestAverage       = "QueryResourceQuotaSpecRAMRequestAverage"
+	QueryResourceQuotaSpecRAMRequestMax           = "QueryResourceQuotaSpecRAMRequestMax"
+	QueryResourceQuotaSpecCPULimitAverage         = "QueryResourceQuotaSpecCPULimitAverage"
+	QueryResourceQuotaSpecCPULimitMax             = "QueryResourceQuotaSpecCPULimitMax"
+	QueryResourceQuotaSpecRAMLimitAverage         = "QueryResourceQuotaSpecRAMLimitAverage"
+	QueryResourceQuotaSpecRAMLimitMax             = "QueryResourceQuotaSpecRAMLimitMax"
+	QueryResourceQuotaStatusUsedCPURequestAverage = "QueryResourceQuotaStatusUsedCPURequestAverage"
+	QueryResourceQuotaStatusUsedCPURequestMax     = "QueryResourceQuotaStatusUsedCPURequestMax"
+	QueryResourceQuotaStatusUsedRAMRequestAverage = "QueryResourceQuotaStatusUsedRAMRequestAverage"
+	QueryResourceQuotaStatusUsedRAMRequestMax     = "QueryResourceQuotaStatusUsedRAMRequestMax"
+	QueryResourceQuotaStatusUsedCPULimitAverage   = "QueryResourceQuotaStatusUsedCPULimitAverage"
+	QueryResourceQuotaStatusUsedCPULimitMax       = "QueryResourceQuotaStatusUsedCPULimitMax"
+	QueryResourceQuotaStatusUsedRAMLimitAverage   = "QueryResourceQuotaStatusUsedRAMLimitAverage"
+	QueryResourceQuotaStatusUsedRAMLimitMax       = "QueryResourceQuotaStatusUsedRAMLimitMax"
+
+	// Data Coverage
+	QueryDataCoverage = "QueryDataCoverage"
+)
+
 type MetricsQuerier interface {
 	// Cluster Disks
 
@@ -17,6 +227,7 @@ type MetricsQuerier interface {
 	QueryLocalStorageUsedMax(start, end time.Time) *Future[LocalStorageUsedMaxResult]
 	QueryLocalStorageBytes(start, end time.Time) *Future[LocalStorageBytesResult]
 
+	// Local Storage Metrics aggregated exclusively on NodeUID
 	QueryKMLocalStorageUsedAvg(start, end time.Time) *Future[NodeUIDValueResult]
 	QueryKMLocalStorageUsedMax(start, end time.Time) *Future[NodeUIDValueResult]
 	QueryKMLocalStorageBytes(start, end time.Time) *Future[UIDValueResult]
@@ -43,6 +254,7 @@ type MetricsQuerier interface {
 
 	// Cluster Management
 	QueryClusterInfo(start, end time.Time) *Future[ClusterInfoResult]
+	QueryClusterKubeModelVersion(start, end time.Time) *Future[ClusterKubeModelVersionResult]
 	QueryClusterUptime(start, end time.Time) *Future[UptimeResult]
 	QueryClusterManagementDuration(start, end time.Time) *Future[ClusterManagementDurationResult]
 	QueryClusterManagementPricePerHr(start, end time.Time) *Future[ClusterManagementPricePerHrResult]
@@ -97,6 +309,7 @@ type MetricsQuerier interface {
 	QueryPodPVCAllocation(start, end time.Time) *Future[PodPVCAllocationResult]
 	QueryPVCBytesRequested(start, end time.Time) *Future[PVCBytesRequestedResult]
 	QueryPVCInfo(start, end time.Time) *Future[PVCInfoResult]
+	// UID aggregated version of PVCInfo query
 	QueryKMPVCInfo(start, end time.Time) *Future[PVCInfoResult]
 	QueryPVCUptime(start, end time.Time) *Future[UptimeResult]
 	QueryPVCBytesUsedAverage(start, end time.Time) *Future[PVCUIDValueResult]

+ 16 - 0
core/pkg/source/decoders.go

@@ -62,6 +62,7 @@ const (
 	SameZoneLabel        = "same_zone"
 	SameRegionLabel      = "same_region"
 	NatGatewayLabel      = "nat_gateway"
+	KubeModelVersion     = "kubemodel_version"
 )
 
 const (
@@ -671,6 +672,21 @@ func DecodeClusterInfoResult(result *QueryResult) *ClusterInfoResult {
 	}
 }
 
+type ClusterKubeModelVersionResult struct {
+	UID     string
+	Version string
+}
+
+func DecodeClusterKubeModelVersionResult(result *QueryResult) *ClusterKubeModelVersionResult {
+	uid, _ := result.GetString(UIDLabel)
+	version, _ := result.GetString(KubeModelVersion)
+
+	return &ClusterKubeModelVersionResult{
+		UID:     uid,
+		Version: version,
+	}
+}
+
 type ClusterManagementDurationResult struct {
 	UID         string
 	Cluster     string

+ 1084 - 0
core/pkg/source/mock.go

@@ -0,0 +1,1084 @@
+package source
+
+import (
+	"time"
+
+	"github.com/julienschmidt/httprouter"
+	"github.com/opencost/opencost/core/pkg/clusters"
+	"github.com/opencost/opencost/core/pkg/diagnostics"
+)
+
+//--------------------------------------------------------------------------
+//  Mock MetricsQuerier (per-method override map, NoOp fallback)
+//--------------------------------------------------------------------------
+
+var _ MetricsQuerier = (*MockMetricsQuerier)(nil)
+
+// MockMetricsQuerier is a test double for MetricsQuerier. Set a field to a
+// non-nil *Future[T] to override what a specific method returns; unset methods
+// fall back to NoOpMetricsQuerier (empty results, no error).
+type MockMetricsQuerier struct {
+	noop      *NoOpMetricsQuerier
+	overrides map[string]any
+}
+
+func NewMockMetricsQuerier() *MockMetricsQuerier {
+	return &MockMetricsQuerier{
+		noop:      NewNoOpMetricsQuerier(),
+		overrides: make(map[string]any),
+	}
+}
+
+// SetOverride registers a []*T result slice to be returned when method is called.
+// Pass nil to clear an override.
+func (m *MockMetricsQuerier) SetOverride(method string, result any) {
+	if result == nil {
+		delete(m.overrides, method)
+		return
+	}
+	m.overrides[method] = result
+}
+
+// getFutureFromOverride looks up method in overrides and, if found, wraps the
+// stored []*T slice in a Future via NewFutureFrom. Falls back to fallback otherwise.
+func getFutureFromOverride[T any](overrides map[string]any, method string, fallback func() *Future[T]) *Future[T] {
+	if v, ok := overrides[method]; ok {
+		if results, ok := v.([]*T); ok {
+			return NewFutureFrom(results)
+		}
+	}
+	return fallback()
+}
+
+// Local Cluster Disks
+
+func (m *MockMetricsQuerier) QueryLocalStorageActiveMinutes(start, end time.Time) *Future[LocalStorageActiveMinutesResult] {
+	return getFutureFromOverride(m.overrides, QueryLocalStorageActiveMinutes, func() *Future[LocalStorageActiveMinutesResult] {
+		return m.noop.QueryLocalStorageActiveMinutes(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryLocalStorageUsedAvg(start, end time.Time) *Future[LocalStorageUsedAvgResult] {
+	return getFutureFromOverride(m.overrides, QueryLocalStorageUsedAvg, func() *Future[LocalStorageUsedAvgResult] {
+		return m.noop.QueryLocalStorageUsedAvg(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryLocalStorageUsedMax(start, end time.Time) *Future[LocalStorageUsedMaxResult] {
+	return getFutureFromOverride(m.overrides, QueryLocalStorageUsedMax, func() *Future[LocalStorageUsedMaxResult] {
+		return m.noop.QueryLocalStorageUsedMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryLocalStorageBytes(start, end time.Time) *Future[LocalStorageBytesResult] {
+	return getFutureFromOverride(m.overrides, QueryLocalStorageBytes, func() *Future[LocalStorageBytesResult] {
+		return m.noop.QueryLocalStorageBytes(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryKMLocalStorageUsedAvg(start, end time.Time) *Future[NodeUIDValueResult] {
+	return getFutureFromOverride(m.overrides, QueryKMLocalStorageUsedAvg, func() *Future[NodeUIDValueResult] {
+		return m.noop.QueryKMLocalStorageUsedAvg(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryKMLocalStorageUsedMax(start, end time.Time) *Future[NodeUIDValueResult] {
+	return getFutureFromOverride(m.overrides, QueryKMLocalStorageUsedMax, func() *Future[NodeUIDValueResult] {
+		return m.noop.QueryKMLocalStorageUsedMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryKMLocalStorageBytes(start, end time.Time) *Future[UIDValueResult] {
+	return getFutureFromOverride(m.overrides, QueryKMLocalStorageBytes, func() *Future[UIDValueResult] {
+		return m.noop.QueryKMLocalStorageBytes(start, end)
+	})
+}
+
+// Nodes
+
+func (m *MockMetricsQuerier) QueryNodeInfo(start, end time.Time) *Future[NodeInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeInfo, func() *Future[NodeInfoResult] {
+		return m.noop.QueryNodeInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryNodeUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeActiveMinutes(start, end time.Time) *Future[NodeActiveMinutesResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeActiveMinutes, func() *Future[NodeActiveMinutesResult] {
+		return m.noop.QueryNodeActiveMinutes(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeCPUCoresCapacity(start, end time.Time) *Future[NodeCPUCoresCapacityResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeCPUCoresCapacity, func() *Future[NodeCPUCoresCapacityResult] {
+		return m.noop.QueryNodeCPUCoresCapacity(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeCPUCoresAllocatable(start, end time.Time) *Future[NodeCPUCoresAllocatableResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeCPUCoresAllocatable, func() *Future[NodeCPUCoresAllocatableResult] {
+		return m.noop.QueryNodeCPUCoresAllocatable(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeRAMBytesCapacity(start, end time.Time) *Future[NodeRAMBytesCapacityResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeRAMBytesCapacity, func() *Future[NodeRAMBytesCapacityResult] {
+		return m.noop.QueryNodeRAMBytesCapacity(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeRAMBytesAllocatable(start, end time.Time) *Future[NodeRAMBytesAllocatableResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeRAMBytesAllocatable, func() *Future[NodeRAMBytesAllocatableResult] {
+		return m.noop.QueryNodeRAMBytesAllocatable(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeGPUCount(start, end time.Time) *Future[NodeGPUCountResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeGPUCount, func() *Future[NodeGPUCountResult] {
+		return m.noop.QueryNodeGPUCount(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeCPUModeTotal(start, end time.Time) *Future[NodeCPUModeTotalResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeCPUModeTotal, func() *Future[NodeCPUModeTotalResult] {
+		return m.noop.QueryNodeCPUModeTotal(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeIsSpot(start, end time.Time) *Future[NodeIsSpotResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeIsSpot, func() *Future[NodeIsSpotResult] {
+		return m.noop.QueryNodeIsSpot(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeRAMSystemPercent(start, end time.Time) *Future[NodeRAMSystemPercentResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeRAMSystemPercent, func() *Future[NodeRAMSystemPercentResult] {
+		return m.noop.QueryNodeRAMSystemPercent(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeRAMUserPercent(start, end time.Time) *Future[NodeRAMUserPercentResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeRAMUserPercent, func() *Future[NodeRAMUserPercentResult] {
+		return m.noop.QueryNodeRAMUserPercent(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeResourceCapacities(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeResourceCapacities, func() *Future[ResourceResult] {
+		return m.noop.QueryNodeResourceCapacities(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeResourcesAllocatable(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeResourcesAllocatable, func() *Future[ResourceResult] {
+		return m.noop.QueryNodeResourcesAllocatable(start, end)
+	})
+}
+
+// Load Balancers
+
+func (m *MockMetricsQuerier) QueryLBActiveMinutes(start, end time.Time) *Future[LBActiveMinutesResult] {
+	return getFutureFromOverride(m.overrides, QueryLBActiveMinutes, func() *Future[LBActiveMinutesResult] {
+		return m.noop.QueryLBActiveMinutes(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryLBPricePerHr(start, end time.Time) *Future[LBPricePerHrResult] {
+	return getFutureFromOverride(m.overrides, QueryLBPricePerHr, func() *Future[LBPricePerHrResult] {
+		return m.noop.QueryLBPricePerHr(start, end)
+	})
+}
+
+// Cluster Management
+
+func (m *MockMetricsQuerier) QueryClusterInfo(start, end time.Time) *Future[ClusterInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryClusterInfo, func() *Future[ClusterInfoResult] {
+		return m.noop.QueryClusterInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryClusterKubeModelVersion(start, end time.Time) *Future[ClusterKubeModelVersionResult] {
+	return getFutureFromOverride(m.overrides, QueryClusterKubeModelVersion, func() *Future[ClusterKubeModelVersionResult] {
+		return m.noop.QueryClusterKubeModelVersion(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryClusterUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryClusterUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryClusterUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryClusterManagementDuration(start, end time.Time) *Future[ClusterManagementDurationResult] {
+	return getFutureFromOverride(m.overrides, QueryClusterManagementDuration, func() *Future[ClusterManagementDurationResult] {
+		return m.noop.QueryClusterManagementDuration(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryClusterManagementPricePerHr(start, end time.Time) *Future[ClusterManagementPricePerHrResult] {
+	return getFutureFromOverride(m.overrides, QueryClusterManagementPricePerHr, func() *Future[ClusterManagementPricePerHrResult] {
+		return m.noop.QueryClusterManagementPricePerHr(start, end)
+	})
+}
+
+// Pods
+
+func (m *MockMetricsQuerier) QueryPods(start, end time.Time) *Future[PodsResult] {
+	return getFutureFromOverride(m.overrides, QueryPods, func() *Future[PodsResult] {
+		return m.noop.QueryPods(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPodsUID(start, end time.Time) *Future[PodsResult] {
+	return getFutureFromOverride(m.overrides, QueryPodsUID, func() *Future[PodsResult] {
+		return m.noop.QueryPodsUID(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPodInfo(start, end time.Time) *Future[PodInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryPodInfo, func() *Future[PodInfoResult] {
+		return m.noop.QueryPodInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPodUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryPodUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryPodUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPodOwners(start, end time.Time) *Future[OwnerResult] {
+	return getFutureFromOverride(m.overrides, QueryPodOwners, func() *Future[OwnerResult] {
+		return m.noop.QueryPodOwners(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPodPVCVolumes(start, end time.Time) *Future[PodPVCVolumeResult] {
+	return getFutureFromOverride(m.overrides, QueryPodPVCVolumes, func() *Future[PodPVCVolumeResult] {
+		return m.noop.QueryPodPVCVolumes(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPodNetworkEgressBytes(start, end time.Time) *Future[PodNetworkBytesResult] {
+	return getFutureFromOverride(m.overrides, QueryPodNetworkEgressBytes, func() *Future[PodNetworkBytesResult] {
+		return m.noop.QueryPodNetworkEgressBytes(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPodNetworkIngressBytes(start, end time.Time) *Future[PodNetworkBytesResult] {
+	return getFutureFromOverride(m.overrides, QueryPodNetworkIngressBytes, func() *Future[PodNetworkBytesResult] {
+		return m.noop.QueryPodNetworkIngressBytes(start, end)
+	})
+}
+
+// Container
+
+func (m *MockMetricsQuerier) QueryContainerUptime(start, end time.Time) *Future[ContainerUptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryContainerUptime, func() *Future[ContainerUptimeResult] {
+		return m.noop.QueryContainerUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryContainerResourceRequests(start, end time.Time) *Future[ContainerResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryContainerResourceRequests, func() *Future[ContainerResourceResult] {
+		return m.noop.QueryContainerResourceRequests(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryContainerResourceLimits(start, end time.Time) *Future[ContainerResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryContainerResourceLimits, func() *Future[ContainerResourceResult] {
+		return m.noop.QueryContainerResourceLimits(start, end)
+	})
+}
+
+// RAM
+
+func (m *MockMetricsQuerier) QueryRAMBytesAllocated(start, end time.Time) *Future[RAMBytesAllocatedResult] {
+	return getFutureFromOverride(m.overrides, QueryRAMBytesAllocated, func() *Future[RAMBytesAllocatedResult] {
+		return m.noop.QueryRAMBytesAllocated(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryRAMRequests(start, end time.Time) *Future[RAMRequestsResult] {
+	return getFutureFromOverride(m.overrides, QueryRAMRequests, func() *Future[RAMRequestsResult] {
+		return m.noop.QueryRAMRequests(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryRAMLimits(start, end time.Time) *Future[RAMLimitsResult] {
+	return getFutureFromOverride(m.overrides, QueryRAMLimits, func() *Future[RAMLimitsResult] {
+		return m.noop.QueryRAMLimits(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryRAMUsageAvg(start, end time.Time) *Future[RAMUsageAvgResult] {
+	return getFutureFromOverride(m.overrides, QueryRAMUsageAvg, func() *Future[RAMUsageAvgResult] {
+		return m.noop.QueryRAMUsageAvg(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryRAMUsageMax(start, end time.Time) *Future[RAMUsageMaxResult] {
+	return getFutureFromOverride(m.overrides, QueryRAMUsageMax, func() *Future[RAMUsageMaxResult] {
+		return m.noop.QueryRAMUsageMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeRAMPricePerGiBHr(start, end time.Time) *Future[NodeRAMPricePerGiBHrResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeRAMPricePerGiBHr, func() *Future[NodeRAMPricePerGiBHrResult] {
+		return m.noop.QueryNodeRAMPricePerGiBHr(start, end)
+	})
+}
+
+// CPU
+
+func (m *MockMetricsQuerier) QueryCPUCoresAllocated(start, end time.Time) *Future[CPUCoresAllocatedResult] {
+	return getFutureFromOverride(m.overrides, QueryCPUCoresAllocated, func() *Future[CPUCoresAllocatedResult] {
+		return m.noop.QueryCPUCoresAllocated(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryCPURequests(start, end time.Time) *Future[CPURequestsResult] {
+	return getFutureFromOverride(m.overrides, QueryCPURequests, func() *Future[CPURequestsResult] {
+		return m.noop.QueryCPURequests(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryCPULimits(start, end time.Time) *Future[CPULimitsResult] {
+	return getFutureFromOverride(m.overrides, QueryCPULimits, func() *Future[CPULimitsResult] {
+		return m.noop.QueryCPULimits(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryCPUUsageAvg(start, end time.Time) *Future[CPUUsageAvgResult] {
+	return getFutureFromOverride(m.overrides, QueryCPUUsageAvg, func() *Future[CPUUsageAvgResult] {
+		return m.noop.QueryCPUUsageAvg(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryCPUUsageMax(start, end time.Time) *Future[CPUUsageMaxResult] {
+	return getFutureFromOverride(m.overrides, QueryCPUUsageMax, func() *Future[CPUUsageMaxResult] {
+		return m.noop.QueryCPUUsageMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeCPUPricePerHr(start, end time.Time) *Future[NodeCPUPricePerHrResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeCPUPricePerHr, func() *Future[NodeCPUPricePerHrResult] {
+		return m.noop.QueryNodeCPUPricePerHr(start, end)
+	})
+}
+
+// GPU
+
+func (m *MockMetricsQuerier) QueryGPUsAllocated(start, end time.Time) *Future[GPUsAllocatedResult] {
+	return getFutureFromOverride(m.overrides, QueryGPUsAllocated, func() *Future[GPUsAllocatedResult] {
+		return m.noop.QueryGPUsAllocated(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryGPUsRequested(start, end time.Time) *Future[GPUsRequestedResult] {
+	return getFutureFromOverride(m.overrides, QueryGPUsRequested, func() *Future[GPUsRequestedResult] {
+		return m.noop.QueryGPUsRequested(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryGPUsUsageAvg(start, end time.Time) *Future[GPUsUsageAvgResult] {
+	return getFutureFromOverride(m.overrides, QueryGPUsUsageAvg, func() *Future[GPUsUsageAvgResult] {
+		return m.noop.QueryGPUsUsageAvg(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryGPUsUsageMax(start, end time.Time) *Future[GPUsUsageMaxResult] {
+	return getFutureFromOverride(m.overrides, QueryGPUsUsageMax, func() *Future[GPUsUsageMaxResult] {
+		return m.noop.QueryGPUsUsageMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNodeGPUPricePerHr(start, end time.Time) *Future[NodeGPUPricePerHrResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeGPUPricePerHr, func() *Future[NodeGPUPricePerHrResult] {
+		return m.noop.QueryNodeGPUPricePerHr(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryGPUInfo(start, end time.Time) *Future[GPUInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryGPUInfo, func() *Future[GPUInfoResult] {
+		return m.noop.QueryGPUInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryIsGPUShared(start, end time.Time) *Future[IsGPUSharedResult] {
+	return getFutureFromOverride(m.overrides, QueryIsGPUShared, func() *Future[IsGPUSharedResult] {
+		return m.noop.QueryIsGPUShared(start, end)
+	})
+}
+
+// Device
+
+func (m *MockMetricsQuerier) QueryDCGMDeviceInfo(start, end time.Time) *Future[DCGMDeviceInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryDCGMDeviceInfo, func() *Future[DCGMDeviceInfoResult] {
+		return m.noop.QueryDCGMDeviceInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryDCGMDeviceUptime(start, end time.Time) *Future[DCGMDeviceUptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryDCGMDeviceUptime, func() *Future[DCGMDeviceUptimeResult] {
+		return m.noop.QueryDCGMDeviceUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryDCGMContainerUsageAvg(start, end time.Time) *Future[DCGMDeviceContainerUsageResult] {
+	return getFutureFromOverride(m.overrides, QueryDCGMContainerUsageAvg, func() *Future[DCGMDeviceContainerUsageResult] {
+		return m.noop.QueryDCGMContainerUsageAvg(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryDCGMContainerUsageMax(start, end time.Time) *Future[DCGMDeviceContainerUsageResult] {
+	return getFutureFromOverride(m.overrides, QueryDCGMContainerUsageMax, func() *Future[DCGMDeviceContainerUsageResult] {
+		return m.noop.QueryDCGMContainerUsageMax(start, end)
+	})
+}
+
+// PVC
+
+func (m *MockMetricsQuerier) QueryPodPVCAllocation(start, end time.Time) *Future[PodPVCAllocationResult] {
+	return getFutureFromOverride(m.overrides, QueryPodPVCAllocation, func() *Future[PodPVCAllocationResult] {
+		return m.noop.QueryPodPVCAllocation(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPVCBytesRequested(start, end time.Time) *Future[PVCBytesRequestedResult] {
+	return getFutureFromOverride(m.overrides, QueryPVCBytesRequested, func() *Future[PVCBytesRequestedResult] {
+		return m.noop.QueryPVCBytesRequested(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPVCInfo(start, end time.Time) *Future[PVCInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryPVCInfo, func() *Future[PVCInfoResult] {
+		return m.noop.QueryPVCInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryKMPVCInfo(start, end time.Time) *Future[PVCInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryKMPVCInfo, func() *Future[PVCInfoResult] {
+		return m.noop.QueryKMPVCInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPVCUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryPVCUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryPVCUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPVCBytesUsedAverage(start, end time.Time) *Future[PVCUIDValueResult] {
+	return getFutureFromOverride(m.overrides, QueryPVCBytesUsedAverage, func() *Future[PVCUIDValueResult] {
+		return m.noop.QueryPVCBytesUsedAverage(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPVCBytesUsedMax(start, end time.Time) *Future[PVCUIDValueResult] {
+	return getFutureFromOverride(m.overrides, QueryPVCBytesUsedMax, func() *Future[PVCUIDValueResult] {
+		return m.noop.QueryPVCBytesUsedMax(start, end)
+	})
+}
+
+// PV
+
+func (m *MockMetricsQuerier) QueryPVBytes(start, end time.Time) *Future[PVBytesResult] {
+	return getFutureFromOverride(m.overrides, QueryPVBytes, func() *Future[PVBytesResult] {
+		return m.noop.QueryPVBytes(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPVPricePerGiBHour(start, end time.Time) *Future[PVPricePerGiBHourResult] {
+	return getFutureFromOverride(m.overrides, QueryPVPricePerGiBHour, func() *Future[PVPricePerGiBHourResult] {
+		return m.noop.QueryPVPricePerGiBHour(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPVInfo(start, end time.Time) *Future[PVInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryPVInfo, func() *Future[PVInfoResult] {
+		return m.noop.QueryPVInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPVActiveMinutes(start, end time.Time) *Future[PVActiveMinutesResult] {
+	return getFutureFromOverride(m.overrides, QueryPVActiveMinutes, func() *Future[PVActiveMinutesResult] {
+		return m.noop.QueryPVActiveMinutes(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPVUsedAverage(start, end time.Time) *Future[PVUsedAvgResult] {
+	return getFutureFromOverride(m.overrides, QueryPVUsedAverage, func() *Future[PVUsedAvgResult] {
+		return m.noop.QueryPVUsedAverage(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPVUsedMax(start, end time.Time) *Future[PVUsedMaxResult] {
+	return getFutureFromOverride(m.overrides, QueryPVUsedMax, func() *Future[PVUsedMaxResult] {
+		return m.noop.QueryPVUsedMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryKMPVInfo(start, end time.Time) *Future[PVInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryKMPVInfo, func() *Future[PVInfoResult] {
+		return m.noop.QueryKMPVInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPVUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryPVUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryPVUptime(start, end)
+	})
+}
+
+// Deployment
+
+func (m *MockMetricsQuerier) QueryDeploymentInfo(start, end time.Time) *Future[DeploymentInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryDeploymentInfo, func() *Future[DeploymentInfoResult] {
+		return m.noop.QueryDeploymentInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryDeploymentUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryDeploymentUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryDeploymentUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryDeploymentLabels(start, end time.Time) *Future[LabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryDeploymentLabels, func() *Future[LabelsResult] {
+		return m.noop.QueryDeploymentLabels(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryDeploymentAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return getFutureFromOverride(m.overrides, QueryDeploymentAnnotations, func() *Future[AnnotationsResult] {
+		return m.noop.QueryDeploymentAnnotations(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryDeploymentMatchLabels(start, end time.Time) *Future[DeploymentLabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryDeploymentMatchLabels, func() *Future[DeploymentLabelsResult] {
+		return m.noop.QueryDeploymentMatchLabels(start, end)
+	})
+}
+
+// StatefulSet
+
+func (m *MockMetricsQuerier) QueryStatefulSetInfo(start, end time.Time) *Future[StatefulSetInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryStatefulSetInfo, func() *Future[StatefulSetInfoResult] {
+		return m.noop.QueryStatefulSetInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryStatefulSetUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryStatefulSetUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryStatefulSetUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryStatefulSetLabels(start, end time.Time) *Future[LabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryStatefulSetLabels, func() *Future[LabelsResult] {
+		return m.noop.QueryStatefulSetLabels(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryStatefulSetAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return getFutureFromOverride(m.overrides, QueryStatefulSetAnnotations, func() *Future[AnnotationsResult] {
+		return m.noop.QueryStatefulSetAnnotations(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryStatefulSetMatchLabels(start, end time.Time) *Future[StatefulSetLabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryStatefulSetMatchLabels, func() *Future[StatefulSetLabelsResult] {
+		return m.noop.QueryStatefulSetMatchLabels(start, end)
+	})
+}
+
+// DaemonSet
+
+func (m *MockMetricsQuerier) QueryDaemonSetInfo(start, end time.Time) *Future[DaemonSetInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryDaemonSetInfo, func() *Future[DaemonSetInfoResult] {
+		return m.noop.QueryDaemonSetInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryDaemonSetUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryDaemonSetUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryDaemonSetUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryDaemonSetLabels(start, end time.Time) *Future[LabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryDaemonSetLabels, func() *Future[LabelsResult] {
+		return m.noop.QueryDaemonSetLabels(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryDaemonSetAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return getFutureFromOverride(m.overrides, QueryDaemonSetAnnotations, func() *Future[AnnotationsResult] {
+		return m.noop.QueryDaemonSetAnnotations(start, end)
+	})
+}
+
+// Job
+
+func (m *MockMetricsQuerier) QueryJobInfo(start, end time.Time) *Future[JobInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryJobInfo, func() *Future[JobInfoResult] {
+		return m.noop.QueryJobInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryJobUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryJobUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryJobUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryJobLabels(start, end time.Time) *Future[LabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryJobLabels, func() *Future[LabelsResult] {
+		return m.noop.QueryJobLabels(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryJobAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return getFutureFromOverride(m.overrides, QueryJobAnnotations, func() *Future[AnnotationsResult] {
+		return m.noop.QueryJobAnnotations(start, end)
+	})
+}
+
+// CronJob
+
+func (m *MockMetricsQuerier) QueryCronJobInfo(start, end time.Time) *Future[CronJobInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryCronJobInfo, func() *Future[CronJobInfoResult] {
+		return m.noop.QueryCronJobInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryCronJobUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryCronJobUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryCronJobUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryCronJobLabels(start, end time.Time) *Future[LabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryCronJobLabels, func() *Future[LabelsResult] {
+		return m.noop.QueryCronJobLabels(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryCronJobAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return getFutureFromOverride(m.overrides, QueryCronJobAnnotations, func() *Future[AnnotationsResult] {
+		return m.noop.QueryCronJobAnnotations(start, end)
+	})
+}
+
+// ReplicaSet
+
+func (m *MockMetricsQuerier) QueryReplicaSetInfo(start, end time.Time) *Future[ReplicaSetInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryReplicaSetInfo, func() *Future[ReplicaSetInfoResult] {
+		return m.noop.QueryReplicaSetInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryReplicaSetUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryReplicaSetUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryReplicaSetUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryReplicaSetLabels(start, end time.Time) *Future[LabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryReplicaSetLabels, func() *Future[LabelsResult] {
+		return m.noop.QueryReplicaSetLabels(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryReplicaSetAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return getFutureFromOverride(m.overrides, QueryReplicaSetAnnotations, func() *Future[AnnotationsResult] {
+		return m.noop.QueryReplicaSetAnnotations(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryReplicaSetOwners(start, end time.Time) *Future[OwnerResult] {
+	return getFutureFromOverride(m.overrides, QueryReplicaSetOwners, func() *Future[OwnerResult] {
+		return m.noop.QueryReplicaSetOwners(start, end)
+	})
+}
+
+// Namespace
+
+func (m *MockMetricsQuerier) QueryNamespaceInfo(start, end time.Time) *Future[NamespaceInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryNamespaceInfo, func() *Future[NamespaceInfoResult] {
+		return m.noop.QueryNamespaceInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNamespaceUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryNamespaceUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryNamespaceUptime(start, end)
+	})
+}
+
+// Service
+
+func (m *MockMetricsQuerier) QueryServiceInfo(start, end time.Time) *Future[ServiceInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryServiceInfo, func() *Future[ServiceInfoResult] {
+		return m.noop.QueryServiceInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryServiceUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryServiceUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryServiceUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryServiceSelectorLabels(start, end time.Time) *Future[ServiceLabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryServiceSelectorLabels, func() *Future[ServiceLabelsResult] {
+		return m.noop.QueryServiceSelectorLabels(start, end)
+	})
+}
+
+// Network Egress
+
+func (m *MockMetricsQuerier) QueryNetZoneGiB(start, end time.Time) *Future[NetZoneGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetZoneGiB, func() *Future[NetZoneGiBResult] {
+		return m.noop.QueryNetZoneGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetZonePricePerGiB(start, end time.Time) *Future[NetZonePricePerGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetZonePricePerGiB, func() *Future[NetZonePricePerGiBResult] {
+		return m.noop.QueryNetZonePricePerGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetRegionGiB(start, end time.Time) *Future[NetRegionGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetRegionGiB, func() *Future[NetRegionGiBResult] {
+		return m.noop.QueryNetRegionGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetRegionPricePerGiB(start, end time.Time) *Future[NetRegionPricePerGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetRegionPricePerGiB, func() *Future[NetRegionPricePerGiBResult] {
+		return m.noop.QueryNetRegionPricePerGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetInternetGiB(start, end time.Time) *Future[NetInternetGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetInternetGiB, func() *Future[NetInternetGiBResult] {
+		return m.noop.QueryNetInternetGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetInternetPricePerGiB(start, end time.Time) *Future[NetInternetPricePerGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetInternetPricePerGiB, func() *Future[NetInternetPricePerGiBResult] {
+		return m.noop.QueryNetInternetPricePerGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetInternetServiceGiB(start, end time.Time) *Future[NetInternetServiceGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetInternetServiceGiB, func() *Future[NetInternetServiceGiBResult] {
+		return m.noop.QueryNetInternetServiceGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetNatGatewayPricePerGiB(start, end time.Time) *Future[NetNatGatewayPricePerGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetNatGatewayPricePerGiB, func() *Future[NetNatGatewayPricePerGiBResult] {
+		return m.noop.QueryNetNatGatewayPricePerGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetNatGatewayGiB(start, end time.Time) *Future[NetNatGatewayGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetNatGatewayGiB, func() *Future[NetNatGatewayGiBResult] {
+		return m.noop.QueryNetNatGatewayGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetTransferBytes(start, end time.Time) *Future[NetTransferBytesResult] {
+	return getFutureFromOverride(m.overrides, QueryNetTransferBytes, func() *Future[NetTransferBytesResult] {
+		return m.noop.QueryNetTransferBytes(start, end)
+	})
+}
+
+// Network Ingress
+
+func (m *MockMetricsQuerier) QueryNetZoneIngressGiB(start, end time.Time) *Future[NetZoneIngressGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetZoneIngressGiB, func() *Future[NetZoneIngressGiBResult] {
+		return m.noop.QueryNetZoneIngressGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetRegionIngressGiB(start, end time.Time) *Future[NetRegionIngressGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetRegionIngressGiB, func() *Future[NetRegionIngressGiBResult] {
+		return m.noop.QueryNetRegionIngressGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetInternetIngressGiB(start, end time.Time) *Future[NetInternetIngressGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetInternetIngressGiB, func() *Future[NetInternetIngressGiBResult] {
+		return m.noop.QueryNetInternetIngressGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetInternetServiceIngressGiB(start, end time.Time) *Future[NetInternetServiceIngressGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetInternetServiceIngressGiB, func() *Future[NetInternetServiceIngressGiBResult] {
+		return m.noop.QueryNetInternetServiceIngressGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetNatGatewayIngressPricePerGiB(start, end time.Time) *Future[NetNatGatewayPricePerGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetNatGatewayIngressPricePerGiB, func() *Future[NetNatGatewayPricePerGiBResult] {
+		return m.noop.QueryNetNatGatewayIngressPricePerGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetNatGatewayIngressGiB(start, end time.Time) *Future[NetNatGatewayIngressGiBResult] {
+	return getFutureFromOverride(m.overrides, QueryNetNatGatewayIngressGiB, func() *Future[NetNatGatewayIngressGiBResult] {
+		return m.noop.QueryNetNatGatewayIngressGiB(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNetReceiveBytes(start, end time.Time) *Future[NetReceiveBytesResult] {
+	return getFutureFromOverride(m.overrides, QueryNetReceiveBytes, func() *Future[NetReceiveBytesResult] {
+		return m.noop.QueryNetReceiveBytes(start, end)
+	})
+}
+
+// Annotations
+
+func (m *MockMetricsQuerier) QueryNamespaceAnnotations(start, end time.Time) *Future[NamespaceAnnotationsResult] {
+	return getFutureFromOverride(m.overrides, QueryNamespaceAnnotations, func() *Future[NamespaceAnnotationsResult] {
+		return m.noop.QueryNamespaceAnnotations(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPodAnnotations(start, end time.Time) *Future[PodAnnotationsResult] {
+	return getFutureFromOverride(m.overrides, QueryPodAnnotations, func() *Future[PodAnnotationsResult] {
+		return m.noop.QueryPodAnnotations(start, end)
+	})
+}
+
+// Labels
+
+func (m *MockMetricsQuerier) QueryNodeLabels(start, end time.Time) *Future[NodeLabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryNodeLabels, func() *Future[NodeLabelsResult] {
+		return m.noop.QueryNodeLabels(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryNamespaceLabels(start, end time.Time) *Future[NamespaceLabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryNamespaceLabels, func() *Future[NamespaceLabelsResult] {
+		return m.noop.QueryNamespaceLabels(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPodLabels(start, end time.Time) *Future[PodLabelsResult] {
+	return getFutureFromOverride(m.overrides, QueryPodLabels, func() *Future[PodLabelsResult] {
+		return m.noop.QueryPodLabels(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPodsWithDaemonSetOwner(start, end time.Time) *Future[PodsWithDaemonSetOwnerResult] {
+	return getFutureFromOverride(m.overrides, QueryPodsWithDaemonSetOwner, func() *Future[PodsWithDaemonSetOwnerResult] {
+		return m.noop.QueryPodsWithDaemonSetOwner(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryPodsWithJobOwner(start, end time.Time) *Future[PodsWithJobOwnerResult] {
+	return getFutureFromOverride(m.overrides, QueryPodsWithJobOwner, func() *Future[PodsWithJobOwnerResult] {
+		return m.noop.QueryPodsWithJobOwner(start, end)
+	})
+}
+
+// ReplicaSet -> Controller mapping
+
+func (m *MockMetricsQuerier) QueryPodsWithReplicaSetOwner(start, end time.Time) *Future[PodsWithReplicaSetOwnerResult] {
+	return getFutureFromOverride(m.overrides, QueryPodsWithReplicaSetOwner, func() *Future[PodsWithReplicaSetOwnerResult] {
+		return m.noop.QueryPodsWithReplicaSetOwner(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryReplicaSetsWithoutOwners(start, end time.Time) *Future[ReplicaSetsWithoutOwnersResult] {
+	return getFutureFromOverride(m.overrides, QueryReplicaSetsWithoutOwners, func() *Future[ReplicaSetsWithoutOwnersResult] {
+		return m.noop.QueryReplicaSetsWithoutOwners(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryReplicaSetsWithRollout(start, end time.Time) *Future[ReplicaSetsWithRolloutResult] {
+	return getFutureFromOverride(m.overrides, QueryReplicaSetsWithRollout, func() *Future[ReplicaSetsWithRolloutResult] {
+		return m.noop.QueryReplicaSetsWithRollout(start, end)
+	})
+}
+
+// ResourceQuotas
+
+func (m *MockMetricsQuerier) QueryResourceQuotaInfo(start, end time.Time) *Future[ResourceQuotaInfoResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaInfo, func() *Future[ResourceQuotaInfoResult] {
+		return m.noop.QueryResourceQuotaInfo(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaUptime(start, end time.Time) *Future[UptimeResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaUptime, func() *Future[UptimeResult] {
+		return m.noop.QueryResourceQuotaUptime(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaSpecCPURequestAverage(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaSpecCPURequestAverage, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaSpecCPURequestAverage(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaSpecCPURequestMax(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaSpecCPURequestMax, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaSpecCPURequestMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaSpecRAMRequestAverage(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaSpecRAMRequestAverage, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaSpecRAMRequestAverage(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaSpecRAMRequestMax(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaSpecRAMRequestMax, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaSpecRAMRequestMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaSpecCPULimitAverage(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaSpecCPULimitAverage, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaSpecCPULimitAverage(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaSpecCPULimitMax(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaSpecCPULimitMax, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaSpecCPULimitMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaSpecRAMLimitAverage(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaSpecRAMLimitAverage, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaSpecRAMLimitAverage(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaSpecRAMLimitMax(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaSpecRAMLimitMax, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaSpecRAMLimitMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaStatusUsedCPURequestAverage(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaStatusUsedCPURequestAverage, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaStatusUsedCPURequestAverage(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaStatusUsedCPURequestMax(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaStatusUsedCPURequestMax, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaStatusUsedCPURequestMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaStatusUsedRAMRequestAverage(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaStatusUsedRAMRequestAverage, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaStatusUsedRAMRequestAverage(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaStatusUsedRAMRequestMax(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaStatusUsedRAMRequestMax, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaStatusUsedRAMRequestMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaStatusUsedCPULimitAverage(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaStatusUsedCPULimitAverage, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaStatusUsedCPULimitAverage(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaStatusUsedCPULimitMax(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaStatusUsedCPULimitMax, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaStatusUsedCPULimitMax(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaStatusUsedRAMLimitAverage(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaStatusUsedRAMLimitAverage, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaStatusUsedRAMLimitAverage(start, end)
+	})
+}
+
+func (m *MockMetricsQuerier) QueryResourceQuotaStatusUsedRAMLimitMax(start, end time.Time) *Future[ResourceResult] {
+	return getFutureFromOverride(m.overrides, QueryResourceQuotaStatusUsedRAMLimitMax, func() *Future[ResourceResult] {
+		return m.noop.QueryResourceQuotaStatusUsedRAMLimitMax(start, end)
+	})
+}
+
+// Data Coverage Query
+
+func (m *MockMetricsQuerier) QueryDataCoverage(limitDays int) (time.Time, time.Time, error) {
+	if v, ok := m.overrides[QueryDataCoverage]; ok {
+		if f, ok := v.(func(int) (time.Time, time.Time, error)); ok {
+			return f(limitDays)
+		}
+	}
+	return m.noop.QueryDataCoverage(limitDays)
+}
+
+//--------------------------------------------------------------------------
+//  Mock OpenCostDataSource
+//--------------------------------------------------------------------------
+
+var _ OpenCostDataSource = (*MockOpenCostDataSource)(nil)
+
+// MockOpenCostDataSource is a minimal OpenCostDataSource for tests. Set fields
+// directly to configure what the data source returns.
+type MockOpenCostDataSource struct {
+	Querier            *MockMetricsQuerier
+	ClusterMapValue    clusters.ClusterMap
+	ClusterInfoValue   clusters.ClusterInfoProvider
+	BatchDurationValue time.Duration
+	ResolutionValue    time.Duration
+}
+
+func NewMockOpenCostDataSource() *MockOpenCostDataSource {
+	return &MockOpenCostDataSource{
+		Querier: NewMockMetricsQuerier(),
+	}
+}
+
+func (m *MockOpenCostDataSource) RegisterEndPoints(_ *httprouter.Router) {}
+
+func (m *MockOpenCostDataSource) RegisterDiagnostics(_ diagnostics.DiagnosticService) {}
+
+func (m *MockOpenCostDataSource) Metrics() MetricsQuerier {
+	return m.Querier
+}
+
+func (m *MockOpenCostDataSource) ClusterMap() clusters.ClusterMap {
+	return m.ClusterMapValue
+}
+
+func (m *MockOpenCostDataSource) ClusterInfo() clusters.ClusterInfoProvider {
+	return m.ClusterInfoValue
+}
+
+func (m *MockOpenCostDataSource) BatchDuration() time.Duration {
+	return m.BatchDurationValue
+}
+
+func (m *MockOpenCostDataSource) Resolution() time.Duration {
+	return m.ResolutionValue
+}

+ 716 - 0
core/pkg/source/noop.go

@@ -0,0 +1,716 @@
+package source
+
+import (
+	"time"
+)
+
+//--------------------------------------------------------------------------
+//  No-Op MetricsQuerier (empty query results)
+//--------------------------------------------------------------------------
+
+// NoOpMetricsQuerier is a no-op implementation of the MetricsQuerier interface
+// that returns empty results for all queries.
+type NoOpMetricsQuerier struct{}
+
+// NewNoOpMetricsQuerier creates a new mock metrics querier
+func NewNoOpMetricsQuerier() *NoOpMetricsQuerier {
+	return &NoOpMetricsQuerier{}
+}
+
+// Local Cluster Disks
+
+func (m *NoOpMetricsQuerier) QueryLocalStorageActiveMinutes(start, end time.Time) *Future[LocalStorageActiveMinutesResult] {
+	return newEmptyResult(DecodeLocalStorageActiveMinutesResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryLocalStorageUsedAvg(start, end time.Time) *Future[LocalStorageUsedAvgResult] {
+	return newEmptyResult(DecodeLocalStorageUsedAvgResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryLocalStorageUsedMax(start, end time.Time) *Future[LocalStorageUsedMaxResult] {
+	return newEmptyResult(DecodeLocalStorageUsedMaxResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryLocalStorageBytes(start, end time.Time) *Future[LocalStorageBytesResult] {
+	return newEmptyResult(DecodeLocalStorageBytesResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryKMLocalStorageUsedAvg(start, end time.Time) *Future[NodeUIDValueResult] {
+	return newEmptyResult(DecodeNodeUIDValueResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryKMLocalStorageUsedMax(start, end time.Time) *Future[NodeUIDValueResult] {
+	return newEmptyResult(DecodeNodeUIDValueResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryKMLocalStorageBytes(start, end time.Time) *Future[UIDValueResult] {
+	return newEmptyResult(DecodeUIDValueResult)
+}
+
+// Nodes
+
+func (m *NoOpMetricsQuerier) QueryNodeInfo(start, end time.Time) *Future[NodeInfoResult] {
+	return newEmptyResult(DecodeNodeInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeActiveMinutes(start, end time.Time) *Future[NodeActiveMinutesResult] {
+	return newEmptyResult(DecodeNodeActiveMinutesResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeCPUCoresCapacity(start, end time.Time) *Future[NodeCPUCoresCapacityResult] {
+	return newEmptyResult(DecodeNodeCPUCoresCapacityResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeCPUCoresAllocatable(start, end time.Time) *Future[NodeCPUCoresAllocatableResult] {
+	return newEmptyResult(DecodeNodeCPUCoresAllocatableResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeRAMBytesCapacity(start, end time.Time) *Future[NodeRAMBytesCapacityResult] {
+	return newEmptyResult(DecodeNodeRAMBytesCapacityResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeRAMBytesAllocatable(start, end time.Time) *Future[NodeRAMBytesAllocatableResult] {
+	return newEmptyResult(DecodeNodeRAMBytesAllocatableResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeGPUCount(start, end time.Time) *Future[NodeGPUCountResult] {
+	return newEmptyResult(DecodeNodeGPUCountResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeCPUModeTotal(start, end time.Time) *Future[NodeCPUModeTotalResult] {
+	return newEmptyResult(DecodeNodeCPUModeTotalResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeIsSpot(start, end time.Time) *Future[NodeIsSpotResult] {
+	return newEmptyResult(DecodeNodeIsSpotResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeRAMSystemPercent(start, end time.Time) *Future[NodeRAMSystemPercentResult] {
+	return newEmptyResult(DecodeNodeRAMSystemPercentResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeRAMUserPercent(start, end time.Time) *Future[NodeRAMUserPercentResult] {
+	return newEmptyResult(DecodeNodeRAMUserPercentResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeResourceCapacities(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeResourcesAllocatable(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+// Load Balancers
+
+func (m *NoOpMetricsQuerier) QueryLBActiveMinutes(start, end time.Time) *Future[LBActiveMinutesResult] {
+	return newEmptyResult(DecodeLBActiveMinutesResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryLBPricePerHr(start, end time.Time) *Future[LBPricePerHrResult] {
+	return newEmptyResult(DecodeLBPricePerHrResult)
+}
+
+// Cluster Management
+
+func (m *NoOpMetricsQuerier) QueryClusterInfo(start, end time.Time) *Future[ClusterInfoResult] {
+	return newEmptyResult(DecodeClusterInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryClusterKubeModelVersion(start, end time.Time) *Future[ClusterKubeModelVersionResult] {
+	return newEmptyResult(DecodeClusterKubeModelVersionResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryClusterUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryClusterManagementDuration(start, end time.Time) *Future[ClusterManagementDurationResult] {
+	return newEmptyResult(DecodeClusterManagementDurationResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryClusterManagementPricePerHr(start, end time.Time) *Future[ClusterManagementPricePerHrResult] {
+	return newEmptyResult(DecodeClusterManagementPricePerHrResult)
+}
+
+// Pods
+
+func (m *NoOpMetricsQuerier) QueryPods(start, end time.Time) *Future[PodsResult] {
+	return newEmptyResult(DecodePodsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPodsUID(start, end time.Time) *Future[PodsResult] {
+	return newEmptyResult(DecodePodsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPodInfo(start, end time.Time) *Future[PodInfoResult] {
+	return newEmptyResult(DecodePodInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPodUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPodOwners(start, end time.Time) *Future[OwnerResult] {
+	return newEmptyResult(DecodeOwnerResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPodPVCVolumes(start, end time.Time) *Future[PodPVCVolumeResult] {
+	return newEmptyResult(DecodePodPVCVolumeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPodNetworkEgressBytes(start, end time.Time) *Future[PodNetworkBytesResult] {
+	return newEmptyResult(DecodePodNetworkBytesResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPodNetworkIngressBytes(start, end time.Time) *Future[PodNetworkBytesResult] {
+	return newEmptyResult(DecodePodNetworkBytesResult)
+}
+
+// Container
+
+func (m *NoOpMetricsQuerier) QueryContainerUptime(start, end time.Time) *Future[ContainerUptimeResult] {
+	return newEmptyResult(DecodeContainerUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryContainerResourceRequests(start, end time.Time) *Future[ContainerResourceResult] {
+	return newEmptyResult(DecodeContainerResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryContainerResourceLimits(start, end time.Time) *Future[ContainerResourceResult] {
+	return newEmptyResult(DecodeContainerResourceResult)
+}
+
+// RAM
+
+func (m *NoOpMetricsQuerier) QueryRAMBytesAllocated(start, end time.Time) *Future[RAMBytesAllocatedResult] {
+	return newEmptyResult(DecodeRAMBytesAllocatedResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryRAMRequests(start, end time.Time) *Future[RAMRequestsResult] {
+	return newEmptyResult(DecodeRAMRequestsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryRAMLimits(start, end time.Time) *Future[RAMLimitsResult] {
+	return newEmptyResult(DecodeRAMLimitsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryRAMUsageAvg(start, end time.Time) *Future[RAMUsageAvgResult] {
+	return newEmptyResult(DecodeRAMUsageAvgResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryRAMUsageMax(start, end time.Time) *Future[RAMUsageMaxResult] {
+	return newEmptyResult(DecodeRAMUsageMaxResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeRAMPricePerGiBHr(start, end time.Time) *Future[NodeRAMPricePerGiBHrResult] {
+	return newEmptyResult(DecodeNodeRAMPricePerGiBHrResult)
+}
+
+// CPU
+
+func (m *NoOpMetricsQuerier) QueryCPUCoresAllocated(start, end time.Time) *Future[CPUCoresAllocatedResult] {
+	return newEmptyResult(DecodeCPUCoresAllocatedResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryCPURequests(start, end time.Time) *Future[CPURequestsResult] {
+	return newEmptyResult(DecodeCPURequestsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryCPULimits(start, end time.Time) *Future[CPULimitsResult] {
+	return newEmptyResult(DecodeCPULimitsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryCPUUsageAvg(start, end time.Time) *Future[CPUUsageAvgResult] {
+	return newEmptyResult(DecodeCPUUsageAvgResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryCPUUsageMax(start, end time.Time) *Future[CPUUsageMaxResult] {
+	return newEmptyResult(DecodeCPUUsageMaxResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeCPUPricePerHr(start, end time.Time) *Future[NodeCPUPricePerHrResult] {
+	return newEmptyResult(DecodeNodeCPUPricePerHrResult)
+}
+
+// GPU
+
+func (m *NoOpMetricsQuerier) QueryGPUsAllocated(start, end time.Time) *Future[GPUsAllocatedResult] {
+	return newEmptyResult(DecodeGPUsAllocatedResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryGPUsRequested(start, end time.Time) *Future[GPUsRequestedResult] {
+	return newEmptyResult(DecodeGPUsRequestedResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryGPUsUsageAvg(start, end time.Time) *Future[GPUsUsageAvgResult] {
+	return newEmptyResult(DecodeGPUsUsageAvgResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryGPUsUsageMax(start, end time.Time) *Future[GPUsUsageMaxResult] {
+	return newEmptyResult(DecodeGPUsUsageMaxResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNodeGPUPricePerHr(start, end time.Time) *Future[NodeGPUPricePerHrResult] {
+	return newEmptyResult(DecodeNodeGPUPricePerHrResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryGPUInfo(start, end time.Time) *Future[GPUInfoResult] {
+	return newEmptyResult(DecodeGPUInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryIsGPUShared(start, end time.Time) *Future[IsGPUSharedResult] {
+	return newEmptyResult(DecodeIsGPUSharedResult)
+}
+
+// Device
+
+func (m *NoOpMetricsQuerier) QueryDCGMDeviceInfo(start, end time.Time) *Future[DCGMDeviceInfoResult] {
+	return newEmptyResult(DecodeDCGMDeviceInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryDCGMDeviceUptime(start, end time.Time) *Future[DCGMDeviceUptimeResult] {
+	return newEmptyResult(DecodeDCGMDeviceUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryDCGMContainerUsageAvg(start, end time.Time) *Future[DCGMDeviceContainerUsageResult] {
+	return newEmptyResult(DecodeDCGMDeviceContainerUsageResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryDCGMContainerUsageMax(start, end time.Time) *Future[DCGMDeviceContainerUsageResult] {
+	return newEmptyResult(DecodeDCGMDeviceContainerUsageResult)
+}
+
+// PVC
+
+func (m *NoOpMetricsQuerier) QueryPodPVCAllocation(start, end time.Time) *Future[PodPVCAllocationResult] {
+	return newEmptyResult(DecodePodPVCAllocationResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPVCBytesRequested(start, end time.Time) *Future[PVCBytesRequestedResult] {
+	return newEmptyResult(DecodePVCBytesRequestedResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPVCInfo(start, end time.Time) *Future[PVCInfoResult] {
+	return newEmptyResult(DecodePVCInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryKMPVCInfo(start, end time.Time) *Future[PVCInfoResult] {
+	return newEmptyResult(DecodePVCInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPVCUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPVCBytesUsedAverage(start, end time.Time) *Future[PVCUIDValueResult] {
+	return newEmptyResult(DecodePVCUIDValueResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPVCBytesUsedMax(start, end time.Time) *Future[PVCUIDValueResult] {
+	return newEmptyResult(DecodePVCUIDValueResult)
+}
+
+// PV
+
+func (m *NoOpMetricsQuerier) QueryPVBytes(start, end time.Time) *Future[PVBytesResult] {
+	return newEmptyResult(DecodePVBytesResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPVPricePerGiBHour(start, end time.Time) *Future[PVPricePerGiBHourResult] {
+	return newEmptyResult(DecodePVPricePerGiBHourResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPVInfo(start, end time.Time) *Future[PVInfoResult] {
+	return newEmptyResult(DecodePVInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPVActiveMinutes(start, end time.Time) *Future[PVActiveMinutesResult] {
+	return newEmptyResult(DecodePVActiveMinutesResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPVUsedAverage(start, end time.Time) *Future[PVUsedAvgResult] {
+	return newEmptyResult(DecodePVUsedAvgResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPVUsedMax(start, end time.Time) *Future[PVUsedMaxResult] {
+	return newEmptyResult(DecodePVUsedMaxResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryKMPVInfo(start, end time.Time) *Future[PVInfoResult] {
+	return newEmptyResult(DecodePVInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPVUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+// Deployment
+
+func (m *NoOpMetricsQuerier) QueryDeploymentInfo(start, end time.Time) *Future[DeploymentInfoResult] {
+	return newEmptyResult(DecodeDeploymentInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryDeploymentUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryDeploymentLabels(start, end time.Time) *Future[LabelsResult] {
+	return newEmptyResult(DecodeLabelsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryDeploymentAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return newEmptyResult(DecodeAnnotationsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryDeploymentMatchLabels(start, end time.Time) *Future[DeploymentLabelsResult] {
+	return newEmptyResult(DecodeDeploymentLabelsResult)
+}
+
+// StatefulSet
+
+func (m *NoOpMetricsQuerier) QueryStatefulSetInfo(start, end time.Time) *Future[StatefulSetInfoResult] {
+	return newEmptyResult(DecodeStatefulSetInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryStatefulSetUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryStatefulSetLabels(start, end time.Time) *Future[LabelsResult] {
+	return newEmptyResult(DecodeLabelsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryStatefulSetAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return newEmptyResult(DecodeAnnotationsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryStatefulSetMatchLabels(start, end time.Time) *Future[StatefulSetLabelsResult] {
+	return newEmptyResult(DecodeStatefulSetLabelsResult)
+}
+
+// DaemonSet
+
+func (m *NoOpMetricsQuerier) QueryDaemonSetInfo(start, end time.Time) *Future[DaemonSetInfoResult] {
+	return newEmptyResult(DecodeDaemonSetInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryDaemonSetUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryDaemonSetLabels(start, end time.Time) *Future[LabelsResult] {
+	return newEmptyResult(DecodeLabelsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryDaemonSetAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return newEmptyResult(DecodeAnnotationsResult)
+}
+
+// Job
+
+func (m *NoOpMetricsQuerier) QueryJobInfo(start, end time.Time) *Future[JobInfoResult] {
+	return newEmptyResult(DecodeJobInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryJobUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryJobLabels(start, end time.Time) *Future[LabelsResult] {
+	return newEmptyResult(DecodeLabelsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryJobAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return newEmptyResult(DecodeAnnotationsResult)
+}
+
+// CronJob
+
+func (m *NoOpMetricsQuerier) QueryCronJobInfo(start, end time.Time) *Future[CronJobInfoResult] {
+	return newEmptyResult(DecodeCronJobInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryCronJobUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryCronJobLabels(start, end time.Time) *Future[LabelsResult] {
+	return newEmptyResult(DecodeLabelsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryCronJobAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return newEmptyResult(DecodeAnnotationsResult)
+}
+
+// ReplicaSet
+
+func (m *NoOpMetricsQuerier) QueryReplicaSetInfo(start, end time.Time) *Future[ReplicaSetInfoResult] {
+	return newEmptyResult(DecodeReplicaSetInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryReplicaSetUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryReplicaSetLabels(start, end time.Time) *Future[LabelsResult] {
+	return newEmptyResult(DecodeLabelsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryReplicaSetAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	return newEmptyResult(DecodeAnnotationsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryReplicaSetOwners(start, end time.Time) *Future[OwnerResult] {
+	return newEmptyResult(DecodeOwnerResult)
+}
+
+// Namespace
+
+func (m *NoOpMetricsQuerier) QueryNamespaceInfo(start, end time.Time) *Future[NamespaceInfoResult] {
+	return newEmptyResult(DecodeNamespaceInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNamespaceUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+// Service
+
+func (m *NoOpMetricsQuerier) QueryServiceInfo(start, end time.Time) *Future[ServiceInfoResult] {
+	return newEmptyResult(DecodeServiceInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryServiceUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryServiceSelectorLabels(start, end time.Time) *Future[ServiceLabelsResult] {
+	return newEmptyResult(DecodeServiceLabelsResult)
+}
+
+// Network Egress
+
+func (m *NoOpMetricsQuerier) QueryNetZoneGiB(start, end time.Time) *Future[NetZoneGiBResult] {
+	return newEmptyResult(DecodeNetZoneGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetZonePricePerGiB(start, end time.Time) *Future[NetZonePricePerGiBResult] {
+	return newEmptyResult(DecodeNetZonePricePerGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetRegionGiB(start, end time.Time) *Future[NetRegionGiBResult] {
+	return newEmptyResult(DecodeNetRegionGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetRegionPricePerGiB(start, end time.Time) *Future[NetRegionPricePerGiBResult] {
+	return newEmptyResult(DecodeNetRegionPricePerGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetInternetGiB(start, end time.Time) *Future[NetInternetGiBResult] {
+	return newEmptyResult(DecodeNetInternetGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetInternetPricePerGiB(start, end time.Time) *Future[NetInternetPricePerGiBResult] {
+	return newEmptyResult(DecodeNetInternetPricePerGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetInternetServiceGiB(start, end time.Time) *Future[NetInternetServiceGiBResult] {
+	return newEmptyResult(DecodeNetInternetServiceGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetNatGatewayPricePerGiB(start, end time.Time) *Future[NetNatGatewayPricePerGiBResult] {
+	return newEmptyResult(DecodeNetNatGatewayPricePerGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetNatGatewayGiB(start, end time.Time) *Future[NetNatGatewayGiBResult] {
+	return newEmptyResult(DecodeNetNatGatewayGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetTransferBytes(start, end time.Time) *Future[NetTransferBytesResult] {
+	return newEmptyResult(DecodeNetTransferBytesResult)
+}
+
+// Network Ingress
+
+func (m *NoOpMetricsQuerier) QueryNetZoneIngressGiB(start, end time.Time) *Future[NetZoneIngressGiBResult] {
+	return newEmptyResult(DecodeNetZoneIngressGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetRegionIngressGiB(start, end time.Time) *Future[NetRegionIngressGiBResult] {
+	return newEmptyResult(DecodeNetRegionIngressGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetInternetIngressGiB(start, end time.Time) *Future[NetInternetIngressGiBResult] {
+	return newEmptyResult(DecodeNetInternetIngressGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetInternetServiceIngressGiB(start, end time.Time) *Future[NetInternetServiceIngressGiBResult] {
+	return newEmptyResult(DecodeNetInternetServiceIngressGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetNatGatewayIngressPricePerGiB(start, end time.Time) *Future[NetNatGatewayPricePerGiBResult] {
+	return newEmptyResult(DecodeNetNatGatewayPricePerGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetNatGatewayIngressGiB(start, end time.Time) *Future[NetNatGatewayIngressGiBResult] {
+	return newEmptyResult(DecodeNetNatGatewayIngressGiBResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNetReceiveBytes(start, end time.Time) *Future[NetReceiveBytesResult] {
+	return newEmptyResult(DecodeNetReceiveBytesResult)
+}
+
+// Annotations
+
+func (m *NoOpMetricsQuerier) QueryNamespaceAnnotations(start, end time.Time) *Future[NamespaceAnnotationsResult] {
+	return newEmptyResult(DecodeNamespaceAnnotationsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPodAnnotations(start, end time.Time) *Future[PodAnnotationsResult] {
+	return newEmptyResult(DecodePodAnnotationsResult)
+}
+
+// Labels
+
+func (m *NoOpMetricsQuerier) QueryNodeLabels(start, end time.Time) *Future[NodeLabelsResult] {
+	return newEmptyResult(DecodeNodeLabelsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryNamespaceLabels(start, end time.Time) *Future[NamespaceLabelsResult] {
+	return newEmptyResult(DecodeNamespaceLabelsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPodLabels(start, end time.Time) *Future[PodLabelsResult] {
+	return newEmptyResult(DecodePodLabelsResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPodsWithDaemonSetOwner(start, end time.Time) *Future[PodsWithDaemonSetOwnerResult] {
+	return newEmptyResult(DecodePodsWithDaemonSetOwnerResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryPodsWithJobOwner(start, end time.Time) *Future[PodsWithJobOwnerResult] {
+	return newEmptyResult(DecodePodsWithJobOwnerResult)
+}
+
+// ReplicaSet -> Controller mapping
+
+func (m *NoOpMetricsQuerier) QueryPodsWithReplicaSetOwner(start, end time.Time) *Future[PodsWithReplicaSetOwnerResult] {
+	return newEmptyResult(DecodePodsWithReplicaSetOwnerResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryReplicaSetsWithoutOwners(start, end time.Time) *Future[ReplicaSetsWithoutOwnersResult] {
+	return newEmptyResult(DecodeReplicaSetsWithoutOwnersResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryReplicaSetsWithRollout(start, end time.Time) *Future[ReplicaSetsWithRolloutResult] {
+	return newEmptyResult(DecodeReplicaSetsWithRolloutResult)
+}
+
+// ResourceQuotas
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaInfo(start, end time.Time) *Future[ResourceQuotaInfoResult] {
+	return newEmptyResult(DecodeResourceQuotaInfoResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaUptime(start, end time.Time) *Future[UptimeResult] {
+	return newEmptyResult(DecodeUptimeResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaSpecCPURequestAverage(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaSpecCPURequestMax(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaSpecRAMRequestAverage(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaSpecRAMRequestMax(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaSpecCPULimitAverage(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaSpecCPULimitMax(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaSpecRAMLimitAverage(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaSpecRAMLimitMax(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaStatusUsedCPURequestAverage(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaStatusUsedCPURequestMax(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaStatusUsedRAMRequestAverage(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaStatusUsedRAMRequestMax(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaStatusUsedCPULimitAverage(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaStatusUsedCPULimitMax(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaStatusUsedRAMLimitAverage(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryResourceQuotaStatusUsedRAMLimitMax(start, end time.Time) *Future[ResourceResult] {
+	return newEmptyResult(DecodeResourceResult)
+}
+
+// Data Coverage Query
+
+func (m *NoOpMetricsQuerier) QueryDataCoverage(_ int) (time.Time, time.Time, error) {
+	return time.Time{}, time.Time{}, nil
+}
+
+// Extra methods not in MetricsQuerier interface
+
+func (m *NoOpMetricsQuerier) QueryLocalStorageCost(start, end time.Time) *Future[LocalStorageCostResult] {
+	return newEmptyResult(DecodeLocalStorageCostResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryLocalStorageUsedCost(start, end time.Time) *Future[LocalStorageUsedCostResult] {
+	return newEmptyResult(DecodeLocalStorageUsedCostResult)
+}
+
+func (m *NoOpMetricsQuerier) QueryServiceLabels(start, end time.Time) *Future[ServiceLabelsResult] {
+	return newEmptyResult(DecodeServiceLabelsResult)
+}
+
+func newEmptyResult[T any](decoder ResultDecoder[T]) *Future[T] {
+	ch := make(QueryResultsChan)
+	go func() {
+		results := NewQueryResults("")
+		ch <- results
+	}()
+
+	return NewFuture(decoder, ch)
+}

+ 856 - 0
core/pkg/source/record.go

@@ -0,0 +1,856 @@
+package source
+
+import (
+	"time"
+)
+
+var _ MetricsQuerier = (*RecordMetricsQuerier)(nil)
+
+// RecordMetricsQuerier is a wrapper implementation of MetricsQuerier which counts the number of times each function is
+// called
+type RecordMetricsQuerier struct {
+	Calls   map[string]int
+	Querier MetricsQuerier
+}
+
+// NewRecordMetricsQuerier creates a new mock metrics querier
+func NewRecordMetricsQuerier(querier MetricsQuerier) *RecordMetricsQuerier {
+	return &RecordMetricsQuerier{
+		Calls:   make(map[string]int),
+		Querier: querier,
+	}
+}
+
+// Helper to record method calls
+func (m *RecordMetricsQuerier) recordCall(method string) {
+	m.Calls[method]++
+}
+
+// Local Cluster Disks
+
+func (m *RecordMetricsQuerier) QueryLocalStorageActiveMinutes(start, end time.Time) *Future[LocalStorageActiveMinutesResult] {
+	m.recordCall(QueryLocalStorageActiveMinutes)
+	return m.Querier.QueryLocalStorageActiveMinutes(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryLocalStorageUsedAvg(start, end time.Time) *Future[LocalStorageUsedAvgResult] {
+	m.recordCall(QueryLocalStorageUsedAvg)
+	return m.Querier.QueryLocalStorageUsedAvg(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryLocalStorageUsedMax(start, end time.Time) *Future[LocalStorageUsedMaxResult] {
+	m.recordCall(QueryLocalStorageUsedMax)
+	return m.Querier.QueryLocalStorageUsedMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryLocalStorageBytes(start, end time.Time) *Future[LocalStorageBytesResult] {
+	m.recordCall(QueryLocalStorageBytes)
+	return m.Querier.QueryLocalStorageBytes(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryKMLocalStorageUsedAvg(start, end time.Time) *Future[NodeUIDValueResult] {
+	m.recordCall(QueryKMLocalStorageUsedAvg)
+	return m.Querier.QueryKMLocalStorageUsedAvg(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryKMLocalStorageUsedMax(start, end time.Time) *Future[NodeUIDValueResult] {
+	m.recordCall(QueryKMLocalStorageUsedMax)
+	return m.Querier.QueryKMLocalStorageUsedMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryKMLocalStorageBytes(start, end time.Time) *Future[UIDValueResult] {
+	m.recordCall(QueryKMLocalStorageBytes)
+	return m.Querier.QueryKMLocalStorageBytes(start, end)
+}
+
+// Nodes
+
+func (m *RecordMetricsQuerier) QueryNodeInfo(start, end time.Time) *Future[NodeInfoResult] {
+	m.recordCall(QueryNodeInfo)
+	return m.Querier.QueryNodeInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryNodeUptime)
+	return m.Querier.QueryNodeUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeActiveMinutes(start, end time.Time) *Future[NodeActiveMinutesResult] {
+	m.recordCall(QueryNodeActiveMinutes)
+	return m.Querier.QueryNodeActiveMinutes(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeCPUCoresCapacity(start, end time.Time) *Future[NodeCPUCoresCapacityResult] {
+	m.recordCall(QueryNodeCPUCoresCapacity)
+	return m.Querier.QueryNodeCPUCoresCapacity(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeCPUCoresAllocatable(start, end time.Time) *Future[NodeCPUCoresAllocatableResult] {
+	m.recordCall(QueryNodeCPUCoresAllocatable)
+	return m.Querier.QueryNodeCPUCoresAllocatable(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeRAMBytesCapacity(start, end time.Time) *Future[NodeRAMBytesCapacityResult] {
+	m.recordCall(QueryNodeRAMBytesCapacity)
+	return m.Querier.QueryNodeRAMBytesCapacity(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeRAMBytesAllocatable(start, end time.Time) *Future[NodeRAMBytesAllocatableResult] {
+	m.recordCall(QueryNodeRAMBytesAllocatable)
+	return m.Querier.QueryNodeRAMBytesAllocatable(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeGPUCount(start, end time.Time) *Future[NodeGPUCountResult] {
+	m.recordCall(QueryNodeGPUCount)
+	return m.Querier.QueryNodeGPUCount(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeCPUModeTotal(start, end time.Time) *Future[NodeCPUModeTotalResult] {
+	m.recordCall(QueryNodeCPUModeTotal)
+	return m.Querier.QueryNodeCPUModeTotal(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeIsSpot(start, end time.Time) *Future[NodeIsSpotResult] {
+	m.recordCall(QueryNodeIsSpot)
+	return m.Querier.QueryNodeIsSpot(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeRAMSystemPercent(start, end time.Time) *Future[NodeRAMSystemPercentResult] {
+	m.recordCall(QueryNodeRAMSystemPercent)
+	return m.Querier.QueryNodeRAMSystemPercent(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeRAMUserPercent(start, end time.Time) *Future[NodeRAMUserPercentResult] {
+	m.recordCall(QueryNodeRAMUserPercent)
+	return m.Querier.QueryNodeRAMUserPercent(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeResourceCapacities(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryNodeResourceCapacities)
+	return m.Querier.QueryNodeResourceCapacities(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeResourcesAllocatable(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryNodeResourcesAllocatable)
+	return m.Querier.QueryNodeResourcesAllocatable(start, end)
+}
+
+// Load Balancers
+
+func (m *RecordMetricsQuerier) QueryLBActiveMinutes(start, end time.Time) *Future[LBActiveMinutesResult] {
+	m.recordCall(QueryLBActiveMinutes)
+	return m.Querier.QueryLBActiveMinutes(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryLBPricePerHr(start, end time.Time) *Future[LBPricePerHrResult] {
+	m.recordCall(QueryLBPricePerHr)
+	return m.Querier.QueryLBPricePerHr(start, end)
+}
+
+// Cluster Management
+
+func (m *RecordMetricsQuerier) QueryClusterInfo(start, end time.Time) *Future[ClusterInfoResult] {
+	m.recordCall(QueryClusterInfo)
+	return m.Querier.QueryClusterInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryClusterKubeModelVersion(start, end time.Time) *Future[ClusterKubeModelVersionResult] {
+	m.recordCall(QueryClusterKubeModelVersion)
+	return m.Querier.QueryClusterKubeModelVersion(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryClusterUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryClusterUptime)
+	return m.Querier.QueryClusterUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryClusterManagementDuration(start, end time.Time) *Future[ClusterManagementDurationResult] {
+	m.recordCall(QueryClusterManagementDuration)
+	return m.Querier.QueryClusterManagementDuration(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryClusterManagementPricePerHr(start, end time.Time) *Future[ClusterManagementPricePerHrResult] {
+	m.recordCall(QueryClusterManagementPricePerHr)
+	return m.Querier.QueryClusterManagementPricePerHr(start, end)
+}
+
+// Pods
+
+func (m *RecordMetricsQuerier) QueryPods(start, end time.Time) *Future[PodsResult] {
+	m.recordCall(QueryPods)
+	return m.Querier.QueryPods(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPodsUID(start, end time.Time) *Future[PodsResult] {
+	m.recordCall(QueryPodsUID)
+	return m.Querier.QueryPodsUID(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPodInfo(start, end time.Time) *Future[PodInfoResult] {
+	m.recordCall(QueryPodInfo)
+	return m.Querier.QueryPodInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPodUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryPodUptime)
+	return m.Querier.QueryPodUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPodOwners(start, end time.Time) *Future[OwnerResult] {
+	m.recordCall(QueryPodOwners)
+	return m.Querier.QueryPodOwners(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPodPVCVolumes(start, end time.Time) *Future[PodPVCVolumeResult] {
+	m.recordCall(QueryPodPVCVolumes)
+	return m.Querier.QueryPodPVCVolumes(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPodNetworkEgressBytes(start, end time.Time) *Future[PodNetworkBytesResult] {
+	m.recordCall(QueryPodNetworkEgressBytes)
+	return m.Querier.QueryPodNetworkEgressBytes(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPodNetworkIngressBytes(start, end time.Time) *Future[PodNetworkBytesResult] {
+	m.recordCall(QueryPodNetworkIngressBytes)
+	return m.Querier.QueryPodNetworkIngressBytes(start, end)
+}
+
+// Container
+
+func (m *RecordMetricsQuerier) QueryContainerUptime(start, end time.Time) *Future[ContainerUptimeResult] {
+	m.recordCall(QueryContainerUptime)
+	return m.Querier.QueryContainerUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryContainerResourceRequests(start, end time.Time) *Future[ContainerResourceResult] {
+	m.recordCall(QueryContainerResourceRequests)
+	return m.Querier.QueryContainerResourceRequests(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryContainerResourceLimits(start, end time.Time) *Future[ContainerResourceResult] {
+	m.recordCall(QueryContainerResourceLimits)
+	return m.Querier.QueryContainerResourceLimits(start, end)
+}
+
+// RAM
+
+func (m *RecordMetricsQuerier) QueryRAMBytesAllocated(start, end time.Time) *Future[RAMBytesAllocatedResult] {
+	m.recordCall(QueryRAMBytesAllocated)
+	return m.Querier.QueryRAMBytesAllocated(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryRAMRequests(start, end time.Time) *Future[RAMRequestsResult] {
+	m.recordCall(QueryRAMRequests)
+	return m.Querier.QueryRAMRequests(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryRAMLimits(start, end time.Time) *Future[RAMLimitsResult] {
+	m.recordCall(QueryRAMLimits)
+	return m.Querier.QueryRAMLimits(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryRAMUsageAvg(start, end time.Time) *Future[RAMUsageAvgResult] {
+	m.recordCall(QueryRAMUsageAvg)
+	return m.Querier.QueryRAMUsageAvg(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryRAMUsageMax(start, end time.Time) *Future[RAMUsageMaxResult] {
+	m.recordCall(QueryRAMUsageMax)
+	return m.Querier.QueryRAMUsageMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeRAMPricePerGiBHr(start, end time.Time) *Future[NodeRAMPricePerGiBHrResult] {
+	m.recordCall(QueryNodeRAMPricePerGiBHr)
+	return m.Querier.QueryNodeRAMPricePerGiBHr(start, end)
+}
+
+// CPU
+
+func (m *RecordMetricsQuerier) QueryCPUCoresAllocated(start, end time.Time) *Future[CPUCoresAllocatedResult] {
+	m.recordCall(QueryCPUCoresAllocated)
+	return m.Querier.QueryCPUCoresAllocated(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryCPURequests(start, end time.Time) *Future[CPURequestsResult] {
+	m.recordCall(QueryCPURequests)
+	return m.Querier.QueryCPURequests(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryCPULimits(start, end time.Time) *Future[CPULimitsResult] {
+	m.recordCall(QueryCPULimits)
+	return m.Querier.QueryCPULimits(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryCPUUsageAvg(start, end time.Time) *Future[CPUUsageAvgResult] {
+	m.recordCall(QueryCPUUsageAvg)
+	return m.Querier.QueryCPUUsageAvg(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryCPUUsageMax(start, end time.Time) *Future[CPUUsageMaxResult] {
+	m.recordCall(QueryCPUUsageMax)
+	return m.Querier.QueryCPUUsageMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeCPUPricePerHr(start, end time.Time) *Future[NodeCPUPricePerHrResult] {
+	m.recordCall(QueryNodeCPUPricePerHr)
+	return m.Querier.QueryNodeCPUPricePerHr(start, end)
+}
+
+// GPU
+
+func (m *RecordMetricsQuerier) QueryGPUsAllocated(start, end time.Time) *Future[GPUsAllocatedResult] {
+	m.recordCall(QueryGPUsAllocated)
+	return m.Querier.QueryGPUsAllocated(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryGPUsRequested(start, end time.Time) *Future[GPUsRequestedResult] {
+	m.recordCall(QueryGPUsRequested)
+	return m.Querier.QueryGPUsRequested(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryGPUsUsageAvg(start, end time.Time) *Future[GPUsUsageAvgResult] {
+	m.recordCall(QueryGPUsUsageAvg)
+	return m.Querier.QueryGPUsUsageAvg(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryGPUsUsageMax(start, end time.Time) *Future[GPUsUsageMaxResult] {
+	m.recordCall(QueryGPUsUsageMax)
+	return m.Querier.QueryGPUsUsageMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNodeGPUPricePerHr(start, end time.Time) *Future[NodeGPUPricePerHrResult] {
+	m.recordCall(QueryNodeGPUPricePerHr)
+	return m.Querier.QueryNodeGPUPricePerHr(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryGPUInfo(start, end time.Time) *Future[GPUInfoResult] {
+	m.recordCall(QueryGPUInfo)
+	return m.Querier.QueryGPUInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryIsGPUShared(start, end time.Time) *Future[IsGPUSharedResult] {
+	m.recordCall(QueryIsGPUShared)
+	return m.Querier.QueryIsGPUShared(start, end)
+}
+
+// Device
+
+func (m *RecordMetricsQuerier) QueryDCGMDeviceInfo(start, end time.Time) *Future[DCGMDeviceInfoResult] {
+	m.recordCall(QueryDCGMDeviceInfo)
+	return m.Querier.QueryDCGMDeviceInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryDCGMDeviceUptime(start, end time.Time) *Future[DCGMDeviceUptimeResult] {
+	m.recordCall(QueryDCGMDeviceUptime)
+	return m.Querier.QueryDCGMDeviceUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryDCGMContainerUsageAvg(start, end time.Time) *Future[DCGMDeviceContainerUsageResult] {
+	m.recordCall(QueryDCGMContainerUsageAvg)
+	return m.Querier.QueryDCGMContainerUsageAvg(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryDCGMContainerUsageMax(start, end time.Time) *Future[DCGMDeviceContainerUsageResult] {
+	m.recordCall(QueryDCGMContainerUsageMax)
+	return m.Querier.QueryDCGMContainerUsageMax(start, end)
+}
+
+// PVC
+
+func (m *RecordMetricsQuerier) QueryPodPVCAllocation(start, end time.Time) *Future[PodPVCAllocationResult] {
+	m.recordCall(QueryPodPVCAllocation)
+	return m.Querier.QueryPodPVCAllocation(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPVCBytesRequested(start, end time.Time) *Future[PVCBytesRequestedResult] {
+	m.recordCall(QueryPVCBytesRequested)
+	return m.Querier.QueryPVCBytesRequested(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPVCInfo(start, end time.Time) *Future[PVCInfoResult] {
+	m.recordCall(QueryPVCInfo)
+	return m.Querier.QueryPVCInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryKMPVCInfo(start, end time.Time) *Future[PVCInfoResult] {
+	m.recordCall(QueryKMPVCInfo)
+	return m.Querier.QueryKMPVCInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPVCUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryPVCUptime)
+	return m.Querier.QueryPVCUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPVCBytesUsedAverage(start, end time.Time) *Future[PVCUIDValueResult] {
+	m.recordCall(QueryPVCBytesUsedAverage)
+	return m.Querier.QueryPVCBytesUsedAverage(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPVCBytesUsedMax(start, end time.Time) *Future[PVCUIDValueResult] {
+	m.recordCall(QueryPVCBytesUsedMax)
+	return m.Querier.QueryPVCBytesUsedMax(start, end)
+}
+
+// PV
+
+func (m *RecordMetricsQuerier) QueryPVBytes(start, end time.Time) *Future[PVBytesResult] {
+	m.recordCall(QueryPVBytes)
+	return m.Querier.QueryPVBytes(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPVPricePerGiBHour(start, end time.Time) *Future[PVPricePerGiBHourResult] {
+	m.recordCall(QueryPVPricePerGiBHour)
+	return m.Querier.QueryPVPricePerGiBHour(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPVInfo(start, end time.Time) *Future[PVInfoResult] {
+	m.recordCall(QueryPVInfo)
+	return m.Querier.QueryPVInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPVActiveMinutes(start, end time.Time) *Future[PVActiveMinutesResult] {
+	m.recordCall(QueryPVActiveMinutes)
+	return m.Querier.QueryPVActiveMinutes(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPVUsedAverage(start, end time.Time) *Future[PVUsedAvgResult] {
+	m.recordCall(QueryPVUsedAverage)
+	return m.Querier.QueryPVUsedAverage(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPVUsedMax(start, end time.Time) *Future[PVUsedMaxResult] {
+	m.recordCall(QueryPVUsedMax)
+	return m.Querier.QueryPVUsedMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryKMPVInfo(start, end time.Time) *Future[PVInfoResult] {
+	m.recordCall(QueryKMPVInfo)
+	return m.Querier.QueryKMPVInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPVUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryPVUptime)
+	return m.Querier.QueryPVUptime(start, end)
+}
+
+// Deployment
+
+func (m *RecordMetricsQuerier) QueryDeploymentInfo(start, end time.Time) *Future[DeploymentInfoResult] {
+	m.recordCall(QueryDeploymentInfo)
+	return m.Querier.QueryDeploymentInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryDeploymentUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryDeploymentUptime)
+	return m.Querier.QueryDeploymentUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryDeploymentLabels(start, end time.Time) *Future[LabelsResult] {
+	m.recordCall(QueryDeploymentLabels)
+	return m.Querier.QueryDeploymentLabels(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryDeploymentAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	m.recordCall(QueryDeploymentAnnotations)
+	return m.Querier.QueryDeploymentAnnotations(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryDeploymentMatchLabels(start, end time.Time) *Future[DeploymentLabelsResult] {
+	m.recordCall(QueryDeploymentMatchLabels)
+	return m.Querier.QueryDeploymentMatchLabels(start, end)
+}
+
+// StatefulSet
+
+func (m *RecordMetricsQuerier) QueryStatefulSetInfo(start, end time.Time) *Future[StatefulSetInfoResult] {
+	m.recordCall(QueryStatefulSetInfo)
+	return m.Querier.QueryStatefulSetInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryStatefulSetUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryStatefulSetUptime)
+	return m.Querier.QueryStatefulSetUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryStatefulSetLabels(start, end time.Time) *Future[LabelsResult] {
+	m.recordCall(QueryStatefulSetLabels)
+	return m.Querier.QueryStatefulSetLabels(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryStatefulSetAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	m.recordCall(QueryStatefulSetAnnotations)
+	return m.Querier.QueryStatefulSetAnnotations(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryStatefulSetMatchLabels(start, end time.Time) *Future[StatefulSetLabelsResult] {
+	m.recordCall(QueryStatefulSetMatchLabels)
+	return m.Querier.QueryStatefulSetMatchLabels(start, end)
+}
+
+// DaemonSet
+
+func (m *RecordMetricsQuerier) QueryDaemonSetInfo(start, end time.Time) *Future[DaemonSetInfoResult] {
+	m.recordCall(QueryDaemonSetInfo)
+	return m.Querier.QueryDaemonSetInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryDaemonSetUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryDaemonSetUptime)
+	return m.Querier.QueryDaemonSetUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryDaemonSetLabels(start, end time.Time) *Future[LabelsResult] {
+	m.recordCall(QueryDaemonSetLabels)
+	return m.Querier.QueryDaemonSetLabels(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryDaemonSetAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	m.recordCall(QueryDaemonSetAnnotations)
+	return m.Querier.QueryDaemonSetAnnotations(start, end)
+}
+
+// Job
+
+func (m *RecordMetricsQuerier) QueryJobInfo(start, end time.Time) *Future[JobInfoResult] {
+	m.recordCall(QueryJobInfo)
+	return m.Querier.QueryJobInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryJobUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryJobUptime)
+	return m.Querier.QueryJobUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryJobLabels(start, end time.Time) *Future[LabelsResult] {
+	m.recordCall(QueryJobLabels)
+	return m.Querier.QueryJobLabels(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryJobAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	m.recordCall(QueryJobAnnotations)
+	return m.Querier.QueryJobAnnotations(start, end)
+}
+
+// CronJob
+
+func (m *RecordMetricsQuerier) QueryCronJobInfo(start, end time.Time) *Future[CronJobInfoResult] {
+	m.recordCall(QueryCronJobInfo)
+	return m.Querier.QueryCronJobInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryCronJobUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryCronJobUptime)
+	return m.Querier.QueryCronJobUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryCronJobLabels(start, end time.Time) *Future[LabelsResult] {
+	m.recordCall(QueryCronJobLabels)
+	return m.Querier.QueryCronJobLabels(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryCronJobAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	m.recordCall(QueryCronJobAnnotations)
+	return m.Querier.QueryCronJobAnnotations(start, end)
+}
+
+// ReplicaSet
+
+func (m *RecordMetricsQuerier) QueryReplicaSetInfo(start, end time.Time) *Future[ReplicaSetInfoResult] {
+	m.recordCall(QueryReplicaSetInfo)
+	return m.Querier.QueryReplicaSetInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryReplicaSetUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryReplicaSetUptime)
+	return m.Querier.QueryReplicaSetUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryReplicaSetLabels(start, end time.Time) *Future[LabelsResult] {
+	m.recordCall(QueryReplicaSetLabels)
+	return m.Querier.QueryReplicaSetLabels(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryReplicaSetAnnotations(start, end time.Time) *Future[AnnotationsResult] {
+	m.recordCall(QueryReplicaSetAnnotations)
+	return m.Querier.QueryReplicaSetAnnotations(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryReplicaSetOwners(start, end time.Time) *Future[OwnerResult] {
+	m.recordCall(QueryReplicaSetOwners)
+	return m.Querier.QueryReplicaSetOwners(start, end)
+}
+
+// Namespace
+
+func (m *RecordMetricsQuerier) QueryNamespaceInfo(start, end time.Time) *Future[NamespaceInfoResult] {
+	m.recordCall(QueryNamespaceInfo)
+	return m.Querier.QueryNamespaceInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNamespaceUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryNamespaceUptime)
+	return m.Querier.QueryNamespaceUptime(start, end)
+}
+
+// Service
+
+func (m *RecordMetricsQuerier) QueryServiceInfo(start, end time.Time) *Future[ServiceInfoResult] {
+	m.recordCall(QueryServiceInfo)
+	return m.Querier.QueryServiceInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryServiceUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryServiceUptime)
+	return m.Querier.QueryServiceUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryServiceSelectorLabels(start, end time.Time) *Future[ServiceLabelsResult] {
+	m.recordCall(QueryServiceSelectorLabels)
+	return m.Querier.QueryServiceSelectorLabels(start, end)
+}
+
+// Network Egress
+
+func (m *RecordMetricsQuerier) QueryNetZoneGiB(start, end time.Time) *Future[NetZoneGiBResult] {
+	m.recordCall(QueryNetZoneGiB)
+	return m.Querier.QueryNetZoneGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetZonePricePerGiB(start, end time.Time) *Future[NetZonePricePerGiBResult] {
+	m.recordCall(QueryNetZonePricePerGiB)
+	return m.Querier.QueryNetZonePricePerGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetRegionGiB(start, end time.Time) *Future[NetRegionGiBResult] {
+	m.recordCall(QueryNetRegionGiB)
+	return m.Querier.QueryNetRegionGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetRegionPricePerGiB(start, end time.Time) *Future[NetRegionPricePerGiBResult] {
+	m.recordCall(QueryNetRegionPricePerGiB)
+	return m.Querier.QueryNetRegionPricePerGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetInternetGiB(start, end time.Time) *Future[NetInternetGiBResult] {
+	m.recordCall(QueryNetInternetGiB)
+	return m.Querier.QueryNetInternetGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetInternetPricePerGiB(start, end time.Time) *Future[NetInternetPricePerGiBResult] {
+	m.recordCall(QueryNetInternetPricePerGiB)
+	return m.Querier.QueryNetInternetPricePerGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetInternetServiceGiB(start, end time.Time) *Future[NetInternetServiceGiBResult] {
+	m.recordCall(QueryNetInternetServiceGiB)
+	return m.Querier.QueryNetInternetServiceGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetNatGatewayPricePerGiB(start, end time.Time) *Future[NetNatGatewayPricePerGiBResult] {
+	m.recordCall(QueryNetNatGatewayPricePerGiB)
+	return m.Querier.QueryNetNatGatewayPricePerGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetNatGatewayGiB(start, end time.Time) *Future[NetNatGatewayGiBResult] {
+	m.recordCall(QueryNetNatGatewayGiB)
+	return m.Querier.QueryNetNatGatewayGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetTransferBytes(start, end time.Time) *Future[NetTransferBytesResult] {
+	m.recordCall(QueryNetTransferBytes)
+	return m.Querier.QueryNetTransferBytes(start, end)
+}
+
+// Network Ingress
+
+func (m *RecordMetricsQuerier) QueryNetZoneIngressGiB(start, end time.Time) *Future[NetZoneIngressGiBResult] {
+	m.recordCall(QueryNetZoneIngressGiB)
+	return m.Querier.QueryNetZoneIngressGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetRegionIngressGiB(start, end time.Time) *Future[NetRegionIngressGiBResult] {
+	m.recordCall(QueryNetRegionIngressGiB)
+	return m.Querier.QueryNetRegionIngressGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetInternetIngressGiB(start, end time.Time) *Future[NetInternetIngressGiBResult] {
+	m.recordCall(QueryNetInternetIngressGiB)
+	return m.Querier.QueryNetInternetIngressGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetInternetServiceIngressGiB(start, end time.Time) *Future[NetInternetServiceIngressGiBResult] {
+	m.recordCall(QueryNetInternetServiceIngressGiB)
+	return m.Querier.QueryNetInternetServiceIngressGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetNatGatewayIngressPricePerGiB(start, end time.Time) *Future[NetNatGatewayPricePerGiBResult] {
+	m.recordCall(QueryNetNatGatewayIngressPricePerGiB)
+	return m.Querier.QueryNetNatGatewayIngressPricePerGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetNatGatewayIngressGiB(start, end time.Time) *Future[NetNatGatewayIngressGiBResult] {
+	m.recordCall(QueryNetNatGatewayIngressGiB)
+	return m.Querier.QueryNetNatGatewayIngressGiB(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNetReceiveBytes(start, end time.Time) *Future[NetReceiveBytesResult] {
+	m.recordCall(QueryNetReceiveBytes)
+	return m.Querier.QueryNetReceiveBytes(start, end)
+}
+
+// Annotations
+
+func (m *RecordMetricsQuerier) QueryNamespaceAnnotations(start, end time.Time) *Future[NamespaceAnnotationsResult] {
+	m.recordCall(QueryNamespaceAnnotations)
+	return m.Querier.QueryNamespaceAnnotations(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPodAnnotations(start, end time.Time) *Future[PodAnnotationsResult] {
+	m.recordCall(QueryPodAnnotations)
+	return m.Querier.QueryPodAnnotations(start, end)
+}
+
+// Labels
+
+func (m *RecordMetricsQuerier) QueryNodeLabels(start, end time.Time) *Future[NodeLabelsResult] {
+	m.recordCall(QueryNodeLabels)
+	return m.Querier.QueryNodeLabels(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryNamespaceLabels(start, end time.Time) *Future[NamespaceLabelsResult] {
+	m.recordCall(QueryNamespaceLabels)
+	return m.Querier.QueryNamespaceLabels(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPodLabels(start, end time.Time) *Future[PodLabelsResult] {
+	m.recordCall(QueryPodLabels)
+	return m.Querier.QueryPodLabels(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPodsWithDaemonSetOwner(start, end time.Time) *Future[PodsWithDaemonSetOwnerResult] {
+	m.recordCall(QueryPodsWithDaemonSetOwner)
+	return m.Querier.QueryPodsWithDaemonSetOwner(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryPodsWithJobOwner(start, end time.Time) *Future[PodsWithJobOwnerResult] {
+	m.recordCall(QueryPodsWithJobOwner)
+	return m.Querier.QueryPodsWithJobOwner(start, end)
+}
+
+// ReplicaSet -> Controller mapping
+
+func (m *RecordMetricsQuerier) QueryPodsWithReplicaSetOwner(start, end time.Time) *Future[PodsWithReplicaSetOwnerResult] {
+	m.recordCall(QueryPodsWithReplicaSetOwner)
+	return m.Querier.QueryPodsWithReplicaSetOwner(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryReplicaSetsWithoutOwners(start, end time.Time) *Future[ReplicaSetsWithoutOwnersResult] {
+	m.recordCall(QueryReplicaSetsWithoutOwners)
+	return m.Querier.QueryReplicaSetsWithoutOwners(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryReplicaSetsWithRollout(start, end time.Time) *Future[ReplicaSetsWithRolloutResult] {
+	m.recordCall(QueryReplicaSetsWithRollout)
+	return m.Querier.QueryReplicaSetsWithRollout(start, end)
+}
+
+// ResourceQuotas
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaInfo(start, end time.Time) *Future[ResourceQuotaInfoResult] {
+	m.recordCall(QueryResourceQuotaInfo)
+	return m.Querier.QueryResourceQuotaInfo(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaUptime(start, end time.Time) *Future[UptimeResult] {
+	m.recordCall(QueryResourceQuotaUptime)
+	return m.Querier.QueryResourceQuotaUptime(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaSpecCPURequestAverage(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaSpecCPURequestAverage)
+	return m.Querier.QueryResourceQuotaSpecCPURequestAverage(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaSpecCPURequestMax(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaSpecCPURequestMax)
+	return m.Querier.QueryResourceQuotaSpecCPURequestMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaSpecRAMRequestAverage(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaSpecRAMRequestAverage)
+	return m.Querier.QueryResourceQuotaSpecRAMRequestAverage(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaSpecRAMRequestMax(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaSpecRAMRequestMax)
+	return m.Querier.QueryResourceQuotaSpecRAMRequestMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaSpecCPULimitAverage(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaSpecCPULimitAverage)
+	return m.Querier.QueryResourceQuotaSpecCPULimitAverage(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaSpecCPULimitMax(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaSpecCPULimitMax)
+	return m.Querier.QueryResourceQuotaSpecCPULimitMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaSpecRAMLimitAverage(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaSpecRAMLimitAverage)
+	return m.Querier.QueryResourceQuotaSpecRAMLimitAverage(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaSpecRAMLimitMax(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaSpecRAMLimitMax)
+	return m.Querier.QueryResourceQuotaSpecRAMLimitMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaStatusUsedCPURequestAverage(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaStatusUsedCPURequestAverage)
+	return m.Querier.QueryResourceQuotaStatusUsedCPURequestAverage(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaStatusUsedCPURequestMax(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaStatusUsedCPURequestMax)
+	return m.Querier.QueryResourceQuotaStatusUsedCPURequestMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaStatusUsedRAMRequestAverage(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaStatusUsedRAMRequestAverage)
+	return m.Querier.QueryResourceQuotaStatusUsedRAMRequestAverage(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaStatusUsedRAMRequestMax(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaStatusUsedRAMRequestMax)
+	return m.Querier.QueryResourceQuotaStatusUsedRAMRequestMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaStatusUsedCPULimitAverage(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaStatusUsedCPULimitAverage)
+	return m.Querier.QueryResourceQuotaStatusUsedCPULimitAverage(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaStatusUsedCPULimitMax(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaStatusUsedCPULimitMax)
+	return m.Querier.QueryResourceQuotaStatusUsedCPULimitMax(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaStatusUsedRAMLimitAverage(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaStatusUsedRAMLimitAverage)
+	return m.Querier.QueryResourceQuotaStatusUsedRAMLimitAverage(start, end)
+}
+
+func (m *RecordMetricsQuerier) QueryResourceQuotaStatusUsedRAMLimitMax(start, end time.Time) *Future[ResourceResult] {
+	m.recordCall(QueryResourceQuotaStatusUsedRAMLimitMax)
+	return m.Querier.QueryResourceQuotaStatusUsedRAMLimitMax(start, end)
+}
+
+// Data Coverage Query
+
+func (m *RecordMetricsQuerier) QueryDataCoverage(limitDays int) (time.Time, time.Time, error) {
+	m.recordCall(QueryDataCoverage)
+	return m.Querier.QueryDataCoverage(limitDays)
+}

+ 329 - 0
core/pkg/source/record_test.go

@@ -0,0 +1,329 @@
+package source
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestRecordMetricsQuerier_Counts(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	r := NewRecordMetricsQuerier(NewNoOpMetricsQuerier())
+
+	r.QueryLocalStorageActiveMinutes(start, end)
+	r.QueryLocalStorageUsedAvg(start, end)
+	r.QueryLocalStorageUsedMax(start, end)
+	r.QueryLocalStorageBytes(start, end)
+	r.QueryKMLocalStorageUsedAvg(start, end)
+	r.QueryKMLocalStorageUsedMax(start, end)
+	r.QueryKMLocalStorageBytes(start, end)
+	r.QueryNodeInfo(start, end)
+	r.QueryNodeUptime(start, end)
+	r.QueryNodeActiveMinutes(start, end)
+	r.QueryNodeCPUCoresCapacity(start, end)
+	r.QueryNodeCPUCoresAllocatable(start, end)
+	r.QueryNodeRAMBytesCapacity(start, end)
+	r.QueryNodeRAMBytesAllocatable(start, end)
+	r.QueryNodeGPUCount(start, end)
+	r.QueryNodeCPUModeTotal(start, end)
+	r.QueryNodeIsSpot(start, end)
+	r.QueryNodeRAMSystemPercent(start, end)
+	r.QueryNodeRAMUserPercent(start, end)
+	r.QueryNodeResourceCapacities(start, end)
+	r.QueryNodeResourcesAllocatable(start, end)
+	r.QueryLBActiveMinutes(start, end)
+	r.QueryLBPricePerHr(start, end)
+	r.QueryClusterInfo(start, end)
+	r.QueryClusterUptime(start, end)
+	r.QueryClusterManagementDuration(start, end)
+	r.QueryClusterManagementPricePerHr(start, end)
+	r.QueryPods(start, end)
+	r.QueryPodsUID(start, end)
+	r.QueryPodInfo(start, end)
+	r.QueryPodUptime(start, end)
+	r.QueryPodOwners(start, end)
+	r.QueryPodPVCVolumes(start, end)
+	r.QueryPodNetworkEgressBytes(start, end)
+	r.QueryPodNetworkIngressBytes(start, end)
+	r.QueryContainerUptime(start, end)
+	r.QueryContainerResourceRequests(start, end)
+	r.QueryContainerResourceLimits(start, end)
+	r.QueryRAMBytesAllocated(start, end)
+	r.QueryRAMRequests(start, end)
+	r.QueryRAMLimits(start, end)
+	r.QueryRAMUsageAvg(start, end)
+	r.QueryRAMUsageMax(start, end)
+	r.QueryNodeRAMPricePerGiBHr(start, end)
+	r.QueryCPUCoresAllocated(start, end)
+	r.QueryCPURequests(start, end)
+	r.QueryCPULimits(start, end)
+	r.QueryCPUUsageAvg(start, end)
+	r.QueryCPUUsageMax(start, end)
+	r.QueryNodeCPUPricePerHr(start, end)
+	r.QueryGPUsAllocated(start, end)
+	r.QueryGPUsRequested(start, end)
+	r.QueryGPUsUsageAvg(start, end)
+	r.QueryGPUsUsageMax(start, end)
+	r.QueryNodeGPUPricePerHr(start, end)
+	r.QueryGPUInfo(start, end)
+	r.QueryIsGPUShared(start, end)
+	r.QueryDCGMDeviceInfo(start, end)
+	r.QueryDCGMDeviceUptime(start, end)
+	r.QueryDCGMContainerUsageAvg(start, end)
+	r.QueryDCGMContainerUsageMax(start, end)
+	r.QueryPodPVCAllocation(start, end)
+	r.QueryPVCBytesRequested(start, end)
+	r.QueryPVCInfo(start, end)
+	r.QueryKMPVCInfo(start, end)
+	r.QueryPVCUptime(start, end)
+	r.QueryPVCBytesUsedAverage(start, end)
+	r.QueryPVCBytesUsedMax(start, end)
+	r.QueryPVBytes(start, end)
+	r.QueryPVPricePerGiBHour(start, end)
+	r.QueryPVInfo(start, end)
+	r.QueryPVActiveMinutes(start, end)
+	r.QueryPVUsedAverage(start, end)
+	r.QueryPVUsedMax(start, end)
+	r.QueryKMPVInfo(start, end)
+	r.QueryPVUptime(start, end)
+	r.QueryDeploymentInfo(start, end)
+	r.QueryDeploymentUptime(start, end)
+	r.QueryDeploymentLabels(start, end)
+	r.QueryDeploymentAnnotations(start, end)
+	r.QueryDeploymentMatchLabels(start, end)
+	r.QueryStatefulSetInfo(start, end)
+	r.QueryStatefulSetUptime(start, end)
+	r.QueryStatefulSetLabels(start, end)
+	r.QueryStatefulSetAnnotations(start, end)
+	r.QueryStatefulSetMatchLabels(start, end)
+	r.QueryDaemonSetInfo(start, end)
+	r.QueryDaemonSetUptime(start, end)
+	r.QueryDaemonSetLabels(start, end)
+	r.QueryDaemonSetAnnotations(start, end)
+	r.QueryJobInfo(start, end)
+	r.QueryJobUptime(start, end)
+	r.QueryJobLabels(start, end)
+	r.QueryJobAnnotations(start, end)
+	r.QueryCronJobInfo(start, end)
+	r.QueryCronJobUptime(start, end)
+	r.QueryCronJobLabels(start, end)
+	r.QueryCronJobAnnotations(start, end)
+	r.QueryReplicaSetInfo(start, end)
+	r.QueryReplicaSetUptime(start, end)
+	r.QueryReplicaSetLabels(start, end)
+	r.QueryReplicaSetAnnotations(start, end)
+	r.QueryReplicaSetOwners(start, end)
+	r.QueryNamespaceInfo(start, end)
+	r.QueryNamespaceUptime(start, end)
+	r.QueryServiceInfo(start, end)
+	r.QueryServiceUptime(start, end)
+	r.QueryServiceSelectorLabels(start, end)
+	r.QueryNetZoneGiB(start, end)
+	r.QueryNetZonePricePerGiB(start, end)
+	r.QueryNetRegionGiB(start, end)
+	r.QueryNetRegionPricePerGiB(start, end)
+	r.QueryNetInternetGiB(start, end)
+	r.QueryNetInternetPricePerGiB(start, end)
+	r.QueryNetInternetServiceGiB(start, end)
+	r.QueryNetNatGatewayPricePerGiB(start, end)
+	r.QueryNetNatGatewayGiB(start, end)
+	r.QueryNetTransferBytes(start, end)
+	r.QueryNetZoneIngressGiB(start, end)
+	r.QueryNetRegionIngressGiB(start, end)
+	r.QueryNetInternetIngressGiB(start, end)
+	r.QueryNetInternetServiceIngressGiB(start, end)
+	r.QueryNetNatGatewayIngressPricePerGiB(start, end)
+	r.QueryNetNatGatewayIngressGiB(start, end)
+	r.QueryNetReceiveBytes(start, end)
+	r.QueryNamespaceAnnotations(start, end)
+	r.QueryPodAnnotations(start, end)
+	r.QueryNodeLabels(start, end)
+	r.QueryNamespaceLabels(start, end)
+	r.QueryPodLabels(start, end)
+	r.QueryPodsWithDaemonSetOwner(start, end)
+	r.QueryPodsWithJobOwner(start, end)
+	r.QueryPodsWithReplicaSetOwner(start, end)
+	r.QueryReplicaSetsWithoutOwners(start, end)
+	r.QueryReplicaSetsWithRollout(start, end)
+	r.QueryResourceQuotaInfo(start, end)
+	r.QueryResourceQuotaUptime(start, end)
+	r.QueryResourceQuotaSpecCPURequestAverage(start, end)
+	r.QueryResourceQuotaSpecCPURequestMax(start, end)
+	r.QueryResourceQuotaSpecRAMRequestAverage(start, end)
+	r.QueryResourceQuotaSpecRAMRequestMax(start, end)
+	r.QueryResourceQuotaSpecCPULimitAverage(start, end)
+	r.QueryResourceQuotaSpecCPULimitMax(start, end)
+	r.QueryResourceQuotaSpecRAMLimitAverage(start, end)
+	r.QueryResourceQuotaSpecRAMLimitMax(start, end)
+	r.QueryResourceQuotaStatusUsedCPURequestAverage(start, end)
+	r.QueryResourceQuotaStatusUsedCPURequestMax(start, end)
+	r.QueryResourceQuotaStatusUsedRAMRequestAverage(start, end)
+	r.QueryResourceQuotaStatusUsedRAMRequestMax(start, end)
+	r.QueryResourceQuotaStatusUsedCPULimitAverage(start, end)
+	r.QueryResourceQuotaStatusUsedCPULimitMax(start, end)
+	r.QueryResourceQuotaStatusUsedRAMLimitAverage(start, end)
+	r.QueryResourceQuotaStatusUsedRAMLimitMax(start, end)
+	r.QueryDataCoverage(30)
+
+	expected := map[string]int{
+		"QueryLocalStorageActiveMinutes":                1,
+		"QueryLocalStorageUsedAvg":                      1,
+		"QueryLocalStorageUsedMax":                      1,
+		"QueryLocalStorageBytes":                        1,
+		"QueryKMLocalStorageUsedAvg":                    1,
+		"QueryKMLocalStorageUsedMax":                    1,
+		"QueryKMLocalStorageBytes":                      1,
+		"QueryNodeInfo":                                 1,
+		"QueryNodeUptime":                               1,
+		"QueryNodeActiveMinutes":                        1,
+		"QueryNodeCPUCoresCapacity":                     1,
+		"QueryNodeCPUCoresAllocatable":                  1,
+		"QueryNodeRAMBytesCapacity":                     1,
+		"QueryNodeRAMBytesAllocatable":                  1,
+		"QueryNodeGPUCount":                             1,
+		"QueryNodeCPUModeTotal":                         1,
+		"QueryNodeIsSpot":                               1,
+		"QueryNodeRAMSystemPercent":                     1,
+		"QueryNodeRAMUserPercent":                       1,
+		"QueryNodeResourceCapacities":                   1,
+		"QueryNodeResourcesAllocatable":                 1,
+		"QueryLBActiveMinutes":                          1,
+		"QueryLBPricePerHr":                             1,
+		"QueryClusterInfo":                              1,
+		"QueryClusterUptime":                            1,
+		"QueryClusterManagementDuration":                1,
+		"QueryClusterManagementPricePerHr":              1,
+		"QueryPods":                                     1,
+		"QueryPodsUID":                                  1,
+		"QueryPodInfo":                                  1,
+		"QueryPodUptime":                                1,
+		"QueryPodOwners":                                1,
+		"QueryPodPVCVolumes":                            1,
+		"QueryPodNetworkEgressBytes":                    1,
+		"QueryPodNetworkIngressBytes":                   1,
+		"QueryContainerUptime":                          1,
+		"QueryContainerResourceRequests":                1,
+		"QueryContainerResourceLimits":                  1,
+		"QueryRAMBytesAllocated":                        1,
+		"QueryRAMRequests":                              1,
+		"QueryRAMLimits":                                1,
+		"QueryRAMUsageAvg":                              1,
+		"QueryRAMUsageMax":                              1,
+		"QueryNodeRAMPricePerGiBHr":                     1,
+		"QueryCPUCoresAllocated":                        1,
+		"QueryCPURequests":                              1,
+		"QueryCPULimits":                                1,
+		"QueryCPUUsageAvg":                              1,
+		"QueryCPUUsageMax":                              1,
+		"QueryNodeCPUPricePerHr":                        1,
+		"QueryGPUsAllocated":                            1,
+		"QueryGPUsRequested":                            1,
+		"QueryGPUsUsageAvg":                             1,
+		"QueryGPUsUsageMax":                             1,
+		"QueryNodeGPUPricePerHr":                        1,
+		"QueryGPUInfo":                                  1,
+		"QueryIsGPUShared":                              1,
+		"QueryDCGMDeviceInfo":                           1,
+		"QueryDCGMDeviceUptime":                         1,
+		"QueryDCGMContainerUsageAvg":                    1,
+		"QueryDCGMContainerUsageMax":                    1,
+		"QueryPodPVCAllocation":                         1,
+		"QueryPVCBytesRequested":                        1,
+		"QueryPVCInfo":                                  1,
+		"QueryKMPVCInfo":                                1,
+		"QueryPVCUptime":                                1,
+		"QueryPVCBytesUsedAverage":                      1,
+		"QueryPVCBytesUsedMax":                          1,
+		"QueryPVBytes":                                  1,
+		"QueryPVPricePerGiBHour":                        1,
+		"QueryPVInfo":                                   1,
+		"QueryPVActiveMinutes":                          1,
+		"QueryPVUsedAverage":                            1,
+		"QueryPVUsedMax":                                1,
+		"QueryKMPVInfo":                                 1,
+		"QueryPVUptime":                                 1,
+		"QueryDeploymentInfo":                           1,
+		"QueryDeploymentUptime":                         1,
+		"QueryDeploymentLabels":                         1,
+		"QueryDeploymentAnnotations":                    1,
+		"QueryDeploymentMatchLabels":                    1,
+		"QueryStatefulSetInfo":                          1,
+		"QueryStatefulSetUptime":                        1,
+		"QueryStatefulSetLabels":                        1,
+		"QueryStatefulSetAnnotations":                   1,
+		"QueryStatefulSetMatchLabels":                   1,
+		"QueryDaemonSetInfo":                            1,
+		"QueryDaemonSetUptime":                          1,
+		"QueryDaemonSetLabels":                          1,
+		"QueryDaemonSetAnnotations":                     1,
+		"QueryJobInfo":                                  1,
+		"QueryJobUptime":                                1,
+		"QueryJobLabels":                                1,
+		"QueryJobAnnotations":                           1,
+		"QueryCronJobInfo":                              1,
+		"QueryCronJobUptime":                            1,
+		"QueryCronJobLabels":                            1,
+		"QueryCronJobAnnotations":                       1,
+		"QueryReplicaSetInfo":                           1,
+		"QueryReplicaSetUptime":                         1,
+		"QueryReplicaSetLabels":                         1,
+		"QueryReplicaSetAnnotations":                    1,
+		"QueryReplicaSetOwners":                         1,
+		"QueryNamespaceInfo":                            1,
+		"QueryNamespaceUptime":                          1,
+		"QueryServiceInfo":                              1,
+		"QueryServiceUptime":                            1,
+		"QueryServiceSelectorLabels":                    1,
+		"QueryNetZoneGiB":                               1,
+		"QueryNetZonePricePerGiB":                       1,
+		"QueryNetRegionGiB":                             1,
+		"QueryNetRegionPricePerGiB":                     1,
+		"QueryNetInternetGiB":                           1,
+		"QueryNetInternetPricePerGiB":                   1,
+		"QueryNetInternetServiceGiB":                    1,
+		"QueryNetNatGatewayPricePerGiB":                 1,
+		"QueryNetNatGatewayGiB":                         1,
+		"QueryNetTransferBytes":                         1,
+		"QueryNetZoneIngressGiB":                        1,
+		"QueryNetRegionIngressGiB":                      1,
+		"QueryNetInternetIngressGiB":                    1,
+		"QueryNetInternetServiceIngressGiB":             1,
+		"QueryNetNatGatewayIngressPricePerGiB":          1,
+		"QueryNetNatGatewayIngressGiB":                  1,
+		"QueryNetReceiveBytes":                          1,
+		"QueryNamespaceAnnotations":                     1,
+		"QueryPodAnnotations":                           1,
+		"QueryNodeLabels":                               1,
+		"QueryNamespaceLabels":                          1,
+		"QueryPodLabels":                                1,
+		"QueryPodsWithDaemonSetOwner":                   1,
+		"QueryPodsWithJobOwner":                         1,
+		"QueryPodsWithReplicaSetOwner":                  1,
+		"QueryReplicaSetsWithoutOwners":                 1,
+		"QueryReplicaSetsWithRollout":                   1,
+		"QueryResourceQuotaInfo":                        1,
+		"QueryResourceQuotaUptime":                      1,
+		"QueryResourceQuotaSpecCPURequestAverage":       1,
+		"QueryResourceQuotaSpecCPURequestMax":           1,
+		"QueryResourceQuotaSpecRAMRequestAverage":       1,
+		"QueryResourceQuotaSpecRAMRequestMax":           1,
+		"QueryResourceQuotaSpecCPULimitAverage":         1,
+		"QueryResourceQuotaSpecCPULimitMax":             1,
+		"QueryResourceQuotaSpecRAMLimitAverage":         1,
+		"QueryResourceQuotaSpecRAMLimitMax":             1,
+		"QueryResourceQuotaStatusUsedCPURequestAverage": 1,
+		"QueryResourceQuotaStatusUsedCPURequestMax":     1,
+		"QueryResourceQuotaStatusUsedRAMRequestAverage": 1,
+		"QueryResourceQuotaStatusUsedRAMRequestMax":     1,
+		"QueryResourceQuotaStatusUsedCPULimitAverage":   1,
+		"QueryResourceQuotaStatusUsedCPULimitMax":       1,
+		"QueryResourceQuotaStatusUsedRAMLimitAverage":   1,
+		"QueryResourceQuotaStatusUsedRAMLimitMax":       1,
+		"QueryDataCoverage":                             1,
+	}
+
+	require.Equal(t, expected, r.Calls)
+}

+ 20 - 0
modules/collector-source/pkg/collector/collector.go

@@ -50,6 +50,7 @@ func NewOpenCostMetricStore() metric.MetricStore {
 	memStore.Register(NewLBPricePerHourMetricCollector())
 	memStore.Register(NewLBActiveMinutesMetricCollector())
 	memStore.Register(NewClusterInfoMetricCollector())
+	memStore.Register(NewClusterCompleteKubeModelMetricCollector())
 	memStore.Register(NewClusterUptimeMetricCollector())
 	memStore.Register(NewClusterManagementDurationMetricCollector())
 	memStore.Register(NewClusterManagementPricePerHourMetricCollector())
@@ -840,6 +841,25 @@ func NewClusterInfoMetricCollector() *metric.MetricCollector {
 	)
 }
 
+//	avg(
+//		cluster_info{
+//			<some_custom_filter>
+//		}
+//	) by (uid, complete_kubemodel)[%s:%dm]
+
+func NewClusterCompleteKubeModelMetricCollector() *metric.MetricCollector {
+	return metric.NewMetricCollector(
+		metric.ClusterCompleteKubeModelID,
+		metric.ClusterInfo,
+		[]string{
+			source.UIDLabel,
+			source.KubeModelVersion,
+		},
+		aggregator.Info,
+		nil,
+	)
+}
+
 //	avg(
 //		cluster_info{
 //			<some_custom_filter>

+ 4 - 0
modules/collector-source/pkg/collector/metricsquerier.go

@@ -223,6 +223,10 @@ func (c *collectorMetricsQuerier) QueryClusterInfo(start, end time.Time) *source
 	return queryCollector(c, start, end, metric.ClusterInfoID, source.DecodeClusterInfoResult)
 }
 
+func (c *collectorMetricsQuerier) QueryClusterKubeModelVersion(start, end time.Time) *source.Future[source.ClusterKubeModelVersionResult] {
+	return queryCollector(c, start, end, metric.ClusterCompleteKubeModelID, source.DecodeClusterKubeModelVersionResult)
+}
+
 func (c *collectorMetricsQuerier) QueryClusterUptime(start, end time.Time) *source.Future[source.UptimeResult] {
 	return queryCollector(c, start, end, metric.ClusterUptimeID, source.DecodeUptimeResult)
 }

+ 2 - 1
modules/collector-source/pkg/metric/collector.go

@@ -48,6 +48,7 @@ const (
 	LBPricePerHourID                           MetricCollectorID = "LBPricePerHour"
 	LBActiveMinutesID                          MetricCollectorID = "LBActiveMinutes"
 	ClusterInfoID                              MetricCollectorID = "ClusterInfo"
+	ClusterCompleteKubeModelID                 MetricCollectorID = "ClusterCompleteKubeModel"
 	ClusterUptimeID                            MetricCollectorID = "ClusterUptime"
 	ClusterManagementDurationID                MetricCollectorID = "ClusterManagementDuration"
 	ClusterManagementPricePerHourID            MetricCollectorID = "ClusterManagementPricePerHour"
@@ -64,7 +65,7 @@ const (
 	RAMLimitsID                                MetricCollectorID = "RAMLimits"
 	RAMUsageAverageID                          MetricCollectorID = "RAMUsageAverage"
 	RAMUsageMaxID                              MetricCollectorID = "RAMBytesUsageMax"
-	CPUCoresAllocatedID                        MetricCollectorID = "CPUCoresAllocated"
+	CPUCoresAllocatedID                        MetricCollectorID = "CPUCoreAllocationAvg"
 	CPURequestsID                              MetricCollectorID = "CPURequestsID"
 	CPULimitsID                                MetricCollectorID = "CPULimitsID"
 	CPUUsageAverageID                          MetricCollectorID = "CPUUsageAverage"

+ 4 - 0
modules/collector-source/pkg/scrape/clusterinfo.go

@@ -1,7 +1,10 @@
 package scrape
 
 import (
+	"fmt"
+
 	"github.com/opencost/opencost/core/pkg/clusters"
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
 	"github.com/opencost/opencost/core/pkg/source"
 	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
 )
@@ -43,6 +46,7 @@ func (cis *ClusterInfoScrapper) Scrape() []metric.Update {
 		source.AccountIDLabel:       accountID,
 		source.ProvisionerNameLabel: provisioner,
 		source.RegionLabel:          region,
+		source.KubeModelVersion:     fmt.Sprintf("%d", kubemodel.DefaultCodecVersion),
 	}
 
 	scrapeResults = append(scrapeResults, metric.Update{

+ 60 - 6
modules/collector-source/pkg/scrape/scrapecontroller.go

@@ -6,6 +6,7 @@ import (
 
 	"github.com/opencost/opencost/core/pkg/clustercache"
 	"github.com/opencost/opencost/core/pkg/clusters"
+	coreenv "github.com/opencost/opencost/core/pkg/env"
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/nodestats"
 	"github.com/opencost/opencost/core/pkg/util/atomic"
@@ -21,6 +22,57 @@ type ScrapeController struct {
 	updater        metric.Updater
 }
 
+// getDefaultMetricFilter builds a MetricFilter from environment variable configuration.
+// Metrics whose corresponding env var is false (the default) are added to the deny set.
+func getDefaultMetricFilter() MetricFilter {
+	f := MetricFilter{}
+	deny := func(name string) { f[name] = struct{}{} }
+
+	if !coreenv.IsEmitPodAnnotationsMetric() {
+		deny(metric.KubePodAnnotations)
+	}
+	if !coreenv.IsEmitNamespaceAnnotationsMetric() {
+		deny(metric.KubeNamespaceAnnotations)
+	}
+	if !coreenv.IsEmitDeploymentLabelsMetric() {
+		deny(metric.DeploymentLabels)
+	}
+	if !coreenv.IsEmitDeploymentAnnotationsMetric() {
+		deny(metric.DeploymentAnnotations)
+	}
+	if !coreenv.IsEmitStatefulSetLabelsMetric() {
+		deny(metric.StatefulSetLabels)
+	}
+	if !coreenv.IsEmitStatefulSetAnnotationsMetric() {
+		deny(metric.StatefulSetAnnotations)
+	}
+	if !coreenv.IsEmitDaemonSetLabelsMetric() {
+		deny(metric.DaemonSetLabels)
+	}
+	if !coreenv.IsEmitDaemonSetAnnotationsMetric() {
+		deny(metric.DaemonSetAnnotations)
+	}
+	if !coreenv.IsEmitJobLabelsMetric() {
+		deny(metric.JobLabels)
+	}
+	if !coreenv.IsEmitJobAnnotationsMetric() {
+		deny(metric.JobAnnotations)
+	}
+	if !coreenv.IsEmitCronJobLabelsMetric() {
+		deny(metric.CronJobLabels)
+	}
+	if !coreenv.IsEmitCronJobAnnotationsMetric() {
+		deny(metric.CronJobAnnotations)
+	}
+	if !coreenv.IsEmitReplicaSetLabelsMetric() {
+		deny(metric.ReplicaSetLabels)
+	}
+	if !coreenv.IsEmitReplicaSetAnnotationsMetric() {
+		deny(metric.ReplicaSetAnnotations)
+	}
+	return f
+}
+
 func NewScrapeController(
 	clusterUID string,
 	scrapeInterval string,
@@ -30,24 +82,26 @@ func NewScrapeController(
 	clusterCache clustercache.ClusterCache,
 	statSummaryClient nodestats.StatSummaryClient,
 ) *ScrapeController {
+	// Start with env-driven defaults, then layer in any caller-supplied entries.
+	filter := getDefaultMetricFilter()
 
 	var scrapers []Scraper
-	clusterInfoScrapper := newClusterInfoScrapper(clusterUID, clusterInfoProvider)
+	clusterInfoScrapper := withFilter(newClusterInfoScrapper(clusterUID, clusterInfoProvider), filter)
 	scrapers = append(scrapers, clusterInfoScrapper)
 
-	clusterCacheScraper := newClusterCacheScraper(clusterCache)
+	clusterCacheScraper := withFilter(newClusterCacheScraper(clusterCache), filter)
 	scrapers = append(scrapers, clusterCacheScraper)
 
-	opencostScraper := newOpenCostScraper()
+	opencostScraper := withFilter(newOpenCostScraper(), filter)
 	scrapers = append(scrapers, opencostScraper)
 
-	statSummaryScraper := newStatSummaryScraper(statSummaryClient, clusterCache)
+	statSummaryScraper := withFilter(newStatSummaryScraper(statSummaryClient, clusterCache), filter)
 	scrapers = append(scrapers, statSummaryScraper)
 
-	networkScraper := newNetworkScraper(networkPort, clusterCache)
+	networkScraper := withFilter(newNetworkScraper(networkPort, clusterCache), filter)
 	scrapers = append(scrapers, networkScraper)
 
-	dcgmScraper := newDCGMScrapper(clusterCache)
+	dcgmScraper := withFilter(newDCGMScrapper(clusterCache), filter)
 	scrapers = append(scrapers, dcgmScraper)
 
 	si, err := util.NewInterval(scrapeInterval)

+ 30 - 0
modules/collector-source/pkg/scrape/scraper.go

@@ -9,6 +9,36 @@ type Scraper interface {
 	Scrape() []metric.Update
 }
 
+// MetricFilter is a set of metric names to suppress from scrape output.
+// An empty or nil filter allows all metrics through.
+type MetricFilter map[string]struct{}
+
+// filteredScraper wraps a Scraper and removes any Update whose Name appears in the filter.
+type filteredScraper struct {
+	inner  Scraper
+	filter MetricFilter
+}
+
+// withFilter returns s wrapped with f so that denied metric names are stripped from Scrape output.
+// If f is empty, s is returned unchanged.
+func withFilter(s Scraper, f MetricFilter) Scraper {
+	if len(f) == 0 {
+		return s
+	}
+	return &filteredScraper{inner: s, filter: f}
+}
+
+func (fs *filteredScraper) Scrape() []metric.Update {
+	results := fs.inner.Scrape()
+	out := results[:0]
+	for _, u := range results {
+		if _, denied := fs.filter[u.Name]; !denied {
+			out = append(out, u)
+		}
+	}
+	return out
+}
+
 type ScrapeFunc func() []metric.Update
 
 func concurrentScrape(scrapeFuncs ...ScrapeFunc) []metric.Update {

+ 226 - 0
modules/collector-source/pkg/scrape/scraper_test.go

@@ -0,0 +1,226 @@
+package scrape
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	coreenv "github.com/opencost/opencost/core/pkg/env"
+	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
+)
+
+// staticScraper is a test double that returns a fixed set of updates.
+type staticScraper struct {
+	updates []metric.Update
+}
+
+func (s *staticScraper) Scrape() []metric.Update { return s.updates }
+
+func updates(names ...string) []metric.Update {
+	out := make([]metric.Update, len(names))
+	for i, n := range names {
+		out[i] = metric.Update{Name: n}
+	}
+	return out
+}
+
+func updateNames(us []metric.Update) []string {
+	names := make([]string, len(us))
+	for i, u := range us {
+		names[i] = u.Name
+	}
+	return names
+}
+
+func TestWithFilter_EmptyFilterReturnsOriginalScraper(t *testing.T) {
+	s := &staticScraper{}
+	wrapped := withFilter(s, MetricFilter{})
+	assert.Same(t, s, wrapped, "empty filter should return the original scraper unchanged")
+}
+
+func TestWithFilter_NilFilterReturnsOriginalScraper(t *testing.T) {
+	s := &staticScraper{}
+	wrapped := withFilter(s, nil)
+	assert.Same(t, s, wrapped, "nil filter should return the original scraper unchanged")
+}
+
+func TestWithFilter_NonEmptyFilterWraps(t *testing.T) {
+	s := &staticScraper{}
+	f := MetricFilter{"some_metric": {}}
+	wrapped := withFilter(s, f)
+	_, isFiltered := wrapped.(*filteredScraper)
+	assert.True(t, isFiltered, "non-empty filter should wrap in a filteredScraper")
+}
+
+func TestFilteredScraper_AllowsMetricsNotInFilter(t *testing.T) {
+	s := &staticScraper{updates: updates("cpu_usage", "memory_usage")}
+	f := MetricFilter{"kube_pod_annotations": {}}
+	wrapped := withFilter(s, f)
+
+	got := wrapped.Scrape()
+	assert.Equal(t, []string{"cpu_usage", "memory_usage"}, updateNames(got))
+}
+
+func TestFilteredScraper_BlocksDeniedMetrics(t *testing.T) {
+	s := &staticScraper{updates: updates("cpu_usage", metric.KubePodAnnotations, "memory_usage")}
+	f := MetricFilter{metric.KubePodAnnotations: {}}
+	wrapped := withFilter(s, f)
+
+	got := wrapped.Scrape()
+	assert.Equal(t, []string{"cpu_usage", "memory_usage"}, updateNames(got))
+}
+
+func TestFilteredScraper_BlocksMultipleDeniedMetrics(t *testing.T) {
+	s := &staticScraper{updates: updates(
+		metric.KubePodAnnotations,
+		"cpu_usage",
+		metric.KubeNamespaceAnnotations,
+		"memory_usage",
+	)}
+	f := MetricFilter{
+		metric.KubePodAnnotations:       {},
+		metric.KubeNamespaceAnnotations: {},
+	}
+	wrapped := withFilter(s, f)
+
+	got := wrapped.Scrape()
+	assert.Equal(t, []string{"cpu_usage", "memory_usage"}, updateNames(got))
+}
+
+func TestFilteredScraper_EmptyResultsPassThrough(t *testing.T) {
+	s := &staticScraper{updates: []metric.Update{}}
+	f := MetricFilter{metric.KubePodAnnotations: {}}
+	wrapped := withFilter(s, f)
+
+	got := wrapped.Scrape()
+	assert.Empty(t, got)
+}
+
+func TestFilteredScraper_AllMetricsDenied(t *testing.T) {
+	s := &staticScraper{updates: updates(metric.KubePodAnnotations, metric.KubeNamespaceAnnotations)}
+	f := MetricFilter{
+		metric.KubePodAnnotations:       {},
+		metric.KubeNamespaceAnnotations: {},
+	}
+	wrapped := withFilter(s, f)
+
+	got := wrapped.Scrape()
+	assert.Empty(t, got)
+}
+
+func TestGetDefaultMetricFilter_PodAnnotationsDisabled(t *testing.T) {
+	t.Setenv(coreenv.EmitPodAnnotationsMetricEnvVar, "false")
+	t.Setenv(coreenv.EmitNamespaceAnnotationsMetricEnvVar, "true")
+
+	f := getDefaultMetricFilter()
+
+	_, podDenied := f[metric.KubePodAnnotations]
+	_, nsDenied := f[metric.KubeNamespaceAnnotations]
+	assert.True(t, podDenied, "pod annotations should be denied when env var is false")
+	assert.False(t, nsDenied, "namespace annotations should be allowed when env var is true")
+}
+
+func TestGetDefaultMetricFilter_NamespaceAnnotationsDisabled(t *testing.T) {
+	t.Setenv(coreenv.EmitPodAnnotationsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitNamespaceAnnotationsMetricEnvVar, "false")
+
+	f := getDefaultMetricFilter()
+
+	_, podDenied := f[metric.KubePodAnnotations]
+	_, nsDenied := f[metric.KubeNamespaceAnnotations]
+	assert.False(t, podDenied, "pod annotations should be allowed when env var is true")
+	assert.True(t, nsDenied, "namespace annotations should be denied when env var is false")
+}
+
+func TestGetDefaultMetricFilter_BothDisabledByDefault(t *testing.T) {
+	t.Setenv(coreenv.EmitPodAnnotationsMetricEnvVar, "")
+	t.Setenv(coreenv.EmitNamespaceAnnotationsMetricEnvVar, "")
+
+	f := getDefaultMetricFilter()
+
+	_, podDenied := f[metric.KubePodAnnotations]
+	_, nsDenied := f[metric.KubeNamespaceAnnotations]
+	assert.True(t, podDenied, "pod annotations should be denied by default")
+	assert.True(t, nsDenied, "namespace annotations should be denied by default")
+}
+
+func TestGetDefaultMetricFilter_AllEnabled(t *testing.T) {
+	t.Setenv(coreenv.EmitPodAnnotationsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitNamespaceAnnotationsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitDeploymentLabelsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitDeploymentAnnotationsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitStatefulSetLabelsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitStatefulSetAnnotationsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitDaemonSetLabelsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitDaemonSetAnnotationsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitJobLabelsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitJobAnnotationsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitCronJobLabelsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitCronJobAnnotationsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitReplicaSetLabelsMetricEnvVar, "true")
+	t.Setenv(coreenv.EmitReplicaSetAnnotationsMetricEnvVar, "true")
+
+	f := getDefaultMetricFilter()
+
+	assert.Empty(t, f, "filter should be empty when all metrics are enabled")
+}
+
+func TestGetDefaultMetricFilter_AnnotationsDefaultFalse(t *testing.T) {
+	// Unset all env vars to exercise pure defaults.
+	for _, v := range []string{
+		coreenv.EmitPodAnnotationsMetricEnvVar,
+		coreenv.EmitNamespaceAnnotationsMetricEnvVar,
+		coreenv.EmitDeploymentAnnotationsMetricEnvVar,
+		coreenv.EmitStatefulSetAnnotationsMetricEnvVar,
+		coreenv.EmitDaemonSetAnnotationsMetricEnvVar,
+		coreenv.EmitJobAnnotationsMetricEnvVar,
+		coreenv.EmitCronJobAnnotationsMetricEnvVar,
+		coreenv.EmitReplicaSetAnnotationsMetricEnvVar,
+	} {
+		t.Setenv(v, "")
+	}
+
+	f := getDefaultMetricFilter()
+
+	for _, name := range []string{
+		metric.KubePodAnnotations,
+		metric.KubeNamespaceAnnotations,
+		metric.DeploymentAnnotations,
+		metric.StatefulSetAnnotations,
+		metric.DaemonSetAnnotations,
+		metric.JobAnnotations,
+		metric.CronJobAnnotations,
+		metric.ReplicaSetAnnotations,
+	} {
+		_, denied := f[name]
+		assert.True(t, denied, "%s should be denied by default", name)
+	}
+}
+
+func TestGetDefaultMetricFilter_LabelsDefaultTrue(t *testing.T) {
+	// Unset all label env vars to exercise pure defaults.
+	for _, v := range []string{
+		coreenv.EmitDeploymentLabelsMetricEnvVar,
+		coreenv.EmitStatefulSetLabelsMetricEnvVar,
+		coreenv.EmitDaemonSetLabelsMetricEnvVar,
+		coreenv.EmitJobLabelsMetricEnvVar,
+		coreenv.EmitCronJobLabelsMetricEnvVar,
+		coreenv.EmitReplicaSetLabelsMetricEnvVar,
+	} {
+		t.Setenv(v, "")
+	}
+
+	f := getDefaultMetricFilter()
+
+	for _, name := range []string{
+		metric.DeploymentLabels,
+		metric.StatefulSetLabels,
+		metric.DaemonSetLabels,
+		metric.JobLabels,
+		metric.CronJobLabels,
+		metric.ReplicaSetLabels,
+	} {
+		_, denied := f[name]
+		assert.False(t, denied, "%s should be allowed by default", name)
+	}
+}

+ 18 - 0
modules/prometheus-source/pkg/prom/metricsquerier.go

@@ -661,6 +661,24 @@ func (pds *PrometheusMetricsQuerier) QueryClusterInfo(start, end time.Time) *sou
 	return source.NewFuture(source.DecodeClusterInfoResult, ctx.QueryAtTime(queryClusterInfo, end))
 }
 
+func (pds *PrometheusMetricsQuerier) QueryClusterKubeModelVersion(start, end time.Time) *source.Future[source.ClusterKubeModelVersionResult] {
+	const queryName = "QueryClusterKubeModelVersion"
+	const queryFmtClusterCompleteKubeModel = `avg(avg_over_time(cluster_info{%s}[%s])) by (%s, uid, complete_kubemodel)`
+
+	cfg := pds.promConfig
+
+	durStr := timeutil.DurationString(end.Sub(start))
+	if durStr == "" {
+		panic(fmt.Sprintf("failed to parse duration string passed to %s", queryName))
+	}
+
+	queryClusterCompleteKubeModel := fmt.Sprintf(queryFmtClusterCompleteKubeModel, cfg.ClusterFilter, durStr, cfg.ClusterLabel)
+	log.Debugf(PrometheusMetricsQueryLogFormat, queryName, end.Unix(), queryClusterCompleteKubeModel)
+
+	ctx := pds.promContexts.NewNamedContext(ClusterContextName)
+	return source.NewFuture(source.DecodeClusterKubeModelVersionResult, ctx.QueryAtTime(queryClusterCompleteKubeModel, end))
+}
+
 func (pds *PrometheusMetricsQuerier) QueryClusterUptime(start, end time.Time) *source.Future[source.UptimeResult] {
 	const queryName = "QueryClusterUptime"
 	const queryFmtClusterUptime = `avg(cluster_info{%s}) by (%s, uid)[%s:%dm]`

+ 2 - 1
pkg/costmodel/costmodel.go

@@ -73,7 +73,8 @@ func NewCostModel(
 	var kubeModel *km.KubeModel
 	var err error
 	if dataSource != nil {
-		kubeModel, err = km.NewKubeModel(clusterUID, dataSource)
+
+		kubeModel, err = km.NewKubeModel(clusterUID, coreenv.IsKubeModelV1Forced(), dataSource)
 		if err != nil {
 			// KubeModel is required. Log a fatal error if we fail to init.
 			log.Fatalf("error initializing KubeModel: %s", err)

+ 6 - 5
pkg/costmodel/metrics.go

@@ -9,6 +9,7 @@ import (
 
 	"github.com/opencost/opencost/core/pkg/clustercache"
 	"github.com/opencost/opencost/core/pkg/clusters"
+	coreenv "github.com/opencost/opencost/core/pkg/env"
 	"github.com/opencost/opencost/core/pkg/errors"
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/source"
@@ -361,11 +362,11 @@ func NewCostModelMetricsEmitter(clusterCache clustercache.ClusterCache, provider
 
 	metrics.InitKubeMetrics(clusterInfo, clusterCache, metricsConfig, &metrics.KubeMetricsOpts{
 		EmitKubecostControllerMetrics: true,
-		EmitNamespaceAnnotations:      env.IsEmitNamespaceAnnotationsMetric(),
-		EmitPodAnnotations:            env.IsEmitPodAnnotationsMetric(),
-		EmitKubeStateMetrics:          env.IsEmitKsmV1Metrics(),
-		EmitKubeStateMetricsV1Only:    env.IsEmitKsmV1MetricsOnly(),
-		EmitDeprecatedMetrics:         env.IsEmitDeprecatedMetrics(),
+		EmitNamespaceAnnotations:      coreenv.IsEmitNamespaceAnnotationsMetric(),
+		EmitPodAnnotations:            coreenv.IsEmitPodAnnotationsMetric(),
+		EmitKubeStateMetrics:          coreenv.IsEmitKsmV1Metrics(),
+		EmitKubeStateMetricsV1Only:    coreenv.IsEmitKsmV1MetricsOnly(),
+		EmitDeprecatedMetrics:         coreenv.IsEmitDeprecatedMetrics(),
 	})
 
 	metrics.InitOpencostTelemetry(metricsConfig)

+ 11 - 7
pkg/costmodel/router.go

@@ -531,15 +531,19 @@ func Initialize(router *httprouter.Router, additionalConfigWatchers ...*watcher.
 	costModel := NewCostModel(clusterUID, dataSource, cloudProvider, k8sCache, clusterMap, dataSource.BatchDuration())
 	metricsEmitter := NewCostModelMetricsEmitter(k8sCache, cloudProvider, clusterInfoProvider, costModel)
 
-	appName := sysenv.GetAppName()
 	var kubeModelPipeline *km.Pipeline
-	if p, err := km.NewPipeline(appName, clusterUID, store, costModel); err != nil {
-		log.Errorf("Failed to initialize KubeModel pipeline: %v", err)
-	} else {
-		p.Start()
-		kubeModelPipeline = p
+	var kubeModelQuerier km.Querier
+	if sysenv.IsKubeModelExported() {
+		appName := sysenv.GetAppName()
+
+		if p, err := km.NewPipeline(appName, clusterUID, store, costModel); err != nil {
+			log.Errorf("Failed to initialize KubeModel pipeline: %v", err)
+		} else {
+			p.Start()
+			kubeModelPipeline = p
+		}
+		kubeModelQuerier = km.NewQuerier(appName, clusterUID, store)
 	}
-	kubeModelQuerier := km.NewQuerier(appName, clusterUID, store)
 
 	a := &Accesses{
 		DataSource:          dataSource,

+ 0 - 33
pkg/env/costmodel.go

@@ -62,13 +62,6 @@ const (
 	CollectorDataSourceEnabledEnvVar = "COLLECTOR_DATA_SOURCE_ENABLED"
 	LocalCollectorDirectoryEnvVar    = "LOCAL_COLLECTOR_DIRECTORY"
 
-	EmitPodAnnotationsMetricEnvVar       = "EMIT_POD_ANNOTATIONS_METRIC"
-	EmitNamespaceAnnotationsMetricEnvVar = "EMIT_NAMESPACE_ANNOTATIONS_METRIC"
-	EmitDeprecatedMetrics                = "EMIT_DEPRECATED_METRICS"
-
-	EmitKsmV1MetricsEnvVar = "EMIT_KSM_V1_METRICS"
-	EmitKsmV1MetricsOnly   = "EMIT_KSM_V1_METRICS_ONLY"
-
 	LogCollectionEnabledEnvVar    = "LOG_COLLECTION_ENABLED"
 	ProductAnalyticsEnabledEnvVar = "PRODUCT_ANALYTICS_ENABLED"
 	ErrorReportingEnabledEnvVar   = "ERROR_REPORTING_ENABLED"
@@ -157,32 +150,6 @@ func GetMetricsConfigmapName() string {
 	return env.Get(MetricsConfigmapName, "metrics-config")
 }
 
-// IsEmitNamespaceAnnotationsMetric returns true if cost-model is configured to emit the kube_namespace_annotations metric
-// containing the namespace annotations
-func IsEmitNamespaceAnnotationsMetric() bool {
-	return env.GetBool(EmitNamespaceAnnotationsMetricEnvVar, false)
-}
-
-// IsEmitPodAnnotationsMetric returns true if cost-model is configured to emit the kube_pod_annotations metric containing
-// pod annotations.
-func IsEmitPodAnnotationsMetric() bool {
-	return env.GetBool(EmitPodAnnotationsMetricEnvVar, false)
-}
-
-// IsEmitKsmV1Metrics returns true if cost-model is configured to emit all necessary KSM v1
-// metrics that were removed in KSM v2
-func IsEmitKsmV1Metrics() bool {
-	return env.GetBool(EmitKsmV1MetricsEnvVar, true)
-}
-
-func IsEmitKsmV1MetricsOnly() bool {
-	return env.GetBool(EmitKsmV1MetricsOnly, false)
-}
-
-func IsEmitDeprecatedMetrics() bool {
-	return env.GetBool(EmitDeprecatedMetrics, false)
-}
-
 // GetAWSAccessKeyID returns the environment variable value for AWSAccessKeyIDEnvVar which represents
 // the AWS access key for authentication
 func GetAWSAccessKeyID() string {

+ 1 - 1
pkg/kubemodel/pipeline.go

@@ -30,7 +30,7 @@ func NewPipeline(appName, clusterUID string, store storage.Storage, cm ocexporte
 		return nil, fmt.Errorf("NewPipeline: clusterUID cannot be empty")
 	}
 
-	config := ocexporter.NewPipelinesExportConfig(appName, clusterUID, "", false, env.GetExportKubeModel())
+	config := ocexporter.NewPipelinesExportConfig(appName, clusterUID, "", false, env.IsKubeModelExported())
 
 	controllers := ocexporter.NewPipelineExportControllers(store, cm, config)
 

+ 8 - 5
pkg/metrics/kubemodel.go

@@ -6,6 +6,8 @@ import (
 	"github.com/opencost/opencost/core/pkg/clustercache"
 	"github.com/opencost/opencost/core/pkg/clusters"
 	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
 	coreutil "github.com/opencost/opencost/core/pkg/util"
 	"github.com/opencost/opencost/core/pkg/util/promutil"
 	"github.com/prometheus/client_golang/prometheus"
@@ -184,11 +186,12 @@ func (c KubeModelCollector) scrapeClusterInfo(disabled map[string]struct{}) []ku
 	}
 	info := c.ClusterInfo.GetClusterInfo()
 	labels := map[string]string{
-		"uid":              info[clusters.ClusterInfoIdKey],
-		"provider":         info[clusters.ClusterInfoProviderKey],
-		"account_id":       info[clusters.ClusterInfoAccountKey],
-		"provisioner_name": info[clusters.ClusterInfoProvisionerKey],
-		"region":           info[clusters.ClusterInfoRegionKey],
+		"uid":                   info[clusters.ClusterInfoIdKey],
+		"provider":              info[clusters.ClusterInfoProviderKey],
+		"account_id":            info[clusters.ClusterInfoAccountKey],
+		"provisioner_name":      info[clusters.ClusterInfoProvisionerKey],
+		"region":                info[clusters.ClusterInfoRegionKey],
+		source.KubeModelVersion: fmt.Sprintf("%d", kubemodel.DefaultCodecVersion),
 	}
 	// GCP uses "project" instead of "account"
 	if labels["account_id"] == "" {