From 27e8bc93e592811c9e596e4303c414abc9b24b08 Mon Sep 17 00:00:00 2001 From: Ilia Lazebnik Date: Tue, 28 Apr 2020 07:44:08 +0300 Subject: [PATCH] resource/aws_spot_fleet_request: Launch Template Support (#12732) Output from acceptance testing: ``` --- PASS: TestAccAWSSpotFleetRequest_associatePublicIpAddress (416.10s) --- PASS: TestAccAWSSpotFleetRequest_basic (346.62s) --- PASS: TestAccAWSSpotFleetRequest_changePriceForcesNewRequest (612.73s) --- PASS: TestAccAWSSpotFleetRequest_disappears (229.25s) --- PASS: TestAccAWSSpotFleetRequest_diversifiedAllocation (351.50s) --- PASS: TestAccAWSSpotFleetRequest_fleetType (286.87s) --- PASS: TestAccAWSSpotFleetRequest_iamInstanceProfileArn (285.22s) --- PASS: TestAccAWSSpotFleetRequest_instanceInterruptionBehavior (284.16s) --- PASS: TestAccAWSSpotFleetRequest_LaunchSpecification_EbsBlockDevice_KmsKeyId (142.74s) --- PASS: TestAccAWSSpotFleetRequest_LaunchSpecification_RootBlockDevice_KmsKeyId (145.99s) --- PASS: TestAccAWSSpotFleetRequest_launchSpecToLaunchTemplate (540.97s) --- PASS: TestAccAWSSpotFleetRequest_launchTemplate (284.34s) --- PASS: TestAccAWSSpotFleetRequest_launchTemplate_multiple (213.22s) --- PASS: TestAccAWSSpotFleetRequest_launchTemplateToLaunchSpec (542.07s) --- PASS: TestAccAWSSpotFleetRequest_launchTemplateWithOverrides (216.46s) --- PASS: TestAccAWSSpotFleetRequest_lowestPriceAzInGivenList (273.30s) --- PASS: TestAccAWSSpotFleetRequest_lowestPriceAzOrSubnetInRegion (345.74s) --- PASS: TestAccAWSSpotFleetRequest_lowestPriceSubnetInGivenList (285.52s) --- PASS: TestAccAWSSpotFleetRequest_multipleInstancePools (351.19s) --- PASS: TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameAz (337.09s) --- PASS: TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameSubnet (216.98s) --- PASS: TestAccAWSSpotFleetRequest_overriddingSpotPrice (320.64s) --- PASS: TestAccAWSSpotFleetRequest_placementTenancyAndGroup (63.07s) --- PASS: TestAccAWSSpotFleetRequest_tags (354.51s) --- PASS: TestAccAWSSpotFleetRequest_updateExcessCapacityTerminationPolicy (542.49s) --- PASS: TestAccAWSSpotFleetRequest_updateTargetCapacity (828.53s) --- PASS: TestAccAWSSpotFleetRequest_withEBSDisk (279.53s) --- PASS: TestAccAWSSpotFleetRequest_WithELBs (268.50s) --- PASS: TestAccAWSSpotFleetRequest_withoutSpotPrice (350.86s) --- PASS: TestAccAWSSpotFleetRequest_withTags (282.52s) --- PASS: TestAccAWSSpotFleetRequest_WithTargetGroups (410.53s) --- PASS: TestAccAWSSpotFleetRequest_withWeightedCapacity (289.97s) ``` --- aws/resource_aws_spot_fleet_request.go | 338 +++++++++++- aws/resource_aws_spot_fleet_request_test.go | 497 ++++++++++++++---- .../docs/r/spot_fleet_request.html.markdown | 107 +++- 3 files changed, 817 insertions(+), 125 deletions(-) diff --git a/aws/resource_aws_spot_fleet_request.go b/aws/resource_aws_spot_fleet_request.go index 4f33c33e88e..110198d2ead 100644 --- a/aws/resource_aws_spot_fleet_request.go +++ b/aws/resource_aws_spot_fleet_request.go @@ -54,7 +54,7 @@ func resourceAwsSpotFleetRequest() *schema.Resource { // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SpotFleetLaunchSpecification.html "launch_specification": { Type: schema.TypeSet, - Required: true, + Optional: true, ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -309,9 +309,92 @@ func resourceAwsSpotFleetRequest() *schema.Resource { }, }, }, - Set: hashLaunchSpecification, + Set: hashLaunchSpecification, + ExactlyOneOf: []string{"launch_specification", "launch_template_config"}, + }, + "launch_template_config": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + ExactlyOneOf: []string{"launch_specification", "launch_template_config"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "launch_template_specification": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateLaunchTemplateId, + }, + "name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateLaunchTemplateName, + }, + "version": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + }, + }, + }, + "overrides": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "availability_zone": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "instance_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "spot_price": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "subnet_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "weighted_capacity": { + Type: schema.TypeFloat, + Optional: true, + Computed: true, + ForceNew: true, + }, + "priority": { + Type: schema.TypeFloat, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + }, + Set: hashLaunchTemplateOverrides, + }, + }, + }, }, - // Everything on a spot fleet is ForceNew except target_capacity + // Everything on a spot fleet is ForceNew except target_capacity and excess_capacity_termination_policy, + // see https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#ModifySpotFleetRequestInput "target_capacity": { Type: schema.TypeInt, Required: true, @@ -661,12 +744,12 @@ func readSpotFleetBlockDeviceMappingsFromConfig( func buildAwsSpotFleetLaunchSpecifications( d *schema.ResourceData, meta interface{}) ([]*ec2.SpotFleetLaunchSpecification, error) { - user_specs := d.Get("launch_specification").(*schema.Set).List() - specs := make([]*ec2.SpotFleetLaunchSpecification, len(user_specs)) - for i, user_spec := range user_specs { - user_spec_map := user_spec.(map[string]interface{}) + userSpecs := d.Get("launch_specification").(*schema.Set).List() + specs := make([]*ec2.SpotFleetLaunchSpecification, len(userSpecs)) + for i, userSpec := range userSpecs { + userSpecMap := userSpec.(map[string]interface{}) // panic: interface conversion: interface {} is map[string]interface {}, not *schema.ResourceData - opts, err := buildSpotFleetLaunchSpecification(user_spec_map, meta) + opts, err := buildSpotFleetLaunchSpecification(userSpecMap, meta) if err != nil { return nil, err } @@ -676,19 +759,93 @@ func buildAwsSpotFleetLaunchSpecifications( return specs, nil } +func buildLaunchTemplateConfigs(d *schema.ResourceData) ([]*ec2.LaunchTemplateConfig, error) { + launchTemplateConfigs := d.Get("launch_template_config").(*schema.Set) + configs := make([]*ec2.LaunchTemplateConfig, 0) + + for _, launchTemplateConfig := range launchTemplateConfigs.List() { + + ltc := &ec2.LaunchTemplateConfig{} + + ltcMap := launchTemplateConfig.(map[string]interface{}) + + //launch template spec + if v, ok := ltcMap["launch_template_specification"]; ok { + vL := v.([]interface{}) + lts := vL[0].(map[string]interface{}) + + flts := &ec2.FleetLaunchTemplateSpecification{} + + if v, ok := lts["id"].(string); ok && v != "" { + flts.LaunchTemplateId = aws.String(v) + } + + if v, ok := lts["name"].(string); ok && v != "" { + flts.LaunchTemplateName = aws.String(v) + } + + if v, ok := lts["version"].(string); ok && v != "" { + flts.Version = aws.String(v) + } + + ltc.LaunchTemplateSpecification = flts + + } + + if v, ok := ltcMap["overrides"]; ok && v.(*schema.Set).Len() > 0 { + vL := v.(*schema.Set).List() + overrides := make([]*ec2.LaunchTemplateOverrides, 0) + + for _, v := range vL { + ors := v.(map[string]interface{}) + lto := &ec2.LaunchTemplateOverrides{} + + if v, ok := ors["availability_zone"].(string); ok && v != "" { + lto.AvailabilityZone = aws.String(v) + } + + if v, ok := ors["instance_type"].(string); ok && v != "" { + lto.InstanceType = aws.String(v) + } + + if v, ok := ors["spot_price"].(string); ok && v != "" { + lto.SpotPrice = aws.String(v) + } + + if v, ok := ors["subnet_id"].(string); ok && v != "" { + lto.SubnetId = aws.String(v) + } + + if v, ok := ors["weighted_capacity"].(float64); ok && v > 0 { + lto.WeightedCapacity = aws.Float64(v) + } + + if v, ok := ors["priority"].(float64); ok { + lto.Priority = aws.Float64(v) + } + + overrides = append(overrides, lto) + } + + ltc.Overrides = overrides + } + + configs = append(configs, ltc) + } + + return configs, nil +} + func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) error { // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RequestSpotFleet.html conn := meta.(*AWSClient).ec2conn - launch_specs, err := buildAwsSpotFleetLaunchSpecifications(d, meta) - if err != nil { - return err - } + _, launchSpecificationOk := d.GetOk("launch_specification") + _, launchTemplateConfigsOk := d.GetOk("launch_template_config") // http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-SpotFleetRequestConfigData spotFleetConfig := &ec2.SpotFleetRequestConfigData{ IamFleetRole: aws.String(d.Get("iam_fleet_role").(string)), - LaunchSpecifications: launch_specs, TargetCapacity: aws.Int64(int64(d.Get("target_capacity").(int))), ClientToken: aws.String(resource.UniqueId()), TerminateInstancesWithExpiration: aws.Bool(d.Get("terminate_instances_with_expiration").(bool)), @@ -698,6 +855,22 @@ func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) TagSpecifications: ec2TagSpecificationsFromMap(d.Get("tags").(map[string]interface{}), ec2.ResourceTypeSpotFleetRequest), } + if launchSpecificationOk { + launchSpecs, err := buildAwsSpotFleetLaunchSpecifications(d, meta) + if err != nil { + return err + } + spotFleetConfig.LaunchSpecifications = launchSpecs + } + + if launchTemplateConfigsOk { + launchTemplates, err := buildLaunchTemplateConfigs(d) + if err != nil { + return err + } + spotFleetConfig.LaunchTemplateConfigs = launchTemplates + } + if v, ok := d.GetOk("excess_capacity_termination_policy"); ok { spotFleetConfig.ExcessCapacityTerminationPolicy = aws.String(v.(string)) } @@ -717,22 +890,22 @@ func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) } if v, ok := d.GetOk("valid_from"); ok { - valid_from, err := time.Parse(time.RFC3339, v.(string)) + validFrom, err := time.Parse(time.RFC3339, v.(string)) if err != nil { return err } - spotFleetConfig.ValidFrom = aws.Time(valid_from) + spotFleetConfig.ValidFrom = aws.Time(validFrom) } if v, ok := d.GetOk("valid_until"); ok { - valid_until, err := time.Parse(time.RFC3339, v.(string)) + validUntil, err := time.Parse(time.RFC3339, v.(string)) if err != nil { return err } - spotFleetConfig.ValidUntil = aws.Time(valid_until) + spotFleetConfig.ValidUntil = aws.Time(validUntil) } else { - valid_until := time.Now().Add(24 * time.Hour) - spotFleetConfig.ValidUntil = aws.Time(valid_until) + validUntil := time.Now().Add(24 * time.Hour) + spotFleetConfig.ValidUntil = aws.Time(validUntil) } if v, ok := d.GetOk("load_balancers"); ok && v.(*schema.Set).Len() > 0 { @@ -776,9 +949,13 @@ func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) // Since IAM is eventually consistent, we retry creation as a newly created role may not // take effect immediately, resulting in an InvalidSpotFleetRequestConfig error var resp *ec2.RequestSpotFleetOutput - err = resource.Retry(10*time.Minute, func() *resource.RetryError { + err := resource.Retry(10*time.Minute, func() *resource.RetryError { + var err error resp, err = conn.RequestSpotFleet(spotFleetOpts) + if isAWSErr(err, "InvalidSpotFleetRequestConfig", "Duplicate: Parameter combination") { + return resource.NonRetryableError(fmt.Errorf("Error creating Spot fleet request: %s", err)) + } if isAWSErr(err, "InvalidSpotFleetRequestConfig", "") { return resource.RetryableError(fmt.Errorf("Error creating Spot fleet request, retrying: %s", err)) } @@ -804,7 +981,7 @@ func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) Pending: []string{ec2.BatchStateSubmitted}, Target: []string{ec2.BatchStateActive}, Refresh: resourceAwsSpotFleetRequestStateRefreshFunc(d, meta), - Timeout: 10 * time.Minute, + Timeout: d.Timeout(schema.TimeoutCreate), //10 * time.Minute, MinTimeout: 10 * time.Second, Delay: 30 * time.Second, } @@ -948,9 +1125,9 @@ func resourceAwsSpotFleetRequestRead(d *schema.ResourceData, meta interface{}) e // if the request is cancelled, then it is gone cancelledStates := map[string]bool{ - "cancelled": true, - "cancelled_running": true, - "cancelled_terminating": true, + ec2.BatchStateCancelled: true, + ec2.BatchStateCancelledRunning: true, + ec2.BatchStateCancelledTerminating: true, } if _, ok := cancelledStates[*sfr.SpotFleetRequestState]; ok { d.SetId("") @@ -1019,9 +1196,44 @@ func resourceAwsSpotFleetRequestRead(d *schema.ResourceData, meta interface{}) e return fmt.Errorf("error setting tags: %s", err) } + if len(config.LaunchTemplateConfigs) > 0 { + if err := d.Set("launch_template_config", flattenFleetLaunchTemplateConfig(config.LaunchTemplateConfigs)); err != nil { + return fmt.Errorf("error setting launch_template_config: %s", err) + } + } + return nil } +func flattenSpotFleetRequestLaunchTemplateOverrides(override *ec2.LaunchTemplateOverrides) map[string]interface{} { + m := make(map[string]interface{}) + + if override.AvailabilityZone != nil { + m["availability_zone"] = aws.StringValue(override.AvailabilityZone) + } + if override.InstanceType != nil { + m["instance_type"] = aws.StringValue(override.InstanceType) + } + + if override.SpotPrice != nil { + m["spot_price"] = aws.StringValue(override.SpotPrice) + } + + if override.SubnetId != nil { + m["subnet_id"] = aws.StringValue(override.SubnetId) + } + + if override.WeightedCapacity != nil { + m["weighted_capacity"] = aws.Float64Value(override.WeightedCapacity) + } + + if override.Priority != nil { + m["priority"] = aws.Float64Value(override.Priority) + } + + return m +} + func launchSpecsToSet(launchSpecs []*ec2.SpotFleetLaunchSpecification, conn *ec2.EC2) (*schema.Set, error) { specSet := &schema.Set{F: hashLaunchSpecification} for _, spec := range launchSpecs { @@ -1374,6 +1586,31 @@ func hashLaunchSpecification(v interface{}) int { return hashcode.String(buf.String()) } +func hashLaunchTemplateOverrides(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + if m["availability_zone"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["availability_zone"].(string))) + } + if m["subnet_id"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["subnet_id"].(string))) + } + if m["spot_price"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["spot_price"].(string))) + } + if m["instance_type"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["instance_type"].(string))) + } + if m["weighted_capacity"] != nil { + buf.WriteString(fmt.Sprintf("%f-", m["weighted_capacity"].(float64))) + } + if m["priority"] != nil { + buf.WriteString(fmt.Sprintf("%f-", m["priority"].(float64))) + } + + return hashcode.String(buf.String()) +} + func hashEbsBlockDevice(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) @@ -1385,3 +1622,56 @@ func hashEbsBlockDevice(v interface{}) int { } return hashcode.String(buf.String()) } + +func flattenFleetLaunchTemplateConfig(ltcs []*ec2.LaunchTemplateConfig) []map[string]interface{} { + result := make([]map[string]interface{}, 0) + + for _, ltc := range ltcs { + ltcRes := map[string]interface{}{} + + if ltc.LaunchTemplateSpecification != nil { + ltcRes["launch_template_specification"] = flattenFleetLaunchTemplateSpecification(ltc.LaunchTemplateSpecification) + } + + if ltc.Overrides != nil { + ltcRes["overrides"] = flattenLaunchTemplateOverrides(ltc.Overrides) + } + + result = append(result, ltcRes) + } + + return result +} + +func flattenFleetLaunchTemplateSpecification(flt *ec2.FleetLaunchTemplateSpecification) []map[string]interface{} { + attrs := map[string]interface{}{} + result := make([]map[string]interface{}, 0) + + // unlike autoscaling.LaunchTemplateConfiguration, FleetLaunchTemplateSpecs only return what was set + if flt.LaunchTemplateId != nil { + attrs["id"] = aws.StringValue(flt.LaunchTemplateId) + } + + if flt.LaunchTemplateName != nil { + attrs["name"] = aws.StringValue(flt.LaunchTemplateName) + } + + // version is returned only if it was previously set + if flt.Version != nil { + attrs["version"] = aws.StringValue(flt.Version) + } else { + attrs["version"] = nil + } + + result = append(result, attrs) + + return result +} + +func flattenLaunchTemplateOverrides(overrides []*ec2.LaunchTemplateOverrides) *schema.Set { + overrideSet := &schema.Set{F: hashLaunchTemplateOverrides} + for _, override := range overrides { + overrideSet.Add(flattenSpotFleetRequestLaunchTemplateOverrides(override)) + } + return overrideSet +} diff --git a/aws/resource_aws_spot_fleet_request_test.go b/aws/resource_aws_spot_fleet_request_test.go index 01e334e3b66..8ab04a02c0f 100644 --- a/aws/resource_aws_spot_fleet_request_test.go +++ b/aws/resource_aws_spot_fleet_request_test.go @@ -148,6 +148,151 @@ func TestAccAWSSpotFleetRequest_associatePublicIpAddress(t *testing.T) { }) } +func TestAccAWSSpotFleetRequest_launchTemplate(t *testing.T) { + var sfr ec2.SpotFleetRequestConfig + rName := acctest.RandString(10) + rInt := acctest.RandInt() + validUntil := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339) + resourceName := "aws_spot_fleet_request.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSpotFleetRequestLaunchTemplateConfig(rName, rInt, validUntil), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists(resourceName, &sfr), + resource.TestCheckResourceAttr(resourceName, "spot_request_state", "active"), + resource.TestCheckResourceAttr(resourceName, "launch_specification.#", "0"), + resource.TestCheckResourceAttr(resourceName, "launch_template_config.#", "1"), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_launchTemplate_multiple(t *testing.T) { + var sfr ec2.SpotFleetRequestConfig + rName := acctest.RandString(10) + rInt := acctest.RandInt() + validUntil := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339) + resourceName := "aws_spot_fleet_request.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSpotFleetRequestLaunchTemplateMultipleConfig(rName, rInt, validUntil), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists(resourceName, &sfr), + resource.TestCheckResourceAttr(resourceName, "spot_request_state", "active"), + resource.TestCheckResourceAttr(resourceName, "launch_specification.#", "0"), + resource.TestCheckResourceAttr(resourceName, "launch_template_config.#", "2"), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_launchTemplateWithOverrides(t *testing.T) { + var sfr ec2.SpotFleetRequestConfig + rName := acctest.RandString(10) + rInt := acctest.RandInt() + validUntil := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339) + resourceName := "aws_spot_fleet_request.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSpotFleetRequestLaunchTemplateConfigWithOverrides(rName, rInt, validUntil), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists(resourceName, &sfr), + resource.TestCheckResourceAttr(resourceName, "spot_request_state", "active"), + resource.TestCheckResourceAttr(resourceName, "launch_specification.#", "0"), + resource.TestCheckResourceAttr(resourceName, "launch_template_config.#", "1"), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_launchTemplateToLaunchSpec(t *testing.T) { + var before, after ec2.SpotFleetRequestConfig + rName := acctest.RandString(10) + rInt := acctest.RandInt() + validUntil := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339) + resourceName := "aws_spot_fleet_request.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSpotFleetRequestLaunchTemplateConfig(rName, rInt, validUntil), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists(resourceName, &before), + resource.TestCheckResourceAttr(resourceName, "spot_request_state", "active"), + resource.TestCheckResourceAttr(resourceName, "launch_specification.#", "0"), + resource.TestCheckResourceAttr(resourceName, "launch_template_config.#", "1"), + ), + }, + { + Config: testAccAWSSpotFleetRequestConfig(rName, rInt, validUntil), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists(resourceName, &after), + resource.TestCheckResourceAttr(resourceName, "spot_request_state", "active"), + resource.TestCheckResourceAttr(resourceName, "spot_price", "0.005"), + resource.TestCheckResourceAttr(resourceName, "launch_specification.#", "1"), + testAccCheckAWSSpotFleetRequestConfigRecreated(t, &before, &after), + ), + }, + }, + }) +} + +func TestAccAWSSpotFleetRequest_launchSpecToLaunchTemplate(t *testing.T) { + var before, after ec2.SpotFleetRequestConfig + rName := acctest.RandString(10) + rInt := acctest.RandInt() + validUntil := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339) + resourceName := "aws_spot_fleet_request.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSpotFleetRequestConfig(rName, rInt, validUntil), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists(resourceName, &before), + resource.TestCheckResourceAttr(resourceName, "spot_request_state", "active"), + resource.TestCheckResourceAttr(resourceName, "spot_price", "0.005"), + resource.TestCheckResourceAttr(resourceName, "launch_specification.#", "1"), + ), + }, + { + Config: testAccAWSSpotFleetRequestLaunchTemplateConfig(rName, rInt, validUntil), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists(resourceName, &after), + resource.TestCheckResourceAttr(resourceName, "spot_request_state", "active"), + resource.TestCheckResourceAttr(resourceName, "launch_specification.#", "0"), + resource.TestCheckResourceAttr(resourceName, "launch_template_config.#", "1"), + testAccCheckAWSSpotFleetRequestConfigRecreated(t, &before, &after), + ), + }, + }, + }) +} + func TestAccAWSSpotFleetRequest_instanceInterruptionBehavior(t *testing.T) { var sfr ec2.SpotFleetRequestConfig rName := acctest.RandString(10) @@ -427,6 +572,29 @@ func TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameAz(t *testing.T) { }) } +func testAccCheckAWSSpotFleetRequest_IamInstanceProfileArn( + sfr *ec2.SpotFleetRequestConfig) resource.TestCheckFunc { + return func(s *terraform.State) error { + if len(sfr.SpotFleetRequestConfig.LaunchSpecifications) == 0 { + return errors.New("Missing launch specification") + } + + spec := *sfr.SpotFleetRequestConfig.LaunchSpecifications[0] + + profile := spec.IamInstanceProfile + if profile == nil { + return fmt.Errorf("Expected IamInstanceProfile to be set, got nil") + } + //Validate the string whether it is ARN + re := regexp.MustCompile(`arn:aws:iam::\d{12}:instance-profile/?[a-zA-Z0-9+=,.@-_].*`) + if !re.MatchString(*profile.Arn) { + return fmt.Errorf("Expected IamInstanceProfile input as ARN, got %s", *profile.Arn) + } + + return nil + } +} + func TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameSubnet(t *testing.T) { var sfr ec2.SpotFleetRequestConfig rName := acctest.RandString(10) @@ -745,7 +913,6 @@ func TestAccAWSSpotFleetRequest_WithTargetGroups(t *testing.T) { rInt := acctest.RandInt() validUntil := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339) resourceName := "aws_spot_fleet_request.test" - resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2SpotFleetRequest(t) }, Providers: testAccProviders, @@ -783,6 +950,30 @@ func TestAccAWSSpotFleetRequest_WithInstanceStoreAmi(t *testing.T) { }) } +func TestAccAWSSpotFleetRequest_disappears(t *testing.T) { + var sfr ec2.SpotFleetRequestConfig + rName := acctest.RandString(10) + rInt := acctest.RandInt() + validUntil := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339) + resourceName := "aws_spot_fleet_request.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2SpotFleetRequest(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSpotFleetRequestConfig(rName, rInt, validUntil), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSSpotFleetRequestExists(resourceName, &sfr), + testAccCheckAWSSpotFleetRequestDisappears(&sfr), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func testAccCheckAWSSpotFleetRequestConfigRecreated(t *testing.T, before, after *ec2.SpotFleetRequestConfig) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -826,6 +1017,16 @@ func testAccCheckAWSSpotFleetRequestExists( } } +func testAccCheckAWSSpotFleetRequestDisappears(sfr *ec2.SpotFleetRequestConfig) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + sfrId := aws.StringValue(sfr.SpotFleetRequestId) + err := deleteSpotFleetRequest(sfrId, true, 5*time.Minute, conn) + + return err + } +} + func testAccCheckAWSSpotFleetRequestDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).ec2conn @@ -885,38 +1086,17 @@ func testAccCheckAWSSpotFleetRequest_PlacementAttributes( if placement == nil { return fmt.Errorf("Expected placement to be set, got nil") } - if *placement.Tenancy != "dedicated" { + if *placement.Tenancy != ec2.TenancyDedicated { return fmt.Errorf("Expected placement tenancy to be %q, got %q", "dedicated", *placement.Tenancy) } + if aws.StringValue(placement.GroupName) != fmt.Sprintf("test-pg-%s", rName) { return fmt.Errorf("Expected placement group to be %q, got %q", fmt.Sprintf("test-pg-%s", rName), aws.StringValue(placement.GroupName)) } return nil } -} - -func testAccCheckAWSSpotFleetRequest_IamInstanceProfileArn( - sfr *ec2.SpotFleetRequestConfig) resource.TestCheckFunc { - return func(s *terraform.State) error { - if len(sfr.SpotFleetRequestConfig.LaunchSpecifications) == 0 { - return errors.New("Missing launch specification") - } - - spec := *sfr.SpotFleetRequestConfig.LaunchSpecifications[0] - - profile := spec.IamInstanceProfile - if profile == nil { - return fmt.Errorf("Expected IamInstanceProfile to be set, got nil") - } - //Validate the string whether it is ARN - re := regexp.MustCompile(`arn:aws:iam::\d{12}:instance-profile/?[a-zA-Z0-9+=,.@-_].*`) - if !re.MatchString(*profile.Arn) { - return fmt.Errorf("Expected IamInstanceProfile input as ARN, got %s", *profile.Arn) - } - return nil - } } func testAccPreCheckAWSEc2SpotFleetRequest(t *testing.T) { @@ -936,7 +1116,7 @@ func testAccPreCheckAWSEc2SpotFleetRequest(t *testing.T) { } func testAccAWSSpotFleetRequestConfigBase(rName string, rInt int) string { - return fmt.Sprintf(` + return testAccLatestAmazonLinuxHvmEbsAmiConfig() + fmt.Sprintf(` data "aws_availability_zones" "available" { state = "available" @@ -945,14 +1125,17 @@ data "aws_availability_zones" "available" { values = ["opt-in-not-required"] } } - -resource "aws_key_pair" "debugging" { - key_name = "tmp-key-%[1]s" +resource "aws_key_pair" "test" { + key_name = %[1]q public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com" + + tags = { + Name = %[1]q + } } resource "aws_iam_role" "test-role" { - name = "test-role-%[1]s" + name = %[1]q 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: +launch configuration block. If you want to specify multiple values, then separate launch configuration blocks should be used or launch template overrides should be configured, one per subnet: + +### Using multiple launch specifications ```hcl resource "aws_spot_fleet_request" "foo" { @@ -78,6 +110,48 @@ resource "aws_spot_fleet_request" "foo" { } ``` + +### Using multiple launch configurations + +```hcl +data "aws_subnet_ids" "example" { + vpc_id = "${var.vpc_id}" +} + +resource "aws_launch_template" "foo" { + name = "launch-template" + image_id = "ami-516b9131" + instance_type = "m1.small" + key_name = "some-key" + spot_price = "0.05" +} + +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_template_config { + launch_template_specification { + id = "${aws_launch_template.foo.id}" + version = "${aws_launch_template.foo.latest_version}" + } + overrides { + subnet_id = "${data.aws_subnets.example.ids[0]}" + } + overrides { + subnet_id = "${data.aws_subnets.example.ids[1]}" + } + overrides { + subnet_id = "${data.aws_subnets.example.ids[2]}" + } + } + + depends_on = ["aws_iam_policy_attachment.test-attach"] +} +``` + ## Argument Reference Most of these arguments directly correspond to the @@ -88,9 +162,9 @@ Most of these arguments directly correspond to the CancelSpotFleetRequests or when the Spot fleet request expires, if you set terminateInstancesWithExpiration. * `replace_unhealthy_instances` - (Optional) Indicates whether Spot fleet should replace unhealthy instances. Default `false`. -* `launch_specification` - Used to define the launch configuration of the +* `launch_specification` - (Optional) Used to define the launch configuration of the spot-fleet request. Can be specified multiple times to define different bids -across different markets and instance types. +across different markets and instance types. Conflicts with `launch_template_config`. At least one of `launch_specification` or `launch_template_config` is required. **Note:** This takes in similar but not identical inputs as [`aws_instance`](instance.html). There are limitations on @@ -98,6 +172,7 @@ across different markets and instance types. [reference documentation](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SpotFleetLaunchSpecification.html). Any normal [`aws_instance`](instance.html) parameter that corresponds to those inputs may be used and it have a additional parameter `iam_instance_profile_arn` takes `aws_iam_instance_profile` attribute `arn` as input. +* `launch_template_config` - (Optional) Launch template configuration block. See [Launch Template Configs](#launch-template-configs) below for more details. Conflicts with `launch_specification`. At least one of `launch_specification` or `launch_template_config` is required. * `spot_price` - (Optional; Default: On-demand price) The maximum bid price per unit hour. * `wait_for_fulfillment` - (Optional; Default: false) If set, Terraform will wait for the Spot Request to be fulfilled, and will throw an error if the @@ -129,6 +204,32 @@ across different markets and instance types. * `target_group_arns` (Optional) A list of `aws_alb_target_group` ARNs, for use with Application Load Balancing. * `tags` - (Optional) A map of tags to assign to the resource. +### Launch Template Configs + +The `launch_template_config` block supports the following: + +* `launch_template_specification` - (Required) Launch template specification. See [Launch Template Specification](#launch-template-specification) below for more details. +* `overrides` - (Optional) One or more override configurations. See [Overrides](#overrides) below for more details. + +### Launch Template Specification + +* `id` - The ID of the launch template. Conflicts with `name`. +* `name` - The name of the launch template. Conflicts with `id`. +* `version` - (Optional) Template version. Unlike the autoscaling equivalent, does not support `$Latest` or `$Default`, so use the launch_template resource's attribute, e.g. `"${aws_launch_template.foo.latest_version}"`. It will use the default version if omitted. + + **Note:** The specified launch template can specify only a subset of the + inputs of [`aws_launch_template`](launch_template.html). There are limitations on + what you can specify as spot fleet does not support all the attributes that are supported by autoscaling groups. [AWS documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-launch-templates.html#launch-templates-spot-fleet) is currently sparse, but at least `instance_initiated_shutdown_behavior` is confirmed unsupported. + +### Overrides + +* `availability_zone` - (Optional) The availability zone in which to place the request. +* `instance_type` - (Optional) The type of instance to request. +* `priority` - (Optional) The priority for the launch template override. The lower the number, the higher the priority. If no number is set, the launch template override has the lowest priority. +* `spot_price` - (Optional) The maximum spot bid for this override request. +* `subnet_id` - (Optional) The subnet in which to launch the requested instance. +* `weighted_capacity` - (Optional) The capacity added to the fleet by a fulfilled request. + ### Timeouts The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: