localstack-core 4.11.2.dev14__py3-none-any.whl → 4.12.1.dev18__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/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/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/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 +3 -0
- localstack/services/sns/v2/provider.py +100 -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.dev18.dist-info}/METADATA +6 -6
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/RECORD +76 -75
- localstack_core-4.12.1.dev18.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.dev18.data}/scripts/localstack +0 -0
- {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev18.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev18.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/WHEEL +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/top_level.txt +0 -0
|
@@ -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,
|
|
@@ -12,18 +12,35 @@ from botocore.model import (
|
|
|
12
12
|
|
|
13
13
|
from localstack.aws.api.stepfunctions import (
|
|
14
14
|
Definition,
|
|
15
|
+
InvalidDefinition,
|
|
15
16
|
MockInput,
|
|
16
17
|
MockResponseValidationMode,
|
|
17
18
|
StateName,
|
|
19
|
+
TestStateInput,
|
|
18
20
|
ValidationException,
|
|
19
21
|
)
|
|
20
22
|
from localstack.services.stepfunctions.asl.antlr.runtime.ASLParser import ASLParser
|
|
23
|
+
from localstack.services.stepfunctions.asl.component.state.state import CommonStateField
|
|
24
|
+
from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.state_map import (
|
|
25
|
+
StateMap,
|
|
26
|
+
)
|
|
27
|
+
from localstack.services.stepfunctions.asl.component.state.state_execution.state_parallel.state_parallel import (
|
|
28
|
+
StateParallel,
|
|
29
|
+
)
|
|
21
30
|
from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service import (
|
|
22
31
|
StateTaskService,
|
|
23
32
|
)
|
|
24
33
|
from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service_api_gateway import (
|
|
25
34
|
StateTaskServiceApiGateway,
|
|
26
35
|
)
|
|
36
|
+
from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.state_task import (
|
|
37
|
+
StateTask,
|
|
38
|
+
)
|
|
39
|
+
from localstack.services.stepfunctions.asl.component.state.state_fail.state_fail import StateFail
|
|
40
|
+
from localstack.services.stepfunctions.asl.component.state.state_pass.state_pass import StatePass
|
|
41
|
+
from localstack.services.stepfunctions.asl.component.state.state_succeed.state_succeed import (
|
|
42
|
+
StateSucceed,
|
|
43
|
+
)
|
|
27
44
|
from localstack.services.stepfunctions.asl.component.state.state_type import StateType
|
|
28
45
|
from localstack.services.stepfunctions.asl.component.test_state.program.test_state_program import (
|
|
29
46
|
TestStateProgram,
|
|
@@ -59,10 +76,92 @@ class TestStateStaticAnalyser(StaticAnalyser):
|
|
|
59
76
|
return test_program.test_state is not None
|
|
60
77
|
|
|
61
78
|
@staticmethod
|
|
62
|
-
def
|
|
79
|
+
def validate_role_arn_required(
|
|
80
|
+
mock_input: MockInput, definition: Definition, state_name: StateName
|
|
81
|
+
) -> None:
|
|
63
82
|
test_program, _ = TestStateAmazonStateLanguageParser.parse(definition, state_name)
|
|
64
83
|
test_state = test_program.test_state
|
|
84
|
+
if isinstance(test_state, StateTask) and mock_input is None:
|
|
85
|
+
raise ValidationException("RoleArn must be specified when testing a Task state")
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def validate_mock(test_state_input: TestStateInput) -> None:
|
|
89
|
+
test_program, _ = TestStateAmazonStateLanguageParser.parse(
|
|
90
|
+
test_state_input.get("definition"), test_state_input.get("stateName")
|
|
91
|
+
)
|
|
92
|
+
test_state = test_program.test_state
|
|
93
|
+
mock_input = test_state_input.get("mock")
|
|
94
|
+
|
|
95
|
+
TestStateStaticAnalyser.validate_test_state_allows_mocking(
|
|
96
|
+
mock_input=mock_input, test_state=test_state
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if mock_input is None:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
if test_state_input.get("revealSecrets"):
|
|
103
|
+
raise ValidationException(
|
|
104
|
+
"TestState does not support RevealSecrets when a mock is specified."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if {"result", "errorOutput"} <= mock_input.keys():
|
|
108
|
+
raise ValidationException(
|
|
109
|
+
"A test mock should have only one of the following fields: [result, errorOutput]."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
mock_result_raw = mock_input.get("result")
|
|
113
|
+
if mock_result_raw is None:
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
mock_result = json.loads(mock_result_raw)
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
raise ValidationException("Mocked result must be valid JSON")
|
|
119
|
+
|
|
120
|
+
if isinstance(test_state, StateMap):
|
|
121
|
+
TestStateStaticAnalyser.validate_mock_result_matches_map_definition(
|
|
122
|
+
mock_result=mock_result, test_state=test_state
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if isinstance(test_state, StateTaskService):
|
|
126
|
+
field_validation_mode = mock_input.get(
|
|
127
|
+
"fieldValidationMode", MockResponseValidationMode.STRICT
|
|
128
|
+
)
|
|
129
|
+
TestStateStaticAnalyser.validate_mock_result_matches_api_shape(
|
|
130
|
+
mock_result=mock_result,
|
|
131
|
+
field_validation_mode=field_validation_mode,
|
|
132
|
+
test_state=test_state,
|
|
133
|
+
)
|
|
65
134
|
|
|
135
|
+
@staticmethod
|
|
136
|
+
def validate_test_state_allows_mocking(
|
|
137
|
+
mock_input: MockInput, test_state: CommonStateField
|
|
138
|
+
) -> None:
|
|
139
|
+
if mock_input is None and isinstance(test_state, (StateMap, StateParallel)):
|
|
140
|
+
# This is a literal message when a Map or Parallel state is not accompanied by a mock in a test state request.
|
|
141
|
+
# The message is the same for both cases and is not parametrised anyhow.
|
|
142
|
+
raise InvalidDefinition(
|
|
143
|
+
"TestState API does not support Map or Parallel states. Supported state types include: [Task, Wait, Pass, Succeed, Fail, Choice]"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if mock_input is not None and isinstance(test_state, (StatePass, StateFail, StateSucceed)):
|
|
147
|
+
raise ValidationException(
|
|
148
|
+
f"State type '{test_state.state_type.name}' is not supported when a mock is specified"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def validate_mock_result_matches_map_definition(mock_result: Any, test_state: StateMap):
|
|
153
|
+
if test_state.result_writer is not None and not isinstance(mock_result, dict):
|
|
154
|
+
raise ValidationException("Mocked result must be a JSON object.")
|
|
155
|
+
|
|
156
|
+
if test_state.result_writer is None and not isinstance(mock_result, list):
|
|
157
|
+
raise ValidationException("Mocked result must be an array.")
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def validate_mock_result_matches_api_shape(
|
|
161
|
+
mock_result: Any,
|
|
162
|
+
field_validation_mode: MockResponseValidationMode,
|
|
163
|
+
test_state: StateTaskService,
|
|
164
|
+
):
|
|
66
165
|
# apigateway:invoke: has no equivalent in the AWS SDK service integration.
|
|
67
166
|
# Hence, the validation against botocore doesn't apply.
|
|
68
167
|
# See the note in https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html
|
|
@@ -70,119 +169,106 @@ class TestStateStaticAnalyser(StaticAnalyser):
|
|
|
70
169
|
if isinstance(test_state, StateTaskServiceApiGateway):
|
|
71
170
|
return
|
|
72
171
|
|
|
73
|
-
if
|
|
74
|
-
|
|
75
|
-
|
|
172
|
+
if field_validation_mode == MockResponseValidationMode.NONE:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
boto_service_name = test_state._get_boto_service_name()
|
|
176
|
+
service_action_name = test_state._get_boto_service_action()
|
|
177
|
+
output_shape = test_state._get_boto_operation_model(
|
|
178
|
+
boto_service_name=boto_service_name, service_action_name=service_action_name
|
|
179
|
+
).output_shape
|
|
180
|
+
|
|
181
|
+
# If the operation has no output, there's nothing to validate
|
|
182
|
+
if output_shape is None:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
def _raise_type_error(expected_type: str, field_name: str) -> None:
|
|
186
|
+
raise ValidationException(
|
|
187
|
+
f"Mock result schema validation error: Field '{field_name}' must be {expected_type}"
|
|
76
188
|
)
|
|
77
|
-
|
|
78
|
-
|
|
189
|
+
|
|
190
|
+
def _validate_value(value: Any, shape: Shape, field_name: str | None = None) -> None:
|
|
191
|
+
# Document type accepts any JSON value
|
|
192
|
+
if shape.type_name == "document":
|
|
79
193
|
return
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
194
|
+
|
|
195
|
+
if isinstance(shape, StructureShape):
|
|
196
|
+
if not isinstance(value, dict):
|
|
197
|
+
# this is a defensive check, the mock result is loaded from JSON before, so should always be a dict
|
|
198
|
+
raise ValidationException(
|
|
199
|
+
f"Mock result must be a valid JSON object, but got '{type(value)}' instead"
|
|
200
|
+
)
|
|
201
|
+
# Build a mapping from SFN-normalised member keys -> botocore member shapes
|
|
202
|
+
members = shape.members
|
|
203
|
+
sfn_key_to_member_shape: dict[str, Shape] = {
|
|
204
|
+
StateTaskService._to_sfn_cased(member_key): member_shape
|
|
205
|
+
for member_key, member_shape in members.items()
|
|
206
|
+
}
|
|
207
|
+
if field_validation_mode == MockResponseValidationMode.STRICT:
|
|
208
|
+
# Ensure required members are present, using SFN-normalised keys
|
|
209
|
+
for required_key in shape.required_members:
|
|
210
|
+
sfn_required_key = StateTaskService._to_sfn_cased(required_key)
|
|
211
|
+
if sfn_required_key not in value:
|
|
212
|
+
raise ValidationException(
|
|
213
|
+
f"Mock result schema validation error: Required field '{sfn_required_key}' is missing"
|
|
214
|
+
)
|
|
215
|
+
# Validate present fields (match SFN-normalised keys to member shapes)
|
|
216
|
+
for mock_field_name, mock_field_value in value.items():
|
|
217
|
+
member_shape = sfn_key_to_member_shape.get(mock_field_name)
|
|
218
|
+
if member_shape is None:
|
|
219
|
+
# Fields that are present in mock but are not in the API spec should not raise validation errors - forward compatibility
|
|
220
|
+
continue
|
|
221
|
+
_validate_value(mock_field_value, member_shape, mock_field_name)
|
|
85
222
|
return
|
|
86
223
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
224
|
+
if isinstance(shape, ListShape):
|
|
225
|
+
if not isinstance(value, list):
|
|
226
|
+
_raise_type_error("an array", field_name)
|
|
227
|
+
member_shape = shape.member
|
|
228
|
+
for list_item in value:
|
|
229
|
+
_validate_value(list_item, member_shape, field_name)
|
|
230
|
+
return
|
|
92
231
|
|
|
93
|
-
|
|
94
|
-
|
|
232
|
+
if isinstance(shape, MapShape):
|
|
233
|
+
if not isinstance(value, dict):
|
|
234
|
+
_raise_type_error("an object", field_name)
|
|
235
|
+
value_shape = shape.value
|
|
236
|
+
for _, map_item_value in value.items():
|
|
237
|
+
_validate_value(map_item_value, value_shape, field_name)
|
|
95
238
|
return
|
|
96
239
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
member_shape = sfn_key_to_member_shape.get(mock_field_name)
|
|
130
|
-
if member_shape is None:
|
|
131
|
-
# Fields that are present in mock but are not in the API spec should not raise validation errors - forward compatibility
|
|
132
|
-
continue
|
|
133
|
-
_validate_value(mock_field_value, member_shape, mock_field_name)
|
|
134
|
-
return
|
|
135
|
-
|
|
136
|
-
if isinstance(shape, ListShape):
|
|
137
|
-
if not isinstance(value, list):
|
|
138
|
-
_raise_type_error("an array", field_name)
|
|
139
|
-
member_shape = shape.member
|
|
140
|
-
for list_item in value:
|
|
141
|
-
_validate_value(list_item, member_shape, field_name)
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
if isinstance(shape, MapShape):
|
|
145
|
-
if not isinstance(value, dict):
|
|
146
|
-
_raise_type_error("an object", field_name)
|
|
147
|
-
value_shape = shape.value
|
|
148
|
-
for _, map_item_value in value.items():
|
|
149
|
-
_validate_value(map_item_value, value_shape, field_name)
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
# Primitive shapes and others
|
|
153
|
-
type_name = shape.type_name
|
|
154
|
-
match type_name:
|
|
155
|
-
case "string" | "timestamp":
|
|
156
|
-
if not isinstance(value, str):
|
|
157
|
-
_raise_type_error("a string", field_name)
|
|
158
|
-
# Validate enum if present
|
|
159
|
-
if isinstance(shape, StringShape):
|
|
160
|
-
enum = getattr(shape, "enum", None)
|
|
161
|
-
if enum and value not in enum:
|
|
162
|
-
raise ValidationException(
|
|
163
|
-
f"Mock result schema validation error: Field '{field_name}' is not an expected value"
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
case "integer" | "long":
|
|
167
|
-
if not isinstance(value, int) or isinstance(value, bool):
|
|
168
|
-
_raise_type_error("a number", field_name)
|
|
169
|
-
|
|
170
|
-
case "float" | "double":
|
|
171
|
-
if not (isinstance(value, (int, float)) or isinstance(value, bool)):
|
|
172
|
-
_raise_type_error("a number", field_name)
|
|
173
|
-
|
|
174
|
-
case "boolean":
|
|
175
|
-
if not isinstance(value, bool):
|
|
176
|
-
_raise_type_error("a boolean", field_name)
|
|
177
|
-
|
|
178
|
-
case "blob":
|
|
179
|
-
if not (isinstance(value, (str, bytes))):
|
|
180
|
-
_raise_type_error("a string", field_name)
|
|
181
|
-
|
|
182
|
-
# Perform validation against the output shape
|
|
183
|
-
_validate_value(mock_result, output_shape)
|
|
184
|
-
# Non-service tasks or other cases: nothing to validate
|
|
185
|
-
return
|
|
240
|
+
# Primitive shapes and others
|
|
241
|
+
type_name = shape.type_name
|
|
242
|
+
match type_name:
|
|
243
|
+
case "string" | "timestamp":
|
|
244
|
+
if not isinstance(value, str):
|
|
245
|
+
_raise_type_error("a string", field_name)
|
|
246
|
+
# Validate enum if present
|
|
247
|
+
if isinstance(shape, StringShape):
|
|
248
|
+
enum = getattr(shape, "enum", None)
|
|
249
|
+
if enum and value not in enum:
|
|
250
|
+
raise ValidationException(
|
|
251
|
+
f"Mock result schema validation error: Field '{field_name}' is not an expected value"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
case "integer" | "long":
|
|
255
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
256
|
+
_raise_type_error("a number", field_name)
|
|
257
|
+
|
|
258
|
+
case "float" | "double":
|
|
259
|
+
if not (isinstance(value, (int, float)) or isinstance(value, bool)):
|
|
260
|
+
_raise_type_error("a number", field_name)
|
|
261
|
+
|
|
262
|
+
case "boolean":
|
|
263
|
+
if not isinstance(value, bool):
|
|
264
|
+
_raise_type_error("a boolean", field_name)
|
|
265
|
+
|
|
266
|
+
case "blob":
|
|
267
|
+
if not (isinstance(value, (str, bytes))):
|
|
268
|
+
_raise_type_error("a string", field_name)
|
|
269
|
+
|
|
270
|
+
# Perform validation against the output shape
|
|
271
|
+
_validate_value(mock_result, output_shape)
|
|
186
272
|
|
|
187
273
|
def analyse(self, definition: str) -> None:
|
|
188
274
|
_, parser_rule_context = TestStateAmazonStateLanguageParser.parse(
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
|
-
import json
|
|
5
4
|
import logging
|
|
6
|
-
from typing import Final
|
|
5
|
+
from typing import Any, Final
|
|
7
6
|
|
|
8
7
|
from localstack.aws.api.events import PutEventsRequestEntry
|
|
9
8
|
from localstack.aws.api.stepfunctions import (
|
|
@@ -111,7 +110,7 @@ class Execution:
|
|
|
111
110
|
local_mock_test_case: Final[LocalMockTestCase | None]
|
|
112
111
|
|
|
113
112
|
start_date: Final[Timestamp]
|
|
114
|
-
input_data: Final[
|
|
113
|
+
input_data: Final[dict[str, Any] | None]
|
|
115
114
|
input_details: Final[CloudWatchEventsExecutionDataDetails | None]
|
|
116
115
|
trace_header: Final[TraceHeader | None]
|
|
117
116
|
_cloud_watch_logging_session: Final[CloudWatchLoggingSession | None]
|
|
@@ -119,7 +118,7 @@ class Execution:
|
|
|
119
118
|
exec_status: ExecutionStatus | None
|
|
120
119
|
stop_date: Timestamp | None
|
|
121
120
|
|
|
122
|
-
output:
|
|
121
|
+
output: dict[str, Any] | None
|
|
123
122
|
output_details: CloudWatchEventsExecutionDataDetails | None
|
|
124
123
|
|
|
125
124
|
error: SensitiveError | None
|
|
@@ -141,7 +140,7 @@ class Execution:
|
|
|
141
140
|
start_date: Timestamp,
|
|
142
141
|
cloud_watch_logging_session: CloudWatchLoggingSession | None,
|
|
143
142
|
activity_store: dict[Arn, Activity],
|
|
144
|
-
input_data:
|
|
143
|
+
input_data: dict[str, Any] | None = None,
|
|
145
144
|
trace_header: TraceHeader | None = None,
|
|
146
145
|
state_machine_alias_arn: Arn | None = None,
|
|
147
146
|
local_mock_test_case: LocalMockTestCase | None = None,
|
|
@@ -57,7 +57,6 @@ from localstack.aws.api.stepfunctions import (
|
|
|
57
57
|
LongArn,
|
|
58
58
|
MaxConcurrency,
|
|
59
59
|
MissingRequiredParameter,
|
|
60
|
-
MockInput,
|
|
61
60
|
Name,
|
|
62
61
|
PageSize,
|
|
63
62
|
PageToken,
|
|
@@ -162,6 +161,7 @@ from localstack.services.stepfunctions.stepfunctions_utils import (
|
|
|
162
161
|
normalise_max_results,
|
|
163
162
|
)
|
|
164
163
|
from localstack.state import StateVisitor
|
|
164
|
+
from localstack.utils.aws import arns
|
|
165
165
|
from localstack.utils.aws.arns import (
|
|
166
166
|
ARN_PARTITION_REGEX,
|
|
167
167
|
stepfunctions_activity_arn,
|
|
@@ -242,12 +242,6 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
|
|
|
242
242
|
f"Invalid Arn: 'Resource type not valid in this context: {lower_resource_type}'"
|
|
243
243
|
)
|
|
244
244
|
|
|
245
|
-
@staticmethod
|
|
246
|
-
def _validate_test_state_mock_input(mock_input: MockInput) -> None:
|
|
247
|
-
if {"result", "errorOutput"} <= mock_input.keys():
|
|
248
|
-
# FIXME create proper error
|
|
249
|
-
raise ValidationException("Cannot define both 'result' and 'errorOutput'")
|
|
250
|
-
|
|
251
245
|
@staticmethod
|
|
252
246
|
def _validate_activity_name(name: str) -> None:
|
|
253
247
|
# The activity name is validated according to the AWS StepFunctions documentation, the name should not contain:
|
|
@@ -1514,11 +1508,7 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
|
|
|
1514
1508
|
raise ValidationException("State not found in definition")
|
|
1515
1509
|
|
|
1516
1510
|
mock_input = request.get("mock")
|
|
1517
|
-
|
|
1518
|
-
self._validate_test_state_mock_input(mock_input)
|
|
1519
|
-
TestStateStaticAnalyser.validate_mock(
|
|
1520
|
-
mock_input=mock_input, definition=definition, state_name=state_name
|
|
1521
|
-
)
|
|
1511
|
+
TestStateStaticAnalyser.validate_mock(test_state_input=request)
|
|
1522
1512
|
|
|
1523
1513
|
if state_configuration := request.get("stateConfiguration"):
|
|
1524
1514
|
# TODO: Add validations for this i.e assert len(input) <= failureCount
|
|
@@ -1543,10 +1533,27 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
|
|
|
1543
1533
|
arn = stepfunctions_state_machine_arn(
|
|
1544
1534
|
name=name, account_id=context.account_id, region_name=context.region
|
|
1545
1535
|
)
|
|
1536
|
+
role_arn = request.get("roleArn")
|
|
1537
|
+
if role_arn is None:
|
|
1538
|
+
TestStateStaticAnalyser.validate_role_arn_required(
|
|
1539
|
+
mock_input=mock_input, definition=definition, state_name=state_name
|
|
1540
|
+
)
|
|
1541
|
+
# HACK: Added dummy role ARN because it is a required field in Execution.
|
|
1542
|
+
# To allow optional roleArn for the test state but preserve the mandatory one for regular executions
|
|
1543
|
+
# we likely need to remove inheritance TestStateExecution(Execution) in favor of composition.
|
|
1544
|
+
# TestState execution starts to have too many simplifications compared to a regular execution
|
|
1545
|
+
# which renders the inheritance mechanism harmful.
|
|
1546
|
+
# TODO make role_arn optional in TestStateExecution
|
|
1547
|
+
role_arn = arns.iam_role_arn(
|
|
1548
|
+
role_name=f"RoleFor-{name}",
|
|
1549
|
+
account_id=context.account_id,
|
|
1550
|
+
region_name=context.region,
|
|
1551
|
+
)
|
|
1552
|
+
|
|
1546
1553
|
state_machine = TestStateMachine(
|
|
1547
1554
|
name=name,
|
|
1548
1555
|
arn=arn,
|
|
1549
|
-
role_arn=
|
|
1556
|
+
role_arn=role_arn,
|
|
1550
1557
|
definition=request["definition"],
|
|
1551
1558
|
)
|
|
1552
1559
|
|
|
@@ -1561,7 +1568,7 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
|
|
|
1561
1568
|
|
|
1562
1569
|
execution = TestStateExecution(
|
|
1563
1570
|
name=exec_name,
|
|
1564
|
-
role_arn=
|
|
1571
|
+
role_arn=role_arn,
|
|
1565
1572
|
exec_arn=exec_arn,
|
|
1566
1573
|
account_id=context.account_id,
|
|
1567
1574
|
region_name=context.region,
|
|
@@ -23,6 +23,7 @@ from localstack.services.iam.iam_patches import apply_iam_patches
|
|
|
23
23
|
from localstack.services.moto import call_moto
|
|
24
24
|
from localstack.services.plugins import ServiceLifecycleHook
|
|
25
25
|
from localstack.services.sts.models import SessionConfig, sts_stores
|
|
26
|
+
from localstack.state import StateVisitor
|
|
26
27
|
from localstack.utils.aws.arns import extract_account_id_from_arn
|
|
27
28
|
from localstack.utils.aws.request_context import extract_access_key_id_from_auth_header
|
|
28
29
|
|
|
@@ -50,6 +51,12 @@ class StsProvider(StsApi, ServiceLifecycleHook):
|
|
|
50
51
|
def __init__(self):
|
|
51
52
|
apply_iam_patches()
|
|
52
53
|
|
|
54
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
55
|
+
from moto.sts.models import sts_backends
|
|
56
|
+
|
|
57
|
+
visitor.visit(sts_backends)
|
|
58
|
+
visitor.visit(sts_stores)
|
|
59
|
+
|
|
53
60
|
def get_caller_identity(self, context: RequestContext, **kwargs) -> GetCallerIdentityResponse:
|
|
54
61
|
response = call_moto(context)
|
|
55
62
|
if "user/moto" in response["Arn"] and "sts" in response["Arn"]:
|