localstack-core 4.11.2.dev14__py3-none-any.whl → 4.12.1.dev25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of localstack-core might be problematic. Click here for more details.
- localstack/aws/api/ec2/__init__.py +13 -0
- localstack/aws/api/iam/__init__.py +1 -0
- localstack/aws/api/lambda_/__init__.py +616 -0
- localstack/aws/api/logs/__init__.py +188 -0
- localstack/aws/api/opensearch/__init__.py +11 -0
- localstack/aws/api/route53/__init__.py +3 -0
- localstack/aws/api/s3/__init__.py +2 -0
- localstack/aws/api/s3control/__init__.py +19 -0
- localstack/aws/api/secretsmanager/__init__.py +9 -0
- localstack/aws/connect.py +35 -15
- localstack/aws/protocol/parser.py +6 -1
- localstack/aws/spec-patches.json +0 -38
- localstack/config.py +8 -0
- localstack/constants.py +3 -0
- localstack/dev/kubernetes/__main__.py +39 -14
- localstack/runtime/analytics.py +11 -0
- localstack/services/acm/provider.py +13 -1
- localstack/services/apigateway/legacy/provider.py +25 -4
- localstack/services/cloudformation/engine/v2/change_set_model.py +9 -0
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +3 -1
- localstack/services/cloudformation/engine/v2/change_set_resource_support_checker.py +114 -0
- localstack/services/cloudformation/provider.py +26 -1
- localstack/services/cloudformation/provider_utils.py +20 -0
- localstack/services/cloudformation/resource_provider.py +5 -4
- localstack/services/cloudformation/scaffolding/__main__.py +94 -22
- localstack/services/cloudformation/v2/provider.py +41 -0
- localstack/services/cloudwatch/models.py +10 -2
- localstack/services/cloudwatch/provider_v2.py +15 -20
- localstack/services/kinesis/packages.py +1 -1
- localstack/services/kms/models.py +6 -2
- localstack/services/lambda_/analytics.py +11 -2
- localstack/services/lambda_/invocation/event_manager.py +15 -11
- localstack/services/lambda_/invocation/lambda_models.py +4 -0
- localstack/services/lambda_/invocation/lambda_service.py +11 -0
- localstack/services/lambda_/provider.py +70 -13
- localstack/services/opensearch/packages.py +34 -20
- localstack/services/route53/provider.py +7 -0
- localstack/services/route53resolver/provider.py +5 -0
- localstack/services/s3/constants.py +5 -0
- localstack/services/s3/exceptions.py +9 -0
- localstack/services/s3/models.py +9 -1
- localstack/services/s3/provider.py +25 -30
- localstack/services/s3/utils.py +46 -1
- localstack/services/s3control/provider.py +6 -0
- localstack/services/scheduler/provider.py +4 -2
- localstack/services/secretsmanager/provider.py +4 -0
- localstack/services/ses/provider.py +4 -0
- localstack/services/sns/constants.py +13 -0
- localstack/services/sns/provider.py +5 -0
- localstack/services/sns/v2/models.py +4 -0
- localstack/services/sns/v2/provider.py +145 -0
- localstack/services/sqs/constants.py +6 -0
- localstack/services/sqs/provider.py +9 -1
- localstack/services/sqs/resource_providers/aws_sqs_queue.py +61 -46
- localstack/services/ssm/provider.py +6 -0
- localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +193 -107
- localstack/services/stepfunctions/backend/execution.py +4 -5
- localstack/services/stepfunctions/provider.py +21 -14
- localstack/services/sts/provider.py +7 -0
- localstack/services/support/provider.py +5 -1
- localstack/services/swf/provider.py +5 -1
- localstack/services/transcribe/provider.py +7 -0
- localstack/testing/aws/lambda_utils.py +1 -1
- localstack/testing/aws/util.py +2 -1
- localstack/testing/config.py +1 -0
- localstack/utils/aws/client_types.py +2 -4
- localstack/utils/bootstrap.py +2 -2
- localstack/utils/catalog/catalog.py +3 -2
- localstack/utils/container_utils/container_client.py +22 -13
- localstack/utils/container_utils/docker_cmd_client.py +6 -6
- localstack/version.py +2 -2
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/METADATA +6 -6
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/RECORD +81 -80
- localstack_core-4.12.1.dev25.dist-info/plux.json +1 -0
- localstack_core-4.11.2.dev14.dist-info/plux.json +0 -1
- {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack +0 -0
- {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/WHEEL +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/top_level.txt +0 -0
localstack/aws/spec-patches.json
CHANGED
|
@@ -1372,43 +1372,5 @@
|
|
|
1372
1372
|
"path": "/operations/CreateApiMapping/http/responseCode",
|
|
1373
1373
|
"value": 200
|
|
1374
1374
|
}
|
|
1375
|
-
],
|
|
1376
|
-
"cloudwatch/2010-08-01/service-2": [
|
|
1377
|
-
{
|
|
1378
|
-
"op": "add",
|
|
1379
|
-
"path": "/metadata/awsQueryCompatible",
|
|
1380
|
-
"value": {}
|
|
1381
|
-
},
|
|
1382
|
-
{
|
|
1383
|
-
"op": "add",
|
|
1384
|
-
"path": "/metadata/jsonVersion",
|
|
1385
|
-
"value": "1.0"
|
|
1386
|
-
},
|
|
1387
|
-
{
|
|
1388
|
-
"op": "add",
|
|
1389
|
-
"path": "/metadata/targetPrefix",
|
|
1390
|
-
"value": "GraniteServiceVersion20100801"
|
|
1391
|
-
},
|
|
1392
|
-
{
|
|
1393
|
-
"op": "replace",
|
|
1394
|
-
"path": "/metadata/protocol",
|
|
1395
|
-
"value": "smithy-rpc-v2-cbor"
|
|
1396
|
-
},
|
|
1397
|
-
{
|
|
1398
|
-
"op": "replace",
|
|
1399
|
-
"path": "/metadata/protocols",
|
|
1400
|
-
"value": [
|
|
1401
|
-
"smithy-rpc-v2-cbor",
|
|
1402
|
-
"json",
|
|
1403
|
-
"query"
|
|
1404
|
-
]
|
|
1405
|
-
},
|
|
1406
|
-
{
|
|
1407
|
-
"op": "add",
|
|
1408
|
-
"path": "/shapes/ConflictException/error",
|
|
1409
|
-
"value": {
|
|
1410
|
-
"httpStatusCode": 409
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
1375
|
]
|
|
1414
1376
|
}
|
localstack/config.py
CHANGED
|
@@ -225,6 +225,11 @@ def parse_boolean_env(env_var_name: str) -> bool | None:
|
|
|
225
225
|
return None
|
|
226
226
|
|
|
227
227
|
|
|
228
|
+
def parse_comma_separated_list(env_var_name: str) -> list[str]:
|
|
229
|
+
"""Parse a comma separated list from the given environment variable."""
|
|
230
|
+
return os.environ.get(env_var_name, "").strip().split(",")
|
|
231
|
+
|
|
232
|
+
|
|
228
233
|
def is_env_true(env_var_name: str) -> bool:
|
|
229
234
|
"""Whether the given environment variable has a truthy value."""
|
|
230
235
|
return os.environ.get(env_var_name, "").lower().strip() in TRUE_STRINGS
|
|
@@ -1211,6 +1216,9 @@ CFN_PER_RESOURCE_TIMEOUT = int(os.environ.get("CFN_PER_RESOURCE_TIMEOUT") or 300
|
|
|
1211
1216
|
# EXPERIMENTAL
|
|
1212
1217
|
CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES = is_env_not_false("CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES")
|
|
1213
1218
|
|
|
1219
|
+
# Decrease the waiting time for resource deployment
|
|
1220
|
+
CFN_NO_WAIT_ITERATIONS: str | int | None = os.environ.get("CFN_NO_WAIT_ITERATIONS")
|
|
1221
|
+
|
|
1214
1222
|
# bind address of local DNS server
|
|
1215
1223
|
DNS_ADDRESS = os.environ.get("DNS_ADDRESS") or "0.0.0.0"
|
|
1216
1224
|
# port of the local DNS server
|
localstack/constants.py
CHANGED
|
@@ -117,6 +117,9 @@ LOCALSTACK_INFRA_PROCESS = "LOCALSTACK_INFRA_PROCESS"
|
|
|
117
117
|
# AWS region us-east-1
|
|
118
118
|
AWS_REGION_US_EAST_1 = "us-east-1"
|
|
119
119
|
|
|
120
|
+
# AWS region eu-west-1
|
|
121
|
+
AWS_REGION_EU_WEST_1 = "eu-west-1"
|
|
122
|
+
|
|
120
123
|
# environment variable to override max pool connections
|
|
121
124
|
try:
|
|
122
125
|
MAX_POOL_CONNECTIONS = int(os.environ["MAX_POOL_CONNECTIONS"])
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import os
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess as sp
|
|
3
5
|
from typing import Literal
|
|
4
6
|
|
|
5
7
|
import click
|
|
@@ -315,12 +317,11 @@ def generate_k8s_helm_overrides(
|
|
|
315
317
|
return overrides
|
|
316
318
|
|
|
317
319
|
|
|
318
|
-
def write_file(content: dict, output_path: str
|
|
319
|
-
|
|
320
|
-
with open(path, "w") as f:
|
|
320
|
+
def write_file(content: dict, output_path: str):
|
|
321
|
+
with open(output_path, "w") as f:
|
|
321
322
|
f.write(yaml.dump(content))
|
|
322
323
|
f.close()
|
|
323
|
-
print(f"Generated file at {
|
|
324
|
+
print(f"Generated file at {output_path}")
|
|
324
325
|
|
|
325
326
|
|
|
326
327
|
def print_file(content: dict, file_name: str):
|
|
@@ -330,6 +331,22 @@ def print_file(content: dict, file_name: str):
|
|
|
330
331
|
print("=====================================")
|
|
331
332
|
|
|
332
333
|
|
|
334
|
+
def generate_k3d_command(config_file_path: str) -> str:
|
|
335
|
+
return f"k3d cluster create --config {config_file_path}"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def generate_helm_command(overrides_file_path: str) -> str:
|
|
339
|
+
return f"helm upgrade --install localstack localstack/localstack -f {overrides_file_path}"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def execute_deployment(config_file_path: str, overrides_file_path: str):
|
|
343
|
+
"""
|
|
344
|
+
Use the k3d and helm commands to create a cluster and deploy LocalStack in one command
|
|
345
|
+
"""
|
|
346
|
+
sp.check_call(shlex.split(generate_k3d_command(config_file_path)))
|
|
347
|
+
sp.check_call(shlex.split(generate_helm_command(overrides_file_path)))
|
|
348
|
+
|
|
349
|
+
|
|
333
350
|
@click.command("run")
|
|
334
351
|
@click.option(
|
|
335
352
|
"--pro", is_flag=True, default=None, help="Mount the localstack-pro code into the cluster."
|
|
@@ -386,6 +403,13 @@ def print_file(content: dict, file_name: str):
|
|
|
386
403
|
help="DNS port to expose from the kubernetes node. It is applied only if --expose-dns is set.",
|
|
387
404
|
type=click.IntRange(0, 65535),
|
|
388
405
|
)
|
|
406
|
+
@click.option(
|
|
407
|
+
"--execute",
|
|
408
|
+
"-x",
|
|
409
|
+
is_flag=True,
|
|
410
|
+
default=False,
|
|
411
|
+
help="Execute deployment from generated config files. Implies -w/--write.",
|
|
412
|
+
)
|
|
389
413
|
@click.argument("command", nargs=-1, required=False)
|
|
390
414
|
def run(
|
|
391
415
|
pro: bool = None,
|
|
@@ -400,6 +424,7 @@ def run(
|
|
|
400
424
|
port: int = None,
|
|
401
425
|
expose_dns: bool = False,
|
|
402
426
|
dns_port: int = 53,
|
|
427
|
+
execute: bool = False,
|
|
403
428
|
):
|
|
404
429
|
"""
|
|
405
430
|
A tool for localstack developers to generate the kubernetes cluster configuration file and the overrides to mount the localstack code into the cluster.
|
|
@@ -416,25 +441,25 @@ def run(
|
|
|
416
441
|
overrides_file = overrides_file or "overrides.yml"
|
|
417
442
|
config_file = config_file or "configuration.yml"
|
|
418
443
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
444
|
+
overrides_file_path = os.path.join(output_dir, overrides_file)
|
|
445
|
+
config_file_path = os.path.join(output_dir, config_file)
|
|
446
|
+
|
|
447
|
+
if write or execute:
|
|
448
|
+
write_file(config, config_file_path)
|
|
449
|
+
write_file(overrides, overrides_file_path)
|
|
450
|
+
if execute:
|
|
451
|
+
execute_deployment(config_file, overrides_file)
|
|
422
452
|
else:
|
|
423
453
|
print_file(config, config_file)
|
|
424
454
|
print_file(overrides, overrides_file)
|
|
425
455
|
|
|
426
|
-
overrides_file_path = os.path.join(output_dir, overrides_file)
|
|
427
|
-
config_file_path = os.path.join(output_dir, config_file)
|
|
428
|
-
|
|
429
456
|
print("\nTo create a k3d cluster with the generated configuration, follow these steps:")
|
|
430
457
|
print("1. Run the following command to create the cluster:")
|
|
431
|
-
print(f"\n
|
|
458
|
+
print(f"\n {generate_k3d_command(config_file_path)}\n")
|
|
432
459
|
|
|
433
460
|
print("2. Once the cluster is created, start LocalStack with the generated overrides:")
|
|
434
461
|
print("\n helm repo add localstack https://localstack.github.io/helm-charts # (if required)")
|
|
435
|
-
print(
|
|
436
|
-
f"\n helm upgrade --install localstack localstack/localstack -f {overrides_file_path}\n"
|
|
437
|
-
)
|
|
462
|
+
print(f"\n {generate_helm_command(overrides_file_path)}\n")
|
|
438
463
|
|
|
439
464
|
|
|
440
465
|
def main():
|
localstack/runtime/analytics.py
CHANGED
|
@@ -7,10 +7,14 @@ from localstack.utils.analytics import log
|
|
|
7
7
|
|
|
8
8
|
LOG = logging.getLogger(__name__)
|
|
9
9
|
|
|
10
|
+
# Config options for which both usage and values are reported in analytics.
|
|
11
|
+
# Important: This list must only contain options whose values do not contain PII or sensitive data.
|
|
10
12
|
TRACKED_ENV_VAR = [
|
|
11
13
|
"ACTIVATE_PRO",
|
|
12
14
|
"ALLOW_NONSTANDARD_REGIONS",
|
|
13
15
|
"BEDROCK_PREWARM",
|
|
16
|
+
"CFN_IGNORE_UNSUPPORTED_TYPE_CREATE",
|
|
17
|
+
"CFN_IGNORE_UNSUPPORTED_TYPE_UPDATE",
|
|
14
18
|
"CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES",
|
|
15
19
|
"CLOUDFRONT_LAMBDA_EDGE",
|
|
16
20
|
"CONTAINER_RUNTIME",
|
|
@@ -26,6 +30,7 @@ TRACKED_ENV_VAR = [
|
|
|
26
30
|
"DYNAMODB_IN_MEMORY",
|
|
27
31
|
"DYNAMODB_REMOVE_EXPIRED_ITEMS",
|
|
28
32
|
"EAGER_SERVICE_LOADING",
|
|
33
|
+
"EC2_DOCKER_INIT",
|
|
29
34
|
"EC2_VM_MANAGER",
|
|
30
35
|
"ECS_TASK_EXECUTOR",
|
|
31
36
|
"EDGE_PORT",
|
|
@@ -71,9 +76,15 @@ TRACKED_ENV_VAR = [
|
|
|
71
76
|
"USE_SSL",
|
|
72
77
|
]
|
|
73
78
|
|
|
79
|
+
# Config options for which only the usage is reported in analytics.
|
|
80
|
+
# Use this for options which may hold sensitive data or PII.
|
|
74
81
|
PRESENCE_ENV_VAR = [
|
|
75
82
|
"DATA_DIR",
|
|
76
83
|
"EDGE_FORWARD_URL", # Not functional; deprecated in 1.4.0, removed in 3.0.0
|
|
84
|
+
"EC2_HYPERVISOR_URI",
|
|
85
|
+
"EC2_REFERENCE_DOMAIN",
|
|
86
|
+
"EC2_LIBVIRT_NETWORK",
|
|
87
|
+
"EC2_LIBVIRT_POOL",
|
|
77
88
|
"GATEWAY_LISTEN",
|
|
78
89
|
"HOSTNAME",
|
|
79
90
|
"HOSTNAME_EXTERNAL",
|
|
@@ -17,6 +17,16 @@ from localstack.utils.patch import patch
|
|
|
17
17
|
moto_settings.ACM_VALIDATION_WAIT = min(10, moto_settings.ACM_VALIDATION_WAIT)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
@patch(acm_models.AWSCertificateManagerBackend.list_certificates)
|
|
21
|
+
def list_certificates(list_certificates_orig, self, statuses, includes):
|
|
22
|
+
# Normalize keyTypes filter to match our describe() output format (hyphens)
|
|
23
|
+
if includes and "keyTypes" in includes:
|
|
24
|
+
includes["keyTypes"] = [
|
|
25
|
+
kt.replace("RSA_", "RSA-").replace("EC_", "EC-") for kt in includes["keyTypes"]
|
|
26
|
+
]
|
|
27
|
+
return list_certificates_orig(self, statuses, includes)
|
|
28
|
+
|
|
29
|
+
|
|
20
30
|
@patch(acm_models.CertBundle.describe)
|
|
21
31
|
def describe(describe_orig, self):
|
|
22
32
|
# TODO fix! Terrible hack (for parity). Moto adds certain required fields only if status is PENDING_VALIDATION.
|
|
@@ -71,8 +81,10 @@ def describe(describe_orig, self):
|
|
|
71
81
|
cert[key] = value
|
|
72
82
|
cert["Serial"] = str(cert.get("Serial") or "")
|
|
73
83
|
|
|
74
|
-
if cert.get("KeyAlgorithm") in ["RSA_1024", "RSA_2048"]:
|
|
84
|
+
if cert.get("KeyAlgorithm") in ["RSA_1024", "RSA_2048", "RSA_3072", "RSA_4096"]:
|
|
75
85
|
cert["KeyAlgorithm"] = cert["KeyAlgorithm"].replace("RSA_", "RSA-")
|
|
86
|
+
if cert.get("KeyAlgorithm") in ["EC_prime256v1", "EC_secp384r1", "EC_secp521r1"]:
|
|
87
|
+
cert["KeyAlgorithm"] = cert["KeyAlgorithm"].replace("EC_", "EC-")
|
|
76
88
|
|
|
77
89
|
# add subject alternative names
|
|
78
90
|
if cert["DomainName"] not in sans:
|
|
@@ -94,6 +94,7 @@ from localstack.aws.api.apigateway import (
|
|
|
94
94
|
UsagePlans,
|
|
95
95
|
VpcLink,
|
|
96
96
|
VpcLinks,
|
|
97
|
+
VpcLinkStatus,
|
|
97
98
|
)
|
|
98
99
|
from localstack.aws.connect import connect_to
|
|
99
100
|
from localstack.aws.forwarder import create_aws_request_context
|
|
@@ -1821,11 +1822,31 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
1821
1822
|
tags: MapOfStringToString = None,
|
|
1822
1823
|
**kwargs,
|
|
1823
1824
|
) -> VpcLink:
|
|
1825
|
+
# TODO add tag support
|
|
1826
|
+
if not name:
|
|
1827
|
+
raise BadRequestException("Name cannot be empty")
|
|
1828
|
+
for arn in target_arns:
|
|
1829
|
+
try:
|
|
1830
|
+
parse_arn(arn)
|
|
1831
|
+
except InvalidArnException:
|
|
1832
|
+
raise BadRequestException(f"Invalid ARN. ARN is not well formed {arn}")
|
|
1833
|
+
|
|
1824
1834
|
region_details = get_apigateway_store(context=context)
|
|
1825
1835
|
link_id = short_uid()
|
|
1826
|
-
entry = {
|
|
1836
|
+
entry = {
|
|
1837
|
+
"id": link_id,
|
|
1838
|
+
"status": VpcLinkStatus.PENDING,
|
|
1839
|
+
"name": name,
|
|
1840
|
+
"description": description,
|
|
1841
|
+
"targetArns": target_arns,
|
|
1842
|
+
}
|
|
1827
1843
|
region_details.vpc_links[link_id] = entry
|
|
1828
1844
|
result = to_vpc_link_response_json(entry)
|
|
1845
|
+
|
|
1846
|
+
# update the status and status message of the store object
|
|
1847
|
+
entry["status"] = VpcLinkStatus.AVAILABLE
|
|
1848
|
+
entry["statusMessage"] = "Your vpc link is ready for use"
|
|
1849
|
+
|
|
1829
1850
|
return VpcLink(**result)
|
|
1830
1851
|
|
|
1831
1852
|
def get_vpc_links(
|
|
@@ -1845,7 +1866,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
1845
1866
|
region_details = get_apigateway_store(context=context)
|
|
1846
1867
|
vpc_link = region_details.vpc_links.get(vpc_link_id)
|
|
1847
1868
|
if vpc_link is None:
|
|
1848
|
-
raise NotFoundException(
|
|
1869
|
+
raise NotFoundException("Invalid Vpc Link identifier specified")
|
|
1849
1870
|
result = to_vpc_link_response_json(vpc_link)
|
|
1850
1871
|
return VpcLink(**result)
|
|
1851
1872
|
|
|
@@ -1859,7 +1880,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
1859
1880
|
region_details = get_apigateway_store(context=context)
|
|
1860
1881
|
vpc_link = region_details.vpc_links.get(vpc_link_id)
|
|
1861
1882
|
if vpc_link is None:
|
|
1862
|
-
raise NotFoundException(
|
|
1883
|
+
raise NotFoundException("Invalid Vpc Link identifier specified")
|
|
1863
1884
|
result = apply_json_patch_safe(vpc_link, patch_operations)
|
|
1864
1885
|
result = to_vpc_link_response_json(result)
|
|
1865
1886
|
return VpcLink(**result)
|
|
@@ -1868,7 +1889,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
1868
1889
|
region_details = get_apigateway_store(context=context)
|
|
1869
1890
|
vpc_link = region_details.vpc_links.pop(vpc_link_id, None)
|
|
1870
1891
|
if vpc_link is None:
|
|
1871
|
-
raise NotFoundException(
|
|
1892
|
+
raise NotFoundException("Invalid Vpc Link identifier specified")
|
|
1872
1893
|
|
|
1873
1894
|
# request validators
|
|
1874
1895
|
|
|
@@ -1169,6 +1169,15 @@ class ChangeSetModel:
|
|
|
1169
1169
|
fn_transform,
|
|
1170
1170
|
],
|
|
1171
1171
|
)
|
|
1172
|
+
|
|
1173
|
+
# special case of where either the before or after state does not specify properties but
|
|
1174
|
+
# the resource was in the previous template
|
|
1175
|
+
if (
|
|
1176
|
+
terminal_value_type.change_type == ChangeType.UNCHANGED
|
|
1177
|
+
and properties.change_type != ChangeType.UNCHANGED
|
|
1178
|
+
):
|
|
1179
|
+
change_type = ChangeType.MODIFIED
|
|
1180
|
+
|
|
1172
1181
|
requires_replacement = self._resolve_requires_replacement(
|
|
1173
1182
|
node_properties=properties, resource_type=terminal_value_type
|
|
1174
1183
|
)
|
|
@@ -1069,7 +1069,9 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
|
|
|
1069
1069
|
|
|
1070
1070
|
def _resolve_parameter_type(value: str, type_: str) -> Any:
|
|
1071
1071
|
match type_:
|
|
1072
|
-
case "List<
|
|
1072
|
+
case s if re.match(r"List<[^>]+>", s):
|
|
1073
|
+
return [item.strip() for item in value.split(",")]
|
|
1074
|
+
case "CommaDelimitedList":
|
|
1073
1075
|
return [item.strip() for item in value.split(",")]
|
|
1074
1076
|
case "Number":
|
|
1075
1077
|
# TODO: validate the parameter type at template parse time (or whatever is in parity with AWS) so we know this cannot fail
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from localstack.services.cloudformation.engine.v2.change_set_model import (
|
|
2
|
+
NodeResource,
|
|
3
|
+
)
|
|
4
|
+
from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
|
|
5
|
+
ChangeSetModelVisitor,
|
|
6
|
+
)
|
|
7
|
+
from localstack.services.cloudformation.resources import AWS_AVAILABLE_CFN_RESOURCES
|
|
8
|
+
from localstack.utils.catalog.catalog import (
|
|
9
|
+
AwsServicesSupportStatus,
|
|
10
|
+
CatalogPlugin,
|
|
11
|
+
CfnResourceSupportStatus,
|
|
12
|
+
)
|
|
13
|
+
from localstack.utils.catalog.common import (
|
|
14
|
+
AwsServicesSupportInLatest,
|
|
15
|
+
AwsServiceSupportAtRuntime,
|
|
16
|
+
CloudFormationResourcesSupportAtRuntime,
|
|
17
|
+
CloudFormationResourcesSupportInLatest,
|
|
18
|
+
)
|
|
19
|
+
from localstack.utils.catalog.plugins import get_aws_catalog
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# TODO handle all available resource types
|
|
23
|
+
def _get_service_name(resource_type: str) -> str | None:
|
|
24
|
+
parts = resource_type.split("::")
|
|
25
|
+
if len(parts) == 1:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
match parts:
|
|
29
|
+
case _ if "Cognito::IdentityPool" in resource_type:
|
|
30
|
+
return "cognito-identity"
|
|
31
|
+
case [*_, "Cognito", "UserPool"]:
|
|
32
|
+
return "cognito-idp"
|
|
33
|
+
case [*_, "Cognito", _]:
|
|
34
|
+
return "cognito-idp"
|
|
35
|
+
case [*_, "Elasticsearch", _]:
|
|
36
|
+
return "es"
|
|
37
|
+
case [*_, "OpenSearchService", _]:
|
|
38
|
+
return "opensearch"
|
|
39
|
+
case [*_, "KinesisFirehose", _]:
|
|
40
|
+
return "firehose"
|
|
41
|
+
case [*_, "ResourceGroups", _]:
|
|
42
|
+
return "resource-groups"
|
|
43
|
+
case [*_, "CertificateManager", _]:
|
|
44
|
+
return "acm"
|
|
45
|
+
case _ if "ElasticLoadBalancing::" in resource_type:
|
|
46
|
+
return "elb"
|
|
47
|
+
case _ if "ElasticLoadBalancingV2::" in resource_type:
|
|
48
|
+
return "elbv2"
|
|
49
|
+
case _ if "ApplicationAutoScaling::" in resource_type:
|
|
50
|
+
return "application-autoscaling"
|
|
51
|
+
case _ if "MSK::" in resource_type:
|
|
52
|
+
return "kafka"
|
|
53
|
+
case _ if "Timestream::" in resource_type:
|
|
54
|
+
return "timestream-write"
|
|
55
|
+
case [_, service, *_]:
|
|
56
|
+
return service.lower()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_resource_failure_message(
|
|
60
|
+
resource_type: str, status: AwsServicesSupportStatus | CfnResourceSupportStatus
|
|
61
|
+
) -> str:
|
|
62
|
+
service_name = _get_service_name(resource_type) or "malformed"
|
|
63
|
+
template = "Sorry, the {resource} resource in the {service} service is not supported."
|
|
64
|
+
match status:
|
|
65
|
+
case CloudFormationResourcesSupportAtRuntime.NOT_IMPLEMENTED:
|
|
66
|
+
template = "Sorry, the {resource} resource (from the {service} service) is not supported by this version of LocalStack, but is available in the latest version."
|
|
67
|
+
case CloudFormationResourcesSupportInLatest.NOT_SUPPORTED:
|
|
68
|
+
template = "Sorry, the {resource} resource (from the {service} service) is not currently supported by LocalStack."
|
|
69
|
+
case AwsServiceSupportAtRuntime.AVAILABLE_WITH_LICENSE_UPGRADE:
|
|
70
|
+
template = "Sorry, the {service} service (for the {resource} resource) is not included within your LocalStack license, but is available in an upgraded license."
|
|
71
|
+
case AwsServiceSupportAtRuntime.NOT_IMPLEMENTED:
|
|
72
|
+
template = "The API for service {service} (for the {resource} resource) is either not included in your current license plan or has not yet been emulated by LocalStack."
|
|
73
|
+
case AwsServicesSupportInLatest.NOT_SUPPORTED:
|
|
74
|
+
template = "Sorry, the {service} (for the {resource} resource) service is not currently supported by LocalStack."
|
|
75
|
+
case AwsServicesSupportInLatest.SUPPORTED_WITH_LICENSE_UPGRADE:
|
|
76
|
+
template = "Sorry, the {service} service (for the {resource} resource) is not supported by this version of LocalStack, but is available in the latest version if you upgrade to the latest stable version."
|
|
77
|
+
return template.format(
|
|
78
|
+
resource=resource_type,
|
|
79
|
+
service=service_name,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ChangeSetResourceSupportChecker(ChangeSetModelVisitor):
|
|
84
|
+
catalog: CatalogPlugin
|
|
85
|
+
|
|
86
|
+
TITLE_MESSAGE = "Unsupported resources detected:"
|
|
87
|
+
|
|
88
|
+
def __init__(self):
|
|
89
|
+
self._resource_failure_messages: dict[str, str] = {}
|
|
90
|
+
self.catalog = get_aws_catalog()
|
|
91
|
+
|
|
92
|
+
def visit_node_resource(self, node_resource: NodeResource):
|
|
93
|
+
resource_type = node_resource.type_.value
|
|
94
|
+
if resource_type not in self._resource_failure_messages:
|
|
95
|
+
if resource_type not in AWS_AVAILABLE_CFN_RESOURCES:
|
|
96
|
+
# Ignore non-AWS resources
|
|
97
|
+
pass
|
|
98
|
+
support_status = self._resource_support_status(resource_type)
|
|
99
|
+
if support_status == CloudFormationResourcesSupportAtRuntime.AVAILABLE:
|
|
100
|
+
pass
|
|
101
|
+
else:
|
|
102
|
+
failure_message = _build_resource_failure_message(resource_type, support_status)
|
|
103
|
+
self._resource_failure_messages[resource_type] = failure_message
|
|
104
|
+
super().visit_node_resource(node_resource)
|
|
105
|
+
|
|
106
|
+
def _resource_support_status(
|
|
107
|
+
self, resource_type: str
|
|
108
|
+
) -> AwsServicesSupportStatus | CfnResourceSupportStatus:
|
|
109
|
+
service_name = _get_service_name(resource_type)
|
|
110
|
+
return self.catalog.get_cloudformation_resource_status(resource_type, service_name, True)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def failure_messages(self) -> list[str]:
|
|
114
|
+
return list(self._resource_failure_messages.values())
|
|
@@ -5,6 +5,7 @@ import re
|
|
|
5
5
|
from collections import defaultdict
|
|
6
6
|
from copy import deepcopy
|
|
7
7
|
|
|
8
|
+
from localstack import config
|
|
8
9
|
from localstack.aws.api import CommonServiceException, RequestContext, handler
|
|
9
10
|
from localstack.aws.api.cloudformation import (
|
|
10
11
|
AlreadyExistsException,
|
|
@@ -120,6 +121,7 @@ from localstack.services.cloudformation.stores import (
|
|
|
120
121
|
find_stack_by_id,
|
|
121
122
|
get_cloudformation_store,
|
|
122
123
|
)
|
|
124
|
+
from localstack.services.plugins import ServiceLifecycleHook
|
|
123
125
|
from localstack.state import StateVisitor
|
|
124
126
|
from localstack.utils.collections import (
|
|
125
127
|
remove_attributes,
|
|
@@ -177,7 +179,30 @@ class InternalFailure(CommonServiceException):
|
|
|
177
179
|
super().__init__("InternalFailure", status_code=500, message=message, sender_fault=False)
|
|
178
180
|
|
|
179
181
|
|
|
180
|
-
class CloudformationProvider(CloudformationApi):
|
|
182
|
+
class CloudformationProvider(CloudformationApi, ServiceLifecycleHook):
|
|
183
|
+
def on_before_start(self):
|
|
184
|
+
self._validate_config()
|
|
185
|
+
|
|
186
|
+
def _validate_config(self):
|
|
187
|
+
no_wait_value: int = 5
|
|
188
|
+
try:
|
|
189
|
+
no_wait_value = int(config.CFN_NO_WAIT_ITERATIONS or 5)
|
|
190
|
+
except (TypeError, ValueError):
|
|
191
|
+
LOG.warning(
|
|
192
|
+
"You have set CFN_NO_WAIT_ITERATIONS to an invalid value: '%s'. It must be an integer greater or equal to 0. Using the default of 5",
|
|
193
|
+
config.CFN_NO_WAIT_ITERATIONS,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if no_wait_value < 0:
|
|
197
|
+
LOG.warning(
|
|
198
|
+
"You have set CFN_NO_WAIT_ITERATIONS to an invalid value: '%s'. It must be an integer greater or equal to 0. Using the default of 5",
|
|
199
|
+
config.CFN_NO_WAIT_ITERATIONS,
|
|
200
|
+
)
|
|
201
|
+
no_wait_value = 5
|
|
202
|
+
|
|
203
|
+
# Set the configuration back
|
|
204
|
+
config.CFN_NO_WAIT_ITERATIONS = no_wait_value
|
|
205
|
+
|
|
181
206
|
def _stack_status_is_active(self, stack_status: str) -> bool:
|
|
182
207
|
return stack_status not in [StackStatus.DELETE_COMPLETE]
|
|
183
208
|
|
|
@@ -275,6 +275,26 @@ def convert_values_to_numbers(input_dict: dict, keys_to_skip: list[str] | None =
|
|
|
275
275
|
return recursive_convert(input_dict)
|
|
276
276
|
|
|
277
277
|
|
|
278
|
+
def resource_tags_to_remove_or_update(
|
|
279
|
+
prev_tags: list[dict], new_tags: list[dict]
|
|
280
|
+
) -> tuple[list[str], dict[str, str]]:
|
|
281
|
+
"""
|
|
282
|
+
When updating resources that have tags, we need to determine which tags to remove and which to add/update,
|
|
283
|
+
as these are typically done in separate API calls. The format of prev_tags and new_tags is expected to
|
|
284
|
+
be [{ "Key": tagName, "Value": tagValue }, ...]. The return value will be a tuple of (tags_to_remove, tags_to_update),
|
|
285
|
+
where:
|
|
286
|
+
- tags_to_remove is a list of tag keys that are present in prev_tags but not in new_tags.
|
|
287
|
+
- tags_to_update is a dict of tags to add or update, with the format: { tagName: tagValue, ... }.
|
|
288
|
+
"""
|
|
289
|
+
prev_tag_keys = [tag["Key"] for tag in prev_tags]
|
|
290
|
+
new_tag_keys = [tag["Key"] for tag in new_tags]
|
|
291
|
+
tags_to_remove = list(set(prev_tag_keys) - set(new_tag_keys))
|
|
292
|
+
|
|
293
|
+
# convert from list of dicts, to a single dict because that's what tag_queue APIs expect.
|
|
294
|
+
tags_to_update = {tag["Key"]: tag["Value"] for tag in new_tags}
|
|
295
|
+
return (tags_to_remove, tags_to_update)
|
|
296
|
+
|
|
297
|
+
|
|
278
298
|
# LocalStack specific utilities
|
|
279
299
|
def get_schema_path(file_path: Path) -> dict:
|
|
280
300
|
file_name_base = file_path.name.removesuffix(".py").removesuffix(".py.enc")
|
|
@@ -436,11 +436,11 @@ class ResourceProviderExecutor:
|
|
|
436
436
|
resource: dict,
|
|
437
437
|
raw_payload: ResourceProviderPayload,
|
|
438
438
|
max_timeout: int = config.CFN_PER_RESOURCE_TIMEOUT,
|
|
439
|
-
sleep_time: float =
|
|
439
|
+
sleep_time: float = 1,
|
|
440
440
|
) -> ProgressEvent[Properties]:
|
|
441
441
|
payload = copy.deepcopy(raw_payload)
|
|
442
442
|
|
|
443
|
-
max_iterations = max(ceil(max_timeout / sleep_time),
|
|
443
|
+
max_iterations = max(ceil(max_timeout / sleep_time), 10)
|
|
444
444
|
|
|
445
445
|
for current_iteration in range(max_iterations):
|
|
446
446
|
resource_type = get_resource_type({"Type": raw_payload["resourceType"]})
|
|
@@ -486,10 +486,11 @@ class ResourceProviderExecutor:
|
|
|
486
486
|
payload["requestData"]["resourceProperties"] = event.resource_model
|
|
487
487
|
resource["Properties"] = event.resource_model
|
|
488
488
|
|
|
489
|
-
if current_iteration
|
|
490
|
-
|
|
489
|
+
if current_iteration < config.CFN_NO_WAIT_ITERATIONS:
|
|
490
|
+
pass
|
|
491
491
|
else:
|
|
492
492
|
time.sleep(sleep_time)
|
|
493
|
+
|
|
493
494
|
case OperationStatus.PENDING:
|
|
494
495
|
# come back to this resource in another iteration
|
|
495
496
|
return event
|