Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add VPC IPAM Service and Update VPC Resources to support IPAM Parameters #21998

Merged
merged 25 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changelog/21998.txt
Original file line number Diff line number Diff line change
@@ -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.
```
7 changes: 7 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
227 changes: 169 additions & 58 deletions internal/service/ec2/vpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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(),
},
}
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()

Expand All @@ -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()
Expand Down
Loading