localstack-core 4.10.1.dev7__py3-none-any.whl → 4.11.2.dev14__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 (152) hide show
  1. localstack/aws/api/acm/__init__.py +122 -122
  2. localstack/aws/api/apigateway/__init__.py +604 -561
  3. localstack/aws/api/cloudcontrol/__init__.py +63 -63
  4. localstack/aws/api/cloudformation/__init__.py +1201 -969
  5. localstack/aws/api/cloudwatch/__init__.py +375 -375
  6. localstack/aws/api/config/__init__.py +784 -786
  7. localstack/aws/api/dynamodb/__init__.py +753 -759
  8. localstack/aws/api/dynamodbstreams/__init__.py +74 -74
  9. localstack/aws/api/ec2/__init__.py +10062 -8826
  10. localstack/aws/api/es/__init__.py +453 -453
  11. localstack/aws/api/events/__init__.py +552 -552
  12. localstack/aws/api/firehose/__init__.py +541 -543
  13. localstack/aws/api/iam/__init__.py +866 -572
  14. localstack/aws/api/kinesis/__init__.py +235 -147
  15. localstack/aws/api/kms/__init__.py +341 -336
  16. localstack/aws/api/lambda_/__init__.py +974 -621
  17. localstack/aws/api/logs/__init__.py +988 -675
  18. localstack/aws/api/opensearch/__init__.py +903 -785
  19. localstack/aws/api/pipes/__init__.py +336 -336
  20. localstack/aws/api/redshift/__init__.py +1257 -1166
  21. localstack/aws/api/resource_groups/__init__.py +175 -175
  22. localstack/aws/api/resourcegroupstaggingapi/__init__.py +103 -67
  23. localstack/aws/api/route53/__init__.py +296 -254
  24. localstack/aws/api/route53resolver/__init__.py +397 -396
  25. localstack/aws/api/s3/__init__.py +1412 -1349
  26. localstack/aws/api/s3control/__init__.py +594 -594
  27. localstack/aws/api/scheduler/__init__.py +118 -118
  28. localstack/aws/api/secretsmanager/__init__.py +221 -216
  29. localstack/aws/api/ses/__init__.py +227 -227
  30. localstack/aws/api/sns/__init__.py +115 -115
  31. localstack/aws/api/sqs/__init__.py +100 -100
  32. localstack/aws/api/ssm/__init__.py +1977 -1971
  33. localstack/aws/api/stepfunctions/__init__.py +375 -333
  34. localstack/aws/api/sts/__init__.py +142 -66
  35. localstack/aws/api/support/__init__.py +112 -112
  36. localstack/aws/api/swf/__init__.py +378 -386
  37. localstack/aws/api/transcribe/__init__.py +425 -425
  38. localstack/aws/handlers/logging.py +8 -4
  39. localstack/aws/handlers/service.py +22 -3
  40. localstack/aws/protocol/parser.py +1 -1
  41. localstack/aws/protocol/serializer.py +1 -1
  42. localstack/aws/scaffold.py +15 -17
  43. localstack/cli/localstack.py +6 -1
  44. localstack/deprecations.py +0 -6
  45. localstack/dev/kubernetes/__main__.py +38 -3
  46. localstack/services/acm/provider.py +4 -0
  47. localstack/services/apigateway/helpers.py +5 -9
  48. localstack/services/apigateway/legacy/provider.py +60 -24
  49. localstack/services/apigateway/patches.py +0 -9
  50. localstack/services/cloudformation/engine/template_preparer.py +6 -2
  51. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +12 -0
  52. localstack/services/cloudformation/provider.py +2 -2
  53. localstack/services/cloudformation/v2/provider.py +6 -6
  54. localstack/services/cloudwatch/provider.py +10 -3
  55. localstack/services/cloudwatch/provider_v2.py +6 -3
  56. localstack/services/configservice/provider.py +5 -1
  57. localstack/services/dynamodb/provider.py +1 -0
  58. localstack/services/dynamodb/v2/provider.py +1 -0
  59. localstack/services/dynamodbstreams/provider.py +6 -0
  60. localstack/services/dynamodbstreams/v2/provider.py +6 -0
  61. localstack/services/ec2/provider.py +6 -0
  62. localstack/services/es/provider.py +6 -0
  63. localstack/services/events/provider.py +4 -0
  64. localstack/services/events/v1/provider.py +9 -0
  65. localstack/services/firehose/provider.py +5 -0
  66. localstack/services/iam/provider.py +4 -0
  67. localstack/services/kinesis/packages.py +1 -1
  68. localstack/services/kms/models.py +44 -24
  69. localstack/services/kms/provider.py +97 -16
  70. localstack/services/lambda_/api_utils.py +40 -21
  71. localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +1 -1
  72. localstack/services/lambda_/invocation/assignment.py +4 -1
  73. localstack/services/lambda_/invocation/execution_environment.py +21 -2
  74. localstack/services/lambda_/invocation/lambda_models.py +27 -2
  75. localstack/services/lambda_/invocation/lambda_service.py +51 -3
  76. localstack/services/lambda_/invocation/models.py +9 -1
  77. localstack/services/lambda_/invocation/version_manager.py +18 -3
  78. localstack/services/lambda_/packages.py +1 -1
  79. localstack/services/lambda_/provider.py +240 -96
  80. localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
  81. localstack/services/lambda_/runtimes.py +10 -3
  82. localstack/services/logs/provider.py +45 -19
  83. localstack/services/opensearch/provider.py +53 -3
  84. localstack/services/resource_groups/provider.py +5 -1
  85. localstack/services/resourcegroupstaggingapi/provider.py +6 -1
  86. localstack/services/s3/provider.py +29 -16
  87. localstack/services/s3/utils.py +35 -14
  88. localstack/services/s3control/provider.py +101 -2
  89. localstack/services/s3control/validation.py +50 -0
  90. localstack/services/sns/constants.py +3 -1
  91. localstack/services/sns/publisher.py +15 -6
  92. localstack/services/sns/v2/models.py +30 -1
  93. localstack/services/sns/v2/provider.py +794 -31
  94. localstack/services/sns/v2/utils.py +20 -0
  95. localstack/services/sqs/models.py +37 -10
  96. localstack/services/stepfunctions/asl/component/common/path/result_path.py +1 -1
  97. localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +0 -1
  98. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +0 -1
  99. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +8 -8
  100. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/{mock_eval_utils.py → local_mock_eval_utils.py} +13 -9
  101. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +6 -6
  102. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +1 -1
  103. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +4 -0
  104. localstack/services/stepfunctions/asl/component/test_state/state/base_mock.py +118 -0
  105. localstack/services/stepfunctions/asl/component/test_state/state/common.py +82 -0
  106. localstack/services/stepfunctions/asl/component/test_state/state/execution.py +139 -0
  107. localstack/services/stepfunctions/asl/component/test_state/state/map.py +77 -0
  108. localstack/services/stepfunctions/asl/component/test_state/state/task.py +44 -0
  109. localstack/services/stepfunctions/asl/eval/environment.py +30 -22
  110. localstack/services/stepfunctions/asl/eval/states.py +1 -1
  111. localstack/services/stepfunctions/asl/eval/test_state/environment.py +49 -9
  112. localstack/services/stepfunctions/asl/eval/test_state/program_state.py +22 -0
  113. localstack/services/stepfunctions/asl/jsonata/jsonata.py +5 -1
  114. localstack/services/stepfunctions/asl/parse/preprocessor.py +67 -24
  115. localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py +5 -4
  116. localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py +222 -31
  117. localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +170 -22
  118. localstack/services/stepfunctions/backend/execution.py +6 -6
  119. localstack/services/stepfunctions/backend/execution_worker.py +5 -5
  120. localstack/services/stepfunctions/backend/test_state/execution.py +36 -0
  121. localstack/services/stepfunctions/backend/test_state/execution_worker.py +33 -1
  122. localstack/services/stepfunctions/backend/test_state/test_state_mock.py +127 -0
  123. localstack/services/stepfunctions/local_mocking/__init__.py +9 -0
  124. localstack/services/stepfunctions/{mocking → local_mocking}/mock_config.py +24 -17
  125. localstack/services/stepfunctions/provider.py +78 -27
  126. localstack/services/stepfunctions/test_state/mock_config.py +47 -0
  127. localstack/testing/pytest/fixtures.py +28 -0
  128. localstack/testing/snapshots/transformer_utility.py +7 -0
  129. localstack/testing/testselection/matching.py +0 -1
  130. localstack/utils/analytics/publisher.py +37 -155
  131. localstack/utils/analytics/service_request_aggregator.py +6 -4
  132. localstack/utils/aws/arns.py +7 -0
  133. localstack/utils/aws/client_types.py +0 -8
  134. localstack/utils/batching.py +258 -0
  135. localstack/utils/catalog/catalog_loader.py +111 -3
  136. localstack/utils/collections.py +23 -11
  137. localstack/utils/crypto.py +109 -0
  138. localstack/version.py +2 -2
  139. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/METADATA +7 -6
  140. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/RECORD +149 -141
  141. localstack_core-4.11.2.dev14.dist-info/plux.json +1 -0
  142. localstack/services/stepfunctions/mocking/__init__.py +0 -0
  143. localstack/utils/batch_policy.py +0 -124
  144. localstack_core-4.10.1.dev7.dist-info/plux.json +0 -1
  145. /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
  146. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack +0 -0
  147. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack-supervisor +0 -0
  148. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack.bat +0 -0
  149. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/WHEEL +0 -0
  150. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/entry_points.txt +0 -0
  151. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/licenses/LICENSE.txt +0 -0
  152. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,6 @@
