Skip to content

Add tables aws_s3tables_namespace, aws_s3tables_table and aws_s3tables_table_bucket #2498

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

Merged
merged 13 commits into from
May 21, 2025
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
27 changes: 27 additions & 0 deletions aws/multi_region.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,33 @@ func CloudWatchRegionsMatrix(ctx context.Context, d *plugin.QueryData) []map[str
return SupportedRegionMatrixWithExclusions(AWS_MONITORING_SERVICE_ID, []string{})(ctx, d)
}

func S3TablesRegionsMatrix(ctx context.Context, d *plugin.QueryData) []map[string]interface{} {

commonColumnData, err := getCommonColumns(ctx, d, nil)
if err != nil {
plugin.Logger(ctx).Error("S3TablesRegionsMatrix", "getCommonColumns", "unable to get common column info", err)
panic(err)
}
partitionName := commonColumnData.(*awsCommonColumnData).Partition

// Get AWS partition based on the partition name
// Get supported service along with the endpoints for the partition
partition, err := getPartitionValueByPartitionName(partitionName)
if err != nil {
panic(fmt.Errorf("S3TablesRegionsMatrix: failed to get the endpoint details for the partition '%s', %v", partitionName, err))
}

s3SupportedRegions := partition.Services[AWS_S3_SERVICE_ID].Endpoints
var unsupportedRegionsForS3Tables []string
for region, ed := range s3SupportedRegions {
if !slices.Contains(ed.SignatureVersions, "s3v4") {
unsupportedRegionsForS3Tables = append(unsupportedRegionsForS3Tables, region)
}
}

return SupportedRegionMatrixWithExclusions(AWS_S3_SERVICE_ID, unsupportedRegionsForS3Tables)(ctx, d)
}

