gcpprovider.go 23 KB

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