From 92335b742a2a2878bd6eea44360582492e662ba8 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Tue, 3 Feb 2015 13:11:05 -0600 Subject: [PATCH 1/2] provider/aws: aws_main_route_table_association This resource allows an existing Route Table to be assigned as the "main" Route Table of a VPC. This means that the Route Table will be used for any subnets within the VPC without an explicit Route Table assigned [1]. This is particularly useful in getting an Internet Gateway in place as the default for a VPC, since the automatically created Main Route Table does not have one [2]. Note that this resource is an abstraction over an association and does not map directly to a CRUD-able object in AWS. In order to retain a coherent "Delete" operation for this resource, we remember the ID of the AWS-created Route Table and reset the VPC's main Route Table to it when this resource is deleted. refs #843, #748 [1] http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Route_Tables.html#RouteTableDetails [2] http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Internet_Gateway.html#Add_IGW_Routing --- builtin/providers/aws/provider.go | 41 ++--- ...source_aws_main_route_table_association.go | 155 ++++++++++++++++++ ...e_aws_main_route_table_association_test.go | 148 +++++++++++++++++ 3 files changed, 324 insertions(+), 20 deletions(-) create mode 100644 builtin/providers/aws/resource_aws_main_route_table_association.go create mode 100644 builtin/providers/aws/resource_aws_main_route_table_association_test.go diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 3c417d32d..90e43011a 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -45,26 +45,27 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "aws_autoscaling_group": resourceAwsAutoscalingGroup(), - "aws_db_instance": resourceAwsDbInstance(), - "aws_db_parameter_group": resourceAwsDbParameterGroup(), - "aws_db_security_group": resourceAwsDbSecurityGroup(), - "aws_db_subnet_group": resourceAwsDbSubnetGroup(), - "aws_eip": resourceAwsEip(), - "aws_elb": resourceAwsElb(), - "aws_instance": resourceAwsInstance(), - "aws_internet_gateway": resourceAwsInternetGateway(), - "aws_key_pair": resourceAwsKeyPair(), - "aws_launch_configuration": resourceAwsLaunchConfiguration(), - "aws_network_acl": resourceAwsNetworkAcl(), - "aws_route53_record": resourceAwsRoute53Record(), - "aws_route53_zone": resourceAwsRoute53Zone(), - "aws_route_table": resourceAwsRouteTable(), - "aws_route_table_association": resourceAwsRouteTableAssociation(), - "aws_s3_bucket": resourceAwsS3Bucket(), - "aws_security_group": resourceAwsSecurityGroup(), - "aws_subnet": resourceAwsSubnet(), - "aws_vpc": resourceAwsVpc(), + "aws_autoscaling_group": resourceAwsAutoscalingGroup(), + "aws_db_instance": resourceAwsDbInstance(), + "aws_db_parameter_group": resourceAwsDbParameterGroup(), + "aws_db_security_group": resourceAwsDbSecurityGroup(), + "aws_db_subnet_group": resourceAwsDbSubnetGroup(), + "aws_eip": resourceAwsEip(), + "aws_elb": resourceAwsElb(), + "aws_instance": resourceAwsInstance(), + "aws_internet_gateway": resourceAwsInternetGateway(), + "aws_key_pair": resourceAwsKeyPair(), + "aws_launch_configuration": resourceAwsLaunchConfiguration(), + "aws_main_route_table_association": resourceAwsMainRouteTableAssociation(), + "aws_network_acl": resourceAwsNetworkAcl(), + "aws_route53_record": resourceAwsRoute53Record(), + "aws_route53_zone": resourceAwsRoute53Zone(), + "aws_route_table": resourceAwsRouteTable(), + "aws_route_table_association": resourceAwsRouteTableAssociation(), + "aws_s3_bucket": resourceAwsS3Bucket(), + "aws_security_group": resourceAwsSecurityGroup(), + "aws_subnet": resourceAwsSubnet(), + "aws_vpc": resourceAwsVpc(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/aws/resource_aws_main_route_table_association.go b/builtin/providers/aws/resource_aws_main_route_table_association.go new file mode 100644 index 000000000..f656f3760 --- /dev/null +++ b/builtin/providers/aws/resource_aws_main_route_table_association.go @@ -0,0 +1,155 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/mitchellh/goamz/ec2" +) + +func resourceAwsMainRouteTableAssociation() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsMainRouteTableAssociationCreate, + Read: resourceAwsMainRouteTableAssociationRead, + Update: resourceAwsMainRouteTableAssociationUpdate, + Delete: resourceAwsMainRouteTableAssociationDelete, + + Schema: map[string]*schema.Schema{ + "vpc_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "route_table_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + // We use this field to record the main route table that is automatically + // created when the VPC is created. We need this to be able to "destroy" + // our main route table association, which we do by returning this route + // table to its original place as the Main Route Table for the VPC. + "original_route_table_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsMainRouteTableAssociationCreate(d *schema.ResourceData, meta interface{}) error { + ec2conn := meta.(*AWSClient).ec2conn + vpcId := d.Get("vpc_id").(string) + routeTableId := d.Get("route_table_id").(string) + + log.Printf("[INFO] Creating main route table association: %s => %s", vpcId, routeTableId) + + mainAssociation, err := findMainRouteTableAssociation(ec2conn, vpcId) + if err != nil { + return err + } + + resp, err := ec2conn.ReassociateRouteTable( + mainAssociation.AssociationId, + routeTableId, + ) + if err != nil { + return err + } + + d.Set("original_route_table_id", mainAssociation.RouteTableId) + d.SetId(resp.AssociationId) + log.Printf("[INFO] New main route table association ID: %s", d.Id()) + + return nil +} + +func resourceAwsMainRouteTableAssociationRead(d *schema.ResourceData, meta interface{}) error { + ec2conn := meta.(*AWSClient).ec2conn + + mainAssociation, err := findMainRouteTableAssociation( + ec2conn, + d.Get("vpc_id").(string)) + if err != nil { + return err + } + + if mainAssociation.AssociationId != d.Id() { + // It seems it doesn't exist anymore, so clear the ID + d.SetId("") + } + + return nil +} + +// Update is almost exactly like Create, except we want to retain the +// original_route_table_id - this needs to stay recorded as the AWS-created +// table from VPC creation. +func resourceAwsMainRouteTableAssociationUpdate(d *schema.ResourceData, meta interface{}) error { + ec2conn := meta.(*AWSClient).ec2conn + vpcId := d.Get("vpc_id").(string) + routeTableId := d.Get("route_table_id").(string) + + log.Printf("[INFO] Updating main route table association: %s => %s", vpcId, routeTableId) + + resp, err := ec2conn.ReassociateRouteTable(d.Id(), routeTableId) + if err != nil { + return err + } + + d.SetId(resp.AssociationId) + log.Printf("[INFO] New main route table association ID: %s", d.Id()) + + return nil +} + +func resourceAwsMainRouteTableAssociationDelete(d *schema.ResourceData, meta interface{}) error { + ec2conn := meta.(*AWSClient).ec2conn + vpcId := d.Get("vpc_id").(string) + originalRouteTableId := d.Get("original_route_table_id").(string) + + log.Printf("[INFO] Deleting main route table association by resetting Main Route Table for VPC: %s to its original Route Table: %s", + vpcId, + originalRouteTableId) + + resp, err := ec2conn.ReassociateRouteTable(d.Id(), originalRouteTableId) + if err != nil { + return err + } + + log.Printf("[INFO] Resulting Association ID: %s", resp.AssociationId) + + return nil +} + +func findMainRouteTableAssociation(ec2conn *ec2.EC2, vpcId string) (*ec2.RouteTableAssociation, error) { + mainRouteTable, err := findMainRouteTable(ec2conn, vpcId) + if err != nil { + return nil, err + } + + for _, a := range mainRouteTable.Associations { + if a.Main { + return &a, nil + } + } + return nil, fmt.Errorf("Could not find main routing table association for VPC: %s", vpcId) +} + +func findMainRouteTable(ec2conn *ec2.EC2, vpcId string) (*ec2.RouteTable, error) { + filter := ec2.NewFilter() + filter.Add("association.main", "true") + filter.Add("vpc-id", vpcId) + routeResp, err := ec2conn.DescribeRouteTables(nil, filter) + if err != nil { + return nil, err + } else if len(routeResp.RouteTables) != 1 { + return nil, fmt.Errorf( + "Expected to find a single main routing table for VPC: %s, but found %d", + vpcId, + len(routeResp.RouteTables)) + } + + return &routeResp.RouteTables[0], nil +} diff --git a/builtin/providers/aws/resource_aws_main_route_table_association_test.go b/builtin/providers/aws/resource_aws_main_route_table_association_test.go new file mode 100644 index 000000000..937014cae --- /dev/null +++ b/builtin/providers/aws/resource_aws_main_route_table_association_test.go @@ -0,0 +1,148 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSMainRouteTableAssociation(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckMainRouteTableAssociationDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccMainRouteTableAssociationConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckMainRouteTableAssociation( + "aws_main_route_table_association.foo", + "aws_vpc.foo", + "aws_route_table.foo", + ), + ), + }, + resource.TestStep{ + Config: testAccMainRouteTableAssociationConfigUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckMainRouteTableAssociation( + "aws_main_route_table_association.foo", + "aws_vpc.foo", + "aws_route_table.bar", + ), + ), + }, + }, + }) +} + +func testAccCheckMainRouteTableAssociationDestroy(s *terraform.State) error { + if len(s.RootModule().Resources) > 0 { + return fmt.Errorf("Expected all resources to be gone, but found: %#v", s.RootModule().Resources) + } + + return nil +} + +func testAccCheckMainRouteTableAssociation( + mainRouteTableAssociationResource string, + vpcResource string, + routeTableResource string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[mainRouteTableAssociationResource] + if !ok { + return fmt.Errorf("Not found: %s", mainRouteTableAssociationResource) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + vpc, ok := s.RootModule().Resources[vpcResource] + if !ok { + return fmt.Errorf("Not found: %s", vpcResource) + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + mainAssociation, err := findMainRouteTableAssociation(conn, vpc.Primary.ID) + if err != nil { + return err + } + + if mainAssociation.AssociationId != rs.Primary.ID { + return fmt.Errorf("Found wrong main association: %s", + mainAssociation.AssociationId) + } + + return nil + } +} + +const testAccMainRouteTableAssociationConfig = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.foo.id}" + cidr_block = "10.1.1.0/24" +} + +resource "aws_internet_gateway" "foo" { + vpc_id = "${aws_vpc.foo.id}" +} + +resource "aws_route_table" "foo" { + vpc_id = "${aws_vpc.foo.id}" + route { + cidr_block = "10.0.0.0/8" + gateway_id = "${aws_internet_gateway.foo.id}" + } +} + +resource "aws_main_route_table_association" "foo" { + vpc_id = "${aws_vpc.foo.id}" + route_table_id = "${aws_route_table.foo.id}" +} +` + +const testAccMainRouteTableAssociationConfigUpdate = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.foo.id}" + cidr_block = "10.1.1.0/24" +} + +resource "aws_internet_gateway" "foo" { + vpc_id = "${aws_vpc.foo.id}" +} + +// Need to keep the old route table around when we update the +// main_route_table_association, otherwise Terraform will try to destroy the +// route table too early, and will fail because it's still the main one +resource "aws_route_table" "foo" { + vpc_id = "${aws_vpc.foo.id}" + route { + cidr_block = "10.0.0.0/8" + gateway_id = "${aws_internet_gateway.foo.id}" + } +} + +resource "aws_route_table" "bar" { + vpc_id = "${aws_vpc.foo.id}" + route { + cidr_block = "10.0.0.0/8" + gateway_id = "${aws_internet_gateway.foo.id}" + } +} + +resource "aws_main_route_table_association" "foo" { + vpc_id = "${aws_vpc.foo.id}" + route_table_id = "${aws_route_table.bar.id}" +} +` From f852a01c2206b04cf26444ab6d447b4ad5ded1b3 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Tue, 3 Feb 2015 15:09:16 -0600 Subject: [PATCH 2/2] providers/aws: docs for aws_main_route_table_association --- .../r/main_route_table_assoc.html.markdown | 44 +++++++++++++++++++ .../docs/providers/aws/r/vpc.html.markdown | 3 +- website/source/layouts/aws.erb | 4 ++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 website/source/docs/providers/aws/r/main_route_table_assoc.html.markdown diff --git a/website/source/docs/providers/aws/r/main_route_table_assoc.html.markdown b/website/source/docs/providers/aws/r/main_route_table_assoc.html.markdown new file mode 100644 index 000000000..a89d2ddee --- /dev/null +++ b/website/source/docs/providers/aws/r/main_route_table_assoc.html.markdown @@ -0,0 +1,44 @@ +--- +layout: "aws" +page_title: "AWS: aws_main_route_table_association" +sidebar_current: "docs-aws-resource-main-route-table-assoc" +description: |- + Provides a resource for managing the main routing table of a VPC. +--- + +# aws\_main\_route\_table\_association + +Provides a resource for managing the main routing table of a VPC. + +## Example Usage + +``` +resource "aws_main_route_table_association" "a" { + vpc_id = "${aws_vpc.foo.id}" + route_table_id = "${aws_route_table.bar.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `vpc_id` - (Required) The ID of the VPC whose main route table should be set +* `route_table_id` - (Required) The ID of the Route Table to set as the new + main route table for the target VPC + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the Route Table Association +* `original_route_table_id` - Used internally, see __Notes__ below + +## Notes + +On VPC creation, the AWS API always creates an initial Main Route Table. This +resource records the ID of that Route Table under `original_route_table_id`. +The "Delete" action for a `main_route_table_association` consists of resetting +this original table as the Main Route Table for the VPC. You'll see this +additional Route Table in the AWS console; it must remain intact in order for +the `main_route_table_association` delete to work properly. diff --git a/website/source/docs/providers/aws/r/vpc.html.markdown b/website/source/docs/providers/aws/r/vpc.html.markdown index f2ab8da16..48e56d340 100644 --- a/website/source/docs/providers/aws/r/vpc.html.markdown +++ b/website/source/docs/providers/aws/r/vpc.html.markdown @@ -53,6 +53,7 @@ The following attributes are exported: * `enable_dns_support` - Whether or not the VPC has DNS support * `enable_dns_hostnames` - Whether or not the VPC has DNS hostname support * `main_route_table_id` - The ID of the main route table associated with - this VPC. + this VPC. Note that you can change a VPC's main route table by using an + [`aws_main_route_table_association`](/docs/providers/aws/r/main_route_table_assoc.html). * `default_network_acl_id` - The ID of the network ACL created by default on VPC creation * `default_security_group_id` - The ID of the security group created by default on VPC creation diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index d79f44580..030192dfd 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -53,6 +53,10 @@ aws_launch_configuration + > + aws_main_route_table_association + + > aws_network_acl