// Return a matrix of regions supported by serviceID, which will then be
// queried for the table in parallel. This result factors in things like
// regions that are opted-in, regions for the service and even the `regions`
Expand Down
3 changes: 3 additions & 0 deletions aws/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,9 @@ func Plugin(ctx context.Context) *plugin.Plugin {
"aws_s3_multipart_upload": tableAwsS3MultipartUpload(ctx),
"aws_s3_object": tableAwsS3Object(ctx),
"aws_s3_object_version": tableAwsS3ObjectVersion(ctx),
"aws_s3tables_namespace": tableAwsS3tablesNamespace(ctx),
"aws_s3tables_table": tableAwsS3tablesTable(ctx),
"aws_s3tables_table_bucket": tableAwsS3tablesTableBucket(ctx),
"aws_sagemaker_app": tableAwsSageMakerApp(ctx),
"aws_sagemaker_domain": tableAwsSageMakerDomain(ctx),
"aws_sagemaker_endpoint_configuration": tableAwsSageMakerEndpointConfiguration(ctx),
Expand Down
18 changes: 18 additions & 0 deletions aws/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/route53resolver"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3control"
"github.com/aws/aws-sdk-go-v2/service/s3tables"
"github.com/aws/aws-sdk-go-v2/service/sagemaker"
"github.com/aws/aws-sdk-go-v2/service/scheduler"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
Expand Down Expand Up @@ -1355,6 +1356,23 @@ func S3Client(ctx context.Context, d *plugin.QueryData, region string) (*s3.Clie
return svc, nil
}

// S3TablesClient returns the service client for AWS S3 Tables service.
func S3TablesClient(ctx context.Context, d *plugin.QueryData) (*s3tables.Client, error) {
region := d.EqualsQualString(matrixKeyRegion)
cfg, err := getClientForRegion(ctx, d, region)
if err != nil {
return nil, err
}

if cfg == nil {
return nil, nil
}
// Configure a new AWS session using the updated config with service-specific endpoint options
conn := s3tables.NewFromConfig(*cfg)

return conn, nil
}

func S3ControlClient(ctx context.Context, d *plugin.QueryData, region string) (*s3control.Client, error) {
cfg, err := getClientForRegion(ctx, d, region)
if err != nil {
Expand Down
221 changes: 221 additions & 0 deletions aws/table_aws_s3tables_namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package aws

import (
"context"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3tables"
"github.com/aws/aws-sdk-go-v2/service/s3tables/types"

"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"
)

func tableAwsS3tablesNamespace(_ context.Context) *plugin.Table {
return &plugin.Table{
Name: "aws_s3tables_namespace",
Description: "AWS S3Tables Namespace",
Get: &plugin.GetConfig{
KeyColumns: plugin.AllColumns([]string{"namespace", "table_bucket_arn"}),
IgnoreConfig: &plugin.IgnoreConfig{
ShouldIgnoreErrorFunc: shouldIgnoreErrors([]string{"NotFoundException"}),
},
Hydrate: getS3tablesNamespaceById,
Tags: map[string]string{"service": "s3tables", "action": "ListNamespaces"},
},
List: &plugin.ListConfig{
ParentHydrate: listS3tablesTableBuckets,
Hydrate: listS3tablesNamespaces,
KeyColumns: plugin.KeyColumnSlice{
{Name: "table_bucket_arn", Require: plugin.Optional},
},
Tags: map[string]string{"service": "s3tables", "action": "ListNamespaces"},
},
GetMatrixItemFunc: S3TablesRegionsMatrix,
Columns: awsRegionalColumns([]*plugin.Column{
{
Name: "namespace_id",
Description: "The system-assigned unique identifier for the namespace.",
Type: proto.ColumnType_STRING,
Transform: transform.FromField("Namespace.NamespaceId"),
},
{
Name: "namespace",
Description: "The name of the namespace.",
Type: proto.ColumnType_STRING,
Hydrate: getFirstIndexNamespaceValue,
Transform: transform.FromValue(),
},
{
Name: "created_at",
Description: "The date and time the namespace was created at.",
Type: proto.ColumnType_TIMESTAMP,
Transform: transform.FromField("Namespace.CreatedAt"),
},
{
Name: "created_by",
Description: "The ID of the account that created the namespace.",
Type: proto.ColumnType_STRING,
Transform: transform.FromField("Namespace.CreatedBy"),
},
{
Name: "owner_account_id",
Description: "The ID of the account that owns the namespace.",
Type: proto.ColumnType_STRING,
Transform: transform.FromField("Namespace.OwnerAccountId"),
},
{
Name: "table_bucket_id",
Description: "The system-assigned unique identifier for the table bucket that contains this namespace.",
Type: proto.ColumnType_STRING,
Transform: transform.FromField("Namespace.TableBucketId"),
},
{
Name: "table_bucket_arn",
Description: "The Amazon Resource Name (ARN) of the table bucket associated with the namespace.",
Type: proto.ColumnType_STRING,
},

// Steampipe Standard Columns
{
Name: "title",
Description: resourceInterfaceDescription("title"),
Type: proto.ColumnType_STRING,
Hydrate: getFirstIndexNamespaceValue,
Transform: transform.FromValue(),
},
}),
}
}

// NamespaceInfo holds namespace data along with its parent bucket info
type NamespaceInfo struct {
Namespace types.NamespaceSummary
TableBucketName string
TableBucketArn string
}


//// LIST FUNCTION

func listS3tablesNamespaces(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
// Get bucket details from parent hydrate
bucket := h.Item.(types.TableBucketSummary)

// Minimize the number of API calls
if d.EqualsQualString("table_bucket_arn") != "" && d.EqualsQualString("table_bucket_arn") != *bucket.Arn {
return nil, nil
}

// Create Session
svc, err := S3TablesClient(ctx, d)
if err != nil {
plugin.Logger(ctx).Error("aws_s3tables_namespace.listS3tablesNamespaces", "connection_error", err)
return nil, err
}

if svc == nil {
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 := &s3tables.ListNamespacesInput{
TableBucketARN: bucket.Arn,
MaxNamespaces: aws.Int32(maxLimit),
}

paginator := s3tables.NewListNamespacesPaginator(svc, input, func(o *s3tables.ListNamespacesPaginatorOptions) {
o.Limit = maxLimit
o.StopOnDuplicateToken = true
})

// List call
for paginator.HasMorePages() {
// apply rate limiting
d.WaitForListRateLimit(ctx)

output, err := paginator.NextPage(ctx)
if err != nil {
plugin.Logger(ctx).Error("aws_s3tables_namespace.listS3tablesNamespaces", "api_error", err)
return nil, err
}

for _, namespace := range output.Namespaces {
d.StreamLeafListItem(ctx, &NamespaceInfo{
Namespace: namespace,
TableBucketName: *bucket.Name,
TableBucketArn: *bucket.Arn,
})

// 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 getS3tablesNamespaceById(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) {
namespace := d.EqualsQualString("namespace")
tableBucketArn := d.EqualsQualString("table_bucket_arn")

// Create service
svc, err := S3TablesClient(ctx, d)
if err != nil {
plugin.Logger(ctx).Error("aws_s3tables_namespace.getS3tablesNamespaceById", "connection_error", err)
return nil, err
}

if svc == nil {
return nil, nil
}

// List namespaces for the table bucket and filter by ID
input := &s3tables.GetNamespaceInput{
TableBucketARN: aws.String(tableBucketArn),
Namespace: aws.String(namespace),
}

output, err := svc.GetNamespace(ctx, input)
if err != nil {
plugin.Logger(ctx).Error("aws_s3tables_namespace.getS3tablesNamespaceById", "api_error", err)
return nil, err
}

if output != nil {
return &NamespaceInfo{
Namespace: types.NamespaceSummary{
NamespaceId: output.NamespaceId,
Namespace: output.Namespace,
CreatedAt: output.CreatedAt,
CreatedBy: output.CreatedBy,
OwnerAccountId: output.OwnerAccountId,
TableBucketId: output.TableBucketId,
},
TableBucketArn: tableBucketArn,
}, nil
}

return nil, nil
}

func getFirstIndexNamespaceValue(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
info := h.Item.(*NamespaceInfo)
if len(info.Namespace.Namespace) > 0 {
return info.Namespace.Namespace[0], nil
}
return "", nil
}
Loading
Loading