gcpprovider.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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. "strconv"
  12. "strings"
  13. "k8s.io/klog"
  14. "cloud.google.com/go/bigquery"
  15. "cloud.google.com/go/compute/metadata"
  16. "golang.org/x/oauth2"
  17. "golang.org/x/oauth2/google"
  18. compute "google.golang.org/api/compute/v1"
  19. v1 "k8s.io/api/core/v1"
  20. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  21. "k8s.io/client-go/kubernetes"
  22. )
  23. type userAgentTransport struct {
  24. userAgent string
  25. base http.RoundTripper
  26. }
  27. func (t userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  28. req.Header.Set("User-Agent", t.userAgent)
  29. return t.base.RoundTrip(req)
  30. }
  31. // GCP implements a provider interface for GCP
  32. type GCP struct {
  33. Pricing map[string]*GCPPricing
  34. Clientset *kubernetes.Clientset
  35. APIKey string
  36. BaseCPUPrice string
  37. }
  38. // QuerySQL should query BigQuery for billing data for out of cluster costs. TODO: Implement.
  39. func (*GCP) QuerySQL(query string) ([]byte, error) {
  40. ctx := context.Background()
  41. client, err := bigquery.NewClient(ctx, "guestbook-219823")
  42. if err != nil {
  43. return nil, err
  44. }
  45. q := client.Query()
  46. it, err := q.Read(ctx)
  47. if err != nil {
  48. return nil, err
  49. }
  50. }
  51. // ClusterName returns the name of a GKE cluster, as provided by metadata.
  52. func (*GCP) ClusterName() ([]byte, error) {
  53. metadataClient := metadata.NewClient(&http.Client{Transport: userAgentTransport{
  54. userAgent: "kubecost",
  55. base: http.DefaultTransport,
  56. }})
  57. attribute, err := metadataClient.InstanceAttributeValue("cluster-name")
  58. if err != nil {
  59. return nil, err
  60. }
  61. m := make(map[string]string)
  62. m["name"] = attribute
  63. m["provider"] = "GCP"
  64. return json.Marshal(m)
  65. }
  66. // AddServiceKey adds the service key as required for GetDisks
  67. func (*GCP) AddServiceKey(formValues url.Values) error {
  68. key := formValues.Get("key")
  69. k := []byte(key)
  70. return ioutil.WriteFile("/var/configs/key.json", k, 0644)
  71. }
  72. // 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.
  73. func (*GCP) GetDisks() ([]byte, error) {
  74. // metadata API setup
  75. metadataClient := metadata.NewClient(&http.Client{Transport: userAgentTransport{
  76. userAgent: "kubecost",
  77. base: http.DefaultTransport,
  78. }})
  79. projID, err := metadataClient.ProjectID()
  80. if err != nil {
  81. return nil, err
  82. }
  83. client, err := google.DefaultClient(oauth2.NoContext,
  84. "https://www.googleapis.com/auth/compute.readonly")
  85. if err != nil {
  86. return nil, err
  87. }
  88. svc, err := compute.New(client)
  89. if err != nil {
  90. return nil, err
  91. }
  92. res, err := svc.Disks.AggregatedList(projID).Do()
  93. if err != nil {
  94. return nil, err
  95. }
  96. return json.Marshal(res)
  97. }
  98. // GCPPricing represents GCP pricing data for a SKU
  99. type GCPPricing struct {
  100. Name string `json:"name"`
  101. SKUID string `json:"skuId"`
  102. Description string `json:"description"`
  103. Category *GCPResourceInfo `json:"category"`
  104. ServiceRegions []string `json:"serviceRegions"`
  105. PricingInfo []*PricingInfo `json:"pricingInfo"`
  106. ServiceProviderName string `json:"serviceProviderName"`
  107. Node *Node `json:"node"`
  108. }
  109. // PricingInfo contains metadata about a cost.
  110. type PricingInfo struct {
  111. Summary string `json:"summary"`
  112. PricingExpression *PricingExpression `json:"pricingExpression"`
  113. CurrencyConversionRate int `json:"currencyConversionRate"`
  114. EffectiveTime string `json:""`
  115. }
  116. // PricingExpression contains metadata about a cost.
  117. type PricingExpression struct {
  118. UsageUnit string `json:"usageUnit"`
  119. UsageUnitDescription string `json:"usageUnitDescription"`
  120. BaseUnit string `json:"baseUnit"`
  121. BaseUnitConversionFactor int64 `json:"-"`
  122. DisplayQuantity int `json:"displayQuantity"`
  123. TieredRates []*TieredRates `json:"tieredRates"`
  124. }
  125. // TieredRates contain data about variable pricing.
  126. type TieredRates struct {
  127. StartUsageAmount int `json:"startUsageAmount"`
  128. UnitPrice *UnitPriceInfo `json:"unitPrice"`
  129. }
  130. // UnitPriceInfo contains data about the actual price being charged.
  131. type UnitPriceInfo struct {
  132. CurrencyCode string `json:"currencyCode"`
  133. Units string `json:"units"`
  134. Nanos float64 `json:"nanos"`
  135. }
  136. // GCPResourceInfo contains metadata about the node.
  137. type GCPResourceInfo struct {
  138. ServiceDisplayName string `json:"serviceDisplayName"`
  139. ResourceFamily string `json:"resourceFamily"`
  140. ResourceGroup string `json:"resourceGroup"`
  141. UsageType string `json:"usageType"`
  142. }
  143. func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]bool) (map[string]*GCPPricing, string) {
  144. gcpPricingList := make(map[string]*GCPPricing)
  145. var nextPageToken string
  146. dec := json.NewDecoder(r)
  147. for {
  148. t, err := dec.Token()
  149. if err == io.EOF {
  150. break
  151. }
  152. if t == "skus" {
  153. dec.Token() // [
  154. for dec.More() {
  155. product := &GCPPricing{}
  156. err := dec.Decode(&product)
  157. if err != nil {
  158. fmt.Printf("Error: " + err.Error())
  159. break
  160. }
  161. usageType := strings.ToLower(product.Category.UsageType)
  162. instanceType := strings.ToLower(product.Category.ResourceGroup)
  163. if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "CUSTOM") {
  164. instanceType = "custom"
  165. }
  166. var partialCPU float64
  167. if strings.ToLower(instanceType) == "f1micro" {
  168. partialCPU = 0.2
  169. } else if strings.ToLower(instanceType) == "g1small" {
  170. partialCPU = 0.5
  171. }
  172. for _, sr := range product.ServiceRegions {
  173. region := sr
  174. candidateKey := region + "," + instanceType + "," + usageType
  175. if _, ok := inputKeys[candidateKey]; ok {
  176. lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
  177. var nanos float64
  178. if len(product.PricingInfo) > 0 {
  179. nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
  180. } else {
  181. continue
  182. }
  183. hourlyPrice := nanos * math.Pow10(-9)
  184. if hourlyPrice == 0 {
  185. continue
  186. } else if strings.Contains(strings.ToUpper(product.Description), "RAM") {
  187. if instanceType == "custom" {
  188. klog.V(2).Infof("RAM custom sku is: " + product.Name)
  189. }
  190. if _, ok := gcpPricingList[candidateKey]; ok {
  191. gcpPricingList[candidateKey].Node.RAMCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  192. } else {
  193. product.Node = &Node{
  194. RAMCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  195. }
  196. if partialCPU != 0 {
  197. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  198. }
  199. product.Node.UsageType = usageType
  200. gcpPricingList[candidateKey] = product
  201. }
  202. break
  203. } else {
  204. if _, ok := gcpPricingList[candidateKey]; ok {
  205. gcpPricingList[candidateKey].Node.VCPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
  206. } else {
  207. product.Node = &Node{
  208. VCPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
  209. }
  210. if partialCPU != 0 {
  211. product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
  212. }
  213. product.Node.UsageType = usageType
  214. gcpPricingList[candidateKey] = product
  215. }
  216. break
  217. }
  218. }
  219. }
  220. }
  221. }
  222. if t == "nextPageToken" {
  223. pageToken, err := dec.Token()
  224. if err != nil {
  225. klog.V(2).Infof("Error parsing nextpage token: " + err.Error())
  226. break
  227. }
  228. if pageToken.(string) != "" {
  229. nextPageToken = pageToken.(string)
  230. } else {
  231. nextPageToken = "done"
  232. }
  233. }
  234. }
  235. return gcpPricingList, nextPageToken
  236. }
  237. func (gcp *GCP) parsePages(inputKeys map[string]bool) (map[string]*GCPPricing, error) {
  238. var pages []map[string]*GCPPricing
  239. url := "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + gcp.APIKey //AIzaSyDXQPG_MHUEy9neR7stolq6l0ujXmjJlvk
  240. klog.V(2).Infof("URL: %s", url)
  241. var parsePagesHelper func(string) error
  242. parsePagesHelper = func(pageToken string) error {
  243. if pageToken == "done" {
  244. return nil
  245. } else if pageToken != "" {
  246. url = url + "&pageToken=" + pageToken
  247. }
  248. resp, err := http.Get(url)
  249. if err != nil {
  250. return err
  251. }
  252. page, token := gcp.parsePage(resp.Body, inputKeys)
  253. pages = append(pages, page)
  254. return parsePagesHelper(token)
  255. }
  256. err := parsePagesHelper("")
  257. returnPages := make(map[string]*GCPPricing)
  258. for _, page := range pages {
  259. for k, v := range page {
  260. if val, ok := returnPages[k]; ok { //keys may need to be merged
  261. if val.Node.RAMCost != "" && val.Node.VCPUCost == "" {
  262. val.Node.VCPUCost = v.Node.VCPUCost
  263. } else if val.Node.VCPUCost != "" && val.Node.RAMCost == "" {
  264. val.Node.RAMCost = v.Node.RAMCost
  265. } else {
  266. returnPages[k] = v
  267. }
  268. } else {
  269. returnPages[k] = v
  270. }
  271. }
  272. }
  273. return returnPages, err
  274. }
  275. // 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.
  276. func (gcp *GCP) DownloadPricingData() error {
  277. nodeList, err := gcp.Clientset.CoreV1().Nodes().List(metav1.ListOptions{})
  278. if err != nil {
  279. return err
  280. }
  281. inputkeys := make(map[string]bool)
  282. for _, n := range nodeList.Items {
  283. labels := n.GetObjectMeta().GetLabels()
  284. key := gcp.GetKey(labels)
  285. inputkeys[key.Features()] = true
  286. }
  287. pages, err := gcp.parsePages(inputkeys)
  288. if err != nil {
  289. return err
  290. }
  291. gcp.Pricing = pages
  292. c, err := GetDefaultPricingData("default.json")
  293. if err != nil {
  294. klog.V(2).Infof("Error downloading default pricing data: %s", err.Error())
  295. }
  296. gcp.BaseCPUPrice = c.CPU
  297. return nil
  298. }
  299. type gcpKey struct {
  300. Labels map[string]string
  301. }
  302. func (gcp *GCP) GetKey(labels map[string]string) Key {
  303. return &gcpKey{
  304. Labels: labels,
  305. }
  306. }
  307. func (gcp *gcpKey) ID() string {
  308. return ""
  309. }
  310. // GetKey maps node labels to information needed to retrieve pricing data
  311. func (gcp *gcpKey) Features() string {
  312. instanceType := strings.ToLower(strings.Join(strings.Split(gcp.Labels[v1.LabelInstanceType], "-")[:2], ""))
  313. if instanceType == "n1highmem" || instanceType == "n1highcpu" {
  314. instanceType = "n1standard" // These are priced the same. TODO: support n1ultrahighmem
  315. } else if strings.HasPrefix(instanceType, "custom") {
  316. instanceType = "custom" // The suffix of custom does not matter
  317. }
  318. region := strings.ToLower(gcp.Labels[v1.LabelZoneRegion])
  319. var usageType string
  320. if t, ok := gcp.Labels["cloud.google.com/gke-preemptible"]; ok && t == "true" {
  321. usageType = "preemptible"
  322. } else {
  323. usageType = "ondemand"
  324. }
  325. return region + "," + instanceType + "," + usageType
  326. }
  327. // AllNodePricing returns the GCP pricing objects stored
  328. func (gcp *GCP) AllNodePricing() (interface{}, error) {
  329. return gcp.Pricing, nil
  330. }
  331. // NodePricing returns GCP pricing data for a single node
  332. func (gcp *GCP) NodePricing(key Key) (*Node, error) {
  333. if n, ok := gcp.Pricing[key.Features()]; ok {
  334. klog.V(2).Infof("Returning pricing for node %s: %+v from SKU %s", key, n.Node, n.Name)
  335. n.Node.BaseCPUPrice = gcp.BaseCPUPrice
  336. return n.Node, nil
  337. }
  338. klog.V(1).Infof("Warning: no pricing data found for %s", key)
  339. return nil, fmt.Errorf("Warning: no pricing data found for %s", key)
  340. }