localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. localstack/aws/api/cloudformation/__init__.py +1 -0
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +4 -0
  5. localstack/aws/api/ec2/__init__.py +1113 -56
  6. localstack/aws/api/iam/__init__.py +7 -0
  7. localstack/aws/api/kinesis/__init__.py +19 -0
  8. localstack/aws/api/kms/__init__.py +6 -0
  9. localstack/aws/api/lambda_/__init__.py +13 -0
  10. localstack/aws/api/logs/__init__.py +15 -0
  11. localstack/aws/api/redshift/__init__.py +9 -3
  12. localstack/aws/api/route53/__init__.py +2 -0
  13. localstack/aws/api/s3/__init__.py +12 -0
  14. localstack/aws/api/s3control/__init__.py +32 -0
  15. localstack/aws/api/ssm/__init__.py +2 -0
  16. localstack/aws/client.py +7 -2
  17. localstack/aws/forwarder.py +52 -5
  18. localstack/aws/handlers/analytics.py +1 -1
  19. localstack/aws/handlers/logging.py +12 -2
  20. localstack/aws/handlers/metric_handler.py +41 -1
  21. localstack/aws/handlers/service.py +32 -9
  22. localstack/aws/protocol/parser.py +440 -21
  23. localstack/aws/protocol/serializer.py +684 -64
  24. localstack/aws/protocol/service_router.py +120 -20
  25. localstack/aws/skeleton.py +4 -2
  26. localstack/aws/spec-patches.json +58 -0
  27. localstack/aws/spec.py +33 -13
  28. localstack/cli/exceptions.py +1 -1
  29. localstack/cli/localstack.py +4 -4
  30. localstack/cli/lpm.py +3 -4
  31. localstack/cli/profiles.py +1 -2
  32. localstack/config.py +18 -12
  33. localstack/constants.py +4 -29
  34. localstack/dev/kubernetes/__main__.py +1 -1
  35. localstack/dev/run/paths.py +1 -1
  36. localstack/dns/plugins.py +5 -1
  37. localstack/dns/server.py +12 -3
  38. localstack/packages/api.py +9 -8
  39. localstack/packages/core.py +2 -2
  40. localstack/packages/plugins.py +0 -8
  41. localstack/runtime/init.py +1 -1
  42. localstack/services/apigateway/legacy/provider.py +53 -3
  43. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  44. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  45. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
  46. localstack/services/apigateway/next_gen/provider.py +5 -0
  47. localstack/services/cloudformation/engine/entities.py +12 -1
  48. localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
  49. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
  50. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
  51. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
  52. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
  53. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
  54. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  55. localstack/services/cloudformation/engine/v2/resolving.py +6 -4
  56. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  57. localstack/services/cloudformation/resource_provider.py +5 -1
  58. localstack/services/cloudformation/resources.py +24149 -0
  59. localstack/services/cloudformation/v2/entities.py +6 -3
  60. localstack/services/cloudformation/v2/provider.py +172 -27
  61. localstack/services/cloudformation/v2/types.py +8 -4
  62. localstack/services/cloudwatch/provider_v2.py +25 -28
  63. localstack/services/dynamodb/packages.py +2 -1
  64. localstack/services/dynamodb/provider.py +42 -0
  65. localstack/services/dynamodb/v2/provider.py +42 -0
  66. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  67. localstack/services/es/provider.py +2 -2
  68. localstack/services/events/event_rule_engine.py +31 -13
  69. localstack/services/events/models.py +4 -5
  70. localstack/services/events/target.py +17 -9
  71. localstack/services/iam/provider.py +11 -116
  72. localstack/services/iam/resources/policy_simulator.py +133 -0
  73. localstack/services/kinesis/models.py +15 -2
  74. localstack/services/kinesis/provider.py +77 -0
  75. localstack/services/kms/provider.py +14 -5
  76. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  77. localstack/services/lambda_/packages.py +1 -1
  78. localstack/services/logs/provider.py +1 -1
  79. localstack/services/moto.py +2 -1
  80. localstack/services/opensearch/cluster.py +15 -7
  81. localstack/services/opensearch/packages.py +26 -7
  82. localstack/services/opensearch/provider.py +6 -1
  83. localstack/services/opensearch/versions.py +56 -7
  84. localstack/services/s3/constants.py +5 -2
  85. localstack/services/s3/cors.py +4 -4
  86. localstack/services/s3/notifications.py +1 -1
  87. localstack/services/s3/presigned_url.py +27 -43
  88. localstack/services/s3/provider.py +67 -11
  89. localstack/services/s3/utils.py +42 -11
  90. localstack/services/ses/provider.py +16 -7
  91. localstack/services/sns/constants.py +7 -1
  92. localstack/services/sns/v2/models.py +167 -0
  93. localstack/services/sns/v2/provider.py +860 -2
  94. localstack/services/sns/v2/utils.py +130 -0
  95. localstack/services/sqs/developer_api.py +205 -0
  96. localstack/services/sqs/models.py +42 -3
  97. localstack/services/sqs/provider.py +8 -309
  98. localstack/services/sqs/query_api.py +1 -1
  99. localstack/services/sqs/utils.py +121 -2
  100. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  101. localstack/testing/aws/cloudformation_utils.py +1 -1
  102. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  103. localstack/testing/pytest/container.py +4 -5
  104. localstack/testing/pytest/fixtures.py +20 -19
  105. localstack/testing/pytest/in_memory_localstack.py +0 -4
  106. localstack/testing/pytest/marking.py +13 -4
  107. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  108. localstack/testing/pytest/util.py +1 -1
  109. localstack/testing/pytest/validation_tracking.py +1 -2
  110. localstack/testing/snapshots/transformer_utility.py +5 -0
  111. localstack/utils/analytics/events.py +2 -2
  112. localstack/utils/analytics/metadata.py +1 -2
  113. localstack/utils/analytics/metrics/counter.py +6 -8
  114. localstack/utils/analytics/publisher.py +1 -2
  115. localstack/utils/analytics/service_request_aggregator.py +2 -2
  116. localstack/utils/archives.py +11 -11
  117. localstack/utils/aws/arns.py +17 -9
  118. localstack/utils/aws/aws_responses.py +7 -7
  119. localstack/utils/aws/aws_stack.py +2 -3
  120. localstack/utils/aws/message_forwarding.py +1 -2
  121. localstack/utils/aws/request_context.py +4 -5
  122. localstack/utils/batch_policy.py +3 -3
  123. localstack/utils/bootstrap.py +7 -7
  124. localstack/utils/catalog/catalog.py +139 -0
  125. localstack/utils/catalog/catalog_loader.py +11 -0
  126. localstack/utils/catalog/common.py +58 -0
  127. localstack/utils/catalog/plugins.py +28 -0
  128. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  129. localstack/utils/collections.py +7 -8
  130. localstack/utils/config_listener.py +1 -1
  131. localstack/utils/container_networking.py +2 -3
  132. localstack/utils/container_utils/container_client.py +115 -131
  133. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  134. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  135. localstack/utils/diagnose.py +2 -3
  136. localstack/utils/docker_utils.py +3 -4
  137. localstack/utils/files.py +31 -7
  138. localstack/utils/functions.py +3 -2
  139. localstack/utils/http.py +4 -5
  140. localstack/utils/json.py +19 -5
  141. localstack/utils/kinesis/kinesis_connector.py +2 -1
  142. localstack/utils/net.py +6 -6
  143. localstack/utils/no_exit_argument_parser.py +2 -2
  144. localstack/utils/numbers.py +9 -2
  145. localstack/utils/objects.py +6 -5
  146. localstack/utils/patch.py +2 -1
  147. localstack/utils/run.py +10 -9
  148. localstack/utils/scheduler.py +11 -11
  149. localstack/utils/server/tcp_proxy.py +2 -2
  150. localstack/utils/serving.py +2 -3
  151. localstack/utils/strings.py +10 -11
  152. localstack/utils/sync.py +126 -1
  153. localstack/utils/tagging.py +1 -4
  154. localstack/utils/testutil.py +5 -4
  155. localstack/utils/threads.py +2 -2
  156. localstack/utils/time.py +11 -3
  157. localstack/utils/urls.py +1 -3
  158. localstack/version.py +2 -2
  159. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/METADATA +17 -12
  160. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/RECORD +168 -164
  161. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/entry_points.txt +4 -2
  162. localstack_core-4.10.1.dev7.dist-info/plux.json +1 -0
  163. localstack/packages/terraform.py +0 -46
  164. localstack/services/cloudformation/deploy.html +0 -144
  165. localstack/services/cloudformation/deploy_ui.py +0 -47
  166. localstack/services/cloudformation/plugins.py +0 -12
  167. localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
  168. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack +0 -0
  169. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack-supervisor +0 -0
  170. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack.bat +0 -0
  171. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/WHEEL +0 -0
  172. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/licenses/LICENSE.txt +0 -0
  173. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/top_level.txt +0 -0
