Skip to content

Commit

Permalink
Merge pull request #19426 from alewando/route-table-read-retry
Browse files Browse the repository at this point in the history
aws_route(_table): Wrap CRUD operations on route tables and routes in retries
  • Loading branch information
ewbankkit authored Jun 18, 2021
2 parents 4e252a8 + c2953e4 commit 3e31593
Show file tree
Hide file tree
Showing 33 changed files with 2,163 additions and 1,815 deletions.
23 changes: 23 additions & 0 deletions .changelog/19426.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
```release-note:enhancement
resource/aws_route_table: Add retries when creating, deleting and replacing routes
```

```release-note:enhancement
resource/aws_route: Add retries when creating, deleting and replacing routes
```

```release-note:enhancement
resource/aws_default_route_table: Add retries when creating, deleting and replacing routes
```

```release-note:enhancement
resource/aws_default_route_table: Add retries when creating, deleting and replacing routes
```

```release-note:enhancement
resource/aws_main_route_table_association: Wait for association to reach the required state
```

```release-note:enhancement
resource/aws_route_table_association: Wait for association to reach the required state
```
1 change: 1 addition & 0 deletions .semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ rules:
paths:
exclude:
- "*_test.go"
- aws/internal/tfresource/*.go
include:
- aws/
patterns:
Expand Down
3 changes: 2 additions & 1 deletion aws/diff_suppress_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
awspolicy "github.com/jen20/awspolicyequivalence"
tfnet "github.com/terraform-providers/terraform-provider-aws/aws/internal/net"
)

func suppressEquivalentAwsPolicyDiffs(k, old, new string, d *schema.ResourceData) bool {
Expand Down Expand Up @@ -131,7 +132,7 @@ func suppressEquivalentJsonOrYamlDiffs(k, old, new string, d *schema.ResourceDat
// suppressEqualCIDRBlockDiffs provides custom difference suppression for CIDR blocks
// that have different string values but represent the same CIDR.
func suppressEqualCIDRBlockDiffs(k, old, new string, d *schema.ResourceData) bool {
return cidrBlocksEqual(old, new)
return tfnet.CIDRBlocksEqual(old, new)
}

// suppressEquivalentTime suppresses differences for time values that represent the same
Expand Down
5 changes: 3 additions & 2 deletions aws/ec2_transit_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
tfnet "github.com/terraform-providers/terraform-provider-aws/aws/internal/net"
)

func decodeEc2TransitGatewayRouteID(id string) (string, string, error) {
Expand Down Expand Up @@ -109,8 +110,8 @@ func ec2DescribeTransitGatewayRoute(conn *ec2.EC2, transitGatewayRouteTableID, d
if route == nil {
continue
}
if cidrBlocksEqual(aws.StringValue(route.DestinationCidrBlock), destination) {
cidrString := canonicalCidrBlock(aws.StringValue(route.DestinationCidrBlock))
if tfnet.CIDRBlocksEqual(aws.StringValue(route.DestinationCidrBlock), destination) {
cidrString := tfnet.CanonicalCIDRBlock(aws.StringValue(route.DestinationCidrBlock))
route.DestinationCidrBlock = aws.String(cidrString)
return route, nil
}
Expand Down
11 changes: 11 additions & 0 deletions aws/internal/net/cidr.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,14 @@ func CIDRBlocksEqual(cidr1, cidr2 string) bool {

return ip2.String() == ip1.String() && ipnet2.String() == ipnet1.String()
}

// CanonicalCIDRBlock returns the canonical representation of a CIDR block.
// This function is especially useful for hash functions for sets which include IPv6 CIDR blocks.
func CanonicalCIDRBlock(cidr string) string {
_, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return cidr
}

return ipnet.String()
}
25 changes: 22 additions & 3 deletions aws/internal/net/cidr_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package net
package net_test

import (
"testing"

tfnet "github.com/terraform-providers/terraform-provider-aws/aws/internal/net"
)

func Test_CIDRBlocksEqual(t *testing.T) {
func TestCIDRBlocksEqual(t *testing.T) {
for _, ts := range []struct {
cidr1 string
cidr2 string
Expand All @@ -18,9 +20,26 @@ func Test_CIDRBlocksEqual(t *testing.T) {
{"::/0", "::0/0", true},
{"", "", false},
} {
equal := CIDRBlocksEqual(ts.cidr1, ts.cidr2)
equal := tfnet.CIDRBlocksEqual(ts.cidr1, ts.cidr2)
if ts.equal != equal {
t.Fatalf("CIDRBlocksEqual(%q, %q) should be: %t", ts.cidr1, ts.cidr2, ts.equal)
}
}
}

func TestCanonicalCIDRBlock(t *testing.T) {
for _, ts := range []struct {
cidr string
expected string
}{
{"10.2.2.0/24", "10.2.2.0/24"},
{"::/0", "::/0"},
{"::0/0", "::/0"},
{"", ""},
} {
got := tfnet.CanonicalCIDRBlock(ts.cidr)
if ts.expected != got {
t.Fatalf("CanonicalCIDRBlock(%q) should be: %q, got: %q", ts.cidr, ts.expected, got)
}
}
}
12 changes: 12 additions & 0 deletions aws/internal/service/ec2/enum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ec2

const (
// https://docs.aws.amazon.com/vpc/latest/privatelink/vpce-interface.html#vpce-interface-lifecycle
VpcEndpointStateAvailable = "available"
VpcEndpointStateDeleted = "deleted"
VpcEndpointStateDeleting = "deleting"
VpcEndpointStateFailed = "failed"
VpcEndpointStatePending = "pending"
VpcEndpointStatePendingAcceptance = "pendingAcceptance"
VpcEndpointStateRejected = "rejected"
)
11 changes: 8 additions & 3 deletions aws/internal/service/ec2/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import (

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/go-multierror"
multierror "github.com/hashicorp/go-multierror"
)

const (
ErrCodeInvalidParameterException = "InvalidParameterException"
ErrCodeInvalidParameterValue = "InvalidParameterValue"
ErrCodeGatewayNotAttached = "Gateway.NotAttached"
ErrCodeInvalidAssociationIDNotFound = "InvalidAssociationID.NotFound"
ErrCodeInvalidParameter = "InvalidParameter"
ErrCodeInvalidParameterException = "InvalidParameterException"
ErrCodeInvalidParameterValue = "InvalidParameterValue"
)

const (
Expand All @@ -27,6 +30,7 @@ const (

const (
ErrCodeInvalidRouteNotFound = "InvalidRoute.NotFound"
ErrCodeInvalidRouteTableIdNotFound = "InvalidRouteTableId.NotFound"
ErrCodeInvalidRouteTableIDNotFound = "InvalidRouteTableID.NotFound"
)

Expand Down Expand Up @@ -55,6 +59,7 @@ const (
)

const (
ErrCodeInvalidSubnetIdNotFound = "InvalidSubnetId.NotFound"
ErrCodeInvalidSubnetIDNotFound = "InvalidSubnetID.NotFound"
)

Expand Down
168 changes: 143 additions & 25 deletions aws/internal/service/ec2/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,87 @@ func NetworkInterfaceSecurityGroup(conn *ec2.EC2, networkInterfaceID string, sec
return result, err
}

// MainRouteTableAssociationByID returns the main route table association corresponding to the specified identifier.
// Returns NotFoundError if no route table association is found.
func MainRouteTableAssociationByID(conn *ec2.EC2, associationID string) (*ec2.RouteTableAssociation, error) {
association, err := RouteTableAssociationByID(conn, associationID)

if err != nil {
return nil, err
}

if !aws.BoolValue(association.Main) {
return nil, &resource.NotFoundError{
Message: fmt.Sprintf("%s is not the association with the main route table", associationID),
}
}

return association, err
}

// MainRouteTableAssociationByVpcID returns the main route table association for the specified VPC.
// Returns NotFoundError if no route table association is found.
func MainRouteTableAssociationByVpcID(conn *ec2.EC2, vpcID string) (*ec2.RouteTableAssociation, error) {
routeTable, err := MainRouteTableByVpcID(conn, vpcID)

if err != nil {
return nil, err
}

for _, association := range routeTable.Associations {
if aws.BoolValue(association.Main) {
if state := aws.StringValue(association.AssociationState.State); state == ec2.RouteTableAssociationStateCodeDisassociated {
continue
}

return association, nil
}
}

return nil, &resource.NotFoundError{}
}

// RouteTableAssociationByID returns the route table association corresponding to the specified identifier.
// Returns NotFoundError if no route table association is found.
func RouteTableAssociationByID(conn *ec2.EC2, associationID string) (*ec2.RouteTableAssociation, error) {
input := &ec2.DescribeRouteTablesInput{
Filters: tfec2.BuildAttributeFilterList(map[string]string{
"association.route-table-association-id": associationID,
}),
}

routeTable, err := RouteTable(conn, input)

if err != nil {
return nil, err
}

for _, association := range routeTable.Associations {
if aws.StringValue(association.RouteTableAssociationId) == associationID {
if state := aws.StringValue(association.AssociationState.State); state == ec2.RouteTableAssociationStateCodeDisassociated {
return nil, &resource.NotFoundError{Message: state}
}

return association, nil
}
}

return nil, &resource.NotFoundError{}
}

// MainRouteTableByVpcID returns the main route table for the specified VPC.
// Returns NotFoundError if no route table is found.
func MainRouteTableByVpcID(conn *ec2.EC2, vpcID string) (*ec2.RouteTable, error) {
input := &ec2.DescribeRouteTablesInput{
Filters: tfec2.BuildAttributeFilterList(map[string]string{
"association.main": "true",
"vpc-id": vpcID,
}),
}

return RouteTable(conn, input)
}

// RouteTableByID returns the route table corresponding to the specified identifier.
// Returns NotFoundError if no route table is found.
func RouteTableByID(conn *ec2.EC2, routeTableID string) (*ec2.RouteTable, error) {
Expand Down Expand Up @@ -538,59 +619,96 @@ func VpcByID(conn *ec2.EC2, id string) (*ec2.Vpc, error) {
return nil, nil
}

// VpcEndpointByID looks up a VpcEndpoint by ID. When not found, returns nil and potentially an API error.
func VpcEndpointByID(conn *ec2.EC2, id string) (*ec2.VpcEndpoint, error) {
// VpcEndpointByID returns the VPC endpoint corresponding to the specified identifier.
// Returns NotFoundError if no VPC endpoint is found.
func VpcEndpointByID(conn *ec2.EC2, vpcEndpointID string) (*ec2.VpcEndpoint, error) {
input := &ec2.DescribeVpcEndpointsInput{
VpcEndpointIds: aws.StringSlice([]string{id}),
VpcEndpointIds: aws.StringSlice([]string{vpcEndpointID}),
}

output, err := conn.DescribeVpcEndpoints(input)
vpcEndpoint, err := VpcEndpoint(conn, input)

if err != nil {
return nil, err
}

if output == nil {
return nil, nil
}

for _, vpcEndpoint := range output.VpcEndpoints {
if vpcEndpoint == nil {
continue
if state := aws.StringValue(vpcEndpoint.State); state == tfec2.VpcEndpointStateDeleted {
return nil, &resource.NotFoundError{
Message: state,
LastRequest: input,
}
}

if aws.StringValue(vpcEndpoint.VpcEndpointId) != id {
continue
// Eventual consistency check.
if aws.StringValue(vpcEndpoint.VpcEndpointId) != vpcEndpointID {
return nil, &resource.NotFoundError{
LastRequest: input,
}

return vpcEndpoint, nil
}

return nil, nil
return vpcEndpoint, nil
}

// VpcEndpointRouteTableAssociation returns the associated Route Table ID if found
func VpcEndpointRouteTableAssociation(conn *ec2.EC2, vpcEndpointID string, routeTableID string) (*string, error) {
var result *string
func VpcEndpoint(conn *ec2.EC2, input *ec2.DescribeVpcEndpointsInput) (*ec2.VpcEndpoint, error) {
output, err := conn.DescribeVpcEndpoints(input)

vpcEndpoint, err := VpcEndpointByID(conn, vpcEndpointID)
if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidVpcEndpointIdNotFound) {
return nil, &resource.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return nil, err
}

if vpcEndpoint == nil {
return nil, nil
if output == nil || len(output.VpcEndpoints) == 0 || output.VpcEndpoints[0] == nil {
return nil, &resource.NotFoundError{
Message: "Empty result",
LastRequest: input,
}
}

return output.VpcEndpoints[0], nil
}

// VpcEndpointRouteTableAssociationExists returns NotFoundError if no association for the specified VPC endpoint and route table IDs is found.
func VpcEndpointRouteTableAssociationExists(conn *ec2.EC2, vpcEndpointID string, routeTableID string) error {
vpcEndpoint, err := VpcEndpointByID(conn, vpcEndpointID)

if err != nil {
return err
}

for _, vpcEndpointRouteTableID := range vpcEndpoint.RouteTableIds {
if aws.StringValue(vpcEndpointRouteTableID) == routeTableID {
result = vpcEndpointRouteTableID
break
return nil
}
}

return result, err
return &resource.NotFoundError{
LastError: fmt.Errorf("VPC Endpoint Route Table Association (%s/%s) not found", vpcEndpointID, routeTableID),
}
}

// VpcEndpointSubnetAssociationExists returns NotFoundError if no association for the specified VPC endpoint and subnet IDs is found.
func VpcEndpointSubnetAssociationExists(conn *ec2.EC2, vpcEndpointID string, subnetID string) error {
vpcEndpoint, err := VpcEndpointByID(conn, vpcEndpointID)

if err != nil {
return err
}

for _, vpcEndpointSubnetID := range vpcEndpoint.SubnetIds {
if aws.StringValue(vpcEndpointSubnetID) == subnetID {
return nil
}
}

return &resource.NotFoundError{
LastError: fmt.Errorf("VPC Endpoint Subnet Association (%s/%s) not found", vpcEndpointID, subnetID),
}
}

// VpcPeeringConnectionByID returns the VPC peering connection corresponding to the specified identifier.
Expand Down
8 changes: 8 additions & 0 deletions aws/internal/service/ec2/id.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ func TransitGatewayPrefixListReferenceParseID(id string) (string, string, error)
return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected transit-gateway-route-table-id%[2]sprefix-list-id", id, transitGatewayPrefixListReferenceSeparator)
}

func VpcEndpointRouteTableAssociationCreateID(vpcEndpointID, routeTableID string) string {
return fmt.Sprintf("a-%s%d", vpcEndpointID, hashcode.String(routeTableID))
}

func VpcEndpointSubnetAssociationCreateID(vpcEndpointID, subnetID string) string {
return fmt.Sprintf("a-%s%d", vpcEndpointID, hashcode.String(subnetID))
}

func VpnGatewayVpcAttachmentCreateID(vpnGatewayID, vpcID string) string {
return fmt.Sprintf("vpn-attachment-%x", hashcode.String(fmt.Sprintf("%s-%s", vpcID, vpnGatewayID)))
}
Loading

0 comments on commit 3e31593

Please sign in to comment.