Просмотр исходного кода

Merge pull request #989 from kubecost/kaelan-custompricingfix

Add logic for custom pricing values in assets.
Kaelan Patel 4 лет назад
Родитель
Сommit
7a16d04e69
3 измененных файлов с 291 добавлено и 18 удалено
  1. 33 8
      pkg/costmodel/cluster.go
  2. 97 7
      pkg/costmodel/cluster_helpers.go
  3. 161 3
      pkg/costmodel/cluster_helpers_test.go

+ 33 - 8
pkg/costmodel/cluster.go

@@ -2,6 +2,7 @@ package costmodel
 
 import (
 	"fmt"
+	"strconv"
 	"time"
 
 	"github.com/kubecost/cost-model/pkg/util/timeutil"
@@ -169,7 +170,7 @@ func ClusterDisks(client prometheus.Client, provider cloud.Provider, duration, o
 
 	diskMap := map[string]*Disk{}
 
-	pvCosts(diskMap, resolution, resActiveMins, resPVSize, resPVCost)
+	pvCosts(diskMap, resolution, resActiveMins, resPVSize, resPVCost, provider)
 
 	for _, result := range resLocalStorageCost {
 		cluster, err := result.GetString(env.GetPromClusterLabel())
@@ -442,23 +443,23 @@ func ClusterNodes(cp cloud.Provider, client prometheus.Client, duration, offset
 	activeDataMap := buildActiveDataMap(resActiveMins, resolution)
 
 	gpuCountMap := buildGPUCountMap(resNodeGPUCount)
+	preemptibleMap := buildPreemptibleMap(resIsSpot)
 
-	cpuCostMap, clusterAndNameToType1 := buildCPUCostMap(resNodeCPUHourlyCost)
-	ramCostMap, clusterAndNameToType2 := buildRAMCostMap(resNodeRAMHourlyCost)
-	gpuCostMap, clusterAndNameToType3 := buildGPUCostMap(resNodeGPUHourlyCost, gpuCountMap)
+	cpuCostMap, clusterAndNameToType1 := buildCPUCostMap(resNodeCPUHourlyCost, cp, preemptibleMap)
+	ramCostMap, clusterAndNameToType2 := buildRAMCostMap(resNodeRAMHourlyCost, cp, preemptibleMap)
+	gpuCostMap, clusterAndNameToType3 := buildGPUCostMap(resNodeGPUHourlyCost, gpuCountMap, cp, preemptibleMap)
 
 	clusterAndNameToTypeIntermediate := mergeTypeMaps(clusterAndNameToType1, clusterAndNameToType2)
 	clusterAndNameToType := mergeTypeMaps(clusterAndNameToTypeIntermediate, clusterAndNameToType3)
 
 	cpuCoresMap := buildCPUCoresMap(resNodeCPUCores)
-
 	ramBytesMap := buildRAMBytesMap(resNodeRAMBytes)
 
 	ramUserPctMap := buildRAMUserPctMap(resNodeRAMUserPct)
 	ramSystemPctMap := buildRAMSystemPctMap(resNodeRAMSystemPct)
 
 	cpuBreakdownMap := buildCPUBreakdownMap(resNodeCPUModeTotal)
-	preemptibleMap := buildPreemptibleMap(resIsSpot)
+
 	labelsMap := buildLabelsMap(resLabels)
 
 	costTimesMinuteAndCount(activeDataMap, cpuCostMap, cpuCoresMap)
@@ -1072,7 +1073,7 @@ func ClusterCostsOverTime(cli prometheus.Client, provider cloud.Provider, startS
 	}, nil
 }
 
-func pvCosts(diskMap map[string]*Disk, resolution time.Duration, resActiveMins, resPVSize, resPVCost []*prom.QueryResult) {
+func pvCosts(diskMap map[string]*Disk, resolution time.Duration, resActiveMins, resPVSize, resPVCost []*prom.QueryResult, cp cloud.Provider) {
 	for _, result := range resActiveMins {
 		cluster, err := result.GetString(env.GetPromClusterLabel())
 		if err != nil {
@@ -1134,6 +1135,12 @@ func pvCosts(diskMap map[string]*Disk, resolution time.Duration, resActiveMins,
 		diskMap[key].Bytes = bytes
 	}
 
+	customPricingEnabled := cloud.CustomPricesEnabled(cp)
+	customPricingConfig, err := cp.GetConfig()
+	if err != nil {
+		log.Warningf("ClusterDisks: failed to load custom pricing: %s", err)
+	}
+
 	for _, result := range resPVCost {
 		cluster, err := result.GetString(env.GetPromClusterLabel())
 		if err != nil {
@@ -1148,7 +1155,25 @@ func pvCosts(diskMap map[string]*Disk, resolution time.Duration, resActiveMins,
 
 		// TODO niko/assets storage class
 
-		cost := result.Values[0].Value
+		var cost float64
+
+		if customPricingEnabled && customPricingConfig != nil {
+
+			customPVCostStr := customPricingConfig.Storage
+
+			customPVCost, err := strconv.ParseFloat(customPVCostStr, 64)
+			if err != nil {
+				log.Warningf("ClusterDisks: error parsing custom PV price: %s", customPVCostStr)
+			}
+
+			cost = customPVCost
+
+		} else {
+
+			cost = result.Values[0].Value
+
+		}
+
 		key := fmt.Sprintf("%s/%s", cluster, name)
 		if _, ok := diskMap[key]; !ok {
 			diskMap[key] = &Disk{

+ 97 - 7
pkg/costmodel/cluster_helpers.go

@@ -1,9 +1,11 @@
 package costmodel
 
 import (
-	"github.com/kubecost/cost-model/pkg/cloud"
+	"strconv"
 	"time"
 
+	"github.com/kubecost/cost-model/pkg/cloud"
+
 	"github.com/kubecost/cost-model/pkg/env"
 	"github.com/kubecost/cost-model/pkg/log"
 	"github.com/kubecost/cost-model/pkg/prom"
@@ -28,6 +30,8 @@ func mergeTypeMaps(clusterAndNameToType1, clusterAndNameToType2 map[nodeIdentifi
 
 func buildCPUCostMap(
 	resNodeCPUCost []*prom.QueryResult,
+	cp cloud.Provider,
+	preemptible map[NodeIdentifier]bool,
 ) (
 	map[NodeIdentifier]float64,
 	map[nodeIdentifierNoProviderID]string,
@@ -36,6 +40,12 @@ func buildCPUCostMap(
 	cpuCostMap := make(map[NodeIdentifier]float64)
 	clusterAndNameToType := make(map[nodeIdentifierNoProviderID]string)
 
+	customPricingEnabled := cloud.CustomPricesEnabled(cp)
+	customPricingConfig, err := cp.GetConfig()
+	if err != nil {
+		log.Warningf("ClusterNodes: failed to load custom pricing: %s", err)
+	}
+
 	for _, result := range resNodeCPUCost {
 		cluster, err := result.GetString(env.GetPromClusterLabel())
 		if err != nil {
@@ -51,8 +61,6 @@ func buildCPUCostMap(
 		nodeType, _ := result.GetString("instance_type")
 		providerID, _ := result.GetString("provider_id")
 
-		cpuCost := result.Values[0].Value
-
 		key := NodeIdentifier{
 			Cluster:    cluster,
 			Name:       name,
@@ -63,6 +71,29 @@ func buildCPUCostMap(
 			Name:    name,
 		}
 
+		var cpuCost float64
+
+		if customPricingEnabled && customPricingConfig != nil {
+
+			var customCPUStr string
+			if spot, ok := preemptible[key]; ok && spot {
+				customCPUStr = customPricingConfig.SpotCPU
+			} else {
+				customCPUStr = customPricingConfig.CPU
+			}
+
+			customCPUCost, err := strconv.ParseFloat(customCPUStr, 64)
+			if err != nil {
+				log.Warningf("ClusterNodes: error parsing custom CPU price: %s", customCPUStr)
+			}
+			cpuCost = customCPUCost
+
+		} else {
+
+			cpuCost = result.Values[0].Value
+
+		}
+
 		clusterAndNameToType[keyNon] = nodeType
 
 		cpuCostMap[key] = cpuCost
@@ -73,6 +104,8 @@ func buildCPUCostMap(
 
 func buildRAMCostMap(
 	resNodeRAMCost []*prom.QueryResult,
+	cp cloud.Provider,
+	preemptible map[NodeIdentifier]bool,
 ) (
 	map[NodeIdentifier]float64,
 	map[nodeIdentifierNoProviderID]string,
@@ -81,6 +114,12 @@ func buildRAMCostMap(
 	ramCostMap := make(map[NodeIdentifier]float64)
 	clusterAndNameToType := make(map[nodeIdentifierNoProviderID]string)
 
+	customPricingEnabled := cloud.CustomPricesEnabled(cp)
+	customPricingConfig, err := cp.GetConfig()
+	if err != nil {
+		log.Warningf("ClusterNodes: failed to load custom pricing: %s", err)
+	}
+
 	for _, result := range resNodeRAMCost {
 		cluster, err := result.GetString(env.GetPromClusterLabel())
 		if err != nil {
@@ -96,8 +135,6 @@ func buildRAMCostMap(
 		nodeType, _ := result.GetString("instance_type")
 		providerID, _ := result.GetString("provider_id")
 
-		ramCost := result.Values[0].Value
-
 		key := NodeIdentifier{
 			Cluster:    cluster,
 			Name:       name,
@@ -108,7 +145,31 @@ func buildRAMCostMap(
 			Name:    name,
 		}
 
+		var ramCost float64
+
+		if customPricingEnabled && customPricingConfig != nil {
+
+			var customRAMStr string
+			if spot, ok := preemptible[key]; ok && spot {
+				customRAMStr = customPricingConfig.SpotRAM
+			} else {
+				customRAMStr = customPricingConfig.RAM
+			}
+
+			customRAMCost, err := strconv.ParseFloat(customRAMStr, 64)
+			if err != nil {
+				log.Warningf("ClusterNodes: error parsing custom RAM price: %s", customRAMStr)
+			}
+			ramCost = customRAMCost / 1024 / 1024 / 1024
+
+		} else {
+
+			ramCost = result.Values[0].Value
+
+		}
+
 		clusterAndNameToType[keyNon] = nodeType
+
 		ramCostMap[key] = ramCost
 	}
 
@@ -118,6 +179,8 @@ func buildRAMCostMap(
 func buildGPUCostMap(
 	resNodeGPUCost []*prom.QueryResult,
 	gpuCountMap map[NodeIdentifier]float64,
+	cp cloud.Provider,
+	preemptible map[NodeIdentifier]bool,
 ) (
 	map[NodeIdentifier]float64,
 	map[nodeIdentifierNoProviderID]string,
@@ -126,6 +189,12 @@ func buildGPUCostMap(
 	gpuCostMap := make(map[NodeIdentifier]float64)
 	clusterAndNameToType := make(map[nodeIdentifierNoProviderID]string)
 
+	customPricingEnabled := cloud.CustomPricesEnabled(cp)
+	customPricingConfig, err := cp.GetConfig()
+	if err != nil {
+		log.Warningf("ClusterNodes: failed to load custom pricing: %s", err)
+	}
+
 	for _, result := range resNodeGPUCost {
 		cluster, err := result.GetString(env.GetPromClusterLabel())
 		if err != nil {
@@ -141,8 +210,6 @@ func buildGPUCostMap(
 		nodeType, _ := result.GetString("instance_type")
 		providerID, _ := result.GetString("provider_id")
 
-		gpuCost := result.Values[0].Value
-
 		key := NodeIdentifier{
 			Cluster:    cluster,
 			Name:       name,
@@ -153,6 +220,29 @@ func buildGPUCostMap(
 			Name:    name,
 		}
 
+		var gpuCost float64
+
+		if customPricingEnabled && customPricingConfig != nil {
+
+			var customGPUStr string
+			if spot, ok := preemptible[key]; ok && spot {
+				customGPUStr = customPricingConfig.SpotGPU
+			} else {
+				customGPUStr = customPricingConfig.GPU
+			}
+
+			customGPUCost, err := strconv.ParseFloat(customGPUStr, 64)
+			if err != nil {
+				log.Warningf("ClusterNodes: error parsing custom GPU price: %s", customGPUStr)
+			}
+			gpuCost = customGPUCost
+
+		} else {
+
+			gpuCost = result.Values[0].Value
+
+		}
+
 		clusterAndNameToType[keyNon] = nodeType
 
 		// If gpu count is available use it to multiply gpu cost

+ 161 - 3
pkg/costmodel/cluster_helpers_test.go

@@ -1,12 +1,14 @@
 package costmodel
 
 import (
-	"github.com/kubecost/cost-model/pkg/prom"
-	"github.com/kubecost/cost-model/pkg/util"
 	"reflect"
 	"testing"
 	"time"
 
+	"github.com/kubecost/cost-model/pkg/cloud"
+	"github.com/kubecost/cost-model/pkg/prom"
+	"github.com/kubecost/cost-model/pkg/util"
+
 	"github.com/davecgh/go-spew/spew"
 )
 
@@ -849,10 +851,166 @@ func TestBuildGPUCostMap(t *testing.T) {
 
 	for _, testCase := range cases {
 		t.Run(testCase.name, func(t *testing.T) {
-			result, _ := buildGPUCostMap(testCase.promResult, testCase.countMap)
+			testProvider := &cloud.CustomProvider{
+				Config: cloud.NewProviderConfig("fakeFile"),
+			}
+			testPreemptible := make(map[NodeIdentifier]bool)
+			result, _ := buildGPUCostMap(testCase.promResult, testCase.countMap, testProvider, testPreemptible)
 			if !reflect.DeepEqual(result, testCase.expected) {
 				t.Errorf("buildGPUCostMap case %s failed. Got %+v but expected %+v", testCase.name, result, testCase.expected)
 			}
 		})
 	}
 }
+
+func TestAssetCustompricing(t *testing.T) {
+
+	nodePromResult := []*prom.QueryResult{
+		{
+			Metric: map[string]interface{}{
+				"cluster_id":    "cluster1",
+				"node":          "node1",
+				"instance_type": "type1",
+				"provider_id":   "provider1",
+			},
+			Values: []*util.Vector{
+				&util.Vector{
+					Timestamp: 0,
+					Value:     0.5,
+				},
+			},
+		},
+	}
+
+	pvCostPromResult := []*prom.QueryResult{
+		{
+			Metric: map[string]interface{}{
+				"cluster_id":       "cluster1",
+				"persistentvolume": "pvc1",
+				"provider_id":      "provider1",
+			},
+			Values: []*util.Vector{
+				&util.Vector{
+					Timestamp: 0,
+					Value:     1.0,
+				},
+			},
+		},
+	}
+
+	pvSizePromResult := []*prom.QueryResult{
+		{
+			Metric: map[string]interface{}{
+				"cluster_id":       "cluster1",
+				"persistentvolume": "pvc1",
+				"provider_id":      "provider1",
+			},
+			Values: []*util.Vector{
+				&util.Vector{
+					Timestamp: 0,
+					Value:     1073741824.0,
+				},
+			},
+		},
+	}
+
+	pvMinsPromResult := []*prom.QueryResult{
+		{
+			Metric: map[string]interface{}{
+				"cluster_id":       "cluster1",
+				"persistentvolume": "pvc1",
+				"provider_id":      "provider1",
+			},
+			Values: []*util.Vector{
+				&util.Vector{
+					Timestamp: 0,
+					Value:     60.0,
+				},
+			},
+		},
+	}
+
+	gpuCountMap := map[NodeIdentifier]float64{
+		NodeIdentifier{
+			Cluster:    "cluster1",
+			Name:       "node1",
+			ProviderID: "provider1",
+		}: 2,
+	}
+
+	nodeKey := NodeIdentifier{
+		Cluster:    "cluster1",
+		Name:       "node1",
+		ProviderID: "provider1",
+	}
+
+	cases := []struct {
+		name             string
+		customPricingMap map[string]string
+		expectedPricing  map[string]float64
+	}{
+		{
+			name:             "No custom pricing",
+			customPricingMap: map[string]string{},
+			expectedPricing: map[string]float64{
+				"CPU":     0.5,
+				"RAM":     0.5,
+				"GPU":     1.0,
+				"Storage": 1.0,
+			},
+		},
+		{
+			name: "Custom pricing enabled",
+			customPricingMap: map[string]string{
+				"CPU":                 "20.0",
+				"RAM":                 "4.0",
+				"GPU":                 "500.0",
+				"Storage":             "0.1",
+				"customPricesEnabled": "true",
+			},
+			expectedPricing: map[string]float64{
+				"CPU":     0.027397,              // 20.0 / 730
+				"RAM":     5.102716386318207e-12, // 4.0 / 730 / 1024^3
+				"GPU":     1.369864,              // 500.0 / 730 * 2
+				"Storage": 0.000137,              // 0.1 / 730 * (1073741824.0 / 1024 / 1024 / 1024) * (60 / 60) => 0.1 / 730 * 1 * 1
+			},
+		},
+	}
+
+	for _, testCase := range cases {
+		t.Run(testCase.name, func(t *testing.T) {
+			testProvider := &cloud.CustomProvider{
+				Config: cloud.NewProviderConfig(""),
+			}
+			testProvider.UpdateConfigFromConfigMap(testCase.customPricingMap)
+
+			testPreemptible := make(map[NodeIdentifier]bool)
+			cpuMap, _ := buildCPUCostMap(nodePromResult, testProvider, testPreemptible)
+			ramMap, _ := buildRAMCostMap(nodePromResult, testProvider, testPreemptible)
+			gpuMap, _ := buildGPUCostMap(nodePromResult, gpuCountMap, testProvider, testPreemptible)
+
+			cpuResult := cpuMap[nodeKey]
+			ramResult := ramMap[nodeKey]
+			gpuResult := gpuMap[nodeKey]
+
+			diskMap := map[string]*Disk{}
+			pvCosts(diskMap, time.Hour, pvMinsPromResult, pvSizePromResult, pvCostPromResult, testProvider)
+
+			diskResult := diskMap["cluster1/pvc1"].Cost
+
+			if !util.IsApproximately(cpuResult, testCase.expectedPricing["CPU"]) {
+				t.Errorf("CPU custom pricing error in %s. Got %v but expected %v", testCase.name, cpuResult, testCase.expectedPricing["CPU"])
+			}
+			if !util.IsApproximately(ramResult, testCase.expectedPricing["RAM"]) {
+				t.Errorf("RAM custom pricing error in %s. Got %v but expected %v", testCase.name, ramResult, testCase.expectedPricing["RAM"])
+			}
+			if !util.IsApproximately(gpuResult, testCase.expectedPricing["GPU"]) {
+				t.Errorf("GPU custom pricing error in %s. Got %v but expected %v", testCase.name, gpuResult, testCase.expectedPricing["GPU"])
+			}
+			if !util.IsApproximately(diskResult, testCase.expectedPricing["Storage"]) {
+				t.Errorf("Disk custom pricing error in %s. Got %v but expected %v", testCase.name, diskResult, testCase.expectedPricing["Storage"])
+			}
+		})
+	}
+
+}