localstack/constants.py CHANGED
@@ -80,6 +80,10 @@ ENV_INTERNAL_TEST_RUN = "LOCALSTACK_INTERNAL_TEST_RUN"
80
80
  # environment variable name to tag collect metrics during a test run
81
81
  ENV_INTERNAL_TEST_COLLECT_METRIC = "LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC"
82
82
 
83
+ # environment variable name to indicate that metrics should be stored within the container
84
+ ENV_INTERNAL_TEST_STORE_METRICS_IN_LOCALSTACK = "LOCALSTACK_INTERNAL_TEST_METRICS_IN_LOCALSTACK"
85
+ ENV_INTERNAL_TEST_STORE_METRICS_PATH = "LOCALSTACK_INTERNAL_TEST_STORE_METRICS_PATH"
86
+
83
87
  # environment variable that flags whether pro was activated. do not use it for security purposes!
84
88
  ENV_PRO_ACTIVATED = "PRO_ACTIVATED"
85
89
 
@@ -102,32 +106,6 @@ FALSE_STRINGS = ("0", "false", "False")
102
106
  # strings with valid log levels for LS_LOG
103
107
  LOG_LEVELS = ("trace-internal", "trace", "debug", "info", "warn", "error", "warning")
104
108
 
105
- # the version of elasticsearch that is pre-seeded into the base image (sync with Dockerfile.base)
106
- ELASTICSEARCH_DEFAULT_VERSION = "Elasticsearch_7.10"
107
- # See https://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/aes-supported-plugins.html
108
- ELASTICSEARCH_PLUGIN_LIST = [
109
- "analysis-icu",
110
- "ingest-attachment",
111
- "analysis-kuromoji",
112
- "mapper-murmur3",
113
- "mapper-size",
114
- "analysis-phonetic",
115
- "analysis-smartcn",
116
- "analysis-stempel",
117
- "analysis-ukrainian",
118
- ]
119
- # Default ES modules to exclude (save apprx 66MB in the final image)
120
- ELASTICSEARCH_DELETE_MODULES = ["ingest-geoip"]
121
-
122
- # the version of opensearch which is used by default
123
- OPENSEARCH_DEFAULT_VERSION = "OpenSearch_2.11"
124
-
125
- # See https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-plugins.html
126
- OPENSEARCH_PLUGIN_LIST = [
127
- "ingest-attachment",
128
- "analysis-kuromoji",
129
- ]
130
-
131
109
  # API endpoint for analytics events
