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

localstack / localstack / 22166837280

19 Feb 2026 02:47AM UTC coverage: 87.003% (-0.004%) from 87.007%
22166837280

push

github

web-flow
SNS: Move Tagging Functionality to Provider Methods (#13608)

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

122 existing lines in 11 files now uncovered.

69763 of 80185 relevant lines covered (87.0%)

0.87 hits per line

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

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

12
from botocore.exceptions import ClientError
1✔
13

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

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

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

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

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

273

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

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

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

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

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

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

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

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

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

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

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

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

397
                    function_version = get_function_version_from_arn(function_arn)
×
398
                    function_role = function_version.config.role
×
399

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

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

414
    def on_after_init(self):
1✔
415
        self.router.register_routes()
1✔
416
        get_runtime_executor().validate_environment()
1✔
417

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

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

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

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

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

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

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

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

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

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

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

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

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

544
        subnet_id = subnet_ids[0]
1✔
545
        if not bool(SUBNET_ID_REGEX.match(subnet_id)):
1✔
546
            raise ValidationException(
1✔
547
                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]*]"
548
            )
549

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

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

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

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

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

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

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

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

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

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

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

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

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

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

776
    @staticmethod
1✔
777
    def _verify_env_variables(env_vars: dict[str, str]):
1✔
778
        dumped_env_vars = json.dumps(env_vars, separators=(",", ":"))
1✔
779
        if (
1✔
780
            len(dumped_env_vars.encode("utf-8"))
781
            > config.LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES
782
        ):
783
            raise InvalidParameterValueException(
1✔
784
                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}",
785
                Type="User",
786
            )
787

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

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

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

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

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

857
                    layer = self.layer_fetcher.fetch_layer(layer_version_arn)
×
858
                    if layer is None:
×
859
                        # TODO: detect user or role from context when IAM users are implemented
860
                        user = "user/localstack-testing"
×
861
                        raise AccessDeniedException(
×
862
                            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"
863
                        )
864

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

875
            # only the first two matches in the array are considered for the error message
876
            layer_arn = ":".join(layer_version_arn.split(":")[:-1])
1✔
877
            if layer_arn in visited_layers:
1✔
878
                conflict_layer_version_arn = visited_layers[layer_arn]
1✔
879
                raise InvalidParameterValueException(
1✔
880
                    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.",
881
                    Type="User",
882
                )
883
            visited_layers[layer_arn] = layer_version_arn
1✔
884

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

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

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

906
        capacity_provider_name = capacity_provider_arn.split(":")[-1]
×
907
        self.get_capacity_provider(context, capacity_provider_name)
×
908

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

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

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

942
        fn = self._get_function(function_name=function_name, region=region, account_id=account_id)
1✔
943

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

951
        fn.recursive_loop = recursive_loop
1✔
952
        return PutFunctionRecursionConfigResponse(RecursiveLoop=fn.recursive_loop)
1✔
953

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

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

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

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

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

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

993
        if not api_utils.is_role_arn(request.get("Role")):
1✔
994
            raise ValidationException(
1✔
995
                f"1 validation error detected: Value '{request.get('Role')}'"
996
                + " 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+=,.@\\-_/]+"
997
            )
998
        if not self.lambda_service.can_assume_role(request.get("Role"), context.region):
1✔
999
            raise InvalidParameterValueException(
×
1000
                "The role defined for the function cannot be assumed by Lambda.", Type="User"
1001
            )
1002
        package_type = request.get("PackageType", PackageType.Zip)
1✔
1003
        runtime = request.get("Runtime")
1✔
1004
        self._validate_runtime(package_type, runtime)
1✔
1005

1006
        request_function_name = request.get("FunctionName")
1✔
1007

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

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

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

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

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

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

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

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

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

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

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

1248
        return api_utils.map_config_out(
1✔
1249
            version, return_qualified_arn=False, return_update_status=False
1250
        )
1251

1252
    def _validate_runtime(self, package_type, runtime):
1✔
1253
        runtimes = ALL_RUNTIMES
1✔
1254
        if config.LAMBDA_RUNTIME_VALIDATION:
1✔
1255
            runtimes = list(itertools.chain(RUNTIMES_AGGREGATED.values()))
1✔
1256

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

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

1271
    def _validate_managed_instances_runtime(self, runtime):
1✔
1272
        if runtime not in VALID_MANAGED_INSTANCE_RUNTIMES:
×
1273
            raise InvalidParameterValueException(
×
1274
                f"Runtime Enum {runtime} does not support specified feature: Lambda Managed Instances"
1275
            )
1276

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

1282
        if latest_runtime is not None:
1✔
1283
            LOG.debug(
1✔
1284
                "The Lambda runtime %s is deprecated. Please upgrade to a supported Lambda runtime such as %s.",
1285
                deprecated_runtime,
1286
                latest_runtime,
1287
            )
1288
            raise InvalidParameterValueException(
1✔
1289
                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.",
1290
                Type="User",
1291
            )
1292

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

1300
        # in case we got ARN or partial ARN
1301
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1302
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
1303
        state = lambda_stores[account_id][region]
1✔
1304

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

1312
        # TODO: lock modification of latest version
1313
        # TODO: notify service for changes relevant to re-provisioning of $LATEST
1314
        latest_version = function.latest()
1✔
1315
        latest_version_config = latest_version.config
1✔
1316

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

1325
        replace_kwargs = {}
1✔
1326
        if "EphemeralStorage" in request:
1✔
1327
            replace_kwargs["ephemeral_storage"] = LambdaEphemeralStorage(
×
1328
                request.get("EphemeralStorage", {}).get("Size", 512)
1329
            )  # TODO: do defaults here apply as well?
1330

1331
        if "Role" in request:
1✔
1332
            if not api_utils.is_role_arn(request["Role"]):
1✔
1333
                raise ValidationException(
1✔
1334
                    f"1 validation error detected: Value '{request.get('Role')}'"
1335
                    + " 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+=,.@\\-_/]+"
1336
                )
1337
            replace_kwargs["role"] = request["Role"]
1✔
1338

1339
        if "Description" in request:
1✔
1340
            replace_kwargs["description"] = request["Description"]
1✔
1341

1342
        if "Timeout" in request:
1✔
1343
            replace_kwargs["timeout"] = request["Timeout"]
1✔
1344

1345
        if "MemorySize" in request:
1✔
1346
            replace_kwargs["memory_size"] = request["MemorySize"]
1✔
1347

1348
        if "DeadLetterConfig" in request:
1✔
1349
            replace_kwargs["dead_letter_arn"] = request.get("DeadLetterConfig", {}).get("TargetArn")
1✔
1350

1351
        if vpc_config := request.get("VpcConfig"):
1✔
1352
            replace_kwargs["vpc_config"] = self._build_vpc_config(account_id, region, vpc_config)
1✔
1353

1354
        if "Handler" in request:
1✔
1355
            replace_kwargs["handler"] = request["Handler"]
1✔
1356

1357
        if "Runtime" in request:
1✔
1358
            runtime = request["Runtime"]
1✔
1359

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

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

1383
        if "Environment" in request:
1✔
1384
            if env_vars := request.get("Environment", {}).get("Variables", {}):
1✔
1385
                self._verify_env_variables(env_vars)
1✔
1386
            replace_kwargs["environment"] = env_vars
1✔
1387

1388
        if "Layers" in request:
1✔
1389
            new_layers = request["Layers"]
1✔
1390
            if new_layers:
1✔
1391
                self._validate_layers(new_layers, region=region, account_id=account_id)
1✔
1392
            replace_kwargs["layers"] = self.map_layers(new_layers)
1✔
1393

1394
        if "ImageConfig" in request:
1✔
1395
            new_image_config = request["ImageConfig"]
1✔
1396
            replace_kwargs["image_config"] = ImageConfig(
1✔
1397
                command=new_image_config.get("Command"),
1398
                entrypoint=new_image_config.get("EntryPoint"),
1399
                working_directory=new_image_config.get("WorkingDirectory"),
1400
            )
1401

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

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

1417
            last_config = latest_version_config.logging_config
1✔
1418

1419
            # add partial update
1420
            new_logging_config = last_config | logging_config
1✔
1421

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

1430
            replace_kwargs["logging_config"] = new_logging_config
1✔
1431

1432
        if "TracingConfig" in request:
1✔
1433
            new_mode = request.get("TracingConfig", {}).get("Mode")
×
1434
            if new_mode:
×
1435
                replace_kwargs["tracing_config_mode"] = new_mode
×
1436

1437
        if "CapacityProviderConfig" in request:
1✔
1438
            capacity_provider_config = request["CapacityProviderConfig"]
×
1439
            self._validate_capacity_provider_config(capacity_provider_config, context)
×
1440

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

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

1478
        return api_utils.map_config_out(new_latest_version)
1✔
1479

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

1489
        function_name = request.get("FunctionName")
1✔
1490
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1491
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
1492

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

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

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

1524
        if publish_to := request.get("PublishTo"):
1✔
1525
            self._validate_publish_to(publish_to)
×
1526

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

1551
        old_function_version = function.versions.get("$LATEST")
1✔
1552
        replace_kwargs = {"code": code} if code else {"image": image}
1✔
1553

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

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

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

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

1615
        if qualifier and api_utils.qualifier_is_alias(qualifier):
1✔
1616
            raise InvalidParameterValueException(
×
1617
                "Deletion of aliases is not currently supported.",
1618
                Type="User",
1619
            )
1620

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

1627
        unqualified_function_arn = api_utils.unqualified_lambda_arn(
1✔
1628
            function_name=function_name, region=region, account=account_id
1629
        )
1630
        if function_name not in store.functions:
1✔
1631
            e = ResourceNotFoundException(
1✔
1632
                f"Function not found: {unqualified_function_arn}",
1633
                Type="User",
1634
            )
1635
            raise e
1✔
1636
        function = store.functions.get(function_name)
1✔
1637

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

1666
        return DeleteFunctionResponse(StatusCode=202 if function_has_capacity_provider else 204)
1✔
1667

1668
    def list_functions(
1✔
1669
        self,
1670
        context: RequestContext,
1671
        master_region: MasterRegion = None,  # (only relevant for lambda@edge)
1672
        function_version: FunctionVersionApi = None,
1673
        marker: String = None,
1674
        max_items: MaxListItems = None,
1675
        **kwargs,
1676
    ) -> ListFunctionsResponse:
1677
        state = lambda_stores[context.account_id][context.region]
1✔
1678

1679
        if function_version and function_version != FunctionVersionApi.ALL:
1✔
1680
            raise ValidationException(
1✔
1681
                f"1 validation error detected: Value '{function_version}'"
1682
                + " at 'functionVersion' failed to satisfy constraint: Member must satisfy enum value set: [ALL]"
1683
            )
1684

1685
        if function_version == FunctionVersionApi.ALL:
1✔
1686
            # include all versions for all function
1687
            versions = [v for f in state.functions.values() for v in f.versions.values()]
1✔
1688
            return_qualified_arn = True
1✔
1689
        else:
1690
            versions = [f.latest() for f in state.functions.values()]
1✔
1691
            return_qualified_arn = False
1✔
1692

1693
        versions = [
1✔
1694
            api_utils.map_to_list_response(
1695
                api_utils.map_config_out(fc, return_qualified_arn=return_qualified_arn)
1696
            )
1697
            for fc in versions
1698
        ]
1699
        versions = PaginatedList(versions)
1✔
1700
        page, token = versions.get_page(
1✔
1701
            lambda version: version["FunctionArn"],
1702
            marker,
1703
            max_items,
1704
        )
1705
        return ListFunctionsResponse(Functions=page, NextMarker=token)
1✔
1706

1707
    def get_function(
1✔
1708
        self,
1709
        context: RequestContext,
1710
        function_name: NamespacedFunctionName,
1711
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
1712
        **kwargs,
1713
    ) -> GetFunctionResponse:
1714
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1715
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
1716
            function_name, qualifier, context
1717
        )
1718

1719
        fn = lambda_stores[account_id][region].functions.get(function_name)
1✔
1720
        if fn is None:
1✔
1721
            if qualifier is None:
1✔
1722
                raise ResourceNotFoundException(
1✔
1723
                    f"Function not found: {api_utils.unqualified_lambda_arn(function_name, account_id, region)}",
1724
                    Type="User",
1725
                )
1726
            else:
1727
                raise ResourceNotFoundException(
1✔
1728
                    f"Function not found: {api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)}",
1729
                    Type="User",
1730
                )
1731
        alias_name = None
1✔
1732
        if qualifier and api_utils.qualifier_is_alias(qualifier):
1✔
1733
            if qualifier not in fn.aliases:
1✔
1734
                alias_arn = api_utils.qualified_lambda_arn(
1✔
1735
                    function_name, qualifier, account_id, region
1736
                )
1737
                raise ResourceNotFoundException(f"Function not found: {alias_arn}", Type="User")
1✔
1738
            alias_name = qualifier
1✔
1739
            qualifier = fn.aliases[alias_name].function_version
1✔
1740

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

1769
        return GetFunctionResponse(
1✔
1770
            Configuration=api_utils.map_config_out(
1771
                version, return_qualified_arn=bool(qualifier), alias_name=alias_name
1772
            ),
1773
            Code=code_location,  # TODO
1774
            Concurrency=concurrency,
1775
            **additional_fields,
1776
        )
1777

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

1798
    def invoke(
1✔
1799
        self,
1800
        context: RequestContext,
1801
        function_name: NamespacedFunctionName,
1802
        invocation_type: InvocationType | None = None,
1803
        log_type: LogType | None = None,
1804
        client_context: String | None = None,
1805
        durable_execution_name: DurableExecutionName | None = None,
1806
        payload: IO[Blob] | None = None,
1807
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
1808
        tenant_id: TenantId | None = None,
1809
        **kwargs,
1810
    ) -> InvocationResponse:
1811
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
1812
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
1813
            function_name, qualifier, context
1814
        )
1815

1816
        user_agent = context.request.user_agent.string
1✔
1817

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

1849
        if invocation_type == InvocationType.Event:
1✔
1850
            # This happens when invocation type is event
1851
            return InvocationResponse(StatusCode=202)
1✔
1852
        if invocation_type == InvocationType.DryRun:
1✔
1853
            # This happens when invocation type is dryrun
1854
            return InvocationResponse(StatusCode=204)
1✔
1855
        LOG.debug("Lambda invocation duration: %0.2fms", (time.perf_counter() - time_before) * 1000)
1✔
1856

1857
        response = InvocationResponse(
1✔
1858
            StatusCode=200,
1859
            Payload=invocation_result.payload,
1860
            ExecutedVersion=invocation_result.executed_version,
1861
        )
1862

1863
        if invocation_result.is_error:
1✔
1864
            response["FunctionError"] = "Unhandled"
1✔
1865

1866
        if log_type == LogType.Tail:
