• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 21891233725

10 Feb 2026 10:59PM UTC coverage: 86.883% (+0.01%) from 86.871%
21891233725

push

github

web-flow
SFN: Fix Local mock iterations for states without retry (#13693)

Co-authored-by: Hernan <hernaner28@gmail.com>

The Step Functions Local mock configuration was using RetryCount to index numbered mock responses. In case of successful invocations or states without retry configuration this caused all invocations to return the same response ("0") instead of iterating through the sequence ("0", "1", "2", etc.).

RetryCount only increments on actual retry attempts (failures), not on successful invocations. This made the mock iteration feature unusable for testing state transition scenarios with multiple invocations of the same state.

- Added next_local_mock_invocation_number dict to execution environment to track mock invocations.
- Modified get_current_local_mocked_response() to use next_local_mock_invocation_number instead of retry_count.
- Shared next_local_mock_invocation_number between parent and child frames to maintain consistent counting across execution context

An existing `aws.services.stepfunctions.v2.local_mocking.test_base_scenarios.TestBaseScenarios.test_map_state_lambda test` was giving a false positive. It used a mock response with only one mocked invocation and should have failed when 2 invocations were done. Effectively, it started to fail after the fix and mocked response has been adjusted.

Also, adds `test_numbered_mock_responses_multiple_success_invocations` that tests multiple success invocations in a regular top-level state, outside of map configuration.

7 of 7 new or added lines in 1 file covered. (100.0%)

255 existing lines in 12 files now uncovered.

69977 of 80542 relevant lines covered (86.88%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.14
/localstack-core/localstack/services/lambda_/provider.py
1
import base64
1✔
2
import dataclasses
1✔
3
import datetime
1✔
4
import itertools
1✔
5
import json
1✔
6
import logging
1✔
7
import re
1✔
8
import threading
1✔
9
import time
1✔
10
from typing import IO, Any
1✔
11

12
from botocore.exceptions import ClientError
1✔
13

14
from localstack import config
1✔
15
from localstack.aws.api import RequestContext, ServiceException, handler
1✔
16
from localstack.aws.api.lambda_ import (
1✔
17
    AccountLimit,
18
    AccountUsage,
19
    AddLayerVersionPermissionResponse,
20
    AddPermissionRequest,
21
    AddPermissionResponse,
22
    Alias,
23
    AliasConfiguration,
24
    AliasRoutingConfiguration,
25
    AllowedPublishers,
26
    Architecture,
27
    Arn,
28
    Blob,
29
    BlobStream,
30
    CapacityProviderConfig,
31
    CodeSigningConfigArn,
32
    CodeSigningConfigNotFoundException,
33
    CodeSigningPolicies,
34
    CompatibleArchitectures,
35
    CompatibleRuntimes,
36
    Concurrency,
37
    Cors,
38
    CreateCodeSigningConfigResponse,
39
    CreateEventSourceMappingRequest,
40
    CreateFunctionRequest,
41
    CreateFunctionUrlConfigResponse,
42
    DeleteCodeSigningConfigResponse,
43
    DeleteFunctionResponse,
44
    Description,
45
    DestinationConfig,
46
    DurableExecutionName,
47
    EventSourceMappingConfiguration,
48
    FunctionCodeLocation,
49
    FunctionConfiguration,
50
    FunctionEventInvokeConfig,
51
    FunctionName,
52
    FunctionUrlAuthType,
53
    FunctionUrlQualifier,
54
    FunctionVersionLatestPublished,
55
    GetAccountSettingsResponse,
56
    GetCodeSigningConfigResponse,
57
    GetFunctionCodeSigningConfigResponse,
58
    GetFunctionConcurrencyResponse,
59
    GetFunctionRecursionConfigResponse,
60
    GetFunctionResponse,
61
    GetFunctionUrlConfigResponse,
62
    GetLayerVersionPolicyResponse,
63
    GetLayerVersionResponse,
64
    GetPolicyResponse,
65
    GetProvisionedConcurrencyConfigResponse,
66
    InvalidParameterValueException,
67
    InvocationResponse,
68
    InvocationType,
69
    InvokeAsyncResponse,
70
    InvokeMode,
71
    LambdaApi,
72
    LambdaManagedInstancesCapacityProviderConfig,
73
    LastUpdateStatus,
74
    LayerName,
75
    LayerPermissionAllowedAction,
76
    LayerPermissionAllowedPrincipal,
77
    LayersListItem,
78
    LayerVersionArn,
79
    LayerVersionContentInput,
80
    LayerVersionNumber,
81
    LicenseInfo,
82
    ListAliasesResponse,
83
    ListCodeSigningConfigsResponse,
84
    ListEventSourceMappingsResponse,
85
    ListFunctionEventInvokeConfigsResponse,
86
    ListFunctionsByCodeSigningConfigResponse,
87
    ListFunctionsResponse,
88
    ListFunctionUrlConfigsResponse,
89
    ListLayersResponse,
90
    ListLayerVersionsResponse,
91
    ListProvisionedConcurrencyConfigsResponse,
92
    ListTagsResponse,
93
    ListVersionsByFunctionResponse,
94
    LogFormat,
95
    LoggingConfig,
96
    LogType,
97
    MasterRegion,
98
    MaxFunctionEventInvokeConfigListItems,
99
    MaximumEventAgeInSeconds,
100
    MaximumRetryAttempts,
101
    MaxItems,
102
    MaxLayerListItems,
103
    MaxListItems,
104
    MaxProvisionedConcurrencyConfigListItems,
105
    NamespacedFunctionName,
106
    NamespacedStatementId,
107
    NumericLatestPublishedOrAliasQualifier,
108
    OnFailure,
109
    OnSuccess,
110
    OrganizationId,
111
    PackageType,
112
    PositiveInteger,
113
    PreconditionFailedException,
114
    ProvisionedConcurrencyConfigListItem,
115
    ProvisionedConcurrencyConfigNotFoundException,
116
    ProvisionedConcurrencyStatusEnum,
117
    PublishLayerVersionResponse,
118
    PutFunctionCodeSigningConfigResponse,
119
    PutFunctionRecursionConfigResponse,
120
    PutProvisionedConcurrencyConfigResponse,
121
    Qualifier,
122
    RecursiveLoop,
123
    ReservedConcurrentExecutions,
124
    ResourceConflictException,
125
    ResourceNotFoundException,
126
    Runtime,
127
    RuntimeVersionConfig,
128
    SnapStart,
129
    SnapStartApplyOn,
130
    SnapStartOptimizationStatus,
131
    SnapStartResponse,
132
    State,
133
    StatementId,
134
    StateReasonCode,
135
    String,
136
    TaggableResource,
137
    TagKeyList,
138
    Tags,
139
    TenantId,
140
    TracingMode,
141
    UnqualifiedFunctionName,
142
    UpdateCodeSigningConfigResponse,
143
    UpdateEventSourceMappingRequest,
144
    UpdateFunctionCodeRequest,
145
    UpdateFunctionConfigurationRequest,
146
    UpdateFunctionUrlConfigResponse,
147
    VersionWithLatestPublished,
148
)
149
from localstack.aws.api.lambda_ import FunctionVersion as FunctionVersionApi
1✔
150
from localstack.aws.api.lambda_ import ServiceException as LambdaServiceException
1✔
151
from localstack.aws.api.pipes import (
1✔
152
    DynamoDBStreamStartPosition,
153
    KinesisStreamStartPosition,
154
)
155
from localstack.aws.connect import connect_to
1✔
156
from localstack.aws.spec import load_service
1✔
157
from localstack.services.edge import ROUTER
1✔
158
from localstack.services.lambda_ import api_utils
1✔
159
from localstack.services.lambda_ import hooks as lambda_hooks
1✔
160
from localstack.services.lambda_.analytics import (
1✔
161
    FunctionInitializationType,
162
    FunctionOperation,
163
    FunctionStatus,
164
    function_counter,
165
)
166
from localstack.services.lambda_.api_utils import (
1✔
167
    ARCHITECTURES,
168
    STATEMENT_ID_REGEX,
169
    SUBNET_ID_REGEX,
170
    function_locators_from_arn,
171
)
172
from localstack.services.lambda_.event_source_mapping.esm_config_factory import (
1✔
173
    EsmConfigFactory,
174
)
175
from localstack.services.lambda_.event_source_mapping.esm_worker import (
1✔
176
    EsmState,
177
    EsmWorker,
178
)
179
from localstack.services.lambda_.event_source_mapping.esm_worker_factory import (
1✔
180
    EsmWorkerFactory,
181
)
182
from localstack.services.lambda_.event_source_mapping.pipe_utils import get_internal_client
1✔
183
from localstack.services.lambda_.invocation import AccessDeniedException
1✔
184
from localstack.services.lambda_.invocation.execution_environment import (
1✔
185
    EnvironmentStartupTimeoutException,
186
)
187
from localstack.services.lambda_.invocation.lambda_models import (
1✔
188
    AliasRoutingConfig,
189
    CodeSigningConfig,
190
    EventInvokeConfig,
191
    Function,
192
    FunctionResourcePolicy,
193
    FunctionUrlConfig,
194
    FunctionVersion,
195
    ImageConfig,
196
    LambdaEphemeralStorage,
197
    Layer,
198
    LayerPolicy,
199
    LayerPolicyStatement,
200
    LayerVersion,
201
    ProvisionedConcurrencyConfiguration,
202
    RequestEntityTooLargeException,
203
    ResourcePolicy,
204
    UpdateStatus,
205
    ValidationException,
206
    VersionAlias,
207
    VersionFunctionConfiguration,
208
    VersionIdentifier,
209
    VersionState,
210
    VpcConfig,
211
)
212
from localstack.services.lambda_.invocation.lambda_service import (
1✔
213
    LambdaService,
214
    create_image_code,
215
    destroy_code_if_not_used,
216
    lambda_stores,
217
    store_lambda_archive,
218
    store_s3_bucket_archive,
219
)
220
from localstack.services.lambda_.invocation.models import CapacityProvider as CapacityProviderModel
1✔
221
from localstack.services.lambda_.invocation.models import LambdaStore
1✔
222
from localstack.services.lambda_.invocation.runtime_executor import get_runtime_executor
1✔
223
from localstack.services.lambda_.lambda_utils import HINT_LOG
1✔
224
from localstack.services.lambda_.layerfetcher.layer_fetcher import LayerFetcher
1✔
225
from localstack.services.lambda_.provider_utils import (
1✔
226
    LambdaLayerVersionIdentifier,
227
    get_function_version,
228
    get_function_version_from_arn,
229
)
230
from localstack.services.lambda_.runtimes import (
1✔
231
    ALL_RUNTIMES,
232
    DEPRECATED_RUNTIMES,
233
    DEPRECATED_RUNTIMES_UPGRADES,
234
    RUNTIMES_AGGREGATED,
235
    SNAP_START_SUPPORTED_RUNTIMES,
236
    VALID_MANAGED_INSTANCE_RUNTIMES,
237
    VALID_RUNTIMES,
238
)
239
from localstack.services.lambda_.urlrouter import FunctionUrlRouter
1✔
240
from localstack.services.plugins import ServiceLifecycleHook
1✔
241
from localstack.state import StateVisitor
1✔
242
from localstack.utils.aws.arns import (
1✔
243
    ArnData,
244
    capacity_provider_arn,
245
    extract_resource_from_arn,
246
    extract_service_from_arn,
247
    get_partition,
248
    lambda_event_source_mapping_arn,
249
    parse_arn,
250
)
251
from localstack.utils.aws.client_types import ServicePrincipal
1✔
252
from localstack.utils.bootstrap import is_api_enabled
1✔
253
from localstack.utils.collections import PaginatedList, merge_recursive
1✔
254
from localstack.utils.event_matcher import validate_event_pattern
1✔
255
from localstack.utils.strings import get_random_hex, short_uid, to_bytes, to_str
1✔
256
from localstack.utils.sync import poll_condition
1✔
257
from localstack.utils.urls import localstack_host
1✔
258

259
LOG = logging.getLogger(__name__)
1✔
260

261
CAPACITY_PROVIDER_ARN_NAME = "arn:aws[a-zA-Z-]*:lambda:(eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:\\d{12}:capacity-provider:[a-zA-Z0-9-_]+"
1✔
262
LAMBDA_DEFAULT_TIMEOUT = 3
1✔
263
LAMBDA_DEFAULT_MEMORY_SIZE = 128
1✔
264

265
LAMBDA_TAG_LIMIT_PER_RESOURCE = 50
1✔
266
LAMBDA_LAYERS_LIMIT_PER_FUNCTION = 5
1✔
267

268
TAG_KEY_CUSTOM_URL = "_custom_id_"
1✔
269
# Requirements (from RFC3986 & co): not longer than 63, first char must be
270
# alpha, then alphanumeric or hyphen, except cannot start or end with hyphen
271
TAG_KEY_CUSTOM_URL_VALIDATOR = re.compile(r"^[A-Za-z]([A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$")
1✔
272

273

274
class LambdaProvider(LambdaApi, ServiceLifecycleHook):
1✔
275
    lambda_service: LambdaService
1✔
276
    create_fn_lock: threading.RLock
1✔
277
    create_layer_lock: threading.RLock
1✔
278
    router: FunctionUrlRouter
1✔
279
    esm_workers: dict[str, EsmWorker]
1✔
280
    layer_fetcher: LayerFetcher | None
1✔
281

282
    def __init__(self) -> None:
1✔
283
        self.lambda_service = LambdaService()
1✔
284
        self.create_fn_lock = threading.RLock()
1✔
285
        self.create_layer_lock = threading.RLock()
1✔
286
        self.router = FunctionUrlRouter(ROUTER, self.lambda_service)
1✔
287
        self.esm_workers = {}
1✔
288
        self.layer_fetcher = None
1✔
289
        lambda_hooks.inject_layer_fetcher.run(self)
1✔
290

291
    def accept_state_visitor(self, visitor: StateVisitor):
1✔
292
        visitor.visit(lambda_stores)
×
293

294
    def on_before_state_reset(self):
1✔
295
        for esm_worker in self.esm_workers.values():
×
296
            esm_worker.stop_for_shutdown()
×
297
        self.esm_workers = {}
×
298
        self.lambda_service.stop()
×
299

300
    def on_after_state_reset(self):
1✔
301
        self.router.lambda_service = self.lambda_service = LambdaService()
×
302

303
    def on_before_state_load(self):
1✔
304
        self.lambda_service.stop()
×
305

306
    def on_after_state_load(self):
1✔
307
        self.lambda_service = LambdaService()
×
308
        self.router.lambda_service = self.lambda_service
×
309

310
        for account_id, account_bundle in lambda_stores.items():
×
311
            for region_name, state in account_bundle.items():
×
312
                for fn in state.functions.values():
×
313
                    # HACK to model a volatile variable that should be ignored for persistence
314
                    # Identifier unique to this function and LocalStack instance.
315
                    # A LocalStack restart or persistence load should create a new instance id.
316
                    # Used for retaining invoke queues across version updates for $LATEST, but
317
                    # separate unrelated instances.
UNCOV
318
                    fn.instance_id = short_uid()
×
319

UNCOV
320
                    for fn_version in fn.versions.values():
×
321
                        try:
×
322
                            # $LATEST is not invokable for Lambda functions with a capacity provider
323
                            # and has a different State (i.e., ActiveNonInvokable)
UNCOV
324
                            is_capacity_provider_latest = (
×
325
                                fn_version.config.capacity_provider_config
326
                                and fn_version.id.qualifier == "$LATEST"
327
                            )
328
                            if not is_capacity_provider_latest:
×
329
                                # Restore the "Pending" state for the function version and start it
330
                                new_state = VersionState(
×
331
                                    state=State.Pending,
332
                                    code=StateReasonCode.Creating,
333
                                    reason="The function is being created.",
334
                                )
335
                                new_config = dataclasses.replace(fn_version.config, state=new_state)
×
UNCOV
336
                                new_version = dataclasses.replace(fn_version, config=new_config)
×
UNCOV
337
                                fn.versions[fn_version.id.qualifier] = new_version
×
UNCOV
338
                                self.lambda_service.create_function_version(fn_version).result(
×
339
                                    timeout=5
340
                                )
341
                        except Exception:
×
UNCOV
342
                            LOG.warning(
×
343
                                "Failed to restore function version %s",
344
                                fn_version.id.qualified_arn(),
345
                                exc_info=LOG.isEnabledFor(logging.DEBUG),
346
                            )
347
                    # restore provisioned concurrency per function considering both versions and aliases
348
                    for (
×
349
                        provisioned_qualifier,
350
                        provisioned_config,
351
                    ) in fn.provisioned_concurrency_configs.items():
352
                        fn_arn = None
×
353
                        try:
×
UNCOV
354
                            if api_utils.qualifier_is_alias(provisioned_qualifier):
×
355
                                alias = fn.aliases.get(provisioned_qualifier)
×
UNCOV
356
                                resolved_version = fn.versions.get(alias.function_version)
×
UNCOV
357
                                fn_arn = resolved_version.id.qualified_arn()
×
UNCOV
358
                            elif api_utils.qualifier_is_version(provisioned_qualifier):
×
UNCOV
359
                                fn_version = fn.versions.get(provisioned_qualifier)
×
360
                                fn_arn = fn_version.id.qualified_arn()
×
361
                            else:
UNCOV
362
                                raise InvalidParameterValueException(
×
363
                                    "Invalid qualifier type:"
364
                                    " Qualifier can only be an alias or a version for provisioned concurrency."
365
                                )
366

UNCOV
367
                            manager = self.lambda_service.get_lambda_version_manager(fn_arn)
×
UNCOV
368
                            manager.update_provisioned_concurrency_config(
×
369
                                provisioned_config.provisioned_concurrent_executions
370
                            )
UNCOV
371
                        except Exception:
×
372
                            LOG.warning(
×
373
                                "Failed to restore provisioned concurrency %s for function %s",
374
                                provisioned_config,
375
                                fn_arn,
376
                                exc_info=LOG.isEnabledFor(logging.DEBUG),
377
                            )
378

UNCOV
379
                for esm in state.event_source_mappings.values():
×
380
                    # Restores event source workers
UNCOV
381
                    function_arn = esm.get("FunctionArn")
×
382

383
                    # TODO: How do we know the event source is up?
384
                    # A basic poll to see if the mapped Lambda function is active/failed
UNCOV
385
                    if not poll_condition(
×
386
                        lambda: get_function_version_from_arn(function_arn).config.state.state
387
                        in [State.Active, State.Failed],
388
                        timeout=10,
389
                    ):
UNCOV
390
                        LOG.warning(
×
391
                            "Creating ESM for Lambda that is not in running state: %s",
392
                            function_arn,
393
                        )
394

395
                    function_version = get_function_version_from_arn(function_arn)
×
UNCOV
396
                    function_role = function_version.config.role
×
397

UNCOV
398
                    is_esm_enabled = esm.get("State", EsmState.DISABLED) not in (
×
399
                        EsmState.DISABLED,
400
                        EsmState.DISABLING,
401
                    )
UNCOV
402
                    esm_worker = EsmWorkerFactory(
×
403
                        esm, function_role, is_esm_enabled
404
                    ).get_esm_worker()
405

406
                    # Note: a worker is created in the DISABLED state if not enabled
UNCOV
407
                    esm_worker.create()
×
408
                    # TODO: assigning the esm_worker to the dict only works after .create(). Could it cause a race
409
                    #  condition if we get a shutdown here and have a worker thread spawned but not accounted for?
UNCOV
410
                    self.esm_workers[esm_worker.uuid] = esm_worker
×
411

412
    def on_after_init(self):
1✔
413
        self.router.register_routes()
1✔
414
        get_runtime_executor().validate_environment()
1✔
415

416
    def on_before_stop(self) -> None:
1✔
417
        for esm_worker in self.esm_workers.values():
1✔
418
            esm_worker.stop_for_shutdown()
1✔
419

420
        # TODO: should probably unregister routes?
421
        self.lambda_service.stop()
1✔
422

423
    @staticmethod
1✔
424
    def _get_function(function_name: str, account_id: str, region: str) -> Function:
1✔
425
        state = lambda_stores[account_id][region]
1✔
426
        function = state.functions.get(function_name)
1✔
427
        if not function:
1✔
428
            arn = api_utils.unqualified_lambda_arn(
1✔
429
                function_name=function_name,
430
                account=account_id,
431
                region=region,
432
            )
433
            raise ResourceNotFoundException(
1✔
434
                f"Function not found: {arn}",
435
                Type="User",
436
            )
437
        return function
1✔
438

439
    @staticmethod
1✔
440
    def _get_esm(uuid: str, account_id: str, region: str) -> EventSourceMappingConfiguration:
1✔
441
        state = lambda_stores[account_id][region]
1✔
442
        esm = state.event_source_mappings.get(uuid)
1✔
443
        if not esm:
1✔
444
            arn = lambda_event_source_mapping_arn(uuid, account_id, region)
1✔
445
            raise ResourceNotFoundException(
1✔
446
                f"Event source mapping not found: {arn}",
447
                Type="User",
448
            )
449
        return esm
1✔
450

451
    @staticmethod
1✔
452
    def _get_capacity_provider(
1✔
453
        capacity_provider_name: str,
454
        account_id: str,
455
        region: str,
456
        error_msg_template: str = "Capacity provider not found: {}",
457
    ) -> CapacityProviderModel:
458
        state = lambda_stores[account_id][region]
1✔
459
        cp = state.capacity_providers.get(capacity_provider_name)
1✔
460
        if not cp:
1✔
461
            arn = capacity_provider_arn(capacity_provider_name, account_id, region)
1✔
462
            raise ResourceNotFoundException(
1✔
463
                error_msg_template.format(arn),
464
                Type="User",
465
            )
UNCOV
466
        return cp
×
467

468
    @staticmethod
1✔
469
    def _validate_qualifier_expression(qualifier: str) -> None:
1✔
470
        if error_messages := api_utils.validate_qualifier(qualifier):
1✔
471
            raise ValidationException(
×
472
                message=api_utils.construct_validation_exception_message(error_messages)
473
            )
474

475
    @staticmethod
1✔
476
    def _validate_publish_to(publish_to: str):
1✔
UNCOV
477
        if publish_to != FunctionVersionLatestPublished.LATEST_PUBLISHED:
×
UNCOV
478
            raise ValidationException(
×
479
                message=f"1 validation error detected: Value '{publish_to}' at 'publishTo' failed to satisfy constraint: Member must satisfy enum value set: [LATEST_PUBLISHED]"
480
            )
481

482
    @staticmethod
1✔
483
    def _resolve_fn_qualifier(resolved_fn: Function, qualifier: str | None) -> tuple[str, str]:
1✔
484
        """Attempts to resolve a given qualifier and returns a qualifier that exists or
485
        raises an appropriate ResourceNotFoundException.
486

487
        :param resolved_fn: The resolved lambda function
488
        :param qualifier: The qualifier to be resolved or None
489
        :return: Tuple of (resolved qualifier, function arn either qualified or unqualified)"""
490
        function_name = resolved_fn.function_name
1✔
491
        # assuming function versions need to live in the same account and region
492
        account_id = resolved_fn.latest().id.account
1✔
493
        region = resolved_fn.latest().id.region
1✔
494
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
495
        if qualifier is not None:
1✔
496
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
497
            if api_utils.qualifier_is_alias(qualifier):
1✔
498
                if qualifier not in resolved_fn.aliases:
1✔
499
                    raise ResourceNotFoundException(f"Cannot find alias arn: {fn_arn}", Type="User")
1✔
500
            elif api_utils.qualifier_is_version(qualifier) or qualifier == "$LATEST":
1✔
501
                if qualifier not in resolved_fn.versions:
1✔
502
                    raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
503
            else:
504
                # matches qualifier pattern but invalid alias or version
505
                raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
506
        resolved_qualifier = qualifier or "$LATEST"
1✔
507
        return resolved_qualifier, fn_arn
1✔
508

509
    @staticmethod
1✔
510
    def _function_revision_id(resolved_fn: Function, resolved_qualifier: str) -> str:
1✔
511
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
512
            return resolved_fn.aliases[resolved_qualifier].revision_id
1✔
513
        # Assumes that a non-alias is a version
514
        else:
515
            return resolved_fn.versions[resolved_qualifier].config.revision_id
1✔
516

517
    def _resolve_vpc_id(self, account_id: str, region_name: str, subnet_id: str) -> str:
1✔
518
        ec2_client = connect_to(aws_access_key_id=account_id, region_name=region_name).ec2
1✔
519
        try:
1✔
520
            return ec2_client.describe_subnets(SubnetIds=[subnet_id])["Subnets"][0]["VpcId"]
1✔
521
        except ec2_client.exceptions.ClientError as e:
1✔
522
            code = e.response["Error"]["Code"]
1✔
523
            message = e.response["Error"]["Message"]
1✔
524
            raise InvalidParameterValueException(
1✔
525
                f"Error occurred while DescribeSubnets. EC2 Error Code: {code}. EC2 Error Message: {message}",
526
                Type="User",
527
            )
528

529
    def _build_vpc_config(
1✔
530
        self,
531
        account_id: str,
532
        region_name: str,
533
        vpc_config: dict | None = None,
534
    ) -> VpcConfig | None:
535
        if not vpc_config or not is_api_enabled("ec2"):
1✔
536
            return None
1✔
537

538
        subnet_ids = vpc_config.get("SubnetIds", [])
1✔
539
        if subnet_ids is not None and len(subnet_ids) == 0:
1✔
540
            return VpcConfig(vpc_id="", security_group_ids=[], subnet_ids=[])
1✔
541

542
        subnet_id = subnet_ids[0]
1✔
543
        if not bool(SUBNET_ID_REGEX.match(subnet_id)):
1✔
544
            raise ValidationException(
1✔
545
                f"1 validation error detected: Value '[{subnet_id}]' at 'vpcConfig.subnetIds' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 1024, Member must have length greater than or equal to 0, Member must satisfy regular expression pattern: subnet-[0-9a-z]*]"
546
            )
547

548
        return VpcConfig(
1✔
549
            vpc_id=self._resolve_vpc_id(account_id, region_name, subnet_id),
550
            security_group_ids=vpc_config.get("SecurityGroupIds", []),
551
            subnet_ids=subnet_ids,
552
        )
553

554
    def _create_version_model(
1✔
555
        self,
556
        function_name: str,
557
        region: str,
558
        account_id: str,
559
        description: str | None = None,
560
        revision_id: str | None = None,
561
        code_sha256: str | None = None,
562
        publish_to: FunctionVersionLatestPublished | None = None,
563
        is_active: bool = False,
564
    ) -> tuple[FunctionVersion, bool]:
565
        """
566
        Release a new version to the model if all restrictions are met.
567
        Restrictions:
568
          - CodeSha256, if provided, must equal the current latest version code hash
569
          - RevisionId, if provided, must equal the current latest version revision id
570
          - Some changes have been done to the latest version since last publish
571
        Will return a tuple of the version, and whether the version was published (True) or the latest available version was taken (False).
572
        This can happen if the latest version has not been changed since the last version publish, in this case the last version will be returned.
573

574
        :param function_name: Function name to be published
575
        :param region: Region of the function
576
        :param account_id: Account of the function
577
        :param description: new description of the version (will be the description of the function if missing)
578
        :param revision_id: Revision id, function will raise error if it does not match latest revision id
579
        :param code_sha256: Code sha256, function will raise error if it does not match latest code hash
580
        :return: Tuple of (published version, whether version was released or last released version returned, since nothing changed)
581
        """
582
        current_latest_version = get_function_version(
1✔
583
            function_name=function_name, qualifier="$LATEST", account_id=account_id, region=region
584
        )
585
        if revision_id and current_latest_version.config.revision_id != revision_id:
1✔
586
            raise PreconditionFailedException(
1✔
587
                "The Revision Id provided does not match the latest Revision Id. Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
588
                Type="User",
589
            )
590

591
        # check if code hashes match if they are specified
592
        current_hash = (
1✔
593
            current_latest_version.config.code.code_sha256
594
            if current_latest_version.config.package_type == PackageType.Zip
595
            else current_latest_version.config.image.code_sha256
596
        )
597
        # if the code is a zip package and hot reloaded (hot reloading is currently only supported for zip packagetypes)
598
        # we cannot enforce the codesha256 check
599
        is_hot_reloaded_zip_package = (
1✔
600
            current_latest_version.config.package_type == PackageType.Zip
601
            and current_latest_version.config.code.is_hot_reloading()
602
        )
603
        if code_sha256 and current_hash != code_sha256 and not is_hot_reloaded_zip_package:
1✔
604
            raise InvalidParameterValueException(
1✔
605
                f"CodeSHA256 ({code_sha256}) is different from current CodeSHA256 in $LATEST ({current_hash}). Please try again with the CodeSHA256 in $LATEST.",
606
                Type="User",
607
            )
608

609
        state = lambda_stores[account_id][region]
1✔
610
        function = state.functions.get(function_name)
1✔
611
        changes = {}
1✔
612
        if description is not None:
1✔
613
            changes["description"] = description
1✔
614
        # TODO copy environment instead of restarting one, get rid of all the "Pending"s
615

616
        with function.lock:
1✔
617
            if function.next_version > 1 and (
1✔
618
                prev_version := function.versions.get(str(function.next_version - 1))
619
            ):
620
                if (
1✔
621
                    prev_version.config.internal_revision
622
                    == current_latest_version.config.internal_revision
623
                ):
624
                    return prev_version, False
1✔
625
            # TODO check if there was a change since last version
626
            if publish_to == FunctionVersionLatestPublished.LATEST_PUBLISHED:
1✔
UNCOV
627
                qualifier = "$LATEST.PUBLISHED"
×
628
            else:
629
                qualifier = str(function.next_version)
1✔
630
                function.next_version += 1
1✔
631
            new_id = VersionIdentifier(
1✔
632
                function_name=function_name,
633
                qualifier=qualifier,
634
                region=region,
635
                account=account_id,
636
            )
637

638
            if current_latest_version.config.capacity_provider_config:
1✔
639
                # for lambda managed functions, snap start is not supported
UNCOV
640
                snap_start = None
×
641
            else:
642
                apply_on = current_latest_version.config.snap_start["ApplyOn"]
1✔
643
                optimization_status = SnapStartOptimizationStatus.Off
1✔
644
                if apply_on == SnapStartApplyOn.PublishedVersions:
1✔
UNCOV
645
                    optimization_status = SnapStartOptimizationStatus.On
×
646
                snap_start = SnapStartResponse(
1✔
647
                    ApplyOn=apply_on,
648
                    OptimizationStatus=optimization_status,
649
                )
650

651
            last_update = None
1✔
652
            new_state = VersionState(
1✔
653
                state=State.Pending,
654
                code=StateReasonCode.Creating,
655
                reason="The function is being created.",
656
            )
657
            if publish_to == FunctionVersionLatestPublished.LATEST_PUBLISHED:
1✔
UNCOV
658
                last_update = UpdateStatus(
×
659
                    status=LastUpdateStatus.InProgress,
660
                    code="Updating",
661
                    reason="The function is being updated.",
662
                )
UNCOV
663
                if is_active:
×
UNCOV
664
                    new_state = VersionState(state=State.Active)
×
665
            new_version = dataclasses.replace(
1✔
666
                current_latest_version,
667
                config=dataclasses.replace(
668
                    current_latest_version.config,
669
                    last_update=last_update,
670
                    state=new_state,
671
                    snap_start=snap_start,
672
                    **changes,
673
                ),
674
                id=new_id,
675
            )
676
            function.versions[qualifier] = new_version
1✔
677
        return new_version, True
1✔
678

679
    def _publish_version_from_existing_version(
1✔
680
        self,
681
        function_name: str,
682
        region: str,
683
        account_id: str,
684
        description: str | None = None,
685
        revision_id: str | None = None,
686
        code_sha256: str | None = None,
687
        publish_to: FunctionVersionLatestPublished | None = None,
688
    ) -> FunctionVersion:
689
        """
690
        Publish version from an existing, already initialized LATEST
691

692
        :param function_name: Function name
693
        :param region: region
694
        :param account_id: account id
695
        :param description: description
696
        :param revision_id: revision id (check if current version matches)
697
        :param code_sha256: code sha (check if current code matches)
698
        :return: new version
699
        """
700
        is_active = True if publish_to == FunctionVersionLatestPublished.LATEST_PUBLISHED else False
1✔
701
        new_version, changed = self._create_version_model(
1✔
702
            function_name=function_name,
703
            region=region,
704
            account_id=account_id,
705
            description=description,
706
            revision_id=revision_id,
707
            code_sha256=code_sha256,
708
            publish_to=publish_to,
709
            is_active=is_active,
710
        )
711
        if not changed:
1✔
712
            return new_version
1✔
713

714
        if new_version.config.capacity_provider_config:
1✔
UNCOV
715
            self.lambda_service.publish_version_async(new_version)
×
716
        else:
717
            self.lambda_service.publish_version(new_version)
1✔
718
        state = lambda_stores[account_id][region]
1✔
719
        function = state.functions.get(function_name)
1✔
720

721
        # Update revision id for $LATEST version
722
        # TODO: re-evaluate data model to prevent this dirty hack just for bumping the revision id
723
        latest_version = function.versions["$LATEST"]
1✔
724
        function.versions["$LATEST"] = dataclasses.replace(
1✔
725
            latest_version, config=dataclasses.replace(latest_version.config)
726
        )
727
        if new_version.config.capacity_provider_config:
1✔
728
            # publish_version happens async for functions with a capacity provider.
729
            # Therefore, we return the new_version with State=Pending or LastUpdateStatus=InProgress ($LATEST.PUBLISHED)
UNCOV
730
            return new_version
×
731
        else:
732
            # Regular functions yield an Active state modified during `publish_version` (sync).
733
            # Therefore, we need to get the updated version from the store.
734
            updated_version = function.versions.get(new_version.id.qualifier)
1✔
735
            return updated_version
1✔
736

737
    def _publish_version_with_changes(
1✔
738
        self,
739
        function_name: str,
740
        region: str,
741
        account_id: str,
742
        description: str | None = None,
743
        revision_id: str | None = None,
744
        code_sha256: str | None = None,
745
        publish_to: FunctionVersionLatestPublished | None = None,
746
        is_active: bool = False,
747
    ) -> FunctionVersion:
748
        """
749
        Publish version together with a new latest version (publish on create / update)
750

751
        :param function_name: Function name
752
        :param region: region
753
        :param account_id: account id
754
        :param description: description
755
        :param revision_id: revision id (check if current version matches)
756
        :param code_sha256: code sha (check if current code matches)
757
        :return: new version
758
        """
759
        new_version, changed = self._create_version_model(
1✔
760
            function_name=function_name,
761
            region=region,
762
            account_id=account_id,
763
            description=description,
764
            revision_id=revision_id,
765
            code_sha256=code_sha256,
766
            publish_to=publish_to,
767
            is_active=is_active,
768
        )
769
        if not changed:
1✔
UNCOV
770
            return new_version
×
771
        self.lambda_service.create_function_version(new_version)
1✔
772
        return new_version
1✔
773

774
    @staticmethod
1✔
775
    def _verify_env_variables(env_vars: dict[str, str]):
1✔
776
        dumped_env_vars = json.dumps(env_vars, separators=(",", ":"))
1✔
777
        if (
1✔
778
            len(dumped_env_vars.encode("utf-8"))
779
            > config.LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES
780
        ):
781
            raise InvalidParameterValueException(
1✔
782
                f"Lambda was unable to configure your environment variables because the environment variables you have provided exceeded the 4KB limit. String measured: {dumped_env_vars}",
783
                Type="User",
784
            )
785

786
    @staticmethod
1✔
787
    def _validate_snapstart(snap_start: SnapStart, runtime: Runtime):
1✔
788
        apply_on = snap_start.get("ApplyOn")
1✔
789
        if apply_on not in [
1✔
790
            SnapStartApplyOn.PublishedVersions,
791
            SnapStartApplyOn.None_,
792
        ]:
793
            raise ValidationException(
1✔
794
                f"1 validation error detected: Value '{apply_on}' at 'snapStart.applyOn' failed to satisfy constraint: Member must satisfy enum value set: [PublishedVersions, None]"
795
            )
796

797
        if runtime not in SNAP_START_SUPPORTED_RUNTIMES:
1✔
UNCOV
798
            raise InvalidParameterValueException(
×
799
                f"{runtime} is not supported for SnapStart enabled functions.", Type="User"
800
            )
801

802
    def _validate_layers(self, new_layers: list[str], region: str, account_id: str):
1✔
803
        if len(new_layers) > LAMBDA_LAYERS_LIMIT_PER_FUNCTION:
1✔
804
            raise InvalidParameterValueException(
1✔
805
                "Cannot reference more than 5 layers.", Type="User"
806
            )
807

808
        visited_layers = {}
1✔
809
        for layer_version_arn in new_layers:
1✔
810
            (
1✔
811
                layer_region,
812
                layer_account_id,
813
                layer_name,
814
                layer_version_str,
815
            ) = api_utils.parse_layer_arn(layer_version_arn)
816
            if layer_version_str is None:
1✔
817
                raise ValidationException(
1✔
818
                    f"1 validation error detected: Value '[{layer_version_arn}]'"
819
                    + " at 'layers' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 2048, Member must have length greater than or equal to 1, Member must satisfy regular expression pattern: "
820
                    + "(arn:(aws[a-zA-Z-]*)?:lambda:(eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:\\d{12}:layer:[a-zA-Z0-9-_]+:[0-9]+)|(arn:[a-zA-Z0-9-]+:lambda:::awslayer:[a-zA-Z0-9-_]+), Member must not be null]",
821
                )
822

823
            state = lambda_stores[layer_account_id][layer_region]
1✔
824
            layer = state.layers.get(layer_name)
1✔
825
            layer_version = None
1✔
826
            if layer is not None:
1✔
827
                layer_version = layer.layer_versions.get(layer_version_str)
1✔
828
            if layer_account_id == account_id:
1✔
829
                if region and layer_region != region:
1✔
830
                    raise InvalidParameterValueException(
1✔
831
                        f"Layers are not in the same region as the function. "
832
                        f"Layers are expected to be in region {region}.",
833
                        Type="User",
834
                    )
835
                if layer is None or layer.layer_versions.get(layer_version_str) is None:
1✔
836
                    raise InvalidParameterValueException(
1✔
837
                        f"Layer version {layer_version_arn} does not exist.", Type="User"
838
                    )
839
            else:  # External layer from other account
840
                # TODO: validate IAM layer policy here, allowing access by default for now and only checking region
UNCOV
841
                if region and layer_region != region:
×
842
                    # TODO: detect user or role from context when IAM users are implemented
843
                    user = "user/localstack-testing"
×
UNCOV
844
                    raise AccessDeniedException(
×
845
                        f"User: arn:{get_partition(region)}:iam::{account_id}:{user} is not authorized to perform: lambda:GetLayerVersion on resource: {layer_version_arn} because no resource-based policy allows the lambda:GetLayerVersion action"
846
                    )
UNCOV
847
                if layer is None or layer_version is None:
×
848
                    # Limitation: cannot fetch external layers when using the same account id as the target layer
849
                    # because we do not want to trigger the layer fetcher for every non-existing layer.
UNCOV
850
                    if self.layer_fetcher is None:
×
851
                        raise NotImplementedError(
852
                            "Fetching shared layers from AWS is a pro feature."
853
                        )
854

UNCOV
855
                    layer = self.layer_fetcher.fetch_layer(layer_version_arn)
×
UNCOV
856
                    if layer is None:
×
857
                        # TODO: detect user or role from context when IAM users are implemented
UNCOV
858
                        user = "user/localstack-testing"
×
859
                        raise AccessDeniedException(
×
860
                            f"User: arn:{get_partition(region)}:iam::{account_id}:{user} is not authorized to perform: lambda:GetLayerVersion on resource: {layer_version_arn} because no resource-based policy allows the lambda:GetLayerVersion action"
861
                        )
862

863
                    # Distinguish between new layer and new layer version
UNCOV
864
                    if layer_version is None:
×
865
                        # Create whole layer from scratch
UNCOV
866
                        state.layers[layer_name] = layer
×
867
                    else:
868
                        # Create layer version if another version of the same layer already exists
UNCOV
869
                        state.layers[layer_name].layer_versions[layer_version_str] = (
×
870
                            layer.layer_versions.get(layer_version_str)
871
                        )
872

873
            # only the first two matches in the array are considered for the error message
874
            layer_arn = ":".join(layer_version_arn.split(":")[:-1])
1✔
875
            if layer_arn in visited_layers:
1✔
876
                conflict_layer_version_arn = visited_layers[layer_arn]
1✔
877
                raise InvalidParameterValueException(
1✔
878
                    f"Two different versions of the same layer are not allowed to be referenced in the same function. {conflict_layer_version_arn} and {layer_version_arn} are versions of the same layer.",
879
                    Type="User",
880
                )
881
            visited_layers[layer_arn] = layer_version_arn
1✔
882

883
    def _validate_capacity_provider_config(
1✔
884
        self, capacity_provider_config: CapacityProviderConfig, context: RequestContext
885
    ):
UNCOV
886
        if not capacity_provider_config.get("LambdaManagedInstancesCapacityProviderConfig"):
×
887
            raise ValidationException(
×
888
                "1 validation error detected: Value null at 'capacityProviderConfig.lambdaManagedInstancesCapacityProviderConfig' failed to satisfy constraint: Member must not be null"
889
            )
890

UNCOV
891
        capacity_provider_arn = capacity_provider_config.get(
×
892
            "LambdaManagedInstancesCapacityProviderConfig", {}
893
        ).get("CapacityProviderArn")
UNCOV
894
        if not capacity_provider_arn:
×
UNCOV
895
            raise ValidationException(
×
896
                "1 validation error detected: Value null at 'capacityProviderConfig.lambdaManagedInstancesCapacityProviderConfig.capacityProviderArn' failed to satisfy constraint: Member must not be null"
897
            )
898

UNCOV
899
        if not re.match(CAPACITY_PROVIDER_ARN_NAME, capacity_provider_arn):
×
UNCOV
900
            raise ValidationException(
×
901
                f"1 validation error detected: Value '{capacity_provider_arn}' at 'capacityProviderConfig.lambdaManagedInstancesCapacityProviderConfig.capacityProviderArn' failed to satisfy constraint: Member must satisfy regular expression pattern: {CAPACITY_PROVIDER_ARN_NAME}"
902
            )
903

UNCOV
904
        capacity_provider_name = capacity_provider_arn.split(":")[-1]
×
UNCOV
905
        self.get_capacity_provider(context, capacity_provider_name)
×
906

907
    @staticmethod
1✔
908
    def map_layers(new_layers: list[str]) -> list[LayerVersion]:
1✔
909
        layers = []
1✔
910
        for layer_version_arn in new_layers:
1✔
911
            region_name, account_id, layer_name, layer_version = api_utils.parse_layer_arn(
1✔
912
                layer_version_arn
913
            )
914
            layer = lambda_stores[account_id][region_name].layers.get(layer_name)
1✔
915
            layer_version = layer.layer_versions.get(layer_version)
1✔
916
            layers.append(layer_version)
1✔
917
        return layers
1✔
918

919
    def get_function_recursion_config(
1✔
920
        self,
921
        context: RequestContext,
922
        function_name: UnqualifiedFunctionName,
923
        **kwargs,
924
    ) -> GetFunctionRecursionConfigResponse:
925
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
926
        function_name = api_utils.get_function_name(function_name, context)
1✔
927
        fn = self._get_function(function_name=function_name, region=region, account_id=account_id)
1✔
928
        return GetFunctionRecursionConfigResponse(RecursiveLoop=fn.recursive_loop)
1✔
929

930
    def put_function_recursion_config(
1✔
931
        self,
932
        context: RequestContext,
933
        function_name: UnqualifiedFunctionName,
934
        recursive_loop: RecursiveLoop,
935
        **kwargs,
936
    ) -> PutFunctionRecursionConfigResponse:
937
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
938
        function_name = api_utils.get_function_name(function_name, context)
1✔
939

940
        fn = self._get_function(function_name=function_name, region=region, account_id=account_id)
1✔
941

942
        allowed_values = list(RecursiveLoop.__members__.values())
1✔
943
        if recursive_loop not in allowed_values:
1✔
944
            raise ValidationException(
1✔
945
                f"1 validation error detected: Value '{recursive_loop}' at 'recursiveLoop' failed to satisfy constraint: "
946
                f"Member must satisfy enum value set: [Terminate, Allow]"
947
            )
948

949
        fn.recursive_loop = recursive_loop
1✔
950
        return PutFunctionRecursionConfigResponse(RecursiveLoop=fn.recursive_loop)
1✔
951

952
    @handler(operation="CreateFunction", expand=False)
1✔
953
    def create_function(
1✔
954
        self,
955
        context: RequestContext,
956
        request: CreateFunctionRequest,
957
    ) -> FunctionConfiguration:
958
        context_region = context.region
1✔
959
        context_account_id = context.account_id
1✔
960

961
        zip_file = request.get("Code", {}).get("ZipFile")
1✔
962
        if zip_file and len(zip_file) > config.LAMBDA_LIMITS_CODE_SIZE_ZIPPED:
1✔
963
            raise RequestEntityTooLargeException(
1✔
964
                f"Zipped size must be smaller than {config.LAMBDA_LIMITS_CODE_SIZE_ZIPPED} bytes"
965
            )
966

967
        if context.request.content_length > config.LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE:
1✔
968
            raise RequestEntityTooLargeException(
1✔
969
                f"Request must be smaller than {config.LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE} bytes for the CreateFunction operation"
970
            )
971

972
        if architectures := request.get("Architectures"):
1✔
973
            if len(architectures) != 1:
1✔
974
                raise ValidationException(
1✔
975
                    f"1 validation error detected: Value '[{', '.join(architectures)}]' at 'architectures' failed to "
976
                    f"satisfy constraint: Member must have length less than or equal to 1",
977
                )
978
            if architectures[0] not in ARCHITECTURES:
1✔
979
                raise ValidationException(
1✔
980
                    f"1 validation error detected: Value '[{', '.join(architectures)}]' at 'architectures' failed to "
981
                    f"satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: "
982
                    f"[x86_64, arm64], Member must not be null]",
983
                )
984

985
        if env_vars := request.get("Environment", {}).get("Variables"):
1✔
986
            self._verify_env_variables(env_vars)
1✔
987

988
        if layers := request.get("Layers", []):
1✔
989
            self._validate_layers(layers, region=context_region, account_id=context_account_id)
1✔
990

991
        if not api_utils.is_role_arn(request.get("Role")):
1✔
992
            raise ValidationException(
1✔
993
                f"1 validation error detected: Value '{request.get('Role')}'"
994
                + " at 'role' failed to satisfy constraint: Member must satisfy regular expression pattern: arn:(aws[a-zA-Z-]*)?:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+"
995
            )
996
        if not self.lambda_service.can_assume_role(request.get("Role"), context.region):
1✔
UNCOV
997
            raise InvalidParameterValueException(
×
998
                "The role defined for the function cannot be assumed by Lambda.", Type="User"
999
            )
1000
        package_type = request.get("PackageType", PackageType.Zip)
1✔
1001
        runtime = request.get("Runtime")
1✔
1002
        self._validate_runtime(package_type, runtime)
1✔
1003

1004
        request_function_name = request.get("FunctionName")
1✔
1005

1006
        function_name, *_ = api_utils.get_name_and_qualifier(
1✔
1007
            function_arn_or_name=request_function_name,
1008
            qualifier=None,
1009
            context=context,
1010
        )
1011

1012
        if runtime in DEPRECATED_RUNTIMES:
1✔
1013
            LOG.warning(
1✔
1014
                "The Lambda runtime %s} is deprecated. "
1015
                "Please upgrade the runtime for the function %s: "
1016
                "https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html",
1017
                runtime,
1018
                function_name,
1019
            )
1020
        if snap_start := request.get("SnapStart"):
1✔
1021
            self._validate_snapstart(snap_start, runtime)
1✔
1022
        if publish_to := request.get("PublishTo"):
1✔
UNCOV
1023
            self._validate_publish_to(publish_to)
×
1024
        state = lambda_stores[context_account_id][context_region]
1✔
1025

1026
        with self.create_fn_lock:
1✔
1027
            if function_name in state.functions:
1✔
UNCOV
1028
                raise ResourceConflictException(f"Function already exist: {function_name}")
×
1029
            fn = Function(function_name=function_name)
1✔
1030
            arn = VersionIdentifier(
1✔
1031
                function_name=function_name,
1032
                qualifier="$LATEST",
1033
                region=context_region,
1034
                account=context_account_id,
1035
            )
1036
            # save function code to s3
1037
            code = None
1✔
1038
            image = None
1✔
1039
            image_config = None
1✔
1040
            runtime_version_config = RuntimeVersionConfig(
1✔
1041
                # Limitation: the runtime id (presumably sha256 of image) is currently hardcoded
1042
                # Potential implementation: provide (cached) sha256 hash of used Docker image
1043
                RuntimeVersionArn=f"arn:{context.partition}:lambda:{context_region}::runtime:8eeff65f6809a3ce81507fe733fe09b835899b99481ba22fd75b5a7338290ec1"
1044
            )
1045
            request_code = request.get("Code")
1✔
1046
            if package_type == PackageType.Zip:
1✔
1047
                # TODO verify if correct combination of code is set
1048
                if zip_file := request_code.get("ZipFile"):
1✔
1049
                    code = store_lambda_archive(
1✔
1050
                        archive_file=zip_file,
1051
                        function_name=function_name,
1052
                        region_name=context_region,
1053
                        account_id=context_account_id,
1054
                    )
1055
                elif s3_bucket := request_code.get("S3Bucket"):
1✔
1056
                    s3_key = request_code["S3Key"]
1✔
1057
                    s3_object_version = request_code.get("S3ObjectVersion")
1✔
1058
                    code = store_s3_bucket_archive(
1✔
1059
                        archive_bucket=s3_bucket,
1060
                        archive_key=s3_key,
1061
                        archive_version=s3_object_version,
1062
                        function_name=function_name,
1063
                        region_name=context_region,
1064
                        account_id=context_account_id,
1065
                    )
1066
                else:
UNCOV
1067
                    raise LambdaServiceException("A ZIP file or S3 bucket is required")
×
1068
            elif package_type == PackageType.Image:
1✔
1069
                image = request_code.get("ImageUri")
1✔
1070
                if not image:
1✔
UNCOV
1071
                    raise LambdaServiceException(
×
1072
                        "An image is required when the package type is set to 'image'"
1073
                    )
1074
                image = create_image_code(image_uri=image)
1✔
1075

1076
                image_config_req = request.get("ImageConfig", {})
1✔
1077
                image_config = ImageConfig(
1✔
1078
                    command=image_config_req.get("Command"),
1079
                    entrypoint=image_config_req.get("EntryPoint"),
1080
                    working_directory=image_config_req.get("WorkingDirectory"),
1081
                )
1082
                # Runtime management controls are not available when providing a custom image
1083
                runtime_version_config = None
1✔
1084

1085
            capacity_provider_config = None
1✔
1086
            memory_size = request.get("MemorySize", LAMBDA_DEFAULT_MEMORY_SIZE)
1✔
1087
            if "CapacityProviderConfig" in request:
1✔
UNCOV
1088
                capacity_provider_config = request["CapacityProviderConfig"]
×
UNCOV
1089
                self._validate_capacity_provider_config(capacity_provider_config, context)
×
UNCOV
1090
                self._validate_managed_instances_runtime(runtime)
×
1091

1092
                default_config = CapacityProviderConfig(
×
1093
                    LambdaManagedInstancesCapacityProviderConfig=LambdaManagedInstancesCapacityProviderConfig(
1094
                        ExecutionEnvironmentMemoryGiBPerVCpu=2.0,
1095
                        PerExecutionEnvironmentMaxConcurrency=16,
1096
                    )
1097
                )
UNCOV
1098
                capacity_provider_config = merge_recursive(default_config, capacity_provider_config)
×
UNCOV
1099
                memory_size = 2048
×
UNCOV
1100
                if request.get("LoggingConfig", {}).get("LogFormat") == LogFormat.Text:
×
UNCOV
1101
                    raise InvalidParameterValueException(
×
1102
                        'LogLevel is not supported when LogFormat is set to "Text". Remove LogLevel from your request or change the LogFormat to "JSON" and try again.',
1103
                        Type="User",
1104
                    )
1105
            if "LoggingConfig" in request:
1✔
1106
                logging_config = request["LoggingConfig"]
1✔
1107
                LOG.warning(
1✔
1108
                    "Advanced Lambda Logging Configuration is currently mocked "
1109
                    "and will not impact the logging behavior. "
1110
                    "Please create a feature request if needed."
1111
                )
1112

1113
                # when switching to JSON, app and system level log is auto set to INFO
1114
                if logging_config.get("LogFormat", None) == LogFormat.JSON:
1✔
1115
                    logging_config = {
1✔
1116
                        "ApplicationLogLevel": "INFO",
1117
                        "SystemLogLevel": "INFO",
1118
                        "LogGroup": f"/aws/lambda/{function_name}",
1119
                    } | logging_config
1120
                else:
UNCOV
1121
                    logging_config = (
×
1122
                        LoggingConfig(
1123
                            LogFormat=LogFormat.Text, LogGroup=f"/aws/lambda/{function_name}"
1124
                        )
1125
                        | logging_config
1126
                    )
1127

1128
            elif capacity_provider_config:
1✔
UNCOV
1129
                logging_config = LoggingConfig(
×
1130
                    LogFormat=LogFormat.JSON,
1131
                    LogGroup=f"/aws/lambda/{function_name}",
1132
                    ApplicationLogLevel="INFO",
1133
                    SystemLogLevel="INFO",
1134
                )
1135
            else:
1136
                logging_config = LoggingConfig(
1✔
1137
                    LogFormat=LogFormat.Text, LogGroup=f"/aws/lambda/{function_name}"
1138
                )
1139
            snap_start = (
1✔
1140
                None
1141
                if capacity_provider_config
1142
                else SnapStartResponse(
1143
                    ApplyOn=request.get("SnapStart", {}).get("ApplyOn", SnapStartApplyOn.None_),
1144
                    OptimizationStatus=SnapStartOptimizationStatus.Off,
1145
                )
1146
            )
1147
            version = FunctionVersion(
1✔
1148
                id=arn,
1149
                config=VersionFunctionConfiguration(
1150
                    last_modified=api_utils.format_lambda_date(datetime.datetime.now()),
1151
                    description=request.get("Description", ""),
1152
                    role=request["Role"],
1153
                    timeout=request.get("Timeout", LAMBDA_DEFAULT_TIMEOUT),
1154
                    runtime=request.get("Runtime"),
1155
                    memory_size=memory_size,
1156
                    handler=request.get("Handler"),
1157
                    package_type=package_type,
1158
                    environment=env_vars,
1159
                    architectures=request.get("Architectures") or [Architecture.x86_64],
1160
                    tracing_config_mode=request.get("TracingConfig", {}).get(
1161
                        "Mode", TracingMode.PassThrough
1162
                    ),
1163
                    image=image,
1164
                    image_config=image_config,
1165
                    code=code,
1166
                    layers=self.map_layers(layers),
1167
                    internal_revision=short_uid(),
1168
                    ephemeral_storage=LambdaEphemeralStorage(
1169
                        size=request.get("EphemeralStorage", {}).get("Size", 512)
1170
                    ),
1171
                    snap_start=snap_start,
1172
                    runtime_version_config=runtime_version_config,
1173
                    dead_letter_arn=request.get("DeadLetterConfig", {}).get("TargetArn"),
1174
                    vpc_config=self._build_vpc_config(
1175
                        context_account_id, context_region, request.get("VpcConfig")
1176
                    ),
1177
                    state=VersionState(
1178
                        state=State.Pending,
1179
                        code=StateReasonCode.Creating,
1180
                        reason="The function is being created.",
1181
                    ),
1182
                    logging_config=logging_config,
1183
                    # TODO: might need something like **optional_kwargs if None
1184
                    #   -> Test with regular GetFunction (i.e., without a capacity provider)
1185
                    capacity_provider_config=capacity_provider_config,
1186
                ),
1187
            )
1188
            version_post_response = None
1✔
1189
            if capacity_provider_config:
1✔
UNCOV
1190
                version_post_response = dataclasses.replace(
×
1191
                    version,
1192
                    config=dataclasses.replace(
1193
                        version.config,
1194
                        last_update=UpdateStatus(status=LastUpdateStatus.Successful),
1195
                        state=VersionState(state=State.ActiveNonInvocable),
1196
                    ),
1197
                )
1198
            fn.versions["$LATEST"] = version_post_response or version
1✔
1199
            state.functions[function_name] = fn
1✔
1200
        initialization_type = (
1✔
1201
            FunctionInitializationType.lambda_managed_instances
1202
            if capacity_provider_config
1203
            else FunctionInitializationType.on_demand
1204
        )
1205
        function_counter.labels(
1✔
1206
            operation=FunctionOperation.create,
1207
            runtime=runtime or "n/a",
1208
            status=FunctionStatus.success,
1209
            invocation_type="n/a",
1210
            package_type=package_type,
1211
            initialization_type=initialization_type,
1212
        )
1213
        # TODO: consider potential other side effects of not having a function version for $LATEST
1214
        # Provisioning happens upon publishing for functions using a capacity provider
1215
        if not capacity_provider_config:
1✔
1216
            self.lambda_service.create_function_version(version)
1✔
1217

1218
        if tags := request.get("Tags"):
1✔
1219
            # This will check whether the function exists.
1220
            self._store_tags(arn.unqualified_arn(), tags)
1✔
1221

1222
        if request.get("Publish"):
1✔
1223
            version = self._publish_version_with_changes(
1✔
1224
                function_name=function_name,
1225
                region=context_region,
1226
                account_id=context_account_id,
1227
                publish_to=request.get("PublishTo"),
1228
            )
1229

1230
        if config.LAMBDA_SYNCHRONOUS_CREATE:
1✔
1231
            # block via retrying until "terminal" condition reached before returning
1232
            if not poll_condition(
×
1233
                lambda: get_function_version(
1234
                    function_name, version.id.qualifier, version.id.account, version.id.region
1235
                ).config.state.state
1236
                in [State.Active, State.ActiveNonInvocable, State.Failed],
1237
                timeout=10,
1238
            ):
UNCOV
1239
                LOG.warning(
×
1240
                    "LAMBDA_SYNCHRONOUS_CREATE is active, but waiting for %s reached timeout.",
1241
                    function_name,
1242
                )
1243

1244
        return api_utils.map_config_out(
1✔
1245
            version, return_qualified_arn=False, return_update_status=False
1246
        )
1247

1248
    def _validate_runtime(self, package_type, runtime):
1✔
1249
        runtimes = ALL_RUNTIMES
1✔
1250
        if config.LAMBDA_RUNTIME_VALIDATION:
1✔
1251
            runtimes = list(itertools.chain(RUNTIMES_AGGREGATED.values()))
1✔
1252

1253
        if package_type == PackageType.Zip and runtime not in runtimes:
1✔
1254
            # deprecated runtimes have different error
1255
            if runtime in DEPRECATED_RUNTIMES:
1✔
1256
                HINT_LOG.info(
1✔
1257
                    "Set env variable LAMBDA_RUNTIME_VALIDATION to 0"
1258
                    " in order to allow usage of deprecated runtimes"
1259
                )
1260
                self._check_for_recomended_migration_target(runtime)
1✔
1261

1262
            raise InvalidParameterValueException(
1✔
1263
                f"Value {runtime} at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: {VALID_RUNTIMES} or be a valid ARN",
1264
                Type="User",
1265
            )
1266

1267
    def _validate_managed_instances_runtime(self, runtime):
1✔
UNCOV
1268
        if runtime not in VALID_MANAGED_INSTANCE_RUNTIMES:
×
UNCOV
1269
            raise InvalidParameterValueException(
×
1270
                f"Runtime Enum {runtime} does not support specified feature: Lambda Managed Instances"
1271
            )
1272

1273
    def _check_for_recomended_migration_target(self, deprecated_runtime):
1✔
1274
        # AWS offers recommended runtime for migration for "newly" deprecated runtimes
1275
        # in order to preserve parity with error messages we need the code bellow
1276
        latest_runtime = DEPRECATED_RUNTIMES_UPGRADES.get(deprecated_runtime)
1✔
1277

1278
        if latest_runtime is not None:
1✔
1279
            LOG.debug(
1✔
1280
                "The Lambda runtime %s is deprecated. Please upgrade to a supported Lambda runtime such as %s.",
1281
                deprecated_runtime,
1282
                latest_runtime,
1283
            )
1284
            raise InvalidParameterValueException(
1✔
1285
                f"The runtime parameter of {deprecated_runtime} is no longer supported for creating or updating AWS Lambda functions. We recommend you use a supported runtime while creating or updating functions.",
1286
                Type="User",
1287
            )
1288

1289
    @handler(operation="UpdateFunctionConfiguration", expand=False)
1✔
1290
    def update_function_configuration(
1✔
1291
        self, context: RequestContext, request: UpdateFunctionConfigurationRequest
1292
    ) -> FunctionConfiguration:
1293
        """updates the $LATEST version of the function"""
1294
        function_name = request.get("FunctionName")
1✔
1295

1296
        # in case we got ARN or partial ARN
1297
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1298
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
1299
        state = lambda_stores[account_id][region]
1✔
1300

1301
        if function_name not in state.functions:
1✔
UNCOV
1302
            raise ResourceNotFoundException(
×
1303
                f"Function not found: {api_utils.unqualified_lambda_arn(function_name=function_name, region=region, account=account_id)}",
1304
                Type="User",
1305
            )
1306
        function = state.functions[function_name]
1✔
1307

1308
        # TODO: lock modification of latest version
1309
        # TODO: notify service for changes relevant to re-provisioning of $LATEST
1310
        latest_version = function.latest()
1✔
1311
        latest_version_config = latest_version.config
1✔
1312

1313
        revision_id = request.get("RevisionId")
1✔
1314
        if revision_id and revision_id != latest_version.config.revision_id:
1✔
1315
            raise PreconditionFailedException(
1✔
1316
                "The Revision Id provided does not match the latest Revision Id. "
1317
                "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
1318
                Type="User",
1319
            )
1320

1321
        replace_kwargs = {}
1✔
1322
        if "EphemeralStorage" in request:
1✔
UNCOV
1323
            replace_kwargs["ephemeral_storage"] = LambdaEphemeralStorage(
×
1324
                request.get("EphemeralStorage", {}).get("Size", 512)
1325
            )  # TODO: do defaults here apply as well?
1326

1327
        if "Role" in request:
1✔
1328
            if not api_utils.is_role_arn(request["Role"]):
1✔
1329
                raise ValidationException(
1✔
1330
                    f"1 validation error detected: Value '{request.get('Role')}'"
1331
                    + " at 'role' failed to satisfy constraint: Member must satisfy regular expression pattern: arn:(aws[a-zA-Z-]*)?:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+"
1332
                )
1333
            replace_kwargs["role"] = request["Role"]
1✔
1334

1335
        if "Description" in request:
1✔
1336
            replace_kwargs["description"] = request["Description"]
1✔
1337

1338
        if "Timeout" in request:
1✔
1339
            replace_kwargs["timeout"] = request["Timeout"]
1✔
1340

1341
        if "MemorySize" in request:
1✔
1342
            replace_kwargs["memory_size"] = request["MemorySize"]
1✔
1343

1344
        if "DeadLetterConfig" in request:
1✔
1345
            replace_kwargs["dead_letter_arn"] = request.get("DeadLetterConfig", {}).get("TargetArn")
1✔
1346

1347
        if vpc_config := request.get("VpcConfig"):
1✔
1348
            replace_kwargs["vpc_config"] = self._build_vpc_config(account_id, region, vpc_config)
1✔
1349

1350
        if "Handler" in request:
1✔
1351
            replace_kwargs["handler"] = request["Handler"]
1✔
1352

1353
        if "Runtime" in request:
1✔
1354
            runtime = request["Runtime"]
1✔
1355

1356
            if runtime not in ALL_RUNTIMES:
1✔
1357
                raise InvalidParameterValueException(
1✔
1358
                    f"Value {runtime} at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: {VALID_RUNTIMES} or be a valid ARN",
1359
                    Type="User",
1360
                )
1361
            if runtime in DEPRECATED_RUNTIMES:
1✔
UNCOV
1362
                LOG.warning(
×
1363
                    "The Lambda runtime %s is deprecated. "
1364
                    "Please upgrade the runtime for the function %s: "
1365
                    "https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html",
1366
                    runtime,
1367
                    function_name,
1368
                )
1369
            replace_kwargs["runtime"] = request["Runtime"]
1✔
1370

1371
        if snap_start := request.get("SnapStart"):
1✔
1372
            runtime = replace_kwargs.get("runtime") or latest_version_config.runtime
1✔
1373
            self._validate_snapstart(snap_start, runtime)
1✔
1374
            replace_kwargs["snap_start"] = SnapStartResponse(
1✔
1375
                ApplyOn=snap_start.get("ApplyOn", SnapStartApplyOn.None_),
1376
                OptimizationStatus=SnapStartOptimizationStatus.Off,
1377
            )
1378

1379
        if "Environment" in request:
1✔
1380
            if env_vars := request.get("Environment", {}).get("Variables", {}):
1✔
1381
                self._verify_env_variables(env_vars)
1✔
1382
            replace_kwargs["environment"] = env_vars
1✔
1383

1384
        if "Layers" in request:
1✔
1385
            new_layers = request["Layers"]
1✔
1386
            if new_layers:
1✔
1387
                self._validate_layers(new_layers, region=region, account_id=account_id)
1✔
1388
            replace_kwargs["layers"] = self.map_layers(new_layers)
1✔
1389

1390
        if "ImageConfig" in request:
1✔
1391
            new_image_config = request["ImageConfig"]
1✔
1392
            replace_kwargs["image_config"] = ImageConfig(
1✔
1393
                command=new_image_config.get("Command"),
1394
                entrypoint=new_image_config.get("EntryPoint"),
1395
                working_directory=new_image_config.get("WorkingDirectory"),
1396
            )
1397

1398
        if "LoggingConfig" in request:
1✔
1399
            logging_config = request["LoggingConfig"]
1✔
1400
            LOG.warning(
1✔
1401
                "Advanced Lambda Logging Configuration is currently mocked "
1402
                "and will not impact the logging behavior. "
1403
                "Please create a feature request if needed."
1404
            )
1405

1406
            # when switching to JSON, app and system level log is auto set to INFO
1407
            if logging_config.get("LogFormat", None) == LogFormat.JSON:
1✔
1408
                logging_config = {
1✔
1409
                    "ApplicationLogLevel": "INFO",
1410
                    "SystemLogLevel": "INFO",
1411
                } | logging_config
1412

1413
            last_config = latest_version_config.logging_config
1✔
1414

1415
            # add partial update
1416
            new_logging_config = last_config | logging_config
1✔
1417

1418
            # in case we switched from JSON to Text we need to remove LogLevel keys
1419
            if (
1✔
1420
                new_logging_config.get("LogFormat") == LogFormat.Text
1421
                and last_config.get("LogFormat") == LogFormat.JSON
1422
            ):
1423
                new_logging_config.pop("ApplicationLogLevel", None)
1✔
1424
                new_logging_config.pop("SystemLogLevel", None)
1✔
1425

1426
            replace_kwargs["logging_config"] = new_logging_config
1✔
1427

1428
        if "TracingConfig" in request:
1✔
UNCOV
1429
            new_mode = request.get("TracingConfig", {}).get("Mode")
×
1430
            if new_mode:
×
UNCOV
1431
                replace_kwargs["tracing_config_mode"] = new_mode
×
1432

1433
        if "CapacityProviderConfig" in request:
1✔
UNCOV
1434
            capacity_provider_config = request["CapacityProviderConfig"]
×
UNCOV
1435
            self._validate_capacity_provider_config(capacity_provider_config, context)
×
1436

1437
            if latest_version.config.capacity_provider_config and not request[
×
1438
                "CapacityProviderConfig"
1439
            ].get("LambdaManagedInstancesCapacityProviderConfig"):
UNCOV
1440
                raise ValidationException(
×
1441
                    "1 validation error detected: Value null at 'capacityProviderConfig.lambdaManagedInstancesCapacityProviderConfig' failed to satisfy constraint: Member must not be null"
1442
                )
UNCOV
1443
            if not latest_version.config.capacity_provider_config:
×
UNCOV
1444
                raise InvalidParameterValueException(
×
1445
                    "CapacityProviderConfig isn't supported for Lambda Default functions.",
1446
                    Type="User",
1447
                )
1448

1449
            default_config = CapacityProviderConfig(
×
1450
                LambdaManagedInstancesCapacityProviderConfig=LambdaManagedInstancesCapacityProviderConfig(
1451
                    ExecutionEnvironmentMemoryGiBPerVCpu=2.0,
1452
                    PerExecutionEnvironmentMaxConcurrency=16,
1453
                )
1454
            )
UNCOV
1455
            capacity_provider_config = merge_recursive(default_config, capacity_provider_config)
×
UNCOV
1456
            replace_kwargs["capacity_provider_config"] = capacity_provider_config
×
1457
        new_latest_version = dataclasses.replace(
1✔
1458
            latest_version,
1459
            config=dataclasses.replace(
1460
                latest_version_config,
1461
                last_modified=api_utils.generate_lambda_date(),
1462
                internal_revision=short_uid(),
1463
                last_update=UpdateStatus(
1464
                    status=LastUpdateStatus.InProgress,
1465
                    code="Creating",
1466
                    reason="The function is being created.",
1467
                ),
1468
                **replace_kwargs,
1469
            ),
1470
        )
1471
        function.versions["$LATEST"] = new_latest_version  # TODO: notify
1✔
1472
        self.lambda_service.update_version(new_version=new_latest_version)
1✔
1473

1474
        return api_utils.map_config_out(new_latest_version)
1✔
1475

1476
    @handler(operation="UpdateFunctionCode", expand=False)
1✔
1477
    def update_function_code(
1✔
1478
        self, context: RequestContext, request: UpdateFunctionCodeRequest
1479
    ) -> FunctionConfiguration:
1480
        """updates the $LATEST version of the function"""
1481
        # only supports normal zip packaging atm
1482
        # if request.get("Publish"):
1483
        #     self.lambda_service.create_function_version()
1484

1485
        function_name = request.get("FunctionName")
1✔
1486
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1487
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
1488

1489
        store = lambda_stores[account_id][region]
1✔
1490
        if function_name not in store.functions:
1✔
UNCOV
1491
            raise ResourceNotFoundException(
×
1492
                f"Function not found: {api_utils.unqualified_lambda_arn(function_name=function_name, region=region, account=account_id)}",
1493
                Type="User",
1494
            )
1495
        function = store.functions[function_name]
1✔
1496

1497
        revision_id = request.get("RevisionId")
1✔
1498
        if revision_id and revision_id != function.latest().config.revision_id:
1✔
1499
            raise PreconditionFailedException(
1✔
1500
                "The Revision Id provided does not match the latest Revision Id. "
1501
                "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
1502
                Type="User",
1503
            )
1504

1505
        # TODO verify if correct combination of code is set
1506
        image = None
1✔
1507
        if (
1✔
1508
            request.get("ZipFile") or request.get("S3Bucket")
1509
        ) and function.latest().config.package_type == PackageType.Image:
1510
            raise InvalidParameterValueException(
1✔
1511
                "Please provide ImageUri when updating a function with packageType Image.",
1512
                Type="User",
1513
            )
1514
        elif request.get("ImageUri") and function.latest().config.package_type == PackageType.Zip:
1✔
1515
            raise InvalidParameterValueException(
1✔
1516
                "Please don't provide ImageUri when updating a function with packageType Zip.",
1517
                Type="User",
1518
            )
1519

1520
        if publish_to := request.get("PublishTo"):
1✔
UNCOV
1521
            self._validate_publish_to(publish_to)
×
1522

1523
        if zip_file := request.get("ZipFile"):
1✔
1524
            code = store_lambda_archive(
1✔
1525
                archive_file=zip_file,
1526
                function_name=function_name,
1527
                region_name=region,
1528
                account_id=account_id,
1529
            )
1530
        elif s3_bucket := request.get("S3Bucket"):
1✔
1531
            s3_key = request["S3Key"]
1✔
1532
            s3_object_version = request.get("S3ObjectVersion")
1✔
1533
            code = store_s3_bucket_archive(
1✔
1534
                archive_bucket=s3_bucket,
1535
                archive_key=s3_key,
1536
                archive_version=s3_object_version,
1537
                function_name=function_name,
1538
                region_name=region,
1539
                account_id=account_id,
1540
            )
1541
        elif image := request.get("ImageUri"):
1✔
1542
            code = None
1✔
1543
            image = create_image_code(image_uri=image)
1✔
1544
        else:
1545
            raise LambdaServiceException("A ZIP file, S3 bucket, or image is required")
×
1546

1547
        old_function_version = function.versions.get("$LATEST")
1✔
1548
        replace_kwargs = {"code": code} if code else {"image": image}
1✔
1549

1550
        if architectures := request.get("Architectures"):
1✔
1551
            if len(architectures) != 1:
×
1552
                raise ValidationException(
×
1553
                    f"1 validation error detected: Value '[{', '.join(architectures)}]' at 'architectures' failed to "
1554
                    f"satisfy constraint: Member must have length less than or equal to 1",
1555
                )
1556
            # An empty list of architectures is also forbidden. Further exceptions are tested here for create_function:
1557
            # tests.aws.services.lambda_.test_lambda_api.TestLambdaFunction.test_create_lambda_exceptions
UNCOV
1558
            if architectures[0] not in ARCHITECTURES:
×
UNCOV
1559
                raise ValidationException(
×
1560
                    f"1 validation error detected: Value '[{', '.join(architectures)}]' at 'architectures' failed to "
1561
                    f"satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: "
1562
                    f"[x86_64, arm64], Member must not be null]",
1563
                )
UNCOV
1564
            replace_kwargs["architectures"] = architectures
×
1565

1566
        config = dataclasses.replace(
1✔
1567
            old_function_version.config,
1568
            internal_revision=short_uid(),
1569
            last_modified=api_utils.generate_lambda_date(),
1570
            last_update=UpdateStatus(
1571
                status=LastUpdateStatus.InProgress,
1572
                code="Creating",
1573
                reason="The function is being created.",
1574
            ),
1575
            **replace_kwargs,
1576
        )
1577
        function_version = dataclasses.replace(old_function_version, config=config)
1✔
1578
        function.versions["$LATEST"] = function_version
1✔
1579

1580
        self.lambda_service.update_version(new_version=function_version)
1✔
1581
        if request.get("Publish"):
1✔
1582
            function_version = self._publish_version_with_changes(
1✔
1583
                function_name=function_name,
1584
                region=region,
1585
                account_id=account_id,
1586
                publish_to=publish_to,
1587
                is_active=True,
1588
            )
1589
        return api_utils.map_config_out(
1✔
1590
            function_version, return_qualified_arn=bool(request.get("Publish"))
1591
        )
1592

1593
    # TODO: does deleting the latest published version affect the next versions number?
1594
    # TODO: what happens when we call this with a qualifier and a fully qualified ARN? (+ conflicts?)
1595
    # TODO: test different ARN patterns (shorthand ARN?)
1596
    # TODO: test deleting across regions?
1597
    # TODO: test mismatch between context region and region in ARN
1598
    # TODO: test qualifier $LATEST, alias-name and version
1599
    def delete_function(
1✔
1600
        self,
1601
        context: RequestContext,
1602
        function_name: NamespacedFunctionName,
1603
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
1604
        **kwargs,
1605
    ) -> DeleteFunctionResponse:
1606
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1607
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
1608
            function_name, qualifier, context
1609
        )
1610

1611
        if qualifier and api_utils.qualifier_is_alias(qualifier):
1✔
UNCOV
1612
            raise InvalidParameterValueException(
×
1613
                "Deletion of aliases is not currently supported.",
1614
                Type="User",
1615
            )
1616

1617
        store = lambda_stores[account_id][region]
1✔
1618
        if qualifier == "$LATEST":
1✔
1619
            raise InvalidParameterValueException(
1✔
1620
                "$LATEST version cannot be deleted without deleting the function.", Type="User"
1621
            )
1622

1623
        if function_name not in store.functions:
1✔
1624
            e = ResourceNotFoundException(
1✔
1625
                f"Function not found: {api_utils.unqualified_lambda_arn(function_name=function_name, region=region, account=account_id)}",
1626
                Type="User",
1627
            )
1628
            raise e
1✔
1629
        function = store.functions.get(function_name)
1✔
1630

1631
        function_has_capacity_provider = False
1✔
1632
        if qualifier:
1✔
1633
            # delete a version of the function
1634
            version = function.versions.pop(qualifier, None)
1✔
1635
            if version:
1✔
1636
                if version.config.capacity_provider_config:
1✔
UNCOV
1637
                    function_has_capacity_provider = True
×
1638
                self.lambda_service.stop_version(version.id.qualified_arn())
1✔
1639
                destroy_code_if_not_used(code=version.config.code, function=function)
1✔
1640
        else:
1641
            # delete the whole function
1642
            # TODO: introduce locking for safe deletion: We could create a new version at the API layer before
1643
            #  the old version gets cleaned up in the internal lambda service.
1644
            function = store.functions.pop(function_name)
1✔
1645
            for version in function.versions.values():
1✔
1646
                # Functions with a capacity provider do NOT have a version manager for $LATEST because only
1647
                # published versions are invokable.
1648
                if version.config.capacity_provider_config:
1✔
UNCOV
1649
                    function_has_capacity_provider = True
×
UNCOV
1650
                    if version.id.qualifier == "$LATEST":
×
UNCOV
1651
                        pass
×
1652
                else:
1653
                    self.lambda_service.stop_version(qualified_arn=version.id.qualified_arn())
1✔
1654
                # we can safely destroy the code here
1655
                if version.config.code:
1✔
1656
                    version.config.code.destroy()
1✔
1657

1658
        return DeleteFunctionResponse(StatusCode=202 if function_has_capacity_provider else 204)
1✔
1659

1660
    def list_functions(
1✔
1661
        self,
1662
        context: RequestContext,
1663
        master_region: MasterRegion = None,  # (only relevant for lambda@edge)
1664
        function_version: FunctionVersionApi = None,
1665
        marker: String = None,
1666
        max_items: MaxListItems = None,
1667
        **kwargs,
1668
    ) -> ListFunctionsResponse:
1669
        state = lambda_stores[context.account_id][context.region]
1✔
1670

1671
        if function_version and function_version != FunctionVersionApi.ALL:
1✔
1672
            raise ValidationException(
1✔
1673
                f"1 validation error detected: Value '{function_version}'"
1674
                + " at 'functionVersion' failed to satisfy constraint: Member must satisfy enum value set: [ALL]"
1675
            )
1676

1677
        if function_version == FunctionVersionApi.ALL:
1✔
1678
            # include all versions for all function
1679
            versions = [v for f in state.functions.values() for v in f.versions.values()]
1✔
1680
            return_qualified_arn = True
1✔
1681
        else:
1682
            versions = [f.latest() for f in state.functions.values()]
1✔
1683
            return_qualified_arn = False
1✔
1684

1685
        versions = [
1✔
1686
            api_utils.map_to_list_response(
1687
                api_utils.map_config_out(fc, return_qualified_arn=return_qualified_arn)
1688
            )
1689
            for fc in versions
1690
        ]
1691
        versions = PaginatedList(versions)
1✔
1692
        page, token = versions.get_page(
1✔
1693
            lambda version: version["FunctionArn"],
1694
            marker,
1695
            max_items,
1696
        )
1697
        return ListFunctionsResponse(Functions=page, NextMarker=token)
1✔
1698

1699
    def get_function(
1✔
1700
        self,
1701
        context: RequestContext,
1702
        function_name: NamespacedFunctionName,
1703
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
1704
        **kwargs,
1705
    ) -> GetFunctionResponse:
1706
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1707
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
1708
            function_name, qualifier, context
1709
        )
