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.
- 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
|
@@ -10,6 +10,7 @@ from functools import reduce
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Literal, TypedDict, TypeVar
|
|
12
12
|
|
|
13
|
+
import boto3
|
|
13
14
|
import click
|
|
14
15
|
from jinja2 import Environment, FileSystemLoader
|
|
15
16
|
from yaml import safe_dump
|
|
@@ -140,14 +141,76 @@ class SchemaProvider:
|
|
|
140
141
|
) from e
|
|
141
142
|
|
|
142
143
|
|
|
144
|
+
class LiveSchemaProvider:
|
|
145
|
+
"""
|
|
146
|
+
Provides CloudFormation resource schemas by fetching them from the live AWS CloudFormation service, rather than
|
|
147
|
+
a local zip file.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, cfn_client):
|
|
151
|
+
self.cfn_client = cfn_client
|
|
152
|
+
|
|
153
|
+
def available_schemas(self, pattern: str) -> list[str]:
|
|
154
|
+
"""
|
|
155
|
+
Return the names of available CloudFormation resource types. `pattern` should be something like
|
|
156
|
+
AWS::S3::Bucket or AWS::S3::*, depending on whether you want all resources for a service or a specific one.
|
|
157
|
+
The result is a list of matching resource type names (e.g. [AWS::S3::Bucket, AWS::S3::Object, ...])
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
is_wildcard = pattern.endswith("*")
|
|
161
|
+
pattern = pattern[:-1] if is_wildcard else pattern
|
|
162
|
+
matching_names = []
|
|
163
|
+
|
|
164
|
+
params = {
|
|
165
|
+
"Visibility": "PUBLIC",
|
|
166
|
+
"Type": "RESOURCE",
|
|
167
|
+
"DeprecatedStatus": "LIVE",
|
|
168
|
+
"Filters": {"Category": "AWS_TYPES", "TypeNamePrefix": pattern},
|
|
169
|
+
}
|
|
170
|
+
next_token: str | None = None
|
|
171
|
+
|
|
172
|
+
# Note: pagination is necessary since list_types requires multiple calls even to get a single result.
|
|
173
|
+
while True:
|
|
174
|
+
if next_token:
|
|
175
|
+
params["NextToken"] = next_token
|
|
176
|
+
response = self.cfn_client.list_types(**params)
|
|
177
|
+
|
|
178
|
+
# collect any matching type names (if wildcard, all; else exact match only)
|
|
179
|
+
matching_names.extend(
|
|
180
|
+
[
|
|
181
|
+
type_summary["TypeName"]
|
|
182
|
+
for type_summary in response.get("TypeSummaries", [])
|
|
183
|
+
if (is_wildcard or type_summary["TypeName"] == pattern)
|
|
184
|
+
]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
next_token = response.get("NextToken")
|
|
188
|
+
if not next_token:
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
return matching_names
|
|
192
|
+
|
|
193
|
+
def schema(self, type_name: ResourceName) -> ResourceSchema:
|
|
194
|
+
"""
|
|
195
|
+
Given a CloudFormation ResourceName (representing something like "AWS::S3::Bucket"), return the resource
|
|
196
|
+
schema as dict.
|
|
197
|
+
"""
|
|
198
|
+
response = self.cfn_client.describe_type(
|
|
199
|
+
Type="RESOURCE",
|
|
200
|
+
TypeName=type_name.full_name,
|
|
201
|
+
)
|
|
202
|
+
schema_str = response.get("Schema")
|
|
203
|
+
if not schema_str:
|
|
204
|
+
raise click.ClickException(
|
|
205
|
+
f"Could not fetch schema for CloudFormation resource type: {type_name}"
|
|
206
|
+
)
|
|
207
|
+
return json.loads(schema_str)
|
|
208
|
+
|
|
209
|
+
|
|
143
210
|
LOCALSTACK_ROOT_DIR = Path(__file__).parent.joinpath("../../../../..").resolve()
|
|
144
211
|
LOCALSTACK_PRO_ROOT_DIR = LOCALSTACK_ROOT_DIR.joinpath("../localstack-pro").resolve()
|
|
145
|
-
TESTS_ROOT_DIR = LOCALSTACK_ROOT_DIR.joinpath(
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
TESTS_PRO_ROOT_DIR = LOCALSTACK_PRO_ROOT_DIR.joinpath(
|
|
149
|
-
"localstack-pro-core/tests/aws/services/cloudformation/resource_providers"
|
|
150
|
-
)
|
|
212
|
+
TESTS_ROOT_DIR = LOCALSTACK_ROOT_DIR.joinpath("tests/aws/services")
|
|
213
|
+
TESTS_PRO_ROOT_DIR = LOCALSTACK_PRO_ROOT_DIR.joinpath("localstack-pro-core/tests/aws/services")
|
|
151
214
|
|
|
152
215
|
assert LOCALSTACK_ROOT_DIR.is_dir(), f"{LOCALSTACK_ROOT_DIR} does not exist"
|
|
153
216
|
assert LOCALSTACK_PRO_ROOT_DIR.is_dir(), f"{LOCALSTACK_PRO_ROOT_DIR} does not exist"
|
|
@@ -193,7 +256,7 @@ def template_path(
|
|
|
193
256
|
output_path = (
|
|
194
257
|
tests_root_dir(pro)
|
|
195
258
|
.joinpath(
|
|
196
|
-
f"{resource_name.python_compatible_service_name.lower()}/
|
|
259
|
+
f"{resource_name.python_compatible_service_name.lower()}/resource_providers/templates/{stub}"
|
|
197
260
|
)
|
|
198
261
|
.resolve()
|
|
199
262
|
)
|
|
@@ -202,7 +265,7 @@ def template_path(
|
|
|
202
265
|
test_path = (
|
|
203
266
|
root_dir(pro)
|
|
204
267
|
.joinpath(
|
|
205
|
-
f"tests/aws/
|
|
268
|
+
f"tests/aws/{resource_name.python_compatible_service_name.lower()}/resource_providers/templates"
|
|
206
269
|
)
|
|
207
270
|
.resolve()
|
|
208
271
|
)
|
|
@@ -276,7 +339,7 @@ class TemplateRenderer:
|
|
|
276
339
|
# e.g. .../resource_providers/aws_iam_role/test_X.py vs. .../resource_providers/iam/test_X.py
|
|
277
340
|
# add extra parameters
|
|
278
341
|
tests_output_path = root_dir(self.pro).joinpath(
|
|
279
|
-
f"tests/aws/
|
|
342
|
+
f"tests/aws/{resource_name.python_compatible_service_name.lower()}/resource_providers/templates"
|
|
280
343
|
)
|
|
281
344
|
match file_type:
|
|
282
345
|
case FileType.getatt_test:
|
|
@@ -284,7 +347,9 @@ class TemplateRenderer:
|
|
|
284
347
|
kwargs["service"] = resource_name.service.lower()
|
|
285
348
|
kwargs["resource"] = resource_name.resource.lower()
|
|
286
349
|
kwargs["template_path"] = str(
|
|
287
|
-
template_path(
|
|
350
|
+
template_path(
|
|
351
|
+
resource_name, FileType.attribute_template, tests_output_path, pro=self.pro
|
|
352
|
+
)
|
|
288
353
|
)
|
|
289
354
|
case FileType.provider:
|
|
290
355
|
property_ir = generate_ir_for_type(
|
|
@@ -318,17 +383,25 @@ class TemplateRenderer:
|
|
|
318
383
|
kwargs["pro"] = self.pro
|
|
319
384
|
case FileType.integration_test:
|
|
320
385
|
kwargs["black_box_template_path"] = str(
|
|
321
|
-
template_path(
|
|
386
|
+
template_path(
|
|
387
|
+
resource_name, FileType.minimal_template, tests_output_path, pro=self.pro
|
|
388
|
+
)
|
|
322
389
|
)
|
|
323
390
|
kwargs["update_template_path"] = str(
|
|
324
391
|
template_path(
|
|
325
392
|
resource_name,
|
|
326
393
|
FileType.update_without_replacement_template,
|
|
327
394
|
tests_output_path,
|
|
395
|
+
pro=self.pro,
|
|
328
396
|
)
|
|
329
397
|
)
|
|
330
398
|
kwargs["autogenerated_template_path"] = str(
|
|
331
|
-
template_path(
|
|
399
|
+
template_path(
|
|
400
|
+
resource_name,
|
|
401
|
+
FileType.autogenerated_template,
|
|
402
|
+
tests_output_path,
|
|
403
|
+
pro=self.pro,
|
|
404
|
+
)
|
|
332
405
|
)
|
|
333
406
|
# case FileType.cloudcontrol_test:
|
|
334
407
|
case FileType.parity_test:
|
|
@@ -531,20 +604,24 @@ class FileWriter:
|
|
|
531
604
|
),
|
|
532
605
|
FileType.integration_test: tests_root_dir(self.pro).joinpath(
|
|
533
606
|
self.resource_name.python_compatible_service_name.lower(),
|
|
607
|
+
"resource_providers",
|
|
534
608
|
self.resource_name.path_compatible_full_name(),
|
|
535
609
|
"test_basic.py",
|
|
536
610
|
),
|
|
537
611
|
FileType.getatt_test: tests_root_dir(self.pro).joinpath(
|
|
538
612
|
self.resource_name.python_compatible_service_name.lower(),
|
|
613
|
+
"resource_providers",
|
|
539
614
|
self.resource_name.path_compatible_full_name(),
|
|
540
615
|
"test_exploration.py",
|
|
541
616
|
),
|
|
542
617
|
# FileType.cloudcontrol_test: tests_root_dir(self.pro).joinpath(
|
|
543
618
|
# self.resource_name.python_compatible_service_name.lower(),
|
|
619
|
+
# "resource_providers",
|
|
544
620
|
# f"test_aws_{self.resource_name.service.lower()}_{self.resource_name.resource.lower()}_cloudcontrol.py",
|
|
545
621
|
# ),
|
|
546
622
|
FileType.parity_test: tests_root_dir(self.pro).joinpath(
|
|
547
623
|
self.resource_name.python_compatible_service_name.lower(),
|
|
624
|
+
"resource_providers",
|
|
548
625
|
self.resource_name.path_compatible_full_name(),
|
|
549
626
|
"test_parity.py",
|
|
550
627
|
),
|
|
@@ -558,7 +635,9 @@ class FileWriter:
|
|
|
558
635
|
FileType.autogenerated_template,
|
|
559
636
|
]
|
|
560
637
|
for template_type in templates:
|
|
561
|
-
self.destination_files[template_type] = template_path(
|
|
638
|
+
self.destination_files[template_type] = template_path(
|
|
639
|
+
self.resource_name, template_type, pro=self.pro
|
|
640
|
+
)
|
|
562
641
|
|
|
563
642
|
def write(self, file_type: FileType, contents: str):
|
|
564
643
|
file_destination = self.destination_files[file_type]
|
|
@@ -763,21 +842,14 @@ def generate(
|
|
|
763
842
|
console = Console()
|
|
764
843
|
console.rule(title=resource_type)
|
|
765
844
|
|
|
766
|
-
schema_provider =
|
|
767
|
-
zipfile_path=Path(__file__).parent.joinpath("CloudformationSchema.zip")
|
|
768
|
-
)
|
|
845
|
+
schema_provider = LiveSchemaProvider(boto3.client("cloudformation"))
|
|
769
846
|
|
|
770
847
|
template_root = Path(__file__).parent.joinpath("templates")
|
|
771
848
|
env = Environment(
|
|
772
849
|
loader=FileSystemLoader(template_root),
|
|
773
850
|
)
|
|
774
851
|
|
|
775
|
-
|
|
776
|
-
if parts[-1] == "*":
|
|
777
|
-
# generate all resource types for that service
|
|
778
|
-
matching_resources = [x for x in schema_provider.schemas.keys() if x.startswith(parts[0])]
|
|
779
|
-
else:
|
|
780
|
-
matching_resources = [resource_type]
|
|
852
|
+
matching_resources = schema_provider.available_schemas(resource_type)
|
|
781
853
|
|
|
782
854
|
for matching_resource in matching_resources:
|
|
783
855
|
console.rule(title=matching_resource)
|
|
@@ -103,6 +103,9 @@ from localstack.services.cloudformation.engine.v2.change_set_model_transform imp
|
|
|
103
103
|
from localstack.services.cloudformation.engine.v2.change_set_model_validator import (
|
|
104
104
|
ChangeSetModelValidator,
|
|
105
105
|
)
|
|
106
|
+
from localstack.services.cloudformation.engine.v2.change_set_resource_support_checker import (
|
|
107
|
+
ChangeSetResourceSupportChecker,
|
|
108
|
+
)
|
|
106
109
|
from localstack.services.cloudformation.engine.validations import ValidationError
|
|
107
110
|
from localstack.services.cloudformation.provider import (
|
|
108
111
|
ARN_CHANGESET_REGEX,
|
|
@@ -222,6 +225,12 @@ def find_stack_instance(stack_set: StackSet, account: str, region: str) -> Stack
|
|
|
222
225
|
|
|
223
226
|
class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
224
227
|
def on_before_start(self):
|
|
228
|
+
# TODO: make sure to bring `_validate_config` from the base class when removing it
|
|
229
|
+
# as this ensures we have a valid CFN_NO_WAIT_ITERATIONS value
|
|
230
|
+
super().on_before_start()
|
|
231
|
+
self._log_create_issue_info()
|
|
232
|
+
|
|
233
|
+
def _log_create_issue_info(self):
|
|
225
234
|
base = "https://github.com/localstack/localstack/issues/new"
|
|
226
235
|
query_args = {
|
|
227
236
|
"template": "bug-report.yml",
|
|
@@ -417,6 +426,38 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
417
426
|
update_model.node_template.change_type = ChangeType.MODIFIED
|
|
418
427
|
change_set.processed_template = transformed_after_template
|
|
419
428
|
|
|
429
|
+
if not config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
|
|
430
|
+
support_visitor = ChangeSetResourceSupportChecker()
|
|
431
|
+
support_visitor.visit(change_set.update_model.node_template)
|
|
432
|
+
failure_messages = support_visitor.failure_messages
|
|
433
|
+
if failure_messages:
|
|
434
|
+
reason_suffix = ", ".join(failure_messages)
|
|
435
|
+
status_reason = f"{ChangeSetResourceSupportChecker.TITLE_MESSAGE} {reason_suffix}"
|
|
436
|
+
|
|
437
|
+
change_set.status_reason = status_reason
|
|
438
|
+
change_set.set_change_set_status(ChangeSetStatus.FAILED)
|
|
439
|
+
failure_transitions = {
|
|
440
|
+
ChangeSetType.CREATE: (
|
|
441
|
+
StackStatus.ROLLBACK_IN_PROGRESS,
|
|
442
|
+
StackStatus.CREATE_FAILED,
|
|
443
|
+
),
|
|
444
|
+
ChangeSetType.UPDATE: (
|
|
445
|
+
StackStatus.UPDATE_ROLLBACK_IN_PROGRESS,
|
|
446
|
+
StackStatus.UPDATE_ROLLBACK_FAILED,
|
|
447
|
+
),
|
|
448
|
+
ChangeSetType.IMPORT: (
|
|
449
|
+
StackStatus.IMPORT_ROLLBACK_IN_PROGRESS,
|
|
450
|
+
StackStatus.IMPORT_ROLLBACK_FAILED,
|
|
451
|
+
),
|
|
452
|
+
}
|
|
453
|
+
transitions = failure_transitions.get(change_set.change_set_type)
|
|
454
|
+
if transitions:
|
|
455
|
+
first_status, *remaining_statuses = transitions
|
|
456
|
+
change_set.stack.set_stack_status(first_status, status_reason)
|
|
457
|
+
for status in remaining_statuses:
|
|
458
|
+
change_set.stack.set_stack_status(status)
|
|
459
|
+
return
|
|
460
|
+
|
|
420
461
|
@handler("CreateChangeSet", expand=False)
|
|
421
462
|
def create_change_set(
|
|
422
463
|
self, context: RequestContext, request: CreateChangeSetInput
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
|
|
3
|
-
from localstack.aws.api.cloudwatch import
|
|
3
|
+
from localstack.aws.api.cloudwatch import (
|
|
4
|
+
AlarmHistoryItem,
|
|
5
|
+
CompositeAlarm,
|
|
6
|
+
DashboardBody,
|
|
7
|
+
MetricAlarm,
|
|
8
|
+
StateValue,
|
|
9
|
+
)
|
|
4
10
|
from localstack.services.stores import (
|
|
5
11
|
AccountRegionBundle,
|
|
6
12
|
BaseStore,
|
|
@@ -72,6 +78,8 @@ class LocalStackDashboard:
|
|
|
72
78
|
dashboard_name: str
|
|
73
79
|
dashboard_arn: str
|
|
74
80
|
dashboard_body: DashboardBody
|
|
81
|
+
last_modified: datetime.datetime
|
|
82
|
+
size: int
|
|
75
83
|
|
|
76
84
|
def __init__(
|
|
77
85
|
self, account_id: str, region: str, dashboard_name: str, dashboard_body: DashboardBody
|
|
@@ -99,7 +107,7 @@ class CloudWatchStore(BaseStore):
|
|
|
99
107
|
|
|
100
108
|
# Contains all the Alarm Histories. Per documentation, an alarm history is retained even if the alarm is deleted,
|
|
101
109
|
# making it necessary to save this at store level
|
|
102
|
-
histories: list[
|
|
110
|
+
histories: list[AlarmHistoryItem] = LocalAttribute(default=list)
|
|
103
111
|
|
|
104
112
|
dashboards: dict[str, LocalStackDashboard] = LocalAttribute(default=dict)
|
|
105
113
|
|
|
@@ -9,6 +9,7 @@ from localstack.aws.api import CommonServiceException, RequestContext, handler
|
|
|
9
9
|
from localstack.aws.api.cloudwatch import (
|
|
10
10
|
AccountId,
|
|
11
11
|
ActionPrefix,
|
|
12
|
+
AlarmHistoryItem,
|
|
12
13
|
AlarmName,
|
|
13
14
|
AlarmNamePrefix,
|
|
14
15
|
AlarmNames,
|
|
@@ -276,7 +277,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
276
277
|
# Paginate
|
|
277
278
|
timestamp_value_dicts = [
|
|
278
279
|
{
|
|
279
|
-
"Timestamp": timestamp,
|
|
280
|
+
"Timestamp": datetime.datetime.fromtimestamp(timestamp, tz=datetime.UTC),
|
|
280
281
|
"Value": float(value),
|
|
281
282
|
}
|
|
282
283
|
for timestamp, value in zip(timestamps, values, strict=False)
|
|
@@ -760,7 +761,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
760
761
|
self,
|
|
761
762
|
context: RequestContext,
|
|
762
763
|
alarm: LocalStackAlarm,
|
|
763
|
-
state_value:
|
|
764
|
+
state_value: StateValue,
|
|
764
765
|
state_reason: str,
|
|
765
766
|
state_reason_data: dict = None,
|
|
766
767
|
):
|
|
@@ -780,18 +781,17 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
780
781
|
"stateReasonData": state_reason_data,
|
|
781
782
|
},
|
|
782
783
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
else "CompositeAlarm",
|
|
793
|
-
}
|
|
784
|
+
alarm_history_item = AlarmHistoryItem(
|
|
785
|
+
Timestamp=alarm.alarm["StateUpdatedTimestamp"],
|
|
786
|
+
HistoryItemType=HistoryItemType.StateUpdate,
|
|
787
|
+
AlarmName=alarm.alarm["AlarmName"],
|
|
788
|
+
HistoryData=json.dumps(history_data),
|
|
789
|
+
HistorySummary=f"Alarm updated from {old_state} to {state_value}",
|
|
790
|
+
AlarmType="MetricAlarm"
|
|
791
|
+
if isinstance(alarm, LocalStackMetricAlarm)
|
|
792
|
+
else "CompositeAlarm",
|
|
794
793
|
)
|
|
794
|
+
store.histories.append(alarm_history_item)
|
|
795
795
|
alarm.alarm["StateValue"] = state_value
|
|
796
796
|
alarm.alarm["StateReason"] = state_reason
|
|
797
797
|
if state_reason_data:
|
|
@@ -837,15 +837,10 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
837
837
|
if alarm_name:
|
|
838
838
|
history = [h for h in history if h["AlarmName"] == alarm_name]
|
|
839
839
|
|
|
840
|
-
def _get_timestamp(input: dict):
|
|
841
|
-
if timestamp_string := input.get("Timestamp"):
|
|
842
|
-
return datetime.datetime.fromisoformat(timestamp_string)
|
|
843
|
-
return None
|
|
844
|
-
|
|
845
840
|
if start_date:
|
|
846
|
-
history = [h for h in history if (date :=
|
|
841
|
+
history = [h for h in history if (date := h.get("Timestamp")) and date >= start_date]
|
|
847
842
|
if end_date:
|
|
848
|
-
history = [h for h in history if (date :=
|
|
843
|
+
history = [h for h in history if (date := h.get("Timestamp")) and date <= end_date]
|
|
849
844
|
return DescribeAlarmHistoryOutput(AlarmHistoryItems=history)
|
|
850
845
|
|
|
851
846
|
def _evaluate_composite_alarms(self, context: RequestContext, triggering_alarm):
|
|
@@ -7,7 +7,7 @@ from localstack.packages import InstallTarget, Package
|
|
|
7
7
|
from localstack.packages.core import GitHubReleaseInstaller, NodePackageInstaller
|
|
8
8
|
from localstack.packages.java import JavaInstallerMixin, java_package
|
|
9
9
|
|
|
10
|
-
_KINESIS_MOCK_VERSION = os.environ.get("KINESIS_MOCK_VERSION") or "0.5.
|
|
10
|
+
_KINESIS_MOCK_VERSION = os.environ.get("KINESIS_MOCK_VERSION") or "0.5.2"
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class KinesisMockEngine(StrEnum):
|
|
@@ -232,7 +232,10 @@ class KmsCryptoKey:
|
|
|
232
232
|
|
|
233
233
|
if key_spec.startswith("RSA"):
|
|
234
234
|
key_size = RSA_CRYPTO_KEY_LENGTHS.get(key_spec)
|
|
235
|
-
|
|
235
|
+
if key_material:
|
|
236
|
+
key = crypto_serialization.load_der_private_key(key_material, password=None)
|
|
237
|
+
else:
|
|
238
|
+
key = rsa.generate_private_key(public_exponent=65537, key_size=key_size)
|
|
236
239
|
elif key_spec.startswith("ECC"):
|
|
237
240
|
curve = ECC_CURVES.get(key_spec)
|
|
238
241
|
if key_material:
|
|
@@ -636,7 +639,8 @@ class KmsKey:
|
|
|
636
639
|
# https://docs.aws.amazon.com/kms/latest/APIReference/API_TagResource.html
|
|
637
640
|
# "To edit a tag, specify an existing tag key and a new tag value."
|
|
638
641
|
for i, tag in enumerate(tags, start=1):
|
|
639
|
-
|
|
642
|
+
if tag.get("TagKey") != TAG_KEY_CUSTOM_KEY_MATERIAL:
|
|
643
|
+
validate_tag(i, tag)
|
|
640
644
|
self.tags[tag.get("TagKey")] = tag.get("TagValue")
|
|
641
645
|
|
|
642
646
|
def schedule_key_deletion(self, pending_window_in_days: int) -> None:
|
|
@@ -14,9 +14,10 @@ function_counter = LabeledCounter(
|
|
|
14
14
|
"status",
|
|
15
15
|
"runtime",
|
|
16
16
|
"package_type",
|
|
17
|
-
# only for operation "invoke"
|
|
18
|
-
"
|
|
17
|
+
"invocation_type", # only for operation "invoke", otherwise "n/a"
|
|
18
|
+
"initialization_type",
|
|
19
19
|
],
|
|
20
|
+
schema_version=2,
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
|
|
@@ -38,6 +39,14 @@ class FunctionStatus(StrEnum):
|
|
|
38
39
|
invocation_error = "invocation_error"
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
class FunctionInitializationType(StrEnum):
|
|
43
|
+
# Maps to the Lambda environment variable AWS_LAMBDA_INITIALIZATION_TYPE
|
|
44
|
+
on_demand = "on-demand"
|
|
45
|
+
lambda_managed_instances = "lambda-managed-instances"
|
|
46
|
+
# Only applies to the operation "invoke" because provisioned concurrency is not configured on "create"
|
|
47
|
+
provisioned_concurrency = "provisioned-concurrency"
|
|
48
|
+
|
|
49
|
+
|
|
41
50
|
esm_counter = LabeledCounter(namespace=NAMESPACE, name="esm", labels=["source", "status"])
|
|
42
51
|
|
|
43
52
|
|
|
@@ -13,6 +13,7 @@ from botocore.config import Config
|
|
|
13
13
|
from localstack import config
|
|
14
14
|
from localstack.aws.api.lambda_ import InvocationType, TooManyRequestsException
|
|
15
15
|
from localstack.services.lambda_.analytics import (
|
|
16
|
+
FunctionInitializationType,
|
|
16
17
|
FunctionOperation,
|
|
17
18
|
FunctionStatus,
|
|
18
19
|
function_counter,
|
|
@@ -198,22 +199,22 @@ class Poller:
|
|
|
198
199
|
def handle_message(self, message: dict) -> None:
|
|
199
200
|
failure_cause = None
|
|
200
201
|
qualifier = self.version_manager.function_version.id.qualifier
|
|
202
|
+
function_config = self.version_manager.function_version.config
|
|
201
203
|
event_invoke_config = self.version_manager.function.event_invoke_configs.get(qualifier)
|
|
202
204
|
runtime = None
|
|
203
205
|
status = None
|
|
206
|
+
# TODO: handle initialization_type provisioned-concurrency, which requires enriching invocation_result
|
|
207
|
+
initialization_type = (
|
|
208
|
+
FunctionInitializationType.lambda_managed_instances
|
|
209
|
+
if function_config.CapacityProviderConfig
|
|
210
|
+
else FunctionInitializationType.on_demand
|
|
211
|
+
)
|
|
204
212
|
try:
|
|
205
213
|
sqs_invocation = SQSInvocation.decode(message["Body"])
|
|
206
214
|
invocation = sqs_invocation.invocation
|
|
207
215
|
try:
|
|
208
216
|
invocation_result = self.version_manager.invoke(invocation=invocation)
|
|
209
|
-
|
|
210
|
-
function_counter.labels(
|
|
211
|
-
operation=FunctionOperation.invoke,
|
|
212
|
-
runtime=function_config.runtime or "n/a",
|
|
213
|
-
status=FunctionStatus.success,
|
|
214
|
-
invocation_type=InvocationType.Event,
|
|
215
|
-
package_type=function_config.package_type,
|
|
216
|
-
).increment()
|
|
217
|
+
status = FunctionStatus.success
|
|
217
218
|
except Exception as e:
|
|
218
219
|
# Reserved concurrency == 0
|
|
219
220
|
if self.version_manager.function.reserved_concurrent_executions == 0:
|
|
@@ -223,6 +224,7 @@ class Poller:
|
|
|
223
224
|
elif not has_enough_time_for_retry(sqs_invocation, event_invoke_config):
|
|
224
225
|
failure_cause = "EventAgeExceeded"
|
|
225
226
|
status = FunctionStatus.event_age_exceeded_error
|
|
227
|
+
|
|
226
228
|
if failure_cause:
|
|
227
229
|
invocation_result = InvocationResult(
|
|
228
230
|
is_error=True, request_id=invocation.request_id, payload=None, logs=None
|
|
@@ -240,14 +242,14 @@ class Poller:
|
|
|
240
242
|
sqs_client.delete_message(
|
|
241
243
|
QueueUrl=self.event_queue_url, ReceiptHandle=message["ReceiptHandle"]
|
|
242
244
|
)
|
|
243
|
-
|
|
244
|
-
package_type = self.version_manager.function_version.config.package_type
|
|
245
|
+
assert status, "status MUST be set before returning"
|
|
245
246
|
function_counter.labels(
|
|
246
247
|
operation=FunctionOperation.invoke,
|
|
247
248
|
runtime=runtime or "n/a",
|
|
248
249
|
status=status,
|
|
249
250
|
invocation_type=InvocationType.Event,
|
|
250
|
-
package_type=package_type,
|
|
251
|
+
package_type=function_config.package_type,
|
|
252
|
+
initialization_type=initialization_type,
|
|
251
253
|
).increment()
|
|
252
254
|
|
|
253
255
|
# Good summary blogpost: https://haithai91.medium.com/aws-lambdas-retry-behaviors-edff90e1cf1b
|
|
@@ -257,6 +259,8 @@ class Poller:
|
|
|
257
259
|
if event_invoke_config and event_invoke_config.maximum_retry_attempts is not None:
|
|
258
260
|
max_retry_attempts = event_invoke_config.maximum_retry_attempts
|
|
259
261
|
|
|
262
|
+
assert invocation_result, "Invocation result MUST exist if we are not returning before"
|
|
263
|
+
|
|
260
264
|
# An invocation error either leads to a terminal failure or to a scheduled retry
|
|
261
265
|
if invocation_result.is_error: # invocation error
|
|
262
266
|
failure_cause = None
|
|
@@ -30,6 +30,7 @@ from localstack.aws.api.lambda_ import (
|
|
|
30
30
|
CodeSigningPolicies,
|
|
31
31
|
Cors,
|
|
32
32
|
DestinationConfig,
|
|
33
|
+
FunctionScalingConfig,
|
|
33
34
|
FunctionUrlAuthType,
|
|
34
35
|
InstanceRequirements,
|
|
35
36
|
InvocationType,
|
|
@@ -625,6 +626,9 @@ class Function:
|
|
|
625
626
|
provisioned_concurrency_configs: dict[str, ProvisionedConcurrencyConfiguration] = (
|
|
626
627
|
dataclasses.field(default_factory=dict)
|
|
627
628
|
)
|
|
629
|
+
function_scaling_configs: dict[str, FunctionScalingConfig] = dataclasses.field(
|
|
630
|
+
default_factory=dict
|
|
631
|
+
)
|
|
628
632
|
|
|
629
633
|
lock: threading.RLock = dataclasses.field(default_factory=threading.RLock)
|
|
630
634
|
next_version: int = 1
|
|
@@ -29,6 +29,7 @@ from localstack.aws.connect import connect_to
|
|
|
29
29
|
from localstack.constants import AWS_REGION_US_EAST_1
|
|
30
30
|
from localstack.services.lambda_ import hooks as lambda_hooks
|
|
31
31
|
from localstack.services.lambda_.analytics import (
|
|
32
|
+
FunctionInitializationType,
|
|
32
33
|
FunctionOperation,
|
|
33
34
|
FunctionStatus,
|
|
34
35
|
function_counter,
|
|
@@ -313,6 +314,12 @@ class LambdaService:
|
|
|
313
314
|
raise ResourceNotFoundException(f"Function not found: {invoked_arn}", Type="User")
|
|
314
315
|
runtime = version.config.runtime or "n/a"
|
|
315
316
|
package_type = version.config.package_type
|
|
317
|
+
# Not considering provisioned concurrency for such early errors
|
|
318
|
+
initialization_type = (
|
|
319
|
+
FunctionInitializationType.lambda_managed_instances
|
|
320
|
+
if version.config.CapacityProviderConfig
|
|
321
|
+
else FunctionInitializationType.on_demand
|
|
322
|
+
)
|
|
316
323
|
if version.config.CapacityProviderConfig and qualifier == "$LATEST":
|
|
317
324
|
if function.versions.get("$LATEST.PUBLISHED"):
|
|
318
325
|
raise InvalidParameterValueException(
|
|
@@ -355,6 +362,7 @@ class LambdaService:
|
|
|
355
362
|
status=status,
|
|
356
363
|
invocation_type=invocation_type,
|
|
357
364
|
package_type=package_type,
|
|
365
|
+
initialization_type=initialization_type,
|
|
358
366
|
).increment()
|
|
359
367
|
raise ResourceConflictException(
|
|
360
368
|
f"The operation cannot be performed at this time. The function is currently in the following state: {state}"
|
|
@@ -373,6 +381,7 @@ class LambdaService:
|
|
|
373
381
|
status=FunctionStatus.invalid_payload_error,
|
|
374
382
|
invocation_type=invocation_type,
|
|
375
383
|
package_type=package_type,
|
|
384
|
+
initialization_type=initialization_type,
|
|
376
385
|
).increment()
|
|
377
386
|
# MAYBE: improve parity of detailed exception message (quite cumbersome)
|
|
378
387
|
raise InvalidRequestContentException(
|
|
@@ -417,12 +426,14 @@ class LambdaService:
|
|
|
417
426
|
if invocation_result.is_error
|
|
418
427
|
else FunctionStatus.success
|
|
419
428
|
)
|
|
429
|
+
# TODO: handle initialization_type provisioned-concurrency, requires enriching invocation_result
|
|
420
430
|
function_counter.labels(
|
|
421
431
|
operation=FunctionOperation.invoke,
|
|
422
432
|
runtime=runtime,
|
|
423
433
|
status=status,
|
|
424
434
|
invocation_type=invocation_type,
|
|
425
435
|
package_type=package_type,
|
|
436
|
+
initialization_type=initialization_type,
|
|
426
437
|
).increment()
|
|
427
438
|
return invocation_result
|
|
428
439
|
|