Quellcode durchsuchen

Proportional load bal costs (#2)

* experimenting with load balancer cost attribution

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* start tracking load balancer allocations

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* bingen updates

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* re-gen codecs

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* code clean up

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* re-gen codecs for load balancer

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* wip proportional load balancer issues

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* also add accumulated totals into map

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* wip loadbal costs

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* fixed bad merge, begin using asset totals store

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* debugging/clean up

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* fix tests

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* support azure's single load balancer per cluster

* provide interpolation for values, test on load balancer

* fix tests

* provide backwards compat for functionality

---------

Signed-off-by: Alex Meijer <ameijer@kubecost.com>
Alex Meijer vor 2 Jahren
Ursprung
Commit
7bf2a6b54f

+ 1 - 11
pkg/costmodel/aggregation.go

@@ -2267,7 +2267,7 @@ func (a *Accesses) ComputeAllocationHandler(w http.ResponseWriter, r *http.Reque
 	// include aggregated labels/annotations if true
 	includeAggregatedMetadata := qp.GetBool("includeAggregatedMetadata", false)
 
-	asr, err := a.Model.QueryAllocation(window, resolution, step, aggregateBy, includeIdle, idleByNode, includeProportionalAssetResourceCosts, includeAggregatedMetadata)
+	asr, err := a.Model.QueryAllocation(window, resolution, step, aggregateBy, includeIdle, idleByNode, includeProportionalAssetResourceCosts, includeAggregatedMetadata, accumulateBy)
 	if err != nil {
 		if strings.Contains(strings.ToLower(err.Error()), "bad request") {
 			WriteError(w, BadRequest(err.Error()))
@@ -2278,16 +2278,6 @@ func (a *Accesses) ComputeAllocationHandler(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
-	// Accumulate, if requested
-	if accumulateBy != kubecost.AccumulateOptionNone {
-		asr, err = asr.Accumulate(accumulateBy)
-		if err != nil {
-			log.Errorf("error accumulating by %v: %s", accumulateBy, err)
-			WriteError(w, InternalServerError(fmt.Errorf("error accumulating by %v: %s", accumulateBy, err).Error()))
-			return
-		}
-	}
-
 	w.Write(WrapData(asr, nil))
 }
 

+ 1 - 1
pkg/costmodel/allocation.go

@@ -55,7 +55,7 @@ const (
 	queryFmtPodsWithReplicaSetOwner     = `sum(avg_over_time(kube_pod_owner{owner_kind="ReplicaSet", %s}[%s])) by (pod, owner_name, namespace ,%s)`
 	queryFmtReplicaSetsWithoutOwners    = `avg(avg_over_time(kube_replicaset_owner{owner_kind="<none>", owner_name="<none>", %s}[%s])) by (replicaset, namespace, %s)`
 	queryFmtReplicaSetsWithRolloutOwner = `avg(avg_over_time(kube_replicaset_owner{owner_kind="Rollout", %s}[%s])) by (replicaset, namespace, owner_kind, owner_name, %s)`
-	queryFmtLBCostPerHr                 = `avg(avg_over_time(kubecost_load_balancer_cost{%s}[%s])) by (namespace, service_name, %s)`
+	queryFmtLBCostPerHr                 = `avg(avg_over_time(kubecost_load_balancer_cost{%s}[%s])) by (namespace, service_name, ingress_ip, %s)`
 	queryFmtLBActiveMins                = `count(kubecost_load_balancer_cost{%s}) by (namespace, service_name, %s)[%s:%s]`
 	queryFmtOldestSample                = `min_over_time(timestamp(group(node_cpu_hourly_cost{%s}))[%s:%s])`
 	queryFmtNewestSample                = `max_over_time(timestamp(group(node_cpu_hourly_cost{%s}))[%s:%s])`

+ 49 - 9
pkg/costmodel/allocation_helpers.go

@@ -960,7 +960,7 @@ func applyLabels(podMap map[podKey]*pod, nodeLabels map[nodeKey]map[string]strin
 
 			alloc.Properties.Labels = allocLabels
 			alloc.Properties.NamespaceLabels = nsLabels
-			
+
 		}
 	}
 }
@@ -1342,7 +1342,8 @@ func getLoadBalancerCosts(lbMap map[serviceKey]*lbCost, resLBCost, resLBActiveMi
 			continue
 		}
 
-		lbStart, lbEnd := calculateStartAndEnd(res, resolution)
+		// load balancers have interpolation for costs, we don't need to offset the resolution
+		lbStart, lbEnd := calculateStartAndEnd(res, resolution, false)
 		if lbStart.IsZero() || lbEnd.IsZero() {
 			log.Warnf("CostModel.ComputeAllocation: pvc %s has no running time", serviceKey)
 		}
@@ -1358,11 +1359,32 @@ func getLoadBalancerCosts(lbMap map[serviceKey]*lbCost, resLBCost, resLBActiveMi
 		if err != nil {
 			continue
 		}
+
+		// get the ingress IP to determine if this is a private LB
+		ip, err := res.GetString("ingress_ip")
+		if err != nil {
+			log.Warnf("error getting ingress ip for key %s: %v, skipping", serviceKey, err)
+			// do not count the time that the service was being created or deleted
+			// ingress IP will be empty string
+			// only add cost to allocation when external IP is provisioned
+			if ip == "" {
+				continue
+			}
+		}
+
 		// Apply cost as price-per-hour * hours
 		if lb, ok := lbMap[serviceKey]; ok {
 			lbPricePerHr := res.Values[0].Value
-			hours := lb.End.Sub(lb.Start).Hours()
-			lb.TotalCost += lbPricePerHr * hours
+			// interpolate any missing data
+			resolutionHours := resolution.Hours()
+			resultHours := lb.End.Sub(lb.Start).Hours()
+			scaleFactor := (resolutionHours + resultHours) / resultHours
+
+			// after scaling, we can adjust the timings to reflect the interpolated data
+			lb.End = lb.End.Add(resolution)
+
+			lb.TotalCost += lbPricePerHr * resultHours * scaleFactor
+			lb.Private = privateIPCheck(ip)
 		} else {
 			log.DedupedWarningf(20, "CostModel: found minutes for key that does not exist: %s", serviceKey)
 		}
@@ -1407,6 +1429,22 @@ func applyLoadBalancersToPods(window kubecost.Window, podMap map[podKey]*pod, lb
 			alloc.LoadBalancerCost += lb.TotalCost * hours / totalHours
 		}
 
+		for _, alloc := range allocs {
+			if alloc.LoadBalancers == nil {
+				alloc.LoadBalancers = kubecost.LbAllocations{}
+			}
+
+			if _, found := alloc.LoadBalancers[sKey.String()]; found {
+				alloc.LoadBalancers[sKey.String()].Cost += alloc.LoadBalancerCost
+			} else {
+				alloc.LoadBalancers[sKey.String()] = &kubecost.LbAllocation{
+					Service: sKey.Namespace + "/" + sKey.Service,
+					Cost:    alloc.LoadBalancerCost,
+					Private: lb.Private,
+				}
+			}
+		}
+
 		// If there was no overlap apply to Unmounted pod
 		if len(allocHours) == 0 {
 			pod := getUnmountedPodForCluster(window, podMap, sKey.Cluster)
@@ -1736,7 +1774,7 @@ func buildPVMap(resolution time.Duration, pvMap map[pvKey]*pv, resPVCostPerGiBHo
 			continue
 		}
 
-		pvStart, pvEnd := calculateStartAndEnd(result, resolution)
+		pvStart, pvEnd := calculateStartAndEnd(result, resolution, true)
 		if pvStart.IsZero() || pvEnd.IsZero() {
 			log.Warnf("CostModel.ComputeAllocation: pv %s has no running time", key)
 		}
@@ -1805,7 +1843,7 @@ func buildPVCMap(resolution time.Duration, pvcMap map[pvcKey]*pvc, pvMap map[pvK
 		pvKey := newPVKey(cluster, volume)
 		pvcKey := newPVCKey(cluster, namespace, name)
 
-		pvcStart, pvcEnd := calculateStartAndEnd(res, resolution)
+		pvcStart, pvcEnd := calculateStartAndEnd(res, resolution, true)
 		if pvcStart.IsZero() || pvcEnd.IsZero() {
 			log.Warnf("CostModel.ComputeAllocation: pvc %s has no running time", pvcKey)
 		}
@@ -2139,10 +2177,12 @@ func getUnmountedPodForNamespace(window kubecost.Window, podMap map[podKey]*pod,
 	return thisPod
 }
 
-func calculateStartAndEnd(result *prom.QueryResult, resolution time.Duration) (time.Time, time.Time) {
+func calculateStartAndEnd(result *prom.QueryResult, resolution time.Duration, offsetResolution bool) (time.Time, time.Time) {
 	s := time.Unix(int64(result.Values[0].Timestamp), 0).UTC()
-	// subtract resolution from start time to cover full time period
-	s = s.Add(-resolution)
+	if offsetResolution {
+		// subtract resolution from start time to cover full time period
+		s = s.Add(-resolution)
+	}
 	e := time.Unix(int64(result.Values[len(result.Values)-1].Timestamp), 0).UTC()
 	return s, e
 }

+ 4 - 3
pkg/costmodel/allocation_helpers_test.go

@@ -2,11 +2,12 @@ package costmodel
 
 import (
 	"fmt"
+	"testing"
+	"time"
+
 	"github.com/opencost/opencost/pkg/kubecost"
 	"github.com/opencost/opencost/pkg/prom"
 	"github.com/opencost/opencost/pkg/util"
-	"testing"
-	"time"
 )
 
 const Ki = 1024
@@ -498,7 +499,7 @@ func TestCalculateStartAndEnd(t *testing.T) {
 
 	for name, testCase := range testCases {
 		t.Run(name, func(t *testing.T) {
-			start, end := calculateStartAndEnd(testCase.result, testCase.resolution)
+			start, end := calculateStartAndEnd(testCase.result, testCase.resolution, true)
 			if !start.Equal(testCase.expectedStart) {
 				t.Errorf("start to not match expected %v : %v", start, testCase.expectedStart)
 			}

+ 1 - 0
pkg/costmodel/allocation_types.go

@@ -211,4 +211,5 @@ type lbCost struct {
 	TotalCost float64
 	Start     time.Time
 	End       time.Time
+	Private   bool
 }

+ 1 - 1
pkg/costmodel/assets.go

@@ -84,7 +84,7 @@ func (cm *CostModel) ComputeAssets(start, end time.Time) (*kubecost.AssetSet, er
 			e = end
 		}
 
-		loadBalancer := kubecost.NewLoadBalancer(lb.Name, lb.Cluster, lb.ProviderID, s, e, kubecost.NewWindow(&start, &end))
+		loadBalancer := kubecost.NewLoadBalancer(lb.Name, lb.Cluster, lb.ProviderID, s, e, kubecost.NewWindow(&start, &end), lb.Private)
 		cm.PropertiesFromCluster(loadBalancer.Properties)
 		loadBalancer.Cost = lb.Cost
 		assetSet.Insert(loadBalancer, nil)

+ 58 - 26
pkg/costmodel/cluster.go

@@ -2,6 +2,7 @@ package costmodel
 
 import (
 	"fmt"
+	"net"
 	"strconv"
 	"time"
 
@@ -142,12 +143,6 @@ type DiskIdentifier struct {
 }
 
 func ClusterDisks(client prometheus.Client, provider models.Provider, start, end time.Time) (map[DiskIdentifier]*Disk, error) {
-	// Query for the duration between start and end
-	durStr := timeutil.DurationString(end.Sub(start))
-	if durStr == "" {
-		return nil, fmt.Errorf("illegal duration value for %s", kubecost.NewClosedWindow(start, end))
-	}
-
 	// Start from the time "end", querying backwards
 	t := end
 
@@ -162,6 +157,10 @@ func ClusterDisks(client prometheus.Client, provider models.Provider, start, end
 		log.DedupedWarningf(3, "ClusterDisks(): Configured ETL resolution (%d seconds) is below the 60 seconds threshold. Overriding with 1 minute.", int(resolution.Seconds()))
 	}
 
+	durStr := timeutil.DurationString(end.Sub(start))
+	if durStr == "" {
+		return nil, fmt.Errorf("illegal duration value for %s", kubecost.NewClosedWindow(start, end))
+	}
 	// hourlyToCumulative is a scaling factor that, when multiplied by an hourly
 	// value, converts it to a cumulative value; i.e.
 	// [$/hr] * [min/res]*[hr/min] = [$/res]
@@ -548,12 +547,6 @@ func costTimesMinute(activeDataMap map[NodeIdentifier]activeData, costMap map[No
 }
 
 func ClusterNodes(cp models.Provider, client prometheus.Client, start, end time.Time) (map[NodeIdentifier]*Node, error) {
-	// Query for the duration between start and end
-	durStr := timeutil.DurationString(end.Sub(start))
-	if durStr == "" {
-		return nil, fmt.Errorf("illegal duration value for %s", kubecost.NewClosedWindow(start, end))
-	}
-
 	// Start from the time "end", querying backwards
 	t := end
 
@@ -568,14 +561,24 @@ func ClusterNodes(cp models.Provider, client prometheus.Client, start, end time.
 		log.DedupedWarningf(3, "ClusterNodes(): Configured ETL resolution (%d seconds) is below the 60 seconds threshold. Overriding with 1 minute.", int(resolution.Seconds()))
 	}
 
+	// Query for the duration between start and end
+	// note, we need to append the resolution here because data is not given for the last <resolution> minutes
+	// this gives us a bit of query overlap, but the most recent minsPerResolution mins in each query are not returned
+	endInclResolution := end.Add(time.Duration(minsPerResolution) * time.Minute)
+	duration := endInclResolution.Sub(start)
+	durStr := timeutil.DurationString(duration)
+	if durStr == "" {
+		return nil, fmt.Errorf("illegal duration value for %s", kubecost.NewClosedWindow(start, end))
+	}
+
 	requiredCtx := prom.NewNamedContext(client, prom.ClusterContextName)
 	optionalCtx := prom.NewNamedContext(client, prom.ClusterOptionalContextName)
 
 	queryNodeCPUHourlyCost := fmt.Sprintf(`avg(avg_over_time(node_cpu_hourly_cost{%s}[%s])) by (%s, node, instance_type, provider_id)`, env.GetPromClusterFilter(), durStr, env.GetPromClusterLabel())
-  queryNodeCPUCoresCapacity := fmt.Sprintf(`avg(avg_over_time(kube_node_status_capacity_cpu_cores{%s}[%s])) by (%s, node)`, env.GetPromClusterFilter(), durStr, env.GetPromClusterLabel())
+	queryNodeCPUCoresCapacity := fmt.Sprintf(`avg(avg_over_time(kube_node_status_capacity_cpu_cores{%s}[%s])) by (%s, node)`, env.GetPromClusterFilter(), durStr, env.GetPromClusterLabel())
 	queryNodeCPUCoresAllocatable := fmt.Sprintf(`avg(avg_over_time(kube_node_status_allocatable_cpu_cores{%s}[%s])) by (%s, node)`, env.GetPromClusterFilter(), durStr, env.GetPromClusterLabel())
 	queryNodeRAMHourlyCost := fmt.Sprintf(`avg(avg_over_time(node_ram_hourly_cost{%s}[%s])) by (%s, node, instance_type, provider_id) / 1024 / 1024 / 1024`, env.GetPromClusterFilter(), durStr, env.GetPromClusterLabel())
-  queryNodeRAMBytesCapacity := fmt.Sprintf(`avg(avg_over_time(kube_node_status_capacity_memory_bytes{%s}[%s])) by (%s, node)`, env.GetPromClusterFilter(), durStr, env.GetPromClusterLabel())
+	queryNodeRAMBytesCapacity := fmt.Sprintf(`avg(avg_over_time(kube_node_status_capacity_memory_bytes{%s}[%s])) by (%s, node)`, env.GetPromClusterFilter(), durStr, env.GetPromClusterLabel())
 	queryNodeRAMBytesAllocatable := fmt.Sprintf(`avg(avg_over_time(kube_node_status_allocatable_memory_bytes{%s}[%s])) by (%s, node)`, env.GetPromClusterFilter(), durStr, env.GetPromClusterLabel())
 	queryNodeGPUCount := fmt.Sprintf(`avg(avg_over_time(node_gpu_count{%s}[%s])) by (%s, node, provider_id)`, env.GetPromClusterFilter(), durStr, env.GetPromClusterLabel())
 	queryNodeGPUHourlyCost := fmt.Sprintf(`avg(avg_over_time(node_gpu_hourly_cost{%s}[%s])) by (%s, node, instance_type, provider_id)`, env.GetPromClusterFilter(), durStr, env.GetPromClusterLabel())
@@ -717,14 +720,10 @@ type LoadBalancer struct {
 	Start      time.Time
 	End        time.Time
 	Minutes    float64
+	Private    bool
 }
 
 func ClusterLoadBalancers(client prometheus.Client, start, end time.Time) (map[LoadBalancerIdentifier]*LoadBalancer, error) {
-	// Query for the duration between start and end
-	durStr := timeutil.DurationString(end.Sub(start))
-	if durStr == "" {
-		return nil, fmt.Errorf("illegal duration value for %s", kubecost.NewClosedWindow(start, end))
-	}
 
 	// Start from the time "end", querying backwards
 	t := end
@@ -740,6 +739,12 @@ func ClusterLoadBalancers(client prometheus.Client, start, end time.Time) (map[L
 		log.DedupedWarningf(3, "ClusterLoadBalancers(): Configured ETL resolution (%d seconds) is below the 60 seconds threshold. Overriding with 1 minute.", int(resolution.Seconds()))
 	}
 
+	// Query for the duration between start and end
+	durStr := timeutil.DurationString(end.Sub(start))
+	if durStr == "" {
+		return nil, fmt.Errorf("illegal duration value for %s", kubecost.NewClosedWindow(start, end))
+	}
+
 	ctx := prom.NewNamedContext(client, prom.ClusterContextName)
 
 	queryLBCost := fmt.Sprintf(`avg(avg_over_time(kubecost_load_balancer_cost{%s}[%s])) by (namespace, service_name, %s, ingress_ip)`, env.GetPromClusterFilter(), durStr, env.GetPromClusterLabel())
@@ -829,6 +834,12 @@ func ClusterLoadBalancers(client prometheus.Client, start, end time.Time) (map[L
 			continue
 		}
 
+		providerID, err := result.GetString("ingress_ip")
+		if err != nil {
+			log.DedupedWarningf(5, "ClusterLoadBalancers: LB cost data missing ingress_ip")
+			// only update asset cost when an actual IP was returned
+			continue
+		}
 		key := LoadBalancerIdentifier{
 			Cluster:   cluster,
 			Namespace: namespace,
@@ -838,16 +849,28 @@ func ClusterLoadBalancers(client prometheus.Client, start, end time.Time) (map[L
 		// Apply cost as price-per-hour * hours
 		if lb, ok := loadBalancerMap[key]; ok {
 			lbPricePerHr := result.Values[0].Value
-			hrs := lb.Minutes / 60.0
+
+			// interpolate any missing data
+			resultMins := lb.Minutes
+			scaleFactor := (resultMins + resolution.Minutes()) / resultMins
+
+			hrs := (lb.Minutes * scaleFactor) / 60.0
 			lb.Cost += lbPricePerHr * hrs
+			lb.Private = privateIPCheck(providerID)
 		} else {
-			log.DedupedWarningf(20, "ClusterLoadBalancers: found minutes for key that does not exist: %s", key)
+			log.DedupedWarningf(20, "ClusterLoadBalancers: found minutes for key that does not exist: %v", key)
 		}
 	}
 
 	return loadBalancerMap, nil
 }
 
+// Check if an ip is private.
+func privateIPCheck(ip string) bool {
+	ipAddress := net.ParseIP(ip)
+	return ipAddress.IsPrivate()
+}
+
 // ComputeClusterCosts gives the cumulative and monthly-rate cluster costs over a window of time for all clusters.
 func (a *Accesses) ComputeClusterCosts(client prometheus.Client, provider models.Provider, window, offset time.Duration, withBreakdown bool) (map[string]*ClusterCosts, error) {
 	if window < 10*time.Minute {
@@ -859,8 +882,6 @@ func (a *Accesses) ComputeClusterCosts(client prometheus.Client, provider models
 
 	mins := end.Sub(start).Minutes()
 
-	windowStr := timeutil.DurationString(window)
-
 	// minsPerResolution determines accuracy and resource use for the following
 	// queries. Smaller values (higher resolution) result in better accuracy,
 	// but more expensive queries, and vice-a-versa.
@@ -872,6 +893,12 @@ func (a *Accesses) ComputeClusterCosts(client prometheus.Client, provider models
 		log.DedupedWarningf(3, "ComputeClusterCosts(): Configured ETL resolution (%d seconds) is below the 60 seconds threshold. Overriding with 1 minute.", int(resolution.Seconds()))
 	}
 
+	// note, we need to append the resolution here because data is not given for the last <resolution> minutes
+	// this gives us a bit of query overlap, but the most recent minsPerResolution mins in each query are not returned
+	endInclResolution := end.Add(time.Duration(minsPerResolution) * time.Minute)
+	duration := endInclResolution.Sub(start)
+	windowStr := timeutil.DurationString(duration)
+
 	// hourlyToCumulative is a scaling factor that, when multiplied by an hourly
 	// value, converts it to a cumulative value; i.e.
 	// [$/hr] * [min/res]*[hr/min] = [$/res]
@@ -1343,8 +1370,6 @@ func pvCosts(diskMap map[DiskIdentifier]*Disk, resolution time.Duration, resActi
 		e := time.Unix(int64(result.Values[len(result.Values)-1].Timestamp), 0)
 		mins := e.Sub(s).Minutes()
 
-		// TODO niko/assets if mins >= threshold, interpolate for missing data?
-
 		diskMap[key].End = e
 		diskMap[key].Start = s
 		diskMap[key].Minutes = mins
@@ -1418,7 +1443,14 @@ func pvCosts(diskMap map[DiskIdentifier]*Disk, resolution time.Duration, resActi
 				Breakdown: &ClusterCostsBreakdown{},
 			}
 		}
-		diskMap[key].Cost = cost * (diskMap[key].Bytes / 1024 / 1024 / 1024) * (diskMap[key].Minutes / 60)
+		// interpolate any missing data
+		resultMins := diskMap[key].Minutes
+		scaleFactor := 0.0
+		if resultMins > 0 {
+			scaleFactor = (resultMins + resolution.Minutes()) / resultMins
+			diskMap[key].Minutes = resultMins + resolution.Minutes()
+		}
+		diskMap[key].Cost = cost * (diskMap[key].Bytes / 1024 / 1024 / 1024) * (diskMap[key].Minutes / 60) * scaleFactor
 		providerID, _ := result.GetString("provider_id") // just put the providerID set up here, it's the simplest query.
 		if providerID != "" {
 			diskMap[key].ProviderID = provider.ParsePVID(providerID)

+ 128 - 3
pkg/costmodel/costmodel.go

@@ -2299,17 +2299,19 @@ func measureTimeAsync(start time.Time, threshold time.Duration, name string, ch
 	}
 }
 
-func (cm *CostModel) QueryAllocation(window kubecost.Window, resolution, step time.Duration, aggregate []string, includeIdle, idleByNode, includeProportionalAssetResourceCosts, includeAggregatedMetadata bool) (*kubecost.AllocationSetRange, error) {
+func (cm *CostModel) QueryAllocation(window kubecost.Window, resolution, step time.Duration, aggregate []string, includeIdle, idleByNode, includeProportionalAssetResourceCosts, includeAggregatedMetadata bool, accumulateBy kubecost.AccumulateOption) (*kubecost.AllocationSetRange, error) {
 	// Validate window is legal
 	if window.IsOpen() || window.IsNegative() {
 		return nil, fmt.Errorf("illegal window: %s", window)
 	}
 
+	var totalsStore kubecost.TotalsStore
 	// Idle is required for proportional asset costs
 	if includeProportionalAssetResourceCosts {
 		if !includeIdle {
 			return nil, errors.New("bad request - includeIdle must be set true if includeProportionalAssetResourceCosts is true")
 		}
+		totalsStore = kubecost.NewMemoryTotalsStore()
 	}
 
 	// Begin with empty response
@@ -2319,6 +2321,7 @@ func (cm *CostModel) QueryAllocation(window kubecost.Window, resolution, step ti
 	// appending each to the response.
 	stepStart := *window.Start()
 	stepEnd := stepStart.Add(step)
+	var isAzure bool
 	for window.End().After(stepStart) {
 		allocSet, err := cm.ComputeAllocation(stepStart, stepEnd, resolution)
 		if err != nil {
@@ -2331,6 +2334,25 @@ func (cm *CostModel) QueryAllocation(window kubecost.Window, resolution, step ti
 				return nil, fmt.Errorf("error computing assets for %s: %w", kubecost.NewClosedWindow(stepStart, stepEnd), err)
 			}
 
+			if includeProportionalAssetResourceCosts {
+
+				// AKS is a special case - there can be a maximum of 2
+				// load balancers (1 public and 1 private) in an AKS cluster
+				// therefore, when calculating PARCs for load balancers,
+				// we must know if this is an AKS cluster
+				for _, node := range assetSet.Nodes {
+					if _, found := node.Labels["label_kubernetes_azure_com_cluster"]; found {
+						isAzure = true
+						break
+					}
+				}
+
+				_, err := kubecost.UpdateAssetTotalsStore(totalsStore, assetSet)
+				if err != nil {
+					log.Errorf("ETL: error updating asset resource totals for %s: %s", assetSet.Window, err)
+				}
+			}
+
 			idleSet, err := computeIdleAllocations(allocSet, assetSet, true)
 			if err != nil {
 				return nil, fmt.Errorf("error computing idle allocations for %s: %w", kubecost.NewClosedWindow(stepStart, stepEnd), err)
@@ -2360,6 +2382,109 @@ func (cm *CostModel) QueryAllocation(window kubecost.Window, resolution, step ti
 		return nil, fmt.Errorf("error aggregating for %s: %w", window, err)
 	}
 
+	// Accumulate, if requested
+	if accumulateBy != kubecost.AccumulateOptionNone {
+		asr, err = asr.Accumulate(accumulateBy)
+		if err != nil {
+			log.Errorf("error accumulating by %v: %s", accumulateBy, err)
+			return nil, fmt.Errorf("error accumulating by %v: %s", accumulateBy, err)
+		}
+
+		// when accumulating and returning PARCs, we need the totals for the
+		// accumulated windows to accurately compute a fraction
+		if includeProportionalAssetResourceCosts {
+			assetSet, err := cm.ComputeAssets(*asr.Window().Start(), *asr.Window().End())
+			if err != nil {
+				return nil, fmt.Errorf("error computing assets for %s: %w", kubecost.NewClosedWindow(*asr.Window().Start(), *asr.Window().End()), err)
+			}
+
+			_, err = kubecost.UpdateAssetTotalsStore(totalsStore, assetSet)
+			if err != nil {
+				log.Errorf("ETL: error updating asset resource totals for %s: %s", kubecost.NewClosedWindow(*asr.Window().Start(), *asr.Window().End()), err)
+			}
+
+		}
+	}
+
+	if includeProportionalAssetResourceCosts {
+
+		for _, as := range asr.Allocations {
+			totalStoreByNode, ok := totalsStore.GetAssetTotalsByNode(as.Start(), as.End())
+			if !ok {
+				log.Errorf("unable to locate allocation totals for node for window %v - %v", as.Start(), as.End())
+				return nil, fmt.Errorf("unable to locate allocation totals for node for window %v - %v", as.Start(), as.End())
+			}
+
+			totalStoreByCluster, ok := totalsStore.GetAssetTotalsByCluster(as.Start(), as.End())
+			if !ok {
+				log.Errorf("unable to locate allocation totals for cluster for window %v - %v", as.Start(), as.End())
+				return nil, fmt.Errorf("unable to locate allocation totals for cluster for window %v - %v", as.Start(), as.End())
+			}
+
+			var totalPublicLbCost, totalPrivateLbCost float64
+			if isAzure {
+				// loop through all assetTotals, adding all load balancer costs by public and private
+				for _, tot := range totalStoreByNode {
+					if tot.PrivateLoadBalancer {
+						totalPrivateLbCost += tot.LoadBalancerCost
+					} else {
+						totalPublicLbCost += tot.LoadBalancerCost
+					}
+				}
+			}
+
+			// loop through each allocation set, using total cost from totals store
+			for _, alloc := range as.Allocations {
+				for rawKey, parc := range alloc.ProportionalAssetResourceCosts {
+
+					key := strings.TrimSuffix(strings.ReplaceAll(rawKey, ",", "/"), "/")
+					// for each parc , check the totals store for each
+					// on a totals hit, set the corresponding total and calculate percentage
+					var totals *kubecost.AssetTotals
+					if totalsLoc, found := totalStoreByCluster[key]; found {
+						totals = totalsLoc
+					}
+
+					if totalsLoc, found := totalStoreByNode[key]; found {
+						totals = totalsLoc
+					}
+
+					if totals == nil {
+						log.Errorf("unable to locate asset totals for allocation %s", key)
+						return nil, fmt.Errorf("unable to locate allocation totals for allocation")
+
+					}
+
+					parc.CPUTotalCost = totals.CPUCost
+					parc.GPUTotalCost = totals.GPUCost
+					parc.RAMTotalCost = totals.RAMCost
+					if !isAzure {
+						parc.LoadBalancerTotalCost = totals.LoadBalancerCost
+					} else if len(alloc.LoadBalancers) > 0 {
+						// Azure is a special case - use computed totals above
+						// use the lbAllocations in the object to determine if
+						// this PARC is a public or private load balancer
+						// then set the total accordingly
+						// AKS only has 1 public and 1 private load balancer
+
+						lbAlloc, found := alloc.LoadBalancers[key]
+						if found {
+							if lbAlloc.Private {
+								parc.LoadBalancerTotalCost = totalPrivateLbCost
+							} else {
+								parc.LoadBalancerTotalCost = totalPublicLbCost
+							}
+						}
+					}
+
+					kubecost.ComputePercentages(&parc)
+					alloc.ProportionalAssetResourceCosts[rawKey] = parc
+				}
+			}
+
+		}
+	}
+
 	return asr, nil
 }
 
@@ -2373,10 +2498,10 @@ func computeIdleAllocations(allocSet *kubecost.AllocationSet, assetSet *kubecost
 
 	if idleByNode {
 		allocTotals = kubecost.ComputeAllocationTotals(allocSet, kubecost.AllocationNodeProp)
-		assetTotals = kubecost.ComputeAssetTotals(assetSet, kubecost.AssetNodeProp)
+		assetTotals = kubecost.ComputeAssetTotals(assetSet, true)
 	} else {
 		allocTotals = kubecost.ComputeAllocationTotals(allocSet, kubecost.AllocationClusterProp)
-		assetTotals = kubecost.ComputeAssetTotals(assetSet, kubecost.AssetClusterProp)
+		assetTotals = kubecost.ComputeAssetTotals(assetSet, false)
 	}
 
 	start, end := *allocSet.Window.Start(), *allocSet.Window.End()

+ 167 - 64
pkg/kubecost/allocation.go

@@ -92,6 +92,32 @@ type Allocation struct {
 	// and appended to an Allocation, and so by default is is nil.
 	ProportionalAssetResourceCosts ProportionalAssetResourceCosts `json:"proportionalAssetResourceCosts"` //@bingen:field[ignore]
 	SharedCostBreakdown            SharedCostBreakdowns           `json:"sharedCostBreakdown"`            //@bingen:field[ignore]
+	LoadBalancers                  LbAllocations                  `json:"LoadBalancers"`                  // @bingen:field[version=17]
+
+}
+
+type LbAllocations map[string]*LbAllocation
+
+func (orig LbAllocations) Clone() LbAllocations {
+	if orig == nil {
+		return nil
+	}
+
+	newAllocs := LbAllocations{}
+
+	for key, lbAlloc := range orig {
+		newAllocs[key] = &LbAllocation{
+			Service: lbAlloc.Service,
+			Cost:    lbAlloc.Cost,
+		}
+	}
+	return newAllocs
+}
+
+type LbAllocation struct {
+	Service string  `json:"service"`
+	Cost    float64 `json:"cost"`
+	Private bool    `json:"private"`
 }
 
 // RawAllocationOnlyData is information that only belong in "raw" Allocations,
@@ -252,24 +278,29 @@ func (pva *PVAllocation) Equal(that *PVAllocation) bool {
 }
 
 type ProportionalAssetResourceCost struct {
-	Cluster                    string  `json:"cluster"`
-	Node                       string  `json:"node,omitempty"`
-	ProviderID                 string  `json:"providerID,omitempty"`
-	CPUPercentage              float64 `json:"cpuPercentage"`
-	GPUPercentage              float64 `json:"gpuPercentage"`
-	RAMPercentage              float64 `json:"ramPercentage"`
-	NodeResourceCostPercentage float64 `json:"nodeResourceCostPercentage"`
-	GPUTotalCost               float64 `json:"-"`
-	GPUProportionalCost        float64 `json:"-"`
-	CPUTotalCost               float64 `json:"-"`
-	CPUProportionalCost        float64 `json:"-"`
-	RAMTotalCost               float64 `json:"-"`
-	RAMProportionalCost        float64 `json:"-"`
-}
-
-func (parc ProportionalAssetResourceCost) Key(insertByNode bool) string {
-	if insertByNode {
-		return parc.Cluster + "," + parc.Node
+	Cluster                string  `json:"cluster"`
+	Name                   string  `json:"name,omitempty"`
+	Type                   string  `json:"name,omitempty"`
+	ProviderID             string  `json:"providerID,omitempty"`
+	CPUPercentage          float64 `json:"cpuPercentage"`
+	GPUPercentage          float64 `json:"gpuPercentage"`
+	RAMPercentage          float64 `json:"ramPercentage"`
+	LoadBalancerPercentage float64 `json:"loadBalancerPercentage"`
+
+	NodeResourceCostPercentage   float64 `json:"nodeResourceCostPercentage"`
+	GPUTotalCost                 float64 `json:"-"`
+	GPUProportionalCost          float64 `json:"-"`
+	CPUTotalCost                 float64 `json:"-"`
+	CPUProportionalCost          float64 `json:"-"`
+	RAMTotalCost                 float64 `json:"-"`
+	RAMProportionalCost          float64 `json:"-"`
+	LoadBalancerProportionalCost float64 `json:"-"`
+	LoadBalancerTotalCost        float64 `json:"-"`
+}
+
+func (parc ProportionalAssetResourceCost) Key(insertByName bool) string {
+	if insertByName {
+		return parc.Cluster + "," + parc.Name
 	} else {
 		return parc.Cluster
 	}
@@ -287,36 +318,36 @@ func (parcs ProportionalAssetResourceCosts) Clone() ProportionalAssetResourceCos
 	return cloned
 }
 
-func (parcs ProportionalAssetResourceCosts) Insert(parc ProportionalAssetResourceCost, insertByNode bool) {
-	if !insertByNode {
-		parc.Node = ""
+func (parcs ProportionalAssetResourceCosts) Insert(parc ProportionalAssetResourceCost, insertByName bool) {
+	if !insertByName {
+		parc.Name = ""
+		parc.Type = ""
 		parc.ProviderID = ""
 	}
-	if curr, ok := parcs[parc.Key(insertByNode)]; ok {
+	if curr, ok := parcs[parc.Key(insertByName)]; ok {
 
 		toInsert := ProportionalAssetResourceCost{
-			Node:                curr.Node,
-			Cluster:             curr.Cluster,
-			ProviderID:          curr.ProviderID,
-			CPUTotalCost:        curr.CPUTotalCost + parc.CPUTotalCost,
-			CPUProportionalCost: curr.CPUProportionalCost + parc.CPUProportionalCost,
-			RAMTotalCost:        curr.RAMTotalCost + parc.RAMTotalCost,
-			RAMProportionalCost: curr.RAMProportionalCost + parc.RAMProportionalCost,
-			GPUProportionalCost: curr.GPUProportionalCost + parc.GPUProportionalCost,
-			GPUTotalCost:        curr.GPUTotalCost + parc.GPUTotalCost,
-		}
-
-		computePercentages(&toInsert)
-		parcs[parc.Key(insertByNode)] = toInsert
+			Name:                         curr.Name,
+			Type:                         curr.Type,
+			Cluster:                      curr.Cluster,
+			ProviderID:                   curr.ProviderID,
+			CPUProportionalCost:          curr.CPUProportionalCost + parc.CPUProportionalCost,
+			RAMProportionalCost:          curr.RAMProportionalCost + parc.RAMProportionalCost,
+			GPUProportionalCost:          curr.GPUProportionalCost + parc.GPUProportionalCost,
+			LoadBalancerProportionalCost: curr.LoadBalancerProportionalCost + parc.LoadBalancerProportionalCost,
+		}
+
+		ComputePercentages(&toInsert)
+		parcs[parc.Key(insertByName)] = toInsert
 	} else {
-		computePercentages(&parc)
-		parcs[parc.Key(insertByNode)] = parc
+		ComputePercentages(&parc)
+		parcs[parc.Key(insertByName)] = parc
 	}
 }
 
-func computePercentages(toInsert *ProportionalAssetResourceCost) {
+func ComputePercentages(toInsert *ProportionalAssetResourceCost) {
 	// compute percentages
-	totalCost := toInsert.RAMTotalCost + toInsert.CPUTotalCost + toInsert.GPUTotalCost
+	totalNodeCost := toInsert.RAMTotalCost + toInsert.CPUTotalCost + toInsert.GPUTotalCost
 
 	if toInsert.CPUTotalCost > 0 {
 		toInsert.CPUPercentage = toInsert.CPUProportionalCost / toInsert.CPUTotalCost
@@ -326,21 +357,25 @@ func computePercentages(toInsert *ProportionalAssetResourceCost) {
 		toInsert.GPUPercentage = toInsert.GPUProportionalCost / toInsert.GPUTotalCost
 	}
 
+	if toInsert.LoadBalancerTotalCost > 0 {
+		toInsert.LoadBalancerPercentage = toInsert.LoadBalancerProportionalCost / toInsert.LoadBalancerTotalCost
+	}
+
 	if toInsert.RAMTotalCost > 0 {
 		toInsert.RAMPercentage = toInsert.RAMProportionalCost / toInsert.RAMTotalCost
 	}
 
-	ramFraction := toInsert.RAMTotalCost / totalCost
+	ramFraction := toInsert.RAMTotalCost / totalNodeCost
 	if ramFraction != ramFraction || ramFraction < 0 {
 		ramFraction = 0
 	}
 
-	cpuFraction := toInsert.CPUTotalCost / totalCost
+	cpuFraction := toInsert.CPUTotalCost / totalNodeCost
 	if cpuFraction != cpuFraction || cpuFraction < 0 {
 		cpuFraction = 0
 	}
 
-	gpuFraction := toInsert.GPUTotalCost / totalCost
+	gpuFraction := toInsert.GPUTotalCost / totalNodeCost
 	if gpuFraction != gpuFraction || gpuFraction < 0 {
 		gpuFraction = 0
 	}
@@ -352,12 +387,12 @@ func computePercentages(toInsert *ProportionalAssetResourceCost) {
 func (parcs ProportionalAssetResourceCosts) Add(that ProportionalAssetResourceCosts) {
 
 	for _, parc := range that {
-		// if node field is empty, we know this is a cluster level PARC aggregation
-		insertByNode := true
-		if parc.Node == "" {
-			insertByNode = false
+		// if name field is empty, we know this is a cluster level PARC aggregation
+		insertByName := true
+		if parc.Name == "" {
+			insertByName = false
 		}
-		parcs.Insert(parc, insertByNode)
+		parcs.Insert(parc, insertByName)
 	}
 }
 
@@ -477,6 +512,7 @@ func (a *Allocation) Clone() *Allocation {
 		RawAllocationOnly:              a.RawAllocationOnly.Clone(),
 		ProportionalAssetResourceCosts: a.ProportionalAssetResourceCosts.Clone(),
 		SharedCostBreakdown:            a.SharedCostBreakdown.Clone(),
+		LoadBalancers:                  a.LoadBalancers.Clone(),
 	}
 }
 
@@ -999,11 +1035,38 @@ func (a *Allocation) add(that *Allocation) {
 	a.NetworkCostAdjustment += that.NetworkCostAdjustment
 	a.LoadBalancerCostAdjustment += that.LoadBalancerCostAdjustment
 
+	// Sum LoadBalancer Allocations
+	a.LoadBalancers = a.LoadBalancers.Add(that.LoadBalancers)
+
 	// Any data that is in a "raw allocation only" is not valid in any
 	// sort of cumulative Allocation (like one that is added).
 	a.RawAllocationOnly = nil
 }
 
+func (thisLbAllocs LbAllocations) Add(thatLbAllocs LbAllocations) LbAllocations {
+	// loop through both sets of LB allocations, building a new LBAllocations that has the summed set
+	mergedLbAllocs := thisLbAllocs.Clone()
+	if thatLbAllocs != nil {
+		if mergedLbAllocs == nil {
+			mergedLbAllocs = LbAllocations{}
+		}
+		for lbKey, thatlbAlloc := range thatLbAllocs {
+			thisLbAlloc, ok := mergedLbAllocs[lbKey]
+			if !ok {
+				thisLbAlloc = &LbAllocation{
+					Service: thatlbAlloc.Service,
+					Cost:    thatlbAlloc.Cost,
+				}
+				mergedLbAllocs[lbKey] = thisLbAlloc
+			} else {
+				thisLbAlloc.Cost += thatlbAlloc.Cost
+			}
+
+		}
+	}
+	return mergedLbAllocs
+}
+
 // AllocationSet stores a set of Allocations, each with a unique name, that share
 // a window. An AllocationSet is mutable, so treat it like a threadsafe map.
 type AllocationSet struct {
@@ -1195,6 +1258,12 @@ func (as *AllocationSet) AggregateBy(aggregateBy []string, options *AllocationAg
 	for _, alloc := range as.Allocations {
 
 		alloc.Properties.AggregatedMetadata = options.IncludeAggregatedMetadata
+		// build a parallel set of allocations to only be used
+		// for computing PARCs
+		if options.IncludeProportionalAssetResourceCosts {
+			parcSet.Insert(alloc.Clone())
+		}
+
 		// External allocations get aggregated post-hoc (see step 6) and do
 		// not necessarily contain complete sets of properties, so they are
 		// moved to a separate AllocationSet.
@@ -1218,12 +1287,6 @@ func (as *AllocationSet) AggregateBy(aggregateBy []string, options *AllocationAg
 				aggSet.Insert(alloc)
 			}
 
-			// build a parallel set of allocations to only be used
-			// for computing PARCs
-			if options.IncludeProportionalAssetResourceCosts {
-				parcSet.Insert(alloc.Clone())
-			}
-
 			continue
 		}
 
@@ -1294,7 +1357,7 @@ func (as *AllocationSet) AggregateBy(aggregateBy []string, options *AllocationAg
 	// (2b) If proportional asset resource costs are to be included, compute them
 	// and add them to the allocations.
 	if options.IncludeProportionalAssetResourceCosts {
-		err := deriveProportionalAssetResourceCosts(options, as, shareSet)
+		err := deriveProportionalAssetResourceCosts(options, as, shareSet, parcSet)
 		if err != nil {
 			log.Debugf("AggregateBy: failed to derive proportional asset resource costs from idle coefficients: %s", err)
 			return fmt.Errorf("AggregateBy: failed to derive proportional asset resource costs from idle coefficients: %s", err)
@@ -1895,7 +1958,7 @@ func computeIdleCoeffs(options *AllocationAggregationOptions, as *AllocationSet,
 	return coeffs, totals, nil
 }
 
-func deriveProportionalAssetResourceCosts(options *AllocationAggregationOptions, as *AllocationSet, shareSet *AllocationSet) error {
+func deriveProportionalAssetResourceCosts(options *AllocationAggregationOptions, as *AllocationSet, shareSet, parcsSet *AllocationSet) error {
 
 	// Compute idle coefficients, then save them in AllocationAggregationOptions
 	// [idle_id][allocation name][resource] = [coeff]
@@ -1906,11 +1969,7 @@ func deriveProportionalAssetResourceCosts(options *AllocationAggregationOptions,
 	totals := map[string]map[string]float64{}
 
 	// Record allocation values first, then normalize by totals to get percentages
-	for _, alloc := range as.Allocations {
-		if alloc.IsIdle() {
-			// Skip idle allocations in coefficient calculation
-			continue
-		}
+	for _, alloc := range parcsSet.Allocations {
 
 		idleId, err := alloc.getIdleId(options)
 		if err != nil {
@@ -1931,6 +1990,22 @@ func deriveProportionalAssetResourceCosts(options *AllocationAggregationOptions,
 		if _, ok := coeffs[idleId][name]; !ok {
 			coeffs[idleId][name] = map[string]float64{}
 		}
+		// idle IDs for load balancers are their services
+		for key := range alloc.LoadBalancers {
+			if _, ok := totals[key]; !ok {
+				totals[key] = map[string]float64{}
+			}
+
+			if _, ok := coeffs[key]; !ok {
+				coeffs[key] = map[string]map[string]float64{}
+			}
+			if _, ok := coeffs[key][name]; !ok {
+				coeffs[key][name] = map[string]float64{}
+			}
+
+			coeffs[key][name]["loadbalancer"] += alloc.LoadBalancerTotalCost()
+			totals[key]["loadbalancer"] += alloc.LoadBalancerTotalCost()
+		}
 
 		coeffs[idleId][name]["cpu"] += alloc.CPUTotalCost()
 		coeffs[idleId][name]["gpu"] += alloc.GPUTotalCost()
@@ -1976,6 +2051,23 @@ func deriveProportionalAssetResourceCosts(options *AllocationAggregationOptions,
 		totals[idleId]["cpu"] += alloc.CPUTotalCost()
 		totals[idleId]["gpu"] += alloc.GPUTotalCost()
 		totals[idleId]["ram"] += alloc.RAMTotalCost()
+
+		// idle IDs for load balancers are their services
+		for key := range alloc.LoadBalancers {
+			if _, ok := totals[key]; !ok {
+				totals[key] = map[string]float64{}
+			}
+
+			if _, ok := coeffs[key]; !ok {
+				coeffs[key] = map[string]map[string]float64{}
+			}
+			if _, ok := coeffs[key][name]; !ok {
+				coeffs[key][name] = map[string]float64{}
+			}
+			coeffs[key][name]["loadbalancer"] += alloc.LoadBalancerTotalCost()
+			totals[key]["loadbalancer"] += alloc.LoadBalancerTotalCost()
+		}
+
 	}
 
 	// after totals are computed, loop through and set parcs on allocations
@@ -1988,15 +2080,26 @@ func deriveProportionalAssetResourceCosts(options *AllocationAggregationOptions,
 		alloc.ProportionalAssetResourceCosts = ProportionalAssetResourceCosts{}
 		alloc.ProportionalAssetResourceCosts.Insert(ProportionalAssetResourceCost{
 			Cluster:             alloc.Properties.Cluster,
-			Node:                alloc.Properties.Node,
+			Name:                alloc.Properties.Node,
+			Type:                "Node",
 			ProviderID:          alloc.Properties.ProviderID,
-			GPUTotalCost:        totals[idleId]["gpu"],
-			CPUTotalCost:        totals[idleId]["cpu"],
-			RAMTotalCost:        totals[idleId]["ram"],
 			GPUProportionalCost: coeffs[idleId][alloc.Name]["gpu"],
 			CPUProportionalCost: coeffs[idleId][alloc.Name]["cpu"],
 			RAMProportionalCost: coeffs[idleId][alloc.Name]["ram"],
 		}, options.IdleByNode)
+		// insert a separate PARC for the load balancer
+		if alloc.LoadBalancerCost != 0 {
+			for key, svc := range alloc.LoadBalancers {
+
+				alloc.ProportionalAssetResourceCosts.Insert(ProportionalAssetResourceCost{
+					Cluster:                      alloc.Properties.Cluster,
+					Name:                         svc.Service,
+					Type:                         "LoadBalancer",
+					LoadBalancerProportionalCost: coeffs[key][alloc.Name]["loadbalancer"],
+				}, options.IdleByNode)
+			}
+		}
+
 	}
 
 	return nil

+ 2 - 1
pkg/kubecost/allocation_json.go

@@ -55,6 +55,7 @@ type AllocationJSON struct {
 	TotalEfficiency                *float64                        `json:"totalEfficiency"`
 	RawAllocationOnly              *RawAllocationOnlyData          `json:"rawAllocationOnly,omitempty"`
 	ProportionalAssetResourceCosts *ProportionalAssetResourceCosts `json:"proportionalAssetResourceCosts,omitempty"`
+	LoadBalancers                  LbAllocations                   `json:"lbAllocations"`
 	SharedCostBreakdown            *SharedCostBreakdowns           `json:"sharedCostBreakdown,omitempty"`
 }
 
@@ -106,8 +107,8 @@ func (aj *AllocationJSON) BuildFromAllocation(a *Allocation) {
 	aj.TotalEfficiency = formatFloat64ForResponse(a.TotalEfficiency())
 	aj.RawAllocationOnly = a.RawAllocationOnly
 	aj.ProportionalAssetResourceCosts = &a.ProportionalAssetResourceCosts
+	aj.LoadBalancers = a.LoadBalancers
 	aj.SharedCostBreakdown = &a.SharedCostBreakdown
-
 }
 
 // formatFloat64ForResponse - take an existing float64, round it to 6 decimal places and return is possible, or return nil if invalid

+ 114 - 139
pkg/kubecost/allocation_test.go

@@ -1114,51 +1114,33 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 			expectedParcResults: map[string]ProportionalAssetResourceCosts{
 				"namespace1": {
 					"cluster1": ProportionalAssetResourceCost{
-						Cluster:                    "cluster1",
-						Node:                       "",
-						ProviderID:                 "",
-						CPUPercentage:              0.16667,
-						GPUPercentage:              0.16667,
-						RAMPercentage:              0.27083,
-						NodeResourceCostPercentage: 0.22619,
-						GPUTotalCost:               18,
-						GPUProportionalCost:        3,
-						CPUTotalCost:               18,
-						CPUProportionalCost:        3,
-						RAMTotalCost:               48,
-						RAMProportionalCost:        13,
+						Cluster:             "cluster1",
+						Name:                "",
+						Type:                "",
+						ProviderID:          "",
+						GPUProportionalCost: 3,
+						CPUProportionalCost: 3,
+						RAMProportionalCost: 13,
 					},
 				},
 				"namespace2": {
 					"cluster1": ProportionalAssetResourceCost{
-						Cluster:                    "cluster1",
-						Node:                       "",
-						ProviderID:                 "",
-						CPUPercentage:              0.16667,
-						GPUPercentage:              0.16667,
-						RAMPercentage:              0.0625,
-						NodeResourceCostPercentage: 0.10714,
-						GPUTotalCost:               18,
-						GPUProportionalCost:        3,
-						CPUTotalCost:               18,
-						CPUProportionalCost:        3,
-						RAMTotalCost:               48,
-						RAMProportionalCost:        3,
+						Cluster:             "cluster1",
+						Name:                "",
+						Type:                "",
+						ProviderID:          "",
+						GPUProportionalCost: 3,
+						CPUProportionalCost: 3,
+						RAMProportionalCost: 3,
 					},
 					"cluster2": ProportionalAssetResourceCost{
-						Cluster:                    "cluster2",
-						Node:                       "",
-						ProviderID:                 "",
-						CPUPercentage:              0.16667,
-						GPUPercentage:              0.16667,
-						RAMPercentage:              0.16667,
-						NodeResourceCostPercentage: 0.16667,
-						GPUTotalCost:               18,
-						GPUProportionalCost:        3,
-						CPUTotalCost:               18,
-						CPUProportionalCost:        3,
-						RAMTotalCost:               18,
-						RAMProportionalCost:        3,
+						Cluster:             "cluster2",
+						Name:                "",
+						Type:                "",
+						ProviderID:          "",
+						GPUProportionalCost: 3,
+						CPUProportionalCost: 3,
+						RAMProportionalCost: 3,
 					},
 				},
 			},
@@ -1566,113 +1548,71 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 			expectedParcResults: map[string]ProportionalAssetResourceCosts{
 				"namespace1": {
 					"cluster1,c1nodes": ProportionalAssetResourceCost{
-						Cluster:                    "cluster1",
-						Node:                       "c1nodes",
-						ProviderID:                 "c1nodes",
-						CPUPercentage:              0.16667,
-						GPUPercentage:              0.16667,
-						RAMPercentage:              0.27083,
-						NodeResourceCostPercentage: 0.22619,
-						GPUTotalCost:               18,
-						GPUProportionalCost:        3,
-						CPUTotalCost:               18,
-						CPUProportionalCost:        3,
-						RAMTotalCost:               48,
-						RAMProportionalCost:        13,
+						Cluster:             "cluster1",
+						Name:                "c1nodes",
+						Type:                "Node",
+						ProviderID:          "c1nodes",
+						GPUProportionalCost: 3,
+						CPUProportionalCost: 3,
+						RAMProportionalCost: 13,
 					},
 					"cluster2,node2": ProportionalAssetResourceCost{
-						Cluster:                    "cluster2",
-						Node:                       "node2",
-						ProviderID:                 "node2",
-						CPUPercentage:              0.16667,
-						GPUPercentage:              0.16667,
-						RAMPercentage:              0.0625,
-						NodeResourceCostPercentage: 0.10714,
-						GPUTotalCost:               18,
-						GPUProportionalCost:        3,
-						CPUTotalCost:               18,
-						CPUProportionalCost:        3,
-						RAMTotalCost:               48,
-						RAMProportionalCost:        3,
+						Cluster:             "cluster2",
+						Name:                "node2",
+						Type:                "Node",
+						ProviderID:          "node2",
+						GPUProportionalCost: 3,
+						CPUProportionalCost: 3,
+						RAMProportionalCost: 3,
 					},
 				},
 				"namespace2": {
 					"cluster1,c1nodes": ProportionalAssetResourceCost{
-						Cluster:                    "cluster1",
-						Node:                       "c1nodes",
-						ProviderID:                 "c1nodes",
-						CPUPercentage:              0.16667,
-						GPUPercentage:              0.16667,
-						RAMPercentage:              0.0625,
-						NodeResourceCostPercentage: 0.10714,
-						GPUTotalCost:               18,
-						GPUProportionalCost:        3,
-						CPUTotalCost:               18,
-						CPUProportionalCost:        3,
-						RAMTotalCost:               48,
-						RAMProportionalCost:        3,
+						Cluster:             "cluster1",
+						Name:                "c1nodes",
+						Type:                "Node",
+						ProviderID:          "c1nodes",
+						GPUProportionalCost: 3,
+						CPUProportionalCost: 3,
+						RAMProportionalCost: 3,
 					},
 					"cluster2,node1": ProportionalAssetResourceCost{
-						Cluster:                    "cluster2",
-						Node:                       "node1",
-						ProviderID:                 "node1",
-						CPUPercentage:              0.5,
-						GPUPercentage:              0.5,
-						RAMPercentage:              0.5,
-						NodeResourceCostPercentage: 0.5,
-						GPUTotalCost:               4,
-						GPUProportionalCost:        2,
-						CPUTotalCost:               4,
-						CPUProportionalCost:        2,
-						RAMTotalCost:               4,
-						RAMProportionalCost:        2,
+						Cluster:             "cluster2",
+						Name:                "node1",
+						Type:                "Node",
+						ProviderID:          "node1",
+						GPUProportionalCost: 2,
+						CPUProportionalCost: 2,
+						RAMProportionalCost: 2,
 					},
 					"cluster2,node2": ProportionalAssetResourceCost{
-						Cluster:                    "cluster2",
-						Node:                       "node2",
-						ProviderID:                 "node2",
-						CPUPercentage:              0.5,
-						GPUPercentage:              0.5,
-						RAMPercentage:              0.5,
-						NodeResourceCostPercentage: 0.5,
-						GPUTotalCost:               2,
-						GPUProportionalCost:        1,
-						CPUTotalCost:               2,
-						CPUProportionalCost:        1,
-						RAMTotalCost:               2,
-						RAMProportionalCost:        1,
+						Cluster:             "cluster2",
+						Name:                "node2",
+						Type:                "Node",
+						ProviderID:          "node2",
+						GPUProportionalCost: 1,
+						CPUProportionalCost: 1,
+						RAMProportionalCost: 1,
 					},
 				},
 				"namespace3": {
 					"cluster2,node3": ProportionalAssetResourceCost{
-						Cluster:                    "cluster2",
-						Node:                       "node3",
-						ProviderID:                 "node3",
-						CPUPercentage:              0.5,
-						GPUPercentage:              0.5,
-						RAMPercentage:              0.5,
-						NodeResourceCostPercentage: 0.5,
-						GPUTotalCost:               4,
-						GPUProportionalCost:        2,
-						CPUTotalCost:               4,
-						CPUProportionalCost:        2,
-						RAMTotalCost:               4,
-						RAMProportionalCost:        2,
+						Cluster:             "cluster2",
+						Name:                "node3",
+						Type:                "Node",
+						ProviderID:          "node3",
+						GPUProportionalCost: 2,
+						CPUProportionalCost: 2,
+						RAMProportionalCost: 2,
 					},
 					"cluster2,node2": ProportionalAssetResourceCost{
-						Cluster:                    "cluster2",
-						Node:                       "node2",
-						ProviderID:                 "node2",
-						CPUPercentage:              0.5,
-						GPUPercentage:              0.5,
-						RAMPercentage:              0.5,
-						NodeResourceCostPercentage: 0.5,
-						GPUTotalCost:               2,
-						GPUProportionalCost:        1,
-						CPUTotalCost:               2,
-						CPUProportionalCost:        1,
-						RAMTotalCost:               2,
-						RAMProportionalCost:        1,
+						Cluster:             "cluster2",
+						Name:                "node2",
+						Type:                "Node",
+						ProviderID:          "node2",
+						GPUProportionalCost: 1,
+						CPUProportionalCost: 1,
+						RAMProportionalCost: 1,
 					},
 				},
 			},
@@ -1951,7 +1891,8 @@ func TestAllocationSet_insertMatchingWindow(t *testing.T) {
 func TestParcInsert(t *testing.T) {
 	pod1_hour1 := ProportionalAssetResourceCost{
 		Cluster:                    "cluster1",
-		Node:                       "node1",
+		Name:                       "node1",
+		Type:                       "Node",
 		ProviderID:                 "i-1234",
 		CPUPercentage:              0.125,
 		GPUPercentage:              0,
@@ -1963,7 +1904,8 @@ func TestParcInsert(t *testing.T) {
 
 	pod1_hour2 := ProportionalAssetResourceCost{
 		Cluster:                    "cluster1",
-		Node:                       "node1",
+		Name:                       "node1",
+		Type:                       "Node",
 		ProviderID:                 "i-1234",
 		CPUPercentage:              0.0,
 		GPUPercentage:              0,
@@ -1974,7 +1916,8 @@ func TestParcInsert(t *testing.T) {
 
 	pod1_hour3 := ProportionalAssetResourceCost{
 		Cluster:                    "cluster1",
-		Node:                       "node1",
+		Name:                       "node1",
+		Type:                       "Node",
 		ProviderID:                 "i-1234",
 		CPUPercentage:              0.0,
 		GPUPercentage:              0,
@@ -1985,7 +1928,8 @@ func TestParcInsert(t *testing.T) {
 
 	pod2_hour1 := ProportionalAssetResourceCost{
 		Cluster:                    "cluster1",
-		Node:                       "node2",
+		Name:                       "node2",
+		Type:                       "Node",
 		ProviderID:                 "i-1234",
 		CPUPercentage:              0.0,
 		GPUPercentage:              0,
@@ -1997,7 +1941,8 @@ func TestParcInsert(t *testing.T) {
 
 	pod2_hour2 := ProportionalAssetResourceCost{
 		Cluster:                    "cluster1",
-		Node:                       "node2",
+		Name:                       "node2",
+		Type:                       "Node",
 		ProviderID:                 "i-1234",
 		CPUPercentage:              0.0,
 		GPUPercentage:              0,
@@ -2009,7 +1954,8 @@ func TestParcInsert(t *testing.T) {
 
 	pod2_hour3 := ProportionalAssetResourceCost{
 		Cluster:                    "cluster1",
-		Node:                       "node2",
+		Name:                       "node2",
+		Type:                       "Node",
 		ProviderID:                 "i-1234",
 		CPUPercentage:              0.0,
 		GPUPercentage:              0,
@@ -2021,7 +1967,8 @@ func TestParcInsert(t *testing.T) {
 
 	pod3_hour1 := ProportionalAssetResourceCost{
 		Cluster:                    "cluster1",
-		Node:                       "node3",
+		Name:                       "node3",
+		Type:                       "Node",
 		ProviderID:                 "i-1234",
 		CPUPercentage:              0.0,
 		GPUPercentage:              0,
@@ -2033,7 +1980,8 @@ func TestParcInsert(t *testing.T) {
 
 	pod3_hour2 := ProportionalAssetResourceCost{
 		Cluster:                    "cluster1",
-		Node:                       "node3",
+		Name:                       "node3",
+		Type:                       "Node",
 		ProviderID:                 "i-1234",
 		CPUPercentage:              0.0,
 		GPUPercentage:              0,
@@ -2045,7 +1993,8 @@ func TestParcInsert(t *testing.T) {
 
 	pod3_hour3 := ProportionalAssetResourceCost{
 		Cluster:                    "cluster1",
-		Node:                       "node3",
+		Name:                       "node3",
+		Type:                       "Node",
 		ProviderID:                 "i-1234",
 		CPUPercentage:              0.0,
 		GPUPercentage:              0,
@@ -2067,6 +2016,32 @@ func TestParcInsert(t *testing.T) {
 	parcs.Insert(pod3_hour3, true)
 	log.Debug("added all parcs")
 
+	// set totals, compute percentaves
+	parc1, ok := parcs["cluster1,node1"]
+	if !ok {
+		t.Fatalf("parc1 not found")
+	}
+	parc1.CPUTotalCost = 12
+
+	parc2, ok := parcs["cluster1,node2"]
+	if !ok {
+		t.Fatalf("parc2 not found")
+	}
+	parc2.CPUTotalCost = 12
+
+	parc3, ok := parcs["cluster1,node3"]
+	if !ok {
+		t.Fatalf("parc1 not found")
+	}
+	parc3.CPUTotalCost = 12
+
+	ComputePercentages(&parc1)
+	ComputePercentages(&parc2)
+	ComputePercentages(&parc3)
+	parcs["cluster1,node1"] = parc1
+	parcs["cluster1,node2"] = parc2
+	parcs["cluster1,node3"] = parc3
+
 	expectedParcs := ProportionalAssetResourceCosts{
 		"cluster1,node1": ProportionalAssetResourceCost{
 			CPUPercentage:              0.041666666666666664,

+ 3 - 1
pkg/kubecost/asset.go

@@ -2185,10 +2185,11 @@ type LoadBalancer struct {
 	Window     Window
 	Adjustment float64
 	Cost       float64
+	Private    bool
 }
 
 // NewLoadBalancer instantiates and returns a new LoadBalancer
-func NewLoadBalancer(name, cluster, providerID string, start, end time.Time, window Window) *LoadBalancer {
+func NewLoadBalancer(name, cluster, providerID string, start, end time.Time, window Window, private bool) *LoadBalancer {
 	properties := &AssetProperties{
 		Category:   NetworkCategory,
 		Name:       name,
@@ -2203,6 +2204,7 @@ func NewLoadBalancer(name, cluster, providerID string, start, end time.Time, win
 		Start:      start,
 		End:        end,
 		Window:     window,
+		Private:    private,
 	}
 }
 

+ 2 - 2
pkg/kubecost/asset_json_test.go

@@ -419,7 +419,7 @@ func TestNode_Unmarshal(t *testing.T) {
 
 func TestLoadBalancer_Unmarshal(t *testing.T) {
 
-	lb1 := NewLoadBalancer("loadbalancer1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	lb1 := NewLoadBalancer("loadbalancer1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow, false)
 	lb1.Cost = 12.0
 	lb1.SetAdjustment(4.0)
 
@@ -515,7 +515,7 @@ func TestAssetset_Unmarshal(t *testing.T) {
 	disk := NewDisk("disk1", "cluster1", "disk1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
 	network := NewNetwork("network1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
 	node := NewNode("node1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
-	lb := NewLoadBalancer("loadbalancer1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	lb := NewLoadBalancer("loadbalancer1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow, false)
 	sa := NewSharedAsset("sharedasset1", unmarshalWindow)
 
 	assetList := []Asset{any, cloud, cm, disk, network, node, lb, sa}

+ 2 - 0
pkg/kubecost/bingen.go

@@ -58,6 +58,8 @@ package kubecost
 // @bingen:generate:PVAllocations
 // @bingen:generate:PVKey
 // @bingen:generate:PVAllocation
+// @bingen:generate:LbAllocations
+// @bingen:generate:LbAllocation
 // @bingen:end
 
 // @bingen:set[name=Audit,version=1]

+ 205 - 0
pkg/kubecost/kubecost_codecs.go

@@ -84,6 +84,7 @@ var typeMap map[string]reflect.Type = map[string]reflect.Type{
 	"CoverageSet":                   reflect.TypeOf((*CoverageSet)(nil)).Elem(),
 	"Disk":                          reflect.TypeOf((*Disk)(nil)).Elem(),
 	"EqualityAudit":                 reflect.TypeOf((*EqualityAudit)(nil)).Elem(),
+	"LbAllocation":                  reflect.TypeOf((*LbAllocation)(nil)).Elem(),
 	"LoadBalancer":                  reflect.TypeOf((*LoadBalancer)(nil)).Elem(),
 	"Network":                       reflect.TypeOf((*Network)(nil)).Elem(),
 	"Node":                          reflect.TypeOf((*Node)(nil)).Elem(),
@@ -761,6 +762,41 @@ func (target *Allocation) MarshalBinaryWithContext(ctx *EncodingContext) (err er
 		// --- [end][write][struct](RawAllocationOnlyData) ---
 
 	}
+	// --- [begin][write][alias](LbAllocations) ---
+	if map[string]*LbAllocation(target.LoadBalancers) == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		// --- [begin][write][map](map[string]*LbAllocation) ---
+		buff.WriteInt(len(map[string]*LbAllocation(target.LoadBalancers))) // map length
+		for vv, zz := range map[string]*LbAllocation(target.LoadBalancers) {
+			if ctx.IsStringTable() {
+				d := ctx.Table.AddOrGet(vv)
+				buff.WriteInt(d) // write table index
+			} else {
+				buff.WriteString(vv) // write string
+			}
+			if zz == nil {
+				buff.WriteUInt8(uint8(0)) // write nil byte
+			} else {
+				buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+				// --- [begin][write][struct](LbAllocation) ---
+				buff.WriteInt(0) // [compatibility, unused]
+				errH := zz.MarshalBinaryWithContext(ctx)
+				if errH != nil {
+					return errH
+				}
+				// --- [end][write][struct](LbAllocation) ---
+
+			}
+		}
+		// --- [end][write][map](map[string]*LbAllocation) ---
+
+	}
+	// --- [end][write][alias](LbAllocations) ---
+
 	return nil
 }
 
@@ -1024,6 +1060,55 @@ func (target *Allocation) UnmarshalBinaryWithContext(ctx *DecodingContext) (err
 		// --- [end][read][struct](RawAllocationOnlyData) ---
 
 	}
+	// field version check
+	if uint8(17) <= version {
+		// --- [begin][read][alias](LbAllocations) ---
+		var xx map[string]*LbAllocation
+		if buff.ReadUInt8() == uint8(0) {
+			xx = nil
+		} else {
+			// --- [begin][read][map](map[string]*LbAllocation) ---
+			aaa := buff.ReadInt() // map len
+			yy := make(map[string]*LbAllocation, aaa)
+			for j := 0; j < aaa; j++ {
+				var vv string
+				var ccc string
+				if ctx.IsStringTable() {
+					ddd := buff.ReadInt() // read string index
+					ccc = ctx.Table[ddd]
+				} else {
+					ccc = buff.ReadString() // read string
+				}
+				bbb := ccc
+				vv = bbb
+
+				var zz *LbAllocation
+				if buff.ReadUInt8() == uint8(0) {
+					zz = nil
+				} else {
+					// --- [begin][read][struct](LbAllocation) ---
+					eee := &LbAllocation{}
+					buff.ReadInt() // [compatibility, unused]
+					errH := eee.UnmarshalBinaryWithContext(ctx)
+					if errH != nil {
+						return errH
+					}
+					zz = eee
+					// --- [end][read][struct](LbAllocation) ---
+
+				}
+				yy[vv] = zz
+			}
+			xx = yy
+			// --- [end][read][map](map[string]*LbAllocation) ---
+
+		}
+		target.LoadBalancers = LbAllocations(xx)
+		// --- [end][read][alias](LbAllocations) ---
+
+	} else {
+	}
+
 	return nil
 }
 
@@ -7203,6 +7288,126 @@ func (target *EqualityAudit) UnmarshalBinaryWithContext(ctx *DecodingContext) (e
 	return nil
 }
 
+//--------------------------------------------------------------------------
+//  LbAllocation
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this LbAllocation instance
+// into a byte array
+func (target *LbAllocation) MarshalBinary() (data []byte, err error) {
+	ctx := &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  nil,
+	}
+
+	e := target.MarshalBinaryWithContext(ctx)
+	if e != nil {
+		return nil, e
+	}
+
+	encBytes := ctx.Buffer.Bytes()
+	return encBytes, nil
+}
+
+// MarshalBinaryWithContext serializes the internal properties of this LbAllocation instance
+// into a byte array leveraging a predefined context.
+func (target *LbAllocation) MarshalBinaryWithContext(ctx *EncodingContext) (err error) {
+	// panics are recovered and propagated as errors
+	defer func() {
+		if r := recover(); r != nil {
+			if e, ok := r.(error); ok {
+				err = e
+			} else if s, ok := r.(string); ok {
+				err = fmt.Errorf("Unexpected panic: %s", s)
+			} else {
+				err = fmt.Errorf("Unexpected panic: %+v", r)
+			}
+		}
+	}()
+
+	buff := ctx.Buffer
+	buff.WriteUInt8(AllocationCodecVersion) // version
+
+	if ctx.IsStringTable() {
+		a := ctx.Table.AddOrGet(target.Service)
+		buff.WriteInt(a) // write table index
+	} else {
+		buff.WriteString(target.Service) // write string
+	}
+	buff.WriteFloat64(target.Cost) // write float64
+	return nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the LbAllocation type
+func (target *LbAllocation) UnmarshalBinary(data []byte) error {
+	var table []string
+	buff := util.NewBufferFromBytes(data)
+
+	// string table header validation
+	if isBinaryTag(data, BinaryTagStringTable) {
+		buff.ReadBytes(len(BinaryTagStringTable)) // strip tag length
+		tl := buff.ReadInt()                      // table length
+		if tl > 0 {
+			table = make([]string, tl, tl)
+			for i := 0; i < tl; i++ {
+				table[i] = buff.ReadString()
+			}
+		}
+	}
+
+	ctx := &DecodingContext{
+		Buffer: buff,
+		Table:  table,
+	}
+
+	err := target.UnmarshalBinaryWithContext(ctx)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
+// the LbAllocation type
+func (target *LbAllocation) UnmarshalBinaryWithContext(ctx *DecodingContext) (err error) {
+	// panics are recovered and propagated as errors
+	defer func() {
+		if r := recover(); r != nil {
+			if e, ok := r.(error); ok {
+				err = e
+			} else if s, ok := r.(string); ok {
+				err = fmt.Errorf("Unexpected panic: %s", s)
+			} else {
+				err = fmt.Errorf("Unexpected panic: %+v", r)
+			}
+		}
+	}()
+
+	buff := ctx.Buffer
+	version := buff.ReadUInt8()
+
+	if version > AllocationCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling LbAllocation. Expected %d or less, got %d", AllocationCodecVersion, version)
+	}
+
+	var b string
+	if ctx.IsStringTable() {
+		c := buff.ReadInt() // read string index
+		b = ctx.Table[c]
+	} else {
+		b = buff.ReadString() // read string
+	}
+	a := b
+	target.Service = a
+
+	d := buff.ReadFloat64() // read float64
+	target.Cost = d
+
+	return nil
+}
+
 //--------------------------------------------------------------------------
 //  LoadBalancer
 //--------------------------------------------------------------------------

+ 2 - 2
pkg/kubecost/mock.go

@@ -476,10 +476,10 @@ func GenerateMockAssetSets(start, end time.Time) []*AssetSet {
 	node3Network.Cost = 2.0
 
 	// Add LoadBalancers
-	cluster2LoadBalancer1 := NewLoadBalancer("namespace2/loadBalancer1", "cluster2", "lb1", start, end, NewWindow(&start, &end))
+	cluster2LoadBalancer1 := NewLoadBalancer("namespace2/loadBalancer1", "cluster2", "lb1", start, end, NewWindow(&start, &end), false)
 	cluster2LoadBalancer1.Cost = 10.0
 
-	cluster2LoadBalancer2 := NewLoadBalancer("namespace2/loadBalancer2", "cluster2", "lb2", start, end, NewWindow(&start, &end))
+	cluster2LoadBalancer2 := NewLoadBalancer("namespace2/loadBalancer2", "cluster2", "lb2", start, end, NewWindow(&start, &end), false)
 	cluster2LoadBalancer2.Cost = 15.0
 
 	assetSet1 := NewAssetSet(start, end, cluster1Nodes, cluster2Node1, cluster2Node2, cluster2Node3, cluster2Disk1,

+ 31 - 22
pkg/kubecost/totals.go

@@ -210,6 +210,7 @@ type AssetTotals struct {
 	PersistentVolumeCostAdjustment  float64   `json:"persistentVolumeCostAdjustment"`
 	RAMCost                         float64   `json:"ramCost"`
 	RAMCostAdjustment               float64   `json:"ramCostAdjustment"`
+	PrivateLoadBalancer             bool      `json:"privateLoadBalancer"`
 }
 
 // ClearAdjustments sets all adjustment fields to 0.0
@@ -245,6 +246,7 @@ func (art *AssetTotals) Clone() *AssetTotals {
 		PersistentVolumeCostAdjustment:  art.PersistentVolumeCostAdjustment,
 		RAMCost:                         art.RAMCost,
 		RAMCostAdjustment:               art.RAMCostAdjustment,
+		PrivateLoadBalancer:             art.PrivateLoadBalancer,
 	}
 }
 
@@ -295,7 +297,7 @@ func (art *AssetTotals) TotalCost() float64 {
 // use the fully-qualified (cluster, node) tuple.
 // NOTE: we're not capturing LoadBalancers here yet, but only because we don't
 // yet need them. They could be added.
-func ComputeAssetTotals(as *AssetSet, prop AssetProperty) map[string]*AssetTotals {
+func ComputeAssetTotals(as *AssetSet, byAsset bool) map[string]*AssetTotals {
 	arts := map[string]*AssetTotals{}
 
 	// Attached disks are tracked by matching their name with the name of the
@@ -306,7 +308,7 @@ func ComputeAssetTotals(as *AssetSet, prop AssetProperty) map[string]*AssetTotal
 	for _, node := range as.Nodes {
 		// Default to computing totals by Cluster, but allow override to use Node.
 		key := node.Properties.Cluster
-		if prop == AssetNodeProp {
+		if byAsset {
 			key = fmt.Sprintf("%s/%s", node.Properties.Cluster, node.Properties.Name)
 		}
 
@@ -397,25 +399,30 @@ func ComputeAssetTotals(as *AssetSet, prop AssetProperty) map[string]*AssetTotal
 		arts[key].GPUCostAdjustment += gpuCostAdjustment
 	}
 
-	// Only record LoadBalancer and ClusterManagement when prop
-	// is cluster. We can't breakdown these types by Node.
-	if prop == AssetClusterProp {
-		for _, lb := range as.LoadBalancers {
-			key := lb.Properties.Cluster
+	for _, lb := range as.LoadBalancers {
+		// Default to computing totals by Cluster, but allow override to use LoadBalancer.
+		key := lb.Properties.Cluster
+		if byAsset {
+			key = fmt.Sprintf("%s/%s", lb.Properties.Cluster, lb.Properties.Name)
+		}
 
-			if _, ok := arts[key]; !ok {
-				arts[key] = &AssetTotals{
-					Start:   lb.Start,
-					End:     lb.End,
-					Cluster: lb.Properties.Cluster,
-				}
+		if _, ok := arts[key]; !ok {
+			arts[key] = &AssetTotals{
+				Start:               lb.Start,
+				End:                 lb.End,
+				Cluster:             lb.Properties.Cluster,
+				Node:                lb.Properties.Name,
+				PrivateLoadBalancer: lb.Private,
 			}
-
-			arts[key].Count++
-			arts[key].LoadBalancerCost += lb.Cost
-			arts[key].LoadBalancerCostAdjustment += lb.Adjustment
 		}
 
+		arts[key].LoadBalancerCost += lb.Cost
+		arts[key].LoadBalancerCostAdjustment += lb.Adjustment
+	}
+
+	// Only record ClusterManagement when prop
+	// is cluster. We can't breakdown these types by Node.
+	if !byAsset {
 		for _, cm := range as.ClusterManagement {
 			key := cm.Properties.Cluster
 
@@ -447,7 +454,7 @@ func ComputeAssetTotals(as *AssetSet, prop AssetProperty) map[string]*AssetTotal
 		// cluster/node. But if we're aggregating by cluster only, then
 		// reset the key to just the cluster.
 		key := name
-		if prop == AssetClusterProp {
+		if !byAsset {
 			key = disk.Properties.Cluster
 		}
 
@@ -458,7 +465,7 @@ func ComputeAssetTotals(as *AssetSet, prop AssetProperty) map[string]*AssetTotal
 				Cluster: disk.Properties.Cluster,
 			}
 
-			if prop == AssetNodeProp {
+			if byAsset {
 				arts[key].Node = disk.Properties.Name
 			}
 		}
@@ -471,7 +478,7 @@ func ComputeAssetTotals(as *AssetSet, prop AssetProperty) map[string]*AssetTotal
 			arts[key].Count++
 			arts[key].AttachedVolumeCost += disk.Cost
 			arts[key].AttachedVolumeCostAdjustment += disk.Adjustment
-		} else if prop == AssetClusterProp {
+		} else if !byAsset {
 			// Here, we're looking at a PersistentVolume because we're not
 			// looking at an AttachedVolume. Only record PersistentVolume data
 			// at the cluster level (i.e. prop == AssetClusterProp).
@@ -621,10 +628,10 @@ func UpdateAssetTotalsStore(arts AssetTotalsStore, as *AssetSet) (*AssetTotalsSe
 	start := *as.Window.Start()
 	end := *as.Window.End()
 
-	artsByCluster := ComputeAssetTotals(as, AssetClusterProp)
+	artsByCluster := ComputeAssetTotals(as, false)
 	arts.SetAssetTotalsByCluster(start, end, artsByCluster)
 
-	artsByNode := ComputeAssetTotals(as, AssetNodeProp)
+	artsByNode := ComputeAssetTotals(as, true)
 	arts.SetAssetTotalsByNode(start, end, artsByNode)
 
 	log.Debugf("ETL: Asset: updated resource totals for %s", as.Window)
@@ -730,6 +737,8 @@ func (mts *MemoryTotalsStore) GetAssetTotalsByCluster(start time.Time, end time.
 func (mts *MemoryTotalsStore) GetAssetTotalsByNode(start time.Time, end time.Time) (map[string]*AssetTotals, bool) {
 	k := storeKey(start, end)
 	if raw, ok := mts.assetTotalsByNode.Get(k); !ok {
+		// it's possible that after accumulation, the time chunks stored here
+		// are being queried combined
 		return map[string]*AssetTotals{}, false
 	} else {
 		original := raw.(map[string]*AssetTotals)