1710

1711
        fn = lambda_stores[account_id][region].functions.get(function_name)
1✔
1712
        if fn is None:
1✔
1713
            if qualifier is None:
1✔
1714
                raise ResourceNotFoundException(
1✔
1715
                    f"Function not found: {api_utils.unqualified_lambda_arn(function_name, account_id, region)}",
1716
                    Type="User",
1717
                )
1718
            else:
1719
                raise ResourceNotFoundException(
1✔
1720
                    f"Function not found: {api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)}",
1721
                    Type="User",
1722
                )
1723
        alias_name = None
1✔
1724
        if qualifier and api_utils.qualifier_is_alias(qualifier):
1✔
1725
            if qualifier not in fn.aliases:
1✔
1726
                alias_arn = api_utils.qualified_lambda_arn(
1✔
1727
                    function_name, qualifier, account_id, region
1728
                )
1729
                raise ResourceNotFoundException(f"Function not found: {alias_arn}", Type="User")
1✔
1730
            alias_name = qualifier
1✔
1731
            qualifier = fn.aliases[alias_name].function_version
1✔
1732

1733
        version = get_function_version(
1✔
1734
            function_name=function_name,
1735
            qualifier=qualifier,
1736
            account_id=account_id,
1737
            region=region,
1738
        )
1739
        tags = self._get_tags(api_utils.unqualified_lambda_arn(function_name, account_id, region))
1✔
1740
        additional_fields = {}
1✔
1741
        if tags:
1✔
1742
            additional_fields["Tags"] = tags
1✔
1743
        code_location = None
1✔
1744
        if code := version.config.code:
1✔
1745
            code_location = FunctionCodeLocation(
1✔
1746
                Location=code.generate_presigned_url(endpoint_url=config.external_service_url()),
1747
                RepositoryType="S3",
1748
            )
1749
        elif image := version.config.image:
1✔
1750
            code_location = FunctionCodeLocation(
1✔
1751
                ImageUri=image.image_uri,
1752
                RepositoryType=image.repository_type,
1753
                ResolvedImageUri=image.resolved_image_uri,
1754
            )
1755
        concurrency = None
1✔
1756
        if fn.reserved_concurrent_executions:
1✔
1757
            concurrency = Concurrency(
1✔
1758
                ReservedConcurrentExecutions=fn.reserved_concurrent_executions
1759
            )
1760

1761
        return GetFunctionResponse(
1✔
1762
            Configuration=api_utils.map_config_out(
1763
                version, return_qualified_arn=bool(qualifier), alias_name=alias_name
1764
            ),
1765
            Code=code_location,  # TODO
1766
            Concurrency=concurrency,
1767
            **additional_fields,
1768
        )
