diff --git a/.changelog/39600.txt b/.changelog/39600.txt new file mode 100644 index 00000000000..796358090c7 --- /dev/null +++ b/.changelog/39600.txt @@ -0,0 +1,16 @@ +```release-note:bug +resource/aws_vpc_ipam_pool: Fix `InvalidParameterCombination: The request can only contain PubliclyAdvertisable if the AddressFamily is IPv6 and PublicIpSource is byoip` errors +``` + +```release-note:bug +resource/aws_vpc_ipam_pool: Change `publicly_advertisable` to [ForceNew](https://developer.hashicorp.com/terraform/plugin/sdkv2/schemas/schema-behaviors#forcenew) +``` + +```release-note:enhancement +resource/aws_vpc_ipam: Add `enable_private_gua` argument +``` + + +```release-note:enhancement +resource/aws_vpc_ipv6_cidr_block_association: Add `ip_source` and `ipv6_address_attribute` attributes +``` \ No newline at end of file diff --git a/internal/service/ec2/ipam_.go b/internal/service/ec2/ipam_.go index 3bb1ea5aeb7..5f1afabeab4 100644 --- a/internal/service/ec2/ipam_.go +++ b/internal/service/ec2/ipam_.go @@ -67,6 +67,11 @@ func resourceIPAM() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "enable_private_gua": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, "operating_regions": { Type: schema.TypeSet, Required: true, @@ -137,6 +142,10 @@ func resourceIPAMCreate(ctx context.Context, d *schema.ResourceData, meta interf input.Description = aws.String(v.(string)) } + if v, ok := d.GetOk("enable_private_gua"); ok { + input.EnablePrivateGua = aws.Bool(v.(bool)) + } + if v, ok := d.GetOk("tier"); ok { input.Tier = awstypes.IpamTier(v.(string)) } @@ -176,6 +185,7 @@ func resourceIPAMRead(ctx context.Context, d *schema.ResourceData, meta interfac d.Set("default_resource_discovery_association_id", ipam.DefaultResourceDiscoveryAssociationId) d.Set("default_resource_discovery_id", ipam.DefaultResourceDiscoveryId) d.Set(names.AttrDescription, ipam.Description) + d.Set("enable_private_gua", ipam.EnablePrivateGua) if err := d.Set("operating_regions", flattenIPAMOperatingRegions(ipam.OperatingRegions)); err != nil { return sdkdiag.AppendErrorf(diags, "setting operating_regions: %s", err) } @@ -202,6 +212,10 @@ func resourceIPAMUpdate(ctx context.Context, d *schema.ResourceData, meta interf input.Description = aws.String(d.Get(names.AttrDescription).(string)) } + if d.HasChange("enable_private_gua") { + input.EnablePrivateGua = aws.Bool(d.Get("enable_private_gua").(bool)) + } + if d.HasChange("operating_regions") { o, n := d.GetChange("operating_regions") if o == nil { diff --git a/internal/service/ec2/ipam_pool.go b/internal/service/ec2/ipam_pool.go index 3266ea2fc83..6f674203e0a 100644 --- a/internal/service/ec2/ipam_pool.go +++ b/internal/service/ec2/ipam_pool.go @@ -131,6 +131,7 @@ func resourceIPAMPool() *schema.Resource { "publicly_advertisable": { Type: schema.TypeBool, Optional: true, + ForceNew: true, }, "source_ipam_pool_id": { Type: schema.TypeString, @@ -153,11 +154,18 @@ func resourceIPAMPoolCreate(ctx context.Context, d *schema.ResourceData, meta in var diags diag.Diagnostics conn := meta.(*conns.AWSClient).EC2Client(ctx) + scopeID := d.Get("ipam_scope_id").(string) + scope, err := findIPAMScopeByID(ctx, conn, scopeID) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading IPAM Scope (%s): %s", scopeID, err) + } + addressFamily := awstypes.AddressFamily(d.Get("address_family").(string)) input := &ec2.CreateIpamPoolInput{ AddressFamily: addressFamily, ClientToken: aws.String(id.UniqueId()), - IpamScopeId: aws.String(d.Get("ipam_scope_id").(string)), + IpamScopeId: aws.String(scopeID), TagSpecifications: getTagSpecificationsIn(ctx, awstypes.ResourceTypeIpamPool), } @@ -193,15 +201,12 @@ func resourceIPAMPoolCreate(ctx context.Context, d *schema.ResourceData, meta in input.AwsService = awstypes.IpamPoolAwsService(v.(string)) } - var publicIpSource awstypes.IpamPoolPublicIpSource if v, ok := d.GetOk("public_ip_source"); ok { - publicIpSource = awstypes.IpamPoolPublicIpSource(v.(string)) - input.PublicIpSource = publicIpSource + input.PublicIpSource = awstypes.IpamPoolPublicIpSource(v.(string)) } - // PubliclyAdvertisable must be set if if the AddressFamily is IPv6 and PublicIpSource is byoip. // The request can only contain PubliclyAdvertisable if the AddressFamily is IPv6 and PublicIpSource is byoip. - if addressFamily == awstypes.AddressFamilyIpv6 && publicIpSource != awstypes.IpamPoolPublicIpSourceAmazon { + if addressFamily == awstypes.AddressFamilyIpv6 && scope.IpamScopeType == awstypes.IpamScopeTypePublic { input.PubliclyAdvertisable = aws.Bool(d.Get("publicly_advertisable").(bool)) } diff --git a/internal/service/ec2/ipam_pool_test.go b/internal/service/ec2/ipam_pool_test.go index 3ce9531917c..ca3ff469ade 100644 --- a/internal/service/ec2/ipam_pool_test.go +++ b/internal/service/ec2/ipam_pool_test.go @@ -234,6 +234,27 @@ func TestAccIPAMPool_tags(t *testing.T) { }) } +func TestAccIPAMPool_ipv6PrivateScope(t *testing.T) { + ctx := acctest.Context(t) + var pool awstypes.IpamPool + resourceName := "aws_vpc_ipam_pool.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIPAMPoolDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIPAMPoolConfig_ipv6PrivateScope, + Check: resource.ComposeTestCheckFunc( + testAccCheckIPAMPoolExists(ctx, resourceName, &pool), + ), + }, + }, + }) +} + func testAccCheckIPAMPoolExists(ctx context.Context, n string, v *awstypes.IpamPool) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -285,6 +306,19 @@ func testAccCheckIPAMPoolDestroy(ctx context.Context) resource.TestCheckFunc { } } +func testAccCheckIPAMPoolCIDRCreate(ctx context.Context, ipampool *awstypes.IpamPool) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Client(ctx) + + _, err := conn.ProvisionIpamPoolCidr(ctx, &ec2.ProvisionIpamPoolCidrInput{ + IpamPoolId: ipampool.IpamPoolId, + Cidr: aws.String("10.0.0.0/16"), + }) + + return err + } +} + const testAccIPAMPoolConfig_base = ` data "aws_region" "current" {} @@ -345,19 +379,6 @@ resource "aws_vpc_ipam_pool" "test" { } `) -func testAccCheckIPAMPoolCIDRCreate(ctx context.Context, ipampool *awstypes.IpamPool) resource.TestCheckFunc { - return func(s *terraform.State) error { - conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Client(ctx) - - _, err := conn.ProvisionIpamPoolCidr(ctx, &ec2.ProvisionIpamPoolCidrInput{ - IpamPoolId: ipampool.IpamPoolId, - Cidr: aws.String("10.0.0.0/16"), - }) - - return err - } -} - func testAccIPAMPoolConfig_tags(tagKey1, tagValue1 string) string { return acctest.ConfigCompose(testAccIPAMPoolConfig_base, fmt.Sprintf(` resource "aws_vpc_ipam_pool" "test" { @@ -384,3 +405,15 @@ resource "aws_vpc_ipam_pool" "test" { } `, tagKey1, tagValue1, tagKey2, tagValue2)) } + +var testAccIPAMPoolConfig_ipv6PrivateScope = acctest.ConfigCompose(testAccIPAMScopeConfig_base, ` +resource "aws_vpc_ipam_scope" "test" { + ipam_id = aws_vpc_ipam.test.id +} + +resource "aws_vpc_ipam_pool" "test" { + address_family = "ipv6" + ipam_scope_id = aws_vpc_ipam_scope.test.id + locale = data.aws_region.current.name +} +`) diff --git a/internal/service/ec2/ipam_test.go b/internal/service/ec2/ipam_test.go index 655847cced9..d14b8d92a9d 100644 --- a/internal/service/ec2/ipam_test.go +++ b/internal/service/ec2/ipam_test.go @@ -39,6 +39,7 @@ func TestAccIPAM_basic(t *testing.T) { testAccCheckIPAMExists(ctx, resourceName, &ipam), resource.TestCheckResourceAttrSet(resourceName, names.AttrARN), resource.TestCheckResourceAttr(resourceName, names.AttrDescription, ""), + resource.TestCheckResourceAttr(resourceName, "enable_private_gua", acctest.CtFalse), resource.TestCheckResourceAttr(resourceName, "operating_regions.#", acctest.Ct1), resource.TestCheckResourceAttr(resourceName, "scope_count", acctest.Ct2), resource.TestMatchResourceAttr(resourceName, "private_default_scope_id", regexache.MustCompile(`^ipam-scope-[0-9a-f]+`)), @@ -262,6 +263,40 @@ func TestAccIPAM_tags(t *testing.T) { }) } +func TestAccIPAM_enablePrivateGUA(t *testing.T) { + ctx := acctest.Context(t) + var ipam awstypes.Ipam + resourceName := "aws_vpc_ipam.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIPAMDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIPAMConfig_enablePrivateGUA(true), + Check: resource.ComposeTestCheckFunc( + testAccCheckIPAMExists(ctx, resourceName, &ipam), + resource.TestCheckResourceAttr(resourceName, "enable_private_gua", acctest.CtTrue), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccIPAMConfig_enablePrivateGUA(false), + Check: resource.ComposeTestCheckFunc( + testAccCheckIPAMExists(ctx, resourceName, &ipam), + resource.TestCheckResourceAttr(resourceName, "enable_private_gua", acctest.CtFalse), + ), + }, + }, + }) +} + func testAccCheckIPAMExists(ctx context.Context, n string, v *awstypes.Ipam) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -269,10 +304,6 @@ func testAccCheckIPAMExists(ctx context.Context, n string, v *awstypes.Ipam) res return fmt.Errorf("Not found: %s", n) } - if rs.Primary.ID == "" { - return fmt.Errorf("No IPAM ID is set") - } - conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Client(ctx) output, err := tfec2.FindIPAMByID(ctx, conn, rs.Primary.ID) @@ -426,3 +457,17 @@ resource "aws_vpc_ipam" "test" { } `, tier) } + +func testAccIPAMConfig_enablePrivateGUA(enablePrivateGUA bool) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + enable_private_gua = %[1]t + + operating_regions { + region_name = data.aws_region.current.name + } +} +`, enablePrivateGUA) +} diff --git a/internal/service/ec2/vpc_ipv6_cidr_block_association.go b/internal/service/ec2/vpc_ipv6_cidr_block_association.go index 916684e69a2..5920af6bd6d 100644 --- a/internal/service/ec2/vpc_ipv6_cidr_block_association.go +++ b/internal/service/ec2/vpc_ipv6_cidr_block_association.go @@ -49,6 +49,14 @@ func resourceVPCIPv6CIDRBlockAssociation() *schema.Resource { ForceNew: true, ConflictsWith: []string{"ipv6_pool", "ipv6_ipam_pool_id", "ipv6_cidr_block", "ipv6_netmask_length"}, }, + "ip_source": { + Type: schema.TypeString, + Computed: true, + }, + "ipv6_address_attribute": { + Type: schema.TypeString, + Computed: true, + }, "ipv6_cidr_block": { Type: schema.TypeString, Optional: true, @@ -154,6 +162,8 @@ func resourceVPCIPv6CIDRBlockAssociationRead(ctx context.Context, d *schema.Reso isAmazonIPv6Pool := ipv6PoolID == amazonIPv6PoolID d.Set("assign_generated_ipv6_cidr_block", isAmazonIPv6Pool) + d.Set("ip_source", vpcIpv6CidrBlockAssociation.IpSource) + d.Set("ipv6_address_attribute", vpcIpv6CidrBlockAssociation.Ipv6AddressAttribute) d.Set("ipv6_cidr_block", vpcIpv6CidrBlockAssociation.Ipv6CidrBlock) d.Set("ipv6_pool", ipv6PoolID) d.Set(names.AttrVPCID, vpc.VpcId) diff --git a/internal/service/ec2/vpc_ipv6_cidr_block_association_test.go b/internal/service/ec2/vpc_ipv6_cidr_block_association_test.go index be53c000c7d..34722918a21 100644 --- a/internal/service/ec2/vpc_ipv6_cidr_block_association_test.go +++ b/internal/service/ec2/vpc_ipv6_cidr_block_association_test.go @@ -39,6 +39,10 @@ func TestAccVPCIPv6CIDRBlockAssociation_basic(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccCheckVPCIPv6CIDRBlockAssociationExists(ctx, resource1Name, &associationSecondary), testAccCheckVPCIPv6CIDRBlockAssociationExists(ctx, resource2Name, &associationTertiary), + resource.TestCheckResourceAttr(resource1Name, "ip_source", "amazon"), + resource.TestCheckResourceAttr(resource2Name, "ip_source", "amazon"), + resource.TestCheckResourceAttr(resource1Name, "ipv6_address_attribute", "public"), + resource.TestCheckResourceAttr(resource2Name, "ipv6_address_attribute", "public"), resource.TestCheckResourceAttr(resource1Name, "ipv6_pool", "Amazon"), resource.TestCheckResourceAttr(resource2Name, "ipv6_pool", "Amazon"), ), diff --git a/website/docs/r/vpc_ipam.html.markdown b/website/docs/r/vpc_ipam.html.markdown index 72723ed0f5a..cf87b2aa0e9 100644 --- a/website/docs/r/vpc_ipam.html.markdown +++ b/website/docs/r/vpc_ipam.html.markdown @@ -61,6 +61,7 @@ This resource supports the following arguments: * `cascade` - (Optional) Enables you to quickly delete an IPAM, private scopes, pools in private scopes, and any allocations in the pools in private scopes. * `description` - (Optional) A description for the IPAM. +* `enable_private_gua` - (Optional) Enable this option to use your own GUA ranges as private IPv6 addresses. Default: `false`. * `operating_regions` - (Required) Determines which locales can be chosen when you create pools. Locale is the Region where you want to make an IPAM pool available for allocations. You can only create pools with locales that match the operating Regions of the IPAM. You can only create VPCs from a pool whose locale matches the VPC's Region. You specify a region using the [region_name](#operating_regions) parameter. You **must** set your provider block region as an operating_region. * `tier` - (Optional) specifies the IPAM tier. Valid options include `free` and `advanced`. Default is `advanced`. * `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. diff --git a/website/docs/r/vpc_ipv6_cidr_block_association.html.markdown b/website/docs/r/vpc_ipv6_cidr_block_association.html.markdown index b4a4275de59..e696bf26615 100644 --- a/website/docs/r/vpc_ipv6_cidr_block_association.html.markdown +++ b/website/docs/r/vpc_ipv6_cidr_block_association.html.markdown @@ -47,7 +47,9 @@ This resource supports the following arguments: This resource exports the following attributes in addition to the arguments above: -* `id` - The ID of the VPC CIDR association +* `id` - The ID of the VPC CIDR association. +* `ip_source` - The source that allocated the IP address space. Values: `amazon`, `byoip`, `none`. +* `ipv6_address_attribute` - Public IPv6 addresses are those advertised on the internet from AWS. Private IP addresses are not and cannot be advertised on the internet from AWS. Values: `public`, `private`. ## Import