localstack-core 4.7.1.dev49__py3-none-any.whl → 4.10.1.dev12__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 (253) hide show
  1. localstack/aws/api/cloudformation/__init__.py +18 -4
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +6 -2
  5. localstack/aws/api/dynamodb/__init__.py +30 -0
  6. localstack/aws/api/ec2/__init__.py +1522 -65
  7. localstack/aws/api/iam/__init__.py +7 -0
  8. localstack/aws/api/kinesis/__init__.py +19 -0
  9. localstack/aws/api/kms/__init__.py +6 -0
  10. localstack/aws/api/lambda_/__init__.py +13 -0
  11. localstack/aws/api/logs/__init__.py +15 -0
  12. localstack/aws/api/redshift/__init__.py +9 -3
  13. localstack/aws/api/route53/__init__.py +5 -0
  14. localstack/aws/api/s3/__init__.py +12 -0
  15. localstack/aws/api/s3control/__init__.py +54 -0
  16. localstack/aws/api/ssm/__init__.py +2 -0
  17. localstack/aws/api/transcribe/__init__.py +17 -0
  18. localstack/aws/client.py +7 -2
  19. localstack/aws/forwarder.py +52 -5
  20. localstack/aws/handlers/analytics.py +1 -1
  21. localstack/aws/handlers/internal_requests.py +6 -1
  22. localstack/aws/handlers/logging.py +12 -2
  23. localstack/aws/handlers/metric_handler.py +41 -1
  24. localstack/aws/handlers/service.py +40 -20
  25. localstack/aws/mocking.py +2 -2
  26. localstack/aws/patches.py +2 -2
  27. localstack/aws/protocol/parser.py +459 -32
  28. localstack/aws/protocol/serializer.py +689 -69
  29. localstack/aws/protocol/service_router.py +120 -20
  30. localstack/aws/protocol/validate.py +1 -1
  31. localstack/aws/scaffold.py +1 -1
  32. localstack/aws/skeleton.py +4 -2
  33. localstack/aws/spec-patches.json +58 -0
  34. localstack/aws/spec.py +37 -16
  35. localstack/cli/exceptions.py +1 -1
  36. localstack/cli/localstack.py +6 -6
  37. localstack/cli/lpm.py +3 -4
  38. localstack/cli/plugins.py +1 -1
  39. localstack/cli/profiles.py +1 -2
  40. localstack/config.py +25 -18
  41. localstack/constants.py +4 -29
  42. localstack/dev/kubernetes/__main__.py +130 -7
  43. localstack/dev/run/configurators.py +1 -4
  44. localstack/dev/run/paths.py +1 -1
  45. localstack/dns/plugins.py +5 -1
  46. localstack/dns/server.py +13 -4
  47. localstack/logging/format.py +3 -3
  48. localstack/packages/api.py +9 -8
  49. localstack/packages/core.py +2 -2
  50. localstack/packages/plugins.py +0 -8
  51. localstack/runtime/analytics.py +3 -0
  52. localstack/runtime/hooks.py +1 -1
  53. localstack/runtime/init.py +2 -2
  54. localstack/runtime/main.py +5 -5
  55. localstack/runtime/patches.py +2 -2
  56. localstack/services/apigateway/helpers.py +1 -4
  57. localstack/services/apigateway/legacy/helpers.py +7 -8
  58. localstack/services/apigateway/legacy/integration.py +4 -3
  59. localstack/services/apigateway/legacy/invocations.py +6 -5
  60. localstack/services/apigateway/legacy/provider.py +148 -68
  61. localstack/services/apigateway/legacy/templates.py +1 -1
  62. localstack/services/apigateway/next_gen/execute_api/handlers/method_request.py +7 -2
  63. localstack/services/apigateway/next_gen/execute_api/handlers/resource_router.py +1 -2
  64. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  65. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  66. localstack/services/apigateway/next_gen/execute_api/template_mapping.py +2 -2
  67. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +114 -9
  68. localstack/services/apigateway/next_gen/provider.py +5 -0
  69. localstack/services/apigateway/resource_providers/aws_apigateway_resource.py +1 -1
  70. localstack/services/cloudformation/api_utils.py +4 -8
  71. localstack/services/cloudformation/cfn_utils.py +1 -1
  72. localstack/services/cloudformation/engine/entities.py +14 -4
  73. localstack/services/cloudformation/engine/template_deployer.py +6 -4
  74. localstack/services/cloudformation/engine/transformers.py +6 -4
  75. localstack/services/cloudformation/engine/v2/change_set_model.py +201 -13
  76. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +52 -3
  77. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +117 -76
  78. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +205 -52
  79. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +350 -116
  80. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +56 -14
  81. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  82. localstack/services/cloudformation/engine/v2/resolving.py +7 -5
  83. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  84. localstack/services/cloudformation/provider.py +7 -5
  85. localstack/services/cloudformation/resource_provider.py +7 -1
  86. localstack/services/cloudformation/resources.py +24149 -0
  87. localstack/services/cloudformation/service_models.py +2 -2
  88. localstack/services/cloudformation/v2/entities.py +19 -9
  89. localstack/services/cloudformation/v2/provider.py +336 -106
  90. localstack/services/cloudformation/v2/types.py +13 -7
  91. localstack/services/cloudformation/v2/utils.py +4 -1
  92. localstack/services/cloudwatch/alarm_scheduler.py +4 -1
  93. localstack/services/cloudwatch/provider.py +18 -13
  94. localstack/services/cloudwatch/provider_v2.py +25 -28
  95. localstack/services/dynamodb/packages.py +2 -1
  96. localstack/services/dynamodb/provider.py +42 -0
  97. localstack/services/dynamodb/server.py +2 -2
  98. localstack/services/dynamodb/v2/provider.py +42 -0
  99. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  100. localstack/services/edge.py +1 -1
  101. localstack/services/es/provider.py +2 -2
  102. localstack/services/events/event_rule_engine.py +31 -13
  103. localstack/services/events/models.py +4 -5
  104. localstack/services/events/provider.py +17 -14
  105. localstack/services/events/target.py +17 -9
  106. localstack/services/events/v1/provider.py +5 -5
  107. localstack/services/firehose/provider.py +14 -4
  108. localstack/services/iam/provider.py +11 -116
  109. localstack/services/iam/resources/policy_simulator.py +133 -0
  110. localstack/services/kinesis/models.py +15 -2
  111. localstack/services/kinesis/provider.py +86 -3
  112. localstack/services/kms/provider.py +14 -5
  113. localstack/services/lambda_/api_utils.py +6 -3
  114. localstack/services/lambda_/invocation/docker_runtime_executor.py +1 -1
  115. localstack/services/lambda_/invocation/event_manager.py +1 -1
  116. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  117. localstack/services/lambda_/invocation/lambda_models.py +10 -7
  118. localstack/services/lambda_/invocation/lambda_service.py +5 -1
  119. localstack/services/lambda_/packages.py +1 -1
  120. localstack/services/lambda_/provider.py +4 -3
  121. localstack/services/lambda_/provider_utils.py +1 -1
  122. localstack/services/logs/provider.py +36 -19
  123. localstack/services/moto.py +2 -1
  124. localstack/services/opensearch/cluster.py +15 -7
  125. localstack/services/opensearch/packages.py +26 -7
  126. localstack/services/opensearch/provider.py +8 -2
  127. localstack/services/opensearch/versions.py +56 -7
  128. localstack/services/plugins.py +11 -7
  129. localstack/services/providers.py +10 -2
  130. localstack/services/redshift/provider.py +0 -21
  131. localstack/services/s3/constants.py +5 -2
  132. localstack/services/s3/cors.py +4 -4
  133. localstack/services/s3/models.py +1 -1
  134. localstack/services/s3/notifications.py +55 -39
  135. localstack/services/s3/presigned_url.py +35 -54
  136. localstack/services/s3/provider.py +73 -15
  137. localstack/services/s3/utils.py +42 -22
  138. localstack/services/s3/validation.py +46 -32
  139. localstack/services/s3/website_hosting.py +4 -2
  140. localstack/services/ses/provider.py +18 -8
  141. localstack/services/sns/constants.py +7 -1
  142. localstack/services/sns/executor.py +9 -2
  143. localstack/services/sns/provider.py +8 -5
  144. localstack/services/sns/publisher.py +31 -16
  145. localstack/services/sns/v2/models.py +167 -0
  146. localstack/services/sns/v2/provider.py +867 -0
  147. localstack/services/sns/v2/utils.py +130 -0
  148. localstack/services/sqs/constants.py +1 -1
  149. localstack/services/sqs/developer_api.py +205 -0
  150. localstack/services/sqs/models.py +48 -5
  151. localstack/services/sqs/provider.py +38 -311
  152. localstack/services/sqs/query_api.py +6 -2
  153. localstack/services/sqs/utils.py +121 -2
  154. localstack/services/ssm/provider.py +1 -1
  155. localstack/services/stepfunctions/asl/component/intrinsic/member.py +1 -1
  156. localstack/services/stepfunctions/asl/component/state/state_choice/comparison/comparison.py +5 -11
  157. localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py +2 -2
  158. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +2 -2
  159. localstack/services/stepfunctions/asl/component/state/state_execution/state_parallel/state_parallel.py +1 -1
  160. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py +2 -2
  161. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +1 -1
  162. localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py +2 -2
  163. localstack/services/stepfunctions/asl/component/state/state_succeed/state_succeed.py +1 -1
  164. localstack/services/stepfunctions/asl/component/state/state_wait/state_wait.py +1 -1
  165. localstack/services/stepfunctions/asl/eval/environment.py +1 -1
  166. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  167. localstack/services/stepfunctions/backend/execution.py +2 -1
  168. localstack/services/stores.py +1 -1
  169. localstack/services/transcribe/provider.py +6 -1
  170. localstack/state/codecs.py +61 -0
  171. localstack/state/core.py +11 -5
  172. localstack/state/pickle.py +10 -49
  173. localstack/testing/aws/cloudformation_utils.py +1 -1
  174. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  175. localstack/testing/pytest/cloudformation/transformers.py +0 -0
  176. localstack/testing/pytest/container.py +4 -5
  177. localstack/testing/pytest/fixtures.py +33 -31
  178. localstack/testing/pytest/in_memory_localstack.py +0 -4
  179. localstack/testing/pytest/marking.py +38 -11
  180. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  181. localstack/testing/pytest/util.py +1 -1
  182. localstack/testing/pytest/validation_tracking.py +1 -2
  183. localstack/testing/snapshots/transformer_utility.py +6 -1
  184. localstack/utils/analytics/events.py +2 -2
  185. localstack/utils/analytics/metadata.py +6 -4
  186. localstack/utils/analytics/metrics/counter.py +8 -15
  187. localstack/utils/analytics/publisher.py +1 -2
  188. localstack/utils/analytics/service_providers.py +19 -0
  189. localstack/utils/analytics/service_request_aggregator.py +2 -2
  190. localstack/utils/archives.py +11 -11
  191. localstack/utils/asyncio.py +2 -2
  192. localstack/utils/aws/arns.py +24 -29
  193. localstack/utils/aws/aws_responses.py +8 -8
  194. localstack/utils/aws/aws_stack.py +2 -3
  195. localstack/utils/aws/dead_letter_queue.py +1 -5
  196. localstack/utils/aws/message_forwarding.py +1 -2
  197. localstack/utils/aws/request_context.py +4 -5
  198. localstack/utils/aws/resources.py +1 -1
  199. localstack/utils/aws/templating.py +1 -1
  200. localstack/utils/batch_policy.py +3 -3
  201. localstack/utils/bootstrap.py +21 -13
  202. localstack/utils/catalog/catalog.py +139 -0
  203. localstack/utils/catalog/catalog_loader.py +119 -0
  204. localstack/utils/catalog/common.py +58 -0
  205. localstack/utils/catalog/plugins.py +28 -0
  206. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  207. localstack/utils/collections.py +7 -8
  208. localstack/utils/config_listener.py +1 -1
  209. localstack/utils/container_networking.py +2 -3
  210. localstack/utils/container_utils/container_client.py +135 -136
  211. localstack/utils/container_utils/docker_cmd_client.py +85 -69
  212. localstack/utils/container_utils/docker_sdk_client.py +69 -66
  213. localstack/utils/crypto.py +10 -10
  214. localstack/utils/diagnose.py +3 -4
  215. localstack/utils/docker_utils.py +9 -5
  216. localstack/utils/files.py +33 -13
  217. localstack/utils/functions.py +4 -3
  218. localstack/utils/http.py +11 -11
  219. localstack/utils/json.py +20 -6
  220. localstack/utils/kinesis/kinesis_connector.py +2 -1
  221. localstack/utils/net.py +15 -9
  222. localstack/utils/no_exit_argument_parser.py +2 -2
  223. localstack/utils/numbers.py +9 -2
  224. localstack/utils/objects.py +7 -6
  225. localstack/utils/patch.py +10 -3
  226. localstack/utils/run.py +12 -11
  227. localstack/utils/scheduler.py +11 -11
  228. localstack/utils/server/tcp_proxy.py +2 -2
  229. localstack/utils/serving.py +3 -4
  230. localstack/utils/strings.py +15 -16
  231. localstack/utils/sync.py +126 -1
  232. localstack/utils/tagging.py +8 -6
  233. localstack/utils/testutil.py +8 -8
  234. localstack/utils/threads.py +2 -2
  235. localstack/utils/time.py +12 -4
  236. localstack/utils/urls.py +1 -3
  237. localstack/utils/xray/traceid.py +1 -1
  238. localstack/version.py +16 -3
  239. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/METADATA +18 -14
  240. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/RECORD +248 -239
  241. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/entry_points.txt +8 -4
  242. localstack_core-4.10.1.dev12.dist-info/plux.json +1 -0
  243. localstack/packages/terraform.py +0 -46
  244. localstack/services/cloudformation/deploy.html +0 -144
  245. localstack/services/cloudformation/deploy_ui.py +0 -47
  246. localstack/services/cloudformation/plugins.py +0 -12
  247. localstack_core-4.7.1.dev49.dist-info/plux.json +0 -1
  248. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack +0 -0
  249. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack-supervisor +0 -0
  250. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack.bat +0 -0
  251. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/WHEEL +0 -0
  252. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/licenses/LICENSE.txt +0 -0
  253. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.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:
@@ -286,7 +287,7 @@ def ping(host):
286
287
  """Returns True if the host responds to a ping request"""
287
288
  is_in_windows = is_windows()
288
289
  ping_opts = "-n 1 -w 2000" if is_in_windows else "-c 1 -W 2"
289
- args = "ping %s %s" % (ping_opts, host)
290
+ args = f"ping {ping_opts} {host}"
290
291
  return (
291
292
  subprocess.call(
292
293
  args, shell=not is_in_windows, stdout=subprocess.PIPE, stderr=subprocess.PIPE
@@ -435,8 +436,8 @@ TMP_FOLDER = os.path.join(tempfile.gettempdir(), "localstack")
435
436
  VOLUME_DIR = os.environ.get("LOCALSTACK_VOLUME_DIR", "").strip() or TMP_FOLDER
436
437
 
437
438
  # fix for Mac OS, to be able to mount /var/folders in Docker
438
- if TMP_FOLDER.startswith("/var/folders/") and os.path.exists("/private%s" % TMP_FOLDER):
439
- TMP_FOLDER = "/private%s" % TMP_FOLDER
439
+ if TMP_FOLDER.startswith("/var/folders/") and os.path.exists(f"/private{TMP_FOLDER}"):
440
+ TMP_FOLDER = f"/private{TMP_FOLDER}"
440
441
 
441
442
  # whether to enable verbose debug logging ("LOG" is used when using the CLI with LOCALSTACK_LOG instead of LS_LOG)
442
443
  LS_LOG = eval_log_type("LS_LOG") or eval_log_type("LOG")
@@ -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
 
@@ -1184,6 +1185,8 @@ elif _override_dynamodb_v2 == "v2":
1184
1185
  os.environ["PROVIDER_OVERRIDE_DYNAMODBSTREAMS"] = "v2"
1185
1186
  DDB_STREAMS_PROVIDER_V2 = True
1186
1187
 
1188
+ SNS_PROVIDER_V2 = os.environ.get("PROVIDER_OVERRIDE_SNS", "") == "v2"
1189
+
1187
1190
  # TODO remove fallback to LAMBDA_DOCKER_NETWORK with next minor version
1188
1191
  MAIN_DOCKER_NETWORK = os.environ.get("MAIN_DOCKER_NETWORK", "") or LAMBDA_DOCKER_NETWORK
1189
1192
 
@@ -1237,8 +1240,8 @@ def use_custom_dns():
1237
1240
 
1238
1241
 
1239
1242
  # s3 virtual host name
1240
- S3_VIRTUAL_HOSTNAME = "s3.%s" % LOCALSTACK_HOST.host
1241
- S3_STATIC_WEBSITE_HOSTNAME = "s3-website.%s" % LOCALSTACK_HOST.host
1243
+ S3_VIRTUAL_HOSTNAME = f"s3.{LOCALSTACK_HOST.host}"
1244
+ S3_STATIC_WEBSITE_HOSTNAME = f"s3-website.{LOCALSTACK_HOST.host}"
1242
1245
 
1243
1246
  BOTO_WAITER_DELAY = int(os.environ.get("BOTO_WAITER_DELAY") or "1")
1244
1247
  BOTO_WAITER_MAX_ATTEMPTS = int(os.environ.get("BOTO_WAITER_MAX_ATTEMPTS") or "120")
@@ -1257,7 +1260,6 @@ IN_MEMORY_CLIENT = is_env_true("IN_MEMORY_CLIENT")
1257
1260
  LOCALSTACK_RESPONSE_HEADER_ENABLED = is_env_not_false("LOCALSTACK_RESPONSE_HEADER_ENABLED")
1258
1261
 
1259
1262
  # Serialization backend for the LocalStack internal state (`dill` is used by default`).
1260
- # `jsonpickle` enables the new experimental backend.
1261
1263
  STATE_SERIALIZATION_BACKEND = os.environ.get("STATE_SERIALIZATION_BACKEND", "").strip() or "dill"
1262
1264
 
1263
1265
  # List of environment variable names used for configuration that are passed from the host into the LocalStack container.
@@ -1450,6 +1452,11 @@ def is_collect_metrics_mode() -> bool:
1450
1452
  return is_env_true(ENV_INTERNAL_TEST_COLLECT_METRIC)
1451
1453
 
1452
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
+
1453
1460
  def collect_config_items() -> list[tuple[str, Any]]:
1454
1461
  """Returns a list of key-value tuples of LocalStack configuration values."""
1455
1462
  none = object() # sentinel object
@@ -1502,10 +1509,10 @@ def get_protocol() -> str:
1502
1509
 
1503
1510
 
1504
1511
  def external_service_url(
1505
- host: Optional[str] = None,
1506
- port: Optional[int] = None,
1507
- protocol: Optional[str] = None,
1508
- subdomains: Optional[str] = None,
1512
+ host: str | None = None,
1513
+ port: int | None = None,
1514
+ protocol: str | None = None,
1515
+ subdomains: str | None = None,
1509
1516
  ) -> str:
1510
1517
  """Returns a service URL (e.g., SQS queue URL) to an external client (e.g., boto3) potentially running on another
1511
1518
  machine than LocalStack. The configurations LOCALSTACK_HOST and USE_SSL can customize these returned URLs.
@@ -1522,10 +1529,10 @@ def external_service_url(
1522
1529
 
1523
1530
 
1524
1531
  def internal_service_url(
1525
- host: Optional[str] = None,
1526
- port: Optional[int] = None,
1527
- protocol: Optional[str] = None,
1528
- subdomains: Optional[str] = None,
1532
+ host: str | None = None,
1533
+ port: int | None = None,
1534
+ protocol: str | None = None,
1535
+ subdomains: str | None = None,
1529
1536
  ) -> str:
1530
1537
  """Returns a service URL for internal use within LocalStack (i.e., same host).
1531
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
 
@@ -5,6 +5,12 @@ from typing import Literal
5
5
  import click
6
6
  import yaml
7
7
 
8
+ EDGE_SERVICE_NODE_PORT = 30066
9
+ NODE_PORT_START = 30010
10
+ SERVICE_PORT_START = 4510
11
+ NUMBER_OF_SERVICE_PORTS = 50
12
+ EDGE_SERVICE_DNS_PORT = 31053
13
+
8
14
 
9
15
  @dataclasses.dataclass
10
16
  class MountPoint:
@@ -27,7 +33,7 @@ def generate_mount_points(
27
33
 
28
34
  # container paths
29
35
  target_path = "/opt/code/localstack/"
30
- 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")
31
37
 
32
38
  # Community code
33
39
  if pro:
@@ -139,7 +145,12 @@ def generate_mount_points(
139
145
  return mount_points
140
146
 
141
147
 
142
- 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
+ ):
143
154
  volumes = [
144
155
  {
145
156
  "volume": f"{mount_point.host_path}:{mount_point.node_path}",
@@ -148,9 +159,64 @@ def generate_k8s_cluster_config(mount_points: list[MountPoint], port: int = 4566
148
159
  for mount_point in mount_points
149
160
  ]
150
161
 
151
- ports = [{"port": f"{port}:31566", "nodeFilters": ["server:0"]}]
162
+ ports = [
163
+ # main gateway port
164
+ {
165
+ "nodeFilters": [
166
+ "server:0",
167
+ ],
168
+ "port": f"{port}:{EDGE_SERVICE_NODE_PORT}",
169
+ },
170
+ # 443 https port for main gateway
171
+ {
172
+ "nodeFilters": [
173
+ "server:0",
174
+ ],
175
+ "port": f"443:{EDGE_SERVICE_NODE_PORT}",
176
+ },
177
+ # Node ports
178
+ {
179
+ "nodeFilters": [
180
+ "server:0",
181
+ ],
182
+ "port": f"{SERVICE_PORT_START}-{SERVICE_PORT_START + NUMBER_OF_SERVICE_PORTS - 1}:{NODE_PORT_START}-{NODE_PORT_START + NUMBER_OF_SERVICE_PORTS - 1}",
183
+ },
184
+ ]
152
185
 
153
- config = {"apiVersion": "k3d.io/v1alpha5", "kind": "Simple", "volumes": volumes, "ports": ports}
186
+ if expose_dns:
187
+ ports.append(
188
+ {
189
+ "nodeFilters": [
190
+ "server:0",
191
+ ],
192
+ "port": f"{dns_port}:{EDGE_SERVICE_DNS_PORT}/udp",
193
+ }
194
+ )
195
+ ports.append(
196
+ {
197
+ "nodeFilters": [
198
+ "server:0",
199
+ ],
200
+ "port": f"{dns_port}:{EDGE_SERVICE_DNS_PORT}/tcp",
201
+ }
202
+ )
203
+
204
+ config = {
205
+ "apiVersion": "k3d.io/v1alpha5",
206
+ "kind": "Simple",
207
+ "volumes": volumes,
208
+ "ports": ports,
209
+ "options": {
210
+ "k3s": {
211
+ "extraArgs": [
212
+ {
213
+ "arg": "--kubelet-arg=container-log-max-size=1Gi",
214
+ "nodeFilters": ["server:*"],
215
+ },
216
+ ],
217
+ },
218
+ },
219
+ }
154
220
 
155
221
  return config
156
222
 
@@ -159,7 +225,7 @@ def snake_to_kebab_case(string: str):
159
225
  return string.lower().replace("_", "-")
160
226
 
161
227
 
162
- def generate_k8s_cluster_overrides(
228
+ def generate_k8s_helm_overrides(
163
229
  mount_points: list[MountPoint], pro: bool = False, env: list[str] | None = None
164
230
  ):
165
231
  volumes = [
@@ -200,10 +266,48 @@ def generate_k8s_cluster_overrides(
200
266
  "name": "CONTAINER_RUNTIME",
201
267
  "value": "kubernetes",
202
268
  },
269
+ {
270
+ "name": "RDS_MYSQL_DOCKER",
271
+ "value": "1",
272
+ },
273
+ {
274
+ "name": "ENABLE_DMS",
275
+ "value": "1",
276
+ },
277
+ {
278
+ "name": "ENABLE_BEDROCK",
279
+ "value": "1",
280
+ },
281
+ {
282
+ "name": "DOCDB_PROXY_CONTAINER",
283
+ "value": "1",
284
+ },
285
+ {
286
+ "name": "GLUE_JOB_EXECUTOR_PROVIDER",
287
+ "value": "v2",
288
+ },
289
+ {
290
+ "name": "CLOUDFRONT_LAMBDA_EDGE",
291
+ "value": "1",
292
+ },
203
293
  ]
204
294
 
205
295
  image_repository = "localstack/localstack-pro" if pro else "localstack/localstack"
206
296
 
297
+ service = {
298
+ "edgeService": {
299
+ "nodePort": EDGE_SERVICE_NODE_PORT,
300
+ },
301
+ "externalServicePorts": {
302
+ "start": SERVICE_PORT_START,
303
+ "end": SERVICE_PORT_START + NUMBER_OF_SERVICE_PORTS,
304
+ "nodePortStart": NODE_PORT_START,
305
+ },
306
+ "dnsService": {
307
+ "enabled": True,
308
+ "nodePort": EDGE_SERVICE_DNS_PORT,
309
+ },
310
+ }
207
311
  overrides = {
208
312
  "debug": True,
209
313
  "volumes": volumes,
@@ -211,6 +315,9 @@ def generate_k8s_cluster_overrides(
211
315
  "extraEnvVars": extra_env_vars,
212
316
  "image": {"repository": image_repository},
213
317
  "lambda": {"executor": "kubernetes"},
318
+ "service": service,
319
+ "readinessProbe": {"initialDelaySeconds": 10},
320
+ "livenessProbe": {"initialDelaySeconds": 10},
214
321
  }
215
322
 
216
323
  return overrides
@@ -275,6 +382,18 @@ def print_file(content: dict, file_name: str):
275
382
  help="Port to expose from the kubernetes node",
276
383
  type=click.IntRange(0, 65535),
277
384
  )
385
+ @click.option(
386
+ "--expose-dns",
387
+ is_flag=True,
388
+ default=False,
389
+ help="Expose DNS port from the kubernetes node.",
390
+ )
391
+ @click.option(
392
+ "--dns-port",
393
+ default=53,
394
+ help="DNS port to expose from the kubernetes node. It is applied only if --expose-dns is set.",
395
+ type=click.IntRange(0, 65535),
396
+ )
278
397
  @click.argument("command", nargs=-1, required=False)
279
398
  def run(
280
399
  pro: bool = None,
@@ -287,15 +406,19 @@ def run(
287
406
  command: str = None,
288
407
  env: list[str] = None,
289
408
  port: int = None,
409
+ expose_dns: bool = False,
410
+ dns_port: int = 53,
290
411
  ):
291
412
  """
292
413
  A tool for localstack developers to generate the kubernetes cluster configuration file and the overrides to mount the localstack code into the cluster.
293
414
  """
294
415
  mount_points = generate_mount_points(pro, mount_moto, mount_entrypoints)
295
416
 
296
- config = generate_k8s_cluster_config(mount_points, port=port)
417
+ config = generate_k8s_cluster_config(
418
+ mount_points, port=port, expose_dns=expose_dns, dns_port=dns_port
419
+ )
297
420
 
298
- overrides = generate_k8s_cluster_overrides(mount_points, pro=pro, env=env)
421
+ overrides = generate_k8s_helm_overrides(mount_points, pro=pro, env=env)
299
422
 
300
423
  output_dir = output_dir or os.getcwd()
301
424
  overrides_file = overrides_file or "overrides.yml"
@@ -363,10 +363,7 @@ def _list_files_in_container_image(container_client: ContainerClient, image_name
363
363
  try:
364
364
  # docker export yields paths without prefixed slashes, so we add them here
365
365
  # since the file is pretty big (~4MB for community, ~7MB for pro) we gzip it
366
- cmd = "docker export %s | tar -t | awk '{ print \"/\" $0 }' | gzip > %s" % (
367
- container_id,
368
- cache_file,
369
- )
366
+ cmd = f"docker export {container_id} | tar -t | awk '{{ print \"/\" $0 }}' | gzip > {cache_file}"
370
367
  run(cmd, shell=True)
371
368
  finally:
372
369
  container_client.remove_container(container_id)
@@ -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
@@ -254,7 +254,7 @@ class NonLoggingHandler(DNSHandler):
254
254
  THREAD_LOCAL.client_address = self.client_address
255
255
  THREAD_LOCAL.server = self.server
256
256
  THREAD_LOCAL.request = self.request
257
- return super(NonLoggingHandler, self).handle(*args, **kwargs)
257
+ return super().handle(*args, **kwargs)
258
258
  except Exception:
259
259
  pass
260
260
 
@@ -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
@@ -32,7 +32,7 @@ class DefaultFormatter(logging.Formatter):
32
32
  """
33
33
 
34
34
  def __init__(self, fmt=LOG_FORMAT, datefmt=LOG_DATE_FORMAT):
35
- super(DefaultFormatter, self).__init__(fmt=fmt, datefmt=datefmt)
35
+ super().__init__(fmt=fmt, datefmt=datefmt)
36
36
 
37
37
 
38
38
  class AddFormattedAttributes(logging.Filter):
@@ -48,7 +48,7 @@ class AddFormattedAttributes(logging.Filter):
48
48
  max_thread_len: int
49
49
 
50
50
  def __init__(self, max_name_len: int = None, max_thread_len: int = None):
51
- super(AddFormattedAttributes, self).__init__()
51
+ super().__init__()
52
52
  self.max_name_len = max_name_len if max_name_len else MAX_NAME_LEN
53
53
  self.max_thread_len = max_thread_len if max_thread_len else MAX_THREAD_NAME_LEN
54
54
 
@@ -75,7 +75,7 @@ class MaskSensitiveInputFilter(logging.Filter):
75
75
  patterns: list[tuple[re.Pattern[bytes], bytes]]
76
76
 
77
77
  def __init__(self, sensitive_keys: list[str]):
78
- super(MaskSensitiveInputFilter, self).__init__()
78
+ super().__init__()
79
79
 
80
80
  self.patterns = [
81
81
  (re.compile(to_bytes(rf'"{key}":\s*"[^"]+"')), to_bytes(f'"{key}": "******"'))
@@ -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")
@@ -11,6 +11,7 @@ TRACKED_ENV_VAR = [
11
11
  "ACTIVATE_PRO",
12
12
  "ALLOW_NONSTANDARD_REGIONS",
13
13
  "BEDROCK_PREWARM",
14
+ "CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES",
14
15
  "CLOUDFRONT_LAMBDA_EDGE",
15
16
  "CONTAINER_RUNTIME",
16
17
  "DEBUG",
@@ -53,6 +54,7 @@ TRACKED_ENV_VAR = [
53
54
  "LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT",
54
55
  "LEGACY_EDGE_PROXY", # Not functional; deprecated in 1.0.0, removed in 2.0.0
55
56
  "LS_LOG",
57
+ "LOCALSTACK_K8S_DEPLOYMENT_METHOD",
56
58
  "MOCK_UNIMPLEMENTED", # Not functional; deprecated in 1.3.0, removed in 3.0.0
57
59
  "OPENSEARCH_ENDPOINT_STRATEGY",
58
60
  "PERSISTENCE",
@@ -78,6 +80,7 @@ PRESENCE_ENV_VAR = [
78
80
  "HOSTNAME_FROM_LAMBDA",
79
81
  "HOST_TMP_FOLDER", # Not functional; deprecated in 1.0.0, removed in 2.0.0
80
82
  "INIT_SCRIPTS_PATH", # Not functional; deprecated in 1.1.0, removed in 2.0.0
83
+ "KUBERNETES_SERVICE_HOST",
81
84
  "LAMBDA_DEBUG_MODE_CONFIG_PATH",
82
85
  "LEGACY_DIRECTORIES", # Not functional; deprecated in 1.1.0, removed in 2.0.0
83
86
  "LEGACY_INIT_DIR", # Not functional; deprecated in 1.1.0, removed in 2.0.0
@@ -65,7 +65,7 @@ class HookManager(PluginManager):
65
65
  fn_plugin(*args, **kwargs)
66
66
 
67
67
  def __str__(self):
68
- return "HookManager(%s)" % self.namespace
68
+ return f"HookManager({self.namespace})"
69
69
 
70
70
  def __repr__(self):
71
71
  return self.__str__()
@@ -89,9 +89,9 @@ 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
- raise OSError("Script %s returned a non-zero exit code %s" % (path, exit_code))
94
+ raise OSError(f"Script {path} returned a non-zero exit code {exit_code}")
95
95
 
96
96
 
97
97
  class PythonScriptRunner(ScriptRunner):
@@ -20,13 +20,13 @@ def print_runtime_information(in_docker: bool = False):
20
20
  if in_docker:
21
21
  try:
22
22
  container_name = get_main_container_name()
23
- print("LocalStack Docker container name: %s" % container_name)
23
+ print(f"LocalStack Docker container name: {container_name}")
24
24
  inspect_result = DOCKER_CLIENT.inspect_container(container_name)
25
25
  container_id = inspect_result["Id"]
26
- print("LocalStack Docker container id: %s" % container_id[:12])
26
+ print(f"LocalStack Docker container id: {container_id[:12]}")
27
27
  image_details = DOCKER_CLIENT.inspect_image(inspect_result["Image"])
28
28
  digests = image_details.get("RepoDigests") or ["Unavailable"]
29
- print("LocalStack Docker image sha: %s" % digests[0])
29
+ print(f"LocalStack Docker image sha: {digests[0]}")
30
30
  except ContainerException:
31
31
  print(
32
32
  "LocalStack Docker container info: Failed to inspect the LocalStack docker container. "
@@ -44,10 +44,10 @@ def print_runtime_information(in_docker: bool = False):
44
44
  )
45
45
 
46
46
  if config.LOCALSTACK_BUILD_DATE:
47
- print("LocalStack build date: %s" % config.LOCALSTACK_BUILD_DATE)
47
+ print(f"LocalStack build date: {config.LOCALSTACK_BUILD_DATE}")
48
48
 
49
49
  if config.LOCALSTACK_BUILD_GIT_HASH:
50
- print("LocalStack build git hash: %s" % config.LOCALSTACK_BUILD_GIT_HASH)
50
+ print(f"LocalStack build git hash: {config.LOCALSTACK_BUILD_GIT_HASH}")
51
51
 
52
52
  print()
53
53