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

localstack / localstack / 17115498067

20 Aug 2025 09:05PM UTC coverage: 86.876% (-0.01%) from 86.889%
17115498067

push

github

simonrw
Handle parameter conversions for different transforms

13 of 13 new or added lines in 2 files covered. (100.0%)

42 existing lines in 7 files now uncovered.

67023 of 77148 relevant lines covered (86.88%)

0.87 hits per line

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

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

248
LOG = logging.getLogger(__name__)
1✔
249

250
LAMBDA_DEFAULT_TIMEOUT = 3
1✔
251
LAMBDA_DEFAULT_MEMORY_SIZE = 128
1✔
252

253
LAMBDA_TAG_LIMIT_PER_RESOURCE = 50
1✔
254
LAMBDA_LAYERS_LIMIT_PER_FUNCTION = 5
1✔
255

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

261

262
class LambdaProvider(LambdaApi, ServiceLifecycleHook):
1✔
263
    lambda_service: LambdaService
1✔
264
    create_fn_lock: threading.RLock
1✔
265
    create_layer_lock: threading.RLock
1✔
266
    router: FunctionUrlRouter
1✔
267
    esm_workers: dict[str, EsmWorker]
1✔
268
    layer_fetcher: LayerFetcher | None
1✔
269

270
    def __init__(self) -> None:
1✔
271
        self.lambda_service = LambdaService()
1✔
272
        self.create_fn_lock = threading.RLock()
1✔
273
        self.create_layer_lock = threading.RLock()
1✔
274
        self.router = FunctionUrlRouter(ROUTER, self.lambda_service)
1✔
275
        self.esm_workers = {}
1✔
276
        self.layer_fetcher = None
1✔
277
        lambda_hooks.inject_layer_fetcher.run(self)
1✔
278

279
    def accept_state_visitor(self, visitor: StateVisitor):
1✔
280
        visitor.visit(lambda_stores)
×
281

282
    def on_before_state_reset(self):
1✔
283
        self.lambda_service.stop()
×
284

285
    def on_after_state_reset(self):
1✔
286
        self.router.lambda_service = self.lambda_service = LambdaService()
×
287

288
    def on_before_state_load(self):
1✔
289
        self.lambda_service.stop()
×
290

291
    def on_after_state_load(self):
1✔
292
        self.lambda_service = LambdaService()
×
293
        self.router.lambda_service = self.lambda_service
×
294

295
        for account_id, account_bundle in lambda_stores.items():
×
296
            for region_name, state in account_bundle.items():
×
297
                for fn in state.functions.values():
×
298
                    for fn_version in fn.versions.values():
×
299
                        # restore the "Pending" state for every function version and start it
300
                        try:
×
301
                            new_state = VersionState(
×
302
                                state=State.Pending,
303
                                code=StateReasonCode.Creating,
304
                                reason="The function is being created.",
305
                            )
306
                            new_config = dataclasses.replace(fn_version.config, state=new_state)
×
307
                            new_version = dataclasses.replace(fn_version, config=new_config)
×
308
                            fn.versions[fn_version.id.qualifier] = new_version
×
309
                            self.lambda_service.create_function_version(fn_version).result(
×
310
                                timeout=5
311
                            )
312
                        except Exception:
×
313
                            LOG.warning(
×
314
                                "Failed to restore function version %s",
315
                                fn_version.id.qualified_arn(),
316
                                exc_info=LOG.isEnabledFor(logging.DEBUG),
317
                            )
318
                    # restore provisioned concurrency per function considering both versions and aliases
319
                    for (
×
320
                        provisioned_qualifier,
321
                        provisioned_config,
322
                    ) in fn.provisioned_concurrency_configs.items():
323
                        fn_arn = None
×
324
                        try:
×
325
                            if api_utils.qualifier_is_alias(provisioned_qualifier):
×
326
                                alias = fn.aliases.get(provisioned_qualifier)
×
327
                                resolved_version = fn.versions.get(alias.function_version)
×
328
                                fn_arn = resolved_version.id.qualified_arn()
×
329
                            elif api_utils.qualifier_is_version(provisioned_qualifier):
×
330
                                fn_version = fn.versions.get(provisioned_qualifier)
×
331
                                fn_arn = fn_version.id.qualified_arn()
×
332
                            else:
333
                                raise InvalidParameterValueException(
×
334
                                    "Invalid qualifier type:"
335
                                    " Qualifier can only be an alias or a version for provisioned concurrency."
336
                                )
337

338
                            manager = self.lambda_service.get_lambda_version_manager(fn_arn)
×
339
                            manager.update_provisioned_concurrency_config(
×
340
                                provisioned_config.provisioned_concurrent_executions
341
                            )
342
                        except Exception:
×
343
                            LOG.warning(
×
344
                                "Failed to restore provisioned concurrency %s for function %s",
345
                                provisioned_config,
346
                                fn_arn,
347
                                exc_info=LOG.isEnabledFor(logging.DEBUG),
348
                            )
349

350
                for esm in state.event_source_mappings.values():
×
351
                    # Restores event source workers
352
                    function_arn = esm.get("FunctionArn")
×
353

354
                    # TODO: How do we know the event source is up?
355
                    # A basic poll to see if the mapped Lambda function is active/failed
356
                    if not poll_condition(
×
357
                        lambda: get_function_version_from_arn(function_arn).config.state.state
358
                        in [State.Active, State.Failed],
359
                        timeout=10,
360
                    ):
361
                        LOG.warning(
×
362
                            "Creating ESM for Lambda that is not in running state: %s",
363
                            function_arn,
364
                        )
365

366
                    function_version = get_function_version_from_arn(function_arn)
×
367
                    function_role = function_version.config.role
×
368

369
                    is_esm_enabled = esm.get("State", EsmState.DISABLED) not in (
×
370
                        EsmState.DISABLED,
371
                        EsmState.DISABLING,
372
                    )
373
                    esm_worker = EsmWorkerFactory(
×
374
                        esm, function_role, is_esm_enabled
375
                    ).get_esm_worker()
376

377
                    # Note: a worker is created in the DISABLED state if not enabled
378
                    esm_worker.create()
×
379
                    # TODO: assigning the esm_worker to the dict only works after .create(). Could it cause a race
380
                    #  condition if we get a shutdown here and have a worker thread spawned but not accounted for?
381
                    self.esm_workers[esm_worker.uuid] = esm_worker
×
382

383
    def on_after_init(self):
1✔
384
        self.router.register_routes()
1✔
385
        get_runtime_executor().validate_environment()
1✔
386

387
    def on_before_stop(self) -> None:
1✔
388
        for esm_worker in self.esm_workers.values():
1✔
389
            esm_worker.stop_for_shutdown()
1✔
390

391
        # TODO: should probably unregister routes?
392
        self.lambda_service.stop()
1✔
393

394
    @staticmethod
1✔
395
    def _get_function(function_name: str, account_id: str, region: str) -> Function:
1✔
396
        state = lambda_stores[account_id][region]
1✔
397
        function = state.functions.get(function_name)
1✔
398
        if not function:
1✔
399
            arn = api_utils.unqualified_lambda_arn(
1✔
400
                function_name=function_name,
401
                account=account_id,
402
                region=region,
403
            )
404
            raise ResourceNotFoundException(
1✔
405
                f"Function not found: {arn}",
406
                Type="User",
407
            )
408
        return function
1✔
409

410
    @staticmethod
1✔
411
    def _get_esm(uuid: str, account_id: str, region: str) -> EventSourceMappingConfiguration:
1✔
412
        state = lambda_stores[account_id][region]
1✔
413
        esm = state.event_source_mappings.get(uuid)
1✔
414
        if not esm:
1✔
415
            arn = lambda_event_source_mapping_arn(uuid, account_id, region)
1✔
416
            raise ResourceNotFoundException(
1✔
417
                f"Event source mapping not found: {arn}",
418
                Type="User",
419
            )
420
        return esm
1✔
421

422
    @staticmethod
1✔
423
    def _validate_qualifier_expression(qualifier: str) -> None:
1✔
424
        if error_messages := api_utils.validate_qualifier(qualifier):
1✔
425
            raise ValidationException(
×
426
                message=api_utils.construct_validation_exception_message(error_messages)
427
            )
428

429
    @staticmethod
1✔
430
    def _resolve_fn_qualifier(resolved_fn: Function, qualifier: str | None) -> tuple[str, str]:
1✔
431
        """Attempts to resolve a given qualifier and returns a qualifier that exists or
432
        raises an appropriate ResourceNotFoundException.
433

434
        :param resolved_fn: The resolved lambda function
435
        :param qualifier: The qualifier to be resolved or None
436
        :return: Tuple of (resolved qualifier, function arn either qualified or unqualified)"""
437
        function_name = resolved_fn.function_name
1✔
438
        # assuming function versions need to live in the same account and region
439
        account_id = resolved_fn.latest().id.account
1✔
440
        region = resolved_fn.latest().id.region
1✔
441
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
442
        if qualifier is not None:
1✔
443
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
444
            if api_utils.qualifier_is_alias(qualifier):
1✔
445
                if qualifier not in resolved_fn.aliases:
1✔
446
                    raise ResourceNotFoundException(f"Cannot find alias arn: {fn_arn}", Type="User")
1✔
447
            elif api_utils.qualifier_is_version(qualifier) or qualifier == "$LATEST":
1✔
448
                if qualifier not in resolved_fn.versions:
1✔
449
                    raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
450
            else:
451
                # matches qualifier pattern but invalid alias or version
452
                raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
453
        resolved_qualifier = qualifier or "$LATEST"
1✔
454
        return resolved_qualifier, fn_arn
1✔
455

456
    @staticmethod
1✔
457
    def _function_revision_id(resolved_fn: Function, resolved_qualifier: str) -> str:
1✔
458
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
459
            return resolved_fn.aliases[resolved_qualifier].revision_id
1✔
460
        # Assumes that a non-alias is a version
461
        else:
462
            return resolved_fn.versions[resolved_qualifier].config.revision_id
1✔
463

464
    def _resolve_vpc_id(self, account_id: str, region_name: str, subnet_id: str) -> str:
1✔
465
        ec2_client = connect_to(aws_access_key_id=account_id, region_name=region_name).ec2
1✔
466
        try:
1✔
467
            return ec2_client.describe_subnets(SubnetIds=[subnet_id])["Subnets"][0]["VpcId"]
1✔
468
        except ec2_client.exceptions.ClientError as e:
1✔
469
            code = e.response["Error"]["Code"]
1✔
470
            message = e.response["Error"]["Message"]
1✔
471
            raise InvalidParameterValueException(
1✔
472
                f"Error occurred while DescribeSubnets. EC2 Error Code: {code}. EC2 Error Message: {message}",
473
                Type="User",
474
            )
475

476
    def _build_vpc_config(
1✔
477
        self,
478
        account_id: str,
479
        region_name: str,
480
        vpc_config: dict | None = None,
481
    ) -> VpcConfig | None:
482
        if not vpc_config or not is_api_enabled("ec2"):
1✔
483
            return None
1✔
484

485
        subnet_ids = vpc_config.get("SubnetIds", [])
1✔
486
        if subnet_ids is not None and len(subnet_ids) == 0:
1✔
487
            return VpcConfig(vpc_id="", security_group_ids=[], subnet_ids=[])
1✔
488

489
        subnet_id = subnet_ids[0]
1✔
490
        if not bool(SUBNET_ID_REGEX.match(subnet_id)):
1✔
491
            raise ValidationException(
1✔
492
                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]*$]"
493
            )
494

495
        return VpcConfig(
1✔
496
            vpc_id=self._resolve_vpc_id(account_id, region_name, subnet_id),
497
            security_group_ids=vpc_config.get("SecurityGroupIds", []),
498
            subnet_ids=subnet_ids,
499
        )
500

501
    def _create_version_model(
1✔
502
        self,
503
        function_name: str,
504
        region: str,
505
        account_id: str,
506
        description: str | None = None,
507
        revision_id: str | None = None,
508
        code_sha256: str | None = None,
509
    ) -> tuple[FunctionVersion, bool]:
510
        """
511
        Release a new version to the model if all restrictions are met.
512
        Restrictions:
513
          - CodeSha256, if provided, must equal the current latest version code hash
514
          - RevisionId, if provided, must equal the current latest version revision id
515
          - Some changes have been done to the latest version since last publish
516
        Will return a tuple of the version, and whether the version was published (True) or the latest available version was taken (False).
517
        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.
518

519
        :param function_name: Function name to be published
520
        :param region: Region of the function
521
        :param account_id: Account of the function
522
        :param description: new description of the version (will be the description of the function if missing)
523
        :param revision_id: Revision id, function will raise error if it does not match latest revision id
524
        :param code_sha256: Code sha256, function will raise error if it does not match latest code hash
525
        :return: Tuple of (published version, whether version was released or last released version returned, since nothing changed)
526
        """
527
        current_latest_version = get_function_version(
1✔
528
            function_name=function_name, qualifier="$LATEST", account_id=account_id, region=region
529
        )
530
        if revision_id and current_latest_version.config.revision_id != revision_id:
1✔
531
            raise PreconditionFailedException(
1✔
532
                "The Revision Id provided does not match the latest Revision Id. Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
533
                Type="User",
534
            )
535

536
        # check if code hashes match if they are specified
537
        current_hash = (
1✔
538
            current_latest_version.config.code.code_sha256
539
            if current_latest_version.config.package_type == PackageType.Zip
540
            else current_latest_version.config.image.code_sha256
541
        )
542
        # if the code is a zip package and hot reloaded (hot reloading is currently only supported for zip packagetypes)
543
        # we cannot enforce the codesha256 check
544
        is_hot_reloaded_zip_package = (
1✔
545
            current_latest_version.config.package_type == PackageType.Zip
546
            and current_latest_version.config.code.is_hot_reloading()
547
        )
548
        if code_sha256 and current_hash != code_sha256 and not is_hot_reloaded_zip_package:
1✔
549
            raise InvalidParameterValueException(
1✔
550
                f"CodeSHA256 ({code_sha256}) is different from current CodeSHA256 in $LATEST ({current_hash}). Please try again with the CodeSHA256 in $LATEST.",
551
                Type="User",
552
            )
553

554
        state = lambda_stores[account_id][region]
1✔
555
        function = state.functions.get(function_name)
1✔
556
        changes = {}
1✔
557
        if description is not None:
1✔
558
            changes["description"] = description
1✔
559
        # TODO copy environment instead of restarting one, get rid of all the "Pending"s
560

561
        with function.lock:
1✔
562
            if function.next_version > 1 and (
1✔
563
                prev_version := function.versions.get(str(function.next_version - 1))
564
            ):
565
                if (
1✔
566
                    prev_version.config.internal_revision
567
                    == current_latest_version.config.internal_revision
568
                ):
569
                    return prev_version, False
1✔
570
            # TODO check if there was a change since last version
571
            next_version = str(function.next_version)
1✔
572
            function.next_version += 1
1✔
573
            new_id = VersionIdentifier(
1✔
574
                function_name=function_name,
575
                qualifier=next_version,
576
                region=region,
577
                account=account_id,
578
            )
579
            apply_on = current_latest_version.config.snap_start["ApplyOn"]
1✔
580
            optimization_status = SnapStartOptimizationStatus.Off
1✔
581
            if apply_on == SnapStartApplyOn.PublishedVersions:
1✔
582
                optimization_status = SnapStartOptimizationStatus.On
×
583
            snap_start = SnapStartResponse(
1✔
584
                ApplyOn=apply_on,
585
                OptimizationStatus=optimization_status,
586
            )
587
            new_version = dataclasses.replace(
1✔
588
                current_latest_version,
589
                config=dataclasses.replace(
590
                    current_latest_version.config,
591
                    last_update=None,  # versions never have a last update status
592
                    state=VersionState(
593
                        state=State.Pending,
594
                        code=StateReasonCode.Creating,
595
                        reason="The function is being created.",
596
                    ),
597
                    snap_start=snap_start,
598
                    **changes,
599
                ),
600
                id=new_id,
601
            )
602
            function.versions[next_version] = new_version
1✔
603
        return new_version, True
1✔
604

605
    def _publish_version_from_existing_version(
1✔
606
        self,
607
        function_name: str,
608
        region: str,
609
        account_id: str,
610
        description: str | None = None,
611
        revision_id: str | None = None,
612
        code_sha256: str | None = None,
613
    ) -> FunctionVersion:
614
        """
615
        Publish version from an existing, already initialized LATEST
616

617
        :param function_name: Function name
618
        :param region: region
619
        :param account_id: account id
620
        :param description: description
621
        :param revision_id: revision id (check if current version matches)
622
        :param code_sha256: code sha (check if current code matches)
623
        :return: new version
624
        """
625
        new_version, changed = self._create_version_model(
1✔
626
            function_name=function_name,
627
            region=region,
628
            account_id=account_id,
629
            description=description,
630
            revision_id=revision_id,
631
            code_sha256=code_sha256,
632
        )
633
        if not changed:
1✔
634
            return new_version
1✔
635
        self.lambda_service.publish_version(new_version)
1✔
636
        state = lambda_stores[account_id][region]
1✔
637
        function = state.functions.get(function_name)
1✔
638
        # TODO: re-evaluate data model to prevent this dirty hack just for bumping the revision id
639
        latest_version = function.versions["$LATEST"]
1✔
640
        function.versions["$LATEST"] = dataclasses.replace(
1✔
641
            latest_version, config=dataclasses.replace(latest_version.config)
642
        )
643
        return function.versions.get(new_version.id.qualifier)
1✔
644

645
    def _publish_version_with_changes(
1✔
646
        self,
647
        function_name: str,
648
        region: str,
649
        account_id: str,
650
        description: str | None = None,
651
        revision_id: str | None = None,
652
        code_sha256: str | None = None,
653
    ) -> FunctionVersion:
654
        """
655
        Publish version together with a new latest version (publish on create / update)
656

657
        :param function_name: Function name
658
        :param region: region
659
        :param account_id: account id
660
        :param description: description
661
        :param revision_id: revision id (check if current version matches)
662
        :param code_sha256: code sha (check if current code matches)
663
        :return: new version
664
        """
665
        new_version, changed = self._create_version_model(
1✔
666
            function_name=function_name,
667
            region=region,
668
            account_id=account_id,
669
            description=description,
670
            revision_id=revision_id,
671
            code_sha256=code_sha256,
672
        )
673
        if not changed:
1✔
674
            return new_version
×
675
        self.lambda_service.create_function_version(new_version)
1✔
676
        return new_version
1✔
677

678
    @staticmethod
1✔
679
    def _verify_env_variables(env_vars: dict[str, str]):
1✔
680
        dumped_env_vars = json.dumps(env_vars, separators=(",", ":"))
1✔
681
        if (
1✔
682
            len(dumped_env_vars.encode("utf-8"))
683
            > config.LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES
684
        ):
685
            raise InvalidParameterValueException(
1✔
686
                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}",
687
                Type="User",
688
            )
689

690
    @staticmethod
1✔
691
    def _validate_snapstart(snap_start: SnapStart, runtime: Runtime):
1✔
692
        apply_on = snap_start.get("ApplyOn")
1✔
693
        if apply_on not in [
1✔
694
            SnapStartApplyOn.PublishedVersions,
695
            SnapStartApplyOn.None_,
696
        ]:
697
            raise ValidationException(
1✔
698
                f"1 validation error detected: Value '{apply_on}' at 'snapStart.applyOn' failed to satisfy constraint: Member must satisfy enum value set: [PublishedVersions, None]"
699
            )
700

701
        if runtime not in SNAP_START_SUPPORTED_RUNTIMES:
1✔
702
            raise InvalidParameterValueException(
×
703
                f"{runtime} is not supported for SnapStart enabled functions.", Type="User"
704
            )
705

706
    def _validate_layers(self, new_layers: list[str], region: str, account_id: str):
1✔
707
        if len(new_layers) > LAMBDA_LAYERS_LIMIT_PER_FUNCTION:
1✔
708
            raise InvalidParameterValueException(
1✔
709
                "Cannot reference more than 5 layers.", Type="User"
710
            )
711

712
        visited_layers = {}
1✔
713
        for layer_version_arn in new_layers:
1✔
714
            (
1✔
715
                layer_region,
716
                layer_account_id,
717
                layer_name,
718
                layer_version_str,
719
            ) = api_utils.parse_layer_arn(layer_version_arn)
720
            if layer_version_str is None:
1✔
721
                raise ValidationException(
1✔
722
                    f"1 validation error detected: Value '[{layer_version_arn}]'"
723
                    + r" at 'layers' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 140, Member must have length greater than or equal to 1, Member must satisfy regular expression pattern: (arn:[a-zA-Z0-9-]+:lambda:[a-z]{2}((-gov)|(-iso(b?)))?-[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]",
724
                )
725

726
            state = lambda_stores[layer_account_id][layer_region]
1✔
727
            layer = state.layers.get(layer_name)
1✔
728
            layer_version = None
1✔
729
            if layer is not None:
1✔
730
                layer_version = layer.layer_versions.get(layer_version_str)
1✔
731
            if layer_account_id == account_id:
1✔
732
                if region and layer_region != region:
1✔
733
                    raise InvalidParameterValueException(
1✔
734
                        f"Layers are not in the same region as the function. "
735
                        f"Layers are expected to be in region {region}.",
736
                        Type="User",
737
                    )
738
                if layer is None or layer.layer_versions.get(layer_version_str) is None:
1✔
739
                    raise InvalidParameterValueException(
1✔
740
                        f"Layer version {layer_version_arn} does not exist.", Type="User"
741
                    )
742
            else:  # External layer from other account
