diff --git a/.changelog/17846.txt b/.changelog/17846.txt new file mode 100644 index 00000000000..b192c857d70 --- /dev/null +++ b/.changelog/17846.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_network_interface: Add `private_ip_list`, `private_ip_list_enabled`, `ipv6_address_list`, and `ipv6_address_list_enabled` attributes +``` diff --git a/internal/service/ec2/network_interface.go b/internal/service/ec2/network_interface.go index 5fb59f94200..28a1b3124e9 100644 --- a/internal/service/ec2/network_interface.go +++ b/internal/service/ec2/network_interface.go @@ -1,6 +1,7 @@ package ec2 import ( + "context" "fmt" "log" "time" @@ -9,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" @@ -85,7 +87,19 @@ func ResourceNetworkInterface() *schema.Resource { Type: schema.TypeInt, Optional: true, Computed: true, - ConflictsWith: []string{"ipv6_addresses"}, + ConflictsWith: []string{"ipv6_addresses", "ipv6_address_list"}, + }, + "ipv6_address_list": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"ipv6_addresses", "ipv6_address_count"}, + }, + "ipv6_address_list_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, }, "ipv6_addresses": { Type: schema.TypeSet, @@ -95,7 +109,7 @@ func ResourceNetworkInterface() *schema.Resource { Type: schema.TypeString, ValidateFunc: validation.IsIPv6Address, }, - ConflictsWith: []string{"ipv6_address_count"}, + ConflictsWith: []string{"ipv6_address_count", "ipv6_address_list"}, }, "ipv6_prefixes": { Type: schema.TypeSet, @@ -135,15 +149,29 @@ func ResourceNetworkInterface() *schema.Resource { Computed: true, }, "private_ips": { - Type: schema.TypeSet, - Optional: true, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"private_ip_list"}, }, "private_ips_count": { - Type: schema.TypeInt, + Type: schema.TypeInt, + Optional: true, + Computed: true, + ConflictsWith: []string{"private_ip_list"}, + }, + "private_ip_list": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"private_ips", "private_ips_count"}, + }, + "private_ip_list_enabled": { + Type: schema.TypeBool, Optional: true, - Computed: true, + Default: false, }, "security_groups": { Type: schema.TypeSet, @@ -165,7 +193,129 @@ func ResourceNetworkInterface() *schema.Resource { "tags_all": tftags.TagsSchemaComputed(), }, - CustomizeDiff: verify.SetTagsDiff, + CustomizeDiff: customdiff.Sequence( + verify.SetTagsDiff, + customdiff.ForceNewIf("private_ips", func(_ context.Context, d *schema.ResourceDiff, meta interface{}) bool { + privateIPListEnabled := d.Get("private_ip_list_enabled").(bool) + if privateIPListEnabled { + return false + } + _, new := d.GetChange("private_ips") + if new != nil { + oldPrimaryIP := "" + if v, ok := d.GetOk("private_ip_list"); ok { + for _, ip := range v.([]interface{}) { + oldPrimaryIP = ip.(string) + break + } + } + for _, ip := range new.(*schema.Set).List() { + // no need for new resource if we still have the primary ip + if oldPrimaryIP == ip.(string) { + return false + } + } + // new primary ip requires a new resource + return true + } else { + return false + } + }), + customdiff.ForceNewIf("private_ip_list", func(_ context.Context, d *schema.ResourceDiff, meta interface{}) bool { + privateIPListEnabled := d.Get("private_ip_list_enabled").(bool) + if !privateIPListEnabled { + return false + } + old, new := d.GetChange("private_ip_list") + if old != nil && new != nil { + oldPrimaryIP := "" + newPrimaryIP := "" + for _, ip := range old.([]interface{}) { + oldPrimaryIP = ip.(string) + break + } + for _, ip := range new.([]interface{}) { + newPrimaryIP = ip.(string) + break + } + + // change in primary private ip requires a new resource + return oldPrimaryIP != newPrimaryIP + } else { + return false + } + }), + customdiff.ComputedIf("private_ips", func(_ context.Context, diff *schema.ResourceDiff, meta interface{}) bool { + if !diff.Get("private_ip_list_enabled").(bool) { + // it is not computed if we are actively updating it + if diff.HasChange("private_ips") { + return false + } else { + return diff.HasChange("private_ips_count") + } + } else { + return diff.HasChange("private_ip_list") + } + }), + customdiff.ComputedIf("private_ips_count", func(_ context.Context, diff *schema.ResourceDiff, meta interface{}) bool { + if !diff.Get("private_ip_list_enabled").(bool) { + // it is not computed if we are actively updating it + if diff.HasChange("private_ips_count") { + return false + } else { + // compute the new count if private_ips change + return diff.HasChange("private_ips") + } + } else { + // compute the new count if private_ip_list changes + return diff.HasChange("private_ip_list") + } + }), + customdiff.ComputedIf("private_ip_list", func(_ context.Context, diff *schema.ResourceDiff, meta interface{}) bool { + if diff.Get("private_ip_list_enabled").(bool) { + // if the list is controlling it does not need to be computed + return false + } else { + // list is not controlling so compute new list if private_ips or private_ips_count changes + return diff.HasChange("private_ips") || diff.HasChange("private_ips_count") || diff.HasChange("private_ip_list") + } + }), + customdiff.ComputedIf("ipv6_addresses", func(_ context.Context, diff *schema.ResourceDiff, meta interface{}) bool { + if !diff.Get("ipv6_address_list_enabled").(bool) { + // it is not computed if we are actively updating it + if diff.HasChange("private_ips") { + return false + } else { + return diff.HasChange("ipv6_address_count") + } + } else { + return diff.HasChange("ipv6_address_list") + } + }), + customdiff.ComputedIf("ipv6_address_count", func(_ context.Context, diff *schema.ResourceDiff, meta interface{}) bool { + if !diff.Get("ipv6_address_list_enabled").(bool) { + // it is not computed if we are actively updating it + if diff.HasChange("ipv6_address_count") { + return false + } else { + // compute the new count if ipv6_addresses change + return diff.HasChange("ipv6_addresses") + } + } else { + // compute the new count if ipv6_address_list changes + return diff.HasChange("ipv6_address_list") + } + }), + customdiff.ComputedIf("ipv6_address_list", func(_ context.Context, diff *schema.ResourceDiff, meta interface{}) bool { + if diff.Get("ipv6_address_list_enabled").(bool) { + // if the list is controlling it does not need to be computed + return false + } else { + // list is not controlling so compute new list if anything changes + return diff.HasChange("ipv6_addresses") || diff.HasChange("ipv6_address_count") || diff.HasChange("ipv6_address_list") + } + }), + ), } } @@ -191,7 +341,7 @@ func resourceNetworkInterfaceCreate(d *schema.ResourceData, meta interface{}) er if v, ok := d.GetOk("ipv4_prefixes"); ok && v.(*schema.Set).Len() > 0 { ipv4PrefixesSpecified = true - input.Ipv4Prefixes = expandIpv4PrefixSpecificationRequests(v.(*schema.Set).List()) + input.Ipv4Prefixes = expandIPv4PrefixSpecificationRequests(v.(*schema.Set).List()) } if v, ok := d.GetOk("ipv4_prefix_count"); ok { @@ -203,24 +353,48 @@ func resourceNetworkInterfaceCreate(d *schema.ResourceData, meta interface{}) er } if v, ok := d.GetOk("ipv6_addresses"); ok && v.(*schema.Set).Len() > 0 { - input.Ipv6Addresses = expandInstanceIpv6Addresses(v.(*schema.Set).List()) + input.Ipv6Addresses = expandInstanceIPv6Addresses(v.(*schema.Set).List()) } if v, ok := d.GetOk("ipv6_prefixes"); ok && v.(*schema.Set).Len() > 0 { ipv6PrefixesSpecified = true - input.Ipv6Prefixes = expandIpv6PrefixSpecificationRequests(v.(*schema.Set).List()) + input.Ipv6Prefixes = expandIPv6PrefixSpecificationRequests(v.(*schema.Set).List()) } if v, ok := d.GetOk("ipv6_prefix_count"); ok { input.Ipv6PrefixCount = aws.Int64(int64(v.(int))) } - if v, ok := d.GetOk("private_ips"); ok && v.(*schema.Set).Len() > 0 { - input.PrivateIpAddresses = expandPrivateIpAddressSpecifications(v.(*schema.Set).List()) - } - - if v, ok := d.GetOk("private_ips_count"); ok { - input.SecondaryPrivateIpAddressCount = aws.Int64(int64(v.(int))) + if d.Get("private_ip_list_enabled").(bool) { + if v, ok := d.GetOk("private_ip_list"); ok && len(v.([]interface{})) > 0 { + input.PrivateIpAddresses = expandPrivateIPAddressSpecifications(v.([]interface{})) + } + } else { + if v, ok := d.GetOk("private_ips"); ok && v.(*schema.Set).Len() > 0 { + privateIPs := v.(*schema.Set).List() + // total includes the primary + totalPrivateIPs := len(privateIPs) + // private_ips_count is for secondaries + if v, ok := d.GetOk("private_ips_count"); ok { + // reduce total count if necessary + if v.(int)+1 < totalPrivateIPs { + totalPrivateIPs = v.(int) + 1 + } + } + // truncate the list + countLimitedIPs := make([]interface{}, totalPrivateIPs) + for i, ip := range privateIPs { + countLimitedIPs[i] = ip.(string) + if i == totalPrivateIPs-1 { + break + } + } + input.PrivateIpAddresses = expandPrivateIPAddressSpecifications(countLimitedIPs) + } else { + if v, ok := d.GetOk("private_ips_count"); ok { + input.SecondaryPrivateIpAddressCount = aws.Int64(int64(v.(int))) + } + } } if v, ok := d.GetOk("security_groups"); ok && v.(*schema.Set).Len() > 0 { @@ -246,6 +420,25 @@ func resourceNetworkInterfaceCreate(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("error waiting for EC2 Network Interface (%s) create: %w", d.Id(), err) } + if !d.Get("private_ip_list_enabled").(bool) { + // add more ips to match the count + if v, ok := d.GetOk("private_ips"); ok && v.(*schema.Set).Len() > 0 { + totalPrivateIPs := v.(*schema.Set).Len() + if privateIPsCount, ok := d.GetOk("private_ips_count"); ok { + if privateIPsCount.(int)+1 > totalPrivateIPs { + input := &ec2.AssignPrivateIpAddressesInput{ + NetworkInterfaceId: aws.String(d.Id()), + SecondaryPrivateIpAddressCount: aws.Int64(int64(privateIPsCount.(int) + 1 - totalPrivateIPs)), + } + _, err := conn.AssignPrivateIpAddresses(input) + if err != nil { + return fmt.Errorf("Failure to assign Private IPs: %s", err) + } + } + } + } + } + if len(tags) > 0 && (ipv4PrefixesSpecified || ipv6PrefixesSpecified) { if err := UpdateTags(conn, d.Id(), nil, tags); err != nil { return fmt.Errorf("error updating EC2 Network Interface (%s) tags: %w", d.Id(), err) @@ -322,7 +515,7 @@ func resourceNetworkInterfaceRead(d *schema.ResourceData, meta interface{}) erro d.Set("description", eni.Description) d.Set("interface_type", eni.InterfaceType) - if err := d.Set("ipv4_prefixes", flattenIpv4PrefixSpecifications(eni.Ipv4Prefixes)); err != nil { + if err := d.Set("ipv4_prefixes", flattenIPv4PrefixSpecifications(eni.Ipv4Prefixes)); err != nil { return fmt.Errorf("error setting ipv4_prefixes: %w", err) } @@ -330,11 +523,15 @@ func resourceNetworkInterfaceRead(d *schema.ResourceData, meta interface{}) erro d.Set("ipv6_address_count", len(eni.Ipv6Addresses)) + if err := d.Set("ipv6_address_list", flattenNetworkInterfaceIPv6Addresses(eni.Ipv6Addresses)); err != nil { + return fmt.Errorf("error setting ipv6 address list: %s", err) + } + if err := d.Set("ipv6_addresses", flattenNetworkInterfaceIPv6Addresses(eni.Ipv6Addresses)); err != nil { return fmt.Errorf("error setting ipv6_addresses: %w", err) } - if err := d.Set("ipv6_prefixes", flattenIpv6PrefixSpecifications(eni.Ipv6Prefixes)); err != nil { + if err := d.Set("ipv6_prefixes", flattenIPv6PrefixSpecifications(eni.Ipv6Prefixes)); err != nil { return fmt.Errorf("error setting ipv6_prefixes: %w", err) } @@ -346,12 +543,16 @@ func resourceNetworkInterfaceRead(d *schema.ResourceData, meta interface{}) erro d.Set("private_dns_name", eni.PrivateDnsName) d.Set("private_ip", eni.PrivateIpAddress) - if err := d.Set("private_ips", flattenNetworkInterfacePrivateIpAddresses(eni.PrivateIpAddresses)); err != nil { + if err := d.Set("private_ips", FlattenNetworkInterfacePrivateIPAddresses(eni.PrivateIpAddresses)); err != nil { return fmt.Errorf("error setting private_ips: %w", err) } d.Set("private_ips_count", len(eni.PrivateIpAddresses)-1) + if err := d.Set("private_ip_list", FlattenNetworkInterfacePrivateIPAddresses(eni.PrivateIpAddresses)); err != nil { + return fmt.Errorf("error setting private_ip_list: %s", err) + } + if err := d.Set("security_groups", FlattenGroupIdentifiers(eni.Groups)); err != nil { return fmt.Errorf("error setting security_groups: %w", err) } @@ -375,6 +576,7 @@ func resourceNetworkInterfaceRead(d *schema.ResourceData, meta interface{}) erro func resourceNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn + privateIPsNetChange := 0 if d.HasChange("attachment") { oa, na := d.GetChange("attachment") @@ -400,7 +602,7 @@ func resourceNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) er } } - if d.HasChange("private_ips") { + if d.HasChange("private_ips") && !d.Get("private_ip_list_enabled").(bool) { o, n := d.GetChange("private_ips") if o == nil { o = new(schema.Set) @@ -426,6 +628,8 @@ func resourceNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) er if err != nil { return fmt.Errorf("error unassigning EC2 Network Interface (%s) private IPv4 addresses: %w", d.Id(), err) } + + privateIPsNetChange -= unassignIPs.Len() } // Assign new IP addresses. @@ -442,10 +646,63 @@ func resourceNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) er if err != nil { return fmt.Errorf("error assigning EC2 Network Interface (%s) private IPv4 addresses: %w", d.Id(), err) } + privateIPsNetChange += assignIPs.Len() } } - if d.HasChange("private_ips_count") { + if d.HasChange("private_ip_list") && d.Get("private_ip_list_enabled").(bool) { + o, n := d.GetChange("private_ip_list") + if o == nil { + o = make([]string, 0) + } + if n == nil { + n = make([]string, 0) + } + if len(o.([]interface{}))-1 > 0 { + privateIPsToUnassign := make([]interface{}, len(o.([]interface{}))-1) + idx := 0 + for i, ip := range o.([]interface{}) { + // skip primary private ip address + if i == 0 { + continue + } + privateIPsToUnassign[idx] = ip + log.Printf("[INFO] Unassigning private ip %s", ip) + idx += 1 + } + + // Unassign the secondary IP addresses + input := &ec2.UnassignPrivateIpAddressesInput{ + NetworkInterfaceId: aws.String(d.Id()), + PrivateIpAddresses: flex.ExpandStringList(privateIPsToUnassign), + } + _, err := conn.UnassignPrivateIpAddresses(input) + if err != nil { + return fmt.Errorf("Failure to unassign Private IPs: %s", err) + } + } + + // Assign each ip one-by-one in order to retain order + for i, ip := range n.([]interface{}) { + // skip primary private ip address + if i == 0 { + continue + } + privateIPToAssign := []interface{}{ip} + log.Printf("[INFO] Assigning private ip %s", ip) + + input := &ec2.AssignPrivateIpAddressesInput{ + NetworkInterfaceId: aws.String(d.Id()), + PrivateIpAddresses: flex.ExpandStringList(privateIPToAssign), + } + _, err := conn.AssignPrivateIpAddresses(input) + if err != nil { + return fmt.Errorf("Failure to assign Private IPs: %s", err) + } + } + } + + if d.HasChange("private_ips_count") && !d.Get("private_ip_list_enabled").(bool) { o, n := d.GetChange("private_ips_count") privateIPs := d.Get("private_ips").(*schema.Set).List() privateIPsFiltered := privateIPs[:0] @@ -458,7 +715,7 @@ func resourceNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) er } if o != nil && n != nil && n != len(privateIPsFiltered) { - if diff := n.(int) - o.(int); diff > 0 { + if diff := n.(int) - o.(int) - privateIPsNetChange; diff > 0 { input := &ec2.AssignPrivateIpAddressesInput{ NetworkInterfaceId: aws.String(d.Id()), SecondaryPrivateIpAddressCount: aws.Int64(int64(diff)), @@ -564,7 +821,7 @@ func resourceNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) er } } - if d.HasChange("ipv6_addresses") { + if d.HasChange("ipv6_addresses") && !d.Get("ipv6_address_list_enabled").(bool) { o, n := d.GetChange("ipv6_addresses") if o == nil { o = new(schema.Set) @@ -609,7 +866,7 @@ func resourceNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) er } } - if d.HasChange("ipv6_address_count") { + if d.HasChange("ipv6_address_count") && !d.Get("ipv6_address_list_enabled").(bool) { o, n := d.GetChange("ipv6_address_count") ipv6Addresses := d.Get("ipv6_addresses").(*schema.Set).List() @@ -642,6 +899,50 @@ func resourceNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) er } } + if d.HasChange("ipv6_address_list") && d.Get("ipv6_address_list_enabled").(bool) { + o, n := d.GetChange("ipv6_address_list") + if o == nil { + o = make([]string, 0) + } + if n == nil { + n = make([]string, 0) + } + + // Unassign old IPV6 addresses + if len(o.([]interface{})) > 0 { + unassignIPs := make([]interface{}, len(o.([]interface{}))) + for i, ip := range o.([]interface{}) { + unassignIPs[i] = ip + log.Printf("[INFO] Unassigning ipv6 address %s", ip) + } + + log.Printf("[INFO] Unassigning ipv6 addresses") + input := &ec2.UnassignIpv6AddressesInput{ + NetworkInterfaceId: aws.String(d.Id()), + Ipv6Addresses: flex.ExpandStringList(unassignIPs), + } + _, err := conn.UnassignIpv6Addresses(input) + if err != nil { + return fmt.Errorf("failure to unassign IPV6 Addresses: %s", err) + } + } + + // Assign each ip one-by-one in order to retain order + for _, ip := range n.([]interface{}) { + privateIPToAssign := []interface{}{ip} + log.Printf("[INFO] Assigning ipv6 address %s", ip) + + input := &ec2.AssignIpv6AddressesInput{ + NetworkInterfaceId: aws.String(d.Id()), + Ipv6Addresses: flex.ExpandStringList(privateIPToAssign), + } + _, err := conn.AssignIpv6Addresses(input) + if err != nil { + return fmt.Errorf("Failure to assign IPV6 Addresses: %s", err) + } + } + } + if d.HasChange("ipv6_prefixes") { o, n := d.GetChange("ipv6_prefixes") if o == nil { @@ -921,7 +1222,7 @@ func flattenNetworkInterfaceAttachment(apiObject *ec2.NetworkInterfaceAttachment return tfMap } -func expandPrivateIpAddressSpecification(tfString string) *ec2.PrivateIpAddressSpecification { +func expandPrivateIPAddressSpecification(tfString string) *ec2.PrivateIpAddressSpecification { if tfString == "" { return nil } @@ -933,7 +1234,7 @@ func expandPrivateIpAddressSpecification(tfString string) *ec2.PrivateIpAddressS return apiObject } -func expandPrivateIpAddressSpecifications(tfList []interface{}) []*ec2.PrivateIpAddressSpecification { +func expandPrivateIPAddressSpecifications(tfList []interface{}) []*ec2.PrivateIpAddressSpecification { if len(tfList) == 0 { return nil } @@ -947,7 +1248,7 @@ func expandPrivateIpAddressSpecifications(tfList []interface{}) []*ec2.PrivateIp continue } - apiObject := expandPrivateIpAddressSpecification(tfString) + apiObject := expandPrivateIPAddressSpecification(tfString) if apiObject == nil { continue @@ -963,7 +1264,7 @@ func expandPrivateIpAddressSpecifications(tfList []interface{}) []*ec2.PrivateIp return apiObjects } -func expandInstanceIpv6Address(tfString string) *ec2.InstanceIpv6Address { +func expandInstanceIPv6Address(tfString string) *ec2.InstanceIpv6Address { if tfString == "" { return nil } @@ -975,7 +1276,7 @@ func expandInstanceIpv6Address(tfString string) *ec2.InstanceIpv6Address { return apiObject } -func expandInstanceIpv6Addresses(tfList []interface{}) []*ec2.InstanceIpv6Address { +func expandInstanceIPv6Addresses(tfList []interface{}) []*ec2.InstanceIpv6Address { if len(tfList) == 0 { return nil } @@ -989,7 +1290,7 @@ func expandInstanceIpv6Addresses(tfList []interface{}) []*ec2.InstanceIpv6Addres continue } - apiObject := expandInstanceIpv6Address(tfString) + apiObject := expandInstanceIPv6Address(tfString) if apiObject == nil { continue @@ -1001,7 +1302,7 @@ func expandInstanceIpv6Addresses(tfList []interface{}) []*ec2.InstanceIpv6Addres return apiObjects } -func flattenNetworkInterfacePrivateIpAddress(apiObject *ec2.NetworkInterfacePrivateIpAddress) string { +func flattenNetworkInterfacePrivateIPAddress(apiObject *ec2.NetworkInterfacePrivateIpAddress) string { if apiObject == nil { return "" } @@ -1015,7 +1316,7 @@ func flattenNetworkInterfacePrivateIpAddress(apiObject *ec2.NetworkInterfacePriv return tfString } -func flattenNetworkInterfacePrivateIpAddresses(apiObjects []*ec2.NetworkInterfacePrivateIpAddress) []string { +func FlattenNetworkInterfacePrivateIPAddresses(apiObjects []*ec2.NetworkInterfacePrivateIpAddress) []string { if len(apiObjects) == 0 { return nil } @@ -1027,7 +1328,7 @@ func flattenNetworkInterfacePrivateIpAddresses(apiObjects []*ec2.NetworkInterfac continue } - tfList = append(tfList, flattenNetworkInterfacePrivateIpAddress(apiObject)) + tfList = append(tfList, flattenNetworkInterfacePrivateIPAddress(apiObject)) } return tfList @@ -1065,7 +1366,7 @@ func flattenNetworkInterfaceIPv6Addresses(apiObjects []*ec2.NetworkInterfaceIpv6 return tfList } -func expandIpv4PrefixSpecificationRequest(tfString string) *ec2.Ipv4PrefixSpecificationRequest { +func expandIPv4PrefixSpecificationRequest(tfString string) *ec2.Ipv4PrefixSpecificationRequest { if tfString == "" { return nil } @@ -1077,7 +1378,7 @@ func expandIpv4PrefixSpecificationRequest(tfString string) *ec2.Ipv4PrefixSpecif return apiObject } -func expandIpv4PrefixSpecificationRequests(tfList []interface{}) []*ec2.Ipv4PrefixSpecificationRequest { +func expandIPv4PrefixSpecificationRequests(tfList []interface{}) []*ec2.Ipv4PrefixSpecificationRequest { if len(tfList) == 0 { return nil } @@ -1091,7 +1392,7 @@ func expandIpv4PrefixSpecificationRequests(tfList []interface{}) []*ec2.Ipv4Pref continue } - apiObject := expandIpv4PrefixSpecificationRequest(tfString) + apiObject := expandIPv4PrefixSpecificationRequest(tfString) if apiObject == nil { continue @@ -1103,7 +1404,7 @@ func expandIpv4PrefixSpecificationRequests(tfList []interface{}) []*ec2.Ipv4Pref return apiObjects } -func expandIpv6PrefixSpecificationRequest(tfString string) *ec2.Ipv6PrefixSpecificationRequest { +func expandIPv6PrefixSpecificationRequest(tfString string) *ec2.Ipv6PrefixSpecificationRequest { if tfString == "" { return nil } @@ -1115,7 +1416,7 @@ func expandIpv6PrefixSpecificationRequest(tfString string) *ec2.Ipv6PrefixSpecif return apiObject } -func expandIpv6PrefixSpecificationRequests(tfList []interface{}) []*ec2.Ipv6PrefixSpecificationRequest { +func expandIPv6PrefixSpecificationRequests(tfList []interface{}) []*ec2.Ipv6PrefixSpecificationRequest { if len(tfList) == 0 { return nil } @@ -1129,7 +1430,7 @@ func expandIpv6PrefixSpecificationRequests(tfList []interface{}) []*ec2.Ipv6Pref continue } - apiObject := expandIpv6PrefixSpecificationRequest(tfString) + apiObject := expandIPv6PrefixSpecificationRequest(tfString) if apiObject == nil { continue @@ -1141,7 +1442,7 @@ func expandIpv6PrefixSpecificationRequests(tfList []interface{}) []*ec2.Ipv6Pref return apiObjects } -func flattenIpv4PrefixSpecification(apiObject *ec2.Ipv4PrefixSpecification) string { +func flattenIPv4PrefixSpecification(apiObject *ec2.Ipv4PrefixSpecification) string { if apiObject == nil { return "" } @@ -1155,7 +1456,7 @@ func flattenIpv4PrefixSpecification(apiObject *ec2.Ipv4PrefixSpecification) stri return tfString } -func flattenIpv4PrefixSpecifications(apiObjects []*ec2.Ipv4PrefixSpecification) []string { +func flattenIPv4PrefixSpecifications(apiObjects []*ec2.Ipv4PrefixSpecification) []string { if len(apiObjects) == 0 { return nil } @@ -1167,13 +1468,13 @@ func flattenIpv4PrefixSpecifications(apiObjects []*ec2.Ipv4PrefixSpecification) continue } - tfList = append(tfList, flattenIpv4PrefixSpecification(apiObject)) + tfList = append(tfList, flattenIPv4PrefixSpecification(apiObject)) } return tfList } -func flattenIpv6PrefixSpecification(apiObject *ec2.Ipv6PrefixSpecification) string { +func flattenIPv6PrefixSpecification(apiObject *ec2.Ipv6PrefixSpecification) string { if apiObject == nil { return "" } @@ -1187,7 +1488,7 @@ func flattenIpv6PrefixSpecification(apiObject *ec2.Ipv6PrefixSpecification) stri return tfString } -func flattenIpv6PrefixSpecifications(apiObjects []*ec2.Ipv6PrefixSpecification) []string { +func flattenIPv6PrefixSpecifications(apiObjects []*ec2.Ipv6PrefixSpecification) []string { if len(apiObjects) == 0 { return nil } @@ -1199,7 +1500,7 @@ func flattenIpv6PrefixSpecifications(apiObjects []*ec2.Ipv6PrefixSpecification) continue } - tfList = append(tfList, flattenIpv6PrefixSpecification(apiObject)) + tfList = append(tfList, flattenIPv6PrefixSpecification(apiObject)) } return tfList diff --git a/internal/service/ec2/network_interface_data_source.go b/internal/service/ec2/network_interface_data_source.go index 8e7e1593686..5562ac3a4f7 100644 --- a/internal/service/ec2/network_interface_data_source.go +++ b/internal/service/ec2/network_interface_data_source.go @@ -203,7 +203,7 @@ func dataSourceNetworkInterfaceRead(d *schema.ResourceData, meta interface{}) er d.Set("owner_id", ownerID) d.Set("private_dns_name", eni.PrivateDnsName) d.Set("private_ip", eni.PrivateIpAddress) - d.Set("private_ips", flattenNetworkInterfacePrivateIpAddresses(eni.PrivateIpAddresses)) + d.Set("private_ips", FlattenNetworkInterfacePrivateIPAddresses(eni.PrivateIpAddresses)) d.Set("requester_id", eni.RequesterId) d.Set("subnet_id", eni.SubnetId) d.Set("vpc_id", eni.VpcId) diff --git a/internal/service/ec2/network_interface_test.go b/internal/service/ec2/network_interface_test.go index a3a69c8b6a2..d286e1a10a9 100644 --- a/internal/service/ec2/network_interface_test.go +++ b/internal/service/ec2/network_interface_test.go @@ -2,7 +2,9 @@ package ec2_test import ( "fmt" + "reflect" "regexp" + "sort" "strings" "testing" @@ -53,9 +55,10 @@ func TestAccEC2NetworkInterface_basic(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, }, }) @@ -81,9 +84,10 @@ func TestAccEC2NetworkInterface_ipv6(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENIIPV6MultipleConfig(rName), @@ -125,9 +129,10 @@ func TestAccEC2NetworkInterface_tags(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENITags2Config(rName, "key1", "value1updated", "key2", "value2"), @@ -169,9 +174,10 @@ func TestAccEC2NetworkInterface_ipv6Count(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENIIPV6CountConfig(rName, 2), @@ -258,9 +264,10 @@ func TestAccEC2NetworkInterface_description(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENIDescriptionConfig(rName, "description 2"), @@ -290,6 +297,10 @@ func TestAccEC2NetworkInterface_description(t *testing.T) { } func TestAccEC2NetworkInterface_attachment(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var conf ec2.NetworkInterface resourceName := "aws_network_interface.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -314,9 +325,10 @@ func TestAccEC2NetworkInterface_attachment(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, }, }) @@ -341,9 +353,10 @@ func TestAccEC2NetworkInterface_ignoreExternalAttachment(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, }, }) @@ -368,9 +381,10 @@ func TestAccEC2NetworkInterface_sourceDestCheck(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENISourceDestCheckConfig(rName, true), @@ -409,9 +423,10 @@ func TestAccEC2NetworkInterface_privateIPsCount(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENIPrivateIPsCountConfig(rName, 2), @@ -421,9 +436,10 @@ func TestAccEC2NetworkInterface_privateIPsCount(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENIPrivateIPsCountConfig(rName, 0), @@ -433,9 +449,10 @@ func TestAccEC2NetworkInterface_privateIPsCount(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENIPrivateIPsCountConfig(rName, 1), @@ -445,9 +462,10 @@ func TestAccEC2NetworkInterface_privateIPsCount(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, }, }) @@ -472,9 +490,10 @@ func TestAccEC2NetworkInterface_ENIInterfaceType_efa(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, }, }) @@ -500,9 +519,10 @@ func TestAccEC2NetworkInterface_ENI_ipv4Prefix(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENIIPV4PrefixMultipleConfig(rName), @@ -543,9 +563,10 @@ func TestAccEC2NetworkInterface_ENI_ipv4PrefixCount(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENIIPV4PrefixCountConfig(rName, 2), @@ -592,9 +613,10 @@ func TestAccEC2NetworkInterface_ENI_ipv6Prefix(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENIIPV6PrefixMultipleConfig(rName), @@ -635,9 +657,10 @@ func TestAccEC2NetworkInterface_ENI_ipv6PrefixCount(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, }, { Config: testAccENIIPV6PrefixCountConfig(rName, 2), @@ -664,6 +687,246 @@ func TestAccEC2NetworkInterface_ENI_ipv6PrefixCount(t *testing.T) { }) } +func TestAccEC2NetworkInterface_privateIPSet(t *testing.T) { + var networkInterface, lastInterface ec2.NetworkInterface + resourceName := "aws_network_interface.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckENIDestroy, + Steps: []resource.TestStep{ + { // Configuration with three private_ips + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.44", "172.16.10.59", "172.16.10.123"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.44", "172.16.10.59", "172.16.10.123"}, &networkInterface), + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, + }, + { // Change order of private_ips + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.123", "172.16.10.44", "172.16.10.59"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.44", "172.16.10.59", "172.16.10.123"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Add secondaries to private_ips + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.123", "172.16.10.12", "172.16.10.44", "172.16.10.59"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.44", "172.16.10.12", "172.16.10.59", "172.16.10.123"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Remove secondary to private_ips + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.123", "172.16.10.44", "172.16.10.59"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.44", "172.16.10.59", "172.16.10.123"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Remove primary + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.123", "172.16.10.59", "172.16.10.57"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.57", "172.16.10.59", "172.16.10.123"}, &networkInterface), + testAccCheckENIDifferent(&lastInterface, &networkInterface), // different + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Use count to add IPs + Config: testAccENIConfig_privateIPSetCount(rName, 4), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Change list, retain primary + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.44", "172.16.10.57"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.44", "172.16.10.57"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // New list + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.17"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.17"}, &networkInterface), + testAccCheckENIDifferent(&lastInterface, &networkInterface), // different + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + }, + }) +} + +func TestAccEC2NetworkInterface_privateIPList(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var networkInterface, lastInterface ec2.NetworkInterface + resourceName := "aws_network_interface.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckENIDestroy, + Steps: []resource.TestStep{ + { // Build a set incrementally in order + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.17"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.17"}, &networkInterface), + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"private_ip_list_enabled", "ipv6_address_list_enabled"}, + }, + { // Add to set + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.17", "172.16.10.45"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.17", "172.16.10.45"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Add to set + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.17", "172.16.10.45", "172.16.10.89"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.17", "172.16.10.45", "172.16.10.89"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Add to set + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.17", "172.16.10.45", "172.16.10.89", "172.16.10.122"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.17", "172.16.10.45", "172.16.10.89", "172.16.10.122"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Change from set to list using same order + Config: testAccENIConfig_privateIPList(rName, []string{"172.16.10.17", "172.16.10.45", "172.16.10.89", "172.16.10.122"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPList([]string{"172.16.10.17", "172.16.10.45", "172.16.10.89", "172.16.10.122"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Change order of private_ip_list + Config: testAccENIConfig_privateIPList(rName, []string{"172.16.10.17", "172.16.10.89", "172.16.10.45", "172.16.10.122"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPList([]string{"172.16.10.17", "172.16.10.89", "172.16.10.45", "172.16.10.122"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Remove secondaries from end + Config: testAccENIConfig_privateIPList(rName, []string{"172.16.10.17", "172.16.10.89", "172.16.10.45"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPList([]string{"172.16.10.17", "172.16.10.89", "172.16.10.45"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Add secondaries to end + Config: testAccENIConfig_privateIPList(rName, []string{"172.16.10.17", "172.16.10.89", "172.16.10.45", "172.16.10.123"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPList([]string{"172.16.10.17", "172.16.10.89", "172.16.10.45", "172.16.10.123"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Add secondaries to middle + Config: testAccENIConfig_privateIPList(rName, []string{"172.16.10.17", "172.16.10.89", "172.16.10.77", "172.16.10.45", "172.16.10.123"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPList([]string{"172.16.10.17", "172.16.10.89", "172.16.10.77", "172.16.10.45", "172.16.10.123"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Remove secondaries from middle + Config: testAccENIConfig_privateIPList(rName, []string{"172.16.10.17", "172.16.10.89", "172.16.10.123"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPList([]string{"172.16.10.17", "172.16.10.89", "172.16.10.123"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Use count to add IPs + Config: testAccENIConfig_privateIPSetCount(rName, 4), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Change to specific list - forces new + Config: testAccENIConfig_privateIPList(rName, []string{"172.16.10.59", "172.16.10.123", "172.16.10.38"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPList([]string{"172.16.10.59", "172.16.10.123", "172.16.10.38"}, &networkInterface), + testAccCheckENIDifferent(&lastInterface, &networkInterface), // different + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Change first of private_ip_list - forces new + Config: testAccENIConfig_privateIPList(rName, []string{"172.16.10.123", "172.16.10.59", "172.16.10.38"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPList([]string{"172.16.10.123", "172.16.10.59", "172.16.10.38"}, &networkInterface), + testAccCheckENIDifferent(&lastInterface, &networkInterface), // different + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + { // Change from list to set using same set + Config: testAccENIConfig_privateIPSet(rName, []string{"172.16.10.123", "172.16.10.59", "172.16.10.38"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckENIExists(resourceName, &networkInterface), + testAccCheckENIPrivateIPSet([]string{"172.16.10.123", "172.16.10.59", "172.16.10.38"}, &networkInterface), + testAccCheckENISame(&lastInterface, &networkInterface), // same + testAccCheckENIExists(resourceName, &lastInterface), + ), + }, + }, + }) +} + // checkResourceAttrPrivateDNSName ensures the Terraform state exactly matches a private DNS name // // For example: ip-172-16-10-100.us-west-2.compute.internal @@ -758,6 +1021,67 @@ func testAccCheckENIMakeExternalAttachment(n string, conf *ec2.NetworkInterface) } } +func testAccCheckENIPrivateIPSet(ips []string, iface *ec2.NetworkInterface) resource.TestCheckFunc { + return func(s *terraform.State) error { + iIPs := tfec2.FlattenNetworkInterfacePrivateIPAddresses(iface.PrivateIpAddresses) + + if !stringSlicesEqualIgnoreOrder(ips, iIPs) { + return fmt.Errorf("expected private IP set %s, got %s", strings.Join(ips, ","), strings.Join(iIPs, ",")) + } + + return nil + } +} + +func testAccCheckENIPrivateIPList(ips []string, iface *ec2.NetworkInterface) resource.TestCheckFunc { + return func(s *terraform.State) error { + iIPs := tfec2.FlattenNetworkInterfacePrivateIPAddresses(iface.PrivateIpAddresses) + + if !stringSlicesEqual(ips, iIPs) { + return fmt.Errorf("expected private IP set %s, got %s", strings.Join(ips, ","), strings.Join(iIPs, ",")) + } + + return nil + } +} + +func stringSlicesEqualIgnoreOrder(s1, s2 []string) bool { + if len(s1) != len(s2) { + return false + } + + sort.Strings(s1) + sort.Strings(s2) + + return reflect.DeepEqual(s1, s2) +} + +func stringSlicesEqual(s1, s2 []string) bool { + if len(s1) != len(s2) { + return false + } + + return reflect.DeepEqual(s1, s2) +} + +func testAccCheckENISame(iface1 *ec2.NetworkInterface, iface2 *ec2.NetworkInterface) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.StringValue(iface1.NetworkInterfaceId) != aws.StringValue(iface2.NetworkInterfaceId) { + return fmt.Errorf("interface %s should not have been replaced with %s", aws.StringValue(iface1.NetworkInterfaceId), aws.StringValue(iface2.NetworkInterfaceId)) + } + return nil + } +} + +func testAccCheckENIDifferent(iface1 *ec2.NetworkInterface, iface2 *ec2.NetworkInterface) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.StringValue(iface1.NetworkInterfaceId) == aws.StringValue(iface2.NetworkInterfaceId) { + return fmt.Errorf("interface %s should have been replaced, have %s", aws.StringValue(iface1.NetworkInterfaceId), aws.StringValue(iface2.NetworkInterfaceId)) + } + return nil + } +} + func testAccENIIPV4BaseConfig(rName string) string { return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` resource "aws_vpc" "test" { @@ -877,7 +1201,7 @@ resource "aws_network_interface" "test" { } func testAccENIIPV6CountConfig(rName string, ipv6Count int) string { - return acctest.ConfigCompose(testAccENIIPV6BaseConfig(rName) + fmt.Sprintf(` + return acctest.ConfigCompose(testAccENIIPV6BaseConfig(rName), fmt.Sprintf(` resource "aws_network_interface" "test" { subnet_id = aws_subnet.test.id private_ips = ["172.16.10.100"] @@ -907,7 +1231,7 @@ resource "aws_network_interface" "test" { } func testAccENISourceDestCheckConfig(rName string, sourceDestCheck bool) string { - return acctest.ConfigCompose(testAccENIIPV6BaseConfig(rName) + fmt.Sprintf(` + return acctest.ConfigCompose(testAccENIIPV6BaseConfig(rName), fmt.Sprintf(` resource "aws_network_interface" "test" { subnet_id = aws_subnet.test.id source_dest_check = %[2]t @@ -1091,7 +1415,7 @@ resource "aws_network_interface" "test" { } func testAccENIIPV4PrefixCountConfig(rName string, ipv4PrefixCount int) string { - return acctest.ConfigCompose(testAccENIIPV4BaseConfig(rName) + fmt.Sprintf(` + return acctest.ConfigCompose(testAccENIIPV4BaseConfig(rName), fmt.Sprintf(` resource "aws_network_interface" "test" { subnet_id = aws_subnet.test.id ipv4_prefix_count = %[2]d @@ -1135,7 +1459,7 @@ resource "aws_network_interface" "test" { } func testAccENIIPV6PrefixCountConfig(rName string, ipv6PrefixCount int) string { - return acctest.ConfigCompose(testAccENIIPV6BaseConfig(rName) + fmt.Sprintf(` + return acctest.ConfigCompose(testAccENIIPV6BaseConfig(rName), fmt.Sprintf(` resource "aws_network_interface" "test" { subnet_id = aws_subnet.test.id private_ips = ["172.16.10.100"] @@ -1148,3 +1472,40 @@ resource "aws_network_interface" "test" { } `, rName, ipv6PrefixCount)) } + +func testAccENIConfig_privateIPSet(rName string, privateIPs []string) string { + return acctest.ConfigCompose( + testAccENIIPV6BaseConfig(rName), + fmt.Sprintf(` +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test.id + security_groups = [aws_security_group.test.id] + private_ips = ["%[1]s"] +} +`, strings.Join(privateIPs, `", "`))) +} + +func testAccENIConfig_privateIPSetCount(rName string, count int) string { + return acctest.ConfigCompose( + testAccENIIPV6BaseConfig(rName), + fmt.Sprintf(` +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test.id + security_groups = [aws_security_group.test.id] + private_ips_count = %[1]d +} +`, count)) +} + +func testAccENIConfig_privateIPList(rName string, privateIPs []string) string { + return acctest.ConfigCompose( + testAccENIIPV6BaseConfig(rName), + fmt.Sprintf(` +resource "aws_network_interface" "test" { + subnet_id = aws_subnet.test.id + security_groups = [aws_security_group.test.id] + private_ip_list_enabled = true + private_ip_list = ["%[1]s"] +} +`, strings.Join(privateIPs, `", "`))) +} diff --git a/website/docs/r/network_interface.markdown b/website/docs/r/network_interface.markdown index fee7ab277d2..89799f2f0a7 100644 --- a/website/docs/r/network_interface.markdown +++ b/website/docs/r/network_interface.markdown @@ -25,30 +25,51 @@ resource "aws_network_interface" "test" { } ``` +### Example of Managing Multiple IPs on a Network Interface + +By default, private IPs are managed through the `private_ips` and `private_ips_count` arguments which manage IPs as a set of IPs that are configured without regard to order. For a new network interface, the same primary IP address is consistently selected from a given set of addresses, regardless of the order provided. However, modifications of the set of addresses of an existing interface will not alter the current primary IP address unless it has been removed from the set. + +In order to manage the private IPs as a sequentially ordered list, configure `private_ip_list_enabled` to `true` and use `private_ip_list` to manage the IPs. This will disable the `private_ips` and `private_ips_count` settings, which must be removed from the config file but are still exported. Note that changing the first address of `private_ip_list`, which is the primary, always requires a new interface. + +If you are managing a specific set or list of IPs, instead of just using `private_ips_count`, this is a potential workflow for also leveraging `private_ips_count` to have AWS automatically assign additional IP addresses: + +1. Comment out `private_ips`, `private_ip_list`, `private_ip_list_enabled` in your configuration +2. Set the desired `private_ips_count` (count of the number of secondaries, the primary is not included) +3. Apply to assign the extra IPs +4. Remove `private_ips_count` and restore your settings from the first step +5. Add the new IPs to your current settings +6. Apply again to update the stored state + +This process can also be used to remove IP addresses in addition to the option of manually removing them. Adding IP addresses in a manually is more difficult because it requires knowledge of which addresses are available. + ## Argument Reference -The following arguments are supported: +The following arguments are required: * `subnet_id` - (Required) Subnet ID to create the ENI in. -* `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. -* `ipv4_prefixes` - (Optional) One or more IPv4 prefixes assigned to the network interface. -* `ipv4_prefix_count` - (Optional) The number of IPv4 prefixes that AWS automatically assigns to the network interface. -* `ipv6_prefixes` - (Optional) One or more IPv6 prefixes assigned to the network interface. -* `ipv6_prefix_count` - (Optional) The number of IPv6 prefixes that AWS automatically assigns to the network interface. --> **NOTE:** Changing `interface_type` will cause the resource to be destroyed and re-created. +The following arguments are optional: -* `interface_type` - (Optional) Type of network interface to create. Set to `efa` for Elastic Fabric Adapter. -* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `attachment` - (Optional) Configuration block to define the attachment of the ENI. See below. +* `description` - (Optional) Description for the network interface. +* `interface_type` - (Optional) Type of network interface to create. Set to `efa` for Elastic Fabric Adapter. Changing `interface_type` will cause the resource to be destroyed and re-created. +* `ipv4_prefix_count` - (Optional) Number of IPv4 prefixes that AWS automatically assigns to the network interface. +* `ipv4_prefixes` - (Optional) One or more IPv4 prefixes assigned to the network interface. +* `ipv6_address_count` - (Optional) 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. +* `ipv6_address_list_enable` - (Optional) Whether `ipv6_addreses_list` is allowed and controls the IPs to assign to the ENI and `ipv6_addresses` and `ipv6_addresses_count` become read-only. Default false. +* `ipv6_address_list` - (Optional) List of private IPs to assign to the ENI in sequential order. +* `ipv6_addresses` - (Optional) One or more specific IPv6 addresses from the IPv6 CIDR block range of your subnet. Addresses are assigned without regard to order. You can't use this option if you're specifying `ipv6_address_count`. +* `ipv6_prefix_count` - (Optional) Number of IPv6 prefixes that AWS automatically assigns to the network interface. +* `ipv6_prefixes` - (Optional) One or more IPv6 prefixes assigned to the network interface. +* `private_ip_list` - (Optional) List of private IPs to assign to the ENI in sequential order. Requires setting `private_ip_list_enable` to `true`. +* `private_ip_list_enable` - (Optional) Whether `private_ip_list` is allowed and controls the IPs to assign to the ENI and `private_ips` and `private_ips_count` become read-only. Default false. +* `private_ips` - (Optional) List of private IPs to assign to the ENI without regard to order. +* `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. +* `security_groups` - (Optional) List of security group IDs to assign to the ENI. +* `source_dest_check` - (Optional) Whether to enable source destination checking for the ENI. Default true. +* `tags` - (Optional) Map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. -The `attachment` block supports: +### `attachment` * `instance` - (Required) ID of the instance to attach to. * `device_index` - (Required) Integer to define the devices index. @@ -57,12 +78,12 @@ The `attachment` block supports: In addition to all arguments above, the following attributes are exported: -* `arn` - The ARN of the network interface. -* `id` - The ID of the network interface. -* `mac_address` - The MAC address of the network interface. -* `owner_id` - The AWS account ID of the owner of the network interface. -* `private_dns_name` - The private DNS name of the network interface (IPv4). -* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). +* `arn` - ARN of the network interface. +* `id` - ID of the network interface. +* `mac_address` - MAC address of the network interface. +* `owner_id` - AWS account ID of the owner of the network interface. +* `private_dns_name` - Private DNS name of the network interface (IPv4). +* `tags_all` - Map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). ## Import