Skip to content

Commit

Permalink
Merge pull request #30795 from hashicorp/b-aws_vpc.ipv6_ipam_pool_id
Browse files Browse the repository at this point in the history
r/aws_vpc: IPv6 IPAM pool corrections
  • Loading branch information
ewbankkit authored Apr 18, 2023
2 parents 1ae03a7 + adf1ca2 commit d2c2ccb
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 32 deletions.
3 changes: 3 additions & 0 deletions .changelog/30795.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_vpc: Don't overwrite any configured value for `ipv6_ipam_pool_id` with _IPAM Managed_
```
3 changes: 2 additions & 1 deletion internal/service/ec2/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ func vpnConnectionType_Values() []string {
}

const (
AmazonIPv6PoolID = "Amazon"
amazonIPv6PoolID = "Amazon"
ipamManagedIPv6PoolID = "IPAM Managed"
)

const (
Expand Down
17 changes: 8 additions & 9 deletions internal/service/ec2/ipam_pool_cidr.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func ResourceIPAMPoolCIDR() *schema.Resource {
},

CustomizeDiff: customdiff.All(
ResourceIPAMPoolCIDRCustomizeDiff,
resourceIPAMPoolCIDRCustomizeDiff,
),

Schema: map[string]*schema.Schema{
Expand Down Expand Up @@ -130,12 +130,12 @@ func resourceIPAMPoolCIDRCreate(ctx context.Context, d *schema.ResourceData, met

// its possible that cidr is computed based on netmask_length
cidrBlock := aws.StringValue(output.IpamPoolCidr.Cidr)
poolCidrId := aws.StringValue(output.IpamPoolCidr.IpamPoolCidrId)
poolCidrID := aws.StringValue(output.IpamPoolCidr.IpamPoolCidrId)

ipamPoolCidr, err := WaitIPAMPoolCIDRIdCreated(ctx, conn, poolCidrId, poolID, cidrBlock, d.Timeout(schema.TimeoutDelete))
ipamPoolCidr, err := WaitIPAMPoolCIDRIdCreated(ctx, conn, poolCidrID, poolID, cidrBlock, d.Timeout(schema.TimeoutDelete))

if err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for IPAM Pool CIDR with ID (%s) create: %s", poolCidrId, err)
return sdkdiag.AppendErrorf(diags, "waiting for IPAM Pool CIDR (%s) create: %s", poolCidrID, err)
}

// This resource's ID is a concatenated id of `<cidr>_<poolid>`
Expand All @@ -152,7 +152,7 @@ func resourceIPAMPoolCIDRRead(ctx context.Context, d *schema.ResourceData, meta
cidrBlock, poolID, err := IPAMPoolCIDRParseResourceID(d.Id())

if err != nil {
return sdkdiag.AppendErrorf(diags, "reading IPAM Pool CIDR (%s): %s", d.Id(), err)
return sdkdiag.AppendFromErr(diags, err)
}

output, err := FindIPAMPoolCIDRByTwoPartKey(ctx, conn, cidrBlock, poolID)
Expand All @@ -179,10 +179,9 @@ func resourceIPAMPoolCIDRDelete(ctx context.Context, d *schema.ResourceData, met
conn := meta.(*conns.AWSClient).EC2Conn()

cidrBlock, poolID, err := IPAMPoolCIDRParseResourceID(d.Id())
poolCidrId := d.Get("ipam_pool_cidr_id").(string)

if err != nil {
return sdkdiag.AppendErrorf(diags, "deleting IPAM Pool CIDR (%s): %s", d.Id(), err)
return sdkdiag.AppendFromErr(diags, err)
}

log.Printf("[DEBUG] Deleting IPAM Pool CIDR: %s", d.Id())
Expand All @@ -200,7 +199,7 @@ func resourceIPAMPoolCIDRDelete(ctx context.Context, d *schema.ResourceData, met
return sdkdiag.AppendErrorf(diags, "deleting IPAM Pool CIDR (%s): %s", d.Id(), err)
}

if _, err := WaitIPAMPoolCIDRDeleted(ctx, conn, cidrBlock, poolID, poolCidrId, d.Timeout(schema.TimeoutDelete)); err != nil {
if _, err := WaitIPAMPoolCIDRDeleted(ctx, conn, cidrBlock, poolID, d.Get("ipam_pool_cidr_id").(string), d.Timeout(schema.TimeoutDelete)); err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for IPAM Pool CIDR (%s) delete: %s", d.Id(), err)
}

Expand Down Expand Up @@ -244,7 +243,7 @@ func expandIPAMCIDRAuthorizationContext(tfMap map[string]interface{}) *ec2.IpamC
return apiObject
}

func ResourceIPAMPoolCIDRCustomizeDiff(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
func resourceIPAMPoolCIDRCustomizeDiff(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
// cidr can be set by a value returned from IPAM or explicitly in config.
if diff.Id() != "" && diff.HasChange("cidr") {
// If netmask is set then cidr is derived from IPAM, ignore changes.
Expand Down
5 changes: 3 additions & 2 deletions internal/service/ec2/ipam_pool_cidr_allocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ func resourceIPAMPoolCIDRAllocationCreate(ctx context.Context, d *schema.Resourc
if err != nil {
return sdkdiag.AppendErrorf(diags, "creating IPAM Pool CIDR Allocation: %s", err)
}

d.SetId(IPAMPoolCIDRAllocationCreateResourceID(aws.StringValue(output.IpamPoolAllocation.IpamPoolAllocationId), ipamPoolID))

if _, err := WaitIPAMPoolCIDRAllocationCreated(ctx, conn, aws.StringValue(output.IpamPoolAllocation.IpamPoolAllocationId), ipamPoolID, d.Timeout(schema.TimeoutCreate)); err != nil {
Expand All @@ -140,7 +141,7 @@ func resourceIPAMPoolCIDRAllocationRead(ctx context.Context, d *schema.ResourceD
allocationID, poolID, err := IPAMPoolCIDRAllocationParseResourceID(d.Id())

if err != nil {
return sdkdiag.AppendErrorf(diags, "parsing IPAM Pool CIDR Allocation (%s): %s", d.Id(), err)
return sdkdiag.AppendFromErr(diags, err)
}

allocation, err := FindIPAMPoolAllocationByTwoPartKey(ctx, conn, allocationID, poolID)
Expand Down Expand Up @@ -172,7 +173,7 @@ func resourceIPAMPoolCIDRAllocationDelete(ctx context.Context, d *schema.Resourc
allocationID, poolID, err := IPAMPoolCIDRAllocationParseResourceID(d.Id())

if err != nil {
return sdkdiag.AppendErrorf(diags, "deleting IPAM Pool CIDR Allocation (%s): %s", d.Id(), err)
return sdkdiag.AppendFromErr(diags, err)
}

log.Printf("[DEBUG] Deleting IPAM Pool CIDR Allocation: %s", d.Id())
Expand Down
80 changes: 66 additions & 14 deletions internal/service/ec2/vpc_.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import (
"log"
"strconv"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn"
"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/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"
"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 @@ -221,12 +224,17 @@ func resourceVPCCreate(ctx context.Context, d *schema.ResourceData, meta interfa
input.Ipv6NetmaskLength = aws.Int64(int64(v.(int)))
}

output, err := conn.CreateVpcWithContext(ctx, input)
outputRaw, err := tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout, func() (interface{}, error) {
return conn.CreateVpcWithContext(ctx, input)
// "UnsupportedOperation: The operation AllocateIpamPoolCidr is not supported. Account 123456789012 is not monitored by IPAM ipam-07b079e3392782a55."
}, errCodeUnsupportedOperation, "is not monitored by IPAM")

if err != nil {
return sdkdiag.AppendErrorf(diags, "creating EC2 VPC: %s", err)
}

output := outputRaw.(*ec2.CreateVpcOutput)

d.SetId(aws.StringValue(output.Vpc.VpcId))

vpc, err := WaitVPCCreated(ctx, conn, d.Id())
Expand Down Expand Up @@ -355,31 +363,35 @@ func resourceVPCRead(ctx context.Context, d *schema.ResourceData, meta interface
d.Set("default_security_group_id", v.GroupId)
}

d.Set("assign_generated_ipv6_cidr_block", nil)
d.Set("ipv6_cidr_block", nil)
d.Set("ipv6_cidr_block_network_border_group", nil)
d.Set("ipv6_ipam_pool_id", nil)
d.Set("ipv6_netmask_length", nil)

ipv6CIDRBlockAssociation := defaultIPv6CIDRBlockAssociation(vpc, d.Get("ipv6_association_id").(string))

if ipv6CIDRBlockAssociation == nil {
if ipv6CIDRBlockAssociation := defaultIPv6CIDRBlockAssociation(vpc, d.Get("ipv6_association_id").(string)); ipv6CIDRBlockAssociation == nil {
d.Set("assign_generated_ipv6_cidr_block", nil)
d.Set("ipv6_association_id", nil)
d.Set("ipv6_cidr_block", nil)
d.Set("ipv6_cidr_block_network_border_group", nil)
d.Set("ipv6_ipam_pool_id", nil)
d.Set("ipv6_netmask_length", nil)
} else {
cidrBlock := aws.StringValue(ipv6CIDRBlockAssociation.Ipv6CidrBlock)
ipv6PoolID := aws.StringValue(ipv6CIDRBlockAssociation.Ipv6Pool)
isAmazonIPv6Pool := ipv6PoolID == AmazonIPv6PoolID
isAmazonIPv6Pool := ipv6PoolID == amazonIPv6PoolID
d.Set("assign_generated_ipv6_cidr_block", isAmazonIPv6Pool)
d.Set("ipv6_association_id", ipv6CIDRBlockAssociation.AssociationId)
d.Set("ipv6_cidr_block", cidrBlock)
d.Set("ipv6_cidr_block_network_border_group", ipv6CIDRBlockAssociation.NetworkBorderGroup)
if !isAmazonIPv6Pool {
d.Set("ipv6_ipam_pool_id", ipv6PoolID)
if isAmazonIPv6Pool {
d.Set("ipv6_ipam_pool_id", nil)
} else {
if ipv6PoolID == ipamManagedIPv6PoolID {
d.Set("ipv6_ipam_pool_id", d.Get("ipv6_ipam_pool_id"))
} else {
d.Set("ipv6_ipam_pool_id", ipv6PoolID)
}
}
d.Set("ipv6_netmask_length", nil)
if ipv6PoolID != "" && !isAmazonIPv6Pool {
parts := strings.Split(cidrBlock, "/")
if len(parts) == 2 {
if v, err := strconv.Atoi(parts[1]); err != nil {
if v, err := strconv.Atoi(parts[1]); err == nil {
d.Set("ipv6_netmask_length", v)
} else {
log.Printf("[WARN] Unable to parse CIDR (%s) netmask length: %s", cidrBlock, err)
Expand Down Expand Up @@ -499,6 +511,24 @@ func resourceVPCDelete(ctx context.Context, d *schema.ResourceData, meta interfa
return sdkdiag.AppendErrorf(diags, "waiting for EC2 VPC (%s) delete: %s", d.Id(), err)
}

// If the VPC's CIDR block was allocated from an IPAM pool, wait for the allocation to disappear.
ipamPoolID := d.Get("ipv4_ipam_pool_id").(string)
if ipamPoolID == "" {
ipamPoolID = d.Get("ipv6_ipam_pool_id").(string)
}
if ipamPoolID != "" && ipamPoolID != amazonIPv6PoolID {
const (
timeout = 20 * time.Minute // IPAM eventual consistency
)
_, err := tfresource.RetryUntilNotFound(ctx, timeout, func() (interface{}, error) {
return findIPAMPoolAllocationsForVPC(ctx, conn, ipamPoolID, d.Id())
})

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

return diags
}

Expand Down Expand Up @@ -785,3 +815,25 @@ func modifyVPCTenancy(ctx context.Context, conn *ec2.EC2, vpcID string, v string

return nil
}

func findIPAMPoolAllocationsForVPC(ctx context.Context, conn *ec2.EC2, poolID, vpcID string) ([]*ec2.IpamPoolAllocation, error) {
input := &ec2.GetIpamPoolAllocationsInput{
IpamPoolId: aws.String(poolID),
}

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

if err != nil {
return nil, err
}

output = slices.Filter(output, func(v *ec2.IpamPoolAllocation) bool {
return aws.StringValue(v.ResourceType) == ec2.IpamPoolAllocationResourceTypeVpc && aws.StringValue(v.ResourceId) == vpcID
})

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

return output, nil
}
2 changes: 1 addition & 1 deletion internal/service/ec2/vpc_default_vpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func resourceDefaultVPCCreate(ctx context.Context, d *schema.ResourceData, meta
associationID = aws.StringValue(v.AssociationId)
oldIPv6CIDRBlock = aws.StringValue(v.Ipv6CidrBlock)
oldIPv6CIDRBlockNetworkBorderGroup = aws.StringValue(v.NetworkBorderGroup)
if ipv6PoolID := aws.StringValue(v.Ipv6Pool); ipv6PoolID == AmazonIPv6PoolID {
if ipv6PoolID := aws.StringValue(v.Ipv6Pool); ipv6PoolID == amazonIPv6PoolID {
oldAssignGeneratedIPv6CIDRBlock = true
} else {
oldIPv6PoolID = ipv6PoolID
Expand Down
4 changes: 2 additions & 2 deletions internal/service/ec2/vpc_ipv4_cidr_block_association_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ resource "aws_vpc_ipv4_cidr_block_association" "tertiary_cidr" {
}

func testAccVPCIPv4CIDRBlockAssociationConfig_ipam(rName string, netmaskLength int) string {
return acctest.ConfigCompose(testAccIPAMIPv4Config_base(rName), fmt.Sprintf(`
return acctest.ConfigCompose(testAccVPCConfig_baseIPAMIPv4(rName), fmt.Sprintf(`
resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
Expand All @@ -246,7 +246,7 @@ resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" {
}

func testAccVPCIPv4CIDRBlockAssociationConfig_ipamExplicit(rName, cidr string) string {
return acctest.ConfigCompose(testAccIPAMIPv4Config_base(rName), fmt.Sprintf(`
return acctest.ConfigCompose(testAccVPCConfig_baseIPAMIPv4(rName), fmt.Sprintf(`
resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
Expand Down
91 changes: 88 additions & 3 deletions internal/service/ec2/vpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,40 @@ func TestAccVPC_IPAMIPv4BasicExplicitCIDR(t *testing.T) {
})
}

func TestAccVPC_IPAMIPv6(t *testing.T) {
ctx := acctest.Context(t)
if testing.Short() {
t.Skip("skipping long-running test in short mode")
}

var vpc ec2.Vpc
resourceName := "aws_vpc.test"
ipamPoolResourceName := "aws_vpc_ipam_pool.test"
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckVPCDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccVPCConfig_ipamIPv6(rName, 28),
Check: resource.ComposeAggregateTestCheckFunc(
acctest.CheckVPCExists(ctx, resourceName, &vpc),
resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "false"),
resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"),
resource.TestCheckResourceAttrSet(resourceName, "ipv6_association_id"),
resource.TestMatchResourceAttr(resourceName, "ipv6_cidr_block", regexp.MustCompile(`/56$`)),
resource.TestCheckResourceAttrSet(resourceName, "ipv6_cidr_block_network_border_group"),
resource.TestCheckResourceAttrPair(resourceName, "ipv6_ipam_pool_id", ipamPoolResourceName, "id"),
resource.TestCheckResourceAttr(resourceName, "ipv6_netmask_length", "56"),
),
},
},
})
}

func testAccCheckVPCDestroy(ctx context.Context) resource.TestCheckFunc {
return func(s *terraform.State) error {
conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn()
Expand Down Expand Up @@ -1172,7 +1206,7 @@ resource "aws_vpc" "test" {
`, rName)
}

func testAccIPAMIPv4Config_base(rName string) string {
func testAccVPCConfig_baseIPAMIPv4(rName string) string {
return fmt.Sprintf(`
data "aws_region" "current" {}
Expand Down Expand Up @@ -1204,7 +1238,7 @@ resource "aws_vpc_ipam_pool_cidr" "test" {
}

func testAccVPCConfig_ipamIPv4(rName string, netmaskLength int) string {
return acctest.ConfigCompose(testAccIPAMIPv4Config_base(rName), fmt.Sprintf(`
return acctest.ConfigCompose(testAccVPCConfig_baseIPAMIPv4(rName), fmt.Sprintf(`
resource "aws_vpc" "test" {
ipv4_ipam_pool_id = aws_vpc_ipam_pool.test.id
ipv4_netmask_length = %[2]d
Expand All @@ -1219,7 +1253,7 @@ resource "aws_vpc" "test" {
}

func testAccVPCConfig_ipamIPv4ExplicitCIDR(rName, cidr string) string {
return acctest.ConfigCompose(testAccIPAMIPv4Config_base(rName), fmt.Sprintf(`
return acctest.ConfigCompose(testAccVPCConfig_baseIPAMIPv4(rName), fmt.Sprintf(`
resource "aws_vpc" "test" {
ipv4_ipam_pool_id = aws_vpc_ipam_pool.test.id
cidr_block = %[2]q
Expand All @@ -1232,3 +1266,54 @@ resource "aws_vpc" "test" {
}
`, rName, cidr))
}

func testAccVPCConfig_baseIPAMIPv6(rName string) string {
return fmt.Sprintf(`
data "aws_region" "current" {}
resource "aws_vpc_ipam" "test" {
operating_regions {
region_name = data.aws_region.current.name
}
tags = {
Name = %[1]q
}
}
resource "aws_vpc_ipam_pool" "test" {
address_family = "ipv6"
ipam_scope_id = aws_vpc_ipam.test.public_default_scope_id
locale = data.aws_region.current.name
aws_service = "ec2"
public_ip_source = "amazon"
allocation_max_netmask_length = 128
tags = {
Name = %[1]q
}
}
resource "aws_vpc_ipam_pool_cidr" "test" {
ipam_pool_id = aws_vpc_ipam_pool.test.id
netmask_length = 52
}
`, rName)
}

func testAccVPCConfig_ipamIPv6(rName string, netmaskLength int) string {
return acctest.ConfigCompose(testAccVPCConfig_baseIPAMIPv6(rName), fmt.Sprintf(`
resource "aws_vpc" "test" {
cidr_block = "10.1.0.0/16"
ipv6_ipam_pool_id = aws_vpc_ipam_pool.test.id
ipv6_netmask_length = 56
tags = {
Name = %[1]q
}
depends_on = [aws_vpc_ipam_pool_cidr.test]
}
`, rName, netmaskLength))
}

0 comments on commit d2c2ccb

Please sign in to comment.