localstack-core 4.7.1.dev49__py3-none-any.whl → 4.10.1.dev12__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 +18 -4
- localstack/aws/api/cloudwatch/__init__.py +41 -1
- localstack/aws/api/config/__init__.py +4 -0
- localstack/aws/api/core.py +6 -2
- localstack/aws/api/dynamodb/__init__.py +30 -0
- localstack/aws/api/ec2/__init__.py +1522 -65
- 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 +5 -0
- localstack/aws/api/s3/__init__.py +12 -0
- localstack/aws/api/s3control/__init__.py +54 -0
- localstack/aws/api/ssm/__init__.py +2 -0
- localstack/aws/api/transcribe/__init__.py +17 -0
- localstack/aws/client.py +7 -2
- localstack/aws/forwarder.py +52 -5
- localstack/aws/handlers/analytics.py +1 -1
- localstack/aws/handlers/internal_requests.py +6 -1
- localstack/aws/handlers/logging.py +12 -2
- localstack/aws/handlers/metric_handler.py +41 -1
- localstack/aws/handlers/service.py +40 -20
- localstack/aws/mocking.py +2 -2
- localstack/aws/patches.py +2 -2
- localstack/aws/protocol/parser.py +459 -32
- localstack/aws/protocol/serializer.py +689 -69
- localstack/aws/protocol/service_router.py +120 -20
- localstack/aws/protocol/validate.py +1 -1
- localstack/aws/scaffold.py +1 -1
- localstack/aws/skeleton.py +4 -2
- localstack/aws/spec-patches.json +58 -0
- localstack/aws/spec.py +37 -16
- localstack/cli/exceptions.py +1 -1
- localstack/cli/localstack.py +6 -6
- localstack/cli/lpm.py +3 -4
- localstack/cli/plugins.py +1 -1
- localstack/cli/profiles.py +1 -2
- localstack/config.py +25 -18
- localstack/constants.py +4 -29
- localstack/dev/kubernetes/__main__.py +130 -7
- localstack/dev/run/configurators.py +1 -4
- localstack/dev/run/paths.py +1 -1
- localstack/dns/plugins.py +5 -1
- localstack/dns/server.py +13 -4
- localstack/logging/format.py +3 -3
- localstack/packages/api.py +9 -8
- localstack/packages/core.py +2 -2
- localstack/packages/plugins.py +0 -8
- localstack/runtime/analytics.py +3 -0
- localstack/runtime/hooks.py +1 -1
- localstack/runtime/init.py +2 -2
- localstack/runtime/main.py +5 -5
- localstack/runtime/patches.py +2 -2
- localstack/services/apigateway/helpers.py +1 -4
- localstack/services/apigateway/legacy/helpers.py +7 -8
- localstack/services/apigateway/legacy/integration.py +4 -3
- localstack/services/apigateway/legacy/invocations.py +6 -5
- localstack/services/apigateway/legacy/provider.py +148 -68
- localstack/services/apigateway/legacy/templates.py +1 -1
- localstack/services/apigateway/next_gen/execute_api/handlers/method_request.py +7 -2
- localstack/services/apigateway/next_gen/execute_api/handlers/resource_router.py +1 -2
- 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/template_mapping.py +2 -2
- localstack/services/apigateway/next_gen/execute_api/test_invoke.py +114 -9
- localstack/services/apigateway/next_gen/provider.py +5 -0
- localstack/services/apigateway/resource_providers/aws_apigateway_resource.py +1 -1
- localstack/services/cloudformation/api_utils.py +4 -8
- localstack/services/cloudformation/cfn_utils.py +1 -1
- localstack/services/cloudformation/engine/entities.py +14 -4
- localstack/services/cloudformation/engine/template_deployer.py +6 -4
- localstack/services/cloudformation/engine/transformers.py +6 -4
- localstack/services/cloudformation/engine/v2/change_set_model.py +201 -13
- localstack/services/cloudformation/engine/v2/change_set_model_describer.py +52 -3
- localstack/services/cloudformation/engine/v2/change_set_model_executor.py +117 -76
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +205 -52
- localstack/services/cloudformation/engine/v2/change_set_model_transform.py +350 -116
- localstack/services/cloudformation/engine/v2/change_set_model_validator.py +56 -14
- localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
- localstack/services/cloudformation/engine/v2/resolving.py +7 -5
- localstack/services/cloudformation/engine/yaml_parser.py +9 -2
- localstack/services/cloudformation/provider.py +7 -5
- localstack/services/cloudformation/resource_provider.py +7 -1
- localstack/services/cloudformation/resources.py +24149 -0
- localstack/services/cloudformation/service_models.py +2 -2
- localstack/services/cloudformation/v2/entities.py +19 -9
- localstack/services/cloudformation/v2/provider.py +336 -106
- localstack/services/cloudformation/v2/types.py +13 -7
- localstack/services/cloudformation/v2/utils.py +4 -1
- localstack/services/cloudwatch/alarm_scheduler.py +4 -1
- localstack/services/cloudwatch/provider.py +18 -13
- 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/server.py +2 -2
- localstack/services/dynamodb/v2/provider.py +42 -0
- localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
- localstack/services/edge.py +1 -1
- 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/provider.py +17 -14
- localstack/services/events/target.py +17 -9
- localstack/services/events/v1/provider.py +5 -5
- localstack/services/firehose/provider.py +14 -4
- 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 +86 -3
- localstack/services/kms/provider.py +14 -5
- localstack/services/lambda_/api_utils.py +6 -3
- localstack/services/lambda_/invocation/docker_runtime_executor.py +1 -1
- localstack/services/lambda_/invocation/event_manager.py +1 -1
- localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
- localstack/services/lambda_/invocation/lambda_models.py +10 -7
- localstack/services/lambda_/invocation/lambda_service.py +5 -1
- localstack/services/lambda_/packages.py +1 -1
- localstack/services/lambda_/provider.py +4 -3
- localstack/services/lambda_/provider_utils.py +1 -1
- localstack/services/logs/provider.py +36 -19
- 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 +8 -2
- localstack/services/opensearch/versions.py +56 -7
- localstack/services/plugins.py +11 -7
- localstack/services/providers.py +10 -2
- localstack/services/redshift/provider.py +0 -21
- localstack/services/s3/constants.py +5 -2
- localstack/services/s3/cors.py +4 -4
- localstack/services/s3/models.py +1 -1
- localstack/services/s3/notifications.py +55 -39
- localstack/services/s3/presigned_url.py +35 -54
- localstack/services/s3/provider.py +73 -15
- localstack/services/s3/utils.py +42 -22
- localstack/services/s3/validation.py +46 -32
- localstack/services/s3/website_hosting.py +4 -2
- localstack/services/ses/provider.py +18 -8
- localstack/services/sns/constants.py +7 -1
- localstack/services/sns/executor.py +9 -2
- localstack/services/sns/provider.py +8 -5
- localstack/services/sns/publisher.py +31 -16
- localstack/services/sns/v2/models.py +167 -0
- localstack/services/sns/v2/provider.py +867 -0
- localstack/services/sns/v2/utils.py +130 -0
- localstack/services/sqs/constants.py +1 -1
- localstack/services/sqs/developer_api.py +205 -0
- localstack/services/sqs/models.py +48 -5
- localstack/services/sqs/provider.py +38 -311
- localstack/services/sqs/query_api.py +6 -2
- localstack/services/sqs/utils.py +121 -2
- localstack/services/ssm/provider.py +1 -1
- localstack/services/stepfunctions/asl/component/intrinsic/member.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_choice/comparison/comparison.py +5 -11
- localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_execution/state_parallel/state_parallel.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_succeed/state_succeed.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_wait/state_wait.py +1 -1
- localstack/services/stepfunctions/asl/eval/environment.py +1 -1
- localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
- localstack/services/stepfunctions/backend/execution.py +2 -1
- localstack/services/stores.py +1 -1
- localstack/services/transcribe/provider.py +6 -1
- localstack/state/codecs.py +61 -0
- localstack/state/core.py +11 -5
- localstack/state/pickle.py +10 -49
- localstack/testing/aws/cloudformation_utils.py +1 -1
- localstack/testing/pytest/cloudformation/fixtures.py +3 -3
- localstack/testing/pytest/cloudformation/transformers.py +0 -0
- localstack/testing/pytest/container.py +4 -5
- localstack/testing/pytest/fixtures.py +33 -31
- localstack/testing/pytest/in_memory_localstack.py +0 -4
- localstack/testing/pytest/marking.py +38 -11
- 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 +6 -1
- localstack/utils/analytics/events.py +2 -2
- localstack/utils/analytics/metadata.py +6 -4
- localstack/utils/analytics/metrics/counter.py +8 -15
- localstack/utils/analytics/publisher.py +1 -2
- localstack/utils/analytics/service_providers.py +19 -0
- localstack/utils/analytics/service_request_aggregator.py +2 -2
- localstack/utils/archives.py +11 -11
- localstack/utils/asyncio.py +2 -2
- localstack/utils/aws/arns.py +24 -29
- localstack/utils/aws/aws_responses.py +8 -8
- localstack/utils/aws/aws_stack.py +2 -3
- localstack/utils/aws/dead_letter_queue.py +1 -5
- localstack/utils/aws/message_forwarding.py +1 -2
- localstack/utils/aws/request_context.py +4 -5
- localstack/utils/aws/resources.py +1 -1
- localstack/utils/aws/templating.py +1 -1
- localstack/utils/batch_policy.py +3 -3
- localstack/utils/bootstrap.py +21 -13
- localstack/utils/catalog/catalog.py +139 -0
- localstack/utils/catalog/catalog_loader.py +119 -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 +135 -136
- localstack/utils/container_utils/docker_cmd_client.py +85 -69
- localstack/utils/container_utils/docker_sdk_client.py +69 -66
- localstack/utils/crypto.py +10 -10
- localstack/utils/diagnose.py +3 -4
- localstack/utils/docker_utils.py +9 -5
- localstack/utils/files.py +33 -13
- localstack/utils/functions.py +4 -3
- localstack/utils/http.py +11 -11
- localstack/utils/json.py +20 -6
- localstack/utils/kinesis/kinesis_connector.py +2 -1
- localstack/utils/net.py +15 -9
- localstack/utils/no_exit_argument_parser.py +2 -2
- localstack/utils/numbers.py +9 -2
- localstack/utils/objects.py +7 -6
- localstack/utils/patch.py +10 -3
- localstack/utils/run.py +12 -11
- localstack/utils/scheduler.py +11 -11
- localstack/utils/server/tcp_proxy.py +2 -2
- localstack/utils/serving.py +3 -4
- localstack/utils/strings.py +15 -16
- localstack/utils/sync.py +126 -1
- localstack/utils/tagging.py +8 -6
- localstack/utils/testutil.py +8 -8
- localstack/utils/threads.py +2 -2
- localstack/utils/time.py +12 -4
- localstack/utils/urls.py +1 -3
- localstack/utils/xray/traceid.py +1 -1
- localstack/version.py +16 -3
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/METADATA +18 -14
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/RECORD +248 -239
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/entry_points.txt +8 -4
- localstack_core-4.10.1.dev12.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.dev49.dist-info/plux.json +0 -1
- {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack +0 -0
- {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/WHEEL +0 -0
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/top_level.txt +0 -0
|
@@ -26,6 +26,7 @@ from localstack.aws.api.opensearch import (
|
|
|
26
26
|
CognitoOptions,
|
|
27
27
|
CognitoOptionsStatus,
|
|
28
28
|
ColdStorageOptions,
|
|
29
|
+
CompatibleVersionsMap,
|
|
29
30
|
CreateDomainRequest,
|
|
30
31
|
CreateDomainResponse,
|
|
31
32
|
DeleteDomainResponse,
|
|
@@ -75,7 +76,6 @@ from localstack.aws.api.opensearch import (
|
|
|
75
76
|
VolumeType,
|
|
76
77
|
VPCDerivedInfoStatus,
|
|
77
78
|
)
|
|
78
|
-
from localstack.constants import OPENSEARCH_DEFAULT_VERSION
|
|
79
79
|
from localstack.services.opensearch import versions
|
|
80
80
|
from localstack.services.opensearch.cluster import SecurityOptions
|
|
81
81
|
from localstack.services.opensearch.cluster_manager import (
|
|
@@ -84,6 +84,7 @@ from localstack.services.opensearch.cluster_manager import (
|
|
|
84
84
|
create_cluster_manager,
|
|
85
85
|
)
|
|
86
86
|
from localstack.services.opensearch.models import OpenSearchStore, opensearch_stores
|
|
87
|
+
from localstack.services.opensearch.packages import OPENSEARCH_DEFAULT_VERSION
|
|
87
88
|
from localstack.services.plugins import ServiceLifecycleHook
|
|
88
89
|
from localstack.state import AssetDirectory, StateVisitor
|
|
89
90
|
from localstack.utils.aws.arns import parse_arn
|
|
@@ -466,10 +467,11 @@ class OpensearchProvider(OpensearchApi, ServiceLifecycleHook):
|
|
|
466
467
|
preferred_port=preferred_port,
|
|
467
468
|
)
|
|
468
469
|
except Exception:
|
|
469
|
-
LOG.
|
|
470
|
+
LOG.error(
|
|
470
471
|
"Could not restore domain %s in region %s.",
|
|
471
472
|
domain_name,
|
|
472
473
|
region,
|
|
474
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
473
475
|
)
|
|
474
476
|
|
|
475
477
|
def on_before_state_reset(self):
|
|
@@ -649,6 +651,10 @@ class OpensearchProvider(OpensearchApi, ServiceLifecycleHook):
|
|
|
649
651
|
for comp in versions.compatible_versions
|
|
650
652
|
if comp["SourceVersion"] == version_filter
|
|
651
653
|
]
|
|
654
|
+
if not compatible_versions:
|
|
655
|
+
compatible_versions = [
|
|
656
|
+
CompatibleVersionsMap(SourceVersion=version_filter, TargetVersions=[])
|
|
657
|
+
]
|
|
652
658
|
return GetCompatibleVersionsResponse(CompatibleVersions=compatible_versions)
|
|
653
659
|
|
|
654
660
|
def describe_domain_config(
|
|
@@ -13,20 +13,24 @@ from localstack.utils.common import get_arch
|
|
|
13
13
|
|
|
14
14
|
# Internal representation of the OpenSearch versions (without the "OpenSearch_" prefix)
|
|
15
15
|
_opensearch_install_versions = {
|
|
16
|
+
"3.1": "3.1.0",
|
|
17
|
+
"2.19": "2.19.3",
|
|
18
|
+
"2.17": "2.17.1",
|
|
19
|
+
"2.15": "2.15.0",
|
|
16
20
|
"2.13": "2.13.0",
|
|
17
21
|
"2.11": "2.11.1",
|
|
18
22
|
"2.9": "2.9.0",
|
|
19
23
|
"2.7": "2.7.0",
|
|
20
24
|
"2.5": "2.5.0",
|
|
21
25
|
"2.3": "2.3.0",
|
|
22
|
-
"1.3": "1.3.
|
|
26
|
+
"1.3": "1.3.20",
|
|
23
27
|
"1.2": "1.2.4",
|
|
24
28
|
"1.1": "1.1.0",
|
|
25
29
|
"1.0": "1.0.0",
|
|
26
30
|
}
|
|
27
31
|
# Internal representation of the Elasticsearch versions (without the "Elasticsearch_" prefix)
|
|
28
32
|
_elasticsearch_install_versions = {
|
|
29
|
-
"7.10": "7.10.
|
|
33
|
+
"7.10": "7.10.2",
|
|
30
34
|
"7.9": "7.9.3",
|
|
31
35
|
"7.8": "7.8.1",
|
|
32
36
|
"7.7": "7.7.1",
|
|
@@ -221,6 +225,9 @@ compatible_versions = [
|
|
|
221
225
|
"OpenSearch_2.9",
|
|
222
226
|
"OpenSearch_2.11",
|
|
223
227
|
"OpenSearch_2.13",
|
|
228
|
+
"OpenSearch_2.15",
|
|
229
|
+
"OpenSearch_2.17",
|
|
230
|
+
"OpenSearch_2.19",
|
|
224
231
|
],
|
|
225
232
|
),
|
|
226
233
|
CompatibleVersionsMap(
|
|
@@ -231,28 +238,68 @@ compatible_versions = [
|
|
|
231
238
|
"OpenSearch_2.9",
|
|
232
239
|
"OpenSearch_2.11",
|
|
233
240
|
"OpenSearch_2.13",
|
|
241
|
+
"OpenSearch_2.15",
|
|
242
|
+
"OpenSearch_2.17",
|
|
243
|
+
"OpenSearch_2.19",
|
|
234
244
|
],
|
|
235
245
|
),
|
|
236
246
|
CompatibleVersionsMap(
|
|
237
247
|
SourceVersion="OpenSearch_2.5",
|
|
238
|
-
TargetVersions=[
|
|
248
|
+
TargetVersions=[
|
|
249
|
+
"OpenSearch_2.7",
|
|
250
|
+
"OpenSearch_2.9",
|
|
251
|
+
"OpenSearch_2.11",
|
|
252
|
+
"OpenSearch_2.13",
|
|
253
|
+
"OpenSearch_2.15",
|
|
254
|
+
"OpenSearch_2.17",
|
|
255
|
+
"OpenSearch_2.19",
|
|
256
|
+
],
|
|
239
257
|
),
|
|
240
258
|
CompatibleVersionsMap(
|
|
241
259
|
SourceVersion="OpenSearch_2.7",
|
|
242
|
-
TargetVersions=[
|
|
260
|
+
TargetVersions=[
|
|
261
|
+
"OpenSearch_2.9",
|
|
262
|
+
"OpenSearch_2.11",
|
|
263
|
+
"OpenSearch_2.13",
|
|
264
|
+
"OpenSearch_2.15",
|
|
265
|
+
"OpenSearch_2.17",
|
|
266
|
+
"OpenSearch_2.19",
|
|
267
|
+
],
|
|
243
268
|
),
|
|
244
269
|
CompatibleVersionsMap(
|
|
245
270
|
SourceVersion="OpenSearch_2.9",
|
|
246
|
-
TargetVersions=[
|
|
271
|
+
TargetVersions=[
|
|
272
|
+
"OpenSearch_2.11",
|
|
273
|
+
"OpenSearch_2.13",
|
|
274
|
+
"OpenSearch_2.15",
|
|
275
|
+
"OpenSearch_2.17",
|
|
276
|
+
"OpenSearch_2.19",
|
|
277
|
+
],
|
|
247
278
|
),
|
|
248
279
|
CompatibleVersionsMap(
|
|
249
280
|
SourceVersion="OpenSearch_2.11",
|
|
250
|
-
TargetVersions=["OpenSearch_2.13"],
|
|
281
|
+
TargetVersions=["OpenSearch_2.13", "OpenSearch_2.15", "OpenSearch_2.17", "OpenSearch_2.19"],
|
|
282
|
+
),
|
|
283
|
+
CompatibleVersionsMap(
|
|
284
|
+
SourceVersion="OpenSearch_2.13",
|
|
285
|
+
TargetVersions=["OpenSearch_2.15", "OpenSearch_2.17", "OpenSearch_2.19"],
|
|
286
|
+
),
|
|
287
|
+
CompatibleVersionsMap(
|
|
288
|
+
SourceVersion="OpenSearch_2.15",
|
|
289
|
+
TargetVersions=["OpenSearch_2.17", "OpenSearch_2.19"],
|
|
290
|
+
),
|
|
291
|
+
CompatibleVersionsMap(
|
|
292
|
+
SourceVersion="OpenSearch_2.17",
|
|
293
|
+
TargetVersions=["OpenSearch_2.19"],
|
|
294
|
+
),
|
|
295
|
+
CompatibleVersionsMap(
|
|
296
|
+
SourceVersion="OpenSearch_2.19",
|
|
297
|
+
TargetVersions=["OpenSearch_3.1"],
|
|
251
298
|
),
|
|
252
299
|
]
|
|
253
300
|
|
|
254
301
|
|
|
255
|
-
def get_install_type_and_version(version: str) ->
|
|
302
|
+
def get_install_type_and_version(version: str) -> tuple[EngineType, str]:
|
|
256
303
|
engine_type = EngineType(version.split("_")[0])
|
|
257
304
|
|
|
258
305
|
if version not in install_versions:
|
|
@@ -297,6 +344,8 @@ def get_download_url(install_version: str, engine_type: EngineType) -> str:
|
|
|
297
344
|
return _opensearch_url(install_version)
|
|
298
345
|
elif engine_type == EngineType.Elasticsearch:
|
|
299
346
|
return _es_url(install_version)
|
|
347
|
+
else:
|
|
348
|
+
raise ValueError(f"Unknown OpenSearch engine type: {engine_type}")
|
|
300
349
|
|
|
301
350
|
|
|
302
351
|
def fetch_latest_versions() -> dict[str, str]: # pragma: no cover
|
localstack/services/plugins.py
CHANGED
|
@@ -286,19 +286,19 @@ class ServiceManager:
|
|
|
286
286
|
container = self.get_service_container(name)
|
|
287
287
|
|
|
288
288
|
if not container:
|
|
289
|
-
raise ValueError("no such service
|
|
289
|
+
raise ValueError(f"no such service {name}")
|
|
290
290
|
|
|
291
291
|
if container.state == ServiceState.STARTING:
|
|
292
292
|
if not poll_condition(lambda: container.state != ServiceState.STARTING, timeout=30):
|
|
293
|
-
raise TimeoutError("gave up waiting for service
|
|
293
|
+
raise TimeoutError(f"gave up waiting for service {name} to start")
|
|
294
294
|
|
|
295
295
|
if container.state == ServiceState.STOPPING:
|
|
296
296
|
if not poll_condition(lambda: container.state == ServiceState.STOPPED, timeout=30):
|
|
297
|
-
raise TimeoutError("gave up waiting for service
|
|
297
|
+
raise TimeoutError(f"gave up waiting for service {name} to stop")
|
|
298
298
|
|
|
299
299
|
with container.lock:
|
|
300
300
|
if container.state == ServiceState.DISABLED:
|
|
301
|
-
raise ServiceDisabled("service
|
|
301
|
+
raise ServiceDisabled(f"service {name} is disabled")
|
|
302
302
|
|
|
303
303
|
if container.state == ServiceState.RUNNING:
|
|
304
304
|
return container.service
|
|
@@ -314,7 +314,7 @@ class ServiceManager:
|
|
|
314
314
|
raise container.errors[-1]
|
|
315
315
|
|
|
316
316
|
raise ServiceStateException(
|
|
317
|
-
"service
|
|
317
|
+
f"service {name} is not ready ({container.state}) and could not be started"
|
|
318
318
|
)
|
|
319
319
|
|
|
320
320
|
# legacy map compatibility
|
|
@@ -692,7 +692,7 @@ def check_service_health(api, expect_shutdown=False):
|
|
|
692
692
|
LOG.warning('Service "%s" not yet available, retrying...', api)
|
|
693
693
|
else:
|
|
694
694
|
LOG.warning('Service "%s" still shutting down, retrying...', api)
|
|
695
|
-
raise Exception("Service check failed for api:
|
|
695
|
+
raise Exception(f"Service check failed for api: {api}")
|
|
696
696
|
|
|
697
697
|
|
|
698
698
|
@hooks.on_infra_start(should_load=lambda: config.EAGER_SERVICE_LOADING)
|
|
@@ -708,4 +708,8 @@ def eager_load_services():
|
|
|
708
708
|
except ServiceDisabled as e:
|
|
709
709
|
LOG.debug("%s", e)
|
|
710
710
|
except Exception:
|
|
711
|
-
LOG.
|
|
711
|
+
LOG.error(
|
|
712
|
+
"could not load service plugin %s",
|
|
713
|
+
api,
|
|
714
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
715
|
+
)
|
localstack/services/providers.py
CHANGED
|
@@ -41,7 +41,7 @@ def apigateway_legacy():
|
|
|
41
41
|
return Service.for_provider(provider, dispatch_table_factory=MotoFallbackDispatcher)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
@aws_provider()
|
|
44
|
+
@aws_provider(api="cloudformation", name="engine-legacy")
|
|
45
45
|
def cloudformation():
|
|
46
46
|
from localstack.services.cloudformation.provider import CloudformationProvider
|
|
47
47
|
|
|
@@ -49,7 +49,7 @@ def cloudformation():
|
|
|
49
49
|
return Service.for_provider(provider)
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
@aws_provider(api="cloudformation"
|
|
52
|
+
@aws_provider(api="cloudformation")
|
|
53
53
|
def cloudformation_v2():
|
|
54
54
|
from localstack.services.cloudformation.v2.provider import CloudformationProviderV2
|
|
55
55
|
|
|
@@ -318,6 +318,14 @@ def sns():
|
|
|
318
318
|
return Service.for_provider(provider, dispatch_table_factory=MotoFallbackDispatcher)
|
|
319
319
|
|
|
320
320
|
|
|
321
|
+
@aws_provider(api="sns", name="v2")
|
|
322
|
+
def sns_v2():
|
|
323
|
+
from localstack.services.sns.v2.provider import SnsProvider
|
|
324
|
+
|
|
325
|
+
provider = SnsProvider()
|
|
326
|
+
return Service.for_provider(provider)
|
|
327
|
+
|
|
328
|
+
|
|
321
329
|
@aws_provider()
|
|
322
330
|
def sqs():
|
|
323
331
|
from localstack.services.sqs.provider import SqsProvider
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
-
from moto.redshift import responses as redshift_responses
|
|
4
3
|
from moto.redshift.models import redshift_backends
|
|
5
4
|
|
|
6
5
|
from localstack import config
|
|
@@ -12,26 +11,6 @@ from localstack.aws.api.redshift import (
|
|
|
12
11
|
)
|
|
13
12
|
from localstack.services.moto import call_moto
|
|
14
13
|
from localstack.state import AssetDirectory, StateVisitor
|
|
15
|
-
from localstack.utils.common import recurse_object
|
|
16
|
-
from localstack.utils.patch import patch
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@patch(redshift_responses.itemize)
|
|
20
|
-
def itemize(fn, data, parent_key=None, *args, **kwargs):
|
|
21
|
-
# TODO: potentially add additional required tags here!
|
|
22
|
-
list_parent_tags = ["ClusterSubnetGroups"]
|
|
23
|
-
|
|
24
|
-
def fix_keys(o, **kwargs):
|
|
25
|
-
if isinstance(o, dict):
|
|
26
|
-
for k, v in o.items():
|
|
27
|
-
if k in list_parent_tags:
|
|
28
|
-
if isinstance(v, dict) and "item" in v:
|
|
29
|
-
v[k[:-1]] = v.pop("item")
|
|
30
|
-
return o
|
|
31
|
-
|
|
32
|
-
result = fn(data, *args, **kwargs)
|
|
33
|
-
recurse_object(result, fix_keys)
|
|
34
|
-
return result
|
|
35
14
|
|
|
36
15
|
|
|
37
16
|
class RedshiftProvider(RedshiftApi):
|
|
@@ -10,8 +10,6 @@ from localstack.aws.api.s3 import (
|
|
|
10
10
|
)
|
|
11
11
|
from localstack.aws.api.s3 import Type as GranteeType
|
|
12
12
|
|
|
13
|
-
S3_VIRTUAL_HOST_FORWARDED_HEADER = "x-s3-vhost-forwarded-for"
|
|
14
|
-
|
|
15
13
|
S3_UPLOAD_PART_MIN_SIZE = 5242880
|
|
16
14
|
"""
|
|
17
15
|
This is minimum size allowed by S3 when uploading more than one part for a Multipart Upload, except for the last part
|
|
@@ -21,6 +19,11 @@ This is minimum size allowed by S3 when uploading more than one part for a Multi
|
|
|
21
19
|
DEFAULT_PRE_SIGNED_ACCESS_KEY_ID = "test"
|
|
22
20
|
DEFAULT_PRE_SIGNED_SECRET_ACCESS_KEY = "test"
|
|
23
21
|
|
|
22
|
+
S3_HOST_ID = "9Gjjt1m+cjU4OPvX9O9/8RuvnG41MRb/18Oux2o5H5MY7ISNTlXN+Dz9IG62/ILVxhAGI0qyPfg="
|
|
23
|
+
"""
|
|
24
|
+
S3 is returning a Host Id as part of its exceptions
|
|
25
|
+
"""
|
|
26
|
+
|
|
24
27
|
AUTHENTICATED_USERS_ACL_GROUP = "http://acs.amazonaws.com/groups/global/AuthenticatedUsers"
|
|
25
28
|
ALL_USERS_ACL_GROUP = "http://acs.amazonaws.com/groups/global/AllUsers"
|
|
26
29
|
LOG_DELIVERY_ACL_GROUP = "http://acs.amazonaws.com/groups/s3/LogDelivery"
|
localstack/services/s3/cors.py
CHANGED
|
@@ -21,13 +21,13 @@ from localstack.aws.protocol.op_router import RestServiceOperationRouter
|
|
|
21
21
|
from localstack.aws.spec import get_service_catalog
|
|
22
22
|
from localstack.config import S3_VIRTUAL_HOSTNAME
|
|
23
23
|
from localstack.http import Request, Response
|
|
24
|
+
from localstack.services.s3.constants import S3_HOST_ID
|
|
24
25
|
from localstack.services.s3.utils import S3_VIRTUAL_HOSTNAME_REGEX
|
|
25
26
|
|
|
26
27
|
# TODO: add more logging statements
|
|
27
28
|
LOG = logging.getLogger(__name__)
|
|
28
29
|
|
|
29
30
|
_s3_virtual_host_regex = re.compile(S3_VIRTUAL_HOSTNAME_REGEX)
|
|
30
|
-
FAKE_HOST_ID = "9Gjjt1m+cjU4OPvX9O9/8RuvnG41MRb/18Oux2o5H5MY7ISNTlXN+Dz9IG62/ILVxhAGI0qyPfg="
|
|
31
31
|
|
|
32
32
|
# TODO: refactor those to expose the needed methods maybe in another way that both can import
|
|
33
33
|
add_default_headers = CorsResponseEnricher.add_cors_headers
|
|
@@ -135,7 +135,7 @@ class S3CorsHandler(Handler):
|
|
|
135
135
|
if is_options_request:
|
|
136
136
|
context.operation = self._get_op_from_request(request)
|
|
137
137
|
raise BadRequest(
|
|
138
|
-
"Insufficient information. Origin request header needed.", HostId=
|
|
138
|
+
"Insufficient information. Origin request header needed.", HostId=S3_HOST_ID
|
|
139
139
|
)
|
|
140
140
|
else:
|
|
141
141
|
# If the header is missing, Amazon S3 doesn't treat the request as a cross-origin request,
|
|
@@ -167,7 +167,7 @@ class S3CorsHandler(Handler):
|
|
|
167
167
|
context.operation = self._get_op_from_request(request)
|
|
168
168
|
raise AccessForbidden(
|
|
169
169
|
message,
|
|
170
|
-
HostId=
|
|
170
|
+
HostId=S3_HOST_ID,
|
|
171
171
|
Method=request.headers.get("Access-Control-Request-Method", "OPTIONS"),
|
|
172
172
|
ResourceType="BUCKET",
|
|
173
173
|
)
|
|
@@ -182,7 +182,7 @@ class S3CorsHandler(Handler):
|
|
|
182
182
|
context.operation = self._get_op_from_request(request)
|
|
183
183
|
raise AccessForbidden(
|
|
184
184
|
"CORSResponse: This CORS request is not allowed. This is usually because the evalution of Origin, request method / Access-Control-Request-Method or Access-Control-Request-Headers are not whitelisted by the resource's CORS spec.",
|
|
185
|
-
HostId=
|
|
185
|
+
HostId=S3_HOST_ID,
|
|
186
186
|
Method=request.headers.get("Access-Control-Request-Method"),
|
|
187
187
|
ResourceType="OBJECT",
|
|
188
188
|
)
|
localstack/services/s3/models.py
CHANGED
|
@@ -638,7 +638,7 @@ class KeyStore:
|
|
|
638
638
|
|
|
639
639
|
def values(self, *_, **__) -> list[S3Object | S3DeleteMarker]:
|
|
640
640
|
# we create a shallow copy with dict to avoid size changed during iteration
|
|
641
|
-
return
|
|
641
|
+
return list(dict(self._store).values())
|
|
642
642
|
|
|
643
643
|
def is_empty(self) -> bool:
|
|
644
644
|
return not self._store
|
|
@@ -21,6 +21,7 @@ from localstack.aws.api.s3 import (
|
|
|
21
21
|
Event,
|
|
22
22
|
EventBridgeConfiguration,
|
|
23
23
|
EventList,
|
|
24
|
+
InvalidArgument,
|
|
24
25
|
LambdaFunctionArn,
|
|
25
26
|
LambdaFunctionConfiguration,
|
|
26
27
|
NotificationConfiguration,
|
|
@@ -34,8 +35,8 @@ from localstack.aws.api.s3 import (
|
|
|
34
35
|
TopicConfiguration,
|
|
35
36
|
)
|
|
36
37
|
from localstack.aws.connect import connect_to
|
|
38
|
+
from localstack.services.s3.exceptions import MalformedXML
|
|
37
39
|
from localstack.services.s3.models import S3Bucket, S3DeleteMarker, S3Object
|
|
38
|
-
from localstack.services.s3.utils import _create_invalid_argument_exc
|
|
39
40
|
from localstack.utils.aws import arns
|
|
40
41
|
from localstack.utils.aws.arns import ARN_PARTITION_REGEX, get_partition, parse_arn, s3_bucket_arn
|
|
41
42
|
from localstack.utils.aws.client_types import ServicePrincipal
|
|
@@ -104,7 +105,7 @@ class S3EventNotificationContext:
|
|
|
104
105
|
key_storage_class: StorageClass | None
|
|
105
106
|
|
|
106
107
|
@classmethod
|
|
107
|
-
def
|
|
108
|
+
def from_request_context(
|
|
108
109
|
cls,
|
|
109
110
|
request_context: RequestContext,
|
|
110
111
|
s3_bucket: S3Bucket,
|
|
@@ -272,26 +273,29 @@ class BaseNotifier:
|
|
|
272
273
|
arn, argument_name = self._get_arn_value_and_name(configuration)
|
|
273
274
|
|
|
274
275
|
if not re.match(f"{ARN_PARTITION_REGEX}:{self.service_name}:", arn):
|
|
275
|
-
raise
|
|
276
|
-
"The ARN could not be parsed",
|
|
276
|
+
raise InvalidArgument(
|
|
277
|
+
"The ARN could not be parsed",
|
|
278
|
+
ArgumentName=argument_name,
|
|
279
|
+
ArgumentValue=arn,
|
|
277
280
|
)
|
|
281
|
+
|
|
278
282
|
if not verification_ctx.skip_destination_validation:
|
|
279
283
|
self._verify_target(arn, verification_ctx)
|
|
280
284
|
|
|
281
285
|
if filter_rules := configuration.get("Filter", {}).get("Key", {}).get("FilterRules"):
|
|
282
286
|
for rule in filter_rules:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
if "Name" not in rule or "Value" not in rule:
|
|
288
|
+
raise MalformedXML()
|
|
289
|
+
|
|
290
|
+
if rule["Name"].lower() not in ["suffix", "prefix"]:
|
|
291
|
+
raise InvalidArgument(
|
|
286
292
|
"filter rule name must be either prefix or suffix",
|
|
287
|
-
|
|
288
|
-
rule["
|
|
289
|
-
)
|
|
290
|
-
if not rule["Value"]:
|
|
291
|
-
raise _create_invalid_argument_exc(
|
|
292
|
-
"filter value cannot be empty", rule["Name"], rule["Value"]
|
|
293
|
+
ArgumentName="FilterRule.Name",
|
|
294
|
+
ArgumentValue=rule["Name"],
|
|
293
295
|
)
|
|
294
296
|
|
|
297
|
+
rule["Name"] = rule["Name"].capitalize()
|
|
298
|
+
|
|
295
299
|
@staticmethod
|
|
296
300
|
def _get_test_payload(verification_ctx: BucketVerificationContext):
|
|
297
301
|
return {
|
|
@@ -382,7 +386,7 @@ class SqsNotifier(BaseNotifier):
|
|
|
382
386
|
|
|
383
387
|
@staticmethod
|
|
384
388
|
def _get_arn_value_and_name(queue_configuration: QueueConfiguration) -> tuple[QueueArn, str]:
|
|
385
|
-
return queue_configuration.get("QueueArn", ""), "
|
|
389
|
+
return queue_configuration.get("QueueArn", ""), "Queue"
|
|
386
390
|
|
|
387
391
|
def _verify_target(self, target_arn: str, verification_ctx: BucketVerificationContext) -> None:
|
|
388
392
|
if not is_api_enabled("sqs"):
|
|
@@ -402,13 +406,20 @@ class SqsNotifier(BaseNotifier):
|
|
|
402
406
|
queue_url = sqs_client.get_queue_url(
|
|
403
407
|
QueueName=arn_data["resource"], QueueOwnerAWSAccountId=arn_data["account"]
|
|
404
408
|
)["QueueUrl"]
|
|
405
|
-
except ClientError:
|
|
406
|
-
|
|
407
|
-
|
|
409
|
+
except ClientError as e:
|
|
410
|
+
code = e.response["Error"]["Code"]
|
|
411
|
+
LOG.error(
|
|
412
|
+
"Could not validate the notification destination %s: %s",
|
|
413
|
+
target_arn,
|
|
414
|
+
code,
|
|
415
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
416
|
+
)
|
|
417
|
+
raise InvalidArgument(
|
|
408
418
|
"Unable to validate the following destination configurations",
|
|
409
|
-
|
|
410
|
-
|
|
419
|
+
ArgumentName=target_arn,
|
|
420
|
+
ArgumentValue="The destination queue does not exist",
|
|
411
421
|
)
|
|
422
|
+
|
|
412
423
|
# send test event with the request metadata for permissions
|
|
413
424
|
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-how-to-event-types-and-destinations.html#supported-notification-event-types
|
|
414
425
|
sqs_client = connect_to(region_name=arn_data["region"]).sqs.request_metadata(
|
|
@@ -424,10 +435,10 @@ class SqsNotifier(BaseNotifier):
|
|
|
424
435
|
verification_ctx.bucket_name,
|
|
425
436
|
target_arn,
|
|
426
437
|
)
|
|
427
|
-
raise
|
|
438
|
+
raise InvalidArgument(
|
|
428
439
|
"Unable to validate the following destination configurations",
|
|
429
|
-
|
|
430
|
-
|
|
440
|
+
ArgumentName=target_arn,
|
|
441
|
+
ArgumentValue="Permissions on the destination queue do not allow S3 to publish notifications from this bucket",
|
|
431
442
|
) from e
|
|
432
443
|
|
|
433
444
|
def notify(self, ctx: S3EventNotificationContext, config: QueueConfiguration):
|
|
@@ -454,10 +465,11 @@ class SqsNotifier(BaseNotifier):
|
|
|
454
465
|
MessageSystemAttributes=system_attributes,
|
|
455
466
|
)
|
|
456
467
|
except Exception:
|
|
457
|
-
LOG.
|
|
468
|
+
LOG.error(
|
|
458
469
|
'Unable to send notification for S3 bucket "%s" to SQS queue "%s"',
|
|
459
470
|
ctx.bucket_name,
|
|
460
471
|
parsed_arn["resource"],
|
|
472
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
461
473
|
)
|
|
462
474
|
|
|
463
475
|
|
|
@@ -483,10 +495,10 @@ class SnsNotifier(BaseNotifier):
|
|
|
483
495
|
try:
|
|
484
496
|
sns_client.get_topic_attributes(TopicArn=target_arn)
|
|
485
497
|
except ClientError:
|
|
486
|
-
raise
|
|
498
|
+
raise InvalidArgument(
|
|
487
499
|
"Unable to validate the following destination configurations",
|
|
488
|
-
|
|
489
|
-
|
|
500
|
+
ArgumentName=target_arn,
|
|
501
|
+
ArgumentValue="The destination topic does not exist",
|
|
490
502
|
)
|
|
491
503
|
|
|
492
504
|
sns_client = connect_to(region_name=arn_data["region"]).sns.request_metadata(
|
|
@@ -506,10 +518,10 @@ class SnsNotifier(BaseNotifier):
|
|
|
506
518
|
verification_ctx.bucket_name,
|
|
507
519
|
target_arn,
|
|
508
520
|
)
|
|
509
|
-
raise
|
|
521
|
+
raise InvalidArgument(
|
|
510
522
|
"Unable to validate the following destination configurations",
|
|
511
|
-
|
|
512
|
-
|
|
523
|
+
ArgumentName=target_arn,
|
|
524
|
+
ArgumentValue="Permissions on the destination topic do not allow S3 to publish notifications from this bucket",
|
|
513
525
|
) from e
|
|
514
526
|
|
|
515
527
|
def notify(self, ctx: S3EventNotificationContext, config: TopicConfiguration):
|
|
@@ -536,10 +548,11 @@ class SnsNotifier(BaseNotifier):
|
|
|
536
548
|
Subject="Amazon S3 Notification",
|
|
537
549
|
)
|
|
538
550
|
except Exception:
|
|
539
|
-
LOG.
|
|
551
|
+
LOG.error(
|
|
540
552
|
'Unable to send notification for S3 bucket "%s" to SNS topic "%s"',
|
|
541
553
|
ctx.bucket_name,
|
|
542
554
|
topic_arn,
|
|
555
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
543
556
|
)
|
|
544
557
|
|
|
545
558
|
|
|
@@ -567,10 +580,10 @@ class LambdaNotifier(BaseNotifier):
|
|
|
567
580
|
try:
|
|
568
581
|
lambda_client.get_function(FunctionName=target_arn)
|
|
569
582
|
except ClientError:
|
|
570
|
-
raise
|
|
583
|
+
raise InvalidArgument(
|
|
571
584
|
"Unable to validate the following destination configurations",
|
|
572
|
-
|
|
573
|
-
|
|
585
|
+
ArgumentName=target_arn,
|
|
586
|
+
ArgumentValue="The destination Lambda does not exist",
|
|
574
587
|
)
|
|
575
588
|
lambda_client = connect_to(region_name=arn_data["region"]).lambda_.request_metadata(
|
|
576
589
|
source_arn=s3_bucket_arn(verification_ctx.bucket_name, region=verification_ctx.region),
|
|
@@ -579,10 +592,10 @@ class LambdaNotifier(BaseNotifier):
|
|
|
579
592
|
try:
|
|
580
593
|
lambda_client.invoke(FunctionName=target_arn, InvocationType=InvocationType.DryRun)
|
|
581
594
|
except ClientError as e:
|
|
582
|
-
raise
|
|
595
|
+
raise InvalidArgument(
|
|
583
596
|
"Unable to validate the following destination configurations",
|
|
584
|
-
|
|
585
|
-
|
|
597
|
+
ArgumentName=f"{target_arn}, null",
|
|
598
|
+
ArgumentValue=f"Not authorized to invoke function [{target_arn}]",
|
|
586
599
|
) from e
|
|
587
600
|
|
|
588
601
|
def notify(self, ctx: S3EventNotificationContext, config: LambdaFunctionConfiguration):
|
|
@@ -604,10 +617,11 @@ class LambdaNotifier(BaseNotifier):
|
|
|
604
617
|
Payload=payload,
|
|
605
618
|
)
|
|
606
619
|
except Exception:
|
|
607
|
-
LOG.
|
|
620
|
+
LOG.error(
|
|
608
621
|
'Unable to send notification for S3 bucket "%s" to Lambda function "%s".',
|
|
609
622
|
ctx.bucket_name,
|
|
610
623
|
lambda_arn,
|
|
624
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
611
625
|
)
|
|
612
626
|
|
|
613
627
|
|
|
@@ -729,8 +743,10 @@ class EventBridgeNotifier(BaseNotifier):
|
|
|
729
743
|
try:
|
|
730
744
|
events_client.put_events(Entries=[entry])
|
|
731
745
|
except Exception:
|
|
732
|
-
LOG.
|
|
733
|
-
'Unable to send notification for S3 bucket "%s" to EventBridge',
|
|
746
|
+
LOG.error(
|
|
747
|
+
'Unable to send notification for S3 bucket "%s" to EventBridge',
|
|
748
|
+
ctx.bucket_name,
|
|
749
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
734
750
|
)
|
|
735
751
|
|
|
736
752
|
|