132
110
  API_ENDPOINT = os.environ.get("API_ENDPOINT") or "https://api.localstack.cloud/v1"
133
111
  # new analytics API endpoint
@@ -171,9 +149,6 @@ DEFAULT_DEVELOP_PORT = 5678
171
149
  DEFAULT_BUCKET_MARKER_LOCAL = "hot-reload"
172
150
  LEGACY_DEFAULT_BUCKET_MARKER_LOCAL = "__local__"
173
151
 
174
- # user that starts the opensearch process if the current user is root
175
- OS_USER_OPENSEARCH = "localstack"
176
-
177
152
  # output string that indicates that the stack is ready
178
153
  READY_MARKER_OUTPUT = "Ready."
179
154
 
@@ -32,7 +32,7 @@ def generate_mount_points(
32
32
 
33
33
  # container paths
34
34
  target_path = "/opt/code/localstack/"
35
- venv_path = os.path.join(target_path, ".venv", "lib", "python3.11", "site-packages")
35
+ venv_path = os.path.join(target_path, ".venv", "lib", "python3.13", "site-packages")
36
36
 
37
37
  # Community code
38
38
  if pro:
@@ -68,7 +68,7 @@ class ContainerPaths:
68
68
  """Important paths in the container"""
69
69
 
70
70
  project_dir: str = "/opt/code/localstack"
71
- site_packages_target_dir: str = "/opt/code/localstack/.venv/lib/python3.11/site-packages"
71
+ site_packages_target_dir: str = "/opt/code/localstack/.venv/lib/python3.13/site-packages"
72
72
  docker_entrypoint: str = "/usr/local/bin/docker-entrypoint.sh"
73
73
  localstack_supervisor: str = "/usr/local/bin/localstack-supervisor"
74
74
  localstack_source_dir: str
localstack/dns/plugins.py CHANGED
@@ -11,8 +11,12 @@ DNS_SHUTDOWN_PRIORITY = -30
11
11
  """Make sure the DNS server is shut down after the ON_AFTER_SERVICE_SHUTDOWN_HANDLERS, which in turn is after
12
12
  SERVICE_SHUTDOWN_PRIORITY. Currently this value needs to be less than -20"""
13
13
 
14
+ DNS_START_PRIORITY = 20
15
+ """Make sure the DNS server is started before the pro activation, to ensure proper DNS resolution for the activate call,
16
+ if the resolv.conf is set to localhost from outside the container"""
14
17
 
15
- @hooks.on_infra_start(priority=10)
18
+
19
+ @hooks.on_infra_start(priority=DNS_START_PRIORITY)
16
20
  def start_dns_server():
17
21
  try:
18
22
  from localstack.dns import server
localstack/dns/server.py CHANGED
@@ -445,13 +445,22 @@ class Resolver(DnsServerProtocol):
445
445
  return True
446
446
  return False
447
447
 
448
+ def _find_matching_aliases(self, question: DNSQuestion) -> list[AliasTarget] | None:
449
+ """
450
+ Find aliases matching the question, supporting wildcards.
451
+ """
452
+ qlabel = DNSLabel(to_bytes(question.qname))
453
+ qtype = RecordType[QTYPE[question.qtype]]
454
+ for (label, rtype), targets in self.aliases.items():
455
+ if rtype == qtype and qlabel.matchWildcard(label):
456
+ return targets
457
+ return None
458
+
448
459
  def _resolve_alias(
449
460
  self, request: DNSRecord, reply: DNSRecord, client_address: ClientAddress
450
461
  ) -> bool:
451
462
  if request.q.qtype in (QTYPE.A, QTYPE.AAAA, QTYPE.CNAME):
452
- key = (DNSLabel(to_bytes(request.q.qname)), RecordType[QTYPE[request.q.qtype]])
453
- # check if we have aliases defined for our given qname/qtype pair
454
- if aliases := self.aliases.get(key):
463
+ if aliases := self._find_matching_aliases(request.q):
455
464
  for alias in aliases:
456
465
  # if there is no health check, or the healthcheck is successful, we will consider this alias
457
466
  # take the first alias passing this check
@@ -3,10 +3,11 @@ import functools
3
3
  import logging
4
4
  import os
5
5
  from collections import defaultdict
6
+ from collections.abc import Callable
6
7
  from enum import Enum
7
8
  from inspect import getmodule
8
- from threading import RLock
9
- from typing import Any, Callable, Generic, Optional, ParamSpec, TypeVar
9
+ from threading import Lock, RLock
10
+ from typing import Any, Generic, ParamSpec, TypeVar
10
11
 
11
12
  from plux import Plugin, PluginManager, PluginSpec # type: ignore
12
13
 
@@ -56,7 +57,7 @@ class PackageInstaller(abc.ABC):
56
57
  multiple versions).
