Skip to content

Commit

Permalink
Merge pull request #31778 from bschaatsbergen/add-secondary-eips-to-nat
Browse files Browse the repository at this point in the history
Add secondary IPs to AWS NAT Gateway
  • Loading branch information
ewbankkit authored Jul 24, 2023
2 parents be9d5ac + 6bfcb5c commit 9bfe243
Show file tree
Hide file tree
Showing 10 changed files with 786 additions and 97 deletions.
11 changes: 11 additions & 0 deletions .changelog/31778.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```release-note:enhancement
resource/aws_nat_gateway: Add `secondary_allocation_ids`, `secondary_private_ip_addresses` and `secondary_private_ip_address_count` arguments
```

```release-note:enhancement
resource/aws_nat_gateway: Add configurable timeouts
```

```release-note:enhancement
data-source/aws_nat_gateway: Add `secondary_allocation_ids`, `secondary_private_ip_addresses` and `secondary_private_ip_address_count` attributes
```
32 changes: 32 additions & 0 deletions internal/service/ec2/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -6453,6 +6453,38 @@ func FindNATGatewayByID(ctx context.Context, conn *ec2.EC2, id string) (*ec2.Nat
return output, nil
}

func FindNATGatewayAddressByNATGatewayIDAndAllocationID(ctx context.Context, conn *ec2.EC2, natGatewayID, allocationID string) (*ec2.NatGatewayAddress, error) {
output, err := FindNATGatewayByID(ctx, conn, natGatewayID)

if err != nil {
return nil, err
}

for _, v := range output.NatGatewayAddresses {
if aws.StringValue(v.AllocationId) == allocationID {
return v, nil
}
}

return nil, &retry.NotFoundError{}
}

func FindNATGatewayAddressByNATGatewayIDAndPrivateIP(ctx context.Context, conn *ec2.EC2, natGatewayID, privateIP string) (*ec2.NatGatewayAddress, error) {
output, err := FindNATGatewayByID(ctx, conn, natGatewayID)

if err != nil {
return nil, err
}

for _, v := range output.NatGatewayAddresses {
if aws.StringValue(v.PrivateIp) == privateIP {
return v, nil
}
}

return nil, &retry.NotFoundError{}
}

func FindPlacementGroupByName(ctx context.Context, conn *ec2.EC2, name string) (*ec2.PlacementGroup, error) {
input := &ec2.DescribePlacementGroupsInput{
GroupNames: aws.StringSlice([]string{name}),
Expand Down
32 changes: 32 additions & 0 deletions internal/service/ec2/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,38 @@ func StatusNATGatewayState(ctx context.Context, conn *ec2.EC2, id string) retry.
}
}

func StatusNATGatewayAddressByNATGatewayIDAndAllocationID(ctx context.Context, conn *ec2.EC2, natGatewayID, allocationID string) retry.StateRefreshFunc {
return func() (interface{}, string, error) {
output, err := FindNATGatewayAddressByNATGatewayIDAndAllocationID(ctx, conn, natGatewayID, allocationID)

if tfresource.NotFound(err) {
return nil, "", nil
}

if err != nil {
return nil, "", err
}

return output, aws.StringValue(output.Status), nil
}
}

func StatusNATGatewayAddressByNATGatewayIDAndPrivateIP(ctx context.Context, conn *ec2.EC2, natGatewayID, privateIP string) retry.StateRefreshFunc {
return func() (interface{}, string, error) {
output, err := FindNATGatewayAddressByNATGatewayIDAndPrivateIP(ctx, conn, natGatewayID, privateIP)

if tfresource.NotFound(err) {
return nil, "", nil
}

if err != nil {
return nil, "", err
}

return output, aws.StringValue(output.Status), nil
}
}

const (
RouteStatusReady = "ready"
)
Expand Down
215 changes: 209 additions & 6 deletions internal/service/ec2/vpc_nat_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@ package ec2

