diff --git a/.changelog/21161.txt b/.changelog/21161.txt new file mode 100644 index 000000000000..923b29c5f64b --- /dev/null +++ b/.changelog/21161.txt @@ -0,0 +1,15 @@ +```release-note:bug +resource/aws_route: Use custom `timeouts` values +``` + +```release-notes:enhancement +resource/aws_route_table: Add [custom `timeouts`](https://www.terraform.io/docs/language/resources/syntax.html#operation-timeouts) block +``` + +```release-notes:enhancement +resource/aws_default_route_table: Add [custom `timeouts`](https://www.terraform.io/docs/language/resources/syntax.html#operation-timeouts) block +``` + +```release-notes:enhancement +resource/aws_vpn_gateway_route_propagation: Add [custom `timeouts`](https://www.terraform.io/docs/language/resources/syntax.html#operation-timeouts) block +``` \ No newline at end of file diff --git a/aws/internal/service/ec2/finder/finder.go b/aws/internal/service/ec2/finder/finder.go index c567d6ecfd74..ea6cb4fcd53b 100644 --- a/aws/internal/service/ec2/finder/finder.go +++ b/aws/internal/service/ec2/finder/finder.go @@ -398,10 +398,7 @@ func RouteTable(conn *ec2.EC2, input *ec2.DescribeRouteTablesInput) (*ec2.RouteT } if output == nil || len(output.RouteTables) == 0 || output.RouteTables[0] == nil { - return nil, &resource.NotFoundError{ - Message: "Empty result", - LastRequest: input, - } + return nil, tfresource.NewEmptyResultError(input) } return output.RouteTables[0], nil @@ -426,7 +423,9 @@ func RouteByIPv4Destination(conn *ec2.EC2, routeTableID, destinationCidr string) } } - return nil, &resource.NotFoundError{} + return nil, &resource.NotFoundError{ + LastError: fmt.Errorf("Route in Route Table (%s) with IPv4 destination (%s) not found", routeTableID, destinationCidr), + } } // RouteByIPv6Destination returns the route corresponding to the specified IPv6 destination. @@ -444,7 +443,9 @@ func RouteByIPv6Destination(conn *ec2.EC2, routeTableID, destinationIpv6Cidr str } } - return nil, &resource.NotFoundError{} + return nil, &resource.NotFoundError{ + LastError: fmt.Errorf("Route in Route Table (%s) with IPv6 destination (%s) not found", routeTableID, destinationIpv6Cidr), + } } // RouteByPrefixListIDDestination returns the route corresponding to the specified prefix list destination. @@ -461,7 +462,9 @@ func RouteByPrefixListIDDestination(conn *ec2.EC2, routeTableID, prefixListID st } } - return nil, &resource.NotFoundError{} + return nil, &resource.NotFoundError{ + LastError: fmt.Errorf("Route in Route Table (%s) with Prefix List ID destination (%s) not found", routeTableID, prefixListID), + } } // SecurityGroupByID looks up a security group by ID. Returns a resource.NotFoundError if not found. @@ -758,10 +761,7 @@ func VpcEndpoint(conn *ec2.EC2, input *ec2.DescribeVpcEndpointsInput) (*ec2.VpcE } if output == nil || len(output.VpcEndpoints) == 0 || output.VpcEndpoints[0] == nil { - return nil, &resource.NotFoundError{ - Message: "Empty result", - LastRequest: input, - } + return nil, tfresource.NewEmptyResultError(input) } return output.VpcEndpoints[0], nil @@ -801,7 +801,7 @@ func VpcEndpointSubnetAssociationExists(conn *ec2.EC2, vpcEndpointID string, sub } return &resource.NotFoundError{ - LastError: fmt.Errorf("VPC Endpoint Subnet Association (%s/%s) not found", vpcEndpointID, subnetID), + LastError: fmt.Errorf("VPC Endpoint (%s) Subnet (%s) Association not found", vpcEndpointID, subnetID), } } @@ -824,6 +824,25 @@ func VpcPeeringConnectionByID(conn *ec2.EC2, id string) (*ec2.VpcPeeringConnecti return output.VpcPeeringConnections[0], nil } +// VpnGatewayRoutePropagationExists returns NotFoundError if no route propagation for the specified VPN gateway is found. +func VpnGatewayRoutePropagationExists(conn *ec2.EC2, routeTableID, gatewayID string) error { + routeTable, err := RouteTableByID(conn, routeTableID) + + if err != nil { + return err + } + + for _, v := range routeTable.PropagatingVgws { + if aws.StringValue(v.GatewayId) == gatewayID { + return nil + } + } + + return &resource.NotFoundError{ + LastError: fmt.Errorf("Route Table (%s) VPN Gateway (%s) route propagation not found", routeTableID, gatewayID), + } +} + // VpnGatewayVpcAttachment returns the attachment between the specified VPN gateway and VPC. // Returns nil and potentially an error if no attachment is found. func VpnGatewayVpcAttachment(conn *ec2.EC2, vpnGatewayID, vpcID string) (*ec2.VpcAttachment, error) { diff --git a/aws/internal/service/ec2/id.go b/aws/internal/service/ec2/id.go index 30728ae105c3..1c09a226d71b 100644 --- a/aws/internal/service/ec2/id.go +++ b/aws/internal/service/ec2/id.go @@ -124,3 +124,20 @@ func VpcEndpointSubnetAssociationCreateID(vpcEndpointID, subnetID string) string func VpnGatewayVpcAttachmentCreateID(vpnGatewayID, vpcID string) string { return fmt.Sprintf("vpn-attachment-%x", hashcode.String(fmt.Sprintf("%s-%s", vpcID, vpnGatewayID))) } + +const vpnGatewayRoutePropagationIDSeparator = "_" + +func VpnGatewayRoutePropagationCreateID(routeTableID, gatewayID string) string { + parts := []string{gatewayID, routeTableID} + id := strings.Join(parts, vpnGatewayRoutePropagationIDSeparator) + return id +} + +func VpnGatewayRoutePropagationParseID(id string) (string, string, error) { + parts := strings.Split(id, vpnGatewayRoutePropagationIDSeparator) + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[1], parts[0], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected vpn-gateway-id%[2]sroute-table-id", id, vpnGatewayRoutePropagationIDSeparator) +} diff --git a/aws/internal/service/ec2/waiter/waiter.go b/aws/internal/service/ec2/waiter/waiter.go index 826a9aa3d8e0..581e39bee09b 100644 --- a/aws/internal/service/ec2/waiter/waiter.go +++ b/aws/internal/service/ec2/waiter/waiter.go @@ -21,6 +21,9 @@ const ( // General timeout for EC2 resource creations to propagate PropagationTimeout = 2 * time.Minute + + RouteTableNotFoundChecks = 1000 // Should exceed any reasonable custom timeout value. + RouteTableAssociationCreatedNotFoundChecks = 1000 // Should exceed any reasonable custom timeout value. ) const ( @@ -264,12 +267,12 @@ const ( NetworkAclEntryPropagationTimeout = 5 * time.Minute ) -func RouteDeleted(conn *ec2.EC2, routeFinder finder.RouteFinder, routeTableID, destination string) (*ec2.Route, error) { +func RouteDeleted(conn *ec2.EC2, routeFinder finder.RouteFinder, routeTableID, destination string, timeout time.Duration) (*ec2.Route, error) { stateConf := &resource.StateChangeConf{ Pending: []string{RouteStatusReady}, Target: []string{}, Refresh: RouteStatus(conn, routeFinder, routeTableID, destination), - Timeout: PropagationTimeout, + Timeout: timeout, ContinuousTargetOccurence: 2, } @@ -282,12 +285,12 @@ func RouteDeleted(conn *ec2.EC2, routeFinder finder.RouteFinder, routeTableID, d return nil, err } -func RouteReady(conn *ec2.EC2, routeFinder finder.RouteFinder, routeTableID, destination string) (*ec2.Route, error) { +func RouteReady(conn *ec2.EC2, routeFinder finder.RouteFinder, routeTableID, destination string, timeout time.Duration) (*ec2.Route, error) { stateConf := &resource.StateChangeConf{ Pending: []string{}, Target: []string{RouteStatusReady}, Refresh: RouteStatus(conn, routeFinder, routeTableID, destination), - Timeout: PropagationTimeout, + Timeout: timeout, ContinuousTargetOccurence: 2, } @@ -306,21 +309,14 @@ const ( RouteTableAssociationCreatedTimeout = 5 * time.Minute RouteTableAssociationUpdatedTimeout = 5 * time.Minute RouteTableAssociationDeletedTimeout = 5 * time.Minute - - RouteTableReadyTimeout = 10 * time.Minute - RouteTableDeletedTimeout = 5 * time.Minute - RouteTableUpdatedTimeout = 5 * time.Minute - - RouteTableNotFoundChecks = 40 - RouteTableAssociationCreatedNotFoundChecks = 40 ) -func RouteTableReady(conn *ec2.EC2, id string) (*ec2.RouteTable, error) { +func RouteTableReady(conn *ec2.EC2, id string, timeout time.Duration) (*ec2.RouteTable, error) { stateConf := &resource.StateChangeConf{ Pending: []string{}, Target: []string{RouteTableStatusReady}, Refresh: RouteTableStatus(conn, id), - Timeout: RouteTableReadyTimeout, + Timeout: timeout, NotFoundChecks: RouteTableNotFoundChecks, } @@ -333,12 +329,12 @@ func RouteTableReady(conn *ec2.EC2, id string) (*ec2.RouteTable, error) { return nil, err } -func RouteTableDeleted(conn *ec2.EC2, id string) (*ec2.RouteTable, error) { +func RouteTableDeleted(conn *ec2.EC2, id string, timeout time.Duration) (*ec2.RouteTable, error) { stateConf := &resource.StateChangeConf{ Pending: []string{RouteTableStatusReady}, Target: []string{}, Refresh: RouteTableStatus(conn, id), - Timeout: RouteTableDeletedTimeout, + Timeout: timeout, } outputRaw, err := stateConf.WaitForState() diff --git a/aws/resource_aws_default_route_table.go b/aws/resource_aws_default_route_table.go index 0d400f4c2008..ef486d58aed5 100644 --- a/aws/resource_aws_default_route_table.go +++ b/aws/resource_aws_default_route_table.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "strings" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" @@ -27,6 +28,11 @@ func resourceAwsDefaultRouteTable() *schema.Resource { State: resourceAwsDefaultRouteTableImport, }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(2 * time.Minute), + Update: schema.DefaultTimeout(2 * time.Minute), + }, + // // The top-level attributes must be a superset of the aws_route_table resource's attributes as common CRUD handlers are used. // @@ -211,7 +217,7 @@ func resourceAwsDefaultRouteTableCreate(d *schema.ResourceData, meta interface{} return fmt.Errorf("error deleting Route in EC2 Default Route Table (%s) with destination (%s): %w", d.Id(), destination, err) } - _, err = waiter.RouteDeleted(conn, routeFinder, routeTableID, destination) + _, err = waiter.RouteDeleted(conn, routeFinder, routeTableID, destination, d.Timeout(schema.TimeoutCreate)) if err != nil { return fmt.Errorf("error waiting for Route in EC2 Default Route Table (%s) with destination (%s) to delete: %w", d.Id(), destination, err) @@ -223,7 +229,7 @@ func resourceAwsDefaultRouteTableCreate(d *schema.ResourceData, meta interface{} for _, v := range v.(*schema.Set).List() { v := v.(string) - if err := ec2RouteTableEnableVgwRoutePropagation(conn, d.Id(), v); err != nil { + if err := ec2RouteTableEnableVgwRoutePropagation(conn, d.Id(), v, d.Timeout(schema.TimeoutCreate)); err != nil { return err } } @@ -234,7 +240,7 @@ func resourceAwsDefaultRouteTableCreate(d *schema.ResourceData, meta interface{} for _, v := range v.(*schema.Set).List() { v := v.(map[string]interface{}) - if err := ec2RouteTableAddRoute(conn, d.Id(), v); err != nil { + if err := ec2RouteTableAddRoute(conn, d.Id(), v, d.Timeout(schema.TimeoutCreate)); err != nil { return err } } diff --git a/aws/resource_aws_route.go b/aws/resource_aws_route.go index 3ec120e060bf..bdea68caf017 100644 --- a/aws/resource_aws_route.go +++ b/aws/resource_aws_route.go @@ -9,7 +9,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/tfawserr" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2" @@ -49,6 +48,7 @@ func resourceAwsRoute() *schema.Resource { Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(2 * time.Minute), + Update: schema.DefaultTimeout(2 * time.Minute), Delete: schema.DefaultTimeout(5 * time.Minute), }, @@ -244,7 +244,7 @@ func resourceAwsRouteCreate(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error creating Route in Route Table (%s) with destination (%s): %w", routeTableID, destination, err) } - _, err = waiter.RouteReady(conn, routeFinder, routeTableID, destination) + _, err = waiter.RouteReady(conn, routeFinder, routeTableID, destination, d.Timeout(schema.TimeoutCreate)) if err != nil { return fmt.Errorf("error waiting for Route in Route Table (%s) with destination (%s) to become available: %w", routeTableID, destination, err) @@ -385,7 +385,7 @@ func resourceAwsRouteUpdate(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error updating Route in Route Table (%s) with destination (%s): %w", routeTableID, destination, err) } - _, err = waiter.RouteReady(conn, routeFinder, routeTableID, destination) + _, err = waiter.RouteReady(conn, routeFinder, routeTableID, destination, d.Timeout(schema.TimeoutUpdate)) if err != nil { return fmt.Errorf("error waiting for Route in Route Table (%s) with destination (%s) to become available: %w", routeTableID, destination, err) @@ -425,34 +425,20 @@ func resourceAwsRouteDelete(d *schema.ResourceData, meta interface{}) error { } log.Printf("[DEBUG] Deleting Route: %s", input) - err = resource.Retry(d.Timeout(schema.TimeoutDelete), func() *resource.RetryError { - _, err = conn.DeleteRoute(input) - - if err == nil { - return nil - } - - if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidRouteNotFound) { - return nil - } - - // Local routes (which may have been imported) cannot be deleted. Remove from state. - if tfawserr.ErrMessageContains(err, tfec2.ErrCodeInvalidParameterValue, "cannot remove local route") { - return nil - } - - if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidParameterException) { - return resource.RetryableError(err) - } - - return resource.NonRetryableError(err) - }) + _, err = tfresource.RetryWhenAwsErrCodeEquals( + d.Timeout(schema.TimeoutDelete), + func() (interface{}, error) { + return conn.DeleteRoute(input) + }, + tfec2.ErrCodeInvalidParameterException, + ) - if tfresource.TimedOut(err) { - _, err = conn.DeleteRoute(input) + if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidRouteNotFound) { + return nil } - if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidRouteNotFound) { + // Local routes (which may have been imported) cannot be deleted. Remove from state. + if tfawserr.ErrMessageContains(err, tfec2.ErrCodeInvalidParameterValue, "cannot remove local route") { return nil } @@ -460,7 +446,7 @@ func resourceAwsRouteDelete(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error deleting Route in Route Table (%s) with destination (%s): %w", routeTableID, destination, err) } - _, err = waiter.RouteDeleted(conn, routeFinder, routeTableID, destination) + _, err = waiter.RouteDeleted(conn, routeFinder, routeTableID, destination, d.Timeout(schema.TimeoutDelete)) if err != nil { return fmt.Errorf("error waiting for Route in Route Table (%s) with destination (%s) to delete: %w", routeTableID, destination, err) diff --git a/aws/resource_aws_route_table.go b/aws/resource_aws_route_table.go index 4095f291c3a0..93c560d3230d 100644 --- a/aws/resource_aws_route_table.go +++ b/aws/resource_aws_route_table.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strings" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" @@ -50,6 +51,12 @@ func resourceAwsRouteTable() *schema.Resource { State: schema.ImportStatePassthrough, }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(2 * time.Minute), + Update: schema.DefaultTimeout(2 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + Schema: map[string]*schema.Schema{ "arn": { Type: schema.TypeString, @@ -181,7 +188,7 @@ func resourceAwsRouteTableCreate(d *schema.ResourceData, meta interface{}) error d.SetId(aws.StringValue(output.RouteTable.RouteTableId)) - if _, err := waiter.RouteTableReady(conn, d.Id()); err != nil { + if _, err := waiter.RouteTableReady(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { return fmt.Errorf("error waiting for Route Table (%s) to become available: %w", d.Id(), err) } @@ -189,7 +196,7 @@ func resourceAwsRouteTableCreate(d *schema.ResourceData, meta interface{}) error for _, v := range v.(*schema.Set).List() { v := v.(string) - if err := ec2RouteTableEnableVgwRoutePropagation(conn, d.Id(), v); err != nil { + if err := ec2RouteTableEnableVgwRoutePropagation(conn, d.Id(), v, d.Timeout(schema.TimeoutCreate)); err != nil { return err } } @@ -199,7 +206,7 @@ func resourceAwsRouteTableCreate(d *schema.ResourceData, meta interface{}) error for _, v := range v.(*schema.Set).List() { v := v.(map[string]interface{}) - if err := ec2RouteTableAddRoute(conn, d.Id(), v); err != nil { + if err := ec2RouteTableAddRoute(conn, d.Id(), v, d.Timeout(schema.TimeoutCreate)); err != nil { return err } } @@ -285,7 +292,7 @@ func resourceAwsRouteTableUpdate(d *schema.ResourceData, meta interface{}) error for _, v := range add { v := v.(string) - if err := ec2RouteTableEnableVgwRoutePropagation(conn, d.Id(), v); err != nil { + if err := ec2RouteTableEnableVgwRoutePropagation(conn, d.Id(), v, d.Timeout(schema.TimeoutCreate)); err != nil { return err } } @@ -312,7 +319,7 @@ func resourceAwsRouteTableUpdate(d *schema.ResourceData, meta interface{}) error addRoute = false if oldTarget != newTarget { - if err := ec2RouteTableUpdateRoute(conn, d.Id(), vNew); err != nil { + if err := ec2RouteTableUpdateRoute(conn, d.Id(), vNew, d.Timeout(schema.TimeoutUpdate)); err != nil { return err } } @@ -320,7 +327,7 @@ func resourceAwsRouteTableUpdate(d *schema.ResourceData, meta interface{}) error } if addRoute { - if err := ec2RouteTableAddRoute(conn, d.Id(), vNew); err != nil { + if err := ec2RouteTableAddRoute(conn, d.Id(), vNew, d.Timeout(schema.TimeoutUpdate)); err != nil { return err } } @@ -344,7 +351,7 @@ func resourceAwsRouteTableUpdate(d *schema.ResourceData, meta interface{}) error } if delRoute { - if err := ec2RouteTableDeleteRoute(conn, d.Id(), vOld); err != nil { + if err := ec2RouteTableDeleteRoute(conn, d.Id(), vOld, d.Timeout(schema.TimeoutUpdate)); err != nil { return err } } @@ -395,7 +402,7 @@ func resourceAwsRouteTableDelete(d *schema.ResourceData, meta interface{}) error // Wait for the route table to really destroy log.Printf("[DEBUG] Waiting for route table (%s) deletion", d.Id()) - if _, err := waiter.RouteTableDeleted(conn, d.Id()); err != nil { + if _, err := waiter.RouteTableDeleted(conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { return fmt.Errorf("error waiting for Route Table (%s) deletion: %w", d.Id(), err) } @@ -469,7 +476,7 @@ func resourceAwsRouteTableHash(v interface{}) int { } // ec2RouteTableAddRoute adds a route to the specified route table. -func ec2RouteTableAddRoute(conn *ec2.EC2, routeTableID string, tfMap map[string]interface{}) error { +func ec2RouteTableAddRoute(conn *ec2.EC2, routeTableID string, tfMap map[string]interface{}, timeout time.Duration) error { if err := validateNestedExactlyOneOf(tfMap, routeTableValidDestinations); err != nil { return fmt.Errorf("error creating route: %w", err) } @@ -502,7 +509,7 @@ func ec2RouteTableAddRoute(conn *ec2.EC2, routeTableID string, tfMap map[string] log.Printf("[DEBUG] Creating Route: %s", input) _, err := tfresource.RetryWhenAwsErrCodeEquals( - waiter.PropagationTimeout, + timeout, func() (interface{}, error) { return conn.CreateRoute(input) }, @@ -514,7 +521,7 @@ func ec2RouteTableAddRoute(conn *ec2.EC2, routeTableID string, tfMap map[string] return fmt.Errorf("error creating Route in Route Table (%s) with destination (%s): %w", routeTableID, destination, err) } - _, err = waiter.RouteReady(conn, routeFinder, routeTableID, destination) + _, err = waiter.RouteReady(conn, routeFinder, routeTableID, destination, timeout) if err != nil { return fmt.Errorf("error waiting for Route in Route Table (%s) with destination (%s) to become available: %w", routeTableID, destination, err) @@ -524,7 +531,7 @@ func ec2RouteTableAddRoute(conn *ec2.EC2, routeTableID string, tfMap map[string] } // ec2RouteTableDeleteRoute deletes a route from the specified route table. -func ec2RouteTableDeleteRoute(conn *ec2.EC2, routeTableID string, tfMap map[string]interface{}) error { +func ec2RouteTableDeleteRoute(conn *ec2.EC2, routeTableID string, tfMap map[string]interface{}, timeout time.Duration) error { destinationAttributeKey, destination := routeTableRouteDestinationAttribute(tfMap) input := &ec2.DeleteRouteInput{ @@ -558,7 +565,7 @@ func ec2RouteTableDeleteRoute(conn *ec2.EC2, routeTableID string, tfMap map[stri return fmt.Errorf("error deleting Route in Route Table (%s) with destination (%s): %w", routeTableID, destination, err) } - _, err = waiter.RouteDeleted(conn, routeFinder, routeTableID, destination) + _, err = waiter.RouteDeleted(conn, routeFinder, routeTableID, destination, timeout) if err != nil { return fmt.Errorf("error waiting for Route in Route Table (%s) with destination (%s) to delete: %w", routeTableID, destination, err) @@ -568,7 +575,7 @@ func ec2RouteTableDeleteRoute(conn *ec2.EC2, routeTableID string, tfMap map[stri } // ec2RouteTableUpdateRoute updates a route in the specified route table. -func ec2RouteTableUpdateRoute(conn *ec2.EC2, routeTableID string, tfMap map[string]interface{}) error { +func ec2RouteTableUpdateRoute(conn *ec2.EC2, routeTableID string, tfMap map[string]interface{}, timeout time.Duration) error { if err := validateNestedExactlyOneOf(tfMap, routeTableValidDestinations); err != nil { return fmt.Errorf("error updating route: %w", err) } @@ -606,7 +613,7 @@ func ec2RouteTableUpdateRoute(conn *ec2.EC2, routeTableID string, tfMap map[stri return fmt.Errorf("error updating Route in Route Table (%s) with destination (%s): %w", routeTableID, destination, err) } - _, err = waiter.RouteReady(conn, routeFinder, routeTableID, destination) + _, err = waiter.RouteReady(conn, routeFinder, routeTableID, destination, timeout) if err != nil { return fmt.Errorf("error waiting for Route in Route Table (%s) with destination (%s) to become available: %w", routeTableID, destination, err) @@ -636,7 +643,7 @@ func ec2RouteTableDisableVgwRoutePropagation(conn *ec2.EC2, routeTableID, gatewa // ec2RouteTableEnableVgwRoutePropagation attempts to enable VGW route propagation. // The specified eventual consistency timeout is respected. // Any error is returned. -func ec2RouteTableEnableVgwRoutePropagation(conn *ec2.EC2, routeTableID, gatewayID string) error { +func ec2RouteTableEnableVgwRoutePropagation(conn *ec2.EC2, routeTableID, gatewayID string, timeout time.Duration) error { input := &ec2.EnableVgwRoutePropagationInput{ GatewayId: aws.String(gatewayID), RouteTableId: aws.String(routeTableID), @@ -644,7 +651,7 @@ func ec2RouteTableEnableVgwRoutePropagation(conn *ec2.EC2, routeTableID, gateway log.Printf("[DEBUG] Enabling Route Table (%s) VPN Gateway (%s) route propagation", routeTableID, gatewayID) _, err := tfresource.RetryWhenAwsErrCodeEquals( - waiter.PropagationTimeout, + timeout, func() (interface{}, error) { return conn.EnableVgwRoutePropagation(input) }, diff --git a/aws/resource_aws_vpn_gateway_route_propagation.go b/aws/resource_aws_vpn_gateway_route_propagation.go index 5bdc6988ded9..e6143ed6c079 100644 --- a/aws/resource_aws_vpn_gateway_route_propagation.go +++ b/aws/resource_aws_vpn_gateway_route_propagation.go @@ -3,11 +3,12 @@ package aws import ( "fmt" "log" + "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/waiter" + tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/finder" "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) @@ -17,13 +18,18 @@ func resourceAwsVpnGatewayRoutePropagation() *schema.Resource { Read: resourceAwsVpnGatewayRoutePropagationRead, Delete: resourceAwsVpnGatewayRoutePropagationDisable, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(2 * time.Minute), + Delete: schema.DefaultTimeout(2 * time.Minute), + }, + Schema: map[string]*schema.Schema{ - "vpn_gateway_id": { + "route_table_id": { Type: schema.TypeString, Required: true, ForceNew: true, }, - "route_table_id": { + "vpn_gateway_id": { Type: schema.TypeString, Required: true, ForceNew: true, @@ -35,35 +41,36 @@ func resourceAwsVpnGatewayRoutePropagation() *schema.Resource { func resourceAwsVpnGatewayRoutePropagationEnable(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn - gwID := d.Get("vpn_gateway_id").(string) - rtID := d.Get("route_table_id").(string) + gatewayID := d.Get("vpn_gateway_id").(string) + routeTableID := d.Get("route_table_id").(string) + err := ec2RouteTableEnableVgwRoutePropagation(conn, routeTableID, gatewayID, d.Timeout(schema.TimeoutCreate)) - log.Printf("[INFO] Enabling VGW propagation from %s to %s", gwID, rtID) - _, err := conn.EnableVgwRoutePropagation(&ec2.EnableVgwRoutePropagationInput{ - GatewayId: aws.String(gwID), - RouteTableId: aws.String(rtID), - }) if err != nil { - return fmt.Errorf("error enabling VGW propagation: %s", err) + return err } - d.SetId(fmt.Sprintf("%s_%s", gwID, rtID)) - return nil + d.SetId(tfec2.VpnGatewayRoutePropagationCreateID(routeTableID, gatewayID)) + + return resourceAwsVpnGatewayRoutePropagationRead(d, meta) } func resourceAwsVpnGatewayRoutePropagationDisable(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn - gwID := d.Get("vpn_gateway_id").(string) - rtID := d.Get("route_table_id").(string) + routeTableID, gatewayID, err := tfec2.VpnGatewayRoutePropagationParseID(d.Id()) - log.Printf("[INFO] Disabling VGW propagation from %s to %s", gwID, rtID) - _, err := conn.DisableVgwRoutePropagation(&ec2.DisableVgwRoutePropagationInput{ - GatewayId: aws.String(gwID), - RouteTableId: aws.String(rtID), - }) if err != nil { - return fmt.Errorf("error disabling VGW propagation: %s", err) + return err + } + + err = ec2RouteTableDisableVgwRoutePropagation(conn, routeTableID, gatewayID) + + if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidRouteTableIDNotFound) { + return nil + } + + if err != nil { + return err } return nil @@ -72,38 +79,22 @@ func resourceAwsVpnGatewayRoutePropagationDisable(d *schema.ResourceData, meta i func resourceAwsVpnGatewayRoutePropagationRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn - gwID := d.Get("vpn_gateway_id").(string) - rtID := d.Get("route_table_id").(string) - - log.Printf("[INFO] Reading route table %s to check for VPN gateway %s", rtID, gwID) - rt, err := waiter.RouteTableReady(conn, rtID) - - if !d.IsNewResource() && tfresource.NotFound(err) { - log.Printf("[WARN] Route table (%s) not found, removing VPN gateway route propagation (%s) from state", rtID, d.Id()) - d.SetId("") - return nil - } + routeTableID, gatewayID, err := tfec2.VpnGatewayRoutePropagationParseID(d.Id()) if err != nil { - return fmt.Errorf("error getting route table (%s) status while reading VPN gateway route propagation: %w", rtID, err) + return err } - if rt == nil { - log.Printf("[INFO] Route table %q doesn't exist, so dropping %q route propagation from state", rtID, gwID) + err = finder.VpnGatewayRoutePropagationExists(conn, routeTableID, gatewayID) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Route Table (%s) VPN Gateway (%s) route propagation not found, removing from state", routeTableID, gatewayID) d.SetId("") return nil } - exists := false - for _, vgw := range rt.PropagatingVgws { - if aws.StringValue(vgw.GatewayId) == gwID { - exists = true - } - } - if !exists { - log.Printf("[INFO] %s is no longer propagating to %s, so dropping route propagation from state", rtID, gwID) - d.SetId("") - return nil + if err != nil { + return fmt.Errorf("error reading Route Table (%s) VPN Gateway (%s) route propagation: %w", routeTableID, gatewayID, err) } return nil diff --git a/aws/resource_aws_vpn_gateway_route_propagation_test.go b/aws/resource_aws_vpn_gateway_route_propagation_test.go index 92f0bf12918b..33314b383ca9 100644 --- a/aws/resource_aws_vpn_gateway_route_propagation_test.go +++ b/aws/resource_aws_vpn_gateway_route_propagation_test.go @@ -1,98 +1,148 @@ package aws import ( - "errors" "fmt" "testing" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/waiter" + tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func TestAccAWSVPNGatewayRoutePropagation_basic(t *testing.T) { - var rtID, gwID string + resourceName := "aws_vpn_gateway_route_propagation.test" + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSVPNGatewayRoutePropagationDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSVPNGatewayRoutePropagation_basic, - Check: func(state *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).ec2conn - - rs := state.RootModule().Resources["aws_vpn_gateway_route_propagation.foo"] - if rs == nil { - return errors.New("missing resource state") - } - - rtID = rs.Primary.Attributes["route_table_id"] - gwID = rs.Primary.Attributes["vpn_gateway_id"] - - rt, err := waiter.RouteTableReady(conn, rtID) - - if err != nil { - return fmt.Errorf("error getting route table (%s) while checking VPN gateway route propagation: %w", rtID, err) - } - - if rt == nil { - return errors.New("route table doesn't exist") - } - - exists := false - for _, vgw := range rt.PropagatingVgws { - if *vgw.GatewayId == gwID { - exists = true - } - } - if !exists { - return errors.New("route table does not list VPN gateway as a propagator") - } - - return nil - }, + Config: testAccAWSVPNGatewayRoutePropagationConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSVPNGatewayRoutePropagationExists(resourceName), + ), }, }, - CheckDestroy: func(state *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).ec2conn - - rt, err := waiter.RouteTableDeleted(conn, rtID) + }) +} - if err != nil { - return fmt.Errorf("error getting route table (%s) status while checking destroy: %w", rtID, err) - } +func TestAccAWSVPNGatewayRoutePropagation_disappears(t *testing.T) { + resourceName := "aws_vpn_gateway_route_propagation.test" + rName := acctest.RandomWithPrefix("tf-acc-test") - if rt != nil { - return errors.New("route table still exists") - } - return nil + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSVPNGatewayRoutePropagationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSVPNGatewayRoutePropagationConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSVPNGatewayRoutePropagationExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsVpnGatewayRoutePropagation(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, }, }) +} + +func testAccCheckAWSVPNGatewayRoutePropagationExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Route Table VPN Gateway route propagation ID is set") + } + + routeTableID, gatewayID, err := tfec2.VpnGatewayRoutePropagationParseID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + err = finder.VpnGatewayRoutePropagationExists(conn, routeTableID, gatewayID) + + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckAWSVPNGatewayRoutePropagationDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpn_gateway_route_propagation" { + continue + } + + routeTableID, gatewayID, err := tfec2.VpnGatewayRoutePropagationParseID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + err = finder.VpnGatewayRoutePropagationExists(conn, routeTableID, gatewayID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + return fmt.Errorf("Route Table (%s) VPN Gateway (%s) route propagation still exists", routeTableID, gatewayID) + } + + return nil } -const testAccAWSVPNGatewayRoutePropagation_basic = ` -resource "aws_vpc" "foo" { +func testAccAWSVPNGatewayRoutePropagationConfigBasic(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { cidr_block = "10.1.0.0/16" tags = { - Name = "terraform-testacc-vpn-gateway-route-propagation" + Name = %[1]q } } -resource "aws_vpn_gateway" "foo" { - vpc_id = aws_vpc.foo.id +resource "aws_vpn_gateway" "test" { + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } } -resource "aws_route_table" "foo" { - vpc_id = aws_vpc.foo.id +resource "aws_route_table" "test" { + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } } -resource "aws_vpn_gateway_route_propagation" "foo" { - vpn_gateway_id = aws_vpn_gateway.foo.id - route_table_id = aws_route_table.foo.id +resource "aws_vpn_gateway_route_propagation" "test" { + vpn_gateway_id = aws_vpn_gateway.test.id + route_table_id = aws_route_table.test.id +} +`, rName) } -` diff --git a/website/docs/r/default_route_table.html.markdown b/website/docs/r/default_route_table.html.markdown index f708e60ab8da..8a0afbe67477 100644 --- a/website/docs/r/default_route_table.html.markdown +++ b/website/docs/r/default_route_table.html.markdown @@ -98,6 +98,13 @@ In addition to all arguments above, the following attributes are exported: * `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). * `vpc_id` - ID of the VPC. +## Timeouts + +`aws_default_route_table` provides the following [Timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) configuration options: + +- `create` - (Default `2 minutes`) Used for route creation +- `update` - (Default `2 minutes`) Used for route creation + ## Import Default VPC route tables can be imported using the `vpc_id`, e.g. diff --git a/website/docs/r/route.html.markdown b/website/docs/r/route.html.markdown index f5679bf935b1..949feff8c856 100644 --- a/website/docs/r/route.html.markdown +++ b/website/docs/r/route.html.markdown @@ -87,6 +87,7 @@ In addition to all arguments above, the following attributes are exported: `aws_route` provides the following [Timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) configuration options: - `create` - (Default `2 minutes`) Used for route creation +- `update` - (Default `2 minutes`) Used for route creation - `delete` - (Default `5 minutes`) Used for route deletion ## Import diff --git a/website/docs/r/route_table.html.markdown b/website/docs/r/route_table.html.markdown index 4c6e782417dc..eb5d15d7ad86 100644 --- a/website/docs/r/route_table.html.markdown +++ b/website/docs/r/route_table.html.markdown @@ -113,6 +113,14 @@ attribute once the route resource is created. * `owner_id` - The ID of the AWS account that owns the route table. * `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). +## Timeouts + +`aws_default_route_table` provides the following [Timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) configuration options: + +- `create` - (Default `2 minutes`) Used for route creation +- `update` - (Default `2 minutes`) Used for route creation +- `delete` - (Default `5 minutes`) Used for route deletion + ## Import Route Tables can be imported using the route table `id`. For example, to import diff --git a/website/docs/r/vpn_gateway_route_propagation.html.markdown b/website/docs/r/vpn_gateway_route_propagation.html.markdown index 24400a3ee5fd..0764c2b36e55 100644 --- a/website/docs/r/vpn_gateway_route_propagation.html.markdown +++ b/website/docs/r/vpn_gateway_route_propagation.html.markdown @@ -33,3 +33,10 @@ The following arguments are required: ## Attributes Reference No additional attributes are exported. + +## Timeouts + +`aws_vpn_gateway_route_propagation` provides the following [Timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) configuration options: + +- `create` - (Default `2 minutes`) Used for propagation creation +- `delete` - (Default `2 minutes`) Used for propagation deletion \ No newline at end of file