743
                # TODO: validate IAM layer policy here, allowing access by default for now and only checking region
744
                if region and layer_region != region:
×
745
                    # TODO: detect user or role from context when IAM users are implemented
746
                    user = "user/localstack-testing"
×
747
                    raise AccessDeniedException(
×
748
                        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"
749
                    )
750
                if layer is None or layer_version is None:
×
751
                    # Limitation: cannot fetch external layers when using the same account id as the target layer
752
                    # because we do not want to trigger the layer fetcher for every non-existing layer.
753
                    if self.layer_fetcher is None:
×
754
                        raise NotImplementedError(
755
                            "Fetching shared layers from AWS is a pro feature."
756
                        )
757

758
                    layer = self.layer_fetcher.fetch_layer(layer_version_arn)
×
759
                    if layer is None:
×
760
                        # TODO: detect user or role from context when IAM users are implemented
761
                        user = "user/localstack-testing"
×
762
                        raise AccessDeniedException(
×
763
                            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"
764
                        )
765

766
                    # Distinguish between new layer and new layer version
767
                    if layer_version is None:
×
768
                        # Create whole layer from scratch
769
                        state.layers[layer_name] = layer
×
770
                    else:
771
                        # Create layer version if another version of the same layer already exists
772
                        state.layers[layer_name].layer_versions[layer_version_str] = (
×
773
                            layer.layer_versions.get(layer_version_str)
774
                        )
775

776
            # only the first two matches in the array are considered for the error message
777
            layer_arn = ":".join(layer_version_arn.split(":")[:-1])
1✔
778
            if layer_arn in visited_layers:
1✔
779
                conflict_layer_version_arn = visited_layers[layer_arn]
1✔
780
                raise InvalidParameterValueException(
1✔
781
                    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.",
782
                    Type="User",
783
                )
784
            visited_layers[layer_arn] = layer_version_arn
1✔
785

786
    @staticmethod
1✔
787
    def map_layers(new_layers: list[str]) -> list[LayerVersion]:
1✔
788
        layers = []
1✔
789
        for layer_version_arn in new_layers:
1✔
790
            region_name, account_id, layer_name, layer_version = api_utils.parse_layer_arn(
1✔
791
                layer_version_arn
792
            )
793
            layer = lambda_stores[account_id][region_name].layers.get(layer_name)
1✔
794
            layer_version = layer.layer_versions.get(layer_version)
1✔
795
            layers.append(layer_version)
1✔
796
        return layers
1✔
797

798
    def get_function_recursion_config(
1✔
799
        self,
800
        context: RequestContext,
801
        function_name: UnqualifiedFunctionName,
802
        **kwargs,
803
    ) -> GetFunctionRecursionConfigResponse:
804
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
805
        function_name = api_utils.get_function_name(function_name, context)
1✔
806
        fn = self._get_function(function_name=function_name, region=region, account_id=account_id)
1✔
807
        return GetFunctionRecursionConfigResponse(RecursiveLoop=fn.recursive_loop)
1✔
808

809
    def put_function_recursion_config(
1✔
810
        self,
811
        context: RequestContext,
812
        function_name: UnqualifiedFunctionName,
813
        recursive_loop: RecursiveLoop,
814
        **kwargs,
815
    ) -> PutFunctionRecursionConfigResponse:
816
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
817
        function_name = api_utils.get_function_name(function_name, context)
1✔
818

819
        fn = self._get_function(function_name=function_name, region=region, account_id=account_id)
1✔
820

821
        allowed_values = list(RecursiveLoop.__members__.values())
1✔
822
        if recursive_loop not in allowed_values:
1✔
823
            raise ValidationException(
1✔
824
                f"1 validation error detected: Value '{recursive_loop}' at 'recursiveLoop' failed to satisfy constraint: "
825
                f"Member must satisfy enum value set: [Terminate, Allow]"
826
            )
827

828
        fn.recursive_loop = recursive_loop
1✔
829
        return PutFunctionRecursionConfigResponse(RecursiveLoop=fn.recursive_loop)
1✔
830

831
    @handler(operation="CreateFunction", expand=False)
1✔
832
    def create_function(
1✔
833
        self,
834
        context: RequestContext,
835
        request: CreateFunctionRequest,
836
    ) -> FunctionConfiguration:
837
        context_region = context.region
1✔
838
        context_account_id = context.account_id
1✔
839

840
        zip_file = request.get("Code", {}).get("ZipFile")
1✔
841
        if zip_file and len(zip_file) > config.LAMBDA_LIMITS_CODE_SIZE_ZIPPED:
1✔
842
            raise RequestEntityTooLargeException(
1✔
843
                f"Zipped size must be smaller than {config.LAMBDA_LIMITS_CODE_SIZE_ZIPPED} bytes"
844
            )
845

846
        if context.request.content_length > config.LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE:
1✔
847
            raise RequestEntityTooLargeException(
1✔
848
                f"Request must be smaller than {config.LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE} bytes for the CreateFunction operation"
849
            )
850

851
        if architectures := request.get("Architectures"):
1✔
852
            if len(architectures) != 1:
1✔
853
                raise ValidationException(
1✔
854
                    f"1 validation error detected: Value '[{', '.join(architectures)}]' at 'architectures' failed to "
855
                    f"satisfy constraint: Member must have length less than or equal to 1",
856
                )
857
            if architectures[0] not in ARCHITECTURES:
1✔
858
                raise ValidationException(
1✔
859
                    f"1 validation error detected: Value '[{', '.join(architectures)}]' at 'architectures' failed to "
860
                    f"satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: "
861
                    f"[x86_64, arm64], Member must not be null]",
862
                )
863

864
        if env_vars := request.get("Environment", {}).get("Variables"):
1✔
865
            self._verify_env_variables(env_vars)
1✔
866

867
        if layers := request.get("Layers", []):
1✔
868
            self._validate_layers(layers, region=context_region, account_id=context_account_id)
1✔
869

870
        if not api_utils.is_role_arn(request.get("Role")):
1✔
871
            raise ValidationException(
1✔
872
                f"1 validation error detected: Value '{request.get('Role')}'"
873
                + " 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+=,.@\\-_/]+"
874
            )
875
        if not self.lambda_service.can_assume_role(request.get("Role"), context.region):
1✔
876
            raise InvalidParameterValueException(
×
877
                "The role defined for the function cannot be assumed by Lambda.", Type="User"
878
            )
879
        package_type = request.get("PackageType", PackageType.Zip)
1✔
880
        runtime = request.get("Runtime")
1✔
881
        self._validate_runtime(package_type, runtime)
1✔
882

883
        request_function_name = request.get("FunctionName")
1✔
884

885
        function_name, *_ = api_utils.get_name_and_qualifier(
1✔
886
            function_arn_or_name=request_function_name,
887
            qualifier=None,
888
            context=context,
889
        )
890

891
        if runtime in DEPRECATED_RUNTIMES:
1✔
892
            LOG.warning(
1✔
893
                "The Lambda runtime %s} is deprecated. "
894
                "Please upgrade the runtime for the function %s: "
895
                "https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html",
896
                runtime,
897
                function_name,
898
            )
899
        if snap_start := request.get("SnapStart"):
1✔
900
            self._validate_snapstart(snap_start, runtime)
1✔
901
        state = lambda_stores[context_account_id][context_region]
1✔
902

903
        with self.create_fn_lock:
1✔
904
            if function_name in state.functions:
1✔
905
                raise ResourceConflictException(f"Function already exist: {function_name}")
×
906
            fn = Function(function_name=function_name)
1✔
907
            arn = VersionIdentifier(
1✔
908
                function_name=function_name,
909
                qualifier="$LATEST",
910
                region=context_region,
911
                account=context_account_id,
912
            )
913
            # save function code to s3
914
            code = None
1✔
915
            image = None
1✔
916
            image_config = None
1✔
917
            runtime_version_config = RuntimeVersionConfig(
1✔
918
                # Limitation: the runtime id (presumably sha256 of image) is currently hardcoded
919
                # Potential implementation: provide (cached) sha256 hash of used Docker image
920
                RuntimeVersionArn=f"arn:{context.partition}:lambda:{context_region}::runtime:8eeff65f6809a3ce81507fe733fe09b835899b99481ba22fd75b5a7338290ec1"
921
            )
922
            request_code = request.get("Code")
1✔
923
            if package_type == PackageType.Zip:
1✔
924
                # TODO verify if correct combination of code is set
925
                if zip_file := request_code.get("ZipFile"):
1✔
926
                    code = store_lambda_archive(
1✔
927
                        archive_file=zip_file,
928
                        function_name=function_name,
929
                        region_name=context_region,
930
                        account_id=context_account_id,
931
                    )
932
                elif s3_bucket := request_code.get("S3Bucket"):
1✔
933
                    s3_key = request_code["S3Key"]
1✔
934
                    s3_object_version = request_code.get("S3ObjectVersion")
1✔
935
                    code = store_s3_bucket_archive(
1✔
936
                        archive_bucket=s3_bucket,
937
                        archive_key=s3_key,
938
                        archive_version=s3_object_version,
939
                        function_name=function_name,
940
                        region_name=context_region,
941
                        account_id=context_account_id,
942
                    )
943
                else:
944
                    raise LambdaServiceException("Gotta have s3 bucket or zip file")
×
945
            elif package_type == PackageType.Image:
1✔
946
                image = request_code.get("ImageUri")
1✔
947
                if not image:
1✔
948
                    raise LambdaServiceException("Gotta have an image when package type is image")
×
949
                image = create_image_code(image_uri=image)
1✔
950

951
                image_config_req = request.get("ImageConfig", {})
1✔
952
                image_config = ImageConfig(
1✔
953
                    command=image_config_req.get("Command"),
954
                    entrypoint=image_config_req.get("EntryPoint"),
955
                    working_directory=image_config_req.get("WorkingDirectory"),
956
                )
957
                # Runtime management controls are not available when providing a custom image
958
                runtime_version_config = None
1✔
959
            if "LoggingConfig" in request:
1✔
960
                logging_config = request["LoggingConfig"]
1✔
961
                LOG.warning(
1✔
962
                    "Advanced Lambda Logging Configuration is currently mocked "
963
                    "and will not impact the logging behavior. "
964
                    "Please create a feature request if needed."
965
                )
966

967
                # when switching to JSON, app and system level log is auto set to INFO
968
                if logging_config.get("LogFormat", None) == LogFormat.JSON:
1✔
969
                    logging_config = {
1✔
970
                        "ApplicationLogLevel": "INFO",
971
                        "SystemLogLevel": "INFO",
972
                        "LogGroup": f"/aws/lambda/{function_name}",
973
                    } | logging_config
974
                else:
975
                    logging_config = (
×
976
                        LoggingConfig(
977
                            LogFormat=LogFormat.Text, LogGroup=f"/aws/lambda/{function_name}"
978
                        )
979
                        | logging_config
980
                    )
981

982
            else:
983
                logging_config = LoggingConfig(
1✔
984
                    LogFormat=LogFormat.Text, LogGroup=f"/aws/lambda/{function_name}"
985
                )
986

987
            version = FunctionVersion(
1✔
988
                id=arn,
989
                config=VersionFunctionConfiguration(
990
                    last_modified=api_utils.format_lambda_date(datetime.datetime.now()),
991
                    description=request.get("Description", ""),
992
                    role=request["Role"],
993
                    timeout=request.get("Timeout", LAMBDA_DEFAULT_TIMEOUT),
994
                    runtime=request.get("Runtime"),
995
                    memory_size=request.get("MemorySize", LAMBDA_DEFAULT_MEMORY_SIZE),
996
                    handler=request.get("Handler"),
997
                    package_type=package_type,
998
                    environment=env_vars,
999
                    architectures=request.get("Architectures") or [Architecture.x86_64],
1000
                    tracing_config_mode=request.get("TracingConfig", {}).get(
1001
                        "Mode", TracingMode.PassThrough
1002
                    ),
1003
                    image=image,
1004
                    image_config=image_config,
1005
                    code=code,
1006
                    layers=self.map_layers(layers),
1007
                    internal_revision=short_uid(),
1008
                    ephemeral_storage=LambdaEphemeralStorage(
1009
                        size=request.get("EphemeralStorage", {}).get("Size", 512)
1010
                    ),
1011
                    snap_start=SnapStartResponse(
1012
                        ApplyOn=request.get("SnapStart", {}).get("ApplyOn", SnapStartApplyOn.None_),
1013
                        OptimizationStatus=SnapStartOptimizationStatus.Off,
1014
                    ),
1015
                    runtime_version_config=runtime_version_config,
1016
                    dead_letter_arn=request.get("DeadLetterConfig", {}).get("TargetArn"),
1017
                    vpc_config=self._build_vpc_config(
1018
                        context_account_id, context_region, request.get("VpcConfig")
1019
                    ),
1020
                    state=VersionState(
1021
                        state=State.Pending,
1022
                        code=StateReasonCode.Creating,
1023
                        reason="The function is being created.",
1024
                    ),
1025
                    logging_config=logging_config,
1026
                ),
1027
            )
1028
            fn.versions["$LATEST"] = version
1✔
1029
            state.functions[function_name] = fn
1✔
1030
        function_counter.labels(
1✔
1031
            operation=FunctionOperation.create,
1032
            runtime=runtime or "n/a",
1033
            status=FunctionStatus.success,
1034
            invocation_type="n/a",
1035
            package_type=package_type,
1036
        )
1037
        self.lambda_service.create_function_version(version)
1✔
1038

1039
        if tags := request.get("Tags"):
1✔
1040
            # This will check whether the function exists.
1041
            self._store_tags(arn.unqualified_arn(), tags)
1✔
1042

1043
        if request.get("Publish"):
1✔
1044
            version = self._publish_version_with_changes(
1✔
1045
                function_name=function_name, region=context_region, account_id=context_account_id
1046
            )
1047

1048
        if config.LAMBDA_SYNCHRONOUS_CREATE:
1✔
1049
            # block via retrying until "terminal" condition reached before returning
1050
            if not poll_condition(
×
1051
                lambda: get_function_version(
1052
                    function_name, version.id.qualifier, version.id.account, version.id.region
1053
                ).config.state.state
1054
                in [State.Active, State.Failed],
1055
                timeout=10,
1056
            ):
1057
                LOG.warning(
×
1058
                    "LAMBDA_SYNCHRONOUS_CREATE is active, but waiting for %s reached timeout.",
1059
                    function_name,
1060
                )
1061

1062
        return api_utils.map_config_out(
1✔
1063
            version, return_qualified_arn=False, return_update_status=False
1064
        )
1065

1066
    def _validate_runtime(self, package_type, runtime):
1✔
1067
        runtimes = ALL_RUNTIMES
1✔
1068
        if config.LAMBDA_RUNTIME_VALIDATION:
1✔
1069
            runtimes = list(itertools.chain(RUNTIMES_AGGREGATED.values()))
1✔
1070

1071
        if package_type == PackageType.Zip and runtime not in runtimes:
1✔
1072
            # deprecated runtimes have different error
1073
            if runtime in DEPRECATED_RUNTIMES:
1✔
1074
                HINT_LOG.info(
1✔
1075
                    "Set env variable LAMBDA_RUNTIME_VALIDATION to 0"
1076
                    " in order to allow usage of deprecated runtimes"
1077
                )
1078
                self._check_for_recomended_migration_target(runtime)
1✔
1079

1080
            raise InvalidParameterValueException(
1✔
1081
                f"Value {runtime} at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: {VALID_RUNTIMES} or be a valid ARN",
1082
                Type="User",
1083
            )
1084

1085
    def _check_for_recomended_migration_target(self, deprecated_runtime):
1✔
1086
        # AWS offers recommended runtime for migration for "newly" deprecated runtimes
1087
        # in order to preserve parity with error messages we need the code bellow
1088
        latest_runtime = DEPRECATED_RUNTIMES_UPGRADES.get(deprecated_runtime)
1✔
1089

1090
        if latest_runtime is not None:
1✔
1091
            LOG.debug(
1✔
1092
                "The Lambda runtime %s is deprecated. Please upgrade to a supported Lambda runtime such as %s.",
1093
                deprecated_runtime,
1094
                latest_runtime,
1095
            )
1096
            raise InvalidParameterValueException(
1✔
1097
                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.",
1098
                Type="User",
1099
            )
1100

1101
    @handler(operation="UpdateFunctionConfiguration", expand=False)
1✔
1102
    def update_function_configuration(
1✔
1103
        self, context: RequestContext, request: UpdateFunctionConfigurationRequest
1104
    ) -> FunctionConfiguration:
1105
        """updates the $LATEST version of the function"""
1106
        function_name = request.get("FunctionName")
1✔
1107

1108
        # in case we got ARN or partial ARN
1109
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1110
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
1111
        state = lambda_stores[account_id][region]
1✔
1112

1113
        if function_name not in state.functions:
1✔
1114
            raise ResourceNotFoundException(
×
1115
                f"Function not found: {api_utils.unqualified_lambda_arn(function_name=function_name, region=region, account=account_id)}",
1116
                Type="User",
1117
            )
1118
        function = state.functions[function_name]
1✔
1119

1120
        # TODO: lock modification of latest version
1121
        # TODO: notify service for changes relevant to re-provisioning of $LATEST
1122
        latest_version = function.latest()
1✔
1123
        latest_version_config = latest_version.config
1✔
1124

1125
        revision_id = request.get("RevisionId")
1✔
1126
        if revision_id and revision_id != latest_version.config.revision_id:
1✔
1127
            raise PreconditionFailedException(
1✔
1128
                "The Revision Id provided does not match the latest Revision Id. "
1129
                "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
1130
                Type="User",
1131
            )
1132

1133
        replace_kwargs = {}
1✔
1134
        if "EphemeralStorage" in request:
1✔
1135
            replace_kwargs["ephemeral_storage"] = LambdaEphemeralStorage(
×
1136
                request.get("EphemeralStorage", {}).get("Size", 512)
1137
            )  # TODO: do defaults here apply as well?
1138

1139
        if "Role" in request:
1✔
1140
            if not api_utils.is_role_arn(request["Role"]):
1✔
1141
                raise ValidationException(
1✔
1142
                    f"1 validation error detected: Value '{request.get('Role')}'"
1143
                    + " 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+=,.@\\-_/]+"
1144
                )
1145
            replace_kwargs["role"] = request["Role"]
1✔
1146

1147
        if "Description" in request:
1✔
1148
            replace_kwargs["description"] = request["Description"]
1✔
1149

1150
        if "Timeout" in request:
1✔
1151
            replace_kwargs["timeout"] = request["Timeout"]
1✔
1152

1153
        if "MemorySize" in request:
1✔
1154
            replace_kwargs["memory_size"] = request["MemorySize"]
1✔
1155

1156
        if "DeadLetterConfig" in request:
1✔
1157
            replace_kwargs["dead_letter_arn"] = request.get("DeadLetterConfig", {}).get("TargetArn")
1✔
1158

1159
        if vpc_config := request.get("VpcConfig"):
1✔
1160
            replace_kwargs["vpc_config"] = self._build_vpc_config(account_id, region, vpc_config)
1✔
1161

1162
        if "Handler" in request:
1✔
1163
            replace_kwargs["handler"] = request["Handler"]
1✔
1164

1165
        if "Runtime" in request:
1✔
1166
            runtime = request["Runtime"]
1✔
1167

1168
            if runtime not in ALL_RUNTIMES:
1✔
1169
                raise InvalidParameterValueException(
1✔
1170
                    f"Value {runtime} at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: {VALID_RUNTIMES} or be a valid ARN",
1171
                    Type="User",
1172
                )
1173
            if runtime in DEPRECATED_RUNTIMES:
1✔
1174
                LOG.warning(
×
1175
                    "The Lambda runtime %s is deprecated. "
1176
                    "Please upgrade the runtime for the function %s: "
1177
                    "https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html",
1178
                    runtime,
1179
                    function_name,
1180
                )
1181
            replace_kwargs["runtime"] = request["Runtime"]
1✔
1182

1183
        if snap_start := request.get("SnapStart"):
1✔
1184
            runtime = replace_kwargs.get("runtime") or latest_version_config.runtime
1✔
1185
            self._validate_snapstart(snap_start, runtime)
1✔
1186
            replace_kwargs["snap_start"] = SnapStartResponse(
1✔
1187
                ApplyOn=snap_start.get("ApplyOn", SnapStartApplyOn.None_),
1188
                OptimizationStatus=SnapStartOptimizationStatus.Off,
1189
            )
1190

1191
        if "Environment" in request:
1✔
1192
            if env_vars := request.get("Environment", {}).get("Variables", {}):
1✔
1193
                self._verify_env_variables(env_vars)
1✔
1194
            replace_kwargs["environment"] = env_vars
1✔
1195

1196
        if "Layers" in request:
1✔
1197
            new_layers = request["Layers"]
1✔
1198
            if new_layers:
1✔
1199
                self._validate_layers(new_layers, region=region, account_id=account_id)
1✔
1200
            replace_kwargs["layers"] = self.map_layers(new_layers)
1✔
1201

1202
        if "ImageConfig" in request:
1✔
1203
            new_image_config = request["ImageConfig"]
1✔
1204
            replace_kwargs["image_config"] = ImageConfig(
1✔
1205
                command=new_image_config.get("Command"),
1206
                entrypoint=new_image_config.get("EntryPoint"),
1207
                working_directory=new_image_config.get("WorkingDirectory"),
1208
            )
1209

1210
        if "LoggingConfig" in request:
1✔
1211
            logging_config = request["LoggingConfig"]
