diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 29db928eb..def8b9995 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -268,6 +268,7 @@ func Provider() terraform.ResourceProvider { "aws_nat_gateway": resourceAwsNatGateway(), "aws_network_acl": resourceAwsNetworkAcl(), "aws_default_network_acl": resourceAwsDefaultNetworkAcl(), + "aws_default_route_table": resourceAwsDefaultRouteTable(), "aws_network_acl_rule": resourceAwsNetworkAclRule(), "aws_network_interface": resourceAwsNetworkInterface(), "aws_opsworks_application": resourceAwsOpsworksApplication(), diff --git a/builtin/providers/aws/resource_aws_default_route_table.go b/builtin/providers/aws/resource_aws_default_route_table.go new file mode 100644 index 000000000..118a582e1 --- /dev/null +++ b/builtin/providers/aws/resource_aws_default_route_table.go @@ -0,0 +1,204 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsDefaultRouteTable() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsDefaultRouteTableCreate, + Read: resourceAwsDefaultRouteTableRead, + Update: resourceAwsRouteTableUpdate, + Delete: resourceAwsDefaultRouteTableDelete, + + Schema: map[string]*schema.Schema{ + "default_route_table_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "vpc_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "propagating_vgws": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "route": &schema.Schema{ + Type: schema.TypeSet, + Computed: true, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cidr_block": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "gateway_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "instance_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "nat_gateway_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "vpc_peering_connection_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "network_interface_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + Set: resourceAwsRouteTableHash, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceAwsDefaultRouteTableCreate(d *schema.ResourceData, meta interface{}) error { + d.SetId(d.Get("default_route_table_id").(string)) + + conn := meta.(*AWSClient).ec2conn + rtRaw, _, err := resourceAwsRouteTableStateRefreshFunc(conn, d.Id())() + if err != nil { + return err + } + if rtRaw == nil { + log.Printf("[WARN] Default Route Table not found") + d.SetId("") + return nil + } + + rt := rtRaw.(*ec2.RouteTable) + + d.Set("vpc_id", rt.VpcId) + + // revoke all default and pre-existing routes on the default route table. + // In the UPDATE method, we'll apply only the rules in the configuration. + log.Printf("[DEBUG] Revoking default routes for Default Route Table for %s", d.Id()) + if err := revokeAllRouteTableRules(d.Id(), meta); err != nil { + return err + } + + return resourceAwsRouteTableUpdate(d, meta) +} + +func resourceAwsDefaultRouteTableRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + // look up default route table for VPC + filter1 := &ec2.Filter{ + Name: aws.String("association.main"), + Values: []*string{aws.String("true")}, + } + filter2 := &ec2.Filter{ + Name: aws.String("vpc-id"), + Values: []*string{aws.String(d.Get("vpc_id").(string))}, + } + + findOpts := &ec2.DescribeRouteTablesInput{ + Filters: []*ec2.Filter{filter1, filter2}, + } + + resp, err := conn.DescribeRouteTables(findOpts) + if err != nil { + return err + } + + if len(resp.RouteTables) < 1 || resp.RouteTables[0] == nil { + return fmt.Errorf("Default Route table not found") + } + + rt := resp.RouteTables[0] + + d.Set("default_route_table_id", rt.RouteTableId) + d.SetId(*rt.RouteTableId) + + // re-use regular AWS Route Table READ. This is an extra API call but saves us + // from trying to manually keep parity + return resourceAwsRouteTableRead(d, meta) +} + +func resourceAwsDefaultRouteTableDelete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[WARN] Cannot destroy Default Route Table. Terraform will remove this resource from the state file, however resources may remain.") + d.SetId("") + return nil +} + +// revokeAllRouteTableRules revoke all routes on the Default Route Table +// This should only be ran once at creation time of this resource +func revokeAllRouteTableRules(defaultRouteTableId string, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + log.Printf("\n***\nrevokeAllRouteTableRules\n***\n") + + resp, err := conn.DescribeRouteTables(&ec2.DescribeRouteTablesInput{ + RouteTableIds: []*string{aws.String(defaultRouteTableId)}, + }) + if err != nil { + return err + } + + if len(resp.RouteTables) < 1 || resp.RouteTables[0] == nil { + return fmt.Errorf("Default Route table not found") + } + + rt := resp.RouteTables[0] + + // Remove all Gateway association + for _, r := range rt.PropagatingVgws { + log.Printf( + "[INFO] Deleting VGW propagation from %s: %s", + defaultRouteTableId, *r.GatewayId) + _, err := conn.DisableVgwRoutePropagation(&ec2.DisableVgwRoutePropagationInput{ + RouteTableId: aws.String(defaultRouteTableId), + GatewayId: r.GatewayId, + }) + if err != nil { + return err + } + } + + // Delete all routes + for _, r := range rt.Routes { + // you cannot delete the local route + if r.GatewayId != nil && *r.GatewayId == "local" { + continue + } + log.Printf( + "[INFO] Deleting route from %s: %s", + defaultRouteTableId, *r.DestinationCidrBlock) + _, err := conn.DeleteRoute(&ec2.DeleteRouteInput{ + RouteTableId: aws.String(defaultRouteTableId), + DestinationCidrBlock: r.DestinationCidrBlock, + }) + if err != nil { + return err + } + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_default_route_table_test.go b/builtin/providers/aws/resource_aws_default_route_table_test.go new file mode 100644 index 000000000..1d52a3fd7 --- /dev/null +++ b/builtin/providers/aws/resource_aws_default_route_table_test.go @@ -0,0 +1,240 @@ +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 TestAccAWSDefaultRouteTable_basic(t *testing.T) { + var v ec2.RouteTable + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_default_route_table.foo", + Providers: testAccProviders, + CheckDestroy: testAccCheckDefaultRouteTableDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDefaultRouteTableConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRouteTableExists( + "aws_default_route_table.foo", &v), + ), + }, + }, + }) +} + +func TestAccAWSDefaultRouteTable_swap(t *testing.T) { + var v ec2.RouteTable + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_default_route_table.foo", + Providers: testAccProviders, + CheckDestroy: testAccCheckDefaultRouteTableDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDefaultRouteTable_change, + Check: resource.ComposeTestCheckFunc( + testAccCheckRouteTableExists( + "aws_default_route_table.foo", &v), + ), + }, + + // This config will swap out the original Default Route Table and replace + // it with the custom route table. While this is not advised, it's a + // behavior that may happen, in which case a follow up plan will show (in + // this case) a diff as the table now needs to be updated to match the + // config + resource.TestStep{ + Config: testAccDefaultRouteTable_change_mod, + Check: resource.ComposeTestCheckFunc( + testAccCheckRouteTableExists( + "aws_default_route_table.foo", &v), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckDefaultRouteTableDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_default_route_table" { + continue + } + + // Try to find the resource + resp, err := conn.DescribeRouteTables(&ec2.DescribeRouteTablesInput{ + RouteTableIds: []*string{aws.String(rs.Primary.ID)}, + }) + if err == nil { + if len(resp.RouteTables) > 0 { + return fmt.Errorf("still exist.") + } + + return nil + } + + // Verify the error is what we want + ec2err, ok := err.(awserr.Error) + if !ok { + return err + } + if ec2err.Code() != "InvalidRouteTableID.NotFound" { + return err + } + } + + return nil +} + +func testAccCheckDefaultRouteTableExists(s *terraform.State) error { + // We can't destroy this resource; it comes and goes with the VPC itself. + return nil +} + +const testAccDefaultRouteTableConfig = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" + enable_dns_hostnames = true + + tags { + Name = "tf-default-route-table-test" + } +} + +resource "aws_default_route_table" "foo" { + default_route_table_id = "${aws_vpc.foo.default_route_table_id}" + + route { + cidr_block = "10.0.1.0/32" + gateway_id = "${aws_internet_gateway.gw.id}" + } + + tags { + Name = "tf-default-route-table-test" + } +} + +resource "aws_internet_gateway" "gw" { + vpc_id = "${aws_vpc.foo.id}" + + tags { + Name = "tf-default-route-table-test" + } +}` + +const testAccDefaultRouteTable_change = ` +provider "aws" { + region = "us-west-2" +} + +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" + enable_dns_hostnames = true + + tags { + Name = "tf-default-route-table" + } +} + +resource "aws_default_route_table" "foo" { + default_route_table_id = "${aws_vpc.foo.default_route_table_id}" + + route { + cidr_block = "10.0.1.0/32" + gateway_id = "${aws_internet_gateway.gw.id}" + } + + tags { + Name = "this was the first main" + } +} + +resource "aws_internet_gateway" "gw" { + vpc_id = "${aws_vpc.foo.id}" + + tags { + Name = "main-igw" + } +} + +# Thing to help testing changes +resource "aws_route_table" "r" { + vpc_id = "${aws_vpc.foo.id}" + + route { + cidr_block = "10.0.1.0/24" + gateway_id = "${aws_internet_gateway.gw.id}" + } + + tags { + Name = "other" + } +} +` + +const testAccDefaultRouteTable_change_mod = ` +provider "aws" { + region = "us-west-2" +} + +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" + enable_dns_hostnames = true + + tags { + Name = "tf-default-route-table" + } +} + +resource "aws_default_route_table" "foo" { + default_route_table_id = "${aws_vpc.foo.default_route_table_id}" + + route { + cidr_block = "10.0.1.0/32" + gateway_id = "${aws_internet_gateway.gw.id}" + } + + tags { + Name = "this was the first main" + } +} + +resource "aws_internet_gateway" "gw" { + vpc_id = "${aws_vpc.foo.id}" + + tags { + Name = "main-igw" + } +} + +# Thing to help testing changes +resource "aws_route_table" "r" { + vpc_id = "${aws_vpc.foo.id}" + + route { + cidr_block = "10.0.1.0/24" + gateway_id = "${aws_internet_gateway.gw.id}" + } + + tags { + Name = "other" + } +} + +resource "aws_main_route_table_association" "a" { + vpc_id = "${aws_vpc.foo.id}" + route_table_id = "${aws_route_table.r.id}" +} +` diff --git a/builtin/providers/aws/resource_aws_vpc.go b/builtin/providers/aws/resource_aws_vpc.go index caf39f267..3622f1a01 100644 --- a/builtin/providers/aws/resource_aws_vpc.go +++ b/builtin/providers/aws/resource_aws_vpc.go @@ -75,6 +75,11 @@ func resourceAwsVpc() *schema.Resource { Computed: true, }, + "default_route_table_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), }, } @@ -217,8 +222,15 @@ func resourceAwsVpcRead(d *schema.ResourceData, meta interface{}) error { d.Set("main_route_table_id", *v[0].RouteTableId) } - resourceAwsVpcSetDefaultNetworkAcl(conn, d) - resourceAwsVpcSetDefaultSecurityGroup(conn, d) + if err := resourceAwsVpcSetDefaultNetworkAcl(conn, d); err != nil { + log.Printf("[WARN] Unable to set Default Network ACL: %s", err) + } + if err := resourceAwsVpcSetDefaultSecurityGroup(conn, d); err != nil { + log.Printf("[WARN] Unable to set Default Security Group: %s", err) + } + if err := resourceAwsVpcSetDefaultRouteTable(conn, d); err != nil { + log.Printf("[WARN] Unable to set Default Route Table: %s", err) + } return nil } @@ -410,3 +422,32 @@ func resourceAwsVpcSetDefaultSecurityGroup(conn *ec2.EC2, d *schema.ResourceData return nil } + +func resourceAwsVpcSetDefaultRouteTable(conn *ec2.EC2, d *schema.ResourceData) error { + filter1 := &ec2.Filter{ + Name: aws.String("association.main"), + Values: []*string{aws.String("true")}, + } + filter2 := &ec2.Filter{ + Name: aws.String("vpc-id"), + Values: []*string{aws.String(d.Id())}, + } + + findOpts := &ec2.DescribeRouteTablesInput{ + Filters: []*ec2.Filter{filter1, filter2}, + } + + resp, err := conn.DescribeRouteTables(findOpts) + if err != nil { + return err + } + + if len(resp.RouteTables) < 1 || resp.RouteTables[0] == nil { + return fmt.Errorf("Default Route table not found") + } + + // There Can Be Only 1 ... Default Route Table + d.Set("default_route_table_id", resp.RouteTables[0].RouteTableId) + + return nil +} diff --git a/builtin/providers/aws/resource_aws_vpc_test.go b/builtin/providers/aws/resource_aws_vpc_test.go index 485dd1dc4..dcec54941 100644 --- a/builtin/providers/aws/resource_aws_vpc_test.go +++ b/builtin/providers/aws/resource_aws_vpc_test.go @@ -26,6 +26,8 @@ func TestAccAWSVpc_basic(t *testing.T) { testAccCheckVpcCidr(&vpc, "10.1.0.0/16"), resource.TestCheckResourceAttr( "aws_vpc.foo", "cidr_block", "10.1.0.0/16"), + resource.TestCheckResourceAttrSet( + "aws_vpc.foo", "default_route_table_id"), ), }, }, diff --git a/website/source/docs/providers/aws/r/default_route_table.html.markdown b/website/source/docs/providers/aws/r/default_route_table.html.markdown new file mode 100644 index 000000000..bf4ca6eb0 --- /dev/null +++ b/website/source/docs/providers/aws/r/default_route_table.html.markdown @@ -0,0 +1,88 @@ +--- +layout: "aws" +page_title: "AWS: aws_default_route_table" +sidebar_current: "docs-aws-resource-default-route-table|" +description: |- + Provides a resource to manage a Default VPC Routing Table. +--- + +# aws\_default\_route\_table + +Provides a resource to manage a Default VPC Routing Table. + +Each VPC created in AWS comes with a Default Route Table that can be managed, but not +destroyed. **This is an advanced resource**, and has special caveats to be aware +of when using it. Please read this document in its entirety before using this +resource. It is recommened you **do not** use both `aws_default_route_table` to +manage the default route table **and** use the `aws_main_route_table_association`, +due to possible conflict in routes. + +The `aws_default_route_table` behaves differently from normal resources, in that +Terraform does not _create_ this resource, but instead attempts to "adopt" it +into management. We can do this because each VPC created has a Default Route +Table that cannot be destroyed, and is created with a single route. + +When Terraform first adopts the Default Route Table, it **immediately removes all +defined routes**. It then proceeds to create any routes specified in the +configuration. This step is required so that only the routes specified in the +configuration present in the Default Route Table. + +For more information about Route Tables, see the AWS Documentation on +[Route Tables][aws-route-tables]. + +For more information about managing normal Route Tables in Terraform, see our +documentation on [aws_route_table][tf-route-tables]. + +~> **NOTE on Route Tables and Routes:** Terraform currently +provides both a standalone [Route resource](route.html) and a Route Table resource with routes +defined in-line. At this time you cannot use a Route Table with in-line routes +in conjunction with any Route resources. Doing so will cause +a conflict of rule settings and will overwrite routes. + + +## Example usage with tags: + +``` +resource "aws_default_route_table" "r" { + default_route_table_id = "${aws_vpc.foo.default_route_table_id}" + route { + ... + } + + tags { + Name = "default table" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `default_route_table_id` - (Required) The ID of the Default Routing Table. +* `route` - (Optional) A list of route objects. Their keys are documented below. +* `tags` - (Optional) A mapping of tags to assign to the resource. +* `propagating_vgws` - (Optional) A list of virtual gateways for propagation. + +Each route supports the following: + +* `cidr_block` - (Required) The CIDR block of the route. +* `gateway_id` - (Optional) The Internet Gateway ID. +* `nat_gateway_id` - (Optional) The NAT Gateway ID. +* `instance_id` - (Optional) The EC2 instance ID. +* `vpc_peering_connection_id` - (Optional) The VPC Peering ID. +* `network_interface_id` - (Optional) The ID of the elastic network interface (eni) to use. + +Each route must contain either a `gateway_id`, an `instance_id`, a `nat_gateway_id`, a +`vpc_peering_connection_id` or a `network_interface_id`. Note that the default route, mapping +the VPC's CIDR block to "local", is created implicitly and cannot be specified. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the routing table + + +[aws-route-tables]: http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Route_Tables.html#Route_Replacing_Main_Table +[tf-route-tables]: /docs/providers/aws/r/route_table.html diff --git a/website/source/docs/providers/aws/r/vpc.html.markdown b/website/source/docs/providers/aws/r/vpc.html.markdown index d13ba043c..0ce906712 100644 --- a/website/source/docs/providers/aws/r/vpc.html.markdown +++ b/website/source/docs/providers/aws/r/vpc.html.markdown @@ -61,6 +61,7 @@ The following attributes are exported: [`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 +* `default_route_table_id` - The ID of the route table created by default on VPC creation [1]: http://docs.aws.amazon.com/fr_fr/AWSEC2/latest/UserGuide/vpc-classiclink.html diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 18a024297..04dbccfbd 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -878,6 +878,10 @@ aws_default_network_acl + > + aws_default_route_table + + > aws_flow_log