diff --git a/.changelog/32243.txt b/.changelog/32243.txt new file mode 100644 index 00000000000..8953d713b7d --- /dev/null +++ b/.changelog/32243.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_servicecatalog_principal_portfolio_association: Fix `ResourceNotFoundException` errors on resource Delete when configured `principal_type` is `IAM_PATTERN` +``` \ No newline at end of file diff --git a/internal/service/servicecatalog/find.go b/internal/service/servicecatalog/find.go index ab05b8269f8..0862bcf97e4 100644 --- a/internal/service/servicecatalog/find.go +++ b/internal/service/servicecatalog/find.go @@ -149,36 +149,3 @@ func FindTagOptionResourceAssociation(ctx context.Context, conn *servicecatalog. return result, err } - -func FindPrincipalPortfolioAssociation(ctx context.Context, conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID string) (*servicecatalog.Principal, error) { - input := &servicecatalog.ListPrincipalsForPortfolioInput{ - PortfolioId: aws.String(portfolioID), - } - - if acceptLanguage != "" { - input.AcceptLanguage = aws.String(acceptLanguage) - } - - var result *servicecatalog.Principal - - err := conn.ListPrincipalsForPortfolioPagesWithContext(ctx, input, func(page *servicecatalog.ListPrincipalsForPortfolioOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, deet := range page.Principals { - if deet == nil { - continue - } - - if aws.StringValue(deet.PrincipalARN) == principalARN { - result = deet - return false - } - } - - return !lastPage - }) - - return result, err -} diff --git a/internal/service/servicecatalog/id.go b/internal/service/servicecatalog/id.go index a977cd77e89..4af593f2019 100644 --- a/internal/service/servicecatalog/id.go +++ b/internal/service/servicecatalog/id.go @@ -77,20 +77,6 @@ func ProvisioningArtifactParseID(id string) (string, string, error) { return parts[0], parts[1], nil } -func PrincipalPortfolioAssociationParseID(id string) (string, string, string, error) { - parts := strings.SplitN(id, ",", 3) - - if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { - return "", "", "", fmt.Errorf("unexpected format of ID (%s), expected acceptLanguage,principalARN,portfolioID", id) - } - - return parts[0], parts[1], parts[2], nil -} - -func PrincipalPortfolioAssociationID(acceptLanguage, principalARN, portfolioID string) string { - return strings.Join([]string{acceptLanguage, principalARN, portfolioID}, ",") -} - func PortfolioConstraintsID(acceptLanguage, portfolioID, productID string) string { return strings.Join([]string{acceptLanguage, portfolioID, productID}, ":") } diff --git a/internal/service/servicecatalog/principal_portfolio_association.go b/internal/service/servicecatalog/principal_portfolio_association.go index 7b3a98060e9..fae6b29fe54 100644 --- a/internal/service/servicecatalog/principal_portfolio_association.go +++ b/internal/service/servicecatalog/principal_portfolio_association.go @@ -5,7 +5,10 @@ package servicecatalog import ( "context" + "fmt" "log" + "strings" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/servicecatalog" @@ -16,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) @@ -25,14 +29,24 @@ func ResourcePrincipalPortfolioAssociation() *schema.Resource { CreateWithoutTimeout: resourcePrincipalPortfolioAssociationCreate, ReadWithoutTimeout: resourcePrincipalPortfolioAssociationRead, DeleteWithoutTimeout: resourcePrincipalPortfolioAssociationDelete, + Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(PrincipalPortfolioAssociationReadyTimeout), - Read: schema.DefaultTimeout(PrincipalPortfolioAssociationReadTimeout), - Delete: schema.DefaultTimeout(PrincipalPortfolioAssociationDeleteTimeout), + Create: schema.DefaultTimeout(3 * time.Minute), + Read: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(3 * time.Minute), + }, + + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourcePrincipalPortfolioAssociationV0().CoreConfigSchema().ImpliedType(), + Upgrade: principalPortfolioAssociationUpgradeV0, + Version: 0, + }, }, Schema: map[string]*schema.Schema{ @@ -68,50 +82,33 @@ func resourcePrincipalPortfolioAssociationCreate(ctx context.Context, d *schema. var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ServiceCatalogConn(ctx) + acceptLanguage, principalARN, portfolioID, principalType := d.Get("accept_language").(string), d.Get("principal_arn").(string), d.Get("portfolio_id").(string), d.Get("principal_type").(string) + id := PrincipalPortfolioAssociationCreateResourceID(acceptLanguage, principalARN, portfolioID, principalType) input := &servicecatalog.AssociatePrincipalWithPortfolioInput{ - PortfolioId: aws.String(d.Get("portfolio_id").(string)), - PrincipalARN: aws.String(d.Get("principal_arn").(string)), + AcceptLanguage: aws.String(acceptLanguage), + PortfolioId: aws.String(portfolioID), + PrincipalARN: aws.String(principalARN), + PrincipalType: aws.String(principalType), } - if v, ok := d.GetOk("accept_language"); ok { - input.AcceptLanguage = aws.String(v.(string)) - } + _, err := tfresource.RetryWhenAWSErrMessageContains(ctx, d.Timeout(schema.TimeoutCreate), func() (interface{}, error) { + return conn.AssociatePrincipalWithPortfolioWithContext(ctx, input) + }, servicecatalog.ErrCodeInvalidParametersException, "profile does not exist") - if v, ok := d.GetOk("principal_type"); ok { - input.PrincipalType = aws.String(v.(string)) + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating Service Catalog Principal Portfolio Association (%s): %s", id, err) } - var output *servicecatalog.AssociatePrincipalWithPortfolioOutput - err := retry.RetryContext(ctx, d.Timeout(schema.TimeoutCreate), func() *retry.RetryError { - var err error - - output, err = conn.AssociatePrincipalWithPortfolioWithContext(ctx, input) - - if tfawserr.ErrMessageContains(err, servicecatalog.ErrCodeInvalidParametersException, "profile does not exist") { - return retry.RetryableError(err) - } - - if err != nil { - return retry.NonRetryableError(err) - } + d.SetId(id) - return nil + _, err = tfresource.RetryWhenNotFound(ctx, d.Timeout(schema.TimeoutRead), func() (interface{}, error) { + return FindPrincipalPortfolioAssociation(ctx, conn, acceptLanguage, principalARN, portfolioID, principalType) }) - if tfresource.TimedOut(err) { - output, err = conn.AssociatePrincipalWithPortfolioWithContext(ctx, input) - } - if err != nil { - return sdkdiag.AppendErrorf(diags, "associating Service Catalog Principal with Portfolio: %s", err) - } - - if output == nil { - return sdkdiag.AppendErrorf(diags, "creating Service Catalog Principal Portfolio Association: empty response") + return sdkdiag.AppendErrorf(diags, "waiting for Service Catalog Principal Portfolio Association (%s) create: %s", d.Id(), err) } - d.SetId(PrincipalPortfolioAssociationID(d.Get("accept_language").(string), d.Get("principal_arn").(string), d.Get("portfolio_id").(string))) - return append(diags, resourcePrincipalPortfolioAssociationRead(ctx, d, meta)...) } @@ -119,30 +116,21 @@ func resourcePrincipalPortfolioAssociationRead(ctx context.Context, d *schema.Re var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ServiceCatalogConn(ctx) - acceptLanguage, principalARN, portfolioID, err := PrincipalPortfolioAssociationParseID(d.Id()) - + acceptLanguage, principalARN, portfolioID, principalType, err := PrincipalPortfolioAssociationParseResourceID(d.Id()) if err != nil { - return sdkdiag.AppendErrorf(diags, "could not parse ID (%s): %s", d.Id(), err) - } - - if acceptLanguage == "" { - acceptLanguage = AcceptLanguageEnglish + return sdkdiag.AppendFromErr(diags, err) } - output, err := WaitPrincipalPortfolioAssociationReady(ctx, conn, acceptLanguage, principalARN, portfolioID, d.Timeout(schema.TimeoutRead)) + output, err := FindPrincipalPortfolioAssociation(ctx, conn, acceptLanguage, principalARN, portfolioID, principalType) - if !d.IsNewResource() && (tfresource.NotFound(err) || tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException)) { + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] Service Catalog Principal Portfolio Association (%s) not found, removing from state", d.Id()) d.SetId("") return diags } if err != nil { - return sdkdiag.AppendErrorf(diags, "describing Service Catalog Principal Portfolio Association (%s): %s", d.Id(), err) - } - - if output == nil { - return sdkdiag.AppendErrorf(diags, "getting Service Catalog Principal Portfolio Association (%s): empty response", d.Id()) + return sdkdiag.AppendErrorf(diags, "reading Service Catalog Principal Portfolio Association (%s): %s", d.Id(), err) } d.Set("accept_language", acceptLanguage) @@ -157,22 +145,19 @@ func resourcePrincipalPortfolioAssociationDelete(ctx context.Context, d *schema. var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ServiceCatalogConn(ctx) - acceptLanguage, principalARN, portfolioID, err := PrincipalPortfolioAssociationParseID(d.Id()) - + acceptLanguage, principalARN, portfolioID, principalType, err := PrincipalPortfolioAssociationParseResourceID(d.Id()) if err != nil { - return sdkdiag.AppendErrorf(diags, "could not parse ID (%s): %s", d.Id(), err) - } - - if acceptLanguage == "" { - acceptLanguage = AcceptLanguageEnglish + return sdkdiag.AppendFromErr(diags, err) } input := &servicecatalog.DisassociatePrincipalFromPortfolioInput{ PortfolioId: aws.String(portfolioID), PrincipalARN: aws.String(principalARN), AcceptLanguage: aws.String(acceptLanguage), + PrincipalType: aws.String(principalType), } + log.Printf("[WARN] Deleting Service Catalog Principal Portfolio Association: %s", d.Id()) _, err = conn.DisassociatePrincipalFromPortfolioWithContext(ctx, input) if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { @@ -180,18 +165,129 @@ func resourcePrincipalPortfolioAssociationDelete(ctx context.Context, d *schema. } if err != nil { - return sdkdiag.AppendErrorf(diags, "disassociating Service Catalog Principal from Portfolio (%s): %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "deleting Service Catalog Principal Portfolio Association (%s): %s", d.Id(), err) } - err = WaitPrincipalPortfolioAssociationDeleted(ctx, conn, acceptLanguage, principalARN, portfolioID, d.Timeout(schema.TimeoutDelete)) + _, err = tfresource.RetryUntilNotFound(ctx, d.Timeout(schema.TimeoutDelete), func() (interface{}, error) { + return FindPrincipalPortfolioAssociation(ctx, conn, acceptLanguage, principalARN, portfolioID, principalType) + }) - if tfresource.NotFound(err) || tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { - return diags + if err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for Service Catalog Principal Portfolio Association (%s) delete: %s", d.Id(), err) + } + + return diags +} + +const principalPortfolioAssociationResourceIDSeparator = "," + +func PrincipalPortfolioAssociationParseResourceID(id string) (string, string, string, string, error) { + parts := strings.SplitN(id, principalPortfolioAssociationResourceIDSeparator, 4) + + if len(parts) != 4 || parts[0] == "" || parts[1] == "" || parts[2] == "" || parts[3] == "" { + return "", "", "", "", fmt.Errorf("unexpected format of ID (%[1]s), expected acceptLanguage%[2]sprincipalARN%[2]sportfolioID%[2]sprincipalType", id, principalPortfolioAssociationResourceIDSeparator) } + return parts[0], parts[1], parts[2], parts[3], nil +} + +func PrincipalPortfolioAssociationCreateResourceID(acceptLanguage, principalARN, portfolioID, principalType string) string { + return strings.Join([]string{acceptLanguage, principalARN, portfolioID, principalType}, principalPortfolioAssociationResourceIDSeparator) +} + +func findPrincipalForPortfolio(ctx context.Context, conn *servicecatalog.ServiceCatalog, input *servicecatalog.ListPrincipalsForPortfolioInput, filter tfslices.Predicate[*servicecatalog.Principal]) (*servicecatalog.Principal, error) { + output, err := findPrincipalsForPortfolio(ctx, conn, input, filter) + if err != nil { - return sdkdiag.AppendErrorf(diags, "waiting for Service Catalog Principal Portfolio Disassociation (%s): %s", d.Id(), err) + return nil, err } - return diags + return tfresource.AssertSinglePtrResult(output) +} + +func findPrincipalsForPortfolio(ctx context.Context, conn *servicecatalog.ServiceCatalog, input *servicecatalog.ListPrincipalsForPortfolioInput, filter tfslices.Predicate[*servicecatalog.Principal]) ([]*servicecatalog.Principal, error) { + var output []*servicecatalog.Principal + + err := conn.ListPrincipalsForPortfolioPagesWithContext(ctx, input, func(page *servicecatalog.ListPrincipalsForPortfolioOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.Principals { + if v != nil && filter(v) { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} + +func FindPrincipalPortfolioAssociation(ctx context.Context, conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID, principalType string) (*servicecatalog.Principal, error) { + input := &servicecatalog.ListPrincipalsForPortfolioInput{ + AcceptLanguage: aws.String(acceptLanguage), + PortfolioId: aws.String(portfolioID), + } + filter := func(v *servicecatalog.Principal) bool { + return aws.StringValue(v.PrincipalARN) == principalARN && aws.StringValue(v.PrincipalType) == principalType + } + + return findPrincipalForPortfolio(ctx, conn, input, filter) +} + +// aws_autoscaling_group aws_servicecatalog_principal_portfolio_association's Schema @v5.15.0 minus validators. +func resourcePrincipalPortfolioAssociationV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "accept_language": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: AcceptLanguageEnglish, + }, + "portfolio_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "principal_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "principal_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: servicecatalog.PrincipalTypeIam, + }, + }, + } +} + +func principalPortfolioAssociationUpgradeV0(_ context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + if rawState == nil { + rawState = map[string]interface{}{} + } + + // Is resource ID in the correct format? + if _, _, _, _, err := PrincipalPortfolioAssociationParseResourceID(rawState["id"].(string)); err != nil { + acceptLanguage, principalARN, portfolioID, principalType := rawState["accept_language"].(string), rawState["principal_arn"].(string), rawState["portfolio_id"].(string), rawState["principal_type"].(string) + rawState["id"] = PrincipalPortfolioAssociationCreateResourceID(acceptLanguage, principalARN, portfolioID, principalType) + } + + return rawState, nil } diff --git a/internal/service/servicecatalog/principal_portfolio_association_test.go b/internal/service/servicecatalog/principal_portfolio_association_test.go index db2ab4c93c8..be50b5f527a 100644 --- a/internal/service/servicecatalog/principal_portfolio_association_test.go +++ b/internal/service/servicecatalog/principal_portfolio_association_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/aws/aws-sdk-go/service/servicecatalog" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" @@ -19,8 +18,6 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) -// add sweeper to delete known test servicecat principal portfolio associations - func TestAccServiceCatalogPrincipalPortfolioAssociation_basic(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_servicecatalog_principal_portfolio_association.test" @@ -49,6 +46,33 @@ func TestAccServiceCatalogPrincipalPortfolioAssociation_basic(t *testing.T) { }) } +func TestAccServiceCatalogPrincipalPortfolioAssociation_iam_pattern(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_servicecatalog_principal_portfolio_association.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, servicecatalog.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPrincipalPortfolioAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPrincipalPortfolioAssociationConfig_iam_pattern(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckPrincipalPortfolioAssociationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "portfolio_id", "aws_servicecatalog_portfolio.test", "id"), + resource.TestCheckResourceAttr(resourceName, "principal_type", "IAM_PATTERN"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} func TestAccServiceCatalogPrincipalPortfolioAssociation_disappears(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_servicecatalog_principal_portfolio_association.test" @@ -72,6 +96,41 @@ func TestAccServiceCatalogPrincipalPortfolioAssociation_disappears(t *testing.T) }) } +func TestAccServiceCatalogPrincipalPortfolioAssociation_migrateV0(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_servicecatalog_principal_portfolio_association.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, servicecatalog.EndpointsID), + CheckDestroy: testAccCheckPrincipalPortfolioAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "aws": { + Source: "hashicorp/aws", + VersionConstraint: "5.15.0", + }, + }, + Config: testAccPrincipalPortfolioAssociationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + // Can't call this as the old ID format is invalid. + // testAccCheckPrincipalPortfolioAssociationExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "principal_type", "IAM"), + ), + }, + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Config: testAccPrincipalPortfolioAssociationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckPrincipalPortfolioAssociationExists(ctx, resourceName), + ), + }, + }, + }) +} + func testAccCheckPrincipalPortfolioAssociationDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).ServiceCatalogConn(ctx) @@ -81,50 +140,45 @@ func testAccCheckPrincipalPortfolioAssociationDestroy(ctx context.Context) resou continue } - acceptLanguage, principalARN, portfolioID, err := tfservicecatalog.PrincipalPortfolioAssociationParseID(rs.Primary.ID) - + acceptLanguage, principalARN, portfolioID, principalType, err := tfservicecatalog.PrincipalPortfolioAssociationParseResourceID(rs.Primary.ID) if err != nil { - return fmt.Errorf("could not parse ID (%s): %w", rs.Primary.ID, err) + return err } - err = tfservicecatalog.WaitPrincipalPortfolioAssociationDeleted(ctx, conn, acceptLanguage, principalARN, portfolioID, tfservicecatalog.PrincipalPortfolioAssociationDeleteTimeout) + _, err = tfservicecatalog.FindPrincipalPortfolioAssociation(ctx, conn, acceptLanguage, principalARN, portfolioID, principalType) - if tfresource.NotFound(err) || tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + if tfresource.NotFound(err) { continue } if err != nil { - return fmt.Errorf("waiting for Service Catalog Principal Portfolio Association to be destroyed (%s): %w", rs.Primary.ID, err) + return err } + + return fmt.Errorf("Service Catalog Principal Portfolio Association (%s) still exists", rs.Primary.ID) } return nil } } -func testAccCheckPrincipalPortfolioAssociationExists(ctx context.Context, resourceName string) resource.TestCheckFunc { +func testAccCheckPrincipalPortfolioAssociationExists(ctx context.Context, n string) 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) } - acceptLanguage, principalARN, portfolioID, err := tfservicecatalog.PrincipalPortfolioAssociationParseID(rs.Primary.ID) - + acceptLanguage, principalARN, portfolioID, principalType, err := tfservicecatalog.PrincipalPortfolioAssociationParseResourceID(rs.Primary.ID) if err != nil { - return fmt.Errorf("could not parse ID (%s): %w", rs.Primary.ID, err) + return err } conn := acctest.Provider.Meta().(*conns.AWSClient).ServiceCatalogConn(ctx) - _, err = tfservicecatalog.WaitPrincipalPortfolioAssociationReady(ctx, conn, acceptLanguage, principalARN, portfolioID, tfservicecatalog.PrincipalPortfolioAssociationReadyTimeout) - - if err != nil { - return fmt.Errorf("waiting for Service Catalog Principal Portfolio Association existence (%s): %w", rs.Primary.ID, err) - } + _, err = tfservicecatalog.FindPrincipalPortfolioAssociation(ctx, conn, acceptLanguage, principalARN, portfolioID, principalType) - return nil + return err } } @@ -163,3 +217,13 @@ resource "aws_servicecatalog_principal_portfolio_association" "test" { } `) } + +func testAccPrincipalPortfolioAssociationConfig_iam_pattern(rName string) string { + return acctest.ConfigCompose(testAccPrincipalPortfolioAssociationConfig_base(rName), ` +resource "aws_servicecatalog_principal_portfolio_association" "test" { + portfolio_id = aws_servicecatalog_portfolio.test.id + principal_arn = "arn:${data.aws_partition.current.partition}:iam:::role/${aws_iam_role.test.name}" + principal_type = "IAM_PATTERN" +} +`) +} diff --git a/internal/service/servicecatalog/status.go b/internal/service/servicecatalog/status.go index 207d75f3953..4839e6990ee 100644 --- a/internal/service/servicecatalog/status.go +++ b/internal/service/servicecatalog/status.go @@ -296,26 +296,6 @@ func StatusProvisioningArtifact(ctx context.Context, conn *servicecatalog.Servic } } -func StatusPrincipalPortfolioAssociation(ctx context.Context, conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - output, err := FindPrincipalPortfolioAssociation(ctx, conn, acceptLanguage, principalARN, portfolioID) - - if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { - return nil, StatusNotFound, err - } - - if err != nil { - return nil, servicecatalog.StatusFailed, fmt.Errorf("describing principal portfolio association: %w", err) - } - - if output == nil { - return nil, StatusNotFound, err - } - - return output, servicecatalog.StatusAvailable, err - } -} - func StatusLaunchPaths(ctx context.Context, conn *servicecatalog.ServiceCatalog, acceptLanguage, productID string) retry.StateRefreshFunc { return func() (interface{}, string, error) { input := &servicecatalog.ListLaunchPathsInput{ diff --git a/internal/service/servicecatalog/sweep.go b/internal/service/servicecatalog/sweep.go index e378ac164b2..85e4c598112 100644 --- a/internal/service/servicecatalog/sweep.go +++ b/internal/service/servicecatalog/sweep.go @@ -306,7 +306,7 @@ func sweepPrincipalPortfolioAssociations(region string) error { r := ResourcePrincipalPortfolioAssociation() d := r.Data(nil) - d.SetId(PrincipalPortfolioAssociationID(AcceptLanguageEnglish, aws.StringValue(principal.PrincipalARN), aws.StringValue(detail.Id))) + d.SetId(PrincipalPortfolioAssociationCreateResourceID(AcceptLanguageEnglish, aws.StringValue(principal.PrincipalARN), aws.StringValue(detail.Id), aws.StringValue(principal.PrincipalType))) sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) } diff --git a/internal/service/servicecatalog/wait.go b/internal/service/servicecatalog/wait.go index 051abc1fc46..95865ab7ef8 100644 --- a/internal/service/servicecatalog/wait.go +++ b/internal/service/servicecatalog/wait.go @@ -15,53 +15,50 @@ import ( ) const ( - BudgetResourceAssociationDeleteTimeout = 3 * time.Minute - BudgetResourceAssociationReadTimeout = 10 * time.Minute - BudgetResourceAssociationReadyTimeout = 3 * time.Minute - ConstraintDeleteTimeout = 3 * time.Minute - ConstraintReadTimeout = 10 * time.Minute - ConstraintReadyTimeout = 3 * time.Minute - ConstraintUpdateTimeout = 3 * time.Minute - LaunchPathsReadyTimeout = 3 * time.Minute - OrganizationsAccessStableTimeout = 3 * time.Minute - PortfolioConstraintsReadyTimeout = 3 * time.Minute - PortfolioCreateTimeout = 30 * time.Minute - PortfolioDeleteTimeout = 30 * time.Minute - PortfolioReadTimeout = 10 * time.Minute - PortfolioShareCreateTimeout = 3 * time.Minute - PortfolioShareDeleteTimeout = 3 * time.Minute - PortfolioShareReadTimeout = 10 * time.Minute - PortfolioShareUpdateTimeout = 3 * time.Minute - PortfolioUpdateTimeout = 30 * time.Minute - PrincipalPortfolioAssociationDeleteTimeout = 3 * time.Minute - PrincipalPortfolioAssociationReadTimeout = 10 * time.Minute - PrincipalPortfolioAssociationReadyTimeout = 3 * time.Minute - ProductDeleteTimeout = 5 * time.Minute - ProductPortfolioAssociationDeleteTimeout = 3 * time.Minute - ProductPortfolioAssociationReadTimeout = 10 * time.Minute - ProductPortfolioAssociationReadyTimeout = 3 * time.Minute - ProductReadTimeout = 10 * time.Minute - ProductReadyTimeout = 5 * time.Minute - ProductUpdateTimeout = 5 * time.Minute - ProvisionedProductDeleteTimeout = 30 * time.Minute - ProvisionedProductReadTimeout = 10 * time.Minute - ProvisionedProductReadyTimeout = 30 * time.Minute - ProvisionedProductUpdateTimeout = 30 * time.Minute - ProvisioningArtifactDeleteTimeout = 3 * time.Minute - ProvisioningArtifactReadTimeout = 10 * time.Minute - ProvisioningArtifactReadyTimeout = 3 * time.Minute - ProvisioningArtifactUpdateTimeout = 3 * time.Minute - ServiceActionDeleteTimeout = 3 * time.Minute - ServiceActionReadTimeout = 10 * time.Minute - ServiceActionReadyTimeout = 3 * time.Minute - ServiceActionUpdateTimeout = 3 * time.Minute - TagOptionDeleteTimeout = 3 * time.Minute - TagOptionReadTimeout = 10 * time.Minute - TagOptionReadyTimeout = 3 * time.Minute - TagOptionResourceAssociationDeleteTimeout = 3 * time.Minute - TagOptionResourceAssociationReadTimeout = 10 * time.Minute - TagOptionResourceAssociationReadyTimeout = 3 * time.Minute - TagOptionUpdateTimeout = 3 * time.Minute + BudgetResourceAssociationDeleteTimeout = 3 * time.Minute + BudgetResourceAssociationReadTimeout = 10 * time.Minute + BudgetResourceAssociationReadyTimeout = 3 * time.Minute + ConstraintDeleteTimeout = 3 * time.Minute + ConstraintReadTimeout = 10 * time.Minute + ConstraintReadyTimeout = 3 * time.Minute + ConstraintUpdateTimeout = 3 * time.Minute + LaunchPathsReadyTimeout = 3 * time.Minute + OrganizationsAccessStableTimeout = 3 * time.Minute + PortfolioConstraintsReadyTimeout = 3 * time.Minute + PortfolioCreateTimeout = 30 * time.Minute + PortfolioDeleteTimeout = 30 * time.Minute + PortfolioReadTimeout = 10 * time.Minute + PortfolioShareCreateTimeout = 3 * time.Minute + PortfolioShareDeleteTimeout = 3 * time.Minute + PortfolioShareReadTimeout = 10 * time.Minute + PortfolioShareUpdateTimeout = 3 * time.Minute + PortfolioUpdateTimeout = 30 * time.Minute + ProductDeleteTimeout = 5 * time.Minute + ProductPortfolioAssociationDeleteTimeout = 3 * time.Minute + ProductPortfolioAssociationReadTimeout = 10 * time.Minute + ProductPortfolioAssociationReadyTimeout = 3 * time.Minute + ProductReadTimeout = 10 * time.Minute + ProductReadyTimeout = 5 * time.Minute + ProductUpdateTimeout = 5 * time.Minute + ProvisionedProductDeleteTimeout = 30 * time.Minute + ProvisionedProductReadTimeout = 10 * time.Minute + ProvisionedProductReadyTimeout = 30 * time.Minute + ProvisionedProductUpdateTimeout = 30 * time.Minute + ProvisioningArtifactDeleteTimeout = 3 * time.Minute + ProvisioningArtifactReadTimeout = 10 * time.Minute + ProvisioningArtifactReadyTimeout = 3 * time.Minute + ProvisioningArtifactUpdateTimeout = 3 * time.Minute + ServiceActionDeleteTimeout = 3 * time.Minute + ServiceActionReadTimeout = 10 * time.Minute + ServiceActionReadyTimeout = 3 * time.Minute + ServiceActionUpdateTimeout = 3 * time.Minute + TagOptionDeleteTimeout = 3 * time.Minute + TagOptionReadTimeout = 10 * time.Minute + TagOptionReadyTimeout = 3 * time.Minute + TagOptionResourceAssociationDeleteTimeout = 3 * time.Minute + TagOptionResourceAssociationReadTimeout = 10 * time.Minute + TagOptionResourceAssociationReadyTimeout = 3 * time.Minute + TagOptionUpdateTimeout = 3 * time.Minute MinTimeout = 2 * time.Second NotFoundChecks = 5 @@ -441,40 +438,6 @@ func WaitProvisioningArtifactDeleted(ctx context.Context, conn *servicecatalog.S return err } -func WaitPrincipalPortfolioAssociationReady(ctx context.Context, conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID string, timeout time.Duration) (*servicecatalog.Principal, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{StatusNotFound, StatusUnavailable}, - Target: []string{servicecatalog.StatusAvailable}, - Refresh: StatusPrincipalPortfolioAssociation(ctx, conn, acceptLanguage, principalARN, portfolioID), - Timeout: timeout, - ContinuousTargetOccurence: ContinuousTargetOccurrence, - NotFoundChecks: NotFoundChecks, - MinTimeout: MinTimeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*servicecatalog.Principal); ok { - return output, err - } - - return nil, err -} - -func WaitPrincipalPortfolioAssociationDeleted(ctx context.Context, conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID string, timeout time.Duration) error { - stateConf := &retry.StateChangeConf{ - Pending: []string{servicecatalog.StatusAvailable}, - Target: []string{StatusNotFound, StatusUnavailable}, - Refresh: StatusPrincipalPortfolioAssociation(ctx, conn, acceptLanguage, principalARN, portfolioID), - Timeout: timeout, - NotFoundChecks: 1, - } - - _, err := stateConf.WaitForStateContext(ctx) - - return err -} - func WaitLaunchPathsReady(ctx context.Context, conn *servicecatalog.ServiceCatalog, acceptLanguage, productID string, timeout time.Duration) ([]*servicecatalog.LaunchPathSummary, error) { stateConf := &retry.StateChangeConf{ Pending: []string{StatusNotFound}, diff --git a/website/docs/r/servicecatalog_principal_portfolio_association.html.markdown b/website/docs/r/servicecatalog_principal_portfolio_association.html.markdown index 97b4d708739..5f78848eccb 100644 --- a/website/docs/r/servicecatalog_principal_portfolio_association.html.markdown +++ b/website/docs/r/servicecatalog_principal_portfolio_association.html.markdown @@ -31,7 +31,7 @@ The following arguments are required: The following arguments are optional: * `accept_language` - (Optional) Language code. Valid values: `en` (English), `jp` (Japanese), `zh` (Chinese). Default value is `en`. -* `principal_type` - (Optional) Principal type. Setting this argument empty (e.g., `principal_type = ""`) will result in an error. Valid value is `IAM`. Default is `IAM`. +* `principal_type` - (Optional) Principal type. Setting this argument empty (e.g., `principal_type = ""`) will result in an error. Valid values are `IAM` and `IAM_PATTERN`. Default is `IAM`. ## Attribute Reference @@ -49,17 +49,17 @@ This resource exports the following attributes in addition to the arguments abov ## Import -In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import `aws_servicecatalog_principal_portfolio_association` using the accept language, principal ARN, and portfolio ID, separated by a comma. For example: +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import `aws_servicecatalog_principal_portfolio_association` using `accept_language`, `principal_arn`, `portfolio_id`, and `principal_type` separated by a comma. For example: ```terraform import { to = aws_servicecatalog_principal_portfolio_association.example - id = "en,arn:aws:iam::123456789012:user/Eleanor,port-68656c6c6f" + id = "en,arn:aws:iam::123456789012:user/Eleanor,port-68656c6c6f,IAM" } ``` -Using `terraform import`, import `aws_servicecatalog_principal_portfolio_association` using the accept language, principal ARN, and portfolio ID, separated by a comma. For example: +Using `terraform import`, import `aws_servicecatalog_principal_portfolio_association` using `accept_language`, `principal_arn`, `portfolio_id`, and `principal_type` separated by a comma. For example: ```console -% terraform import aws_servicecatalog_principal_portfolio_association.example en,arn:aws:iam::123456789012:user/Eleanor,port-68656c6c6f +% terraform import aws_servicecatalog_principal_portfolio_association.example en,arn:aws:iam::123456789012:user/Eleanor,port-68656c6c6f,IAM ```