gcpprovider.go 21 KB

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