localstack-core 4.13.2.dev93__py3-none-any.whl → 4.13.2.dev95__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. localstack/services/kms/models.py +16 -41
  2. localstack/services/kms/provider.py +36 -21
  3. localstack/services/kms/utils.py +64 -2
  4. localstack/services/lambda_/invocation/models.py +2 -2
  5. localstack/services/lambda_/packages.py +1 -1
  6. localstack/services/lambda_/provider.py +12 -15
  7. localstack/services/opensearch/models.py +2 -5
  8. localstack/services/opensearch/provider.py +7 -9
  9. localstack/services/route53/models.py +3 -1
  10. localstack/services/route53/provider.py +5 -0
  11. localstack/services/s3/models.py +2 -4
  12. localstack/services/s3/provider.py +67 -61
  13. localstack/services/s3control/provider.py +10 -24
  14. localstack/services/sns/models.py +2 -2
  15. localstack/services/sns/provider.py +8 -9
  16. localstack/services/sqs/models.py +3 -0
  17. localstack/services/sqs/provider.py +22 -25
  18. localstack/utils/tagging.py +99 -1
  19. localstack/version.py +2 -2
  20. {localstack_core-4.13.2.dev93.dist-info → localstack_core-4.13.2.dev95.dist-info}/METADATA +1 -1
  21. {localstack_core-4.13.2.dev93.dist-info → localstack_core-4.13.2.dev95.dist-info}/RECORD +26 -26
  22. {localstack_core-4.13.2.dev93.data → localstack_core-4.13.2.dev95.data}/scripts/localstack-supervisor +0 -0
  23. {localstack_core-4.13.2.dev93.dist-info → localstack_core-4.13.2.dev95.dist-info}/WHEEL +0 -0
  24. {localstack_core-4.13.2.dev93.dist-info → localstack_core-4.13.2.dev95.dist-info}/entry_points.txt +0 -0
  25. {localstack_core-4.13.2.dev93.dist-info → localstack_core-4.13.2.dev95.dist-info}/licenses/LICENSE.txt +0 -0
  26. {localstack_core-4.13.2.dev93.dist-info → localstack_core-4.13.2.dev95.dist-info}/top_level.txt +0 -0
@@ -45,16 +45,15 @@ from localstack.aws.api.kms import (
45
45
  OriginType,
46
46
  ReplicateKeyRequest,
47
47
  SigningAlgorithmSpec,
48
- TagList,
49
48
  UnsupportedOperationException,
50
49
  )
51
- from localstack.constants import TAG_KEY_CUSTOM_ID
52
50
  from localstack.services.kms.exceptions import ValidationException
53
- from localstack.services.kms.utils import is_valid_key_arn, validate_tag, validate_tag_list
51
+ from localstack.services.kms.utils import is_valid_key_arn
54
52
  from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
55
53
  from localstack.utils.aws.arns import get_partition, kms_alias_arn, kms_key_arn
56
54
  from localstack.utils.crypto import decrypt, encrypt
57
55
  from localstack.utils.strings import long_uid, to_bytes, to_str
56
+ from localstack.utils.tagging import Tags
58
57
 
59
58
  LOG = logging.getLogger(__name__)
60
59
 
