diff --git a/aws/aws_vpc_ipv4_cidr_block_association.go b/aws/aws_vpc_ipv4_cidr_block_association.go new file mode 100644 index 00000000000..54cf6c01e12 --- /dev/null +++ b/aws/aws_vpc_ipv4_cidr_block_association.go @@ -0,0 +1,144 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +const ( + VpcCidrBlockStateCodeDeleted = "deleted" +) + +func resourceAwsVpcIpv4CidrBlockAssociation() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsVpcIpv4CidrBlockAssociationCreate, + Read: resourceAwsVpcIpv4CidrBlockAssociationRead, + Delete: resourceAwsVpcIpv4CidrBlockAssociationDelete, + + Schema: map[string]*schema.Schema{ + "vpc_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "cidr_block": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.CIDRNetwork(16, 28), // The allowed block size is between a /28 netmask and /16 netmask. + }, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + } +} + +func resourceAwsVpcIpv4CidrBlockAssociationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + req := &ec2.AssociateVpcCidrBlockInput{ + VpcId: aws.String(d.Get("vpc_id").(string)), + CidrBlock: aws.String(d.Get("cidr_block").(string)), + } + log.Printf("[DEBUG] Creating VPC IPv4 CIDR block association: %#v", req) + resp, err := conn.AssociateVpcCidrBlock(req) + if err != nil { + return fmt.Errorf("Error creating VPC IPv4 CIDR block association: %s", err) + } + + d.SetId(aws.StringValue(resp.CidrBlockAssociation.AssociationId)) + + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.VpcCidrBlockStateCodeAssociating}, + Target: []string{ec2.VpcCidrBlockStateCodeAssociated}, + Refresh: vpcIpv4CidrBlockAssociationStateRefresh(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 IPv4 CIDR block association (%s) to become available: %s", d.Id(), err) + } + + return resourceAwsVpcIpv4CidrBlockAssociationRead(d, meta) +} + +func resourceAwsVpcIpv4CidrBlockAssociationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + assocRaw, state, err := vpcIpv4CidrBlockAssociationStateRefresh(conn, d.Get("vpc_id").(string), d.Id())() + if err != nil { + return fmt.Errorf("Error reading IPv4 CIDR block association: %s", err) + } + if state == VpcCidrBlockStateCodeDeleted { + log.Printf("[WARN] IPv4 CIDR block association (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + assoc := assocRaw.(*ec2.VpcCidrBlockAssociation) + d.Set("cidr_block", assoc.CidrBlock) + + return nil +} + +func resourceAwsVpcIpv4CidrBlockAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + log.Printf("[DEBUG] Deleting VPC IPv4 CIDR block association: %s", d.Id()) + _, err := conn.DisassociateVpcCidrBlock(&ec2.DisassociateVpcCidrBlockInput{ + AssociationId: aws.String(d.Id()), + }) + if err != nil { + if isAWSErr(err, "InvalidVpcID.NotFound", "") { + return nil + } + return fmt.Errorf("Error deleting VPC IPv4 CIDR block association: %s", err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.VpcCidrBlockStateCodeDisassociating}, + Target: []string{ec2.VpcCidrBlockStateCodeDisassociated, VpcCidrBlockStateCodeDeleted}, + Refresh: vpcIpv4CidrBlockAssociationStateRefresh(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 IPv4 CIDR block association (%s) to be deleted: %s", d.Id(), err.Error()) + } + + return nil +} + +func vpcIpv4CidrBlockAssociationStateRefresh(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 _, cidrAssociation := range vpc.CidrBlockAssociationSet { + if aws.StringValue(cidrAssociation.AssociationId) == assocId { + return cidrAssociation, aws.StringValue(cidrAssociation.CidrBlockState.State), nil + } + } + } + + return "", VpcCidrBlockStateCodeDeleted, nil + } +} diff --git a/aws/aws_vpc_ipv4_cidr_block_association_test.go b/aws/aws_vpc_ipv4_cidr_block_association_test.go new file mode 100644 index 00000000000..7a60173a612 --- /dev/null +++ b/aws/aws_vpc_ipv4_cidr_block_association_test.go @@ -0,0 +1,141 @@ +package aws + +import ( + "fmt" + "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/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAwsVpcIpv4CidrBlockAssociation_basic(t *testing.T) { + var associationSecondary, associationTertiary ec2.VpcCidrBlockAssociation + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsVpcIpv4CidrBlockAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsVpcIpv4CidrBlockAssociationConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsVpcIpv4CidrBlockAssociationExists("aws_vpc_ipv4_cidr_block_association.secondary_cidr", &associationSecondary), + testAccCheckAdditionalAwsVpcIpv4CidrBlock(&associationSecondary, "172.2.0.0/16"), + testAccCheckAwsVpcIpv4CidrBlockAssociationExists("aws_vpc_ipv4_cidr_block_association.tertiary_cidr", &associationTertiary), + testAccCheckAdditionalAwsVpcIpv4CidrBlock(&associationTertiary, "170.2.0.0/16"), + ), + }, + }, + }) +} + +func testAccCheckAdditionalAwsVpcIpv4CidrBlock(association *ec2.VpcCidrBlockAssociation, expected string) resource.TestCheckFunc { + return func(s *terraform.State) error { + CIDRBlock := association.CidrBlock + if *CIDRBlock != expected { + return fmt.Errorf("Bad CIDR: %s", *association.CidrBlock) + } + + return nil + } +} + +func testAccCheckAwsVpcIpv4CidrBlockAssociationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpc_ipv4_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 _, ipv4Association := range vpc.CidrBlockAssociationSet { + if *ipv4Association.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 testAccCheckAwsVpcIpv4CidrBlockAssociationExists(n string, association *ec2.VpcCidrBlockAssociation) 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 := testAccProvider.Meta().(*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 _, cidrAssociation := range vpc.CidrBlockAssociationSet { + if *cidrAssociation.AssociationId == rs.Primary.ID { + *association = *cidrAssociation + found = true + } + } + + if !found { + return fmt.Errorf("VPC CIDR block association not found") + } + + return nil + } +} + +const testAccAwsVpcIpv4CidrBlockAssociationConfig = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" + tags { + Name = "terraform-testacc-vpc-ipv4-cidr-block-association" + } +} + +resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" { + vpc_id = "${aws_vpc.foo.id}" + cidr_block = "172.2.0.0/16" +} + +resource "aws_vpc_ipv4_cidr_block_association" "tertiary_cidr" { + vpc_id = "${aws_vpc.foo.id}" + cidr_block = "170.2.0.0/16" +} +` diff --git a/aws/provider.go b/aws/provider.go index 6a513c7249f..a5c7e9e1b4e 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -609,6 +609,7 @@ func Provider() terraform.ResourceProvider { "aws_vpc_endpoint_subnet_association": resourceAwsVpcEndpointSubnetAssociation(), "aws_vpc_endpoint_service": resourceAwsVpcEndpointService(), "aws_vpc_endpoint_service_allowed_principal": resourceAwsVpcEndpointServiceAllowedPrincipal(), + "aws_vpc_ipv4_cidr_block_association": resourceAwsVpcIpv4CidrBlockAssociation(), "aws_vpn_connection": resourceAwsVpnConnection(), "aws_vpn_connection_route": resourceAwsVpnConnectionRoute(), "aws_vpn_gateway": resourceAwsVpnGateway(), diff --git a/aws/resource_aws_vpc.go b/aws/resource_aws_vpc.go index e1687477d0a..1006d9d29ca 100644 --- a/aws/resource_aws_vpc.go +++ b/aws/resource_aws_vpc.go @@ -674,3 +674,33 @@ func awsVpcDescribeVpcAttribute(attribute string, vpcId string, conn *ec2.EC2) ( return resp, nil } + +// vpcDescribe returns EC2 API information about the specified VPC. +// If the VPC doesn't exist, return nil. +func vpcDescribe(conn *ec2.EC2, vpcId string) (*ec2.Vpc, error) { + resp, err := conn.DescribeVpcs(&ec2.DescribeVpcsInput{ + VpcIds: aws.StringSlice([]string{vpcId}), + }) + if err != nil { + if !isAWSErr(err, "InvalidVpcID.NotFound", "") { + return nil, err + } + resp = nil + } + + if resp == nil { + return nil, nil + } + + n := len(resp.Vpcs) + switch n { + case 0: + return nil, nil + + case 1: + return resp.Vpcs[0], nil + + default: + return nil, fmt.Errorf("Found %d VPCs for %s, expected 1", n, vpcId) + } +} diff --git a/website/aws.erb b/website/aws.erb index 9fdefaafb52..f4d36921980 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -2230,6 +2230,10 @@ aws_vpc_endpoint_subnet_association +