diff --git a/.changelog/19470.txt b/.changelog/19470.txt new file mode 100644 index 00000000000..6d33e373969 --- /dev/null +++ b/.changelog/19470.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_servicecatalog_principal_portfolio_association +``` \ No newline at end of file diff --git a/aws/data_source_aws_servicecatalog_constraint.go b/aws/data_source_aws_servicecatalog_constraint.go index 71e2a863782..a8b29de52c5 100644 --- a/aws/data_source_aws_servicecatalog_constraint.go +++ b/aws/data_source_aws_servicecatalog_constraint.go @@ -18,7 +18,7 @@ func dataSourceAwsServiceCatalogConstraint() *schema.Resource { "accept_language": { Type: schema.TypeString, Optional: true, - Default: "en", + Default: tfservicecatalog.AcceptLanguageEnglish, ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), }, "description": { diff --git a/aws/internal/service/servicecatalog/enum.go b/aws/internal/service/servicecatalog/enum.go index d8b8db7bc8e..90f3ea3ccfe 100644 --- a/aws/internal/service/servicecatalog/enum.go +++ b/aws/internal/service/servicecatalog/enum.go @@ -3,9 +3,9 @@ package servicecatalog const ( // If AWS adds these to the API, we should use those and remove these. - ServiceCatalogAcceptLanguageEnglish = "en" - ServiceCatalogAcceptLanguageJapanese = "jp" - ServiceCatalogAcceptLanguageChinese = "zh" + AcceptLanguageEnglish = "en" + AcceptLanguageJapanese = "jp" + AcceptLanguageChinese = "zh" ConstraintTypeLaunch = "LAUNCH" ConstraintTypeNotification = "NOTIFICATION" @@ -16,9 +16,9 @@ const ( func AcceptLanguage_Values() []string { return []string{ - ServiceCatalogAcceptLanguageEnglish, - ServiceCatalogAcceptLanguageJapanese, - ServiceCatalogAcceptLanguageChinese, + AcceptLanguageEnglish, + AcceptLanguageJapanese, + AcceptLanguageChinese, } } diff --git a/aws/internal/service/servicecatalog/finder/finder.go b/aws/internal/service/servicecatalog/finder/finder.go index 79157804479..0cf7c7496b8 100644 --- a/aws/internal/service/servicecatalog/finder/finder.go +++ b/aws/internal/service/servicecatalog/finder/finder.go @@ -127,3 +127,36 @@ func TagOptionResourceAssociation(conn *servicecatalog.ServiceCatalog, tagOption return result, err } + +func PrincipalPortfolioAssociation(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.ListPrincipalsForPortfolioPages(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/aws/internal/service/servicecatalog/id.go b/aws/internal/service/servicecatalog/id.go index 03a7f4ef7fe..3eb0a6f8a41 100644 --- a/aws/internal/service/servicecatalog/id.go +++ b/aws/internal/service/servicecatalog/id.go @@ -73,3 +73,17 @@ 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}, ",") +} diff --git a/aws/internal/service/servicecatalog/waiter/status.go b/aws/internal/service/servicecatalog/waiter/status.go index 371aa8c8aa5..338e821b70b 100644 --- a/aws/internal/service/servicecatalog/waiter/status.go +++ b/aws/internal/service/servicecatalog/waiter/status.go @@ -299,3 +299,23 @@ func ProvisioningArtifactStatus(conn *servicecatalog.ServiceCatalog, id, product return output, aws.StringValue(output.Status), err } } + +func PrincipalPortfolioAssociationStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.PrincipalPortfolioAssociation(conn, acceptLanguage, principalARN, portfolioID) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if err != nil { + return nil, servicecatalog.StatusFailed, fmt.Errorf("error describing principal portfolio association: %w", err) + } + + if output == nil { + return nil, StatusNotFound, err + } + + return output, servicecatalog.StatusAvailable, err + } +} diff --git a/aws/internal/service/servicecatalog/waiter/waiter.go b/aws/internal/service/servicecatalog/waiter/waiter.go index 8374a859dbd..b388ff87ff6 100644 --- a/aws/internal/service/servicecatalog/waiter/waiter.go +++ b/aws/internal/service/servicecatalog/waiter/waiter.go @@ -38,6 +38,9 @@ const ( ProvisioningArtifactReadyTimeout = 3 * time.Minute ProvisioningArtifactDeletedTimeout = 3 * time.Minute + PrincipalPortfolioAssociationReadyTimeout = 3 * time.Minute + PrincipalPortfolioAssociationDeleteTimeout = 3 * time.Minute + StatusNotFound = "NOT_FOUND" StatusUnavailable = "UNAVAILABLE" @@ -407,3 +410,36 @@ func ProvisioningArtifactDeleted(conn *servicecatalog.ServiceCatalog, id, produc return nil } + +func PrincipalPortfolioAssociationReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID string) (*servicecatalog.Principal, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: PrincipalPortfolioAssociationStatus(conn, acceptLanguage, principalARN, portfolioID), + Timeout: PrincipalPortfolioAssociationReadyTimeout, + NotFoundChecks: 5, + MinTimeout: 10 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.Principal); ok { + return output, err + } + + return nil, err +} + +func PrincipalPortfolioAssociationDeleted(conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusAvailable}, + Target: []string{StatusNotFound, StatusUnavailable}, + Refresh: PrincipalPortfolioAssociationStatus(conn, acceptLanguage, principalARN, portfolioID), + Timeout: PrincipalPortfolioAssociationDeleteTimeout, + NotFoundChecks: 1, + } + + _, err := stateConf.WaitForState() + + return err +} diff --git a/aws/provider.go b/aws/provider.go index 8cdb9f54066..4a1bd2fbeaa 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -1040,6 +1040,7 @@ func Provider() *schema.Provider { "aws_servicecatalog_service_action": resourceAwsServiceCatalogServiceAction(), "aws_servicecatalog_tag_option": resourceAwsServiceCatalogTagOption(), "aws_servicecatalog_tag_option_resource_association": resourceAwsServiceCatalogTagOptionResourceAssociation(), + "aws_servicecatalog_principal_portfolio_association": resourceAwsServiceCatalogPrincipalPortfolioAssociation(), "aws_servicecatalog_product_portfolio_association": resourceAwsServiceCatalogProductPortfolioAssociation(), "aws_servicecatalog_provisioning_artifact": resourceAwsServiceCatalogProvisioningArtifact(), "aws_service_discovery_http_namespace": resourceAwsServiceDiscoveryHttpNamespace(), diff --git a/aws/resource_aws_servicecatalog_constraint.go b/aws/resource_aws_servicecatalog_constraint.go index 6bc38d756ed..730a57020da 100644 --- a/aws/resource_aws_servicecatalog_constraint.go +++ b/aws/resource_aws_servicecatalog_constraint.go @@ -30,7 +30,7 @@ func resourceAwsServiceCatalogConstraint() *schema.Resource { "accept_language": { Type: schema.TypeString, Optional: true, - Default: "en", + Default: tfservicecatalog.AcceptLanguageEnglish, ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), }, "description": { @@ -151,7 +151,7 @@ func resourceAwsServiceCatalogConstraintRead(d *schema.ResourceData, meta interf acceptLanguage := d.Get("accept_language").(string) if acceptLanguage == "" { - acceptLanguage = "en" + acceptLanguage = tfservicecatalog.AcceptLanguageEnglish } d.Set("accept_language", acceptLanguage) diff --git a/aws/resource_aws_servicecatalog_constraint_test.go b/aws/resource_aws_servicecatalog_constraint_test.go index 7d4d4179999..135b8ff70fe 100644 --- a/aws/resource_aws_servicecatalog_constraint_test.go +++ b/aws/resource_aws_servicecatalog_constraint_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" ) // add sweeper to delete known test servicecat constraints @@ -106,7 +107,7 @@ func TestAccAWSServiceCatalogConstraint_basic(t *testing.T) { Config: testAccAWSServiceCatalogConstraintConfig_basic(rName, rName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsServiceCatalogConstraintExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "accept_language", "en"), + resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish), resource.TestCheckResourceAttr(resourceName, "description", rName), resource.TestCheckResourceAttr(resourceName, "type", "NOTIFICATION"), resource.TestCheckResourceAttrPair(resourceName, "portfolio_id", "aws_servicecatalog_portfolio.test", "id"), diff --git a/aws/resource_aws_servicecatalog_portfolio.go b/aws/resource_aws_servicecatalog_portfolio.go index 4dce2bcb5b2..cff401e65e7 100644 --- a/aws/resource_aws_servicecatalog_portfolio.go +++ b/aws/resource_aws_servicecatalog_portfolio.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" ) func resourceAwsServiceCatalogPortfolio() *schema.Resource { @@ -68,7 +69,7 @@ func resourceAwsServiceCatalogPortfolioCreate(d *schema.ResourceData, meta inter defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) input := servicecatalog.CreatePortfolioInput{ - AcceptLanguage: aws.String("en"), + AcceptLanguage: aws.String(tfservicecatalog.AcceptLanguageEnglish), DisplayName: aws.String(d.Get("name").(string)), IdempotencyToken: aws.String(resource.UniqueId()), Tags: tags.IgnoreAws().ServicecatalogTags(), @@ -98,7 +99,7 @@ func resourceAwsServiceCatalogPortfolioRead(d *schema.ResourceData, meta interfa ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig input := servicecatalog.DescribePortfolioInput{ - AcceptLanguage: aws.String("en"), + AcceptLanguage: aws.String(tfservicecatalog.AcceptLanguageEnglish), } input.Id = aws.String(d.Id()) @@ -138,7 +139,7 @@ func resourceAwsServiceCatalogPortfolioRead(d *schema.ResourceData, meta interfa func resourceAwsServiceCatalogPortfolioUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).scconn input := servicecatalog.UpdatePortfolioInput{ - AcceptLanguage: aws.String("en"), + AcceptLanguage: aws.String(tfservicecatalog.AcceptLanguageEnglish), Id: aws.String(d.Id()), } diff --git a/aws/resource_aws_servicecatalog_portfolio_share.go b/aws/resource_aws_servicecatalog_portfolio_share.go index 2f12c649afc..97375e835aa 100644 --- a/aws/resource_aws_servicecatalog_portfolio_share.go +++ b/aws/resource_aws_servicecatalog_portfolio_share.go @@ -32,7 +32,7 @@ func resourceAwsServiceCatalogPortfolioShare() *schema.Resource { "accept_language": { Type: schema.TypeString, Optional: true, - Default: "en", + Default: tfservicecatalog.AcceptLanguageEnglish, ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), }, "accepted": { diff --git a/aws/resource_aws_servicecatalog_portfolio_share_test.go b/aws/resource_aws_servicecatalog_portfolio_share_test.go index 62afd2a22d8..e4dd573a85a 100644 --- a/aws/resource_aws_servicecatalog_portfolio_share_test.go +++ b/aws/resource_aws_servicecatalog_portfolio_share_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog/finder" ) @@ -34,7 +35,7 @@ func TestAccAWSServiceCatalogPortfolioShare_basic(t *testing.T) { Config: testAccAWSServiceCatalogPortfolioShareConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsServiceCatalogPortfolioShareExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "accept_language", "en"), + resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish), resource.TestCheckResourceAttr(resourceName, "accepted", "false"), resource.TestCheckResourceAttrPair(resourceName, "principal_id", dataSourceName, "account_id"), resource.TestCheckResourceAttrPair(resourceName, "portfolio_id", compareName, "id"), @@ -74,7 +75,7 @@ func TestAccAWSServiceCatalogPortfolioShare_organizationalUnit(t *testing.T) { Config: testAccAWSServiceCatalogPortfolioShareConfig_organizationalUnit(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsServiceCatalogPortfolioShareExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "accept_language", "en"), + resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish), resource.TestCheckResourceAttr(resourceName, "accepted", "true"), resource.TestCheckResourceAttrPair(resourceName, "principal_id", "aws_organizations_organizational_unit.test", "id"), resource.TestCheckResourceAttrPair(resourceName, "portfolio_id", compareName, "id"), diff --git a/aws/resource_aws_servicecatalog_principal_portfolio_association.go b/aws/resource_aws_servicecatalog_principal_portfolio_association.go new file mode 100644 index 00000000000..3099cbef5ee --- /dev/null +++ b/aws/resource_aws_servicecatalog_principal_portfolio_association.go @@ -0,0 +1,184 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsServiceCatalogPrincipalPortfolioAssociation() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsServiceCatalogPrincipalPortfolioAssociationCreate, + Read: resourceAwsServiceCatalogPrincipalPortfolioAssociationRead, + Delete: resourceAwsServiceCatalogPrincipalPortfolioAssociationDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "accept_language": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: tfservicecatalog.AcceptLanguageEnglish, + ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), + }, + "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, + ValidateFunc: validation.StringInSlice(servicecatalog.PrincipalType_Values(), false), + }, + }, + } +} + +func resourceAwsServiceCatalogPrincipalPortfolioAssociationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + + input := &servicecatalog.AssociatePrincipalWithPortfolioInput{ + PortfolioId: aws.String(d.Get("portfolio_id").(string)), + PrincipalARN: aws.String(d.Get("principal_arn").(string)), + } + + if v, ok := d.GetOk("accept_language"); ok { + input.AcceptLanguage = aws.String(v.(string)) + } + + if v, ok := d.GetOk("principal_type"); ok { + input.PrincipalType = aws.String(v.(string)) + } + + var output *servicecatalog.AssociatePrincipalWithPortfolioOutput + err := resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { + var err error + + output, err = conn.AssociatePrincipalWithPortfolio(input) + + if tfawserr.ErrMessageContains(err, servicecatalog.ErrCodeInvalidParametersException, "profile does not exist") { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + output, err = conn.AssociatePrincipalWithPortfolio(input) + } + + if err != nil { + return fmt.Errorf("error associating Service Catalog Principal with Portfolio: %w", err) + } + + if output == nil { + return fmt.Errorf("error creating Service Catalog Principal Portfolio Association: empty response") + } + + d.SetId(tfservicecatalog.PrincipalPortfolioAssociationID(d.Get("accept_language").(string), d.Get("principal_arn").(string), d.Get("portfolio_id").(string))) + + return resourceAwsServiceCatalogPrincipalPortfolioAssociationRead(d, meta) +} + +func resourceAwsServiceCatalogPrincipalPortfolioAssociationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + + acceptLanguage, principalARN, portfolioID, err := tfservicecatalog.PrincipalPortfolioAssociationParseID(d.Id()) + + if err != nil { + return fmt.Errorf("could not parse ID (%s): %w", d.Id(), err) + } + + if acceptLanguage == "" { + acceptLanguage = tfservicecatalog.AcceptLanguageEnglish + } + + output, err := waiter.PrincipalPortfolioAssociationReady(conn, acceptLanguage, principalARN, portfolioID) + + if !d.IsNewResource() && (tfresource.NotFound(err) || tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException)) { + log.Printf("[WARN] Service Catalog Principal Portfolio Association (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error describing Service Catalog Principal Portfolio Association (%s): %w", d.Id(), err) + } + + if output == nil { + return fmt.Errorf("error getting Service Catalog Principal Portfolio Association (%s): empty response", d.Id()) + } + + d.Set("accept_language", acceptLanguage) + d.Set("portfolio_id", portfolioID) + d.Set("principal_arn", output.PrincipalARN) + d.Set("principal_type", output.PrincipalType) + + return nil +} + +func resourceAwsServiceCatalogPrincipalPortfolioAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + + acceptLanguage, principalARN, portfolioID, err := tfservicecatalog.PrincipalPortfolioAssociationParseID(d.Id()) + + if err != nil { + return fmt.Errorf("could not parse ID (%s): %w", d.Id(), err) + } + + if acceptLanguage == "" { + acceptLanguage = tfservicecatalog.AcceptLanguageEnglish + } + + input := &servicecatalog.DisassociatePrincipalFromPortfolioInput{ + PortfolioId: aws.String(portfolioID), + PrincipalARN: aws.String(principalARN), + AcceptLanguage: aws.String(acceptLanguage), + } + + _, err = conn.DisassociatePrincipalFromPortfolio(input) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error disassociating Service Catalog Principal from Portfolio (%s): %w", d.Id(), err) + } + + err = waiter.PrincipalPortfolioAssociationDeleted(conn, acceptLanguage, principalARN, portfolioID) + + if tfresource.NotFound(err) || tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error waiting for Service Catalog Principal Portfolio Disassociation (%s): %w", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_servicecatalog_principal_portfolio_association_test.go b/aws/resource_aws_servicecatalog_principal_portfolio_association_test.go new file mode 100644 index 00000000000..2d2737c0e0e --- /dev/null +++ b/aws/resource_aws_servicecatalog_principal_portfolio_association_test.go @@ -0,0 +1,238 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +// add sweeper to delete known test servicecat principal portfolio associations +func init() { + resource.AddTestSweepers("aws_servicecatalog_principal_portfolio_association", &resource.Sweeper{ + Name: "aws_servicecatalog_principal_portfolio_association", + Dependencies: []string{}, + F: testSweepServiceCatalogPrincipalPortfolioAssociations, + }) +} + +func testSweepServiceCatalogPrincipalPortfolioAssociations(region string) error { + client, err := sharedClientForRegion(region) + + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + + conn := client.(*AWSClient).scconn + sweepResources := make([]*testSweepResource, 0) + var errs *multierror.Error + + input := &servicecatalog.ListPortfoliosInput{} + + err = conn.ListPortfoliosPages(input, func(page *servicecatalog.ListPortfoliosOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, detail := range page.PortfolioDetails { + if detail == nil { + continue + } + + pInput := &servicecatalog.ListPrincipalsForPortfolioInput{ + PortfolioId: detail.Id, + } + + err = conn.ListPrincipalsForPortfolioPages(pInput, func(page *servicecatalog.ListPrincipalsForPortfolioOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, principal := range page.Principals { + if principal == nil { + continue + } + + r := resourceAwsServiceCatalogPrincipalPortfolioAssociation() + d := r.Data(nil) + d.SetId(tfservicecatalog.PrincipalPortfolioAssociationID(tfservicecatalog.AcceptLanguageEnglish, aws.StringValue(principal.PrincipalARN), aws.StringValue(detail.Id))) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error listing Service Catalog Portfolios for Principals %s: %w", region, err)) + continue + } + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error describing Service Catalog Principal Portfolio Associations for %s: %w", region, err)) + } + + if err = testSweepResourceOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping Service Catalog Principal Portfolio Associations for %s: %w", region, err)) + } + + if testSweepSkipSweepError(errs.ErrorOrNil()) { + log.Printf("[WARN] Skipping Service Catalog Principal Portfolio Associations sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} + +func TestAccAWSServiceCatalogPrincipalPortfolioAssociation_basic(t *testing.T) { + resourceName := "aws_servicecatalog_principal_portfolio_association.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, servicecatalog.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsServiceCatalogPrincipalPortfolioAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSServiceCatalogPrincipalPortfolioAssociationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsServiceCatalogPrincipalPortfolioAssociationExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "portfolio_id", "aws_servicecatalog_portfolio.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "principal_arn", "aws_iam_role.test", "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSServiceCatalogPrincipalPortfolioAssociation_disappears(t *testing.T) { + resourceName := "aws_servicecatalog_principal_portfolio_association.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, servicecatalog.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsServiceCatalogPrincipalPortfolioAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSServiceCatalogPrincipalPortfolioAssociationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsServiceCatalogPrincipalPortfolioAssociationExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsServiceCatalogPrincipalPortfolioAssociation(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAwsServiceCatalogPrincipalPortfolioAssociationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).scconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_servicecatalog_principal_portfolio_association" { + continue + } + + acceptLanguage, principalARN, portfolioID, err := tfservicecatalog.PrincipalPortfolioAssociationParseID(rs.Primary.ID) + + if err != nil { + return fmt.Errorf("could not parse ID (%s): %w", rs.Primary.ID, err) + } + + err = waiter.PrincipalPortfolioAssociationDeleted(conn, acceptLanguage, principalARN, portfolioID) + + if tfresource.NotFound(err) || tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return fmt.Errorf("waiting for Service Catalog Principal Portfolio Association to be destroyed (%s): %w", rs.Primary.ID, err) + } + } + + return nil +} + +func testAccCheckAwsServiceCatalogPrincipalPortfolioAssociationExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + acceptLanguage, principalARN, portfolioID, err := tfservicecatalog.PrincipalPortfolioAssociationParseID(rs.Primary.ID) + + if err != nil { + return fmt.Errorf("could not parse ID (%s): %w", rs.Primary.ID, err) + } + + conn := testAccProvider.Meta().(*AWSClient).scconn + + _, err = waiter.PrincipalPortfolioAssociationReady(conn, acceptLanguage, principalARN, portfolioID) + + if err != nil { + return fmt.Errorf("waiting for Service Catalog Principal Portfolio Association existence (%s): %w", rs.Primary.ID, err) + } + + return nil + } +} + +func testAccAWSServiceCatalogPrincipalPortfolioAssociationConfig_base(rName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = %[1]q + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "servicecatalog.${data.aws_partition.current.dns_suffix}" + } + Sid = "" + }] + }) +} + +resource "aws_servicecatalog_portfolio" "test" { + name = %[1]q + provider_name = %[1]q +} +`, rName) +} + +func testAccAWSServiceCatalogPrincipalPortfolioAssociationConfig_basic(rName string) string { + return composeConfig(testAccAWSServiceCatalogPrincipalPortfolioAssociationConfig_base(rName), ` +resource "aws_servicecatalog_principal_portfolio_association" "test" { + portfolio_id = aws_servicecatalog_portfolio.test.id + principal_arn = aws_iam_role.test.arn +} +`) +} diff --git a/aws/resource_aws_servicecatalog_product.go b/aws/resource_aws_servicecatalog_product.go index fe692427ef1..e73856735e2 100644 --- a/aws/resource_aws_servicecatalog_product.go +++ b/aws/resource_aws_servicecatalog_product.go @@ -36,7 +36,7 @@ func resourceAwsServiceCatalogProduct() *schema.Resource { "accept_language": { Type: schema.TypeString, Optional: true, - Default: "en", + Default: tfservicecatalog.AcceptLanguageEnglish, ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), }, "created_time": { diff --git a/aws/resource_aws_servicecatalog_product_portfolio_association.go b/aws/resource_aws_servicecatalog_product_portfolio_association.go index 74644d5b341..20522207ac2 100644 --- a/aws/resource_aws_servicecatalog_product_portfolio_association.go +++ b/aws/resource_aws_servicecatalog_product_portfolio_association.go @@ -30,7 +30,7 @@ func resourceAwsServiceCatalogProductPortfolioAssociation() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, - Default: "en", + Default: tfservicecatalog.AcceptLanguageEnglish, ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), }, "portfolio_id": { diff --git a/aws/resource_aws_servicecatalog_product_portfolio_association_test.go b/aws/resource_aws_servicecatalog_product_portfolio_association_test.go index f81472914ae..66843c5fbe4 100644 --- a/aws/resource_aws_servicecatalog_product_portfolio_association_test.go +++ b/aws/resource_aws_servicecatalog_product_portfolio_association_test.go @@ -86,7 +86,7 @@ func testSweepServiceCatalogProductPortfolioAssociations(region string) error { r := resourceAwsServiceCatalogProductPortfolioAssociation() d := r.Data(nil) - d.SetId(tfservicecatalog.ProductPortfolioAssociationCreateID("en", aws.StringValue(detail.Id), productID)) + d.SetId(tfservicecatalog.ProductPortfolioAssociationCreateID(tfservicecatalog.AcceptLanguageEnglish, aws.StringValue(detail.Id), productID)) sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) } diff --git a/aws/resource_aws_servicecatalog_product_test.go b/aws/resource_aws_servicecatalog_product_test.go index bd7d153e6b1..f3ed7fa89ae 100644 --- a/aws/resource_aws_servicecatalog_product_test.go +++ b/aws/resource_aws_servicecatalog_product_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog/waiter" ) @@ -93,7 +94,7 @@ func TestAccAWSServiceCatalogProduct_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAwsServiceCatalogProductExists(resourceName), testAccMatchResourceAttrRegionalARN(resourceName, "arn", "catalog", regexp.MustCompile(`product/prod-.*`)), - resource.TestCheckResourceAttr(resourceName, "accept_language", "en"), + resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish), testAccCheckResourceAttrRfc3339(resourceName, "created_time"), resource.TestCheckResourceAttr(resourceName, "description", "beskrivning"), resource.TestCheckResourceAttr(resourceName, "distributor", "distributör"), diff --git a/aws/resource_aws_servicecatalog_provisioning_artifact.go b/aws/resource_aws_servicecatalog_provisioning_artifact.go index b767586f25d..cdd73138c82 100644 --- a/aws/resource_aws_servicecatalog_provisioning_artifact.go +++ b/aws/resource_aws_servicecatalog_provisioning_artifact.go @@ -31,7 +31,7 @@ func resourceAwsServiceCatalogProvisioningArtifact() *schema.Resource { "accept_language": { Type: schema.TypeString, Optional: true, - Default: "en", + Default: tfservicecatalog.AcceptLanguageEnglish, ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), }, "active": { diff --git a/aws/resource_aws_servicecatalog_provisioning_artifact_test.go b/aws/resource_aws_servicecatalog_provisioning_artifact_test.go index 008ef7b58d3..05ab38d337f 100644 --- a/aws/resource_aws_servicecatalog_provisioning_artifact_test.go +++ b/aws/resource_aws_servicecatalog_provisioning_artifact_test.go @@ -119,7 +119,7 @@ func TestAccAWSServiceCatalogProvisioningArtifact_basic(t *testing.T) { Config: testAccAWSServiceCatalogProvisioningArtifactConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsServiceCatalogProvisioningArtifactExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "accept_language", "en"), + resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish), resource.TestCheckResourceAttr(resourceName, "active", "true"), resource.TestCheckResourceAttr(resourceName, "description", rName), resource.TestCheckResourceAttr(resourceName, "disable_template_validation", "true"), @@ -181,7 +181,7 @@ func TestAccAWSServiceCatalogProvisioningArtifact_update(t *testing.T) { Config: testAccAWSServiceCatalogProvisioningArtifactConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsServiceCatalogProvisioningArtifactExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "accept_language", "en"), + resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish), resource.TestCheckResourceAttr(resourceName, "active", "true"), resource.TestCheckResourceAttr(resourceName, "description", rName), resource.TestCheckResourceAttr(resourceName, "guidance", servicecatalog.ProvisioningArtifactGuidanceDefault), @@ -226,7 +226,7 @@ func TestAccAWSServiceCatalogProvisioningArtifact_physicalID(t *testing.T) { Config: testAccAWSServiceCatalogProvisioningArtifactConfig_physicalID(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsServiceCatalogProvisioningArtifactExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "accept_language", "en"), + resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish), resource.TestCheckResourceAttr(resourceName, "active", "true"), resource.TestCheckResourceAttr(resourceName, "description", rName), resource.TestCheckResourceAttr(resourceName, "disable_template_validation", "false"), diff --git a/aws/resource_aws_servicecatalog_service_action.go b/aws/resource_aws_servicecatalog_service_action.go index 349cf26cabc..319119e0a6b 100644 --- a/aws/resource_aws_servicecatalog_service_action.go +++ b/aws/resource_aws_servicecatalog_service_action.go @@ -30,7 +30,7 @@ func resourceAwsServiceCatalogServiceAction() *schema.Resource { "accept_language": { Type: schema.TypeString, Optional: true, - Default: "en", + Default: tfservicecatalog.AcceptLanguageEnglish, ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), }, "definition": { diff --git a/aws/resource_aws_servicecatalog_service_action_test.go b/aws/resource_aws_servicecatalog_service_action_test.go index b3a798b5a3b..63adb8775b1 100644 --- a/aws/resource_aws_servicecatalog_service_action_test.go +++ b/aws/resource_aws_servicecatalog_service_action_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" ) // add sweeper to delete known test servicecat service actions @@ -88,7 +89,7 @@ func TestAccAWSServiceCatalogServiceAction_basic(t *testing.T) { Config: testAccAWSServiceCatalogServiceActionConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsServiceCatalogServiceActionExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "accept_language", "en"), + resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish), resource.TestCheckResourceAttr(resourceName, "definition.0.name", "AWS-RestartEC2Instance"), resource.TestCheckResourceAttr(resourceName, "definition.0.version", "1"), resource.TestCheckResourceAttr(resourceName, "description", rName), @@ -144,7 +145,7 @@ func TestAccAWSServiceCatalogServiceAction_update(t *testing.T) { Config: testAccAWSServiceCatalogServiceActionConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsServiceCatalogServiceActionExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "accept_language", "en"), + resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish), resource.TestCheckResourceAttr(resourceName, "definition.0.name", "AWS-RestartEC2Instance"), resource.TestCheckResourceAttr(resourceName, "definition.0.version", "1"), resource.TestCheckResourceAttr(resourceName, "description", rName), @@ -155,7 +156,7 @@ func TestAccAWSServiceCatalogServiceAction_update(t *testing.T) { Config: testAccAWSServiceCatalogServiceActionConfig_update(rName2), Check: resource.ComposeTestCheckFunc( testAccCheckAwsServiceCatalogServiceActionExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "accept_language", "en"), + resource.TestCheckResourceAttr(resourceName, "accept_language", tfservicecatalog.AcceptLanguageEnglish), resource.TestCheckResourceAttrPair(resourceName, "definition.0.assume_role", "aws_iam_role.test", "arn"), resource.TestCheckResourceAttr(resourceName, "description", rName2), resource.TestCheckResourceAttr(resourceName, "name", rName2), diff --git a/website/docs/r/servicecatalog_principal_portfolio_association.html.markdown b/website/docs/r/servicecatalog_principal_portfolio_association.html.markdown new file mode 100644 index 00000000000..90c53712350 --- /dev/null +++ b/website/docs/r/servicecatalog_principal_portfolio_association.html.markdown @@ -0,0 +1,48 @@ +--- +subcategory: "Service Catalog" +layout: "aws" +page_title: "AWS: aws_servicecatalog_principal_portfolio_association" +description: |- + Manages a Service Catalog Principal Portfolio Association +--- + +# Resource: aws_servicecatalog_principal_portfolio_association + +Manages a Service Catalog Principal Portfolio Association. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_servicecatalog_principal_portfolio_association" "example" { + portfolio_id = "port-68656c6c6f" + principal_arn = "arn:aws:iam::123456789012:user/Eleanor" +} +``` + +## Argument Reference + +The following arguments are required: + +* `portfolio_id` - (Required) Portfolio identifier. +* `principal_arn` - (Required) Principal ARN. + +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`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Identifier of the association. + +## Import + +`aws_servicecatalog_principal_portfolio_association` can be imported using the accept language, principal ARN, and portfolio ID, separated by a comma, e.g. + +``` +$ terraform import aws_servicecatalog_principal_portfolio_association.example en,arn:aws:iam::123456789012:user/Eleanor,port-68656c6c6f +```