57
58
  """
58
59
 
59
- def __init__(self, name: str, version: str, install_lock: Optional[RLock] = None):
60
+ def __init__(self, name: str, version: str, install_lock: Lock | None = None):
60
61
  """
61
62
  :param name: technical package name, f.e. "opensearch"
62
63
  :param version: version of the package to install
@@ -70,7 +71,7 @@ class PackageInstaller(abc.ABC):
70
71
  self.install_lock = install_lock or RLock()
71
72
  self._setup_for_target: dict[InstallTarget, bool] = defaultdict(lambda: False)
72
73
 
73
- def install(self, target: Optional[InstallTarget] = None) -> None:
74
+ def install(self, target: InstallTarget | None = None) -> None:
74
75
  """
75
76
  Performs the package installation.
76
77
 
@@ -210,7 +211,7 @@ class Package(abc.ABC, Generic[T]):
210
211
  """
211
212
  return self.get_installer(version).get_installed_dir()
212
213
 
213
- def install(self, version: str | None = None, target: Optional[InstallTarget] = None) -> None:
214
+ def install(self, version: str | None = None, target: InstallTarget | None = None) -> None:
214
215
  """
215
216
  Installs the package in the given version in the preferred target location.
216
217
  :param version: version of the package to install. If None, the default version of the package will be used.
@@ -274,7 +275,7 @@ class MultiPackageInstaller(PackageInstaller):
274
275
  assert len(package_installer) > 0
275
276
  self.package_installer = package_installer
276
277
 
277
- def install(self, target: Optional[InstallTarget] = None) -> None:
278
+ def install(self, target: InstallTarget | None = None) -> None:
278
279
  """
279
280
  Installs the different packages this installer is composed of.
280
281
 
@@ -356,7 +357,7 @@ class PackagesPluginManager(PluginManager[PackagesPlugin]): # type: ignore[misc
356
357
  )
357
358
 
358
359
  def get_packages(
359
- self, package_names: list[str], version: Optional[str] = None
360
+ self, package_names: list[str], version: str | None = None
360
361
  ) -> list[Package[PackageInstaller]]:
361
362
  # Plugin names are unique, but there could be multiple packages with the same name in different scopes
362
363
  plugin_specs_per_name = defaultdict(list)
@@ -390,7 +391,7 @@ T2 = TypeVar("T2")
390
391
  def package(
391
392
  name: str | None = None,
392
393
  scope: str = "community",
393
- should_load: Optional[Callable[[], bool]] = None,
394
+ should_load: Callable[[], bool] | None = None,
394
395
  ) -> Callable[[Callable[[], Package[Any] | list[Package[Any]]]], PluginSpec]:
395
396
  """
396
397
  Decorator for marking methods that create Package instances as a PackagePlugin.
@@ -4,7 +4,7 @@ import re
4
4
  from abc import ABC
5
5
  from functools import lru_cache
6
6
  from sys import version_info
7
- from typing import Any, Optional
7
+ from typing import Any
8
8
 
9
9
  import requests
10
10
 
@@ -238,7 +238,7 @@ class NodePackageInstaller(ExecutableInstaller):
238
238
  self,
239
239
  package_name: str,
240
240
  version: str,
241
- package_spec: Optional[str] = None,
241
+ package_spec: str | None = None,
242
242
  main_module: str = "main.js",
243
243
  ):
244
244
  """
@@ -5,14 +5,6 @@ from localstack.packages.api import Package, package
5
5
  if TYPE_CHECKING:
6
6
  from localstack.packages.ffmpeg import FfmpegPackageInstaller
7
7
  from localstack.packages.java import JavaPackageInstaller
8
- from localstack.packages.terraform import TerraformPackageInstaller
9
-
10
-
11
- @package(name="terraform")
12
- def terraform_package() -> Package["TerraformPackageInstaller"]:
13
- from .terraform import terraform_package
14
-
15
- return terraform_package
16
8
 