1✔
1867
            response["LogResult"] = to_str(
1✔
1868
                base64.b64encode(to_bytes(invocation_result.logs)[-4096:])
1869
            )
1870

1871
        return response
1✔
1872

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

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

1926
    # Alias
1927

1928
    def _create_routing_config_model(
1✔
1929
        self, routing_config_dict: dict[str, float], function_version: FunctionVersion
1930
    ):
1931
        if len(routing_config_dict) > 1:
1✔
1932
            raise InvalidParameterValueException(
1✔
1933
                "Number of items in AdditionalVersionWeights cannot be greater than 1",
1934
                Type="User",
1935
            )
1936
        # should be exactly one item here, still iterating, might be supported in the future
1937
        for key, value in routing_config_dict.items():
1✔
1938
            if value < 0.0 or value >= 1.0:
1✔
1939
                raise ValidationException(
1✔
1940
                    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]"
1941
                )
1942
            if key == function_version.id.qualifier:
1✔
1943
                raise InvalidParameterValueException(
1✔
1944
                    f"Invalid function version {function_version.id.qualifier}. Function version {function_version.id.qualifier} is already included in routing configuration.",
1945
                    Type="User",
1946
                )
1947
            # check if version target is latest, then no routing config is allowed
1948
            if function_version.id.qualifier == "$LATEST":
1✔
1949
                raise InvalidParameterValueException(
1✔
1950
                    "$LATEST is not supported for an alias pointing to more than 1 version"
1951
                )
1952
            if not api_utils.qualifier_is_version(key):
1✔
1953
                raise ValidationException(
1✔
1954
                    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]+]"
1955
                )
1956

1957
            # checking if the version in the config exists
1958
            get_function_version(
1✔
1959
                function_name=function_version.id.function_name,
1960
                qualifier=key,
1961
                region=function_version.id.region,
1962
                account_id=function_version.id.account,
1963
            )
1964
        return AliasRoutingConfig(version_weights=routing_config_dict)
1✔
1965

1966
    def create_alias(
1✔
1967
        self,
1968
        context: RequestContext,
1969
        function_name: FunctionName,
1970
        name: Alias,
1971
        function_version: VersionWithLatestPublished,
1972
        description: Description = None,
1973
        routing_config: AliasRoutingConfiguration = None,
1974
        **kwargs,
1975
    ) -> AliasConfiguration:
1976
        if not api_utils.qualifier_is_alias(name):
1✔
1977
            raise ValidationException(
1✔
1978
                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-_]+)"
1979
            )
1980

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

2009
            alias = VersionAlias(
1✔
2010
                name=name,
2011
                function_version=function_version,
2012
                description=description,
2013
                routing_configuration=routing_configuration,
2014
            )
2015
            function.aliases[name] = alias
1✔
2016
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
2017

2018
    def list_aliases(
1✔
2019
        self,
2020
        context: RequestContext,
2021
        function_name: FunctionName,
2022
        function_version: VersionWithLatestPublished = None,
2023
        marker: String = None,
2024
        max_items: MaxListItems = None,
2025
        **kwargs,
2026
    ) -> ListAliasesResponse:
2027
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2028
        function_name = api_utils.get_function_name(function_name, context)
1✔
2029
        function = self._get_function(
1✔
2030
            function_name=function_name, region=region, account_id=account_id
2031
        )
2032
        aliases = [
1✔
2033
            api_utils.map_alias_out(alias, function)
2034
            for alias in function.aliases.values()
2035
            if function_version is None or alias.function_version == function_version
2036
        ]
2037

2038
        aliases = PaginatedList(aliases)
1✔
2039
        page, token = aliases.get_page(
1✔
2040
            lambda alias: alias["AliasArn"],
2041
            marker,
2042
            max_items,
2043
        )
2044

2045
        return ListAliasesResponse(Aliases=page, NextMarker=token)
1✔
2046

2047
    def delete_alias(
1✔
2048
        self, context: RequestContext, function_name: FunctionName, name: Alias, **kwargs
2049
    ) -> None:
2050
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2051
        function_name = api_utils.get_function_name(function_name, context)
1✔
2052
        function = self._get_function(
1✔
2053
            function_name=function_name, region=region, account_id=account_id
2054
        )
2055
        version_alias = function.aliases.pop(name, None)
1✔
2056

2057
        # cleanup related resources
2058
        if name in function.provisioned_concurrency_configs:
1✔
2059
            function.provisioned_concurrency_configs.pop(name)
1✔
2060

2061
        # TODO: Allow for deactivating/unregistering specific Lambda URLs
2062
        if version_alias and name in function.function_url_configs:
1✔
2063
            url_config = function.function_url_configs.pop(name)
1✔
2064
            LOG.debug(
1✔
2065
                "Stopping aliased Lambda Function URL %s for %s",
2066
                url_config.url,
2067
                url_config.function_name,
2068
            )
2069

2070
    def get_alias(
1✔
2071
        self, context: RequestContext, function_name: FunctionName, name: Alias, **kwargs
2072
    ) -> AliasConfiguration:
2073
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2074
        function_name = api_utils.get_function_name(function_name, context)
1✔
2075
        function = self._get_function(
1✔
2076
            function_name=function_name, region=region, account_id=account_id
2077
        )
2078
        if not (alias := function.aliases.get(name)):
1✔
2079
            raise ResourceNotFoundException(
1✔
2080
                f"Cannot find alias arn: {api_utils.qualified_lambda_arn(function_name=function_name, qualifier=name, region=region, account=account_id)}",
2081
                Type="User",
2082
            )
2083
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
2084

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

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

2132
        return api_utils.map_alias_out(alias=alias, function=function)
1✔
2133

2134
    # =======================================
2135
    # ======= EVENT SOURCE MAPPINGS =========
2136
    # =======================================
2137
    def check_service_resource_exists(
1✔
2138
        self, service: str, resource_arn: str, function_arn: str, function_role_arn: str
2139
    ):
