Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit bb27e3e

Browse files
authored
S3: Move Tagging Functionality into Provider Methods (#13657)
1 parent 7c60109 commit bb27e3e

File tree

4 files changed

+106
-64
lines changed

4 files changed

+106
-64
lines changed

localstack-core/localstack/services/s3/provider.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@
212212
StartAfter,
213213
StorageClass,
214214
Tagging,
215+
TagSet,
215216
Token,
216217
TransitionDefaultMinimumObjectSize,
217218
UploadIdMarker,
@@ -472,6 +473,19 @@ def _get_cross_account_bucket(
472473

473474
return store, s3_bucket
474475

476+
def _create_bucket_tags(self, resource_arn: str, account_id: str, region: str, tags: TagSet):
477+
store = self.get_store(account_id, region)
478+
store.TAGS.tag_resource(arn=resource_arn, tags=tags)
479+
480+
def _remove_all_bucket_tags(self, resource_arn: str, account_id: str, region: str):
481+
store = self.get_store(account_id, region)
482+
store.TAGS.tags.pop(resource_arn, None)
483+
484+
def _list_bucket_tags(self, resource_arn: str, account_id: str, region: str) -> TagSet:
485+
store = self.get_store(account_id, region)
486+
tags = store.TAGS.list_tags_for_resource(resource_arn)["Tags"]
487+
return tags
488+
475489
@staticmethod
476490
def get_store(account_id: str, region_name: str) -> S3Store:
477491
# Use default account id for external access? would need an anonymous one
@@ -500,7 +514,7 @@ def create_bucket(
500514

501515
create_bucket_configuration = request.get("CreateBucketConfiguration") or {}
502516

503-
bucket_tags = create_bucket_configuration.get("Tags")
517+
bucket_tags = create_bucket_configuration.get("Tags", [])
504518
if bucket_tags:
505519
validate_tag_set(bucket_tags, type_set="create-bucket")
506520

@@ -556,9 +570,8 @@ def create_bucket(
556570
store.buckets[bucket_name] = s3_bucket
557571
store.global_bucket_map[bucket_name] = s3_bucket.bucket_account_id
558572
if bucket_tags:
559-
store.TAGS.tag_resource(
560-
arn=s3_bucket.bucket_arn,
561-
tags=bucket_tags,
573+
self._create_bucket_tags(
574+
s3_bucket.bucket_arn, context.account_id, bucket_region, bucket_tags
562575
)
563576
self._cors_handler.invalidate_cache()
564577
self._storage_backend.create_bucket(bucket_name)
@@ -598,6 +611,9 @@ def delete_bucket(
598611
self._preconditions_locks.pop(bucket, None)
599612
# clean up the storage backend
600613
self._storage_backend.delete_bucket(bucket)
614+
self._remove_all_bucket_tags(
615+
s3_bucket.bucket_arn, context.account_id, s3_bucket.bucket_region
616+
)
601617

602618
def list_buckets(
603619
self,
@@ -3251,8 +3267,12 @@ def put_bucket_tagging(
32513267
validate_tag_set(tag_set, type_set="bucket")
32523268

32533269
# remove the previous tags before setting the new ones, it overwrites the whole TagSet
3254-
store.TAGS.tags.pop(s3_bucket.bucket_arn, None)
3255-
store.TAGS.tag_resource(s3_bucket.bucket_arn, tags=tag_set)
3270+
self._remove_all_bucket_tags(
3271+
s3_bucket.bucket_arn, context.account_id, s3_bucket.bucket_region
3272+
)
3273+
self._create_bucket_tags(
3274+
s3_bucket.bucket_arn, context.account_id, s3_bucket.bucket_region, tag_set
3275+
)
32563276

32573277
def get_bucket_tagging(
32583278
self,
@@ -3262,7 +3282,9 @@ def get_bucket_tagging(
32623282
**kwargs,
32633283
) -> GetBucketTaggingOutput:
32643284
store, s3_bucket = self._get_cross_account_bucket(context, bucket)
3265-
tag_set = store.TAGS.list_tags_for_resource(s3_bucket.bucket_arn, root_name="Tags")["Tags"]
3285+
tag_set = self._list_bucket_tags(
3286+
s3_bucket.bucket_arn, context.account_id, s3_bucket.bucket_region
3287+
)
32663288
if not tag_set:
32673289
raise NoSuchTagSet(
32683290
"The TagSet does not exist",
@@ -3280,7 +3302,13 @@ def delete_bucket_tagging(
32803302
) -> None:
32813303
store, s3_bucket = self._get_cross_account_bucket(context, bucket)
32823304

3283-
store.TAGS.tags.pop(s3_bucket.bucket_arn, None)
3305+
# This operation doesn't remove the tags from the store like deleting a resource does, it just sets them as empty.
3306+
self._remove_all_bucket_tags(
3307+
s3_bucket.bucket_arn, context.account_id, s3_bucket.bucket_region
3308+
)
3309+
self._create_bucket_tags(
3310+
s3_bucket.bucket_arn, context.account_id, s3_bucket.bucket_region, []
3311+
)
32843312

32853313
def put_object_tagging(
32863314
self,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from localstack.aws.api import CommonServiceException
2+
3+
4+
class NoSuchResource(CommonServiceException):
5+
def __init__(self, message=None):
6+
super().__init__("NoSuchResource", status_code=404, message=message)
Lines changed: 34 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from localstack.aws.api import CommonServiceException, RequestContext
1+
from localstack.aws.api import RequestContext
22
from localstack.aws.api.s3control import (
33
AccountId,
44
ListTagsForResourceResult,
@@ -9,16 +9,9 @@
99
TagResourceResult,
1010
UntagResourceResult,
1111
)
12-
from localstack.aws.forwarder import NotImplementedAvoidFallbackError
13-
from localstack.services.s3.models import s3_stores
14-
from localstack.services.s3control.validation import validate_tags
12+
from localstack.services.s3.models import S3Store, s3_stores
13+
from localstack.services.s3control.validation import validate_arn_for_tagging, validate_tags
1514
from localstack.state import StateVisitor
16-
from localstack.utils.tagging import TaggingService
17-
18-
19-
class NoSuchResource(CommonServiceException):
20-
def __init__(self, message=None):
21-
super().__init__("NoSuchResource", status_code=404, message=message)
2215

2316

2417
class S3ControlProvider(S3ControlApi):
@@ -33,26 +26,26 @@ def accept_state_visitor(self, visitor: StateVisitor):
3326
"""
3427

3528
@staticmethod
36-
def _get_tagging_service_for_bucket(
37-
resource_arn: S3ResourceArn,
38-
partition: str,
39-
region: str,
40-
account_id: str,
41-
) -> TaggingService:
42-
s3_prefix = f"arn:{partition}:s3:::"
43-
if not resource_arn.startswith(s3_prefix):
44-
# Moto does not support Tagging operations for S3 Control, so we should not forward those operations back
45-
# to it
46-
raise NotImplementedAvoidFallbackError(
47-
"LocalStack only support Bucket tagging operations for S3Control"
48-
)
29+
def get_s3_store(account_id: str, region: str) -> S3Store:
30+
return s3_stores[account_id][region]
4931

50-
store = s3_stores[account_id][region]
51-
bucket_name = resource_arn.removeprefix(s3_prefix)
52-
if bucket_name not in store.global_bucket_map:
53-
raise NoSuchResource("The specified resource doesn't exist.")
32+
def _tag_bucket_resource(
33+
self, resource_arn: str, partition: str, region: str, account_id: str, tags: TagList
34+
) -> None:
35+
tagging_service = self.get_s3_store(account_id, region).TAGS
36+
tagging_service.tag_resource(resource_arn, tags)
5437

55-
return store.TAGS
38+
def _untag_bucket_resource(
39+
self, resource_arn: str, partition: str, region: str, account_id: str, tag_keys: TagKeyList
40+
) -> None:
41+
tagging_service = self.get_s3_store(account_id, region).TAGS
42+
tagging_service.untag_resource(resource_arn, tag_keys)
43+
44+
def _list_bucket_tags(
45+
self, resource_arn: str, partition: str, region: str, account_id: str
46+
) -> TagList:
47+
tagging_service = self.get_s3_store(account_id, region).TAGS
48+
return tagging_service.list_tags_for_resource(resource_arn)["Tags"]
5649

5750
def tag_resource(
5851
self,
@@ -62,17 +55,11 @@ def tag_resource(
6255
tags: TagList,
6356
**kwargs,
6457
) -> TagResourceResult:
65-
# currently S3Control only supports tagging buckets
66-
tagging_service = self._get_tagging_service_for_bucket(
67-
resource_arn=resource_arn,
68-
partition=context.partition,
69-
region=context.region,
70-
account_id=account_id,
71-
)
72-
73-
validate_tags(tags=tags)
74-
tagging_service.tag_resource(resource_arn, tags)
58+
# Currently S3Control only supports tagging buckets
59+
validate_arn_for_tagging(resource_arn, context.partition, account_id, context.region)
60+
validate_tags(tags)
7561

62+
self._tag_bucket_resource(resource_arn, context.partition, context.region, account_id, tags)
7663
return TagResourceResult()
7764

7865
def untag_resource(
@@ -83,28 +70,19 @@ def untag_resource(
8370
tag_keys: TagKeyList,
8471
**kwargs,
8572
) -> UntagResourceResult:
86-
# currently S3Control only supports tagging buckets
87-
tagging_service = self._get_tagging_service_for_bucket(
88-
resource_arn=resource_arn,
89-
partition=context.partition,
90-
region=context.region,
91-
account_id=account_id,
92-
)
93-
94-
tagging_service.untag_resource(resource_arn, tag_keys)
73+
# Currently S3Control only supports tagging buckets
74+
validate_arn_for_tagging(resource_arn, context.partition, account_id, context.region)
9575

76+
self._untag_bucket_resource(
77+
resource_arn, context.partition, context.region, account_id, tag_keys
78+
)
9679
return TagResourceResult()
9780

9881
def list_tags_for_resource(
9982
self, context: RequestContext, account_id: AccountId, resource_arn: S3ResourceArn, **kwargs
10083
) -> ListTagsForResourceResult:
101-
# currently S3Control only supports tagging buckets
102-
tagging_service = self._get_tagging_service_for_bucket(
103-
resource_arn=resource_arn,
104-
partition=context.partition,
105-
region=context.region,
106-
account_id=account_id,
107-
)
84+
# Currently S3Control only supports tagging buckets
85+
validate_arn_for_tagging(resource_arn, context.partition, account_id, context.region)
10886

109-
tags = tagging_service.list_tags_for_resource(resource_arn)
110-
return ListTagsForResourceResult(Tags=tags["Tags"])
87+
tags = self._list_bucket_tags(resource_arn, context.partition, context.region, account_id)
88+
return ListTagsForResourceResult(Tags=tags)

localstack-core/localstack/services/s3control/validation.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,37 @@
11
from localstack.aws.api.s3 import InvalidTag
22
from localstack.aws.api.s3control import Tag, TagList
3+
from localstack.aws.forwarder import NotImplementedAvoidFallbackError
34
from localstack.services.s3.exceptions import MalformedXML
5+
from localstack.services.s3.models import s3_stores
46
from localstack.services.s3.utils import TAG_REGEX
7+
from localstack.services.s3control.exceptions import NoSuchResource
8+
9+
10+
def validate_arn_for_tagging(
11+
resource_arn: str, partition: str, account_id: str, region: str
12+
) -> None:
13+
"""
14+
Validates the resource ARN for the resource being tagged.
15+
16+
:param resource_arn: The ARN of the resource being tagged.
17+
:param partition: The partition the request is originating from.
18+
:param account_id: The account ID of the target resource.
19+
:param region: The region the request is originating from.
20+
:return: None
21+
"""
22+
23+
s3_prefix = f"arn:{partition}:s3:::"
24+
if not resource_arn.startswith(s3_prefix):
25+
# Moto does not support Tagging operations for S3 Control, so we should not forward those operations back
26+
# to it
27+
raise NotImplementedAvoidFallbackError(
28+
"LocalStack only support Bucket tagging operations for S3Control"
29+
)
30+
31+
store = s3_stores[account_id][region]
32+
bucket_name = resource_arn.removeprefix(s3_prefix)
33+
if bucket_name not in store.global_bucket_map:
34+
raise NoSuchResource("The specified resource doesn't exist.")
535

636

737
def validate_tags(tags: TagList):

0 commit comments

Comments
 (0)