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

Firt pass at getting network costs into cost-model

Matt Bolt 6 лет назад
Родитель
Сommit
249cf38395
11 измененных файлов с 528 добавлено и 46 удалено
  1. 3 0
      cloud/aws.json
  2. 26 0
      cloud/awsprovider.go
  3. 3 0
      cloud/azure.json
  4. 26 0
      cloud/azureprovider.go
  5. 4 1
      cloud/default.json
  6. 3 0
      cloud/gcp.json
  7. 26 0
      cloud/gcpprovider.go
  8. 82 41
      cloud/provider.go
  9. 335 2
      costmodel/costmodel.go
  10. 0 2
      go.mod
  11. 20 0
      main.go

+ 3 - 0
cloud/aws.json

@@ -7,6 +7,9 @@
     "GPU": "0.95",
     "spotRAM": "0.000892",
     "storage": "0.00005479452",
+    "zoneNetworkEgress": "0.01",
+    "regionNetworkEgress": "0.01",
+    "internetNetworkEgress": "0.15",
     "spotLabel": "kops.k8s.io/instancegroup",
     "spotLabelValue": "spotinstance-nodes",
     "awsServiceKeyName": "AKIAXW6UVLRRY5RQGGUX",

+ 26 - 0
cloud/awsprovider.go

@@ -663,6 +663,32 @@ func (aws *AWS) DownloadPricingData() error {
 	return nil
 }
 