17
9
 
18
10
  @package(name="ffmpeg")
@@ -89,7 +89,7 @@ class ShellScriptRunner(ScriptRunner):
89
89
  suffixes = [".sh"]
90
90
 
91
91
  def run(self, path: str) -> None:
92
- exit_code = subprocess.call(args=[], executable=path)
92
+ exit_code = subprocess.call(args=[path])
93
93
  if exit_code != 0:
94
94
  raise OSError(f"Script {path} returned a non-zero exit code {exit_code}")
95
95
 
@@ -323,8 +323,18 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
323
323
  tags: MapOfStringToString = None,
324
324
  **kwargs,
325
325
  ) -> ApiKey:
326
+ if name and len(name) > 1024:
327
+ raise BadRequestException("Invalid API Key name, can be at most 1024 characters.")
328
+ if value:
329
+ if len(value) > 128:
330
+ raise BadRequestException("API Key value exceeds maximum size of 128 characters")
331
+ elif len(value) < 20:
332
+ raise BadRequestException("API Key value should be at least 20 characters")
333
+ if description and len(description) > 125000:
334
+ raise BadRequestException("Invalid API Key description specified.")
326
335
  api_key = call_moto(context)
327
-
336
+ if name == "":
337
+ api_key.pop("name", None)
328
338
  # transform array of stage keys [{'restApiId': '0iscapk09u', 'stageName': 'dev'}] into
329
339
  # array of strings ['0iscapk09u/dev']
330
340
  stage_keys = api_key.get("stageKeys", [])
@@ -2054,6 +2064,20 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2054
2064
  for integration_response in integration_responses.values():
2055
2065
  remove_empty_attributes_from_integration_response(integration_response)
2056
2066
 
2067
+ if response.get("connectionType") == "VPC_LINK":
2068
+ # FIXME: this is hacky to workaround moto not saving the VPC Link `connectionId`
2069
+ # only do this internal check of Moto if the integration is of VPC_LINK type
2070
+ moto_rest_api = get_moto_rest_api(context=context, rest_api_id=rest_api_id)
2071
+ try:
2072
+ method = moto_rest_api.resources[resource_id].resource_methods[http_method]
2073
+ integration = method.method_integration
2074
+ if connection_id := getattr(integration, "connection_id", None):
2075
+ response["connectionId"] = connection_id
2076
+
2077
+ except (AttributeError, KeyError):
2078
+ # this error should have been caught by `call_moto`
2079
+ pass
2080
+
2057
2081
  return response
2058
2082
 
