AjayTripathy 7 лет назад
Родитель
Сommit
6bd1bc3d9b

+ 19 - 0
Dockerfile

@@ -0,0 +1,19 @@
+FROM golang:latest
+EXPOSE 9001
+
+EXPOSE 8080
+
+RUN  mkdir -p /go/src \
+  && mkdir -p /go/bin \
+  && mkdir -p /go/pkg
+ENV GOPATH=/go
+ENV PATH=$GOPATH/bin:$PATH   
+
+RUN mkdir -p $GOPATH/src/app 
+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/
+
+WORKDIR $GOPATH/src/app 
+RUN go build -o myapp . 
+CMD ["/go/src/app/myapp"]

+ 728 - 0
cloud/provider.go

@@ -0,0 +1,728 @@
+package cloud
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"math"
+	"net/http"
+	"net/url"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws/awserr"
+
+	"cloud.google.com/go/compute/metadata"
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/google"
+	compute "google.golang.org/api/compute/v1"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/athena"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+type Provider interface {
+	ClusterName() ([]byte, error)
+	AddServiceKey(url.Values) error
+	GetDisks() ([]byte, error)
+	NodePricing(string) (*Node, error)
+	AllNodePricing() (interface{}, error)
+	DownloadPricingData() error
+	GetKey(map[string]string) string
+
+	QuerySQL(string) ([]byte, error)
+}
+
+type Node struct {
+	Cost        string
+	VCPU        string
+	VCPUCost    string
+	RAM         string
+	RAMCost     string
+	Storage     string
+	StorageCost string
+}
+
+func NewProvider(clientset *kubernetes.Clientset, apiKey string) (Provider, error) {
+	if metadata.OnGCE() {
+		log.Printf("ON GCP AND KEY IS: %s", apiKey)
+		return &GCP{
+			Clientset: clientset,
+			ApiKey:    apiKey,
+		}, nil
+	} else {
+		return &AWS{
+			Clientset: clientset,
+		}, nil
+	}
+}
+
+type userAgentTransport struct {
+	userAgent string
+	base      http.RoundTripper
+}
+
+func (t userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+	req.Header.Set("User-Agent", t.userAgent)
+	return t.base.RoundTrip(req)
+}
+
+type GCP struct {
+	Pricing   map[string]*GCPPricing
+	Clientset *kubernetes.Clientset
+	ApiKey    string
+}
+
+func (*GCP) QuerySQL(query string) ([]byte, error) {
+	return nil, nil
+}
+
+func (*GCP) ClusterName() ([]byte, error) {
+	metadataClient := metadata.NewClient(&http.Client{Transport: userAgentTransport{
+		userAgent: "kubecost",
+		base:      http.DefaultTransport,
+	}})
+
+	attribute, err := metadataClient.InstanceAttributeValue("cluster-name")
+	if err != nil {
+		return nil, err
+	}
+
+	m := make(map[string]string)
+	m["name"] = attribute
+	m["provider"] = "GCP"
+	return json.Marshal(m)
+}
+
+func (*GCP) AddServiceKey(formValues url.Values) error {
+	key := formValues.Get("key")
+	k := []byte(key)
+	return ioutil.WriteFile("/var/configs/key.json", k, 0644)
+}
+
+func (*GCP) GetDisks() ([]byte, error) {
+	// metadata API setup
+	metadataClient := metadata.NewClient(&http.Client{Transport: userAgentTransport{
+		userAgent: "kubecost",
+		base:      http.DefaultTransport,
+	}})
+	projID, err := metadataClient.ProjectID()
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := google.DefaultClient(oauth2.NoContext,
+		"https://www.googleapis.com/auth/compute.readonly")
+	if err != nil {
+		return nil, err
+	}
+	svc, err := compute.New(client)
+	if err != nil {
+		return nil, err
+	}
+	res, err := svc.Disks.AggregatedList(projID).Do()
+
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(res)
+
+}
+
+type GCPPricing struct {
+	Name                string           `json:"name"`
+	SKUID               string           `json:"skuId"`
+	Description         string           `json:"description"`
+	Category            *GCPResourceInfo `json:"category"`
+	ServiceRegions      []string         `json:"serviceRegions"`
+	PricingInfo         []*PricingInfo   `json:"pricingInfo"`
+	ServiceProviderName string           `json:"serviceProviderName"`
+	Node                *Node            `json:"node"`
+}
+
+type PricingInfo struct {
+	Summary                string             `json:"summary"`
+	PricingExpression      *PricingExpression `json:"pricingExpression"`
+	CurrencyConversionRate int                `json:"currencyConversionRate"`
+	EffectiveTime          string             `json:""`
+}
+
+type PricingExpression struct {
+	UsageUnit                string         `json:"usageUnit"`
+	UsageUnitDescription     string         `json:"usageUnitDescription"`
+	BaseUnit                 string         `json:"baseUnit"`
+	BaseUnitConversionFactor int64          `json:"-"`
+	DisplayQuantity          int            `json:"displayQuantity"`
+	TieredRates              []*TieredRates `json:"tieredRates"`
+}
+
+type TieredRates struct {
+	StartUsageAmount int            `json:"startUsageAmount"`
+	UnitPrice        *UnitPriceInfo `json:"unitPrice"`
+}
+
+type UnitPriceInfo struct {
+	CurrencyCode string  `json:"currencyCode"`
+	Units        string  `json:"units"`
+	Nanos        float64 `json:"nanos"`
+}
+
+type GCPResourceInfo struct {
+	ServiceDisplayName string `json:"serviceDisplayName"`
+	ResourceFamily     string `json:"resourceFamily"`
+	ResourceGroup      string `json:"resourceGroup"`
+	UsageType          string `json:"usageType"`
+}
+
+func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]bool) (map[string]*GCPPricing, string) {
+	gcpPricingList := make(map[string]*GCPPricing)
+	var nextPageToken string
+	dec := json.NewDecoder(r)
+	for {
+		t, err := dec.Token()
+		if err == io.EOF {
+			break
+		}
+		//fmt.Printf("%v  \n", t)
+		if t == "skus" {
+			dec.Token() // [
+			for dec.More() {
+
+				product := &GCPPricing{}
+				err := dec.Decode(&product)
+				if err != nil {
+					fmt.Printf("Error: " + err.Error())
+					break
+				}
+				usageType := strings.ToLower(product.Category.UsageType)
+				instanceType := strings.ToLower(product.Category.ResourceGroup)
+
+				if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "CUSTOM") {
+					instanceType = "custom"
+				}
+
+				for _, sr := range product.ServiceRegions {
+					region := sr
+
+					candidateKey := region + "," + instanceType + "," + usageType
+					if _, ok := inputKeys[candidateKey]; ok {
+						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)
+						if hourlyPrice == 0 {
+							continue
+						} else if strings.Contains(strings.ToUpper(product.Description), "RAM") {
+							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)
+								gcpPricingList[candidateKey] = product
+							}
+							break
+						} else {
+							if _, ok := gcpPricingList[candidateKey]; ok {
+								gcpPricingList[candidateKey].Node.VCPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
+							} else {
+								product.Node = &Node{
+									VCPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
+								}
+								log.Printf("NODE: %v", product.Node)
+								gcpPricingList[candidateKey] = product
+							}
+							break
+						}
+					}
+				}
+			}
+		}
+		if t == "nextPageToken" {
+			pageToken, err := dec.Token()
+			if err != nil {
+				log.Printf("Error parsing nextpage token: " + err.Error())
+				break
+			}
+			if pageToken.(string) != "" {
+				nextPageToken = pageToken.(string)
+			} else {
+				nextPageToken = "done"
+			}
+		}
+	}
+	return gcpPricingList, nextPageToken
+}
+
+func (gcp *GCP) parsePages(inputKeys map[string]bool) (map[string]*GCPPricing, error) {
+	var pages []map[string]*GCPPricing
+	url := "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + gcp.ApiKey //AIzaSyDXQPG_MHUEy9neR7stolq6l0ujXmjJlvk
+	log.Printf("URL: %s", url)
+	var parsePagesHelper func(string) error
+	parsePagesHelper = func(pageToken string) error {
+		if pageToken == "done" {
+			return nil
+		} else if pageToken != "" {
+			url = url + "&pageToken=" + pageToken
+		}
+		resp, err := http.Get(url)
+		if err != nil {
+			return err
+		}
+		page, token := gcp.parsePage(resp.Body, inputKeys)
+		pages = append(pages, page)
+		return parsePagesHelper(token)
+	}
+	err := parsePagesHelper("")
+	returnPages := make(map[string]*GCPPricing)
+	for _, page := range pages {
+		for k, v := range page {
+			returnPages[k] = v
+		}
+	}
+	return returnPages, err
+}
+
+func (gcp *GCP) DownloadPricingData() error {
+
+	nodeList, err := gcp.Clientset.CoreV1().Nodes().List(metav1.ListOptions{})
+	if err != nil {
+		return err
+	}
+	inputkeys := make(map[string]bool)
+
+	for _, n := range nodeList.Items {
+		labels := n.GetObjectMeta().GetLabels()
+		key := gcp.GetKey(labels)
+		inputkeys[key] = true
+	}
+
+	pages, err := gcp.parsePages(inputkeys)
+
+	if err != nil {
+		return err
+	}
+	gcp.Pricing = pages
+	return nil
+}
+
+func (gcp *GCP) GetKey(labels map[string]string) string {
+
+	instanceType := strings.ToLower(strings.Join(strings.Split(labels["beta.kubernetes.io/instance-type"], "-")[:2], ""))
+	if instanceType == "n1highmem" || instanceType == "n1highcpu" {
+		instanceType = "n1standard" // These are priced the same. TODO: support n1ultrahighmem
+	} else if strings.HasPrefix(instanceType, "custom") {
+		instanceType = "custom" // The suffix of custom does not matter
+	}
+	region := strings.ToLower(labels["failure-domain.beta.kubernetes.io/region"])
+	var usageType string
+	if t, ok := labels["cloud.google.com/gke-preemptible"]; ok && t == "true" {
+		usageType = "preemptible"
+	} else {
+		usageType = "ondemand"
+	}
+	return region + "," + instanceType + "," + usageType
+}
+
+func (gcp *GCP) AllNodePricing() (interface{}, error) {
+	return gcp.Pricing, nil
+}
+
+func (gcp *GCP) NodePricing(key string) (*Node, error) {
+	if n, ok := gcp.Pricing[key]; ok {
+		return n.Node, nil
+	} else {
+		log.Printf("Warning: no pricing data found for %s", key)
+		return nil, fmt.Errorf("Warning: no pricing data found for %s", key)
+	}
+}
+
+type AWS struct {
+	Pricing          map[string]*AWSProductTerms
+	ValidPricingKeys map[string]bool
+	Clientset        *kubernetes.Clientset
+}
+
+type AWSPricing struct {
+	Products map[string]*AWSProduct `json:"products"`
+	Terms    AWSPricingTerms        `json:"terms"`
+}
+
+type AWSProduct struct {
+	Sku        string               `json:"sku"`
+	Attributes AWSProductAttributes `json:"attributes"`
+}
+
+type AWSProductAttributes struct {
+	Location        string `json:"location"`
+	InstanceType    string `json:"instanceType"`
+	Memory          string `json:"memory"`
+	Storage         string `json:"storage"`
+	VCpu            string `json:"vcpu"`
+	UsageType       string `json:"usagetype"`
+	OperatingSystem string `json:"operatingSystem"`
+	PreInstalledSw  string `json:"preInstalledSw"`
+}
+
+type AWSPricingTerms struct {
+	OnDemand map[string]map[string]*AWSOfferTerm `json:"OnDemand"`
+	Reserved map[string]map[string]*AWSOfferTerm `json:"Reserved"`
+}
+
+type AWSOfferTerm struct {
+	Sku             string                  `json:"sku"`
+	PriceDimensions map[string]*AWSRateCode `json:"priceDimensions"`
+}
+
+type AWSRateCode struct {
+	Unit         string          `json:"unit"`
+	PricePerUnit AWSCurrencyCode `json:"pricePerUnit"`
+}
+
+type AWSCurrencyCode struct {
+	USD string `json:"USD"`
+}
+
+type AWSProductTerms struct {
+	Sku      string        `json:"sku"`
+	OnDemand *AWSOfferTerm `json:"OnDemand"`
+	Reserved *AWSOfferTerm `json:"Reserved"`
+	Memory   string        `json:"memory"`
+	Storage  string        `json:"storage"`
+	VCpu     string        `json:"vcpu"`
+}
+
+const OnDemandRateCode = ".JRTCKXETXF"
+const ReservedRateCode = ".38NPMPTW36"
+const HourlyRateCode = ".6YS6EN2CT7"
+
+func (aws *AWS) KubeAttrConversion(location, instanceType, operatingSystem string) string {
+	locationToRegion := map[string]string{
+		"US East (Ohio)":             "us-east-2",
+		"US East (N. Virginia)":      "us-east-1",
+		"US West (N. California)":    "us-west-1",
+		"US West (Oregon)":           "us-west-2",
+		"Asia Pacific (Mumbai)":      "ap-south-1",
+		"Asia Pacific (Osaka-Local)": "ap-northeast-3",
+		"Asia Pacific (Seoul)":       "ap-northeast-2",
+		"Asia Pacific (Singapore)":   "ap-southeast-1",
+		"Asia Pacific (Sydney)":      "ap-southeast-2",
+		"Asia Pacific (Tokyo)":       "ap-northeast-1",
+		"Canada (Central)":           "ca-central-1",
+		"China (Beijing)":            "cn-north-1",
+		"China (Ningxia)":            "cn-northwest-1",
+		"EU (Frankfurt)":             "eu-central-1",
+		"EU (Ireland)":               "eu-west-1",
+		"EU (London)":                "eu-west-2",
+		"EU (Paris)":                 "eu-west-3",
+		"EU (Stockholm)":             "eu-north-1",
+		"South America (São Paulo)":  "sa-east-1",
+		"AWS GovCloud (US-East)":     "us-gov-east-1",
+		"AWS GovCloud (US)":          "us-gov-west-1",
+	}
+
+	operatingSystem = strings.ToLower(operatingSystem)
+
+	region := locationToRegion[location]
+	return region + "," + instanceType + "," + operatingSystem
+}
+
+func (aws *AWS) GetKey(labels map[string]string) string {
+	instanceType := labels["beta.kubernetes.io/instance-type"]
+	operatingSystem := labels["beta.kubernetes.io/os"]
+	region := labels["failure-domain.beta.kubernetes.io/region"]
+	return region + "," + instanceType + "," + operatingSystem
+}
+
+func (aws *AWS) DownloadPricingData() error {
+
+	nodeList, err := aws.Clientset.CoreV1().Nodes().List(metav1.ListOptions{})
+	if err != nil {
+		return err
+	}
+	inputkeys := make(map[string]bool)
+
+	for _, n := range nodeList.Items {
+		labels := n.GetObjectMeta().GetLabels()
+
+		key := aws.GetKey(labels)
+		inputkeys[key] = true
+	}
+
+	aws.Pricing = make(map[string]*AWSProductTerms)
+	aws.ValidPricingKeys = make(map[string]bool)
+	skusToKeys := make(map[string]string)
+
+	resp, err := http.Get("https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/index.json")
+	if err != nil {
+		return err
+	}
+
+	dec := json.NewDecoder(resp.Body)
+	for {
+		t, err := dec.Token()
+		if err == io.EOF {
+			fmt.Printf("done \n")
+			break
+		}
+		if t == "products" {
+			dec.Token() //{
+			for dec.More() {
+				dec.Token() // the sku token
+				product := &AWSProduct{}
+				err := dec.Decode(&product)
+
+				if err != nil {
+					fmt.Printf("Error: " + err.Error())
+					break
+				}
+				if product.Attributes.PreInstalledSw == "NA" &&
+					(strings.HasPrefix(product.Attributes.UsageType, "BoxUsage") || strings.Contains(product.Attributes.UsageType, "-BoxUsage")) {
+					key := aws.KubeAttrConversion(product.Attributes.Location, product.Attributes.InstanceType, product.Attributes.OperatingSystem)
+					if inputkeys[key] {
+						aws.Pricing[key] = &AWSProductTerms{
+							Sku:     product.Sku,
+							Memory:  product.Attributes.Memory,
+							Storage: product.Attributes.Storage,
+							VCpu:    product.Attributes.VCpu,
+						}
+						skusToKeys[product.Sku] = key
+					}
+					aws.ValidPricingKeys[key] = true
+				}
+			}
+		}
+		if t == "terms" {
+			dec.Token()
+			termType, _ := dec.Token()
+			if termType == "OnDemand" {
+				dec.Token() // {
+				for dec.More() {
+					sku, _ := dec.Token()
+					dec.Token()
+					skuOnDemand, _ := dec.Token()
+					offerTerm := &AWSOfferTerm{}
+					err := dec.Decode(&offerTerm)
+					if err != nil {
+						fmt.Printf("Error: " + err.Error())
+					}
+					if sku.(string)+OnDemandRateCode == skuOnDemand {
+						key, ok := skusToKeys[sku.(string)]
+						if ok {
+							aws.Pricing[key].OnDemand = offerTerm
+						}
+					}
+					dec.Token()
+				}
+				dec.Token()
+			}
+		}
+	}
+
+	if err != nil {
+		return err
+	}
+	log.Printf("Body Parsed")
+	return nil
+}
+
+func (aws *AWS) AllNodePricing() (interface{}, error) {
+	return aws.Pricing, nil
+}
+
+func (aws *AWS) NodePricing(key string) (*Node, error) {
+	//return json.Marshal(aws.Pricing[key])
+	terms, ok := aws.Pricing[key]
+	if ok {
+		cost := terms.OnDemand.PriceDimensions[terms.Sku+OnDemandRateCode+HourlyRateCode].PricePerUnit.USD
+		return &Node{
+			Cost:    cost,
+			VCPU:    terms.VCpu,
+			RAM:     terms.Memory,
+			Storage: terms.Storage,
+		}, nil
+	} else if _, ok := aws.ValidPricingKeys[key]; ok {
+		err := aws.DownloadPricingData()
+		if err != nil {
+			return nil, err
+		}
+		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,
+		}, nil
+	} else {
+		return nil, errors.New("Invalid Pricing Key: " + key + "\n")
+	}
+}
+
+func (*AWS) ClusterName() ([]byte, error) {
+
+	attribute := "AWS Cluster #1"
+
+	m := make(map[string]string)
+	m["name"] = attribute
+	m["provider"] = "AWS"
+	return json.Marshal(m)
+}
+
+func (*AWS) AddServiceKey(formValues url.Values) error {
+	keyID := formValues.Get("access_key_ID")
+	key := formValues.Get("secret_access_key")
+	m := make(map[string]string)
+	m["access_key_ID"] = keyID
+	m["secret_access_key"] = key
+	json, err := json.Marshal(m)
+	if err != nil {
+		return err
+	}
+	return ioutil.WriteFile("/var/configs/key.json", json, 0644)
+}
+
+func (*AWS) GetDisks() ([]byte, error) {
+	jsonFile, err := os.Open("/var/configs/key.json")
+	if err == nil {
+		byteValue, _ := ioutil.ReadAll(jsonFile)
+		var result map[string]string
+		json.Unmarshal([]byte(byteValue), &result)
+		os.Setenv("AWS_ACCESS_KEY_ID", result["access_key_ID"])
+		os.Setenv("AWS_SECRET_ACCESS_KEY", result["secret_access_key"])
+	} else if os.IsNotExist(err) {
+		log.Printf("Using Default Credentials")
+	} else {
+		return nil, err
+	}
+	defer jsonFile.Close()
+	byteValue, _ := ioutil.ReadAll(jsonFile)
+	var result map[string]string
+	json.Unmarshal([]byte(byteValue), &result)
+	os.Setenv("AWS_ACCESS_KEY_ID", result["access_key_ID"])
+	os.Setenv("AWS_SECRET_ACCESS_KEY", result["secret_access_key"])
+	c := &aws.Config{
+		Region:      aws.String("us-east-1"),
+		Credentials: credentials.NewEnvCredentials(),
+	}
+	s := session.Must(session.NewSession(c))
+
+	ec2Svc := ec2.New(s)
+	input := &ec2.DescribeVolumesInput{}
+	volumeResult, err := ec2Svc.DescribeVolumes(input)
+	if err != nil {
+		if aerr, ok := err.(awserr.Error); ok {
+			switch aerr.Code() {
+			default:
+				return nil, aerr
+			}
+		} else {
+			return nil, err
+		}
+	}
+	return json.Marshal(volumeResult)
+}
+
+func (*AWS) QuerySQL(query string) ([]byte, error) {
+	jsonFile, err := os.Open("/var/configs/key.json")
+	if err == nil {
+		byteValue, _ := ioutil.ReadAll(jsonFile)
+		var result map[string]string
+		json.Unmarshal([]byte(byteValue), &result)
+		os.Setenv("AWS_ACCESS_KEY_ID", result["access_key_ID"])
+		os.Setenv("AWS_SECRET_ACCESS_KEY", result["secret_access_key"])
+	} else if os.IsNotExist(err) {
+		log.Printf("Using Default Credentials")
+	} else {
+		return nil, err
+	}
+	defer jsonFile.Close()
+	athenaConfigs, err := os.Open("/var/configs/athena.json")
+	if err != nil {
+		return nil, err
+	}
+	defer athenaConfigs.Close()
+	bytes, _ := ioutil.ReadAll(athenaConfigs)
+	var athenaConf map[string]string
+	json.Unmarshal([]byte(bytes), &athenaConf)
+	region := aws.String(athenaConf["region"])
+	resultsBucket := athenaConf["output"]
+	database := athenaConf["database"]
+
+	c := &aws.Config{
+		Region:      region,
+		Credentials: credentials.NewEnvCredentials(),
+	}
+	s := session.Must(session.NewSession(c))
+	svc := athena.New(s)
+
+	var e athena.StartQueryExecutionInput
+
+	var r athena.ResultConfiguration
+	r.SetOutputLocation(resultsBucket)
+	e.SetResultConfiguration(&r)
+
+	e.SetQueryString(query)
+	var q athena.QueryExecutionContext
+	q.SetDatabase(database)
+	e.SetQueryExecutionContext(&q)
+
+	res, err := svc.StartQueryExecution(&e)
+	if err != nil {
+		return nil, err
+	}
+
+	fmt.Println("StartQueryExecution result:")
+	fmt.Println(res.GoString())
+
+	var qri athena.GetQueryExecutionInput
+	qri.SetQueryExecutionId(*res.QueryExecutionId)
+
+	var qrop *athena.GetQueryExecutionOutput
+	duration := time.Duration(2) * time.Second // Pause for 2 seconds
+
+	for {
+		qrop, err = svc.GetQueryExecution(&qri)
+		if err != nil {
+			return nil, err
+		}
+		if *qrop.QueryExecution.Status.State != "RUNNING" {
+			break
+		}
+		time.Sleep(duration)
+	}
+	if *qrop.QueryExecution.Status.State == "SUCCEEDED" {
+
+		var ip athena.GetQueryResultsInput
+		ip.SetQueryExecutionId(*res.QueryExecutionId)
+
+		op, err := svc.GetQueryResults(&ip)
+		if err != nil {
+			return nil, err
+		}
+		bytes, err := json.Marshal(op.ResultSet)
+		if err != nil {
+			return nil, err
+		}
+
+		return bytes, nil
+	} else {
+		return nil, fmt.Errorf("Error getting query results : %s", *qrop.QueryExecution.Status.State)
+	}
+}

