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.
Files changed (82) hide show
  1. localstack/aws/api/ec2/__init__.py +13 -0
  2. localstack/aws/api/iam/__init__.py +1 -0
  3. localstack/aws/api/lambda_/__init__.py +616 -0
  4. localstack/aws/api/logs/__init__.py +188 -0
  5. localstack/aws/api/opensearch/__init__.py +11 -0
  6. localstack/aws/api/route53/__init__.py +3 -0
  7. localstack/aws/api/s3/__init__.py +2 -0
  8. localstack/aws/api/s3control/__init__.py +19 -0
  9. localstack/aws/api/secretsmanager/__init__.py +9 -0
  10. localstack/aws/connect.py +35 -15
  11. localstack/aws/protocol/parser.py +6 -1
  12. localstack/aws/spec-patches.json +0 -38
  13. localstack/config.py +8 -0
  14. localstack/constants.py +3 -0
  15. localstack/dev/kubernetes/__main__.py +39 -14
  16. localstack/runtime/analytics.py +11 -0
  17. localstack/services/acm/provider.py +13 -1
  18. localstack/services/apigateway/legacy/provider.py +25 -4
  19. localstack/services/cloudformation/engine/v2/change_set_model.py +9 -0
  20. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +3 -1
  21. localstack/services/cloudformation/engine/v2/change_set_resource_support_checker.py +114 -0
  22. localstack/services/cloudformation/provider.py +26 -1
  23. localstack/services/cloudformation/provider_utils.py +20 -0
  24. localstack/services/cloudformation/resource_provider.py +5 -4
  25. localstack/services/cloudformation/scaffolding/__main__.py +94 -22
  26. localstack/services/cloudformation/v2/provider.py +41 -0
  27. localstack/services/cloudwatch/models.py +10 -2
  28. localstack/services/cloudwatch/provider_v2.py +15 -20
  29. localstack/services/kinesis/packages.py +1 -1
  30. localstack/services/kms/models.py +6 -2
  31. localstack/services/lambda_/analytics.py +11 -2
  32. localstack/services/lambda_/invocation/event_manager.py +15 -11
  33. localstack/services/lambda_/invocation/lambda_models.py +4 -0
  34. localstack/services/lambda_/invocation/lambda_service.py +11 -0
  35. localstack/services/lambda_/provider.py +70 -13
  36. localstack/services/opensearch/packages.py +34 -20
  37. localstack/services/route53/provider.py +7 -0
  38. localstack/services/route53resolver/provider.py +5 -0
  39. localstack/services/s3/constants.py +5 -0
  40. localstack/services/s3/exceptions.py +9 -0
  41. localstack/services/s3/models.py +9 -1
  42. localstack/services/s3/provider.py +25 -30
  43. localstack/services/s3/utils.py +46 -1
  44. localstack/services/s3control/provider.py +6 -0
  45. localstack/services/scheduler/provider.py +4 -2
  46. localstack/services/secretsmanager/provider.py +4 -0
  47. localstack/services/ses/provider.py +4 -0
  48. localstack/services/sns/constants.py +13 -0
  49. localstack/services/sns/provider.py +5 -0
  50. localstack/services/sns/v2/models.py +4 -0
  51. localstack/services/sns/v2/provider.py +145 -0
  52. localstack/services/sqs/constants.py +6 -0
  53. localstack/services/sqs/provider.py +9 -1
  54. localstack/services/sqs/resource_providers/aws_sqs_queue.py +61 -46
  55. localstack/services/ssm/provider.py +6 -0
  56. localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +193 -107
  57. localstack/services/stepfunctions/backend/execution.py +4 -5
  58. localstack/services/stepfunctions/provider.py +21 -14
  59. localstack/services/sts/provider.py +7 -0
  60. localstack/services/support/provider.py +5 -1
  61. localstack/services/swf/provider.py +5 -1
  62. localstack/services/transcribe/provider.py +7 -0
  63. localstack/testing/aws/lambda_utils.py +1 -1
  64. localstack/testing/aws/util.py +2 -1
  65. localstack/testing/config.py +1 -0
  66. localstack/utils/aws/client_types.py +2 -4
  67. localstack/utils/bootstrap.py +2 -2
  68. localstack/utils/catalog/catalog.py +3 -2
  69. localstack/utils/container_utils/container_client.py +22 -13
  70. localstack/utils/container_utils/docker_cmd_client.py +6 -6
  71. localstack/version.py +2 -2
  72. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/METADATA +6 -6
  73. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/RECORD +81 -80
  74. localstack_core-4.12.1.dev25.dist-info/plux.json +1 -0
  75. localstack_core-4.11.2.dev14.dist-info/plux.json +0 -1
  76. {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack +0 -0
  77. {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack-supervisor +0 -0
  78. {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev25.data}/scripts/localstack.bat +0 -0
  79. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/WHEEL +0 -0
  80. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/entry_points.txt +0 -0
  81. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev25.dist-info}/licenses/LICENSE.txt +0 -0
  82. {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
- "tests/aws/services/cloudformation/resource_providers"
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()}/{resource_name.path_compatible_full_name()}/templates/{stub}"
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/cloudformation/resource_providers/{resource_name.python_compatible_service_name.lower()}/{resource_name.path_compatible_full_name()}"
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/cloudformation/resource_providers/{resource_name.python_compatible_service_name.lower()}/{resource_name.full_name.lower()}"
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(resource_name, FileType.attribute_template, tests_output_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(resource_name, FileType.minimal_template, tests_output_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(resource_name, FileType.autogenerated_template, tests_output_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(self.resource_name, template_type)
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 = SchemaProvider(
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
- parts = resource_type.rpartition("::")
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 CompositeAlarm, DashboardBody, MetricAlarm, StateValue
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[dict] = LocalAttribute(default=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: str,
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
- store.histories.append(
784
- {
785
- "Timestamp": timestamp_millis(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",
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 := _get_timestamp(h)) and date >= start_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 := _get_timestamp(h)) and date <= end_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.1"
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
- key = rsa.generate_private_key(public_exponent=65537, key_size=key_size)
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
- validate_tag(i, tag)
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
- "invocation_type",
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
- function_config = self.version_manager.function_version.config
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
- # status MUST be set before returning
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