2140
        """
2141
        Check if the service resource exists and if the function has access to it.
2142

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

2190
    @handler("CreateEventSourceMapping", expand=False)
1✔
2191
    def create_event_source_mapping(
1✔
2192
        self,
2193
        context: RequestContext,
2194
        request: CreateEventSourceMappingRequest,
2195
    ) -> EventSourceMappingConfiguration:
2196
        return self.create_event_source_mapping_v2(context, request)
1✔
2197

2198
    def create_event_source_mapping_v2(
1✔
2199
        self,
2200
        context: RequestContext,
2201
        request: CreateEventSourceMappingRequest,
2202
    ) -> EventSourceMappingConfiguration:
2203
        # Validations
2204
        function_arn, function_name, state, function_version, function_role = (
1✔
2205
            self.validate_event_source_mapping(context, request)
2206
        )
2207

2208
        esm_config = EsmConfigFactory(request, context, function_arn).get_esm_config()
1✔
2209

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

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

2228
        if destination_config := request.get("DestinationConfig"):
1✔
2229
            if "OnSuccess" in destination_config:
1✔
2230
                raise InvalidParameterValueException(
1✔
2231
                    "Unsupported DestinationConfig parameter for given event source mapping type.",
2232
                    Type="User",
2233
                )
2234

2235
        service = None
1✔
2236
        if "SelfManagedEventSource" in request:
1✔
UNCOV
2237
            service = "kafka"
×
UNCOV
2238
            if "SourceAccessConfigurations" not in request:
×
UNCOV
2239
                raise InvalidParameterValueException(
×
2240
                    "Required 'sourceAccessConfigurations' parameter is missing.", Type="User"
2241
                )
2242
        if service is None and "EventSourceArn" not in request:
1✔
2243
            raise InvalidParameterValueException("Unrecognized event source.", Type="User")
1✔
2244
        if service is None:
1✔
2245
            service = extract_service_from_arn(request["EventSourceArn"])
1✔
2246

2247
        batch_size = api_utils.validate_and_set_batch_size(service, request.get("BatchSize"))
1✔
2248
        if service in ["dynamodb", "kinesis"]:
1✔
2249
            starting_position = request.get("StartingPosition")
1✔
2250
            if not starting_position:
1✔
2251
                raise InvalidParameterValueException(
1✔
2252
                    "1 validation error detected: Value null at 'startingPosition' failed to satisfy constraint: Member must not be null.",
2253
                    Type="User",
2254
                )
2255

2256
            if starting_position not in KinesisStreamStartPosition.__members__:
1✔
2257
                raise ValidationException(
1✔
2258
                    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]"
2259
                )
2260
            # AT_TIMESTAMP is not allowed for DynamoDB Streams
2261
            elif (
1✔
2262
                service == "dynamodb"
2263
                and starting_position not in DynamoDBStreamStartPosition.__members__
2264
            ):
2265
                raise InvalidParameterValueException(
1✔
2266
                    f"Unsupported starting position for arn type: {request['EventSourceArn']}",
2267
                    Type="User",
2268
                )
2269

2270
        if service in ["sqs", "sqs-fifo"]:
1✔
2271
            if batch_size > 10 and request.get("MaximumBatchingWindowInSeconds", 0) == 0:
1✔
2272
                raise InvalidParameterValueException(
1✔
2273
                    "Maximum batch window in seconds must be greater than 0 if maximum batch size is greater than 10",
2274
                    Type="User",
2275
                )
2276

2277
        if (filter_criteria := request.get("FilterCriteria")) is not None:
1✔
2278
            for filter_ in filter_criteria.get("Filters", []):
1✔
2279
                pattern_str = filter_.get("Pattern")
1✔
2280
                if not pattern_str or not isinstance(pattern_str, str):
1✔
UNCOV
2281
                    raise InvalidParameterValueException(
×
2282
                        "Invalid filter pattern definition.", Type="User"
2283
                    )
2284

2285
                if not validate_event_pattern(pattern_str):
1✔
2286
                    raise InvalidParameterValueException(
1✔
2287
                        "Invalid filter pattern definition.", Type="User"
2288
                    )
2289

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

2305
        if qualifier:
1✔
2306
            # make sure the function version/alias exists
2307
            if api_utils.qualifier_is_alias(qualifier):
1✔
2308
                fn_alias = fn.aliases.get(qualifier)
1✔
2309
                if not fn_alias:
1✔
2310
                    raise Exception("unknown alias")  # TODO: cover via test
×
2311
            elif api_utils.qualifier_is_version(qualifier):
1✔
2312
                fn_version = fn.versions.get(qualifier)
1✔
2313
                if not fn_version:
1✔
2314
                    raise Exception("unknown version")  # TODO: cover via test
×
2315
            elif qualifier == "$LATEST":
1✔
2316
                pass
1✔
2317
            elif qualifier == "$LATEST.PUBLISHED":
×
UNCOV
2318
                if fn.versions.get(qualifier):
×
UNCOV
2319
                    pass
×
2320
            else:
UNCOV
2321
                raise Exception("invalid functionname")  # TODO: cover via test
×
2322
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account, region)
1✔
2323

2324
        else:
2325
            fn_arn = api_utils.unqualified_lambda_arn(function_name, account, region)
1✔
2326

2327
        function_version = get_function_version_from_arn(fn_arn)
1✔
2328
        function_role = function_version.config.role
1✔
2329

2330
        if source_arn := request.get("EventSourceArn"):
1✔
2331
            self.check_service_resource_exists(service, source_arn, fn_arn, function_role)
1✔
2332
        # Check we are validating a CreateEventSourceMapping request
2333
        if is_create_esm_request:
1✔
2334

2335
            def _get_mapping_sources(mapping: dict[str, Any]) -> list[str]:
1✔
2336
                if event_source_arn := mapping.get("EventSourceArn"):
1✔
2337
                    return [event_source_arn]
1✔
UNCOV
2338
                return (
×
2339
                    mapping.get("SelfManagedEventSource", {})
2340
                    .get("Endpoints", {})
2341
                    .get("KAFKA_BOOTSTRAP_SERVERS", [])
2342
                )
2343

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

2379
    @handler("UpdateEventSourceMapping", expand=False)
1✔
2380
    def update_event_source_mapping(
1✔
2381
        self,
2382
        context: RequestContext,
2383
        request: UpdateEventSourceMappingRequest,
2384
    ) -> EventSourceMappingConfiguration:
2385
        return self.update_event_source_mapping_v2(context, request)
1✔
2386

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

2410
        # normalize values to overwrite
2411
        event_source_mapping = old_event_source_mapping | request_data
1✔
2412

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

2415
        # Validate the newly updated ESM object. We ignore the output here since we only care whether an Exception is raised.
2416
        function_arn, _, _, function_version, function_role = self.validate_event_source_mapping(
1✔
2417
            context, event_source_mapping
2418
        )
2419

2420
        # remove the FunctionName field
2421
        event_source_mapping.pop("FunctionName", None)
1✔
2422

2423
        if function_arn:
1✔
2424
            event_source_mapping["FunctionArn"] = function_arn
1✔
2425

2426
        # Only apply update if the desired state differs
2427
        enabled = request.get("Enabled")
1✔
2428
        if enabled is not None:
1✔
2429
            if enabled and old_event_source_mapping["State"] != EsmState.ENABLED:
1✔
2430
                event_source_mapping["State"] = EsmState.ENABLING
1✔
2431
            # TODO: What happens when trying to update during an update or failed state?!
2432
            elif not enabled and old_event_source_mapping["State"] == EsmState.ENABLED:
1✔
2433
                event_source_mapping["State"] = EsmState.DISABLING
1✔
2434
        else:
2435
            event_source_mapping["State"] = EsmState.UPDATING
1✔
2436

2437
        # To ensure parity, certain responses need to be immediately returned
2438
        temp_params["State"] = event_source_mapping["State"]
1✔
2439

2440
        state.event_source_mappings[uuid] = event_source_mapping
1✔
2441

2442
        # TODO: Currently, we re-create the entire ESM worker. Look into approach with better performance.
2443
        worker_factory = EsmWorkerFactory(
1✔
2444
            event_source_mapping, function_role, request.get("Enabled", esm_worker.enabled)
2445
        )
2446

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

2451
        # We should stop() the worker since the delete() will remove the ESM from the state mapping.
2452
        esm_worker.stop()
1✔
2453
        # This will either create an EsmWorker in the CREATING state if enabled. Otherwise, the DISABLING state is set.
2454
        updated_esm_worker.create()
1✔
2455

2456
        return {**event_source_mapping, **temp_params}
1✔
2457

2458
    def delete_event_source_mapping(
1✔
2459
        self, context: RequestContext, uuid: String, **kwargs
2460
    ) -> EventSourceMappingConfiguration:
2461
        state = lambda_stores[context.account_id][context.region]
1✔
2462
        event_source_mapping = state.event_source_mappings.get(uuid)
1✔
2463
        if not event_source_mapping:
1✔
2464
            raise ResourceNotFoundException(
1✔
2465
                "The resource you requested does not exist.", Type="User"
2466
            )
2467
        esm = state.event_source_mappings[uuid]
1✔
2468
        # TODO: add proper locking
2469
        esm_worker = self.esm_workers.pop(uuid, None)
1✔
2470
        # Asynchronous delete in v2
2471
        if not esm_worker:
1✔
UNCOV
2472
            raise ResourceNotFoundException(
×
2473
                "The resource you requested does not exist.", Type="User"
2474
            )
2475
        # the full deletion of the ESM is happening asynchronously, but we delete the Tags instantly
2476
        # this behavior is similar to ``get_event_source_mapping`` which will raise right after deletion, but it is not
2477
        # always the case in AWS. Add more testing and align behavior with ``get_event_source_mapping``.
2478
        self._remove_all_tags(event_source_mapping["EventSourceMappingArn"])
1✔
2479
        esm_worker.delete()
1✔
2480
        return {**esm, "State": EsmState.DELETING}
1✔
2481

2482
    def get_event_source_mapping(
1✔
2483
        self, context: RequestContext, uuid: String, **kwargs
2484
    ) -> EventSourceMappingConfiguration:
2485
        state = lambda_stores[context.account_id][context.region]
1✔
2486
        event_source_mapping = state.event_source_mappings.get(uuid)
1✔
2487
        if not event_source_mapping:
1✔
2488
            raise ResourceNotFoundException(
1✔
2489
                "The resource you requested does not exist.", Type="User"
2490
            )
2491
        esm_worker = self.esm_workers.get(uuid)
1✔
2492
        if not esm_worker:
1✔
UNCOV
2493
            raise ResourceNotFoundException(
×
2494
                "The resource you requested does not exist.", Type="User"
2495
            )
2496
        event_source_mapping["State"] = esm_worker.current_state
1✔
2497
        event_source_mapping["StateTransitionReason"] = esm_worker.state_transition_reason
1✔
2498
        return event_source_mapping
1✔
2499

2500
    def list_event_source_mappings(
1✔
2501
        self,
2502
        context: RequestContext,
2503
        event_source_arn: Arn = None,
2504
        function_name: FunctionName = None,
2505
        marker: String = None,
2506
        max_items: MaxListItems = None,
2507
        **kwargs,
2508
    ) -> ListEventSourceMappingsResponse:
2509
        state = lambda_stores[context.account_id][context.region]
1✔
2510

2511
        esms = state.event_source_mappings.values()
1✔
2512
        # TODO: update and test State and StateTransitionReason for ESM v2
2513

2514
        if event_source_arn:  # TODO: validate pattern
1✔
2515
            esms = [e for e in esms if e.get("EventSourceArn") == event_source_arn]
1✔
2516

2517
        if function_name:
1✔
2518
            esms = [e for e in esms if function_name in e["FunctionArn"]]
1✔
2519

2520
        esms = PaginatedList(esms)
1✔
2521
        page, token = esms.get_page(
1✔
2522
            lambda x: x["UUID"],
2523
            marker,
2524
            max_items,
2525
        )
2526
        return ListEventSourceMappingsResponse(EventSourceMappings=page, NextMarker=token)
1✔
2527

2528
    def get_source_type_from_request(self, request: dict[str, Any]) -> str:
1✔
UNCOV
2529
        if event_source_arn := request.get("EventSourceArn", ""):
×
UNCOV
2530
            service = extract_service_from_arn(event_source_arn)
×
UNCOV
2531
            if service == "sqs" and "fifo" in event_source_arn:
×
UNCOV
2532
                service = "sqs-fifo"
×
UNCOV
2533
            return service
×
UNCOV
2534
        elif request.get("SelfManagedEventSource"):
×
UNCOV
2535
            return "kafka"
×
2536

2537
    # =======================================
2538
    # ============ FUNCTION URLS ============
2539
    # =======================================
2540

2541
    @staticmethod
1✔
2542
    def _validate_qualifier(qualifier: str) -> None:
1✔
2543
        if qualifier in ["$LATEST", "$LATEST.PUBLISHED"] or (
1✔
2544
            qualifier and api_utils.qualifier_is_version(qualifier)
2545
        ):
2546
            raise ValidationException(
1✔
2547
                f"1 validation error detected: Value '{qualifier}' at 'qualifier' failed to satisfy constraint: Member must satisfy regular expression pattern: ((?!^\\d+$)^[0-9a-zA-Z-_]+$)"
2548
            )
2549

2550
    @staticmethod
1✔
2551
    def _validate_invoke_mode(invoke_mode: str) -> None:
1✔
2552
        if invoke_mode and invoke_mode not in [InvokeMode.BUFFERED, InvokeMode.RESPONSE_STREAM]:
1✔
2553
            raise ValidationException(
1✔
2554
                f"1 validation error detected: Value '{invoke_mode}' at 'invokeMode' failed to satisfy constraint: Member must satisfy enum value set: [RESPONSE_STREAM, BUFFERED]"
2555
            )
2556
        if invoke_mode == InvokeMode.RESPONSE_STREAM:
1✔
2557
            # TODO should we actually fail for setting RESPONSE_STREAM?
2558
            #  It should trigger InvokeWithResponseStream which is not implemented
2559
            LOG.warning(
1✔
2560
                "The invokeMode 'RESPONSE_STREAM' is not yet supported on LocalStack. The property is only mocked, the execution will still be 'BUFFERED'"
2561
            )
2562

2563
    # TODO: what happens if function state is not active?
2564
    def create_function_url_config(
1✔
2565
        self,
2566
        context: RequestContext,
2567
        function_name: FunctionName,
2568
        auth_type: FunctionUrlAuthType,
2569
        qualifier: FunctionUrlQualifier = None,
2570
        cors: Cors = None,
2571
        invoke_mode: InvokeMode = None,
2572
        **kwargs,
2573
    ) -> CreateFunctionUrlConfigResponse:
2574
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2575
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2576
            function_name, qualifier, context
2577
        )
2578
        state = lambda_stores[account_id][region]
1✔
2579
        self._validate_qualifier(qualifier)
1✔
2580
        self._validate_invoke_mode(invoke_mode)
1✔
2581

2582
        fn = state.functions.get(function_name)
1✔
2583
        if fn is None:
1✔
2584
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2585

2586
        url_config = fn.function_url_configs.get(qualifier or "$LATEST")
1✔
2587
        if url_config:
1✔
2588
            raise ResourceConflictException(
1✔
2589
                f"Failed to create function url config for [functionArn = {url_config.function_arn}]. Error message:  FunctionUrlConfig exists for this Lambda function",
2590
                Type="User",
2591
            )
2592

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

2596
        normalized_qualifier = qualifier or "$LATEST"
1✔
2597

2598
        function_arn = (
1✔
2599
            api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
2600
            if qualifier
2601
            else api_utils.unqualified_lambda_arn(function_name, account_id, region)
2602
        )
2603

2604
        custom_id: str | None = None
1✔
2605

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

2622
            else:
2623
                # Note: we're logging here instead of raising to prioritize
2624
                # strict parity with AWS over the localstack-only custom_id
2625
                LOG.warning(
1✔
2626
                    "Invalid custom ID tag value for lambda URL (%s=%s). "
2627
                    "Replaced with default (random id)",
2628
                    TAG_KEY_CUSTOM_URL,
2629
                    custom_id_tag_value,
2630
                )
2631

2632
        # The url_id is the subdomain used for the URL we're creating. This
2633
        # is either created randomly (as in AWS), or can be passed as a tag
2634
        # to the lambda itself (localstack-only).
2635
        url_id: str
2636
        if custom_id is None:
1✔
2637
            url_id = api_utils.generate_random_url_id()
1✔
2638
        else:
2639
            url_id = custom_id
1✔
2640

2641
        host_definition = localstack_host(custom_port=config.GATEWAY_LISTEN[0].port)
1✔
2642
        fn.function_url_configs[normalized_qualifier] = FunctionUrlConfig(
1✔
2643
            function_arn=function_arn,
2644
            function_name=function_name,
2645
            cors=cors,
2646
            url_id=url_id,
2647
            url=f"http://{url_id}.lambda-url.{context.region}.{host_definition.host_and_port()}/",  # TODO: https support
2648
            auth_type=auth_type,
2649
            creation_time=api_utils.generate_lambda_date(),
2650
            last_modified_time=api_utils.generate_lambda_date(),
2651
            invoke_mode=invoke_mode,
2652
        )
2653

2654
        # persist and start URL
2655
        # TODO: implement URL invoke
2656
        api_url_config = api_utils.map_function_url_config(
1✔
2657
            fn.function_url_configs[normalized_qualifier]
2658
        )
2659

2660
        return CreateFunctionUrlConfigResponse(
1✔
2661
            FunctionUrl=api_url_config["FunctionUrl"],
2662
            FunctionArn=api_url_config["FunctionArn"],
2663
            AuthType=api_url_config["AuthType"],
2664
            Cors=api_url_config["Cors"],
2665
            CreationTime=api_url_config["CreationTime"],
2666
            InvokeMode=api_url_config["InvokeMode"],
2667
        )
2668

2669
    def get_function_url_config(
1✔
2670
        self,
2671
        context: RequestContext,
2672
        function_name: FunctionName,
2673
        qualifier: FunctionUrlQualifier = None,
2674
        **kwargs,
2675
    ) -> GetFunctionUrlConfigResponse:
2676
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2677
        state = lambda_stores[account_id][region]
1✔
2678

2679
        fn_name, qualifier = api_utils.get_name_and_qualifier(function_name, qualifier, context)
1✔
2680

2681
        self._validate_qualifier(qualifier)
1✔
2682

2683
        resolved_fn = state.functions.get(fn_name)
1✔
2684
        if not resolved_fn:
1✔
2685
            raise ResourceNotFoundException(
1✔
2686
                "The resource you requested does not exist.", Type="User"
2687
            )
2688

2689
        qualifier = qualifier or "$LATEST"
1✔
2690
        url_config = resolved_fn.function_url_configs.get(qualifier)
1✔
2691
        if not url_config:
1✔
2692
            raise ResourceNotFoundException(
1✔
2693
                "The resource you requested does not exist.", Type="User"
2694
            )
2695

2696
        return api_utils.map_function_url_config(url_config)
1✔
2697

2698
    def update_function_url_config(
1✔
2699
        self,
2700
        context: RequestContext,
2701
        function_name: FunctionName,
2702
        qualifier: FunctionUrlQualifier = None,
2703
        auth_type: FunctionUrlAuthType = None,
2704
        cors: Cors = None,
2705
        invoke_mode: InvokeMode = None,
2706
        **kwargs,
2707
    ) -> UpdateFunctionUrlConfigResponse:
2708
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2709
        state = lambda_stores[account_id][region]
1✔
2710

2711
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2712
            function_name, qualifier, context
2713
        )
2714
        self._validate_qualifier(qualifier)
1✔
2715
        self._validate_invoke_mode(invoke_mode)
1✔
2716

2717
        fn = state.functions.get(function_name)
1✔
2718
        if not fn:
1✔
2719
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2720

2721
        normalized_qualifier = qualifier or "$LATEST"
1✔
2722

2723
        if (
1✔
2724
            api_utils.qualifier_is_alias(normalized_qualifier)
2725
            and normalized_qualifier not in fn.aliases
2726
        ):
2727
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2728

2729
        url_config = fn.function_url_configs.get(normalized_qualifier)
1✔
2730
        if not url_config:
1✔
2731
            raise ResourceNotFoundException(
1✔
2732
                "The resource you requested does not exist.", Type="User"
2733
            )
2734

2735
        changes = {
1✔
2736
            "last_modified_time": api_utils.generate_lambda_date(),
2737
            **({"cors": cors} if cors is not None else {}),
2738
            **({"auth_type": auth_type} if auth_type is not None else {}),
2739
        }
2740

2741
        if invoke_mode:
1✔
2742
            changes["invoke_mode"] = invoke_mode
1✔
2743

2744
        new_url_config = dataclasses.replace(url_config, **changes)
1✔
2745
        fn.function_url_configs[normalized_qualifier] = new_url_config
1✔
2746

2747
        return UpdateFunctionUrlConfigResponse(
1✔
2748
            FunctionUrl=new_url_config.url,
2749
            FunctionArn=new_url_config.function_arn,
2750
            AuthType=new_url_config.auth_type,
2751
            Cors=new_url_config.cors,
2752
            CreationTime=new_url_config.creation_time,
2753
            LastModifiedTime=new_url_config.last_modified_time,
2754
            InvokeMode=new_url_config.invoke_mode,
2755
        )
2756

2757
    def delete_function_url_config(
1✔
2758
        self,
2759
        context: RequestContext,
2760
        function_name: FunctionName,
2761
        qualifier: FunctionUrlQualifier = None,
2762
        **kwargs,
2763
    ) -> None:
2764
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2765
        state = lambda_stores[account_id][region]
1✔
2766

2767
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2768
            function_name, qualifier, context
2769
        )
2770
        self._validate_qualifier(qualifier)
1✔
2771

2772
        resolved_fn = state.functions.get(function_name)
1✔
2773
        if not resolved_fn:
1✔
2774
            raise ResourceNotFoundException(
1✔
2775
                "The resource you requested does not exist.", Type="User"
2776
            )
2777

2778
        qualifier = qualifier or "$LATEST"
1✔
2779
        url_config = resolved_fn.function_url_configs.get(qualifier)
1✔
2780
        if not url_config:
1✔
2781
            raise ResourceNotFoundException(
1✔
2782
                "The resource you requested does not exist.", Type="User"
2783
            )
2784

2785
        del resolved_fn.function_url_configs[qualifier]
1✔
2786

2787
    def list_function_url_configs(
1✔
2788
        self,
2789
        context: RequestContext,
2790
        function_name: FunctionName,
2791
        marker: String = None,
2792
        max_items: MaxItems = None,
2793
        **kwargs,
2794
    ) -> ListFunctionUrlConfigsResponse:
2795
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2796
        state = lambda_stores[account_id][region]
1✔
2797

2798
        fn_name = api_utils.get_function_name(function_name, context)
1✔
2799
        resolved_fn = state.functions.get(fn_name)
1✔
2800
        if not resolved_fn:
1✔
2801
            raise ResourceNotFoundException("Function does not exist", Type="User")
1✔
2802

2803
        url_configs = [
1✔
2804
            api_utils.map_function_url_config(fn_conf)
2805
            for fn_conf in resolved_fn.function_url_configs.values()
2806
        ]
2807
        url_configs = PaginatedList(url_configs)
1✔
2808
        page, token = url_configs.get_page(
1✔
2809
            lambda url_config: url_config["FunctionArn"],
2810
            marker,
2811
            max_items,
2812
        )
2813
        url_configs = page
1✔
2814
        return ListFunctionUrlConfigsResponse(FunctionUrlConfigs=url_configs, NextMarker=token)
1✔
2815

2816
    # =======================================
2817
    # ============  Permissions  ============
2818
    # =======================================
2819

2820
    @handler("AddPermission", expand=False)
1✔
2821
    def add_permission(
1✔
2822
        self,
2823
        context: RequestContext,
2824
        request: AddPermissionRequest,
2825
    ) -> AddPermissionResponse:
2826
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2827
            request.get("FunctionName"), request.get("Qualifier"), context
2828
        )
2829

2830
        # validate qualifier
2831
        if qualifier is not None:
1✔
2832
            self._validate_qualifier_expression(qualifier)
1✔
2833
            if qualifier == "$LATEST":
1✔
2834
                raise InvalidParameterValueException(
1✔
2835
                    "We currently do not support adding policies for $LATEST.", Type="User"
2836
                )
2837
        account_id, region = api_utils.get_account_and_region(request.get("FunctionName"), context)
1✔
2838

2839
        resolved_fn = self._get_function(function_name, account_id, region)
1✔
2840
        resolved_qualifier, fn_arn = self._resolve_fn_qualifier(resolved_fn, qualifier)
1✔
2841

2842
        revision_id = request.get("RevisionId")
1✔
2843
        if revision_id:
1✔
2844
            fn_revision_id = self._function_revision_id(resolved_fn, resolved_qualifier)
1✔
2845
            if revision_id != fn_revision_id:
1✔
2846
                raise PreconditionFailedException(
1✔
2847
                    "The Revision Id provided does not match the latest Revision Id. "
2848
                    "Call the GetPolicy API to retrieve the latest Revision Id",
2849
                    Type="User",
2850
                )
2851

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

2868
        permission_statement = api_utils.build_statement(
1✔
2869
            partition=context.partition,
2870
            resource_arn=fn_arn,
2871
            statement_id=request["StatementId"],
2872
            action=request["Action"],
2873
            principal=request["Principal"],
2874
            source_arn=request.get("SourceArn"),
2875
            source_account=request.get("SourceAccount"),
2876
            principal_org_id=request.get("PrincipalOrgID"),
2877
            event_source_token=request.get("EventSourceToken"),
2878
            auth_type=request.get("FunctionUrlAuthType"),
2879
        )
2880
        new_policy = existing_policy
1✔
2881
        if not existing_policy:
1✔
2882
            new_policy = FunctionResourcePolicy(
1✔
2883
                policy=ResourcePolicy(Version="2012-10-17", Id="default", Statement=[])
2884
            )
2885
        new_policy.policy.Statement.append(permission_statement)
1✔
2886
        if not existing_policy:
1✔
2887
            resolved_fn.permissions[resolved_qualifier] = new_policy
1✔
2888

2889
        # Update revision id of alias or version
2890
        # TODO: re-evaluate data model to prevent this dirty hack just for bumping the revision id
2891
        # TODO: does that need a `with function.lock` for atomic updates of the policy + revision_id?
2892
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
2893
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
1✔
2894
            resolved_fn.aliases[resolved_qualifier] = dataclasses.replace(resolved_alias)
1✔
2895
        # Assumes that a non-alias is a version
2896
        else:
2897
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
2898
            resolved_fn.versions[resolved_qualifier] = dataclasses.replace(
1✔
2899
                resolved_version, config=dataclasses.replace(resolved_version.config)
2900
            )
2901
        return AddPermissionResponse(Statement=json.dumps(permission_statement))
1✔
2902

2903
    def remove_permission(
1✔
2904
        self,
2905
        context: RequestContext,
2906
        function_name: NamespacedFunctionName,
2907
        statement_id: NamespacedStatementId,
2908
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
2909
        revision_id: String | None = None,
2910
        **kwargs,
2911
    ) -> None:
2912
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2913
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2914
            function_name, qualifier, context
2915
        )
2916
        if qualifier is not None:
1✔
2917
            self._validate_qualifier_expression(qualifier)
1✔
2918

2919
        state = lambda_stores[account_id][region]
1✔
2920
        resolved_fn = state.functions.get(function_name)
1✔
2921
        if resolved_fn is None:
1✔
2922
            fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
2923
            raise ResourceNotFoundException(f"No policy found for: {fn_arn}", Type="User")
1✔
2924

2925
        resolved_qualifier, _ = self._resolve_fn_qualifier(resolved_fn, qualifier)
1✔
2926
        function_permission = resolved_fn.permissions.get(resolved_qualifier)
1✔
2927
        if not function_permission:
1✔
2928
            raise ResourceNotFoundException(
1✔
2929
                "No policy is associated with the given resource.", Type="User"
2930
            )
2931

2932
        # try to find statement in policy and delete it
2933
        statement = None
1✔
2934
        for s in function_permission.policy.Statement:
1✔
2935
            if s["Sid"] == statement_id:
1✔
2936
                statement = s
1✔
2937
                break
1✔
2938

2939
        if not statement:
1✔
2940
            raise ResourceNotFoundException(
1✔
2941
                f"Statement {statement_id} is not found in resource policy.", Type="User"
2942
            )
2943
        fn_revision_id = self._function_revision_id(resolved_fn, resolved_qualifier)
1✔
2944
        if revision_id and revision_id != fn_revision_id:
1✔
UNCOV
2945
            raise PreconditionFailedException(
×
2946
                "The Revision Id provided does not match the latest Revision Id. "
2947
                "Call the GetFunction/GetAlias API to retrieve the latest Revision Id",
2948
                Type="User",
2949
            )
2950
        function_permission.policy.Statement.remove(statement)
1✔
2951

2952
        # Update revision id for alias or version
2953
        # TODO: re-evaluate data model to prevent this dirty hack just for bumping the revision id
2954
        # TODO: does that need a `with function.lock` for atomic updates of the policy + revision_id?
2955
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
UNCOV
2956
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
×
UNCOV
2957
            resolved_fn.aliases[resolved_qualifier] = dataclasses.replace(resolved_alias)
×
2958
        # Assumes that a non-alias is a version
2959
        else:
2960
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
2961
            resolved_fn.versions[resolved_qualifier] = dataclasses.replace(
1✔
2962
                resolved_version, config=dataclasses.replace(resolved_version.config)
2963
            )
2964

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

2969
    def get_policy(
1✔
2970
        self,
2971
        context: RequestContext,
2972
        function_name: NamespacedFunctionName,
2973
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
2974
        **kwargs,
2975
    ) -> GetPolicyResponse:
2976
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
2977
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
2978
            function_name, qualifier, context
2979
        )
2980

2981
        if qualifier is not None:
1✔
2982
            self._validate_qualifier_expression(qualifier)
1✔
2983

2984
        resolved_fn = self._get_function(function_name, account_id, region)
1✔
2985

2986
        resolved_qualifier = qualifier or "$LATEST"
1✔
2987
        function_permission = resolved_fn.permissions.get(resolved_qualifier)
1✔
2988
        if not function_permission:
1✔
2989
            raise ResourceNotFoundException(
1✔
2990
                "The resource you requested does not exist.", Type="User"
2991
            )
2992

2993
        fn_revision_id = None
1✔
2994
        if api_utils.qualifier_is_alias(resolved_qualifier):
1✔
2995
            resolved_alias = resolved_fn.aliases[resolved_qualifier]
1✔
2996
            fn_revision_id = resolved_alias.revision_id
1✔
2997
        # Assumes that a non-alias is a version
2998
        else:
2999
            resolved_version = resolved_fn.versions[resolved_qualifier]
1✔
3000
            fn_revision_id = resolved_version.config.revision_id
1✔
3001

3002
        return GetPolicyResponse(
1✔
3003
            Policy=json.dumps(dataclasses.asdict(function_permission.policy)),
3004
            RevisionId=fn_revision_id,
3005
        )
3006

3007
    # =======================================
3008
    # ========  Code signing config  ========
3009
    # =======================================
3010

3011
    def create_code_signing_config(
1✔
3012
        self,
3013
        context: RequestContext,
3014
        allowed_publishers: AllowedPublishers,
3015
        description: Description | None = None,
3016
        code_signing_policies: CodeSigningPolicies | None = None,
3017
        tags: Tags | None = None,
3018
        **kwargs,
3019
    ) -> CreateCodeSigningConfigResponse:
3020
        account = context.account_id
1✔
3021
        region = context.region
1✔
3022

3023
        state = lambda_stores[account][region]
1✔
3024
        # TODO: can there be duplicates?
3025
        csc_id = f"csc-{get_random_hex(17)}"  # e.g. 'csc-077c33b4c19e26036'
1✔
3026
        csc_arn = f"arn:{context.partition}:lambda:{region}:{account}:code-signing-config:{csc_id}"
1✔
3027
        csc = CodeSigningConfig(
1✔
3028
            csc_id=csc_id,
3029
            arn=csc_arn,
3030
            allowed_publishers=allowed_publishers,
3031
            policies=code_signing_policies,
3032
            last_modified=api_utils.generate_lambda_date(),
3033
            description=description,
3034
        )
3035
        state.code_signing_configs[csc_arn] = csc
1✔
3036
        return CreateCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(csc))
1✔
3037

3038
    def put_function_code_signing_config(
1✔
3039
        self,
3040
        context: RequestContext,
3041
        code_signing_config_arn: CodeSigningConfigArn,
3042
        function_name: NamespacedFunctionName,
3043
        **kwargs,
3044
    ) -> PutFunctionCodeSigningConfigResponse:
3045
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3046
        state = lambda_stores[account_id][region]
1✔
3047
        function_name = api_utils.get_function_name(function_name, context)
1✔
3048

3049
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
3050
        if not csc:
1✔
3051
            raise CodeSigningConfigNotFoundException(
1✔
3052
                f"The code signing configuration cannot be found. Check that the provided configuration is not deleted: {code_signing_config_arn}.",
3053
                Type="User",
3054
            )
3055

3056
        fn = state.functions.get(function_name)
1✔
3057
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
3058
        if not fn:
1✔
3059
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
3060

3061
        fn.code_signing_config_arn = code_signing_config_arn
1✔
3062
        return PutFunctionCodeSigningConfigResponse(
1✔
3063
            CodeSigningConfigArn=code_signing_config_arn, FunctionName=function_name
3064
        )
3065

3066
    def update_code_signing_config(
1✔
3067
        self,
3068
        context: RequestContext,
3069
        code_signing_config_arn: CodeSigningConfigArn,
3070
        description: Description = None,
3071
        allowed_publishers: AllowedPublishers = None,
3072
        code_signing_policies: CodeSigningPolicies = None,
3073
        **kwargs,
3074
    ) -> UpdateCodeSigningConfigResponse:
3075
        state = lambda_stores[context.account_id][context.region]
1✔
3076
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
3077
        if not csc:
1✔
3078
            raise ResourceNotFoundException(
1✔
3079
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
3080
            )
3081

3082
        changes = {
1✔
3083
            **(
3084
                {"allowed_publishers": allowed_publishers} if allowed_publishers is not None else {}
3085
            ),
3086
            **({"policies": code_signing_policies} if code_signing_policies is not None else {}),
3087
            **({"description": description} if description is not None else {}),
3088
        }
3089
        new_csc = dataclasses.replace(
1✔
3090
            csc, last_modified=api_utils.generate_lambda_date(), **changes
3091
        )
3092
        state.code_signing_configs[code_signing_config_arn] = new_csc
1✔
3093

3094
        return UpdateCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(new_csc))
1✔
3095

3096
    def get_code_signing_config(
1✔
3097
        self, context: RequestContext, code_signing_config_arn: CodeSigningConfigArn, **kwargs
3098
    ) -> GetCodeSigningConfigResponse:
3099
        state = lambda_stores[context.account_id][context.region]
1✔
3100
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
3101
        if not csc:
1✔
3102
            raise ResourceNotFoundException(
1✔
3103
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
3104
            )
3105

3106
        return GetCodeSigningConfigResponse(CodeSigningConfig=api_utils.map_csc(csc))
1✔
3107

3108
    def get_function_code_signing_config(
1✔
3109
        self, context: RequestContext, function_name: NamespacedFunctionName, **kwargs
3110
    ) -> GetFunctionCodeSigningConfigResponse:
3111
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3112
        state = lambda_stores[account_id][region]
1✔
3113
        function_name = api_utils.get_function_name(function_name, context)
1✔
3114
        fn = state.functions.get(function_name)
1✔
3115
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
3116
        if not fn:
1✔
3117
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
3118

3119
        if fn.code_signing_config_arn:
1✔
3120
            return GetFunctionCodeSigningConfigResponse(
1✔
3121
                CodeSigningConfigArn=fn.code_signing_config_arn, FunctionName=function_name
3122
            )
3123

3124
        return GetFunctionCodeSigningConfigResponse()
1✔
3125

3126
    def delete_function_code_signing_config(
1✔
3127
        self, context: RequestContext, function_name: NamespacedFunctionName, **kwargs
3128
    ) -> None:
3129
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3130
        state = lambda_stores[account_id][region]
1✔
3131
        function_name = api_utils.get_function_name(function_name, context)
1✔
3132
        fn = state.functions.get(function_name)
1✔
3133
        fn_arn = api_utils.unqualified_lambda_arn(function_name, account_id, region)
1✔
3134
        if not fn:
1✔
3135
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
3136

3137
        fn.code_signing_config_arn = None
1✔
3138

3139
    def delete_code_signing_config(
1✔
3140
        self, context: RequestContext, code_signing_config_arn: CodeSigningConfigArn, **kwargs
3141
    ) -> DeleteCodeSigningConfigResponse:
3142
        state = lambda_stores[context.account_id][context.region]
1✔
3143

3144
        csc = state.code_signing_configs.get(code_signing_config_arn)
1✔
3145
        if not csc:
1✔
3146
            raise ResourceNotFoundException(
1✔
3147
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
3148
            )
3149

3150
        del state.code_signing_configs[code_signing_config_arn]
1✔
3151

3152
        return DeleteCodeSigningConfigResponse()
1✔
3153

3154
    def list_code_signing_configs(
1✔
3155
        self,
3156
        context: RequestContext,
3157
        marker: String = None,
3158
        max_items: MaxListItems = None,
3159
        **kwargs,
3160
    ) -> ListCodeSigningConfigsResponse:
3161
        state = lambda_stores[context.account_id][context.region]
1✔
3162

3163
        cscs = [api_utils.map_csc(csc) for csc in state.code_signing_configs.values()]
1✔
3164
        cscs = PaginatedList(cscs)
1✔
3165
        page, token = cscs.get_page(
1✔
3166
            lambda csc: csc["CodeSigningConfigId"],
3167
            marker,
3168
            max_items,
3169
        )
3170
        return ListCodeSigningConfigsResponse(CodeSigningConfigs=page, NextMarker=token)
1✔
3171

3172
    def list_functions_by_code_signing_config(
1✔
3173
        self,
3174
        context: RequestContext,
3175
        code_signing_config_arn: CodeSigningConfigArn,
3176
        marker: String = None,
3177
        max_items: MaxListItems = None,
3178
        **kwargs,
3179
    ) -> ListFunctionsByCodeSigningConfigResponse:
3180
        account = context.account_id
1✔
3181
        region = context.region
1✔
3182

3183
        state = lambda_stores[account][region]
1✔
3184

3185
        if code_signing_config_arn not in state.code_signing_configs:
1✔
3186
            raise ResourceNotFoundException(
1✔
3187
                f"The Lambda code signing configuration {code_signing_config_arn} can not be found."
3188
            )
3189

3190
        fn_arns = [
1✔
3191
            api_utils.unqualified_lambda_arn(fn.function_name, account, region)
3192
            for fn in state.functions.values()
3193
            if fn.code_signing_config_arn == code_signing_config_arn
3194
        ]
3195

3196
        cscs = PaginatedList(fn_arns)
1✔
3197
        page, token = cscs.get_page(
1✔
3198
            lambda x: x,
3199
            marker,
3200
            max_items,
3201
        )
3202
        return ListFunctionsByCodeSigningConfigResponse(FunctionArns=page, NextMarker=token)
1✔
3203

3204
    # =======================================
3205
    # =========  Account Settings   =========
3206
    # =======================================
3207

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

3213
        fn_count = 0
1✔
3214
        code_size_sum = 0
1✔
3215
        reserved_concurrency_sum = 0
1✔
3216
        for fn in state.functions.values():
1✔
3217
            fn_count += 1
1✔
3218
            for fn_version in fn.versions.values():
1✔
3219
                # Image-based Lambdas do not have a code attribute and count against the ECR quotas instead
3220
                if fn_version.config.package_type == PackageType.Zip:
1✔
3221
                    code_size_sum += fn_version.config.code.code_size
1✔
3222
            if fn.reserved_concurrent_executions is not None:
1✔
3223
                reserved_concurrency_sum += fn.reserved_concurrent_executions
1✔
3224
            for c in fn.provisioned_concurrency_configs.values():
1✔
3225
                reserved_concurrency_sum += c.provisioned_concurrent_executions
1✔
3226
        for layer in state.layers.values():
1✔
3227
            for layer_version in layer.layer_versions.values():
1✔
3228
                code_size_sum += layer_version.code.code_size
1✔
3229
        return GetAccountSettingsResponse(
1✔
3230
            AccountLimit=AccountLimit(
3231
                TotalCodeSize=config.LAMBDA_LIMITS_TOTAL_CODE_SIZE,
3232
                CodeSizeZipped=config.LAMBDA_LIMITS_CODE_SIZE_ZIPPED,
3233
                CodeSizeUnzipped=config.LAMBDA_LIMITS_CODE_SIZE_UNZIPPED,
3234
                ConcurrentExecutions=config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS,
3235
                UnreservedConcurrentExecutions=config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS
3236
                - reserved_concurrency_sum,
3237
            ),
3238
            AccountUsage=AccountUsage(
3239
                TotalCodeSize=code_size_sum,
3240
                FunctionCount=fn_count,
3241
            ),
3242
        )
3243

3244
    # =======================================
3245
    # ==  Provisioned Concurrency Config   ==
3246
    # =======================================
3247

3248
    def _get_provisioned_config(
1✔
3249
        self, context: RequestContext, function_name: str, qualifier: str
3250
    ) -> ProvisionedConcurrencyConfiguration | None:
3251
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3252
        state = lambda_stores[account_id][region]
1✔
3253
        function_name = api_utils.get_function_name(function_name, context)
1✔
3254
        fn = state.functions.get(function_name)
1✔
3255
        if api_utils.qualifier_is_alias(qualifier):
1✔
3256
            fn_alias = None
1✔
3257
            if fn:
1✔
3258
                fn_alias = fn.aliases.get(qualifier)
1✔
3259
            if fn_alias is None:
1✔
3260
                raise ResourceNotFoundException(
1✔
3261
                    f"Cannot find alias arn: {api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)}",
3262
                    Type="User",
3263
                )
3264
        elif api_utils.qualifier_is_version(qualifier):
1✔
3265
            fn_version = None
1✔
3266
            if fn:
1✔
3267
                fn_version = fn.versions.get(qualifier)
1✔
3268
            if fn_version is None:
1✔
3269
                raise ResourceNotFoundException(
1✔
3270
                    f"Function not found: {api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)}",
3271
                    Type="User",
3272
                )
3273

3274
        return fn.provisioned_concurrency_configs.get(qualifier)
1✔
3275

3276
    def put_provisioned_concurrency_config(
1✔
3277
        self,
3278
        context: RequestContext,
3279
        function_name: FunctionName,
3280
        qualifier: Qualifier,
3281
        provisioned_concurrent_executions: PositiveInteger,
3282
        **kwargs,
3283
    ) -> PutProvisionedConcurrencyConfigResponse:
3284
        if provisioned_concurrent_executions <= 0:
1✔
3285
            raise ValidationException(
1✔
3286
                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"
3287
            )
3288

3289
        if qualifier == "$LATEST":
1✔
3290
            raise InvalidParameterValueException(
1✔
3291
                "Provisioned Concurrency Configs cannot be applied to unpublished function versions.",
3292
                Type="User",
3293
            )
3294
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3295
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3296
            function_name, qualifier, context
3297
        )
3298
        state = lambda_stores[account_id][region]
1✔
3299
        fn = state.functions.get(function_name)
1✔
3300

3301
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3302

3303
        if provisioned_config:  # TODO: merge?
1✔
3304
            # TODO: add a test for partial updates (if possible)
3305
            LOG.warning(
1✔
3306
                "Partial update of provisioned concurrency config is currently not supported."
3307
            )
3308

3309
        other_provisioned_sum = sum(
1✔
3310
            [
3311
                provisioned_configs.provisioned_concurrent_executions
3312
                for provisioned_qualifier, provisioned_configs in fn.provisioned_concurrency_configs.items()
3313
                if provisioned_qualifier != qualifier
3314
            ]
3315
        )
3316

3317
        if (
1✔
3318
            fn.reserved_concurrent_executions is not None
3319
            and fn.reserved_concurrent_executions
3320
            < other_provisioned_sum + provisioned_concurrent_executions
3321
        ):
3322
            raise InvalidParameterValueException(
1✔
3323
                "Requested Provisioned Concurrency should not be greater than the reservedConcurrentExecution for function",
3324
                Type="User",
3325
            )
3326

3327
        if provisioned_concurrent_executions > config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS:
1✔
3328
            raise InvalidParameterValueException(
1✔
3329
                f"Specified ConcurrentExecutions for function is greater than account's unreserved concurrency"
3330
                f" [{config.LAMBDA_LIMITS_CONCURRENT_EXECUTIONS}]."
3331
            )
3332

3333
        settings = self.get_account_settings(context)
1✔
3334
        unreserved_concurrent_executions = settings["AccountLimit"][
1✔
3335
            "UnreservedConcurrentExecutions"
3336
        ]
3337
        if (
1✔
3338
            unreserved_concurrent_executions - provisioned_concurrent_executions
3339
            < config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY
3340
        ):
3341
            raise InvalidParameterValueException(
1✔
3342
                f"Specified ConcurrentExecutions for function decreases account's UnreservedConcurrentExecution below"
3343
                f" its minimum value of [{config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY}]."
3344
            )
3345

3346
        provisioned_config = ProvisionedConcurrencyConfiguration(
1✔
3347
            provisioned_concurrent_executions, api_utils.generate_lambda_date()
3348
        )
3349
        fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3350

3351
        if api_utils.qualifier_is_alias(qualifier):
1✔
3352
            alias = fn.aliases.get(qualifier)
1✔
3353
            resolved_version = fn.versions.get(alias.function_version)
1✔
3354

3355
            if (
1✔
3356
                resolved_version
3357
                and fn.provisioned_concurrency_configs.get(alias.function_version) is not None
3358
            ):
3359
                raise ResourceConflictException(
1✔
3360
                    "Alias can't be used for Provisioned Concurrency configuration on an already Provisioned version",
3361
                    Type="User",
3362
                )
3363
            fn_arn = resolved_version.id.qualified_arn()
1✔
3364
        elif api_utils.qualifier_is_version(qualifier):
1✔
3365
            fn_version = fn.versions.get(qualifier)
1✔
3366

3367
            # TODO: might be useful other places, utilize
3368
            pointing_aliases = []
1✔
3369
            for alias in fn.aliases.values():
1✔
3370
                if (
1✔
3371
                    alias.function_version == qualifier
3372
                    and fn.provisioned_concurrency_configs.get(alias.name) is not None
3373
                ):
3374
                    pointing_aliases.append(alias.name)
1✔
3375
            if pointing_aliases:
1✔
3376
                raise ResourceConflictException(
1✔
3377
                    "Version is pointed by a Provisioned Concurrency alias", Type="User"
3378
                )
3379

3380
            fn_arn = fn_version.id.qualified_arn()
1✔
3381

3382
        manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3383

3384
        fn.provisioned_concurrency_configs[qualifier] = provisioned_config
1✔
3385

3386
        manager.update_provisioned_concurrency_config(
1✔
3387
            provisioned_config.provisioned_concurrent_executions
3388
        )
3389

3390
        return PutProvisionedConcurrencyConfigResponse(
1✔
3391
            RequestedProvisionedConcurrentExecutions=provisioned_config.provisioned_concurrent_executions,
3392
            AvailableProvisionedConcurrentExecutions=0,
3393
            AllocatedProvisionedConcurrentExecutions=0,
3394
            Status=ProvisionedConcurrencyStatusEnum.IN_PROGRESS,
3395
            # StatusReason=manager.provisioned_state.status_reason,
3396
            LastModified=provisioned_config.last_modified,  # TODO: does change with configuration or also with state changes?
3397
        )
3398

3399
    def get_provisioned_concurrency_config(
1✔
3400
        self, context: RequestContext, function_name: FunctionName, qualifier: Qualifier, **kwargs
3401
    ) -> GetProvisionedConcurrencyConfigResponse:
3402
        if qualifier == "$LATEST":
1✔
3403
            raise InvalidParameterValueException(
1✔
3404
                "The function resource provided must be an alias or a published version.",
3405
                Type="User",
3406
            )
3407
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3408
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3409
            function_name, qualifier, context
3410
        )
3411

3412
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3413
        if not provisioned_config:
1✔
3414
            raise ProvisionedConcurrencyConfigNotFoundException(
1✔
3415
                "No Provisioned Concurrency Config found for this function", Type="User"
3416
            )
3417

3418
        # TODO: make this compatible with alias pointer migration on update
3419
        if api_utils.qualifier_is_alias(qualifier):
1✔
3420
            state = lambda_stores[account_id][region]
1✔
3421
            fn = state.functions.get(function_name)
1✔
3422
            alias = fn.aliases.get(qualifier)
1✔
3423
            fn_arn = api_utils.qualified_lambda_arn(
1✔
3424
                function_name, alias.function_version, account_id, region
3425
            )
3426
        else:
3427
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3428

3429
        ver_manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3430

3431
        return GetProvisionedConcurrencyConfigResponse(
1✔
3432
            RequestedProvisionedConcurrentExecutions=provisioned_config.provisioned_concurrent_executions,
3433
            LastModified=provisioned_config.last_modified,
3434
            AvailableProvisionedConcurrentExecutions=ver_manager.provisioned_state.available,
3435
            AllocatedProvisionedConcurrentExecutions=ver_manager.provisioned_state.allocated,
3436
            Status=ver_manager.provisioned_state.status,
3437
            StatusReason=ver_manager.provisioned_state.status_reason,
3438
        )
3439

3440
    def list_provisioned_concurrency_configs(
1✔
3441
        self,
3442
        context: RequestContext,
3443
        function_name: FunctionName,
3444
        marker: String = None,
3445
        max_items: MaxProvisionedConcurrencyConfigListItems = None,
3446
        **kwargs,
3447
    ) -> ListProvisionedConcurrencyConfigsResponse:
3448
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3449
        state = lambda_stores[account_id][region]
1✔
3450

3451
        function_name = api_utils.get_function_name(function_name, context)
1✔
3452
        fn = state.functions.get(function_name)
1✔
3453
        if fn is None:
1✔
3454
            raise ResourceNotFoundException(
1✔
3455
                f"Function not found: {api_utils.unqualified_lambda_arn(function_name, account_id, region)}",
3456
                Type="User",
3457
            )
3458

3459
        configs = []
1✔
3460
        for qualifier, pc_config in fn.provisioned_concurrency_configs.items():
1✔
UNCOV
3461
            if api_utils.qualifier_is_alias(qualifier):
×
UNCOV
3462
                alias = fn.aliases.get(qualifier)
×
3463
                fn_arn = api_utils.qualified_lambda_arn(
×
3464
                    function_name, alias.function_version, account_id, region
3465
                )
3466
            else:
UNCOV
3467
                fn_arn = api_utils.qualified_lambda_arn(
×
3468
                    function_name, qualifier, account_id, region
3469
                )
3470

UNCOV
3471
            manager = self.lambda_service.get_lambda_version_manager(fn_arn)
×
3472

UNCOV
3473
            configs.append(
×
3474
                ProvisionedConcurrencyConfigListItem(
3475
                    FunctionArn=api_utils.qualified_lambda_arn(
3476
                        function_name, qualifier, account_id, region
3477
                    ),
3478
                    RequestedProvisionedConcurrentExecutions=pc_config.provisioned_concurrent_executions,
3479
                    AvailableProvisionedConcurrentExecutions=manager.provisioned_state.available,
3480
                    AllocatedProvisionedConcurrentExecutions=manager.provisioned_state.allocated,
3481
                    Status=manager.provisioned_state.status,
3482
                    StatusReason=manager.provisioned_state.status_reason,
3483
                    LastModified=pc_config.last_modified,
3484
                )
3485
            )
3486

3487
        provisioned_concurrency_configs = configs
1✔
3488
        provisioned_concurrency_configs = PaginatedList(provisioned_concurrency_configs)
1✔
3489
        page, token = provisioned_concurrency_configs.get_page(
1✔
3490
            lambda x: x,
3491
            marker,
3492
            max_items,
3493
        )
3494
        return ListProvisionedConcurrencyConfigsResponse(
1✔
3495
            ProvisionedConcurrencyConfigs=page, NextMarker=token
3496
        )
3497

3498
    def delete_provisioned_concurrency_config(
1✔
3499
        self, context: RequestContext, function_name: FunctionName, qualifier: Qualifier, **kwargs
3500
    ) -> None:
3501
        if qualifier == "$LATEST":
1✔
3502
            raise InvalidParameterValueException(
1✔
3503
                "The function resource provided must be an alias or a published version.",
3504
                Type="User",
3505
            )
3506
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3507
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3508
            function_name, qualifier, context
3509
        )
3510
        state = lambda_stores[account_id][region]
1✔
3511
        fn = state.functions.get(function_name)
1✔
3512

3513
        provisioned_config = self._get_provisioned_config(context, function_name, qualifier)
1✔
3514
        # delete is idempotent and doesn't actually care about the provisioned concurrency config not existing
3515
        if provisioned_config:
1✔
3516
            fn.provisioned_concurrency_configs.pop(qualifier)
1✔
3517
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3518
            manager = self.lambda_service.get_lambda_version_manager(fn_arn)
1✔
3519
            manager.update_provisioned_concurrency_config(0)
1✔
3520

3521
    # =======================================
3522
    # =======  Event Invoke Config   ========
3523
    # =======================================
3524

3525
    # "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})?:(.*)"
3526
    # "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)
3527

3528
    def _validate_destination_config(
1✔
3529
        self, store: LambdaStore, function_name: str, destination_config: DestinationConfig
3530
    ):
3531
        def _validate_destination_arn(destination_arn) -> bool:
1✔
3532
            if not api_utils.DESTINATION_ARN_PATTERN.match(destination_arn):
1✔
3533
                # technically we shouldn't handle this in the provider
3534
                raise ValidationException(
1✔
3535
                    "1 validation error detected: Value '"
3536
                    + destination_arn
3537
                    + "' at 'destinationConfig.onFailure.destination' failed to satisfy constraint: Member must satisfy regular expression pattern: "
3538
                    + "$|kafka://([^.]([a-zA-Z0-9\\-_.]{0,248}))|arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\\-])+:((eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1})?:(\\d{12})?:(.*)"
3539
                )
3540

3541
            match destination_arn.split(":")[2]:
1✔
3542
                case "lambda":
1✔
3543
                    fn_parts = api_utils.FULL_FN_ARN_PATTERN.search(destination_arn).groupdict()
1✔
3544
                    if fn_parts:
1✔
3545
                        # check if it exists
3546
                        fn = store.functions.get(fn_parts["function_name"])
1✔
3547
                        if not fn:
1✔
3548
                            raise InvalidParameterValueException(
1✔
3549
                                f"The destination ARN {destination_arn} is invalid.", Type="User"
3550
                            )
3551
                        if fn_parts["function_name"] == function_name:
1✔
3552
                            raise InvalidParameterValueException(
1✔
3553
                                "You can't specify the function as a destination for itself.",
3554
                                Type="User",
3555
                            )
3556
                case "sns" | "sqs" | "events":
1✔
3557
                    pass
1✔
3558
                case _:
1✔
3559
                    return False
1✔
3560
            return True
1✔
3561

3562
        validation_err = False
1✔
3563

3564
        failure_destination = destination_config.get("OnFailure", {}).get("Destination")
1✔
3565
        if failure_destination:
1✔
3566
            validation_err = validation_err or not _validate_destination_arn(failure_destination)
1✔
3567

3568
        success_destination = destination_config.get("OnSuccess", {}).get("Destination")
1✔
3569
        if success_destination:
1✔
3570
            validation_err = validation_err or not _validate_destination_arn(success_destination)
1✔
3571

3572
        if validation_err:
1✔
3573
            on_success_part = (
1✔
3574
                f"OnSuccess(destination={success_destination})" if success_destination else "null"
3575
            )
3576
            on_failure_part = (
1✔
3577
                f"OnFailure(destination={failure_destination})" if failure_destination else "null"
3578
            )
3579
            raise InvalidParameterValueException(
1✔
3580
                f"The provided destination config DestinationConfig(onSuccess={on_success_part}, onFailure={on_failure_part}) is invalid.",
3581
                Type="User",
3582
            )
3583

3584
    def put_function_event_invoke_config(
1✔
3585
        self,
3586
        context: RequestContext,
3587
        function_name: FunctionName,
3588
        qualifier: NumericLatestPublishedOrAliasQualifier = None,
3589
        maximum_retry_attempts: MaximumRetryAttempts = None,
3590
        maximum_event_age_in_seconds: MaximumEventAgeInSeconds = None,
3591
        destination_config: DestinationConfig = None,
3592
        **kwargs,
3593
    ) -> FunctionEventInvokeConfig:
3594
        """
