diff --git a/aws/resource_aws_route_table_association.go b/aws/resource_aws_route_table_association.go index 9dc6e0b4bac..13727031e9f 100644 --- a/aws/resource_aws_route_table_association.go +++ b/aws/resource_aws_route_table_association.go @@ -3,6 +3,7 @@ package aws import ( "fmt" "log" + "strings" "time" "github.com/aws/aws-sdk-go/aws" @@ -18,12 +19,14 @@ func resourceAwsRouteTableAssociation() *schema.Resource { Read: resourceAwsRouteTableAssociationRead, Update: resourceAwsRouteTableAssociationUpdate, Delete: resourceAwsRouteTableAssociationDelete, + Importer: &schema.ResourceImporter{ + State: resourceAwsRouteTableAssociationImport, + }, Schema: map[string]*schema.Schema{ "subnet_id": { Type: schema.TypeString, Required: true, - ForceNew: true, }, "route_table_id": { @@ -47,10 +50,9 @@ func resourceAwsRouteTableAssociationCreate(d *schema.ResourceData, meta interfa SubnetId: aws.String(d.Get("subnet_id").(string)), } - var resp *ec2.AssociateRouteTableOutput - var err error - err = resource.Retry(5*time.Minute, func() *resource.RetryError { - resp, err = conn.AssociateRouteTable(&associationOpts) + var associationID string + err := resource.Retry(5*time.Minute, func() *resource.RetryError { + resp, err := conn.AssociateRouteTable(&associationOpts) if err != nil { if awsErr, ok := err.(awserr.Error); ok { if awsErr.Code() == "InvalidRouteTableID.NotFound" { @@ -59,6 +61,7 @@ func resourceAwsRouteTableAssociationCreate(d *schema.ResourceData, meta interfa } return resource.NonRetryableError(err) } + associationID = *resp.AssociationId return nil }) if err != nil { @@ -66,7 +69,7 @@ func resourceAwsRouteTableAssociationCreate(d *schema.ResourceData, meta interfa } // Set the ID and return - d.SetId(*resp.AssociationId) + d.SetId(associationID) log.Printf("[INFO] Association ID: %s", d.Id()) return nil @@ -153,3 +156,49 @@ func resourceAwsRouteTableAssociationDelete(d *schema.ResourceData, meta interfa return nil } + +func resourceAwsRouteTableAssociationImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + if len(parts) != 2 { + return []*schema.ResourceData{}, fmt.Errorf("Wrong format for import: %s. Use 'subnet ID/route table ID'", d.Id()) + } + + subnetID := parts[0] + routeTableID := parts[1] + + log.Printf("[DEBUG] Importing route table association, subnet: %s, route table: %s", subnetID, routeTableID) + + conn := meta.(*AWSClient).ec2conn + + input := &ec2.DescribeRouteTablesInput{} + input.Filters = buildEC2AttributeFilterList( + map[string]string{ + "association.subnet-id": subnetID, + "association.route-table-id": routeTableID, + }, + ) + + output, err := conn.DescribeRouteTables(input) + if err != nil || len(output.RouteTables) == 0 { + return nil, fmt.Errorf("Error finding route table: %v", err) + } + + rt := output.RouteTables[0] + + var associationID string + for _, a := range rt.Associations { + if aws.StringValue(a.SubnetId) == subnetID { + associationID = aws.StringValue(a.RouteTableAssociationId) + break + } + } + if associationID == "" { + return nil, fmt.Errorf("Error finding route table, ID: %v", *rt.RouteTableId) + } + + d.SetId(associationID) + d.Set("subnet_id", subnetID) + d.Set("route_table_id", routeTableID) + + return []*schema.ResourceData{d}, nil +} diff --git a/aws/resource_aws_route_table_association_test.go b/aws/resource_aws_route_table_association_test.go index d565be12af2..48ceba7781d 100644 --- a/aws/resource_aws_route_table_association_test.go +++ b/aws/resource_aws_route_table_association_test.go @@ -14,6 +14,8 @@ import ( func TestAccAWSRouteTableAssociation_basic(t *testing.T) { var v, v2 ec2.RouteTable + resourceName := "aws_route_table_association.foo" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -23,15 +25,53 @@ func TestAccAWSRouteTableAssociation_basic(t *testing.T) { Config: testAccRouteTableAssociationConfig, Check: resource.ComposeTestCheckFunc( testAccCheckRouteTableAssociationExists( - "aws_route_table_association.foo", &v), + resourceName, &v), ), }, - { Config: testAccRouteTableAssociationConfigChange, Check: resource.ComposeTestCheckFunc( testAccCheckRouteTableAssociationExists( - "aws_route_table_association.foo", &v2), + resourceName, &v2), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAWSRouteTabAssocImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSRouteTableAssociation_replace(t *testing.T) { + var v, v2 ec2.RouteTable + resourceName := "aws_route_table_association.foo" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRouteTableAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRouteTableAssociationConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRouteTableAssociationExists( + resourceName, &v), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAWSRouteTabAssocImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, + { + Config: testAccRouteTableAssociationConfigReplace, + Check: resource.ComposeTestCheckFunc( + testAccCheckRouteTableAssociationExists( + resourceName, &v2), ), }, }, @@ -105,11 +145,22 @@ func testAccCheckRouteTableAssociationExists(n string, v *ec2.RouteTable) resour } } +func testAccAWSRouteTabAssocImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("not found: %s", resourceName) + } + + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["subnet_id"], rs.Primary.Attributes["route_table_id"]), nil + } +} + const testAccRouteTableAssociationConfig = ` resource "aws_vpc" "foo" { cidr_block = "10.1.0.0/16" tags = { - Name = "terraform-testacc-route-table-association" + Name = "tf-acc-route-table-assoc" } } @@ -117,15 +168,14 @@ resource "aws_subnet" "foo" { vpc_id = "${aws_vpc.foo.id}" cidr_block = "10.1.1.0/24" tags = { - Name = "tf-acc-route-table-association" + Name = "tf-acc-route-table-assoc" } } resource "aws_internet_gateway" "foo" { vpc_id = "${aws_vpc.foo.id}" - tags = { - Name = "terraform-testacc-route-table-association" + Name = "tf-acc-route-table-assoc" } } @@ -135,6 +185,9 @@ resource "aws_route_table" "foo" { cidr_block = "10.0.0.0/8" gateway_id = "${aws_internet_gateway.foo.id}" } + tags = { + Name = "tf-acc-route-table-assoc" + } } resource "aws_route_table_association" "foo" { @@ -147,7 +200,7 @@ const testAccRouteTableAssociationConfigChange = ` resource "aws_vpc" "foo" { cidr_block = "10.1.0.0/16" tags = { - Name = "terraform-testacc-route-table-association" + Name = "tf-acc-route-table-assoc" } } @@ -155,7 +208,7 @@ resource "aws_subnet" "foo" { vpc_id = "${aws_vpc.foo.id}" cidr_block = "10.1.1.0/24" tags = { - Name = "tf-acc-route-table-association" + Name = "tf-acc-route-table-assoc" } } @@ -163,7 +216,7 @@ resource "aws_internet_gateway" "foo" { vpc_id = "${aws_vpc.foo.id}" tags = { - Name = "terraform-testacc-route-table-association" + Name = "tf-acc-route-table-assoc" } } @@ -173,6 +226,50 @@ resource "aws_route_table" "bar" { cidr_block = "10.0.0.0/8" gateway_id = "${aws_internet_gateway.foo.id}" } + tags = { + Name = "tf-acc-route-change-table-assoc" + } +} + +resource "aws_route_table_association" "foo" { + route_table_id = "${aws_route_table.bar.id}" + subnet_id = "${aws_subnet.foo.id}" +} +` + +const testAccRouteTableAssociationConfigReplace = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" + tags = { + Name = "tf-acc-route-table-assoc" + } +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.foo.id}" + cidr_block = "10.1.1.0/24" + tags = { + Name = "tf-acc-route-table-assoc" + } +} + +resource "aws_internet_gateway" "foo" { + vpc_id = "${aws_vpc.foo.id}" + + tags = { + Name = "tf-acc-route-table-assoc" + } +} + +resource "aws_route_table" "bar" { + vpc_id = "${aws_vpc.foo.id}" + route { + cidr_block = "10.0.0.0/16" + gateway_id = "${aws_internet_gateway.foo.id}" + } + tags = { + Name = "tf-acc-replace-route-table-assoc" + } } resource "aws_route_table_association" "foo" { diff --git a/website/docs/r/route_table_association.html.markdown b/website/docs/r/route_table_association.html.markdown index d631c953b57..f8f47416772 100644 --- a/website/docs/r/route_table_association.html.markdown +++ b/website/docs/r/route_table_association.html.markdown @@ -32,3 +32,17 @@ In addition to all arguments above, the following attributes are exported: * `id` - The ID of the association +## Import + +~> **NOTE:** Attempting to associate a route table with a subnet, where either +is already associated, will result in an error (e.g., +`Resource.AlreadyAssociated: the specified association for route table +rtb-4176657279 conflicts with an existing association`) unless you first +import the original association. + +Route table associations can be imported using the subnet and route table IDs. +For example, use this command: + +``` +$ terraform import aws_route_table_association.assoc subnet-6777656e646f6c796e/rtb-656c65616e6f72 +```