Quellcode durchsuchen

edge case handling for n1/g1, add implicit ramcost

AjayTripathy vor 7 Jahren
Ursprung
Commit
78b1473723
6 geänderte Dateien mit 274 neuen und 44 gelöschten Zeilen
  1. 2 0
      Dockerfile
  2. 8 0
      cloud/default.json
  3. 176 22
      cloud/provider.go
  4. 82 19
      costmodel/costmodel.go
  5. 1 1
      kubernetes/deployment.yaml
  6. 5 2
      main.go

+ 2 - 0
Dockerfile

@@ -10,6 +10,8 @@ ENV GOPATH=/go
 ENV PATH=$GOPATH/bin:$PATH   
 
 RUN mkdir -p $GOPATH/src/app 
+RUN mkdir -p /models
+ADD ./cloud/default.json /models/default.json
 ADD . $GOPATH/src/app
 ADD ./cloud/ /go/src/app/vendor/github.com/kubecost/cost-model/cloud/
 ADD ./costmodel/ /go/src/app/vendor/github.com/kubecost/cost-model/costmodel/

+ 8 - 0
cloud/default.json

@@ -0,0 +1,8 @@
+{
+    "provider": "custom",
+    "description": "Default prices based on GCP us-central1",
+    "CPU": "0.031611",
+    "spotCPU": "0.006655",
+    "RAM": "0.004237",
+    "spotRAM": "0.000892"
+}

+ 176 - 22
cloud/provider.go

@@ -43,27 +43,142 @@ type Provider interface {
 	QuerySQL(string) ([]byte, error)
 }
 
+func GetDefaultPricingData() (*CustomPricing, error) {
+	jsonFile, err := os.Open("/models/default.json")
+	if err != nil {
+		return nil, err
+	}
+	defer jsonFile.Close()
+	byteValue, err := ioutil.ReadAll(jsonFile)
+	if err != nil {
+		return nil, err
+	}
+	var customPricing *CustomPricing = &CustomPricing{}
+	err = json.Unmarshal([]byte(byteValue), customPricing)
+	if err != nil {
+		return nil, err
+	}
+	return customPricing, nil
+}
+
+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"`
+	SpotLabel      string `json:"spotLabel,omitempty"`
+	SpotLabelValue string `json:"spotLabel,omitempty"`
+}
+
+type NodePrice struct {
+	CPU string
+	RAM string
+}
+
+type CustomProvider struct {
+	Clientset      *kubernetes.Clientset
+	Pricing        map[string]*NodePrice
+	SpotLabel      string
+	SpotLabelValue string
+}
+
+func (*CustomProvider) ClusterName() ([]byte, error) {
+	return nil, nil
+}
+
+func (*CustomProvider) AddServiceKey(url.Values) error {
+	return nil
+}
+
+func (*CustomProvider) GetDisks() ([]byte, error) {
+	return nil, nil
+}
+
+func (c *CustomProvider) AllNodePricing() (interface{}, error) {
+	return c.Pricing, nil
+}
+
+func (c *CustomProvider) NodePricing(key string) (*Node, error) {
+	if _, ok := c.Pricing[key]; !ok {
+		key = "default"
+	}
+	return &Node{
+		VCPUCost: c.Pricing[key].CPU,
+		RAMCost:  c.Pricing[key].RAM,
+	}, nil
+}
+
+func (c *CustomProvider) DownloadPricingData() error {
+
+	if c.Pricing == nil {
+		m := make(map[string]*NodePrice)
+		c.Pricing = m
+	}
+	p, err := GetDefaultPricingData()
+	if err != nil {
+		return err
+	}
+	c.Pricing["default"] = &NodePrice{
+		CPU: p.CPU,
+		RAM: p.RAM,
+	}
+	c.Pricing["default,spot"] = &NodePrice{
+		CPU: p.SpotCPU,
+		RAM: p.SpotRAM,
+	}
+	return nil
+}
+
+func (c *CustomProvider) GetKey(labels map[string]string) string {
+	if labels[c.SpotLabel] != "" && labels[c.SpotLabel] == c.SpotLabelValue {
+		return "default,spot"
+	}
+	return "default" // TODO: multiple custom pricing support.
+}
+
+func (*CustomProvider) QuerySQL(query string) ([]byte, error) {
+	return nil, nil
+}
+
 type Node struct {
-	Cost        string
-	VCPU        string
-	VCPUCost    string
-	RAM         string
-	RAMCost     string
-	Storage     string
-	StorageCost string
+	Cost             string `json:"hourlyCost"`
+	VCPU             string `json:"CPU"`
+	VCPUCost         string `json:"CPUHourlyCost"`
+	RAM              string `json:"RAM"`
+	RAMCost          string `json:"RAMGBHourlyCost"`
+	Storage          string `json:"storage"`
+	StorageCost      string `json:"storageHourlyCost"`
+	UsesBaseCPUPrice bool   `json:"usesDefaultPrice"`
+	BaseCPUPrice     string `json:"baseCPUPrice"` // Used to compute an implicit RAM GB/Hr price when RAM pricing is not provided.
 }
 
 func NewProvider(clientset *kubernetes.Clientset, apiKey string) (Provider, error) {
 	if metadata.OnGCE() {
-		log.Printf("ON GCP AND KEY IS: %s", apiKey)
+		if apiKey == "" {
+			return nil, fmt.Errorf("Supply a GCP Key to start getting data")
+		}
 		return &GCP{
 			Clientset: clientset,
 			ApiKey:    apiKey,
 		}, nil
 	} else {
-		return &AWS{
-			Clientset: clientset,
-		}, nil
+		nodes, err := clientset.CoreV1().Nodes().List(metav1.ListOptions{})
+		if err != nil {
+			return nil, err
+		}
+		provider := strings.ToLower(nodes.Items[0].Spec.ProviderID)
+		if strings.HasPrefix(provider, "aws") {
+			return &AWS{
+				Clientset: clientset,
+			}, nil
+		} else {
+			log.Printf("Unsupported provider, falling back to default")
+			return &CustomProvider{
+				Clientset: clientset,
+			}, nil
+		}
 	}
 }
 