1769

1770
    def get_function_configuration(
1✔
1771
        self,
1772
        context: RequestContext,
1773
        function_name: NamespacedFunctionName,
1774
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
1775
        **kwargs,
1776
    ) -> FunctionConfiguration:
1777
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1778
        # CAVE: THIS RETURN VALUE IS *NOT* THE SAME AS IN get_function (!) but seems to be only configuration part?
1779
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
1780
            function_name, qualifier, context
1781
        )
1782
        version = get_function_version(
1✔
1783
            function_name=function_name,
1784
            qualifier=qualifier,
1785
            account_id=account_id,
1786
            region=region,
1787
        )
1788
        return api_utils.map_config_out(version, return_qualified_arn=bool(qualifier))
1✔
1789

1790
    def invoke(
1✔
1791
        self,
1792
        context: RequestContext,
1793
        function_name: NamespacedFunctionName,
1794
        invocation_type: InvocationType | None = None,
1795
        log_type: LogType | None = None,
1796
        client_context: String | None = None,
1797
        durable_execution_name: DurableExecutionName | None = None,
1798
        payload: IO[Blob] | None = None,
1799
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
1800
        tenant_id: TenantId | None = None,
1801
        **kwargs,
1802
    ) -> InvocationResponse:
1803
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1804
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
1805
            function_name, qualifier, context
1806
        )
1807

1808
        user_agent = context.request.user_agent.string
1✔
1809

1810
        time_before = time.perf_counter()
1✔
1811
        try:
1✔
1812
            invocation_result = self.lambda_service.invoke(
1✔
1813
                function_name=function_name,
1814
                qualifier=qualifier,
1815
                region=region,
1816
                account_id=account_id,
1817
                invocation_type=invocation_type,
1818
                client_context=client_context,
1819
                request_id=context.request_id,
1820
                trace_context=context.trace_context,
1821
                payload=payload.read() if payload else None,
1822
                user_agent=user_agent,
1823
            )
1824
        except ServiceException:
1✔
1825
            raise
1✔
1826
        except EnvironmentStartupTimeoutException as e:
1✔
1827
            raise LambdaServiceException(
1✔
1828
                f"[{context.request_id}] Timeout while starting up lambda environment for function {function_name}:{qualifier}"
1829
            ) from e
1830
        except Exception as e:
1✔
1831
            LOG.error(
1✔
1832
                "[%s] Error while invoking lambda %s",
1833
                context.request_id,
1834
                function_name,
1835
                exc_info=LOG.isEnabledFor(logging.DEBUG),
1836
            )
1837
            raise LambdaServiceException(
1✔
1838
                f"[{context.request_id}] Internal error while executing lambda {function_name}:{qualifier}. Caused by {type(e).__name__}: {e}"
1839
            ) from e
1840

1841
        if invocation_type == InvocationType.Event:
1✔
1842
            # This happens when invocation type is event
1843
            return InvocationResponse(StatusCode=202)
1✔
1844
        if invocation_type == InvocationType.DryRun:
1✔
1845
            # This happens when invocation type is dryrun
1846
            return InvocationResponse(StatusCode=204)
1✔
1847
        LOG.debug("Lambda invocation duration: %0.2fms", (time.perf_counter() - time_before) * 1000)
1✔
1848

1849
        response = InvocationResponse(
1✔
1850
            StatusCode=200,
1851
            Payload=invocation_result.payload,
1852
            ExecutedVersion=invocation_result.executed_version,
1853
        )
1854

1855
        if invocation_result.is_error:
1✔
1856
            response["FunctionError"] = "Unhandled"
1✔
1857

1858
        if log_type == LogType.Tail:
1✔
1859
            response["LogResult"] = to_str(
1✔
1860
                base64.b64encode(to_bytes(invocation_result.logs)[-4096:])
1861
            )
1862

1863
        return response
1✔
1864

1865
    # Version operations
1866
    def publish_version(
1✔
1867
        self,
1868
        context: RequestContext,
1869
        function_name: FunctionName,
1870
        code_sha256: String | None = None,
1871
        description: Description | None = None,
1872
        revision_id: String | None = None,
1873
        publish_to: FunctionVersionLatestPublished | None = None,
1874
        **kwargs,
1875
    ) -> FunctionConfiguration:
1876
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1877
        function_name = api_utils.get_function_name(function_name, context)
1✔
1878
        if publish_to:
1✔
UNCOV
1879
            self._validate_publish_to(publish_to)
×
1880
        new_version = self._publish_version_from_existing_version(
1✔
1881
            function_name=function_name,
1882
            description=description,
1883
            account_id=account_id,
1884
            region=region,
1885
            revision_id=revision_id,
1886
            code_sha256=code_sha256,
1887
            publish_to=publish_to,
1888
        )
1889
        return api_utils.map_config_out(new_version, return_qualified_arn=True)
1✔
1890

1891
    def list_versions_by_function(
1✔
1892
        self,
1893
        context: RequestContext,
1894
        function_name: NamespacedFunctionName,
1895
        marker: String = None,
1896
        max_items: MaxListItems = None,
1897
        **kwargs,
1898
    ) -> ListVersionsByFunctionResponse:
1899
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1900
        function_name = api_utils.get_function_name(function_name, context)
1✔
1901
        function = self._get_function(
1✔
1902
            function_name=function_name, region=region, account_id=account_id
1903
        )
1904
        versions = [
1✔
1905
            api_utils.map_to_list_response(
1906
                api_utils.map_config_out(version=version, return_qualified_arn=True)
1907
            )
1908
            for version in function.versions.values()
1909
        ]
1910
        items = PaginatedList(versions)
1✔
1911
        page, token = items.get_page(
1✔
1912
            lambda item: item,
1913
            marker,
1914
            max_items,
1915
        )
1916
        return ListVersionsByFunctionResponse(Versions=page, NextMarker=token)
1✔
1917

1918
    # Alias
1919

1920
    def _create_routing_config_model(
1✔
1921
        self, routing_config_dict: dict[str, float], function_version: FunctionVersion
1922
    ):
1923
        if len(routing_config_dict) > 1:
1✔
1924
            raise InvalidParameterValueException(
1✔
1925
                "Number of items in AdditionalVersionWeights cannot be greater than 1",
1926
                Type="User",
1927
            )
1928
        # should be exactly one item here, still iterating, might be supported in the future
1929
        for key, value in routing_config_dict.items():
1✔
1930
            if value < 0.0 or value >= 1.0:
1✔
1931
                raise ValidationException(
1✔
1932
                    f"1 validation error detected: Value '{{{key}={value}}}' at 'routingConfig.additionalVersionWeights' failed to satisfy constraint: Map value must satisfy constraint: [Member must have value less than or equal to 1.0, Member must have value greater than or equal to 0.0, Member must not be null]"
1933
                )
1934
            if key == function_version.id.qualifier:
1✔
1935
                raise InvalidParameterValueException(
1✔
1936
                    f"Invalid function version {function_version.id.qualifier}. Function version {function_version.id.qualifier} is already included in routing configuration.",
1937
                    Type="User",
1938
                )
1939
            # check if version target is latest, then no routing config is allowed
1940
            if function_version.id.qualifier == "$LATEST":
1✔
1941
                raise InvalidParameterValueException(
1✔
1942
                    "$LATEST is not supported for an alias pointing to more than 1 version"
1943
                )
1944
            if not api_utils.qualifier_is_version(key):
1✔
1945
                raise ValidationException(
1✔
1946
                    f"1 validation error detected: Value '{{{key}={value}}}' at 'routingConfig.additionalVersionWeights' failed to satisfy constraint: Map keys must satisfy constraint: [Member must have length less than or equal to 1024, Member must have length greater than or equal to 1, Member must satisfy regular expression pattern: [0-9]+]"
1947
                )
1948

1949
            # checking if the version in the config exists
1950
            get_function_version(
1✔
1951
                function_name=function_version.id.function_name,
1952
                qualifier=key,
1953
                region=function_version.id.region,
1954
                account_id=function_version.id.account,
1955
            )
1956
        return AliasRoutingConfig(version_weights=routing_config_dict)
1✔
1957

1958
    def create_alias(
1✔
1959
        self,
1960
        context: RequestContext,
1961
        function_name: FunctionName,
1962
        name: Alias,
1963
        function_version: VersionWithLatestPublished,
1964
        description: Description = None,
1965
        routing_config: AliasRoutingConfiguration = None,
1966
        **kwargs,
1967
    ) -> AliasConfiguration:
1968
        if not api_utils.qualifier_is_alias(name):
1✔
1969
            raise ValidationException(
1✔
1970
                f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: Member must satisfy regular expression pattern: (?!^[0-9]+$)([a-zA-Z0-9-_]+)"
1971
            )
1972

1973
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1974
        function_name = api_utils.get_function_name(function_name, context)
1✔
1975
        target_version = get_function_version(
1✔
1976
            function_name=function_name,
1977
            qualifier=function_version,
1978
            region=region,
1979
            account_id=account_id,
1980
        )
1981
        function = self._get_function(
1✔
1982
            function_name=function_name, region=region, account_id=account_id
1983
        )
1984
        # description is always present, if not specified it's an empty string
1985
        description = description or ""
1✔
1986
        with function.lock:
1✔
1987
            if existing_alias := function.aliases.get(name):
1✔
1988
                raise ResourceConflictException(
1✔
1989
                    f"Alias already exists: {api_utils.map_alias_out(alias=existing_alias, function=function)['AliasArn']}",
1990
                    Type="User",
1991
                )
1992
            # checking if the version exists
1993
            routing_configuration = None
1✔
1994
            if routing_config and (
1✔
1995
                routing_config_dict := routing_config.get("AdditionalVersionWeights")
1996
            ):
1997
                routing_configuration = self._create_routing_config_model(
1✔
1998
                    routing_config_dict, target_version
1999
                )
2000

2001
            alias = VersionAlias(
1✔
2002
                name=name,
2003
                function_version=function_version,
2004
                description=description,
2005
                routing_configuration=routing_configuration,
2006
            )
2007
            function.aliases[name] = alias
1✔
2008
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
2009

2010
    def list_aliases(
1✔
2011
        self,
2012
        context: RequestContext,
2013
        function_name: FunctionName,
2014
        function_version: VersionWithLatestPublished = None,
2015
        marker: String = None,
2016
        max_items: MaxListItems = None,
2017
        **kwargs,
2018
    ) -> ListAliasesResponse:
2019
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2020
        function_name = api_utils.get_function_name(function_name, context)
1✔
2021
        function = self._get_function(
1✔
2022
            function_name=function_name, region=region, account_id=account_id
2023
        )
2024
        aliases = [
1✔
2025
            api_utils.map_alias_out(alias, function)
2026
            for alias in function.aliases.values()
2027
            if function_version is None or alias.function_version == function_version
2028
        ]
2029

2030
        aliases = PaginatedList(aliases)
1✔
2031
        page, token = aliases.get_page(
1✔
2032
            lambda alias: alias["AliasArn"],
2033
            marker,
2034
            max_items,
2035
        )
2036

2037
        return ListAliasesResponse(Aliases=page, NextMarker=token)
1✔
2038

2039
    def delete_alias(
1✔
2040
        self, context: RequestContext, function_name: FunctionName, name: Alias, **kwargs
2041
    ) -> None:
2042
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2043
        function_name = api_utils.get_function_name(function_name, context)
1✔
2044
        function = self._get_function(
1✔
2045
            function_name=function_name, region=region, account_id=account_id
2046
        )
2047
        version_alias = function.aliases.pop(name, None)
1✔
2048

2049
        # cleanup related resources
2050
        if name in function.provisioned_concurrency_configs:
1✔
2051
            function.provisioned_concurrency_configs.pop(name)
1✔
2052

2053
        # TODO: Allow for deactivating/unregistering specific Lambda URLs
2054
        if version_alias and name in function.function_url_configs:
1✔
2055
            url_config = function.function_url_configs.pop(name)
1✔
2056
            LOG.debug(
1✔
2057
                "Stopping aliased Lambda Function URL %s for %s",
2058
                url_config.url,
2059
                url_config.function_name,
2060
            )
2061

2062
    def get_alias(
1✔
2063
        self, context: RequestContext, function_name: FunctionName, name: Alias, **kwargs
2064
    ) -> AliasConfiguration:
2065
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2066
        function_name = api_utils.get_function_name(function_name, context)
1✔
2067
        function = self._get_function(
1✔
2068
            function_name=function_name, region=region, account_id=account_id
2069
        )
2070
        if not (alias := function.aliases.get(name)):
1✔
2071
            raise ResourceNotFoundException(
1✔
2072
                f"Cannot find alias arn: {api_utils.qualified_lambda_arn(function_name=function_name, qualifier=name, region=region, account=account_id)}",
2073
                Type="User",
2074
            )
2075
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
2076

2077
    def update_alias(
1✔
2078
        self,
2079
        context: RequestContext,
2080
        function_name: FunctionName,
2081
        name: Alias,
2082
        function_version: VersionWithLatestPublished = None,
2083
        description: Description = None,
2084
        routing_config: AliasRoutingConfiguration = None,
2085
        revision_id: String = None,
2086
        **kwargs,
2087
    ) -> AliasConfiguration:
2088
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2089
        function_name = api_utils.get_function_name(function_name, context)
1✔
2090
        function = self._get_function(
1✔
2091
            function_name=function_name, region=region, account_id=account_id
2092
        )
2093
        if not (alias := function.aliases.get(name)):
1✔
2094
            fn_arn = api_utils.qualified_lambda_arn(function_name, name, account_id, region)
1✔
2095
            raise ResourceNotFoundException(
1✔
2096
                f"Alias not found: {fn_arn}",
2097
                Type="User",
2098
            )
2099
        if revision_id and alias.revision_id != revision_id:
1✔
2100
            raise PreconditionFailedException(
1✔
2101
                "The Revision Id provided does not match the latest Revision Id. "
2102
                "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
2103
                Type="User",
2104
            )
2105
        changes = {}
1✔
2106
        if function_version is not None:
1✔
2107
            changes |= {"function_version": function_version}
1✔
2108
        if description is not None:
1✔
2109
            changes |= {"description": description}
1✔
2110
        if routing_config is not None:
1✔
2111
            # if it is an empty dict or AdditionalVersionWeights is empty, set routing config to None
2112
            new_routing_config = None
1✔
2113
            if routing_config_dict := routing_config.get("AdditionalVersionWeights"):
1✔
UNCOV
2114
                new_routing_config = self._create_routing_config_model(routing_config_dict)
×
2115
            changes |= {"routing_configuration": new_routing_config}
1✔
2116
        # even if no changes are done, we have to update revision id for some reason
2117
        old_alias = alias
1✔
2118
        alias = dataclasses.replace(alias, **changes)
1✔
2119
        function.aliases[name] = alias
1✔
2120

2121
        # TODO: signal lambda service that pointer potentially changed
2122
        self.lambda_service.update_alias(old_alias=old_alias, new_alias=alias, function=function)
1✔
2123

2124
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
2125

2126
    # =======================================
2127
    # ======= EVENT SOURCE MAPPINGS =========
2128
    # =======================================
2129
    def check_service_resource_exists(
1✔
2130
        self, service: str, resource_arn: str, function_arn: str, function_role_arn: str
2131
    ):
2132
        """
2133
        Check if the service resource exists and if the function has access to it.
2134

2135
        Raises:
2136
            InvalidParameterValueException: If the service resource does not exist or the function does not have access to it.
2137
        """
2138
        arn = parse_arn(resource_arn)
1✔
2139
        source_client = get_internal_client(
1✔
2140
            arn=resource_arn,
2141
            role_arn=function_role_arn,
2142
            service_principal=ServicePrincipal.lambda_,
2143
            source_arn=function_arn,
2144
        )
2145
        if service in ["sqs", "sqs-fifo"]:
1✔
2146
            try:
1✔
2147
                # AWS uses `GetQueueAttributes` internally to verify the queue existence, but we need the `QueueUrl`
2148
                # which is not given directly. We build out a dummy `QueueUrl` which can be parsed by SQS to return
2149
                # the right value
2150
                queue_name = arn["resource"].split("/")[-1]
1✔
2151
                queue_url = f"http://sqs.{arn['region']}.domain/{arn['account']}/{queue_name}"
1✔
2152
                source_client.get_queue_attributes(QueueUrl=queue_url)
1✔
2153
            except ClientError as e:
1✔
2154
                error_code = e.response["Error"]["Code"]
1✔
2155
                if error_code == "AWS.SimpleQueueService.NonExistentQueue":
1✔
2156
                    raise InvalidParameterValueException(
1✔
2157
                        f"Error occurred while ReceiveMessage. SQS Error Code: {error_code}. SQS Error Message: {e.response['Error']['Message']}",
2158
                        Type="User",
2159
                    )
UNCOV
2160
                raise e
×
2161
        elif service in ["kinesis"]:
1✔
2162
            try:
1✔
2163
                source_client.describe_stream(StreamARN=resource_arn)
1✔
2164
            except ClientError as e:
1✔
2165
                if e.response["Error"]["Code"] == "ResourceNotFoundException":
1✔
2166
                    raise InvalidParameterValueException(
1✔
2167
                        f"Stream not found: {resource_arn}",
2168
                        Type="User",
2169
                    )
UNCOV
2170
                raise e
×
2171
        elif service in ["dynamodb"]:
1✔
2172
            try:
1✔
2173
                source_client.describe_stream(StreamArn=resource_arn)
1✔
2174
            except ClientError as e:
1✔
2175
                if e.response["Error"]["Code"] == "ResourceNotFoundException":
1✔
2176
                    raise InvalidParameterValueException(
1✔
2177
                        f"Stream not found: {resource_arn}",
2178
                        Type="User",
2179
                    )
UNCOV
2180
                raise e
×
2181

2182
    @handler("CreateEventSourceMapping", expand=False)
1✔
2183
    def create_event_source_mapping(
1✔
2184
        self,
2185
        context: RequestContext,
2186
        request: CreateEventSourceMappingRequest,
2187
    ) -> EventSourceMappingConfiguration:
2188
        return self.create_event_source_mapping_v2(context, request)
1✔
2189

2190
    def create_event_source_mapping_v2(
1✔
2191
        self,
2192
        context: RequestContext,
2193
        request: CreateEventSourceMappingRequest,
2194
    ) -> EventSourceMappingConfiguration:
2195
        # Validations
2196
        function_arn, function_name, state, function_version, function_role = (
1✔
2197
            self.validate_event_source_mapping(context, request)
2198
        )
2199

2200
        esm_config = EsmConfigFactory(request, context, function_arn).get_esm_config()
1✔
2201

2202
        # Copy esm_config to avoid a race condition with potential async update in the store
2203
        state.event_source_mappings[esm_config["UUID"]] = esm_config.copy()
1✔
2204
        enabled = request.get("Enabled", True)
1✔
2205
        # TODO: check for potential async race condition update -> think about locking
2206
        esm_worker = EsmWorkerFactory(esm_config, function_role, enabled).get_esm_worker()
1✔
2207
        self.esm_workers[esm_worker.uuid] = esm_worker
1✔
2208
        # TODO: check StateTransitionReason, LastModified, LastProcessingResult (concurrent updates requires locking!)
2209
        if tags := request.get("Tags"):
1✔
2210
            self._store_tags(esm_config.get("EventSourceMappingArn"), tags)
1✔
2211
        esm_worker.create()
1✔
2212
        return esm_config
1✔
2213

2214
    def validate_event_source_mapping(self, context, request):
1✔
2215
        # TODO: test whether stream ARNs are valid sources for Pipes or ESM or whether only DynamoDB table ARNs work
2216
        # TODO: Validate MaxRecordAgeInSeconds (i.e cannot subceed 60s but can be -1) and MaxRetryAttempts parameters.
2217
        # See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-maximumrecordageinseconds
2218
        is_create_esm_request = context.operation.name == self.create_event_source_mapping.operation
1✔
2219

2220
        if destination_config := request.get("DestinationConfig"):
1✔
2221
            if "OnSuccess" in destination_config:
1✔
2222
                raise InvalidParameterValueException(
1✔
2223
                    "Unsupported DestinationConfig parameter for given event source mapping type.",
2224
                    Type="User",
2225
                )
2226

2227
        service = None
1✔
2228
        if "SelfManagedEventSource" in request:
1✔
UNCOV
2229
            service = "kafka"
×
UNCOV
2230
            if "SourceAccessConfigurations" not in request:
×
UNCOV
2231
                raise InvalidParameterValueException(
×
2232
                    "Required 'sourceAccessConfigurations' parameter is missing.", Type="User"
2233
                )
2234
        if service is None and "EventSourceArn" not in request:
1✔
2235
            raise InvalidParameterValueException("Unrecognized event source.", Type="User")
1✔
2236
        if service is None:
1✔
2237
            service = extract_service_from_arn(request["EventSourceArn"])
1✔
2238

2239
        batch_size = api_utils.validate_and_set_batch_size(service, request.get("BatchSize"))
1✔
2240
        if service in ["dynamodb", "kinesis"]:
1✔
2241
            starting_position = request.get("StartingPosition")
1✔
2242
            if not starting_position:
1✔
2243
                raise InvalidParameterValueException(
1✔
2244
                    "1 validation error detected: Value null at 'startingPosition' failed to satisfy constraint: Member must not be null.",
2245
                    Type="User",
2246
                )
2247

2248
            if starting_position not in KinesisStreamStartPosition.__members__:
1✔
2249
                raise ValidationException(
1✔
2250
                    f"1 validation error detected: Value '{starting_position}' at 'startingPosition' failed to satisfy constraint: Member must satisfy enum value set: [LATEST, AT_TIMESTAMP, TRIM_HORIZON]"
2251
                )
2252
            # AT_TIMESTAMP is not allowed for DynamoDB Streams
2253
            elif (
1✔
2254
                service == "dynamodb"
2255
                and starting_position not in DynamoDBStreamStartPosition.__members__
2256
            ):
2257
                raise InvalidParameterValueException(
1✔
2258
                    f"Unsupported starting position for arn type: {request['EventSourceArn']}",
2259
                    Type="User",
2260
                )
2261

2262
        if service in ["sqs", "sqs-fifo"]:
1✔
2263
            if batch_size > 10 and request.get("MaximumBatchingWindowInSeconds", 0) == 0:
1✔
2264
                raise InvalidParameterValueException(
1✔
2265
                    "Maximum batch window in seconds must be greater than 0 if maximum batch size is greater than 10",
2266
                    Type="User",
2267
                )
2268

2269
        if (filter_criteria := request.get("FilterCriteria")) is not None:
1✔
2270
            for filter_ in filter_criteria.get("Filters", []):
1✔
2271
                pattern_str = filter_.get("Pattern")
1✔
2272
                if not pattern_str or not isinstance(pattern_str, str):
1✔
UNCOV
2273
                    raise InvalidParameterValueException(
×
2274
                        "Invalid filter pattern definition.", Type="User"
2275
                    )
2276

2277
                if not validate_event_pattern(pattern_str):
1✔
2278
                    raise InvalidParameterValueException(
1✔
2279
                        "Invalid filter pattern definition.", Type="User"
2280
                    )
2281

2282
        # Can either have a FunctionName (i.e CreateEventSourceMapping request) or
2283
        # an internal EventSourceMappingConfiguration representation
2284
        request_function_name = request.get("FunctionName") or request.get("FunctionArn")
1✔
2285
        # can be either a partial arn or a full arn for the version/alias
2286
        function_name, qualifier, account, region = function_locators_from_arn(
1✔
2287
            request_function_name
2288
        )
2289
        # TODO: validate `context.region` vs. `region(request["FunctionName"])` vs. `region(request["EventSourceArn"])`
2290
        account = account or context.account_id
1✔
2291
        region = region or context.region
1✔
2292
        state = lambda_stores[account][region]
1✔
2293
        fn = state.functions.get(function_name)
1✔
2294
        if not fn:
1✔
2295
            raise InvalidParameterValueException("Function does not exist", Type="User")
1✔
2296

2297
        if qualifier:
1✔
2298
            # make sure the function version/alias exists
2299
            if api_utils.qualifier_is_alias(qualifier):
1✔
2300
                fn_alias = fn.aliases.get(qualifier)
1✔
2301
                if not fn_alias:
1✔
2302
                    raise Exception("unknown alias")  # TODO: cover via test
×
2303
            elif api_utils.qualifier_is_version(qualifier):
1✔
2304
                fn_version = fn.versions.get(qualifier)
1✔
2305
                if not fn_version:
1✔
2306
                    raise Exception("unknown version")  # TODO: cover via test
×
2307
            elif qualifier == "$LATEST":
1✔
2308
                pass
1✔
UNCOV
2309
            elif qualifier == "$LATEST.PUBLISHED":
×
UNCOV
2310
                if fn.versions.get(qualifier):
×
UNCOV
2311
                    pass
×
2312
            else:
UNCOV
2313
                raise Exception("invalid functionname")  # TODO: cover via test
×
2314
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account, region)
1✔
2315

2316
        else:
2317
            fn_arn = api_utils.unqualified_lambda_arn(function_name, account, region)
1✔
2318

2319
        function_version = get_function_version_from_arn(fn_arn)
1✔
2320
        function_role = function_version.config.role
1✔
2321

2322
        if source_arn := request.get("EventSourceArn"):
1✔
2323
            self.check_service_resource_exists(service, source_arn, fn_arn, function_role)
1✔
2324
        # Check we are validating a CreateEventSourceMapping request
2325
        if is_create_esm_request:
1✔
2326

2327
            def _get_mapping_sources(mapping: dict[str, Any]) -> list[str]:
1✔
2328
                if event_source_arn := mapping.get("EventSourceArn"):
1✔
2329
                    return [event_source_arn]
1✔
UNCOV
2330
                return (
×
2331
                    mapping.get("SelfManagedEventSource", {})
2332
                    .get("Endpoints", {})
2333
                    .get("KAFKA_BOOTSTRAP_SERVERS", [])
2334
                )
2335

2336
            # check for event source duplicates
2337
            # TODO: currently validated for sqs, kinesis, and dynamodb
2338
            service_id = load_service(service).service_id
1✔
2339
            for uuid, mapping in state.event_source_mappings.items():
1✔
2340
                mapping_sources = _get_mapping_sources(mapping)
1✔
2341
                request_sources = _get_mapping_sources(request)
1✔
2342
                if mapping["FunctionArn"] == fn_arn and (
1✔
2343
                    set(mapping_sources).intersection(request_sources)
2344
                ):
2345
                    if service == "sqs":
1✔
2346
                        # *shakes fist at SQS*
2347
                        raise ResourceConflictException(
1✔
2348
                            f'An event source mapping with {service_id} arn (" {mapping["EventSourceArn"]} ") '
2349
                            f'and function (" {function_name} ") already exists. Please update or delete the '
2350
                            f"existing mapping with UUID {uuid}",
2351
                            Type="User",
2352
                        )
2353
                    elif service == "kafka":
1✔
UNCOV
2354
                        if set(mapping["Topics"]).intersection(request["Topics"]):
×
UNCOV
2355
                            raise ResourceConflictException(
×
2356
                                f'An event source mapping with event source ("{",".join(request_sources)}"), '
2357
                                f'function ("{fn_arn}"), '
2358
                                f'topics ("{",".join(request["Topics"])}") already exists. Please update or delete the '
2359
                                f"existing mapping with UUID {uuid}",
2360
                                Type="User",
2361
                            )
2362
                    else:
2363
                        raise ResourceConflictException(
1✔
2364
                            f'The event source arn (" {mapping["EventSourceArn"]} ") and function '
2365
                            f'(" {function_name} ") provided mapping already exists. Please update or delete the '
2366
                            f"existing mapping with UUID {uuid}",
2367
                            Type="User",
2368
                        )
2369
        return fn_arn, function_name, state, function_version, function_role
1✔
2370

2371
    @handler("UpdateEventSourceMapping", expand=False)
1✔
2372
    def update_event_source_mapping(
1✔
2373
        self,
2374
        context: RequestContext,
2375
        request: UpdateEventSourceMappingRequest,
2376
    ) -> EventSourceMappingConfiguration:
2377
        return self.update_event_source_mapping_v2(context, request)
1✔
2378

2379
    def update_event_source_mapping_v2(
1✔
2380
        self,
2381
        context: RequestContext,
2382
        request: UpdateEventSourceMappingRequest,
2383
    ) -> EventSourceMappingConfiguration:
2384
        # TODO: test and implement this properly (quite complex with many validations and limitations!)
2385
        LOG.warning(
1✔
2386
            "Updating Lambda Event Source Mapping is in experimental state and not yet fully tested."
2387
        )
2388
        state = lambda_stores[context.account_id][context.region]
1✔
2389
        request_data = {**request}
1✔
2390
        uuid = request_data.pop("UUID", None)
1✔
2391
        if not uuid:
1✔
UNCOV
2392
            raise ResourceNotFoundException(
×
2393
                "The resource you requested does not exist.", Type="User"
2394
            )
2395
        old_event_source_mapping = state.event_source_mappings.get(uuid)
1✔
2396
        esm_worker = self.esm_workers.get(uuid)
1✔
2397
        if old_event_source_mapping is None or esm_worker is None:
1✔
2398
            raise ResourceNotFoundException(
1✔
2399
                "The resource you requested does not exist.", Type="User"
2400
            )  # TODO: test?
2401

2402
        # normalize values to overwrite
2403
        event_source_mapping = old_event_source_mapping | request_data
1✔
2404

2405
        temp_params = {}  # values only set for the returned response, not saved internally (e.g. transient state)
1✔
2406

2407
        # Validate the newly updated ESM object. We ignore the output here since we only care whether an Exception is raised.
2408
        function_arn, _, _, function_version, function_role = self.validate_event_source_mapping(
1✔
2409
            context, event_source_mapping
2410
        )
2411

2412
        # remove the FunctionName field
2413
        event_source_mapping.pop("FunctionName", None)
1✔
2414

2415
        if function_arn:
1✔
2416
            event_source_mapping["FunctionArn"] = function_arn
1✔
2417

2418
        # Only apply update if the desired state differs
2419
        enabled = request.get("Enabled")
1✔
2420
        if enabled is not None:
1✔
2421
            if enabled and old_event_source_mapping["State"] != EsmState.ENABLED:
1✔
2422
                event_source_mapping["State"] = EsmState.ENABLING
1✔
2423
            # TODO: What happens when trying to update during an update or failed state?!
2424
            elif not enabled and old_event_source_mapping["State"] == EsmState.ENABLED:
1✔
2425
                event_source_mapping["State"] = EsmState.DISABLING
1✔
2426
        else:
2427
            event_source_mapping["State"] = EsmState.UPDATING
1✔
2428

2429
        # To ensure parity, certain responses need to be immediately returned
2430
        temp_params["State"] = event_source_mapping["State"]
1✔
2431

2432
        state.event_source_mappings[uuid] = event_source_mapping
1✔
2433

2434
        # TODO: Currently, we re-create the entire ESM worker. Look into approach with better performance.
2435
        worker_factory = EsmWorkerFactory(
1✔
2436
            event_source_mapping, function_role, request.get("Enabled", esm_worker.enabled)
2437
        )
2438

2439
        # Get a new ESM worker object but do not active it, since the factory holds all logic for creating new worker from configuration.
2440
        updated_esm_worker = worker_factory.get_esm_worker()
1✔
2441
        self.esm_workers[uuid] = updated_esm_worker
1✔
2442

2443
        # We should stop() the worker since the delete() will remove the ESM from the state mapping.
2444
        esm_worker.stop()
1✔
2445
        # This will either create an EsmWorker in the CREATING state if enabled. Otherwise, the DISABLING state is set.
2446
        updated_esm_worker.create()
1✔
2447

2448
        return {**event_source_mapping, **temp_params}
1✔
2449

2450
    def delete_event_source_mapping(
1✔
2451
        self, context: RequestContext, uuid: String, **kwargs
2452
    ) -> EventSourceMappingConfiguration:
2453
        state = lambda_stores[context.account_id][context.region]
1✔
2454
        event_source_mapping = state.event_source_mappings.get(uuid)
1✔
2455
        if not event_source_mapping:
1✔
2456
            raise ResourceNotFoundException(
1✔
2457
                "The resource you requested does not exist.", Type="User"
2458
            )
2459
        esm = state.event_source_mappings[uuid]
1✔
2460
        # TODO: add proper locking
2461
        esm_worker = self.esm_workers.pop(uuid, None)
1✔
2462
        # Asynchronous delete in v2
2463
        if not esm_worker:
1✔
UNCOV
2464
            raise ResourceNotFoundException(
×
2465
                "The resource you requested does not exist.", Type="User"
2466
            )
2467
        esm_worker.delete()
1✔
2468
        return {**esm, "State": EsmState.DELETING}
1✔
2469

2470
    def get_event_source_mapping(
1✔
2471
        self, context: RequestContext, uuid: String, **kwargs
2472
    ) -> EventSourceMappingConfiguration:
2473
        state = lambda_stores[context.account_id][context.region]
1✔
2474
        event_source_mapping = state.event_source_mappings.get(uuid)
1✔
2475
        if not event_source_mapping:
1✔
2476
            raise ResourceNotFoundException(
1✔
2477
                "The resource you requested does not exist.", Type="User"
2478
            )
2479
        esm_worker = self.esm_workers.get(uuid)
1✔
2480
        if not esm_worker:
1✔
UNCOV
2481
            raise ResourceNotFoundException(
×
2482
                "The resource you requested does not exist.", Type="User"
2483
            )
2484
        event_source_mapping["State"] = esm_worker.current_state
1✔
2485
        event_source_mapping["StateTransitionReason"] = esm_worker.state_transition_reason
1✔
2486
        return event_source_mapping
1✔
2487

2488
    def list_event_source_mappings(
1✔
2489
        self,
2490
        context: RequestContext,
2491
        event_source_arn: Arn = None,
2492
        function_name: FunctionName = None,
2493
        marker: String = None,
2494
        max_items: MaxListItems = None,
2495
        **kwargs,
2496
    ) -> ListEventSourceMappingsResponse:
2497
        state = lambda_stores[context.account_id][context.region]
1✔
2498

2499
        esms = state.event_source_mappings.values()
1✔
2500
        # TODO: update and test State and StateTransitionReason for ESM v2
2501

2502
        if event_source_arn:  # TODO: validate pattern
1✔
2503
            esms = [e for e in esms if e.get("EventSourceArn") == event_source_arn]
1✔
2504

2505
        if function_name:
1✔
2506
            esms = [e for e in esms if function_name in e["FunctionArn"]]
1✔
2507

2508
        esms = PaginatedList(esms)
1✔
2509
        page, token = esms.get_page(
1✔
2510
            lambda x: x["UUID"],
2511
            marker,
2512
            max_items,
2513
        )
2514
        return ListEventSourceMappingsResponse(EventSourceMappings=page, NextMarker=token)
1✔
2515

2516
    def get_source_type_from_request(self, request: dict[str, Any]) -> str:
1✔
UNCOV
2517
        if event_source_arn := request.get("EventSourceArn", ""):
×
UNCOV
2518
            service = extract_service_from_arn(event_source_arn)
×
UNCOV
2519
            if service == "sqs" and "fifo" in event_source_arn:
×
UNCOV
2520
                service = "sqs-fifo"
×
UNCOV
2521
            return service
×
UNCOV
2522
        elif request.get("SelfManagedEventSource"):
×
UNCOV
2523
            return "kafka"
×
2524

2525
    # =======================================
2526
    # ============ FUNCTION URLS ============
2527
    # =======================================
2528

2529
    @staticmethod
1✔
2530
    def _validate_qualifier(qualifier: str) -> None:
1✔
2531
        if qualifier in ["$LATEST", "$LATEST.PUBLISHED"] or (
1✔
2532
            qualifier and api_utils.qualifier_is_version(qualifier)
2533
        ):
2534
            raise ValidationException(
1✔
2535
                f"1 validation error detected: Value '{qualifier}' at 'qualifier' failed to satisfy constraint: Member must satisfy regular expression pattern: ((?!^\\d+$)^[0-9a-zA-Z-_]+$)"
2536
            )
2537

2538
    @staticmethod
1✔
2539
    def _validate_invoke_mode(invoke_mode: str) -> None:
1✔
2540
        if invoke_mode and invoke_mode not in [InvokeMode.BUFFERED, InvokeMode.RESPONSE_STREAM]:
1✔
2541
            raise ValidationException(
1✔
2542
                f"1 validation error detected: Value '{invoke_mode}' at 'invokeMode' failed to satisfy constraint: Member must satisfy enum value set: [RESPONSE_STREAM, BUFFERED]"
2543
            )
2544
        if invoke_mode == InvokeMode.RESPONSE_STREAM:
1✔
2545
            # TODO should we actually fail for setting RESPONSE_STREAM?
2546
            #  It should trigger InvokeWithResponseStream which is not implemented
2547
            LOG.warning(
1✔
2548
                "The invokeMode 'RESPONSE_STREAM' is not yet supported on LocalStack. The property is only mocked, the execution will still be 'BUFFERED'"
2549
            )
2550

2551
    # TODO: what happens if function state is not active?
2552
    def create_function_url_config(
1✔
2553
        self,
2554
        context: RequestContext,
2555
        function_name: FunctionName,
2556
        auth_type: FunctionUrlAuthType,
2557
        qualifier: FunctionUrlQualifier = None,
2558
        cors: Cors = None,
2559
        invoke_mode: InvokeMode = None,
2560
        **kwargs,
2561
    ) -> CreateFunctionUrlConfigResponse:
2562
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2563
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2564
            function_name, qualifier, context
2565
        )
2566
        state = lambda_stores[account_id][region]
1✔
2567
        self._validate_qualifier(qualifier)
1✔
2568
        self._validate_invoke_mode(invoke_mode)
1✔
2569

2570
        fn = state.functions.get(function_name)
1✔
2571
        if fn is None:
1✔
2572
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2573

2574
        url_config = fn.function_url_configs.get(qualifier or "$LATEST")
1✔
2575
        if url_config:
1✔
2576
            raise ResourceConflictException(
1✔
2577
                f"Failed to create function url config for [functionArn = {url_config.function_arn}]. Error message:  FunctionUrlConfig exists for this Lambda function",
2578
                Type="User",
2579
            )
2580

2581
        if qualifier and qualifier != "$LATEST" and qualifier not in fn.aliases:
1✔
2582
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2583

2584
        normalized_qualifier = qualifier or "$LATEST"
1✔
2585

2586
        function_arn = (
1✔
2587
            api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
2588
            if qualifier
2589
            else api_utils.unqualified_lambda_arn(function_name, account_id, region)
2590
        )
2591

2592
        custom_id: str | None = None
1✔
2593

2594
        tags = self._get_tags(api_utils.unqualified_lambda_arn(function_name, account_id, region))
1✔
2595
        if TAG_KEY_CUSTOM_URL in tags:
1✔
2596
            # Note: I really wanted to add verification here that the
2597
            # url_id is unique, so we could surface that to the user ASAP.
2598
            # However, it seems like that information isn't available yet,
2599
            # since (as far as I can tell) we call
2600
            # self.router.register_routes() once, in a single shot, for all
2601
            # of the routes -- and we need to verify that it's unique not
2602
            # just for this particular lambda function, but for the entire
2603
            # lambda provider. Therefore... that idea proved non-trivial!
2604
            custom_id_tag_value = (
1✔
2605
                f"{tags[TAG_KEY_CUSTOM_URL]}-{qualifier}" if qualifier else tags[TAG_KEY_CUSTOM_URL]
2606
            )
2607
            if TAG_KEY_CUSTOM_URL_VALIDATOR.match(custom_id_tag_value):
1✔
2608
                custom_id = custom_id_tag_value
1✔
2609

2610
            else:
2611
                # Note: we're logging here instead of raising to prioritize
2612
                # strict parity with AWS over the localstack-only custom_id
2613
                LOG.warning(
1✔
2614
                    "Invalid custom ID tag value for lambda URL (%s=%s). "
2615
                    "Replaced with default (random id)",
2616
                    TAG_KEY_CUSTOM_URL,
2617
                    custom_id_tag_value,
2618
                )
2619

2620
        # The url_id is the subdomain used for the URL we're creating. This
2621
        # is either created randomly (as in AWS), or can be passed as a tag
2622
        # to the lambda itself (localstack-only).
2623
        url_id: str
2624
        if custom_id is None:
1✔
2625
            url_id = api_utils.generate_random_url_id()
1✔
2626
        else:
2627
            url_id = custom_id
1✔
2628

2629
        host_definition = localstack_host(custom_port=config.GATEWAY_LISTEN[0].port)
1✔
2630
        fn.function_url_configs[normalized_qualifier] = FunctionUrlConfig(
1✔
2631
            function_arn=function_arn,
2632
            function_name=function_name,
2633
            cors=cors,
2634
            url_id=url_id,
2635
            url=f"http://{url_id}.lambda-url.{context.region}.{host_definition.host_and_port()}/",  # TODO: https support
2636
            auth_type=auth_type,
2637
            creation_time=api_utils.generate_lambda_date(),
2638
            last_modified_time=api_utils.generate_lambda_date(),
2639
            invoke_mode=invoke_mode,
2640
        )
2641

2642
        # persist and start URL
2643
        # TODO: implement URL invoke
2644
        api_url_config = api_utils.map_function_url_config(
1✔
2645
            fn.function_url_configs[normalized_qualifier]
2646
        )
2647

2648
        return CreateFunctionUrlConfigResponse(
1✔
2649
            FunctionUrl=api_url_config["FunctionUrl"],
2650
            FunctionArn=api_url_config["FunctionArn"],
2651
            AuthType=api_url_config["AuthType"],
2652
            Cors=api_url_config["Cors"],
2653
            CreationTime=api_url_config["CreationTime"],
2654
            InvokeMode=api_url_config["InvokeMode"],
2655
        )
2656

2657
    def get_function_url_config(
1✔
2658
        self,
2659
        context: RequestContext,
2660
        function_name: FunctionName,
2661
        qualifier: FunctionUrlQualifier = None,
2662
        **kwargs,
2663
    ) -> GetFunctionUrlConfigResponse:
2664
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2665
        state = lambda_stores[account_id][region]
1✔
2666

2667
        fn_name, qualifier = api_utils.get_name_and_qualifier(function_name, qualifier, context)
1✔
2668

2669
        self._validate_qualifier(qualifier)
1✔
2670

2671
        resolved_fn = state.functions.get(fn_name)
1✔
2672
        if not resolved_fn:
1✔
2673
            raise ResourceNotFoundException(
1✔
2674
                "The resource you requested does not exist.", Type="User"
2675
            )
2676

2677
        qualifier = qualifier or "$LATEST"
1✔
2678
        url_config = resolved_fn.function_url_configs.get(qualifier)
1✔
2679
        if not url_config:
1✔
2680
            raise ResourceNotFoundException(
1✔
2681
                "The resource you requested does not exist.", Type="User"
2682
            )
2683

2684
        return api_utils.map_function_url_config(url_config)
1✔
2685

2686
    def update_function_url_config(
1✔
2687
        self,
2688
        context: RequestContext,
2689
        function_name: FunctionName,
2690
        qualifier: FunctionUrlQualifier = None,
2691
        auth_type: FunctionUrlAuthType = None,
2692
        cors: Cors = None,
2693
        invoke_mode: InvokeMode = None,
2694
        **kwargs,
2695
    ) -> UpdateFunctionUrlConfigResponse:
2696
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2697
        state = lambda_stores[account_id][region]
1✔
2698

2699
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2700
            function_name, qualifier, context
2701
        )
2702
        self._validate_qualifier(qualifier)
1✔
2703
        self._validate_invoke_mode(invoke_mode)
1✔
2704

2705
        fn = state.functions.get(function_name)
1✔
2706
        if not fn:
1✔
2707
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2708

2709
        normalized_qualifier = qualifier or "$LATEST"
1✔
2710

2711
        if (
1✔
2712
            api_utils.qualifier_is_alias(normalized_qualifier)
2713
            and normalized_qualifier not in fn.aliases
2714
        ):
2715
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2716

2717
        url_config = fn.function_url_configs.get(normalized_qualifier)
1✔
2718
        if not url_config:
1✔
2719
            raise ResourceNotFoundException(
1✔
2720
                "The resource you requested does not exist.", Type="User"
2721
            )
2722

2723
        changes = {
1✔
2724
            "last_modified_time": api_utils.generate_lambda_date(),
2725
            **({"cors": cors} if cors is not None else {}),
2726
            **({"auth_type": auth_type} if auth_type is not None else {}),
2727
        }
2728

2729
        if invoke_mode:
1✔
2730
            changes["invoke_mode"] = invoke_mode
1✔
2731

2732
        new_url_config = dataclasses.replace(url_config, **changes)
1✔
2733
        fn.function_url_configs[normalized_qualifier] = new_url_config
1✔
2734