1
1
  """Handlers for logging."""
2
2
 
3
3
  import logging
4
- import types
5
4
  from functools import cached_property
6
5
 
7
6
  from localstack.aws.api import RequestContext, ServiceException
@@ -25,10 +24,14 @@ class ExceptionLogger(ExceptionHandler):
25
24
  try:
26
25
  import moto.core.exceptions
27
26
 
28
- self._moto_service_exception = moto.core.exceptions.ServiceException
27
+ self._skip_exceptions(
28
+ ServiceException,
29
+ moto.core.exceptions.ServiceException,
30
+ moto.core.exceptions.RESTError,
31
+ )
29
32
  except (ModuleNotFoundError, AttributeError):
30
33
  # Moto may not be available in stripped-down versions of LocalStack, like LocalStack S3 image.
31
- self._moto_service_exception = types.EllipsisType
34
+ self._skip_exceptions = (ServiceException,)
32
35
 
33
36
  def __call__(
34
37
  self,
@@ -37,11 +40,12 @@ class ExceptionLogger(ExceptionHandler):
37
40
  context: RequestContext,
38
41
  response: Response,
39
42
  ):
40
- if isinstance(exception, (ServiceException, self._moto_service_exception)):
43
+ if isinstance(exception, self._skip_exceptions):
41
44
  # We do not want to log an error/stacktrace if the handler is working as expected, but chooses to throw
