Skip to content

Commit 97c1106

Browse files
authored
Merge pull request #24804 from hashicorp/b-servicecatalog-provisioned-product-record-wait-for-status
r/servicecatalog_provisioned_product: add `TAINTED` to target states in waiters
2 parents 987f548 + 26509ed commit 97c1106

File tree

4 files changed

+31
-64
lines changed

4 files changed

+31
-64
lines changed

.changelog/24804.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
resource/aws_servicecatalog_provisioned_product: Add possible `TAINTED` target state for resource update and remove one of the internal waiters during read
3+
```

internal/service/servicecatalog/provisioned_product.go

+25-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/aws/aws-sdk-go/aws"
99
"github.com/aws/aws-sdk-go/service/servicecatalog"
1010
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
11+
"github.com/hashicorp/go-multierror"
1112
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1213
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1314
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
@@ -415,9 +416,16 @@ func resourceProvisionedProductRead(d *schema.ResourceData, meta interface{}) er
415416
d.Set("status_message", detail.StatusMessage)
416417
d.Set("type", detail.Type)
417418

418-
// tags are only available from the record tied to the provisioned product
419+
// Previously, we waited for the record to only return a target state of 'SUCCEEDED' or 'AVAILABLE'
420+
// but this can interfere complete reads of this resource when an error occurs after initial creation
421+
// or after an invalid update that returns a 'FAILED' record state. Thus, waiters are now present in the CREATE and UPDATE methods of this resource instead.
422+
// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/24574#issuecomment-1126339193
423+
recordInput := &servicecatalog.DescribeRecordInput{
424+
Id: detail.LastProvisioningRecordId,
425+
AcceptLanguage: aws.String(acceptLanguage),
426+
}
419427

420-
recordOutput, err := WaitRecordReady(conn, acceptLanguage, aws.StringValue(detail.LastProvisioningRecordId), RecordReadyTimeout)
428+
recordOutput, err := conn.DescribeRecord(recordInput)
421429

422430
if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) {
423431
log.Printf("[WARN] Service Catalog Provisioned Product (%s) Record (%s) not found, unable to set tags", d.Id(), aws.StringValue(detail.LastProvisioningRecordId))
@@ -432,12 +440,27 @@ func resourceProvisionedProductRead(d *schema.ResourceData, meta interface{}) er
432440
return fmt.Errorf("error getting Service Catalog Provisioned Product (%s) Record (%s): empty response", d.Id(), aws.StringValue(detail.LastProvisioningRecordId))
433441
}
434442

443+
// To enable debugging of potential errors, log as a warning
444+
// instead of exiting prematurely with an error, e.g.
445+
// errors can be present after update to a new version failed and the stack
446+
// rolled back to the current version.
447+
if errors := recordOutput.RecordDetail.RecordErrors; len(errors) > 0 {
448+
var errs *multierror.Error
449+
450+
for _, err := range errors {
451+
errs = multierror.Append(errs, fmt.Errorf("%s: %s", aws.StringValue(err.Code), aws.StringValue(err.Description)))
452+
}
453+
454+
log.Printf("[WARN] Errors found when describing Service Catalog Provisioned Product (%s) Record (%s): %s", d.Id(), aws.StringValue(detail.LastProvisioningRecordId), errs.ErrorOrNil())
455+
}
456+
435457
if err := d.Set("outputs", flattenRecordOutputs(recordOutput.RecordOutputs)); err != nil {
436458
return fmt.Errorf("error setting outputs: %w", err)
437459
}
438460

439461
d.Set("path_id", recordOutput.RecordDetail.PathId)
440462

463+
// tags are only available from the record tied to the provisioned product
441464
tags := recordKeyValueTags(recordOutput.RecordDetail.RecordTags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig)
442465

443466
//lintignore:AWSR002

internal/service/servicecatalog/status.go

-28
Original file line numberDiff line numberDiff line change
@@ -388,34 +388,6 @@ func StatusProvisionedProduct(conn *servicecatalog.ServiceCatalog, acceptLanguag
388388
}
389389
}
390390

391-
func StatusRecord(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string) resource.StateRefreshFunc {
392-
return func() (interface{}, string, error) {
393-
input := &servicecatalog.DescribeRecordInput{
394-
Id: aws.String(id),
395-
}
396-
397-
if acceptLanguage != "" {
398-
input.AcceptLanguage = aws.String(acceptLanguage)
399-
}
400-
401-
output, err := conn.DescribeRecord(input)
402-
403-
if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) {
404-
return nil, StatusNotFound, err
405-
}
406-
407-
if err != nil {
408-
return nil, servicecatalog.StatusFailed, err
409-
}
410-
411-
if output == nil || output.RecordDetail == nil {
412-
return nil, StatusNotFound, err
413-
}
414-
415-
return output, aws.StringValue(output.RecordDetail.Status), err
416-
}
417-
}
418-
419391
func StatusPortfolioConstraints(conn *servicecatalog.ServiceCatalog, acceptLanguage, portfolioID, productID string) resource.StateRefreshFunc {
420392
return func() (interface{}, string, error) {
421393
input := &servicecatalog.ListConstraintsForPortfolioInput{

internal/service/servicecatalog/wait.go

+3-34
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ package servicecatalog
22

33
import (
44
"errors"
5-
"fmt"
65
"time"
76

87
"github.com/aws/aws-sdk-go/aws"
98
"github.com/aws/aws-sdk-go/service/servicecatalog"
109
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
11-
"github.com/hashicorp/go-multierror"
1210
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1311
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
1412
)
@@ -50,7 +48,6 @@ const (
5048
ProvisioningArtifactReadTimeout = 10 * time.Minute
5149
ProvisioningArtifactReadyTimeout = 3 * time.Minute
5250
ProvisioningArtifactUpdateTimeout = 3 * time.Minute
53-
RecordReadyTimeout = 30 * time.Minute
5451
ServiceActionDeleteTimeout = 3 * time.Minute
5552
ServiceActionReadTimeout = 10 * time.Minute
5653
ServiceActionReadyTimeout = 3 * time.Minute
@@ -505,8 +502,10 @@ func WaitLaunchPathsReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, p
505502

506503
func WaitProvisionedProductReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, id, name string, timeout time.Duration) (*servicecatalog.DescribeProvisionedProductOutput, error) {
507504
stateConf := &resource.StateChangeConf{
505+
// "TAINTED" is a valid target state as its described as a stable state per API docs, though can result from a failed update
506+
// such that the stack rolls back to a previous version.
508507
Pending: []string{StatusNotFound, StatusUnavailable, servicecatalog.ProvisionedProductStatusUnderChange, servicecatalog.ProvisionedProductStatusPlanInProgress},
509-
Target: []string{servicecatalog.StatusAvailable},
508+
Target: []string{servicecatalog.StatusAvailable, servicecatalog.ProvisionedProductStatusTainted},
510509
Refresh: StatusProvisionedProduct(conn, acceptLanguage, id, name),
511510
Timeout: timeout,
512511
ContinuousTargetOccurence: ContinuousTargetOccurrence,
@@ -537,36 +536,6 @@ func WaitProvisionedProductTerminated(conn *servicecatalog.ServiceCatalog, accep
537536
return err
538537
}
539538

540-
func WaitRecordReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string, timeout time.Duration) (*servicecatalog.DescribeRecordOutput, error) {
541-
stateConf := &resource.StateChangeConf{
542-
Pending: []string{StatusNotFound, StatusUnavailable, servicecatalog.ProvisionedProductStatusUnderChange, servicecatalog.ProvisionedProductStatusPlanInProgress},
543-
Target: []string{servicecatalog.RecordStatusSucceeded, servicecatalog.StatusAvailable},
544-
Refresh: StatusRecord(conn, acceptLanguage, id),
545-
Timeout: timeout,
546-
ContinuousTargetOccurence: ContinuousTargetOccurrence,
547-
NotFoundChecks: NotFoundChecks,
548-
MinTimeout: MinTimeout,
549-
}
550-
551-
outputRaw, err := stateConf.WaitForState()
552-
553-
if output, ok := outputRaw.(*servicecatalog.DescribeRecordOutput); ok {
554-
if errors := output.RecordDetail.RecordErrors; len(errors) > 0 {
555-
var errs *multierror.Error
556-
557-
for _, err := range output.RecordDetail.RecordErrors {
558-
errs = multierror.Append(errs, fmt.Errorf("%s: %s", aws.StringValue(err.Code), aws.StringValue(err.Description)))
559-
}
560-
561-
tfresource.SetLastError(err, errs.ErrorOrNil())
562-
}
563-
564-
return output, err
565-
}
566-
567-
return nil, err
568-
}
569-
570539
func WaitPortfolioConstraintsReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, portfolioID, productID string, timeout time.Duration) ([]*servicecatalog.ConstraintDetail, error) {
571540
stateConf := &resource.StateChangeConf{
572541
Pending: []string{StatusNotFound},

0 commit comments

Comments
 (0)