diff --git a/aws/data_source_aws_ec2_instance_type_offering.go b/aws/data_source_aws_ec2_instance_type_offering.go new file mode 100644 index 00000000000..fd53d836530 --- /dev/null +++ b/aws/data_source_aws_ec2_instance_type_offering.go @@ -0,0 +1,128 @@ +package aws + +import ( + "fmt" + + "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" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func dataSourceAwsEc2InstanceTypeOffering() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsEc2InstanceTypeOfferingRead, + + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + "instance_type": { + Type: schema.TypeString, + Computed: true, + }, + "location_type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + ec2.LocationTypeAvailabilityZone, + ec2.LocationTypeAvailabilityZoneId, + ec2.LocationTypeRegion, + }, false), + }, + "preferred_instance_types": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func dataSourceAwsEc2InstanceTypeOfferingRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + input := &ec2.DescribeInstanceTypeOfferingsInput{} + + if v, ok := d.GetOk("filter"); ok { + input.Filters = buildAwsDataSourceFilters(v.(*schema.Set)) + } + + if v, ok := d.GetOk("location_type"); ok { + input.LocationType = aws.String(v.(string)) + } + + var foundInstanceTypes []string + + for { + output, err := conn.DescribeInstanceTypeOfferings(input) + + if err != nil { + return fmt.Errorf("error reading EC2 Instance Type Offerings: %w", err) + } + + if output == nil { + break + } + + for _, instanceTypeOffering := range output.InstanceTypeOfferings { + if instanceTypeOffering == nil { + continue + } + + foundInstanceTypes = append(foundInstanceTypes, aws.StringValue(instanceTypeOffering.InstanceType)) + } + + if aws.StringValue(output.NextToken) == "" { + break + } + + input.NextToken = output.NextToken + } + + if len(foundInstanceTypes) == 0 { + return fmt.Errorf("no EC2 Instance Type Offerings found matching criteria; try different search") + } + + var resultInstanceType string + + // Search preferred instance types in their given order and set result + // instance type for first match found + if l := d.Get("preferred_instance_types").([]interface{}); len(l) > 0 { + for _, elem := range l { + preferredInstanceType, ok := elem.(string) + + if !ok { + continue + } + + for _, foundInstanceType := range foundInstanceTypes { + if foundInstanceType == preferredInstanceType { + resultInstanceType = preferredInstanceType + break + } + } + + if resultInstanceType != "" { + break + } + } + } + + if resultInstanceType == "" && len(foundInstanceTypes) > 1 { + return fmt.Errorf("multiple EC2 Instance Offerings found matching criteria; try different search") + } + + if resultInstanceType == "" && len(foundInstanceTypes) == 1 { + resultInstanceType = foundInstanceTypes[0] + } + + if resultInstanceType == "" { + return fmt.Errorf("no EC2 Instance Type Offerings found matching criteria; try different search") + } + + d.Set("instance_type", resultInstanceType) + + d.SetId(resource.UniqueId()) + + return nil +} diff --git a/aws/data_source_aws_ec2_instance_type_offering_test.go b/aws/data_source_aws_ec2_instance_type_offering_test.go new file mode 100644 index 00000000000..5da0f721ed4 --- /dev/null +++ b/aws/data_source_aws_ec2_instance_type_offering_test.go @@ -0,0 +1,143 @@ +package aws + +import ( + "fmt" + "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 TestAccAWSEc2InstanceTypeOfferingDataSource_Filter(t *testing.T) { + dataSourceName := "data.aws_ec2_instance_type_offering.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2InstanceTypeOffering(t) }, + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSEc2InstanceTypeOfferingDataSourceConfigFilter(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(dataSourceName, "instance_type"), + ), + }, + }, + }) +} + +func TestAccAWSEc2InstanceTypeOfferingDataSource_LocationType(t *testing.T) { + dataSourceName := "data.aws_ec2_instance_type_offering.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2InstanceTypeOffering(t) }, + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSEc2InstanceTypeOfferingDataSourceConfigLocationType(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(dataSourceName, "instance_type"), + ), + }, + }, + }) +} + +func TestAccAWSEc2InstanceTypeOfferingDataSource_PreferredInstanceTypes(t *testing.T) { + dataSourceName := "data.aws_ec2_instance_type_offering.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2InstanceTypeOffering(t) }, + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSEc2InstanceTypeOfferingDataSourceConfigPreferredInstanceTypes(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "instance_type", "t3.micro"), + ), + }, + }, + }) +} + +func testAccPreCheckAWSEc2InstanceTypeOffering(t *testing.T) { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + input := &ec2.DescribeInstanceTypeOfferingsInput{ + MaxResults: aws.Int64(5), + } + + _, err := conn.DescribeInstanceTypeOfferings(input) + + if testAccPreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccAWSEc2InstanceTypeOfferingDataSourceConfigFilter() string { + return fmt.Sprintf(` +# Rather than hardcode an instance type in the testing, +# use the first result from all available offerings. +data "aws_ec2_instance_type_offerings" "test" {} + +data "aws_ec2_instance_type_offering" "test" { + filter { + name = "instance-type" + values = [tolist(data.aws_ec2_instance_type_offerings.test.instance_types)[0]] + } +} +`) +} + +func testAccAWSEc2InstanceTypeOfferingDataSourceConfigLocationType() string { + return fmt.Sprintf(` +data "aws_availability_zones" "available" { + state = "available" +} + +# Rather than hardcode an instance type in the testing, +# use the first result from all available offerings. +data "aws_ec2_instance_type_offerings" "test" { + filter { + name = "location" + values = [data.aws_availability_zones.available.names[0]] + } + + location_type = "availability-zone" +} + +data "aws_ec2_instance_type_offering" "test" { + filter { + name = "instance-type" + values = [tolist(data.aws_ec2_instance_type_offerings.test.instance_types)[0]] + } + + filter { + name = "location" + values = [data.aws_availability_zones.available.names[0]] + } + + location_type = "availability-zone" +} +`) +} + +func testAccAWSEc2InstanceTypeOfferingDataSourceConfigPreferredInstanceTypes() string { + return fmt.Sprintf(` +data "aws_ec2_instance_type_offering" "test" { + filter { + name = "instance-type" + values = ["t1.micro", "t2.micro", "t3.micro"] + } + + preferred_instance_types = ["t3.micro", "t2.micro", "t1.micro"] +} +`) +} diff --git a/aws/data_source_aws_ec2_instance_type_offerings.go b/aws/data_source_aws_ec2_instance_type_offerings.go new file mode 100644 index 00000000000..84fac611f41 --- /dev/null +++ b/aws/data_source_aws_ec2_instance_type_offerings.go @@ -0,0 +1,85 @@ +package aws + +import ( + "fmt" + + "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" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func dataSourceAwsEc2InstanceTypeOfferings() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsEc2InstanceTypeOfferingsRead, + + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + "instance_types": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "location_type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + ec2.LocationTypeAvailabilityZone, + ec2.LocationTypeAvailabilityZoneId, + ec2.LocationTypeRegion, + }, false), + }, + }, + } +} + +func dataSourceAwsEc2InstanceTypeOfferingsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + input := &ec2.DescribeInstanceTypeOfferingsInput{} + + if v, ok := d.GetOk("filter"); ok { + input.Filters = buildAwsDataSourceFilters(v.(*schema.Set)) + } + + if v, ok := d.GetOk("location_type"); ok { + input.LocationType = aws.String(v.(string)) + } + + var instanceTypes []string + + for { + output, err := conn.DescribeInstanceTypeOfferings(input) + + if err != nil { + return fmt.Errorf("error reading EC2 Instance Type Offerings: %w", err) + } + + if output == nil { + break + } + + for _, instanceTypeOffering := range output.InstanceTypeOfferings { + if instanceTypeOffering == nil { + continue + } + + instanceTypes = append(instanceTypes, aws.StringValue(instanceTypeOffering.InstanceType)) + } + + if aws.StringValue(output.NextToken) == "" { + break + } + + input.NextToken = output.NextToken + } + + if err := d.Set("instance_types", instanceTypes); err != nil { + return fmt.Errorf("error setting instance_types: %s", err) + } + + d.SetId(resource.UniqueId()) + + return nil +} diff --git a/aws/data_source_aws_ec2_instance_type_offerings_test.go b/aws/data_source_aws_ec2_instance_type_offerings_test.go new file mode 100644 index 00000000000..f81bcd5872f --- /dev/null +++ b/aws/data_source_aws_ec2_instance_type_offerings_test.go @@ -0,0 +1,108 @@ +package aws + +import ( + "fmt" + "testing" + + "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/terraform" +) + +func TestAccAWSEc2InstanceTypeOfferingsDataSource_Filter(t *testing.T) { + dataSourceName := "data.aws_ec2_instance_type_offerings.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2InstanceTypeOfferings(t) }, + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSEc2InstanceTypeOfferingsDataSourceConfigFilter(), + Check: resource.ComposeTestCheckFunc( + testAccCheckEc2InstanceTypeOfferingsInstanceTypes(dataSourceName), + ), + }, + }, + }) +} + +func TestAccAWSEc2InstanceTypeOfferingsDataSource_LocationType(t *testing.T) { + dataSourceName := "data.aws_ec2_instance_type_offerings.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2InstanceTypeOfferings(t) }, + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSEc2InstanceTypeOfferingsDataSourceConfigLocationType(), + Check: resource.ComposeTestCheckFunc( + testAccCheckEc2InstanceTypeOfferingsInstanceTypes(dataSourceName), + ), + }, + }, + }) +} + +func testAccCheckEc2InstanceTypeOfferingsInstanceTypes(dataSourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[dataSourceName] + if !ok { + return fmt.Errorf("Not found: %s", dataSourceName) + } + + if v := rs.Primary.Attributes["instance_types.#"]; v == "0" { + return fmt.Errorf("expected at least one instance_types result, got none") + } + + return nil + } +} + +func testAccPreCheckAWSEc2InstanceTypeOfferings(t *testing.T) { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + input := &ec2.DescribeInstanceTypeOfferingsInput{ + MaxResults: aws.Int64(5), + } + + _, err := conn.DescribeInstanceTypeOfferings(input) + + if testAccPreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccAWSEc2InstanceTypeOfferingsDataSourceConfigFilter() string { + return fmt.Sprintf(` +data "aws_ec2_instance_type_offerings" "test" { + filter { + name = "instance-type" + values = ["t2.micro", "t3.micro"] + } +} +`) +} + +func testAccAWSEc2InstanceTypeOfferingsDataSourceConfigLocationType() string { + return fmt.Sprintf(` +data "aws_availability_zones" "available" { + state = "available" +} + +data "aws_ec2_instance_type_offerings" "test" { + filter { + name = "location" + values = ["${data.aws_availability_zones.available.names[0]}"] + } + + location_type = "availability-zone" +} +`) +} diff --git a/aws/provider.go b/aws/provider.go index abe8391c72f..f627e338ecc 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -195,6 +195,8 @@ func Provider() terraform.ResourceProvider { "aws_ebs_snapshot": dataSourceAwsEbsSnapshot(), "aws_ebs_snapshot_ids": dataSourceAwsEbsSnapshotIds(), "aws_ebs_volume": dataSourceAwsEbsVolume(), + "aws_ec2_instance_type_offering": dataSourceAwsEc2InstanceTypeOffering(), + "aws_ec2_instance_type_offerings": dataSourceAwsEc2InstanceTypeOfferings(), "aws_ec2_transit_gateway": dataSourceAwsEc2TransitGateway(), "aws_ec2_transit_gateway_dx_gateway_attachment": dataSourceAwsEc2TransitGatewayDxGatewayAttachment(), "aws_ec2_transit_gateway_route_table": dataSourceAwsEc2TransitGatewayRouteTable(), diff --git a/website/aws.erb b/website/aws.erb index ac47078a07d..a8ff1aa4d49 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1035,6 +1035,12 @@