diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index f73580d0f..8bcc4bf8a 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -231,6 +231,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..a85388c0e --- /dev/null +++ b/builtin/providers/aws/resource_aws_route.go @@ -0,0 +1,278 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awsutil" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +// AWS Route resource Schema delcaration +func resourceAwsRoute() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRouteCreate, + Read: resourceAwsRouteRead, + Update: resourceAwsRouteUpdate, + Delete: resourceAwsRouteDelete, + + Schema: map[string]*schema.Schema{ + "destination_cidr_block": &schema.Schema{ + Type: schema.TypeString, + Required: 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", + } + createOpts := &ec2.CreateRouteInput{} + + // 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 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", awsutil.Prettify(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", awsutil.Prettify(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.DestinationPrefixListID) + 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)), + } + 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", awsutil.Prettify(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{ + DestinationCIDRBlock: aws.String(d.Get("destination_cidr_block").(string)), + RouteTableID: aws.String(d.Get("route_table_id").(string)), + } + log.Printf("[DEBUG] Route delete opts: %s", awsutil.Prettify(deleteOpts)) + + resp, err := conn.DeleteRoute(deleteOpts) + log.Printf("[DEBUG] Route delete result: %s", awsutil.Prettify(resp)) + if err != nil { + return err + } + + d.SetId("") + + return 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_test.go b/builtin/providers/aws/resource_aws_route_test.go new file mode 100644 index 000000000..39841a6bb --- /dev/null +++ b/builtin/providers/aws/resource_aws_route_test.go @@ -0,0 +1,120 @@ +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 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRouteDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSRouteConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRouteExists("aws_route.bar", &route), + testAccCheckAWSRouteAttributes(&route), + ), + }, + }, + }) +} + +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.ID, + rs.Primary.Attributes["destination_cidr_block"], + ) + + if err != nil { + return err + } + + if r == nil { + return fmt.Errorf("Route not found") + } + + return nil + } +} + +func testAccCheckAWSRouteAttributes(route *ec2.Route) resource.TestCheckFunc { + return func(s *terraform.State) error { + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_route" { + continue + } + if *route.DestinationCIDRBlock != rs.Primary.Attributes["destination_cidr_block"] { + return fmt.Errorf("Bad Destination CIDR Block on Route\n\t expected: %s\n\tgot %s\n", rs.Primary.Attributes["destination_cidr_block"], *route.DestinationCIDRBlock) + } + } + + 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.ID, + rs.Primary.Attributes["destination_cidr_block"], + ) + + if route == nil && err == nil { + return nil + } + } + + return nil +} + +var testAccAWSRouteConfig = 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" "foo" { + route_table_id = "${aws_route_table.foo.id}" + destination_cidr_block = "${aws_vpc.foo.cidr_block}" + 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..42ae71be1 --- /dev/null +++ b/website/source/docs/providers/aws/r/route.html.markdown @@ -0,0 +1,51 @@ +--- +layout: "aws" +page_title: "AWS: aws_route_table" +sidebar_current: "docs-aws-resource-route-table|" +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. + +## 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..e75025afa 100644 --- a/website/source/docs/providers/aws/r/route_table.html.markdown +++ b/website/source/docs/providers/aws/r/route_table.html.markdown @@ -50,5 +50,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 4b34da23a..da61af0d3 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -401,6 +401,10 @@ aws_route53_zone_association +