From e524603d3f1bc543c1bc5678794ee07bbbb0d23e Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Wed, 24 Aug 2016 11:08:46 +0100 Subject: [PATCH] provider/aws: AWS SpotFleet Requests now works with Subnets and AZs (#8320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * provider/aws: Change Spot Fleet Request to allow a combination of subnet_id and availability_zone Also added a complete set of tests that reflect all of the use cases that Amazon document http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-fleet-examples.html It is important to note there that Terraform will be suggesting that users create multiple launch configurations rather than AWS's version of combing values into CSV based parameters. This will ensure that we are able to enforce the correct state Also note that `associate_public_ip_address` now defaults to `false` - a migration has been included in this PR to migration users of this functionality. This needs to be noted in the changelog. The last part of changing functionality here is waiting for the state of the request to become `active`. Before we get to this state, we cannot guarantee that Amazon have accepted the request or it could have failed validation. ``` % make testacc TEST=./builtin/providers/aws % TESTARGS='-run=TestAccAWSSpotFleetRequest_' % 2 ↵ ==> Checking that code complies with gofmt requirements... /Users/stacko/Code/go/bin/stringer go generate $(go list ./... | grep -v /terraform/vendor/) 2016/08/22 15:44:21 Generated command/internal_plugin_list.go TF_ACC=1 go test ./builtin/providers/aws -v -run=TestAccAWSSpotFleetRequest_ -timeout 120m === RUN TestAccAWSSpotFleetRequest_changePriceForcesNewRequest --- PASS: TestAccAWSSpotFleetRequest_changePriceForcesNewRequest (133.90s) === RUN TestAccAWSSpotFleetRequest_lowestPriceAzOrSubnetInRegion --- PASS: TestAccAWSSpotFleetRequest_lowestPriceAzOrSubnetInRegion (76.67s) === RUN TestAccAWSSpotFleetRequest_lowestPriceAzInGivenList --- PASS: TestAccAWSSpotFleetRequest_lowestPriceAzInGivenList (75.22s) === RUN TestAccAWSSpotFleetRequest_lowestPriceSubnetInGivenList --- PASS: TestAccAWSSpotFleetRequest_lowestPriceSubnetInGivenList (96.95s) === RUN TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameAz --- PASS: TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameAz (74.44s) === RUN TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameSubnet --- PASS: TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameSubnet (97.82s) === RUN TestAccAWSSpotFleetRequest_overriddingSpotPrice --- PASS: TestAccAWSSpotFleetRequest_overriddingSpotPrice (76.22s) === RUN TestAccAWSSpotFleetRequest_diversifiedAllocation --- PASS: TestAccAWSSpotFleetRequest_diversifiedAllocation (79.81s) === RUN TestAccAWSSpotFleetRequest_withWeightedCapacity --- PASS: TestAccAWSSpotFleetRequest_withWeightedCapacity (77.15s) === RUN TestAccAWSSpotFleetRequest_CannotUseEmptyKeyName --- PASS: TestAccAWSSpotFleetRequest_CannotUseEmptyKeyName (0.00s) PASS ok github.com/hashicorp/terraform/builtin/providers/aws 788.184s ``` * Update resource_aws_spot_fleet_request.go --- .../aws/resource_aws_spot_fleet_request.go | 193 +++--- ...resource_aws_spot_fleet_request_migrate.go | 33 + ...rce_aws_spot_fleet_request_migrate_test.go | 43 ++ .../resource_aws_spot_fleet_request_test.go | 641 ++++++++++++++++-- .../aws/r/spot_fleet_request.html.markdown | 25 + 5 files changed, 804 insertions(+), 131 deletions(-) create mode 100644 builtin/providers/aws/resource_aws_spot_fleet_request_migrate.go create mode 100644 builtin/providers/aws/resource_aws_spot_fleet_request_migrate_test.go diff --git a/builtin/providers/aws/resource_aws_spot_fleet_request.go b/builtin/providers/aws/resource_aws_spot_fleet_request.go index 669a8239a..dfb0056ef 100644 --- a/builtin/providers/aws/resource_aws_spot_fleet_request.go +++ b/builtin/providers/aws/resource_aws_spot_fleet_request.go @@ -25,6 +25,9 @@ func resourceAwsSpotFleetRequest() *schema.Resource { Delete: resourceAwsSpotFleetRequestDelete, Update: resourceAwsSpotFleetRequestUpdate, + SchemaVersion: 1, + MigrateState: resourceAwsSpotFleetRequestMigrateState, + Schema: map[string]*schema.Schema{ "iam_fleet_role": &schema.Schema{ Type: schema.TypeString, @@ -49,7 +52,7 @@ func resourceAwsSpotFleetRequest() *schema.Resource { "associate_public_ip_address": &schema.Schema{ Type: schema.TypeBool, Optional: true, - Default: true, + Default: false, }, "ebs_block_device": &schema.Schema{ Type: schema.TypeSet, @@ -192,7 +195,6 @@ func resourceAwsSpotFleetRequest() *schema.Resource { Type: schema.TypeBool, Optional: true, }, - // "network_interface_set" "placement_group": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -204,12 +206,6 @@ func resourceAwsSpotFleetRequest() *schema.Resource { Optional: true, ForceNew: true, }, - "subnet_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, "user_data": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -229,9 +225,16 @@ func resourceAwsSpotFleetRequest() *schema.Resource { Optional: true, ForceNew: true, }, + "subnet_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, "availability_zone": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, }, @@ -291,19 +294,16 @@ func resourceAwsSpotFleetRequest() *schema.Resource { func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{}) (*ec2.SpotFleetLaunchSpecification, error) { conn := meta.(*AWSClient).ec2conn - _, hasSubnet := d["subnet_id"] - _, hasAZ := d["availability_zone"] - if !hasAZ && !hasSubnet { - return nil, fmt.Errorf("LaunchSpecification must include a subnet_id or an availability_zone") - } - opts := &ec2.SpotFleetLaunchSpecification{ ImageId: aws.String(d["ami"].(string)), InstanceType: aws.String(d["instance_type"].(string)), SpotPrice: aws.String(d["spot_price"].(string)), - Placement: &ec2.SpotPlacement{ - AvailabilityZone: aws.String(d["availability_zone"].(string)), - }, + } + + if v, ok := d["availability_zone"]; ok { + opts.Placement = &ec2.SpotPlacement{ + AvailabilityZone: aws.String(v.(string)), + } } if v, ok := d["ebs_optimized"]; ok { @@ -327,70 +327,6 @@ func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{ base64.StdEncoding.EncodeToString([]byte(v.(string)))) } - // check for non-default Subnet, and cast it to a String - subnet, hasSubnet := d["subnet_id"] - subnetID := subnet.(string) - - var associatePublicIPAddress bool - if v, ok := d["associate_public_ip_address"]; ok { - associatePublicIPAddress = v.(bool) - } - - var groups []*string - if v, ok := d["security_groups"]; ok { - // Security group names. - // For a nondefault VPC, you must use security group IDs instead. - // See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RunInstances.html - sgs := v.(*schema.Set).List() - if len(sgs) > 0 && hasSubnet { - log.Printf("[WARN] Deprecated. Attempting to use 'security_groups' within a VPC instance. Use 'vpc_security_group_ids' instead.") - } - for _, v := range sgs { - str := v.(string) - groups = append(groups, aws.String(str)) - } - } - - if hasSubnet && associatePublicIPAddress { - // If we have a non-default VPC / Subnet specified, we can flag - // AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided. - // You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise - // you get: Network interfaces and an instance-level subnet ID may not be specified on the same request - // You also need to attach Security Groups to the NetworkInterface instead of the instance, - // to avoid: Network interfaces and an instance-level security groups may not be specified on - // the same request - ni := &ec2.InstanceNetworkInterfaceSpecification{ - AssociatePublicIpAddress: aws.Bool(associatePublicIPAddress), - DeviceIndex: aws.Int64(int64(0)), - SubnetId: aws.String(subnetID), - Groups: groups, - } - - if v, ok := d["private_ip"]; ok { - ni.PrivateIpAddress = aws.String(v.(string)) - } - - if v := d["vpc_security_group_ids"].(*schema.Set); v.Len() > 0 { - for _, v := range v.List() { - ni.Groups = append(ni.Groups, aws.String(v.(string))) - } - } - - opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni} - } else { - if subnetID != "" { - opts.SubnetId = aws.String(subnetID) - } - - if v, ok := d["vpc_security_group_ids"]; ok { - if s := v.(*schema.Set); s.Len() > 0 { - for _, v := range s.List() { - opts.SecurityGroups = append(opts.SecurityGroups, &ec2.GroupIdentifier{GroupId: aws.String(v.(string))}) - } - } - } - } - if v, ok := d["key_name"]; ok { opts.KeyName = aws.String(v.(string)) } @@ -403,6 +339,51 @@ func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{ opts.WeightedCapacity = aws.Float64(wc) } + var groups []*string + if v, ok := d["security_groups"]; ok { + sgs := v.(*schema.Set).List() + for _, v := range sgs { + str := v.(string) + groups = append(groups, aws.String(str)) + } + } + + var groupIds []*string + if v, ok := d["vpc_security_group_ids"]; ok { + if s := v.(*schema.Set); s.Len() > 0 { + for _, v := range s.List() { + opts.SecurityGroups = append(opts.SecurityGroups, &ec2.GroupIdentifier{GroupId: aws.String(v.(string))}) + groupIds = append(groupIds, aws.String(v.(string))) + } + } + } + + subnetId, hasSubnetId := d["subnet_id"] + if hasSubnetId { + opts.SubnetId = aws.String(subnetId.(string)) + } + + associatePublicIpAddress, hasPublicIpAddress := d["associate_public_ip_address"] + if hasPublicIpAddress && associatePublicIpAddress.(bool) == true && hasSubnetId { + + // If we have a non-default VPC / Subnet specified, we can flag + // AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided. + // You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise + // you get: Network interfaces and an instance-level subnet ID may not be specified on the same request + // You also need to attach Security Groups to the NetworkInterface instead of the instance, + // to avoid: Network interfaces and an instance-level security groups may not be specified on + // the same request + ni := &ec2.InstanceNetworkInterfaceSpecification{ + AssociatePublicIpAddress: aws.Bool(true), + DeviceIndex: aws.Int64(int64(0)), + SubnetId: aws.String(subnetId.(string)), + Groups: groupIds, + } + + opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni} + opts.SubnetId = aws.String("") + } + blockDevices, err := readSpotFleetBlockDeviceMappingsFromConfig(d, conn) if err != nil { return nil, err @@ -617,9 +598,52 @@ func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) d.SetId(*resp.SpotFleetRequestId) + log.Printf("[INFO] Spot Fleet Request ID: %s", d.Id()) + log.Println("[INFO] Waiting for Spot Fleet Request to be active") + stateConf := &resource.StateChangeConf{ + Pending: []string{"submitted"}, + Target: []string{"active"}, + Refresh: resourceAwsSpotFleetRequestStateRefreshFunc(d, meta), + Timeout: 10 * time.Minute, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return err + } + return resourceAwsSpotFleetRequestRead(d, meta) } +func resourceAwsSpotFleetRequestStateRefreshFunc(d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + conn := meta.(*AWSClient).ec2conn + req := &ec2.DescribeSpotFleetRequestsInput{ + SpotFleetRequestIds: []*string{aws.String(d.Id())}, + } + resp, err := conn.DescribeSpotFleetRequests(req) + + if err != nil { + log.Printf("Error on retrieving Spot Fleet Request when waiting: %s", err) + return nil, "", nil + } + + if resp == nil { + return nil, "", nil + } + + if len(resp.SpotFleetRequestConfigs) == 0 { + return nil, "", nil + } + + spotFleetRequest := resp.SpotFleetRequestConfigs[0] + + return spotFleetRequest, *spotFleetRequest.SpotFleetRequestState, nil + } +} + func resourceAwsSpotFleetRequestRead(d *schema.ResourceData, meta interface{}) error { // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSpotFleetRequests.html conn := meta.(*AWSClient).ec2conn @@ -773,7 +797,7 @@ func launchSpecToMap( } if l.WeightedCapacity != nil { - m["weighted_capacity"] = fmt.Sprintf("%.3f", aws.Float64Value(l.WeightedCapacity)) + m["weighted_capacity"] = strconv.FormatFloat(*l.WeightedCapacity, 'f', 0, 64) } // m["security_groups"] = securityGroupsToSet(l.SecutiryGroups) @@ -941,9 +965,10 @@ func hashLaunchSpecification(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) buf.WriteString(fmt.Sprintf("%s-", m["ami"].(string))) - if m["availability_zone"] != nil && m["availability_zone"] != "" { + if m["availability_zone"] != "" { buf.WriteString(fmt.Sprintf("%s-", m["availability_zone"].(string))) - } else if m["subnet_id"] != nil && m["subnet_id"] != "" { + } + if m["subnet_id"] != "" { buf.WriteString(fmt.Sprintf("%s-", m["subnet_id"].(string))) } buf.WriteString(fmt.Sprintf("%s-", m["instance_type"].(string))) diff --git a/builtin/providers/aws/resource_aws_spot_fleet_request_migrate.go b/builtin/providers/aws/resource_aws_spot_fleet_request_migrate.go new file mode 100644 index 000000000..dea0a32e8 --- /dev/null +++ b/builtin/providers/aws/resource_aws_spot_fleet_request_migrate.go @@ -0,0 +1,33 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/terraform" +) + +func resourceAwsSpotFleetRequestMigrateState( + v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + switch v { + case 0: + log.Println("[INFO] Found AWS Spot Fleet Request State v0; migrating to v1") + return migrateSpotFleetRequestV0toV1(is) + default: + return is, fmt.Errorf("Unexpected schema version: %d", v) + } +} + +func migrateSpotFleetRequestV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { + if is.Empty() { + log.Println("[DEBUG] Empty Spot Fleet Request State; nothing to migrate.") + return is, nil + } + + log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes) + + is.Attributes["associate_public_ip_address"] = "false" + + log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes) + return is, nil +} diff --git a/builtin/providers/aws/resource_aws_spot_fleet_request_migrate_test.go b/builtin/providers/aws/resource_aws_spot_fleet_request_migrate_test.go new file mode 100644 index 000000000..28e750f59 --- /dev/null +++ b/builtin/providers/aws/resource_aws_spot_fleet_request_migrate_test.go @@ -0,0 +1,43 @@ +package aws + +import ( + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestAWSSpotFleetRequestMigrateState(t *testing.T) { + cases := map[string]struct { + StateVersion int + ID string + Attributes map[string]string + Expected string + Meta interface{} + }{ + "v0_1": { + StateVersion: 0, + ID: "some_id", + Attributes: map[string]string{ + "associate_public_ip_address": "true", + }, + Expected: "false", + }, + } + + for tn, tc := range cases { + is := &terraform.InstanceState{ + ID: tc.ID, + Attributes: tc.Attributes, + } + is, err := resourceAwsSpotFleetRequestMigrateState( + tc.StateVersion, is, tc.Meta) + + if err != nil { + t.Fatalf("bad: %s, err: %#v", tn, err) + } + + if is.Attributes["associate_public_ip_address"] != tc.Expected { + t.Fatalf("bad Spot Fleet Request Migrate: %s\n\n expected: %s", is.Attributes["associate_public_ip_address"], tc.Expected) + } + } +} diff --git a/builtin/providers/aws/resource_aws_spot_fleet_request_test.go b/builtin/providers/aws/resource_aws_spot_fleet_request_test.go index ccb240542..020fb38fe 100644 --- a/builtin/providers/aws/resource_aws_spot_fleet_request_test.go +++ b/builtin/providers/aws/resource_aws_spot_fleet_request_test.go @@ -3,7 +3,6 @@ package aws import ( "encoding/base64" "fmt" - "regexp" "testing" "github.com/aws/aws-sdk-go/aws" @@ -12,7 +11,46 @@ import ( "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSSpotFleetRequest_basic(t *testing.T) { +func TestAccAWSSpotFleetRequest_changePriceForcesNewRequest(t *testing.T) { + var before, after ec2.SpotFleetRequestConfig + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSSpotFleetRequestConfig, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists( + "aws_spot_fleet_request.foo", &before), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_request_state", "active"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_price", "0.005"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.#", "1"), + ), + }, + resource.TestStep{ + Config: testAccAWSSpotFleetRequestConfigChangeSpotBidPrice, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists( + "aws_spot_fleet_request.foo", &after), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_request_state", "active"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.#", "1"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_price", "0.01"), + testAccCheckAWSSpotFleetRequestConfigRecreated(t, &before, &after), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_lowestPriceAzOrSubnetInRegion(t *testing.T) { var sfr ec2.SpotFleetRequestConfig resource.Test(t, resource.TestCase{ @@ -22,33 +60,20 @@ func TestAccAWSSpotFleetRequest_basic(t *testing.T) { Steps: []resource.TestStep{ resource.TestStep{ Config: testAccAWSSpotFleetRequestConfig, - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( testAccCheckAWSSpotFleetRequestExists( "aws_spot_fleet_request.foo", &sfr), - testAccCheckAWSSpotFleetRequestAttributes(&sfr), resource.TestCheckResourceAttr( "aws_spot_fleet_request.foo", "spot_request_state", "active"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.#", "1"), ), }, }, }) } -func TestAccAWSSpotFleetRequest_brokenLaunchSpecification(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, - Steps: []resource.TestStep{ - resource.TestStep{ - Config: testAccAWSSpotFleetRequestConfigBroken, - ExpectError: regexp.MustCompile("LaunchSpecification must include a subnet_id or an availability_zone"), - }, - }, - }) -} - -func TestAccAWSSpotFleetRequest_launchConfiguration(t *testing.T) { +func TestAccAWSSpotFleetRequest_lowestPriceAzInGivenList(t *testing.T) { var sfr ec2.SpotFleetRequestConfig resource.Test(t, resource.TestCase{ @@ -57,13 +82,184 @@ func TestAccAWSSpotFleetRequest_launchConfiguration(t *testing.T) { CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccAWSSpotFleetRequestWithAdvancedLaunchSpecConfig, - Check: resource.ComposeTestCheckFunc( + Config: testAccAWSSpotFleetRequestConfigWithAzs, + Check: resource.ComposeAggregateTestCheckFunc( testAccCheckAWSSpotFleetRequestExists( "aws_spot_fleet_request.foo", &sfr), - testAccCheckAWSSpotFleetRequest_LaunchSpecAttributes(&sfr), resource.TestCheckResourceAttr( "aws_spot_fleet_request.foo", "spot_request_state", "active"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.#", "2"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.1590006269.availability_zone", "us-west-2a"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.3809475891.availability_zone", "us-west-2b"), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_lowestPriceSubnetInGivenList(t *testing.T) { + var sfr ec2.SpotFleetRequestConfig + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSSpotFleetRequestConfigWithSubnet, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists( + "aws_spot_fleet_request.foo", &sfr), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_request_state", "active"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.#", "2"), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameAz(t *testing.T) { + var sfr ec2.SpotFleetRequestConfig + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSSpotFleetRequestConfigMultipleInstanceTypesinSameAz, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists( + "aws_spot_fleet_request.foo", &sfr), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_request_state", "active"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.#", "2"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.1590006269.instance_type", "m1.small"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.1590006269.availability_zone", "us-west-2a"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.3079734941.instance_type", "m3.large"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.3079734941.availability_zone", "us-west-2a"), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameSubnet(t *testing.T) { + var sfr ec2.SpotFleetRequestConfig + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSSpotFleetRequestConfigMultipleInstanceTypesinSameSubnet, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists( + "aws_spot_fleet_request.foo", &sfr), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_request_state", "active"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.#", "2"), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_overriddingSpotPrice(t *testing.T) { + var sfr ec2.SpotFleetRequestConfig + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSSpotFleetRequestConfigOverridingSpotPrice, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists( + "aws_spot_fleet_request.foo", &sfr), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_request_state", "active"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_price", "0.005"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.#", "2"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.522395050.spot_price", "0.01"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.522395050.instance_type", "m3.large"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.1590006269.spot_price", ""), //there will not be a value here since it's not overriding + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.1590006269.instance_type", "m1.small"), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_diversifiedAllocation(t *testing.T) { + var sfr ec2.SpotFleetRequestConfig + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSSpotFleetRequestConfigDiversifiedAllocation, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists( + "aws_spot_fleet_request.foo", &sfr), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_request_state", "active"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.#", "3"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "allocation_strategy", "diversified"), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_withWeightedCapacity(t *testing.T) { + var sfr ec2.SpotFleetRequestConfig + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSSpotFleetRequestConfigWithWeightedCapacity, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists( + "aws_spot_fleet_request.foo", &sfr), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_request_state", "active"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.#", "2"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.2325690000.weighted_capacity", "3"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.2325690000.instance_type", "r3.large"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.3079734941.weighted_capacity", "6"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.3079734941.instance_type", "m3.large"), ), }, }, @@ -77,6 +273,16 @@ func TestAccAWSSpotFleetRequest_CannotUseEmptyKeyName(t *testing.T) { } } +func testAccCheckAWSSpotFleetRequestConfigRecreated(t *testing.T, + before, after *ec2.SpotFleetRequestConfig) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before.SpotFleetRequestId == after.SpotFleetRequestId { + t.Fatalf("Expected change of Spot Fleet Request IDs, but both were %v", before.SpotFleetRequestId) + } + return nil + } +} + func testAccCheckAWSSpotFleetRequestExists( n string, sfr *ec2.SpotFleetRequestConfig) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -110,19 +316,6 @@ func testAccCheckAWSSpotFleetRequestExists( } } -func testAccCheckAWSSpotFleetRequestAttributes( - sfr *ec2.SpotFleetRequestConfig) resource.TestCheckFunc { - return func(s *terraform.State) error { - if *sfr.SpotFleetRequestConfig.SpotPrice != "0.005" { - return fmt.Errorf("Unexpected spot price: %s", *sfr.SpotFleetRequestConfig.SpotPrice) - } - if *sfr.SpotFleetRequestState != "active" { - return fmt.Errorf("Unexpected request state: %s", *sfr.SpotFleetRequestState) - } - return nil - } -} - func testAccCheckAWSSpotFleetRequest_LaunchSpecAttributes( sfr *ec2.SpotFleetRequestConfig) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -213,17 +406,63 @@ resource "aws_spot_fleet_request" "foo" { spot_price = "0.005" target_capacity = 2 valid_until = "2019-11-04T20:44:20Z" + terminate_instances_with_expiration = true launch_specification { instance_type = "m1.small" ami = "ami-d06a90b0" key_name = "${aws_key_pair.debugging.key_name}" - availability_zone = "us-west-2a" } depends_on = ["aws_iam_policy_attachment.test-attach"] } ` -const testAccAWSSpotFleetRequestConfigBroken = ` +const testAccAWSSpotFleetRequestConfigChangeSpotBidPrice = ` +resource "aws_key_pair" "debugging" { + key_name = "tmp-key" + public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com" +} + +resource "aws_iam_policy_attachment" "test-attach" { + name = "test-attachment" + roles = ["${aws_iam_role.test-role.name}"] + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetRole" +} + +resource "aws_iam_role" "test-role" { + name = "test-role" + assume_role_policy = < **NOTE:** Terraform does not support the functionality where multiple `subnet_id` or `availability_zone` parameters can be specified in the same +launch configuration block. If you want to specify multiple values, then separate launch configuration blocks should be used: + +``` +resource "aws_spot_fleet_request" "foo" { + iam_fleet_role = "arn:aws:iam::12345678:role/spot-fleet" + spot_price = "0.005" + target_capacity = 2 + valid_until = "2019-11-04T20:44:20Z" + launch_specification { + instance_type = "m1.small" + ami = "ami-d06a90b0" + key_name = "my-key" + availability_zone = "us-west-2a" + } + launch_specification { + instance_type = "m3.large" + ami = "ami-d06a90b0" + key_name = "my-key" + availability_zone = "us-west-2a" + } + depends_on = ["aws_iam_policy_attachment.test-attach"] +} +``` + ## Argument Reference Most of these arguments directly correspond to the