فهرست منبع

Create Response types to protect SummaryAllocation types against errors marshaling NaN

Signed-off-by: Niko Kovacevic <nikovacevic@gmail.com>
Niko Kovacevic 3 سال پیش
والد
کامیت
903658f2d1
2فایلهای تغییر یافته به همراه245 افزوده شده و 0 حذف شده
  1. 116 0
      pkg/kubecost/summaryallocation_json.go
  2. 129 0
      pkg/kubecost/summaryallocation_json_test.go

+ 116 - 0
pkg/kubecost/summaryallocation_json.go

@@ -0,0 +1,116 @@
+package kubecost
+
+import (
+	"math"
+	"time"
+)
+
+// SummaryAllocationResponse is a sanitized version of SummaryAllocation, which
+// formats fields and protects against issues like mashaling NaNs.
+type SummaryAllocationResponse struct {
+	Name                   string    `json:"name"`
+	Start                  time.Time `json:"start"`
+	End                    time.Time `json:"end"`
+	CPUCoreRequestAverage  *float64  `json:"cpuCoreRequestAverage"`
+	CPUCoreUsageAverage    *float64  `json:"cpuCoreUsageAverage"`
+	CPUCost                *float64  `json:"cpuCost"`
+	GPUCost                *float64  `json:"gpuCost"`
+	NetworkCost            *float64  `json:"networkCost"`
+	LoadBalancerCost       *float64  `json:"loadBalancerCost"`
+	PVCost                 *float64  `json:"pvCost"`
+	RAMBytesRequestAverage *float64  `json:"ramByteRequestAverage"`
+	RAMBytesUsageAverage   *float64  `json:"ramByteUsageAverage"`
+	RAMCost                *float64  `json:"ramCost"`
+	SharedCost             *float64  `json:"sharedCost"`
+	ExternalCost           *float64  `json:"externalCost"`
+}
+
+// ToResponse converts a SummaryAllocation to a SummaryAllocationResponse,
+// protecting against NaN and null values.
+func (sa *SummaryAllocation) ToResponse() *SummaryAllocationResponse {
+	if sa == nil {
+		return nil
+	}
+
+	return &SummaryAllocationResponse{
+		Name:                   sa.Name,
+		Start:                  sa.Start,
+		End:                    sa.End,
+		CPUCoreRequestAverage:  float64ToResponse(sa.CPUCoreRequestAverage),
+		CPUCoreUsageAverage:    float64ToResponse(sa.CPUCoreUsageAverage),
+		CPUCost:                float64ToResponse(sa.CPUCost),
+		GPUCost:                float64ToResponse(sa.GPUCost),
+		NetworkCost:            float64ToResponse(sa.NetworkCost),
+		LoadBalancerCost:       float64ToResponse(sa.LoadBalancerCost),
+		PVCost:                 float64ToResponse(sa.PVCost),
+		RAMBytesRequestAverage: float64ToResponse(sa.RAMBytesRequestAverage),
+		RAMBytesUsageAverage:   float64ToResponse(sa.RAMBytesUsageAverage),
+		RAMCost:                float64ToResponse(sa.RAMCost),
+		SharedCost:             float64ToResponse(sa.SharedCost),
+		ExternalCost:           float64ToResponse(sa.ExternalCost),
+	}
+}
+
+func float64ToResponse(f float64) *float64 {
+	if math.IsNaN(f) || math.IsInf(f, 0) {
+		return nil
+	}
+
+	return &f
+}
+
+// SummaryAllocationSetResponse is a sanitized version of SummaryAllocationSet,
+// which formats fields and protects against issues like marshaling NaNs.
+type SummaryAllocationSetResponse struct {
+	SummaryAllocations map[string]*SummaryAllocationResponse `json:"allocations"`
+	Window             Window                                `json:"window"`
+}
+
+// ToResponse converts a SummaryAllocationSet to a SummaryAllocationSetResponse,
+// protecting against NaN and null values.
+func (sas *SummaryAllocationSet) ToResponse() *SummaryAllocationSetResponse {
+	if sas == nil {
+		return nil
+	}
+
+	sars := make(map[string]*SummaryAllocationResponse, len(sas.SummaryAllocations))
+	for k, v := range sas.SummaryAllocations {
+		sars[k] = v.ToResponse()
+	}
+
+	return &SummaryAllocationSetResponse{
+		SummaryAllocations: sars,
+		Window:             sas.Window.Clone(),
+	}
+}
+
+// SummaryAllocationSetRangeResponse is a sanitized version of SummaryAllocationSetRange,
+// which formats fields and protects against issues like marshaling NaNs.
+type SummaryAllocationSetRangeResponse struct {
+	Step                  time.Duration                   `json:"step"`
+	SummaryAllocationSets []*SummaryAllocationSetResponse `json:"sets"`
+	Window                Window                          `json:"window"`
+}
+
+// ToResponse converts a SummaryAllocationSet to a SummaryAllocationSetResponse,
+// protecting against NaN and null values.
+func (sasr *SummaryAllocationSetRange) ToResponse() *SummaryAllocationSetRangeResponse {
+	if sasr == nil {
+		return nil
+	}
+
+	sasrr := make([]*SummaryAllocationSetResponse, len(sasr.SummaryAllocationSets))
+	for i, v := range sasr.SummaryAllocationSets {
+		sasrr[i] = v.ToResponse()
+	}
+
+	return &SummaryAllocationSetRangeResponse{
+		Step:                  sasr.Step,
+		SummaryAllocationSets: sasrr,
+		Window:                sasr.Window.Clone(),
+	}
+}
+
+func EmptySummaryAllocationSetRangeResponse() *SummaryAllocationSetRangeResponse {
+	return &SummaryAllocationSetRangeResponse{}
+}

