Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/aws_eip enhancement IPAM pool allocation #39604

Merged
merged 7 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/39604.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/aws_eip: Add `ipam_pool_id` argument in support of [public IPAM pools](https://docs.aws.amazon.com/vpc/latest/ipam/tutorials-eip-pool.html)
```

```release-note:enhancement
data-source/aws_eip: Add `ipam_pool_id` attribute
```
5 changes: 3 additions & 2 deletions internal/service/ec2/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,9 @@ func vpnConnectionType_Values() []string {
}

const (
amazonIPv6PoolID = "Amazon"
ipamManagedIPv6PoolID = "IPAM Managed"
amazonIPv6PoolID = "Amazon"
ipamManagedIPv6PoolID = "IPAM Managed"
publicIPv4PoolIDIPAMPoolPrefix = "ipam-pool-"
)

const (
Expand Down
53 changes: 52 additions & 1 deletion internal/service/ec2/ec2_eip.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import (
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/hashicorp/aws-sdk-go-base/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/enum"
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices"
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"
Expand Down Expand Up @@ -96,11 +98,16 @@ func resourceEIP() *schema.Resource {
Optional: true,
Computed: true,
},
"ipam_pool_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
},
"network_border_group": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"network_interface": {
Type: schema.TypeString,
Expand Down Expand Up @@ -171,6 +178,10 @@ func resourceEIPCreate(ctx context.Context, d *schema.ResourceData, meta interfa
input.Domain = types.DomainTypeVpc
}

if v, ok := d.GetOk("ipam_pool_id"); ok {
input.IpamPoolId = aws.String(v.(string))
}

if v, ok := d.GetOk("network_border_group"); ok {
input.NetworkBorderGroup = aws.String(v.(string))
}
Expand Down Expand Up @@ -241,6 +252,9 @@ func resourceEIPRead(ctx context.Context, d *schema.ResourceData, meta interface
d.Set("customer_owned_ipv4_pool", address.CustomerOwnedIpv4Pool)
d.Set(names.AttrDomain, address.Domain)
d.Set("instance", address.InstanceId)
if v := aws.ToString(address.PublicIpv4Pool); strings.HasPrefix(v, publicIPv4PoolIDIPAMPoolPrefix) {
d.Set("ipam_pool_id", v)
}
d.Set("network_border_group", address.NetworkBorderGroup)
d.Set("network_interface", address.NetworkInterfaceId)
d.Set("public_ipv4_pool", address.PublicIpv4Pool)
Expand Down Expand Up @@ -326,6 +340,21 @@ func resourceEIPDelete(ctx context.Context, d *schema.ResourceData, meta interfa
log.Printf("[INFO] Deleting EC2 EIP: %s", d.Id())
_, err := conn.ReleaseAddress(ctx, input)

// If the EIP's CIDR block was allocated from an IPAM pool, wait for the allocation to disappear.
if v, ok := d.GetOk("ipam_pool_id"); ok {
ipamPoolID := v.(string)
const (
timeout = 10 * time.Minute // IPAM eventual consistency
)
_, err := tfresource.RetryUntilNotFound(ctx, timeout, func() (interface{}, error) {
return findIPAMPoolAllocationsForEIP(ctx, conn, ipamPoolID, d.Get("allocation_id").(string))
})

if err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for EC2 EIP (%s) IPAM Pool (%s) Allocation delete: %s", d.Id(), ipamPoolID, err)
}
}

if tfawserr.ErrCodeEquals(err, errCodeInvalidAllocationIDNotFound) {
return diags
}
Expand Down Expand Up @@ -423,3 +452,25 @@ func eipARN(c *conns.AWSClient, allocationID string) string {
Resource: "elastic-ip/" + allocationID,
}.String()
}

func findIPAMPoolAllocationsForEIP(ctx context.Context, conn *ec2.Client, ipamPoolID, eipAllocationID string) ([]types.IpamPoolAllocation, error) {
input := &ec2.GetIpamPoolAllocationsInput{
IpamPoolId: aws.String(ipamPoolID),
}

output, err := findIPAMPoolAllocations(ctx, conn, input)

if err != nil {
return nil, err
}

output = tfslices.Filter(output, func(v types.IpamPoolAllocation) bool {
return v.ResourceType == types.IpamPoolAllocationResourceTypeEip && aws.ToString(v.ResourceId) == eipAllocationID
})

if len(output) == 0 {
return nil, &retry.NotFoundError{}
}

return output, nil
}
8 changes: 8 additions & 0 deletions internal/service/ec2/ec2_eip_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package ec2

import (
"context"
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
Expand Down Expand Up @@ -65,6 +66,10 @@ func dataSourceEIP() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"ipam_pool_id": {
Type: schema.TypeString,
Computed: true,
},
names.AttrNetworkInterfaceID: {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -162,6 +167,9 @@ func dataSourceEIPRead(ctx context.Context, d *schema.ResourceData, meta interfa
d.Set("customer_owned_ipv4_pool", eip.CustomerOwnedIpv4Pool)
d.Set(names.AttrDomain, eip.Domain)
d.Set(names.AttrInstanceID, eip.InstanceId)
if v := aws.ToString(eip.PublicIpv4Pool); strings.HasPrefix(v, publicIPv4PoolIDIPAMPoolPrefix) {
d.Set("ipam_pool_id", v)
}
d.Set(names.AttrNetworkInterfaceID, eip.NetworkInterfaceId)
d.Set("network_interface_owner_id", eip.NetworkInterfaceOwnerId)
d.Set("public_ipv4_pool", eip.PublicIpv4Pool)
Expand Down
3 changes: 2 additions & 1 deletion internal/service/ec2/ec2_eip_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,11 @@ func TestAccEC2EIPDataSource_publicIP(t *testing.T) {
{
Config: testAccEIPDataSourceConfig_publicIP(rName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrPair(dataSourceName, names.AttrDomain, resourceName, names.AttrDomain),
resource.TestCheckResourceAttrPair(dataSourceName, names.AttrID, resourceName, names.AttrID),
resource.TestCheckResourceAttrPair(dataSourceName, "ipam_pool_id", resourceName, "ipam_pool_id"),
resource.TestCheckResourceAttrPair(dataSourceName, "public_dns", resourceName, "public_dns"),
resource.TestCheckResourceAttrPair(dataSourceName, "public_ip", resourceName, "public_ip"),
resource.TestCheckResourceAttrPair(dataSourceName, names.AttrDomain, resourceName, names.AttrDomain),
),
},
},
Expand Down
64 changes: 64 additions & 0 deletions internal/service/ec2/ec2_eip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,31 @@ func TestAccEC2EIP_PublicIPv4Pool_custom(t *testing.T) {
})
}

func TestAccEC2EIP_PublicIPv4Pool_IPAMPoolId(t *testing.T) {
ctx := acctest.Context(t)
var conf types.Address
resourceName := "aws_eip.test"
ipamPoolDataSourceName := "aws_vpc_ipam_pool.test_pool"
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckEIPDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccEIPConfig_publicIPv4_IPAMPoolId(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckEIPExists(ctx, resourceName, &conf),
resource.TestCheckResourceAttrSet(resourceName, "public_ip"),
resource.TestCheckResourceAttrPair(resourceName, "ipam_pool_id", ipamPoolDataSourceName, names.AttrID),
),
},
},
})
}

func TestAccEC2EIP_customerOwnedIPv4Pool(t *testing.T) {
ctx := acctest.Context(t)
var conf types.Address
Expand Down Expand Up @@ -1150,6 +1175,45 @@ resource "aws_eip" "test" {
`, rName, poolName)
}

func testAccEIPConfig_publicIPv4_IPAMPoolId(rName string) string {
return fmt.Sprintf(`
data "aws_region" "current" {}

resource "aws_vpc_ipam" "test" {
operating_regions {
region_name = data.aws_region.current.name
}
tier = "free"
}

resource "aws_vpc_ipam_pool" "test_pool" {
address_family = "ipv4"
ipam_scope_id = aws_vpc_ipam.test.public_default_scope_id
locale = data.aws_region.current.name
public_ip_source = "amazon"
description = "Test Amazon CIDR Pool"
aws_service = "ec2"
}

resource "aws_vpc_ipam_pool_cidr" "test_cidr" {
ipam_pool_id = aws_vpc_ipam_pool.test_pool.id
netmask_length = 30
}

resource "aws_eip" "test" {
domain = "vpc"
ipam_pool_id = aws_vpc_ipam_pool.test_pool.id

tags = {
Name = %[1]q
}

depends_on = [aws_vpc_ipam_pool_cidr.test_cidr]

}
`, rName)
}

func testAccEIPConfig_customerOwnedIPv4Pool(rName string) string {
return fmt.Sprintf(`
data "aws_ec2_coip_pools" "test" {}
Expand Down
6 changes: 3 additions & 3 deletions internal/service/ec2/vpc_.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ func defaultIPv6CIDRBlockAssociation(vpc *types.Vpc, associationID string) *type

if associationID != "" {
for _, v := range vpc.Ipv6CidrBlockAssociationSet {
if state := string(v.Ipv6CidrBlockState.State); state == string(types.VpcCidrBlockStateCodeAssociated) && aws.ToString(v.AssociationId) == associationID {
if state := v.Ipv6CidrBlockState.State; state == types.VpcCidrBlockStateCodeAssociated && aws.ToString(v.AssociationId) == associationID {
ipv6CIDRBlockAssociation = v
break
}
Expand All @@ -559,7 +559,7 @@ func defaultIPv6CIDRBlockAssociation(vpc *types.Vpc, associationID string) *type

if ipv6CIDRBlockAssociation == (types.VpcIpv6CidrBlockAssociation{}) {
for _, v := range vpc.Ipv6CidrBlockAssociationSet {
if string(v.Ipv6CidrBlockState.State) == string(types.VpcCidrBlockStateCodeAssociated) {
if v.Ipv6CidrBlockState.State == types.VpcCidrBlockStateCodeAssociated {
ipv6CIDRBlockAssociation = v
}
}
Expand Down Expand Up @@ -741,7 +741,7 @@ func findIPAMPoolAllocationsForVPC(ctx context.Context, conn *ec2.Client, poolID
}

output = tfslices.Filter(output, func(v types.IpamPoolAllocation) bool {
return string(v.ResourceType) == string(types.IpamPoolAllocationResourceTypeVpc) && aws.ToString(v.ResourceId) == vpcID
return v.ResourceType == types.IpamPoolAllocationResourceTypeVpc && aws.ToString(v.ResourceId) == vpcID
})

if len(output) == 0 {
Expand Down
1 change: 1 addition & 0 deletions website/docs/d/eip.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ This data source exports the following attributes in addition to the arguments a
* `domain` - Whether the address is for use in EC2-Classic (standard) or in a VPC (vpc).
* `id` - If VPC Elastic IP, the allocation identifier. If EC2-Classic Elastic IP, the public IP address.
* `instance_id` - ID of the instance that the address is associated with (if any).
* `ipam_pool_id`- The ID of an IPAM pool which has an Amazon-provided or BYOIP public IPv4 CIDR provisioned to it.
* `network_interface_id` - The ID of the network interface.
* `network_interface_owner_id` - The ID of the AWS account that owns the network interface.
* `private_ip` - Private IP address associated with the Elastic IP address.
Expand Down
10 changes: 10 additions & 0 deletions website/docs/r/eip.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ resource "aws_eip" "byoip-ip" {
}
```

### Allocating EIP from the IPAM Pool

```terraform
resource "aws_eip" "ipam-ip" {
domain = "vpc"
ipam_pool_id = "ipam-pool-07ccc86aa41bef7ce"
}
```

## Argument Reference

This resource supports the following arguments:
Expand All @@ -102,6 +111,7 @@ This resource supports the following arguments:
* `customer_owned_ipv4_pool` - (Optional) ID of a customer-owned address pool. For more on customer owned IP addressed check out [Customer-owned IP addresses guide](https://docs.aws.amazon.com/outposts/latest/userguide/outposts-networking-components.html#ip-addressing).
* `domain` - Indicates if this EIP is for use in VPC (`vpc`).
* `instance` - (Optional) EC2 instance ID.
* `ipam_pool_id`- (Optional) The ID of an IPAM pool which has an Amazon-provided or BYOIP public IPv4 CIDR provisioned to it.
* `network_border_group` - (Optional) Location from which the IP address is advertised. Use this parameter to limit the address to this location.
* `network_interface` - (Optional) Network interface ID to associate with.
* `public_ipv4_pool` - (Optional) EC2 IPv4 address pool identifier or `amazon`.
Expand Down
Loading