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

localstack / localstack / bb17dc98-6f98-41b0-83a2-0623eba28bf8

04 Mar 2025 08:32PM UTC coverage: 86.873% (+0.01%) from 86.862%
bb17dc98-6f98-41b0-83a2-0623eba28bf8

push

circleci

web-flow
ESM: fix CreateESM SQS validation (#12338)

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

124 existing lines in 3 files now uncovered.

61908 of 71263 relevant lines covered (86.87%)

0.87 hits per line

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

91.92
/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, Optional, Tuple
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_.api_utils import (
1✔
154
    ARCHITECTURES,
155
    STATEMENT_ID_REGEX,
156
    SUBNET_ID_REGEX,
157
    function_locators_from_arn,
158
)
159
from localstack.services.lambda_.event_source_mapping.esm_config_factory import (
1✔
160
    EsmConfigFactory,
161
)
162
from localstack.services.lambda_.event_source_mapping.esm_worker import (
1✔
163
    EsmState,
164
    EsmWorker,
165
)
166
from localstack.services.lambda_.event_source_mapping.esm_worker_factory import (
1✔
167
    EsmWorkerFactory,
168
)
169
from localstack.services.lambda_.event_source_mapping.pipe_utils import get_internal_client
1✔
170
from localstack.services.lambda_.invocation import AccessDeniedException
1✔
171
from localstack.services.lambda_.invocation.execution_environment import (
1✔
172
    EnvironmentStartupTimeoutException,
173
)
174
from localstack.services.lambda_.invocation.lambda_models import (
1✔
175
    AliasRoutingConfig,
176
    CodeSigningConfig,
177
    EventInvokeConfig,
178
    Function,
179
    FunctionResourcePolicy,
180
    FunctionUrlConfig,
181
    FunctionVersion,
182
    ImageConfig,
183
    LambdaEphemeralStorage,
184
    Layer,
185
    LayerPolicy,
186
    LayerPolicyStatement,
187
    LayerVersion,
188
    ProvisionedConcurrencyConfiguration,
189
    RequestEntityTooLargeException,
190
    ResourcePolicy,
191
    UpdateStatus,
192
    ValidationException,
193
    VersionAlias,
194
    VersionFunctionConfiguration,
195
    VersionIdentifier,
196
    VersionState,
197
    VpcConfig,
198
)
199
from localstack.services.lambda_.invocation.lambda_service import (
1✔
200
    LambdaService,
201
    create_image_code,
202
    destroy_code_if_not_used,
203
    lambda_stores,
204
    store_lambda_archive,
205
    store_s3_bucket_archive,
206
)
207
from localstack.services.lambda_.invocation.models import LambdaStore
1✔
208
from localstack.services.lambda_.invocation.runtime_executor import get_runtime_executor
1✔
209
from localstack.services.lambda_.lambda_utils import HINT_LOG
1✔
210
from localstack.services.lambda_.layerfetcher.layer_fetcher import LayerFetcher
1✔
211
from localstack.services.lambda_.provider_utils import (
1✔
212
    LambdaLayerVersionIdentifier,
213
    get_function_version,
214
    get_function_version_from_arn,
215
)
216
from localstack.services.lambda_.runtimes import (
1✔
217
    ALL_RUNTIMES,
218
    DEPRECATED_RUNTIMES,
219
    DEPRECATED_RUNTIMES_UPGRADES,
220
    RUNTIMES_AGGREGATED,
221
    VALID_RUNTIMES,
222
)
223
from localstack.services.lambda_.urlrouter import FunctionUrlRouter
1✔
224
from localstack.services.plugins import ServiceLifecycleHook
1✔
225
from localstack.state import StateVisitor
1✔
226
from localstack.utils.aws.arns import (
1✔
227
    ArnData,
228
    extract_resource_from_arn,
229
    extract_service_from_arn,
230
    get_partition,
231
    lambda_event_source_mapping_arn,
232
    parse_arn,
233
)
234
from localstack.utils.aws.client_types import ServicePrincipal
1✔
235
from localstack.utils.bootstrap import is_api_enabled
1✔
236
from localstack.utils.collections import PaginatedList
1✔
237
from localstack.utils.event_matcher import validate_event_pattern
1✔
238
from localstack.utils.lambda_debug_mode.lambda_debug_mode_session import LambdaDebugModeSession
1✔
239
from localstack.utils.strings import get_random_hex, short_uid, to_bytes, to_str
1✔
240
from localstack.utils.sync import poll_condition
1✔
241
from localstack.utils.urls import localstack_host
1✔
242

243
LOG = logging.getLogger(__name__)
1✔
244

245
LAMBDA_DEFAULT_TIMEOUT = 3
1✔
246
LAMBDA_DEFAULT_MEMORY_SIZE = 128
1✔
247

248
LAMBDA_TAG_LIMIT_PER_RESOURCE = 50
1✔
249
LAMBDA_LAYERS_LIMIT_PER_FUNCTION = 5
1✔
250

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

256

257
class LambdaProvider(LambdaApi, ServiceLifecycleHook):
1✔
258
    lambda_service: LambdaService
1✔
259
    create_fn_lock: threading.RLock
1✔
260
    create_layer_lock: threading.RLock
1✔
261
    router: FunctionUrlRouter
1✔
262
    esm_workers: dict[str, EsmWorker]
1✔
263
    layer_fetcher: LayerFetcher | None
1✔
264

265
    def __init__(self) -> None:
1✔
266
        self.lambda_service = LambdaService()
1✔
267
        self.create_fn_lock = threading.RLock()
1✔
268
        self.create_layer_lock = threading.RLock()
1✔
269
        self.router = FunctionUrlRouter(ROUTER, self.lambda_service)
1✔
270
        self.esm_workers = {}
1✔
271
        self.layer_fetcher = None
1✔
272
        lambda_hooks.inject_layer_fetcher.run(self)
1✔
273

274
    def accept_state_visitor(self, visitor: StateVisitor):
1✔
UNCOV
275
        visitor.visit(lambda_stores)
×
276

277
    def on_before_start(self):
1✔
278
        # Attempt to start the Lambda Debug Mode session object.
279
        try:
1✔
280
            lambda_debug_mode_session = LambdaDebugModeSession.get()
1✔
281
            lambda_debug_mode_session.ensure_running()
1✔
UNCOV
282
        except Exception as ex:
×
UNCOV
283
            LOG.error(
×
284
                "Unexpected error encountered when attempting to initialise Lambda Debug Mode '%s'.",
285
                ex,
286
            )
287

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

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

294
    def on_before_state_load(self):
1✔
295
        self.lambda_service.stop()
×
296

297
    def on_after_state_load(self):
1✔
298
        self.lambda_service = LambdaService()
×
299
        self.router.lambda_service = self.lambda_service
×
300

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

344
                            manager = self.lambda_service.get_lambda_version_manager(fn_arn)
×
345
                            manager.update_provisioned_concurrency_config(
×
346
                                provisioned_config.provisioned_concurrent_executions
347
                            )
UNCOV
348
                        except Exception:
×
UNCOV
349
                            LOG.warning(
×
350
                                "Failed to restore provisioned concurrency %s for function %s",
351
                                provisioned_config,
352
                                fn_arn,
353
                                exc_info=True,
354
                            )
355

UNCOV
356
                for esm in state.event_source_mappings.values():
×
357
                    # Restores event source workers
358
                    function_arn = esm.get("FunctionArn")
×
359

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

UNCOV
372
                    function_version = get_function_version_from_arn(function_arn)
×
UNCOV
373
                    function_role = function_version.config.role
×
374

375
                    is_esm_enabled = esm.get("State", EsmState.DISABLED) not in (
×
376
                        EsmState.DISABLED,
377
                        EsmState.DISABLING,
378
                    )
UNCOV
379
                    esm_worker = EsmWorkerFactory(
×
380
                        esm, function_role, is_esm_enabled
381
                    ).get_esm_worker()
382

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

389
    def on_after_init(self):
1✔
390
        self.router.register_routes()
1✔
391
        get_runtime_executor().validate_environment()
1✔
392

393
    def on_before_stop(self) -> None:
1✔
394
        for esm_worker in self.esm_workers.values():
1✔
395
            esm_worker.stop_for_shutdown()
1✔
396

397
        # TODO: should probably unregister routes?
398
        self.lambda_service.stop()
1✔
399
        # Attempt to signal to the Lambda Debug Mode session object to stop.
400
        try:
1✔
401
            lambda_debug_mode_session = LambdaDebugModeSession.get()
1✔
402
            lambda_debug_mode_session.signal_stop()
1✔
UNCOV
403
        except Exception as ex:
×
UNCOV
404
            LOG.error(
×
405
                "Unexpected error encountered when attempting to signal Lambda Debug Mode to stop '%s'.",
406
                ex,
407
            )
408

409
    @staticmethod
1✔
410
    def _get_function(function_name: str, account_id: str, region: str) -> Function:
1✔
411
        state = lambda_stores[account_id][region]
1✔
412
        function = state.functions.get(function_name)
1✔
413
        if not function:
1✔
414
            arn = api_utils.unqualified_lambda_arn(
1✔
415
                function_name=function_name,
416
                account=account_id,
417
                region=region,
418
            )
419
            raise ResourceNotFoundException(
1✔
420
                f"Function not found: {arn}",
421
                Type="User",
422
            )
423
        return function
1✔
424

425
    @staticmethod
1✔
426
    def _get_esm(uuid: str, account_id: str, region: str) -> EventSourceMappingConfiguration:
1✔
427
        state = lambda_stores[account_id][region]
1✔
428
        esm = state.event_source_mappings.get(uuid)
1✔
429
        if not esm:
1✔
430
            arn = lambda_event_source_mapping_arn(uuid, account_id, region)
1✔
431
            raise ResourceNotFoundException(
1✔
432
                f"Event source mapping not found: {arn}",
433
                Type="User",
434
            )
435
        return esm
1✔
436

437
    @staticmethod
1✔
438
    def _validate_qualifier_expression(qualifier: str) -> None:
1✔
439
        if error_messages := api_utils.validate_qualifier(qualifier):
1✔
UNCOV
440
            raise ValidationException(
×
441
                message=api_utils.construct_validation_exception_message(error_messages)
442
            )
443

444
    @staticmethod
1✔
445
    def _resolve_fn_qualifier(resolved_fn: Function, qualifier: str | None) -> tuple[str, str]:
1✔
446
        """Attempts to resolve a given qualifier and returns a qualifier that exists or
447
        raises an appropriate ResourceNotFoundException.
448

449
        :param resolved_fn: The resolved lambda function
450
        :param qualifier: The qualifier to be resolved or None
451
        :return: Tuple of (resolved qualifier, function arn either qualified or unqualified)"""
452
        function_name = resolved_fn.function_name
1✔
453
        # assuming function versions need to live in the same account and region
454
        account_id = resolved_fn.latest().id.account
1✔
455
        region = resolved_fn.latest().id.region
1✔
456
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
457
        if qualifier is not None:
1✔
458
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
459
            if api_utils.qualifier_is_alias(qualifier):
1✔
460
                if qualifier not in resolved_fn.aliases:
1✔
461
                    raise ResourceNotFoundException(f"Cannot find alias arn: {fn_arn}", Type="User")
1✔
462
            elif api_utils.qualifier_is_version(qualifier) or qualifier == "$LATEST":
1✔
463
                if qualifier not in resolved_fn.versions:
1✔
464
                    raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
465
            else:
466
                # matches qualifier pattern but invalid alias or version
467
                raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
468
        resolved_qualifier = qualifier or "$LATEST"
1✔
469
        return resolved_qualifier, fn_arn
1✔
470

471
    @staticmethod
1✔
472
    def _function_revision_id(resolved_fn: Function, resolved_qualifier: str) -> str:
1✔
473
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
474
            return resolved_fn.aliases[resolved_qualifier].revision_id
1✔
475
        # Assumes that a non-alias is a version
476
        else:
477
            return resolved_fn.versions[resolved_qualifier].config.revision_id
1✔
478

479
    def _resolve_vpc_id(self, account_id: str, region_name: str, subnet_id: str) -> str:
1✔
480
        ec2_client = connect_to(aws_access_key_id=account_id, region_name=region_name).ec2
1✔
481
        try:
1✔
482
            return ec2_client.describe_subnets(SubnetIds=[subnet_id])["Subnets"][0]["VpcId"]
1✔
483
        except ec2_client.exceptions.ClientError as e:
1✔
484
            code = e.response["Error"]["Code"]
1✔
485
            message = e.response["Error"]["Message"]
1✔
486
            raise InvalidParameterValueException(
1✔
487
                f"Error occurred while DescribeSubnets. EC2 Error Code: {code}. EC2 Error Message: {message}",
488
                Type="User",
489
            )
490

491
    def _build_vpc_config(
1✔
492
        self,
493
        account_id: str,
494
        region_name: str,
495
        vpc_config: Optional[dict] = None,
496
    ) -> VpcConfig | None:
497
        if not vpc_config or not is_api_enabled("ec2"):
1✔
498
            return None
1✔
499

500
        subnet_ids = vpc_config.get("SubnetIds", [])
1✔
501
        if subnet_ids is not None and len(subnet_ids) == 0:
1✔
502
            return VpcConfig(vpc_id="", security_group_ids=[], subnet_ids=[])
1✔
503

504
        subnet_id = subnet_ids[0]
1✔
505
        if not bool(SUBNET_ID_REGEX.match(subnet_id)):
1✔
506
            raise ValidationException(
1✔
507
                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]*$]"
508
            )
509

510
        return VpcConfig(
1✔
511
            vpc_id=self._resolve_vpc_id(account_id, region_name, subnet_id),
512
            security_group_ids=vpc_config.get("SecurityGroupIds", []),
513
            subnet_ids=subnet_ids,
514
        )
515

516
    def _create_version_model(
1✔
517
        self,
518
        function_name: str,
519
        region: str,
520
        account_id: str,
521
        description: str | None = None,
522
        revision_id: str | None = None,
523
        code_sha256: str | None = None,
524
    ) -> tuple[FunctionVersion, bool]:
525
        """
526
        Release a new version to the model if all restrictions are met.
527
        Restrictions:
528
          - CodeSha256, if provided, must equal the current latest version code hash
529
          - RevisionId, if provided, must equal the current latest version revision id
530
          - Some changes have been done to the latest version since last publish
531
        Will return a tuple of the version, and whether the version was published (True) or the latest available version was taken (False).
532
        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.
533

534
        :param function_name: Function name to be published
535
        :param region: Region of the function
536
        :param account_id: Account of the function
537
        :param description: new description of the version (will be the description of the function if missing)
538
        :param revision_id: Revision id, function will raise error if it does not match latest revision id
539
        :param code_sha256: Code sha256, function will raise error if it does not match latest code hash
540
        :return: Tuple of (published version, whether version was released or last released version returned, since nothing changed)
541
        """
542
        current_latest_version = get_function_version(
1✔
543
            function_name=function_name, qualifier="$LATEST", account_id=account_id, region=region
544
        )
545
        if revision_id and current_latest_version.config.revision_id != revision_id:
1✔
546
            raise PreconditionFailedException(
1✔
547
                "The Revision Id provided does not match the latest Revision Id. Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
548
                Type="User",
549
            )
550

551
        # check if code hashes match if they are specified
552
        current_hash = (
1✔
553
            current_latest_version.config.code.code_sha256
554
            if current_latest_version.config.package_type == PackageType.Zip
555
            else current_latest_version.config.image.code_sha256
556
        )
557
        # if the code is a zip package and hot reloaded (hot reloading is currently only supported for zip packagetypes)
558
        # we cannot enforce the codesha256 check
559
        is_hot_reloaded_zip_package = (
1✔
560
            current_latest_version.config.package_type == PackageType.Zip
561
            and current_latest_version.config.code.is_hot_reloading()
562
        )
563
        if code_sha256 and current_hash != code_sha256 and not is_hot_reloaded_zip_package:
1✔
564
            raise InvalidParameterValueException(
1✔
565
                f"CodeSHA256 ({code_sha256}) is different from current CodeSHA256 in $LATEST ({current_hash}). Please try again with the CodeSHA256 in $LATEST.",
566
                Type="User",
567
            )
568

569
        state = lambda_stores[account_id][region]
1✔
570
        function = state.functions.get(function_name)
1✔
571
        changes = {}
1✔
572
        if description is not None:
1✔
573
            changes["description"] = description
1✔
574
        # TODO copy environment instead of restarting one, get rid of all the "Pending"s
575

576
        with function.lock:
1✔
577
            if function.next_version > 1 and (
1✔
578
                prev_version := function.versions.get(str(function.next_version - 1))
579
            ):
580
                if (
1✔
581
                    prev_version.config.internal_revision
582
                    == current_latest_version.config.internal_revision
583
                ):
584
                    return prev_version, False
1✔
585
            # TODO check if there was a change since last version
586
            next_version = str(function.next_version)
1✔
587
            function.next_version += 1
1✔
588
            new_id = VersionIdentifier(
1✔
589
                function_name=function_name,
590
                qualifier=next_version,
591
                region=region,
592
                account=account_id,
593
            )
594
            apply_on = current_latest_version.config.snap_start["ApplyOn"]
1✔
595
            optimization_status = SnapStartOptimizationStatus.Off
1✔
596
            if apply_on == SnapStartApplyOn.PublishedVersions:
1✔
597
                optimization_status = SnapStartOptimizationStatus.On
1✔
598
            snap_start = SnapStartResponse(
1✔
599
                ApplyOn=apply_on,
600
                OptimizationStatus=optimization_status,
601
            )
602
            new_version = dataclasses.replace(
1✔
603
                current_latest_version,
604
                config=dataclasses.replace(
605
                    current_latest_version.config,
606
                    last_update=None,  # versions never have a last update status
607
                    state=VersionState(
608
                        state=State.Pending,
609
                        code=StateReasonCode.Creating,
610
                        reason="The function is being created.",
611
                    ),
612
                    snap_start=snap_start,
613
                    **changes,
614
                ),
615
                id=new_id,
616
            )
617
            function.versions[next_version] = new_version
1✔
618
        return new_version, True
1✔
619

620
    def _publish_version_from_existing_version(
1✔
621
        self,
622
        function_name: str,
623
        region: str,
624
        account_id: str,
625
        description: str | None = None,
626
        revision_id: str | None = None,
627
        code_sha256: str | None = None,
628
    ) -> FunctionVersion:
629
        """
630
        Publish version from an existing, already initialized LATEST
631

632
        :param function_name: Function name
633
        :param region: region
634
        :param account_id: account id
635
        :param description: description
636
        :param revision_id: revision id (check if current version matches)
637
        :param code_sha256: code sha (check if current code matches)
638
        :return: new version
639
        """
640
        new_version, changed = self._create_version_model(
1✔
641
            function_name=function_name,
642
            region=region,
643
            account_id=account_id,
644
            description=description,
645
            revision_id=revision_id,
646
            code_sha256=code_sha256,
647
        )
648
        if not changed:
1✔
649
            return new_version
1✔
650
        self.lambda_service.publish_version(new_version)
1✔
651
        state = lambda_stores[account_id][region]
1✔
652
        function = state.functions.get(function_name)
1✔
653
        # TODO: re-evaluate data model to prevent this dirty hack just for bumping the revision id
654
        latest_version = function.versions["$LATEST"]
1✔
655
        function.versions["$LATEST"] = dataclasses.replace(
1✔
656
            latest_version, config=dataclasses.replace(latest_version.config)
657
        )
658
        return function.versions.get(new_version.id.qualifier)
1✔
659

660
    def _publish_version_with_changes(
1✔
661
        self,
662
        function_name: str,
663
        region: str,
664
        account_id: str,
665
        description: str | None = None,
666
        revision_id: str | None = None,
667
        code_sha256: str | None = None,
668
    ) -> FunctionVersion:
669
        """
670
        Publish version together with a new latest version (publish on create / update)
671

672
        :param function_name: Function name
673
        :param region: region
674
        :param account_id: account id
675
        :param description: description
676
        :param revision_id: revision id (check if current version matches)
677
        :param code_sha256: code sha (check if current code matches)
678
        :return: new version
679
        """
680
        new_version, changed = self._create_version_model(
1✔
681
            function_name=function_name,
682
            region=region,
683
            account_id=account_id,
684
            description=description,
685
            revision_id=revision_id,
686
            code_sha256=code_sha256,
687
        )
688
        if not changed:
1✔
UNCOV
689
            return new_version
×
690
        self.lambda_service.create_function_version(new_version)
1✔
691
        return new_version
1✔
692

693
    @staticmethod
1✔
694
    def _verify_env_variables(env_vars: dict[str, str]):
1✔
695
        dumped_env_vars = json.dumps(env_vars, separators=(",", ":"))
1✔
696
        if (
1✔
697
            len(dumped_env_vars.encode("utf-8"))
698
            > config.LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES
699
        ):
700
            raise InvalidParameterValueException(
1✔
701
                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}",
702
                Type="User",
703
            )
704

705
    @staticmethod
1✔
706
    def _validate_snapstart(snap_start: SnapStart, runtime: Runtime):
1✔
707
        apply_on = snap_start.get("ApplyOn")
1✔
708
        if apply_on not in [
1✔
709
            SnapStartApplyOn.PublishedVersions,
710
            SnapStartApplyOn.None_,
711
        ]:
712
            raise ValidationException(
1✔
713
                f"1 validation error detected: Value '{apply_on}' at 'snapStart.applyOn' failed to satisfy constraint: Member must satisfy enum value set: [PublishedVersions, None]"
714
            )
715

716
    def _validate_layers(self, new_layers: list[str], region: str, account_id: str):
1✔
717
        if len(new_layers) > LAMBDA_LAYERS_LIMIT_PER_FUNCTION:
1✔
718
            raise InvalidParameterValueException(
1✔
719
                "Cannot reference more than 5 layers.", Type="User"
720
            )
721

722
        visited_layers = dict()
1✔
723
        for layer_version_arn in new_layers:
1✔
724
            (
1✔
725
                layer_region,
726
                layer_account_id,
727
                layer_name,
728
                layer_version_str,
729
            ) = api_utils.parse_layer_arn(layer_version_arn)
730
            if layer_version_str is None:
1✔
731
                raise ValidationException(
1✔
732
                    f"1 validation error detected: Value '[{layer_version_arn}]'"
733
                    + 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]",
734
                )
735

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

768
                    layer = self.layer_fetcher.fetch_layer(layer_version_arn)
×
UNCOV
769
                    if layer is None:
×
770
                        # TODO: detect user or role from context when IAM users are implemented
UNCOV
771
                        user = "user/localstack-testing"
×
UNCOV
772
                        raise AccessDeniedException(
×
773
                            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"
774
                        )
775

776
                    # Distinguish between new layer and new layer version
UNCOV
777
                    if layer_version is None:
×
778
                        # Create whole layer from scratch
UNCOV
779
                        state.layers[layer_name] = layer
×
780
                    else:
781
                        # Create layer version if another version of the same layer already exists
UNCOV
782
                        state.layers[layer_name].layer_versions[layer_version_str] = (
×
783
                            layer.layer_versions.get(layer_version_str)
784
                        )
785

786
            # only the first two matches in the array are considered for the error message
787
            layer_arn = ":".join(layer_version_arn.split(":")[:-1])
1✔
788
            if layer_arn in visited_layers:
1✔
789
                conflict_layer_version_arn = visited_layers[layer_arn]
1✔
790
                raise InvalidParameterValueException(
1✔
791
                    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.",
792
                    Type="User",
793
                )
794
            visited_layers[layer_arn] = layer_version_arn
1✔
795

796
    @staticmethod
1✔
797
    def map_layers(new_layers: list[str]) -> list[LayerVersion]:
1✔
798
        layers = []
1✔
799
        for layer_version_arn in new_layers:
1✔
800
            region_name, account_id, layer_name, layer_version = api_utils.parse_layer_arn(
1✔
801
                layer_version_arn
802
            )
803
            layer = lambda_stores[account_id][region_name].layers.get(layer_name)
1✔
804
            layer_version = layer.layer_versions.get(layer_version)
1✔
805
            layers.append(layer_version)
1✔
806
        return layers
1✔
807

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

819
    def put_function_recursion_config(
1✔
820
        self,
821
        context: RequestContext,
822
        function_name: UnqualifiedFunctionName,
823
        recursive_loop: RecursiveLoop,
824
        **kwargs,
825
    ) -> PutFunctionRecursionConfigResponse:
826
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
827
        function_name = api_utils.get_function_name(function_name, context)
1✔
828

829
        fn = self._get_function(function_name=function_name, region=region, account_id=account_id)
1✔
830

831
        allowed_values = list(RecursiveLoop.__members__.values())
1✔
832
        if recursive_loop not in allowed_values:
1✔
833
            raise ValidationException(
1✔
834
                f"1 validation error detected: Value '{recursive_loop}' at 'recursiveLoop' failed to satisfy constraint: "
835
                f"Member must satisfy enum value set: [Terminate, Allow]"
836
            )
837

838
        fn.recursive_loop = recursive_loop
1✔
839
        return PutFunctionRecursionConfigResponse(RecursiveLoop=fn.recursive_loop)
1✔
840

841
    @handler(operation="CreateFunction", expand=False)
1✔
842
    def create_function(
1✔
843
        self,
844
        context: RequestContext,
845
        request: CreateFunctionRequest,
846
    ) -> FunctionConfiguration:
847
        context_region = context.region
1✔
848
        context_account_id = context.account_id
1✔
849

850
        zip_file = request.get("Code", {}).get("ZipFile")
1✔
851
        if zip_file and len(zip_file) > config.LAMBDA_LIMITS_CODE_SIZE_ZIPPED:
1✔
852
            raise RequestEntityTooLargeException(
1✔
853
                f"Zipped size must be smaller than {config.LAMBDA_LIMITS_CODE_SIZE_ZIPPED} bytes"
854
            )
855

856
        if context.request.content_length > config.LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE:
1✔
857
            raise RequestEntityTooLargeException(
1✔
858
                f"Request must be smaller than {config.LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE} bytes for the CreateFunction operation"
859
            )
860

861
        if architectures := request.get("Architectures"):
1✔
862
            if len(architectures) != 1:
1✔
863
                raise ValidationException(
1✔
864
                    f"1 validation error detected: Value '[{', '.join(architectures)}]' at 'architectures' failed to "
865
                    f"satisfy constraint: Member must have length less than or equal to 1",
866
                )
867
            if architectures[0] not in ARCHITECTURES:
1✔
868
                raise ValidationException(
1✔
869
                    f"1 validation error detected: Value '[{', '.join(architectures)}]' at 'architectures' failed to "
870
                    f"satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: "
871
                    f"[x86_64, arm64], Member must not be null]",
872
                )
873

874
        if env_vars := request.get("Environment", {}).get("Variables"):
1✔
875
            self._verify_env_variables(env_vars)
1✔
876

877
        if layers := request.get("Layers", []):
1✔
878
            self._validate_layers(layers, region=context_region, account_id=context_account_id)
1✔
879

880
        if not api_utils.is_role_arn(request.get("Role")):
1✔
881
            raise ValidationException(
1✔
882
                f"1 validation error detected: Value '{request.get('Role')}'"
883
                + " 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+=,.@\\-_/]+"
884
            )
885
        if not self.lambda_service.can_assume_role(request.get("Role"), context.region):
1✔
UNCOV
886
            raise InvalidParameterValueException(
×
887
                "The role defined for the function cannot be assumed by Lambda.", Type="User"
888
            )
889
        package_type = request.get("PackageType", PackageType.Zip)
1✔
890
        runtime = request.get("Runtime")
1✔
891
        self._validate_runtime(package_type, runtime)
1✔
892

893
        request_function_name = request.get("FunctionName")
1✔
894

895
        function_name, *_ = api_utils.get_name_and_qualifier(
1✔
896
            function_arn_or_name=request_function_name,
897
            qualifier=None,
898
            context=context,
899
        )
900

901
        if runtime in DEPRECATED_RUNTIMES:
1✔
902
            LOG.warning(
1✔
903
                "The Lambda runtime %s} is deprecated. "
904
                "Please upgrade the runtime for the function %s: "
905
                "https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html",
906
                runtime,
907
                function_name,
908
            )
909
        if snap_start := request.get("SnapStart"):
1✔
910
            self._validate_snapstart(snap_start, runtime)
1✔
911
        state = lambda_stores[context_account_id][context_region]
1✔
912

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

961
                image_config_req = request.get("ImageConfig", {})
1✔
962
                image_config = ImageConfig(
1✔
963
                    command=image_config_req.get("Command"),
964
                    entrypoint=image_config_req.get("EntryPoint"),
965
                    working_directory=image_config_req.get("WorkingDirectory"),
966
                )
967
                # Runtime management controls are not available when providing a custom image
968
                runtime_version_config = None
1✔
969
            if "LoggingConfig" in request:
1✔
970
                logging_config = request["LoggingConfig"]
1✔
971
                LOG.warning(
1✔
972
                    "Advanced Lambda Logging Configuration is currently mocked "
973
                    "and will not impact the logging behavior. "
974
                    "Please create a feature request if needed."
975
                )
976

977
                # when switching to JSON, app and system level log is auto set to INFO
978
                if logging_config.get("LogFormat", None) == LogFormat.JSON:
1✔
979
                    logging_config = {
1✔
980
                        "ApplicationLogLevel": "INFO",
981
                        "SystemLogLevel": "INFO",
982
                        "LogGroup": f"/aws/lambda/{function_name}",
983
                    } | logging_config
984
                else:
UNCOV
985
                    logging_config = (
×
986
                        LoggingConfig(
987
                            LogFormat=LogFormat.Text, LogGroup=f"/aws/lambda/{function_name}"
988
                        )
989
                        | logging_config
990
                    )
991

992
            else:
993
                logging_config = LoggingConfig(
1✔
994
                    LogFormat=LogFormat.Text, LogGroup=f"/aws/lambda/{function_name}"
995
                )
996

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

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

1046
        if request.get("Publish"):
1✔
1047
            version = self._publish_version_with_changes(
1✔
1048
                function_name=function_name, region=context_region, account_id=context_account_id
1049
            )
1050

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

1065
        return api_utils.map_config_out(
1✔
1066
            version, return_qualified_arn=False, return_update_status=False
1067
        )
1068

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

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

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

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

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

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

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

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

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

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

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

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

1150
        if "Description" in request:
1✔
1151
            replace_kwargs["description"] = request["Description"]
1✔
1152

1153
        if "Timeout" in request:
1✔
1154
            replace_kwargs["timeout"] = request["Timeout"]
1✔
1155

1156
        if "MemorySize" in request:
1✔
1157
            replace_kwargs["memory_size"] = request["MemorySize"]
1✔
1158

1159
        if "DeadLetterConfig" in request:
1✔
1160
            replace_kwargs["dead_letter_arn"] = request.get("DeadLetterConfig", {}).get("TargetArn")
1✔
1161

1162
        if vpc_config := request.get("VpcConfig"):
1✔
1163
            replace_kwargs["vpc_config"] = self._build_vpc_config(account_id, region, vpc_config)
1✔
1164

1165
        if "Handler" in request:
1✔
1166
            replace_kwargs["handler"] = request["Handler"]
1✔
1167

1168
        if "Runtime" in request:
1✔
1169
            runtime = request["Runtime"]
1✔
1170

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

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

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

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

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

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

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

1228
            last_config = latest_version_config.logging_config
1✔
1229

1230
            # add partial update
1231
            new_logging_config = last_config | logging_config
1✔
1232

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

1241
            replace_kwargs["logging_config"] = new_logging_config
1✔
1242

1243
        if "TracingConfig" in request:
1✔
UNCOV
1244
            new_mode = request.get("TracingConfig", {}).get("Mode")
×
UNCOV
1245
            if new_mode:
×
UNCOV
1246
                replace_kwargs["tracing_config_mode"] = new_mode
×
1247

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

1265
        return api_utils.map_config_out(new_latest_version)
1✔
1266

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

1276
        function_name = request.get("FunctionName")
1✔
1277
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1278
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
1279

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

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

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

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

1335
        old_function_version = function.versions.get("$LATEST")
1✔
1336
        replace_kwargs = {"code": code} if code else {"image": image}
1✔
1337

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1572
        time_before = time.perf_counter()
1✔
1573
        try:
1✔
1574
            invocation_result = self.lambda_service.invoke(
1✔
1575
                function_name=function_name,
1576
                qualifier=qualifier,
1577
                region=region,
1578
                account_id=account_id,
1579
                invocation_type=invocation_type,
1580
                client_context=client_context,
1581
                request_id=context.request_id,
1582
                trace_context=context.trace_context,
1583
                payload=payload.read() if payload else None,
1584
            )
1585
        except ServiceException:
1✔
1586
            raise
1✔
1587
        except EnvironmentStartupTimeoutException as e:
1✔
1588
            raise LambdaServiceException("Internal error while executing lambda") from e
1✔
1589
        except Exception as e:
1✔
1590
            LOG.error("Error while invoking lambda", exc_info=e)
1✔
1591
            raise LambdaServiceException("Internal error while executing lambda") from e
1✔
1592

1593
        if invocation_type == InvocationType.Event:
1✔
1594
            # This happens when invocation type is event
1595
            return InvocationResponse(StatusCode=202)
1✔
1596
        if invocation_type == InvocationType.DryRun:
1✔
1597
            # This happens when invocation type is dryrun
1598
            return InvocationResponse(StatusCode=204)
1✔
1599
        LOG.debug("Lambda invocation duration: %0.2fms", (time.perf_counter() - time_before) * 1000)
1✔
1600

1601
        response = InvocationResponse(
1✔
1602
            StatusCode=200,
1603
            Payload=invocation_result.payload,
1604
            ExecutedVersion=invocation_result.executed_version,
1605
        )
1606

1607
        if invocation_result.is_error:
1✔
1608
            response["FunctionError"] = "Unhandled"
1✔
1609

1610
        if log_type == LogType.Tail:
1✔
1611
            response["LogResult"] = to_str(
1✔
1612
                base64.b64encode(to_bytes(invocation_result.logs)[-4096:])
1613
            )
1614

1615
        return response
1✔
1616

1617
    # Version operations
1618
    def publish_version(
1✔
1619
        self,
1620
        context: RequestContext,
1621
        function_name: FunctionName,
1622
        code_sha256: String = None,
1623
        description: Description = None,
1624
        revision_id: String = None,
1625
        **kwargs,
1626
    ) -> FunctionConfiguration:
1627
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1628
        function_name = api_utils.get_function_name(function_name, context)
1✔
1629
        new_version = self._publish_version_from_existing_version(
1✔
1630
            function_name=function_name,
1631
            description=description,
1632
            account_id=account_id,
1633
            region=region,
1634
            revision_id=revision_id,
1635
            code_sha256=code_sha256,
1636
        )
1637
        return api_utils.map_config_out(new_version, return_qualified_arn=True)
1✔
1638

1639
    def list_versions_by_function(
1✔
1640
        self,
1641
        context: RequestContext,
1642
        function_name: NamespacedFunctionName,
1643
        marker: String = None,
1644
        max_items: MaxListItems = None,
1645
        **kwargs,
1646
    ) -> ListVersionsByFunctionResponse:
1647
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1648
        function_name = api_utils.get_function_name(function_name, context)
1✔
1649
        function = self._get_function(
1✔
1650
            function_name=function_name, region=region, account_id=account_id
1651
        )
