localstack-core 4.10.1.dev42__py3-none-any.whl → 4.12.1.dev18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of localstack-core might be problematic. Click here for more details.

Files changed (158) hide show
  1. localstack/aws/api/apigateway/__init__.py +42 -0
  2. localstack/aws/api/cloudformation/__init__.py +161 -0
  3. localstack/aws/api/ec2/__init__.py +1178 -12
  4. localstack/aws/api/iam/__init__.py +228 -0
  5. localstack/aws/api/kms/__init__.py +1 -0
  6. localstack/aws/api/lambda_/__init__.py +1034 -66
  7. localstack/aws/api/logs/__init__.py +500 -0
  8. localstack/aws/api/opensearch/__init__.py +100 -0
  9. localstack/aws/api/redshift/__init__.py +69 -0
  10. localstack/aws/api/resourcegroupstaggingapi/__init__.py +36 -0
  11. localstack/aws/api/route53/__init__.py +45 -0
  12. localstack/aws/api/route53resolver/__init__.py +1 -0
  13. localstack/aws/api/s3/__init__.py +64 -0
  14. localstack/aws/api/s3control/__init__.py +19 -0
  15. localstack/aws/api/secretsmanager/__init__.py +37 -23
  16. localstack/aws/api/stepfunctions/__init__.py +52 -10
  17. localstack/aws/api/sts/__init__.py +52 -0
  18. localstack/aws/connect.py +35 -15
  19. localstack/aws/handlers/logging.py +8 -4
  20. localstack/aws/handlers/service.py +11 -2
  21. localstack/aws/protocol/serializer.py +1 -1
  22. localstack/config.py +8 -0
  23. localstack/constants.py +3 -0
  24. localstack/deprecations.py +0 -6
  25. localstack/dev/kubernetes/__main__.py +39 -14
  26. localstack/runtime/analytics.py +11 -0
  27. localstack/services/acm/provider.py +17 -1
  28. localstack/services/apigateway/legacy/provider.py +28 -15
  29. localstack/services/cloudformation/engine/template_preparer.py +6 -2
  30. localstack/services/cloudformation/engine/v2/change_set_model.py +9 -0
  31. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +15 -1
  32. localstack/services/cloudformation/engine/v2/change_set_resource_support_checker.py +114 -0
  33. localstack/services/cloudformation/provider.py +26 -1
  34. localstack/services/cloudformation/provider_utils.py +20 -0
  35. localstack/services/cloudformation/resource_provider.py +5 -4
  36. localstack/services/cloudformation/scaffolding/__main__.py +94 -22
  37. localstack/services/cloudformation/v2/provider.py +41 -0
  38. localstack/services/cloudwatch/provider.py +10 -3
  39. localstack/services/cloudwatch/provider_v2.py +6 -3
  40. localstack/services/configservice/provider.py +5 -1
  41. localstack/services/dynamodb/provider.py +1 -0
  42. localstack/services/dynamodb/v2/provider.py +1 -0
  43. localstack/services/dynamodbstreams/provider.py +6 -0
  44. localstack/services/dynamodbstreams/v2/provider.py +6 -0
  45. localstack/services/ec2/provider.py +6 -0
  46. localstack/services/es/provider.py +6 -0
  47. localstack/services/events/provider.py +4 -0
  48. localstack/services/events/v1/provider.py +9 -0
  49. localstack/services/firehose/provider.py +5 -0
  50. localstack/services/iam/provider.py +4 -0
  51. localstack/services/kinesis/packages.py +1 -1
  52. localstack/services/kms/models.py +16 -22
  53. localstack/services/kms/provider.py +4 -0
  54. localstack/services/lambda_/analytics.py +11 -2
  55. localstack/services/lambda_/api_utils.py +37 -20
  56. localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +1 -1
  57. localstack/services/lambda_/invocation/assignment.py +4 -1
  58. localstack/services/lambda_/invocation/event_manager.py +15 -11
  59. localstack/services/lambda_/invocation/execution_environment.py +21 -2
  60. localstack/services/lambda_/invocation/lambda_models.py +31 -2
  61. localstack/services/lambda_/invocation/lambda_service.py +62 -3
  62. localstack/services/lambda_/invocation/models.py +9 -1
  63. localstack/services/lambda_/invocation/version_manager.py +18 -3
  64. localstack/services/lambda_/provider.py +307 -106
  65. localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
  66. localstack/services/lambda_/runtimes.py +3 -1
  67. localstack/services/logs/provider.py +9 -0
  68. localstack/services/opensearch/packages.py +34 -20
  69. localstack/services/opensearch/provider.py +53 -3
  70. localstack/services/resource_groups/provider.py +5 -1
  71. localstack/services/resourcegroupstaggingapi/provider.py +6 -1
  72. localstack/services/route53/provider.py +7 -0
  73. localstack/services/route53resolver/provider.py +5 -0
  74. localstack/services/s3/constants.py +5 -0
  75. localstack/services/s3/exceptions.py +9 -0
  76. localstack/services/s3/models.py +9 -1
  77. localstack/services/s3/provider.py +51 -43
  78. localstack/services/s3/utils.py +81 -15
  79. localstack/services/s3control/provider.py +107 -2
  80. localstack/services/s3control/validation.py +50 -0
  81. localstack/services/scheduler/provider.py +4 -2
  82. localstack/services/secretsmanager/provider.py +4 -0
  83. localstack/services/ses/provider.py +4 -0
  84. localstack/services/sns/constants.py +16 -1
  85. localstack/services/sns/provider.py +5 -0
  86. localstack/services/sns/publisher.py +15 -6
  87. localstack/services/sns/v2/models.py +9 -0
  88. localstack/services/sns/v2/provider.py +750 -19
  89. localstack/services/sns/v2/utils.py +12 -0
  90. localstack/services/sqs/constants.py +6 -0
  91. localstack/services/sqs/provider.py +9 -1
  92. localstack/services/sqs/resource_providers/aws_sqs_queue.py +61 -46
  93. localstack/services/ssm/provider.py +6 -0
  94. localstack/services/stepfunctions/asl/component/common/path/result_path.py +1 -1
  95. localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +0 -1
  96. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +0 -1
  97. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +8 -8
  98. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/{mock_eval_utils.py → local_mock_eval_utils.py} +13 -9
  99. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +6 -6
  100. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +1 -1
  101. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +4 -0
  102. localstack/services/stepfunctions/asl/component/test_state/state/base_mock.py +118 -0
  103. localstack/services/stepfunctions/asl/component/test_state/state/common.py +82 -0
  104. localstack/services/stepfunctions/asl/component/test_state/state/execution.py +139 -0
  105. localstack/services/stepfunctions/asl/component/test_state/state/map.py +77 -0
  106. localstack/services/stepfunctions/asl/component/test_state/state/task.py +44 -0
  107. localstack/services/stepfunctions/asl/eval/environment.py +30 -22
  108. localstack/services/stepfunctions/asl/eval/states.py +1 -1
  109. localstack/services/stepfunctions/asl/eval/test_state/environment.py +49 -9
  110. localstack/services/stepfunctions/asl/eval/test_state/program_state.py +22 -0
  111. localstack/services/stepfunctions/asl/jsonata/jsonata.py +5 -1
  112. localstack/services/stepfunctions/asl/parse/preprocessor.py +67 -24
  113. localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py +5 -4
  114. localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py +222 -31
  115. localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +256 -22
  116. localstack/services/stepfunctions/backend/execution.py +10 -11
  117. localstack/services/stepfunctions/backend/execution_worker.py +5 -5
  118. localstack/services/stepfunctions/backend/test_state/execution.py +36 -0
  119. localstack/services/stepfunctions/backend/test_state/execution_worker.py +33 -1
  120. localstack/services/stepfunctions/backend/test_state/test_state_mock.py +127 -0
  121. localstack/services/stepfunctions/local_mocking/__init__.py +9 -0
  122. localstack/services/stepfunctions/{mocking → local_mocking}/mock_config.py +24 -17
  123. localstack/services/stepfunctions/provider.py +83 -25
  124. localstack/services/stepfunctions/test_state/mock_config.py +47 -0
  125. localstack/services/sts/provider.py +7 -0
  126. localstack/services/support/provider.py +5 -1
  127. localstack/services/swf/provider.py +5 -1
  128. localstack/services/transcribe/provider.py +7 -0
  129. localstack/testing/aws/lambda_utils.py +1 -1
  130. localstack/testing/aws/util.py +2 -1
  131. localstack/testing/config.py +1 -0
  132. localstack/testing/pytest/fixtures.py +28 -0
  133. localstack/testing/snapshots/transformer_utility.py +5 -0
  134. localstack/utils/analytics/publisher.py +37 -155
  135. localstack/utils/analytics/service_request_aggregator.py +6 -4
  136. localstack/utils/aws/arns.py +7 -0
  137. localstack/utils/aws/client_types.py +2 -4
  138. localstack/utils/batching.py +258 -0
  139. localstack/utils/bootstrap.py +2 -2
  140. localstack/utils/catalog/catalog.py +3 -2
  141. localstack/utils/collections.py +23 -11
  142. localstack/utils/container_utils/container_client.py +22 -13
  143. localstack/utils/container_utils/docker_cmd_client.py +6 -6
  144. localstack/version.py +2 -2
  145. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/METADATA +7 -7
  146. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/RECORD +155 -146
  147. localstack_core-4.12.1.dev18.dist-info/plux.json +1 -0
  148. localstack/services/stepfunctions/mocking/__init__.py +0 -0
  149. localstack/utils/batch_policy.py +0 -124
  150. localstack_core-4.10.1.dev42.dist-info/plux.json +0 -1
  151. /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
  152. {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack +0 -0
  153. {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack-supervisor +0 -0
  154. {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack.bat +0 -0
  155. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/WHEEL +0 -0
  156. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/entry_points.txt +0 -0
  157. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/licenses/LICENSE.txt +0 -0
  158. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/top_level.txt +0 -0
@@ -19,11 +19,24 @@ from localstack.utils.strings import is_base64, to_bytes
19
19
  from localstack.utils.testutil import create_zip_file
20
20
 
21
21
 
22
+ class LambdaManagedInstancesCapacityProviderConfig(TypedDict):
23
+ CapacityProviderArn: str | None
24
+ PerExecutionEnvironmentMaxConcurrency: int | None
25
+ ExecutionEnvironmentMemoryGiBPerVCpu: float | None
26
+
27
+
28
+ class CapacityProviderConfig(TypedDict):
29
+ LambdaManagedInstancesCapacityProviderConfig: (
30
+ LambdaManagedInstancesCapacityProviderConfig | None
31
+ )
32
+
33
+
22
34
  class LambdaFunctionProperties(TypedDict):
23
35
  Code: Code | None
24
36
  Role: str | None
25
37
  Architectures: list[str] | None
26
38
  Arn: str | None
39
+ CapacityProviderConfig: CapacityProviderConfig | None
27
40
  CodeSigningConfigArn: str | None
28
41
  DeadLetterConfig: DeadLetterConfig | None
29
42
  Description: str | None
@@ -297,6 +310,7 @@ def _transform_function_to_model(function):
297
310
  "Arn",
298
311
  "EphemeralStorage",
299
312
  "Architectures",
313
+ "CapacityProviderConfig",
300
314
  ]
301
315
  response_model = util.select_attributes(function, model_properties)
302
316
  response_model["Arn"] = function["FunctionArn"]
@@ -387,6 +401,7 @@ class LambdaFunctionProvider(ResourceProvider[LambdaFunctionProperties]):
387
401
  "TracingConfig",
388
402
  "VpcConfig",
389
403
  "LoggingConfig",
404
+ "CapacityProviderConfig",
390
405
  ],
391
406
  )
