pricelistapi.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. package aws
  2. import (
  3. "fmt"
  4. "io"
  5. "net/http"
  6. "strings"
  7. "github.com/opencost/opencost/core/pkg/log"
  8. "github.com/opencost/opencost/core/pkg/util/json"
  9. "github.com/opencost/opencost/pkg/env"
  10. )
  11. // OnDemandRateCodes is are sets of identifiers for offerTermCodes matching 'On Demand' rates
  12. var OnDemandRateCodes = map[string]struct{}{
  13. "JRTCKXETXF": {},
  14. }
  15. var OnDemandRateCodesCn = map[string]struct{}{
  16. "99YE2YK9UR": {},
  17. "5Y9WH78GDR": {},
  18. "KW44MY7SZN": {},
  19. }
  20. // HourlyRateCode is appended to a node sku
  21. const (
  22. HourlyRateCode = "6YS6EN2CT7"
  23. HourlyRateCodeCn = "Q7UJUT2CE6"
  24. )
  25. func getListPriceURL(service, region string) string {
  26. if env.GetAWSPricingURL() != "" { // Allow override of pricing URL
  27. return env.GetAWSPricingURL()
  28. }
  29. baseURL := awsPricingBaseURL
  30. if strings.HasPrefix(region, chinaRegionPrefix) {
  31. baseURL = awsChinaPricingBaseURL
  32. }
  33. baseURL += service + pricingCurrentPath
  34. if region != "" {
  35. baseURL += region + "/"
  36. }
  37. return baseURL + pricingIndexFile
  38. }
  39. func QueryEC2PriceList(
  40. region string,
  41. handleProduct func(*PriceListEC2Product),
  42. handleTerm func(term *PriceListEC2Term),
  43. ) error {
  44. pricingURL := getListPriceURL("AmazonEC2", region)
  45. log.Infof("starting download of \"%s\", which is quite large ...", pricingURL)
  46. resp, err := http.Get(pricingURL)
  47. if err != nil {
  48. return fmt.Errorf("Bogus fetch of \"%s\": %w", pricingURL, err)
  49. }
  50. dec := json.NewDecoder(resp.Body)
  51. for {
  52. t, err := dec.Token()
  53. if err == io.EOF {
  54. log.Infof("done loading \"%s\"\n", resp.Request.URL.String())
  55. break
  56. } else if err != nil {
  57. log.Errorf("error parsing response json %v", resp.Body)
  58. break
  59. }
  60. if t == "products" {
  61. _, err := dec.Token() // this should parse the opening "{""
  62. if err != nil {
  63. return err
  64. }
  65. for dec.More() {
  66. _, err := dec.Token() // the sku token
  67. if err != nil {
  68. return err
  69. }
  70. product := &PriceListEC2Product{}
  71. err = dec.Decode(&product)
  72. if err != nil {
  73. log.Errorf("Error parsing response from \"%s\": %v", resp.Request.URL.String(), err.Error())
  74. break
  75. }
  76. handleProduct(product)
  77. }
  78. }
  79. if t == "terms" {
  80. _, err := dec.Token() // this should parse the opening "{""
  81. if err != nil {
  82. return err
  83. }
  84. termType, err := dec.Token()
  85. if err != nil {
  86. return err
  87. }
  88. if termType == "OnDemand" {
  89. _, err := dec.Token()
  90. if err != nil { // again, should parse an opening "{"
  91. return err
  92. }
  93. for dec.More() {
  94. _, err := dec.Token() // sku
  95. if err != nil {
  96. return err
  97. }
  98. _, err = dec.Token() // another opening "{"
  99. if err != nil {
  100. return err
  101. }
  102. // SKUOndemand
  103. _, err = dec.Token()
  104. if err != nil {
  105. return err
  106. }
  107. offerTerm := &PriceListEC2Term{}
  108. err = dec.Decode(&offerTerm)
  109. if err != nil {
  110. log.Errorf("Error decoding AWS Offer Term: %s", err.Error())
  111. }
  112. handleTerm(offerTerm)
  113. _, err = dec.Token()
  114. if err != nil {
  115. return err
  116. }
  117. }
  118. _, err = dec.Token()
  119. if err != nil {
  120. return err
  121. }
  122. }
  123. }
  124. }
  125. return nil
  126. }
  127. // PriceListEC2Response maps a k8s node to an AWS Pricing "product"
  128. type PriceListEC2Response struct {
  129. Products map[string]*PriceListEC2Product `json:"products"`
  130. Terms PriceListEC2Terms `json:"terms"`
  131. }
  132. // PriceListEC2Product represents a purchased SKU
  133. type PriceListEC2Product struct {
  134. Sku string `json:"sku"`
  135. Attributes PriceListEC2ProductAttributes `json:"attributes"`
  136. }
  137. // PriceListEC2ProductAttributes represents metadata about the product used to map to a node.
  138. type PriceListEC2ProductAttributes struct {
  139. ServiceCode string `json:"servicecode"`
  140. InstanceType string `json:"instanceType"`
  141. UsageType string `json:"usagetype"`
  142. Operation string `json:"operation"`
  143. Location string `json:"location"`
  144. LocationType string `json:"locationType"`
  145. RegionCode string `json:"regionCode"`
  146. ServiceName string `json:"servicename"`
  147. // These fields do not appear to return in the api anymore
  148. Memory string `json:"memory"`
  149. Storage string `json:"storage"`
  150. VCpu string `json:"vcpu"`
  151. OperatingSystem string `json:"operatingSystem"`
  152. PreInstalledSw string `json:"preInstalledSw"`
  153. InstanceFamily string `json:"instanceFamily"`
  154. CapacityStatus string `json:"capacitystatus"`
  155. GPU string `json:"gpu"` // GPU represents the number of GPU on the instance
  156. MarketOption string `json:"marketOption"`
  157. }
  158. // PriceListEC2Terms are how you pay for the node: OnDemand, Reserved
  159. type PriceListEC2Terms struct {
  160. OnDemand map[string]map[string]*PriceListEC2Term `json:"OnDemand"`
  161. Reserved map[string]map[string]*PriceListEC2Term `json:"Reserved"`
  162. }
  163. // PriceListEC2Term is a sku extension used to pay for the node.
  164. type PriceListEC2Term struct {
  165. Sku string `json:"sku"`
  166. OfferTermCode string `json:"offerTermCode"`
  167. PriceDimensions map[string]*PriceListEC2PriceDimension `json:"priceDimensions"`
  168. }
  169. func (t *PriceListEC2Term) String() string {
  170. var strs []string
  171. for k, rc := range t.PriceDimensions {
  172. strs = append(strs, fmt.Sprintf("%s:%s", k, rc.String()))
  173. }
  174. return fmt.Sprintf("%s:%s", t.Sku, strings.Join(strs, ","))
  175. }
  176. // PriceListEC2PriceDimension encodes data about the price of a product
  177. type PriceListEC2PriceDimension struct {
  178. Unit string `json:"unit"`
  179. PricePerUnit PriceListEC2PricePerUnit `json:"pricePerUnit"`
  180. }
  181. func (pd *PriceListEC2PriceDimension) String() string {
  182. return fmt.Sprintf("{unit: %s, pricePerUnit: %v", pd.Unit, pd.PricePerUnit)
  183. }
  184. // PriceListEC2PricePerUnit is the localized currency.
  185. type PriceListEC2PricePerUnit struct {
  186. USD string `json:"USD,omitempty"`
  187. CNY string `json:"CNY,omitempty"`
  188. }
  189. // ForCurrency returns the price string for the given currency code, falling
  190. // back to USD if the code is unrecognized or the field is empty.
  191. func (p PriceListEC2PricePerUnit) ForCurrency(code string) string {
  192. switch strings.ToUpper(code) {
  193. case "CNY":
  194. if p.CNY != "" {
  195. return p.CNY
  196. }
  197. }
  198. return p.USD
  199. }