spotapi.go 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. package aws
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "time"
  7. awsConfig "github.com/aws/aws-sdk-go-v2/config"
  8. "github.com/aws/aws-sdk-go-v2/service/ec2"
  9. ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
  10. "github.com/opencost/opencost/core/pkg/log"
  11. )
  12. // SpotPrice holds the most recent spot price for a single instance type in a region.
  13. type SpotPrice struct {
  14. Region string
  15. InstanceType string
  16. Price float64
  17. Timestamp time.Time
  18. }
  19. const osDesc = "Linux/UNIX (Amazon VPC)"
  20. type spotPriceHistoryClient interface {
  21. ec2.DescribeSpotPriceHistoryAPIClient
  22. }
  23. // QuerySpotPrices fetches the current spot price for every Linux/UNIX instance
  24. // type available in the given region
  25. func QuerySpotPrices(ctx context.Context, region string) ([]SpotPrice, error) {
  26. cfg, err := awsConfig.LoadDefaultConfig(ctx, awsConfig.WithRegion(region))
  27. if err != nil {
  28. return nil, fmt.Errorf("loading AWS config for region %s: %w", region, err)
  29. }
  30. return querySpotPrices(ctx, region, ec2.NewFromConfig(cfg))
  31. }
  32. func querySpotPrices(ctx context.Context, region string, client spotPriceHistoryClient) ([]SpotPrice, error) {
  33. paginator := ec2.NewDescribeSpotPriceHistoryPaginator(client, &ec2.DescribeSpotPriceHistoryInput{
  34. ProductDescriptions: []string{osDesc},
  35. })
  36. seen := make(map[ec2Types.InstanceType]struct{})
  37. var results []SpotPrice
  38. for paginator.HasMorePages() {
  39. page, err := paginator.NextPage(ctx)
  40. if err != nil {
  41. return nil, fmt.Errorf("fetching spot price history page for region %s: %w", region, err)
  42. }
  43. for _, item := range page.SpotPriceHistory {
  44. if _, ok := seen[item.InstanceType]; ok {
  45. continue
  46. }
  47. seen[item.InstanceType] = struct{}{}
  48. if item.SpotPrice == nil || item.Timestamp == nil {
  49. log.Warnf("SpotAPI: skipping %s/%s — missing price or timestamp", region, item.InstanceType)
  50. continue
  51. }
  52. price, err := strconv.ParseFloat(*item.SpotPrice, 64)
  53. if err != nil {
  54. log.Warnf("SpotAPI: skipping %s/%s — could not parse price %q: %v", region, item.InstanceType, *item.SpotPrice, err)
  55. continue
  56. }
  57. results = append(results, SpotPrice{
  58. Region: region,
  59. InstanceType: string(item.InstanceType),
  60. Price: price,
  61. Timestamp: *item.Timestamp,
  62. })
  63. }
  64. }
  65. log.Infof("SpotAPI: fetched %d spot prices for region %s", len(results), region)
  66. return results, nil
  67. }