provider/aws: Add aws_default_route_table resource (#8323)

* provider/aws: Add docs for Default Route Table

* add new default_route_table_id attribute, test to VPC

* stub

* add warning to docs

* rough implementation

* first test

* update test, add swap test

* fix typo
This commit is contained in:
Clint 2016-08-25 16:02:44 -05:00 committed by GitHub
parent ab5b463923
commit 49ecfe8921
8 changed files with 583 additions and 2 deletions

View File

@ -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(),

View File

@ -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
}

View File

@ -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}"
}
`

View File

@ -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
}

View File

@ -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"),
),
},
},

View File

@ -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

View File

@ -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

View File

@ -878,6 +878,10 @@
<a href="/docs/providers/aws/r/default_network_acl.html">aws_default_network_acl</a>
</li>
<li<%= sidebar_current("docs-aws-resource-default-route-table") %>>
<a href="/docs/providers/aws/r/default_route_table.html">aws_default_route_table</a>
</li>
<li<%= sidebar_current("docs-aws-resource-flow-log") %>>
<a href="/docs/providers/aws/r/flow_log.html">aws_flow_log</a>
</li>