Skip to content

Commit 00e13dd

Browse files
authored
Merge pull request #24695 from paleg/instance-launch-template-stop
r/aws_instance: fix spot request with stop behaviour
2 parents 4a2f43f + d4b691b commit 00e13dd

File tree

4 files changed

+137
-82
lines changed

4 files changed

+137
-82
lines changed

.changelog/24695.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
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`
3+
```

internal/service/ec2/ec2_instance.go

+77-80
Original file line numberDiff line numberDiff line change
@@ -1987,62 +1987,6 @@ func blockDeviceIsRoot(bd *ec2.InstanceBlockDeviceMapping, instance *ec2.Instanc
19871987
aws.StringValue(bd.DeviceName) == aws.StringValue(instance.RootDeviceName)
19881988
}
19891989

1990-
func fetchLaunchTemplateAMI(specs []interface{}, conn *ec2.EC2) (string, error) {
1991-
if len(specs) < 1 {
1992-
return "", errors.New("Cannot fetch AMI for blank launch template.")
1993-
}
1994-
1995-
spec := specs[0].(map[string]interface{})
1996-
1997-
idValue, idOk := spec["id"]
1998-
nameValue, nameOk := spec["name"]
1999-
2000-
request := &ec2.DescribeLaunchTemplateVersionsInput{}
2001-
2002-
if idOk && idValue != "" {
2003-
request.LaunchTemplateId = aws.String(idValue.(string))
2004-
} else if nameOk && nameValue != "" {
2005-
request.LaunchTemplateName = aws.String(nameValue.(string))
2006-
}
2007-
2008-
var isLatest bool
2009-
defaultFilter := []*ec2.Filter{
2010-
{
2011-
Name: aws.String("is-default-version"),
2012-
Values: aws.StringSlice([]string{"true"}),
2013-
},
2014-
}
2015-
if v, ok := spec["version"]; ok && v != "" {
2016-
switch v {
2017-
case LaunchTemplateVersionDefault:
2018-
request.Filters = defaultFilter
2019-
case LaunchTemplateVersionLatest:
2020-
isLatest = true
2021-
default:
2022-
request.Versions = []*string{aws.String(v.(string))}
2023-
}
2024-
}
2025-
2026-
dltv, err := conn.DescribeLaunchTemplateVersions(request)
2027-
if err != nil {
2028-
return "", err
2029-
}
2030-
2031-
var ltData *ec2.ResponseLaunchTemplateData
2032-
if isLatest {
2033-
index := len(dltv.LaunchTemplateVersions) - 1
2034-
ltData = dltv.LaunchTemplateVersions[index].LaunchTemplateData
2035-
} else {
2036-
ltData = dltv.LaunchTemplateVersions[0].LaunchTemplateData
2037-
}
2038-
2039-
if ltData.ImageId != nil {
2040-
return *ltData.ImageId, nil
2041-
}
2042-
2043-
return "", nil
2044-
}
2045-
20461990
func FetchRootDeviceName(conn *ec2.EC2, amiID string) (*string, error) {
20471991
if amiID == "" {
20481992
return nil, errors.New("Cannot fetch root device name for blank AMI ID.")
@@ -2290,21 +2234,24 @@ func readBlockDeviceMappingsFromConfig(d *schema.ResourceData, conn *ec2.EC2) ([
22902234
}
22912235

22922236
var amiID string
2293-
if v, ok := d.GetOk("launch_template"); ok {
2294-
var err error
2295-
amiID, err = fetchLaunchTemplateAMI(v.([]interface{}), conn)
2237+
2238+
if v, ok := d.GetOk("launch_template"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
2239+
launchTemplateData, err := findLaunchTemplateData(conn, expandLaunchTemplateSpecification(v.([]interface{})[0].(map[string]interface{})))
2240+
22962241
if err != nil {
22972242
return nil, err
22982243
}
2244+
2245+
amiID = aws.StringValue(launchTemplateData.ImageId)
22992246
}
23002247

2301-
// AMI id from attributes overrides ami from launch template
2248+
// AMI from configuration overrides the one from the launch template.
23022249
if v, ok := d.GetOk("ami"); ok {
23032250
amiID = v.(string)
23042251
}
23052252

23062253
if amiID == "" {
2307-
return nil, errors.New("`ami` must be set or provided via launch template")
2254+
return nil, errors.New("`ami` must be set or provided via `launch_template`")
23082255
}
23092256

23102257
if dn, err := FetchRootDeviceName(conn, amiID); err == nil {
@@ -2506,8 +2453,21 @@ func buildInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanceOp
25062453
opts.InstanceType = aws.String(v.(string))
25072454
}
25082455

2509-
if v, ok := d.GetOk("launch_template"); ok {
2510-
opts.LaunchTemplate = expandLaunchTemplateSpecification(v.([]interface{}))
2456+
var instanceInterruptionBehavior string
2457+
2458+
if v, ok := d.GetOk("launch_template"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
2459+
launchTemplateSpecification := expandLaunchTemplateSpecification(v.([]interface{})[0].(map[string]interface{}))
2460+
launchTemplateData, err := findLaunchTemplateData(conn, launchTemplateSpecification)
2461+
2462+
if err != nil {
2463+
return nil, err
2464+
}
2465+
2466+
opts.LaunchTemplate = launchTemplateSpecification
2467+
2468+
if launchTemplateData.InstanceMarketOptions != nil && launchTemplateData.InstanceMarketOptions.SpotOptions != nil {
2469+
instanceInterruptionBehavior = aws.StringValue(launchTemplateData.InstanceMarketOptions.SpotOptions.InstanceInterruptionBehavior)
2470+
}
25112471
}
25122472

25132473
instanceType := d.Get("instance_type").(string)
@@ -2561,7 +2521,6 @@ func buildInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanceOp
25612521
// aws_spot_instance_request. They represent the same data. :-|
25622522
opts.Placement = &ec2.Placement{
25632523
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
2564-
GroupName: aws.String(d.Get("placement_group").(string)),
25652524
}
25662525

25672526
if v, ok := d.GetOk("placement_partition_number"); ok {
@@ -2570,7 +2529,11 @@ func buildInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanceOp
25702529

25712530
opts.SpotPlacement = &ec2.SpotPlacement{
25722531
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
2573-
GroupName: aws.String(d.Get("placement_group").(string)),
2532+
}
2533+
2534+
if v := d.Get("placement_group").(string); instanceInterruptionBehavior == "" || instanceInterruptionBehavior == ec2.InstanceInterruptionBehaviorTerminate {
2535+
opts.Placement.GroupName = aws.String(v)
2536+
opts.SpotPlacement.GroupName = aws.String(v)
25742537
}
25752538

25762539
if v := d.Get("tenancy").(string); v != "" {
@@ -3069,29 +3032,26 @@ func flattenInstanceMaintenanceOptions(apiObject *ec2.InstanceMaintenanceOptions
30693032
return tfMap
30703033
}
30713034

3072-
func expandLaunchTemplateSpecification(specs []interface{}) *ec2.LaunchTemplateSpecification {
3073-
if len(specs) < 1 {
3035+
func expandLaunchTemplateSpecification(tfMap map[string]interface{}) *ec2.LaunchTemplateSpecification {
3036+
if tfMap == nil {
30743037
return nil
30753038
}
30763039

3077-
spec := specs[0].(map[string]interface{})
3040+
apiObject := &ec2.LaunchTemplateSpecification{}
30783041

3079-
idValue, idOk := spec["id"]
3080-
nameValue, nameOk := spec["name"]
3081-
3082-
result := &ec2.LaunchTemplateSpecification{}
3083-
3084-
if idOk && idValue != "" {
3085-
result.LaunchTemplateId = aws.String(idValue.(string))
3086-
} else if nameOk && nameValue != "" {
3087-
result.LaunchTemplateName = aws.String(nameValue.(string))
3042+
// DescribeLaunchTemplates returns both name and id but LaunchTemplateSpecification
3043+
// allows only one of them to be set.
3044+
if v, ok := tfMap["id"]; ok && v != "" {
3045+
apiObject.LaunchTemplateId = aws.String(v.(string))
3046+
} else if v, ok := tfMap["name"]; ok && v != "" {
3047+
apiObject.LaunchTemplateName = aws.String(v.(string))
30883048
}
30893049

3090-
if v, ok := spec["version"]; ok && v != "" {
3091-
result.Version = aws.String(v.(string))
3050+
if v, ok := tfMap["version"].(string); ok && v != "" {
3051+
apiObject.Version = aws.String(v)
30923052
}
30933053

3094-
return result
3054+
return apiObject
30953055
}
30963056

30973057
func flattenInstanceLaunchTemplate(conn *ec2.EC2, instanceID, previousLaunchTemplateVersion string) ([]interface{}, error) {
@@ -3176,6 +3136,43 @@ func findInstanceLaunchTemplateVersion(conn *ec2.EC2, id string) (string, error)
31763136
return launchTemplateVersion, nil
31773137
}
31783138

3139+
func findLaunchTemplateData(conn *ec2.EC2, launchTemplateSpecification *ec2.LaunchTemplateSpecification) (*ec2.ResponseLaunchTemplateData, error) {
3140+
input := &ec2.DescribeLaunchTemplateVersionsInput{}
3141+
3142+
if v := aws.StringValue(launchTemplateSpecification.LaunchTemplateId); v != "" {
3143+
input.LaunchTemplateId = aws.String(v)
3144+
} else if v := aws.StringValue(launchTemplateSpecification.LaunchTemplateName); v != "" {
3145+
input.LaunchTemplateName = aws.String(v)
3146+
}
3147+
3148+
var latestVersion bool
3149+
3150+
if v := aws.StringValue(launchTemplateSpecification.Version); v != "" {
3151+
switch v {
3152+
case LaunchTemplateVersionDefault:
3153+
input.Filters = BuildAttributeFilterList(map[string]string{
3154+
"is-default-version": "true",
3155+
})
3156+
case LaunchTemplateVersionLatest:
3157+
latestVersion = true
3158+
default:
3159+
input.Versions = aws.StringSlice([]string{v})
3160+
}
3161+
}
3162+
3163+
output, err := FindLaunchTemplateVersions(conn, input)
3164+
3165+
if err != nil {
3166+
return nil, fmt.Errorf("reading EC2 Launch Template versions: %w", err)
3167+
}
3168+
3169+
if latestVersion {
3170+
return output[len(output)-1].LaunchTemplateData, nil
3171+
}
3172+
3173+
return output[0].LaunchTemplateData, nil
3174+
}
3175+
31793176
// findLaunchTemplateNameAndVersions returns the specified launch template's name, default version and latest version.
31803177
func findLaunchTemplateNameAndVersions(conn *ec2.EC2, id string) (string, string, string, error) {
31813178
lt, err := FindLaunchTemplateByID(conn, id)

internal/service/ec2/ec2_instance_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -3219,6 +3219,29 @@ func TestAccEC2Instance_LaunchTemplate_swapIDAndName(t *testing.T) {
32193219
})
32203220
}
32213221

3222+
func TestAccEC2Instance_LaunchTemplate_spotAndStop(t *testing.T) {
3223+
var v ec2.Instance
3224+
resourceName := "aws_instance.test"
3225+
launchTemplateResourceName := "aws_launch_template.test"
3226+
rName := sdkacctest.RandomWithPrefix("tf-acc-test")
3227+
3228+
resource.ParallelTest(t, resource.TestCase{
3229+
PreCheck: func() { acctest.PreCheck(t) },
3230+
ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID),
3231+
ProviderFactories: acctest.ProviderFactories,
3232+
CheckDestroy: testAccCheckInstanceDestroy,
3233+
Steps: []resource.TestStep{
3234+
{
3235+
Config: testAccInstanceConfig_templateSpotAndStop(rName),
3236+
Check: resource.ComposeAggregateTestCheckFunc(
3237+
testAccCheckInstanceExists(resourceName, &v),
3238+
resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"),
3239+
),
3240+
},
3241+
},
3242+
})
3243+
}
3244+
32223245
func TestAccEC2Instance_GetPasswordData_falseToTrue(t *testing.T) {
32233246
var before, after ec2.Instance
32243247
resourceName := "aws_instance.test"
@@ -7656,3 +7679,35 @@ resource "aws_instance" "test" {
76567679
}
76577680
`, rName))
76587681
}
7682+
7683+
func testAccInstanceConfig_templateSpotAndStop(rName string) string {
7684+
return acctest.ConfigCompose(
7685+
acctest.ConfigLatestAmazonLinuxHVMEBSAMI(),
7686+
acctest.AvailableEC2InstanceTypeForRegion("t3.micro", "t2.micro", "t1.micro", "m1.small"),
7687+
fmt.Sprintf(`
7688+
resource "aws_launch_template" "test" {
7689+
name = %[1]q
7690+
image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id
7691+
instance_type = data.aws_ec2_instance_type_offering.available.instance_type
7692+
7693+
instance_market_options {
7694+
market_type = "spot"
7695+
7696+
spot_options {
7697+
instance_interruption_behavior = "stop"
7698+
spot_instance_type = "persistent"
7699+
}
7700+
}
7701+
}
7702+
7703+
resource "aws_instance" "test" {
7704+
launch_template {
7705+
name = aws_launch_template.test.name
7706+
}
7707+
7708+
tags = {
7709+
Name = %[1]q
7710+
}
7711+
}
7712+
`, rName))
7713+
}

internal/service/ec2/find.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -4185,7 +4185,7 @@ func FindLaunchTemplateVersion(conn *ec2.EC2, input *ec2.DescribeLaunchTemplateV
41854185
return nil, err
41864186
}
41874187

4188-
if len(output) == 0 || output[0] == nil {
4188+
if len(output) == 0 || output[0] == nil || output[0].LaunchTemplateData == nil {
41894189
return nil, tfresource.NewEmptyResultError(input)
41904190
}
41914191

@@ -4213,7 +4213,7 @@ func FindLaunchTemplateVersions(conn *ec2.EC2, input *ec2.DescribeLaunchTemplate
42134213
return !lastPage
42144214
})
42154215

4216-
if tfawserr.ErrCodeEquals(err, errCodeInvalidLaunchTemplateIdNotFound, errCodeInvalidLaunchTemplateIdVersionNotFound) {
4216+
if tfawserr.ErrCodeEquals(err, errCodeInvalidLaunchTemplateIdNotFound, errCodeInvalidLaunchTemplateNameNotFoundException, errCodeInvalidLaunchTemplateIdVersionNotFound) {
42174217
return nil, &resource.NotFoundError{
42184218
LastError: err,
42194219
LastRequest: input,

0 commit comments

Comments
 (0)