import (
"context"
"fmt"
"log"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
"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"
"github.com/hashicorp/terraform-provider-aws/internal/flex"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
"github.com/hashicorp/terraform-provider-aws/names"
"golang.org/x/exp/slices"
)

// @SDKResource("aws_nat_gateway", name="NAT Gateway")
Expand All @@ -34,6 +39,12 @@ func ResourceNATGateway() *schema.Resource {
StateContext: schema.ImportStatePassthroughContext,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(10 * time.Minute),
Update: schema.DefaultTimeout(10 * time.Minute),
Delete: schema.DefaultTimeout(30 * time.Minute),
},

Schema: map[string]*schema.Schema{
"allocation_id": {
Type: schema.TypeString,
Expand Down Expand Up @@ -66,6 +77,25 @@ func ResourceNATGateway() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"secondary_allocation_ids": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"secondary_private_ip_address_count": {
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
ConflictsWith: []string{"secondary_private_ip_addresses"},
},
"secondary_private_ip_addresses": {
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
ConflictsWith: []string{"secondary_private_ip_address_count"},
},
"subnet_id": {
Type: schema.TypeString,
Required: true,
Expand All @@ -75,7 +105,10 @@ func ResourceNATGateway() *schema.Resource {
names.AttrTagsAll: tftags.TagsSchemaComputed(),
},

CustomizeDiff: verify.SetTagsDiff,
CustomizeDiff: customdiff.All(
resourceNATGatewayCustomizeDiff,
verify.SetTagsDiff,
),
}
}

Expand All @@ -99,6 +132,18 @@ func resourceNATGatewayCreate(ctx context.Context, d *schema.ResourceData, meta
input.PrivateIpAddress = aws.String(v.(string))
}

if v, ok := d.GetOk("secondary_allocation_ids"); ok && v.(*schema.Set).Len() > 0 {
input.SecondaryAllocationIds = flex.ExpandStringSet(v.(*schema.Set))
}

if v, ok := d.GetOk("secondary_private_ip_address_count"); ok {
input.SecondaryPrivateIpAddressCount = aws.Int64(int64(v.(int)))
}

if v, ok := d.GetOk("secondary_private_ip_addresses"); ok && v.(*schema.Set).Len() > 0 {
input.SecondaryPrivateIpAddresses = flex.ExpandStringSet(v.(*schema.Set))
}

if v, ok := d.GetOk("subnet_id"); ok {
input.SubnetId = aws.String(v.(string))
}
Expand All @@ -111,7 +156,7 @@ func resourceNATGatewayCreate(ctx context.Context, d *schema.ResourceData, meta

d.SetId(aws.StringValue(output.NatGateway.NatGatewayId))

if _, err := WaitNATGatewayCreated(ctx, conn, d.Id()); err != nil {
if _, err := WaitNATGatewayCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil {
return diag.Errorf("waiting for EC2 NAT Gateway (%s) create: %s", d.Id(), err)
}

Expand All @@ -133,19 +178,30 @@ func resourceNATGatewayRead(ctx context.Context, d *schema.ResourceData, meta in
return diag.Errorf("reading EC2 NAT Gateway (%s): %s", d.Id(), err)
}

var secondaryAllocationIDs, secondaryPrivateIPAddresses []string

for _, address := range ng.NatGatewayAddresses {
// Length check guarantees the attributes are always set (#30865).
if len(ng.NatGatewayAddresses) == 1 || aws.BoolValue(address.IsPrimary) {
if isPrimary := aws.BoolValue(address.IsPrimary); isPrimary || len(ng.NatGatewayAddresses) == 1 {
d.Set("allocation_id", address.AllocationId)
d.Set("association_id", address.AssociationId)
d.Set("network_interface_id", address.NetworkInterfaceId)
d.Set("private_ip", address.PrivateIp)
d.Set("public_ip", address.PublicIp)
break
} else if !isPrimary {
if allocationID := aws.StringValue(address.AllocationId); allocationID != "" {
secondaryAllocationIDs = append(secondaryAllocationIDs, allocationID)
}
if privateIP := aws.StringValue(address.PrivateIp); privateIP != "" {
secondaryPrivateIPAddresses = append(secondaryPrivateIPAddresses, privateIP)
}
}
}

d.Set("connectivity_type", ng.ConnectivityType)
d.Set("secondary_allocation_ids", secondaryAllocationIDs)
d.Set("secondary_private_ip_address_count", len(secondaryPrivateIPAddresses))
d.Set("secondary_private_ip_addresses", secondaryPrivateIPAddresses)
d.Set("subnet_id", ng.SubnetId)

setTagsOut(ctx, ng.Tags)
Expand All @@ -154,7 +210,123 @@ func resourceNATGatewayRead(ctx context.Context, d *schema.ResourceData, meta in
}

func resourceNATGatewayUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
// Tags only.
conn := meta.(*conns.AWSClient).EC2Conn(ctx)

switch d.Get("connectivity_type").(string) {
case ec2.ConnectivityTypePrivate:
if d.HasChanges("secondary_private_ip_addresses") {
oRaw, nRaw := d.GetChange("secondary_private_ip_addresses")
o, n := oRaw.(*schema.Set), nRaw.(*schema.Set)

if add := n.Difference(o); add.Len() > 0 {
input := &ec2.AssignPrivateNatGatewayAddressInput{
NatGatewayId: aws.String(d.Id()),
PrivateIpAddresses: flex.ExpandStringSet(add),
}

_, err := conn.AssignPrivateNatGatewayAddressWithContext(ctx, input)

if err != nil {
return diag.Errorf("assigning EC2 NAT Gateway (%s) private IP addresses: %s", d.Id(), err)
}

for _, privateIP := range flex.ExpandStringValueSet(add) {
if _, err := WaitNATGatewayAddressAssigned(ctx, conn, d.Id(), privateIP, d.Timeout(schema.TimeoutUpdate)); err != nil {
return diag.Errorf("waiting for EC2 NAT Gateway (%s) private IP address (%s) assign: %s", d.Id(), privateIP, err)
}
}
}

if del := o.Difference(n); del.Len() > 0 {
input := &ec2.UnassignPrivateNatGatewayAddressInput{
NatGatewayId: aws.String(d.Id()),
PrivateIpAddresses: flex.ExpandStringSet(del),
}

_, err := conn.UnassignPrivateNatGatewayAddressWithContext(ctx, input)

if err != nil {
return diag.Errorf("unassigning EC2 NAT Gateway (%s) private IP addresses: %s", d.Id(), err)
}

for _, privateIP := range flex.ExpandStringValueSet(del) {
if _, err := WaitNATGatewayAddressUnassigned(ctx, conn, d.Id(), privateIP, d.Timeout(schema.TimeoutUpdate)); err != nil {
return diag.Errorf("waiting for EC2 NAT Gateway (%s) private IP address (%s) unassign: %s", d.Id(), privateIP, err)
}
}
}
}

case ec2.ConnectivityTypePublic:
if d.HasChanges("secondary_allocation_ids") {
oRaw, nRaw := d.GetChange("secondary_allocation_ids")
o, n := oRaw.(*schema.Set), nRaw.(*schema.Set)

if add := n.Difference(o); add.Len() > 0 {
input := &ec2.AssociateNatGatewayAddressInput{
AllocationIds: flex.ExpandStringSet(add),
NatGatewayId: aws.String(d.Id()),
}

if d.HasChanges("secondary_private_ip_addresses") {
oRaw, nRaw := d.GetChange("secondary_private_ip_addresses")
o, n := oRaw.(*schema.Set), nRaw.(*schema.Set)

if add := n.Difference(o); add.Len() > 0 {
input.PrivateIpAddresses = flex.ExpandStringSet(add)
}
}

_, err := conn.AssociateNatGatewayAddressWithContext(ctx, input)

if err != nil {
return diag.Errorf("associating EC2 NAT Gateway (%s) allocation IDs: %s", d.Id(), err)
}

for _, allocationID := range flex.ExpandStringValueSet(add) {
if _, err := WaitNATGatewayAddressAssociated(ctx, conn, d.Id(), allocationID, d.Timeout(schema.TimeoutUpdate)); err != nil {
return diag.Errorf("waiting for EC2 NAT Gateway (%s) allocation ID (%s) associate: %s", d.Id(), allocationID, err)
}
}
}

if del := o.Difference(n); del.Len() > 0 {
natGateway, err := FindNATGatewayByID(ctx, conn, d.Id())

if err != nil {
return diag.Errorf("reading EC2 NAT Gateway (%s): %s", d.Id(), err)
}

allocationIDs := flex.ExpandStringValueSet(del)
var associationIDs []string

for _, natGatewayAddress := range natGateway.NatGatewayAddresses {
allocationID := aws.StringValue(natGatewayAddress.AllocationId)
if slices.Contains(allocationIDs, allocationID) {
associationIDs = append(associationIDs, aws.StringValue(natGatewayAddress.AssociationId))
}
}

input := &ec2.DisassociateNatGatewayAddressInput{
AssociationIds: aws.StringSlice(associationIDs),
NatGatewayId: aws.String(d.Id()),
}

_, err = conn.DisassociateNatGatewayAddressWithContext(ctx, input)

if err != nil {
return diag.Errorf("disassociating EC2 NAT Gateway (%s) allocation IDs: %s", d.Id(), err)
}

for _, allocationID := range allocationIDs {
if _, err := WaitNATGatewayAddressDisassociated(ctx, conn, d.Id(), allocationID, d.Timeout(schema.TimeoutUpdate)); err != nil {
return diag.Errorf("waiting for EC2 NAT Gateway (%s) allocation ID (%s) disassociate: %s", d.Id(), allocationID, err)
}
}
}
}
}

return resourceNATGatewayRead(ctx, d, meta)
}

Expand All @@ -174,9 +346,40 @@ func resourceNATGatewayDelete(ctx context.Context, d *schema.ResourceData, meta
return diag.Errorf("deleting EC2 NAT Gateway (%s): %s", d.Id(), err)
}

if _, err := WaitNATGatewayDeleted(ctx, conn, d.Id()); err != nil {
if _, err := WaitNATGatewayDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil {
return diag.Errorf("waiting for EC2 NAT Gateway (%s) delete: %s", d.Id(), err)
}

return nil
}

func resourceNATGatewayCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error {
switch connectivityType := diff.Get("connectivity_type").(string); connectivityType {
case ec2.ConnectivityTypePrivate:
if _, ok := diff.GetOk("allocation_id"); ok {
return fmt.Errorf(`allocation_id is not supported with connectivity_type = "%s"`, connectivityType)
}
if v, ok := diff.GetOk("secondary_allocation_ids"); ok && v.(*schema.Set).Len() > 0 {
return fmt.Errorf(`secondary_allocation_ids is not supported with connectivity_type = "%s"`, connectivityType)
}

case ec2.ConnectivityTypePublic:
if v := diff.GetRawConfig().GetAttr("secondary_private_ip_address_count"); v.IsKnown() && !v.IsNull() {
return fmt.Errorf(`secondary_private_ip_address_count is not supported with connectivity_type = "%s"`, connectivityType)
}

if diff.Id() != "" && diff.HasChange("secondary_allocation_ids") {
if err := diff.SetNewComputed("secondary_private_ip_address_count"); err != nil {
return fmt.Errorf("setting secondary_private_ip_address_count to computed: %s", err)
}

if v := diff.GetRawConfig().GetAttr("secondary_private_ip_addresses"); !v.IsKnown() {
if err := diff.SetNewComputed("secondary_private_ip_addresses"); err != nil {
return fmt.Errorf("setting secondary_private_ip_addresses to computed: %s", err)
}
}
}
}

return nil
}
Loading

0 comments on commit 9bfe243

Please sign in to comment.