localstack-core 4.10.1.dev42__py3-none-any.whl → 4.11.2.dev14__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 (116) hide show
  1. localstack/aws/api/apigateway/__init__.py +42 -0
  2. localstack/aws/api/cloudformation/__init__.py +161 -0
  3. localstack/aws/api/ec2/__init__.py +1165 -12
  4. localstack/aws/api/iam/__init__.py +227 -0
  5. localstack/aws/api/kms/__init__.py +1 -0
  6. localstack/aws/api/lambda_/__init__.py +418 -66
  7. localstack/aws/api/logs/__init__.py +312 -0
  8. localstack/aws/api/opensearch/__init__.py +89 -0
  9. localstack/aws/api/redshift/__init__.py +69 -0
  10. localstack/aws/api/resourcegroupstaggingapi/__init__.py +36 -0
  11. localstack/aws/api/route53/__init__.py +42 -0
  12. localstack/aws/api/route53resolver/__init__.py +1 -0
  13. localstack/aws/api/s3/__init__.py +62 -0
  14. localstack/aws/api/secretsmanager/__init__.py +28 -23
  15. localstack/aws/api/stepfunctions/__init__.py +52 -10
  16. localstack/aws/api/sts/__init__.py +52 -0
  17. localstack/aws/handlers/logging.py +8 -4
  18. localstack/aws/handlers/service.py +11 -2
  19. localstack/aws/protocol/serializer.py +1 -1
  20. localstack/deprecations.py +0 -6
  21. localstack/services/acm/provider.py +4 -0
  22. localstack/services/apigateway/legacy/provider.py +28 -15
  23. localstack/services/cloudformation/engine/template_preparer.py +6 -2
  24. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +12 -0
  25. localstack/services/cloudwatch/provider.py +10 -3
  26. localstack/services/cloudwatch/provider_v2.py +6 -3
  27. localstack/services/configservice/provider.py +5 -1
  28. localstack/services/dynamodb/provider.py +1 -0
  29. localstack/services/dynamodb/v2/provider.py +1 -0
  30. localstack/services/dynamodbstreams/provider.py +6 -0
  31. localstack/services/dynamodbstreams/v2/provider.py +6 -0
  32. localstack/services/ec2/provider.py +6 -0
  33. localstack/services/es/provider.py +6 -0
  34. localstack/services/events/provider.py +4 -0
  35. localstack/services/events/v1/provider.py +9 -0
  36. localstack/services/firehose/provider.py +5 -0
  37. localstack/services/iam/provider.py +4 -0
  38. localstack/services/kms/models.py +10 -20
  39. localstack/services/kms/provider.py +4 -0
  40. localstack/services/lambda_/api_utils.py +37 -20
  41. localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +1 -1
  42. localstack/services/lambda_/invocation/assignment.py +4 -1
  43. localstack/services/lambda_/invocation/execution_environment.py +21 -2
  44. localstack/services/lambda_/invocation/lambda_models.py +27 -2
  45. localstack/services/lambda_/invocation/lambda_service.py +51 -3
  46. localstack/services/lambda_/invocation/models.py +9 -1
  47. localstack/services/lambda_/invocation/version_manager.py +18 -3
  48. localstack/services/lambda_/provider.py +239 -95
  49. localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
  50. localstack/services/lambda_/runtimes.py +3 -1
  51. localstack/services/logs/provider.py +9 -0
  52. localstack/services/opensearch/provider.py +53 -3
  53. localstack/services/resource_groups/provider.py +5 -1
  54. localstack/services/resourcegroupstaggingapi/provider.py +6 -1
  55. localstack/services/s3/provider.py +28 -15
  56. localstack/services/s3/utils.py +35 -14
  57. localstack/services/s3control/provider.py +101 -2
  58. localstack/services/s3control/validation.py +50 -0
  59. localstack/services/sns/constants.py +3 -1
  60. localstack/services/sns/publisher.py +15 -6
  61. localstack/services/sns/v2/models.py +6 -0
  62. localstack/services/sns/v2/provider.py +650 -19
  63. localstack/services/sns/v2/utils.py +12 -0
  64. localstack/services/stepfunctions/asl/component/common/path/result_path.py +1 -1
  65. localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +0 -1
  66. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +0 -1
  67. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +8 -8
  68. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/{mock_eval_utils.py → local_mock_eval_utils.py} +13 -9
  69. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +6 -6
  70. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +1 -1
  71. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +4 -0
  72. localstack/services/stepfunctions/asl/component/test_state/state/base_mock.py +118 -0
  73. localstack/services/stepfunctions/asl/component/test_state/state/common.py +82 -0
  74. localstack/services/stepfunctions/asl/component/test_state/state/execution.py +139 -0
  75. localstack/services/stepfunctions/asl/component/test_state/state/map.py +77 -0
  76. localstack/services/stepfunctions/asl/component/test_state/state/task.py +44 -0
  77. localstack/services/stepfunctions/asl/eval/environment.py +30 -22
  78. localstack/services/stepfunctions/asl/eval/states.py +1 -1
  79. localstack/services/stepfunctions/asl/eval/test_state/environment.py +49 -9
  80. localstack/services/stepfunctions/asl/eval/test_state/program_state.py +22 -0
  81. localstack/services/stepfunctions/asl/jsonata/jsonata.py +5 -1
  82. localstack/services/stepfunctions/asl/parse/preprocessor.py +67 -24
  83. localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py +5 -4
  84. localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py +222 -31
  85. localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +170 -22
  86. localstack/services/stepfunctions/backend/execution.py +6 -6
  87. localstack/services/stepfunctions/backend/execution_worker.py +5 -5
  88. localstack/services/stepfunctions/backend/test_state/execution.py +36 -0
  89. localstack/services/stepfunctions/backend/test_state/execution_worker.py +33 -1
  90. localstack/services/stepfunctions/backend/test_state/test_state_mock.py +127 -0
  91. localstack/services/stepfunctions/local_mocking/__init__.py +9 -0
  92. localstack/services/stepfunctions/{mocking → local_mocking}/mock_config.py +24 -17
  93. localstack/services/stepfunctions/provider.py +78 -27
  94. localstack/services/stepfunctions/test_state/mock_config.py +47 -0
  95. localstack/testing/pytest/fixtures.py +28 -0
  96. localstack/testing/snapshots/transformer_utility.py +5 -0
  97. localstack/utils/analytics/publisher.py +37 -155
  98. localstack/utils/analytics/service_request_aggregator.py +6 -4
  99. localstack/utils/aws/arns.py +7 -0
  100. localstack/utils/batching.py +258 -0
  101. localstack/utils/collections.py +23 -11
  102. localstack/version.py +2 -2
  103. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.11.2.dev14.dist-info}/METADATA +5 -5
  104. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.11.2.dev14.dist-info}/RECORD +113 -105
  105. localstack_core-4.11.2.dev14.dist-info/plux.json +1 -0
  106. localstack/services/stepfunctions/mocking/__init__.py +0 -0
  107. localstack/utils/batch_policy.py +0 -124
  108. localstack_core-4.10.1.dev42.dist-info/plux.json +0 -1
  109. /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
  110. {localstack_core-4.10.1.dev42.data → localstack_core-4.11.2.dev14.data}/scripts/localstack +0 -0
  111. {localstack_core-4.10.1.dev42.data → localstack_core-4.11.2.dev14.data}/scripts/localstack-supervisor +0 -0
  112. {localstack_core-4.10.1.dev42.data → localstack_core-4.11.2.dev14.data}/scripts/localstack.bat +0 -0
  113. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.11.2.dev14.dist-info}/WHEEL +0 -0
  114. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.11.2.dev14.dist-info}/entry_points.txt +0 -0
  115. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.11.2.dev14.dist-info}/licenses/LICENSE.txt +0 -0
  116. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.11.2.dev14.dist-info}/top_level.txt +0 -0
@@ -45,6 +45,7 @@ from localstack.services.events.scheduler import JobScheduler
45
45
  from localstack.services.events.v1.models import EventsStore, events_stores
46
46
  from localstack.services.moto import call_moto
47
47
  from localstack.services.plugins import ServiceLifecycleHook
48
+ from localstack.state import StateVisitor
48
49
  from localstack.utils.aws.arns import event_bus_arn, parse_arn
49
50
  from localstack.utils.aws.client_types import ServicePrincipal
50
51
  from localstack.utils.aws.message_forwarding import send_event_to_target
@@ -83,6 +84,14 @@ class EventsProvider(EventsApi, ServiceLifecycleHook):
83
84
  def on_before_stop(self):
84
85
  JobScheduler.shutdown()
85
86
 
87
+ def accept_state_visitor(self, visitor: StateVisitor):
88
+ from moto.events.models import events_backends
89
+
90
+ from localstack.services.events.v1.models import events_stores
91
+
92
+ visitor.visit(events_backends)
93
+ visitor.visit(events_stores)
94
+
86
95
  @route("/_aws/events/rules/<path:rule_arn>/trigger")
87
96
  def trigger_scheduled_rule(self, request: Request, rule_arn: str):
88
97
  """Developer endpoint to trigger a scheduled rule."""
@@ -94,6 +94,7 @@ from localstack.services.firehose.mappers import (
94
94
  convert_source_config_to_desc,
95
95
  )
96
96
  from localstack.services.firehose.models import FirehoseStore, firehose_stores
97
+ from localstack.state import StateVisitor
97
98
  from localstack.utils.aws.arns import (
98
99
  extract_account_id_from_arn,
99
100
  extract_region_from_arn,
@@ -251,8 +252,12 @@ class FirehoseProvider(FirehoseApi):
251
252
 
252
253
  def __init__(self) -> None:
253
254
  super().__init__()
255
+ # TODO: stop/restart the kinesis listeners when stopping the service / reset the state / restore the state
254
256
  self.kinesis_listeners = {}
255
257
 
258
+ def accept_state_visitor(self, visitor: StateVisitor):
259
+ visitor.visit(firehose_stores)
260
+
256
261
  @staticmethod
257
262
  def get_store(account_id: str, region_name: str) -> FirehoseStore:
258
263
  return firehose_stores[account_id][region_name]
@@ -75,6 +75,7 @@ from localstack.services.iam.resources.policy_simulator import (
75
75
  )
76
76
  from localstack.services.iam.resources.service_linked_roles import SERVICE_LINKED_ROLES
77
77
  from localstack.services.moto import call_moto
78
+ from localstack.state import StateVisitor
78
79
  from localstack.utils.aws.request_context import extract_access_key_id_from_auth_header
79
80
 
80
81
  LOG = logging.getLogger(__name__)
@@ -110,6 +111,9 @@ class IamProvider(IamApi):
110
111
  apply_iam_patches()
111
112
  self.policy_simulator = BasicIAMPolicySimulator()
112
113
 
114
+ def accept_state_visitor(self, visitor: StateVisitor):
115
+ visitor.visit(iam_backends)
116
+
113
117
  @handler("CreateRole", expand=False)
114
118
  def create_role(
115
119
  self, context: RequestContext, request: CreateRoleRequest
@@ -20,7 +20,6 @@ from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
20
20
  from cryptography.hazmat.primitives.asymmetric.padding import PSS, PKCS1v15
21
21
  from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
22
22
  from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
23
- from cryptography.hazmat.primitives.kdf.hkdf import HKDF
24
23
  from cryptography.hazmat.primitives.serialization import load_der_public_key
25
24
 
26
25
  from localstack.aws.api.kms import (
@@ -442,17 +441,15 @@ class KmsKey:
442
441
 
443
442
  def derive_shared_secret(self, public_key: bytes) -> bytes:
444
443
  key_spec = self.metadata.get("KeySpec")
445
- match key_spec:
446
- case KeySpec.ECC_NIST_P256 | KeySpec.ECC_SECG_P256K1:
447
- algorithm = hashes.SHA256()
448
- case KeySpec.ECC_NIST_P384:
449
- algorithm = hashes.SHA384()
450
- case KeySpec.ECC_NIST_P521:
451
- algorithm = hashes.SHA512()
452
- case _:
453
- raise InvalidKeyUsageException(
454
- f"{self.metadata['Arn']} key usage is {self.metadata['KeyUsage']} which is not valid for DeriveSharedSecret."
455
- )
444
+ if key_spec not in (
445
+ KeySpec.ECC_NIST_P256,
446
+ KeySpec.ECC_SECG_P256K1,
447
+ KeySpec.ECC_NIST_P384,
448
+ KeySpec.ECC_NIST_P521,
449
+ ):
450
+ raise InvalidKeyUsageException(
451
+ f"{self.metadata['Arn']} key usage is {self.metadata['KeyUsage']} which is not valid for DeriveSharedSecret."
452
+ )
456
453
 
457
454
  # Deserialize public key from DER encoded data to EllipticCurvePublicKey.
458
455
  try:
@@ -460,14 +457,7 @@ class KmsKey:
460
457
  except (UnsupportedAlgorithm, ValueError):
461
458
  raise ValidationException("")
462
459
  shared_secret = self.crypto_key.key.exchange(ec.ECDH(), pub_key)
463
- # Perform shared secret derivation.
464
- return HKDF(
465
- algorithm=algorithm,
466
- salt=None,
467
- info=b"",
468
- length=algorithm.digest_size,
469
- backend=default_backend(),
470
- ).derive(shared_secret)
460
+ return shared_secret
471
461
 
472
462
  # This method gets called when a key is replicated to another region. It's meant to populate the required metadata
473
463
  # fields in a new replica key.
@@ -138,6 +138,7 @@ from localstack.services.kms.utils import (
138
138
  validate_alias_name,
139
139
  )
140
140
  from localstack.services.plugins import ServiceLifecycleHook
141
+ from localstack.state import StateVisitor
141
142
  from localstack.utils.aws.arns import get_partition, kms_alias_arn, parse_arn
142
143
  from localstack.utils.collections import PaginatedList
143
144
  from localstack.utils.common import select_attributes
@@ -201,6 +202,9 @@ class KmsProvider(KmsApi, ServiceLifecycleHook):
201
202
  - VerifyMac
202
203
  """
203
204
 
205
+ def accept_state_visitor(self, visitor: StateVisitor):
206
+ visitor.visit(kms_stores)
207
+
204
208
  #
205
209
  # Helpers
206
210
  #
@@ -3,6 +3,8 @@ Everything related to behavior or implicit functionality goes into `lambda_utils
3
3
  """
4
4
 
5
5
  import datetime
6
+ import hashlib
7
+ import json
6
8
  import random
7
9
  import re
8
10
  import string
@@ -62,13 +64,14 @@ DESTINATION_ARN_PATTERN = re.compile(
62
64
  r"^$|arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\-])+:([a-z]{2}(-gov)?-[a-z]+-\d{1})?:(\d{12})?:(.*)"
63
65
  )
64
66
 
67
+ # TODO: what's the difference between AWS_FUNCTION_NAME_REGEX and FUNCTION_NAME_REGEX? Can we unify?
65
68
  AWS_FUNCTION_NAME_REGEX = re.compile(
66
- "^(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_.]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?$"
69
+ "^(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_.]+)(:(\\$LATEST(\\.PUBLISHED)?|[a-zA-Z0-9-_]+))?$"
67
70
  )
68
71
 
69
72
  # Pattern for extracting various attributes from a full or partial ARN or just a function name.
70
73
  FUNCTION_NAME_REGEX = re.compile(
71
- r"(arn:(aws[a-zA-Z-]*):lambda:)?((?P<region>[a-z]{2}(-gov)?-[a-z]+-\d{1}):)?(?:(?P<account>\d{12}):)?(function:)?(?P<name>[a-zA-Z0-9-_\.]+)(:(?P<qualifier>\$LATEST|[a-zA-Z0-9-_]+))?"
74
+ r"(arn:(aws[a-zA-Z-]*):lambda:)?((?P<region>[a-z]{2}(-gov)?-[a-z]+-\d{1}):)?(?:(?P<account>\d{12}):)?(function:)?(?P<name>[a-zA-Z0-9-_\.]+)(:(?P<qualifier>\$LATEST(\.PUBLISHED)?|[a-zA-Z0-9-_]+))?"
72
75
  ) # also length 1-170 incl.
73
76
  # Pattern for a lambda function handler
74
77
  HANDLER_REGEX = re.compile(r"[^\s]+")
@@ -86,9 +89,11 @@ SIGNING_JOB_ARN_REGEX = re.compile(
86
89
  SIGNING_PROFILE_VERSION_ARN_REGEX = re.compile(
87
90
  r"arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\-])+:([a-z]{2}(-gov)?-[a-z]+-\d{1})?:(\d{12})?:(.*)"
88
91
  )
89
- # Combined pattern for alias and version based on AWS error using "(|[a-zA-Z0-9$_-]+)"
90
- QUALIFIER_REGEX = re.compile(r"(^[a-zA-Z0-9$_-]+$)")
92
+ # Combined pattern for alias and version based on AWS error using "\\$(LATEST(\\.PUBLISHED)?)|[a-zA-Z0-9-_$]+"
93
+ # This regex is based on the snapshotted validation message, just removing the double \\ before $LATEST
94
+ QUALIFIER_REGEX = re.compile(r"^\$(LATEST(\\.PUBLISHED)?)|[a-zA-Z0-9-_$]+$")
91
95
  # Pattern for a version qualifier
96
+ # TODO: do we need to consider $LATEST.PUBLISHED here?
92
97
  VERSION_REGEX = re.compile(r"^[0-9]+$")
93
98
  # Pattern for an alias qualifier
94
99
  # Rules: https://docs.aws.amazon.com/lambda/latest/dg/API_CreateAlias.html#SSS-CreateAlias-request-Name
@@ -107,36 +112,42 @@ LAMBDA_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%f+0000"
107
112
  # An unordered list of all Lambda CPU architectures supported by LocalStack.
108
113
  ARCHITECTURES = [Architecture.arm64, Architecture.x86_64]
109
114
 
110
- # ARN pattern returned in validation exception messages.
111
- # Some excpetions from AWS return a '\.' in the function name regex
112
- # pattern therefore we can sub this value in when appropriate.
113
- ARN_NAME_PATTERN_VALIDATION_TEMPLATE = "(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{{2}}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{{1}}:)?(\\d{{12}}:)?(function:)?([a-zA-Z0-9-_{0}]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?"
115
+ # ARN patterns returned in validation exception messages
116
+ ARN_NAME_PATTERN_GET = r"(arn:(aws[a-zA-Z-]*)?:lambda:)?((eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_\.]+)(:(\$LATEST(\.PUBLISHED)?|[a-zA-Z0-9-_]+))?"
117
+ ARN_NAME_PATTERN_CREATE = r"(arn:(aws[a-zA-Z-]*)?:lambda:)?((eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\$LATEST|[a-zA-Z0-9-_]+))?"
114
118
 
115
119
  # AWS response when invalid ARNs are used in Tag operations.
116
- TAGGABLE_RESOURCE_ARN_PATTERN = "arn:(aws[a-zA-Z-]*):lambda:[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:\\d{12}:(function:[a-zA-Z0-9-_]+(:(\\$LATEST|[a-zA-Z0-9-_]+))?|layer:([a-zA-Z0-9-_]+)|code-signing-config:csc-[a-z0-9]{17}|event-source-mapping:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})"
120
+ TAGGABLE_RESOURCE_ARN_PATTERN = "arn:(aws[a-zA-Z-]*):lambda:(eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:\\d{12}:(function:[a-zA-Z0-9-_]+(:(\\$LATEST|[a-zA-Z0-9-_]+))?|layer:([a-zA-Z0-9-_]+)|code-signing-config:csc-[a-z0-9]{17}|event-source-mapping:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|capacity-provider:[a-zA-Z0-9-_]+)"
117
121
 
118
122
 
119
123
  def validate_function_name(function_name_or_arn: str, operation_type: str):
120
124
  function_name, *_ = function_locators_from_arn(function_name_or_arn)
121
- arn_name_pattern = ARN_NAME_PATTERN_VALIDATION_TEMPLATE.format("")
125
+ arn_name_pattern = ARN_NAME_PATTERN_CREATE
122
126
  max_length = 170
123
127
 
124
- match operation_type:
125
- case "GetFunction" | "Invoke":
126
- arn_name_pattern = ARN_NAME_PATTERN_VALIDATION_TEMPLATE.format(r"\.")
127
- case "CreateFunction" if function_name == function_name_or_arn: # only a function name
128
+ if operation_type == "GetFunction" or operation_type == "Invoke":
129
+ arn_name_pattern = ARN_NAME_PATTERN_GET
130
+ elif operation_type == "CreateFunction":
131
+ # https://docs.aws.amazon.com/lambda/latest/api/API_CreateFunction.html#lambda-CreateFunction-request-FunctionName
132
+ if function_name == function_name_or_arn: # only a function name
128
133
  max_length = 64
129
- case "CreateFunction" | "DeleteFunction":
134
+ else: # full or partial ARN
130
135
  max_length = 140
136
+ elif operation_type == "DeleteFunction":
137
+ max_length = 140
138
+ arn_name_pattern = ARN_NAME_PATTERN_GET
131
139
 
132
140
  validations = []
133
- if len(function_name_or_arn) > max_length:
134
- constraint = f"Member must have length less than or equal to {max_length}"
141
+ if not AWS_FUNCTION_NAME_REGEX.match(function_name_or_arn) or not function_name:
142
+ constraint = f"Member must satisfy regular expression pattern: {arn_name_pattern}"
135
143
  validation_msg = f"Value '{function_name_or_arn}' at 'functionName' failed to satisfy constraint: {constraint}"
136
144
  validations.append(validation_msg)
145
+ if not operation_type == "CreateFunction":
146
+ # Immediately raises rather than summarizing all validations, except for CreateFunction
147
+ return validations
137
148
 
138
- if not AWS_FUNCTION_NAME_REGEX.match(function_name_or_arn) or not function_name:
139
- constraint = f"Member must satisfy regular expression pattern: {arn_name_pattern}"
149
+ if len(function_name_or_arn) > max_length:
150
+ constraint = f"Member must have length less than or equal to {max_length}"
140
151
  validation_msg = f"Value '{function_name_or_arn}' at 'functionName' failed to satisfy constraint: {constraint}"
141
152
  validations.append(validation_msg)
142
153
 
@@ -154,7 +165,7 @@ def validate_qualifier(qualifier: str):
154
165
  validations.append(validation_msg)
155
166
 
156
167
  if not QUALIFIER_REGEX.match(qualifier):
157
- constraint = "Member must satisfy regular expression pattern: (|[a-zA-Z0-9$_-]+)"
168
+ constraint = "Member must satisfy regular expression pattern: \\$(LATEST(\\.PUBLISHED)?)|[a-zA-Z0-9-_$]+"
158
169
  validation_msg = (
159
170
  f"Value '{qualifier}' at 'qualifier' failed to satisfy constraint: {constraint}"
160
171
  )
@@ -571,6 +582,12 @@ def map_config_out(
571
582
  optional_kwargs["CodeSize"] = 0
572
583
  optional_kwargs["CodeSha256"] = version.config.image.code_sha256
573
584
 
585
+ if version.config.CapacityProviderConfig:
586
+ optional_kwargs["CapacityProviderConfig"] = version.config.CapacityProviderConfig
587
+ data = json.dumps(version.config.CapacityProviderConfig, sort_keys=True).encode("utf-8")
588
+ config_sha_256 = hashlib.sha256(data).hexdigest()
589
+ optional_kwargs["ConfigSha256"] = config_sha_256
590
+
574
591
  # output for an alias qualifier is completely the same except for the returned ARN
575
592
  if alias_name:
576
593
  function_arn = f"{':'.join(version.id.qualified_arn().split(':')[:-1])}:{alias_name}"
@@ -36,7 +36,7 @@ from localstack.services.lambda_.event_source_mapping.senders.sender_utils impor
36
36
  )
37
37
  from localstack.utils.aws.arns import parse_arn, s3_bucket_name
38
38
  from localstack.utils.backoff import ExponentialBackoff
39
- from localstack.utils.batch_policy import Batcher
39
+ from localstack.utils.batching import Batcher
40
40
  from localstack.utils.strings import long_uid
41
41
 
42
42
  LOG = logging.getLogger(__name__)
@@ -88,9 +88,12 @@ class AssignmentService(OtherServiceEndpoint):
88
88
  self, version_manager_id: str, function_version: FunctionVersion
89
89
  ) -> ExecutionEnvironment:
90
90
  LOG.debug("Starting new environment")
91
+ initialization_type = "on-demand"
92
+ if function_version.config.CapacityProviderConfig:
93
+ initialization_type = "lambda-managed-instances"
91
94
  execution_environment = ExecutionEnvironment(
92
95
  function_version=function_version,
93
- initialization_type="on-demand",
96
+ initialization_type=initialization_type,
94
97
  on_timeout=self.on_timeout,
95
98
  version_manager_id=version_manager_id,
96
99
  )
@@ -8,6 +8,7 @@ from enum import Enum, auto
8
8
  from threading import RLock, Timer
9
9
 
10
10
  from localstack import config
11
+ from localstack.aws.api.lambda_ import LogFormat
11
12
  from localstack.aws.connect import connect_to
12
13
  from localstack.services.lambda_.invocation.lambda_models import (
13
14
  Credentials,
@@ -149,6 +150,22 @@ class ExecutionEnvironment:
149
150
  # LOCALSTACK_USER conditionally added below
150
151
  }
151
152
  # Conditionally added environment variables
153
+ # Lambda advanced logging controls:
154
+ # https://aws.amazon.com/blogs/compute/introducing-advanced-logging-controls-for-aws-lambda-functions/
155
+ logging_config = self.function_version.config.logging_config
156
+ if logging_config.get("LogFormat") == LogFormat.JSON:
157
+ env_vars["AWS_LAMBDA_LOG_FORMAT"] = logging_config["LogFormat"]
158
+ # TODO: test this (currently not implemented in LocalStack)
159
+ env_vars["AWS_LAMBDA_LOG_LEVEL"] = logging_config["ApplicationLogLevel"].capitalize()
160
+ # Lambda Managed Instances
161
+ if capacity_provider_config := self.function_version.config.CapacityProviderConfig:
162
+ # Disable dropping privileges for parity
163
+ # TODO: implement mixed permissions (maybe in RIE)
164
+ # env_vars["LOCALSTACK_USER"] = "root"
165
+ env_vars["AWS_LAMBDA_MAX_CONCURRENCY"] = capacity_provider_config[
166
+ "LambdaManagedInstancesCapacityProviderConfig"
167
+ ]["PerExecutionEnvironmentMaxConcurrency"]
168
+ env_vars["TZ"] = ":/etc/localtime"
152
169
  if not config.LAMBDA_DISABLE_AWS_ENDPOINT_URL:
153
170
  env_vars["AWS_ENDPOINT_URL"] = (
154
171
  f"http://{self.runtime_executor.get_endpoint_from_executor()}:{config.GATEWAY_LISTEN[0].port}"
@@ -159,8 +176,6 @@ class ExecutionEnvironment:
159
176
  # Will be overridden by the runtime itself unless it is a provided runtime
160
177
  if self.function_version.config.runtime:
161
178
  env_vars["AWS_EXECUTION_ENV"] = "AWS_Lambda_rapid"
162
- if self.function_version.config.environment:
163
- env_vars.update(self.function_version.config.environment)
164
179
  if config.LAMBDA_INIT_DEBUG:
165
180
  # Disable dropping privileges because it breaks debugging
166
181
  env_vars["LOCALSTACK_USER"] = "root"
@@ -175,6 +190,10 @@ class ExecutionEnvironment:
175
190
  env_vars["LOCALSTACK_MAX_PAYLOAD_SIZE"] = int(
176
191
  config.LAMBDA_LIMITS_MAX_FUNCTION_PAYLOAD_SIZE_BYTES
177
192
  )
193
+
194
+ # Let users overwrite any environment variable at last (if they want so)
195
+ if self.function_version.config.environment:
196
+ env_vars.update(self.function_version.config.environment)
178
197
  return env_vars
179
198
 
180
199
  # Lifecycle methods
@@ -12,7 +12,7 @@ import threading
12
12
  from abc import ABCMeta, abstractmethod
13
13
  from datetime import datetime
14
14
  from pathlib import Path
15
- from typing import IO, Literal, TypedDict
15
+ from typing import IO, Literal, Optional, TypedDict
16
16
 
17
17
  import boto3
18
18
  from botocore.exceptions import ClientError
@@ -22,12 +22,19 @@ from localstack.aws.api import CommonServiceException
22
22
  from localstack.aws.api.lambda_ import (
23
23
  AllowedPublishers,
24
24
  Architecture,
25
+ CapacityProviderArn,
26
+ CapacityProviderConfig,
27
+ CapacityProviderPermissionsConfig,
28
+ CapacityProviderScalingConfig,
29
+ CapacityProviderVpcConfig,
25
30
  CodeSigningPolicies,
26
31
  Cors,
27
32
  DestinationConfig,
28
33
  FunctionUrlAuthType,
34
+ InstanceRequirements,
29
35
  InvocationType,
30
36
  InvokeMode,
37
+ KMSKeyArn,
31
38
  LastUpdateStatus,
32
39
  LoggingConfig,
33
40
  PackageType,
@@ -38,12 +45,14 @@ from localstack.aws.api.lambda_ import (
38
45
  SnapStartResponse,
39
46
  State,
40
47
  StateReasonCode,
48
+ Timestamp,
41
49
  TracingMode,
42
50
  )
43
51
  from localstack.aws.connect import connect_to
44
52
  from localstack.constants import AWS_REGION_US_EAST_1, INTERNAL_AWS_SECRET_ACCESS_KEY
45
53
  from localstack.services.lambda_.api_utils import qualified_lambda_arn, unqualified_lambda_arn
46
54
  from localstack.utils.archives import unzip
55
+ from localstack.utils.files import chmod_r
47
56
  from localstack.utils.strings import long_uid, short_uid
48
57
 
49
58
  LOG = logging.getLogger(__name__)
@@ -212,6 +221,7 @@ class S3Code(ArchiveCode):
212
221
  with tempfile.NamedTemporaryFile() as file:
213
222
  self._download_archive_to_file(file)
214
223
  unzip(file.name, str(target_path))
224
+ chmod_r(str(target_path), 0o755)
215
225
 
216
226
  def destroy_cached(self) -> None:
217
227
  """
@@ -331,7 +341,7 @@ class VpcConfig:
331
341
  @dataclasses.dataclass(frozen=True)
332
342
  class UpdateStatus:
333
343
  status: LastUpdateStatus | None
334
- code: str | None = None # TODO: probably not a string
344
+ code: str | None = None
335
345
  reason: str | None = None
336
346
 
337
347
 
@@ -568,6 +578,9 @@ class VersionFunctionConfiguration:
568
578
  vpc_config: VpcConfig | None = None
569
579
 
570
580
  logging_config: LoggingConfig = dataclasses.field(default_factory=dict)
581
+ # TODO: why does `CapacityProviderConfig | None = None` fail with on Python 3.13.9:
582
+ # TypeError: unsupported operand type(s) for |: 'NoneType' and 'NoneType'
583
+ CapacityProviderConfig: Optional[CapacityProviderConfig] = None # noqa
571
584
 
572
585
 
573
586
  @dataclasses.dataclass(frozen=True)
@@ -580,6 +593,18 @@ class FunctionVersion:
580
593
  return self.id.qualified_arn()
581
594
 
582
595
 
596
+ @dataclasses.dataclass
597
+ class CapacityProvider:
598
+ CapacityProviderArn: CapacityProviderArn
599
+ # State is determined dynamically
600
+ VpcConfig: CapacityProviderVpcConfig
601
+ PermissionsConfig: CapacityProviderPermissionsConfig
602
+ InstanceRequirements: InstanceRequirements
603
+ CapacityProviderScalingConfig: CapacityProviderScalingConfig
604
+ LastModified: Timestamp
605
+ KmsKeyArn: KMSKeyArn | None = None
606
+
607
+
583
608
  @dataclasses.dataclass
584
609
  class Function:
585
610
  function_name: str
@@ -5,6 +5,7 @@ import io
5
5
  import logging
6
6
  import os.path
7
7
  import random
8
+ import time
8
9
  import uuid
9
10
  from concurrent.futures import Executor, Future, ThreadPoolExecutor
10
11
  from datetime import datetime
@@ -19,6 +20,7 @@ from localstack.aws.api.lambda_ import (
19
20
  InvalidRequestContentException,
20
21
  InvocationType,
21
22
  LastUpdateStatus,
23
+ NoPublishedVersionException,
22
24
  ResourceConflictException,
23
25
  ResourceNotFoundException,
24
26
  State,
@@ -190,6 +192,9 @@ class LambdaService:
190
192
  lambda_hooks.create_function_version.run(function_version.qualified_arn)
191
193
  return self.task_executor.submit(self._start_lambda_version, version_manager)
192
194
 
195
+ def publish_version_async(self, function_version: FunctionVersion):
196
+ self.task_executor.submit(self.publish_version, function_version)
197
+
193
198
  def publish_version(self, function_version: FunctionVersion):
194
199
  """
195
200
  Synchronously create a function version (manager)
@@ -200,6 +205,14 @@ class LambdaService:
200
205
 
201
206
  :param function_version: Function Version to create
202
207
  """
208
+ # HACK: trying to match the AWS timing behavior of Lambda Managed Instances for the operation
209
+ # publish_version followed by get_function because transitioning LastUpdateStatus from InProgress to
210
+ # Successful happens too fast on LocalStack (thanks to caching in prepare_version).
211
+ # Without this hack, test_latest_published_update_config fails at get_function_response_postpublish
212
+ # and test_lifecycle_invoke is flaky, sometimes not triggering the ResourceConflictException
213
+ # Increasing this sleep too much (e.g., 10s) shouldn't cause any side effects apart from slow responsiveness
214
+ if function_version.config.CapacityProviderConfig:
215
+ time.sleep(0.1)
203
216
  with self.lambda_version_manager_lock:
204
217
  qualified_arn = function_version.id.qualified_arn()
205
218
  version_manager = self.lambda_starting_versions.get(qualified_arn)
@@ -225,7 +238,7 @@ class LambdaService:
225
238
  def invoke(
226
239
  self,
227
240
  function_name: str,
228
- qualifier: str,
241
+ qualifier: str | None,
229
242
  region: str,
230
243
  account_id: str,
231
244
  invocation_type: InvocationType | None,
@@ -258,13 +271,27 @@ class LambdaService:
258
271
  account=account_id,
259
272
  region=region,
260
273
  )
261
- qualifier = qualifier or "$LATEST"
262
274
  state = lambda_stores[account_id][region]
263
275
  function = state.functions.get(function_name)
264
276
 
265
277
  if function is None:
278
+ if not qualifier:
279
+ invoked_arn += ":$LATEST"
266
280
  raise ResourceNotFoundException(f"Function not found: {invoked_arn}", Type="User")
267
281
 
282
+ # A provided qualifier always takes precedence, but the default depends on whether $LATEST.PUBLISHED exists
283
+ version_latest_published = function.versions.get("$LATEST.PUBLISHED")
284
+ if version_latest_published:
285
+ qualifier = qualifier or "$LATEST.PUBLISHED"
286
+ invoked_arn = lambda_arn(
287
+ function_name=function_name,
288
+ qualifier=qualifier,
289
+ account=account_id,
290
+ region=region,
291
+ )
292
+ else:
293
+ qualifier = qualifier or "$LATEST"
294
+
268
295
  if qualifier_is_alias(qualifier):
269
296
  alias = function.aliases.get(qualifier)
270
297
  if not alias:
@@ -282,8 +309,21 @@ class LambdaService:
282
309
  # Need the qualified arn to exactly get the target lambda
283
310
  qualified_arn = qualified_lambda_arn(function_name, version_qualifier, account_id, region)
284
311
  version = function.versions.get(version_qualifier)
312
+ if version is None:
313
+ raise ResourceNotFoundException(f"Function not found: {invoked_arn}", Type="User")
285
314
  runtime = version.config.runtime or "n/a"
286
315
  package_type = version.config.package_type
316
+ if version.config.CapacityProviderConfig and qualifier == "$LATEST":
317
+ if function.versions.get("$LATEST.PUBLISHED"):
318
+ raise InvalidParameterValueException(
319
+ "Functions configured with capacity provider configuration can't be invoked with $LATEST qualifier. To invoke this function, specify a published version qualifier or $LATEST.PUBLISHED.",
320
+ Type="User",
321
+ )
322
+ else:
323
+ raise NoPublishedVersionException(
324
+ "The function can't be invoked because no published version exists. For functions with capacity provider configuration, either publish a version to $LATEST.PUBLISHED, or specify a published version qualifier.",
325
+ Type="User",
326
+ )
287
327
  try:
288
328
  version_manager = self.get_lambda_version_manager(qualified_arn)
289
329
  event_manager = self.get_lambda_event_manager(qualified_arn)
@@ -393,7 +433,10 @@ class LambdaService:
393
433
 
394
434
  :param new_version: New version (with the same qualifier as an older one)
395
435
  """
396
- if new_version.qualified_arn not in self.lambda_running_versions:
436
+ if (
437
+ new_version.qualified_arn not in self.lambda_running_versions
438
+ and not new_version.config.CapacityProviderConfig
439
+ ):
397
440
  raise ValueError(
398
441
  f"Version {new_version.qualified_arn} cannot be updated if an old one is not running"
399
442
  )
@@ -437,6 +480,11 @@ class LambdaService:
437
480
  elif new_state.state == State.Failed:
438
481
  update_status = UpdateStatus(status=LastUpdateStatus.Failed)
439
482
  self.task_executor.submit(new_version_manager.stop)
483
+ elif (
484
+ new_state.state == State.ActiveNonInvocable
485
+ and function_version.config.CapacityProviderConfig
486
+ ):
487
+ update_status = UpdateStatus(status=LastUpdateStatus.Successful)
440
488
  else:
441
489
  # TODO what to do if state pending or inactive is supported?
442
490
  self.task_executor.submit(new_version_manager.stop)
@@ -1,5 +1,10 @@
1
1
  from localstack.aws.api.lambda_ import EventSourceMappingConfiguration
2
- from localstack.services.lambda_.invocation.lambda_models import CodeSigningConfig, Function, Layer
2
+ from localstack.services.lambda_.invocation.lambda_models import (
3
+ CapacityProvider,
4
+ CodeSigningConfig,
5
+ Function,
6
+ Layer,
7
+ )
3
8
  from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
4
9
  from localstack.utils.tagging import TaggingService
5
10
 
@@ -17,6 +22,9 @@ class LambdaStore(BaseStore):
17
22
  # maps layer names to Layers
18
23
  layers: dict[str, Layer] = LocalAttribute(default=dict)
19
24
 
25
+ # maps capacity provider names to respective CapacityProvider
26
+ capacity_providers: dict[str, CapacityProvider] = LocalAttribute(default=dict)
27
+
20
28
  # maps resource ARNs for EventSourceMappings and CodeSigningConfiguration to tags
21
29
  TAGS = LocalAttribute(default=TaggingService)
22
30
 
@@ -100,11 +100,26 @@ class LambdaVersionManager:
100
100
 
101
101
  # code and reason not set for success scenario because only failed states provide this field:
102
102
  # https://docs.aws.amazon.com/lambda/latest/dg/API_GetFunctionConfiguration.html#SSS-GetFunctionConfiguration-response-LastUpdateStatusReasonCode
103
- self.state = VersionState(state=State.Active)
103
+ new_state = State.Active
104
+ if (
105
+ self.function_version.config.CapacityProviderConfig
106
+ and self.function_version.id.qualifier == "$LATEST"
107
+ ):
108
+ new_state = State.ActiveNonInvocable
109
+ # HACK: trying to match the AWS timing behavior of Lambda Managed Instances for the operation
110
+ # update_function_configuration followed by get_function because transitioning LastUpdateStatus from
111
+ # InProgress to Successful happens too fast on LocalStack (thanks to caching in prepare_version).
112
+ # Without this hack, test_latest_published_update_config fails at get_function_response_postupdate_latest
113
+ # TODO: this sleep has side-effects and we should be looking into alternatives
114
+ # Increasing this sleep too much (e.g., 3s) could cause the side effect that a created function is not
115
+ # ready for updates (i.e., rejected with a ResourceConflictException) and failing other tests
116
+ # time.sleep(0.1)
117
+ self.state = VersionState(state=new_state)
104
118
  LOG.debug(
105
- "Changing Lambda %s (id %s) to active",
119
+ "Changing Lambda %s (id %s) to %s",
106
120
  self.function_arn,
107
121
  self.function_version.config.internal_revision,
122
+ new_state,
108
123
  )
109
124
  except Exception as e:
110
125
  self.state = VersionState(
@@ -113,7 +128,7 @@ class LambdaVersionManager:
113
128
  reason=f"Error while creating lambda: {e}",
114
129
  )
115
130
  LOG.debug(
116
- "Changing Lambda %s (id %s) to failed. Reason: %s",
131
+ "Changing Lambda %s (id %s) to Failed. Reason: %s",
117
132
  self.function_arn,
118
133
  self.function_version.config.internal_revision,
119
134
  e,