Przeglądaj źródła

Merge pull request #60 from kubecost/AjayTripathy-gcp-pricing

External gcp pricing
Ajay Tripathy 7 lat temu
rodzic
commit
d734be793c
8 zmienionych plików z 141 dodań i 33 usunięć
  1. 1 0
      Dockerfile
  2. 1 1
      cloud/aws.json
  3. 5 5
      cloud/awsprovider.go
  4. 90 12
      cloud/gcpprovider.go
  5. 29 15
      cloud/provider.go
  6. 1 0
      go.mod
  7. 2 0
      go.sum
  8. 12 0
      main.go

+ 1 - 0
Dockerfile

@@ -33,4 +33,5 @@ COPY --from=build-env /go/bin/app /go/bin/app
 ADD ./cloud/default.json /models/default.json
 ADD ./cloud/azure.json /models/azure.json
 ADD ./cloud/aws.json /models/aws.json
+ADD ./cloud/gcp.json /models/gcp.json
 ENTRYPOINT ["/go/bin/app"]

+ 1 - 1
cloud/aws.json

@@ -12,5 +12,5 @@
     "awsSpotDataRegion":"us-east-2",
     "awsSpotDataBucket": "kc-test-spot",
     "awsSpotDataPrefix": "spotdata",
-    "awsProjectID": "530337586275"
+    "projectID": "530337586275"
 }

+ 5 - 5
cloud/awsprovider.go

@@ -169,7 +169,7 @@ func (k *awsKey) ID() string {
 			return group
 		}
 	}
-	klog.V(3).Info("Could not find instance ID in \"%s\"", k.ProviderID)
+	klog.V(3).Infof("Could not find instance ID in \"%s\"", k.ProviderID)
 	return ""
 }
 
@@ -323,10 +323,6 @@ func (aws *AWS) DownloadPricingData() error {
 		}
 	}
 
