Jelajahi Sumber

Merge pull request #1403 from nickcurie/nick/pv-right-sizing

PV usage metrics to ETL
Ajay Tripathy 3 tahun lalu
induk
melakukan
1058934d86

+ 241 - 13
pkg/costmodel/cluster.go

@@ -107,17 +107,22 @@ func NewClusterCostsFromCumulative(cpu, gpu, ram, storage float64, window, offse
 }
 
 type Disk struct {
-	Cluster      string
-	Name         string
-	ProviderID   string
-	StorageClass string
-	Cost         float64
-	Bytes        float64
-	Local        bool
-	Start        time.Time
-	End          time.Time
-	Minutes      float64
-	Breakdown    *ClusterCostsBreakdown
+	Cluster        string
+	Name           string
+	ProviderID     string
+	StorageClass   string
+	VolumeName     string
+	ClaimName      string
+	ClaimNamespace string
+	Cost           float64
+	Bytes          float64
+	BytesUsedAvg   float64
+	BytesUsedMax   float64
+	Local          bool
+	Start          time.Time
+	End            time.Time
+	Minutes        float64
+	Breakdown      *ClusterCostsBreakdown
 }
 
 type DiskIdentifier struct {
@@ -159,8 +164,13 @@ func ClusterDisks(client prometheus.Client, provider cloud.Provider, start, end
 	queryPVSize := fmt.Sprintf(`avg(avg_over_time(kube_persistentvolume_capacity_bytes[%s])) by (%s, persistentvolume)`, durStr, env.GetPromClusterLabel())
 	queryActiveMins := fmt.Sprintf(`avg(kube_persistentvolume_capacity_bytes) by (%s, persistentvolume)[%s:%dm]`, env.GetPromClusterLabel(), durStr, minsPerResolution)
 	queryPVStorageClass := fmt.Sprintf(`avg(avg_over_time(kubecost_pv_info[%s])) by (%s, persistentvolume, storageclass)`, durStr, env.GetPromClusterLabel())
+	queryPVUsedAvg := fmt.Sprintf(`avg(avg_over_time(kubelet_volume_stats_used_bytes[%s])) by (%s, persistentvolumeclaim, namespace)`, durStr, env.GetPromClusterLabel())
+	queryPVUsedMax := fmt.Sprintf(`max(max_over_time(kubelet_volume_stats_used_bytes[%s])) by (%s, persistentvolumeclaim, namespace)`, durStr, env.GetPromClusterLabel())
+	queryPVCInfo := fmt.Sprintf(`avg(avg_over_time(kube_persistentvolumeclaim_info[%s])) by (%s, volumename, persistentvolumeclaim, namespace)`, durStr, env.GetPromClusterLabel())
 	queryLocalStorageCost := fmt.Sprintf(`sum_over_time(sum(container_fs_limit_bytes{device!="tmpfs", id="/"}) by (instance, %s)[%s:%dm]) / 1024 / 1024 / 1024 * %f * %f`, env.GetPromClusterLabel(), durStr, minsPerResolution, hourlyToCumulative, costPerGBHr)
 	queryLocalStorageUsedCost := fmt.Sprintf(`sum_over_time(sum(container_fs_usage_bytes{device!="tmpfs", id="/"}) by (instance, %s)[%s:%dm]) / 1024 / 1024 / 1024 * %f * %f`, env.GetPromClusterLabel(), durStr, minsPerResolution, hourlyToCumulative, costPerGBHr)
+	queryLocalStorageUsedAvg := fmt.Sprintf(`avg(avg_over_time(container_fs_usage_bytes{device!="tmpfs", id="/"}[%s])) by (instance, %s)`, durStr, env.GetPromClusterLabel())
+	queryLocalStorageUsedMax := fmt.Sprintf(`max(max_over_time(container_fs_usage_bytes{device!="tmpfs", id="/"}[%s])) by (instance, %s)`, durStr, env.GetPromClusterLabel())
 	queryLocalStorageBytes := fmt.Sprintf(`avg_over_time(sum(container_fs_limit_bytes{device!="tmpfs", id="/"}) by (instance, %s)[%s:%dm])`, env.GetPromClusterLabel(), durStr, minsPerResolution)
 	queryLocalActiveMins := fmt.Sprintf(`count(node_total_hourly_cost) by (%s, node)[%s:%dm]`, env.GetPromClusterLabel(), durStr, minsPerResolution)
 
@@ -168,8 +178,13 @@ func ClusterDisks(client prometheus.Client, provider cloud.Provider, start, end
 	resChPVSize := ctx.QueryAtTime(queryPVSize, t)
 	resChActiveMins := ctx.QueryAtTime(queryActiveMins, t)
 	resChPVStorageClass := ctx.QueryAtTime(queryPVStorageClass, t)
+	resChPVUsedAvg := ctx.QueryAtTime(queryPVUsedAvg, t)
+	resChPVUsedMax := ctx.QueryAtTime(queryPVUsedMax, t)
+	resChPVCInfo := ctx.QueryAtTime(queryPVCInfo, t)
 	resChLocalStorageCost := ctx.QueryAtTime(queryLocalStorageCost, t)
 	resChLocalStorageUsedCost := ctx.QueryAtTime(queryLocalStorageUsedCost, t)
+	resChLocalStoreageUsedAvg := ctx.QueryAtTime(queryLocalStorageUsedAvg, t)
+	resChLocalStoreageUsedMax := ctx.QueryAtTime(queryLocalStorageUsedMax, t)
 	resChLocalStorageBytes := ctx.QueryAtTime(queryLocalStorageBytes, t)
 	resChLocalActiveMins := ctx.QueryAtTime(queryLocalActiveMins, t)
 
@@ -177,8 +192,13 @@ func ClusterDisks(client prometheus.Client, provider cloud.Provider, start, end
 	resPVSize, _ := resChPVSize.Await()
 	resActiveMins, _ := resChActiveMins.Await()
 	resPVStorageClass, _ := resChPVStorageClass.Await()
+	resPVUsedAvg, _ := resChPVUsedAvg.Await()
+	resPVUsedMax, _ := resChPVUsedMax.Await()
+	resPVCInfo, _ := resChPVCInfo.Await()
 	resLocalStorageCost, _ := resChLocalStorageCost.Await()
 	resLocalStorageUsedCost, _ := resChLocalStorageUsedCost.Await()
+	resLocalStorageUsedAvg, _ := resChLocalStoreageUsedAvg.Await()
+	resLocalStorageUsedMax, _ := resChLocalStoreageUsedMax.Await()
 	resLocalStorageBytes, _ := resChLocalStorageBytes.Await()
 	resLocalActiveMins, _ := resChLocalActiveMins.Await()
 
@@ -188,7 +208,43 @@ func ClusterDisks(client prometheus.Client, provider cloud.Provider, start, end
 
 	diskMap := map[DiskIdentifier]*Disk{}
 
-	pvCosts(diskMap, resolution, resActiveMins, resPVSize, resPVCost, provider)
+	for _, result := range resPVCInfo {
+		cluster, err := result.GetString(env.GetPromClusterLabel())
+		if err != nil {
+			cluster = env.GetClusterID()
+		}
+
+		volumeName, err := result.GetString("volumename")
+		if err != nil {
+			log.Warnf("ClusterDisks: pv claim data missing volumename")
+			continue
+		}
+		claimName, err := result.GetString("persistentvolumeclaim")
+		if err != nil {
+			log.Warnf("ClusterDisks: pv claim data missing persistentvolumeclaim")
+			continue
+		}
+		claimNamespace, err := result.GetString("namespace")
+		if err != nil {
+			log.Warnf("ClusterDisks: pv claim data missing namespace")
+			continue
+		}
+
+		key := DiskIdentifier{cluster, volumeName}
+		if _, ok := diskMap[key]; !ok {
+			diskMap[key] = &Disk{
+				Cluster:   cluster,
+				Name:      volumeName,
+				Breakdown: &ClusterCostsBreakdown{},
+			}
+		}
+
+		diskMap[key].VolumeName = volumeName
+		diskMap[key].ClaimName = claimName
+		diskMap[key].ClaimNamespace = claimNamespace
+	}
+
+	pvCosts(diskMap, resolution, resActiveMins, resPVSize, resPVCost, resPVUsedAvg, resPVUsedMax, resPVCInfo, provider)
 
 	for _, result := range resLocalStorageCost {
 		cluster, err := result.GetString(env.GetPromClusterLabel())
@@ -243,6 +299,56 @@ func ClusterDisks(client prometheus.Client, provider cloud.Provider, start, end
 		diskMap[key].Breakdown.System = cost / diskMap[key].Cost
 	}
 
+	for _, result := range resLocalStorageUsedAvg {
+		cluster, err := result.GetString(env.GetPromClusterLabel())
+		if err != nil {
+			cluster = env.GetClusterID()
+		}
+
+		name, err := result.GetString("instance")
+		if err != nil {
+			log.Warnf("ClusterDisks: local storage data missing instance")
+			continue
+		}
+
+		bytesAvg := result.Values[0].Value
+		key := DiskIdentifier{cluster, name}
+		if _, ok := diskMap[key]; !ok {
+			diskMap[key] = &Disk{
+				Cluster:   cluster,
+				Name:      name,
+				Breakdown: &ClusterCostsBreakdown{},
+				Local:     true,
+			}
+		}
+		diskMap[key].BytesUsedAvg = bytesAvg
+	}
+
+	for _, result := range resLocalStorageUsedMax {
+		cluster, err := result.GetString(env.GetPromClusterLabel())
+		if err != nil {
+			cluster = env.GetClusterID()
+		}
+
+		name, err := result.GetString("instance")
+		if err != nil {
+			log.Warnf("ClusterDisks: local storage data missing instance")
+			continue
+		}
+
+		bytesMax := result.Values[0].Value
+		key := DiskIdentifier{cluster, name}
+		if _, ok := diskMap[key]; !ok {
+			diskMap[key] = &Disk{
+				Cluster:   cluster,
+				Name:      name,
+				Breakdown: &ClusterCostsBreakdown{},
+				Local:     true,
+			}
+		}
+		diskMap[key].BytesUsedMax = bytesMax
+	}
+
 	for _, result := range resLocalStorageBytes {
 		cluster, err := result.GetString(env.GetPromClusterLabel())
 		if err != nil {
@@ -1181,7 +1287,7 @@ func ClusterCostsOverTime(cli prometheus.Client, provider cloud.Provider, startS
 	}, nil
 }
 
-func pvCosts(diskMap map[DiskIdentifier]*Disk, resolution time.Duration, resActiveMins, resPVSize, resPVCost []*prom.QueryResult, cp cloud.Provider) {
+func pvCosts(diskMap map[DiskIdentifier]*Disk, resolution time.Duration, resActiveMins, resPVSize, resPVCost, resPVUsedAvg, resPVUsedMax, resPVCInfo []*prom.QueryResult, cp cloud.Provider) {
 	for _, result := range resActiveMins {
 		cluster, err := result.GetString(env.GetPromClusterLabel())
 		if err != nil {
@@ -1291,4 +1397,126 @@ func pvCosts(diskMap map[DiskIdentifier]*Disk, resolution time.Duration, resActi
 			diskMap[key].ProviderID = cloud.ParsePVID(providerID)
 		}
 	}
+
+	for _, result := range resPVUsedAvg {
+		cluster, err := result.GetString(env.GetPromClusterLabel())
+		if err != nil {
+			cluster = env.GetClusterID()
+		}
+
+		claimName, err := result.GetString("persistentvolumeclaim")
+		if err != nil {
+			log.Warnf("ClusterDisks: pv usage data missing persistentvolumeclaim")
+			continue
+		}
+		claimNamespace, err := result.GetString("namespace")
+		if err != nil {
+			log.Warnf("ClusterDisks: pv usage data missing namespace")
+			continue
+		}
+
+		var volumeName string
+
+		for _, thatRes := range resPVCInfo {
+
+			thatCluster, err := thatRes.GetString(env.GetPromClusterLabel())
+			if err != nil {
+				thatCluster = env.GetClusterID()
+			}
+
+			thatVolumeName, err := thatRes.GetString("volumename")
+			if err != nil {
+				log.Warnf("ClusterDisks: pv claim data missing volumename")
+				continue
+			}
+			thatClaimName, err := thatRes.GetString("persistentvolumeclaim")
+			if err != nil {
+				log.Warnf("ClusterDisks: pv claim data missing persistentvolumeclaim")
+				continue
+			}
+			thatClaimNamespace, err := thatRes.GetString("namespace")
+			if err != nil {
+				log.Warnf("ClusterDisks: pv claim data missing namespace")
+				continue
+			}
+
+			if cluster == thatCluster && claimName == thatClaimName && claimNamespace == thatClaimNamespace {
+				volumeName = thatVolumeName
+			}
+		}
+
+		usage := result.Values[0].Value
+
+		key := DiskIdentifier{cluster, volumeName}
+
+		if _, ok := diskMap[key]; !ok {
+			diskMap[key] = &Disk{
+				Cluster:   cluster,
+				Name:      volumeName,
+				Breakdown: &ClusterCostsBreakdown{},
+			}
+		}
+		diskMap[key].BytesUsedAvg = usage
+	}
+
+	for _, result := range resPVUsedMax {
+		cluster, err := result.GetString(env.GetPromClusterLabel())
+		if err != nil {
+			cluster = env.GetClusterID()
+		}
+
+		claimName, err := result.GetString("persistentvolumeclaim")
+		if err != nil {
+			log.Warnf("ClusterDisks: pv usage data missing persistentvolumeclaim")
+			continue
+		}
+		claimNamespace, err := result.GetString("namespace")
+		if err != nil {
+			log.Warnf("ClusterDisks: pv usage data missing namespace")
+			continue
+		}
+
+		var volumeName string
+
+		for _, thatRes := range resPVCInfo {
+
+			thatCluster, err := thatRes.GetString(env.GetPromClusterLabel())
+			if err != nil {
+				thatCluster = env.GetClusterID()
+			}
+
+			thatVolumeName, err := thatRes.GetString("volumename")
+			if err != nil {
+				log.Warnf("ClusterDisks: pv claim data missing volumename")
+				continue
+			}
+			thatClaimName, err := thatRes.GetString("persistentvolumeclaim")
+			if err != nil {
+				log.Warnf("ClusterDisks: pv claim data missing persistentvolumeclaim")
+				continue
+			}
+			thatClaimNamespace, err := thatRes.GetString("namespace")
+			if err != nil {
+				log.Warnf("ClusterDisks: pv claim data missing namespace")
+				continue
+			}
+
+			if cluster == thatCluster && claimName == thatClaimName && claimNamespace == thatClaimNamespace {
+				volumeName = thatVolumeName
+			}
+		}
+
+		usage := result.Values[0].Value
+
+		key := DiskIdentifier{cluster, volumeName}
+
+		if _, ok := diskMap[key]; !ok {
+			diskMap[key] = &Disk{
+				Cluster:   cluster,
+				Name:      volumeName,
+				Breakdown: &ClusterCostsBreakdown{},
+			}
+		}
+		diskMap[key].BytesUsedMax = usage
+	}
 }

+ 58 - 1
pkg/costmodel/cluster_helpers_test.go

@@ -936,6 +936,63 @@ func TestAssetCustompricing(t *testing.T) {
 		},
 	}
 
+	pvAvgUsagePromResult := []*prom.QueryResult{
+		{
+			Metric: map[string]interface{}{
+				"cluster_id":            "cluster1",
+				"persistentvolumeclaim": "pv-claim1",
+				"namespace":             "ns1",
+			},
+			Values: []*util.Vector{
+				&util.Vector{
+					Timestamp: 0,
+					Value:     1.0,
+				},
+				&util.Vector{
+					Timestamp: 3600.0,
+					Value:     1.0,
+				},
+			},
+		},
+	}
+
+	pvMaxUsagePromResult := []*prom.QueryResult{
+		{
+			Metric: map[string]interface{}{
+				"cluster_id":            "cluster1",
+				"persistentvolumeclaim": "pv-claim1",
+				"namespace":             "ns1",
+			},
+			Values: []*util.Vector{
+				&util.Vector{
+					Timestamp: 0,
+					Value:     1.0,
+				},
+				&util.Vector{
+					Timestamp: 3600.0,
+					Value:     1.0,
+				},
+			},
+		},
+	}
+
+	pvInfoPromResult := []*prom.QueryResult{
+		{
+			Metric: map[string]interface{}{
+				"cluster_id":            "cluster1",
+				"persistentvolumeclaim": "pv-claim1",
+				"volumename":            "pvc1",
+				"namespace":             "ns1",
+			},
+			Values: []*util.Vector{
+				&util.Vector{
+					Timestamp: 0,
+					Value:     1.0,
+				},
+			},
+		},
+	}
+
 	gpuCountMap := map[NodeIdentifier]float64{
 		NodeIdentifier{
 			Cluster:    "cluster1",
@@ -1000,7 +1057,7 @@ func TestAssetCustompricing(t *testing.T) {
 			gpuResult := gpuMap[nodeKey]
 
 			diskMap := map[DiskIdentifier]*Disk{}
-			pvCosts(diskMap, time.Hour, pvMinsPromResult, pvSizePromResult, pvCostPromResult, testProvider)
+			pvCosts(diskMap, time.Hour, pvMinsPromResult, pvSizePromResult, pvCostPromResult, pvAvgUsagePromResult, pvMaxUsagePromResult, pvInfoPromResult, testProvider)
 
 			diskResult := diskMap[DiskIdentifier{"cluster1", "pvc1"}].Cost
 

+ 71 - 22
pkg/kubecost/asset.go

@@ -1061,17 +1061,22 @@ func (cm *ClusterManagement) String() string {
 
 // Disk represents an in-cluster disk Asset
 type Disk struct {
-	Labels       AssetLabels
-	Properties   *AssetProperties
-	Start        time.Time
-	End          time.Time
-	Window       Window
-	Adjustment   float64
-	Cost         float64
-	ByteHours    float64
-	Local        float64
-	Breakdown    *Breakdown
-	StorageClass string // @bingen:field[version=17]
+	Labels         AssetLabels
+	Properties     *AssetProperties
+	Start          time.Time
+	End            time.Time
+	Window         Window
+	Adjustment     float64
+	Cost           float64
+	ByteHours      float64
+	Local          float64
+	Breakdown      *Breakdown
+	StorageClass   string   // @bingen:field[version=17]
+	ByteHoursUsed  float64  // @bingen:field[version=18]
+	ByteUsageMax   *float64 // @bingen:field[version=18]
+	VolumeName     string   // @bingen:field[version=18]
+	ClaimName      string   // @bingen:field[version=18]
+	ClaimNamespace string   // @bingen:field[version=18]
 }
 
 // NewDisk creates and returns a new Disk Asset
@@ -1263,27 +1268,50 @@ func (d *Disk) add(that *Disk) {
 	d.Cost += that.Cost
 
 	d.ByteHours += that.ByteHours
+	d.ByteHoursUsed += that.ByteHoursUsed
+	d.ByteUsageMax = nil
 
 	// If storage class don't match default it to empty storage class
 	if d.StorageClass != that.StorageClass {
 		d.StorageClass = ""
 	}
+
+	if d.VolumeName != that.VolumeName {
+		d.VolumeName = ""
+	}
+	if d.ClaimName != that.ClaimName {
+		d.ClaimName = ""
+	}
+	if d.ClaimNamespace != that.ClaimNamespace {
+		d.ClaimNamespace = ""
+	}
 }
 
 // Clone returns a cloned instance of the Asset
 func (d *Disk) Clone() Asset {
+	var max *float64
+	if d.ByteUsageMax != nil {
+		copied := *d.ByteUsageMax
+		max = &copied
+	}
+
 	return &Disk{
-		Properties:   d.Properties.Clone(),
-		Labels:       d.Labels.Clone(),
-		Start:        d.Start,
-		End:          d.End,
-		Window:       d.Window.Clone(),
-		Adjustment:   d.Adjustment,
-		Cost:         d.Cost,
-		ByteHours:    d.ByteHours,
-		Local:        d.Local,
-		Breakdown:    d.Breakdown.Clone(),
-		StorageClass: d.StorageClass,
+		Properties:     d.Properties.Clone(),
+		Labels:         d.Labels.Clone(),
+		Start:          d.Start,
+		End:            d.End,
+		Window:         d.Window.Clone(),
+		Adjustment:     d.Adjustment,
+		Cost:           d.Cost,
+		ByteHours:      d.ByteHours,
+		ByteHoursUsed:  d.ByteHoursUsed,
+		ByteUsageMax:   max,
+		Local:          d.Local,
+		Breakdown:      d.Breakdown.Clone(),
+		StorageClass:   d.StorageClass,
+		VolumeName:     d.VolumeName,
+		ClaimName:      d.ClaimName,
+		ClaimNamespace: d.ClaimNamespace,
 	}
 }
 
@@ -1318,6 +1346,18 @@ func (d *Disk) Equal(a Asset) bool {
 	if d.ByteHours != that.ByteHours {
 		return false
 	}
+	if d.ByteHoursUsed != that.ByteHoursUsed {
+		return false
+	}
+	if d.ByteUsageMax != nil && that.ByteUsageMax == nil {
+		return false
+	}
+	if d.ByteUsageMax == nil && that.ByteUsageMax != nil {
+		return false
+	}
+	if (d.ByteUsageMax != nil && that.ByteUsageMax != nil) && *d.ByteUsageMax != *that.ByteUsageMax {
+		return false
+	}
 	if d.Local != that.Local {
 		return false
 	}
@@ -1327,6 +1367,15 @@ func (d *Disk) Equal(a Asset) bool {
 	if d.StorageClass != that.StorageClass {
 		return false
 	}
+	if d.VolumeName != that.VolumeName {
+		return false
+	}
+	if d.ClaimName != that.ClaimName {
+		return false
+	}
+	if d.ClaimNamespace != that.ClaimNamespace {
+		return false
+	}
 
 	return true
 }

+ 30 - 1
pkg/kubecost/asset_json.go

@@ -259,10 +259,19 @@ func (d *Disk) MarshalJSON() ([]byte, error) {
 	jsonEncodeFloat64(buffer, "minutes", d.Minutes(), ",")
 	jsonEncodeFloat64(buffer, "byteHours", d.ByteHours, ",")
 	jsonEncodeFloat64(buffer, "bytes", d.Bytes(), ",")
+	jsonEncodeFloat64(buffer, "byteHoursUsed", d.ByteHoursUsed, ",")
+	if d.ByteUsageMax == nil {
+		jsonEncode(buffer, "byteUsageMax", nil, ",")
+	} else {
+		jsonEncodeFloat64(buffer, "byteUsageMax", *d.ByteUsageMax, ",")
+	}
 	jsonEncode(buffer, "breakdown", d.Breakdown, ",")
 	jsonEncodeFloat64(buffer, "adjustment", d.Adjustment, ",")
 	jsonEncodeFloat64(buffer, "totalCost", d.TotalCost(), ",")
-	jsonEncodeString(buffer, "storageClass", d.StorageClass, "")
+	jsonEncodeString(buffer, "storageClass", d.StorageClass, ",")
+	jsonEncodeString(buffer, "volumeName", d.VolumeName, ",")
+	jsonEncodeString(buffer, "claimName", d.ClaimName, ",")
+	jsonEncodeString(buffer, "claimNamespace", d.ClaimNamespace, "")
 	buffer.WriteString("}")
 	return buffer.Bytes(), nil
 }
@@ -332,10 +341,30 @@ func (d *Disk) InterfaceToDisk(itf interface{}) error {
 	if ByteHours, err := getTypedVal(fmap["byteHours"]); err == nil {
 		d.ByteHours = ByteHours.(float64)
 	}
+	if ByteHoursUsed, err := getTypedVal(fmap["byteHoursUsed"]); err == nil {
+		d.ByteHoursUsed = ByteHoursUsed.(float64)
+	}
+	if ByteUsageMax, err := getTypedVal(fmap["byteUsageMax"]); err == nil {
+		if ByteUsageMax == nil {
+			d.ByteUsageMax = nil
+		} else {
+			max := ByteUsageMax.(float64)
+			d.ByteUsageMax = &max
+		}
+	}
 
 	if StorageClass, err := getTypedVal(fmap["storageClass"]); err == nil {
 		d.StorageClass = StorageClass.(string)
 	}
+	if VolumeName, err := getTypedVal(fmap["volumeName"]); err == nil {
+		d.VolumeName = VolumeName.(string)
+	}
+	if ClaimName, err := getTypedVal(fmap["claimName"]); err == nil {
+		d.ClaimName = ClaimName.(string)
+	}
+	if ClaimNamespace, err := getTypedVal(fmap["claimNamespace"]); err == nil {
+		d.ClaimNamespace = ClaimNamespace.(string)
+	}
 
 	// d.Local is not marhsaled, and cannot be calculated from marshaled values.
 	// Currently, it is just ignored and not set in the resulting unmarshal to Disk

+ 41 - 0
pkg/kubecost/asset_json_test.go

@@ -164,6 +164,9 @@ func TestDisk_Unmarshal(t *testing.T) {
 
 	disk1 := NewDisk("disk1", "cluster1", "disk1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
 	disk1.ByteHours = 60.0 * gb * hours
+	disk1.ByteHoursUsed = 40.0 * gb * hours
+	max := 50.0 * gb * hours
+	disk1.ByteUsageMax = &max
 	disk1.Cost = 4.0
 	disk1.Local = 1.0
 	disk1.SetAdjustment(1.0)
@@ -211,6 +214,12 @@ func TestDisk_Unmarshal(t *testing.T) {
 	if disk1.ByteHours != disk2.ByteHours {
 		t.Fatalf("Disk Unmarshal: ByteHours mutated in unmarshal")
 	}
+	if disk1.ByteHoursUsed != disk2.ByteHoursUsed {
+		t.Fatalf("Disk Unmarshal: ByteHoursUsed mutated in unmarshal")
+	}
+	if *disk1.ByteUsageMax != *disk2.ByteUsageMax {
+		t.Fatalf("Disk Unmarshal: ByteUsageMax mutated in unmarshal")
+	}
 	if disk1.Cost != disk2.Cost {
 		t.Fatalf("Disk Unmarshal: cost mutated in unmarshal")
 	}
@@ -220,6 +229,38 @@ func TestDisk_Unmarshal(t *testing.T) {
 	// it is also ignored in this test; be aware that this means a resulting Disk from an
 	// unmarshal is therefore NOT equal to the originally marshaled Disk.
 
+	disk3 := NewDisk("disk3", "cluster1", "disk3", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+
+	disk3.ByteHours = 60.0 * gb * hours
+	disk3.ByteHoursUsed = 40.0 * gb * hours
+	disk3.ByteUsageMax = nil
+	disk3.Cost = 4.0
+	disk3.Local = 1.0
+	disk3.SetAdjustment(1.0)
+	disk3.Breakdown = &Breakdown{
+		Idle:   0.1,
+		System: 0.2,
+		User:   0.3,
+		Other:  0.4,
+	}
+
+	bytes, _ = json.Marshal(disk3)
+
+	var testdisk2 Disk
+	disk4 := &testdisk2
+
+	err = json.Unmarshal(bytes, disk4)
+
+	// Check if unmarshal was successful
+	if err != nil {
+		t.Fatalf("Disk Unmarshal: unexpected error: %s", err)
+	}
+
+	// Check that both disks have nil max usage
+	if disk3.ByteUsageMax != disk4.ByteUsageMax {
+		t.Fatalf("Disk Unmarshal: ByteUsageMax mutated in unmarshal")
+	}
+
 }
 
 func TestNetwork_Unmarshal(t *testing.T) {

+ 1 - 1
pkg/kubecost/bingen.go

@@ -24,7 +24,7 @@ package kubecost
 // @bingen:generate:Window
 
 // Asset Version Set: Includes Asset pipeline specific resources
-// @bingen:set[name=Assets,version=17]
+// @bingen:set[name=Assets,version=18]
 // @bingen:generate:Any
 // @bingen:generate:Asset
 // @bingen:generate:AssetLabels

+ 105 - 7
pkg/kubecost/kubecost_codecs.go

@@ -13,11 +13,12 @@ package kubecost
 
 import (
 	"fmt"
-	util "github.com/opencost/opencost/pkg/util"
 	"reflect"
 	"strings"
 	"sync"
 	"time"
+
+	util "github.com/opencost/opencost/pkg/util"
 )
 
 const (
@@ -33,17 +34,17 @@ const (
 )
 
 const (
-	// DefaultCodecVersion is used for any resources listed in the Default version set
-	DefaultCodecVersion uint8 = 15
-
-	// AssetsCodecVersion is used for any resources listed in the Assets version set
-	AssetsCodecVersion uint8 = 17
-
 	// AllocationCodecVersion is used for any resources listed in the Allocation version set
 	AllocationCodecVersion uint8 = 15
 
 	// AuditCodecVersion is used for any resources listed in the Audit version set
 	AuditCodecVersion uint8 = 1
+
+	// DefaultCodecVersion is used for any resources listed in the Default version set
+	DefaultCodecVersion uint8 = 15
+
+	// AssetsCodecVersion is used for any resources listed in the Assets version set
+	AssetsCodecVersion uint8 = 18
 )
 
 //--------------------------------------------------------------------------
@@ -4977,6 +4978,32 @@ func (target *Disk) MarshalBinaryWithContext(ctx *EncodingContext) (err error) {
 	} else {
 		buff.WriteString(target.StorageClass) // write string
 	}
+	buff.WriteFloat64(target.ByteHoursUsed) // write float64
+	if target.ByteUsageMax == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		buff.WriteFloat64(*target.ByteUsageMax) // write float64
+	}
+	if ctx.IsStringTable() {
+		f := ctx.Table.AddOrGet(target.VolumeName)
+		buff.WriteInt(f) // write table index
+	} else {
+		buff.WriteString(target.VolumeName) // write string
+	}
+	if ctx.IsStringTable() {
+		g := ctx.Table.AddOrGet(target.ClaimName)
+		buff.WriteInt(g) // write table index
+	} else {
+		buff.WriteString(target.ClaimName) // write string
+	}
+	if ctx.IsStringTable() {
+		h := ctx.Table.AddOrGet(target.ClaimNamespace)
+		buff.WriteInt(h) // write table index
+	} else {
+		buff.WriteString(target.ClaimNamespace) // write string
+	}
 	return nil
 }
 
@@ -5162,6 +5189,77 @@ func (target *Disk) UnmarshalBinaryWithContext(ctx *DecodingContext) (err error)
 		target.StorageClass = "" // default
 	}
 
+	// field version check
+	if uint8(18) <= version {
+		dd := buff.ReadFloat64() // read float64
+		target.ByteHoursUsed = dd
+
+	} else {
+		target.ByteHoursUsed = float64(0) // default
+	}
+
+	// field version check
+	if uint8(18) <= version {
+		if buff.ReadUInt8() == uint8(0) {
+			target.ByteUsageMax = nil
+		} else {
+			ee := buff.ReadFloat64() // read float64
+			target.ByteUsageMax = &ee
+
+		}
+	} else {
+		target.ByteUsageMax = nil
+
+	}
+
+	// field version check
+	if uint8(18) <= version {
+		var gg string
+		if ctx.IsStringTable() {
+			hh := buff.ReadInt() // read string index
+			gg = ctx.Table[hh]
+		} else {
+			gg = buff.ReadString() // read string
+		}
+		ff := gg
+		target.VolumeName = ff
+
+	} else {
+		target.VolumeName = "" // default
+	}
+
+	// field version check
+	if uint8(18) <= version {
+		var ll string
+		if ctx.IsStringTable() {
+			mm := buff.ReadInt() // read string index
+			ll = ctx.Table[mm]
+		} else {
+			ll = buff.ReadString() // read string
+		}
+		kk := ll
+		target.ClaimName = kk
+
+	} else {
+		target.ClaimName = "" // default
+	}
+
+	// field version check
+	if uint8(18) <= version {
+		var oo string
+		if ctx.IsStringTable() {
+			pp := buff.ReadInt() // read string index
+			oo = ctx.Table[pp]
+		} else {
+			oo = buff.ReadString() // read string
+		}
+		nn := oo
+		target.ClaimNamespace = nn
+
+	} else {
+		target.ClaimNamespace = "" // default
+	}
+
 	return nil
 }