1✔
1212
            LOG.warning(
1✔
1213
                "Advanced Lambda Logging Configuration is currently mocked "
1214
                "and will not impact the logging behavior. "
1215
                "Please create a feature request if needed."
1216
            )
1217

1218
            # when switching to JSON, app and system level log is auto set to INFO
1219
            if logging_config.get("LogFormat", None) == LogFormat.JSON:
1✔
1220
                logging_config = {
1✔
1221
                    "ApplicationLogLevel": "INFO",
1222
                    "SystemLogLevel": "INFO",
1223
                } | logging_config
1224

1225
            last_config = latest_version_config.logging_config
1✔
1226

1227
            # add partial update
1228
            new_logging_config = last_config | logging_config
1✔
1229

1230
            # in case we switched from JSON to Text we need to remove LogLevel keys
1231
            if (
1✔
1232
                new_logging_config.get("LogFormat") == LogFormat.Text
1233
                and last_config.get("LogFormat") == LogFormat.JSON
1234
            ):
1235
                new_logging_config.pop("ApplicationLogLevel", None)
1✔
1236
                new_logging_config.pop("SystemLogLevel", None)
1✔
1237

1238
            replace_kwargs["logging_config"] = new_logging_config
1✔
1239

1240
        if "TracingConfig" in request:
1✔
1241
            new_mode = request.get("TracingConfig", {}).get("Mode")
×
1242
            if new_mode:
×
1243
                replace_kwargs["tracing_config_mode"] = new_mode
×
1244

1245
        new_latest_version = dataclasses.replace(
1✔
1246
            latest_version,
1247
            config=dataclasses.replace(
1248
                latest_version_config,
1249
                last_modified=api_utils.generate_lambda_date(),
1250
                internal_revision=short_uid(),
1251
                last_update=UpdateStatus(
1252
                    status=LastUpdateStatus.InProgress,
1253
                    code="Creating",
1254
                    reason="The function is being created.",
1255
                ),
1256
                **replace_kwargs,
1257
            ),
1258
        )
1259
        function.versions["$LATEST"] = new_latest_version  # TODO: notify
1✔
1260
        self.lambda_service.update_version(new_version=new_latest_version)
1✔
1261

1262
        return api_utils.map_config_out(new_latest_version)
1✔
1263

1264
    @handler(operation="UpdateFunctionCode", expand=False)
1✔
1265
    def update_function_code(
1✔
1266
        self, context: RequestContext, request: UpdateFunctionCodeRequest
1267
    ) -> FunctionConfiguration:
1268
        """updates the $LATEST version of the function"""
1269
        # only supports normal zip packaging atm
1270
        # if request.get("Publish"):
1271
        #     self.lambda_service.create_function_version()
1272

1273
        function_name = request.get("FunctionName")
1✔
1274
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1275
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
1276

1277
        store = lambda_stores[account_id][region]
1✔
1278
        if function_name not in store.functions:
1✔
1279
            raise ResourceNotFoundException(
×
1280
                f"Function not found: {api_utils.unqualified_lambda_arn(function_name=function_name, region=region, account=account_id)}",
1281
                Type="User",
1282
            )
1283
        function = store.functions[function_name]
1✔
1284

1285
        revision_id = request.get("RevisionId")
1✔
1286
        if revision_id and revision_id != function.latest().config.revision_id:
1✔
1287
            raise PreconditionFailedException(
1✔
1288
                "The Revision Id provided does not match the latest Revision Id. "
1289
                "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
1290
                Type="User",
1291
            )
1292

1293
        # TODO verify if correct combination of code is set
1294
        image = None
1✔
1295
        if (
1✔
1296
            request.get("ZipFile") or request.get("S3Bucket")
1297
        ) and function.latest().config.package_type == PackageType.Image:
1298
            raise InvalidParameterValueException(
1✔
1299
                "Please provide ImageUri when updating a function with packageType Image.",
1300
                Type="User",
1301
            )
1302
        elif request.get("ImageUri") and function.latest().config.package_type == PackageType.Zip:
1✔
1303
            raise InvalidParameterValueException(
1✔
1304
                "Please don't provide ImageUri when updating a function with packageType Zip.",
1305
                Type="User",
1306
            )
1307

1308
        if zip_file := request.get("ZipFile"):
1✔
1309
            code = store_lambda_archive(
1✔
1310
                archive_file=zip_file,
1311
                function_name=function_name,
1312
                region_name=region,
1313
                account_id=account_id,
1314
            )
1315
        elif s3_bucket := request.get("S3Bucket"):
1✔
1316
            s3_key = request["S3Key"]
1✔
1317
            s3_object_version = request.get("S3ObjectVersion")
1✔
1318
            code = store_s3_bucket_archive(
1✔
1319
                archive_bucket=s3_bucket,
1320
                archive_key=s3_key,
1321
                archive_version=s3_object_version,
1322
                function_name=function_name,
1323
                region_name=region,
1324
                account_id=account_id,
1325
            )
1326
        elif image := request.get("ImageUri"):
1✔
1327
            code = None
1✔
1328
            image = create_image_code(image_uri=image)
1✔
1329
        else:
1330
            raise LambdaServiceException("Gotta have s3 bucket or zip file or image")
×
1331

1332
        old_function_version = function.versions.get("$LATEST")
1✔
1333
        replace_kwargs = {"code": code} if code else {"image": image}
1✔
1334

1335
        if architectures := request.get("Architectures"):
1✔
1336
            if len(architectures) != 1:
×
1337
                raise ValidationException(
×
1338
                    f"1 validation error detected: Value '[{', '.join(architectures)}]' at 'architectures' failed to "
1339
                    f"satisfy constraint: Member must have length less than or equal to 1",
1340
                )
1341
            # An empty list of architectures is also forbidden. Further exceptions are tested here for create_function:
1342
            # tests.aws.services.lambda_.test_lambda_api.TestLambdaFunction.test_create_lambda_exceptions
1343
            if architectures[0] not in ARCHITECTURES:
×
1344
                raise ValidationException(
×
1345
                    f"1 validation error detected: Value '[{', '.join(architectures)}]' at 'architectures' failed to "
1346
                    f"satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: "
1347
                    f"[x86_64, arm64], Member must not be null]",
1348
                )
1349
            replace_kwargs["architectures"] = architectures
×
1350

1351
        config = dataclasses.replace(
1✔
1352
            old_function_version.config,
1353
            internal_revision=short_uid(),
1354
            last_modified=api_utils.generate_lambda_date(),
1355
            last_update=UpdateStatus(
1356
                status=LastUpdateStatus.InProgress,
1357
                code="Creating",
1358
                reason="The function is being created.",
1359
            ),
1360
            **replace_kwargs,
1361
        )
1362
        function_version = dataclasses.replace(old_function_version, config=config)
1✔
1363
        function.versions["$LATEST"] = function_version
1✔
1364

1365
        self.lambda_service.update_version(new_version=function_version)
1✔
1366
        if request.get("Publish"):
1✔
1367
            function_version = self._publish_version_with_changes(
1✔
1368
                function_name=function_name, region=region, account_id=account_id
1369
            )
1370
        return api_utils.map_config_out(
1✔
1371
            function_version, return_qualified_arn=bool(request.get("Publish"))
1372
        )
1373

1374
    # TODO: does deleting the latest published version affect the next versions number?
1375
    # TODO: what happens when we call this with a qualifier and a fully qualified ARN? (+ conflicts?)
1376
    # TODO: test different ARN patterns (shorthand ARN?)
1377
    # TODO: test deleting across regions?
1378
    # TODO: test mismatch between context region and region in ARN
1379
    # TODO: test qualifier $LATEST, alias-name and version
1380
    def delete_function(
1✔
1381
        self,
1382
        context: RequestContext,
1383
        function_name: FunctionName,
1384
        qualifier: Qualifier = None,
1385
        **kwargs,
1386
    ) -> None:
1387
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1388
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
1389
            function_name, qualifier, context
1390
        )
1391

1392
        if qualifier and api_utils.qualifier_is_alias(qualifier):
1✔
1393
            raise InvalidParameterValueException(
×
1394
                "Deletion of aliases is not currently supported.",
1395
                Type="User",
1396
            )
1397

1398
        store = lambda_stores[account_id][region]
1✔
1399
        if qualifier == "$LATEST":
1✔
1400
            raise InvalidParameterValueException(
1✔
1401
                "$LATEST version cannot be deleted without deleting the function.", Type="User"
1402
            )
1403

1404
        if function_name not in store.functions:
1✔
1405
            e = ResourceNotFoundException(
1✔
1406
                f"Function not found: {api_utils.unqualified_lambda_arn(function_name=function_name, region=region, account=account_id)}",
1407
                Type="User",
1408
            )
1409
            raise e
1✔
1410
        function = store.functions.get(function_name)
1✔
1411

1412
        if qualifier:
1✔
1413
            # delete a version of the function
1414
            version = function.versions.pop(qualifier, None)
1✔
1415
            if version:
1✔
1416
                self.lambda_service.stop_version(version.id.qualified_arn())
1✔
1417
                destroy_code_if_not_used(code=version.config.code, function=function)
1✔
1418
        else:
1419
            # delete the whole function
1420
            # TODO: introduce locking for safe deletion: We could create a new version at the API layer before
1421
            #  the old version gets cleaned up in the internal lambda service.
1422
            function = store.functions.pop(function_name)
1✔
1423
            for version in function.versions.values():
1✔
1424
                self.lambda_service.stop_version(qualified_arn=version.id.qualified_arn())
1✔
1425
                # we can safely destroy the code here
1426
                if version.config.code:
1✔
1427
                    version.config.code.destroy()
1✔
1428

1429
    def list_functions(
1✔
1430
        self,
1431
        context: RequestContext,
1432
        master_region: MasterRegion = None,  # (only relevant for lambda@edge)
1433
        function_version: FunctionVersionApi = None,
1434
        marker: String = None,
1435
        max_items: MaxListItems = None,
1436
        **kwargs,
1437
    ) -> ListFunctionsResponse:
1438
        state = lambda_stores[context.account_id][context.region]
1✔
1439

1440
        if function_version and function_version != FunctionVersionApi.ALL:
1✔
1441
            raise ValidationException(
1✔
1442
                f"1 validation error detected: Value '{function_version}'"
1443
                + " at 'functionVersion' failed to satisfy constraint: Member must satisfy enum value set: [ALL]"
1444
            )
1445

1446
        if function_version == FunctionVersionApi.ALL:
1✔
1447
            # include all versions for all function
1448
            versions = [v for f in state.functions.values() for v in f.versions.values()]
1✔
1449
            return_qualified_arn = True
1✔
1450
        else:
1451
            versions = [f.latest() for f in state.functions.values()]
1✔
1452
            return_qualified_arn = False
1✔
1453

1454
        versions = [
1✔
1455
            api_utils.map_to_list_response(
1456
                api_utils.map_config_out(fc, return_qualified_arn=return_qualified_arn)
1457
            )
1458
            for fc in versions
1459
        ]
1460
        versions = PaginatedList(versions)
1✔
1461
        page, token = versions.get_page(
1✔
1462
            lambda version: version["FunctionArn"],
1463
            marker,
1464
            max_items,
1465
        )
1466
        return ListFunctionsResponse(Functions=page, NextMarker=token)
1✔
1467

1468
    def get_function(
1✔
1469
        self,
1470
        context: RequestContext,
1471
        function_name: NamespacedFunctionName,
1472
        qualifier: Qualifier = None,
1473
        **kwargs,
1474
    ) -> GetFunctionResponse:
1475
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1476
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
1477
            function_name, qualifier, context
1478
        )
1479

1480
        fn = lambda_stores[account_id][region].functions.get(function_name)
1✔
1481
        if fn is None:
1✔
1482
            if qualifier is None:
1✔
1483
                raise ResourceNotFoundException(
1✔
1484
                    f"Function not found: {api_utils.unqualified_lambda_arn(function_name, account_id, region)}",
1485
                    Type="User",
1486
                )
1487
            else:
1488
                raise ResourceNotFoundException(
1✔
1489
                    f"Function not found: {api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)}",
1490
                    Type="User",
1491
                )
1492
        alias_name = None
1✔
1493
        if qualifier and api_utils.qualifier_is_alias(qualifier):
1✔
1494
            if qualifier not in fn.aliases:
1✔
1495
                alias_arn = api_utils.qualified_lambda_arn(
1✔
1496
                    function_name, qualifier, account_id, region
1497
                )
1498
                raise ResourceNotFoundException(f"Function not found: {alias_arn}", Type="User")
1✔
1499
            alias_name = qualifier
1✔
1500
            qualifier = fn.aliases[alias_name].function_version
1✔
1501

1502
        version = get_function_version(
1✔
1503
            function_name=function_name,
1504
            qualifier=qualifier,
1505
            account_id=account_id,
1506
            region=region,
1507
        )
1508
        tags = self._get_tags(api_utils.unqualified_lambda_arn(function_name, account_id, region))
1✔
1509
        additional_fields = {}
1✔
1510
        if tags:
1✔
1511
            additional_fields["Tags"] = tags
1✔
1512
        code_location = None
1✔
1513
        if code := version.config.code:
1✔
1514
            code_location = FunctionCodeLocation(
1✔
1515
                Location=code.generate_presigned_url(), RepositoryType="S3"
1516
            )
1517
        elif image := version.config.image:
1✔
1518
            code_location = FunctionCodeLocation(
1✔
1519
                ImageUri=image.image_uri,
1520
                RepositoryType=image.repository_type,
1521
                ResolvedImageUri=image.resolved_image_uri,
1522
            )
1523
        concurrency = None
1✔
1524
        if fn.reserved_concurrent_executions:
1✔
1525
            concurrency = Concurrency(
1✔
1526
                ReservedConcurrentExecutions=fn.reserved_concurrent_executions
1527
            )
1528

1529
        return GetFunctionResponse(
1✔
1530
            Configuration=api_utils.map_config_out(
1531
                version, return_qualified_arn=bool(qualifier), alias_name=alias_name
1532
            ),
1533
            Code=code_location,  # TODO
1534
            Concurrency=concurrency,
1535
            **additional_fields,
1536
        )
1537

1538
    def get_function_configuration(
1✔
1539
        self,
1540
        context: RequestContext,
1541
        function_name: NamespacedFunctionName,
1542
        qualifier: Qualifier = None,
1543
        **kwargs,
1544
    ) -> FunctionConfiguration:
1545
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1546
        # CAVE: THIS RETURN VALUE IS *NOT* THE SAME AS IN get_function (!) but seems to be only configuration part?
1547
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
1548
            function_name, qualifier, context
1549
        )
1550
        version = get_function_version(
1✔
1551
            function_name=function_name,
1552
            qualifier=qualifier,
1553
            account_id=account_id,
1554
            region=region,
1555
        )
1556
        return api_utils.map_config_out(version, return_qualified_arn=bool(qualifier))
1✔
1557

1558
    def invoke(
1✔
1559
        self,
1560
        context: RequestContext,
1561
        function_name: NamespacedFunctionName,
1562
        invocation_type: InvocationType = None,
1563
        log_type: LogType = None,
1564
        client_context: String = None,
1565
        payload: IO[Blob] = None,
1566
        qualifier: Qualifier = None,
1567
        **kwargs,
1568
    ) -> InvocationResponse:
1569
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1570
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
1571
            function_name, qualifier, context
1572
        )
1573

1574
        user_agent = context.request.user_agent.string
1✔
1575

1576
        time_before = time.perf_counter()
1✔
1577
        try:
1✔
1578
            invocation_result = self.lambda_service.invoke(
1✔
1579
                function_name=function_name,
1580
                qualifier=qualifier,
1581
                region=region,
1582
                account_id=account_id,
1583
                invocation_type=invocation_type,
1584
                client_context=client_context,
1585
                request_id=context.request_id,
1586
                trace_context=context.trace_context,
1587
                payload=payload.read() if payload else None,
1588
                user_agent=user_agent,
1589
            )
1590
        except ServiceException:
1✔
1591
            raise
1✔
1592
        except EnvironmentStartupTimeoutException as e:
1✔
1593
            raise LambdaServiceException(
1✔
1594
                f"[{context.request_id}] Timeout while starting up lambda environment for function {function_name}:{qualifier}"
1595
            ) from e
1596
        except Exception as e:
1✔
1597
            LOG.error(
1✔
1598
                "[%s] Error while invoking lambda %s",
1599
                context.request_id,
1600
                function_name,
1601
                exc_info=LOG.isEnabledFor(logging.DEBUG),
1602
            )
1603
            raise LambdaServiceException(
1✔
1604
                f"[{context.request_id}] Internal error while executing lambda {function_name}:{qualifier}. Caused by {type(e).__name__}: {e}"
1605
            ) from e
1606

1607
        if invocation_type == InvocationType.Event:
1✔
1608
            # This happens when invocation type is event
1609
            return InvocationResponse(StatusCode=202)
1✔
1610
        if invocation_type == InvocationType.DryRun:
1✔
1611
            # This happens when invocation type is dryrun
1612
            return InvocationResponse(StatusCode=204)
1✔
1613
        LOG.debug("Lambda invocation duration: %0.2fms", (time.perf_counter() - time_before) * 1000)
1✔
1614

1615
        response = InvocationResponse(
1✔
1616
            StatusCode=200,
1617
            Payload=invocation_result.payload,
1618
            ExecutedVersion=invocation_result.executed_version,
1619
        )
1620

1621
        if invocation_result.is_error:
1✔
1622
            response["FunctionError"] = "Unhandled"
1✔
1623

1624
        if log_type == LogType.Tail:
1✔
1625
            response["LogResult"] = to_str(
1✔
1626
                base64.b64encode(to_bytes(invocation_result.logs)[-4096:])
1627
            )
1628

1629
        return response
1✔
1630

1631
    # Version operations
1632
    def publish_version(
1✔
1633
        self,
1634
        context: RequestContext,
1635
        function_name: FunctionName,
1636
        code_sha256: String = None,
1637
        description: Description = None,
1638
        revision_id: String = None,
1639
        **kwargs,
1640
    ) -> FunctionConfiguration:
1641
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1642
        function_name = api_utils.get_function_name(function_name, context)
1✔
1643
        new_version = self._publish_version_from_existing_version(
1✔
1644
            function_name=function_name,
1645
            description=description,
1646
            account_id=account_id,
1647
            region=region,
1648
            revision_id=revision_id,
1649
            code_sha256=code_sha256,
1650
        )
1651
        return api_utils.map_config_out(new_version, return_qualified_arn=True)
1✔
1652

1653
    def list_versions_by_function(
1✔
1654
        self,
1655
        context: RequestContext,
1656
        function_name: NamespacedFunctionName,
1657
        marker: String = None,
1658
        max_items: MaxListItems = None,
1659
        **kwargs,
1660
    ) -> ListVersionsByFunctionResponse:
1661
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1662
        function_name = api_utils.get_function_name(function_name, context)
1✔
1663
        function = self._get_function(
1✔
1664
            function_name=function_name, region=region, account_id=account_id
1665
        )
1666
        versions = [
1✔
1667
            api_utils.map_to_list_response(
1668
                api_utils.map_config_out(version=version, return_qualified_arn=True)
1669
            )
1670
            for version in function.versions.values()
1671
        ]
1672
        items = PaginatedList(versions)
1✔
1673
        page, token = items.get_page(
1✔
1674
            lambda item: item,
1675
            marker,
1676
            max_items,
1677
        )
1678
        return ListVersionsByFunctionResponse(Versions=page, NextMarker=token)
1✔
1679

1680
    # Alias
1681

1682
    def _create_routing_config_model(
1✔
1683
        self, routing_config_dict: dict[str, float], function_version: FunctionVersion
1684
    ):
1685
        if len(routing_config_dict) > 1:
1✔
1686
            raise InvalidParameterValueException(
1✔
1687
                "Number of items in AdditionalVersionWeights cannot be greater than 1",
1688
                Type="User",
1689
            )
1690
        # should be exactly one item here, still iterating, might be supported in the future
1691
        for key, value in routing_config_dict.items():
1✔
1692
            if value < 0.0 or value >= 1.0:
1✔
1693
                raise ValidationException(
1✔
1694
                    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]"
1695
                )
1696
            if key == function_version.id.qualifier:
1✔
1697
                raise InvalidParameterValueException(
1✔
1698
                    f"Invalid function version {function_version.id.qualifier}. Function version {function_version.id.qualifier} is already included in routing configuration.",
1699
                    Type="User",
1700
                )
1701
            # check if version target is latest, then no routing config is allowed
1702
            if function_version.id.qualifier == "$LATEST":
1✔
1703
                raise InvalidParameterValueException(
1✔
1704
                    "$LATEST is not supported for an alias pointing to more than 1 version"
1705
                )
1706
            if not api_utils.qualifier_is_version(key):
1✔
1707
                raise ValidationException(
1✔
1708
                    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]+, Member must not be null]"
1709
                )
1710

1711
            # checking if the version in the config exists
1712
            get_function_version(
1✔
1713
                function_name=function_version.id.function_name,
1714
                qualifier=key,
1715
                region=function_version.id.region,
1716
                account_id=function_version.id.account,
1717
            )
1718
        return AliasRoutingConfig(version_weights=routing_config_dict)
1✔
1719

1720
    def create_alias(
1✔
1721
        self,
1722
        context: RequestContext,
1723
        function_name: FunctionName,
1724
        name: Alias,
1725
        function_version: Version,
1726
        description: Description = None,
1727
        routing_config: AliasRoutingConfiguration = None,
1728
        **kwargs,
1729
    ) -> AliasConfiguration:
1730
        if not api_utils.qualifier_is_alias(name):
1✔
1731
            raise ValidationException(
1✔
1732
                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-_]+)"
1733
            )
1734

1735
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1736
        function_name = api_utils.get_function_name(function_name, context)
1✔
1737
        target_version = get_function_version(
1✔
1738
            function_name=function_name,
1739
            qualifier=function_version,
1740
            region=region,
1741
            account_id=account_id,
1742
        )
1743
        function = self._get_function(
1✔
1744
            function_name=function_name, region=region, account_id=account_id
1745
        )
1746
        # description is always present, if not specified it's an empty string
1747
        description = description or ""
1✔
1748
        with function.lock:
1✔
1749
            if existing_alias := function.aliases.get(name):
1✔
1750
                raise ResourceConflictException(
1✔
1751
                    f"Alias already exists: {api_utils.map_alias_out(alias=existing_alias, function=function)['AliasArn']}",
1752
                    Type="User",
1753
                )
1754
            # checking if the version exists
1755
            routing_configuration = None
1✔
1756
            if routing_config and (
1✔
1757
                routing_config_dict := routing_config.get("AdditionalVersionWeights")
1758
            ):
1759
                routing_configuration = self._create_routing_config_model(
1✔
1760
                    routing_config_dict, target_version
1761
                )
1762

1763
            alias = VersionAlias(
1✔
1764
                name=name,
1765
                function_version=function_version,
1766
                description=description,
1767
                routing_configuration=routing_configuration,
1768
            )
1769
            function.aliases[name] = alias
1✔
1770
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
1771

1772
    def list_aliases(
1✔
1773
        self,
1774
        context: RequestContext,
1775
        function_name: FunctionName,
1776
        function_version: Version = None,
1777
        marker: String = None,
1778
        max_items: MaxListItems = None,
1779
        **kwargs,
1780
    ) -> ListAliasesResponse:
1781
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1782
        function_name = api_utils.get_function_name(function_name, context)
1✔
1783
        function = self._get_function(
1✔
1784
            function_name=function_name, region=region, account_id=account_id
1785
        )
1786
        aliases = [
1✔
1787
            api_utils.map_alias_out(alias, function)
1788
            for alias in function.aliases.values()
1789
            if function_version is None or alias.function_version == function_version
1790
        ]
1791

1792
        aliases = PaginatedList(aliases)
1✔
1793
        page, token = aliases.get_page(
1✔
1794
            lambda alias: alias["AliasArn"],
1795
            marker,
1796
            max_items,
1797
        )
1798

1799
        return ListAliasesResponse(Aliases=page, NextMarker=token)
1✔
1800

1801
    def delete_alias(
1✔
1802
        self, context: RequestContext, function_name: FunctionName, name: Alias, **kwargs
1803
    ) -> None:
1804
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1805
        function_name = api_utils.get_function_name(function_name, context)
1✔
1806
        function = self._get_function(
1✔
1807
            function_name=function_name, region=region, account_id=account_id
1808
        )
1809
        version_alias = function.aliases.pop(name, None)
1✔
1810

1811
        # cleanup related resources
1812
        if name in function.provisioned_concurrency_configs:
1✔
1813
            function.provisioned_concurrency_configs.pop(name)
1✔
1814

1815
        # TODO: Allow for deactivating/unregistering specific Lambda URLs
1816
        if version_alias and name in function.function_url_configs:
1✔
1817
            url_config = function.function_url_configs.pop(name)
1✔
1818
            LOG.debug(
1✔
1819
                "Stopping aliased Lambda Function URL %s for %s",
1820
                url_config.url,
1821
                url_config.function_name,
1822
            )
1823

1824
    def get_alias(
1✔
1825
        self, context: RequestContext, function_name: FunctionName, name: Alias, **kwargs
1826
    ) -> AliasConfiguration:
1827
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1828
        function_name = api_utils.get_function_name(function_name, context)
1✔
1829
        function = self._get_function(
1✔
1830
            function_name=function_name, region=region, account_id=account_id
1831
        )
1832
        if not (alias := function.aliases.get(name)):
1✔
1833
            raise ResourceNotFoundException(
1✔
1834
                f"Cannot find alias arn: {api_utils.qualified_lambda_arn(function_name=function_name, qualifier=name, region=region, account=account_id)}",
1835
                Type="User",
1836
            )
1837
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
1838

1839
    def update_alias(
1✔
1840
        self,
1841
        context: RequestContext,
1842
        function_name: FunctionName,
1843
        name: Alias,
1844
        function_version: Version = None,
1845
        description: Description = None,
1846
        routing_config: AliasRoutingConfiguration = None,
1847
        revision_id: String = None,
1848
        **kwargs,
1849
    ) -> AliasConfiguration:
1850
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1851
        function_name = api_utils.get_function_name(function_name, context)
1✔
1852
        function = self._get_function(
1✔
1853
            function_name=function_name, region=region, account_id=account_id
1854
        )
1855
        if not (alias := function.aliases.get(name)):
1✔
1856
            fn_arn = api_utils.qualified_lambda_arn(function_name, name, account_id, region)
1✔
1857
            raise ResourceNotFoundException(
1✔
1858
                f"Alias not found: {fn_arn}",
1859
                Type="User",
1860
            )
1861
        if revision_id and alias.revision_id != revision_id:
1✔
1862
            raise PreconditionFailedException(
1✔
1863
                "The Revision Id provided does not match the latest Revision Id. "
1864
                "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
1865
                Type="User",
1866
            )
1867
        changes = {}
1✔
1868
        if function_version is not None:
1✔
1869
            changes |= {"function_version": function_version}
1✔
1870
        if description is not None:
1✔
1871
            changes |= {"description": description}
1✔
1872
        if routing_config is not None:
1✔
1873
            # if it is an empty dict or AdditionalVersionWeights is empty, set routing config to None
1874
            new_routing_config = None
1✔
1875
            if routing_config_dict := routing_config.get("AdditionalVersionWeights"):
1✔
1876
                new_routing_config = self._create_routing_config_model(routing_config_dict)
×
1877
            changes |= {"routing_configuration": new_routing_config}
1✔
1878
        # even if no changes are done, we have to update revision id for some reason
1879
        old_alias = alias
1✔
1880
        alias = dataclasses.replace(alias, **changes)
1✔
1881
        function.aliases[name] = alias
1✔
1882

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

1886
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
1887

1888
    # =======================================
1889
    # ======= EVENT SOURCE MAPPINGS =========
1890
    # =======================================
1891
    def check_service_resource_exists(
1✔
1892
        self, service: str, resource_arn: str, function_arn: str, function_role_arn: str
1893
    ):
1894
        """
1895
        Check if the service resource exists and if the function has access to it.
1896

1897
        Raises:
1898
            InvalidParameterValueException: If the service resource does not exist or the function does not have access to it.
1899
        """
1900
        arn = parse_arn(resource_arn)
1✔
1901
        source_client = get_internal_client(
1✔
1902
            arn=resource_arn,
1903
            role_arn=function_role_arn,
1904
            service_principal=ServicePrincipal.lambda_,
1905
            source_arn=function_arn,
1906
        )
1907
        if service in ["sqs", "sqs-fifo"]:
1✔
1908
            try:
1✔
1909
                # AWS uses `GetQueueAttributes` internally to verify the queue existence, but we need the `QueueUrl`
1910
                # which is not given directly. We build out a dummy `QueueUrl` which can be parsed by SQS to return
1911
                # the right value
1912
                queue_name = arn["resource"].split("/")[-1]
1✔
1913
                queue_url = f"http://sqs.{arn['region']}.domain/{arn['account']}/{queue_name}"
1✔
1914
                source_client.get_queue_attributes(QueueUrl=queue_url)
1✔
1915
            except ClientError as e:
1✔
1916
                error_code = e.response["Error"]["Code"]
1✔
1917
                if error_code == "AWS.SimpleQueueService.NonExistentQueue":
1✔
1918
                    raise InvalidParameterValueException(
1✔
1919
                        f"Error occurred while ReceiveMessage. SQS Error Code: {error_code}. SQS Error Message: {e.response['Error']['Message']}",
1920
                        Type="User",
1921
                    )
1922
                raise e
×
1923
        elif service in ["kinesis"]:
1✔
1924
            try:
1✔
1925
                source_client.describe_stream(StreamARN=resource_arn)
1✔
1926
            except ClientError as e:
1✔
1927
                if e.response["Error"]["Code"] == "ResourceNotFoundException":
1✔
1928
                    raise InvalidParameterValueException(
1✔
1929
                        f"Stream not found: {resource_arn}",
1930
                        Type="User",
1931
                    )
1932
                raise e
×
1933
        elif service in ["dynamodb"]:
1✔
1934
            try:
1✔
1935
                source_client.describe_stream(StreamArn=resource_arn)
1✔
1936
            except ClientError as e:
1✔
1937
                if e.response["Error"]["Code"] == "ResourceNotFoundException":
1✔
1938
                    raise InvalidParameterValueException(
1✔
1939
                        f"Stream not found: {resource_arn}",
1940
                        Type="User",
1941
                    )
1942
                raise e
×
1943

1944
    @handler("CreateEventSourceMapping", expand=False)
1✔
1945
    def create_event_source_mapping(
1✔
1946
        self,
1947
        context: RequestContext,
1948
        request: CreateEventSourceMappingRequest,
1949
    ) -> EventSourceMappingConfiguration:
1950
        return self.create_event_source_mapping_v2(context, request)
1✔
1951

1952
    def create_event_source_mapping_v2(
1✔
1953
        self,
1954
        context: RequestContext,
1955
        request: CreateEventSourceMappingRequest,
1956
    ) -> EventSourceMappingConfiguration:
1957
        # Validations
1958
        function_arn, function_name, state, function_version, function_role = (
1✔
1959
            self.validate_event_source_mapping(context, request)
1960
        )
1961

1962
        esm_config = EsmConfigFactory(request, context, function_arn).get_esm_config()
1✔
1963

1964
        # Copy esm_config to avoid a race condition with potential async update in the store
1965
        state.event_source_mappings[esm_config["UUID"]] = esm_config.copy()
1✔
1966
        enabled = request.get("Enabled", True)
1✔
1967
        # TODO: check for potential async race condition update -> think about locking
1968
        esm_worker = EsmWorkerFactory(esm_config, function_role, enabled).get_esm_worker()
1✔
1969
        self.esm_workers[esm_worker.uuid] = esm_worker
1✔
1970
        # TODO: check StateTransitionReason, LastModified, LastProcessingResult (concurrent updates requires locking!)
1971
        if tags := request.get("Tags"):
1✔
1972
            self._store_tags(esm_config.get("EventSourceMappingArn"), tags)
1✔
1973
        esm_worker.create()
1✔
1974
        return esm_config
1✔
1975

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

1982
        if destination_config := request.get("DestinationConfig"):
1✔
1983
            if "OnSuccess" in destination_config:
1✔
1984
                raise InvalidParameterValueException(
1✔
1985
                    "Unsupported DestinationConfig parameter for given event source mapping type.",
1986
                    Type="User",
1987
                )
1988

1989
        service = None
1✔
1990
        if "SelfManagedEventSource" in request:
1✔
1991
            service = "kafka"
×
1992
            if "SourceAccessConfigurations" not in request:
×
1993
                raise InvalidParameterValueException(
×
1994
                    "Required 'sourceAccessConfigurations' parameter is missing.", Type="User"
1995
                )
1996
        if service is None and "EventSourceArn" not in request:
1✔
1997
            raise InvalidParameterValueException("Unrecognized event source.", Type="User")
1✔
1998
        if service is None:
1✔
1999
            service = extract_service_from_arn(request["EventSourceArn"])
1✔
2000

2001
        batch_size = api_utils.validate_and_set_batch_size(service, request.get("BatchSize"))
1✔
2002
        if service in ["dynamodb", "kinesis"]:
1✔
2003
            starting_position = request.get("StartingPosition")
1✔
2004
            if not starting_position:
1✔
2005
                raise InvalidParameterValueException(
1✔
2006
                    "1 validation error detected: Value null at 'startingPosition' failed to satisfy constraint: Member must not be null.",
2007
                    Type="User",
2008
                )
2009

2010
            if starting_position not in KinesisStreamStartPosition.__members__:
1✔
2011
                raise ValidationException(
1✔
2012
                    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]"
2013
                )
2014
            # AT_TIMESTAMP is not allowed for DynamoDB Streams
2015
            elif (
1✔
2016
                service == "dynamodb"
2017
                and starting_position not in DynamoDBStreamStartPosition.__members__
2018
            ):
2019
                raise InvalidParameterValueException(
1✔
2020
                    f"Unsupported starting position for arn type: {request['EventSourceArn']}",
2021
                    Type="User",
2022
                )
2023

2024
        if service in ["sqs", "sqs-fifo"]:
1✔
2025
            if batch_size > 10 and request.get("MaximumBatchingWindowInSeconds", 0) == 0:
1✔
2026
                raise InvalidParameterValueException(
1✔
2027
                    "Maximum batch window in seconds must be greater than 0 if maximum batch size is greater than 10",
2028
                    Type="User",
2029
                )
2030

2031
        if (filter_criteria := request.get("FilterCriteria")) is not None:
1✔
2032
            for filter_ in filter_criteria.get("Filters", []):
1✔
2033
                pattern_str = filter_.get("Pattern")
1✔
2034
                if not pattern_str or not isinstance(pattern_str, str):
1✔
2035
                    raise InvalidParameterValueException(
×
2036
                        "Invalid filter pattern definition.", Type="User"
2037
                    )
2038

2039
                if not validate_event_pattern(pattern_str):
1✔
2040
                    raise InvalidParameterValueException(
1✔
2041
                        "Invalid filter pattern definition.", Type="User"
2042
                    )
2043

2044
        # Can either have a FunctionName (i.e CreateEventSourceMapping request) or
2045
        # an internal EventSourceMappingConfiguration representation
2046
        request_function_name = request.get("FunctionName") or request.get("FunctionArn")
1✔
2047
        # can be either a partial arn or a full arn for the version/alias
2048
        function_name, qualifier, account, region = function_locators_from_arn(
1✔
2049
            request_function_name
2050
        )
2051
        # TODO: validate `context.region` vs. `region(request["FunctionName"])` vs. `region(request["EventSourceArn"])`
2052
        account = account or context.account_id
1✔
2053
        region = region or context.region
1✔
2054
        state = lambda_stores[account][region]
1✔
2055
        fn = state.functions.get(function_name)
1✔
2056
        if not fn:
1✔
2057
            raise InvalidParameterValueException("Function does not exist", Type="User")
1✔
2058

2059
        if qualifier:
1✔
2060
            # make sure the function version/alias exists
2061
            if api_utils.qualifier_is_alias(qualifier):
1✔
2062
                fn_alias = fn.aliases.get(qualifier)
1✔
2063
                if not fn_alias:
1✔
2064
                    raise Exception("unknown alias")  # TODO: cover via test
×
2065
            elif api_utils.qualifier_is_version(qualifier):
1✔
2066
                fn_version = fn.versions.get(qualifier)
1✔
2067
                if not fn_version:
1✔
2068
                    raise Exception("unknown version")  # TODO: cover via test
×
2069
            elif qualifier == "$LATEST":
1✔
2070
                pass
1✔
2071
            else:
2072
                raise Exception("invalid functionname")  # TODO: cover via test
×
2073
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account, region)
1✔
2074

2075
        else:
2076
            fn_arn = api_utils.unqualified_lambda_arn(function_name, account, region)
1✔
2077

2078
        function_version = get_function_version_from_arn(fn_arn)
1✔
2079
        function_role = function_version.config.role
1✔
2080

2081
        if source_arn := request.get("EventSourceArn"):
1✔
2082
            self.check_service_resource_exists(service, source_arn, fn_arn, function_role)
1✔
2083
        # Check we are validating a CreateEventSourceMapping request
2084
        if is_create_esm_request:
1✔
2085

2086
            def _get_mapping_sources(mapping: dict[str, Any]) -> list[str]:
1✔
2087
                if event_source_arn := mapping.get("EventSourceArn"):
1✔
2088
                    return [event_source_arn]
1✔
2089
                return (
×
2090
                    mapping.get("SelfManagedEventSource", {})
2091
                    .get("Endpoints", {})
2092
                    .get("KAFKA_BOOTSTRAP_SERVERS", [])
2093
                )
2094

2095
            # check for event source duplicates
2096
            # TODO: currently validated for sqs, kinesis, and dynamodb
2097
            service_id = load_service(service).service_id
1✔
2098
            for uuid, mapping in state.event_source_mappings.items():
1✔
2099
                mapping_sources = _get_mapping_sources(mapping)
1✔
2100
                request_sources = _get_mapping_sources(request)
1✔
2101
                if mapping["FunctionArn"] == fn_arn and (
1✔
2102
                    set(mapping_sources).intersection(request_sources)
2103
                ):
2104
                    if service == "sqs":
1✔
2105
                        # *shakes fist at SQS*
2106
                        raise ResourceConflictException(
1✔
2107
                            f'An event source mapping with {service_id} arn (" {mapping["EventSourceArn"]} ") '
2108
                            f'and function (" {function_name} ") already exists. Please update or delete the '
2109
                            f"existing mapping with UUID {uuid}",
2110
                            Type="User",
2111
                        )
2112
                    elif service == "kafka":
1✔
2113
                        if set(mapping["Topics"]).intersection(request["Topics"]):
×
2114
                            raise ResourceConflictException(
×
2115
                                f'An event source mapping with event source ("{",".join(request_sources)}"), '
2116
                                f'function ("{fn_arn}"), '
2117
                                f'topics ("{",".join(request["Topics"])}") already exists. Please update or delete the '
2118
                                f"existing mapping with UUID {uuid}",
2119
                                Type="User",
2120
                            )
2121
                    else:
2122
                        raise ResourceConflictException(
1✔
2123
                            f'The event source arn (" {mapping["EventSourceArn"]} ") and function '
2124
                            f'(" {function_name} ") provided mapping already exists. Please update or delete the '
2125
                            f"existing mapping with UUID {uuid}",
2126
                            Type="User",
2127
                        )
2128
        return fn_arn, function_name, state, function_version, function_role
1✔
2129

2130
    @handler("UpdateEventSourceMapping", expand=False)
1✔
2131
    def update_event_source_mapping(
1✔
2132
        self,
2133
        context: RequestContext,
2134
        request: UpdateEventSourceMappingRequest,
2135
    ) -> EventSourceMappingConfiguration:
2136
        return self.update_event_source_mapping_v2(context, request)
1✔
2137

2138
    def update_event_source_mapping_v2(
1✔
2139
        self,
2140
        context: RequestContext,
2141
        request: UpdateEventSourceMappingRequest,
2142
    ) -> EventSourceMappingConfiguration:
2143
        # TODO: test and implement this properly (quite complex with many validations and limitations!)
2144
        LOG.warning(
1✔
2145
            "Updating Lambda Event Source Mapping is in experimental state and not yet fully tested."
2146
        )
2147
        state = lambda_stores[context.account_id][context.region]
1✔
2148
        request_data = {**request}
1✔
2149
        uuid = request_data.pop("UUID", None)
1✔
2150
        if not uuid:
1✔
2151
            raise ResourceNotFoundException(
×
2152
                "The resource you requested does not exist.", Type="User"
2153
            )
2154
        old_event_source_mapping = state.event_source_mappings.get(uuid)
1✔
2155
        esm_worker = self.esm_workers.get(uuid)
1✔
2156
        if old_event_source_mapping is None or esm_worker is None:
1✔
2157
            raise ResourceNotFoundException(
1✔
2158
                "The resource you requested does not exist.", Type="User"
2159
            )  # TODO: test?
2160

2161
        # normalize values to overwrite
2162
        event_source_mapping = old_event_source_mapping | request_data
1✔
2163

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

2166
        # Validate the newly updated ESM object. We ignore the output here since we only care whether an Exception is raised.
2167
        function_arn, _, _, function_version, function_role = self.validate_event_source_mapping(
1✔
2168
            context, event_source_mapping
2169
        )
2170

2171
        # remove the FunctionName field
2172
        event_source_mapping.pop("FunctionName", None)
1✔
2173

2174
        if function_arn:
1✔
2175
            event_source_mapping["FunctionArn"] = function_arn
1✔
2176

2177
        # Only apply update if the desired state differs
2178
        enabled = request.get("Enabled")
1✔
2179
        if enabled is not None:
1✔
2180
            if enabled and old_event_source_mapping["State"] != EsmState.ENABLED:
1✔
2181
                event_source_mapping["State"] = EsmState.ENABLING
1✔
2182
            # TODO: What happens when trying to update during an update or failed state?!
2183
            elif not enabled and old_event_source_mapping["State"] == EsmState.ENABLED:
1✔
2184
                event_source_mapping["State"] = EsmState.DISABLING
1✔
2185
        else:
2186
            event_source_mapping["State"] = EsmState.UPDATING
1✔
2187

2188
        # To ensure parity, certain responses need to be immediately returned
2189
        temp_params["State"] = event_source_mapping["State"]
1✔
2190

2191
        state.event_source_mappings[uuid] = event_source_mapping
1✔
2192

2193
        # TODO: Currently, we re-create the entire ESM worker. Look into approach with better performance.
2194
        worker_factory = EsmWorkerFactory(
1✔
2195
            event_source_mapping, function_role, request.get("Enabled", esm_worker.enabled)
2196
        )
2197

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

2202
        # We should stop() the worker since the delete() will remove the ESM from the state mapping.
2203
        esm_worker.stop()
1✔
2204
        # This will either create an EsmWorker in the CREATING state if enabled. Otherwise, the DISABLING state is set.
2205
        updated_esm_worker.create()
1✔
2206