42
45
  # a service exception. It may also throw a Moto ServiceException, which should not be logged either
43
46
  # because ServiceExceptionHandler understands it.
44
47
  return
48
+
45
49
  if self.logger.isEnabledFor(level=logging.DEBUG):
46
50
  self.logger.exception("exception during call chain", exc_info=exception)
47
51
  else:
@@ -7,6 +7,7 @@ from collections import defaultdict
7
7
  from typing import Any
8
8
 
9
9
  from botocore.model import OperationModel, ServiceModel
10
+ from plux.core.plugin import PluginDisabled
10
11
 
11
12
  from localstack import config
12
13
  from localstack.http import Response
@@ -157,14 +158,17 @@ class ServiceExceptionSerializer(ExceptionHandler):
157
158
  def __init__(self):
158
159
  self.handle_internal_failures = True
159
160
 
161
+ self._moto_service_exception = types.EllipsisType
162
+ self._moto_rest_error = types.EllipsisType
163
+
160
164
  try:
161
165
  import moto.core.exceptions
162
166
 
163
167
  self._moto_service_exception = moto.core.exceptions.ServiceException
168
+ self._moto_rest_error = moto.core.exceptions.RESTError
164
169
  except (ModuleNotFoundError, AttributeError) as exc:
165
170
  # Moto may not be available in stripped-down versions of LocalStack, like LocalStack S3 image.
166
- LOG.debug("Unable to set up Moto ServiceException translation: %s", exc)
167
- self._moto_service_exception = types.EllipsisType
171
+ LOG.debug("Unable to set up Moto exception translation: %s", exc)
168
172
 
169
173
  def __call__(
170
174
  self,
@@ -199,6 +203,12 @@ class ServiceExceptionSerializer(ExceptionHandler):
199
203
  code=exception.code,
200
204
  message=exception.message,
201
205
  )
206
+ elif isinstance(exception, self._moto_rest_error):
207
+ # Some Moto exceptions (like ones raised by EC2) are of type RESTError.
208
+ error = CommonServiceException(
209
+ code=exception.error_type,
210
+ message=exception.message,
211
+ )
202
212
 
203
213
  elif not isinstance(exception, ServiceException):
204
214
  if not self.handle_internal_failures:
@@ -222,7 +232,16 @@ class ServiceExceptionSerializer(ExceptionHandler):
222
232
  operation = context.service.operation_model(context.service.operation_names[0])
223
233
  msg = f"exception while calling {service_name} with unknown operation: {message}"
224
234
 
225
- status_code = 501 if config.FAIL_FAST else 500
235
+ # Check for license restricted plugin message and set status code to 501
236
+ if (
237
+ isinstance(exception, PluginDisabled)
238
+ and "not part of the active license agreement"
239
+ in str(getattr(exception, "reason", "")).lower()
240
+ ):
241
+ status_code = 501
242
+ msg = f"exception while calling {service_name}.{operation.name}: {str(getattr(exception, 'reason', ''))}"
243
+ else:
244
+ status_code = 501 if config.FAIL_FAST else 500
226
245
 
