Skip to content

Commit 0e390ab

Browse files
authored
Merge pull request #24758 from hashicorp/b-servicecatalog-provisioned-product-wait-for-status
r/servicecatalog_provisioned_product: remove waiter from read CRUD op to prevent irreversible state
2 parents 58aad4a + 3d48a64 commit 0e390ab

File tree

4 files changed

+108
-6
lines changed

4 files changed

+108
-6
lines changed

.changelog/24758.txt

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
```release-note:bug
2+
resource/aws_servicecatalog_provisioned_product: Prevent error when retrieving a provisioned product in a non-available state
3+
```
4+
5+
```release-note:enhancement
6+
resource/aws_servicecatalog_provisioned_product: Wait for provisioning to finish
7+
```
8+
9+
```release-note:enhancement
10+
resource/aws_servicecatalog_provisioned_product: Wait for update to finish

internal/service/servicecatalog/provisioned_product.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,10 @@ func resourceProvisionedProductCreate(d *schema.ResourceData, meta interface{})
347347

348348
d.SetId(aws.StringValue(output.RecordDetail.ProvisionedProductId))
349349

350+
if _, err := WaitProvisionedProductReady(conn, d.Get("accept_language").(string), d.Id(), "", d.Timeout(schema.TimeoutCreate)); err != nil {
351+
return fmt.Errorf("error waiting for Service Catalog Provisioned Product (%s) create: %w", d.Id(), err)
352+
}
353+
350354
return resourceProvisionedProductRead(d, meta)
351355
}
352356

@@ -368,7 +372,12 @@ func resourceProvisionedProductRead(d *schema.ResourceData, meta interface{}) er
368372
acceptLanguage = v.(string)
369373
}
370374

371-
output, err := WaitProvisionedProductReady(conn, acceptLanguage, d.Id(), "", d.Timeout(schema.TimeoutRead))
375+
input := &servicecatalog.DescribeProvisionedProductInput{
376+
Id: aws.String(d.Id()),
377+
AcceptLanguage: aws.String(acceptLanguage),
378+
}
379+
380+
output, err := conn.DescribeProvisionedProduct(input)
372381

373382
if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) {
374383
log.Printf("[WARN] Service Catalog Provisioned Product (%s) not found, removing from state", d.Id())
@@ -514,6 +523,10 @@ func resourceProvisionedProductUpdate(d *schema.ResourceData, meta interface{})
514523
return fmt.Errorf("error updating Service Catalog Provisioned Product (%s): %w", d.Id(), err)
515524
}
516525

526+
if _, err := WaitProvisionedProductReady(conn, d.Get("accept_language").(string), d.Id(), "", d.Timeout(schema.TimeoutUpdate)); err != nil {
527+
return fmt.Errorf("error waiting for Service Catalog Provisioned Product (%s) update: %w", d.Id(), err)
528+
}
529+
517530
return resourceProvisionedProductRead(d, meta)
518531
}
519532

internal/service/servicecatalog/provisioned_product_test.go

+70-5
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,72 @@ func TestAccServiceCatalogProvisionedProduct_basic(t *testing.T) {
3030
CheckDestroy: testAccCheckProvisionedProductDestroy,
3131
Steps: []resource.TestStep{
3232
{
33-
Config: testAccProvisionedProductConfig_basic(rName, domain, acctest.DefaultEmailAddress),
33+
Config: testAccProvisionedProductConfig_basic(rName, domain, acctest.DefaultEmailAddress, "10.1.0.0/16"),
34+
Check: resource.ComposeTestCheckFunc(
35+
testAccCheckProvisionedProductExists(resourceName),
36+
resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish),
37+
acctest.MatchResourceAttrRegionalARN(resourceName, "arn", servicecatalog.ServiceName, regexp.MustCompile(fmt.Sprintf(`stack/%s/pp-.*`, rName))),
38+
acctest.CheckResourceAttrRFC3339(resourceName, "created_time"),
39+
resource.TestCheckResourceAttrSet(resourceName, "last_provisioning_record_id"),
40+
resource.TestCheckResourceAttrSet(resourceName, "last_record_id"),
41+
resource.TestCheckResourceAttrSet(resourceName, "last_successful_provisioning_record_id"),
42+
resource.TestCheckResourceAttr(resourceName, "name", rName),
43+
// One output will default to the launched CloudFormation Stack (provisioned outside terraform).
44+
// While another output will describe the output parameter configured in the S3 object resource,
45+
// which we can check as follows.
46+
resource.TestCheckResourceAttr(resourceName, "outputs.#", "2"),
47+
resource.TestCheckTypeSetElemNestedAttrs(resourceName, "outputs.*", map[string]string{
48+
"description": "VPC ID",
49+
"key": "VpcID",
50+
}),
51+
resource.TestMatchTypeSetElemNestedAttrs(resourceName, "outputs.*", map[string]*regexp.Regexp{
52+
"value": regexp.MustCompile(`vpc-.+`),
53+
}),
54+
resource.TestCheckResourceAttrPair(resourceName, "path_id", "data.aws_servicecatalog_launch_paths.test", "summaries.0.path_id"),
55+
resource.TestCheckResourceAttrPair(resourceName, "product_id", "aws_servicecatalog_product.test", "id"),
56+
resource.TestCheckResourceAttrPair(resourceName, "provisioning_artifact_name", "aws_servicecatalog_product.test", "provisioning_artifact_parameters.0.name"),
57+
resource.TestCheckResourceAttr(resourceName, "status", servicecatalog.StatusAvailable),
58+
resource.TestCheckResourceAttr(resourceName, "type", "CFN_STACK"),
59+
),
60+
},
61+
{
62+
ResourceName: resourceName,
63+
ImportState: true,
64+
ImportStateVerify: true,
65+
ImportStateVerifyIgnore: []string{
66+
"accept_language",
67+
"ignore_errors",
68+
"provisioning_artifact_name",
69+
"provisioning_parameters",
70+
"retain_physical_resources",
71+
},
72+
},
73+
},
74+
})
75+
}
76+
77+
// TestAccServiceCatalogProvisionedProduct_update verifies the resource update
78+
// of only a change in provisioning_parameters
79+
func TestAccServiceCatalogProvisionedProduct_update(t *testing.T) {
80+
resourceName := "aws_servicecatalog_provisioned_product.test"
81+
82+
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
83+
domain := fmt.Sprintf("http://%s", acctest.RandomDomainName())
84+
85+
resource.ParallelTest(t, resource.TestCase{
86+
PreCheck: func() { acctest.PreCheck(t) },
87+
ErrorCheck: acctest.ErrorCheck(t, servicecatalog.EndpointsID),
88+
ProviderFactories: acctest.ProviderFactories,
89+
CheckDestroy: testAccCheckProvisionedProductDestroy,
90+
Steps: []resource.TestStep{
91+
{
92+
Config: testAccProvisionedProductConfig_basic(rName, domain, acctest.DefaultEmailAddress, "10.1.0.0/16"),
93+
Check: resource.ComposeTestCheckFunc(
94+
testAccCheckProvisionedProductExists(resourceName),
95+
),
96+
},
97+
{
98+
Config: testAccProvisionedProductConfig_basic(rName, domain, acctest.DefaultEmailAddress, "10.10.0.0/16"),
3499
Check: resource.ComposeTestCheckFunc(
35100
testAccCheckProvisionedProductExists(resourceName),
36101
resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish),
@@ -87,7 +152,7 @@ func TestAccServiceCatalogProvisionedProduct_disappears(t *testing.T) {
87152
CheckDestroy: testAccCheckProvisionedProductDestroy,
88153
Steps: []resource.TestStep{
89154
{
90-
Config: testAccProvisionedProductConfig_basic(rName, domain, acctest.DefaultEmailAddress),
155+
Config: testAccProvisionedProductConfig_basic(rName, domain, acctest.DefaultEmailAddress, "10.1.0.0/16"),
91156
Check: resource.ComposeTestCheckFunc(
92157
testAccCheckProvisionedProductExists(resourceName),
93158
acctest.CheckResourceDisappears(acctest.Provider, tfservicecatalog.ResourceProvisionedProduct(), resourceName),
@@ -298,7 +363,7 @@ data "aws_servicecatalog_launch_paths" "test" {
298363
`, rName, domain, email)
299364
}
300365

