Selaa lähdekoodia

feat: Implement OpenCost Data Model v2 protobuf architecture (#3369)

Signed-off-by: Sparsh <sparsh.raj30@gmail.com>
Signed-off-by: Sparsh Raj <49100336+spa-raj@users.noreply.github.com>
Sparsh Raj 7 kuukautta sitten
vanhempi
sitoutus
c73cedb9e1

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

@@ -265,7 +265,7 @@ func TestProtobufDecoder(t *testing.T) {
 
 	testProtoBufDecoder(t, ProtobufDecoder, customCostTests)
 
-	labelsResponse := model.GenerateMockLabelResponse(start, "1d")
+	labelsResponse := model.GenerateMockLabelResponse(start, pb.Resolution_RESOLUTION_1D)
 	labelsResponseRaw, err := proto.Marshal(labelsResponse)
 	if err != nil {
 		t.Errorf("failed to marshal custom cost set: %s", err.Error())

+ 4 - 4
core/pkg/model/helper.go

@@ -16,14 +16,14 @@ func ConvertWindow(window *pb.Window) (opencost.Window, error) {
 	}
 	var res time.Duration
 	switch window.Resolution {
-	case "1d":
+	case pb.Resolution_RESOLUTION_1D:
 		res = timeutil.Day
-	case "1h":
+	case pb.Resolution_RESOLUTION_1H:
 		res = time.Hour
-	case "10m":
+	case pb.Resolution_RESOLUTION_10M:
 		res = time.Minute * 10
 	default:
-		return opencost.Window{}, fmt.Errorf("invalid window resolution %s", window.Resolution)
+		return opencost.Window{}, fmt.Errorf("invalid window resolution %v", window.Resolution)
 	}
 
 	start := window.Start.AsTime().UTC()

+ 8 - 9
core/pkg/model/helper_test.go

@@ -1,14 +1,13 @@
 package model
 
 import (
-	"reflect"
-	"testing"
-	"time"
-
 	"github.com/opencost/opencost/core/pkg/model/pb"
 	"github.com/opencost/opencost/core/pkg/opencost"
 	"github.com/opencost/opencost/core/pkg/util/timeutil"
 	"google.golang.org/protobuf/types/known/timestamppb"
+	"reflect"
+	"testing"
+	"time"
 )
 
 func TestConvertWindow(t *testing.T) {
@@ -31,7 +30,7 @@ func TestConvertWindow(t *testing.T) {
 		{
 			name: "invalid resolution",
 			window: &pb.Window{
-				Resolution: "invalid",
+				Resolution: 999,
 				Start:      timestamppb.New(timeDay),
 			},
 			want:    opencost.Window{},
@@ -40,7 +39,7 @@ func TestConvertWindow(t *testing.T) {
 		{
 			name: "invalid time",
 			window: &pb.Window{
-				Resolution: "1d",
+				Resolution: pb.Resolution_RESOLUTION_1D,
 				Start:      timestamppb.New(invalidTime),
 			},
 			want:    opencost.Window{},
@@ -49,7 +48,7 @@ func TestConvertWindow(t *testing.T) {
 		{
 			name: "valid 1d",
 			window: &pb.Window{
-				Resolution: "1d",
+				Resolution: pb.Resolution_RESOLUTION_1D,
 				Start:      timestamppb.New(timeDay),
 			},
 			want:    opencost.NewClosedWindow(timeDay, timeDay.Add(timeutil.Day)),
@@ -58,7 +57,7 @@ func TestConvertWindow(t *testing.T) {
 		{
 			name: "valid 1h",
 			window: &pb.Window{
-				Resolution: "1h",
+				Resolution: pb.Resolution_RESOLUTION_1H,
 				Start:      timestamppb.New(timeHour),
 			},
 			want:    opencost.NewClosedWindow(timeHour, timeHour.Add(time.Hour)),
@@ -67,7 +66,7 @@ func TestConvertWindow(t *testing.T) {
 		{
 			name: "valid 10m",
 			window: &pb.Window{
-				Resolution: "10m",
+				Resolution: pb.Resolution_RESOLUTION_10M,
 				Start:      timestamppb.New(timeTenMinute),
 			},
 			want:    opencost.NewClosedWindow(timeTenMinute, timeTenMinute.Add(10*time.Minute)),

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

@@ -61,7 +61,7 @@ func GenerateMockCustomCostSet(start, end time.Time) *pb.CustomCostResponse {
 	}
 }
 
-func GenerateMockLabelResponse(start time.Time, res string) *pb.LabelsResponse {
+func GenerateMockLabelResponse(start time.Time, res pb.Resolution) *pb.LabelsResponse {
 	return &pb.LabelsResponse{
 		Type:    "account-labels",
 		GroupId: "billing_account_xzy",

+ 246 - 0
core/pkg/model/pb/kubemodel/cluster.pb.go

@@ -0,0 +1,246 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.9
+// 	protoc        v6.32.1
+// source: kubemodel/cluster.proto
+
+package kubemodel
+
+import (
+	pb "github.com/opencost/opencost/core/pkg/model/pb"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Provider represents the cloud provider or infrastructure type
+type Provider int32
+
+const (
+	Provider_PROVIDER_UNSPECIFIED  Provider = 0
+	Provider_PROVIDER_AWS          Provider = 1
+	Provider_PROVIDER_GCP          Provider = 2
+	Provider_PROVIDER_AZURE        Provider = 3
+	Provider_PROVIDER_ON_PREMISES  Provider = 4
+	Provider_PROVIDER_ALIBABA      Provider = 5
+	Provider_PROVIDER_DIGITALOCEAN Provider = 6
+	Provider_PROVIDER_ORACLE       Provider = 7
+)
+
+// Enum value maps for Provider.
+var (
+	Provider_name = map[int32]string{
+		0: "PROVIDER_UNSPECIFIED",
+		1: "PROVIDER_AWS",
+		2: "PROVIDER_GCP",
+		3: "PROVIDER_AZURE",
+		4: "PROVIDER_ON_PREMISES",
+		5: "PROVIDER_ALIBABA",
+		6: "PROVIDER_DIGITALOCEAN",
+		7: "PROVIDER_ORACLE",
+	}
+	Provider_value = map[string]int32{
+		"PROVIDER_UNSPECIFIED":  0,
+		"PROVIDER_AWS":          1,
+		"PROVIDER_GCP":          2,
+		"PROVIDER_AZURE":        3,
+		"PROVIDER_ON_PREMISES":  4,
+		"PROVIDER_ALIBABA":      5,
+		"PROVIDER_DIGITALOCEAN": 6,
+		"PROVIDER_ORACLE":       7,
+	}
+)
+
+func (x Provider) Enum() *Provider {
+	p := new(Provider)
+	*p = x
+	return p
+}
+
+func (x Provider) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (Provider) Descriptor() protoreflect.EnumDescriptor {
+	return file_kubemodel_cluster_proto_enumTypes[0].Descriptor()
+}
+
+func (Provider) Type() protoreflect.EnumType {
+	return &file_kubemodel_cluster_proto_enumTypes[0]
+}
+
+func (x Provider) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use Provider.Descriptor instead.
+func (Provider) EnumDescriptor() ([]byte, []int) {
+	return file_kubemodel_cluster_proto_rawDescGZIP(), []int{0}
+}
+
+// Cluster represents the top-level Kubernetes cluster
+type Cluster struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Identification
+	// User-configured cluster identifier, defaults to kube-system namespace UID
+	// The kube-system namespace UID is unique per cluster and stable across its lifetime
+	ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
+	// Properties
+	Provider Provider `protobuf:"varint,2,opt,name=provider,proto3,enum=kubemodel.Provider" json:"provider,omitempty"`
+	Account  string   `protobuf:"bytes,3,opt,name=account,proto3" json:"account,omitempty"`
+	Name     string   `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
+	// Centralized window definition for entire cluster
+	// All resources inherit this window unless they specify their own duration
+	Window        *pb.Window `protobuf:"bytes,5,opt,name=window,proto3" json:"window,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Cluster) Reset() {
+	*x = Cluster{}
+	mi := &file_kubemodel_cluster_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Cluster) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Cluster) ProtoMessage() {}
+
+func (x *Cluster) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_cluster_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Cluster.ProtoReflect.Descriptor instead.
+func (*Cluster) Descriptor() ([]byte, []int) {
+	return file_kubemodel_cluster_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Cluster) GetID() string {
+	if x != nil {
+		return x.ID
+	}
+	return ""
+}
+
+func (x *Cluster) GetProvider() Provider {
+	if x != nil {
+		return x.Provider
+	}
+	return Provider_PROVIDER_UNSPECIFIED
+}
+
+func (x *Cluster) GetAccount() string {
+	if x != nil {
+		return x.Account
+	}
+	return ""
+}
+
+func (x *Cluster) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *Cluster) GetWindow() *pb.Window {
+	if x != nil {
+		return x.Window
+	}
+	return nil
+}
+
+var File_kubemodel_cluster_proto protoreflect.FileDescriptor
+
+const file_kubemodel_cluster_proto_rawDesc = "" +
+	"\n" +
+	"\x17kubemodel/cluster.proto\x12\tkubemodel\x1a\x12model/window.proto\"\x9f\x01\n" +
+	"\aCluster\x12\x0e\n" +
+	"\x02ID\x18\x01 \x01(\tR\x02ID\x12/\n" +
+	"\bprovider\x18\x02 \x01(\x0e2\x13.kubemodel.ProviderR\bprovider\x12\x18\n" +
+	"\aaccount\x18\x03 \x01(\tR\aaccount\x12\x12\n" +
+	"\x04name\x18\x04 \x01(\tR\x04name\x12%\n" +
+	"\x06window\x18\x05 \x01(\v2\r.model.WindowR\x06window*\xbc\x01\n" +
+	"\bProvider\x12\x18\n" +
+	"\x14PROVIDER_UNSPECIFIED\x10\x00\x12\x10\n" +
+	"\fPROVIDER_AWS\x10\x01\x12\x10\n" +
+	"\fPROVIDER_GCP\x10\x02\x12\x12\n" +
+	"\x0ePROVIDER_AZURE\x10\x03\x12\x18\n" +
+	"\x14PROVIDER_ON_PREMISES\x10\x04\x12\x14\n" +
+	"\x10PROVIDER_ALIBABA\x10\x05\x12\x19\n" +
+	"\x15PROVIDER_DIGITALOCEAN\x10\x06\x12\x13\n" +
+	"\x0fPROVIDER_ORACLE\x10\aB:Z8github.com/opencost/opencost/core/pkg/model/pb/kubemodelb\x06proto3"
+
+var (
+	file_kubemodel_cluster_proto_rawDescOnce sync.Once
+	file_kubemodel_cluster_proto_rawDescData []byte
+)
+
+func file_kubemodel_cluster_proto_rawDescGZIP() []byte {
+	file_kubemodel_cluster_proto_rawDescOnce.Do(func() {
+		file_kubemodel_cluster_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kubemodel_cluster_proto_rawDesc), len(file_kubemodel_cluster_proto_rawDesc)))
+	})
+	return file_kubemodel_cluster_proto_rawDescData
+}
+
+var file_kubemodel_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_kubemodel_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_kubemodel_cluster_proto_goTypes = []any{
+	(Provider)(0),     // 0: kubemodel.Provider
+	(*Cluster)(nil),   // 1: kubemodel.Cluster
+	(*pb.Window)(nil), // 2: model.Window
+}
+var file_kubemodel_cluster_proto_depIdxs = []int32{
+	0, // 0: kubemodel.Cluster.provider:type_name -> kubemodel.Provider
+	2, // 1: kubemodel.Cluster.window:type_name -> model.Window
+	2, // [2:2] is the sub-list for method output_type
+	2, // [2:2] is the sub-list for method input_type
+	2, // [2:2] is the sub-list for extension type_name
+	2, // [2:2] is the sub-list for extension extendee
+	0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_kubemodel_cluster_proto_init() }
+func file_kubemodel_cluster_proto_init() {
+	if File_kubemodel_cluster_proto != nil {
+		return
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_kubemodel_cluster_proto_rawDesc), len(file_kubemodel_cluster_proto_rawDesc)),
+			NumEnums:      1,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_kubemodel_cluster_proto_goTypes,
+		DependencyIndexes: file_kubemodel_cluster_proto_depIdxs,
+		EnumInfos:         file_kubemodel_cluster_proto_enumTypes,
+		MessageInfos:      file_kubemodel_cluster_proto_msgTypes,
+	}.Build()
+	File_kubemodel_cluster_proto = out.File
+	file_kubemodel_cluster_proto_goTypes = nil
+	file_kubemodel_cluster_proto_depIdxs = nil
+}

+ 258 - 0
core/pkg/model/pb/kubemodel/container.pb.go

@@ -0,0 +1,258 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.9
+// 	protoc        v6.32.1
+// source: kubemodel/container.proto
+
+package kubemodel
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Container represents a container within a pod (allocated resource)
+type Container struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Identification
+	PodID string `protobuf:"bytes,1,opt,name=podID,proto3" json:"podID,omitempty"`
+	// Properties
+	Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+	// Resource lifecycle (only when different from cluster window)
+	CreationTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=creationTime,proto3,oneof" json:"creationTime,omitempty"`
+	DeletionTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=deletionTime,proto3,oneof" json:"deletionTime,omitempty"`
+	// Usage metrics
+	// CPU usage in core-hours
+	CpuCoreHours float32 `protobuf:"fixed32,5,opt,name=cpuCoreHours,proto3" json:"cpuCoreHours,omitempty"`
+	// CPU request average in cores
+	CpuCoreRequestAverage float32 `protobuf:"fixed32,6,opt,name=cpuCoreRequestAverage,proto3" json:"cpuCoreRequestAverage,omitempty"`
+	// CPU usage average in cores
+	CpuCoreUsageAverage float32 `protobuf:"fixed32,7,opt,name=cpuCoreUsageAverage,proto3" json:"cpuCoreUsageAverage,omitempty"`
+	// CPU usage max in cores
+	CpuCoreUsageMax float32 `protobuf:"fixed32,8,opt,name=cpuCoreUsageMax,proto3" json:"cpuCoreUsageMax,omitempty"`
+	// RAM usage in byte-hours
+	RamByteHours int64 `protobuf:"varint,9,opt,name=ramByteHours,proto3" json:"ramByteHours,omitempty"`
+	// RAM request average in bytes
+	RamBytesRequestAverage int64 `protobuf:"varint,10,opt,name=ramBytesRequestAverage,proto3" json:"ramBytesRequestAverage,omitempty"`
+	// RAM usage average in bytes
+	RamBytesUsageAverage int64 `protobuf:"varint,11,opt,name=ramBytesUsageAverage,proto3" json:"ramBytesUsageAverage,omitempty"`
+	// RAM usage max in bytes
+	RamBytesUsageMax int64 `protobuf:"varint,12,opt,name=ramBytesUsageMax,proto3" json:"ramBytesUsageMax,omitempty"`
+	// Diagnostic information about this resource
+	Diagnostic    *DiagnosticResult `protobuf:"bytes,99,opt,name=diagnostic,proto3,oneof" json:"diagnostic,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Container) Reset() {
+	*x = Container{}
+	mi := &file_kubemodel_container_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Container) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Container) ProtoMessage() {}
+
+func (x *Container) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_container_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Container.ProtoReflect.Descriptor instead.
+func (*Container) Descriptor() ([]byte, []int) {
+	return file_kubemodel_container_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Container) GetPodID() string {
+	if x != nil {
+		return x.PodID
+	}
+	return ""
+}
+
+func (x *Container) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *Container) GetCreationTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.CreationTime
+	}
+	return nil
+}
+
+func (x *Container) GetDeletionTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.DeletionTime
+	}
+	return nil
+}
+
+func (x *Container) GetCpuCoreHours() float32 {
+	if x != nil {
+		return x.CpuCoreHours
+	}
+	return 0
+}
+
+func (x *Container) GetCpuCoreRequestAverage() float32 {
+	if x != nil {
+		return x.CpuCoreRequestAverage
+	}
+	return 0
+}
+
+func (x *Container) GetCpuCoreUsageAverage() float32 {
+	if x != nil {
+		return x.CpuCoreUsageAverage
+	}
+	return 0
+}
+
+func (x *Container) GetCpuCoreUsageMax() float32 {
+	if x != nil {
+		return x.CpuCoreUsageMax
+	}
+	return 0
+}
+
+func (x *Container) GetRamByteHours() int64 {
+	if x != nil {
+		return x.RamByteHours
+	}
+	return 0
+}
+
+func (x *Container) GetRamBytesRequestAverage() int64 {
+	if x != nil {
+		return x.RamBytesRequestAverage
+	}
+	return 0
+}
+
+func (x *Container) GetRamBytesUsageAverage() int64 {
+	if x != nil {
+		return x.RamBytesUsageAverage
+	}
+	return 0
+}
+
+func (x *Container) GetRamBytesUsageMax() int64 {
+	if x != nil {
+		return x.RamBytesUsageMax
+	}
+	return 0
+}
+
+func (x *Container) GetDiagnostic() *DiagnosticResult {
+	if x != nil {
+		return x.Diagnostic
+	}
+	return nil
+}
+
+var File_kubemodel_container_proto protoreflect.FileDescriptor
+
+const file_kubemodel_container_proto_rawDesc = "" +
+	"\n" +
+	"\x19kubemodel/container.proto\x12\tkubemodel\x1a\x1akubemodel/diagnostic.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa4\x05\n" +
+	"\tContainer\x12\x14\n" +
+	"\x05podID\x18\x01 \x01(\tR\x05podID\x12\x12\n" +
+	"\x04name\x18\x02 \x01(\tR\x04name\x12C\n" +
+	"\fcreationTime\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\fcreationTime\x88\x01\x01\x12C\n" +
+	"\fdeletionTime\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\fdeletionTime\x88\x01\x01\x12\"\n" +
+	"\fcpuCoreHours\x18\x05 \x01(\x02R\fcpuCoreHours\x124\n" +
+	"\x15cpuCoreRequestAverage\x18\x06 \x01(\x02R\x15cpuCoreRequestAverage\x120\n" +
+	"\x13cpuCoreUsageAverage\x18\a \x01(\x02R\x13cpuCoreUsageAverage\x12(\n" +
+	"\x0fcpuCoreUsageMax\x18\b \x01(\x02R\x0fcpuCoreUsageMax\x12\"\n" +
+	"\framByteHours\x18\t \x01(\x03R\framByteHours\x126\n" +
+	"\x16ramBytesRequestAverage\x18\n" +
+	" \x01(\x03R\x16ramBytesRequestAverage\x122\n" +
+	"\x14ramBytesUsageAverage\x18\v \x01(\x03R\x14ramBytesUsageAverage\x12*\n" +
+	"\x10ramBytesUsageMax\x18\f \x01(\x03R\x10ramBytesUsageMax\x12@\n" +
+	"\n" +
+	"diagnostic\x18c \x01(\v2\x1b.kubemodel.DiagnosticResultH\x02R\n" +
+	"diagnostic\x88\x01\x01B\x0f\n" +
+	"\r_creationTimeB\x0f\n" +
+	"\r_deletionTimeB\r\n" +
+	"\v_diagnosticB:Z8github.com/opencost/opencost/core/pkg/model/pb/kubemodelb\x06proto3"
+
+var (
+	file_kubemodel_container_proto_rawDescOnce sync.Once
+	file_kubemodel_container_proto_rawDescData []byte
+)
+
+func file_kubemodel_container_proto_rawDescGZIP() []byte {
+	file_kubemodel_container_proto_rawDescOnce.Do(func() {
+		file_kubemodel_container_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kubemodel_container_proto_rawDesc), len(file_kubemodel_container_proto_rawDesc)))
+	})
+	return file_kubemodel_container_proto_rawDescData
+}
+
+var file_kubemodel_container_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_kubemodel_container_proto_goTypes = []any{
+	(*Container)(nil),             // 0: kubemodel.Container
+	(*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp
+	(*DiagnosticResult)(nil),      // 2: kubemodel.DiagnosticResult
+}
+var file_kubemodel_container_proto_depIdxs = []int32{
+	1, // 0: kubemodel.Container.creationTime:type_name -> google.protobuf.Timestamp
+	1, // 1: kubemodel.Container.deletionTime:type_name -> google.protobuf.Timestamp
+	2, // 2: kubemodel.Container.diagnostic:type_name -> kubemodel.DiagnosticResult
+	3, // [3:3] is the sub-list for method output_type
+	3, // [3:3] is the sub-list for method input_type
+	3, // [3:3] is the sub-list for extension type_name
+	3, // [3:3] is the sub-list for extension extendee
+	0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_kubemodel_container_proto_init() }
+func file_kubemodel_container_proto_init() {
+	if File_kubemodel_container_proto != nil {
+		return
+	}
+	file_kubemodel_diagnostic_proto_init()
+	file_kubemodel_container_proto_msgTypes[0].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_kubemodel_container_proto_rawDesc), len(file_kubemodel_container_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_kubemodel_container_proto_goTypes,
+		DependencyIndexes: file_kubemodel_container_proto_depIdxs,
+		MessageInfos:      file_kubemodel_container_proto_msgTypes,
+	}.Build()
+	File_kubemodel_container_proto = out.File
+	file_kubemodel_container_proto_goTypes = nil
+	file_kubemodel_container_proto_depIdxs = nil
+}

+ 298 - 0
core/pkg/model/pb/kubemodel/controller.pb.go

@@ -0,0 +1,298 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.9
+// 	protoc        v6.32.1
+// source: kubemodel/controller.proto
+
+package kubemodel
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type ControllerKind int32
+
+const (
+	ControllerKind_KIND_UNSPECIFIED ControllerKind = 0
+	ControllerKind_DEPLOYMENT       ControllerKind = 1
+	ControllerKind_STATEFULSET      ControllerKind = 2
+	ControllerKind_DAEMONSET        ControllerKind = 3
+	ControllerKind_JOB              ControllerKind = 4
+	ControllerKind_CRONJOB          ControllerKind = 5
+	ControllerKind_REPLICASET       ControllerKind = 6
+)
+
+// Enum value maps for ControllerKind.
+var (
+	ControllerKind_name = map[int32]string{
+		0: "KIND_UNSPECIFIED",
+		1: "DEPLOYMENT",
+		2: "STATEFULSET",
+		3: "DAEMONSET",
+		4: "JOB",
+		5: "CRONJOB",
+		6: "REPLICASET",
+	}
+	ControllerKind_value = map[string]int32{
+		"KIND_UNSPECIFIED": 0,
+		"DEPLOYMENT":       1,
+		"STATEFULSET":      2,
+		"DAEMONSET":        3,
+		"JOB":              4,
+		"CRONJOB":          5,
+		"REPLICASET":       6,
+	}
+)
+
+func (x ControllerKind) Enum() *ControllerKind {
+	p := new(ControllerKind)
+	*p = x
+	return p
+}
+
+func (x ControllerKind) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (ControllerKind) Descriptor() protoreflect.EnumDescriptor {
+	return file_kubemodel_controller_proto_enumTypes[0].Descriptor()
+}
+
+func (ControllerKind) Type() protoreflect.EnumType {
+	return &file_kubemodel_controller_proto_enumTypes[0]
+}
+
+func (x ControllerKind) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use ControllerKind.Descriptor instead.
+func (ControllerKind) EnumDescriptor() ([]byte, []int) {
+	return file_kubemodel_controller_proto_rawDescGZIP(), []int{0}
+}
+
+// Controller represents a Kubernetes workload controller
+type Controller struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Identification
+	ID          string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
+	NamespaceID string `protobuf:"bytes,2,opt,name=namespaceID,proto3" json:"namespaceID,omitempty"`
+	// Properties
+	Name        string            `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+	Kind        ControllerKind    `protobuf:"varint,4,opt,name=kind,proto3,enum=kubemodel.ControllerKind" json:"kind,omitempty"`
+	Labels      map[string]string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	Annotations map[string]string `protobuf:"bytes,6,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	// Resource lifecycle (only when different from cluster window)
+	CreationTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=creationTime,proto3,oneof" json:"creationTime,omitempty"`
+	DeletionTime *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=deletionTime,proto3,oneof" json:"deletionTime,omitempty"`
+	// Diagnostic information about this resource
+	Diagnostic    *DiagnosticResult `protobuf:"bytes,99,opt,name=diagnostic,proto3,oneof" json:"diagnostic,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Controller) Reset() {
+	*x = Controller{}
+	mi := &file_kubemodel_controller_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Controller) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Controller) ProtoMessage() {}
+
+func (x *Controller) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_controller_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Controller.ProtoReflect.Descriptor instead.
+func (*Controller) Descriptor() ([]byte, []int) {
+	return file_kubemodel_controller_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Controller) GetID() string {
+	if x != nil {
+		return x.ID
+	}
+	return ""
+}
+
+func (x *Controller) GetNamespaceID() string {
+	if x != nil {
+		return x.NamespaceID
+	}
+	return ""
+}
+
+func (x *Controller) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *Controller) GetKind() ControllerKind {
+	if x != nil {
+		return x.Kind
+	}
+	return ControllerKind_KIND_UNSPECIFIED
+}
+
+func (x *Controller) GetLabels() map[string]string {
+	if x != nil {
+		return x.Labels
+	}
+	return nil
+}
+
+func (x *Controller) GetAnnotations() map[string]string {
+	if x != nil {
+		return x.Annotations
+	}
+	return nil
+}
+
+func (x *Controller) GetCreationTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.CreationTime
+	}
+	return nil
+}
+
+func (x *Controller) GetDeletionTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.DeletionTime
+	}
+	return nil
+}
+
+func (x *Controller) GetDiagnostic() *DiagnosticResult {
+	if x != nil {
+		return x.Diagnostic
+	}
+	return nil
+}
+
+var File_kubemodel_controller_proto protoreflect.FileDescriptor
+
+const file_kubemodel_controller_proto_rawDesc = "" +
+	"\n" +
+	"\x1akubemodel/controller.proto\x12\tkubemodel\x1a\x1akubemodel/diagnostic.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xfe\x04\n" +
+	"\n" +
+	"Controller\x12\x0e\n" +
+	"\x02ID\x18\x01 \x01(\tR\x02ID\x12 \n" +
+	"\vnamespaceID\x18\x02 \x01(\tR\vnamespaceID\x12\x12\n" +
+	"\x04name\x18\x03 \x01(\tR\x04name\x12-\n" +
+	"\x04kind\x18\x04 \x01(\x0e2\x19.kubemodel.ControllerKindR\x04kind\x129\n" +
+	"\x06labels\x18\x05 \x03(\v2!.kubemodel.Controller.LabelsEntryR\x06labels\x12H\n" +
+	"\vannotations\x18\x06 \x03(\v2&.kubemodel.Controller.AnnotationsEntryR\vannotations\x12C\n" +
+	"\fcreationTime\x18\a \x01(\v2\x1a.google.protobuf.TimestampH\x00R\fcreationTime\x88\x01\x01\x12C\n" +
+	"\fdeletionTime\x18\b \x01(\v2\x1a.google.protobuf.TimestampH\x01R\fdeletionTime\x88\x01\x01\x12@\n" +
+	"\n" +
+	"diagnostic\x18c \x01(\v2\x1b.kubemodel.DiagnosticResultH\x02R\n" +
+	"diagnostic\x88\x01\x01\x1a9\n" +
+	"\vLabelsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" +
+	"\x10AnnotationsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x0f\n" +
+	"\r_creationTimeB\x0f\n" +
+	"\r_deletionTimeB\r\n" +
+	"\v_diagnostic*|\n" +
+	"\x0eControllerKind\x12\x14\n" +
+	"\x10KIND_UNSPECIFIED\x10\x00\x12\x0e\n" +
+	"\n" +
+	"DEPLOYMENT\x10\x01\x12\x0f\n" +
+	"\vSTATEFULSET\x10\x02\x12\r\n" +
+	"\tDAEMONSET\x10\x03\x12\a\n" +
+	"\x03JOB\x10\x04\x12\v\n" +
+	"\aCRONJOB\x10\x05\x12\x0e\n" +
+	"\n" +
+	"REPLICASET\x10\x06B:Z8github.com/opencost/opencost/core/pkg/model/pb/kubemodelb\x06proto3"
+
+var (
+	file_kubemodel_controller_proto_rawDescOnce sync.Once
+	file_kubemodel_controller_proto_rawDescData []byte
+)
+
+func file_kubemodel_controller_proto_rawDescGZIP() []byte {
+	file_kubemodel_controller_proto_rawDescOnce.Do(func() {
+		file_kubemodel_controller_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kubemodel_controller_proto_rawDesc), len(file_kubemodel_controller_proto_rawDesc)))
+	})
+	return file_kubemodel_controller_proto_rawDescData
+}
+
+var file_kubemodel_controller_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_kubemodel_controller_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
+var file_kubemodel_controller_proto_goTypes = []any{
+	(ControllerKind)(0),           // 0: kubemodel.ControllerKind
+	(*Controller)(nil),            // 1: kubemodel.Controller
+	nil,                           // 2: kubemodel.Controller.LabelsEntry
+	nil,                           // 3: kubemodel.Controller.AnnotationsEntry
+	(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp
+	(*DiagnosticResult)(nil),      // 5: kubemodel.DiagnosticResult
+}
+var file_kubemodel_controller_proto_depIdxs = []int32{
+	0, // 0: kubemodel.Controller.kind:type_name -> kubemodel.ControllerKind
+	2, // 1: kubemodel.Controller.labels:type_name -> kubemodel.Controller.LabelsEntry
+	3, // 2: kubemodel.Controller.annotations:type_name -> kubemodel.Controller.AnnotationsEntry
+	4, // 3: kubemodel.Controller.creationTime:type_name -> google.protobuf.Timestamp
+	4, // 4: kubemodel.Controller.deletionTime:type_name -> google.protobuf.Timestamp
+	5, // 5: kubemodel.Controller.diagnostic:type_name -> kubemodel.DiagnosticResult
+	6, // [6:6] is the sub-list for method output_type
+	6, // [6:6] is the sub-list for method input_type
+	6, // [6:6] is the sub-list for extension type_name
+	6, // [6:6] is the sub-list for extension extendee
+	0, // [0:6] is the sub-list for field type_name
+}
+
+func init() { file_kubemodel_controller_proto_init() }
+func file_kubemodel_controller_proto_init() {
+	if File_kubemodel_controller_proto != nil {
+		return
+	}
+	file_kubemodel_diagnostic_proto_init()
+	file_kubemodel_controller_proto_msgTypes[0].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_kubemodel_controller_proto_rawDesc), len(file_kubemodel_controller_proto_rawDesc)),
+			NumEnums:      1,
+			NumMessages:   3,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_kubemodel_controller_proto_goTypes,
+		DependencyIndexes: file_kubemodel_controller_proto_depIdxs,
+		EnumInfos:         file_kubemodel_controller_proto_enumTypes,
+		MessageInfos:      file_kubemodel_controller_proto_msgTypes,
+	}.Build()
+	File_kubemodel_controller_proto = out.File
+	file_kubemodel_controller_proto_goTypes = nil
+	file_kubemodel_controller_proto_depIdxs = nil
+}

+ 266 - 0
core/pkg/model/pb/kubemodel/diagnostic.pb.go

@@ -0,0 +1,266 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.9
+// 	protoc        v6.32.1
+// source: kubemodel/diagnostic.proto
+
+package kubemodel
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// DiagnosticResult represents the result of a diagnostic run
+// This matches the JSON structure from core/pkg/diagnostics/diagnostics.go
+type DiagnosticResult struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Unique Identifier for the diagnostic run result
+	Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+	// Name of the diagnostic that ran
+	Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+	// Description of the diagnostic run, human readable description
+	Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"`
+	// Category of the diagnostic run, used to group similar diagnostics
+	Category string `protobuf:"bytes,4,opt,name=category,proto3" json:"category,omitempty"`
+	// Timestamp when the diagnostic run was executed
+	Timestamp *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+	// Error message if the diagnostic run failed (optional)
+	Error string `protobuf:"bytes,6,opt,name=error,proto3" json:"error,omitempty"`
+	// Additional custom information about the diagnostic run
+	// Using string values to match map[string]any from JSON
+	Details       map[string]string `protobuf:"bytes,7,rep,name=details,proto3" json:"details,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *DiagnosticResult) Reset() {
+	*x = DiagnosticResult{}
+	mi := &file_kubemodel_diagnostic_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DiagnosticResult) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DiagnosticResult) ProtoMessage() {}
+
+func (x *DiagnosticResult) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_diagnostic_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DiagnosticResult.ProtoReflect.Descriptor instead.
+func (*DiagnosticResult) Descriptor() ([]byte, []int) {
+	return file_kubemodel_diagnostic_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *DiagnosticResult) GetId() string {
+	if x != nil {
+		return x.Id
+	}
+	return ""
+}
+
+func (x *DiagnosticResult) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *DiagnosticResult) GetDescription() string {
+	if x != nil {
+		return x.Description
+	}
+	return ""
+}
+
+func (x *DiagnosticResult) GetCategory() string {
+	if x != nil {
+		return x.Category
+	}
+	return ""
+}
+
+func (x *DiagnosticResult) GetTimestamp() *timestamppb.Timestamp {
+	if x != nil {
+		return x.Timestamp
+	}
+	return nil
+}
+
+func (x *DiagnosticResult) GetError() string {
+	if x != nil {
+		return x.Error
+	}
+	return ""
+}
+
+func (x *DiagnosticResult) GetDetails() map[string]string {
+	if x != nil {
+		return x.Details
+	}
+	return nil
+}
+
+// DiagnosticsRunReport contains the start time and all diagnostic results
+// This matches the JSON structure from core/pkg/diagnostics/diagnostics.go
+type DiagnosticsRunReport struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Application name that the diagnostics run belongs to
+	Application string `protobuf:"bytes,1,opt,name=application,proto3" json:"application,omitempty"`
+	// Time when the full diagnostics run started
+	StartTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=startTime,proto3" json:"startTime,omitempty"`
+	// All results of the diagnostics run
+	Results       []*DiagnosticResult `protobuf:"bytes,3,rep,name=results,proto3" json:"results,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *DiagnosticsRunReport) Reset() {
+	*x = DiagnosticsRunReport{}
+	mi := &file_kubemodel_diagnostic_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DiagnosticsRunReport) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DiagnosticsRunReport) ProtoMessage() {}
+
+func (x *DiagnosticsRunReport) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_diagnostic_proto_msgTypes[1]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DiagnosticsRunReport.ProtoReflect.Descriptor instead.
+func (*DiagnosticsRunReport) Descriptor() ([]byte, []int) {
+	return file_kubemodel_diagnostic_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *DiagnosticsRunReport) GetApplication() string {
+	if x != nil {
+		return x.Application
+	}
+	return ""
+}
+
+func (x *DiagnosticsRunReport) GetStartTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.StartTime
+	}
+	return nil
+}
+
+func (x *DiagnosticsRunReport) GetResults() []*DiagnosticResult {
+	if x != nil {
+		return x.Results
+	}
+	return nil
+}
+
+var File_kubemodel_diagnostic_proto protoreflect.FileDescriptor
+
+const file_kubemodel_diagnostic_proto_rawDesc = "" +
+	"\n" +
+	"\x1akubemodel/diagnostic.proto\x12\tkubemodel\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc4\x02\n" +
+	"\x10DiagnosticResult\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
+	"\x04name\x18\x02 \x01(\tR\x04name\x12 \n" +
+	"\vdescription\x18\x03 \x01(\tR\vdescription\x12\x1a\n" +
+	"\bcategory\x18\x04 \x01(\tR\bcategory\x128\n" +
+	"\ttimestamp\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12\x14\n" +
+	"\x05error\x18\x06 \x01(\tR\x05error\x12B\n" +
+	"\adetails\x18\a \x03(\v2(.kubemodel.DiagnosticResult.DetailsEntryR\adetails\x1a:\n" +
+	"\fDetailsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa9\x01\n" +
+	"\x14DiagnosticsRunReport\x12 \n" +
+	"\vapplication\x18\x01 \x01(\tR\vapplication\x128\n" +
+	"\tstartTime\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tstartTime\x125\n" +
+	"\aresults\x18\x03 \x03(\v2\x1b.kubemodel.DiagnosticResultR\aresultsB:Z8github.com/opencost/opencost/core/pkg/model/pb/kubemodelb\x06proto3"
+
+var (
+	file_kubemodel_diagnostic_proto_rawDescOnce sync.Once
+	file_kubemodel_diagnostic_proto_rawDescData []byte
+)
+
+func file_kubemodel_diagnostic_proto_rawDescGZIP() []byte {
+	file_kubemodel_diagnostic_proto_rawDescOnce.Do(func() {
+		file_kubemodel_diagnostic_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kubemodel_diagnostic_proto_rawDesc), len(file_kubemodel_diagnostic_proto_rawDesc)))
+	})
+	return file_kubemodel_diagnostic_proto_rawDescData
+}
+
+var file_kubemodel_diagnostic_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
+var file_kubemodel_diagnostic_proto_goTypes = []any{
+	(*DiagnosticResult)(nil),      // 0: kubemodel.DiagnosticResult
+	(*DiagnosticsRunReport)(nil),  // 1: kubemodel.DiagnosticsRunReport
+	nil,                           // 2: kubemodel.DiagnosticResult.DetailsEntry
+	(*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp
+}
+var file_kubemodel_diagnostic_proto_depIdxs = []int32{
+	3, // 0: kubemodel.DiagnosticResult.timestamp:type_name -> google.protobuf.Timestamp
+	2, // 1: kubemodel.DiagnosticResult.details:type_name -> kubemodel.DiagnosticResult.DetailsEntry
+	3, // 2: kubemodel.DiagnosticsRunReport.startTime:type_name -> google.protobuf.Timestamp
+	0, // 3: kubemodel.DiagnosticsRunReport.results:type_name -> kubemodel.DiagnosticResult
+	4, // [4:4] is the sub-list for method output_type
+	4, // [4:4] is the sub-list for method input_type
+	4, // [4:4] is the sub-list for extension type_name
+	4, // [4:4] is the sub-list for extension extendee
+	0, // [0:4] is the sub-list for field type_name
+}
+
+func init() { file_kubemodel_diagnostic_proto_init() }
+func file_kubemodel_diagnostic_proto_init() {
+	if File_kubemodel_diagnostic_proto != nil {
+		return
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_kubemodel_diagnostic_proto_rawDesc), len(file_kubemodel_diagnostic_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   3,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_kubemodel_diagnostic_proto_goTypes,
+		DependencyIndexes: file_kubemodel_diagnostic_proto_depIdxs,
+		MessageInfos:      file_kubemodel_diagnostic_proto_msgTypes,
+	}.Build()
+	File_kubemodel_diagnostic_proto = out.File
+	file_kubemodel_diagnostic_proto_goTypes = nil
+	file_kubemodel_diagnostic_proto_depIdxs = nil
+}

+ 366 - 0
core/pkg/model/pb/kubemodel/gpu.pb.go

@@ -0,0 +1,366 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.9
+// 	protoc        v6.32.1
+// source: kubemodel/gpu.proto
+
+package kubemodel
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// GPUDevice represents a GPU device with DCGM integration (provisioned resource)
+// This tracks available GPU capacity on a node
+type GPUDevice struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Identification
+	ID     string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`         // GPU UUID (hardware identifier)
+	NodeID string `protobuf:"bytes,2,opt,name=nodeID,proto3" json:"nodeID,omitempty"` // Node hosting this GPU device
+	// Properties
+	DeviceNumber int32  `protobuf:"varint,3,opt,name=deviceNumber,proto3" json:"deviceNumber,omitempty"`
+	ModelName    string `protobuf:"bytes,4,opt,name=modelName,proto3" json:"modelName,omitempty"`
+	// GPU sharing information
+	IsShared        bool    `protobuf:"varint,6,opt,name=isShared,proto3" json:"isShared,omitempty"`
+	SharePercentage float32 `protobuf:"fixed32,9,opt,name=sharePercentage,proto3" json:"sharePercentage,omitempty"`
+	// Capacity metrics
+	// GPU hours available
+	GpuHours float32 `protobuf:"fixed32,10,opt,name=gpuHours,proto3" json:"gpuHours,omitempty"`
+	// GPU request average percentage (0-100)
+	GpuRequestAverage float32 `protobuf:"fixed32,11,opt,name=gpuRequestAverage,proto3" json:"gpuRequestAverage,omitempty"`
+	// GPU usage average percentage (0-100)
+	GpuUsageAverage float32 `protobuf:"fixed32,12,opt,name=gpuUsageAverage,proto3" json:"gpuUsageAverage,omitempty"`
+	// GPU usage max percentage (0-100)
+	GpuUsageMax float32 `protobuf:"fixed32,13,opt,name=gpuUsageMax,proto3" json:"gpuUsageMax,omitempty"`
+	// GPU memory capacity in bytes
+	MemoryBytes int64 `protobuf:"varint,14,opt,name=memoryBytes,proto3" json:"memoryBytes,omitempty"`
+	// Diagnostic information about this resource
+	Diagnostic    *DiagnosticResult `protobuf:"bytes,99,opt,name=diagnostic,proto3,oneof" json:"diagnostic,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GPUDevice) Reset() {
+	*x = GPUDevice{}
+	mi := &file_kubemodel_gpu_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GPUDevice) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GPUDevice) ProtoMessage() {}
+
+func (x *GPUDevice) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_gpu_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GPUDevice.ProtoReflect.Descriptor instead.
+func (*GPUDevice) Descriptor() ([]byte, []int) {
+	return file_kubemodel_gpu_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *GPUDevice) GetID() string {
+	if x != nil {
+		return x.ID
+	}
+	return ""
+}
+
+func (x *GPUDevice) GetNodeID() string {
+	if x != nil {
+		return x.NodeID
+	}
+	return ""
+}
+
+func (x *GPUDevice) GetDeviceNumber() int32 {
+	if x != nil {
+		return x.DeviceNumber
+	}
+	return 0
+}
+
+func (x *GPUDevice) GetModelName() string {
+	if x != nil {
+		return x.ModelName
+	}
+	return ""
+}
+
+func (x *GPUDevice) GetIsShared() bool {
+	if x != nil {
+		return x.IsShared
+	}
+	return false
+}
+
+func (x *GPUDevice) GetSharePercentage() float32 {
+	if x != nil {
+		return x.SharePercentage
+	}
+	return 0
+}
+
+func (x *GPUDevice) GetGpuHours() float32 {
+	if x != nil {
+		return x.GpuHours
+	}
+	return 0
+}
+
+func (x *GPUDevice) GetGpuRequestAverage() float32 {
+	if x != nil {
+		return x.GpuRequestAverage
+	}
+	return 0
+}
+
+func (x *GPUDevice) GetGpuUsageAverage() float32 {
+	if x != nil {
+		return x.GpuUsageAverage
+	}
+	return 0
+}
+
+func (x *GPUDevice) GetGpuUsageMax() float32 {
+	if x != nil {
+		return x.GpuUsageMax
+	}
+	return 0
+}
+
+func (x *GPUDevice) GetMemoryBytes() int64 {
+	if x != nil {
+		return x.MemoryBytes
+	}
+	return 0
+}
+
+func (x *GPUDevice) GetDiagnostic() *DiagnosticResult {
+	if x != nil {
+		return x.Diagnostic
+	}
+	return nil
+}
+
+// GPUUsage represents GPU resources consumed by a container (allocated resource)
+// This tracks actual GPU usage by containers for cost analysis
+type GPUUsage struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Identification
+	ContainerID string `protobuf:"bytes,1,opt,name=containerID,proto3" json:"containerID,omitempty"` // Container consuming GPU resources
+	GpuDeviceID string `protobuf:"bytes,2,opt,name=gpuDeviceID,proto3" json:"gpuDeviceID,omitempty"` // Reference to the GPU device being used
+	// Usage metrics
+	// GPU usage in device-hours consumed
+	GpuHours float32 `protobuf:"fixed32,3,opt,name=gpuHours,proto3" json:"gpuHours,omitempty"`
+	// GPU request in percentage (0-100)
+	GpuRequestPercentage float32 `protobuf:"fixed32,4,opt,name=gpuRequestPercentage,proto3" json:"gpuRequestPercentage,omitempty"`
+	// GPU usage average percentage (0-100)
+	GpuUsageAverage float32 `protobuf:"fixed32,5,opt,name=gpuUsageAverage,proto3" json:"gpuUsageAverage,omitempty"`
+	// GPU usage max percentage (0-100)
+	GpuUsageMax float32 `protobuf:"fixed32,6,opt,name=gpuUsageMax,proto3" json:"gpuUsageMax,omitempty"`
+	// GPU memory usage in bytes
+	MemoryBytesUsed int64 `protobuf:"varint,7,opt,name=memoryBytesUsed,proto3" json:"memoryBytesUsed,omitempty"`
+	// Diagnostic information about this resource
+	Diagnostic    *DiagnosticResult `protobuf:"bytes,99,opt,name=diagnostic,proto3,oneof" json:"diagnostic,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GPUUsage) Reset() {
+	*x = GPUUsage{}
+	mi := &file_kubemodel_gpu_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GPUUsage) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GPUUsage) ProtoMessage() {}
+
+func (x *GPUUsage) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_gpu_proto_msgTypes[1]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GPUUsage.ProtoReflect.Descriptor instead.
+func (*GPUUsage) Descriptor() ([]byte, []int) {
+	return file_kubemodel_gpu_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *GPUUsage) GetContainerID() string {
+	if x != nil {
+		return x.ContainerID
+	}
+	return ""
+}
+
+func (x *GPUUsage) GetGpuDeviceID() string {
+	if x != nil {
+		return x.GpuDeviceID
+	}
+	return ""
+}
+
+func (x *GPUUsage) GetGpuHours() float32 {
+	if x != nil {
+		return x.GpuHours
+	}
+	return 0
+}
+
+func (x *GPUUsage) GetGpuRequestPercentage() float32 {
+	if x != nil {
+		return x.GpuRequestPercentage
+	}
+	return 0
+}
+
+func (x *GPUUsage) GetGpuUsageAverage() float32 {
+	if x != nil {
+		return x.GpuUsageAverage
+	}
+	return 0
+}
+
+func (x *GPUUsage) GetGpuUsageMax() float32 {
+	if x != nil {
+		return x.GpuUsageMax
+	}
+	return 0
+}
+
+func (x *GPUUsage) GetMemoryBytesUsed() int64 {
+	if x != nil {
+		return x.MemoryBytesUsed
+	}
+	return 0
+}
+
+func (x *GPUUsage) GetDiagnostic() *DiagnosticResult {
+	if x != nil {
+		return x.Diagnostic
+	}
+	return nil
+}
+
+var File_kubemodel_gpu_proto protoreflect.FileDescriptor
+
+const file_kubemodel_gpu_proto_rawDesc = "" +
+	"\n" +
+	"\x13kubemodel/gpu.proto\x12\tkubemodel\x1a\x1akubemodel/diagnostic.proto\"\xc4\x03\n" +
+	"\tGPUDevice\x12\x0e\n" +
+	"\x02ID\x18\x01 \x01(\tR\x02ID\x12\x16\n" +
+	"\x06nodeID\x18\x02 \x01(\tR\x06nodeID\x12\"\n" +
+	"\fdeviceNumber\x18\x03 \x01(\x05R\fdeviceNumber\x12\x1c\n" +
+	"\tmodelName\x18\x04 \x01(\tR\tmodelName\x12\x1a\n" +
+	"\bisShared\x18\x06 \x01(\bR\bisShared\x12(\n" +
+	"\x0fsharePercentage\x18\t \x01(\x02R\x0fsharePercentage\x12\x1a\n" +
+	"\bgpuHours\x18\n" +
+	" \x01(\x02R\bgpuHours\x12,\n" +
+	"\x11gpuRequestAverage\x18\v \x01(\x02R\x11gpuRequestAverage\x12(\n" +
+	"\x0fgpuUsageAverage\x18\f \x01(\x02R\x0fgpuUsageAverage\x12 \n" +
+	"\vgpuUsageMax\x18\r \x01(\x02R\vgpuUsageMax\x12 \n" +
+	"\vmemoryBytes\x18\x0e \x01(\x03R\vmemoryBytes\x12@\n" +
+	"\n" +
+	"diagnostic\x18c \x01(\v2\x1b.kubemodel.DiagnosticResultH\x00R\n" +
+	"diagnostic\x88\x01\x01B\r\n" +
+	"\v_diagnostic\"\xe5\x02\n" +
+	"\bGPUUsage\x12 \n" +
+	"\vcontainerID\x18\x01 \x01(\tR\vcontainerID\x12 \n" +
+	"\vgpuDeviceID\x18\x02 \x01(\tR\vgpuDeviceID\x12\x1a\n" +
+	"\bgpuHours\x18\x03 \x01(\x02R\bgpuHours\x122\n" +
+	"\x14gpuRequestPercentage\x18\x04 \x01(\x02R\x14gpuRequestPercentage\x12(\n" +
+	"\x0fgpuUsageAverage\x18\x05 \x01(\x02R\x0fgpuUsageAverage\x12 \n" +
+	"\vgpuUsageMax\x18\x06 \x01(\x02R\vgpuUsageMax\x12(\n" +
+	"\x0fmemoryBytesUsed\x18\a \x01(\x03R\x0fmemoryBytesUsed\x12@\n" +
+	"\n" +
+	"diagnostic\x18c \x01(\v2\x1b.kubemodel.DiagnosticResultH\x00R\n" +
+	"diagnostic\x88\x01\x01B\r\n" +
+	"\v_diagnosticB:Z8github.com/opencost/opencost/core/pkg/model/pb/kubemodelb\x06proto3"
+
+var (
+	file_kubemodel_gpu_proto_rawDescOnce sync.Once
+	file_kubemodel_gpu_proto_rawDescData []byte
+)
+
+func file_kubemodel_gpu_proto_rawDescGZIP() []byte {
+	file_kubemodel_gpu_proto_rawDescOnce.Do(func() {
+		file_kubemodel_gpu_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kubemodel_gpu_proto_rawDesc), len(file_kubemodel_gpu_proto_rawDesc)))
+	})
+	return file_kubemodel_gpu_proto_rawDescData
+}
+
+var file_kubemodel_gpu_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_kubemodel_gpu_proto_goTypes = []any{
+	(*GPUDevice)(nil),        // 0: kubemodel.GPUDevice
+	(*GPUUsage)(nil),         // 1: kubemodel.GPUUsage
+	(*DiagnosticResult)(nil), // 2: kubemodel.DiagnosticResult
+}
+var file_kubemodel_gpu_proto_depIdxs = []int32{
+	2, // 0: kubemodel.GPUDevice.diagnostic:type_name -> kubemodel.DiagnosticResult
+	2, // 1: kubemodel.GPUUsage.diagnostic:type_name -> kubemodel.DiagnosticResult
+	2, // [2:2] is the sub-list for method output_type
+	2, // [2:2] is the sub-list for method input_type
+	2, // [2:2] is the sub-list for extension type_name
+	2, // [2:2] is the sub-list for extension extendee
+	0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_kubemodel_gpu_proto_init() }
+func file_kubemodel_gpu_proto_init() {
+	if File_kubemodel_gpu_proto != nil {
+		return
+	}
+	file_kubemodel_diagnostic_proto_init()
+	file_kubemodel_gpu_proto_msgTypes[0].OneofWrappers = []any{}
+	file_kubemodel_gpu_proto_msgTypes[1].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_kubemodel_gpu_proto_rawDesc), len(file_kubemodel_gpu_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_kubemodel_gpu_proto_goTypes,
+		DependencyIndexes: file_kubemodel_gpu_proto_depIdxs,
+		MessageInfos:      file_kubemodel_gpu_proto_msgTypes,
+	}.Build()
+	File_kubemodel_gpu_proto = out.File
+	file_kubemodel_gpu_proto_goTypes = nil
+	file_kubemodel_gpu_proto_depIdxs = nil
+}

+ 213 - 0
core/pkg/model/pb/kubemodel/namespace.pb.go

@@ -0,0 +1,213 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.9
+// 	protoc        v6.32.1
+// source: kubemodel/namespace.proto
+
+package kubemodel
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Namespace represents a Kubernetes namespace (allocated resource grouping)
+type Namespace struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Identification
+	ID        string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
+	ClusterID string `protobuf:"bytes,2,opt,name=clusterID,proto3" json:"clusterID,omitempty"`
+	// Properties
+	Name        string            `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+	Labels      map[string]string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	Annotations map[string]string `protobuf:"bytes,5,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	// Resource lifecycle (only when different from cluster window)
+	CreationTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=creationTime,proto3,oneof" json:"creationTime,omitempty"`
+	DeletionTime *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=deletionTime,proto3,oneof" json:"deletionTime,omitempty"`
+	// Diagnostic information about this resource
+	Diagnostic    *DiagnosticResult `protobuf:"bytes,99,opt,name=diagnostic,proto3,oneof" json:"diagnostic,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Namespace) Reset() {
+	*x = Namespace{}
+	mi := &file_kubemodel_namespace_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Namespace) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Namespace) ProtoMessage() {}
+
+func (x *Namespace) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_namespace_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Namespace.ProtoReflect.Descriptor instead.
+func (*Namespace) Descriptor() ([]byte, []int) {
+	return file_kubemodel_namespace_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Namespace) GetID() string {
+	if x != nil {
+		return x.ID
+	}
+	return ""
+}
+
+func (x *Namespace) GetClusterID() string {
+	if x != nil {
+		return x.ClusterID
+	}
+	return ""
+}
+
+func (x *Namespace) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *Namespace) GetLabels() map[string]string {
+	if x != nil {
+		return x.Labels
+	}
+	return nil
+}
+
+func (x *Namespace) GetAnnotations() map[string]string {
+	if x != nil {
+		return x.Annotations
+	}
+	return nil
+}
+
+func (x *Namespace) GetCreationTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.CreationTime
+	}
+	return nil
+}
+
+func (x *Namespace) GetDeletionTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.DeletionTime
+	}
+	return nil
+}
+
+func (x *Namespace) GetDiagnostic() *DiagnosticResult {
+	if x != nil {
+		return x.Diagnostic
+	}
+	return nil
+}
+
+var File_kubemodel_namespace_proto protoreflect.FileDescriptor
+
+const file_kubemodel_namespace_proto_rawDesc = "" +
+	"\n" +
+	"\x19kubemodel/namespace.proto\x12\tkubemodel\x1a\x1akubemodel/diagnostic.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc8\x04\n" +
+	"\tNamespace\x12\x0e\n" +
+	"\x02ID\x18\x01 \x01(\tR\x02ID\x12\x1c\n" +
+	"\tclusterID\x18\x02 \x01(\tR\tclusterID\x12\x12\n" +
+	"\x04name\x18\x03 \x01(\tR\x04name\x128\n" +
+	"\x06labels\x18\x04 \x03(\v2 .kubemodel.Namespace.LabelsEntryR\x06labels\x12G\n" +
+	"\vannotations\x18\x05 \x03(\v2%.kubemodel.Namespace.AnnotationsEntryR\vannotations\x12C\n" +
+	"\fcreationTime\x18\a \x01(\v2\x1a.google.protobuf.TimestampH\x00R\fcreationTime\x88\x01\x01\x12C\n" +
+	"\fdeletionTime\x18\b \x01(\v2\x1a.google.protobuf.TimestampH\x01R\fdeletionTime\x88\x01\x01\x12@\n" +
+	"\n" +
+	"diagnostic\x18c \x01(\v2\x1b.kubemodel.DiagnosticResultH\x02R\n" +
+	"diagnostic\x88\x01\x01\x1a9\n" +
+	"\vLabelsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" +
+	"\x10AnnotationsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x0f\n" +
+	"\r_creationTimeB\x0f\n" +
+	"\r_deletionTimeB\r\n" +
+	"\v_diagnosticB:Z8github.com/opencost/opencost/core/pkg/model/pb/kubemodelb\x06proto3"
+
+var (
+	file_kubemodel_namespace_proto_rawDescOnce sync.Once
+	file_kubemodel_namespace_proto_rawDescData []byte
+)
+
+func file_kubemodel_namespace_proto_rawDescGZIP() []byte {
+	file_kubemodel_namespace_proto_rawDescOnce.Do(func() {
+		file_kubemodel_namespace_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kubemodel_namespace_proto_rawDesc), len(file_kubemodel_namespace_proto_rawDesc)))
+	})
+	return file_kubemodel_namespace_proto_rawDescData
+}
+
+var file_kubemodel_namespace_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
+var file_kubemodel_namespace_proto_goTypes = []any{
+	(*Namespace)(nil),             // 0: kubemodel.Namespace
+	nil,                           // 1: kubemodel.Namespace.LabelsEntry
+	nil,                           // 2: kubemodel.Namespace.AnnotationsEntry
+	(*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp
+	(*DiagnosticResult)(nil),      // 4: kubemodel.DiagnosticResult
+}
+var file_kubemodel_namespace_proto_depIdxs = []int32{
+	1, // 0: kubemodel.Namespace.labels:type_name -> kubemodel.Namespace.LabelsEntry
+	2, // 1: kubemodel.Namespace.annotations:type_name -> kubemodel.Namespace.AnnotationsEntry
+	3, // 2: kubemodel.Namespace.creationTime:type_name -> google.protobuf.Timestamp
+	3, // 3: kubemodel.Namespace.deletionTime:type_name -> google.protobuf.Timestamp
+	4, // 4: kubemodel.Namespace.diagnostic:type_name -> kubemodel.DiagnosticResult
+	5, // [5:5] is the sub-list for method output_type
+	5, // [5:5] is the sub-list for method input_type
+	5, // [5:5] is the sub-list for extension type_name
+	5, // [5:5] is the sub-list for extension extendee
+	0, // [0:5] is the sub-list for field type_name
+}
+
+func init() { file_kubemodel_namespace_proto_init() }
+func file_kubemodel_namespace_proto_init() {
+	if File_kubemodel_namespace_proto != nil {
+		return
+	}
+	file_kubemodel_diagnostic_proto_init()
+	file_kubemodel_namespace_proto_msgTypes[0].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_kubemodel_namespace_proto_rawDesc), len(file_kubemodel_namespace_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   3,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_kubemodel_namespace_proto_goTypes,
+		DependencyIndexes: file_kubemodel_namespace_proto_depIdxs,
+		MessageInfos:      file_kubemodel_namespace_proto_msgTypes,
+	}.Build()
+	File_kubemodel_namespace_proto = out.File
+	file_kubemodel_namespace_proto_goTypes = nil
+	file_kubemodel_namespace_proto_depIdxs = nil
+}

+ 340 - 0
core/pkg/model/pb/kubemodel/network.pb.go

@@ -0,0 +1,340 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.9
+// 	protoc        v6.32.1
+// source: kubemodel/network.proto
+
+package kubemodel
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// ServicePort represents a port exposed by a service
+type ServicePort struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Name          string                 `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Protocol      string                 `protobuf:"bytes,2,opt,name=protocol,proto3" json:"protocol,omitempty"`
+	Port          int32                  `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"`
+	TargetPort    int32                  `protobuf:"varint,4,opt,name=targetPort,proto3" json:"targetPort,omitempty"`
+	NodePort      int32                  `protobuf:"varint,5,opt,name=nodePort,proto3" json:"nodePort,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ServicePort) Reset() {
+	*x = ServicePort{}
+	mi := &file_kubemodel_network_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ServicePort) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ServicePort) ProtoMessage() {}
+
+func (x *ServicePort) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_network_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ServicePort.ProtoReflect.Descriptor instead.
+func (*ServicePort) Descriptor() ([]byte, []int) {
+	return file_kubemodel_network_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *ServicePort) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *ServicePort) GetProtocol() string {
+	if x != nil {
+		return x.Protocol
+	}
+	return ""
+}
+
+func (x *ServicePort) GetPort() int32 {
+	if x != nil {
+		return x.Port
+	}
+	return 0
+}
+
+func (x *ServicePort) GetTargetPort() int32 {
+	if x != nil {
+		return x.TargetPort
+	}
+	return 0
+}
+
+func (x *ServicePort) GetNodePort() int32 {
+	if x != nil {
+		return x.NodePort
+	}
+	return 0
+}
+
+// Service represents a K8s Service (allocated resource)
+type Service struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Identification
+	ID        string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
+	ClusterID string `protobuf:"bytes,2,opt,name=clusterID,proto3" json:"clusterID,omitempty"`
+	// Properties
+	Name        string            `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+	ServiceType string            `protobuf:"bytes,4,opt,name=serviceType,proto3" json:"serviceType,omitempty"`
+	Ports       []*ServicePort    `protobuf:"bytes,5,rep,name=ports,proto3" json:"ports,omitempty"`
+	Labels      map[string]string `protobuf:"bytes,6,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	Annotations map[string]string `protobuf:"bytes,7,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	// Resource lifecycle (only when different from cluster window)
+	CreationTime *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=creationTime,proto3,oneof" json:"creationTime,omitempty"`
+	DeletionTime *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=deletionTime,proto3,oneof" json:"deletionTime,omitempty"`
+	// Usage metrics
+	// Network transfer bytes sent through this service
+	NetworkTransferBytes int64 `protobuf:"varint,10,opt,name=networkTransferBytes,proto3" json:"networkTransferBytes,omitempty"`
+	// Network bytes received through this service
+	NetworkReceiveBytes int64 `protobuf:"varint,11,opt,name=networkReceiveBytes,proto3" json:"networkReceiveBytes,omitempty"`
+	// Diagnostic information about this resource
+	Diagnostic    *DiagnosticResult `protobuf:"bytes,99,opt,name=diagnostic,proto3,oneof" json:"diagnostic,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Service) Reset() {
+	*x = Service{}
+	mi := &file_kubemodel_network_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Service) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Service) ProtoMessage() {}
+
+func (x *Service) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_network_proto_msgTypes[1]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Service.ProtoReflect.Descriptor instead.
+func (*Service) Descriptor() ([]byte, []int) {
+	return file_kubemodel_network_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *Service) GetID() string {
+	if x != nil {
+		return x.ID
+	}
+	return ""
+}
+
+func (x *Service) GetClusterID() string {
+	if x != nil {
+		return x.ClusterID
+	}
+	return ""
+}
+
+func (x *Service) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *Service) GetServiceType() string {
+	if x != nil {
+		return x.ServiceType
+	}
+	return ""
+}
+
+func (x *Service) GetPorts() []*ServicePort {
+	if x != nil {
+		return x.Ports
+	}
+	return nil
+}
+
+func (x *Service) GetLabels() map[string]string {
+	if x != nil {
+		return x.Labels
+	}
+	return nil
+}
+
+func (x *Service) GetAnnotations() map[string]string {
+	if x != nil {
+		return x.Annotations
+	}
+	return nil
+}
+
+func (x *Service) GetCreationTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.CreationTime
+	}
+	return nil
+}
+
+func (x *Service) GetDeletionTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.DeletionTime
+	}
+	return nil
+}
+
+func (x *Service) GetNetworkTransferBytes() int64 {
+	if x != nil {
+		return x.NetworkTransferBytes
+	}
+	return 0
+}
+
+func (x *Service) GetNetworkReceiveBytes() int64 {
+	if x != nil {
+		return x.NetworkReceiveBytes
+	}
+	return 0
+}
+
+func (x *Service) GetDiagnostic() *DiagnosticResult {
+	if x != nil {
+		return x.Diagnostic
+	}
+	return nil
+}
+
+var File_kubemodel_network_proto protoreflect.FileDescriptor
+
+const file_kubemodel_network_proto_rawDesc = "" +
+	"\n" +
+	"\x17kubemodel/network.proto\x12\tkubemodel\x1a\x1akubemodel/diagnostic.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x8d\x01\n" +
+	"\vServicePort\x12\x12\n" +
+	"\x04name\x18\x01 \x01(\tR\x04name\x12\x1a\n" +
+	"\bprotocol\x18\x02 \x01(\tR\bprotocol\x12\x12\n" +
+	"\x04port\x18\x03 \x01(\x05R\x04port\x12\x1e\n" +
+	"\n" +
+	"targetPort\x18\x04 \x01(\x05R\n" +
+	"targetPort\x12\x1a\n" +
+	"\bnodePort\x18\x05 \x01(\x05R\bnodePort\"\xf8\x05\n" +
+	"\aService\x12\x0e\n" +
+	"\x02ID\x18\x01 \x01(\tR\x02ID\x12\x1c\n" +
+	"\tclusterID\x18\x02 \x01(\tR\tclusterID\x12\x12\n" +
+	"\x04name\x18\x03 \x01(\tR\x04name\x12 \n" +
+	"\vserviceType\x18\x04 \x01(\tR\vserviceType\x12,\n" +
+	"\x05ports\x18\x05 \x03(\v2\x16.kubemodel.ServicePortR\x05ports\x126\n" +
+	"\x06labels\x18\x06 \x03(\v2\x1e.kubemodel.Service.LabelsEntryR\x06labels\x12E\n" +
+	"\vannotations\x18\a \x03(\v2#.kubemodel.Service.AnnotationsEntryR\vannotations\x12C\n" +
+	"\fcreationTime\x18\b \x01(\v2\x1a.google.protobuf.TimestampH\x00R\fcreationTime\x88\x01\x01\x12C\n" +
+	"\fdeletionTime\x18\t \x01(\v2\x1a.google.protobuf.TimestampH\x01R\fdeletionTime\x88\x01\x01\x122\n" +
+	"\x14networkTransferBytes\x18\n" +
+	" \x01(\x03R\x14networkTransferBytes\x120\n" +
+	"\x13networkReceiveBytes\x18\v \x01(\x03R\x13networkReceiveBytes\x12@\n" +
+	"\n" +
+	"diagnostic\x18c \x01(\v2\x1b.kubemodel.DiagnosticResultH\x02R\n" +
+	"diagnostic\x88\x01\x01\x1a9\n" +
+	"\vLabelsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" +
+	"\x10AnnotationsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x0f\n" +
+	"\r_creationTimeB\x0f\n" +
+	"\r_deletionTimeB\r\n" +
+	"\v_diagnosticB:Z8github.com/opencost/opencost/core/pkg/model/pb/kubemodelb\x06proto3"
+
+var (
+	file_kubemodel_network_proto_rawDescOnce sync.Once
+	file_kubemodel_network_proto_rawDescData []byte
+)
+
+func file_kubemodel_network_proto_rawDescGZIP() []byte {
+	file_kubemodel_network_proto_rawDescOnce.Do(func() {
+		file_kubemodel_network_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kubemodel_network_proto_rawDesc), len(file_kubemodel_network_proto_rawDesc)))
+	})
+	return file_kubemodel_network_proto_rawDescData
+}
+
+var file_kubemodel_network_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_kubemodel_network_proto_goTypes = []any{
+	(*ServicePort)(nil),           // 0: kubemodel.ServicePort
+	(*Service)(nil),               // 1: kubemodel.Service
+	nil,                           // 2: kubemodel.Service.LabelsEntry
+	nil,                           // 3: kubemodel.Service.AnnotationsEntry
+	(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp
+	(*DiagnosticResult)(nil),      // 5: kubemodel.DiagnosticResult
+}
+var file_kubemodel_network_proto_depIdxs = []int32{
+	0, // 0: kubemodel.Service.ports:type_name -> kubemodel.ServicePort
+	2, // 1: kubemodel.Service.labels:type_name -> kubemodel.Service.LabelsEntry
+	3, // 2: kubemodel.Service.annotations:type_name -> kubemodel.Service.AnnotationsEntry
+	4, // 3: kubemodel.Service.creationTime:type_name -> google.protobuf.Timestamp
+	4, // 4: kubemodel.Service.deletionTime:type_name -> google.protobuf.Timestamp
+	5, // 5: kubemodel.Service.diagnostic:type_name -> kubemodel.DiagnosticResult
+	6, // [6:6] is the sub-list for method output_type
+	6, // [6:6] is the sub-list for method input_type
+	6, // [6:6] is the sub-list for extension type_name
+	6, // [6:6] is the sub-list for extension extendee
+	0, // [0:6] is the sub-list for field type_name
+}
+
+func init() { file_kubemodel_network_proto_init() }
+func file_kubemodel_network_proto_init() {
+	if File_kubemodel_network_proto != nil {
+		return
+	}
+	file_kubemodel_diagnostic_proto_init()
+	file_kubemodel_network_proto_msgTypes[1].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_kubemodel_network_proto_rawDesc), len(file_kubemodel_network_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   4,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_kubemodel_network_proto_goTypes,
+		DependencyIndexes: file_kubemodel_network_proto_depIdxs,
+		MessageInfos:      file_kubemodel_network_proto_msgTypes,
+	}.Build()
+	File_kubemodel_network_proto = out.File
+	file_kubemodel_network_proto_goTypes = nil
+	file_kubemodel_network_proto_depIdxs = nil
+}

+ 222 - 0
core/pkg/model/pb/kubemodel/node.pb.go

@@ -0,0 +1,222 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.9
+// 	protoc        v6.32.1
+// source: kubemodel/node.proto
+
+package kubemodel
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Node represents a Kubernetes worker node (provisioned resource)
+type Node struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Identification
+	ID        string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
+	ClusterID string `protobuf:"bytes,2,opt,name=clusterID,proto3" json:"clusterID,omitempty"`
+	// Properties
+	ProviderResourceID string            `protobuf:"bytes,3,opt,name=providerResourceID,proto3" json:"providerResourceID,omitempty"`
+	Name               string            `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
+	Labels             map[string]string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	Annotations        map[string]string `protobuf:"bytes,6,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	// Resource lifecycle (only when different from cluster window)
+	CreationTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=creationTime,proto3,oneof" json:"creationTime,omitempty"`
+	DeletionTime *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=deletionTime,proto3,oneof" json:"deletionTime,omitempty"`
+	// Diagnostic information about this resource
+	Diagnostic    *DiagnosticResult `protobuf:"bytes,99,opt,name=diagnostic,proto3,oneof" json:"diagnostic,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Node) Reset() {
+	*x = Node{}
+	mi := &file_kubemodel_node_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Node) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Node) ProtoMessage() {}
+
+func (x *Node) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_node_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Node.ProtoReflect.Descriptor instead.
+func (*Node) Descriptor() ([]byte, []int) {
+	return file_kubemodel_node_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Node) GetID() string {
+	if x != nil {
+		return x.ID
+	}
+	return ""
+}
+
+func (x *Node) GetClusterID() string {
+	if x != nil {
+		return x.ClusterID
+	}
+	return ""
+}
+
+func (x *Node) GetProviderResourceID() string {
+	if x != nil {
+		return x.ProviderResourceID
+	}
+	return ""
+}
+
+func (x *Node) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *Node) GetLabels() map[string]string {
+	if x != nil {
+		return x.Labels
+	}
+	return nil
+}
+
+func (x *Node) GetAnnotations() map[string]string {
+	if x != nil {
+		return x.Annotations
+	}
+	return nil
+}
+
+func (x *Node) GetCreationTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.CreationTime
+	}
+	return nil
+}
+
+func (x *Node) GetDeletionTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.DeletionTime
+	}
+	return nil
+}
+
+func (x *Node) GetDiagnostic() *DiagnosticResult {
+	if x != nil {
+		return x.Diagnostic
+	}
+	return nil
+}
+
+var File_kubemodel_node_proto protoreflect.FileDescriptor
+
+const file_kubemodel_node_proto_rawDesc = "" +
+	"\n" +
+	"\x14kubemodel/node.proto\x12\tkubemodel\x1a\x1akubemodel/diagnostic.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe9\x04\n" +
+	"\x04Node\x12\x0e\n" +
+	"\x02ID\x18\x01 \x01(\tR\x02ID\x12\x1c\n" +
+	"\tclusterID\x18\x02 \x01(\tR\tclusterID\x12.\n" +
+	"\x12providerResourceID\x18\x03 \x01(\tR\x12providerResourceID\x12\x12\n" +
+	"\x04name\x18\x04 \x01(\tR\x04name\x123\n" +
+	"\x06labels\x18\x05 \x03(\v2\x1b.kubemodel.Node.LabelsEntryR\x06labels\x12B\n" +
+	"\vannotations\x18\x06 \x03(\v2 .kubemodel.Node.AnnotationsEntryR\vannotations\x12C\n" +
+	"\fcreationTime\x18\a \x01(\v2\x1a.google.protobuf.TimestampH\x00R\fcreationTime\x88\x01\x01\x12C\n" +
+	"\fdeletionTime\x18\b \x01(\v2\x1a.google.protobuf.TimestampH\x01R\fdeletionTime\x88\x01\x01\x12@\n" +
+	"\n" +
+	"diagnostic\x18c \x01(\v2\x1b.kubemodel.DiagnosticResultH\x02R\n" +
+	"diagnostic\x88\x01\x01\x1a9\n" +
+	"\vLabelsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" +
+	"\x10AnnotationsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x0f\n" +
+	"\r_creationTimeB\x0f\n" +
+	"\r_deletionTimeB\r\n" +
+	"\v_diagnosticB:Z8github.com/opencost/opencost/core/pkg/model/pb/kubemodelb\x06proto3"
+
+var (
+	file_kubemodel_node_proto_rawDescOnce sync.Once
+	file_kubemodel_node_proto_rawDescData []byte
+)
+
+func file_kubemodel_node_proto_rawDescGZIP() []byte {
+	file_kubemodel_node_proto_rawDescOnce.Do(func() {
+		file_kubemodel_node_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kubemodel_node_proto_rawDesc), len(file_kubemodel_node_proto_rawDesc)))
+	})
+	return file_kubemodel_node_proto_rawDescData
+}
+
+var file_kubemodel_node_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
+var file_kubemodel_node_proto_goTypes = []any{
+	(*Node)(nil),                  // 0: kubemodel.Node
+	nil,                           // 1: kubemodel.Node.LabelsEntry
+	nil,                           // 2: kubemodel.Node.AnnotationsEntry
+	(*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp
+	(*DiagnosticResult)(nil),      // 4: kubemodel.DiagnosticResult
+}
+var file_kubemodel_node_proto_depIdxs = []int32{
+	1, // 0: kubemodel.Node.labels:type_name -> kubemodel.Node.LabelsEntry
+	2, // 1: kubemodel.Node.annotations:type_name -> kubemodel.Node.AnnotationsEntry
+	3, // 2: kubemodel.Node.creationTime:type_name -> google.protobuf.Timestamp
+	3, // 3: kubemodel.Node.deletionTime:type_name -> google.protobuf.Timestamp
+	4, // 4: kubemodel.Node.diagnostic:type_name -> kubemodel.DiagnosticResult
+	5, // [5:5] is the sub-list for method output_type
+	5, // [5:5] is the sub-list for method input_type
+	5, // [5:5] is the sub-list for extension type_name
+	5, // [5:5] is the sub-list for extension extendee
+	0, // [0:5] is the sub-list for field type_name
+}
+
+func init() { file_kubemodel_node_proto_init() }
+func file_kubemodel_node_proto_init() {
+	if File_kubemodel_node_proto != nil {
+		return
+	}
+	file_kubemodel_diagnostic_proto_init()
+	file_kubemodel_node_proto_msgTypes[0].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_kubemodel_node_proto_rawDesc), len(file_kubemodel_node_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   3,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_kubemodel_node_proto_goTypes,
+		DependencyIndexes: file_kubemodel_node_proto_depIdxs,
+		MessageInfos:      file_kubemodel_node_proto_msgTypes,
+	}.Build()
+	File_kubemodel_node_proto = out.File
+	file_kubemodel_node_proto_goTypes = nil
+	file_kubemodel_node_proto_depIdxs = nil
+}

+ 343 - 0
core/pkg/model/pb/kubemodel/pod.pb.go

@@ -0,0 +1,343 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.9
+// 	protoc        v6.32.1
+// source: kubemodel/pod.proto
+
+package kubemodel
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Pod represents a Kubernetes pod (allocated resource grouping)
+type Pod struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Identification
+	ID           string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
+	NamespaceID  string `protobuf:"bytes,2,opt,name=namespaceID,proto3" json:"namespaceID,omitempty"`
+	ControllerID string `protobuf:"bytes,3,opt,name=controllerID,proto3" json:"controllerID,omitempty"`
+	NodeID       string `protobuf:"bytes,4,opt,name=nodeID,proto3" json:"nodeID,omitempty"`
+	// Properties
+	Name        string            `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"`
+	Labels      map[string]string `protobuf:"bytes,6,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	Annotations map[string]string `protobuf:"bytes,7,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	// Resource lifecycle (only when different from cluster window)
+	CreationTime *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=creationTime,proto3,oneof" json:"creationTime,omitempty"`
+	DeletionTime *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=deletionTime,proto3,oneof" json:"deletionTime,omitempty"`
+	// Usage metrics
+	// CPU usage in core-hours
+	CpuCoreHours float32 `protobuf:"fixed32,10,opt,name=cpuCoreHours,proto3" json:"cpuCoreHours,omitempty"`
+	// CPU request average in cores
+	CpuCoreRequestAverage float32 `protobuf:"fixed32,11,opt,name=cpuCoreRequestAverage,proto3" json:"cpuCoreRequestAverage,omitempty"`
+	// CPU usage average in cores
+	CpuCoreUsageAverage float32 `protobuf:"fixed32,12,opt,name=cpuCoreUsageAverage,proto3" json:"cpuCoreUsageAverage,omitempty"`
+	// CPU usage max in cores
+	CpuCoreUsageMax float32 `protobuf:"fixed32,13,opt,name=cpuCoreUsageMax,proto3" json:"cpuCoreUsageMax,omitempty"`
+	// RAM usage in byte-hours
+	RamByteHours int64 `protobuf:"varint,14,opt,name=ramByteHours,proto3" json:"ramByteHours,omitempty"`
+	// RAM request average in bytes
+	RamBytesRequestAverage int64 `protobuf:"varint,15,opt,name=ramBytesRequestAverage,proto3" json:"ramBytesRequestAverage,omitempty"`
+	// RAM usage average in bytes
+	RamBytesUsageAverage int64 `protobuf:"varint,16,opt,name=ramBytesUsageAverage,proto3" json:"ramBytesUsageAverage,omitempty"`
+	// RAM usage max in bytes
+	RamBytesUsageMax int64 `protobuf:"varint,17,opt,name=ramBytesUsageMax,proto3" json:"ramBytesUsageMax,omitempty"`
+	// Storage usage in byte-hours
+	StorageByteHours int64 `protobuf:"varint,18,opt,name=storageByteHours,proto3" json:"storageByteHours,omitempty"`
+	// Network transfer bytes sent
+	NetworkTransferBytes int64 `protobuf:"varint,19,opt,name=networkTransferBytes,proto3" json:"networkTransferBytes,omitempty"`
+	// Network bytes received
+	NetworkReceiveBytes int64 `protobuf:"varint,20,opt,name=networkReceiveBytes,proto3" json:"networkReceiveBytes,omitempty"`
+	// Diagnostic information about this resource
+	Diagnostic    *DiagnosticResult `protobuf:"bytes,99,opt,name=diagnostic,proto3,oneof" json:"diagnostic,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Pod) Reset() {
+	*x = Pod{}
+	mi := &file_kubemodel_pod_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Pod) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Pod) ProtoMessage() {}
+
+func (x *Pod) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_pod_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Pod.ProtoReflect.Descriptor instead.
+func (*Pod) Descriptor() ([]byte, []int) {
+	return file_kubemodel_pod_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Pod) GetID() string {
+	if x != nil {
+		return x.ID
+	}
+	return ""
+}
+
+func (x *Pod) GetNamespaceID() string {
+	if x != nil {
+		return x.NamespaceID
+	}
+	return ""
+}
+
+func (x *Pod) GetControllerID() string {
+	if x != nil {
+		return x.ControllerID
+	}
+	return ""
+}
+
+func (x *Pod) GetNodeID() string {
+	if x != nil {
+		return x.NodeID
+	}
+	return ""
+}
+
+func (x *Pod) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *Pod) GetLabels() map[string]string {
+	if x != nil {
+		return x.Labels
+	}
+	return nil
+}
+
+func (x *Pod) GetAnnotations() map[string]string {
+	if x != nil {
+		return x.Annotations
+	}
+	return nil
+}
+
+func (x *Pod) GetCreationTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.CreationTime
+	}
+	return nil
+}
+
+func (x *Pod) GetDeletionTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.DeletionTime
+	}
+	return nil
+}
+
+func (x *Pod) GetCpuCoreHours() float32 {
+	if x != nil {
+		return x.CpuCoreHours
+	}
+	return 0
+}
+
+func (x *Pod) GetCpuCoreRequestAverage() float32 {
+	if x != nil {
+		return x.CpuCoreRequestAverage
+	}
+	return 0
+}
+
+func (x *Pod) GetCpuCoreUsageAverage() float32 {
+	if x != nil {
+		return x.CpuCoreUsageAverage
+	}
+	return 0
+}
+
+func (x *Pod) GetCpuCoreUsageMax() float32 {
+	if x != nil {
+		return x.CpuCoreUsageMax
+	}
+	return 0
+}
+
+func (x *Pod) GetRamByteHours() int64 {
+	if x != nil {
+		return x.RamByteHours
+	}
+	return 0
+}
+
+func (x *Pod) GetRamBytesRequestAverage() int64 {
+	if x != nil {
+		return x.RamBytesRequestAverage
+	}
+	return 0
+}
+
+func (x *Pod) GetRamBytesUsageAverage() int64 {
+	if x != nil {
+		return x.RamBytesUsageAverage
+	}
+	return 0
+}
+
+func (x *Pod) GetRamBytesUsageMax() int64 {
+	if x != nil {
+		return x.RamBytesUsageMax
+	}
+	return 0
+}
+
+func (x *Pod) GetStorageByteHours() int64 {
+	if x != nil {
+		return x.StorageByteHours
+	}
+	return 0
+}
+
+func (x *Pod) GetNetworkTransferBytes() int64 {
+	if x != nil {
+		return x.NetworkTransferBytes
+	}
+	return 0
+}
+
+func (x *Pod) GetNetworkReceiveBytes() int64 {
+	if x != nil {
+		return x.NetworkReceiveBytes
+	}
+	return 0
+}
+
+func (x *Pod) GetDiagnostic() *DiagnosticResult {
+	if x != nil {
+		return x.Diagnostic
+	}
+	return nil
+}
+
+var File_kubemodel_pod_proto protoreflect.FileDescriptor
+
+const file_kubemodel_pod_proto_rawDesc = "" +
+	"\n" +
+	"\x13kubemodel/pod.proto\x12\tkubemodel\x1a\x1akubemodel/diagnostic.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xfa\b\n" +
+	"\x03Pod\x12\x0e\n" +
+	"\x02ID\x18\x01 \x01(\tR\x02ID\x12 \n" +
+	"\vnamespaceID\x18\x02 \x01(\tR\vnamespaceID\x12\"\n" +
+	"\fcontrollerID\x18\x03 \x01(\tR\fcontrollerID\x12\x16\n" +
+	"\x06nodeID\x18\x04 \x01(\tR\x06nodeID\x12\x12\n" +
+	"\x04name\x18\x05 \x01(\tR\x04name\x122\n" +
+	"\x06labels\x18\x06 \x03(\v2\x1a.kubemodel.Pod.LabelsEntryR\x06labels\x12A\n" +
+	"\vannotations\x18\a \x03(\v2\x1f.kubemodel.Pod.AnnotationsEntryR\vannotations\x12C\n" +
+	"\fcreationTime\x18\b \x01(\v2\x1a.google.protobuf.TimestampH\x00R\fcreationTime\x88\x01\x01\x12C\n" +
+	"\fdeletionTime\x18\t \x01(\v2\x1a.google.protobuf.TimestampH\x01R\fdeletionTime\x88\x01\x01\x12\"\n" +
+	"\fcpuCoreHours\x18\n" +
+	" \x01(\x02R\fcpuCoreHours\x124\n" +
+	"\x15cpuCoreRequestAverage\x18\v \x01(\x02R\x15cpuCoreRequestAverage\x120\n" +
+	"\x13cpuCoreUsageAverage\x18\f \x01(\x02R\x13cpuCoreUsageAverage\x12(\n" +
+	"\x0fcpuCoreUsageMax\x18\r \x01(\x02R\x0fcpuCoreUsageMax\x12\"\n" +
+	"\framByteHours\x18\x0e \x01(\x03R\framByteHours\x126\n" +
+	"\x16ramBytesRequestAverage\x18\x0f \x01(\x03R\x16ramBytesRequestAverage\x122\n" +
+	"\x14ramBytesUsageAverage\x18\x10 \x01(\x03R\x14ramBytesUsageAverage\x12*\n" +
+	"\x10ramBytesUsageMax\x18\x11 \x01(\x03R\x10ramBytesUsageMax\x12*\n" +
+	"\x10storageByteHours\x18\x12 \x01(\x03R\x10storageByteHours\x122\n" +
+	"\x14networkTransferBytes\x18\x13 \x01(\x03R\x14networkTransferBytes\x120\n" +
+	"\x13networkReceiveBytes\x18\x14 \x01(\x03R\x13networkReceiveBytes\x12@\n" +
+	"\n" +
+	"diagnostic\x18c \x01(\v2\x1b.kubemodel.DiagnosticResultH\x02R\n" +
+	"diagnostic\x88\x01\x01\x1a9\n" +
+	"\vLabelsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" +
+	"\x10AnnotationsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x0f\n" +
+	"\r_creationTimeB\x0f\n" +
+	"\r_deletionTimeB\r\n" +
+	"\v_diagnosticB:Z8github.com/opencost/opencost/core/pkg/model/pb/kubemodelb\x06proto3"
+
+var (
+	file_kubemodel_pod_proto_rawDescOnce sync.Once
+	file_kubemodel_pod_proto_rawDescData []byte
+)
+
+func file_kubemodel_pod_proto_rawDescGZIP() []byte {
+	file_kubemodel_pod_proto_rawDescOnce.Do(func() {
+		file_kubemodel_pod_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kubemodel_pod_proto_rawDesc), len(file_kubemodel_pod_proto_rawDesc)))
+	})
+	return file_kubemodel_pod_proto_rawDescData
+}
+
+var file_kubemodel_pod_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
+var file_kubemodel_pod_proto_goTypes = []any{
+	(*Pod)(nil),                   // 0: kubemodel.Pod
+	nil,                           // 1: kubemodel.Pod.LabelsEntry
+	nil,                           // 2: kubemodel.Pod.AnnotationsEntry
+	(*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp
+	(*DiagnosticResult)(nil),      // 4: kubemodel.DiagnosticResult
+}
+var file_kubemodel_pod_proto_depIdxs = []int32{
+	1, // 0: kubemodel.Pod.labels:type_name -> kubemodel.Pod.LabelsEntry
+	2, // 1: kubemodel.Pod.annotations:type_name -> kubemodel.Pod.AnnotationsEntry
+	3, // 2: kubemodel.Pod.creationTime:type_name -> google.protobuf.Timestamp
+	3, // 3: kubemodel.Pod.deletionTime:type_name -> google.protobuf.Timestamp
+	4, // 4: kubemodel.Pod.diagnostic:type_name -> kubemodel.DiagnosticResult
+	5, // [5:5] is the sub-list for method output_type
+	5, // [5:5] is the sub-list for method input_type
+	5, // [5:5] is the sub-list for extension type_name
+	5, // [5:5] is the sub-list for extension extendee
+	0, // [0:5] is the sub-list for field type_name
+}
+
+func init() { file_kubemodel_pod_proto_init() }
+func file_kubemodel_pod_proto_init() {
+	if File_kubemodel_pod_proto != nil {
+		return
+	}
+	file_kubemodel_diagnostic_proto_init()
+	file_kubemodel_pod_proto_msgTypes[0].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_kubemodel_pod_proto_rawDesc), len(file_kubemodel_pod_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   3,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_kubemodel_pod_proto_goTypes,
+		DependencyIndexes: file_kubemodel_pod_proto_depIdxs,
+		MessageInfos:      file_kubemodel_pod_proto_msgTypes,
+	}.Build()
+	File_kubemodel_pod_proto = out.File
+	file_kubemodel_pod_proto_goTypes = nil
+	file_kubemodel_pod_proto_depIdxs = nil
+}

+ 418 - 0
core/pkg/model/pb/kubemodel/storage.pb.go

@@ -0,0 +1,418 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.9
+// 	protoc        v6.32.1
+// source: kubemodel/storage.proto
+
+package kubemodel
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Volume represents a persistent volume (provisioned resource)
+type Volume struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Identification
+	ID        string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
+	ClusterID string `protobuf:"bytes,2,opt,name=clusterID,proto3" json:"clusterID,omitempty"`
+	// Properties
+	Name         string            `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+	StorageClass string            `protobuf:"bytes,4,opt,name=storageClass,proto3" json:"storageClass,omitempty"`
+	Labels       map[string]string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	Annotations  map[string]string `protobuf:"bytes,6,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	// Resource lifecycle (only when different from cluster window)
+	CreationTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=creationTime,proto3,oneof" json:"creationTime,omitempty"`
+	DeletionTime *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=deletionTime,proto3,oneof" json:"deletionTime,omitempty"`
+	// Usage metrics
+	// Storage capacity in bytes
+	CapacityBytes int64 `protobuf:"varint,9,opt,name=capacityBytes,proto3" json:"capacityBytes,omitempty"`
+	// Diagnostic information about this resource
+	Diagnostic    *DiagnosticResult `protobuf:"bytes,99,opt,name=diagnostic,proto3,oneof" json:"diagnostic,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Volume) Reset() {
+	*x = Volume{}
+	mi := &file_kubemodel_storage_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Volume) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Volume) ProtoMessage() {}
+
+func (x *Volume) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_storage_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Volume.ProtoReflect.Descriptor instead.
+func (*Volume) Descriptor() ([]byte, []int) {
+	return file_kubemodel_storage_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Volume) GetID() string {
+	if x != nil {
+		return x.ID
+	}
+	return ""
+}
+
+func (x *Volume) GetClusterID() string {
+	if x != nil {
+		return x.ClusterID
+	}
+	return ""
+}
+
+func (x *Volume) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *Volume) GetStorageClass() string {
+	if x != nil {
+		return x.StorageClass
+	}
+	return ""
+}
+
+func (x *Volume) GetLabels() map[string]string {
+	if x != nil {
+		return x.Labels
+	}
+	return nil
+}
+
+func (x *Volume) GetAnnotations() map[string]string {
+	if x != nil {
+		return x.Annotations
+	}
+	return nil
+}
+
+func (x *Volume) GetCreationTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.CreationTime
+	}
+	return nil
+}
+
+func (x *Volume) GetDeletionTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.DeletionTime
+	}
+	return nil
+}
+
+func (x *Volume) GetCapacityBytes() int64 {
+	if x != nil {
+		return x.CapacityBytes
+	}
+	return 0
+}
+
+func (x *Volume) GetDiagnostic() *DiagnosticResult {
+	if x != nil {
+		return x.Diagnostic
+	}
+	return nil
+}
+
+// PersistentVolumeClaim represents a PVC (allocated resource) that refers to a Volume
+type PersistentVolumeClaim struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Identification
+	ID          string  `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
+	NamespaceID string  `protobuf:"bytes,2,opt,name=namespaceID,proto3" json:"namespaceID,omitempty"`
+	VolumeID    *string `protobuf:"bytes,3,opt,name=volumeID,proto3,oneof" json:"volumeID,omitempty"`
+	PodID       *string `protobuf:"bytes,4,opt,name=podID,proto3,oneof" json:"podID,omitempty"`
+	// Properties
+	Name         string            `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"`
+	StorageClass string            `protobuf:"bytes,6,opt,name=storageClass,proto3" json:"storageClass,omitempty"`
+	Labels       map[string]string `protobuf:"bytes,7,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	Annotations  map[string]string `protobuf:"bytes,8,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	// Resource lifecycle (only when different from cluster window)
+	CreationTime *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=creationTime,proto3,oneof" json:"creationTime,omitempty"`
+	DeletionTime *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=deletionTime,proto3,oneof" json:"deletionTime,omitempty"`
+	// Usage metrics
+	// Storage usage in byte-hours
+	StorageByteHours int64 `protobuf:"varint,11,opt,name=storageByteHours,proto3" json:"storageByteHours,omitempty"`
+	// Requested storage capacity in bytes
+	RequestedBytes int64 `protobuf:"varint,12,opt,name=requestedBytes,proto3" json:"requestedBytes,omitempty"`
+	// Diagnostic information about this resource
+	Diagnostic    *DiagnosticResult `protobuf:"bytes,99,opt,name=diagnostic,proto3,oneof" json:"diagnostic,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *PersistentVolumeClaim) Reset() {
+	*x = PersistentVolumeClaim{}
+	mi := &file_kubemodel_storage_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *PersistentVolumeClaim) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PersistentVolumeClaim) ProtoMessage() {}
+
+func (x *PersistentVolumeClaim) ProtoReflect() protoreflect.Message {
+	mi := &file_kubemodel_storage_proto_msgTypes[1]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PersistentVolumeClaim.ProtoReflect.Descriptor instead.
+func (*PersistentVolumeClaim) Descriptor() ([]byte, []int) {
+	return file_kubemodel_storage_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *PersistentVolumeClaim) GetID() string {
+	if x != nil {
+		return x.ID
+	}
+	return ""
+}
+
+func (x *PersistentVolumeClaim) GetNamespaceID() string {
+	if x != nil {
+		return x.NamespaceID
+	}
+	return ""
+}
+
+func (x *PersistentVolumeClaim) GetVolumeID() string {
+	if x != nil && x.VolumeID != nil {
+		return *x.VolumeID
+	}
+	return ""
+}
+
+func (x *PersistentVolumeClaim) GetPodID() string {
+	if x != nil && x.PodID != nil {
+		return *x.PodID
+	}
+	return ""
+}
+
+func (x *PersistentVolumeClaim) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *PersistentVolumeClaim) GetStorageClass() string {
+	if x != nil {
+		return x.StorageClass
+	}
+	return ""
+}
+
+func (x *PersistentVolumeClaim) GetLabels() map[string]string {
+	if x != nil {
+		return x.Labels
+	}
+	return nil
+}
+
+func (x *PersistentVolumeClaim) GetAnnotations() map[string]string {
+	if x != nil {
+		return x.Annotations
+	}
+	return nil
+}
+
+func (x *PersistentVolumeClaim) GetCreationTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.CreationTime
+	}
+	return nil
+}
+
+func (x *PersistentVolumeClaim) GetDeletionTime() *timestamppb.Timestamp {
+	if x != nil {
+		return x.DeletionTime
+	}
+	return nil
+}
+
+func (x *PersistentVolumeClaim) GetStorageByteHours() int64 {
+	if x != nil {
+		return x.StorageByteHours
+	}
+	return 0
+}
+
+func (x *PersistentVolumeClaim) GetRequestedBytes() int64 {
+	if x != nil {
+		return x.RequestedBytes
+	}
+	return 0
+}
+
+func (x *PersistentVolumeClaim) GetDiagnostic() *DiagnosticResult {
+	if x != nil {
+		return x.Diagnostic
+	}
+	return nil
+}
+
+var File_kubemodel_storage_proto protoreflect.FileDescriptor
+
+const file_kubemodel_storage_proto_rawDesc = "" +
+	"\n" +
+	"\x17kubemodel/storage.proto\x12\tkubemodel\x1a\x1akubemodel/diagnostic.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x89\x05\n" +
+	"\x06Volume\x12\x0e\n" +
+	"\x02ID\x18\x01 \x01(\tR\x02ID\x12\x1c\n" +
+	"\tclusterID\x18\x02 \x01(\tR\tclusterID\x12\x12\n" +
+	"\x04name\x18\x03 \x01(\tR\x04name\x12\"\n" +
+	"\fstorageClass\x18\x04 \x01(\tR\fstorageClass\x125\n" +
+	"\x06labels\x18\x05 \x03(\v2\x1d.kubemodel.Volume.LabelsEntryR\x06labels\x12D\n" +
+	"\vannotations\x18\x06 \x03(\v2\".kubemodel.Volume.AnnotationsEntryR\vannotations\x12C\n" +
+	"\fcreationTime\x18\a \x01(\v2\x1a.google.protobuf.TimestampH\x00R\fcreationTime\x88\x01\x01\x12C\n" +
+	"\fdeletionTime\x18\b \x01(\v2\x1a.google.protobuf.TimestampH\x01R\fdeletionTime\x88\x01\x01\x12$\n" +
+	"\rcapacityBytes\x18\t \x01(\x03R\rcapacityBytes\x12@\n" +
+	"\n" +
+	"diagnostic\x18c \x01(\v2\x1b.kubemodel.DiagnosticResultH\x02R\n" +
+	"diagnostic\x88\x01\x01\x1a9\n" +
+	"\vLabelsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" +
+	"\x10AnnotationsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x0f\n" +
+	"\r_creationTimeB\x0f\n" +
+	"\r_deletionTimeB\r\n" +
+	"\v_diagnostic\"\xbb\x06\n" +
+	"\x15PersistentVolumeClaim\x12\x0e\n" +
+	"\x02ID\x18\x01 \x01(\tR\x02ID\x12 \n" +
+	"\vnamespaceID\x18\x02 \x01(\tR\vnamespaceID\x12\x1f\n" +
+	"\bvolumeID\x18\x03 \x01(\tH\x00R\bvolumeID\x88\x01\x01\x12\x19\n" +
+	"\x05podID\x18\x04 \x01(\tH\x01R\x05podID\x88\x01\x01\x12\x12\n" +
+	"\x04name\x18\x05 \x01(\tR\x04name\x12\"\n" +
+	"\fstorageClass\x18\x06 \x01(\tR\fstorageClass\x12D\n" +
+	"\x06labels\x18\a \x03(\v2,.kubemodel.PersistentVolumeClaim.LabelsEntryR\x06labels\x12S\n" +
+	"\vannotations\x18\b \x03(\v21.kubemodel.PersistentVolumeClaim.AnnotationsEntryR\vannotations\x12C\n" +
+	"\fcreationTime\x18\t \x01(\v2\x1a.google.protobuf.TimestampH\x02R\fcreationTime\x88\x01\x01\x12C\n" +
+	"\fdeletionTime\x18\n" +
+	" \x01(\v2\x1a.google.protobuf.TimestampH\x03R\fdeletionTime\x88\x01\x01\x12*\n" +
+	"\x10storageByteHours\x18\v \x01(\x03R\x10storageByteHours\x12&\n" +
+	"\x0erequestedBytes\x18\f \x01(\x03R\x0erequestedBytes\x12@\n" +
+	"\n" +
+	"diagnostic\x18c \x01(\v2\x1b.kubemodel.DiagnosticResultH\x04R\n" +
+	"diagnostic\x88\x01\x01\x1a9\n" +
+	"\vLabelsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" +
+	"\x10AnnotationsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\v\n" +
+	"\t_volumeIDB\b\n" +
+	"\x06_podIDB\x0f\n" +
+	"\r_creationTimeB\x0f\n" +
+	"\r_deletionTimeB\r\n" +
+	"\v_diagnosticB:Z8github.com/opencost/opencost/core/pkg/model/pb/kubemodelb\x06proto3"
+
+var (
+	file_kubemodel_storage_proto_rawDescOnce sync.Once
+	file_kubemodel_storage_proto_rawDescData []byte
+)
+
+func file_kubemodel_storage_proto_rawDescGZIP() []byte {
+	file_kubemodel_storage_proto_rawDescOnce.Do(func() {
+		file_kubemodel_storage_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kubemodel_storage_proto_rawDesc), len(file_kubemodel_storage_proto_rawDesc)))
+	})
+	return file_kubemodel_storage_proto_rawDescData
+}
+
+var file_kubemodel_storage_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
+var file_kubemodel_storage_proto_goTypes = []any{
+	(*Volume)(nil),                // 0: kubemodel.Volume
+	(*PersistentVolumeClaim)(nil), // 1: kubemodel.PersistentVolumeClaim
+	nil,                           // 2: kubemodel.Volume.LabelsEntry
+	nil,                           // 3: kubemodel.Volume.AnnotationsEntry
+	nil,                           // 4: kubemodel.PersistentVolumeClaim.LabelsEntry
+	nil,                           // 5: kubemodel.PersistentVolumeClaim.AnnotationsEntry
+	(*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
+	(*DiagnosticResult)(nil),      // 7: kubemodel.DiagnosticResult
+}
+var file_kubemodel_storage_proto_depIdxs = []int32{
+	2,  // 0: kubemodel.Volume.labels:type_name -> kubemodel.Volume.LabelsEntry
+	3,  // 1: kubemodel.Volume.annotations:type_name -> kubemodel.Volume.AnnotationsEntry
+	6,  // 2: kubemodel.Volume.creationTime:type_name -> google.protobuf.Timestamp
+	6,  // 3: kubemodel.Volume.deletionTime:type_name -> google.protobuf.Timestamp
+	7,  // 4: kubemodel.Volume.diagnostic:type_name -> kubemodel.DiagnosticResult
+	4,  // 5: kubemodel.PersistentVolumeClaim.labels:type_name -> kubemodel.PersistentVolumeClaim.LabelsEntry
+	5,  // 6: kubemodel.PersistentVolumeClaim.annotations:type_name -> kubemodel.PersistentVolumeClaim.AnnotationsEntry
+	6,  // 7: kubemodel.PersistentVolumeClaim.creationTime:type_name -> google.protobuf.Timestamp
+	6,  // 8: kubemodel.PersistentVolumeClaim.deletionTime:type_name -> google.protobuf.Timestamp
+	7,  // 9: kubemodel.PersistentVolumeClaim.diagnostic:type_name -> kubemodel.DiagnosticResult
+	10, // [10:10] is the sub-list for method output_type
+	10, // [10:10] is the sub-list for method input_type
+	10, // [10:10] is the sub-list for extension type_name
+	10, // [10:10] is the sub-list for extension extendee
+	0,  // [0:10] is the sub-list for field type_name
+}
+
+func init() { file_kubemodel_storage_proto_init() }
+func file_kubemodel_storage_proto_init() {
+	if File_kubemodel_storage_proto != nil {
+		return
+	}
+	file_kubemodel_diagnostic_proto_init()
+	file_kubemodel_storage_proto_msgTypes[0].OneofWrappers = []any{}
+	file_kubemodel_storage_proto_msgTypes[1].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_kubemodel_storage_proto_rawDesc), len(file_kubemodel_storage_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   6,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_kubemodel_storage_proto_goTypes,
+		DependencyIndexes: file_kubemodel_storage_proto_depIdxs,
+		MessageInfos:      file_kubemodel_storage_proto_msgTypes,
+	}.Build()
+	File_kubemodel_storage_proto = out.File
+	file_kubemodel_storage_proto_goTypes = nil
+	file_kubemodel_storage_proto_depIdxs = nil
+}

+ 30 - 30
core/pkg/model/pb/labels.pb.go

@@ -2,7 +2,7 @@
 // versions:
 // 	protoc-gen-go v1.36.9
 // 	protoc        v6.32.1
-// source: protos/model/labels.proto
+// source: model/labels.proto
 
 package pb
 
@@ -39,7 +39,7 @@ type LabelsResponse struct {
 
 func (x *LabelsResponse) Reset() {
 	*x = LabelsResponse{}
-	mi := &file_protos_model_labels_proto_msgTypes[0]
+	mi := &file_model_labels_proto_msgTypes[0]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -51,7 +51,7 @@ func (x *LabelsResponse) String() string {
 func (*LabelsResponse) ProtoMessage() {}
 
 func (x *LabelsResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_protos_model_labels_proto_msgTypes[0]
+	mi := &file_model_labels_proto_msgTypes[0]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -64,7 +64,7 @@ func (x *LabelsResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use LabelsResponse.ProtoReflect.Descriptor instead.
 func (*LabelsResponse) Descriptor() ([]byte, []int) {
-	return file_protos_model_labels_proto_rawDescGZIP(), []int{0}
+	return file_model_labels_proto_rawDescGZIP(), []int{0}
 }
 
 func (x *LabelsResponse) GetType() string {
@@ -105,7 +105,7 @@ type LabelSet struct {
 
 func (x *LabelSet) Reset() {
 	*x = LabelSet{}
-	mi := &file_protos_model_labels_proto_msgTypes[1]
+	mi := &file_model_labels_proto_msgTypes[1]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -117,7 +117,7 @@ func (x *LabelSet) String() string {
 func (*LabelSet) ProtoMessage() {}
 
 func (x *LabelSet) ProtoReflect() protoreflect.Message {
-	mi := &file_protos_model_labels_proto_msgTypes[1]
+	mi := &file_model_labels_proto_msgTypes[1]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -130,7 +130,7 @@ func (x *LabelSet) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use LabelSet.ProtoReflect.Descriptor instead.
 func (*LabelSet) Descriptor() ([]byte, []int) {
-	return file_protos_model_labels_proto_rawDescGZIP(), []int{1}
+	return file_model_labels_proto_rawDescGZIP(), []int{1}
 }
 
 func (x *LabelSet) GetLabels() map[string]string {
@@ -140,11 +140,11 @@ func (x *LabelSet) GetLabels() map[string]string {
 	return nil
 }
 
-var File_protos_model_labels_proto protoreflect.FileDescriptor
+var File_model_labels_proto protoreflect.FileDescriptor
 
-const file_protos_model_labels_proto_rawDesc = "" +
+const file_model_labels_proto_rawDesc = "" +
 	"\n" +
-	"\x19protos/model/labels.proto\x12\x05model\x1a\x19protos/model/window.proto\"\xfa\x01\n" +
+	"\x12model/labels.proto\x12\x05model\x1a\x12model/window.proto\"\xfa\x01\n" +
 	"\x0eLabelsResponse\x12\x12\n" +
 	"\x04type\x18\x01 \x01(\tR\x04type\x12\x19\n" +
 	"\bgroup_id\x18\x02 \x01(\tR\agroupId\x12%\n" +
@@ -161,26 +161,26 @@ const file_protos_model_labels_proto_rawDesc = "" +
 	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B0Z.github.com/opencost/opencost/core/pkg/model/pbb\x06proto3"
 
 var (
-	file_protos_model_labels_proto_rawDescOnce sync.Once
-	file_protos_model_labels_proto_rawDescData []byte
+	file_model_labels_proto_rawDescOnce sync.Once
+	file_model_labels_proto_rawDescData []byte
 )
 
-func file_protos_model_labels_proto_rawDescGZIP() []byte {
-	file_protos_model_labels_proto_rawDescOnce.Do(func() {
-		file_protos_model_labels_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protos_model_labels_proto_rawDesc), len(file_protos_model_labels_proto_rawDesc)))
+func file_model_labels_proto_rawDescGZIP() []byte {
+	file_model_labels_proto_rawDescOnce.Do(func() {
+		file_model_labels_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_model_labels_proto_rawDesc), len(file_model_labels_proto_rawDesc)))
 	})
-	return file_protos_model_labels_proto_rawDescData
+	return file_model_labels_proto_rawDescData
 }
 
-var file_protos_model_labels_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
-var file_protos_model_labels_proto_goTypes = []any{
+var file_model_labels_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_model_labels_proto_goTypes = []any{
 	(*LabelsResponse)(nil), // 0: model.LabelsResponse
 	(*LabelSet)(nil),       // 1: model.LabelSet
 	nil,                    // 2: model.LabelsResponse.LabelSetsEntry
 	nil,                    // 3: model.LabelSet.LabelsEntry
 	(*Window)(nil),         // 4: model.Window
 }
-var file_protos_model_labels_proto_depIdxs = []int32{
+var file_model_labels_proto_depIdxs = []int32{
 	4, // 0: model.LabelsResponse.window:type_name -> model.Window
 	2, // 1: model.LabelsResponse.label_sets:type_name -> model.LabelsResponse.LabelSetsEntry
 	3, // 2: model.LabelSet.labels:type_name -> model.LabelSet.LabelsEntry
@@ -192,27 +192,27 @@ var file_protos_model_labels_proto_depIdxs = []int32{
 	0, // [0:4] is the sub-list for field type_name
 }
 
-func init() { file_protos_model_labels_proto_init() }
-func file_protos_model_labels_proto_init() {
-	if File_protos_model_labels_proto != nil {
+func init() { file_model_labels_proto_init() }
+func file_model_labels_proto_init() {
+	if File_model_labels_proto != nil {
 		return
 	}
-	file_protos_model_window_proto_init()
+	file_model_window_proto_init()
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
-			RawDescriptor: unsafe.Slice(unsafe.StringData(file_protos_model_labels_proto_rawDesc), len(file_protos_model_labels_proto_rawDesc)),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_model_labels_proto_rawDesc), len(file_model_labels_proto_rawDesc)),
 			NumEnums:      0,
 			NumMessages:   4,
 			NumExtensions: 0,
 			NumServices:   0,
 		},
-		GoTypes:           file_protos_model_labels_proto_goTypes,
-		DependencyIndexes: file_protos_model_labels_proto_depIdxs,
-		MessageInfos:      file_protos_model_labels_proto_msgTypes,
+		GoTypes:           file_model_labels_proto_goTypes,
+		DependencyIndexes: file_model_labels_proto_depIdxs,
+		MessageInfos:      file_model_labels_proto_msgTypes,
 	}.Build()
-	File_protos_model_labels_proto = out.File
-	file_protos_model_labels_proto_goTypes = nil
-	file_protos_model_labels_proto_depIdxs = nil
+	File_model_labels_proto = out.File
+	file_model_labels_proto_goTypes = nil
+	file_model_labels_proto_depIdxs = nil
 }

+ 40 - 40
core/pkg/model/pb/messages.pb.go

@@ -2,7 +2,7 @@
 // versions:
 // 	protoc-gen-go v1.36.9
 // 	protoc        v6.32.1
-// source: protos/customcost/messages.proto
+// source: customcost/messages.proto
 
 package pb
 
@@ -36,7 +36,7 @@ type CustomCostRequest struct {
 
 func (x *CustomCostRequest) Reset() {
 	*x = CustomCostRequest{}
-	mi := &file_protos_customcost_messages_proto_msgTypes[0]
+	mi := &file_customcost_messages_proto_msgTypes[0]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -48,7 +48,7 @@ func (x *CustomCostRequest) String() string {
 func (*CustomCostRequest) ProtoMessage() {}
 
 func (x *CustomCostRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_protos_customcost_messages_proto_msgTypes[0]
+	mi := &file_customcost_messages_proto_msgTypes[0]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -61,7 +61,7 @@ func (x *CustomCostRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use CustomCostRequest.ProtoReflect.Descriptor instead.
 func (*CustomCostRequest) Descriptor() ([]byte, []int) {
-	return file_protos_customcost_messages_proto_rawDescGZIP(), []int{0}
+	return file_customcost_messages_proto_rawDescGZIP(), []int{0}
 }
 
 func (x *CustomCostRequest) GetStart() *timestamppb.Timestamp {
@@ -94,7 +94,7 @@ type CustomCostResponseSet struct {
 
 func (x *CustomCostResponseSet) Reset() {
 	*x = CustomCostResponseSet{}
-	mi := &file_protos_customcost_messages_proto_msgTypes[1]
+	mi := &file_customcost_messages_proto_msgTypes[1]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -106,7 +106,7 @@ func (x *CustomCostResponseSet) String() string {
 func (*CustomCostResponseSet) ProtoMessage() {}
 
 func (x *CustomCostResponseSet) ProtoReflect() protoreflect.Message {
-	mi := &file_protos_customcost_messages_proto_msgTypes[1]
+	mi := &file_customcost_messages_proto_msgTypes[1]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -119,7 +119,7 @@ func (x *CustomCostResponseSet) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use CustomCostResponseSet.ProtoReflect.Descriptor instead.
 func (*CustomCostResponseSet) Descriptor() ([]byte, []int) {
-	return file_protos_customcost_messages_proto_rawDescGZIP(), []int{1}
+	return file_customcost_messages_proto_rawDescGZIP(), []int{1}
 }
 
 func (x *CustomCostResponseSet) GetResps() []*CustomCostResponse {
@@ -161,7 +161,7 @@ type CustomCostResponse struct {
 
 func (x *CustomCostResponse) Reset() {
 	*x = CustomCostResponse{}
-	mi := &file_protos_customcost_messages_proto_msgTypes[2]
+	mi := &file_customcost_messages_proto_msgTypes[2]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -173,7 +173,7 @@ func (x *CustomCostResponse) String() string {
 func (*CustomCostResponse) ProtoMessage() {}
 
 func (x *CustomCostResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_protos_customcost_messages_proto_msgTypes[2]
+	mi := &file_customcost_messages_proto_msgTypes[2]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -186,7 +186,7 @@ func (x *CustomCostResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use CustomCostResponse.ProtoReflect.Descriptor instead.
 func (*CustomCostResponse) Descriptor() ([]byte, []int) {
-	return file_protos_customcost_messages_proto_rawDescGZIP(), []int{2}
+	return file_customcost_messages_proto_rawDescGZIP(), []int{2}
 }
 
 func (x *CustomCostResponse) GetMetadata() map[string]string {
@@ -300,7 +300,7 @@ type CustomCost struct {
 
 func (x *CustomCost) Reset() {
 	*x = CustomCost{}
-	mi := &file_protos_customcost_messages_proto_msgTypes[3]
+	mi := &file_customcost_messages_proto_msgTypes[3]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -312,7 +312,7 @@ func (x *CustomCost) String() string {
 func (*CustomCost) ProtoMessage() {}
 
 func (x *CustomCost) ProtoReflect() protoreflect.Message {
-	mi := &file_protos_customcost_messages_proto_msgTypes[3]
+	mi := &file_customcost_messages_proto_msgTypes[3]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -325,7 +325,7 @@ func (x *CustomCost) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use CustomCost.ProtoReflect.Descriptor instead.
 func (*CustomCost) Descriptor() ([]byte, []int) {
-	return file_protos_customcost_messages_proto_rawDescGZIP(), []int{3}
+	return file_customcost_messages_proto_rawDescGZIP(), []int{3}
 }
 
 func (x *CustomCost) GetMetadata() map[string]string {
@@ -496,7 +496,7 @@ type CustomCostExtendedAttributes struct {
 
 func (x *CustomCostExtendedAttributes) Reset() {
 	*x = CustomCostExtendedAttributes{}
-	mi := &file_protos_customcost_messages_proto_msgTypes[4]
+	mi := &file_customcost_messages_proto_msgTypes[4]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -508,7 +508,7 @@ func (x *CustomCostExtendedAttributes) String() string {
 func (*CustomCostExtendedAttributes) ProtoMessage() {}
 
 func (x *CustomCostExtendedAttributes) ProtoReflect() protoreflect.Message {
-	mi := &file_protos_customcost_messages_proto_msgTypes[4]
+	mi := &file_customcost_messages_proto_msgTypes[4]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -521,7 +521,7 @@ func (x *CustomCostExtendedAttributes) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use CustomCostExtendedAttributes.ProtoReflect.Descriptor instead.
 func (*CustomCostExtendedAttributes) Descriptor() ([]byte, []int) {
-	return file_protos_customcost_messages_proto_rawDescGZIP(), []int{4}
+	return file_customcost_messages_proto_rawDescGZIP(), []int{4}
 }
 
 func (x *CustomCostExtendedAttributes) GetBillingPeriodStart() *timestamppb.Timestamp {
@@ -678,11 +678,11 @@ func (x *CustomCostExtendedAttributes) GetPricingCategory() string {
 	return ""
 }
 
-var File_protos_customcost_messages_proto protoreflect.FileDescriptor
+var File_customcost_messages_proto protoreflect.FileDescriptor
 
-const file_protos_customcost_messages_proto_rawDesc = "" +
+const file_customcost_messages_proto_rawDesc = "" +
 	"\n" +
-	" protos/customcost/messages.proto\x12\x13customcost.messages\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\"\xae\x01\n" +
+	"\x19customcost/messages.proto\x12\x13customcost.messages\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\"\xae\x01\n" +
 	"\x11CustomCostRequest\x120\n" +
 	"\x05start\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x05start\x12,\n" +
 	"\x03end\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x03end\x129\n" +
@@ -788,19 +788,19 @@ const file_protos_customcost_messages_proto_rawDesc = "" +
 	"\x0eGetCustomCosts\x12&.customcost.messages.CustomCostRequest\x1a*.customcost.messages.CustomCostResponseSetB0Z.github.com/opencost/opencost/core/pkg/model/pbb\x06proto3"
 
 var (
-	file_protos_customcost_messages_proto_rawDescOnce sync.Once
-	file_protos_customcost_messages_proto_rawDescData []byte
+	file_customcost_messages_proto_rawDescOnce sync.Once
+	file_customcost_messages_proto_rawDescData []byte
 )
 
-func file_protos_customcost_messages_proto_rawDescGZIP() []byte {
-	file_protos_customcost_messages_proto_rawDescOnce.Do(func() {
-		file_protos_customcost_messages_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protos_customcost_messages_proto_rawDesc), len(file_protos_customcost_messages_proto_rawDesc)))
+func file_customcost_messages_proto_rawDescGZIP() []byte {
+	file_customcost_messages_proto_rawDescOnce.Do(func() {
+		file_customcost_messages_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_customcost_messages_proto_rawDesc), len(file_customcost_messages_proto_rawDesc)))
 	})
-	return file_protos_customcost_messages_proto_rawDescData
+	return file_customcost_messages_proto_rawDescData
 }
 
-var file_protos_customcost_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
-var file_protos_customcost_messages_proto_goTypes = []any{
+var file_customcost_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
+var file_customcost_messages_proto_goTypes = []any{
 	(*CustomCostRequest)(nil),            // 0: customcost.messages.CustomCostRequest
 	(*CustomCostResponseSet)(nil),        // 1: customcost.messages.CustomCostResponseSet
 	(*CustomCostResponse)(nil),           // 2: customcost.messages.CustomCostResponse
@@ -812,7 +812,7 @@ var file_protos_customcost_messages_proto_goTypes = []any{
 	(*timestamppb.Timestamp)(nil),        // 8: google.protobuf.Timestamp
 	(*durationpb.Duration)(nil),          // 9: google.protobuf.Duration
 }
-var file_protos_customcost_messages_proto_depIdxs = []int32{
+var file_customcost_messages_proto_depIdxs = []int32{
 	8,  // 0: customcost.messages.CustomCostRequest.start:type_name -> google.protobuf.Timestamp
 	8,  // 1: customcost.messages.CustomCostRequest.end:type_name -> google.protobuf.Timestamp
 	9,  // 2: customcost.messages.CustomCostRequest.resolution:type_name -> google.protobuf.Duration
@@ -835,28 +835,28 @@ var file_protos_customcost_messages_proto_depIdxs = []int32{
 	0,  // [0:13] is the sub-list for field type_name
 }
 
-func init() { file_protos_customcost_messages_proto_init() }
-func file_protos_customcost_messages_proto_init() {
-	if File_protos_customcost_messages_proto != nil {
+func init() { file_customcost_messages_proto_init() }
+func file_customcost_messages_proto_init() {
+	if File_customcost_messages_proto != nil {
 		return
 	}
-	file_protos_customcost_messages_proto_msgTypes[3].OneofWrappers = []any{}
-	file_protos_customcost_messages_proto_msgTypes[4].OneofWrappers = []any{}
+	file_customcost_messages_proto_msgTypes[3].OneofWrappers = []any{}
+	file_customcost_messages_proto_msgTypes[4].OneofWrappers = []any{}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
-			RawDescriptor: unsafe.Slice(unsafe.StringData(file_protos_customcost_messages_proto_rawDesc), len(file_protos_customcost_messages_proto_rawDesc)),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_customcost_messages_proto_rawDesc), len(file_customcost_messages_proto_rawDesc)),
 			NumEnums:      0,
 			NumMessages:   8,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
-		GoTypes:           file_protos_customcost_messages_proto_goTypes,
-		DependencyIndexes: file_protos_customcost_messages_proto_depIdxs,
-		MessageInfos:      file_protos_customcost_messages_proto_msgTypes,
+		GoTypes:           file_customcost_messages_proto_goTypes,
+		DependencyIndexes: file_customcost_messages_proto_depIdxs,
+		MessageInfos:      file_customcost_messages_proto_msgTypes,
 	}.Build()
-	File_protos_customcost_messages_proto = out.File
-	file_protos_customcost_messages_proto_goTypes = nil
-	file_protos_customcost_messages_proto_depIdxs = nil
+	File_customcost_messages_proto = out.File
+	file_customcost_messages_proto_goTypes = nil
+	file_customcost_messages_proto_depIdxs = nil
 }

+ 2 - 2
core/pkg/model/pb/messages_grpc.pb.go

@@ -2,7 +2,7 @@
 // versions:
 // - protoc-gen-go-grpc v1.5.1
 // - protoc             v6.32.1
-// source: protos/customcost/messages.proto
+// source: customcost/messages.proto
 
 package pb
 
@@ -117,5 +117,5 @@ var CustomCostsSource_ServiceDesc = grpc.ServiceDesc{
 		},
 	},
 	Streams:  []grpc.StreamDesc{},
-	Metadata: "protos/customcost/messages.proto",
+	Metadata: "customcost/messages.proto",
 }

+ 101 - 42
core/pkg/model/pb/window.pb.go

@@ -2,7 +2,7 @@
 // versions:
 // 	protoc-gen-go v1.36.9
 // 	protoc        v6.32.1
-// source: protos/model/window.proto
+// source: model/window.proto
 
 package pb
 
@@ -22,11 +22,61 @@ const (
 	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
 )
 
+// Resolution represents the time granularity for data aggregation
+type Resolution int32
+
+const (
+	Resolution_RESOLUTION_10M Resolution = 0 // 10 minutes
+	Resolution_RESOLUTION_1H  Resolution = 1 // 1 hour
+	Resolution_RESOLUTION_1D  Resolution = 2 // 1 day
+)
+
+// Enum value maps for Resolution.
+var (
+	Resolution_name = map[int32]string{
+		0: "RESOLUTION_10M",
+		1: "RESOLUTION_1H",
+		2: "RESOLUTION_1D",
+	}
+	Resolution_value = map[string]int32{
+		"RESOLUTION_10M": 0,
+		"RESOLUTION_1H":  1,
+		"RESOLUTION_1D":  2,
+	}
+)
+
+func (x Resolution) Enum() *Resolution {
+	p := new(Resolution)
+	*p = x
+	return p
+}
+
+func (x Resolution) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (Resolution) Descriptor() protoreflect.EnumDescriptor {
+	return file_model_window_proto_enumTypes[0].Descriptor()
+}
+
+func (Resolution) Type() protoreflect.EnumType {
+	return &file_model_window_proto_enumTypes[0]
+}
+
+func (x Resolution) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use Resolution.Descriptor instead.
+func (Resolution) EnumDescriptor() ([]byte, []int) {
+	return file_model_window_proto_rawDescGZIP(), []int{0}
+}
+
 // Window defines a unit of time by a resolution and a start time
 type Window struct {
 	state protoimpl.MessageState `protogen:"open.v1"`
-	// resolution in "1h" or "1d" format
-	Resolution string `protobuf:"bytes,1,opt,name=resolution,proto3" json:"resolution,omitempty"`
+	// resolution defines the time granularity
+	Resolution Resolution `protobuf:"varint,1,opt,name=resolution,proto3,enum=model.Resolution" json:"resolution,omitempty"`
 	// the start time of the window described
 	Start         *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=start,proto3" json:"start,omitempty"`
 	unknownFields protoimpl.UnknownFields
@@ -35,7 +85,7 @@ type Window struct {
 
 func (x *Window) Reset() {
 	*x = Window{}
-	mi := &file_protos_model_window_proto_msgTypes[0]
+	mi := &file_model_window_proto_msgTypes[0]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -47,7 +97,7 @@ func (x *Window) String() string {
 func (*Window) ProtoMessage() {}
 
 func (x *Window) ProtoReflect() protoreflect.Message {
-	mi := &file_protos_model_window_proto_msgTypes[0]
+	mi := &file_model_window_proto_msgTypes[0]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -60,14 +110,14 @@ func (x *Window) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use Window.ProtoReflect.Descriptor instead.
 func (*Window) Descriptor() ([]byte, []int) {
-	return file_protos_model_window_proto_rawDescGZIP(), []int{0}
+	return file_model_window_proto_rawDescGZIP(), []int{0}
 }
 
-func (x *Window) GetResolution() string {
+func (x *Window) GetResolution() Resolution {
 	if x != nil {
 		return x.Resolution
 	}
-	return ""
+	return Resolution_RESOLUTION_10M
 }
 
 func (x *Window) GetStart() *timestamppb.Timestamp {
@@ -77,63 +127,72 @@ func (x *Window) GetStart() *timestamppb.Timestamp {
 	return nil
 }
 
-var File_protos_model_window_proto protoreflect.FileDescriptor
+var File_model_window_proto protoreflect.FileDescriptor
 
-const file_protos_model_window_proto_rawDesc = "" +
+const file_model_window_proto_rawDesc = "" +
 	"\n" +
-	"\x19protos/model/window.proto\x12\x05model\x1a\x1fgoogle/protobuf/timestamp.proto\"Z\n" +
-	"\x06Window\x12\x1e\n" +
+	"\x12model/window.proto\x12\x05model\x1a\x1fgoogle/protobuf/timestamp.proto\"m\n" +
+	"\x06Window\x121\n" +
 	"\n" +
-	"resolution\x18\x01 \x01(\tR\n" +
+	"resolution\x18\x01 \x01(\x0e2\x11.model.ResolutionR\n" +
 	"resolution\x120\n" +
-	"\x05start\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05startB0Z.github.com/opencost/opencost/core/pkg/model/pbb\x06proto3"
+	"\x05start\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05start*F\n" +
+	"\n" +
+	"Resolution\x12\x12\n" +
+	"\x0eRESOLUTION_10M\x10\x00\x12\x11\n" +
+	"\rRESOLUTION_1H\x10\x01\x12\x11\n" +
+	"\rRESOLUTION_1D\x10\x02B0Z.github.com/opencost/opencost/core/pkg/model/pbb\x06proto3"
 
 var (
-	file_protos_model_window_proto_rawDescOnce sync.Once
-	file_protos_model_window_proto_rawDescData []byte
+	file_model_window_proto_rawDescOnce sync.Once
+	file_model_window_proto_rawDescData []byte
 )
 
-func file_protos_model_window_proto_rawDescGZIP() []byte {
-	file_protos_model_window_proto_rawDescOnce.Do(func() {
-		file_protos_model_window_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protos_model_window_proto_rawDesc), len(file_protos_model_window_proto_rawDesc)))
+func file_model_window_proto_rawDescGZIP() []byte {
+	file_model_window_proto_rawDescOnce.Do(func() {
+		file_model_window_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_model_window_proto_rawDesc), len(file_model_window_proto_rawDesc)))
 	})
-	return file_protos_model_window_proto_rawDescData
+	return file_model_window_proto_rawDescData
 }
 
-var file_protos_model_window_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
-var file_protos_model_window_proto_goTypes = []any{
-	(*Window)(nil),                // 0: model.Window
-	(*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp
+var file_model_window_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_model_window_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_model_window_proto_goTypes = []any{
+	(Resolution)(0),               // 0: model.Resolution
+	(*Window)(nil),                // 1: model.Window
+	(*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp
 }
-var file_protos_model_window_proto_depIdxs = []int32{
-	1, // 0: model.Window.start:type_name -> google.protobuf.Timestamp
-	1, // [1:1] is the sub-list for method output_type
-	1, // [1:1] is the sub-list for method input_type
-	1, // [1:1] is the sub-list for extension type_name
-	1, // [1:1] is the sub-list for extension extendee
-	0, // [0:1] is the sub-list for field type_name
+var file_model_window_proto_depIdxs = []int32{
+	0, // 0: model.Window.resolution:type_name -> model.Resolution
+	2, // 1: model.Window.start:type_name -> google.protobuf.Timestamp
+	2, // [2:2] is the sub-list for method output_type
+	2, // [2:2] is the sub-list for method input_type
+	2, // [2:2] is the sub-list for extension type_name
+	2, // [2:2] is the sub-list for extension extendee
+	0, // [0:2] is the sub-list for field type_name
 }
 
-func init() { file_protos_model_window_proto_init() }
-func file_protos_model_window_proto_init() {
-	if File_protos_model_window_proto != nil {
+func init() { file_model_window_proto_init() }
+func file_model_window_proto_init() {
+	if File_model_window_proto != nil {
 		return
 	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
-			RawDescriptor: unsafe.Slice(unsafe.StringData(file_protos_model_window_proto_rawDesc), len(file_protos_model_window_proto_rawDesc)),
-			NumEnums:      0,
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_model_window_proto_rawDesc), len(file_model_window_proto_rawDesc)),
+			NumEnums:      1,
 			NumMessages:   1,
 			NumExtensions: 0,
 			NumServices:   0,
 		},
-		GoTypes:           file_protos_model_window_proto_goTypes,
-		DependencyIndexes: file_protos_model_window_proto_depIdxs,
-		MessageInfos:      file_protos_model_window_proto_msgTypes,
+		GoTypes:           file_model_window_proto_goTypes,
+		DependencyIndexes: file_model_window_proto_depIdxs,
+		EnumInfos:         file_model_window_proto_enumTypes,
+		MessageInfos:      file_model_window_proto_msgTypes,
 	}.Build()
-	File_protos_model_window_proto = out.File
-	file_protos_model_window_proto_goTypes = nil
-	file_protos_model_window_proto_depIdxs = nil
+	File_model_window_proto = out.File
+	file_model_window_proto_goTypes = nil
+	file_model_window_proto_depIdxs = nil
 }

+ 3 - 2
generate.sh

@@ -1,6 +1,7 @@
 #!/usr/bin/env sh
 #
 
-protoc --go_out=./core --go_opt=module=github.com/opencost/opencost/core \
+# Generate core protobuf files
+protoc --proto_path=protos --go_out=./core --go_opt=module=github.com/opencost/opencost/core \
     --go-grpc_out=./core --go-grpc_opt=module=github.com/opencost/opencost/core \
-    protos/**/*.proto
+    protos/**/*.proto

+ 36 - 0
protos/kubemodel/cluster.proto

@@ -0,0 +1,36 @@
+syntax = "proto3";
+
+import "model/window.proto";
+
+package kubemodel;
+option go_package = "github.com/opencost/opencost/core/pkg/model/pb/kubemodel";
+
+// Provider represents the cloud provider or infrastructure type
+enum Provider {
+  PROVIDER_UNSPECIFIED = 0;
+  PROVIDER_AWS = 1;
+  PROVIDER_GCP = 2;
+  PROVIDER_AZURE = 3;
+  PROVIDER_ON_PREMISES = 4;
+  PROVIDER_ALIBABA = 5;
+  PROVIDER_DIGITALOCEAN = 6;
+  PROVIDER_ORACLE = 7;
+}
+
+// Cluster represents the top-level Kubernetes cluster
+message Cluster {
+  // Identification
+  // User-configured cluster identifier, defaults to kube-system namespace UID
+  // The kube-system namespace UID is unique per cluster and stable across its lifetime
+  string ID = 1;
+
+  // Properties
+  Provider provider = 2;
+  string account = 3;
+  string name = 4;
+
+  // Centralized window definition for entire cluster
+  // All resources inherit this window unless they specify their own duration
+  model.Window window = 5;
+
+}

+ 48 - 0
protos/kubemodel/container.proto

@@ -0,0 +1,48 @@
+syntax = "proto3";
+
+import "kubemodel/diagnostic.proto";
+import "google/protobuf/timestamp.proto";
+
+package kubemodel;
+option go_package = "github.com/opencost/opencost/core/pkg/model/pb/kubemodel";
+
+// Container represents a container within a pod (allocated resource)
+message Container {
+  // Identification
+  string podID = 1;
+
+  // Properties
+  string name = 2;
+
+  // Resource lifecycle (only when different from cluster window)
+  optional google.protobuf.Timestamp creationTime = 3;
+  optional google.protobuf.Timestamp deletionTime = 4;
+
+  // Usage metrics
+  // CPU usage in core-hours
+  float cpuCoreHours = 5;
+
+  // CPU request average in cores
+  float cpuCoreRequestAverage = 6;
+
+  // CPU usage average in cores
+  float cpuCoreUsageAverage = 7;
+
+  // CPU usage max in cores
+  float cpuCoreUsageMax = 8;
+
+  // RAM usage in byte-hours
+  int64 ramByteHours = 9;
+
+  // RAM request average in bytes
+  int64 ramBytesRequestAverage = 10;
+
+  // RAM usage average in bytes
+  int64 ramBytesUsageAverage = 11;
+
+  // RAM usage max in bytes
+  int64 ramBytesUsageMax = 12;
+
+  // Diagnostic information about this resource
+  optional DiagnosticResult diagnostic = 99;
+}

+ 37 - 0
protos/kubemodel/controller.proto

@@ -0,0 +1,37 @@
+syntax = "proto3";
+
+import "kubemodel/diagnostic.proto";
+import "google/protobuf/timestamp.proto";
+
+package kubemodel;
+option go_package = "github.com/opencost/opencost/core/pkg/model/pb/kubemodel";
+
+enum ControllerKind {
+  KIND_UNSPECIFIED = 0;
+  DEPLOYMENT = 1;
+  STATEFULSET = 2;
+  DAEMONSET = 3;
+  JOB = 4;
+  CRONJOB = 5;
+  REPLICASET = 6;
+}
+
+// Controller represents a Kubernetes workload controller
+message Controller {
+  // Identification
+  string ID = 1;
+  string namespaceID = 2;
+
+  // Properties
+  string name = 3;
+  ControllerKind kind = 4;
+  map<string, string> labels = 5;
+  map<string, string> annotations = 6;
+
+  // Resource lifecycle (only when different from cluster window)
+  optional google.protobuf.Timestamp creationTime = 7;
+  optional google.protobuf.Timestamp deletionTime = 8;
+
+  // Diagnostic information about this resource
+  optional DiagnosticResult diagnostic = 99;
+}

+ 46 - 0
protos/kubemodel/diagnostic.proto

@@ -0,0 +1,46 @@
+syntax = "proto3";
+
+import "google/protobuf/timestamp.proto";
+
+package kubemodel;
+option go_package = "github.com/opencost/opencost/core/pkg/model/pb/kubemodel";
+
+
+// DiagnosticResult represents the result of a diagnostic run
+// This matches the JSON structure from core/pkg/diagnostics/diagnostics.go
+message DiagnosticResult {
+  // Unique Identifier for the diagnostic run result
+  string id = 1;
+
+  // Name of the diagnostic that ran
+  string name = 2;
+
+  // Description of the diagnostic run, human readable description
+  string description = 3;
+
+  // Category of the diagnostic run, used to group similar diagnostics
+  string category = 4;
+
+  // Timestamp when the diagnostic run was executed
+  google.protobuf.Timestamp timestamp = 5;
+
+  // Error message if the diagnostic run failed (optional)
+  string error = 6;
+
+  // Additional custom information about the diagnostic run
+  // Using string values to match map[string]any from JSON
+  map<string, string> details = 7;
+}
+
+// DiagnosticsRunReport contains the start time and all diagnostic results
+// This matches the JSON structure from core/pkg/diagnostics/diagnostics.go
+message DiagnosticsRunReport {
+  // Application name that the diagnostics run belongs to
+  string application = 1;
+
+  // Time when the full diagnostics run started
+  google.protobuf.Timestamp startTime = 2;
+
+  // All results of the diagnostics run
+  repeated DiagnosticResult results = 3;
+}

+ 68 - 0
protos/kubemodel/gpu.proto

@@ -0,0 +1,68 @@
+syntax = "proto3";
+
+import "kubemodel/diagnostic.proto";
+
+package kubemodel;
+option go_package = "github.com/opencost/opencost/core/pkg/model/pb/kubemodel";
+
+// GPUDevice represents a GPU device with DCGM integration (provisioned resource)
+// This tracks available GPU capacity on a node
+message GPUDevice {
+  // Identification
+  string ID = 1;          // GPU UUID (hardware identifier)
+  string nodeID = 2;      // Node hosting this GPU device
+
+  // Properties
+  int32 deviceNumber = 3;
+  string modelName = 4;
+
+  // GPU sharing information
+  bool isShared = 6;
+  float sharePercentage = 9;
+
+  // Capacity metrics
+  // GPU hours available
+  float gpuHours = 10;
+
+  // GPU request average percentage (0-100)
+  float gpuRequestAverage = 11;
+
+  // GPU usage average percentage (0-100)
+  float gpuUsageAverage = 12;
+
+  // GPU usage max percentage (0-100)
+  float gpuUsageMax = 13;
+
+  // GPU memory capacity in bytes
+  int64 memoryBytes = 14;
+
+  // Diagnostic information about this resource
+  optional DiagnosticResult diagnostic = 99;
+}
+
+// GPUUsage represents GPU resources consumed by a container (allocated resource)
+// This tracks actual GPU usage by containers for cost analysis
+message GPUUsage {
+  // Identification
+  string containerID = 1;  // Container consuming GPU resources
+  string gpuDeviceID = 2;  // Reference to the GPU device being used
+
+  // Usage metrics
+  // GPU usage in device-hours consumed
+  float gpuHours = 3;
+
+  // GPU request in percentage (0-100)
+  float gpuRequestPercentage = 4;
+
+  // GPU usage average percentage (0-100)
+  float gpuUsageAverage = 5;
+
+  // GPU usage max percentage (0-100)
+  float gpuUsageMax = 6;
+
+  // GPU memory usage in bytes
+  int64 memoryBytesUsed = 7;
+
+  // Diagnostic information about this resource
+  optional DiagnosticResult diagnostic = 99;
+}

+ 26 - 0
protos/kubemodel/namespace.proto

@@ -0,0 +1,26 @@
+syntax = "proto3";
+
+import "kubemodel/diagnostic.proto";
+import "google/protobuf/timestamp.proto";
+
+package kubemodel;
+option go_package = "github.com/opencost/opencost/core/pkg/model/pb/kubemodel";
+
+// Namespace represents a Kubernetes namespace (allocated resource grouping)
+message Namespace {
+  // Identification
+  string ID = 1;
+  string clusterID = 2;
+
+  // Properties
+  string name = 3;
+  map<string, string> labels = 4;
+  map<string, string> annotations = 5;
+
+  // Resource lifecycle (only when different from cluster window)
+  optional google.protobuf.Timestamp creationTime = 7;
+  optional google.protobuf.Timestamp deletionTime = 8;
+
+  // Diagnostic information about this resource
+  optional DiagnosticResult diagnostic = 99;
+}

+ 45 - 0
protos/kubemodel/network.proto

@@ -0,0 +1,45 @@
+syntax = "proto3";
+
+import "kubemodel/diagnostic.proto";
+import "google/protobuf/timestamp.proto";
+
+package kubemodel;
+option go_package = "github.com/opencost/opencost/core/pkg/model/pb/kubemodel";
+
+
+// ServicePort represents a port exposed by a service
+message ServicePort {
+  string name = 1;
+  string protocol = 2;
+  int32 port = 3;
+  int32 targetPort = 4;
+  int32 nodePort = 5;
+}
+
+// Service represents a K8s Service (allocated resource)
+message Service {
+  // Identification
+  string ID = 1;
+  string clusterID = 2;
+
+  // Properties
+  string name = 3;
+  string serviceType = 4;
+  repeated ServicePort ports = 5;
+  map<string, string> labels = 6;
+  map<string, string> annotations = 7;
+
+  // Resource lifecycle (only when different from cluster window)
+  optional google.protobuf.Timestamp creationTime = 8;
+  optional google.protobuf.Timestamp deletionTime = 9;
+
+  // Usage metrics
+  // Network transfer bytes sent through this service
+  int64 networkTransferBytes = 10;
+
+  // Network bytes received through this service
+  int64 networkReceiveBytes = 11;
+
+  // Diagnostic information about this resource
+  optional DiagnosticResult diagnostic = 99;
+}

+ 27 - 0
protos/kubemodel/node.proto

@@ -0,0 +1,27 @@
+syntax = "proto3";
+
+import "kubemodel/diagnostic.proto";
+import "google/protobuf/timestamp.proto";
+
+package kubemodel;
+option go_package = "github.com/opencost/opencost/core/pkg/model/pb/kubemodel";
+
+// Node represents a Kubernetes worker node (provisioned resource)
+message Node {
+  // Identification
+  string ID = 1;
+  string clusterID = 2;
+
+  // Properties
+  string providerResourceID = 3;
+  string name = 4;
+  map<string, string> labels = 5;
+  map<string, string> annotations = 6;
+
+  // Resource lifecycle (only when different from cluster window)
+  optional google.protobuf.Timestamp creationTime = 7;
+  optional google.protobuf.Timestamp deletionTime = 8;
+
+  // Diagnostic information about this resource
+  optional DiagnosticResult diagnostic = 99;
+}

+ 62 - 0
protos/kubemodel/pod.proto

@@ -0,0 +1,62 @@
+syntax = "proto3";
+
+import "kubemodel/diagnostic.proto";
+import "google/protobuf/timestamp.proto";
+
+package kubemodel;
+option go_package = "github.com/opencost/opencost/core/pkg/model/pb/kubemodel";
+
+// Pod represents a Kubernetes pod (allocated resource grouping)
+message Pod {
+  // Identification
+  string ID = 1;
+  string namespaceID = 2;
+  string controllerID = 3;
+  string nodeID = 4;
+
+  // Properties
+  string name = 5;
+  map<string, string> labels = 6;
+  map<string, string> annotations = 7;
+
+  // Resource lifecycle (only when different from cluster window)
+  optional google.protobuf.Timestamp creationTime = 8;
+  optional google.protobuf.Timestamp deletionTime = 9;
+
+  // Usage metrics
+  // CPU usage in core-hours
+  float cpuCoreHours = 10;
+
+  // CPU request average in cores
+  float cpuCoreRequestAverage = 11;
+
+  // CPU usage average in cores
+  float cpuCoreUsageAverage = 12;
+
+  // CPU usage max in cores
+  float cpuCoreUsageMax = 13;
+
+  // RAM usage in byte-hours
+  int64 ramByteHours = 14;
+
+  // RAM request average in bytes
+  int64 ramBytesRequestAverage = 15;
+
+  // RAM usage average in bytes
+  int64 ramBytesUsageAverage = 16;
+
+  // RAM usage max in bytes
+  int64 ramBytesUsageMax = 17;
+
+  // Storage usage in byte-hours
+  int64 storageByteHours = 18;
+
+  // Network transfer bytes sent
+  int64 networkTransferBytes = 19;
+
+  // Network bytes received
+  int64 networkReceiveBytes = 20;
+
+  // Diagnostic information about this resource
+  optional DiagnosticResult diagnostic = 99;
+}

+ 60 - 0
protos/kubemodel/storage.proto

@@ -0,0 +1,60 @@
+syntax = "proto3";
+
+import "kubemodel/diagnostic.proto";
+import "google/protobuf/timestamp.proto";
+
+package kubemodel;
+option go_package = "github.com/opencost/opencost/core/pkg/model/pb/kubemodel";
+
+// Volume represents a persistent volume (provisioned resource)
+message Volume {
+  // Identification
+  string ID = 1;
+  string clusterID = 2;
+
+  // Properties
+  string name = 3;
+  string storageClass = 4;
+  map<string, string> labels = 5;
+  map<string, string> annotations = 6;
+
+ // Resource lifecycle (only when different from cluster window)
+  optional google.protobuf.Timestamp creationTime = 7;
+  optional google.protobuf.Timestamp deletionTime = 8;
+
+  // Usage metrics
+  // Storage capacity in bytes
+  int64 capacityBytes = 9;
+
+  // Diagnostic information about this resource
+  optional DiagnosticResult diagnostic = 99;
+}
+
+// PersistentVolumeClaim represents a PVC (allocated resource) that refers to a Volume
+message PersistentVolumeClaim {
+  // Identification
+  string ID = 1;
+  string namespaceID = 2;
+  optional string volumeID = 3;
+  optional string podID = 4;
+
+  // Properties
+  string name = 5;
+  string storageClass = 6;
+  map<string, string> labels = 7;
+  map<string, string> annotations = 8;
+
+  // Resource lifecycle (only when different from cluster window)
+  optional google.protobuf.Timestamp creationTime = 9;
+  optional google.protobuf.Timestamp deletionTime = 10;
+
+  // Usage metrics
+  // Storage usage in byte-hours
+  int64 storageByteHours = 11;
+
+  // Requested storage capacity in bytes
+  int64 requestedBytes = 12;
+
+  // Diagnostic information about this resource
+  optional DiagnosticResult diagnostic = 99;
+}

+ 2 - 2
protos/model/labels.proto

@@ -2,7 +2,7 @@ syntax = "proto3";
 
 package model;
 
-import "protos/model/window.proto";
+import "model/window.proto";
 
 // Sets the golang package for the protobuf generated code
 option go_package = "github.com/opencost/opencost/core/pkg/model/pb";
@@ -16,7 +16,7 @@ message LabelsResponse {
   string group_id = 2;
 
   // The window for the label sets
-  model.Window window = 3;
+  Window window = 3;
 
   // Mapping of LabelSets for individual items by a unique identifier
   map<string, LabelSet> label_sets = 4;

+ 9 - 2
protos/model/window.proto

@@ -7,10 +7,17 @@ import "google/protobuf/timestamp.proto";
 // Sets the golang package for the protobuf generated code
 option go_package = "github.com/opencost/opencost/core/pkg/model/pb";
 
+// Resolution represents the time granularity for data aggregation
+enum Resolution {
+  RESOLUTION_10M = 0;  // 10 minutes
+  RESOLUTION_1H = 1;   // 1 hour
+  RESOLUTION_1D = 2;   // 1 day
+}
+
 // Window defines a unit of time by a resolution and a start time
 message Window {
-  // resolution in "1h" or "1d" format
-  string resolution = 1;
+  // resolution defines the time granularity
+  Resolution resolution = 1;
   // the start time of the window described
   google.protobuf.Timestamp start = 2;
 }