gcpprovider.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. package cloud
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "io/ioutil"
  8. "math"
  9. "net/http"
  10. "net/url"
  11. "os"
  12. "regexp"
  13. "strconv"
  14. "strings"
  15. "k8s.io/klog"
  16. "cloud.google.com/go/bigquery"
  17. "cloud.google.com/go/compute/metadata"
  18. "golang.org/x/oauth2"
  19. "golang.org/x/oauth2/google"
  20. compute "google.golang.org/api/compute/v1"
  21. "google.golang.org/api/iterator"
  22. v1 "k8s.io/api/core/v1"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. "k8s.io/client-go/kubernetes"
  25. )
  26. const GKE_GPU_TAG = "cloud.google.com/gke-accelerator"
  27. const BigqueryUpdateType = "bigqueryupdate"
  28. type userAgentTransport struct {
  29. userAgent string
  30. base http.RoundTripper
  31. }
  32. func (t userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  33. req.Header.Set("User-Agent", t.userAgent)
  34. return t.base.RoundTrip(req)
  35. }
  36. // GCP implements a provider interface for GCP
  37. type GCP struct {
  38. Pricing map[string]*GCPPricing
  39. Clientset *kubernetes.Clientset
  40. APIKey string
  41. BaseCPUPrice string
  42. ProjectID string
  43. BillingDataDataset string
  44. *CustomProvider
  45. }
  46. type gcpAllocation struct {
  47. Aggregator bigquery.NullString
  48. Environment bigquery.NullString
  49. Service string
  50. Cost float64
  51. }
  52. func gcpAllocationToOutOfClusterAllocation(gcpAlloc gcpAllocation) *OutOfClusterAllocation {
  53. var aggregator string
  54. if gcpAlloc.Aggregator.Valid {
  55. aggregator = gcpAlloc.Aggregator.StringVal
  56. }
  57. var environment string
  58. if gcpAlloc.Environment.Valid {
  59. environment = gcpAlloc.Environment.StringVal
  60. }
  61. return &OutOfClusterAllocation{
  62. Aggregator: aggregator,
  63. Environment: environment,
  64. Service: gcpAlloc.Service,
  65. Cost: gcpAlloc.Cost,
  66. }
  67. }
  68. func (gcp *GCP) GetConfig() (*CustomPricing, error) {
  69. c, err := GetDefaultPricingData("gcp.json")
  70. if err != nil {
  71. return nil, err
  72. }
  73. return c, nil
  74. }
  75. type BigQueryConfig struct {
  76. ProjectID string `json:"projectID"`
  77. BillingDataDataset string `json:"billingDataDataset"`
  78. Key map[string]string `json:"key"`
  79. }
  80. func (gcp *GCP) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error) {
  81. c, err := GetDefaultPricingData("gcp.json")
  82. if err != nil {
  83. return nil, err
  84. }
  85. path := os.Getenv("CONFIG_PATH")
  86. if path == "" {
  87. path = "/models/"
  88. }
  89. if updateType == BigqueryUpdateType {
  90. a := BigQueryConfig{}
  91. err = json.NewDecoder(r).Decode(&a)
  92. if err != nil {
  93. return nil, err
  94. }
  95. c.ProjectID = a.ProjectID
  96. c.BillingDataDataset = a.BillingDataDataset
  97. j, err := json.Marshal(a.Key)
  98. if err != nil {
  99. return nil, err
  100. }
  101. keyPath := path + "key.json"
  102. err = ioutil.WriteFile(keyPath, j, 0644)
  103. if err != nil {
  104. return nil, err
  105. }
  106. } else {
  107. a := make(map[string]string)
  108. err = json.NewDecoder(r).Decode(&a)
  109. if err != nil {
  110. return nil, err
  111. }
  112. for k, v := range a {
  113. kUpper := strings.Title(k) // Just so we consistently supply / receive the same values, uppercase the first letter.
  114. err := SetCustomPricingField(c, kUpper, v)
  115. if err != nil {
  116. return nil, err
  117. }
  118. }
  119. }
  120. cj, err := json.Marshal(c)
  121. if err != nil {
  122. return nil, err
  123. }
  124. configPath := path + "gcp.json"
  125. err = ioutil.WriteFile(configPath, cj, 0644)
  126. if err != nil {
  127. return nil, err
  128. }
  129. return c, nil
  130. }
  131. func (gcp *GCP) ExternalAllocations(start string, end string, aggregator string) ([]*OutOfClusterAllocation, error) {
  132. c, err := GetDefaultPricingData("gcp.json")
  133. if err != nil {
  134. return nil, err
  135. }
  136. // start, end formatted like: "2019-04-20 00:00:00"
  137. queryString := fmt.Sprintf(`SELECT
  138. service,
  139. labels.key as aggregator,
  140. labels.value as environment,
  141. SUM(cost) as cost
  142. FROM (SELECT
  143. service.description as service,
  144. labels,
  145. cost
  146. FROM %s
  147. WHERE usage_start_time >= "%s" AND usage_start_time < "%s")
  148. LEFT JOIN UNNEST(labels) as labels
  149. 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"
  150. GROUP BY aggregator, environment, service;`, c.BillingDataDataset, start, end) // For example, "billing_data.gcp_billing_export_v1_01AC9F_74CF1D_5565A2"
  151. klog.V(3).Infof("Querying \"%s\" with : %s", c.ProjectID, queryString)
  152. return gcp.QuerySQL(queryString)
  153. }
  154. // QuerySQL should query BigQuery for billing data for out of cluster costs.
  155. func (gcp *GCP) QuerySQL(query string) ([]*OutOfClusterAllocation, error) {
  156. ctx := context.Background()
  157. client, err := bigquery.NewClient(ctx, gcp.ProjectID) // For example, "guestbook-227502"
  158. if err != nil {
  159. return nil, err
  160. }
  161. q := client.Query(query)
  162. it, err := q.Read(ctx)
  163. if err != nil {
  164. return nil, err
  165. }
  166. var allocations []*OutOfClusterAllocation
  167. for {
  168. var a gcpAllocation
  169. err := it.Next(&a)
  170. if err == iterator.Done {
  171. break
  172. }
  173. if err != nil {
  174. return nil, err
  175. }
  176. allocations = append(allocations, gcpAllocationToOutOfClusterAllocation(a))
  177. }
  178. return allocations, nil
  179. }
  180. // ClusterName returns the name of a GKE cluster, as provided by metadata.
  181. func (*GCP) ClusterName() ([]byte, error) {
  182. metadataClient := metadata.NewClient(&http.Client{Transport: userAgentTransport{
  183. userAgent: "kubecost",
  184. base: http.DefaultTransport,
  185. }})
  186. attribute, err := metadataClient.InstanceAttributeValue("cluster-name")
  187. if err != nil {
  188. return nil, err
  189. }
  190. m := make(map[string]string)
  191. m["name"] = attribute
  192. m["provider"] = "GCP"
  193. return json.Marshal(m)
  194. }
  195. // AddServiceKey adds the service key as required for GetDisks
  196. func (*GCP) AddServiceKey(formValues url.Values) error {
  197. key := formValues.Get("key")
  198. k := []byte(key)
  199. return ioutil.WriteFile("/var/configs/key.json", k, 0644)
  200. }
  201. // GetDisks returns the GCP disks backing PVs. Useful because sometimes k8s will not clean up PVs correctly. Requires a json config in /var/configs with key region.
  202. func (*GCP) GetDisks() ([]byte, error) {
  203. // metadata API setup
  204. metadataClient := metadata.NewClient(&http.Client{Transport: userAgentTransport{
  205. userAgent: "kubecost",
  206. base: http.DefaultTransport,
  207. }})
  208. projID, err := metadataClient.ProjectID()
  209. if err != nil {
  210. return nil, err
  211. }
  212. client, err := google.DefaultClient(oauth2.NoContext,
  213. "https://www.googleapis.com/auth/compute.readonly")
  214. if err != nil {
  215. return nil, err
  216. }
  217. svc, err := compute.New(client)
  218. if err != nil {
  219. return nil, err
  220. }
  221. res, err := svc.Disks.AggregatedList(projID).Do()
  222. if err != nil {
  223. return nil, err
  224. }
  225. return json.Marshal(res)
  226. }
  227. // GCPPricing represents GCP pricing data for a SKU
  228. type GCPPricing struct {
  229. Name string `json:"name"`
  230. SKUID string `json:"skuId"`
  231. Description string `json:"description"`
  232. Category *GCPResourceInfo `json:"category"`
  233. ServiceRegions []string `json:"serviceRegions"`
  234. PricingInfo []*PricingInfo `json:"pricingInfo"`
  235. ServiceProviderName string `json:"serviceProviderName"`
  236. Node *Node `json:"node"`
  237. }
  238. // PricingInfo contains metadata about a cost.
  239. type PricingInfo struct {
  240. Summary string `json:"summary"`
  241. PricingExpression *PricingExpression `json:"pricingExpression"`
  242. CurrencyConversionRate int `json:"currencyConversionRate"`
  243. EffectiveTime string `json:""`
  244. }
  245. // PricingExpression contains metadata about a cost.
  246. type PricingExpression struct {
  247. UsageUnit string `json:"usageUnit"`
  248. UsageUnitDescription string `json:"usageUnitDescription"`
  249. BaseUnit string `json:"baseUnit"`
  250. BaseUnitConversionFactor int64 `json:"-"`
  251. DisplayQuantity int `json:"displayQuantity"`
  252. TieredRates []*TieredRates `json:"tieredRates"`
  253. }
  254. // TieredRates contain data about variable pricing.
  255. type TieredRates struct {
  256. StartUsageAmount int `json:"startUsageAmount"`
  257. UnitPrice *UnitPriceInfo `json:"unitPrice"`
  258. }
  259. // UnitPriceInfo contains data about the actual price being charged.
  260. type UnitPriceInfo struct {
  261. CurrencyCode string `json:"currencyCode"`
  262. Units string `json:"units"`
  263. Nanos float64 `json:"nanos"`
  264. }
  265. // GCPResourceInfo contains metadata about the node.
  266. type GCPResourceInfo struct {
  267. ServiceDisplayName string `json:"serviceDisplayName"`
  268. ResourceFamily string `json:"resourceFamily"`
  269. ResourceGroup string `json:"resourceGroup"`
  270. UsageType string `json:"usageType"`
  271. }
  272. func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key) (map[string]*GCPPricing, string, error) {
  273. gcpPricingList := make(map[string]*GCPPricing)
  274. var nextPageToken string
  275. dec := json.NewDecoder(r)
  276. for {
  277. t, err := dec.Token()
  278. if err == io.EOF {
  279. break
  280. }
  281. if t == "skus" {
  282. _, err := dec.Token() // consumes [
  283. if err != nil {
  284. return nil, "", err
  285. }
  286. for dec.More() {
  287. product := &GCPPricing{}
  288. err := dec.Decode(&product)
  289. if err != nil {
  290. return nil, "", err
  291. }
  292. usageType := strings.ToLower(product.Category.UsageType)
  293. instanceType := strings.ToLower(product.Category.ResourceGroup)
  294. if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "CUSTOM") {
  295. instanceType = "custom"
  296. }
  297. var partialCPU float64
  298. if strings.ToLower(instanceType) == "f1micro" {
  299. partialCPU = 0.2
  300. } else if strings.ToLower(instanceType) == "g1small" {
  301. partialCPU = 0.5
  302. }
  303. var gpuType string
  304. provIdRx := regexp.MustCompile("(Nvidia Tesla [^ ]+) ")
  305. for matchnum, group := range provIdRx.FindStringSubmatch(product.Description) {
  306. if matchnum == 1 {
  307. gpuType = strings.ToLower(strings.Join(strings.Split(group, " "), "-"))
  308. klog.V(4).Info("GPU type found: " + gpuType)
  309. }
  310. }
  311. for _, sr := range product.ServiceRegions {
  312. region := sr
  313. candidateKey := region + "," + instanceType + "," + usageType
  314. candidateKeyGPU := candidateKey + ",gpu"
  315. if gpuType != "" {
  316. lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
  317. var nanos float64
  318. if len(product.PricingInfo) > 0 {
  319. nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
  320. } else {
  321. continue
  322. }
  323. hourlyPrice := nanos * math.Pow10(-9)
  324. for k, key := range inputKeys {
  325. if key.GPUType() == gpuType {
  326. if region == strings.Split(k, ",")[0] {
  327. klog.V(3).Infof("Matched GPU to node in region \"%s\"", region)
  328. candidateKeyGPU = key.Features()
  329. if pl, ok := gcpPricingList[candidateKeyGPU]; ok {
  330. pl.Node.GPUName = gpuType
  331. pl.Node.GPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  332. pl.Node.GPU = "1"
  333. } else {
  334. product.Node = &Node{
  335. GPUName: gpuType,
  336. GPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  337. GPU: "1",
  338. }
  339. klog.V(3).Infof("Added data for " + candidateKeyGPU)
  340. gcpPricingList[candidateKeyGPU] = product
  341. }
  342. }
  343. }
  344. }
  345. } else {
  346. _, ok := inputKeys[candidateKey]
  347. _, ok2 := inputKeys[candidateKeyGPU]
  348. if ok || ok2 {
  349. lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
  350. var nanos float64
  351. if len(product.PricingInfo) > 0 {
  352. nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
  353. } else {
  354. continue
  355. }
  356. hourlyPrice := nanos * math.Pow10(-9)
  357. if hourlyPrice == 0 {
  358. continue
  359. } else if strings.Contains(strings.ToUpper(product.Description), "RAM") {
  360. if instanceType == "custom" {
  361. klog.V(4).Infof("RAM custom sku is: " + product.Name)
  362. }
  363. if _, ok := gcpPricingList[candidateKey]; ok {
  364. gcpPricingList[candidateKey].Node.RAMCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  365. } else {
  366. product.Node = &Node{
  367. RAMCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  368. }
  369. if partialCPU != 0 {
  370. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  371. }
  372. product.Node.UsageType = usageType
  373. gcpPricingList[candidateKey] = product
  374. }
  375. if _, ok := gcpPricingList[candidateKeyGPU]; ok {
  376. gcpPricingList[candidateKeyGPU].Node.RAMCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  377. } else {
  378. product.Node = &Node{
  379. RAMCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  380. }
  381. if partialCPU != 0 {
  382. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  383. }
  384. product.Node.UsageType = usageType
  385. gcpPricingList[candidateKeyGPU] = product
  386. }
  387. break
  388. } else {
  389. if _, ok := gcpPricingList[candidateKey]; ok {
  390. gcpPricingList[candidateKey].Node.VCPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  391. } else {
  392. product.Node = &Node{
  393. VCPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  394. }
  395. if partialCPU != 0 {
  396. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  397. }
  398. product.Node.UsageType = usageType
  399. gcpPricingList[candidateKey] = product
  400. }
  401. if _, ok := gcpPricingList[candidateKeyGPU]; ok {
  402. gcpPricingList[candidateKeyGPU].Node.VCPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  403. } else {
  404. product.Node = &Node{
  405. VCPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  406. }
  407. if partialCPU != 0 {
  408. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  409. }
  410. product.Node.UsageType = usageType
  411. gcpPricingList[candidateKeyGPU] = product
  412. }
  413. break
  414. }
  415. }
  416. }
  417. }
  418. }
  419. }
  420. if t == "nextPageToken" {
  421. pageToken, err := dec.Token()
  422. if err != nil {
  423. klog.V(2).Infof("Error parsing nextpage token: " + err.Error())
  424. return nil, "", err
  425. }
  426. if pageToken.(string) != "" {
  427. nextPageToken = pageToken.(string)
  428. } else {
  429. nextPageToken = "done"
  430. }
  431. }
  432. }
  433. return gcpPricingList, nextPageToken, nil
  434. }
  435. func (gcp *GCP) parsePages(inputKeys map[string]Key) (map[string]*GCPPricing, error) {
  436. var pages []map[string]*GCPPricing
  437. url := "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + gcp.APIKey
  438. klog.V(2).Infof("Fetch GCP Billing Data from URL: %s", url)
  439. var parsePagesHelper func(string) error
  440. parsePagesHelper = func(pageToken string) error {
  441. if pageToken == "done" {
  442. return nil
  443. } else if pageToken != "" {
  444. url = url + "&pageToken=" + pageToken
  445. }
  446. resp, err := http.Get(url)
  447. if err != nil {
  448. return err
  449. }
  450. page, token, err := gcp.parsePage(resp.Body, inputKeys)
  451. if err != nil {
  452. return err
  453. }
  454. pages = append(pages, page)
  455. return parsePagesHelper(token)
  456. }
  457. err := parsePagesHelper("")
  458. if err != nil {
  459. return nil, err
  460. }
  461. returnPages := make(map[string]*GCPPricing)
  462. for _, page := range pages {
  463. for k, v := range page {
  464. if val, ok := returnPages[k]; ok { //keys may need to be merged
  465. if val.Node.RAMCost != "" && val.Node.VCPUCost == "" {
  466. val.Node.VCPUCost = v.Node.VCPUCost
  467. } else if val.Node.VCPUCost != "" && val.Node.RAMCost == "" {
  468. val.Node.RAMCost = v.Node.RAMCost
  469. } else {
  470. returnPages[k] = v
  471. }
  472. } else {
  473. returnPages[k] = v
  474. }
  475. }
  476. }
  477. return returnPages, err
  478. }
  479. // 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.
  480. func (gcp *GCP) DownloadPricingData() error {
  481. c, err := GetDefaultPricingData("gcp.json")
  482. if err != nil {
  483. klog.V(2).Infof("Error downloading default pricing data: %s", err.Error())
  484. }
  485. gcp.BaseCPUPrice = c.CPU
  486. gcp.ProjectID = c.ProjectID
  487. gcp.BillingDataDataset = c.BillingDataDataset
  488. nodeList, err := gcp.Clientset.CoreV1().Nodes().List(metav1.ListOptions{})
  489. if err != nil {
  490. return err
  491. }
  492. inputkeys := make(map[string]Key)
  493. for _, n := range nodeList.Items {
  494. labels := n.GetObjectMeta().GetLabels()
  495. key := gcp.GetKey(labels)
  496. inputkeys[key.Features()] = key
  497. }
  498. pages, err := gcp.parsePages(inputkeys)
  499. if err != nil {
  500. return err
  501. }
  502. gcp.Pricing = pages
  503. return nil
  504. }
  505. type gcpKey struct {
  506. Labels map[string]string
  507. }
  508. func (gcp *GCP) GetKey(labels map[string]string) Key {
  509. return &gcpKey{
  510. Labels: labels,
  511. }
  512. }
  513. func (gcp *gcpKey) ID() string {
  514. return ""
  515. }
  516. func (gcp *gcpKey) GPUType() string {
  517. if t, ok := gcp.Labels[GKE_GPU_TAG]; ok {
  518. klog.V(4).Infof("GPU of type: \"%s\" found", t)
  519. return t
  520. }
  521. return ""
  522. }
  523. // GetKey maps node labels to information needed to retrieve pricing data
  524. func (gcp *gcpKey) Features() string {
  525. instanceType := strings.ToLower(strings.Join(strings.Split(gcp.Labels[v1.LabelInstanceType], "-")[:2], ""))
  526. if instanceType == "n1highmem" || instanceType == "n1highcpu" {
  527. instanceType = "n1standard" // These are priced the same. TODO: support n1ultrahighmem
  528. } else if strings.HasPrefix(instanceType, "custom") {
  529. instanceType = "custom" // The suffix of custom does not matter
  530. }
  531. region := strings.ToLower(gcp.Labels[v1.LabelZoneRegion])
  532. var usageType string
  533. if t, ok := gcp.Labels["cloud.google.com/gke-preemptible"]; ok && t == "true" {
  534. usageType = "preemptible"
  535. } else {
  536. usageType = "ondemand"
  537. }
  538. if _, ok := gcp.Labels[GKE_GPU_TAG]; ok {
  539. return region + "," + instanceType + "," + usageType + "," + "gpu"
  540. }
  541. return region + "," + instanceType + "," + usageType
  542. }
  543. // AllNodePricing returns the GCP pricing objects stored
  544. func (gcp *GCP) AllNodePricing() (interface{}, error) {
  545. return gcp.Pricing, nil
  546. }
  547. // NodePricing returns GCP pricing data for a single node
  548. func (gcp *GCP) NodePricing(key Key) (*Node, error) {
  549. if n, ok := gcp.Pricing[key.Features()]; ok {
  550. klog.V(4).Infof("Returning pricing for node %s: %+v from SKU %s", key, n.Node, n.Name)
  551. n.Node.BaseCPUPrice = gcp.BaseCPUPrice
  552. return n.Node, nil
  553. }
  554. klog.V(1).Infof("Warning: no pricing data found for %s: %s", key.Features(), key)
  555. return nil, fmt.Errorf("Warning: no pricing data found for %s", key)
  556. }