Skip to content

Commit

Permalink
New Data Source: aws_ec2_spot_price (#12504)
Browse files Browse the repository at this point in the history
* d/aws_ec2_instance_spot_price - add new data source

Adds a data source to fetch the most recently published Spot Price
value for a given EC2 instance type and availability zone. The value
can be manipulated and fed into spot price parameters in other resources,
allowing for dynamic adjusted of spot requests.

* remove accidental inclusion of local test example

* make instance_type and availability_zone optional

* simplify DescribeSpotPriceHistory usage

since we are only interested in fetching the latest SpotPrice,
we don't need to iterate over a series of requests, as we only
ever need to fetch call's worth of data

* copypasta

* add more thorough tests for aws_ec2_instance_spot_price resource

* use dynamic data sources for filtering in tests

* format spot_price_timestamp as RFC3339

* rebase aws/provider.go

* fix up tests

* lint and typo fixes

* update docs

* whitespace

* aws_ec2_instance_spot_price -> aws_ec2_spot_price

* support spot price data by solely filtering

add a test for sourcing an aws_ec2_spot_price data source using
filter{} blocks exclusively. this commit also involves a rework of
the DescribeSpotPriceHistory AWS API call, namely using the native
SDK paging function; without this function, making a singular direct
call to the DescribeSpotPriceHistory SDK function returned a response with
an empty slice, but the underlying API call returned a response with a non-nil
NextToken value. to support this, we simply use the
DescribeSpotPriceHistoryPages unconditionally during reads, and
add all found SpotPriceHistory objects to a slice.

* update documentation following resource rename

* copypasta documention updates
  • Loading branch information
p0pr0ck5 authored Jul 31, 2020
1 parent c7b96fb commit 37c0224
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 0 deletions.
88 changes: 88 additions & 0 deletions aws/data_source_aws_ec2_spot_price.go
Original file line number Diff line number Diff line change
@@ -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
}
139 changes: 139 additions & 0 deletions aws/data_source_aws_ec2_spot_price_test.go
Original file line number Diff line number Diff line change
@@ -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]]
}
}
`)
}
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
3 changes: 3 additions & 0 deletions website/aws.erb
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,9 @@
<li>
<a href="/docs/providers/aws/d/ec2_local_gateway_virtual_interface_groups.html">aws_ec2_local_gateway_virtual_interface_groups</a>
</li>
<li>
<a href="/docs/providers/aws/d/ec2_spot_price.html">aws_ec2_spot_price</a>
</li>
<li>
<a href="/docs/providers/aws/d/ec2_transit_gateway.html">aws_ec2_transit_gateway</a>
</li>
Expand Down
45 changes: 45 additions & 0 deletions website/docs/d/ec2_spot_price.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
subcategory: "EC2"
layout: "aws"
page_title: "AWS: aws_ec2_instance_spot_price"
description: |-
Information about most recent Spot Price for a given EC2 instance.
---

# Data Source: aws_ec2_instance_spot_price

Information about most recent Spot Price for a given EC2 instance.

## Example Usage

```hcl
data "aws_ec2_instance_spot_price" "example" {
instance_type = "t3.medium"
availability_zone = "us-west-2a"
filter {
name = "product-description"
values = ["Linux/UNIX"]
}
}
```

## Argument Reference

The following arguments are supported:

* `instance_type` - (Optional) The type of instance for which to query Spot Price information.
* `availability_zone` - (Optional) The availability zone in which to query Spot price information.
* `filter` - (Optional) One or more configuration blocks containing name-values filters. See the [EC2 API Reference](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSpotPriceHistory.html) for supported filters. Detailed below.

### filter Argument Reference

* `name` - (Required) Name of the filter.
* `values` - (Required) List of one or more values for the filter.

## Attribute Reference

In addition to all arguments above, the following attributes are exported:

* `spot_price` - The most recent Spot Price value for the given instance type and AZ.
* `spot_price_timestamp` - The timestamp at which the Spot Price value was published.

0 comments on commit 37c0224

Please sign in to comment.