301-
func testAccProvisionedProductConfig_basic(rName, domain, email string) string {
366+
func testAccProvisionedProductConfig_basic(rName, domain, email, vpcCidr string) string {
302367
return acctest.ConfigCompose(testAccProvisionedProductTemplateURLBaseConfig(rName, domain, email),
303368
fmt.Sprintf(`
304369
resource "aws_servicecatalog_provisioned_product" "test" {
@@ -309,15 +374,15 @@ resource "aws_servicecatalog_provisioned_product" "test" {
309374
310375
provisioning_parameters {
311376
key = "VPCPrimaryCIDR"
312-
value = "10.1.0.0/16"
377+
value = %[2]q
313378
}
314379
315380
provisioning_parameters {
316381
key = "LeaveMeEmpty"
317382
value = ""
318383
}
319384
}
320-
`, rName))
385+
`, rName, vpcCidr))
321386
}
322387

323388
func testAccProvisionedProductConfig_tags(rName, tagKey, tagValue, domain, email string) string {

internal/service/servicecatalog/wait.go

+14
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package servicecatalog
22

33
import (
4+
"errors"
5+
"fmt"
46
"time"
57

68
"github.com/aws/aws-sdk-go/aws"
79
"github.com/aws/aws-sdk-go/service/servicecatalog"
810
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
11+
"github.com/hashicorp/go-multierror"
912
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1013
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
1114
)
@@ -514,6 +517,7 @@ func WaitProvisionedProductReady(conn *servicecatalog.ServiceCatalog, acceptLang
514517
outputRaw, err := stateConf.WaitForState()
515518

516519
if output, ok := outputRaw.(*servicecatalog.DescribeProvisionedProductOutput); ok {
520+
tfresource.SetLastError(err, errors.New(aws.StringValue(output.ProvisionedProductDetail.StatusMessage)))
517521
return output, err
518522
}
519523

@@ -547,6 +551,16 @@ func WaitRecordReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, id str
547551
outputRaw, err := stateConf.WaitForState()
548552

549553
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+
550564
return output, err
551565
}
552566

0 commit comments

Comments
 (0)