@@ -78,9 +193,10 @@ func (t userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error)
 }
 
 type GCP struct {
-	Pricing   map[string]*GCPPricing
-	Clientset *kubernetes.Clientset
-	ApiKey    string
+	Pricing      map[string]*GCPPricing
+	Clientset    *kubernetes.Clientset
+	ApiKey       string
+	BaseCPUPrice string
 }
 
 func (*GCP) QuerySQL(query string) ([]byte, error) {
@@ -210,6 +326,13 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]bool) (map[string]*G
 				if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "CUSTOM") {
 					instanceType = "custom"
 				}
+				// instance.toLowerCase() === “f1micro”
+				var partialCPU float64
+				if strings.ToLower(instanceType) == "f1micro" {
+					partialCPU = 0.2
+				} else if strings.ToLower(instanceType) == "g1small" {
+					partialCPU = 0.5
+				}
 
 				for _, sr := range product.ServiceRegions {
 					region := sr
@@ -228,13 +351,18 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]bool) (map[string]*G
 						if hourlyPrice == 0 {
 							continue
 						} else if strings.Contains(strings.ToUpper(product.Description), "RAM") {
+							if instanceType == "custom" {
+								log.Printf("RAM custom sku is: " + product.Name)
+							}
 							if _, ok := gcpPricingList[candidateKey]; ok {
 								gcpPricingList[candidateKey].Node.RAMCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
 							} else {
 								product.Node = &Node{
 									RAMCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
 								}
-								log.Printf("NODE: %v", product.Node)
+								if partialCPU != 0 {
+									product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
+								}
 								gcpPricingList[candidateKey] = product
 							}
 							break
@@ -245,7 +373,9 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]bool) (map[string]*G
 								product.Node = &Node{
 									VCPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
 								}
-								log.Printf("NODE: %v", product.Node)
+								if partialCPU != 0 {
+									product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
+								}
 								gcpPricingList[candidateKey] = product
 							}
 							break
@@ -293,7 +423,17 @@ func (gcp *GCP) parsePages(inputKeys map[string]bool) (map[string]*GCPPricing, e
 	returnPages := make(map[string]*GCPPricing)
 	for _, page := range pages {
 		for k, v := range page {
-			returnPages[k] = v
+			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
+				}
+			} else {
+				returnPages[k] = v
+			}
 		}
 	}
 	return returnPages, err
