Explorar o código

KubeModel Changes

Signed-off-by: Sean Holcomb <seanholcomb@gmail.com>
Sean Holcomb hai 1 semana
pai
achega
190033c447
Modificáronse 100 ficheiros con 16756 adicións e 3259 borrados
  1. 41 8
      core/pkg/clustercache/clustercache.go
  2. 15 0
      core/pkg/clustercache/helper.go
  3. 89 0
      core/pkg/clustercache/helper_test.go
  4. 49 0
      core/pkg/clustercache/mock.go
  5. 1 1
      core/pkg/exporter/controller.go
  6. 13 6
      core/pkg/exporter/encoder.go
  7. 2 10
      core/pkg/exporter/pathing/bingenpath.go
  8. 53 0
      core/pkg/exporter/pathing/staticpath.go
  9. 143 0
      core/pkg/exporter/pathing/staticpath_test.go
  10. 3 1
      core/pkg/model/kubemodel/bingen.go
  11. 30 14
      core/pkg/model/kubemodel/cluster.go
  12. 131 0
      core/pkg/model/kubemodel/cluster_test.go
  13. 29 82
      core/pkg/model/kubemodel/container.go
  14. 123 0
      core/pkg/model/kubemodel/container_test.go
  15. 58 0
      core/pkg/model/kubemodel/cronjob.go
  16. 148 0
      core/pkg/model/kubemodel/cronjob_test.go
  17. 59 0
      core/pkg/model/kubemodel/daemonset.go
  18. 148 0
      core/pkg/model/kubemodel/daemonset_test.go
  19. 66 0
      core/pkg/model/kubemodel/dcgm.go
  20. 138 0
      core/pkg/model/kubemodel/dcgm_test.go
  21. 59 0
      core/pkg/model/kubemodel/deployment.go
  22. 148 0
      core/pkg/model/kubemodel/deployment_test.go
  23. 0 91
      core/pkg/model/kubemodel/device.go
  24. 0 85
      core/pkg/model/kubemodel/device_usage.go
  25. 58 0
      core/pkg/model/kubemodel/job.go
  26. 148 0
      core/pkg/model/kubemodel/job_test.go
  27. 53 24
      core/pkg/model/kubemodel/kubemodel.go
  28. 7611 558
      core/pkg/model/kubemodel/kubemodel_codecs.go
  29. 21 456
      core/pkg/model/kubemodel/kubemodel_codecs_test.go
  30. 63 0
      core/pkg/model/kubemodel/kubemodel_helpers_test.go
  31. 109 65
      core/pkg/model/kubemodel/kubemodel_test.go
  32. 0 627
      core/pkg/model/kubemodel/merge.go
  33. 266 0
      core/pkg/model/kubemodel/mock.go
  34. 32 22
      core/pkg/model/kubemodel/namespace.go
  35. 143 0
      core/pkg/model/kubemodel/namespace_test.go
  36. 28 0
      core/pkg/model/kubemodel/networktrafficdetail.go
  37. 31 70
      core/pkg/model/kubemodel/node.go
  38. 143 0
      core/pkg/model/kubemodel/node_test.go
  39. 28 39
      core/pkg/model/kubemodel/owner.go
  40. 41 0
      core/pkg/model/kubemodel/owner_test.go
  41. 45 27
      core/pkg/model/kubemodel/pod.go
  42. 148 0
      core/pkg/model/kubemodel/pod_test.go
  43. 0 14
      core/pkg/model/kubemodel/provider.go
  44. 28 50
      core/pkg/model/kubemodel/pv.go
  45. 123 0
      core/pkg/model/kubemodel/pv_test.go
  46. 35 36
      core/pkg/model/kubemodel/pvc.go
  47. 128 0
      core/pkg/model/kubemodel/pvc_test.go
  48. 59 0
      core/pkg/model/kubemodel/replicaset.go
  49. 148 0
      core/pkg/model/kubemodel/replicaset_test.go
  50. 5 4
      core/pkg/model/kubemodel/resource.go
  51. 35 16
      core/pkg/model/kubemodel/resourcequota.go
  52. 135 0
      core/pkg/model/kubemodel/resourcequota_test.go
  53. 65 22
      core/pkg/model/kubemodel/service.go
  54. 164 0
      core/pkg/model/kubemodel/service_test.go
  55. 59 0
      core/pkg/model/kubemodel/statefulset.go
  56. 148 0
      core/pkg/model/kubemodel/statefulset_test.go
  57. 1 2
      core/pkg/model/kubemodel/unit.go
  58. 69 0
      core/pkg/model/pricingmodel/pricingmodel.go
  59. 36 0
      core/pkg/model/shared/provider.go
  60. 55 0
      core/pkg/model/shared/provider_test.go
  61. 9 0
      core/pkg/model/shared/usagetype.go
  62. 3 0
      core/pkg/nodestats/nodes_test.go
  63. 8 3
      core/pkg/opencost/exporter/controllers.go
  64. 48 37
      core/pkg/opencost/exporter/exporter_test.go
  65. 113 5
      core/pkg/opencost/exporter/exporters.go
  66. 0 22
      core/pkg/opencost/mock.go
  67. 100 24
      core/pkg/source/datasource.go
  68. 617 169
      core/pkg/source/decoders.go
  69. 20 0
      core/pkg/source/queryresult.go
  70. 33 0
      core/pkg/storage/storefactory.go
  71. 80 81
      go.mod
  72. 28 42
      go.sum
  73. 3 0
      justfile
  74. 3 0
      modules/collector-source/go.mod
  75. 796 23
      modules/collector-source/pkg/collector/collector.go
  76. 265 41
      modules/collector-source/pkg/collector/metricsquerier.go
  77. 1 1
      modules/collector-source/pkg/collector/metricsquerier_test.go
  78. 3 0
      modules/collector-source/pkg/event/scrape.go
  79. 57 1
      modules/collector-source/pkg/metric/collector.go
  80. 25 1
      modules/collector-source/pkg/metric/metrics.go
  81. 0 6
      modules/collector-source/pkg/metric/repository.go
  82. 617 151
      modules/collector-source/pkg/scrape/clustercache.go
  83. 715 89
      modules/collector-source/pkg/scrape/clustercache_test.go
  84. 48 0
      modules/collector-source/pkg/scrape/index.go
  85. 75 0
      modules/collector-source/pkg/scrape/index_test.go
  86. 1 1
      modules/collector-source/pkg/scrape/scrapecontroller.go
  87. 25 6
      modules/collector-source/pkg/scrape/statsummary.go
  88. 53 10
      modules/collector-source/pkg/scrape/statsummary_test.go
  89. 1123 120
      modules/prometheus-source/pkg/prom/metricsquerier.go
  90. 7 3
      modules/prometheus-source/pkg/prom/metricsquerier_test.go
  91. 4 23
      pkg/cloud/provider/cloud_test.go
  92. 14 1
      pkg/clustercache/clustercache.go
  93. 8 1
      pkg/clustercache/clustercache2.go
  94. 5 0
      pkg/cmd/costmodel/costmodel.go
  95. 16 16
      pkg/costmodel/allocation.go
  96. 5 5
      pkg/costmodel/allocation_helpers.go
  97. 1 1
      pkg/costmodel/cluster.go
  98. 3 7
      pkg/costmodel/cluster_helpers.go
  99. 1 9
      pkg/costmodel/costmodel.go
  100. 16 30
      pkg/costmodel/costmodel_test.go

+ 41 - 8
core/pkg/clustercache/clustercache.go

@@ -71,9 +71,11 @@ type Service struct {
 }
 }
 
 
 type DaemonSet struct {
 type DaemonSet struct {
+	UID            types.UID
 	Name           string
 	Name           string
 	Namespace      string
 	Namespace      string
 	Labels         map[string]string
 	Labels         map[string]string
+	Annotations    map[string]string
 	SpecContainers []v1.Container
 	SpecContainers []v1.Container
 }
 }
 
 
@@ -122,10 +124,12 @@ type StorageClass struct {
 }
 }
 
 
 type Job struct {
 type Job struct {
-	UID       types.UID
-	Name      string
-	Namespace string
-	Status    batchv1.JobStatus
+	UID         types.UID
+	Name        string
+	Namespace   string
+	Labels      map[string]string
+	Annotations map[string]string
+	Status      batchv1.JobStatus
 }
 }
 
 
 type PersistentVolume struct {
 type PersistentVolume struct {
@@ -155,6 +159,8 @@ type ReplicaSet struct {
 	UID             types.UID
 	UID             types.UID
 	Name            string
 	Name            string
 	Namespace       string
 	Namespace       string
+	Labels          map[string]string
+	Annotations     map[string]string
 	OwnerReferences []metav1.OwnerReference
 	OwnerReferences []metav1.OwnerReference
 	SpecSelector    *metav1.LabelSelector
 	SpecSelector    *metav1.LabelSelector
 	Spec            appsv1.ReplicaSetSpec
 	Spec            appsv1.ReplicaSetSpec
@@ -168,6 +174,14 @@ type ResourceQuota struct {
 	Status    v1.ResourceQuotaStatus
 	Status    v1.ResourceQuotaStatus
 }
 }
 
 
+type CronJob struct {
+	UID         types.UID
+	Name        string
+	Namespace   string
+	Labels      map[string]string
+	Annotations map[string]string
+}
+
 type Volume struct {
 type Volume struct {
 }
 }
 
 
@@ -304,9 +318,11 @@ func TransformService(input *v1.Service) *Service {
 
 
 func TransformDaemonSet(input *appsv1.DaemonSet) *DaemonSet {
 func TransformDaemonSet(input *appsv1.DaemonSet) *DaemonSet {
 	return &DaemonSet{
 	return &DaemonSet{
+		UID:            input.UID,
 		Name:           input.Name,
 		Name:           input.Name,
 		Namespace:      input.Namespace,
 		Namespace:      input.Namespace,
 		Labels:         input.Labels,
 		Labels:         input.Labels,
+		Annotations:    input.Annotations,
 		SpecContainers: input.Spec.Template.Spec.Containers,
 		SpecContainers: input.Spec.Template.Spec.Containers,
 	}
 	}
 }
 }
@@ -376,10 +392,22 @@ func TransformStorageClass(input *stv1.StorageClass) *StorageClass {
 
 
 func TransformJob(input *batchv1.Job) *Job {
 func TransformJob(input *batchv1.Job) *Job {
 	return &Job{
 	return &Job{
-		UID:       input.UID,
-		Name:      input.Name,
-		Namespace: input.Namespace,
-		Status:    input.Status,
+		UID:         input.UID,
+		Name:        input.Name,
+		Namespace:   input.Namespace,
+		Labels:      input.Labels,
+		Annotations: input.Annotations,
+		Status:      input.Status,
+	}
+}
+
+func TransformCronJob(input *batchv1.CronJob) *CronJob {
+	return &CronJob{
+		UID:         input.UID,
+		Name:        input.Name,
+		Namespace:   input.Namespace,
+		Labels:      input.Labels,
+		Annotations: input.Annotations,
 	}
 	}
 }
 }
 
 
@@ -405,6 +433,8 @@ func TransformReplicaSet(input *appsv1.ReplicaSet) *ReplicaSet {
 		UID:             input.UID,
 		UID:             input.UID,
 		Name:            input.Name,
 		Name:            input.Name,
 		Namespace:       input.Namespace,
 		Namespace:       input.Namespace,
+		Labels:          input.Labels,
+		Annotations:     input.Annotations,
 		OwnerReferences: input.OwnerReferences,
 		OwnerReferences: input.OwnerReferences,
 		Spec:            input.Spec,
 		Spec:            input.Spec,
 		SpecSelector:    input.Spec.Selector,
 		SpecSelector:    input.Spec.Selector,
@@ -466,6 +496,9 @@ type ClusterCache interface {
 	// GetAllJobs returns all the cached jobs
 	// GetAllJobs returns all the cached jobs
 	GetAllJobs() []*Job
 	GetAllJobs() []*Job
 
 
+	// GetAllCronJobs returns all the cached cronjobs
+	GetAllCronJobs() []*CronJob
+
 	// GetAllPodDisruptionBudgets returns all cached pod disruption budgets
 	// GetAllPodDisruptionBudgets returns all cached pod disruption budgets
 	GetAllPodDisruptionBudgets() []*PodDisruptionBudget
 	GetAllPodDisruptionBudgets() []*PodDisruptionBudget
 
 

+ 15 - 0
core/pkg/clustercache/helper.go

@@ -0,0 +1,15 @@
+package clustercache
+
+func GetLoadBalancerIngressAddress(service *Service) []string {
+	var addresses []string
+	for _, loadBalancerIngress := range service.Status.LoadBalancer.Ingress {
+		address := loadBalancerIngress.IP
+		// Some cloud providers use hostname rather than IP
+		if address == "" {
+			address = loadBalancerIngress.Hostname
+		}
+		addresses = append(addresses, address)
+
+	}
+	return addresses
+}

+ 89 - 0
core/pkg/clustercache/helper_test.go

@@ -0,0 +1,89 @@
+package clustercache
+
+import (
+	"testing"
+
+	v1 "k8s.io/api/core/v1"
+)
+
+func TestGetLoadBalancerIngressAddress(t *testing.T) {
+	tests := []struct {
+		name     string
+		service  *Service
+		expected []string
+	}{
+		{
+			name:     "no ingresses",
+			service:  &Service{},
+			expected: nil,
+		},
+		{
+			name: "single IP ingress",
+			service: &Service{
+				Status: v1.ServiceStatus{
+					LoadBalancer: v1.LoadBalancerStatus{
+						Ingress: []v1.LoadBalancerIngress{
+							{IP: "1.2.3.4"},
+						},
+					},
+				},
+			},
+			expected: []string{"1.2.3.4"},
+		},
+		{
+			name: "single hostname ingress",
+			service: &Service{
+				Status: v1.ServiceStatus{
+					LoadBalancer: v1.LoadBalancerStatus{
+						Ingress: []v1.LoadBalancerIngress{
+							{Hostname: "lb.example.com"},
+						},
+					},
+				},
+			},
+			expected: []string{"lb.example.com"},
+		},
+		{
+			name: "IP takes priority over hostname",
+			service: &Service{
+				Status: v1.ServiceStatus{
+					LoadBalancer: v1.LoadBalancerStatus{
+						Ingress: []v1.LoadBalancerIngress{
+							{IP: "1.2.3.4", Hostname: "lb.example.com"},
+						},
+					},
+				},
+			},
+			expected: []string{"1.2.3.4"},
+		},
+		{
+			name: "multiple ingresses",
+			service: &Service{
+				Status: v1.ServiceStatus{
+					LoadBalancer: v1.LoadBalancerStatus{
+						Ingress: []v1.LoadBalancerIngress{
+							{IP: "1.2.3.4"},
+							{Hostname: "lb2.example.com"},
+							{IP: "5.6.7.8"},
+						},
+					},
+				},
+			},
+			expected: []string{"1.2.3.4", "lb2.example.com", "5.6.7.8"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := GetLoadBalancerIngressAddress(tt.service)
+			if len(got) != len(tt.expected) {
+				t.Fatalf("got %v, want %v", got, tt.expected)
+			}
+			for i := range tt.expected {
+				if got[i] != tt.expected[i] {
+					t.Errorf("index %d: got %q, want %q", i, got[i], tt.expected[i])
+				}
+			}
+		})
+	}
+}

+ 49 - 0
core/pkg/clustercache/mock.go

@@ -0,0 +1,49 @@
+package clustercache
+
+// MockClusterCache is a configurable implementation of ClusterCache for use in tests.
+// Each field corresponds to the slice returned by the matching GetAll* method.
+// Any field left nil causes its method to return nil.
+type MockClusterCache struct {
+	Nodes                  []*Node
+	Pods                   []*Pod
+	Namespaces             []*Namespace
+	Services               []*Service
+	DaemonSets             []*DaemonSet
+	Deployments            []*Deployment
+	StatefulSets           []*StatefulSet
+	ReplicaSets            []*ReplicaSet
+	PersistentVolumes      []*PersistentVolume
+	PersistentVolumeClaims []*PersistentVolumeClaim
+	StorageClasses         []*StorageClass
+	Jobs                   []*Job
+	CronJobs               []*CronJob
+	PodDisruptionBudgets   []*PodDisruptionBudget
+	ReplicationControllers []*ReplicationController
+	ResourceQuotas         []*ResourceQuota
+}
+
+func (m *MockClusterCache) Run()  {}
+func (m *MockClusterCache) Stop() {}
+
+func (m *MockClusterCache) GetAllNodes() []*Node                         { return m.Nodes }
+func (m *MockClusterCache) GetAllPods() []*Pod                           { return m.Pods }
+func (m *MockClusterCache) GetAllNamespaces() []*Namespace               { return m.Namespaces }
+func (m *MockClusterCache) GetAllServices() []*Service                   { return m.Services }
+func (m *MockClusterCache) GetAllDaemonSets() []*DaemonSet               { return m.DaemonSets }
+func (m *MockClusterCache) GetAllDeployments() []*Deployment             { return m.Deployments }
+func (m *MockClusterCache) GetAllStatefulSets() []*StatefulSet           { return m.StatefulSets }
+func (m *MockClusterCache) GetAllReplicaSets() []*ReplicaSet             { return m.ReplicaSets }
+func (m *MockClusterCache) GetAllPersistentVolumes() []*PersistentVolume { return m.PersistentVolumes }
+func (m *MockClusterCache) GetAllPersistentVolumeClaims() []*PersistentVolumeClaim {
+	return m.PersistentVolumeClaims
+}
+func (m *MockClusterCache) GetAllStorageClasses() []*StorageClass { return m.StorageClasses }
+func (m *MockClusterCache) GetAllJobs() []*Job                    { return m.Jobs }
+func (m *MockClusterCache) GetAllCronJobs() []*CronJob            { return m.CronJobs }
+func (m *MockClusterCache) GetAllPodDisruptionBudgets() []*PodDisruptionBudget {
+	return m.PodDisruptionBudgets
+}
+func (m *MockClusterCache) GetAllReplicationControllers() []*ReplicationController {
+	return m.ReplicationControllers
+}
+func (m *MockClusterCache) GetAllResourceQuotas() []*ResourceQuota { return m.ResourceQuotas }

+ 1 - 1
core/pkg/exporter/controller.go

@@ -277,7 +277,7 @@ func (g *ComputeExportControllerGroup[T]) Name() string {
 
 
 func (g *ComputeExportControllerGroup[T]) Start(interval time.Duration) bool {
 func (g *ComputeExportControllerGroup[T]) Start(interval time.Duration) bool {
 	if len(g.controllers) == 0 {
 	if len(g.controllers) == 0 {
-		log.Warnf("ComputeExportControllerGroup[%s] has no controllers to start", typeutil.TypeOf[T]())
+		log.Debugf("ComputeExportControllerGroup[%s] has no controllers to start", typeutil.TypeOf[T]())
 		return false
 		return false
 	}
 	}
 
 

+ 13 - 6
core/pkg/exporter/encoder.go

@@ -12,6 +12,13 @@ import (
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/proto"
 )
 )
 
 
+const (
+	BingenExt = "bingen"
+	JSONExt   = "json"
+	GZipExt   = "gz"
+	PBExt     = "binpb"
+)
+
 // Encoder[T] is a generic interface for encoding an instance of a T type into a byte slice.
 // Encoder[T] is a generic interface for encoding an instance of a T type into a byte slice.
 type Encoder[T any] interface {
 type Encoder[T any] interface {
 	Encode(*T) ([]byte, error)
 	Encode(*T) ([]byte, error)
@@ -52,7 +59,7 @@ func NewBingenEncoder[T any, U BinaryMarshalerPtr[T]]() Encoder[T] {
 // 'T' type with the ".bingen" file extension.
 // 'T' type with the ".bingen" file extension.
 func NewBingenFileEncoder[T any, U BinaryMarshalerPtr[T]]() Encoder[T] {
 func NewBingenFileEncoder[T any, U BinaryMarshalerPtr[T]]() Encoder[T] {
 	return &BingenEncoder[T, U]{
 	return &BingenEncoder[T, U]{
-		fileExt: "bingen",
+		fileExt: BingenExt,
 	}
 	}
 }
 }
 
 
@@ -99,7 +106,7 @@ func (j *JSONEncoder[T]) EncodeTo(writer io.Writer, data *T) error {
 // FileExt returns the file extension for the encoded data. In this case, it returns "json" to indicate
 // FileExt returns the file extension for the encoded data. In this case, it returns "json" to indicate
 // that the data is in JSON format.
 // that the data is in JSON format.
 func (j *JSONEncoder[T]) FileExt() string {
 func (j *JSONEncoder[T]) FileExt() string {
-	return "json"
+	return JSONExt
 }
 }
 
 
 type GZipEncoder[T any] struct {
 type GZipEncoder[T any] struct {
@@ -175,9 +182,9 @@ func gZipEncode(data []byte, level int) ([]byte, error) {
 func (gz *GZipEncoder[T]) FileExt() string {
 func (gz *GZipEncoder[T]) FileExt() string {
 	prev := gz.encoder.FileExt()
 	prev := gz.encoder.FileExt()
 	if prev == "" {
 	if prev == "" {
-		return "gz"
+		return GZipExt
 	}
 	}
-	return prev + ".gz"
+	return fmt.Sprintf("%s.%s", prev, GZipExt)
 }
 }
 
 
 // ProtoMessagePtr [T] is a generic constraint to ensure types passed to the encoder implement
 // ProtoMessagePtr [T] is a generic constraint to ensure types passed to the encoder implement
@@ -225,7 +232,7 @@ func (p *ProtobufEncoder[T, U]) EncodeTo(writer io.Writer, data *T) error {
 // FileExt returns the file extension for the encoded data. In this case, it returns an empty string
 // FileExt returns the file extension for the encoded data. In this case, it returns an empty string
 // to indicate that there is no specific file extension for the binary encoded data.
 // to indicate that there is no specific file extension for the binary encoded data.
 func (p *ProtobufEncoder[T, U]) FileExt() string {
 func (p *ProtobufEncoder[T, U]) FileExt() string {
-	return "binpb"
+	return PBExt
 }
 }
 
 
 // ProtoJsonEncoder [T, U] is a generic encoder that uses the proto.Message interface to encode data in json format.
 // ProtoJsonEncoder [T, U] is a generic encoder that uses the proto.Message interface to encode data in json format.
@@ -268,5 +275,5 @@ func (p *ProtoJsonEncoder[T, U]) EncodeTo(writer io.Writer, data *T) error {
 // FileExt returns the file extension for the encoded data. In this case, it returns an empty string
 // FileExt returns the file extension for the encoded data. In this case, it returns an empty string
 // to indicate that there is no specific file extension for the binary encoded data.
 // to indicate that there is no specific file extension for the binary encoded data.
 func (p *ProtoJsonEncoder[T, U]) FileExt() string {
 func (p *ProtoJsonEncoder[T, U]) FileExt() string {
-	return "json"
+	return JSONExt
 }
 }

+ 2 - 10
core/pkg/exporter/pathing/bingenpath.go

@@ -7,14 +7,12 @@ import (
 
 
 	"github.com/opencost/opencost/core/pkg/exporter/pathing/pathutils"
 	"github.com/opencost/opencost/core/pkg/exporter/pathing/pathutils"
 	"github.com/opencost/opencost/core/pkg/opencost"
 	"github.com/opencost/opencost/core/pkg/opencost"
-	"github.com/opencost/opencost/core/pkg/pipelines"
 	"github.com/opencost/opencost/core/pkg/util/timeutil"
 	"github.com/opencost/opencost/core/pkg/util/timeutil"
 )
 )
 
 
 const (
 const (
-	DefaultRootDir   string = "federated"
-	BaseStorageDir   string = "etl/bingen"
-	FinOpsAgentAppID string = "finops-agent"
+	DefaultRootDir string = "federated"
+	BaseStorageDir string = "etl/bingen"
 )
 )
 
 
 // BingenStoragePathFormatter is an implementation of the StoragePathFormatter interface for
 // BingenStoragePathFormatter is an implementation of the StoragePathFormatter interface for
@@ -34,12 +32,6 @@ func NewDefaultStoragePathFormatter(clusterId, pipeline string, resolution *time
 		res = timeutil.FormatStoreResolution(*resolution)
 		res = timeutil.FormatStoreResolution(*resolution)
 	}
 	}
 
 
-	// KubeModel uses a distinct pathing pattern which breaks with the original
-	// Allocations and Assets bingen pathing.
-	if pipeline == pipelines.KubeModelPipelineName {
-		return NewKubeModelStoragePathFormatter(FinOpsAgentAppID, clusterId, res)
-	}
-
 	return NewBingenStoragePathFormatter(DefaultRootDir, clusterId, pipeline, res)
 	return NewBingenStoragePathFormatter(DefaultRootDir, clusterId, pipeline, res)
 }
 }
 
 

+ 53 - 0
core/pkg/exporter/pathing/staticpath.go

@@ -0,0 +1,53 @@
+package pathing
+
+import (
+	"fmt"
+	"path"
+)
+
+// StaticFileStoragePathFormatter is an implementation of the StoragePathFormatter interface
+// for static files whose path does not vary with time. The format is:
+//
+//	<rootDir>/<pipeline>/<prefix>.<in>.<fileExt>
+//
+// If prefix is empty the leading dot is omitted. If fileExt is empty the trailing dot is omitted.
+type StaticFileStoragePathFormatter struct {
+	rootDir  string
+	pipeline string
+}
+
+// NewStaticFileStoragePathFormatter creates a StaticFileStoragePathFormatter with the given
+// root directory and pipeline name.
+func NewStaticFileStoragePathFormatter(rootDir, pipeline string) (*StaticFileStoragePathFormatter, error) {
+	if rootDir == "" {
+		return nil, fmt.Errorf("rootDir cannot be empty")
+	}
+	if pipeline == "" {
+		return nil, fmt.Errorf("pipeline cannot be empty")
+	}
+	return &StaticFileStoragePathFormatter{
+		rootDir:  rootDir,
+		pipeline: pipeline,
+	}, nil
+}
+
+// Dir returns the directory where static files are placed.
+func (s *StaticFileStoragePathFormatter) Dir() string {
+	return path.Join(s.rootDir, s.pipeline)
+}
+
+// ToFullPath returns the full path for a static file. The in parameter is used as the
+// file name and may include subdirectory segments. prefix and fileExt are optional and
+// apply only to the base file name component of in.
+func (s *StaticFileStoragePathFormatter) ToFullPath(prefix string, in string, fileExt string) string {
+	dir, base := path.Split(in)
+
+	name := base
+	if prefix != "" {
+		name = fmt.Sprintf("%s.%s", prefix, base)
+	}
+	if fileExt != "" {
+		name = fmt.Sprintf("%s.%s", name, fileExt)
+	}
+	return path.Join(s.rootDir, s.pipeline, dir, name)
+}

+ 143 - 0
core/pkg/exporter/pathing/staticpath_test.go

@@ -0,0 +1,143 @@
+package pathing
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestNewStaticFileStoragePathFormatter(t *testing.T) {
+	tests := []struct {
+		name     string
+		rootDir  string
+		pipeline string
+		wantErr  bool
+	}{
+		{"empty rootDir returns error", "", "pricingmodel", true},
+		{"empty pipeline returns error", "cloud-agent", "", true},
+		{"valid args returns formatter", "cloud-agent", "pricingmodel", false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			f, err := NewStaticFileStoragePathFormatter(tt.rootDir, tt.pipeline)
+			if tt.wantErr {
+				require.Error(t, err)
+				require.Nil(t, f)
+			} else {
+				require.NoError(t, err)
+				require.NotNil(t, f)
+			}
+		})
+	}
+}
+
+func TestStaticFileStoragePathFormatter_Dir(t *testing.T) {
+	tests := []struct {
+		rootDir  string
+		pipeline string
+		expected string
+	}{
+		{"cloud-agent", "pricingmodel", "cloud-agent/pricingmodel"},
+		{"/var/data", "kubemodel", "/var/data/kubemodel"},
+		{"root", "a/b/c", "root/a/b/c"},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.expected, func(t *testing.T) {
+			f, err := NewStaticFileStoragePathFormatter(tt.rootDir, tt.pipeline)
+			require.NoError(t, err)
+			require.Equal(t, tt.expected, f.Dir())
+		})
+	}
+}
+
+func TestStaticFilePathFormatter(t *testing.T) {
+	type testCase struct {
+		name     string
+		rootDir  string
+		pipeline string
+		prefix   string
+		in       string
+		fileExt  string
+		expected string
+	}
+
+	testCases := []testCase{
+		{
+			name:     "no prefix no ext",
+			rootDir:  "cloud-agent",
+			pipeline: "pricingmodel",
+			prefix:   "",
+			in:       "aws_list_pricing_api",
+			fileExt:  "",
+			expected: "cloud-agent/pricingmodel/aws_list_pricing_api",
+		},
+		{
+			name:     "no prefix with ext",
+			rootDir:  "cloud-agent",
+			pipeline: "pricingmodel",
+			prefix:   "",
+			in:       "aws_list_pricing_api",
+			fileExt:  "bin",
+			expected: "cloud-agent/pricingmodel/aws_list_pricing_api.bin",
+		},
+		{
+			name:     "with prefix with ext",
+			rootDir:  "cloud-agent",
+			pipeline: "pricingmodel",
+			prefix:   "v1",
+			in:       "aws_list_pricing_api",
+			fileExt:  "bin",
+			expected: "cloud-agent/pricingmodel/v1.aws_list_pricing_api.bin",
+		},
+		{
+			name:     "with prefix no ext",
+			rootDir:  "cloud-agent",
+			pipeline: "pricingmodel",
+			prefix:   "v1",
+			in:       "aws_list_pricing_api",
+			fileExt:  "",
+			expected: "cloud-agent/pricingmodel/v1.aws_list_pricing_api",
+		},
+		{
+			name:     "in with subdir no prefix no ext",
+			rootDir:  "cloud-agent",
+			pipeline: "pricingmodel",
+			prefix:   "",
+			in:       "us-east-1/aws_list_pricing_api",
+			fileExt:  "",
+			expected: "cloud-agent/pricingmodel/us-east-1/aws_list_pricing_api",
+		},
+		{
+			name:     "in with subdir with prefix and ext",
+			rootDir:  "cloud-agent",
+			pipeline: "pricingmodel",
+			prefix:   "v1",
+			in:       "us-east-1/aws_list_pricing_api",
+			fileExt:  "bin",
+			expected: "cloud-agent/pricingmodel/us-east-1/v1.aws_list_pricing_api.bin",
+		},
+		{
+			name:     "in with nested subdir with ext",
+			rootDir:  "/var/data",
+			pipeline: "kubemodel",
+			prefix:   "",
+			in:       "2024/01/nodes",
+			fileExt:  "bin",
+			expected: "/var/data/kubemodel/2024/01/nodes.bin",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			pathing, err := NewStaticFileStoragePathFormatter(tc.rootDir, tc.pipeline)
+			if err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+
+			result := pathing.ToFullPath(tc.prefix, tc.in, tc.fileExt)
+			require.Equal(t, tc.expected, result)
+		})
+	}
+}

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

@@ -20,4 +20,6 @@ package kubemodel
 //
 //
 ////////////////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////////////////
 
 
-//go:generate bingen -package=kubemodel -version=1 -buffer=github.com/opencost/opencost/core/pkg/util
+// @bingen:define[string]:github.com/opencost/opencost/core/pkg/model/shared.Provider
+
+//go:generate bingen -package=kubemodel -version=2 -buffer=github.com/opencost/opencost/core/pkg/util

+ 30 - 14
core/pkg/model/kubemodel/cluster.go

@@ -1,33 +1,49 @@
 package kubemodel
 package kubemodel
 
 
 import (
 import (
-	"errors"
+	"fmt"
 	"time"
 	"time"
+
+	"github.com/opencost/opencost/core/pkg/model/shared"
 )
 )
 
 
 // @bingen:generate:Cluster
 // @bingen:generate:Cluster
 type Cluster struct {
 type Cluster struct {
-	UID      string    `json:"uid"`      // @bingen:field[version=1]
-	Provider Provider  `json:"provider"` // @bingen:field[version=1]
-	Account  string    `json:"account"`  // @bingen:field[version=1]
-	Name     string    `json:"name"`     // @bingen:field[version=1]
-	Start    time.Time `json:"start"`    // @bingen:field[version=1]
-	End      time.Time `json:"end"`      // @bingen:field[version=1]
+	UID      string          `json:"uid"`      // @bingen:field[version=1]
+	Provider shared.Provider `json:"provider"` // @bingen:field[version=1]
+	Account  string          `json:"account"`  // @bingen:field[version=1]
+	Name     string          `json:"name"`     // @bingen:field[version=1]
+	Region   string          `json:"region"`   // @bingen:field[version=2]
+	Start    time.Time       `json:"start"`    // @bingen:field[version=1]
+	End      time.Time       `json:"end"`      // @bingen:field[version=1]
+}
+
+func (c *Cluster) ValidateCluster(window Window) error {
+	if c.UID == "" {
+		err := fmt.Errorf("UID is missing for Cluster with name '%s'", c.Name)
+		return err
+	}
+
+	if err := checkWindow(window, c.Start, c.End); err != nil {
+		return err
+	}
+
+	return nil
 }
 }
 
 
-func (kms *KubeModelSet) RegisterCluster(uid string) error {
-	if uid == "" {
-		err := errors.New("RegisterCluster: uid is nil")
+func (kms *KubeModelSet) RegisterCluster(cluster *Cluster) error {
+	if err := cluster.ValidateCluster(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterCluster: invalid cluster: %w", err)
 		kms.Error(err)
 		kms.Error(err)
 		return err
 		return err
 	}
 	}
 
 
 	if kms.Cluster == nil {
 	if kms.Cluster == nil {
-		kms.Cluster = &Cluster{UID: uid}
-	} else if uid != kms.Cluster.UID {
-		kms.Warnf("RegisterCluster(%s): attempting to change cluster UID from %s to %s", uid, kms.Cluster.UID, uid)
+		kms.Cluster = cluster
+	} else if cluster.UID != kms.Cluster.UID {
+		kms.Warnf("RegisterCluster(%s): attempting to change cluster UID from %s to %s", cluster.UID, kms.Cluster.UID, cluster.UID)
 	} else {
 	} else {
-		kms.Debugf("RegisterCluster(%s): cluster already registered", uid)
+		kms.Debugf("RegisterCluster(%s): cluster already registered", cluster.UID)
 	}
 	}
 
 
 	return nil
 	return nil

+ 131 - 0
core/pkg/model/kubemodel/cluster_test.go

@@ -0,0 +1,131 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateCluster(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name    string
+		cluster *Cluster
+		wantErr string
+	}{
+		{
+			name:    "empty UID",
+			cluster: &Cluster{Name: "my-cluster", Start: start, End: end},
+			wantErr: "UID is missing for Cluster with name 'my-cluster'",
+		},
+		{
+			name:    "outside window",
+			cluster: &Cluster{UID: "cluster-uid", Name: "my-cluster", Start: start.Add(-time.Hour), End: end},
+			wantErr: checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name:    "valid",
+			cluster: &Cluster{UID: "cluster-uid", Name: "my-cluster", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.cluster.ValidateCluster(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterCluster(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newCluster := func(uid, name string) *Cluster {
+		return &Cluster{UID: uid, Name: name, Start: start, End: end}
+	}
+
+	tests := []struct {
+		name    string
+		setup   func(*KubeModelSet)
+		cluster *Cluster
+		wantErr string
+		want    *KubeModelSet
+	}{
+		{
+			name:    "validation failure",
+			cluster: &Cluster{UID: "", Name: "my-cluster", Start: start, End: end},
+			wantErr: "RegisterCluster: invalid cluster: UID is missing for Cluster with name 'my-cluster'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterCluster: invalid cluster: UID is missing for Cluster with name 'my-cluster'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:    "registers cluster",
+			cluster: newCluster("cluster-uid", "my-cluster"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Cluster = newCluster("cluster-uid", "my-cluster")
+				return kms
+			}(),
+		},
+		{
+			name: "same UID is a no-op",
+			setup: func(kms *KubeModelSet) {
+				kms.RegisterCluster(newCluster("cluster-uid", "original"))
+			},
+			cluster: newCluster("cluster-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Cluster = newCluster("cluster-uid", "original")
+				return kms
+			}(),
+		},
+		{
+			name: "different UID emits warning and keeps original",
+			setup: func(kms *KubeModelSet) {
+				kms.RegisterCluster(newCluster("cluster-uid", "original"))
+			},
+			cluster: newCluster("another-uid", "another"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Cluster = newCluster("cluster-uid", "original")
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelWarning, Message: "RegisterCluster(another-uid): attempting to change cluster UID from cluster-uid to another-uid"},
+				}
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterCluster(tt.cluster)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 29 - 82
core/pkg/model/kubemodel/container.go

@@ -5,105 +5,52 @@ import (
 	"time"
 	"time"
 )
 )
 
 
+// @bingen:generate:Container
 type Container struct {
 type Container struct {
-	PodUID                     string                 `json:"podUid"`
-	Name                       string                 `json:"name"`
-	DurationSeconds            Measurement            `json:"durationSeconds"`
-	CpuMillicoreSeconds        Measurement            `json:"cpuMillicoreSeconds"`
-	CpuMillicoreUsageMax       Measurement            `json:"cpuMillicoreUsageMax"`
-	CpuMillicoreRequestSeconds Measurement            `json:"cpuMillicoreRequestSeconds"`
-	RAMByteSeconds             Measurement            `json:"ramByteSeconds"`
-	RAMByteUsageMax            Measurement            `json:"ramByteUsageMax"`
-	RAMByteSecondRequest       Measurement            `json:"ramByteSecondRequest"`
-	VolumeStorageByteSeconds   map[string]Measurement `json:"volumeStorageByteSeconds,omitempty"`
-	VolumeStorageByteUsageMax  map[string]Measurement `json:"volumeStorageByteUsageMax,omitempty"`
-	CpuMillicoreLimitSeconds   Measurement            `json:"cpuMillicoreLimitSeconds,omitempty"`
-	RAMByteSecondsLimit        Measurement            `json:"ramByteSecondsLimit,omitempty"`
-	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"`
+	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"`
 }
 }
 
 
-func (c *Container) CpuMillicoreUsageAverage() Measurement {
-	if c.DurationSeconds == 0 {
-		return 0
-	}
-	return c.CpuMillicoreSeconds / c.DurationSeconds
-}
-
-func (c *Container) RAMByteUsageAverage() Measurement {
-	if c.DurationSeconds == 0 {
-		return 0
-	}
-	return c.RAMByteSeconds / c.DurationSeconds
-}
-
-func (c *Container) TotalStorageByteSeconds() Measurement {
-	var total Measurement
-	for _, ByteSeconds := range c.VolumeStorageByteSeconds {
-		total += ByteSeconds
-	}
-	return total
-}
-
-func (c *Container) TotalStorageByteUsageMax() Measurement {
-	var max Measurement
-	for _, usage := range c.VolumeStorageByteUsageMax {
-		if usage > max {
-			max = usage
-		}
-	}
-	return max
+func (c *Container) GetKey() string {
+	return fmt.Sprintf("%s/%s", c.PodUID, c.Name)
 }
 }
 
 
-func (c *Container) StorageByteUsageAverage() Measurement {
-	if c.DurationSeconds == 0 {
-		return 0
+func (c *Container) ValidateContainer(window Window) error {
+	if c.PodUID == "" {
+		return fmt.Errorf("PodUID is missing for Container with name '%s'", c.Name)
 	}
 	}
-	totalByteSeconds := c.TotalStorageByteSeconds()
-	return totalByteSeconds / c.DurationSeconds
-}
 
 
-func (c *Container) CpuMillicoreRequestAverage() Measurement {
-	if c.DurationSeconds == 0 {
-		return 0
+	if c.Name == "" {
+		return fmt.Errorf("Name is missing for Container on pod '%s'", c.PodUID)
 	}
 	}
-	return c.CpuMillicoreRequestSeconds / c.DurationSeconds
-}
-
-func (c *Container) RAMByteRequestAverage() Measurement {
-	if c.DurationSeconds == 0 {
-		return 0
-	}
-	return c.RAMByteSecondRequest / c.DurationSeconds
-}
 
 
-func (c *Container) CpuMillicoreLimitAverage() Measurement {
-	if c.DurationSeconds == 0 {
-		return 0
+	if err := checkWindow(window, c.Start, c.End); err != nil {
+		return err
 	}
 	}
-	return c.CpuMillicoreLimitSeconds / c.DurationSeconds
-}
 
 
-func (c *Container) RAMByteLimitAverage() Measurement {
-	if c.DurationSeconds == 0 {
-		return 0
-	}
-	return c.RAMByteSecondsLimit / c.DurationSeconds
+	return nil
 }
 }
 
 
-func (kms *KubeModelSet) RegisterContainer(uid, name, podUID string) error {
-	if uid == "" {
-		err := fmt.Errorf("UID is nil for Container '%s'", name)
+func (kms *KubeModelSet) RegisterContainer(container *Container) error {
+	if err := container.ValidateContainer(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterContainer: invalid container: %w", err)
 		kms.Error(err)
 		kms.Error(err)
 		return err
 		return err
 	}
 	}
 
 
-	if _, ok := kms.Containers[uid]; !ok {
-		kms.Containers[uid] = &Container{
-			PodUID: podUID,
-			Name:   name,
-		}
-
+	key := container.GetKey()
+	if _, ok := kms.Containers[key]; !ok {
+		kms.Containers[key] = container
 		kms.Metadata.ObjectCount++
 		kms.Metadata.ObjectCount++
 	}
 	}
 
 

+ 123 - 0
core/pkg/model/kubemodel/container_test.go

@@ -0,0 +1,123 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateContainer(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name      string
+		container *Container
+		wantErr   string
+	}{
+		{
+			name:      "empty PodUID",
+			container: &Container{Name: "my-container", Start: start, End: end},
+			wantErr:   "PodUID is missing for Container with name 'my-container'",
+		},
+		{
+			name:      "empty Name",
+			container: &Container{PodUID: "pod-uid", Start: start, End: end},
+			wantErr:   "Name is missing for Container on pod 'pod-uid'",
+		},
+		{
+			name:      "outside window",
+			container: &Container{PodUID: "pod-uid", Name: "my-container", Start: start.Add(-time.Hour), End: end},
+			wantErr:   checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name:      "valid",
+			container: &Container{PodUID: "pod-uid", Name: "my-container", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.container.ValidateContainer(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterContainer(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newContainer := func(podUID, name string) *Container {
+		return &Container{PodUID: podUID, Name: name, Start: start, End: end}
+	}
+
+	tests := []struct {
+		name      string
+		setup     func(*KubeModelSet)
+		container *Container
+		wantErr   string
+		want      *KubeModelSet
+	}{
+		{
+			name:      "validation failure",
+			container: &Container{PodUID: "", Name: "my-container", Start: start, End: end},
+			wantErr:   "RegisterContainer: invalid container: PodUID is missing for Container with name 'my-container'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterContainer: invalid container: PodUID is missing for Container with name 'my-container'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:      "registers container",
+			container: newContainer("pod-uid", "my-container"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Containers["pod-uid/my-container"] = newContainer("pod-uid", "my-container")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				kms.RegisterContainer(newContainer("pod-uid", "original"))
+			},
+			container: newContainer("pod-uid", "original"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Containers["pod-uid/original"] = newContainer("pod-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterContainer(tt.container)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 58 - 0
core/pkg/model/kubemodel/cronjob.go

@@ -0,0 +1,58 @@
+package kubemodel
+
+import (
+	"fmt"
+	"time"
+)
+
+// @bingen:generate:CronJob
+// CronJob represents a Kubernetes CronJob resource
+type CronJob struct {
+	UID          string            `json:"uid"`
+	NamespaceUID string            `json:"namespaceUid"`
+	Name         string            `json:"name"`
+	Labels       map[string]string `json:"labels,omitempty"`
+	Annotations  map[string]string `json:"annotations,omitempty"`
+	Start        time.Time         `json:"start,omitempty"`
+	End          time.Time         `json:"end,omitempty"`
+}
+
+func (c *CronJob) ValidateCronJob(window Window) error {
+	if c.UID == "" {
+		return fmt.Errorf("UID is missing for CronJob with name '%s'", c.Name)
+	}
+
+	if c.Name == "" {
+		return fmt.Errorf("Name is missing for CronJob '%s'", c.UID)
+	}
+
+	if c.NamespaceUID == "" {
+		return fmt.Errorf("NamespaceUID is missing for CronJob '%s'", c.UID)
+	}
+
+	if err := checkWindow(window, c.Start, c.End); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (kms *KubeModelSet) RegisterCronJob(cronJob *CronJob) error {
+	if err := cronJob.ValidateCronJob(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterCronJob: invalid cronjob: %w", err)
+		kms.Error(err)
+		return err
+	}
+
+	if _, ok := kms.CronJobs[cronJob.UID]; !ok {
+		if kms.Cluster == nil {
+			kms.Warnf("RegisterCronJob: Cluster is nil")
+		}
+
+		kms.CronJobs[cronJob.UID] = cronJob
+
+		kms.Metadata.ObjectCount++
+	}
+
+	return nil
+}

+ 148 - 0
core/pkg/model/kubemodel/cronjob_test.go

@@ -0,0 +1,148 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateCronJob(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name    string
+		cronJob *CronJob
+		wantErr string
+	}{
+		{
+			name:    "empty UID",
+			cronJob: &CronJob{Name: "my-cj", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "UID is missing for CronJob with name 'my-cj'",
+		},
+		{
+			name:    "empty Name",
+			cronJob: &CronJob{UID: "cj-uid", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "Name is missing for CronJob 'cj-uid'",
+		},
+		{
+			name:    "empty NamespaceUID",
+			cronJob: &CronJob{UID: "cj-uid", Name: "my-cj", Start: start, End: end},
+			wantErr: "NamespaceUID is missing for CronJob 'cj-uid'",
+		},
+		{
+			name:    "outside window",
+			cronJob: &CronJob{UID: "cj-uid", Name: "my-cj", NamespaceUID: "ns-uid", Start: start.Add(-time.Hour), End: end},
+			wantErr: checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name:    "valid",
+			cronJob: &CronJob{UID: "cj-uid", Name: "my-cj", NamespaceUID: "ns-uid", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.cronJob.ValidateCronJob(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterCronJob(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newCronJob := func(uid, name string) *CronJob {
+		return &CronJob{UID: uid, Name: name, NamespaceUID: "ns-uid", Start: start, End: end}
+	}
+	withCluster := func(kms *KubeModelSet) {
+		kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+	}
+
+	tests := []struct {
+		name    string
+		setup   func(*KubeModelSet)
+		cronJob *CronJob
+		wantErr string
+		want    *KubeModelSet
+	}{
+		{
+			name:    "validation failure",
+			cronJob: &CronJob{UID: "", Name: "my-cj", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "RegisterCronJob: invalid cronjob: UID is missing for CronJob with name 'my-cj'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterCronJob: invalid cronjob: UID is missing for CronJob with name 'my-cj'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:    "warns when cluster is nil",
+			cronJob: newCronJob("cj-uid", "my-cj"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.CronJobs["cj-uid"] = newCronJob("cj-uid", "my-cj")
+				kms.Metadata.ObjectCount = 1
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelWarning, Message: "RegisterCronJob: Cluster is nil"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:    "registers cronjob with cluster",
+			setup:   withCluster,
+			cronJob: newCronJob("cj-uid", "my-cj"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.CronJobs["cj-uid"] = newCronJob("cj-uid", "my-cj")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				withCluster(kms)
+				kms.RegisterCronJob(newCronJob("cj-uid", "original"))
+			},
+			cronJob: newCronJob("cj-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.CronJobs["cj-uid"] = newCronJob("cj-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterCronJob(tt.cronJob)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 59 - 0
core/pkg/model/kubemodel/daemonset.go

@@ -0,0 +1,59 @@
+package kubemodel
+
+import (
+	"fmt"
+	"time"
+)
+
+// @bingen:generate:DaemonSet
+// DaemonSet represents a Kubernetes DaemonSet resource
+type DaemonSet struct {
+	UID              string            `json:"uid"`
+	NamespaceUID     string            `json:"namespaceUid"`
+	Name             string            `json:"name"`
+	Labels           map[string]string `json:"labels,omitempty"`
+	Annotations      map[string]string `json:"annotations,omitempty"`
+	DevicePluginInfo map[string]string `json:"devicePluginInfo"`
+	Start            time.Time         `json:"start,omitempty"`
+	End              time.Time         `json:"end,omitempty"`
+}
+
+func (d *DaemonSet) ValidateDaemonSet(window Window) error {
+	if d.UID == "" {
+		return fmt.Errorf("UID is missing for DaemonSet with name '%s'", d.Name)
+	}
+
+	if d.Name == "" {
+		return fmt.Errorf("Name is missing for DaemonSet '%s'", d.UID)
+	}
+
+	if d.NamespaceUID == "" {
+		return fmt.Errorf("NamespaceUID is missing for DaemonSet '%s'", d.UID)
+	}
+
+	if err := checkWindow(window, d.Start, d.End); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (kms *KubeModelSet) RegisterDaemonSet(daemonSet *DaemonSet) error {
+	if err := daemonSet.ValidateDaemonSet(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterDaemonSet: invalid daemonset: %w", err)
+		kms.Error(err)
+		return err
+	}
+
+	if _, ok := kms.DaemonSets[daemonSet.UID]; !ok {
+		if kms.Cluster == nil {
+			kms.Warnf("RegisterDaemonSet: Cluster is nil")
+		}
+
+		kms.DaemonSets[daemonSet.UID] = daemonSet
+
+		kms.Metadata.ObjectCount++
+	}
+
+	return nil
+}

+ 148 - 0
core/pkg/model/kubemodel/daemonset_test.go

@@ -0,0 +1,148 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateDaemonSet(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name      string
+		daemonSet *DaemonSet
+		wantErr   string
+	}{
+		{
+			name:      "empty UID",
+			daemonSet: &DaemonSet{Name: "my-ds", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:   "UID is missing for DaemonSet with name 'my-ds'",
+		},
+		{
+			name:      "empty Name",
+			daemonSet: &DaemonSet{UID: "ds-uid", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:   "Name is missing for DaemonSet 'ds-uid'",
+		},
+		{
+			name:      "empty NamespaceUID",
+			daemonSet: &DaemonSet{UID: "ds-uid", Name: "my-ds", Start: start, End: end},
+			wantErr:   "NamespaceUID is missing for DaemonSet 'ds-uid'",
+		},
+		{
+			name:      "outside window",
+			daemonSet: &DaemonSet{UID: "ds-uid", Name: "my-ds", NamespaceUID: "ns-uid", Start: start.Add(-time.Hour), End: end},
+			wantErr:   checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name:      "valid",
+			daemonSet: &DaemonSet{UID: "ds-uid", Name: "my-ds", NamespaceUID: "ns-uid", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.daemonSet.ValidateDaemonSet(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterDaemonSet(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newDaemonSet := func(uid, name string) *DaemonSet {
+		return &DaemonSet{UID: uid, Name: name, NamespaceUID: "ns-uid", Start: start, End: end}
+	}
+	withCluster := func(kms *KubeModelSet) {
+		kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+	}
+
+	tests := []struct {
+		name      string
+		setup     func(*KubeModelSet)
+		daemonSet *DaemonSet
+		wantErr   string
+		want      *KubeModelSet
+	}{
+		{
+			name:      "validation failure",
+			daemonSet: &DaemonSet{UID: "", Name: "my-ds", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:   "RegisterDaemonSet: invalid daemonset: UID is missing for DaemonSet with name 'my-ds'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterDaemonSet: invalid daemonset: UID is missing for DaemonSet with name 'my-ds'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:      "warns when cluster is nil",
+			daemonSet: newDaemonSet("ds-uid", "my-ds"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.DaemonSets["ds-uid"] = newDaemonSet("ds-uid", "my-ds")
+				kms.Metadata.ObjectCount = 1
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelWarning, Message: "RegisterDaemonSet: Cluster is nil"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:      "registers daemonset with cluster",
+			setup:     withCluster,
+			daemonSet: newDaemonSet("ds-uid", "my-ds"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.DaemonSets["ds-uid"] = newDaemonSet("ds-uid", "my-ds")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				withCluster(kms)
+				kms.RegisterDaemonSet(newDaemonSet("ds-uid", "original"))
+			},
+			daemonSet: newDaemonSet("ds-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.DaemonSets["ds-uid"] = newDaemonSet("ds-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterDaemonSet(tt.daemonSet)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 66 - 0
core/pkg/model/kubemodel/dcgm.go

@@ -0,0 +1,66 @@
+package kubemodel
+
+import (
+	"fmt"
+	"time"
+)
+
+// DCGMDevice holds recording from the DCGM exporter which provides identification and usage metrics for
+// Nvidia gpu. These Nvidia devices can be incorporated into the cluster via k8s Device Plugin API or DRAs.
+// While the DCGM exporter does provide unique identifiers for the containers that it is reporting metrics on,
+// It is split out here to provide some isolate from the rest of the KubeModel which represent universal structures
+// from the k8s API. It is left to the end user to interpret the relationships to the rest of the cluster based on
+// container unique identifiers
+// @bingen:generate:DCGMDevice
+type DCGMDevice struct {
+	UUID      string             `json:"uuid"`
+	Start     time.Time          `json:"start"`
+	End       time.Time          `json:"end"`
+	Device    string             `json:"device"`
+	ModelName string             `json:"modelName"`
+	PodUsages map[string]DCGMPod `json:"podUsages"`
+}
+
+// @bingen:generate:DCGMPod
+type DCGMPod struct {
+	ContainerUsages map[string]DCGMContainer `json:"container-usages"`
+}
+
+// @bingen:generate:DCGMContainer
+type DCGMContainer struct {
+	UsageAvg float64 `json:"usageAvg"`
+	UsageMax float64 `json:"usageMax"`
+}
+
+func (d *DCGMDevice) ValidateDCGMDevice(window Window) error {
+	if d.UUID == "" {
+		return fmt.Errorf("UUID is missing for DCGMDevice with device '%s'", d.Device)
+	}
+
+	if err := checkWindow(window, d.Start, d.End); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// RegisterDCGMDevice validates and adds a DCGMDevice to the set, keyed by UUID.
+func (kms *KubeModelSet) RegisterDCGMDevice(device *DCGMDevice) error {
+	if err := device.ValidateDCGMDevice(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterDCGMDevice: invalid dcgm device: %w", err)
+		kms.Error(err)
+		return err
+	}
+
+	if _, ok := kms.DCGMDevices[device.UUID]; !ok {
+		if kms.Cluster == nil {
+			kms.Warnf("RegisterDCGMDevice: Cluster is nil")
+		}
+
+		kms.DCGMDevices[device.UUID] = device
+
+		kms.Metadata.ObjectCount++
+	}
+
+	return nil
+}

+ 138 - 0
core/pkg/model/kubemodel/dcgm_test.go

@@ -0,0 +1,138 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateDCGMDevice(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name    string
+		device  *DCGMDevice
+		wantErr string
+	}{
+		{
+			name:    "empty UUID",
+			device:  &DCGMDevice{Device: "GPU-0", Start: start, End: end},
+			wantErr: "UUID is missing for DCGMDevice with device 'GPU-0'",
+		},
+		{
+			name:    "outside window",
+			device:  &DCGMDevice{UUID: "gpu-uuid", Device: "GPU-0", Start: start.Add(-time.Hour), End: end},
+			wantErr: checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name:   "valid",
+			device: &DCGMDevice{UUID: "gpu-uuid", Device: "GPU-0", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.device.ValidateDCGMDevice(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterDCGMDevice(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newDevice := func(uuid, device string) *DCGMDevice {
+		return &DCGMDevice{UUID: uuid, Device: device, Start: start, End: end}
+	}
+	withCluster := func(kms *KubeModelSet) {
+		kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+	}
+
+	tests := []struct {
+		name    string
+		setup   func(*KubeModelSet)
+		device  *DCGMDevice
+		wantErr string
+		want    *KubeModelSet
+	}{
+		{
+			name:    "validation failure",
+			device:  &DCGMDevice{UUID: "", Device: "GPU-0", Start: start, End: end},
+			wantErr: "RegisterDCGMDevice: invalid dcgm device: UUID is missing for DCGMDevice with device 'GPU-0'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterDCGMDevice: invalid dcgm device: UUID is missing for DCGMDevice with device 'GPU-0'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:   "warns when cluster is nil",
+			device: newDevice("gpu-uuid", "GPU-0"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.DCGMDevices["gpu-uuid"] = newDevice("gpu-uuid", "GPU-0")
+				kms.Metadata.ObjectCount = 1
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelWarning, Message: "RegisterDCGMDevice: Cluster is nil"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:   "registers device with cluster",
+			setup:  withCluster,
+			device: newDevice("gpu-uuid", "GPU-0"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.DCGMDevices["gpu-uuid"] = newDevice("gpu-uuid", "GPU-0")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				withCluster(kms)
+				kms.RegisterDCGMDevice(newDevice("gpu-uuid", "GPU-0"))
+			},
+			device: newDevice("gpu-uuid", "GPU-1"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.DCGMDevices["gpu-uuid"] = newDevice("gpu-uuid", "GPU-0")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterDCGMDevice(tt.device)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 59 - 0
core/pkg/model/kubemodel/deployment.go

@@ -0,0 +1,59 @@
+package kubemodel
+
+import (
+	"fmt"
+	"time"
+)
+
+// @bingen:generate:Deployment
+// Deployment represents a Kubernetes Deployment resource
+type Deployment struct {
+	UID          string            `json:"uid"`
+	NamespaceUID string            `json:"namespaceUid"`
+	Name         string            `json:"name"`
+	Labels       map[string]string `json:"labels,omitempty"`
+	Annotations  map[string]string `json:"annotations,omitempty"`
+	MatchLabels  map[string]string `json:"matchLabels"`
+	Start        time.Time         `json:"start"`
+	End          time.Time         `json:"end"`
+}
+
+func (d *Deployment) ValidateDeployment(window Window) error {
+	if d.UID == "" {
+		return fmt.Errorf("UID is missing for Deployment with name '%s'", d.Name)
+	}
+
+	if d.Name == "" {
+		return fmt.Errorf("Name is missing for Deployment '%s'", d.UID)
+	}
+
+	if d.NamespaceUID == "" {
+		return fmt.Errorf("NamespaceUID is missing for Deployment '%s'", d.UID)
+	}
+
+	if err := checkWindow(window, d.Start, d.End); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (kms *KubeModelSet) RegisterDeployment(deployment *Deployment) error {
+	if err := deployment.ValidateDeployment(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterDeployment: invalid deployment: %w", err)
+		kms.Error(err)
+		return err
+	}
+
+	if _, ok := kms.Deployments[deployment.UID]; !ok {
+		if kms.Cluster == nil {
+			kms.Warnf("RegisterDeployment: Cluster is nil")
+		}
+
+		kms.Deployments[deployment.UID] = deployment
+
+		kms.Metadata.ObjectCount++
+	}
+
+	return nil
+}

+ 148 - 0
core/pkg/model/kubemodel/deployment_test.go

@@ -0,0 +1,148 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateDeployment(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name       string
+		deployment *Deployment
+		wantErr    string
+	}{
+		{
+			name:       "empty UID",
+			deployment: &Deployment{Name: "my-deployment", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:    "UID is missing for Deployment with name 'my-deployment'",
+		},
+		{
+			name:       "empty Name",
+			deployment: &Deployment{UID: "dep-uid", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:    "Name is missing for Deployment 'dep-uid'",
+		},
+		{
+			name:       "empty NamespaceUID",
+			deployment: &Deployment{UID: "dep-uid", Name: "my-deployment", Start: start, End: end},
+			wantErr:    "NamespaceUID is missing for Deployment 'dep-uid'",
+		},
+		{
+			name:       "outside window",
+			deployment: &Deployment{UID: "dep-uid", Name: "my-deployment", NamespaceUID: "ns-uid", Start: start.Add(-time.Hour), End: end},
+			wantErr:    checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name:       "valid",
+			deployment: &Deployment{UID: "dep-uid", Name: "my-deployment", NamespaceUID: "ns-uid", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.deployment.ValidateDeployment(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterDeployment(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newDeployment := func(uid, name string) *Deployment {
+		return &Deployment{UID: uid, Name: name, NamespaceUID: "ns-uid", Start: start, End: end}
+	}
+	withCluster := func(kms *KubeModelSet) {
+		kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+	}
+
+	tests := []struct {
+		name       string
+		setup      func(*KubeModelSet)
+		deployment *Deployment
+		wantErr    string
+		want       *KubeModelSet
+	}{
+		{
+			name:       "validation failure",
+			deployment: &Deployment{UID: "", Name: "my-deployment", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:    "RegisterDeployment: invalid deployment: UID is missing for Deployment with name 'my-deployment'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterDeployment: invalid deployment: UID is missing for Deployment with name 'my-deployment'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:       "warns when cluster is nil",
+			deployment: newDeployment("dep-uid", "my-deployment"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Deployments["dep-uid"] = newDeployment("dep-uid", "my-deployment")
+				kms.Metadata.ObjectCount = 1
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelWarning, Message: "RegisterDeployment: Cluster is nil"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:       "registers deployment with cluster",
+			setup:      withCluster,
+			deployment: newDeployment("dep-uid", "my-deployment"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.Deployments["dep-uid"] = newDeployment("dep-uid", "my-deployment")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				withCluster(kms)
+				kms.RegisterDeployment(newDeployment("dep-uid", "original"))
+			},
+			deployment: newDeployment("dep-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.Deployments["dep-uid"] = newDeployment("dep-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterDeployment(tt.deployment)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 0 - 91
core/pkg/model/kubemodel/device.go

@@ -1,91 +0,0 @@
-package kubemodel
-
-import (
-	"errors"
-	"fmt"
-	"time"
-)
-
-type Device struct {
-	UID               string      `json:"uid"`            // Device UUID (hardware identifier)
-	Type              string      `json:"type,omitempty"` // Device type (e.g., "device", "tpu")
-	NodeUID           string      `json:"nodeUid"`        // Node hosting this device
-	DeviceNumber      int32       `json:"deviceNumber"`
-	ModelName         string      `json:"modelName"`
-	IsShared          bool        `json:"isShared"` // Device sharing information
-	SharePercentage   float64     `json:"sharePercentage"`
-	UsageSeconds      float64     `json:"usageSeconds"`      // Device seconds available
-	MemoryByteSeconds Measurement `json:"memoryByteSeconds"` // Device memory capacity in Byte-seconds
-	PowerWattSeconds  float64     `json:"powerWattSeconds"`  // Device power consumption in watt-seconds (Joules)
-	PowerWattMax      float64     `json:"powerWattMax"`      // Device max power consumption in watts
-	// Version 2 fields - Lifecycle tracking
-	Start           time.Time   `json:"start,omitempty"` // Device availability start
-	End             time.Time   `json:"end,omitempty"`   // Device availability end
-	DurationSeconds Measurement `json:"durationSeconds"` // Duration device was available
-}
-
-// Validate validates the Device fields
-func (d *Device) Validate() error {
-	if d.UID == "" {
-		return errors.New("UID is required")
-	}
-	if d.NodeUID == "" {
-		return errors.New("NodeUID is required")
-	}
-	if d.SharePercentage < 0 || d.SharePercentage > 100 {
-		return fmt.Errorf("SharePercentage must be 0-100, got %.2f", d.SharePercentage)
-	}
-	if d.PowerWattSeconds < 0 {
-		return fmt.Errorf("PowerWattSeconds cannot be negative, got %.2f", d.PowerWattSeconds)
-	}
-	if d.PowerWattMax < 0 {
-		return fmt.Errorf("PowerWattMax cannot be negative, got %.2f", d.PowerWattMax)
-	}
-	return nil
-}
-
-// Clone creates a deep copy of the Device
-func (d *Device) Clone() *Device {
-	if d == nil {
-		return nil
-	}
-
-	cloned := &Device{
-		UID:               d.UID,
-		Type:              d.Type,
-		NodeUID:           d.NodeUID,
-		DeviceNumber:      d.DeviceNumber,
-		ModelName:         d.ModelName,
-		IsShared:          d.IsShared,
-		SharePercentage:   d.SharePercentage,
-		UsageSeconds:      d.UsageSeconds,
-		MemoryByteSeconds: d.MemoryByteSeconds,
-		PowerWattSeconds:  d.PowerWattSeconds,
-		PowerWattMax:      d.PowerWattMax,
-		DurationSeconds:   d.DurationSeconds,
-	}
-
-	cloned.Start = d.Start
-	cloned.End = d.End
-
-	return cloned
-}
-
-func (kms *KubeModelSet) RegisterDevice(uid, nodeUID string) error {
-	if uid == "" {
-		err := fmt.Errorf("UID is nil for Device")
-		kms.Error(err)
-		return err
-	}
-
-	if _, ok := kms.Devices[uid]; !ok {
-		kms.Devices[uid] = &Device{
-			UID:     uid,
-			NodeUID: nodeUID,
-		}
-
-		kms.Metadata.ObjectCount++
-	}
-
-	return nil
-}

+ 0 - 85
core/pkg/model/kubemodel/device_usage.go

@@ -1,85 +0,0 @@
-package kubemodel
-
-import (
-	"errors"
-	"fmt"
-	"time"
-)
-
-type DeviceUsage struct {
-	ContainerUID          string      `json:"containerUid"`
-	DeviceUID             string      `json:"deviceUid"`
-	UsageSeconds          Measurement `json:"usageSeconds"`
-	UsagePercentageMax    float64     `json:"usagePercentageMax"`
-	MemoryByteSecondsUsed Measurement `json:"memoryByteSecondsUsed"`
-	DeviceType            string      `json:"deviceType,omitempty"`
-	DurationSeconds       Measurement `json:"durationSeconds,omitempty"`
-	Start                 time.Time   `json:"start"`
-	End                   time.Time   `json:"end"`
-}
-
-func (u *DeviceUsage) Validate() error {
-	if u.ContainerUID == "" {
-		return errors.New("ContainerUID is required")
-	}
-	if u.DeviceUID == "" {
-		return errors.New("DeviceUID is required")
-	}
-	if u.UsagePercentageMax < 0 || u.UsagePercentageMax > 100 {
-		return fmt.Errorf("UsagePercentageMax must be 0-100, got %.2f", u.UsagePercentageMax)
-	}
-	return nil
-}
-
-func (u *DeviceUsage) Clone() *DeviceUsage {
-	if u == nil {
-		return nil
-	}
-
-	cloned := &DeviceUsage{
-		ContainerUID:          u.ContainerUID,
-		DeviceUID:             u.DeviceUID,
-		UsageSeconds:          u.UsageSeconds,
-		UsagePercentageMax:    u.UsagePercentageMax,
-		MemoryByteSecondsUsed: u.MemoryByteSecondsUsed,
-		DeviceType:            u.DeviceType,
-		DurationSeconds:       u.DurationSeconds,
-		Start:                 u.Start,
-		End:                   u.End,
-	}
-
-	return cloned
-}
-
-func (u *DeviceUsage) UsageAverage() Measurement {
-	if u.DurationSeconds == 0 {
-		return 0
-	}
-	return (u.UsageSeconds / u.DurationSeconds) * 100
-}
-
-func (u *DeviceUsage) MemoryByteUsageAverage() Measurement {
-	if u.DurationSeconds == 0 {
-		return 0
-	}
-	return u.MemoryByteSecondsUsed / u.DurationSeconds
-}
-
-func (kms *KubeModelSet) RegisterUsage(id, containerID, deviceId string) error {
-	if id == "" {
-		err := fmt.Errorf("UID is nil for DeviceUsage")
-		kms.Error(err)
-		return err
-	}
-
-	if _, ok := kms.DeviceUsages[id]; !ok {
-		kms.DeviceUsages[id] = &DeviceUsage{
-			ContainerUID: containerID,
-			DeviceUID:    deviceId,
-		}
-
-		kms.Metadata.ObjectCount++
-	}
-
-	return nil
-}

+ 58 - 0
core/pkg/model/kubemodel/job.go

@@ -0,0 +1,58 @@
+package kubemodel
+
+import (
+	"fmt"
+	"time"
+)
+
+// @bingen:generate:Job
+// Job represents a Kubernetes Job resource
+type Job struct {
+	UID          string            `json:"uid"`
+	NamespaceUID string            `json:"namespaceUid"`
+	Name         string            `json:"name"`
+	Labels       map[string]string `json:"labels,omitempty"`
+	Annotations  map[string]string `json:"annotations,omitempty"`
+	Start        time.Time         `json:"start,omitempty"`
+	End          time.Time         `json:"end,omitempty"`
+}
+
+func (j *Job) ValidateJob(window Window) error {
+	if j.UID == "" {
+		return fmt.Errorf("UID is missing for Job with name '%s'", j.Name)
+	}
+
+	if j.Name == "" {
+		return fmt.Errorf("Name is missing for Job '%s'", j.UID)
+	}
+
+	if j.NamespaceUID == "" {
+		return fmt.Errorf("NamespaceUID is missing for Job '%s'", j.UID)
+	}
+
+	if err := checkWindow(window, j.Start, j.End); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (kms *KubeModelSet) RegisterJob(job *Job) error {
+	if err := job.ValidateJob(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterJob: invalid job: %w", err)
+		kms.Error(err)
+		return err
+	}
+
+	if _, ok := kms.Jobs[job.UID]; !ok {
+		if kms.Cluster == nil {
+			kms.Warnf("RegisterJob: Cluster is nil")
+		}
+
+		kms.Jobs[job.UID] = job
+
+		kms.Metadata.ObjectCount++
+	}
+
+	return nil
+}

+ 148 - 0
core/pkg/model/kubemodel/job_test.go

@@ -0,0 +1,148 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateJob(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name    string
+		job     *Job
+		wantErr string
+	}{
+		{
+			name:    "empty UID",
+			job:     &Job{Name: "my-job", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "UID is missing for Job with name 'my-job'",
+		},
+		{
+			name:    "empty Name",
+			job:     &Job{UID: "job-uid", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "Name is missing for Job 'job-uid'",
+		},
+		{
+			name:    "empty NamespaceUID",
+			job:     &Job{UID: "job-uid", Name: "my-job", Start: start, End: end},
+			wantErr: "NamespaceUID is missing for Job 'job-uid'",
+		},
+		{
+			name:    "outside window",
+			job:     &Job{UID: "job-uid", Name: "my-job", NamespaceUID: "ns-uid", Start: start.Add(-time.Hour), End: end},
+			wantErr: checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name: "valid",
+			job:  &Job{UID: "job-uid", Name: "my-job", NamespaceUID: "ns-uid", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.job.ValidateJob(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterJob(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newJob := func(uid, name string) *Job {
+		return &Job{UID: uid, Name: name, NamespaceUID: "ns-uid", Start: start, End: end}
+	}
+	withCluster := func(kms *KubeModelSet) {
+		kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+	}
+
+	tests := []struct {
+		name    string
+		setup   func(*KubeModelSet)
+		job     *Job
+		wantErr string
+		want    *KubeModelSet
+	}{
+		{
+			name:    "validation failure",
+			job:     &Job{UID: "", Name: "my-job", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "RegisterJob: invalid job: UID is missing for Job with name 'my-job'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterJob: invalid job: UID is missing for Job with name 'my-job'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name: "warns when cluster is nil",
+			job:  newJob("job-uid", "my-job"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Jobs["job-uid"] = newJob("job-uid", "my-job")
+				kms.Metadata.ObjectCount = 1
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelWarning, Message: "RegisterJob: Cluster is nil"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:  "registers job with cluster",
+			setup: withCluster,
+			job:   newJob("job-uid", "my-job"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.Jobs["job-uid"] = newJob("job-uid", "my-job")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				withCluster(kms)
+				kms.RegisterJob(newJob("job-uid", "original"))
+			},
+			job: newJob("job-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.Jobs["job-uid"] = newJob("job-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterJob(tt.job)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 53 - 24
core/pkg/model/kubemodel/kubemodel.go

@@ -1,27 +1,30 @@
 package kubemodel
 package kubemodel
 
 
 import (
 import (
+	"fmt"
 	"time"
 	"time"
 )
 )
 
 
-// TODO: should we add a lock so that we can safely modify KubeModelSet in parallel?
-
 // @bingen:generate[streamable,stringtable]:KubeModelSet
 // @bingen:generate[streamable,stringtable]:KubeModelSet
 type KubeModelSet struct {
 type KubeModelSet struct {
-	Metadata               *Metadata                         `json:"meta"`                   // @bingen:field[version=1]
-	Window                 Window                            `json:"window"`                 // @bingen:field[version=1]
-	Cluster                *Cluster                          `json:"cluster"`                // @bingen:field[version=1]
-	Namespaces             map[string]*Namespace             `json:"namespaces"`             // @bingen:field[version=1]
-	ResourceQuotas         map[string]*ResourceQuota         `json:"resourceQuotas"`         // @bingen:field[version=1]
-	Containers             map[string]*Container             `json:"containers,omitempty"`   // @bingen:field[ignore]
-	Owners                 map[string]*Owner                 `json:"owners,omitempty"`       // @bingen:field[ignore]
-	Devices                map[string]*Device                `json:"devices,omitempty"`      // @bingen:field[ignore]
-	DeviceUsages           map[string]*DeviceUsage           `json:"deviceUsages,omitempty"` // @bingen:field[ignore]
-	Nodes                  map[string]*Node                  `json:"nodes,omitempty"`        // @bingen:field[ignore]
-	Pods                   map[string]*Pod                   `json:"pods,omitempty"`         // @bingen:field[ignore]
-	PersistentVolumeClaims map[string]*PersistentVolumeClaim `json:"pvcs,omitempty"`         // @bingen:field[ignore]
-	Services               map[string]*Service               `json:"services,omitempty"`     // @bingen:field[ignore]
-	Volumes                map[string]*PersistentVolume      `json:"volumes,omitempty"`      // @bingen:field[ignore]
+	Metadata               *Metadata                         `json:"meta"`              // @bingen:field[version=1]
+	Window                 Window                            `json:"window"`            // @bingen:field[version=1]
+	Cluster                *Cluster                          `json:"cluster"`           // @bingen:field[version=1]
+	Namespaces             map[string]*Namespace             `json:"namespaces"`        // @bingen:field[version=1]
+	ResourceQuotas         map[string]*ResourceQuota         `json:"resourceQuotas"`    // @bingen:field[version=1]
+	Services               map[string]*Service               `json:"services"`          // @bingen:field[version=2]
+	Deployments            map[string]*Deployment            `json:"deployments"`       // @bingen:field[version=2]
+	StatefulSets           map[string]*StatefulSet           `json:"statefulSets"`      // @bingen:field[version=2]
+	DaemonSets             map[string]*DaemonSet             `json:"daemonSets"`        // @bingen:field[version=2]
+	Jobs                   map[string]*Job                   `json:"jobs"`              // @bingen:field[version=2]
+	CronJobs               map[string]*CronJob               `json:"cronJobs"`          // @bingen:field[version=2]
+	ReplicaSets            map[string]*ReplicaSet            `json:"replicaSets"`       // @bingen:field[version=2]
+	Nodes                  map[string]*Node                  `json:"nodes"`             // @bingen:field[version=2]
+	PersistentVolumes      map[string]*PersistentVolume      `json:"persistentVolumes"` // @bingen:field[version=2]
+	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]
 	idx                    *kubeModelSetIndexes              // @bingen:field[ignore]
 	idx                    *kubeModelSetIndexes              // @bingen:field[ignore]
 }
 }
 
 
@@ -38,16 +41,20 @@ func NewKubeModelSet(start time.Time, end time.Time) *KubeModelSet {
 			End:   end,
 			End:   end,
 		},
 		},
 		Containers:             map[string]*Container{},
 		Containers:             map[string]*Container{},
-		Owners:                 map[string]*Owner{},
-		Devices:                map[string]*Device{},
-		DeviceUsages:           map[string]*DeviceUsage{},
+		Deployments:            map[string]*Deployment{},
+		StatefulSets:           map[string]*StatefulSet{},
+		DaemonSets:             map[string]*DaemonSet{},
+		Jobs:                   map[string]*Job{},
+		CronJobs:               map[string]*CronJob{},
+		ReplicaSets:            map[string]*ReplicaSet{},
 		Namespaces:             map[string]*Namespace{},
 		Namespaces:             map[string]*Namespace{},
 		Nodes:                  map[string]*Node{},
 		Nodes:                  map[string]*Node{},
+		DCGMDevices:            map[string]*DCGMDevice{},
 		Pods:                   map[string]*Pod{},
 		Pods:                   map[string]*Pod{},
 		PersistentVolumeClaims: map[string]*PersistentVolumeClaim{},
 		PersistentVolumeClaims: map[string]*PersistentVolumeClaim{},
 		ResourceQuotas:         map[string]*ResourceQuota{},
 		ResourceQuotas:         map[string]*ResourceQuota{},
 		Services:               map[string]*Service{},
 		Services:               map[string]*Service{},
-		Volumes:                map[string]*PersistentVolume{},
+		PersistentVolumes:      map[string]*PersistentVolume{},
 		idx:                    newKubeModelSetIndexes(),
 		idx:                    newKubeModelSetIndexes(),
 	}
 	}
 	return kms
 	return kms
@@ -76,16 +83,38 @@ func (kms *KubeModelSet) IsEmpty() bool {
 
 
 	// Check if all resource maps are empty
 	// Check if all resource maps are empty
 	return len(kms.Containers) == 0 &&
 	return len(kms.Containers) == 0 &&
-		len(kms.Owners) == 0 &&
-		len(kms.Devices) == 0 &&
-		len(kms.DeviceUsages) == 0 &&
+		len(kms.Deployments) == 0 &&
+		len(kms.StatefulSets) == 0 &&
+		len(kms.DaemonSets) == 0 &&
+		len(kms.Jobs) == 0 &&
+		len(kms.CronJobs) == 0 &&
+		len(kms.ReplicaSets) == 0 &&
 		len(kms.Namespaces) == 0 &&
 		len(kms.Namespaces) == 0 &&
 		len(kms.Nodes) == 0 &&
 		len(kms.Nodes) == 0 &&
+		len(kms.DCGMDevices) == 0 &&
 		len(kms.Pods) == 0 &&
 		len(kms.Pods) == 0 &&
 		len(kms.PersistentVolumeClaims) == 0 &&
 		len(kms.PersistentVolumeClaims) == 0 &&
 		len(kms.ResourceQuotas) == 0 &&
 		len(kms.ResourceQuotas) == 0 &&
 		len(kms.Services) == 0 &&
 		len(kms.Services) == 0 &&
-		len(kms.Volumes) == 0
+		len(kms.PersistentVolumes) == 0
+}
+
+// checkWindow validates that the given start/end times are fully contained within
+// the KubeModelSet window. It records and returns an error if they are not.
+func checkWindow(window Window, start, end time.Time) error {
+	if window.Start.After(start) ||
+		window.Start.After(end) ||
+		window.End.Before(start) ||
+		window.End.Before(end) {
+		return fmt.Errorf(
+			"start or end time (%s-%s) is outside of the window %s-%s",
+			start.Format(time.RFC3339),
+			end.Format(time.RFC3339),
+			window.Start.Format(time.RFC3339),
+			window.End.Format(time.RFC3339),
+		)
+	}
+	return nil
 }
 }
 
 
 type kubeModelSetIndexes struct {
 type kubeModelSetIndexes struct {

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 7611 - 558
core/pkg/model/kubemodel/kubemodel_codecs.go


+ 21 - 456
core/pkg/model/kubemodel/kubemodel_codecs_test.go

@@ -1,474 +1,39 @@
 package kubemodel
 package kubemodel
 
 
 import (
 import (
-	"errors"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/require"
 )
 )
 
 
-func TestKubeModelMarshalBinary(t *testing.T) {
-	s := time.Now().UTC().Truncate(time.Hour)
-	e := s.Add(time.Hour)
+func TestKubeModelSetCodecRoundTrip(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
 
 
-	// Test empty KubeModelSet
+	t.Run("empty KubeModelSet", func(t *testing.T) {
+		kms := NewKubeModelSet(start, end)
 
 
-	kms := NewKubeModelSet(s, e)
+		b, err := kms.MarshalBinary()
+		require.NoError(t, err)
 
 
-	b, err := kms.MarshalBinary()
-	require.NoError(t, err)
+		act := new(KubeModelSet)
+		err = act.UnmarshalBinary(b)
+		require.NoError(t, err)
 
 
-	var act = new(KubeModelSet)
-	err = act.UnmarshalBinary(b)
-	require.NoError(t, err)
+		KubeModelSetEquals(t, kms, act)
+	})
 
 
-	require.Equal(t, kms.Metadata, act.Metadata)
-	require.Equal(t, kms.Window, act.Window)
-	require.Equal(t, kms.Cluster, act.Cluster)
-	require.Equal(t, kms.Namespaces, act.Namespaces)
-	require.Equal(t, kms.ResourceQuotas, act.ResourceQuotas)
+	t.Run("full KubeModelSet", func(t *testing.T) {
+		kms := NewMockKubeModelSet(start, end)
 
 
-	// Test non-empty KubeModelSet
+		b, err := kms.MarshalBinary()
+		require.NoError(t, err)
 
 
-	kms = NewKubeModelSet(s, e)
+		act := new(KubeModelSet)
+		err = act.UnmarshalBinary(b)
+		require.NoError(t, err)
 
 
-	kms.Metadata.CreatedAt = time.Now().UTC()
-
-	kms.RegisterCluster("cluster")
-	kms.Cluster.Start = s
-	kms.Cluster.End = e
-
-	kms.RegisterNamespace("ns1", "ns1")
-	kms.Namespaces["ns1"].Start = s
-	kms.Namespaces["ns1"].End = e
-	kms.Namespaces["ns1"].Labels = map[string]string{"label1": "label1"}
-	kms.Namespaces["ns1"].Annotations = map[string]string{"anno1": "anno1"}
-
-	kms.RegisterNamespace("ns2", "ns2")
-	kms.Namespaces["ns2"].Start = s
-	kms.Namespaces["ns2"].End = e
-	kms.Namespaces["ns2"].Labels = map[string]string{"label2": "label2"}
-	kms.Namespaces["ns2"].Annotations = map[string]string{"anno2": "anno2"}
-
-	kms.RegisterResourceQuota("rq1", "rq1", "ns1")
-	kms.ResourceQuotas["rq1"].Start = s
-	kms.ResourceQuotas["rq1"].End = e
-	kms.ResourceQuotas["rq1"].Spec = &ResourceQuotaSpec{
-		Hard: &ResourceQuotaSpecHard{
-			Requests: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-			Limits: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-		},
-	}
-	kms.ResourceQuotas["rq1"].Status = &ResourceQuotaStatus{
-		Used: &ResourceQuotaStatusUsed{
-			Requests: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-			Limits: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-		},
-	}
-
-	kms.RegisterResourceQuota("rq2", "rq2", "ns1")
-	kms.ResourceQuotas["rq2"].Start = s
-	kms.ResourceQuotas["rq2"].End = e
-	kms.ResourceQuotas["rq2"].Spec = &ResourceQuotaSpec{
-		Hard: &ResourceQuotaSpecHard{
-			Requests: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-			Limits: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-		},
-	}
-	kms.ResourceQuotas["rq2"].Status = &ResourceQuotaStatus{
-		Used: &ResourceQuotaStatusUsed{
-			Requests: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-			Limits: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-		},
-	}
-
-	kms.RegisterResourceQuota("rq3", "rq3", "ns2")
-	kms.ResourceQuotas["rq3"].Start = s
-	kms.ResourceQuotas["rq3"].End = e
-	kms.ResourceQuotas["rq3"].Spec = &ResourceQuotaSpec{
-		Hard: &ResourceQuotaSpecHard{
-			Requests: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-			Limits: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-		},
-	}
-	kms.ResourceQuotas["rq3"].Status = &ResourceQuotaStatus{
-		Used: &ResourceQuotaStatusUsed{
-			Requests: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-			Limits: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-		},
-	}
-
-	kms.RegisterResourceQuota("rq4", "rq4", "ns2")
-	kms.ResourceQuotas["rq4"].Start = s
-	kms.ResourceQuotas["rq4"].End = e
-	kms.ResourceQuotas["rq4"].Spec = &ResourceQuotaSpec{
-		Hard: &ResourceQuotaSpecHard{
-			Requests: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-			Limits: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-		},
-	}
-	kms.ResourceQuotas["rq4"].Status = &ResourceQuotaStatus{
-		Used: &ResourceQuotaStatusUsed{
-			Requests: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-			Limits: ResourceQuantities{
-				ResourceCPU: ResourceQuantity{
-					Resource: ResourceCPU,
-					Unit:     UnitMillicore,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-				ResourceMemory: ResourceQuantity{
-					Resource: ResourceMemory,
-					Unit:     UnitByte,
-					Values: Stats{
-						StatAvg: 1,
-						StatMax: 1,
-						StatP85: 1,
-						StatP95: 1,
-					},
-				},
-			},
-		},
-	}
-
-	kms.Error(errors.New("test error"))
-	kms.Warn("test warning")
-	kms.Info("test info")
-	kms.Debug("test debug")
-	kms.Trace("test trace")
-
-	kms.Metadata.CompletedAt = time.Now().UTC()
-
-	b, err = kms.MarshalBinary()
-	require.NoError(t, err)
-
-	act = new(KubeModelSet)
-	err = act.UnmarshalBinary(b)
-	require.NoError(t, err)
-
-	require.Equal(t, kms.Metadata, act.Metadata)
-	require.Equal(t, kms.Window, act.Window)
-	require.Equal(t, kms.Cluster, act.Cluster)
-	require.Equal(t, kms.Namespaces, act.Namespaces)
-	require.Equal(t, kms.ResourceQuotas, act.ResourceQuotas)
+		KubeModelSetEquals(t, kms, act)
+	})
 }
 }

+ 63 - 0
core/pkg/model/kubemodel/kubemodel_helpers_test.go

@@ -0,0 +1,63 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+// requireWindowEqual compares two start/end time pairs using time.Time.Equal,
+// which is insensitive to monotonic clock readings and timezone representation.
+func requireWindowEqual(t *testing.T, thisStart, thatStart, thisEnd, thatEnd time.Time) {
+	t.Helper()
+	require.True(t, thisStart.Equal(thatStart), "Start mismatch: %v != %v", thisStart, thatStart)
+	require.True(t, thisEnd.Equal(thatEnd), "End mismatch: %v != %v", thisEnd, thatEnd)
+}
+
+// DiagnosticEquals asserts that two Diagnostics have identical state.
+// Timestamp is excluded from comparison since it is volatile across instances.
+func DiagnosticEquals(t *testing.T, this, that Diagnostic) {
+	t.Helper()
+	require.Equal(t, this.Level, that.Level)
+	require.Equal(t, this.Message, that.Message)
+	require.Equal(t, this.Details, that.Details)
+}
+
+// MetadataEquals asserts that two Metadata structs have identical state.
+// Timestamps (CreatedAt, CompletedAt, Diagnostic.Timestamp) are excluded
+// from comparison since they are volatile across instances.
+func MetadataEquals(t *testing.T, this, that *Metadata) {
+	t.Helper()
+	require.Equal(t, this.ObjectCount, that.ObjectCount)
+	require.Equal(t, this.DiagnosticLevel, that.DiagnosticLevel)
+	require.Equal(t, len(this.Diagnostics), len(that.Diagnostics))
+	for i := range this.Diagnostics {
+		DiagnosticEquals(t, this.Diagnostics[i], that.Diagnostics[i])
+	}
+}
+
+// KubeModelSetEquals asserts that two KubeModelSets have identical state.
+// Metadata timestamps (CreatedAt, CompletedAt, Diagnostic.Timestamp) are
+// excluded from comparison since they are volatile across instances.
+func KubeModelSetEquals(t *testing.T, this, that *KubeModelSet) {
+	t.Helper()
+	require.Equal(t, this.Window, that.Window)
+	require.Equal(t, this.Cluster, that.Cluster)
+	MetadataEquals(t, this.Metadata, that.Metadata)
+	require.Equal(t, this.Namespaces, that.Namespaces)
+	require.Equal(t, this.ResourceQuotas, that.ResourceQuotas)
+	require.Equal(t, this.Containers, that.Containers)
+	require.Equal(t, this.Deployments, that.Deployments)
+	require.Equal(t, this.StatefulSets, that.StatefulSets)
+	require.Equal(t, this.DaemonSets, that.DaemonSets)
+	require.Equal(t, this.Jobs, that.Jobs)
+	require.Equal(t, this.CronJobs, that.CronJobs)
+	require.Equal(t, this.ReplicaSets, that.ReplicaSets)
+	require.Equal(t, this.Nodes, that.Nodes)
+	require.Equal(t, this.Pods, that.Pods)
+	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)
+}

+ 109 - 65
core/pkg/model/kubemodel/kubemodel_test.go

@@ -2,12 +2,76 @@ package kubemodel
 
 
 import (
 import (
 	"errors"
 	"errors"
+	"fmt"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/require"
 )
 )
 
 
+func TestCheckWindow(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	windowErrMsg := func(s, e time.Time) string {
+		return fmt.Sprintf(
+			"start or end time (%s-%s) is outside of the window %s-%s",
+			s.Format(time.RFC3339),
+			e.Format(time.RFC3339),
+			start.Format(time.RFC3339),
+			end.Format(time.RFC3339),
+		)
+	}
+
+	tests := []struct {
+		name    string
+		start   time.Time
+		end     time.Time
+		wantErr string
+	}{
+		{
+			name:    "start before window",
+			start:   start.Add(-time.Minute),
+			end:     end,
+			wantErr: windowErrMsg(start.Add(-time.Minute), end),
+		},
+		{
+			name:    "end after window",
+			start:   start,
+			end:     end.Add(time.Minute),
+			wantErr: windowErrMsg(start, end.Add(time.Minute)),
+		},
+		{
+			name:    "entirely outside window",
+			start:   end.Add(time.Hour),
+			end:     end.Add(2 * time.Hour),
+			wantErr: windowErrMsg(end.Add(time.Hour), end.Add(2*time.Hour)),
+		},
+		{
+			name:  "exact window boundaries",
+			start: start,
+			end:   end,
+		},
+		{
+			name:  "within window",
+			start: start.Add(15 * time.Minute),
+			end:   end.Add(-15 * time.Minute),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := checkWindow(window, tt.start, tt.end)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
 func TestKubeModel(t *testing.T) {
 func TestKubeModel(t *testing.T) {
 	start := time.Now().UTC().Truncate(time.Hour)
 	start := time.Now().UTC().Truncate(time.Hour)
 	end := start.Add(time.Hour)
 	end := start.Add(time.Hour)
@@ -33,11 +97,11 @@ func TestKubeModel(t *testing.T) {
 
 
 			kms := NewKubeModelSet(start, end)
 			kms := NewKubeModelSet(start, end)
 
 
-			err = kms.RegisterCluster("")
+			err = kms.RegisterCluster(&Cluster{UID: ""})
 			require.NotNil(t, err)
 			require.NotNil(t, err)
 
 
 			require.Len(t, kms.GetErrors(), 1)
 			require.Len(t, kms.GetErrors(), 1)
-			require.Equal(t, "RegisterCluster: uid is nil", kms.GetErrors()[0].Message)
+			require.Equal(t, "RegisterCluster: invalid cluster: UID is missing for Cluster with name ''", kms.GetErrors()[0].Message)
 			require.Nil(t, kms.Cluster)
 			require.Nil(t, kms.Cluster)
 		})
 		})
 
 
@@ -47,7 +111,7 @@ func TestKubeModel(t *testing.T) {
 
 
 			kms := NewKubeModelSet(start, end)
 			kms := NewKubeModelSet(start, end)
 
 
-			err = kms.RegisterCluster(clusterUID)
+			err = kms.RegisterCluster(&Cluster{UID: clusterUID, Start: start, End: end})
 			require.Nil(t, err)
 			require.Nil(t, err)
 
 
 			require.Len(t, kms.GetErrors(), 0)
 			require.Len(t, kms.GetErrors(), 0)
@@ -61,7 +125,7 @@ func TestKubeModel(t *testing.T) {
 
 
 			kms := NewKubeModelSet(start, end)
 			kms := NewKubeModelSet(start, end)
 
 
-			err = kms.RegisterCluster(clusterUID)
+			err = kms.RegisterCluster(&Cluster{UID: clusterUID, Start: start, End: end})
 			require.Nil(t, err)
 			require.Nil(t, err)
 
 
 			require.Len(t, kms.GetErrors(), 0)
 			require.Len(t, kms.GetErrors(), 0)
@@ -69,7 +133,7 @@ func TestKubeModel(t *testing.T) {
 			require.Equal(t, clusterUID, kms.Cluster.UID)
 			require.Equal(t, clusterUID, kms.Cluster.UID)
 
 
 			// Register cluster with same UID, expect no-op on second try
 			// Register cluster with same UID, expect no-op on second try
-			err = kms.RegisterCluster(clusterUID)
+			err = kms.RegisterCluster(&Cluster{UID: clusterUID, Start: start, End: end})
 			require.Nil(t, err)
 			require.Nil(t, err)
 
 
 			require.Len(t, kms.GetErrors(), 0)
 			require.Len(t, kms.GetErrors(), 0)
@@ -77,7 +141,7 @@ func TestKubeModel(t *testing.T) {
 			require.Equal(t, clusterUID, kms.Cluster.UID)
 			require.Equal(t, clusterUID, kms.Cluster.UID)
 
 
 			// Register cluster with another UID (should not happen), expect no-op
 			// Register cluster with another UID (should not happen), expect no-op
-			err = kms.RegisterCluster("another-uid")
+			err = kms.RegisterCluster(&Cluster{UID: "another-uid", Start: start, End: end})
 			require.Nil(t, err)
 			require.Nil(t, err)
 
 
 			require.Len(t, kms.GetWarnings(), 1)
 			require.Len(t, kms.GetWarnings(), 1)
@@ -93,11 +157,11 @@ func TestKubeModel(t *testing.T) {
 
 
 			kms := NewKubeModelSet(start, end)
 			kms := NewKubeModelSet(start, end)
 
 
-			err = kms.RegisterNamespace("", "")
+			err = kms.RegisterNamespace(&Namespace{UID: "", Name: ""})
 			require.NotNil(t, err)
 			require.NotNil(t, err)
 
 
 			require.Len(t, kms.GetErrors(), 1)
 			require.Len(t, kms.GetErrors(), 1)
-			require.Equal(t, "UID is nil for Namespace ''", kms.GetErrors()[0].Message)
+			require.Equal(t, "RegisterNamespace: invalid namespace: UID is missing for Namespace with name ''", kms.GetErrors()[0].Message)
 			require.Len(t, kms.Namespaces, 0)
 			require.Len(t, kms.Namespaces, 0)
 		})
 		})
 
 
@@ -109,13 +173,13 @@ func TestKubeModel(t *testing.T) {
 			testUID := "uid"
 			testUID := "uid"
 			testName := "name"
 			testName := "name"
 
 
-			err = kms.RegisterNamespace(testUID, testName)
+			err = kms.RegisterNamespace(&Namespace{UID: testUID, Name: testName, Start: start, End: end})
 			require.Nil(t, err)
 			require.Nil(t, err)
 
 
 			require.Len(t, kms.GetWarnings(), 1)
 			require.Len(t, kms.GetWarnings(), 1)
-			require.Equal(t, "RegisterNamespace(uid, name): Cluster is nil", kms.GetWarnings()[0].Message)
+			require.Equal(t, "RegisterNamespace: Cluster is nil", kms.GetWarnings()[0].Message)
 
 
-			testNamespace := &Namespace{UID: testUID, ClusterUID: "", Name: testName}
+			testNamespace := &Namespace{UID: testUID, Name: testName, Start: start, End: end}
 
 
 			require.NotNil(t, kms.Namespaces[testUID])
 			require.NotNil(t, kms.Namespaces[testUID])
 			require.Equal(t, testNamespace, kms.Namespaces[testUID])
 			require.Equal(t, testNamespace, kms.Namespaces[testUID])
@@ -128,7 +192,7 @@ func TestKubeModel(t *testing.T) {
 			var err error
 			var err error
 
 
 			kms := NewKubeModelSet(start, end)
 			kms := NewKubeModelSet(start, end)
-			err = kms.RegisterCluster("cluster-uid")
+			err = kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
 			require.Nil(t, err)
 			require.Nil(t, err)
 
 
 			// At this point we have a KMS with a cluster registered
 			// At this point we have a KMS with a cluster registered
@@ -136,20 +200,20 @@ func TestKubeModel(t *testing.T) {
 			testUID := "uid"
 			testUID := "uid"
 			testName := "name"
 			testName := "name"
 
 
-			err = kms.RegisterNamespace(testUID, testName)
+			err = kms.RegisterNamespace(&Namespace{UID: testUID, Name: testName, Start: start, End: end})
 			require.Nil(t, err)
 			require.Nil(t, err)
 
 
 			require.Len(t, kms.GetErrors(), 0)
 			require.Len(t, kms.GetErrors(), 0)
 			require.NotNil(t, kms.Namespaces[testUID])
 			require.NotNil(t, kms.Namespaces[testUID])
 
 
-			testNamespace := &Namespace{UID: testUID, ClusterUID: "cluster-uid", Name: testName}
+			testNamespace := &Namespace{UID: testUID, Name: testName, Start: start, End: end}
 
 
 			require.Equal(t, testNamespace, kms.Namespaces[testUID])
 			require.Equal(t, testNamespace, kms.Namespaces[testUID])
 			require.Equal(t, testNamespace, kms.idx.namespaceByName[testName])
 			require.Equal(t, testNamespace, kms.idx.namespaceByName[testName])
 			require.Equal(t, 1, kms.Metadata.ObjectCount)
 			require.Equal(t, 1, kms.Metadata.ObjectCount)
 
 
 			// Register same namespace again, expect no-op on second try
 			// Register same namespace again, expect no-op on second try
-			err = kms.RegisterNamespace(testUID, testName)
+			err = kms.RegisterNamespace(&Namespace{UID: testUID, Name: testName, Start: start, End: end})
 			require.Nil(t, err)
 			require.Nil(t, err)
 
 
 			require.Len(t, kms.GetErrors(), 0)
 			require.Len(t, kms.GetErrors(), 0)
@@ -166,55 +230,42 @@ func TestKubeModel(t *testing.T) {
 
 
 			kms := NewKubeModelSet(start, end)
 			kms := NewKubeModelSet(start, end)
 
 
-			err = kms.RegisterResourceQuota("", "test", "")
+			err = kms.RegisterResourceQuota(&ResourceQuota{UID: "", Name: "test"})
 			require.NotNil(t, err)
 			require.NotNil(t, err)
 			require.Len(t, kms.GetErrors(), 1)
 			require.Len(t, kms.GetErrors(), 1)
-			require.Equal(t, "UID is nil for ResourceQuota 'test'", kms.GetErrors()[0].Message)
+			require.Equal(t, "RegisterResourceQuota: invalid resource quota: UID is missing for ResourceQuota with name 'test'", kms.GetErrors()[0].Message)
 			require.Len(t, kms.ResourceQuotas, 0)
 			require.Len(t, kms.ResourceQuotas, 0)
 		})
 		})
 
 
-		t.Run("register resource quota on KMS w/o namespace", func(t *testing.T) {
+		t.Run("register resource quota with empty NamespaceUID", func(t *testing.T) {
 			var err error
 			var err error
 
 
 			kms := NewKubeModelSet(start, end)
 			kms := NewKubeModelSet(start, end)
 
 
-			testUID := "uid"
-			testName := "name"
-
-			err = kms.RegisterResourceQuota(testUID, testName, "unregistered-namespace")
-			require.Nil(t, err)
-			require.Len(t, kms.GetWarnings(), 1)
-			require.Equal(t, "RegisterResourceQuota(uid, name, unregistered-namespace): missing namespace", kms.GetWarnings()[0].Message)
-
-			testRQ := &ResourceQuota{
-				UID:          "uid",
-				NamespaceUID: "",
-				Name:         "name",
-				Spec:         &ResourceQuotaSpec{Hard: &ResourceQuotaSpecHard{}},
-				Status:       &ResourceQuotaStatus{Used: &ResourceQuotaStatusUsed{}},
-			}
-
-			require.NotNil(t, kms.ResourceQuotas[testUID])
-			require.Equal(t, testRQ, kms.ResourceQuotas[testUID])
-			require.Equal(t, 1, kms.Metadata.ObjectCount)
+			err = kms.RegisterResourceQuota(&ResourceQuota{UID: "uid", Name: "name", NamespaceUID: ""})
+			require.NotNil(t, err)
+			require.Len(t, kms.GetErrors(), 1)
+			require.Equal(t, "RegisterResourceQuota: invalid resource quota: NamespaceUID is missing for ResourceQuota 'uid'", kms.GetErrors()[0].Message)
+			require.Len(t, kms.ResourceQuotas, 0)
 		})
 		})
 
 
 		t.Run("register resource quota on KMS w/ namespace", func(t *testing.T) {
 		t.Run("register resource quota on KMS w/ namespace", func(t *testing.T) {
 			kms := NewKubeModelSet(start, end)
 			kms := NewKubeModelSet(start, end)
-			kms.RegisterCluster("cluster-uid")
-			kms.RegisterNamespace("namespace-uid", "namespace")
+			kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+			kms.RegisterNamespace(&Namespace{UID: "namespace-uid", Name: "namespace", Start: start, End: end})
 			// At this point we have a KMS with a cluster and namespace registered
 			// At this point we have a KMS with a cluster and namespace registered
 
 
 			testUID := "uid"
 			testUID := "uid"
 			testName := "name"
 			testName := "name"
-			testNamespace := "namespace" // Register RQ in namespace that was already registered
 
 
-			kms.RegisterResourceQuota(testUID, testName, testNamespace)
+			kms.RegisterResourceQuota(&ResourceQuota{UID: testUID, Name: testName, NamespaceUID: "namespace-uid", Start: start, End: end})
 
 
 			testRQ := &ResourceQuota{
 			testRQ := &ResourceQuota{
 				UID:          "uid",
 				UID:          "uid",
 				NamespaceUID: "namespace-uid",
 				NamespaceUID: "namespace-uid",
 				Name:         "name",
 				Name:         "name",
+				Start:        start,
+				End:          end,
 				Spec:         &ResourceQuotaSpec{Hard: &ResourceQuotaSpecHard{}},
 				Spec:         &ResourceQuotaSpec{Hard: &ResourceQuotaSpecHard{}},
 				Status:       &ResourceQuotaStatus{Used: &ResourceQuotaStatusUsed{}},
 				Status:       &ResourceQuotaStatus{Used: &ResourceQuotaStatusUsed{}},
 			}
 			}
@@ -225,7 +276,7 @@ func TestKubeModel(t *testing.T) {
 			require.Equal(t, 2, kms.Metadata.ObjectCount) // 1 namespace and 1 RQ
 			require.Equal(t, 2, kms.Metadata.ObjectCount) // 1 namespace and 1 RQ
 
 
 			// Register same RQ again, expect no-op on second try
 			// Register same RQ again, expect no-op on second try
-			kms.RegisterResourceQuota(testUID, testName, testNamespace)
+			kms.RegisterResourceQuota(&ResourceQuota{UID: testUID, Name: testName, NamespaceUID: "namespace-uid", Start: start, End: end})
 			require.Len(t, kms.GetErrors(), 0)
 			require.Len(t, kms.GetErrors(), 0)
 			require.NotNil(t, kms.ResourceQuotas[testUID])
 			require.NotNil(t, kms.ResourceQuotas[testUID])
 			require.Equal(t, testRQ, kms.ResourceQuotas[testUID])
 			require.Equal(t, testRQ, kms.ResourceQuotas[testUID])
@@ -234,12 +285,12 @@ func TestKubeModel(t *testing.T) {
 
 
 		t.Run("register multiple RQs in multiple namespaces", func(t *testing.T) {
 		t.Run("register multiple RQs in multiple namespaces", func(t *testing.T) {
 			kms := NewKubeModelSet(start, end)
 			kms := NewKubeModelSet(start, end)
-			kms.RegisterCluster("cluster-uid")
-			kms.RegisterNamespace("namespace-1-uid", "namespace-1")
-			kms.RegisterNamespace("namespace-2-uid", "namespace-2")
+			kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+			kms.RegisterNamespace(&Namespace{UID: "namespace-1-uid", Name: "namespace-1", Start: start, End: end})
+			kms.RegisterNamespace(&Namespace{UID: "namespace-2-uid", Name: "namespace-2", Start: start, End: end})
 
 
-			kms.RegisterResourceQuota("uid-1", "name-1", "namespace-1")
-			kms.RegisterResourceQuota("uid-2", "name-2", "namespace-2")
+			kms.RegisterResourceQuota(&ResourceQuota{UID: "uid-1", Name: "name-1", NamespaceUID: "namespace-1-uid", Start: start, End: end})
+			kms.RegisterResourceQuota(&ResourceQuota{UID: "uid-2", Name: "name-2", NamespaceUID: "namespace-2-uid", Start: start, End: end})
 
 
 			require.Len(t, kms.GetErrors(), 0)
 			require.Len(t, kms.GetErrors(), 0)
 			require.NotNil(t, kms.ResourceQuotas)
 			require.NotNil(t, kms.ResourceQuotas)
@@ -249,6 +300,8 @@ func TestKubeModel(t *testing.T) {
 				UID:          "uid-1",
 				UID:          "uid-1",
 				NamespaceUID: "namespace-1-uid",
 				NamespaceUID: "namespace-1-uid",
 				Name:         "name-1",
 				Name:         "name-1",
+				Start:        start,
+				End:          end,
 				Spec:         &ResourceQuotaSpec{Hard: &ResourceQuotaSpecHard{}},
 				Spec:         &ResourceQuotaSpec{Hard: &ResourceQuotaSpecHard{}},
 				Status:       &ResourceQuotaStatus{Used: &ResourceQuotaStatusUsed{}},
 				Status:       &ResourceQuotaStatus{Used: &ResourceQuotaStatusUsed{}},
 			}
 			}
@@ -256,6 +309,8 @@ func TestKubeModel(t *testing.T) {
 				UID:          "uid-2",
 				UID:          "uid-2",
 				NamespaceUID: "namespace-2-uid",
 				NamespaceUID: "namespace-2-uid",
 				Name:         "name-2",
 				Name:         "name-2",
+				Start:        start,
+				End:          end,
 				Spec:         &ResourceQuotaSpec{Hard: &ResourceQuotaSpecHard{}},
 				Spec:         &ResourceQuotaSpec{Hard: &ResourceQuotaSpecHard{}},
 				Status:       &ResourceQuotaStatus{Used: &ResourceQuotaStatusUsed{}},
 				Status:       &ResourceQuotaStatus{Used: &ResourceQuotaStatusUsed{}},
 			}
 			}
@@ -264,24 +319,13 @@ func TestKubeModel(t *testing.T) {
 			require.Equal(t, testRQ2, kms.ResourceQuotas["uid-2"])
 			require.Equal(t, testRQ2, kms.ResourceQuotas["uid-2"])
 			require.Equal(t, 4, kms.Metadata.ObjectCount) // 2 namespaces and 2 RQs
 			require.Equal(t, 4, kms.Metadata.ObjectCount) // 2 namespaces and 2 RQs
 
 
-			// Register a third RQ with an invalid namespace
-			kms.RegisterResourceQuota("uid-3", "name-3", "namespace-3")
-
-			require.Len(t, kms.GetWarnings(), 1)
-			require.Equal(t, "RegisterResourceQuota(uid-3, name-3, namespace-3): missing namespace", kms.GetWarnings()[0].Message)
-
-			testRQ3 := &ResourceQuota{
-				UID:          "uid-3",
-				NamespaceUID: "",
-				Name:         "name-3",
-				Spec:         &ResourceQuotaSpec{Hard: &ResourceQuotaSpecHard{}},
-				Status:       &ResourceQuotaStatus{Used: &ResourceQuotaStatusUsed{}},
-			}
-
-			require.Len(t, kms.ResourceQuotas, 3)
-			require.NotNil(t, kms.ResourceQuotas["uid-3"])
-			require.Equal(t, testRQ3, kms.ResourceQuotas["uid-3"])
-			require.Equal(t, 5, kms.Metadata.ObjectCount) // 2 namespaces and 3 RQs
+			// Register a third RQ with empty NamespaceUID — expect error, not registered
+			err := kms.RegisterResourceQuota(&ResourceQuota{UID: "uid-3", Name: "name-3", NamespaceUID: ""})
+			require.NotNil(t, err)
+			require.Len(t, kms.GetErrors(), 1)
+			require.Equal(t, "RegisterResourceQuota: invalid resource quota: NamespaceUID is missing for ResourceQuota 'uid-3'", kms.GetErrors()[0].Message)
+			require.Len(t, kms.ResourceQuotas, 2)         // still 2
+			require.Equal(t, 4, kms.Metadata.ObjectCount) // unchanged
 		})
 		})
 	})
 	})
 }
 }

+ 0 - 627
core/pkg/model/kubemodel/merge.go

@@ -1,627 +0,0 @@
-package kubemodel
-
-import (
-	"fmt"
-	"maps"
-	"math"
-	"slices"
-)
-
-func Merge(kms1, kms2 *KubeModelSet) (*KubeModelSet, error) {
-	if kms1 == nil && kms2 == nil {
-		return nil, fmt.Errorf("both KubeModelSets are nil")
-	}
-	if kms1 == nil {
-		return kms2, nil
-	}
-	if kms2 == nil {
-		return kms1, nil
-	}
-
-	if kms1.Cluster != nil && kms2.Cluster != nil && kms1.Cluster.UID != kms2.Cluster.UID {
-		return nil, fmt.Errorf(
-			"cannot merge KubeModelSets from different clusters: %s vs %s",
-			kms1.Cluster.UID, kms2.Cluster.UID)
-	}
-
-	windowStart := kms1.Window.Start
-	if kms2.Window.Start.Before(windowStart) {
-		windowStart = kms2.Window.Start
-	}
-	windowEnd := kms1.Window.End
-	if kms2.Window.End.After(windowEnd) {
-		windowEnd = kms2.Window.End
-	}
-
-	merged := NewKubeModelSet(windowStart, windowEnd)
-
-	if kms1.Metadata != nil && kms2.Metadata != nil {
-		if kms2.Metadata.CreatedAt.Before(kms1.Metadata.CreatedAt) {
-			merged.Metadata.CreatedAt = kms2.Metadata.CreatedAt
-		} else {
-			merged.Metadata.CreatedAt = kms1.Metadata.CreatedAt
-		}
-		if kms2.Metadata.CompletedAt.After(kms1.Metadata.CompletedAt) {
-			merged.Metadata.CompletedAt = kms2.Metadata.CompletedAt
-		} else {
-			merged.Metadata.CompletedAt = kms1.Metadata.CompletedAt
-		}
-		merged.Metadata.ObjectCount = kms1.Metadata.ObjectCount + kms2.Metadata.ObjectCount
-		merged.Metadata.Diagnostics = append(
-			append([]Diagnostic{}, kms1.Metadata.Diagnostics...),
-			kms2.Metadata.Diagnostics...,
-		)
-	} else if kms1.Metadata != nil {
-		merged.Metadata.CreatedAt = kms1.Metadata.CreatedAt
-		merged.Metadata.CompletedAt = kms1.Metadata.CompletedAt
-		merged.Metadata.ObjectCount = kms1.Metadata.ObjectCount
-		merged.Metadata.Diagnostics = append([]Diagnostic{}, kms1.Metadata.Diagnostics...)
-	} else if kms2.Metadata != nil {
-		merged.Metadata.CreatedAt = kms2.Metadata.CreatedAt
-		merged.Metadata.CompletedAt = kms2.Metadata.CompletedAt
-		merged.Metadata.ObjectCount = kms2.Metadata.ObjectCount
-		merged.Metadata.Diagnostics = append([]Diagnostic{}, kms2.Metadata.Diagnostics...)
-	}
-
-	merged.Cluster = kms1.Cluster
-	if merged.Cluster == nil {
-		merged.Cluster = kms2.Cluster
-	}
-
-	mergeNamespaces(merged, kms1, kms2)
-	mergeResourceQuotas(merged, kms1, kms2)
-	mergeNodes(merged, kms1, kms2)
-	mergePods(merged, kms1, kms2)
-	mergeContainers(merged, kms1, kms2)
-	mergeOwners(merged, kms1, kms2)
-	mergeServices(merged, kms1, kms2)
-	mergeVolumes(merged, kms1, kms2)
-	mergePVCs(merged, kms1, kms2)
-	mergeDevices(merged, kms1, kms2)
-	mergeDeviceUsages(merged, kms1, kms2)
-
-	return merged, nil
-}
-
-func mergeNamespaces(merged, kms1, kms2 *KubeModelSet) {
-	for uid, ns := range kms1.Namespaces {
-		merged.Namespaces[uid] = copyNamespace(ns)
-		merged.idx.namespaceNameToID[ns.Name] = ns.UID
-		merged.Metadata.ObjectCount++
-	}
-	for uid, ns2 := range kms2.Namespaces {
-		if ns1, exists := merged.Namespaces[uid]; exists {
-			// Merge Start/End timestamps for existing namespace
-			if ns2.Start.Before(ns1.Start) {
-				ns1.Start = ns2.Start
-			}
-			if ns2.End.After(ns1.End) {
-				ns1.End = ns2.End
-			}
-		} else {
-			merged.Namespaces[uid] = copyNamespace(ns2)
-			merged.idx.namespaceNameToID[ns2.Name] = ns2.UID
-			merged.Metadata.ObjectCount++
-		}
-	}
-}
-
-func mergeResourceQuotas(merged, kms1, kms2 *KubeModelSet) {
-	for uid, rq := range kms1.ResourceQuotas {
-		merged.ResourceQuotas[uid] = copyResourceQuota(rq)
-		merged.Metadata.ObjectCount++
-	}
-	for uid, rq2 := range kms2.ResourceQuotas {
-		if rq1, exists := merged.ResourceQuotas[uid]; exists {
-			// Merge Start/End timestamps for existing resource quota
-			if rq2.Start.Before(rq1.Start) {
-				rq1.Start = rq2.Start
-			}
-			if rq2.End.After(rq1.End) {
-				rq1.End = rq2.End
-			}
-		} else {
-			merged.ResourceQuotas[uid] = copyResourceQuota(rq2)
-			merged.Metadata.ObjectCount++
-		}
-	}
-}
-
-func mergeNodes(merged, kms1, kms2 *KubeModelSet) {
-	for uid, node := range kms1.Nodes {
-		merged.Nodes[uid] = copyNode(node)
-		merged.Metadata.ObjectCount++
-	}
-	for uid, node2 := range kms2.Nodes {
-		if node1, exists := merged.Nodes[uid]; exists {
-			node1.CpuMillicoreSeconds += node2.CpuMillicoreSeconds
-			node1.RAMByteSeconds += node2.RAMByteSeconds
-			node1.CpuMillicoreUsageMax = max(node1.CpuMillicoreUsageMax, node2.CpuMillicoreUsageMax)
-			node1.RAMByteUsageMax = max(node1.RAMByteUsageMax, node2.RAMByteUsageMax)
-			node1.DurationSeconds += node2.DurationSeconds
-
-			if node2.Start.Before(node1.Start) {
-				node1.Start = node2.Start
-			}
-			if node2.End.After(node1.End) {
-				node1.End = node2.End
-			}
-
-			for volumeUID, volume2 := range node2.AttachedVolumes {
-				if volume1, exists := node1.AttachedVolumes[volumeUID]; exists {
-					volume1.UsageByteSeconds += volume2.UsageByteSeconds
-					volume1.DurationSeconds += volume2.DurationSeconds
-					if volume2.CapacityBytes > volume1.CapacityBytes {
-						volume1.CapacityBytes = volume2.CapacityBytes
-					}
-				} else {
-					node1.AttachedVolumes[volumeUID] = &NodeVolumeUsage{
-						VolumeUID:        volume2.VolumeUID,
-						CapacityBytes:    volume2.CapacityBytes,
-						UsageByteSeconds: volume2.UsageByteSeconds,
-						VolumeType:       volume2.VolumeType,
-						ProviderID:       volume2.ProviderID,
-						DurationSeconds:  volume2.DurationSeconds,
-					}
-				}
-			}
-		} else {
-			merged.Nodes[uid] = copyNode(node2)
-			merged.Metadata.ObjectCount++
-		}
-	}
-}
-
-func mergePods(merged, kms1, kms2 *KubeModelSet) {
-	for uid, pod := range kms1.Pods {
-		merged.Pods[uid] = copyPod(pod)
-		merged.Metadata.ObjectCount++
-	}
-	for uid, pod2 := range kms2.Pods {
-		if pod1, exists := merged.Pods[uid]; exists {
-			pod1.NetworkReceiveBytes += pod2.NetworkReceiveBytes
-			pod1.NetworkTransferBytes += pod2.NetworkTransferBytes
-			pod1.DurationSeconds += pod2.DurationSeconds
-
-			if pod2.Start.Before(pod1.Start) {
-				pod1.Start = pod2.Start
-			}
-			if pod2.End.After(pod1.End) {
-				pod1.End = pod2.End
-			}
-		} else {
-			merged.Pods[uid] = copyPod(pod2)
-			merged.Metadata.ObjectCount++
-		}
-	}
-}
-
-func mergeContainers(merged, kms1, kms2 *KubeModelSet) {
-	for uid, container := range kms1.Containers {
-		merged.Containers[uid] = copyContainer(container)
-		merged.Metadata.ObjectCount++
-	}
-	for uid, container2 := range kms2.Containers {
-		if container1, exists := merged.Containers[uid]; exists {
-			container1.CpuMillicoreSeconds += container2.CpuMillicoreSeconds
-			container1.RAMByteSeconds += container2.RAMByteSeconds
-			container1.CpuMillicoreUsageMax = max(container1.CpuMillicoreUsageMax, container2.CpuMillicoreUsageMax)
-			container1.RAMByteUsageMax = max(container1.RAMByteUsageMax, container2.RAMByteUsageMax)
-
-			for volumeUID, ByteSeconds := range container2.VolumeStorageByteSeconds {
-				container1.VolumeStorageByteSeconds[volumeUID] += ByteSeconds
-			}
-			for volumeUID, usageMax := range container2.VolumeStorageByteUsageMax {
-				if currentMax, exists := container1.VolumeStorageByteUsageMax[volumeUID]; exists {
-					container1.VolumeStorageByteUsageMax[volumeUID] = max(currentMax, usageMax)
-				} else {
-					container1.VolumeStorageByteUsageMax[volumeUID] = usageMax
-				}
-			}
-
-			container1.CpuMillicoreRequestSeconds += container2.CpuMillicoreRequestSeconds
-			container1.RAMByteSecondRequest += container2.RAMByteSecondRequest
-			container1.CpuMillicoreLimitSeconds += container2.CpuMillicoreLimitSeconds
-			container1.RAMByteSecondsLimit += container2.RAMByteSecondsLimit
-
-			container1.DurationSeconds += container2.DurationSeconds
-
-			// Merge Start/End timestamps
-			if container2.Start.Before(container1.Start) {
-				container1.Start = container2.Start
-			}
-			if container2.End.After(container1.End) {
-				container1.End = container2.End
-			}
-		} else {
-			merged.Containers[uid] = copyContainer(container2)
-			merged.Metadata.ObjectCount++
-		}
-	}
-}
-
-func mergeOwners(merged, kms1, kms2 *KubeModelSet) {
-	for uid, owner := range kms1.Owners {
-		merged.Owners[uid] = copyOwner(owner)
-		merged.Metadata.ObjectCount++
-	}
-	for uid, owner2 := range kms2.Owners {
-		if owner1, exists := merged.Owners[uid]; exists {
-			if owner2.Start.Before(owner1.Start) {
-				owner1.Start = owner2.Start
-			}
-			if owner2.End.After(owner1.End) {
-				owner1.End = owner2.End
-			}
-		} else {
-			merged.Owners[uid] = copyOwner(owner2)
-			merged.Metadata.ObjectCount++
-		}
-	}
-}
-
-func mergeServices(merged, kms1, kms2 *KubeModelSet) {
-	for uid, svc := range kms1.Services {
-		merged.Services[uid] = copyService(svc)
-		merged.Metadata.ObjectCount++
-	}
-	for uid, svc2 := range kms2.Services {
-		if svc1, exists := merged.Services[uid]; exists {
-			svc1.NetworkTransferBytes += svc2.NetworkTransferBytes
-			svc1.NetworkReceiveBytes += svc2.NetworkReceiveBytes
-			svc1.DurationSeconds += svc2.DurationSeconds
-
-			if svc2.Start.Before(svc1.Start) {
-				svc1.Start = svc2.Start
-			}
-			if svc2.End.After(svc1.End) {
-				svc1.End = svc2.End
-			}
-		} else {
-			merged.Services[uid] = copyService(svc2)
-			merged.Metadata.ObjectCount++
-		}
-	}
-}
-
-func mergeVolumes(merged, kms1, kms2 *KubeModelSet) {
-	for uid, vol := range kms1.Volumes {
-		merged.Volumes[uid] = copyVolume(vol)
-		merged.Metadata.ObjectCount++
-	}
-	for uid, vol2 := range kms2.Volumes {
-		if vol1, exists := merged.Volumes[uid]; exists {
-			if vol2.Start.Before(vol1.Start) {
-				vol1.Start = vol2.Start
-			}
-			if vol2.End.After(vol1.End) {
-				vol1.End = vol2.End
-			}
-			vol1.DurationSeconds += vol2.DurationSeconds
-		} else {
-			merged.Volumes[uid] = copyVolume(vol2)
-			merged.Metadata.ObjectCount++
-		}
-	}
-}
-
-func mergePVCs(merged, kms1, kms2 *KubeModelSet) {
-	for uid, pvc := range kms1.PersistentVolumeClaims {
-		merged.PersistentVolumeClaims[uid] = copyPVC(pvc)
-		merged.Metadata.ObjectCount++
-	}
-	for uid, pvc2 := range kms2.PersistentVolumeClaims {
-		if pvc1, exists := merged.PersistentVolumeClaims[uid]; exists {
-			pvc1.StorageByteSeconds += pvc2.StorageByteSeconds
-			pvc1.ActualUsedByteSeconds += pvc2.ActualUsedByteSeconds
-			pvc1.DurationSeconds += pvc2.DurationSeconds
-
-			if pvc2.Start.Before(pvc1.Start) {
-				pvc1.Start = pvc2.Start
-			}
-			if pvc2.End.After(pvc1.End) {
-				pvc1.End = pvc2.End
-			}
-			if pvc2.BoundAt.After(pvc1.BoundAt) {
-				pvc1.BoundAt = pvc2.BoundAt
-			}
-		} else {
-			merged.PersistentVolumeClaims[uid] = copyPVC(pvc2)
-			merged.Metadata.ObjectCount++
-		}
-	}
-}
-
-func mergeDevices(merged, kms1, kms2 *KubeModelSet) {
-	for uid, dev := range kms1.Devices {
-		merged.Devices[uid] = copyDevice(dev)
-		merged.Metadata.ObjectCount++
-	}
-	for uid, dev2 := range kms2.Devices {
-		if dev1, exists := merged.Devices[uid]; exists {
-			dev1.UsageSeconds += dev2.UsageSeconds
-			dev1.MemoryByteSeconds += dev2.MemoryByteSeconds
-			dev1.PowerWattSeconds += dev2.PowerWattSeconds
-			dev1.PowerWattMax = math.Max(dev1.PowerWattMax, dev2.PowerWattMax)
-			dev1.DurationSeconds += dev2.DurationSeconds
-
-			if dev2.Start.Before(dev1.Start) {
-				dev1.Start = dev2.Start
-			}
-			if dev2.End.After(dev1.End) {
-				dev1.End = dev2.End
-			}
-		} else {
-			merged.Devices[uid] = copyDevice(dev2)
-			merged.Metadata.ObjectCount++
-		}
-	}
-}
-
-func mergeDeviceUsages(merged, kms1, kms2 *KubeModelSet) {
-	for uid, usage := range kms1.DeviceUsages {
-		merged.DeviceUsages[uid] = copyDeviceUsage(usage)
-		merged.Metadata.ObjectCount++
-	}
-	for uid, usage2 := range kms2.DeviceUsages {
-		if usage1, exists := merged.DeviceUsages[uid]; exists {
-			usage1.UsageSeconds += usage2.UsageSeconds
-			usage1.MemoryByteSecondsUsed += usage2.MemoryByteSecondsUsed
-			usage1.UsagePercentageMax = math.Max(usage1.UsagePercentageMax, usage2.UsagePercentageMax)
-			usage1.DurationSeconds += usage2.DurationSeconds
-
-			// Merge Start/End timestamps
-			if usage2.Start.Before(usage1.Start) {
-				usage1.Start = usage2.Start
-			}
-			if usage2.End.After(usage1.End) {
-				usage1.End = usage2.End
-			}
-		} else {
-			merged.DeviceUsages[uid] = copyDeviceUsage(usage2)
-			merged.Metadata.ObjectCount++
-		}
-	}
-}
-
-func copyNamespace(ns *Namespace) *Namespace {
-	return &Namespace{
-		ClusterUID:  ns.ClusterUID,
-		UID:         ns.UID,
-		Name:        ns.Name,
-		Labels:      maps.Clone(ns.Labels),
-		Annotations: maps.Clone(ns.Annotations),
-		Start:       ns.Start,
-		End:         ns.End,
-	}
-}
-
-func copyResourceQuota(rq *ResourceQuota) *ResourceQuota {
-	copied := &ResourceQuota{
-		UID:          rq.UID,
-		Name:         rq.Name,
-		NamespaceUID: rq.NamespaceUID,
-		Start:        rq.Start,
-		End:          rq.End,
-	}
-	if rq.Spec != nil {
-		copied.Spec = &ResourceQuotaSpec{}
-		if rq.Spec.Hard != nil {
-			copied.Spec.Hard = &ResourceQuotaSpecHard{
-				Requests: copyResourceQuantities(rq.Spec.Hard.Requests),
-				Limits:   copyResourceQuantities(rq.Spec.Hard.Limits),
-			}
-		}
-	}
-	if rq.Status != nil {
-		copied.Status = &ResourceQuotaStatus{}
-		if rq.Status.Used != nil {
-			copied.Status.Used = &ResourceQuotaStatusUsed{
-				Requests: copyResourceQuantities(rq.Status.Used.Requests),
-				Limits:   copyResourceQuantities(rq.Status.Used.Limits),
-			}
-		}
-	}
-	return copied
-}
-
-func copyResourceQuantities(rq ResourceQuantities) ResourceQuantities {
-	if rq == nil {
-		return nil
-	}
-	copied := make(ResourceQuantities, len(rq))
-	for k, v := range rq {
-		copied[k] = v
-	}
-	return copied
-}
-
-func copyNode(node *Node) *Node {
-	copied := &Node{
-		UID:                  node.UID,
-		Name:                 node.Name,
-		ProviderResourceUID:  node.ProviderResourceUID,
-		Labels:               maps.Clone(node.Labels),
-		Annotations:          maps.Clone(node.Annotations),
-		CpuMillicoreSeconds:  node.CpuMillicoreSeconds,
-		RAMByteSeconds:       node.RAMByteSeconds,
-		CpuMillicoreUsageMax: node.CpuMillicoreUsageMax,
-		RAMByteUsageMax:      node.RAMByteUsageMax,
-		DurationSeconds:      node.DurationSeconds,
-		AttachedVolumes:      make(map[string]*NodeVolumeUsage),
-		Start:                node.Start,
-		End:                  node.End,
-	}
-
-	for volumeUID, volume := range node.AttachedVolumes {
-		copied.AttachedVolumes[volumeUID] = &NodeVolumeUsage{
-			VolumeUID:        volume.VolumeUID,
-			CapacityBytes:    volume.CapacityBytes,
-			UsageByteSeconds: volume.UsageByteSeconds,
-			VolumeType:       volume.VolumeType,
-			ProviderID:       volume.ProviderID,
-			DurationSeconds:  volume.DurationSeconds,
-		}
-	}
-
-	return copied
-}
-
-func copyPod(pod *Pod) *Pod {
-	return &Pod{
-		UID:                  pod.UID,
-		Name:                 pod.Name,
-		NamespaceUID:         pod.NamespaceUID,
-		OwnerUID:             pod.OwnerUID,
-		NodeUID:              pod.NodeUID,
-		Labels:               maps.Clone(pod.Labels),
-		Annotations:          maps.Clone(pod.Annotations),
-		NetworkReceiveBytes:  pod.NetworkReceiveBytes,
-		NetworkTransferBytes: pod.NetworkTransferBytes,
-		DurationSeconds:      pod.DurationSeconds,
-		Start:                pod.Start,
-		End:                  pod.End,
-	}
-}
-
-func copyContainer(container *Container) *Container {
-	return &Container{
-		PodUID:                     container.PodUID,
-		Name:                       container.Name,
-		CpuMillicoreSeconds:        container.CpuMillicoreSeconds,
-		RAMByteSeconds:             container.RAMByteSeconds,
-		CpuMillicoreUsageMax:       container.CpuMillicoreUsageMax,
-		RAMByteUsageMax:            container.RAMByteUsageMax,
-		VolumeStorageByteSeconds:   maps.Clone(container.VolumeStorageByteSeconds),
-		VolumeStorageByteUsageMax:  maps.Clone(container.VolumeStorageByteUsageMax),
-		DurationSeconds:            container.DurationSeconds,
-		CpuMillicoreRequestSeconds: container.CpuMillicoreRequestSeconds,
-		RAMByteSecondRequest:       container.RAMByteSecondRequest,
-		CpuMillicoreLimitSeconds:   container.CpuMillicoreLimitSeconds,
-		RAMByteSecondsLimit:        container.RAMByteSecondsLimit,
-		Start:                      container.Start,
-		End:                        container.End,
-	}
-}
-
-func copyOwner(owner *Owner) *Owner {
-	return &Owner{
-		UID:          owner.UID,
-		Name:         owner.Name,
-		NamespaceUID: owner.NamespaceUID,
-		Kind:         owner.Kind,
-		Labels:       maps.Clone(owner.Labels),
-		Annotations:  maps.Clone(owner.Annotations),
-		Start:        owner.Start,
-		End:          owner.End,
-	}
-}
-
-func copyService(svc *Service) *Service {
-	return &Service{
-		UID:                  svc.UID,
-		NamespaceUID:         svc.NamespaceUID,
-		Name:                 svc.Name,
-		Type:                 svc.Type,
-		Hostname:             svc.Hostname,
-		Labels:               maps.Clone(svc.Labels),
-		Annotations:          maps.Clone(svc.Annotations),
-		NetworkTransferBytes: svc.NetworkTransferBytes,
-		NetworkReceiveBytes:  svc.NetworkReceiveBytes,
-		DurationSeconds:      svc.DurationSeconds,
-		Selector:             maps.Clone(svc.Selector),
-		Ports:                slices.Clone(svc.Ports),
-		Start:                svc.Start,
-		End:                  svc.End,
-	}
-}
-
-func copyVolume(vol *PersistentVolume) *PersistentVolume {
-	return &PersistentVolume{
-		UID:                   vol.UID,
-		ClusterUID:            vol.ClusterUID,
-		Name:                  vol.Name,
-		Namespace:             vol.Namespace,
-		Labels:                maps.Clone(vol.Labels),
-		Annotations:           maps.Clone(vol.Annotations),
-		StorageClass:          vol.StorageClass,
-		SizeBytes:             vol.SizeBytes,
-		Type:                  vol.Type,
-		CSIDriver:             vol.CSIDriver,
-		ProviderVolumeID:      vol.ProviderVolumeID,
-		AccessModes:           slices.Clone(vol.AccessModes),
-		ReclaimPolicy:         vol.ReclaimPolicy,
-		Region:                vol.Region,
-		Zone:                  vol.Zone,
-		Start:                 vol.Start,
-		End:                   vol.End,
-		DurationSeconds:       vol.DurationSeconds,
-		NodeAffinity:          vol.NodeAffinity,
-		ProvisionedIOPS:       vol.ProvisionedIOPS,
-		ProvisionedThroughput: vol.ProvisionedThroughput,
-		PerformanceMode:       vol.PerformanceMode,
-	}
-}
-
-func copyPVC(pvc *PersistentVolumeClaim) *PersistentVolumeClaim {
-	copied := &PersistentVolumeClaim{
-		UID:                   pvc.UID,
-		NamespaceUID:          pvc.NamespaceUID,
-		Name:                  pvc.Name,
-		Labels:                maps.Clone(pvc.Labels),
-		Annotations:           maps.Clone(pvc.Annotations),
-		StorageClass:          pvc.StorageClass,
-		StorageByteSeconds:    pvc.StorageByteSeconds,
-		RequestedBytes:        pvc.RequestedBytes,
-		Size:                  pvc.Size,
-		VolumeName:            pvc.VolumeName,
-		AccessModes:           slices.Clone(pvc.AccessModes),
-		Start:                 pvc.Start,
-		End:                   pvc.End,
-		BoundAt:               pvc.BoundAt,
-		DurationSeconds:       pvc.DurationSeconds,
-		ActualUsedByteSeconds: pvc.ActualUsedByteSeconds,
-	}
-	if pvc.VolumeUID != nil {
-		volumeUID := *pvc.VolumeUID
-		copied.VolumeUID = &volumeUID
-	}
-	if pvc.PodUID != nil {
-		podUID := *pvc.PodUID
-		copied.PodUID = &podUID
-	}
-	return copied
-}
-
-func copyDevice(dev *Device) *Device {
-	return &Device{
-		UID:               dev.UID,
-		Type:              dev.Type,
-		NodeUID:           dev.NodeUID,
-		DeviceNumber:      dev.DeviceNumber,
-		ModelName:         dev.ModelName,
-		IsShared:          dev.IsShared,
-		SharePercentage:   dev.SharePercentage,
-		UsageSeconds:      dev.UsageSeconds,
-		MemoryByteSeconds: dev.MemoryByteSeconds,
-		PowerWattSeconds:  dev.PowerWattSeconds,
-		PowerWattMax:      dev.PowerWattMax,
-		DurationSeconds:   dev.DurationSeconds,
-		Start:             dev.Start,
-		End:               dev.End,
-	}
-}
-
-func copyDeviceUsage(usage *DeviceUsage) *DeviceUsage {
-	return &DeviceUsage{
-		ContainerUID:          usage.ContainerUID,
-		DeviceUID:             usage.DeviceUID,
-		UsageSeconds:          usage.UsageSeconds,
-		UsagePercentageMax:    usage.UsagePercentageMax,
-		MemoryByteSecondsUsed: usage.MemoryByteSecondsUsed,
-		DeviceType:            usage.DeviceType,
-		DurationSeconds:       usage.DurationSeconds,
-		Start:                 usage.Start,
-		End:                   usage.End,
-	}
-}

+ 266 - 0
core/pkg/model/kubemodel/mock.go

@@ -0,0 +1,266 @@
+package kubemodel
+
+import (
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/model/shared"
+)
+
+// NewMockKubeModelSet returns a KubeModelSet populated with one instance of every
+// type. All time values are derived from start/end so the caller controls
+// monotonic-clock safety (use Truncate before passing in).
+func NewMockKubeModelSet(start, end time.Time) *KubeModelSet {
+	kms := NewKubeModelSet(start, end)
+
+	// --- Cluster ---
+	kms.RegisterCluster(&Cluster{
+		UID:      "cluster-uid",
+		Provider: shared.ProviderAWS,
+		Account:  "123456789012",
+		Name:     "test-cluster",
+		Region:   "us-east-1",
+		Start:    start,
+		End:      end,
+	})
+
+	// --- Namespace ---
+	kms.RegisterNamespace(&Namespace{
+		UID:         "ns-uid",
+		Name:        "default",
+		Labels:      map[string]string{"env": "test"},
+		Annotations: map[string]string{"note": "mock"},
+		Start:       start,
+		End:         end,
+	})
+
+	// --- ResourceQuota ---
+	kms.RegisterResourceQuota(&ResourceQuota{
+		UID:          "rq-uid",
+		NamespaceUID: "ns-uid",
+		Name:         "default-quota",
+		Spec: &ResourceQuotaSpec{
+			Hard: &ResourceQuotaSpecHard{
+				Requests: ResourceQuantities{
+					ResourceCPU:    {Resource: ResourceCPU, Unit: UnitMillicore, Values: Stats{StatAvg: 4000, StatMax: 4000}},
+					ResourceMemory: {Resource: ResourceMemory, Unit: UnitByte, Values: Stats{StatAvg: 8e9, StatMax: 8e9}},
+				},
+				Limits: ResourceQuantities{
+					ResourceCPU:    {Resource: ResourceCPU, Unit: UnitMillicore, Values: Stats{StatAvg: 8000, StatMax: 8000}},
+					ResourceMemory: {Resource: ResourceMemory, Unit: UnitByte, Values: Stats{StatAvg: 16e9, StatMax: 16e9}},
+				},
+			},
+		},
+		Status: &ResourceQuotaStatus{
+			Used: &ResourceQuotaStatusUsed{
+				Requests: ResourceQuantities{
+					ResourceCPU:    {Resource: ResourceCPU, Unit: UnitMillicore, Values: Stats{StatAvg: 500, StatMax: 800}},
+					ResourceMemory: {Resource: ResourceMemory, Unit: UnitByte, Values: Stats{StatAvg: 1e9, StatMax: 2e9}},
+				},
+			},
+		},
+		Start: start,
+		End:   end,
+	})
+
+	// --- Node ---
+	kms.RegisterNode(&Node{
+		UID:        "node-uid",
+		ProviderID: "aws:///us-east-1a/i-0abc123def456",
+		Name:       "node-1",
+		Labels:     map[string]string{"node.kubernetes.io/instance-type": "m5.large"},
+		ResourceCapacities: ResourceQuantities{
+			ResourceCPU:    {Resource: ResourceCPU, Unit: UnitMillicore, Values: Stats{StatAvg: 2000, StatMax: 2000}},
+			ResourceMemory: {Resource: ResourceMemory, Unit: UnitByte, Values: Stats{StatAvg: 8e9, StatMax: 8e9}},
+		},
+		ResourcesAllocatable: ResourceQuantities{
+			ResourceCPU:    {Resource: ResourceCPU, Unit: UnitMillicore, Values: Stats{StatAvg: 1900, StatMax: 1900}},
+			ResourceMemory: {Resource: ResourceMemory, Unit: UnitByte, Values: Stats{StatAvg: 7e9, StatMax: 7e9}},
+		},
+		FileSystem: FileSystem{
+			CapacityBytes: 100e9,
+			UsageByteAvg:  40e9,
+			UsageByteMax:  55e9,
+		},
+		Start: start,
+		End:   end,
+	})
+
+	// --- Pod ---
+	kms.RegisterPod(&Pod{
+		UID:          "pod-uid",
+		NamespaceUID: "ns-uid",
+		NodeUID:      "node-uid",
+		Name:         "my-pod-abc12",
+		Owners:       []Owner{{UID: "dep-uid", Kind: OwnerKindDeployment, Controller: true}},
+		PVCVolumes:   []PodPVCVolume{{Name: "data", PersistentVolumeClaimUID: "pvc-uid"}},
+		Labels:       map[string]string{"app": "my-app", "version": "v1"},
+		Annotations:  map[string]string{"prometheus.io/scrape": "true"},
+		NetworkTrafficDetails: []NetworkTrafficDetail{
+			{
+				PodUID:           "pod-uid",
+				Endpoint:         "10.0.0.5:443",
+				TrafficDirection: TrafficDirectionEgress,
+				TrafficType:      TrafficTypeCrossZone,
+				IsNatGateway:     false,
+				Bytes:            1024,
+			},
+		},
+		Start: start,
+		End:   end,
+	})
+
+	// --- Container ---
+	kms.RegisterContainer(&Container{
+		PodUID: "pod-uid",
+		Name:   "app",
+		ResourceRequests: ResourceQuantities{
+			ResourceCPU:    {Resource: ResourceCPU, Unit: UnitMillicore, Values: Stats{StatAvg: 250, StatMax: 250}},
+			ResourceMemory: {Resource: ResourceMemory, Unit: UnitByte, Values: Stats{StatAvg: 512e6, StatMax: 512e6}},
+		},
+		ResourceLimits: ResourceQuantities{
+			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,
+	})
+
+	// --- Deployment ---
+	kms.RegisterDeployment(&Deployment{
+		UID:          "dep-uid",
+		NamespaceUID: "ns-uid",
+		Name:         "my-app",
+		Labels:       map[string]string{"app": "my-app"},
+		Annotations:  map[string]string{"deployment.kubernetes.io/revision": "3"},
+		MatchLabels:  map[string]string{"app": "my-app"},
+		Start:        start,
+		End:          end,
+	})
+
+	// --- StatefulSet ---
+	kms.RegisterStatefulSet(&StatefulSet{
+		UID:          "sts-uid",
+		NamespaceUID: "ns-uid",
+		Name:         "my-statefulset",
+		Labels:       map[string]string{"app": "my-statefulset"},
+		Annotations:  map[string]string{"note": "test"},
+		MatchLabels:  map[string]string{"app": "my-statefulset"},
+		Start:        start,
+		End:          end,
+	})
+
+	// --- DaemonSet ---
+	kms.RegisterDaemonSet(&DaemonSet{
+		UID:          "ds-uid",
+		NamespaceUID: "ns-uid",
+		Name:         "my-daemonset",
+		Labels:       map[string]string{"app": "my-daemonset"},
+		Annotations:  map[string]string{"note": "test"},
+		Start:        start,
+		End:          end,
+	})
+
+	// --- Job ---
+	kms.RegisterJob(&Job{
+		UID:          "job-uid",
+		NamespaceUID: "ns-uid",
+		Name:         "my-job",
+		Labels:       map[string]string{"app": "my-job"},
+		Annotations:  map[string]string{"note": "test"},
+		Start:        start,
+		End:          end,
+	})
+
+	// --- CronJob ---
+	kms.RegisterCronJob(&CronJob{
+		UID:          "cj-uid",
+		NamespaceUID: "ns-uid",
+		Name:         "my-cronjob",
+		Labels:       map[string]string{"app": "my-cronjob"},
+		Annotations:  map[string]string{"note": "test"},
+		Start:        start,
+		End:          end,
+	})
+
+	// --- ReplicaSet ---
+	kms.RegisterReplicaSet(&ReplicaSet{
+		UID:          "rs-uid",
+		NamespaceUID: "ns-uid",
+		Name:         "my-app-7d9f8b",
+		Owners:       []Owner{{UID: "dep-uid", Kind: OwnerKindDeployment, Controller: true}},
+		Labels:       map[string]string{"app": "my-app", "pod-template-hash": "7d9f8b"},
+		Annotations:  map[string]string{"note": "test"},
+		Start:        start,
+		End:          end,
+	})
+
+	// --- Service ---
+	kms.RegisterService(&Service{
+		UID:          "svc-uid",
+		NamespaceUID: "ns-uid",
+		Name:         "my-app",
+		Type:         ServiceTypeClusterIP,
+		Selector:     map[string]string{"app": "my-app"},
+		Start:        start,
+		End:          end,
+	})
+
+	// --- PersistentVolumeClaim ---
+	kms.RegisterPVC(&PersistentVolumeClaim{
+		UID:                 "pvc-uid",
+		NamespaceUID:        "ns-uid",
+		Name:                "data",
+		PersistentVolumeUID: "pv-uid",
+		StorageClass:        "gp2",
+		RequestedBytes:      50e9,
+		UsageBytesAvg:       20e9,
+		UsageBytesMax:       35e9,
+		Start:               start,
+		End:                 end,
+	})
+
+	// --- PersistentVolume ---
+	kms.RegisterPersistentVolume(&PersistentVolume{
+		UID:             "pv-uid",
+		Name:            "pvc-abc123",
+		StorageClass:    "gp2",
+		CSIVolumeHandle: "vol-0abc123def456789",
+		SizeBytes:       50e9,
+		Start:           start,
+		End:             end,
+	})
+
+	// --- DCGMDevice ---
+	kms.RegisterDCGMDevice(&DCGMDevice{
+		UUID:      "GPU-abc123def-456-789",
+		Device:    "0",
+		ModelName: "Tesla T4",
+		PodUsages: map[string]DCGMPod{
+			"pod-uid": {
+				ContainerUsages: map[string]DCGMContainer{
+					"app": {UsageAvg: 0.65, UsageMax: 0.92},
+				},
+			},
+		},
+		Start: start,
+		End:   end,
+	})
+
+	// --- Diagnostics ---
+	kms.Error(errMock("mock error"))
+	kms.Warn("mock warning")
+	kms.Info("mock info")
+
+	return kms
+}
+
+// errMock is a minimal error implementation that avoids importing errors/fmt in the mock.
+type errMock string
+
+func (e errMock) Error() string { return string(e) }

+ 32 - 22
core/pkg/model/kubemodel/namespace.go

@@ -7,38 +7,48 @@ import (
 
 
 // @bingen:generate:Namespace
 // @bingen:generate:Namespace
 type Namespace struct {
 type Namespace struct {
-	UID         string            `json:"uid"`             // @bingen:field[version=1]
-	ClusterUID  string            `json:"clusterUID"`      // @bingen:field[version=1]
-	Name        string            `json:"name"`            // @bingen:field[version=1]
-	Labels      map[string]string `json:"labels"`          // @bingen:field[version=1]
-	Annotations map[string]string `json:"annotations"`     // @bingen:field[version=1]
-	Start       time.Time         `json:"start,omitempty"` // @bingen:field[version=1]
-	End         time.Time         `json:"end,omitempty"`   // @bingen:field[version=1]
+	UID string `json:"uid"` // @bingen:field[version=1]
+	// This field was included in the initial bigen codec but was never populated. If you need to add a new string
+	// field to this structure rename this field and delete this comment
+	bingenPlaceHolder string            // @bingen:field[version=1]
+	Name              string            `json:"name"`        // @bingen:field[version=1]
+	Labels            map[string]string `json:"labels"`      // @bingen:field[version=1]
+	Annotations       map[string]string `json:"annotations"` // @bingen:field[version=1]
+	Start             time.Time         `json:"start"`       // @bingen:field[version=1]
+	End               time.Time         `json:"end"`         // @bingen:field[version=1]
 }
 }
 
 
-func (kms *KubeModelSet) RegisterNamespace(uid, name string) error {
-	if uid == "" {
-		err := fmt.Errorf("UID is nil for Namespace '%s'", name)
-		kms.Error(err)
+func (n *Namespace) ValidateNamespace(window Window) error {
+	if n.UID == "" {
+		return fmt.Errorf("UID is missing for Namespace with name '%s'", n.Name)
+	}
+
+	if n.Name == "" {
+		return fmt.Errorf("Name is missing for Namespace '%s'", n.UID)
+	}
+
+	if err := checkWindow(window, n.Start, n.End); err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	if _, ok := kms.Namespaces[uid]; !ok {
-		clusterUID := ""
+	return nil
+}
+
+func (kms *KubeModelSet) RegisterNamespace(namespace *Namespace) error {
+	if err := namespace.ValidateNamespace(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterNamespace: invalid namespace: %w", err)
+		kms.Error(err)
+		return err
+	}
 
 
+	if _, ok := kms.Namespaces[namespace.UID]; !ok {
 		if kms.Cluster == nil {
 		if kms.Cluster == nil {
-			kms.Warnf("RegisterNamespace(%s, %s): Cluster is nil", uid, name)
-		} else {
-			clusterUID = kms.Cluster.UID
+			kms.Warnf("RegisterNamespace: Cluster is nil")
 		}
 		}
 
 
-		kms.Namespaces[uid] = &Namespace{
-			UID:        uid,
-			ClusterUID: clusterUID,
-			Name:       name,
-		}
+		kms.Namespaces[namespace.UID] = namespace
 
 
-		kms.idx.namespaceByName[name] = kms.Namespaces[uid]
+		kms.idx.namespaceByName[namespace.Name] = namespace
 
 
 		kms.Metadata.ObjectCount++
 		kms.Metadata.ObjectCount++
 	}
 	}

+ 143 - 0
core/pkg/model/kubemodel/namespace_test.go

@@ -0,0 +1,143 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateNamespace(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name      string
+		namespace *Namespace
+		wantErr   string
+	}{
+		{
+			name:      "empty UID",
+			namespace: &Namespace{Name: "my-ns", Start: start, End: end},
+			wantErr:   "UID is missing for Namespace with name 'my-ns'",
+		},
+		{
+			name:      "empty Name",
+			namespace: &Namespace{UID: "ns-uid", Start: start, End: end},
+			wantErr:   "Name is missing for Namespace 'ns-uid'",
+		},
+		{
+			name:      "outside window",
+			namespace: &Namespace{UID: "ns-uid", Name: "my-ns", Start: start.Add(-time.Hour), End: end},
+			wantErr:   checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name:      "valid",
+			namespace: &Namespace{UID: "ns-uid", Name: "my-ns", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.namespace.ValidateNamespace(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterNamespace(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newNamespace := func(uid, name string) *Namespace {
+		return &Namespace{UID: uid, Name: name, Start: start, End: end}
+	}
+	withCluster := func(kms *KubeModelSet) {
+		kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+	}
+
+	tests := []struct {
+		name      string
+		setup     func(*KubeModelSet)
+		namespace *Namespace
+		wantErr   string
+		want      *KubeModelSet
+	}{
+		{
+			name:      "validation failure",
+			namespace: &Namespace{UID: "", Name: "my-ns", Start: start, End: end},
+			wantErr:   "RegisterNamespace: invalid namespace: UID is missing for Namespace with name 'my-ns'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterNamespace: invalid namespace: UID is missing for Namespace with name 'my-ns'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:      "warns when cluster is nil",
+			namespace: newNamespace("ns-uid", "my-ns"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Namespaces["ns-uid"] = newNamespace("ns-uid", "my-ns")
+				kms.Metadata.ObjectCount = 1
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelWarning, Message: "RegisterNamespace: Cluster is nil"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:      "registers namespace with cluster",
+			setup:     withCluster,
+			namespace: newNamespace("ns-uid", "my-ns"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.Namespaces["ns-uid"] = newNamespace("ns-uid", "my-ns")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				withCluster(kms)
+				kms.RegisterNamespace(newNamespace("ns-uid", "original"))
+			},
+			namespace: newNamespace("ns-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.Namespaces["ns-uid"] = newNamespace("ns-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterNamespace(tt.namespace)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 28 - 0
core/pkg/model/kubemodel/networktrafficdetail.go

@@ -0,0 +1,28 @@
+package kubemodel
+
+// @bingen:generate:TrafficDirection
+type TrafficDirection string
+
+const (
+	TrafficDirectionEgress  TrafficDirection = "Egress"
+	TrafficDirectionIngress TrafficDirection = "Ingress"
+)
+
+// @bingen:generate:TrafficType
+type TrafficType string
+
+const (
+	TrafficTypeCrossZone   TrafficType = "CrossZone"
+	TrafficTypeCrossRegion TrafficType = "CrossRegion"
+	TrafficTypeInternet    TrafficType = "Internet"
+)
+
+// @bingen:generate:NetworkTrafficDetail
+type NetworkTrafficDetail struct {
+	PodUID           string           `json:"podUid"`
+	Endpoint         string           `json:"endpoint,omitempty"`
+	TrafficDirection TrafficDirection `json:"trafficDirection"`
+	TrafficType      TrafficType      `json:"trafficType"`
+	IsNatGateway     bool             `json:"isNatGateway"`
+	Bytes            float64          `json:"bytes"`
+}

+ 31 - 70
core/pkg/model/kubemodel/node.go

@@ -5,99 +5,60 @@ import (
 	"time"
 	"time"
 )
 )
 
 
+// @bingen:generate:Node
 // Node represents a Kubernetes node with capacity-based resource tracking.
 // Node represents a Kubernetes node with capacity-based resource tracking.
 // All resource measures (CPU, RAM) represent node capacity, not requests or limits.
 // All resource measures (CPU, RAM) represent node capacity, not requests or limits.
 // This aligns with the principle that cost allocation should be based on provisioned capacity.
 // This aligns with the principle that cost allocation should be based on provisioned capacity.
 type Node struct {
 type Node struct {
-	UID                  string                      `json:"uid"`
-	ProviderResourceUID  string                      `json:"providerResourceUid"`
-	Name                 string                      `json:"name"`
-	Labels               map[string]string           `json:"labels,omitempty"`
-	Annotations          map[string]string           `json:"annotations,omitempty"`
-	DurationSeconds      Measurement                 `json:"durationSeconds"`
-	CpuMillicoreSeconds  Measurement                 `json:"cpuMillicoreSeconds"` // Node CPU capacity in millicore-seconds
-	RAMByteSeconds       Measurement                 `json:"ramByteSeconds"`      // Node RAM capacity in Byte-seconds
-	AttachedVolumes      map[string]*NodeVolumeUsage `json:"attachedVolumes,omitempty"`
-	CpuMillicoreUsageMax Measurement                 `json:"cpuMillicoreUsageMax"` // Peak CPU usage observed
-	RAMByteUsageMax      Measurement                 `json:"ramByteUsageMax"`      // Peak RAM usage observed
-	Start                time.Time                   `json:"start,omitempty"`      // Node creation/start timestamp
-	End                  time.Time                   `json:"end,omitempty"`        // Node deletion/end timestamp (nil if still running)
+	UID                  string             `json:"uid"`
+	ProviderID           string             `json:"providerId"`
+	Name                 string             `json:"name"`
+	Labels               map[string]string  `json:"labels"`
+	ResourceCapacities   ResourceQuantities `json:"resourceCapacities"`
+	ResourcesAllocatable ResourceQuantities `json:"resourcesAllocatable"`
+	FileSystem           FileSystem         `json:"fileSystem"`
+	Start                time.Time          `json:"start"`
+	End                  time.Time          `json:"end"`
 }
 }
 
 
-// NodeVolumeUsage tracks storage usage for a disk volume attached to a node.
-// Used for cost allocation of cloud storage resources (e.g., AWS EBS volumes).
-type NodeVolumeUsage struct {
-	VolumeUID        string      `json:"volumeUid"`        // "root" for primary disk, or actual volume UID for additional volumes
-	CapacityBytes    Measurement `json:"capacityBytes"`    // Total capacity of the volume in bytes
-	UsageByteSeconds Measurement `json:"usageByteSeconds"` // Cumulative usage (Byte × seconds) over measurement window
-	VolumeType       string      `json:"volumeType"`       // "root" for primary disk, "persistent" for additional PVs
-	ProviderID       string      `json:"providerId"`       // Cloud provider volume ID (e.g., "vol-xxxxx" for AWS EBS)
-	DurationSeconds  Measurement `json:"durationSeconds"`  // Duration the volume was attached during measurement window in seconds
+// @bingen:generate:FileSystem
+// FileSystem records information for a nodes local storage
+type FileSystem struct {
+	CapacityBytes float64 `json:"capacityBytes"` // Total capacity of the volume in bytes
+	UsageByteAvg  float64 `json:"usageByteAvg"`
+	UsageByteMax  float64 `json:"usageByteMax"`
 }
 }
 
 
-// CpuMillicoreUsageAverage calculates the average CPU usage in millicores over the uptime period.
-// Returns 0 if uptime is 0 to avoid division by zero.
-func (n *Node) CpuMillicoreUsageAverage() Measurement {
-	if n.DurationSeconds == 0 {
-		return 0
+func (n *Node) ValidateNode(window Window) error {
+	if n.UID == "" {
+		return fmt.Errorf("UID is missing for Node with name '%s'", n.Name)
 	}
 	}
-	return n.CpuMillicoreSeconds / n.DurationSeconds
-}
 
 
-// RAMByteUsageAverage calculates the average RAM usage in bytes over the uptime period.
-// Returns 0 if uptime is 0 to avoid division by zero.
-func (n *Node) RAMByteUsageAverage() Measurement {
-	if n.DurationSeconds == 0 {
-		return 0
+	if n.Name == "" {
+		return fmt.Errorf("Name is missing for Node '%s'", n.UID)
 	}
 	}
-	return n.RAMByteSeconds / n.DurationSeconds
-}
-
-// TotalVolumeUsageByteSeconds returns the sum of all volume usage Byte-seconds across all attached volumes.
-func (n *Node) TotalVolumeUsageByteSeconds() Measurement {
-	var total Measurement
-	for _, volume := range n.AttachedVolumes {
-		total += volume.UsageByteSeconds
-	}
-	return total
-}
 
 
-// TotalVolumeCapacityBytes returns the sum of all volume capacities across all attached volumes.
-func (n *Node) TotalVolumeCapacityBytes() Measurement {
-	var total Measurement
-	for _, volume := range n.AttachedVolumes {
-		total += volume.CapacityBytes
+	if err := checkWindow(window, n.Start, n.End); err != nil {
+		return err
 	}
 	}
-	return total
-}
 
 
-// GetVolumeUsageAverage calculates the average storage usage in bytes for a specific volume over the uptime period.
-// Returns 0 if uptime is 0 or volume doesn't exist.
-func (n *Node) GetVolumeUsageAverage(volumeUID string) Measurement {
-	volume, exists := n.AttachedVolumes[volumeUID]
-	if !exists || n.DurationSeconds == 0 {
-		return 0
-	}
-	return volume.UsageByteSeconds / n.DurationSeconds
+	return nil
 }
 }
 
 
-func (kms *KubeModelSet) RegisterNode(uid, name string) error {
-	if uid == "" {
-		err := fmt.Errorf("UID is nil for Node '%s'", name)
+// RegisterNode validates and adds a node to the set
+func (kms *KubeModelSet) RegisterNode(node *Node) error {
+	if err := node.ValidateNode(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterNode: invalid node: %w", err)
 		kms.Error(err)
 		kms.Error(err)
 		return err
 		return err
 	}
 	}
 
 
-	if _, ok := kms.Nodes[uid]; !ok {
+	if _, ok := kms.Nodes[node.UID]; !ok {
 		if kms.Cluster == nil {
 		if kms.Cluster == nil {
-			kms.Warnf("RegisterNode(%s, %s): Cluster is nil", uid, name)
+			kms.Warnf("RegisterNode: Cluster is nil")
 		}
 		}
 
 
-		kms.Nodes[uid] = &Node{
-			UID:             uid,
-			Name:            name,
-			AttachedVolumes: make(map[string]*NodeVolumeUsage),
-		}
+		kms.Nodes[node.UID] = node
 
 
 		kms.Metadata.ObjectCount++
 		kms.Metadata.ObjectCount++
 	}
 	}

+ 143 - 0
core/pkg/model/kubemodel/node_test.go

@@ -0,0 +1,143 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateNode(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name    string
+		node    *Node
+		wantErr string
+	}{
+		{
+			name:    "empty UID",
+			node:    &Node{Name: "my-node", Start: start, End: end},
+			wantErr: "UID is missing for Node with name 'my-node'",
+		},
+		{
+			name:    "empty Name",
+			node:    &Node{UID: "node-uid", Start: start, End: end},
+			wantErr: "Name is missing for Node 'node-uid'",
+		},
+		{
+			name:    "outside window",
+			node:    &Node{UID: "node-uid", Name: "my-node", Start: start.Add(-time.Hour), End: end},
+			wantErr: checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name: "valid",
+			node: &Node{UID: "node-uid", Name: "my-node", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.node.ValidateNode(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterNode(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newNode := func(uid, name string) *Node {
+		return &Node{UID: uid, Name: name, Start: start, End: end}
+	}
+	withCluster := func(kms *KubeModelSet) {
+		kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+	}
+
+	tests := []struct {
+		name    string
+		setup   func(*KubeModelSet)
+		node    *Node
+		wantErr string
+		want    *KubeModelSet
+	}{
+		{
+			name:    "validation failure",
+			node:    &Node{UID: "", Name: "my-node", Start: start, End: end},
+			wantErr: "RegisterNode: invalid node: UID is missing for Node with name 'my-node'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterNode: invalid node: UID is missing for Node with name 'my-node'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name: "warns when cluster is nil",
+			node: newNode("node-uid", "my-node"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Nodes["node-uid"] = newNode("node-uid", "my-node")
+				kms.Metadata.ObjectCount = 1
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelWarning, Message: "RegisterNode: Cluster is nil"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:  "registers node with cluster",
+			setup: withCluster,
+			node:  newNode("node-uid", "my-node"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.Nodes["node-uid"] = newNode("node-uid", "my-node")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				withCluster(kms)
+				kms.RegisterNode(newNode("node-uid", "original"))
+			},
+			node: newNode("node-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.Nodes["node-uid"] = newNode("node-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterNode(tt.node)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 28 - 39
core/pkg/model/kubemodel/owner.go

@@ -1,10 +1,12 @@
 package kubemodel
 package kubemodel
 
 
 import (
 import (
-	"fmt"
-	"time"
+	"strings"
+
+	"github.com/opencost/opencost/core/pkg/log"
 )
 )
 
 
+// @bingen:generate:OwnerKind
 type OwnerKind string
 type OwnerKind string
 
 
 const (
 const (
@@ -16,43 +18,30 @@ const (
 	OwnerKindReplicaSet  OwnerKind = "replicaset"
 	OwnerKindReplicaSet  OwnerKind = "replicaset"
 )
 )
 
 
-// Owner represents a Kubernetes resource owner (workload controller)
-type Owner struct {
-	UID          string            `json:"uid"`
-	NamespaceUID string            `json:"namespaceUid"`
-	Name         string            `json:"name"`
-	Kind         OwnerKind         `json:"kind"`
-	Labels       map[string]string `json:"labels,omitempty"`
-	Annotations  map[string]string `json:"annotations,omitempty"`
-	Start        time.Time         `json:"start,omitempty"`
-	End          time.Time         `json:"end,omitempty"`
-}
-
-func (kms *KubeModelSet) RegisterOwner(uid, name, namespace, kind string) error {
-	if uid == "" {
-		err := fmt.Errorf("UID is nil for Owner '%s'", name)
-		kms.Error(err)
-		return err
-	}
-
-	if _, ok := kms.Owners[uid]; !ok {
-		namespaceUID := ""
-
-		if ns, ok := kms.idx.namespaceByName[namespace]; !ok {
-			kms.Warnf("RegisterOwner(%s, %s, %s, %s): missing namespace '%s'", uid, name, namespace, kind, namespace)
-		} else {
-			namespaceUID = ns.UID
-		}
-
-		kms.Owners[uid] = &Owner{
-			UID:          uid,
-			Name:         name,
-			NamespaceUID: namespaceUID,
-			Kind:         OwnerKind(kind),
-		}
-
-		kms.Metadata.ObjectCount++
+func ParseOwnerKind(kind string) OwnerKind {
+	switch strings.ToLower(kind) {
+	case "deployment":
+		return OwnerKindDeployment
+	case "statefulset":
+		return OwnerKindStatefulSet
+	case "daemonset":
+		return OwnerKindDaemonSet
+	case "job":
+		return OwnerKindJob
+	case "cronjob":
+		return OwnerKindCronJob
+	case "replicaset":
+		return OwnerKindReplicaSet
+	default:
+		log.Warnf("failed to find owner kind for '%s'", kind)
+		return OwnerKind(strings.ToLower(kind))
 	}
 	}
+}
 
 
-	return nil
+// @bingen:generate:Owner
+// Owner represents a Kubernetes resource owner (workload controller)
+type Owner struct {
+	UID        string    `json:"uid"`
+	Controller bool      `json:"controller"`
+	Kind       OwnerKind `json:"kind"`
 }
 }

+ 41 - 0
core/pkg/model/kubemodel/owner_test.go

@@ -0,0 +1,41 @@
+package kubemodel
+
+import (
+	"testing"
+)
+
+func TestParseOwnerKind(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected OwnerKind
+	}{
+		// Exact canonical values
+		{"deployment", OwnerKindDeployment},
+		{"statefulset", OwnerKindStatefulSet},
+		{"daemonset", OwnerKindDaemonSet},
+		{"job", OwnerKindJob},
+		{"cronjob", OwnerKindCronJob},
+		{"replicaset", OwnerKindReplicaSet},
+		// Case-insensitive
+		{"Deployment", OwnerKindDeployment},
+		{"StatefulSet", OwnerKindStatefulSet},
+		{"DaemonSet", OwnerKindDaemonSet},
+		{"Job", OwnerKindJob},
+		{"CronJob", OwnerKindCronJob},
+		{"ReplicaSet", OwnerKindReplicaSet},
+		{"DEPLOYMENT", OwnerKindDeployment},
+		// Unknown input: lowercased passthrough (no mapping)
+		{"Rollout", OwnerKind("rollout")},
+		{"CustomController", OwnerKind("customcontroller")},
+		{"", OwnerKind("")},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.input, func(t *testing.T) {
+			got := ParseOwnerKind(tt.input)
+			if got != tt.expected {
+				t.Errorf("ParseOwnerKind(%q) = %q, want %q", tt.input, got, tt.expected)
+			}
+		})
+	}
+}

+ 45 - 27
core/pkg/model/kubemodel/pod.go

@@ -5,43 +5,61 @@ import (
 	"time"
 	"time"
 )
 )
 
 
+// @bingen:generate:PodPVCVolume
+type PodPVCVolume struct {
+	Name                     string `json:"name"`
+	PersistentVolumeClaimUID string `json:"persistentVolumeClaimUid"`
+}
+
+// @bingen:generate:Pod
 type Pod struct {
 type Pod struct {
-	UID                  string            `json:"uid"`
-	NamespaceUID         string            `json:"namespaceUid"`
-	OwnerUID             string            `json:"ownerUid"` // Reference to Owner (Deployment, StatefulSet, etc.)
-	NodeUID              string            `json:"nodeUid"`
-	Name                 string            `json:"name"`
-	Labels               map[string]string `json:"labels,omitempty"`
-	Annotations          map[string]string `json:"annotations,omitempty"`
-	DurationSeconds      Measurement       `json:"durationSeconds"`
-	NetworkTransferBytes Measurement       `json:"networkTransferBytes"`
-	NetworkReceiveBytes  Measurement       `json:"networkReceiveBytes"`
-	Start                time.Time         `json:"start,omitempty"` // Pod creation/start timestamp
-	End                  time.Time         `json:"end,omitempty"`   // Pod deletion/end timestamp (nil if still running)
+	UID                   string                 `json:"uid"`
+	NamespaceUID          string                 `json:"namespaceUid"`
+	NodeUID               string                 `json:"nodeUid"`
+	Name                  string                 `json:"name"`
+	Owners                []Owner                `json:"owners"`
+	PVCVolumes            []PodPVCVolume         `json:"pvcVolumes,omitempty"`
+	Labels                map[string]string      `json:"labels,omitempty"`
+	Annotations           map[string]string      `json:"annotations,omitempty"`
+	NetworkTrafficDetails []NetworkTrafficDetail `json:"networkTrafficDetails,omitempty"`
+	Start                 time.Time              `json:"start"`
+	End                   time.Time              `json:"end"`
 }
 }
 
 
-func (kms *KubeModelSet) RegisterPod(uid, name, namespace string) error {
-	if uid == "" {
-		err := fmt.Errorf("UID is nil for Pod '%s'", name)
-		kms.Error(err)
+func (p *Pod) ValidatePod(window Window) error {
+	if p.UID == "" {
+		return fmt.Errorf("UID is missing for Pod with name '%s'", p.Name)
+	}
+
+	if p.Name == "" {
+		return fmt.Errorf("Name is missing for Pod '%s'", p.UID)
+	}
+
+	if p.NamespaceUID == "" {
+		return fmt.Errorf("NamespaceUID is missing for Pod '%s'", p.UID)
+	}
+
+	if err := checkWindow(window, p.Start, p.End); err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	if _, ok := kms.Pods[uid]; !ok {
-		namespaceUID := ""
+	return nil
+}
 
 
-		if ns, ok := kms.idx.namespaceByName[namespace]; !ok {
-			kms.Warnf("RegisterPod(%s, %s, %s): missing namespace '%s'", uid, name, namespace, namespace)
-		} else {
-			namespaceUID = ns.UID
-		}
+func (kms *KubeModelSet) RegisterPod(pod *Pod) error {
+	if err := pod.ValidatePod(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterPod: invalid pod: %w", err)
+		kms.Error(err)
+		return err
+	}
 
 
-		kms.Pods[uid] = &Pod{
-			UID:          uid,
-			Name:         name,
-			NamespaceUID: namespaceUID,
+	if _, ok := kms.Pods[pod.UID]; !ok {
+		if kms.Cluster == nil {
+			kms.Warnf("RegisterPod: Cluster is nil")
 		}
 		}
 
 
+		kms.Pods[pod.UID] = pod
+
 		kms.Metadata.ObjectCount++
 		kms.Metadata.ObjectCount++
 	}
 	}
 
 

+ 148 - 0
core/pkg/model/kubemodel/pod_test.go

@@ -0,0 +1,148 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidatePod(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name    string
+		pod     *Pod
+		wantErr string
+	}{
+		{
+			name:    "empty UID",
+			pod:     &Pod{Name: "my-pod", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "UID is missing for Pod with name 'my-pod'",
+		},
+		{
+			name:    "empty Name",
+			pod:     &Pod{UID: "pod-uid", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "Name is missing for Pod 'pod-uid'",
+		},
+		{
+			name:    "empty NamespaceUID",
+			pod:     &Pod{UID: "pod-uid", Name: "my-pod", Start: start, End: end},
+			wantErr: "NamespaceUID is missing for Pod 'pod-uid'",
+		},
+		{
+			name:    "outside window",
+			pod:     &Pod{UID: "pod-uid", Name: "my-pod", NamespaceUID: "ns-uid", Start: start.Add(-time.Hour), End: end},
+			wantErr: checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name: "valid",
+			pod:  &Pod{UID: "pod-uid", Name: "my-pod", NamespaceUID: "ns-uid", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.pod.ValidatePod(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterPod(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newPod := func(uid, name string) *Pod {
+		return &Pod{UID: uid, Name: name, NamespaceUID: "ns-uid", Start: start, End: end}
+	}
+	withCluster := func(kms *KubeModelSet) {
+		kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+	}
+
+	tests := []struct {
+		name    string
+		setup   func(*KubeModelSet)
+		pod     *Pod
+		wantErr string
+		want    *KubeModelSet
+	}{
+		{
+			name:    "validation failure",
+			pod:     &Pod{UID: "", Name: "my-pod", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "RegisterPod: invalid pod: UID is missing for Pod with name 'my-pod'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterPod: invalid pod: UID is missing for Pod with name 'my-pod'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name: "warns when cluster is nil",
+			pod:  newPod("pod-uid", "my-pod"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Pods["pod-uid"] = newPod("pod-uid", "my-pod")
+				kms.Metadata.ObjectCount = 1
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelWarning, Message: "RegisterPod: Cluster is nil"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:  "registers pod with cluster",
+			setup: withCluster,
+			pod:   newPod("pod-uid", "my-pod"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.Pods["pod-uid"] = newPod("pod-uid", "my-pod")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				withCluster(kms)
+				kms.RegisterPod(newPod("pod-uid", "original"))
+			},
+			pod: newPod("pod-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.Pods["pod-uid"] = newPod("pod-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterPod(tt.pod)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 0 - 14
core/pkg/model/kubemodel/provider.go

@@ -1,14 +0,0 @@
-package kubemodel
-
-// @bingen:generate:Provider
-type Provider string
-
-const (
-	ProviderEmpty        Provider = ""
-	ProviderAWS          Provider = "AWS"
-	ProviderGCP          Provider = "GCP"
-	ProviderAzure        Provider = "Azure"
-	ProviderAlibaba      Provider = "Alibaba"
-	ProviderDigitalOcean Provider = "DigitalOcean"
-	ProviderOracle       Provider = "Oracle"
-)

+ 28 - 50
core/pkg/model/kubemodel/pv.go

@@ -5,64 +5,42 @@ import (
 	"time"
 	"time"
 )
 )
 
 
+// @bingen:generate:PersistentVolume
 type PersistentVolume struct {
 type PersistentVolume struct {
-	// Version 1 fields
-	UID          string            `json:"uid"`
-	ClusterUID   string            `json:"clusterUid"`
-	Name         string            `json:"name"`
-	Namespace    string            `json:"namespace"`
-	Labels       map[string]string `json:"labels,omitempty"`
-	Annotations  map[string]string `json:"annotations,omitempty"`
-	StorageClass string            `json:"storageClass"`
-	SizeBytes    Measurement       `json:"size"`
-	// awsElasticBlockStore, azureDisk, gcePersistentDisk, csi, nfs, local, etc.
-	Type string `json:"type,omitempty"`
-	// ebs.csi.aws.com, disk.csi.azure.com, etc.
-	CSIDriver string `json:"csiDriver,omitempty"`
-	// Cloud provider's volume identifier
-	ProviderVolumeID string `json:"providerVolumeId,omitempty"`
-	// ReadWriteOnce, ReadWriteMany, ReadOnlyMany
-	AccessModes []string `json:"accessModes,omitempty"`
-	// Retain, Delete, Recycle
-	ReclaimPolicy string `json:"reclaimPolicy,omitempty"`
-	// Cloud region for cross-region cost tracking
-	Region string `json:"region,omitempty"`
-	// Availability zone for cross-AZ cost tracking
-	Zone string `json:"zone,omitempty"`
-	// Volume lifecycle timestamps
-	Start time.Time `json:"start"`         // Volume creation timestamp
-	End   time.Time `json:"end,omitempty"` // Volume deletion timestamp (nil if still active)
-	// Duration volume existed within measurement window
-	DurationSeconds Measurement `json:"durationSeconds"`
-	// JSON-encoded node affinity for local volumes
-	NodeAffinity string `json:"nodeAffinity,omitempty"`
-	// Storage performance characteristics
-	ProvisionedIOPS       Measurement `json:"provisionedIops,omitempty"`       // Provisioned IOPS (AWS io1/io2, Azure Premium)
-	ProvisionedThroughput Measurement `json:"provisionedThroughput,omitempty"` // Provisioned throughput in MB/s
-	PerformanceMode       string      `json:"performanceMode,omitempty"`       // "generalPurpose", "maxIO", "provisioned"
+	UID             string    `json:"uid"`
+	Name            string    `json:"name"`
+	StorageClass    string    `json:"storageClass"`
+	CSIVolumeHandle string    `json:"csiVolumeHandle,omitempty"`
+	SizeBytes       float64   `json:"size"`
+	Start           time.Time `json:"start"`
+	End             time.Time `json:"end"`
 }
 }
 
 
-func (kms *KubeModelSet) RegisterVolume(uid, name string) error {
-	if uid == "" {
-		err := fmt.Errorf("UID is nil for PersistentVolume '%s'", name)
-		kms.Error(err)
+func (p *PersistentVolume) ValidatePersistentVolume(window Window) error {
+	if p.UID == "" {
+		return fmt.Errorf("UID is missing for PersistentVolume with name '%s'", p.Name)
+	}
+
+	if p.Name == "" {
+		return fmt.Errorf("Name is missing for PersistentVolume '%s'", p.UID)
+	}
+
+	if err := checkWindow(window, p.Start, p.End); err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	if _, ok := kms.Volumes[uid]; !ok {
-		clusterUID := ""
+	return nil
+}
 
 
-		if kms.Cluster == nil {
-			kms.Warnf("RegisterVolume(%s, %s): Cluster is nil", uid, name)
-		} else {
-			clusterUID = kms.Cluster.UID
-		}
+func (kms *KubeModelSet) RegisterPersistentVolume(pv *PersistentVolume) error {
+	if err := pv.ValidatePersistentVolume(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterPersistentVolume: invalid persistent volume: %w", err)
+		kms.Error(err)
+		return err
+	}
 
 
-		kms.Volumes[uid] = &PersistentVolume{
-			UID:        uid,
-			ClusterUID: clusterUID,
-			Name:       name,
-		}
+	if _, ok := kms.PersistentVolumes[pv.UID]; !ok {
+		kms.PersistentVolumes[pv.UID] = pv
 
 
 		kms.Metadata.ObjectCount++
 		kms.Metadata.ObjectCount++
 	}
 	}

+ 123 - 0
core/pkg/model/kubemodel/pv_test.go

@@ -0,0 +1,123 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidatePersistentVolume(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name    string
+		pv      *PersistentVolume
+		wantErr string
+	}{
+		{
+			name:    "empty UID",
+			pv:      &PersistentVolume{Name: "my-pv", Start: start, End: end},
+			wantErr: "UID is missing for PersistentVolume with name 'my-pv'",
+		},
+		{
+			name:    "empty Name",
+			pv:      &PersistentVolume{UID: "pv-uid", Start: start, End: end},
+			wantErr: "Name is missing for PersistentVolume 'pv-uid'",
+		},
+		{
+			name:    "outside window",
+			pv:      &PersistentVolume{UID: "pv-uid", Name: "my-pv", Start: start.Add(-time.Hour), End: end},
+			wantErr: checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name: "valid",
+			pv:   &PersistentVolume{UID: "pv-uid", Name: "my-pv", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.pv.ValidatePersistentVolume(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterPersistentVolume(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newPV := func(uid, name string) *PersistentVolume {
+		return &PersistentVolume{UID: uid, Name: name, Start: start, End: end}
+	}
+
+	tests := []struct {
+		name    string
+		setup   func(*KubeModelSet)
+		pv      *PersistentVolume
+		wantErr string
+		want    *KubeModelSet
+	}{
+		{
+			name:    "validation failure",
+			pv:      &PersistentVolume{UID: "", Name: "my-pv", Start: start, End: end},
+			wantErr: "RegisterPersistentVolume: invalid persistent volume: UID is missing for PersistentVolume with name 'my-pv'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterPersistentVolume: invalid persistent volume: UID is missing for PersistentVolume with name 'my-pv'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name: "registers persistent volume",
+			pv:   newPV("pv-uid", "my-pv"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.PersistentVolumes["pv-uid"] = newPV("pv-uid", "my-pv")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				kms.RegisterPersistentVolume(newPV("pv-uid", "original"))
+			},
+			pv: newPV("pv-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.PersistentVolumes["pv-uid"] = newPV("pv-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterPersistentVolume(tt.pv)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 35 - 36
core/pkg/model/kubemodel/pvc.go

@@ -5,50 +5,49 @@ import (
 	"time"
 	"time"
 )
 )
 
 
+// @bingen:generate:PersistentVolumeClaim
 type PersistentVolumeClaim struct {
 type PersistentVolumeClaim struct {
-	// Version 1 fields
-	UID                string            `json:"uid"`
-	NamespaceUID       string            `json:"namespaceUid"`
-	VolumeUID          *string           `json:"volumeUid,omitempty"`
-	PodUID             *string           `json:"podUid,omitempty"`
-	Name               string            `json:"name"`
-	Labels             map[string]string `json:"labels,omitempty"`
-	Annotations        map[string]string `json:"annotations,omitempty"`
-	StorageClass       string            `json:"storageClass"`
-	StorageByteSeconds Measurement       `json:"storageByteSeconds"`
-	RequestedBytes     Measurement       `json:"requestedBytes"`
-	Size               Measurement       `json:"size"` // Size in bytes
-	VolumeName         string            `json:"volumeName"`
-	// ReadWriteOnce, ReadWriteMany, ReadOnlyMany
-	AccessModes           []string    `json:"accessModes,omitempty"`
-	ActualUsedByteSeconds Measurement `json:"actualUsedByteSeconds,omitempty"`
-	Start                 time.Time   `json:"start"`         // PVC creation timestamp
-	End                   time.Time   `json:"end,omitempty"` // PVC deletion timestamp (nil if still active)
-	BoundAt               time.Time   `json:"boundAt,omitempty"`
-	DurationSeconds       Measurement `json:"durationSeconds,omitempty"`
+	UID                 string    `json:"uid"`
+	NamespaceUID        string    `json:"namespaceUid"`
+	Name                string    `json:"name"`
+	PersistentVolumeUID string    `json:"persistentVolumeUID,omitempty"`
+	StorageClass        string    `json:"storageClass"`
+	Start               time.Time `json:"start"`
+	End                 time.Time `json:"end"`
+	RequestedBytes      float64   `json:"requestedBytes"`
+	UsageBytesAvg       float64   `json:"usageBytesAvg"`
+	UsageBytesMax       float64   `json:"usageBytesMax"`
 }
 }
 
 
-func (kms *KubeModelSet) RegisterPVC(uid, name, namespace string) error {
-	if uid == "" {
-		err := fmt.Errorf("UID is nil for PVC '%s'", name)
-		kms.Error(err)
+func (p *PersistentVolumeClaim) ValidatePVC(window Window) error {
+	if p.UID == "" {
+		return fmt.Errorf("UID is missing for PVC with name '%s'", p.Name)
+	}
+
+	if p.Name == "" {
+		return fmt.Errorf("Name is missing for PVC '%s'", p.UID)
+	}
+
+	if p.NamespaceUID == "" {
+		return fmt.Errorf("NamespaceUID is missing for PVC '%s'", p.UID)
+	}
+
+	if err := checkWindow(window, p.Start, p.End); err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	if _, ok := kms.PersistentVolumeClaims[uid]; !ok {
-		namespaceUID := ""
+	return nil
+}
 
 
-		if ns, ok := kms.idx.namespaceByName[namespace]; !ok {
-			kms.Warnf("RegisterPVC(%s, %s, %s): missing namespace '%s'", uid, name, namespace, namespace)
-		} else {
-			namespaceUID = ns.UID
-		}
+func (kms *KubeModelSet) RegisterPVC(pvc *PersistentVolumeClaim) error {
+	if err := pvc.ValidatePVC(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterPVC: invalid pvc: %w", err)
+		kms.Error(err)
+		return err
+	}
 
 
-		kms.PersistentVolumeClaims[uid] = &PersistentVolumeClaim{
-			UID:          uid,
-			Name:         name,
-			NamespaceUID: namespaceUID,
-		}
+	if _, ok := kms.PersistentVolumeClaims[pvc.UID]; !ok {
+		kms.PersistentVolumeClaims[pvc.UID] = pvc
 
 
 		kms.Metadata.ObjectCount++
 		kms.Metadata.ObjectCount++
 	}
 	}

+ 128 - 0
core/pkg/model/kubemodel/pvc_test.go

@@ -0,0 +1,128 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidatePVC(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name    string
+		pvc     *PersistentVolumeClaim
+		wantErr string
+	}{
+		{
+			name:    "empty UID",
+			pvc:     &PersistentVolumeClaim{Name: "my-pvc", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "UID is missing for PVC with name 'my-pvc'",
+		},
+		{
+			name:    "empty Name",
+			pvc:     &PersistentVolumeClaim{UID: "pvc-uid", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "Name is missing for PVC 'pvc-uid'",
+		},
+		{
+			name:    "empty NamespaceUID",
+			pvc:     &PersistentVolumeClaim{UID: "pvc-uid", Name: "my-pvc", Start: start, End: end},
+			wantErr: "NamespaceUID is missing for PVC 'pvc-uid'",
+		},
+		{
+			name:    "outside window",
+			pvc:     &PersistentVolumeClaim{UID: "pvc-uid", Name: "my-pvc", NamespaceUID: "ns-uid", Start: start.Add(-time.Hour), End: end},
+			wantErr: checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name: "valid",
+			pvc:  &PersistentVolumeClaim{UID: "pvc-uid", Name: "my-pvc", NamespaceUID: "ns-uid", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.pvc.ValidatePVC(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterPVC(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newPVC := func(uid, name string) *PersistentVolumeClaim {
+		return &PersistentVolumeClaim{UID: uid, Name: name, NamespaceUID: "ns-uid", Start: start, End: end}
+	}
+
+	tests := []struct {
+		name    string
+		setup   func(*KubeModelSet)
+		pvc     *PersistentVolumeClaim
+		wantErr string
+		want    *KubeModelSet
+	}{
+		{
+			name:    "validation failure",
+			pvc:     &PersistentVolumeClaim{UID: "", Name: "my-pvc", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "RegisterPVC: invalid pvc: UID is missing for PVC with name 'my-pvc'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterPVC: invalid pvc: UID is missing for PVC with name 'my-pvc'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name: "registers pvc",
+			pvc:  newPVC("pvc-uid", "my-pvc"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.PersistentVolumeClaims["pvc-uid"] = newPVC("pvc-uid", "my-pvc")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				kms.RegisterPVC(newPVC("pvc-uid", "original"))
+			},
+			pvc: newPVC("pvc-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.PersistentVolumeClaims["pvc-uid"] = newPVC("pvc-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterPVC(tt.pvc)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 59 - 0
core/pkg/model/kubemodel/replicaset.go

@@ -0,0 +1,59 @@
+package kubemodel
+
+import (
+	"fmt"
+	"time"
+)
+
+// @bingen:generate:ReplicaSet
+// ReplicaSet represents a Kubernetes ReplicaSet resource
+type ReplicaSet struct {
+	UID          string            `json:"uid"`
+	NamespaceUID string            `json:"namespaceUid"`
+	Name         string            `json:"name"`
+	Owners       []Owner           `json:"owners"`
+	Labels       map[string]string `json:"labels,omitempty"`
+	Annotations  map[string]string `json:"annotations,omitempty"`
+	Start        time.Time         `json:"start,omitempty"`
+	End          time.Time         `json:"end,omitempty"`
+}
+
+func (r *ReplicaSet) ValidateReplicaSet(window Window) error {
+	if r.UID == "" {
+		return fmt.Errorf("UID is missing for ReplicaSet with name '%s'", r.Name)
+	}
+
+	if r.Name == "" {
+		return fmt.Errorf("Name is missing for ReplicaSet '%s'", r.UID)
+	}
+
+	if r.NamespaceUID == "" {
+		return fmt.Errorf("NamespaceUID is missing for ReplicaSet '%s'", r.UID)
+	}
+
+	if err := checkWindow(window, r.Start, r.End); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (kms *KubeModelSet) RegisterReplicaSet(replicaSet *ReplicaSet) error {
+	if err := replicaSet.ValidateReplicaSet(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterReplicaSet: invalid replicaset: %w", err)
+		kms.Error(err)
+		return err
+	}
+
+	if _, ok := kms.ReplicaSets[replicaSet.UID]; !ok {
+		if kms.Cluster == nil {
+			kms.Warnf("RegisterReplicaSet: Cluster is nil")
+		}
+
+		kms.ReplicaSets[replicaSet.UID] = replicaSet
+
+		kms.Metadata.ObjectCount++
+	}
+
+	return nil
+}

+ 148 - 0
core/pkg/model/kubemodel/replicaset_test.go

@@ -0,0 +1,148 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateReplicaSet(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name       string
+		replicaSet *ReplicaSet
+		wantErr    string
+	}{
+		{
+			name:       "empty UID",
+			replicaSet: &ReplicaSet{Name: "my-rs", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:    "UID is missing for ReplicaSet with name 'my-rs'",
+		},
+		{
+			name:       "empty Name",
+			replicaSet: &ReplicaSet{UID: "rs-uid", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:    "Name is missing for ReplicaSet 'rs-uid'",
+		},
+		{
+			name:       "empty NamespaceUID",
+			replicaSet: &ReplicaSet{UID: "rs-uid", Name: "my-rs", Start: start, End: end},
+			wantErr:    "NamespaceUID is missing for ReplicaSet 'rs-uid'",
+		},
+		{
+			name:       "outside window",
+			replicaSet: &ReplicaSet{UID: "rs-uid", Name: "my-rs", NamespaceUID: "ns-uid", Start: start.Add(-time.Hour), End: end},
+			wantErr:    checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name:       "valid",
+			replicaSet: &ReplicaSet{UID: "rs-uid", Name: "my-rs", NamespaceUID: "ns-uid", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.replicaSet.ValidateReplicaSet(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterReplicaSet(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newReplicaSet := func(uid, name string) *ReplicaSet {
+		return &ReplicaSet{UID: uid, Name: name, NamespaceUID: "ns-uid", Start: start, End: end}
+	}
+	withCluster := func(kms *KubeModelSet) {
+		kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+	}
+
+	tests := []struct {
+		name       string
+		setup      func(*KubeModelSet)
+		replicaSet *ReplicaSet
+		wantErr    string
+		want       *KubeModelSet
+	}{
+		{
+			name:       "validation failure",
+			replicaSet: &ReplicaSet{UID: "", Name: "my-rs", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:    "RegisterReplicaSet: invalid replicaset: UID is missing for ReplicaSet with name 'my-rs'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterReplicaSet: invalid replicaset: UID is missing for ReplicaSet with name 'my-rs'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:       "warns when cluster is nil",
+			replicaSet: newReplicaSet("rs-uid", "my-rs"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.ReplicaSets["rs-uid"] = newReplicaSet("rs-uid", "my-rs")
+				kms.Metadata.ObjectCount = 1
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelWarning, Message: "RegisterReplicaSet: Cluster is nil"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:       "registers replicaset with cluster",
+			setup:      withCluster,
+			replicaSet: newReplicaSet("rs-uid", "my-rs"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.ReplicaSets["rs-uid"] = newReplicaSet("rs-uid", "my-rs")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				withCluster(kms)
+				kms.RegisterReplicaSet(newReplicaSet("rs-uid", "original"))
+			},
+			replicaSet: newReplicaSet("rs-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.ReplicaSets["rs-uid"] = newReplicaSet("rs-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterReplicaSet(tt.replicaSet)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 5 - 4
core/pkg/model/kubemodel/resource.go

@@ -4,10 +4,11 @@ package kubemodel
 type Resource string
 type Resource string
 
 
 const (
 const (
-	ResourceCPU     Resource = "cpu"
-	ResourceMemory  Resource = "memory"
-	ResourceGPU     Resource = "gpu"
-	ResourceStorage Resource = "storage"
+	ResourceCPU          Resource = "cpu"
+	ResourceMemory       Resource = "memory"
+	ResourceNvidia       Resource = "nvidia.com/gpu"
+	ResourceNvidiaShared Resource = "nvidia.com/gpu.shared"
+	ResourceAWSVGPU      Resource = "k8s.amazonaws.com/vgpu"
 )
 )
 
 
 // @bingen:generate:ResourceQuantity
 // @bingen:generate:ResourceQuantity

+ 35 - 16
core/pkg/model/kubemodel/resourcequota.go

@@ -70,30 +70,49 @@ func (stat *ResourceQuotaStatusUsed) SetLimit(resource Resource, unit Unit, stat
 	stat.Limits.Set(resource, unit, statType, value)
 	stat.Limits.Set(resource, unit, statType, value)
 }
 }
 
 
-func (kms *KubeModelSet) RegisterResourceQuota(uid, name, namespace string) error {
-	if uid == "" {
-		err := fmt.Errorf("UID is nil for ResourceQuota '%s'", name)
-		kms.Error(err)
+func (rq *ResourceQuota) ValidateResourceQuota(window Window) error {
+	if rq.UID == "" {
+		return fmt.Errorf("UID is missing for ResourceQuota with name '%s'", rq.Name)
+	}
+
+	if rq.Name == "" {
+		return fmt.Errorf("Name is missing for ResourceQuota '%s'", rq.UID)
+	}
+
+	if rq.NamespaceUID == "" {
+		return fmt.Errorf("NamespaceUID is missing for ResourceQuota '%s'", rq.UID)
+	}
+
+	if err := checkWindow(window, rq.Start, rq.End); err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	if _, ok := kms.ResourceQuotas[uid]; !ok {
-		namespaceUID := ""
+	return nil
+}
 
 
-		if _, ok := kms.idx.namespaceByName[namespace]; !ok {
-			kms.Warnf("RegisterResourceQuota(%s, %s, %s): missing namespace", uid, name, namespace)
-		} else {
-			namespaceUID = kms.idx.namespaceByName[namespace].UID
+func (kms *KubeModelSet) RegisterResourceQuota(resourceQuota *ResourceQuota) error {
+	if err := resourceQuota.ValidateResourceQuota(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterResourceQuota: invalid resource quota: %w", err)
+		kms.Error(err)
+		return err
+	}
+
+	if _, ok := kms.ResourceQuotas[resourceQuota.UID]; !ok {
+		// Initialize Spec and Status if they're nil
+		if resourceQuota.Spec == nil {
+			resourceQuota.Spec = &ResourceQuotaSpec{Hard: &ResourceQuotaSpecHard{}}
+		} else if resourceQuota.Spec.Hard == nil {
+			resourceQuota.Spec.Hard = &ResourceQuotaSpecHard{}
 		}
 		}
 
 
-		kms.ResourceQuotas[uid] = &ResourceQuota{
-			UID:          uid,
-			Name:         name,
-			NamespaceUID: namespaceUID,
-			Spec:         &ResourceQuotaSpec{Hard: &ResourceQuotaSpecHard{}},
-			Status:       &ResourceQuotaStatus{Used: &ResourceQuotaStatusUsed{}},
+		if resourceQuota.Status == nil {
+			resourceQuota.Status = &ResourceQuotaStatus{Used: &ResourceQuotaStatusUsed{}}
+		} else if resourceQuota.Status.Used == nil {
+			resourceQuota.Status.Used = &ResourceQuotaStatusUsed{}
 		}
 		}
 
 
+		kms.ResourceQuotas[resourceQuota.UID] = resourceQuota
+
 		kms.Metadata.ObjectCount++
 		kms.Metadata.ObjectCount++
 	}
 	}
 
 

+ 135 - 0
core/pkg/model/kubemodel/resourcequota_test.go

@@ -0,0 +1,135 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateResourceQuota(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name          string
+		resourceQuota *ResourceQuota
+		wantErr       string
+	}{
+		{
+			name:          "empty UID",
+			resourceQuota: &ResourceQuota{Name: "my-rq", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:       "UID is missing for ResourceQuota with name 'my-rq'",
+		},
+		{
+			name:          "empty Name",
+			resourceQuota: &ResourceQuota{UID: "rq-uid", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:       "Name is missing for ResourceQuota 'rq-uid'",
+		},
+		{
+			name:          "empty NamespaceUID",
+			resourceQuota: &ResourceQuota{UID: "rq-uid", Name: "my-rq", Start: start, End: end},
+			wantErr:       "NamespaceUID is missing for ResourceQuota 'rq-uid'",
+		},
+		{
+			name:          "outside window",
+			resourceQuota: &ResourceQuota{UID: "rq-uid", Name: "my-rq", NamespaceUID: "ns-uid", Start: start.Add(-time.Hour), End: end},
+			wantErr:       checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name:          "valid",
+			resourceQuota: &ResourceQuota{UID: "rq-uid", Name: "my-rq", NamespaceUID: "ns-uid", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.resourceQuota.ValidateResourceQuota(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterResourceQuota(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newRQ := func(uid, name string) *ResourceQuota {
+		return &ResourceQuota{UID: uid, Name: name, NamespaceUID: "ns-uid", Start: start, End: end}
+	}
+	// RegisterResourceQuota initializes nil Spec/Status on registration.
+	newRegisteredRQ := func(uid, name string) *ResourceQuota {
+		rq := newRQ(uid, name)
+		rq.Spec = &ResourceQuotaSpec{Hard: &ResourceQuotaSpecHard{}}
+		rq.Status = &ResourceQuotaStatus{Used: &ResourceQuotaStatusUsed{}}
+		return rq
+	}
+
+	tests := []struct {
+		name          string
+		setup         func(*KubeModelSet)
+		resourceQuota *ResourceQuota
+		wantErr       string
+		want          *KubeModelSet
+	}{
+		{
+			name:          "validation failure",
+			resourceQuota: &ResourceQuota{UID: "", Name: "my-rq", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:       "RegisterResourceQuota: invalid resource quota: UID is missing for ResourceQuota with name 'my-rq'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterResourceQuota: invalid resource quota: UID is missing for ResourceQuota with name 'my-rq'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:          "registers resource quota",
+			resourceQuota: newRQ("rq-uid", "my-rq"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.ResourceQuotas["rq-uid"] = newRegisteredRQ("rq-uid", "my-rq")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				kms.RegisterResourceQuota(newRQ("rq-uid", "original"))
+			},
+			resourceQuota: newRQ("rq-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.ResourceQuotas["rq-uid"] = newRegisteredRQ("rq-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterResourceQuota(tt.resourceQuota)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

+ 65 - 22
core/pkg/model/kubemodel/service.go

@@ -1,7 +1,12 @@
 package kubemodel
 package kubemodel
 
 
-import "time"
+import (
+	"fmt"
+	"strings"
+	"time"
+)
 
 
+// @bingen:generate:ServiceType
 type ServiceType string
 type ServiceType string
 
 
 const (
 const (
@@ -11,14 +16,24 @@ const (
 	ServiceTypeExternalName ServiceType = "ExternalName"
 	ServiceTypeExternalName ServiceType = "ExternalName"
 )
 )
 
 
-type ServicePort struct {
-	Name       string `json:"name"`
-	Port       uint16 `json:"port"`
-	TargetPort uint16 `json:"targetPort"`
-	NodePort   uint16 `json:"nodePort"`
-	Protocol   string `json:"protocol"`
+// ParseServiceType converts a string to a ServiceType, performing case-insensitive matching.
+// Returns ServiceTypeClusterIP (the default) if the service type string is not recognized.
+func ParseServiceType(serviceType string) ServiceType {
+	switch strings.ToLower(serviceType) {
+	case "clusterip":
+		return ServiceTypeClusterIP
+	case "nodeport":
+		return ServiceTypeNodePort
+	case "loadbalancer", "lb":
+		return ServiceTypeLoadBalancer
+	case "externalname":
+		return ServiceTypeExternalName
+	default:
+		return ServiceTypeClusterIP
+	}
 }
 }
 
 
+// @bingen:generate:Service
 // Service represents a Kubernetes Service with network traffic tracking for cost allocation.
 // Service represents a Kubernetes Service with network traffic tracking for cost allocation.
 //
 //
 // Network Cost Allocation Strategy:
 // Network Cost Allocation Strategy:
@@ -39,22 +54,50 @@ type ServicePort struct {
 // - Data transfer: $0.09/GB for internet egress
 // - Data transfer: $0.09/GB for internet egress
 // Total Service Cost = (LB hours × hourly rate) + (LCU hours × LCU rate) + (NetworkTransferBytes × transfer rate)
 // Total Service Cost = (LB hours × hourly rate) + (LCU hours × LCU rate) + (NetworkTransferBytes × transfer rate)
 type Service struct {
 type Service struct {
-	UID                  string            `json:"uid"`
-	NamespaceUID         string            `json:"namespaceUid"`
-	Name                 string            `json:"name"`
-	Type                 ServiceType       `json:"type"`
-	Hostname             string            `json:"hostname,omitempty"`
-	Labels               map[string]string `json:"labels,omitempty"`
-	Annotations          map[string]string `json:"annotations,omitempty"`
-	Ports                []ServicePort     `json:"ports,omitempty"`
-	Start                time.Time         `json:"start"`
-	End                  time.Time         `json:"end"`
-	NetworkTransferBytes Measurement       `json:"networkTransferBytes"`
-	NetworkReceiveBytes  Measurement       `json:"networkReceiveBytes"`
+	UID          string      `json:"uid"`
+	NamespaceUID string      `json:"namespaceUid"`
+	Name         string      `json:"name"`
+	Type         ServiceType `json:"type"`
+	Start        time.Time   `json:"start"`
+	End          time.Time   `json:"end"`
 	// Label selector to identify pods/containers targeted by this service
 	// Label selector to identify pods/containers targeted by this service
 	// Maps label keys to values (e.g., {"app": "nginx", "tier": "frontend"})
 	// Maps label keys to values (e.g., {"app": "nginx", "tier": "frontend"})
 	// Pods with matching labels will receive traffic from this service
 	// Pods with matching labels will receive traffic from this service
-	Selector map[string]string `json:"selector,omitempty"`
-	// Lifecycle tracking
-	DurationSeconds Measurement `json:"durationSeconds"` // Duration service existed within measurement window
+	Selector         map[string]string `json:"selector,omitempty"`
+	LBIngressAddress string            `json:"lbIngressAddress,omitempty"`
+}
+
+func (s *Service) ValidateService(window Window) error {
+	if s.UID == "" {
+		return fmt.Errorf("UID is missing for Service with name '%s'", s.Name)
+	}
+
+	if s.Name == "" {
+		return fmt.Errorf("Name is missing for Service '%s'", s.UID)
+	}
+
+	if s.NamespaceUID == "" {
+		return fmt.Errorf("NamespaceUID is missing for Service '%s'", s.UID)
+	}
+
+	if err := checkWindow(window, s.Start, s.End); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (kms *KubeModelSet) RegisterService(service *Service) error {
+	if err := service.ValidateService(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterService: invalid service: %w", err)
+		kms.Error(err)
+		return err
+	}
+
+	if _, ok := kms.Services[service.UID]; !ok {
+		kms.Services[service.UID] = service
+		kms.Metadata.ObjectCount++
+	}
+
+	return nil
 }
 }

+ 164 - 0
core/pkg/model/kubemodel/service_test.go

@@ -0,0 +1,164 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateService(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name    string
+		service *Service
+		wantErr string
+	}{
+		{
+			name:    "empty UID",
+			service: &Service{Name: "my-svc", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "UID is missing for Service with name 'my-svc'",
+		},
+		{
+			name:    "empty Name",
+			service: &Service{UID: "svc-uid", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "Name is missing for Service 'svc-uid'",
+		},
+		{
+			name:    "empty NamespaceUID",
+			service: &Service{UID: "svc-uid", Name: "my-svc", Start: start, End: end},
+			wantErr: "NamespaceUID is missing for Service 'svc-uid'",
+		},
+		{
+			name:    "outside window",
+			service: &Service{UID: "svc-uid", Name: "my-svc", NamespaceUID: "ns-uid", Start: start.Add(-time.Hour), End: end},
+			wantErr: checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name:    "valid",
+			service: &Service{UID: "svc-uid", Name: "my-svc", NamespaceUID: "ns-uid", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.service.ValidateService(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterService(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newService := func(uid, name string) *Service {
+		return &Service{UID: uid, Name: name, NamespaceUID: "ns-uid", Start: start, End: end}
+	}
+
+	tests := []struct {
+		name    string
+		setup   func(*KubeModelSet)
+		service *Service
+		wantErr string
+		want    *KubeModelSet
+	}{
+		{
+			name:    "validation failure",
+			service: &Service{UID: "", Name: "my-svc", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr: "RegisterService: invalid service: UID is missing for Service with name 'my-svc'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterService: invalid service: UID is missing for Service with name 'my-svc'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:    "registers service",
+			service: newService("svc-uid", "my-svc"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Services["svc-uid"] = newService("svc-uid", "my-svc")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				kms.RegisterService(newService("svc-uid", "original"))
+			},
+			service: newService("svc-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Services["svc-uid"] = newService("svc-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterService(tt.service)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}
+
+func TestParseServiceType(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected ServiceType
+	}{
+		// Exact canonical values
+		{"ClusterIP", ServiceTypeClusterIP},
+		{"NodePort", ServiceTypeNodePort},
+		{"LoadBalancer", ServiceTypeLoadBalancer},
+		{"ExternalName", ServiceTypeExternalName},
+		// Case-insensitive
+		{"clusterip", ServiceTypeClusterIP},
+		{"nodeport", ServiceTypeNodePort},
+		{"loadbalancer", ServiceTypeLoadBalancer},
+		{"externalname", ServiceTypeExternalName},
+		{"CLUSTERIP", ServiceTypeClusterIP},
+		{"LOADBALANCER", ServiceTypeLoadBalancer},
+		// "lb" alias
+		{"lb", ServiceTypeLoadBalancer},
+		{"LB", ServiceTypeLoadBalancer},
+		// Unknown input defaults to ClusterIP
+		{"", ServiceTypeClusterIP},
+		{"unknown", ServiceTypeClusterIP},
+		{"ingress", ServiceTypeClusterIP},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.input, func(t *testing.T) {
+			got := ParseServiceType(tt.input)
+			if got != tt.expected {
+				t.Errorf("ParseServiceType(%q) = %q, want %q", tt.input, got, tt.expected)
+			}
+		})
+	}
+}

+ 59 - 0
core/pkg/model/kubemodel/statefulset.go

@@ -0,0 +1,59 @@
+package kubemodel
+
+import (
+	"fmt"
+	"time"
+)
+
+// @bingen:generate:StatefulSet
+// StatefulSet represents a Kubernetes StatefulSet resource
+type StatefulSet struct {
+	UID          string            `json:"uid"`
+	NamespaceUID string            `json:"namespaceUid"`
+	Name         string            `json:"name"`
+	Labels       map[string]string `json:"labels,omitempty"`
+	Annotations  map[string]string `json:"annotations,omitempty"`
+	MatchLabels  map[string]string `json:"matchLabels,omitempty"`
+	Start        time.Time         `json:"start,omitempty"`
+	End          time.Time         `json:"end,omitempty"`
+}
+
+func (s *StatefulSet) ValidateStatefulSet(window Window) error {
+	if s.UID == "" {
+		return fmt.Errorf("UID is missing for StatefulSet with name '%s'", s.Name)
+	}
+
+	if s.Name == "" {
+		return fmt.Errorf("Name is missing for StatefulSet '%s'", s.UID)
+	}
+
+	if s.NamespaceUID == "" {
+		return fmt.Errorf("NamespaceUID is missing for StatefulSet '%s'", s.UID)
+	}
+
+	if err := checkWindow(window, s.Start, s.End); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (kms *KubeModelSet) RegisterStatefulSet(statefulSet *StatefulSet) error {
+	if err := statefulSet.ValidateStatefulSet(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterStatefulSet: invalid statefulset: %w", err)
+		kms.Error(err)
+		return err
+	}
+
+	if _, ok := kms.StatefulSets[statefulSet.UID]; !ok {
+		if kms.Cluster == nil {
+			kms.Warnf("RegisterStatefulSet: Cluster is nil")
+		}
+
+		kms.StatefulSets[statefulSet.UID] = statefulSet
+
+		kms.Metadata.ObjectCount++
+	}
+
+	return nil
+}

+ 148 - 0
core/pkg/model/kubemodel/statefulset_test.go

@@ -0,0 +1,148 @@
+package kubemodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateStatefulSet(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+	window := Window{Start: start, End: end}
+
+	tests := []struct {
+		name        string
+		statefulSet *StatefulSet
+		wantErr     string
+	}{
+		{
+			name:        "empty UID",
+			statefulSet: &StatefulSet{Name: "my-sts", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:     "UID is missing for StatefulSet with name 'my-sts'",
+		},
+		{
+			name:        "empty Name",
+			statefulSet: &StatefulSet{UID: "sts-uid", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:     "Name is missing for StatefulSet 'sts-uid'",
+		},
+		{
+			name:        "empty NamespaceUID",
+			statefulSet: &StatefulSet{UID: "sts-uid", Name: "my-sts", Start: start, End: end},
+			wantErr:     "NamespaceUID is missing for StatefulSet 'sts-uid'",
+		},
+		{
+			name:        "outside window",
+			statefulSet: &StatefulSet{UID: "sts-uid", Name: "my-sts", NamespaceUID: "ns-uid", Start: start.Add(-time.Hour), End: end},
+			wantErr:     checkWindow(window, start.Add(-time.Hour), end).Error(),
+		},
+		{
+			name:        "valid",
+			statefulSet: &StatefulSet{UID: "sts-uid", Name: "my-sts", NamespaceUID: "ns-uid", Start: start, End: end},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.statefulSet.ValidateStatefulSet(window)
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestRegisterStatefulSet(t *testing.T) {
+	start := time.Now().UTC().Truncate(time.Hour)
+	end := start.Add(time.Hour)
+
+	newStatefulSet := func(uid, name string) *StatefulSet {
+		return &StatefulSet{UID: uid, Name: name, NamespaceUID: "ns-uid", Start: start, End: end}
+	}
+	withCluster := func(kms *KubeModelSet) {
+		kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
+	}
+
+	tests := []struct {
+		name        string
+		setup       func(*KubeModelSet)
+		statefulSet *StatefulSet
+		wantErr     string
+		want        *KubeModelSet
+	}{
+		{
+			name:        "validation failure",
+			statefulSet: &StatefulSet{UID: "", Name: "my-sts", NamespaceUID: "ns-uid", Start: start, End: end},
+			wantErr:     "RegisterStatefulSet: invalid statefulset: UID is missing for StatefulSet with name 'my-sts'",
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelError, Message: "RegisterStatefulSet: invalid statefulset: UID is missing for StatefulSet with name 'my-sts'"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:        "warns when cluster is nil",
+			statefulSet: newStatefulSet("sts-uid", "my-sts"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				kms.StatefulSets["sts-uid"] = newStatefulSet("sts-uid", "my-sts")
+				kms.Metadata.ObjectCount = 1
+				kms.Metadata.Diagnostics = []Diagnostic{
+					{Level: DiagnosticLevelWarning, Message: "RegisterStatefulSet: Cluster is nil"},
+				}
+				return kms
+			}(),
+		},
+		{
+			name:        "registers statefulset with cluster",
+			setup:       withCluster,
+			statefulSet: newStatefulSet("sts-uid", "my-sts"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.StatefulSets["sts-uid"] = newStatefulSet("sts-uid", "my-sts")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+		{
+			name: "duplicate registration is a no-op",
+			setup: func(kms *KubeModelSet) {
+				withCluster(kms)
+				kms.RegisterStatefulSet(newStatefulSet("sts-uid", "original"))
+			},
+			statefulSet: newStatefulSet("sts-uid", "duplicate"),
+			want: func() *KubeModelSet {
+				kms := NewKubeModelSet(start, end)
+				withCluster(kms)
+				kms.StatefulSets["sts-uid"] = newStatefulSet("sts-uid", "original")
+				kms.Metadata.ObjectCount = 1
+				return kms
+			}(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			kms := NewKubeModelSet(start, end)
+			if tt.setup != nil {
+				tt.setup(kms)
+			}
+
+			err := kms.RegisterStatefulSet(tt.statefulSet)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			KubeModelSetEquals(t, tt.want, kms)
+		})
+	}
+}

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

@@ -3,9 +3,8 @@ package kubemodel
 // @bingen:generate:Unit
 // @bingen:generate:Unit
 type Unit string
 type Unit string
 
 
-type Measurement = float64
-
 const (
 const (
+	UnitCore            = "c"
 	UnitMillicore       = "m"
 	UnitMillicore       = "m"
 	UnitByte            = "B"
 	UnitByte            = "B"
 	UnitSecond          = "s"
 	UnitSecond          = "s"

+ 69 - 0
core/pkg/model/pricingmodel/pricingmodel.go

@@ -0,0 +1,69 @@
+package pricingmodel
+
+import (
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/model/shared"
+)
+
+// TODO: assess whether we need any of this, or whether we can adapt all existing
+// references to it to use core/pkg/pricing concepts, instead.
+//
+// See:
+//   pkg/cloud/aws/pricinglistpricingsource.go
+//   pkg/cloud/azure/retailpricingsource.go
+//   pkg/cloud/gcp/billingpricingsource.go
+
+type PricingSource interface {
+	// PricingSourceType returns the instance type of the PricingSource, each implementation of this interface should
+	// provide a unique type that all instances should return
+	PricingSourceType() PricingSourceType
+	// PricingSourceKey returns the unique key of the PricingSource instance. In PricingSource implementation that may
+	// have multiple instances running side by side this key (derived from some configuration will) will Identify each
+	// instance. In PricingSource implementations where there will only be a single instance (ex Provider List Pricing)
+	// The PricingSourceKey should match the PricingSourceType
+	PricingSourceKey() string
+	GetPricing() (*PricingModelSet, error)
+}
+
+type PricingSourceType string
+
+type PricingModelSet struct {
+	TimeStamp   time.Time
+	SourceType  PricingSourceType
+	SourceKey   string
+	NodePricing map[NodeKey]NodePricing
+}
+
+// NewPricingModelSet creates a PricingModelSet with SourceKey initialized to sourceType.
+func NewPricingModelSet(timeStamp time.Time, sourceType PricingSourceType, sourceKey string) *PricingModelSet {
+	return &PricingModelSet{
+		TimeStamp:   timeStamp,
+		SourceType:  sourceType,
+		SourceKey:   sourceKey,
+		NodePricing: make(map[NodeKey]NodePricing),
+	}
+}
+
+type NodePricingType string
+
+const (
+	NodePricingTypeTotal   NodePricingType = "Total"
+	NodePricingTypeCPUCore NodePricingType = "CPUCore"
+	NodePricingTypeRamGB   NodePricingType = "RamGB"
+	NodePricingTypeDevice  NodePricingType = "Device"
+)
+
+type NodeKey struct {
+	Provider    shared.Provider
+	PricingType NodePricingType
+	UsageType   shared.UsageType
+	Region      string
+	NodeType    string
+	Family      string
+	DeviceType  string
+}
+
+type NodePricing struct {
+	HourlyRate float64
+}

+ 36 - 0
core/pkg/model/shared/provider.go

@@ -0,0 +1,36 @@
+package shared
+
+import "strings"
+
+type Provider string
+
+const (
+	ProviderEmpty        Provider = ""
+	ProviderAWS          Provider = "AWS"
+	ProviderGCP          Provider = "GCP"
+	ProviderAzure        Provider = "Azure"
+	ProviderAlibaba      Provider = "Alibaba"
+	ProviderDigitalOcean Provider = "DigitalOcean"
+	ProviderOracle       Provider = "Oracle"
+)
+
+// ParseProvider converts a string to a Provider type, performing case-insensitive matching.
+// Returns ProviderEmpty if the provider string is not recognized.
+func ParseProvider(provider string) Provider {
+	switch strings.ToLower(provider) {
+	case "aws", "amazon":
+		return ProviderAWS
+	case "gcp", "gce", "google":
+		return ProviderGCP
+	case "azure", "microsoft":
+		return ProviderAzure
+	case "alibaba":
+		return ProviderAlibaba
+	case "digitalocean", "do":
+		return ProviderDigitalOcean
+	case "oracle", "oci":
+		return ProviderOracle
+	default:
+		return ProviderEmpty
+	}
+}

+ 55 - 0
core/pkg/model/shared/provider_test.go

@@ -0,0 +1,55 @@
+package shared
+
+import (
+	"testing"
+)
+
+func TestParseProvider(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected Provider
+	}{
+		// Canonical values
+		{"AWS", ProviderAWS},
+		{"GCP", ProviderGCP},
+		{"Azure", ProviderAzure},
+		{"Alibaba", ProviderAlibaba},
+		{"DigitalOcean", ProviderDigitalOcean},
+		{"Oracle", ProviderOracle},
+		// Case-insensitive
+		{"aws", ProviderAWS},
+		{"gcp", ProviderGCP},
+		{"azure", ProviderAzure},
+		{"alibaba", ProviderAlibaba},
+		{"digitalocean", ProviderDigitalOcean},
+		{"oracle", ProviderOracle},
+		{"AWS", ProviderAWS},
+		{"AZURE", ProviderAzure},
+		// Aliases
+		{"amazon", ProviderAWS},
+		{"Amazon", ProviderAWS},
+		{"gce", ProviderGCP},
+		{"GCE", ProviderGCP},
+		{"google", ProviderGCP},
+		{"Google", ProviderGCP},
+		{"microsoft", ProviderAzure},
+		{"Microsoft", ProviderAzure},
+		{"do", ProviderDigitalOcean},
+		{"DO", ProviderDigitalOcean},
+		{"oci", ProviderOracle},
+		{"OCI", ProviderOracle},
+		// Unknown input returns empty
+		{"", ProviderEmpty},
+		{"unknown", ProviderEmpty},
+		{"scaleway", ProviderEmpty},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.input, func(t *testing.T) {
+			got := ParseProvider(tt.input)
+			if got != tt.expected {
+				t.Errorf("ParseProvider(%q) = %q, want %q", tt.input, got, tt.expected)
+			}
+		})
+	}
+}

+ 9 - 0
core/pkg/model/shared/usagetype.go

@@ -0,0 +1,9 @@
+package shared
+
+type UsageType string
+
+const (
+	UsageTypeEmpty    UsageType = ""
+	UsageTypeOnDemand UsageType = "OnDemand"
+	UsageTypeSpot     UsageType = "Spot"
+)

+ 3 - 0
core/pkg/nodestats/nodes_test.go

@@ -125,6 +125,9 @@ func (tcc *NodesOnlyClusterCache) GetAllStorageClasses() []*clustercache.Storage
 // GetAllJobs returns all the cached jobs
 // GetAllJobs returns all the cached jobs
 func (tcc *NodesOnlyClusterCache) GetAllJobs() []*clustercache.Job { return nil }
 func (tcc *NodesOnlyClusterCache) GetAllJobs() []*clustercache.Job { return nil }
 
 
+// GetAllJobs returns all the cached cronjobs
+func (tcc *NodesOnlyClusterCache) GetAllCronJobs() []*clustercache.CronJob { return nil }
+
 // GetAllPodDisruptionBudgets returns all cached pod disruption budgets
 // GetAllPodDisruptionBudgets returns all cached pod disruption budgets
 func (tcc *NodesOnlyClusterCache) GetAllPodDisruptionBudgets() []*clustercache.PodDisruptionBudget {
 func (tcc *NodesOnlyClusterCache) GetAllPodDisruptionBudgets() []*clustercache.PodDisruptionBudget {
 	return nil
 	return nil

+ 8 - 3
core/pkg/opencost/exporter/controllers.go

@@ -31,6 +31,7 @@ type ComputePipelineSource interface {
 // PipelinesExportConfig is a configuration struct that contains the export resolutions for
 // PipelinesExportConfig is a configuration struct that contains the export resolutions for
 // allocation, assets, and network insights pipelines.
 // allocation, assets, and network insights pipelines.
 type PipelinesExportConfig struct {
 type PipelinesExportConfig struct {
+	AppName                           string
 	ClusterUID                        string
 	ClusterUID                        string
 	ClusterName                       string
 	ClusterName                       string
 	AllocationPiplineResolutions      []time.Duration
 	AllocationPiplineResolutions      []time.Duration
@@ -52,8 +53,9 @@ func defaultPipelineExportResolutions() []time.Duration {
 
 
 // NewPipelinesExportConfig returns the default export configuration for all pipelines
 // NewPipelinesExportConfig returns the default export configuration for all pipelines
 // which is set to export hourly and daily for allocations, assets, and network insights.
 // which is set to export hourly and daily for allocations, assets, and network insights.
-func NewPipelinesExportConfig(clusterUID, clusterName string) PipelinesExportConfig {
+func NewPipelinesExportConfig(appName, clusterUID, clusterName string) PipelinesExportConfig {
 	return PipelinesExportConfig{
 	return PipelinesExportConfig{
+		AppName:                           appName,
 		ClusterUID:                        clusterUID,
 		ClusterUID:                        clusterUID,
 		ClusterName:                       clusterName,
 		ClusterName:                       clusterName,
 		AllocationPiplineResolutions:      defaultPipelineExportResolutions(),
 		AllocationPiplineResolutions:      defaultPipelineExportResolutions(),
@@ -175,10 +177,11 @@ func NewPipelineExportControllers(store storage.Storage, cm ComputePipelineSourc
 		var kubeModelController *export.ComputeExportController[kubemodel.KubeModelSet]
 		var kubeModelController *export.ComputeExportController[kubemodel.KubeModelSet]
 		var err error
 		var err error
 		if config.Streaming {
 		if config.Streaming {
-			kubeModelController, err = NewStreamingComputePipelineExportController(config.ClusterUID, store, kubeModelSource, res, config.Compression)
+			kubeModelController, err = NewStreamingKubeModelComputePipelineExportController(config.AppName, config.ClusterUID, store, kubeModelSource, res, config.Compression)
 		} else {
 		} else {
-			kubeModelController, err = NewComputePipelineExportController(config.ClusterUID, store, kubeModelSource, res)
+			kubeModelController, err = NewKubeModelComputePipelineExportController(config.AppName, config.ClusterUID, store, kubeModelSource, res)
 		}
 		}
+
 		if err != nil {
 		if err != nil {
 			log.Errorf("Failed to create KubeModel export controller for resolution: %s - %v", timeutil.DurationString(res), err)
 			log.Errorf("Failed to create KubeModel export controller for resolution: %s - %v", timeutil.DurationString(res), err)
 			continue
 			continue
@@ -199,10 +202,12 @@ func (pec *PipelineExportControllers) Start(interval time.Duration) {
 	pec.AllocationExportController.Start(interval)
 	pec.AllocationExportController.Start(interval)
 	pec.AssetExportController.Start(interval)
 	pec.AssetExportController.Start(interval)
 	pec.NetworkInsightExportController.Start(interval)
 	pec.NetworkInsightExportController.Start(interval)
+	pec.KubeModelExportController.Start(interval)
 }
 }
 
 
 func (pec *PipelineExportControllers) Stop() {
 func (pec *PipelineExportControllers) Stop() {
 	pec.AllocationExportController.Stop()
 	pec.AllocationExportController.Stop()
 	pec.AssetExportController.Stop()
 	pec.AssetExportController.Stop()
 	pec.NetworkInsightExportController.Stop()
 	pec.NetworkInsightExportController.Stop()
+	pec.KubeModelExportController.Stop()
 }
 }

+ 48 - 37
core/pkg/opencost/exporter/exporter_test.go

@@ -15,11 +15,14 @@ import (
 	"github.com/opencost/opencost/core/pkg/pipelines"
 	"github.com/opencost/opencost/core/pkg/pipelines"
 	"github.com/opencost/opencost/core/pkg/source"
 	"github.com/opencost/opencost/core/pkg/source"
 	"github.com/opencost/opencost/core/pkg/storage"
 	"github.com/opencost/opencost/core/pkg/storage"
+	"github.com/opencost/opencost/core/pkg/util/timeutil"
 )
 )
 
 
 const (
 const (
-	TestClusterId  = "test-cluster"
-	TestResolution = 24 * time.Hour
+	TestAppName     = "test-app"
+	TestClusterID   = "test-cluster-id"
+	TestClusterName = "test-cluster"
+	TestResolution  = 24 * time.Hour
 )
 )
 
 
 type GenerateMockSet[T any] func(start, end time.Time) *T
 type GenerateMockSet[T any] func(start, end time.Time) *T
@@ -63,7 +66,7 @@ func NewMockNetworkInsightSource() exporter.ComputeSource[opencost.NetworkInsigh
 func NewMockKubeModelSource() exporter.ComputeSource[kubemodel.KubeModelSet] {
 func NewMockKubeModelSource() exporter.ComputeSource[kubemodel.KubeModelSet] {
 	return &MockSource[kubemodel.KubeModelSet]{
 	return &MockSource[kubemodel.KubeModelSet]{
 		generate: func(start, end time.Time) *kubemodel.KubeModelSet {
 		generate: func(start, end time.Time) *kubemodel.KubeModelSet {
-			return opencost.GenerateMockKubeModelSet(start, end)
+			return kubemodel.NewMockKubeModelSet(start, end)
 		},
 		},
 	}
 	}
 }
 }
@@ -164,12 +167,12 @@ func TestExporters(t *testing.T) {
 	t.Run("allocation exporter", func(t *testing.T) {
 	t.Run("allocation exporter", func(t *testing.T) {
 		allocSource := NewMockAllocationSource()
 		allocSource := NewMockAllocationSource()
 		memStore := storage.NewMemoryStorage()
 		memStore := storage.NewMemoryStorage()
-		p, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.AllocationPipelineName, ptr(TestResolution))
+		p, err := pathing.NewDefaultStoragePathFormatter(TestClusterName, pipelines.AllocationPipelineName, ptr(TestResolution))
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create path formatter: %v", err)
 			t.Fatalf("failed to create path formatter: %v", err)
 		}
 		}
 
 
-		allocExporter, err := NewComputePipelineExporter[opencost.AllocationSet](TestClusterId, TestResolution, memStore)
+		allocExporter, err := NewComputePipelineExporter[opencost.AllocationSet](TestClusterName, TestResolution, memStore)
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create allocation exporter: %v", err)
 			t.Fatalf("failed to create allocation exporter: %v", err)
 		}
 		}
@@ -187,18 +190,18 @@ func TestExporters(t *testing.T) {
 			t.Fatalf("failed to export allocation data: %v", err)
 			t.Fatalf("failed to export allocation data: %v", err)
 		}
 		}
 
 
-		validateFileCreation[opencost.AllocationSet](t, memStore, p, start, end)
+		validateFileCreation[opencost.AllocationSet](t, memStore, p, "", start, end)
 	})
 	})
 
 
 	t.Run("asset exporter", func(t *testing.T) {
 	t.Run("asset exporter", func(t *testing.T) {
 		assetSource := NewMockAssetSource()
 		assetSource := NewMockAssetSource()
 		memStore := storage.NewMemoryStorage()
 		memStore := storage.NewMemoryStorage()
-		p, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.AssetsPipelineName, ptr(TestResolution))
+		p, err := pathing.NewDefaultStoragePathFormatter(TestClusterName, pipelines.AssetsPipelineName, ptr(TestResolution))
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create path formatter: %v", err)
 			t.Fatalf("failed to create path formatter: %v", err)
 		}
 		}
 
 
-		assetExporter, err := NewComputePipelineExporter[opencost.AssetSet](TestClusterId, TestResolution, memStore)
+		assetExporter, err := NewComputePipelineExporter[opencost.AssetSet](TestClusterName, TestResolution, memStore)
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create allocation exporter: %v", err)
 			t.Fatalf("failed to create allocation exporter: %v", err)
 		}
 		}
@@ -216,18 +219,18 @@ func TestExporters(t *testing.T) {
 			t.Fatalf("failed to export asset data: %v", err)
 			t.Fatalf("failed to export asset data: %v", err)
 		}
 		}
 
 
-		validateFileCreation[opencost.AssetSet](t, memStore, p, start, end)
+		validateFileCreation[opencost.AssetSet](t, memStore, p, "", start, end)
 	})
 	})
 
 
 	t.Run("network insight exporter", func(t *testing.T) {
 	t.Run("network insight exporter", func(t *testing.T) {
 		netInsightSource := NewMockNetworkInsightSource()
 		netInsightSource := NewMockNetworkInsightSource()
 		memStore := storage.NewMemoryStorage()
 		memStore := storage.NewMemoryStorage()
-		p, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.NetworkInsightPipelineName, ptr(TestResolution))
+		p, err := pathing.NewDefaultStoragePathFormatter(TestClusterName, pipelines.NetworkInsightPipelineName, ptr(TestResolution))
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create path formatter: %v", err)
 			t.Fatalf("failed to create path formatter: %v", err)
 		}
 		}
 
 
-		netInsightExporter, err := NewComputePipelineExporter[opencost.NetworkInsightSet](TestClusterId, TestResolution, memStore)
+		netInsightExporter, err := NewComputePipelineExporter[opencost.NetworkInsightSet](TestClusterName, TestResolution, memStore)
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create net insights exporter: %v", err)
 			t.Fatalf("failed to create net insights exporter: %v", err)
 		}
 		}
@@ -245,18 +248,19 @@ func TestExporters(t *testing.T) {
 			t.Fatalf("failed to export net insights data: %v", err)
 			t.Fatalf("failed to export net insights data: %v", err)
 		}
 		}
 
 
-		validateFileCreation[opencost.NetworkInsightSet](t, memStore, p, start, end)
+		validateFileCreation[opencost.NetworkInsightSet](t, memStore, p, "", start, end)
 	})
 	})
 
 
 	t.Run("KubeModel exporter", func(t *testing.T) {
 	t.Run("KubeModel exporter", func(t *testing.T) {
 		kubeModelSource := NewMockKubeModelSource()
 		kubeModelSource := NewMockKubeModelSource()
 		memStore := storage.NewMemoryStorage()
 		memStore := storage.NewMemoryStorage()
-		p, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.KubeModelPipelineName, ptr(TestResolution))
+		res := timeutil.FormatStoreResolution(TestResolution)
+		p, err := pathing.NewKubeModelStoragePathFormatter(TestAppName, TestClusterID, res)
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create path formatter: %v", err)
 			t.Fatalf("failed to create path formatter: %v", err)
 		}
 		}
 
 
-		kubeModelExporter, err := NewComputePipelineExporter[kubemodel.KubeModelSet](TestClusterId, TestResolution, memStore)
+		kubeModelExporter, err := NewKubeModelComputePipelineExporter[kubemodel.KubeModelSet](TestAppName, TestClusterID, TestResolution, memStore)
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create KubeModel exporter: %v", err)
 			t.Fatalf("failed to create KubeModel exporter: %v", err)
 		}
 		}
@@ -274,14 +278,14 @@ func TestExporters(t *testing.T) {
 			t.Fatalf("failed to export KubeModel data: %v", err)
 			t.Fatalf("failed to export KubeModel data: %v", err)
 		}
 		}
 
 
-		validateFileCreation[kubemodel.KubeModelSet](t, memStore, p, start, end)
+		validateFileCreation[kubemodel.KubeModelSet](t, memStore, p, exporter.BingenExt, start, end)
 	})
 	})
 
 
 	t.Run("unknown exporter", func(t *testing.T) {
 	t.Run("unknown exporter", func(t *testing.T) {
 		memStore := storage.NewMemoryStorage()
 		memStore := storage.NewMemoryStorage()
 
 
 		// Invalid pipeline
 		// Invalid pipeline
-		_, err := NewComputePipelineExporter[UnknownSet](TestClusterId, TestResolution, memStore)
+		_, err := NewComputePipelineExporter[UnknownSet](TestClusterName, TestResolution, memStore)
 		if err == nil {
 		if err == nil {
 			t.Fatalf("expected error creating unknown pipeline exporter, got nil")
 			t.Fatalf("expected error creating unknown pipeline exporter, got nil")
 		}
 		}
@@ -300,8 +304,9 @@ func TestPipelineExportControllers(t *testing.T) {
 		memStore := storage.NewMemoryStorage()
 		memStore := storage.NewMemoryStorage()
 
 
 		exportControllers := NewPipelineExportControllers(memStore, pipelineComputeSource, PipelinesExportConfig{
 		exportControllers := NewPipelineExportControllers(memStore, pipelineComputeSource, PipelinesExportConfig{
-			ClusterUID:                        TestClusterId,
-			ClusterName:                       TestClusterId,
+			AppName:                           TestAppName,
+			ClusterUID:                        TestClusterID,
+			ClusterName:                       TestClusterName,
 			AllocationPiplineResolutions:      []time.Duration{TestResolution},
 			AllocationPiplineResolutions:      []time.Duration{TestResolution},
 			AssetPipelineResolutons:           []time.Duration{TestResolution},
 			AssetPipelineResolutons:           []time.Duration{TestResolution},
 			NetworkInsightPipelineResolutions: []time.Duration{TestResolution},
 			NetworkInsightPipelineResolutions: []time.Duration{TestResolution},
@@ -315,22 +320,22 @@ func TestPipelineExportControllers(t *testing.T) {
 		time.Sleep(time.Second + (750 * time.Millisecond))
 		time.Sleep(time.Second + (750 * time.Millisecond))
 		exportControllers.Stop()
 		exportControllers.Stop()
 
 
-		allocPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.AllocationPipelineName, ptr(TestResolution))
+		allocPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterName, pipelines.AllocationPipelineName, ptr(TestResolution))
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create allocations path formatter: %v", err)
 			t.Fatalf("failed to create allocations path formatter: %v", err)
 		}
 		}
-		assetPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.AssetsPipelineName, ptr(TestResolution))
+		assetPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterName, pipelines.AssetsPipelineName, ptr(TestResolution))
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create assets path formatter: %v", err)
 			t.Fatalf("failed to create assets path formatter: %v", err)
 		}
 		}
-		netPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.NetworkInsightPipelineName, ptr(TestResolution))
+		netPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterName, pipelines.NetworkInsightPipelineName, ptr(TestResolution))
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create net insights path formatter: %v", err)
 			t.Fatalf("failed to create net insights path formatter: %v", err)
 		}
 		}
 
 
-		validateFileCreation[opencost.AllocationSet](t, memStore, allocPath, start, end)
-		validateFileCreation[opencost.AssetSet](t, memStore, assetPath, start, end)
-		validateFileCreation[opencost.NetworkInsightSet](t, memStore, netPath, start, end)
+		validateFileCreation[opencost.AllocationSet](t, memStore, allocPath, "", start, end)
+		validateFileCreation[opencost.AssetSet](t, memStore, assetPath, "", start, end)
+		validateFileCreation[opencost.NetworkInsightSet](t, memStore, netPath, "", start, end)
 	})
 	})
 
 
 	t.Run("with auto-set to minute resolution", func(t *testing.T) {
 	t.Run("with auto-set to minute resolution", func(t *testing.T) {
@@ -338,8 +343,9 @@ func TestPipelineExportControllers(t *testing.T) {
 		memStore := storage.NewMemoryStorage()
 		memStore := storage.NewMemoryStorage()
 
 
 		exportControllers := NewPipelineExportControllers(memStore, pipelineComputeSource, PipelinesExportConfig{
 		exportControllers := NewPipelineExportControllers(memStore, pipelineComputeSource, PipelinesExportConfig{
-			ClusterUID:                        TestClusterId,
-			ClusterName:                       TestClusterId,
+			AppName:                           TestAppName,
+			ClusterUID:                        TestClusterID,
+			ClusterName:                       TestClusterName,
 			AllocationPiplineResolutions:      []time.Duration{TestResolution},
 			AllocationPiplineResolutions:      []time.Duration{TestResolution},
 			AssetPipelineResolutons:           []time.Duration{TestResolution},
 			AssetPipelineResolutons:           []time.Duration{TestResolution},
 			NetworkInsightPipelineResolutions: []time.Duration{TestResolution},
 			NetworkInsightPipelineResolutions: []time.Duration{TestResolution},
@@ -353,29 +359,29 @@ func TestPipelineExportControllers(t *testing.T) {
 		time.Sleep(time.Second + (750 * time.Millisecond))
 		time.Sleep(time.Second + (750 * time.Millisecond))
 		exportControllers.Stop()
 		exportControllers.Stop()
 
 
-		allocPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.AllocationPipelineName, ptr(TestResolution))
+		allocPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterName, pipelines.AllocationPipelineName, ptr(TestResolution))
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create allocations path formatter: %v", err)
 			t.Fatalf("failed to create allocations path formatter: %v", err)
 		}
 		}
-		assetPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.AssetsPipelineName, ptr(TestResolution))
+		assetPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterName, pipelines.AssetsPipelineName, ptr(TestResolution))
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create assets path formatter: %v", err)
 			t.Fatalf("failed to create assets path formatter: %v", err)
 		}
 		}
-		netPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.NetworkInsightPipelineName, ptr(TestResolution))
+		netPath, err := pathing.NewDefaultStoragePathFormatter(TestClusterName, pipelines.NetworkInsightPipelineName, ptr(TestResolution))
 		if err != nil {
 		if err != nil {
 			t.Fatalf("failed to create net insights path formatter: %v", err)
 			t.Fatalf("failed to create net insights path formatter: %v", err)
 		}
 		}
 
 
-		validateFileCreation[opencost.AllocationSet](t, memStore, allocPath, start, end)
-		validateFileCreation[opencost.AssetSet](t, memStore, assetPath, start, end)
-		validateFileCreation[opencost.NetworkInsightSet](t, memStore, netPath, start, end)
+		validateFileCreation[opencost.AllocationSet](t, memStore, allocPath, "", start, end)
+		validateFileCreation[opencost.AssetSet](t, memStore, assetPath, "", start, end)
+		validateFileCreation[opencost.NetworkInsightSet](t, memStore, netPath, "", start, end)
 	})
 	})
 
 
 	t.Run("with default export config", func(t *testing.T) {
 	t.Run("with default export config", func(t *testing.T) {
 		pipelineComputeSource := NewMockPipelineComputeSource()
 		pipelineComputeSource := NewMockPipelineComputeSource()
 		memStore := storage.NewMemoryStorage()
 		memStore := storage.NewMemoryStorage()
 
 
-		exportControllers := NewPipelineExportControllers(memStore, pipelineComputeSource, NewPipelinesExportConfig(TestClusterId, TestClusterId))
+		exportControllers := NewPipelineExportControllers(memStore, pipelineComputeSource, NewPipelinesExportConfig(TestAppName, TestClusterID, TestClusterName))
 
 
 		if len(exportControllers.AllocationExportController.Resolutions()) != 2 {
 		if len(exportControllers.AllocationExportController.Resolutions()) != 2 {
 			t.Fatalf("expected 2 allocation resolutions, got %d", len(exportControllers.AllocationExportController.Resolutions()))
 			t.Fatalf("expected 2 allocation resolutions, got %d", len(exportControllers.AllocationExportController.Resolutions()))
@@ -393,7 +399,7 @@ func TestPipelineExportControllers(t *testing.T) {
 		pipelineComputeSource := NewMockPipelineComputeSourceWith(48 * time.Hour)
 		pipelineComputeSource := NewMockPipelineComputeSourceWith(48 * time.Hour)
 		memStore := storage.NewMemoryStorage()
 		memStore := storage.NewMemoryStorage()
 
 
-		exportControllers := NewPipelineExportControllers(memStore, pipelineComputeSource, NewPipelinesExportConfig(TestClusterId, TestClusterId))
+		exportControllers := NewPipelineExportControllers(memStore, pipelineComputeSource, NewPipelinesExportConfig(TestAppName, TestClusterID, TestClusterName))
 
 
 		if len(exportControllers.AllocationExportController.Resolutions()) != 0 {
 		if len(exportControllers.AllocationExportController.Resolutions()) != 0 {
 			t.Fatalf("expected 0 allocation resolutions, got %d", len(exportControllers.AllocationExportController.Resolutions()))
 			t.Fatalf("expected 0 allocation resolutions, got %d", len(exportControllers.AllocationExportController.Resolutions()))
@@ -410,7 +416,7 @@ func TestPipelineExportControllers(t *testing.T) {
 		pipelineComputeSource := NewMockPipelineComputeSource()
 		pipelineComputeSource := NewMockPipelineComputeSource()
 		memStore := storage.NewMemoryStorage()
 		memStore := storage.NewMemoryStorage()
 
 
-		exportControllers := NewPipelineExportControllers(memStore, pipelineComputeSource, NewPipelinesExportConfig("", ""))
+		exportControllers := NewPipelineExportControllers(memStore, pipelineComputeSource, NewPipelinesExportConfig("", "", ""))
 
 
 		if len(exportControllers.AllocationExportController.Resolutions()) != 0 {
 		if len(exportControllers.AllocationExportController.Resolutions()) != 0 {
 			t.Fatalf("expected 0 allocation resolutions, got %d", len(exportControllers.AllocationExportController.Resolutions()))
 			t.Fatalf("expected 0 allocation resolutions, got %d", len(exportControllers.AllocationExportController.Resolutions()))
@@ -425,10 +431,15 @@ func TestPipelineExportControllers(t *testing.T) {
 }
 }
 
 
 // test helper function that will load a path from a storage implementation and ensure that the file is not empty and can be decoded, etc...
 // test helper function that will load a path from a storage implementation and ensure that the file is not empty and can be decoded, etc...
-func validateFileCreation[T any, U PipelineData[T]](t *testing.T, memStore storage.Storage, p pathing.StoragePathFormatter[opencost.Window], start, end time.Time) {
+func validateFileCreation[T any, U PipelineData[T]](
+	t *testing.T,
+	memStore storage.Storage,
+	p pathing.StoragePathFormatter[opencost.Window],
+	ext string,
+	start, end time.Time) {
 	t.Helper()
 	t.Helper()
 
 
-	expectedPath := p.ToFullPath("", opencost.NewClosedWindow(start, end), "")
+	expectedPath := p.ToFullPath("", opencost.NewClosedWindow(start, end), ext)
 
 
 	fileContents, err := memStore.Read(expectedPath)
 	fileContents, err := memStore.Read(expectedPath)
 	if err != nil {
 	if err != nil {

+ 113 - 5
core/pkg/opencost/exporter/exporters.go

@@ -10,6 +10,7 @@ import (
 	"github.com/opencost/opencost/core/pkg/exporter/validator"
 	"github.com/opencost/opencost/core/pkg/exporter/validator"
 	"github.com/opencost/opencost/core/pkg/pipelines"
 	"github.com/opencost/opencost/core/pkg/pipelines"
 	"github.com/opencost/opencost/core/pkg/storage"
 	"github.com/opencost/opencost/core/pkg/storage"
+	"github.com/opencost/opencost/core/pkg/util/timeutil"
 	"github.com/opencost/opencost/core/pkg/util/typeutil"
 	"github.com/opencost/opencost/core/pkg/util/typeutil"
 )
 )
 
 
@@ -38,7 +39,7 @@ const (
 // NewComputePipelineExporter creates a new `ComputeExporter[T]` instance which is used to export computed data
 // NewComputePipelineExporter creates a new `ComputeExporter[T]` instance which is used to export computed data
 // by window for a specific pipeline.
 // by window for a specific pipeline.
 func NewComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
 func NewComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
-	clusterId string,
+	clusterName string,
 	resolution time.Duration,
 	resolution time.Duration,
 	store storage.Storage,
 	store storage.Storage,
 ) (export.ComputeExporter[T], error) {
 ) (export.ComputeExporter[T], error) {
@@ -47,14 +48,17 @@ func NewComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T], S validat
 		return nil, fmt.Errorf("failed to extract pipeline name for type: %s", typeutil.TypeOf[T]())
 		return nil, fmt.Errorf("failed to extract pipeline name for type: %s", typeutil.TypeOf[T]())
 	}
 	}
 
 
-	pathing, err := pathing.NewDefaultStoragePathFormatter(clusterId, pipelineName, &resolution)
+	pathing, err := pathing.NewDefaultStoragePathFormatter(clusterName, pipelineName, &resolution)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("failed to create path formatter: %w", err)
 		return nil, fmt.Errorf("failed to create path formatter: %w", err)
 	}
 	}
 
 
+	var encoder export.Encoder[T]
+	encoder = export.NewBingenEncoder[T, U]()
+
 	return export.NewComputeStorageExporter(
 	return export.NewComputeStorageExporter(
 		pathing,
 		pathing,
-		export.NewBingenEncoder[T, U](),
+		encoder,
 		store,
 		store,
 		validator.NewSetValidator[T, S](resolution),
 		validator.NewSetValidator[T, S](resolution),
 		false,
 		false,
@@ -102,12 +106,12 @@ func NewStreamingComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T],
 // NewComputePipelineExportController creates a new `ComputeExportController[T]` instance which is used to export computed data
 // NewComputePipelineExportController creates a new `ComputeExportController[T]` instance which is used to export computed data
 // using the provided source, storage, resolution, and source resolution.
 // using the provided source, storage, resolution, and source resolution.
 func NewComputePipelineExportController[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
 func NewComputePipelineExportController[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
-	clusterId string,
+	clusterName string,
 	store storage.Storage,
 	store storage.Storage,
 	source export.ComputeSource[T],
 	source export.ComputeSource[T],
 	resolution time.Duration,
 	resolution time.Duration,
 ) (*export.ComputeExportController[T], error) {
 ) (*export.ComputeExportController[T], error) {
-	exporter, err := NewComputePipelineExporter[T, U, S](clusterId, resolution, store)
+	exporter, err := NewComputePipelineExporter[T, U, S](clusterName, resolution, store)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("failed to create compute exporter: %w", err)
 		return nil, fmt.Errorf("failed to create compute exporter: %w", err)
 	}
 	}
@@ -131,3 +135,107 @@ func NewStreamingComputePipelineExportController[T any, U export.BinaryMarshaler
 
 
 	return export.NewComputeExportController(source, exporter, resolution), nil
 	return export.NewComputeExportController(source, exporter, resolution), nil
 }
 }
+
+// NewKubeModelComputePipelineExporter creates a new `ComputeExporter[T]` instance which is used to export computed data
+// by window for a specific pipeline.
+func NewKubeModelComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
+	appName string,
+	clusterId string,
+	resolution time.Duration,
+	store storage.Storage,
+) (export.ComputeExporter[T], error) {
+	pipelineName := pipelines.NameFor[T]()
+	if pipelineName == "" {
+		return nil, fmt.Errorf("failed to extract pipeline name for type: %s", typeutil.TypeOf[T]())
+	}
+	res := timeutil.FormatStoreResolution(resolution)
+	pathing, err := pathing.NewKubeModelStoragePathFormatter(appName, clusterId, res)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create path formatter: %w", err)
+	}
+
+	encoder := export.NewBingenFileEncoder[T, U]()
+
+	return export.NewComputeStorageExporter(
+		pathing,
+		encoder,
+		store,
+		validator.NewSetValidator[T, S](resolution),
+		false,
+	), nil
+}
+
+// NewStreamingComputePipelineExporter creates a new `ComputeExporter[T]` instance which is used to export computed data
+// by window for a specific pipeline.
+func NewStreamingKubeModelComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
+	appName string,
+	clusterId string,
+	resolution time.Duration,
+	store storage.Storage,
+	compressionLevel ExportCompressionLevel,
+) (export.ComputeExporter[T], error) {
+	pipelineName := pipelines.NameFor[T]()
+	if pipelineName == "" {
+		return nil, fmt.Errorf("failed to extract pipeline name for type: %s", typeutil.TypeOf[T]())
+	}
+
+	res := timeutil.FormatStoreResolution(resolution)
+	pathing, err := pathing.NewKubeModelStoragePathFormatter(appName, clusterId, res)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create path formatter: %w", err)
+	}
+
+	if !compressionLevel.IsValid() {
+		return nil, fmt.Errorf("invalid compression level passed: %d is not a valid compression level", int(compressionLevel))
+	}
+
+	var encoder export.Encoder[T]
+	if compressionLevel != ExportCompressionLevelNone {
+		encoder = export.NewGZipEncoderWithLevel(export.NewBingenEncoder[T, U](), int(compressionLevel))
+	} else {
+		encoder = export.NewBingenEncoder[T, U]()
+	}
+
+	return export.NewComputeStorageExporter(
+		pathing,
+		encoder,
+		store,
+		validator.NewSetValidator[T, S](resolution),
+		true,
+	), nil
+}
+
+// NewKubeModelComputePipelineExportController creates a new `ComputeExportController[T]` instance which is used to export computed data
+// using the provided source, storage, resolution, and source resolution.
+func NewKubeModelComputePipelineExportController[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
+	appName string,
+	clusterId string,
+	store storage.Storage,
+	source export.ComputeSource[T],
+	resolution time.Duration,
+) (*export.ComputeExportController[T], error) {
+	exporter, err := NewKubeModelComputePipelineExporter[T, U, S](appName, clusterId, resolution, store)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create compute exporter: %w", err)
+	}
+
+	return export.NewComputeExportController(source, exporter, resolution), nil
+}
+
+// NewStreamingComputePipelineExportController creates a new `ComputeExportController[T]` instance which is used to stream/export the
+// computed data using the provided source, storage, resolution, and source resolution.
+func NewStreamingKubeModelComputePipelineExportController[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
+	appName string,
+	clusterId string,
+	store storage.Storage,
+	source export.ComputeSource[T],
+	resolution time.Duration,
+	compressionLevel ExportCompressionLevel,
+) (*export.ComputeExportController[T], error) {
+	exporter, err := NewStreamingKubeModelComputePipelineExporter[T, U, S](appName, clusterId, resolution, store, compressionLevel)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create compute exporter: %w", err)
+	}
+
+	return export.NewComputeExportController(source, exporter, resolution), nil
+}

+ 0 - 22
core/pkg/opencost/mock.go

@@ -3,8 +3,6 @@ package opencost
 import (
 import (
 	"fmt"
 	"fmt"
 	"time"
 	"time"
-
-	"github.com/opencost/opencost/core/pkg/model/kubemodel"
 )
 )
 
 
 const gb = 1024 * 1024 * 1024
 const gb = 1024 * 1024 * 1024
@@ -1015,23 +1013,3 @@ func GenerateMockCloudCostSet(start, end time.Time, provider, integration string
 
 
 	return ccs
 	return ccs
 }
 }
-
-// GenerateMockKubeModelSet creates generic KubeModel set
-func GenerateMockKubeModelSet(start, end time.Time) *kubemodel.KubeModelSet {
-	kms := kubemodel.NewKubeModelSet(start, end)
-
-	kms.Cluster = &kubemodel.Cluster{
-		UID:  "clusterUID",
-		Name: "cluster",
-	}
-
-	kms.RegisterNamespace("namespace-1", "namespace-1")
-	kms.RegisterNamespace("namespace-2", "namespace-2")
-
-	kms.RegisterResourceQuota("resourcequota-1", "resourcequota-1", "namespace-1")
-	kms.RegisterResourceQuota("resourcequota-2", "resourcequota-2", "namespace-1")
-	kms.RegisterResourceQuota("resourcequota-3", "resourcequota-3", "namespace-2")
-	kms.RegisterResourceQuota("resourcequota-4", "resourcequota-4", "namespace-2")
-
-	return kms
-}

+ 100 - 24
core/pkg/source/datasource.go

@@ -10,9 +10,6 @@ import (
 
 
 type MetricsQuerier interface {
 type MetricsQuerier interface {
 	// Cluster Disks
 	// Cluster Disks
-	QueryPVActiveMinutes(start, end time.Time) *Future[PVActiveMinutesResult]
-	QueryPVUsedAverage(start, end time.Time) *Future[PVUsedAvgResult]
-	QueryPVUsedMax(start, end time.Time) *Future[PVUsedMaxResult]
 
 
 	// Local Cluster Disks
 	// Local Cluster Disks
 	QueryLocalStorageActiveMinutes(start, end time.Time) *Future[LocalStorageActiveMinutesResult]
 	QueryLocalStorageActiveMinutes(start, end time.Time) *Future[LocalStorageActiveMinutesResult]
@@ -20,7 +17,13 @@ type MetricsQuerier interface {
 	QueryLocalStorageUsedMax(start, end time.Time) *Future[LocalStorageUsedMaxResult]
 	QueryLocalStorageUsedMax(start, end time.Time) *Future[LocalStorageUsedMaxResult]
 	QueryLocalStorageBytes(start, end time.Time) *Future[LocalStorageBytesResult]
 	QueryLocalStorageBytes(start, end time.Time) *Future[LocalStorageBytesResult]
 
 
+	QueryKMLocalStorageUsedAvg(start, end time.Time) *Future[NodeUIDValueResult]
+	QueryKMLocalStorageUsedMax(start, end time.Time) *Future[NodeUIDValueResult]
+	QueryKMLocalStorageBytes(start, end time.Time) *Future[UIDValueResult]
+
 	// Nodes
 	// Nodes
+	QueryNodeInfo(start, end time.Time) *Future[NodeInfoResult]
+	QueryNodeUptime(start, end time.Time) *Future[UptimeResult]
 	QueryNodeActiveMinutes(start, end time.Time) *Future[NodeActiveMinutesResult]
 	QueryNodeActiveMinutes(start, end time.Time) *Future[NodeActiveMinutesResult]
 	QueryNodeCPUCoresCapacity(start, end time.Time) *Future[NodeCPUCoresCapacityResult]
 	QueryNodeCPUCoresCapacity(start, end time.Time) *Future[NodeCPUCoresCapacityResult]
 	QueryNodeCPUCoresAllocatable(start, end time.Time) *Future[NodeCPUCoresAllocatableResult]
 	QueryNodeCPUCoresAllocatable(start, end time.Time) *Future[NodeCPUCoresAllocatableResult]
@@ -31,12 +34,15 @@ type MetricsQuerier interface {
 	QueryNodeIsSpot(start, end time.Time) *Future[NodeIsSpotResult]
 	QueryNodeIsSpot(start, end time.Time) *Future[NodeIsSpotResult]
 	QueryNodeRAMSystemPercent(start, end time.Time) *Future[NodeRAMSystemPercentResult]
 	QueryNodeRAMSystemPercent(start, end time.Time) *Future[NodeRAMSystemPercentResult]
 	QueryNodeRAMUserPercent(start, end time.Time) *Future[NodeRAMUserPercentResult]
 	QueryNodeRAMUserPercent(start, end time.Time) *Future[NodeRAMUserPercentResult]
+	QueryNodeResourceCapacities(start, end time.Time) *Future[ResourceResult]
+	QueryNodeResourcesAllocatable(start, end time.Time) *Future[ResourceResult]
 
 
 	// Load Balancers
 	// Load Balancers
 	QueryLBActiveMinutes(start, end time.Time) *Future[LBActiveMinutesResult]
 	QueryLBActiveMinutes(start, end time.Time) *Future[LBActiveMinutesResult]
 	QueryLBPricePerHr(start, end time.Time) *Future[LBPricePerHrResult]
 	QueryLBPricePerHr(start, end time.Time) *Future[LBPricePerHrResult]
 
 
 	// Cluster Management
 	// Cluster Management
+	QueryClusterInfo(start, end time.Time) *Future[ClusterInfoResult]
 	QueryClusterUptime(start, end time.Time) *Future[UptimeResult]
 	QueryClusterUptime(start, end time.Time) *Future[UptimeResult]
 	QueryClusterManagementDuration(start, end time.Time) *Future[ClusterManagementDurationResult]
 	QueryClusterManagementDuration(start, end time.Time) *Future[ClusterManagementDurationResult]
 	QueryClusterManagementPricePerHr(start, end time.Time) *Future[ClusterManagementPricePerHrResult]
 	QueryClusterManagementPricePerHr(start, end time.Time) *Future[ClusterManagementPricePerHrResult]
@@ -44,6 +50,17 @@ type MetricsQuerier interface {
 	// Pods
 	// Pods
 	QueryPods(start, end time.Time) *Future[PodsResult]
 	QueryPods(start, end time.Time) *Future[PodsResult]
 	QueryPodsUID(start, end time.Time) *Future[PodsResult]
 	QueryPodsUID(start, end time.Time) *Future[PodsResult]
+	QueryPodInfo(start, end time.Time) *Future[PodInfoResult]
+	QueryPodUptime(start, end time.Time) *Future[UptimeResult]
+	QueryPodOwners(start, end time.Time) *Future[OwnerResult]
+	QueryPodPVCVolumes(start, end time.Time) *Future[PodPVCVolumeResult]
+	QueryPodNetworkEgressBytes(start, end time.Time) *Future[PodNetworkBytesResult]
+	QueryPodNetworkIngressBytes(start, end time.Time) *Future[PodNetworkBytesResult]
+
+	// Container
+	QueryContainerUptime(start, end time.Time) *Future[ContainerUptimeResult]
+	QueryContainerResourceRequests(start, end time.Time) *Future[ContainerResourceResult]
+	QueryContainerResourceLimits(start, end time.Time) *Future[ContainerResourceResult]
 
 
 	// RAM
 	// RAM
 	QueryRAMBytesAllocated(start, end time.Time) *Future[RAMBytesAllocatedResult]
 	QueryRAMBytesAllocated(start, end time.Time) *Future[RAMBytesAllocatedResult]
@@ -70,19 +87,79 @@ type MetricsQuerier interface {
 	QueryGPUInfo(start, end time.Time) *Future[GPUInfoResult]
 	QueryGPUInfo(start, end time.Time) *Future[GPUInfoResult]
 	QueryIsGPUShared(start, end time.Time) *Future[IsGPUSharedResult]
 	QueryIsGPUShared(start, end time.Time) *Future[IsGPUSharedResult]
 
 
+	// Device
+	QueryDCGMDeviceInfo(start, end time.Time) *Future[DCGMDeviceInfoResult]
+	QueryDCGMDeviceUptime(start, end time.Time) *Future[DCGMDeviceUptimeResult]
+	QueryDCGMContainerUsageAvg(start, end time.Time) *Future[DCGMDeviceContainerUsageResult]
+	QueryDCGMContainerUsageMax(start, end time.Time) *Future[DCGMDeviceContainerUsageResult]
+
 	// PVC
 	// PVC
 	QueryPodPVCAllocation(start, end time.Time) *Future[PodPVCAllocationResult]
 	QueryPodPVCAllocation(start, end time.Time) *Future[PodPVCAllocationResult]
 	QueryPVCBytesRequested(start, end time.Time) *Future[PVCBytesRequestedResult]
 	QueryPVCBytesRequested(start, end time.Time) *Future[PVCBytesRequestedResult]
 	QueryPVCInfo(start, end time.Time) *Future[PVCInfoResult]
 	QueryPVCInfo(start, end time.Time) *Future[PVCInfoResult]
+	QueryKMPVCInfo(start, end time.Time) *Future[PVCInfoResult]
+	QueryPVCUptime(start, end time.Time) *Future[UptimeResult]
+	QueryPVCBytesUsedAverage(start, end time.Time) *Future[PVCUIDValueResult]
+	QueryPVCBytesUsedMax(start, end time.Time) *Future[PVCUIDValueResult]
 
 
 	// PV
 	// PV
 	QueryPVBytes(start, end time.Time) *Future[PVBytesResult]
 	QueryPVBytes(start, end time.Time) *Future[PVBytesResult]
 	QueryPVPricePerGiBHour(start, end time.Time) *Future[PVPricePerGiBHourResult]
 	QueryPVPricePerGiBHour(start, end time.Time) *Future[PVPricePerGiBHourResult]
 	QueryPVInfo(start, end time.Time) *Future[PVInfoResult]
 	QueryPVInfo(start, end time.Time) *Future[PVInfoResult]
+	QueryPVActiveMinutes(start, end time.Time) *Future[PVActiveMinutesResult]
+	QueryPVUsedAverage(start, end time.Time) *Future[PVUsedAvgResult]
+	QueryPVUsedMax(start, end time.Time) *Future[PVUsedMaxResult]
+	QueryKMPVInfo(start, end time.Time) *Future[PVInfoResult]
+	QueryPVUptime(start, end time.Time) *Future[UptimeResult]
+
+	// Deployment
+	QueryDeploymentInfo(start, end time.Time) *Future[DeploymentInfoResult]
+	QueryDeploymentUptime(start, end time.Time) *Future[UptimeResult]
+	QueryDeploymentLabels(start, end time.Time) *Future[LabelsResult]
+	QueryDeploymentAnnotations(start, end time.Time) *Future[AnnotationsResult]
+	QueryDeploymentMatchLabels(start, end time.Time) *Future[DeploymentLabelsResult]
+
+	// StatefulSet
+	QueryStatefulSetInfo(start, end time.Time) *Future[StatefulSetInfoResult]
+	QueryStatefulSetUptime(start, end time.Time) *Future[UptimeResult]
+	QueryStatefulSetLabels(start, end time.Time) *Future[LabelsResult]
+	QueryStatefulSetAnnotations(start, end time.Time) *Future[AnnotationsResult]
+	QueryStatefulSetMatchLabels(start, end time.Time) *Future[StatefulSetLabelsResult]
+
+	// DaemonSet
+	QueryDaemonSetInfo(start, end time.Time) *Future[DaemonSetInfoResult]
+	QueryDaemonSetUptime(start, end time.Time) *Future[UptimeResult]
+	QueryDaemonSetLabels(start, end time.Time) *Future[LabelsResult]
+	QueryDaemonSetAnnotations(start, end time.Time) *Future[AnnotationsResult]
+
+	// Job
+	QueryJobInfo(start, end time.Time) *Future[JobInfoResult]
+	QueryJobUptime(start, end time.Time) *Future[UptimeResult]
+	QueryJobLabels(start, end time.Time) *Future[LabelsResult]
+	QueryJobAnnotations(start, end time.Time) *Future[AnnotationsResult]
+
+	// CronJob
+	QueryCronJobInfo(start, end time.Time) *Future[CronJobInfoResult]
+	QueryCronJobUptime(start, end time.Time) *Future[UptimeResult]
+	QueryCronJobLabels(start, end time.Time) *Future[LabelsResult]
+	QueryCronJobAnnotations(start, end time.Time) *Future[AnnotationsResult]
+
+	// ReplicaSet
+	QueryReplicaSetInfo(start, end time.Time) *Future[ReplicaSetInfoResult]
+	QueryReplicaSetUptime(start, end time.Time) *Future[UptimeResult]
+	QueryReplicaSetLabels(start, end time.Time) *Future[LabelsResult]
+	QueryReplicaSetAnnotations(start, end time.Time) *Future[AnnotationsResult]
+	QueryReplicaSetOwners(start, end time.Time) *Future[OwnerResult]
 
 
 	// Namespace
 	// Namespace
+	QueryNamespaceInfo(start, end time.Time) *Future[NamespaceInfoResult]
 	QueryNamespaceUptime(start, end time.Time) *Future[UptimeResult]
 	QueryNamespaceUptime(start, end time.Time) *Future[UptimeResult]
 
 
+	// Service
+	QueryServiceInfo(start, end time.Time) *Future[ServiceInfoResult]
+	QueryServiceUptime(start, end time.Time) *Future[UptimeResult]
+	QueryServiceSelectorLabels(start, end time.Time) *Future[ServiceLabelsResult]
+
 	// Network Egress
 	// Network Egress
 	QueryNetZoneGiB(start, end time.Time) *Future[NetZoneGiBResult]
 	QueryNetZoneGiB(start, end time.Time) *Future[NetZoneGiBResult]
 	QueryNetZonePricePerGiB(start, end time.Time) *Future[NetZonePricePerGiBResult]
 	QueryNetZonePricePerGiB(start, end time.Time) *Future[NetZonePricePerGiBResult]
@@ -112,11 +189,9 @@ type MetricsQuerier interface {
 	QueryNodeLabels(start, end time.Time) *Future[NodeLabelsResult]
 	QueryNodeLabels(start, end time.Time) *Future[NodeLabelsResult]
 	QueryNamespaceLabels(start, end time.Time) *Future[NamespaceLabelsResult]
 	QueryNamespaceLabels(start, end time.Time) *Future[NamespaceLabelsResult]
 	QueryPodLabels(start, end time.Time) *Future[PodLabelsResult]
 	QueryPodLabels(start, end time.Time) *Future[PodLabelsResult]
-	QueryServiceLabels(start, end time.Time) *Future[ServiceLabelsResult]
-	QueryDeploymentLabels(start, end time.Time) *Future[DeploymentLabelsResult]
-	QueryStatefulSetLabels(start, end time.Time) *Future[StatefulSetLabelsResult]
-	QueryDaemonSetLabels(start, end time.Time) *Future[DaemonSetLabelsResult]
-	QueryJobLabels(start, end time.Time) *Future[JobLabelsResult]
+
+	QueryPodsWithDaemonSetOwner(start, end time.Time) *Future[PodsWithDaemonSetOwnerResult]
+	QueryPodsWithJobOwner(start, end time.Time) *Future[PodsWithJobOwnerResult]
 
 
 	// ReplicaSet -> Controller mapping
 	// ReplicaSet -> Controller mapping
 	QueryPodsWithReplicaSetOwner(start, end time.Time) *Future[PodsWithReplicaSetOwnerResult]
 	QueryPodsWithReplicaSetOwner(start, end time.Time) *Future[PodsWithReplicaSetOwnerResult]
@@ -124,23 +199,24 @@ type MetricsQuerier interface {
 	QueryReplicaSetsWithRollout(start, end time.Time) *Future[ReplicaSetsWithRolloutResult]
 	QueryReplicaSetsWithRollout(start, end time.Time) *Future[ReplicaSetsWithRolloutResult]
 
 
 	// ResourceQuotas
 	// ResourceQuotas
+	QueryResourceQuotaInfo(start, end time.Time) *Future[ResourceQuotaInfoResult]
 	QueryResourceQuotaUptime(start, end time.Time) *Future[UptimeResult]
 	QueryResourceQuotaUptime(start, end time.Time) *Future[UptimeResult]
-	QueryResourceQuotaSpecCPURequestAverage(start, end time.Time) *Future[ResourceQuotaSpecCPURequestAvgResult]
-	QueryResourceQuotaSpecCPURequestMax(start, end time.Time) *Future[ResourceQuotaSpecCPURequestMaxResult]
-	QueryResourceQuotaSpecRAMRequestAverage(start, end time.Time) *Future[ResourceQuotaSpecRAMRequestAvgResult]
-	QueryResourceQuotaSpecRAMRequestMax(start, end time.Time) *Future[ResourceQuotaSpecRAMRequestMaxResult]
-	QueryResourceQuotaSpecCPULimitAverage(start, end time.Time) *Future[ResourceQuotaSpecCPULimitAvgResult]
-	QueryResourceQuotaSpecCPULimitMax(start, end time.Time) *Future[ResourceQuotaSpecCPULimitMaxResult]
-	QueryResourceQuotaSpecRAMLimitAverage(start, end time.Time) *Future[ResourceQuotaSpecRAMLimitAvgResult]
-	QueryResourceQuotaSpecRAMLimitMax(start, end time.Time) *Future[ResourceQuotaSpecRAMLimitMaxResult]
-	QueryResourceQuotaStatusUsedCPURequestAverage(start, end time.Time) *Future[ResourceQuotaStatusUsedCPURequestAvgResult]
-	QueryResourceQuotaStatusUsedCPURequestMax(start, end time.Time) *Future[ResourceQuotaStatusUsedCPURequestMaxResult]
-	QueryResourceQuotaStatusUsedRAMRequestAverage(start, end time.Time) *Future[ResourceQuotaStatusUsedRAMRequestAvgResult]
-	QueryResourceQuotaStatusUsedRAMRequestMax(start, end time.Time) *Future[ResourceQuotaStatusUsedRAMRequestMaxResult]
-	QueryResourceQuotaStatusUsedCPULimitAverage(start, end time.Time) *Future[ResourceQuotaStatusUsedCPULimitAvgResult]
-	QueryResourceQuotaStatusUsedCPULimitMax(start, end time.Time) *Future[ResourceQuotaStatusUsedCPULimitMaxResult]
-	QueryResourceQuotaStatusUsedRAMLimitAverage(start, end time.Time) *Future[ResourceQuotaStatusUsedRAMLimitAvgResult]
-	QueryResourceQuotaStatusUsedRAMLimitMax(start, end time.Time) *Future[ResourceQuotaStatusUsedRAMLimitMaxResult]
+	QueryResourceQuotaSpecCPURequestAverage(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaSpecCPURequestMax(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaSpecRAMRequestAverage(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaSpecRAMRequestMax(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaSpecCPULimitAverage(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaSpecCPULimitMax(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaSpecRAMLimitAverage(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaSpecRAMLimitMax(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaStatusUsedCPURequestAverage(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaStatusUsedCPURequestMax(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaStatusUsedRAMRequestAverage(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaStatusUsedRAMRequestMax(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaStatusUsedCPULimitAverage(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaStatusUsedCPULimitMax(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaStatusUsedRAMLimitAverage(start, end time.Time) *Future[ResourceResult]
+	QueryResourceQuotaStatusUsedRAMLimitMax(start, end time.Time) *Future[ResourceResult]
 
 
 	// Data Coverage Query
 	// Data Coverage Query
 	QueryDataCoverage(limitDays int) (time.Time, time.Time, error)
 	QueryDataCoverage(limitDays int) (time.Time, time.Time, error)

+ 617 - 169
core/pkg/source/decoders.go

@@ -3,6 +3,7 @@ package source
 import (
 import (
 	"time"
 	"time"
 
 
+	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/util"
 	"github.com/opencost/opencost/core/pkg/util"
 )
 )
 
 
@@ -13,34 +14,49 @@ const (
 	RegionLabel          = "region"
 	RegionLabel          = "region"
 	ClusterIDLabel       = "cluster_id"
 	ClusterIDLabel       = "cluster_id"
 	NamespaceLabel       = "namespace"
 	NamespaceLabel       = "namespace"
+	NamespaceUIDLabel    = "namespace_uid"
 	NodeLabel            = "node"
 	NodeLabel            = "node"
+	NodeUIDLabel         = "node_uid"
 	InstanceLabel        = "instance"
 	InstanceLabel        = "instance"
 	InstanceTypeLabel    = "instance_type"
 	InstanceTypeLabel    = "instance_type"
 	ContainerLabel       = "container"
 	ContainerLabel       = "container"
 	PodLabel             = "pod"
 	PodLabel             = "pod"
+	PodUIDLabel          = "pod_uid"
 	PodNameLabel         = "pod_name"
 	PodNameLabel         = "pod_name"
+	PodVolumeNameLabel   = "pod_volume_name"
 	ProviderIDLabel      = "provider_id"
 	ProviderIDLabel      = "provider_id"
 	DeviceLabel          = "device"
 	DeviceLabel          = "device"
 	PVCLabel             = "persistentvolumeclaim"
 	PVCLabel             = "persistentvolumeclaim"
+	PVCUIDLabel          = "persistentvolumeclaim_uid"
 	PVLabel              = "persistentvolume"
 	PVLabel              = "persistentvolume"
+	CSIVolumeHandleLabel = "csi_volume_handle"
 	StorageClassLabel    = "storageclass"
 	StorageClassLabel    = "storageclass"
 	VolumeNameLabel      = "volumename"
 	VolumeNameLabel      = "volumename"
+	PVUIDLabel           = "persistentvolume_uid"
 	ServiceLabel         = "service"
 	ServiceLabel         = "service"
 	ServiceNameLabel     = "service_name"
 	ServiceNameLabel     = "service_name"
+	ServiceTypeLabel     = "service_type"
 	IngressIPLabel       = "ingress_ip"
 	IngressIPLabel       = "ingress_ip"
+	LBIngressAddress     = "lb_ingress_address"
 	ProvisionerNameLabel = "provisioner_name"
 	ProvisionerNameLabel = "provisioner_name"
 	UIDLabel             = "uid"
 	UIDLabel             = "uid"
 	KubernetesNodeLabel  = "kubernetes_node"
 	KubernetesNodeLabel  = "kubernetes_node"
 	ModeLabel            = "mode"
 	ModeLabel            = "mode"
 	ModelNameLabel       = "modelName"
 	ModelNameLabel       = "modelName"
+	HostNameLabel        = "Hostname"
 	UUIDLabel            = "UUID"
 	UUIDLabel            = "UUID"
 	ResourceLabel        = "resource"
 	ResourceLabel        = "resource"
 	DeploymentLabel      = "deployment"
 	DeploymentLabel      = "deployment"
 	StatefulSetLabel     = "statefulSet"
 	StatefulSetLabel     = "statefulSet"
+	DaemonSetLabel       = "daemonset"
+	JobLabel             = "job"
+	CronJobLabel         = "cronjob"
 	ReplicaSetLabel      = "replicaset"
 	ReplicaSetLabel      = "replicaset"
 	ResourceQuotaLabel   = "resourcequota"
 	ResourceQuotaLabel   = "resourcequota"
 	OwnerNameLabel       = "owner_name"
 	OwnerNameLabel       = "owner_name"
 	OwnerKindLabel       = "owner_kind"
 	OwnerKindLabel       = "owner_kind"
+	OwnerUIDLabel        = "owner_uid"
+	ControllerLabel      = "controller"
 	UnitLabel            = "unit"
 	UnitLabel            = "unit"
 	InternetLabel        = "internet"
 	InternetLabel        = "internet"
 	SameZoneLabel        = "same_zone"
 	SameZoneLabel        = "same_zone"
@@ -52,6 +68,41 @@ const (
 	NoneLabelValue = "<none>"
 	NoneLabelValue = "<none>"
 )
 )
 
 
+type UIDValueResult struct {
+	UID   string
+	Value float64
+}
+
+func DecodeUIDValueResult(result *QueryResult) *UIDValueResult {
+	return decodeValueResult(result, UIDLabel)
+}
+
+type NodeUIDValueResult UIDValueResult
+
+func DecodeNodeUIDValueResult(result *QueryResult) *NodeUIDValueResult {
+	return (*NodeUIDValueResult)(decodeValueResult(result, NodeUIDLabel))
+}
+
+type PVCUIDValueResult UIDValueResult
+
+func DecodePVCUIDValueResult(result *QueryResult) *PVCUIDValueResult {
+	return (*PVCUIDValueResult)(decodeValueResult(result, PVCUIDLabel))
+}
+
+func decodeValueResult(result *QueryResult, uidLabel string) *UIDValueResult {
+	uid, _ := result.GetString(uidLabel)
+	var value float64
+	if len(result.Values) > 0 {
+		value = result.Values[0].Value
+	} else {
+		log.Warnf("Error decoding value for uid '%s': empty value returned", uid)
+	}
+	return &UIDValueResult{
+		UID:   uid,
+		Value: value,
+	}
+}
+
 // UptimeResult represents the first and last recorded sample timestamp within the query window
 // UptimeResult represents the first and last recorded sample timestamp within the query window
 type UptimeResult struct {
 type UptimeResult struct {
 	UID   string
 	UID   string
@@ -60,30 +111,7 @@ type UptimeResult struct {
 }
 }
 
 
 func (res *UptimeResult) GetStartEnd(windowStart, windowEnd time.Time, resolution time.Duration) (time.Time, time.Time) {
 func (res *UptimeResult) GetStartEnd(windowStart, windowEnd time.Time, resolution time.Duration) (time.Time, time.Time) {
-	first := res.First
-	last := res.Last
-	// The only corner-case here is what to do if you only get one timestamp.
-	// This dilemma still requires the use of the resolution, and can be
-	// clamped using the window. In this case, we want to honor the existence
-	// of the pod by giving "one resolution" worth of duration, half on each
-	// side of the given timestamp.
-	if first.Equal(last) {
-		first = first.Add(-1 * resolution / time.Duration(2))
-		last = last.Add(resolution / time.Duration(2))
-	}
-	if first.Before(windowStart) {
-		first = windowStart
-	}
-	if last.After(windowEnd) {
-		last = windowEnd
-	}
-	// prevent end times in the future
-	now := time.Now().UTC()
-	if last.After(now) {
-		last = now
-	}
-
-	return first, last
+	return getStartEnd(res.First, res.Last, windowStart, windowEnd, resolution)
 }
 }
 
 
 func DecodeUptimeResult(result *QueryResult) *UptimeResult {
 func DecodeUptimeResult(result *QueryResult) *UptimeResult {
@@ -98,14 +126,78 @@ func DecodeUptimeResult(result *QueryResult) *UptimeResult {
 	}
 	}
 }
 }
 
 
+// Container requires some special results because container name and pod UID is required to uniqly identify it
+
+type ContainerUptimeResult struct {
+	UptimeResult
+	Container string
+}
+
+func DecodeContainerUptimeResult(result *QueryResult) *ContainerUptimeResult {
+	container, _ := result.GetString(ContainerLabel)
+	ur := DecodeUptimeResult(result)
+	return &ContainerUptimeResult{
+		UptimeResult: *ur,
+		Container:    container,
+	}
+}
+
+type ContainerResourceResult struct {
+	ResourceResult
+	Container string
+}
+
+func DecodeContainerResourceResult(result *QueryResult) *ContainerResourceResult {
+	container, _ := result.GetString(ContainerLabel)
+	rr := DecodeResourceResult(result)
+	return &ContainerResourceResult{
+		ResourceResult: *rr,
+		Container:      container,
+	}
+}
+
+type LabelsResult struct {
+	UID     string
+	Cluster string
+	Labels  map[string]string
+}
+
+func DecodeLabelsResult(result *QueryResult) *LabelsResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	labels := result.GetLabels()
+
+	return &LabelsResult{
+		UID:     uid,
+		Cluster: cluster,
+		Labels:  labels,
+	}
+}
+
+type AnnotationsResult struct {
+	UID         string
+	Cluster     string
+	Annotations map[string]string
+}
+
+func DecodeAnnotationsResult(result *QueryResult) *AnnotationsResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	annotations := result.GetAnnotations()
+
+	return &AnnotationsResult{
+		UID:         uid,
+		Cluster:     cluster,
+		Annotations: annotations,
+	}
+}
+
 type PVResult struct {
 type PVResult struct {
-	UID              string
 	Cluster          string
 	Cluster          string
 	PersistentVolume string
 	PersistentVolume string
 }
 }
 
 
 type PVUsedAvgResult struct {
 type PVUsedAvgResult struct {
-	UID                   string
 	Cluster               string
 	Cluster               string
 	Namespace             string
 	Namespace             string
 	PersistentVolumeClaim string
 	PersistentVolumeClaim string
@@ -114,13 +206,11 @@ type PVUsedAvgResult struct {
 }
 }
 
 
 func DecodePVUsedAvgResult(result *QueryResult) *PVUsedAvgResult {
 func DecodePVUsedAvgResult(result *QueryResult) *PVUsedAvgResult {
-	uid, _ := result.GetString(UIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	namespace, _ := result.GetNamespace()
 	namespace, _ := result.GetNamespace()
 	pvc, _ := result.GetString(PVCLabel)
 	pvc, _ := result.GetString(PVCLabel)
 
 
 	return &PVUsedAvgResult{
 	return &PVUsedAvgResult{
-		UID:                   uid,
 		Cluster:               cluster,
 		Cluster:               cluster,
 		Namespace:             namespace,
 		Namespace:             namespace,
 		PersistentVolumeClaim: pvc,
 		PersistentVolumeClaim: pvc,
@@ -150,7 +240,6 @@ func DecodePVActiveMinutesResult(result *QueryResult) *PVActiveMinutesResult {
 }
 }
 
 
 type PVUsedMaxResult struct {
 type PVUsedMaxResult struct {
-	UID                   string
 	Cluster               string
 	Cluster               string
 	Namespace             string
 	Namespace             string
 	PersistentVolumeClaim string
 	PersistentVolumeClaim string
@@ -158,13 +247,11 @@ type PVUsedMaxResult struct {
 }
 }
 
 
 func DecodePVUsedMaxResult(result *QueryResult) *PVUsedMaxResult {
 func DecodePVUsedMaxResult(result *QueryResult) *PVUsedMaxResult {
-	uid, _ := result.GetString(UIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	namespace, _ := result.GetNamespace()
 	namespace, _ := result.GetNamespace()
 	pvc, _ := result.GetString(PVCLabel)
 	pvc, _ := result.GetString(PVCLabel)
 
 
 	return &PVUsedMaxResult{
 	return &PVUsedMaxResult{
-		UID:                   uid,
 		Cluster:               cluster,
 		Cluster:               cluster,
 		Namespace:             namespace,
 		Namespace:             namespace,
 		PersistentVolumeClaim: pvc,
 		PersistentVolumeClaim: pvc,
@@ -173,7 +260,6 @@ func DecodePVUsedMaxResult(result *QueryResult) *PVUsedMaxResult {
 }
 }
 
 
 type LocalStorageActiveMinutesResult struct {
 type LocalStorageActiveMinutesResult struct {
-	UID        string
 	Cluster    string
 	Cluster    string
 	Node       string
 	Node       string
 	ProviderID string
 	ProviderID string
@@ -182,7 +268,6 @@ type LocalStorageActiveMinutesResult struct {
 }
 }
 
 
 func DecodeLocalStorageActiveMinutesResult(result *QueryResult) *LocalStorageActiveMinutesResult {
 func DecodeLocalStorageActiveMinutesResult(result *QueryResult) *LocalStorageActiveMinutesResult {
-	uid, _ := result.GetString(UIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	node, _ := result.GetNode()
 	node, _ := result.GetNode()
 	if node == "" {
 	if node == "" {
@@ -191,7 +276,6 @@ func DecodeLocalStorageActiveMinutesResult(result *QueryResult) *LocalStorageAct
 	providerId, _ := result.GetProviderID()
 	providerId, _ := result.GetProviderID()
 
 
 	return &LocalStorageActiveMinutesResult{
 	return &LocalStorageActiveMinutesResult{
-		UID:        uid,
 		Cluster:    cluster,
 		Cluster:    cluster,
 		Node:       node,
 		Node:       node,
 		ProviderID: providerId,
 		ProviderID: providerId,
@@ -315,6 +399,30 @@ func DecodeLocalStorageBytesResult(result *QueryResult) *LocalStorageBytesResult
 	}
 	}
 }
 }
 
 
+type NodeInfoResult struct {
+	UID          string
+	Cluster      string
+	Node         string
+	ProviderID   string
+	InstanceType string
+}
+
+func DecodeNodeInfoResult(result *QueryResult) *NodeInfoResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetNode()
+	providerId, _ := result.GetProviderID()
+	instanceType, _ := result.GetInstanceType()
+
+	return &NodeInfoResult{
+		UID:          uid,
+		Cluster:      cluster,
+		Node:         node,
+		ProviderID:   providerId,
+		InstanceType: instanceType,
+	}
+}
+
 type NodeActiveMinutesResult struct {
 type NodeActiveMinutesResult struct {
 	UID        string
 	UID        string
 	Cluster    string
 	Cluster    string
@@ -339,22 +447,28 @@ func DecodeNodeActiveMinutesResult(result *QueryResult) *NodeActiveMinutesResult
 }
 }
 
 
 type NodeCPUCoresCapacityResult struct {
 type NodeCPUCoresCapacityResult struct {
-	UID     string
-	Cluster string
-	Node    string
-	Data    []*util.Vector
+	UID      string
+	Cluster  string
+	Node     string
+	CPUCores float64
 }
 }
 
 
 func DecodeNodeCPUCoresCapacityResult(result *QueryResult) *NodeCPUCoresCapacityResult {
 func DecodeNodeCPUCoresCapacityResult(result *QueryResult) *NodeCPUCoresCapacityResult {
 	uid, _ := result.GetString(UIDLabel)
 	uid, _ := result.GetString(UIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	node, _ := result.GetNode()
 	node, _ := result.GetNode()
+	var value float64
+	if len(result.Values) > 0 {
+		value = result.Values[0].Value
+	} else {
+		log.Warnf("Error decoding node CPU cores capacity result for node '%s': empty value returned", uid)
+	}
 
 
 	return &NodeCPUCoresCapacityResult{
 	return &NodeCPUCoresCapacityResult{
-		UID:     uid,
-		Cluster: cluster,
-		Node:    node,
-		Data:    result.Values,
+		UID:      uid,
+		Cluster:  cluster,
+		Node:     node,
+		CPUCores: value,
 	}
 	}
 }
 }
 
 
@@ -365,22 +479,28 @@ func DecodeNodeCPUCoresAllocatableResult(result *QueryResult) *NodeCPUCoresAlloc
 }
 }
 
 
 type NodeRAMBytesCapacityResult struct {
 type NodeRAMBytesCapacityResult struct {
-	UID     string
-	Cluster string
-	Node    string
-	Data    []*util.Vector
+	UID      string
+	Cluster  string
+	Node     string
+	RAMBytes float64
 }
 }
 
 
 func DecodeNodeRAMBytesCapacityResult(result *QueryResult) *NodeRAMBytesCapacityResult {
 func DecodeNodeRAMBytesCapacityResult(result *QueryResult) *NodeRAMBytesCapacityResult {
 	uid, _ := result.GetString(UIDLabel)
 	uid, _ := result.GetString(UIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	node, _ := result.GetNode()
 	node, _ := result.GetNode()
+	var value float64
+	if len(result.Values) > 0 {
+		value = result.Values[0].Value
+	} else {
+		log.Warnf("Error decoding node RAM bytes capacity result for node '%s': empty value returned", uid)
+	}
 
 
 	return &NodeRAMBytesCapacityResult{
 	return &NodeRAMBytesCapacityResult{
-		UID:     uid,
-		Cluster: cluster,
-		Node:    node,
-		Data:    result.Values,
+		UID:      uid,
+		Cluster:  cluster,
+		Node:     node,
+		RAMBytes: value,
 	}
 	}
 }
 }
 
 
@@ -395,8 +515,7 @@ type NodeGPUCountResult struct {
 	Cluster    string
 	Cluster    string
 	Node       string
 	Node       string
 	ProviderID string
 	ProviderID string
-
-	Data []*util.Vector
+	GPUCount   float64
 }
 }
 
 
 func DecodeNodeGPUCountResult(result *QueryResult) *NodeGPUCountResult {
 func DecodeNodeGPUCountResult(result *QueryResult) *NodeGPUCountResult {
@@ -404,13 +523,19 @@ func DecodeNodeGPUCountResult(result *QueryResult) *NodeGPUCountResult {
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	node, _ := result.GetNode()
 	node, _ := result.GetNode()
 	providerId, _ := result.GetProviderID()
 	providerId, _ := result.GetProviderID()
+	var value float64
+	if len(result.Values) > 0 {
+		value = result.Values[0].Value
+	} else {
+		log.Warnf("Error decoding node GPU count capacity result for node '%s': empty value returned", uid)
+	}
 
 
 	return &NodeGPUCountResult{
 	return &NodeGPUCountResult{
 		UID:        uid,
 		UID:        uid,
 		Cluster:    cluster,
 		Cluster:    cluster,
 		Node:       node,
 		Node:       node,
 		ProviderID: providerId,
 		ProviderID: providerId,
-		Data:       result.Values,
+		GPUCount:   value,
 	}
 	}
 }
 }
 
 
@@ -519,6 +644,33 @@ func DecodeLBPricePerHrResult(result *QueryResult) *LBPricePerHrResult {
 	return DecodeLBActiveMinutesResult(result)
 	return DecodeLBActiveMinutesResult(result)
 }
 }
 
 
+type ClusterInfoResult struct {
+	UID         string
+	Cluster     string
+	Provider    string
+	AccountID   string
+	Provisioner string
+	Region      string
+}
+
+func DecodeClusterInfoResult(result *QueryResult) *ClusterInfoResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetString(ClusterNameLabel)
+	provider, _ := result.GetString(ProviderLabel)
+	accountID, _ := result.GetString(AccountIDLabel)
+	provisioner, _ := result.GetString(ProvisionerNameLabel)
+	region, _ := result.GetString(RegionLabel)
+
+	return &ClusterInfoResult{
+		UID:         uid,
+		Cluster:     cluster,
+		Provider:    provider,
+		AccountID:   accountID,
+		Provisioner: provisioner,
+		Region:      region,
+	}
+}
+
 type ClusterManagementDurationResult struct {
 type ClusterManagementDurationResult struct {
 	UID         string
 	UID         string
 	Cluster     string
 	Cluster     string
@@ -545,6 +697,75 @@ func DecodeClusterManagementPricePerHrResult(result *QueryResult) *ClusterManage
 	return DecodeClusterManagementDurationResult(result)
 	return DecodeClusterManagementDurationResult(result)
 }
 }
 
 
+type PodInfoResult struct {
+	UID          string
+	Cluster      string
+	Pod          string
+	NamespaceUID string
+	NodeUID      string
+}
+
+func DecodePodInfoResult(result *QueryResult) *PodInfoResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	pod, _ := result.GetPod()
+	namespaceUID, _ := result.GetString(NamespaceUIDLabel)
+	nodeUID, _ := result.GetString(NodeUIDLabel)
+
+	return &PodInfoResult{
+		UID:          uid,
+		Cluster:      cluster,
+		Pod:          pod,
+		NamespaceUID: namespaceUID,
+		NodeUID:      nodeUID,
+	}
+}
+
+type PodPVCVolumeResult struct {
+	UID           string
+	Cluster       string
+	PVCUID        string
+	PodVolumeName string
+}
+
+func DecodePodPVCVolumeResult(result *QueryResult) *PodPVCVolumeResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	pvcUID, _ := result.GetString(PVCUIDLabel)
+	podVolumeName, _ := result.GetString(PodVolumeNameLabel)
+
+	return &PodPVCVolumeResult{
+		UID:           uid,
+		Cluster:       cluster,
+		PVCUID:        pvcUID,
+		PodVolumeName: podVolumeName,
+	}
+}
+
+type OwnerResult struct {
+	UID        string
+	Cluster    string
+	OwnerUID   string
+	OwnerKind  string
+	Controller bool
+}
+
+func DecodeOwnerResult(result *QueryResult) *OwnerResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	ownerUID, _ := result.GetString(OwnerUIDLabel)
+	ownerKind, _ := result.GetString(OwnerKindLabel)
+	controller, _ := result.GetBool(ControllerLabel)
+
+	return &OwnerResult{
+		UID:        uid,
+		Cluster:    cluster,
+		OwnerUID:   ownerUID,
+		OwnerKind:  ownerKind,
+		Controller: controller,
+	}
+}
+
 type PodsResult struct {
 type PodsResult struct {
 	UID       string
 	UID       string
 	Cluster   string
 	Cluster   string
@@ -766,7 +987,7 @@ type GPUsUsageAvgResult struct {
 }
 }
 
 
 func DecodeGPUsUsageAvgResult(result *QueryResult) *GPUsUsageAvgResult {
 func DecodeGPUsUsageAvgResult(result *QueryResult) *GPUsUsageAvgResult {
-	uid, _ := result.GetString(UIDLabel)
+	uid, _ := result.GetString(PodUIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	namespace, _ := result.GetNamespace()
 	namespace, _ := result.GetNamespace()
 	pod, _ := result.GetPod()
 	pod, _ := result.GetPod()
@@ -792,7 +1013,7 @@ type GPUsUsageMaxResult struct {
 }
 }
 
 
 func DecodeGPUsUsageMaxResult(result *QueryResult) *GPUsUsageMaxResult {
 func DecodeGPUsUsageMaxResult(result *QueryResult) *GPUsUsageMaxResult {
-	uid, _ := result.GetString(UIDLabel)
+	uid, _ := result.GetString(PodUIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	namespace, _ := result.GetNamespace()
 	namespace, _ := result.GetNamespace()
 	pod, _ := result.GetPod()
 	pod, _ := result.GetPod()
@@ -847,7 +1068,7 @@ type GPUInfoResult struct {
 }
 }
 
 
 func DecodeGPUInfoResult(result *QueryResult) *GPUInfoResult {
 func DecodeGPUInfoResult(result *QueryResult) *GPUInfoResult {
-	uid, _ := result.GetString(UIDLabel)
+	uid, _ := result.GetString(PodUIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	namespace, _ := result.GetNamespace()
 	namespace, _ := result.GetNamespace()
 	pod, _ := result.GetPod()
 	pod, _ := result.GetPod()
@@ -954,18 +1175,21 @@ func DecodePVCBytesRequestedResult(result *QueryResult) *PVCBytesRequestedResult
 type PVCInfoResult struct {
 type PVCInfoResult struct {
 	UID                   string
 	UID                   string
 	Cluster               string
 	Cluster               string
+	NamespaceUID          string
 	Namespace             string
 	Namespace             string
 	VolumeName            string
 	VolumeName            string
+	PVUID                 string
 	PersistentVolumeClaim string
 	PersistentVolumeClaim string
 	StorageClass          string
 	StorageClass          string
-
-	Data []*util.Vector
+	Data                  []*util.Vector
 }
 }
 
 
 func DecodePVCInfoResult(result *QueryResult) *PVCInfoResult {
 func DecodePVCInfoResult(result *QueryResult) *PVCInfoResult {
 	uid, _ := result.GetString(UIDLabel)
 	uid, _ := result.GetString(UIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
+	namespaceUID, _ := result.GetString(NamespaceUIDLabel)
 	namespace, _ := result.GetNamespace()
 	namespace, _ := result.GetNamespace()
+	pvUID, _ := result.GetString(PVUIDLabel)
 	volumeName, _ := result.GetString(VolumeNameLabel)
 	volumeName, _ := result.GetString(VolumeNameLabel)
 	pvc, _ := result.GetString(PVCLabel)
 	pvc, _ := result.GetString(PVCLabel)
 	storageClass, _ := result.GetString(StorageClassLabel)
 	storageClass, _ := result.GetString(StorageClassLabel)
@@ -973,11 +1197,14 @@ func DecodePVCInfoResult(result *QueryResult) *PVCInfoResult {
 	return &PVCInfoResult{
 	return &PVCInfoResult{
 		UID:                   uid,
 		UID:                   uid,
 		Cluster:               cluster,
 		Cluster:               cluster,
+		NamespaceUID:          namespaceUID,
 		Namespace:             namespace,
 		Namespace:             namespace,
+		PVUID:                 pvUID,
 		VolumeName:            volumeName,
 		VolumeName:            volumeName,
 		PersistentVolumeClaim: pvc,
 		PersistentVolumeClaim: pvc,
 		StorageClass:          storageClass,
 		StorageClass:          storageClass,
-		Data:                  result.Values,
+
+		Data: result.Values,
 	}
 	}
 }
 }
 
 
@@ -985,20 +1212,25 @@ type PVBytesResult struct {
 	UID              string
 	UID              string
 	Cluster          string
 	Cluster          string
 	PersistentVolume string
 	PersistentVolume string
-
-	Data []*util.Vector
+	Value            float64
 }
 }
 
 
 func DecodePVBytesResult(result *QueryResult) *PVBytesResult {
 func DecodePVBytesResult(result *QueryResult) *PVBytesResult {
 	uid, _ := result.GetString(UIDLabel)
 	uid, _ := result.GetString(UIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	pv, _ := result.GetString(PVLabel)
 	pv, _ := result.GetString(PVLabel)
+	var value float64
+	if len(result.Values) > 0 {
+		value = result.Values[0].Value
+	} else {
+		log.Warnf("Error decoding PV bytes result for pv '%s': empty value returned", uid)
+	}
 
 
 	return &PVBytesResult{
 	return &PVBytesResult{
 		UID:              uid,
 		UID:              uid,
 		Cluster:          cluster,
 		Cluster:          cluster,
 		PersistentVolume: pv,
 		PersistentVolume: pv,
-		Data:             result.Values,
+		Value:            value,
 	}
 	}
 }
 }
 
 
@@ -1036,8 +1268,8 @@ type PVInfoResult struct {
 	PersistentVolume string
 	PersistentVolume string
 	StorageClass     string
 	StorageClass     string
 	ProviderID       string
 	ProviderID       string
-
-	Data []*util.Vector
+	CSIVolumeHandle  string
+	Data             []*util.Vector
 }
 }
 
 
 func DecodePVInfoResult(result *QueryResult) *PVInfoResult {
 func DecodePVInfoResult(result *QueryResult) *PVInfoResult {
@@ -1046,6 +1278,7 @@ func DecodePVInfoResult(result *QueryResult) *PVInfoResult {
 	storageClass, _ := result.GetString(StorageClassLabel)
 	storageClass, _ := result.GetString(StorageClassLabel)
 	providerId, _ := result.GetProviderID()
 	providerId, _ := result.GetProviderID()
 	pv, _ := result.GetString(PVLabel)
 	pv, _ := result.GetString(PVLabel)
+	csiVolumeHandle, _ := result.GetString(CSIVolumeHandleLabel)
 
 
 	return &PVInfoResult{
 	return &PVInfoResult{
 		UID:              uid,
 		UID:              uid,
@@ -1053,7 +1286,47 @@ func DecodePVInfoResult(result *QueryResult) *PVInfoResult {
 		PersistentVolume: pv,
 		PersistentVolume: pv,
 		StorageClass:     storageClass,
 		StorageClass:     storageClass,
 		ProviderID:       providerId,
 		ProviderID:       providerId,
-		Data:             result.Values,
+		CSIVolumeHandle:  csiVolumeHandle,
+
+		Data: result.Values,
+	}
+}
+
+type PodNetworkBytesResult struct {
+	UID        string
+	Cluster    string
+	Service    string
+	Internet   bool
+	SameRegion bool
+	SameZone   bool
+	NatGateway bool
+	Value      float64
+}
+
+func DecodePodNetworkBytesResult(result *QueryResult) *PodNetworkBytesResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	service, _ := result.GetString(ServiceLabel)
+	internet, _ := result.GetBool(InternetLabel)
+	sameRegion, _ := result.GetBool(SameRegionLabel)
+	sameZone, _ := result.GetBool(SameZoneLabel)
+	natGateway, _ := result.GetBool(NatGatewayLabel)
+	var value float64
+	if len(result.Values) > 0 {
+		value = result.Values[0].Value
+	} else {
+		log.Warnf("Error decoding pod network bytes result for pod '%s': empty value returned", uid)
+	}
+
+	return &PodNetworkBytesResult{
+		UID:        uid,
+		Cluster:    cluster,
+		Service:    service,
+		Internet:   internet,
+		SameRegion: sameRegion,
+		SameZone:   sameZone,
+		NatGateway: natGateway,
+		Value:      value,
 	}
 	}
 }
 }
 
 
@@ -1118,7 +1391,6 @@ type NetInternetServiceGiBResult = NetworkGiBResult
 
 
 type NetNatGatewayPricePerGiBResult = NetworkPricePerGiBResult
 type NetNatGatewayPricePerGiBResult = NetworkPricePerGiBResult
 type NetNatGatewayGiBResult = NetworkGiBResult
 type NetNatGatewayGiBResult = NetworkGiBResult
-
 type NetZoneIngressGiBResult = NetworkGiBResult
 type NetZoneIngressGiBResult = NetworkGiBResult
 type NetRegionIngressGiBResult = NetworkGiBResult
 type NetRegionIngressGiBResult = NetworkGiBResult
 type NetInternetIngressGiBResult = NetworkGiBResult
 type NetInternetIngressGiBResult = NetworkGiBResult
@@ -1290,7 +1562,6 @@ type NodeLabelsResult struct {
 	Cluster string
 	Cluster string
 	Node    string
 	Node    string
 	Labels  map[string]string
 	Labels  map[string]string
-	Data    []*util.Vector
 }
 }
 
 
 func DecodeNodeLabelsResult(result *QueryResult) *NodeLabelsResult {
 func DecodeNodeLabelsResult(result *QueryResult) *NodeLabelsResult {
@@ -1304,7 +1575,24 @@ func DecodeNodeLabelsResult(result *QueryResult) *NodeLabelsResult {
 		Cluster: cluster,
 		Cluster: cluster,
 		Node:    node,
 		Node:    node,
 		Labels:  labels,
 		Labels:  labels,
-		Data:    result.Values,
+	}
+}
+
+type NamespaceInfoResult struct {
+	UID       string
+	Cluster   string
+	Namespace string
+}
+
+func DecodeNamespaceInfoResult(result *QueryResult) *NamespaceInfoResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+
+	return &NamespaceInfoResult{
+		UID:       uid,
+		Cluster:   cluster,
+		Namespace: namespace,
 	}
 	}
 }
 }
 
 
@@ -1384,6 +1672,54 @@ func DecodeServiceLabelsResult(result *QueryResult) *ServiceLabelsResult {
 	}
 	}
 }
 }
 
 
+type ServiceInfoResult struct {
+	UID              string
+	Cluster          string
+	NamespaceUID     string
+	Service          string
+	ServiceType      string
+	LBIngressAddress string
+}
+
+func DecodeServiceInfoResult(result *QueryResult) *ServiceInfoResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	namespaceUID, _ := result.GetString(NamespaceUIDLabel)
+	service, _ := result.GetString(ServiceLabel)
+	serviceType, _ := result.GetString(ServiceTypeLabel)
+	lbIngressAddress, _ := result.GetString(LBIngressAddress)
+
+	return &ServiceInfoResult{
+		UID:              uid,
+		Cluster:          cluster,
+		NamespaceUID:     namespaceUID,
+		Service:          service,
+		ServiceType:      serviceType,
+		LBIngressAddress: lbIngressAddress,
+	}
+}
+
+type DeploymentInfoResult struct {
+	UID          string
+	Cluster      string
+	NamespaceUID string
+	Deployment   string
+}
+
+func DecodeDeploymentInfoResult(result *QueryResult) *DeploymentInfoResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	namespaceUID, _ := result.GetString(NamespaceUIDLabel)
+	deployment, _ := result.GetString(DeploymentLabel)
+
+	return &DeploymentInfoResult{
+		UID:          uid,
+		Cluster:      cluster,
+		NamespaceUID: namespaceUID,
+		Deployment:   deployment,
+	}
+}
+
 type DeploymentLabelsResult struct {
 type DeploymentLabelsResult struct {
 	UID        string
 	UID        string
 	Cluster    string
 	Cluster    string
@@ -1410,6 +1746,111 @@ func DecodeDeploymentLabelsResult(result *QueryResult) *DeploymentLabelsResult {
 	}
 	}
 }
 }
 
 
+type StatefulSetInfoResult struct {
+	UID          string
+	Cluster      string
+	NamespaceUID string
+	StatefulSet  string
+}
+
+func DecodeStatefulSetInfoResult(result *QueryResult) *StatefulSetInfoResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	namespaceUID, _ := result.GetString(NamespaceUIDLabel)
+	statefulSet, _ := result.GetString(StatefulSetLabel)
+
+	return &StatefulSetInfoResult{
+		UID:          uid,
+		Cluster:      cluster,
+		NamespaceUID: namespaceUID,
+		StatefulSet:  statefulSet,
+	}
+}
+
+type DaemonSetInfoResult struct {
+	UID          string
+	Cluster      string
+	NamespaceUID string
+	DaemonSet    string
+}
+
+func DecodeDaemonSetInfoResult(result *QueryResult) *DaemonSetInfoResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	namespaceUID, _ := result.GetString(NamespaceUIDLabel)
+	daemonSet, _ := result.GetString(DaemonSetLabel)
+
+	return &DaemonSetInfoResult{
+		UID:          uid,
+		Cluster:      cluster,
+		NamespaceUID: namespaceUID,
+		DaemonSet:    daemonSet,
+	}
+}
+
+type JobInfoResult struct {
+	UID          string
+	Cluster      string
+	NamespaceUID string
+	Job          string
+}
+
+func DecodeJobInfoResult(result *QueryResult) *JobInfoResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	namespaceUID, _ := result.GetString(NamespaceUIDLabel)
+	job, _ := result.GetString(JobLabel)
+
+	return &JobInfoResult{
+		UID:          uid,
+		Cluster:      cluster,
+		NamespaceUID: namespaceUID,
+		Job:          job,
+	}
+}
+
+type CronJobInfoResult struct {
+	UID          string
+	Cluster      string
+	NamespaceUID string
+	CronJob      string
+}
+
+func DecodeCronJobInfoResult(result *QueryResult) *CronJobInfoResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	namespaceUID, _ := result.GetString(NamespaceUIDLabel)
+	cronJob, _ := result.GetString(CronJobLabel)
+
+	return &CronJobInfoResult{
+		UID:          uid,
+		Cluster:      cluster,
+		NamespaceUID: namespaceUID,
+		CronJob:      cronJob,
+	}
+}
+
+type ReplicaSetInfoResult struct {
+	UID          string
+	Cluster      string
+	NamespaceUID string
+	ReplicaSet   string
+}
+
+func DecodeReplicaSetInfoResult(result *QueryResult) *ReplicaSetInfoResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	namespaceUID, _ := result.GetString(NamespaceUIDLabel)
+	replicaSet, _ := result.GetString(ReplicaSetLabel)
+
+	return &ReplicaSetInfoResult{
+		UID:          uid,
+		Cluster:      cluster,
+		NamespaceUID: namespaceUID,
+		ReplicaSet:   replicaSet,
+	}
+}
+
 type StatefulSetLabelsResult struct {
 type StatefulSetLabelsResult struct {
 	UID         string
 	UID         string
 	Cluster     string
 	Cluster     string
@@ -1436,61 +1877,51 @@ func DecodeStatefulSetLabelsResult(result *QueryResult) *StatefulSetLabelsResult
 	}
 	}
 }
 }
 
 
-type DaemonSetLabelsResult struct {
+type PodsWithDaemonSetOwnerResult struct {
 	UID       string
 	UID       string
 	Cluster   string
 	Cluster   string
 	Namespace string
 	Namespace string
 	Pod       string
 	Pod       string
 	DaemonSet string
 	DaemonSet string
-	Labels    map[string]string
-	Data      []*util.Vector
 }
 }
 
 
-func DecodeDaemonSetLabelsResult(result *QueryResult) *DaemonSetLabelsResult {
+func DecodePodsWithDaemonSetOwnerResult(result *QueryResult) *PodsWithDaemonSetOwnerResult {
 	uid, _ := result.GetString(UIDLabel)
 	uid, _ := result.GetString(UIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	namespace, _ := result.GetNamespace()
 	namespace, _ := result.GetNamespace()
 	pod, _ := result.GetPod()
 	pod, _ := result.GetPod()
 	daemonSet, _ := result.GetString(OwnerNameLabel)
 	daemonSet, _ := result.GetString(OwnerNameLabel)
-	labels := result.GetLabels()
 
 
-	return &DaemonSetLabelsResult{
+	return &PodsWithDaemonSetOwnerResult{
 		UID:       uid,
 		UID:       uid,
 		Cluster:   cluster,
 		Cluster:   cluster,
 		Namespace: namespace,
 		Namespace: namespace,
 		Pod:       pod,
 		Pod:       pod,
 		DaemonSet: daemonSet,
 		DaemonSet: daemonSet,
-		Labels:    labels,
-		Data:      result.Values,
 	}
 	}
 }
 }
 
 
-type JobLabelsResult struct {
+type PodsWithJobOwnerResult struct {
 	UID       string
 	UID       string
 	Cluster   string
 	Cluster   string
 	Namespace string
 	Namespace string
 	Pod       string
 	Pod       string
 	Job       string
 	Job       string
-	Labels    map[string]string
-	Data      []*util.Vector
 }
 }
 
 
-func DecodeJobLabelsResult(result *QueryResult) *JobLabelsResult {
+func DecodePodsWithJobOwnerResult(result *QueryResult) *PodsWithJobOwnerResult {
 	uid, _ := result.GetString(UIDLabel)
 	uid, _ := result.GetString(UIDLabel)
 	cluster, _ := result.GetCluster()
 	cluster, _ := result.GetCluster()
 	namespace, _ := result.GetNamespace()
 	namespace, _ := result.GetNamespace()
 	pod, _ := result.GetPod()
 	pod, _ := result.GetPod()
 	job, _ := result.GetString(OwnerNameLabel)
 	job, _ := result.GetString(OwnerNameLabel)
-	labels := result.GetLabels()
 
 
-	return &JobLabelsResult{
+	return &PodsWithJobOwnerResult{
 		UID:       uid,
 		UID:       uid,
 		Cluster:   cluster,
 		Cluster:   cluster,
 		Namespace: namespace,
 		Namespace: namespace,
 		Pod:       pod,
 		Pod:       pod,
 		Job:       job,
 		Job:       job,
-		Labels:    labels,
-		Data:      result.Values,
 	}
 	}
 }
 }
 
 
@@ -1567,126 +1998,143 @@ func DecodeReplicaSetsWithRolloutResult(result *QueryResult) *ReplicaSetsWithRol
 	}
 	}
 }
 }
 
 
-type ResourceQuotaMetricResult struct {
+type ResourceQuotaInfoResult struct {
 	UID           string
 	UID           string
-	Namespace     string
+	NamespaceUID  string
 	ResourceQuota string
 	ResourceQuota string
-	Resource      string
-	Unit          string
-	Data          []*util.Vector
 }
 }
 
 
-func DecodeResourceQuotaMetricResult(result *QueryResult) *ResourceQuotaMetricResult {
+func DecodeResourceQuotaInfoResult(result *QueryResult) *ResourceQuotaInfoResult {
 	uid, _ := result.GetString(UIDLabel)
 	uid, _ := result.GetString(UIDLabel)
-	namespace, _ := result.GetNamespace()
+	namespaceUID, _ := result.GetString(NamespaceUIDLabel)
 	resourceQuota, _ := result.GetString(ResourceQuotaLabel)
 	resourceQuota, _ := result.GetString(ResourceQuotaLabel)
-	resource, _ := result.GetString(ResourceLabel)
-	unit, _ := result.GetString(UnitLabel)
 
 
-	return &ResourceQuotaMetricResult{
+	return &ResourceQuotaInfoResult{
 		UID:           uid,
 		UID:           uid,
-		Namespace:     namespace,
+		NamespaceUID:  namespaceUID,
 		ResourceQuota: resourceQuota,
 		ResourceQuota: resourceQuota,
-		Resource:      resource,
-		Unit:          unit,
-		Data:          result.Values,
 	}
 	}
 }
 }
 
 
-type ResourceQuotaSpecCPURequestAvgResult = ResourceQuotaMetricResult
-
-func DecodeResourceQuotaSpecCPURequestAvgResult(result *QueryResult) *ResourceQuotaSpecCPURequestAvgResult {
-	return DecodeResourceQuotaMetricResult(result)
-}
-
-type ResourceQuotaSpecCPURequestMaxResult = ResourceQuotaMetricResult
-
-func DecodeResourceQuotaSpecCPURequestMaxResult(result *QueryResult) *ResourceQuotaSpecCPURequestMaxResult {
-	return DecodeResourceQuotaMetricResult(result)
-}
-
-type ResourceQuotaSpecRAMRequestAvgResult = ResourceQuotaMetricResult
-
-func DecodeResourceQuotaSpecRAMRequestAvgResult(result *QueryResult) *ResourceQuotaSpecRAMRequestAvgResult {
-	return DecodeResourceQuotaMetricResult(result)
-}
-
-type ResourceQuotaSpecRAMRequestMaxResult = ResourceQuotaMetricResult
-
-func DecodeResourceQuotaSpecRAMRequestMaxResult(result *QueryResult) *ResourceQuotaSpecRAMRequestMaxResult {
-	return DecodeResourceQuotaMetricResult(result)
-}
-
-type ResourceQuotaSpecCPULimitAvgResult = ResourceQuotaMetricResult
-
-func DecodeResourceQuotaSpecCPULimitAvgResult(result *QueryResult) *ResourceQuotaSpecCPULimitAvgResult {
-	return DecodeResourceQuotaMetricResult(result)
-}
-
-type ResourceQuotaSpecCPULimitMaxResult = ResourceQuotaMetricResult
-
-func DecodeResourceQuotaSpecCPULimitMaxResult(result *QueryResult) *ResourceQuotaSpecCPULimitMaxResult {
-	return DecodeResourceQuotaMetricResult(result)
-}
-
-type ResourceQuotaSpecRAMLimitAvgResult = ResourceQuotaMetricResult
-
-func DecodeResourceQuotaSpecRAMLimitAvgResult(result *QueryResult) *ResourceQuotaSpecRAMLimitAvgResult {
-	return DecodeResourceQuotaMetricResult(result)
+type ResourceResult struct {
+	UID      string
+	Resource string
+	Unit     string
+	Value    float64
 }
 }
 
 
-type ResourceQuotaSpecRAMLimitMaxResult = ResourceQuotaMetricResult
+func DecodeResourceResult(result *QueryResult) *ResourceResult {
+	uid, _ := result.GetString(UIDLabel)
+	resource, _ := result.GetString(ResourceLabel)
+	unit, _ := result.GetString(UnitLabel)
+	var value float64
+	if len(result.Values) > 0 {
+		value = result.Values[0].Value
+	} else {
+		log.Warnf("Error decoding resource for uid '%s': empty value returned", uid)
+	}
 
 
-func DecodeResourceQuotaSpecRAMLimitMaxResult(result *QueryResult) *ResourceQuotaSpecRAMLimitMaxResult {
-	return DecodeResourceQuotaMetricResult(result)
+	return &ResourceResult{
+		UID:      uid,
+		Resource: resource,
+		Unit:     unit,
+		Value:    value,
+	}
 }
 }
 
 
-type ResourceQuotaStatusUsedCPURequestAvgResult = ResourceQuotaMetricResult
-
-func DecodeResourceQuotaStatusUsedCPURequestAvgResult(result *QueryResult) *ResourceQuotaStatusUsedCPURequestAvgResult {
-	return DecodeResourceQuotaMetricResult(result)
+// DCGM needs specialized results because it uses UUID instead of the uid label that we use.
+type DCGMDeviceInfoResult struct {
+	UUID      string
+	Device    string
+	ModelName string
+	HostName  string
 }
 }
 
 
-type ResourceQuotaStatusUsedCPURequestMaxResult = ResourceQuotaMetricResult
+func DecodeDCGMDeviceInfoResult(result *QueryResult) *DCGMDeviceInfoResult {
+	uuid, _ := result.GetString(UUIDLabel)
+	device, _ := result.GetString(DeviceLabel)
+	modelName, _ := result.GetString(ModelNameLabel)
+	hostName, _ := result.GetString(HostNameLabel)
 
 
-func DecodeResourceQuotaStatusUsedCPURequestMaxResult(result *QueryResult) *ResourceQuotaStatusUsedCPURequestMaxResult {
-	return DecodeResourceQuotaMetricResult(result)
+	return &DCGMDeviceInfoResult{
+		UUID:      uuid,
+		Device:    device,
+		ModelName: modelName,
+		HostName:  hostName,
+	}
 }
 }
 
 
-type ResourceQuotaStatusUsedRAMRequestAvgResult = ResourceQuotaMetricResult
-
-func DecodeResourceQuotaStatusUsedRAMRequestAvgResult(result *QueryResult) *ResourceQuotaStatusUsedRAMRequestAvgResult {
-	return DecodeResourceQuotaMetricResult(result)
+type DCGMDeviceUptimeResult struct {
+	UUID  string
+	First time.Time
+	Last  time.Time
 }
 }
 
 
-type ResourceQuotaStatusUsedRAMRequestMaxResult = ResourceQuotaMetricResult
-
-func DecodeResourceQuotaStatusUsedRAMRequestMaxResult(result *QueryResult) *ResourceQuotaStatusUsedRAMRequestMaxResult {
-	return DecodeResourceQuotaMetricResult(result)
+func (res *DCGMDeviceUptimeResult) GetStartEnd(windowStart, windowEnd time.Time, resolution time.Duration) (time.Time, time.Time) {
+	return getStartEnd(res.First, res.Last, windowStart, windowEnd, resolution)
 }
 }
 
 
-type ResourceQuotaStatusUsedCPULimitAvgResult = ResourceQuotaMetricResult
+func getStartEnd(first, last, windowStart, windowEnd time.Time, resolution time.Duration) (time.Time, time.Time) {
+	// The only corner-case here is what to do if you only get one timestamp.
+	// This dilemma still requires the use of the resolution, and can be
+	// clamped using the window. In this case, we want to honor the existence
+	// of the pod by giving "one resolution" worth of duration, half on each
+	// side of the given timestamp.
+	if first.Equal(last) {
+		first = first.Add(-1 * resolution / time.Duration(2))
+		last = last.Add(resolution / time.Duration(2))
+	}
+	if first.Before(windowStart) {
+		first = windowStart
+	}
+	if last.After(windowEnd) {
+		last = windowEnd
+	}
+	// prevent end times in the future
+	now := time.Now().UTC()
+	if last.After(now) {
+		last = now
+	}
 
 
-func DecodeResourceQuotaStatusUsedCPULimitAvgResult(result *QueryResult) *ResourceQuotaStatusUsedCPULimitAvgResult {
-	return DecodeResourceQuotaMetricResult(result)
+	return first, last
 }
 }
 
 
-type ResourceQuotaStatusUsedCPULimitMaxResult = ResourceQuotaMetricResult
+func DecodeDCGMDeviceUptimeResult(result *QueryResult) *DCGMDeviceUptimeResult {
+	uuid, _ := result.GetString(UUIDLabel)
+	first := time.Unix(int64(result.Values[0].Timestamp), 0).UTC()
+	last := time.Unix(int64(result.Values[len(result.Values)-1].Timestamp), 0).UTC()
 
 
-func DecodeResourceQuotaStatusUsedCPULimitMaxResult(result *QueryResult) *ResourceQuotaStatusUsedCPULimitMaxResult {
-	return DecodeResourceQuotaMetricResult(result)
+	return &DCGMDeviceUptimeResult{
+		UUID:  uuid,
+		First: first,
+		Last:  last,
+	}
 }
 }
 
 
-type ResourceQuotaStatusUsedRAMLimitAvgResult = ResourceQuotaMetricResult
-
-func DecodeResourceQuotaStatusUsedRAMLimitAvgResult(result *QueryResult) *ResourceQuotaStatusUsedRAMLimitAvgResult {
-	return DecodeResourceQuotaMetricResult(result)
+type DCGMDeviceContainerUsageResult struct {
+	UUID      string
+	PodUID    string
+	Container string
+	Value     float64
 }
 }
 
 
-type ResourceQuotaStatusUsedRAMLimitMaxResult = ResourceQuotaMetricResult
+func DecodeDCGMDeviceContainerUsageResult(result *QueryResult) *DCGMDeviceContainerUsageResult {
+	uuid, _ := result.GetString(UUIDLabel)
+	podUID, _ := result.GetString(PodUIDLabel)
+	container, _ := result.GetString(ContainerLabel)
+	var value float64
+	if len(result.Values) > 0 {
+		value = result.Values[0].Value
+	} else {
+		log.Warnf("Error decoding DCGM Device Container Udage Result for device '%s': empty value returned", uuid)
+	}
 
 
-func DecodeResourceQuotaStatusUsedRAMLimitMaxResult(result *QueryResult) *ResourceQuotaStatusUsedRAMLimitMaxResult {
-	return DecodeResourceQuotaMetricResult(result)
+	return &DCGMDeviceContainerUsageResult{
+		UUID:      uuid,
+		PodUID:    podUID,
+		Container: container,
+		Value:     value,
+	}
 }
 }
 
 
 func DecodeAll[T any](results []*QueryResult, decode ResultDecoder[T]) []*T {
 func DecodeAll[T any](results []*QueryResult, decode ResultDecoder[T]) []*T {

+ 20 - 0
core/pkg/source/queryresult.go

@@ -2,6 +2,7 @@ package source
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"strconv"
 	"strings"
 	"strings"
 
 
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/log"
@@ -173,6 +174,25 @@ func (qr *QueryResult) GetString(field string) (string, error) {
 	return strField, nil
 	return strField, nil
 }
 }
 
 
+func (qr *QueryResult) GetBool(field string) (bool, error) {
+	f, ok := qr.Metric[field]
+	if !ok {
+		return false, fmt.Errorf("'%s' field does not exist in data result vector", field)
+	}
+
+	switch f.(type) {
+	case bool:
+		return f.(bool), nil
+	case string:
+		b, err := strconv.ParseBool(f.(string))
+		if err != nil {
+			return false, fmt.Errorf("string value for field could not be parsed to bool: %w", err)
+		}
+		return b, nil
+	}
+	return false, fmt.Errorf("field did not have an appropriate type for bool conversion: %T", f)
+}
+
 // GetStrings returns the requested fields, or an error if it does not exist
 // GetStrings returns the requested fields, or an error if it does not exist
 func (qr *QueryResult) GetStrings(fields ...string) (map[string]string, error) {
 func (qr *QueryResult) GetStrings(fields ...string) (map[string]string, error) {
 	values := map[string]string{}
 	values := map[string]string{}

+ 33 - 0
core/pkg/storage/storefactory.go

@@ -5,6 +5,7 @@ import (
 	"os"
 	"os"
 
 
 	"github.com/opencost/opencost/core/pkg/env"
 	"github.com/opencost/opencost/core/pkg/env"
+	"github.com/opencost/opencost/core/pkg/log"
 )
 )
 
 
 // GetDefaultStorage initializes the default shared storage which is required for kubecost. Panics
 // GetDefaultStorage initializes the default shared storage which is required for kubecost. Panics
@@ -17,6 +18,38 @@ func GetDefaultStorage() Storage {
 	return store
 	return store
 }
 }
 
 
+// GetConfiguredStorage retrieves the default shared storage which is required for running an opencost.
+func GetConfiguredStorage() Storage {
+	const warningMessage = `Failed to create local directory '%s' - %s.
+		Did you mean to enable the collector? For persistent storage, it's recommended to use Prometheus, 
+		or set a storage bucket configuration at %s. 
+
+		%s`
+
+	// Try bucket storage if it exists
+	store, err := TryGetDefaultStorage()
+	if err == nil {
+		return store
+	}
+
+	// Fallback to a local storage bucket
+	dir := env.GetConfigPath()
+	err = os.MkdirAll(dir, os.ModePerm)
+	if err != nil {
+		log.Warnf(
+			warningMessage,
+			dir,
+			err.Error(),
+			env.GetDefaultStorageConfigFilePath(),
+			"Falling back to an in-memory file system for collector, which will lose any persistent storage upon restart.",
+		)
+
+		return NewMemoryStorage()
+	}
+
+	return NewFileStorage(dir)
+}
+
 // TryGetDefaultStorage will attempt to load the default bucket configuration, but will not panic
 // TryGetDefaultStorage will attempt to load the default bucket configuration, but will not panic
 // if the config file does not exist.
 // if the config file does not exist.
 func TryGetDefaultStorage() (Storage, error) {
 func TryGetDefaultStorage() (Storage, error) {

+ 80 - 81
go.mod

@@ -60,7 +60,7 @@ require (
 	golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
 	golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
 	golang.org/x/oauth2 v0.35.0
 	golang.org/x/oauth2 v0.35.0
 	golang.org/x/sync v0.20.0
 	golang.org/x/sync v0.20.0
-	golang.org/x/text v0.35.0
+	golang.org/x/text v0.36.0
 	google.golang.org/api v0.269.0
 	google.golang.org/api v0.269.0
 	google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
 	google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
 	k8s.io/api v0.36.0
 	k8s.io/api v0.36.0
@@ -69,87 +69,31 @@ require (
 )
 )
 
 
 require (
 require (
-	cel.dev/expr v0.25.1 // indirect
-	cloud.google.com/go/monitoring v1.24.3 // indirect
-	github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
-	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
-	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
-	github.com/Masterminds/semver/v3 v3.4.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
-	github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
-	github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
-	github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
-	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
-	github.com/gabriel-vasile/mimetype v1.4.13 // indirect
-	github.com/go-ini/ini v1.67.0 // indirect
-	github.com/go-jose/go-jose/v4 v4.1.4 // indirect
-	github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
-	github.com/go-openapi/swag/conv v0.25.5 // indirect
-	github.com/go-openapi/swag/fileutils v0.25.5 // indirect
-	github.com/go-openapi/swag/jsonname v0.25.5 // indirect
-	github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
-	github.com/go-openapi/swag/loading v0.25.5 // indirect
-	github.com/go-openapi/swag/mangling v0.25.5 // indirect
-	github.com/go-openapi/swag/netutils v0.25.5 // indirect
-	github.com/go-openapi/swag/stringutils v0.25.5 // indirect
-	github.com/go-openapi/swag/typeutils v0.25.5 // indirect
-	github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
-	github.com/go-playground/locales v0.14.1 // indirect
-	github.com/go-playground/universal-translator v0.18.1 // indirect
-	github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
-	github.com/gofrs/flock v0.13.0 // indirect
-	github.com/google/go-querystring v1.1.0 // indirect
-	github.com/google/jsonschema-go v0.4.3 // indirect
-	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
-	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
-	github.com/klauspost/crc32 v1.3.0 // indirect
-	github.com/leodido/go-urn v1.4.0 // indirect
-	github.com/minio/crc64nvme v1.1.1 // indirect
-	github.com/minio/minio-go/v7 v7.0.98 // indirect
-	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
-	github.com/philhofer/fwd v1.2.0 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
-	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
-	github.com/prometheus/common v0.67.5 // indirect
-	github.com/sagikazarmark/locafero v0.12.0 // indirect
-	github.com/segmentio/asm v1.2.1 // indirect
-	github.com/segmentio/encoding v0.5.4 // indirect
-	github.com/sony/gobreaker v1.0.0 // indirect
-	github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
-	github.com/tinylib/msgp v1.6.3 // indirect
-	github.com/x448/float16 v0.8.4 // indirect
-	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
-	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
-	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
-	go.opentelemetry.io/contrib/detectors/gcp v1.40.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.41.0 // indirect
-	go.opentelemetry.io/otel/sdk/metric v1.41.0 // indirect
-	go.yaml.in/yaml/v2 v2.4.3 // indirect
-	go.yaml.in/yaml/v3 v3.0.4 // indirect
-	golang.org/x/telemetry v0.0.0-20260213145524-e0ab670178e1 // indirect
-	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
-	k8s.io/kubelet v0.36.0 // indirect
-	sigs.k8s.io/randfill v1.0.0 // indirect
-	sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
-	sigs.k8s.io/yaml v1.6.0 // indirect
+	github.com/google/go-cmp v0.7.0
+	github.com/oracle/oci-go-sdk/v65 v65.117.1
 )
 )
 
 
 require (
 require (
+	cel.dev/expr v0.25.1 // indirect
 	cloud.google.com/go v0.123.0 // indirect
 	cloud.google.com/go v0.123.0 // indirect
 	cloud.google.com/go/auth v0.18.2 // indirect
 	cloud.google.com/go/auth v0.18.2 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
 	cloud.google.com/go/iam v1.5.3 // indirect
 	cloud.google.com/go/iam v1.5.3 // indirect
+	cloud.google.com/go/monitoring v1.24.3 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
-	github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect
-	github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 // indirect
-	github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect
+	github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
+	github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
+	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
 	github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
 	github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
 	github.com/Azure/go-autorest/autorest/validation v0.3.2 // indirect
 	github.com/Azure/go-autorest/autorest/validation v0.3.2 // indirect
-	github.com/Azure/go-autorest/logger v0.2.2 // indirect
-	github.com/Azure/go-autorest/tracing v0.6.1 // indirect
+	github.com/Azure/go-autorest/logger v0.2.1 // indirect
+	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
+	github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
+	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
+	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
+	github.com/Masterminds/semver/v3 v3.4.0 // indirect
 	github.com/apache/arrow/go/v15 v15.0.2 // indirect
 	github.com/apache/arrow/go/v15 v15.0.2 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
@@ -160,86 +104,141 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
+	github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
 	github.com/dimchansky/utfbom v1.1.1 // indirect
 	github.com/dimchansky/utfbom v1.1.1 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/emicklei/go-restful/v3 v3.13.0 // indirect
 	github.com/emicklei/go-restful/v3 v3.13.0 // indirect
+	github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
+	github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
 	github.com/fatih/color v1.18.0 // indirect
 	github.com/fatih/color v1.18.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.9.0 // indirect
 	github.com/fsnotify/fsnotify v1.9.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.13 // indirect
+	github.com/go-ini/ini v1.67.0 // indirect
+	github.com/go-jose/go-jose/v4 v4.1.4 // indirect
 	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/jsonpointer v0.22.4 // indirect
 	github.com/go-openapi/jsonpointer v0.22.4 // indirect
 	github.com/go-openapi/jsonreference v0.21.4 // indirect
 	github.com/go-openapi/jsonreference v0.21.4 // indirect
 	github.com/go-openapi/swag v0.25.5 // indirect
 	github.com/go-openapi/swag v0.25.5 // indirect
+	github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
+	github.com/go-openapi/swag/conv v0.25.5 // indirect
+	github.com/go-openapi/swag/fileutils v0.25.5 // indirect
+	github.com/go-openapi/swag/jsonname v0.25.5 // indirect
+	github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
+	github.com/go-openapi/swag/loading v0.25.5 // indirect
+	github.com/go-openapi/swag/mangling v0.25.5 // indirect
+	github.com/go-openapi/swag/netutils v0.25.5 // indirect
+	github.com/go-openapi/swag/stringutils v0.25.5 // indirect
+	github.com/go-openapi/swag/typeutils v0.25.5 // indirect
+	github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
+	github.com/gofrs/flock v0.13.0 // indirect
 	github.com/gofrs/uuid v4.4.0+incompatible // indirect
 	github.com/gofrs/uuid v4.4.0+incompatible // indirect
-	github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
+	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 	github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
 	github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
-	github.com/google/flatbuffers v25.12.19+incompatible // indirect
+	github.com/google/flatbuffers v23.5.26+incompatible // indirect
 	github.com/google/gnostic-models v0.7.1 // indirect
 	github.com/google/gnostic-models v0.7.1 // indirect
-	github.com/google/go-cmp v0.7.0
+	github.com/google/go-querystring v1.1.0 // indirect
+	github.com/google/jsonschema-go v0.4.3 // indirect
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
 	github.com/googleapis/gax-go/v2 v2.17.0 // indirect
 	github.com/googleapis/gax-go/v2 v2.17.0 // indirect
 	github.com/gorilla/css v1.0.1 // indirect
 	github.com/gorilla/css v1.0.1 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
+	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
 	github.com/hashicorp/yamux v0.1.2 // indirect
 	github.com/hashicorp/yamux v0.1.2 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.18.4 // indirect
 	github.com/klauspost/compress v1.18.4 // indirect
 	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+	github.com/klauspost/crc32 v1.3.0 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
+	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/minio/crc64nvme v1.1.1 // indirect
 	github.com/minio/md5-simd v1.1.2 // indirect
 	github.com/minio/md5-simd v1.1.2 // indirect
+	github.com/minio/minio-go/v7 v7.0.98 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
 	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/oklog/run v1.2.0 // indirect
 	github.com/oklog/run v1.2.0 // indirect
 	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
 	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
-	github.com/oracle/oci-go-sdk/v65 v65.109.0
-	github.com/pierrec/lz4/v4 v4.1.25 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+	github.com/philhofer/fwd v1.2.0 // indirect
+	github.com/pierrec/lz4/v4 v4.1.18 // indirect
 	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	github.com/prometheus/procfs v0.20.1 // indirect
+	github.com/prometheus/common v0.67.5 // indirect
+	github.com/prometheus/procfs v0.19.2 // indirect
 	github.com/rs/xid v1.6.0 // indirect
 	github.com/rs/xid v1.6.0 // indirect
+	github.com/sagikazarmark/locafero v0.12.0 // indirect
+	github.com/segmentio/asm v1.2.1 // indirect
+	github.com/segmentio/encoding v0.5.4 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
+	github.com/sony/gobreaker/v2 v2.4.0 // indirect
 	github.com/spf13/afero v1.15.0 // indirect
 	github.com/spf13/afero v1.15.0 // indirect
 	github.com/spf13/cast v1.10.0 // indirect
 	github.com/spf13/cast v1.10.0 // indirect
 	github.com/spf13/pflag v1.0.10 // indirect
 	github.com/spf13/pflag v1.0.10 // indirect
+	github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
-	github.com/zeebo/xxh3 v1.1.0 // indirect
+	github.com/tinylib/msgp v1.6.3 // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
+	github.com/zeebo/xxh3 v1.0.2 // indirect
+	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+	go.opentelemetry.io/contrib/detectors/gcp v1.40.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
 	go.opentelemetry.io/otel/metric v1.41.0 // indirect
 	go.opentelemetry.io/otel/metric v1.41.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.41.0 // indirect
+	go.opentelemetry.io/otel/sdk/metric v1.41.0 // indirect
 	go.opentelemetry.io/otel/trace v1.41.0 // indirect
 	go.opentelemetry.io/otel/trace v1.41.0 // indirect
-	go.uber.org/atomic v1.10.0 // indirect
+	go.yaml.in/yaml/v2 v2.4.3 // indirect
+	go.yaml.in/yaml/v3 v3.0.4 // indirect
 	golang.org/x/crypto v0.49.0 // indirect
 	golang.org/x/crypto v0.49.0 // indirect
-	golang.org/x/mod v0.33.0 // indirect
+	golang.org/x/mod v0.34.0 // indirect
 	golang.org/x/net v0.52.0 // indirect
 	golang.org/x/net v0.52.0 // indirect
 	golang.org/x/sys v0.42.0 // indirect
 	golang.org/x/sys v0.42.0 // indirect
+	golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
 	golang.org/x/term v0.41.0 // indirect
 	golang.org/x/term v0.41.0 // indirect
 	golang.org/x/time v0.14.0 // indirect
 	golang.org/x/time v0.14.0 // indirect
-	golang.org/x/tools v0.42.0 // indirect
+	golang.org/x/tools v0.43.0 // indirect
 	golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
 	golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
 	google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 // indirect
 	google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect
 	google.golang.org/grpc v1.79.3 // indirect
 	google.golang.org/grpc v1.79.3 // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
-	gopkg.in/ini.v1 v1.67.1 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/klog/v2 v2.140.0 // indirect
 	k8s.io/klog/v2 v2.140.0 // indirect
 	k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
 	k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
+	k8s.io/kubelet v0.36.0 // indirect
 	k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
 	k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
 	sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
 	sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
+	sigs.k8s.io/yaml v1.6.0 // indirect
 )
 )

+ 28 - 42
go.sum

@@ -45,17 +45,14 @@ github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5ne
 github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE=
 github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE=
 github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs=
 github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs=
 github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
 github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
+github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc=
 github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk=
 github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk=
-github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4=
-github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8=
 github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM=
 github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM=
 github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo=
 github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc=
 github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0=
 github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0=
-github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 h1:Q9R3utmFg9K1B4OYtAZ7ZUUvIUdzQt7G2MN5Hi/d670=
-github.com/Azure/go-autorest/autorest/azure/cli v0.4.7/go.mod h1:bVrAueELJ0CKLBpUHDIvD516TwmHmzqwCpvONWRsw3s=
+github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
 github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
 github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
-github.com/Azure/go-autorest/autorest/date v0.3.1 h1:o9Z8Jyt+VJJTCZ/UORishuHOusBwolhjokt9s5k8I4w=
-github.com/Azure/go-autorest/autorest/date v0.3.1/go.mod h1:Dz/RDmXlfiFFS/eW+b/xMUSFs1tboPVy6UjgADToWDM=
 github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
 github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
 github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw=
 github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw=
 github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
 github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
@@ -63,12 +60,10 @@ github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+W
 github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M=
 github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M=
 github.com/Azure/go-autorest/autorest/validation v0.3.2 h1:myD3tcvs+Fk1bkJ1Xx7xidop4z4FWvWADiMGMXeVd2E=
 github.com/Azure/go-autorest/autorest/validation v0.3.2 h1:myD3tcvs+Fk1bkJ1Xx7xidop4z4FWvWADiMGMXeVd2E=
 github.com/Azure/go-autorest/autorest/validation v0.3.2/go.mod h1:4z7eU88lSINAB5XL8mhfPumiUdoAQo/c7qXwbsM8Zhc=
 github.com/Azure/go-autorest/autorest/validation v0.3.2/go.mod h1:4z7eU88lSINAB5XL8mhfPumiUdoAQo/c7qXwbsM8Zhc=
+github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
 github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
 github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
-github.com/Azure/go-autorest/logger v0.2.2 h1:hYqBsEBywrrOSW24kkOCXRcKfKhK76OzLTfF+MYDE2o=
-github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos9XYr9dYTFzpqgibw=
+github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
-github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0=
-github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc=
 github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
 github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
 github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
 github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
@@ -245,16 +240,15 @@ github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
 github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
 github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
 github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
+github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
-github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
 github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
 github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
-github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
+github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
 github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
 github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
 github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -371,16 +365,16 @@ github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
 github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
 github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
-github.com/oracle/oci-go-sdk/v65 v65.109.0 h1:EsbFVvcV+uid9SoQnFQbTKS6FgqsM9NtoKmUIovKsbk=
-github.com/oracle/oci-go-sdk/v65 v65.109.0/go.mod h1:8ZzvzuEG/cFLFZhxg/Mg1w19KqyXBKO3c17QIc5PkGs=
+github.com/oracle/oci-go-sdk/v65 v65.117.1 h1:DurEtbj8xEuF2B6K73uifNqWJ4dh6CJPADVmNzclrGU=
+github.com/oracle/oci-go-sdk/v65 v65.117.1/go.mod h1:oo33NDf2XPqx3/N6oLG4jFlrqJ0xu4Rlt9SfuAbtDFs=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
 github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
 github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
 github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
 github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
 github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
-github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
-github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
+github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
+github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -396,8 +390,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
 github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
 github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
 github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
 github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
-github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
-github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
+github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
+github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
 github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
@@ -417,8 +411,8 @@ github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfv
 github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
 github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
 github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
 github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
 github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
 github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
-github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
-github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/sony/gobreaker/v2 v2.4.0 h1:g2KJRW1Ubty3+ZOcSEUN7K+REQJdN6yo6XvaML+jptg=
+github.com/sony/gobreaker/v2 v2.4.0/go.mod h1:pTyFJgcZ3h2tdQVLZZruK2C0eoFL1fb/G83wK1ZQl+s=
 github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
 github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
 github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
 github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
 github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
 github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -437,17 +431,11 @@ github.com/stackitcloud/stackit-sdk-go/core v0.24.0/go.mod h1:osMglDby4csGZ5sIfh
 github.com/stackitcloud/stackit-sdk-go/services/cost v0.2.1 h1:U2sBfMeBCdZUvCW+vqPbo+HPtGxMjCF21PYyQncPnpg=
 github.com/stackitcloud/stackit-sdk-go/services/cost v0.2.1 h1:U2sBfMeBCdZUvCW+vqPbo+HPtGxMjCF21PYyQncPnpg=
 github.com/stackitcloud/stackit-sdk-go/services/cost v0.2.1/go.mod h1:Qt/scoasQrONlQ9FauvafUJ/3sP3xIFnhBQC8/Yhqgc=
 github.com/stackitcloud/stackit-sdk-go/services/cost v0.2.1/go.mod h1:Qt/scoasQrONlQ9FauvafUJ/3sP3xIFnhBQC8/Yhqgc=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -467,8 +455,8 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
 github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
 github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
 github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
-github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
-github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
+github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
+github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
 go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
 go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
 go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
 go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
 go.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/rxAWVlHsHIZ3fT2sA=
 go.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/rxAWVlHsHIZ3fT2sA=
@@ -489,9 +477,8 @@ go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3Tri
 go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=
 go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=
 go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
 go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
 go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
 go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
-go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
 go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
@@ -521,8 +508,8 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
-golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
+golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
+golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -561,8 +548,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
 golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
 golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/telemetry v0.0.0-20260213145524-e0ab670178e1 h1:QNaHp8YvpPswfDNxlCmJyeesxbGOgaKf41iT9/QrErY=
-golang.org/x/telemetry v0.0.0-20260213145524-e0ab670178e1/go.mod h1:NuITXsA9cTiqnXtVk+/wrBT2Ja4X5hsfGOYRJ6kgYjs=
+golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=
+golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -577,8 +564,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
+golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
 golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
 golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
 golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
 golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
 golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -588,8 +575,8 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
-golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
+golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
+golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -623,9 +610,8 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf
 gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
 gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
-gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

+ 3 - 0
justfile

@@ -9,6 +9,9 @@ default:
 # format all Go code
 # format all Go code
 fmt:
 fmt:
     go fmt ./...
     go fmt ./...
+    cd ./core && go fmt ./...
+    cd ./modules/collector-source && go fmt ./...
+    cd ./modules/prometheus-source && go fmt ./...
 
 
 # check if code is formatted
 # check if code is formatted
 fmt-check:
 fmt-check:

+ 3 - 0
modules/collector-source/go.mod

@@ -8,6 +8,7 @@ require (
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/kubecost/events v0.0.8
 	github.com/kubecost/events v0.0.8
 	github.com/opencost/opencost/core v0.0.0-20250521155634-81d2b597d1bc
 	github.com/opencost/opencost/core v0.0.0-20250521155634-81d2b597d1bc
+	github.com/stretchr/testify v1.11.1
 	golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
 	golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
 	k8s.io/api v0.36.0
 	k8s.io/api v0.36.0
 	k8s.io/apimachinery v0.36.0
 	k8s.io/apimachinery v0.36.0
@@ -86,6 +87,7 @@ require (
 	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
 	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
 	github.com/prometheus/common v0.67.5 // indirect
 	github.com/prometheus/common v0.67.5 // indirect
 	github.com/rs/xid v1.6.0 // indirect
 	github.com/rs/xid v1.6.0 // indirect
@@ -126,6 +128,7 @@ require (
 	google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
 	google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/client-go v0.36.0 // indirect
 	k8s.io/client-go v0.36.0 // indirect
 	k8s.io/klog/v2 v2.140.0 // indirect
 	k8s.io/klog/v2 v2.140.0 // indirect
 	k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
 	k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 796 - 23
modules/collector-source/pkg/collector/collector.go


+ 265 - 41
modules/collector-source/pkg/collector/metricsquerier.go

@@ -86,6 +86,14 @@ func (c *collectorMetricsQuerier) QueryLocalStorageBytes(start, end time.Time) *
 	return queryCollector(c, start, end, metric.LocalStorageBytesID, source.DecodeLocalStorageBytesResult)
 	return queryCollector(c, start, end, metric.LocalStorageBytesID, source.DecodeLocalStorageBytesResult)
 }
 }
 
 
+func (c *collectorMetricsQuerier) QueryNodeInfo(start, end time.Time) *source.Future[source.NodeInfoResult] {
+	return queryCollector(c, start, end, metric.NodeInfoID, source.DecodeNodeInfoResult)
+}
+
+func (c *collectorMetricsQuerier) QueryNodeUptime(start, end time.Time) *source.Future[source.UptimeResult] {
+	return queryCollector(c, start, end, metric.NodeUptimeID, source.DecodeUptimeResult)
+}
+
 func (c *collectorMetricsQuerier) QueryNodeActiveMinutes(start, end time.Time) *source.Future[source.NodeActiveMinutesResult] {
 func (c *collectorMetricsQuerier) QueryNodeActiveMinutes(start, end time.Time) *source.Future[source.NodeActiveMinutesResult] {
 	return queryCollector(c, start, end, metric.NodeActiveMinutesID, source.DecodeNodeActiveMinutesResult)
 	return queryCollector(c, start, end, metric.NodeActiveMinutesID, source.DecodeNodeActiveMinutesResult)
 }
 }
@@ -195,6 +203,14 @@ func (c *collectorMetricsQuerier) QueryNodeRAMUserPercent(start, end time.Time)
 	return f
 	return f
 }
 }
 
 
+func (c *collectorMetricsQuerier) QueryNodeResourceCapacities(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.NodeResourceCapacitiesID, source.DecodeResourceResult)
+}
+
+func (c *collectorMetricsQuerier) QueryNodeResourcesAllocatable(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.NodeResourcesAllocatableID, source.DecodeResourceResult)
+}
+
 func (c *collectorMetricsQuerier) QueryLBActiveMinutes(start, end time.Time) *source.Future[source.LBActiveMinutesResult] {
 func (c *collectorMetricsQuerier) QueryLBActiveMinutes(start, end time.Time) *source.Future[source.LBActiveMinutesResult] {
 	return queryCollector(c, start, end, metric.LBActiveMinutesID, source.DecodeLBActiveMinutesResult)
 	return queryCollector(c, start, end, metric.LBActiveMinutesID, source.DecodeLBActiveMinutesResult)
 }
 }
@@ -203,6 +219,10 @@ func (c *collectorMetricsQuerier) QueryLBPricePerHr(start, end time.Time) *sourc
 	return queryCollector(c, start, end, metric.LBPricePerHourID, source.DecodeLBPricePerHrResult)
 	return queryCollector(c, start, end, metric.LBPricePerHourID, source.DecodeLBPricePerHrResult)
 }
 }
 
 
+func (c *collectorMetricsQuerier) QueryClusterInfo(start, end time.Time) *source.Future[source.ClusterInfoResult] {
+	return queryCollector(c, start, end, metric.ClusterInfoID, source.DecodeClusterInfoResult)
+}
+
 func (c *collectorMetricsQuerier) QueryClusterUptime(start, end time.Time) *source.Future[source.UptimeResult] {
 func (c *collectorMetricsQuerier) QueryClusterUptime(start, end time.Time) *source.Future[source.UptimeResult] {
 	return queryCollector(c, start, end, metric.ClusterUptimeID, source.DecodeUptimeResult)
 	return queryCollector(c, start, end, metric.ClusterUptimeID, source.DecodeUptimeResult)
 }
 }
@@ -224,6 +244,42 @@ func (c *collectorMetricsQuerier) QueryPodsUID(start, end time.Time) *source.Fut
 	return queryCollector(c, start, end, metric.PodActiveMinutesID, source.DecodePodsResult)
 	return queryCollector(c, start, end, metric.PodActiveMinutesID, source.DecodePodsResult)
 }
 }
 
 
+func (c *collectorMetricsQuerier) QueryPodInfo(start, end time.Time) *source.Future[source.PodInfoResult] {
+	return queryCollector(c, start, end, metric.PodInfoID, source.DecodePodInfoResult)
+}
+
+func (c *collectorMetricsQuerier) QueryPodUptime(start, end time.Time) *source.Future[source.UptimeResult] {
+	return queryCollector(c, start, end, metric.PodUptimeID, source.DecodeUptimeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryPodOwners(start, end time.Time) *source.Future[source.OwnerResult] {
+	return queryCollector(c, start, end, metric.PodOwnerID, source.DecodeOwnerResult)
+}
+
+func (c *collectorMetricsQuerier) QueryPodPVCVolumes(start, end time.Time) *source.Future[source.PodPVCVolumeResult] {
+	return queryCollector(c, start, end, metric.PodPVCVolumeID, source.DecodePodPVCVolumeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryPodNetworkEgressBytes(start, end time.Time) *source.Future[source.PodNetworkBytesResult] {
+	return queryCollector(c, start, end, metric.PodNetworkEgressBytesID, source.DecodePodNetworkBytesResult)
+}
+
+func (c *collectorMetricsQuerier) QueryPodNetworkIngressBytes(start, end time.Time) *source.Future[source.PodNetworkBytesResult] {
+	return queryCollector(c, start, end, metric.PodNetworkIngressBytesID, source.DecodePodNetworkBytesResult)
+}
+
+func (c *collectorMetricsQuerier) QueryContainerUptime(start, end time.Time) *source.Future[source.ContainerUptimeResult] {
+	return queryCollector(c, start, end, metric.ContainerUptimeID, source.DecodeContainerUptimeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryContainerResourceRequests(start, end time.Time) *source.Future[source.ContainerResourceResult] {
+	return queryCollector(c, start, end, metric.ContainerResourceRequestsID, source.DecodeContainerResourceResult)
+}
+
+func (c *collectorMetricsQuerier) QueryContainerResourceLimits(start, end time.Time) *source.Future[source.ContainerResourceResult] {
+	return queryCollector(c, start, end, metric.ContainerResourceLimitsID, source.DecodeContainerResourceResult)
+}
+
 func (c *collectorMetricsQuerier) QueryRAMBytesAllocated(start, end time.Time) *source.Future[source.RAMBytesAllocatedResult] {
 func (c *collectorMetricsQuerier) QueryRAMBytesAllocated(start, end time.Time) *source.Future[source.RAMBytesAllocatedResult] {
 	return queryCollector(c, start, end, metric.RAMBytesAllocatedID, source.DecodeRAMBytesAllocatedResult)
 	return queryCollector(c, start, end, metric.RAMBytesAllocatedID, source.DecodeRAMBytesAllocatedResult)
 }
 }
@@ -300,6 +356,22 @@ func (c *collectorMetricsQuerier) QueryIsGPUShared(start, end time.Time) *source
 	return queryCollector(c, start, end, metric.IsGPUSharedID, source.DecodeIsGPUSharedResult)
 	return queryCollector(c, start, end, metric.IsGPUSharedID, source.DecodeIsGPUSharedResult)
 }
 }
 
 
+func (c *collectorMetricsQuerier) QueryDCGMDeviceInfo(start, end time.Time) *source.Future[source.DCGMDeviceInfoResult] {
+	return queryCollector(c, start, end, metric.DCGMInfoID, source.DecodeDCGMDeviceInfoResult)
+}
+
+func (c *collectorMetricsQuerier) QueryDCGMDeviceUptime(start, end time.Time) *source.Future[source.DCGMDeviceUptimeResult] {
+	return queryCollector(c, start, end, metric.DCGMUptimeID, source.DecodeDCGMDeviceUptimeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryDCGMContainerUsageAvg(start, end time.Time) *source.Future[source.DCGMDeviceContainerUsageResult] {
+	return queryCollector(c, start, end, metric.DCGMContainerUsageAvgID, source.DecodeDCGMDeviceContainerUsageResult)
+}
+
+func (c *collectorMetricsQuerier) QueryDCGMContainerUsageMax(start, end time.Time) *source.Future[source.DCGMDeviceContainerUsageResult] {
+	return queryCollector(c, start, end, metric.DCGMContainerUsageMaxID, source.DecodeDCGMDeviceContainerUsageResult)
+}
+
 func (c *collectorMetricsQuerier) QueryPodPVCAllocation(start, end time.Time) *source.Future[source.PodPVCAllocationResult] {
 func (c *collectorMetricsQuerier) QueryPodPVCAllocation(start, end time.Time) *source.Future[source.PodPVCAllocationResult] {
 	return queryCollector(c, start, end, metric.PodPVCAllocationID, source.DecodePodPVCAllocationResult)
 	return queryCollector(c, start, end, metric.PodPVCAllocationID, source.DecodePodPVCAllocationResult)
 }
 }
@@ -312,6 +384,26 @@ func (c *collectorMetricsQuerier) QueryPVCInfo(start, end time.Time) *source.Fut
 	return queryCollector(c, start, end, metric.PVCInfoID, source.DecodePVCInfoResult)
 	return queryCollector(c, start, end, metric.PVCInfoID, source.DecodePVCInfoResult)
 }
 }
 
 
+func (c *collectorMetricsQuerier) QueryKMPVCInfo(start, end time.Time) *source.Future[source.PVCInfoResult] {
+	return queryCollector(c, start, end, metric.KMPVCInfoID, source.DecodePVCInfoResult)
+}
+
+func (c *collectorMetricsQuerier) QueryKMLocalStorageUsedAvg(start, end time.Time) *source.Future[source.NodeUIDValueResult] {
+	return queryCollector(c, start, end, metric.KMLocalStorageUsedAverageID, source.DecodeNodeUIDValueResult)
+}
+
+func (c *collectorMetricsQuerier) QueryKMLocalStorageUsedMax(start, end time.Time) *source.Future[source.NodeUIDValueResult] {
+	return queryCollector(c, start, end, metric.KMLocalStorageUsedMaxID, source.DecodeNodeUIDValueResult)
+}
+
+func (c *collectorMetricsQuerier) QueryKMLocalStorageBytes(start, end time.Time) *source.Future[source.UIDValueResult] {
+	return queryCollector(c, start, end, metric.KMLocalStorageBytesID, source.DecodeUIDValueResult)
+}
+
+func (c *collectorMetricsQuerier) QueryPVCUptime(start, end time.Time) *source.Future[source.UptimeResult] {
+	return queryCollector(c, start, end, metric.PVCUptimeID, source.DecodeUptimeResult)
+}
+
 func (c *collectorMetricsQuerier) QueryPVBytes(start, end time.Time) *source.Future[source.PVBytesResult] {
 func (c *collectorMetricsQuerier) QueryPVBytes(start, end time.Time) *source.Future[source.PVBytesResult] {
 	return queryCollector(c, start, end, metric.PVBytesID, source.DecodePVBytesResult)
 	return queryCollector(c, start, end, metric.PVBytesID, source.DecodePVBytesResult)
 }
 }
@@ -324,6 +416,26 @@ func (c *collectorMetricsQuerier) QueryPVInfo(start, end time.Time) *source.Futu
 	return queryCollector(c, start, end, metric.PVInfoID, source.DecodePVInfoResult)
 	return queryCollector(c, start, end, metric.PVInfoID, source.DecodePVInfoResult)
 }
 }
 
 
+func (c *collectorMetricsQuerier) QueryKMPVInfo(start, end time.Time) *source.Future[source.PVInfoResult] {
+	return queryCollector(c, start, end, metric.KMPVInfoID, source.DecodePVInfoResult)
+}
+
+func (c *collectorMetricsQuerier) QueryPVCBytesUsedAverage(start, end time.Time) *source.Future[source.PVCUIDValueResult] {
+	return queryCollector(c, start, end, metric.PVCBytesUsedAverageID, source.DecodePVCUIDValueResult)
+}
+
+func (c *collectorMetricsQuerier) QueryPVCBytesUsedMax(start, end time.Time) *source.Future[source.PVCUIDValueResult] {
+	return queryCollector(c, start, end, metric.PVCBytesUsedMaxID, source.DecodePVCUIDValueResult)
+}
+
+func (c *collectorMetricsQuerier) QueryPVUptime(start, end time.Time) *source.Future[source.UptimeResult] {
+	return queryCollector(c, start, end, metric.PVUptimeID, source.DecodeUptimeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryNamespaceInfo(start, end time.Time) *source.Future[source.NamespaceInfoResult] {
+	return queryCollector(c, start, end, metric.NamespaceInfoID, source.DecodeNamespaceInfoResult)
+}
+
 func (c *collectorMetricsQuerier) QueryNamespaceUptime(start, end time.Time) *source.Future[source.UptimeResult] {
 func (c *collectorMetricsQuerier) QueryNamespaceUptime(start, end time.Time) *source.Future[source.UptimeResult] {
 	return queryCollector(c, start, end, metric.NamespaceUptimeID, source.DecodeUptimeResult)
 	return queryCollector(c, start, end, metric.NamespaceUptimeID, source.DecodeUptimeResult)
 }
 }
@@ -416,24 +528,132 @@ func (c *collectorMetricsQuerier) QueryPodLabels(start, end time.Time) *source.F
 	return queryCollector(c, start, end, metric.PodLabelsID, source.DecodePodLabelsResult)
 	return queryCollector(c, start, end, metric.PodLabelsID, source.DecodePodLabelsResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryServiceLabels(start, end time.Time) *source.Future[source.ServiceLabelsResult] {
+func (c *collectorMetricsQuerier) QueryServiceInfo(start, end time.Time) *source.Future[source.ServiceInfoResult] {
+	return queryCollector(c, start, end, metric.ServiceInfoID, source.DecodeServiceInfoResult)
+}
+
+func (c *collectorMetricsQuerier) QueryServiceUptime(start, end time.Time) *source.Future[source.UptimeResult] {
+	return queryCollector(c, start, end, metric.ServiceUptimeID, source.DecodeUptimeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryServiceSelectorLabels(start, end time.Time) *source.Future[source.ServiceLabelsResult] {
 	return queryCollector(c, start, end, metric.ServiceLabelsID, source.DecodeServiceLabelsResult)
 	return queryCollector(c, start, end, metric.ServiceLabelsID, source.DecodeServiceLabelsResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryDeploymentLabels(start, end time.Time) *source.Future[source.DeploymentLabelsResult] {
-	return queryCollector(c, start, end, metric.DeploymentLabelsID, source.DecodeDeploymentLabelsResult)
+func (c *collectorMetricsQuerier) QueryDeploymentInfo(start, end time.Time) *source.Future[source.DeploymentInfoResult] {
+	return queryCollector(c, start, end, metric.DeploymentInfoID, source.DecodeDeploymentInfoResult)
+}
+
+func (c *collectorMetricsQuerier) QueryDeploymentUptime(start, end time.Time) *source.Future[source.UptimeResult] {
+	return queryCollector(c, start, end, metric.DeploymentUptimeID, source.DecodeUptimeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryDeploymentLabels(start, end time.Time) *source.Future[source.LabelsResult] {
+	return queryCollector(c, start, end, metric.DeploymentLabelsID, source.DecodeLabelsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryDeploymentAnnotations(start, end time.Time) *source.Future[source.AnnotationsResult] {
+	return queryCollector(c, start, end, metric.DeploymentAnnotationsID, source.DecodeAnnotationsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryDeploymentMatchLabels(start, end time.Time) *source.Future[source.DeploymentLabelsResult] {
+	return queryCollector(c, start, end, metric.DeploymentMatchLabelsID, source.DecodeDeploymentLabelsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryStatefulSetInfo(start, end time.Time) *source.Future[source.StatefulSetInfoResult] {
+	return queryCollector(c, start, end, metric.StatefulSetInfoID, source.DecodeStatefulSetInfoResult)
+}
+
+func (c *collectorMetricsQuerier) QueryStatefulSetUptime(start, end time.Time) *source.Future[source.UptimeResult] {
+	return queryCollector(c, start, end, metric.StatefulSetUptimeID, source.DecodeUptimeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryStatefulSetLabels(start, end time.Time) *source.Future[source.LabelsResult] {
+	return queryCollector(c, start, end, metric.StatefulSetLabelsID, source.DecodeLabelsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryStatefulSetAnnotations(start, end time.Time) *source.Future[source.AnnotationsResult] {
+	return queryCollector(c, start, end, metric.StatefulSetAnnotationsID, source.DecodeAnnotationsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryStatefulSetMatchLabels(start, end time.Time) *source.Future[source.StatefulSetLabelsResult] {
+	return queryCollector(c, start, end, metric.StatefulSetMatchLabelsID, source.DecodeStatefulSetLabelsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryDaemonSetInfo(start, end time.Time) *source.Future[source.DaemonSetInfoResult] {
+	return queryCollector(c, start, end, metric.DaemonSetInfoID, source.DecodeDaemonSetInfoResult)
+}
+
+func (c *collectorMetricsQuerier) QueryDaemonSetUptime(start, end time.Time) *source.Future[source.UptimeResult] {
+	return queryCollector(c, start, end, metric.DaemonSetUptimeID, source.DecodeUptimeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryDaemonSetLabels(start, end time.Time) *source.Future[source.LabelsResult] {
+	return queryCollector(c, start, end, metric.DaemonSetLabelsID, source.DecodeLabelsResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryStatefulSetLabels(start, end time.Time) *source.Future[source.StatefulSetLabelsResult] {
-	return queryCollector(c, start, end, metric.StatefulSetLabelsID, source.DecodeStatefulSetLabelsResult)
+func (c *collectorMetricsQuerier) QueryDaemonSetAnnotations(start, end time.Time) *source.Future[source.AnnotationsResult] {
+	return queryCollector(c, start, end, metric.DaemonSetAnnotationsID, source.DecodeAnnotationsResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryDaemonSetLabels(start, end time.Time) *source.Future[source.DaemonSetLabelsResult] {
-	return queryCollector(c, start, end, metric.DaemonSetLabelsID, source.DecodeDaemonSetLabelsResult)
+func (c *collectorMetricsQuerier) QueryJobInfo(start, end time.Time) *source.Future[source.JobInfoResult] {
+	return queryCollector(c, start, end, metric.JobInfoID, source.DecodeJobInfoResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryJobLabels(start, end time.Time) *source.Future[source.JobLabelsResult] {
-	return queryCollector(c, start, end, metric.JobLabelsID, source.DecodeJobLabelsResult)
+func (c *collectorMetricsQuerier) QueryJobUptime(start, end time.Time) *source.Future[source.UptimeResult] {
+	return queryCollector(c, start, end, metric.JobUptimeID, source.DecodeUptimeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryJobLabels(start, end time.Time) *source.Future[source.LabelsResult] {
+	return queryCollector(c, start, end, metric.JobLabelsID, source.DecodeLabelsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryJobAnnotations(start, end time.Time) *source.Future[source.AnnotationsResult] {
+	return queryCollector(c, start, end, metric.JobAnnotationsID, source.DecodeAnnotationsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryCronJobInfo(start, end time.Time) *source.Future[source.CronJobInfoResult] {
+	return queryCollector(c, start, end, metric.CronJobInfoID, source.DecodeCronJobInfoResult)
+}
+
+func (c *collectorMetricsQuerier) QueryCronJobUptime(start, end time.Time) *source.Future[source.UptimeResult] {
+	return queryCollector(c, start, end, metric.CronJobUptimeID, source.DecodeUptimeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryCronJobLabels(start, end time.Time) *source.Future[source.LabelsResult] {
+	return queryCollector(c, start, end, metric.CronJobLabelsID, source.DecodeLabelsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryCronJobAnnotations(start, end time.Time) *source.Future[source.AnnotationsResult] {
+	return queryCollector(c, start, end, metric.CronJobAnnotationsID, source.DecodeAnnotationsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryReplicaSetInfo(start, end time.Time) *source.Future[source.ReplicaSetInfoResult] {
+	return queryCollector(c, start, end, metric.ReplicaSetInfoID, source.DecodeReplicaSetInfoResult)
+}
+
+func (c *collectorMetricsQuerier) QueryReplicaSetUptime(start, end time.Time) *source.Future[source.UptimeResult] {
+	return queryCollector(c, start, end, metric.ReplicaSetUptimeID, source.DecodeUptimeResult)
+}
+
+func (c *collectorMetricsQuerier) QueryReplicaSetLabels(start, end time.Time) *source.Future[source.LabelsResult] {
+	return queryCollector(c, start, end, metric.ReplicaSetLabelsID, source.DecodeLabelsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryReplicaSetAnnotations(start, end time.Time) *source.Future[source.AnnotationsResult] {
+	return queryCollector(c, start, end, metric.ReplicaSetAnnotationsID, source.DecodeAnnotationsResult)
+}
+
+func (c *collectorMetricsQuerier) QueryReplicaSetOwners(start, end time.Time) *source.Future[source.OwnerResult] {
+	return queryCollector(c, start, end, metric.ReplicaSetOwnerID, source.DecodeOwnerResult)
+}
+
+func (c *collectorMetricsQuerier) QueryPodsWithDaemonSetOwner(start, end time.Time) *source.Future[source.PodsWithDaemonSetOwnerResult] {
+	return queryCollector(c, start, end, metric.PodsWithDaemonSetOwnerID, source.DecodePodsWithDaemonSetOwnerResult)
+}
+
+func (c *collectorMetricsQuerier) QueryPodsWithJobOwner(start, end time.Time) *source.Future[source.PodsWithJobOwnerResult] {
+	return queryCollector(c, start, end, metric.PodsWithJobOwnerID, source.DecodePodsWithJobOwnerResult)
 }
 }
 
 
 func (c *collectorMetricsQuerier) QueryPodsWithReplicaSetOwner(start, end time.Time) *source.Future[source.PodsWithReplicaSetOwnerResult] {
 func (c *collectorMetricsQuerier) QueryPodsWithReplicaSetOwner(start, end time.Time) *source.Future[source.PodsWithReplicaSetOwnerResult] {
@@ -448,72 +668,76 @@ func (c *collectorMetricsQuerier) QueryReplicaSetsWithRollout(start, end time.Ti
 	return queryCollector(c, start, end, metric.ReplicaSetsWithRolloutID, source.DecodeReplicaSetsWithRolloutResult)
 	return queryCollector(c, start, end, metric.ReplicaSetsWithRolloutID, source.DecodeReplicaSetsWithRolloutResult)
 }
 }
 
 
+func (c *collectorMetricsQuerier) QueryResourceQuotaInfo(start, end time.Time) *source.Future[source.ResourceQuotaInfoResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaInfoID, source.DecodeResourceQuotaInfoResult)
+}
+
 func (c *collectorMetricsQuerier) QueryResourceQuotaUptime(start, end time.Time) *source.Future[source.UptimeResult] {
 func (c *collectorMetricsQuerier) QueryResourceQuotaUptime(start, end time.Time) *source.Future[source.UptimeResult] {
 	return queryCollector(c, start, end, metric.ResourceQuotaUptimeID, source.DecodeUptimeResult)
 	return queryCollector(c, start, end, metric.ResourceQuotaUptimeID, source.DecodeUptimeResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaSpecCPURequestAverage(start, end time.Time) *source.Future[source.ResourceQuotaSpecCPURequestAvgResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaSpecCPURequestAverageID, source.DecodeResourceQuotaSpecCPURequestAvgResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaSpecCPURequestAverage(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaSpecCPURequestAverageID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaSpecCPURequestMax(start, end time.Time) *source.Future[source.ResourceQuotaSpecCPURequestMaxResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaSpecCPURequestMaxID, source.DecodeResourceQuotaSpecCPURequestMaxResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaSpecCPURequestMax(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaSpecCPURequestMaxID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaSpecRAMRequestAverage(start, end time.Time) *source.Future[source.ResourceQuotaSpecRAMRequestAvgResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaSpecRAMRequestAverageID, source.DecodeResourceQuotaSpecRAMRequestAvgResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaSpecRAMRequestAverage(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaSpecRAMRequestAverageID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaSpecRAMRequestMax(start, end time.Time) *source.Future[source.ResourceQuotaSpecRAMRequestMaxResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaSpecRAMRequestMaxID, source.DecodeResourceQuotaSpecRAMRequestMaxResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaSpecRAMRequestMax(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaSpecRAMRequestMaxID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaSpecCPULimitAverage(start, end time.Time) *source.Future[source.ResourceQuotaSpecCPULimitAvgResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaSpecCPULimitAverageID, source.DecodeResourceQuotaSpecCPULimitAvgResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaSpecCPULimitAverage(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaSpecCPULimitAverageID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaSpecCPULimitMax(start, end time.Time) *source.Future[source.ResourceQuotaSpecCPULimitMaxResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaSpecCPULimitMaxID, source.DecodeResourceQuotaSpecCPULimitMaxResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaSpecCPULimitMax(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaSpecCPULimitMaxID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaSpecRAMLimitAverage(start, end time.Time) *source.Future[source.ResourceQuotaSpecRAMLimitAvgResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaSpecRAMLimitAverageID, source.DecodeResourceQuotaSpecRAMLimitAvgResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaSpecRAMLimitAverage(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaSpecRAMLimitAverageID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaSpecRAMLimitMax(start, end time.Time) *source.Future[source.ResourceQuotaSpecRAMLimitMaxResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaSpecRAMLimitMaxID, source.DecodeResourceQuotaSpecRAMLimitMaxResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaSpecRAMLimitMax(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaSpecRAMLimitMaxID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedCPURequestAverage(start, end time.Time) *source.Future[source.ResourceQuotaStatusUsedCPURequestAvgResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedCPURequestAverageID, source.DecodeResourceQuotaStatusUsedCPURequestAvgResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedCPURequestAverage(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedCPURequestAverageID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedCPURequestMax(start, end time.Time) *source.Future[source.ResourceQuotaStatusUsedCPURequestMaxResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedCPURequestMaxID, source.DecodeResourceQuotaStatusUsedCPURequestMaxResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedCPURequestMax(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedCPURequestMaxID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedRAMRequestAverage(start, end time.Time) *source.Future[source.ResourceQuotaStatusUsedRAMRequestAvgResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedRAMRequestAverageID, source.DecodeResourceQuotaStatusUsedRAMRequestAvgResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedRAMRequestAverage(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedRAMRequestAverageID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedRAMRequestMax(start, end time.Time) *source.Future[source.ResourceQuotaStatusUsedRAMRequestMaxResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedRAMRequestMaxID, source.DecodeResourceQuotaStatusUsedRAMRequestMaxResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedRAMRequestMax(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedRAMRequestMaxID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedCPULimitAverage(start, end time.Time) *source.Future[source.ResourceQuotaStatusUsedCPULimitAvgResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedCPULimitAverageID, source.DecodeResourceQuotaStatusUsedCPULimitAvgResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedCPULimitAverage(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedCPULimitAverageID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedCPULimitMax(start, end time.Time) *source.Future[source.ResourceQuotaStatusUsedCPULimitMaxResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedCPULimitMaxID, source.DecodeResourceQuotaStatusUsedCPULimitMaxResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedCPULimitMax(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedCPULimitMaxID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedRAMLimitAverage(start, end time.Time) *source.Future[source.ResourceQuotaStatusUsedRAMLimitAvgResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedRAMLimitAverageID, source.DecodeResourceQuotaStatusUsedRAMLimitAvgResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedRAMLimitAverage(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedRAMLimitAverageID, source.DecodeResourceResult)
 }
 }
 
 
-func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedRAMLimitMax(start, end time.Time) *source.Future[source.ResourceQuotaStatusUsedRAMLimitMaxResult] {
-	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedRAMLimitMaxID, source.DecodeResourceQuotaStatusUsedRAMLimitMaxResult)
+func (c *collectorMetricsQuerier) QueryResourceQuotaStatusUsedRAMLimitMax(start, end time.Time) *source.Future[source.ResourceResult] {
+	return queryCollector(c, start, end, metric.ResourceQuotaStatusUsedRAMLimitMaxID, source.DecodeResourceResult)
 }
 }
 
 
 func (c *collectorMetricsQuerier) QueryDataCoverage(limitDays int) (time.Time, time.Time, error) {
 func (c *collectorMetricsQuerier) QueryDataCoverage(limitDays int) (time.Time, time.Time, error) {

+ 1 - 1
modules/collector-source/pkg/collector/metricsquerier_test.go

@@ -52,7 +52,7 @@ func GetMockCollectorProvider() StoreProvider {
 	gpu1Info := map[string]string{
 	gpu1Info := map[string]string{
 		source.NamespaceLabel: "namespace1",
 		source.NamespaceLabel: "namespace1",
 		source.PodLabel:       "pod1",
 		source.PodLabel:       "pod1",
-		source.UIDLabel:       "pod-uuid1",
+		source.PodUIDLabel:    "pod-uuid1",
 		"container":           "container1",
 		"container":           "container1",
 		"gpu":                 "0",
 		"gpu":                 "0",
 		"UUID":                "GPU-1",
 		"UUID":                "GPU-1",

+ 3 - 0
modules/collector-source/pkg/event/scrape.go

@@ -14,6 +14,9 @@ const (
 	ReplicaSetScraperType    = "replicasets"
 	ReplicaSetScraperType    = "replicasets"
 	DeploymentScraperType    = "deployments"
 	DeploymentScraperType    = "deployments"
 	StatefulSetScraperType   = "statefulsets"
 	StatefulSetScraperType   = "statefulsets"
+	DaemonSetScraperType     = "daemonsets"
+	JobScraperType           = "jobs"
+	CronJobScraperType       = "cronjobs"
 	ServiceScraperType       = "services"
 	ServiceScraperType       = "services"
 	PodScraperType           = "pods"
 	PodScraperType           = "pods"
 	PvScraperType            = "pvs"
 	PvScraperType            = "pvs"

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

@@ -19,33 +19,51 @@ const (
 	PVUsedAverageID                            MetricCollectorID = "PVUsedAverage"
 	PVUsedAverageID                            MetricCollectorID = "PVUsedAverage"
 	PVUsedMaxID                                MetricCollectorID = "PVUsedMax"
 	PVUsedMaxID                                MetricCollectorID = "PVUsedMax"
 	PVCInfoID                                  MetricCollectorID = "PVCInfo"
 	PVCInfoID                                  MetricCollectorID = "PVCInfo"
+	KMPVCInfoID                                MetricCollectorID = "KMPVCInfo"
+	PVCUptimeID                                MetricCollectorID = "PVCUptime"
 	PVActiveMinutesID                          MetricCollectorID = "PVActiveMinutes"
 	PVActiveMinutesID                          MetricCollectorID = "PVActiveMinutes"
+	PVUptimeID                                 MetricCollectorID = "PVUptime"
 	LocalStorageUsedActiveMinutesID            MetricCollectorID = "LocalStorageUsedCost"
 	LocalStorageUsedActiveMinutesID            MetricCollectorID = "LocalStorageUsedCost"
 	LocalStorageUsedAverageID                  MetricCollectorID = "LocalStorageUsedAverage"
 	LocalStorageUsedAverageID                  MetricCollectorID = "LocalStorageUsedAverage"
 	LocalStorageUsedMaxID                      MetricCollectorID = "LocalStorageUsedMax"
 	LocalStorageUsedMaxID                      MetricCollectorID = "LocalStorageUsedMax"
 	LocalStorageBytesID                        MetricCollectorID = "LocalStorageBytesID"
 	LocalStorageBytesID                        MetricCollectorID = "LocalStorageBytesID"
 	LocalStorageActiveMinutesID                MetricCollectorID = "LocalStorageActiveMinutes"
 	LocalStorageActiveMinutesID                MetricCollectorID = "LocalStorageActiveMinutes"
+	KMLocalStorageUsedAverageID                MetricCollectorID = "KMLocalStorageUsedAverage"
+	KMLocalStorageUsedMaxID                    MetricCollectorID = "KMLocalStorageUsedMax"
+	KMLocalStorageBytesID                      MetricCollectorID = "KMLocalStorageBytes"
 	NodeCPUCoresCapacityID                     MetricCollectorID = "NodeCPUCoresCapacity"
 	NodeCPUCoresCapacityID                     MetricCollectorID = "NodeCPUCoresCapacity"
 	NodeCPUCoresAllocatableID                  MetricCollectorID = "NodeCPUCoresAllocatable"
 	NodeCPUCoresAllocatableID                  MetricCollectorID = "NodeCPUCoresAllocatable"
 	NodeRAMBytesCapacityID                     MetricCollectorID = "NodeRAMBytesCapacity"
 	NodeRAMBytesCapacityID                     MetricCollectorID = "NodeRAMBytesCapacity"
 	NodeRAMBytesAllocatableID                  MetricCollectorID = "NodeRAMBytesAllocatable"
 	NodeRAMBytesAllocatableID                  MetricCollectorID = "NodeRAMBytesAllocatable"
 	NodeGPUCountID                             MetricCollectorID = "NodeGPUCount"
 	NodeGPUCountID                             MetricCollectorID = "NodeGPUCount"
 	NodeLabelsID                               MetricCollectorID = "NodeLabels"
 	NodeLabelsID                               MetricCollectorID = "NodeLabels"
+	NodeInfoID                                 MetricCollectorID = "NodeInfo"
+	NodeUptimeID                               MetricCollectorID = "NodeUptime"
 	NodeActiveMinutesID                        MetricCollectorID = "NodeActiveMinutes"
 	NodeActiveMinutesID                        MetricCollectorID = "NodeActiveMinutes"
 	NodeCPUModeTotalID                         MetricCollectorID = "NodeCPUModeTotal"
 	NodeCPUModeTotalID                         MetricCollectorID = "NodeCPUModeTotal"
 	NodeRAMSystemUsageAverageID                MetricCollectorID = "NodeRAMSystemUsageAverage"
 	NodeRAMSystemUsageAverageID                MetricCollectorID = "NodeRAMSystemUsageAverage"
 	NodeRAMUserUsageAverageID                  MetricCollectorID = "NodeRAMUserUsageAverage"
 	NodeRAMUserUsageAverageID                  MetricCollectorID = "NodeRAMUserUsageAverage"
+	NodeResourceCapacitiesID                   MetricCollectorID = "NodeResourceCapacities"
+	NodeResourcesAllocatableID                 MetricCollectorID = "NodeResourcesAllocatable"
 	LBPricePerHourID                           MetricCollectorID = "LBPricePerHour"
 	LBPricePerHourID                           MetricCollectorID = "LBPricePerHour"
 	LBActiveMinutesID                          MetricCollectorID = "LBActiveMinutes"
 	LBActiveMinutesID                          MetricCollectorID = "LBActiveMinutes"
+	ClusterInfoID                              MetricCollectorID = "ClusterInfo"
 	ClusterUptimeID                            MetricCollectorID = "ClusterUptime"
 	ClusterUptimeID                            MetricCollectorID = "ClusterUptime"
 	ClusterManagementDurationID                MetricCollectorID = "ClusterManagementDuration"
 	ClusterManagementDurationID                MetricCollectorID = "ClusterManagementDuration"
 	ClusterManagementPricePerHourID            MetricCollectorID = "ClusterManagementPricePerHour"
 	ClusterManagementPricePerHourID            MetricCollectorID = "ClusterManagementPricePerHour"
 	PodActiveMinutesID                         MetricCollectorID = "PodActiveMinutes"
 	PodActiveMinutesID                         MetricCollectorID = "PodActiveMinutes"
+	PodInfoID                                  MetricCollectorID = "PodInfo"
+	PodUptimeID                                MetricCollectorID = "PodUptime"
+	PodOwnerID                                 MetricCollectorID = "PodOwner"
+	PodPVCVolumeID                             MetricCollectorID = "PodPVCVolume"
+	ContainerUptimeID                          MetricCollectorID = "ContainerUptime"
+	PodNetworkEgressBytesID                    MetricCollectorID = "PodNetworkEgressBytes"
+	PodNetworkIngressBytesID                   MetricCollectorID = "PodNetworkIngressBytes"
 	RAMBytesAllocatedID                        MetricCollectorID = "RAMBytesAllocated"
 	RAMBytesAllocatedID                        MetricCollectorID = "RAMBytesAllocated"
 	RAMRequestsID                              MetricCollectorID = "RAMRequests"
 	RAMRequestsID                              MetricCollectorID = "RAMRequests"
 	RAMLimitsID                                MetricCollectorID = "RAMLimits"
 	RAMLimitsID                                MetricCollectorID = "RAMLimits"
 	RAMUsageAverageID                          MetricCollectorID = "RAMUsageAverage"
 	RAMUsageAverageID                          MetricCollectorID = "RAMUsageAverage"
-	RAMUsageMaxID                              MetricCollectorID = "RAMUsageMax"
+	RAMUsageMaxID                              MetricCollectorID = "RAMBytesUsageMax"
 	CPUCoresAllocatedID                        MetricCollectorID = "CPUCoresAllocated"
 	CPUCoresAllocatedID                        MetricCollectorID = "CPUCoresAllocated"
 	CPURequestsID                              MetricCollectorID = "CPURequestsID"
 	CPURequestsID                              MetricCollectorID = "CPURequestsID"
 	CPULimitsID                                MetricCollectorID = "CPULimitsID"
 	CPULimitsID                                MetricCollectorID = "CPULimitsID"
@@ -61,10 +79,17 @@ const (
 	NodeRAMPricePerGiBHourID                   MetricCollectorID = "NodeRAMPricePerGiBHour"
 	NodeRAMPricePerGiBHourID                   MetricCollectorID = "NodeRAMPricePerGiBHour"
 	NodeGPUPricePerHourID                      MetricCollectorID = "NodeGPUPricePerHour"
 	NodeGPUPricePerHourID                      MetricCollectorID = "NodeGPUPricePerHour"
 	NodeIsSpotID                               MetricCollectorID = "NodeIsSpot"
 	NodeIsSpotID                               MetricCollectorID = "NodeIsSpot"
+	DCGMInfoID                                 MetricCollectorID = "DCGMInfo"
+	DCGMUptimeID                               MetricCollectorID = "DCGMUptime"
+	DCGMContainerUsageAvgID                    MetricCollectorID = "DCGMContainerUsageAvg"
+	DCGMContainerUsageMaxID                    MetricCollectorID = "DCGMContainerUsageMax"
 	PodPVCAllocationID                         MetricCollectorID = "PodPVCAllocation"
 	PodPVCAllocationID                         MetricCollectorID = "PodPVCAllocation"
 	PVCBytesRequestedID                        MetricCollectorID = "PVCBytesRequested"
 	PVCBytesRequestedID                        MetricCollectorID = "PVCBytesRequested"
+	PVCBytesUsedAverageID                      MetricCollectorID = "PVCBytesUsedAverage"
+	PVCBytesUsedMaxID                          MetricCollectorID = "PVCBytesUsedMax"
 	PVBytesID                                  MetricCollectorID = "PVBytesID"
 	PVBytesID                                  MetricCollectorID = "PVBytesID"
 	PVInfoID                                   MetricCollectorID = "PVInfo"
 	PVInfoID                                   MetricCollectorID = "PVInfo"
+	KMPVInfoID                                 MetricCollectorID = "KMPVInfo"
 	NetZoneGiBID                               MetricCollectorID = "NetZoneGiB"
 	NetZoneGiBID                               MetricCollectorID = "NetZoneGiB"
 	NetZonePricePerGiBID                       MetricCollectorID = "NetZonePricePerGiB"
 	NetZonePricePerGiBID                       MetricCollectorID = "NetZonePricePerGiB"
 	NetRegionGiBID                             MetricCollectorID = "NetRegionGiB"
 	NetRegionGiBID                             MetricCollectorID = "NetRegionGiB"
@@ -82,19 +107,50 @@ const (
 	NetInternetServiceIngressGiBID             MetricCollectorID = "NetInternetServiceIngressGiB"
 	NetInternetServiceIngressGiBID             MetricCollectorID = "NetInternetServiceIngressGiB"
 	NetNatGatewayIngressGiBID                  MetricCollectorID = "NetNatGatewayIngressGiB"
 	NetNatGatewayIngressGiBID                  MetricCollectorID = "NetNatGatewayIngressGiB"
 	NetReceiveBytesID                          MetricCollectorID = "NetReceiveBytes"
 	NetReceiveBytesID                          MetricCollectorID = "NetReceiveBytes"
+	NamespaceInfoID                            MetricCollectorID = "NamespaceInfo"
 	NamespaceUptimeID                          MetricCollectorID = "NamespaceUptime"
 	NamespaceUptimeID                          MetricCollectorID = "NamespaceUptime"
 	NamespaceLabelsID                          MetricCollectorID = "NamespaceLabels"
 	NamespaceLabelsID                          MetricCollectorID = "NamespaceLabels"
 	NamespaceAnnotationsID                     MetricCollectorID = "NamespaceAnnotations"
 	NamespaceAnnotationsID                     MetricCollectorID = "NamespaceAnnotations"
 	PodLabelsID                                MetricCollectorID = "PodLabels"
 	PodLabelsID                                MetricCollectorID = "PodLabels"
 	PodAnnotationsID                           MetricCollectorID = "PodAnnotations"
 	PodAnnotationsID                           MetricCollectorID = "PodAnnotations"
 	ServiceLabelsID                            MetricCollectorID = "ServiceLabels"
 	ServiceLabelsID                            MetricCollectorID = "ServiceLabels"
+	ServiceInfoID                              MetricCollectorID = "ServiceInfo"
+	ServiceUptimeID                            MetricCollectorID = "ServiceUptime"
+	DeploymentInfoID                           MetricCollectorID = "DeploymentInfo"
+	DeploymentUptimeID                         MetricCollectorID = "DeploymentUptime"
 	DeploymentLabelsID                         MetricCollectorID = "DeploymentLabels"
 	DeploymentLabelsID                         MetricCollectorID = "DeploymentLabels"
+	DeploymentAnnotationsID                    MetricCollectorID = "DeploymentAnnotations"
+	DeploymentMatchLabelsID                    MetricCollectorID = "DeploymentMatchLabels"
+	StatefulSetInfoID                          MetricCollectorID = "StatefulSetInfo"
+	StatefulSetUptimeID                        MetricCollectorID = "StatefulSetUptime"
 	StatefulSetLabelsID                        MetricCollectorID = "StatefulSetLabels"
 	StatefulSetLabelsID                        MetricCollectorID = "StatefulSetLabels"
+	StatefulSetAnnotationsID                   MetricCollectorID = "StatefulSetAnnotations"
+	StatefulSetMatchLabelsID                   MetricCollectorID = "StatefulSetMatchLabels"
+	DaemonSetInfoID                            MetricCollectorID = "DaemonSetInfo"
+	DaemonSetUptimeID                          MetricCollectorID = "DaemonSetUptime"
 	DaemonSetLabelsID                          MetricCollectorID = "DaemonSetLabels"
 	DaemonSetLabelsID                          MetricCollectorID = "DaemonSetLabels"
+	DaemonSetAnnotationsID                     MetricCollectorID = "DaemonSetAnnotations"
+	JobInfoID                                  MetricCollectorID = "JobInfo"
+	JobUptimeID                                MetricCollectorID = "JobUptime"
 	JobLabelsID                                MetricCollectorID = "JobLabels"
 	JobLabelsID                                MetricCollectorID = "JobLabels"
+	JobAnnotationsID                           MetricCollectorID = "JobAnnotations"
+	CronJobInfoID                              MetricCollectorID = "CronJobInfo"
+	CronJobUptimeID                            MetricCollectorID = "CronJobUptime"
+	CronJobLabelsID                            MetricCollectorID = "CronJobLabels"
+	CronJobAnnotationsID                       MetricCollectorID = "CronJobAnnotations"
+	ReplicaSetInfoID                           MetricCollectorID = "ReplicaSetInfo"
+	ReplicaSetUptimeID                         MetricCollectorID = "ReplicaSetUptime"
+	ReplicaSetLabelsID                         MetricCollectorID = "ReplicaSetLabels"
+	ReplicaSetAnnotationsID                    MetricCollectorID = "ReplicaSetAnnotations"
+	ReplicaSetOwnerID                          MetricCollectorID = "ReplicaSetOwner"
+	PodsWithDaemonSetOwnerID                   MetricCollectorID = "PodsWithDaemonSetOwner"
+	PodsWithJobOwnerID                         MetricCollectorID = "PodsWithJobOwner"
 	PodsWithReplicaSetOwnerID                  MetricCollectorID = "PodsWithReplicaSetOwner"
 	PodsWithReplicaSetOwnerID                  MetricCollectorID = "PodsWithReplicaSetOwner"
 	ReplicaSetsWithoutOwnersID                 MetricCollectorID = "ReplicaSetsWithoutOwners"
 	ReplicaSetsWithoutOwnersID                 MetricCollectorID = "ReplicaSetsWithoutOwners"
 	ReplicaSetsWithRolloutID                   MetricCollectorID = "ReplicaSetsWithRollout"
 	ReplicaSetsWithRolloutID                   MetricCollectorID = "ReplicaSetsWithRollout"
+	ContainerResourceRequestsID                MetricCollectorID = "ContainerResourceRequests"
+	ContainerResourceLimitsID                  MetricCollectorID = "ContainerResourceLimits"
+	ResourceQuotaInfoID                        MetricCollectorID = "ResourceQuotaInfo"
 	ResourceQuotaUptimeID                      MetricCollectorID = "ResourceQuotaUptime"
 	ResourceQuotaUptimeID                      MetricCollectorID = "ResourceQuotaUptime"
 	ResourceQuotaSpecCPURequestAverageID       MetricCollectorID = "ResourceQuotaSpecCPURequestAverage"
 	ResourceQuotaSpecCPURequestAverageID       MetricCollectorID = "ResourceQuotaSpecCPURequestAverage"
 	ResourceQuotaSpecCPURequestMaxID           MetricCollectorID = "ResourceQuotaSpecCPURequestMax"
 	ResourceQuotaSpecCPURequestMaxID           MetricCollectorID = "ResourceQuotaSpecCPURequestMax"

+ 25 - 1
modules/collector-source/pkg/metric/metrics.go

@@ -3,6 +3,11 @@ package metric
 const (
 const (
 	// Cluster Cache Metrics
 	// Cluster Cache Metrics
 	ClusterInfo                                           = "cluster_info"
 	ClusterInfo                                           = "cluster_info"
+	NodeInfo                                              = "node_info"
+	NodeResourceCapacities                                = "node_resource_capacities"
+	NodeResourcesAllocatable                              = "node_resources_allocatable"
+	PodInfo                                               = "pod_info"
+	PodPVCVolume                                          = "pod_pvc_volume"
 	KubeNodeStatusCapacityCPUCores                        = "kube_node_status_capacity_cpu_cores"
 	KubeNodeStatusCapacityCPUCores                        = "kube_node_status_capacity_cpu_cores"
 	KubeNodeStatusCapacityMemoryBytes                     = "kube_node_status_capacity_memory_bytes"
 	KubeNodeStatusCapacityMemoryBytes                     = "kube_node_status_capacity_memory_bytes"
 	KubeNodeStatusAllocatableCPUCores                     = "kube_node_status_allocatable_cpu_cores"
 	KubeNodeStatusAllocatableCPUCores                     = "kube_node_status_allocatable_cpu_cores"
@@ -18,12 +23,31 @@ const (
 	KubePersistentVolumeClaimResourceRequestsStorageBytes = "kube_persistentvolumeclaim_resource_requests_storage_bytes"
 	KubePersistentVolumeClaimResourceRequestsStorageBytes = "kube_persistentvolumeclaim_resource_requests_storage_bytes"
 	KubecostPVInfo                                        = "kubecost_pv_info"
 	KubecostPVInfo                                        = "kubecost_pv_info"
 	KubePersistentVolumeCapacityBytes                     = "kube_persistentvolume_capacity_bytes"
 	KubePersistentVolumeCapacityBytes                     = "kube_persistentvolume_capacity_bytes"
+	DeploymentInfo                                        = "deployment_info"
+	DeploymentLabels                                      = "deployment_labels"
+	DeploymentAnnotations                                 = "deployment_annotations"
 	DeploymentMatchLabels                                 = "deployment_match_labels"
 	DeploymentMatchLabels                                 = "deployment_match_labels"
+	StatefulSetInfo                                       = "statefulset_info"
+	StatefulSetLabels                                     = "statefulset_labels"
+	StatefulSetAnnotations                                = "statefulset_annotations"
+	StatefulSetMatchLabels                                = "statefulSet_match_labels"
+	DaemonSetInfo                                         = "daemonset_info"
+	DaemonSetLabels                                       = "daemonset_labels"
+	DaemonSetAnnotations                                  = "daemonset_annotations"
+	JobInfo                                               = "job_info"
+	JobLabels                                             = "job_labels"
+	JobAnnotations                                        = "job_annotations"
+	CronJobInfo                                           = "cronjob_info"
+	CronJobLabels                                         = "cronjob_labels"
+	CronJobAnnotations                                    = "cronjob_annotations"
+	ReplicaSetInfo                                        = "replicaset_info"
+	ReplicaSetLabels                                      = "replicaset_labels"
+	ReplicaSetAnnotations                                 = "replicaset_annotations"
 	NamespaceInfo                                         = "namespace_info"
 	NamespaceInfo                                         = "namespace_info"
 	KubeNamespaceLabels                                   = "kube_namespace_labels"
 	KubeNamespaceLabels                                   = "kube_namespace_labels"
 	KubeNamespaceAnnotations                              = "kube_namespace_annotations"
 	KubeNamespaceAnnotations                              = "kube_namespace_annotations"
+	ServiceInfo                                           = "service_info"
 	ServiceSelectorLabels                                 = "service_selector_labels"
 	ServiceSelectorLabels                                 = "service_selector_labels"
-	StatefulSetMatchLabels                                = "statefulSet_match_labels"
 	KubeReplicasetOwner                                   = "kube_replicaset_owner"
 	KubeReplicasetOwner                                   = "kube_replicaset_owner"
 	ContainerCPUAllocation                                = "container_cpu_allocation"
 	ContainerCPUAllocation                                = "container_cpu_allocation"
 	ContainerMemoryAllocationBytes                        = "container_memory_allocation_bytes"
 	ContainerMemoryAllocationBytes                        = "container_memory_allocation_bytes"

+ 0 - 6
modules/collector-source/pkg/metric/repository.go

@@ -126,12 +126,6 @@ func (r *resolutionStores) update(
 	defer r.lock.Unlock()
 	defer r.lock.Unlock()
 	limit := r.resolution.Limit()
 	limit := r.resolution.Limit()
 	if updateSet.Timestamp.Before(limit) {
 	if updateSet.Timestamp.Before(limit) {
-		log.Debugf(
-			"skipping update on resolution '%s' because Timestamp '%s' is before the limit '%s",
-			r.resolution.Interval(),
-			updateSet.Timestamp.Format(time.RFC3339),
-			limit.Format(time.RFC3339),
-		)
 		return
 		return
 	}
 	}
 
 

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 617 - 151
modules/collector-source/pkg/scrape/clustercache.go


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 715 - 89
modules/collector-source/pkg/scrape/clustercache_test.go


+ 48 - 0
modules/collector-source/pkg/scrape/index.go

@@ -0,0 +1,48 @@
+package scrape
+
+import (
+	"github.com/opencost/opencost/core/pkg/clustercache"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+// pvcKey is a composite key for a PersistentVolumeClaim (name + namespace).
+type pvcKey struct {
+	name      string
+	namespace string
+}
+
+// buildNodeIndex returns a map from node name to UID.
+func buildNodeIndex(nodes []*clustercache.Node) map[string]types.UID {
+	m := make(map[string]types.UID, len(nodes))
+	for _, node := range nodes {
+		m[node.Name] = node.UID
+	}
+	return m
+}
+
+// buildNamespaceIndex returns a map from namespace name to UID.
+func buildNamespaceIndex(namespaces []*clustercache.Namespace) map[string]types.UID {
+	m := make(map[string]types.UID, len(namespaces))
+	for _, ns := range namespaces {
+		m[ns.Name] = ns.UID
+	}
+	return m
+}
+
+// buildPVCIndex returns a map from (name, namespace) to PVC UID.
+func buildPVCIndex(pvcs []*clustercache.PersistentVolumeClaim) map[pvcKey]types.UID {
+	m := make(map[pvcKey]types.UID, len(pvcs))
+	for _, pvc := range pvcs {
+		m[pvcKey{name: pvc.Name, namespace: pvc.Namespace}] = pvc.UID
+	}
+	return m
+}
+
+// buildPVIndex returns a map from PV name to UID.
+func buildPVIndex(pvs []*clustercache.PersistentVolume) map[string]types.UID {
+	m := make(map[string]types.UID, len(pvs))
+	for _, pv := range pvs {
+		m[pv.Name] = pv.UID
+	}
+	return m
+}

+ 75 - 0
modules/collector-source/pkg/scrape/index_test.go

@@ -0,0 +1,75 @@
+package scrape
+
+import (
+	"testing"
+
+	"github.com/opencost/opencost/core/pkg/clustercache"
+	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+func TestBuildNodeIndex_Empty(t *testing.T) {
+	m := buildNodeIndex(nil)
+	require.Empty(t, m)
+}
+
+func TestBuildNodeIndex(t *testing.T) {
+	nodes := []*clustercache.Node{
+		{Name: "node-a", UID: "uid-a"},
+		{Name: "node-b", UID: "uid-b"},
+	}
+	m := buildNodeIndex(nodes)
+	require.Equal(t, types.UID("uid-a"), m["node-a"])
+	require.Equal(t, types.UID("uid-b"), m["node-b"])
+	require.Len(t, m, 2)
+}
+
+func TestBuildNamespaceIndex_Empty(t *testing.T) {
+	m := buildNamespaceIndex(nil)
+	require.Empty(t, m)
+}
+
+func TestBuildNamespaceIndex(t *testing.T) {
+	namespaces := []*clustercache.Namespace{
+		{Name: "default", UID: "uid-default"},
+		{Name: "kube-system", UID: "uid-kube-system"},
+	}
+	m := buildNamespaceIndex(namespaces)
+	require.Equal(t, types.UID("uid-default"), m["default"])
+	require.Equal(t, types.UID("uid-kube-system"), m["kube-system"])
+	require.Len(t, m, 2)
+}
+
+func TestBuildPVCIndex_Empty(t *testing.T) {
+	m := buildPVCIndex(nil)
+	require.Empty(t, m)
+}
+
+func TestBuildPVCIndex(t *testing.T) {
+	pvcs := []*clustercache.PersistentVolumeClaim{
+		{Name: "pvc-a", Namespace: "ns-1", UID: "uid-pvc-a"},
+		{Name: "pvc-a", Namespace: "ns-2", UID: "uid-pvc-a-ns2"}, // same name, different namespace
+		{Name: "pvc-b", Namespace: "ns-1", UID: "uid-pvc-b"},
+	}
+	m := buildPVCIndex(pvcs)
+	require.Equal(t, types.UID("uid-pvc-a"), m[pvcKey{name: "pvc-a", namespace: "ns-1"}])
+	require.Equal(t, types.UID("uid-pvc-a-ns2"), m[pvcKey{name: "pvc-a", namespace: "ns-2"}])
+	require.Equal(t, types.UID("uid-pvc-b"), m[pvcKey{name: "pvc-b", namespace: "ns-1"}])
+	require.Len(t, m, 3)
+}
+
+func TestBuildPVIndex_Empty(t *testing.T) {
+	m := buildPVIndex(nil)
+	require.Empty(t, m)
+}
+
+func TestBuildPVIndex(t *testing.T) {
+	pvs := []*clustercache.PersistentVolume{
+		{Name: "pv-a", UID: "uid-pv-a"},
+		{Name: "pv-b", UID: "uid-pv-b"},
+	}
+	m := buildPVIndex(pvs)
+	require.Equal(t, types.UID("uid-pv-a"), m["pv-a"])
+	require.Equal(t, types.UID("uid-pv-b"), m["pv-b"])
+	require.Len(t, m, 2)
+}

+ 1 - 1
modules/collector-source/pkg/scrape/scrapecontroller.go

@@ -41,7 +41,7 @@ func NewScrapeController(
 	opencostScraper := newOpenCostScraper()
 	opencostScraper := newOpenCostScraper()
 	scrapers = append(scrapers, opencostScraper)
 	scrapers = append(scrapers, opencostScraper)
 
 
-	statSummaryScraper := newStatSummaryScraper(statSummaryClient)
+	statSummaryScraper := newStatSummaryScraper(statSummaryClient, clusterCache)
 	scrapers = append(scrapers, statSummaryScraper)
 	scrapers = append(scrapers, statSummaryScraper)
 
 
 	networkScraper := newNetworkScraper(networkPort, clusterCache)
 	networkScraper := newNetworkScraper(networkPort, clusterCache)

+ 25 - 6
modules/collector-source/pkg/scrape/statsummary.go

@@ -2,6 +2,7 @@ package scrape
 
 
 import (
 import (
 	"github.com/kubecost/events"
 	"github.com/kubecost/events"
+	"github.com/opencost/opencost/core/pkg/clustercache"
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/nodestats"
 	"github.com/opencost/opencost/core/pkg/nodestats"
 	"github.com/opencost/opencost/core/pkg/source"
 	"github.com/opencost/opencost/core/pkg/source"
@@ -11,16 +12,22 @@ import (
 )
 )
 
 
 type StatSummaryScraper struct {
 type StatSummaryScraper struct {
-	client nodestats.StatSummaryClient
+	client       nodestats.StatSummaryClient
+	clusterCache clustercache.ClusterCache
 }
 }
 
 
-func newStatSummaryScraper(client nodestats.StatSummaryClient) Scraper {
+func newStatSummaryScraper(client nodestats.StatSummaryClient, clusterCache clustercache.ClusterCache) Scraper {
 	return &StatSummaryScraper{
 	return &StatSummaryScraper{
-		client: client,
+		client:       client,
+		clusterCache: clusterCache,
 	}
 	}
 }
 }
 
 
 func (s *StatSummaryScraper) Scrape() []metric.Update {
 func (s *StatSummaryScraper) Scrape() []metric.Update {
+
+	nodeNameToUID := buildNodeIndex(s.clusterCache.GetAllNodes())
+	pvcNameToUID := buildPVCIndex(s.clusterCache.GetAllPersistentVolumeClaims())
+
 	var scrapeResults []metric.Update
 	var scrapeResults []metric.Update
 	nodeStats, err := s.client.GetNodeData()
 	nodeStats, err := s.client.GetNodeData()
 
 
@@ -41,11 +48,14 @@ func (s *StatSummaryScraper) Scrape() []metric.Update {
 
 
 	for _, stat := range nodeStats {
 	for _, stat := range nodeStats {
 		nodeName := stat.Node.NodeName
 		nodeName := stat.Node.NodeName
+		nodeUID := string(nodeNameToUID[nodeName])
+
 		if stat.Node.CPU != nil && stat.Node.CPU.UsageCoreNanoSeconds != nil {
 		if stat.Node.CPU != nil && stat.Node.CPU.UsageCoreNanoSeconds != nil {
 			scrapeResults = append(scrapeResults, metric.Update{
 			scrapeResults = append(scrapeResults, metric.Update{
 				Name: metric.NodeCPUSecondsTotal,
 				Name: metric.NodeCPUSecondsTotal,
 				Labels: map[string]string{
 				Labels: map[string]string{
 					source.KubernetesNodeLabel: nodeName,
 					source.KubernetesNodeLabel: nodeName,
+					source.UIDLabel:            nodeUID,
 					source.ModeLabel:           "", // TODO
 					source.ModeLabel:           "", // TODO
 				},
 				},
 				Value: float64(*stat.Node.CPU.UsageCoreNanoSeconds) * 1e-9,
 				Value: float64(*stat.Node.CPU.UsageCoreNanoSeconds) * 1e-9,
@@ -57,6 +67,7 @@ func (s *StatSummaryScraper) Scrape() []metric.Update {
 				Name: metric.NodeFSCapacityBytes,
 				Name: metric.NodeFSCapacityBytes,
 				Labels: map[string]string{
 				Labels: map[string]string{
 					source.InstanceLabel: nodeName,
 					source.InstanceLabel: nodeName,
+					source.UIDLabel:      nodeUID,
 					source.DeviceLabel:   "local", // This value has to be populated but isn't important here
 					source.DeviceLabel:   "local", // This value has to be populated but isn't important here
 				},
 				},
 				Value: float64(*stat.Node.Fs.CapacityBytes),
 				Value: float64(*stat.Node.Fs.CapacityBytes),
@@ -71,6 +82,7 @@ func (s *StatSummaryScraper) Scrape() []metric.Update {
 			if pod.Network != nil {
 			if pod.Network != nil {
 				networkLabels := map[string]string{
 				networkLabels := map[string]string{
 					source.UIDLabel:       podUID,
 					source.UIDLabel:       podUID,
+					source.NodeUIDLabel:   nodeUID,
 					source.PodLabel:       podName,
 					source.PodLabel:       podName,
 					source.NamespaceLabel: namespace,
 					source.NamespaceLabel: namespace,
 				}
 				}
@@ -93,12 +105,15 @@ func (s *StatSummaryScraper) Scrape() []metric.Update {
 				if _, ok := seenPVC[*volumeStats.PVCRef]; ok {
 				if _, ok := seenPVC[*volumeStats.PVCRef]; ok {
 					continue
 					continue
 				}
 				}
+				pvcUID := string(pvcNameToUID[pvcKey{name: volumeStats.PVCRef.Name, namespace: volumeStats.PVCRef.Namespace}])
 				scrapeResults = append(scrapeResults, metric.Update{
 				scrapeResults = append(scrapeResults, metric.Update{
 					Name: metric.KubeletVolumeStatsUsedBytes,
 					Name: metric.KubeletVolumeStatsUsedBytes,
 					Labels: map[string]string{
 					Labels: map[string]string{
 						source.PVCLabel:       volumeStats.PVCRef.Name,
 						source.PVCLabel:       volumeStats.PVCRef.Name,
 						source.NamespaceLabel: volumeStats.PVCRef.Namespace,
 						source.NamespaceLabel: volumeStats.PVCRef.Namespace,
 						source.UIDLabel:       podUID,
 						source.UIDLabel:       podUID,
+						source.NodeUIDLabel:   nodeUID,
+						source.PVCUIDLabel:    pvcUID,
 					},
 					},
 					Value: float64(*volumeStats.UsedBytes),
 					Value: float64(*volumeStats.UsedBytes),
 				})
 				})
@@ -116,6 +131,7 @@ func (s *StatSummaryScraper) Scrape() []metric.Update {
 							source.NodeLabel:      nodeName,
 							source.NodeLabel:      nodeName,
 							source.InstanceLabel:  nodeName,
 							source.InstanceLabel:  nodeName,
 							source.UIDLabel:       podUID,
 							source.UIDLabel:       podUID,
+							source.NodeUIDLabel:   nodeUID,
 						},
 						},
 						Value: float64(*container.CPU.UsageCoreNanoSeconds) * 1e-9,
 						Value: float64(*container.CPU.UsageCoreNanoSeconds) * 1e-9,
 					})
 					})
@@ -130,6 +146,7 @@ func (s *StatSummaryScraper) Scrape() []metric.Update {
 							source.NodeLabel:      nodeName,
 							source.NodeLabel:      nodeName,
 							source.InstanceLabel:  nodeName,
 							source.InstanceLabel:  nodeName,
 							source.UIDLabel:       podUID,
 							source.UIDLabel:       podUID,
+							source.NodeUIDLabel:   nodeUID,
 						},
 						},
 						Value: float64(*container.Memory.WorkingSetBytes),
 						Value: float64(*container.Memory.WorkingSetBytes),
 					})
 					})
@@ -139,9 +156,11 @@ func (s *StatSummaryScraper) Scrape() []metric.Update {
 					scrapeResults = append(scrapeResults, metric.Update{
 					scrapeResults = append(scrapeResults, metric.Update{
 						Name: metric.ContainerFSUsageBytes,
 						Name: metric.ContainerFSUsageBytes,
 						Labels: map[string]string{
 						Labels: map[string]string{
-							source.InstanceLabel: nodeName,
-							source.DeviceLabel:   "local",
-							source.UIDLabel:      podUID,
+							source.InstanceLabel:  nodeName,
+							source.DeviceLabel:    "local",
+							source.UIDLabel:       podUID,
+							source.NodeUIDLabel:   nodeUID,
+							source.ContainerLabel: container.Name,
 						},
 						},
 						Value: float64(*container.Rootfs.UsedBytes),
 						Value: float64(*container.Rootfs.UsedBytes),
 					})
 					})

+ 53 - 10
modules/collector-source/pkg/scrape/statsummary_test.go

@@ -6,10 +6,12 @@ import (
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
+	"github.com/opencost/opencost/core/pkg/clustercache"
 	"github.com/opencost/opencost/core/pkg/source"
 	"github.com/opencost/opencost/core/pkg/source"
 	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
 	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
 	"github.com/opencost/opencost/modules/collector-source/pkg/util"
 	"github.com/opencost/opencost/modules/collector-source/pkg/util"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
 	stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
 	stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
 )
 )
 
 
@@ -24,10 +26,21 @@ func (m *mockStatSummaryClient) GetNodeData() ([]*stats.Summary, error) {
 
 
 func TestStatScraper_Scrape(t *testing.T) {
 func TestStatScraper_Scrape(t *testing.T) {
 	start1, _ := time.Parse(time.RFC3339, Start1Str)
 	start1, _ := time.Parse(time.RFC3339, Start1Str)
+
+	testCache := &clustercache.MockClusterCache{
+		Nodes: []*clustercache.Node{
+			{Name: "node1", UID: types.UID("node-uid-1")},
+		},
+		PersistentVolumeClaims: []*clustercache.PersistentVolumeClaim{
+			{Name: "pvc1", Namespace: "namespace1", UID: types.UID("pvc-uid-1")},
+		},
+	}
+
 	tests := map[string]struct {
 	tests := map[string]struct {
-		summaries []*stats.Summary
-		err       error
-		expected  []metric.Update
+		summaries    []*stats.Summary
+		err          error
+		clusterCache *clustercache.MockClusterCache
+		expected     []metric.Update
 	}{
 	}{
 		"nil values": {
 		"nil values": {
 			summaries: []*stats.Summary{
 			summaries: []*stats.Summary{
@@ -125,6 +138,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 			expected: []metric.Update{},
 			expected: []metric.Update{},
 		},
 		},
 		"single node": {
 		"single node": {
+			clusterCache: testCache,
 			summaries: []*stats.Summary{
 			summaries: []*stats.Summary{
 				{
 				{
 					Node: stats.NodeStats{
 					Node: stats.NodeStats{
@@ -198,6 +212,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 					Name: metric.NodeCPUSecondsTotal,
 					Name: metric.NodeCPUSecondsTotal,
 					Labels: map[string]string{
 					Labels: map[string]string{
 						source.KubernetesNodeLabel: "node1",
 						source.KubernetesNodeLabel: "node1",
+						source.UIDLabel:            "node-uid-1",
 						source.ModeLabel:           "",
 						source.ModeLabel:           "",
 					},
 					},
 					Value: 2,
 					Value: 2,
@@ -206,6 +221,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 					Name: metric.NodeFSCapacityBytes,
 					Name: metric.NodeFSCapacityBytes,
 					Labels: map[string]string{
 					Labels: map[string]string{
 						source.InstanceLabel: "node1",
 						source.InstanceLabel: "node1",
+						source.UIDLabel:      "node-uid-1",
 						source.DeviceLabel:   "local",
 						source.DeviceLabel:   "local",
 					},
 					},
 					Value: float64(2 * util.GB),
 					Value: float64(2 * util.GB),
@@ -214,6 +230,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 					Name: metric.ContainerNetworkReceiveBytesTotal,
 					Name: metric.ContainerNetworkReceiveBytesTotal,
 					Labels: map[string]string{
 					Labels: map[string]string{
 						source.UIDLabel:       "uid1",
 						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
 						source.PodLabel:       "pod1",
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
 						source.NamespaceLabel: "namespace1",
 					},
 					},
@@ -223,6 +240,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 					Name: metric.ContainerNetworkTransmitBytesTotal,
 					Name: metric.ContainerNetworkTransmitBytesTotal,
 					Labels: map[string]string{
 					Labels: map[string]string{
 						source.UIDLabel:       "uid1",
 						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
 						source.PodLabel:       "pod1",
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
 						source.NamespaceLabel: "namespace1",
 					},
 					},
@@ -234,6 +252,8 @@ func TestStatScraper_Scrape(t *testing.T) {
 						source.PVCLabel:       "pvc1",
 						source.PVCLabel:       "pvc1",
 						source.NamespaceLabel: "namespace1",
 						source.NamespaceLabel: "namespace1",
 						source.UIDLabel:       "uid1",
 						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
+						source.PVCUIDLabel:    "pvc-uid-1",
 					},
 					},
 					Value: float64(1 * util.GB),
 					Value: float64(1 * util.GB),
 				},
 				},
@@ -246,6 +266,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 						source.NodeLabel:      "node1",
 						source.NodeLabel:      "node1",
 						source.InstanceLabel:  "node1",
 						source.InstanceLabel:  "node1",
 						source.UIDLabel:       "uid1",
 						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
 					},
 					},
 					Value: 1,
 					Value: 1,
 				},
 				},
@@ -258,21 +279,25 @@ func TestStatScraper_Scrape(t *testing.T) {
 						source.NodeLabel:      "node1",
 						source.NodeLabel:      "node1",
 						source.InstanceLabel:  "node1",
 						source.InstanceLabel:  "node1",
 						source.UIDLabel:       "uid1",
 						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
 					},
 					},
 					Value: float64(5 * util.MB),
 					Value: float64(5 * util.MB),
 				},
 				},
 				{
 				{
 					Name: metric.ContainerFSUsageBytes,
 					Name: metric.ContainerFSUsageBytes,
 					Labels: map[string]string{
 					Labels: map[string]string{
-						source.InstanceLabel: "node1",
-						source.DeviceLabel:   "local",
-						source.UIDLabel:      "uid1",
+						source.InstanceLabel:  "node1",
+						source.DeviceLabel:    "local",
+						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
+						source.ContainerLabel: "container1",
 					},
 					},
 					Value: float64(1 * util.GB),
 					Value: float64(1 * util.GB),
 				},
 				},
 			},
 			},
 		},
 		},
 		"single node with error": {
 		"single node with error": {
+			clusterCache: testCache,
 			summaries: []*stats.Summary{
 			summaries: []*stats.Summary{
 				{
 				{
 					Node: stats.NodeStats{
 					Node: stats.NodeStats{
@@ -347,6 +372,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 					Name: metric.NodeCPUSecondsTotal,
 					Name: metric.NodeCPUSecondsTotal,
 					Labels: map[string]string{
 					Labels: map[string]string{
 						source.KubernetesNodeLabel: "node1",
 						source.KubernetesNodeLabel: "node1",
+						source.UIDLabel:            "node-uid-1",
 						source.ModeLabel:           "",
 						source.ModeLabel:           "",
 					},
 					},
 					Value: 2,
 					Value: 2,
@@ -355,6 +381,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 					Name: metric.NodeFSCapacityBytes,
 					Name: metric.NodeFSCapacityBytes,
 					Labels: map[string]string{
 					Labels: map[string]string{
 						source.InstanceLabel: "node1",
 						source.InstanceLabel: "node1",
+						source.UIDLabel:      "node-uid-1",
 						source.DeviceLabel:   "local",
 						source.DeviceLabel:   "local",
 					},
 					},
 					Value: float64(2 * util.GB),
 					Value: float64(2 * util.GB),
@@ -363,6 +390,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 					Name: metric.ContainerNetworkReceiveBytesTotal,
 					Name: metric.ContainerNetworkReceiveBytesTotal,
 					Labels: map[string]string{
 					Labels: map[string]string{
 						source.UIDLabel:       "uid1",
 						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
 						source.PodLabel:       "pod1",
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
 						source.NamespaceLabel: "namespace1",
 					},
 					},
@@ -372,6 +400,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 					Name: metric.ContainerNetworkTransmitBytesTotal,
 					Name: metric.ContainerNetworkTransmitBytesTotal,
 					Labels: map[string]string{
 					Labels: map[string]string{
 						source.UIDLabel:       "uid1",
 						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
 						source.PodLabel:       "pod1",
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
 						source.NamespaceLabel: "namespace1",
 					},
 					},
@@ -383,6 +412,8 @@ func TestStatScraper_Scrape(t *testing.T) {
 						source.PVCLabel:       "pvc1",
 						source.PVCLabel:       "pvc1",
 						source.NamespaceLabel: "namespace1",
 						source.NamespaceLabel: "namespace1",
 						source.UIDLabel:       "uid1",
 						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
+						source.PVCUIDLabel:    "pvc-uid-1",
 					},
 					},
 					Value: float64(1 * util.GB),
 					Value: float64(1 * util.GB),
 				},
 				},
@@ -395,6 +426,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 						source.NodeLabel:      "node1",
 						source.NodeLabel:      "node1",
 						source.InstanceLabel:  "node1",
 						source.InstanceLabel:  "node1",
 						source.UIDLabel:       "uid1",
 						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
 					},
 					},
 					Value: 1,
 					Value: 1,
 				},
 				},
@@ -407,21 +439,25 @@ func TestStatScraper_Scrape(t *testing.T) {
 						source.NodeLabel:      "node1",
 						source.NodeLabel:      "node1",
 						source.InstanceLabel:  "node1",
 						source.InstanceLabel:  "node1",
 						source.UIDLabel:       "uid1",
 						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
 					},
 					},
 					Value: float64(5 * util.MB),
 					Value: float64(5 * util.MB),
 				},
 				},
 				{
 				{
 					Name: metric.ContainerFSUsageBytes,
 					Name: metric.ContainerFSUsageBytes,
 					Labels: map[string]string{
 					Labels: map[string]string{
-						source.InstanceLabel: "node1",
-						source.DeviceLabel:   "local",
-						source.UIDLabel:      "uid1",
+						source.InstanceLabel:  "node1",
+						source.DeviceLabel:    "local",
+						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
+						source.ContainerLabel: "container1",
 					},
 					},
 					Value: float64(1 * util.GB),
 					Value: float64(1 * util.GB),
 				},
 				},
 			},
 			},
 		},
 		},
 		"repeat pvc": {
 		"repeat pvc": {
+			clusterCache: testCache,
 			summaries: []*stats.Summary{
 			summaries: []*stats.Summary{
 				{
 				{
 					Node: stats.NodeStats{
 					Node: stats.NodeStats{
@@ -478,6 +514,8 @@ func TestStatScraper_Scrape(t *testing.T) {
 						source.PVCLabel:       "pvc1",
 						source.PVCLabel:       "pvc1",
 						source.NamespaceLabel: "namespace1",
 						source.NamespaceLabel: "namespace1",
 						source.UIDLabel:       "uid1",
 						source.UIDLabel:       "uid1",
+						source.NodeUIDLabel:   "node-uid-1",
+						source.PVCUIDLabel:    "pvc-uid-1",
 					},
 					},
 					Value: float64(1 * util.GB),
 					Value: float64(1 * util.GB),
 				},
 				},
@@ -486,8 +524,13 @@ func TestStatScraper_Scrape(t *testing.T) {
 	}
 	}
 	for name, tt := range tests {
 	for name, tt := range tests {
 		t.Run(name, func(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
+			cache := clustercache.ClusterCache(tt.clusterCache)
+			if tt.clusterCache == nil {
+				cache = &clustercache.MockClusterCache{}
+			}
 			s := &StatSummaryScraper{
 			s := &StatSummaryScraper{
-				client: &mockStatSummaryClient{results: tt.summaries},
+				client:       &mockStatSummaryClient{results: tt.summaries, err: tt.err},
+				clusterCache: cache,
 			}
 			}
 			scrapeResults := s.Scrape()
 			scrapeResults := s.Scrape()
 
 

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1123 - 120
modules/prometheus-source/pkg/prom/metricsquerier.go


+ 7 - 3
modules/prometheus-source/pkg/prom/metricsquerier_test.go

@@ -103,6 +103,9 @@ func TestQueryLogs(t *testing.T) {
 		"QueryLocalStorageUsedAvg":                      func(s, e time.Time) { querier.QueryLocalStorageUsedAvg(s, e) },
 		"QueryLocalStorageUsedAvg":                      func(s, e time.Time) { querier.QueryLocalStorageUsedAvg(s, e) },
 		"QueryLocalStorageUsedMax":                      func(s, e time.Time) { querier.QueryLocalStorageUsedMax(s, e) },
 		"QueryLocalStorageUsedMax":                      func(s, e time.Time) { querier.QueryLocalStorageUsedMax(s, e) },
 		"QueryLocalStorageBytes":                        func(s, e time.Time) { querier.QueryLocalStorageBytes(s, e) },
 		"QueryLocalStorageBytes":                        func(s, e time.Time) { querier.QueryLocalStorageBytes(s, e) },
+		"QueryKMLocalStorageUsedAvg":                    func(s, e time.Time) { querier.QueryKMLocalStorageUsedAvg(s, e) },
+		"QueryKMLocalStorageUsedMax":                    func(s, e time.Time) { querier.QueryKMLocalStorageUsedMax(s, e) },
+		"QueryKMLocalStorageBytes":                      func(s, e time.Time) { querier.QueryKMLocalStorageBytes(s, e) },
 		"QueryNodeActiveMinutes":                        func(s, e time.Time) { querier.QueryNodeActiveMinutes(s, e) },
 		"QueryNodeActiveMinutes":                        func(s, e time.Time) { querier.QueryNodeActiveMinutes(s, e) },
 		"QueryNodeCPUCoresCapacity":                     func(s, e time.Time) { querier.QueryNodeCPUCoresCapacity(s, e) },
 		"QueryNodeCPUCoresCapacity":                     func(s, e time.Time) { querier.QueryNodeCPUCoresCapacity(s, e) },
 		"QueryNodeCPUCoresAllocatable":                  func(s, e time.Time) { querier.QueryNodeCPUCoresAllocatable(s, e) },
 		"QueryNodeCPUCoresAllocatable":                  func(s, e time.Time) { querier.QueryNodeCPUCoresAllocatable(s, e) },
@@ -141,6 +144,7 @@ func TestQueryLogs(t *testing.T) {
 		"QueryPodPVCAllocation":                         func(s, e time.Time) { querier.QueryPodPVCAllocation(s, e) },
 		"QueryPodPVCAllocation":                         func(s, e time.Time) { querier.QueryPodPVCAllocation(s, e) },
 		"QueryPVCBytesRequested":                        func(s, e time.Time) { querier.QueryPVCBytesRequested(s, e) },
 		"QueryPVCBytesRequested":                        func(s, e time.Time) { querier.QueryPVCBytesRequested(s, e) },
 		"QueryPVCInfo":                                  func(s, e time.Time) { querier.QueryPVCInfo(s, e) },
 		"QueryPVCInfo":                                  func(s, e time.Time) { querier.QueryPVCInfo(s, e) },
+		"QueryKMPVCInfo":                                func(s, e time.Time) { querier.QueryKMPVCInfo(s, e) },
 		"QueryPVBytes":                                  func(s, e time.Time) { querier.QueryPVBytes(s, e) },
 		"QueryPVBytes":                                  func(s, e time.Time) { querier.QueryPVBytes(s, e) },
 		"QueryPVPricePerGiBHour":                        func(s, e time.Time) { querier.QueryPVPricePerGiBHour(s, e) },
 		"QueryPVPricePerGiBHour":                        func(s, e time.Time) { querier.QueryPVPricePerGiBHour(s, e) },
 		"QueryPVInfo":                                   func(s, e time.Time) { querier.QueryPVInfo(s, e) },
 		"QueryPVInfo":                                   func(s, e time.Time) { querier.QueryPVInfo(s, e) },
@@ -162,9 +166,9 @@ func TestQueryLogs(t *testing.T) {
 		"QueryNodeLabels":                               func(s, e time.Time) { querier.QueryNodeLabels(s, e) },
 		"QueryNodeLabels":                               func(s, e time.Time) { querier.QueryNodeLabels(s, e) },
 		"QueryNamespaceLabels":                          func(s, e time.Time) { querier.QueryNamespaceLabels(s, e) },
 		"QueryNamespaceLabels":                          func(s, e time.Time) { querier.QueryNamespaceLabels(s, e) },
 		"QueryPodLabels":                                func(s, e time.Time) { querier.QueryPodLabels(s, e) },
 		"QueryPodLabels":                                func(s, e time.Time) { querier.QueryPodLabels(s, e) },
-		"QueryServiceLabels":                            func(s, e time.Time) { querier.QueryServiceLabels(s, e) },
-		"QueryDeploymentLabels":                         func(s, e time.Time) { querier.QueryDeploymentLabels(s, e) },
-		"QueryStatefulSetLabels":                        func(s, e time.Time) { querier.QueryStatefulSetLabels(s, e) },
+		"QueryServiceSelectorLabels":                    func(s, e time.Time) { querier.QueryServiceSelectorLabels(s, e) },
+		"QueryDeploymentMatchLabels":                    func(s, e time.Time) { querier.QueryDeploymentMatchLabels(s, e) },
+		"QueryStatefulSetMatchLabels":                   func(s, e time.Time) { querier.QueryStatefulSetMatchLabels(s, e) },
 		"QueryDaemonSetLabels":                          func(s, e time.Time) { querier.QueryDaemonSetLabels(s, e) },
 		"QueryDaemonSetLabels":                          func(s, e time.Time) { querier.QueryDaemonSetLabels(s, e) },
 		"QueryJobLabels":                                func(s, e time.Time) { querier.QueryJobLabels(s, e) },
 		"QueryJobLabels":                                func(s, e time.Time) { querier.QueryJobLabels(s, e) },
 		"QueryPodsWithReplicaSetOwner":                  func(s, e time.Time) { querier.QueryPodsWithReplicaSetOwner(s, e) },
 		"QueryPodsWithReplicaSetOwner":                  func(s, e time.Time) { querier.QueryPodsWithReplicaSetOwner(s, e) },

+ 4 - 23
pkg/cloud/provider/cloud_test.go

@@ -279,7 +279,7 @@ func TestNodePriceFromCSVWithGPULabels(t *testing.T) {
 
 
 	c.DownloadPricingData()
 	c.DownloadPricingData()
 
 
-	fc := NewFakeNodeCache([]*clustercache.Node{n})
+	fc := &clustercache.MockClusterCache{Nodes: []*clustercache.Node{n}}
 	fm := FakeClusterMap{}
 	fm := FakeClusterMap{}
 	d, _ := time.ParseDuration("1m")
 	d, _ := time.ParseDuration("1m")
 
 
@@ -347,7 +347,7 @@ func TestRKE2NodePriceFromCSVWithGPULabels(t *testing.T) {
 
 
 	c.DownloadPricingData()
 	c.DownloadPricingData()
 
 
-	fc := NewFakeNodeCache([]*clustercache.Node{n})
+	fc := &clustercache.MockClusterCache{Nodes: []*clustercache.Node{n}}
 	fm := FakeClusterMap{}
 	fm := FakeClusterMap{}
 	d, _ := time.ParseDuration("1m")
 	d, _ := time.ParseDuration("1m")
 
 
@@ -560,25 +560,6 @@ func TestNodePriceFromCSVWithRegion(t *testing.T) {
 	}
 	}
 }
 }
 
 
-type FakeCache struct {
-	nodes []*clustercache.Node
-	clustercache.ClusterCache
-}
-
-func (f FakeCache) GetAllNodes() []*clustercache.Node {
-	return f.nodes
-}
-
-func (f FakeCache) GetAllDaemonSets() []*clustercache.DaemonSet {
-	return nil
-}
-
-func NewFakeNodeCache(nodes []*clustercache.Node) FakeCache {
-	return FakeCache{
-		nodes: nodes,
-	}
-}
-
 type FakeClusterMap struct {
 type FakeClusterMap struct {
 	clusters.ClusterMap
 	clusters.ClusterMap
 }
 }
@@ -664,7 +645,7 @@ func TestNodePriceFromCSVWithBadConfig(t *testing.T) {
 	n.Labels["foo"] = "labelFooWant"
 	n.Labels["foo"] = "labelFooWant"
 	n.Labels[v1.LabelTopologyRegion] = "regionone"
 	n.Labels[v1.LabelTopologyRegion] = "regionone"
 
 
-	fc := NewFakeNodeCache([]*clustercache.Node{n})
+	fc := &clustercache.MockClusterCache{Nodes: []*clustercache.Node{n}}
 	fm := FakeClusterMap{}
 	fm := FakeClusterMap{}
 	d, _ := time.ParseDuration("1m")
 	d, _ := time.ParseDuration("1m")
 
 
@@ -721,7 +702,7 @@ func TestSourceMatchesFromCSV(t *testing.T) {
 	n3.Labels[v1.LabelTopologyRegion] = "eastus2"
 	n3.Labels[v1.LabelTopologyRegion] = "eastus2"
 	n3.Labels[v1.LabelInstanceTypeStable] = "Standard_F32s_v2"
 	n3.Labels[v1.LabelInstanceTypeStable] = "Standard_F32s_v2"
 
 
-	fc := NewFakeNodeCache([]*clustercache.Node{n, n2, n3})
+	fc := &clustercache.MockClusterCache{Nodes: []*clustercache.Node{n, n2, n3}}
 	fm := FakeClusterMap{}
 	fm := FakeClusterMap{}
 	d, _ := time.ParseDuration("1m")
 	d, _ := time.ParseDuration("1m")
 
 

+ 14 - 1
pkg/clustercache/clustercache.go

@@ -31,6 +31,7 @@ type KubernetesClusterCache struct {
 	pvcWatch                   WatchController
 	pvcWatch                   WatchController
 	storageClassWatch          WatchController
 	storageClassWatch          WatchController
 	jobsWatch                  WatchController
 	jobsWatch                  WatchController
+	cronjobsWatch              WatchController
 	pdbWatch                   WatchController
 	pdbWatch                   WatchController
 	replicationControllerWatch WatchController
 	replicationControllerWatch WatchController
 	resourceQuotasWatch        WatchController
 	resourceQuotasWatch        WatchController
@@ -73,6 +74,7 @@ func NewKubernetesClusterCacheV1(client kubernetes.Interface) cc.ClusterCache {
 		pvcWatch:                   NewCachingWatcher(coreRestClient, "persistentvolumeclaims", &v1.PersistentVolumeClaim{}, "", fields.Everything()),
 		pvcWatch:                   NewCachingWatcher(coreRestClient, "persistentvolumeclaims", &v1.PersistentVolumeClaim{}, "", fields.Everything()),
 		storageClassWatch:          NewCachingWatcher(storageRestClient, "storageclasses", &stv1.StorageClass{}, "", fields.Everything()),
 		storageClassWatch:          NewCachingWatcher(storageRestClient, "storageclasses", &stv1.StorageClass{}, "", fields.Everything()),
 		jobsWatch:                  NewCachingWatcher(batchClient, "jobs", &batchv1.Job{}, "", fields.Everything()),
 		jobsWatch:                  NewCachingWatcher(batchClient, "jobs", &batchv1.Job{}, "", fields.Everything()),
+		cronjobsWatch:              NewCachingWatcher(batchClient, "cronjobs", &batchv1.CronJob{}, "", fields.Everything()),
 		pdbWatch:                   NewCachingWatcher(pdbClient, "poddisruptionbudgets", &policyv1.PodDisruptionBudget{}, "", fields.Everything()),
 		pdbWatch:                   NewCachingWatcher(pdbClient, "poddisruptionbudgets", &policyv1.PodDisruptionBudget{}, "", fields.Everything()),
 		replicationControllerWatch: NewCachingWatcher(coreRestClient, "replicationcontrollers", &v1.ReplicationController{}, "", fields.Everything()),
 		replicationControllerWatch: NewCachingWatcher(coreRestClient, "replicationcontrollers", &v1.ReplicationController{}, "", fields.Everything()),
 		resourceQuotasWatch:        NewCachingWatcher(coreRestClient, "resourcequotas", &v1.ResourceQuota{}, "", fields.Everything()),
 		resourceQuotasWatch:        NewCachingWatcher(coreRestClient, "resourcequotas", &v1.ResourceQuota{}, "", fields.Everything()),
@@ -82,7 +84,7 @@ func NewKubernetesClusterCacheV1(client kubernetes.Interface) cc.ClusterCache {
 	cancel := make(chan struct{})
 	cancel := make(chan struct{})
 	var wg sync.WaitGroup
 	var wg sync.WaitGroup
 	if env.HasKubernetesResourceAccess() {
 	if env.HasKubernetesResourceAccess() {
-		wg.Add(15)
+		wg.Add(16)
 		go initializeCache(kcc.namespaceWatch, &wg, cancel)
 		go initializeCache(kcc.namespaceWatch, &wg, cancel)
 		go initializeCache(kcc.nodeWatch, &wg, cancel)
 		go initializeCache(kcc.nodeWatch, &wg, cancel)
 		go initializeCache(kcc.podWatch, &wg, cancel)
 		go initializeCache(kcc.podWatch, &wg, cancel)
@@ -95,6 +97,7 @@ func NewKubernetesClusterCacheV1(client kubernetes.Interface) cc.ClusterCache {
 		go initializeCache(kcc.pvcWatch, &wg, cancel)
 		go initializeCache(kcc.pvcWatch, &wg, cancel)
 		go initializeCache(kcc.storageClassWatch, &wg, cancel)
 		go initializeCache(kcc.storageClassWatch, &wg, cancel)
 		go initializeCache(kcc.jobsWatch, &wg, cancel)
 		go initializeCache(kcc.jobsWatch, &wg, cancel)
+		go initializeCache(kcc.cronjobsWatch, &wg, cancel)
 		go initializeCache(kcc.pdbWatch, &wg, cancel)
 		go initializeCache(kcc.pdbWatch, &wg, cancel)
 		go initializeCache(kcc.replicationControllerWatch, &wg, cancel)
 		go initializeCache(kcc.replicationControllerWatch, &wg, cancel)
 		go initializeCache(kcc.resourceQuotasWatch, &wg, cancel)
 		go initializeCache(kcc.resourceQuotasWatch, &wg, cancel)
@@ -125,6 +128,7 @@ func (kcc *KubernetesClusterCache) Run() {
 	go kcc.pvcWatch.Run(1, stopCh)
 	go kcc.pvcWatch.Run(1, stopCh)
 	go kcc.storageClassWatch.Run(1, stopCh)
 	go kcc.storageClassWatch.Run(1, stopCh)
 	go kcc.jobsWatch.Run(1, stopCh)
 	go kcc.jobsWatch.Run(1, stopCh)
+	go kcc.cronjobsWatch.Run(1, stopCh)
 	go kcc.pdbWatch.Run(1, stopCh)
 	go kcc.pdbWatch.Run(1, stopCh)
 	go kcc.replicationControllerWatch.Run(1, stopCh)
 	go kcc.replicationControllerWatch.Run(1, stopCh)
 	go kcc.resourceQuotasWatch.Run(1, stopCh)
 	go kcc.resourceQuotasWatch.Run(1, stopCh)
@@ -249,6 +253,15 @@ func (kcc *KubernetesClusterCache) GetAllJobs() []*cc.Job {
 	return jobs
 	return jobs
 }
 }
 
 
+func (kcc *KubernetesClusterCache) GetAllCronJobs() []*cc.CronJob {
+	var cronjobs []*cc.CronJob
+	items := kcc.cronjobsWatch.GetAll()
+	for _, cronjob := range items {
+		cronjobs = append(cronjobs, cc.TransformCronJob(cronjob.(*batchv1.CronJob)))
+	}
+	return cronjobs
+}
+
 func (kcc *KubernetesClusterCache) GetAllPodDisruptionBudgets() []*cc.PodDisruptionBudget {
 func (kcc *KubernetesClusterCache) GetAllPodDisruptionBudgets() []*cc.PodDisruptionBudget {
 	var pdbs []*cc.PodDisruptionBudget
 	var pdbs []*cc.PodDisruptionBudget
 	items := kcc.pdbWatch.GetAll()
 	items := kcc.pdbWatch.GetAll()

+ 8 - 1
pkg/clustercache/clustercache2.go

@@ -25,6 +25,7 @@ type KubernetesClusterCacheV2 struct {
 	persistentVolumeClaimStore *GenericStore[*v1.PersistentVolumeClaim, *cc.PersistentVolumeClaim]
 	persistentVolumeClaimStore *GenericStore[*v1.PersistentVolumeClaim, *cc.PersistentVolumeClaim]
 	storageClassStore          *GenericStore[*stv1.StorageClass, *cc.StorageClass]
 	storageClassStore          *GenericStore[*stv1.StorageClass, *cc.StorageClass]
 	jobStore                   *GenericStore[*batchv1.Job, *cc.Job]
 	jobStore                   *GenericStore[*batchv1.Job, *cc.Job]
+	cronJobStore               *GenericStore[*batchv1.CronJob, *cc.CronJob]
 	replicationControllerStore *GenericStore[*v1.ReplicationController, *cc.ReplicationController]
 	replicationControllerStore *GenericStore[*v1.ReplicationController, *cc.ReplicationController]
 	replicaSetStore            *GenericStore[*appsv1.ReplicaSet, *cc.ReplicaSet]
 	replicaSetStore            *GenericStore[*appsv1.ReplicaSet, *cc.ReplicaSet]
 	pdbStore                   *GenericStore[*policyv1.PodDisruptionBudget, *cc.PodDisruptionBudget]
 	pdbStore                   *GenericStore[*policyv1.PodDisruptionBudget, *cc.PodDisruptionBudget]
@@ -47,6 +48,7 @@ func NewKubernetesClusterCacheV2(clientset kubernetes.Interface) *KubernetesClus
 		statefulSetStore:           CreateStore(clientset.AppsV1().RESTClient(), "statefulsets", cc.TransformStatefulSet),
 		statefulSetStore:           CreateStore(clientset.AppsV1().RESTClient(), "statefulsets", cc.TransformStatefulSet),
 		storageClassStore:          CreateStore(clientset.StorageV1().RESTClient(), "storageclasses", cc.TransformStorageClass),
 		storageClassStore:          CreateStore(clientset.StorageV1().RESTClient(), "storageclasses", cc.TransformStorageClass),
 		jobStore:                   CreateStore(clientset.BatchV1().RESTClient(), "jobs", cc.TransformJob),
 		jobStore:                   CreateStore(clientset.BatchV1().RESTClient(), "jobs", cc.TransformJob),
+		cronJobStore:               CreateStore(clientset.BatchV1().RESTClient(), "cronjobs", cc.TransformCronJob),
 		pdbStore:                   CreateStore(clientset.PolicyV1().RESTClient(), "poddisruptionbudgets", cc.TransformPodDisruptionBudget),
 		pdbStore:                   CreateStore(clientset.PolicyV1().RESTClient(), "poddisruptionbudgets", cc.TransformPodDisruptionBudget),
 		resourceQuotasStore:        CreateStore(clientset.CoreV1().RESTClient(), "resourcequotas", cc.TransformResourceQuota),
 		resourceQuotasStore:        CreateStore(clientset.CoreV1().RESTClient(), "resourcequotas", cc.TransformResourceQuota),
 		stopCh:                     make(chan struct{}),
 		stopCh:                     make(chan struct{}),
@@ -57,7 +59,7 @@ func (kcc *KubernetesClusterCacheV2) Run() {
 	var wg sync.WaitGroup
 	var wg sync.WaitGroup
 
 
 	if env.HasKubernetesResourceAccess() {
 	if env.HasKubernetesResourceAccess() {
-		wg.Add(15)
+		wg.Add(16)
 		kcc.namespaceStore.Watch(kcc.stopCh, wg.Done)
 		kcc.namespaceStore.Watch(kcc.stopCh, wg.Done)
 		kcc.nodeStore.Watch(kcc.stopCh, wg.Done)
 		kcc.nodeStore.Watch(kcc.stopCh, wg.Done)
 		kcc.persistentVolumeClaimStore.Watch(kcc.stopCh, wg.Done)
 		kcc.persistentVolumeClaimStore.Watch(kcc.stopCh, wg.Done)
@@ -71,6 +73,7 @@ func (kcc *KubernetesClusterCacheV2) Run() {
 		kcc.statefulSetStore.Watch(kcc.stopCh, wg.Done)
 		kcc.statefulSetStore.Watch(kcc.stopCh, wg.Done)
 		kcc.storageClassStore.Watch(kcc.stopCh, wg.Done)
 		kcc.storageClassStore.Watch(kcc.stopCh, wg.Done)
 		kcc.jobStore.Watch(kcc.stopCh, wg.Done)
 		kcc.jobStore.Watch(kcc.stopCh, wg.Done)
+		kcc.cronJobStore.Watch(kcc.stopCh, wg.Done)
 		kcc.pdbStore.Watch(kcc.stopCh, wg.Done)
 		kcc.pdbStore.Watch(kcc.stopCh, wg.Done)
 		kcc.resourceQuotasStore.Watch(kcc.stopCh, wg.Done)
 		kcc.resourceQuotasStore.Watch(kcc.stopCh, wg.Done)
 	}
 	}
@@ -129,6 +132,10 @@ func (kcc *KubernetesClusterCacheV2) GetAllJobs() []*cc.Job {
 	return kcc.jobStore.GetAll()
 	return kcc.jobStore.GetAll()
 }
 }
 
 
+func (kcc *KubernetesClusterCacheV2) GetAllCronJobs() []*cc.CronJob {
+	return kcc.cronJobStore.GetAll()
+}
+
 func (kcc *KubernetesClusterCacheV2) GetAllReplicationControllers() []*cc.ReplicationController {
 func (kcc *KubernetesClusterCacheV2) GetAllReplicationControllers() []*cc.ReplicationController {
 	return kcc.replicationControllerStore.GetAll()
 	return kcc.replicationControllerStore.GetAll()
 }
 }

+ 5 - 0
pkg/cmd/costmodel/costmodel.go

@@ -58,6 +58,7 @@ func Execute(conf *Config) error {
 		if conf.CarbonEstimatesEnabled {
 		if conf.CarbonEstimatesEnabled {
 			router.GET("/assets/carbon", a.ComputeAssetsCarbonHandler)
 			router.GET("/assets/carbon", a.ComputeAssetsCarbonHandler)
 		}
 		}
+		router.GET("/kubemodel", a.KubeModelHandler)
 
 
 	}
 	}
 
 
@@ -124,6 +125,10 @@ func Execute(conf *Config) error {
 	case <-ctx.Done():
 	case <-ctx.Done():
 		log.Infof("Shutdown signal received, starting graceful shutdown...")
 		log.Infof("Shutdown signal received, starting graceful shutdown...")
 
 
+		if a.KubeModelPipeline != nil {
+			a.KubeModelPipeline.Stop()
+		}
+
 		if customCostPipelineService != nil {
 		if customCostPipelineService != nil {
 			customCostPipelineService.Stop()
 			customCostPipelineService.Stop()
 		}
 		}

+ 16 - 16
pkg/costmodel/allocation.go

@@ -341,16 +341,16 @@ func (cm *CostModel) computeAllocation(start, end time.Time) (*opencost.Allocati
 	resChPodLabels := source.WithGroup(grp, ds.QueryPodLabels(start, end))
 	resChPodLabels := source.WithGroup(grp, ds.QueryPodLabels(start, end))
 	resChPodAnnotations := source.WithGroup(grp, ds.QueryPodAnnotations(start, end))
 	resChPodAnnotations := source.WithGroup(grp, ds.QueryPodAnnotations(start, end))
 
 
-	resChServiceLabels := source.WithGroup(grp, ds.QueryServiceLabels(start, end))
-	resChDeploymentLabels := source.WithGroup(grp, ds.QueryDeploymentLabels(start, end))
-	resChStatefulSetLabels := source.WithGroup(grp, ds.QueryStatefulSetLabels(start, end))
-	resChDaemonSetLabels := source.WithGroup(grp, ds.QueryDaemonSetLabels(start, end))
+	resChServiceSelectorLabels := source.WithGroup(grp, ds.QueryServiceSelectorLabels(start, end))
+	resChDeploymentMatchLabels := source.WithGroup(grp, ds.QueryDeploymentMatchLabels(start, end))
+	resChStatefulSetMatchLabels := source.WithGroup(grp, ds.QueryStatefulSetMatchLabels(start, end))
+	resChPodsWithDaemonSetOwner := source.WithGroup(grp, ds.QueryPodsWithDaemonSetOwner(start, end))
 
 
 	resChPodsWithReplicaSetOwner := source.WithGroup(grp, ds.QueryPodsWithReplicaSetOwner(start, end))
 	resChPodsWithReplicaSetOwner := source.WithGroup(grp, ds.QueryPodsWithReplicaSetOwner(start, end))
 	resChReplicaSetsWithoutOwners := source.WithGroup(grp, ds.QueryReplicaSetsWithoutOwners(start, end))
 	resChReplicaSetsWithoutOwners := source.WithGroup(grp, ds.QueryReplicaSetsWithoutOwners(start, end))
 	resChReplicaSetsWithRolloutOwner := source.WithGroup(grp, ds.QueryReplicaSetsWithRollout(start, end))
 	resChReplicaSetsWithRolloutOwner := source.WithGroup(grp, ds.QueryReplicaSetsWithRollout(start, end))
 
 
-	resChJobLabels := source.WithGroup(grp, ds.QueryJobLabels(start, end))
+	resChPodsWithJobOwner := source.WithGroup(grp, ds.QueryPodsWithJobOwner(start, end))
 
 
 	resChLBCostPerHr := source.WithGroup(grp, ds.QueryLBPricePerHr(start, end))
 	resChLBCostPerHr := source.WithGroup(grp, ds.QueryLBPricePerHr(start, end))
 	resChLBActiveMins := source.WithGroup(grp, ds.QueryLBActiveMinutes(start, end))
 	resChLBActiveMins := source.WithGroup(grp, ds.QueryLBActiveMinutes(start, end))
@@ -408,14 +408,14 @@ func (cm *CostModel) computeAllocation(start, end time.Time) (*opencost.Allocati
 	resNamespaceAnnotations, _ := resChNamespaceAnnotations.Await()
 	resNamespaceAnnotations, _ := resChNamespaceAnnotations.Await()
 	resPodLabels, _ := resChPodLabels.Await()
 	resPodLabels, _ := resChPodLabels.Await()
 	resPodAnnotations, _ := resChPodAnnotations.Await()
 	resPodAnnotations, _ := resChPodAnnotations.Await()
-	resServiceLabels, _ := resChServiceLabels.Await()
-	resDeploymentLabels, _ := resChDeploymentLabels.Await()
-	resStatefulSetLabels, _ := resChStatefulSetLabels.Await()
-	resDaemonSetLabels, _ := resChDaemonSetLabels.Await()
+	resServiceSelectorLabels, _ := resChServiceSelectorLabels.Await()
+	resDeploymentMatchLabels, _ := resChDeploymentMatchLabels.Await()
+	resStatefulSetMatchLabels, _ := resChStatefulSetMatchLabels.Await()
+	resPodsWithDaemonSetOwner, _ := resChPodsWithDaemonSetOwner.Await()
 	resPodsWithReplicaSetOwner, _ := resChPodsWithReplicaSetOwner.Await()
 	resPodsWithReplicaSetOwner, _ := resChPodsWithReplicaSetOwner.Await()
 	resReplicaSetsWithoutOwners, _ := resChReplicaSetsWithoutOwners.Await()
 	resReplicaSetsWithoutOwners, _ := resChReplicaSetsWithoutOwners.Await()
 	resReplicaSetsWithRolloutOwner, _ := resChReplicaSetsWithRolloutOwner.Await()
 	resReplicaSetsWithRolloutOwner, _ := resChReplicaSetsWithRolloutOwner.Await()
-	resJobLabels, _ := resChJobLabels.Await()
+	resPodsWithJobOwner, _ := resChPodsWithJobOwner.Await()
 	resLBCostPerHr, _ := resChLBCostPerHr.Await()
 	resLBCostPerHr, _ := resChLBCostPerHr.Await()
 	resLBActiveMins, _ := resChLBActiveMins.Await()
 	resLBActiveMins, _ := resChLBActiveMins.Await()
 
 
@@ -474,10 +474,10 @@ func (cm *CostModel) computeAllocation(start, end time.Time) (*opencost.Allocati
 	applyLabels(podMap, nodeLabels, namespaceLabels, podLabels)
 	applyLabels(podMap, nodeLabels, namespaceLabels, podLabels)
 	applyAnnotations(podMap, namespaceAnnotations, podAnnotations)
 	applyAnnotations(podMap, namespaceAnnotations, podAnnotations)
 
 
-	podDeploymentMap := labelsToPodControllerMap(podLabels, resToDeploymentLabels(resDeploymentLabels))
-	podStatefulSetMap := labelsToPodControllerMap(podLabels, resToStatefulSetLabels(resStatefulSetLabels))
-	podDaemonSetMap := resToPodDaemonSetMap(resDaemonSetLabels, podUIDKeyMap, ingestPodUID)
-	podJobMap := resToPodJobMap(resJobLabels, podUIDKeyMap, ingestPodUID)
+	podDeploymentMap := labelsToPodControllerMap(podLabels, resToDeploymentLabels(resDeploymentMatchLabels))
+	podStatefulSetMap := labelsToPodControllerMap(podLabels, resToStatefulSetLabels(resStatefulSetMatchLabels))
+	podDaemonSetMap := resToPodDaemonSetMap(resPodsWithDaemonSetOwner, podUIDKeyMap, ingestPodUID)
+	podJobMap := resToPodJobMap(resPodsWithJobOwner, podUIDKeyMap, ingestPodUID)
 	podReplicaSetMap := resToPodReplicaSetMap(resPodsWithReplicaSetOwner, resReplicaSetsWithoutOwners, resReplicaSetsWithRolloutOwner, podUIDKeyMap, ingestPodUID)
 	podReplicaSetMap := resToPodReplicaSetMap(resPodsWithReplicaSetOwner, resReplicaSetsWithoutOwners, resReplicaSetsWithRolloutOwner, podUIDKeyMap, ingestPodUID)
 	applyControllersToPods(podMap, podDeploymentMap)
 	applyControllersToPods(podMap, podDeploymentMap)
 	applyControllersToPods(podMap, podStatefulSetMap)
 	applyControllersToPods(podMap, podStatefulSetMap)
@@ -485,9 +485,9 @@ func (cm *CostModel) computeAllocation(start, end time.Time) (*opencost.Allocati
 	applyControllersToPods(podMap, podJobMap)
 	applyControllersToPods(podMap, podJobMap)
 	applyControllersToPods(podMap, podReplicaSetMap)
 	applyControllersToPods(podMap, podReplicaSetMap)
 
 
-	serviceLabels := getServiceLabels(resServiceLabels)
+	serviceSelectorLabels := getServiceSelectorLabels(resServiceSelectorLabels)
 	allocsByService := map[serviceKey][]*opencost.Allocation{}
 	allocsByService := map[serviceKey][]*opencost.Allocation{}
-	applyServicesToPods(podMap, podLabels, allocsByService, serviceLabels)
+	applyServicesToPods(podMap, podLabels, allocsByService, serviceSelectorLabels)
 
 
 	// TODO breakdown network costs?
 	// TODO breakdown network costs?
 
 

+ 5 - 5
pkg/costmodel/allocation_helpers.go

@@ -1383,7 +1383,7 @@ func labelsToPodControllerMap(podLabels map[podKey]map[string]string, controller
 	return podControllerMap
 	return podControllerMap
 }
 }
 
 
-func resToPodDaemonSetMap(resDaemonSetLabels []*source.DaemonSetLabelsResult, podUIDKeyMap map[podKey][]podKey, ingestPodUID bool) map[podKey]controllerKey {
+func resToPodDaemonSetMap(resDaemonSetLabels []*source.PodsWithDaemonSetOwnerResult, podUIDKeyMap map[podKey][]podKey, ingestPodUID bool) map[podKey]controllerKey {
 	daemonSetLabels := map[podKey]controllerKey{}
 	daemonSetLabels := map[podKey]controllerKey{}
 
 
 	for _, res := range resDaemonSetLabels {
 	for _, res := range resDaemonSetLabels {
@@ -1419,7 +1419,7 @@ func resToPodDaemonSetMap(resDaemonSetLabels []*source.DaemonSetLabelsResult, po
 	return daemonSetLabels
 	return daemonSetLabels
 }
 }
 
 
-func resToPodJobMap(resJobLabels []*source.JobLabelsResult, podUIDKeyMap map[podKey][]podKey, ingestPodUID bool) map[podKey]controllerKey {
+func resToPodJobMap(resJobLabels []*source.PodsWithJobOwnerResult, podUIDKeyMap map[podKey][]podKey, ingestPodUID bool) map[podKey]controllerKey {
 	jobLabels := map[podKey]controllerKey{}
 	jobLabels := map[podKey]controllerKey{}
 
 
 	for _, res := range resJobLabels {
 	for _, res := range resJobLabels {
@@ -1549,10 +1549,10 @@ func applyControllersToPods(podMap map[podKey]*pod, podControllerMap map[podKey]
 
 
 /* Service Helpers */
 /* Service Helpers */
 
 
-func getServiceLabels(resServiceLabels []*source.ServiceLabelsResult) map[serviceKey]map[string]string {
+func getServiceSelectorLabels(resServiceSelectorLabels []*source.ServiceLabelsResult) map[serviceKey]map[string]string {
 	serviceLabels := map[serviceKey]map[string]string{}
 	serviceLabels := map[serviceKey]map[string]string{}
 
 
-	for _, res := range resServiceLabels {
+	for _, res := range resServiceSelectorLabels {
 		serviceKey, err := newResultServiceKey(res.Cluster, res.Namespace, res.Service)
 		serviceKey, err := newResultServiceKey(res.Cluster, res.Namespace, res.Service)
 		if err != nil {
 		if err != nil {
 			continue
 			continue
@@ -2154,7 +2154,7 @@ func applyPVBytes(pvMap map[pvKey]*pv, resPVBytes []*source.PVBytesResult) {
 			continue
 			continue
 		}
 		}
 
 
-		pvBytesUsed := res.Data[0].Value
+		pvBytesUsed := res.Value
 		if pvBytesUsed < PV_USAGE_SANITY_LIMIT_BYTES {
 		if pvBytesUsed < PV_USAGE_SANITY_LIMIT_BYTES {
 			pvMap[key].Bytes = pvBytesUsed
 			pvMap[key].Bytes = pvBytesUsed
 		} else {
 		} else {

+ 1 - 1
pkg/costmodel/cluster.go

@@ -764,7 +764,7 @@ func pvCosts(
 
 
 		// TODO niko/assets storage class
 		// TODO niko/assets storage class
 
 
-		bytes := result.Data[0].Value
+		bytes := result.Value
 		key := DiskIdentifier{cluster, name}
 		key := DiskIdentifier{cluster, name}
 		if _, ok := diskMap[key]; !ok {
 		if _, ok := diskMap[key]; !ok {
 			diskMap[key] = &Disk{
 			diskMap[key] = &Disk{

+ 3 - 7
pkg/costmodel/cluster_helpers.go

@@ -292,7 +292,7 @@ func buildGPUCountMap(resNodeGPUCount []*source.NodeGPUCountResult) map[NodeIden
 			continue
 			continue
 		}
 		}
 
 
-		gpuCount := result.Data[0].Value
+		gpuCount := result.GPUCount
 		providerID := result.ProviderID
 		providerID := result.ProviderID
 
 
 		key := NodeIdentifier{
 		key := NodeIdentifier{
@@ -321,13 +321,11 @@ func buildCPUCoresMap(resNodeCPUCores []*source.NodeCPUCoresCapacityResult) map[
 			continue
 			continue
 		}
 		}
 
 
-		cpuCores := result.Data[0].Value
-
 		key := nodeIdentifierNoProviderID{
 		key := nodeIdentifierNoProviderID{
 			Cluster: cluster,
 			Cluster: cluster,
 			Name:    name,
 			Name:    name,
 		}
 		}
-		m[key] = cpuCores
+		m[key] = result.CPUCores
 	}
 	}
 
 
 	return m
 	return m
@@ -348,13 +346,11 @@ func buildRAMBytesMap(resNodeRAMBytes []*source.NodeRAMBytesCapacityResult) map[
 			continue
 			continue
 		}
 		}
 
 
-		ramBytes := result.Data[0].Value
-
 		key := nodeIdentifierNoProviderID{
 		key := nodeIdentifierNoProviderID{
 			Cluster: cluster,
 			Cluster: cluster,
 			Name:    name,
 			Name:    name,
 		}
 		}
-		m[key] = ramBytes
+		m[key] = result.RAMBytes
 	}
 	}
 
 
 	return m
 	return m

+ 1 - 9
pkg/costmodel/costmodel.go

@@ -1377,15 +1377,7 @@ func (cm *CostModel) GetLBCost() (map[serviceKey]*costAnalyzerCloud.LoadBalancer
 				return nil, err
 				return nil, err
 			}
 			}
 			newLoadBalancer := *loadBalancer
 			newLoadBalancer := *loadBalancer
-			for _, loadBalancerIngress := range service.Status.LoadBalancer.Ingress {
-				address := loadBalancerIngress.IP
-				// Some cloud providers use hostname rather than IP
-				if address == "" {
-					address = loadBalancerIngress.Hostname
-				}
-				newLoadBalancer.IngressIPAddresses = append(newLoadBalancer.IngressIPAddresses, address)
-
-			}
+			newLoadBalancer.IngressIPAddresses = clustercache.GetLoadBalancerIngressAddress(service)
 			loadBalancerMap[key] = &newLoadBalancer
 			loadBalancerMap[key] = &newLoadBalancer
 		}
 		}
 	}
 	}

+ 16 - 30
pkg/costmodel/costmodel_test.go

@@ -471,7 +471,7 @@ func TestNodeCostAnnotations(t *testing.T) {
 
 
 	costModel := &CostModel{
 	costModel := &CostModel{
 		Provider: customProvider,
 		Provider: customProvider,
-		Cache: NewFakeNodeCache([]*clustercache.Node{
+		Cache: &clustercache.MockClusterCache{Nodes: []*clustercache.Node{
 			{
 			{
 				Name: "test-node-001",
 				Name: "test-node-001",
 				Labels: map[string]string{
 				Labels: map[string]string{
@@ -488,7 +488,7 @@ func TestNodeCostAnnotations(t *testing.T) {
 					"opencost.io/node-ram-hourly-cost": "222",
 					"opencost.io/node-ram-hourly-cost": "222",
 				},
 				},
 			},
 			},
-		}),
+		}},
 	}
 	}
 	assert.NotNil(t, costModel)
 	assert.NotNil(t, costModel)
 
 
@@ -548,21 +548,23 @@ func TestCustomProviderGPUNodeUsesDefaultHourlyPricing(t *testing.T) {
 
 
 	costModel := &CostModel{
 	costModel := &CostModel{
 		Provider: customProvider,
 		Provider: customProvider,
-		Cache: NewFakeNodeCache([]*clustercache.Node{
-			{
-				Name: "on-prem-gpu-node",
-				Labels: map[string]string{
-					"kubernetes.io/arch": "amd64",
-				},
-				Status: v1.NodeStatus{
-					Capacity: v1.ResourceList{
-						v1.ResourceCPU:    resource.MustParse("16"),
-						v1.ResourceMemory: resource.MustParse("128Gi"),
-						"nvidia.com/gpu":  resource.MustParse("2"),
+		Cache: &clustercache.MockClusterCache{
+			Nodes: []*clustercache.Node{
+				{
+					Name: "on-prem-gpu-node",
+					Labels: map[string]string{
+						"kubernetes.io/arch": "amd64",
+					},
+					Status: v1.NodeStatus{
+						Capacity: v1.ResourceList{
+							v1.ResourceCPU:    resource.MustParse("16"),
+							v1.ResourceMemory: resource.MustParse("128Gi"),
+							"nvidia.com/gpu":  resource.MustParse("2"),
+						},
 					},
 					},
 				},
 				},
 			},
 			},
-		}),
+		},
 	}
 	}
 
 
 	nodeCost, err := costModel.GetNodeCost()
 	nodeCost, err := costModel.GetNodeCost()
@@ -577,19 +579,3 @@ func TestCustomProviderGPUNodeUsesDefaultHourlyPricing(t *testing.T) {
 	assert.Equal(t, "2.000000", node.GPU)
 	assert.Equal(t, "2.000000", node.GPU)
 	assert.Empty(t, node.ProviderID)
 	assert.Empty(t, node.ProviderID)
 }
 }
-
-// FakeNodeCache implements ClusterCache interface for testing
-type FakeNodeCache struct {
-	clustercache.ClusterCache
-	nodes []*clustercache.Node
-}
-
-func (f FakeNodeCache) GetAllNodes() []*clustercache.Node {
-	return f.nodes
-}
-
-func NewFakeNodeCache(nodes []*clustercache.Node) FakeNodeCache {
-	return FakeNodeCache{
-		nodes: nodes,
-	}
-}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio