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.
- localstack/aws/api/acm/__init__.py +122 -122
- localstack/aws/api/apigateway/__init__.py +604 -561
- localstack/aws/api/cloudcontrol/__init__.py +63 -63
- localstack/aws/api/cloudformation/__init__.py +1201 -969
- localstack/aws/api/cloudwatch/__init__.py +375 -375
- localstack/aws/api/config/__init__.py +784 -786
- localstack/aws/api/dynamodb/__init__.py +753 -759
- localstack/aws/api/dynamodbstreams/__init__.py +74 -74
- localstack/aws/api/ec2/__init__.py +10062 -8826
- localstack/aws/api/es/__init__.py +453 -453
- localstack/aws/api/events/__init__.py +552 -552
- localstack/aws/api/firehose/__init__.py +541 -543
- localstack/aws/api/iam/__init__.py +866 -572
- localstack/aws/api/kinesis/__init__.py +235 -147
- localstack/aws/api/kms/__init__.py +341 -336
- localstack/aws/api/lambda_/__init__.py +974 -621
- localstack/aws/api/logs/__init__.py +988 -675
- localstack/aws/api/opensearch/__init__.py +903 -785
- localstack/aws/api/pipes/__init__.py +336 -336
- localstack/aws/api/redshift/__init__.py +1257 -1166
- localstack/aws/api/resource_groups/__init__.py +175 -175
- localstack/aws/api/resourcegroupstaggingapi/__init__.py +103 -67
- localstack/aws/api/route53/__init__.py +296 -254
- localstack/aws/api/route53resolver/__init__.py +397 -396
- localstack/aws/api/s3/__init__.py +1412 -1349
- localstack/aws/api/s3control/__init__.py +594 -594
- localstack/aws/api/scheduler/__init__.py +118 -118
- localstack/aws/api/secretsmanager/__init__.py +221 -216
- localstack/aws/api/ses/__init__.py +227 -227
- localstack/aws/api/sns/__init__.py +115 -115
- localstack/aws/api/sqs/__init__.py +100 -100
- localstack/aws/api/ssm/__init__.py +1977 -1971
- localstack/aws/api/stepfunctions/__init__.py +375 -333
- localstack/aws/api/sts/__init__.py +142 -66
- localstack/aws/api/support/__init__.py +112 -112
- localstack/aws/api/swf/__init__.py +378 -386
- localstack/aws/api/transcribe/__init__.py +425 -425
- localstack/aws/handlers/logging.py +8 -4
- localstack/aws/handlers/service.py +22 -3
- localstack/aws/protocol/parser.py +1 -1
- localstack/aws/protocol/serializer.py +1 -1
- localstack/aws/scaffold.py +15 -17
- localstack/cli/localstack.py +6 -1
- localstack/deprecations.py +0 -6
- localstack/dev/kubernetes/__main__.py +38 -3
- localstack/services/acm/provider.py +4 -0
- localstack/services/apigateway/helpers.py +5 -9
- localstack/services/apigateway/legacy/provider.py +60 -24
- localstack/services/apigateway/patches.py +0 -9
- localstack/services/cloudformation/engine/template_preparer.py +6 -2
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +12 -0
- localstack/services/cloudformation/provider.py +2 -2
- localstack/services/cloudformation/v2/provider.py +6 -6
- localstack/services/cloudwatch/provider.py +10 -3
- localstack/services/cloudwatch/provider_v2.py +6 -3
- localstack/services/configservice/provider.py +5 -1
- localstack/services/dynamodb/provider.py +1 -0
- localstack/services/dynamodb/v2/provider.py +1 -0
- localstack/services/dynamodbstreams/provider.py +6 -0
- localstack/services/dynamodbstreams/v2/provider.py +6 -0
- localstack/services/ec2/provider.py +6 -0
- localstack/services/es/provider.py +6 -0
- localstack/services/events/provider.py +4 -0
- localstack/services/events/v1/provider.py +9 -0
- localstack/services/firehose/provider.py +5 -0
- localstack/services/iam/provider.py +4 -0
- localstack/services/kinesis/packages.py +1 -1
- localstack/services/kms/models.py +44 -24
- localstack/services/kms/provider.py +97 -16
- localstack/services/lambda_/api_utils.py +40 -21
- localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +1 -1
- localstack/services/lambda_/invocation/assignment.py +4 -1
- localstack/services/lambda_/invocation/execution_environment.py +21 -2
- localstack/services/lambda_/invocation/lambda_models.py +27 -2
- localstack/services/lambda_/invocation/lambda_service.py +51 -3
- localstack/services/lambda_/invocation/models.py +9 -1
- localstack/services/lambda_/invocation/version_manager.py +18 -3
- localstack/services/lambda_/packages.py +1 -1
- localstack/services/lambda_/provider.py +240 -96
- localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
- localstack/services/lambda_/runtimes.py +10 -3
- localstack/services/logs/provider.py +45 -19
- localstack/services/opensearch/provider.py +53 -3
- localstack/services/resource_groups/provider.py +5 -1
- localstack/services/resourcegroupstaggingapi/provider.py +6 -1
- localstack/services/s3/provider.py +29 -16
- localstack/services/s3/utils.py +35 -14
- localstack/services/s3control/provider.py +101 -2
- localstack/services/s3control/validation.py +50 -0
- localstack/services/sns/constants.py +3 -1
- localstack/services/sns/publisher.py +15 -6
- localstack/services/sns/v2/models.py +30 -1
- localstack/services/sns/v2/provider.py +794 -31
- localstack/services/sns/v2/utils.py +20 -0
- localstack/services/sqs/models.py +37 -10
- localstack/services/stepfunctions/asl/component/common/path/result_path.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +0 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +0 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +8 -8
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/{mock_eval_utils.py → local_mock_eval_utils.py} +13 -9
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +6 -6
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +4 -0
- localstack/services/stepfunctions/asl/component/test_state/state/base_mock.py +118 -0
- localstack/services/stepfunctions/asl/component/test_state/state/common.py +82 -0
- localstack/services/stepfunctions/asl/component/test_state/state/execution.py +139 -0
- localstack/services/stepfunctions/asl/component/test_state/state/map.py +77 -0
- localstack/services/stepfunctions/asl/component/test_state/state/task.py +44 -0
- localstack/services/stepfunctions/asl/eval/environment.py +30 -22
- localstack/services/stepfunctions/asl/eval/states.py +1 -1
- localstack/services/stepfunctions/asl/eval/test_state/environment.py +49 -9
- localstack/services/stepfunctions/asl/eval/test_state/program_state.py +22 -0
- localstack/services/stepfunctions/asl/jsonata/jsonata.py +5 -1
- localstack/services/stepfunctions/asl/parse/preprocessor.py +67 -24
- localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py +5 -4
- localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py +222 -31
- localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +170 -22
- localstack/services/stepfunctions/backend/execution.py +6 -6
- localstack/services/stepfunctions/backend/execution_worker.py +5 -5
- localstack/services/stepfunctions/backend/test_state/execution.py +36 -0
- localstack/services/stepfunctions/backend/test_state/execution_worker.py +33 -1
- localstack/services/stepfunctions/backend/test_state/test_state_mock.py +127 -0
- localstack/services/stepfunctions/local_mocking/__init__.py +9 -0
- localstack/services/stepfunctions/{mocking → local_mocking}/mock_config.py +24 -17
- localstack/services/stepfunctions/provider.py +78 -27
- localstack/services/stepfunctions/test_state/mock_config.py +47 -0
- localstack/testing/pytest/fixtures.py +28 -0
- localstack/testing/snapshots/transformer_utility.py +7 -0
- localstack/testing/testselection/matching.py +0 -1
- localstack/utils/analytics/publisher.py +37 -155
- localstack/utils/analytics/service_request_aggregator.py +6 -4
- localstack/utils/aws/arns.py +7 -0
- localstack/utils/aws/client_types.py +0 -8
- localstack/utils/batching.py +258 -0
- localstack/utils/catalog/catalog_loader.py +111 -3
- localstack/utils/collections.py +23 -11
- localstack/utils/crypto.py +109 -0
- localstack/version.py +2 -2
- {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/METADATA +7 -6
- {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/RECORD +149 -141
- localstack_core-4.11.2.dev14.dist-info/plux.json +1 -0
- localstack/services/stepfunctions/mocking/__init__.py +0 -0
- localstack/utils/batch_policy.py +0 -124
- localstack_core-4.10.1.dev7.dist-info/plux.json +0 -1
- /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
- {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack +0 -0
- {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/WHEEL +0 -0
- {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
-
|
|
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) [
|
|
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.
|
|
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
|
-
|
|
119
|
+
moto_groups = copy.deepcopy(dict(region_backend.groups)).values()
|
|
109
120
|
|
|
110
121
|
groups = [
|
|
111
|
-
group.to_describe_dict()
|
|
112
|
-
for
|
|
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=
|
|
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=
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
localstack/services/s3/utils.py
CHANGED
|
@@ -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
|
|
441
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
883
|
+
elif not TAG_REGEX.match(value):
|
|
868
884
|
raise InvalidTag(
|
|
869
|
-
"The TagValue you have provided is invalid", TagKey=key, TagValue=
|
|
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
|
|
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
|
|
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
|
|
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",
|