Skip to content
Draft
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
22 changes: 22 additions & 0 deletions internal/service/ec2/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -4416,6 +4416,28 @@ func findIPAMResourceDiscoveryAssociationByID(ctx context.Context, conn *ec2.Cli
return output, nil
}

func findIPAMResourceCIDRs(ctx context.Context, conn *ec2.Client, input *ec2.GetIpamResourceCidrsInput) ([]awstypes.IpamResourceCidr, error) {
var output []awstypes.IpamResourceCidr

pages := ec2.NewGetIpamResourceCidrsPaginator(conn, input)
for pages.HasMorePages() {
page, err := pages.NextPage(ctx)
if err != nil {
if tfawserr.ErrCodeEquals(err, errCodeInvalidIPAMPoolIdNotFound) {
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: input,
}
}
return nil, err
}

output = append(output, page.IpamResourceCidrs...)
}

return output, nil
}

func findIPAMScope(ctx context.Context, conn *ec2.Client, input *ec2.DescribeIpamScopesInput) (*awstypes.IpamScope, error) {
output, err := findIPAMScopes(ctx, conn, input)

Expand Down
4 changes: 4 additions & 0 deletions internal/service/ec2/vpc_default_subnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ func resourceDefaultSubnet() *schema.Resource {
// - availability_zone_id is Computed-only
// - cidr_block is Computed-only
// - enable_lni_at_device_index is Computed-only
// - ipv4_ipam_pool_id is omitted as it's not set in resourceSubnetRead
// - ipv4_netmask_length is omitted as it's not set in resourceSubnetRead
// - ipv6_cidr_block is Optional/Computed as it's automatically assigned if ipv6_native = true
// - ipv6_ipam_pool_id is omitted as it's not set in resourceSubnetRead
// - ipv6_netmask_length is omitted as it's not set in resourceSubnetRead
// - map_public_ip_on_launch has a Default of true
// - outpost_arn is Computed-only
// - vpc_id is Computed-only
Expand Down
117 changes: 116 additions & 1 deletion internal/service/ec2/vpc_ipam_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package ec2

import (
"context"
"fmt"
"log"
"strings"
"time"
Expand All @@ -15,6 +16,7 @@ import (
"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/id"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"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"
Expand All @@ -41,7 +43,8 @@ func resourceIPAMPool() *schema.Resource {
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(3 * time.Minute),
// Cross-region resources take 20+ minutes to be monitored by IPAM
Create: schema.DefaultTimeout(35 * time.Minute),
Update: schema.DefaultTimeout(3 * time.Minute),
Delete: schema.DefaultTimeout(3 * time.Minute),
},
Expand Down Expand Up @@ -138,6 +141,37 @@ func resourceIPAMPool() *schema.Resource {
Optional: true,
ForceNew: true,
},
"source_resource": {
Type: schema.TypeList,
Optional: true,
ForceNew: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
names.AttrResourceID: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
names.AttrResourceOwner: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"resource_region": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
names.AttrResourceType: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateDiagFunc: enum.Validate[awstypes.IpamPoolSourceResourceType](),
},
},
},
},
names.AttrState: {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -213,6 +247,76 @@ func resourceIPAMPoolCreate(ctx context.Context, d *schema.ResourceData, meta an
input.SourceIpamPoolId = aws.String(v.(string))
}

var sourceResourceData map[string]any
if v, ok := d.GetOk("source_resource"); ok && len(v.([]any)) > 0 {
sourceResourceData = v.([]any)[0].(map[string]any)
}

if sourceResourceData != nil {
resourceID := sourceResourceData[names.AttrResourceID].(string)
resourceRegion := sourceResourceData["resource_region"].(string)
resourceOwner := aws.String(sourceResourceData[names.AttrResourceOwner].(string))
resourceType := awstypes.IpamPoolSourceResourceType(sourceResourceData[names.AttrResourceType].(string))

log.Printf("[DEBUG] Checking if resource %s exists in region %s before waiting for IPAM discovery", resourceID, resourceRegion)

resourceConn := conn
if resourceRegion != meta.(*conns.AWSClient).Region(ctx) {
resourceCtx := conns.NewResourceContext(ctx, names.EC2ServiceID, "aws_vpc_ipam_pool", resourceRegion)
resourceConn = meta.(*conns.AWSClient).EC2Client(resourceCtx)
}

if resourceType == awstypes.IpamPoolSourceResourceTypeVpc {
if _, err := findVPCByID(ctx, resourceConn, resourceID); err != nil {
return sdkdiag.AppendErrorf(diags, "source_resource VPC (%s) does not exist: %s", resourceID, err)
}
}

log.Printf("[DEBUG] Resource %s exists, waiting for IPAM to manage the resource", resourceID)

// Wait for the resource to be managed by IPAM - can take 20+ minutes
_, err = tfresource.RetryWhenNotFound(ctx, d.Timeout(schema.TimeoutCreate), func(ctx context.Context) (any, error) {
input := &ec2.GetIpamResourceCidrsInput{
IpamScopeId: aws.String(scopeID),
Filters: []awstypes.Filter{
{
Name: aws.String("resource-id"),
Values: []string{resourceID},
},
{
Name: aws.String("management-state"),
Values: []string{"managed"},
},
},
}

resources, err := findIPAMResourceCIDRs(ctx, conn, input)
if err != nil {
return nil, err
}

if len(resources) == 0 {
return nil, &retry.NotFoundError{
Message: fmt.Sprintf("resource %s not yet managed by IPAM", resourceID),
}
}

log.Printf("[DEBUG] Resource %s is now managed by IPAM", resourceID)
return resources, nil
})

if err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for resource %s to be managed by IPAM: %s", resourceID, err)
}

input.SourceResource = &awstypes.IpamPoolSourceResourceRequest{
ResourceId: aws.String(resourceID),
ResourceOwner: resourceOwner,
ResourceRegion: aws.String(resourceRegion),
ResourceType: resourceType,
}
}

output, err := conn.CreateIpamPool(ctx, input)

if err != nil {
Expand Down Expand Up @@ -258,6 +362,17 @@ func resourceIPAMPoolRead(ctx context.Context, d *schema.ResourceData, meta any)
d.Set("publicly_advertisable", pool.PubliclyAdvertisable)
d.Set("public_ip_source", pool.PublicIpSource)
d.Set("source_ipam_pool_id", pool.SourceIpamPoolId)
if pool.SourceResource != nil {
tfMap := map[string]any{
names.AttrResourceID: aws.ToString(pool.SourceResource.ResourceId),
names.AttrResourceOwner: aws.ToString(pool.SourceResource.ResourceOwner),
"resource_region": aws.ToString(pool.SourceResource.ResourceRegion),
names.AttrResourceType: string(pool.SourceResource.ResourceType),
}
d.Set("source_resource", []any{tfMap})
} else {
d.Set("source_resource", nil)
}
d.Set(names.AttrState, pool.State)

setTagsOut(ctx, pool.Tags)
Expand Down
105 changes: 104 additions & 1 deletion internal/service/ec2/vpc_ipam_pool_cidr.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ import (
awstypes "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-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
intretry "github.com/hashicorp/terraform-provider-aws/internal/retry"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/internal/types"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
"github.com/hashicorp/terraform-provider-aws/names"
)
Expand Down Expand Up @@ -179,6 +182,107 @@ func resourceIPAMPoolCIDRDelete(ctx context.Context, d *schema.ResourceData, met
return sdkdiag.AppendFromErr(diags, err)
}

ipamPool, err := findIPAMPoolByID(ctx, conn, poolID)
if intretry.NotFound(err) {
log.Printf("[DEBUG] IPAM Pool (%s) not found, skipping IPAM Pool CIDR (%s) delete", poolID, d.Id())
return diags
} else if err != nil {
return sdkdiag.AppendErrorf(diags, "reading IPAM Pool (%s): %s", poolID, err)
}

// VPC / Subnet allocations take upto 20m to be released after resource deletion.
log.Printf("[DEBUG] Checking for allocations from CIDR %s in IPAM Pool (%s) that need to be released", cidrBlock, poolID)

// Create a region-specific client for the pool's locale
poolRegion := aws.ToString(ipamPool.Locale)
allocationCtx := conns.NewResourceContext(ctx, names.EC2ServiceID, "aws_vpc_ipam_pool_cidr", poolRegion)
allocationConn := meta.(*conns.AWSClient).EC2Client(allocationCtx)

ipamPoolAllocationsInput := &ec2.GetIpamPoolAllocationsInput{
IpamPoolId: aws.String(poolID),
Filters: []awstypes.Filter{
{
Name: aws.String("resource-type"),
Values: []string{string(awstypes.IpamPoolAllocationResourceTypeVpc), string(awstypes.IpamPoolAllocationResourceTypeSubnet)},
},
},
}
allocations, err := findIPAMPoolAllocations(allocationCtx, allocationConn, ipamPoolAllocationsInput)
if intretry.NotFound(err) {
allocations = nil
} else if err != nil {
return sdkdiag.AppendErrorf(diags, "listing IPAM Pool (%s) allocations: %s", poolID, err)
}

var allocationsToTrack []awstypes.IpamPoolAllocation
for _, allocation := range allocations {
allocationCIDR := aws.ToString(allocation.Cidr)

if !types.CIDRBlocksOverlap(cidrBlock, allocationCIDR) {
continue
}
allocationsToTrack = append(allocationsToTrack, allocation)
}

if len(allocationsToTrack) > 0 {
log.Printf("[DEBUG] Found %d VPC/Subnet allocation(s) from CIDR %s that need to be released", len(allocationsToTrack), cidrBlock)

for _, allocation := range allocationsToTrack {
resourceID := aws.ToString(allocation.ResourceId)
allocationCIDR := aws.ToString(allocation.Cidr)
resourceType := allocation.ResourceType

switch resourceType {
case awstypes.IpamPoolAllocationResourceTypeVpc:
_, err := findVPCByID(allocationCtx, allocationConn, resourceID)
if err == nil {
return sdkdiag.AppendErrorf(diags, "VPC %s (CIDR: %s) must be deleted before IPAM Pool CIDR can be deprovisioned", resourceID, allocationCIDR)
}
log.Printf("[DEBUG] VPC %s already deleted, waiting for allocation (CIDR: %s) to be released from IPAM Pool %s", resourceID, allocationCIDR, poolID)
case awstypes.IpamPoolAllocationResourceTypeSubnet:
_, err := findSubnetByID(allocationCtx, allocationConn, resourceID)
if err == nil {
return sdkdiag.AppendErrorf(diags, "subnet %s (CIDR: %s) must be deleted before IPAM Pool CIDR can be deprovisioned", resourceID, allocationCIDR)
}
log.Printf("[DEBUG] Subnet %s already deleted, waiting for allocation (CIDR: %s) to be released from IPAM Pool %s", resourceID, allocationCIDR, poolID)
}
}

log.Printf("[DEBUG] Waiting for IPAM to release %d allocation(s) from CIDR %s", len(allocationsToTrack), cidrBlock)

_, err = tfresource.RetryUntilNotFound(allocationCtx, d.Timeout(schema.TimeoutDelete), func(ctx context.Context) (any, error) {
allocations, err := findIPAMPoolAllocations(ctx, allocationConn, ipamPoolAllocationsInput)
if intretry.NotFound(err) {
log.Printf("[DEBUG] IPAM Pool (%s) deleted during wait, allocations released", poolID)
return nil, &retry.NotFoundError{}
}
if err != nil {
return nil, err
}

for _, allocation := range allocations {
allocationCIDR := aws.ToString(allocation.Cidr)

if !types.CIDRBlocksOverlap(cidrBlock, allocationCIDR) {
continue
}

return allocation, nil
}
return nil, &retry.NotFoundError{}
})

if intretry.TimedOut(err) {
return sdkdiag.AppendErrorf(diags, "timeout waiting for IPAM Pool (%s) allocations to be released after %s", poolID, d.Timeout(schema.TimeoutDelete))
}

if err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for IPAM Pool (%s) allocations to be released: %s", poolID, err)
}
} else {
log.Printf("[DEBUG] No VPC/Subnet allocations found for CIDR %s, proceeding with deprovision", cidrBlock)
}

log.Printf("[DEBUG] Deleting IPAM Pool CIDR: %s", d.Id())
input := ec2.DeprovisionIpamPoolCidrInput{
Cidr: aws.String(cidrBlock),
Expand All @@ -198,7 +302,6 @@ func resourceIPAMPoolCIDRDelete(ctx context.Context, d *schema.ResourceData, met
if _, err := waitIPAMPoolCIDRDeleted(ctx, conn, d.Get("ipam_pool_cidr_id").(string), poolID, cidrBlock, d.Timeout(schema.TimeoutDelete)); err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for IPAM Pool CIDR (%s) delete: %s", d.Id(), err)
}

return diags
}

Expand Down
Loading