Repository: turbot/steampipe-plugin-aws (language: go)

Issue:
Add table aws_servicecatalog_provisioned_product
These are basically instantiated products.
**References**
List: https://docs.aws.amazon.com/servicecatalog/latest/dg/API_SearchProvisionedProducts.html
Get: https://docs.aws.amazon.com/servicecatalog/latest/dg/API_DescribeProvisionedProduct.html
TF: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/servicecatalog_provisioned_product

Warning: API_SearchProvisionedProducts and I bet the other search methods in this service return items in a random order if you don't provide a sort parameter. This will mess up pagination since the page token is actually an offset.


Discussion / hints:

Fix patch:
diff --git a/aws/plugin.go b/aws/plugin.go
index 4aebb8b6d..fd18f2a0f 100644
--- a/aws/plugin.go
+++ b/aws/plugin.go
@@ -412,11 +412,12 @@ func Plugin(ctx context.Context) *plugin.Plugin {
 			"aws_securitylake_data_lake":                                   tableAwsSecurityLakeDataLake(ctx),
 			"aws_securitylake_subscriber":                                  tableAwsSecurityLakeSubscriber(ctx),
 			"aws_serverlessapplicationrepository_application":              tableAwsServerlessApplicationRepositoryApplication(ctx),
+			"aws_servicecatalog_portfolio":                                 tableAwsServicecatalogPortfolio(ctx),
+			"aws_servicecatalog_product":                                   tableAwsServicecatalogProduct(ctx),
+			"aws_servicecatalog_provisioned_product":                       tableAwsServicecatalogProvisionedProduct(ctx),
 			"aws_service_discovery_instance":                               tableAwsServiceDiscoveryInstance(ctx),
 			"aws_service_discovery_namespace":                              tableAwsServiceDiscoveryNamespace(ctx),
 			"aws_service_discovery_service":                                tableAwsServiceDiscoveryService(ctx),
-			"aws_servicecatalog_portfolio":                                 tableAwsServicecatalogPortfolio(ctx),
-			"aws_servicecatalog_product":                                   tableAwsServicecatalogProduct(ctx),
 			"aws_servicequotas_default_service_quota":                      tableAwsServiceQuotasDefaultServiceQuota(ctx),
 			"aws_servicequotas_service_quota":                              tableAwsServiceQuotasServiceQuota(ctx),
 			"aws_servicequotas_service_quota_change_request":               tableAwsServiceQuotasServiceQuotaChangeRequest(ctx),
diff --git a/aws/table_aws_servicecatalog_provisioned_product.go b/aws/table_aws_servicecatalog_provisioned_product.go
new file mode 100644
index 000000000..5aae45a46
--- /dev/null
+++ b/aws/table_aws_servicecatalog_provisioned_product.go
@@ -0,0 +1,346 @@
+package aws
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/servicecatalog"
+	"github.com/aws/aws-sdk-go-v2/service/servicecatalog/types"
+
+	servicecatalogv1 "github.com/aws/aws-sdk-go/service/servicecatalog"
+
+	"github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto"
+	"github.com/turbot/steampipe-plugin-sdk/v5/plugin"
+	"github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform"
+)
+
+//// TABLE DEFINITION
+
+func tableAwsServicecatalogProvisionedProduct(_ context.Context) *plugin.Table {
+	return &plugin.Table{
+		Name:        "aws_servicecatalog_provisioned_product",
+		Description: "AWS Service Catalog Provisioned Product",
+		Get: &plugin.GetConfig{
+			KeyColumns: plugin.AnyColumn([]string{"id", "name"}),
+			IgnoreConfig: &plugin.IgnoreConfig{
+				ShouldIgnoreErrorFunc: shouldIgnoreErrors([]string{"ResourceNotFoundException"}),
+			},
+			Hydrate: getServiceCatalogProvisionedProduct,
+			Tags:    map[string]string{"service": "servicecatalog", "action": "DescribeProvisionedProduct"},
+		},
+		List: &plugin.ListConfig{
+			Hydrate: listServiceCatalogProvisionedProducts,
+			Tags:    map[string]string{"service": "servicecatalog", "action": "SearchProvisionedProducts"},
+			KeyColumns: plugin.KeyColumnSlice{
+				{
+					Name:    "accept_language",
+					Require: plugin.Optional,
+				},
+				{
+					Name:    "created_time",
+					Require: plugin.Optional,
+				},
+				{
+					Name:    "last_record_id",
+					Require: plugin.Optional,
+				},
+				{
+					Name:    "idempotency_token",
+					Require: plugin.Optional,
+				},
+				{
+					Name:    "product_id",
+					Require: plugin.Optional,
+				},
+				{
+					Name:    "type",
+					Require: plugin.Optional,
+				},
+				{
+					Name:    "status",
+					Require: plugin.Optional,
+				},
+				{
+					Name:    "last_provisioning_record_id",
+					Require: plugin.Optional,
+				},
+				{
+					Name:    "last_successful_provisioning_record_id",
+					Require: plugin.Optional,
+				},
+			},
+		},
+		HydrateConfig: []plugin.HydrateConfig{
+			{
+				Func: getServiceCatalogProvisionedProduct,
+				Tags: map[string]string{"service": "servicecatalog", "action": "DescribeProvisionedProduct"},
+			},
+		},
+		GetMatrixItemFunc: SupportedRegionMatrix(servicecatalogv1.EndpointsID),
+		Columns: awsRegionalColumns([]*plugin.Column{
+			{
+				Name:        "arn",
+				Description: "The ARN of the provisioned product.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.Arn"),
+			},
+			{
+				Name:        "id",
+				Description: "The identifier of the provisioned product.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.Id"),
+			},
+			{
+				Name:        "created_time",
+				Description: "The UTC time stamp of the creation time.",
+				Type:        proto.ColumnType_TIMESTAMP,
+				Transform:   transform.FromField("ProvisionedProductDetail.CreatedTime"),
+			},
+			{
+				Name:        "idempotency_token",
+				Description: "A unique identifier that you provide to ensure idempotency. If multiple requests differ only by the idempotency token, the same response is returned for each repeated request.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.IdempotencyToken"),
+			},
+			{
+				Name:        "last_provisioning_record_id",
+				Description: "The record identifier of the last request performed on this provisioned product.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.LastProvisioningRecordId"),
+			},
+			{
+				Name:        "accept_language",
+				Description: "The language code.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromQual("accept_language"),
+			},
+			{
+				Name:        "last_record_id",
+				Description: "The record identifier of the last request performed on this provisioned product.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.LastRecordId"),
+			},
+			{
+				Name:        "last_successful_provisioning_record_id",
+				Description: "The record identifier of the last successful request performed on this provisioned product.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.LastSuccessfulProvisioningRecordId"),
+			},
+			{
+				Name:        "launch_role_arn",
+				Description: "The ARN of the launch role associated with the provisioned product.",
+				Type:        proto.ColumnType_STRING,
+				Hydrate:     getServiceCatalogProvisionedProduct,
+				Transform:   transform.FromField("ProvisionedProductDetail.LaunchRoleArn"),
+			},
+			{
+				Name:        "name",
+				Description: "The user-friendly name of the provisioned product.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.Name"),
+			},
+			{
+				Name:        "product_id",
+				Description: "The product identifier. For example, prod-abcdzk7xy33qa.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.ProductId"),
+			},
+			{
+				Name:        "provisioning_artifact_id",
+				Description: "The identifier of the provisioning artifact. For example, pa-4abcdjnxjj6ne.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.ProvisioningArtifactId"),
+			},
+			{
+				Name:        "status",
+				Description: "The current status of the provisioned product.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.Status"),
+			},
+			{
+				Name:        "status_message",
+				Description: "The current status message of the provisioned product.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.StatusMessage"),
+			},
+			{
+				Name:        "type",
+				Description: "The type of provisioned product. The supported values are CFN_STACK and CFN_STACKSET.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.Type"),
+			},
+			{
+				Name:        "cloud_watch_dashboards",
+				Description: "Any CloudWatch dashboards that were created when provisioning the product.",
+				Type:        proto.ColumnType_JSON,
+			},
+
+			// Steampipe standard columns
+			{
+				Name:        "title",
+				Description: resourceInterfaceDescription("title"),
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ProvisionedProductDetail.Name"),
+			},
+			{
+				Name:        "akas",
+				Description: resourceInterfaceDescription("akas"),
+				Type:        proto.ColumnType_JSON,
+				Transform:   transform.FromField("ProvisionedProductDetail.Arn").Transform(transform.EnsureStringArray),
+			},
+		}),
+	}
+}
+
+//// LIST FUNCTION
+
+func listServiceCatalogProvisionedProducts(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) {
+
+	// Create Client
+	svc, err := ServiceCatalogClient(ctx, d)
+	if err != nil {
+		plugin.Logger(ctx).Error("aws_servicecatalog_provisioned_product.listServiceCatalogProvisionedProducts", "client_error", err)
+		return nil, err
+	}
+	if svc == nil {
+		// Unsupported region check
+		return nil, nil
+	}
+
+	// Limiting the results
+	maxLimit := int32(100)
+	if d.QueryContext.Limit != nil {
+		limit := int32(*d.QueryContext.Limit)
+		if limit < maxLimit {
+			maxLimit = limit
+		}
+	}
+
+	input := &servicecatalog.SearchProvisionedProductsInput{
+		PageSize: maxLimit,
+	}
+
+	if d.EqualsQualString("accept_language") != "" {
+		input.AcceptLanguage = aws.String(d.EqualsQualString("accept_language"))
+	}
+
+	filters := buildServiceCatalogProvisionedProductFilter(ctx, d.Quals)
+	if filters != nil {
+		input.Filters = filters
+	}
+
+	paginator := servicecatalog.NewSearchProvisionedProductsPaginator(svc, input, func(o *servicecatalog.SearchProvisionedProductsPaginatorOptions) {
+		o.Limit = maxLimit
+		o.StopOnDuplicateToken = true
+	})
+
+	for paginator.HasMorePages() {
+		output, err := paginator.NextPage(ctx)
+		if err != nil {
+			plugin.Logger(ctx).Error("aws_servicecatalog_provisioned_product.listServiceCatalogProvisionedProducts", "api_error", err)
+			return nil, err
+		}
+
+		for _, item := range output.ProvisionedProducts {
+			d.StreamListItem(ctx, &servicecatalog.DescribeProvisionedProductOutput{
+				ProvisionedProductDetail: &types.ProvisionedProductDetail{
+					Arn:                                item.Arn,
+					CreatedTime:                        item.CreatedTime,
+					Name:                               item.Name,
+					Id:                                 item.Id,
+					IdempotencyToken:                   item.IdempotencyToken,
+					LastProvisioningRecordId:           item.LastProvisioningRecordId,
+					LastRecordId:                       item.LastRecordId,
+					LastSuccessfulProvisioningRecordId: item.LastSuccessfulProvisioningRecordId,
+					ProductId:                          item.ProductId,
+					ProvisioningArtifactId:             item.ProvisioningArtifactId,
+					Status:                             item.Status,
+					StatusMessage:                      item.StatusMessage,
+					Type:                               item.Type,
+				},
+			})
+
+			// Context may get cancelled due to manual cancellation or if the limit has been reached
+			if d.RowsRemaining(ctx) == 0 {
+				return nil, nil
+			}
+		}
+	}
+
+	return nil, nil
+}
+
+//// HYDRATE FUNCTIONS
+
+func getServiceCatalogProvisionedProduct(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
+	var id, name string
+	id = d.EqualsQualString("id")
+	name = d.EqualsQualString("name")
+
+	if id == "" && name == "" {
+		return nil, nil
+	}
+
+	if id != "" && name != "" {
+		return nil, fmt.Errorf("Both ProvisionedProductName and ProvisionedProductId cannot be passed in the where clause simultaneously")
+	}
+
+	// Create client
+	svc, err := ServiceCatalogClient(ctx, d)
+	if err != nil {
+		plugin.Logger(ctx).Error("aws_servicecatalog_provisioned_product.getServiceCatalogProvisionedProduct", "client_error", err)
+		return nil, err
+	}
+	if svc == nil {
+		// Unsupported region check
+		return nil, nil
+	}
+
+	// Input parameter
+	params := &servicecatalog.DescribeProvisionedProductInput{}
+	// Get call by passing id or name
+	if id != "" {
+		params.Id = aws.String(id)
+	} else {
+		params.Name = aws.String(name)
+	}
+	op, err := svc.DescribeProvisionedProduct(ctx, params)
+	if err != nil {
+		plugin.Logger(ctx).Error("aws_servicecatalog_provisioned_product.getServiceCatalogProvisionedProduct", "api_error", err)
+		return nil, err
+	}
+	return op, nil
+}
+
+//// UTILITY FUNCTIONS
+
+// Buid servicecatalog product list call filter param
+
+func buildServiceCatalogProvisionedProductFilter(ctx context.Context, quals plugin.KeyColumnQualMap) map[string][]string {
+	filterQuals := map[string]string{
+		"created_time":                           "createdTime",
+		"last_record_id":                         "lastRecordId",
+		"idempotency_token":                      "idempotencyToken",
+		"product_id":                             "productId",
+		"type":                                   "type",
+		"status":                                 "status",
+		"last_provisioning_record_id":            "lastProvisioningRecordId",
+		"last_successful_provisioning_record_id": "lastSuccessfulProvisioningRecordId",
+	}
+
+	filters := []string{}
+	for columnName := range filterQuals {
+		if quals[columnName] != nil {
+			value := getQualsValueByColumn(quals, columnName, "string")
+			val, ok := value.(string)
+			if ok {
+				filters = append(filters, fmt.Sprintf("%s:%s", filterQuals[columnName], val))
+			}
+		}
+	}
+	if len(filters) > 0 {
+		return map[string][]string{"SearchQuery": filters}
+	}
+	return nil
+}
diff --git a/docs/tables/aws_servicecatalog_provisioned_product.md b/docs/tables/aws_servicecatalog_provisioned_product.md
new file mode 100644
index 000000000..607689382
--- /dev/null
+++ b/docs/tables/aws_servicecatalog_provisioned_product.md
@@ -0,0 +1,206 @@
+---
+title: "Steampipe Table: aws_servicecatalog_provisioned_product - Query AWS Service Catalog Provisioned Product using SQL"
+description: "Allows users to query AWS Service Catalog Provisioned Product data including product details, owner, type, and associated metadata."
+---
+
+# Table: aws_servicecatalog_provisioned_product - Query AWS Service Catalog Provisioned Product using SQL
+
+A provisioned product is a resourced instance of a product. For example, provisioning a product based on a CloudFormation template launches a CloudFormation stack and its underlying resources.
+
+## Table Usage Guide
+
+The `aws_servicecatalog_provisioned_product` table in Steampipe provides you with information about provisioned products within AWS Service Catalog. This table allows you, as a DevOps engineer, to query product-specific details, including product owner, type, and associated metadata. You can utilize this table to gather insights on products such as their distribution status, launch paths, provisioning artifacts, and more. The schema outlines the various attributes of the AWS Service Catalog Provisioned Product for you, including the product ARN, creation time, product type, owner.
+
+**Important notes:**
+This table supports optional quals. Queries with optional quals are optimised to use search filters. Optional quals are supported for the following columns:
+	- `created_time`
+	- `id`
+	- `last_record_id`
+	- `idempotency_token`
+	- `name`
+	- `product_id`
+	- `type`
+	- `status`
+	- `last_provisioning_record_id`
+	- `last_successful_provisioning_record_id`
+
+## Examples
+
+### Basic info
+This query can be very useful for getting a comprehensive overview of provisioned products in your AWS environment, particularly for inventory management, auditing, and tracking the status and details of various service catalog products. It helps in understanding what services are currently deployed, their status, and key identifiers that might be required for further management or automation tasks.
+
+```sql+postgres
+select
+  name,
+  id,
+  arn,
+  type,
+  product_id,
+  status,
+  created_time,
+  last_provisioning_record_id
+from
+  aws_servicecatalog_provisioned_product;
+```
+
+```sql+sqlite
+select
+  name,
+  id,
+  arn,
+  type,
+  product_id,
+  status,
+  created_time,
+  last_provisioning_record_id
+from
+  aws_servicecatalog_provisioned_product;
+```
+
+### List the provisioned products created in the last 7 days
+The query you've provided is useful for retrieving information about AWS Service Catalog provisioned products that were created within the last 7 days. It's particularly valuable for monitoring and managing recent resource provisioning activities in AWS.
+
+```sql+postgres
+select
+  name,
+  id,
+  arn,
+  type,
+  product_id,
+  status,
+  created_time,
+  last_provisioning_record_id
+from
+  aws_servicecatalog_provisioned_product
+where
+  created_time >= (current_date - interval '7' day)
+order by
+  created_time;
+```
+
+```sql+sqlite
+select
+  name,
+  id,
+  arn,
+  type,
+  product_id,
+  status,
+  created_time,
+  last_provisioning_record_id
+from
+  aws_servicecatalog_provisioned_product
+where
+  created_time >= date('now', '-7 day')
+order by
+  created_time;
+```
+
+### Get product details of the successfully provisioned product
+Filters provisioned products to include only those products where a provisioning process has been successfully completed at least once. This can be particularly useful for maintaining an inventory of active and successfully set up resources, aiding in tracking and managing AWS resources effectively.
+
+```sql+postgres
+select
+  name,
+  id,
+  arn,
+  type,
+  product_id,
+  status,
+  created_time,
+  last_provisioning_record_id
+from
+  aws_servicecatalog_provisioned_product
+where
+  last_successful_provisioning_record_id is not null;
+```
+
+```sql+sqlite
+select
+  name,
+  id,
+  arn,
+  type,
+  product_id,
+  status,
+  created_time,
+  last_provisioning_record_id
+from
+  aws_servicecatalog_provisioned_product
+where
+  last_successful_provisioning_record_id is not null;
+```
+
+### List details of the successfully provisioned product
+This allows you to see detailed information about each provisioned product, such as its name, type, support URL, and support email, alongside its provisioning ID. It's particularly useful for having a consolidated view of product details along with their provisioning status, especially for those products with a successful provisioning record.
+
+```sql+postgres
+select
+  pr.id as provisioning_id,
+  p.name as product_name,
+  p.id as product_view_id,
+  p.product_id,
+  p.type as product_type,
+  p.support_url as product_support_url,
+  p.support_email as product_support_email
+from
+  aws_servicecatalog_provisioned_product as pr,
+  aws_servicecatalog_product as p
+where
+  pr.product_id = p.product_id
+  and last_successful_provisioning_record_id is not null;
+```
+
+```sql+sqlite
+select
+  pr.id as provisioning_id,
+  p.name as product_name,
+  p.id as product_view_id,
+  p.product_id,
+  p.type as product_type,
+  p.support_url as product_support_url,
+  p.support_email as product_support_email
+from
+  aws_servicecatalog_provisioned_product as pr
+join
+  aws_servicecatalog_product as p on pr.product_id = p.product_id
+where
+  pr.last_successful_provisioning_record_id is not null;
+```
+
+### List the provisioned products of CFN_STACK type
+Ensures that the provisioned products that have been successfully deployed at least once. This is beneficial for monitoring and auditing successful deployments, understanding resource utilization, and managing CloudFormation-based resources effectively.
+
+```sql+postgres
+select
+  name,
+  id,
+  arn,
+  type,
+  product_id,
+  status,
+  created_time,
+  last_provisioning_record_id
+from
+  aws_servicecatalog_provisioned_product
+where
+  type = 'CFN_STACK'
+  and last_successful_provisioning_record_id is not null;
+```
+
+```sql+sqlite
+select
+  name,
+  id,
+  arn,
+  type,
+  product_id,
+  status,
+  created_time,
+  last_provisioning_record_id
+from
+  aws_servicecatalog_provisioned_product
+where
+  type = 'CFN_STACK'
+  and last_successful_provisioning_record_id is not null;
+```
\ No newline at end of file


Test patch: