pricinglistpricingsource.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. package aws
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/opencost/opencost/core/pkg/env"
  10. "github.com/opencost/opencost/core/pkg/log"
  11. "github.com/opencost/opencost/core/pkg/model/pricingmodel"
  12. "github.com/opencost/opencost/core/pkg/model/shared"
  13. )
  14. const pricingCacheTTL = 24 * time.Hour
  15. const pricingCacheDir = "pricingsource/aws"
  16. const pricingCacheFile = "cached_ec2_pricingmodelset"
  17. const PricingListPricingSourceType pricingmodel.PricingSourceType = "aws_pricing_list_api"
  18. type PricingListPricingSourceConfig struct {
  19. CurrencyCode string
  20. }
  21. type PricingListPricingSource struct {
  22. config PricingListPricingSourceConfig
  23. }
  24. func NewPricingListPricingSource(cfg PricingListPricingSourceConfig) *PricingListPricingSource {
  25. return &PricingListPricingSource{config: cfg}
  26. }
  27. func (p *PricingListPricingSource) cacheFilePath() (string, error) {
  28. dir := env.GetPathFromConfig(pricingCacheDir)
  29. if _, e := os.Stat(dir); e != nil && os.IsNotExist(e) {
  30. err := os.MkdirAll(dir, os.ModePerm)
  31. if err != nil {
  32. return "", err
  33. }
  34. }
  35. return filepath.Join(dir, pricingCacheFile), nil
  36. }
  37. func (p *PricingListPricingSource) PricingSourceType() pricingmodel.PricingSourceType {
  38. return PricingListPricingSourceType
  39. }
  40. // PricingSourceKey returns the PricingSourceType because it is meant to run single instance.
  41. func (p *PricingListPricingSource) PricingSourceKey() string {
  42. return string(PricingListPricingSourceType)
  43. }
  44. func (p *PricingListPricingSource) GetPricing() (*pricingmodel.PricingModelSet, error) {
  45. log.Infof("PricingListPricingSource: starting AWS EC2 pricing list download (large file, this may take a while)")
  46. start := time.Now()
  47. now := time.Now().UTC()
  48. pms := pricingmodel.NewPricingModelSet(now, p.PricingSourceType(), p.PricingSourceKey())
  49. skuToNodeKey := make(map[string]pricingmodel.NodeKey)
  50. var productCount, termCount int
  51. const logInterval = 50000
  52. // When parsing product we create keys based off of product attributes and link those to a SKU.
  53. handleProduct := func(product *PriceListEC2Product) {
  54. productCount++
  55. if productCount%logInterval == 0 {
  56. log.Infof("PricingListPricingSource: processed %d products...", productCount)
  57. }
  58. attr := product.Attributes
  59. if attr.LocationType != "AWS Region" {
  60. return
  61. }
  62. if !((strings.HasPrefix(attr.UsageType, "BoxUsage") || strings.Contains(attr.UsageType, "-BoxUsage")) &&
  63. (attr.CapacityStatus == "Used" || attr.CapacityStatus == "") &&
  64. (attr.MarketOption == "OnDemand" || attr.MarketOption == "")) {
  65. return
  66. }
  67. if attr.OperatingSystem != "" && attr.OperatingSystem != "NA" && attr.OperatingSystem != "Linux" {
  68. return
  69. }
  70. if attr.PreInstalledSw != "" && attr.PreInstalledSw != "NA" {
  71. }
  72. if attr.RegionCode == "" || attr.InstanceType == "" {
  73. return
  74. }
  75. skuToNodeKey[product.Sku] = pricingmodel.NodeKey{
  76. Provider: shared.ProviderAWS,
  77. Region: attr.RegionCode,
  78. NodeType: attr.InstanceType,
  79. UsageType: shared.UsageTypeOnDemand,
  80. PricingType: pricingmodel.NodePricingTypeTotal,
  81. }
  82. }
  83. // Terms are used to define pricing and have the sku to look up the appropriate key.
  84. handleTerm := func(term *PriceListEC2Term) {
  85. termCount++
  86. if termCount%logInterval == 0 {
  87. log.Infof("PricingListPricingSource: processed %d terms, %d pricing entries so far...", termCount, len(pms.NodePricing))
  88. }
  89. nodeKey, ok := skuToNodeKey[term.Sku]
  90. if !ok {
  91. return
  92. }
  93. hourlyRateCode := HourlyRateCode
  94. if _, ok = OnDemandRateCodes[term.OfferTermCode]; !ok {
  95. if _, okCN := OnDemandRateCodesCn[term.OfferTermCode]; !okCN {
  96. // Skip if term is not OnDemand
  97. return
  98. }
  99. hourlyRateCode = HourlyRateCodeCn
  100. }
  101. priceDimensionKey := strings.Join([]string{term.Sku, term.OfferTermCode, hourlyRateCode}, ".")
  102. pricingDimension, ok := term.PriceDimensions[priceDimensionKey]
  103. if !ok {
  104. return
  105. }
  106. priceStr := pricingDimension.PricePerUnit.ForCurrency(p.config.CurrencyCode)
  107. price, err := strconv.ParseFloat(priceStr, 64)
  108. if err != nil {
  109. log.Errorf("failed to parse str to float '%s': %s", priceStr, err.Error())
  110. return
  111. }
  112. pms.NodePricing[nodeKey] = pricingmodel.NodePricing{
  113. HourlyRate: price,
  114. }
  115. }
  116. err := QueryEC2PriceList("", handleProduct, handleTerm)
  117. if err != nil {
  118. return nil, fmt.Errorf("failed to query list pricing data %w", err)
  119. }
  120. log.Infof("PricingListPricingSource: completed in %s — %d products, %d terms, %d pricing entries",
  121. time.Since(start).Round(time.Second), productCount, termCount, len(pms.NodePricing))
  122. return pms, nil
  123. }