-	if err != nil {
-		return err
-	}
-
 	sp, err := parseSpotData(aws.SpotDataBucket, aws.SpotDataPrefix, aws.ProjectID, aws.SpotDataRegion, aws.ServiceKeyName, aws.ServiceKeySecret)
 	if err != nil {
 		klog.V(1).Infof("Error downloading spot data %s", err.Error())
@@ -564,6 +560,10 @@ func (*AWS) GetDisks() ([]byte, error) {
 	return json.Marshal(volumeResult)
 }
 
+func (*AWS) ExternalAllocations(start string, end string) ([]*OutOfClusterAllocation, error) {
+	return nil, nil // TODO: transform the QuerySQL lines into the new OutOfClusterAllocation Struct
+}
+
 // QuerySQL can query a properly configured Athena database.
 // Used to fetch billing data.
 // Requires a json config in /var/configs with key region, output, and database.

+ 90 - 12
cloud/gcpprovider.go

@@ -1,6 +1,7 @@
 package cloud
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -13,10 +14,12 @@ import (
 
 	"k8s.io/klog"
 
+	"cloud.google.com/go/bigquery"
 	"cloud.google.com/go/compute/metadata"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
 	compute "google.golang.org/api/compute/v1"
+	"google.golang.org/api/iterator"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
@@ -34,15 +37,87 @@ func (t userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error)
 
 // GCP implements a provider interface for GCP
 type GCP struct {
-	Pricing      map[string]*GCPPricing
-	Clientset    *kubernetes.Clientset
-	APIKey       string
-	BaseCPUPrice string
+	Pricing            map[string]*GCPPricing
+	Clientset          *kubernetes.Clientset
+	APIKey             string
+	BaseCPUPrice       string
+	ProjectID          string
+	BillingDataDataset string
 }
 
-// QuerySQL should query BigQuery for billing data for out of cluster costs. TODO: Implement.
-func (*GCP) QuerySQL(query string) ([]byte, error) {
-	return nil, nil
+type gcpAllocation struct {
+	Aggregator  bigquery.NullString
+	Environment bigquery.NullString
+	Service     string
+	Cost        float64
+}
+
+func gcpAllocationToOutOfClusterAllocation(gcpAlloc gcpAllocation) *OutOfClusterAllocation {
+	var aggregator string
+	if gcpAlloc.Aggregator.Valid {
+		aggregator = gcpAlloc.Aggregator.StringVal
+	}
+
+	var environment string
+	if gcpAlloc.Environment.Valid {
+		environment = gcpAlloc.Environment.StringVal
+	}
+
+	return &OutOfClusterAllocation{
+		Aggregator:  aggregator,
+		Environment: environment,
+		Service:     gcpAlloc.Service,
+		Cost:        gcpAlloc.Cost,
+	}
+}
+
+func (gcp *GCP) ExternalAllocations(start string, end string) ([]*OutOfClusterAllocation, error) {
+	// start, end formatted like: "2019-04-20 00:00:00"
+	queryString := fmt.Sprintf(`SELECT
+					service,
+					labels.key as aggregator,
+					labels.value as environment,
+					SUM(cost) as cost
+					FROM  (SELECT 
+							service.description as service,
+							labels,
+							cost 
+						FROM %s
+						WHERE usage_start_time >= "%s" AND usage_start_time < "%s")
+						LEFT JOIN UNNEST(labels) as labels
+						ON labels.key = "kubernetes_namespace" OR labels.key = "kubernetes_container" OR labels.key = "kubernetes_deployment" OR labels.key = "kubernetes_pod" OR labels.key = "kubernetes_daemonset"
+				GROUP BY aggregator, environment, service;`, gcp.BillingDataDataset, start, end) // For example, "billing_data.gcp_billing_export_v1_01AC9F_74CF1D_5565A2"
+	klog.V(3).Infof("HERE IS THE PROJECT ID: %s", gcp.ProjectID)
+	klog.V(3).Infof("HERE IS THE QUERY STRING: %s", queryString)
+	return gcp.QuerySQL(queryString)
+}
+
+// QuerySQL should query BigQuery for billing data for out of cluster costs.
+func (gcp *GCP) QuerySQL(query string) ([]*OutOfClusterAllocation, error) {
+	ctx := context.Background()
+	client, err := bigquery.NewClient(ctx, gcp.ProjectID) // For example, "guestbook-227502"
+	if err != nil {
+		return nil, err
+	}
+
+	q := client.Query(query)
+	it, err := q.Read(ctx)
+	if err != nil {
+		return nil, err
+	}
+	var allocations []*OutOfClusterAllocation
+	for {
+		var a gcpAllocation
+		err := it.Next(&a)
+		if err == iterator.Done {
+			break
+		}
+		if err != nil {
+			return nil, err
+		}
+		allocations = append(allocations, gcpAllocationToOutOfClusterAllocation(a))
+	}
+	return allocations, nil
 }
 
 // ClusterName returns the name of a GKE cluster, as provided by metadata.
@@ -293,6 +368,14 @@ func (gcp *GCP) parsePages(inputKeys map[string]bool) (map[string]*GCPPricing, e
 // DownloadPricingData fetches data from the GCP Pricing API. Requires a key-- a kubecost key is provided for quickstart, but should be replaced by a users.
 func (gcp *GCP) DownloadPricingData() error {
 
+	c, err := GetDefaultPricingData("gcp.json")
+	if err != nil {
+		klog.V(2).Infof("Error downloading default pricing data: %s", err.Error())
+	}
+	gcp.BaseCPUPrice = c.CPU
+	gcp.ProjectID = c.ProjectID
+	gcp.BillingDataDataset = c.BillingDataDataset
+
 	nodeList, err := gcp.Clientset.CoreV1().Nodes().List(metav1.ListOptions{})
 	if err != nil {
 		return err
@@ -311,11 +394,6 @@ func (gcp *GCP) DownloadPricingData() error {
 		return err
 	}
 	gcp.Pricing = pages
-	c, err := GetDefaultPricingData("default.json")
-	if err != nil {
-		klog.V(2).Infof("Error downloading default pricing data: %s", err.Error())
-	}
-	gcp.BaseCPUPrice = c.CPU
 
 	return nil
 }

+ 29 - 15
cloud/provider.go

@@ -38,6 +38,15 @@ type Key interface {
 	Features() string // Features are a comma separated string of node metadata that could match pricing
 }
 
+// OutOfClusterAllocation represents a cloud provider cost not associated with kubernetes
+type OutOfClusterAllocation struct {
+	Aggregator  string  `json:"aggregator"`
+	Environment string  `json:"environment"`
+	Service     string  `json:"service"`
+	Cost        float64 `json:"cost"`
+	Cluster     string  `json:"cluster"`
+}
+
 // Provider represents a k8s provider.
 type Provider interface {
 	ClusterName() ([]byte, error)
@@ -48,7 +57,7 @@ type Provider interface {
 	DownloadPricingData() error
 	GetKey(map[string]string) Key
 
-	QuerySQL(string) ([]byte, error)
+	ExternalAllocations(string, string) ([]*OutOfClusterAllocation, error)
 }
 
 // GetDefaultPricingData will search for a json file representing pricing data in /models/ and use it for base pricing info.
@@ -71,20 +80,21 @@ func GetDefaultPricingData(fname string) (*CustomPricing, error) {
 }
 
 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:"spotLabelValue,omitempty"`
-	ServiceKeyName   string `json:"awsServiceKeyName,omitempty"`
-	ServiceKeySecret string `json:"awsServiceKeySecret,omitempty"`
-	SpotDataRegion   string `json:"awsSpotDataRegion,omitempty"`
-	SpotDataBucket   string `json:"awsSpotDataBucket,omitempty"`
-	SpotDataPrefix   string `json:"awsSpotDataPrefix,omitempty"`
-	ProjectID        string `json:"awsProjectID,omitempty"`
+	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:"spotLabelValue,omitempty"`
+	ServiceKeyName     string `json:"awsServiceKeyName,omitempty"`
+	ServiceKeySecret   string `json:"awsServiceKeySecret,omitempty"`
+	SpotDataRegion     string `json:"awsSpotDataRegion,omitempty"`
+	SpotDataBucket     string `json:"awsSpotDataBucket,omitempty"`
+	SpotDataPrefix     string `json:"awsSpotDataPrefix,omitempty"`
+	ProjectID          string `json:"projectID,omitempty"`
+	BillingDataDataset string `json:"billingDataDataset,omitempty"`
 }
 
 type NodePrice struct {
@@ -172,6 +182,10 @@ func (c *CustomProvider) GetKey(labels map[string]string) Key {
 	}
 }
 
+func (*CustomProvider) ExternalAllocations(start string, end string) ([]*OutOfClusterAllocation, error) {
+	return nil, nil // TODO: transform the QuerySQL lines into the new OutOfClusterAllocation Struct
+}
+
 func (*CustomProvider) QuerySQL(query string) ([]byte, error) {
 	return nil, nil
 }

+ 1 - 0
go.mod

@@ -8,6 +8,7 @@ require (
 	cloud.google.com/go v0.34.0
 	github.com/aws/aws-sdk-go v1.19.10
 	github.com/golang/mock v1.2.0
+	github.com/googleapis/gax-go v2.0.2+incompatible // indirect
 	github.com/jszwec/csvutil v1.2.1
 	github.com/julienschmidt/httprouter v1.2.0
 	github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829

+ 2 - 0
go.sum

@@ -49,6 +49,8 @@ github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeq
 github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
 github.com/google/uuid v0.0.0-20171113160352-8c31c18f31ed/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww=
+github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
 github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k=
 github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=

+ 12 - 0
main.go

@@ -104,6 +104,17 @@ func (a *Accesses) CostDataModelRange(w http.ResponseWriter, r *http.Request, ps
 	w.Write(wrapData(data, err))
 }
 
+func (a *Accesses) OutofClusterCosts(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")
+
+	data, err := a.Cloud.ExternalAllocations(start, end)
+	w.Write(wrapData(data, err))
+}
+
 func (p *Accesses) GetAllNodePricing(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Access-Control-Allow-Origin", "*")
@@ -252,6 +263,7 @@ func main() {
 	router := httprouter.New()
 	router.GET("/costDataModel", a.CostDataModel)
 	router.GET("/costDataModelRange", a.CostDataModelRange)
+	router.GET("/outOfClusterCosts", a.OutofClusterCosts)
 	router.GET("/allNodePricing", a.GetAllNodePricing)
 	router.GET("/healthz", Healthz)
 	router.POST("/refreshPricing", a.RefreshPricingData)