@@ -319,6 +459,12 @@ func (gcp *GCP) DownloadPricingData() error {
 		return err
 	}
 	gcp.Pricing = pages
+	c, err := GetDefaultPricingData()
+	if err != nil {
+		log.Printf("Error downloading default pricing data: %s", err.Error())
+	}
+	gcp.BaseCPUPrice = c.CPU
+
 	return nil
 }
 
@@ -346,6 +492,8 @@ func (gcp *GCP) AllNodePricing() (interface{}, error) {
 
 func (gcp *GCP) NodePricing(key string) (*Node, error) {
 	if n, ok := gcp.Pricing[key]; ok {
+		log.Printf("Returning pricing for node %s: %+v from SKU %s", key, n.Node, n.Name)
+		n.Node.BaseCPUPrice = gcp.BaseCPUPrice
 		return n.Node, nil
 	} else {
 		log.Printf("Warning: no pricing data found for %s", key)
@@ -357,6 +505,7 @@ type AWS struct {
 	Pricing          map[string]*AWSProductTerms
 	ValidPricingKeys map[string]bool
 	Clientset        *kubernetes.Clientset
+	BaseCPUPrice     string
 }
 
 type AWSPricing struct {
@@ -538,7 +687,11 @@ func (aws *AWS) DownloadPricingData() error {
 	if err != nil {
 		return err
 	}
-	log.Printf("Body Parsed")
+	c, err := GetDefaultPricingData()
+	if err != nil {
+		log.Printf("Error downloading default pricing data: %s", err.Error())
+	}
+	aws.BaseCPUPrice = c.CPU
 	return nil
 }
 
@@ -565,10 +718,11 @@ func (aws *AWS) NodePricing(key string) (*Node, error) {
 		terms := aws.Pricing[key]
 		cost := terms.OnDemand.PriceDimensions[terms.Sku+OnDemandRateCode+HourlyRateCode].PricePerUnit.USD
 		return &Node{
-			Cost:    cost,
-			VCPU:    terms.VCpu,
-			RAM:     terms.Memory,
-			Storage: terms.Storage,
+			Cost:         cost,
+			VCPU:         terms.VCpu,
+			RAM:          terms.Memory,
+			Storage:      terms.Storage,
+			BaseCPUPrice: aws.BaseCPUPrice,
 		}, nil
 	} else {
 		return nil, errors.New("Invalid Pricing Key: " + key + "\n")

+ 82 - 19
costmodel/costmodel.go

@@ -3,7 +3,9 @@ package costmodel
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"log"
+	"math"
 	"net/http"
 	"strconv"
 	"time"
@@ -35,23 +37,25 @@ 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"`
-	GPUReq       []*Vector               `json:"gpureq"`
-	PVData       []*PersistentVolumeData `json:"pvData"`
-	Labels       map[string]string       `json:"labels"`
+	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"`
 }
 
 type Vector struct {
@@ -177,13 +181,32 @@ func ComputeCostData(cli prometheusClient.Client, clientset *kubernetes.Clientse
 				PVData:       pvReq,
 				Labels:       labels,
 			}
-
+			costs.CPUAllocation = getContainerAllocation(costs.CPUReq, costs.CPUUsed)
+			costs.RAMAllocation = getContainerAllocation(costs.RAMReq, costs.RAMUsed)
 			containerNameCost[ns+","+podName+","+containerName] = costs
 		}
 	}
 	return containerNameCost, err
 }
 
+func getContainerAllocation(req []*Vector, used []*Vector) []*Vector {
+	if req == nil || len(req) == 0 {
+		return used
+	}
+	if used == nil || len(used) == 0 {
+		return req
+	}
+	var allocation []*Vector
+	for i, reqV := range req {
+		usedV := used[i]
+		allocation = append(allocation, &Vector{
+			Timestamp: usedV.Timestamp,
+			Value:     math.Max(usedV.Value, reqV.Value),
+		})
+	}
+	return allocation
+}
+
 func getNodeCost(clientset *kubernetes.Clientset, cloud costAnalyzerCloud.Provider) (map[string]*costAnalyzerCloud.Node, error) {
 	nodeList, err := clientset.CoreV1().Nodes().List(metav1.ListOptions{})
 	if err != nil {
@@ -197,6 +220,45 @@ func getNodeCost(clientset *kubernetes.Clientset, cloud costAnalyzerCloud.Provid
 		if err != nil {
 			log.Printf("Error getting node. Error: " + err.Error())
 		}
+
+		var cpu float64
+		if cnode.VCPU == "" {
+			cpu = float64(n.Status.Capacity.Cpu().Value())
+			cnode.VCPU = n.Status.Capacity.Cpu().String()
+		} else {
+			cpu, _ = strconv.ParseFloat(cnode.VCPU, 64)
+		}
+		var ram float64
+		log.Printf("CNODE RAM : %s", cnode.RAM)
+		if cnode.RAM == "" {
+			log.Printf("RAMSTRING: %s", n.Status.Capacity.Memory().String())
+			cnode.RAM = n.Status.Capacity.Memory().String()
+			ram = float64(n.Status.Capacity.Memory().Value())
+		} else {
+			ram, _ = strconv.ParseFloat(cnode.RAM, 64)
+		}
+		log.Printf("RAM USAGE: %f", ram)
+		if cnode.RAMCost == "" { // We couldn't find a ramcost, so fix cpu and allocate ram accordingly
+			basePrice, _ := strconv.ParseFloat(cnode.BaseCPUPrice, 64)
+			log.Printf("BASEPRICE: %f", basePrice)
+			totalCPUPrice := basePrice * cpu
+			log.Printf("TOTALCPUPRICE: %f", basePrice)
+			var nodePrice float64
+			if cnode.Cost != "" {
+				log.Printf("Use given nodeprice as whole node price")
+				nodePrice, _ = strconv.ParseFloat(cnode.Cost, 64)
+			} else {
+				log.Printf("Use cpuprice as whole node price")
+				nodePrice, _ = strconv.ParseFloat(cnode.VCPUCost, 64) // all the price was allocated the the CPU
+			}
+			log.Printf("NODEPRICE: %f", basePrice)
+			ramPrice := (nodePrice - totalCPUPrice) / (ram / 1024 / 1024 / 1024)
+			if ramPrice < 0 {
+				ramPrice = 0
+			}
+			cnode.RAMCost = fmt.Sprintf("%f", ramPrice)
+			log.Printf(cnode.RAMCost)
+		}
 		nodes[name] = cnode
 	}
 	return nodes, nil
@@ -414,7 +476,8 @@ func ComputeCostDataRange(cli prometheusClient.Client, clientset *kubernetes.Cli
 				PVData:       pvReq,
 				Labels:       labels,
 			}
-
+			costs.RAMAllocation = getContainerAllocation(costs.RAMReq, costs.RAMUsed)
+			costs.CPUAllocation = getContainerAllocation(costs.CPUReq, costs.CPUUsed)
 			containerNameCost[ns+","+podName+","+containerName] = costs
 		}
 	}

+ 1 - 1
kubernetes/deployment.yaml

@@ -29,5 +29,5 @@ spec:
             - name: PROMETHEUS_SERVER_ENDPOINT
               value: <add a prometheus server endpoint> # kube-state-metrics and prometheus must be installed.
             - name: CLOUD_PROVIDER_API_KEY
-              value: "" # The GCP Pricing API requires a key.
+              value: "AIzaSyDXQPG_MHUEy9neR7stolq6l0ujXmjJlvk" # The GCP Pricing API requires a key.
           imagePullPolicy: Always

+ 5 - 2
main.go

@@ -111,12 +111,15 @@ func main() {
 		Cloud:            cloudProvider,
 	}
 
-	a.Cloud.DownloadPricingData()
+	err = a.Cloud.DownloadPricingData()
+	if err != nil {
+		log.Printf("Failed to download pricing data: " + err.Error())
+	}
 
 	router := httprouter.New()
 	router.GET("/costDataModel", a.CostDataModel)
 	router.GET("/costDataModelRange", a.CostDataModelRange)
 	router.POST("/refreshPricing", a.RefreshPricingData)
 
-	log.Fatal(http.ListenAndServe(":9001", router))
+	log.Fatal(http.ListenAndServe(":9003", router))
 }