diff --git a/.changelog/19316.txt b/.changelog/19316.txt new file mode 100644 index 00000000000..e29d6221bf7 --- /dev/null +++ b/.changelog/19316.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_servicecatalog_provisioning_artifact +``` diff --git a/aws/internal/service/servicecatalog/id.go b/aws/internal/service/servicecatalog/id.go index 14bfac365e6..03a7f4ef7fe 100644 --- a/aws/internal/service/servicecatalog/id.go +++ b/aws/internal/service/servicecatalog/id.go @@ -60,3 +60,16 @@ func TagOptionResourceAssociationParseID(id string) (string, string, error) { func TagOptionResourceAssociationID(tagOptionID, resourceID string) string { return strings.Join([]string{tagOptionID, resourceID}, ":") } + +func ProvisioningArtifactID(artifactID, productID string) string { + return strings.Join([]string{artifactID, productID}, ":") +} + +func ProvisioningArtifactParseID(id string) (string, string, error) { + parts := strings.SplitN(id, ":", 2) + + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%s), expected artifactID:productID", id) + } + return parts[0], parts[1], nil +} diff --git a/aws/internal/service/servicecatalog/waiter/status.go b/aws/internal/service/servicecatalog/waiter/status.go index 2ea72602cc7..371aa8c8aa5 100644 --- a/aws/internal/service/servicecatalog/waiter/status.go +++ b/aws/internal/service/servicecatalog/waiter/status.go @@ -274,3 +274,28 @@ func TagOptionResourceAssociationStatus(conn *servicecatalog.ServiceCatalog, tag return output, servicecatalog.StatusAvailable, err } } + +func ProvisioningArtifactStatus(conn *servicecatalog.ServiceCatalog, id, productID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.DescribeProvisioningArtifactInput{ + ProvisioningArtifactId: aws.String(id), + ProductId: aws.String(productID), + } + + output, err := conn.DescribeProvisioningArtifact(input) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if err != nil { + return nil, servicecatalog.StatusFailed, err + } + + if output == nil || output.ProvisioningArtifactDetail == nil { + return nil, StatusUnavailable, err + } + + return output, aws.StringValue(output.Status), err + } +} diff --git a/aws/internal/service/servicecatalog/waiter/waiter.go b/aws/internal/service/servicecatalog/waiter/waiter.go index 3d8b4293b7c..8374a859dbd 100644 --- a/aws/internal/service/servicecatalog/waiter/waiter.go +++ b/aws/internal/service/servicecatalog/waiter/waiter.go @@ -35,11 +35,14 @@ const ( TagOptionResourceAssociationReadyTimeout = 3 * time.Minute TagOptionResourceAssociationDeleteTimeout = 3 * time.Minute + ProvisioningArtifactReadyTimeout = 3 * time.Minute + ProvisioningArtifactDeletedTimeout = 3 * time.Minute + StatusNotFound = "NOT_FOUND" StatusUnavailable = "UNAVAILABLE" // AWS documentation is wrong, says that status will be "AVAILABLE" but it is actually "CREATED" - ProductStatusCreated = "CREATED" + StatusCreated = "CREATED" OrganizationAccessStatusError = "ERROR" ) @@ -47,7 +50,7 @@ const ( func ProductReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, productID string) (*servicecatalog.DescribeProductAsAdminOutput, error) { stateConf := &resource.StateChangeConf{ Pending: []string{servicecatalog.StatusCreating, StatusNotFound, StatusUnavailable}, - Target: []string{servicecatalog.StatusAvailable, ProductStatusCreated}, + Target: []string{servicecatalog.StatusAvailable, StatusCreated}, Refresh: ProductStatus(conn, acceptLanguage, productID), Timeout: ProductReadyTimeout, } @@ -63,7 +66,7 @@ func ProductReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, productID func ProductDeleted(conn *servicecatalog.ServiceCatalog, acceptLanguage, productID string) (*servicecatalog.DescribeProductAsAdminOutput, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{servicecatalog.StatusCreating, servicecatalog.StatusAvailable, ProductStatusCreated, StatusUnavailable}, + Pending: []string{servicecatalog.StatusCreating, servicecatalog.StatusAvailable, StatusCreated, StatusUnavailable}, Target: []string{StatusNotFound}, Refresh: ProductStatus(conn, acceptLanguage, productID), Timeout: ProductDeleteTimeout, @@ -366,3 +369,41 @@ func TagOptionResourceAssociationDeleted(conn *servicecatalog.ServiceCatalog, ta return err } + +func ProvisioningArtifactReady(conn *servicecatalog.ServiceCatalog, id, productID string) (*servicecatalog.DescribeProvisioningArtifactOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusCreating, StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.StatusAvailable, StatusCreated}, + Refresh: ProvisioningArtifactStatus(conn, id, productID), + Timeout: ProvisioningArtifactReadyTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.DescribeProvisioningArtifactOutput); ok { + return output, err + } + + return nil, err +} + +func ProvisioningArtifactDeleted(conn *servicecatalog.ServiceCatalog, id, productID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusCreating, servicecatalog.StatusAvailable, StatusCreated, StatusUnavailable}, + Target: []string{StatusNotFound}, + Refresh: ProvisioningArtifactStatus(conn, id, productID), + Timeout: ProvisioningArtifactDeletedTimeout, + } + + _, err := stateConf.WaitForState() + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return err + } + + return nil +} diff --git a/aws/provider.go b/aws/provider.go index ffb54b872c7..b0cc423bedc 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -1031,6 +1031,7 @@ func Provider() *schema.Provider { "aws_servicecatalog_tag_option": resourceAwsServiceCatalogTagOption(), "aws_servicecatalog_tag_option_resource_association": resourceAwsServiceCatalogTagOptionResourceAssociation(), "aws_servicecatalog_product_portfolio_association": resourceAwsServiceCatalogProductPortfolioAssociation(), + "aws_servicecatalog_provisioning_artifact": resourceAwsServiceCatalogProvisioningArtifact(), "aws_service_discovery_http_namespace": resourceAwsServiceDiscoveryHttpNamespace(), "aws_service_discovery_private_dns_namespace": resourceAwsServiceDiscoveryPrivateDnsNamespace(), "aws_service_discovery_public_dns_namespace": resourceAwsServiceDiscoveryPublicDnsNamespace(), diff --git a/aws/resource_aws_servicecatalog_constraint_test.go b/aws/resource_aws_servicecatalog_constraint_test.go index 84237028eaa..7d4d4179999 100644 --- a/aws/resource_aws_servicecatalog_constraint_test.go +++ b/aws/resource_aws_servicecatalog_constraint_test.go @@ -240,31 +240,27 @@ resource "aws_s3_bucket_object" "test" { bucket = aws_s3_bucket.test.id key = "%[1]s.json" - content = < A "provisioning artifact" is also referred to as a "version." + +~> **NOTE:** You cannot create a provisioning artifact for a product that was shared with you. + +~> **NOTE:** The user or role that use this resource must have the `cloudformation:GetTemplate` IAM policy permission. This policy permission is required when using the `template_physical_id` argument. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_servicecatalog_provisioning_artifact" "example" { + name = "example" + product_id = aws_servicecatalog_product.example.id + type = "CLOUD_FORMATION_TEMPLATE" + template_url = "https://${aws_s3_bucket.example.bucket_regional_domain_name}/${aws_s3_bucket_object.example.key}" +} +``` + +## Argument Reference + +The following arguments are required: + +* `product_id` - (Required) Identifier of the product. +* `template_physical_id` - (Required if `template_url` is not provided) Template source as the physical ID of the resource that contains the template. Currently only supports CloudFormation stack ARN. Specify the physical ID as `arn:[partition]:cloudformation:[region]:[account ID]:stack/[stack name]/[resource ID]`. +* `template_url` - (Required if `template_physical_id` is not provided) Template source as URL of the CloudFormation template in Amazon S3. + +The following arguments are optional: + +* `accept_language` - (Optional) Language code. Valid values: `en` (English), `jp` (Japanese), `zh` (Chinese). The default value is `en`. +* `active` - (Optional) Whether the product version is active. Inactive provisioning artifacts are invisible to end users. End users cannot launch or update a provisioned product from an inactive provisioning artifact. Default is `true`. +* `description` - (Optional) Description of the provisioning artifact (i.e., version), including how it differs from the previous provisioning artifact. +* `disable_template_validation` - (Optional) Whether AWS Service Catalog stops validating the specified provisioning artifact template even if it is invalid. +* `guidance` - (Optional) Information set by the administrator to provide guidance to end users about which provisioning artifacts to use. Valid values are `DEFAULT` and `DEPRECATED`. The default is `DEFAULT`. Users are able to make updates to a provisioned product of a deprecated version but cannot launch new provisioned products using a deprecated version. +* `name` - (Optional) Name of the provisioning artifact (for example, `v1`, `v2beta`). No spaces are allowed. +* `type` - (Optional) Type of provisioning artifact. Valid values: `CLOUD_FORMATION_TEMPLATE`, `MARKETPLACE_AMI`, `MARKETPLACE_CAR` (Marketplace Clusters and AWS Resources). + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `created_time` - Time when the provisioning artifact was created. +* `id` - Provisioning Artifact identifier and product identifier separated by a colon. +* `status` - Status of the provisioning artifact. + +## Import + +`aws_servicecatalog_provisioning_artifact` can be imported using the provisioning artifact ID and product ID separated by a colon, e.g. + +``` +$ terraform import aws_servicecatalog_provisioning_artifact.example pa-ij2b6lusy6dec:prod-el3an0rma3 +```