2207
        return {**event_source_mapping, **temp_params}
1✔
2208

2209
    def delete_event_source_mapping(
1✔
2210
        self, context: RequestContext, uuid: String, **kwargs
2211
    ) -> EventSourceMappingConfiguration:
2212
        state = lambda_stores[context.account_id][context.region]
1✔
2213
        event_source_mapping = state.event_source_mappings.get(uuid)
1✔
2214
        if not event_source_mapping:
1✔
2215
            raise ResourceNotFoundException(
1✔
2216
                "The resource you requested does not exist.", Type="User"
2217
            )
2218
        esm = state.event_source_mappings[uuid]
1✔
2219
        # TODO: add proper locking
2220
        esm_worker = self.esm_workers.pop(uuid, None)
1✔
2221
        # Asynchronous delete in v2
2222
        if not esm_worker:
1✔
2223
            raise ResourceNotFoundException(
×
2224
                "The resource you requested does not exist.", Type="User"
2225
            )
2226
        esm_worker.delete()
1✔
2227
        return {**esm, "State": EsmState.DELETING}
1✔
2228

2229
    def get_event_source_mapping(
1✔
2230
        self, context: RequestContext, uuid: String, **kwargs
2231
    ) -> EventSourceMappingConfiguration:
2232
        state = lambda_stores[context.account_id][context.region]
1✔
2233
        event_source_mapping = state.event_source_mappings.get(uuid)
1✔
2234
        if not event_source_mapping:
1✔
2235
            raise ResourceNotFoundException(
1✔
2236
                "The resource you requested does not exist.", Type="User"
2237
            )
2238
        esm_worker = self.esm_workers.get(uuid)
1✔
2239
        if not esm_worker:
1✔
UNCOV
2240
            raise ResourceNotFoundException(
×
2241
                "The resource you requested does not exist.", Type="User"
2242
            )
2243
        event_source_mapping["State"] = esm_worker.current_state
1✔
2244
        event_source_mapping["StateTransitionReason"] = esm_worker.state_transition_reason
1✔
2245
        return event_source_mapping
1✔
2246

2247
    def list_event_source_mappings(
1✔
2248
        self,
2249
        context: RequestContext,
2250
        event_source_arn: Arn = None,
2251
        function_name: FunctionName = None,
2252
        marker: String = None,
2253
        max_items: MaxListItems = None,
2254
        **kwargs,
2255
    ) -> ListEventSourceMappingsResponse:
2256
        state = lambda_stores[context.account_id][context.region]
1✔
2257

2258
        esms = state.event_source_mappings.values()
1✔
2259
        # TODO: update and test State and StateTransitionReason for ESM v2
2260

2261
        if event_source_arn:  # TODO: validate pattern
1✔
2262
            esms = [e for e in esms if e.get("EventSourceArn") == event_source_arn]
1✔
2263

2264
        if function_name:
1✔
2265
            esms = [e for e in esms if function_name in e["FunctionArn"]]
1✔
2266

2267
        esms = PaginatedList(esms)
1✔
2268
        page, token = esms.get_page(
1✔
2269
            lambda x: x["UUID"],
2270
            marker,
2271
            max_items,
2272
        )
2273
        return ListEventSourceMappingsResponse(EventSourceMappings=page, NextMarker=token)
1✔
2274

2275
    def get_source_type_from_request(self, request: dict[str, Any]) -> str:
1✔
2276
        if event_source_arn := request.get("EventSourceArn", ""):
×
2277
            service = extract_service_from_arn(event_source_arn)
×
2278
            if service == "sqs" and "fifo" in event_source_arn:
×
2279
                service = "sqs-fifo"
×
2280
            return service
×
2281
        elif request.get("SelfManagedEventSource"):
×
2282
            return "kafka"
×
2283

2284
    # =======================================
2285
    # ============ FUNCTION URLS ============
2286
    # =======================================
2287

2288
    @staticmethod
1✔
2289
    def _validate_qualifier(qualifier: str) -> None:
1✔
2290
        if qualifier == "$LATEST" or (qualifier and api_utils.qualifier_is_version(qualifier)):
1✔
2291
            raise ValidationException(
1✔
2292
                f"1 validation error detected: Value '{qualifier}' at 'qualifier' failed to satisfy constraint: Member must satisfy regular expression pattern: ((?!^\\d+$)^[0-9a-zA-Z-_]+$)"
2293
            )
2294

2295
    @staticmethod
1✔
2296
    def _validate_invoke_mode(invoke_mode: str) -> None:
1✔
2297
        if invoke_mode and invoke_mode not in [InvokeMode.BUFFERED, InvokeMode.RESPONSE_STREAM]:
1✔
2298
            raise ValidationException(
1✔
2299
                f"1 validation error detected: Value '{invoke_mode}' at 'invokeMode' failed to satisfy constraint: Member must satisfy enum value set: [RESPONSE_STREAM, BUFFERED]"
2300
            )
2301
        if invoke_mode == InvokeMode.RESPONSE_STREAM:
1✔
2302
            # TODO should we actually fail for setting RESPONSE_STREAM?
2303
            #  It should trigger InvokeWithResponseStream which is not implemented
2304
            LOG.warning(
1✔
2305
                "The invokeMode 'RESPONSE_STREAM' is not yet supported on LocalStack. The property is only mocked, the execution will still be 'BUFFERED'"
2306
            )
2307

2308
    # TODO: what happens if function state is not active?
2309
    def create_function_url_config(
1✔
2310
        self,
2311
        context: RequestContext,
2312
        function_name: FunctionName,
2313
        auth_type: FunctionUrlAuthType,
2314
        qualifier: FunctionUrlQualifier = None,
2315
        cors: Cors = None,
2316
        invoke_mode: InvokeMode = None,
2317
        **kwargs,
2318
    ) -> CreateFunctionUrlConfigResponse:
2319
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2320
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2321
            function_name, qualifier, context
2322
        )
2323
        state = lambda_stores[account_id][region]
1✔
2324
        self._validate_qualifier(qualifier)
1✔
2325
        self._validate_invoke_mode(invoke_mode)
1✔
2326

2327
        fn = state.functions.get(function_name)
1✔
2328
        if fn is None:
1✔
2329
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2330

2331
        url_config = fn.function_url_configs.get(qualifier or "$LATEST")
1✔
2332
        if url_config:
1✔
2333
            raise ResourceConflictException(
1✔
2334
                f"Failed to create function url config for [functionArn = {url_config.function_arn}]. Error message:  FunctionUrlConfig exists for this Lambda function",
2335
                Type="User",
2336
            )
2337

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

2341
        normalized_qualifier = qualifier or "$LATEST"
1✔
2342

2343
        function_arn = (
1✔
2344
            api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
2345
            if qualifier
2346
            else api_utils.unqualified_lambda_arn(function_name, account_id, region)
2347
        )
2348

2349
        custom_id: str | None = None
1✔
2350

2351
        tags = self._get_tags(api_utils.unqualified_lambda_arn(function_name, account_id, region))
1✔
2352
        if TAG_KEY_CUSTOM_URL in tags:
1✔
2353
            # Note: I really wanted to add verification here that the
2354
            # url_id is unique, so we could surface that to the user ASAP.
2355
            # However, it seems like that information isn't available yet,
2356
            # since (as far as I can tell) we call
2357
            # self.router.register_routes() once, in a single shot, for all
2358
            # of the routes -- and we need to verify that it's unique not
2359
            # just for this particular lambda function, but for the entire
2360
            # lambda provider. Therefore... that idea proved non-trivial!
2361
            custom_id_tag_value = (
1✔
2362
                f"{tags[TAG_KEY_CUSTOM_URL]}-{qualifier}" if qualifier else tags[TAG_KEY_CUSTOM_URL]
2363
            )
2364
            if TAG_KEY_CUSTOM_URL_VALIDATOR.match(custom_id_tag_value):
1✔
2365
                custom_id = custom_id_tag_value
1✔
2366

2367
            else:
2368
                # Note: we're logging here instead of raising to prioritize
2369
                # strict parity with AWS over the localstack-only custom_id
2370
                LOG.warning(
1✔
2371
                    "Invalid custom ID tag value for lambda URL (%s=%s). "
2372
                    "Replaced with default (random id)",
2373
                    TAG_KEY_CUSTOM_URL,
2374
                    custom_id_tag_value,
2375
                )
2376

2377
        # The url_id is the subdomain used for the URL we're creating. This
2378
        # is either created randomly (as in AWS), or can be passed as a tag
2379
        # to the lambda itself (localstack-only).
2380
        url_id: str
2381
        if custom_id is None:
1✔
2382
            url_id = api_utils.generate_random_url_id()
1✔
2383
        else:
2384
            url_id = custom_id
1✔
2385

2386
        host_definition = localstack_host(custom_port=config.GATEWAY_LISTEN[0].port)
1✔
2387
        fn.function_url_configs[normalized_qualifier] = FunctionUrlConfig(
1✔
2388
            function_arn=function_arn,
2389
            function_name=function_name,
2390
            cors=cors,
2391
            url_id=url_id,
2392
            url=f"http://{url_id}.lambda-url.{context.region}.{host_definition.host_and_port()}/",  # TODO: https support
2393
            auth_type=auth_type,
2394
            creation_time=api_utils.generate_lambda_date(),
2395
            last_modified_time=api_utils.generate_lambda_date(),
2396
            invoke_mode=invoke_mode,
2397
        )
2398

2399
        # persist and start URL
2400
        # TODO: implement URL invoke
2401
        api_url_config = api_utils.map_function_url_config(
1✔
2402
            fn.function_url_configs[normalized_qualifier]
2403
        )
2404

2405
        return CreateFunctionUrlConfigResponse(
1✔
2406
            FunctionUrl=api_url_config["FunctionUrl"],
2407
            FunctionArn=api_url_config["FunctionArn"],
2408
            AuthType=api_url_config["AuthType"],
2409
            Cors=api_url_config["Cors"],
2410
            CreationTime=api_url_config["CreationTime"],
2411
            InvokeMode=api_url_config["InvokeMode"],
2412
        )
2413

2414
    def get_function_url_config(
1✔
2415
        self,
2416
        context: RequestContext,
2417
        function_name: FunctionName,
2418
        qualifier: FunctionUrlQualifier = None,
2419
        **kwargs,
2420
    ) -> GetFunctionUrlConfigResponse:
2421
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2422
        state = lambda_stores[account_id][region]
1✔
2423

2424
        fn_name, qualifier = api_utils.get_name_and_qualifier(function_name, qualifier, context)
1✔
2425

2426
        self._validate_qualifier(qualifier)
1✔
2427

2428
        resolved_fn = state.functions.get(fn_name)
1✔
2429
        if not resolved_fn:
1✔
2430
            raise ResourceNotFoundException(
1✔
2431
                "The resource you requested does not exist.", Type="User"
2432
            )
2433

2434
        qualifier = qualifier or "$LATEST"
1✔
2435
        url_config = resolved_fn.function_url_configs.get(qualifier)
1✔
2436
        if not url_config:
1✔
2437
            raise ResourceNotFoundException(
1✔
2438
                "The resource you requested does not exist.", Type="User"
2439
            )
2440

2441
        return api_utils.map_function_url_config(url_config)
1✔
2442

2443
    def update_function_url_config(
1✔
2444
        self,
2445
        context: RequestContext,
2446
        function_name: FunctionName,
2447
        qualifier: FunctionUrlQualifier = None,
2448
        auth_type: FunctionUrlAuthType = None,
2449
        cors: Cors = None,
2450
        invoke_mode: InvokeMode = None,
2451
        **kwargs,
2452
    ) -> UpdateFunctionUrlConfigResponse:
2453
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2454
        state = lambda_stores[account_id][region]
1✔
2455

2456
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2457
            function_name, qualifier, context
2458
        )
2459
        self._validate_qualifier(qualifier)
1✔
2460
        self._validate_invoke_mode(invoke_mode)
1✔
2461

2462
        fn = state.functions.get(function_name)
1✔
2463
        if not fn:
1✔
2464
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2465

2466
        normalized_qualifier = qualifier or "$LATEST"
1✔
2467

2468
        if (
1✔
2469
            api_utils.qualifier_is_alias(normalized_qualifier)
2470
            and normalized_qualifier not in fn.aliases
2471
        ):
2472
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2473

2474
        url_config = fn.function_url_configs.get(normalized_qualifier)
1✔
2475
        if not url_config:
1✔
2476
            raise ResourceNotFoundException(
1✔
2477
                "The resource you requested does not exist.", Type="User"
2478
            )
2479

2480
        changes = {
1✔
2481
            "last_modified_time": api_utils.generate_lambda_date(),
2482
            **({"cors": cors} if cors is not None else {}),
2483
            **({"auth_type": auth_type} if auth_type is not None else {}),
2484
        }
2485

2486
        if invoke_mode:
1✔
2487
            changes["invoke_mode"] = invoke_mode
1✔
2488

2489
        new_url_config = dataclasses.replace(url_config, **changes)
1✔
2490
        fn.function_url_configs[normalized_qualifier] = new_url_config
1✔
2491

2492
        return UpdateFunctionUrlConfigResponse(
1✔
2493
            FunctionUrl=new_url_config.url,
2494
            FunctionArn=new_url_config.function_arn,
2495
            AuthType=new_url_config.auth_type,
2496
            Cors=new_url_config.cors,
2497
            CreationTime=new_url_config.creation_time,
2498
            LastModifiedTime=new_url_config.last_modified_time,
2499
            InvokeMode=new_url_config.invoke_mode,
2500
        )
2501

2502
    def delete_function_url_config(
1✔
2503
        self,
2504
        context: RequestContext,
2505
        function_name: FunctionName,
2506
        qualifier: FunctionUrlQualifier = None,
2507
        **kwargs,
2508
    ) -> None:
2509
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2510
        state = lambda_stores[account_id][region]
1✔
2511

2512
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2513
            function_name, qualifier, context
2514
        )
2515
        self._validate_qualifier(qualifier)
1✔
2516

2517
        resolved_fn = state.functions.get(function_name)
1✔
2518
        if not resolved_fn:
1✔
2519
            raise ResourceNotFoundException(
1✔
2520
                "The resource you requested does not exist.", Type="User"
2521
            )
2522

2523
        qualifier = qualifier or "$LATEST"
1✔
2524
        url_config = resolved_fn.function_url_configs.get(qualifier)
1✔
2525
        if not url_config:
1✔
2526
            raise ResourceNotFoundException(
1✔
2527
                "The resource you requested does not exist.", Type="User"
2528
            )
2529

2530
        del resolved_fn.function_url_configs[qualifier]
1✔
2531

2532
    def list_function_url_configs(
1✔
2533
        self,
2534
        context: RequestContext,
2535
        function_name: FunctionName,
2536
        marker: String = None,
2537
        max_items: MaxItems = None,
2538
        **kwargs,
2539
    ) -> ListFunctionUrlConfigsResponse:
2540
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2541
        state = lambda_stores[account_id][region]
1✔
2542

2543
        fn_name = api_utils.get_function_name(function_name, context)
1✔
2544
        resolved_fn = state.functions.get(fn_name)
1✔
2545
        if not resolved_fn:
1✔
2546
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2547

2548
        url_configs = [
1✔
2549
            api_utils.map_function_url_config(fn_conf)
2550
            for fn_conf in resolved_fn.function_url_configs.values()
2551
        ]
2552
        url_configs = PaginatedList(url_configs)
1✔
2553
        page, token = url_configs.get_page(
1✔
2554
            lambda url_config: url_config["FunctionArn"],
2555
            marker,
2556
            max_items,
2557
        )
2558
        url_configs = page
1✔
2559
        return ListFunctionUrlConfigsResponse(FunctionUrlConfigs=url_configs, NextMarker=token)
1✔
2560

2561
    # =======================================
2562
    # ============  Permissions  ============
2563
    # =======================================
2564

2565
    @handler("AddPermission", expand=False)
1✔
2566
    def add_permission(
1✔
2567
        self,
2568
        context: RequestContext,
2569
        request: AddPermissionRequest,
2570
    ) -> AddPermissionResponse:
2571
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2572
            request.get("FunctionName"), request.get("Qualifier"), context
2573
        )
2574

2575
        # validate qualifier
2576
        if qualifier is not None:
1✔
2577
            self._validate_qualifier_expression(qualifier)
1✔
2578
            if qualifier == "$LATEST":
1✔
2579
                raise InvalidParameterValueException(
1✔
2580
                    "We currently do not support adding policies for $LATEST.", Type="User"
2581
                )
2582
        account_id, region = api_utils.get_account_and_region(request.get("FunctionName"), context)
1✔
2583

2584
        resolved_fn = self._get_function(function_name, account_id, region)
1✔
2585
        resolved_qualifier, fn_arn = self._resolve_fn_qualifier(resolved_fn, qualifier)
1✔
2586

2587
        revision_id = request.get("RevisionId")
1✔
2588
        if revision_id:
1✔
2589
            fn_revision_id = self._function_revision_id(resolved_fn, resolved_qualifier)
1✔
2590
            if revision_id != fn_revision_id:
1✔
2591
                raise PreconditionFailedException(
1✔
2592
                    "The Revision Id provided does not match the latest Revision Id. "
2593
                    "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
2594
                    Type="User",
2595
                )
2596

2597
        request_sid = request["StatementId"]
1✔
2598
        if not bool(STATEMENT_ID_REGEX.match(request_sid)):
1✔
2599
            raise ValidationException(
1✔
2600
                f"1 validation error detected: Value '{request_sid}' at 'statementId' failed to satisfy constraint: Member must satisfy regular expression pattern: ([a-zA-Z0-9-_]+)"
2601
            )
2602
        # check for an already existing policy and any conflicts in existing statements
2603
        existing_policy = resolved_fn.permissions.get(resolved_qualifier)
1✔
2604
        if existing_policy:
1✔
2605
            if request_sid in [s["Sid"] for s in existing_policy.policy.Statement]:
1✔
2606
                # uniqueness scope: statement id needs to be unique per qualified function ($LATEST, version, or alias)
2607
                # Counterexample: the same sid can exist within $LATEST, version, and alias
2608
                raise ResourceConflictException(
1✔
2609
                    f"The statement id ({request_sid}) provided already exists. Please provide a new statement id, or remove the existing statement.",
2610
                    Type="User",
2611
                )
2612

2613
        permission_statement = api_utils.build_statement(
1✔
2614
            partition=context.partition,
2615
            resource_arn=fn_arn,
2616
            statement_id=request["StatementId"],
2617
            action=request["Action"],
2618
            principal=request["Principal"],
2619
            source_arn=request.get("SourceArn"),
2620
            source_account=request.get("SourceAccount"),
2621
            principal_org_id=request.get("PrincipalOrgID"),
2622
            event_source_token=request.get("EventSourceToken"),
2623
            auth_type=request.get("FunctionUrlAuthType"),
2624
        )
2625
        new_policy = existing_policy
1✔
2626
        if not existing_policy:
1✔
2627
            new_policy = FunctionResourcePolicy(
1✔
2628
                policy=ResourcePolicy(Version="2012-10-17", Id="default", Statement=[])
2629
            )
2630
        new_policy.policy.Statement.append(permission_statement)
1✔
2631
        if not existing_policy:
1✔
2632
            resolved_fn.permissions[resolved_qualifier] = new_policy
1✔
2633

2634
        # Update revision id of alias or version
2635
        # TODO: re-evaluate data model to prevent this dirty hack just for bumping the revision id
2636
        # TODO: does that need a `with function.lock` for atomic updates of the policy + revision_id?
2637
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
2638
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
1✔
2639
            resolved_fn.aliases[resolved_qualifier] = dataclasses.replace(resolved_alias)
1✔
2640
        # Assumes that a non-alias is a version
2641
        else:
2642
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
2643
            resolved_fn.versions[resolved_qualifier] = dataclasses.replace(
1✔
2644
                resolved_version, config=dataclasses.replace(resolved_version.config)
2645
            )
2646
        return AddPermissionResponse(Statement=json.dumps(permission_statement))
1✔
2647

2648
    def remove_permission(
1✔
2649
        self,
2650
        context: RequestContext,
2651
        function_name: FunctionName,
2652
        statement_id: NamespacedStatementId,
2653
        qualifier: Qualifier = None,
2654
        revision_id: String = None,
2655
        **kwargs,
2656
    ) -> None:
2657
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2658
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2659
            function_name, qualifier, context
2660
        )
2661
        if qualifier is not None:
1✔
2662
            self._validate_qualifier_expression(qualifier)
1✔
2663

2664
        state = lambda_stores[account_id][region]
1✔
2665
        resolved_fn = state.functions.get(function_name)
1✔
2666
        if resolved_fn is None:
1✔
2667
            fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
2668
            raise ResourceNotFoundException(f"No policy found for: {fn_arn}", Type="User")
1✔
2669

2670
        resolved_qualifier, _ = self._resolve_fn_qualifier(resolved_fn, qualifier)
1✔
2671
        function_permission = resolved_fn.permissions.get(resolved_qualifier)
1✔
2672
        if not function_permission:
1✔
2673
            raise ResourceNotFoundException(
1✔
2674
                "No policy is associated with the given resource.", Type="User"
2675
            )
2676

2677
        # try to find statement in policy and delete it
2678
        statement = None
1✔
2679
        for s in function_permission.policy.Statement:
1✔
2680
            if s["Sid"] == statement_id:
1✔
2681
                statement = s
1✔
2682
                break
1✔
2683

2684
        if not statement:
1✔
2685
            raise ResourceNotFoundException(
1✔
2686
                f"Statement {statement_id} is not found in resource policy.", Type="User"
2687
            )
