Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fb2072b
service cat debuggery
YakDriver Aug 19, 2025
9dfca50
Allows deletion when in `TAINTED` status
gdavison Aug 21, 2025
75ba850
Retries creating `aws_servicecatalog_constraint` while waiting for IA…
gdavison Aug 21, 2025
da402c4
Handles deletion if `TAINTED` or becomes `TAINTED` during deletion
gdavison Aug 21, 2025
fb7fe02
Updates test for current behaviour
gdavison Aug 21, 2025
0c92f86
Reverts resource changes from "service cat debuggery"
gdavison Aug 21, 2025
c8d9f45
Updates tests
gdavison Aug 21, 2025
7b103cb
Merge branch 'main' into b-servicecatalog-pp-status
ewbankkit Aug 27, 2025
1039db4
Add copyright header.
ewbankkit Aug 27, 2025
d7d8d27
Run 'make fix-constants PKG=servicecatalog'.
ewbankkit Aug 27, 2025
848d84c
Fix providerlint 'AWSAT005: avoid hardcoded ARN AWS partitions, use a…
ewbankkit Aug 27, 2025
c2be5c3
Fix golangci-lint 'whitespace'.
ewbankkit Aug 27, 2025
59e2323
Use 'testAccCheckProvisionedProductStatus'.
ewbankkit Aug 27, 2025
8c23299
Fix terrafmt errors.
ewbankkit Aug 27, 2025
c21a1b0
Add 'findProvisionedProduct' and 'findRecord'.
ewbankkit Aug 28, 2025
69fd528
statusProvisionedProduct: Use 'findProvisionedProductByTwoPartKey'.
ewbankkit Aug 28, 2025
eee6cb8
Acceptance test output:
ewbankkit Aug 28, 2025
a999fe9
Tidy up 'provisionedProductFailureError.Status'.
ewbankkit Aug 28, 2025
34342d1
Fix golangci-lint 'unparam'.
ewbankkit Aug 28, 2025
ef2c3ac
ProvisionedProduct: Force state refresh if Update results in TAINTED …
ewbankkit Aug 28, 2025
ec8b264
Merge branch 'main' into b-servicecatalog-pp-status
jar-b Sep 2, 2025
6d218dc
r/aws_servicecatalog_provisioned_product(test): add expected error co…
jar-b Sep 2, 2025
4140e7f
r/aws_servicecatalog_provisioned_product: rollback params, artifact I…
jar-b Sep 2, 2025
d861fc1
r/aws_servicecatalog_provisioned_product(test): refine plan, state ch…
jar-b Sep 2, 2025
5b1f474
r/aws_servicecatalog_provisioned_product(test): simplify test config
jar-b Sep 2, 2025
bfde42d
r/aws_servicecatalog_provisioned_product(test): tidy test template
jar-b Sep 2, 2025
52778c5
chore: changelog
jar-b Sep 2, 2025
ed015b6
r/aws_servicecatalog_provisioned_product: simplify custom error type
jar-b Sep 2, 2025
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/43956.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_servicecatalog_provisioned_product: Set `provisioning_parameters` and `provisioning_artifact_id` to the values from the last successful deployment when update fails
```
4 changes: 4 additions & 0 deletions internal/service/servicecatalog/constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ func resourceConstraintCreate(ctx context.Context, d *schema.ResourceData, meta
return retry.RetryableError(err)
}

if errs.IsAErrorMessageContains[*awstypes.InvalidParametersException](err, "Access denied while assuming the role") {
return retry.RetryableError(err)
}

if errs.IsA[*awstypes.ResourceNotFoundException](err) {
return retry.RetryableError(err)
}
Expand Down
8 changes: 4 additions & 4 deletions internal/service/servicecatalog/exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ var (
ResourceTagOption = resourceTagOption
ResourceTagOptionResourceAssociation = resourceTagOptionResourceAssociation

FindPortfolioByID = findPortfolioByID
FindPortfolioShare = findPortfolioShare
FindPrincipalPortfolioAssociation = findPrincipalPortfolioAssociation
FindPortfolioByID = findPortfolioByID
FindPortfolioShare = findPortfolioShare
FindPrincipalPortfolioAssociation = findPrincipalPortfolioAssociation
FindProvisionedProductByTwoPartKey = findProvisionedProductByTwoPartKey

BudgetResourceAssociationParseID = budgetResourceAssociationParseID
ProductPortfolioAssociationParseID = productPortfolioAssociationParseID
Expand All @@ -36,7 +37,6 @@ var (
WaitOrganizationsAccessStable = waitOrganizationsAccessStable
WaitProductPortfolioAssociationDeleted = waitProductPortfolioAssociationDeleted
WaitProductPortfolioAssociationReady = waitProductPortfolioAssociationReady
WaitProvisionedProductReady = waitProvisionedProductReady
WaitTagOptionResourceAssociationDeleted = waitTagOptionResourceAssociationDeleted
WaitTagOptionResourceAssociationReady = waitTagOptionResourceAssociationReady

Expand Down
374 changes: 274 additions & 100 deletions internal/service/servicecatalog/provisioned_product.go

Large diffs are not rendered by default.

220 changes: 204 additions & 16 deletions internal/service/servicecatalog/provisioned_product_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ import (

"github.com/YakDriver/regexache"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/servicecatalog"
awstypes "github.com/aws/aws-sdk-go-v2/service/servicecatalog/types"
sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/errs"
tfservicecatalog "github.com/hashicorp/terraform-provider-aws/internal/service/servicecatalog"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/names"
)

Expand Down Expand Up @@ -457,6 +460,127 @@ func TestAccServiceCatalogProvisionedProduct_productTagUpdateAfterError(t *testi
})
}

// Validates that a provisioned product in tainted status properly triggers an update
// on subsequent applies.
// Ref: https://github.com/hashicorp/terraform-provider-aws/issues/42585
func TestAccServiceCatalogProvisionedProduct_retryTaintedUpdate(t *testing.T) {
ctx := acctest.Context(t)
resourceName := "aws_servicecatalog_provisioned_product.test"
artifactsDataSourceName := "data.aws_servicecatalog_provisioning_artifacts.product_artifacts"
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
initialArtifactID := "provisioning_artifact_details.0.id"
newArtifactID := "provisioning_artifact_details.1.id"
var v awstypes.ProvisionedProductDetail

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ServiceCatalogServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckProvisionedProductDestroy(ctx),
Steps: []resource.TestStep{
// Step 1 - Setup
{
Config: testAccProvisionedProductConfig_retryTaintedUpdate(rName, false, false, "original"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckProvisionedProductExists(ctx, resourceName, &v),
resource.TestCheckResourceAttrPair(resourceName, "provisioning_artifact_id", artifactsDataSourceName, initialArtifactID),
),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrStatus), knownvalue.StringExact("AVAILABLE")),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("provisioning_parameters"), knownvalue.ListExact([]knownvalue.Check{
knownvalue.ObjectExact(map[string]knownvalue.Check{
names.AttrKey: knownvalue.StringExact("FailureSimulation"),
"use_previous_value": knownvalue.Bool(false),
names.AttrValue: knownvalue.StringExact(acctest.CtFalse),
}),
knownvalue.ObjectExact(map[string]knownvalue.Check{
names.AttrKey: knownvalue.StringExact("ExtraParam"),
"use_previous_value": knownvalue.Bool(false),
names.AttrValue: knownvalue.StringExact("original"),
}),
})),
},
},
// Step 2 - Trigger a failure, leaving the provisioned product tainted
{
Config: testAccProvisionedProductConfig_retryTaintedUpdate(rName, true, true, "updated"),
ExpectError: regexache.MustCompile(`The following resource\(s\) failed to update:`),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate),
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("provisioning_parameters"), knownvalue.ListExact([]knownvalue.Check{
knownvalue.ObjectExact(map[string]knownvalue.Check{
names.AttrKey: knownvalue.StringExact("FailureSimulation"),
"use_previous_value": knownvalue.Bool(false),
names.AttrValue: knownvalue.StringExact(acctest.CtTrue),
}),
knownvalue.ObjectExact(map[string]knownvalue.Check{
names.AttrKey: knownvalue.StringExact("ExtraParam"),
"use_previous_value": knownvalue.Bool(false),
names.AttrValue: knownvalue.StringExact("updated"),
}),
})),
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrStatus), knownvalue.StringExact("TAINTED")),
// Verify state is rolled back to the parameters from the original setup run
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("provisioning_parameters"), knownvalue.ListExact([]knownvalue.Check{
knownvalue.ObjectExact(map[string]knownvalue.Check{
names.AttrKey: knownvalue.StringExact("FailureSimulation"),
"use_previous_value": knownvalue.Bool(false),
names.AttrValue: knownvalue.StringExact(acctest.CtFalse),
}),
knownvalue.ObjectExact(map[string]knownvalue.Check{
names.AttrKey: knownvalue.StringExact("ExtraParam"),
"use_previous_value": knownvalue.Bool(false),
names.AttrValue: knownvalue.StringExact("original"),
}),
})),
},
},
// Step 3 - Verify an update is planned, even without configuration changes
{
Config: testAccProvisionedProductConfig_retryTaintedUpdate(rName, true, true, "updated"),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate),
},
},
ExpectError: regexache.MustCompile(`The following resource\(s\) failed to update:`),
},
// Step 4 - Resolve the failure, verifying an update is completed
{
Config: testAccProvisionedProductConfig_retryTaintedUpdate(rName, true, false, "updated"),
Check: resource.ComposeTestCheckFunc(
testAccCheckProvisionedProductExists(ctx, resourceName, &v),
resource.TestCheckResourceAttrPair(resourceName, "provisioning_artifact_id", artifactsDataSourceName, newArtifactID),
),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate),
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrStatus), knownvalue.StringExact("AVAILABLE")),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("provisioning_parameters"), knownvalue.ListExact([]knownvalue.Check{
knownvalue.ObjectExact(map[string]knownvalue.Check{
names.AttrKey: knownvalue.StringExact("FailureSimulation"),
"use_previous_value": knownvalue.Bool(false),
names.AttrValue: knownvalue.StringExact(acctest.CtFalse),
}),
knownvalue.ObjectExact(map[string]knownvalue.Check{
names.AttrKey: knownvalue.StringExact("ExtraParam"),
"use_previous_value": knownvalue.Bool(false),
names.AttrValue: knownvalue.StringExact("updated"),
}),
})),
},
},
},
})
}

func testAccCheckProvisionedProductDestroy(ctx context.Context) resource.TestCheckFunc {
return func(s *terraform.State) error {
conn := acctest.Provider.Meta().(*conns.AWSClient).ServiceCatalogClient(ctx)
Expand All @@ -466,43 +590,39 @@ func testAccCheckProvisionedProductDestroy(ctx context.Context) resource.TestChe
continue
}

input := &servicecatalog.DescribeProvisionedProductInput{
Id: aws.String(rs.Primary.ID),
AcceptLanguage: aws.String(rs.Primary.Attributes["accept_language"]),
}
_, err := conn.DescribeProvisionedProduct(ctx, input)
_, err := tfservicecatalog.FindProvisionedProductByTwoPartKey(ctx, conn, rs.Primary.ID, rs.Primary.Attributes["accept_language"])

if errs.IsA[*awstypes.ResourceNotFoundException](err) {
if tfresource.NotFound(err) {
continue
}

if err != nil {
return err
}

return fmt.Errorf("Service Catalog Provisioned Product (%s) still exists", rs.Primary.ID)
return fmt.Errorf("Service Catalog Provisioned Product %s still exists", rs.Primary.ID)
}

return nil
}
}

func testAccCheckProvisionedProductExists(ctx context.Context, resourceName string, pprod *awstypes.ProvisionedProductDetail) resource.TestCheckFunc {
func testAccCheckProvisionedProductExists(ctx context.Context, n string, v *awstypes.ProvisionedProductDetail) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[resourceName]

rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("resource not found: %s", resourceName)
return fmt.Errorf("Not found: %s", n)
}

conn := acctest.Provider.Meta().(*conns.AWSClient).ServiceCatalogClient(ctx)

out, err := tfservicecatalog.WaitProvisionedProductReady(ctx, conn, tfservicecatalog.AcceptLanguageEnglish, rs.Primary.ID, "", tfservicecatalog.ProvisionedProductReadyTimeout)
output, err := tfservicecatalog.FindProvisionedProductByTwoPartKey(ctx, conn, rs.Primary.ID, rs.Primary.Attributes["accept_language"])

if err != nil {
return fmt.Errorf("describing Service Catalog Provisioned Product (%s): %w", rs.Primary.ID, err)
return err
}

*pprod = *out.ProvisionedProductDetail
*v = *output.ProvisionedProductDetail

return nil
}
Expand Down Expand Up @@ -988,3 +1108,71 @@ resource "aws_s3_bucket" "conflict" {
}
`, rName, conflictingBucketName, tagValue))
}

