awspricingsource.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. package aws
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. "time"
  7. "github.com/opencost/opencost/core/pkg/log"
  8. "github.com/opencost/opencost/core/pkg/model/shared"
  9. "github.com/opencost/opencost/core/pkg/pricing"
  10. "github.com/opencost/opencost/core/pkg/unit"
  11. )
  12. type AWSPricingSourceConfig struct {
  13. CurrencyCode string
  14. }
  15. type AWSPricingSource struct {
  16. config AWSPricingSourceConfig
  17. }
  18. func NewAWSPricingSource(cfg AWSPricingSourceConfig) *AWSPricingSource {
  19. return &AWSPricingSource{config: cfg}
  20. }
  21. func (p *AWSPricingSource) GetPricing() (*pricing.PricingSet, error) {
  22. log.Infof("PricingSource (AWS): starting EC2 pricing list download (large file, this may take a while)")
  23. start := time.Now()
  24. ps := &pricing.PricingSet{
  25. NodePricing: []*pricing.NodePricing{},
  26. PersistentVolumePricing: []*pricing.PersistentVolumePricing{},
  27. }
  28. skuToNodeKey := make(map[string]nodeKey)
  29. skuToVolumeKey := make(map[string]volumeKey)
  30. var productCount, termCount int
  31. const logInterval = 50000
  32. region := ""
  33. if strings.ToUpper(p.config.CurrencyCode) == "CNY" {
  34. region = "cn-north-1"
  35. log.Infof("PricingSource (AWS): Using China pricing endpoint for CNY currency")
  36. }
  37. // When parsing product we create keys based off of product attributes and link those to a SKU.
  38. handleProduct := func(product *PriceListEC2Product) {
  39. productCount++
  40. if productCount%logInterval == 0 {
  41. log.Infof("PricingSource (AWS): processed %d products...", productCount)
  42. }
  43. attr := product.Attributes
  44. if attr.LocationType != "AWS Region" {
  45. return
  46. }
  47. // Handle EC2 instances
  48. if (strings.HasPrefix(attr.UsageType, "BoxUsage") || strings.Contains(attr.UsageType, "-BoxUsage")) &&
  49. (attr.CapacityStatus == "Used" || attr.CapacityStatus == "") &&
  50. (attr.MarketOption == "OnDemand" || attr.MarketOption == "") {
  51. if attr.OperatingSystem != "" && attr.OperatingSystem != "NA" && attr.OperatingSystem != "Linux" {
  52. return
  53. }
  54. if attr.RegionCode == "" || attr.InstanceType == "" {
  55. return
  56. }
  57. skuToNodeKey[product.Sku] = nodeKey{
  58. Region: attr.RegionCode,
  59. InstanceType: attr.InstanceType,
  60. }
  61. return
  62. }
  63. // Handle EBS volumes
  64. if strings.Contains(attr.UsageType, "EBS:Volume") {
  65. // Extract the volume type from the usage type (e.g., "USE1-EBS:VolumeUsage.gp3" -> "EBS:VolumeUsage.gp3")
  66. usageTypeMatch := usageTypeRegex.FindStringSubmatch(attr.UsageType)
  67. if len(usageTypeMatch) == 0 {
  68. return
  69. }
  70. usageTypeNoRegion := usageTypeMatch[len(usageTypeMatch)-1]
  71. // Map to volume type
  72. volumeType, ok := awsVolumeTypes[usageTypeNoRegion]
  73. if !ok {
  74. return
  75. }
  76. if attr.RegionCode == "" {
  77. return
  78. }
  79. skuToVolumeKey[product.Sku] = volumeKey{
  80. Region: attr.RegionCode,
  81. VolumeType: volumeType,
  82. UsageType: usageTypeNoRegion,
  83. }
  84. }
  85. }
  86. // Terms are used to define pricing and have the sku to look up the appropriate key.
  87. handleTerm := func(term *PriceListEC2Term) {
  88. termCount++
  89. if termCount%logInterval == 0 {
  90. log.Infof("PricingSource (AWS): processed %d terms, %d node pricing, %d volume pricing so far...",
  91. termCount, len(ps.NodePricing), len(ps.PersistentVolumePricing))
  92. }
  93. // Check if this SKU is for a node or volume we're tracking
  94. nk, isNode := skuToNodeKey[term.Sku]
  95. vk, isVolume := skuToVolumeKey[term.Sku]
  96. if !isNode && !isVolume {
  97. return
  98. }
  99. // Determine the hourly rate code based on the offer term
  100. hourlyRateCode := HourlyRateCode
  101. if _, ok := OnDemandRateCodes[term.OfferTermCode]; !ok {
  102. if _, okCN := OnDemandRateCodesCn[term.OfferTermCode]; !okCN {
  103. // Skip if term is not OnDemand
  104. return
  105. }
  106. hourlyRateCode = HourlyRateCodeCn
  107. }
  108. priceDimensionKey := strings.Join([]string{term.Sku, term.OfferTermCode, hourlyRateCode}, ".")
  109. pricingDimension, ok := term.PriceDimensions[priceDimensionKey]
  110. if !ok {
  111. return
  112. }
  113. priceStr := pricingDimension.PricePerUnit.ForCurrency(p.config.CurrencyCode)
  114. price, err := strconv.ParseFloat(priceStr, 64)
  115. if err != nil {
  116. log.Errorf("failed to parse price '%s': %s", priceStr, err.Error())
  117. return
  118. }
  119. // TODO: handle currency?
  120. // Parse the currency from config, default to USD if invalid
  121. // currency, err := unit.ParseCurrency(p.config.CurrencyCode)
  122. // if err != nil {
  123. // log.Warnf("invalid currency code '%s', defaulting to USD: %s", p.config.CurrencyCode, err.Error())
  124. // currency = unit.USD
  125. // }
  126. // Handle node pricing
  127. if isNode {
  128. nodePricing := &pricing.NodePricing{
  129. Properties: pricing.NodePricingProperties{
  130. Provider: shared.ProviderAWS,
  131. Region: nk.Region,
  132. InstanceType: nk.InstanceType,
  133. Provisioning: pricing.ProvisioningOnDemand,
  134. },
  135. Prices: pricing.Prices{
  136. pricing.ResourceNode: pricing.Price{
  137. Unit: unit.Hour,
  138. Price: price,
  139. },
  140. },
  141. }
  142. ps.NodePricing = append(ps.NodePricing, nodePricing)
  143. }
  144. // Handle volume pricing
  145. if isVolume {
  146. // AWS volume pricing is per GB-month, convert to per GB-hour
  147. hourlyPrice := price / 730.0
  148. volumePricing := &pricing.PersistentVolumePricing{
  149. Properties: pricing.PersistentVolumePricingProperties{
  150. Provider: shared.ProviderAWS,
  151. Region: vk.Region,
  152. VolumeType: vk.VolumeType,
  153. },
  154. Prices: pricing.Prices{
  155. pricing.ResourceStorage: pricing.Price{
  156. Unit: unit.Hour,
  157. Price: hourlyPrice,
  158. },
  159. },
  160. }
  161. ps.PersistentVolumePricing = append(ps.PersistentVolumePricing, volumePricing)
  162. }
  163. }
  164. err := QueryEC2PriceList(region, handleProduct, handleTerm)
  165. if err != nil {
  166. return nil, fmt.Errorf("failed to query list pricing data %w", err)
  167. }
  168. log.Infof("PricingSource (AWS): completed in %s — %d products, %d terms, %d node pricing, %d volume pricing",
  169. time.Since(start).Round(time.Second), productCount, termCount, len(ps.NodePricing), len(ps.PersistentVolumePricing))
  170. return ps, nil
  171. }