+ 129 - 0
pkg/kubecost/summaryallocation_json_test.go

@@ -0,0 +1,129 @@
+package kubecost
+
+import (
+	"encoding/json"
+	"math"
+	"testing"
+	"time"
+
+	"github.com/opencost/opencost/pkg/util/timeutil"
+)
+
+func TestSummaryAllocationSetRangeResponse_MarshalJSON(t *testing.T) {
+	// Set a 1-day (start, end)
+	s := time.Date(2023, time.March, 13, 0, 0, 0, 0, time.UTC)
+	e := s.Add(timeutil.Day)
+
+	// Set some basic numbers that can be used to asset accuracy later
+	adjustment := -0.14
+	bytes := 2183471523842.00
+	cores := 2.78
+	cost := 12.50
+	gpus := 1.00
+
+	// Test a normal allocation for numerical accuracy
+	alloc := &Allocation{
+		Name: "cluster/node/namespace/pod/alloc",
+		Properties: &AllocationProperties{
+			Cluster:   "cluster",
+			Node:      "node",
+			Namespace: "namespace",
+			Pod:       "pod",
+			Container: "alloc",
+		},
+		CPUCoreHours:               cores,
+		CPUCoreRequestAverage:      cores,
+		CPUCoreUsageAverage:        cores,
+		CPUCost:                    cost,
+		CPUCostAdjustment:          adjustment,
+		GPUHours:                   gpus,
+		GPUCost:                    cost,
+		GPUCostAdjustment:          adjustment,
+		NetworkTransferBytes:       bytes,
+		NetworkReceiveBytes:        bytes,
+		NetworkCost:                cost,
+		NetworkCrossZoneCost:       cost,
+		NetworkCrossRegionCost:     cost,
+		NetworkInternetCost:        cost,
+		NetworkCostAdjustment:      adjustment,
+		LoadBalancerCost:           cost,
+		LoadBalancerCostAdjustment: adjustment,
+		PVs: PVAllocations{
+			PVKey{Cluster: "cluster", Name: "pv"}: &PVAllocation{
+				ByteHours: bytes,
+				Cost:      cost,
+			},
+		},
+		PVCostAdjustment:       adjustment,
+		RAMByteHours:           bytes,
+		RAMBytesRequestAverage: bytes,
+		RAMBytesUsageAverage:   bytes,
+		RAMCost:                cost,
+		RAMCostAdjustment:      adjustment,
+		SharedCost:             cost,
+		ExternalCost:           cost,
+	}
+
+	// Test an allocation with NaN values for JSON marshal errors
+	allocWithNaN := &Allocation{
+		Name: "cluster/node/namespace/pod/nan",
+		Properties: &AllocationProperties{
+			Cluster:   "cluster",
+			Node:      "node",
+			Namespace: "namespace",
+			Pod:       "pod",
+			Container: "nan",
+		},
+		CPUCoreHours:               math.NaN(),
+		CPUCoreRequestAverage:      math.NaN(),
+		CPUCoreUsageAverage:        math.NaN(),
+		CPUCost:                    math.NaN(),
+		CPUCostAdjustment:          math.NaN(),
+		GPUHours:                   gpus,
+		GPUCost:                    cost,
+		GPUCostAdjustment:          adjustment,
+		NetworkTransferBytes:       bytes,
+		NetworkReceiveBytes:        bytes,
+		NetworkCost:                cost,
+		NetworkCrossZoneCost:       cost,
+		NetworkCrossRegionCost:     cost,
+		NetworkInternetCost:        cost,
+		NetworkCostAdjustment:      adjustment,
+		LoadBalancerCost:           cost,
+		LoadBalancerCostAdjustment: adjustment,
+		PVs: PVAllocations{
+			PVKey{Cluster: "cluster", Name: "pv"}: &PVAllocation{
+				ByteHours: bytes,
+				Cost:      cost,
+			},
+		},
+		PVCostAdjustment:       adjustment,
+		RAMByteHours:           bytes,
+		RAMBytesRequestAverage: bytes,
+		RAMBytesUsageAverage:   bytes,
+		RAMCost:                cost,
+		RAMCostAdjustment:      adjustment,
+		SharedCost:             cost,
+		ExternalCost:           cost,
+	}
+
+	// Convert to SummaryAllocationSetRange
+	as := NewAllocationSet(s, e, alloc, allocWithNaN)
+	sas := NewSummaryAllocationSet(as, nil, nil, true, true)
+	sasr := NewSummaryAllocationSetRange(sas)
+
+	// Confirm that SummaryAllocationSetRange does error because on NaN
+	_, err := json.Marshal(sasr)
+	if err == nil {
+		t.Fatalf("expected NaN values to cause error")
+	}
+
+	// Convert to response
+	sasrr := sasr.ToResponse()
+
+	// Confirm that same SummaryAllocationSetRangeResponse does NOT error
+	_, err = json.Marshal(sasrr)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+}