func testAccProvisionedProductConfig_retryTaintedUpdate(rName string, useNewVersion bool, simulateFailure bool, extraParam string) string {
return acctest.ConfigCompose(
testAccProvisionedProductPortfolioBaseConfig(rName),
fmt.Sprintf(`
locals {
initial_provisioning_artifact = data.aws_servicecatalog_provisioning_artifacts.product_artifacts.provisioning_artifact_details[0]
new_provisioning_artifact = data.aws_servicecatalog_provisioning_artifacts.product_artifacts.provisioning_artifact_details[1]
}

resource "aws_servicecatalog_provisioned_product" "test" {
name = %[1]q
product_id = aws_servicecatalog_product.test.id
provisioning_artifact_id = %[2]t ? local.new_provisioning_artifact.id : local.initial_provisioning_artifact.id

provisioning_parameters {
key = "FailureSimulation"
value = "%[3]t"
}

provisioning_parameters {
key = "ExtraParam"
value = %[4]q
}
}

resource "aws_servicecatalog_product" "test" {
description = %[1]q
name = %[1]q
owner = "test"
type = "CLOUD_FORMATION_TEMPLATE"

provisioning_artifact_parameters {
name = "%[1]s - Initial"
description = "Initial"
template_url = "https://${aws_s3_bucket.test.bucket_regional_domain_name}/${aws_s3_object.test.key}"
type = "CLOUD_FORMATION_TEMPLATE"
}
}

resource "aws_servicecatalog_provisioning_artifact" "new_version" {
product_id = aws_servicecatalog_product.test.id

name = "%[1]s - New"
description = "New"
template_url = "https://${aws_s3_bucket.test.bucket_regional_domain_name}/${aws_s3_object.test.key}"
type = "CLOUD_FORMATION_TEMPLATE"
}

data "aws_servicecatalog_provisioning_artifacts" "product_artifacts" {
product_id = aws_servicecatalog_product.test.id

depends_on = [aws_servicecatalog_provisioning_artifact.new_version]
}

resource "aws_s3_bucket" "test" {
bucket = %[1]q
force_destroy = true
}

resource "aws_s3_object" "test" {
bucket = aws_s3_bucket.test.id
key = "product_template.yaml"

source = "${path.module}/testdata/retry-tainted-update/product_template.yaml"
}
`, rName, useNewVersion, simulateFailure, extraParam))
}
33 changes: 0 additions & 33 deletions internal/service/servicecatalog/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,39 +321,6 @@ func statusLaunchPaths(ctx context.Context, conn *servicecatalog.Client, acceptL
}
}

func statusProvisionedProduct(ctx context.Context, conn *servicecatalog.Client, acceptLanguage, id, name string) retry.StateRefreshFunc {
return func() (any, string, error) {
input := &servicecatalog.DescribeProvisionedProductInput{}

if acceptLanguage != "" {
input.AcceptLanguage = aws.String(acceptLanguage)
}

// one or the other but not both
if id != "" {
input.Id = aws.String(id)
} else if name != "" {
input.Name = aws.String(name)
}

output, err := conn.DescribeProvisionedProduct(ctx, input)

if errs.IsA[*awstypes.ResourceNotFoundException](err) {
return nil, "", nil
}

if err != nil {
return nil, "", err
}

if output == nil || output.ProvisionedProductDetail == nil {
return nil, "", nil
}

return output, string(output.ProvisionedProductDetail.Status), err
}
}

func statusPortfolioConstraints(ctx context.Context, conn *servicecatalog.Client, acceptLanguage, portfolioID, productID string) retry.StateRefreshFunc {
return func() (any, string, error) {
input := &servicecatalog.ListConstraintsForPortfolioInput{
Expand Down
Loading
Loading