localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev42__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 (208) hide show
  1. localstack/aws/api/acm/__init__.py +122 -122
  2. localstack/aws/api/apigateway/__init__.py +560 -559
  3. localstack/aws/api/cloudcontrol/__init__.py +63 -63
  4. localstack/aws/api/cloudformation/__init__.py +1041 -969
  5. localstack/aws/api/cloudwatch/__init__.py +408 -368
  6. localstack/aws/api/config/__init__.py +788 -786
  7. localstack/aws/api/core.py +4 -0
  8. localstack/aws/api/dynamodb/__init__.py +753 -759
  9. localstack/aws/api/dynamodbstreams/__init__.py +74 -74
  10. localstack/aws/api/ec2/__init__.py +9713 -8573
  11. localstack/aws/api/es/__init__.py +453 -453
  12. localstack/aws/api/events/__init__.py +552 -552
  13. localstack/aws/api/firehose/__init__.py +541 -543
  14. localstack/aws/api/iam/__init__.py +646 -572
  15. localstack/aws/api/kinesis/__init__.py +251 -144
  16. localstack/aws/api/kms/__init__.py +343 -333
  17. localstack/aws/api/lambda_/__init__.py +585 -571
  18. localstack/aws/api/logs/__init__.py +682 -666
  19. localstack/aws/api/opensearch/__init__.py +814 -785
  20. localstack/aws/api/pipes/__init__.py +336 -336
  21. localstack/aws/api/redshift/__init__.py +1192 -1164
  22. localstack/aws/api/resource_groups/__init__.py +175 -175
  23. localstack/aws/api/resourcegroupstaggingapi/__init__.py +67 -67
  24. localstack/aws/api/route53/__init__.py +256 -254
  25. localstack/aws/api/route53resolver/__init__.py +396 -396
  26. localstack/aws/api/s3/__init__.py +1358 -1345
  27. localstack/aws/api/s3control/__init__.py +616 -584
  28. localstack/aws/api/scheduler/__init__.py +118 -118
  29. localstack/aws/api/secretsmanager/__init__.py +193 -193
  30. localstack/aws/api/ses/__init__.py +227 -227
  31. localstack/aws/api/sns/__init__.py +115 -115
  32. localstack/aws/api/sqs/__init__.py +100 -100
  33. localstack/aws/api/ssm/__init__.py +1978 -1970
  34. localstack/aws/api/stepfunctions/__init__.py +323 -323
  35. localstack/aws/api/sts/__init__.py +90 -66
  36. localstack/aws/api/support/__init__.py +112 -112
  37. localstack/aws/api/swf/__init__.py +378 -386
  38. localstack/aws/api/transcribe/__init__.py +425 -425
  39. localstack/aws/client.py +7 -2
  40. localstack/aws/forwarder.py +52 -5
  41. localstack/aws/handlers/analytics.py +1 -1
  42. localstack/aws/handlers/logging.py +12 -2
  43. localstack/aws/handlers/metric_handler.py +41 -1
  44. localstack/aws/handlers/service.py +43 -10
  45. localstack/aws/protocol/parser.py +440 -21
  46. localstack/aws/protocol/serializer.py +684 -64
  47. localstack/aws/protocol/service_router.py +120 -20
  48. localstack/aws/scaffold.py +15 -17
  49. localstack/aws/skeleton.py +4 -2
  50. localstack/aws/spec-patches.json +58 -0
  51. localstack/aws/spec.py +33 -13
  52. localstack/cli/exceptions.py +1 -1
  53. localstack/cli/localstack.py +10 -5
  54. localstack/cli/lpm.py +3 -4
  55. localstack/cli/profiles.py +1 -2
  56. localstack/config.py +18 -12
  57. localstack/constants.py +4 -29
  58. localstack/dev/kubernetes/__main__.py +39 -4
  59. localstack/dev/run/paths.py +1 -1
  60. localstack/dns/plugins.py +5 -1
  61. localstack/dns/server.py +12 -3
  62. localstack/packages/api.py +9 -8
  63. localstack/packages/core.py +2 -2
  64. localstack/packages/plugins.py +0 -8
  65. localstack/runtime/init.py +1 -1
  66. localstack/services/apigateway/helpers.py +5 -9
  67. localstack/services/apigateway/legacy/provider.py +85 -12
  68. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  69. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  70. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
  71. localstack/services/apigateway/next_gen/provider.py +5 -0
  72. localstack/services/apigateway/patches.py +0 -9
  73. localstack/services/cloudformation/engine/entities.py +12 -1
  74. localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
  75. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
  76. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
  77. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
  78. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
  79. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
  80. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  81. localstack/services/cloudformation/engine/v2/resolving.py +6 -4
  82. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  83. localstack/services/cloudformation/provider.py +2 -2
  84. localstack/services/cloudformation/resource_provider.py +5 -1
  85. localstack/services/cloudformation/resources.py +24149 -0
  86. localstack/services/cloudformation/v2/entities.py +6 -3
  87. localstack/services/cloudformation/v2/provider.py +178 -33
  88. localstack/services/cloudformation/v2/types.py +8 -4
  89. localstack/services/cloudwatch/provider_v2.py +25 -28
  90. localstack/services/dynamodb/packages.py +2 -1
  91. localstack/services/dynamodb/provider.py +42 -0
  92. localstack/services/dynamodb/v2/provider.py +42 -0
  93. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  94. localstack/services/es/provider.py +2 -2
  95. localstack/services/events/event_rule_engine.py +31 -13
  96. localstack/services/events/models.py +4 -5
  97. localstack/services/events/target.py +17 -9
  98. localstack/services/iam/provider.py +11 -116
  99. localstack/services/iam/resources/policy_simulator.py +133 -0
  100. localstack/services/kinesis/models.py +15 -2
  101. localstack/services/kinesis/packages.py +1 -1
  102. localstack/services/kinesis/provider.py +77 -0
  103. localstack/services/kms/models.py +34 -4
  104. localstack/services/kms/provider.py +107 -21
  105. localstack/services/lambda_/api_utils.py +3 -1
  106. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  107. localstack/services/lambda_/packages.py +1 -1
  108. localstack/services/lambda_/provider.py +1 -1
  109. localstack/services/lambda_/runtimes.py +8 -3
  110. localstack/services/logs/provider.py +36 -19
  111. localstack/services/moto.py +2 -1
  112. localstack/services/opensearch/cluster.py +15 -7
  113. localstack/services/opensearch/packages.py +26 -7
  114. localstack/services/opensearch/provider.py +6 -1
  115. localstack/services/opensearch/versions.py +56 -7
  116. localstack/services/s3/constants.py +5 -2
  117. localstack/services/s3/cors.py +4 -4
  118. localstack/services/s3/notifications.py +1 -1
  119. localstack/services/s3/presigned_url.py +27 -43
  120. localstack/services/s3/provider.py +68 -12
  121. localstack/services/s3/utils.py +42 -11
  122. localstack/services/ses/provider.py +16 -7
  123. localstack/services/sns/constants.py +7 -1
  124. localstack/services/sns/v2/models.py +190 -0
  125. localstack/services/sns/v2/provider.py +992 -2
  126. localstack/services/sns/v2/utils.py +138 -0
  127. localstack/services/sqs/developer_api.py +205 -0
  128. localstack/services/sqs/models.py +79 -13
  129. localstack/services/sqs/provider.py +8 -309
  130. localstack/services/sqs/query_api.py +1 -1
  131. localstack/services/sqs/utils.py +121 -2
  132. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  133. localstack/testing/aws/cloudformation_utils.py +1 -1
  134. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  135. localstack/testing/pytest/container.py +4 -5
  136. localstack/testing/pytest/fixtures.py +20 -19
  137. localstack/testing/pytest/in_memory_localstack.py +0 -4
  138. localstack/testing/pytest/marking.py +13 -4
  139. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  140. localstack/testing/pytest/util.py +1 -1
  141. localstack/testing/pytest/validation_tracking.py +1 -2
  142. localstack/testing/snapshots/transformer_utility.py +7 -0
  143. localstack/testing/testselection/matching.py +0 -1
  144. localstack/utils/analytics/events.py +2 -2
  145. localstack/utils/analytics/metadata.py +1 -2
  146. localstack/utils/analytics/metrics/counter.py +6 -8
  147. localstack/utils/analytics/publisher.py +1 -2
  148. localstack/utils/analytics/service_request_aggregator.py +2 -2
  149. localstack/utils/archives.py +11 -11
  150. localstack/utils/aws/arns.py +17 -9
  151. localstack/utils/aws/aws_responses.py +7 -7
  152. localstack/utils/aws/aws_stack.py +2 -3
  153. localstack/utils/aws/client_types.py +0 -8
  154. localstack/utils/aws/message_forwarding.py +1 -2
  155. localstack/utils/aws/request_context.py +4 -5
  156. localstack/utils/batch_policy.py +3 -3
  157. localstack/utils/bootstrap.py +7 -7
  158. localstack/utils/catalog/catalog.py +139 -0
  159. localstack/utils/catalog/catalog_loader.py +119 -0
  160. localstack/utils/catalog/common.py +58 -0
  161. localstack/utils/catalog/plugins.py +28 -0
  162. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  163. localstack/utils/collections.py +7 -8
  164. localstack/utils/config_listener.py +1 -1
  165. localstack/utils/container_networking.py +2 -3
  166. localstack/utils/container_utils/container_client.py +115 -131
  167. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  168. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  169. localstack/utils/crypto.py +109 -0
  170. localstack/utils/diagnose.py +2 -3
  171. localstack/utils/docker_utils.py +3 -4
  172. localstack/utils/files.py +31 -7
  173. localstack/utils/functions.py +3 -2
  174. localstack/utils/http.py +4 -5
  175. localstack/utils/json.py +19 -5
  176. localstack/utils/kinesis/kinesis_connector.py +2 -1
  177. localstack/utils/net.py +6 -6
  178. localstack/utils/no_exit_argument_parser.py +2 -2
  179. localstack/utils/numbers.py +9 -2
  180. localstack/utils/objects.py +6 -5
  181. localstack/utils/patch.py +2 -1
  182. localstack/utils/run.py +10 -9
  183. localstack/utils/scheduler.py +11 -11
  184. localstack/utils/server/tcp_proxy.py +2 -2
  185. localstack/utils/serving.py +2 -3
  186. localstack/utils/strings.py +10 -11
  187. localstack/utils/sync.py +126 -1
  188. localstack/utils/tagging.py +1 -4
  189. localstack/utils/testutil.py +5 -4
  190. localstack/utils/threads.py +2 -2
  191. localstack/utils/time.py +11 -3
  192. localstack/utils/urls.py +1 -3
  193. localstack/version.py +2 -2
  194. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/METADATA +19 -13
  195. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/RECORD +203 -199
  196. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/entry_points.txt +4 -2
  197. localstack_core-4.10.1.dev42.dist-info/plux.json +1 -0
  198. localstack/packages/terraform.py +0 -46
  199. localstack/services/cloudformation/deploy.html +0 -144
  200. localstack/services/cloudformation/deploy_ui.py +0 -47
  201. localstack/services/cloudformation/plugins.py +0 -12
  202. localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
  203. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack +0 -0
  204. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack-supervisor +0 -0
  205. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack.bat +0 -0
  206. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/WHEEL +0 -0
  207. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/licenses/LICENSE.txt +0 -0
  208. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/top_level.txt +0 -0
