gcpprovider.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  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. "sync"
  16. "k8s.io/klog"
  17. "cloud.google.com/go/bigquery"
  18. "cloud.google.com/go/compute/metadata"
  19. "golang.org/x/oauth2"
  20. "golang.org/x/oauth2/google"
  21. compute "google.golang.org/api/compute/v1"
  22. "google.golang.org/api/iterator"
  23. v1 "k8s.io/api/core/v1"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. "k8s.io/client-go/kubernetes"
  26. )
  27. const GKE_GPU_TAG = "cloud.google.com/gke-accelerator"
  28. const BigqueryUpdateType = "bigqueryupdate"
  29. type userAgentTransport struct {
  30. userAgent string
  31. base http.RoundTripper
  32. }
  33. func (t userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  34. req.Header.Set("User-Agent", t.userAgent)
  35. return t.base.RoundTrip(req)
  36. }
  37. // GCP implements a provider interface for GCP
  38. type GCP struct {
  39. Pricing map[string]*GCPPricing
  40. Clientset *kubernetes.Clientset
  41. APIKey string
  42. BaseCPUPrice string
  43. ProjectID string
  44. BillingDataDataset string
  45. DownloadPricingDataLock sync.RWMutex
  46. *CustomProvider
  47. }
  48. type gcpAllocation struct {
  49. Aggregator bigquery.NullString
  50. Environment bigquery.NullString
  51. Service string
  52. Cost float64
  53. }
  54. func gcpAllocationToOutOfClusterAllocation(gcpAlloc gcpAllocation) *OutOfClusterAllocation {
  55. var aggregator string
  56. if gcpAlloc.Aggregator.Valid {
  57. aggregator = gcpAlloc.Aggregator.StringVal
  58. }
  59. var environment string
  60. if gcpAlloc.Environment.Valid {
  61. environment = gcpAlloc.Environment.StringVal
  62. }
  63. return &OutOfClusterAllocation{
  64. Aggregator: aggregator,
  65. Environment: environment,
  66. Service: gcpAlloc.Service,
  67. Cost: gcpAlloc.Cost,
  68. }
  69. }
  70. func (gcp *GCP) GetConfig() (*CustomPricing, error) {
  71. c, err := GetDefaultPricingData("gcp.json")
  72. if err != nil {
  73. return nil, err
  74. }
  75. return c, nil
  76. }
  77. type BigQueryConfig struct {
  78. ProjectID string `json:"projectID"`
  79. BillingDataDataset string `json:"billingDataDataset"`
  80. Key map[string]string `json:"key"`
  81. }
  82. func (gcp *GCP) GetManagementPlatform() (string, error) {
  83. nodes, err := gcp.Clientset.CoreV1().Nodes().List(metav1.ListOptions{})
  84. if err != nil {
  85. return "", err
  86. }
  87. if len(nodes.Items) > 0 {
  88. n := nodes.Items[0]
  89. version := n.Status.NodeInfo.KubeletVersion
  90. if strings.Contains(version, "gke") {
  91. return "gke", nil
  92. }
  93. }
  94. return "", nil
  95. }
  96. func (gcp *GCP) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error) {
  97. c, err := GetDefaultPricingData("gcp.json")
  98. if err != nil {
  99. return nil, err
  100. }
  101. path := os.Getenv("CONFIG_PATH")
  102. if path == "" {
  103. path = "/models/"
  104. }
  105. if updateType == BigqueryUpdateType {
  106. a := BigQueryConfig{}
  107. err = json.NewDecoder(r).Decode(&a)
  108. if err != nil {
  109. return nil, err
  110. }
  111. c.ProjectID = a.ProjectID
  112. c.BillingDataDataset = a.BillingDataDataset
  113. j, err := json.Marshal(a.Key)
  114. if err != nil {
  115. return nil, err
  116. }
  117. keyPath := path + "key.json"
  118. err = ioutil.WriteFile(keyPath, j, 0644)
  119. if err != nil {
  120. return nil, err
  121. }
  122. } else {
  123. a := make(map[string]string)
  124. err = json.NewDecoder(r).Decode(&a)
  125. if err != nil {
  126. return nil, err
  127. }
  128. for k, v := range a {
  129. kUpper := strings.Title(k) // Just so we consistently supply / receive the same values, uppercase the first letter.
  130. err := SetCustomPricingField(c, kUpper, v)
  131. if err != nil {
  132. return nil, err
  133. }
  134. }
  135. }
  136. cj, err := json.Marshal(c)
  137. if err != nil {
  138. return nil, err
  139. }
  140. configPath := path + "gcp.json"
  141. err = ioutil.WriteFile(configPath, cj, 0644)
  142. if err != nil {
  143. return nil, err
  144. }
  145. return c, nil
  146. }
  147. // ExternalAllocations represents tagged assets outside the scope of kubernetes.
  148. // "start" and "end" are dates of the format YYYY-MM-DD
  149. // "aggregator" is the tag used to determine how to allocate those assets, ie namespace, pod, etc.
  150. func (gcp *GCP) ExternalAllocations(start string, end string, aggregator string) ([]*OutOfClusterAllocation, error) {
  151. c, err := GetDefaultPricingData("gcp.json")
  152. if err != nil {
  153. return nil, err
  154. }
  155. // start, end formatted like: "2019-04-20 00:00:00"
  156. queryString := fmt.Sprintf(`SELECT
  157. service,
  158. labels.key as aggregator,
  159. labels.value as environment,
  160. SUM(cost) as cost
  161. FROM (SELECT
  162. service.description as service,
  163. labels,
  164. cost
  165. FROM %s
  166. WHERE usage_start_time >= "%s" AND usage_start_time < "%s")
  167. LEFT JOIN UNNEST(labels) as labels
  168. 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"
  169. GROUP BY aggregator, environment, service;`, c.BillingDataDataset, start, end) // For example, "billing_data.gcp_billing_export_v1_01AC9F_74CF1D_5565A2"
  170. klog.V(3).Infof("Querying \"%s\" with : %s", c.ProjectID, queryString)
  171. return gcp.QuerySQL(queryString)
  172. }
  173. // QuerySQL should query BigQuery for billing data for out of cluster costs.
  174. func (gcp *GCP) QuerySQL(query string) ([]*OutOfClusterAllocation, error) {
  175. c, err := GetDefaultPricingData("gcp.json")
  176. if err != nil {
  177. return nil, err
  178. }
  179. ctx := context.Background()
  180. client, err := bigquery.NewClient(ctx, c.ProjectID) // For example, "guestbook-227502"
  181. if err != nil {
  182. return nil, err
  183. }
  184. q := client.Query(query)
  185. it, err := q.Read(ctx)
  186. if err != nil {
  187. return nil, err
  188. }
  189. var allocations []*OutOfClusterAllocation
  190. for {
  191. var a gcpAllocation
  192. err := it.Next(&a)
  193. if err == iterator.Done {
  194. break
  195. }
  196. if err != nil {
  197. return nil, err
  198. }
  199. allocations = append(allocations, gcpAllocationToOutOfClusterAllocation(a))
  200. }
  201. return allocations, nil
  202. }
  203. // ClusterName returns the name of a GKE cluster, as provided by metadata.
  204. func (*GCP) ClusterName() ([]byte, error) {
  205. metadataClient := metadata.NewClient(&http.Client{Transport: userAgentTransport{
  206. userAgent: "kubecost",
  207. base: http.DefaultTransport,
  208. }})
  209. attribute, err := metadataClient.InstanceAttributeValue("cluster-name")
  210. if err != nil {
  211. return nil, err
  212. }
  213. m := make(map[string]string)
  214. m["name"] = attribute
  215. m["provider"] = "GCP"
  216. return json.Marshal(m)
  217. }
  218. // AddServiceKey adds the service key as required for GetDisks
  219. func (*GCP) AddServiceKey(formValues url.Values) error {
  220. key := formValues.Get("key")
  221. k := []byte(key)
  222. return ioutil.WriteFile("/var/configs/key.json", k, 0644)
  223. }
  224. // 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.
  225. func (*GCP) GetDisks() ([]byte, error) {
  226. // metadata API setup
  227. metadataClient := metadata.NewClient(&http.Client{Transport: userAgentTransport{
  228. userAgent: "kubecost",
  229. base: http.DefaultTransport,
  230. }})
  231. projID, err := metadataClient.ProjectID()
  232. if err != nil {
  233. return nil, err
  234. }
  235. client, err := google.DefaultClient(oauth2.NoContext,
  236. "https://www.googleapis.com/auth/compute.readonly")
  237. if err != nil {
  238. return nil, err
  239. }
  240. svc, err := compute.New(client)
  241. if err != nil {
  242. return nil, err
  243. }
  244. res, err := svc.Disks.AggregatedList(projID).Do()
  245. if err != nil {
  246. return nil, err
  247. }
  248. return json.Marshal(res)
  249. }
  250. // GCPPricing represents GCP pricing data for a SKU
  251. type GCPPricing struct {
  252. Name string `json:"name"`
  253. SKUID string `json:"skuId"`
  254. Description string `json:"description"`
  255. Category *GCPResourceInfo `json:"category"`
  256. ServiceRegions []string `json:"serviceRegions"`
  257. PricingInfo []*PricingInfo `json:"pricingInfo"`
  258. ServiceProviderName string `json:"serviceProviderName"`
  259. Node *Node `json:"node"`
  260. PV *PV `json:"pv"`
  261. }
  262. // PricingInfo contains metadata about a cost.
  263. type PricingInfo struct {
  264. Summary string `json:"summary"`
  265. PricingExpression *PricingExpression `json:"pricingExpression"`
  266. CurrencyConversionRate int `json:"currencyConversionRate"`
  267. EffectiveTime string `json:""`
  268. }
  269. // PricingExpression contains metadata about a cost.
  270. type PricingExpression struct {
  271. UsageUnit string `json:"usageUnit"`
  272. UsageUnitDescription string `json:"usageUnitDescription"`
  273. BaseUnit string `json:"baseUnit"`
  274. BaseUnitConversionFactor int64 `json:"-"`
  275. DisplayQuantity int `json:"displayQuantity"`
  276. TieredRates []*TieredRates `json:"tieredRates"`
  277. }
  278. // TieredRates contain data about variable pricing.
  279. type TieredRates struct {
  280. StartUsageAmount int `json:"startUsageAmount"`
  281. UnitPrice *UnitPriceInfo `json:"unitPrice"`
  282. }
  283. // UnitPriceInfo contains data about the actual price being charged.
  284. type UnitPriceInfo struct {
  285. CurrencyCode string `json:"currencyCode"`
  286. Units string `json:"units"`
  287. Nanos float64 `json:"nanos"`
  288. }
  289. // GCPResourceInfo contains metadata about the node.
  290. type GCPResourceInfo struct {
  291. ServiceDisplayName string `json:"serviceDisplayName"`
  292. ResourceFamily string `json:"resourceFamily"`
  293. ResourceGroup string `json:"resourceGroup"`
  294. UsageType string `json:"usageType"`
  295. }
  296. func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[string]PVKey) (map[string]*GCPPricing, string, error) {
  297. gcpPricingList := make(map[string]*GCPPricing)
  298. var nextPageToken string
  299. dec := json.NewDecoder(r)
  300. for {
  301. t, err := dec.Token()
  302. if err == io.EOF {
  303. break
  304. }
  305. if t == "skus" {
  306. _, err := dec.Token() // consumes [
  307. if err != nil {
  308. return nil, "", err
  309. }
  310. for dec.More() {
  311. product := &GCPPricing{}
  312. err := dec.Decode(&product)
  313. if err != nil {
  314. return nil, "", err
  315. }
  316. usageType := strings.ToLower(product.Category.UsageType)
  317. instanceType := strings.ToLower(product.Category.ResourceGroup)
  318. if instanceType == "ssd" {
  319. lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
  320. var nanos float64
  321. if len(product.PricingInfo) > 0 {
  322. nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
  323. } else {
  324. continue
  325. }
  326. hourlyPrice := (nanos * math.Pow10(-9)) / 730
  327. for _, sr := range product.ServiceRegions {
  328. region := sr
  329. candidateKey := region + "," + "ssd"
  330. if _, ok := pvKeys[candidateKey]; ok {
  331. product.PV = &PV{
  332. Cost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  333. }
  334. gcpPricingList[candidateKey] = product
  335. continue
  336. }
  337. }
  338. continue
  339. } else if instanceType == "pdstandard" {
  340. lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
  341. var nanos float64
  342. if len(product.PricingInfo) > 0 {
  343. nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
  344. } else {
  345. continue
  346. }
  347. hourlyPrice := (nanos * math.Pow10(-9)) / 730
  348. for _, sr := range product.ServiceRegions {
  349. region := sr
  350. candidateKey := region + "," + "pdstandard"
  351. if _, ok := pvKeys[candidateKey]; ok {
  352. product.PV = &PV{
  353. Cost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  354. }
  355. gcpPricingList[candidateKey] = product
  356. continue
  357. }
  358. }
  359. continue
  360. }
  361. if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "CUSTOM") {
  362. instanceType = "custom"
  363. }
  364. var partialCPU float64
  365. if strings.ToLower(instanceType) == "f1micro" {
  366. partialCPU = 0.2
  367. } else if strings.ToLower(instanceType) == "g1small" {
  368. partialCPU = 0.5
  369. }
  370. var gpuType string
  371. provIdRx := regexp.MustCompile("(Nvidia Tesla [^ ]+) ")
  372. for matchnum, group := range provIdRx.FindStringSubmatch(product.Description) {
  373. if matchnum == 1 {
  374. gpuType = strings.ToLower(strings.Join(strings.Split(group, " "), "-"))
  375. klog.V(4).Info("GPU type found: " + gpuType)
  376. }
  377. }
  378. for _, sr := range product.ServiceRegions {
  379. region := sr
  380. candidateKey := region + "," + instanceType + "," + usageType
  381. candidateKeyGPU := candidateKey + ",gpu"
  382. if gpuType != "" {
  383. lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
  384. var nanos float64
  385. if len(product.PricingInfo) > 0 {
  386. nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
  387. } else {
  388. continue
  389. }
  390. hourlyPrice := nanos * math.Pow10(-9)
  391. for k, key := range inputKeys {
  392. if key.GPUType() == gpuType+","+usageType {
  393. if region == strings.Split(k, ",")[0] {
  394. klog.V(3).Infof("Matched GPU to node in region \"%s\"", region)
  395. matchedKey := key.Features()
  396. if pl, ok := gcpPricingList[matchedKey]; ok {
  397. pl.Node.GPUName = gpuType
  398. pl.Node.GPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  399. pl.Node.GPU = "1"
  400. } else {
  401. product.Node = &Node{
  402. GPUName: gpuType,
  403. GPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  404. GPU: "1",
  405. }
  406. gcpPricingList[matchedKey] = product
  407. }
  408. klog.V(3).Infof("Added data for " + matchedKey)
  409. }
  410. }
  411. }
  412. } else {
  413. _, ok := inputKeys[candidateKey]
  414. _, ok2 := inputKeys[candidateKeyGPU]
  415. if ok || ok2 {
  416. lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
  417. var nanos float64
  418. if len(product.PricingInfo) > 0 {
  419. nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
  420. } else {
  421. continue
  422. }
  423. hourlyPrice := nanos * math.Pow10(-9)
  424. if hourlyPrice == 0 {
  425. continue
  426. } else if strings.Contains(strings.ToUpper(product.Description), "RAM") {
  427. if instanceType == "custom" {
  428. klog.V(4).Infof("RAM custom sku is: " + product.Name)
  429. }
  430. if _, ok := gcpPricingList[candidateKey]; ok {
  431. gcpPricingList[candidateKey].Node.RAMCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  432. } else {
  433. product = &GCPPricing{}
  434. product.Node = &Node{
  435. RAMCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  436. }
  437. if partialCPU != 0 {
  438. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  439. }
  440. product.Node.UsageType = usageType
  441. gcpPricingList[candidateKey] = product
  442. }
  443. if _, ok := gcpPricingList[candidateKeyGPU]; ok {
  444. klog.V(1).Infof("Adding RAM %f for %s", hourlyPrice, candidateKeyGPU)
  445. gcpPricingList[candidateKeyGPU].Node.RAMCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  446. } else {
  447. klog.V(1).Infof("Adding RAM %f for %s", hourlyPrice, candidateKeyGPU)
  448. product = &GCPPricing{}
  449. product.Node = &Node{
  450. RAMCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  451. }
  452. if partialCPU != 0 {
  453. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  454. }
  455. product.Node.UsageType = usageType
  456. gcpPricingList[candidateKeyGPU] = product
  457. }
  458. break
  459. } else {
  460. if _, ok := gcpPricingList[candidateKey]; ok {
  461. gcpPricingList[candidateKey].Node.VCPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  462. } else {
  463. product = &GCPPricing{}
  464. product.Node = &Node{
  465. VCPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  466. }
  467. if partialCPU != 0 {
  468. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  469. }
  470. product.Node.UsageType = usageType
  471. gcpPricingList[candidateKey] = product
  472. }
  473. if _, ok := gcpPricingList[candidateKeyGPU]; ok {
  474. gcpPricingList[candidateKeyGPU].Node.VCPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  475. } else {
  476. product = &GCPPricing{}
  477. product.Node = &Node{
  478. VCPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  479. }
  480. if partialCPU != 0 {
  481. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  482. }
  483. product.Node.UsageType = usageType
  484. gcpPricingList[candidateKeyGPU] = product
  485. }
  486. break
  487. }
  488. }
  489. }
  490. }
  491. }
  492. }
  493. if t == "nextPageToken" {
  494. pageToken, err := dec.Token()
  495. if err != nil {
  496. klog.V(2).Infof("Error parsing nextpage token: " + err.Error())
  497. return nil, "", err
  498. }
  499. if pageToken.(string) != "" {
  500. nextPageToken = pageToken.(string)
  501. } else {
  502. nextPageToken = "done"
  503. }
  504. }
  505. }
  506. return gcpPricingList, nextPageToken, nil
  507. }
  508. func (gcp *GCP) parsePages(inputKeys map[string]Key, pvKeys map[string]PVKey) (map[string]*GCPPricing, error) {
  509. var pages []map[string]*GCPPricing
  510. url := "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + gcp.APIKey
  511. klog.V(2).Infof("Fetch GCP Billing Data from URL: %s", url)
  512. var parsePagesHelper func(string) error
  513. parsePagesHelper = func(pageToken string) error {
  514. if pageToken == "done" {
  515. return nil
  516. } else if pageToken != "" {
  517. url = url + "&pageToken=" + pageToken
  518. }
  519. resp, err := http.Get(url)
  520. if err != nil {
  521. return err
  522. }
  523. page, token, err := gcp.parsePage(resp.Body, inputKeys, pvKeys)
  524. if err != nil {
  525. return err
  526. }
  527. pages = append(pages, page)
  528. return parsePagesHelper(token)
  529. }
  530. err := parsePagesHelper("")
  531. if err != nil {
  532. return nil, err
  533. }
  534. returnPages := make(map[string]*GCPPricing)
  535. for _, page := range pages {
  536. klog.V(1).Infof("Page: %s : %+v", page)
  537. for k, v := range page {
  538. klog.V(1).Infof("Unmerged Page: %s : %+v", k, v)
  539. }
  540. }
  541. for _, page := range pages {
  542. for k, v := range page {
  543. if val, ok := returnPages[k]; ok { //keys may need to be merged
  544. if val.Node != nil {
  545. if val.Node.VCPUCost == "" {
  546. val.Node.VCPUCost = v.Node.VCPUCost
  547. }
  548. if val.Node.RAMCost == "" {
  549. val.Node.RAMCost = v.Node.RAMCost
  550. }
  551. if val.Node.GPUCost == "" {
  552. val.Node.GPUCost = v.Node.GPUCost
  553. }
  554. }
  555. if val.PV != nil {
  556. if val.PV.Cost == "" {
  557. val.PV.Cost = v.PV.Cost
  558. }
  559. }
  560. } else {
  561. returnPages[k] = v
  562. }
  563. }
  564. }
  565. klog.V(1).Infof("ALL PAGES: %+v", returnPages)
  566. for k, v := range returnPages {
  567. klog.V(1).Infof("Returned Page: %s : %+v", k, v.Node)
  568. }
  569. return returnPages, err
  570. }
  571. // 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.
  572. func (gcp *GCP) DownloadPricingData() error {
  573. gcp.DownloadPricingDataLock.Lock()
  574. defer gcp.DownloadPricingDataLock.Unlock()
  575. c, err := GetDefaultPricingData("gcp.json")
  576. if err != nil {
  577. klog.V(2).Infof("Error downloading default pricing data: %s", err.Error())
  578. return err
  579. }
  580. gcp.BaseCPUPrice = c.CPU
  581. gcp.ProjectID = c.ProjectID
  582. gcp.BillingDataDataset = c.BillingDataDataset
  583. nodeList, err := gcp.Clientset.CoreV1().Nodes().List(metav1.ListOptions{})
  584. if err != nil {
  585. return err
  586. }
  587. inputkeys := make(map[string]Key)
  588. for _, n := range nodeList.Items {
  589. labels := n.GetObjectMeta().GetLabels()
  590. key := gcp.GetKey(labels)
  591. inputkeys[key.Features()] = key
  592. }
  593. pvList, err := gcp.Clientset.CoreV1().PersistentVolumes().List(metav1.ListOptions{})
  594. if err != nil {
  595. return err
  596. }
  597. storageClasses, err := gcp.Clientset.StorageV1().StorageClasses().List(metav1.ListOptions{})
  598. storageClassMap := make(map[string]map[string]string)
  599. for _, storageClass := range storageClasses.Items {
  600. params := storageClass.Parameters
  601. storageClassMap[storageClass.ObjectMeta.Name] = params
  602. }
  603. pvkeys := make(map[string]PVKey)
  604. for _, pv := range pvList.Items {
  605. params, ok := storageClassMap[pv.Spec.StorageClassName]
  606. if !ok {
  607. klog.Infof("Unable to find params for storageClassName %s", pv.Name)
  608. continue
  609. }
  610. key := gcp.GetPVKey(&pv, params)
  611. pvkeys[key.Features()] = key
  612. }
  613. pages, err := gcp.parsePages(inputkeys, pvkeys)
  614. if err != nil {
  615. return err
  616. }
  617. gcp.Pricing = pages
  618. return nil
  619. }
  620. func (gcp *GCP) PVPricing(pvk PVKey) (*PV, error) {
  621. gcp.DownloadPricingDataLock.RLock()
  622. defer gcp.DownloadPricingDataLock.RUnlock()
  623. pricing, ok := gcp.Pricing[pvk.Features()]
  624. if !ok {
  625. klog.V(2).Infof("Persistent Volume pricing not found for %s", pvk)
  626. return &PV{}, nil
  627. }
  628. return pricing.PV, nil
  629. }
  630. type pvKey struct {
  631. Labels map[string]string
  632. StorageClass string
  633. StorageClassParameters map[string]string
  634. }
  635. func (gcp *GCP) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string) PVKey {
  636. return &pvKey{
  637. Labels: pv.Labels,
  638. StorageClass: pv.Spec.StorageClassName,
  639. StorageClassParameters: parameters,
  640. }
  641. }
  642. func (key *pvKey) Features() string {
  643. storageClass := key.StorageClassParameters["type"]
  644. if storageClass == "pd-ssd" {
  645. storageClass = "ssd"
  646. } else if storageClass == "pd-standard" {
  647. storageClass = "pdstandard"
  648. }
  649. return key.Labels[v1.LabelZoneRegion] + "," + storageClass
  650. }
  651. type gcpKey struct {
  652. Labels map[string]string
  653. }
  654. func (gcp *GCP) GetKey(labels map[string]string) Key {
  655. return &gcpKey{
  656. Labels: labels,
  657. }
  658. }
  659. func (gcp *gcpKey) ID() string {
  660. return ""
  661. }
  662. func (gcp *gcpKey) GPUType() string {
  663. if t, ok := gcp.Labels[GKE_GPU_TAG]; ok {
  664. var usageType string
  665. if t, ok := gcp.Labels["cloud.google.com/gke-preemptible"]; ok && t == "true" {
  666. usageType = "preemptible"
  667. } else {
  668. usageType = "ondemand"
  669. }
  670. klog.V(4).Infof("GPU of type: \"%s\" found", t)
  671. return t + "," + usageType
  672. }
  673. return ""
  674. }
  675. // GetKey maps node labels to information needed to retrieve pricing data
  676. func (gcp *gcpKey) Features() string {
  677. instanceType := strings.ToLower(strings.Join(strings.Split(gcp.Labels[v1.LabelInstanceType], "-")[:2], ""))
  678. if instanceType == "n1highmem" || instanceType == "n1highcpu" {
  679. instanceType = "n1standard" // These are priced the same. TODO: support n1ultrahighmem
  680. } else if strings.HasPrefix(instanceType, "custom") {
  681. instanceType = "custom" // The suffix of custom does not matter
  682. }
  683. region := strings.ToLower(gcp.Labels[v1.LabelZoneRegion])
  684. var usageType string
  685. if t, ok := gcp.Labels["cloud.google.com/gke-preemptible"]; ok && t == "true" {
  686. usageType = "preemptible"
  687. } else {
  688. usageType = "ondemand"
  689. }
  690. if _, ok := gcp.Labels[GKE_GPU_TAG]; ok {
  691. return region + "," + instanceType + "," + usageType + "," + "gpu"
  692. }
  693. return region + "," + instanceType + "," + usageType
  694. }
  695. // AllNodePricing returns the GCP pricing objects stored
  696. func (gcp *GCP) AllNodePricing() (interface{}, error) {
  697. gcp.DownloadPricingDataLock.RLock()
  698. defer gcp.DownloadPricingDataLock.RUnlock()
  699. return gcp.Pricing, nil
  700. }
  701. // NodePricing returns GCP pricing data for a single node
  702. func (gcp *GCP) NodePricing(key Key) (*Node, error) {
  703. gcp.DownloadPricingDataLock.RLock()
  704. defer gcp.DownloadPricingDataLock.RUnlock()
  705. if n, ok := gcp.Pricing[key.Features()]; ok {
  706. klog.V(4).Infof("Returning pricing for node %s: %+v from SKU %s", key, n.Node, n.Name)
  707. n.Node.BaseCPUPrice = gcp.BaseCPUPrice
  708. return n.Node, nil
  709. }
  710. klog.V(1).Infof("Warning: no pricing data found for %s: %s", key.Features(), key)
  711. return nil, fmt.Errorf("Warning: no pricing data found for %s", key)
  712. }