diff --git a/aws/data_source_aws_ec2_spot_price.go b/aws/data_source_aws_ec2_spot_price.go new file mode 100644 index 00000000000..7b16eb12f31 --- /dev/null +++ b/aws/data_source_aws_ec2_spot_price.go @@ -0,0 +1,88 @@ +package aws + +import ( + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func dataSourceAwsEc2SpotPrice() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsEc2SpotPriceRead, + + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + "instance_type": { + Type: schema.TypeString, + Optional: true, + }, + "availability_zone": { + Type: schema.TypeString, + Optional: true, + }, + "spot_price": { + Type: schema.TypeString, + Computed: true, + }, + "spot_price_timestamp": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsEc2SpotPriceRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + now := time.Now() + input := &ec2.DescribeSpotPriceHistoryInput{ + StartTime: &now, + } + + if v, ok := d.GetOk("instance_type"); ok { + instanceType := v.(string) + input.InstanceTypes = []*string{ + aws.String(instanceType), + } + } + + if v, ok := d.GetOk("availability_zone"); ok { + availabilityZone := v.(string) + input.AvailabilityZone = aws.String(availabilityZone) + } + + if v, ok := d.GetOk("filter"); ok { + input.Filters = buildAwsDataSourceFilters(v.(*schema.Set)) + } + + var foundSpotPrice []*ec2.SpotPrice + + err := conn.DescribeSpotPriceHistoryPages(input, func(output *ec2.DescribeSpotPriceHistoryOutput, lastPage bool) bool { + foundSpotPrice = append(foundSpotPrice, output.SpotPriceHistory...) + return true + }) + if err != nil { + return fmt.Errorf("error reading EC2 Spot Price History: %w", err) + } + + if len(foundSpotPrice) == 0 { + return fmt.Errorf("no EC2 Spot Price History found matching criteria; try different search") + } + + if len(foundSpotPrice) > 1 { + return fmt.Errorf("multiple EC2 Spot Price History results found matching criteria; try different search") + } + + resultSpotPrice := foundSpotPrice[0] + + d.Set("spot_price", resultSpotPrice.SpotPrice) + d.Set("spot_price_timestamp", (*resultSpotPrice.Timestamp).Format(time.RFC3339)) + d.SetId(resource.UniqueId()) + + return nil +} diff --git a/aws/data_source_aws_ec2_spot_price_test.go b/aws/data_source_aws_ec2_spot_price_test.go new file mode 100644 index 00000000000..a08764b13fc --- /dev/null +++ b/aws/data_source_aws_ec2_spot_price_test.go @@ -0,0 +1,139 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccAwsEc2SpotPriceDataSource(t *testing.T) { + dataSourceName := "data.aws_ec2_spot_price.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAwsEc2SpotPrice(t) }, + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAwsEc2SpotPriceDataSourceConfig(), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr(dataSourceName, "spot_price", regexp.MustCompile(`^\d+\.\d+$`)), + resource.TestMatchResourceAttr(dataSourceName, "spot_price_timestamp", regexp.MustCompile(rfc3339RegexPattern)), + ), + }, + }, + }) +} + +func TestAccAwsEc2SpotPriceDataSourceFilter(t *testing.T) { + dataSourceName := "data.aws_ec2_spot_price.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAwsEc2SpotPrice(t) }, + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAwsEc2SpotPriceDataSourceFilterConfig(), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr(dataSourceName, "spot_price", regexp.MustCompile(`^\d+\.\d+$`)), + resource.TestMatchResourceAttr(dataSourceName, "spot_price_timestamp", regexp.MustCompile(rfc3339RegexPattern)), + ), + }, + }, + }) +} + +func testAccPreCheckAwsEc2SpotPrice(t *testing.T) { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + input := &ec2.DescribeSpotPriceHistoryInput{ + MaxResults: aws.Int64(5), + } + + _, err := conn.DescribeSpotPriceHistory(input) + + if testAccPreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccAwsEc2SpotPriceDataSourceConfig() string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +data "aws_ec2_instance_type_offering" "test" { + filter { + name = "instance-type" + values = ["m5.xlarge"] + } +} + +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "region-name" + values = [data.aws_region.current.name] + } +} + +data "aws_ec2_spot_price" "test" { + instance_type = data.aws_ec2_instance_type_offering.test.instance_type + + availability_zone = data.aws_availability_zones.available.names[0] + + filter { + name = "product-description" + values = ["Linux/UNIX"] + } +} +`) +} + +func testAccAwsEc2SpotPriceDataSourceFilterConfig() string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +data "aws_ec2_instance_type_offering" "test" { + filter { + name = "instance-type" + values = ["m5.xlarge"] + } +} + +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "region-name" + values = [data.aws_region.current.name] + } +} + +data "aws_ec2_spot_price" "test" { + filter { + name = "product-description" + values = ["Linux/UNIX"] + } + + filter { + name = "instance-type" + values = [data.aws_ec2_instance_type_offering.test.instance_type] + } + + filter { + name = "availability-zone" + values = [data.aws_availability_zones.available.names[0]] + } +} +`) +} diff --git a/aws/provider.go b/aws/provider.go index 7f8b23a3c82..e6f699cfdc1 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -221,6 +221,7 @@ func Provider() terraform.ResourceProvider { "aws_ec2_local_gateway_virtual_interface": dataSourceAwsEc2LocalGatewayVirtualInterface(), "aws_ec2_local_gateway_virtual_interface_group": dataSourceAwsEc2LocalGatewayVirtualInterfaceGroup(), "aws_ec2_local_gateway_virtual_interface_groups": dataSourceAwsEc2LocalGatewayVirtualInterfaceGroups(), + "aws_ec2_spot_price": dataSourceAwsEc2SpotPrice(), "aws_ec2_transit_gateway": dataSourceAwsEc2TransitGateway(), "aws_ec2_transit_gateway_dx_gateway_attachment": dataSourceAwsEc2TransitGatewayDxGatewayAttachment(), "aws_ec2_transit_gateway_peering_attachment": dataSourceAwsEc2TransitGatewayPeeringAttachment(), diff --git a/website/aws.erb b/website/aws.erb index 9bfefea77e6..006ba66aef0 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1148,6 +1148,9 @@