| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- package aws
- import (
- "encoding/json"
- "fmt"
- "net/http"
- "strconv"
- "strings"
- "time"
- "github.com/opencost/opencost/core/pkg/clustercache"
- "github.com/opencost/opencost/core/pkg/log"
- "github.com/opencost/opencost/pkg/env"
- )
- const (
- usageTypeFargateLinuxX86CPU = "Fargate-vCPU-Hours:perCPU"
- usageTypeFargateLinuxX86RAM = "Fargate-GB-Hours"
- usageTypeFargateLinuxArmCPU = "Fargate-ARM-vCPU-Hours:perCPU"
- usageTypeFargateLinuxArmRAM = "Fargate-ARM-GB-Hours"
- usageTypeFargateWindowsCPU = "Fargate-Windows-vCPU-Hours:perCPU"
- usageTypeFargateWindowsLicense = "Fargate-Windows-OS-Hours:perCPU"
- usageTypeFargateWindowsRAM = "Fargate-Windows-GB-Hours"
- )
- var fargateUsageTypes = []string{
- usageTypeFargateLinuxX86CPU,
- usageTypeFargateLinuxX86RAM,
- usageTypeFargateLinuxArmCPU,
- usageTypeFargateLinuxArmRAM,
- usageTypeFargateWindowsCPU,
- usageTypeFargateWindowsLicense,
- usageTypeFargateWindowsRAM,
- }
- type FargateRegionPricing map[string]float64
- func (f FargateRegionPricing) Validate() error {
- for _, usageType := range fargateUsageTypes {
- if _, ok := f[usageType]; !ok {
- return fmt.Errorf("missing pricing for usageType %s", usageType)
- }
- }
- return nil
- }
- type FargatePricing struct {
- regions map[string]FargateRegionPricing
- }
- func NewFargatePricing() *FargatePricing {
- return &FargatePricing{
- regions: make(map[string]FargateRegionPricing),
- }
- }
- func (f *FargatePricing) Initialize(nodeList []*clustercache.Node) error {
- url := f.getPricingURL(nodeList)
- log.Infof("Downloading Fargate pricing data from %s", url)
- client := &http.Client{
- Timeout: 30 * time.Second,
- }
- resp, err := client.Get(url)
- if err != nil {
- return fmt.Errorf("downloading pricing data: %w", err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return fmt.Errorf("pricing download failed: status=%d", resp.StatusCode)
- }
- var pricing AWSPricing
- if err := json.NewDecoder(resp.Body).Decode(&pricing); err != nil {
- return fmt.Errorf("parsing pricing data: %w", err)
- }
- return f.populatePricing(&pricing)
- }
- func (f *FargatePricing) getPricingURL(nodeList []*clustercache.Node) string {
- // Allow override of pricing URL for air-gapped environments
- if override := env.GetAWSECSPricingURLOverride(); override != "" {
- return override
- }
- return getPricingListURL("AmazonECS", nodeList)
- }
- func (f *FargatePricing) populatePricing(pricing *AWSPricing) error {
- // Populate pricing for each region
- productLoop:
- for sku, product := range pricing.Products {
- for _, usageType := range fargateUsageTypes {
- if strings.HasSuffix(product.Attributes.UsageType, usageType) {
- region := product.Attributes.RegionCode
- if _, ok := f.regions[region]; !ok {
- f.regions[region] = make(FargateRegionPricing)
- }
- skuPrice, err := f.getPricingOfSKU(sku, &pricing.Terms)
- if err != nil {
- return fmt.Errorf("error getting pricing for sku %s: %s", sku, err)
- }
- f.regions[region][usageType] = skuPrice
- continue productLoop
- }
- }
- }
- // Validate pricing - do we have all the pricing we need?
- for region, regionPricing := range f.regions {
- err := regionPricing.Validate()
- if err != nil {
- // Be failsafe here and just log warnings
- log.Warnf("Fargate pricing data is (partially) missing pricing for %s: %s", region, err)
- }
- }
- return nil
- }
- func (f *FargatePricing) getPricingOfSKU(sku string, allTerms *AWSPricingTerms) (float64, error) {
- skuTerm, ok := allTerms.OnDemand[sku]
- if !ok {
- return 0, fmt.Errorf("missing pricing for sku %s", sku)
- }
- for _, offerTerm := range skuTerm {
- if _, isMatch := OnDemandRateCodes[offerTerm.OfferTermCode]; isMatch {
- priceDimensionKey := strings.Join([]string{sku, offerTerm.OfferTermCode, HourlyRateCode}, ".")
- if dimension, ok := offerTerm.PriceDimensions[priceDimensionKey]; ok {
- return strconv.ParseFloat(dimension.PricePerUnit.USD, 64)
- }
- } else if _, isMatch := OnDemandRateCodesCn[offerTerm.OfferTermCode]; isMatch {
- priceDimensionKey := strings.Join([]string{sku, offerTerm.OfferTermCode, HourlyRateCodeCn}, ".")
- if dimension, ok := offerTerm.PriceDimensions[priceDimensionKey]; ok {
- return strconv.ParseFloat(dimension.PricePerUnit.CNY, 64)
- }
- }
- }
- return 0, fmt.Errorf("missing pricing for sku %s", sku)
- }
- func (f *FargatePricing) GetHourlyPricing(region string, os, arch string) (cpu, memory float64, err error) {
- regionPricing, ok := f.regions[region]
- if !ok {
- return 0, 0, fmt.Errorf("missing pricing for region %s", region)
- }
- switch os {
- case "linux":
- switch arch {
- case "amd64":
- cpu = regionPricing[usageTypeFargateLinuxX86CPU]
- memory = regionPricing[usageTypeFargateLinuxX86RAM]
- return
- case "arm64":
- cpu = regionPricing[usageTypeFargateLinuxArmCPU]
- memory = regionPricing[usageTypeFargateLinuxArmRAM]
- return
- }
- case "windows":
- cpuOnly := regionPricing[usageTypeFargateWindowsCPU]
- cpuLicense := regionPricing[usageTypeFargateWindowsLicense]
- cpu = cpuOnly + cpuLicense
- memory = regionPricing[usageTypeFargateWindowsRAM]
- return
- }
- return 0, 0, fmt.Errorf("unknown os/arch combination: %s/%s", os, arch)
- }
|