Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addressing Issue 32229 - cleaning up IAM_PATTERN properly #32243

Merged
merged 13 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/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
Loading