2688
        fn_revision_id = self._function_revision_id(resolved_fn, resolved_qualifier)
1✔
2689
        if revision_id and revision_id != fn_revision_id:
1✔
2690
            raise PreconditionFailedException(
1✔
2691
                "The Revision Id provided does not match the latest Revision Id. "
2692
                "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
2693
                Type="User",
2694
            )
2695
        function_permission.policy.Statement.remove(statement)
1✔
2696

2697
        # Update revision id for alias or version
2698
        # TODO: re-evaluate data model to prevent this dirty hack just for bumping the revision id
2699
        # TODO: does that need a `with function.lock` for atomic updates of the policy + revision_id?
2700
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
2701
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
×
2702
            resolved_fn.aliases[resolved_qualifier] = dataclasses.replace(resolved_alias)
×
2703
        # Assumes that a non-alias is a version
2704
        else:
2705
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
2706
            resolved_fn.versions[resolved_qualifier] = dataclasses.replace(
1✔
2707
                resolved_version, config=dataclasses.replace(resolved_version.config)
2708
            )
2709

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

2714
    def get_policy(
1✔
2715
        self,
2716
        context: RequestContext,
2717
        function_name: NamespacedFunctionName,
2718
        qualifier: Qualifier = None,
2719
        **kwargs,
2720
    ) -> GetPolicyResponse:
2721
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2722
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2723
            function_name, qualifier, context
2724
        )
2725

2726
        if qualifier is not None:
1✔
2727
            self._validate_qualifier_expression(qualifier)
1✔
2728

2729
        resolved_fn = self._get_function(function_name, account_id, region)
1✔
2730

2731
        resolved_qualifier = qualifier or "$LATEST"
1✔
2732
        function_permission = resolved_fn.permissions.get(resolved_qualifier)
1✔
2733
        if not function_permission:
1✔
2734
            raise ResourceNotFoundException(
1✔
2735
                "The resource you requested does not exist.", Type="User"
2736
            )
2737

2738
        fn_revision_id = None
1✔
2739
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
2740
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
1✔
2741
            fn_revision_id = resolved_alias.revision_id
1✔
2742
        # Assumes that a non-alias is a version
2743
        else:
2744
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
2745
            fn_revision_id = resolved_version.config.revision_id
1✔
2746

2747
        return GetPolicyResponse(
1✔
2748
            Policy=json.dumps(dataclasses.asdict(function_permission.policy)),
2749
            RevisionId=fn_revision_id,
2750
        )
2751

2752
    # =======================================
2753
    # ========  Code signing config  ========
2754
    # =======================================
2755

2756
    def create_code_signing_config(
1✔
2757
        self,
2758
        context: RequestContext,
2759
        allowed_publishers: AllowedPublishers,
2760
        description: Description = None,
2761
        code_signing_policies: CodeSigningPolicies = None,
2762
        tags: Tags = None,
2763
        **kwargs,
2764
    ) -> CreateCodeSigningConfigResponse:
2765
        account = context.account_id
1✔
2766
        region = context.region
1✔
2767

2768
        state = lambda_stores[account][region]
1✔
2769
        # TODO: can there be duplicates?
2770
        csc_id = f"csc-{get_random_hex(17)}"  # e.g. 'csc-077c33b4c19e26036'
1✔
2771
        csc_arn = f"arn:{context.partition}:lambda:{region}:{account}:code-signing-config:{csc_id}"
1✔
2772
        csc = CodeSigningConfig(
1✔
2773
            csc_id=csc_id,
2774
            arn=csc_arn,
2775
            allowed_publishers=allowed_publishers,
2776
            policies=code_signing_policies,
2777
            last_modified=api_utils.generate_lambda_date(),
2778
            description=description,
2779
        )
2780
        state.code_signing_configs[csc_arn] = csc
1✔
2781
        return CreateCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(csc))
1✔
2782

2783
    def put_function_code_signing_config(
1✔
2784
        self,
2785
        context: RequestContext,
2786
        code_signing_config_arn: CodeSigningConfigArn,
2787
        function_name: FunctionName,
2788
        **kwargs,
2789
    ) -> PutFunctionCodeSigningConfigResponse:
2790
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2791
        state = lambda_stores[account_id][region]
1✔
2792
        function_name = api_utils.get_function_name(function_name, context)
1✔
2793

2794
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
2795
        if not csc:
1✔
2796
            raise CodeSigningConfigNotFoundException(
1✔
2797
                f"The code signing configuration cannot be found. Check that the provided configuration is not deleted: {code_signing_config_arn}.",
2798
                Type="User",
2799
            )
2800

2801
        fn = state.functions.get(function_name)
1✔
2802
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
2803
        if not fn:
1✔
2804
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
2805

2806
        fn.code_signing_config_arn = code_signing_config_arn
1✔
2807
        return PutFunctionCodeSigningConfigResponse(
1✔
2808
            CodeSigningConfigArn=code_signing_config_arn, FunctionName=function_name
2809
        )
2810

2811
    def update_code_signing_config(
1✔
2812
        self,
2813
        context: RequestContext,
2814
        code_signing_config_arn: CodeSigningConfigArn,
2815
        description: Description = None,
2816
        allowed_publishers: AllowedPublishers = None,
2817
        code_signing_policies: CodeSigningPolicies = None,
2818
        **kwargs,
2819
    ) -> UpdateCodeSigningConfigResponse:
2820
        state = lambda_stores[context.account_id][context.region]
1✔
2821
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
2822
        if not csc:
1✔
2823
            raise ResourceNotFoundException(
1✔
2824
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
2825
            )
2826

2827
        changes = {
1✔
2828
            **(
2829
                {"allowed_publishers": allowed_publishers} if allowed_publishers is not None else {}
2830
            ),
2831
            **({"policies": code_signing_policies} if code_signing_policies is not None else {}),
2832
            **({"description": description} if description is not None else {}),
2833
        }
2834
        new_csc = dataclasses.replace(
1✔
2835
            csc, last_modified=api_utils.generate_lambda_date(), **changes
2836
        )
2837
        state.code_signing_configs[code_signing_config_arn] = new_csc
1✔
2838

2839
        return UpdateCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(new_csc))
1✔
2840

2841
    def get_code_signing_config(
1✔
2842
        self, context: RequestContext, code_signing_config_arn: CodeSigningConfigArn, **kwargs
2843
    ) -> GetCodeSigningConfigResponse:
2844
        state = lambda_stores[context.account_id][context.region]
1✔
2845
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
2846
        if not csc:
1✔
2847
            raise ResourceNotFoundException(
1✔
2848
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
2849
            )
2850

2851
        return GetCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(csc))
1✔
2852

2853
    def get_function_code_signing_config(
1✔
2854
        self, context: RequestContext, function_name: FunctionName, **kwargs
2855
    ) -> GetFunctionCodeSigningConfigResponse:
2856
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2857
        state = lambda_stores[account_id][region]
1✔
2858
        function_name = api_utils.get_function_name(function_name, context)
1✔
2859
        fn = state.functions.get(function_name)
1✔
2860
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
2861
        if not fn:
1✔
2862
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
2863

2864
        if fn.code_signing_config_arn:
1✔
2865
            return GetFunctionCodeSigningConfigResponse(
1✔
2866
                CodeSigningConfigArn=fn.code_signing_config_arn, FunctionName=function_name
2867
            )
2868

2869
        return GetFunctionCodeSigningConfigResponse()
1✔
2870

2871
    def delete_function_code_signing_config(
1✔
2872
        self, context: RequestContext, function_name: FunctionName, **kwargs
2873
    ) -> None:
2874
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2875
        state = lambda_stores[account_id][region]
1✔
2876
        function_name = api_utils.get_function_name(function_name, context)
1✔
2877
        fn = state.functions.get(function_name)
1✔
2878
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
2879
        if not fn:
1✔
2880
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
2881

2882
        fn.code_signing_config_arn = None
1✔
2883

2884
    def delete_code_signing_config(
1✔
2885
        self, context: RequestContext, code_signing_config_arn: CodeSigningConfigArn, **kwargs
2886
    ) -> DeleteCodeSigningConfigResponse:
2887
        state = lambda_stores[context.account_id][context.region]
1✔
2888

2889
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
2890
        if not csc:
1✔
2891
            raise ResourceNotFoundException(
1✔
2892
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
2893
            )
2894

2895
        del state.code_signing_configs[code_signing_config_arn]
1✔
2896

2897
        return DeleteCodeSigningConfigResponse()
1✔
2898

2899
    def list_code_signing_configs(
1✔
2900
        self,
2901
        context: RequestContext,
2902
        marker: String = None,
2903
        max_items: MaxListItems = None,
2904
        **kwargs,
2905
    ) -> ListCodeSigningConfigsResponse:
2906
        state = lambda_stores[context.account_id][context.region]
1✔
2907

2908
        cscs = [api_utils.map_csc(csc) for csc in state.code_signing_configs.values()]
1✔
2909
        cscs = PaginatedList(cscs)
1✔
2910
        page, token = cscs.get_page(
1✔
2911
            lambda csc: csc["CodeSigningConfigId"],
2912
            marker,
2913
            max_items,
2914
        )
2915
        return ListCodeSigningConfigsResponse(CodeSigningConfigs=page, NextMarker=token)
1✔
2916

2917
    def list_functions_by_code_signing_config(
1✔
2918
        self,
2919
        context: RequestContext,
2920
        code_signing_config_arn: CodeSigningConfigArn,
2921
        marker: String = None,
2922
        max_items: MaxListItems = None,
2923
        **kwargs,
2924
    ) -> ListFunctionsByCodeSigningConfigResponse:
2925
        account = context.account_id
1✔
2926
        region = context.region
1✔
2927

2928
        state = lambda_stores[account][region]
1✔
2929

2930
        if code_signing_config_arn not in state.code_signing_configs:
1✔
2931
            raise ResourceNotFoundException(
1✔
2932
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
2933
            )
2934

2935
        fn_arns = [
1✔
2936
            api_utils.unqualified_lambda_arn(fn.function_name, account, region)
2937
            for fn in state.functions.values()
2938
            if fn.code_signing_config_arn == code_signing_config_arn
2939
        ]
2940

2941
        cscs = PaginatedList(fn_arns)
1✔
2942
        page, token = cscs.get_page(
1✔
2943
            lambda x: x,
2944
            marker,
2945
            max_items,
2946
        )
2947
        return ListFunctionsByCodeSigningConfigResponse(FunctionArns=page, NextMarker=token)
1✔
2948

2949
    # =======================================
2950
    # =========  Account Settings   =========
2951
    # =======================================
2952

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

2958
        fn_count = 0
1✔
2959
        code_size_sum = 0
1✔
2960
        reserved_concurrency_sum = 0
1✔
2961
        for fn in state.functions.values():
1✔
2962
            fn_count += 1
1✔
2963
            for fn_version in fn.versions.values():
1✔
2964
                # Image-based Lambdas do not have a code attribute and count against the ECR quotas instead
2965
                if fn_version.config.package_type == PackageType.Zip:
1✔
2966
                    code_size_sum += fn_version.config.code.code_size
1✔
2967
            if fn.reserved_concurrent_executions is not None:
1✔
2968
                reserved_concurrency_sum += fn.reserved_concurrent_executions
1✔
2969
            for c in fn.provisioned_concurrency_configs.values():
1✔
2970
                reserved_concurrency_sum += c.provisioned_concurrent_executions
1✔
2971
        for layer in state.layers.values():
1✔
2972
            for layer_version in layer.layer_versions.values():
1✔
2973
                code_size_sum += layer_version.code.code_size
1✔
2974
        return GetAccountSettingsResponse(
1✔
2975
            AccountLimit=AccountLimit(
2976
                TotalCodeSize=config.LAMBDA_LIMITS_TOTAL_CODE_SIZE,
2977
                CodeSizeZipped=config.LAMBDA_LIMITS_CODE_SIZE_ZIPPED,
2978
                CodeSizeUnzipped=config.LAMBDA_LIMITS_CODE_SIZE_UNZIPPED,
2979
                ConcurrentExecutions=config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS,
2980
                UnreservedConcurrentExecutions=config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS
2981
                - reserved_concurrency_sum,
2982
            ),
2983
            AccountUsage=AccountUsage(
2984
                TotalCodeSize=code_size_sum,
2985
                FunctionCount=fn_count,
2986
            ),
2987
        )
2988

2989
    # =======================================
2990
    # ==  Provisioned Concurrency Config   ==
2991
    # =======================================
2992

2993
    def _get_provisioned_config(
1✔
2994
        self, context: RequestContext, function_name: str, qualifier: str
2995
    ) -> ProvisionedConcurrencyConfiguration | None:
2996
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2997
        state = lambda_stores[account_id][region]
1✔
2998
        function_name = api_utils.get_function_name(function_name, context)
1✔
2999
        fn = state.functions.get(function_name)
1✔
3000
        if api_utils.qualifier_is_alias(qualifier):
1✔
3001
            fn_alias = None
1✔
3002
            if fn:
1✔
3003
                fn_alias = fn.aliases.get(qualifier)
1✔
3004
            if fn_alias is None:
1✔
3005
                raise ResourceNotFoundException(
1✔
3006
                    f"Cannot find alias arn: {api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)}",
3007
                    Type="User",
3008
                )
3009
        elif api_utils.qualifier_is_version(qualifier):
1✔
3010
            fn_version = None
1✔
3011
            if fn:
1✔
3012
                fn_version = fn.versions.get(qualifier)
1✔
3013
            if fn_version is None:
1✔
3014
                raise ResourceNotFoundException(
1✔
3015
                    f"Function not found: {api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)}",
3016
                    Type="User",
3017
                )
3018

3019
        return fn.provisioned_concurrency_configs.get(qualifier)
1✔
3020

3021
    def put_provisioned_concurrency_config(
1✔
3022
        self,
3023
        context: RequestContext,
3024
        function_name: FunctionName,
3025
        qualifier: Qualifier,
3026
        provisioned_concurrent_executions: PositiveInteger,
3027
        **kwargs,
3028
    ) -> PutProvisionedConcurrencyConfigResponse:
3029
        if provisioned_concurrent_executions <= 0:
1✔
3030
            raise ValidationException(
1✔
3031
                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"
3032
            )
3033

3034
        if qualifier == "$LATEST":
1✔
3035
            raise InvalidParameterValueException(
1✔
3036
                "Provisioned Concurrency Configs cannot be applied to unpublished function versions.",
3037
                Type="User",
3038
            )
3039
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3040
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3041
            function_name, qualifier, context
3042
        )
3043
        state = lambda_stores[account_id][region]
1✔
3044
        fn = state.functions.get(function_name)
1✔
3045

3046
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3047

3048
        if provisioned_config:  # TODO: merge?
1✔
3049
            # TODO: add a test for partial updates (if possible)
3050
            LOG.warning(
1✔
3051
                "Partial update of provisioned concurrency config is currently not supported."
3052
            )
3053

3054
        other_provisioned_sum = sum(
1✔
3055
            [
3056
                provisioned_configs.provisioned_concurrent_executions
3057
                for provisioned_qualifier, provisioned_configs in fn.provisioned_concurrency_configs.items()
3058
                if provisioned_qualifier != qualifier
3059
            ]
3060
        )
3061

3062
        if (
1✔
3063
            fn.reserved_concurrent_executions is not None
3064
            and fn.reserved_concurrent_executions
3065
            < other_provisioned_sum + provisioned_concurrent_executions
3066
        ):
3067
            raise InvalidParameterValueException(
1✔
3068
                "Requested Provisioned Concurrency should not be greater than the reservedConcurrentExecution for function",
3069
                Type="User",
3070
            )
3071

3072
        if provisioned_concurrent_executions > config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS:
1✔
3073
            raise InvalidParameterValueException(
1✔
3074
                f"Specified ConcurrentExecutions for function is greater than account's unreserved concurrency"
3075
                f" [{config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS}]."
3076
            )
3077

3078
        settings = self.get_account_settings(context)
1✔
3079
        unreserved_concurrent_executions = settings["AccountLimit"][
1✔
3080
            "UnreservedConcurrentExecutions"
3081
        ]
3082
        if (
1✔
3083
            unreserved_concurrent_executions - provisioned_concurrent_executions
3084
            < config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY
3085
        ):
3086
            raise InvalidParameterValueException(
1✔
3087
                f"Specified ConcurrentExecutions for function decreases account's UnreservedConcurrentExecution below"
3088
                f" its minimum value of [{config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY}]."
3089
            )
3090

3091
        provisioned_config = ProvisionedConcurrencyConfiguration(
1✔
3092
            provisioned_concurrent_executions, api_utils.generate_lambda_date()
3093
        )
3094
        fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3095

3096
        if api_utils.qualifier_is_alias(qualifier):
1✔
3097
            alias = fn.aliases.get(qualifier)
1✔
3098
            resolved_version = fn.versions.get(alias.function_version)
1✔
3099

3100
            if (
1✔
3101
                resolved_version
3102
                and fn.provisioned_concurrency_configs.get(alias.function_version) is not None
3103
            ):
3104
                raise ResourceConflictException(
1✔
3105
                    "Alias can't be used for Provisioned Concurrency configuration on an already Provisioned version",
3106
                    Type="User",
3107
                )
3108
            fn_arn = resolved_version.id.qualified_arn()
1✔
3109
        elif api_utils.qualifier_is_version(qualifier):
1✔
3110
            fn_version = fn.versions.get(qualifier)
1✔
3111

3112
            # TODO: might be useful other places, utilize
3113
            pointing_aliases = []
1✔
3114
            for alias in fn.aliases.values():
1✔
3115
                if (
1✔
3116
                    alias.function_version == qualifier
3117
                    and fn.provisioned_concurrency_configs.get(alias.name) is not None
3118
                ):
3119
                    pointing_aliases.append(alias.name)
1✔
3120
            if pointing_aliases:
1✔
3121
                raise ResourceConflictException(
1✔
3122
                    "Version is pointed by a Provisioned Concurrency alias", Type="User"
3123
                )
3124

3125
            fn_arn = fn_version.id.qualified_arn()
1✔
3126

3127
        manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3128

3129
        fn.provisioned_concurrency_configs[qualifier] = provisioned_config
1✔
3130

3131
        manager.update_provisioned_concurrency_config(
1✔
3132
            provisioned_config.provisioned_concurrent_executions
3133
        )
3134

3135
        return PutProvisionedConcurrencyConfigResponse(
1✔
3136
            RequestedProvisionedConcurrentExecutions=provisioned_config.provisioned_concurrent_executions,
3137
            AvailableProvisionedConcurrentExecutions=0,
3138
            AllocatedProvisionedConcurrentExecutions=0,
3139
            Status=ProvisionedConcurrencyStatusEnum.IN_PROGRESS,
3140
            # StatusReason=manager.provisioned_state.status_reason,
3141
            LastModified=provisioned_config.last_modified,  # TODO: does change with configuration or also with state changes?
3142
        )
3143

3144
    def get_provisioned_concurrency_config(
1✔
3145
        self, context: RequestContext, function_name: FunctionName, qualifier: Qualifier, **kwargs
3146
    ) -> GetProvisionedConcurrencyConfigResponse:
3147
        if qualifier == "$LATEST":
1✔
3148
            raise InvalidParameterValueException(
1✔
3149
                "The function resource provided must be an alias or a published version.",
3150
                Type="User",
3151
            )
3152
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3153
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3154
            function_name, qualifier, context
3155
        )
3156

3157
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3158
        if not provisioned_config:
1✔
3159
            raise ProvisionedConcurrencyConfigNotFoundException(
1✔
3160
                "No Provisioned Concurrency Config found for this function", Type="User"
3161
            )
3162

3163
        # TODO: make this compatible with alias pointer migration on update
3164
        if api_utils.qualifier_is_alias(qualifier):
1✔
3165
            state = lambda_stores[account_id][region]
1✔
3166
            fn = state.functions.get(function_name)
1✔
3167
            alias = fn.aliases.get(qualifier)
1✔
3168
            fn_arn = api_utils.qualified_lambda_arn(
1✔
3169
                function_name, alias.function_version, account_id, region
3170
            )
3171
        else:
3172
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3173

3174
        ver_manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3175

3176
        return GetProvisionedConcurrencyConfigResponse(
1✔
3177
            RequestedProvisionedConcurrentExecutions=provisioned_config.provisioned_concurrent_executions,
3178
            LastModified=provisioned_config.last_modified,
3179
            AvailableProvisionedConcurrentExecutions=ver_manager.provisioned_state.available,
3180
            AllocatedProvisionedConcurrentExecutions=ver_manager.provisioned_state.allocated,
3181
            Status=ver_manager.provisioned_state.status,
3182
            StatusReason=ver_manager.provisioned_state.status_reason,
3183
        )
3184

3185
    def list_provisioned_concurrency_configs(
1✔
3186
        self,
3187
        context: RequestContext,
3188
        function_name: FunctionName,
3189
        marker: String = None,
3190
        max_items: MaxProvisionedConcurrencyConfigListItems = None,
3191
        **kwargs,
3192
    ) -> ListProvisionedConcurrencyConfigsResponse:
3193
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3194
        state = lambda_stores[account_id][region]
1✔
3195

3196
        function_name = api_utils.get_function_name(function_name, context)
1✔
3197
        fn = state.functions.get(function_name)
1✔
3198
        if fn is None:
1✔
3199
            raise ResourceNotFoundException(
1✔
3200
                f"Function not found: {api_utils.unqualified_lambda_arn(function_name, account_id, region)}",
3201
                Type="User",
3202
            )
3203

3204
        configs = []
1✔
3205
        for qualifier, pc_config in fn.provisioned_concurrency_configs.items():
