localstack-core 4.10.1.dev42__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/apigateway/__init__.py +42 -0
- localstack/aws/api/cloudformation/__init__.py +161 -0
- localstack/aws/api/ec2/__init__.py +1165 -12
- localstack/aws/api/iam/__init__.py +227 -0
- localstack/aws/api/kms/__init__.py +1 -0
- localstack/aws/api/lambda_/__init__.py +418 -66
- localstack/aws/api/logs/__init__.py +312 -0
- localstack/aws/api/opensearch/__init__.py +89 -0
- localstack/aws/api/redshift/__init__.py +69 -0
- localstack/aws/api/resourcegroupstaggingapi/__init__.py +36 -0
- localstack/aws/api/route53/__init__.py +42 -0
- localstack/aws/api/route53resolver/__init__.py +1 -0
- localstack/aws/api/s3/__init__.py +62 -0
- localstack/aws/api/secretsmanager/__init__.py +28 -23
- localstack/aws/api/stepfunctions/__init__.py +52 -10
- localstack/aws/api/sts/__init__.py +52 -0
- localstack/aws/handlers/logging.py +8 -4
- localstack/aws/handlers/service.py +11 -2
- localstack/aws/protocol/serializer.py +1 -1
- localstack/deprecations.py +0 -6
- localstack/services/acm/provider.py +4 -0
- localstack/services/apigateway/legacy/provider.py +28 -15
- localstack/services/cloudformation/engine/template_preparer.py +6 -2
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +12 -0
- 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/kms/models.py +10 -20
- localstack/services/kms/provider.py +4 -0
- localstack/services/lambda_/api_utils.py +37 -20
- 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_/provider.py +239 -95
- localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
- localstack/services/lambda_/runtimes.py +3 -1
- localstack/services/logs/provider.py +9 -0
- 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 +28 -15
- 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 +6 -0
- localstack/services/sns/v2/provider.py +650 -19
- localstack/services/sns/v2/utils.py +12 -0
- 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 +5 -0
- 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/batching.py +258 -0
- localstack/utils/collections.py +23 -11
- localstack/version.py +2 -2
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.11.2.dev14.dist-info}/METADATA +5 -5
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.11.2.dev14.dist-info}/RECORD +113 -105
- 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.dev42.dist-info/plux.json +0 -1
- /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
- {localstack_core-4.10.1.dev42.data → localstack_core-4.11.2.dev14.data}/scripts/localstack +0 -0
- {localstack_core-4.10.1.dev42.data → localstack_core-4.11.2.dev14.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.10.1.dev42.data → localstack_core-4.11.2.dev14.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.11.2.dev14.dist-info}/WHEEL +0 -0
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.11.2.dev14.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.11.2.dev14.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.10.1.dev42.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"]
|
|
@@ -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)
|
|
@@ -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,16 +492,12 @@ 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
502
|
if bucket_region in ("us-east-1", "aws-global"):
|
|
506
503
|
raise InvalidLocationConstraint(
|
|
@@ -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",
|
|
@@ -1,5 +1,104 @@
|
|
|
1
|
-
from localstack.aws.api
|
|
1
|
+
from localstack.aws.api import CommonServiceException, RequestContext
|
|
2
|
+
from localstack.aws.api.s3control import (
|
|
3
|
+
AccountId,
|
|
4
|
+
ListTagsForResourceResult,
|
|
5
|
+
S3ControlApi,
|
|
6
|
+
S3ResourceArn,
|
|
7
|
+
TagKeyList,
|
|
8
|
+
TagList,
|
|
9
|
+
TagResourceResult,
|
|
10
|
+
UntagResourceResult,
|
|
11
|
+
)
|
|
12
|
+
from localstack.aws.forwarder import NotImplementedAvoidFallbackError
|
|
13
|
+
from localstack.services.s3.models import s3_stores
|
|
14
|
+
from localstack.services.s3control.validation import validate_tags
|
|
15
|
+
from localstack.utils.tagging import TaggingService
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NoSuchResource(CommonServiceException):
|
|
19
|
+
def __init__(self, message=None):
|
|
20
|
+
super().__init__("NoSuchResource", status_code=404, message=message)
|
|
2
21
|
|
|
3
22
|
|
|
4
23
|
class S3ControlProvider(S3ControlApi):
|
|
5
|
-
|
|
24
|
+
"""
|
|
25
|
+
S3Control is a management interface for S3, and can access some of its internals with no public API
|
|
26
|
+
This requires us to access the s3 stores directly
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _get_tagging_service_for_bucket(
|
|
31
|
+
resource_arn: S3ResourceArn,
|
|
32
|
+
partition: str,
|
|
33
|
+
region: str,
|
|
34
|
+
account_id: str,
|
|
35
|
+
) -> TaggingService:
|
|
36
|
+
s3_prefix = f"arn:{partition}:s3:::"
|
|
37
|
+
if not resource_arn.startswith(s3_prefix):
|
|
38
|
+
# Moto does not support Tagging operations for S3 Control, so we should not forward those operations back
|
|
39
|
+
# to it
|
|
40
|
+
raise NotImplementedAvoidFallbackError(
|
|
41
|
+
"LocalStack only support Bucket tagging operations for S3Control"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
store = s3_stores[account_id][region]
|
|
45
|
+
bucket_name = resource_arn.removeprefix(s3_prefix)
|
|
46
|
+
if bucket_name not in store.global_bucket_map:
|
|
47
|
+
raise NoSuchResource("The specified resource doesn't exist.")
|
|
48
|
+
|
|
49
|
+
return store.TAGS
|
|
50
|
+
|
|
51
|
+
def tag_resource(
|
|
52
|
+
self,
|
|
53
|
+
context: RequestContext,
|
|
54
|
+
account_id: AccountId,
|
|
55
|
+
resource_arn: S3ResourceArn,
|
|
56
|
+
tags: TagList,
|
|
57
|
+
**kwargs,
|
|
58
|
+
) -> TagResourceResult:
|
|
59
|
+
# currently S3Control only supports tagging buckets
|
|
60
|
+
tagging_service = self._get_tagging_service_for_bucket(
|
|
61
|
+
resource_arn=resource_arn,
|
|
62
|
+
partition=context.partition,
|
|
63
|
+
region=context.region,
|
|
64
|
+
account_id=account_id,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
validate_tags(tags=tags)
|
|
68
|
+
tagging_service.tag_resource(resource_arn, tags)
|
|
69
|
+
|
|
70
|
+
return TagResourceResult()
|
|
71
|
+
|
|
72
|
+
def untag_resource(
|
|
73
|
+
self,
|
|
74
|
+
context: RequestContext,
|
|
75
|
+
account_id: AccountId,
|
|
76
|
+
resource_arn: S3ResourceArn,
|
|
77
|
+
tag_keys: TagKeyList,
|
|
78
|
+
**kwargs,
|
|
79
|
+
) -> UntagResourceResult:
|
|
80
|
+
# currently S3Control only supports tagging buckets
|
|
81
|
+
tagging_service = self._get_tagging_service_for_bucket(
|
|
82
|
+
resource_arn=resource_arn,
|
|
83
|
+
partition=context.partition,
|
|
84
|
+
region=context.region,
|
|
85
|
+
account_id=account_id,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
tagging_service.untag_resource(resource_arn, tag_keys)
|
|
89
|
+
|
|
90
|
+
return TagResourceResult()
|
|
91
|
+
|
|
92
|
+
def list_tags_for_resource(
|
|
93
|
+
self, context: RequestContext, account_id: AccountId, resource_arn: S3ResourceArn, **kwargs
|
|
94
|
+
) -> ListTagsForResourceResult:
|
|
95
|
+
# currently S3Control only supports tagging buckets
|
|
96
|
+
tagging_service = self._get_tagging_service_for_bucket(
|
|
97
|
+
resource_arn=resource_arn,
|
|
98
|
+
partition=context.partition,
|
|
99
|
+
region=context.region,
|
|
100
|
+
account_id=account_id,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
tags = tagging_service.list_tags_for_resource(resource_arn)
|
|
104
|
+
return ListTagsForResourceResult(Tags=tags["Tags"])
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from localstack.aws.api.s3 import InvalidTag
|
|
2
|
+
from localstack.aws.api.s3control import Tag, TagList
|
|
3
|
+
from localstack.services.s3.exceptions import MalformedXML
|
|
4
|
+
from localstack.services.s3.utils import TAG_REGEX
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_tags(tags: TagList):
|
|
8
|
+
"""
|
|
9
|
+
Validate the tags provided. This is the same function as S3, but with different error messages
|
|
10
|
+
:param tags: a TagList object
|
|
11
|
+
:raises MalformedXML if the object does not conform to the schema
|
|
12
|
+
:raises InvalidTag if the tag key or value are outside the set of validations defined by S3 and S3Control
|
|
13
|
+
:return: None
|
|
14
|
+
"""
|
|
15
|
+
keys = set()
|
|
16
|
+
for tag in tags:
|
|
17
|
+
tag: Tag
|
|
18
|
+
if set(tag) != {"Key", "Value"}:
|
|
19
|
+
raise MalformedXML()
|
|
20
|
+
|
|
21
|
+
key = tag["Key"]
|
|
22
|
+
value = tag["Value"]
|
|
23
|
+
|
|
24
|
+
if key is None or value is None:
|
|
25
|
+
raise MalformedXML()
|
|
26
|
+
|
|
27
|
+
if key in keys:
|
|
28
|
+
raise InvalidTag(
|
|
29
|
+
"There are duplicate tag keys in your request. Remove the duplicate tag keys and try again.",
|
|
30
|
+
TagKey=key,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if key.startswith("aws:"):
|
|
34
|
+
raise InvalidTag(
|
|
35
|
+
'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.',
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if not TAG_REGEX.match(key):
|
|
39
|
+
raise InvalidTag(
|
|
40
|
+
"This request contains a tag key or value that isn't valid. Valid characters include the following: [a-zA-Z+-=._:/]. Tag keys can contain up to 128 characters. Tag values can contain up to 256 characters.",
|
|
41
|
+
TagKey=key,
|
|
42
|
+
)
|
|
43
|
+
elif not TAG_REGEX.match(value):
|
|
44
|
+
raise InvalidTag(
|
|
45
|
+
"This request contains a tag key or value that isn't valid. Valid characters include the following: [a-zA-Z+-=._:/]. Tag keys can contain up to 128 characters. Tag values can contain up to 256 characters.",
|
|
46
|
+
TagKey=key,
|
|
47
|
+
TagValue=value,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
keys.add(key)
|