localstack/config.py CHANGED
@@ -10,7 +10,7 @@ import time
10
10
  import warnings
11
11
  from collections import defaultdict
12
12
  from collections.abc import Mapping
13
- from typing import Any, Optional, TypeVar, Union
13
+ from typing import Any, TypeVar
14
14
 
15
15
  from localstack import constants
16
16
  from localstack.constants import (
@@ -19,6 +19,7 @@ from localstack.constants import (
19
19
  DEFAULT_VOLUME_DIR,
20
20
  ENV_INTERNAL_TEST_COLLECT_METRIC,
21
21
  ENV_INTERNAL_TEST_RUN,
22
+ ENV_INTERNAL_TEST_STORE_METRICS_IN_LOCALSTACK,
22
23
  FALSE_STRINGS,
23
24
  LOCALHOST,
24
25
  LOCALHOST_IP,
@@ -208,13 +209,13 @@ class Directories:
208
209
  return str(self.__dict__)
209
210
 
210
211
 
211
- def eval_log_type(env_var_name: str) -> Union[str, bool]:
212
+ def eval_log_type(env_var_name: str) -> str | bool:
212
213
  """Get the log type from environment variable"""
213
214
  ls_log = os.environ.get(env_var_name, "").lower().strip()
214
215
  return ls_log if ls_log in LOG_LEVELS else False
215
216
 
216
217
 
217
- def parse_boolean_env(env_var_name: str) -> Optional[bool]:
218
+ def parse_boolean_env(env_var_name: str) -> bool | None:
218
219
  """Parse the value of the given env variable and return True/False, or None if it is not a boolean value."""
219
220
  value = os.environ.get(env_var_name, "").lower().strip()
220
221
  if value in TRUE_STRINGS:
@@ -649,7 +650,7 @@ class UniqueHostAndPortList(list[HostAndPort]):
649
650
  - Identical identical hosts and ports are de-duped
650
651
  """
651
652
 
652
- def __init__(self, iterable: Union[list[HostAndPort], None] = None):
653
+ def __init__(self, iterable: list[HostAndPort] | None = None):
653
654
  super().__init__(iterable or [])
654
655
  self._ensure_unique()
655
656
 
@@ -1451,6 +1452,11 @@ def is_collect_metrics_mode() -> bool:
1451
1452
  return is_env_true(ENV_INTERNAL_TEST_COLLECT_METRIC)
1452
1453
 
1453
1454
 
1455
+ def store_test_metrics_in_local_filesystem() -> bool:
1456
+ """Returns True if test metrics should be stored in the local filesystem (instead of the system that runs pytest)."""
1457
+ return is_env_true(ENV_INTERNAL_TEST_STORE_METRICS_IN_LOCALSTACK)
1458
+
1459
+
1454
1460
  def collect_config_items() -> list[tuple[str, Any]]:
1455
1461
  """Returns a list of key-value tuples of LocalStack configuration values."""
1456
1462
  none = object() # sentinel object
@@ -1503,10 +1509,10 @@ def get_protocol() -> str:
1503
1509
 
1504
1510
 
1505
1511
  def external_service_url(
1506
- host: Optional[str] = None,
1507
- port: Optional[int] = None,
1508
- protocol: Optional[str] = None,
1509
- subdomains: Optional[str] = None,
1512
+ host: str | None = None,
1513
+ port: int | None = None,
1514
+ protocol: str | None = None,
1515
+ subdomains: str | None = None,
1510
1516
  ) -> str:
1511
1517
  """Returns a service URL (e.g., SQS queue URL) to an external client (e.g., boto3) potentially running on another
1512
1518
  machine than LocalStack. The configurations LOCALSTACK_HOST and USE_SSL can customize these returned URLs.
@@ -1523,10 +1529,10 @@ def external_service_url(
1523
1529
 
1524
1530
 
1525
1531
  def internal_service_url(
1526
- host: Optional[str] = None,
1527
- port: Optional[int] = None,
1528
- protocol: Optional[str] = None,
1529
- subdomains: Optional[str] = None,
1532
+ host: str | None = None,
1533
+ port: int | None = None,
1534
+ protocol: str | None = None,
1535
+ subdomains: str | None = None,
1530
1536
  ) -> str:
1531
1537
  """Returns a service URL for internal use within LocalStack (i.e., same host).
1532
1538
  The configuration USE_SSL can customize these returned URLs but LOCALSTACK_HOST has no effect.
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
 
@@ -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
@@ -32,7 +33,7 @@ def generate_mount_points(
32
33
 
33
34
  # container paths
34
35
  target_path = "/opt/code/localstack/"
35
- venv_path = os.path.join(target_path, ".venv", "lib", "python3.11", "site-packages")
36
+ venv_path = os.path.join(target_path, ".venv", "lib", "python3.13", "site-packages")
36
37
 
37
38
  # Community code
38
39
  if pro:
@@ -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
 
@@ -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
 
@@ -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
@@ -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", [])
@@ -466,11 +476,19 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
466
476
 
467
477
  @handler("PutRestApi", expand=False)
468
478
  def put_rest_api(self, context: RequestContext, request: PutRestApiRequest) -> RestApi:
479
+ body_data = request["body"].read()
480
+ try:
481
+ openapi_spec = parse_json_or_yaml(to_str(body_data))
482
+ except Exception:
483
+ raise BadRequestException("Invalid OpenAPI input.")
469
484
  # TODO: take into account the mode: overwrite or merge
470
485
  # the default is now `merge`, but we are removing everything
471
486
  rest_api = get_moto_rest_api(context, request["restApiId"])
472
487
  rest_api, warnings = import_api_from_openapi_spec(
473
- rest_api, context=context, request=request
488
+ rest_api,
489
+ context=context,
490
+ open_api_spec=openapi_spec,
491
+ mode=request.get("mode") or PutMode.merge,
474
492
  )
475
493
 
476
494
  rest_api.root_resource_id = get_moto_rest_api_root_resource(rest_api)
@@ -1502,7 +1520,10 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
1502
1520
  **kwargs,
1503
1521
  ) -> DocumentationPartIds:
1504
1522
  body_data = body.read()
1505
- openapi_spec = parse_json_or_yaml(to_str(body_data))
1523
+ try:
1524
+ openapi_spec = parse_json_or_yaml(to_str(body_data))
1525
+ except Exception:
1526
+ raise BadRequestException("Unable to build importer with provided input.")
1506
1527
 
1507
1528
  rest_api_container = get_rest_api_container(context, rest_api_id=rest_api_id)
1508
1529
 
@@ -2002,7 +2023,11 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2002
2023
  body_data = body.read()
2003
2024
 
2004
2025
  # create rest api
2005
- openapi_spec = parse_json_or_yaml(to_str(body_data))
2026
+ try:
2027
+ openapi_spec = parse_json_or_yaml(to_str(body_data))
2028
+ except Exception:
2029
+ raise BadRequestException("Invalid OpenAPI input.")
2030
+
2006
2031
  create_api_request = CreateRestApiRequest(name=openapi_spec.get("info").get("title"))
2007
2032
  create_api_context = create_custom_context(
2008
2033
  context,
@@ -2043,17 +2068,39 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2043
2068
  **kwargs,
2044
2069
  ) -> Integration:
2045
2070
  try:
2046
- response: Integration = call_moto(context)
2047
- except CommonServiceException as e:
2048
- # the Exception raised by moto does not have the right message not status code
2049
- if e.code == "NotFoundException":
2050
- raise NotFoundException("Invalid Integration identifier specified")
2051
- raise
2071
+ moto_rest_api = get_moto_rest_api(context, rest_api_id)
2072
+ except NotFoundException:
2073
+ raise NotFoundException("Invalid Resource identifier specified")
2074
+
2075
+ if not (moto_resource := moto_rest_api.resources.get(resource_id)):
2076
+ raise NotFoundException("Invalid Resource identifier specified")
2077
+
2078
+ if not (moto_method := moto_resource.resource_methods.get(http_method)):
2079
+ raise NotFoundException("Invalid Method identifier specified")
2080
+
2081
+ if not moto_method.method_integration:
2082
+ raise NotFoundException("Invalid Integration identifier specified")
2083
+
2084
+ response: Integration = call_moto(context)
2052
2085
 
2053
2086
  if integration_responses := response.get("integrationResponses"):
2054
2087
  for integration_response in integration_responses.values():
2055
2088
  remove_empty_attributes_from_integration_response(integration_response)
2056
2089
 
2090
+ if response.get("connectionType") == "VPC_LINK":
2091
+ # FIXME: this is hacky to workaround moto not saving the VPC Link `connectionId`
2092
+ # only do this internal check of Moto if the integration is of VPC_LINK type
2093
+ moto_rest_api = get_moto_rest_api(context=context, rest_api_id=rest_api_id)
2094
+ try:
2095
+ method = moto_rest_api.resources[resource_id].resource_methods[http_method]
2096
+ integration = method.method_integration
2097
+ if connection_id := getattr(integration, "connection_id", None):
2098
+ response["connectionId"] = connection_id
2099
+
2100
+ except (AttributeError, KeyError):
2101
+ # this error should have been caught by `call_moto`
2102
+ pass
2103
+
2057
2104
  return response
2058
2105
 
2059
2106
  def put_integration(
@@ -2108,6 +2155,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2108
2155
  moto_request.setdefault("timeoutInMillis", 29000)
2109
2156
  if integration_type in (IntegrationType.HTTP, IntegrationType.HTTP_PROXY):
2110
2157
  moto_request.setdefault("connectionType", ConnectionType.INTERNET)
2158
+
2111
2159
  response = call_moto_with_request(context, moto_request)
2112
2160
  remove_empty_attributes_from_integration(integration=response)
2113
2161
 
@@ -2115,6 +2163,13 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2115
2163
  if integration_type == "MOCK":
2116
2164
  response.pop("uri", None)
2117
2165
 
2166
+ # TODO: moto does not save the connection_id
2167
+ elif moto_request.get("connectionType") == "VPC_LINK":
2168
+ connection_id = moto_request.get("connectionId", "")
2169
+ # attach the connection id to the moto object
2170
+ method.method_integration.connection_id = connection_id
2171
+ response["connectionId"] = connection_id
2172
+
2118
2173
  return response
2119
2174
 
2120
2175
  def update_integration(
@@ -2136,6 +2191,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2136
2191
  raise NotFoundException("Invalid Integration identifier specified")
2137
2192
 
2138
2193
  integration = method.method_integration
2194
+ # TODO: validate the patch operations
2139
2195
  patch_api_gateway_entity(integration, patch_operations)
2140
2196
 
2141
2197
  # fix data types
@@ -2144,8 +2200,12 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2144
2200
  if skip_verification := (integration.tls_config or {}).get("insecureSkipVerification"):
2145
2201
  integration.tls_config["insecureSkipVerification"] = str_to_bool(skip_verification)
2146
2202
 
2147
- integration_dict: Integration = integration.to_json()
2148
- return integration_dict
2203
+ response: Integration = integration.to_json()
2204
+
2205
+ if connection_id := getattr(integration, "connection_id", None):
2206
+ response["connectionId"] = connection_id
2207
+
2208
+ return response
2149
2209
 
2150
2210
  def delete_integration(
2151
2211
  self,
@@ -2393,6 +2453,10 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2393
2453
  for api_key in api_keys:
2394
2454
  api_key.pop("value")
2395
2455
 
2456
+ if limit is not None:
2457
+ if limit < 1 or limit > 500:
2458
+ limit = None
2459
+
2396
2460
  item_list = PaginatedList(api_keys)
2397
2461
 
2398
2462
  def token_generator(item):
@@ -2417,6 +2481,14 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2417
2481
  patch_operations: ListOfPatchOperation = None,
2418
2482
  **kwargs,
2419
2483
  ) -> ApiKey:
2484
+ for patch_op in patch_operations:
2485
+ if patch_op["path"] not in ("/description", "/enabled", "/name", "/customerId"):
2486
+ raise BadRequestException(
2487
+ f"Invalid patch path '{patch_op['path']}' specified for op '{patch_op['op']}'. Must be one of: [/description, /enabled, /name, /customerId]"
2488
+ )
2489
+
2490
+ if patch_op["path"] == "/description" and len(patch_op["value"]) > 125000:
2491
+ raise BadRequestException("Invalid API Key description specified.")
2420
2492
  response: ApiKey = call_moto(context)
2421
2493
  if "value" in response:
2422
2494
  response.pop("value", None)
@@ -2977,6 +3049,7 @@ def create_custom_context(
2977
3049
  ctx = create_aws_request_context(
2978
3050
  service_name=context.service.service_name,
2979
3051
  action=action,
3052
+ protocol=context.service.protocol,
2980
3053
  parameters=parameters,
2981
3054
  region=context.region,
2982
3055
  )
@@ -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