diff --git a/builtin/providers/aws/resource_aws_spot_fleet_request.go b/builtin/providers/aws/resource_aws_spot_fleet_request.go index 669a8239aafa..04a466e5c774 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,31 +327,45 @@ 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) + if v, ok := d["key_name"]; ok { + opts.KeyName = aws.String(v.(string)) + } - var associatePublicIPAddress bool - if v, ok := d["associate_public_ip_address"]; ok { - associatePublicIPAddress = v.(bool) + if v, ok := d["weighted_capacity"]; ok && v != "" { + wc, err := strconv.ParseFloat(v.(string), 64) + if err != nil { + return nil, err + } + opts.WeightedCapacity = aws.Float64(wc) } 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 { + 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 @@ -360,47 +374,14 @@ func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{ // 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), + AssociatePublicIpAddress: aws.Bool(true), 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))) - } + SubnetId: aws.String(subnetId.(string)), + Groups: groupIds, } 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)) - } - - if v, ok := d["weighted_capacity"]; ok && v != "" { - wc, err := strconv.ParseFloat(v.(string), 64) - if err != nil { - return nil, err - } - opts.WeightedCapacity = aws.Float64(wc) + opts.SubnetId = aws.String("") } blockDevices, err := readSpotFleetBlockDeviceMappingsFromConfig(d, conn) @@ -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"] = floatToString(*l.WeightedCapacity) } // 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))) @@ -959,3 +984,7 @@ func hashEbsBlockDevice(v interface{}) int { buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string))) return hashcode.String(buf.String()) } + +func floatToString(input float64) string { + return strconv.FormatFloat(input, 'f', 0, 64) +} 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 000000000000..dea0a32e841f --- /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 000000000000..28e750f59950 --- /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 ccb240542d48..020fb38fe025 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,182 @@ 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), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "spot_request_state", "active"), + resource.TestCheckResourceAttr( + "aws_spot_fleet_request.foo", "launch_specification.#", "1"), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_lowestPriceAzInGivenList(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: testAccAWSSpotFleetRequestConfigWithAzs, + 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.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), - testAccCheckAWSSpotFleetRequestAttributes(&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_brokenLaunchSpecification(t *testing.T) { +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: testAccAWSSpotFleetRequestConfigBroken, - ExpectError: regexp.MustCompile("LaunchSpecification must include a subnet_id or an availability_zone"), + 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_launchConfiguration(t *testing.T) { +func TestAccAWSSpotFleetRequest_withWeightedCapacity(t *testing.T) { var sfr ec2.SpotFleetRequestConfig resource.Test(t, resource.TestCase{ @@ -57,13 +244,22 @@ func TestAccAWSSpotFleetRequest_launchConfiguration(t *testing.T) { CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccAWSSpotFleetRequestWithAdvancedLaunchSpecConfig, - Check: resource.ComposeTestCheckFunc( + Config: testAccAWSSpotFleetRequestConfigWithWeightedCapacity, + 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.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