Skip to content

Commit

Permalink
Merge pull request #32243 from mc-etcher/b-iam_pattern_cleanup_for_se…
Browse files Browse the repository at this point in the history
…rvice_catalog

Addressing Issue 32229 - cleaning up IAM_PATTERN properly
  • Loading branch information
ewbankkit authored Sep 6, 2023
2 parents 93cc3b0 + b2d64f2 commit 315cc9a
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 238 deletions.
3 changes: 3 additions & 0 deletions .changelog/32243.txt
Original file line number Diff line number Diff line change
@@ -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`
```
33 changes: 0 additions & 33 deletions internal/service/servicecatalog/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
14 changes: 0 additions & 14 deletions internal/service/servicecatalog/id.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}, ":")
}
220 changes: 158 additions & 62 deletions internal/service/servicecatalog/principal_portfolio_association.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)

Expand All @@ -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{
Expand Down Expand Up @@ -68,81 +82,55 @@ 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)...)
}

func resourcePrincipalPortfolioAssociationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
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)
Expand All @@ -157,41 +145,149 @@ 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) {
return diags
}

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
}
Loading

0 comments on commit 315cc9a

Please sign in to comment.