2735
        return UpdateFunctionUrlConfigResponse(
1✔
2736
            FunctionUrl=new_url_config.url,
2737
            FunctionArn=new_url_config.function_arn,
2738
            AuthType=new_url_config.auth_type,
2739
            Cors=new_url_config.cors,
2740
            CreationTime=new_url_config.creation_time,
2741
            LastModifiedTime=new_url_config.last_modified_time,
2742
            InvokeMode=new_url_config.invoke_mode,
2743
        )
2744

2745
    def delete_function_url_config(
1✔
2746
        self,
2747
        context: RequestContext,
2748
        function_name: FunctionName,
2749
        qualifier: FunctionUrlQualifier = None,
2750
        **kwargs,
2751
    ) -> None:
2752
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2753
        state = lambda_stores[account_id][region]
1✔
2754

2755
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2756
            function_name, qualifier, context
2757
        )
2758
        self._validate_qualifier(qualifier)
1✔
2759

2760
        resolved_fn = state.functions.get(function_name)
1✔
2761
        if not resolved_fn:
1✔
2762
            raise ResourceNotFoundException(
1✔
2763
                "The resource you requested does not exist.", Type="User"
2764
            )
2765

2766
        qualifier = qualifier or "$LATEST"
1✔
2767
        url_config = resolved_fn.function_url_configs.get(qualifier)
1✔
2768
        if not url_config:
1✔
2769
            raise ResourceNotFoundException(
1✔
2770
                "The resource you requested does not exist.", Type="User"
2771
            )
2772

2773
        del resolved_fn.function_url_configs[qualifier]
1✔
2774

2775
    def list_function_url_configs(
1✔
2776
        self,
2777
        context: RequestContext,
2778
        function_name: FunctionName,
2779
        marker: String = None,
2780
        max_items: MaxItems = None,
2781
        **kwargs,
2782
    ) -> ListFunctionUrlConfigsResponse:
2783
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2784
        state = lambda_stores[account_id][region]
1✔
2785

2786
        fn_name = api_utils.get_function_name(function_name, context)
1✔
2787
        resolved_fn = state.functions.get(fn_name)
1✔
2788
        if not resolved_fn:
1✔
2789
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2790

2791
        url_configs = [
1✔
2792
            api_utils.map_function_url_config(fn_conf)
2793
            for fn_conf in resolved_fn.function_url_configs.values()
2794
        ]
2795
        url_configs = PaginatedList(url_configs)
1✔
2796
        page, token = url_configs.get_page(
1✔
2797
            lambda url_config: url_config["FunctionArn"],
2798
            marker,
2799
            max_items,
2800
        )
2801
        url_configs = page
1✔
2802
        return ListFunctionUrlConfigsResponse(FunctionUrlConfigs=url_configs, NextMarker=token)
1✔
2803

2804
    # =======================================
2805
    # ============  Permissions  ============
2806
    # =======================================
2807

2808
    @handler("AddPermission", expand=False)
1✔
2809
    def add_permission(
1✔
2810
        self,
2811
        context: RequestContext,
2812
        request: AddPermissionRequest,
2813
    ) -> AddPermissionResponse:
2814
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2815
            request.get("FunctionName"), request.get("Qualifier"), context
2816
        )
2817

2818
        # validate qualifier
2819
        if qualifier is not None:
1✔
2820
            self._validate_qualifier_expression(qualifier)
1✔
2821
            if qualifier == "$LATEST":
1✔
2822
                raise InvalidParameterValueException(
1✔
2823
                    "We currently do not support adding policies for $LATEST.", Type="User"
2824
                )
2825
        account_id, region = api_utils.get_account_and_region(request.get("FunctionName"), context)
1✔
2826

2827
        resolved_fn = self._get_function(function_name, account_id, region)
1✔
2828
        resolved_qualifier, fn_arn = self._resolve_fn_qualifier(resolved_fn, qualifier)
1✔
2829

2830
        revision_id = request.get("RevisionId")
1✔
2831
        if revision_id:
1✔
2832
            fn_revision_id = self._function_revision_id(resolved_fn, resolved_qualifier)
1✔
2833
            if revision_id != fn_revision_id:
1✔
2834
                raise PreconditionFailedException(
1✔
2835
                    "The Revision Id provided does not match the latest Revision Id. "
2836
                    "Call the GetPolicy API to retrieve the latest Revision Id",
2837
                    Type="User",
2838
                )
2839

2840
        request_sid = request["StatementId"]
1✔
2841
        if not bool(STATEMENT_ID_REGEX.match(request_sid)):
1✔
2842
            raise ValidationException(
1✔
2843
                f"1 validation error detected: Value '{request_sid}' at 'statementId' failed to satisfy constraint: Member must satisfy regular expression pattern: ([a-zA-Z0-9-_]+)"
2844
            )
2845
        # check for an already existing policy and any conflicts in existing statements
2846
        existing_policy = resolved_fn.permissions.get(resolved_qualifier)
1✔
2847
        if existing_policy:
1✔
2848
            if request_sid in [s["Sid"] for s in existing_policy.policy.Statement]:
1✔
2849
                # uniqueness scope: statement id needs to be unique per qualified function ($LATEST, version, or alias)
2850
                # Counterexample: the same sid can exist within $LATEST, version, and alias
2851
                raise ResourceConflictException(
1✔
2852
                    f"The statement id ({request_sid}) provided already exists. Please provide a new statement id, or remove the existing statement.",
2853
                    Type="User",
2854
                )
2855

2856
        permission_statement = api_utils.build_statement(
1✔
2857
            partition=context.partition,
2858
            resource_arn=fn_arn,
2859
            statement_id=request["StatementId"],
2860
            action=request["Action"],
2861
            principal=request["Principal"],
2862
            source_arn=request.get("SourceArn"),
2863
            source_account=request.get("SourceAccount"),
2864
            principal_org_id=request.get("PrincipalOrgID"),
2865
            event_source_token=request.get("EventSourceToken"),
2866
            auth_type=request.get("FunctionUrlAuthType"),
2867
        )
2868
        new_policy = existing_policy
1✔
2869
        if not existing_policy:
1✔
2870
            new_policy = FunctionResourcePolicy(
1✔
2871
                policy=ResourcePolicy(Version="2012-10-17", Id="default", Statement=[])
2872
            )
2873
        new_policy.policy.Statement.append(permission_statement)
1✔
2874
        if not existing_policy:
1✔
2875
            resolved_fn.permissions[resolved_qualifier] = new_policy
1✔
2876

2877
        # Update revision id of alias or version
2878
        # TODO: re-evaluate data model to prevent this dirty hack just for bumping the revision id
2879
        # TODO: does that need a `with function.lock` for atomic updates of the policy + revision_id?
2880
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
2881
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
1✔
2882
            resolved_fn.aliases[resolved_qualifier] = dataclasses.replace(resolved_alias)
1✔
2883
        # Assumes that a non-alias is a version
2884
        else:
2885
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
2886
            resolved_fn.versions[resolved_qualifier] = dataclasses.replace(
1✔
2887
                resolved_version, config=dataclasses.replace(resolved_version.config)
2888
            )
2889
        return AddPermissionResponse(Statement=json.dumps(permission_statement))
1✔
2890

2891
    def remove_permission(
1✔
2892
        self,
2893
        context: RequestContext,
2894
        function_name: NamespacedFunctionName,
2895
        statement_id: NamespacedStatementId,
2896
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
2897
        revision_id: String | None = None,
2898
        **kwargs,
2899
    ) -> None:
2900
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2901
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2902
            function_name, qualifier, context
2903
        )
2904
        if qualifier is not None:
1✔
2905
            self._validate_qualifier_expression(qualifier)
1✔
2906

2907
        state = lambda_stores[account_id][region]
1✔
2908
        resolved_fn = state.functions.get(function_name)
1✔
2909
        if resolved_fn is None:
1✔
2910
            fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
2911
            raise ResourceNotFoundException(f"No policy found for: {fn_arn}", Type="User")
1✔
2912

2913
        resolved_qualifier, _ = self._resolve_fn_qualifier(resolved_fn, qualifier)
1✔
2914
        function_permission = resolved_fn.permissions.get(resolved_qualifier)
1✔
2915
        if not function_permission:
1✔
2916
            raise ResourceNotFoundException(
1✔
2917
                "No policy is associated with the given resource.", Type="User"
2918
            )
2919

2920
        # try to find statement in policy and delete it
2921
        statement = None
1✔
2922
        for s in function_permission.policy.Statement:
1✔
2923
            if s["Sid"] == statement_id:
1✔
2924
                statement = s
1✔
2925
                break
1✔
2926

2927
        if not statement:
1✔
2928
            raise ResourceNotFoundException(
1✔
2929
                f"Statement {statement_id} is not found in resource policy.", Type="User"
2930
            )
2931
        fn_revision_id = self._function_revision_id(resolved_fn, resolved_qualifier)
1✔
2932
        if revision_id and revision_id != fn_revision_id:
1✔
UNCOV
2933
            raise PreconditionFailedException(
×
2934
                "The Revision Id provided does not match the latest Revision Id. "
2935
                "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
2936
                Type="User",
2937
            )
2938
        function_permission.policy.Statement.remove(statement)
1✔
2939

2940
        # Update revision id for alias or version
2941
        # TODO: re-evaluate data model to prevent this dirty hack just for bumping the revision id
2942
        # TODO: does that need a `with function.lock` for atomic updates of the policy + revision_id?
2943
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
UNCOV
2944
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
×
UNCOV
2945
            resolved_fn.aliases[resolved_qualifier] = dataclasses.replace(resolved_alias)
×
2946
        # Assumes that a non-alias is a version
2947
        else:
2948
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
2949
            resolved_fn.versions[resolved_qualifier] = dataclasses.replace(
1✔
2950
                resolved_version, config=dataclasses.replace(resolved_version.config)
2951
            )
2952

2953
        # remove the policy as a whole when there's no statement left in it
2954
        if len(function_permission.policy.Statement) == 0:
1✔
2955
            del resolved_fn.permissions[resolved_qualifier]
1✔
2956

2957
    def get_policy(
1✔
2958
        self,
2959
        context: RequestContext,
2960
        function_name: NamespacedFunctionName,
2961
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
2962
        **kwargs,
2963
    ) -> GetPolicyResponse:
2964
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2965
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2966
            function_name, qualifier, context
2967
        )
2968

2969
        if qualifier is not None:
1✔
2970
            self._validate_qualifier_expression(qualifier)
1✔
2971

2972
        resolved_fn = self._get_function(function_name, account_id, region)
1✔
2973

2974
        resolved_qualifier = qualifier or "$LATEST"
1✔
2975
        function_permission = resolved_fn.permissions.get(resolved_qualifier)
1✔
2976
        if not function_permission:
1✔
2977
            raise ResourceNotFoundException(
1✔
2978
                "The resource you requested does not exist.", Type="User"
2979
            )
2980

2981
        fn_revision_id = None
1✔
2982
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
2983
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
1✔
2984
            fn_revision_id = resolved_alias.revision_id
1✔
2985
        # Assumes that a non-alias is a version
2986
        else:
2987
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
2988
            fn_revision_id = resolved_version.config.revision_id
1✔
2989

2990
        return GetPolicyResponse(
1✔
2991
            Policy=json.dumps(dataclasses.asdict(function_permission.policy)),
2992
            RevisionId=fn_revision_id,
2993
        )
2994

2995
    # =======================================
2996
    # ========  Code signing config  ========
2997
    # =======================================
2998

2999
    def create_code_signing_config(
1✔
3000
        self,
3001
        context: RequestContext,
3002
        allowed_publishers: AllowedPublishers,
3003
        description: Description | None = None,
3004
        code_signing_policies: CodeSigningPolicies | None = None,
3005
        tags: Tags | None = None,
3006
        **kwargs,
3007
    ) -> CreateCodeSigningConfigResponse:
3008
        account = context.account_id
1✔
3009
        region = context.region
1✔
3010

3011
        state = lambda_stores[account][region]
1✔
3012
        # TODO: can there be duplicates?
3013
        csc_id = f"csc-{get_random_hex(17)}"  # e.g. 'csc-077c33b4c19e26036'
1✔
3014
        csc_arn = f"arn:{context.partition}:lambda:{region}:{account}:code-signing-config:{csc_id}"
1✔
3015
        csc = CodeSigningConfig(
1✔
3016
            csc_id=csc_id,
3017
            arn=csc_arn,
3018
            allowed_publishers=allowed_publishers,
3019
            policies=code_signing_policies,
3020
            last_modified=api_utils.generate_lambda_date(),
3021
            description=description,
3022
        )
3023
        state.code_signing_configs[csc_arn] = csc
1✔
3024
        return CreateCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(csc))
1✔
3025

3026
    def put_function_code_signing_config(
1✔
3027
        self,
3028
        context: RequestContext,
3029
        code_signing_config_arn: CodeSigningConfigArn,
3030
        function_name: NamespacedFunctionName,
3031
        **kwargs,
3032
    ) -> PutFunctionCodeSigningConfigResponse:
3033
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3034
        state = lambda_stores[account_id][region]
1✔
3035
        function_name = api_utils.get_function_name(function_name, context)
1✔
3036

3037
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
3038
        if not csc:
1✔
3039
            raise CodeSigningConfigNotFoundException(
1✔
3040
                f"The code signing configuration cannot be found. Check that the provided configuration is not deleted: {code_signing_config_arn}.",
3041
                Type="User",
3042
            )
3043

3044
        fn = state.functions.get(function_name)
1✔
3045
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
3046
        if not fn:
1✔
3047
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
3048

3049
        fn.code_signing_config_arn = code_signing_config_arn
1✔
3050
        return PutFunctionCodeSigningConfigResponse(
1✔
3051
            CodeSigningConfigArn=code_signing_config_arn, FunctionName=function_name
3052
        )
3053

3054
    def update_code_signing_config(
1✔
3055
        self,
3056
        context: RequestContext,
3057
        code_signing_config_arn: CodeSigningConfigArn,
3058
        description: Description = None,
3059
        allowed_publishers: AllowedPublishers = None,
3060
        code_signing_policies: CodeSigningPolicies = None,
3061
        **kwargs,
3062
    ) -> UpdateCodeSigningConfigResponse:
3063
        state = lambda_stores[context.account_id][context.region]
1✔
3064
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
3065
        if not csc:
1✔
3066
            raise ResourceNotFoundException(
1✔
3067
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
3068
            )
3069

3070
        changes = {
1✔
3071
            **(
3072
                {"allowed_publishers": allowed_publishers} if allowed_publishers is not None else {}
3073
            ),
3074
            **({"policies": code_signing_policies} if code_signing_policies is not None else {}),
3075
            **({"description": description} if description is not None else {}),
3076
        }
3077
        new_csc = dataclasses.replace(
1✔
3078
            csc, last_modified=api_utils.generate_lambda_date(), **changes
3079
        )
3080
        state.code_signing_configs[code_signing_config_arn] = new_csc
1✔
3081

3082
        return UpdateCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(new_csc))
1✔
3083

3084
    def get_code_signing_config(
1✔
3085
        self, context: RequestContext, code_signing_config_arn: CodeSigningConfigArn, **kwargs
3086
    ) -> GetCodeSigningConfigResponse:
3087
        state = lambda_stores[context.account_id][context.region]
1✔
3088
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
3089
        if not csc:
1✔
3090
            raise ResourceNotFoundException(
1✔
3091
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
3092
            )
3093

3094
        return GetCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(csc))
1✔
3095

3096
    def get_function_code_signing_config(
1✔
3097
        self, context: RequestContext, function_name: NamespacedFunctionName, **kwargs
3098
    ) -> GetFunctionCodeSigningConfigResponse:
3099
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3100
        state = lambda_stores[account_id][region]
1✔
3101
        function_name = api_utils.get_function_name(function_name, context)
1✔
3102
        fn = state.functions.get(function_name)
1✔
3103
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
3104
        if not fn:
1✔
3105
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
3106

3107
        if fn.code_signing_config_arn:
1✔
3108
            return GetFunctionCodeSigningConfigResponse(
1✔
3109
                CodeSigningConfigArn=fn.code_signing_config_arn, FunctionName=function_name
3110
            )
3111

3112
        return GetFunctionCodeSigningConfigResponse()
1✔
3113

3114
    def delete_function_code_signing_config(
1✔
3115
        self, context: RequestContext, function_name: NamespacedFunctionName, **kwargs
3116
    ) -> None:
3117
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3118
        state = lambda_stores[account_id][region]
1✔
3119
        function_name = api_utils.get_function_name(function_name, context)
1✔
3120
        fn = state.functions.get(function_name)
1✔
3121
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
3122
        if not fn:
1✔
3123
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
3124

3125
        fn.code_signing_config_arn = None
1✔
3126

3127
    def delete_code_signing_config(
1✔
3128
        self, context: RequestContext, code_signing_config_arn: CodeSigningConfigArn, **kwargs
3129
    ) -> DeleteCodeSigningConfigResponse:
3130
        state = lambda_stores[context.account_id][context.region]
1✔
3131

3132
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
3133
        if not csc:
1✔
3134
            raise ResourceNotFoundException(
1✔
3135
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
3136
            )
3137

3138
        del state.code_signing_configs[code_signing_config_arn]
1✔
3139

3140
        return DeleteCodeSigningConfigResponse()
1✔
3141

3142
    def list_code_signing_configs(
1✔
3143
        self,
3144
        context: RequestContext,
3145
        marker: String = None,
3146
        max_items: MaxListItems = None,
3147
        **kwargs,
3148
    ) -> ListCodeSigningConfigsResponse:
3149
        state = lambda_stores[context.account_id][context.region]
1✔
3150

3151
        cscs = [api_utils.map_csc(csc) for csc in state.code_signing_configs.values()]
1✔
3152
        cscs = PaginatedList(cscs)
1✔
3153
        page, token = cscs.get_page(
1✔
3154
            lambda csc: csc["CodeSigningConfigId"],
3155
            marker,
3156
            max_items,
3157
        )
3158
        return ListCodeSigningConfigsResponse(CodeSigningConfigs=page, NextMarker=token)
1✔
3159

3160
    def list_functions_by_code_signing_config(
1✔
3161
        self,
3162
        context: RequestContext,
3163
        code_signing_config_arn: CodeSigningConfigArn,
3164
        marker: String = None,
3165
        max_items: MaxListItems = None,
3166
        **kwargs,
3167
    ) -> ListFunctionsByCodeSigningConfigResponse:
3168
        account = context.account_id
1✔
3169
        region = context.region
1✔
3170

3171
        state = lambda_stores[account][region]
1✔
3172

3173
        if code_signing_config_arn not in state.code_signing_configs:
1✔
3174
            raise ResourceNotFoundException(
1✔
3175
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
3176
            )
3177

3178
        fn_arns = [
1✔
3179
            api_utils.unqualified_lambda_arn(fn.function_name, account, region)
3180
            for fn in state.functions.values()
3181
            if fn.code_signing_config_arn == code_signing_config_arn
3182
        ]
3183

3184
        cscs = PaginatedList(fn_arns)
1✔
3185
        page, token = cscs.get_page(
1✔
3186
            lambda x: x,
3187
            marker,
3188
            max_items,
3189
        )
3190
        return ListFunctionsByCodeSigningConfigResponse(FunctionArns=page, NextMarker=token)
1✔
3191

3192
    # =======================================
3193
    # =========  Account Settings   =========
3194
    # =======================================
3195

3196
    # CAVE: these settings & usages are *per* region!
3197
    # Lambda quotas: https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html
3198
    def get_account_settings(self, context: RequestContext, **kwargs) -> GetAccountSettingsResponse:
1✔
3199
        state = lambda_stores[context.account_id][context.region]
1✔
3200

3201
        fn_count = 0
1✔
3202
        code_size_sum = 0
1✔
3203
        reserved_concurrency_sum = 0
1✔
3204
        for fn in state.functions.values():
1✔
3205
            fn_count += 1
1✔
3206
            for fn_version in fn.versions.values():
1✔
3207
                # Image-based Lambdas do not have a code attribute and count against the ECR quotas instead
3208
                if fn_version.config.package_type == PackageType.Zip:
1✔
3209
                    code_size_sum += fn_version.config.code.code_size
1✔
3210
            if fn.reserved_concurrent_executions is not None:
1✔
3211
                reserved_concurrency_sum += fn.reserved_concurrent_executions
1✔
3212
            for c in fn.provisioned_concurrency_configs.values():
1✔
3213
                reserved_concurrency_sum += c.provisioned_concurrent_executions
1✔
3214
        for layer in state.layers.values():
1✔
3215
            for layer_version in layer.layer_versions.values():
1✔
3216
                code_size_sum += layer_version.code.code_size
1✔
3217
        return GetAccountSettingsResponse(
1✔
3218
            AccountLimit=AccountLimit(
3219
                TotalCodeSize=config.LAMBDA_LIMITS_TOTAL_CODE_SIZE,
3220
                CodeSizeZipped=config.LAMBDA_LIMITS_CODE_SIZE_ZIPPED,
3221
                CodeSizeUnzipped=config.LAMBDA_LIMITS_CODE_SIZE_UNZIPPED,
3222
                ConcurrentExecutions=config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS,
3223
                UnreservedConcurrentExecutions=config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS
3224
                - reserved_concurrency_sum,
3225
            ),
3226
            AccountUsage=AccountUsage(
3227
                TotalCodeSize=code_size_sum,
3228
                FunctionCount=fn_count,
3229
            ),
3230
        )
3231

3232
    # =======================================
3233
    # ==  Provisioned Concurrency Config   ==
3234
    # =======================================
3235

3236
    def _get_provisioned_config(
1✔
3237
        self, context: RequestContext, function_name: str, qualifier: str
3238
    ) -> ProvisionedConcurrencyConfiguration | None:
3239
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3240
        state = lambda_stores[account_id][region]
1✔
3241
        function_name = api_utils.get_function_name(function_name, context)
1✔
3242
        fn = state.functions.get(function_name)
1✔
3243
        if api_utils.qualifier_is_alias(qualifier):
1✔
3244
            fn_alias = None
1✔
3245
            if fn:
1✔
3246
                fn_alias = fn.aliases.get(qualifier)
1✔
3247
            if fn_alias is None:
1✔
3248
                raise ResourceNotFoundException(
1✔
3249
                    f"Cannot find alias arn: {api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)}",
3250
                    Type="User",
3251
                )
3252
        elif api_utils.qualifier_is_version(qualifier):
1✔
3253
            fn_version = None
1✔
3254
            if fn:
1✔
3255
                fn_version = fn.versions.get(qualifier)
1✔
3256
            if fn_version is None:
1✔
3257
                raise ResourceNotFoundException(
1✔
3258
                    f"Function not found: {api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)}",
3259
                    Type="User",
3260
                )
3261

3262
        return fn.provisioned_concurrency_configs.get(qualifier)
1✔
3263

3264
    def put_provisioned_concurrency_config(
1✔
3265
        self,
3266
        context: RequestContext,
3267
        function_name: FunctionName,
3268
        qualifier: Qualifier,
3269
        provisioned_concurrent_executions: PositiveInteger,
3270
        **kwargs,
3271
    ) -> PutProvisionedConcurrencyConfigResponse:
3272
        if provisioned_concurrent_executions <= 0:
1✔
3273
            raise ValidationException(
1✔
3274
                f"1 validation error detected: Value '{provisioned_concurrent_executions}' at 'provisionedConcurrentExecutions' failed to satisfy constraint: Member must have value greater than or equal to 1"
3275
            )
3276

3277
        if qualifier == "$LATEST":
1✔
3278
            raise InvalidParameterValueException(
1✔
3279
                "Provisioned Concurrency Configs cannot be applied to unpublished function versions.",
3280
                Type="User",
3281
            )
3282
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3283
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3284
            function_name, qualifier, context
3285
        )
3286
        state = lambda_stores[account_id][region]
1✔
3287
        fn = state.functions.get(function_name)
1✔
3288

3289
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3290

3291
        if provisioned_config:  # TODO: merge?
1✔
3292
            # TODO: add a test for partial updates (if possible)
3293
            LOG.warning(
1✔
3294
                "Partial update of provisioned concurrency config is currently not supported."
3295
            )
3296

3297
        other_provisioned_sum = sum(
1✔
3298
            [
3299
                provisioned_configs.provisioned_concurrent_executions
3300
                for provisioned_qualifier, provisioned_configs in fn.provisioned_concurrency_configs.items()
3301
                if provisioned_qualifier != qualifier
3302
            ]
3303
        )
3304

3305
        if (
1✔
3306
            fn.reserved_concurrent_executions is not None
3307
            and fn.reserved_concurrent_executions
3308
            < other_provisioned_sum + provisioned_concurrent_executions
3309
        ):
3310
            raise InvalidParameterValueException(
1✔
3311
                "Requested Provisioned Concurrency should not be greater than the reservedConcurrentExecution for function",
3312
                Type="User",
3313
            )
3314

3315
        if provisioned_concurrent_executions > config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS:
1✔
3316
            raise InvalidParameterValueException(
1✔
3317
                f"Specified ConcurrentExecutions for function is greater than account's unreserved concurrency"
3318
                f" [{config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS}]."
3319
            )
3320

3321
        settings = self.get_account_settings(context)
1✔
3322
        unreserved_concurrent_executions = settings["AccountLimit"][
1✔
3323
            "UnreservedConcurrentExecutions"
3324
        ]
3325
        if (
1✔
3326
            unreserved_concurrent_executions - provisioned_concurrent_executions
3327
            < config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY
3328
        ):
3329
            raise InvalidParameterValueException(
1✔
3330
                f"Specified ConcurrentExecutions for function decreases account's UnreservedConcurrentExecution below"
3331
                f" its minimum value of [{config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY}]."
3332
            )
3333

