Skip to content

Commit

Permalink
resource/aws_spot_instance_request: Handle read-after-create eventual…
Browse files Browse the repository at this point in the history
… consistency

Reference: #12679
Reference: #16796

Output from acceptance testing in AWS Commercial:

```
--- PASS: TestAccAWSSpotInstanceRequest_basic (65.29s)
--- PASS: TestAccAWSSpotInstanceRequest_vpc (75.75s)
--- PASS: TestAccAWSSpotInstanceRequest_InterruptStop (96.45s)
--- PASS: TestAccAWSSpotInstanceRequest_withBlockDuration (97.68s)
--- PASS: TestAccAWSSpotInstanceRequest_withLaunchGroup (108.30s)
--- PASS: TestAccAWSSpotInstanceRequest_SubnetAndSGAndPublicIpAddress (119.75s)
--- PASS: TestAccAWSSpotInstanceRequest_InterruptHibernate (125.90s)
--- PASS: TestAccAWSSpotInstanceRequest_NetworkInterfaceAttributes (140.51s)
--- PASS: TestAccAWSSpotInstanceRequest_tags (165.36s)
--- PASS: TestAccAWSSpotInstanceRequest_getPasswordData (187.44s)
--- PASS: TestAccAWSSpotInstanceRequest_disappears (320.26s)
--- PASS: TestAccAWSSpotInstanceRequest_withoutSpotPrice (328.46s)
--- PASS: TestAccAWSSpotInstanceRequest_validUntil (338.82s)
```

Output from acceptance testing in AWS GovCloud (US):

```
--- PASS: TestAccAWSSpotInstanceRequest_withoutSpotPrice (66.13s)
--- PASS: TestAccAWSSpotInstanceRequest_basic (66.24s)
--- PASS: TestAccAWSSpotInstanceRequest_withBlockDuration (66.32s)
--- PASS: TestAccAWSSpotInstanceRequest_InterruptHibernate (74.19s)
--- PASS: TestAccAWSSpotInstanceRequest_vpc (95.40s)
--- PASS: TestAccAWSSpotInstanceRequest_withLaunchGroup (107.94s)
--- PASS: TestAccAWSSpotInstanceRequest_validUntil (107.98s)
--- PASS: TestAccAWSSpotInstanceRequest_InterruptStop (127.16s)
--- PASS: TestAccAWSSpotInstanceRequest_tags (155.38s)
--- PASS: TestAccAWSSpotInstanceRequest_SubnetAndSGAndPublicIpAddress (160.34s)
--- PASS: TestAccAWSSpotInstanceRequest_NetworkInterfaceAttributes (161.00s)
--- PASS: TestAccAWSSpotInstanceRequest_getPasswordData (209.51s)
--- PASS: TestAccAWSSpotInstanceRequest_disappears (332.58s)
```
  • Loading branch information
bflad committed Mar 29, 2021
1 parent 2bf0416 commit 0370b33
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 18 deletions.
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_spot_instance_request: Handle read-after-create eventual consistency
```
4 changes: 4 additions & 0 deletions aws/internal/service/ec2/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ const (
InvalidGroupNotFound = "InvalidGroup.NotFound"
)

const (
ErrCodeInvalidSpotInstanceRequestIDNotFound = "InvalidSpotInstanceRequestID.NotFound"
)

const (
ErrCodeInvalidSubnetIDNotFound = "InvalidSubnetID.NotFound"
)
Expand Down
31 changes: 31 additions & 0 deletions aws/internal/service/ec2/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,37 @@ func SecurityGroupByID(conn *ec2.EC2, id string) (*ec2.SecurityGroup, error) {
return result.SecurityGroups[0], nil
}

// SpotInstanceRequestByID looks up a SpotInstanceRequest by ID. When not found, returns nil and potentially an API error.
func SpotInstanceRequestByID(conn *ec2.EC2, id string) (*ec2.SpotInstanceRequest, error) {
input := &ec2.DescribeSpotInstanceRequestsInput{
SpotInstanceRequestIds: aws.StringSlice([]string{id}),
}

output, err := conn.DescribeSpotInstanceRequests(input)

if err != nil {
return nil, err
}

if output == nil {
return nil, nil
}

for _, spotInstanceRequest := range output.SpotInstanceRequests {
if spotInstanceRequest == nil {
continue
}

if aws.StringValue(spotInstanceRequest.SpotInstanceRequestId) != id {
continue
}

return spotInstanceRequest, nil
}

return nil, nil
}

// SubnetByID looks up a Subnet by ID. When not found, returns nil and potentially an API error.
func SubnetByID(conn *ec2.EC2, id string) (*ec2.Subnet, error) {
input := &ec2.DescribeSubnetsInput{
Expand Down
66 changes: 48 additions & 18 deletions aws/resource_aws_spot_instance_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ 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"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
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"
iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsSpotInstanceRequest() *schema.Resource {
Expand Down Expand Up @@ -250,35 +255,60 @@ func resourceAwsSpotInstanceRequestRead(d *schema.ResourceData, meta interface{}
conn := meta.(*AWSClient).ec2conn
ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig

req := &ec2.DescribeSpotInstanceRequestsInput{
SpotInstanceRequestIds: []*string{aws.String(d.Id())},
}
resp, err := conn.DescribeSpotInstanceRequests(req)
var request *ec2.SpotInstanceRequest

if err != nil {
// If the spot request was not found, return nil so that we can show
// that it is gone.
if isAWSErr(err, "InvalidSpotInstanceRequestID.NotFound", "") {
log.Printf("[WARN] EC2 Spot Instance Request (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError {
var err error

request, err = finder.SpotInstanceRequestByID(conn, d.Id())

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

// Some other error, report it
return err
if err != nil {
return resource.NonRetryableError(err)
}

if d.IsNewResource() && request == nil {
return resource.RetryableError(&resource.NotFoundError{
LastError: fmt.Errorf("EC2 Spot Instance Request (%s) not found", d.Id()),
})
}

return nil
})

if tfresource.TimedOut(err) {
request, err = finder.SpotInstanceRequestByID(conn, d.Id())
}

// If nothing was found, then return no state
if len(resp.SpotInstanceRequests) == 0 {
if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidSpotInstanceRequestIDNotFound) {
log.Printf("[WARN] EC2 Spot Instance Request (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

if err != nil {
return fmt.Errorf("error reading EC2 Spot Instance Request (%s): %w", d.Id(), err)
}

if request == nil {
if d.IsNewResource() {
return fmt.Errorf("error reading EC2 Spot Instance Request (%s): not found after creation", d.Id())
}

log.Printf("[WARN] EC2 Spot Instance Request (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

request := resp.SpotInstanceRequests[0]
if aws.StringValue(request.State) == ec2.SpotInstanceStateCancelled || aws.StringValue(request.State) == ec2.SpotInstanceStateClosed {
if d.IsNewResource() {
return fmt.Errorf("error reading EC2 Spot Instance Request (%s): %s after creation", d.Id(), aws.StringValue(request.State))
}

// if the request is cancelled or closed, then it is gone
if *request.State == ec2.SpotInstanceStateCancelled || *request.State == ec2.SpotInstanceStateClosed {
log.Printf("[WARN] EC2 Spot Instance Request (%s) %s, removing from state", d.Id(), aws.StringValue(request.State))
d.SetId("")
return nil
}
Expand Down

0 comments on commit 0370b33

Please sign in to comment.