localstack-core 4.10.1.dev7__py3-none-any.whl → 4.11.2.dev14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. localstack/aws/api/acm/__init__.py +122 -122
  2. localstack/aws/api/apigateway/__init__.py +604 -561
  3. localstack/aws/api/cloudcontrol/__init__.py +63 -63
  4. localstack/aws/api/cloudformation/__init__.py +1201 -969
  5. localstack/aws/api/cloudwatch/__init__.py +375 -375
  6. localstack/aws/api/config/__init__.py +784 -786
  7. localstack/aws/api/dynamodb/__init__.py +753 -759
  8. localstack/aws/api/dynamodbstreams/__init__.py +74 -74
  9. localstack/aws/api/ec2/__init__.py +10062 -8826
  10. localstack/aws/api/es/__init__.py +453 -453
  11. localstack/aws/api/events/__init__.py +552 -552
  12. localstack/aws/api/firehose/__init__.py +541 -543
  13. localstack/aws/api/iam/__init__.py +866 -572
  14. localstack/aws/api/kinesis/__init__.py +235 -147
  15. localstack/aws/api/kms/__init__.py +341 -336
  16. localstack/aws/api/lambda_/__init__.py +974 -621
  17. localstack/aws/api/logs/__init__.py +988 -675
  18. localstack/aws/api/opensearch/__init__.py +903 -785
  19. localstack/aws/api/pipes/__init__.py +336 -336
  20. localstack/aws/api/redshift/__init__.py +1257 -1166
  21. localstack/aws/api/resource_groups/__init__.py +175 -175
  22. localstack/aws/api/resourcegroupstaggingapi/__init__.py +103 -67
  23. localstack/aws/api/route53/__init__.py +296 -254
  24. localstack/aws/api/route53resolver/__init__.py +397 -396
  25. localstack/aws/api/s3/__init__.py +1412 -1349
  26. localstack/aws/api/s3control/__init__.py +594 -594
  27. localstack/aws/api/scheduler/__init__.py +118 -118
  28. localstack/aws/api/secretsmanager/__init__.py +221 -216
  29. localstack/aws/api/ses/__init__.py +227 -227
  30. localstack/aws/api/sns/__init__.py +115 -115
  31. localstack/aws/api/sqs/__init__.py +100 -100
  32. localstack/aws/api/ssm/__init__.py +1977 -1971
  33. localstack/aws/api/stepfunctions/__init__.py +375 -333
  34. localstack/aws/api/sts/__init__.py +142 -66
  35. localstack/aws/api/support/__init__.py +112 -112
  36. localstack/aws/api/swf/__init__.py +378 -386
  37. localstack/aws/api/transcribe/__init__.py +425 -425
  38. localstack/aws/handlers/logging.py +8 -4
  39. localstack/aws/handlers/service.py +22 -3
  40. localstack/aws/protocol/parser.py +1 -1
  41. localstack/aws/protocol/serializer.py +1 -1
  42. localstack/aws/scaffold.py +15 -17
  43. localstack/cli/localstack.py +6 -1
  44. localstack/deprecations.py +0 -6
  45. localstack/dev/kubernetes/__main__.py +38 -3
  46. localstack/services/acm/provider.py +4 -0
  47. localstack/services/apigateway/helpers.py +5 -9
  48. localstack/services/apigateway/legacy/provider.py +60 -24
  49. localstack/services/apigateway/patches.py +0 -9
  50. localstack/services/cloudformation/engine/template_preparer.py +6 -2
  51. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +12 -0
  52. localstack/services/cloudformation/provider.py +2 -2
  53. localstack/services/cloudformation/v2/provider.py +6 -6
  54. localstack/services/cloudwatch/provider.py +10 -3
  55. localstack/services/cloudwatch/provider_v2.py +6 -3
  56. localstack/services/configservice/provider.py +5 -1
  57. localstack/services/dynamodb/provider.py +1 -0
  58. localstack/services/dynamodb/v2/provider.py +1 -0
  59. localstack/services/dynamodbstreams/provider.py +6 -0
  60. localstack/services/dynamodbstreams/v2/provider.py +6 -0
  61. localstack/services/ec2/provider.py +6 -0
  62. localstack/services/es/provider.py +6 -0
  63. localstack/services/events/provider.py +4 -0
  64. localstack/services/events/v1/provider.py +9 -0
  65. localstack/services/firehose/provider.py +5 -0
  66. localstack/services/iam/provider.py +4 -0
  67. localstack/services/kinesis/packages.py +1 -1
  68. localstack/services/kms/models.py +44 -24
  69. localstack/services/kms/provider.py +97 -16
  70. localstack/services/lambda_/api_utils.py +40 -21
  71. localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +1 -1
  72. localstack/services/lambda_/invocation/assignment.py +4 -1
  73. localstack/services/lambda_/invocation/execution_environment.py +21 -2
  74. localstack/services/lambda_/invocation/lambda_models.py +27 -2
  75. localstack/services/lambda_/invocation/lambda_service.py +51 -3
  76. localstack/services/lambda_/invocation/models.py +9 -1
  77. localstack/services/lambda_/invocation/version_manager.py +18 -3
  78. localstack/services/lambda_/packages.py +1 -1
  79. localstack/services/lambda_/provider.py +240 -96
  80. localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
  81. localstack/services/lambda_/runtimes.py +10 -3
  82. localstack/services/logs/provider.py +45 -19
  83. localstack/services/opensearch/provider.py +53 -3
  84. localstack/services/resource_groups/provider.py +5 -1
  85. localstack/services/resourcegroupstaggingapi/provider.py +6 -1
  86. localstack/services/s3/provider.py +29 -16
  87. localstack/services/s3/utils.py +35 -14
  88. localstack/services/s3control/provider.py +101 -2
  89. localstack/services/s3control/validation.py +50 -0
  90. localstack/services/sns/constants.py +3 -1
  91. localstack/services/sns/publisher.py +15 -6
  92. localstack/services/sns/v2/models.py +30 -1
  93. localstack/services/sns/v2/provider.py +794 -31
  94. localstack/services/sns/v2/utils.py +20 -0
  95. localstack/services/sqs/models.py +37 -10
  96. localstack/services/stepfunctions/asl/component/common/path/result_path.py +1 -1
  97. localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +0 -1
  98. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +0 -1
  99. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +8 -8
  100. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/{mock_eval_utils.py → local_mock_eval_utils.py} +13 -9
  101. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +6 -6
  102. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +1 -1
  103. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +4 -0
  104. localstack/services/stepfunctions/asl/component/test_state/state/base_mock.py +118 -0
  105. localstack/services/stepfunctions/asl/component/test_state/state/common.py +82 -0
  106. localstack/services/stepfunctions/asl/component/test_state/state/execution.py +139 -0
  107. localstack/services/stepfunctions/asl/component/test_state/state/map.py +77 -0
  108. localstack/services/stepfunctions/asl/component/test_state/state/task.py +44 -0
  109. localstack/services/stepfunctions/asl/eval/environment.py +30 -22
  110. localstack/services/stepfunctions/asl/eval/states.py +1 -1
  111. localstack/services/stepfunctions/asl/eval/test_state/environment.py +49 -9
  112. localstack/services/stepfunctions/asl/eval/test_state/program_state.py +22 -0
  113. localstack/services/stepfunctions/asl/jsonata/jsonata.py +5 -1
  114. localstack/services/stepfunctions/asl/parse/preprocessor.py +67 -24
  115. localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py +5 -4
  116. localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py +222 -31
  117. localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +170 -22
  118. localstack/services/stepfunctions/backend/execution.py +6 -6
  119. localstack/services/stepfunctions/backend/execution_worker.py +5 -5
  120. localstack/services/stepfunctions/backend/test_state/execution.py +36 -0
  121. localstack/services/stepfunctions/backend/test_state/execution_worker.py +33 -1
  122. localstack/services/stepfunctions/backend/test_state/test_state_mock.py +127 -0
  123. localstack/services/stepfunctions/local_mocking/__init__.py +9 -0
  124. localstack/services/stepfunctions/{mocking → local_mocking}/mock_config.py +24 -17
  125. localstack/services/stepfunctions/provider.py +78 -27
  126. localstack/services/stepfunctions/test_state/mock_config.py +47 -0
  127. localstack/testing/pytest/fixtures.py +28 -0
  128. localstack/testing/snapshots/transformer_utility.py +7 -0
  129. localstack/testing/testselection/matching.py +0 -1
  130. localstack/utils/analytics/publisher.py +37 -155
  131. localstack/utils/analytics/service_request_aggregator.py +6 -4
  132. localstack/utils/aws/arns.py +7 -0
  133. localstack/utils/aws/client_types.py +0 -8
  134. localstack/utils/batching.py +258 -0
  135. localstack/utils/catalog/catalog_loader.py +111 -3
  136. localstack/utils/collections.py +23 -11
  137. localstack/utils/crypto.py +109 -0
  138. localstack/version.py +2 -2
  139. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/METADATA +7 -6
  140. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/RECORD +149 -141
  141. localstack_core-4.11.2.dev14.dist-info/plux.json +1 -0
  142. localstack/services/stepfunctions/mocking/__init__.py +0 -0
  143. localstack/utils/batch_policy.py +0 -124
  144. localstack_core-4.10.1.dev7.dist-info/plux.json +0 -1
  145. /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
  146. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack +0 -0
  147. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack-supervisor +0 -0
  148. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack.bat +0 -0
  149. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/WHEEL +0 -0
  150. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/entry_points.txt +0 -0
  151. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/licenses/LICENSE.txt +0 -0
  152. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/top_level.txt +0 -0