@@ -114,9 +113,6 @@ RESERVED_ALIASES = [
114
113
  # list of key names that should be skipped when serializing the encryption context
115
114
  IGNORED_CONTEXT_KEYS = ["aws-crypto-public-key"]
116
115
 
117
- # special tag name to allow specifying a custom key material for created keys
118
- TAG_KEY_CUSTOM_KEY_MATERIAL = "_custom_key_material_"
119
-
120
116
 
121
117
  def _serialize_ciphertext_blob(ciphertext: Ciphertext) -> bytes:
122
118
  header = struct.pack(
@@ -289,7 +285,6 @@ class KmsCryptoKey:
289
285
  class KmsKey:
290
286
  metadata: KeyMetadata
291
287
  crypto_key: KmsCryptoKey
292
- tags: dict[str, str]
293
288
  policy: str
294
289
  is_key_rotation_enabled: bool
295
290
  rotation_period_in_days: int
@@ -302,18 +297,13 @@ class KmsKey:
302
297
  create_key_request: CreateKeyRequest = None,
303
298
  account_id: str = None,
304
299
  region: str = None,
300
+ custom_key_material: bytes | None = None,
301
+ custom_key_id: str | None = None,
305
302
  ):
306
303
  create_key_request = create_key_request or CreateKeyRequest()
307
304
  self.previous_keys = []
308
305
 
309
- # Please keep in mind that tags of a key could be present in the request, they are not a part of metadata. At
310
- # least in the sense of DescribeKey not returning them with the rest of the metadata. Instead, tags are more
311
- # like aliases:
312
- # https://docs.aws.amazon.com/kms/latest/APIReference/API_DescribeKey.html
313
- # "DescribeKey does not return the following information: ... Tags on the KMS key."
314
- self.tags = {}
315
- self.add_tags(create_key_request.get("Tags"))
316
- # Same goes for the policy. It is in the request, but not in the metadata.
306
+ # Policy is in the request but not in the metadata.
317
307
  self.policy = create_key_request.get("Policy") or self._get_default_key_policy(
318
308
  account_id, region
319
309
  )
@@ -322,13 +312,7 @@ class KmsKey:
322
312
  # disable it."
323
313
  self.is_key_rotation_enabled = False
324
314
 
325
- self._populate_metadata(create_key_request, account_id, region)
326
- custom_key_material = None
327
- if TAG_KEY_CUSTOM_KEY_MATERIAL in self.tags:
328
- # check if the _custom_key_material_ tag is specified, to use a custom key material for this key
329
- custom_key_material = base64.b64decode(self.tags[TAG_KEY_CUSTOM_KEY_MATERIAL])
330
- # remove the _custom_key_material_ tag from the tags to not readily expose the custom key material
331
- del self.tags[TAG_KEY_CUSTOM_KEY_MATERIAL]
315
+ self._populate_metadata(create_key_request, account_id, region, custom_key_id=custom_key_id)
332
316
  self.crypto_key = KmsCryptoKey(self.metadata.get("KeySpec"), custom_key_material)
333
317
  self._internal_key_id = uuid.uuid4()
334
318
 
@@ -560,7 +544,11 @@ class KmsKey:
560
544
  #
561
545
  # Data keys are symmetric, data key pairs are asymmetric.
562
546
  def _populate_metadata(
563
- self, create_key_request: CreateKeyRequest, account_id: str, region: str
547
+ self,
548
+ create_key_request: CreateKeyRequest,
549
+ account_id: str,
550
+ region: str,
551
+ custom_key_id: str | None = None,
564
552
  ) -> None:
565
553
  self.metadata = KeyMetadata()
566
554
  # Metadata fields coming from a creation request
@@ -597,9 +585,8 @@ class KmsKey:
597
585
  else KeyState.PendingImport
598
586
  )
599
587
 
600
- if TAG_KEY_CUSTOM_ID in self.tags:
601
- # check if the _custom_id_ tag is specified, to set a user-defined KeyId for this key
602
- self.metadata["KeyId"] = self.tags[TAG_KEY_CUSTOM_ID].strip()
588
+ if custom_key_id:
589
+ self.metadata["KeyId"] = custom_key_id
603
590
  elif self.metadata.get("MultiRegion"):
604
591
  # https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html
605
592
  # "Notice that multi-Region keys have a distinctive key ID that begins with mrk-. You can use the mrk- prefix to
@@ -625,21 +612,6 @@ class KmsKey:
625
612
  ReplicaKeys=[],
626
613
  )
627
614
 
628
- def add_tags(self, tags: TagList) -> None:
629
- # Just in case we get None from somewhere.
630
- if not tags:
631
- return
632
-
633
- validate_tag_list(tags)
634
-
635
- # Do not care if we overwrite an existing tag:
636
- # https://docs.aws.amazon.com/kms/latest/APIReference/API_TagResource.html
637
- # "To edit a tag, specify an existing tag key and a new tag value."
638
- for i, tag in enumerate(tags, start=1):
639
- if tag.get("TagKey") != TAG_KEY_CUSTOM_KEY_MATERIAL:
640
- validate_tag(i, tag)
641
- self.tags[tag.get("TagKey")] = tag.get("TagValue")
642
-
643
615
  def schedule_key_deletion(self, pending_window_in_days: int) -> None:
644
616
  self.metadata["Enabled"] = False
645
617
  # TODO For MultiRegion keys, the status of replicas get set to "PendingDeletion", while the primary key
@@ -870,5 +842,8 @@ class KmsStore(BaseStore):
870
842
  # maps import tokens to import data
871
843
  imports: dict[str, KeyImportState] = LocalAttribute(default=dict)
872
844
 
845
+ # maps key arn to tags
846
+ tags: Tags = LocalAttribute(default=Tags)
847
+
873
848
 
874
849
  kms_stores = AccountRegionBundle("kms", KmsStore)
@@ -137,9 +137,12 @@ from localstack.services.kms.models import (
137
137
  )
138
138
  from localstack.services.kms.utils import (
139
139
  execute_dry_run_capable,
140
+ get_custom_key_id,
141
+ get_custom_key_material,
140
142
  is_valid_key_arn,
141
143
  parse_key_arn,
142
144
  validate_alias_name,
145
+ validate_and_filter_tags,
143
146
  )
144
147
  from localstack.services.plugins import ServiceLifecycleHook
145
148
  from localstack.state import StateVisitor
@@ -229,7 +232,13 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
229
232
  account_id: str, region_name: str, request: CreateKeyRequest = None
230
233
  ) -> KmsKey:
231
234
  store = kms_stores[account_id][region_name]
232
- key = KmsKey(request, account_id, region_name)
235
+ key = KmsKey(
236
+ request,
237
+ account_id,
238
+ region_name,
239
+ get_custom_key_material(request),
240
+ get_custom_key_id(request),
241
+ )
233
242
  key_id = key.metadata["KeyId"]
234
243
  store.keys[key_id] = key
235
244
  return key
@@ -299,11 +308,9 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
299
308
  store = kms_stores[account_id][region_name]
300
309
  if alias_name not in RESERVED_ALIASES or alias_name in store.aliases:
301
310
  return
302
- create_key_request = {}
303
311
  key_id = KmsProvider._create_kms_key(
304
312
  account_id,
305
313
  region_name,
306
- create_key_request,
307
314
  ).metadata.get("KeyId")
308
315
  create_alias_request = CreateAliasRequest(AliasName=alias_name, TargetKeyId=key_id)
309
316
  KmsProvider._create_kms_alias(account_id, region_name, create_alias_request)
@@ -393,19 +400,25 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
393
400
  def _is_rsa_spec(key_spec: str) -> bool:
394
401
  return key_spec in [KeySpec.RSA_2048, KeySpec.RSA_3072, KeySpec.RSA_4096]
395
402
 
396
- # These tagging methods are overwritten in the Pro implementation.
397
- def _get_key_tags(self, key: KmsKey, account_id: str, region: str) -> TagList:
398
- return [Tag(TagKey=key, TagValue=value) for key, value in key.tags.items()]
403
+ def _get_key_tags(self, account_id: str, region: str, resource_arn: str) -> TagList:
404
+ store = self._get_store(account_id, region)
405
+ return [
406
+ Tag(TagKey=key, TagValue=value)
407
+ for key, value in store.tags.get_tags(resource_arn).items()
408
+ ]
399
409
 
400
- def _set_key_tags(self, key: KmsKey, account_id: str, region: str, tags: TagList) -> None:
401
- return
410
+ def _set_key_tags(self, account_id: str, region: str, resource_arn: str, tags: TagList) -> None:
411
+ validated_tags = validate_and_filter_tags(tags)
412
+ store = self._get_store(account_id, region)
413
+ store.tags.update_tags(
414
+ resource_arn, {tag["TagKey"]: tag["TagValue"] for tag in validated_tags}
415
+ )
402
416
 
403
417
  def _remove_key_tags(
404
- self, key: KmsKey, account_id: str, region: str, tag_keys: TagKeyList
418
+ self, account_id: str, region: str, resource_arn: str, tag_keys: TagKeyList
405
419
  ) -> None:
406
- # AWS doesn't seem to mind removal of a non-existent tag, so we do not raise any exception.
407
- for tag_key in tag_keys:
408
- key.tags.pop(tag_key, None)
420
+ store = self._get_store(account_id, region)
421
+ store.tags.delete_tags(resource_arn, tag_keys)
409
422
 
410
423
  #
411
424
  # Operation Handlers
@@ -417,8 +430,10 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
417
430
  context: RequestContext,
418
431
  request: CreateKeyRequest = None,
419
432
  ) -> CreateKeyResponse:
433
+ tags = validate_and_filter_tags(request.get("Tags", []))
420
434
  key = self._create_kms_key(context.account_id, context.region, request)
421
- self._set_key_tags(key, context.account_id, context.region, request.get("Tags", []))
435
+ if tags:
436
+ self._set_key_tags(context.account_id, context.region, key.metadata["Arn"], tags)
422
437
  return CreateKeyResponse(KeyMetadata=key.metadata)
423
438
 
424
439
  @handler("ScheduleKeyDeletion", expand=False)
@@ -1487,7 +1502,9 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1487
1502
  key = self._get_kms_key(
1488
1503
  context.account_id, context.region, request.get("KeyId"), any_key_state_allowed=True
1489
1504
  )
1490
- keys_list = PaginatedList(self._get_key_tags(key, context.account_id, context.region))
1505
+ keys_list = PaginatedList(
1506
+ self._get_key_tags(context.account_id, context.region, key.metadata["Arn"])
1507
+ )
1491
1508
  page, next_token = keys_list.get_page(
1492
1509
  lambda tag: tag.get("TagKey"),
1493
1510
  next_token=request.get("Marker"),
@@ -1519,7 +1536,6 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1519
1536
 
1520
1537
  @handler("TagResource", expand=False)
1521
1538
  def tag_resource(self, context: RequestContext, request: TagResourceRequest) -> None:
1522
- tags = request["Tags"]
1523
1539
  key = self._get_kms_key(
1524
1540
  context.account_id,
1525
1541
  context.region,
@@ -1527,14 +1543,11 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1527
1543
  enabled_key_allowed=True,
1528
1544
  disabled_key_allowed=True,
1529
1545
  )
1530
- key.add_tags(tags)
1531
- self._set_key_tags(key, context.account_id, context.region, tags)
1546
+ if tags := request["Tags"]:
1547
+ self._set_key_tags(context.account_id, context.region, key.metadata["Arn"], tags)
1532
1548
 
1533
1549
  @handler("UntagResource", expand=False)
1534
1550
  def untag_resource(self, context: RequestContext, request: UntagResourceRequest) -> None:
1535
- if not (tag_keys := request.get("TagKeys", [])):
1536
- return
1537
-
1538
1551
  key = self._get_kms_key(
1539
1552
  context.account_id,
1540
1553
  context.region,
@@ -1542,7 +1555,9 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
1542
1555
  enabled_key_allowed=True,
1543
1556
  disabled_key_allowed=True,
1544
1557
  )
1545
- self._remove_key_tags(key, context.account_id, context.region, tag_keys)
1558
+ self._remove_key_tags(
1559
+ context.account_id, context.region, key.metadata["Arn"], request["TagKeys"]
1560
+ )
1546
1561
 
1547
1562
  def derive_shared_secret(
1548
1563
  self,
@@ -1,13 +1,23 @@
1
+ import base64
1
2
  import re
2
3
  from collections.abc import Callable
3
4
 
4
- from localstack.aws.api.kms import DryRunOperationException, Tag, TagException, TagList
5
+ from localstack.aws.api.kms import (
6
+ CreateKeyRequest,
7
+ DryRunOperationException,
8
+ Tag,
9
+ TagException,
10
+ TagList,
11
+ )
12
+ from localstack.constants import TAG_KEY_CUSTOM_ID
5
13
  from localstack.services.kms.exceptions import ValidationException
6
14
  from localstack.utils.aws.arns import ARN_PARTITION_REGEX
7
15
 
8
16
  KMS_KEY_ARN_PATTERN = re.compile(
9
17
  rf"{ARN_PARTITION_REGEX}:kms:(?P<region_name>[^:]+):(?P<account_id>\d{{12}}):((?=key/)key/|(?=alias/))(?P<key_id>[^:]+)$"
10
18
  )
19
+ # special tag name to allow specifying a custom key material for created keys
20
+ TAG_KEY_CUSTOM_KEY_MATERIAL = "_custom_key_material_"
11
21
 
12
22
 
13
23
  def get_hash_algorithm(signing_algorithm: str) -> str:
@@ -60,13 +70,65 @@ def validate_tag(tag_position: int, tag: Tag) -> None:
60
70
  raise TagException("Tags beginning with aws: are reserved")
61
71
 
62
72
 
63
- def validate_tag_list(tag_list: TagList) -> None:
73
+ def validate_and_filter_tags(tag_list: TagList) -> TagList:
74
+ """
75
+ Validates tags and filters out LocalStack specific tags
76
+
77
+ :param tag_list: The list of tags provided to apply to the KMS key.
78
+ :returns: Filtered and validated list of tags to apply to the KMS key.
79
+ """
64
80
  unique_tag_keys = {tag["TagKey"] for tag in tag_list}
65
81
  if len(unique_tag_keys) < len(tag_list):
66
82
  raise TagException("Duplicate tag keys")
67
83
  if len(tag_list) > 50:
68
84
  raise TagException("Too many tags")
69
85
 
86
+ validated_tags = []
87
+ for i, tag in enumerate(tag_list, start=1):
88
+ if tag["TagKey"] != TAG_KEY_CUSTOM_KEY_MATERIAL:
89
+ validate_tag(i, tag)
90
+ validated_tags.append(tag)
91
+
92
+ return validated_tags
93
+
94
+
95
+ def get_custom_key_material(request: CreateKeyRequest | None) -> bytes | None:
96
+ """
97
+ Retrieves custom material which is sent in a CreateKeyRequest via the Tags.
98
+
99
+ :param request: The request for creating a KMS key.
100
+ :returns: Custom key material for the KMS key.
101
+ """
102
+ if not request:
103
+ return None
104
+
105
+ custom_key_material = None
106
+ tags = request.get("Tags", [])
107
+ for tag in tags:
108
+ if tag["TagKey"] == TAG_KEY_CUSTOM_KEY_MATERIAL:
109
+ custom_key_material = base64.b64decode(tag["TagValue"])
110
+
111
+ return custom_key_material
112
+
113
+
114
+ def get_custom_key_id(request: CreateKeyRequest | None) -> bytes | None:
115
+ """
116
+ Retrieves a custom Key ID for the KMS key which is sent in a CreateKeyRequest via the Tags.
117
+
118
+ :param request: The request for creating a KMS key.
119
+ :returns: THe custom Key ID for the KMS key.
120
+ """
121
+ if not request:
122
+ return None
123
+
124
+ custom_key_id = None
125
+ tags = request.get("Tags", [])
126
+ for tag in tags:
127
+ if tag["TagKey"] == TAG_KEY_CUSTOM_ID:
128
+ custom_key_id = tag["TagValue"]
129
+
130
+ return custom_key_id
131
+
70
132
 
71
133
  def execute_dry_run_capable[T](func: Callable[..., T], dry_run: bool, *args, **kwargs) -> T:
72
134
  """
@@ -6,7 +6,7 @@ from localstack.services.lambda_.invocation.lambda_models import (
6
6
  Layer,
7
7
  )
8
8
  from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
9
- from localstack.utils.tagging import TaggingService
9
+ from localstack.utils.tagging import Tags
10
10
 
11
11
 
12
12
  class LambdaStore(BaseStore):
@@ -26,7 +26,7 @@ class LambdaStore(BaseStore):
26
26
  capacity_providers: dict[str, CapacityProvider] = LocalAttribute(default=dict)
27
27
 
28
28
  # maps resource ARNs for EventSourceMappings and CodeSigningConfiguration to tags
29
- TAGS: TaggingService = LocalAttribute(default=TaggingService)
29
+ tags: Tags = LocalAttribute(default=Tags)
30
30
 
31
31
 
32
32
  lambda_stores = AccountRegionBundle("lambda", LambdaStore)
@@ -12,7 +12,7 @@ from localstack.utils.platform import get_arch
12
12
  """Customized LocalStack version of the AWS Lambda Runtime Interface Emulator (RIE).
13
13
  https://github.com/localstack/lambda-runtime-init/blob/localstack/README-LOCALSTACK.md
14
14
  """
15
- LAMBDA_RUNTIME_DEFAULT_VERSION = "v0.1.40-pre"
15
+ LAMBDA_RUNTIME_DEFAULT_VERSION = "v0.1.41-pre"
16
16
  LAMBDA_RUNTIME_VERSION = config.LAMBDA_INIT_RELEASE_VERSION or LAMBDA_RUNTIME_DEFAULT_VERSION
17
17
  LAMBDA_RUNTIME_INIT_URL = "https://github.com/localstack/lambda-runtime-init/releases/download/{version}/aws-lambda-rie-{arch}"
18
18
 
@@ -4415,28 +4415,25 @@ class LambdaProvider(LambdaApi, ServiceLifecycleHook):
4415
4415
  # only Function, Event Source Mapping, and Code Signing Config (not currently supported by LocalStack) ARNs
4416
4416
  # are available for tagging in AWS
4417
4417
 
4418
+ @staticmethod
4418
4419
  def _update_resource_tags(
4419
- self, resource_arn: str, account_id: str, region: str, tags: dict[str, str]
4420
+ resource_arn: str, account_id: str, region: str, tags: dict[str, str]
4420
4421
  ) -> None:
4421
- tagger_service = lambda_stores[account_id][region].TAGS
4422
- tag_svc_adapted_tags = [{"Key": key, "Value": value} for key, value in tags.items()]
4423
- tagger_service.tag_resource(resource_arn, tag_svc_adapted_tags)
4422
+ lambda_stores[account_id][region].tags.update_tags(resource_arn, tags)
4424
4423
 
4425
- def _list_resource_tags(
4426
- self, resource_arn: str, account_id: str, region: str
4427
- ) -> dict[str, str]:
4428
- tagger_service = lambda_stores[account_id][region].TAGS
4429
- return tagger_service.tags.get(resource_arn, {})
4424
+ @staticmethod
4425
+ def _list_resource_tags(resource_arn: str, account_id: str, region: str) -> dict[str, str]:
4426
+ return lambda_stores[account_id][region].tags.get_tags(resource_arn)
4430
4427
 
4428
+ @staticmethod
4431
4429
  def _remove_resource_tags(
4432
- self, resource_arn: str, account_id: str, region: str, keys: TagKeyList
4430
+ resource_arn: str, account_id: str, region: str, keys: TagKeyList
4433
4431
  ) -> None:
4434
- tagger_service = lambda_stores[account_id][region].TAGS
4435
- tagger_service.untag_resource(resource_arn, keys)
4432
+ lambda_stores[account_id][region].tags.delete_tags(resource_arn, keys)
4436
4433
 
4437
- def _remove_all_resource_tags(self, resource_arn: str, account_id: str, region: str) -> None:
4438
- tagger_service = lambda_stores[account_id][region].TAGS
4439
- return tagger_service.tags.pop(resource_arn, None)
4434
+ @staticmethod
4435
+ def _remove_all_resource_tags(resource_arn: str, account_id: str, region: str) -> None:
4436
+ lambda_stores[account_id][region].tags.delete_all_tags(resource_arn)
4440
4437
 
4441
4438
  def _get_tags(self, resource: TaggableResource) -> dict[str, str]:
4442
4439
  account_id, region = self._get_account_id_and_region_for_taggable_resource(resource)
@@ -2,18 +2,15 @@ from localstack.aws.api.opensearch import DomainStatus
2
2
  from localstack.services.stores import (
3
3
  AccountRegionBundle,
4
4
  BaseStore,
5
- CrossRegionAttribute,
6
5
  LocalAttribute,
7
6
  )
8
- from localstack.utils.tagging import TaggingService
7
+ from localstack.utils.tagging import Tags
9
8
 
10
9
 
11
10
  class OpenSearchStore(BaseStore):
12
11
  # storage for domain resources (access should be protected with the _domain_mutex)
13
12
  opensearch_domains: dict[str, DomainStatus] = LocalAttribute(default=dict)
14
-
15
- # static tagging service instance
16
- TAGS = CrossRegionAttribute(default=TaggingService)
13
+ tags: Tags = LocalAttribute(default=Tags)
17
14
 
18
15
 
19
16
  opensearch_stores = AccountRegionBundle("opensearch", OpenSearchStore)
@@ -518,22 +518,20 @@ class OpensearchProvider(OpensearchApi, ServiceLifecycleHook):
518
518
  cluster_manager().remove(DomainKey(domain_name, region, account_id).arn)
519
519
 
520
520
  def _add_tags(self, context: RequestContext, arn: ARN, tag_list: TagList) -> None:
521
- self.get_store(context.account_id, context.region).TAGS.tag_resource(arn, tag_list)
521
+ self.get_store(context.account_id, context.region).tags.update_tags(
522
+ arn, {tag["Key"]: tag["Value"] for tag in tag_list}
523
+ )
522
524
 
523
525
  def _remove_tags(self, context: RequestContext, arn: ARN, tag_keys: StringList) -> None:
524
- self.get_store(context.account_id, context.region).TAGS.untag_resource(arn, tag_keys)
526
+ self.get_store(context.account_id, context.region).tags.delete_tags(arn, tag_keys)
525
527
 
526
528
  def _remove_all_tags(self, context: RequestContext, arn: ARN) -> None:
527
- self.get_store(context.account_id, context.region).TAGS.del_resource(arn)
529
+ self.get_store(context.account_id, context.region).tags.delete_all_tags(arn)
528
530
 
529
531
  def _list_tags(self, context: RequestContext, arn: ARN) -> TagList:
530
- # The tagging service returns a dictionary with the given root name
531
532
  store = self.get_store(context.account_id, context.region)
532
- tags = store.TAGS.list_tags_for_resource(arn=arn, root_name="root")
533
- # Extract the actual list of tags for the typed response
534
- tag_list: TagList = tags["root"]
535
-
536
- return tag_list
533
+ tags = store.tags.get_tags(arn)
534
+ return [{"Key": key, "Value": value} for key, value in tags.items()]
537
535
 
538
536
  @handler("CreateDomain", expand=False)
539
537
  def create_domain(
@@ -1,10 +1,12 @@
1
1
  from localstack.aws.api.route53 import DelegationSet
2
2
  from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
3
+ from localstack.utils.tagging import Tags
3
4
 
4
5
 
5
6
  class Route53Store(BaseStore):
6
7
  # maps delegation set ID to reusable delegation set details
7
8
  reusable_delegation_sets: dict[str, DelegationSet] = LocalAttribute(default=dict)
9
+ tags: Tags = LocalAttribute(default=Tags)
8
10
 
9
11
 
10
- route53_stores = AccountRegionBundle("route53", Route53Store)
12
+ route53_stores = AccountRegionBundle("route53", Route53Store, validate=False)
@@ -27,6 +27,7 @@ from localstack.aws.api.route53 import (
27
27
  from localstack.aws.connect import connect_to
28
28
  from localstack.services.moto import call_moto
29
29
  from localstack.services.plugins import ServiceLifecycleHook
30
+ from localstack.services.route53.models import Route53Store, route53_stores
30
31
  from localstack.state import StateVisitor
31
32
 
32
33
 
@@ -37,6 +38,10 @@ class Route53Provider(Route53Api, ServiceLifecycleHook):
37
38
  visitor.visit(route53_backends)
38
39
  visitor.visit(route53_stores)
39
40
 
41
+ @staticmethod
42
+ def get_route53_store(account_id: str, region: str) -> Route53Store:
43
+ return route53_stores[account_id][region]
44
+
40
45
  # No tag deletion logic to handle in Community. Overwritten in Pro implementation.
41
46
  def remove_resource_tags(
42
47
  self, context: RequestContext, resource_type: str, resource_id: str
@@ -91,7 +91,7 @@ from localstack.services.stores import (
91
91
  LocalAttribute,
92
92
  )
93
93
  from localstack.utils.aws import arns
94
- from localstack.utils.tagging import TaggingService
94
+ from localstack.utils.tagging import Tags
95
95
 
96
96
  LOG = logging.getLogger(__name__)
97
97
 
@@ -763,9 +763,7 @@ class S3Store(BaseStore):
763
763
  buckets: dict[BucketName, S3Bucket] = CrossRegionAttribute(default=dict)
764
764
  global_bucket_map: dict[BucketName, AccountId] = CrossAccountAttribute(default=dict)
765
765
  aws_managed_kms_key_id: SSEKMSKeyId = LocalAttribute(default=str)
766
-
767
- # static tagging service instance
768
- TAGS: TaggingService = CrossAccountAttribute(default=TaggingService)
766
+ tags: Tags = LocalAttribute(default=Tags)
769
767
 
770
768
 
771
769
  class BucketCorsIndex: