gcpprovider.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092
  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. "time"
  17. "k8s.io/klog"
  18. "cloud.google.com/go/bigquery"
  19. "cloud.google.com/go/compute/metadata"
  20. "github.com/kubecost/cost-model/clustercache"
  21. "golang.org/x/oauth2"
  22. "golang.org/x/oauth2/google"
  23. compute "google.golang.org/api/compute/v1"
  24. "google.golang.org/api/iterator"
  25. v1 "k8s.io/api/core/v1"
  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 clustercache.ClusterCache
  41. APIKey string
  42. BaseCPUPrice string
  43. ProjectID string
  44. BillingDataDataset string
  45. DownloadPricingDataLock sync.RWMutex
  46. ReservedInstances []*GCPReservedInstance
  47. Config *ProviderConfig
  48. *CustomProvider
  49. }
  50. type gcpAllocation struct {
  51. Aggregator bigquery.NullString
  52. Environment bigquery.NullString
  53. Service string
  54. Cost float64
  55. }
  56. func gcpAllocationToOutOfClusterAllocation(gcpAlloc gcpAllocation) *OutOfClusterAllocation {
  57. var aggregator string
  58. if gcpAlloc.Aggregator.Valid {
  59. aggregator = gcpAlloc.Aggregator.StringVal
  60. }
  61. var environment string
  62. if gcpAlloc.Environment.Valid {
  63. environment = gcpAlloc.Environment.StringVal
  64. }
  65. return &OutOfClusterAllocation{
  66. Aggregator: aggregator,
  67. Environment: environment,
  68. Service: gcpAlloc.Service,
  69. Cost: gcpAlloc.Cost,
  70. }
  71. }
  72. func (gcp *GCP) GetLocalStorageQuery(offset string) (string, error) {
  73. localStorageCost := 0.04 // TODO: Set to the price for the appropriate storage class. It's not trivial to determine the local storage disk type
  74. return fmt.Sprintf(`sum(sum(container_fs_limit_bytes{device!="tmpfs", id="/"} %s) by (instance, cluster_id)) by (cluster_id) / 1024 / 1024 / 1024 * %f`, offset, localStorageCost), nil
  75. }
  76. func (gcp *GCP) GetConfig() (*CustomPricing, error) {
  77. c, err := gcp.Config.GetCustomPricingData()
  78. if err != nil {
  79. return nil, err
  80. }
  81. if c.Discount == "" {
  82. c.Discount = "30%"
  83. }
  84. if c.NegotiatedDiscount == "" {
  85. c.NegotiatedDiscount = "0%"
  86. }
  87. return c, nil
  88. }
  89. type BigQueryConfig struct {
  90. ProjectID string `json:"projectID"`
  91. BillingDataDataset string `json:"billingDataDataset"`
  92. Key map[string]string `json:"key"`
  93. }
  94. func (gcp *GCP) GetManagementPlatform() (string, error) {
  95. nodes := gcp.Clientset.GetAllNodes()
  96. if len(nodes) > 0 {
  97. n := nodes[0]
  98. version := n.Status.NodeInfo.KubeletVersion
  99. if strings.Contains(version, "gke") {
  100. return "gke", nil
  101. }
  102. }
  103. return "", nil
  104. }
  105. func (gcp *GCP) UpdateConfigFromConfigMap(a map[string]string) (*CustomPricing, error) {
  106. return gcp.Config.UpdateFromMap(a)
  107. }
  108. func (gcp *GCP) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error) {
  109. return gcp.Config.Update(func(c *CustomPricing) error {
  110. if updateType == BigqueryUpdateType {
  111. a := BigQueryConfig{}
  112. err := json.NewDecoder(r).Decode(&a)
  113. if err != nil {
  114. return err
  115. }
  116. c.ProjectID = a.ProjectID
  117. c.BillingDataDataset = a.BillingDataDataset
  118. j, err := json.Marshal(a.Key)
  119. if err != nil {
  120. return err
  121. }
  122. path := os.Getenv("CONFIG_PATH")
  123. if path == "" {
  124. path = "/models/"
  125. }
  126. keyPath := path + "key.json"
  127. err = ioutil.WriteFile(keyPath, j, 0644)
  128. if err != nil {
  129. return err
  130. }
  131. } else {
  132. a := make(map[string]interface{})
  133. err := json.NewDecoder(r).Decode(&a)
  134. if err != nil {
  135. return err
  136. }
  137. for k, v := range a {
  138. kUpper := strings.Title(k) // Just so we consistently supply / receive the same values, uppercase the first letter.
  139. vstr, ok := v.(string)
  140. if ok {
  141. err := SetCustomPricingField(c, kUpper, vstr)
  142. if err != nil {
  143. return err
  144. }
  145. } else {
  146. sci := v.(map[string]interface{})
  147. sc := make(map[string]string)
  148. for k, val := range sci {
  149. sc[k] = val.(string)
  150. }
  151. c.SharedCosts = sc //todo: support reflection/multiple map fields
  152. }
  153. }
  154. }
  155. remoteEnabled := os.Getenv(remoteEnabled)
  156. if remoteEnabled == "true" {
  157. err := UpdateClusterMeta(os.Getenv(clusterIDKey), c.ClusterName)
  158. if err != nil {
  159. return err
  160. }
  161. }
  162. return nil
  163. })
  164. }
  165. // ExternalAllocations represents tagged assets outside the scope of kubernetes.
  166. // "start" and "end" are dates of the format YYYY-MM-DD
  167. // "aggregator" is the tag used to determine how to allocate those assets, ie namespace, pod, etc.
  168. func (gcp *GCP) ExternalAllocations(start string, end string, aggregator string, filterType string, filterValue string) ([]*OutOfClusterAllocation, error) {
  169. c, err := gcp.Config.GetCustomPricingData()
  170. if err != nil {
  171. return nil, err
  172. }
  173. // start, end formatted like: "2019-04-20 00:00:00"
  174. queryString := fmt.Sprintf(`SELECT
  175. service,
  176. labels.key as aggregator,
  177. labels.value as environment,
  178. SUM(cost) as cost
  179. FROM (SELECT
  180. service.description as service,
  181. labels,
  182. cost
  183. FROM %s
  184. WHERE usage_start_time >= "%s" AND usage_start_time < "%s")
  185. LEFT JOIN UNNEST(labels) as labels
  186. ON labels.key = "%s"
  187. GROUP BY aggregator, environment, service;`, c.BillingDataDataset, start, end, aggregator) // For example, "billing_data.gcp_billing_export_v1_01AC9F_74CF1D_5565A2"
  188. klog.V(4).Infof("Querying \"%s\" with : %s", c.ProjectID, queryString)
  189. return gcp.QuerySQL(queryString)
  190. }
  191. // QuerySQL should query BigQuery for billing data for out of cluster costs.
  192. func (gcp *GCP) QuerySQL(query string) ([]*OutOfClusterAllocation, error) {
  193. c, err := gcp.Config.GetCustomPricingData()
  194. if err != nil {
  195. return nil, err
  196. }
  197. ctx := context.Background()
  198. client, err := bigquery.NewClient(ctx, c.ProjectID) // For example, "guestbook-227502"
  199. if err != nil {
  200. return nil, err
  201. }
  202. q := client.Query(query)
  203. it, err := q.Read(ctx)
  204. if err != nil {
  205. return nil, err
  206. }
  207. var allocations []*OutOfClusterAllocation
  208. for {
  209. var a gcpAllocation
  210. err := it.Next(&a)
  211. if err == iterator.Done {
  212. break
  213. }
  214. if err != nil {
  215. return nil, err
  216. }
  217. allocations = append(allocations, gcpAllocationToOutOfClusterAllocation(a))
  218. }
  219. return allocations, nil
  220. }
  221. // ClusterName returns the name of a GKE cluster, as provided by metadata.
  222. func (gcp *GCP) ClusterInfo() (map[string]string, error) {
  223. remote := os.Getenv(remoteEnabled)
  224. remoteEnabled := false
  225. if os.Getenv(remote) == "true" {
  226. remoteEnabled = true
  227. }
  228. metadataClient := metadata.NewClient(&http.Client{Transport: userAgentTransport{
  229. userAgent: "kubecost",
  230. base: http.DefaultTransport,
  231. }})
  232. attribute, err := metadataClient.InstanceAttributeValue("cluster-name")
  233. if err != nil {
  234. klog.Infof("Error loading metadata cluster-name: %s", err.Error())
  235. }
  236. c, err := gcp.GetConfig()
  237. if err != nil {
  238. klog.V(1).Infof("Error opening config: %s", err.Error())
  239. }
  240. if c.ClusterName != "" {
  241. attribute = c.ClusterName
  242. }
  243. m := make(map[string]string)
  244. m["name"] = attribute
  245. m["provider"] = "GCP"
  246. m["id"] = os.Getenv(clusterIDKey)
  247. m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
  248. return m, nil
  249. }
  250. // AddServiceKey adds the service key as required for GetDisks
  251. func (*GCP) AddServiceKey(formValues url.Values) error {
  252. key := formValues.Get("key")
  253. k := []byte(key)
  254. return ioutil.WriteFile("/var/configs/key.json", k, 0644)
  255. }
  256. // 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.
  257. func (*GCP) GetDisks() ([]byte, error) {
  258. // metadata API setup
  259. metadataClient := metadata.NewClient(&http.Client{Transport: userAgentTransport{
  260. userAgent: "kubecost",
  261. base: http.DefaultTransport,
  262. }})
  263. projID, err := metadataClient.ProjectID()
  264. if err != nil {
  265. return nil, err
  266. }
  267. client, err := google.DefaultClient(oauth2.NoContext,
  268. "https://www.googleapis.com/auth/compute.readonly")
  269. if err != nil {
  270. return nil, err
  271. }
  272. svc, err := compute.New(client)
  273. if err != nil {
  274. return nil, err
  275. }
  276. res, err := svc.Disks.AggregatedList(projID).Do()
  277. if err != nil {
  278. return nil, err
  279. }
  280. return json.Marshal(res)
  281. }
  282. // GCPPricing represents GCP pricing data for a SKU
  283. type GCPPricing struct {
  284. Name string `json:"name"`
  285. SKUID string `json:"skuId"`
  286. Description string `json:"description"`
  287. Category *GCPResourceInfo `json:"category"`
  288. ServiceRegions []string `json:"serviceRegions"`
  289. PricingInfo []*PricingInfo `json:"pricingInfo"`
  290. ServiceProviderName string `json:"serviceProviderName"`
  291. Node *Node `json:"node"`
  292. PV *PV `json:"pv"`
  293. }
  294. // PricingInfo contains metadata about a cost.
  295. type PricingInfo struct {
  296. Summary string `json:"summary"`
  297. PricingExpression *PricingExpression `json:"pricingExpression"`
  298. CurrencyConversionRate int `json:"currencyConversionRate"`
  299. EffectiveTime string `json:""`
  300. }
  301. // PricingExpression contains metadata about a cost.
  302. type PricingExpression struct {
  303. UsageUnit string `json:"usageUnit"`
  304. UsageUnitDescription string `json:"usageUnitDescription"`
  305. BaseUnit string `json:"baseUnit"`
  306. BaseUnitConversionFactor int64 `json:"-"`
  307. DisplayQuantity int `json:"displayQuantity"`
  308. TieredRates []*TieredRates `json:"tieredRates"`
  309. }
  310. // TieredRates contain data about variable pricing.
  311. type TieredRates struct {
  312. StartUsageAmount int `json:"startUsageAmount"`
  313. UnitPrice *UnitPriceInfo `json:"unitPrice"`
  314. }
  315. // UnitPriceInfo contains data about the actual price being charged.
  316. type UnitPriceInfo struct {
  317. CurrencyCode string `json:"currencyCode"`
  318. Units string `json:"units"`
  319. Nanos float64 `json:"nanos"`
  320. }
  321. // GCPResourceInfo contains metadata about the node.
  322. type GCPResourceInfo struct {
  323. ServiceDisplayName string `json:"serviceDisplayName"`
  324. ResourceFamily string `json:"resourceFamily"`
  325. ResourceGroup string `json:"resourceGroup"`
  326. UsageType string `json:"usageType"`
  327. }
  328. func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[string]PVKey) (map[string]*GCPPricing, string, error) {
  329. gcpPricingList := make(map[string]*GCPPricing)
  330. var nextPageToken string
  331. dec := json.NewDecoder(r)
  332. for {
  333. t, err := dec.Token()
  334. if err == io.EOF {
  335. break
  336. }
  337. if t == "skus" {
  338. _, err := dec.Token() // consumes [
  339. if err != nil {
  340. return nil, "", err
  341. }
  342. for dec.More() {
  343. product := &GCPPricing{}
  344. err := dec.Decode(&product)
  345. if err != nil {
  346. return nil, "", err
  347. }
  348. usageType := strings.ToLower(product.Category.UsageType)
  349. instanceType := strings.ToLower(product.Category.ResourceGroup)
  350. if instanceType == "ssd" && !strings.Contains(product.Description, "Regional") { // TODO: support regional
  351. lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
  352. var nanos float64
  353. if len(product.PricingInfo) > 0 {
  354. nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
  355. } else {
  356. continue
  357. }
  358. hourlyPrice := (nanos * math.Pow10(-9)) / 730
  359. for _, sr := range product.ServiceRegions {
  360. region := sr
  361. candidateKey := region + "," + "ssd"
  362. if _, ok := pvKeys[candidateKey]; ok {
  363. product.PV = &PV{
  364. Cost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  365. }
  366. gcpPricingList[candidateKey] = product
  367. continue
  368. }
  369. }
  370. continue
  371. } else if instanceType == "pdstandard" && !strings.Contains(product.Description, "Regional") { // TODO: support regional
  372. lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
  373. var nanos float64
  374. if len(product.PricingInfo) > 0 {
  375. nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
  376. } else {
  377. continue
  378. }
  379. hourlyPrice := (nanos * math.Pow10(-9)) / 730
  380. for _, sr := range product.ServiceRegions {
  381. region := sr
  382. candidateKey := region + "," + "pdstandard"
  383. if _, ok := pvKeys[candidateKey]; ok {
  384. product.PV = &PV{
  385. Cost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  386. }
  387. gcpPricingList[candidateKey] = product
  388. continue
  389. }
  390. }
  391. continue
  392. }
  393. if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "CUSTOM") {
  394. instanceType = "custom"
  395. }
  396. if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "N2") {
  397. instanceType = "n2standard"
  398. }
  399. /*
  400. var partialCPU float64
  401. if strings.ToLower(instanceType) == "f1micro" {
  402. partialCPU = 0.2
  403. } else if strings.ToLower(instanceType) == "g1small" {
  404. partialCPU = 0.5
  405. }
  406. */
  407. var gpuType string
  408. provIdRx := regexp.MustCompile("(Nvidia Tesla [^ ]+) ")
  409. for matchnum, group := range provIdRx.FindStringSubmatch(product.Description) {
  410. if matchnum == 1 {
  411. gpuType = strings.ToLower(strings.Join(strings.Split(group, " "), "-"))
  412. klog.V(4).Info("GPU type found: " + gpuType)
  413. }
  414. }
  415. for _, sr := range product.ServiceRegions {
  416. region := sr
  417. candidateKey := region + "," + instanceType + "," + usageType
  418. candidateKeyGPU := candidateKey + ",gpu"
  419. if gpuType != "" {
  420. lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
  421. var nanos float64
  422. if len(product.PricingInfo) > 0 {
  423. nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
  424. } else {
  425. continue
  426. }
  427. hourlyPrice := nanos * math.Pow10(-9)
  428. for k, key := range inputKeys {
  429. if key.GPUType() == gpuType+","+usageType {
  430. if region == strings.Split(k, ",")[0] {
  431. klog.V(3).Infof("Matched GPU to node in region \"%s\"", region)
  432. klog.V(4).Infof("PRODUCT DESCRIPTION: %s", product.Description)
  433. matchedKey := key.Features()
  434. if pl, ok := gcpPricingList[matchedKey]; ok {
  435. pl.Node.GPUName = gpuType
  436. pl.Node.GPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  437. pl.Node.GPU = "1"
  438. } else {
  439. product.Node = &Node{
  440. GPUName: gpuType,
  441. GPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  442. GPU: "1",
  443. }
  444. gcpPricingList[matchedKey] = product
  445. }
  446. klog.V(3).Infof("Added data for " + matchedKey)
  447. }
  448. }
  449. }
  450. } else {
  451. _, ok := inputKeys[candidateKey]
  452. _, ok2 := inputKeys[candidateKeyGPU]
  453. if ok || ok2 {
  454. lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
  455. var nanos float64
  456. if len(product.PricingInfo) > 0 {
  457. nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
  458. } else {
  459. continue
  460. }
  461. hourlyPrice := nanos * math.Pow10(-9)
  462. if hourlyPrice == 0 {
  463. continue
  464. } else if strings.Contains(strings.ToUpper(product.Description), "RAM") {
  465. if instanceType == "custom" {
  466. klog.V(4).Infof("RAM custom sku is: " + product.Name)
  467. }
  468. if _, ok := gcpPricingList[candidateKey]; ok {
  469. gcpPricingList[candidateKey].Node.RAMCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  470. } else {
  471. product = &GCPPricing{}
  472. product.Node = &Node{
  473. RAMCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  474. }
  475. /*
  476. if partialCPU != 0 {
  477. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  478. }
  479. */
  480. product.Node.UsageType = usageType
  481. gcpPricingList[candidateKey] = product
  482. }
  483. if _, ok := gcpPricingList[candidateKeyGPU]; ok {
  484. klog.V(1).Infof("Adding RAM %f for %s", hourlyPrice, candidateKeyGPU)
  485. gcpPricingList[candidateKeyGPU].Node.RAMCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  486. } else {
  487. klog.V(1).Infof("Adding RAM %f for %s", hourlyPrice, candidateKeyGPU)
  488. product = &GCPPricing{}
  489. product.Node = &Node{
  490. RAMCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  491. }
  492. /*
  493. if partialCPU != 0 {
  494. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  495. }
  496. */
  497. product.Node.UsageType = usageType
  498. gcpPricingList[candidateKeyGPU] = product
  499. }
  500. break
  501. } else {
  502. if _, ok := gcpPricingList[candidateKey]; ok {
  503. gcpPricingList[candidateKey].Node.VCPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  504. } else {
  505. product = &GCPPricing{}
  506. product.Node = &Node{
  507. VCPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  508. }
  509. /*
  510. if partialCPU != 0 {
  511. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  512. }
  513. */
  514. product.Node.UsageType = usageType
  515. gcpPricingList[candidateKey] = product
  516. }
  517. if _, ok := gcpPricingList[candidateKeyGPU]; ok {
  518. gcpPricingList[candidateKeyGPU].Node.VCPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  519. } else {
  520. product = &GCPPricing{}
  521. product.Node = &Node{
  522. VCPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  523. }
  524. /*
  525. if partialCPU != 0 {
  526. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  527. }
  528. */
  529. product.Node.UsageType = usageType
  530. gcpPricingList[candidateKeyGPU] = product
  531. }
  532. break
  533. }
  534. }
  535. }
  536. }
  537. }
  538. }
  539. if t == "nextPageToken" {
  540. pageToken, err := dec.Token()
  541. if err != nil {
  542. klog.V(2).Infof("Error parsing nextpage token: " + err.Error())
  543. return nil, "", err
  544. }
  545. if pageToken.(string) != "" {
  546. nextPageToken = pageToken.(string)
  547. } else {
  548. nextPageToken = "done"
  549. }
  550. }
  551. }
  552. return gcpPricingList, nextPageToken, nil
  553. }
  554. func (gcp *GCP) parsePages(inputKeys map[string]Key, pvKeys map[string]PVKey) (map[string]*GCPPricing, error) {
  555. var pages []map[string]*GCPPricing
  556. url := "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + gcp.APIKey
  557. klog.V(2).Infof("Fetch GCP Billing Data from URL: %s", url)
  558. var parsePagesHelper func(string) error
  559. parsePagesHelper = func(pageToken string) error {
  560. if pageToken == "done" {
  561. return nil
  562. } else if pageToken != "" {
  563. url = url + "&pageToken=" + pageToken
  564. }
  565. resp, err := http.Get(url)
  566. if err != nil {
  567. return err
  568. }
  569. page, token, err := gcp.parsePage(resp.Body, inputKeys, pvKeys)
  570. if err != nil {
  571. return err
  572. }
  573. pages = append(pages, page)
  574. return parsePagesHelper(token)
  575. }
  576. err := parsePagesHelper("")
  577. if err != nil {
  578. return nil, err
  579. }
  580. returnPages := make(map[string]*GCPPricing)
  581. for _, page := range pages {
  582. for k, v := range page {
  583. if val, ok := returnPages[k]; ok { //keys may need to be merged
  584. if val.Node != nil {
  585. if val.Node.VCPUCost == "" {
  586. val.Node.VCPUCost = v.Node.VCPUCost
  587. }
  588. if val.Node.RAMCost == "" {
  589. val.Node.RAMCost = v.Node.RAMCost
  590. }
  591. if val.Node.GPUCost == "" {
  592. val.Node.GPUCost = v.Node.GPUCost
  593. val.Node.GPU = v.Node.GPU
  594. val.Node.GPUName = v.Node.GPUName
  595. }
  596. }
  597. if val.PV != nil {
  598. if val.PV.Cost == "" {
  599. val.PV.Cost = v.PV.Cost
  600. }
  601. }
  602. } else {
  603. returnPages[k] = v
  604. }
  605. }
  606. }
  607. klog.V(1).Infof("ALL PAGES: %+v", returnPages)
  608. for k, v := range returnPages {
  609. klog.V(1).Infof("Returned Page: %s : %+v", k, v.Node)
  610. }
  611. return returnPages, err
  612. }
  613. // 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.
  614. func (gcp *GCP) DownloadPricingData() error {
  615. gcp.DownloadPricingDataLock.Lock()
  616. defer gcp.DownloadPricingDataLock.Unlock()
  617. c, err := gcp.Config.GetCustomPricingData()
  618. if err != nil {
  619. klog.V(2).Infof("Error downloading default pricing data: %s", err.Error())
  620. return err
  621. }
  622. gcp.BaseCPUPrice = c.CPU
  623. gcp.ProjectID = c.ProjectID
  624. gcp.BillingDataDataset = c.BillingDataDataset
  625. nodeList := gcp.Clientset.GetAllNodes()
  626. inputkeys := make(map[string]Key)
  627. for _, n := range nodeList {
  628. labels := n.GetObjectMeta().GetLabels()
  629. key := gcp.GetKey(labels)
  630. inputkeys[key.Features()] = key
  631. }
  632. pvList := gcp.Clientset.GetAllPersistentVolumes()
  633. storageClasses := gcp.Clientset.GetAllStorageClasses()
  634. storageClassMap := make(map[string]map[string]string)
  635. for _, storageClass := range storageClasses {
  636. params := storageClass.Parameters
  637. storageClassMap[storageClass.ObjectMeta.Name] = params
  638. if storageClass.GetAnnotations()["storageclass.kubernetes.io/is-default-class"] == "true" || storageClass.GetAnnotations()["storageclass.beta.kubernetes.io/is-default-class"] == "true" {
  639. storageClassMap["default"] = params
  640. storageClassMap[""] = params
  641. }
  642. }
  643. pvkeys := make(map[string]PVKey)
  644. for _, pv := range pvList {
  645. params, ok := storageClassMap[pv.Spec.StorageClassName]
  646. if !ok {
  647. klog.Infof("Unable to find params for storageClassName %s", pv.Name)
  648. continue
  649. }
  650. key := gcp.GetPVKey(pv, params)
  651. pvkeys[key.Features()] = key
  652. }
  653. reserved, err := gcp.getReservedInstances()
  654. if err != nil {
  655. klog.V(1).Infof("Failed to lookup reserved instance data: %s", err.Error())
  656. } else {
  657. klog.V(1).Infof("Found %d reserved instances", len(reserved))
  658. gcp.ReservedInstances = reserved
  659. for _, r := range reserved {
  660. klog.V(1).Infof("%s", r)
  661. }
  662. }
  663. pages, err := gcp.parsePages(inputkeys, pvkeys)
  664. if err != nil {
  665. return err
  666. }
  667. gcp.Pricing = pages
  668. return nil
  669. }
  670. func (gcp *GCP) PVPricing(pvk PVKey) (*PV, error) {
  671. gcp.DownloadPricingDataLock.RLock()
  672. defer gcp.DownloadPricingDataLock.RUnlock()
  673. pricing, ok := gcp.Pricing[pvk.Features()]
  674. if !ok {
  675. klog.V(4).Infof("Persistent Volume pricing not found for %s: %s", pvk.GetStorageClass(), pvk.Features())
  676. return &PV{}, nil
  677. }
  678. return pricing.PV, nil
  679. }
  680. // Stubbed NetworkPricing for GCP. Pull directly from gcp.json for now
  681. func (gcp *GCP) NetworkPricing() (*Network, error) {
  682. cpricing, err := gcp.Config.GetCustomPricingData()
  683. if err != nil {
  684. return nil, err
  685. }
  686. znec, err := strconv.ParseFloat(cpricing.ZoneNetworkEgress, 64)
  687. if err != nil {
  688. return nil, err
  689. }
  690. rnec, err := strconv.ParseFloat(cpricing.RegionNetworkEgress, 64)
  691. if err != nil {
  692. return nil, err
  693. }
  694. inec, err := strconv.ParseFloat(cpricing.InternetNetworkEgress, 64)
  695. if err != nil {
  696. return nil, err
  697. }
  698. return &Network{
  699. ZoneNetworkEgressCost: znec,
  700. RegionNetworkEgressCost: rnec,
  701. InternetNetworkEgressCost: inec,
  702. }, nil
  703. }
  704. const (
  705. GCPReservedInstanceResourceTypeRAM string = "MEMORY"
  706. GCPReservedInstanceResourceTypeCPU string = "VCPU"
  707. GCPReservedInstanceStatusActive string = "ACTIVE"
  708. GCPReservedInstancePlanOneYear string = "TWELVE_MONTH"
  709. GCPReservedInstancePlanThreeYear string = "THIRTY_SIX_MONTH"
  710. )
  711. type GCPReservedInstancePlan struct {
  712. Name string
  713. CPUCost float64
  714. RAMCost float64
  715. }
  716. type GCPReservedInstance struct {
  717. ReservedRAM int64
  718. ReservedCPU int64
  719. Plan *GCPReservedInstancePlan
  720. StartDate time.Time
  721. EndDate time.Time
  722. Region string
  723. }
  724. func (r *GCPReservedInstance) String() string {
  725. return fmt.Sprintf("[CPU: %d, RAM: %d, Region: %s, Start: %s, End: %s]", r.ReservedCPU, r.ReservedRAM, r.Region, r.StartDate.String(), r.EndDate.String())
  726. }
  727. type GCPReservedCounter struct {
  728. RemainingCPU int64
  729. RemainingRAM int64
  730. Instance *GCPReservedInstance
  731. }
  732. func newReservedCounter(instance *GCPReservedInstance) *GCPReservedCounter {
  733. return &GCPReservedCounter{
  734. RemainingCPU: instance.ReservedCPU,
  735. RemainingRAM: instance.ReservedRAM,
  736. Instance: instance,
  737. }
  738. }
  739. // Two available Reservation plans for GCP, 1-year and 3-year
  740. var gcpReservedInstancePlans map[string]*GCPReservedInstancePlan = map[string]*GCPReservedInstancePlan{
  741. GCPReservedInstancePlanOneYear: &GCPReservedInstancePlan{
  742. Name: GCPReservedInstancePlanOneYear,
  743. CPUCost: 0.019915,
  744. RAMCost: 0.002669,
  745. },
  746. GCPReservedInstancePlanThreeYear: &GCPReservedInstancePlan{
  747. Name: GCPReservedInstancePlanThreeYear,
  748. CPUCost: 0.014225,
  749. RAMCost: 0.001907,
  750. },
  751. }
  752. func (gcp *GCP) ApplyReservedInstancePricing(nodes map[string]*Node) {
  753. numReserved := len(gcp.ReservedInstances)
  754. // Early return if no reserved instance data loaded
  755. if numReserved == 0 {
  756. klog.V(4).Infof("[Reserved] No Reserved Instances")
  757. return
  758. }
  759. now := time.Now()
  760. counters := make(map[string][]*GCPReservedCounter)
  761. for _, r := range gcp.ReservedInstances {
  762. if now.Before(r.StartDate) || now.After(r.EndDate) {
  763. klog.V(1).Infof("[Reserved] Skipped Reserved Instance due to dates")
  764. continue
  765. }
  766. _, ok := counters[r.Region]
  767. counter := newReservedCounter(r)
  768. if !ok {
  769. counters[r.Region] = []*GCPReservedCounter{counter}
  770. } else {
  771. counters[r.Region] = append(counters[r.Region], counter)
  772. }
  773. }
  774. gcpNodes := make(map[string]*v1.Node)
  775. currentNodes := gcp.Clientset.GetAllNodes()
  776. // Create a node name -> node map
  777. for _, gcpNode := range currentNodes {
  778. gcpNodes[gcpNode.GetName()] = gcpNode
  779. }
  780. // go through all provider nodes using k8s nodes for region
  781. for nodeName, node := range nodes {
  782. // Reset reserved allocation to prevent double allocation
  783. node.Reserved = nil
  784. kNode, ok := gcpNodes[nodeName]
  785. if !ok {
  786. klog.V(4).Infof("[Reserved] Could not find K8s Node with name: %s", nodeName)
  787. continue
  788. }
  789. nodeRegion, ok := kNode.Labels[v1.LabelZoneRegion]
  790. if !ok {
  791. klog.V(4).Infof("[Reserved] Could not find node region")
  792. continue
  793. }
  794. reservedCounters, ok := counters[nodeRegion]
  795. if !ok {
  796. klog.V(4).Infof("[Reserved] Could not find counters for region: %s", nodeRegion)
  797. continue
  798. }
  799. node.Reserved = &ReservedInstanceData{
  800. ReservedCPU: 0,
  801. ReservedRAM: 0,
  802. }
  803. for _, reservedCounter := range reservedCounters {
  804. if reservedCounter.RemainingCPU != 0 {
  805. nodeCPU, _ := strconv.ParseInt(node.VCPU, 10, 64)
  806. nodeCPU -= node.Reserved.ReservedCPU
  807. node.Reserved.CPUCost = reservedCounter.Instance.Plan.CPUCost
  808. if reservedCounter.RemainingCPU >= nodeCPU {
  809. reservedCounter.RemainingCPU -= nodeCPU
  810. node.Reserved.ReservedCPU += nodeCPU
  811. } else {
  812. node.Reserved.ReservedCPU += reservedCounter.RemainingCPU
  813. reservedCounter.RemainingCPU = 0
  814. }
  815. }
  816. if reservedCounter.RemainingRAM != 0 {
  817. nodeRAMF, _ := strconv.ParseFloat(node.RAMBytes, 64)
  818. nodeRAM := int64(nodeRAMF)
  819. nodeRAM -= node.Reserved.ReservedRAM
  820. node.Reserved.RAMCost = reservedCounter.Instance.Plan.RAMCost
  821. if reservedCounter.RemainingRAM >= nodeRAM {
  822. reservedCounter.RemainingRAM -= nodeRAM
  823. node.Reserved.ReservedRAM += nodeRAM
  824. } else {
  825. node.Reserved.ReservedRAM += reservedCounter.RemainingRAM
  826. reservedCounter.RemainingRAM = 0
  827. }
  828. }
  829. }
  830. }
  831. }
  832. func (gcp *GCP) getReservedInstances() ([]*GCPReservedInstance, error) {
  833. var results []*GCPReservedInstance
  834. ctx := context.Background()
  835. computeService, err := compute.NewService(ctx)
  836. if err != nil {
  837. return nil, err
  838. }
  839. commitments, err := computeService.RegionCommitments.AggregatedList(gcp.ProjectID).Do()
  840. if err != nil {
  841. return nil, err
  842. }
  843. for regionKey, commitList := range commitments.Items {
  844. for _, commit := range commitList.Commitments {
  845. if commit.Status != GCPReservedInstanceStatusActive {
  846. continue
  847. }
  848. var vcpu int64 = 0
  849. var ram int64 = 0
  850. for _, resource := range commit.Resources {
  851. switch resource.Type {
  852. case GCPReservedInstanceResourceTypeRAM:
  853. ram = resource.Amount * 1024 * 1024
  854. case GCPReservedInstanceResourceTypeCPU:
  855. vcpu = resource.Amount
  856. default:
  857. klog.V(4).Infof("Failed to handle resource type: %s", resource.Type)
  858. }
  859. }
  860. var region string
  861. regionStr := strings.Split(regionKey, "/")
  862. if len(regionStr) == 2 {
  863. region = regionStr[1]
  864. }
  865. timeLayout := "2006-01-02T15:04:05Z07:00"
  866. startTime, err := time.Parse(timeLayout, commit.StartTimestamp)
  867. if err != nil {
  868. klog.V(1).Infof("Failed to parse start date: %s", commit.StartTimestamp)
  869. continue
  870. }
  871. endTime, err := time.Parse(timeLayout, commit.EndTimestamp)
  872. if err != nil {
  873. klog.V(1).Infof("Failed to parse end date: %s", commit.EndTimestamp)
  874. continue
  875. }
  876. // Look for a plan based on the name. Default to One Year if it fails
  877. plan, ok := gcpReservedInstancePlans[commit.Plan]
  878. if !ok {
  879. plan = gcpReservedInstancePlans[GCPReservedInstancePlanOneYear]
  880. }
  881. results = append(results, &GCPReservedInstance{
  882. Region: region,
  883. ReservedRAM: ram,
  884. ReservedCPU: vcpu,
  885. Plan: plan,
  886. StartDate: startTime,
  887. EndDate: endTime,
  888. })
  889. }
  890. }
  891. return results, nil
  892. }
  893. type pvKey struct {
  894. Labels map[string]string
  895. StorageClass string
  896. StorageClassParameters map[string]string
  897. }
  898. func (key *pvKey) GetStorageClass() string {
  899. return key.StorageClass
  900. }
  901. func (gcp *GCP) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string) PVKey {
  902. return &pvKey{
  903. Labels: pv.Labels,
  904. StorageClass: pv.Spec.StorageClassName,
  905. StorageClassParameters: parameters,
  906. }
  907. }
  908. func (key *pvKey) Features() string {
  909. // TODO: regional cluster pricing.
  910. storageClass := key.StorageClassParameters["type"]
  911. if storageClass == "pd-ssd" {
  912. storageClass = "ssd"
  913. } else if storageClass == "pd-standard" {
  914. storageClass = "pdstandard"
  915. }
  916. return key.Labels[v1.LabelZoneRegion] + "," + storageClass
  917. }
  918. type gcpKey struct {
  919. Labels map[string]string
  920. }
  921. func (gcp *GCP) GetKey(labels map[string]string) Key {
  922. return &gcpKey{
  923. Labels: labels,
  924. }
  925. }
  926. func (gcp *gcpKey) ID() string {
  927. return ""
  928. }
  929. func (gcp *gcpKey) GPUType() string {
  930. if t, ok := gcp.Labels[GKE_GPU_TAG]; ok {
  931. var usageType string
  932. if t, ok := gcp.Labels["cloud.google.com/gke-preemptible"]; ok && t == "true" {
  933. usageType = "preemptible"
  934. } else {
  935. usageType = "ondemand"
  936. }
  937. klog.V(4).Infof("GPU of type: \"%s\" found", t)
  938. return t + "," + usageType
  939. }
  940. return ""
  941. }
  942. // GetKey maps node labels to information needed to retrieve pricing data
  943. func (gcp *gcpKey) Features() string {
  944. instanceType := strings.ToLower(strings.Join(strings.Split(gcp.Labels[v1.LabelInstanceType], "-")[:2], ""))
  945. if instanceType == "n1highmem" || instanceType == "n1highcpu" {
  946. instanceType = "n1standard" // These are priced the same. TODO: support n1ultrahighmem
  947. } else if strings.HasPrefix(instanceType, "custom") {
  948. instanceType = "custom" // The suffix of custom does not matter
  949. }
  950. region := strings.ToLower(gcp.Labels[v1.LabelZoneRegion])
  951. var usageType string
  952. if t, ok := gcp.Labels["cloud.google.com/gke-preemptible"]; ok && t == "true" {
  953. usageType = "preemptible"
  954. } else {
  955. usageType = "ondemand"
  956. }
  957. if _, ok := gcp.Labels[GKE_GPU_TAG]; ok {
  958. return region + "," + instanceType + "," + usageType + "," + "gpu"
  959. }
  960. return region + "," + instanceType + "," + usageType
  961. }
  962. // AllNodePricing returns the GCP pricing objects stored
  963. func (gcp *GCP) AllNodePricing() (interface{}, error) {
  964. gcp.DownloadPricingDataLock.RLock()
  965. defer gcp.DownloadPricingDataLock.RUnlock()
  966. return gcp.Pricing, nil
  967. }
  968. // NodePricing returns GCP pricing data for a single node
  969. func (gcp *GCP) NodePricing(key Key) (*Node, error) {
  970. gcp.DownloadPricingDataLock.RLock()
  971. defer gcp.DownloadPricingDataLock.RUnlock()
  972. if n, ok := gcp.Pricing[key.Features()]; ok {
  973. klog.V(4).Infof("Returning pricing for node %s: %+v from SKU %s", key, n.Node, n.Name)
  974. n.Node.BaseCPUPrice = gcp.BaseCPUPrice
  975. return n.Node, nil
  976. }
  977. klog.V(1).Infof("[Warning] no pricing data found for %s: %s", key.Features(), key)
  978. return nil, fmt.Errorf("Warning: no pricing data found for %s", key)
  979. }