@@ -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"]
@@ -23,7 +23,7 @@ from localstack.aws.api.lambda_ import Runtime
23
23
  # 5. Run the unit test to check the runtime setup:
24
24
  # tests.unit.services.lambda_.test_api_utils.TestApiUtils.test_check_runtime
25
25
  # 6. Review special tests including:
26
- # a) [ext] tests.aws.services.lambda_.test_lambda_endpoint_injection
26
+ # a) [pro] tests.aws.services.lambda_.test_lambda_endpoint_injection
27
27
  # 7. Before merging, run the ext integration tests to cover transparent endpoint injection testing.
28
28
  # 8. Add the new runtime to the K8 image build: https://github.com/localstack/lambda-images
29
29
  # 9. Inform the web team to update the resource browser (consider offering an endpoint in the future)
@@ -34,12 +34,14 @@ 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",
40
41
  Runtime.nodejs16_x: "nodejs:16",
41
42
  Runtime.nodejs14_x: "nodejs:14", # deprecated Dec 4, 2023 => Jan 9, 2024 => Feb 8, 2024
42
43
  Runtime.nodejs12_x: "nodejs:12", # deprecated Mar 31, 2023 => Mar 31, 2023 => Apr 30, 2023
44
+ Runtime.python3_14: "python:3.14",
43
45
  Runtime.python3_13: "python:3.13",
44
46
  Runtime.python3_12: "python:3.12",
45
47
  Runtime.python3_11: "python:3.11",
@@ -47,6 +49,7 @@ IMAGE_MAPPING: dict[Runtime, str] = {
47
49
  Runtime.python3_9: "python:3.9",
48
50
  Runtime.python3_8: "python:3.8",
49
51
  Runtime.python3_7: "python:3.7", # deprecated Dec 4, 2023 => Jan 9, 2024 => Feb 8, 2024
52
+ Runtime.java25: "java:25",
50
53
  Runtime.java21: "java:21",
51
54
  Runtime.java17: "java:17",
52
55
  Runtime.java11: "java:11",
@@ -110,12 +113,14 @@ ALL_RUNTIMES: list[Runtime] = list(IMAGE_MAPPING.keys())
110
113
  # => Remove deprecated runtimes from this testing list
111
114
  RUNTIMES_AGGREGATED = {
112
115
  "nodejs": [
116
+ Runtime.nodejs24_x,
113
117
  Runtime.nodejs22_x,
114
118
  Runtime.nodejs20_x,
115
119
  Runtime.nodejs18_x,
116
120
  Runtime.nodejs16_x,
117
121
  ],
118
122
  "python": [
123
+ Runtime.python3_14,
119
124
  Runtime.python3_13,
120
125
  Runtime.python3_12,
121
126
  Runtime.python3_11,
@@ -124,6 +129,7 @@ RUNTIMES_AGGREGATED = {
124
129
  Runtime.python3_8,
125
130
  ],
126
131
  "java": [
132
+ Runtime.java25,
127
133
  Runtime.java21,
128
134
  Runtime.java17,
129
135
  Runtime.java11,
@@ -155,12 +161,13 @@ SNAP_START_SUPPORTED_RUNTIMES = [
155
161
  Runtime.java11,
156
162
  Runtime.java17,
157
163
  Runtime.java21,
164
+ Runtime.java25,
158
165
  Runtime.python3_12,
159
166
  Runtime.python3_13,
160
167
  Runtime.dotnet8,
161
168
  ]
162
169
 
163
170
  # An ordered list of all Lambda runtimes considered valid by AWS. Matching snapshots in test_create_lambda_exceptions
164
- VALID_RUNTIMES: str = "[nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, 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]"
165
172
  # An ordered list of all Lambda runtimes for layers considered valid by AWS. Matching snapshots in test_layer_exceptions
166
- VALID_LAYER_RUNTIMES: str = "[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, python3.13, python3.14, nodejs16.x, provided.al2, nodejs4.3-edge, nodejs18.x, ruby3.2, python3.4, ruby3.3, ruby3.4, ruby2.5, python3.6, python2.7]"
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,
@@ -22,10 +23,13 @@ from localstack.aws.api.logs import (
22
23
  InputLogEvents,
23
24
  InvalidParameterException,
24
25
  KmsKeyId,
26
+ ListLogGroupsRequest,
27
+ ListLogGroupsResponse,
25
28
  ListTagsForResourceResponse,
26
29
  ListTagsLogGroupResponse,
27
30
  LogGroupClass,
28
31
  LogGroupName,
32
+ LogGroupSummary,
29
33
  LogsApi,
30
34
  LogStreamName,
31
35
  PutLogEventsResponse,
@@ -40,10 +44,11 @@ from localstack.services import moto
40
44
  from localstack.services.logs.models import get_moto_logs_backend, logs_stores
41
45
  from localstack.services.moto import call_moto
42
46
  from localstack.services.plugins import ServiceLifecycleHook
47
+ from localstack.state import StateVisitor
43
48
  from localstack.utils.aws import arns
44
49
  from localstack.utils.aws.client_types import ServicePrincipal
45
50
  from localstack.utils.bootstrap import is_api_enabled
46
- from localstack.utils.common import is_number
51
+ from localstack.utils.numbers import is_number
47
52
  from localstack.utils.patch import patch
48
53
 
49
54
  LOG = logging.getLogger(__name__)
@@ -54,14 +59,20 @@ class LogsProvider(LogsApi, ServiceLifecycleHook):
54
59
  super().__init__()
55
60
  self.cw_client = connect_to().cloudwatch
56
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
+
57
68
  def put_log_events(
58
69
  self,
59
70
  context: RequestContext,
60
71
  log_group_name: LogGroupName,
61
72
  log_stream_name: LogStreamName,
62
73
  log_events: InputLogEvents,
63
- sequence_token: SequenceToken = None,
64
- entity: Entity = None,
74
+ sequence_token: SequenceToken | None = None,
75
+ entity: Entity | None = None,
65
76
  **kwargs,
66
77
  ) -> PutLogEventsResponse:
67
78
  logs_backend = get_moto_logs_backend(context.account_id, context.region)
@@ -97,33 +108,32 @@ class LogsProvider(LogsApi, ServiceLifecycleHook):
97
108
  ) -> DescribeLogGroupsResponse:
98
109
  region_backend = get_moto_logs_backend(context.account_id, context.region)
99
110
 
100
- prefix: str = request.get("logGroupNamePrefix", "")
101
- pattern: str = request.get("logGroupNamePattern", "")
111
+ prefix: str | None = request.get("logGroupNamePrefix", "")
112
+ pattern: str | None = request.get("logGroupNamePattern", "")
102
113
 
103
114
  if pattern and prefix:
104
115
  raise InvalidParameterException(
105
116
  "LogGroup name prefix and LogGroup name pattern are mutually exclusive parameters."
106
117
  )
107
118
 
108
- copy_groups = copy.deepcopy(dict(region_backend.groups))
119
+ moto_groups = copy.deepcopy(dict(region_backend.groups)).values()
109
120
 
110
121
  groups = [
111
- group.to_describe_dict()
112
- for name, group in copy_groups.items()
122
+ {"logGroupClass": LogGroupClass.STANDARD} | group.to_describe_dict()
123
+ for group in sorted(moto_groups, key=lambda g: g.name)
113
124
  if not (prefix or pattern)
114
- or (prefix and name.startswith(prefix))
115
- or (pattern and pattern in name)
125
+ or (prefix and group.name.startswith(prefix))
126
+ or (pattern and pattern in group.name)
116
127
  ]
117
128
 
118
- groups = sorted(groups, key=lambda x: x["logGroupName"])
119
129
  return DescribeLogGroupsResponse(logGroups=groups)
120
130
 
121
131
  @handler("DescribeLogStreams", expand=False)
122
132
  def describe_log_streams(
123
133
  self, context: RequestContext, request: DescribeLogStreamsRequest
124
134
  ) -> DescribeLogStreamsResponse:
125
- log_group_name: str = request.get("logGroupName")
126
- log_group_identifier: str = request.get("logGroupIdentifier")
135
+ log_group_name: str | None = request.get("logGroupName")
136
+ log_group_identifier: str | None = request.get("logGroupIdentifier")
127
137
 
128
138
  if log_group_identifier and log_group_name:
129
139
  raise CommonServiceException(
@@ -138,13 +148,30 @@ class LogsProvider(LogsApi, ServiceLifecycleHook):
138
148
 
139
149
  return moto.call_moto_with_request(context, request_copy)
140
150
 
151
+ @handler("ListLogGroups", expand=False)
152
+ def list_log_groups(
153
+ self, context: RequestContext, request: ListLogGroupsRequest
154
+ ) -> ListLogGroupsResponse:
155
+ pattern: str | None = request.get("logGroupNamePattern")
156
+ region_backend: LogsBackend = get_moto_logs_backend(context.account_id, context.region)
157
+ moto_groups = copy.deepcopy(region_backend.groups).values()
158
+ groups = [
159
+ LogGroupSummary(
160
+ logGroupName=group.name, logGroupArn=group.arn, logGroupClass=LogGroupClass.STANDARD
161
+ )
162
+ for group in sorted(moto_groups, key=lambda g: g.name)
163
+ if not pattern or pattern in group.name
164
+ ]
165
+ return ListLogGroupsResponse(logGroups=groups)
166
+
141
167
  def create_log_group(
142
168
  self,
143
169
  context: RequestContext,
144
170
  log_group_name: LogGroupName,
145
- kms_key_id: KmsKeyId = None,
146
- tags: Tags = None,
147
- log_group_class: LogGroupClass = None,
171
+ kms_key_id: KmsKeyId | None = None,
172
+ tags: Tags | None = None,
173
+ log_group_class: LogGroupClass | None = None,
174
+ deletion_protection_enabled: DeletionProtectionEnabled | None = None,
148
175
  **kwargs,
149
176
  ) -> None:
150
177
  call_moto(context)
@@ -442,10 +469,9 @@ def moto_to_describe_dict(target, self):
442
469
  # reported race condition in https://github.com/localstack/localstack/issues/8011
443
470
  # making copy of "streams" dict here to avoid issues while summing up storedBytes
444
471
  copy_streams = copy.deepcopy(self.streams)
445
- # parity tests shows that the arn ends with ":*"
446
- arn = self.arn if self.arn.endswith(":*") else f"{self.arn}:*"
447
472
  log_group = {
448
- "arn": arn,
473
+ "arn": f"{self.arn}:*",
474
+ "logGroupArn": self.arn,
449
475
  "creationTime": self.creation_time,
450
476
  "logGroupName": self.name,
451
477
  "metricFilterCount": 0,
@@ -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
@@ -323,6 +323,7 @@ from localstack.services.s3.validation import (
323
323
  from localstack.services.s3.website_hosting import register_website_hosting_routes
324
324
  from localstack.state import AssetDirectory, StateVisitor
325
325
  from localstack.utils.aws.arns import s3_bucket_name
326
+ from localstack.utils.aws.aws_stack import get_valid_regions_for_service
326
327
  from localstack.utils.collections import select_from_typed_dict
327
328
  from localstack.utils.strings import short_uid, to_bytes, to_str
328
329
 
@@ -491,18 +492,14 @@ class S3Provider(S3Api, ServiceLifecycleHook):
491
492
  if not is_bucket_name_valid(bucket_name):
492
493
  raise InvalidBucketName("The specified bucket is not valid.", BucketName=bucket_name)
493
494
 
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
-
495
+ create_bucket_configuration = request.get("CreateBucketConfiguration") or {}
496
+ bucket_region = create_bucket_configuration.get("LocationConstraint")
497
+ bucket_tags = create_bucket_configuration.get("Tags")
498
+ if bucket_tags:
499
+ validate_tag_set(bucket_tags, type_set="create-bucket")
500
+ if bucket_region:
504
501
  if context.region == AWS_REGION_US_EAST_1:
505
- if bucket_region == "us-east-1":
502
+ if bucket_region in ("us-east-1", "aws-global"):
506
503
  raise InvalidLocationConstraint(
507
504
  "The specified location-constraint is not valid",
508
505
  LocationConstraint=bucket_region,
@@ -527,8 +524,9 @@ class S3Provider(S3Api, ServiceLifecycleHook):
527
524
  if existing_bucket_owner != context.account_id:
528
525
  raise BucketAlreadyExists()
529
526
 
530
- # if the existing bucket has the same owner, the behaviour will depend on the region
531
- if bucket_region != "us-east-1":
527
+ # if the existing bucket has the same owner, the behaviour will depend on the region and if the request has
528
+ # tags
529
+ if bucket_region != AWS_REGION_US_EAST_1 or bucket_tags:
532
530
  raise BucketAlreadyOwnedByYou(
533
531
  "Your previous request to create the named bucket succeeded and you already own it.",
534
532
  BucketName=bucket_name,
@@ -547,6 +545,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
547
545
  # see https://docs.aws.amazon.com/AmazonS3/latest/API/API_Owner.html
548
546
  owner = get_owner_for_account_id(context.account_id)
549
547
  acl = get_access_control_policy_for_new_resource_request(request, owner=owner)
548
+
550
549
  s3_bucket = S3Bucket(
551
550
  name=bucket_name,
552
551
  account_id=context.account_id,
@@ -559,6 +558,11 @@ class S3Provider(S3Api, ServiceLifecycleHook):
559
558
 
560
559
  store.buckets[bucket_name] = s3_bucket
561
560
  store.global_bucket_map[bucket_name] = s3_bucket.bucket_account_id
561
+ if bucket_tags:
562
+ store.TAGS.tag_resource(
563
+ arn=s3_bucket.bucket_arn,
564
+ tags=bucket_tags,
565
+ )
562
566
  self._cors_handler.invalidate_cache()
563
567
  self._storage_backend.create_bucket(bucket_name)
564
568
 
@@ -607,6 +611,13 @@ class S3Provider(S3Api, ServiceLifecycleHook):
607
611
  bucket_region: BucketRegion = None,
608
612
  **kwargs,
609
613
  ) -> ListBucketsOutput:
614
+ if bucket_region and not config.ALLOW_NONSTANDARD_REGIONS:
615
+ if bucket_region not in get_valid_regions_for_service(self.service):
616
+ raise InvalidArgument(
617
+ f"Argument value {bucket_region} is not a valid AWS Region",
618
+ ArgumentName="bucket-region",
619
+ )
620
+
610
621
  owner = get_owner_for_account_id(context.account_id)
611
622
  store = self.get_store(context.account_id, context.region)
612
623
 
@@ -1189,12 +1200,14 @@ class S3Provider(S3Api, ServiceLifecycleHook):
1189
1200
  response["ChecksumType"] = checksum_type
1190
1201
 
1191
1202
  add_encryption_to_response(response, s3_object=s3_object)
1203
+ object_tags = store.TAGS.tags.get(
1204
+ get_unique_key_id(bucket_name, object_key, s3_object.version_id)
1205
+ )
1206
+ if object_tags:
1207
+ response["TagCount"] = len(object_tags)
1192
1208
 
1193
1209
  # if you specify the VersionId, AWS won't return the Expiration header, even if that's the current version
1194
1210
  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
1211
  if expiration_header := self._get_expiration_header(
1199
1212
  s3_bucket.lifecycle_rules,
1200
1213
  bucket_name,
@@ -421,6 +421,7 @@ def get_failed_upload_part_copy_source_preconditions(
421
421
  if_none_match = request.get("CopySourceIfNoneMatch")
422
422
  if_unmodified_since = request.get("CopySourceIfUnmodifiedSince")
423
423
  if_modified_since = request.get("CopySourceIfModifiedSince")
424
+ last_modified = second_resolution_datetime(last_modified)
424
425
 
425
426
  if if_match:
426
427
  if if_match.strip('"') != etag.strip('"'):
@@ -431,15 +432,15 @@ def get_failed_upload_part_copy_source_preconditions(
431
432
  if if_unmodified_since:
432
433
  return None
433
434
 
434
- if if_unmodified_since and if_unmodified_since < last_modified:
435
+ if if_unmodified_since and second_resolution_datetime(if_unmodified_since) < last_modified:
435
436
  return "x-amz-copy-source-If-Unmodified-Since"
436
437
 
437
438
  if if_none_match and if_none_match.strip('"') == etag.strip('"'):
438
439
  return "x-amz-copy-source-If-None-Match"
439
440
 
440
- if if_modified_since and last_modified < if_modified_since < datetime.datetime.now(
441
- tz=_gmt_zone_info
442
- ):
441
+ if if_modified_since and last_modified <= second_resolution_datetime(
442
+ if_modified_since
443
+ ) < datetime.datetime.now(tz=_gmt_zone_info):
443
444
  return "x-amz-copy-source-If-Modified-Since"
444
445
 
445
446
 
@@ -701,6 +702,10 @@ def str_to_rfc_1123_datetime(value: str) -> datetime.datetime:
701
702
  return datetime.datetime.strptime(value, RFC1123).replace(tzinfo=_gmt_zone_info)
702
703
 
703
704
 
705
+ def second_resolution_datetime(src: datetime.datetime) -> datetime.datetime:
706
+ return src.replace(microsecond=0)
707
+
708
+
704
709
  def add_expiration_days_to_datetime(user_datatime: datetime.datetime, exp_days: int) -> str:
705
710
  """
706
711
  This adds expiration days to a datetime, rounding to the next day at midnight UTC.
@@ -836,13 +841,20 @@ def parse_tagging_header(tagging_header: TaggingHeader) -> dict:
836
841
  )
837
842
 
838
843
 
839
- def validate_tag_set(tag_set: TagSet, type_set: Literal["bucket", "object"] = "bucket"):
844
+ def validate_tag_set(
845
+ tag_set: TagSet, type_set: Literal["bucket", "object", "create-bucket"] = "bucket"
846
+ ):
840
847
  keys = set()
841
848
  for tag in tag_set:
842
849
  if set(tag) != {"Key", "Value"}:
843
850
  raise MalformedXML()
844
851
 
845
852
  key = tag["Key"]
853
+ value = tag["Value"]
854
+
855
+ if key is None or value is None:
856
+ raise MalformedXML()
857
+
846
858
  if key in keys:
847
859
  raise InvalidTag(
848
860
  "Cannot provide multiple Tags with the same key",
@@ -852,11 +864,15 @@ def validate_tag_set(tag_set: TagSet, type_set: Literal["bucket", "object"] = "b
852
864
  if key.startswith("aws:"):
853
865
  if type_set == "bucket":
854
866
  message = "System tags cannot be added/updated by requester"
855
- else:
867
+ elif type_set == "object":
856
868
  message = "Your TagKey cannot be prefixed with aws:"
869
+ else:
870
+ message = 'User-defined tag keys can\'t start with "aws:". This prefix is reserved for system tags. Remove "aws:" from your tag keys and try again.'
857
871
  raise InvalidTag(
858
872
  message,
859
- TagKey=key,
873
+ # weirdly, AWS does not return the `TagKey` field here, but it does if the TagKey does not match the
874
+ # regex in the next step
875
+ TagKey=key if type_set != "create-bucket" else None,
860
876
  )
861
877
 
862
878
  if not TAG_REGEX.match(key):
@@ -864,9 +880,9 @@ def validate_tag_set(tag_set: TagSet, type_set: Literal["bucket", "object"] = "b
864
880
  "The TagKey you have provided is invalid",
865
881
  TagKey=key,
866
882
  )
867
- elif not TAG_REGEX.match(tag["Value"]):
883
+ elif not TAG_REGEX.match(value):
868
884
  raise InvalidTag(
869
- "The TagValue you have provided is invalid", TagKey=key, TagValue=tag["Value"]
885
+ "The TagValue you have provided is invalid", TagKey=key, TagValue=value
870
886
  )
871
887
 
872
888
  keys.add(key)
@@ -908,6 +924,7 @@ def get_failed_precondition_copy_source(
908
924
  :param etag: source object ETag
909
925
  :return str: the failed precondition to raise
910
926
  """
927
+ last_modified = second_resolution_datetime(last_modified)
911
928
  if (cs_if_match := request.get("CopySourceIfMatch")) and etag.strip('"') != cs_if_match.strip(
912
929
  '"'
913
930
  ):
@@ -915,7 +932,7 @@ def get_failed_precondition_copy_source(
915
932
 
916
933
  elif (
917
934
  cs_if_unmodified_since := request.get("CopySourceIfUnmodifiedSince")
918
- ) and last_modified > cs_if_unmodified_since:
935
+ ) and last_modified > second_resolution_datetime(cs_if_unmodified_since):
919
936
  return "x-amz-copy-source-If-Unmodified-Since"
920
937
 
921
938
  elif (cs_if_none_match := request.get("CopySourceIfNoneMatch")) and etag.strip(
@@ -925,7 +942,9 @@ def get_failed_precondition_copy_source(
925
942
 
926
943
  elif (
927
944
  cs_if_modified_since := request.get("CopySourceIfModifiedSince")
928
- ) and last_modified < cs_if_modified_since < datetime.datetime.now(tz=_gmt_zone_info):
945
+ ) and last_modified <= second_resolution_datetime(cs_if_modified_since) < datetime.datetime.now(
946
+ tz=_gmt_zone_info
947
+ ):
929
948
  return "x-amz-copy-source-If-Modified-Since"
930
949
 
931
950
 
@@ -943,13 +962,13 @@ def validate_failed_precondition(
943
962
  """
944
963
  precondition_failed = None
945
964
  # last_modified needs to be rounded to a second so that strict equality can be enforced from a RFC1123 header
946
- last_modified = last_modified.replace(microsecond=0)
965
+ last_modified = second_resolution_datetime(last_modified)
947
966
  if (if_match := request.get("IfMatch")) and etag != if_match.strip('"'):
948
967
  precondition_failed = "If-Match"
949
968
 
950
969
  elif (
951
970
  if_unmodified_since := request.get("IfUnmodifiedSince")
952
- ) and last_modified > if_unmodified_since:
971
+ ) and last_modified > second_resolution_datetime(if_unmodified_since):
953
972
  precondition_failed = "If-Unmodified-Since"
954
973
 
955
974
  if precondition_failed:
@@ -960,7 +979,9 @@ def validate_failed_precondition(
960
979
 
961
980
  if ((if_none_match := request.get("IfNoneMatch")) and etag == if_none_match.strip('"')) or (
962
981
  (if_modified_since := request.get("IfModifiedSince"))
963
- and last_modified <= if_modified_since < datetime.datetime.now(tz=_gmt_zone_info)
982
+ and last_modified
983
+ <= second_resolution_datetime(if_modified_since)
984
+ < datetime.datetime.now(tz=_gmt_zone_info)
964
985
  ):
965
986
  raise CommonServiceException(
966
987
  message="Not Modified",