3595
        Destination ARNs can be:
3596
        * SQS arn
3597
        * SNS arn
3598
        * Lambda arn
3599
        * EventBridge arn
3600

3601
        Differences between put_ and update_:
3602
            * put overwrites any existing config
3603
            * update allows changes only single values while keeping the rest of existing ones
3604
            * update fails on non-existing configs
3605

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

3610
        """
3611
        if (
1✔
3612
            maximum_event_age_in_seconds is None
3613
            and maximum_retry_attempts is None
3614
            and destination_config is None
3615
        ):
3616
            raise InvalidParameterValueException(
1✔
3617
                "You must specify at least one of error handling or destination setting.",
3618
                Type="User",
3619
            )
3620
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3621
        state = lambda_stores[account_id][region]
1✔
3622
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3623
            function_name, qualifier, context
3624
        )
3625
        fn = state.functions.get(function_name)
1✔
3626
        if not fn or (qualifier and not (qualifier in fn.aliases or qualifier in fn.versions)):
1✔
3627
            raise ResourceNotFoundException("The function doesn't exist.", Type="User")
1✔
3628

3629
        qualifier = qualifier or "$LATEST"
1✔
3630

3631
        # validate and normalize destination config
3632
        if destination_config:
1✔
3633
            self._validate_destination_config(state, function_name, destination_config)
1✔
3634

3635
        destination_config = DestinationConfig(
1✔
3636
            OnSuccess=OnSuccess(
3637
                Destination=(destination_config or {}).get("OnSuccess", {}).get("Destination")
3638
            ),
3639
            OnFailure=OnFailure(
3640
                Destination=(destination_config or {}).get("OnFailure", {}).get("Destination")
3641
            ),
3642
        )
3643

3644
        config = EventInvokeConfig(
1✔
3645
            function_name=function_name,
3646
            qualifier=qualifier,
3647
            maximum_event_age_in_seconds=maximum_event_age_in_seconds,
3648
            maximum_retry_attempts=maximum_retry_attempts,
3649
            last_modified=api_utils.generate_lambda_date(),
3650
            destination_config=destination_config,
3651
        )
3652
        fn.event_invoke_configs[qualifier] = config
1✔
3653

3654
        return FunctionEventInvokeConfig(
1✔
3655
            LastModified=datetime.datetime.strptime(
3656
                config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3657
            ),
3658
            FunctionArn=api_utils.qualified_lambda_arn(
3659
                function_name, qualifier or "$LATEST", account_id, region
3660
            ),
3661
            DestinationConfig=destination_config,
3662
            MaximumEventAgeInSeconds=maximum_event_age_in_seconds,
3663
            MaximumRetryAttempts=maximum_retry_attempts,
3664
        )
3665

3666
    def get_function_event_invoke_config(
1✔
3667
        self,
3668
        context: RequestContext,
3669
        function_name: FunctionName,
3670
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
3671
        **kwargs,
3672
    ) -> FunctionEventInvokeConfig:
3673
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3674
        state = lambda_stores[account_id][region]
1✔
3675
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3676
            function_name, qualifier, context
3677
        )
3678

3679
        qualifier = qualifier or "$LATEST"
1✔
3680
        fn = state.functions.get(function_name)
1✔
3681
        if not fn:
1✔
3682
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3683
            raise ResourceNotFoundException(
1✔
3684
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3685
            )
3686

3687
        config = fn.event_invoke_configs.get(qualifier)
1✔
3688
        if not config:
1✔
3689
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3690
            raise ResourceNotFoundException(
1✔
3691
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3692
            )
3693

3694
        return FunctionEventInvokeConfig(
1✔
3695
            LastModified=datetime.datetime.strptime(
3696
                config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3697
            ),
3698
            FunctionArn=api_utils.qualified_lambda_arn(
3699
                function_name, qualifier, account_id, region
3700
            ),
3701
            DestinationConfig=config.destination_config,
3702
            MaximumEventAgeInSeconds=config.maximum_event_age_in_seconds,
3703
            MaximumRetryAttempts=config.maximum_retry_attempts,
3704
        )
3705

3706
    def list_function_event_invoke_configs(
1✔
3707
        self,
3708
        context: RequestContext,
3709
        function_name: FunctionName,
3710
        marker: String = None,
3711
        max_items: MaxFunctionEventInvokeConfigListItems = None,
3712
        **kwargs,
3713
    ) -> ListFunctionEventInvokeConfigsResponse:
3714
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3715
        state = lambda_stores[account_id][region]
1✔
3716
        fn = state.functions.get(function_name)
1✔
3717
        if not fn:
1✔
3718
            raise ResourceNotFoundException("The function doesn't exist.", Type="User")
1✔
3719

3720
        event_invoke_configs = [
1✔
3721
            FunctionEventInvokeConfig(
3722
                LastModified=c.last_modified,
3723
                FunctionArn=api_utils.qualified_lambda_arn(
3724
                    function_name, c.qualifier, account_id, region
3725
                ),
3726
                MaximumEventAgeInSeconds=c.maximum_event_age_in_seconds,
3727
                MaximumRetryAttempts=c.maximum_retry_attempts,
3728
                DestinationConfig=c.destination_config,
3729
            )
3730
            for c in fn.event_invoke_configs.values()
3731
        ]
3732

3733
        event_invoke_configs = PaginatedList(event_invoke_configs)
1✔
3734
        page, token = event_invoke_configs.get_page(
1✔
3735
            lambda x: x["FunctionArn"],
3736
            marker,
3737
            max_items,
3738
        )
3739
        return ListFunctionEventInvokeConfigsResponse(
1✔
3740
            FunctionEventInvokeConfigs=page, NextMarker=token
3741
        )
3742

3743
    def delete_function_event_invoke_config(
1✔
3744
        self,
3745
        context: RequestContext,
3746
        function_name: FunctionName,
3747
        qualifier: NumericLatestPublishedOrAliasQualifier | None = None,
3748
        **kwargs,
3749
    ) -> None:
3750
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3751
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3752
            function_name, qualifier, context
3753
        )
3754
        state = lambda_stores[account_id][region]
1✔
3755
        fn = state.functions.get(function_name)
1✔
3756
        resolved_qualifier = qualifier or "$LATEST"
1✔
3757
        fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3758
        if not fn:
1✔
3759
            raise ResourceNotFoundException(
1✔
3760
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3761
            )
3762

3763
        config = fn.event_invoke_configs.get(resolved_qualifier)
1✔
3764
        if not config:
1✔
3765
            raise ResourceNotFoundException(
1✔
3766
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3767
            )
3768

3769
        del fn.event_invoke_configs[resolved_qualifier]
1✔
3770

3771
    def update_function_event_invoke_config(
1✔
3772
        self,
3773
        context: RequestContext,
3774
        function_name: FunctionName,
3775
        qualifier: NumericLatestPublishedOrAliasQualifier = None,
3776
        maximum_retry_attempts: MaximumRetryAttempts = None,
3777
        maximum_event_age_in_seconds: MaximumEventAgeInSeconds = None,
3778
        destination_config: DestinationConfig = None,
3779
        **kwargs,
3780
    ) -> FunctionEventInvokeConfig:
3781
        # like put but only update single fields via replace
3782
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
3783
        state = lambda_stores[account_id][region]
1✔
3784
        function_name, qualifier = api_utils.get_name_and_qualifier(
1✔
3785
            function_name, qualifier, context
3786
        )
3787

3788
        if (
1✔
3789
            maximum_event_age_in_seconds is None
3790
            and maximum_retry_attempts is None
3791
            and destination_config is None
3792
        ):
UNCOV
3793
            raise InvalidParameterValueException(
×
3794
                "You must specify at least one of error handling or destination setting.",
3795
                Type="User",
3796
            )
3797

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

3802
        qualifier = qualifier or "$LATEST"
1✔
3803

3804
        config = fn.event_invoke_configs.get(qualifier)
1✔
3805
        if not config:
1✔
3806
            fn_arn = api_utils.qualified_lambda_arn(function_name, qualifier, account_id, region)
1✔
3807
            raise ResourceNotFoundException(
1✔
3808
                f"The function {fn_arn} doesn't have an EventInvokeConfig", Type="User"
3809
            )
3810

3811
        if destination_config:
1✔
UNCOV
3812
            self._validate_destination_config(state, function_name, destination_config)
×
3813

3814
        optional_kwargs = {
1✔
3815
            k: v
3816
            for k, v in {
3817
                "destination_config": destination_config,
3818
                "maximum_retry_attempts": maximum_retry_attempts,
3819
                "maximum_event_age_in_seconds": maximum_event_age_in_seconds,
3820
            }.items()
3821
            if v is not None
3822
        }
3823

3824
        new_config = dataclasses.replace(
1✔
3825
            config, last_modified=api_utils.generate_lambda_date(), **optional_kwargs
3826
        )
3827
        fn.event_invoke_configs[qualifier] = new_config
1✔
3828

3829
        return FunctionEventInvokeConfig(
1✔
3830
            LastModified=datetime.datetime.strptime(
3831
                new_config.last_modified, api_utils.LAMBDA_DATE_FORMAT
3832
            ),
3833
            FunctionArn=api_utils.qualified_lambda_arn(
3834
                function_name, qualifier or "$LATEST", account_id, region
3835
            ),
3836
            DestinationConfig=new_config.destination_config,
3837
            MaximumEventAgeInSeconds=new_config.maximum_event_age_in_seconds,
3838
            MaximumRetryAttempts=new_config.maximum_retry_attempts,
3839
        )
3840

3841
    # =======================================
3842
    # ======  Layer & Layer Versions  =======
3843
    # =======================================
3844

3845
    @staticmethod
1✔
3846
    def _resolve_layer(
1✔
3847
        layer_name_or_arn: str, context: RequestContext
3848
    ) -> tuple[str, str, str, str | None]:
3849
        """