392
407
  if "Timeout" in kwargs:
@@ -408,11 +423,27 @@ class LambdaFunctionProvider(ResourceProvider[LambdaFunctionProperties]):
408
423
  }
409
424
 
410
425
  kwargs["Code"] = _get_lambda_code_param(model)
426
+
427
+ # For managed instance lambdas, we publish them immediately
428
+ if "CapacityProviderConfig" in kwargs:
429
+ kwargs["Publish"] = True
430
+ kwargs["PublishTo"] = "LATEST_PUBLISHED"
431
+
411
432
  create_response = lambda_client.create_function(**kwargs)
433
+ # TODO: if version is in the schema, just put it in the model instead of the custom context
434
+ request.custom_context["Version"] = create_response["Version"] # $LATEST.PUBLISHED
412
435
  model["Arn"] = create_response["FunctionArn"]
413
436
 
414
- get_fn_response = lambda_client.get_function(FunctionName=model["Arn"])
437
+ if request.custom_context.get("Version") == "$LATEST.PUBLISHED":
438
+ # for managed instance lambdas, we need to wait until the version is published & active
439
+ get_fn_response = lambda_client.get_function(
440
+ FunctionName=model["FunctionName"], Qualifier=request.custom_context["Version"]
441
+ )
442
+ else:
443
+ get_fn_response = lambda_client.get_function(FunctionName=model["Arn"])
444
+
415
445
  match get_fn_response["Configuration"]["State"]:
446
+ # TODO: explicitly handle new ActiveNonInvocable state?
416
447
  case "Pending":
417
448
  return ProgressEvent(
418
449
  status=OperationStatus.IN_PROGRESS,
@@ -541,6 +572,7 @@ class LambdaFunctionProvider(ResourceProvider[LambdaFunctionProperties]):
541
572
  "TracingConfig",
542
573
  "VpcConfig",
543
574
  "LoggingConfig",
575
+ "CapacityProviderConfig",
544
576
  ]
545
577
  update_config_props = util.select_attributes(request.desired_state, config_keys)
546
578
  function_name = request.previous_state["FunctionName"]
@@ -34,6 +34,7 @@ from localstack.aws.api.lambda_ import Runtime
34
34
  # => Synchronize the order with the "Supported runtimes" under "AWS Lambda runtimes" (a)
35
35
  # => Add comments for deprecated runtimes using <Deprecation date> => <Block function create> => <Block function update>
36
36
  IMAGE_MAPPING: dict[Runtime, str] = {
37
+ Runtime.nodejs24_x: "nodejs:24",
37
38
  Runtime.nodejs22_x: "nodejs:22",
38
39
  Runtime.nodejs20_x: "nodejs:20",
39
40
  Runtime.nodejs18_x: "nodejs:18",
@@ -112,6 +113,7 @@ ALL_RUNTIMES: list[Runtime] = list(IMAGE_MAPPING.keys())
112
113
  # => Remove deprecated runtimes from this testing list
113
114
  RUNTIMES_AGGREGATED = {
114
115
  "nodejs": [
116
+ Runtime.nodejs24_x,
115
117
  Runtime.nodejs22_x,
116
118
  Runtime.nodejs20_x,
117
119
  Runtime.nodejs18_x,
@@ -166,6 +168,6 @@ SNAP_START_SUPPORTED_RUNTIMES = [
166
168
  ]
167
169
 
168
170
  # An ordered list of all Lambda runtimes considered valid by AWS. Matching snapshots in test_create_lambda_exceptions
169
- VALID_RUNTIMES: str = "[nodejs20.x, python3.14, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, java25, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9]"
171
+ VALID_RUNTIMES: str = "[nodejs20.x, python3.14, provided.al2023, python3.12, python3.13, nodejs24.x, nodejs22.x, java17, nodejs16.x, java25, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9]"
170
172
  # An ordered list of all Lambda runtimes for layers considered valid by AWS. Matching snapshots in test_layer_exceptions
171
173
  VALID_LAYER_RUNTIMES: str = "[ruby3.5, ruby2.6, dotnetcore1.0, python3.7, nodejs8.10, nasa, ruby2.7, python2.7-greengrass, dotnetcore2.0, python3.8, java21, dotnet6, dotnetcore2.1, python3.9, java11, nodejs6.10, provided, dotnetcore3.1, dotnet8, java25, java17, nodejs, nodejs4.3, java8.al2, go1.x, dotnet10, nodejs20.x, go1.9, byol, nodejs10.x, provided.al2023, nodejs22.x, python3.10, java8, nodejs12.x, python3.11, nodejs24.x, nodejs8.x, python3.12, nodejs14.x, nodejs8.9, nodejs26.x, python3.13, python3.14, nodejs16.x, python3.15, provided.al2, nodejs4.3-edge, nodejs18.x, ruby3.2, python3.4, ruby3.3, ruby3.4, ruby2.5, python3.6, python2.7]"
@@ -14,6 +14,7 @@ from moto.logs.models import LogStream as MotoLogStream
14
14
  from localstack.aws.api import CommonServiceException, RequestContext, handler
15
15
  from localstack.aws.api.logs import (
16
16
  AmazonResourceName,
17
+ DeletionProtectionEnabled,
17
18
  DescribeLogGroupsRequest,
18
19
  DescribeLogGroupsResponse,
19
20
  DescribeLogStreamsRequest,
@@ -43,6 +44,7 @@ from localstack.services import moto
43
44
  from localstack.services.logs.models import get_moto_logs_backend, logs_stores
44
45
  from localstack.services.moto import call_moto
45
46
  from localstack.services.plugins import ServiceLifecycleHook
47
+ from localstack.state import StateVisitor
46
48
  from localstack.utils.aws import arns
47
49
  from localstack.utils.aws.client_types import ServicePrincipal
48
50
  from localstack.utils.bootstrap import is_api_enabled
@@ -57,6 +59,12 @@ class LogsProvider(LogsApi, ServiceLifecycleHook):
57
59
  super().__init__()
58
60
  self.cw_client = connect_to().cloudwatch
59
61
 
62
+ def accept_state_visitor(self, visitor: StateVisitor):
63
+ from moto.logs.models import logs_backends
64
+
65
+ visitor.visit(logs_backends)
66
+ visitor.visit(logs_stores)
67
+
60
68
  def put_log_events(
61
69
  self,
62
70
  context: RequestContext,
@@ -163,6 +171,7 @@ class LogsProvider(LogsApi, ServiceLifecycleHook):
163
171
  kms_key_id: KmsKeyId | None = None,
164
172
  tags: Tags | None = None,
165
173
  log_group_class: LogGroupClass | None = None,
174
+ deletion_protection_enabled: DeletionProtectionEnabled | None = None,
166
175
  **kwargs,
167
176
  ) -> None:
168
177
  call_moto(context)
@@ -107,19 +107,31 @@ class OpensearchPackageInstaller(PackageInstaller):
107
107
  # setup security based on the version
108
108
  self._setup_security(install_dir, parsed_version)
109
109
 
110
+ # Determine network configuration to use for plugin downloads
111
+ sys_props = {
112
+ **java_system_properties_proxy(),
113
+ **java_system_properties_ssl(
114
+ os.path.join(install_dir, "jdk", "bin", "keytool"),
115
+ {"JAVA_HOME": os.path.join(install_dir, "jdk")},
116
+ ),
117
+ }
118
+ java_opts = system_properties_to_cli_args(sys_props)
119
+
120
+ keystore_binary = os.path.join(install_dir, "bin", "opensearch-keystore")
121
+ if os.path.exists(keystore_binary):
122
+ # initialize and create the keystore. Concurrent starts of ES will all try to create it at the same
123
+ # time, and fail with a race condition. Creating once when installing solves the issue without
124
+ # the need to lock the starts
125
+ # Ultimately, each cluster should have its own `config` file and maybe not share the same one
126
+ output = run(
127
+ [keystore_binary, "create"],
128
+ env_vars={"OPENSEARCH_JAVA_OPTS": " ".join(java_opts)},
129
+ )
130
+ LOG.debug("Keystore init output: %s", output)
131
+
110
132
  # install other default plugins for opensearch 1.1+
111
133
  # https://forum.opensearch.org/t/ingest-attachment-cannot-be-installed/6494/12
112
134
  if parsed_version >= "1.1.0":
113
- # Determine network configuration to use for plugin downloads
114
- sys_props = {
115
- **java_system_properties_proxy(),
116
- **java_system_properties_ssl(
117
- os.path.join(install_dir, "jdk", "bin", "keytool"),
118
- {"JAVA_HOME": os.path.join(install_dir, "jdk")},
119
- ),
120
- }
121
- java_opts = system_properties_to_cli_args(sys_props)
122
-
123
135
  for plugin in OPENSEARCH_PLUGIN_LIST:
124
136
  plugin_binary = os.path.join(install_dir, "bin", "opensearch-plugin")
125
137
  plugin_dir = os.path.join(install_dir, "plugins", plugin)
@@ -322,6 +334,18 @@ class ElasticsearchPackageInstaller(PackageInstaller):
322
334
  if not os.environ.get("IGNORE_ES_DOWNLOAD_ERRORS"):
323
335
  raise
324
336
 
337
+ keystore_binary = os.path.join(install_dir, "bin", "elasticsearch-keystore")
338
+ if os.path.exists(keystore_binary):
339
+ # initialize and create the keystore. Concurrent starts of ES will all try to create it at the same
340
+ # time, and fail with a race condition. Creating once when installing solves the issue without
341
+ # the need to lock the starts
342
+ # Ultimately, each cluster should have its own `config` file and maybe not share the same one
343
+ output = run(
344
+ [keystore_binary, "create"],
345
+ env_vars={"ES_JAVA_OPTS": " ".join(java_opts)},
346
+ )
347
+ LOG.debug("Keystore init output: %s", output)
348
+
325
349
  # delete some plugins to free up space
326
350
  for plugin in ELASTICSEARCH_DELETE_MODULES:
327
351
  module_dir = os.path.join(install_dir, "modules", plugin)
@@ -341,16 +365,6 @@ class ElasticsearchPackageInstaller(PackageInstaller):
341
365
  if jvm_options != jvm_options_replaced:
342
366
  save_file(jvm_options_file, jvm_options_replaced)
343
367
 
344
- # patch JVM options file - replace hardcoded heap size settings
345
- jvm_options_file = os.path.join(install_dir, "config", "jvm.options")
346
- if os.path.exists(jvm_options_file):
347
- jvm_options = load_file(jvm_options_file)
348
- jvm_options_replaced = re.sub(
349
- r"(^-Xm[sx][a-zA-Z0-9.]+$)", r"# \1", jvm_options, flags=re.MULTILINE
350
- )
351
- if jvm_options != jvm_options_replaced:
352
- save_file(jvm_options_file, jvm_options_replaced)
353
-
354
368
  def _get_install_marker_path(self, install_dir: str) -> str:
355
369
  return os.path.join(install_dir, "bin", "elasticsearch")
356
370
 
@@ -116,6 +116,11 @@ DEFAULT_OPENSEARCH_DOMAIN_ENDPOINT_OPTIONS = DomainEndpointOptions(
116
116
  CustomEndpointEnabled=False,
117
117
  )
118
118
 
119
+ DEFAULT_AUTOTUNE_OPTIONS = AutoTuneOptionsOutput(
120
+ State=AutoTuneState.ENABLED,
121
+ UseOffPeakWindow=False,
122
+ )
123
+
119
124
 
120
125
  def cluster_manager() -> ClusterManager:
121
126
  global __CLUSTER_MANAGER
@@ -203,6 +208,13 @@ def _status_to_config(status: DomainStatus) -> DomainConfig:
203
208
  cluster_cfg = status.get("ClusterConfig") or {}
204
209
  default_cfg = DEFAULT_OPENSEARCH_CLUSTER_CONFIG
205
210
  config_status = get_domain_config_status()
211
+ autotune_options = status.get("AutoTuneOptions") or DEFAULT_AUTOTUNE_OPTIONS
212
+ autotune_state = autotune_options.get("State") or AutoTuneState.ENABLED
213
+ desired_state = (
214
+ AutoTuneDesiredState.ENABLED
215
+ if autotune_state == AutoTuneState.ENABLED
216
+ else AutoTuneDesiredState.DISABLED
217
+ )
206
218
  return DomainConfig(
207
219
  AccessPolicies=AccessPoliciesStatus(
208
220
  Options=status.get("AccessPolicies", ""),
@@ -275,15 +287,16 @@ def _status_to_config(status: DomainStatus) -> DomainConfig:
275
287
  ),
276
288
  AutoTuneOptions=AutoTuneOptionsStatus(
277
289
  Options=AutoTuneOptions(
278
- DesiredState=AutoTuneDesiredState.ENABLED,
290
+ DesiredState=desired_state,
279
291
  RollbackOnDisable=RollbackOnDisable.NO_ROLLBACK,
280
292
  MaintenanceSchedules=[],
293
+ UseOffPeakWindow=autotune_options.get("UseOffPeakWindow", False),
281
294
  ),
282
295
  Status=AutoTuneStatus(
283
296
  CreationDate=config_status.get("CreationDate"),
284
297
  UpdateDate=config_status.get("UpdateDate"),
285
298
  UpdateVersion=config_status.get("UpdateVersion"),
286
- State=AutoTuneState.ENABLED,
299
+ State=autotune_state,
287
300
  PendingDeletion=config_status.get("PendingDeletion"),
288
301
  ),
289
302
  ),
@@ -315,6 +328,22 @@ def get_domain_status(
315
328
  stored_status.update(request)
316
329
  default_cfg.update(request.get("ClusterConfig", {}))
317
330
 
331
+ autotune_options = stored_status.get("AutoTuneOptions") or deepcopy(DEFAULT_AUTOTUNE_OPTIONS)
332
+ if request and (request_options := request.get("AutoTuneOptions")):
333
+ desired_state = request_options.get("DesiredState") or AutoTuneDesiredState.ENABLED
334
+ state = (
335
+ AutoTuneState.ENABLED
336
+ if desired_state == AutoTuneDesiredState.ENABLED
337
+ else AutoTuneState.DISABLED
338
+ )
339
+ autotune_options = AutoTuneOptionsOutput(
340
+ State=state,
341
+ UseOffPeakWindow=request_options.get(
342
+ "UseOffPeakWindow", autotune_options.get("UseOffPeakWindow", False)
343
+ ),
344
+ )
345
+ stored_status["AutoTuneOptions"] = autotune_options
346
+
318
347
  domain_processing_status = stored_status.get("DomainProcessingStatus", None)
319
348
  processing = stored_status.get("Processing", True)
320
349
  if deleted:
@@ -377,7 +406,10 @@ def get_domain_status(
377
406
  AdvancedSecurityOptions=AdvancedSecurityOptions(
378
407
  Enabled=False, InternalUserDatabaseEnabled=False
379
408
  ),
380
- AutoTuneOptions=AutoTuneOptionsOutput(State=AutoTuneState.ENABLE_IN_PROGRESS),
409
+ AutoTuneOptions=AutoTuneOptionsOutput(
410
+ State=stored_status.get("AutoTuneOptions", {}).get("State"),
411
+ UseOffPeakWindow=autotune_options.get("UseOffPeakWindow", False),
412
+ ),
381
413
  )
382
414
  return new_status
383
415
 
@@ -582,6 +614,24 @@ class OpensearchProvider(OpensearchApi, ServiceLifecycleHook):
582
614
  if domain_status is None:
583
615
  raise ResourceNotFoundException(f"Domain not found: {domain_key.domain_name}")
584
616
 
617
+ if payload.get("AutoTuneOptions"):
618
+ auto_request = payload.pop("AutoTuneOptions")
619
+ desired_state = auto_request.get("DesiredState") or AutoTuneDesiredState.ENABLED
620
+
621
+ state = (
622
+ AutoTuneState.ENABLED
623
+ if desired_state == AutoTuneDesiredState.ENABLED
624
+ else AutoTuneState.DISABLED
625
+ )
626
+
627
+ current_autotune = domain_status.get("AutoTuneOptions", {})
628
+ domain_status["AutoTuneOptions"] = AutoTuneOptionsOutput(
629
+ State=state,
630
+ UseOffPeakWindow=auto_request.get(
631
+ "UseOffPeakWindow", current_autotune.get("UseOffPeakWindow", False)
632
+ ),
633
+ )
634
+
585
635
  status_update: dict = _update_domain_config_request_to_status(payload)
586
636
  domain_status.update(status_update)
587
637
 
@@ -1,5 +1,9 @@
1
1
  from localstack.aws.api.resource_groups import ResourceGroupsApi
2
+ from localstack.state import StateVisitor
2
3
 
3
4
 
4
5
  class ResourceGroupsProvider(ResourceGroupsApi):
5
- pass
6
+ def accept_state_visitor(self, visitor: StateVisitor):
7
+ from moto.resourcegroups.models import resourcegroups_backends
8
+
9
+ visitor.visit(resourcegroups_backends)
@@ -1,7 +1,12 @@
1
1
  from abc import ABC
2
2
 
3
3
  from localstack.aws.api.resourcegroupstaggingapi import ResourcegroupstaggingapiApi
4
+ from localstack.state import StateVisitor
4
5
 
5
6
 
6
7
  class ResourcegroupstaggingapiProvider(ResourcegroupstaggingapiApi, ABC):
7
- pass
8
+ def accept_state_visitor(self, visitor: StateVisitor):
9
+ # currently, Moto resourcegroupstaggingapi stores all tags into the other services backend, so their backend
10
+ # does not hold any state and is not worth saving. It only holds direct references to other services
11
+ # It only holds pagination tokens that are not worth keeping
12
+ pass
@@ -26,9 +26,16 @@ from localstack.aws.api.route53 import (
26
26
  from localstack.aws.connect import connect_to
27
27
  from localstack.services.moto import call_moto
28
28
  from localstack.services.plugins import ServiceLifecycleHook
29
+ from localstack.state import StateVisitor
29
30
 
30
31
 
31
32
  class Route53Provider(Route53Api, ServiceLifecycleHook):
33
+ def accept_state_visitor(self, visitor: StateVisitor):
34
+ from localstack.services.route53.models import route53_stores
35
+
36
+ visitor.visit(route53_backends)
37
+ visitor.visit(route53_stores)
38
+
32
39
  def create_hosted_zone(
33
40
  self,
34
41
  context: RequestContext,
@@ -100,6 +100,7 @@ from localstack.services.route53resolver.utils import (
100
100
  validate_mutation_protection,
101
101
  validate_priority,
102
102
  )
103
+ from localstack.state import StateVisitor
103
104
  from localstack.utils.aws import arns
104
105
  from localstack.utils.aws.arns import extract_account_id_from_arn, extract_region_from_arn
105
106
  from localstack.utils.collections import select_from_typed_dict
@@ -107,6 +108,10 @@ from localstack.utils.patch import patch
107
108
 
108
109
 
109
110
  class Route53ResolverProvider(Route53ResolverApi):
111
+ def accept_state_visitor(self, visitor: StateVisitor):
112
+ visitor.visit(route53resolver_backends)
113
+ visitor.visit(route53resolver_stores)
114
+
110
115
  @staticmethod
111
116
  def get_store(account_id: str, region: str) -> Route53ResolverStore:
112
117
  return route53resolver_stores[account_id][region]
@@ -1,4 +1,5 @@
1
1
  from localstack.aws.api.s3 import (
2
+ BucketLocationConstraint,
2
3
  ChecksumAlgorithm,
3
4
  Grantee,
4
5
  Permission,
@@ -66,6 +67,10 @@ CHECKSUM_ALGORITHMS: list[ChecksumAlgorithm] = [
66
67
  ChecksumAlgorithm.CRC64NVME,
67
68
  ]
68
69
 
70
+ BUCKET_LOCATION_CONSTRAINTS = [constraint.value for constraint in BucketLocationConstraint]
71
+
72
+ EU_WEST_1_LOCATION_CONSTRAINTS = [BucketLocationConstraint.EU, BucketLocationConstraint.eu_west_1]
73
+
69
74
  # response header overrides the client may request
70
75
  ALLOWED_HEADER_OVERRIDES = {
71
76
  "ResponseContentType": "ContentType",
@@ -51,3 +51,12 @@ class InvalidBucketOwnerAWSAccountID(CommonServiceException):
51
51
  class TooManyConfigurations(CommonServiceException):
52
52
  def __init__(self, message=None) -> None:
53
53
  super().__init__("TooManyConfigurations", status_code=400, message=message)
54
+
55
+
56
+ class IllegalLocationConstraintException(CommonServiceException):
57
+ def __init__(self, location_constraint: str):
58
+ super().__init__(
59
+ "IllegalLocationConstraintException",
60
+ status_code=400,
61
+ message=f"The {location_constraint} location constraint is incompatible for the region specific endpoint this request was sent to.",
62
+ )
@@ -16,6 +16,7 @@ from localstack.aws.api.s3 import (
16
16
  BadDigest,
17
17
  BucketAccelerateStatus,
18
18
  BucketKeyEnabled,
19
+ BucketLocationConstraint,
19
20
  BucketName,
20
21
  BucketRegion,
21
22
  BucketVersioningStatus,
@@ -76,7 +77,11 @@ from localstack.services.s3.constants import (
76
77
  S3_UPLOAD_PART_MIN_SIZE,
77
78
  )
78
79
  from localstack.services.s3.exceptions import InvalidRequest
79
- from localstack.services.s3.utils import CombinedCrcHash, get_s3_checksum, rfc_1123_datetime
80
+ from localstack.services.s3.utils import (
81
+ CombinedCrcHash,
82
+ get_s3_checksum,
83
+ rfc_1123_datetime,
84
+ )
80
85
  from localstack.services.stores import (
81
86
  AccountRegionBundle,
82
87
  BaseStore,
@@ -101,6 +106,7 @@ class S3Bucket:
101
106
  name: BucketName
102
107
  bucket_account_id: AccountId
103
108
  bucket_region: BucketRegion
109
+ location_constraint: BucketLocationConstraint | Literal[""]
104
110
  creation_date: datetime
105
111
  multiparts: dict[MultipartUploadId, "S3Multipart"]
106
112
  objects: Union["KeyStore", "VersionedKeyStore"]
@@ -137,10 +143,12 @@ class S3Bucket:
137
143
  acl: AccessControlPolicy = None,
138
144
  object_ownership: ObjectOwnership = None,
139
145
  object_lock_enabled_for_bucket: bool = None,
146
+ location_constraint: BucketLocationConstraint | Literal[""] = "",
140
147
  ):
141
148
  self.name = name
142
149
  self.bucket_account_id = account_id
143
150
  self.bucket_region = bucket_region
151
+ self.location_constraint = location_constraint
144
152
  # If ObjectLock is enabled, it forces the bucket to be versioned as well
145
153
  self.versioning_status = None if not object_lock_enabled_for_bucket else "Enabled"
146
154
  self.objects = KeyStore() if not object_lock_enabled_for_bucket else VersionedKeyStore()
@@ -33,6 +33,7 @@ from localstack.aws.api.s3 import (
33
33
  BucketAlreadyOwnedByYou,
34
34
  BucketCannedACL,
35
35
  BucketLifecycleConfiguration,
36
+ BucketLocationConstraint,
36
37
  BucketLoggingStatus,
37
38
  BucketName,
38
39
  BucketNotEmpty,
@@ -117,7 +118,6 @@ from localstack.aws.api.s3 import (
117
118
  InvalidArgument,
118
119
  InvalidBucketName,
119
120
  InvalidDigest,
120
- InvalidLocationConstraint,
121
121
  InvalidObjectState,
122
122
  InvalidPartNumber,
123
123
  InvalidPartOrder,
@@ -229,7 +229,7 @@ from localstack.aws.handlers import (
229
229
  preprocess_request,
230
230
  serve_custom_service_request_handlers,
231
231
  )
232
- from localstack.constants import AWS_REGION_US_EAST_1
232
+ from localstack.constants import AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1
233
233
  from localstack.services.edge import ROUTER
234
234
  from localstack.services.plugins import ServiceLifecycleHook
235
235
  from localstack.services.s3.codec import AwsChunkedDecoder
@@ -278,6 +278,7 @@ from localstack.services.s3.utils import (
278
278
  etag_to_base_64_content_md5,
279
279
  extract_bucket_key_version_id_from_copy_source,
280
280
  generate_safe_version_id,
281
+ get_bucket_location_xml,
281
282
  get_canned_acl,
282
283
  get_class_attrs_from_spec_class,
283
284
  get_failed_precondition_copy_source,
@@ -304,6 +305,7 @@ from localstack.services.s3.utils import (
304
305
  validate_dict_fields,
305
306
  validate_failed_precondition,
306
307
  validate_kms_key_id,
308
+ validate_location_constraint,
307
309
  validate_tag_set,
308
310
  )
309
311
  from localstack.services.s3.validation import (
@@ -323,6 +325,7 @@ from localstack.services.s3.validation import (
323
325
  from localstack.services.s3.website_hosting import register_website_hosting_routes
324
326
  from localstack.state import AssetDirectory, StateVisitor
325
327
  from localstack.utils.aws.arns import s3_bucket_name
328
+ from localstack.utils.aws.aws_stack import get_valid_regions_for_service
326
329
  from localstack.utils.collections import select_from_typed_dict
327
330
  from localstack.utils.strings import short_uid, to_bytes, to_str
328
331
 
@@ -491,34 +494,20 @@ class S3Provider(S3Api, ServiceLifecycleHook):
491
494
  if not is_bucket_name_valid(bucket_name):
492
495
  raise InvalidBucketName("The specified bucket is not valid.", BucketName=bucket_name)
493
496
 
494
- # the XML parser returns an empty dict if the body contains the following:
495
- # <CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/" />
496
- # but it also returns an empty dict if the body is fully empty. We need to differentiate the 2 cases by checking
497
- # if the body is empty or not
498
- if context.request.data and (
499
- (create_bucket_configuration := request.get("CreateBucketConfiguration")) is not None
500
- ):
501
- if not (bucket_region := create_bucket_configuration.get("LocationConstraint")):
502
- raise MalformedXML()
503
-
504
- if context.region == AWS_REGION_US_EAST_1:
505
- if bucket_region in ("us-east-1", "aws-global"):
506
- raise InvalidLocationConstraint(
507
- "The specified location-constraint is not valid",
508
- LocationConstraint=bucket_region,
509
- )
510
- elif context.region != bucket_region:
511
- raise CommonServiceException(
512
- code="IllegalLocationConstraintException",
513
- message=f"The {bucket_region} location constraint is incompatible for the region specific endpoint this request was sent to.",
514
- )
515
- else:
497
+ create_bucket_configuration = request.get("CreateBucketConfiguration") or {}
498
+
499
+ bucket_tags = create_bucket_configuration.get("Tags")
500
+ if bucket_tags:
501
+ validate_tag_set(bucket_tags, type_set="create-bucket")
502
+
503
+ location_constraint = create_bucket_configuration.get("LocationConstraint", "")
504
+ validate_location_constraint(context.region, location_constraint)
505
+
506
+ bucket_region = location_constraint
507
+ if not location_constraint:
516
508
  bucket_region = AWS_REGION_US_EAST_1
517
- if context.region != bucket_region:
518
- raise CommonServiceException(
519
- code="IllegalLocationConstraintException",
520
- message="The unspecified location constraint is incompatible for the region specific endpoint this request was sent to.",
521
- )
509
+ if location_constraint == BucketLocationConstraint.EU:
510
+ bucket_region = AWS_REGION_EU_WEST_1
522
511
 
523
512
  store = self.get_store(context.account_id, bucket_region)
524
513
 
@@ -527,8 +516,9 @@ class S3Provider(S3Api, ServiceLifecycleHook):
527
516
  if existing_bucket_owner != context.account_id:
528
517
  raise BucketAlreadyExists()
529
518
 
530
- # if the existing bucket has the same owner, the behaviour will depend on the region
531
- if bucket_region != "us-east-1":
519
+ # if the existing bucket has the same owner, the behaviour will depend on the region and if the request has
520
+ # tags
521
+ if bucket_region != AWS_REGION_US_EAST_1 or bucket_tags:
532
522
  raise BucketAlreadyOwnedByYou(
533
523
  "Your previous request to create the named bucket succeeded and you already own it.",
534
524
  BucketName=bucket_name,
@@ -547,6 +537,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
547
537
  # see https://docs.aws.amazon.com/AmazonS3/latest/API/API_Owner.html
548
538
  owner = get_owner_for_account_id(context.account_id)
549
539
  acl = get_access_control_policy_for_new_resource_request(request, owner=owner)
540
+
550
541
  s3_bucket = S3Bucket(
551
542
  name=bucket_name,
552
543
  account_id=context.account_id,
@@ -555,10 +546,16 @@ class S3Provider(S3Api, ServiceLifecycleHook):
555
546
  acl=acl,
556
547
  object_ownership=request.get("ObjectOwnership"),
557
548
  object_lock_enabled_for_bucket=request.get("ObjectLockEnabledForBucket"),
549
+ location_constraint=location_constraint,
558
550
  )
559
551
 
560
552
  store.buckets[bucket_name] = s3_bucket
561
553
  store.global_bucket_map[bucket_name] = s3_bucket.bucket_account_id
554
+ if bucket_tags:
555
+ store.TAGS.tag_resource(
556
+ arn=s3_bucket.bucket_arn,
557
+ tags=bucket_tags,
558
+ )
562
559
  self._cors_handler.invalidate_cache()
563
560
  self._storage_backend.create_bucket(bucket_name)
564
561
 
@@ -607,6 +604,13 @@ class S3Provider(S3Api, ServiceLifecycleHook):
607
604
  bucket_region: BucketRegion = None,
608
605
  **kwargs,
609
606
  ) -> ListBucketsOutput:
607
+ if bucket_region and not config.ALLOW_NONSTANDARD_REGIONS:
608
+ if bucket_region not in get_valid_regions_for_service(self.service):
609
+ raise InvalidArgument(
610
+ f"Argument value {bucket_region} is not a valid AWS Region",
611
+ ArgumentName="bucket-region",
612
+ )
613
+
610
614
  owner = get_owner_for_account_id(context.account_id)
611
615
  store = self.get_store(context.account_id, context.region)
612
616
 
@@ -698,16 +702,18 @@ class S3Provider(S3Api, ServiceLifecycleHook):
698
702
  """
699
703
  store, s3_bucket = self._get_cross_account_bucket(context, bucket)
700
704
 
701
- location_constraint = (
702
- '<?xml version="1.0" encoding="UTF-8"?>\n'
703
- '<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">{{location}}</LocationConstraint>'
704
- )
705
-
706
- location = s3_bucket.bucket_region if s3_bucket.bucket_region != "us-east-1" else ""
707
- location_constraint = location_constraint.replace("{{location}}", location)
705
+ # TODO: Remove usage of getattr once persistence mechanism is updated.
706
+ # If the stored constraint is None the bucket was made before the storage of location_constraint.
707
+ # The EU location constraint wasn't supported before this point so we can safely default to the region.
708
+ location_constraint = getattr(s3_bucket, "location_constraint", None)
709
+ if location_constraint is None:
710
+ location_constraint = (
711
+ s3_bucket.bucket_region if s3_bucket.bucket_region != "us-east-1" else ""
712
+ )
708
713
 
709
- response = GetBucketLocationOutput(LocationConstraint=location_constraint)
710
- return response
714
+ return GetBucketLocationOutput(
715
+ LocationConstraint=get_bucket_location_xml(location_constraint)
716
+ )
711
717
 
712
718
  @handler("PutObject", expand=False)
713
719
  def put_object(
@@ -1189,12 +1195,14 @@ class S3Provider(S3Api, ServiceLifecycleHook):
1189
1195
  response["ChecksumType"] = checksum_type
1190
1196
 
1191
1197
  add_encryption_to_response(response, s3_object=s3_object)
1198
+ object_tags = store.TAGS.tags.get(
1199
+ get_unique_key_id(bucket_name, object_key, s3_object.version_id)
1200
+ )
1201
+ if object_tags:
1202
+ response["TagCount"] = len(object_tags)
1192
1203
 
1193
1204
  # if you specify the VersionId, AWS won't return the Expiration header, even if that's the current version
1194
1205
  if not version_id and s3_bucket.lifecycle_rules:
1195
- object_tags = store.TAGS.tags.get(
1196
- get_unique_key_id(bucket_name, object_key, s3_object.version_id)
1197
- )
1198
1206
  if expiration_header := self._get_expiration_header(
1199
1207
  s3_bucket.lifecycle_rules,
1200
1208
  bucket_name,