diff --git a/aws/resource_aws_ec2_fleet_test.go b/aws/resource_aws_ec2_fleet_test.go index b3a44fb3d5d..2ee8f058d69 100644 --- a/aws/resource_aws_ec2_fleet_test.go +++ b/aws/resource_aws_ec2_fleet_test.go @@ -4,7 +4,9 @@ import ( "errors" "fmt" "strconv" + "strings" "testing" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" @@ -986,6 +988,183 @@ func TestAccAWSEc2Fleet_Type(t *testing.T) { }) } +// Test for the bug described in https://github.com/terraform-providers/terraform-provider-aws/issues/6777 +func TestAccAWSEc2Fleet_TemplateMultipleNetworkInterfaces(t *testing.T) { + var fleet1 ec2.FleetData + resourceName := "aws_ec2_fleet.test" + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2Fleet(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEc2FleetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEc2FleetConfig_multipleNetworkInterfaces(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEc2FleetExists(resourceName, &fleet1), + resource.TestCheckResourceAttr(resourceName, "type", "maintain"), + testAccCheckAWSEc2FleetHistory(resourceName, "The associatePublicIPAddress parameter cannot be specified when launching with multiple network interfaces"), + ), + }, + }, + }) +} + +func testAccAWSEc2FleetConfig_multipleNetworkInterfaces(rInt int) string { + return fmt.Sprintf(` +data "aws_ami" "test" { + most_recent = true + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + owners = ["099720109477"] # Canonical +} + +resource "aws_vpc" "test" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_internet_gateway" "test" { + vpc_id = "${aws_vpc.test.id}" +} + +resource "aws_subnet" "test" { + cidr_block = "10.1.0.0/24" + vpc_id = "${aws_vpc.test.id}" +} + +resource "aws_security_group" "test" { + name = "security-group-%d" + description = "Testacc SSH security group" + vpc_id = "${aws_vpc.test.id}" + + ingress { + protocol = "tcp" + from_port = 22 + to_port = 22 + cidr_blocks = ["0.0.0.0/0"] + } + egress { + protocol = "-1" + from_port = 0 + to_port = 0 + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_network_interface" "test" { + subnet_id = "${aws_subnet.test.id}" + security_groups = ["${aws_security_group.test.id}"] +} + +resource "aws_launch_template" "test" { + name = "testacc-lt-%d" + image_id = "${data.aws_ami.test.id}" + + instance_market_options { + spot_options { + spot_instance_type = "persistent" + } + market_type="spot" + } + + network_interfaces { + device_index = 0 + delete_on_termination = true + network_interface_id = "${aws_network_interface.test.id}" + } + network_interfaces { + device_index = 1 + delete_on_termination = true + subnet_id = "${aws_subnet.test.id}" + } + +} + +resource "aws_ec2_fleet" "test" { + terminate_instances = true + + launch_template_config { + launch_template_specification { + launch_template_id = "${aws_launch_template.test.id}" + version = "${aws_launch_template.test.latest_version}" + } + # allow to choose from several instance types if there is no spot capacity for some type + override { + instance_type = "t2.micro" + } + override { + instance_type = "t3.micro" + } + override { + instance_type = "t3.small" + } + } + + target_capacity_specification { + default_target_capacity_type = "spot" + total_target_capacity = 1 + } +} +`, rInt, rInt) +} + +func testAccCheckAWSEc2FleetHistory(resourceName string, errorMsg string) resource.TestCheckFunc { + return func(s *terraform.State) error { + time.Sleep(time.Minute * 2) // We have to wait a bit for the history to get populated. + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No EC2 Fleet ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + input := &ec2.DescribeFleetHistoryInput{ + FleetId: aws.String(rs.Primary.ID), + StartTime: aws.Time(time.Now().Add(time.Hour * -2)), + } + + output, err := conn.DescribeFleetHistory(input) + + if err != nil { + return err + } + + if output == nil { + return fmt.Errorf("EC2 Fleet history not found") + } + + if output.HistoryRecords == nil { + return fmt.Errorf("No fleet history records found for fleet %s", rs.Primary.ID) + } + + for _, record := range output.HistoryRecords { + if record == nil { + continue + } + if strings.Contains(aws.StringValue(record.EventInformation.EventDescription), errorMsg) { + return fmt.Errorf("Error %s found in fleet history event", errorMsg) + } + } + + return nil + } +} + func testAccCheckAWSEc2FleetExists(resourceName string, fleet *ec2.FleetData) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] diff --git a/aws/resource_aws_launch_template.go b/aws/resource_aws_launch_template.go index 0d91c0d9488..c4b7248328b 100644 --- a/aws/resource_aws_launch_template.go +++ b/aws/resource_aws_launch_template.go @@ -357,8 +357,10 @@ func resourceAwsLaunchTemplate() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "associate_public_ip_address": { - Type: schema.TypeBool, - Optional: true, + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: suppressEquivalentTypeStringBoolean, + ValidateFunc: validateTypeStringNullableBoolean, }, "delete_on_termination": { Type: schema.TypeBool, @@ -921,15 +923,17 @@ func getNetworkInterfaces(n []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecifi var ipv4Addresses []string networkInterface := map[string]interface{}{ - "associate_public_ip_address": aws.BoolValue(v.AssociatePublicIpAddress), - "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), + "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), + } + if v.AssociatePublicIpAddress != nil { + networkInterface["associate_public_ip_address"] = strconv.FormatBool(aws.BoolValue(v.AssociatePublicIpAddress)) } if len(v.Ipv6Addresses) > 0 { @@ -1149,7 +1153,10 @@ func buildLaunchTemplateData(d *schema.ResourceData) (*ec2.RequestLaunchTemplate continue } niData := ni.(map[string]interface{}) - networkInterface := readNetworkInterfacesFromConfig(niData) + networkInterface, err := readNetworkInterfacesFromConfig(niData) + if err != nil { + return nil, err + } networkInterfaces = append(networkInterfaces, networkInterface) } opts.NetworkInterfaces = networkInterfaces @@ -1256,7 +1263,7 @@ func readEbsBlockDeviceFromConfig(ebs map[string]interface{}) (*ec2.LaunchTempla return ebsDevice, nil } -func readNetworkInterfacesFromConfig(ni map[string]interface{}) *ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest { +func readNetworkInterfacesFromConfig(ni map[string]interface{}) (*ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest, error) { var ipv4Addresses []*ec2.PrivateIpAddressSpecification var ipv6Addresses []*ec2.InstanceIpv6AddressRequest var privateIpAddress string @@ -1276,8 +1283,14 @@ func readNetworkInterfacesFromConfig(ni map[string]interface{}) *ec2.LaunchTempl if v, ok := ni["network_interface_id"].(string); ok && v != "" { networkInterface.NetworkInterfaceId = aws.String(v) - } else if v, ok := ni["associate_public_ip_address"]; ok { - networkInterface.AssociatePublicIpAddress = aws.Bool(v.(bool)) + } + + if v, ok := ni["associate_public_ip_address"]; ok && v.(string) != "" { + vBool, err := strconv.ParseBool(v.(string)) + if err != nil { + return nil, fmt.Errorf("error converting associate_public_ip_address %q from string to boolean: %s", v.(string), err) + } + networkInterface.AssociatePublicIpAddress = aws.Bool(vBool) } if v, ok := ni["private_ip_address"].(string); ok && v != "" { @@ -1320,7 +1333,7 @@ func readNetworkInterfacesFromConfig(ni map[string]interface{}) *ec2.LaunchTempl networkInterface.PrivateIpAddresses = ipv4Addresses } - return networkInterface + return networkInterface, nil } func readIamInstanceProfileFromConfig(iip map[string]interface{}) *ec2.LaunchTemplateIamInstanceProfileSpecificationRequest { diff --git a/aws/resource_aws_launch_template_test.go b/aws/resource_aws_launch_template_test.go index a73a3d9fa12..c54bee67d35 100644 --- a/aws/resource_aws_launch_template_test.go +++ b/aws/resource_aws_launch_template_test.go @@ -541,7 +541,7 @@ func TestAccAWSLaunchTemplate_networkInterface(t *testing.T) { testAccCheckAWSLaunchTemplateExists(resourceName, &template), 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", "false"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.associate_public_ip_address", ""), resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.ipv4_address_count", "2"), ), }, @@ -554,6 +554,55 @@ func TestAccAWSLaunchTemplate_networkInterface(t *testing.T) { }) } +func TestAccAWSLaunchTemplate_associatePublicIPAddress(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_associatePublicIpAddress(rName, "true"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchTemplateExists(resourceName, &template), + 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", "true"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.ipv4_address_count", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSLaunchTemplateConfig_associatePublicIpAddress(rName, "false"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchTemplateExists(resourceName, &template), + 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", "false"), + resource.TestCheckResourceAttr(resourceName, "network_interfaces.0.ipv4_address_count", "2"), + ), + }, + { + Config: testAccAWSLaunchTemplateConfig_associatePublicIpAddress(rName, "null"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchTemplateExists(resourceName, &template), + 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.ipv4_address_count", "2"), + ), + }, + }, + }) +} + func TestAccAWSLaunchTemplate_networkInterface_ipv6Addresses(t *testing.T) { var template ec2.LaunchTemplate resourceName := "aws_launch_template.test" @@ -1077,6 +1126,33 @@ resource "aws_launch_template" "test" { } ` +func testAccAWSLaunchTemplateConfig_associatePublicIpAddress(rName, associatePublicIPAddress string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_subnet" "test" { + vpc_id = "${aws_vpc.test.id}" + cidr_block = "10.1.0.0/24" +} + +resource "aws_network_interface" "test" { + subnet_id = "${aws_subnet.test.id}" +} + +resource "aws_launch_template" "test" { + name = %[1]q + + network_interfaces { + network_interface_id = "${aws_network_interface.test.id}" + associate_public_ip_address = %[2]s + ipv4_address_count = 2 + } +} +`, rName, associatePublicIPAddress) +} + const testAccAWSLaunchTemplateConfig_networkInterface_ipv6Addresses = ` resource "aws_launch_template" "test" { name = "network-interface-ipv6-addresses-launch-template"