localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev7__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/cloudformation/__init__.py +1 -0
- localstack/aws/api/cloudwatch/__init__.py +41 -1
- localstack/aws/api/config/__init__.py +4 -0
- localstack/aws/api/core.py +4 -0
- localstack/aws/api/ec2/__init__.py +1113 -56
- localstack/aws/api/iam/__init__.py +7 -0
- localstack/aws/api/kinesis/__init__.py +19 -0
- localstack/aws/api/kms/__init__.py +6 -0
- localstack/aws/api/lambda_/__init__.py +13 -0
- localstack/aws/api/logs/__init__.py +15 -0
- localstack/aws/api/redshift/__init__.py +9 -3
- localstack/aws/api/route53/__init__.py +2 -0
- localstack/aws/api/s3/__init__.py +12 -0
- localstack/aws/api/s3control/__init__.py +32 -0
- localstack/aws/api/ssm/__init__.py +2 -0
- localstack/aws/client.py +7 -2
- localstack/aws/forwarder.py +52 -5
- localstack/aws/handlers/analytics.py +1 -1
- localstack/aws/handlers/logging.py +12 -2
- localstack/aws/handlers/metric_handler.py +41 -1
- localstack/aws/handlers/service.py +32 -9
- localstack/aws/protocol/parser.py +440 -21
- localstack/aws/protocol/serializer.py +684 -64
- localstack/aws/protocol/service_router.py +120 -20
- localstack/aws/skeleton.py +4 -2
- localstack/aws/spec-patches.json +58 -0
- localstack/aws/spec.py +33 -13
- localstack/cli/exceptions.py +1 -1
- localstack/cli/localstack.py +4 -4
- localstack/cli/lpm.py +3 -4
- localstack/cli/profiles.py +1 -2
- localstack/config.py +18 -12
- localstack/constants.py +4 -29
- localstack/dev/kubernetes/__main__.py +1 -1
- localstack/dev/run/paths.py +1 -1
- localstack/dns/plugins.py +5 -1
- localstack/dns/server.py +12 -3
- localstack/packages/api.py +9 -8
- localstack/packages/core.py +2 -2
- localstack/packages/plugins.py +0 -8
- localstack/runtime/init.py +1 -1
- localstack/services/apigateway/legacy/provider.py +53 -3
- localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
- localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
- localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
- localstack/services/apigateway/next_gen/provider.py +5 -0
- localstack/services/cloudformation/engine/entities.py +12 -1
- localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
- localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
- localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
- localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
- localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
- localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
- localstack/services/cloudformation/engine/v2/resolving.py +6 -4
- localstack/services/cloudformation/engine/yaml_parser.py +9 -2
- localstack/services/cloudformation/resource_provider.py +5 -1
- localstack/services/cloudformation/resources.py +24149 -0
- localstack/services/cloudformation/v2/entities.py +6 -3
- localstack/services/cloudformation/v2/provider.py +172 -27
- localstack/services/cloudformation/v2/types.py +8 -4
- localstack/services/cloudwatch/provider_v2.py +25 -28
- localstack/services/dynamodb/packages.py +2 -1
- localstack/services/dynamodb/provider.py +42 -0
- localstack/services/dynamodb/v2/provider.py +42 -0
- localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
- localstack/services/es/provider.py +2 -2
- localstack/services/events/event_rule_engine.py +31 -13
- localstack/services/events/models.py +4 -5
- localstack/services/events/target.py +17 -9
- localstack/services/iam/provider.py +11 -116
- localstack/services/iam/resources/policy_simulator.py +133 -0
- localstack/services/kinesis/models.py +15 -2
- localstack/services/kinesis/provider.py +77 -0
- localstack/services/kms/provider.py +14 -5
- localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
- localstack/services/lambda_/packages.py +1 -1
- localstack/services/logs/provider.py +1 -1
- localstack/services/moto.py +2 -1
- localstack/services/opensearch/cluster.py +15 -7
- localstack/services/opensearch/packages.py +26 -7
- localstack/services/opensearch/provider.py +6 -1
- localstack/services/opensearch/versions.py +56 -7
- localstack/services/s3/constants.py +5 -2
- localstack/services/s3/cors.py +4 -4
- localstack/services/s3/notifications.py +1 -1
- localstack/services/s3/presigned_url.py +27 -43
- localstack/services/s3/provider.py +67 -11
- localstack/services/s3/utils.py +42 -11
- localstack/services/ses/provider.py +16 -7
- localstack/services/sns/constants.py +7 -1
- localstack/services/sns/v2/models.py +167 -0
- localstack/services/sns/v2/provider.py +860 -2
- localstack/services/sns/v2/utils.py +130 -0
- localstack/services/sqs/developer_api.py +205 -0
- localstack/services/sqs/models.py +42 -3
- localstack/services/sqs/provider.py +8 -309
- localstack/services/sqs/query_api.py +1 -1
- localstack/services/sqs/utils.py +121 -2
- localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
- localstack/testing/aws/cloudformation_utils.py +1 -1
- localstack/testing/pytest/cloudformation/fixtures.py +3 -3
- localstack/testing/pytest/container.py +4 -5
- localstack/testing/pytest/fixtures.py +20 -19
- localstack/testing/pytest/in_memory_localstack.py +0 -4
- localstack/testing/pytest/marking.py +13 -4
- localstack/testing/pytest/stepfunctions/utils.py +4 -3
- localstack/testing/pytest/util.py +1 -1
- localstack/testing/pytest/validation_tracking.py +1 -2
- localstack/testing/snapshots/transformer_utility.py +5 -0
- localstack/utils/analytics/events.py +2 -2
- localstack/utils/analytics/metadata.py +1 -2
- localstack/utils/analytics/metrics/counter.py +6 -8
- localstack/utils/analytics/publisher.py +1 -2
- localstack/utils/analytics/service_request_aggregator.py +2 -2
- localstack/utils/archives.py +11 -11
- localstack/utils/aws/arns.py +17 -9
- localstack/utils/aws/aws_responses.py +7 -7
- localstack/utils/aws/aws_stack.py +2 -3
- localstack/utils/aws/message_forwarding.py +1 -2
- localstack/utils/aws/request_context.py +4 -5
- localstack/utils/batch_policy.py +3 -3
- localstack/utils/bootstrap.py +7 -7
- localstack/utils/catalog/catalog.py +139 -0
- localstack/utils/catalog/catalog_loader.py +11 -0
- localstack/utils/catalog/common.py +58 -0
- localstack/utils/catalog/plugins.py +28 -0
- localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
- localstack/utils/collections.py +7 -8
- localstack/utils/config_listener.py +1 -1
- localstack/utils/container_networking.py +2 -3
- localstack/utils/container_utils/container_client.py +115 -131
- localstack/utils/container_utils/docker_cmd_client.py +42 -42
- localstack/utils/container_utils/docker_sdk_client.py +63 -62
- localstack/utils/diagnose.py +2 -3
- localstack/utils/docker_utils.py +3 -4
- localstack/utils/files.py +31 -7
- localstack/utils/functions.py +3 -2
- localstack/utils/http.py +4 -5
- localstack/utils/json.py +19 -5
- localstack/utils/kinesis/kinesis_connector.py +2 -1
- localstack/utils/net.py +6 -6
- localstack/utils/no_exit_argument_parser.py +2 -2
- localstack/utils/numbers.py +9 -2
- localstack/utils/objects.py +6 -5
- localstack/utils/patch.py +2 -1
- localstack/utils/run.py +10 -9
- localstack/utils/scheduler.py +11 -11
- localstack/utils/server/tcp_proxy.py +2 -2
- localstack/utils/serving.py +2 -3
- localstack/utils/strings.py +10 -11
- localstack/utils/sync.py +126 -1
- localstack/utils/tagging.py +1 -4
- localstack/utils/testutil.py +5 -4
- localstack/utils/threads.py +2 -2
- localstack/utils/time.py +11 -3
- localstack/utils/urls.py +1 -3
- localstack/version.py +2 -2
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/METADATA +17 -12
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/RECORD +168 -164
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/entry_points.txt +4 -2
- localstack_core-4.10.1.dev7.dist-info/plux.json +1 -0
- localstack/packages/terraform.py +0 -46
- localstack/services/cloudformation/deploy.html +0 -144
- localstack/services/cloudformation/deploy_ui.py +0 -47
- localstack/services/cloudformation/plugins.py +0 -12
- localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/WHEEL +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/top_level.txt +0 -0
localstack/constants.py
CHANGED
|
@@ -80,6 +80,10 @@ ENV_INTERNAL_TEST_RUN = "LOCALSTACK_INTERNAL_TEST_RUN"
|
|
|
80
80
|
# environment variable name to tag collect metrics during a test run
|
|
81
81
|
ENV_INTERNAL_TEST_COLLECT_METRIC = "LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC"
|
|
82
82
|
|
|
83
|
+
# environment variable name to indicate that metrics should be stored within the container
|
|
84
|
+
ENV_INTERNAL_TEST_STORE_METRICS_IN_LOCALSTACK = "LOCALSTACK_INTERNAL_TEST_METRICS_IN_LOCALSTACK"
|
|
85
|
+
ENV_INTERNAL_TEST_STORE_METRICS_PATH = "LOCALSTACK_INTERNAL_TEST_STORE_METRICS_PATH"
|
|
86
|
+
|
|
83
87
|
# environment variable that flags whether pro was activated. do not use it for security purposes!
|
|
84
88
|
ENV_PRO_ACTIVATED = "PRO_ACTIVATED"
|
|
85
89
|
|
|
@@ -102,32 +106,6 @@ FALSE_STRINGS = ("0", "false", "False")
|
|
|
102
106
|
# strings with valid log levels for LS_LOG
|
|
103
107
|
LOG_LEVELS = ("trace-internal", "trace", "debug", "info", "warn", "error", "warning")
|
|
104
108
|
|
|
105
|
-
# the version of elasticsearch that is pre-seeded into the base image (sync with Dockerfile.base)
|
|
106
|
-
ELASTICSEARCH_DEFAULT_VERSION = "Elasticsearch_7.10"
|
|
107
|
-
# See https://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/aes-supported-plugins.html
|
|
108
|
-
ELASTICSEARCH_PLUGIN_LIST = [
|
|
109
|
-
"analysis-icu",
|
|
110
|
-
"ingest-attachment",
|
|
111
|
-
"analysis-kuromoji",
|
|
112
|
-
"mapper-murmur3",
|
|
113
|
-
"mapper-size",
|
|
114
|
-
"analysis-phonetic",
|
|
115
|
-
"analysis-smartcn",
|
|
116
|
-
"analysis-stempel",
|
|
117
|
-
"analysis-ukrainian",
|
|
118
|
-
]
|
|
119
|
-
# Default ES modules to exclude (save apprx 66MB in the final image)
|
|
120
|
-
ELASTICSEARCH_DELETE_MODULES = ["ingest-geoip"]
|
|
121
|
-
|
|
122
|
-
# the version of opensearch which is used by default
|
|
123
|
-
OPENSEARCH_DEFAULT_VERSION = "OpenSearch_2.11"
|
|
124
|
-
|
|
125
|
-
# See https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-plugins.html
|
|
126
|
-
OPENSEARCH_PLUGIN_LIST = [
|
|
127
|
-
"ingest-attachment",
|
|
128
|
-
"analysis-kuromoji",
|
|
129
|
-
]
|
|
130
|
-
|
|
131
109
|
# API endpoint for analytics events
|
|
132
110
|
API_ENDPOINT = os.environ.get("API_ENDPOINT") or "https://api.localstack.cloud/v1"
|
|
133
111
|
# new analytics API endpoint
|
|
@@ -171,9 +149,6 @@ DEFAULT_DEVELOP_PORT = 5678
|
|
|
171
149
|
DEFAULT_BUCKET_MARKER_LOCAL = "hot-reload"
|
|
172
150
|
LEGACY_DEFAULT_BUCKET_MARKER_LOCAL = "__local__"
|
|
173
151
|
|
|
174
|
-
# user that starts the opensearch process if the current user is root
|
|
175
|
-
OS_USER_OPENSEARCH = "localstack"
|
|
176
|
-
|
|
177
152
|
# output string that indicates that the stack is ready
|
|
178
153
|
READY_MARKER_OUTPUT = "Ready."
|
|
179
154
|
|
|
@@ -32,7 +32,7 @@ def generate_mount_points(
|
|
|
32
32
|
|
|
33
33
|
# container paths
|
|
34
34
|
target_path = "/opt/code/localstack/"
|
|
35
|
-
venv_path = os.path.join(target_path, ".venv", "lib", "python3.
|
|
35
|
+
venv_path = os.path.join(target_path, ".venv", "lib", "python3.13", "site-packages")
|
|
36
36
|
|
|
37
37
|
# Community code
|
|
38
38
|
if pro:
|
localstack/dev/run/paths.py
CHANGED
|
@@ -68,7 +68,7 @@ class ContainerPaths:
|
|
|
68
68
|
"""Important paths in the container"""
|
|
69
69
|
|
|
70
70
|
project_dir: str = "/opt/code/localstack"
|
|
71
|
-
site_packages_target_dir: str = "/opt/code/localstack/.venv/lib/python3.
|
|
71
|
+
site_packages_target_dir: str = "/opt/code/localstack/.venv/lib/python3.13/site-packages"
|
|
72
72
|
docker_entrypoint: str = "/usr/local/bin/docker-entrypoint.sh"
|
|
73
73
|
localstack_supervisor: str = "/usr/local/bin/localstack-supervisor"
|
|
74
74
|
localstack_source_dir: str
|
localstack/dns/plugins.py
CHANGED
|
@@ -11,8 +11,12 @@ DNS_SHUTDOWN_PRIORITY = -30
|
|
|
11
11
|
"""Make sure the DNS server is shut down after the ON_AFTER_SERVICE_SHUTDOWN_HANDLERS, which in turn is after
|
|
12
12
|
SERVICE_SHUTDOWN_PRIORITY. Currently this value needs to be less than -20"""
|
|
13
13
|
|
|
14
|
+
DNS_START_PRIORITY = 20
|
|
15
|
+
"""Make sure the DNS server is started before the pro activation, to ensure proper DNS resolution for the activate call,
|
|
16
|
+
if the resolv.conf is set to localhost from outside the container"""
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
|
|
19
|
+
@hooks.on_infra_start(priority=DNS_START_PRIORITY)
|
|
16
20
|
def start_dns_server():
|
|
17
21
|
try:
|
|
18
22
|
from localstack.dns import server
|
localstack/dns/server.py
CHANGED
|
@@ -445,13 +445,22 @@ class Resolver(DnsServerProtocol):
|
|
|
445
445
|
return True
|
|
446
446
|
return False
|
|
447
447
|
|
|
448
|
+
def _find_matching_aliases(self, question: DNSQuestion) -> list[AliasTarget] | None:
|
|
449
|
+
"""
|
|
450
|
+
Find aliases matching the question, supporting wildcards.
|
|
451
|
+
"""
|
|
452
|
+
qlabel = DNSLabel(to_bytes(question.qname))
|
|
453
|
+
qtype = RecordType[QTYPE[question.qtype]]
|
|
454
|
+
for (label, rtype), targets in self.aliases.items():
|
|
455
|
+
if rtype == qtype and qlabel.matchWildcard(label):
|
|
456
|
+
return targets
|
|
457
|
+
return None
|
|
458
|
+
|
|
448
459
|
def _resolve_alias(
|
|
449
460
|
self, request: DNSRecord, reply: DNSRecord, client_address: ClientAddress
|
|
450
461
|
) -> bool:
|
|
451
462
|
if request.q.qtype in (QTYPE.A, QTYPE.AAAA, QTYPE.CNAME):
|
|
452
|
-
|
|
453
|
-
# check if we have aliases defined for our given qname/qtype pair
|
|
454
|
-
if aliases := self.aliases.get(key):
|
|
463
|
+
if aliases := self._find_matching_aliases(request.q):
|
|
455
464
|
for alias in aliases:
|
|
456
465
|
# if there is no health check, or the healthcheck is successful, we will consider this alias
|
|
457
466
|
# take the first alias passing this check
|
localstack/packages/api.py
CHANGED
|
@@ -3,10 +3,11 @@ import functools
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
from collections import defaultdict
|
|
6
|
+
from collections.abc import Callable
|
|
6
7
|
from enum import Enum
|
|
7
8
|
from inspect import getmodule
|
|
8
|
-
from threading import RLock
|
|
9
|
-
from typing import Any,
|
|
9
|
+
from threading import Lock, RLock
|
|
10
|
+
from typing import Any, Generic, ParamSpec, TypeVar
|
|
10
11
|
|
|
11
12
|
from plux import Plugin, PluginManager, PluginSpec # type: ignore
|
|
12
13
|
|
|
@@ -56,7 +57,7 @@ class PackageInstaller(abc.ABC):
|
|
|
56
57
|
multiple versions).
|
|
57
58
|
"""
|
|
58
59
|
|
|
59
|
-
def __init__(self, name: str, version: str, install_lock:
|
|
60
|
+
def __init__(self, name: str, version: str, install_lock: Lock | None = None):
|
|
60
61
|
"""
|
|
61
62
|
:param name: technical package name, f.e. "opensearch"
|
|
62
63
|
:param version: version of the package to install
|
|
@@ -70,7 +71,7 @@ class PackageInstaller(abc.ABC):
|
|
|
70
71
|
self.install_lock = install_lock or RLock()
|
|
71
72
|
self._setup_for_target: dict[InstallTarget, bool] = defaultdict(lambda: False)
|
|
72
73
|
|
|
73
|
-
def install(self, target:
|
|
74
|
+
def install(self, target: InstallTarget | None = None) -> None:
|
|
74
75
|
"""
|
|
75
76
|
Performs the package installation.
|
|
76
77
|
|
|
@@ -210,7 +211,7 @@ class Package(abc.ABC, Generic[T]):
|
|
|
210
211
|
"""
|
|
211
212
|
return self.get_installer(version).get_installed_dir()
|
|
212
213
|
|
|
213
|
-
def install(self, version: str | None = None, target:
|
|
214
|
+
def install(self, version: str | None = None, target: InstallTarget | None = None) -> None:
|
|
214
215
|
"""
|
|
215
216
|
Installs the package in the given version in the preferred target location.
|
|
216
217
|
:param version: version of the package to install. If None, the default version of the package will be used.
|
|
@@ -274,7 +275,7 @@ class MultiPackageInstaller(PackageInstaller):
|
|
|
274
275
|
assert len(package_installer) > 0
|
|
275
276
|
self.package_installer = package_installer
|
|
276
277
|
|
|
277
|
-
def install(self, target:
|
|
278
|
+
def install(self, target: InstallTarget | None = None) -> None:
|
|
278
279
|
"""
|
|
279
280
|
Installs the different packages this installer is composed of.
|
|
280
281
|
|
|
@@ -356,7 +357,7 @@ class PackagesPluginManager(PluginManager[PackagesPlugin]): # type: ignore[misc
|
|
|
356
357
|
)
|
|
357
358
|
|
|
358
359
|
def get_packages(
|
|
359
|
-
self, package_names: list[str], version:
|
|
360
|
+
self, package_names: list[str], version: str | None = None
|
|
360
361
|
) -> list[Package[PackageInstaller]]:
|
|
361
362
|
# Plugin names are unique, but there could be multiple packages with the same name in different scopes
|
|
362
363
|
plugin_specs_per_name = defaultdict(list)
|
|
@@ -390,7 +391,7 @@ T2 = TypeVar("T2")
|
|
|
390
391
|
def package(
|
|
391
392
|
name: str | None = None,
|
|
392
393
|
scope: str = "community",
|
|
393
|
-
should_load:
|
|
394
|
+
should_load: Callable[[], bool] | None = None,
|
|
394
395
|
) -> Callable[[Callable[[], Package[Any] | list[Package[Any]]]], PluginSpec]:
|
|
395
396
|
"""
|
|
396
397
|
Decorator for marking methods that create Package instances as a PackagePlugin.
|
localstack/packages/core.py
CHANGED
|
@@ -4,7 +4,7 @@ import re
|
|
|
4
4
|
from abc import ABC
|
|
5
5
|
from functools import lru_cache
|
|
6
6
|
from sys import version_info
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
9
|
import requests
|
|
10
10
|
|
|
@@ -238,7 +238,7 @@ class NodePackageInstaller(ExecutableInstaller):
|
|
|
238
238
|
self,
|
|
239
239
|
package_name: str,
|
|
240
240
|
version: str,
|
|
241
|
-
package_spec:
|
|
241
|
+
package_spec: str | None = None,
|
|
242
242
|
main_module: str = "main.js",
|
|
243
243
|
):
|
|
244
244
|
"""
|
localstack/packages/plugins.py
CHANGED
|
@@ -5,14 +5,6 @@ from localstack.packages.api import Package, package
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from localstack.packages.ffmpeg import FfmpegPackageInstaller
|
|
7
7
|
from localstack.packages.java import JavaPackageInstaller
|
|
8
|
-
from localstack.packages.terraform import TerraformPackageInstaller
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@package(name="terraform")
|
|
12
|
-
def terraform_package() -> Package["TerraformPackageInstaller"]:
|
|
13
|
-
from .terraform import terraform_package
|
|
14
|
-
|
|
15
|
-
return terraform_package
|
|
16
8
|
|
|
17
9
|
|
|
18
10
|
@package(name="ffmpeg")
|
localstack/runtime/init.py
CHANGED
|
@@ -89,7 +89,7 @@ class ShellScriptRunner(ScriptRunner):
|
|
|
89
89
|
suffixes = [".sh"]
|
|
90
90
|
|
|
91
91
|
def run(self, path: str) -> None:
|
|
92
|
-
exit_code = subprocess.call(args=[]
|
|
92
|
+
exit_code = subprocess.call(args=[path])
|
|
93
93
|
if exit_code != 0:
|
|
94
94
|
raise OSError(f"Script {path} returned a non-zero exit code {exit_code}")
|
|
95
95
|
|
|
@@ -323,8 +323,18 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
323
323
|
tags: MapOfStringToString = None,
|
|
324
324
|
**kwargs,
|
|
325
325
|
) -> ApiKey:
|
|
326
|
+
if name and len(name) > 1024:
|
|
327
|
+
raise BadRequestException("Invalid API Key name, can be at most 1024 characters.")
|
|
328
|
+
if value:
|
|
329
|
+
if len(value) > 128:
|
|
330
|
+
raise BadRequestException("API Key value exceeds maximum size of 128 characters")
|
|
331
|
+
elif len(value) < 20:
|
|
332
|
+
raise BadRequestException("API Key value should be at least 20 characters")
|
|
333
|
+
if description and len(description) > 125000:
|
|
334
|
+
raise BadRequestException("Invalid API Key description specified.")
|
|
326
335
|
api_key = call_moto(context)
|
|
327
|
-
|
|
336
|
+
if name == "":
|
|
337
|
+
api_key.pop("name", None)
|
|
328
338
|
# transform array of stage keys [{'restApiId': '0iscapk09u', 'stageName': 'dev'}] into
|
|
329
339
|
# array of strings ['0iscapk09u/dev']
|
|
330
340
|
stage_keys = api_key.get("stageKeys", [])
|
|
@@ -2054,6 +2064,20 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
2054
2064
|
for integration_response in integration_responses.values():
|
|
2055
2065
|
remove_empty_attributes_from_integration_response(integration_response)
|
|
2056
2066
|
|
|
2067
|
+
if response.get("connectionType") == "VPC_LINK":
|
|
2068
|
+
# FIXME: this is hacky to workaround moto not saving the VPC Link `connectionId`
|
|
2069
|
+
# only do this internal check of Moto if the integration is of VPC_LINK type
|
|
2070
|
+
moto_rest_api = get_moto_rest_api(context=context, rest_api_id=rest_api_id)
|
|
2071
|
+
try:
|
|
2072
|
+
method = moto_rest_api.resources[resource_id].resource_methods[http_method]
|
|
2073
|
+
integration = method.method_integration
|
|
2074
|
+
if connection_id := getattr(integration, "connection_id", None):
|
|
2075
|
+
response["connectionId"] = connection_id
|
|
2076
|
+
|
|
2077
|
+
except (AttributeError, KeyError):
|
|
2078
|
+
# this error should have been caught by `call_moto`
|
|
2079
|
+
pass
|
|
2080
|
+
|
|
2057
2081
|
return response
|
|
2058
2082
|
|
|
2059
2083
|
def put_integration(
|
|
@@ -2108,6 +2132,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
2108
2132
|
moto_request.setdefault("timeoutInMillis", 29000)
|
|
2109
2133
|
if integration_type in (IntegrationType.HTTP, IntegrationType.HTTP_PROXY):
|
|
2110
2134
|
moto_request.setdefault("connectionType", ConnectionType.INTERNET)
|
|
2135
|
+
|
|
2111
2136
|
response = call_moto_with_request(context, moto_request)
|
|
2112
2137
|
remove_empty_attributes_from_integration(integration=response)
|
|
2113
2138
|
|
|
@@ -2115,6 +2140,13 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
2115
2140
|
if integration_type == "MOCK":
|
|
2116
2141
|
response.pop("uri", None)
|
|
2117
2142
|
|
|
2143
|
+
# TODO: moto does not save the connection_id
|
|
2144
|
+
elif moto_request.get("connectionType") == "VPC_LINK":
|
|
2145
|
+
connection_id = moto_request.get("connectionId", "")
|
|
2146
|
+
# attach the connection id to the moto object
|
|
2147
|
+
method.method_integration.connection_id = connection_id
|
|
2148
|
+
response["connectionId"] = connection_id
|
|
2149
|
+
|
|
2118
2150
|
return response
|
|
2119
2151
|
|
|
2120
2152
|
def update_integration(
|
|
@@ -2136,6 +2168,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
2136
2168
|
raise NotFoundException("Invalid Integration identifier specified")
|
|
2137
2169
|
|
|
2138
2170
|
integration = method.method_integration
|
|
2171
|
+
# TODO: validate the patch operations
|
|
2139
2172
|
patch_api_gateway_entity(integration, patch_operations)
|
|
2140
2173
|
|
|
2141
2174
|
# fix data types
|
|
@@ -2144,8 +2177,12 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
2144
2177
|
if skip_verification := (integration.tls_config or {}).get("insecureSkipVerification"):
|
|
2145
2178
|
integration.tls_config["insecureSkipVerification"] = str_to_bool(skip_verification)
|
|
2146
2179
|
|
|
2147
|
-
|
|
2148
|
-
|
|
2180
|
+
response: Integration = integration.to_json()
|
|
2181
|
+
|
|
2182
|
+
if connection_id := getattr(integration, "connection_id", None):
|
|
2183
|
+
response["connectionId"] = connection_id
|
|
2184
|
+
|
|
2185
|
+
return response
|
|
2149
2186
|
|
|
2150
2187
|
def delete_integration(
|
|
2151
2188
|
self,
|
|
@@ -2393,6 +2430,10 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
2393
2430
|
for api_key in api_keys:
|
|
2394
2431
|
api_key.pop("value")
|
|
2395
2432
|
|
|
2433
|
+
if limit is not None:
|
|
2434
|
+
if limit < 1 or limit > 500:
|
|
2435
|
+
limit = None
|
|
2436
|
+
|
|
2396
2437
|
item_list = PaginatedList(api_keys)
|
|
2397
2438
|
|
|
2398
2439
|
def token_generator(item):
|
|
@@ -2417,6 +2458,14 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
2417
2458
|
patch_operations: ListOfPatchOperation = None,
|
|
2418
2459
|
**kwargs,
|
|
2419
2460
|
) -> ApiKey:
|
|
2461
|
+
for patch_op in patch_operations:
|
|
2462
|
+
if patch_op["path"] not in ("/description", "/enabled", "/name", "/customerId"):
|
|
2463
|
+
raise BadRequestException(
|
|
2464
|
+
f"Invalid patch path '{patch_op['path']}' specified for op '{patch_op['op']}'. Must be one of: [/description, /enabled, /name, /customerId]"
|
|
2465
|
+
)
|
|
2466
|
+
|
|
2467
|
+
if patch_op["path"] == "/description" and len(patch_op["value"]) > 125000:
|
|
2468
|
+
raise BadRequestException("Invalid API Key description specified.")
|
|
2420
2469
|
response: ApiKey = call_moto(context)
|
|
2421
2470
|
if "value" in response:
|
|
2422
2471
|
response.pop("value", None)
|
|
@@ -2977,6 +3026,7 @@ def create_custom_context(
|
|
|
2977
3026
|
ctx = create_aws_request_context(
|
|
2978
3027
|
service_name=context.service.service_name,
|
|
2979
3028
|
action=action,
|
|
3029
|
+
protocol=context.service.protocol,
|
|
2980
3030
|
parameters=parameters,
|
|
2981
3031
|
region=context.region,
|
|
2982
3032
|
)
|
|
@@ -211,6 +211,9 @@ class RestApiAwsIntegration(RestApiIntegration):
|
|
|
211
211
|
action = parsed_uri["path"]
|
|
212
212
|
|
|
213
213
|
if target := self.get_action_service_target(service_name, action):
|
|
214
|
+
# TODO: properly implement the auto-`Content-Type` headers depending on the service protocol
|
|
215
|
+
# e.g. `x-amz-json-1.0` for DynamoDB
|
|
216
|
+
# this is needed to properly support multi-protocol
|
|
214
217
|
headers["X-Amz-Target"] = target
|
|
215
218
|
|
|
216
219
|
query_params["Action"] = action
|
|
@@ -8,7 +8,7 @@ from werkzeug.datastructures import Headers
|
|
|
8
8
|
from localstack.aws.api.apigateway import Integration
|
|
9
9
|
|
|
10
10
|
from ..context import EndpointResponse, IntegrationRequest, RestApiInvocationContext
|
|
11
|
-
from ..gateway_response import ApiConfigurationError, IntegrationFailureError
|
|
11
|
+
from ..gateway_response import ApiConfigurationError, IntegrationFailureError, InternalServerError
|
|
12
12
|
from ..header_utils import build_multi_value_headers
|
|
13
13
|
from .core import RestApiIntegration
|
|
14
14
|
|
|
@@ -72,7 +72,7 @@ class RestApiHttpIntegration(BaseRestApiHttpIntegration):
|
|
|
72
72
|
except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema) as e:
|
|
73
73
|
LOG.warning("Execution failed due to configuration error: Invalid endpoint address")
|
|
74
74
|
LOG.debug("The URI specified for the HTTP/HTTP_PROXY integration is invalid: %s", uri)
|
|
75
|
-
raise
|
|
75
|
+
raise InternalServerError("Internal server error") from e
|
|
76
76
|
|
|
77
77
|
except (requests.exceptions.Timeout, requests.exceptions.SSLError) as e:
|
|
78
78
|
# TODO make the exception catching more fine grained
|
|
@@ -127,7 +127,7 @@ class RestApiHttpProxyIntegration(BaseRestApiHttpIntegration):
|
|
|
127
127
|
except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema) as e:
|
|
128
128
|
LOG.warning("Execution failed due to configuration error: Invalid endpoint address")
|
|
129
129
|
LOG.debug("The URI specified for the HTTP/HTTP_PROXY integration is invalid: %s", uri)
|
|
130
|
-
raise
|
|
130
|
+
raise InternalServerError("Internal server error") from e
|
|
131
131
|
|
|
132
132
|
except (requests.exceptions.Timeout, requests.exceptions.SSLError):
|
|
133
133
|
# TODO make the exception catching more fine grained
|
|
@@ -62,6 +62,17 @@ TEST_INVOKE_TEMPLATE_MOCK = """Execution log for request {request_id}
|
|
|
62
62
|
{formatted_date} : Method completed with status: {method_response_status}
|
|
63
63
|
"""
|
|
64
64
|
|
|
65
|
+
TEST_INVOKE_TEMPLATE_FAILED = """Execution log for request {request_id}
|
|
66
|
+
{formatted_date} : Starting execution for request: {request_id}
|
|
67
|
+
{formatted_date} : HTTP Method: {request_method}, Resource Path: {resource_path}
|
|
68
|
+
{formatted_date} : Method request path: {method_request_path_parameters}
|
|
69
|
+
{formatted_date} : Method request query string: {method_request_query_string}
|
|
70
|
+
{formatted_date} : Method request headers: {method_request_headers}
|
|
71
|
+
{formatted_date} : Method request body before transformations: {method_request_body}
|
|
72
|
+
{formatted_date} : Execution failed due to {error_type}: {error_message}
|
|
73
|
+
{formatted_date} : Method completed with status: {method_response_status}
|
|
74
|
+
"""
|
|
75
|
+
|
|
65
76
|
|
|
66
77
|
def _dump_headers(headers: Headers) -> str:
|
|
67
78
|
if not headers:
|
|
@@ -80,9 +91,9 @@ def log_template(invocation_context: RestApiInvocationContext, response_headers:
|
|
|
80
91
|
formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
|
|
81
92
|
request = invocation_context.invocation_request
|
|
82
93
|
context_var = invocation_context.context_variables
|
|
83
|
-
integration_req = invocation_context.integration_request
|
|
84
|
-
endpoint_resp = invocation_context.endpoint_response
|
|
85
|
-
method_resp = invocation_context.invocation_response
|
|
94
|
+
integration_req = invocation_context.integration_request or {}
|
|
95
|
+
endpoint_resp = invocation_context.endpoint_response or {}
|
|
96
|
+
method_resp = invocation_context.invocation_response or {}
|
|
86
97
|
# TODO: if endpoint_uri is an ARN, it means it's an AWS_PROXY integration
|
|
87
98
|
# this should be transformed to the true URL of a lambda invoke call
|
|
88
99
|
endpoint_uri = integration_req.get("uri", "")
|
|
@@ -116,7 +127,7 @@ def log_mock_template(
|
|
|
116
127
|
formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
|
|
117
128
|
request = invocation_context.invocation_request
|
|
118
129
|
context_var = invocation_context.context_variables
|
|
119
|
-
method_resp = invocation_context.invocation_response
|
|
130
|
+
method_resp = invocation_context.invocation_response or {}
|
|
120
131
|
|
|
121
132
|
return TEST_INVOKE_TEMPLATE_MOCK.format(
|
|
122
133
|
formatted_date=formatted_date,
|
|
@@ -133,6 +144,29 @@ def log_mock_template(
|
|
|
133
144
|
)
|
|
134
145
|
|
|
135
146
|
|
|
147
|
+
def log_failed_template(
|
|
148
|
+
invocation_context: RestApiInvocationContext, response_status_code: int
|
|
149
|
+
) -> str:
|
|
150
|
+
formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
|
|
151
|
+
request = invocation_context.invocation_request
|
|
152
|
+
context_var = invocation_context.context_variables
|
|
153
|
+
|
|
154
|
+
return TEST_INVOKE_TEMPLATE_FAILED.format(
|
|
155
|
+
formatted_date=formatted_date,
|
|
156
|
+
request_id=context_var["requestId"],
|
|
157
|
+
resource_path=request["path"],
|
|
158
|
+
request_method=request["http_method"],
|
|
159
|
+
method_request_path_parameters=dict_to_string(request["path_parameters"]),
|
|
160
|
+
method_request_query_string=dict_to_string(request["query_string_parameters"]),
|
|
161
|
+
method_request_headers=_dump_headers(request.get("headers")),
|
|
162
|
+
method_request_body=to_str(request.get("body", "")),
|
|
163
|
+
method_response_status=response_status_code,
|
|
164
|
+
# TODO: fix the error message
|
|
165
|
+
error_type="",
|
|
166
|
+
error_message="",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
136
170
|
def create_test_chain() -> HandlerChain[RestApiInvocationContext]:
|
|
137
171
|
return HandlerChain(
|
|
138
172
|
request_handlers=[
|
|
@@ -216,7 +250,9 @@ def create_test_invocation_context(
|
|
|
216
250
|
responseOverride=ContextVarsResponseOverride(header={}, status=0),
|
|
217
251
|
)
|
|
218
252
|
invocation_context.trace_id = parse_handler.populate_trace_id({})
|
|
219
|
-
resource_method =
|
|
253
|
+
resource_method = (
|
|
254
|
+
resource["resourceMethods"].get(http_method) or resource["resourceMethods"]["ANY"]
|
|
255
|
+
)
|
|
220
256
|
invocation_context.resource = resource
|
|
221
257
|
invocation_context.resource_method = resource_method
|
|
222
258
|
invocation_context.integration = resource_method["methodIntegration"]
|
|
@@ -256,7 +292,15 @@ def run_test_invocation(
|
|
|
256
292
|
# AWS does not return the Content-Length for TestInvokeMethod
|
|
257
293
|
response_headers.remove("Content-Length")
|
|
258
294
|
|
|
259
|
-
if
|
|
295
|
+
if not invocation_context.invocation_response:
|
|
296
|
+
# TODO: this is an heuristic to guess if we encounter an exception in the call
|
|
297
|
+
# in the future, we should attach the exception to the context so we could act on it and properly
|
|
298
|
+
# log as we go through the invocation, so that if we have an error we stop logging at the right moment
|
|
299
|
+
for header in ("Content-Type", "X-Amzn-Trace-Id"):
|
|
300
|
+
response_headers.remove(header)
|
|
301
|
+
log = log_failed_template(invocation_context, test_response.status_code)
|
|
302
|
+
|
|
303
|
+
elif is_mock_integration:
|
|
260
304
|
# TODO: revisit how we're building the logs
|
|
261
305
|
log = log_mock_template(invocation_context, response_headers)
|
|
262
306
|
else:
|
|
@@ -429,6 +429,11 @@ class ApigatewayNextGenProvider(ApigatewayProvider):
|
|
|
429
429
|
if not resource:
|
|
430
430
|
raise NotFoundException("Invalid Resource identifier specified")
|
|
431
431
|
|
|
432
|
+
resource_methods = resource.resource_methods
|
|
433
|
+
|
|
434
|
+
if request["httpMethod"] not in resource_methods and "ANY" not in resource_methods:
|
|
435
|
+
raise NotFoundException("Invalid Method identifier specified")
|
|
436
|
+
|
|
432
437
|
# test httpMethod
|
|
433
438
|
|
|
434
439
|
rest_api_container = get_rest_api_container(context, rest_api_id=rest_api_id)
|
|
@@ -14,7 +14,13 @@ from localstack.services.cloudformation.engine.v2.change_set_model import (
|
|
|
14
14
|
)
|
|
15
15
|
from localstack.utils.aws import arns
|
|
16
16
|
from localstack.utils.collections import select_attributes
|
|
17
|
-
from localstack.utils.id_generator import
|
|
17
|
+
from localstack.utils.id_generator import (
|
|
18
|
+
ExistingIds,
|
|
19
|
+
ResourceIdentifier,
|
|
20
|
+
Tags,
|
|
21
|
+
generate_short_uid,
|
|
22
|
+
generate_uid,
|
|
23
|
+
)
|
|
18
24
|
from localstack.utils.json import clone_safe
|
|
19
25
|
from localstack.utils.objects import recurse_object
|
|
20
26
|
from localstack.utils.strings import long_uid, short_uid
|
|
@@ -75,6 +81,11 @@ class StackIdentifier(ResourceIdentifier):
|
|
|
75
81
|
return generate_short_uid(resource_identifier=self, existing_ids=existing_ids, tags=tags)
|
|
76
82
|
|
|
77
83
|
|
|
84
|
+
class StackIdentifierV2(StackIdentifier):
|
|
85
|
+
def generate(self, existing_ids: ExistingIds = None, tags: Tags = None) -> str:
|
|
86
|
+
return generate_uid(resource_identifier=self, existing_ids=existing_ids, tags=tags)
|
|
87
|
+
|
|
88
|
+
|
|
78
89
|
# TODO: remove metadata (flatten into individual fields)
|
|
79
90
|
class Stack:
|
|
80
91
|
change_sets: list["StackChangeSet"]
|
|
@@ -681,9 +681,6 @@ class ChangeSetModel:
|
|
|
681
681
|
scope=arguments_scope, before_value=before_arguments, after_value=after_arguments
|
|
682
682
|
)
|
|
683
683
|
|
|
684
|
-
if intrinsic_function == "Ref" and arguments.value == "AWS::NoValue":
|
|
685
|
-
arguments.value = Nothing
|
|
686
|
-
|
|
687
684
|
if is_created(before=before_arguments, after=after_arguments):
|
|
688
685
|
change_type = ChangeType.CREATED
|
|
689
686
|
elif is_removed(before=before_arguments, after=after_arguments):
|
|
@@ -20,6 +20,7 @@ from localstack.services.cloudformation.engine.v2.change_set_model_preproc impor
|
|
|
20
20
|
PreprocResource,
|
|
21
21
|
)
|
|
22
22
|
from localstack.services.cloudformation.v2.entities import ChangeSet
|
|
23
|
+
from localstack.utils.numbers import is_number
|
|
23
24
|
|
|
24
25
|
CHANGESET_KNOWN_AFTER_APPLY: Final[str] = "{{changeSet:KNOWN_AFTER_APPLY}}"
|
|
25
26
|
|
|
@@ -96,6 +97,19 @@ class ChangeSetModelDescriber(ChangeSetModelPreproc):
|
|
|
96
97
|
|
|
97
98
|
return value
|
|
98
99
|
|
|
100
|
+
def visit_node_intrinsic_function(self, node_intrinsic_function: NodeIntrinsicFunction):
|
|
101
|
+
"""
|
|
102
|
+
Intrinsic function results are always strings when referring to the describe output
|
|
103
|
+
"""
|
|
104
|
+
# TODO: what about other places?
|
|
105
|
+
# TODO: should this be put in the preproc?
|
|
106
|
+
delta = super().visit_node_intrinsic_function(node_intrinsic_function)
|
|
107
|
+
if is_number(delta.before):
|
|
108
|
+
delta.before = str(delta.before)
|
|
109
|
+
if is_number(delta.after):
|
|
110
|
+
delta.after = str(delta.after)
|
|
111
|
+
return delta
|
|
112
|
+
|
|
99
113
|
def visit_node_intrinsic_function_fn_join(
|
|
100
114
|
self, node_intrinsic_function: NodeIntrinsicFunction
|
|
101
115
|
) -> PreprocEntityDelta:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import logging
|
|
3
|
+
import os
|
|
3
4
|
import re
|
|
4
5
|
import uuid
|
|
5
6
|
from collections.abc import Callable
|
|
@@ -105,14 +106,15 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
|
|
|
105
106
|
except Exception as e:
|
|
106
107
|
failure_message = str(e)
|
|
107
108
|
|
|
109
|
+
is_deletion = self._change_set.stack.status == StackStatus.DELETE_IN_PROGRESS
|
|
108
110
|
if self._deferred_actions:
|
|
109
|
-
if
|
|
110
|
-
# TODO: differentiate between update and create
|
|
111
|
-
self._change_set.stack.set_stack_status(StackStatus.ROLLBACK_IN_PROGRESS)
|
|
112
|
-
else:
|
|
111
|
+
if not is_deletion:
|
|
113
112
|
# TODO: correct status
|
|
113
|
+
# TODO: differentiate between update and create
|
|
114
114
|
self._change_set.stack.set_stack_status(
|
|
115
|
-
StackStatus.
|
|
115
|
+
StackStatus.ROLLBACK_IN_PROGRESS
|
|
116
|
+
if failure_message
|
|
117
|
+
else StackStatus.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
|
|
116
118
|
)
|
|
117
119
|
|
|
118
120
|
# perform all deferred actions such as deletions. These must happen in reverse from their
|
|
@@ -122,7 +124,7 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
|
|
|
122
124
|
LOG.debug("executing deferred action: '%s'", deferred.name)
|
|
123
125
|
deferred.action()
|
|
124
126
|
|
|
125
|
-
if failure_message:
|
|
127
|
+
if failure_message and not is_deletion:
|
|
126
128
|
# TODO: differentiate between update and create
|
|
127
129
|
self._change_set.stack.set_stack_status(StackStatus.ROLLBACK_COMPLETE)
|
|
128
130
|
|
|
@@ -515,14 +517,11 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
|
|
|
515
517
|
resource_type,
|
|
516
518
|
f'No resource provider found for "{resource_type}"',
|
|
517
519
|
)
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
"Deployment of resource type %s will fail in upcoming LocalStack releases unless CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES is explicitly enabled.",
|
|
524
|
-
resource_type,
|
|
525
|
-
)
|
|
520
|
+
if "CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES" not in os.environ:
|
|
521
|
+
LOG.warning(
|
|
522
|
+
"Deployment of resource type %s succeeded, but will fail in upcoming LocalStack releases unless CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES is explicitly enabled.",
|
|
523
|
+
resource_type,
|
|
524
|
+
)
|
|
526
525
|
event = ProgressEvent(
|
|
527
526
|
OperationStatus.SUCCESS,
|
|
528
527
|
resource_model={},
|
|
@@ -577,7 +576,6 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
|
|
|
577
576
|
)
|
|
578
577
|
# TODO: do we actually need this line?
|
|
579
578
|
resolved_resource.update(extra_resource_properties)
|
|
580
|
-
|
|
581
579
|
case OperationStatus.FAILED:
|
|
582
580
|
reason = event.message
|
|
583
581
|
LOG.warning(
|