diff --git a/aws/resource_aws_network_interface.go b/aws/resource_aws_network_interface.go index afe43d40c63..7bc23ef4447 100644 --- a/aws/resource_aws_network_interface.go +++ b/aws/resource_aws_network_interface.go @@ -9,10 +9,10 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" ) @@ -56,7 +56,6 @@ func resourceAwsNetworkInterface() *schema.Resource { Optional: true, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, "private_ips_count": { @@ -70,7 +69,6 @@ func resourceAwsNetworkInterface() *schema.Resource { Optional: true, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, "source_dest_check": { @@ -113,6 +111,22 @@ func resourceAwsNetworkInterface() *schema.Resource { }, "tags": tagsSchema(), + "ipv6_address_count": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ConflictsWith: []string{"ipv6_addresses"}, + }, + "ipv6_addresses": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv6Address, + }, + ConflictsWith: []string{"ipv6_address_count"}, + }, }, } } @@ -126,14 +140,12 @@ func resourceAwsNetworkInterfaceCreate(d *schema.ResourceData, meta interface{}) TagSpecifications: ec2TagSpecificationsFromMap(d.Get("tags").(map[string]interface{}), ec2.ResourceTypeNetworkInterface), } - security_groups := d.Get("security_groups").(*schema.Set).List() - if len(security_groups) != 0 { - request.Groups = expandStringList(security_groups) + if v, ok := d.GetOk("security_groups"); ok && v.(*schema.Set).Len() > 0 { + request.Groups = expandStringSet(v.(*schema.Set)) } - private_ips := d.Get("private_ips").(*schema.Set).List() - if len(private_ips) != 0 { - request.PrivateIpAddresses = expandPrivateIPAddresses(private_ips) + if v, ok := d.GetOk("private_ips"); ok && v.(*schema.Set).Len() > 0 { + request.PrivateIpAddresses = expandPrivateIPAddresses(v.(*schema.Set).List()) } if v, ok := d.GetOk("description"); ok { @@ -144,6 +156,14 @@ func resourceAwsNetworkInterfaceCreate(d *schema.ResourceData, meta interface{}) request.SecondaryPrivateIpAddressCount = aws.Int64(int64(v.(int))) } + if v, ok := d.GetOk("ipv6_address_count"); ok { + request.Ipv6AddressCount = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("ipv6_addresses"); ok && v.(*schema.Set).Len() > 0 { + request.Ipv6Addresses = expandIP6Addresses(v.(*schema.Set).List()) + } + log.Printf("[DEBUG] Creating network interface") resp, err := conn.CreateNetworkInterface(request) if err != nil { @@ -152,7 +172,38 @@ func resourceAwsNetworkInterfaceCreate(d *schema.ResourceData, meta interface{}) d.SetId(*resp.NetworkInterface.NetworkInterfaceId) - return resourceAwsNetworkInterfaceUpdate(d, meta) + if err := waitForNetworkInterfaceCreation(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return fmt.Errorf("error waiting for Network Interface (%s) creation: %s", d.Id(), err) + } + + //Default value is enabled + if !d.Get("source_dest_check").(bool) { + request := &ec2.ModifyNetworkInterfaceAttributeInput{ + NetworkInterfaceId: aws.String(d.Id()), + SourceDestCheck: &ec2.AttributeBooleanValue{Value: aws.Bool(false)}, + } + + _, err := conn.ModifyNetworkInterfaceAttribute(request) + if err != nil { + return fmt.Errorf("Failure updating SourceDestCheck: %s", err) + } + } + + if v, ok := d.GetOk("attachment"); ok && v.(*schema.Set).Len() > 0 { + attachment := v.(*schema.Set).List()[0].(map[string]interface{}) + di := attachment["device_index"].(int) + attachReq := &ec2.AttachNetworkInterfaceInput{ + DeviceIndex: aws.Int64(int64(di)), + InstanceId: aws.String(attachment["instance"].(string)), + NetworkInterfaceId: aws.String(d.Id()), + } + _, err := conn.AttachNetworkInterface(attachReq) + if err != nil { + return fmt.Errorf("Error attaching ENI: %s", err) + } + } + + return resourceAwsNetworkInterfaceRead(d, meta) } func resourceAwsNetworkInterfaceRead(d *schema.ResourceData, meta interface{}) error { @@ -165,8 +216,9 @@ func resourceAwsNetworkInterfaceRead(d *schema.ResourceData, meta interface{}) e describeResp, err := conn.DescribeNetworkInterfaces(describe_network_interfaces_request) if err != nil { - if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidNetworkInterfaceID.NotFound" { + if isAWSErr(err, "InvalidNetworkInterfaceID.NotFound", "") { // The ENI is gone now, so just remove it from the state + log.Printf("[WARN] EC2 Network Interface (%s) not found, removing from state", d.Id()) d.SetId("") return nil } @@ -207,6 +259,11 @@ func resourceAwsNetworkInterfaceRead(d *schema.ResourceData, meta interface{}) e d.Set("source_dest_check", eni.SourceDestCheck) d.Set("subnet_id", eni.SubnetId) + d.Set("ipv6_address_count", len(eni.Ipv6Addresses)) + + if err := d.Set("ipv6_addresses", flattenEc2NetworkInterfaceIpv6Address(eni.Ipv6Addresses)); err != nil { + return fmt.Errorf("error setting ipv6 addresses: %s", err) + } if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(eni.TagSet).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %s", err) @@ -246,7 +303,7 @@ func resourceAwsNetworkInterfaceDetach(oa *schema.Set, meta interface{}, eniId s conn := meta.(*AWSClient).ec2conn _, detach_err := conn.DetachNetworkInterface(detach_request) if detach_err != nil { - if awsErr, _ := detach_err.(awserr.Error); awsErr.Code() != "InvalidAttachmentID.NotFound" { + if !isAWSErr(detach_err, "InvalidAttachmentID.NotFound", "") { return fmt.Errorf("Error detaching ENI: %s", detach_err) } } @@ -311,7 +368,7 @@ func resourceAwsNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) if unassignIps.Len() != 0 { input := &ec2.UnassignPrivateIpAddressesInput{ NetworkInterfaceId: aws.String(d.Id()), - PrivateIpAddresses: expandStringList(unassignIps.List()), + PrivateIpAddresses: expandStringSet(unassignIps), } _, err := conn.UnassignPrivateIpAddresses(input) if err != nil { @@ -324,7 +381,7 @@ func resourceAwsNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) if assignIps.Len() != 0 { input := &ec2.AssignPrivateIpAddressesInput{ NetworkInterfaceId: aws.String(d.Id()), - PrivateIpAddresses: expandStringList(assignIps.List()), + PrivateIpAddresses: expandStringSet(assignIps), } _, err := conn.AssignPrivateIpAddresses(input) if err != nil { @@ -333,9 +390,79 @@ func resourceAwsNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) } } - // ModifyNetworkInterfaceAttribute needs to be called after creating an ENI - // since CreateNetworkInterface doesn't take SourceDeskCheck parameter. - if d.HasChange("source_dest_check") || d.IsNewResource() { + if d.HasChange("ipv6_addresses") { + o, n := d.GetChange("ipv6_addresses") + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) + } + + os := o.(*schema.Set) + ns := n.(*schema.Set) + + // Unassign old IPV6 addresses + unassignIps := os.Difference(ns) + if unassignIps.Len() != 0 { + input := &ec2.UnassignIpv6AddressesInput{ + NetworkInterfaceId: aws.String(d.Id()), + Ipv6Addresses: expandStringSet(unassignIps), + } + _, err := conn.UnassignIpv6Addresses(input) + if err != nil { + return fmt.Errorf("failure to unassign IPV6 Addresses: %s", err) + } + } + + // Assign new IPV6 addresses + assignIps := ns.Difference(os) + if assignIps.Len() != 0 { + input := &ec2.AssignIpv6AddressesInput{ + NetworkInterfaceId: aws.String(d.Id()), + Ipv6Addresses: expandStringSet(assignIps), + } + _, err := conn.AssignIpv6Addresses(input) + if err != nil { + return fmt.Errorf("Failure to assign IPV6 Addresses: %s", err) + } + } + } + + if d.HasChange("ipv6_address_count") { + o, n := d.GetChange("ipv6_address_count") + ipv6Addresses := d.Get("ipv6_addresses").(*schema.Set).List() + + if o != nil && n != nil && n != len(ipv6Addresses) { + + diff := n.(int) - o.(int) + + // Surplus of IPs, add the diff + if diff > 0 { + input := &ec2.AssignIpv6AddressesInput{ + NetworkInterfaceId: aws.String(d.Id()), + Ipv6AddressCount: aws.Int64(int64(diff)), + } + _, err := conn.AssignIpv6Addresses(input) + if err != nil { + return fmt.Errorf("failure to assign IPV6 Addresses: %s", err) + } + } + + if diff < 0 { + input := &ec2.UnassignIpv6AddressesInput{ + NetworkInterfaceId: aws.String(d.Id()), + Ipv6Addresses: expandStringList(ipv6Addresses[0:int(math.Abs(float64(diff)))]), + } + _, err := conn.UnassignIpv6Addresses(input) + if err != nil { + return fmt.Errorf("failure to unassign IPV6 Addresses: %s", err) + } + } + } + } + + if d.HasChange("source_dest_check") { request := &ec2.ModifyNetworkInterfaceAttributeInput{ NetworkInterfaceId: aws.String(d.Id()), SourceDestCheck: &ec2.AttributeBooleanValue{Value: aws.Bool(d.Get("source_dest_check").(bool))}, @@ -343,23 +470,23 @@ func resourceAwsNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) _, err := conn.ModifyNetworkInterfaceAttribute(request) if err != nil { - return fmt.Errorf("Failure updating ENI: %s", err) + return fmt.Errorf("failure updating Source Dest Check on ENI: %s", err) } } - if d.HasChange("private_ips_count") && !d.IsNewResource() { + if d.HasChange("private_ips_count") { o, n := d.GetChange("private_ips_count") - private_ips := d.Get("private_ips").(*schema.Set).List() - private_ips_filtered := private_ips[:0] - primary_ip := d.Get("private_ip") + privateIPs := d.Get("private_ips").(*schema.Set).List() + privateIPsFiltered := privateIPs[:0] + primaryIP := d.Get("private_ip") - for _, ip := range private_ips { - if ip != primary_ip { - private_ips_filtered = append(private_ips_filtered, ip) + for _, ip := range privateIPs { + if ip != primaryIP { + privateIPsFiltered = append(privateIPsFiltered, ip) } } - if o != nil && n != nil && n != len(private_ips_filtered) { + if o != nil && n != nil && n != len(privateIPsFiltered) { diff := n.(int) - o.(int) @@ -378,7 +505,7 @@ func resourceAwsNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) if diff < 0 { input := &ec2.UnassignPrivateIpAddressesInput{ NetworkInterfaceId: aws.String(d.Id()), - PrivateIpAddresses: expandStringList(private_ips_filtered[0:int(math.Abs(float64(diff)))]), + PrivateIpAddresses: expandStringList(privateIPsFiltered[0:int(math.Abs(float64(diff)))]), } _, err := conn.UnassignPrivateIpAddresses(input) if err != nil { @@ -391,7 +518,7 @@ func resourceAwsNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) if d.HasChange("security_groups") { request := &ec2.ModifyNetworkInterfaceAttributeInput{ NetworkInterfaceId: aws.String(d.Id()), - Groups: expandStringList(d.Get("security_groups").(*schema.Set).List()), + Groups: expandStringSet(d.Get("security_groups").(*schema.Set)), } _, err := conn.ModifyNetworkInterfaceAttribute(request) @@ -412,7 +539,7 @@ func resourceAwsNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) } } - if d.HasChange("tags") && !d.IsNewResource() { + if d.HasChange("tags") { o, n := d.GetChange("tags") if err := keyvaluetags.Ec2UpdateTags(conn, d.Id(), o, n); err != nil { @@ -428,9 +555,8 @@ func resourceAwsNetworkInterfaceDelete(d *schema.ResourceData, meta interface{}) log.Printf("[INFO] Deleting ENI: %s", d.Id()) - detach_err := resourceAwsNetworkInterfaceDetach(d.Get("attachment").(*schema.Set), meta, d.Id()) - if detach_err != nil { - return detach_err + if err := resourceAwsNetworkInterfaceDetach(d.Get("attachment").(*schema.Set), meta, d.Id()); err != nil { + return err } deleteEniOpts := ec2.DeleteNetworkInterfaceInput{ @@ -581,3 +707,17 @@ func networkInterfaceStateRefresh(conn *ec2.EC2, eniId string) resource.StateRef } } } + +func waitForNetworkInterfaceCreation(conn *ec2.EC2, id string, timeout time.Duration) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"pending"}, + Target: []string{ec2.NetworkInterfaceStatusAvailable}, + Refresh: networkInterfaceStateRefresh(conn, id), + Timeout: timeout, + Delay: 30 * time.Second, + } + + _, err := stateConf.WaitForState() + + return err +} diff --git a/aws/resource_aws_network_interface_sg_attachment_test.go b/aws/resource_aws_network_interface_sg_attachment_test.go index 4bc60834103..3fe47374d08 100644 --- a/aws/resource_aws_network_interface_sg_attachment_test.go +++ b/aws/resource_aws_network_interface_sg_attachment_test.go @@ -56,7 +56,7 @@ func TestAccAWSNetworkInterfaceSGAttachment_disappears(t *testing.T) { Config: testAccAwsNetworkInterfaceSGAttachmentConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSNetworkInterfaceSGAttachmentExists(resourceName, &networkInterface), - testAccCheckAWSENIDisappears(&networkInterface), + testAccCheckResourceDisappears(testAccProvider, resourceAwsNetworkInterfaceSGAttachment(), resourceName), ), ExpectNonEmptyPlan: true, }, diff --git a/aws/resource_aws_network_interface_test.go b/aws/resource_aws_network_interface_test.go index fb2cd03353b..3d0b34ee889 100644 --- a/aws/resource_aws_network_interface_test.go +++ b/aws/resource_aws_network_interface_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -71,35 +70,30 @@ func testSweepEc2NetworkInterfaces(region string) error { func TestAccAWSENI_basic(t *testing.T) { var conf ec2.NetworkInterface + resourceName := "aws_network_interface.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_network_interface.bar", + IDRefreshName: resourceName, Providers: testAccProviders, CheckDestroy: testAccCheckAWSENIDestroy, Steps: []resource.TestStep{ { Config: testAccAWSENIConfig(), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSENIExists("aws_network_interface.bar", &conf), + testAccCheckAWSENIExists(resourceName, &conf), testAccCheckAWSENIAttributes(&conf), + resource.TestCheckResourceAttr(resourceName, "private_ips.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "private_dns_name"), + resource.TestCheckResourceAttrSet(resourceName, "mac_address"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "0"), + resource.TestCheckResourceAttr(resourceName, "description", "Managed by Terraform"), testAccCheckAWSENIAvailabilityZone("data.aws_availability_zones.available", "names.0", &conf), - resource.TestCheckResourceAttr( - "aws_network_interface.bar", "private_ips.#", "1"), - resource.TestCheckResourceAttrSet( - "aws_network_interface.bar", "private_dns_name"), - resource.TestCheckResourceAttrSet( - "aws_network_interface.bar", "mac_address"), - resource.TestCheckResourceAttr( - "aws_network_interface.bar", "description", "Managed by Terraform"), - resource.TestCheckResourceAttr( - "aws_network_interface.bar", "outpost_arn", ""), - resource.TestCheckResourceAttr( - "aws_network_interface.bar", "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "outpost_arn", ""), ), }, { - ResourceName: "aws_network_interface.bar", + ResourceName: resourceName, ImportState: true, ImportStateVerify: true, }, @@ -107,6 +101,53 @@ func TestAccAWSENI_basic(t *testing.T) { }) } +func TestAccAWSENI_ipv6(t *testing.T) { + var conf ec2.NetworkInterface + resourceName := "aws_network_interface.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: resourceName, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSENIDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSENIIPV6Config(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSENIExists(resourceName, &conf), + testAccCheckAWSENIAttributes(&conf), + resource.TestCheckResourceAttr(resourceName, "ipv6_address_count", "1"), + resource.TestCheckResourceAttr(resourceName, "ipv6_addresses.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSENIIPV6MultipleConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSENIExists(resourceName, &conf), + testAccCheckAWSENIAttributes(&conf), + resource.TestCheckResourceAttr(resourceName, "ipv6_address_count", "2"), + resource.TestCheckResourceAttr(resourceName, "ipv6_addresses.#", "2"), + ), + }, + { + Config: testAccAWSENIIPV6Config(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSENIExists(resourceName, &conf), + testAccCheckAWSENIAttributes(&conf), + resource.TestCheckResourceAttr(resourceName, "ipv6_address_count", "1"), + resource.TestCheckResourceAttr(resourceName, "ipv6_addresses.#", "1"), + ), + }, + }, + }) +} + func TestAccAWSENI_tags(t *testing.T) { resourceName := "aws_network_interface.test" rName := acctest.RandomWithPrefix("tf-acc-test") @@ -152,9 +193,57 @@ func TestAccAWSENI_tags(t *testing.T) { }) } +func TestAccAWSENI_ipv6_count(t *testing.T) { + var conf ec2.NetworkInterface + resourceName := "aws_network_interface.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: resourceName, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSENIDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSENIIPV6CountConfig(1, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSENIExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "ipv6_address_count", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSENIIPV6CountConfig(2, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSENIExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "ipv6_address_count", "2"), + ), + }, + { + Config: testAccAWSENIIPV6CountConfig(0, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSENIExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "ipv6_address_count", "0"), + ), + }, + { + Config: testAccAWSENIIPV6CountConfig(1, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSENIExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "ipv6_address_count", "1"), + ), + }, + }, + }) +} + func TestAccAWSENI_disappears(t *testing.T) { var networkInterface ec2.NetworkInterface - resourceName := "aws_network_interface.bar" + resourceName := "aws_network_interface.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -165,7 +254,7 @@ func TestAccAWSENI_disappears(t *testing.T) { Config: testAccAWSENIConfig(), Check: resource.ComposeTestCheckFunc( testAccCheckAWSENIExists(resourceName, &networkInterface), - testAccCheckAWSENIDisappears(&networkInterface), + testAccCheckResourceDisappears(testAccProvider, resourceAwsNetworkInterface(), resourceName), ), ExpectNonEmptyPlan: true, }, @@ -175,32 +264,31 @@ func TestAccAWSENI_disappears(t *testing.T) { func TestAccAWSENI_updatedDescription(t *testing.T) { var conf ec2.NetworkInterface + resourceName := "aws_network_interface.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_network_interface.bar", + IDRefreshName: resourceName, Providers: testAccProviders, CheckDestroy: testAccCheckAWSENIDestroy, Steps: []resource.TestStep{ { Config: testAccAWSENIConfig(), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSENIExists("aws_network_interface.bar", &conf), - resource.TestCheckResourceAttr( - "aws_network_interface.bar", "description", "Managed by Terraform"), + testAccCheckAWSENIExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "description", "Managed by Terraform"), ), }, { - ResourceName: "aws_network_interface.bar", + ResourceName: resourceName, ImportState: true, ImportStateVerify: true, }, { Config: testAccAWSENIConfigUpdatedDescription(), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSENIExists("aws_network_interface.bar", &conf), - resource.TestCheckResourceAttr( - "aws_network_interface.bar", "description", "Updated ENI Description"), + testAccCheckAWSENIExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "description", "Updated ENI Description"), ), }, }, @@ -209,27 +297,25 @@ func TestAccAWSENI_updatedDescription(t *testing.T) { func TestAccAWSENI_attached(t *testing.T) { var conf ec2.NetworkInterface + resourceName := "aws_network_interface.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_network_interface.bar", + IDRefreshName: resourceName, Providers: testAccProviders, CheckDestroy: testAccCheckAWSENIDestroy, Steps: []resource.TestStep{ { Config: testAccAWSENIConfigWithAttachment(), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSENIExists("aws_network_interface.bar", &conf), + testAccCheckAWSENIExists(resourceName, &conf), testAccCheckAWSENIAttributesWithAttachment(&conf), testAccCheckAWSENIAvailabilityZone("data.aws_availability_zones.available", "names.0", &conf), - resource.TestCheckResourceAttr( - "aws_network_interface.bar", "private_ips.#", "1"), - resource.TestCheckResourceAttr( - "aws_network_interface.bar", "tags.Name", "bar_interface"), + resource.TestCheckResourceAttr(resourceName, "private_ips.#", "1"), ), }, { - ResourceName: "aws_network_interface.bar", + ResourceName: resourceName, ImportState: true, ImportStateVerify: true, }, @@ -239,24 +325,25 @@ func TestAccAWSENI_attached(t *testing.T) { func TestAccAWSENI_ignoreExternalAttachment(t *testing.T) { var conf ec2.NetworkInterface + resourceName := "aws_network_interface.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_network_interface.bar", + IDRefreshName: resourceName, Providers: testAccProviders, CheckDestroy: testAccCheckAWSENIDestroy, Steps: []resource.TestStep{ { Config: testAccAWSENIConfigExternalAttachment(), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSENIExists("aws_network_interface.bar", &conf), + testAccCheckAWSENIExists(resourceName, &conf), testAccCheckAWSENIAttributes(&conf), testAccCheckAWSENIAvailabilityZone("data.aws_availability_zones.available", "names.0", &conf), - testAccCheckAWSENIMakeExternalAttachment("aws_instance.foo", &conf), + testAccCheckAWSENIMakeExternalAttachment("aws_instance.test", &conf), ), }, { - ResourceName: "aws_network_interface.bar", + ResourceName: resourceName, ImportState: true, ImportStateVerify: true, }, @@ -266,49 +353,63 @@ func TestAccAWSENI_ignoreExternalAttachment(t *testing.T) { func TestAccAWSENI_sourceDestCheck(t *testing.T) { var conf ec2.NetworkInterface + resourceName := "aws_network_interface.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_network_interface.bar", + IDRefreshName: resourceName, Providers: testAccProviders, CheckDestroy: testAccCheckAWSENIDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSENIConfigWithSourceDestCheck(), + Config: testAccAWSENIConfigWithSourceDestCheck(false), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSENIExists("aws_network_interface.bar", &conf), - resource.TestCheckResourceAttr( - "aws_network_interface.bar", "source_dest_check", "false"), + testAccCheckAWSENIExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "source_dest_check", "false"), ), }, { - ResourceName: "aws_network_interface.bar", + ResourceName: resourceName, ImportState: true, ImportStateVerify: true, }, + { + Config: testAccAWSENIConfigWithSourceDestCheck(true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSENIExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "source_dest_check", "true"), + ), + }, + { + Config: testAccAWSENIConfigWithSourceDestCheck(false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSENIExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "source_dest_check", "false"), + ), + }, }, }) } func TestAccAWSENI_computedIPs(t *testing.T) { var conf ec2.NetworkInterface + resourceName := "aws_network_interface.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_network_interface.bar", + IDRefreshName: resourceName, Providers: testAccProviders, CheckDestroy: testAccCheckAWSENIDestroy, Steps: []resource.TestStep{ { Config: testAccAWSENIConfigWithNoPrivateIPs(), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSENIExists("aws_network_interface.bar", &conf), - resource.TestCheckResourceAttr( - "aws_network_interface.bar", "private_ips.#", "1"), + testAccCheckAWSENIExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "private_ips.#", "1"), ), }, { - ResourceName: "aws_network_interface.bar", + ResourceName: resourceName, ImportState: true, ImportStateVerify: true, }, @@ -389,10 +490,10 @@ func testAccCheckAWSENIExists(n string, res *ec2.NetworkInterface) resource.Test } conn := testAccProvider.Meta().(*AWSClient).ec2conn - describe_network_interfaces_request := &ec2.DescribeNetworkInterfacesInput{ + input := &ec2.DescribeNetworkInterfacesInput{ NetworkInterfaceIds: []*string{aws.String(rs.Primary.ID)}, } - describeResp, err := conn.DescribeNetworkInterfaces(describe_network_interfaces_request) + describeResp, err := conn.DescribeNetworkInterfaces(input) if err != nil { return err @@ -487,13 +588,13 @@ func testAccCheckAWSENIDestroy(s *terraform.State) error { } conn := testAccProvider.Meta().(*AWSClient).ec2conn - describe_network_interfaces_request := &ec2.DescribeNetworkInterfacesInput{ + input := &ec2.DescribeNetworkInterfacesInput{ NetworkInterfaceIds: []*string{aws.String(rs.Primary.ID)}, } - _, err := conn.DescribeNetworkInterfaces(describe_network_interfaces_request) + _, err := conn.DescribeNetworkInterfaces(input) if err != nil { - if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidNetworkInterfaceID.NotFound" { + if isAWSErr(err, "InvalidNetworkInterfaceID.NotFound", "") { return nil } @@ -504,73 +605,99 @@ func testAccCheckAWSENIDestroy(s *terraform.State) error { return nil } -func testAccCheckAWSENIDisappears(networkInterface *ec2.NetworkInterface) resource.TestCheckFunc { - return func(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).ec2conn - - input := &ec2.DeleteNetworkInterfaceInput{ - NetworkInterfaceId: networkInterface.NetworkInterfaceId, - } - _, err := conn.DeleteNetworkInterface(input) - - return err - } -} - func testAccCheckAWSENIMakeExternalAttachment(n string, conf *ec2.NetworkInterface) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok || rs.Primary.ID == "" { return fmt.Errorf("Not found: %s", n) } - attach_request := &ec2.AttachNetworkInterfaceInput{ + input := &ec2.AttachNetworkInterfaceInput{ DeviceIndex: aws.Int64(1), InstanceId: aws.String(rs.Primary.ID), NetworkInterfaceId: conf.NetworkInterfaceId, } conn := testAccProvider.Meta().(*AWSClient).ec2conn - _, attach_err := conn.AttachNetworkInterface(attach_request) - if attach_err != nil { - return fmt.Errorf("Error attaching ENI: %s", attach_err) + _, err := conn.AttachNetworkInterface(input) + if err != nil { + return fmt.Errorf("Error attaching ENI: %s", err) } return nil } } func testAccAWSENIConfig() string { - return fmt.Sprintf(` -resource "aws_vpc" "foo" { + return testAccAvailableAZsNoOptInConfig() + fmt.Sprintf(` +resource "aws_vpc" "test" { cidr_block = "172.16.0.0/16" enable_dns_hostnames = true tags = { - Name = "terraform-testacc-network-interface" + Name = "tf-acc-network-interface" } } -data "aws_availability_zones" "available" { - state = "available" +resource "aws_subnet" "test" { + vpc_id = aws_vpc.test.id + cidr_block = "172.16.10.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + tags = { + Name = "tf-acc-network-interface" + } +} + +resource "aws_security_group" "test" { + vpc_id = aws_vpc.test.id + description = "test" + name = "tf-acc-network-interface" - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] + egress { + from_port = 0 + to_port = 0 + protocol = "tcp" + cidr_blocks = ["10.0.0.0/16"] + } + + tags = { + Name = "tf-acc-network-interface" } } -resource "aws_subnet" "foo" { - vpc_id = aws_vpc.foo.id +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test.id + private_ips = ["172.16.10.100"] + security_groups = [aws_security_group.test.id] + description = "Managed by Terraform" +} +`) +} + +func testAccAWSENIIPV6ConfigBase(rName string) string { + return testAccAvailableAZsNoOptInConfig() + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "172.16.0.0/16" + assign_generated_ipv6_cidr_block = true + enable_dns_hostnames = true + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "test" { + vpc_id = aws_vpc.test.id cidr_block = "172.16.10.0/24" + ipv6_cidr_block = cidrsubnet(aws_vpc.test.ipv6_cidr_block, 8, 16) availability_zone = data.aws_availability_zones.available.names[0] tags = { - Name = "tf-acc-network-interface" + Name = %[1]q } } -resource "aws_security_group" "foo" { - vpc_id = aws_vpc.foo.id - description = "foo" - name = "foo" +resource "aws_security_group" "test" { + vpc_id = aws_vpc.test.id + description = "test" + name = %[1]q egress { from_port = 0 @@ -578,51 +705,74 @@ resource "aws_security_group" "foo" { protocol = "tcp" cidr_blocks = ["10.0.0.0/16"] } + + tags = { + Name = %[1]q + } +} +`, rName) } -resource "aws_network_interface" "bar" { - subnet_id = aws_subnet.foo.id +func testAccAWSENIIPV6Config(rName string) string { + return testAccAWSENIIPV6ConfigBase(rName) + fmt.Sprintf(` +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test.id private_ips = ["172.16.10.100"] - security_groups = [aws_security_group.foo.id] + ipv6_addresses = [cidrhost(aws_subnet.test.ipv6_cidr_block, 4)] + security_groups = [aws_security_group.test.id] description = "Managed by Terraform" } `) } +func testAccAWSENIIPV6MultipleConfig(rName string) string { + return testAccAWSENIIPV6ConfigBase(rName) + fmt.Sprintf(` +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test.id + private_ips = ["172.16.10.100"] + ipv6_addresses = [cidrhost(aws_subnet.test.ipv6_cidr_block, 4), cidrhost(aws_subnet.test.ipv6_cidr_block, 8)] + security_groups = [aws_security_group.test.id] + description = "Managed by Terraform" +} +`) +} + +func testAccAWSENIIPV6CountConfig(ipCount int, rName string) string { + return testAccAWSENIIPV6ConfigBase(rName) + fmt.Sprintf(` +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test.id + private_ips = ["172.16.10.100"] + ipv6_address_count = %[1]d + security_groups = [aws_security_group.test.id] + description = "Managed by Terraform" +} +`, ipCount) +} + func testAccAWSENIConfigUpdatedDescription() string { - return fmt.Sprintf(` -resource "aws_vpc" "foo" { + return testAccAvailableAZsNoOptInConfig() + fmt.Sprintf(` +resource "aws_vpc" "test" { cidr_block = "172.16.0.0/16" enable_dns_hostnames = true tags = { - Name = "terraform-testacc-network-interface-update-desc" - } -} - -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] + Name = "terraform-testacc-network-interface" } } -resource "aws_subnet" "foo" { - vpc_id = aws_vpc.foo.id +resource "aws_subnet" "test" { + vpc_id = aws_vpc.test.id cidr_block = "172.16.10.0/24" availability_zone = data.aws_availability_zones.available.names[0] - tags = { - Name = "tf-acc-network-interface-update-desc" + Name = "tf-acc-network-interface" } } -resource "aws_security_group" "foo" { - vpc_id = aws_vpc.foo.id - description = "foo" - name = "foo" +resource "aws_security_group" "test" { + vpc_id = aws_vpc.test.id + description = "test" + name = "tf-acc-network-interface" egress { from_port = 0 @@ -632,22 +782,18 @@ resource "aws_security_group" "foo" { } } -resource "aws_network_interface" "bar" { - subnet_id = aws_subnet.foo.id +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test.id private_ips = ["172.16.10.100"] - security_groups = [aws_security_group.foo.id] + security_groups = [aws_security_group.test.id] description = "Updated ENI Description" - - tags = { - Name = "bar_interface" - } } `) } -func testAccAWSENIConfigWithSourceDestCheck() string { - return fmt.Sprintf(` -resource "aws_vpc" "foo" { +func testAccAWSENIConfigWithSourceDestCheck(enabled bool) string { + return testAccAvailableAZsNoOptInConfig() + fmt.Sprintf(` +resource "aws_vpc" "test" { cidr_block = "172.16.0.0/16" enable_dns_hostnames = true @@ -656,17 +802,8 @@ resource "aws_vpc" "foo" { } } -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } -} - -resource "aws_subnet" "foo" { - vpc_id = aws_vpc.foo.id +resource "aws_subnet" "test" { + vpc_id = aws_vpc.test.id cidr_block = "172.16.10.0/24" availability_zone = data.aws_availability_zones.available.names[0] @@ -675,17 +812,21 @@ resource "aws_subnet" "foo" { } } -resource "aws_network_interface" "bar" { - subnet_id = aws_subnet.foo.id - source_dest_check = false +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test.id + source_dest_check = %[1]t private_ips = ["172.16.10.100"] + + tags = { + Name = "tf-acc-network-interface-w-source-dest-check" + } } -`) +`, enabled) } func testAccAWSENIConfigWithNoPrivateIPs() string { - return fmt.Sprintf(` -resource "aws_vpc" "foo" { + return testAccAvailableAZsNoOptInConfig() + fmt.Sprintf(` +resource "aws_vpc" "test" { cidr_block = "172.16.0.0/16" enable_dns_hostnames = true @@ -694,17 +835,8 @@ resource "aws_vpc" "foo" { } } -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } -} - -resource "aws_subnet" "foo" { - vpc_id = aws_vpc.foo.id +resource "aws_subnet" "test" { + vpc_id = aws_vpc.test.id cidr_block = "172.16.10.0/24" availability_zone = data.aws_availability_zones.available.names[0] @@ -713,16 +845,18 @@ resource "aws_subnet" "foo" { } } -resource "aws_network_interface" "bar" { - subnet_id = aws_subnet.foo.id +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test.id source_dest_check = false } `) } func testAccAWSENIConfigWithAttachment() string { - return testAccLatestAmazonLinuxHvmEbsAmiConfig() + fmt.Sprintf(` -resource "aws_vpc" "foo" { + return composeConfig(testAccLatestAmazonLinuxHvmEbsAmiConfig(), + testAccAvailableEc2InstanceTypeForRegion("t3.micro", "t2.micro"), + testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +resource "aws_vpc" "test" { cidr_block = "172.16.0.0/16" enable_dns_hostnames = true @@ -731,73 +865,66 @@ resource "aws_vpc" "foo" { } } -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } -} - -resource "aws_subnet" "foo" { - vpc_id = aws_vpc.foo.id +resource "aws_subnet" "test1" { + vpc_id = aws_vpc.test.id cidr_block = "172.16.10.0/24" availability_zone = data.aws_availability_zones.available.names[0] tags = { - Name = "tf-acc-network-interface-w-attachment-foo" + Name = "tf-acc-network-interface-w-attachment-test" } } -resource "aws_subnet" "bar" { - vpc_id = aws_vpc.foo.id +resource "aws_subnet" "test2" { + vpc_id = aws_vpc.test.id cidr_block = "172.16.11.0/24" availability_zone = data.aws_availability_zones.available.names[0] tags = { - Name = "tf-acc-network-interface-w-attachment-bar" + Name = "tf-acc-network-interface-w-attachment-test" } } -resource "aws_security_group" "foo" { - vpc_id = aws_vpc.foo.id - description = "foo" - name = "foo" +resource "aws_security_group" "test" { + vpc_id = aws_vpc.test.id + description = "test" + name = "tf-acc-network-interface-w-attachment-test" } -resource "aws_instance" "foo" { +resource "aws_instance" "test" { ami = data.aws_ami.amzn-ami-minimal-hvm-ebs.id - instance_type = "t2.micro" - subnet_id = aws_subnet.bar.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type + subnet_id = aws_subnet.test2.id associate_public_ip_address = false private_ip = "172.16.11.50" tags = { - Name = "foo-tf-eni-test" + Name = "test-tf-eni-test" } } -resource "aws_network_interface" "bar" { - subnet_id = aws_subnet.foo.id +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test1.id private_ips = ["172.16.10.100"] - security_groups = [aws_security_group.foo.id] + security_groups = [aws_security_group.test.id] attachment { - instance = aws_instance.foo.id + instance = aws_instance.test.id device_index = 1 } tags = { - Name = "bar_interface" + Name = "test_interface" } } -`) +`)) } func testAccAWSENIConfigExternalAttachment() string { - return testAccLatestAmazonLinuxHvmEbsAmiConfig() + fmt.Sprintf(` -resource "aws_vpc" "foo" { + return composeConfig(testAccLatestAmazonLinuxHvmEbsAmiConfig(), + testAccAvailableEc2InstanceTypeForRegion("t3.micro", "t2.micro"), + testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +resource "aws_vpc" "test" { cidr_block = "172.16.0.0/16" enable_dns_hostnames = true @@ -806,45 +933,36 @@ resource "aws_vpc" "foo" { } } -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } -} - -resource "aws_subnet" "foo" { - vpc_id = aws_vpc.foo.id +resource "aws_subnet" "test1" { + vpc_id = aws_vpc.test.id cidr_block = "172.16.10.0/24" availability_zone = data.aws_availability_zones.available.names[0] tags = { - Name = "tf-acc-network-interface-external-attachment-foo" + Name = "tf-acc-network-interface-external-attachment-test" } } -resource "aws_subnet" "bar" { - vpc_id = aws_vpc.foo.id +resource "aws_subnet" "test2" { + vpc_id = aws_vpc.test.id cidr_block = "172.16.11.0/24" availability_zone = data.aws_availability_zones.available.names[0] tags = { - Name = "tf-acc-network-interface-external-attachment-bar" + Name = "tf-acc-network-interface-external-attachment-test" } } -resource "aws_security_group" "foo" { - vpc_id = aws_vpc.foo.id - description = "foo" - name = "foo" +resource "aws_security_group" "test" { + vpc_id = aws_vpc.test.id + description = "test" + name = "tf-acc-network-interface-external-attachment-test" } -resource "aws_instance" "foo" { +resource "aws_instance" "test" { ami = data.aws_ami.amzn-ami-minimal-hvm-ebs.id - instance_type = "t2.micro" - subnet_id = aws_subnet.bar.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type + subnet_id = aws_subnet.test2.id associate_public_ip_address = false private_ip = "172.16.11.50" @@ -853,16 +971,16 @@ resource "aws_instance" "foo" { } } -resource "aws_network_interface" "bar" { - subnet_id = aws_subnet.foo.id +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test1.id private_ips = ["172.16.10.100"] - security_groups = [aws_security_group.foo.id] + security_groups = [aws_security_group.test.id] tags = { - Name = "bar_interface" + Name = "test_interface" } } -`) +`)) } func testAccAWSENIConfigPrivateIpsCount(privateIpsCount int) string { diff --git a/aws/structure.go b/aws/structure.go index c27082eb17b..a125fdafddd 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -1122,6 +1122,18 @@ func expandPrivateIPAddresses(ips []interface{}) []*ec2.PrivateIpAddressSpecific return dtos } +func expandIP6Addresses(ips []interface{}) []*ec2.InstanceIpv6Address { + dtos := make([]*ec2.InstanceIpv6Address, 0, len(ips)) + for _, v := range ips { + ipv6Address := &ec2.InstanceIpv6Address{ + Ipv6Address: aws.String(v.(string)), + } + + dtos = append(dtos, ipv6Address) + } + return dtos +} + //Flattens network interface attachment into a map[string]interface func flattenAttachment(a *ec2.NetworkInterfaceAttachment) map[string]interface{} { att := make(map[string]interface{}) diff --git a/website/docs/r/network_interface.markdown b/website/docs/r/network_interface.markdown index 71f9855bf61..df298d2c741 100644 --- a/website/docs/r/network_interface.markdown +++ b/website/docs/r/network_interface.markdown @@ -33,6 +33,8 @@ The following arguments are supported: * `description` - (Optional) A description for the network interface. * `private_ips` - (Optional) List of private IPs to assign to the ENI. * `private_ips_count` - (Optional) Number of secondary private IPs to assign to the ENI. The total number of private IPs will be 1 + private_ips_count, as a primary private IP will be assiged to an ENI by default. +* `ipv6_addresses` - (Optional) One or more specific IPv6 addresses from the IPv6 CIDR block range of your subnet. You can't use this option if you're specifying `ipv6_address_count`. +* `ipv6_address_count` - (Optional) The number of IPv6 addresses to assign to a network interface. You can't use this option if specifying specific `ipv6_addresses`. If your subnet has the AssignIpv6AddressOnCreation attribute set to `true`, you can specify `0` to override this setting. * `security_groups` - (Optional) List of security group IDs to assign to the ENI. * `attachment` - (Optional) Block to define the attachment of the ENI. Documented below. * `source_dest_check` - (Optional) Whether to enable source destination checking for the ENI. Default true.