From 139557f719a9d2b3338cabeee9320e980f6b8d3f Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Fri, 28 Dec 2018 14:30:20 -0500 Subject: [PATCH 1/5] r/aws_route_table_association: Allow force replacement --- aws/resource_aws_route_table_association.go | 68 +++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/aws/resource_aws_route_table_association.go b/aws/resource_aws_route_table_association.go index 9dc6e0b4bac..57b8bc3a63f 100644 --- a/aws/resource_aws_route_table_association.go +++ b/aws/resource_aws_route_table_association.go @@ -30,6 +30,12 @@ func resourceAwsRouteTableAssociation() *schema.Resource { Type: schema.TypeString, Required: true, }, + + "force_replace": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, }, } } @@ -47,17 +53,40 @@ func resourceAwsRouteTableAssociationCreate(d *schema.ResourceData, meta interfa SubnetId: aws.String(d.Get("subnet_id").(string)), } - var resp *ec2.AssociateRouteTableOutput + var associationID string var err error - err = resource.Retry(5*time.Minute, func() *resource.RetryError { - resp, err = conn.AssociateRouteTable(&associationOpts) + err = resource.Retry(30*time.Second, func() *resource.RetryError { + resp, err := conn.AssociateRouteTable(&associationOpts) if err != nil { if awsErr, ok := err.(awserr.Error); ok { if awsErr.Code() == "InvalidRouteTableID.NotFound" { return resource.RetryableError(awsErr) } + if awsErr.Code() == "Resource.AlreadyAssociated" && d.Get( + "force_replace").(bool) { + + associationID, err = findExistingSubnetAssociationID( + conn, + d.Get("subnet_id").(string), + ) + if err != nil { + return resource.NonRetryableError(err) + } + + log.Print("[INFO] Replacing route table association") + input := &ec2.ReplaceRouteTableAssociationInput{ + AssociationId: aws.String(associationID), + RouteTableId: aws.String(d.Get("route_table_id").(string)), + } + _, err = conn.ReplaceRouteTableAssociation(input) + if err == nil { + return nil + } + } } return resource.NonRetryableError(err) + } else { + associationID = *resp.AssociationId } return nil }) @@ -66,7 +95,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 +182,34 @@ func resourceAwsRouteTableAssociationDelete(d *schema.ResourceData, meta interfa return nil } + +func findExistingSubnetAssociationID(conn *ec2.EC2, subnetID string) (string, error) { + // only way to get association id is through the route table + + input := &ec2.DescribeRouteTablesInput{} + input.Filters = buildEC2AttributeFilterList( + map[string]string{ + "association.subnet-id": subnetID, + }, + ) + + output, err := conn.DescribeRouteTables(input) + if err != nil || len(output.RouteTables) == 0 { + return "", fmt.Errorf("Error finding route table: %v", err) + } + + rt := output.RouteTables[0] + + var associationID string + for _, a := range rt.Associations { + if *a.SubnetId == subnetID { + associationID = *a.RouteTableAssociationId + break + } + } + if associationID == "" { + return "", fmt.Errorf("Error finding route table, ID: %v", *rt.RouteTableId) + } + + return associationID, nil +} From 70204b40bb28dd1c0efafd6557d452ba5ee082d4 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Fri, 28 Dec 2018 14:36:56 -0500 Subject: [PATCH 2/5] r/aws_route_table_association: Update docs --- .../docs/r/route_table_association.html.markdown | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/website/docs/r/route_table_association.html.markdown b/website/docs/r/route_table_association.html.markdown index d631c953b57..63d9276f322 100644 --- a/website/docs/r/route_table_association.html.markdown +++ b/website/docs/r/route_table_association.html.markdown @@ -19,12 +19,25 @@ resource "aws_route_table_association" "a" { } ``` +## Example Replacement Usage + +If the subnet already has an associated route table, normally an error will be thrown if attempting to associate another route table. However, using `force_replace`, no error will be thrown and the subnet will become associated with the new route table instead. + +```hcl +resource "aws_route_table_association" "a" { + subnet_id = "${aws_subnet.foo.id}" + route_table_id = "${aws_route_table.bar.id}" + force_replace = true +} +``` + ## Argument Reference The following arguments are supported: * `subnet_id` - (Required) The subnet ID to create an association. * `route_table_id` - (Required) The ID of the routing table to associate with. +* `force_replace` - (Optional) Boolean indicating whether to replace an existing association or not. ## Attributes Reference From f23a7ff6cfa93b1cb24a9ddcaa0c8dfb7cde5d75 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Fri, 28 Dec 2018 15:22:45 -0500 Subject: [PATCH 3/5] r/aws_route_table_association: Add acc tests --- ...source_aws_route_table_association_test.go | 88 +++++++++++++++++-- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/aws/resource_aws_route_table_association_test.go b/aws/resource_aws_route_table_association_test.go index d565be12af2..b079e6c24da 100644 --- a/aws/resource_aws_route_table_association_test.go +++ b/aws/resource_aws_route_table_association_test.go @@ -38,6 +38,33 @@ func TestAccAWSRouteTableAssociation_basic(t *testing.T) { }) } +func TestAccAWSRouteTableAssociation_replace(t *testing.T) { + var v, v2 ec2.RouteTable + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRouteTableAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRouteTableAssociationConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRouteTableAssociationExists( + "aws_route_table_association.foo", &v), + ), + }, + + { + Config: testAccRouteTableAssociationConfigReplace, + Check: resource.ComposeTestCheckFunc( + testAccCheckRouteTableAssociationExists( + "aws_route_table_association.bar", &v2), + ), + }, + }, + }) +} + func testAccCheckRouteTableAssociationDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).ec2conn @@ -109,7 +136,7 @@ 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 +144,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 +161,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 +176,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 +184,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 +192,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 +202,9 @@ 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" { @@ -180,3 +212,45 @@ resource "aws_route_table_association" "foo" { 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" "bar" { + route_table_id = "${aws_route_table.bar.id}" + subnet_id = "${aws_subnet.foo.id}" + force_replace = true +} +` From 3b1544656f9eb069a723058b9a38e79d42f97d87 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Fri, 19 Jul 2019 18:51:48 -0400 Subject: [PATCH 4/5] r/aws_route_table_association: Allow imports --- aws/resource_aws_route_table_association.go | 62 ++++++++----------- ...source_aws_route_table_association_test.go | 26 ++++++-- .../r/route_table_association.html.markdown | 27 ++++---- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/aws/resource_aws_route_table_association.go b/aws/resource_aws_route_table_association.go index 57b8bc3a63f..a9e4ed4af5c 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,24 +19,20 @@ 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": { Type: schema.TypeString, Required: true, }, - - "force_replace": { - Type: schema.TypeBool, - Optional: true, - Default: false, - }, }, } } @@ -54,35 +51,13 @@ func resourceAwsRouteTableAssociationCreate(d *schema.ResourceData, meta interfa } var associationID string - var err error - err = resource.Retry(30*time.Second, func() *resource.RetryError { + err := resource.Retry(30*time.Second, func() *resource.RetryError { resp, err := conn.AssociateRouteTable(&associationOpts) if err != nil { if awsErr, ok := err.(awserr.Error); ok { if awsErr.Code() == "InvalidRouteTableID.NotFound" { return resource.RetryableError(awsErr) } - if awsErr.Code() == "Resource.AlreadyAssociated" && d.Get( - "force_replace").(bool) { - - associationID, err = findExistingSubnetAssociationID( - conn, - d.Get("subnet_id").(string), - ) - if err != nil { - return resource.NonRetryableError(err) - } - - log.Print("[INFO] Replacing route table association") - input := &ec2.ReplaceRouteTableAssociationInput{ - AssociationId: aws.String(associationID), - RouteTableId: aws.String(d.Get("route_table_id").(string)), - } - _, err = conn.ReplaceRouteTableAssociation(input) - if err == nil { - return nil - } - } } return resource.NonRetryableError(err) } else { @@ -183,19 +158,30 @@ func resourceAwsRouteTableAssociationDelete(d *schema.ResourceData, meta interfa return nil } -func findExistingSubnetAssociationID(conn *ec2.EC2, subnetID string) (string, error) { - // only way to get association id is through the route table +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.subnet-id": subnetID, + "association.route-table-id": routeTableID, }, ) output, err := conn.DescribeRouteTables(input) if err != nil || len(output.RouteTables) == 0 { - return "", fmt.Errorf("Error finding route table: %v", err) + return nil, fmt.Errorf("Error finding route table: %v", err) } rt := output.RouteTables[0] @@ -208,8 +194,12 @@ func findExistingSubnetAssociationID(conn *ec2.EC2, subnetID string) (string, er } } if associationID == "" { - return "", fmt.Errorf("Error finding route table, ID: %v", *rt.RouteTableId) + return nil, fmt.Errorf("Error finding route table, ID: %v", *rt.RouteTableId) } - return associationID, nil + 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 b079e6c24da..e531f2af30f 100644 --- a/aws/resource_aws_route_table_association_test.go +++ b/aws/resource_aws_route_table_association_test.go @@ -40,6 +40,7 @@ func TestAccAWSRouteTableAssociation_basic(t *testing.T) { 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) }, @@ -50,15 +51,20 @@ func TestAccAWSRouteTableAssociation_replace(t *testing.T) { Config: testAccRouteTableAssociationConfig, Check: resource.ComposeTestCheckFunc( testAccCheckRouteTableAssociationExists( - "aws_route_table_association.foo", &v), + resourceName, &v), ), }, - + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAWSRouteTabAssocImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, { Config: testAccRouteTableAssociationConfigReplace, Check: resource.ComposeTestCheckFunc( testAccCheckRouteTableAssociationExists( - "aws_route_table_association.bar", &v2), + resourceName, &v2), ), }, }, @@ -132,6 +138,17 @@ 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" @@ -248,9 +265,8 @@ resource "aws_route_table" "bar" { } } -resource "aws_route_table_association" "bar" { +resource "aws_route_table_association" "foo" { route_table_id = "${aws_route_table.bar.id}" subnet_id = "${aws_subnet.foo.id}" - force_replace = true } ` diff --git a/website/docs/r/route_table_association.html.markdown b/website/docs/r/route_table_association.html.markdown index 63d9276f322..f8f47416772 100644 --- a/website/docs/r/route_table_association.html.markdown +++ b/website/docs/r/route_table_association.html.markdown @@ -19,25 +19,12 @@ resource "aws_route_table_association" "a" { } ``` -## Example Replacement Usage - -If the subnet already has an associated route table, normally an error will be thrown if attempting to associate another route table. However, using `force_replace`, no error will be thrown and the subnet will become associated with the new route table instead. - -```hcl -resource "aws_route_table_association" "a" { - subnet_id = "${aws_subnet.foo.id}" - route_table_id = "${aws_route_table.bar.id}" - force_replace = true -} -``` - ## Argument Reference The following arguments are supported: * `subnet_id` - (Required) The subnet ID to create an association. * `route_table_id` - (Required) The ID of the routing table to associate with. -* `force_replace` - (Optional) Boolean indicating whether to replace an existing association or not. ## Attributes Reference @@ -45,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 +``` From 07a1091128ccb925be9e416051fe607c5e8b960e Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Wed, 31 Jul 2019 13:31:47 -0400 Subject: [PATCH 5/5] Fix up minor issues --- aws/resource_aws_route_table_association.go | 9 ++++----- aws/resource_aws_route_table_association_test.go | 13 ++++++++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/aws/resource_aws_route_table_association.go b/aws/resource_aws_route_table_association.go index a9e4ed4af5c..13727031e9f 100644 --- a/aws/resource_aws_route_table_association.go +++ b/aws/resource_aws_route_table_association.go @@ -51,7 +51,7 @@ func resourceAwsRouteTableAssociationCreate(d *schema.ResourceData, meta interfa } var associationID string - err := resource.Retry(30*time.Second, func() *resource.RetryError { + err := resource.Retry(5*time.Minute, func() *resource.RetryError { resp, err := conn.AssociateRouteTable(&associationOpts) if err != nil { if awsErr, ok := err.(awserr.Error); ok { @@ -60,9 +60,8 @@ func resourceAwsRouteTableAssociationCreate(d *schema.ResourceData, meta interfa } } return resource.NonRetryableError(err) - } else { - associationID = *resp.AssociationId } + associationID = *resp.AssociationId return nil }) if err != nil { @@ -188,8 +187,8 @@ func resourceAwsRouteTableAssociationImport(d *schema.ResourceData, meta interfa var associationID string for _, a := range rt.Associations { - if *a.SubnetId == subnetID { - associationID = *a.RouteTableAssociationId + if aws.StringValue(a.SubnetId) == subnetID { + associationID = aws.StringValue(a.RouteTableAssociationId) break } } diff --git a/aws/resource_aws_route_table_association_test.go b/aws/resource_aws_route_table_association_test.go index e531f2af30f..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,17 +25,22 @@ 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, + }, }, }) }