3334
        provisioned_config = ProvisionedConcurrencyConfiguration(
1✔
3335
            provisioned_concurrent_executions, api_utils.generate_lambda_date()
3336
        )
3337
        fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3338

3339
        if api_utils.qualifier_is_alias(qualifier):
1✔
3340
            alias = fn.aliases.get(qualifier)
1✔
3341
            resolved_version = fn.versions.get(alias.function_version)
1✔
3342

3343
            if (
1✔
3344
                resolved_version
3345
                and fn.provisioned_concurrency_configs.get(alias.function_version) is not None
3346
            ):
3347
                raise ResourceConflictException(
1✔
3348
                    "Alias can't be used for Provisioned Concurrency configuration on an already Provisioned version",
3349
                    Type="User",
3350
                )
3351
            fn_arn = resolved_version.id.qualified_arn()
1✔
3352
        elif api_utils.qualifier_is_version(qualifier):
1✔
3353
            fn_version = fn.versions.get(qualifier)
1✔
3354

3355
            # TODO: might be useful other places, utilize
3356
            pointing_aliases = []
1✔
3357
            for alias in fn.aliases.values():
1✔
3358
                if (
1✔
3359
                    alias.function_version == qualifier
3360
                    and fn.provisioned_concurrency_configs.get(alias.name) is not None
3361
                ):
3362
                    pointing_aliases.append(alias.name)
1✔
3363
            if pointing_aliases:
1✔
3364
                raise ResourceConflictException(
1✔
3365
                    "Version is pointed by a Provisioned Concurrency alias", Type="User"
3366
                )
3367

3368
            fn_arn = fn_version.id.qualified_arn()
1✔
3369

3370
        manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3371

3372
        fn.provisioned_concurrency_configs[qualifier] = provisioned_config
1✔
3373

3374
        manager.update_provisioned_concurrency_config(
1✔
3375
            provisioned_config.provisioned_concurrent_executions
3376
        )
3377

3378
        return PutProvisionedConcurrencyConfigResponse(
1✔
3379
            RequestedProvisionedConcurrentExecutions=provisioned_config.provisioned_concurrent_executions,
3380
            AvailableProvisionedConcurrentExecutions=0,
3381
            AllocatedProvisionedConcurrentExecutions=0,
3382
            Status=ProvisionedConcurrencyStatusEnum.IN_PROGRESS,
3383
            # StatusReason=manager.provisioned_state.status_reason,
3384
            LastModified=provisioned_config.last_modified,  # TODO: does change with configuration or also with state changes?
3385
        )
3386

3387
    def get_provisioned_concurrency_config(
1✔
3388
        self, context: RequestContext, function_name: FunctionName, qualifier: Qualifier, **kwargs
3389
    ) -> GetProvisionedConcurrencyConfigResponse:
3390
        if qualifier == "$LATEST":
1✔
3391
            raise InvalidParameterValueException(
1✔
3392
                "The function resource provided must be an alias or a published version.",
3393
                Type="User",
3394
            )
3395
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3396
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3397
            function_name, qualifier, context
3398
        )
3399

3400
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3401
        if not provisioned_config:
1✔
3402
            raise ProvisionedConcurrencyConfigNotFoundException(
1✔
3403
                "No Provisioned Concurrency Config found for this function", Type="User"
3404
            )
3405

3406
        # TODO: make this compatible with alias pointer migration on update
3407
        if api_utils.qualifier_is_alias(qualifier):
1✔
3408
            state = lambda_stores[account_id][region]
1✔
3409
            fn = state.functions.get(function_name)
1✔
3410
            alias = fn.aliases.get(qualifier)
1✔
3411
            fn_arn = api_utils.qualified_lambda_arn(
1✔
3412
                function_name, alias.function_version, account_id, region
3413
            )
3414
        else:
3415
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3416

3417
        ver_manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3418

3419
        return GetProvisionedConcurrencyConfigResponse(
1✔
3420
            RequestedProvisionedConcurrentExecutions=provisioned_config.provisioned_concurrent_executions,
3421
            LastModified=provisioned_config.last_modified,
3422
            AvailableProvisionedConcurrentExecutions=ver_manager.provisioned_state.available,
3423
            AllocatedProvisionedConcurrentExecutions=ver_manager.provisioned_state.allocated,
3424
            Status=ver_manager.provisioned_state.status,
3425
            StatusReason=ver_manager.provisioned_state.status_reason,
3426
        )
3427

3428
    def list_provisioned_concurrency_configs(
1✔
3429
        self,
3430
        context: RequestContext,
3431
        function_name: FunctionName,
3432
        marker: String = None,
3433
        max_items: MaxProvisionedConcurrencyConfigListItems = None,
3434
        **kwargs,
3435
    ) -> ListProvisionedConcurrencyConfigsResponse:
3436
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3437
        state = lambda_stores[account_id][region]
1✔
3438

3439
        function_name = api_utils.get_function_name(function_name, context)
1✔
3440
        fn = state.functions.get(function_name)
1✔
3441
        if fn is None:
1✔
3442
            raise ResourceNotFoundException(
1✔
3443
                f"Function not found: {api_utils.unqualified_lambda_arn(function_name, account_id, region)}",
3444
                Type="User",
3445
            )
3446

3447
        configs = []
1✔
3448
        for qualifier, pc_config in fn.provisioned_concurrency_configs.items():
1✔
UNCOV
3449
            if api_utils.qualifier_is_alias(qualifier):
×
UNCOV
3450
                alias = fn.aliases.get(qualifier)
×
UNCOV
3451
                fn_arn = api_utils.qualified_lambda_arn(
×
3452
                    function_name, alias.function_version, account_id, region
3453
                )
3454
            else:
UNCOV
3455
                fn_arn = api_utils.qualified_lambda_arn(
×
3456
                    function_name, qualifier, account_id, region
3457
                )
3458

UNCOV
3459
            manager = self.lambda_service.get_lambda_version_manager(fn_arn)
×
3460

UNCOV
3461
            configs.append(
×
3462
                ProvisionedConcurrencyConfigListItem(
3463
                    FunctionArn=api_utils.qualified_lambda_arn(
3464
                        function_name, qualifier, account_id, region
3465
                    ),
3466
                    RequestedProvisionedConcurrentExecutions=pc_config.provisioned_concurrent_executions,
3467
                    AvailableProvisionedConcurrentExecutions=manager.provisioned_state.available,
3468
                    AllocatedProvisionedConcurrentExecutions=manager.provisioned_state.allocated,
3469
                    Status=manager.provisioned_state.status,
3470
                    StatusReason=manager.provisioned_state.status_reason,
3471
                    LastModified=pc_config.last_modified,
3472
                )
3473
            )
3474

3475
        provisioned_concurrency_configs = configs
1✔
3476
        provisioned_concurrency_configs = PaginatedList(provisioned_concurrency_configs)
1✔
3477
        page, token = provisioned_concurrency_configs.get_page(
1✔
3478
            lambda x: x,
3479
            marker,
3480
            max_items,
3481
        )
3482
        return ListProvisionedConcurrencyConfigsResponse(
1✔
3483
            ProvisionedConcurrencyConfigs=page, NextMarker=token
3484
        )
3485

3486
    def delete_provisioned_concurrency_config(
1✔
3487
        self, context: RequestContext, function_name: FunctionName, qualifier: Qualifier, **kwargs
3488
    ) -> None:
3489
        if qualifier == "$LATEST":
1✔
3490
            raise InvalidParameterValueException(
1✔
3491
                "The function resource provided must be an alias or a published version.",
3492
                Type="User",
3493
            )
3494
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3495
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3496
            function_name, qualifier, context
3497
        )
3498
        state = lambda_stores[account_id][region]
1✔
3499
        fn = state.functions.get(function_name)
1✔
3500

3501
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3502
        # delete is idempotent and doesn't actually care about the provisioned concurrency config not existing
3503
        if provisioned_config:
1✔
3504
            fn.provisioned_concurrency_configs.pop(qualifier)
1✔
3505
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3506
            manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3507
            manager.update_provisioned_concurrency_config(0)
1✔
3508

3509
    # =======================================
3510
    # =======  Event Invoke Config   ========
3511
    # =======================================
3512

3513
    # "1 validation error detected: Value 'arn:aws:_-/!lambda:<region>:111111111111:function:<function-name:1>' at 'destinationConfig.onFailure.destination' failed to satisfy constraint: Member must satisfy regular expression pattern: ^$|arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\\-])+:([a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\\d{1})?:(\\d{12})?:(.*)"
3514
    # "1 validation error detected: Value 'arn:aws:_-/!lambda:<region>:111111111111:function:<function-name:1>' at 'destinationConfig.onFailure.destination' failed to satisfy constraint: Member must satisfy regular expression pattern: ^$|arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\\-])+:([a-z]2((-gov)|(-iso(b?)))?-[a-z]+-\\d1)?:(\\d12)?:(.*)" ... (expected → actual)
3515

3516
    def _validate_destination_config(
1✔
3517
        self, store: LambdaStore, function_name: str, destination_config: DestinationConfig
3518
    ):
3519
        def _validate_destination_arn(destination_arn) -> bool:
1✔
3520
            if not api_utils.DESTINATION_ARN_PATTERN.match(destination_arn):
1✔
3521
                # technically we shouldn't handle this in the provider
3522
                raise ValidationException(
1✔
3523
                    "1 validation error detected: Value '"
3524
                    + destination_arn
3525
                    + "' at 'destinationConfig.onFailure.destination' failed to satisfy constraint: Member must satisfy regular expression pattern: "
3526
                    + "$|kafka://([^.]([a-zA-Z0-9\\-_.]{0,248}))|arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\\-])+:((eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1})?:(\\d{12})?:(.*)"
3527
                )
3528

3529
            match destination_arn.split(":")[2]:
1✔
3530
                case "lambda":
1✔
3531
                    fn_parts = api_utils.FULL_FN_ARN_PATTERN.search(destination_arn).groupdict()
1✔
3532
                    if fn_parts:
1✔
3533
                        # check if it exists
3534
                        fn = store.functions.get(fn_parts["function_name"])
1✔
3535
                        if not fn:
1✔
3536
                            raise InvalidParameterValueException(
1✔
3537
                                f"The destination ARN {destination_arn} is invalid.", Type="User"
3538
                            )
3539
                        if fn_parts["function_name"] == function_name:
1✔
3540
                            raise InvalidParameterValueException(
1✔
3541
                                "You can't specify the function as a destination for itself.",
3542
                                Type="User",
3543
                            )
3544
                case "sns" | "sqs" | "events":
1✔
3545
                    pass
1✔
3546
                case _:
1✔
3547
                    return False
1✔
3548
            return True
1✔
3549

3550
        validation_err = False
1✔
3551

3552
        failure_destination = destination_config.get("OnFailure", {}).get("Destination")
1✔
3553
        if failure_destination:
1✔
3554
            validation_err = validation_err or not _validate_destination_arn(failure_destination)
1✔
3555

3556
        success_destination = destination_config.get("OnSuccess", {}).get("Destination")
1✔
3557
        if success_destination:
1✔
3558
            validation_err = validation_err or not _validate_destination_arn(success_destination)
1✔
3559

3560
        if validation_err:
1✔
3561
            on_success_part = (
1✔
3562
                f"OnSuccess(destination={success_destination})" if success_destination else "null"
3563
            )
3564
            on_failure_part = (
1✔
3565
                f"OnFailure(destination={failure_destination})" if failure_destination else "null"
3566
            )
3567
            raise InvalidParameterValueException(
1✔
3568
                f"The provided destination config DestinationConfig(onSuccess={on_success_part}, onFailure={on_failure_part}) is invalid.",
3569
                Type="User",
3570
            )
3571

3572
    def put_function_event_invoke_config(
1✔
3573
        self,
3574
        context: RequestContext,
3575
        function_name: FunctionName,
3576
        qualifier: NumericLatestPublishedOrAliasQualifier = None,
3577
        maximum_retry_attempts: MaximumRetryAttempts = None,
3578
        maximum_event_age_in_seconds: MaximumEventAgeInSeconds = None,
3579
        destination_config: DestinationConfig = None,
3580
        **kwargs,
3581
    ) -> FunctionEventInvokeConfig:
3582
        """
3583
        Destination ARNs can be:
3584
        * SQS arn
3585
        * SNS arn
3586
        * Lambda arn
3587
        * EventBridge arn
3588

3589
        Differences between put_ and update_:
3590
            * put overwrites any existing config
3591
            * update allows changes only single values while keeping the rest of existing ones
3592
            * update fails on non-existing configs
3593

3594
        Differences between destination and DLQ
3595
            * "However, a dead-letter queue is part of a function's version-specific configuration, so it is locked in when you publish a version."
3596
            *  "On-failure destinations also support additional targets and include details about the function's response in the invocation record."
3597

3598
        """
3599
        if (
1✔
3600
            maximum_event_age_in_seconds is None
3601
            and maximum_retry_attempts is None
3602
            and destination_config is None
3603
        ):
3604
            raise InvalidParameterValueException(
1✔
3605
                "You must specify at least one of error handling or destination setting.",
3606
                Type="User",
3607
            )
3608
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3609
        state = lambda_stores[account_id][region]
1✔
3610
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3611
            function_name, qualifier, context
3612
        )
3613
        fn = state.functions.get(function_name)
1✔
3614
        if not fn or (qualifier and not (qualifier in fn.aliases or qualifier in fn.versions)):
1✔
3615
            raise ResourceNotFoundException("The function doesn't exist.", Type="User")
1✔
3616

3617
        qualifier = qualifier or "$LATEST"
1✔
3618

3619
        # validate and normalize destination config
3620
        if destination_config:
1✔
3621
            self._validate_destination_config(state, function_name, destination_config)
1✔
3622

3623
        destination_config = DestinationConfig(
1✔
3624
            OnSuccess=OnSuccess(
3625
                Destination=(destination_config or {}).get("OnSuccess", {}).get("Destination")
3626
            ),
3627
            OnFailure=OnFailure(
3628
                Destination=(destination_config or {}).get("OnFailure", {}).get("Destination")
3629
            ),
3630
        )
3631

3632
        config = EventInvokeConfig(
1✔
3633
            function_name=function_name,
3634
            qualifier=qualifier,
3635
            maximum_event_age_in_seconds=maximum_event_age_in_seconds,
3636
            maximum_retry_attempts=maximum_retry_attempts,
3637
            last_modified=api_utils.generate_lambda_date(),
3638
            destination_config=destination_config,
3639
        )
3640
        fn.event_invoke_configs[qualifier] = config
1✔
3641

3642
        return FunctionEventInvokeConfig(
1✔
3643
            LastModified=datetime.datetime.strptime(
3644
                config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3645
            ),
3646
            FunctionArn=api_utils.qualified_lambda_arn(
3647
                function_name, qualifier or "$LATEST", account_id, region
3648
            ),
3649
            DestinationConfig=destination_config,
3650
            MaximumEventAgeInSeconds=maximum_event_age_in_seconds,
3651
            MaximumRetryAttempts=maximum_retry_attempts,
3652
        )
3653

3654
    def get_function_event_invoke_config(
1✔
3655
        self,
3656
        context: RequestContext,
3657
        function_name: FunctionName,
3658
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
3659
        **kwargs,
3660
    ) -> FunctionEventInvokeConfig:
3661
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3662
        state = lambda_stores[account_id][region]
1✔
3663
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3664
            function_name, qualifier, context
3665
        )
3666

3667
        qualifier = qualifier or "$LATEST"
1✔
3668
        fn = state.functions.get(function_name)
1✔
3669
        if not fn:
1✔
3670
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3671
            raise ResourceNotFoundException(
1✔
3672
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3673
            )
3674

3675
        config = fn.event_invoke_configs.get(qualifier)
1✔
3676
        if not config:
1✔
3677
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3678
            raise ResourceNotFoundException(
1✔
3679
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3680
            )
3681

3682
        return FunctionEventInvokeConfig(
1✔
3683
            LastModified=datetime.datetime.strptime(
3684
                config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3685
            ),
3686
            FunctionArn=api_utils.qualified_lambda_arn(
3687
                function_name, qualifier, account_id, region
3688
            ),
3689
            DestinationConfig=config.destination_config,
3690
            MaximumEventAgeInSeconds=config.maximum_event_age_in_seconds,
3691
            MaximumRetryAttempts=config.maximum_retry_attempts,
3692
        )
3693

3694
    def list_function_event_invoke_configs(
1✔
3695
        self,
3696
        context: RequestContext,
3697
        function_name: FunctionName,
3698
        marker: String = None,
3699
        max_items: MaxFunctionEventInvokeConfigListItems = None,
3700
        **kwargs,
3701
    ) -> ListFunctionEventInvokeConfigsResponse:
3702
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3703
        state = lambda_stores[account_id][region]
1✔
3704
        fn = state.functions.get(function_name)
1✔
3705
        if not fn:
1✔
3706
            raise ResourceNotFoundException("The function doesn't exist.", Type="User")
1✔
3707

3708
        event_invoke_configs = [
1✔
3709
            FunctionEventInvokeConfig(
3710
                LastModified=c.last_modified,
3711
                FunctionArn=api_utils.qualified_lambda_arn(
3712
                    function_name, c.qualifier, account_id, region
3713
                ),
3714
                MaximumEventAgeInSeconds=c.maximum_event_age_in_seconds,
3715
                MaximumRetryAttempts=c.maximum_retry_attempts,
3716
                DestinationConfig=c.destination_config,
3717
            )
3718
            for c in fn.event_invoke_configs.values()
3719
        ]
3720

3721
        event_invoke_configs = PaginatedList(event_invoke_configs)
1✔
3722
        page, token = event_invoke_configs.get_page(
1✔
3723
            lambda x: x["FunctionArn"],
3724
            marker,
3725
            max_items,
3726
        )
3727
        return ListFunctionEventInvokeConfigsResponse(
1✔
3728
            FunctionEventInvokeConfigs=page, NextMarker=token
3729
        )
3730

3731
    def delete_function_event_invoke_config(
1✔
3732
        self,
3733
        context: RequestContext,
3734
        function_name: FunctionName,
3735
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
3736
        **kwargs,
3737
    ) -> None:
3738
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3739
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3740
            function_name, qualifier, context
3741
        )
3742
        state = lambda_stores[account_id][region]
1✔
3743
        fn = state.functions.get(function_name)
1✔
3744
        resolved_qualifier = qualifier or "$LATEST"
1✔
3745
        fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3746
        if not fn:
1✔
3747
            raise ResourceNotFoundException(
1✔
3748
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3749
            )
3750

3751
        config = fn.event_invoke_configs.get(resolved_qualifier)
1✔
3752
        if not config:
1✔
3753
            raise ResourceNotFoundException(
1✔
3754
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3755
            )
3756

3757
        del fn.event_invoke_configs[resolved_qualifier]
1✔
3758

3759
    def update_function_event_invoke_config(
1✔
3760
        self,
3761
        context: RequestContext,
3762
        function_name: FunctionName,
3763
        qualifier: NumericLatestPublishedOrAliasQualifier = None,
3764
        maximum_retry_attempts: MaximumRetryAttempts = None,
3765
        maximum_event_age_in_seconds: MaximumEventAgeInSeconds = None,
3766
        destination_config: DestinationConfig = None,
3767
        **kwargs,
3768
    ) -> FunctionEventInvokeConfig:
3769
        # like put but only update single fields via replace
3770
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3771
        state = lambda_stores[account_id][region]
1✔
3772
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3773
            function_name, qualifier, context
3774
        )
3775

3776
        if (
1✔
3777
            maximum_event_age_in_seconds is None
3778
            and maximum_retry_attempts is None
3779
            and destination_config is None
3780
        ):
UNCOV
3781
            raise InvalidParameterValueException(
×
3782
                "You must specify at least one of error handling or destination setting.",
3783
                Type="User",
3784
            )
3785

3786
        fn = state.functions.get(function_name)
1✔
3787
        if not fn or (qualifier and not (qualifier in fn.aliases or qualifier in fn.versions)):
1✔
3788
            raise ResourceNotFoundException("The function doesn't exist.", Type="User")
1✔
3789

3790
        qualifier = qualifier or "$LATEST"
1✔
3791

3792
        config = fn.event_invoke_configs.get(qualifier)
1✔
3793
        if not config:
1✔
3794
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3795
            raise ResourceNotFoundException(
1✔
3796
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3797
            )
3798

3799
        if destination_config:
1✔
UNCOV
3800
            self._validate_destination_config(state, function_name, destination_config)
×
3801

3802
        optional_kwargs = {
1✔
3803
            k: v
3804
            for k, v in {
3805
                "destination_config": destination_config,
3806
                "maximum_retry_attempts": maximum_retry_attempts,
3807
                "maximum_event_age_in_seconds": maximum_event_age_in_seconds,
3808
            }.items()
3809
            if v is not None
3810
        }
3811

3812
        new_config = dataclasses.replace(
1✔
3813
            config, last_modified=api_utils.generate_lambda_date(), **optional_kwargs
3814
        )
3815
        fn.event_invoke_configs[qualifier] = new_config
1✔
3816

3817
        return FunctionEventInvokeConfig(
1✔
3818
            LastModified=datetime.datetime.strptime(
3819
                new_config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3820
            ),
3821
            FunctionArn=api_utils.qualified_lambda_arn(
3822
                function_name, qualifier or "$LATEST", account_id, region
3823
            ),
3824
            DestinationConfig=new_config.destination_config,
3825
            MaximumEventAgeInSeconds=new_config.maximum_event_age_in_seconds,
3826
            MaximumRetryAttempts=new_config.maximum_retry_attempts,
3827
        )
3828

3829
    # =======================================
3830
    # ======  Layer & Layer Versions  =======
3831
    # =======================================
3832

3833
    @staticmethod
1✔
3834
    def _resolve_layer(
1✔
3835
        layer_name_or_arn: str, context: RequestContext
3836
    ) -> tuple[str, str, str, str | None]:
3837
        """
3838
        Return locator attributes for a given Lambda layer.
3839

3840
        :param layer_name_or_arn: Layer name or ARN
3841
        :param context: Request context
3842
        :return: Tuple of region, account ID, layer name, layer version
3843
        """
3844
        if api_utils.is_layer_arn(layer_name_or_arn):
1✔
3845
            return api_utils.parse_layer_arn(layer_name_or_arn)
1✔
3846

3847
        return context.region, context.account_id, layer_name_or_arn, None
1✔
3848

3849
    def publish_layer_version(
1✔
3850
        self,
3851
        context: RequestContext,
3852
        layer_name: LayerName,
3853
        content: LayerVersionContentInput,
3854
        description: Description | None = None,
3855
        compatible_runtimes: CompatibleRuntimes | None = None,
3856
        license_info: LicenseInfo | None = None,
3857
        compatible_architectures: CompatibleArchitectures | None = None,
3858
        **kwargs,
3859
    ) -> PublishLayerVersionResponse:
3860
        """
3861
        On first use of a LayerName a new layer is created and for each subsequent call with the same LayerName a new version is created.
3862
        Note that there are no $LATEST versions with layers!
3863

3864
        """
3865
        account = context.account_id
1✔
3866
        region = context.region
1✔
3867

3868
        validation_errors = api_utils.validate_layer_runtimes_and_architectures(
1✔
3869
            compatible_runtimes, compatible_architectures
3870
        )
3871
        if validation_errors:
1✔
3872
            raise ValidationException(
1✔
3873
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {'; '.join(validation_errors)}"
3874
            )
3875

3876
        state = lambda_stores[account][region]
1✔
3877
        with self.create_layer_lock:
1✔
3878
            if layer_name not in state.layers:
1✔
3879
                # we don't have a version so create new layer object
3880
                # lock is required to avoid creating two v1 objects for the same name
3881
                layer = Layer(
1✔
3882
                    arn=api_utils.layer_arn(layer_name=layer_name, account=account, region=region)
3883
                )
3884
                state.layers[layer_name] = layer
1✔
3885

3886
        layer = state.layers[layer_name]
1✔
3887
        with layer.next_version_lock:
1✔
3888
            next_version = LambdaLayerVersionIdentifier(
1✔
3889
                account_id=account, region=region, layer_name=layer_name
3890
            ).generate(next_version=layer.next_version)
3891
            # When creating a layer with user defined layer version, it is possible that we
3892
            # create layer versions out of order.
3893
            # ie. a user could replicate layer v2 then layer v1. It is important to always keep the maximum possible
3894
            # value for next layer to avoid overwriting existing versions
3895
            if layer.next_version <= next_version:
1✔
3896
                # We don't need to update layer.next_version if the created version is lower than the "next in line"
3897
                layer.next_version = max(next_version, layer.next_version) + 1
1✔
3898

3899
        # creating a new layer
3900
        if content.get("ZipFile"):
1✔
3901
            code = store_lambda_archive(
1✔
3902
                archive_file=content["ZipFile"],
3903
                function_name=layer_name,
3904
                region_name=region,
3905
                account_id=account,
3906
            )
3907
        else:
3908
            code = store_s3_bucket_archive(
1✔
3909
                archive_bucket=content["S3Bucket"],
3910
                archive_key=content["S3Key"],
3911
                archive_version=content.get("S3ObjectVersion"),
3912
                function_name=layer_name,
3913
                region_name=region,
3914
                account_id=account,
3915
            )
3916

3917
        new_layer_version = LayerVersion(
1✔
3918
            layer_version_arn=api_utils.layer_version_arn(
3919
                layer_name=layer_name,
3920
                account=account,
3921
                region=region,
3922
                version=str(next_version),
3923
            ),
3924
            layer_arn=layer.arn,
3925
            version=next_version,
3926
            description=description or "",
3927
            license_info=license_info,
3928
            compatible_runtimes=compatible_runtimes,
3929
            compatible_architectures=compatible_architectures,
3930
            created=api_utils.generate_lambda_date(),
3931
            code=code,
3932
        )
