localstack-core 4.11.2.dev14__py3-none-any.whl → 4.12.1.dev25__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.

Potentially problematic release.


This version of localstack-core might be problematic. Click here for more details.

Files changed (82) hide show
  1. localstack/aws/api/ec2/__init__.py +13 -0
  2. localstack/aws/api/iam/__init__.py +1 -0
  3. localstack/aws/api/lambda_/__init__.py +616 -0
  4. localstack/aws/api/logs/__init__.py +188 -0
  5. localstack/aws/api/opensearch/__init__.py +11 -0
  6. localstack/aws/api/route53/__init__.py +3 -0
  7. localstack/aws/api/s3/__init__.py +2 -0
  8. localstack/aws/api/s3control/__init__.py +19 -0
  9. localstack/aws/api/secretsmanager/__init__.py +9 -0
  10. localstack/aws/connect.py +35 -15
  11. localstack/aws/protocol/parser.py +6 -1
  12. localstack/aws/spec-patches.json +0 -38
  13. localstack/config.py +8 -0
  14. localstack/constants.py +3 -0
  15. localstack/dev/kubernetes/__main__.py +39 -14
  16. localstack/runtime/analytics.py +11 -0
  17. localstack/services/acm/provider.py +13 -1
  18. localstack/services/apigateway/legacy/provider.py +25 -4
  19. localstack/services/cloudformation/engine/v2/change_set_model.py +9 -0
  20. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +3 -1
  21. localstack/services/cloudformation/engine/v2/change_set_resource_support_checker.py +114 -0
  22. localstack/services/cloudformation/provider.py +26 -1
  23. localstack/services/cloudformation/provider_utils.py +20 -0
  24. localstack/services/cloudformation/resource_provider.py +5 -4
  25. localstack/services/cloudformation/scaffolding/__main__.py +94 -22
  26. localstack/services/cloudformation/v2/provider.py +41 -0
  27. localstack/services/cloudwatch/models.py +10 -2
  28. localstack/services/cloudwatch/provider_v2.py +15 -20
  29. localstack/services/kinesis/packages.py +1 -1
  30. localstack/services/kms/models.py +6 -2
  31. localstack/services/lambda_/analytics.py +11 -2
  32. localstack/services/lambda_/invocation/event_manager.py +15 -11
  33. localstack/services/lambda_/invocation/lambda_models.py +4 -0
  34. localstack/services/lambda_/invocation/lambda_service.py +11 -0
  35. localstack/services/lambda_/provider.py +70 -13
  36. localstack/services/opensearch/packages.py +34 -20
  37. localstack/services/route53/provider.py +7 -0
  38. localstack/services/route53resolver/provider.py +5 -0
  39. localstack/services/s3/constants.py +5 -0
  40. localstack/services/s3/exceptions.py +9 -0
  41. localstack/services/s3/models.py +9 -1
  42. localstack/services/s3/provider.py +25 -30
  43. localstack/services/s3/utils.py +46 -1
  44. localstack/services/s3control/provider.py +6 -0
  45. localstack/services/scheduler/provider.py +4 -2
  46. localstack/services/secretsmanager/provider.py +4 -0
  47. localstack/services/ses/provider.py +4 -0
  48. localstack/services/sns/constants.py +13 -0
  49. localstack/services/sns/provider.py +5 -0
  50. localstack/services/sns/v2/models.py +4 -0
  51. localstack/services/sns/v2/provider.py +145 -0
  52. localstack/services/sqs/constants.py +6 -0
  53. localstack/services/sqs/provider.py +9 -1
  54. localstack/services/sqs/resource_providers/aws_sqs_queue.py +61 -46
  55. localstack/services/ssm/provider.py +6 -0
  56. localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +193 -107
  57. localstack/services/stepfunctions/backend/execution.py +4 -5
  58. localstack/services/stepfunctions/provider.py +21 -14
  59. localstack/services/sts/provider.py +7 -0
  60. localstack/services/support/provider.py +5 -1
  61. localstack/services/swf/provider.py +5 -1
  62. localstack/services/transcribe/provider.py +7 -0
  63. localstack/testing/aws/lambda_utils.py +1 -1
  64. localstack/testing/aws/util.py +2 -1
  65. localstack/testing/config.py +1 -0
  66. localstack/utils/aws/client_types.py +2 -4
  67. localstack/utils/bootstrap.py +2 -2
  68. localstack/utils/catalog/catalog.py +3 -2
  69. localstack/utils/container_utils/container_client.py +22 -13
  70. localstack/utils/container_utils/docker_cmd_client.py +6 -6
  71. localstack/version.py +2 -2
  72. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/METADATA +6 -6
  73. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/RECORD +81 -80
  74. localstack_core-4.12.1.dev25.dist-info/plux.json +1 -0
  75. localstack_core-4.11.2.dev14.dist-info/plux.json +0 -1
  76. {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack +0 -0
  77. {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack-supervisor +0 -0
  78. {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack.bat +0 -0
  79. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/WHEEL +0 -0
  80. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/entry_points.txt +0 -0
  81. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/licenses/LICENSE.txt +0 -0
  82. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/top_level.txt +0 -0
@@ -86,6 +86,7 @@ from localstack.utils.aws.arns import (
86
86
  from localstack.utils.collections import PaginatedList, select_from_typed_dict
87
87
  from localstack.utils.strings import short_uid, to_bytes, to_str
88
88
 
89
+ from ...state import StateVisitor
89
90
  from .analytics import internal_api_calls
90
91
 
91
92
  # set up logger
@@ -118,6 +119,10 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
118
119
  self._publisher = PublishDispatcher()
119
120
  self._signature_cert_pem: str = SNS_SERVER_CERT
120
121
 
122
+ def accept_state_visitor(self, visitor: StateVisitor):
123
+ visitor.visit(sns_backends)
124
+ visitor.visit(sns_stores)
125
+
121
126
  def on_before_stop(self):
122
127
  self._publisher.shutdown()
123
128
 
@@ -7,6 +7,7 @@ from typing import Literal, TypedDict
7
7
  from localstack.aws.api.sns import (
8
8
  Endpoint,
9
9
  MessageAttributeMap,
10
+ PhoneNumber,
10
11
  PlatformApplication,
11
12
  PublishBatchRequestEntry,
12
13
  TopicAttributesMap,
@@ -28,6 +29,7 @@ class Topic(TypedDict, total=True):
28
29
  arn: str
29
30
  name: str
30
31
  attributes: TopicAttributesMap
32
+ data_protection_policy: str
31
33
  subscriptions: list[str]
32
34
 
33
35
 
@@ -192,5 +194,7 @@ class SnsStore(BaseStore):
192
194
 
193
195
  TAGS: TaggingService = CrossRegionAttribute(default=TaggingService)
194
196
 
197
+ PHONE_NUMBERS_OPTED_OUT: list[PhoneNumber] = CrossRegionAttribute(default=list)
198
+
195
199
 
196
200
  sns_stores = AccountRegionBundle("sns", SnsStore)
@@ -10,14 +10,18 @@ from rolo import Request, Router, route
10
10
 
11
11
  from localstack.aws.api import CommonServiceException, RequestContext
12
12
  from localstack.aws.api.sns import (
13
+ ActionsList,
13
14
  AmazonResourceName,
14
15
  BatchEntryIdsNotDistinctException,
16
+ CheckIfPhoneNumberIsOptedOutResponse,
15
17
  ConfirmSubscriptionResponse,
16
18
  CreateEndpointResponse,
17
19
  CreatePlatformApplicationResponse,
18
20
  CreateTopicResponse,
21
+ DelegatesList,
19
22
  Endpoint,
20
23
  EndpointDisabledException,
24
+ GetDataProtectionPolicyResponse,
21
25
  GetEndpointAttributesResponse,
22
26
  GetPlatformApplicationAttributesResponse,
23
27
  GetSMSAttributesResponse,
@@ -26,6 +30,7 @@ from localstack.aws.api.sns import (
26
30
  InvalidParameterException,
27
31
  InvalidParameterValueException,
28
32
  ListEndpointsByPlatformApplicationResponse,
33
+ ListPhoneNumbersOptedOutResponse,
29
34
  ListPlatformApplicationsResponse,
30
35
  ListString,
31
36
  ListSubscriptionsByTopicResponse,
@@ -35,6 +40,7 @@ from localstack.aws.api.sns import (
35
40
  MapStringToString,
36
41
  MessageAttributeMap,
37
42
  NotFoundException,
43
+ OptInPhoneNumberResponse,
38
44
  PhoneNumber,
39
45
  PlatformApplication,
40
46
  PublishBatchRequestEntryList,
@@ -57,10 +63,12 @@ from localstack.aws.api.sns import (
57
63
  attributeValue,
58
64
  authenticateOnUnsubscribe,
59
65
  endpoint,
66
+ label,
60
67
  message,
61
68
  messageStructure,
62
69
  nextToken,
63
70
  protocol,
71
+ string,
64
72
  subject,
65
73
  subscriptionARN,
66
74
  topicARN,
@@ -84,6 +92,7 @@ from localstack.services.sns.constants import (
84
92
  SUBSCRIPTION_TOKENS_ENDPOINT,
85
93
  VALID_APPLICATION_PLATFORMS,
86
94
  VALID_MSG_ATTR_NAME_CHARS,
95
+ VALID_POLICY_ACTIONS,
87
96
  VALID_SUBSCRIPTION_ATTR_NAME,
88
97
  )
89
98
  from localstack.services.sns.filter import FilterPolicyValidator
@@ -118,6 +127,7 @@ from localstack.services.sns.v2.utils import (
118
127
  parse_and_validate_topic_arn,
119
128
  validate_subscription_attribute,
120
129
  )
130
+ from localstack.state import StateVisitor
121
131
  from localstack.utils.aws.arns import (
122
132
  extract_account_id_from_arn,
123
133
  extract_region_from_arn,
@@ -142,6 +152,9 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
142
152
  self._publisher = PublishDispatcher()
143
153
  self._signature_cert_pem: str = SNS_SERVER_CERT
144
154
 
155
+ def accept_state_visitor(self, visitor: StateVisitor):
156
+ visitor.visit(sns_stores)
157
+
145
158
  def on_before_stop(self):
146
159
  self._publisher.shutdown()
147
160
 
@@ -202,6 +215,8 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
202
215
  if not name_match:
203
216
  raise InvalidParameterException("Invalid parameter: Topic Name")
204
217
 
218
+ attributes["EffectiveDeliveryPolicy"] = _create_default_effective_delivery_policy()
219
+
205
220
  topic = _create_topic(name=name, attributes=attributes, context=context)
206
221
  if tags:
207
222
  self.tag_resource(context=context, resource_arn=topic_arn, tags=tags)
@@ -1073,6 +1088,116 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
1073
1088
 
1074
1089
  return GetSMSAttributesResponse(attributes=return_attributes)
1075
1090
 
1091
+ #
1092
+ # Phone number operations
1093
+ #
1094
+
1095
+ def check_if_phone_number_is_opted_out(
1096
+ self, context: RequestContext, phone_number: PhoneNumber, **kwargs
1097
+ ) -> CheckIfPhoneNumberIsOptedOutResponse:
1098
+ store = sns_stores[context.account_id][context.region]
1099
+ return CheckIfPhoneNumberIsOptedOutResponse(
1100
+ isOptedOut=phone_number in store.PHONE_NUMBERS_OPTED_OUT
1101
+ )
1102
+
1103
+ def list_phone_numbers_opted_out(
1104
+ self, context: RequestContext, next_token: string | None = None, **kwargs
1105
+ ) -> ListPhoneNumbersOptedOutResponse:
1106
+ store = self.get_store(context.account_id, context.region)
1107
+ numbers_opted_out = PaginatedList(store.PHONE_NUMBERS_OPTED_OUT)
1108
+ page, nxt = numbers_opted_out.get_page(
1109
+ token_generator=lambda x: x,
1110
+ next_token=next_token,
1111
+ page_size=100,
1112
+ )
1113
+ phone_numbers = {"phoneNumbers": page, "nextToken": nxt}
1114
+ return ListPhoneNumbersOptedOutResponse(**phone_numbers)
1115
+
1116
+ def opt_in_phone_number(
1117
+ self, context: RequestContext, phone_number: PhoneNumber, **kwargs
1118
+ ) -> OptInPhoneNumberResponse:
1119
+ store = self.get_store(context.account_id, context.region)
1120
+ if phone_number in store.PHONE_NUMBERS_OPTED_OUT:
1121
+ store.PHONE_NUMBERS_OPTED_OUT.remove(phone_number)
1122
+ return OptInPhoneNumberResponse()
1123
+
1124
+ #
1125
+ # Permission operations
1126
+ #
1127
+
1128
+ def add_permission(
1129
+ self,
1130
+ context: RequestContext,
1131
+ topic_arn: topicARN,
1132
+ label: label,
1133
+ aws_account_id: DelegatesList,
1134
+ action_name: ActionsList,
1135
+ **kwargs,
1136
+ ) -> None:
1137
+ topic: Topic = self._get_topic(topic_arn, context)
1138
+ policy = json.loads(topic["attributes"]["Policy"])
1139
+ statement = next(
1140
+ (statement for statement in policy["Statement"] if statement["Sid"] == label),
1141
+ None,
1142
+ )
1143
+
1144
+ if statement:
1145
+ raise InvalidParameterException("Invalid parameter: Statement already exists")
1146
+
1147
+ if any(action not in VALID_POLICY_ACTIONS for action in action_name):
1148
+ raise InvalidParameterException(
1149
+ "Invalid parameter: Policy statement action out of service scope!"
1150
+ )
1151
+
1152
+ principals = [
1153
+ f"arn:{get_partition(context.region)}:iam::{account_id}:root"
1154
+ for account_id in aws_account_id
1155
+ ]
1156
+ actions = [f"SNS:{action}" for action in action_name]
1157
+
1158
+ statement = {
1159
+ "Sid": label,
1160
+ "Effect": "Allow",
1161
+ "Principal": {"AWS": principals[0] if len(principals) == 1 else principals},
1162
+ "Action": actions[0] if len(actions) == 1 else actions,
1163
+ "Resource": topic_arn,
1164
+ }
1165
+
1166
+ policy["Statement"].append(statement)
1167
+ topic["attributes"]["Policy"] = json.dumps(policy)
1168
+
1169
+ def remove_permission(
1170
+ self, context: RequestContext, topic_arn: topicARN, label: label, **kwargs
1171
+ ) -> None:
1172
+ topic = self._get_topic(topic_arn, context)
1173
+ policy = json.loads(topic["attributes"]["Policy"])
1174
+ statements = policy["Statement"]
1175
+ statements = [statement for statement in statements if statement["Sid"] != label]
1176
+ policy["Statement"] = statements
1177
+ topic["attributes"]["Policy"] = json.dumps(policy)
1178
+
1179
+ #
1180
+ # Data Protection Policy operations
1181
+ #
1182
+
1183
+ def get_data_protection_policy(
1184
+ self, context: RequestContext, resource_arn: topicARN, **kwargs
1185
+ ) -> GetDataProtectionPolicyResponse:
1186
+ topic = self._get_topic(resource_arn, context)
1187
+ return GetDataProtectionPolicyResponse(
1188
+ DataProtectionPolicy=topic.get("data_protection_policy")
1189
+ )
1190
+
1191
+ def put_data_protection_policy(
1192
+ self,
1193
+ context: RequestContext,
1194
+ resource_arn: topicARN,
1195
+ data_protection_policy: attributeValue,
1196
+ **kwargs,
1197
+ ) -> None:
1198
+ topic = self._get_topic(resource_arn, context)
1199
+ topic["data_protection_policy"] = data_protection_policy
1200
+
1076
1201
  def list_tags_for_resource(
1077
1202
  self, context: RequestContext, resource_arn: AmazonResourceName, **kwargs
1078
1203
  ) -> ListTagsForResourceResponse:
@@ -1170,6 +1295,26 @@ def _default_attributes(topic: Topic, context: RequestContext) -> TopicAttribute
1170
1295
  return default_attributes
1171
1296
 
1172
1297
 
1298
+ def _create_default_effective_delivery_policy():
1299
+ return json.dumps(
1300
+ {
1301
+ "http": {
1302
+ "defaultHealthyRetryPolicy": {
1303
+ "minDelayTarget": 20,
1304
+ "maxDelayTarget": 20,
1305
+ "numRetries": 3,
1306
+ "numMaxDelayRetries": 0,
1307
+ "numNoDelayRetries": 0,
1308
+ "numMinDelayRetries": 0,
1309
+ "backoffFunction": "linear",
1310
+ },
1311
+ "disableSubscriptionOverrides": False,
1312
+ "defaultRequestPolicy": {"headerContentType": "text/plain; charset=UTF-8"},
1313
+ }
1314
+ }
1315
+ )
1316
+
1317
+
1173
1318
  def _create_default_topic_policy(topic: Topic, context: RequestContext) -> str:
1174
1319
  return json.dumps(
1175
1320
  {
@@ -31,6 +31,12 @@ INTERNAL_QUEUE_ATTRIBUTES = [
31
31
  QueueAttributeName.QueueArn,
32
32
  ]
33
33
 
34
+ #
35
+ # If these attributes are set to their default values, they are effectively
36
+ # deleted from the queue attributes and not returned in future calls to get_queue_attributes()
37
+ #
38
+ DELETE_IF_DEFAULT = {"KmsMasterKeyId": "", "KmsDataKeyReusePeriodSeconds": "300"}
39
+
34
40
  INVALID_STANDARD_QUEUE_ATTRIBUTES = [
35
41
  QueueAttributeName.FifoQueue,
36
42
  QueueAttributeName.ContentBasedDeduplication,
@@ -101,6 +101,7 @@ from localstack.services.sqs.utils import (
101
101
  parse_queue_url,
102
102
  )
103
103
  from localstack.services.stores import AccountRegionBundle
104
+ from localstack.state import StateVisitor
104
105
  from localstack.utils.aws.arns import parse_arn
105
106
  from localstack.utils.bootstrap import is_api_enabled
106
107
  from localstack.utils.cloudwatch.cloudwatch_util import (
@@ -659,6 +660,9 @@ class SqsProvider(SqsApi, ServiceLifecycleHook):
659
660
  self._router_rules = []
660
661
  self._init_cloudwatch_metrics_reporting()
661
662
 
663
+ def accept_state_visitor(self, visitor: StateVisitor):
664
+ visitor.visit(sqs_stores)
665
+
662
666
  @staticmethod
663
667
  def get_store(account_id: str, region: str) -> SqsStore:
664
668
  return sqs_stores[account_id][region]
@@ -1269,7 +1273,11 @@ class SqsProvider(SqsApi, ServiceLifecycleHook):
1269
1273
  for k, v in attributes.items():
1270
1274
  if k in sqs_constants.INTERNAL_QUEUE_ATTRIBUTES:
1271
1275
  raise InvalidAttributeName(f"Unknown Attribute {k}.")
1272
- queue.attributes[k] = v
1276
+ if k in sqs_constants.DELETE_IF_DEFAULT and v == sqs_constants.DELETE_IF_DEFAULT[k]:
1277
+ if k in queue.attributes:
1278
+ del queue.attributes[k]
1279
+ else:
1280
+ queue.attributes[k] = v
1273
1281
 
1274
1282
  # Special cases
1275
1283
  if queue.attributes.get(QueueAttributeName.Policy) == "":
@@ -64,6 +64,35 @@ class SQSQueueProvider(ResourceProvider[SQSQueueProperties]):
64
64
  TYPE = "AWS::SQS::Queue" # Autogenerated. Don't change
65
65
  SCHEMA = util.get_schema_path(Path(__file__)) # Autogenerated. Don't change
66
66
 
67
+ # Values used when a property is removed from a template and needs to be set to its default.
68
+ # If AWS changes their defaults in the future, our parity tests should break.
69
+ DEFAULT_ATTRIBUTE_VALUES = {
70
+ "ReceiveMessageWaitTimeSeconds": "0",
71
+ "DelaySeconds": "0",
72
+ "KmsMasterKeyId": "",
73
+ "RedrivePolicy": "",
74
+ "MessageRetentionPeriod": "345600",
75
+ "MaximumMessageSize": "262144", # Note: CloudFormation sets this to 256KB on update, but 1MB on create
76
+ "VisibilityTimeout": "30",
77
+ "KmsDataKeyReusePeriodSeconds": "300",
78
+ }
79
+
80
+ # Private method for creating a unique queue name, if none is specified.
81
+ def _autogenerated_queue_name(self, request: ResourceRequest[SQSQueueProperties]) -> str:
82
+ queue_name = util.generate_default_name(request.stack_name, request.logical_resource_id)
83
+ isFifoQueue = request.desired_state.get("FifoQueue")
84
+
85
+ # Note that it's an SQS FIFO queue only if the FifoQueue property is set to boolean True, or the string "true"
86
+ # (case insensitive). If it's None (property was omitted) or False, or any type of string (e.g. a typo
87
+ # such as "Fasle"), then it's not a FIFO queue. This extra check is needed because the CloudFormation engine
88
+ # doesn't fully validate the FifoQueue property before passing it to the resource provider.
89
+ if (
90
+ isFifoQueue == True # noqa: E712
91
+ or (isinstance(isFifoQueue, str) and isFifoQueue.lower() == "true")
92
+ ):
93
+ queue_name = f"{queue_name[:-5]}.fifo"
94
+ return queue_name
95
+
67
96
  def create(
68
97
  self,
69
98
  request: ResourceRequest[SQSQueueProperties],
@@ -74,8 +103,6 @@ class SQSQueueProvider(ResourceProvider[SQSQueueProperties]):
74
103
  Primary identifier fields:
75
104
  - /properties/QueueUrl
76
105
 
77
-
78
-
79
106
  Create-only properties:
80
107
  - /properties/FifoQueue
81
108
  - /properties/QueueName
@@ -92,26 +119,13 @@ class SQSQueueProvider(ResourceProvider[SQSQueueProperties]):
92
119
  - sqs:TagQueue
93
120
 
94
121
  """
95
- # TODO: validations
122
+ # TODO: validations - what validations are needed?
96
123
  model = request.desired_state
97
124
  sqs = request.aws_client_factory.sqs
98
125
 
99
- if model.get("FifoQueue", False):
100
- model["FifoQueue"] = model["FifoQueue"]
101
-
102
- queue_name = model.get("QueueName")
103
- if not queue_name:
104
- # TODO: verify patterns here
105
- if model.get("FifoQueue"):
106
- queue_name = util.generate_default_name(
107
- request.stack_name, request.logical_resource_id
108
- )[:-5]
109
- queue_name = f"{queue_name}.fifo"
110
- else:
111
- queue_name = util.generate_default_name(
112
- request.stack_name, request.logical_resource_id
113
- )
114
- model["QueueName"] = queue_name
126
+ # if no QueueName is specified, automatically generate one
127
+ if not model.get("QueueName"):
128
+ model["QueueName"] = self._autogenerated_queue_name(request)
115
129
 
116
130
  attributes = self._compile_sqs_queue_attributes(model)
117
131
  result = request.aws_client_factory.sqs.create_queue(
@@ -184,38 +198,30 @@ class SQSQueueProvider(ResourceProvider[SQSQueueProperties]):
184
198
  """
185
199
  sqs = request.aws_client_factory.sqs
186
200
  model = request.desired_state
201
+ prev_model = request.previous_state
187
202
 
188
203
  assert request.previous_state is not None
189
204
 
190
- should_replace = (
191
- request.desired_state.get("QueueName", request.previous_state["QueueName"])
192
- != request.previous_state["QueueName"]
193
- ) or (
194
- request.desired_state.get("FifoQueue", request.previous_state.get("FifoQueue"))
195
- != request.previous_state.get("FifoQueue")
205
+ queue_url = prev_model["QueueUrl"]
206
+ self._populate_missing_attributes_with_defaults(model)
207
+ sqs.set_queue_attributes(
208
+ QueueUrl=queue_url, Attributes=self._compile_sqs_queue_attributes(model)
196
209
  )
197
210
 
198
- if not should_replace:
199
- return ProgressEvent(OperationStatus.SUCCESS, resource_model=request.previous_state)
200
-
201
- # TODO: copied from the create handler, extract?
202
- if model.get("FifoQueue"):
203
- queue_name = util.generate_default_name(
204
- request.stack_name, request.logical_resource_id
205
- )[:-5]
206
- queue_name = f"{queue_name}.fifo"
207
- else:
208
- queue_name = util.generate_default_name(request.stack_name, request.logical_resource_id)
209
-
210
- # replacement (TODO: find out if we should handle this in the provider or outside of it)
211
- # delete old queue
212
- sqs.delete_queue(QueueUrl=request.previous_state["QueueUrl"])
213
- # create new queue (TODO: re-use create logic to make this more robust, e.g. for
214
- # auto-generated queue names)
215
- model["QueueUrl"] = sqs.create_queue(QueueName=queue_name)["QueueUrl"]
216
- model["Arn"] = sqs.get_queue_attributes(
217
- QueueUrl=model["QueueUrl"], AttributeNames=["QueueArn"]
218
- )["Attributes"]["QueueArn"]
211
+ (tags_to_remove, tags_to_add_or_update) = util.resource_tags_to_remove_or_update(
212
+ prev_model.get("Tags", []), model.get("Tags", [])
213
+ )
214
+ sqs.untag_queue(QueueUrl=queue_url, TagKeys=tags_to_remove)
215
+ sqs.tag_queue(QueueUrl=queue_url, Tags=tags_to_add_or_update)
216
+
217
+ model["QueueUrl"] = queue_url
218
+ model["Arn"] = request.previous_state["Arn"]
219
+
220
+ # For QueueName and FifoQueue, always use the value from the previous model. These fields
221
+ # are create-only, so they cannot be changed via an update (even though they might be omitted)
222
+ model["QueueName"] = prev_model.get("QueueName")
223
+ model["FifoQueue"] = prev_model.get("FifoQueue", False)
224
+
219
225
  return ProgressEvent(OperationStatus.SUCCESS, resource_model=model)
220
226
 
221
227
  def _compile_sqs_queue_attributes(self, properties: SQSQueueProperties) -> dict[str, str]:
@@ -250,6 +256,15 @@ class SQSQueueProvider(ResourceProvider[SQSQueueProperties]):
250
256
 
251
257
  return result
252
258
 
259
+ def _populate_missing_attributes_with_defaults(self, properties: SQSQueueProperties) -> None:
260
+ """
261
+ For any attribute that is missing from the desired state, populate it with the default value.
262
+ This is the only way to remove an attribute from an existing SQS queue's configuration.
263
+ :param properties: the properties passed from cloudformation
264
+ """
265
+ for k, v in self.DEFAULT_ATTRIBUTE_VALUES.items():
266
+ properties.setdefault(k, v)
267
+
253
268
  def list(
254
269
  self,
255
270
  request: ResourceRequest[SQSQueueProperties],
@@ -78,6 +78,7 @@ from localstack.aws.api.ssm import (
78
78
  )
79
79
  from localstack.aws.connect import connect_to
80
80
  from localstack.services.moto import call_moto, call_moto_with_request
81
+ from localstack.state import StateVisitor
81
82
  from localstack.utils.aws.arns import extract_resource_from_arn, is_arn
82
83
  from localstack.utils.bootstrap import is_api_enabled
83
84
  from localstack.utils.collections import remove_attributes
@@ -105,6 +106,11 @@ class InvalidParameterNameException(ValidationException):
105
106
 
106
107
  # TODO: check if _normalize_name(..) calls are still required here
107
108
  class SsmProvider(SsmApi, ABC):
109
+ def accept_state_visitor(self, visitor: StateVisitor):
110
+ from moto.ssm.models import ssm_backends
111
+
112
+ visitor.visit(ssm_backends)
113
+
108
114
  def get_parameters(
109
115
  self,
110
116
  context: RequestContext,