3850
        Return locator attributes for a given Lambda layer.
3851

3852
        :param layer_name_or_arn: Layer name or ARN
3853
        :param context: Request context
3854
        :return: Tuple of region, account ID, layer name, layer version
3855
        """
3856
        if api_utils.is_layer_arn(layer_name_or_arn):
1✔
3857
            return api_utils.parse_layer_arn(layer_name_or_arn)
1✔
3858

3859
        return context.region, context.account_id, layer_name_or_arn, None
1✔
3860

3861
    def publish_layer_version(
1✔
3862
        self,
3863
        context: RequestContext,
3864
        layer_name: LayerName,
3865
        content: LayerVersionContentInput,
3866
        description: Description | None = None,
3867
        compatible_runtimes: CompatibleRuntimes | None = None,
3868
        license_info: LicenseInfo | None = None,
3869
        compatible_architectures: CompatibleArchitectures | None = None,
3870
        **kwargs,
3871
    ) -> PublishLayerVersionResponse:
3872
        """
3873
        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.
3874
        Note that there are no $LATEST versions with layers!
3875

3876
        """
3877
        account = context.account_id
1✔
3878
        region = context.region
1✔
3879

3880
        validation_errors = api_utils.validate_layer_runtimes_and_architectures(
1✔
3881
            compatible_runtimes, compatible_architectures
3882
        )
3883
        if validation_errors:
1✔
3884
            raise ValidationException(
1✔
3885
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {'; '.join(validation_errors)}"
3886
            )
3887

3888
        state = lambda_stores[account][region]
1✔
3889
        with self.create_layer_lock:
1✔
3890
            if layer_name not in state.layers:
1✔
3891
                # we don't have a version so create new layer object
3892
                # lock is required to avoid creating two v1 objects for the same name
3893
                layer = Layer(
1✔
3894
                    arn=api_utils.layer_arn(layer_name=layer_name, account=account, region=region)
3895
                )
3896
                state.layers[layer_name] = layer
1✔
3897

3898
        layer = state.layers[layer_name]
1✔
3899
        with layer.next_version_lock:
1✔
3900
            next_version = LambdaLayerVersionIdentifier(
1✔
3901
                account_id=account, region=region, layer_name=layer_name
3902
            ).generate(next_version=layer.next_version)
3903
            # When creating a layer with user defined layer version, it is possible that we
3904
            # create layer versions out of order.
3905
            # ie. a user could replicate layer v2 then layer v1. It is important to always keep the maximum possible
3906
            # value for next layer to avoid overwriting existing versions
3907
            if layer.next_version <= next_version:
1✔
3908
                # We don't need to update layer.next_version if the created version is lower than the "next in line"
3909
                layer.next_version = max(next_version, layer.next_version) + 1
1✔
3910

3911
        # creating a new layer
3912
        if content.get("ZipFile"):
1✔
3913
            code = store_lambda_archive(
1✔
3914
                archive_file=content["ZipFile"],
3915
                function_name=layer_name,
3916
                region_name=region,
3917
                account_id=account,
3918
            )
3919
        else:
3920
            code = store_s3_bucket_archive(
1✔
3921
                archive_bucket=content["S3Bucket"],
3922
                archive_key=content["S3Key"],
3923
                archive_version=content.get("S3ObjectVersion"),
3924
                function_name=layer_name,
3925
                region_name=region,
3926
                account_id=account,
3927
            )
3928

3929
        new_layer_version = LayerVersion(
1✔
3930
            layer_version_arn=api_utils.layer_version_arn(
3931
                layer_name=layer_name,
3932
                account=account,
3933
                region=region,
3934
                version=str(next_version),
3935
            ),
3936
            layer_arn=layer.arn,
3937
            version=next_version,
3938
            description=description or "",
3939
            license_info=license_info,
3940
            compatible_runtimes=compatible_runtimes,
3941
            compatible_architectures=compatible_architectures,
3942
            created=api_utils.generate_lambda_date(),
3943
            code=code,
3944
        )
3945

3946
        layer.layer_versions[str(next_version)] = new_layer_version
1✔
3947

3948
        return api_utils.map_layer_out(new_layer_version)
1✔
3949

3950
    def get_layer_version(
1✔
3951
        self,
3952
        context: RequestContext,
3953
        layer_name: LayerName,
3954
        version_number: LayerVersionNumber,
3955
        **kwargs,
3956
    ) -> GetLayerVersionResponse:
3957
        # TODO: handle layer_name as an ARN
3958

3959
        region_name, account_id, layer_name, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
3960
        state = lambda_stores[account_id][region_name]
1✔
3961

3962
        layer = state.layers.get(layer_name)
1✔
3963
        if version_number < 1:
1✔
3964
            raise InvalidParameterValueException("Layer Version Cannot be less than 1", Type="User")
1✔
3965
        if layer is None:
1✔
3966
            raise ResourceNotFoundException(
1✔
3967
                "The resource you requested does not exist.", Type="User"
3968
            )
3969
        layer_version = layer.layer_versions.get(str(version_number))
1✔
3970
        if layer_version is None:
1✔
3971
            raise ResourceNotFoundException(
1✔
3972
                "The resource you requested does not exist.", Type="User"
3973
            )
3974
        return api_utils.map_layer_out(layer_version)
1✔
3975

3976
    def get_layer_version_by_arn(
1✔
3977
        self, context: RequestContext, arn: LayerVersionArn, **kwargs
3978
    ) -> GetLayerVersionResponse:
3979
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
3980
            arn, context
3981
        )
3982

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

3989
        store = lambda_stores[account_id][region_name]
1✔
3990
        if not (layers := store.layers.get(layer_name)):
1✔
UNCOV
3991
            raise ResourceNotFoundException(
×
3992
                "The resource you requested does not exist.", Type="User"
3993
            )
3994

3995
        layer_version = layers.layer_versions.get(layer_version)
1✔
3996

3997
        if not layer_version:
1✔
3998
            raise ResourceNotFoundException(
1✔
3999
                "The resource you requested does not exist.", Type="User"
4000
            )
4001

4002
        return api_utils.map_layer_out(layer_version)
1✔
4003

4004
    def list_layers(
1✔
4005
        self,
4006
        context: RequestContext,
4007
        compatible_runtime: Runtime | None = None,
4008
        marker: String | None = None,
4009
        max_items: MaxLayerListItems | None = None,
4010
        compatible_architecture: Architecture | None = None,
4011
        **kwargs,
4012
    ) -> ListLayersResponse:
4013
        validation_errors = []
1✔
4014

4015
        validation_error_arch = api_utils.validate_layer_architecture(compatible_architecture)
1✔
4016
        if validation_error_arch:
1✔
4017
            validation_errors.append(validation_error_arch)
1✔
4018

4019
        validation_error_runtime = api_utils.validate_layer_runtime(compatible_runtime)
1✔
4020
        if validation_error_runtime:
1✔
4021
            validation_errors.append(validation_error_runtime)
1✔
4022

4023
        if validation_errors:
1✔
4024
            raise ValidationException(
1✔
4025
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {';'.join(validation_errors)}"
4026
            )
4027
        # TODO: handle filter: compatible_runtime
4028
        # TODO: handle filter: compatible_architecture
4029

4030
        state = lambda_stores[context.account_id][context.region]
×
4031
        layers = state.layers
×
4032

4033
        # 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?
4034

UNCOV
4035
        responses: list[LayersListItem] = []
×
UNCOV
4036
        for layer_name, layer in layers.items():
×
4037
            # fetch latest version
UNCOV
4038
            layer_versions = list(layer.layer_versions.values())
×
UNCOV
4039
            sorted(layer_versions, key=lambda x: x.version)
×
UNCOV
4040
            latest_layer_version = layer_versions[-1]
×
4041
            responses.append(
×
4042
                LayersListItem(
4043
                    LayerName=layer_name,
4044
                    LayerArn=layer.arn,
4045
                    LatestMatchingVersion=api_utils.map_layer_out(latest_layer_version),
4046
                )
4047
            )
4048

UNCOV
4049
        responses = PaginatedList(responses)
×
UNCOV
4050
        page, token = responses.get_page(
×
4051
            lambda version: version,
4052
            marker,
4053
            max_items,
4054
        )
4055

UNCOV
4056
        return ListLayersResponse(NextMarker=token, Layers=page)
×
4057

4058
    def list_layer_versions(
1✔
4059
        self,
4060
        context: RequestContext,
4061
        layer_name: LayerName,
4062
        compatible_runtime: Runtime | None = None,
4063
        marker: String | None = None,
4064
        max_items: MaxLayerListItems | None = None,
4065
        compatible_architecture: Architecture | None = None,
4066
        **kwargs,
4067
    ) -> ListLayerVersionsResponse:
4068
        validation_errors = api_utils.validate_layer_runtimes_and_architectures(
1✔
4069
            [compatible_runtime] if compatible_runtime else [],
4070
            [compatible_architecture] if compatible_architecture else [],
4071
        )
4072
        if validation_errors:
1✔
UNCOV
4073
            raise ValidationException(
×
4074
                f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {';'.join(validation_errors)}"
4075
            )
4076

4077
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
4078
            layer_name, context
4079
        )
4080
        state = lambda_stores[account_id][region_name]
1✔
4081

4082
        # TODO: Test & handle filter: compatible_runtime
4083
        # TODO: Test & handle filter: compatible_architecture
4084
        all_layer_versions = []
1✔
4085
        layer = state.layers.get(layer_name)
1✔
4086
        if layer is not None:
1✔
4087
            for layer_version in layer.layer_versions.values():
1✔
4088
                all_layer_versions.append(api_utils.map_layer_out(layer_version))
1✔
4089

4090
        all_layer_versions.sort(key=lambda x: x["Version"], reverse=True)
1✔
4091
        all_layer_versions = PaginatedList(all_layer_versions)
1✔
4092
        page, token = all_layer_versions.get_page(
1✔
4093
            lambda version: version["LayerVersionArn"],
4094
            marker,
4095
            max_items,
4096
        )
4097
        return ListLayerVersionsResponse(NextMarker=token, LayerVersions=page)
1✔
4098

4099
    def delete_layer_version(
1✔
4100
        self,
4101
        context: RequestContext,
4102
        layer_name: LayerName,
4103
        version_number: LayerVersionNumber,
4104
        **kwargs,
4105
    ) -> None:
4106
        if version_number < 1:
1✔
4107
            raise InvalidParameterValueException("Layer Version Cannot be less than 1", Type="User")
1✔
4108

4109
        region_name, account_id, layer_name, layer_version = LambdaProvider._resolve_layer(
1✔
4110
            layer_name, context
4111
        )
4112

4113
        store = lambda_stores[account_id][region_name]
1✔
4114
        layer = store.layers.get(layer_name, {})
1✔
4115
        if layer:
1✔
4116
            layer.layer_versions.pop(str(version_number), None)
1✔
4117

4118
    # =======================================
4119
    # =====  Layer Version Permissions  =====
4120
    # =======================================
4121
    # TODO: lock updates that change revision IDs
4122

4123
    def add_layer_version_permission(
1✔
4124
        self,
4125
        context: RequestContext,
4126
        layer_name: LayerName,
4127
        version_number: LayerVersionNumber,
4128
        statement_id: StatementId,
4129
        action: LayerPermissionAllowedAction,
4130
        principal: LayerPermissionAllowedPrincipal,
4131
        organization_id: OrganizationId = None,
4132
        revision_id: String = None,
4133
        **kwargs,
4134
    ) -> AddLayerVersionPermissionResponse:
4135
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
4136
        # `layer_n` contains the layer name.
4137
        region_name, account_id, layer_n, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
4138

4139
        if action != "lambda:GetLayerVersion":
1✔
4140
            raise ValidationException(
1✔
4141
                f"1 validation error detected: Value '{action}' at 'action' failed to satisfy constraint: Member must satisfy regular expression pattern: lambda:GetLayerVersion"
4142
            )
4143

4144
        store = lambda_stores[account_id][region_name]
1✔
4145
        layer = store.layers.get(layer_n)
1✔
4146

4147
        layer_version_arn = api_utils.layer_version_arn(
1✔
4148
            layer_name, account_id, region_name, str(version_number)
4149
        )
4150

4151
        if layer is None:
1✔
4152
            raise ResourceNotFoundException(
1✔
4153
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4154
            )
4155
        layer_version = layer.layer_versions.get(str(version_number))
1✔
4156
        if layer_version is None:
1✔
4157
            raise ResourceNotFoundException(
1✔
4158
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4159
            )
4160
        # do we have a policy? if not set one
4161
        if layer_version.policy is None:
1✔
4162
            layer_version.policy = LayerPolicy()
1✔
4163

4164
        if statement_id in layer_version.policy.statements:
1✔
4165
            raise ResourceConflictException(
1✔
4166
                f"The statement id ({statement_id}) provided already exists. Please provide a new statement id, or remove the existing statement.",
4167
                Type="User",
4168
            )
4169

4170
        if revision_id and layer_version.policy.revision_id != revision_id:
1✔
4171
            raise PreconditionFailedException(
1✔
4172
                "The Revision Id provided does not match the latest Revision Id. "
4173
                "Call the GetLayerPolicy API to retrieve the latest Revision Id",
4174
                Type="User",
4175
            )
4176

4177
        statement = LayerPolicyStatement(
1✔
4178
            sid=statement_id, action=action, principal=principal, organization_id=organization_id
4179
        )
4180

4181
        old_statements = layer_version.policy.statements
1✔
4182
        layer_version.policy = dataclasses.replace(
1✔
4183
            layer_version.policy, statements={**old_statements, statement_id: statement}
4184
        )
4185

4186
        return AddLayerVersionPermissionResponse(
1✔
4187
            Statement=json.dumps(
4188
                {
4189
                    "Sid": statement.sid,
4190
                    "Effect": "Allow",
4191
                    "Principal": statement.principal,
4192
                    "Action": statement.action,
4193
                    "Resource": layer_version.layer_version_arn,
4194
                }
4195
            ),
4196
            RevisionId=layer_version.policy.revision_id,
4197
        )
4198

4199
    def remove_layer_version_permission(
1✔
4200
        self,
4201
        context: RequestContext,
4202
        layer_name: LayerName,
4203
        version_number: LayerVersionNumber,
4204
        statement_id: StatementId,
4205
        revision_id: String = None,
4206
        **kwargs,
4207
    ) -> None:
4208
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
4209
        # `layer_n` contains the layer name.
4210
        region_name, account_id, layer_n, layer_version = LambdaProvider._resolve_layer(
1✔
4211
            layer_name, context
4212
        )
4213

4214
        layer_version_arn = api_utils.layer_version_arn(
1✔
4215
            layer_name, account_id, region_name, str(version_number)
4216
        )
4217

4218
        state = lambda_stores[account_id][region_name]
1✔
4219
        layer = state.layers.get(layer_n)
1✔
4220
        if layer is None:
1✔
4221
            raise ResourceNotFoundException(
1✔
4222
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4223
            )
4224
        layer_version = layer.layer_versions.get(str(version_number))
1✔
4225
        if layer_version is None:
1✔
4226
            raise ResourceNotFoundException(
1✔
4227
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4228
            )
4229

4230
        if revision_id and layer_version.policy.revision_id != revision_id:
1✔
4231
            raise PreconditionFailedException(
1✔
4232
                "The Revision Id provided does not match the latest Revision Id. "
4233
                "Call the GetLayerPolicy API to retrieve the latest Revision Id",
4234
                Type="User",
4235
            )
4236

4237
        if statement_id not in layer_version.policy.statements:
1✔
4238
            raise ResourceNotFoundException(
1✔
4239
                f"Statement {statement_id} is not found in resource policy.", Type="User"
4240
            )
4241

4242
        old_statements = layer_version.policy.statements
1✔
4243
        layer_version.policy = dataclasses.replace(
1✔
4244
            layer_version.policy,
4245
            statements={k: v for k, v in old_statements.items() if k != statement_id},
4246
        )
4247

4248
    def get_layer_version_policy(
1✔
4249
        self,
4250
        context: RequestContext,
4251
        layer_name: LayerName,
4252
        version_number: LayerVersionNumber,
4253
        **kwargs,
4254
    ) -> GetLayerVersionPolicyResponse:
4255
        # `layer_name` can either be layer name or ARN. It is used to generate error messages.
4256
        # `layer_n` contains the layer name.
4257
        region_name, account_id, layer_n, _ = LambdaProvider._resolve_layer(layer_name, context)
1✔
4258

4259
        layer_version_arn = api_utils.layer_version_arn(
1✔
4260
            layer_name, account_id, region_name, str(version_number)
4261
        )
4262

4263
        store = lambda_stores[account_id][region_name]
1✔
4264
        layer = store.layers.get(layer_n)
1✔
4265

4266
        if layer is None:
1✔
4267
            raise ResourceNotFoundException(
1✔
4268
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4269
            )
4270

4271
        layer_version = layer.layer_versions.get(str(version_number))
1✔
4272
        if layer_version is None:
1✔
4273
            raise ResourceNotFoundException(
1✔
4274
                f"Layer version {layer_version_arn} does not exist.", Type="User"
4275
            )
4276

4277
        if layer_version.policy is None:
1✔
4278
            raise ResourceNotFoundException(
1✔
4279
                "No policy is associated with the given resource.", Type="User"
4280
            )
4281

4282
        return GetLayerVersionPolicyResponse(
1✔
4283
            Policy=json.dumps(
4284
                {
4285
                    "Version": layer_version.policy.version,
4286
                    "Id": layer_version.policy.id,
4287
                    "Statement": [
4288
                        {
4289
                            "Sid": ps.sid,
4290
                            "Effect": "Allow",
4291
                            "Principal": ps.principal,
4292
                            "Action": ps.action,
4293
                            "Resource": layer_version.layer_version_arn,
4294
                        }
4295
                        for ps in layer_version.policy.statements.values()
4296
                    ],
4297
                }
4298
            ),
4299
            RevisionId=layer_version.policy.revision_id,
4300
        )
4301

4302
    # =======================================
4303
    # =======  Function Concurrency  ========
4304
    # =======================================
4305
    # (Reserved) function concurrency is scoped to the whole function
4306

4307
    def get_function_concurrency(
1✔
4308
        self, context: RequestContext, function_name: FunctionName, **kwargs
4309
    ) -> GetFunctionConcurrencyResponse:
4310
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4311
        function_name = api_utils.get_function_name(function_name, context)
1✔
4312
        fn = self._get_function(function_name=function_name, region=region, account_id=account_id)
1✔
4313
        return GetFunctionConcurrencyResponse(
1✔
4314
            ReservedConcurrentExecutions=fn.reserved_concurrent_executions
4315
        )
4316

4317
    def put_function_concurrency(
1✔
4318
        self,
4319
        context: RequestContext,
4320
        function_name: FunctionName,
4321
        reserved_concurrent_executions: ReservedConcurrentExecutions,
4322
        **kwargs,
4323
    ) -> Concurrency:
4324
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4325

4326
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
4327
        if qualifier:
1✔
4328
            raise InvalidParameterValueException(
1✔
4329
                "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.",
4330
                Type="User",
4331
            )
4332

4333
        store = lambda_stores[account_id][region]
1✔
4334
        fn = store.functions.get(function_name)
1✔
4335
        if not fn:
1✔
4336
            fn_arn = api_utils.qualified_lambda_arn(
1✔
4337
                function_name,
4338
                qualifier="$LATEST",
4339
                account=account_id,
4340
                region=region,
4341
            )
4342
            raise ResourceNotFoundException(f"Function not found: {fn_arn}", Type="User")
1✔
4343

4344
        settings = self.get_account_settings(context)
1✔
4345
        unreserved_concurrent_executions = settings["AccountLimit"][
1✔
4346
            "UnreservedConcurrentExecutions"
4347
        ]
4348

4349
        # The existing reserved concurrent executions for the same function are already deduced in
4350
        # unreserved_concurrent_executions but must not count because the new one will replace the existing one.
4351
        # Joel tested this behavior manually against AWS (2023-11-28).
4352
        existing_reserved_concurrent_executions = (
1✔
4353
            fn.reserved_concurrent_executions if fn.reserved_concurrent_executions else 0
4354
        )
4355
        if (
1✔
4356
            unreserved_concurrent_executions
4357
            - reserved_concurrent_executions
4358
            + existing_reserved_concurrent_executions
4359
        ) < config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY:
4360
            raise InvalidParameterValueException(
1✔
4361
                f"Specified ReservedConcurrentExecutions for function decreases account's UnreservedConcurrentExecution below its minimum value of [{config.LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY}]."
4362
            )
4363

4364
        total_provisioned_concurrency = sum(
1✔
4365
            [
4366
                provisioned_configs.provisioned_concurrent_executions
4367
                for provisioned_configs in fn.provisioned_concurrency_configs.values()
4368
            ]
4369
        )
4370
        if total_provisioned_concurrency > reserved_concurrent_executions:
1✔
4371
            raise InvalidParameterValueException(
1✔
4372
                f" ReservedConcurrentExecutions  {reserved_concurrent_executions} should not be lower than function's total provisioned concurrency [{total_provisioned_concurrency}]."
4373
            )
4374

4375
        fn.reserved_concurrent_executions = reserved_concurrent_executions
1✔
4376

4377
        return Concurrency(ReservedConcurrentExecutions=fn.reserved_concurrent_executions)
1✔
4378

4379
    def delete_function_concurrency(
1✔
4380
        self, context: RequestContext, function_name: FunctionName, **kwargs
4381
    ) -> None:
4382
        account_id, region = api_utils.get_account_and_region(function_name, context)
1✔
4383
        function_name, qualifier = api_utils.get_name_and_qualifier(function_name, None, context)
1✔
4384
        store = lambda_stores[account_id][region]
1✔
4385
        fn = store.functions.get(function_name)
1✔
4386
        fn.reserved_concurrent_executions = None
1✔
4387

4388
    # =======================================
4389
    # ===============  TAGS   ===============
4390
    # =======================================
4391
    # only Function, Event Source Mapping, and Code Signing Config (not currently supported by LocalStack) ARNs
4392
    # are available for tagging in AWS
4393

4394
    def _update_resource_tags(
1✔
4395
        self, resource_arn: str, account_id: str, region: str, tags: dict[str, str]
4396
    ) -> None:
4397
        tagger_service = lambda_stores[account_id][region].TAGS
1✔
4398
        tag_svc_adapted_tags = [{"Key": key, "Value": value} for key, value in tags.items()]
1✔
4399
        tagger_service.tag_resource(resource_arn, tag_svc_adapted_tags)
1✔
4400

4401
    def _list_resource_tags(
1✔
4402
        self, resource_arn: str, account_id: str, region: str
4403
    ) -> dict[str, str]:
4404
        tagger_service = lambda_stores[account_id][region].TAGS
1✔
4405
        return tagger_service.tags.get(resource_arn, {})
1✔
4406

4407
    def _remove_resource_tags(
1✔
4408
        self, resource_arn: str, account_id: str, region: str, keys: TagKeyList
4409
    ) -> None:
4410
        tagger_service = lambda_stores[account_id][region].TAGS
1✔
4411
        tagger_service.untag_resource(resource_arn, keys)
1✔
4412

4413
    def _remove_all_resource_tags(self, resource_arn: str, account_id: str, region: str) -> None:
1✔
4414
        tagger_service = lambda_stores[account_id][region].TAGS
1✔
4415
        return tagger_service.tags.pop(resource_arn, None)
1✔
4416

4417
    def _get_tags(self, resource: TaggableResource) -> dict[str, str]:
1✔
4418
        account_id, region = self._get_account_id_and_region_for_taggable_resource(resource)
1✔
4419
        tags = self._list_resource_tags(resource_arn=resource, account_id=account_id, region=region)
1✔
4420
        return tags
1✔
4421

4422
    def _store_tags(self, resource: TaggableResource, tags: dict[str, str]) -> None:
1✔
4423
        account_id, region = self._get_account_id_and_region_for_taggable_resource(resource)
1✔
4424
        existing_tags = self._list_resource_tags(
1✔
4425
            resource_arn=resource, account_id=account_id, region=region
4426
        )
4427
        if len({**existing_tags, **tags}) > LAMBDA_TAG_LIMIT_PER_RESOURCE:
1✔
4428
            # note: we cannot use | on `ImmutableDict` and regular `dict`
4429
            raise InvalidParameterValueException(
1✔
4430
                "Number of tags exceeds resource tag limit.", Type="User"
4431
            )
4432
        self._update_resource_tags(
1✔
4433
            resource_arn=resource,
4434
            account_id=account_id,
4435
            region=region,
4436
            tags=tags,
4437
        )
4438

4439
    def _remove_tags(self, resource: TaggableResource, keys: TagKeyList) -> None:
1✔
4440
        account_id, region = self._get_account_id_and_region_for_taggable_resource(resource)
1✔
4441
        self._remove_resource_tags(
1✔
4442
            resource_arn=resource, account_id=account_id, region=region, keys=keys
4443
        )
4444

4445
    def _remove_all_tags(self, resource: TaggableResource) -> None:
1✔
4446
        account_id, region = self._get_account_id_and_region_for_taggable_resource(resource)
1✔
4447
        self._remove_all_resource_tags(resource_arn=resource, account_id=account_id, region=region)
1✔
4448

4449
    def _get_account_id_and_region_for_taggable_resource(
1✔
4450
        self, resource: TaggableResource
4451
    ) -> tuple[str, str]:
4452
        """
4453
        Takes a resource ARN for a TaggableResource (Lambda Function, Event Source Mapping, Code Signing Config, or Capacity Provider) and returns a corresponding
4454
        LambdaStore for its region and account.
4455

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

4458
        Raises:
4459
            ValidationException: If the resource ARN is not a full ARN for a TaggableResource.
4460
            ResourceNotFoundException: If the specified resource does not exist.
4461
            InvalidParameterValueException: If the resource ARN is a qualified Lambda Function.
4462
        """
4463

4464
        def _raise_validation_exception():
1✔
4465
            raise ValidationException(
1✔
4466
                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}"
4467
            )
4468

4469
        # Check whether the ARN we have been passed is correctly formatted
4470
        parsed_resource_arn: ArnData = None
1✔
4471
        try:
1✔
4472
            parsed_resource_arn = parse_arn(resource)
1✔
4473
        except Exception:
1✔
4474
            _raise_validation_exception()
1✔
4475

4476
        # TODO: Should we be checking whether this is a full ARN?
4477
        region, account_id, resource_type = map(
1✔
4478
            parsed_resource_arn.get, ("region", "account", "resource")
4479
        )
4480

4481
        if not all((region, account_id, resource_type)):
1✔
UNCOV
4482
            _raise_validation_exception()
×
4483

4484
        if not (parts := resource_type.split(":")):
1✔
UNCOV
4485
            _raise_validation_exception()
×
4486

4487
        resource_type, resource_identifier, *qualifier = parts
1✔
4488

4489
        # Qualifier validation raises before checking for NotFound
4490
        if qualifier:
1✔
4491
            if resource_type == "function":
1✔
4492
                raise InvalidParameterValueException(
1✔
4493
                    "Tags on function aliases and versions are not supported. Please specify a function ARN.",
4494
                    Type="User",
4495
                )
4496
            _raise_validation_exception()
1✔
4497

4498
        if resource_type == "event-source-mapping":
1✔
4499
            self._get_esm(resource_identifier, account_id, region)
1✔
4500
        elif resource_type == "code-signing-config":
1✔
4501
            raise NotImplementedError("Resource tagging on CSC not yet implemented.")
4502
        elif resource_type == "function":
1✔
4503
            self._get_function(
1✔
4504
                function_name=resource_identifier, account_id=account_id, region=region
4505
            )
4506
        elif resource_type == "capacity-provider":
1✔
4507
            self._get_capacity_provider(resource_identifier, account_id, region)
1✔
4508
        else:
4509
            _raise_validation_exception()
1✔
4510

4511
        # If no exceptions are raised, assume ARN and referenced resource is valid for tag operations
4512
        return account_id, region
1✔
4513

4514
    def tag_resource(
1✔
4515
        self, context: RequestContext, resource: TaggableResource, tags: Tags, **kwargs
4516
    ) -> None:
4517
        if not tags:
1✔
4518
            raise InvalidParameterValueException(
1✔
4519
                "An error occurred and the request cannot be processed.", Type="User"
4520
            )
4521
        self._store_tags(resource, tags)
1✔
4522

4523
        if (resource_id := extract_resource_from_arn(resource)) and resource_id.startswith(
1✔
4524
            "function"
4525
        ):
4526
            name, _, account, region = function_locators_from_arn(resource)
1✔
4527
            function = self._get_function(name, account, region)
1✔
4528
            with function.lock:
1✔
4529
                # dirty hack for changed revision id, should reevaluate model to prevent this:
4530
                latest_version = function.versions["$LATEST"]
1✔
4531
                function.versions["$LATEST"] = dataclasses.replace(
1✔
4532
                    latest_version, config=dataclasses.replace(latest_version.config)
4533
                )
4534

4535
    def list_tags(
1✔
4536
        self, context: RequestContext, resource: TaggableResource, **kwargs
4537
    ) -> ListTagsResponse:
4538
        tags = self._get_tags(resource)
1✔
4539
        return ListTagsResponse(Tags=tags)
1✔
4540

4541
    def untag_resource(
1✔
4542
        self, context: RequestContext, resource: TaggableResource, tag_keys: TagKeyList, **kwargs
4543
    ) -> None:
4544
        if not tag_keys:
1✔
4545
            raise ValidationException(
1✔
4546
                "1 validation error detected: Value null at 'tagKeys' failed to satisfy constraint: Member must not be null"
4547
            )  # should probably be generalized a bit
4548

4549
        self._remove_tags(resource, tag_keys)
1✔
4550

4551
        if (resource_id := extract_resource_from_arn(resource)) and resource_id.startswith(
1✔
4552
            "function"
4553
        ):
4554
            name, _, account, region = function_locators_from_arn(resource)
1✔
4555
            function = self._get_function(name, account, region)
1✔
4556
            # TODO: Potential race condition
4557
            with function.lock:
1✔
4558
                # dirty hack for changed revision id, should reevaluate model to prevent this:
4559
                latest_version = function.versions["$LATEST"]
1✔
4560
                function.versions["$LATEST"] = dataclasses.replace(
1✔
4561
                    latest_version, config=dataclasses.replace(latest_version.config)
4562
                )
4563

4564
    # =======================================
4565
    # =======  LEGACY / DEPRECATED   ========
4566
    # =======================================
4567

4568
    def invoke_async(
1✔
4569
        self,
4570
        context: RequestContext,
4571
        function_name: NamespacedFunctionName,
4572
        invoke_args: IO[BlobStream],
4573
        **kwargs,
4574
    ) -> InvokeAsyncResponse:
4575
        """LEGACY API endpoint. Even AWS heavily discourages its usage."""
4576
        raise NotImplementedError
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc