2
0

fargate.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. package aws
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/opencost/opencost/core/pkg/clustercache"
  10. "github.com/opencost/opencost/core/pkg/log"
  11. "github.com/opencost/opencost/pkg/env"
  12. )
  13. const (
  14. usageTypeFargateLinuxX86CPU = "Fargate-vCPU-Hours:perCPU"
  15. usageTypeFargateLinuxX86RAM = "Fargate-GB-Hours"
  16. usageTypeFargateLinuxArmCPU = "Fargate-ARM-vCPU-Hours:perCPU"
  17. usageTypeFargateLinuxArmRAM = "Fargate-ARM-GB-Hours"
  18. usageTypeFargateWindowsCPU = "Fargate-Windows-vCPU-Hours:perCPU"
  19. usageTypeFargateWindowsLicense = "Fargate-Windows-OS-Hours:perCPU"
  20. usageTypeFargateWindowsRAM = "Fargate-Windows-GB-Hours"
  21. )
  22. var fargateUsageTypes = []string{
  23. usageTypeFargateLinuxX86CPU,
  24. usageTypeFargateLinuxX86RAM,
  25. usageTypeFargateLinuxArmCPU,
  26. usageTypeFargateLinuxArmRAM,
  27. usageTypeFargateWindowsCPU,
  28. usageTypeFargateWindowsLicense,
  29. usageTypeFargateWindowsRAM,
  30. }
  31. type FargateRegionPricing map[string]float64
  32. func (f FargateRegionPricing) Validate() error {
  33. for _, usageType := range fargateUsageTypes {
  34. if _, ok := f[usageType]; !ok {
  35. return fmt.Errorf("missing pricing for usageType %s", usageType)
  36. }
  37. }
  38. return nil
  39. }
  40. type FargatePricing struct {
  41. regions map[string]FargateRegionPricing
  42. }
  43. func NewFargatePricing() *FargatePricing {
  44. return &FargatePricing{
  45. regions: make(map[string]FargateRegionPricing),
  46. }
  47. }
  48. func (f *FargatePricing) Initialize(nodeList []*clustercache.Node) error {
  49. url := f.getPricingURL(nodeList)
  50. log.Infof("Downloading Fargate pricing data from %s", url)
  51. client := &http.Client{
  52. Timeout: 30 * time.Second,
  53. }
  54. resp, err := client.Get(url)
  55. if err != nil {
  56. return fmt.Errorf("downloading pricing data: %w", err)
  57. }
  58. defer resp.Body.Close()
  59. if resp.StatusCode != http.StatusOK {
  60. return fmt.Errorf("pricing download failed: status=%d", resp.StatusCode)
  61. }
  62. var pricing AWSPricing
  63. if err := json.NewDecoder(resp.Body).Decode(&pricing); err != nil {
  64. return fmt.Errorf("parsing pricing data: %w", err)
  65. }
  66. return f.populatePricing(&pricing)
  67. }
  68. func (f *FargatePricing) getPricingURL(nodeList []*clustercache.Node) string {
  69. // Allow override of pricing URL for air-gapped environments
  70. if override := env.GetAWSECSPricingURLOverride(); override != "" {
  71. return override
  72. }
  73. return getPricingListURL("AmazonECS", nodeList)
  74. }
  75. func (f *FargatePricing) populatePricing(pricing *AWSPricing) error {
  76. // Populate pricing for each region
  77. productLoop:
  78. for sku, product := range pricing.Products {
  79. for _, usageType := range fargateUsageTypes {
  80. if strings.HasSuffix(product.Attributes.UsageType, usageType) {
  81. region := product.Attributes.RegionCode
  82. if _, ok := f.regions[region]; !ok {
  83. f.regions[region] = make(FargateRegionPricing)
  84. }
  85. skuPrice, err := f.getPricingOfSKU(sku, &pricing.Terms)
  86. if err != nil {
  87. return fmt.Errorf("error getting pricing for sku %s: %s", sku, err)
  88. }
  89. f.regions[region][usageType] = skuPrice
  90. continue productLoop
  91. }
  92. }
  93. }
  94. // Validate pricing - do we have all the pricing we need?
  95. for region, regionPricing := range f.regions {
  96. err := regionPricing.Validate()
  97. if err != nil {
  98. // Be failsafe here and just log warnings
  99. log.Warnf("Fargate pricing data is (partially) missing pricing for %s: %s", region, err)
  100. }
  101. }
  102. return nil
  103. }
  104. func (f *FargatePricing) getPricingOfSKU(sku string, allTerms *AWSPricingTerms) (float64, error) {
  105. skuTerm, ok := allTerms.OnDemand[sku]
  106. if !ok {
  107. return 0, fmt.Errorf("missing pricing for sku %s", sku)
  108. }
  109. for _, offerTerm := range skuTerm {
  110. if _, isMatch := OnDemandRateCodes[offerTerm.OfferTermCode]; isMatch {
  111. priceDimensionKey := strings.Join([]string{sku, offerTerm.OfferTermCode, HourlyRateCode}, ".")
  112. if dimension, ok := offerTerm.PriceDimensions[priceDimensionKey]; ok {
  113. return strconv.ParseFloat(dimension.PricePerUnit.USD, 64)
  114. }
  115. } else if _, isMatch := OnDemandRateCodesCn[offerTerm.OfferTermCode]; isMatch {
  116. priceDimensionKey := strings.Join([]string{sku, offerTerm.OfferTermCode, HourlyRateCodeCn}, ".")
  117. if dimension, ok := offerTerm.PriceDimensions[priceDimensionKey]; ok {
  118. return strconv.ParseFloat(dimension.PricePerUnit.CNY, 64)
  119. }
  120. }
  121. }
  122. return 0, fmt.Errorf("missing pricing for sku %s", sku)
  123. }
  124. func (f *FargatePricing) GetHourlyPricing(region string, os, arch string) (cpu, memory float64, err error) {
  125. regionPricing, ok := f.regions[region]
  126. if !ok {
  127. return 0, 0, fmt.Errorf("missing pricing for region %s", region)
  128. }
  129. switch os {
  130. case "linux":
  131. switch arch {
  132. case "amd64":
  133. cpu = regionPricing[usageTypeFargateLinuxX86CPU]
  134. memory = regionPricing[usageTypeFargateLinuxX86RAM]
  135. return
  136. case "arm64":
  137. cpu = regionPricing[usageTypeFargateLinuxArmCPU]
  138. memory = regionPricing[usageTypeFargateLinuxArmRAM]
  139. return
  140. }
  141. case "windows":
  142. cpuOnly := regionPricing[usageTypeFargateWindowsCPU]
  143. cpuLicense := regionPricing[usageTypeFargateWindowsLicense]
  144. cpu = cpuOnly + cpuLicense
  145. memory = regionPricing[usageTypeFargateWindowsRAM]
  146. return
  147. }
  148. return 0, 0, fmt.Errorf("unknown os/arch combination: %s/%s", os, arch)
  149. }