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.
- localstack/aws/api/ec2/__init__.py +13 -0
- localstack/aws/api/iam/__init__.py +1 -0
- localstack/aws/api/lambda_/__init__.py +616 -0
- localstack/aws/api/logs/__init__.py +188 -0
- localstack/aws/api/opensearch/__init__.py +11 -0
- localstack/aws/api/route53/__init__.py +3 -0
- localstack/aws/api/s3/__init__.py +2 -0
- localstack/aws/api/s3control/__init__.py +19 -0
- localstack/aws/api/secretsmanager/__init__.py +9 -0
- localstack/aws/connect.py +35 -15
- localstack/aws/protocol/parser.py +6 -1
- localstack/aws/spec-patches.json +0 -38
- localstack/config.py +8 -0
- localstack/constants.py +3 -0
- localstack/dev/kubernetes/__main__.py +39 -14
- localstack/runtime/analytics.py +11 -0
- localstack/services/acm/provider.py +13 -1
- localstack/services/apigateway/legacy/provider.py +25 -4
- localstack/services/cloudformation/engine/v2/change_set_model.py +9 -0
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +3 -1
- localstack/services/cloudformation/engine/v2/change_set_resource_support_checker.py +114 -0
- localstack/services/cloudformation/provider.py +26 -1
- localstack/services/cloudformation/provider_utils.py +20 -0
- localstack/services/cloudformation/resource_provider.py +5 -4
- localstack/services/cloudformation/scaffolding/__main__.py +94 -22
- localstack/services/cloudformation/v2/provider.py +41 -0
- localstack/services/cloudwatch/models.py +10 -2
- localstack/services/cloudwatch/provider_v2.py +15 -20
- localstack/services/kinesis/packages.py +1 -1
- localstack/services/kms/models.py +6 -2
- localstack/services/lambda_/analytics.py +11 -2
- localstack/services/lambda_/invocation/event_manager.py +15 -11
- localstack/services/lambda_/invocation/lambda_models.py +4 -0
- localstack/services/lambda_/invocation/lambda_service.py +11 -0
- localstack/services/lambda_/provider.py +70 -13
- localstack/services/opensearch/packages.py +34 -20
- localstack/services/route53/provider.py +7 -0
- localstack/services/route53resolver/provider.py +5 -0
- localstack/services/s3/constants.py +5 -0
- localstack/services/s3/exceptions.py +9 -0
- localstack/services/s3/models.py +9 -1
- localstack/services/s3/provider.py +25 -30
- localstack/services/s3/utils.py +46 -1
- localstack/services/s3control/provider.py +6 -0
- localstack/services/scheduler/provider.py +4 -2
- localstack/services/secretsmanager/provider.py +4 -0
- localstack/services/ses/provider.py +4 -0
- localstack/services/sns/constants.py +13 -0
- localstack/services/sns/provider.py +5 -0
- localstack/services/sns/v2/models.py +4 -0
- localstack/services/sns/v2/provider.py +145 -0
- localstack/services/sqs/constants.py +6 -0
- localstack/services/sqs/provider.py +9 -1
- localstack/services/sqs/resource_providers/aws_sqs_queue.py +61 -46
- localstack/services/ssm/provider.py +6 -0
- localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +193 -107
- localstack/services/stepfunctions/backend/execution.py +4 -5
- localstack/services/stepfunctions/provider.py +21 -14
- localstack/services/sts/provider.py +7 -0
- localstack/services/support/provider.py +5 -1
- localstack/services/swf/provider.py +5 -1
- localstack/services/transcribe/provider.py +7 -0
- localstack/testing/aws/lambda_utils.py +1 -1
- localstack/testing/aws/util.py +2 -1
- localstack/testing/config.py +1 -0
- localstack/utils/aws/client_types.py +2 -4
- localstack/utils/bootstrap.py +2 -2
- localstack/utils/catalog/catalog.py +3 -2
- localstack/utils/container_utils/container_client.py +22 -13
- localstack/utils/container_utils/docker_cmd_client.py +6 -6
- localstack/version.py +2 -2
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/METADATA +6 -6
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/RECORD +81 -80
- localstack_core-4.12.1.dev25.dist-info/plux.json +1 -0
- localstack_core-4.11.2.dev14.dist-info/plux.json +0 -1
- {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack +0 -0
- {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/WHEEL +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
-
|
|
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
|
|
100
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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,
|