gcpprovider.go 32 KB

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