2059
2083
  def put_integration(
@@ -2108,6 +2132,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2108
2132
  moto_request.setdefault("timeoutInMillis", 29000)
2109
2133
  if integration_type in (IntegrationType.HTTP, IntegrationType.HTTP_PROXY):
2110
2134
  moto_request.setdefault("connectionType", ConnectionType.INTERNET)
2135
+
2111
2136
  response = call_moto_with_request(context, moto_request)
2112
2137
  remove_empty_attributes_from_integration(integration=response)
2113
2138
 
@@ -2115,6 +2140,13 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2115
2140
  if integration_type == "MOCK":
2116
2141
  response.pop("uri", None)
2117
2142
 
2143
+ # TODO: moto does not save the connection_id
2144
+ elif moto_request.get("connectionType") == "VPC_LINK":
2145
+ connection_id = moto_request.get("connectionId", "")
2146
+ # attach the connection id to the moto object
2147
+ method.method_integration.connection_id = connection_id
2148
+ response["connectionId"] = connection_id
2149
+
2118
2150
  return response
2119
2151
 
2120
2152
  def update_integration(
@@ -2136,6 +2168,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2136
2168
  raise NotFoundException("Invalid Integration identifier specified")
2137
2169
 
2138
2170
  integration = method.method_integration
2171
+ # TODO: validate the patch operations
2139
2172
  patch_api_gateway_entity(integration, patch_operations)
2140
2173
 
2141
2174
  # fix data types
@@ -2144,8 +2177,12 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2144
2177
  if skip_verification := (integration.tls_config or {}).get("insecureSkipVerification"):
2145
2178
  integration.tls_config["insecureSkipVerification"] = str_to_bool(skip_verification)
2146
2179
 
2147
- integration_dict: Integration = integration.to_json()
2148
- return integration_dict
2180
+ response: Integration = integration.to_json()
2181
+
2182
+ if connection_id := getattr(integration, "connection_id", None):
2183
+ response["connectionId"] = connection_id
2184
+
2185
+ return response
2149
2186
 
2150
2187
  def delete_integration(
2151
2188
  self,
@@ -2393,6 +2430,10 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2393
2430
  for api_key in api_keys:
2394
2431
  api_key.pop("value")
2395
2432
 
2433
+ if limit is not None:
2434
+ if limit < 1 or limit > 500:
2435
+ limit = None
2436
+
2396
2437
  item_list = PaginatedList(api_keys)
2397
2438
 
2398
2439
  def token_generator(item):
@@ -2417,6 +2458,14 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2417
2458
  patch_operations: ListOfPatchOperation = None,
2418
2459
  **kwargs,
2419
2460
  ) -> ApiKey:
2461
+ for patch_op in patch_operations:
2462
+ if patch_op["path"] not in ("/description", "/enabled", "/name", "/customerId"):
2463
+ raise BadRequestException(
2464
+ f"Invalid patch path '{patch_op['path']}' specified for op '{patch_op['op']}'. Must be one of: [/description, /enabled, /name, /customerId]"
2465
+ )
2466
+
2467
+ if patch_op["path"] == "/description" and len(patch_op["value"]) > 125000:
2468
+ raise BadRequestException("Invalid API Key description specified.")
2420
2469
  response: ApiKey = call_moto(context)
2421
2470
  if "value" in response:
2422
2471
  response.pop("value", None)
@@ -2977,6 +3026,7 @@ def create_custom_context(
2977
3026
  ctx = create_aws_request_context(
2978
3027
  service_name=context.service.service_name,
2979
3028
  action=action,
3029
+ protocol=context.service.protocol,
2980
3030
  parameters=parameters,
2981
3031
  region=context.region,
2982
3032
  )
@@ -211,6 +211,9 @@ class RestApiAwsIntegration(RestApiIntegration):
211
211
  action = parsed_uri["path"]
212
212
 
213
213
  if target := self.get_action_service_target(service_name, action):
214
+ # TODO: properly implement the auto-`Content-Type` headers depending on the service protocol
215
+ # e.g. `x-amz-json-1.0` for DynamoDB
216
+ # this is needed to properly support multi-protocol
214
217
  headers["X-Amz-Target"] = target
215
218
 
216
219
  query_params["Action"] = action
@@ -8,7 +8,7 @@ from werkzeug.datastructures import Headers
8
8
  from localstack.aws.api.apigateway import Integration
9
9
 
10
10
  from ..context import EndpointResponse, IntegrationRequest, RestApiInvocationContext
11
- from ..gateway_response import ApiConfigurationError, IntegrationFailureError
11
+ from ..gateway_response import ApiConfigurationError, IntegrationFailureError, InternalServerError
12
12
  from ..header_utils import build_multi_value_headers
13
13
  from .core import RestApiIntegration
14
14
 
@@ -72,7 +72,7 @@ class RestApiHttpIntegration(BaseRestApiHttpIntegration):
72
72
  except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema) as e:
73
73
  LOG.warning("Execution failed due to configuration error: Invalid endpoint address")
74
74
  LOG.debug("The URI specified for the HTTP/HTTP_PROXY integration is invalid: %s", uri)
75
- raise ApiConfigurationError("Internal server error") from e
75
+ raise InternalServerError("Internal server error") from e
76
76
 
77
77
  except (requests.exceptions.Timeout, requests.exceptions.SSLError) as e:
78
78
  # TODO make the exception catching more fine grained
@@ -127,7 +127,7 @@ class RestApiHttpProxyIntegration(BaseRestApiHttpIntegration):
127
127
  except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema) as e:
128
128
  LOG.warning("Execution failed due to configuration error: Invalid endpoint address")
129
129
  LOG.debug("The URI specified for the HTTP/HTTP_PROXY integration is invalid: %s", uri)
130
- raise ApiConfigurationError("Internal server error") from e
130
+ raise InternalServerError("Internal server error") from e
131
131
 
132
132
  except (requests.exceptions.Timeout, requests.exceptions.SSLError):
133
133
  # TODO make the exception catching more fine grained
@@ -62,6 +62,17 @@ TEST_INVOKE_TEMPLATE_MOCK = """Execution log for request {request_id}
62
62
  {formatted_date} : Method completed with status: {method_response_status}
63
63
  """
64
64
 
65
+ TEST_INVOKE_TEMPLATE_FAILED = """Execution log for request {request_id}
66
+ {formatted_date} : Starting execution for request: {request_id}
67
+ {formatted_date} : HTTP Method: {request_method}, Resource Path: {resource_path}
68
+ {formatted_date} : Method request path: {method_request_path_parameters}
69
+ {formatted_date} : Method request query string: {method_request_query_string}
70
+ {formatted_date} : Method request headers: {method_request_headers}
71
+ {formatted_date} : Method request body before transformations: {method_request_body}
72
+ {formatted_date} : Execution failed due to {error_type}: {error_message}
73
+ {formatted_date} : Method completed with status: {method_response_status}
74
+ """
75
+
65
76
 
66
77
  def _dump_headers(headers: Headers) -> str:
67
78
  if not headers:
@@ -80,9 +91,9 @@ def log_template(invocation_context: RestApiInvocationContext, response_headers:
80
91
  formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
81
92
  request = invocation_context.invocation_request
82
93
  context_var = invocation_context.context_variables
83
- integration_req = invocation_context.integration_request
84
- endpoint_resp = invocation_context.endpoint_response
85
- method_resp = invocation_context.invocation_response
94
+ integration_req = invocation_context.integration_request or {}
95
+ endpoint_resp = invocation_context.endpoint_response or {}
96
+ method_resp = invocation_context.invocation_response or {}
86
97
  # TODO: if endpoint_uri is an ARN, it means it's an AWS_PROXY integration
87
98
  # this should be transformed to the true URL of a lambda invoke call
88
99
  endpoint_uri = integration_req.get("uri", "")
@@ -116,7 +127,7 @@ def log_mock_template(
116
127
  formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
117
128
  request = invocation_context.invocation_request
118
129
  context_var = invocation_context.context_variables
119
- method_resp = invocation_context.invocation_response
130
+ method_resp = invocation_context.invocation_response or {}
120
131
 
121
132
  return TEST_INVOKE_TEMPLATE_MOCK.format(
122
133
  formatted_date=formatted_date,
@@ -133,6 +144,29 @@ def log_mock_template(
133
144
  )
134
145
 
135
146
 
147
+ def log_failed_template(
148
+ invocation_context: RestApiInvocationContext, response_status_code: int
149
+ ) -> str:
150
+ formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
151
+ request = invocation_context.invocation_request
152
+ context_var = invocation_context.context_variables
153
+
154
+ return TEST_INVOKE_TEMPLATE_FAILED.format(
155
+ formatted_date=formatted_date,
156
+ request_id=context_var["requestId"],
157
+ resource_path=request["path"],
158
+ request_method=request["http_method"],
159
+ method_request_path_parameters=dict_to_string(request["path_parameters"]),
160
+ method_request_query_string=dict_to_string(request["query_string_parameters"]),
161
+ method_request_headers=_dump_headers(request.get("headers")),
162
+ method_request_body=to_str(request.get("body", "")),
163
+ method_response_status=response_status_code,
164
+ # TODO: fix the error message
165
+ error_type="",
166
+ error_message="",
167
+ )
168
+
169
+
136
170
  def create_test_chain() -> HandlerChain[RestApiInvocationContext]:
137
171
  return HandlerChain(
138
172
  request_handlers=[
@@ -216,7 +250,9 @@ def create_test_invocation_context(
216
250
  responseOverride=ContextVarsResponseOverride(header={}, status=0),
217
251
  )
218
252
  invocation_context.trace_id = parse_handler.populate_trace_id({})
219
- resource_method = resource["resourceMethods"][http_method]
253
+ resource_method = (
254
+ resource["resourceMethods"].get(http_method) or resource["resourceMethods"]["ANY"]
255
+ )
220
256
  invocation_context.resource = resource
221
257
  invocation_context.resource_method = resource_method
222
258
  invocation_context.integration = resource_method["methodIntegration"]
@@ -256,7 +292,15 @@ def run_test_invocation(
256
292
  # AWS does not return the Content-Length for TestInvokeMethod
257
293
  response_headers.remove("Content-Length")
258
294
 
259
- if is_mock_integration:
295
+ if not invocation_context.invocation_response:
296
+ # TODO: this is an heuristic to guess if we encounter an exception in the call
297
+ # in the future, we should attach the exception to the context so we could act on it and properly
298
+ # log as we go through the invocation, so that if we have an error we stop logging at the right moment
299
+ for header in ("Content-Type", "X-Amzn-Trace-Id"):
300
+ response_headers.remove(header)
301
+ log = log_failed_template(invocation_context, test_response.status_code)
302
+
303
+ elif is_mock_integration:
260
304
  # TODO: revisit how we're building the logs
261
305
  log = log_mock_template(invocation_context, response_headers)
262
306
  else:
@@ -429,6 +429,11 @@ class ApigatewayNextGenProvider(ApigatewayProvider):
429
429
  if not resource:
430
430
  raise NotFoundException("Invalid Resource identifier specified")
431
431
 
432
+ resource_methods = resource.resource_methods
433
+
434
+ if request["httpMethod"] not in resource_methods and "ANY" not in resource_methods:
435
+ raise NotFoundException("Invalid Method identifier specified")
436
+
432
437
  # test httpMethod
433
438
 
434
439
  rest_api_container = get_rest_api_container(context, rest_api_id=rest_api_id)
@@ -14,7 +14,13 @@ from localstack.services.cloudformation.engine.v2.change_set_model import (
14
14
  )
15
15
  from localstack.utils.aws import arns
16
16
  from localstack.utils.collections import select_attributes
17
- from localstack.utils.id_generator import ExistingIds, ResourceIdentifier, Tags, generate_short_uid
17
+ from localstack.utils.id_generator import (
18
+ ExistingIds,
19
+ ResourceIdentifier,
20
+ Tags,
21
+ generate_short_uid,
22
+ generate_uid,
23
+ )
18
24
  from localstack.utils.json import clone_safe
19
25
  from localstack.utils.objects import recurse_object
20
26
  from localstack.utils.strings import long_uid, short_uid
@@ -75,6 +81,11 @@ class StackIdentifier(ResourceIdentifier):
75
81
  return generate_short_uid(resource_identifier=self, existing_ids=existing_ids, tags=tags)
76
82
 
77
83
 
84
+ class StackIdentifierV2(StackIdentifier):
85
+ def generate(self, existing_ids: ExistingIds = None, tags: Tags = None) -> str:
86
+ return generate_uid(resource_identifier=self, existing_ids=existing_ids, tags=tags)
87
+
88
+
78
89
  # TODO: remove metadata (flatten into individual fields)
79
90
  class Stack:
80
91
  change_sets: list["StackChangeSet"]
@@ -681,9 +681,6 @@ class ChangeSetModel:
681
681
  scope=arguments_scope, before_value=before_arguments, after_value=after_arguments
682
682
  )
683
683
 
684
- if intrinsic_function == "Ref" and arguments.value == "AWS::NoValue":
685
- arguments.value = Nothing
686
-
687
684
  if is_created(before=before_arguments, after=after_arguments):
688
685
  change_type = ChangeType.CREATED
689
686
  elif is_removed(before=before_arguments, after=after_arguments):
@@ -20,6 +20,7 @@ from localstack.services.cloudformation.engine.v2.change_set_model_preproc impor
20
20
  PreprocResource,
21
21
  )
22
22
  from localstack.services.cloudformation.v2.entities import ChangeSet
23
+ from localstack.utils.numbers import is_number
23
24
 
24
25
  CHANGESET_KNOWN_AFTER_APPLY: Final[str] = "{{changeSet:KNOWN_AFTER_APPLY}}"
25
26
 
@@ -96,6 +97,19 @@ class ChangeSetModelDescriber(ChangeSetModelPreproc):
96
97
 
97
98
  return value
98
99
 
100
+ def visit_node_intrinsic_function(self, node_intrinsic_function: NodeIntrinsicFunction):
101
+ """
102
+ Intrinsic function results are always strings when referring to the describe output
103
+ """
104
+ # TODO: what about other places?
105
+ # TODO: should this be put in the preproc?
106
+ delta = super().visit_node_intrinsic_function(node_intrinsic_function)
107
+ if is_number(delta.before):
108
+ delta.before = str(delta.before)
109
+ if is_number(delta.after):
110
+ delta.after = str(delta.after)
111
+ return delta
112
+
99
113
  def visit_node_intrinsic_function_fn_join(
100
114
  self, node_intrinsic_function: NodeIntrinsicFunction
101
115
  ) -> PreprocEntityDelta:
@@ -1,5 +1,6 @@
1
1
  import copy
2
2
  import logging
3
+ import os
3
4
  import re
4
5
  import uuid
5
6
  from collections.abc import Callable
@@ -105,14 +106,15 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
105
106
  except Exception as e:
106
107
  failure_message = str(e)
107
108
 
109
+ is_deletion = self._change_set.stack.status == StackStatus.DELETE_IN_PROGRESS
108
110
  if self._deferred_actions:
109
- if failure_message:
110
- # TODO: differentiate between update and create
111
- self._change_set.stack.set_stack_status(StackStatus.ROLLBACK_IN_PROGRESS)
112
- else:
111
+ if not is_deletion:
113
112
  # TODO: correct status
113
+ # TODO: differentiate between update and create
114
114
  self._change_set.stack.set_stack_status(
115
- StackStatus.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
115
+ StackStatus.ROLLBACK_IN_PROGRESS
116
+ if failure_message
117
+ else StackStatus.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
116
118
  )
117
119
 
118
120
  # perform all deferred actions such as deletions. These must happen in reverse from their
@@ -122,7 +124,7 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
122
124
  LOG.debug("executing deferred action: '%s'", deferred.name)
123
125
  deferred.action()
124
126
 
125
- if failure_message:
127
+ if failure_message and not is_deletion:
126
128
  # TODO: differentiate between update and create
127
129
  self._change_set.stack.set_stack_status(StackStatus.ROLLBACK_COMPLETE)
128
130
 
@@ -515,14 +517,11 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
515
517
  resource_type,
516
518
  f'No resource provider found for "{resource_type}"',
517
519
  )
518
- LOG.warning(
519
- "Deployment of resource type %s successful due to config CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES",
520
- resource_type,
521
- )
522
- LOG.warning(
523
- "Deployment of resource type %s will fail in upcoming LocalStack releases unless CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES is explicitly enabled.",
524
- resource_type,
525
- )
520
+ if "CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES" not in os.environ:
521
+ LOG.warning(
522
+ "Deployment of resource type %s succeeded, but will fail in upcoming LocalStack releases unless CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES is explicitly enabled.",
523
+ resource_type,
524
+ )
526
525
  event = ProgressEvent(
527
526
  OperationStatus.SUCCESS,
528
527
  resource_model={},
@@ -577,7 +576,6 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
577
576
  )
578
577
  # TODO: do we actually need this line?
579
578
  resolved_resource.update(extra_resource_properties)
580
-
581
579
  case OperationStatus.FAILED:
582
580
  reason = event.message
583
581
  LOG.warning(