3933

3934
        layer.layer_versions[str(next_version)] = new_layer_version
1✔
3935

3936
        return api_utils.map_layer_out(new_layer_version)
1✔
3937

3938
    def get_layer_version(
1✔
3939
        self,
3940
        context: RequestContext,
3941
        layer_name: LayerName,
3942
        version_number: LayerVersionNumber,
3943
        **kwargs,
3944
    ) -> GetLayerVersionResponse:
3945
        # TODO: handle layer_name as an ARN
3946

3947
        region_name, account_id, layer_name, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
3948
        state = lambda_stores[account_id][region_name]
1✔
3949

3950
        layer = state.layers.get(layer_name)
1✔
3951
        if version_number < 1:
1✔
3952
            raise InvalidParameterValueException("Layer Version Cannot be less than 1", Type="User")
1✔
3953
        if layer is None:
1✔
3954
            raise ResourceNotFoundException(
1✔
3955
                "The resource you requested does not exist.", Type="User"
3956
            )
3957
        layer_version = layer.layer_versions.get(str(version_number))
1✔
3958
        if layer_version is None:
1✔
3959
            raise ResourceNotFoundException(
1✔
3960
                "The resource you requested does not exist.", Type="User"
3961
            )
3962
        return api_utils.map_layer_out(layer_version)
1✔
3963

3964
    def get_layer_version_by_arn(
1✔
3965
        self, context: RequestContext, arn: LayerVersionArn, **kwargs
3966
    ) -> GetLayerVersionResponse:
3967
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
3968
            arn, context
3969
        )
3970

3971
        if not layer_version:
1✔
3972
            raise ValidationException(
1✔
3973
                f"1 validation error detected: Value '{arn}' at 'arn' failed to satisfy constraint: Member must satisfy regular expression pattern: "
3974
                + "(arn:(aws[a-zA-Z-]*)?:lambda:(eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:\\d{12}:layer:[a-zA-Z0-9-_]+:[0-9]+)|(arn:[a-zA-Z0-9-]+:lambda:::awslayer:[a-zA-Z0-9-_]+)"
3975
            )
3976

3977
        store = lambda_stores[account_id][region_name]
1✔
3978
        if not (layers := store.layers.get(layer_name)):
1✔
UNCOV
3979
            raise ResourceNotFoundException(
×
3980
                "The resource you requested does not exist.", Type="User"
3981
            )
3982

3983
        layer_version = layers.layer_versions.get(layer_version)
1✔
3984

3985
        if not layer_version:
1✔
3986
            raise ResourceNotFoundException(
1✔
3987
                "The resource you requested does not exist.", Type="User"
3988
            )
3989

3990
        return api_utils.map_layer_out(layer_version)
1✔
3991

3992
    def list_layers(
1✔
3993
        self,
3994
        context: RequestContext,
3995
        compatible_runtime: Runtime | None = None,
3996
        marker: String | None = None,
3997
        max_items: MaxLayerListItems | None = None,
3998
        compatible_architecture: Architecture | None = None,
3999
        **kwargs,
4000
    ) -> ListLayersResponse:
4001
        validation_errors = []
1✔
4002

4003
        validation_error_arch = api_utils.validate_layer_architecture(compatible_architecture)
1✔
4004
        if validation_error_arch:
1✔
4005
            validation_errors.append(validation_error_arch)
1✔
4006

4007
        validation_error_runtime = api_utils.validate_layer_runtime(compatible_runtime)
1✔
4008
        if validation_error_runtime:
1✔
4009
            validation_errors.append(validation_error_runtime)
1✔
4010

4011
        if validation_errors:
1✔
4012
            raise ValidationException(
1✔
4013
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {';'.join(validation_errors)}"
4014
            )
4015
        # TODO: handle filter: compatible_runtime
4016
        # TODO: handle filter: compatible_architecture
4017

UNCOV
4018
        state = lambda_stores[context.account_id][context.region]
×
4019
        layers = state.layers
×
4020

4021
        # TODO: test how filters behave together with only returning layers here? Does it return the latest "matching" layer, i.e. does it ignore later layer versions that don't match?
4022

UNCOV
4023
        responses: list[LayersListItem] = []
×
UNCOV
4024
        for layer_name, layer in layers.items():
×
4025
            # fetch latest version
UNCOV
4026
            layer_versions = list(layer.layer_versions.values())
×
UNCOV
4027
            sorted(layer_versions, key=lambda x: x.version)
×
UNCOV
4028
            latest_layer_version = layer_versions[-1]
×
UNCOV
4029
            responses.append(
×
4030
                LayersListItem(
4031
                    LayerName=layer_name,
4032
                    LayerArn=layer.arn,
4033
                    LatestMatchingVersion=api_utils.map_layer_out(latest_layer_version),
4034
                )
4035
            )
4036

4037
        responses = PaginatedList(responses)
×
UNCOV
4038
        page, token = responses.get_page(
×
4039
            lambda version: version,
4040
            marker,
4041
            max_items,
4042
        )
4043

UNCOV
4044
        return ListLayersResponse(NextMarker=token, Layers=page)
×
4045

4046
    def list_layer_versions(
1✔
4047
        self,
4048
        context: RequestContext,
4049
        layer_name: LayerName,
4050
        compatible_runtime: Runtime | None = None,
4051
        marker: String | None = None,
4052
        max_items: MaxLayerListItems | None = None,
4053
        compatible_architecture: Architecture | None = None,
4054
        **kwargs,
4055
    ) -> ListLayerVersionsResponse:
4056
        validation_errors = api_utils.validate_layer_runtimes_and_architectures(
1✔
4057
            [compatible_runtime] if compatible_runtime else [],
4058
            [compatible_architecture] if compatible_architecture else [],
4059
        )
4060
        if validation_errors:
1✔
UNCOV
4061
            raise ValidationException(
×
4062
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {';'.join(validation_errors)}"
4063
            )
4064

4065
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
4066
            layer_name, context
4067
        )
4068
        state = lambda_stores[account_id][region_name]
1✔
4069

4070
        # TODO: Test & handle filter: compatible_runtime
4071
        # TODO: Test & handle filter: compatible_architecture
4072
        all_layer_versions = []
1✔
4073
        layer = state.layers.get(layer_name)
1✔
4074
        if layer is not None:
1✔
4075
            for layer_version in layer.layer_versions.values():
1✔
4076
                all_layer_versions.append(api_utils.map_layer_out(layer_version))
1✔
4077

4078
        all_layer_versions.sort(key=lambda x: x["Version"], reverse=True)
1✔
4079
        all_layer_versions = PaginatedList(all_layer_versions)
1✔
4080
        page, token = all_layer_versions.get_page(
1✔
4081
            lambda version: version["LayerVersionArn"],
4082
            marker,
4083
            max_items,
4084
        )
4085
        return ListLayerVersionsResponse(NextMarker=token, LayerVersions=page)
1✔
4086

4087
    def delete_layer_version(
1✔
4088
        self,
4089
        context: RequestContext,
4090
        layer_name: LayerName,
4091
        version_number: LayerVersionNumber,
4092
        **kwargs,
4093
    ) -> None:
4094
        if version_number < 1:
1✔
4095
            raise InvalidParameterValueException("Layer Version Cannot be less than 1", Type="User")
1✔
4096

4097
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
4098
            layer_name, context
4099
        )
4100

4101
        store = lambda_stores[account_id][region_name]
1✔
4102
        layer = store.layers.get(layer_name, {})
1✔
4103
        if layer:
1✔
4104
            layer.layer_versions.pop(str(version_number), None)
1✔
4105

4106
    # =======================================
4107
    # =====  Layer Version Permissions  =====
4108
    # =======================================
4109
    # TODO: lock updates that change revision IDs
4110

4111
    def add_layer_version_permission(
1✔
4112
        self,
4113
        context: RequestContext,
4114
        layer_name: LayerName,
4115
        version_number: LayerVersionNumber,
4116
        statement_id: StatementId,
4117
        action: LayerPermissionAllowedAction,
4118
        principal: LayerPermissionAllowedPrincipal,
4119
        organization_id: OrganizationId = None,
4120
        revision_id: String = None,
4121
        **kwargs,
4122
    ) -> AddLayerVersionPermissionResponse:
4123
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
4124
        # `layer_n` contains the layer name.
4125
        region_name, account_id, layer_n, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
4126

4127
        if action != "lambda:GetLayerVersion":
1✔
4128
            raise ValidationException(
1✔
4129
                f"1 validation error detected: Value '{action}' at 'action' failed to satisfy constraint: Member must satisfy regular expression pattern: lambda:GetLayerVersion"
4130
            )
4131

4132
        store = lambda_stores[account_id][region_name]
1✔
4133
        layer = store.layers.get(layer_n)
1✔
4134

4135
        layer_version_arn = api_utils.layer_version_arn(
1✔
4136
            layer_name, account_id, region_name, str(version_number)
4137
        )
4138

4139
        if layer is None:
1✔
4140
            raise ResourceNotFoundException(
1✔
4141
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4142
            )
4143
        layer_version = layer.layer_versions.get(str(version_number))
1✔
4144
        if layer_version is None:
1✔
4145
            raise ResourceNotFoundException(
1✔
4146
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4147
            )
4148
        # do we have a policy? if not set one
4149
        if layer_version.policy is None:
1✔
4150
            layer_version.policy = LayerPolicy()
1✔
4151

4152
        if statement_id in layer_version.policy.statements:
1✔
4153
            raise ResourceConflictException(
1✔
4154
                f"The statement id ({statement_id}) provided already exists. Please provide a new statement id, or remove the existing statement.",
4155
                Type="User",
4156
            )
4157

4158
        if revision_id and layer_version.policy.revision_id != revision_id:
1✔
4159
            raise PreconditionFailedException(
1✔
4160
                "The Revision Id provided does not match the latest Revision Id. "
4161
                "Call the GetLayerPolicy API to retrieve the latest Revision Id",
4162
                Type="User",
4163
            )
4164

4165
        statement = LayerPolicyStatement(
1✔
4166
            sid=statement_id, action=action, principal=principal, organization_id=organization_id
4167
        )
4168

4169
        old_statements = layer_version.policy.statements
1✔
4170
        layer_version.policy = dataclasses.replace(
1✔
4171
            layer_version.policy, statements={**old_statements, statement_id: statement}
4172
        )
4173

4174
        return AddLayerVersionPermissionResponse(
1✔
4175
            Statement=json.dumps(
4176
                {
4177
                    "Sid": statement.sid,
4178
                    "Effect": "Allow",
4179
                    "Principal": statement.principal,
4180
                    "Action": statement.action,
4181
                    "Resource": layer_version.layer_version_arn,
4182
                }
4183
            ),
4184
            RevisionId=layer_version.policy.revision_id,
4185
        )
4186

4187
    def remove_layer_version_permission(
1✔
4188
        self,
4189
        context: RequestContext,
4190
        layer_name: LayerName,
4191
        version_number: LayerVersionNumber,
4192
        statement_id: StatementId,
4193
        revision_id: String = None,
4194
        **kwargs,
4195
    ) -> None:
4196
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
4197
        # `layer_n` contains the layer name.
4198
        region_name, account_id, layer_n, layer_version = LambdaProvider._resolve_layer(
1✔
4199
            layer_name, context
4200
        )
4201

4202
        layer_version_arn = api_utils.layer_version_arn(
1✔
4203
            layer_name, account_id, region_name, str(version_number)
4204
        )
4205

4206
        state = lambda_stores[account_id][region_name]
1✔
4207
        layer = state.layers.get(layer_n)
1✔
4208
        if layer is None:
1✔
4209
            raise ResourceNotFoundException(
1✔
4210
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4211
            )
4212
        layer_version = layer.layer_versions.get(str(version_number))
1✔
4213
        if layer_version is None:
1✔
4214
            raise ResourceNotFoundException(
1✔
4215
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4216
            )
4217

4218
        if revision_id and layer_version.policy.revision_id != revision_id:
1✔
4219
            raise PreconditionFailedException(
1✔
4220
                "The Revision Id provided does not match the latest Revision Id. "
4221
                "Call the GetLayerPolicy API to retrieve the latest Revision Id",
4222
                Type="User",
4223
            )
4224

4225
        if statement_id not in layer_version.policy.statements:
1✔
4226
            raise ResourceNotFoundException(
1✔
4227
                f"Statement {statement_id} is not found in resource policy.", Type="User"
4228
            )
4229

4230
        old_statements = layer_version.policy.statements
1✔
4231
        layer_version.policy = dataclasses.replace(
1✔
4232
            layer_version.policy,
4233
            statements={k: v for k, v in old_statements.items() if k != statement_id},
4234
        )
4235

4236
    def get_layer_version_policy(
1✔
4237
        self,
4238
        context: RequestContext,
4239
        layer_name: LayerName,
4240
        version_number: LayerVersionNumber,
4241
        **kwargs,
4242
    ) -> GetLayerVersionPolicyResponse:
4243
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
4244
        # `layer_n` contains the layer name.
4245
        region_name, account_id, layer_n, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
4246

4247
        layer_version_arn = api_utils.layer_version_arn(
1✔
4248
            layer_name, account_id, region_name, str(version_number)
4249
        )
4250

4251
        store = lambda_stores[account_id][region_name]
1✔
4252
        layer = store.layers.get(layer_n)
1✔
4253

4254
        if layer is None:
1✔
4255
            raise ResourceNotFoundException(
1✔
4256
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4257
            )
4258

4259
        layer_version = layer.layer_versions.get(str(version_number))
1✔
4260
        if layer_version is None:
1✔
4261
            raise ResourceNotFoundException(
1✔
4262
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4263
            )
4264

4265
        if layer_version.policy is None:
1✔
4266
            raise ResourceNotFoundException(
1✔
4267
                "No policy is associated with the given resource.", Type="User"
4268
            )
4269

4270
        return GetLayerVersionPolicyResponse(
1✔
4271
            Policy=json.dumps(
4272
                {
4273
                    "Version": layer_version.policy.version,
4274
                    "Id": layer_version.policy.id,
4275
                    "Statement": [
4276
                        {
4277
                            "Sid": ps.sid,
4278
                            "Effect": "Allow",
4279
                            "Principal": ps.principal,
4280
                            "Action": ps.action,
4281
                            "Resource": layer_version.layer_version_arn,
4282
                        }
4283
                        for ps in layer_version.policy.statements.values()
4284
                    ],
4285
                }
4286
            ),
4287
            RevisionId=layer_version.policy.revision_id,
4288
        )
4289

4290
    # =======================================
4291
    # =======  Function Concurrency  ========
4292
    # =======================================
4293
    # (Reserved) function concurrency is scoped to the whole function
4294

4295
    def get_function_concurrency(
1✔
4296
        self, context: RequestContext, function_name: FunctionName, **kwargs
4297
    ) -> GetFunctionConcurrencyResponse:
4298
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4299
        function_name = api_utils.get_function_name(function_name, context)
1✔
4300
        fn = self._get_function(function_name=function_name, region=region, account_id=account_id)
1✔
4301
        return GetFunctionConcurrencyResponse(
1✔
4302
            ReservedConcurrentExecutions=fn.reserved_concurrent_executions
4303
        )
4304

4305
    def put_function_concurrency(
1✔
4306
        self,
4307
        context: RequestContext,
4308
        function_name: FunctionName,
4309
        reserved_concurrent_executions: ReservedConcurrentExecutions,
4310
        **kwargs,
4311
    ) -> Concurrency:
4312
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4313

4314
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
4315
        if qualifier:
1✔
4316
            raise InvalidParameterValueException(
1✔
4317
                "This operation is permitted on Lambda functions only. Aliases and versions do not support this operation. Please specify either a function name or an unqualified function ARN.",
4318
                Type="User",
4319
            )
4320

4321
        store = lambda_stores[account_id][region]
1✔
4322
        fn = store.functions.get(function_name)
1✔
4323
        if not fn:
1✔
4324
            fn_arn = api_utils.qualified_lambda_arn(
1✔
4325
                function_name,
4326
                qualifier="$LATEST",
4327
                account=account_id,
4328
                region=region,
4329
            )
4330
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
4331

4332
        settings = self.get_account_settings(context)
1✔
4333
        unreserved_concurrent_executions = settings["AccountLimit"][
1✔
4334
            "UnreservedConcurrentExecutions"
4335
        ]
4336

4337
        # The existing reserved concurrent executions for the same function are already deduced in
4338
        # unreserved_concurrent_executions but must not count because the new one will replace the existing one.
4339
        # Joel tested this behavior manually against AWS (2023-11-28).
4340
        existing_reserved_concurrent_executions = (
1✔
4341
            fn.reserved_concurrent_executions if fn.reserved_concurrent_executions else 0
4342
        )
4343
        if (
1✔
4344
            unreserved_concurrent_executions
4345
            - reserved_concurrent_executions
4346
            + existing_reserved_concurrent_executions
4347
        ) < config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY:
4348
            raise InvalidParameterValueException(
1✔
4349
                f"Specified ReservedConcurrentExecutions for function decreases account's UnreservedConcurrentExecution below its minimum value of [{config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY}]."
4350
            )
4351

4352
        total_provisioned_concurrency = sum(
1✔
4353
            [
4354
                provisioned_configs.provisioned_concurrent_executions
4355
                for provisioned_configs in fn.provisioned_concurrency_configs.values()
4356
            ]
4357
        )
4358
        if total_provisioned_concurrency > reserved_concurrent_executions:
1✔
4359
            raise InvalidParameterValueException(
1✔
4360
                f" ReservedConcurrentExecutions  {reserved_concurrent_executions} should not be lower than function's total provisioned concurrency [{total_provisioned_concurrency}]."
4361
            )
4362

4363
        fn.reserved_concurrent_executions = reserved_concurrent_executions
1✔
4364

4365
        return Concurrency(ReservedConcurrentExecutions=fn.reserved_concurrent_executions)
1✔
4366

4367
    def delete_function_concurrency(
1✔
4368
        self, context: RequestContext, function_name: FunctionName, **kwargs
4369
    ) -> None:
4370
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4371
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
4372
        store = lambda_stores[account_id][region]
1✔
4373
        fn = store.functions.get(function_name)
1✔
4374
        fn.reserved_concurrent_executions = None
1✔
4375

4376
    # =======================================
4377
    # ===============  TAGS   ===============
4378
    # =======================================
4379
    # only Function, Event Source Mapping, and Code Signing Config (not currently supported by LocalStack) ARNs an are available for tagging in AWS
4380

4381
    def _get_tags(self, resource: TaggableResource) -> dict[str, str]:
1✔
4382
        state = self.fetch_lambda_store_for_tagging(resource)
1✔
4383
        lambda_adapted_tags = {
1✔
4384
            tag["Key"]: tag["Value"]
4385
            for tag in state.TAGS.list_tags_for_resource(resource).get("Tags")
4386
        }
4387
        return lambda_adapted_tags
1✔
4388

4389
    def _store_tags(self, resource: TaggableResource, tags: dict[str, str]):
1✔
4390
        state = self.fetch_lambda_store_for_tagging(resource)
1✔
4391
        if len(state.TAGS.tags.get(resource, {}) | tags) > LAMBDA_TAG_LIMIT_PER_RESOURCE:
1✔
4392
            raise InvalidParameterValueException(
1✔
4393
                "Number of tags exceeds resource tag limit.", Type="User"
4394
            )
4395

4396
        tag_svc_adapted_tags = [{"Key": key, "Value": value} for key, value in tags.items()]
1✔
4397
        state.TAGS.tag_resource(resource, tag_svc_adapted_tags)
1✔
4398

4399
    def fetch_lambda_store_for_tagging(self, resource: TaggableResource) -> LambdaStore:
1✔
4400
        """
4401
        Takes a resource ARN for a TaggableResource (Lambda Function, Event Source Mapping, Code Signing Config, or Capacity Provider) and returns a corresponding
4402
        LambdaStore for its region and account.
4403

4404
        In addition, this function validates that the ARN is a valid TaggableResource type, and that the TaggableResource exists.
4405

4406
        Raises:
4407
            ValidationException: If the resource ARN is not a full ARN for a TaggableResource.
4408
            ResourceNotFoundException: If the specified resource does not exist.
4409
            InvalidParameterValueException: If the resource ARN is a qualified Lambda Function.
4410
        """
4411

4412
        def _raise_validation_exception():
1✔
4413
            raise ValidationException(
1✔
4414
                f"1 validation error detected: Value '{resource}' at 'resource' failed to satisfy constraint: Member must satisfy regular expression pattern: {api_utils.TAGGABLE_RESOURCE_ARN_PATTERN}"
4415
            )
4416

4417
        # Check whether the ARN we have been passed is correctly formatted
4418
        parsed_resource_arn: ArnData = None
1✔
4419
        try:
1✔
4420
            parsed_resource_arn = parse_arn(resource)
1✔
4421
        except Exception:
1✔
4422
            _raise_validation_exception()
1✔
4423

4424
        # TODO: Should we be checking whether this is a full ARN?
4425
        region, account_id, resource_type = map(
1✔
4426
            parsed_resource_arn.get, ("region", "account", "resource")
4427
        )
4428

4429
        if not all((region, account_id, resource_type)):
1✔
UNCOV
4430
            _raise_validation_exception()
×
4431

4432
        if not (parts := resource_type.split(":")):
1✔
UNCOV
4433
            _raise_validation_exception()
×
4434

4435
        resource_type, resource_identifier, *qualifier = parts
1✔
4436

4437
        # Qualifier validation raises before checking for NotFound
4438
        if qualifier:
1✔
4439
            if resource_type == "function":
1✔
4440
                raise InvalidParameterValueException(
1✔
4441
                    "Tags on function aliases and versions are not supported. Please specify a function ARN.",
4442
                    Type="User",
4443
                )
4444
            _raise_validation_exception()
1✔
4445

4446
        if resource_type == "event-source-mapping":
1✔
4447
            self._get_esm(resource_identifier, account_id, region)
1✔
4448
        elif resource_type == "code-signing-config":
1✔
4449
            raise NotImplementedError("Resource tagging on CSC not yet implemented.")
4450
        elif resource_type == "function":
1✔
4451
            self._get_function(
1✔
4452
                function_name=resource_identifier, account_id=account_id, region=region
4453
            )
4454
        elif resource_type == "capacity-provider":
1✔
4455
            self._get_capacity_provider(resource_identifier, account_id, region)
1✔
4456
        else:
4457
            _raise_validation_exception()
1✔
4458

4459
        # If no exceptions are raised, assume ARN and referenced resource is valid for tag operations
4460
        return lambda_stores[account_id][region]
1✔
4461

4462
    def tag_resource(
1✔
4463
        self, context: RequestContext, resource: TaggableResource, tags: Tags, **kwargs
4464
    ) -> None:
4465
        if not tags:
1✔
4466
            raise InvalidParameterValueException(
1✔
4467
                "An error occurred and the request cannot be processed.", Type="User"
4468
            )
4469
        self._store_tags(resource, tags)
1✔
4470

4471
        if (resource_id := extract_resource_from_arn(resource)) and resource_id.startswith(
1✔
4472
            "function"
4473
        ):
4474
            name, _, account, region = function_locators_from_arn(resource)
1✔
4475
            function = self._get_function(name, account, region)
1✔
4476
            with function.lock:
1✔
4477
                # dirty hack for changed revision id, should reevaluate model to prevent this:
4478
                latest_version = function.versions["$LATEST"]
1✔
4479
                function.versions["$LATEST"] = dataclasses.replace(
1✔
4480
                    latest_version, config=dataclasses.replace(latest_version.config)
4481
                )
4482

4483
    def list_tags(
1✔
4484
        self, context: RequestContext, resource: TaggableResource, **kwargs
4485
    ) -> ListTagsResponse:
4486
        tags = self._get_tags(resource)
1✔
4487
        return ListTagsResponse(Tags=tags)
1✔
4488

4489
    def untag_resource(
1✔
4490
        self, context: RequestContext, resource: TaggableResource, tag_keys: TagKeyList, **kwargs
4491
    ) -> None:
4492
        if not tag_keys:
1✔
4493
            raise ValidationException(
1✔
4494
                "1 validation error detected: Value null at 'tagKeys' failed to satisfy constraint: Member must not be null"
4495
            )  # should probably be generalized a bit
4496

4497
        state = self.fetch_lambda_store_for_tagging(resource)
1✔
4498
        state.TAGS.untag_resource(resource, tag_keys)
1✔
4499

4500
        if (resource_id := extract_resource_from_arn(resource)) and resource_id.startswith(
1✔
4501
            "function"
4502
        ):
4503
            name, _, account, region = function_locators_from_arn(resource)
1✔
4504
            function = self._get_function(name, account, region)
1✔
4505
            # TODO: Potential race condition
4506
            with function.lock:
1✔
4507
                # dirty hack for changed revision id, should reevaluate model to prevent this:
4508
                latest_version = function.versions["$LATEST"]
1✔
4509
                function.versions["$LATEST"] = dataclasses.replace(
1✔
4510
                    latest_version, config=dataclasses.replace(latest_version.config)
4511
                )
4512

4513
    # =======================================
4514
    # =======  LEGACY / DEPRECATED   ========
4515
    # =======================================
4516

4517
    def invoke_async(
1✔
4518
        self,
4519
        context: RequestContext,
4520
        function_name: NamespacedFunctionName,
4521
        invoke_args: IO[BlobStream],
4522
        **kwargs,
4523
    ) -> InvokeAsyncResponse:
4524
        """LEGACY API endpoint. Even AWS heavily discourages its usage."""
4525
        raise NotImplementedError
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc