Răsfoiți Sursa

Merge pull request #81 from kubecost/AjayTripathy-pv-cost

add PV data to the cost model
Ajay Tripathy 7 ani în urmă
părinte
comite
af513a0708
5 a modificat fișierele cu 361 adăugiri și 79 ștergeri
  1. 25 0
      cloud/awsprovider.go
  2. 120 10
      cloud/gcpprovider.go
  3. 29 1
      cloud/provider.go
  4. 127 50
      costmodel/costmodel.go
  5. 60 18
      main.go

+ 25 - 0
cloud/awsprovider.go

@@ -302,6 +302,31 @@ func (k *awsKey) Features() string {
 	return key
 }
 
+func (aws *AWS) PVPricing(pvk PVKey) (*PV, error) {
+	return nil, nil
+}
+
+func (key *awsPVKey) Features() string {
+	storageClass := key.StorageClassName
+	if storageClass == "standard" {
+		storageClass = "pdstandard"
+	}
+	return key.Labels[v1.LabelZoneRegion] + storageClass
+}
+
+type awsPVKey struct {
+	Labels                 map[string]string
+	StorageClassParameters map[string]string
+	StorageClassName       string
+}
+
+func (aws *AWS) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string) PVKey {
+	return &awsPVKey{
+		Labels:           pv.Labels,
+		StorageClassName: pv.Spec.StorageClassName,
+	}
+}
+
 // GetKey maps node labels to information needed to retrieve pricing data
 func (aws *AWS) GetKey(labels map[string]string) Key {
 	return &awsKey{

+ 120 - 10
cloud/gcpprovider.go

@@ -269,6 +269,7 @@ type GCPPricing struct {
 	PricingInfo         []*PricingInfo   `json:"pricingInfo"`
 	ServiceProviderName string           `json:"serviceProviderName"`
 	Node                *Node            `json:"node"`
+	PV                  *PV              `json:"pv"`
 }
 
 // PricingInfo contains metadata about a cost.
@@ -310,7 +311,7 @@ type GCPResourceInfo struct {
 	UsageType          string `json:"usageType"`
 }
 
-func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key) (map[string]*GCPPricing, string, error) {
+func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[string]PVKey) (map[string]*GCPPricing, string, error) {
 	gcpPricingList := make(map[string]*GCPPricing)
 	var nextPageToken string
 	dec := json.NewDecoder(r)
@@ -334,6 +335,51 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key) (map[string]*GC
 				usageType := strings.ToLower(product.Category.UsageType)
 				instanceType := strings.ToLower(product.Category.ResourceGroup)
 
+				if instanceType == "ssd" {
+					lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
+					var nanos float64
+					if len(product.PricingInfo) > 0 {
+						nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
+					} else {
+						continue
+					}
+					hourlyPrice := (nanos * math.Pow10(-9)) / 730
+
+					for _, sr := range product.ServiceRegions {
+						region := sr
+						candidateKey := region + "," + "ssd"
+						if _, ok := pvKeys[candidateKey]; ok {
+							product.PV = &PV{
+								Cost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
+							}
+							gcpPricingList[candidateKey] = product
+							continue
+						}
+					}
+					continue
+				} else if instanceType == "pdstandard" {
+					lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
+					var nanos float64
+					if len(product.PricingInfo) > 0 {
+						nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
+					} else {
+						continue
+					}
+					hourlyPrice := (nanos * math.Pow10(-9)) / 730
+					for _, sr := range product.ServiceRegions {
+						region := sr
+						candidateKey := region + "," + "pdstandard"
+						if _, ok := pvKeys[candidateKey]; ok {
+							product.PV = &PV{
+								Cost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
+							}
+							gcpPricingList[candidateKey] = product
+							continue
+						}
+					}
+					continue
+				}
+
 				if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "CUSTOM") {
 					instanceType = "custom"
 				}
@@ -482,7 +528,7 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key) (map[string]*GC
 	return gcpPricingList, nextPageToken, nil
 }
 
-func (gcp *GCP) parsePages(inputKeys map[string]Key) (map[string]*GCPPricing, error) {
+func (gcp *GCP) parsePages(inputKeys map[string]Key, pvKeys map[string]PVKey) (map[string]*GCPPricing, error) {
 	var pages []map[string]*GCPPricing
 	url := "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + gcp.APIKey
 	klog.V(2).Infof("Fetch GCP Billing Data from URL: %s", url)
@@ -497,7 +543,7 @@ func (gcp *GCP) parsePages(inputKeys map[string]Key) (map[string]*GCPPricing, er
 		if err != nil {
 			return err
 		}
-		page, token, err := gcp.parsePage(resp.Body, inputKeys)
+		page, token, err := gcp.parsePage(resp.Body, inputKeys, pvKeys)
 		if err != nil {
 			return err
 		}
@@ -512,12 +558,20 @@ func (gcp *GCP) parsePages(inputKeys map[string]Key) (map[string]*GCPPricing, er
 	for _, page := range pages {
 		for k, v := range page {
 			if val, ok := returnPages[k]; ok { //keys may need to be merged
-				if val.Node.RAMCost != "" && val.Node.VCPUCost == "" {
-					val.Node.VCPUCost = v.Node.VCPUCost
-				} else if val.Node.VCPUCost != "" && val.Node.RAMCost == "" {
-					val.Node.RAMCost = v.Node.RAMCost
-				} else {
-					returnPages[k] = v
+				if val.Node != nil {
+					if val.Node.RAMCost != "" && val.Node.VCPUCost == "" {
+						val.Node.VCPUCost = v.Node.VCPUCost
+					} else if val.Node.VCPUCost != "" && val.Node.RAMCost == "" {
+						val.Node.RAMCost = v.Node.RAMCost
+					} else {
+						returnPages[k] = v
+					}
+				} else if val.PV != nil {
+					if val.PV.Cost != "" {
+						val.PV.Cost = v.PV.Cost
+					} else {
+						returnPages[k] = v
+					}
 				}
 			} else {
 				returnPages[k] = v
@@ -551,7 +605,30 @@ func (gcp *GCP) DownloadPricingData() error {
 		inputkeys[key.Features()] = key
 	}
 
-	pages, err := gcp.parsePages(inputkeys)
+	pvList, err := gcp.Clientset.CoreV1().PersistentVolumes().List(metav1.ListOptions{})
+	if err != nil {
+		return err
+	}
+
+	storageClasses, err := gcp.Clientset.StorageV1().StorageClasses().List(metav1.ListOptions{})
+	storageClassMap := make(map[string]map[string]string)
+	for _, storageClass := range storageClasses.Items {
+		params := storageClass.Parameters
+		storageClassMap[storageClass.ObjectMeta.Name] = params
+	}
+
+	pvkeys := make(map[string]PVKey)
+	for _, pv := range pvList.Items {
+		params, ok := storageClassMap[pv.Spec.StorageClassName]
+		if !ok {
+			klog.Infof("Unable to find params for storageClassName %s", pv.Name)
+			continue
+		}
+		key := gcp.GetPVKey(&pv, params)
+		pvkeys[key.Features()] = key
+	}
+
+	pages, err := gcp.parsePages(inputkeys, pvkeys)
 
 	if err != nil {
 		return err
@@ -561,6 +638,39 @@ func (gcp *GCP) DownloadPricingData() error {
 	return nil
 }
 
+func (gcp *GCP) PVPricing(pvk PVKey) (*PV, error) {
+	pricing, ok := gcp.Pricing[pvk.Features()]
+	if !ok {
+		klog.V(2).Infof("Persistent Volume pricing not found for %s", pvk)
+		return &PV{}, nil
+	}
+	return pricing.PV, nil
+}
+
+type pvKey struct {
+	Labels                 map[string]string
+	StorageClass           string
+	StorageClassParameters map[string]string
+}
+
+func (gcp *GCP) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string) PVKey {
+	return &pvKey{
+		Labels:                 pv.Labels,
+		StorageClass:           pv.Spec.StorageClassName,
+		StorageClassParameters: parameters,
+	}
+}
+
+func (key *pvKey) Features() string {
+	storageClass := key.StorageClassParameters["type"]
+	if storageClass == "pd-ssd" {
+		storageClass = "ssd"
+	} else if storageClass == "pd-standard" {
+		storageClass = "pdstandard"
+	}
+	return key.Labels[v1.LabelZoneRegion] + "," + storageClass
+}
+
 type gcpKey struct {
 	Labels map[string]string
 }

+ 29 - 1
cloud/provider.go

@@ -15,11 +15,12 @@ import (
 
 	"cloud.google.com/go/compute/metadata"
 
+	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
 )
 
-// Node is the interface by which the provider and cost model communicate.
+// Node is the interface by which the provider and cost model communicate Node prices.
 // The provider will best-effort try to fill out this struct.
 type Node struct {
 	Cost             string `json:"hourlyCost"`
@@ -38,6 +39,16 @@ type Node struct {
 	GPUCost          string `json:"gpuCost"`
 }
 
+// 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 {
+	Cost       string            `json:"hourlyCost"`
+	Class      string            `json:"storageClass"`
+	Size       string            `json:"size"`
+	Region     string            `json:"region"`
+	Parameters map[string]string `json:"parameters"`
+}
+
 // Key represents a way for nodes to match between the k8s API and a pricing API
 type Key interface {
 	ID() string       // ID represents an exact match
@@ -45,6 +56,10 @@ type Key interface {
 	GPUType() string  // GPUType returns "" if no GPU exists, but the name of the GPU otherwise
 }
 
+type PVKey interface {
+	Features() string
+}
+
 // OutOfClusterAllocation represents a cloud provider cost not associated with kubernetes
 type OutOfClusterAllocation struct {
 	Aggregator  string  `json:"aggregator"`
@@ -60,9 +75,11 @@ type Provider interface {
 	AddServiceKey(url.Values) error
 	GetDisks() ([]byte, error)
 	NodePricing(Key) (*Node, error)
+	PVPricing(PVKey) (*PV, error)
 	AllNodePricing() (interface{}, error)
 	DownloadPricingData() error
 	GetKey(map[string]string) Key
+	GetPVKey(*v1.PersistentVolume, map[string]string) PVKey
 	UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error)
 	GetConfig() (*CustomPricing, error)
 
@@ -274,6 +291,17 @@ func (*CustomProvider) QuerySQL(query string) ([]byte, error) {
 	return nil, nil
 }
 
+func (*CustomProvider) PVPricing(pvk PVKey) (*PV, error) {
+	return nil, nil
+}
+
+func (*CustomProvider) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string) PVKey {
+	return &awsPVKey{
+		Labels:           pv.Labels,
+		StorageClassName: pv.Spec.StorageClassName,
+	}
+}
+
 // NewProvider looks at the nodespec or provider metadata server to decide which provider to instantiate.
 func NewProvider(clientset *kubernetes.Clientset, apiKey string) (Provider, error) {
 	if metadata.OnGCE() {

+ 127 - 50
costmodel/costmodel.go

@@ -39,26 +39,26 @@ const (
 )
 
 type CostData struct {
-	Name            string                  `json:"name"`
-	PodName         string                  `json:"podName"`
-	NodeName        string                  `json:"nodeName"`
-	NodeData        *costAnalyzerCloud.Node `json:"node"`
-	Namespace       string                  `json:"namespace"`
-	Deployments     []string                `json:"deployments"`
-	Services        []string                `json:"services"`
-	Daemonsets      []string                `json:"daemonsets"`
-	Statefulsets    []string                `json:"statefulsets"`
-	Jobs            []string                `json:"jobs"`
-	RAMReq          []*Vector               `json:"ramreq"`
-	RAMUsed         []*Vector               `json:"ramused"`
-	CPUReq          []*Vector               `json:"cpureq"`
-	CPUUsed         []*Vector               `json:"cpuused"`
-	RAMAllocation   []*Vector               `json:"ramallocated"`
-	CPUAllocation   []*Vector               `json:"cpuallocated"`
-	GPUReq          []*Vector               `json:"gpureq"`
-	PVData          []*PersistentVolumeData `json:"pvData"`
-	Labels          map[string]string       `json:"labels"`
-	NamespaceLabels map[string]string       `json:"namespaceLabels"`
+	Name            string                       `json:"name"`
+	PodName         string                       `json:"podName"`
+	NodeName        string                       `json:"nodeName"`
+	NodeData        *costAnalyzerCloud.Node      `json:"node"`
+	Namespace       string                       `json:"namespace"`
+	Deployments     []string                     `json:"deployments"`
+	Services        []string                     `json:"services"`
+	Daemonsets      []string                     `json:"daemonsets"`
+	Statefulsets    []string                     `json:"statefulsets"`
+	Jobs            []string                     `json:"jobs"`
+	RAMReq          []*Vector                    `json:"ramreq"`
+	RAMUsed         []*Vector                    `json:"ramused"`
+	CPUReq          []*Vector                    `json:"cpureq"`
+	CPUUsed         []*Vector                    `json:"cpuused"`
+	RAMAllocation   []*Vector                    `json:"ramallocated"`
+	CPUAllocation   []*Vector                    `json:"cpuallocated"`
+	GPUReq          []*Vector                    `json:"gpureq"`
+	PVCData         []*PersistentVolumeClaimData `json:"pvcData"`
+	Labels          map[string]string            `json:"labels"`
+	NamespaceLabels map[string]string            `json:"namespaceLabels"`
 }
 
 type Vector struct {
@@ -114,9 +114,9 @@ func ComputeCostData(cli prometheusClient.Client, clientset kubernetes.Interface
 			), "pod_name","$1","pod","(.+)"
 		) 
 	) by (namespace,container_name,pod_name,node)`
-	queryPVRequests := `avg(kube_persistentvolumeclaim_info) by (persistentvolumeclaim, storageclass, namespace) 
+	queryPVRequests := `avg(kube_persistentvolumeclaim_info) by (persistentvolumeclaim, storageclass, namespace, volumename) 
 	                    * 
-	                    on (persistentvolumeclaim, namespace) group_right(storageclass) 
+	                    on (persistentvolumeclaim, namespace) group_right(storageclass, volumename) 
 			    sum(kube_persistentvolumeclaim_resource_requests_storage_bytes) by (persistentvolumeclaim, namespace)`
 	normalization := `max(count_over_time(kube_pod_container_resource_requests_memory_bytes{}[` + window + `]))`
 	resultRAMRequests, err := query(cli, queryRAMRequests)
@@ -183,6 +183,11 @@ func ComputeCostData(cli prometheusClient.Client, clientset kubernetes.Interface
 		return nil, err
 	}
 
+	err = addPVData(clientset, pvClaimMapping, cloud)
+	if err != nil {
+		return nil, err
+	}
+
 	containerNameCost := make(map[string]*CostData)
 	containers := make(map[string]bool)
 
@@ -259,7 +264,7 @@ func ComputeCostData(cli prometheusClient.Client, clientset kubernetes.Interface
 				}
 			}
 
-			var podPVs []*PersistentVolumeData
+			var podPVs []*PersistentVolumeClaimData
 			podClaims := pod.Spec.Volumes
 			for _, vol := range podClaims {
 				if vol.PersistentVolumeClaim != nil {
@@ -311,7 +316,7 @@ func ComputeCostData(cli prometheusClient.Client, clientset kubernetes.Interface
 					CPUUsedV = []*Vector{&Vector{}}
 				}
 
-				var pvReq []*PersistentVolumeData
+				var pvReq []*PersistentVolumeClaimData
 				if i == 0 { // avoid duplicating by just assigning all claims to the first container.
 					pvReq = podPVs
 				}
@@ -332,7 +337,7 @@ func ComputeCostData(cli prometheusClient.Client, clientset kubernetes.Interface
 					CPUReq:          CPUReqV,
 					CPUUsed:         CPUUsedV,
 					GPUReq:          GPUReqV,
-					PVData:          pvReq,
+					PVCData:         pvReq,
 					Labels:          podLabels,
 					NamespaceLabels: nsLabels,
 				}
@@ -522,6 +527,54 @@ func getContainerAllocation(req []*Vector, used []*Vector) []*Vector {
 
 	return allocation
 }
+func addPVData(clientset kubernetes.Interface, pvClaimMapping map[string]*PersistentVolumeClaimData, cloud costAnalyzerCloud.Provider) error {
+	storageClasses, err := clientset.StorageV1().StorageClasses().List(metav1.ListOptions{})
+	if err != nil {
+		return err
+	}
+	storageClassMap := make(map[string]map[string]string)
+	for _, storageClass := range storageClasses.Items {
+		params := storageClass.Parameters
+		storageClassMap[storageClass.ObjectMeta.Name] = params
+	}
+
+	pvs, err := clientset.CoreV1().PersistentVolumes().List(metav1.ListOptions{})
+	pvMap := make(map[string]*costAnalyzerCloud.PV)
+	for _, pv := range pvs.Items {
+		parameters, ok := storageClassMap[pv.Spec.StorageClassName]
+		if !ok {
+			klog.V(2).Infof("Unable to find parameters for storage class \"%s\"", pv.Spec.StorageClassName)
+		}
+		cacPv := &costAnalyzerCloud.PV{
+			Class:      pv.Spec.StorageClassName,
+			Region:     pv.Labels[v1.LabelZoneRegion],
+			Parameters: parameters,
+		}
+		err := GetPVCost(cacPv, &pv, cloud)
+		if err != nil {
+			return err
+		}
+		pvMap[pv.Name] = cacPv
+	}
+
+	for _, pvc := range pvClaimMapping {
+		pvc.Volume = pvMap[pvc.VolumeName]
+	}
+	return nil
+}
+
+func GetPVCost(pv *costAnalyzerCloud.PV, kpv *v1.PersistentVolume, cloud costAnalyzerCloud.Provider) error {
+	key := cloud.GetPVKey(kpv, pv.Parameters)
+	pvWithCost, err := cloud.PVPricing(key)
+	if err != nil {
+		return err
+	}
+	if pvWithCost == nil {
+		return nil
+	}
+	pv.Cost = pvWithCost.Cost
+	return err
+}
 
 func getNodeCost(clientset kubernetes.Interface, cloud costAnalyzerCloud.Provider) (map[string]*costAnalyzerCloud.Node, error) {
 	nodeList, err := clientset.CoreV1().Nodes().List(metav1.ListOptions{})
@@ -719,9 +772,9 @@ func ComputeCostDataRange(cli prometheusClient.Client, clientset kubernetes.Inte
 				), "pod_name","$1","pod","(.+)"
 			) 
 		) by (namespace,container_name,pod_name,node)`
-	queryPVRequests := `avg(kube_persistentvolumeclaim_info) by (persistentvolumeclaim, storageclass, namespace) 
+	queryPVRequests := `avg(kube_persistentvolumeclaim_info) by (persistentvolumeclaim, storageclass, namespace, volumename) 
 							* 
-							on (persistentvolumeclaim, namespace) group_right(storageclass) 
+							on (persistentvolumeclaim, namespace) group_right(storageclass, volumename) 
 					sum(kube_persistentvolumeclaim_resource_requests_storage_bytes) by (persistentvolumeclaim, namespace)`
 	normalization := `max(count_over_time(kube_pod_container_resource_requests_memory_bytes{}[` + windowString + `]))`
 
@@ -803,6 +856,11 @@ func ComputeCostDataRange(cli prometheusClient.Client, clientset kubernetes.Inte
 		return nil, err
 	}
 
+	err = addPVData(clientset, pvClaimMapping, cloud)
+	if err != nil {
+		return nil, err
+	}
+
 	containerNameCost := make(map[string]*CostData)
 	containers := make(map[string]bool)
 
@@ -878,7 +936,7 @@ func ComputeCostDataRange(cli prometheusClient.Client, clientset kubernetes.Inte
 				}
 			}
 
-			var podPVs []*PersistentVolumeData
+			var podPVs []*PersistentVolumeClaimData
 			podClaims := pod.Spec.Volumes
 			for _, vol := range podClaims {
 				if vol.PersistentVolumeClaim != nil {
@@ -931,7 +989,7 @@ func ComputeCostDataRange(cli prometheusClient.Client, clientset kubernetes.Inte
 					CPUUsedV = []*Vector{}
 				}
 
-				var pvReq []*PersistentVolumeData
+				var pvReq []*PersistentVolumeClaimData
 				if i == 0 { // avoid duplicating by just assigning all claims to the first container.
 					pvReq = podPVs
 				}
@@ -952,7 +1010,7 @@ func ComputeCostDataRange(cli prometheusClient.Client, clientset kubernetes.Inte
 					CPUReq:          CPUReqV,
 					CPUUsed:         CPUUsedV,
 					GPUReq:          GPUReqV,
-					PVData:          pvReq,
+					PVCData:         pvReq,
 					Labels:          podLabels,
 					NamespaceLabels: nsLabels,
 				}
@@ -1070,11 +1128,13 @@ func getStatefulSetsOfPod(pod v1.Pod) []string {
 	return []string{}
 }
 
-type PersistentVolumeData struct {
-	Class     string    `json:"class"`
-	Claim     string    `json:"claim"`
-	Namespace string    `json:"namespace"`
-	Values    []*Vector `json:"values"`
+type PersistentVolumeClaimData struct {
+	Class      string                `json:"class"`
+	Claim      string                `json:"claim"`
+	Namespace  string                `json:"namespace"`
+	VolumeName string                `json:"volumeName"`
+	Volume     *costAnalyzerCloud.PV `json:"persistentVolume"`
+	Values     []*Vector             `json:"values"`
 }
 
 func getCost(qr interface{}) (map[string][]*Vector, error) {
@@ -1118,8 +1178,8 @@ func getCost(qr interface{}) (map[string][]*Vector, error) {
 	return toReturn, nil
 }
 
-func getPVInfoVectors(qr interface{}) (map[string]*PersistentVolumeData, error) {
-	pvmap := make(map[string]*PersistentVolumeData)
+func getPVInfoVectors(qr interface{}) (map[string]*PersistentVolumeClaimData, error) {
+	pvmap := make(map[string]*PersistentVolumeClaimData)
 	for _, val := range qr.(map[string]interface{})["data"].(map[string]interface{})["result"].([]interface{}) {
 		metricInterface, ok := val.(map[string]interface{})["metric"]
 		if !ok {
@@ -1137,7 +1197,6 @@ func getPVInfoVectors(qr interface{}) (map[string]*PersistentVolumeData, error)
 		if !ok {
 			return nil, fmt.Errorf("Claim field improperly formatted")
 		}
-
 		pvnamespace, ok := metricMap["namespace"]
 		if !ok {
 			return nil, fmt.Errorf("Namespace field does not exist in data result vector")
@@ -1146,6 +1205,14 @@ func getPVInfoVectors(qr interface{}) (map[string]*PersistentVolumeData, error)
 		if !ok {
 			return nil, fmt.Errorf("Namespace field improperly formatted")
 		}
+		pv, ok := metricMap["volumename"]
+		if !ok {
+			return nil, fmt.Errorf("Volumename field does not exist in data result vector")
+		}
+		pvStr, ok := pv.(string)
+		if !ok {
+			return nil, fmt.Errorf("Volumename field improperly formatted")
+		}
 		pvclass, ok := metricMap["storageclass"]
 		if !ok { // TODO: We need to look up the actual PV and PV capacity. For now just proceed with "".
 			klog.V(2).Infof("Storage Class not found for claim \"%s/%s\".", pvnamespaceStr, pvclaimStr)
@@ -1174,18 +1241,19 @@ func getPVInfoVectors(qr interface{}) (map[string]*PersistentVolumeData, error)
 			})
 		}
 		key := pvnamespaceStr + "," + pvclaimStr
-		pvmap[key] = &PersistentVolumeData{
-			Class:     pvclassStr,
-			Claim:     pvclaimStr,
-			Namespace: pvnamespaceStr,
-			Values:    vectors,
+		pvmap[key] = &PersistentVolumeClaimData{
+			Class:      pvclassStr,
+			Claim:      pvclaimStr,
+			Namespace:  pvnamespaceStr,
+			VolumeName: pvStr,
+			Values:     vectors,
 		}
 	}
 	return pvmap, nil
 }
 
-func getPVInfoVector(qr interface{}) (map[string]*PersistentVolumeData, error) {
-	pvmap := make(map[string]*PersistentVolumeData)
+func getPVInfoVector(qr interface{}) (map[string]*PersistentVolumeClaimData, error) {
+	pvmap := make(map[string]*PersistentVolumeClaimData)
 	for _, val := range qr.(map[string]interface{})["data"].(map[string]interface{})["result"].([]interface{}) {
 		metricInterface, ok := val.(map[string]interface{})["metric"]
 		if !ok {
@@ -1211,6 +1279,14 @@ func getPVInfoVector(qr interface{}) (map[string]*PersistentVolumeData, error) {
 		if !ok {
 			return nil, fmt.Errorf("Namespace field improperly formatted")
 		}
+		pv, ok := metricMap["volumename"]
+		if !ok {
+			return nil, fmt.Errorf("Volumename field does not exist in data result vector")
+		}
+		pvStr, ok := pv.(string)
+		if !ok {
+			return nil, fmt.Errorf("Volumename field improperly formatted")
+		}
 		pvclass, ok := metricMap["storageclass"]
 		if !ok { // TODO: We need to look up the actual PV and PV capacity. For now just proceed with "".
 			klog.V(2).Infof("Storage Class not found for claim \"%s/%s\".", pvnamespaceStr, pvclaimStr)
@@ -1238,11 +1314,12 @@ func getPVInfoVector(qr interface{}) (map[string]*PersistentVolumeData, error) {
 		})
 
 		key := pvnamespaceStr + "," + pvclaimStr
-		pvmap[key] = &PersistentVolumeData{
-			Class:     pvclassStr,
-			Claim:     pvclaimStr,
-			Namespace: pvnamespaceStr,
-			Values:    vectors,
+		pvmap[key] = &PersistentVolumeClaimData{
+			Class:      pvclassStr,
+			Claim:      pvclaimStr,
+			Namespace:  pvnamespaceStr,
+			VolumeName: pvStr,
+			Values:     vectors,
 		}
 	}
 	return pvmap, nil

+ 60 - 18
main.go

@@ -16,6 +16,8 @@ import (
 	costModel "github.com/kubecost/cost-model/costmodel"
 	prometheusClient "github.com/prometheus/client_golang/api"
 	prometheusAPI "github.com/prometheus/client_golang/api/prometheus/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/prometheus/client_golang/prometheus"
 
@@ -35,15 +37,16 @@ var (
 )
 
 type Accesses struct {
-	PrometheusClient       prometheusClient.Client
-	KubeClientSet          kubernetes.Interface
-	Cloud                  costAnalyzerCloud.Provider
-	CPUPriceRecorder       *prometheus.GaugeVec
-	RAMPriceRecorder       *prometheus.GaugeVec
-	GPUPriceRecorder       *prometheus.GaugeVec
-	NodeTotalPriceRecorder *prometheus.GaugeVec
-	RAMAllocationRecorder  *prometheus.GaugeVec
-	CPUAllocationRecorder  *prometheus.GaugeVec
+	PrometheusClient              prometheusClient.Client
+	KubeClientSet                 kubernetes.Interface
+	Cloud                         costAnalyzerCloud.Provider
+	CPUPriceRecorder              *prometheus.GaugeVec
+	RAMPriceRecorder              *prometheus.GaugeVec
+	PersistentVolumePriceRecorder *prometheus.GaugeVec
+	GPUPriceRecorder              *prometheus.GaugeVec
+	NodeTotalPriceRecorder        *prometheus.GaugeVec
+	RAMAllocationRecorder         *prometheus.GaugeVec
+	CPUAllocationRecorder         *prometheus.GaugeVec
 }
 
 type DataEnvelope struct {
@@ -216,6 +219,13 @@ func (a *Accesses) recordPrices() {
 
 				totalCost := cpu*cpuCost + ramCost*(ram/1024/1024/1024) + gpu*gpuCost
 
+				if costs.PVCData != nil {
+					for _, pvc := range costs.PVCData {
+						pvCost, _ := strconv.ParseFloat(pvc.Volume.Cost, 64)
+						a.PersistentVolumePriceRecorder.WithLabelValues(pvc.VolumeName, pvc.VolumeName).Set(pvCost)
+					}
+				}
+
 				a.CPUPriceRecorder.WithLabelValues(nodeName, nodeName).Set(cpuCost)
 				a.RAMPriceRecorder.WithLabelValues(nodeName, nodeName).Set(ramCost)
 				a.GPUPriceRecorder.WithLabelValues(nodeName, nodeName).Set(gpuCost)
@@ -231,6 +241,31 @@ func (a *Accesses) recordPrices() {
 				if len(costs.CPUAllocation) > 0 {
 					a.CPUAllocationRecorder.WithLabelValues(namespace, podName, containerName, nodeName, nodeName).Set(costs.CPUAllocation[0].Value)
 				}
+
+				storageClasses, _ := a.KubeClientSet.StorageV1().StorageClasses().List(metav1.ListOptions{})
+
+				storageClassMap := make(map[string]map[string]string)
+				for _, storageClass := range storageClasses.Items {
+					params := storageClass.Parameters
+					storageClassMap[storageClass.ObjectMeta.Name] = params
+				}
+
+				pvs, _ := a.KubeClientSet.CoreV1().PersistentVolumes().List(metav1.ListOptions{})
+				for _, pv := range pvs.Items {
+					parameters, ok := storageClassMap[pv.Spec.StorageClassName]
+					if !ok {
+						klog.V(2).Infof("Unable to find parameters for storage class \"%s\"", pv.Spec.StorageClassName)
+					}
+					cacPv := &costAnalyzerCloud.PV{
+						Class:      pv.Spec.StorageClassName,
+						Region:     pv.Labels[v1.LabelZoneRegion],
+						Parameters: parameters,
+					}
+					costModel.GetPVCost(cacPv, &pv, a.Cloud)
+					c, _ := strconv.ParseFloat(cacPv.Cost, 64)
+					a.PersistentVolumePriceRecorder.WithLabelValues(pv.Name, pv.Name).Set(c)
+				}
+
 			}
 			time.Sleep(time.Minute)
 		}
@@ -296,6 +331,11 @@ func main() {
 		Help: "node_total_hourly_cost Total node cost per hour",
 	}, []string{"instance", "node"})
 
+	pvGv := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+		Name: "pv_hourly_cost",
+		Help: "pv_hourly_cost Cost per GB per hour on a persistent disk",
+	}, []string{"volumename", "persistentvolume"})
+
 	RAMAllocation := prometheus.NewGaugeVec(prometheus.GaugeOpts{
 		Name: "container_memory_allocation_bytes",
 		Help: "container_memory_allocation_bytes Bytes of RAM used",
@@ -310,19 +350,21 @@ func main() {
 	prometheus.MustRegister(ramGv)
 	prometheus.MustRegister(gpuGv)
 	prometheus.MustRegister(totalGv)
+	prometheus.MustRegister(pvGv)
 	prometheus.MustRegister(RAMAllocation)
 	prometheus.MustRegister(CPUAllocation)
 
 	a := Accesses{
-		PrometheusClient:       promCli,
-		KubeClientSet:          kubeClientset,
-		Cloud:                  cloudProvider,
-		CPUPriceRecorder:       cpuGv,
-		RAMPriceRecorder:       ramGv,
-		GPUPriceRecorder:       gpuGv,
-		NodeTotalPriceRecorder: totalGv,
-		RAMAllocationRecorder:  RAMAllocation,
-		CPUAllocationRecorder:  CPUAllocation,
+		PrometheusClient:              promCli,
+		KubeClientSet:                 kubeClientset,
+		Cloud:                         cloudProvider,
+		CPUPriceRecorder:              cpuGv,
+		RAMPriceRecorder:              ramGv,
+		GPUPriceRecorder:              gpuGv,
+		NodeTotalPriceRecorder:        totalGv,
+		RAMAllocationRecorder:         RAMAllocation,
+		CPUAllocationRecorder:         CPUAllocation,
+		PersistentVolumePriceRecorder: pvGv,
 	}
 
 	err = a.Cloud.DownloadPricingData()