diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 299aaa2c6..5ed49de86 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -242,6 +242,7 @@ func Provider() terraform.ResourceProvider { "aws_route53_zone_association": resourceAwsRoute53ZoneAssociation(), "aws_route53_zone": resourceAwsRoute53Zone(), "aws_route53_health_check": resourceAwsRoute53HealthCheck(), + "aws_route": resourceAwsRoute(), "aws_route_table": resourceAwsRouteTable(), "aws_route_table_association": resourceAwsRouteTableAssociation(), "aws_s3_bucket": resourceAwsS3Bucket(), diff --git a/builtin/providers/aws/resource_aws_route.go b/builtin/providers/aws/resource_aws_route.go new file mode 100644 index 000000000..96f731484 --- /dev/null +++ b/builtin/providers/aws/resource_aws_route.go @@ -0,0 +1,304 @@ +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/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +// AWS Route resource Schema declaration +func resourceAwsRoute() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRouteCreate, + Read: resourceAwsRouteRead, + Update: resourceAwsRouteUpdate, + Delete: resourceAwsRouteDelete, + Exists: resourceAwsRouteExists, + + Schema: map[string]*schema.Schema{ + "destination_cidr_block": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "destination_prefix_list_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "gateway_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "instance_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "instance_owner_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "network_interface_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "origin": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "state": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "route_table_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "vpc_peering_connection_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceAwsRouteCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + var numTargets int + var setTarget string + allowedTargets := []string{ + "gateway_id", + "instance_id", + "network_interface_id", + "vpc_peering_connection_id", + } + + // Check if more than 1 target is specified + for _, target := range allowedTargets { + if len(d.Get(target).(string)) > 0 { + numTargets++ + setTarget = target + } + } + + if numTargets > 1 { + fmt.Errorf("Error: more than 1 target specified. Only 1 of gateway_id" + + "instance_id, network_interface_id, route_table_id or" + + "vpc_peering_connection_id is allowed.") + } + + createOpts := &ec2.CreateRouteInput{} + // Formulate CreateRouteInput based on the target type + switch setTarget { + case "gateway_id": + createOpts = &ec2.CreateRouteInput{ + RouteTableId: aws.String(d.Get("route_table_id").(string)), + DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), + GatewayId: aws.String(d.Get("gateway_id").(string)), + } + case "instance_id": + createOpts = &ec2.CreateRouteInput{ + RouteTableId: aws.String(d.Get("route_table_id").(string)), + DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), + InstanceId: aws.String(d.Get("instance_id").(string)), + } + case "network_interface_id": + createOpts = &ec2.CreateRouteInput{ + RouteTableId: aws.String(d.Get("route_table_id").(string)), + DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), + NetworkInterfaceId: aws.String(d.Get("network_interface_id").(string)), + } + case "vpc_peering_connection_id": + createOpts = &ec2.CreateRouteInput{ + RouteTableId: aws.String(d.Get("route_table_id").(string)), + DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), + VpcPeeringConnectionId: aws.String(d.Get("vpc_peering_connection_id").(string)), + } + default: + fmt.Errorf("Error: invalid target type specified.") + } + log.Printf("[DEBUG] Route create config: %s", createOpts) + + // Create the route + _, err := conn.CreateRoute(createOpts) + if err != nil { + return fmt.Errorf("Error creating route: %s", err) + } + + route, err := findResourceRoute(conn, d.Get("route_table_id").(string), d.Get("destination_cidr_block").(string)) + if err != nil { + fmt.Errorf("Error: %s", err) + } + + d.SetId(routeIDHash(d, route)) + + return resourceAwsRouteRead(d, meta) +} + +func resourceAwsRouteRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + route, err := findResourceRoute(conn, d.Get("route_table_id").(string), d.Get("destination_cidr_block").(string)) + if err != nil { + return err + } + + d.Set("destination_prefix_list_id", route.DestinationPrefixListId) + d.Set("gateway_id", route.GatewayId) + d.Set("instance_id", route.InstanceId) + d.Set("instance_owner_id", route.InstanceOwnerId) + d.Set("network_interface_id", route.NetworkInterfaceId) + d.Set("origin", route.Origin) + d.Set("state", route.State) + d.Set("vpc_peering_connection_id", route.VpcPeeringConnectionId) + + return nil +} + +func resourceAwsRouteUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + var numTargets int + var setTarget string + allowedTargets := []string{ + "gateway_id", + "instance_id", + "network_interface_id", + "vpc_peering_connection_id", + } + replaceOpts := &ec2.ReplaceRouteInput{} + + // Check if more than 1 target is specified + for _, target := range allowedTargets { + if len(d.Get(target).(string)) > 0 { + numTargets++ + setTarget = target + } + } + + if numTargets > 1 { + fmt.Errorf("Error: more than 1 target specified. Only 1 of gateway_id" + + "instance_id, network_interface_id, route_table_id or" + + "vpc_peering_connection_id is allowed.") + } + + // Formulate ReplaceRouteInput based on the target type + switch setTarget { + case "gateway_id": + replaceOpts = &ec2.ReplaceRouteInput{ + RouteTableId: aws.String(d.Get("route_table_id").(string)), + DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), + GatewayId: aws.String(d.Get("gateway_id").(string)), + } + case "instance_id": + replaceOpts = &ec2.ReplaceRouteInput{ + RouteTableId: aws.String(d.Get("route_table_id").(string)), + DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), + InstanceId: aws.String(d.Get("instance_id").(string)), + //NOOP: Ensure we don't blow away network interface id that is set after instance is launched + NetworkInterfaceId: aws.String(d.Get("network_interface_id").(string)), + } + case "network_interface_id": + replaceOpts = &ec2.ReplaceRouteInput{ + RouteTableId: aws.String(d.Get("route_table_id").(string)), + DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), + NetworkInterfaceId: aws.String(d.Get("network_interface_id").(string)), + } + case "vpc_peering_connection_id": + replaceOpts = &ec2.ReplaceRouteInput{ + RouteTableId: aws.String(d.Get("route_table_id").(string)), + DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), + VpcPeeringConnectionId: aws.String(d.Get("vpc_peering_connection_id").(string)), + } + default: + fmt.Errorf("Error: invalid target type specified.") + } + log.Printf("[DEBUG] Route replace config: %s", replaceOpts) + + // Replace the route + _, err := conn.ReplaceRoute(replaceOpts) + if err != nil { + return err + } + + return nil +} + +func resourceAwsRouteDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + deleteOpts := &ec2.DeleteRouteInput{ + RouteTableId: aws.String(d.Get("route_table_id").(string)), + DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), + } + log.Printf("[DEBUG] Route delete opts: %s", deleteOpts) + + resp, err := conn.DeleteRoute(deleteOpts) + log.Printf("[DEBUG] Route delete result: %s", resp) + if err != nil { + return err + } + + d.SetId("") + return nil +} + +func resourceAwsRouteExists(d *schema.ResourceData, meta interface{}) (bool, error) { + conn := meta.(*AWSClient).ec2conn + routeTableId := d.Get("route_table_id").(string) + + findOpts := &ec2.DescribeRouteTablesInput{ + RouteTableIds: []*string{&routeTableId}, + } + + res, err := conn.DescribeRouteTables(findOpts) + if err != nil { + return false, err + } + + cidr := d.Get("destination_cidr_block").(string) + for _, route := range (*res.RouteTables[0]).Routes { + if *route.DestinationCidrBlock == cidr { + return true, nil + } + } + + return false, nil +} + +// Create an ID for a route +func routeIDHash(d *schema.ResourceData, r *ec2.Route) string { + return fmt.Sprintf("r-%s%d", d.Get("route_table_id").(string), hashcode.String(*r.DestinationCidrBlock)) +} + +// Helper: retrieve a route +func findResourceRoute(conn *ec2.EC2, rtbid string, cidr string) (*ec2.Route, error) { + routeTableID := rtbid + + findOpts := &ec2.DescribeRouteTablesInput{ + RouteTableIds: []*string{&routeTableID}, + } + + resp, err := conn.DescribeRouteTables(findOpts) + if err != nil { + return nil, err + } + + for _, route := range (*resp.RouteTables[0]).Routes { + if *route.DestinationCidrBlock == cidr { + return route, nil + } + } + + return nil, nil +} diff --git a/builtin/providers/aws/resource_aws_route_table.go b/builtin/providers/aws/resource_aws_route_table.go index d5b76f715..38e95363e 100644 --- a/builtin/providers/aws/resource_aws_route_table.go +++ b/builtin/providers/aws/resource_aws_route_table.go @@ -41,6 +41,7 @@ func resourceAwsRouteTable() *schema.Resource { "route": &schema.Schema{ Type: schema.TypeSet, + Computed: true, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ diff --git a/builtin/providers/aws/resource_aws_route_test.go b/builtin/providers/aws/resource_aws_route_test.go new file mode 100644 index 000000000..2e59e35aa --- /dev/null +++ b/builtin/providers/aws/resource_aws_route_test.go @@ -0,0 +1,298 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSRoute_basic(t *testing.T) { + var route ec2.Route + + //aws creates a default route + testCheck := func(s *terraform.State) error { + if *route.DestinationCidrBlock != "10.3.0.0/16" { + return fmt.Errorf("Destination Cidr (Expected=%s, Actual=%s)\n", "10.3.0.0/16", *route.DestinationCidrBlock) + } + + name := "aws_internet_gateway.foo" + gwres, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s\n", name) + } + + if *route.GatewayId != gwres.Primary.ID { + return fmt.Errorf("Internet Gateway Id (Expected=%s, Actual=%s)\n", gwres.Primary.ID, *route.GatewayId) + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRouteDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSRouteBasicConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRouteExists("aws_route.bar", &route), + testCheck, + ), + }, + }, + }) +} + +func TestAccAWSRoute_changeCidr(t *testing.T) { + var route ec2.Route + var routeTable ec2.RouteTable + + //aws creates a default route + testCheck := func(s *terraform.State) error { + if *route.DestinationCidrBlock != "10.3.0.0/16" { + return fmt.Errorf("Destination Cidr (Expected=%s, Actual=%s)\n", "10.3.0.0/16", *route.DestinationCidrBlock) + } + + name := "aws_internet_gateway.foo" + gwres, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s\n", name) + } + + if *route.GatewayId != gwres.Primary.ID { + return fmt.Errorf("Internet Gateway Id (Expected=%s, Actual=%s)\n", gwres.Primary.ID, *route.GatewayId) + } + + return nil + } + + testCheckChange := func(s *terraform.State) error { + if *route.DestinationCidrBlock != "10.2.0.0/16" { + return fmt.Errorf("Destination Cidr (Expected=%s, Actual=%s)\n", "10.2.0.0/16", *route.DestinationCidrBlock) + } + + name := "aws_internet_gateway.foo" + gwres, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s\n", name) + } + + if *route.GatewayId != gwres.Primary.ID { + return fmt.Errorf("Internet Gateway Id (Expected=%s, Actual=%s)\n", gwres.Primary.ID, *route.GatewayId) + } + + if rtlen := len(routeTable.Routes); rtlen != 2 { + return fmt.Errorf("Route Table has too many routes (Expected=%d, Actual=%d)\n", rtlen, 2) + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRouteDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSRouteBasicConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRouteExists("aws_route.bar", &route), + testCheck, + ), + }, + resource.TestStep{ + Config: testAccAWSRouteBasicConfigChangeCidr, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRouteExists("aws_route.bar", &route), + testAccCheckRouteTableExists("aws_route_table.foo", &routeTable), + testCheckChange, + ), + }, + }, + }) +} + +// Acceptance test if mixed inline and external routes are implemented +/* +func TestAccAWSRoute_mix(t *testing.T) { + var rt ec2.RouteTable + var route ec2.Route + + //aws creates a default route + testCheck := func(s *terraform.State) error { + if *route.DestinationCidrBlock != "0.0.0.0/0" { + return fmt.Errorf("Destination Cidr (Expected=%s, Actual=%s)\n", "0.0.0.0/0", *route.DestinationCidrBlock) + } + + name := "aws_internet_gateway.foo" + gwres, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s\n", name) + } + + if *route.GatewayId != gwres.Primary.ID { + return fmt.Errorf("Internet Gateway Id (Expected=%s, Actual=%s)\n", gwres.Primary.ID, *route.GatewayId) + } + + if len(rt.Routes) != 3 { + return fmt.Errorf("bad routes: %#v", rt.Routes) + } + + routes := make(map[string]*ec2.Route) + for _, r := range rt.Routes { + routes[*r.DestinationCidrBlock] = r + } + + if _, ok := routes["10.1.0.0/16"]; !ok { + return fmt.Errorf("Missing route %s: %#v", "10.1.0.0/16", rt.Routes) + } + if _, ok := routes["10.2.0.0/16"]; !ok { + return fmt.Errorf("Missing route %s: %#v", "10.2.0.0/16", rt.Routes) + } + if _, ok := routes["0.0.0.0/0"]; !ok { + return fmt.Errorf("Missing route %s: %#v", "0.0.0.0/0", rt.Routes) + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRouteDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSRouteMixConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRouteTableExists("aws_route_table.foo", &rt), + testAccCheckAWSRouteExists("aws_route.bar", &route), + testCheck, + ), + }, + }, + }) +} +*/ + +func testAccCheckAWSRouteExists(n string, res *ec2.Route) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s\n", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + r, err := findResourceRoute( + conn, + rs.Primary.Attributes["route_table_id"], + rs.Primary.Attributes["destination_cidr_block"], + ) + + if err != nil { + return err + } + + if r == nil { + return fmt.Errorf("Route not found") + } + + *res = *r + + return nil + } +} + +func testAccCheckAWSRouteDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_route" { + continue + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + route, err := findResourceRoute( + conn, + rs.Primary.Attributes["route_table_id"], + rs.Primary.Attributes["destination_cidr_block"], + ) + + if route == nil && err == nil { + return nil + } + } + + return nil +} + +var testAccAWSRouteBasicConfig = fmt.Sprint(` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_internet_gateway" "foo" { + vpc_id = "${aws_vpc.foo.id}" +} + +resource "aws_route_table" "foo" { + vpc_id = "${aws_vpc.foo.id}" +} + +resource "aws_route" "bar" { + route_table_id = "${aws_route_table.foo.id}" + destination_cidr_block = "10.3.0.0/16" + gateway_id = "${aws_internet_gateway.foo.id}" +} +`) + +var testAccAWSRouteBasicConfigChangeCidr = fmt.Sprint(` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_internet_gateway" "foo" { + vpc_id = "${aws_vpc.foo.id}" +} + +resource "aws_route_table" "foo" { + vpc_id = "${aws_vpc.foo.id}" +} + +resource "aws_route" "bar" { + route_table_id = "${aws_route_table.foo.id}" + destination_cidr_block = "10.2.0.0/16" + gateway_id = "${aws_internet_gateway.foo.id}" +} +`) + +// Acceptance test if mixed inline and external routes are implemented +var testAccAWSRouteMixConfig = fmt.Sprint(` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +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.2.0.0/16" + gateway_id = "${aws_internet_gateway.foo.id}" + } +} + +resource "aws_route" "bar" { + route_table_id = "${aws_route_table.foo.id}" + destination_cidr_block = "0.0.0.0/0" + gateway_id = "${aws_internet_gateway.foo.id}" +} +`) diff --git a/website/source/docs/providers/aws/r/route.html.markdown b/website/source/docs/providers/aws/r/route.html.markdown new file mode 100644 index 000000000..3606555e6 --- /dev/null +++ b/website/source/docs/providers/aws/r/route.html.markdown @@ -0,0 +1,57 @@ +--- +layout: "aws" +page_title: "AWS: aws_route" +sidebar_current: "docs-aws-resource-route|" +description: |- + Provides a resource to create a routing entry in a VPC routing table. +--- + +# aws\_route + +Provides a resource to create a routing table entry (a route) in a VPC routing table. + +~> **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 rules. + +## Example usage: + +``` +resource "aws_route" "r" { + route_table_id = "rtb-4fbb3ac4" + destination_cidr_block = "10.0.1.0/22" + vpc_peering_connection_id = "pcx-45ff3dc1" + depends_on = ["aws_route_table.testing"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `route_table_id` - (Required) The ID of the routing table. +* `destination_cidr_block` - (Required) The destination CIDR block. +* `vpc_peering_connection_id` - (Optional) An ID of a VPC peering connection. +* `gateway_id` - (Optional) An ID of a VPC internet gateway or a virtual private gateway. +* `instance_id` - (Optional) An ID of a NAT instance. +* `network_interface_id` - (Optional) An ID of a network interface. + +Each route must contain either a `gateway_id`, an `instance_id` or 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: + +~> **NOTE:** Only the target type that is specified (one of the above) +will be exported as an attribute once the resource is created. + +* `route_table_id` - The ID of the routing table. +* `destination_cidr_block` - The destination CIDR block. +* `vpc_peering_connection_id` - An ID of a VPC peering connection. +* `gateway_id` - An ID of a VPC internet gateway or a virtual private gateway. +* `instance_id` - An ID of a NAT instance. +* `network_interface_id` - An ID of a network interface. diff --git a/website/source/docs/providers/aws/r/route_table.html.markdown b/website/source/docs/providers/aws/r/route_table.html.markdown index 1bbeb45c3..e751b7193 100644 --- a/website/source/docs/providers/aws/r/route_table.html.markdown +++ b/website/source/docs/providers/aws/r/route_table.html.markdown @@ -10,6 +10,12 @@ description: |- Provides a resource to create a VPC routing table. +~> **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 rules. + ## Example usage with tags: ``` @@ -50,5 +56,7 @@ is created implicitly and cannot be specified. ## Attributes Reference The following attributes are exported: +~> **NOTE:** Only the target that is entered is exported as a readable +attribute once the route resource is created. * `id` - The ID of the routing table diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index f68dbeb6d..a12f55972 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -433,6 +433,10 @@ aws_route53_zone_association + > + aws_route + +