diff --git a/.changelog/21998.txt b/.changelog/21998.txt new file mode 100644 index 00000000000..cb20c7bd196 --- /dev/null +++ b/.changelog/21998.txt @@ -0,0 +1,27 @@ +```release-note:new-resource +aws_vpc_ipam +``` +```release-note:new-resource +aws_vpc_ipam_pool +``` +```release-note:new-resource +aws_vpc_ipam_scope +``` +```release-note:new-resource +aws_vpc_ipam_pool_cidr +``` +```release-note:new-resource +aws_vpc_ipam_pool_cidr_allocation +``` +```release-note:new-resource +vpc_ipv6_cidr_block_association +``` +```release-note:new-data-source +aws_vpc_pool_data_source +``` +```release-note:enhancement +resource/aws_vpc: `cidr_block` value can now either be set explicitly or computed computed via AWS IPAM. When set explicitly, the request proceeds as before. To be computed, config uses IPAM to generate by passing `ipv{4,6}_ipam_pool_id` if the `aws_vpc_ipam_pool` has `allocation_default_netmask_length` set or by specifying both `ipv{4,6}_ipam_pool_id` & `ipv{4,6}_netmask_length` which the vpc backend calls will use to request a cidr that matches the netmask length from the specified ipam pool. +``` +```release-note:enhancement +resource/vpc_ipv4_cidr_block_association: `cidr_block` value can now either be set explicitly or computed via AWS IPAM. When set explicitly, the request proceeds as before. To be computed, config uses IPAM to generate by passing `ipv{4,6}_ipam_pool_id` if the `aws_vpc_ipam_pool` has `allocation_default_netmask_length` set or by specifying both `ipv{4,6}_ipam_pool_id` & `ipv{4,6}_netmask_length` which the vpc backend calls will use to request a cidr that matches the netmask length from the specified ipam pool. +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 30bd3d76078..3a749bc6768 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -472,6 +472,7 @@ func Provider() *schema.Provider { "aws_vpc_dhcp_options": ec2.DataSourceVPCDHCPOptions(), "aws_vpc_endpoint_service": ec2.DataSourceVPCEndpointService(), "aws_vpc_endpoint": ec2.DataSourceVPCEndpoint(), + "aws_vpc_ipam_pool": ec2.DataSourceVPCIpamPool(), "aws_vpc_peering_connection": ec2.DataSourceVPCPeeringConnection(), "aws_vpc_peering_connections": ec2.DataSourceVPCPeeringConnections(), "aws_vpc": ec2.DataSourceVPC(), @@ -1098,7 +1099,13 @@ func Provider() *schema.Provider { "aws_vpc_endpoint_service": ec2.ResourceVPCEndpointService(), "aws_vpc_endpoint_service_allowed_principal": ec2.ResourceVPCEndpointServiceAllowedPrincipal(), "aws_vpc_endpoint_subnet_association": ec2.ResourceVPCEndpointSubnetAssociation(), + "aws_vpc_ipam": ec2.ResourceVPCIpam(), + "aws_vpc_ipam_pool": ec2.ResourceVPCIpamPool(), + "aws_vpc_ipam_pool_cidr_allocation": ec2.ResourceVPCIpamPoolCidrAllocation(), + "aws_vpc_ipam_pool_cidr": ec2.ResourceVPCIpamPoolCidr(), + "aws_vpc_ipam_scope": ec2.ResourceVPCIpamScope(), "aws_vpc_ipv4_cidr_block_association": ec2.ResourceVPCIPv4CIDRBlockAssociation(), + "aws_vpc_ipv6_cidr_block_association": ec2.ResourceVPCIPv6CIDRBlockAssociation(), "aws_vpc_peering_connection": ec2.ResourceVPCPeeringConnection(), "aws_vpc_peering_connection_accepter": ec2.ResourceVPCPeeringConnectionAccepter(), "aws_vpc_peering_connection_options": ec2.ResourceVPCPeeringConnectionOptions(), diff --git a/internal/service/ec2/vpc.go b/internal/service/ec2/vpc.go index 5e5a09ce431..2d3c104f5f1 100644 --- a/internal/service/ec2/vpc.go +++ b/internal/service/ec2/vpc.go @@ -21,6 +21,13 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/verify" ) +const ( + VPCCIDRMaxIPv4 = 28 + VPCCIDRMinIPv4 = 16 + VPCCIDRMaxIPv6 = 56 +) + +// acceptance tests for byoip related tests are in vpc_byoip_test.go func ResourceVPC() *schema.Resource { //lintignore:R011 return &schema.Resource{ @@ -35,104 +42,134 @@ func ResourceVPC() *schema.Resource { CustomizeDiff: customdiff.All( resourceVPCCustomizeDiff, verify.SetTagsDiff, + func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + // cidr_block can be set by a value returned from IPAM or explicitly in config + if diff.Id() != "" && diff.HasChange("cidr_block") { + // if netmask is set then cidr_block is derived from ipam, ignore changes + if diff.Get("ipv4_netmask_length") != 0 { + return diff.Clear("cidr_block") + } + return diff.ForceNew("cidr_block") + } + return nil + }, ), SchemaVersion: 1, MigrateState: VPCMigrateState, Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "assign_generated_ipv6_cidr_block": { + Type: schema.TypeBool, + Optional: true, + ConflictsWith: []string{"ipv6_ipam_pool_id"}, + }, "cidr_block": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.IsCIDRNetwork(16, 28), + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.IsCIDRNetwork(VPCCIDRMinIPv4, VPCCIDRMaxIPv4), + ConflictsWith: []string{"ipv4_netmask_length"}, }, - - "instance_tenancy": { - Type: schema.TypeString, - Optional: true, - Default: ec2.TenancyDefault, - ValidateFunc: validation.StringInSlice([]string{ec2.TenancyDefault, ec2.TenancyDedicated}, false), + "default_network_acl_id": { + Type: schema.TypeString, + Computed: true, + }, + "dhcp_options_id": { + Type: schema.TypeString, + Computed: true, + }, + "default_security_group_id": { + Type: schema.TypeString, + Computed: true, + }, + "default_route_table_id": { + Type: schema.TypeString, + Computed: true, }, - "enable_dns_hostnames": { Type: schema.TypeBool, Optional: true, Computed: true, }, - "enable_dns_support": { Type: schema.TypeBool, Optional: true, Default: true, }, - "enable_classiclink": { Type: schema.TypeBool, Optional: true, Computed: true, }, - "enable_classiclink_dns_support": { Type: schema.TypeBool, Optional: true, Computed: true, }, - - "assign_generated_ipv6_cidr_block": { - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - - "main_route_table_id": { - Type: schema.TypeString, - Computed: true, - }, - - "default_network_acl_id": { - Type: schema.TypeString, - Computed: true, - }, - - "dhcp_options_id": { - Type: schema.TypeString, - Computed: true, + "instance_tenancy": { + Type: schema.TypeString, + Optional: true, + Default: ec2.TenancyDefault, + ValidateFunc: validation.StringInSlice([]string{ec2.TenancyDefault, ec2.TenancyDedicated}, false), }, - - "default_security_group_id": { + "ipv4_ipam_pool_id": { Type: schema.TypeString, - Computed: true, + Optional: true, + ForceNew: true, }, - - "default_route_table_id": { - Type: schema.TypeString, - Computed: true, + "ipv4_netmask_length": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntBetween(VPCCIDRMinIPv4, VPCCIDRMaxIPv4), + ConflictsWith: []string{"cidr_block"}, + RequiredWith: []string{"ipv4_ipam_pool_id"}, }, - "ipv6_association_id": { Type: schema.TypeString, Computed: true, }, - "ipv6_cidr_block": { - Type: schema.TypeString, - Computed: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"ipv6_netmask_length", "assign_generated_ipv6_cidr_block"}, + RequiredWith: []string{"ipv6_ipam_pool_id"}, + ValidateFunc: validation.Any( + validation.StringIsEmpty, + validation.All( + verify.ValidIPv6CIDRNetworkAddress, + validation.IsCIDRNetwork(VPCCIDRMaxIPv6, VPCCIDRMaxIPv6)), + ), }, - - "arn": { + "ipv6_ipam_pool_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"assign_generated_ipv6_cidr_block"}, + }, + "ipv6_netmask_length": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntInSlice([]int{VPCCIDRMaxIPv6}), + ConflictsWith: []string{"ipv6_cidr_block"}, + RequiredWith: []string{"ipv6_ipam_pool_id"}, + }, + "main_route_table_id": { Type: schema.TypeString, Computed: true, }, - - "tags": tftags.TagsSchema(), - - "tags_all": tftags.TagsSchemaComputed(), - "owner_id": { Type: schema.TypeString, Computed: true, }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), }, } } @@ -144,12 +181,35 @@ func resourceVPCCreate(d *schema.ResourceData, meta interface{}) error { // Create the VPC createOpts := &ec2.CreateVpcInput{ - CidrBlock: aws.String(d.Get("cidr_block").(string)), InstanceTenancy: aws.String(d.Get("instance_tenancy").(string)), AmazonProvidedIpv6CidrBlock: aws.Bool(d.Get("assign_generated_ipv6_cidr_block").(bool)), TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeVpc), } + if v, ok := d.GetOk("cidr_block"); ok { + createOpts.CidrBlock = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ipv4_ipam_pool_id"); ok { + createOpts.Ipv4IpamPoolId = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ipv4_netmask_length"); ok { + createOpts.Ipv4NetmaskLength = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("ipv6_ipam_pool_id"); ok { + createOpts.Ipv6IpamPoolId = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ipv6_cidr_block"); ok { + createOpts.Ipv6CidrBlock = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ipv6_netmask_length"); ok { + createOpts.Ipv6NetmaskLength = aws.Int64(int64(v.(int))) + } + log.Printf("[DEBUG] VPC create config: %#v", *createOpts) vpcResp, err := conn.CreateVpc(createOpts) if err != nil { @@ -329,13 +389,15 @@ func resourceVPCRead(d *schema.ResourceData, meta interface{}) error { d.Set("owner_id", vpc.OwnerId) // Make sure those values are set, if an IPv6 block exists it'll be set in the loop - d.Set("assign_generated_ipv6_cidr_block", false) d.Set("ipv6_association_id", "") d.Set("ipv6_cidr_block", "") - + // assign_generated_ipv6_cidr_block is not returned by the API + // leave unassigned if not referenced + if v := d.Get("assign_generated_ipv6_cidr_block"); v != "" { + d.Set("assign_generated_ipv6_cidr_block", aws.Bool(v.(bool))) + } for _, a := range vpc.Ipv6CidrBlockAssociationSet { if aws.StringValue(a.Ipv6CidrBlockState.State) == ec2.VpcCidrBlockStateCodeAssociated { //we can only ever have 1 IPv6 block associated at once - d.Set("assign_generated_ipv6_cidr_block", true) d.Set("ipv6_association_id", a.AssociationId) d.Set("ipv6_cidr_block", a.Ipv6CidrBlock) } @@ -557,6 +619,39 @@ func resourceVPCUpdate(d *schema.ResourceData, meta interface{}) error { } } + if d.HasChanges("ipv6_cidr_block", "ipv6_ipam_pool_id") { + log.Printf("[INFO] Modifying ipam IPv6 CIDR") + + // if assoc id exists it needs to be disassociated + if v, ok := d.GetOk("ipv6_association_id"); ok { + if err := ipv6DisassociateCidrBlock(conn, d.Id(), v.(string)); err != nil { + return err + } + } + if v := d.Get("ipv6_ipam_pool_id"); v != "" { + modifyOpts := &ec2.AssociateVpcCidrBlockInput{ + VpcId: &vpcid, + Ipv6IpamPoolId: aws.String(v.(string)), + } + + if v := d.Get("ipv6_netmask_length"); v != 0 { + modifyOpts.Ipv6NetmaskLength = aws.Int64(int64(v.(int))) + } + + if v := d.Get("ipv6_cidr_block"); v != "" { + modifyOpts.Ipv6CidrBlock = aws.String(v.(string)) + } + + resp, err := conn.AssociateVpcCidrBlock(modifyOpts) + if err != nil { + return err + } + if err := waitForEc2VpcIpv6CidrBlockAssociationCreate(conn, d.Id(), aws.StringValue(resp.Ipv6CidrBlockAssociation.AssociationId)); err != nil { + return fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR to become associated: %w", d.Id(), err) + } + } + } + if d.HasChange("instance_tenancy") { modifyOpts := &ec2.ModifyVpcTenancyInput{ VpcId: aws.String(vpcid), @@ -616,6 +711,22 @@ func resourceVPCDelete(d *schema.ResourceData, meta interface{}) error { return nil } +func ipv6DisassociateCidrBlock(conn *ec2.EC2, id, allocationId string) error { + log.Printf("[INFO] Disassociating IPv6 CIDR association id: %s", allocationId) + modifyOpts := &ec2.DisassociateVpcCidrBlockInput{ + AssociationId: aws.String(allocationId), + } + if _, err := conn.DisassociateVpcCidrBlock(modifyOpts); err != nil { + return err + } + log.Printf("[DEBUG] Waiting for EC2 VPC (%s) IPv6 CIDR to become disassociated", id) + if err := waitForEc2VpcIpv6CidrBlockAssociationDelete(conn, id, allocationId); err != nil { + return fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR to become disassociated: %w", id, err) + } + + return nil +} + func resourceVPCCustomizeDiff(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { if diff.HasChange("assign_generated_ipv6_cidr_block") { if err := diff.SetNewComputed("ipv6_association_id"); err != nil { @@ -842,7 +953,7 @@ func waitForEc2VpcIpv6CidrBlockAssociationCreate(conn *ec2.EC2, vpcID, associati }, Target: []string{ec2.VpcCidrBlockStateCodeAssociated}, Refresh: Ipv6CidrStateRefreshFunc(conn, vpcID, associationID), - Timeout: 1 * time.Minute, + Timeout: 10 * time.Minute, } _, err := stateConf.WaitForState() @@ -857,7 +968,7 @@ func waitForEc2VpcIpv6CidrBlockAssociationDelete(conn *ec2.EC2, vpcID, associati }, Target: []string{ec2.VpcCidrBlockStateCodeDisassociated}, Refresh: Ipv6CidrStateRefreshFunc(conn, vpcID, associationID), - Timeout: 1 * time.Minute, + Timeout: 5 * time.Minute, NotFoundChecks: 1, } _, err := stateConf.WaitForState() diff --git a/internal/service/ec2/vpc_byoip_test.go b/internal/service/ec2/vpc_byoip_test.go new file mode 100644 index 00000000000..57a7d046029 --- /dev/null +++ b/internal/service/ec2/vpc_byoip_test.go @@ -0,0 +1,548 @@ +package ec2_test + +import ( + "fmt" + "os" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" +) + +// Due to the nature of byoip cidrs, we have each possible test represented as a single test with +// multiple steps in order to share the dependencies + +// IPAM IPv6 BYOIP Tests +func TestAccVPCIpam_ByoipIPv6(t *testing.T) { + if os.Getenv("IPAM_BYOIP_IPV6_MESSAGE") == "" || os.Getenv("IPAM_BYOIP_IPV6_SIGNATURE") == "" || os.Getenv("IPAM_BYOIP_IPV6_PROVISIONED_CIDR") == "" { + t.Skip("Environment variable IPAM_BYOIP_IPV6_MESSAGE, IPAM_BYOIP_IPV6_SIGNATURE, or IPAM_BYOIP_IPV6_PROVISIONED_CIDR is not set") + } + + var m string + var s string + var p string + var ipv6CidrVPC string + var ipv6CidrAssoc string + + // test passing an explicit CIDR to aws_vpc + if os.Getenv("IPAM_BYOIP_IPV6_EXPLICIT_CIDR_VPC") != "" { + ipv6CidrVPC = os.Getenv("IPAM_BYOIP_IPV6_EXPLICIT_CIDR_VPC") + } + + // test passing an explicit CIDR to aws_vpc_ipv6_cidr_block_association + if os.Getenv("IPAM_BYOIP_IPV6_EXPLICIT_CIDR_ASSOCIATE") != "" { + ipv6CidrAssoc = os.Getenv("IPAM_BYOIP_IPV6_EXPLICIT_CIDR_ASSOCIATE") + } + + m = os.Getenv("IPAM_BYOIP_IPV6_MESSAGE") + s = os.Getenv("IPAM_BYOIP_IPV6_SIGNATURE") + p = os.Getenv("IPAM_BYOIP_IPV6_PROVISIONED_CIDR") + + resourceName := "aws_vpc.test" + assocName := "aws_vpc_ipv6_cidr_block_association.test" + var vpc ec2.Vpc + var associationIPv6 ec2.VpcIpv6CidrBlockAssociation + netmaskLength := 56 + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIPv6CIDRBlockAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: ipv4VPCIpamByoipIPv6DefaultNetmask(p, m, s), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "ec2", regexp.MustCompile(`vpc/vpc-.+`)), + resource.TestCheckNoResourceAttr(resourceName, "ipv6_netmask_length"), + resource.TestMatchResourceAttr(resourceName, "ipv6_association_id", regexp.MustCompile(`^vpc-cidr-assoc-.+`)), + resource.TestMatchResourceAttr(resourceName, "ipv6_cidr_block", regexp.MustCompile(`/56$`)), + ), + }, + // disassociate ipv6 + { + Config: testAccVPCIpamIPv6ByoipCIDRBase(p, m, s), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "ipv6_association_id", "")), + }, + { + Config: testAccVPCIpamIPv6ByoipExplicitNetmask(p, m, s, netmaskLength), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "ec2", regexp.MustCompile(`vpc/vpc-.+`)), + resource.TestCheckResourceAttr(resourceName, "ipv6_netmask_length", strconv.Itoa(netmaskLength)), + resource.TestMatchResourceAttr(resourceName, "ipv6_association_id", regexp.MustCompile(`^vpc-cidr-assoc-.+`)), + resource.TestMatchResourceAttr(resourceName, "ipv6_cidr_block", regexp.MustCompile(`/56$`)), + ), + }, + // // disassociate ipv6 + { + Config: testAccVPCIpamIPv6ByoipCIDRBase(p, m, s), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "ipv6_association_id", "")), + }, + { + Config: testAccVPCIpamIPv6ByoipExplicitCIDR(p, m, s, ipv6CidrVPC), + SkipFunc: testAccVPCIpamIPv6ByoipSkipExplicitCidr(t, ipv6CidrVPC), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "ec2", regexp.MustCompile(`vpc/vpc-.+`)), + resource.TestMatchResourceAttr(resourceName, "ipv6_association_id", regexp.MustCompile(`^vpc-cidr-assoc-.+`)), + resource.TestCheckResourceAttr(resourceName, "ipv6_cidr_block", ipv6CidrVPC), + ), + }, + // disassociate ipv6 + { + Config: testAccVPCIpamIPv6ByoipCIDRBase(p, m, s), + SkipFunc: testAccVPCIpamIPv6ByoipSkipExplicitCidr(t, ipv6CidrVPC), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "ipv6_association_id", "")), + }, + // aws_vpc_ipv6_cidr_block_association + { + Config: testAccVPCIpamByoipIPv6CIDRBlockAssociationIpamDefaultNetmask(p, m, s), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + testAccCheckVPCIPv6CIDRBlockAssociationExists(assocName, &associationIPv6), + testAccCheckVPCAssociationIPv6CIDRPrefix(&associationIPv6, strconv.Itoa(netmaskLength)), + ), + }, + // disassociate ipv6 + { + Config: testAccVPCIpamIPv6ByoipCIDRBase(p, m, s), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc)), + // vpc will still have association id because its based on the aws_vpc_ipv6_cidr_block_association resource + }, + { + Config: testAccVPCIpamIPv6ByoipCIDRBase(p, m, s), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "ipv6_association_id", "")), + }, + { + Config: testAccVPCIpamByoipIPv6CIDRBlockAssociationIpamExplicitNetmask(p, m, s, netmaskLength), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCIPv6CIDRBlockAssociationExists(assocName, &associationIPv6), + testAccCheckVPCAssociationIPv6CIDRPrefix(&associationIPv6, strconv.Itoa(netmaskLength)), + ), + }, + // disassociate ipv6 + { + Config: testAccVPCIpamIPv6ByoipCIDRBase(p, m, s), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc)), + // vpc will still have association id because its based on the aws_vpc_ipv6_cidr_block_association resource + }, + { + Config: testAccVPCIpamIPv6ByoipCIDRBase(p, m, s), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "ipv6_association_id", "")), + }, + { + Config: testAccVPCIpamByoipIPv6CIDRBlockAssociationIpamExplicitCIDR(p, m, s, ipv6CidrAssoc), + SkipFunc: testAccVPCIpamIPv6ByoipSkipExplicitCidr(t, ipv6CidrAssoc), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCIPv6CIDRBlockAssociationExists(assocName, &associationIPv6), + resource.TestCheckResourceAttr(assocName, "ipv6_cidr_block", ipv6CidrAssoc), + ), + }, + }, + }) +} + +func testAccCheckVPCIPv6CIDRBlockAssociationDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpc_ipv6_cidr_block_association" { + continue + } + + // Try to find the VPC + DescribeVpcOpts := &ec2.DescribeVpcsInput{ + VpcIds: []*string{aws.String(rs.Primary.Attributes["vpc_id"])}, + } + resp, err := conn.DescribeVpcs(DescribeVpcOpts) + if err == nil { + vpc := resp.Vpcs[0] + + for _, ipv6Association := range vpc.Ipv6CidrBlockAssociationSet { + if *ipv6Association.AssociationId == rs.Primary.ID { + return fmt.Errorf("VPC CIDR block association still exists") + } + } + + return nil + } + + // Verify the error is what we want + ec2err, ok := err.(awserr.Error) + if !ok { + return err + } + if ec2err.Code() != "InvalidVpcID.NotFound" { + return err + } + } + + return nil +} + +func testAccCheckVPCIPv6CIDRBlockAssociationExists(n string, association *ec2.VpcIpv6CidrBlockAssociation) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No VPC ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + DescribeVpcOpts := &ec2.DescribeVpcsInput{ + VpcIds: []*string{aws.String(rs.Primary.Attributes["vpc_id"])}, + } + resp, err := conn.DescribeVpcs(DescribeVpcOpts) + if err != nil { + return err + } + if len(resp.Vpcs) == 0 { + return fmt.Errorf("VPC not found") + } + + vpc := resp.Vpcs[0] + found := false + for _, ipv6CidrAssociation := range vpc.Ipv6CidrBlockAssociationSet { + if *ipv6CidrAssociation.AssociationId == rs.Primary.ID { + *association = *ipv6CidrAssociation + found = true + } + } + + if !found { + return fmt.Errorf("VPC CIDR block association not found") + } + + return nil + } +} + +func testAccCheckVPCAssociationIPv6CIDRPrefix(association *ec2.VpcIpv6CidrBlockAssociation, expected string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if strings.Split(aws.StringValue(association.Ipv6CidrBlock), "/")[1] != expected { + return fmt.Errorf("Bad cidr prefix: %s", aws.StringValue(association.Ipv6CidrBlock)) + } + + return nil + } +} + +func testAccVPCIpamIPv6ByoipSkipExplicitCidr(t *testing.T, ipv6CidrVPC string) func() (bool, error) { + return func() (bool, error) { + if ipv6CidrVPC != "" { + return false, nil + } + t.Log("Skipping step: Environment variable IPAM_BYOIP_IPV6_EXPLICIT_CIDR_VPC or IPAM_BYOIP_IPV6_EXPLICIT_CIDR_ASSOCIATE must be set.") + return true, nil + } +} + +func testAccVPCIpamIPv6ByoipCIDRBase(cidr, msg, signature string) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +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 + publicly_advertisable = false + aws_service = "ec2" + allocation_default_netmask_length = 56 +} + +resource "aws_vpc_ipam_pool_cidr" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = %[1]q + + cidr_authorization_context { + message = %[2]q + signature = %[3]q + } +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + `, cidr, msg, signature) +} + +func ipv4VPCIpamByoipIPv6DefaultNetmask(cidr, msg, signature string) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +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 + publicly_advertisable = false + aws_service = "ec2" + allocation_default_netmask_length = 56 +} + +resource "aws_vpc_ipam_pool_cidr" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = %[1]q + + cidr_authorization_context { + message = %[2]q + signature = %[3]q + } +} + +resource "aws_vpc" "test" { + ipv6_ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr_block = "10.0.0.0/16" + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} + `, cidr, msg, signature) +} + +func testAccVPCIpamIPv6ByoipExplicitNetmask(cidr, msg, signature string, netmask int) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +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 + publicly_advertisable = false + aws_service = "ec2" + allocation_default_netmask_length = 56 +} + +resource "aws_vpc_ipam_pool_cidr" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = %[1]q + + cidr_authorization_context { + message = %[2]q + signature = %[3]q + } +} + +resource "aws_vpc" "test" { + ipv6_ipam_pool_id = aws_vpc_ipam_pool.test.id + ipv6_netmask_length = %[4]d + cidr_block = "10.0.0.0/16" + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} + `, cidr, msg, signature, netmask) +} + +func testAccVPCIpamIPv6ByoipExplicitCIDR(cidr, msg, signature, vpcCidr string) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +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 + publicly_advertisable = false + aws_service = "ec2" + allocation_default_netmask_length = 56 +} + +resource "aws_vpc_ipam_pool_cidr" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = %[1]q + + cidr_authorization_context { + message = %[2]q + signature = %[3]q + } +} + +resource "aws_vpc" "test" { + ipv6_ipam_pool_id = aws_vpc_ipam_pool.test.id + ipv6_cidr_block = %[4]q + cidr_block = "10.0.0.0/16" + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} + `, cidr, msg, signature, vpcCidr) +} + +func testAccVPCIpamByoipIPv6CIDRBlockAssociationIpamDefaultNetmask(cidr, msg, signature string) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +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 + publicly_advertisable = false + aws_service = "ec2" + allocation_default_netmask_length = 56 +} + +resource "aws_vpc_ipam_pool_cidr" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = %[1]q + + cidr_authorization_context { + message = %[2]q + signature = %[3]q + } +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpc_ipv6_cidr_block_association" "test" { + ipv6_ipam_pool_id = aws_vpc_ipam_pool.test.id + vpc_id = aws_vpc.test.id + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} + `, cidr, msg, signature) +} + +func testAccVPCIpamByoipIPv6CIDRBlockAssociationIpamExplicitNetmask(cidr, msg, signature string, netmask int) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +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 + publicly_advertisable = false + aws_service = "ec2" + allocation_default_netmask_length = 56 +} + +resource "aws_vpc_ipam_pool_cidr" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = %[1]q + + cidr_authorization_context { + message = %[2]q + signature = %[3]q + } +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpc_ipv6_cidr_block_association" "test" { + ipv6_ipam_pool_id = aws_vpc_ipam_pool.test.id + ipv6_netmask_length = %[4]d + vpc_id = aws_vpc.test.id + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} + `, cidr, msg, signature, netmask) +} + +func testAccVPCIpamByoipIPv6CIDRBlockAssociationIpamExplicitCIDR(cidr, msg, signature, vpcCidr string) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +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 + publicly_advertisable = false + aws_service = "ec2" + allocation_default_netmask_length = 56 +} + +resource "aws_vpc_ipam_pool_cidr" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = %[1]q + + cidr_authorization_context { + message = %[2]q + signature = %[3]q + } +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpc_ipv6_cidr_block_association" "test" { + ipv6_ipam_pool_id = aws_vpc_ipam_pool.test.id + ipv6_cidr_block = %[4]q + vpc_id = aws_vpc.test.id + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} + `, cidr, msg, signature, vpcCidr) +} diff --git a/internal/service/ec2/vpc_ipam.go b/internal/service/ec2/vpc_ipam.go new file mode 100644 index 00000000000..642ce61b0b2 --- /dev/null +++ b/internal/service/ec2/vpc_ipam.go @@ -0,0 +1,365 @@ +package ec2 + +import ( + "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/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceVPCIpam() *schema.Resource { + return &schema.Resource{ + Create: resourceVPCIpamCreate, + Read: resourceVPCIpamRead, + Update: resourceVPCIpamUpdate, + Delete: resourceVPCIpamDelete, + CustomizeDiff: customdiff.Sequence(verify.SetTagsDiff), + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "operating_regions": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "region_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidRegionName, + }, + }, + }, + }, + "private_default_scope_id": { + Type: schema.TypeString, + Computed: true, + }, + "public_default_scope_id": { + Type: schema.TypeString, + Computed: true, + }, + "scope_count": { + Type: schema.TypeInt, + Computed: true, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + } +} + +const ( + IpamStatusAvailable = "Available" + InvalidIpamIdNotFound = "InvalidIpamId.NotFound" + IpamCreateTimeout = 3 * time.Minute + IpamCreateDeley = 5 * time.Second + IpamDeleteTimeout = 3 * time.Minute + IpamDeleteDelay = 5 * time.Second +) + +func resourceVPCIpamCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + current_region := meta.(*conns.AWSClient).Region + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + + input := &ec2.CreateIpamInput{ + ClientToken: aws.String(resource.UniqueId()), + TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, "ipam"), + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + operatingRegions := d.Get("operating_regions").(*schema.Set).List() + if !expandIpamOperatingRegionsContainsCurrentRegion(operatingRegions, current_region) { + return fmt.Errorf("Must include (%s) as a operating_region", current_region) + } + input.OperatingRegions = expandIpamOperatingRegions(operatingRegions) + + log.Printf("[DEBUG] Creating IPAM: %s", input) + output, err := conn.CreateIpam(input) + if err != nil { + return fmt.Errorf("Error creating ipam: %w", err) + } + d.SetId(aws.StringValue(output.Ipam.IpamId)) + log.Printf("[INFO] IPAM ID: %s", d.Id()) + + if _, err = WaitIpamAvailable(conn, d.Id(), IpamCreateTimeout); err != nil { + return fmt.Errorf("error waiting for IPAM (%s) to be Available: %w", d.Id(), err) + } + + return resourceVPCIpamRead(d, meta) +} + +func resourceVPCIpamRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + ipam, err := findIpamById(conn, d.Id()) + + if err != nil && !tfawserr.ErrCodeEquals(err, InvalidIpamIdNotFound) { + return err + } + + if !d.IsNewResource() && ipam == nil { + log.Printf("[WARN] IPAM (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("arn", ipam.IpamArn) + d.Set("description", ipam.Description) + d.Set("operating_regions", flattenIpamOperatingRegions(ipam.OperatingRegions)) + d.Set("public_default_scope_id", ipam.PublicDefaultScopeId) + d.Set("private_default_scope_id", ipam.PrivateDefaultScopeId) + d.Set("scope_count", ipam.ScopeCount) + + tags := KeyValueTags(ipam.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + + return nil +} + +func resourceVPCIpamUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + if err := UpdateTags(conn, d.Id(), o, n); err != nil { + return fmt.Errorf("error updating tags: %w", err) + } + } + input := &ec2.ModifyIpamInput{ + IpamId: aws.String(d.Id()), + } + + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChange("operating_regions") { + o, n := d.GetChange("operating_regions") + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) + } + + os := o.(*schema.Set) + ns := n.(*schema.Set) + operatingRegionUpdateAdd := expandIpamOperatingRegionsUpdateAddRegions(ns.Difference(os).List()) + operatingRegionUpdateRemove := expandIpamOperatingRegionsUpdateDeleteRegions(os.Difference(ns).List()) + + if len(operatingRegionUpdateAdd) != 0 { + input.AddOperatingRegions = operatingRegionUpdateAdd + } + + if len(operatingRegionUpdateRemove) != 0 { + input.RemoveOperatingRegions = operatingRegionUpdateRemove + } + _, err := conn.ModifyIpam(input) + if err != nil { + return fmt.Errorf("Error modifying operating regions to ipam: %w", err) + } + } + + return nil +} + +func resourceVPCIpamDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + input := &ec2.DeleteIpamInput{ + IpamId: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Deleting IPAM: %s", input) + _, err := conn.DeleteIpam(input) + if err != nil { + return fmt.Errorf("error deleting IPAM: (%s): %w", d.Id(), err) + } + + if _, err = WaiterIpamDeleted(conn, d.Id(), IpamDeleteTimeout); err != nil { + if tfawserr.ErrCodeEquals(err, InvalidIpamIdNotFound) { + return nil + } + return fmt.Errorf("error waiting for IPAM (%s) to be deleted: %w", d.Id(), err) + } + + return nil +} + +func findIpamById(conn *ec2.EC2, id string) (*ec2.Ipam, error) { + input := &ec2.DescribeIpamsInput{ + IpamIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeIpams(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.Ipams) == 0 || output.Ipams[0] == nil { + return nil, nil + } + + return output.Ipams[0], nil +} + +func WaitIpamAvailable(conn *ec2.EC2, ipamId string, timeout time.Duration) (*ec2.Ipam, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.IpamStateCreateInProgress}, + Target: []string{ec2.IpamStateCreateComplete}, + Refresh: statusIpamStatus(conn, ipamId), + Timeout: timeout, + Delay: IpamCreateDeley, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.Ipam); ok { + return output, err + } + + return nil, err +} + +func WaiterIpamDeleted(conn *ec2.EC2, ipamId string, timeout time.Duration) (*ec2.Ipam, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.IpamStateCreateComplete, ec2.IpamStateModifyComplete}, + Target: []string{InvalidIpamIdNotFound}, + Refresh: statusIpamStatus(conn, ipamId), + Timeout: timeout, + Delay: IpamDeleteDelay, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.Ipam); ok { + return output, err + } + + return nil, err +} + +func statusIpamStatus(conn *ec2.EC2, ipamId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + + output, err := findIpamById(conn, ipamId) + + if tfawserr.ErrCodeEquals(err, InvalidIpamIdNotFound) { + return output, InvalidIpamIdNotFound, nil + } + + // there was an unhandled error in the Finder + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.State), nil + } +} + +func expandIpamOperatingRegions(operatingRegions []interface{}) []*ec2.AddIpamOperatingRegion { + regions := make([]*ec2.AddIpamOperatingRegion, 0, len(operatingRegions)) + for _, regionRaw := range operatingRegions { + region := regionRaw.(map[string]interface{}) + regions = append(regions, expandIpamOperatingRegion(region)) + } + + return regions +} + +func expandIpamOperatingRegion(operatingRegion map[string]interface{}) *ec2.AddIpamOperatingRegion { + region := &ec2.AddIpamOperatingRegion{ + RegionName: aws.String(operatingRegion["region_name"].(string)), + } + return region +} + +func flattenIpamOperatingRegions(operatingRegions []*ec2.IpamOperatingRegion) []interface{} { + regions := []interface{}{} + for _, operatingRegion := range operatingRegions { + regions = append(regions, flattenIpamOperatingRegion(operatingRegion)) + } + return regions +} + +func flattenIpamOperatingRegion(operatingRegion *ec2.IpamOperatingRegion) map[string]interface{} { + region := make(map[string]interface{}) + region["region_name"] = aws.StringValue(operatingRegion.RegionName) + return region +} + +func expandIpamOperatingRegionsUpdateAddRegions(operatingRegions []interface{}) []*ec2.AddIpamOperatingRegion { + regionUpdates := make([]*ec2.AddIpamOperatingRegion, 0, len(operatingRegions)) + for _, regionRaw := range operatingRegions { + region := regionRaw.(map[string]interface{}) + regionUpdates = append(regionUpdates, expandIpamOperatingRegionsUpdateAddRegion(region)) + } + return regionUpdates +} + +func expandIpamOperatingRegionsUpdateAddRegion(operatingRegion map[string]interface{}) *ec2.AddIpamOperatingRegion { + regionUpdate := &ec2.AddIpamOperatingRegion{ + RegionName: aws.String(operatingRegion["region_name"].(string)), + } + return regionUpdate +} + +func expandIpamOperatingRegionsUpdateDeleteRegions(operatingRegions []interface{}) []*ec2.RemoveIpamOperatingRegion { + regionUpdates := make([]*ec2.RemoveIpamOperatingRegion, 0, len(operatingRegions)) + for _, regionRaw := range operatingRegions { + region := regionRaw.(map[string]interface{}) + regionUpdates = append(regionUpdates, expandIpamOperatingRegionsUpdateDeleteRegion(region)) + } + return regionUpdates +} + +func expandIpamOperatingRegionsUpdateDeleteRegion(operatingRegion map[string]interface{}) *ec2.RemoveIpamOperatingRegion { + regionUpdate := &ec2.RemoveIpamOperatingRegion{ + RegionName: aws.String(operatingRegion["region_name"].(string)), + } + return regionUpdate +} + +func expandIpamOperatingRegionsContainsCurrentRegion(operatingRegions []interface{}, current_region string) bool { + for _, regionRaw := range operatingRegions { + region := regionRaw.(map[string]interface{}) + if region["region_name"].(string) == current_region { + return true + } + } + return false +} diff --git a/internal/service/ec2/vpc_ipam_pool.go b/internal/service/ec2/vpc_ipam_pool.go new file mode 100644 index 00000000000..266286534b3 --- /dev/null +++ b/internal/service/ec2/vpc_ipam_pool.go @@ -0,0 +1,448 @@ +package ec2 + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "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" + 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" +) + +func ResourceVPCIpamPool() *schema.Resource { + return &schema.Resource{ + Create: ResourceVPCIpamPoolCreate, + Read: ResourceVPCIpamPoolRead, + Update: ResourceVPCIpamPoolUpdate, + Delete: ResourceVPCIpamPoolDelete, + CustomizeDiff: customdiff.Sequence(verify.SetTagsDiff), + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "address_family": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(ec2.AddressFamily_Values(), false), + }, + "publicly_advertisable": { + Type: schema.TypeBool, + Optional: true, + }, + "allocation_default_netmask_length": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 128), + }, + "allocation_max_netmask_length": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 128), + }, + "allocation_min_netmask_length": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 128), + }, + "allocation_resource_tags": tftags.TagsSchema(), + "auto_import": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "aws_service": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(ec2.IpamPoolAwsService_Values(), false), + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "ipam_scope_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "ipam_scope_type": { + Type: schema.TypeString, + Computed: true, + }, + "locale": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.Any( + validation.StringInSlice([]string{"None"}, false), + verify.ValidRegionName, + ), + Default: "None", + }, + "pool_depth": { + Type: schema.TypeInt, + Computed: true, + }, + "source_ipam_pool_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + } +} + +const ( + IpamPoolCreateTimeout = 3 * time.Minute + InvalidIpamPoolIdNotFound = "InvalidIpamPoolId.NotFound" + IpamPoolUpdateTimeout = 3 * time.Minute + IpamPoolDeleteTimeout = 3 * time.Minute + IpamPoolAvailableDelay = 5 * time.Second + IpamPoolDeleteDelay = 5 * time.Second +) + +func ResourceVPCIpamPoolCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + + input := &ec2.CreateIpamPoolInput{ + AddressFamily: aws.String(d.Get("address_family").(string)), + ClientToken: aws.String(resource.UniqueId()), + IpamScopeId: aws.String(d.Get("ipam_scope_id").(string)), + TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, "ipam-pool"), + } + + if v := d.Get("publicly_advertisable"); v != "" && d.Get("address_family") == ec2.AddressFamilyIpv6 { + input.PubliclyAdvertisable = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("allocation_default_netmask_length"); ok { + input.AllocationDefaultNetmaskLength = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("allocation_max_netmask_length"); ok { + input.AllocationMaxNetmaskLength = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("allocation_min_netmask_length"); ok { + input.AllocationMinNetmaskLength = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("allocation_resource_tags"); ok && len(v.(map[string]interface{})) > 0 { + input.AllocationResourceTags = ipamResourceTags(tftags.New(v.(map[string]interface{}))) + } + + if v, ok := d.GetOk("auto_import"); ok { + input.AutoImport = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("locale"); ok && v != "None" { + input.Locale = aws.String(v.(string)) + } + + if v, ok := d.GetOk("aws_service"); ok { + input.AwsService = aws.String(v.(string)) + } + + if v, ok := d.GetOk("source_ipam_pool_id"); ok { + input.SourceIpamPoolId = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating IPAM Pool: %s", input) + output, err := conn.CreateIpamPool(input) + if err != nil { + return fmt.Errorf("Error creating ipam pool in ipam scope (%s): %w", d.Get("ipam_scope_id").(string), err) + } + d.SetId(aws.StringValue(output.IpamPool.IpamPoolId)) + log.Printf("[INFO] IPAM Pool ID: %s", d.Id()) + + if _, err = WaitIpamPoolAvailable(conn, d.Id(), IpamPoolCreateTimeout); err != nil { + return fmt.Errorf("error waiting for IPAM Pool (%s) to be Available: %w", d.Id(), err) + } + + return ResourceVPCIpamPoolRead(d, meta) +} + +func ResourceVPCIpamPoolRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + pool, err := FindIpamPoolById(conn, d.Id()) + + if err != nil && !tfawserr.ErrCodeEquals(err, InvalidIpamPoolIdNotFound) { + return err + } + + if !d.IsNewResource() && pool == nil { + log.Printf("[WARN] IPAM Pool (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("arn", pool.IpamPoolArn) + + scopeId := strings.Split(*pool.IpamScopeArn, "/")[1] + + d.Set("address_family", pool.AddressFamily) + + if pool.PubliclyAdvertisable != nil { + d.Set("publicly_advertisable", pool.PubliclyAdvertisable) + } + + d.Set("allocation_resource_tags", KeyValueTags(ec2TagsFromIpamAllocationTags(pool.AllocationResourceTags)).Map()) + d.Set("auto_import", pool.AutoImport) + d.Set("description", pool.Description) + d.Set("ipam_scope_id", scopeId) + d.Set("ipam_scope_type", pool.IpamScopeType) + d.Set("locale", pool.Locale) + d.Set("pool_depth", pool.PoolDepth) + d.Set("aws_service", pool.AwsService) + d.Set("source_ipam_pool_id", pool.SourceIpamPoolId) + d.Set("state", pool.State) + + tags := KeyValueTags(pool.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + + return nil +} + +func ResourceVPCIpamPoolUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + if err := UpdateTags(conn, d.Id(), o, n); err != nil { + return fmt.Errorf("error updating tags: %w", err) + } + } + input := &ec2.ModifyIpamPoolInput{ + IpamPoolId: aws.String(d.Id()), + } + + if d.HasChangesExcept("tags_all", "allocation_resource_tags") { + if v, ok := d.GetOk("allocation_default_netmask_length"); ok { + input.AllocationDefaultNetmaskLength = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("auto_import"); ok { + input.AutoImport = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("allocation_max_netmask_length"); ok { + input.AllocationMaxNetmaskLength = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("allocation_min_netmask_length"); ok { + input.AllocationMinNetmaskLength = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + } + + if d.HasChange("allocation_resource_tags") { + o, n := d.GetChange("allocation_resource_tags") + oldTags := tftags.New(o) + newTags := tftags.New(n) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input.RemoveAllocationResourceTags = ipamResourceTags(removedTags.IgnoreAWS()) + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input.AddAllocationResourceTags = ipamResourceTags(updatedTags.IgnoreAWS()) + } + } + + log.Printf("[DEBUG] Updating IPAM pool: %s", input) + _, err := conn.ModifyIpamPool(input) + if err != nil { + return fmt.Errorf("error updating IPAM Pool (%s): %w", d.Id(), err) + } + + if _, err = WaitIpamPoolUpdate(conn, d.Id(), IpamPoolUpdateTimeout); err != nil { + return fmt.Errorf("error waiting for IPAM Pool (%s) to be Available: %w", d.Id(), err) + } + + return ResourceVPCIpamPoolRead(d, meta) +} + +func ResourceVPCIpamPoolDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + input := &ec2.DeleteIpamPoolInput{ + IpamPoolId: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Deleting IPAM Pool: %s", input) + _, err := conn.DeleteIpamPool(input) + if err != nil { + return fmt.Errorf("error deleting IPAM Pool: (%s): %w", d.Id(), err) + } + + if _, err = WaitIpamPoolDeleted(conn, d.Id(), IpamPoolDeleteTimeout); err != nil { + if tfresource.NotFound(err) { + return nil + } + return fmt.Errorf("error waiting for IPAM Pool (%s) to be deleted: %w", d.Id(), err) + } + + return nil +} + +func FindIpamPoolById(conn *ec2.EC2, id string) (*ec2.IpamPool, error) { + input := &ec2.DescribeIpamPoolsInput{ + IpamPoolIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeIpamPools(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.IpamPools) == 0 || output.IpamPools[0] == nil { + return nil, nil + } + + return output.IpamPools[0], nil +} + +func WaitIpamPoolAvailable(conn *ec2.EC2, ipamPoolId string, timeout time.Duration) (*ec2.IpamPool, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.IpamPoolStateCreateInProgress}, + Target: []string{ec2.IpamPoolStateCreateComplete}, + Refresh: statusIpamPoolStatus(conn, ipamPoolId), + Timeout: timeout, + Delay: IpamPoolAvailableDelay, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.IpamPool); ok { + return output, err + } + + return nil, err +} + +func WaitIpamPoolUpdate(conn *ec2.EC2, ipamPoolId string, timeout time.Duration) (*ec2.IpamPool, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.IpamPoolStateModifyInProgress}, + Target: []string{ec2.IpamPoolStateModifyComplete}, + Refresh: statusIpamPoolStatus(conn, ipamPoolId), + Timeout: timeout, + Delay: IpamPoolAvailableDelay, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.IpamPool); ok { + return output, err + } + + return nil, err +} + +func WaitIpamPoolDeleted(conn *ec2.EC2, ipamPoolId string, timeout time.Duration) (*ec2.IpamPool, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.IpamPoolStateDeleteInProgress}, + Target: []string{InvalidIpamPoolIdNotFound}, + Refresh: statusIpamPoolStatus(conn, ipamPoolId), + Timeout: timeout, + Delay: IpamPoolDeleteDelay, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.IpamPool); ok { + return output, err + } + + return nil, err +} + +func statusIpamPoolStatus(conn *ec2.EC2, ipamPoolId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + + output, err := FindIpamPoolById(conn, ipamPoolId) + + if tfawserr.ErrCodeEquals(err, InvalidIpamPoolIdNotFound) { + return output, InvalidIpamPoolIdNotFound, nil + } + + // there was an unhandled error in the Finder + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.State), nil + } +} + +func ipamResourceTags(tags tftags.KeyValueTags) []*ec2.RequestIpamResourceTag { + result := make([]*ec2.RequestIpamResourceTag, 0, len(tags)) + + for k, v := range tags.Map() { + tag := &ec2.RequestIpamResourceTag{ + Key: aws.String(k), + Value: aws.String(v), + } + + result = append(result, tag) + } + + return result +} + +func ec2TagsFromIpamAllocationTags(rts []*ec2.IpamResourceTag) []*ec2.Tag { + if len(rts) == 0 { + return nil + } + + tags := []*ec2.Tag{} + for _, ts := range rts { + tags = append(tags, &ec2.Tag{ + Key: ts.Key, + Value: ts.Value, + }) + } + + return tags +} diff --git a/internal/service/ec2/vpc_ipam_pool_cidr.go b/internal/service/ec2/vpc_ipam_pool_cidr.go new file mode 100644 index 00000000000..b963ade0fae --- /dev/null +++ b/internal/service/ec2/vpc_ipam_pool_cidr.go @@ -0,0 +1,275 @@ +package ec2 + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "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/verify" +) + +func ResourceVPCIpamPoolCidr() *schema.Resource { + return &schema.Resource{ + Create: resourceVPCIpamPoolCidrCreate, + Read: resourceVPCIpamPoolCidrRead, + Delete: resourceVPCIpamPoolCidrDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "cidr": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ValidateFunc: validation.Any( + verify.ValidIPv4CIDRNetworkAddress, + verify.ValidIPv6CIDRNetworkAddress, + ), + }, + "cidr_authorization_context": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "message": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "signature": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + "ipam_pool_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +const ( + IpamPoolCidrCreateTimeout = 10 * time.Minute + // allocations releases are eventually consistent with a max time of 20m + IpamPoolCidrDeleteTimeout = 32 * time.Minute + IpamPoolCidrAvailableDelay = 5 * time.Second + IpamPoolCidrDeleteDelay = 5 * time.Second +) + +func resourceVPCIpamPoolCidrCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + pool_id := d.Get("ipam_pool_id").(string) + + input := &ec2.ProvisionIpamPoolCidrInput{ + IpamPoolId: aws.String(pool_id), + } + + if v, ok := d.GetOk("cidr_authorization_context"); ok { + input.CidrAuthorizationContext = expandVPCIpamPoolCidrCidrAuthorizationContext(v.([]interface{})) + } + + if v, ok := d.GetOk("cidr"); ok { + input.Cidr = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Provisioning IPAM Pool Cidr: %s", input) + output, err := conn.ProvisionIpamPoolCidr(input) + if err != nil { + return fmt.Errorf("Error provisioning ipam pool cidr in ipam pool (%s): %w", d.Get("ipam_pool_id").(string), err) + } + + cidr := aws.StringValue(output.IpamPoolCidr.Cidr) + id := encodeIpamPoolCidrId(cidr, pool_id) + + if _, err = WaitIpamPoolCidrAvailable(conn, id, IpamPoolCidrCreateTimeout); err != nil { + return fmt.Errorf("error waiting for IPAM Pool Cidr (%s) to be provision: %w", id, err) + } + + d.SetId(id) + return resourceVPCIpamPoolCidrRead(d, meta) +} + +func resourceVPCIpamPoolCidrRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + cidr, pool_id, err := FindIpamPoolCidr(conn, d.Id()) + + if err != nil { + return err + } + + if !d.IsNewResource() && cidr == nil { + log.Printf("[WARN] IPAM Pool Cidr (%s) not found or was deprovisioned, removing from state", cidr) + d.SetId("") + return nil + } + + if aws.StringValue(cidr.State) == ec2.IpamPoolCidrStateDeprovisioned { + log.Printf("[WARN] IPAM Pool Cidr (%s) not found or was deprovisioned, removing from state", cidr) + d.SetId("") + return nil + } + + d.Set("cidr", cidr.Cidr) + // pool id is not returned in describe, adding from concatenated id + d.Set("ipam_pool_id", pool_id) + + return nil +} + +func resourceVPCIpamPoolCidrDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + cidr, pool_id, err := DecodeIpamPoolCidrID(d.Id()) + + input := &ec2.DeprovisionIpamPoolCidrInput{ + Cidr: aws.String(cidr), + IpamPoolId: aws.String(pool_id), + } + return resource.Retry(IpamPoolCidrDeleteTimeout, func() *resource.RetryError { + log.Printf("[DEBUG] Deprovisioning IPAM Pool Cidr: %s", input) + // releasing allocations is eventually consistent and can cause deprovisioning to fail + _, err = conn.DeprovisionIpamPoolCidr(input) + + if err != nil { + // IncorrectState err can mean: State = "deprovisioned" || State = "pending-deprovision" + if tfawserr.ErrMessageContains(err, "IncorrectState", "") { + output, err := WaitIpamPoolCidrDeleted(conn, d.Id(), IpamPoolCidrDeleteTimeout) + if err != nil { + // State = failed-deprovision + return resource.RetryableError(fmt.Errorf("Expected cidr to be deprovisioned but was in state %s", aws.StringValue(output.State))) + } + // State = deprovisioned + return nil + } + return resource.NonRetryableError(fmt.Errorf("error deprovisioning ipam pool cidr: (%s): %w", cidr, err)) + } + + output, err := WaitIpamPoolCidrDeleted(conn, d.Id(), IpamPoolCidrDeleteTimeout) + if err != nil { + // State = failed-deprovision + return resource.RetryableError(fmt.Errorf("Expected cidr to be deprovisioned but was in state %s", aws.StringValue(output.State))) + } + // State = deprovisioned + return nil + }) +} + +func FindIpamPoolCidr(conn *ec2.EC2, id string) (*ec2.IpamPoolCidr, string, error) { + cidr_block, pool_id, err := DecodeIpamPoolCidrID(id) + if err != nil { + return nil, "", fmt.Errorf("error decoding ID (%s): %w", cidr_block, err) + } + input := &ec2.GetIpamPoolCidrsInput{ + IpamPoolId: aws.String(pool_id), + Filters: []*ec2.Filter{ + { + Name: aws.String("cidr"), + Values: aws.StringSlice([]string{cidr_block}), + }, + }, + } + + output, err := conn.GetIpamPoolCidrs(input) + + if err != nil { + return nil, "", err + } + + if output == nil || len(output.IpamPoolCidrs) == 0 || output.IpamPoolCidrs[0] == nil { + return nil, "", nil + } + + return output.IpamPoolCidrs[0], pool_id, nil +} + +func WaitIpamPoolCidrAvailable(conn *ec2.EC2, id string, timeout time.Duration) (*ec2.IpamPoolCidr, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.IpamPoolCidrStatePendingProvision}, + Target: []string{ec2.IpamPoolCidrStateProvisioned}, + Refresh: statusIpamPoolCidrStatus(conn, id), + Timeout: timeout, + Delay: IpamPoolCidrAvailableDelay, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.IpamPoolCidr); ok { + return output, err + } + + return nil, err +} + +func WaitIpamPoolCidrDeleted(conn *ec2.EC2, id string, timeout time.Duration) (*ec2.IpamPoolCidr, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.IpamPoolCidrStatePendingDeprovision, ec2.IpamPoolCidrStateProvisioned}, + Target: []string{ec2.IpamPoolCidrStateDeprovisioned}, + Refresh: statusIpamPoolCidrStatus(conn, id), + Timeout: timeout, + Delay: IpamPoolCidrDeleteDelay, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.IpamPoolCidr); ok { + return output, err + } + + return nil, err +} + +func statusIpamPoolCidrStatus(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + + output, _, err := FindIpamPoolCidr(conn, id) + + // there was an unhandled error in the Finder + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.State), nil + } +} + +func encodeIpamPoolCidrId(cidr, pool_id string) string { + return fmt.Sprintf("%s_%s", cidr, pool_id) +} + +func DecodeIpamPoolCidrID(id string) (string, string, error) { + idParts := strings.Split(id, "_") + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + return "", "", fmt.Errorf("expected ID in the form of cidr_poolId, given: %q", id) + } + return idParts[0], idParts[1], nil +} + +func expandVPCIpamPoolCidrCidrAuthorizationContext(l []interface{}) *ec2.IpamCidrAuthorizationContext { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + cac := &ec2.IpamCidrAuthorizationContext{ + Message: aws.String(m["message"].(string)), + Signature: aws.String(m["signature"].(string)), + } + + return cac +} diff --git a/internal/service/ec2/vpc_ipam_pool_cidr_allocation.go b/internal/service/ec2/vpc_ipam_pool_cidr_allocation.go new file mode 100644 index 00000000000..c4fff108ba2 --- /dev/null +++ b/internal/service/ec2/vpc_ipam_pool_cidr_allocation.go @@ -0,0 +1,204 @@ +package ec2 + +import ( + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "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/verify" +) + +func ResourceVPCIpamPoolCidrAllocation() *schema.Resource { + return &schema.Resource{ + Create: ResourceVPCIpamPoolCidrAllocationCreate, + Read: ResourceVPCIpamPoolCidrAllocationRead, + Delete: ResourceVPCIpamPoolCidrAllocationDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "cidr": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ConflictsWith: []string{"netmask_length"}, + ValidateFunc: validation.Any( + verify.ValidIPv4CIDRNetworkAddress, + validation.IsCIDRNetwork(VPCCIDRMinIPv4, VPCCIDRMaxIPv4), + ), + }, + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "ipam_pool_allocation_id": { + Type: schema.TypeString, + Computed: true, + }, + "ipam_pool_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "netmask_length": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntBetween(VPCCIDRMinIPv4, VPCCIDRMaxIPv4), + ConflictsWith: []string{"cidr"}, + }, + "resource_id": { + Type: schema.TypeString, + Computed: true, + }, + "resource_owner": { + Type: schema.TypeString, + Computed: true, + }, + "resource_type": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +const ( + IpamPoolAllocationNotFound = "InvalidIpamPoolCidrAllocationId.NotFound" +) + +func ResourceVPCIpamPoolCidrAllocationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + pool_id := d.Get("ipam_pool_id").(string) + + input := &ec2.AllocateIpamPoolCidrInput{ + ClientToken: aws.String(resource.UniqueId()), + IpamPoolId: aws.String(pool_id), + } + + if v, ok := d.GetOk("cidr"); ok { + input.Cidr = aws.String(v.(string)) + } + + if v := d.Get("netmask_length"); v != 0 { + input.NetmaskLength = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating IPAM Pool Allocation: %s", input) + output, err := conn.AllocateIpamPoolCidr(input) + if err != nil { + return fmt.Errorf("Error allocating cidr from IPAM pool (%s): %w", d.Get("ipam_pool_id").(string), err) + } + d.SetId(encodeIpamPoolCidrAllocationID(aws.StringValue(output.IpamPoolAllocation.IpamPoolAllocationId), pool_id)) + + return ResourceVPCIpamPoolCidrAllocationRead(d, meta) +} + +func ResourceVPCIpamPoolCidrAllocationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + cidr_allocation, pool_id, err := FindIpamPoolCidrAllocation(conn, d.Id()) + + if err != nil { + return err + } + + if !d.IsNewResource() && cidr_allocation == nil { + log.Printf("[WARN] IPAM Pool Cidr Allocation (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("ipam_pool_allocation_id", cidr_allocation.IpamPoolAllocationId) + d.Set("ipam_pool_id", pool_id) + + d.Set("cidr", cidr_allocation.Cidr) + + if cidr_allocation.ResourceId != nil { + d.Set("resource_id", cidr_allocation.ResourceId) + } + if cidr_allocation.ResourceOwner != nil { + d.Set("resource_owner", cidr_allocation.ResourceOwner) + } + if cidr_allocation.ResourceType != nil { + d.Set("resource_type", cidr_allocation.ResourceType) + } + + return nil +} + +func ResourceVPCIpamPoolCidrAllocationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + input := &ec2.ReleaseIpamPoolAllocationInput{ + IpamPoolAllocationId: aws.String(d.Get("ipam_pool_allocation_id").(string)), + IpamPoolId: aws.String(d.Get("ipam_pool_id").(string)), + Cidr: aws.String(d.Get("cidr").(string)), + } + + log.Printf("[DEBUG] Releasing IPAM Pool CIDR Allocation: %s", input) + output, err := conn.ReleaseIpamPoolAllocation(input) + if err != nil || !aws.BoolValue(output.Success) { + if tfawserr.ErrCodeEquals(err, InvalidIpamPoolIdNotFound) { + return nil + } + return fmt.Errorf("error releasing IPAM CIDR Allocation: (%s): %w", d.Id(), err) + } + + return nil +} + +func FindIpamPoolCidrAllocation(conn *ec2.EC2, id string) (*ec2.IpamPoolAllocation, string, error) { + + allocation_id, pool_id, err := DecodeIpamPoolCidrAllocationID(id) + if err != nil { + return nil, "", fmt.Errorf("error decoding ID (%s): %w", allocation_id, err) + } + + input := &ec2.GetIpamPoolAllocationsInput{ + IpamPoolId: aws.String(pool_id), + Filters: []*ec2.Filter{ + { + Name: aws.String("ipam-pool-allocation-id"), + Values: aws.StringSlice([]string{allocation_id}), + }, + }, + } + + output, err := conn.GetIpamPoolAllocations(input) + + if err != nil { + return nil, "", err + } + + if output == nil || len(output.IpamPoolAllocations) == 0 || output.IpamPoolAllocations[0] == nil { + return nil, "", nil + } + + return output.IpamPoolAllocations[0], pool_id, nil +} + +func encodeIpamPoolCidrAllocationID(allocation_id, pool_id string) string { + return fmt.Sprintf("%s_%s", allocation_id, pool_id) +} + +func DecodeIpamPoolCidrAllocationID(id string) (string, string, error) { + idParts := strings.Split(id, "_") + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + return "", "", fmt.Errorf("expected ID in the form of allocationId_poolId, given: %q", id) + } + return idParts[0], idParts[1], nil +} diff --git a/internal/service/ec2/vpc_ipam_pool_cidr_allocation_test.go b/internal/service/ec2/vpc_ipam_pool_cidr_allocation_test.go new file mode 100644 index 00000000000..da24a4123dc --- /dev/null +++ b/internal/service/ec2/vpc_ipam_pool_cidr_allocation_test.go @@ -0,0 +1,174 @@ +package ec2_test + +import ( + "fmt" + "regexp" + "strings" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" +) + +func TestAccVPCIpamPoolAllocation_ipv4Basic(t *testing.T) { + var allocation ec2.IpamPoolAllocation + resourceName := "aws_vpc_ipam_pool_cidr_allocation.test" + cidr := "172.2.0.0/28" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIpamPoolAllocationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamPoolAllocationIpv4(cidr), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCIpamAllocationExists(resourceName, &allocation), + resource.TestCheckResourceAttr(resourceName, "cidr", cidr), + resource.TestMatchResourceAttr(resourceName, "id", regexp.MustCompile(`^ipam-pool-alloc-[\da-f]+_ipam-pool(-[\da-f]+)$`)), + resource.TestMatchResourceAttr(resourceName, "ipam_pool_allocation_id", regexp.MustCompile(`^ipam-pool-alloc-[\da-f]+$`)), + resource.TestCheckResourceAttrPair(resourceName, "ipam_pool_id", "aws_vpc_ipam_pool.test", "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccVPCIpamPoolAllocation_ipv4BasicNetmask(t *testing.T) { + var allocation ec2.IpamPoolAllocation + resourceName := "aws_vpc_ipam_pool_cidr_allocation.test" + netmask := "28" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIpamPoolAllocationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamPoolAllocationIpv4Netmask(netmask), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCIpamAllocationExists(resourceName, &allocation), + testAccCheckVPCIpamCidrPrefix(&allocation, netmask), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"netmask_length"}, + }, + }, + }) +} + +func testAccCheckVPCIpamAllocationExists(n string, allocation *ec2.IpamPoolAllocation) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + id := rs.Primary.ID + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + cidr_allocation, _, err := tfec2.FindIpamPoolCidrAllocation(conn, id) + + if err != nil { + return err + } + *allocation = *cidr_allocation + + return nil + } +} + +func testAccCheckVPCIpamCidrPrefix(allocation *ec2.IpamPoolAllocation, expected string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if strings.Split(aws.StringValue(allocation.Cidr), "/")[1] != expected { + return fmt.Errorf("Bad cidr prefix: %s", aws.StringValue(allocation.Cidr)) + } + + return nil + } +} + +func testAccCheckVPCIpamPoolAllocationDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpc_ipam_pool_cidr_allocation" { + continue + } + + id := rs.Primary.ID + _, _, err := tfec2.FindIpamPoolCidrAllocation(conn, id) + + if err != nil { + if tfawserr.ErrCodeEquals(err, tfec2.IpamPoolAllocationNotFound) || tfawserr.ErrCodeEquals(err, tfec2.InvalidIpamPoolIdNotFound) { + return nil + } + return err + } + + } + + return nil +} + +const testAccVPCIpamPoolCidrPrivateBase = ` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + description = "test" + operating_regions { + region_name = data.aws_region.current.name + } +} + +resource "aws_vpc_ipam_pool" "test" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.test.private_default_scope_id + locale = data.aws_region.current.name +} + +resource "aws_vpc_ipam_pool_cidr" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = "172.2.0.0/24" +} +` + +func testAccVPCIpamPoolAllocationIpv4(cidr string) string { + return testAccVPCIpamPoolCidrPrivateBase + fmt.Sprintf(` +resource "aws_vpc_ipam_pool_cidr_allocation" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = %[1]q + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} +`, cidr) +} + +func testAccVPCIpamPoolAllocationIpv4Netmask(netmask string) string { + return testAccVPCIpamPoolCidrPrivateBase + fmt.Sprintf(` +resource "aws_vpc_ipam_pool_cidr_allocation" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + netmask_length = %[1]q + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} +`, netmask) +} diff --git a/internal/service/ec2/vpc_ipam_pool_cidr_test.go b/internal/service/ec2/vpc_ipam_pool_cidr_test.go new file mode 100644 index 00000000000..8cae0db8bb4 --- /dev/null +++ b/internal/service/ec2/vpc_ipam_pool_cidr_test.go @@ -0,0 +1,116 @@ +package ec2_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccVPCIpamPoolCidr_ipv4Basic(t *testing.T) { + var cidr ec2.IpamPoolCidr + resourceName := "aws_vpc_ipam_pool_cidr.test" + cidr_range := "10.0.0.0/24" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIpamProvisionedPoolCidrDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamProvisionedPoolCidrIpv4(cidr_range), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCIpamCidrExists(resourceName, &cidr), + resource.TestCheckResourceAttr(resourceName, "cidr", cidr_range), + resource.TestCheckResourceAttrPair(resourceName, "ipam_pool_id", "aws_vpc_ipam_pool.test", "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckVPCIpamCidrExists(n string, cidr *ec2.IpamPoolCidr) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + id := rs.Primary.ID + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + found_cidr, _, err := tfec2.FindIpamPoolCidr(conn, id) + + if err != nil { + return err + } + *cidr = *found_cidr + + return nil + } +} + +func testAccCheckVPCIpamProvisionedPoolCidrDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpc_ipam_pool_cidr" { + continue + } + + id := rs.Primary.ID + + _, pool_id, err := tfec2.DecodeIpamPoolCidrID(id) + if err != nil { + return fmt.Errorf("error decoding ID (%s): %w", id, err) + } + + if _, err = tfec2.WaitIpamPoolDeleted(conn, pool_id, tfec2.IpamPoolDeleteTimeout); err != nil { + if tfresource.NotFound(err) { + return nil + } + return fmt.Errorf("error waiting for IPAM Pool (%s) to be deleted: %w", id, err) + } + } + + return nil +} + +const testAccVPCIpamPoolCidrBase = ` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + description = "test" + operating_regions { + region_name = data.aws_region.current.name + } +} +` + +const testAccVPCIpamPoolCidrPrivatePool = ` +resource "aws_vpc_ipam_pool" "test" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.test.private_default_scope_id + locale = data.aws_region.current.name +} +` + +func testAccVPCIpamProvisionedPoolCidrIpv4(cidr string) string { + return testAccVPCIpamPoolCidrBase + testAccVPCIpamPoolCidrPrivatePool + fmt.Sprintf(` +resource "aws_vpc_ipam_pool_cidr" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = %[1]q +} +`, cidr) +} diff --git a/internal/service/ec2/vpc_ipam_pool_data_source.go b/internal/service/ec2/vpc_ipam_pool_data_source.go new file mode 100644 index 00000000000..86618a1a66e --- /dev/null +++ b/internal/service/ec2/vpc_ipam_pool_data_source.go @@ -0,0 +1,140 @@ +package ec2 + +import ( + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" +) + +func DataSourceVPCIpamPool() *schema.Resource { + return &schema.Resource{ + Read: dataSourceVPCIpamPoolRead, + + Schema: map[string]*schema.Schema{ + "filter": DataSourceFiltersSchema(), + "ipam_pool_id": { + Type: schema.TypeString, + Optional: true, + }, + // computed + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "address_family": { + Type: schema.TypeString, + Computed: true, + }, + "publicly_advertisable": { + Type: schema.TypeBool, + Computed: true, + }, + "allocation_default_netmask_length": { + Type: schema.TypeInt, + Computed: true, + }, + "allocation_max_netmask_length": { + Type: schema.TypeInt, + Computed: true, + }, + "allocation_min_netmask_length": { + Type: schema.TypeInt, + Computed: true, + }, + "allocation_resource_tags": tftags.TagsSchema(), + "auto_import": { + Type: schema.TypeBool, + Computed: true, + }, + "aws_service": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "id": { + Type: schema.TypeString, + Optional: true, + }, + "ipam_scope_id": { + Type: schema.TypeString, + Computed: true, + }, + "ipam_scope_type": { + Type: schema.TypeString, + Computed: true, + }, + "locale": { + Type: schema.TypeString, + Computed: true, + }, + "pool_depth": { + Type: schema.TypeInt, + Computed: true, + }, + "source_ipam_pool_id": { + Type: schema.TypeString, + Computed: true, + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tftags.TagsSchemaComputed(), + }, + } +} + +func dataSourceVPCIpamPoolRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + input := &ec2.DescribeIpamPoolsInput{} + + if v, ok := d.GetOk("ipam_pool_id"); ok { + input.IpamPoolIds = aws.StringSlice([]string{v.(string)}) + + } + + filters, filtersOk := d.GetOk("filter") + if filtersOk { + input.Filters = BuildFiltersDataSource(filters.(*schema.Set)) + } + + output, err := conn.DescribeIpamPools(input) + var pool *ec2.IpamPool + + if err != nil { + return err + } + + if output == nil || len(output.IpamPools) == 0 || output.IpamPools[0] == nil { + return nil + } + pool = output.IpamPools[0] + + d.SetId(aws.StringValue(pool.IpamPoolId)) + + if pool.PubliclyAdvertisable != nil { + d.Set("publicly_advertisable", pool.PubliclyAdvertisable) + } + scopeId := strings.Split(*pool.IpamScopeArn, "/")[1] + + d.Set("allocation_resource_tags", KeyValueTags(ec2TagsFromIpamAllocationTags(pool.AllocationResourceTags)).Map()) + d.Set("auto_import", pool.AutoImport) + d.Set("arn", pool.IpamPoolArn) + d.Set("description", pool.Description) + d.Set("ipam_scope_id", scopeId) + d.Set("ipam_scope_type", pool.IpamScopeType) + d.Set("locale", pool.Locale) + d.Set("pool_depth", pool.PoolDepth) + d.Set("aws_service", pool.AwsService) + d.Set("source_ipam_pool_id", pool.SourceIpamPoolId) + d.Set("state", pool.State) + + return nil +} diff --git a/internal/service/ec2/vpc_ipam_pool_data_source_test.go b/internal/service/ec2/vpc_ipam_pool_data_source_test.go new file mode 100644 index 00000000000..c6f72599d1c --- /dev/null +++ b/internal/service/ec2/vpc_ipam_pool_data_source_test.go @@ -0,0 +1,71 @@ +package ec2_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" +) + +func TestAccDataSourceVPCIpamPool_basic(t *testing.T) { + resourceName := "aws_vpc_ipam_pool.test" + dataSourceName := "data.aws_vpc_ipam_pool.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamPoolOptions, + Check: resource.ComposeTestCheckFunc( + testAccCheckDataSourceVPCIpamPoolID(dataSourceName), + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "auto_import", resourceName, "auto_import"), + resource.TestCheckResourceAttrPair(dataSourceName, "ipam_scope_id", resourceName, "ipam_scope_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "ipam_scope_type", resourceName, "ipam_scope_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "locale", resourceName, "locale"), + resource.TestCheckResourceAttrPair(dataSourceName, "pool_depth", resourceName, "pool_depth"), + resource.TestCheckResourceAttrPair(dataSourceName, "state", resourceName, "state"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags", resourceName, "tags"), + ), + }, + }, + }) +} + +func testAccCheckDataSourceVPCIpamPoolID(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Can't find ipam pool: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("ipam pool ID not set") + } + return nil + } +} + +const testAccVPCIpamPoolOptions = testAccVPCIpamPoolBase + ` +resource "aws_vpc_ipam_pool" "test" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.test.private_default_scope_id + auto_import = true + allocation_default_netmask_length = 32 + allocation_max_netmask_length = 32 + allocation_min_netmask_length = 32 + allocation_resource_tags = { + test = "1" + } + description = "test" +} + +data "aws_vpc_ipam_pool" "test" { + depends_on = [aws_vpc_ipam_pool.test] +} +` diff --git a/internal/service/ec2/vpc_ipam_pool_test.go b/internal/service/ec2/vpc_ipam_pool_test.go new file mode 100644 index 00000000000..82a865fd317 --- /dev/null +++ b/internal/service/ec2/vpc_ipam_pool_test.go @@ -0,0 +1,237 @@ +package ec2_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccVPCIpamPool_basic(t *testing.T) { + var pool ec2.IpamPool + resourceName := "aws_vpc_ipam_pool.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIpamPoolDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamPool, + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCIpamPoolExists(resourceName, &pool), + resource.TestCheckResourceAttr(resourceName, "address_family", "ipv4"), + resource.TestCheckResourceAttr(resourceName, "auto_import", "false"), + resource.TestCheckResourceAttr(resourceName, "locale", "None"), + resource.TestCheckResourceAttr(resourceName, "state", "create-complete"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccVPCIpamPoolUpdates, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "address_family", "ipv4"), + resource.TestCheckResourceAttr(resourceName, "auto_import", "true"), + resource.TestCheckResourceAttr(resourceName, "locale", "None"), + resource.TestCheckResourceAttr(resourceName, "state", "modify-complete"), + resource.TestCheckResourceAttr(resourceName, "allocation_default_netmask_length", "32"), + resource.TestCheckResourceAttr(resourceName, "allocation_max_netmask_length", "32"), + resource.TestCheckResourceAttr(resourceName, "allocation_min_netmask_length", "32"), + resource.TestCheckResourceAttr(resourceName, "allocation_resource_tags.test", "1"), + ), + }, + }, + }) +} + +func TestAccVPCIpamPool_tags(t *testing.T) { + resourceName := "aws_vpc_ipam_pool.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIpamPoolDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamPoolTagsConfig("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccVPCIpamPoolTags2Config("key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccVPCIpamPoolTagsConfig("key2", "value2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckVPCIpamPoolExists(n string, pool *ec2.IpamPool) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + id := rs.Primary.ID + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + found_pool, err := tfec2.FindIpamPoolById(conn, id) + + if err != nil { + return err + } + *pool = *found_pool + + return nil + } +} + +func TestAccVPCIpamPool_ipv6Basic(t *testing.T) { + var pool ec2.IpamPool + resourceName := "aws_vpc_ipam_pool.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIpamPoolDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamPool_ipv6, + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCIpamPoolExists(resourceName, &pool), + resource.TestCheckResourceAttr(resourceName, "address_family", "ipv6"), + resource.TestCheckResourceAttr(resourceName, "auto_import", "false"), + resource.TestCheckResourceAttr(resourceName, "state", "create-complete"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckVPCIpamPoolDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpc_ipam_pool" { + continue + } + + id := rs.Primary.ID + + if _, err := tfec2.WaitIpamPoolDeleted(conn, id, tfec2.IpamPoolDeleteTimeout); err != nil { + if tfresource.NotFound(err) { + return nil + } + return fmt.Errorf("error waiting for IPAM Pool (%s) to be deleted: %w", id, err) + } + } + + return nil +} + +const testAccVPCIpamPoolBase = ` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + description = "test" + operating_regions { + region_name = data.aws_region.current.name + } +} +` + +const testAccVPCIpamPool = testAccVPCIpamPoolBase + ` +resource "aws_vpc_ipam_pool" "test" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.test.private_default_scope_id +} +` + +const testAccVPCIpamPoolUpdates = testAccVPCIpamPoolBase + ` +resource "aws_vpc_ipam_pool" "test" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.test.private_default_scope_id + auto_import = true + allocation_default_netmask_length = 32 + allocation_max_netmask_length = 32 + allocation_min_netmask_length = 32 + allocation_resource_tags = { + test = "1" + } + description = "test" +} +` + +const testAccVPCIpamPool_ipv6 = testAccVPCIpamPoolBase + ` +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 + description = "ipv6 test" + publicly_advertisable = false +} +` + +func testAccVPCIpamPoolTagsConfig(tagKey1, tagValue1 string) string { + return testAccVPCIpamPoolBase + fmt.Sprintf(` +resource "aws_vpc_ipam_pool" "test" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.test.private_default_scope_id + tags = { + %[1]q = %[2]q + } +} +`, tagKey1, tagValue1) +} + +func testAccVPCIpamPoolTags2Config(tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return testAccVPCIpamPoolBase + fmt.Sprintf(` + + +resource "aws_vpc_ipam_pool" "test" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.test.private_default_scope_id + tags = { + %[1]q = %[2]q + %[3]q = %[4]q + } +} + `, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/ec2/vpc_ipam_scope.go b/internal/service/ec2/vpc_ipam_scope.go new file mode 100644 index 00000000000..607ee528554 --- /dev/null +++ b/internal/service/ec2/vpc_ipam_scope.go @@ -0,0 +1,268 @@ +package ec2 + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + 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" +) + +func ResourceVPCIpamScope() *schema.Resource { + return &schema.Resource{ + Create: ResourceVPCIpamScopeCreate, + Read: ResourceVPCIpamScopeRead, + Update: ResourceVPCIpamScopeUpdate, + Delete: ResourceVPCIpamScopeDelete, + CustomizeDiff: customdiff.Sequence(verify.SetTagsDiff), + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "ipam_arn": { + Type: schema.TypeString, + Computed: true, + }, + "ipam_id": { + Type: schema.TypeString, + Required: true, + }, + "ipam_scope_type": { + Type: schema.TypeString, + Computed: true, + }, + "is_default": { + Type: schema.TypeBool, + Computed: true, + }, + "pool_count": { + Type: schema.TypeInt, + Computed: true, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + } +} + +const ( + IpamScopeCreateTimeout = 3 * time.Minute + IpamScopeCreateDeley = 5 * time.Second + IpamScopeDeleteTimeout = 3 * time.Minute + IpamScopeDeleteDelay = 5 * time.Second + + IpamScopeStatusAvailable = "Available" + InvalidIpamScopeIdNotFound = "InvalidIpamScopeId.NotFound" +) + +func ResourceVPCIpamScopeCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + + input := &ec2.CreateIpamScopeInput{ + ClientToken: aws.String(resource.UniqueId()), + IpamId: aws.String(d.Get("ipam_id").(string)), + TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, "ipam-scope"), + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating IPAM Scope: %s", input) + output, err := conn.CreateIpamScope(input) + if err != nil { + return fmt.Errorf("Error creating ipam scope in ipam (%s): %w", d.Get("ipam_id").(string), err) + } + d.SetId(aws.StringValue(output.IpamScope.IpamScopeId)) + log.Printf("[INFO] IPAM Scope ID: %s", d.Id()) + + if _, err = waitIpamScopeAvailable(conn, d.Id(), IpamScopeCreateTimeout); err != nil { + return fmt.Errorf("error waiting for IPAM Scope (%s) to be Available: %w", d.Id(), err) + } + + return ResourceVPCIpamScopeRead(d, meta) +} + +func ResourceVPCIpamScopeRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + scope, err := findIpamScopeById(conn, d.Id()) + ipamId := strings.Split(*scope.IpamArn, "/")[1] + + if err != nil && !tfawserr.ErrCodeEquals(err, InvalidIpamScopeIdNotFound) { + return err + } + + if !d.IsNewResource() && scope == nil { + log.Printf("[WARN] IPAM Scope (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("arn", scope.IpamScopeArn) + d.Set("description", scope.Description) + d.Set("ipam_arn", scope.IpamArn) + d.Set("ipam_id", ipamId) + d.Set("ipam_scope_type", scope.IpamScopeType) + d.Set("is_default", scope.IsDefault) + d.Set("pool_count", scope.PoolCount) + + tags := KeyValueTags(scope.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + + return nil +} + +func ResourceVPCIpamScopeUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + if err := UpdateTags(conn, d.Id(), o, n); err != nil { + return fmt.Errorf("error updating tags: %w", err) + } + } + + if d.HasChange("description") { + // moved `ModifyIpamScope` call here due to bug during development, can likely be moved out of if statement scope later + input := &ec2.ModifyIpamScopeInput{ + IpamScopeId: aws.String(d.Id()), + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + log.Printf("[DEBUG] Updating IPAM scope: %s", input) + _, err := conn.ModifyIpamScope(input) + if err != nil { + return fmt.Errorf("error updating IPAM Scope (%s): %w", d.Id(), err) + } + } + + return ResourceVPCIpamScopeRead(d, meta) +} + +func ResourceVPCIpamScopeDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + input := &ec2.DeleteIpamScopeInput{ + IpamScopeId: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Deleting IPAM Scope: %s", input) + _, err := conn.DeleteIpamScope(input) + if err != nil { + return fmt.Errorf("error deleting IPAM Scope: (%s): %w", d.Id(), err) + } + + if _, err = WaitIpamScopeDeleted(conn, d.Id(), IpamScopeDeleteTimeout); err != nil { + if tfresource.NotFound(err) { + return nil + } + return fmt.Errorf("error waiting for IPAM Scope (%s) to be deleted: %w", d.Id(), err) + } + + return nil +} + +func findIpamScopeById(conn *ec2.EC2, id string) (*ec2.IpamScope, error) { + input := &ec2.DescribeIpamScopesInput{ + IpamScopeIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeIpamScopes(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.IpamScopes) == 0 || output.IpamScopes[0] == nil { + return nil, nil + } + + return output.IpamScopes[0], nil +} + +func waitIpamScopeAvailable(conn *ec2.EC2, ipamScopeId string, timeout time.Duration) (*ec2.IpamScope, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.IpamScopeStateCreateInProgress}, + Target: []string{ec2.IpamScopeStateCreateComplete}, + Refresh: statusIpamScopeStatus(conn, ipamScopeId), + Timeout: timeout, + Delay: IpamScopeCreateDeley, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.IpamScope); ok { + return output, err + } + + return nil, err +} + +func WaitIpamScopeDeleted(conn *ec2.EC2, ipamScopeId string, timeout time.Duration) (*ec2.IpamScope, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.IpamScopeStateCreateComplete, ec2.IpamScopeStateModifyComplete}, + Target: []string{InvalidIpamScopeIdNotFound, ec2.IpamScopeStateDeleteComplete}, + Refresh: statusIpamScopeStatus(conn, ipamScopeId), + Timeout: timeout, + Delay: IpamScopeDeleteDelay, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.IpamScope); ok { + return output, err + } + + return nil, err +} + +func statusIpamScopeStatus(conn *ec2.EC2, ipamScopeId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + + output, err := findIpamScopeById(conn, ipamScopeId) + + if tfawserr.ErrCodeEquals(err, InvalidIpamScopeIdNotFound) { + return output, InvalidIpamScopeIdNotFound, nil + } + + // there was an unhandled error in the Finder + if err != nil { + return nil, "", err + } + + return output, ec2.IpamScopeStateCreateComplete, nil + } +} diff --git a/internal/service/ec2/vpc_ipam_scope_test.go b/internal/service/ec2/vpc_ipam_scope_test.go new file mode 100644 index 00000000000..8082aed17ce --- /dev/null +++ b/internal/service/ec2/vpc_ipam_scope_test.go @@ -0,0 +1,158 @@ +package ec2_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccVPCIpamScope_basic(t *testing.T) { + resourceName := "aws_vpc_ipam_scope.test" + ipamName := "aws_vpc_ipam.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIpamScopeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamScope("test"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", "test"), + resource.TestCheckResourceAttr(resourceName, "pool_count", "0"), + resource.TestCheckResourceAttr(resourceName, "is_default", "false"), + resource.TestCheckResourceAttrPair(resourceName, "ipam_arn", ipamName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "ipam_id", ipamName, "id"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccVPCIpamScope("test2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", "test2"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + }, + }) +} + +func TestAccVPCIpamScope_tags(t *testing.T) { + resourceName := "aws_vpc_ipam_scope.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIpamScopeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamScopeTagsConfig("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccVPCIpamScopeTags2Config("key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccVPCIpamScopeTagsConfig("key2", "value2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckVPCIpamScopeDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpc_ipam_scope" { + continue + } + + id := aws.String(rs.Primary.ID) + + if _, err := tfec2.WaitIpamScopeDeleted(conn, *id, tfec2.IpamScopeDeleteTimeout); err != nil { + if tfresource.NotFound(err) { + return nil + } + return fmt.Errorf("error waiting for IPAM Scope (%s) to be deleted: %w", *id, err) + } + } + + return nil +} + +const testAccVPCIpamScopeBase = ` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + description = "test" + operating_regions { + region_name = data.aws_region.current.name + } +} +` + +func testAccVPCIpamScope(desc string) string { + return testAccVPCIpamScopeBase + fmt.Sprintf(` +resource "aws_vpc_ipam_scope" "test" { + ipam_id = aws_vpc_ipam.test.id + description = %[1]q +} +`, desc) +} + +func testAccVPCIpamScopeTagsConfig(tagKey1, tagValue1 string) string { + return testAccVPCIpamScopeBase + fmt.Sprintf(` +resource "aws_vpc_ipam_scope" "test" { + ipam_id = aws_vpc_ipam.test.id + tags = { + %[1]q = %[2]q + } +} +`, tagKey1, tagValue1) +} + +func testAccVPCIpamScopeTags2Config(tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return testAccVPCIpamScopeBase + fmt.Sprintf(` + + +resource "aws_vpc_ipam_scope" "test" { + ipam_id = aws_vpc_ipam.test.id + tags = { + %[1]q = %[2]q + %[3]q = %[4]q + } +} + `, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/ec2/vpc_ipam_test.go b/internal/service/ec2/vpc_ipam_test.go new file mode 100644 index 00000000000..cf813dfae5e --- /dev/null +++ b/internal/service/ec2/vpc_ipam_test.go @@ -0,0 +1,213 @@ +package ec2_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccVPCIpam_basic(t *testing.T) { + resourceName := "aws_vpc_ipam.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIpamDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamBase, + Check: resource.ComposeTestCheckFunc( + acctest.MatchResourceAttrGlobalARN(resourceName, "arn", "ec2", regexp.MustCompile(`ipam/ipam-[\da-f]+$`)), + resource.TestCheckResourceAttr(resourceName, "operating_regions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scope_count", "2"), + resource.TestMatchResourceAttr(resourceName, "private_default_scope_id", regexp.MustCompile(`^ipam-scope-[\da-f]+`)), + resource.TestMatchResourceAttr(resourceName, "public_default_scope_id", regexp.MustCompile(`^ipam-scope-[\da-f]+`)), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccVPCIpam_modifyRegion(t *testing.T) { + var providers []*schema.Provider + resourceName := "aws_vpc_ipam.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckMultipleRegion(t, 2) + }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + ProviderFactories: acctest.FactoriesMultipleRegion(&providers, 2), + CheckDestroy: testAccCheckVPCIpamDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamBase, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", "test"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccVPCIpamOperatingRegion(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", "test ipam"), + ), + }, + { + Config: testAccVPCIpamBase, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", "test"), + ), + }, + }, + }) +} + +func TestAccVPCIpam_tags(t *testing.T) { + resourceName := "aws_vpc_ipam.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIpamDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIpamTagsConfig("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccVPCIpamTags2Config("key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccVPCIpamTagsConfig("key2", "value2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckVPCIpamDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpc_ipam" { + continue + } + + id := aws.String(rs.Primary.ID) + + if _, err := tfec2.WaiterIpamDeleted(conn, *id, tfec2.IpamDeleteTimeout); err != nil { + if tfresource.NotFound(err) { + return nil + } + return fmt.Errorf("error waiting for IPAM to be deleted: %w", err) + } + } + + return nil +} + +const testAccVPCIpamBase = ` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + description = "test" + operating_regions { + region_name = data.aws_region.current.name + } +} +` + +func testAccVPCIpamOperatingRegion() string { + return acctest.ConfigCompose( + acctest.ConfigMultipleRegionProvider(2), ` +data "aws_region" "current" {} + +data "aws_region" "alternate" { + provider = awsalternate +} + + +resource "aws_vpc_ipam" "test" { + description = "test ipam" + operating_regions { + region_name = data.aws_region.current.name + } + operating_regions { + region_name = data.aws_region.alternate.name + } +} +`) +} + +func testAccVPCIpamTagsConfig(tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + description = "test" + operating_regions { + region_name = data.aws_region.current.name + } + tags = { + %[1]q = %[2]q + } +} +`, tagKey1, tagValue1) +} + +func testAccVPCIpamTags2Config(tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + description = "test" + operating_regions { + region_name = data.aws_region.current.name + } + tags = { + %[1]q = %[2]q + %[3]q = %[4]q + } +} + `, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/ec2/vpc_ipv4_cidr_block_association.go b/internal/service/ec2/vpc_ipv4_cidr_block_association.go index 32da1575b1a..9e17c12243e 100644 --- a/internal/service/ec2/vpc_ipv4_cidr_block_association.go +++ b/internal/service/ec2/vpc_ipv4_cidr_block_association.go @@ -1,6 +1,7 @@ package ec2 import ( + "context" "fmt" "log" "time" @@ -26,19 +27,40 @@ func ResourceVPCIPv4CIDRBlockAssociation() *schema.Resource { Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, - + CustomizeDiff: func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + // cidr_block can be set by a value returned from IPAM or explicitly in config + if diff.Id() != "" && diff.HasChange("cidr_block") { + // if netmask is set then cidr_block is derived from ipam, ignore changes + if diff.Get("ipv4_netmask_length") != 0 { + return diff.Clear("cidr_block") + } + return diff.ForceNew("cidr_block") + } + return nil + }, Schema: map[string]*schema.Schema{ "vpc_id": { Type: schema.TypeString, Required: true, ForceNew: true, }, - "cidr_block": { Type: schema.TypeString, - Required: true, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.IsCIDRNetwork(VPCCIDRMinIPv4, VPCCIDRMaxIPv4), + }, + "ipv4_ipam_pool_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "ipv4_netmask_length": { + Type: schema.TypeInt, + Optional: true, ForceNew: true, - ValidateFunc: validation.IsCIDRNetwork(16, 28), // The allowed block size is between a /28 netmask and /16 netmask. + ValidateFunc: validation.IntBetween(VPCCIDRMinIPv4, VPCCIDRMaxIPv4), }, }, @@ -53,9 +75,21 @@ func resourceVPCIPv4CIDRBlockAssociationCreate(d *schema.ResourceData, meta inte conn := meta.(*conns.AWSClient).EC2Conn req := &ec2.AssociateVpcCidrBlockInput{ - VpcId: aws.String(d.Get("vpc_id").(string)), - CidrBlock: aws.String(d.Get("cidr_block").(string)), + VpcId: aws.String(d.Get("vpc_id").(string)), } + + if v, ok := d.GetOk("cidr_block"); ok { + req.CidrBlock = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ipv4_ipam_pool_id"); ok { + req.Ipv4IpamPoolId = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ipv4_netmask_length"); ok { + req.Ipv4NetmaskLength = aws.Int64(int64(v.(int))) + } + log.Printf("[DEBUG] Creating VPC IPv4 CIDR block association: %#v", req) resp, err := conn.AssociateVpcCidrBlock(req) if err != nil { diff --git a/internal/service/ec2/vpc_ipv4_cidr_block_association_test.go b/internal/service/ec2/vpc_ipv4_cidr_block_association_test.go index bd1f9f4f2af..4b810c24ad1 100644 --- a/internal/service/ec2/vpc_ipv4_cidr_block_association_test.go +++ b/internal/service/ec2/vpc_ipv4_cidr_block_association_test.go @@ -2,6 +2,7 @@ package ec2_test import ( "fmt" + "strings" "testing" "github.com/aws/aws-sdk-go/aws" @@ -13,7 +14,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/conns" ) -func TestAccEC2VPCIPv4CIDRBlockAssociation_basic(t *testing.T) { +func TestAccVPCIPv4CIDRBlockAssociation_basic(t *testing.T) { var associationSecondary, associationTertiary ec2.VpcCidrBlockAssociation resource.ParallelTest(t, resource.TestCase{ @@ -45,6 +46,47 @@ func TestAccEC2VPCIPv4CIDRBlockAssociation_basic(t *testing.T) { }) } +func TestAccVPCIPv4CIDRBlockAssociation_IpamBasic(t *testing.T) { + var associationSecondary ec2.VpcCidrBlockAssociation + netmaskLength := "28" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIPv4CIDRBlockAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIPv4CIDRBlockAssociationIpam(netmaskLength), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCIPv4CIDRBlockAssociationExists("aws_vpc_ipv4_cidr_block_association.secondary_cidr", &associationSecondary), + testAccCheckVPCAssociationCIDRPrefix(&associationSecondary, netmaskLength), + ), + }, + }, + }) +} + +func TestAccVPCIPv4CIDRBlockAssociation_IpamBasicExplicitCIDR(t *testing.T) { + var associationSecondary ec2.VpcCidrBlockAssociation + cidr := "172.2.0.32/28" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIPv4CIDRBlockAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIPv4CIDRBlockAssociationIpamExplicitCIDR(cidr), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCIPv4CIDRBlockAssociationExists("aws_vpc_ipv4_cidr_block_association.secondary_cidr", &associationSecondary), + testAccCheckAdditionalVPCIPv4CIDRBlock(&associationSecondary, cidr)), + }, + }, + }) +} + func testAccCheckAdditionalVPCIPv4CIDRBlock(association *ec2.VpcCidrBlockAssociation, expected string) resource.TestCheckFunc { return func(s *terraform.State) error { CIDRBlock := association.CidrBlock @@ -56,6 +98,16 @@ func testAccCheckAdditionalVPCIPv4CIDRBlock(association *ec2.VpcCidrBlockAssocia } } +func testAccCheckVPCAssociationCIDRPrefix(association *ec2.VpcCidrBlockAssociation, expected string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if strings.Split(aws.StringValue(association.CidrBlock), "/")[1] != expected { + return fmt.Errorf("Bad cidr prefix: %s", aws.StringValue(association.CidrBlock)) + } + + return nil + } +} + func testAccCheckVPCIPv4CIDRBlockAssociationDestroy(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn @@ -153,3 +205,45 @@ resource "aws_vpc_ipv4_cidr_block_association" "tertiary_cidr" { cidr_block = "170.2.0.0/16" } ` + +func testAccVPCIPv4CIDRBlockAssociationIpam(netmaskLength string) string { + return testAccVpcIpamBase + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = "terraform-testacc-vpc-ipv4-cidr-block-association" + } +} + +resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" { + ipv4_ipam_pool_id = aws_vpc_ipam_pool.test.id + ipv4_netmask_length = %[1]q + vpc_id = aws_vpc.test.id + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} +`, netmaskLength) +} + +func testAccVPCIPv4CIDRBlockAssociationIpamExplicitCIDR(cidr string) string { + return testAccVpcIpamBase + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = "terraform-testacc-vpc-ipv4-cidr-block-association" + } +} + +resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" { + ipv4_ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr_block = %[1]q + vpc_id = aws_vpc.test.id + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} +`, cidr) +} diff --git a/internal/service/ec2/vpc_ipv6_cidr_block_association.go b/internal/service/ec2/vpc_ipv6_cidr_block_association.go new file mode 100644 index 00000000000..5d9714ce3b9 --- /dev/null +++ b/internal/service/ec2/vpc_ipv6_cidr_block_association.go @@ -0,0 +1,219 @@ +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/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "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/verify" +) + +// acceptance tests for byoip related tests are in vpc_byoip_test.go +func ResourceVPCIPv6CIDRBlockAssociation() *schema.Resource { + return &schema.Resource{ + Create: resourceVPCIPv6CIDRBlockAssociationCreate, + Read: resourceVPCIPv6CIDRBlockAssociationRead, + Delete: resourceVPCIPv6CIDRBlockAssociationDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + CustomizeDiff: func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + // ipv6_cidr_block can be set by a value returned from IPAM or explicitly in config + if diff.Id() != "" && diff.HasChange("ipv6_cidr_block") { + // if netmask is set then ipv6_cidr_block is derived from ipam, ignore changes + if diff.Get("ipv6_netmask_length") != 0 { + return diff.Clear("ipv6_cidr_block") + } + return diff.ForceNew("ipv6_cidr_block") + } + return nil + }, + Schema: map[string]*schema.Schema{ + "vpc_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "ipv6_cidr_block": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.Any( + validation.StringIsEmpty, + validation.All( + verify.ValidIPv6CIDRNetworkAddress, + validation.IsCIDRNetwork(VPCCIDRMaxIPv6, VPCCIDRMaxIPv6)), + ), + }, + // ipam parameters are not required by the API but other usage mechanisms are not implemented yet. TODO ipv6 options: + // --amazon-provided-ipv6-cidr-block + // --ipv6-pool + "ipv6_ipam_pool_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "ipv6_netmask_length": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntInSlice([]int{VPCCIDRMaxIPv6}), + ConflictsWith: []string{"ipv6_cidr_block"}, + // This RequiredWith setting should be applied once L57 is completed + // RequiredWith: []string{"ipv6_ipam_pool_id"}, + }, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + } +} + +func resourceVPCIPv6CIDRBlockAssociationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + req := &ec2.AssociateVpcCidrBlockInput{ + VpcId: aws.String(d.Get("vpc_id").(string)), + } + + if v, ok := d.GetOk("ipv6_cidr_block"); ok { + req.Ipv6CidrBlock = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ipv6_ipam_pool_id"); ok { + req.Ipv6IpamPoolId = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ipv6_netmask_length"); ok { + req.Ipv6NetmaskLength = aws.Int64(int64(v.(int))) + } + + log.Printf("[DEBUG] Creating VPC IPv6 CIDR block association: %#v", req) + resp, err := conn.AssociateVpcCidrBlock(req) + if err != nil { + return fmt.Errorf("Error creating VPC IPv6 CIDR block association: %s", err) + } + + d.SetId(aws.StringValue(resp.Ipv6CidrBlockAssociation.AssociationId)) + + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.VpcCidrBlockStateCodeAssociating}, + Target: []string{ec2.VpcCidrBlockStateCodeAssociated}, + Refresh: vpcIpv6CidrBlockAssociationStateRefresh(conn, d.Get("vpc_id").(string), d.Id()), + Timeout: d.Timeout(schema.TimeoutCreate), + Delay: 10 * time.Second, + MinTimeout: 5 * time.Second, + } + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for IPv6 CIDR block association (%s) to become available: %s", d.Id(), err) + } + + return resourceVPCIPv6CIDRBlockAssociationRead(d, meta) +} + +func resourceVPCIPv6CIDRBlockAssociationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + input := &ec2.DescribeVpcsInput{ + Filters: BuildAttributeFilterList( + map[string]string{ + "ipv6-cidr-block-association.association-id": d.Id(), + }, + ), + } + + log.Printf("[DEBUG] Describing VPCs: %s", input) + output, err := conn.DescribeVpcs(input) + if err != nil { + return fmt.Errorf("error describing VPCs: %s", err) + } + + if output == nil || len(output.Vpcs) == 0 || output.Vpcs[0] == nil { + log.Printf("[WARN] IPv6 CIDR block association (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + vpc := output.Vpcs[0] + + var vpcIpv6CidrBlockAssociation *ec2.VpcIpv6CidrBlockAssociation + for _, ipv6CidrBlockAssociation := range vpc.Ipv6CidrBlockAssociationSet { + if aws.StringValue(ipv6CidrBlockAssociation.AssociationId) == d.Id() { + vpcIpv6CidrBlockAssociation = ipv6CidrBlockAssociation + break + } + } + + if vpcIpv6CidrBlockAssociation == nil { + log.Printf("[WARN] IPv6 CIDR block association (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("ipv6_cidr_block", vpcIpv6CidrBlockAssociation.Ipv6CidrBlock) + d.Set("vpc_id", vpc.VpcId) + + return nil +} + +func resourceVPCIPv6CIDRBlockAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + log.Printf("[DEBUG] Deleting VPC IPv6 CIDR block association: %s", d.Id()) + _, err := conn.DisassociateVpcCidrBlock(&ec2.DisassociateVpcCidrBlockInput{ + AssociationId: aws.String(d.Id()), + }) + if err != nil { + if tfawserr.ErrMessageContains(err, "InvalidVpcID.NotFound", "") { + return nil + } + return fmt.Errorf("Error deleting VPC IPv6 CIDR block association: %s", err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.VpcCidrBlockStateCodeDisassociating}, + Target: []string{ec2.VpcCidrBlockStateCodeDisassociated, VpcCidrBlockStateCodeDeleted}, + Refresh: vpcIpv6CidrBlockAssociationStateRefresh(conn, d.Get("vpc_id").(string), d.Id()), + Timeout: d.Timeout(schema.TimeoutDelete), + Delay: 10 * time.Second, + MinTimeout: 5 * time.Second, + } + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for VPC IPv6 CIDR block association (%s) to be deleted: %s", d.Id(), err.Error()) + } + + return nil +} + +func vpcIpv6CidrBlockAssociationStateRefresh(conn *ec2.EC2, vpcId, assocId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + vpc, err := vpcDescribe(conn, vpcId) + if err != nil { + return nil, "", err + } + + if vpc != nil { + for _, ipv6CidrAssociation := range vpc.Ipv6CidrBlockAssociationSet { + if aws.StringValue(ipv6CidrAssociation.AssociationId) == assocId { + return ipv6CidrAssociation, aws.StringValue(ipv6CidrAssociation.Ipv6CidrBlockState.State), nil + } + } + } + + return "", VpcCidrBlockStateCodeDeleted, nil + } +} diff --git a/internal/service/ec2/vpc_test.go b/internal/service/ec2/vpc_test.go index 13638af5c4b..b54534644c4 100644 --- a/internal/service/ec2/vpc_test.go +++ b/internal/service/ec2/vpc_test.go @@ -3,6 +3,7 @@ package ec2_test import ( "fmt" "regexp" + "strings" "testing" "github.com/aws/aws-sdk-go/aws" @@ -18,7 +19,7 @@ import ( // add sweeper to delete known test vpcs -func TestAccEC2VPC_basic(t *testing.T) { +func TestAccVPC_basic(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -55,7 +56,7 @@ func TestAccEC2VPC_basic(t *testing.T) { }) } -func TestAccEC2VPC_disappears(t *testing.T) { +func TestAccVPC_disappears(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -77,7 +78,7 @@ func TestAccEC2VPC_disappears(t *testing.T) { }) } -func TestAccEC2VPC_DefaultTags_providerOnly(t *testing.T) { +func TestAccVPC_DefaultTags_providerOnly(t *testing.T) { var providers []*schema.Provider var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -134,7 +135,7 @@ func TestAccEC2VPC_DefaultTags_providerOnly(t *testing.T) { }) } -func TestAccEC2VPC_DefaultTags_updateToProviderOnly(t *testing.T) { +func TestAccVPC_DefaultTags_updateToProviderOnly(t *testing.T) { var providers []*schema.Provider var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -176,7 +177,7 @@ func TestAccEC2VPC_DefaultTags_updateToProviderOnly(t *testing.T) { }) } -func TestAccEC2VPC_DefaultTags_updateToResourceOnly(t *testing.T) { +func TestAccVPC_DefaultTags_updateToResourceOnly(t *testing.T) { var providers []*schema.Provider var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -218,7 +219,7 @@ func TestAccEC2VPC_DefaultTags_updateToResourceOnly(t *testing.T) { }) } -func TestAccEC2VPC_DefaultTagsProviderAndResource_nonOverlappingTag(t *testing.T) { +func TestAccVPC_DefaultTagsProviderAndResource_nonOverlappingTag(t *testing.T) { var providers []*schema.Provider var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -282,7 +283,7 @@ func TestAccEC2VPC_DefaultTagsProviderAndResource_nonOverlappingTag(t *testing.T }) } -func TestAccEC2VPC_DefaultTagsProviderAndResource_overlappingTag(t *testing.T) { +func TestAccVPC_DefaultTagsProviderAndResource_overlappingTag(t *testing.T) { var providers []*schema.Provider var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -342,7 +343,7 @@ func TestAccEC2VPC_DefaultTagsProviderAndResource_overlappingTag(t *testing.T) { }) } -func TestAccEC2VPC_DefaultTagsProviderAndResource_duplicateTag(t *testing.T) { +func TestAccVPC_DefaultTagsProviderAndResource_duplicateTag(t *testing.T) { var providers []*schema.Provider resource.ParallelTest(t, resource.TestCase{ @@ -363,12 +364,12 @@ func TestAccEC2VPC_DefaultTagsProviderAndResource_duplicateTag(t *testing.T) { }) } -// TestAccEC2VPC_DynamicResourceTagsMergedWithLocals_ignoreChanges ensures computed "tags_all" +// TestAccVPC_DynamicResourceTagsMergedWithLocals_ignoreChanges ensures computed "tags_all" // attributes are correctly determined when the provider-level default_tags block // is left unused and resource tags (merged with local.tags) are only known at apply time, // with additional lifecycle ignore_changes attributes, thereby eliminating "Inconsistent final plan" errors // Reference: https://github.com/hashicorp/terraform-provider-aws/issues/18366 -func TestAccEC2VPC_DynamicResourceTagsMergedWithLocals_ignoreChanges(t *testing.T) { +func TestAccVPC_DynamicResourceTagsMergedWithLocals_ignoreChanges(t *testing.T) { var providers []*schema.Provider var vpc ec2.Vpc @@ -419,12 +420,12 @@ func TestAccEC2VPC_DynamicResourceTagsMergedWithLocals_ignoreChanges(t *testing. }) } -// TestAccEC2VPC_DynamicResourceTags_ignoreChanges ensures computed "tags_all" +// TestAccVPC_DynamicResourceTags_ignoreChanges ensures computed "tags_all" // attributes are correctly determined when the provider-level default_tags block // is left unused and resource tags are only known at apply time, // with additional lifecycle ignore_changes attributes, thereby eliminating "Inconsistent final plan" errors // Reference: https://github.com/hashicorp/terraform-provider-aws/issues/18366 -func TestAccEC2VPC_DynamicResourceTags_ignoreChanges(t *testing.T) { +func TestAccVPC_DynamicResourceTags_ignoreChanges(t *testing.T) { var providers []*schema.Provider var vpc ec2.Vpc @@ -471,7 +472,7 @@ func TestAccEC2VPC_DynamicResourceTags_ignoreChanges(t *testing.T) { }) } -func TestAccEC2VPC_defaultAndIgnoreTags(t *testing.T) { +func TestAccVPC_defaultAndIgnoreTags(t *testing.T) { var providers []*schema.Provider var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -508,7 +509,7 @@ func TestAccEC2VPC_defaultAndIgnoreTags(t *testing.T) { }) } -func TestAccEC2VPC_ignoreTags(t *testing.T) { +func TestAccVPC_ignoreTags(t *testing.T) { var providers []*schema.Provider var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -539,7 +540,7 @@ func TestAccEC2VPC_ignoreTags(t *testing.T) { }) } -func TestAccEC2VPC_assignGeneratedIPv6CIDRBlock(t *testing.T) { +func TestAccVPC_assignGeneratedIPv6CIDRBlock(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -561,9 +562,10 @@ func TestAccEC2VPC_assignGeneratedIPv6CIDRBlock(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"assign_generated_ipv6_cidr_block"}, }, { Config: testAccVpcConfigAssignGeneratedIpv6CidrBlock(false), @@ -591,7 +593,7 @@ func TestAccEC2VPC_assignGeneratedIPv6CIDRBlock(t *testing.T) { }) } -func TestAccEC2VPC_tenancy(t *testing.T) { +func TestAccVPC_tenancy(t *testing.T) { var vpcDedicated ec2.Vpc var vpcDefault ec2.Vpc resourceName := "aws_vpc.test" @@ -634,7 +636,50 @@ func TestAccEC2VPC_tenancy(t *testing.T) { }) } -func TestAccEC2VPC_tags(t *testing.T) { +func TestAccVPC_IpamIpv4BasicNetmask(t *testing.T) { + var vpc ec2.Vpc + resourceName := "aws_vpc.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVpcDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVpcIpamIpv4(28), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + testAccCheckVpcCidrPrefix(&vpc, "28"), + ), + }, + }, + }) +} + +func TestAccVPC_IpamIpv4BasicExplicitCidr(t *testing.T) { + var vpc ec2.Vpc + resourceName := "aws_vpc.test" + cidr := "172.2.0.32/28" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVpcDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVpcIpamIpv4ExplicitCidr(cidr), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + testAccCheckVpcCidr(&vpc, cidr), + ), + }, + }, + }) +} + +func TestAccVPC_tags(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -678,7 +723,7 @@ func TestAccEC2VPC_tags(t *testing.T) { }) } -func TestAccEC2VPC_update(t *testing.T) { +func TestAccVPC_update(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -759,6 +804,16 @@ func testAccCheckVpcCidr(vpc *ec2.Vpc, expected string) resource.TestCheckFunc { } } +func testAccCheckVpcCidrPrefix(vpc *ec2.Vpc, expected string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if strings.Split(aws.StringValue(vpc.CidrBlock), "/")[1] != expected { + return fmt.Errorf("Bad cidr prefix: got %s, expected %s", aws.StringValue(vpc.CidrBlock), expected) + } + + return nil + } +} + func testAccCheckVpcIdsEqual(vpc1, vpc2 *ec2.Vpc) resource.TestCheckFunc { return func(s *terraform.State) error { if aws.StringValue(vpc1.VpcId) != aws.StringValue(vpc2.VpcId) { @@ -794,7 +849,7 @@ func testAccCheckVpcDisappears(vpc *ec2.Vpc) resource.TestCheckFunc { } // https://github.com/hashicorp/terraform/issues/1301 -func TestAccEC2VPC_bothDNSOptionsSet(t *testing.T) { +func TestAccVPC_bothDNSOptionsSet(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -822,7 +877,7 @@ func TestAccEC2VPC_bothDNSOptionsSet(t *testing.T) { } // https://github.com/hashicorp/terraform/issues/10168 -func TestAccEC2VPC_disabledDNSSupport(t *testing.T) { +func TestAccVPC_disabledDNSSupport(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -848,7 +903,7 @@ func TestAccEC2VPC_disabledDNSSupport(t *testing.T) { }) } -func TestAccEC2VPC_classicLinkOptionSet(t *testing.T) { +func TestAccVPC_classicLinkOptionSet(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -874,7 +929,7 @@ func TestAccEC2VPC_classicLinkOptionSet(t *testing.T) { }) } -func TestAccEC2VPC_classicLinkDNSSupportOptionSet(t *testing.T) { +func TestAccVPC_classicLinkDNSSupportOptionSet(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -1053,3 +1108,47 @@ resource "aws_vpc" "test" { } } ` +const testAccVpcIpamBase = ` +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +resource "aws_vpc_ipam_pool" "test" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.test.private_default_scope_id + locale = data.aws_region.current.name +} + +resource "aws_vpc_ipam_pool_cidr" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = "172.2.0.0/16" +} +` + +func testAccVpcIpamIpv4(netmaskLength int) string { + return testAccVpcIpamBase + fmt.Sprintf(` +resource "aws_vpc" "test" { + ipv4_ipam_pool_id = aws_vpc_ipam_pool.test.id + ipv4_netmask_length = %[1]d + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} +`, netmaskLength) +} + +func testAccVpcIpamIpv4ExplicitCidr(cidr string) string { + return testAccVpcIpamBase + fmt.Sprintf(` +resource "aws_vpc" "test" { + ipv4_ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr_block = %[1]q + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} +`, cidr) +} diff --git a/internal/verify/validate.go b/internal/verify/validate.go index 2b3ecb552b4..b85c51f9b46 100644 --- a/internal/verify/validate.go +++ b/internal/verify/validate.go @@ -229,6 +229,21 @@ func ValidOnceAWeekWindowFormat(v interface{}, k string) (ws []string, errors [] return } +func ValidRegionName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if value == "" { + return ws, errors + } + if !regionRegexp.MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q region name is malformed(%q): %q", + k, regionRegexp, value)) + } + + return +} + func ValidStringIsJSONOrYAML(v interface{}, k string) (ws []string, errors []error) { if looksLikeJSONString(v) { if _, err := structure.NormalizeJsonString(v); err != nil { diff --git a/website/docs/d/vpc_ipam_pool.html.markdown b/website/docs/d/vpc_ipam_pool.html.markdown new file mode 100644 index 00000000000..173c6af5af2 --- /dev/null +++ b/website/docs/d/vpc_ipam_pool.html.markdown @@ -0,0 +1,64 @@ +--- +subcategory: "VPC" +layout: "aws" +page_title: "AWS: aws_vpc_ipam_pool" +description: |- + Returns details about the first IPAM pool that matches search parameters provided. +--- + +# Data Source: aws_vpc_ipam_pool + +`aws_vpc_ipam_pool` provides details about an IPAM pool. + +This resource can prove useful when an ipam pool was created in another root +module and you need the pool's id as an input variable. For example, pools +can be shared via RAM and used to create vpcs with CIDRs from that pool. + +## Example Usage + +The following example shows an account that has only 1 pool, perhaps shared +via RAM, and using that pool id to create a VPC with a CIDR derived from +AWS IPAM. + +```terraform +data "aws_vpc_ipam_pool" "test" {} + +resource "aws_vpc" "test" { + ipv4_ipam_pool_id = data.aws_vpc_ipam_pool.test.id + ipv4_netmask_length = 28 +} +``` + +## Argument Reference + +The arguments of this data source act as filters for querying the available +VPCs in the current region. The given filters must match exactly one +VPC whose data will be exported as attributes. + +* `id` - +* `filter` - Custom filter block as described below. + +## Attributes Reference + +All of the argument attributes except `filter` blocks are also exported as +result attributes. This data source will complete the data by populating +any fields that are not included in the configuration with the data for +the selected VPC. + +The following attribute is additionally exported: + + +* `address_family` - The IP protocol assigned to this pool. +* `publicly_advertisable` - Defines whether or not IPv6 pool space is publicly ∂advertisable over the internet. +* `allocation_default_netmask_length` - A default netmask length for allocations added to this pool. If, for example, the CIDR assigned to this pool is 10.0.0.0/8 and you enter 16 here, new allocations will default to 10.0.0.0/16. +* `allocation_max_netmask_length` - The maximum netmask length that will be required for CIDR allocations in this pool. +* `allocation_min_netmask_length` - The minimum netmask length that will be required for CIDR allocations in this pool. +* `allocation_resource_tags` - Tags that are required to create resources in using this pool. +* `arn` - Amazon Resource Name (ARN) of the pool +* `auto_import` - If enabled, IPAM will continuously look for resources within the CIDR range of this pool and automatically import them as allocations into your IPAM. +* `aws_service` - Limits which service in AWS that the pool can be used in. "ec2", for example, allows users to use space for Elastic IP addresses and VPCs. +* `description` - A description for the IPAM pool. +* `ipam_scope_id` - The ID of the scope the pool belongs to. +* `locale` - Locale is the Region where your pool is 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. +* `source_ipam_pool_id` - The ID of the source IPAM pool. +* `tags` - A map of tags to assigned to the resource. diff --git a/website/docs/r/vpc.html.markdown b/website/docs/r/vpc.html.markdown index 6b686cf5d88..aacb1779a0e 100644 --- a/website/docs/r/vpc.html.markdown +++ b/website/docs/r/vpc.html.markdown @@ -33,13 +33,45 @@ resource "aws_vpc" "main" { } ``` +VPC with CIDR from AWS IPAM: + +```terraform +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "test" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +resource "aws_vpc_ipam_pool" "test" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.test.private_default_scope_id + locale = data.aws_region.current.name +} + +resource "aws_vpc_ipam_pool_cidr" "test" { + ipam_pool_id = aws_vpc_ipam_pool.test.id + cidr = "172.2.0.0/16" +} + +resource "aws_vpc" "test" { + ipv4_ipam_pool_id = aws_vpc_ipam_pool.test.id + ipv4_netmask_length = "28" + depends_on = [ + aws_vpc_ipam_pool_cidr.test + ] +} +``` + ## Argument Reference The following arguments are supported: -* `cidr_block` - (Required) The CIDR block for the VPC. -* `instance_tenancy` - (Optional) A tenancy option for instances launched into the VPC. Default is `default`, which - makes your instances shared on the host. Using either of the other options (`dedicated` or `host`) costs at least $2/hr. +* `cidr_block` - (Optional) The IPv4 CIDR block for the VPC. CIDR can be explicitly set or it can be derived from IPAM using `ipv4_netmask_length`. +* `instance_tenancy` - (Optional) A tenancy option for instances launched into the VPC. Default is `default`, which makes your instances shared on the host. Using either of the other options (`dedicated` or `host`) costs at least $2/hr. +* `ipv4_ipam_pool_id` - (Optional) The ID of an IPv4 IPAM pool you want to use for allocating this VPC's CIDR. IPAM is a VPC feature that you can use to automate your IP address management workflows including assigning, tracking, troubleshooting, and auditing IP addresses across AWS Regions and accounts. Using IPAM you can monitor IP address usage throughout your AWS Organization. +* `ipv4_netmask_length` - (Optional) The netmask length of the IPv4 CIDR you want to allocate to this VPC. Requires specifying a `ipv4_ipam_pool_id`. * `enable_dns_support` - (Optional) A boolean flag to enable/disable DNS support in the VPC. Defaults true. * `enable_dns_hostnames` - (Optional) A boolean flag to enable/disable DNS hostnames in the VPC. Defaults false. * `enable_classiclink` - (Optional) A boolean flag to enable/disable ClassicLink diff --git a/website/docs/r/vpc_ipam.html.markdown b/website/docs/r/vpc_ipam.html.markdown new file mode 100644 index 00000000000..e73be7d8f37 --- /dev/null +++ b/website/docs/r/vpc_ipam.html.markdown @@ -0,0 +1,82 @@ +--- +subcategory: "VPC" +layout: "aws" +page_title: "AWS: aws_vpc_ipam" +description: |- + Provides a IPAM resource. +--- + +# Resource: aws_vpc_ipam + +Provides a IPAM resource. + +## Example Usage + +Basic usage: + +```terraform +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "main" { + description = "My IPAM" + operating_regions { + region_name = data.aws_region.current.name + } + + tags = { + Test = "Main" + } +} +``` + +Shared with multiple operating_regions: + +```terraform +variable "ipam_regions" { + type = list + default = ["us-east-1", "us-west-2"] +} + +resource "aws_vpc_ipam" "example" { + description = "test4" + dynamic operating_regions { + for_each = var.ipam_regions + content { + region_name = operating_regions.value + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `description` - (Optional) A description for the IPAM. +* `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. +* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +### operating_regions + +* `region_name` - (Required) The name of the Region you want to add to the IPAM. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - Amazon Resource Name (ARN) of IPAM +* `id` - The ID of the IPAM +* `private_default_scope_id` - The ID of the IPAM's private scope. A scope is a top-level container in IPAM. Each scope represents an IP-independent network. Scopes enable you to represent networks where you have overlapping IP space. When you create an IPAM, IPAM automatically creates two scopes: public and private. The private scope is intended for private IP space. The public scope is intended for all internet-routable IP space. +* `public_default_scope_id` - The ID of the IPAM's public scope. A scope is a top-level container in IPAM. Each scope represents an IP-independent network. Scopes enable you to represent networks where you have overlapping IP space. When you create an IPAM, IPAM automatically creates two scopes: public and private. The private scope is intended for private +IP space. The public scope is intended for all internet-routable IP space. +* `scope_count` - The number of scopes in the IPAM. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). + + +## Import + +IPAMs can be imported using the `ipam id`, e.g. + +``` +$ terraform import aws_vpc_ipam.example ipam-0178368ad2146a492 +``` diff --git a/website/docs/r/vpc_ipam_pool.html.markdown b/website/docs/r/vpc_ipam_pool.html.markdown new file mode 100644 index 00000000000..a18262492e4 --- /dev/null +++ b/website/docs/r/vpc_ipam_pool.html.markdown @@ -0,0 +1,103 @@ +--- +subcategory: "VPC" +layout: "aws" +page_title: "AWS: aws_vpc_ipam_pool" +description: |- + Provides a IP address pool resource for IPAM. +--- + +# Resource: aws_vpc_ipam_pool + +Provides a VPC resource. + +## Example Usage + +Basic usage: + +```terraform +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "example" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +resource "aws_vpc_ipam_pool" "example" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.example.private_default_scope_id + locale = data.aws_region.current.name +} +``` + +Nested Pools: + +```terraform +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "example" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +resource "aws_vpc_ipam_pool" "parent" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.example.private_default_scope_id +} + +resource "aws_vpc_ipam_pool_cidr" "parent_test" { + ipam_pool_id = aws_vpc_ipam_pool.parent.id + cidr = "172.2.0.0/16" +} + +resource "aws_vpc_ipam_pool" "child" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.example.private_default_scope_id + locale = data.aws_region.current.name + source_ipam_pool_id = aws_vpc_ipam_pool.parent.id +} + + +resource "aws_vpc_ipam_pool_cidr" "child_test" { + ipam_pool_id = aws_vpc_ipam_pool.child.id + cidr = "172.2.0.0/24" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `address_family` - (Optional) The IP protocol assigned to this pool. You must choose either IPv4 or IPv6 protocol for a pool. +* `publicly_advertisable` - (Required) Defines whether or not IPv6 pool space is publicly ∂advertisable over the internet. This option is not available for IPv4 pool space. +* `allocation_default_netmask_length` - (Optional) A default netmask length for allocations added to this pool. If, for example, the CIDR assigned to this pool is 10.0.0.0/8 and you enter 16 here, new allocations will default to 10.0.0.0/16 (unless you provide a different netmask value when you create the new allocation). +* `allocation_max_netmask_length` - (Optional) The maximum netmask length that will be required for CIDR allocations in this pool. +* `allocation_min_netmask_length` - (Optional) The minimum netmask length that will be required for CIDR allocations in this pool. +* `allocation_resource_tags` - (Optional) Tags that are required for resources that use CIDRs from this IPAM pool. Resources that do not have these tags will not be allowed to allocate space from the pool. If the resources have their tags changed after they have allocated space or if the allocation tagging requirements are changed on the pool, the resource may be marked as noncompliant. +* `auto_import` - (Optional) If you include this argument, IPAM automatically imports any VPCs you have in your scope that fall +within the CIDR range in the pool. +* `aws_service` - (Optional) Limits which AWS service the pool can be used in. Only useable on public scopes. Valid Values: `ec2`. +* `description` - (Optional) A description for the IPAM pool. +* `ipam_scope_id` - (Optional) The ID of the scope in which you would like to create the IPAM pool. +* `locale` - (Optional) The locale in which you would like to create the IPAM pool. 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. Possible values: Any AWS region, such as `us-east-1`. +* `source_ipam_pool_id` - (Optional) The ID of the source IPAM pool. Use this argument to create a child pool within an existing pool. +* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - Amazon Resource Name (ARN) of IPAM +* `id` - The ID of the IPAM +* `state` - The ID of the IPAM +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). + + +## Import + +IPAMs can be imported using the `ipam pool id`, e.g. + +``` +$ terraform import aws_vpc_ipam_pool.example ipam-pool-0958f95207d978e1e +``` diff --git a/website/docs/r/vpc_ipam_pool_cidr.html.markdown b/website/docs/r/vpc_ipam_pool_cidr.html.markdown new file mode 100644 index 00000000000..ee297b4f957 --- /dev/null +++ b/website/docs/r/vpc_ipam_pool_cidr.html.markdown @@ -0,0 +1,94 @@ +--- +subcategory: "VPC" +layout: "aws" +page_title: "AWS: aws_vpc_ipam_pool_cidr" +description: |- + Provisions a CIDR from an IPAM address pool. +--- + +# Resource: aws_vpc_ipam_pool_cidr + +Provisions a CIDR from an IPAM address pool. + +~> **NOTE:** Provisioning Public IPv4 or Public IPv6 require [steps outside the scope of this resource](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-byoip.html#prepare-for-byoip). The resource accepts `message` and `signature` as part of the `cidr_authorization_context` attribute but those must be generated ahead of time. Public IPv6 CIDRs that are provisioned into a Pool with `publicly_advertisable = true` and all public IPv4 CIDRs also require creating a Route Origin Authorization (ROA) object in your Regional Internet Registry (RIR). + +## Example Usage + +Basic usage: + +```terraform +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "example" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +resource "aws_vpc_ipam_pool" "example" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.example.private_default_scope_id + locale = data.aws_region.current.name +} + +resource "aws_vpc_ipam_pool_cidr" "example" { + ipam_pool_id = aws_vpc_ipam_pool.example.id + cidr = "172.2.0.0/16" +} +``` + +Provision Public IPv6 Pool CIDRs: + +```terraform +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "example" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +resource "aws_vpc_ipam_pool" "ipv6_test_public" { + address_family = "ipv6" + ipam_scope_id = aws_vpc_ipam.example.public_default_scope_id + locale = "us-east-1" + description = "public ipv6" + advertisable = false +} + +resource "aws_vpc_ipam_pool_cidr" "ipv6_test_public" { + ipam_pool_id = aws_vpc_ipam_pool.ipv6_test_public.id + cidr = var.ipv6_cidr + cidr_authorization_context { + message = var.message + signature = var.signature + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cidr` - (Optional) The CIDR you want to assign to the pool. +* `cidr_authorization_context` - (Optional) A signed document that proves that you are authorized to bring the specified IP address range to Amazon using BYOIP. This is not stored in the state file. See [cidr_authorization_context](#cidr_authorization_context) for more information. +* `ipam_pool_id` - (Required) The ID of the pool to which you want to assign a CIDR. + +### cidr_authorization_context + +* `message` - (Optional) The plain-text authorization message for the prefix and account. +* `signature` - (Optional) The signed authorization message for the prefix and account. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the IPAM Pool Cidr concatenated with the IPAM Pool ID. + +## Import + +IPAMs can be imported using the `_`, e.g. + +``` +$ terraform import aws_vpc_ipam_pool_cidr.example 172.2.0.0/24_ipam-pool-0e634f5a1517cccdc +``` diff --git a/website/docs/r/vpc_ipam_pool_cidr_allocation.html.markdown b/website/docs/r/vpc_ipam_pool_cidr_allocation.html.markdown new file mode 100644 index 00000000000..a278dd31c16 --- /dev/null +++ b/website/docs/r/vpc_ipam_pool_cidr_allocation.html.markdown @@ -0,0 +1,70 @@ +--- +subcategory: "VPC" +layout: "aws" +page_title: "AWS: aws_vpc_ipam_pool_cidr_allocation" +description: |- + Allocates (reserves) a CIDR from an IPAM address pool, preventing usage by IPAM. +--- + +# Resource: aws_vpc_ipam_pool_cidr_allocation + +Allocates (reserves) a CIDR from an IPAM address pool, preventing usage by IPAM. Only works for private IPv4. + +## Example Usage + +Basic usage: + +```terraform +data "aws_region" "current" {} + +resource "aws_vpc_ipam_pool_cidr_allocation" "example" { + ipam_pool_id = aws_vpc_ipam_pool.example.id + cidr = "172.2.0.0/24" + depends_on = [ + aws_vpc_ipam_pool_cidr.example + ] +} + +resource "aws_vpc_ipam_pool_cidr" "example" { + ipam_pool_id = aws_vpc_ipam_pool.example.id + cidr = "172.2.0.0/16" +} + +resource "aws_vpc_ipam_pool" "example" { + address_family = "ipv4" + ipam_scope_id = aws_vpc_ipam.example.private_default_scope_id + locale = data.aws_region.current.name +} + +resource "aws_vpc_ipam" "example" { + operating_regions { + region_name = data.aws_region.current.name + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cidr` - (Optional) The CIDR you want to assign to the pool. +* `description` - (Optional) The description for the allocation. +* `ipam_pool_id` - (Required) The ID of the pool to which you want to assign a CIDR. +* `netmask_length` - (Optional) The netmask length of the CIDR you would like to allocate to the IPAM pool. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the allocation. +* `resource_id` - The ID of the resource. +* `resource_owner` - The owner of the resource. +* `resource_type` - The type of the resource. + +## Import + +IPAMs can be imported using the `allocation id`, e.g. + +``` +$ terraform import aws_vpc_ipam_pool_cidr_allocation.example +``` diff --git a/website/docs/r/vpc_ipam_scope.html.markdown b/website/docs/r/vpc_ipam_scope.html.markdown new file mode 100644 index 00000000000..3e0a8a4cdce --- /dev/null +++ b/website/docs/r/vpc_ipam_scope.html.markdown @@ -0,0 +1,54 @@ +--- +subcategory: "VPC" +layout: "aws" +page_title: "AWS: aws_vpc_ipam_scope" +description: |- + Creates a scope for AWS IPAM. +--- + +# Resource: aws_vpc_ipam_scope + +Creates a scope for AWS IPAM. + +## Example Usage + +Basic usage: + +```terraform +data "aws_region" "current" {} + +resource "aws_vpc_ipam" "example" { + operating_regions { + region_name = data.aws_region.current.name + } +} + +resource "aws_vpc_ipam_scope" "example" { + ipam_id = aws_vpc_ipam.example.id + description = "Another Scope" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `ipam_id` - The ID of the IPAM for which you're creating this scope. +* `description` - (Optional) A description for the scope you're creating. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the IPAM Scope. +* `ipam_arn` - The ARN of the IPAM for which you're creating this scope. +* `is_default` - Defines if the scope is the default scope or not. +* `pool_count` - Count of pools under this scope + +## Import + +IPAMs can be imported using the `scope_id`, e.g. + +``` +$ terraform import aws_vpc_ipam_scope.example ipam-scope-0513c69f283d11dfb +``` diff --git a/website/docs/r/vpc_ipv4_cidr_block_association.html.markdown b/website/docs/r/vpc_ipv4_cidr_block_association.html.markdown index 7a887a98d34..5b2ee4c7858 100644 --- a/website/docs/r/vpc_ipv4_cidr_block_association.html.markdown +++ b/website/docs/r/vpc_ipv4_cidr_block_association.html.markdown @@ -30,7 +30,9 @@ resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" { The following arguments are supported: -* `cidr_block` - (Required) The additional IPv4 CIDR block to associate with the VPC. +* `cidr_block` - (Optional) The IPv4 CIDR block for the VPC. CIDR can be explicitly set or it can be derived from IPAM using `ipv4_netmask_length`. +* `ipv4_ipam_pool_id` - (Optional) The ID of an IPv4 IPAM pool you want to use for allocating this VPC's CIDR. IPAM is a VPC feature that you can use to automate your IP address management workflows including assigning, tracking, troubleshooting, and auditing IP addresses across AWS Regions and accounts. Using IPAM you can monitor IP address usage throughout your AWS Organization. +* `ipv4_netmask_length` - (Optional) The netmask length of the IPv4 CIDR you want to allocate to this VPC. Requires specifying a `ipv4_ipam_pool_id`. * `vpc_id` - (Required) The ID of the VPC to make the association with. ## Timeouts diff --git a/website/docs/r/vpc_ipv6_cidr_block_association.html.markdown b/website/docs/r/vpc_ipv6_cidr_block_association.html.markdown new file mode 100644 index 00000000000..00394090e60 --- /dev/null +++ b/website/docs/r/vpc_ipv6_cidr_block_association.html.markdown @@ -0,0 +1,58 @@ +--- +subcategory: "VPC" +layout: "aws" +page_title: "AWS: aws_vpc_ipv6_cidr_block_association" +description: |- + Associate additional IPv6 CIDR blocks with a VPC +--- + +# Resource: aws_vpc_ipv6_cidr_block_association + +Provides a resource to associate additional IPv6 CIDR blocks with a VPC. + +When a VPC is created, a primary IPv6 CIDR block for the VPC must be specified. +The `aws_vpc_ipv6_cidr_block_association` resource allows further IPv6 CIDR blocks to be added to the VPC. + +## Example Usage + +```terraform +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpc_ipv6_cidr_block_association" "test" { + ipv6_ipam_pool_id = aws_vpc_ipam_pool.test.id + vpc_id = aws_vpc.test.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cidr_block` - (Optional) The IPv6 CIDR block for the VPC. CIDR can be explicitly set or it can be derived from IPAM using `ipv6_netmask_length`. This parameter is required if `ipv6_netmask_length` is not set and he IPAM pool does not have `allocation_default_netmask` set. +* `ipv6_ipam_pool_id` - (Required) The ID of an IPv6 IPAM pool you want to use for allocating this VPC's CIDR. IPAM is a VPC feature that you can use to automate your IP address management workflows including assigning, tracking, troubleshooting, and auditing IP addresses across AWS Regions and accounts. +* `ipv6_netmask_length` - (Optional) The netmask length of the IPv6 CIDR you want to allocate to this VPC. Requires specifying a `ipv6_ipam_pool_id`. This parameter is optional if the IPAM pool has `allocation_default_netmask` set, otherwise it or `cidr_block` are required +* `vpc_id` - (Required) The ID of the VPC to make the association with. + +## Timeouts + +`aws_vpc_ipv6_cidr_block_association` provides the following +[Timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) configuration options: + +- `create` - (Default `10 minutes`) Used for creating the association +- `delete` - (Default `10 minutes`) Used for destroying the association + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the VPC CIDR association + +## Import + +`aws_vpc_ipv6_cidr_block_association` can be imported by using the VPC CIDR Association ID, e.g., + +``` +$ terraform import aws_vpc_ipv6_cidr_block_association.example vpc-cidr-assoc-xxxxxxxx +```