1✔
3206
            if api_utils.qualifier_is_alias(qualifier):
×
3207
                alias = fn.aliases.get(qualifier)
×
3208
                fn_arn = api_utils.qualified_lambda_arn(
×
3209
                    function_name, alias.function_version, account_id, region
3210
                )
3211
            else:
3212
                fn_arn = api_utils.qualified_lambda_arn(
×
3213
                    function_name, qualifier, account_id, region
3214
                )
3215

3216
            manager = self.lambda_service.get_lambda_version_manager(fn_arn)
×
3217

3218
            configs.append(
×
3219
                ProvisionedConcurrencyConfigListItem(
3220
                    FunctionArn=api_utils.qualified_lambda_arn(
3221
                        function_name, qualifier, account_id, region
3222
                    ),
3223
                    RequestedProvisionedConcurrentExecutions=pc_config.provisioned_concurrent_executions,
3224
                    AvailableProvisionedConcurrentExecutions=manager.provisioned_state.available,
3225
                    AllocatedProvisionedConcurrentExecutions=manager.provisioned_state.allocated,
3226
                    Status=manager.provisioned_state.status,
3227
                    StatusReason=manager.provisioned_state.status_reason,
3228
                    LastModified=pc_config.last_modified,
3229
                )
3230
            )
3231

3232
        provisioned_concurrency_configs = configs
1✔
3233
        provisioned_concurrency_configs = PaginatedList(provisioned_concurrency_configs)
1✔
3234
        page, token = provisioned_concurrency_configs.get_page(
1✔
3235
            lambda x: x,
3236
            marker,
3237
            max_items,
3238
        )
3239
        return ListProvisionedConcurrencyConfigsResponse(
1✔
3240
            ProvisionedConcurrencyConfigs=page, NextMarker=token
3241
        )
3242

3243
    def delete_provisioned_concurrency_config(
1✔
3244
        self, context: RequestContext, function_name: FunctionName, qualifier: Qualifier, **kwargs
3245
    ) -> None:
3246
        if qualifier == "$LATEST":
1✔
3247
            raise InvalidParameterValueException(
1✔
3248
                "The function resource provided must be an alias or a published version.",
3249
                Type="User",
3250
            )
3251
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3252
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3253
            function_name, qualifier, context
3254
        )
3255
        state = lambda_stores[account_id][region]
1✔
3256
        fn = state.functions.get(function_name)
1✔
3257

3258
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3259
        # delete is idempotent and doesn't actually care about the provisioned concurrency config not existing
3260
        if provisioned_config:
1✔
3261
            fn.provisioned_concurrency_configs.pop(qualifier)
1✔
3262
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3263
            manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3264
            manager.update_provisioned_concurrency_config(0)
1✔
3265

3266
    # =======================================
3267
    # =======  Event Invoke Config   ========
3268
    # =======================================
3269

3270
    # "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})?:(.*)"
3271
    # "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)
3272

3273
    def _validate_destination_config(
1✔
3274
        self, store: LambdaStore, function_name: str, destination_config: DestinationConfig
3275
    ):
3276
        def _validate_destination_arn(destination_arn) -> bool:
1✔
3277
            if not api_utils.DESTINATION_ARN_PATTERN.match(destination_arn):
1✔
3278
                # technically we shouldn't handle this in the provider
3279
                raise ValidationException(
1✔
3280
                    "1 validation error detected: Value '"
3281
                    + destination_arn
3282
                    + r"' 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})?:(.*)"
3283
                )
3284

3285
            match destination_arn.split(":")[2]:
1✔
3286
                case "lambda":
1✔
3287
                    fn_parts = api_utils.FULL_FN_ARN_PATTERN.search(destination_arn).groupdict()
1✔
3288
                    if fn_parts:
1✔
3289
                        # check if it exists
3290
                        fn = store.functions.get(fn_parts["function_name"])
1✔
3291
                        if not fn:
1✔
3292
                            raise InvalidParameterValueException(
1✔
3293
                                f"The destination ARN {destination_arn} is invalid.", Type="User"
3294
                            )
3295
                        if fn_parts["function_name"] == function_name:
1✔
3296
                            raise InvalidParameterValueException(
1✔
3297
                                "You can't specify the function as a destination for itself.",
3298
                                Type="User",
3299
                            )
3300
                case "sns" | "sqs" | "events":
1✔
3301
                    pass
1✔
3302
                case _:
1✔
3303
                    return False
1✔
3304
            return True
1✔
3305

3306
        validation_err = False
1✔
3307

3308
        failure_destination = destination_config.get("OnFailure", {}).get("Destination")
1✔
3309
        if failure_destination:
1✔
3310
            validation_err = validation_err or not _validate_destination_arn(failure_destination)
1✔
3311

3312
        success_destination = destination_config.get("OnSuccess", {}).get("Destination")
1✔
3313
        if success_destination:
1✔
3314
            validation_err = validation_err or not _validate_destination_arn(success_destination)
1✔
3315

3316
        if validation_err:
1✔
3317
            on_success_part = (
1✔
3318
                f"OnSuccess(destination={success_destination})" if success_destination else "null"
3319
            )
3320
            on_failure_part = (
1✔
3321
                f"OnFailure(destination={failure_destination})" if failure_destination else "null"
3322
            )
3323
            raise InvalidParameterValueException(
1✔
3324
                f"The provided destination config DestinationConfig(onSuccess={on_success_part}, onFailure={on_failure_part}) is invalid.",
3325
                Type="User",
3326
            )
3327

3328
    def put_function_event_invoke_config(
1✔
3329
        self,
3330
        context: RequestContext,
3331
        function_name: FunctionName,
3332
        qualifier: Qualifier = None,
3333
        maximum_retry_attempts: MaximumRetryAttempts = None,
3334
        maximum_event_age_in_seconds: MaximumEventAgeInSeconds = None,
3335
        destination_config: DestinationConfig = None,
3336
        **kwargs,
3337
    ) -> FunctionEventInvokeConfig:
3338
        """
3339
        Destination ARNs can be:
3340
        * SQS arn
3341
        * SNS arn
3342
        * Lambda arn
3343
        * EventBridge arn
3344

3345
        Differences between put_ and update_:
3346
            * put overwrites any existing config
3347
            * update allows changes only single values while keeping the rest of existing ones
3348
            * update fails on non-existing configs
3349

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

3354
        """
3355
        if (
1✔
3356
            maximum_event_age_in_seconds is None
3357
            and maximum_retry_attempts is None
3358
            and destination_config is None
3359
        ):
3360
            raise InvalidParameterValueException(
1✔
3361
                "You must specify at least one of error handling or destination setting.",
3362
                Type="User",
3363
            )
3364
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3365
        state = lambda_stores[account_id][region]
1✔
3366
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3367
            function_name, qualifier, context
3368
        )
3369
        fn = state.functions.get(function_name)
1✔
3370
        if not fn or (qualifier and not (qualifier in fn.aliases or qualifier in fn.versions)):
1✔
3371
            raise ResourceNotFoundException("The function doesn't exist.", Type="User")
1✔
3372

3373
        qualifier = qualifier or "$LATEST"
1✔
3374

3375
        # validate and normalize destination config
3376
        if destination_config:
1✔
3377
            self._validate_destination_config(state, function_name, destination_config)
1✔
3378

3379
        destination_config = DestinationConfig(
1✔
3380
            OnSuccess=OnSuccess(
3381
                Destination=(destination_config or {}).get("OnSuccess", {}).get("Destination")
3382
            ),
3383
            OnFailure=OnFailure(
3384
                Destination=(destination_config or {}).get("OnFailure", {}).get("Destination")
3385
            ),
3386
        )
3387

3388
        config = EventInvokeConfig(
1✔
3389
            function_name=function_name,
3390
            qualifier=qualifier,
3391
            maximum_event_age_in_seconds=maximum_event_age_in_seconds,
3392
            maximum_retry_attempts=maximum_retry_attempts,
3393
            last_modified=api_utils.generate_lambda_date(),
3394
            destination_config=destination_config,
3395
        )
3396
        fn.event_invoke_configs[qualifier] = config
1✔
3397

3398
        return FunctionEventInvokeConfig(
1✔
3399
            LastModified=datetime.datetime.strptime(
3400
                config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3401
            ),
3402
            FunctionArn=api_utils.qualified_lambda_arn(
3403
                function_name, qualifier or "$LATEST", account_id, region
3404
            ),
3405
            DestinationConfig=destination_config,
3406
            MaximumEventAgeInSeconds=maximum_event_age_in_seconds,
3407
            MaximumRetryAttempts=maximum_retry_attempts,
3408
        )
3409

3410
    def get_function_event_invoke_config(
1✔
3411
        self,
3412
        context: RequestContext,
3413
        function_name: FunctionName,
3414
        qualifier: Qualifier = None,
3415
        **kwargs,
3416
    ) -> FunctionEventInvokeConfig:
3417
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3418
        state = lambda_stores[account_id][region]
1✔
3419
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3420
            function_name, qualifier, context
3421
        )
3422

3423
        qualifier = qualifier or "$LATEST"
1✔
3424
        fn = state.functions.get(function_name)
1✔
3425
        if not fn:
1✔
3426
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3427
            raise ResourceNotFoundException(
1✔
3428
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3429
            )
3430

3431
        config = fn.event_invoke_configs.get(qualifier)
1✔
3432
        if not config:
1✔
3433
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3434
            raise ResourceNotFoundException(
1✔
3435
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3436
            )
3437

3438
        return FunctionEventInvokeConfig(
1✔
3439
            LastModified=datetime.datetime.strptime(
3440
                config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3441
            ),
3442
            FunctionArn=api_utils.qualified_lambda_arn(
3443
                function_name, qualifier, account_id, region
3444
            ),
3445
            DestinationConfig=config.destination_config,
3446
            MaximumEventAgeInSeconds=config.maximum_event_age_in_seconds,
3447
            MaximumRetryAttempts=config.maximum_retry_attempts,
3448
        )
3449

3450
    def list_function_event_invoke_configs(
1✔
3451
        self,
3452
        context: RequestContext,
3453
        function_name: FunctionName,
3454
        marker: String = None,
3455
        max_items: MaxFunctionEventInvokeConfigListItems = None,
3456
        **kwargs,
3457
    ) -> ListFunctionEventInvokeConfigsResponse:
3458
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3459
        state = lambda_stores[account_id][region]
1✔
3460
        fn = state.functions.get(function_name)
1✔
3461
        if not fn:
1✔
3462
            raise ResourceNotFoundException("The function doesn't exist.", Type="User")
1✔
3463

3464
        event_invoke_configs = [
1✔
3465
            FunctionEventInvokeConfig(
3466
                LastModified=c.last_modified,
3467
                FunctionArn=api_utils.qualified_lambda_arn(
3468
                    function_name, c.qualifier, account_id, region
3469
                ),
3470
                MaximumEventAgeInSeconds=c.maximum_event_age_in_seconds,
3471
                MaximumRetryAttempts=c.maximum_retry_attempts,
3472
                DestinationConfig=c.destination_config,
3473
            )
3474
            for c in fn.event_invoke_configs.values()
3475
        ]
3476

3477
        event_invoke_configs = PaginatedList(event_invoke_configs)
1✔
3478
        page, token = event_invoke_configs.get_page(
1✔
3479
            lambda x: x["FunctionArn"],
3480
            marker,
3481
            max_items,
3482
        )
3483
        return ListFunctionEventInvokeConfigsResponse(
1✔
3484
            FunctionEventInvokeConfigs=page, NextMarker=token
3485
        )
3486

3487
    def delete_function_event_invoke_config(
1✔
3488
        self,
3489
        context: RequestContext,
3490
        function_name: FunctionName,
3491
        qualifier: Qualifier = None,
3492
        **kwargs,
3493
    ) -> None:
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
        resolved_qualifier = qualifier or "$LATEST"
1✔
3501
        fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3502
        if not fn:
1✔
3503
            raise ResourceNotFoundException(
1✔
3504
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3505
            )
3506

3507
        config = fn.event_invoke_configs.get(resolved_qualifier)
1✔
3508
        if not config:
1✔
3509
            raise ResourceNotFoundException(
1✔
3510
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3511
            )
3512

3513
        del fn.event_invoke_configs[resolved_qualifier]
1✔
3514

3515
    def update_function_event_invoke_config(
1✔
3516
        self,
3517
        context: RequestContext,
3518
        function_name: FunctionName,
3519
        qualifier: Qualifier = None,
3520
        maximum_retry_attempts: MaximumRetryAttempts = None,
3521
        maximum_event_age_in_seconds: MaximumEventAgeInSeconds = None,
3522
        destination_config: DestinationConfig = None,
3523
        **kwargs,
3524
    ) -> FunctionEventInvokeConfig:
3525
        # like put but only update single fields via replace
3526
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3527
        state = lambda_stores[account_id][region]
1✔
3528
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3529
            function_name, qualifier, context
3530
        )
3531

3532
        if (
1✔
3533
            maximum_event_age_in_seconds is None
3534
            and maximum_retry_attempts is None
3535
            and destination_config is None
3536
        ):
3537
            raise InvalidParameterValueException(
×
3538
                "You must specify at least one of error handling or destination setting.",
3539
                Type="User",
3540
            )
3541

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

3546
        qualifier = qualifier or "$LATEST"
1✔
3547

3548
        config = fn.event_invoke_configs.get(qualifier)
1✔
3549
        if not config:
1✔
3550
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3551
            raise ResourceNotFoundException(
1✔
3552
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3553
            )
3554

3555
        if destination_config:
1✔
3556
            self._validate_destination_config(state, function_name, destination_config)
×
3557

3558
        optional_kwargs = {
1✔
3559
            k: v
3560
            for k, v in {
3561
                "destination_config": destination_config,
3562
                "maximum_retry_attempts": maximum_retry_attempts,
3563
                "maximum_event_age_in_seconds": maximum_event_age_in_seconds,
3564
            }.items()
3565
            if v is not None
3566
        }
3567

3568
        new_config = dataclasses.replace(
1✔
3569
            config, last_modified=api_utils.generate_lambda_date(), **optional_kwargs
3570
        )
3571
        fn.event_invoke_configs[qualifier] = new_config
1✔
3572

3573
        return FunctionEventInvokeConfig(
1✔
3574
            LastModified=datetime.datetime.strptime(
3575
                new_config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3576
            ),
3577
            FunctionArn=api_utils.qualified_lambda_arn(
3578
                function_name, qualifier or "$LATEST", account_id, region
3579
            ),
3580
            DestinationConfig=new_config.destination_config,
3581
            MaximumEventAgeInSeconds=new_config.maximum_event_age_in_seconds,
3582
            MaximumRetryAttempts=new_config.maximum_retry_attempts,
3583
        )
3584

3585
    # =======================================
3586
    # ======  Layer & Layer Versions  =======
3587
    # =======================================
3588

3589
    @staticmethod
1✔
3590
    def _resolve_layer(
1✔
3591
        layer_name_or_arn: str, context: RequestContext
3592
    ) -> tuple[str, str, str, str | None]:
3593
        """
3594
        Return locator attributes for a given Lambda layer.
3595

3596
        :param layer_name_or_arn: Layer name or ARN
3597
        :param context: Request context
3598
        :return: Tuple of region, account ID, layer name, layer version
3599
        """
3600
        if api_utils.is_layer_arn(layer_name_or_arn):
1✔
3601
            return api_utils.parse_layer_arn(layer_name_or_arn)
1✔
3602

3603
        return context.region, context.account_id, layer_name_or_arn, None
1✔
3604

3605
    def publish_layer_version(
1✔
3606
        self,
3607
        context: RequestContext,
3608
        layer_name: LayerName,
3609
        content: LayerVersionContentInput,
3610
        description: Description = None,
3611
        compatible_runtimes: CompatibleRuntimes = None,
3612
        license_info: LicenseInfo = None,
3613
        compatible_architectures: CompatibleArchitectures = None,
3614
        **kwargs,
3615
    ) -> PublishLayerVersionResponse:
3616
        """
3617
        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.
3618
        Note that there are no $LATEST versions with layers!
3619

3620
        """
3621
        account = context.account_id
1✔
3622
        region = context.region
1✔
3623

3624
        validation_errors = api_utils.validate_layer_runtimes_and_architectures(
1✔
3625
            compatible_runtimes, compatible_architectures
3626
        )
3627
        if validation_errors:
1✔
3628
            raise ValidationException(
1✔
3629
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {'; '.join(validation_errors)}"
3630
            )
3631

3632
        state = lambda_stores[account][region]
1✔
3633
        with self.create_layer_lock:
1✔
3634
            if layer_name not in state.layers:
1✔
3635
                # we don't have a version so create new layer object
3636
                # lock is required to avoid creating two v1 objects for the same name
3637
                layer = Layer(
1✔
3638
                    arn=api_utils.layer_arn(layer_name=layer_name, account=account, region=region)
3639
                )
3640
                state.layers[layer_name] = layer
1✔
3641

3642
        layer = state.layers[layer_name]
1✔
3643
        with layer.next_version_lock:
1✔
3644
            next_version = LambdaLayerVersionIdentifier(
1✔
3645
                account_id=account, region=region, layer_name=layer_name
3646
            ).generate(next_version=layer.next_version)
3647
            # When creating a layer with user defined layer version, it is possible that we
3648
            # create layer versions out of order.
3649
            # ie. a user could replicate layer v2 then layer v1. It is important to always keep the maximum possible
3650
            # value for next layer to avoid overwriting existing versions
3651
            if layer.next_version <= next_version:
1✔
3652
                # We don't need to update layer.next_version if the created version is lower than the "next in line"
3653
                layer.next_version = max(next_version, layer.next_version) + 1
1✔
3654

3655
        # creating a new layer
3656
        if content.get("ZipFile"):
1✔
3657
            code = store_lambda_archive(
1✔
3658
                archive_file=content["ZipFile"],
3659
                function_name=layer_name,
3660
                region_name=region,
3661
                account_id=account,
3662
            )
3663
        else:
3664
            code = store_s3_bucket_archive(
1✔
3665
                archive_bucket=content["S3Bucket"],
3666
                archive_key=content["S3Key"],
3667
                archive_version=content.get("S3ObjectVersion"),
3668
                function_name=layer_name,
3669
                region_name=region,
3670
                account_id=account,
3671
            )
3672

3673
        new_layer_version = LayerVersion(
1✔
3674
            layer_version_arn=api_utils.layer_version_arn(
3675
                layer_name=layer_name,
3676
                account=account,
3677
                region=region,
3678
                version=str(next_version),
3679
            ),
3680
            layer_arn=layer.arn,
3681
            version=next_version,
3682
            description=description or "",
3683
            license_info=license_info,
3684
            compatible_runtimes=compatible_runtimes,
3685
            compatible_architectures=compatible_architectures,
3686
            created=api_utils.generate_lambda_date(),
3687
            code=code,
3688
        )
3689

3690
        layer.layer_versions[str(next_version)] = new_layer_version
1✔
3691

3692
        return api_utils.map_layer_out(new_layer_version)
1✔
3693

3694
    def get_layer_version(
1✔
3695
        self,
3696
        context: RequestContext,
3697
        layer_name: LayerName,
3698
        version_number: LayerVersionNumber,
3699
        **kwargs,
3700
    ) -> GetLayerVersionResponse:
3701
        # TODO: handle layer_name as an ARN
3702

3703
        region_name, account_id, layer_name, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
3704
        state = lambda_stores[account_id][region_name]
1✔
3705

3706
        layer = state.layers.get(layer_name)
1✔
3707
        if version_number < 1:
1✔
3708
            raise InvalidParameterValueException("Layer Version Cannot be less than 1", Type="User")
1✔
3709
        if layer is None:
1✔
3710
            raise ResourceNotFoundException(
1✔
3711
                "The resource you requested does not exist.", Type="User"
3712
            )
3713
        layer_version = layer.layer_versions.get(str(version_number))
1✔
3714
        if layer_version is None:
1✔
3715
            raise ResourceNotFoundException(
1✔
3716
                "The resource you requested does not exist.", Type="User"
3717
            )
3718
        return api_utils.map_layer_out(layer_version)
1✔
3719

3720
    def get_layer_version_by_arn(
1✔
3721
        self, context: RequestContext, arn: LayerVersionArn, **kwargs
3722
    ) -> GetLayerVersionResponse:
3723
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
3724
            arn, context
3725
        )
3726

3727
        if not layer_version:
1✔
3728
            raise ValidationException(
1✔
3729
                f"1 validation error detected: Value '{arn}' at 'arn' failed to satisfy constraint: Member must satisfy regular expression pattern: "
3730
                + "(arn:(aws[a-zA-Z-]*)?:lambda:[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-_]+)"
3731
            )
3732

3733
        store = lambda_stores[account_id][region_name]
1✔
3734
        if not (layers := store.layers.get(layer_name)):
1✔
3735
            raise ResourceNotFoundException(
×
3736
                "The resource you requested does not exist.", Type="User"
3737
            )
3738

3739
        layer_version = layers.layer_versions.get(layer_version)
1✔
3740

3741
        if not layer_version:
1✔
3742
            raise ResourceNotFoundException(
1✔
3743
                "The resource you requested does not exist.", Type="User"
3744
            )
3745

3746
        return api_utils.map_layer_out(layer_version)
1✔
3747

3748
    def list_layers(
1✔
3749
        self,
3750
        context: RequestContext,
3751
        compatible_runtime: Runtime = None,
3752
        marker: String = None,
3753
        max_items: MaxLayerListItems = None,
3754
        compatible_architecture: Architecture = None,
3755
        **kwargs,
3756
    ) -> ListLayersResponse:
3757
        validation_errors = []
1✔
3758

3759
        validation_error_arch = api_utils.validate_layer_architecture(compatible_architecture)
1✔
3760
        if validation_error_arch:
1✔
3761
            validation_errors.append(validation_error_arch)
1✔
3762

3763
        validation_error_runtime = api_utils.validate_layer_runtime(compatible_runtime)
1✔
3764
        if validation_error_runtime:
1✔
3765
            validation_errors.append(validation_error_runtime)
1✔
3766

3767
        if validation_errors:
1✔
3768
            raise ValidationException(
1✔
3769
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {';'.join(validation_errors)}"
3770
            )
3771
        # TODO: handle filter: compatible_runtime
3772
        # TODO: handle filter: compatible_architecture
3773

3774
        state = lambda_stores[context.account_id][context.region]
×
3775
        layers = state.layers
×
3776

3777
        # 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?
3778

3779
        responses: list[LayersListItem] = []
×
3780
        for layer_name, layer in layers.items():
×
3781
            # fetch latest version
3782
            layer_versions = list(layer.layer_versions.values())
×
3783
            sorted(layer_versions, key=lambda x: x.version)
×
3784
            latest_layer_version = layer_versions[-1]
×
3785
            responses.append(
×
3786
                LayersListItem(
3787
                    LayerName=layer_name,
3788
                    LayerArn=layer.arn,
3789
                    LatestMatchingVersion=api_utils.map_layer_out(latest_layer_version),
3790
                )
3791
            )
3792

3793
        responses = PaginatedList(responses)
×
3794
        page, token = responses.get_page(
×
3795
            lambda version: version,
3796
            marker,
3797
            max_items,
3798
        )
3799

3800
        return ListLayersResponse(NextMarker=token, Layers=page)
×
3801

3802
    def list_layer_versions(
1✔
3803
        self,
3804
        context: RequestContext,
3805
        layer_name: LayerName,
3806
        compatible_runtime: Runtime = None,
3807
        marker: String = None,
3808
        max_items: MaxLayerListItems = None,
3809
        compatible_architecture: Architecture = None,
3810
        **kwargs,
3811
    ) -> ListLayerVersionsResponse:
3812
        validation_errors = api_utils.validate_layer_runtimes_and_architectures(
1✔
3813
            [compatible_runtime] if compatible_runtime else [],
3814
            [compatible_architecture] if compatible_architecture else [],
3815
        )
3816
        if validation_errors:
1✔
3817
            raise ValidationException(
×
3818
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {';'.join(validation_errors)}"
3819
            )
3820

3821
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
3822
            layer_name, context
3823
        )
3824
        state = lambda_stores[account_id][region_name]
1✔
3825

3826
        # TODO: Test & handle filter: compatible_runtime
3827
        # TODO: Test & handle filter: compatible_architecture
3828
        all_layer_versions = []
1✔
3829
        layer = state.layers.get(layer_name)
1✔
3830
        if layer is not None:
1✔
3831
            for layer_version in layer.layer_versions.values():
1✔
3832
                all_layer_versions.append(api_utils.map_layer_out(layer_version))
1✔
3833

3834
        all_layer_versions.sort(key=lambda x: x["Version"], reverse=True)
1✔
3835
        all_layer_versions = PaginatedList(all_layer_versions)
1✔
3836
        page, token = all_layer_versions.get_page(
1✔
3837
            lambda version: version["LayerVersionArn"],
3838
            marker,
3839
            max_items,
3840
        )
3841
        return ListLayerVersionsResponse(NextMarker=token, LayerVersions=page)
1✔
3842

3843
    def delete_layer_version(
1✔
3844
        self,
3845
        context: RequestContext,
3846
        layer_name: LayerName,
3847
        version_number: LayerVersionNumber,
3848
        **kwargs,
3849
    ) -> None:
3850
        if version_number < 1:
1✔
3851
            raise InvalidParameterValueException("Layer Version Cannot be less than 1", Type="User")
1✔
3852

3853
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
3854
            layer_name, context
3855
        )
3856

3857
        store = lambda_stores[account_id][region_name]
1✔
3858
        layer = store.layers.get(layer_name, {})
1✔
3859
        if layer:
1✔
3860
            layer.layer_versions.pop(str(version_number), None)
1✔
3861

3862
    # =======================================
3863
    # =====  Layer Version Permissions  =====
3864
    # =======================================
3865
    # TODO: lock updates that change revision IDs
3866

3867
    def add_layer_version_permission(
1✔
3868
        self,
3869
        context: RequestContext,
3870
        layer_name: LayerName,
3871
        version_number: LayerVersionNumber,
3872
        statement_id: StatementId,
3873
        action: LayerPermissionAllowedAction,
3874
        principal: LayerPermissionAllowedPrincipal,
3875
        organization_id: OrganizationId = None,
3876
        revision_id: String = None,
3877
        **kwargs,
3878
    ) -> AddLayerVersionPermissionResponse:
3879
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
3880
        # `layer_n` contains the layer name.
3881
        region_name, account_id, layer_n, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
3882

3883
        if action != "lambda:GetLayerVersion":
1✔
3884
            raise ValidationException(
1✔
3885
                f"1 validation error detected: Value '{action}' at 'action' failed to satisfy constraint: Member must satisfy regular expression pattern: lambda:GetLayerVersion"
3886
            )
3887

3888
        store = lambda_stores[account_id][region_name]
1✔
3889
        layer = store.layers.get(layer_n)
1✔
3890

3891
        layer_version_arn = api_utils.layer_version_arn(
1✔
3892
            layer_name, account_id, region_name, str(version_number)
3893
        )
3894

3895
        if layer is None:
1✔
3896
            raise ResourceNotFoundException(
1✔
3897
                f"Layer version {layer_version_arn} does not exist.", Type="User"
3898
            )
3899
        layer_version = layer.layer_versions.get(str(version_number))
1✔
3900
        if layer_version is None:
1✔
3901
            raise ResourceNotFoundException(
1✔
3902
                f"Layer version {layer_version_arn} does not exist.", Type="User"
3903
            )
3904
        # do we have a policy? if not set one
3905
        if layer_version.policy is None:
1✔
3906
            layer_version.policy = LayerPolicy()
1✔
3907

3908
        if statement_id in layer_version.policy.statements:
1✔
3909
            raise ResourceConflictException(
1✔
3910
                f"The statement id ({statement_id}) provided already exists. Please provide a new statement id, or remove the existing statement.",
3911
                Type="User",
3912
            )
3913

3914
        if revision_id and layer_version.policy.revision_id != revision_id:
1✔
3915
            raise PreconditionFailedException(
1✔
3916
                "The Revision Id provided does not match the latest Revision Id. "
3917
                "Call the GetLayerPolicy API to retrieve the latest Revision Id",
3918
                Type="User",
3919
            )
3920

3921
        statement = LayerPolicyStatement(
1✔
3922
            sid=statement_id, action=action, principal=principal, organization_id=organization_id
3923
        )
3924

3925
        old_statements = layer_version.policy.statements
1✔
3926
        layer_version.policy = dataclasses.replace(
1✔
3927
            layer_version.policy, statements={**old_statements, statement_id: statement}
3928
        )
3929

3930
        return AddLayerVersionPermissionResponse(
1✔
3931
            Statement=json.dumps(
3932
                {
3933
                    "Sid": statement.sid,
3934
                    "Effect": "Allow",
3935
                    "Principal": statement.principal,
3936
                    "Action": statement.action,
3937
                    "Resource": layer_version.layer_version_arn,
3938
                }
3939
            ),
3940
            RevisionId=layer_version.policy.revision_id,
3941
        )
3942

3943
    def remove_layer_version_permission(
1✔
3944
        self,
3945
        context: RequestContext,
3946
        layer_name: LayerName,
3947
        version_number: LayerVersionNumber,
3948
        statement_id: StatementId,
3949
        revision_id: String = None,
3950
        **kwargs,
3951
    ) -> None:
3952
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
3953
        # `layer_n` contains the layer name.
3954
        region_name, account_id, layer_n, layer_version = LambdaProvider._resolve_layer(
1✔
3955
            layer_name, context
3956
        )
3957

3958
        layer_version_arn = api_utils.layer_version_arn(
1✔
3959
            layer_name, account_id, region_name, str(version_number)
3960
        )
3961

3962
        state = lambda_stores[account_id][region_name]
1✔
3963
        layer = state.layers.get(layer_n)
1✔
3964
        if layer is None:
1✔
3965
            raise ResourceNotFoundException(
1✔
3966
                f"Layer version {layer_version_arn} does not exist.", Type="User"
3967
            )
3968
        layer_version = layer.layer_versions.get(str(version_number))
1✔
3969
        if layer_version is None:
1✔
3970
            raise ResourceNotFoundException(
1✔
3971
                f"Layer version {layer_version_arn} does not exist.", Type="User"
3972
            )
3973

3974
        if revision_id and layer_version.policy.revision_id != revision_id:
1✔
3975
            raise PreconditionFailedException(
1✔
3976
                "The Revision Id provided does not match the latest Revision Id. "
3977
                "Call the GetLayerPolicy API to retrieve the latest Revision Id",
3978
                Type="User",
3979
            )
3980

3981
        if statement_id not in layer_version.policy.statements:
1✔
3982
            raise ResourceNotFoundException(
1✔
3983
                f"Statement {statement_id} is not found in resource policy.", Type="User"
3984
            )
3985

3986
        old_statements = layer_version.policy.statements
1✔
3987
        layer_version.policy = dataclasses.replace(
1✔
3988
            layer_version.policy,
3989
            statements={k: v for k, v in old_statements.items() if k != statement_id},
3990
        )
3991

3992
    def get_layer_version_policy(
1✔
3993
        self,
3994
        context: RequestContext,
3995
        layer_name: LayerName,
3996
        version_number: LayerVersionNumber,
3997
        **kwargs,
3998
    ) -> GetLayerVersionPolicyResponse:
3999
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
4000
        # `layer_n` contains the layer name.
4001
        region_name, account_id, layer_n, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
4002

4003
        layer_version_arn = api_utils.layer_version_arn(
1✔
4004
            layer_name, account_id, region_name, str(version_number)
4005
        )
4006

4007
        store = lambda_stores[account_id][region_name]
1✔
4008
        layer = store.layers.get(layer_n)
1✔
4009

4010
        if layer is None:
1✔
4011
            raise ResourceNotFoundException(
1✔
4012
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4013
            )
4014

4015
        layer_version = layer.layer_versions.get(str(version_number))
1✔
4016
        if layer_version is None:
1✔
4017
            raise ResourceNotFoundException(
1✔
4018
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4019
            )
4020

4021
        if layer_version.policy is None:
1✔
4022
            raise ResourceNotFoundException(
1✔
4023
                "No policy is associated with the given resource.", Type="User"
4024
            )
4025

4026
        return GetLayerVersionPolicyResponse(
1✔
4027
            Policy=json.dumps(
4028
                {
4029
                    "Version": layer_version.policy.version,
4030
                    "Id": layer_version.policy.id,
4031
                    "Statement": [
4032
                        {
4033
                            "Sid": ps.sid,
4034
                            "Effect": "Allow",
4035
                            "Principal": ps.principal,
4036
                            "Action": ps.action,
4037
                            "Resource": layer_version.layer_version_arn,
4038
                        }
4039
                        for ps in layer_version.policy.statements.values()
4040
                    ],
4041
                }
4042
            ),
4043
            RevisionId=layer_version.policy.revision_id,
4044
        )
4045

4046
    # =======================================
4047
    # =======  Function Concurrency  ========
4048
    # =======================================
4049
    # (Reserved) function concurrency is scoped to the whole function
4050

4051
    def get_function_concurrency(
1✔
4052
        self, context: RequestContext, function_name: FunctionName, **kwargs
4053
    ) -> GetFunctionConcurrencyResponse:
4054
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4055
        function_name = api_utils.get_function_name(function_name, context)
1✔
4056
        fn = self._get_function(function_name=function_name, region=region, account_id=account_id)
1✔
4057
        return GetFunctionConcurrencyResponse(
1✔
4058
            ReservedConcurrentExecutions=fn.reserved_concurrent_executions
4059
        )
4060

4061
    def put_function_concurrency(
1✔
4062
        self,
4063
        context: RequestContext,
4064
        function_name: FunctionName,
4065
        reserved_concurrent_executions: ReservedConcurrentExecutions,
4066
        **kwargs,
4067
    ) -> Concurrency:
4068
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4069

4070
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
4071
        if qualifier:
1✔
4072
            raise InvalidParameterValueException(
1✔
4073
                "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.",
4074
                Type="User",
4075
            )
4076

4077
        store = lambda_stores[account_id][region]
1✔
4078
        fn = store.functions.get(function_name)
1✔
4079
        if not fn:
1✔
4080
            fn_arn = api_utils.qualified_lambda_arn(
1✔
4081
                function_name,
4082
                qualifier="$LATEST",
4083
                account=account_id,
4084
                region=region,
4085
            )
4086
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
4087

4088
        settings = self.get_account_settings(context)
1✔
4089
        unreserved_concurrent_executions = settings["AccountLimit"][
1✔
4090
            "UnreservedConcurrentExecutions"
4091
        ]
4092

4093
        # The existing reserved concurrent executions for the same function are already deduced in
4094
        # unreserved_concurrent_executions but must not count because the new one will replace the existing one.
4095
        # Joel tested this behavior manually against AWS (2023-11-28).
4096
        existing_reserved_concurrent_executions = (
1✔
4097
            fn.reserved_concurrent_executions if fn.reserved_concurrent_executions else 0
4098
        )
4099
        if (
1✔
4100
            unreserved_concurrent_executions
4101
            - reserved_concurrent_executions
4102
            + existing_reserved_concurrent_executions
4103
        ) < config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY:
4104
            raise InvalidParameterValueException(
1✔
4105
                f"Specified ReservedConcurrentExecutions for function decreases account's UnreservedConcurrentExecution below its minimum value of [{config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY}]."
4106
            )
4107

4108
        total_provisioned_concurrency = sum(
1✔
4109
            [
4110
                provisioned_configs.provisioned_concurrent_executions
4111
                for provisioned_configs in fn.provisioned_concurrency_configs.values()
4112
            ]
4113
        )
4114
        if total_provisioned_concurrency > reserved_concurrent_executions:
1✔
4115
            raise InvalidParameterValueException(
1✔
4116
                f" ReservedConcurrentExecutions  {reserved_concurrent_executions} should not be lower than function's total provisioned concurrency [{total_provisioned_concurrency}]."
4117
            )
4118

4119
        fn.reserved_concurrent_executions = reserved_concurrent_executions
1✔
4120

4121
        return Concurrency(ReservedConcurrentExecutions=fn.reserved_concurrent_executions)
1✔
4122

4123
    def delete_function_concurrency(
1✔
4124
        self, context: RequestContext, function_name: FunctionName, **kwargs
4125
    ) -> None:
4126
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4127
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
4128
        store = lambda_stores[account_id][region]
1✔
4129
        fn = store.functions.get(function_name)
1✔
4130
        fn.reserved_concurrent_executions = None
1✔
4131

4132
    # =======================================
4133
    # ===============  TAGS   ===============
4134
    # =======================================
4135
    # only Function, Event Source Mapping, and Code Signing Config (not currently supported by LocalStack) ARNs an are available for tagging in AWS
4136

4137
    def _get_tags(self, resource: TaggableResource) -> dict[str, str]:
1✔
4138
        state = self.fetch_lambda_store_for_tagging(resource)
1✔
4139
        lambda_adapted_tags = {
1✔
4140
            tag["Key"]: tag["Value"]
4141
            for tag in state.TAGS.list_tags_for_resource(resource).get("Tags")
4142
        }
4143
        return lambda_adapted_tags
1✔
4144

4145
    def _store_tags(self, resource: TaggableResource, tags: dict[str, str]):
1✔
4146
        state = self.fetch_lambda_store_for_tagging(resource)
1✔
4147
        if len(state.TAGS.tags.get(resource, {}) | tags) > LAMBDA_TAG_LIMIT_PER_RESOURCE:
1✔
4148
            raise InvalidParameterValueException(
1✔
4149
                "Number of tags exceeds resource tag limit.", Type="User"
4150
            )
4151

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

4155
    def fetch_lambda_store_for_tagging(self, resource: TaggableResource) -> LambdaStore:
1✔
4156
        """
4157
        Takes a resource ARN for a TaggableResource (Lambda Function, Event Source Mapping, or Code Signing Config) and returns a corresponding
4158
        LambdaStore for its region and account.
4159

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

4162
        Raises:
4163
            ValidationException: If the resource ARN is not a full ARN for a TaggableResource.
4164
            ResourceNotFoundException: If the specified resource does not exist.
4165
            InvalidParameterValueException: If the resource ARN is a qualified Lambda Function.
4166
        """
4167

4168
        def _raise_validation_exception():
1✔
4169
            raise ValidationException(
1✔
4170
                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}"
4171
            )
4172

4173
        # Check whether the ARN we have been passed is correctly formatted
4174
        parsed_resource_arn: ArnData = None
1✔
4175
        try:
1✔
4176
            parsed_resource_arn = parse_arn(resource)
1✔
4177
        except Exception:
1✔
4178
            _raise_validation_exception()
1✔
4179

4180
        # TODO: Should we be checking whether this is a full ARN?
4181
        region, account_id, resource_type = map(
1✔
4182
            parsed_resource_arn.get, ("region", "account", "resource")
4183
        )
4184

4185
        if not all((region, account_id, resource_type)):
1✔
4186
            _raise_validation_exception()
×
4187

4188
        if not (parts := resource_type.split(":")):
1✔
4189
            _raise_validation_exception()
×
4190

4191
        resource_type, resource_identifier, *qualifier = parts
1✔
4192
        if resource_type not in {"event-source-mapping", "code-signing-config", "function"}:
1✔
4193
            _raise_validation_exception()
1✔
4194

4195
        if qualifier:
1✔
4196
            if resource_type == "function":
1✔
4197
                raise InvalidParameterValueException(
1✔
4198
                    "Tags on function aliases and versions are not supported. Please specify a function ARN.",
4199
                    Type="User",
4200
                )
4201
            _raise_validation_exception()
1✔
4202

4203
        match resource_type:
1✔
4204
            case "event-source-mapping":
1✔
4205
                self._get_esm(resource_identifier, account_id, region)
1✔
4206
            case "code-signing-config":
1✔
4207
                raise NotImplementedError("Resource tagging on CSC not yet implemented.")
4208
            case "function":
1✔
4209
                self._get_function(
1✔
4210
                    function_name=resource_identifier, account_id=account_id, region=region
4211
                )
4212

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

4216
    def tag_resource(
1✔
4217
        self, context: RequestContext, resource: TaggableResource, tags: Tags, **kwargs
4218
    ) -> None:
4219
        if not tags:
1✔
4220
            raise InvalidParameterValueException(
1✔
4221
                "An error occurred and the request cannot be processed.", Type="User"
4222
            )
4223
        self._store_tags(resource, tags)
1✔
4224

4225
        if (resource_id := extract_resource_from_arn(resource)) and resource_id.startswith(
1✔
4226
            "function"
4227
        ):
4228
            name, _, account, region = function_locators_from_arn(resource)
1✔
4229
            function = self._get_function(name, account, region)
1✔
4230
            with function.lock:
1✔
4231
                # dirty hack for changed revision id, should reevaluate model to prevent this:
4232
                latest_version = function.versions["$LATEST"]
1✔
4233
                function.versions["$LATEST"] = dataclasses.replace(
1✔
4234
                    latest_version, config=dataclasses.replace(latest_version.config)
4235
                )
4236

4237
    def list_tags(
1✔
4238
        self, context: RequestContext, resource: TaggableResource, **kwargs
4239
    ) -> ListTagsResponse:
4240
        tags = self._get_tags(resource)
1✔
4241
        return ListTagsResponse(Tags=tags)
1✔
4242

4243
    def untag_resource(
1✔
4244
        self, context: RequestContext, resource: TaggableResource, tag_keys: TagKeyList, **kwargs
4245
    ) -> None:
4246
        if not tag_keys:
1✔
4247
            raise ValidationException(
1✔
4248
                "1 validation error detected: Value null at 'tagKeys' failed to satisfy constraint: Member must not be null"
4249
            )  # should probably be generalized a bit
4250

4251
        state = self.fetch_lambda_store_for_tagging(resource)
1✔
4252
        state.TAGS.untag_resource(resource, tag_keys)
1✔
4253

4254
        if (resource_id := extract_resource_from_arn(resource)) and resource_id.startswith(
1✔
4255
            "function"
4256
        ):
4257
            name, _, account, region = function_locators_from_arn(resource)
1✔
4258
            function = self._get_function(name, account, region)
1✔
4259
            # TODO: Potential race condition
4260
            with function.lock:
1✔
4261
                # dirty hack for changed revision id, should reevaluate model to prevent this:
4262
                latest_version = function.versions["$LATEST"]
1✔
4263
                function.versions["$LATEST"] = dataclasses.replace(
1✔
4264
                    latest_version, config=dataclasses.replace(latest_version.config)
4265
                )
4266

4267
    # =======================================
4268
    # =======  LEGACY / DEPRECATED   ========
4269
    # =======================================
4270

4271
    def invoke_async(
1✔
4272
        self,
4273
        context: RequestContext,
4274
        function_name: NamespacedFunctionName,
4275
        invoke_args: IO[BlobStream],
4276
        **kwargs,
4277
    ) -> InvokeAsyncResponse:
4278
        """LEGACY API endpoint. Even AWS heavily discourages its usage."""
4279
        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