+ 613 - 0
costmodel/costmodel.go

@@ -0,0 +1,613 @@
+package costmodel
+
+import (
+	"context"
+	"encoding/json"
+	"log"
+	"net/http"
+	"strconv"
+	"time"
+
+	costAnalyzerCloud "github.com/kubecost/cost-model/cloud"
+	prometheusClient "github.com/prometheus/client_golang/api"
+	"k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/kubernetes"
+)
+
+const (
+	statusAPIError = 422
+
+	apiPrefix = "/api/v1"
+
+	epAlertManagers   = apiPrefix + "/alertmanagers"
+	epQuery           = apiPrefix + "/query"
+	epQueryRange      = apiPrefix + "/query_range"
+	epLabelValues     = apiPrefix + "/label/:name/values"
+	epSeries          = apiPrefix + "/series"
+	epTargets         = apiPrefix + "/targets"
+	epSnapshot        = apiPrefix + "/admin/tsdb/snapshot"
+	epDeleteSeries    = apiPrefix + "/admin/tsdb/delete_series"
+	epCleanTombstones = apiPrefix + "/admin/tsdb/clean_tombstones"
+	epConfig          = apiPrefix + "/status/config"
+	epFlags           = apiPrefix + "/status/flags"
+)
+
+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"`
+}
+
+type Vector struct {
+	Timestamp float64 `json:"timestamp"`
+	Value     float64 `json:"value"`
+}
+
+func ComputeCostData(cli prometheusClient.Client, clientset *kubernetes.Clientset, cloud costAnalyzerCloud.Provider, window string) (map[string]*CostData, error) {
+	queryRAMRequests := `avg(label_replace(label_replace(avg((count_over_time(kube_pod_container_resource_requests_memory_bytes{container!="",container!="POD"}[` + window + `]) *  avg_over_time(kube_pod_container_resource_requests_memory_bytes{container!="",container!="POD"}[` + window + `]))) by (namespace,container,pod) , "container_name","$1","container","(.+)"), "pod_name","$1","pod","(.+)") ) by (namespace,container_name, pod_name)`
+	queryRAMUsage := `sort_desc(avg(count_over_time(container_memory_usage_bytes{container_name!="",container_name!="POD"}[` + window + `]) * avg_over_time(container_memory_usage_bytes{container_name!="",container_name!="POD"}[` + window + `])) by (namespace,container_name,pod_name,instance))`
+	queryCPURequests := `avg(label_replace(label_replace(avg((count_over_time(kube_pod_container_resource_requests_cpu_cores{container!="",container!="POD"}[` + window + `]) *  avg_over_time(kube_pod_container_resource_requests_cpu_cores{container!="",container!="POD"}[` + window + `]))) by (namespace,container,pod) , "container_name","$1","container","(.+)"), "pod_name","$1","pod","(.+)") ) by (namespace,container_name, pod_name)`
+	queryCPUUsage := `avg(rate(container_cpu_usage_seconds_total{container_name!="",container_name!="POD"}[` + window + `])) by (namespace,container_name,pod_name,instance)`
+	queryGPURequests := `avg(label_replace(label_replace(avg((count_over_time(kube_pod_container_resource_requests{resource="nvidia_com_gpu", container!="",container!="POD"}[` + window + `]) *  avg_over_time(kube_pod_container_resource_requests{resource="nvidia_com_gpu", container!="",container!="POD"}[` + window + `]))) by (namespace,container,pod) , "container_name","$1","container","(.+)"), "pod_name","$1","pod","(.+)") ) by (namespace,container_name, pod_name)`
+	queryPVRequests := `(sum(kube_persistentvolumeclaim_info) by (persistentvolumeclaim, storageclass) + on (persistentvolumeclaim) group_right(storageclass) sum(kube_persistentvolumeclaim_resource_requests_storage_bytes) by (persistentvolumeclaim, namespace))`
+	normalization := `max(count_over_time(kube_pod_container_resource_requests_memory_bytes{}[` + window + `]))`
+	resultRAMRequests, _ := query(cli, queryRAMRequests)
+	resultRAMUsage, _ := query(cli, queryRAMUsage)
+	resultCPURequests, _ := query(cli, queryCPURequests)
+	resultCPUUsage, _ := query(cli, queryCPUUsage)
+	resultGPURequests, _ := query(cli, queryGPURequests)
+	resultPVRequests, _ := query(cli, queryPVRequests)
+	normalizationResult, _ := query(cli, normalization)
+
+	normalizationValue := getNormalization(normalizationResult)
+
+	nodes, err := getNodeCost(clientset, cloud)
+	if err != nil {
+		log.Printf("Warning, no cost model available: " + err.Error())
+	}
+
+	podlist, err := clientset.CoreV1().Pods("").List(metav1.ListOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	podDeploymentsMapping, err := getPodDeployments(clientset, podlist)
+	if err != nil {
+		return nil, err
+	}
+
+	podServicesMapping, err := getPodServices(clientset, podlist)
+	if err != nil {
+		return nil, err
+	}
+
+	pvClaimMapping := getPVInfoVector(resultPVRequests)
+	if err != nil {
+		return nil, err
+	}
+
+	containerNameCost := make(map[string]*CostData)
+	for _, pod := range podlist.Items {
+		podName := pod.GetObjectMeta().GetName()
+		ns := pod.GetObjectMeta().GetNamespace()
+		labels := pod.GetObjectMeta().GetLabels()
+		nodeName := pod.Spec.NodeName
+		var nodeData *costAnalyzerCloud.Node
+		if _, ok := nodes[nodeName]; ok {
+			nodeData = nodes[nodeName]
+		}
+		var podDeployments []string
+		if _, ok := podDeploymentsMapping[ns]; ok {
+			if ds, ok := podDeploymentsMapping[ns][pod.GetObjectMeta().GetName()]; ok {
+				podDeployments = ds
+			} else {
+				podDeployments = []string{}
+			}
+		}
+
+		var podPVs []*PersistentVolumeData
+		podClaims := pod.Spec.Volumes
+		for _, vol := range podClaims {
+			if vol.PersistentVolumeClaim != nil {
+				name := vol.PersistentVolumeClaim.ClaimName
+				if pvClaim, ok := pvClaimMapping[ns+","+name]; ok {
+					podPVs = append(podPVs, pvClaim)
+				}
+			}
+		}
+
+		var podServices []string
+		if _, ok := podServicesMapping[ns]; ok {
+			if svcs, ok := podServicesMapping[ns][pod.GetObjectMeta().GetName()]; ok {
+				podServices = svcs
+			} else {
+				podServices = []string{}
+			}
+		}
+
+		for i, container := range pod.Spec.Containers {
+			containerName := container.Name
+
+			RAMReqV := findContainerMetric(resultRAMRequests, containerName, podName, ns)
+			RAMReqV.Value = RAMReqV.Value / normalizationValue
+			RAMUsedV := findContainerMetric(resultRAMUsage, containerName, podName, ns)
+			RAMUsedV.Value = RAMUsedV.Value / normalizationValue
+			CPUReqV := findContainerMetric(resultCPURequests, containerName, podName, ns)
+			CPUReqV.Value = CPUReqV.Value / normalizationValue
+			GPUReqV := findContainerMetric(resultGPURequests, containerName, podName, ns)
+			GPUReqV.Value = GPUReqV.Value / normalizationValue
+
+			var pvReq []*PersistentVolumeData
+			if i == 0 { // avoid duplicating by just assigning all claims to the first container.
+				pvReq = podPVs
+			}
+
+			costs := &CostData{
+				Name:         containerName,
+				PodName:      podName,
+				NodeName:     nodeName,
+				Namespace:    ns,
+				Deployments:  podDeployments,
+				Services:     podServices,
+				Daemonsets:   getDaemonsetsOfPod(pod),
+				Jobs:         getJobsOfPod(pod),
+				Statefulsets: getStatefulSetsOfPod(pod),
+				NodeData:     nodeData,
+				RAMReq:       []*Vector{RAMReqV},
+				RAMUsed:      []*Vector{RAMUsedV},
+				CPUReq:       []*Vector{CPUReqV},
+				CPUUsed:      []*Vector{findContainerMetric(resultCPUUsage, containerName, podName, ns)},
+				GPUReq:       []*Vector{GPUReqV},
+				PVData:       pvReq,
+				Labels:       labels,
+			}
+
+			containerNameCost[ns+","+podName+","+containerName] = costs
+		}
+	}
+	return containerNameCost, err
+}
+
+func getNodeCost(clientset *kubernetes.Clientset, cloud costAnalyzerCloud.Provider) (map[string]*costAnalyzerCloud.Node, error) {
+	nodeList, err := clientset.CoreV1().Nodes().List(metav1.ListOptions{})
+	if err != nil {
+		return nil, err
+	}
+	nodes := make(map[string]*costAnalyzerCloud.Node)
+	for _, n := range nodeList.Items {
+		name := n.GetObjectMeta().GetName()
+		labels := n.GetObjectMeta().GetLabels()
+		cnode, err := cloud.NodePricing(cloud.GetKey(labels))
+		if err != nil {
+			log.Printf("Error getting node. Error: " + err.Error())
+		}
+		nodes[name] = cnode
+	}
+	return nodes, nil
+}
+
+func getPodServices(clientset *kubernetes.Clientset, podList *v1.PodList) (map[string]map[string][]string, error) {
+	servicesList, err := clientset.Core().Services("").List(metav1.ListOptions{})
+	if err != nil {
+		return nil, err
+	}
+	podServicesMapping := make(map[string]map[string][]string)
+	for _, service := range servicesList.Items {
+		namespace := service.GetObjectMeta().GetNamespace()
+		name := service.GetObjectMeta().GetName()
+
+		if _, ok := podServicesMapping[namespace]; !ok {
+			podServicesMapping[namespace] = make(map[string][]string)
+		}
+		s := labels.Set(service.Spec.Selector).AsSelectorPreValidated()
+		if err != nil {
+			log.Printf("Error doing service label conversion: " + err.Error())
+		}
+		for _, pod := range podList.Items {
+			labelSet := labels.Set(pod.GetObjectMeta().GetLabels())
+			if s.Matches(labelSet) && pod.GetObjectMeta().GetNamespace() == namespace {
+				services, ok := podServicesMapping[namespace][pod.GetObjectMeta().GetName()]
+				if ok {
+					podServicesMapping[namespace][pod.GetObjectMeta().GetName()] = append(services, name)
+				} else {
+					podServicesMapping[namespace][pod.GetObjectMeta().GetName()] = []string{name}
+				}
+			}
+		}
+	}
+	return podServicesMapping, nil
+}
+
+func getPodDeployments(clientset *kubernetes.Clientset, podList *v1.PodList) (map[string]map[string][]string, error) {
+	deploymentsList, err := clientset.AppsV1().Deployments("").List(metav1.ListOptions{})
+	if err != nil {
+		return nil, err
+	}
+	podDeploymentsMapping := make(map[string]map[string][]string) // namespace: podName: [deploymentNames]
+	for _, deployment := range deploymentsList.Items {
+		namespace := deployment.GetObjectMeta().GetNamespace()
+		name := deployment.GetObjectMeta().GetName()
+		if _, ok := podDeploymentsMapping[namespace]; !ok {
+			podDeploymentsMapping[namespace] = make(map[string][]string)
+		}
+		s, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector)
+		if err != nil {
+			log.Printf("Error doing deployment label conversion: " + err.Error())
+		}
+		for _, pod := range podList.Items {
+			labelSet := labels.Set(pod.GetObjectMeta().GetLabels())
+			if s.Matches(labelSet) && pod.GetObjectMeta().GetNamespace() == namespace {
+				deployments, ok := podDeploymentsMapping[namespace][pod.GetObjectMeta().GetName()]
+				if ok {
+					podDeploymentsMapping[namespace][pod.GetObjectMeta().GetName()] = append(deployments, name)
+				} else {
+					podDeploymentsMapping[namespace][pod.GetObjectMeta().GetName()] = []string{name}
+				}
+			}
+		}
+	}
+	return podDeploymentsMapping, nil
+}
+
+func ComputeCostDataRange(cli prometheusClient.Client, clientset *kubernetes.Clientset, cloud costAnalyzerCloud.Provider,
+	startString, endString, windowString string) (map[string]*CostData, error) {
+	queryRAMRequests := `avg(label_replace(label_replace(avg((count_over_time(kube_pod_container_resource_requests_memory_bytes{container!="",container!="POD"}[` + windowString + `]) *  avg_over_time(kube_pod_container_resource_requests_memory_bytes{container!="",container!="POD"}[` + windowString + `]))) by (namespace,container,pod) , "container_name","$1","container","(.+)"), "pod_name","$1","pod","(.+)") ) by (namespace,container_name, pod_name)`
+	queryRAMUsage := `sort_desc(avg(count_over_time(container_memory_usage_bytes{container_name!="",container_name!="POD"}[` + windowString + `]) * avg_over_time(container_memory_usage_bytes{container_name!="",container_name!="POD"}[` + windowString + `])) by (namespace,container_name,pod_name,instance))`
+	queryCPURequests := `avg(label_replace(label_replace(avg((count_over_time(kube_pod_container_resource_requests_cpu_cores{container!="",container!="POD"}[` + windowString + `]) *  avg_over_time(kube_pod_container_resource_requests_cpu_cores{container!="",container!="POD"}[` + windowString + `]))) by (namespace,container,pod) , "container_name","$1","container","(.+)"), "pod_name","$1","pod","(.+)") ) by (namespace,container_name, pod_name)`
+	queryCPUUsage := `avg(rate(container_cpu_usage_seconds_total{container_name!="",container_name!="POD"}[` + windowString + `])) by (namespace,container_name,pod_name,instance)`
+	queryGPURequests := `avg(label_replace(label_replace(avg((count_over_time(kube_pod_container_resource_requests{resource="nvidia_com_gpu", container!="",container!="POD"}[` + windowString + `]) *  avg_over_time(kube_pod_container_resource_requests{resource="nvidia_com_gpu", container!="",container!="POD"}[` + windowString + `]))) by (namespace,container,pod) , "container_name","$1","container","(.+)"), "pod_name","$1","pod","(.+)") ) by (namespace,container_name, pod_name)`
+	queryPVRequests := `(sum(kube_persistentvolumeclaim_info) by (persistentvolumeclaim, storageclass) + on (persistentvolumeclaim) group_right(storageclass) sum(kube_persistentvolumeclaim_resource_requests_storage_bytes) by (persistentvolumeclaim, namespace))`
+	normalization := `max(count_over_time(kube_pod_container_resource_requests_memory_bytes{}[` + windowString + `]))`
+
+	layout := "2006-01-02T15:04:05.000Z"
+
+	start, err := time.Parse(layout, startString)
+	if err != nil {
+		log.Printf("Error parsing time " + startString + ". Error: " + err.Error())
+		return nil, err
+	}
+	end, err := time.Parse(layout, endString)
+	if err != nil {
+		log.Printf("Error parsing time " + endString + ". Error: " + err.Error())
+		return nil, err
+	}
+	window, err := time.ParseDuration(windowString)
+	if err != nil {
+		log.Printf("Error parsing time " + windowString + ". Error: " + err.Error())
+		return nil, err
+	}
+	resultRAMRequests, _ := queryRange(cli, queryRAMRequests, start, end, window)
+	resultRAMUsage, _ := queryRange(cli, queryRAMUsage, start, end, window)
+	resultCPURequests, _ := queryRange(cli, queryCPURequests, start, end, window)
+	resultCPUUsage, _ := queryRange(cli, queryCPUUsage, start, end, window)
+	resultGPURequests, _ := queryRange(cli, queryGPURequests, start, end, window)
+	resultPVRequests, _ := queryRange(cli, queryPVRequests, start, end, window)
+
+	normalizationResult, _ := query(cli, normalization)
+
+	normalizationValue := getNormalization(normalizationResult)
+
+	nodes, err := getNodeCost(clientset, cloud)
+	if err != nil {
+		//return nil, err
+		log.Printf("Warning, no cost model available: " + err.Error())
+	}
+
+	podlist, err := clientset.CoreV1().Pods("").List(metav1.ListOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	podDeploymentsMapping, err := getPodDeployments(clientset, podlist)
+	if err != nil {
+		return nil, err
+	}
+	podServicesMapping, err := getPodServices(clientset, podlist)
+	if err != nil {
+		return nil, err
+	}
+
+	pvClaimMapping := getPVInfoVectors(resultPVRequests)
+	if err != nil {
+		return nil, err
+	}
+
+	containerNameCost := make(map[string]*CostData)
+
+	for _, pod := range podlist.Items {
+		podName := pod.GetObjectMeta().GetName()
+		ns := pod.GetObjectMeta().GetNamespace()
+		labels := pod.GetObjectMeta().GetLabels()
+		nodeName := pod.Spec.NodeName
+		var nodeData *costAnalyzerCloud.Node
+		if _, ok := nodes[nodeName]; ok {
+			nodeData = nodes[nodeName]
+		}
+		var podDeployments []string
+		if _, ok := podDeploymentsMapping[ns]; ok {
+			if ds, ok := podDeploymentsMapping[ns][pod.GetObjectMeta().GetName()]; ok {
+				podDeployments = ds
+			} else {
+				podDeployments = []string{}
+			}
+		}
+		var podServices []string
+		if _, ok := podServicesMapping[ns]; ok {
+			if svcs, ok := podServicesMapping[ns][pod.GetObjectMeta().GetName()]; ok {
+				podServices = svcs
+			} else {
+				podServices = []string{}
+			}
+		}
+
+		var podPVs []*PersistentVolumeData
+		podClaims := pod.Spec.Volumes
+		for _, vol := range podClaims {
+			if vol.PersistentVolumeClaim != nil {
+				name := vol.PersistentVolumeClaim.ClaimName
+				if pvClaim, ok := pvClaimMapping[ns+","+name]; ok {
+					podPVs = append(podPVs, pvClaim)
+				}
+			}
+		}
+
+		for i, container := range pod.Spec.Containers {
+			containerName := container.Name
+
+			RAMReqV := findContainerMetricVectors(resultRAMRequests, containerName, podName, ns)
+			for _, v := range RAMReqV {
+				v.Value = v.Value / normalizationValue
+			}
+
+			RAMUsedV := findContainerMetricVectors(resultRAMUsage, containerName, podName, ns)
+			for _, v := range RAMUsedV {
+				v.Value = v.Value / normalizationValue
+			}
+
+			CPUReqV := findContainerMetricVectors(resultCPURequests, containerName, podName, ns)
+			for _, v := range CPUReqV {
+				v.Value = v.Value / normalizationValue
+			}
+
+			GPUReqV := findContainerMetricVectors(resultGPURequests, containerName, podName, ns)
+			for _, v := range GPUReqV {
+				v.Value = v.Value / normalizationValue
+			}
+
+			var pvReq []*PersistentVolumeData
+			if i == 0 { // avoid duplicating by just assigning all claims to the first container.
+				pvReq = podPVs
+			}
+
+			costs := &CostData{
+				Name:         containerName,
+				PodName:      podName,
+				NodeName:     nodeName,
+				NodeData:     nodeData,
+				Namespace:    ns,
+				Deployments:  podDeployments,
+				Services:     podServices,
+				Daemonsets:   getDaemonsetsOfPod(pod),
+				Jobs:         getJobsOfPod(pod),
+				Statefulsets: getStatefulSetsOfPod(pod),
+				RAMReq:       RAMReqV,
+				RAMUsed:      RAMUsedV,
+				CPUReq:       CPUReqV,
+				CPUUsed:      findContainerMetricVectors(resultCPUUsage, containerName, podName, ns),
+				GPUReq:       GPUReqV,
+				PVData:       pvReq,
+				Labels:       labels,
+			}
+
+			containerNameCost[ns+","+podName+","+containerName] = costs
+		}
+	}
+	return containerNameCost, err
+
+}
+
+func getDaemonsetsOfPod(pod v1.Pod) []string {
+	for _, ownerReference := range pod.ObjectMeta.OwnerReferences {
+		if ownerReference.Kind == "DaemonSet" {
+			return []string{ownerReference.Name}
+		}
+	}
+	return []string{}
+}
+
+func getJobsOfPod(pod v1.Pod) []string {
+	for _, ownerReference := range pod.ObjectMeta.OwnerReferences {
+		if ownerReference.Kind == "Job" {
+			return []string{ownerReference.Name}
+		}
+	}
+	return []string{}
+}
+
+func getStatefulSetsOfPod(pod v1.Pod) []string {
+	for _, ownerReference := range pod.ObjectMeta.OwnerReferences {
+		if ownerReference.Kind == "StatefulSet" {
+			return []string{ownerReference.Name}
+		}
+	}
+	return []string{}
+}
+
+type PersistentVolumeData struct {
+	Class     string    `json:"class"`
+	Claim     string    `json:"claim"`
+	Namespace string    `json:"namespace"`
+	Values    []*Vector `json:"values"`
+}
+
+func getPVInfoVectors(qr interface{}) map[string]*PersistentVolumeData {
+	pvmap := make(map[string]*PersistentVolumeData)
+	for _, val := range qr.(map[string]interface{})["data"].(map[string]interface{})["result"].([]interface{}) {
+		pvclaim := val.(map[string]interface{})["metric"].(map[string]interface{})["persistentvolumeclaim"]
+		pvclass := val.(map[string]interface{})["metric"].(map[string]interface{})["storageclass"]
+		pvnamespace := val.(map[string]interface{})["metric"].(map[string]interface{})["namespace"]
+		values := val.(map[string]interface{})["values"].([]interface{})
+		var vectors []*Vector
+		for _, value := range values {
+			strVal := value.([]interface{})[1].(string)
+			v, _ := strconv.ParseFloat(strVal, 64)
+			vectors = append(vectors, &Vector{
+				Timestamp: value.([]interface{})[0].(float64),
+				Value:     v,
+			})
+		}
+		key := pvnamespace.(string) + "," + pvclaim.(string)
+		pvmap[key] = &PersistentVolumeData{
+			Class:     pvclass.(string),
+			Claim:     pvclaim.(string),
+			Namespace: pvnamespace.(string),
+			Values:    vectors,
+		}
+	}
+	return pvmap
+}
+
+func getPVInfoVector(qr interface{}) map[string]*PersistentVolumeData {
+	pvmap := make(map[string]*PersistentVolumeData)
+	log.Printf("Interface %v. If the interface is nil, prometheus is not running!", qr)
+	for _, val := range qr.(map[string]interface{})["data"].(map[string]interface{})["result"].([]interface{}) {
+		pvclaim := val.(map[string]interface{})["metric"].(map[string]interface{})["persistentvolumeclaim"]
+		pvclass := val.(map[string]interface{})["metric"].(map[string]interface{})["storageclass"]
+		pvnamespace := val.(map[string]interface{})["metric"].(map[string]interface{})["namespace"]
+		value := val.(map[string]interface{})["value"].([]interface{})
+		var vectors []*Vector
+		strVal := value[1].(string)
+		v, _ := strconv.ParseFloat(strVal, 64)
+
+		vectors = append(vectors, &Vector{
+			Timestamp: value[0].(float64),
+			Value:     v,
+		})
+
+		key := pvclaim.(string) + "," + pvnamespace.(string)
+		pvmap[key] = &PersistentVolumeData{
+			Class:     pvclass.(string),
+			Claim:     pvclaim.(string),
+			Namespace: pvnamespace.(string),
+			Values:    vectors,
+		}
+	}
+	return pvmap
+}
+
+func queryRange(cli prometheusClient.Client, query string, start, end time.Time, step time.Duration) (interface{}, error) {
+	u := cli.URL(epQueryRange, nil)
+	q := u.Query()
+	q.Set("query", query)
+	q.Set("start", start.Format(time.RFC3339Nano))
+	q.Set("end", end.Format(time.RFC3339Nano))
+	q.Set("step", strconv.FormatFloat(step.Seconds(), 'f', 3, 64))
+	u.RawQuery = q.Encode()
+
+	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	_, body, err := cli.Do(context.Background(), req)
+	if err != nil {
+		log.Print("ERROR" + err.Error())
+	}
+	if err != nil {
+		return nil, err
+	}
+	var toReturn interface{}
+	err = json.Unmarshal(body, &toReturn)
+	if err != nil {
+		log.Print("ERROR" + err.Error())
+	}
+	return toReturn, err
+}
+
+func query(cli prometheusClient.Client, query string) (interface{}, error) {
+	u := cli.URL(epQuery, nil)
+	q := u.Query()
+	q.Set("query", query)
+	u.RawQuery = q.Encode()
+
+	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	_, body, err := cli.Do(context.Background(), req)
+	if err != nil {
+		return nil, err
+	}
+	var toReturn interface{}
+	err = json.Unmarshal(body, &toReturn)
+	if err != nil {
+		log.Print("ERROR" + err.Error())
+	}
+	return toReturn, err
+}
+
+//todo: don't cast, implement unmarshaler interface
+func getNormalization(qr interface{}) float64 {
+	strNorm := qr.(map[string]interface{})["data"].(map[string]interface{})["result"].([]interface{})[0].(map[string]interface{})["value"].([]interface{})[1].(string)
+	val, _ := strconv.ParseFloat(strNorm, 64)
+	return val
+}
+
+//todo: don't cast, implement unmarshaler interface...
+func findContainerMetric(qr interface{}, cname string, podname string, namespace string) *Vector {
+	for _, val := range qr.(map[string]interface{})["data"].(map[string]interface{})["result"].([]interface{}) {
+		if val.(map[string]interface{})["metric"].(map[string]interface{})["container_name"] == cname &&
+			val.(map[string]interface{})["metric"].(map[string]interface{})["pod_name"] == podname &&
+			val.(map[string]interface{})["metric"].(map[string]interface{})["namespace"] == namespace {
+
+			strVal := val.(map[string]interface{})["value"].([]interface{})[1].(string)
+			value, _ := strconv.ParseFloat(strVal, 64)
+
+			toReturn := &Vector{
+				Timestamp: val.(map[string]interface{})["value"].([]interface{})[0].(float64),
+				Value:     value,
+			}
+			return toReturn
+
+		}
+	}
+	return &Vector{}
+}
+
+func findContainerMetricVectors(qr interface{}, cname string, podname string, namespace string) []*Vector {
+	for _, val := range qr.(map[string]interface{})["data"].(map[string]interface{})["result"].([]interface{}) {
+		if val.(map[string]interface{})["metric"].(map[string]interface{})["container_name"] == cname &&
+			val.(map[string]interface{})["metric"].(map[string]interface{})["pod_name"] == podname &&
+			val.(map[string]interface{})["metric"].(map[string]interface{})["namespace"] == namespace {
+			values := val.(map[string]interface{})["values"].([]interface{})
+			var vectors []*Vector
+			for _, value := range values {
+				strVal := value.([]interface{})[1].(string)
+				v, _ := strconv.ParseFloat(strVal, 64)
+				vectors = append(vectors, &Vector{
+					Timestamp: value.([]interface{})[0].(float64),
+					Value:     v,
+				})
+			}
+			return vectors
+		}
+	}
+	return []*Vector{}
+}

+ 12 - 0
kubernetes/cluster-role-binding.yaml

@@ -0,0 +1,12 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: cost-model
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: cost-model
+subjects:
+  - kind: ServiceAccount
+    name: cost-model
+    namespace: cost-model

+ 77 - 0
kubernetes/cluster-role.yaml

@@ -0,0 +1,77 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: cost-model 
+rules:
+  - apiGroups:
+      - ''
+    resources:
+      - configmaps
+      - deployments
+      - nodes
+      - pods
+      - services
+      - resourcequotas
+      - replicationcontrollers
+      - limitranges
+      - persistentvolumeclaims
+      - persistentvolumes
+      - namespaces
+      - endpoints
+    verbs:
+      - get
+      - list
+      - watch
+  - apiGroups:
+      - extensions
+    resources:
+      - daemonsets
+      - deployments
+      - replicasets
+    verbs:
+      - get
+      - list
+      - watch
+  - apiGroups:
+      - apps
+    resources:
+      - statefulsets
+      - deployments
+      - daemonsets
+      - replicasets
+    verbs:
+      - list
+      - watch
+  - apiGroups:
+      - batch
+    resources:
+      - cronjobs
+      - jobs
+    verbs:
+      - get
+      - list
+      - watch
+  - apiGroups:
+      - autoscaling
+    resources:
+      - horizontalpodautoscalers
+    verbs:
+      - get
+      - list
+      - watch
+  - apiGroups:
+      - policy
+    resources:
+      - poddisruptionbudgets
+    verbs:
+      - get
+      - list
+      - watch
+  - apiGroups: 
+      - storage.k8s.io
+    resources: 
+      - storageclasses
+    verbs:
+      - get
+      - list
+      - watch

+ 33 - 0
kubernetes/deployment.yaml

@@ -0,0 +1,33 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: cost-model
+  labels:
+    app: cost-model
+spec:
+  replicas: 1
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: cost-model
+    spec:
+      restartPolicy: Always
+      serviceAccountName: cost-model
+      containers:
+        - image: ajaytripathy/kubecost-cost-model:latest
+          name: cost-model
+          resources:
+            requests:
+              cpu: "10m"
+              memory: "55M"
+          env:
+            - 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.
+          imagePullPolicy: Always

+ 4 - 0
kubernetes/service-account.yaml

@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: cost-model

+ 12 - 0
kubernetes/service.yaml

@@ -0,0 +1,12 @@
+kind: Service
+apiVersion: v1
+metadata:
+  name: cost-model
+spec:
+  selector:
+    app: cost-model
+  type: ClusterIP
+  ports:
+    - name: cost-model
+      port: 9001
+      targetPort: 9001

+ 122 - 0
main.go

@@ -0,0 +1,122 @@
+package main
+
+import (
+	"encoding/json"
+	"log"
+	"net/http"
+	"os"
+
+	"github.com/julienschmidt/httprouter"
+	costAnalyzerCloud "github.com/kubecost/cost-model/cloud"
+	costModel "github.com/kubecost/cost-model/costmodel"
+	prometheusClient "github.com/prometheus/client_golang/api"
+
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+)
+
+type Accesses struct {
+	PrometheusClient prometheusClient.Client
+	KubeClientSet    *kubernetes.Clientset
+	Cloud            costAnalyzerCloud.Provider
+}
+
+type DataEnvelope struct {
+	Code    int         `json:"code"`
+	Status  string      `json:"status"`
+	Data    interface{} `json:"data"`
+	Message string      `json:"message,omitempty"`
+}
+
+func wrapData(data interface{}, err error) []byte {
+	var resp []byte
+	if err != nil {
+		resp, _ = json.Marshal(&DataEnvelope{
+			Code:    500,
+			Status:  "error",
+			Message: err.Error(),
+			Data:    data,
+		})
+	} else {
+		resp, _ = json.Marshal(&DataEnvelope{
+			Code:   200,
+			Status: "success",
+			Data:   data,
+		})
+
+	}
+	return resp
+}
+
+func (a *Accesses) RefreshPricingData(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+
+	err := a.Cloud.DownloadPricingData()
+
+	w.Write(wrapData(nil, err))
+}
+
+func (a *Accesses) CostDataModel(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+
+	window := r.URL.Query().Get("timeWindow")
+
+	data, err := costModel.ComputeCostData(a.PrometheusClient, a.KubeClientSet, a.Cloud, window)
+	w.Write(wrapData(data, err))
+}
+
+func (a *Accesses) CostDataModelRange(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+
+	start := r.URL.Query().Get("start")
+	end := r.URL.Query().Get("end")
+	window := r.URL.Query().Get("window")
+
+	data, err := costModel.ComputeCostDataRange(a.PrometheusClient, a.KubeClientSet, a.Cloud, start, end, window)
+	w.Write(wrapData(data, err))
+}
+
+func main() {
+	address := os.Getenv("PROMETHEUS_SERVER_ENDPOINT")
+	if address == "" {
+		log.Fatal("No address for prometheus set. Aborting.")
+	}
+	pc := prometheusClient.Config{
+		Address: address,
+	}
+	promCli, _ := prometheusClient.NewClient(pc)
+
+	// Kubernetes API setup
+	kc, err := rest.InClusterConfig()
+	if err != nil {
+		panic(err.Error())
+	}
+	kubeClientset, err := kubernetes.NewForConfig(kc)
+	if err != nil {
+		panic(err.Error())
+	}
+
+	cloudProviderKey := os.Getenv("CLOUD_PROVIDER_API_KEY")
+	cloudProvider, err := costAnalyzerCloud.NewProvider(kubeClientset, cloudProviderKey)
+	if err != nil {
+		panic(err.Error())
+	}
+
+	a := Accesses{
+		PrometheusClient: promCli,
+		KubeClientSet:    kubeClientset,
+		Cloud:            cloudProvider,
+	}
+
+	a.Cloud.DownloadPricingData()
+
+	router := httprouter.New()
+	router.GET("/costDataModel", a.CostDataModel)
+	router.GET("/costDataModelRange", a.CostDataModelRange)
+	router.POST("/refreshPricing", a.RefreshPricingData)
+
+	log.Fatal(http.ListenAndServe(":9001", router))
+}