gcpprovider.go 22 KB

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