diff --git a/aws/resource_aws_launch_template.go b/aws/resource_aws_launch_template.go index 06e4261b0abe..835457b43974 100644 --- a/aws/resource_aws_launch_template.go +++ b/aws/resource_aws_launch_template.go @@ -430,8 +430,14 @@ func resourceAwsLaunchTemplate() *schema.Resource { ValidateFunc: validateTypeStringNullableBoolean, }, "delete_on_termination": { - Type: schema.TypeBool, - Optional: true, + // Use TypeString to allow an "unspecified" value, + // since TypeBool only has true/false with false default. + // The conversion from bare true/false values in + // configurations to TypeString value is currently safe. + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: suppressEquivalentTypeStringBoolean, + ValidateFunc: validateTypeStringNullableBoolean, }, "description": { Type: schema.TypeString, @@ -1119,19 +1125,22 @@ func getNetworkInterfaces(n []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecifi var ipv4Addresses []string networkInterface := map[string]interface{}{ - "delete_on_termination": aws.BoolValue(v.DeleteOnTermination), - "description": aws.StringValue(v.Description), - "device_index": aws.Int64Value(v.DeviceIndex), - "ipv4_address_count": aws.Int64Value(v.SecondaryPrivateIpAddressCount), - "ipv6_address_count": aws.Int64Value(v.Ipv6AddressCount), - "network_interface_id": aws.StringValue(v.NetworkInterfaceId), - "private_ip_address": aws.StringValue(v.PrivateIpAddress), - "subnet_id": aws.StringValue(v.SubnetId), + "description": aws.StringValue(v.Description), + "device_index": aws.Int64Value(v.DeviceIndex), + "ipv4_address_count": aws.Int64Value(v.SecondaryPrivateIpAddressCount), + "ipv6_address_count": aws.Int64Value(v.Ipv6AddressCount), + "network_interface_id": aws.StringValue(v.NetworkInterfaceId), + "private_ip_address": aws.StringValue(v.PrivateIpAddress), + "subnet_id": aws.StringValue(v.SubnetId), } if v.AssociatePublicIpAddress != nil { networkInterface["associate_public_ip_address"] = strconv.FormatBool(aws.BoolValue(v.AssociatePublicIpAddress)) } + if v.DeleteOnTermination != nil { + networkInterface["delete_on_termination"] = strconv.FormatBool(aws.BoolValue(v.DeleteOnTermination)) + } + if len(v.Ipv6Addresses) > 0 { raw, ok := networkInterface["ipv6_addresses"] if !ok { @@ -1507,8 +1516,12 @@ func readNetworkInterfacesFromConfig(ni map[string]interface{}) (*ec2.LaunchTemp var privateIpAddress string networkInterface := &ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{} - if v, ok := ni["delete_on_termination"]; ok { - networkInterface.DeleteOnTermination = aws.Bool(v.(bool)) + if v, ok := ni["delete_on_termination"]; ok && v.(string) != "" { + vBool, err := strconv.ParseBool(v.(string)) + if err != nil { + return nil, fmt.Errorf("error converting delete_on_termination %q from string to boolean: %w", v.(string), err) + } + networkInterface.DeleteOnTermination = aws.Bool(vBool) } if v, ok := ni["description"].(string); ok && v != "" { diff --git a/aws/resource_aws_launch_template_test.go b/aws/resource_aws_launch_template_test.go index 98f0ddee17a7..2dab0724ae13 100644 --- a/aws/resource_aws_launch_template_test.go +++ b/aws/resource_aws_launch_template_test.go @@ -285,6 +285,61 @@ func TestAccAWSLaunchTemplate_ElasticInferenceAccelerator(t *testing.T) { }) } +func TestAccAWSLaunchTemplate_NetworkInterfaces_DeleteOnTermination(t *testing.T) { + var template ec2.LaunchTemplate + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_launch_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLaunchTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLaunchTemplateConfig_NetworkInterfaces_DeleteOnTermination(rName, "true"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchTemplateExists(resourceName, &template), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.#", "1"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.security_groups.#", "1"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.delete_on_termination", "true"), + ), + }, + { + Config: testAccAWSLaunchTemplateConfig_NetworkInterfaces_DeleteOnTermination(rName, "false"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchTemplateExists(resourceName, &template), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.#", "1"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.security_groups.#", "1"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.delete_on_termination", "false"), + ), + }, + { + Config: testAccAWSLaunchTemplateConfig_NetworkInterfaces_DeleteOnTermination(rName, "\"\""), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchTemplateExists(resourceName, &template), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.#", "1"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.security_groups.#", "1"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.delete_on_termination", ""), + ), + }, + { + Config: testAccAWSLaunchTemplateConfig_NetworkInterfaces_DeleteOnTermination(rName, "null"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchTemplateExists(resourceName, &template), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.#", "1"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.security_groups.#", "1"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.delete_on_termination", ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccAWSLaunchTemplate_data(t *testing.T) { var template ec2.LaunchTemplate resourceName := "aws_launch_template.test" @@ -313,6 +368,7 @@ func TestAccAWSLaunchTemplate_data(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "monitoring.#", "1"), resource.TestCheckResourceAttr(resourceName, "network_interfaces.#", "1"), resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.security_groups.#", "1"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.delete_on_termination", ""), resource.TestCheckResourceAttr(resourceName, "placement.#", "1"), resource.TestCheckResourceAttrSet(resourceName, "ram_disk_id"), resource.TestCheckResourceAttr(resourceName, "vpc_security_group_ids.#", "1"), @@ -625,6 +681,7 @@ func TestAccAWSLaunchTemplate_networkInterface(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "network_interfaces.#", "1"), resource.TestCheckResourceAttrSet(resourceName, "network_interfaces.0.network_interface_id"), resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.associate_public_ip_address", ""), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.delete_on_termination", ""), resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.ipv4_address_count", "2"), ), }, @@ -1253,6 +1310,20 @@ resource "aws_autoscaling_group" "test" { `, rName, deleteOnTermination, rName) } +func testAccAWSLaunchTemplateConfig_NetworkInterfaces_DeleteOnTermination(rName string, deleteOnTermination string) string { + return fmt.Sprintf(` +resource "aws_launch_template" "test" { + name = %q + + network_interfaces { + network_interface_id = "eni-123456ab" + security_groups = ["sg-1a23bc45"] + delete_on_termination = %s + } +} +`, rName, deleteOnTermination) +} + func testAccAWSLaunchTemplateConfig_EbsOptimized(rName, ebsOptimized string) string { return fmt.Sprintf(` resource "aws_launch_template" "test" { diff --git a/website/docs/guides/version-3-upgrade.html.md b/website/docs/guides/version-3-upgrade.html.md index 01b50072de63..12324dc4cef4 100644 --- a/website/docs/guides/version-3-upgrade.html.md +++ b/website/docs/guides/version-3-upgrade.html.md @@ -27,6 +27,7 @@ Upgrade topics: - [Resource: aws_dx_gateway](#resource-aws_dx_gateway) - [Resource: aws_elastic_transcoder_preset](#resource-aws_elastic_transcoder_preset) - [Resource: aws_emr_cluster](#resource-aws_emr_cluster) +- [Resource: aws_launch_template](#resource-aws_launch_template) - [Resource: aws_lb_listener_rule](#resource-aws_lb_listener_rule) - [Resource: aws_msk_cluster](#resource-aws_msk_cluster) - [Resource: aws_s3_bucket](#resource-aws_s3_bucket) @@ -376,6 +377,40 @@ resource "aws_emr_cluster" "example" { } ``` +## Resource: aws_launch_template + +### network_interfaces.delete_on_termination Argument type change + +The `network_interfaces.delete_on_termination` argument is now of type `string`, allowing an unspecified value for the argument since the previous `bool` type only allowed for `true/false` and defaulted to `false` when no value was set. Now to enforce `delete_on_termination` to `false`, the string `"false"` or bare `false` value must be used. + +For example, given this previous configuration: + +```hcl +resource "aws_launch_template" "example" { + # ... other configuration ... + + network_interfaces { + # ... other configuration ... + + delete_on_termination = null + } +} +``` + +An updated configuration: + +```hcl +resource "aws_launch_template" "example" { + # ... other configuration ... + + network_interfaces { + # ... other configuration ... + + delete_on_termination = false + } +} +``` + ## Resource: aws_lb_listener_rule ### condition.field and condition.values Arguments Removal