+// Stubbed NetworkPricing for AWS. Pull directly from aws.json for now
+func (c *AWS) NetworkPricing() (*Network, error) {
+	cpricing, err := GetDefaultPricingData("aws.json")
+	if err != nil {
+		return nil, err
+	}
+	znec, err := strconv.ParseFloat(cpricing.ZoneNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	rnec, err := strconv.ParseFloat(cpricing.RegionNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	inec, err := strconv.ParseFloat(cpricing.InternetNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Network{
+		ZoneNetworkEgressCost:     znec,
+		RegionNetworkEgressCost:   rnec,
+		InternetNetworkEgressCost: inec,
+	}, nil
+}
+
 // AllNodePricing returns all the billing data fetched.
 func (aws *AWS) AllNodePricing() (interface{}, error) {
 	aws.DownloadPricingDataLock.RLock()

+ 3 - 0
cloud/azure.json

@@ -6,6 +6,9 @@
     "RAM": "0.001917", 
     "spotRAM": "0.000382",
     "storage": "0.00005479452" ,
+    "zoneNetworkEgress": "0.01",
+    "regionNetworkEgress": "0.01",
+    "internetNetworkEgress": "0.12",
     "azureSubscriptionID": "",
     "azureClientID": "" ,
     "azureClientSecret": "" ,

+ 26 - 0
cloud/azureprovider.go

@@ -429,6 +429,32 @@ func (az *Azure) NodePricing(key Key) (*Node, error) {
 	}, nil
 }
 
+// Stubbed NetworkPricing for Azure. Pull directly from azure.json for now
+func (c *Azure) NetworkPricing() (*Network, error) {
+	cpricing, err := GetDefaultPricingData("azure.json")
+	if err != nil {
+		return nil, err
+	}
+	znec, err := strconv.ParseFloat(cpricing.ZoneNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	rnec, err := strconv.ParseFloat(cpricing.RegionNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	inec, err := strconv.ParseFloat(cpricing.InternetNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Network{
+		ZoneNetworkEgressCost:     znec,
+		RegionNetworkEgressCost:   rnec,
+		InternetNetworkEgressCost: inec,
+	}, nil
+}
+
 type azurePvKey struct {
 	Labels                 map[string]string
 	StorageClass           string

+ 4 - 1
cloud/default.json

@@ -6,5 +6,8 @@
     "RAM": "0.004237",
     "spotRAM": "0.000892",
     "GPU": "0.95",
-    "storage": "0.00005479452"
+    "storage": "0.00005479452",
+    "zoneNetworkEgress": "0.01",
+    "regionNetworkEgress": "0.01",
+    "internetNetworkEgress": "0.12"
 }

+ 3 - 0
cloud/gcp.json

@@ -7,5 +7,8 @@
     "spotRAM": "0.000892",
     "projectID": "guestbook-227502",
     "storage": "0.00005479452",
+    "zoneNetworkEgress": "0.01",
+    "regionNetworkEgress": "0.01",
+    "internetNetworkEgress": "0.12",
     "billingDataDataset": "billing_data.gcp_billing_export_v1_01AC9F_74CF1D_5565A2"
 }

+ 26 - 0
cloud/gcpprovider.go

@@ -716,6 +716,32 @@ func (gcp *GCP) PVPricing(pvk PVKey) (*PV, error) {
 	return pricing.PV, nil
 }
 
+// Stubbed NetworkPricing for GCP. Pull directly from gcp.json for now
+func (c *GCP) NetworkPricing() (*Network, error) {
+	cpricing, err := GetDefaultPricingData("gcp.json")
+	if err != nil {
+		return nil, err
+	}
+	znec, err := strconv.ParseFloat(cpricing.ZoneNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	rnec, err := strconv.ParseFloat(cpricing.RegionNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	inec, err := strconv.ParseFloat(cpricing.InternetNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Network{
+		ZoneNetworkEgressCost:     znec,
+		RegionNetworkEgressCost:   rnec,
+		InternetNetworkEgressCost: inec,
+	}, nil
+}
+
 type pvKey struct {
 	Labels                 map[string]string
 	StorageClass           string

+ 82 - 41
cloud/provider.go

@@ -9,6 +9,7 @@ import (
 	"net/url"
 	"os"
 	"reflect"
+	"strconv"
 	"strings"
 	"sync"
 
@@ -42,6 +43,14 @@ type Node struct {
 	GPUCost          string `json:"gpuCost"`
 }
 
+// Network is the interface by which the provider and cost model communicate network egress prices.
+// The provider will best-effort try to fill out this struct.
+type Network struct {
+	ZoneNetworkEgressCost     float64
+	RegionNetworkEgressCost   float64
+	InternetNetworkEgressCost float64
+}
+
 // PV is the interface by which the provider and cost model communicate PV prices.
 // The provider will best-effort try to fill out this struct.
 type PV struct {
@@ -81,6 +90,7 @@ type Provider interface {
 	GetDisks() ([]byte, error)
 	NodePricing(Key) (*Node, error)
 	PVPricing(PVKey) (*PV, error)
+	NetworkPricing() (*Network, error)
 	AllNodePricing() (interface{}, error)
 	DownloadPricingData() error
 	GetKey(map[string]string) Key
@@ -118,15 +128,18 @@ func GetDefaultPricingData(fname string) (*CustomPricing, error) {
 		return customPricing, nil
 	} else if os.IsNotExist(err) {
 		c := &CustomPricing{
-			Provider:            fname,
-			Description:         "Default prices based on GCP us-central1",
-			CPU:                 "0.031611",
-			SpotCPU:             "0.006655",
-			RAM:                 "0.004237",
-			SpotRAM:             "0.000892",
-			GPU:                 "0.95",
-			Storage:             "0.00005479452",
-			CustomPricesEnabled: "false",
+			Provider:              fname,
+			Description:           "Default prices based on GCP us-central1",
+			CPU:                   "0.031611",
+			SpotCPU:               "0.006655",
+			RAM:                   "0.004237",
+			SpotRAM:               "0.000892",
+			GPU:                   "0.95",
+			Storage:               "0.00005479452",
+			ZoneNetworkEgress:     "0.01",
+			RegionNetworkEgress:   "0.01",
+			InternetNetworkEgress: "0.12",
+			CustomPricesEnabled:   "false",
 		}
 		cj, err := json.Marshal(c)
 		if err != nil {
@@ -146,38 +159,41 @@ func GetDefaultPricingData(fname string) (*CustomPricing, error) {
 const KeyUpdateType = "athenainfo"
 
 type CustomPricing struct {
-	Provider            string `json:"provider"`
-	Description         string `json:"description"`
-	CPU                 string `json:"CPU"`
-	SpotCPU             string `json:"spotCPU"`
-	RAM                 string `json:"RAM"`
-	SpotRAM             string `json:"spotRAM"`
-	GPU                 string `json:"GPU"`
-	SpotGPU             string `json:"spotGPU"`
-	Storage             string `json:"storage"`
-	SpotLabel           string `json:"spotLabel,omitempty"`
-	SpotLabelValue      string `json:"spotLabelValue,omitempty"`
-	GpuLabel            string `json:"gpuLabel,omitempty"`
-	GpuLabelValue       string `json:"gpuLabelValue,omitempty"`
-	ServiceKeyName      string `json:"awsServiceKeyName,omitempty"`
-	ServiceKeySecret    string `json:"awsServiceKeySecret,omitempty"`
-	SpotDataRegion      string `json:"awsSpotDataRegion,omitempty"`
-	SpotDataBucket      string `json:"awsSpotDataBucket,omitempty"`
-	SpotDataPrefix      string `json:"awsSpotDataPrefix,omitempty"`
-	ProjectID           string `json:"projectID,omitempty"`
-	AthenaBucketName    string `json:"athenaBucketName"`
-	AthenaRegion        string `json:"athenaRegion"`
-	AthenaDatabase      string `json:"athenaDatabase"`
-	AthenaTable         string `json:"athenaTable"`
-	BillingDataDataset  string `json:"billingDataDataset,omitempty"`
-	CustomPricesEnabled string `json:"customPricesEnabled"`
-	AzureSubscriptionID string `json:"azureSubscriptionID"`
-	AzureClientID       string `json:"azureClientID"`
-	AzureClientSecret   string `json:"azureClientSecret"`
-	AzureTenantID       string `json:"azureTenantID"`
-	CurrencyCode        string `json:"currencyCode"`
-	Discount            string `json:"discount"`
-	ClusterName         string `json:"clusterName"`
+	Provider              string `json:"provider"`
+	Description           string `json:"description"`
+	CPU                   string `json:"CPU"`
+	SpotCPU               string `json:"spotCPU"`
+	RAM                   string `json:"RAM"`
+	SpotRAM               string `json:"spotRAM"`
+	GPU                   string `json:"GPU"`
+	SpotGPU               string `json:"spotGPU"`
+	Storage               string `json:"storage"`
+	ZoneNetworkEgress     string `json:"zoneNetworkEgress"`
+	RegionNetworkEgress   string `json:"regionNetworkEgress"`
+	InternetNetworkEgress string `json:"internetNetworkEgress"`
+	SpotLabel             string `json:"spotLabel,omitempty"`
+	SpotLabelValue        string `json:"spotLabelValue,omitempty"`
+	GpuLabel              string `json:"gpuLabel,omitempty"`
+	GpuLabelValue         string `json:"gpuLabelValue,omitempty"`
+	ServiceKeyName        string `json:"awsServiceKeyName,omitempty"`
+	ServiceKeySecret      string `json:"awsServiceKeySecret,omitempty"`
+	SpotDataRegion        string `json:"awsSpotDataRegion,omitempty"`
+	SpotDataBucket        string `json:"awsSpotDataBucket,omitempty"`
+	SpotDataPrefix        string `json:"awsSpotDataPrefix,omitempty"`
+	ProjectID             string `json:"projectID,omitempty"`
+	AthenaBucketName      string `json:"athenaBucketName"`
+	AthenaRegion          string `json:"athenaRegion"`
+	AthenaDatabase        string `json:"athenaDatabase"`
+	AthenaTable           string `json:"athenaTable"`
+	BillingDataDataset    string `json:"billingDataDataset,omitempty"`
+	CustomPricesEnabled   string `json:"customPricesEnabled"`
+	AzureSubscriptionID   string `json:"azureSubscriptionID"`
+	AzureClientID         string `json:"azureClientID"`
+	AzureClientSecret     string `json:"azureClientSecret"`
+	AzureTenantID         string `json:"azureTenantID"`
+	CurrencyCode          string `json:"currencyCode"`
+	Discount              string `json:"discount"`
+	ClusterName           string `json:"clusterName"`
 }
 
 func SetCustomPricingField(obj *CustomPricing, name string, value string) error {
@@ -405,6 +421,31 @@ func (c *CustomProvider) PVPricing(pvk PVKey) (*PV, error) {
 	}, nil
 }
 
+func (c *CustomProvider) NetworkPricing() (*Network, error) {
+	cpricing, err := GetDefaultPricingData("default")
+	if err != nil {
+		return nil, err
+	}
+	znec, err := strconv.ParseFloat(cpricing.ZoneNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	rnec, err := strconv.ParseFloat(cpricing.RegionNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	inec, err := strconv.ParseFloat(cpricing.InternetNetworkEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Network{
+		ZoneNetworkEgressCost:     znec,
+		RegionNetworkEgressCost:   rnec,
+		InternetNetworkEgressCost: inec,
+	}, nil
+}
+
 func (*CustomProvider) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string) PVKey {
 	return &awsPVKey{
 		Labels:           pv.Labels,

+ 335 - 2
costmodel/costmodel.go

@@ -111,6 +111,7 @@ type CostData struct {
 	CPUAllocation   []*Vector                    `json:"cpuallocated,omitempty"`
 	GPUReq          []*Vector                    `json:"gpureq,omitempty"`
 	PVCData         []*PersistentVolumeClaimData `json:"pvcData,omitempty"`
+	NetworkData     []*Vector                    `json:"network,omitempty"`
 	Labels          map[string]string            `json:"labels,omitempty"`
 	NamespaceLabels map[string]string            `json:"namespaceLabels,omitempty"`
 }
@@ -172,7 +173,10 @@ const (
 						* 
 						on (persistentvolumeclaim, namespace) group_right(storageclass, volumename) 
 				sum(kube_persistentvolumeclaim_resource_requests_storage_bytes) by (persistentvolumeclaim, namespace)`
-	normalizationStr = `max(count_over_time(kube_pod_container_resource_requests_memory_bytes{}[%s] %s))`
+	queryZoneNetworkUsage     = `sum(increase(kubecost_pod_network_egress_bytes_total{internet="false", sameZone="false", sameRegion="true"}[%s] %s)) by (namespace,pod_name) / 1024 / 1024 / 1024`
+	queryRegionNetworkUsage   = `sum(increase(kubecost_pod_network_egress_bytes_total{internet="false", sameZone="false", sameRegion="false"}[%s] %s)) by (namespace,pod_name) / 1024 / 1024 / 1024`
+	queryInternetNetworkUsage = `sum(increase(kubecost_pod_network_egress_bytes_total{internet="true"}[%s] %s)) by (namespace,pod_name) / 1024 / 1024 / 1024`
+	normalizationStr          = `max(count_over_time(kube_pod_container_resource_requests_memory_bytes{}[%s] %s))`
 )
 
 type PrometheusMetadata struct {
@@ -288,10 +292,13 @@ func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, clientset kube
 	queryCPUUsage := fmt.Sprintf(queryCPUUsageStr, window, offset)
 	queryGPURequests := fmt.Sprintf(queryGPURequestsStr, window, offset, window, offset)
 	queryPVRequests := fmt.Sprintf(queryPVRequestsStr)
+	queryNetZoneRequests := fmt.Sprintf(queryZoneNetworkUsage, window, "")
+	queryNetRegionRequests := fmt.Sprintf(queryRegionNetworkUsage, window, "")
+	queryNetInternetRequests := fmt.Sprintf(queryInternetNetworkUsage, window, "")
 	normalization := fmt.Sprintf(normalizationStr, window, offset)
 
 	var wg sync.WaitGroup
-	wg.Add(8)
+	wg.Add(11)
 
 	var promErr error
 	var resultRAMRequests interface{}
@@ -324,6 +331,21 @@ func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, clientset kube
 		resultPVRequests, promErr = query(cli, queryPVRequests)
 		defer wg.Done()
 	}()
+	var resultNetZoneRequests interface{}
+	go func() {
+		resultNetZoneRequests, promErr = query(cli, queryNetZoneRequests)
+		defer wg.Done()
+	}()
+	var resultNetRegionRequests interface{}
+	go func() {
+		resultNetRegionRequests, promErr = query(cli, queryNetRegionRequests)
+		defer wg.Done()
+	}()
+	var resultNetInternetRequests interface{}
+	go func() {
+		resultNetInternetRequests, promErr = query(cli, queryNetInternetRequests)
+		defer wg.Done()
+	}()
 	var normalizationResult interface{}
 	go func() {
 		normalizationResult, promErr = query(cli, normalization)
@@ -385,6 +407,12 @@ func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, clientset kube
 		}
 	}
 
+	networkUsageMap, err := getNetworkUsageData(resultNetZoneRequests, resultNetRegionRequests, resultNetInternetRequests, false)
+	if err != nil {
+		klog.V(1).Infof("Unable to get Network Cost Data: %s", err.Error())
+		return nil, err
+	}
+
 	containerNameCost := make(map[string]*CostData)
 	containers := make(map[string]bool)
 
@@ -481,6 +509,16 @@ func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, clientset kube
 				}
 			}
 
+			var podNetCosts []*Vector
+			if usage, ok := networkUsageMap[ns+","+podName]; ok {
+				netCosts, err := getNetworkCost(usage, cloud)
+				if err != nil {
+					klog.V(3).Infof("Error pulling network costs: %s", err.Error())
+				} else {
+					podNetCosts = netCosts
+				}
+			}
+
 			var podServices []string
 			if _, ok := podServicesMapping[ns]; ok {
 				if svcs, ok := podServicesMapping[ns][pod.GetObjectMeta().GetName()]; ok {
@@ -523,8 +561,10 @@ func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, clientset kube
 				}
 
 				var pvReq []*PersistentVolumeClaimData
+				var netReq []*Vector
 				if i == 0 { // avoid duplicating by just assigning all claims to the first container.
 					pvReq = podPVs
+					netReq = podNetCosts
 				}
 
 				costs := &CostData{
@@ -544,6 +584,7 @@ func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, clientset kube
 					CPUUsed:         CPUUsedV,
 					GPUReq:          GPUReqV,
 					PVCData:         pvReq,
+					NetworkData:     netReq,
 					Labels:          podLabels,
 					NamespaceLabels: nsLabels,
 				}
@@ -1422,6 +1463,22 @@ type PersistentVolumeClaimData struct {
 	Values     []*Vector             `json:"values"`
 }
 
+// NetworkUsageVNetworkUsageDataector contains the network usage values for egress network traffic
+type NetworkUsageData struct {
+	PodName               string
+	Namespace             string
+	NetworkZoneEgress     []*Vector
+	NetworkRegionEgress   []*Vector
+	NetworkInternetEgress []*Vector
+}
+
+// NetworkUsageVector contains a network usage vector for egress network traffic
+type NetworkUsageVector struct {
+	PodName   string
+	Namespace string
+	Values    []*Vector
+}
+
 func getCost(qr interface{}) (map[string][]*Vector, error) {
 	toReturn := make(map[string][]*Vector)
 	for _, val := range qr.(map[string]interface{})["data"].(map[string]interface{})["result"].([]interface{}) {
@@ -1928,6 +1985,282 @@ func GetContainerMetricVectors(qr interface{}, normalize bool, normalizationValu
 	return containerData, nil
 }
 
+func getNetworkUsageData(zr interface{}, rr interface{}, ir interface{}, isRange bool) (map[string]*NetworkUsageData, error) {
+	var vectorFn func(interface{}) (map[string]*NetworkUsageVector, error)
+
+	if isRange {
+		vectorFn = getNetworkUsageVectors
+	} else {
+		vectorFn = getNetworkUsageVector
+	}
+
+	zoneNetworkMap, err := vectorFn(zr)
+	if err != nil {
+		return nil, err
+	}
+
+	regionNetworkMap, err := vectorFn(rr)
+	if err != nil {
+		return nil, err
+	}
+
+	internetNetworkMap, err := vectorFn(ir)
+	if err != nil {
+		return nil, err
+	}
+
+	usageData := make(map[string]*NetworkUsageData)
+	for k, v := range zoneNetworkMap {
+		existing, ok := usageData[k]
+		if !ok {
+			usageData[k] = &NetworkUsageData{
+				PodName:           v.PodName,
+				Namespace:         v.Namespace,
+				NetworkZoneEgress: v.Values,
+			}
+			continue
+		}
+
+		existing.NetworkZoneEgress = v.Values
+	}
+
+	for k, v := range regionNetworkMap {
+		existing, ok := usageData[k]
+		if !ok {
+			usageData[k] = &NetworkUsageData{
+				PodName:             v.PodName,
+				Namespace:           v.Namespace,
+				NetworkRegionEgress: v.Values,
+			}
+			continue
+		}
+
+		existing.NetworkRegionEgress = v.Values
+	}
+
+	for k, v := range internetNetworkMap {
+		existing, ok := usageData[k]
+		if !ok {
+			usageData[k] = &NetworkUsageData{
+				PodName:               v.PodName,
+				Namespace:             v.Namespace,
+				NetworkInternetEgress: v.Values,
+			}
+			continue
+		}
+
+		existing.NetworkInternetEgress = v.Values
+	}
+
+	return usageData, nil
+}
+
+func getNetworkUsageVector(qr interface{}) (map[string]*NetworkUsageVector, error) {
+	ncdmap := make(map[string]*NetworkUsageVector)
+	data, ok := qr.(map[string]interface{})["data"]
+	if !ok {
+		e, err := wrapPrometheusError(qr)
+		if err != nil {
+			return nil, err
+		}
+		return nil, fmt.Errorf(e)
+	}
+	d, ok := data.(map[string]interface{})
+	if !ok {
+		return nil, fmt.Errorf("Data field improperly formatted in prometheus repsonse")
+	}
+	result, ok := d["result"]
+	if !ok {
+		return nil, fmt.Errorf("Result field not present in prometheus response")
+	}
+	results, ok := result.([]interface{})
+	if !ok {
+		return nil, fmt.Errorf("Result field improperly formatted in prometheus response")
+	}
+	for _, val := range results {
+		metricInterface, ok := val.(map[string]interface{})["metric"]
+		if !ok {
+			return nil, fmt.Errorf("Metric field does not exist in data result vector")
+		}
+		metricMap, ok := metricInterface.(map[string]interface{})
+		if !ok {
+			return nil, fmt.Errorf("Metric field is improperly formatted")
+		}
+
+		podName, ok := metricMap["pod_name"]
+		if !ok {
+			return nil, fmt.Errorf("Pod Name does not exist in data result vector")
+		}
+		podNameStr, ok := podName.(string)
+		if !ok {
+			return nil, fmt.Errorf("Pod Name field improperly formatted")
+		}
+		namespace, ok := metricMap["namespace"]
+		if !ok {
+			return nil, fmt.Errorf("Namespace field does not exist in data result vector")
+		}
+		namespaceStr, ok := namespace.(string)
+		if !ok {
+			return nil, fmt.Errorf("Namespace field improperly formatted")
+		}
+		dataPoint, ok := val.(map[string]interface{})["value"]
+		if !ok {
+			return nil, fmt.Errorf("Value field does not exist in data result vector")
+		}
+		value, ok := dataPoint.([]interface{})
+		if !ok || len(value) != 2 {
+			return nil, fmt.Errorf("Improperly formatted datapoint from Prometheus")
+		}
+		var vectors []*Vector
+		strVal := value[1].(string)
+		v, _ := strconv.ParseFloat(strVal, 64)
+
+		vectors = append(vectors, &Vector{
+			Timestamp: value[0].(float64),
+			Value:     v,
+		})
+
+		key := namespaceStr + "," + podNameStr
+		ncdmap[key] = &NetworkUsageVector{
+			Namespace: namespaceStr,
+			PodName:   podNameStr,
+			Values:    vectors,
+		}
+	}
+	return ncdmap, nil
+}
+
+func getNetworkUsageVectors(qr interface{}) (map[string]*NetworkUsageVector, error) {
+	ncdmap := make(map[string]*NetworkUsageVector)
+	data, ok := qr.(map[string]interface{})["data"]
+	if !ok {
+		e, err := wrapPrometheusError(qr)
+		if err != nil {
+			return nil, err
+		}
+		return nil, fmt.Errorf(e)
+	}
+	d, ok := data.(map[string]interface{})
+	if !ok {
+		return nil, fmt.Errorf("Data field improperly formatted in prometheus repsonse")
+	}
+	result, ok := d["result"]
+	if !ok {
+		return nil, fmt.Errorf("Result field not present in prometheus response")
+	}
+	results, ok := result.([]interface{})
+	if !ok {
+		return nil, fmt.Errorf("Result field improperly formatted in prometheus response")
+	}
+	for _, val := range results {
+		metricInterface, ok := val.(map[string]interface{})["metric"]
+		if !ok {
+			return nil, fmt.Errorf("Metric field does not exist in data result vector")
+		}
+		metricMap, ok := metricInterface.(map[string]interface{})
+		if !ok {
+			return nil, fmt.Errorf("Metric field is improperly formatted")
+		}
+
+		podName, ok := metricMap["pod_name"]
+		if !ok {
+			return nil, fmt.Errorf("Pod Name does not exist in data result vector")
+		}
+		podNameStr, ok := podName.(string)
+		if !ok {
+			return nil, fmt.Errorf("Pod Name field improperly formatted")
+		}
+		namespace, ok := metricMap["namespace"]
+		if !ok {
+			return nil, fmt.Errorf("Namespace field does not exist in data result vector")
+		}
+		namespaceStr, ok := namespace.(string)
+		if !ok {
+			return nil, fmt.Errorf("Namespace field improperly formatted")
+		}
+		values, ok := val.(map[string]interface{})["values"].([]interface{})
+		if !ok {
+			return nil, fmt.Errorf("Values field is improperly formatted")
+		}
+		var vectors []*Vector
+		for _, value := range values {
+			dataPoint, ok := value.([]interface{})
+			if !ok || len(dataPoint) != 2 {
+				return nil, fmt.Errorf("Improperly formatted datapoint from Prometheus")
+			}
+
+			strVal := dataPoint[1].(string)
+			v, _ := strconv.ParseFloat(strVal, 64)
+			vectors = append(vectors, &Vector{
+				Timestamp: math.Round(dataPoint[0].(float64)/10) * 10,
+				Value:     v,
+			})
+		}
+
+		key := namespaceStr + "," + podNameStr
+		ncdmap[key] = &NetworkUsageVector{
+			Namespace: namespaceStr,
+			PodName:   podNameStr,
+			Values:    vectors,
+		}
+	}
+	return ncdmap, nil
+}
+
+func max(x int, rest ...int) int {
+	curr := x
+	for _, v := range rest {
+		if v > curr {
+			curr = v
+		}
+	}
+	return curr
+}
+
+func getNetworkCost(usage *NetworkUsageData, cloud costAnalyzerCloud.Provider) ([]*Vector, error) {
+	var results []*Vector
+
+	pricing, err := cloud.NetworkPricing()
+	if err != nil {
+		return nil, err
+	}
+	zoneCost := pricing.ZoneNetworkEgressCost
+	regionCost := pricing.RegionNetworkEgressCost
+	internetCost := pricing.InternetNetworkEgressCost
+
+	zlen := len(usage.NetworkZoneEgress)
+	rlen := len(usage.NetworkRegionEgress)
+	ilen := len(usage.NetworkInternetEgress)
+
+	l := max(zlen, rlen, ilen)
+	for i := 0; i < l; i++ {
+		var cost float64 = 0
+		var timestamp float64
+
+		if i < zlen {
+			cost += usage.NetworkZoneEgress[i].Value * zoneCost
+			timestamp = usage.NetworkZoneEgress[i].Timestamp
+		}
+
+		if i < rlen {
+			cost += usage.NetworkRegionEgress[i].Value * regionCost
+			timestamp = usage.NetworkRegionEgress[i].Timestamp
+		}
+
+		if i < ilen {
+			cost += usage.NetworkInternetEgress[i].Value * internetCost
+			timestamp = usage.NetworkInternetEgress[i].Timestamp
+		}
+
+		results = append(results, &Vector{
+			Value:     cost,
+			Timestamp: timestamp,
+		})
+	}
+
+	return results, nil
+}
+
 func wrapPrometheusError(qr interface{}) (string, error) {
 	e, ok := qr.(map[string]interface{})["error"]
 	if !ok {

+ 0 - 2
go.mod

@@ -4,8 +4,6 @@ require github.com/kubecost/test/mocks v0.0.0
 
 replace github.com/kubecost/test/mocks v0.0.0 => ./test/mocks
 
-replace git.apache.org/thrift.git => github.com/apache/thrift v0.12.0
-
 require (
 	cloud.google.com/go v0.34.0
 	contrib.go.opencensus.io/exporter/ocagent v0.5.0 // indirect

+ 20 - 0
main.go

@@ -55,6 +55,9 @@ type Accesses struct {
 	CPUAllocationRecorder         *prometheus.GaugeVec
 	GPUAllocationRecorder         *prometheus.GaugeVec
 	ContainerUptimeRecorder       *prometheus.GaugeVec
+	NetworkZoneEgressRecorder     *prometheus.GaugeVec
+	NetworkRegionEgressRecorder   *prometheus.GaugeVec
+	NetworkInternetEgressRecorder *prometheus.GaugeVec
 	Model                         *costModel.CostModel
 }
 
@@ -580,6 +583,19 @@ func main() {
 		Help: "container_uptime_seconds Seconds a container has been running",
 	}, []string{"namespace", "pod", "container"})
 
+	NetworkZoneEgressRecorder := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+		Name: "kubecost_network_zone_egress_cost",
+		Help: "kubecost_network_zone_egress_cost Total cost per GB egress across zones",
+	}, []string{"namespace", "pod"})
+	NetworkRegionEgressRecorder := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+		Name: "kubecost_network_region_egress_cost",
+		Help: "kubecost_network_region_egress_cost Total cost per GB egress across regions",
+	}, []string{"namespace", "pod"})
+	NetworkInternetEgressRecorder := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+		Name: "kubecost_network_internet_egress_cost",
+		Help: "kubecost_network_internet_egress_cost Total cost per GB of internet egress.",
+	}, []string{"namespace", "pod"})
+
 	prometheus.MustRegister(cpuGv)
 	prometheus.MustRegister(ramGv)
 	prometheus.MustRegister(gpuGv)
@@ -588,6 +604,7 @@ func main() {
 	prometheus.MustRegister(RAMAllocation)
 	prometheus.MustRegister(CPUAllocation)
 	prometheus.MustRegister(ContainerUptimeRecorder)
+	prometheus.MustRegister(NetworkZoneEgressRecorder, NetworkRegionEgressRecorder, NetworkInternetEgressRecorder)
 
 	podCache := cache.NewListWatchFromClient(kubeClientset.CoreV1().RESTClient(), "pods", "", fields.Everything())
 
@@ -603,6 +620,9 @@ func main() {
 		CPUAllocationRecorder:         CPUAllocation,
 		GPUAllocationRecorder:         GPUAllocation,
 		ContainerUptimeRecorder:       ContainerUptimeRecorder,
+		NetworkZoneEgressRecorder:     NetworkZoneEgressRecorder,
+		NetworkRegionEgressRecorder:   NetworkRegionEgressRecorder,
+		NetworkInternetEgressRecorder: NetworkInternetEgressRecorder,
 		PersistentVolumePriceRecorder: pvGv,
 		Model:                         costModel.NewCostModel(podCache),
 	}