diff --git a/.changelog/pending.txt b/.changelog/pending.txt new file mode 100644 index 000000000000..4c6f0c175de0 --- /dev/null +++ b/.changelog/pending.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_vpc_endpoint_route_table_association: Handle read-after-create eventual consistency +``` diff --git a/aws/internal/service/ec2/finder/finder.go b/aws/internal/service/ec2/finder/finder.go index cbc272faeb14..ac7522d11af9 100644 --- a/aws/internal/service/ec2/finder/finder.go +++ b/aws/internal/service/ec2/finder/finder.go @@ -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) { diff --git a/aws/resource_aws_vpc_endpoint_route_table_association.go b/aws/resource_aws_vpc_endpoint_route_table_association.go index e9ef933fe60d..5c88d02be7b8 100644 --- a/aws/resource_aws_vpc_endpoint_route_table_association.go +++ b/aws/resource_aws_vpc_endpoint_route_table_association.go @@ -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 { @@ -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 }