1652
        versions = [
1✔
1653
            api_utils.map_to_list_response(
1654
                api_utils.map_config_out(version=version, return_qualified_arn=True)
1655
            )
1656
            for version in function.versions.values()
1657
        ]
1658
        items = PaginatedList(versions)
1✔
1659
        page, token = items.get_page(
1✔
1660
            lambda item: item,
1661
            marker,
1662
            max_items,
1663
        )
1664
        return ListVersionsByFunctionResponse(Versions=page, NextMarker=token)
1✔
1665

1666
    # Alias
1667

1668
    def _create_routing_config_model(
1✔
1669
        self, routing_config_dict: dict[str, float], function_version: FunctionVersion
1670
    ):
1671
        if len(routing_config_dict) > 1:
1✔
1672
            raise InvalidParameterValueException(
1✔
1673
                "Number of items in AdditionalVersionWeights cannot be greater than 1",
1674
                Type="User",
1675
            )
1676
        # should be exactly one item here, still iterating, might be supported in the future
1677
        for key, value in routing_config_dict.items():
1✔
1678
            if value < 0.0 or value >= 1.0:
1✔
1679
                raise ValidationException(
1✔
1680
                    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]"
1681
                )
1682
            if key == function_version.id.qualifier:
1✔
1683
                raise InvalidParameterValueException(
1✔
1684
                    f"Invalid function version {function_version.id.qualifier}. Function version {function_version.id.qualifier} is already included in routing configuration.",
1685
                    Type="User",
1686
                )
1687
            # check if version target is latest, then no routing config is allowed
1688
            if function_version.id.qualifier == "$LATEST":
1✔
1689
                raise InvalidParameterValueException(
1✔
1690
                    "$LATEST is not supported for an alias pointing to more than 1 version"
1691
                )
1692
            if not api_utils.qualifier_is_version(key):
1✔
1693
                raise ValidationException(
1✔
1694
                    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]"
1695
                )
1696

1697
            # checking if the version in the config exists
1698
            get_function_version(
1✔
1699
                function_name=function_version.id.function_name,
1700
                qualifier=key,
1701
                region=function_version.id.region,
1702
                account_id=function_version.id.account,
1703
            )
1704
        return AliasRoutingConfig(version_weights=routing_config_dict)
1✔
1705

1706
    def create_alias(
1✔
1707
        self,
1708
        context: RequestContext,
1709
        function_name: FunctionName,
1710
        name: Alias,
1711
        function_version: Version,
1712
        description: Description = None,
1713
        routing_config: AliasRoutingConfiguration = None,
1714
        **kwargs,
1715
    ) -> AliasConfiguration:
1716
        if not api_utils.qualifier_is_alias(name):
1✔
1717
            raise ValidationException(
1✔
1718
                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-_]+)"
1719
            )
1720

1721
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1722
        function_name = api_utils.get_function_name(function_name, context)
1✔
1723
        target_version = get_function_version(
1✔
1724
            function_name=function_name,
1725
            qualifier=function_version,
1726
            region=region,
1727
            account_id=account_id,
1728
        )
1729
        function = self._get_function(
1✔
1730
            function_name=function_name, region=region, account_id=account_id
1731
        )
1732
        # description is always present, if not specified it's an empty string
1733
        description = description or ""
1✔
1734
        with function.lock:
1✔
1735
            if existing_alias := function.aliases.get(name):
1✔
1736
                raise ResourceConflictException(
1✔
1737
                    f"Alias already exists: {api_utils.map_alias_out(alias=existing_alias, function=function)['AliasArn']}",
1738
                    Type="User",
1739
                )
1740
            # checking if the version exists
1741
            routing_configuration = None
1✔
1742
            if routing_config and (
1✔
1743
                routing_config_dict := routing_config.get("AdditionalVersionWeights")
1744
            ):
1745
                routing_configuration = self._create_routing_config_model(
1✔
1746
                    routing_config_dict, target_version
1747
                )
1748

1749
            alias = VersionAlias(
1✔
1750
                name=name,
1751
                function_version=function_version,
1752
                description=description,
1753
                routing_configuration=routing_configuration,
1754
            )
1755
            function.aliases[name] = alias
1✔
1756
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
1757

1758
    def list_aliases(
1✔
1759
        self,
1760
        context: RequestContext,
1761
        function_name: FunctionName,
1762
        function_version: Version = None,
1763
        marker: String = None,
1764
        max_items: MaxListItems = None,
1765
        **kwargs,
1766
    ) -> ListAliasesResponse:
1767
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1768
        function_name = api_utils.get_function_name(function_name, context)
1✔
1769
        function = self._get_function(
1✔
1770
            function_name=function_name, region=region, account_id=account_id
1771
        )
1772
        aliases = [
1✔
1773
            api_utils.map_alias_out(alias, function)
1774
            for alias in function.aliases.values()
1775
            if function_version is None or alias.function_version == function_version
1776
        ]
1777

1778
        aliases = PaginatedList(aliases)
1✔
1779
        page, token = aliases.get_page(
1✔
1780
            lambda alias: alias["AliasArn"],
1781
            marker,
1782
            max_items,
1783
        )
1784

1785
        return ListAliasesResponse(Aliases=page, NextMarker=token)
1✔
1786

1787
    def delete_alias(
1✔
1788
        self, context: RequestContext, function_name: FunctionName, name: Alias, **kwargs
1789
    ) -> None:
1790
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1791
        function_name = api_utils.get_function_name(function_name, context)
1✔
1792
        function = self._get_function(
1✔
1793
            function_name=function_name, region=region, account_id=account_id
1794
        )
1795
        version_alias = function.aliases.pop(name, None)
1✔
1796

1797
        # cleanup related resources
1798
        if name in function.provisioned_concurrency_configs:
1✔
1799
            function.provisioned_concurrency_configs.pop(name)
1✔
1800

1801
        # TODO: Allow for deactivating/unregistering specific Lambda URLs
1802
        if version_alias and name in function.function_url_configs:
1✔
1803
            url_config = function.function_url_configs.pop(name)
1✔
1804
            LOG.debug(
1✔
1805
                "Stopping aliased Lambda Function URL %s for %s",
1806
                url_config.url,
1807
                url_config.function_name,
1808
            )
1809

1810
    def get_alias(
1✔
1811
        self, context: RequestContext, function_name: FunctionName, name: Alias, **kwargs
1812
    ) -> AliasConfiguration:
1813
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1814
        function_name = api_utils.get_function_name(function_name, context)
1✔
1815
        function = self._get_function(
1✔
1816
            function_name=function_name, region=region, account_id=account_id
1817
        )
1818
        if not (alias := function.aliases.get(name)):
1✔
1819
            raise ResourceNotFoundException(
1✔
1820
                f"Cannot find alias arn: {api_utils.qualified_lambda_arn(function_name=function_name, qualifier=name, region=region, account=account_id)}",
1821
                Type="User",
1822
            )
1823
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
1824

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

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

1872
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
1873

1874
    # =======================================
1875
    # ======= EVENT SOURCE MAPPINGS =========
1876
    # =======================================
1877
    def check_service_resource_exists(
1✔
1878
        self, service: str, resource_arn: str, function_arn: str, function_role_arn: str
1879
    ):
1880
        """
1881
        Check if the service resource exists and if the function has access to it.
1882

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

1930
    @handler("CreateEventSourceMapping", expand=False)
1✔
1931
    def create_event_source_mapping(
1✔
1932
        self,
1933
        context: RequestContext,
1934
        request: CreateEventSourceMappingRequest,
1935
    ) -> EventSourceMappingConfiguration:
1936
        return self.create_event_source_mapping_v2(context, request)
1✔
1937

1938
    def create_event_source_mapping_v2(
1✔
1939
        self,
1940
        context: RequestContext,
1941
        request: CreateEventSourceMappingRequest,
1942
    ) -> EventSourceMappingConfiguration:
1943
        # Validations
1944
        function_arn, function_name, state, function_version, function_role = (
1✔
1945
            self.validate_event_source_mapping(context, request)
1946
        )
1947

1948
        esm_config = EsmConfigFactory(request, context, function_arn).get_esm_config()
1✔
1949

1950
        # Copy esm_config to avoid a race condition with potential async update in the store
1951
        state.event_source_mappings[esm_config["UUID"]] = esm_config.copy()
1✔
1952
        enabled = request.get("Enabled", True)
1✔
1953
        # TODO: check for potential async race condition update -> think about locking
1954
        esm_worker = EsmWorkerFactory(esm_config, function_role, enabled).get_esm_worker()
1✔
1955
        self.esm_workers[esm_worker.uuid] = esm_worker
1✔
1956
        # TODO: check StateTransitionReason, LastModified, LastProcessingResult (concurrent updates requires locking!)
1957
        if tags := request.get("Tags"):
1✔
1958
            self._store_tags(esm_config.get("EventSourceMappingArn"), tags)
1✔
1959
        esm_worker.create()
1✔
1960
        return esm_config
1✔
1961

1962
    def validate_event_source_mapping(self, context, request):
1✔
1963
        # TODO: test whether stream ARNs are valid sources for Pipes or ESM or whether only DynamoDB table ARNs work
1964
        is_create_esm_request = context.operation.name == self.create_event_source_mapping.operation
1✔
1965

1966
        if destination_config := request.get("DestinationConfig"):
1✔
1967
            if "OnSuccess" in destination_config:
1✔
1968
                raise InvalidParameterValueException(
1✔
1969
                    "Unsupported DestinationConfig parameter for given event source mapping type.",
1970
                    Type="User",
1971
                )
1972

1973
        service = None
1✔
1974
        if "SelfManagedEventSource" in request:
1✔
UNCOV
1975
            service = "kafka"
×
UNCOV
1976
            if "SourceAccessConfigurations" not in request:
×
UNCOV
1977
                raise InvalidParameterValueException(
×
1978
                    "Required 'sourceAccessConfigurations' parameter is missing.", Type="User"
1979
                )
1980
        if service is None and "EventSourceArn" not in request:
1✔
1981
            raise InvalidParameterValueException("Unrecognized event source.", Type="User")
1✔
1982
        if service is None:
1✔
1983
            service = extract_service_from_arn(request["EventSourceArn"])
1✔
1984

1985
        batch_size = api_utils.validate_and_set_batch_size(service, request.get("BatchSize"))
1✔
1986
        if service in ["dynamodb", "kinesis"]:
1✔
1987
            starting_position = request.get("StartingPosition")
1✔
1988
            if not starting_position:
1✔
1989
                raise InvalidParameterValueException(
1✔
1990
                    "1 validation error detected: Value null at 'startingPosition' failed to satisfy constraint: Member must not be null.",
1991
                    Type="User",
1992
                )
1993

1994
            if starting_position not in KinesisStreamStartPosition.__members__:
1✔
1995
                raise ValidationException(
1✔
1996
                    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]"
1997
                )
1998
            # AT_TIMESTAMP is not allowed for DynamoDB Streams
1999
            elif (
1✔
2000
                service == "dynamodb"
2001
                and starting_position not in DynamoDBStreamStartPosition.__members__
2002
            ):
2003
                raise InvalidParameterValueException(
1✔
2004
                    f"Unsupported starting position for arn type: {request['EventSourceArn']}",
2005
                    Type="User",
2006
                )
2007

2008
        if service in ["sqs", "sqs-fifo"]:
1✔
2009
            if batch_size > 10 and request.get("MaximumBatchingWindowInSeconds", 0) == 0:
1✔
2010
                raise InvalidParameterValueException(
1✔
2011
                    "Maximum batch window in seconds must be greater than 0 if maximum batch size is greater than 10",
2012
                    Type="User",
2013
                )
2014

2015
        if (filter_criteria := request.get("FilterCriteria")) is not None:
1✔
2016
            for filter_ in filter_criteria.get("Filters", []):
1✔
2017
                pattern_str = filter_.get("Pattern")
1✔
2018
                if not pattern_str or not isinstance(pattern_str, str):
1✔
UNCOV
2019
                    raise InvalidParameterValueException(
×
2020
                        "Invalid filter pattern definition.", Type="User"
2021
                    )
2022

2023
                if not validate_event_pattern(pattern_str):
1✔
2024
                    raise InvalidParameterValueException(
1✔
2025
                        "Invalid filter pattern definition.", Type="User"
2026
                    )
2027

2028
        # Can either have a FunctionName (i.e CreateEventSourceMapping request) or
2029
        # an internal EventSourceMappingConfiguration representation
2030
        request_function_name = request.get("FunctionName") or request.get("FunctionArn")
1✔
2031
        # can be either a partial arn or a full arn for the version/alias
2032
        function_name, qualifier, account, region = function_locators_from_arn(
1✔
2033
            request_function_name
2034
        )
2035
        # TODO: validate `context.region` vs. `region(request["FunctionName"])` vs. `region(request["EventSourceArn"])`
2036
        account = account or context.account_id
1✔
2037
        region = region or context.region
1✔
2038
        state = lambda_stores[account][region]
1✔
2039
        fn = state.functions.get(function_name)
1✔
2040
        if not fn:
1✔
2041
            raise InvalidParameterValueException("Function does not exist", Type="User")
1✔
2042

2043
        if qualifier:
1✔
2044
            # make sure the function version/alias exists
2045
            if api_utils.qualifier_is_alias(qualifier):
1✔
2046
                fn_alias = fn.aliases.get(qualifier)
1✔
2047
                if not fn_alias:
1✔
UNCOV
2048
                    raise Exception("unknown alias")  # TODO: cover via test
×
2049
            elif api_utils.qualifier_is_version(qualifier):
1✔
2050
                fn_version = fn.versions.get(qualifier)
1✔
2051
                if not fn_version:
1✔
2052
                    raise Exception("unknown version")  # TODO: cover via test
×
2053
            elif qualifier == "$LATEST":
1✔
2054
                pass
1✔
2055
            else:
UNCOV
2056
                raise Exception("invalid functionname")  # TODO: cover via test
×
2057
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account, region)
1✔
2058

2059
        else:
2060
            fn_arn = api_utils.unqualified_lambda_arn(function_name, account, region)
1✔
2061

2062
        function_version = get_function_version_from_arn(fn_arn)
1✔
2063
        function_role = function_version.config.role
1✔
2064

2065
        if source_arn := request.get("EventSourceArn"):
1✔
2066
            self.check_service_resource_exists(service, source_arn, fn_arn, function_role)
1✔
2067
        # Check we are validating a CreateEventSourceMapping request
2068
        if is_create_esm_request:
1✔
2069

2070
            def _get_mapping_sources(mapping: dict[str, Any]) -> list[str]:
1✔
2071
                if event_source_arn := mapping.get("EventSourceArn"):
1✔
2072
                    return [event_source_arn]
1✔
UNCOV
2073
                return (
×
2074
                    mapping.get("SelfManagedEventSource", {})
2075
                    .get("Endpoints", {})
2076
                    .get("KAFKA_BOOTSTRAP_SERVERS", [])
2077
                )
2078

2079
            # check for event source duplicates
2080
            # TODO: currently validated for sqs, kinesis, and dynamodb
2081
            service_id = load_service(service).service_id
1✔
2082
            for uuid, mapping in state.event_source_mappings.items():
1✔
2083
                mapping_sources = _get_mapping_sources(mapping)
1✔
2084
                request_sources = _get_mapping_sources(request)
1✔
2085
                if mapping["FunctionArn"] == fn_arn and (
1✔
2086
                    set(mapping_sources).intersection(request_sources)
2087
                ):
2088
                    if service == "sqs":
1✔
2089
                        # *shakes fist at SQS*
2090
                        raise ResourceConflictException(
1✔
2091
                            f'An event source mapping with {service_id} arn (" {mapping["EventSourceArn"]} ") '
2092
                            f'and function (" {function_name} ") already exists. Please update or delete the '
2093
                            f"existing mapping with UUID {uuid}",
2094
                            Type="User",
2095
                        )
2096
                    elif service == "kafka":
1✔
UNCOV
2097
                        if set(mapping["Topics"]).intersection(request["Topics"]):
×
UNCOV
2098
                            raise ResourceConflictException(
×
2099
                                f'An event source mapping with event source ("{",".join(request_sources)}"), '
2100
                                f'function ("{fn_arn}"), '
2101
                                f'topics ("{",".join(request["Topics"])}") already exists. Please update or delete the '
2102
                                f"existing mapping with UUID {uuid}",
2103
                                Type="User",
2104
                            )
2105
                    else:
2106
                        raise ResourceConflictException(
1✔
2107
                            f'The event source arn (" {mapping["EventSourceArn"]} ") and function '
2108
                            f'(" {function_name} ") provided mapping already exists. Please update or delete the '
2109
                            f"existing mapping with UUID {uuid}",
2110
                            Type="User",
2111
                        )
2112
        return fn_arn, function_name, state, function_version, function_role
1✔
2113

2114
    @handler("UpdateEventSourceMapping", expand=False)
1✔
2115
    def update_event_source_mapping(
1✔
2116
        self,
2117
        context: RequestContext,
2118
        request: UpdateEventSourceMappingRequest,
2119
    ) -> EventSourceMappingConfiguration:
2120
        return self.update_event_source_mapping_v2(context, request)
1✔
2121

2122
    def update_event_source_mapping_v2(
1✔
2123
        self,
2124
        context: RequestContext,
2125
        request: UpdateEventSourceMappingRequest,
2126
    ) -> EventSourceMappingConfiguration:
2127
        # TODO: test and implement this properly (quite complex with many validations and limitations!)
2128
        LOG.warning(
1✔
2129
            "Updating Lambda Event Source Mapping is in experimental state and not yet fully tested."
2130
        )
2131
        state = lambda_stores[context.account_id][context.region]
1✔
2132
        request_data = {**request}
1✔
2133
        uuid = request_data.pop("UUID", None)
1✔
2134
        if not uuid:
1✔
UNCOV
2135
            raise ResourceNotFoundException(
×
2136
                "The resource you requested does not exist.", Type="User"
2137
            )
2138
        old_event_source_mapping = state.event_source_mappings.get(uuid)
1✔
2139
        esm_worker = self.esm_workers.get(uuid)
1✔
2140
        if old_event_source_mapping is None or esm_worker is None:
1✔
2141
            raise ResourceNotFoundException(
1✔
2142
                "The resource you requested does not exist.", Type="User"
2143
            )  # TODO: test?
2144

2145
        # normalize values to overwrite
2146
        event_source_mapping = old_event_source_mapping | request_data
1✔
2147

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

2150
        # Validate the newly updated ESM object. We ignore the output here since we only care whether an Exception is raised.
2151
        function_arn, _, _, function_version, function_role = self.validate_event_source_mapping(
1✔
2152
            context, event_source_mapping
2153
        )
2154

2155
        # remove the FunctionName field
2156
        event_source_mapping.pop("FunctionName", None)
1✔
2157

2158
        if function_arn:
1✔
2159
            event_source_mapping["FunctionArn"] = function_arn
1✔
2160

2161
        # Only apply update if the desired state differs
2162
        enabled = request.get("Enabled")
1✔
2163
        if enabled is not None:
1✔
2164
            if enabled and old_event_source_mapping["State"] != EsmState.ENABLED:
1✔
2165
                event_source_mapping["State"] = EsmState.ENABLING
1✔
2166
            # TODO: What happens when trying to update during an update or failed state?!
2167
            elif not enabled and old_event_source_mapping["State"] == EsmState.ENABLED:
1✔
2168
                event_source_mapping["State"] = EsmState.DISABLING
1✔
2169
        else:
2170
            event_source_mapping["State"] = EsmState.UPDATING
1✔
2171

2172
        # To ensure parity, certain responses need to be immediately returned
2173
        temp_params["State"] = event_source_mapping["State"]
1✔
2174

2175
        state.event_source_mappings[uuid] = event_source_mapping
1✔
2176

2177
        # TODO: Currently, we re-create the entire ESM worker. Look into approach with better performance.
2178
        worker_factory = EsmWorkerFactory(
1✔
2179
            event_source_mapping, function_role, request.get("Enabled", esm_worker.enabled)
2180
        )
2181

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

2186
        # We should stop() the worker since the delete() will remove the ESM from the state mapping.
2187
        esm_worker.stop()
1✔
2188
        # This will either create an EsmWorker in the CREATING state if enabled. Otherwise, the DISABLING state is set.
2189
        updated_esm_worker.create()
1✔
2190

2191
        return {**event_source_mapping, **temp_params}
1✔
2192

2193
    def delete_event_source_mapping(
1✔
2194
        self, context: RequestContext, uuid: String, **kwargs
2195
    ) -> EventSourceMappingConfiguration:
2196
        state = lambda_stores[context.account_id][context.region]
1✔
2197
        event_source_mapping = state.event_source_mappings.get(uuid)
1✔
2198
        if not event_source_mapping:
1✔
2199
            raise ResourceNotFoundException(
1✔
2200
                "The resource you requested does not exist.", Type="User"
2201
            )
2202
        esm = state.event_source_mappings[uuid]
1✔
2203
        # TODO: add proper locking
2204
        esm_worker = self.esm_workers.pop(uuid, None)
1✔
2205
        # Asynchronous delete in v2
2206
        if not esm_worker:
1✔
UNCOV
2207
            raise ResourceNotFoundException(
×
2208
                "The resource you requested does not exist.", Type="User"
2209
            )
2210
        esm_worker.delete()
1✔
2211
        return {**esm, "State": EsmState.DELETING}
1✔
2212

2213
    def get_event_source_mapping(
1✔
2214
        self, context: RequestContext, uuid: String, **kwargs
2215
    ) -> EventSourceMappingConfiguration:
2216
        state = lambda_stores[context.account_id][context.region]
1✔
2217
        event_source_mapping = state.event_source_mappings.get(uuid)
1✔
2218
        if not event_source_mapping:
1✔
2219
            raise ResourceNotFoundException(
1✔
2220
                "The resource you requested does not exist.", Type="User"
2221
            )
2222
        esm_worker = self.esm_workers.get(uuid)
1✔
2223
        if not esm_worker:
1✔
UNCOV
2224
            raise ResourceNotFoundException(
×
2225
                "The resource you requested does not exist.", Type="User"
2226
            )
2227
        event_source_mapping["State"] = esm_worker.current_state
1✔
2228
        event_source_mapping["StateTransitionReason"] = esm_worker.state_transition_reason
1✔
2229
        return event_source_mapping
1✔
2230

2231
    def list_event_source_mappings(
1✔
2232
        self,
2233
        context: RequestContext,
2234
        event_source_arn: Arn = None,
2235
        function_name: FunctionName = None,
2236
        marker: String = None,
2237
        max_items: MaxListItems = None,
2238
        **kwargs,
2239
    ) -> ListEventSourceMappingsResponse:
2240
        state = lambda_stores[context.account_id][context.region]
1✔
2241

2242
        esms = state.event_source_mappings.values()
1✔
2243
        # TODO: update and test State and StateTransitionReason for ESM v2
2244

2245
        if event_source_arn:  # TODO: validate pattern
1✔
2246
            esms = [e for e in esms if e.get("EventSourceArn") == event_source_arn]
1✔
2247

2248
        if function_name:
1✔
2249
            esms = [e for e in esms if function_name in e["FunctionArn"]]
1✔
2250

2251
        esms = PaginatedList(esms)
1✔
2252
        page, token = esms.get_page(
1✔
2253
            lambda x: x["UUID"],
2254
            marker,
2255
            max_items,
2256
        )
2257
        return ListEventSourceMappingsResponse(EventSourceMappings=page, NextMarker=token)
1✔
2258

2259
    def get_source_type_from_request(self, request: dict[str, Any]) -> str:
1✔
UNCOV
2260
        if event_source_arn := request.get("EventSourceArn", ""):
×
UNCOV
2261
            service = extract_service_from_arn(event_source_arn)
×
UNCOV
2262
            if service == "sqs" and "fifo" in event_source_arn:
×
UNCOV
2263
                service = "sqs-fifo"
×
UNCOV
2264
            return service
×
UNCOV
2265
        elif request.get("SelfManagedEventSource"):
×
UNCOV
2266
            return "kafka"
×
2267

2268
    # =======================================
2269
    # ============ FUNCTION URLS ============
2270
    # =======================================
2271

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

2279
    @staticmethod
1✔
2280
    def _validate_invoke_mode(invoke_mode: str) -> None:
1✔
2281
        if invoke_mode and invoke_mode not in [InvokeMode.BUFFERED, InvokeMode.RESPONSE_STREAM]:
1✔
2282
            raise ValidationException(
1✔
2283
                f"1 validation error detected: Value '{invoke_mode}' at 'invokeMode' failed to satisfy constraint: Member must satisfy enum value set: [RESPONSE_STREAM, BUFFERED]"
2284
            )
2285
        if invoke_mode == InvokeMode.RESPONSE_STREAM:
1✔
2286
            # TODO should we actually fail for setting RESPONSE_STREAM?
2287
            #  It should trigger InvokeWithResponseStream which is not implemented
2288
            LOG.warning(
1✔
2289
                "The invokeMode 'RESPONSE_STREAM' is not yet supported on LocalStack. The property is only mocked, the execution will still be 'BUFFERED'"
2290
            )
2291

2292
    # TODO: what happens if function state is not active?
2293
    def create_function_url_config(
1✔
2294
        self,
2295
        context: RequestContext,
2296
        function_name: FunctionName,
2297
        auth_type: FunctionUrlAuthType,
2298
        qualifier: FunctionUrlQualifier = None,
2299
        cors: Cors = None,
2300
        invoke_mode: InvokeMode = None,
2301
        **kwargs,
2302
    ) -> CreateFunctionUrlConfigResponse:
2303
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2304
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2305
            function_name, qualifier, context
2306
        )
2307
        state = lambda_stores[account_id][region]
1✔
2308
        self._validate_qualifier(qualifier)
1✔
2309
        self._validate_invoke_mode(invoke_mode)
1✔
2310

2311
        fn = state.functions.get(function_name)
1✔
2312
        if fn is None:
1✔
2313
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2314

2315
        url_config = fn.function_url_configs.get(qualifier or "$LATEST")
1✔
2316
        if url_config:
1✔
2317
            raise ResourceConflictException(
1✔
2318
                f"Failed to create function url config for [functionArn = {url_config.function_arn}]. Error message:  FunctionUrlConfig exists for this Lambda function",
2319
                Type="User",
2320
            )
2321

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

2325
        normalized_qualifier = qualifier or "$LATEST"
1✔
2326

2327
        function_arn = (
1✔
2328
            api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
2329
            if qualifier
2330
            else api_utils.unqualified_lambda_arn(function_name, account_id, region)
2331
        )
2332

2333
        custom_id: str | None = None
1✔
2334

2335
        tags = self._get_tags(api_utils.unqualified_lambda_arn(function_name, account_id, region))
1✔
2336
        if TAG_KEY_CUSTOM_URL in tags:
1✔
2337
            # Note: I really wanted to add verification here that the
2338
            # url_id is unique, so we could surface that to the user ASAP.
2339
            # However, it seems like that information isn't available yet,
2340
            # since (as far as I can tell) we call
2341
            # self.router.register_routes() once, in a single shot, for all
2342
            # of the routes -- and we need to verify that it's unique not
2343
            # just for this particular lambda function, but for the entire
2344
            # lambda provider. Therefore... that idea proved non-trivial!
2345
            custom_id_tag_value = (
1✔
2346
                f"{tags[TAG_KEY_CUSTOM_URL]}-{qualifier}" if qualifier else tags[TAG_KEY_CUSTOM_URL]
2347
            )
2348
            if TAG_KEY_CUSTOM_URL_VALIDATOR.match(custom_id_tag_value):
1✔
2349
                custom_id = custom_id_tag_value
1✔
2350

2351
            else:
2352
                # Note: we're logging here instead of raising to prioritize
2353
                # strict parity with AWS over the localstack-only custom_id
2354
                LOG.warning(
1✔
2355
                    "Invalid custom ID tag value for lambda URL (%s=%s). "
2356
                    "Replaced with default (random id)",
2357
                    TAG_KEY_CUSTOM_URL,
2358
                    custom_id_tag_value,
2359
                )
2360

2361
        # The url_id is the subdomain used for the URL we're creating. This
2362
        # is either created randomly (as in AWS), or can be passed as a tag
2363
        # to the lambda itself (localstack-only).
2364
        url_id: str
2365
        if custom_id is None:
1✔
2366
            url_id = api_utils.generate_random_url_id()
1✔
2367
        else:
2368
            url_id = custom_id
1✔
2369

2370
        host_definition = localstack_host(custom_port=config.GATEWAY_LISTEN[0].port)
1✔
2371
        fn.function_url_configs[normalized_qualifier] = FunctionUrlConfig(
1✔
2372
            function_arn=function_arn,
2373
            function_name=function_name,
2374
            cors=cors,
2375
            url_id=url_id,
2376
            url=f"http://{url_id}.lambda-url.{context.region}.{host_definition.host_and_port()}/",  # TODO: https support
2377
            auth_type=auth_type,
2378
            creation_time=api_utils.generate_lambda_date(),
2379
            last_modified_time=api_utils.generate_lambda_date(),
2380
            invoke_mode=invoke_mode,
2381
        )
2382

2383
        # persist and start URL
2384
        # TODO: implement URL invoke
2385
        api_url_config = api_utils.map_function_url_config(
1✔
2386
            fn.function_url_configs[normalized_qualifier]
2387
        )
2388

2389
        return CreateFunctionUrlConfigResponse(
1✔
2390
            FunctionUrl=api_url_config["FunctionUrl"],
2391
            FunctionArn=api_url_config["FunctionArn"],
2392
            AuthType=api_url_config["AuthType"],
2393
            Cors=api_url_config["Cors"],
2394
            CreationTime=api_url_config["CreationTime"],
2395
            InvokeMode=api_url_config["InvokeMode"],
2396
        )
2397

2398
    def get_function_url_config(
1✔
2399
        self,
2400
        context: RequestContext,
2401
        function_name: FunctionName,
2402
        qualifier: FunctionUrlQualifier = None,
2403
        **kwargs,
2404
    ) -> GetFunctionUrlConfigResponse:
2405
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2406
        state = lambda_stores[account_id][region]
1✔
2407

2408
        fn_name, qualifier = api_utils.get_name_and_qualifier(function_name, qualifier, context)
1✔
2409

2410
        self._validate_qualifier(qualifier)
1✔
2411

2412
        resolved_fn = state.functions.get(fn_name)
1✔
2413
        if not resolved_fn:
1✔
2414
            raise ResourceNotFoundException(
1✔
2415
                "The resource you requested does not exist.", Type="User"
2416
            )
2417

2418
        qualifier = qualifier or "$LATEST"
1✔
2419
        url_config = resolved_fn.function_url_configs.get(qualifier)
1✔
2420
        if not url_config:
1✔
2421
            raise ResourceNotFoundException(
1✔
2422
                "The resource you requested does not exist.", Type="User"
2423
            )
2424

2425
        return api_utils.map_function_url_config(url_config)
1✔
2426

2427
    def update_function_url_config(
1✔
2428
        self,
2429
        context: RequestContext,
2430
        function_name: FunctionName,
2431
        qualifier: FunctionUrlQualifier = None,
2432
        auth_type: FunctionUrlAuthType = None,
2433
        cors: Cors = None,
2434
        invoke_mode: InvokeMode = None,
2435
        **kwargs,
2436
    ) -> UpdateFunctionUrlConfigResponse:
2437
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2438
        state = lambda_stores[account_id][region]
1✔
2439

2440
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2441
            function_name, qualifier, context
2442
        )
2443
        self._validate_qualifier(qualifier)
1✔
2444
        self._validate_invoke_mode(invoke_mode)
1✔
2445

2446
        fn = state.functions.get(function_name)
1✔
2447
        if not fn:
1✔
2448
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2449

2450
        normalized_qualifier = qualifier or "$LATEST"
1✔
2451

2452
        if (
1✔
2453
            api_utils.qualifier_is_alias(normalized_qualifier)
2454
            and normalized_qualifier not in fn.aliases
2455
        ):
2456
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2457

2458
        url_config = fn.function_url_configs.get(normalized_qualifier)
1✔
2459
        if not url_config:
1✔
2460
            raise ResourceNotFoundException(
1✔
2461
                "The resource you requested does not exist.", Type="User"
2462
            )
2463

2464
        changes = {
1✔
2465
            "last_modified_time": api_utils.generate_lambda_date(),
2466
            **({"cors": cors} if cors is not None else {}),
2467
            **({"auth_type": auth_type} if auth_type is not None else {}),
2468
        }
2469

2470
        if invoke_mode:
1✔
2471
            changes["invoke_mode"] = invoke_mode
1✔
2472

2473
        new_url_config = dataclasses.replace(url_config, **changes)
1✔
2474
        fn.function_url_configs[normalized_qualifier] = new_url_config
1✔
2475

2476
        return UpdateFunctionUrlConfigResponse(
1✔
2477
            FunctionUrl=new_url_config.url,
2478
            FunctionArn=new_url_config.function_arn,
2479
            AuthType=new_url_config.auth_type,
2480
            Cors=new_url_config.cors,
2481
            CreationTime=new_url_config.creation_time,
2482
            LastModifiedTime=new_url_config.last_modified_time,
2483
            InvokeMode=new_url_config.invoke_mode,
2484
        )
2485

2486
    def delete_function_url_config(
1✔
2487
        self,
2488
        context: RequestContext,
2489
        function_name: FunctionName,
2490
        qualifier: FunctionUrlQualifier = None,
2491
        **kwargs,
2492
    ) -> None:
2493
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2494
        state = lambda_stores[account_id][region]
1✔
2495

2496
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2497
            function_name, qualifier, context
2498
        )
2499
        self._validate_qualifier(qualifier)
1✔
2500

2501
        resolved_fn = state.functions.get(function_name)
1✔
2502
        if not resolved_fn:
1✔
2503
            raise ResourceNotFoundException(
1✔
2504
                "The resource you requested does not exist.", Type="User"
2505
            )
2506

2507
        qualifier = qualifier or "$LATEST"
1✔
2508
        url_config = resolved_fn.function_url_configs.get(qualifier)
1✔
2509
        if not url_config:
1✔
2510
            raise ResourceNotFoundException(
1✔
2511
                "The resource you requested does not exist.", Type="User"
2512
            )
2513

2514
        del resolved_fn.function_url_configs[qualifier]
1✔
2515

2516
    def list_function_url_configs(
1✔
2517
        self,
2518
        context: RequestContext,
2519
        function_name: FunctionName,
2520
        marker: String = None,
2521
        max_items: MaxItems = None,
2522
        **kwargs,
2523
    ) -> ListFunctionUrlConfigsResponse:
2524
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2525
        state = lambda_stores[account_id][region]
1✔
2526

2527
        fn_name = api_utils.get_function_name(function_name, context)
1✔
2528
        resolved_fn = state.functions.get(fn_name)
1✔
2529
        if not resolved_fn:
1✔
2530
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2531

2532
        url_configs = [
1✔
2533
            api_utils.map_function_url_config(fn_conf)
2534
            for fn_conf in resolved_fn.function_url_configs.values()
2535
        ]
2536
        url_configs = PaginatedList(url_configs)
1✔
2537
        page, token = url_configs.get_page(
1✔
2538
            lambda url_config: url_config["FunctionArn"],
2539
            marker,
2540
            max_items,
2541
        )
2542
        url_configs = page
1✔
2543
        return ListFunctionUrlConfigsResponse(FunctionUrlConfigs=url_configs, NextMarker=token)
1✔
2544

2545
    # =======================================
2546
    # ============  Permissions  ============
2547
    # =======================================
2548

2549
    @handler("AddPermission", expand=False)
1✔
2550
    def add_permission(
1✔
2551
        self,
2552
        context: RequestContext,
2553
        request: AddPermissionRequest,
2554
    ) -> AddPermissionResponse:
2555
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2556
            request.get("FunctionName"), request.get("Qualifier"), context
2557
        )
2558

2559
        # validate qualifier
2560
        if qualifier is not None:
1✔
2561
            self._validate_qualifier_expression(qualifier)
1✔
2562
            if qualifier == "$LATEST":
1✔
2563
                raise InvalidParameterValueException(
1✔
2564
                    "We currently do not support adding policies for $LATEST.", Type="User"
2565
                )
2566
        account_id, region = api_utils.get_account_and_region(request.get("FunctionName"), context)
1✔
2567

2568
        resolved_fn = self._get_function(function_name, account_id, region)
1✔
2569
        resolved_qualifier, fn_arn = self._resolve_fn_qualifier(resolved_fn, qualifier)
1✔
2570

2571
        revision_id = request.get("RevisionId")
1✔
2572
        if revision_id:
1✔
2573
            fn_revision_id = self._function_revision_id(resolved_fn, resolved_qualifier)
1✔
2574
            if revision_id != fn_revision_id:
1✔
2575
                raise PreconditionFailedException(
1✔
2576
                    "The Revision Id provided does not match the latest Revision Id. "
2577
                    "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
2578
                    Type="User",
2579
                )
2580

2581
        request_sid = request["StatementId"]
1✔
2582
        if not bool(STATEMENT_ID_REGEX.match(request_sid)):
1✔
2583
            raise ValidationException(
1✔
2584
                f"1 validation error detected: Value '{request_sid}' at 'statementId' failed to satisfy constraint: Member must satisfy regular expression pattern: ([a-zA-Z0-9-_]+)"
2585
            )
2586
        # check for an already existing policy and any conflicts in existing statements
2587
        existing_policy = resolved_fn.permissions.get(resolved_qualifier)
1✔
2588
        if existing_policy:
1✔
2589
            if request_sid in [s["Sid"] for s in existing_policy.policy.Statement]:
1✔
2590
                # uniqueness scope: statement id needs to be unique per qualified function ($LATEST, version, or alias)
2591
                # Counterexample: the same sid can exist within $LATEST, version, and alias
2592
                raise ResourceConflictException(
1✔
2593
                    f"The statement id ({request_sid}) provided already exists. Please provide a new statement id, or remove the existing statement.",
2594
                    Type="User",
2595
                )
2596

2597
        permission_statement = api_utils.build_statement(
1✔
2598
            partition=context.partition,
2599
            resource_arn=fn_arn,
2600
            statement_id=request["StatementId"],
2601
            action=request["Action"],
2602
            principal=request["Principal"],
2603
            source_arn=request.get("SourceArn"),
2604
            source_account=request.get("SourceAccount"),
2605
            principal_org_id=request.get("PrincipalOrgID"),
2606
            event_source_token=request.get("EventSourceToken"),
2607
            auth_type=request.get("FunctionUrlAuthType"),
2608
        )
2609
        new_policy = existing_policy
1✔
2610
        if not existing_policy:
1✔
2611
            new_policy = FunctionResourcePolicy(
1✔
2612
                policy=ResourcePolicy(Version="2012-10-17", Id="default", Statement=[])
2613
            )
2614
        new_policy.policy.Statement.append(permission_statement)
1✔
2615
        if not existing_policy:
1✔
2616
            resolved_fn.permissions[resolved_qualifier] = new_policy
1✔
2617

2618
        # Update revision id of alias or version
2619
        # TODO: re-evaluate data model to prevent this dirty hack just for bumping the revision id
2620
        # TODO: does that need a `with function.lock` for atomic updates of the policy + revision_id?
2621
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
2622
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
1✔
2623
            resolved_fn.aliases[resolved_qualifier] = dataclasses.replace(resolved_alias)
1✔
2624
        # Assumes that a non-alias is a version
2625
        else:
2626
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
2627
            resolved_fn.versions[resolved_qualifier] = dataclasses.replace(
1✔
2628
                resolved_version, config=dataclasses.replace(resolved_version.config)
2629
            )
2630
        return AddPermissionResponse(Statement=json.dumps(permission_statement))
1✔
2631

2632
    def remove_permission(
1✔
2633
        self,
2634
        context: RequestContext,
2635
        function_name: FunctionName,
2636
        statement_id: NamespacedStatementId,
2637
        qualifier: Qualifier = None,
2638
        revision_id: String = None,
2639
        **kwargs,
2640
    ) -> None:
2641
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2642
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2643
            function_name, qualifier, context
2644
        )
2645
        if qualifier is not None:
1✔
2646
            self._validate_qualifier_expression(qualifier)
1✔
2647

2648
        state = lambda_stores[account_id][region]
1✔
2649
        resolved_fn = state.functions.get(function_name)
1✔
2650
        if resolved_fn is None:
1✔
2651
            fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
2652
            raise ResourceNotFoundException(f"No policy found for: {fn_arn}", Type="User")
1✔
2653

2654
        resolved_qualifier, _ = self._resolve_fn_qualifier(resolved_fn, qualifier)
1✔
2655
        function_permission = resolved_fn.permissions.get(resolved_qualifier)
1✔
2656
        if not function_permission:
1✔
2657
            raise ResourceNotFoundException(
1✔
2658
                "No policy is associated with the given resource.", Type="User"
2659
            )
2660

2661
        # try to find statement in policy and delete it
2662
        statement = None
1✔
2663
        for s in function_permission.policy.Statement:
1✔
2664
            if s["Sid"] == statement_id:
1✔
2665
                statement = s
1✔
2666
                break
1✔
2667

2668
        if not statement:
1✔
2669
            raise ResourceNotFoundException(
1✔
2670
                f"Statement {statement_id} is not found in resource policy.", Type="User"
2671
            )
2672
        fn_revision_id = self._function_revision_id(resolved_fn, resolved_qualifier)
1✔
2673
        if revision_id and revision_id != fn_revision_id:
1✔
2674
            raise PreconditionFailedException(
1✔
2675
                "The Revision Id provided does not match the latest Revision Id. "
2676
                "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
2677
                Type="User",
2678
            )
2679
        function_permission.policy.Statement.remove(statement)
1✔
2680

2681
        # Update revision id for alias or version
2682
        # TODO: re-evaluate data model to prevent this dirty hack just for bumping the revision id
2683
        # TODO: does that need a `with function.lock` for atomic updates of the policy + revision_id?
2684
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
UNCOV
2685
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
×
UNCOV
2686
            resolved_fn.aliases[resolved_qualifier] = dataclasses.replace(resolved_alias)
×
2687
        # Assumes that a non-alias is a version
2688
        else:
2689
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
2690
            resolved_fn.versions[resolved_qualifier] = dataclasses.replace(
1✔
2691
                resolved_version, config=dataclasses.replace(resolved_version.config)
2692
            )
2693

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

2698
    def get_policy(
1✔
2699
        self,
2700
        context: RequestContext,
2701
        function_name: NamespacedFunctionName,
2702
        qualifier: Qualifier = None,
2703
        **kwargs,
2704
    ) -> GetPolicyResponse:
2705
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2706
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2707
            function_name, qualifier, context
2708
        )
2709

2710
        if qualifier is not None:
1✔
2711
            self._validate_qualifier_expression(qualifier)
1✔
2712

2713
        resolved_fn = self._get_function(function_name, account_id, region)
1✔
2714

2715
        resolved_qualifier = qualifier or "$LATEST"
1✔
2716
        function_permission = resolved_fn.permissions.get(resolved_qualifier)
1✔
2717
        if not function_permission:
1✔
2718
            raise ResourceNotFoundException(
1✔
2719
                "The resource you requested does not exist.", Type="User"
2720
            )
2721

2722
        fn_revision_id = None
1✔
2723
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
2724
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
1✔
2725
            fn_revision_id = resolved_alias.revision_id
1✔
2726
        # Assumes that a non-alias is a version
2727
        else:
2728
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
2729
            fn_revision_id = resolved_version.config.revision_id
1✔
2730

2731
        return GetPolicyResponse(
1✔
2732
            Policy=json.dumps(dataclasses.asdict(function_permission.policy)),
2733
            RevisionId=fn_revision_id,
2734
        )
2735

2736
    # =======================================
2737
    # ========  Code signing config  ========
2738
    # =======================================
2739

2740
    def create_code_signing_config(
1✔
2741
        self,
2742
        context: RequestContext,
2743
        allowed_publishers: AllowedPublishers,
2744
        description: Description = None,
2745
        code_signing_policies: CodeSigningPolicies = None,
2746
        tags: Tags = None,
2747
        **kwargs,
2748
    ) -> CreateCodeSigningConfigResponse:
2749
        account = context.account_id
1✔
2750
        region = context.region
1✔
2751

2752
        state = lambda_stores[account][region]
1✔
2753
        # TODO: can there be duplicates?
2754
        csc_id = f"csc-{get_random_hex(17)}"  # e.g. 'csc-077c33b4c19e26036'
1✔
2755
        csc_arn = f"arn:{context.partition}:lambda:{region}:{account}:code-signing-config:{csc_id}"
1✔
2756
        csc = CodeSigningConfig(
1✔
2757
            csc_id=csc_id,
2758
            arn=csc_arn,
2759
            allowed_publishers=allowed_publishers,
2760
            policies=code_signing_policies,
2761
            last_modified=api_utils.generate_lambda_date(),
2762
            description=description,
2763
        )
2764
        state.code_signing_configs[csc_arn] = csc
1✔
2765
        return CreateCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(csc))
1✔
2766

2767
    def put_function_code_signing_config(
1✔
2768
        self,
2769
        context: RequestContext,
2770
        code_signing_config_arn: CodeSigningConfigArn,
2771
        function_name: FunctionName,
2772
        **kwargs,
2773
    ) -> PutFunctionCodeSigningConfigResponse:
2774
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2775
        state = lambda_stores[account_id][region]
1✔
2776
        function_name = api_utils.get_function_name(function_name, context)
1✔
2777

2778
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
2779
        if not csc:
1✔
2780
            raise CodeSigningConfigNotFoundException(
1✔
2781
                f"The code signing configuration cannot be found. Check that the provided configuration is not deleted: {code_signing_config_arn}.",
2782
                Type="User",
2783
            )
2784

2785
        fn = state.functions.get(function_name)
1✔
2786
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
2787
        if not fn:
1✔
2788
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
2789

2790
        fn.code_signing_config_arn = code_signing_config_arn
1✔
2791
        return PutFunctionCodeSigningConfigResponse(
1✔
2792
            CodeSigningConfigArn=code_signing_config_arn, FunctionName=function_name
2793
        )
2794

2795
    def update_code_signing_config(
1✔
2796
        self,
2797
        context: RequestContext,
2798
        code_signing_config_arn: CodeSigningConfigArn,
2799
        description: Description = None,
2800
        allowed_publishers: AllowedPublishers = None,
2801
        code_signing_policies: CodeSigningPolicies = None,
2802
        **kwargs,
2803
    ) -> UpdateCodeSigningConfigResponse:
2804
        state = lambda_stores[context.account_id][context.region]
1✔
2805
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
2806
        if not csc:
1✔
2807
            raise ResourceNotFoundException(
1✔
2808
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
2809
            )
2810

2811
        changes = {
1✔
2812
            **(
2813
                {"allowed_publishers": allowed_publishers} if allowed_publishers is not None else {}
2814
            ),
2815
            **({"policies": code_signing_policies} if code_signing_policies is not None else {}),
2816
            **({"description": description} if description is not None else {}),
2817
        }
2818
        new_csc = dataclasses.replace(
1✔
2819
            csc, last_modified=api_utils.generate_lambda_date(), **changes
2820
        )
2821
        state.code_signing_configs[code_signing_config_arn] = new_csc
1✔
2822

2823
        return UpdateCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(new_csc))
1✔
2824

2825
    def get_code_signing_config(
1✔
2826
        self, context: RequestContext, code_signing_config_arn: CodeSigningConfigArn, **kwargs
2827
    ) -> GetCodeSigningConfigResponse:
2828
        state = lambda_stores[context.account_id][context.region]
1✔
2829
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
2830
        if not csc:
1✔
2831
            raise ResourceNotFoundException(
1✔
2832
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
2833
            )
2834

2835
        return GetCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(csc))
1✔
2836

2837
    def get_function_code_signing_config(
1✔
2838
        self, context: RequestContext, function_name: FunctionName, **kwargs
2839
    ) -> GetFunctionCodeSigningConfigResponse:
2840
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2841
        state = lambda_stores[account_id][region]
1✔
2842
        function_name = api_utils.get_function_name(function_name, context)
1✔
2843
        fn = state.functions.get(function_name)
1✔
2844
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
2845
        if not fn:
1✔
2846
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
2847

2848
        if fn.code_signing_config_arn:
1✔
2849
            return GetFunctionCodeSigningConfigResponse(
1✔
2850
                CodeSigningConfigArn=fn.code_signing_config_arn, FunctionName=function_name
2851
            )
2852

2853
        return GetFunctionCodeSigningConfigResponse()
1✔
2854

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

2866
        fn.code_signing_config_arn = None
1✔
2867

2868
    def delete_code_signing_config(
1✔
2869
        self, context: RequestContext, code_signing_config_arn: CodeSigningConfigArn, **kwargs
2870
    ) -> DeleteCodeSigningConfigResponse:
2871
        state = lambda_stores[context.account_id][context.region]
1✔
2872

2873
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
2874
        if not csc:
1✔
2875
            raise ResourceNotFoundException(
1✔
2876
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
2877
            )
2878

2879
        del state.code_signing_configs[code_signing_config_arn]
1✔
2880

2881
        return DeleteCodeSigningConfigResponse()
1✔
2882

2883
    def list_code_signing_configs(
1✔
2884
        self,
2885
        context: RequestContext,
2886
        marker: String = None,
2887
        max_items: MaxListItems = None,
2888
        **kwargs,
2889
    ) -> ListCodeSigningConfigsResponse:
2890
        state = lambda_stores[context.account_id][context.region]
1✔
2891

2892
        cscs = [api_utils.map_csc(csc) for csc in state.code_signing_configs.values()]
1✔
2893
        cscs = PaginatedList(cscs)
1✔
2894
        page, token = cscs.get_page(
1✔
2895
            lambda csc: csc["CodeSigningConfigId"],
2896
            marker,
2897
            max_items,
2898
        )
2899
        return ListCodeSigningConfigsResponse(CodeSigningConfigs=page, NextMarker=token)
1✔
2900

2901
    def list_functions_by_code_signing_config(
1✔
2902
        self,
2903
        context: RequestContext,
2904
        code_signing_config_arn: CodeSigningConfigArn,
2905
        marker: String = None,
2906
        max_items: MaxListItems = None,
2907
        **kwargs,
2908
    ) -> ListFunctionsByCodeSigningConfigResponse:
2909
        account = context.account_id
1✔
2910
        region = context.region
1✔
2911

2912
        state = lambda_stores[account][region]
1✔
2913

2914
        if code_signing_config_arn not in state.code_signing_configs:
1✔
2915
            raise ResourceNotFoundException(
1✔
2916
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
2917
            )
2918

2919
        fn_arns = [
1✔
2920
            api_utils.unqualified_lambda_arn(fn.function_name, account, region)
2921
            for fn in state.functions.values()
2922
            if fn.code_signing_config_arn == code_signing_config_arn
2923
        ]
2924

2925
        cscs = PaginatedList(fn_arns)
1✔
2926
        page, token = cscs.get_page(
1✔
2927
            lambda x: x,
2928
            marker,
2929
            max_items,
2930
        )
2931
        return ListFunctionsByCodeSigningConfigResponse(FunctionArns=page, NextMarker=token)
1✔
2932

2933
    # =======================================
2934
    # =========  Account Settings   =========
2935
    # =======================================
2936

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

2942
        fn_count = 0
1✔
2943
        code_size_sum = 0
1✔
2944
        reserved_concurrency_sum = 0
1✔
2945
        for fn in state.functions.values():
1✔
2946
            fn_count += 1
1✔
2947
            for fn_version in fn.versions.values():
1✔
2948
                # Image-based Lambdas do not have a code attribute and count against the ECR quotas instead
2949
                if fn_version.config.package_type == PackageType.Zip:
1✔
2950
                    code_size_sum += fn_version.config.code.code_size
1✔
2951
            if fn.reserved_concurrent_executions is not None:
1✔
2952
                reserved_concurrency_sum += fn.reserved_concurrent_executions
1✔
2953
            for c in fn.provisioned_concurrency_configs.values():
1✔
2954
                reserved_concurrency_sum += c.provisioned_concurrent_executions
1✔
2955
        for layer in state.layers.values():
1✔
2956
            for layer_version in layer.layer_versions.values():
1✔
2957
                code_size_sum += layer_version.code.code_size
1✔
2958
        return GetAccountSettingsResponse(
1✔
2959
            AccountLimit=AccountLimit(
2960
                TotalCodeSize=config.LAMBDA_LIMITS_TOTAL_CODE_SIZE,
2961
                CodeSizeZipped=config.LAMBDA_LIMITS_CODE_SIZE_ZIPPED,
2962
                CodeSizeUnzipped=config.LAMBDA_LIMITS_CODE_SIZE_UNZIPPED,
2963
                ConcurrentExecutions=config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS,
2964
                UnreservedConcurrentExecutions=config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS
2965
                - reserved_concurrency_sum,
2966
            ),
2967
            AccountUsage=AccountUsage(
2968
                TotalCodeSize=code_size_sum,
2969
                FunctionCount=fn_count,
2970
            ),
2971
        )
2972

2973
    # =======================================
2974
    # ==  Provisioned Concurrency Config   ==
2975
    # =======================================
2976

2977
    def _get_provisioned_config(
1✔
2978
        self, context: RequestContext, function_name: str, qualifier: str
2979
    ) -> ProvisionedConcurrencyConfiguration | None:
2980
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2981
        state = lambda_stores[account_id][region]
1✔
2982
        function_name = api_utils.get_function_name(function_name, context)
1✔
2983
        fn = state.functions.get(function_name)
1✔
2984
        if api_utils.qualifier_is_alias(qualifier):
1✔
2985
            fn_alias = None
1✔
2986
            if fn:
1✔
2987
                fn_alias = fn.aliases.get(qualifier)
1✔
2988
            if fn_alias is None:
1✔
2989
                raise ResourceNotFoundException(
1✔
2990
                    f"Cannot find alias arn: {api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)}",
2991
                    Type="User",
2992
                )
2993
        elif api_utils.qualifier_is_version(qualifier):
1✔
2994
            fn_version = None
1✔
2995
            if fn:
1✔
2996
                fn_version = fn.versions.get(qualifier)
1✔
2997
            if fn_version is None:
1✔
2998
                raise ResourceNotFoundException(
1✔
2999
                    f"Function not found: {api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)}",
3000
                    Type="User",
3001
                )
3002

3003
        return fn.provisioned_concurrency_configs.get(qualifier)
1✔
3004

3005
    def put_provisioned_concurrency_config(
1✔
3006
        self,
3007
        context: RequestContext,
3008
        function_name: FunctionName,
3009
        qualifier: Qualifier,
3010
        provisioned_concurrent_executions: PositiveInteger,
3011
        **kwargs,
3012
    ) -> PutProvisionedConcurrencyConfigResponse:
3013
        if provisioned_concurrent_executions <= 0:
1✔
3014
            raise ValidationException(
1✔
3015
                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"
3016
            )
3017

3018
        if qualifier == "$LATEST":
1✔
3019
            raise InvalidParameterValueException(
1✔
3020
                "Provisioned Concurrency Configs cannot be applied to unpublished function versions.",
3021
                Type="User",
3022
            )
3023
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3024
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3025
            function_name, qualifier, context
3026
        )
3027
        state = lambda_stores[account_id][region]
1✔
3028
        fn = state.functions.get(function_name)
1✔
3029

3030
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3031

3032
        if provisioned_config:  # TODO: merge?
1✔
3033
            # TODO: add a test for partial updates (if possible)
3034
            LOG.warning(
1✔
3035
                "Partial update of provisioned concurrency config is currently not supported."
3036
            )
3037

3038
        other_provisioned_sum = sum(
1✔
3039
            [
3040
                provisioned_configs.provisioned_concurrent_executions
3041
                for provisioned_qualifier, provisioned_configs in fn.provisioned_concurrency_configs.items()
3042
                if provisioned_qualifier != qualifier
3043
            ]
3044
        )
3045

3046
        if (
1✔
3047
            fn.reserved_concurrent_executions is not None
3048
            and fn.reserved_concurrent_executions
3049
            < other_provisioned_sum + provisioned_concurrent_executions
3050
        ):
3051
            raise InvalidParameterValueException(
1✔
3052
                "Requested Provisioned Concurrency should not be greater than the reservedConcurrentExecution for function",
3053
                Type="User",
3054
            )
3055

3056
        if provisioned_concurrent_executions > config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS:
1✔
3057
            raise InvalidParameterValueException(
1✔
3058
                f"Specified ConcurrentExecutions for function is greater than account's unreserved concurrency"
3059
                f" [{config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS}]."
3060
            )
3061

3062
        settings = self.get_account_settings(context)
1✔
3063
        unreserved_concurrent_executions = settings["AccountLimit"][
1✔
3064
            "UnreservedConcurrentExecutions"
3065
        ]
3066
        if (
1✔
3067
            unreserved_concurrent_executions - provisioned_concurrent_executions
3068
            < config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY
3069
        ):
3070
            raise InvalidParameterValueException(
1✔
3071
                f"Specified ConcurrentExecutions for function decreases account's UnreservedConcurrentExecution below"
3072
                f" its minimum value of [{config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY}]."
3073
            )
3074

3075
        provisioned_config = ProvisionedConcurrencyConfiguration(
1✔
3076
            provisioned_concurrent_executions, api_utils.generate_lambda_date()
3077
        )
3078
        fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3079

3080
        if api_utils.qualifier_is_alias(qualifier):
1✔
3081
            alias = fn.aliases.get(qualifier)
1✔
3082
            resolved_version = fn.versions.get(alias.function_version)
1✔
3083

3084
            if (
1✔
3085
                resolved_version
3086
                and fn.provisioned_concurrency_configs.get(alias.function_version) is not None
3087
            ):
3088
                raise ResourceConflictException(
1✔
3089
                    "Alias can't be used for Provisioned Concurrency configuration on an already Provisioned version",
3090
                    Type="User",
3091
                )
3092
            fn_arn = resolved_version.id.qualified_arn()
1✔
3093
        elif api_utils.qualifier_is_version(qualifier):
1✔
3094
            fn_version = fn.versions.get(qualifier)
1✔
3095

3096
            # TODO: might be useful other places, utilize
3097
            pointing_aliases = []
1✔
3098
            for alias in fn.aliases.values():
1✔
3099
                if (
1✔
3100
                    alias.function_version == qualifier
3101
                    and fn.provisioned_concurrency_configs.get(alias.name) is not None
3102
                ):
3103
                    pointing_aliases.append(alias.name)
1✔
3104
            if pointing_aliases:
1✔
3105
                raise ResourceConflictException(
1✔
3106
                    "Version is pointed by a Provisioned Concurrency alias", Type="User"
3107
                )
3108

3109
            fn_arn = fn_version.id.qualified_arn()
1✔
3110

3111
        manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3112

3113
        fn.provisioned_concurrency_configs[qualifier] = provisioned_config
1✔
3114

3115
        manager.update_provisioned_concurrency_config(
1✔
3116
            provisioned_config.provisioned_concurrent_executions
3117
        )
3118

3119
        return PutProvisionedConcurrencyConfigResponse(
1✔
3120
            RequestedProvisionedConcurrentExecutions=provisioned_config.provisioned_concurrent_executions,
3121
            AvailableProvisionedConcurrentExecutions=0,
3122
            AllocatedProvisionedConcurrentExecutions=0,
3123
            Status=ProvisionedConcurrencyStatusEnum.IN_PROGRESS,
3124
            # StatusReason=manager.provisioned_state.status_reason,
3125
            LastModified=provisioned_config.last_modified,  # TODO: does change with configuration or also with state changes?
3126
        )
3127

3128
    def get_provisioned_concurrency_config(
1✔
3129
        self, context: RequestContext, function_name: FunctionName, qualifier: Qualifier, **kwargs
3130
    ) -> GetProvisionedConcurrencyConfigResponse:
3131
        if qualifier == "$LATEST":
1✔
3132
            raise InvalidParameterValueException(
1✔
3133
                "The function resource provided must be an alias or a published version.",
3134
                Type="User",
3135
            )
3136
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3137
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3138
            function_name, qualifier, context
3139
        )
3140

3141
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3142
        if not provisioned_config:
1✔
3143
            raise ProvisionedConcurrencyConfigNotFoundException(
1✔
3144
                "No Provisioned Concurrency Config found for this function", Type="User"
3145
            )
3146

3147
        # TODO: make this compatible with alias pointer migration on update
3148
        if api_utils.qualifier_is_alias(qualifier):
1✔
3149
            state = lambda_stores[account_id][region]
1✔
3150
            fn = state.functions.get(function_name)
1✔
3151
            alias = fn.aliases.get(qualifier)
1✔
3152
            fn_arn = api_utils.qualified_lambda_arn(
1✔
3153
                function_name, alias.function_version, account_id, region
3154
            )
3155
        else:
3156
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3157

3158
        ver_manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3159

3160
        return GetProvisionedConcurrencyConfigResponse(
1✔
3161
            RequestedProvisionedConcurrentExecutions=provisioned_config.provisioned_concurrent_executions,
3162
            LastModified=provisioned_config.last_modified,
3163
            AvailableProvisionedConcurrentExecutions=ver_manager.provisioned_state.available,
3164
            AllocatedProvisionedConcurrentExecutions=ver_manager.provisioned_state.allocated,
3165
            Status=ver_manager.provisioned_state.status,
3166
            StatusReason=ver_manager.provisioned_state.status_reason,
3167
        )
3168

3169
    def list_provisioned_concurrency_configs(
1✔
3170
        self,
3171
        context: RequestContext,
3172
        function_name: FunctionName,
3173
        marker: String = None,
3174
        max_items: MaxProvisionedConcurrencyConfigListItems = None,
3175
        **kwargs,
3176
    ) -> ListProvisionedConcurrencyConfigsResponse:
3177
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3178
        state = lambda_stores[account_id][region]
1✔
3179

3180
        function_name = api_utils.get_function_name(function_name, context)
1✔
3181
        fn = state.functions.get(function_name)
1✔
3182
        if fn is None:
1✔
3183
            raise ResourceNotFoundException(
1✔
3184
                f"Function not found: {api_utils.unqualified_lambda_arn(function_name, account_id, region)}",
3185
                Type="User",
3186
            )
3187

3188
        configs = []
1✔
3189
        for qualifier, pc_config in fn.provisioned_concurrency_configs.items():
1✔
UNCOV
3190
            if api_utils.qualifier_is_alias(qualifier):
×
UNCOV
3191
                alias = fn.aliases.get(qualifier)
×
UNCOV
3192
                fn_arn = api_utils.qualified_lambda_arn(
×
3193
                    function_name, alias.function_version, account_id, region
3194
                )
3195
            else:
UNCOV
3196
                fn_arn = api_utils.qualified_lambda_arn(
×
3197
                    function_name, qualifier, account_id, region
3198
                )
3199

UNCOV
3200
            manager = self.lambda_service.get_lambda_version_manager(fn_arn)
×
3201

UNCOV
3202
            configs.append(
×
3203
                ProvisionedConcurrencyConfigListItem(
3204
                    FunctionArn=api_utils.qualified_lambda_arn(
3205
                        function_name, qualifier, account_id, region
3206
                    ),
3207
                    RequestedProvisionedConcurrentExecutions=pc_config.provisioned_concurrent_executions,
3208
                    AvailableProvisionedConcurrentExecutions=manager.provisioned_state.available,
3209
                    AllocatedProvisionedConcurrentExecutions=manager.provisioned_state.allocated,
3210
                    Status=manager.provisioned_state.status,
3211
                    StatusReason=manager.provisioned_state.status_reason,
3212
                    LastModified=pc_config.last_modified,
3213
                )
3214
            )
3215

3216
        provisioned_concurrency_configs = configs
1✔
3217
        provisioned_concurrency_configs = PaginatedList(provisioned_concurrency_configs)
1✔
3218
        page, token = provisioned_concurrency_configs.get_page(
1✔
3219
            lambda x: x,
3220
            marker,
3221
            max_items,
3222
        )
3223
        return ListProvisionedConcurrencyConfigsResponse(
1✔
3224
            ProvisionedConcurrencyConfigs=page, NextMarker=token
3225
        )
3226

3227
    def delete_provisioned_concurrency_config(
1✔
3228
        self, context: RequestContext, function_name: FunctionName, qualifier: Qualifier, **kwargs
3229
    ) -> None:
3230
        if qualifier == "$LATEST":
1✔
3231
            raise InvalidParameterValueException(
1✔
3232
                "The function resource provided must be an alias or a published version.",
3233
                Type="User",
3234
            )
3235
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3236
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3237
            function_name, qualifier, context
3238
        )
3239
        state = lambda_stores[account_id][region]
1✔
3240
        fn = state.functions.get(function_name)
1✔
3241

3242
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3243
        # delete is idempotent and doesn't actually care about the provisioned concurrency config not existing
3244
        if provisioned_config:
1✔
3245
            fn.provisioned_concurrency_configs.pop(qualifier)
1✔
3246
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3247
            manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3248
            manager.update_provisioned_concurrency_config(0)
1✔
3249

3250
    # =======================================
3251
    # =======  Event Invoke Config   ========
3252
    # =======================================
3253

3254
    # "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})?:(.*)"
3255
    # "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)
3256

3257
    def _validate_destination_config(
1✔
3258
        self, store: LambdaStore, function_name: str, destination_config: DestinationConfig
3259
    ):
3260
        def _validate_destination_arn(destination_arn) -> bool:
1✔
3261
            if not api_utils.DESTINATION_ARN_PATTERN.match(destination_arn):
1✔
3262
                # technically we shouldn't handle this in the provider
3263
                raise ValidationException(
1✔
3264
                    "1 validation error detected: Value '"
3265
                    + destination_arn
3266
                    + 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})?:(.*)"
3267
                )
3268

3269
            match destination_arn.split(":")[2]:
1✔
3270
                case "lambda":
1✔
3271
                    fn_parts = api_utils.FULL_FN_ARN_PATTERN.search(destination_arn).groupdict()
1✔
3272
                    if fn_parts:
1✔
3273
                        # check if it exists
3274
                        fn = store.functions.get(fn_parts["function_name"])
1✔
3275
                        if not fn:
1✔
3276
                            raise InvalidParameterValueException(
1✔
3277
                                f"The destination ARN {destination_arn} is invalid.", Type="User"
3278
                            )
3279
                        if fn_parts["function_name"] == function_name:
1✔
3280
                            raise InvalidParameterValueException(
1✔
3281
                                "You can't specify the function as a destination for itself.",
3282
                                Type="User",
3283
                            )
3284
                case "sns" | "sqs" | "events":
1✔
3285
                    pass
1✔
3286
                case _:
1✔
3287
                    return False
1✔
3288
            return True
1✔
3289

3290
        validation_err = False
1✔
3291

3292
        failure_destination = destination_config.get("OnFailure", {}).get("Destination")
1✔
3293
        if failure_destination:
1✔
3294
            validation_err = validation_err or not _validate_destination_arn(failure_destination)
1✔
3295

3296
        success_destination = destination_config.get("OnSuccess", {}).get("Destination")
1✔
3297
        if success_destination:
1✔
3298
            validation_err = validation_err or not _validate_destination_arn(success_destination)
1✔
3299

3300
        if validation_err:
1✔
3301
            on_success_part = (
1✔
3302
                f"OnSuccess(destination={success_destination})" if success_destination else "null"
3303
            )
3304
            on_failure_part = (
1✔
3305
                f"OnFailure(destination={failure_destination})" if failure_destination else "null"
3306
            )
3307
            raise InvalidParameterValueException(
1✔
3308
                f"The provided destination config DestinationConfig(onSuccess={on_success_part}, onFailure={on_failure_part}) is invalid.",
3309
                Type="User",
3310
            )
3311

3312
    def put_function_event_invoke_config(
1✔
3313
        self,
3314
        context: RequestContext,
3315
        function_name: FunctionName,
3316
        qualifier: Qualifier = None,
3317
        maximum_retry_attempts: MaximumRetryAttempts = None,
3318
        maximum_event_age_in_seconds: MaximumEventAgeInSeconds = None,
3319
        destination_config: DestinationConfig = None,
3320
        **kwargs,
3321
    ) -> FunctionEventInvokeConfig:
3322
        """
3323
        Destination ARNs can be:
3324
        * SQS arn
3325
        * SNS arn
3326
        * Lambda arn
3327
        * EventBridge arn
3328

3329
        Differences between put_ and update_:
3330
            * put overwrites any existing config
3331
            * update allows changes only single values while keeping the rest of existing ones
3332
            * update fails on non-existing configs
3333

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

3338
        """
3339
        if (
1✔
3340
            maximum_event_age_in_seconds is None
3341
            and maximum_retry_attempts is None
3342
            and destination_config is None
3343
        ):
3344
            raise InvalidParameterValueException(
1✔
3345
                "You must specify at least one of error handling or destination setting.",
3346
                Type="User",
3347
            )
3348
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3349
        state = lambda_stores[account_id][region]
1✔
3350
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3351
            function_name, qualifier, context
3352
        )
3353
        fn = state.functions.get(function_name)
1✔
3354
        if not fn or (qualifier and not (qualifier in fn.aliases or qualifier in fn.versions)):
1✔
3355
            raise ResourceNotFoundException("The function doesn't exist.", Type="User")
1✔
3356

3357
        qualifier = qualifier or "$LATEST"
1✔
3358

3359
        # validate and normalize destination config
3360
        if destination_config:
1✔
3361
            self._validate_destination_config(state, function_name, destination_config)
1✔
3362

3363
        destination_config = DestinationConfig(
1✔
3364
            OnSuccess=OnSuccess(
3365
                Destination=(destination_config or {}).get("OnSuccess", {}).get("Destination")
3366
            ),
3367
            OnFailure=OnFailure(
3368
                Destination=(destination_config or {}).get("OnFailure", {}).get("Destination")
3369
            ),
3370
        )
3371

3372
        config = EventInvokeConfig(
1✔
3373
            function_name=function_name,
3374
            qualifier=qualifier,
3375
            maximum_event_age_in_seconds=maximum_event_age_in_seconds,
3376
            maximum_retry_attempts=maximum_retry_attempts,
3377
            last_modified=api_utils.generate_lambda_date(),
3378
            destination_config=destination_config,
3379
        )
3380
        fn.event_invoke_configs[qualifier] = config
1✔
3381

3382
        return FunctionEventInvokeConfig(
1✔
3383
            LastModified=datetime.datetime.strptime(
3384
                config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3385
            ),
3386
            FunctionArn=api_utils.qualified_lambda_arn(
3387
                function_name, qualifier or "$LATEST", account_id, region
3388
            ),
3389
            DestinationConfig=destination_config,
3390
            MaximumEventAgeInSeconds=maximum_event_age_in_seconds,
3391
            MaximumRetryAttempts=maximum_retry_attempts,
3392
        )
3393

3394
    def get_function_event_invoke_config(
1✔
3395
        self,
3396
        context: RequestContext,
3397
        function_name: FunctionName,
3398
        qualifier: Qualifier = None,
3399
        **kwargs,
3400
    ) -> FunctionEventInvokeConfig:
3401
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3402
        state = lambda_stores[account_id][region]
1✔
3403
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3404
            function_name, qualifier, context
3405
        )
3406

3407
        qualifier = qualifier or "$LATEST"
1✔
3408
        fn = state.functions.get(function_name)
1✔
3409
        if not fn:
1✔
3410
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3411
            raise ResourceNotFoundException(
1✔
3412
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3413
            )
3414

3415
        config = fn.event_invoke_configs.get(qualifier)
1✔
3416
        if not config:
1✔
3417
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3418
            raise ResourceNotFoundException(
1✔
3419
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3420
            )
3421

3422
        return FunctionEventInvokeConfig(
1✔
3423
            LastModified=datetime.datetime.strptime(
3424
                config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3425
            ),
3426
            FunctionArn=api_utils.qualified_lambda_arn(
3427
                function_name, qualifier, account_id, region
3428
            ),
3429
            DestinationConfig=config.destination_config,
3430
            MaximumEventAgeInSeconds=config.maximum_event_age_in_seconds,
3431
            MaximumRetryAttempts=config.maximum_retry_attempts,
3432
        )
3433

3434
    def list_function_event_invoke_configs(
1✔
3435
        self,
3436
        context: RequestContext,
3437
        function_name: FunctionName,
3438
        marker: String = None,
3439
        max_items: MaxFunctionEventInvokeConfigListItems = None,
3440
        **kwargs,
3441
    ) -> ListFunctionEventInvokeConfigsResponse:
3442
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3443
        state = lambda_stores[account_id][region]
1✔
3444
        fn = state.functions.get(function_name)
1✔
3445
        if not fn:
1✔
3446
            raise ResourceNotFoundException("The function doesn't exist.", Type="User")
1✔
3447

3448
        event_invoke_configs = [
1✔
3449
            FunctionEventInvokeConfig(
3450
                LastModified=c.last_modified,
3451
                FunctionArn=api_utils.qualified_lambda_arn(
3452
                    function_name, c.qualifier, account_id, region
3453
                ),
3454
                MaximumEventAgeInSeconds=c.maximum_event_age_in_seconds,
3455
                MaximumRetryAttempts=c.maximum_retry_attempts,
3456
                DestinationConfig=c.destination_config,
3457
            )
3458
            for c in fn.event_invoke_configs.values()
3459
        ]
3460

3461
        event_invoke_configs = PaginatedList(event_invoke_configs)
1✔
3462
        page, token = event_invoke_configs.get_page(
1✔
3463
            lambda x: x["FunctionArn"],
3464
            marker,
3465
            max_items,
3466
        )
3467
        return ListFunctionEventInvokeConfigsResponse(
1✔
3468
            FunctionEventInvokeConfigs=page, NextMarker=token
3469
        )
3470

3471
    def delete_function_event_invoke_config(
1✔
3472
        self,
3473
        context: RequestContext,
3474
        function_name: FunctionName,
3475
        qualifier: Qualifier = None,
3476
        **kwargs,
3477
    ) -> None:
3478
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3479
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3480
            function_name, qualifier, context
3481
        )
3482
        state = lambda_stores[account_id][region]
1✔
3483
        fn = state.functions.get(function_name)
1✔
3484
        resolved_qualifier = qualifier or "$LATEST"
1✔
3485
        fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3486
        if not fn:
1✔
3487
            raise ResourceNotFoundException(
1✔
3488
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3489
            )
3490

3491
        config = fn.event_invoke_configs.get(resolved_qualifier)
1✔
3492
        if not config:
1✔
3493
            raise ResourceNotFoundException(
1✔
3494
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3495
            )
3496

3497
        del fn.event_invoke_configs[resolved_qualifier]
1✔
3498

3499
    def update_function_event_invoke_config(
1✔
3500
        self,
3501
        context: RequestContext,
3502
        function_name: FunctionName,
3503
        qualifier: Qualifier = None,
3504
        maximum_retry_attempts: MaximumRetryAttempts = None,
3505
        maximum_event_age_in_seconds: MaximumEventAgeInSeconds = None,
3506
        destination_config: DestinationConfig = None,
3507
        **kwargs,
3508
    ) -> FunctionEventInvokeConfig:
3509
        # like put but only update single fields via replace
3510
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3511
        state = lambda_stores[account_id][region]
1✔
3512
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3513
            function_name, qualifier, context
3514
        )
3515

3516
        if (
1✔
3517
            maximum_event_age_in_seconds is None
3518
            and maximum_retry_attempts is None
3519
            and destination_config is None
3520
        ):
UNCOV
3521
            raise InvalidParameterValueException(
×
3522
                "You must specify at least one of error handling or destination setting.",
3523
                Type="User",
3524
            )
3525

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

3530
        qualifier = qualifier or "$LATEST"
1✔
3531

3532
        config = fn.event_invoke_configs.get(qualifier)
1✔
3533
        if not config:
1✔
3534
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3535
            raise ResourceNotFoundException(
1✔
3536
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3537
            )
3538

3539
        if destination_config:
1✔
UNCOV
3540
            self._validate_destination_config(state, function_name, destination_config)
×
3541

3542
        optional_kwargs = {
1✔
3543
            k: v
3544
            for k, v in {
3545
                "destination_config": destination_config,
3546
                "maximum_retry_attempts": maximum_retry_attempts,
3547
                "maximum_event_age_in_seconds": maximum_event_age_in_seconds,
3548
            }.items()
3549
            if v is not None
3550
        }
3551

3552
        new_config = dataclasses.replace(
1✔
3553
            config, last_modified=api_utils.generate_lambda_date(), **optional_kwargs
3554
        )
3555
        fn.event_invoke_configs[qualifier] = new_config
1✔
3556

3557
        return FunctionEventInvokeConfig(
1✔
3558
            LastModified=datetime.datetime.strptime(
3559
                new_config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3560
            ),
3561
            FunctionArn=api_utils.qualified_lambda_arn(
3562
                function_name, qualifier or "$LATEST", account_id, region
3563
            ),
3564
            DestinationConfig=new_config.destination_config,
3565
            MaximumEventAgeInSeconds=new_config.maximum_event_age_in_seconds,
3566
            MaximumRetryAttempts=new_config.maximum_retry_attempts,
3567
        )
3568

3569
    # =======================================
3570
    # ======  Layer & Layer Versions  =======
3571
    # =======================================
3572

3573
    @staticmethod
1✔
3574
    def _resolve_layer(
1✔
3575
        layer_name_or_arn: str, context: RequestContext
3576
    ) -> Tuple[str, str, str, Optional[str]]:
3577
        """
3578
        Return locator attributes for a given Lambda layer.
3579

3580
        :param layer_name_or_arn: Layer name or ARN
3581
        :param context: Request context
3582
        :return: Tuple of region, account ID, layer name, layer version
3583
        """
3584
        if api_utils.is_layer_arn(layer_name_or_arn):
1✔
3585
            return api_utils.parse_layer_arn(layer_name_or_arn)
1✔
3586

3587
        return context.region, context.account_id, layer_name_or_arn, None
1✔
3588

3589
    def publish_layer_version(
1✔
3590
        self,
3591
        context: RequestContext,
3592
        layer_name: LayerName,
3593
        content: LayerVersionContentInput,
3594
        description: Description = None,
3595
        compatible_runtimes: CompatibleRuntimes = None,
3596
        license_info: LicenseInfo = None,
3597
        compatible_architectures: CompatibleArchitectures = None,
3598
        **kwargs,
3599
    ) -> PublishLayerVersionResponse:
3600
        """
3601
        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.
3602
        Note that there are no $LATEST versions with layers!
3603

3604
        """
3605
        account = context.account_id
1✔
3606
        region = context.region
1✔
3607

3608
        validation_errors = api_utils.validate_layer_runtimes_and_architectures(
1✔
3609
            compatible_runtimes, compatible_architectures
3610
        )
3611
        if validation_errors:
1✔
3612
            raise ValidationException(
1✔
3613
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {'; '.join(validation_errors)}"
3614
            )
3615

3616
        state = lambda_stores[account][region]
1✔
3617
        with self.create_layer_lock:
1✔
3618
            if layer_name not in state.layers:
1✔
3619
                # we don't have a version so create new layer object
3620
                # lock is required to avoid creating two v1 objects for the same name
3621
                layer = Layer(
1✔
3622
                    arn=api_utils.layer_arn(layer_name=layer_name, account=account, region=region)
3623
                )
3624
                state.layers[layer_name] = layer
1✔
3625

3626
        layer = state.layers[layer_name]
1✔
3627
        with layer.next_version_lock:
1✔
3628
            next_version = LambdaLayerVersionIdentifier(
1✔
3629
                account_id=account, region=region, layer_name=layer_name
3630
            ).generate(next_version=layer.next_version)
3631
            # When creating a layer with user defined layer version, it is possible that we
3632
            # create layer versions out of order.
3633
            # ie. a user could replicate layer v2 then layer v1. It is important to always keep the maximum possible
3634
            # value for next layer to avoid overwriting existing versions
3635
            if layer.next_version <= next_version:
1✔
3636
                # We don't need to update layer.next_version if the created version is lower than the "next in line"
3637
                layer.next_version = max(next_version, layer.next_version) + 1
1✔
3638

3639
        # creating a new layer
3640
        if content.get("ZipFile"):
1✔
3641
            code = store_lambda_archive(
1✔
3642
                archive_file=content["ZipFile"],
3643
                function_name=layer_name,
3644
                region_name=region,
3645
                account_id=account,
3646
            )
3647
        else:
3648
            code = store_s3_bucket_archive(
1✔
3649
                archive_bucket=content["S3Bucket"],
3650
                archive_key=content["S3Key"],
3651
                archive_version=content.get("S3ObjectVersion"),
3652
                function_name=layer_name,
3653
                region_name=region,
3654
                account_id=account,
3655
            )
3656

3657
        new_layer_version = LayerVersion(
1✔
3658
            layer_version_arn=api_utils.layer_version_arn(
3659
                layer_name=layer_name,
3660
                account=account,
3661
                region=region,
3662
                version=str(next_version),
3663
            ),
3664
            layer_arn=layer.arn,
3665
            version=next_version,
3666
            description=description or "",
3667
            license_info=license_info,
3668
            compatible_runtimes=compatible_runtimes,
3669
            compatible_architectures=compatible_architectures,
3670
            created=api_utils.generate_lambda_date(),
3671
            code=code,
3672
        )
3673

3674
        layer.layer_versions[str(next_version)] = new_layer_version
1✔
3675

3676
        return api_utils.map_layer_out(new_layer_version)
1✔
3677

3678
    def get_layer_version(
1✔
3679
        self,
3680
        context: RequestContext,
3681
        layer_name: LayerName,
3682
        version_number: LayerVersionNumber,
3683
        **kwargs,
3684
    ) -> GetLayerVersionResponse:
3685
        # TODO: handle layer_name as an ARN
3686

3687
        region_name, account_id, layer_name, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
3688
        state = lambda_stores[account_id][region_name]
1✔
3689

3690
        layer = state.layers.get(layer_name)
1✔
3691
        if version_number < 1:
1✔
3692
            raise InvalidParameterValueException("Layer Version Cannot be less than 1", Type="User")
1✔
3693
        if layer is None:
1✔
3694
            raise ResourceNotFoundException(
1✔
3695
                "The resource you requested does not exist.", Type="User"
3696
            )
3697
        layer_version = layer.layer_versions.get(str(version_number))
1✔
3698
        if layer_version is None:
1✔
3699
            raise ResourceNotFoundException(
1✔
3700
                "The resource you requested does not exist.", Type="User"
3701
            )
3702
        return api_utils.map_layer_out(layer_version)
1✔
3703

3704
    def get_layer_version_by_arn(
1✔
3705
        self, context: RequestContext, arn: LayerVersionArn, **kwargs
3706
    ) -> GetLayerVersionResponse:
3707
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
3708
            arn, context
3709
        )
3710

3711
        if not layer_version:
1✔
3712
            raise ValidationException(
1✔
3713
                f"1 validation error detected: Value '{arn}' at 'arn' failed to satisfy constraint: Member must satisfy regular expression pattern: "
3714
                + "(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-_]+)"
3715
            )
3716

3717
        store = lambda_stores[account_id][region_name]
1✔
3718
        if not (layers := store.layers.get(layer_name)):
1✔
UNCOV
3719
            raise ResourceNotFoundException(
×
3720
                "The resource you requested does not exist.", Type="User"
3721
            )
3722

3723
        layer_version = layers.layer_versions.get(layer_version)
1✔
3724

3725
        if not layer_version:
1✔
3726
            raise ResourceNotFoundException(
1✔
3727
                "The resource you requested does not exist.", Type="User"
3728
            )
3729

3730
        return api_utils.map_layer_out(layer_version)
1✔
3731

3732
    def list_layers(
1✔
3733
        self,
3734
        context: RequestContext,
3735
        compatible_runtime: Runtime = None,
3736
        marker: String = None,
3737
        max_items: MaxLayerListItems = None,
3738
        compatible_architecture: Architecture = None,
3739
        **kwargs,
3740
    ) -> ListLayersResponse:
3741
        validation_errors = []
1✔
3742

3743
        validation_error_arch = api_utils.validate_layer_architecture(compatible_architecture)
1✔
3744
        if validation_error_arch:
1✔
3745
            validation_errors.append(validation_error_arch)
1✔
3746

3747
        validation_error_runtime = api_utils.validate_layer_runtime(compatible_runtime)
1✔
3748
        if validation_error_runtime:
1✔
3749
            validation_errors.append(validation_error_runtime)
1✔
3750

3751
        if validation_errors:
1✔
3752
            raise ValidationException(
1✔
3753
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {';'.join(validation_errors)}"
3754
            )
3755
        # TODO: handle filter: compatible_runtime
3756
        # TODO: handle filter: compatible_architecture
3757

UNCOV
3758
        state = lambda_stores[context.account_id][context.region]
×
UNCOV
3759
        layers = state.layers
×
3760

3761
        # 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?
3762

3763
        responses: list[LayersListItem] = []
×
UNCOV
3764
        for layer_name, layer in layers.items():
×
3765
            # fetch latest version
UNCOV
3766
            layer_versions = list(layer.layer_versions.values())
×
UNCOV
3767
            sorted(layer_versions, key=lambda x: x.version)
×
UNCOV
3768
            latest_layer_version = layer_versions[-1]
×
UNCOV
3769
            responses.append(
×
3770
                LayersListItem(
3771
                    LayerName=layer_name,
3772
                    LayerArn=layer.arn,
3773
                    LatestMatchingVersion=api_utils.map_layer_out(latest_layer_version),
3774
                )
3775
            )
3776

UNCOV
3777
        responses = PaginatedList(responses)
×
UNCOV
3778
        page, token = responses.get_page(
×
3779
            lambda version: version,
3780
            marker,
3781
            max_items,
3782
        )
3783

UNCOV
3784
        return ListLayersResponse(NextMarker=token, Layers=page)
×
3785

3786
    def list_layer_versions(
1✔
3787
        self,
3788
        context: RequestContext,
3789
        layer_name: LayerName,
3790
        compatible_runtime: Runtime = None,
3791
        marker: String = None,
3792
        max_items: MaxLayerListItems = None,
3793
        compatible_architecture: Architecture = None,
3794
        **kwargs,
3795
    ) -> ListLayerVersionsResponse:
3796
        validation_errors = api_utils.validate_layer_runtimes_and_architectures(
1✔
3797
            [compatible_runtime] if compatible_runtime else [],
3798
            [compatible_architecture] if compatible_architecture else [],
3799
        )
3800
        if validation_errors:
1✔
UNCOV
3801
            raise ValidationException(
×
3802
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {';'.join(validation_errors)}"
3803
            )
3804

3805
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
3806
            layer_name, context
3807
        )
3808
        state = lambda_stores[account_id][region_name]
1✔
3809

3810
        # TODO: Test & handle filter: compatible_runtime
3811
        # TODO: Test & handle filter: compatible_architecture
3812
        all_layer_versions = []
1✔
3813
        layer = state.layers.get(layer_name)
1✔
3814
        if layer is not None:
1✔
3815
            for layer_version in layer.layer_versions.values():
1✔
3816
                all_layer_versions.append(api_utils.map_layer_out(layer_version))
1✔
3817

3818
        all_layer_versions.sort(key=lambda x: x["Version"], reverse=True)
1✔
3819
        all_layer_versions = PaginatedList(all_layer_versions)
1✔
3820
        page, token = all_layer_versions.get_page(
1✔
3821
            lambda version: version["LayerVersionArn"],
3822
            marker,
3823
            max_items,
3824
        )
3825
        return ListLayerVersionsResponse(NextMarker=token, LayerVersions=page)
1✔
3826

3827
    def delete_layer_version(
1✔
3828
        self,
3829
        context: RequestContext,
3830
        layer_name: LayerName,
3831
        version_number: LayerVersionNumber,
3832
        **kwargs,
3833
    ) -> None:
3834
        if version_number < 1:
1✔
3835
            raise InvalidParameterValueException("Layer Version Cannot be less than 1", Type="User")
1✔
3836

3837
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
3838
            layer_name, context
3839
        )
3840

3841
        store = lambda_stores[account_id][region_name]
1✔
3842
        layer = store.layers.get(layer_name, {})
1✔
3843
        if layer:
1✔
3844
            layer.layer_versions.pop(str(version_number), None)
1✔
3845

3846
    # =======================================
3847
    # =====  Layer Version Permissions  =====
3848
    # =======================================
3849
    # TODO: lock updates that change revision IDs
3850

3851
    def add_layer_version_permission(
1✔
3852
        self,
3853
        context: RequestContext,
3854
        layer_name: LayerName,
3855
        version_number: LayerVersionNumber,
3856
        statement_id: StatementId,
3857
        action: LayerPermissionAllowedAction,
3858
        principal: LayerPermissionAllowedPrincipal,
3859
        organization_id: OrganizationId = None,
3860
        revision_id: String = None,
3861
        **kwargs,
3862
    ) -> AddLayerVersionPermissionResponse:
3863
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
3864
        # `layer_n` contains the layer name.
3865
        region_name, account_id, layer_n, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
3866

3867
        if action != "lambda:GetLayerVersion":
1✔
3868
            raise ValidationException(
1✔
3869
                f"1 validation error detected: Value '{action}' at 'action' failed to satisfy constraint: Member must satisfy regular expression pattern: lambda:GetLayerVersion"
3870
            )
3871

3872
        store = lambda_stores[account_id][region_name]
1✔
3873
        layer = store.layers.get(layer_n)
1✔
3874

3875
        layer_version_arn = api_utils.layer_version_arn(
1✔
3876
            layer_name, account_id, region_name, str(version_number)
3877
        )
3878

3879
        if layer is None:
1✔
3880
            raise ResourceNotFoundException(
1✔
3881
                f"Layer version {layer_version_arn} does not exist.", Type="User"
3882
            )
3883
        layer_version = layer.layer_versions.get(str(version_number))
1✔
3884
        if layer_version is None:
1✔
3885
            raise ResourceNotFoundException(
1✔
3886
                f"Layer version {layer_version_arn} does not exist.", Type="User"
3887
            )
3888
        # do we have a policy? if not set one
3889
        if layer_version.policy is None:
1✔
3890
            layer_version.policy = LayerPolicy()
1✔
3891

3892
        if statement_id in layer_version.policy.statements:
1✔
3893
            raise ResourceConflictException(
1✔
3894
                f"The statement id ({statement_id}) provided already exists. Please provide a new statement id, or remove the existing statement.",
3895
                Type="User",
3896
            )
3897

3898
        if revision_id and layer_version.policy.revision_id != revision_id:
1✔
3899
            raise PreconditionFailedException(
1✔
3900
                "The Revision Id provided does not match the latest Revision Id. "
3901
                "Call the GetLayerPolicy API to retrieve the latest Revision Id",
3902
                Type="User",
3903
            )
3904

3905
        statement = LayerPolicyStatement(
1✔
3906
            sid=statement_id, action=action, principal=principal, organization_id=organization_id
3907
        )
3908

3909
        old_statements = layer_version.policy.statements
1✔
3910
        layer_version.policy = dataclasses.replace(
1✔
3911
            layer_version.policy, statements={**old_statements, statement_id: statement}
3912
        )
3913

3914
        return AddLayerVersionPermissionResponse(
1✔
3915
            Statement=json.dumps(
3916
                {
3917
                    "Sid": statement.sid,
3918
                    "Effect": "Allow",
3919
                    "Principal": statement.principal,
3920
                    "Action": statement.action,
3921
                    "Resource": layer_version.layer_version_arn,
3922
                }
3923
            ),
3924
            RevisionId=layer_version.policy.revision_id,
3925
        )
3926

3927
    def remove_layer_version_permission(
1✔
3928
        self,
3929
        context: RequestContext,
3930
        layer_name: LayerName,
3931
        version_number: LayerVersionNumber,
3932
        statement_id: StatementId,
3933
        revision_id: String = None,
3934
        **kwargs,
3935
    ) -> None:
3936
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
3937
        # `layer_n` contains the layer name.
3938
        region_name, account_id, layer_n, layer_version = LambdaProvider._resolve_layer(
1✔
3939
            layer_name, context
3940
        )
3941

3942
        layer_version_arn = api_utils.layer_version_arn(
1✔
3943
            layer_name, account_id, region_name, str(version_number)
3944
        )
3945

3946
        state = lambda_stores[account_id][region_name]
1✔
3947
        layer = state.layers.get(layer_n)
1✔
3948
        if layer is None:
1✔
3949
            raise ResourceNotFoundException(
1✔
3950
                f"Layer version {layer_version_arn} does not exist.", Type="User"
3951
            )
3952
        layer_version = layer.layer_versions.get(str(version_number))
1✔
3953
        if layer_version is None:
1✔
3954
            raise ResourceNotFoundException(
1✔
3955
                f"Layer version {layer_version_arn} does not exist.", Type="User"
3956
            )
3957

3958
        if revision_id and layer_version.policy.revision_id != revision_id:
1✔
3959
            raise PreconditionFailedException(
1✔
3960
                "The Revision Id provided does not match the latest Revision Id. "
3961
                "Call the GetLayerPolicy API to retrieve the latest Revision Id",
3962
                Type="User",
3963
            )
3964

3965
        if statement_id not in layer_version.policy.statements:
1✔
3966
            raise ResourceNotFoundException(
1✔
3967
                f"Statement {statement_id} is not found in resource policy.", Type="User"
3968
            )
3969

3970
        old_statements = layer_version.policy.statements
1✔
3971
        layer_version.policy = dataclasses.replace(
1✔
3972
            layer_version.policy,
3973
            statements={k: v for k, v in old_statements.items() if k != statement_id},
3974
        )
3975

3976
    def get_layer_version_policy(
1✔
3977
        self,
3978
        context: RequestContext,
3979
        layer_name: LayerName,
3980
        version_number: LayerVersionNumber,
3981
        **kwargs,
3982
    ) -> GetLayerVersionPolicyResponse:
3983
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
3984
        # `layer_n` contains the layer name.
3985
        region_name, account_id, layer_n, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
3986

3987
        layer_version_arn = api_utils.layer_version_arn(
1✔
3988
            layer_name, account_id, region_name, str(version_number)
3989
        )
3990

3991
        store = lambda_stores[account_id][region_name]
1✔
3992
        layer = store.layers.get(layer_n)
1✔
3993

3994
        if layer is None:
1✔
3995
            raise ResourceNotFoundException(
1✔
3996
                f"Layer version {layer_version_arn} does not exist.", Type="User"
3997
            )
3998

3999
        layer_version = layer.layer_versions.get(str(version_number))
1✔
4000
        if layer_version is None:
1✔
4001
            raise ResourceNotFoundException(
1✔
4002
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4003
            )
4004

4005
        if layer_version.policy is None:
1✔
4006
            raise ResourceNotFoundException(
1✔
4007
                "No policy is associated with the given resource.", Type="User"
4008
            )
4009

4010
        return GetLayerVersionPolicyResponse(
1✔
4011
            Policy=json.dumps(
4012
                {
4013
                    "Version": layer_version.policy.version,
4014
                    "Id": layer_version.policy.id,
4015
                    "Statement": [
4016
                        {
4017
                            "Sid": ps.sid,
4018
                            "Effect": "Allow",
4019
                            "Principal": ps.principal,
4020
                            "Action": ps.action,
4021
                            "Resource": layer_version.layer_version_arn,
4022
                        }
4023
                        for ps in layer_version.policy.statements.values()
4024
                    ],
4025
                }
4026
            ),
4027
            RevisionId=layer_version.policy.revision_id,
4028
        )
4029

4030
    # =======================================
4031
    # =======  Function Concurrency  ========
4032
    # =======================================
4033
    # (Reserved) function concurrency is scoped to the whole function
4034

4035
    def get_function_concurrency(
1✔
4036
        self, context: RequestContext, function_name: FunctionName, **kwargs
4037
    ) -> GetFunctionConcurrencyResponse:
4038
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4039
        function_name = api_utils.get_function_name(function_name, context)
1✔
4040
        fn = self._get_function(function_name=function_name, region=region, account_id=account_id)
1✔
4041
        return GetFunctionConcurrencyResponse(
1✔
4042
            ReservedConcurrentExecutions=fn.reserved_concurrent_executions
4043
        )
4044

4045
    def put_function_concurrency(
1✔
4046
        self,
4047
        context: RequestContext,
4048
        function_name: FunctionName,
4049
        reserved_concurrent_executions: ReservedConcurrentExecutions,
4050
        **kwargs,
4051
    ) -> Concurrency:
4052
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4053

4054
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
4055
        if qualifier:
1✔
4056
            raise InvalidParameterValueException(
1✔
4057
                "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.",
4058
                Type="User",
4059
            )
4060

4061
        store = lambda_stores[account_id][region]
1✔
4062
        fn = store.functions.get(function_name)
1✔
4063
        if not fn:
1✔
4064
            fn_arn = api_utils.qualified_lambda_arn(
1✔
4065
                function_name,
4066
                qualifier="$LATEST",
4067
                account=account_id,
4068
                region=region,
4069
            )
4070
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
4071

4072
        settings = self.get_account_settings(context)
1✔
4073
        unreserved_concurrent_executions = settings["AccountLimit"][
1✔
4074
            "UnreservedConcurrentExecutions"
4075
        ]
4076

4077
        # The existing reserved concurrent executions for the same function are already deduced in
4078
        # unreserved_concurrent_executions but must not count because the new one will replace the existing one.
4079
        # Joel tested this behavior manually against AWS (2023-11-28).
4080
        existing_reserved_concurrent_executions = (
1✔
4081
            fn.reserved_concurrent_executions if fn.reserved_concurrent_executions else 0
4082
        )
4083
        if (
1✔
4084
            unreserved_concurrent_executions
4085
            - reserved_concurrent_executions
4086
            + existing_reserved_concurrent_executions
4087
        ) < config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY:
4088
            raise InvalidParameterValueException(
1✔
4089
                f"Specified ReservedConcurrentExecutions for function decreases account's UnreservedConcurrentExecution below its minimum value of [{config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY}]."
4090
            )
4091

4092
        total_provisioned_concurrency = sum(
1✔
4093
            [
4094
                provisioned_configs.provisioned_concurrent_executions
4095
                for provisioned_configs in fn.provisioned_concurrency_configs.values()
4096
            ]
4097
        )
4098
        if total_provisioned_concurrency > reserved_concurrent_executions:
1✔
4099
            raise InvalidParameterValueException(
1✔
4100
                f" ReservedConcurrentExecutions  {reserved_concurrent_executions} should not be lower than function's total provisioned concurrency [{total_provisioned_concurrency}]."
4101
            )
4102

4103
        fn.reserved_concurrent_executions = reserved_concurrent_executions
1✔
4104

4105
        return Concurrency(ReservedConcurrentExecutions=fn.reserved_concurrent_executions)
1✔
4106

4107
    def delete_function_concurrency(
1✔
4108
        self, context: RequestContext, function_name: FunctionName, **kwargs
4109
    ) -> None:
4110
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4111
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
4112
        store = lambda_stores[account_id][region]
1✔
4113
        fn = store.functions.get(function_name)
1✔
4114
        fn.reserved_concurrent_executions = None
1✔
4115

4116
    # =======================================
4117
    # ===============  TAGS   ===============
4118
    # =======================================
4119
    # only Function, Event Source Mapping, and Code Signing Config (not currently supported by LocalStack) ARNs an are available for tagging in AWS
4120

4121
    def _get_tags(self, resource: TaggableResource) -> dict[str, str]:
1✔
4122
        state = self.fetch_lambda_store_for_tagging(resource)
1✔
4123
        lambda_adapted_tags = {
1✔
4124
            tag["Key"]: tag["Value"]
4125
            for tag in state.TAGS.list_tags_for_resource(resource).get("Tags")
4126
        }
4127
        return lambda_adapted_tags
1✔
4128

4129
    def _store_tags(self, resource: TaggableResource, tags: dict[str, str]):
1✔
4130
        state = self.fetch_lambda_store_for_tagging(resource)
1✔
4131
        if len(state.TAGS.tags.get(resource, {}) | tags) > LAMBDA_TAG_LIMIT_PER_RESOURCE:
1✔
4132
            raise InvalidParameterValueException(
1✔
4133
                "Number of tags exceeds resource tag limit.", Type="User"
4134
            )
4135

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

4139
    def fetch_lambda_store_for_tagging(self, resource: TaggableResource) -> LambdaStore:
1✔
4140
        """
4141
        Takes a resource ARN for a TaggableResource (Lambda Function, Event Source Mapping, or Code Signing Config) and returns a corresponding
4142
        LambdaStore for its region and account.
4143

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

4146
        Raises:
4147
            ValidationException: If the resource ARN is not a full ARN for a TaggableResource.
4148
            ResourceNotFoundException: If the specified resource does not exist.
4149
            InvalidParameterValueException: If the resource ARN is a qualified Lambda Function.
4150
        """
4151

4152
        def _raise_validation_exception():
1✔
4153
            raise ValidationException(
1✔
4154
                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}"
4155
            )
4156

4157
        # Check whether the ARN we have been passed is correctly formatted
4158
        parsed_resource_arn: ArnData = None
1✔
4159
        try:
1✔
4160
            parsed_resource_arn = parse_arn(resource)
1✔
4161
        except Exception:
1✔
4162
            _raise_validation_exception()
1✔
4163

4164
        # TODO: Should we be checking whether this is a full ARN?
4165
        region, account_id, resource_type = map(
1✔
4166
            parsed_resource_arn.get, ("region", "account", "resource")
4167
        )
4168

4169
        if not all((region, account_id, resource_type)):
1✔
UNCOV
4170
            _raise_validation_exception()
×
4171

4172
        if not (parts := resource_type.split(":")):
1✔
UNCOV
4173
            _raise_validation_exception()
×
4174

4175
        resource_type, resource_identifier, *qualifier = parts
1✔
4176
        if resource_type not in {"event-source-mapping", "code-signing-config", "function"}:
1✔
4177
            _raise_validation_exception()
1✔
4178

4179
        if qualifier:
1✔
4180
            if resource_type == "function":
1✔
4181
                raise InvalidParameterValueException(
1✔
4182
                    "Tags on function aliases and versions are not supported. Please specify a function ARN.",
4183
                    Type="User",
4184
                )
4185
            _raise_validation_exception()
1✔
4186

4187
        match resource_type:
1✔
4188
            case "event-source-mapping":
1✔
4189
                self._get_esm(resource_identifier, account_id, region)
1✔
4190
            case "code-signing-config":
1✔
4191
                raise NotImplementedError("Resource tagging on CSC not yet implemented.")
4192
            case "function":
1✔
4193
                self._get_function(
1✔
4194
                    function_name=resource_identifier, account_id=account_id, region=region
4195
                )
4196

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

4200
    def tag_resource(
1✔
4201
        self, context: RequestContext, resource: TaggableResource, tags: Tags, **kwargs
4202
    ) -> None:
4203
        if not tags:
1✔
4204
            raise InvalidParameterValueException(
1✔
4205
                "An error occurred and the request cannot be processed.", Type="User"
4206
            )
4207
        self._store_tags(resource, tags)
1✔
4208

4209
        if (resource_id := extract_resource_from_arn(resource)) and resource_id.startswith(
1✔
4210
            "function"
4211
        ):
4212
            name, _, account, region = function_locators_from_arn(resource)
1✔
4213
            function = self._get_function(name, account, region)
1✔
4214
            with function.lock:
1✔
4215
                # dirty hack for changed revision id, should reevaluate model to prevent this:
4216
                latest_version = function.versions["$LATEST"]
1✔
4217
                function.versions["$LATEST"] = dataclasses.replace(
1✔
4218
                    latest_version, config=dataclasses.replace(latest_version.config)
4219
                )
4220

4221
    def list_tags(
1✔
4222
        self, context: RequestContext, resource: TaggableResource, **kwargs
4223
    ) -> ListTagsResponse:
4224
        tags = self._get_tags(resource)
1✔
4225
        return ListTagsResponse(Tags=tags)
1✔
4226

4227
    def untag_resource(
1✔
4228
        self, context: RequestContext, resource: TaggableResource, tag_keys: TagKeyList, **kwargs
4229
    ) -> None:
4230
        if not tag_keys:
1✔
4231
            raise ValidationException(
1✔
4232
                "1 validation error detected: Value null at 'tagKeys' failed to satisfy constraint: Member must not be null"
4233
            )  # should probably be generalized a bit
4234

4235
        state = self.fetch_lambda_store_for_tagging(resource)
1✔
4236
        state.TAGS.untag_resource(resource, tag_keys)
1✔
4237

4238
        if (resource_id := extract_resource_from_arn(resource)) and resource_id.startswith(
1✔
4239
            "function"
4240
        ):
4241
            name, _, account, region = function_locators_from_arn(resource)
1✔
4242
            function = self._get_function(name, account, region)
1✔
4243
            # TODO: Potential race condition
4244
            with function.lock:
1✔
4245
                # dirty hack for changed revision id, should reevaluate model to prevent this:
4246
                latest_version = function.versions["$LATEST"]
1✔
4247
                function.versions["$LATEST"] = dataclasses.replace(
1✔
4248
                    latest_version, config=dataclasses.replace(latest_version.config)
4249
                )
4250

4251
    # =======================================
4252
    # =======  LEGACY / DEPRECATED   ========
4253
    # =======================================
4254

4255
    def invoke_async(
1✔
4256
        self,
4257
        context: RequestContext,
4258
        function_name: NamespacedFunctionName,
4259
        invoke_args: IO[BlobStream],
4260
        **kwargs,
4261
    ) -> InvokeAsyncResponse:
4262
        """LEGACY API endpoint. Even AWS heavily discourages its usage."""
4263
        raise NotImplementedError
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc