Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resource/aws_vpc_endpoint_route_table_association: Handle read-after-create eventual consistency #18465

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/pending.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_vpc_endpoint_route_table_association: Handle read-after-create eventual consistency
```
55 changes: 55 additions & 0 deletions aws/internal/service/ec2/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,61 @@ 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) {
input := &ec2.DescribeVpcEndpointsInput{
VpcEndpointIds: aws.StringSlice([]string{id}),
}

output, err := conn.DescribeVpcEndpoints(input)

if err != nil {
return nil, err
}

if output == nil {
return nil, nil
}

for _, vpcEndpoint := range output.VpcEndpoints {
if vpcEndpoint == nil {
continue
}

if aws.StringValue(vpcEndpoint.VpcEndpointId) != id {
continue
}

return vpcEndpoint, nil
}

return nil, nil
}

// VpcEndpointRouteTableAssociation returns the associated Route Table ID if found
func VpcEndpointRouteTableAssociation(conn *ec2.EC2, vpcEndpointID string, routeTableID string) (*string, error) {
var result *string

vpcEndpoint, err := VpcEndpointByID(conn, vpcEndpointID)

if err != nil {
return nil, err
}

if vpcEndpoint == nil {
return nil, nil
}

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

return result, err
}

// VpcPeeringConnectionByID returns the VPC peering connection corresponding to the specified identifier.
// Returns nil and potentially an error if no VPC peering connection is found.
func VpcPeeringConnectionByID(conn *ec2.EC2, id string) (*ec2.VpcPeeringConnection, error) {
Expand Down
62 changes: 47 additions & 15 deletions aws/resource_aws_vpc_endpoint_route_table_association.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"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/terraform-providers/terraform-provider-aws/aws/internal/hashcode"
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/service/ec2/waiter"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsVpcEndpointRouteTableAssociation() *schema.Resource {
Expand Down Expand Up @@ -65,27 +71,53 @@ func resourceAwsVpcEndpointRouteTableAssociationRead(d *schema.ResourceData, met

endpointId := d.Get("vpc_endpoint_id").(string)
rtId := d.Get("route_table_id").(string)
// Human friendly ID for error messages since d.Id() is non-descriptive
id := fmt.Sprintf("%s/%s", endpointId, rtId)

vpce, err := findResourceVpcEndpoint(conn, endpointId)
if err != nil {
if isAWSErr(err, "InvalidVpcEndpointId.NotFound", "") {
log.Printf("[WARN] VPC Endpoint (%s) not found, removing VPC Endpoint/Route Table association (%s) from state", endpointId, d.Id())
d.SetId("")
return nil
var routeTableID *string

err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError {
var err error

routeTableID, err = finder.VpcEndpointRouteTableAssociation(conn, endpointId, rtId)

if d.IsNewResource() && tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidVpcEndpointIdNotFound) {
return resource.RetryableError(err)
}

return err
}
if err != nil {
return resource.NonRetryableError(err)
}

found := false
for _, id := range vpce.RouteTableIds {
if aws.StringValue(id) == rtId {
found = true
break
if d.IsNewResource() && routeTableID == nil {
return resource.RetryableError(&resource.NotFoundError{
LastError: fmt.Errorf("VPC Endpoint Route Table Association (%s) not found", id),
})
}

return nil
})

if tfresource.TimedOut(err) {
routeTableID, err = finder.VpcEndpointRouteTableAssociation(conn, endpointId, rtId)
}
if !found {
log.Printf("[WARN] VPC Endpoint/Route Table association (%s) not found, removing from state", d.Id())

if d.IsNewResource() && tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidVpcEndpointIdNotFound) {
log.Printf("[WARN] VPC Endpoint Route Table Association (%s) not found, removing from state", id)
d.SetId("")
return nil
}

if err != nil {
return fmt.Errorf("error reading VPC Endpoint Route Table Association (%s): %w", id, err)
}

if routeTableID == nil {
if d.IsNewResource() {
return fmt.Errorf("error reading VPC Endpoint Route Table Association (%s): not found after creation", id)
}

log.Printf("[WARN] VPC Endpoint Route Table Association (%s) not found, removing from state", id)
d.SetId("")
return nil
}
Expand Down