diff --git a/.changelog/44471.txt b/.changelog/44471.txt new file mode 100644 index 000000000000..6e930e416cb7 --- /dev/null +++ b/.changelog/44471.txt @@ -0,0 +1,19 @@ +```release-note:bug +resource/aws_s3_access_point: Fix support for S3 Express One Zone Directory Bucket Access Points by ensuring proper AWS SDK endpoint routing +``` + +```release-note:bug +resource/aws_s3control_access_point_policy: Fix support for S3 Express One Zone Directory Bucket Access Points by ensuring proper AWS SDK endpoint routing +``` + +```release-note:bug +resource/aws_s3control_bucket_policy: Fix support for S3 Express One Zone Directory Bucket resources by ensuring proper AWS SDK endpoint routing +``` + +```release-note:bug +resource/aws_s3control_bucket_lifecycle_configuration: Fix support for S3 Express One Zone Directory Bucket resources by ensuring proper AWS SDK endpoint routing +``` + +```release-note:bug +resource/aws_s3control_directory_bucket_access_point_scope: Fix operations by ensuring proper AWS SDK endpoint routing for Directory Buckets +``` \ No newline at end of file diff --git a/internal/conns/awsclient.go b/internal/conns/awsclient.go index 93aca9b3ee53..f4f903b9c70f 100644 --- a/internal/conns/awsclient.go +++ b/internal/conns/awsclient.go @@ -17,6 +17,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws/arn" apigatewayv2_types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3control" "github.com/hashicorp/aws-sdk-go-base/v2/endpoints" baselogging "github.com/hashicorp/aws-sdk-go-base/v2/logging" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -191,6 +192,14 @@ func (c *AWSClient) S3ExpressClient(ctx context.Context) *s3.Client { return c.s3ExpressClient } +// S3ExpressControlClient returns an AWS SDK for Go v2 S3 Control API client suitable for use with S3 Express (directory buckets). +// For Directory Buckets, control plane operations like ListTagsForResource must use the s3express-control.region.amazonaws.com endpoint. +func (c *AWSClient) S3ExpressControlClient(ctx context.Context) *s3control.Client { + return errs.Must(client[*s3control.Client](ctx, c, names.S3Control, map[string]any{ + "endpoint": fmt.Sprintf("https://s3express-control.%s.%s", c.Region(ctx), c.DNSSuffix(ctx)), + })) +} + // S3UsePathStyle returns the s3_force_path_style provider configuration value. func (c *AWSClient) S3UsePathStyle(context.Context) bool { return c.s3UsePathStyle @@ -258,7 +267,7 @@ func (c *AWSClient) DefaultKMSKeyPolicy(ctx context.Context) string { "Resource": "*" } ] -} +} `, c.Partition(ctx), c.AccountID(ctx)) } diff --git a/internal/service/s3control/access_point.go b/internal/service/s3control/access_point.go index 395d3dfa99ac..963c18639a84 100644 --- a/internal/service/s3control/access_point.go +++ b/internal/service/s3control/access_point.go @@ -28,7 +28,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/names" ) -// @SDKResource("aws_s3_access_point, name="Access Point") +// @SDKResource("aws_s3_access_point", name="Access Point") // @Tags(identifierAttribute="arn") func resourceAccessPoint() *schema.Resource { return &schema.Resource{ @@ -155,16 +155,21 @@ func resourceAccessPoint() *schema.Resource { func resourceAccessPointCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics c := meta.(*conns.AWSClient) - conn := c.S3ControlClient(ctx) accountID := c.AccountID(ctx) if v, ok := d.GetOk(names.AttrAccountID); ok { accountID = v.(string) } name := d.Get(names.AttrName).(string) + bucketName := d.Get(names.AttrBucket).(string) + + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + input := s3control.CreateAccessPointInput{ AccountId: aws.String(accountID), - Bucket: aws.String(d.Get(names.AttrBucket).(string)), + Bucket: aws.String(bucketName), Name: aws.String(name), Tags: getTagsIn(ctx), } @@ -224,13 +229,16 @@ func resourceAccessPointCreate(ctx context.Context, d *schema.ResourceData, meta func resourceAccessPointRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics c := meta.(*conns.AWSClient) - conn := c.S3ControlClient(ctx) accountID, name, err := accessPointParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendFromErr(diags, err) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + output, err := findAccessPointByTwoPartKey(ctx, conn, accountID, name) if !d.IsNewResource() && tfresource.NotFound(err) { @@ -335,13 +343,17 @@ func resourceAccessPointRead(ctx context.Context, d *schema.ResourceData, meta a func resourceAccessPointUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) accountID, name, err := accessPointParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendFromErr(diags, err) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + if d.HasChange(names.AttrPolicy) { if v, ok := d.GetOk(names.AttrPolicy); ok && v.(string) != "" && v.(string) != "{}" { policy, err := structure.NormalizeJsonString(v.(string)) @@ -379,13 +391,17 @@ func resourceAccessPointUpdate(ctx context.Context, d *schema.ResourceData, meta func resourceAccessPointDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) accountID, name, err := accessPointParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendFromErr(diags, err) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + log.Printf("[DEBUG] Deleting S3 Access Point: %s", d.Id()) input := s3control.DeleteAccessPointInput{ AccountId: aws.String(accountID), diff --git a/internal/service/s3control/access_point_policy.go b/internal/service/s3control/access_point_policy.go index 5485fce4963d..91672350d667 100644 --- a/internal/service/s3control/access_point_policy.go +++ b/internal/service/s3control/access_point_policy.go @@ -53,9 +53,10 @@ func resourceAccessPointPolicy() *schema.Resource { func resourceAccessPointPolicyCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) - resourceID, err := accessPointCreateResourceID(d.Get("access_point_arn").(string)) + accessPointARN := d.Get("access_point_arn").(string) + resourceID, err := accessPointCreateResourceID(accessPointARN) if err != nil { return sdkdiag.AppendFromErr(diags, err) } @@ -65,6 +66,10 @@ func resourceAccessPointPolicyCreate(ctx context.Context, d *schema.ResourceData return sdkdiag.AppendFromErr(diags, err) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + policy, err := structure.NormalizeJsonString(d.Get(names.AttrPolicy).(string)) if err != nil { return sdkdiag.AppendFromErr(diags, err) @@ -89,13 +94,17 @@ func resourceAccessPointPolicyCreate(ctx context.Context, d *schema.ResourceData func resourceAccessPointPolicyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) accountID, name, err := accessPointParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendFromErr(diags, err) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + policy, status, err := findAccessPointPolicyAndStatusByTwoPartKey(ctx, conn, accountID, name) if !d.IsNewResource() && tfresource.NotFound(err) { @@ -125,13 +134,17 @@ func resourceAccessPointPolicyRead(ctx context.Context, d *schema.ResourceData, func resourceAccessPointPolicyUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) accountID, name, err := accessPointParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendFromErr(diags, err) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + policy, err := structure.NormalizeJsonString(d.Get(names.AttrPolicy).(string)) if err != nil { return sdkdiag.AppendFromErr(diags, err) @@ -154,13 +167,17 @@ func resourceAccessPointPolicyUpdate(ctx context.Context, d *schema.ResourceData func resourceAccessPointPolicyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) accountID, name, err := accessPointParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendFromErr(diags, err) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + log.Printf("[DEBUG] Deleting S3 Access Point Policy: %s", d.Id()) input := s3control.DeleteAccessPointPolicyInput{ AccountId: aws.String(accountID), diff --git a/internal/service/s3control/bucket_lifecycle_configuration.go b/internal/service/s3control/bucket_lifecycle_configuration.go index 5ca00c558e4f..59dae6312013 100644 --- a/internal/service/s3control/bucket_lifecycle_configuration.go +++ b/internal/service/s3control/bucket_lifecycle_configuration.go @@ -130,7 +130,7 @@ func resourceBucketLifecycleConfiguration() *schema.Resource { func resourceBucketLifecycleConfigurationCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) bucket := d.Get(names.AttrBucket).(string) parsedArn, err := arn.Parse(bucket) @@ -140,9 +140,13 @@ func resourceBucketLifecycleConfigurationCreate(ctx context.Context, d *schema.R } if parsedArn.AccountID == "" { - return sdkdiag.AppendErrorf(diags, "parsing S3 Control Bucket ARN (%s): unknown format", d.Id()) + return sdkdiag.AppendErrorf(diags, "parsing S3 Control Bucket ARN (%s): unknown format", bucket) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + input := &s3control.PutBucketLifecycleConfigurationInput{ AccountId: aws.String(parsedArn.AccountID), Bucket: aws.String(bucket), @@ -164,7 +168,7 @@ func resourceBucketLifecycleConfigurationCreate(ctx context.Context, d *schema.R func resourceBucketLifecycleConfigurationRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) parsedArn, err := arn.Parse(d.Id()) @@ -176,6 +180,10 @@ func resourceBucketLifecycleConfigurationRead(ctx context.Context, d *schema.Res return sdkdiag.AppendErrorf(diags, "parsing S3 Control Bucket ARN (%s): unknown format", d.Id()) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + output, err := findBucketLifecycleConfigurationByTwoPartKey(ctx, conn, parsedArn.AccountID, d.Id()) if !d.IsNewResource() && tfresource.NotFound(err) { @@ -199,7 +207,7 @@ func resourceBucketLifecycleConfigurationRead(ctx context.Context, d *schema.Res func resourceBucketLifecycleConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) parsedArn, err := arn.Parse(d.Id()) @@ -211,6 +219,10 @@ func resourceBucketLifecycleConfigurationUpdate(ctx context.Context, d *schema.R return sdkdiag.AppendErrorf(diags, "parsing S3 Control Bucket ARN (%s): unknown format", d.Id()) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + input := &s3control.PutBucketLifecycleConfigurationInput{ AccountId: aws.String(parsedArn.AccountID), Bucket: aws.String(d.Id()), @@ -230,7 +242,7 @@ func resourceBucketLifecycleConfigurationUpdate(ctx context.Context, d *schema.R func resourceBucketLifecycleConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) parsedArn, err := arn.Parse(d.Id()) @@ -242,6 +254,10 @@ func resourceBucketLifecycleConfigurationDelete(ctx context.Context, d *schema.R return sdkdiag.AppendErrorf(diags, "parsing S3 Control Bucket ARN (%s): unknown format", d.Id()) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + log.Printf("[DEBUG] Deleting S3 Control Bucket Lifecycle Configuration: %s", d.Id()) _, err = conn.DeleteBucketLifecycleConfiguration(ctx, &s3control.DeleteBucketLifecycleConfigurationInput{ AccountId: aws.String(parsedArn.AccountID), diff --git a/internal/service/s3control/bucket_policy.go b/internal/service/s3control/bucket_policy.go index 39a7f646ec66..45378298924d 100644 --- a/internal/service/s3control/bucket_policy.go +++ b/internal/service/s3control/bucket_policy.go @@ -59,10 +59,14 @@ func resourceBucketPolicy() *schema.Resource { func resourceBucketPolicyCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) bucket := d.Get(names.AttrBucket).(string) + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + policy, err := structure.NormalizeJsonString(d.Get(names.AttrPolicy).(string)) if err != nil { return sdkdiag.AppendFromErr(diags, err) @@ -86,7 +90,7 @@ func resourceBucketPolicyCreate(ctx context.Context, d *schema.ResourceData, met func resourceBucketPolicyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) parsedArn, err := arn.Parse(d.Id()) if err != nil { @@ -97,6 +101,10 @@ func resourceBucketPolicyRead(ctx context.Context, d *schema.ResourceData, meta return sdkdiag.AppendErrorf(diags, "parsing S3 Control Bucket ARN (%s): unknown format", d.Id()) } + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) + output, err := findBucketPolicyByTwoPartKey(ctx, conn, parsedArn.AccountID, d.Id()) if !d.IsNewResource() && tfresource.NotFound(err) { @@ -127,7 +135,11 @@ func resourceBucketPolicyRead(ctx context.Context, d *schema.ResourceData, meta func resourceBucketPolicyUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) + + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) policy, err := structure.NormalizeJsonString(d.Get(names.AttrPolicy).(string)) if err != nil { @@ -150,7 +162,11 @@ func resourceBucketPolicyUpdate(ctx context.Context, d *schema.ResourceData, met func resourceBucketPolicyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).S3ControlClient(ctx) + c := meta.(*conns.AWSClient) + + // Directory Buckets are supported by the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN + conn := c.S3ControlClient(ctx) parsedArn, err := arn.Parse(d.Id()) diff --git a/internal/service/s3control/directory_bucket_access_point_scope.go b/internal/service/s3control/directory_bucket_access_point_scope.go index ee4dd2f3e5cb..faadeebbf814 100644 --- a/internal/service/s3control/directory_bucket_access_point_scope.go +++ b/internal/service/s3control/directory_bucket_access_point_scope.go @@ -100,6 +100,8 @@ func (r *directoryBucketAccessPointScopeResource) Schema(ctx context.Context, _ func (r *directoryBucketAccessPointScopeResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { var plan directoryBucketAccessPointScopeModel + // Directory Bucket Access Point Scope operations use the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN conn := r.Meta().S3ControlClient(ctx) response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) @@ -128,6 +130,8 @@ func (r *directoryBucketAccessPointScopeResource) Create(ctx context.Context, re func (r *directoryBucketAccessPointScopeResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { var data directoryBucketAccessPointScopeModel + // Directory Bucket Access Point Scope operations use the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN conn := r.Meta().S3ControlClient(ctx) response.Diagnostics.Append(request.State.Get(ctx, &data)...) @@ -163,6 +167,8 @@ func (r *directoryBucketAccessPointScopeResource) Read(ctx context.Context, requ func (r *directoryBucketAccessPointScopeResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { var state, plan directoryBucketAccessPointScopeModel + // Directory Bucket Access Point Scope operations use the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN conn := r.Meta().S3ControlClient(ctx) response.Diagnostics.Append(request.State.Get(ctx, &state)...) @@ -194,6 +200,8 @@ func (r *directoryBucketAccessPointScopeResource) Update(ctx context.Context, re func (r *directoryBucketAccessPointScopeResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { var data directoryBucketAccessPointScopeModel + // Directory Bucket Access Point Scope operations use the standard S3 Control client + // The AWS SDK automatically routes to the correct endpoint based on the resource ARN conn := r.Meta().S3ControlClient(ctx) response.Diagnostics.Append(request.State.Get(ctx, &data)...) diff --git a/internal/service/s3control/exports.go b/internal/service/s3control/exports.go index 5f5fd79ede90..96b92d5afb90 100644 --- a/internal/service/s3control/exports.go +++ b/internal/service/s3control/exports.go @@ -5,6 +5,6 @@ package s3control // Exports for use in other packages. var ( - ListTags = listTags - UpdateTags = updateTags + ListTags = listTagsImpl + UpdateTags = updateTagsImpl ) diff --git a/internal/service/s3control/generate.go b/internal/service/s3control/generate.go index 1124e7d7c418..c96f3494feb3 100644 --- a/internal/service/s3control/generate.go +++ b/internal/service/s3control/generate.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -//go:generate go run ../../generate/tags/main.go -ListTags -ServiceTagsSlice -TagResTypeIsAccountID -TagResTypeElem=AccountId -UpdateTags +//go:generate go run ../../generate/tags/main.go -ListTags -ListTagsFunc=listTagsImpl -ServiceTagsSlice -TagResTypeIsAccountID -TagResTypeElem=AccountId -UpdateTags -UpdateTagsFunc=updateTagsImpl //go:generate go run ../../generate/tags/main.go -ServiceTagsSlice -TagsFunc=svcS3Tags -KeyValueTagsFunc=keyValueTagsFromS3Tags -GetTagsInFunc=getS3TagsIn -SetTagsOutFunc=setS3TagsOut -TagType=S3Tag -- s3_tags_gen.go //go:generate go run ../../generate/servicepackage/main.go //go:generate go run ../../generate/identitytests/main.go diff --git a/internal/service/s3control/tags.go b/internal/service/s3control/tags.go new file mode 100644 index 000000000000..9996f0097ace --- /dev/null +++ b/internal/service/s3control/tags.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package s3control + +import ( + "context" + "strings" + + "github.com/YakDriver/smarterr" + "github.com/aws/aws-sdk-go-v2/service/s3control" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/types/option" +) + +// isDirectoryBucketARN returns true if the ARN represents an S3 Directory Bucket (S3 Express One Zone). +// Directory Bucket ARNs contain "s3express" in the service portion. +func isDirectoryBucketARN(arn string) bool { + return strings.Contains(arn, ":s3express:") +} + +// ListTags lists s3control service tags and set them in Context. +// This overrides the generated function to handle Directory Buckets correctly. +func (p *servicePackage) ListTags(ctx context.Context, meta any, identifier string) error { + c := meta.(*conns.AWSClient) + + var conn *s3control.Client + // Directory Buckets require the S3 Express Control API endpoint + if isDirectoryBucketARN(identifier) { + conn = c.S3ExpressControlClient(ctx) + } else { + conn = c.S3ControlClient(ctx) + } + + tags, err := listTagsImpl(ctx, conn, identifier, c.AccountID(ctx)) + + if err != nil { + return smarterr.NewError(err) + } + + if inContext, ok := tftags.FromContext(ctx); ok { + inContext.TagsOut = option.Some(tags) + } + + return nil +} + +// UpdateTags updates s3control service tags. +// This overrides the generated function to handle Directory Buckets correctly. +func (p *servicePackage) UpdateTags(ctx context.Context, meta any, identifier string, oldTags, newTags any) error { + c := meta.(*conns.AWSClient) + + var conn *s3control.Client + // Directory Buckets require the S3 Express Control API endpoint + if isDirectoryBucketARN(identifier) { + conn = c.S3ExpressControlClient(ctx) + } else { + conn = c.S3ControlClient(ctx) + } + + return updateTagsImpl(ctx, conn, identifier, c.AccountID(ctx), oldTags, newTags) +} diff --git a/internal/service/s3control/tags_gen.go b/internal/service/s3control/tags_gen.go index 0fa3e0b2d292..4cce84ddaac8 100644 --- a/internal/service/s3control/tags_gen.go +++ b/internal/service/s3control/tags_gen.go @@ -9,17 +9,16 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3control" awstypes "github.com/aws/aws-sdk-go-v2/service/s3control/types" "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/logging" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/types/option" "github.com/hashicorp/terraform-provider-aws/names" ) -// listTags lists s3control service tags. +// listTagsImpl lists s3control service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func listTags(ctx context.Context, conn *s3control.Client, identifier, resourceType string, optFns ...func(*s3control.Options)) (tftags.KeyValueTags, error) { +func listTagsImpl(ctx context.Context, conn *s3control.Client, identifier, resourceType string, optFns ...func(*s3control.Options)) (tftags.KeyValueTags, error) { input := s3control.ListTagsForResourceInput{ ResourceArn: aws.String(identifier), AccountId: aws.String(resourceType), @@ -34,23 +33,6 @@ func listTags(ctx context.Context, conn *s3control.Client, identifier, resourceT return keyValueTags(ctx, output.Tags), nil } -// ListTags lists s3control service tags and set them in Context. -// It is called from outside this package. -func (p *servicePackage) ListTags(ctx context.Context, meta any, identifier string) error { - c := meta.(*conns.AWSClient) - tags, err := listTags(ctx, c.S3ControlClient(ctx), identifier, c.AccountID(ctx)) - - if err != nil { - return smarterr.NewError(err) - } - - if inContext, ok := tftags.FromContext(ctx); ok { - inContext.TagsOut = option.Some(tags) - } - - return nil -} - // []*SERVICE.Tag handling // svcTags returns s3control service tags. @@ -99,10 +81,10 @@ func setTagsOut(ctx context.Context, tags []awstypes.Tag) { } } -// updateTags updates s3control service tags. +// updateTagsImpl updates s3control service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func updateTags(ctx context.Context, conn *s3control.Client, identifier, resourceType string, oldTagsMap, newTagsMap any, optFns ...func(*s3control.Options)) error { +func updateTagsImpl(ctx context.Context, conn *s3control.Client, identifier, resourceType string, oldTagsMap, newTagsMap any, optFns ...func(*s3control.Options)) error { oldTags := tftags.New(ctx, oldTagsMap) newTags := tftags.New(ctx, newTagsMap) @@ -142,10 +124,3 @@ func updateTags(ctx context.Context, conn *s3control.Client, identifier, resourc return nil } - -// UpdateTags updates s3control service tags. -// It is called from outside this package. -func (p *servicePackage) UpdateTags(ctx context.Context, meta any, identifier string, oldTags, newTags any) error { - c := meta.(*conns.AWSClient) - return updateTags(ctx, c.S3ControlClient(ctx), identifier, c.AccountID(ctx), oldTags, newTags) -} diff --git a/internal/service/s3control/tags_gen_test.go b/internal/service/s3control/tags_gen_test.go new file mode 100644 index 000000000000..b4674f416974 --- /dev/null +++ b/internal/service/s3control/tags_gen_test.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package s3control + +import "testing" + +func TestIsDirectoryBucketARN(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + arn string + expected bool + }{ + { + name: "Standard S3 bucket ARN", + arn: "arn:partition:s3:::my-bucket", + expected: false, + }, + { + name: "S3 Control Access Point ARN", + arn: "arn:partition:s3:region:123456789012:accesspoint/my-access-point", + expected: false, + }, + { + name: "Directory Bucket ARN (S3 Express)", + arn: "arn:partition:s3express:region:123456789012:bucket/my-directory-bucket--usw2-az1--x-s3", + expected: true, + }, + { + name: "Directory Bucket Access Point ARN", + arn: "arn:partition:s3express:region:123456789012:accesspoint/my-access-point", + expected: true, + }, + { + name: "Empty ARN", + arn: "", + expected: false, + }, + { + name: "Invalid ARN format", + arn: "not-an-arn", + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result := isDirectoryBucketARN(tc.arn) + if result != tc.expected { + t.Errorf("isDirectoryBucketARN(%q) = %v, expected %v", tc.arn, result, tc.expected) + } + }) + } +}