From 328f8c4d721a3331c1393bce0ac3708c2b3d4310 Mon Sep 17 00:00:00 2001 From: Artem Yarmoliuk Date: Fri, 30 Jul 2021 12:40:36 +0800 Subject: [PATCH 01/11] r/aws_instance: fix spot request with stop behaviour --- aws/resource_aws_instance.go | 42 +++++++++++++++++++------ aws/resource_aws_instance_test.go | 52 +++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/aws/resource_aws_instance.go b/aws/resource_aws_instance.go index 6e50473399f4..85bb45777da2 100644 --- a/aws/resource_aws_instance.go +++ b/aws/resource_aws_instance.go @@ -2047,8 +2047,21 @@ func blockDeviceIsRoot(bd *ec2.InstanceBlockDeviceMapping, instance *ec2.Instanc } func fetchLaunchTemplateAmi(specs []interface{}, conn *ec2.EC2) (string, error) { + ltData, err := fetchLaunchTemplateData(specs, conn) + if err != nil { + return "", fmt.Errorf("failed fetching Launch Template AMI ID: %w", err) + } + + if ltData.ImageId != nil { + return *ltData.ImageId, nil + } + + return "", nil +} + +func fetchLaunchTemplateData(specs []interface{}, conn *ec2.EC2) (*ec2.ResponseLaunchTemplateData, error) { if len(specs) < 1 { - return "", errors.New("Cannot fetch AMI for blank launch template.") + return nil, errors.New("Cannot fetch data for blank launch template spec.") } spec := specs[0].(map[string]interface{}) @@ -2084,7 +2097,7 @@ func fetchLaunchTemplateAmi(specs []interface{}, conn *ec2.EC2) (string, error) dltv, err := conn.DescribeLaunchTemplateVersions(request) if err != nil { - return "", err + return nil, err } var ltData *ec2.ResponseLaunchTemplateData @@ -2095,11 +2108,7 @@ func fetchLaunchTemplateAmi(specs []interface{}, conn *ec2.EC2) (string, error) ltData = dltv.LaunchTemplateVersions[0].LaunchTemplateData } - if ltData.ImageId != nil { - return *ltData.ImageId, nil - } - - return "", nil + return ltData, nil } func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) { @@ -2576,8 +2585,20 @@ func buildAwsInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanc opts.InstanceType = aws.String(v.(string)) } + var interruptionBehavior string if v, ok := d.GetOk("launch_template"); ok { opts.LaunchTemplate = expandEc2LaunchTemplateSpecification(v.([]interface{})) + + ltData, err := fetchLaunchTemplateData(v.([]interface{}), conn) + if err != nil { + return nil, err + } + + if ltData.InstanceMarketOptions != nil && ltData.InstanceMarketOptions.SpotOptions != nil { + if v := ltData.InstanceMarketOptions.SpotOptions.InstanceInterruptionBehavior; v != nil { + interruptionBehavior = *v + } + } } instanceType := d.Get("instance_type").(string) @@ -2633,12 +2654,15 @@ func buildAwsInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanc // aws_spot_instance_request. They represent the same data. :-| opts.Placement = &ec2.Placement{ AvailabilityZone: aws.String(d.Get("availability_zone").(string)), - GroupName: aws.String(d.Get("placement_group").(string)), } opts.SpotPlacement = &ec2.SpotPlacement{ AvailabilityZone: aws.String(d.Get("availability_zone").(string)), - GroupName: aws.String(d.Get("placement_group").(string)), + } + + if interruptionBehavior == "" || interruptionBehavior == ec2.InstanceInterruptionBehaviorTerminate { + opts.Placement.GroupName = aws.String(d.Get("placement_group").(string)) + opts.SpotPlacement.GroupName = aws.String(d.Get("placement_group").(string)) } if v := d.Get("tenancy").(string); v != "" { diff --git a/aws/resource_aws_instance_test.go b/aws/resource_aws_instance_test.go index fdaefe286f48..e7621965c120 100644 --- a/aws/resource_aws_instance_test.go +++ b/aws/resource_aws_instance_test.go @@ -2803,6 +2803,30 @@ func TestAccAWSInstance_LaunchTemplate_SwapIDAndName(t *testing.T) { }) } +func TestAccAWSInstance_LaunchTemplate_SpotAndStop(t *testing.T) { + var v ec2.Instance + resourceName := "aws_instance.test" + launchTemplateResourceName := "aws_launch_template.test" + + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccInstanceConfig_WithTemplate_WithSpot_WithStop(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + ), + }, + }, + }) +} + func TestAccAWSInstance_getPasswordData_falseToTrue(t *testing.T) { var before, after ec2.Instance resourceName := "aws_instance.test" @@ -6588,3 +6612,31 @@ resource "aws_instance" "test" { } `, rName)) } + +func testAccInstanceConfig_WithTemplate_WithSpot_WithStop(rName string) string { + return composeConfig( + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + testAccAvailableEc2InstanceTypeForRegion("t3.micro", "t2.micro", "t1.micro", "m1.small"), + fmt.Sprintf(` +resource "aws_launch_template" "test" { + name = %[1]q + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type + + instance_market_options { + market_type = "spot" + + spot_options { + instance_interruption_behavior = "stop" + spot_instance_type = "persistent" + } + } +} + +resource "aws_instance" "test" { + launch_template { + name = aws_launch_template.test.name + } +} +`, rName)) +} From 82039aa78c606e4a03dea52347d6f3e6e5f6f1a2 Mon Sep 17 00:00:00 2001 From: Artem Yarmoliuk Date: Mon, 2 Aug 2021 19:23:07 +0800 Subject: [PATCH 02/11] Add changelog --- .changelog/20372.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/20372.txt diff --git a/.changelog/20372.txt b/.changelog/20372.txt new file mode 100644 index 000000000000..fcadd050490c --- /dev/null +++ b/.changelog/20372.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_instance: Fix creating instance with launch template that sets `instance_interruption_behavior` to `stop` +``` From 3c07cf5f53cc46463bf5734feceb6c0bc668088c Mon Sep 17 00:00:00 2001 From: Artem Yarmoliuk Date: Mon, 2 Aug 2021 19:50:59 +0800 Subject: [PATCH 03/11] Use aws.StringValue --- aws/resource_aws_instance.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/aws/resource_aws_instance.go b/aws/resource_aws_instance.go index 85bb45777da2..77ace803950f 100644 --- a/aws/resource_aws_instance.go +++ b/aws/resource_aws_instance.go @@ -2052,11 +2052,7 @@ func fetchLaunchTemplateAmi(specs []interface{}, conn *ec2.EC2) (string, error) return "", fmt.Errorf("failed fetching Launch Template AMI ID: %w", err) } - if ltData.ImageId != nil { - return *ltData.ImageId, nil - } - - return "", nil + return aws.StringValue(ltData.ImageId), nil } func fetchLaunchTemplateData(specs []interface{}, conn *ec2.EC2) (*ec2.ResponseLaunchTemplateData, error) { @@ -2595,9 +2591,7 @@ func buildAwsInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanc } if ltData.InstanceMarketOptions != nil && ltData.InstanceMarketOptions.SpotOptions != nil { - if v := ltData.InstanceMarketOptions.SpotOptions.InstanceInterruptionBehavior; v != nil { - interruptionBehavior = *v - } + interruptionBehavior = aws.StringValue(ltData.InstanceMarketOptions.SpotOptions.InstanceInterruptionBehavior) } } From 48a3c036964716dfbd64d8d2435aa0d80dd38d10 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Thu, 26 May 2022 15:53:38 -0400 Subject: [PATCH 04/11] Revert "Use aws.StringValue" This reverts commit 3c07cf5f53cc46463bf5734feceb6c0bc668088c. --- aws/resource_aws_instance.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/aws/resource_aws_instance.go b/aws/resource_aws_instance.go index 77ace803950f..85bb45777da2 100644 --- a/aws/resource_aws_instance.go +++ b/aws/resource_aws_instance.go @@ -2052,7 +2052,11 @@ func fetchLaunchTemplateAmi(specs []interface{}, conn *ec2.EC2) (string, error) return "", fmt.Errorf("failed fetching Launch Template AMI ID: %w", err) } - return aws.StringValue(ltData.ImageId), nil + if ltData.ImageId != nil { + return *ltData.ImageId, nil + } + + return "", nil } func fetchLaunchTemplateData(specs []interface{}, conn *ec2.EC2) (*ec2.ResponseLaunchTemplateData, error) { @@ -2591,7 +2595,9 @@ func buildAwsInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanc } if ltData.InstanceMarketOptions != nil && ltData.InstanceMarketOptions.SpotOptions != nil { - interruptionBehavior = aws.StringValue(ltData.InstanceMarketOptions.SpotOptions.InstanceInterruptionBehavior) + if v := ltData.InstanceMarketOptions.SpotOptions.InstanceInterruptionBehavior; v != nil { + interruptionBehavior = *v + } } } From 74aa15dd96bf7e014385261b3f48f312f38247ba Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Thu, 26 May 2022 15:54:03 -0400 Subject: [PATCH 05/11] Revert "r/aws_instance: fix spot request with stop behaviour" This reverts commit 328f8c4d721a3331c1393bce0ac3708c2b3d4310. --- aws/resource_aws_instance.go | 42 ++++++------------------- aws/resource_aws_instance_test.go | 52 ------------------------------- 2 files changed, 9 insertions(+), 85 deletions(-) diff --git a/aws/resource_aws_instance.go b/aws/resource_aws_instance.go index 85bb45777da2..6e50473399f4 100644 --- a/aws/resource_aws_instance.go +++ b/aws/resource_aws_instance.go @@ -2047,21 +2047,8 @@ func blockDeviceIsRoot(bd *ec2.InstanceBlockDeviceMapping, instance *ec2.Instanc } func fetchLaunchTemplateAmi(specs []interface{}, conn *ec2.EC2) (string, error) { - ltData, err := fetchLaunchTemplateData(specs, conn) - if err != nil { - return "", fmt.Errorf("failed fetching Launch Template AMI ID: %w", err) - } - - if ltData.ImageId != nil { - return *ltData.ImageId, nil - } - - return "", nil -} - -func fetchLaunchTemplateData(specs []interface{}, conn *ec2.EC2) (*ec2.ResponseLaunchTemplateData, error) { if len(specs) < 1 { - return nil, errors.New("Cannot fetch data for blank launch template spec.") + return "", errors.New("Cannot fetch AMI for blank launch template.") } spec := specs[0].(map[string]interface{}) @@ -2097,7 +2084,7 @@ func fetchLaunchTemplateData(specs []interface{}, conn *ec2.EC2) (*ec2.ResponseL dltv, err := conn.DescribeLaunchTemplateVersions(request) if err != nil { - return nil, err + return "", err } var ltData *ec2.ResponseLaunchTemplateData @@ -2108,7 +2095,11 @@ func fetchLaunchTemplateData(specs []interface{}, conn *ec2.EC2) (*ec2.ResponseL ltData = dltv.LaunchTemplateVersions[0].LaunchTemplateData } - return ltData, nil + if ltData.ImageId != nil { + return *ltData.ImageId, nil + } + + return "", nil } func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) { @@ -2585,20 +2576,8 @@ func buildAwsInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanc opts.InstanceType = aws.String(v.(string)) } - var interruptionBehavior string if v, ok := d.GetOk("launch_template"); ok { opts.LaunchTemplate = expandEc2LaunchTemplateSpecification(v.([]interface{})) - - ltData, err := fetchLaunchTemplateData(v.([]interface{}), conn) - if err != nil { - return nil, err - } - - if ltData.InstanceMarketOptions != nil && ltData.InstanceMarketOptions.SpotOptions != nil { - if v := ltData.InstanceMarketOptions.SpotOptions.InstanceInterruptionBehavior; v != nil { - interruptionBehavior = *v - } - } } instanceType := d.Get("instance_type").(string) @@ -2654,15 +2633,12 @@ func buildAwsInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanc // aws_spot_instance_request. They represent the same data. :-| opts.Placement = &ec2.Placement{ AvailabilityZone: aws.String(d.Get("availability_zone").(string)), + GroupName: aws.String(d.Get("placement_group").(string)), } opts.SpotPlacement = &ec2.SpotPlacement{ AvailabilityZone: aws.String(d.Get("availability_zone").(string)), - } - - if interruptionBehavior == "" || interruptionBehavior == ec2.InstanceInterruptionBehaviorTerminate { - opts.Placement.GroupName = aws.String(d.Get("placement_group").(string)) - opts.SpotPlacement.GroupName = aws.String(d.Get("placement_group").(string)) + GroupName: aws.String(d.Get("placement_group").(string)), } if v := d.Get("tenancy").(string); v != "" { diff --git a/aws/resource_aws_instance_test.go b/aws/resource_aws_instance_test.go index e7621965c120..fdaefe286f48 100644 --- a/aws/resource_aws_instance_test.go +++ b/aws/resource_aws_instance_test.go @@ -2803,30 +2803,6 @@ func TestAccAWSInstance_LaunchTemplate_SwapIDAndName(t *testing.T) { }) } -func TestAccAWSInstance_LaunchTemplate_SpotAndStop(t *testing.T) { - var v ec2.Instance - resourceName := "aws_instance.test" - launchTemplateResourceName := "aws_launch_template.test" - - rName := acctest.RandomWithPrefix("tf-acc-test") - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), - Providers: testAccProviders, - CheckDestroy: testAccCheckInstanceDestroy, - Steps: []resource.TestStep{ - { - Config: testAccInstanceConfig_WithTemplate_WithSpot_WithStop(rName), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInstanceExists(resourceName, &v), - resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), - ), - }, - }, - }) -} - func TestAccAWSInstance_getPasswordData_falseToTrue(t *testing.T) { var before, after ec2.Instance resourceName := "aws_instance.test" @@ -6612,31 +6588,3 @@ resource "aws_instance" "test" { } `, rName)) } - -func testAccInstanceConfig_WithTemplate_WithSpot_WithStop(rName string) string { - return composeConfig( - testAccLatestAmazonLinuxHvmEbsAmiConfig(), - testAccAvailableEc2InstanceTypeForRegion("t3.micro", "t2.micro", "t1.micro", "m1.small"), - fmt.Sprintf(` -resource "aws_launch_template" "test" { - name = %[1]q - image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id - instance_type = data.aws_ec2_instance_type_offering.available.instance_type - - instance_market_options { - market_type = "spot" - - spot_options { - instance_interruption_behavior = "stop" - spot_instance_type = "persistent" - } - } -} - -resource "aws_instance" "test" { - launch_template { - name = aws_launch_template.test.name - } -} -`, rName)) -} From 053e178842eaad1cd118ca783c67e75f2a1ebc0e Mon Sep 17 00:00:00 2001 From: Oleh Palii Date: Sat, 7 May 2022 21:54:56 +0300 Subject: [PATCH 06/11] fix changelog item --- .changelog/{20372.txt => 24695.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{20372.txt => 24695.txt} (100%) diff --git a/.changelog/20372.txt b/.changelog/24695.txt similarity index 100% rename from .changelog/20372.txt rename to .changelog/24695.txt From 85508fae63f7bde09415ed366af568649de5410a Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Thu, 26 May 2022 16:18:14 -0400 Subject: [PATCH 07/11] Tweak CHANGELOG entry. --- .changelog/24695.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelog/24695.txt b/.changelog/24695.txt index fcadd050490c..7d97765724ba 100644 --- a/.changelog/24695.txt +++ b/.changelog/24695.txt @@ -1,3 +1,3 @@ ```release-note:bug -resource/aws_instance: Fix creating instance with launch template that sets `instance_interruption_behavior` to `stop` +resource/aws_instance: Prevent error `InvalidParameterCombination: The parameter GroupName within placement information cannot be specified when instanceInterruptionBehavior is set to 'STOP'` when using a launch template that sets `instance_interruption_behavior` to `stop` ``` From f6fee44327629014df9de7e1d328ec79184dd79c Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Thu, 26 May 2022 16:18:57 -0400 Subject: [PATCH 08/11] r/aws_instance: Add 'TestAccEC2Instance_LaunchTemplate_spotAndStop'. --- internal/service/ec2/ec2_instance_test.go | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/internal/service/ec2/ec2_instance_test.go b/internal/service/ec2/ec2_instance_test.go index 63169f581991..cd8533e7a6cb 100644 --- a/internal/service/ec2/ec2_instance_test.go +++ b/internal/service/ec2/ec2_instance_test.go @@ -3209,6 +3209,29 @@ func TestAccEC2Instance_LaunchTemplate_swapIDAndName(t *testing.T) { }) } +func TestAccEC2Instance_LaunchTemplate_spotAndStop(t *testing.T) { + var v ec2.Instance + resourceName := "aws_instance.test" + launchTemplateResourceName := "aws_launch_template.test" + rName := sdkacctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccInstanceConfig_templateSpotAndStop(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + ), + }, + }, + }) +} + func TestAccEC2Instance_GetPasswordData_falseToTrue(t *testing.T) { var before, after ec2.Instance resourceName := "aws_instance.test" @@ -7646,3 +7669,35 @@ resource "aws_instance" "test" { } `, rName)) } + +func testAccInstanceConfig_templateSpotAndStop(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigLatestAmazonLinuxHVMEBSAMI(), + acctest.AvailableEC2InstanceTypeForRegion("t3.micro", "t2.micro", "t1.micro", "m1.small"), + fmt.Sprintf(` +resource "aws_launch_template" "test" { + name = %[1]q + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type + + instance_market_options { + market_type = "spot" + + spot_options { + instance_interruption_behavior = "stop" + spot_instance_type = "persistent" + } + } +} + +resource "aws_instance" "test" { + launch_template { + name = aws_launch_template.test.name + } + + tags = { + Name = %[1]q + } +} +`, rName)) +} From ad8d35d4512894d87ce53349b27fc41b4bd53cdb Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Thu, 26 May 2022 16:41:26 -0400 Subject: [PATCH 09/11] Map 'InvalidLaunchTemplateName.NotFoundException' to NotFoundError. --- internal/service/ec2/find.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/ec2/find.go b/internal/service/ec2/find.go index 6fd4e5cc25f9..ddfed2d052ad 100644 --- a/internal/service/ec2/find.go +++ b/internal/service/ec2/find.go @@ -4213,7 +4213,7 @@ func FindLaunchTemplateVersions(conn *ec2.EC2, input *ec2.DescribeLaunchTemplate return !lastPage }) - if tfawserr.ErrCodeEquals(err, errCodeInvalidLaunchTemplateIdNotFound, errCodeInvalidLaunchTemplateIdVersionNotFound) { + if tfawserr.ErrCodeEquals(err, errCodeInvalidLaunchTemplateIdNotFound, errCodeInvalidLaunchTemplateNameNotFoundException, errCodeInvalidLaunchTemplateIdVersionNotFound) { return nil, &resource.NotFoundError{ LastError: err, LastRequest: input, From fb17941063dfb042d6a48a3618a18a3d2ac1a06c Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Thu, 26 May 2022 16:49:33 -0400 Subject: [PATCH 10/11] Map missing LaunchTemplateVersion.LaunchTemplateData to NotFoundError. --- internal/service/ec2/find.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/ec2/find.go b/internal/service/ec2/find.go index ddfed2d052ad..c029b57f7dc1 100644 --- a/internal/service/ec2/find.go +++ b/internal/service/ec2/find.go @@ -4185,7 +4185,7 @@ func FindLaunchTemplateVersion(conn *ec2.EC2, input *ec2.DescribeLaunchTemplateV return nil, err } - if len(output) == 0 || output[0] == nil { + if len(output) == 0 || output[0] == nil || output[0].LaunchTemplateData == nil { return nil, tfresource.NewEmptyResultError(input) } From d4b691bca0a9f339923a23148a0d55671fa916d6 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Thu, 26 May 2022 17:38:52 -0400 Subject: [PATCH 11/11] r/aws_instance: Don't set placement GroupName if instance interruption behavior is 'stop'. --- internal/service/ec2/ec2_instance.go | 157 +++++++++++++-------------- 1 file changed, 77 insertions(+), 80 deletions(-) diff --git a/internal/service/ec2/ec2_instance.go b/internal/service/ec2/ec2_instance.go index f2cdf93ec9b6..e976a0198cd9 100644 --- a/internal/service/ec2/ec2_instance.go +++ b/internal/service/ec2/ec2_instance.go @@ -1989,62 +1989,6 @@ func blockDeviceIsRoot(bd *ec2.InstanceBlockDeviceMapping, instance *ec2.Instanc aws.StringValue(bd.DeviceName) == aws.StringValue(instance.RootDeviceName) } -func fetchLaunchTemplateAMI(specs []interface{}, conn *ec2.EC2) (string, error) { - if len(specs) < 1 { - return "", errors.New("Cannot fetch AMI for blank launch template.") - } - - spec := specs[0].(map[string]interface{}) - - idValue, idOk := spec["id"] - nameValue, nameOk := spec["name"] - - request := &ec2.DescribeLaunchTemplateVersionsInput{} - - if idOk && idValue != "" { - request.LaunchTemplateId = aws.String(idValue.(string)) - } else if nameOk && nameValue != "" { - request.LaunchTemplateName = aws.String(nameValue.(string)) - } - - var isLatest bool - defaultFilter := []*ec2.Filter{ - { - Name: aws.String("is-default-version"), - Values: aws.StringSlice([]string{"true"}), - }, - } - if v, ok := spec["version"]; ok && v != "" { - switch v { - case LaunchTemplateVersionDefault: - request.Filters = defaultFilter - case LaunchTemplateVersionLatest: - isLatest = true - default: - request.Versions = []*string{aws.String(v.(string))} - } - } - - dltv, err := conn.DescribeLaunchTemplateVersions(request) - if err != nil { - return "", err - } - - var ltData *ec2.ResponseLaunchTemplateData - if isLatest { - index := len(dltv.LaunchTemplateVersions) - 1 - ltData = dltv.LaunchTemplateVersions[index].LaunchTemplateData - } else { - ltData = dltv.LaunchTemplateVersions[0].LaunchTemplateData - } - - if ltData.ImageId != nil { - return *ltData.ImageId, nil - } - - return "", nil -} - func FetchRootDeviceName(conn *ec2.EC2, amiID string) (*string, error) { if amiID == "" { return nil, errors.New("Cannot fetch root device name for blank AMI ID.") @@ -2292,21 +2236,24 @@ func readBlockDeviceMappingsFromConfig(d *schema.ResourceData, conn *ec2.EC2) ([ } var amiID string - if v, ok := d.GetOk("launch_template"); ok { - var err error - amiID, err = fetchLaunchTemplateAMI(v.([]interface{}), conn) + + if v, ok := d.GetOk("launch_template"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + launchTemplateData, err := findLaunchTemplateData(conn, expandLaunchTemplateSpecification(v.([]interface{})[0].(map[string]interface{}))) + if err != nil { return nil, err } + + amiID = aws.StringValue(launchTemplateData.ImageId) } - // AMI id from attributes overrides ami from launch template + // AMI from configuration overrides the one from the launch template. if v, ok := d.GetOk("ami"); ok { amiID = v.(string) } if amiID == "" { - return nil, errors.New("`ami` must be set or provided via launch template") + return nil, errors.New("`ami` must be set or provided via `launch_template`") } if dn, err := FetchRootDeviceName(conn, amiID); err == nil { @@ -2508,8 +2455,21 @@ func buildInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanceOp opts.InstanceType = aws.String(v.(string)) } - if v, ok := d.GetOk("launch_template"); ok { - opts.LaunchTemplate = expandLaunchTemplateSpecification(v.([]interface{})) + var instanceInterruptionBehavior string + + if v, ok := d.GetOk("launch_template"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + launchTemplateSpecification := expandLaunchTemplateSpecification(v.([]interface{})[0].(map[string]interface{})) + launchTemplateData, err := findLaunchTemplateData(conn, launchTemplateSpecification) + + if err != nil { + return nil, err + } + + opts.LaunchTemplate = launchTemplateSpecification + + if launchTemplateData.InstanceMarketOptions != nil && launchTemplateData.InstanceMarketOptions.SpotOptions != nil { + instanceInterruptionBehavior = aws.StringValue(launchTemplateData.InstanceMarketOptions.SpotOptions.InstanceInterruptionBehavior) + } } instanceType := d.Get("instance_type").(string) @@ -2563,7 +2523,6 @@ func buildInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanceOp // aws_spot_instance_request. They represent the same data. :-| opts.Placement = &ec2.Placement{ AvailabilityZone: aws.String(d.Get("availability_zone").(string)), - GroupName: aws.String(d.Get("placement_group").(string)), } if v, ok := d.GetOk("placement_partition_number"); ok { @@ -2572,7 +2531,11 @@ func buildInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanceOp opts.SpotPlacement = &ec2.SpotPlacement{ AvailabilityZone: aws.String(d.Get("availability_zone").(string)), - GroupName: aws.String(d.Get("placement_group").(string)), + } + + if v := d.Get("placement_group").(string); instanceInterruptionBehavior == "" || instanceInterruptionBehavior == ec2.InstanceInterruptionBehaviorTerminate { + opts.Placement.GroupName = aws.String(v) + opts.SpotPlacement.GroupName = aws.String(v) } if v := d.Get("tenancy").(string); v != "" { @@ -3071,29 +3034,26 @@ func flattenInstanceMaintenanceOptions(apiObject *ec2.InstanceMaintenanceOptions return tfMap } -func expandLaunchTemplateSpecification(specs []interface{}) *ec2.LaunchTemplateSpecification { - if len(specs) < 1 { +func expandLaunchTemplateSpecification(tfMap map[string]interface{}) *ec2.LaunchTemplateSpecification { + if tfMap == nil { return nil } - spec := specs[0].(map[string]interface{}) + apiObject := &ec2.LaunchTemplateSpecification{} - idValue, idOk := spec["id"] - nameValue, nameOk := spec["name"] - - result := &ec2.LaunchTemplateSpecification{} - - if idOk && idValue != "" { - result.LaunchTemplateId = aws.String(idValue.(string)) - } else if nameOk && nameValue != "" { - result.LaunchTemplateName = aws.String(nameValue.(string)) + // DescribeLaunchTemplates returns both name and id but LaunchTemplateSpecification + // allows only one of them to be set. + if v, ok := tfMap["id"]; ok && v != "" { + apiObject.LaunchTemplateId = aws.String(v.(string)) + } else if v, ok := tfMap["name"]; ok && v != "" { + apiObject.LaunchTemplateName = aws.String(v.(string)) } - if v, ok := spec["version"]; ok && v != "" { - result.Version = aws.String(v.(string)) + if v, ok := tfMap["version"].(string); ok && v != "" { + apiObject.Version = aws.String(v) } - return result + return apiObject } func flattenInstanceLaunchTemplate(conn *ec2.EC2, instanceID, previousLaunchTemplateVersion string) ([]interface{}, error) { @@ -3178,6 +3138,43 @@ func findInstanceLaunchTemplateVersion(conn *ec2.EC2, id string) (string, error) return launchTemplateVersion, nil } +func findLaunchTemplateData(conn *ec2.EC2, launchTemplateSpecification *ec2.LaunchTemplateSpecification) (*ec2.ResponseLaunchTemplateData, error) { + input := &ec2.DescribeLaunchTemplateVersionsInput{} + + if v := aws.StringValue(launchTemplateSpecification.LaunchTemplateId); v != "" { + input.LaunchTemplateId = aws.String(v) + } else if v := aws.StringValue(launchTemplateSpecification.LaunchTemplateName); v != "" { + input.LaunchTemplateName = aws.String(v) + } + + var latestVersion bool + + if v := aws.StringValue(launchTemplateSpecification.Version); v != "" { + switch v { + case LaunchTemplateVersionDefault: + input.Filters = BuildAttributeFilterList(map[string]string{ + "is-default-version": "true", + }) + case LaunchTemplateVersionLatest: + latestVersion = true + default: + input.Versions = aws.StringSlice([]string{v}) + } + } + + output, err := FindLaunchTemplateVersions(conn, input) + + if err != nil { + return nil, fmt.Errorf("reading EC2 Launch Template versions: %w", err) + } + + if latestVersion { + return output[len(output)-1].LaunchTemplateData, nil + } + + return output[0].LaunchTemplateData, nil +} + // findLaunchTemplateNameAndVersions returns the specified launch template's name, default version and latest version. func findLaunchTemplateNameAndVersions(conn *ec2.EC2, id string) (string, string, string, error) { lt, err := FindLaunchTemplateByID(conn, id)