diff --git a/.changelog/19813.txt b/.changelog/19813.txt new file mode 100644 index 00000000000..af1420e8b70 --- /dev/null +++ b/.changelog/19813.txt @@ -0,0 +1,3 @@ +```release-notes:new-data-source +aws_servicecatalog_portfolio_constraints +``` \ 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 a8b29de52c5..4277b4c1771 100644 --- a/aws/data_source_aws_servicecatalog_constraint.go +++ b/aws/data_source_aws_servicecatalog_constraint.go @@ -71,7 +71,13 @@ func dataSourceAwsServiceCatalogConstraintRead(d *schema.ResourceData, meta inte return fmt.Errorf("error getting Service Catalog Constraint: empty response") } - d.Set("accept_language", d.Get("accept_language").(string)) + acceptLanguage := d.Get("accept_language").(string) + + if acceptLanguage == "" { + acceptLanguage = tfservicecatalog.AcceptLanguageEnglish + } + + d.Set("accept_language", acceptLanguage) d.Set("parameters", output.ConstraintParameters) d.Set("status", output.Status) diff --git a/aws/data_source_aws_servicecatalog_portfolio_constraints.go b/aws/data_source_aws_servicecatalog_portfolio_constraints.go new file mode 100644 index 00000000000..0b89f506851 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_portfolio_constraints.go @@ -0,0 +1,151 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog/waiter" +) + +func dataSourceAwsServiceCatalogPortfolioConstraints() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsServiceCatalogPortfolioConstraintsRead, + + Schema: map[string]*schema.Schema{ + "accept_language": { + Type: schema.TypeString, + Optional: true, + Default: tfservicecatalog.AcceptLanguageEnglish, + ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), + }, + "details": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "constraint_id": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "portfolio_id": { + Type: schema.TypeString, + Computed: true, + }, + "product_id": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "portfolio_id": { + Type: schema.TypeString, + Required: true, + }, + "product_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func dataSourceAwsServiceCatalogPortfolioConstraintsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + + output, err := waiter.PortfolioConstraintsReady(conn, d.Get("accept_language").(string), d.Get("portfolio_id").(string), d.Get("product_id").(string)) + + if err != nil { + return fmt.Errorf("error describing Service Catalog Portfolio Constraints: %w", err) + } + + if len(output) == 0 { + return fmt.Errorf("error getting Service Catalog Portfolio Constraints: no results, change your input") + } + + acceptLanguage := d.Get("accept_language").(string) + + if acceptLanguage == "" { + acceptLanguage = tfservicecatalog.AcceptLanguageEnglish + } + + d.Set("accept_language", acceptLanguage) + d.Set("portfolio_id", d.Get("portfolio_id").(string)) + d.Set("product_id", d.Get("product_id").(string)) + + if err := d.Set("details", flattenServiceCatalogConstraintDetails(output)); err != nil { + return fmt.Errorf("error setting details: %w", err) + } + + d.SetId(tfservicecatalog.PortfolioConstraintsID(d.Get("accept_language").(string), d.Get("portfolio_id").(string), d.Get("product_id").(string))) + + return nil +} + +func flattenServiceCatalogConstraintDetail(apiObject *servicecatalog.ConstraintDetail) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.ConstraintId; v != nil { + tfMap["constraint_id"] = aws.StringValue(v) + } + + if v := apiObject.Description; v != nil { + tfMap["description"] = aws.StringValue(v) + } + + if v := apiObject.Owner; v != nil { + tfMap["owner"] = aws.StringValue(v) + } + + if v := apiObject.PortfolioId; v != nil { + tfMap["portfolio_id"] = aws.StringValue(v) + } + + if v := apiObject.ProductId; v != nil { + tfMap["product_id"] = aws.StringValue(v) + } + + if v := apiObject.Type; v != nil { + tfMap["type"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenServiceCatalogConstraintDetails(apiObjects []*servicecatalog.ConstraintDetail) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenServiceCatalogConstraintDetail(apiObject)) + } + + return tfList +} diff --git a/aws/data_source_aws_servicecatalog_portfolio_constraints_test.go b/aws/data_source_aws_servicecatalog_portfolio_constraints_test.go new file mode 100644 index 00000000000..c63eda58851 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_portfolio_constraints_test.go @@ -0,0 +1,45 @@ +package aws + +import ( + "testing" + + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSServiceCatalogPortfolioConstraintDataSource_basic(t *testing.T) { + resourceName := "aws_servicecatalog_constraint.test" + dataSourceName := "data.aws_servicecatalog_portfolio_constraints.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, servicecatalog.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAWSServiceCatalogPortfolioConstraintDataSourceConfig_basic(rName, rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "accept_language", resourceName, "accept_language"), + resource.TestCheckResourceAttr(dataSourceName, "details.#", "1"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.constraint_id", resourceName, "id"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.description", resourceName, "description"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.owner", resourceName, "owner"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.portfolio_id", resourceName, "portfolio_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.product_id", resourceName, "product_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.type", resourceName, "type"), + resource.TestCheckResourceAttrPair(dataSourceName, "portfolio_id", resourceName, "portfolio_id"), + ), + }, + }, + }) +} + +func testAccAWSServiceCatalogPortfolioConstraintDataSourceConfig_basic(rName, description string) string { + return composeConfig(testAccAWSServiceCatalogConstraintConfig_basic(rName, description), ` +data "aws_servicecatalog_portfolio_constraints" "test" { + portfolio_id = aws_servicecatalog_constraint.test.portfolio_id +} +`) +} diff --git a/aws/internal/service/servicecatalog/id.go b/aws/internal/service/servicecatalog/id.go index 3eb0a6f8a41..a15f1a759a2 100644 --- a/aws/internal/service/servicecatalog/id.go +++ b/aws/internal/service/servicecatalog/id.go @@ -87,3 +87,7 @@ func PrincipalPortfolioAssociationParseID(id string) (string, string, string, er 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/aws/internal/service/servicecatalog/waiter/status.go b/aws/internal/service/servicecatalog/waiter/status.go index d1f39a9489a..7f4dfe87241 100644 --- a/aws/internal/service/servicecatalog/waiter/status.go +++ b/aws/internal/service/servicecatalog/waiter/status.go @@ -417,3 +417,47 @@ func RecordStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string return output, aws.StringValue(output.RecordDetail.Status), err } } + +func PortfolioConstraintsStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, portfolioID, productID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.ListConstraintsForPortfolioInput{ + PortfolioId: aws.String(portfolioID), + } + + if acceptLanguage != "" { + input.AcceptLanguage = aws.String(acceptLanguage) + } + + if productID != "" { + input.ProductId = aws.String(productID) + } + + var output []*servicecatalog.ConstraintDetail + + err := conn.ListConstraintsForPortfolioPages(input, func(page *servicecatalog.ListConstraintsForPortfolioOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, deet := range page.ConstraintDetails { + if deet == nil { + continue + } + + output = append(output, deet) + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, nil + } + + if err != nil { + return nil, servicecatalog.StatusFailed, 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 ea2cc78fd43..0e878168594 100644 --- a/aws/internal/service/servicecatalog/waiter/waiter.go +++ b/aws/internal/service/servicecatalog/waiter/waiter.go @@ -48,6 +48,8 @@ const ( RecordReadyTimeout = 3 * time.Minute + PortfolioConstraintsReadyTimeout = 3 * time.Minute + MinTimeout = 2 * time.Second NotFoundChecks = 5 ContinuousTargetOccurrence = 2 @@ -540,3 +542,20 @@ func RecordReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string) return nil, err } + +func PortfolioConstraintsReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, portfolioID, productID string) ([]*servicecatalog.ConstraintDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: PortfolioConstraintsStatus(conn, acceptLanguage, portfolioID, productID), + Timeout: PortfolioConstraintsReadyTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.([]*servicecatalog.ConstraintDetail); ok { + return output, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index e71d04dfcf1..f8c6a4179ed 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -393,6 +393,7 @@ func Provider() *schema.Provider { "aws_secretsmanager_secret_version": dataSourceAwsSecretsManagerSecretVersion(), "aws_servicecatalog_constraint": dataSourceAwsServiceCatalogConstraint(), "aws_servicecatalog_launch_paths": dataSourceAwsServiceCatalogLaunchPaths(), + "aws_servicecatalog_portfolio_constraints": dataSourceAwsServiceCatalogPortfolioConstraints(), "aws_servicecatalog_portfolio": dataSourceAwsServiceCatalogPortfolio(), "aws_servicecatalog_product": dataSourceAwsServiceCatalogProduct(), "aws_servicequotas_service": dataSourceAwsServiceQuotasService(), diff --git a/aws/resource_aws_servicecatalog_constraint.go b/aws/resource_aws_servicecatalog_constraint.go index 730a57020da..8a9f3d46763 100644 --- a/aws/resource_aws_servicecatalog_constraint.go +++ b/aws/resource_aws_servicecatalog_constraint.go @@ -144,7 +144,7 @@ func resourceAwsServiceCatalogConstraintRead(d *schema.ResourceData, meta interf return fmt.Errorf("error describing Service Catalog Constraint (%s): %w", d.Id(), err) } - if output == nil { + if output == nil || output.ConstraintDetail == nil { return fmt.Errorf("error getting Service Catalog Constraint (%s): empty response", d.Id()) } diff --git a/website/docs/d/servicecatalog_portfolio_constraints.html.markdown b/website/docs/d/servicecatalog_portfolio_constraints.html.markdown new file mode 100644 index 00000000000..176fdfcf01b --- /dev/null +++ b/website/docs/d/servicecatalog_portfolio_constraints.html.markdown @@ -0,0 +1,46 @@ +--- +subcategory: "Service Catalog" +layout: "aws" +page_title: "AWS: aws_servicecatalog_portfolio_constraints" +description: |- + Provides information on Service Catalog Portfolio Constraints +--- + +# Data source: aws_servicecatalog_portfolio_constraints + +Provides information on Service Catalog Portfolio Constraints. + +## Example Usage + +### Basic Usage + +```terraform +data "aws_servicecatalog_portfolio_constraints" "example" { + portfolio_id = "port-3lli3b3an" +} +``` + +## Argument Reference + +The following arguments are required: + +* `portfolio_id` - (Required) Portfolio identifier. + +The following arguments are optional: + +* `accept_language` - (Optional) Language code. Valid values: `en` (English), `jp` (Japanese), `zh` (Chinese). Default value is `en`. +* `product_id` - (Optional) Product identifier. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `details` - List of information about the constraints. See details below. + +### details + +* `constraint_id` - Identifier of the constraint. +* `description` - Description of the constraint. +* `portfolio_id` - Identifier of the portfolio the product resides in. The constraint applies only to the instance of the product that lives within this portfolio. +* `product_id` - Identifier of the product the constraint applies to. A constraint applies to a specific instance of a product within a certain portfolio. +* `type` - Type of constraint. Valid values are `LAUNCH`, `NOTIFICATION`, `STACKSET`, and `TEMPLATE`.