227
246
  error = CommonServiceException(
228
247
  "InternalError", msg, status_code=status_code
@@ -1032,7 +1032,7 @@ class BaseCBORRequestParser(RequestParser, ABC):
1032
1032
  return method(stream, additional_info)
1033
1033
  else:
1034
1034
  raise ProtocolParserError(
1035
- f"Unsupported inital byte found for data item- "
1035
+ f"Unsupported initial byte found for data item- "
1036
1036
  f"Major type:{major_type}, Additional info: "
1037
1037
  f"{additional_info}"
1038
1038
  )
@@ -2192,7 +2192,7 @@ class S3ResponseSerializer(RestXMLResponseSerializer):
2192
2192
 
2193
2193
  def _prepare_additional_traits_in_xml(self, root: ETree.Element | None, request_id: str):
2194
2194
  # some tools (Serverless) require a newline after the "<?xml ...>\n" preamble line, e.g., for LocationConstraint
2195
- if root and not root.tail:
2195
+ if root is not None and not root.tail:
2196
2196
  root.tail = "\n"
2197
2197
 
2198
2198
  root.attrib["xmlns"] = self.XML_NAMESPACE
@@ -188,9 +188,7 @@ class ShapeNode:
188
188
  if member in self.shape.required_members:
189
189
  output.write(f" {member}: IO[{q}{to_valid_python_name(shape.name)}{q}]\n")
190
190
  else:
191
- output.write(
192
- f" {member}: Optional[IO[{q}{to_valid_python_name(shape.name)}{q}]]\n"
193
- )
191
+ output.write(f" {member}: {q}IO[{to_valid_python_name(shape.name)}] | None{q}\n")
194
192
  del remaining_members[member]
195
193
  # render the streaming payload first
196
194
  if self.is_response and self.response_operation.has_streaming_output:
@@ -199,25 +197,26 @@ class ShapeNode:
199
197
  shape_name = to_valid_python_name(shape.name)
200
198
  if member in self.shape.required_members:
201
199
  output.write(
202
- f" {member}: Union[{q}{shape_name}{q}, IO[{q}{shape_name}{q}], Iterable[{q}{shape_name}{q}]]\n"
200
+ f" {member}: {q}{shape_name} | IO[{shape_name}] | Iterable[{shape_name}]{q}\n"
203
201
  )
204
202
  else:
205
203
  output.write(
206
- f" {member}: Optional[Union[{q}{shape_name}{q}, IO[{q}{shape_name}{q}], Iterable[{q}{shape_name}{q}]]]\n"
204
+ f" {member}: {q}{shape_name} | IO[{shape_name}] | Iterable[{shape_name}] | None{q}\n"
207
205
  )
208
206
  del remaining_members[member]
209
207
 
210
208
  for k, v in remaining_members.items():
209
+ shape_name = to_valid_python_name(v.name)
211
210
  if k in self.shape.required_members:
212
211
  if v.serialization.get("eventstream"):
213
- output.write(f" {k}: Iterator[{q}{to_valid_python_name(v.name)}{q}]\n")
212
+ output.write(f" {k}: Iterator[{q}{shape_name}{q}]\n")
214
213
  else:
215
- output.write(f" {k}: {q}{to_valid_python_name(v.name)}{q}\n")
214
+ output.write(f" {k}: {q}{shape_name}{q}\n")
216
215
  else:
217
216
  if v.serialization.get("eventstream"):
218
- output.write(f" {k}: Iterator[{q}{to_valid_python_name(v.name)}{q}]\n")
217
+ output.write(f" {k}: Iterator[{q}{shape_name}{q}]\n")
219
218
  else:
220
- output.write(f" {k}: Optional[{q}{to_valid_python_name(v.name)}{q}]\n")
219
+ output.write(f" {k}: {q}{shape_name} | None{q}\n")
221
220
 
222
221
  def _print_as_typed_dict(self, output, doc=True, quote_types=False):
223
222
  name = to_valid_python_name(self.shape.name)
@@ -236,7 +235,7 @@ class ShapeNode:
236
235
  if v.serialization.get("eventstream"):
237
236
  output.write(f' "{k}": Iterator[{q}{member_name}{q}],\n')
238
237
  else:
239
- output.write(f' "{k}": Optional[{q}{member_name}{q}],\n')
238
+ output.write(f' "{k}": {q}{member_name} | None{q},\n')
240
239
  output.write("}, total=False)")
241
240
 
242
241
  def print_shape_doc(self, output, shape):
@@ -256,11 +255,11 @@ class ShapeNode:
256
255
  self._print_structure_declaration(output, doc, quote_types)
257
256
  elif isinstance(shape, ListShape):
258
257
  output.write(
259
- f"{to_valid_python_name(shape.name)} = List[{q}{to_valid_python_name(shape.member.name)}{q}]"
258
+ f"{to_valid_python_name(shape.name)} = list[{q}{to_valid_python_name(shape.member.name)}{q}]"
260
259
  )
261
260
  elif isinstance(shape, MapShape):
262
261
  output.write(
263
- f"{to_valid_python_name(shape.name)} = Dict[{q}{to_valid_python_name(shape.key.name)}{q}, {q}{to_valid_python_name(shape.value.name)}{q}]"
262
+ f"{to_valid_python_name(shape.name)} = dict[{q}{to_valid_python_name(shape.key.name)}{q}, {q}{to_valid_python_name(shape.value.name)}{q}]"
264
263
  )
265
264
  elif isinstance(shape, StringShape):
266
265
  if shape.enum:
@@ -316,9 +315,8 @@ class ShapeNode:
316
315
  def generate_service_types(output, service: ServiceModel, doc=True):
317
316
  output.write("from datetime import datetime\n")
318
317
  output.write("from enum import StrEnum\n")
319
- output.write(
320
- "from typing import Dict, List, Optional, Iterator, Iterable, IO, Union, TypedDict\n"
321
- )
318
+ output.write("from typing import IO, TypedDict\n")
319
+ output.write("from collections.abc import Iterable, Iterator\n")
322
320
  output.write("\n")
323
321
  output.write(
324
322
  "from localstack.aws.api import handler, RequestContext, ServiceException, ServiceRequest"
@@ -372,8 +370,8 @@ def generate_service_api(output, service: ServiceModel, doc=True):
372
370
 
373
371
  output.write(f"class {class_name}:\n")
374
372
  output.write("\n")
375
- output.write(f' service = "{service.service_name}"\n')
376
- output.write(f' version = "{service.api_version}"\n')
373
+ output.write(f' service: str = "{service.service_name}"\n')
374
+ output.write(f' version: str = "{service.api_version}"\n')
377
375
  for op_name in service.operation_names:
378
376
  operation: OperationModel = service.operation_model(op_name)
379
377
 
@@ -433,7 +433,7 @@ def _print_service_table(services: dict[str, str]) -> None:
433
433
 
434
434
  @localstack.command(name="start", short_help="Start LocalStack")
435
435
  @click.option("--docker", is_flag=True, help="Start LocalStack in a docker container [default]")
436
- @click.option("--host", is_flag=True, help="Start LocalStack directly on the host")
436
+ @click.option("--host", is_flag=True, help="Start LocalStack directly on the host", deprecated=True)
437
437
  @click.option("--no-banner", is_flag=True, help="Disable LocalStack banner", default=False)
438
438
  @click.option(
439
439
  "-d", "--detached", is_flag=True, help="Start LocalStack in the background", default=False
@@ -532,6 +532,11 @@ def cmd_start(
532
532
  console.log("starting LocalStack in Docker mode :whale:")
533
533
 
534
534
  if host:
535
+ console.log(
536
+ "Warning: Starting LocalStack in host mode from the CLI is deprecated and will be removed soon. Please use the default Docker mode instead.",
537
+ style="bold red",
538
+ )
539
+
535
540
  # call hooks to prepare host
536
541
  bootstrap.prepare_host(console)
537
542
 
@@ -35,12 +35,6 @@ class EnvVarDeprecation:
35
35
  # Please make sure this is in-sync with https://docs.localstack.cloud/references/configuration/
36
36
  #
37
37
  DEPRECATIONS = [
38
- # Since 0.11.3 - HTTP / HTTPS multiplexing
39
- EnvVarDeprecation(
40
- "USE_SSL",
41
- "0.11.3",
42
- "Each endpoint now supports multiplexing HTTP/HTTPS traffic over the same port. Please remove this environment variable.", # noqa
43
- ),
44
38
  # Since 0.12.8 - PORT_UI was removed
45
39
  EnvVarDeprecation(
46
40
  "PORT_WEB_UI",
@@ -9,6 +9,7 @@ EDGE_SERVICE_NODE_PORT = 30066
9
9
  NODE_PORT_START = 30010
10
10
  SERVICE_PORT_START = 4510
11
11
  NUMBER_OF_SERVICE_PORTS = 50
12
+ EDGE_SERVICE_DNS_PORT = 31053
12
13
 
13
14
 
14
15
  @dataclasses.dataclass
@@ -144,7 +145,12 @@ def generate_mount_points(
144
145
  return mount_points
145
146
 
146
147
 
147
- def generate_k8s_cluster_config(mount_points: list[MountPoint], port: int = 4566):
148
+ def generate_k8s_cluster_config(
149
+ mount_points: list[MountPoint],
150
+ port: int = 4566,
151
+ expose_dns: bool = False,
152
+ dns_port: int = 53,
153
+ ):
148
154
  volumes = [
149
155
  {
150
156
  "volume": f"{mount_point.host_path}:{mount_point.node_path}",
@@ -177,6 +183,16 @@ def generate_k8s_cluster_config(mount_points: list[MountPoint], port: int = 4566
177
183
  },
178
184
  ]
179
185
 
186
+ if expose_dns:
187
+ ports.append(
188
+ {
189
+ "nodeFilters": [
190
+ "server:0",
191
+ ],
192
+ "port": f"{dns_port}:{EDGE_SERVICE_DNS_PORT}",
193
+ }
194
+ )
195
+
180
196
  config = {
181
197
  "apiVersion": "k3d.io/v1alpha5",
182
198
  "kind": "Simple",
@@ -279,7 +295,10 @@ def generate_k8s_helm_overrides(
279
295
  "end": SERVICE_PORT_START + NUMBER_OF_SERVICE_PORTS,
280
296
  "nodePortStart": NODE_PORT_START,
281
297
  },
282
- "dnsService": True,
298
+ "dnsService": {
299
+ "enabled": True,
300
+ "nodePort": EDGE_SERVICE_DNS_PORT,
301
+ },
283
302
  }
284
303
  overrides = {
285
304
  "debug": True,
@@ -355,6 +374,18 @@ def print_file(content: dict, file_name: str):
355
374
  help="Port to expose from the kubernetes node",
356
375
  type=click.IntRange(0, 65535),
357
376
  )
377
+ @click.option(
378
+ "--expose-dns",
379
+ is_flag=True,
380
+ default=False,
381
+ help="Expose DNS port from the kubernetes node.",
382
+ )
383
+ @click.option(
384
+ "--dns-port",
385
+ default=53,
386
+ help="DNS port to expose from the kubernetes node. It is applied only if --expose-dns is set.",
387
+ type=click.IntRange(0, 65535),
388
+ )
358
389
  @click.argument("command", nargs=-1, required=False)
359
390
  def run(
360
391
  pro: bool = None,
@@ -367,13 +398,17 @@ def run(
367
398
  command: str = None,
368
399
  env: list[str] = None,
369
400
  port: int = None,
401
+ expose_dns: bool = False,
402
+ dns_port: int = 53,
370
403
  ):
371
404
  """
372
405
  A tool for localstack developers to generate the kubernetes cluster configuration file and the overrides to mount the localstack code into the cluster.
373
406
  """
374
407
  mount_points = generate_mount_points(pro, mount_moto, mount_entrypoints)
375
408
 
376
- config = generate_k8s_cluster_config(mount_points, port=port)
409
+ config = generate_k8s_cluster_config(
410
+ mount_points, port=port, expose_dns=expose_dns, dns_port=dns_port
411
+ )
377
412
 
378
413
  overrides = generate_k8s_helm_overrides(mount_points, pro=pro, env=env)
379
414
 
@@ -10,6 +10,7 @@ from localstack.aws.api.acm import (
10
10
  RequestCertificateResponse,
11
11
  )
12
12
  from localstack.services import moto
13
+ from localstack.state import StateVisitor
13
14
  from localstack.utils.patch import patch
14
15
 
15
16
  # reduce the validation wait time from 60 (default) to 10 seconds
@@ -100,6 +101,9 @@ def describe(describe_orig, self):
100
101
 
101
102
 
102
103
  class AcmProvider(AcmApi):
104
+ def accept_state_visitor(self, visitor: StateVisitor):
105
+ visitor.visit(acm_models.acm_backends)
106
+
103
107
  @handler("RequestCertificate", expand=False)
104
108
  def request_certificate(
105
109
  self,
@@ -23,7 +23,7 @@ from localstack.aws.api.apigateway import (
23
23
  IntegrationType,
24
24
  Model,
25
25
  NotFoundException,
26
- PutRestApiRequest,
26
+ PutMode,
27
27
  RequestValidator,
28
28
  )
29
29
  from localstack.constants import (
@@ -39,8 +39,7 @@ from localstack.services.apigateway.models import (
39
39
  apigateway_stores,
40
40
  )
41
41
  from localstack.utils import common
42
- from localstack.utils.json import parse_json_or_yaml
43
- from localstack.utils.strings import short_uid, to_bytes, to_str
42
+ from localstack.utils.strings import short_uid, to_bytes
44
43
  from localstack.utils.urls import localstack_host
45
44
 
46
45
  LOG = logging.getLogger(__name__)
@@ -472,11 +471,9 @@ def add_documentation_parts(rest_api_container, documentation):
472
471
 
473
472
 
474
473
  def import_api_from_openapi_spec(
475
- rest_api: MotoRestAPI, context: RequestContext, request: PutRestApiRequest
474
+ rest_api: MotoRestAPI, context: RequestContext, open_api_spec: dict, mode: PutMode
476
475
  ) -> tuple[MotoRestAPI, list[str]]:
477
476
  """Import an API from an OpenAPI spec document"""
478
- body = parse_json_or_yaml(to_str(request["body"].read()))
479
-
480
477
  warnings = []
481
478
 
482
479
  # TODO There is an issue with the botocore specs so the parameters doesn't get populated as it should
@@ -484,15 +481,14 @@ def import_api_from_openapi_spec(
484
481
  # query_params = request.get("parameters") or {}
485
482
  query_params: dict = context.request.values.to_dict()
486
483
 
487
- resolved_schema = resolve_references(copy.deepcopy(body), rest_api_id=rest_api.id)
484
+ resolved_schema = resolve_references(copy.deepcopy(open_api_spec), rest_api_id=rest_api.id)
488
485
  account_id = context.account_id
489
486
  region_name = context.region
490
487
 
491
488
  # TODO:
492
- # 1. validate the "mode" property of the spec document, "merge" or "overwrite", and properly apply it
489
+ # 1. properly apply the mode (overwrite or merge)
493
490
  # for now, it only considers it for the binaryMediaTypes
494
491
  # 2. validate the document type, "swagger" or "openapi"
495
- mode = request.get("mode", "merge")
496
492
 
497
493
  rest_api.version = (
498
494
  str(version) if (version := resolved_schema.get("info", {}).get("version")) else None
@@ -42,6 +42,7 @@ from localstack.aws.api.apigateway import (
42
42
  DomainName,
43
43
  DomainNames,
44
44
  DomainNameStatus,
45
+ EndpointAccessMode,
45
46
  EndpointConfiguration,
46
47
  EndpointType,
47
48
  ExportResponse,
@@ -117,7 +118,11 @@ from localstack.services.apigateway.helpers import (
117
118
  from localstack.services.apigateway.legacy.helpers import multi_value_dict_for_list
118
119
  from localstack.services.apigateway.legacy.invocations import invoke_rest_api_from_request
119
120
  from localstack.services.apigateway.legacy.router_asf import ApigatewayRouter, to_invocation_context
120
- from localstack.services.apigateway.models import ApiGatewayStore, RestApiContainer
121
+ from localstack.services.apigateway.models import (
122
+ ApiGatewayStore,
123
+ RestApiContainer,
124
+ apigateway_stores,
125
+ )
121
126
  from localstack.services.apigateway.next_gen.execute_api.router import (
122
127
  ApiGatewayRouter as ApiGatewayRouterNextGen,
123
128
  )
@@ -125,6 +130,7 @@ from localstack.services.apigateway.patches import apply_patches
125
130
  from localstack.services.edge import ROUTER
126
131
  from localstack.services.moto import call_moto, call_moto_with_request
127
132
  from localstack.services.plugins import ServiceLifecycleHook
133
+ from localstack.state import StateVisitor
128
134
  from localstack.utils.aws.arns import InvalidArnException, get_partition, parse_arn
129
135
  from localstack.utils.collections import (
130
136
  DelSafeDict,
@@ -191,6 +197,12 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
191
197
  apply_patches()
192
198
  self.router.register_routes()
193
199
 
200
+ def accept_state_visitor(self, visitor: StateVisitor):
201
+ from moto.apigateway import apigateway_backends
202
+
203
+ visitor.visit(apigateway_backends)
204
+ visitor.visit(apigateway_stores)
205
+
194
206
  @handler("TestInvokeMethod", expand=False)
195
207
  def test_invoke_method(
196
208
  self, context: RequestContext, request: TestInvokeMethodRequest
@@ -476,11 +488,19 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
476
488
 
477
489
  @handler("PutRestApi", expand=False)
478
490
  def put_rest_api(self, context: RequestContext, request: PutRestApiRequest) -> RestApi:
491
+ body_data = request["body"].read()
492
+ try:
493
+ openapi_spec = parse_json_or_yaml(to_str(body_data))
494
+ except Exception:
495
+ raise BadRequestException("Invalid OpenAPI input.")
479
496
  # TODO: take into account the mode: overwrite or merge
480
497
  # the default is now `merge`, but we are removing everything
481
498
  rest_api = get_moto_rest_api(context, request["restApiId"])
482
499
  rest_api, warnings = import_api_from_openapi_spec(
483
- rest_api, context=context, request=request
500
+ rest_api,
501
+ context=context,
502
+ open_api_spec=openapi_spec,
503
+ mode=request.get("mode") or PutMode.merge,
484
504
  )
485
505
 
486
506
  rest_api.root_resource_id = get_moto_rest_api_root_resource(rest_api)
@@ -503,20 +523,21 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
503
523
  self,
504
524
  context: RequestContext,
505
525
  domain_name: String,
506
- certificate_name: String = None,
507
- certificate_body: String = None,
508
- certificate_private_key: String = None,
509
- certificate_chain: String = None,
510
- certificate_arn: String = None,
511
- regional_certificate_name: String = None,
512
- regional_certificate_arn: String = None,
513
- endpoint_configuration: EndpointConfiguration = None,
514
- tags: MapOfStringToString = None,
515
- security_policy: SecurityPolicy = None,
516
- mutual_tls_authentication: MutualTlsAuthenticationInput = None,
517
- ownership_verification_certificate_arn: String = None,
518
- policy: String = None,
519
- routing_mode: RoutingMode = None,
526
+ certificate_name: String | None = None,
527
+ certificate_body: String | None = None,
528
+ certificate_private_key: String | None = None,
529
+ certificate_chain: String | None = None,
530
+ certificate_arn: String | None = None,
531
+ regional_certificate_name: String | None = None,
532
+ regional_certificate_arn: String | None = None,
533
+ endpoint_configuration: EndpointConfiguration | None = None,
534
+ tags: MapOfStringToString | None = None,
535
+ security_policy: SecurityPolicy | None = None,
536
+ endpoint_access_mode: EndpointAccessMode | None = None,
537
+ mutual_tls_authentication: MutualTlsAuthenticationInput | None = None,
538
+ ownership_verification_certificate_arn: String | None = None,
539
+ policy: String | None = None,
540
+ routing_mode: RoutingMode | None = None,
520
541
  **kwargs,
521
542
  ) -> DomainName:
522
543
  if not domain_name:
@@ -1512,7 +1533,10 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
1512
1533
  **kwargs,
1513
1534
  ) -> DocumentationPartIds:
1514
1535
  body_data = body.read()
1515
- openapi_spec = parse_json_or_yaml(to_str(body_data))
1536
+ try:
1537
+ openapi_spec = parse_json_or_yaml(to_str(body_data))
1538
+ except Exception:
1539
+ raise BadRequestException("Unable to build importer with provided input.")
1516
1540
 
1517
1541
  rest_api_container = get_rest_api_container(context, rest_api_id=rest_api_id)
1518
1542
 
@@ -2012,7 +2036,11 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2012
2036
  body_data = body.read()
2013
2037
 
2014
2038
  # create rest api
2015
- openapi_spec = parse_json_or_yaml(to_str(body_data))
2039
+ try:
2040
+ openapi_spec = parse_json_or_yaml(to_str(body_data))
2041
+ except Exception:
2042
+ raise BadRequestException("Invalid OpenAPI input.")
2043
+
2016
2044
  create_api_request = CreateRestApiRequest(name=openapi_spec.get("info").get("title"))
2017
2045
  create_api_context = create_custom_context(
2018
2046
  context,
@@ -2053,12 +2081,20 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2053
2081
  **kwargs,
2054
2082
  ) -> Integration:
2055
2083
  try:
2056
- response: Integration = call_moto(context)
2057
- except CommonServiceException as e:
2058
- # the Exception raised by moto does not have the right message not status code
2059
- if e.code == "NotFoundException":
2060
- raise NotFoundException("Invalid Integration identifier specified")
2061
- raise
2084
+ moto_rest_api = get_moto_rest_api(context, rest_api_id)
2085
+ except NotFoundException:
2086
+ raise NotFoundException("Invalid Resource identifier specified")
2087
+
2088
+ if not (moto_resource := moto_rest_api.resources.get(resource_id)):
2089
+ raise NotFoundException("Invalid Resource identifier specified")
2090
+
2091
+ if not (moto_method := moto_resource.resource_methods.get(http_method)):
2092
+ raise NotFoundException("Invalid Method identifier specified")
2093
+
2094
+ if not moto_method.method_integration:
2095
+ raise NotFoundException("Invalid Integration identifier specified")
2096
+
2097
+ response: Integration = call_moto(context)
2062
2098
 
2063
2099
  if integration_responses := response.get("integrationResponses"):
2064
2100
  for integration_response in integration_responses.values():
@@ -5,7 +5,6 @@ import logging
5
5
  from moto.apigateway import models as apigateway_models
6
6
  from moto.apigateway.exceptions import (
7
7
  DeploymentNotFoundException,
8
- NoIntegrationDefined,
9
8
  RestAPINotFound,
10
9
  StageStillActive,
11
10
  )
@@ -113,14 +112,6 @@ def apply_patches():
113
112
  )
114
113
  return result
115
114
 
116
- # patch integration error responses
117
- @patch(apigateway_models.Resource.get_integration)
118
- def apigateway_models_resource_get_integration(fn, self, method_type):
119
- resource_method = self.resource_methods.get(method_type, {})
120
- if not resource_method.method_integration:
121
- raise NoIntegrationDefined()
122
- return resource_method.method_integration
123
-
124
115
  @patch(apigateway_models.RestAPI.to_dict)
125
116
  def apigateway_models_rest_api_to_dict(fn, self):
126
117
  resp = fn(self)
@@ -6,6 +6,7 @@ from localstack.services.cloudformation.engine.transformers import (
6
6
  apply_global_transformations,
7
7
  apply_intrinsic_transformations,
8
8
  )
9
+ from localstack.services.cloudformation.engine.validations import ValidationError
9
10
  from localstack.utils.json import clone_safe
10
11
 
11
12
  LOG = logging.getLogger(__name__)
@@ -17,9 +18,12 @@ def parse_template(template: str) -> dict:
17
18
  except Exception:
18
19
  try:
19
20
  return clone_safe(yaml_parser.parse_yaml(template))
20
- except Exception as e:
21
- LOG.debug("Unable to parse CloudFormation template (%s): %s", e, template)
21
+ except ValidationError:
22
+ # The error is handled in the yaml parsing helper
22
23
  raise
24
+ except Exception:
25
+ # TODO: present the user with a better error message including error location
26
+ raise ValidationError("Template format error: YAML not well-formed.")
23
27
 
24
28
 
25
29
  def template_to_json(template: str) -> str:
@@ -677,6 +677,13 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
677
677
  node_condition = self._get_node_condition_if_exists(
678
678
  condition_name=condition_delta.before
679
679
  )
680
+ if is_nothing(node_condition):
681
+ # TODO: I don't think this is a possible state since for us to be evaluating the before state,
682
+ # we must have successfully deployed the stack and as such this case was not reached before
683
+ raise ValidationError(
684
+ f"Template error: unresolved condition dependency {condition_delta.before} in Fn::If"
685
+ )
686
+
680
687
  condition_value = self.visit(node_condition).before
681
688
  if condition_value:
682
689
  arg_delta = self.visit(node_intrinsic_function.arguments.array[1])
@@ -688,6 +695,11 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
688
695
  node_condition = self._get_node_condition_if_exists(
689
696
  condition_name=condition_delta.after
690
697
  )
698
+ if is_nothing(node_condition):
699
+ raise ValidationError(
700
+ f"Template error: unresolved condition dependency {condition_delta.after} in Fn::If"
701
+ )
702
+
691
703
  condition_value = self.visit(node_condition).after
692
704
  if condition_value:
693
705
  arg_delta = self.visit(node_intrinsic_function.arguments.array[1])
@@ -952,8 +952,8 @@ class CloudformationProvider(CloudformationApi):
952
952
  def describe_stack_events(
953
953
  self,
954
954
  context: RequestContext,
955
- stack_name: StackName = None,
956
- next_token: NextToken = None,
955
+ stack_name: StackName,
956
+ next_token: NextToken | None = None,
957
957
  **kwargs,
958
958
  ) -> DescribeStackEventsOutput:
959
959
  if stack_name is None: