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

localstack / localstack / 19821277742

01 Dec 2025 08:16AM UTC coverage: 86.821% (-0.04%) from 86.863%
19821277742

push

github

web-flow
Add Lambda Managed Instances (#13440)

Co-authored-by: Joel Scheuner <joel.scheuner.dev@gmail.com>
Co-authored-by: Anisa Oshafi <anisaoshafi@gmail.com>
Co-authored-by: Cristopher Pinzón <cristopher.pinzon@gmail.com>
Co-authored-by: Alexander Rashed <alexander.rashed@localstack.cloud>
Co-authored-by: Dominik Schubert <dominik.schubert91@gmail.com>
Co-authored-by: Mathieu Cloutier <79954947+cloutierMat@users.noreply.github.com>
Co-authored-by: Simon Walker <simon.walker@localstack.cloud>

127 of 181 new or added lines in 11 files covered. (70.17%)

17 existing lines in 5 files now uncovered.

69556 of 80114 relevant lines covered (86.82%)

0.87 hits per line

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

95.59
/localstack-core/localstack/services/lambda_/api_utils.py
1
"""Utilities related to Lambda API operations such as ARN handling, validations, and output formatting.
2
Everything related to behavior or implicit functionality goes into `lambda_utils.py`.
3
"""
4

5
import datetime
1✔
6
import hashlib
1✔
7
import json
1✔
8
import random
1✔
9
import re
1✔
10
import string
1✔
11
from typing import TYPE_CHECKING, Any
1✔
12

13
from localstack import config
1✔
14
from localstack.aws.api import CommonServiceException, RequestContext
1✔
15
from localstack.aws.api import lambda_ as api_spec
1✔
16
from localstack.aws.api.lambda_ import (
1✔
17
    AliasConfiguration,
18
    Architecture,
19
    DeadLetterConfig,
20
    EnvironmentResponse,
21
    EphemeralStorage,
22
    FunctionConfiguration,
23
    FunctionUrlAuthType,
24
    ImageConfig,
25
    ImageConfigResponse,
26
    InvalidParameterValueException,
27
    LayerVersionContentOutput,
28
    PublishLayerVersionResponse,
29
    ResourceNotFoundException,
30
    TracingConfig,
31
    VpcConfigResponse,
32
)
33
from localstack.services.lambda_.invocation import AccessDeniedException
1✔
34
from localstack.services.lambda_.runtimes import ALL_RUNTIMES, VALID_LAYER_RUNTIMES, VALID_RUNTIMES
1✔
35
from localstack.utils.aws.arns import ARN_PARTITION_REGEX, get_partition
1✔
36
from localstack.utils.collections import merge_recursive
1✔
37

38
if TYPE_CHECKING:
1✔
39
    from localstack.services.lambda_.invocation.lambda_models import (
×
40
        CodeSigningConfig,
41
        Function,
42
        FunctionUrlConfig,
43
        FunctionVersion,
44
        LayerVersion,
45
        VersionAlias,
46
    )
47
    from localstack.services.lambda_.invocation.models import LambdaStore
×
48

49

50
# Pattern for a full (both with and without qualifier) lambda function ARN
51
FULL_FN_ARN_PATTERN = re.compile(
1✔
52
    rf"{ARN_PARTITION_REGEX}:lambda:(?P<region_name>[^:]+):(?P<account_id>\d{{12}}):function:(?P<function_name>[^:]+)(:(?P<qualifier>.*))?$"
53
)
54

55
# Pattern for a full (both with and without qualifier) lambda layer ARN
56
# TODO: It looks like they added `|(arn:[a-zA-Z0-9-]+:lambda:::awslayer:[a-zA-Z0-9-_]+` in 2024-11
57
LAYER_VERSION_ARN_PATTERN = re.compile(
1✔
58
    rf"{ARN_PARTITION_REGEX}:lambda:(?P<region_name>[^:]+):(?P<account_id>\d{{12}}):layer:(?P<layer_name>[^:]+)(:(?P<layer_version>\d+))?$"
59
)
60

61

62
# Pattern for a valid destination arn
63
DESTINATION_ARN_PATTERN = re.compile(
1✔
64
    r"^$|arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\-])+:([a-z]{2}(-gov)?-[a-z]+-\d{1})?:(\d{12})?:(.*)"
65
)
66

67
# TODO: what's the difference between AWS_FUNCTION_NAME_REGEX and FUNCTION_NAME_REGEX? Can we unify?
68
AWS_FUNCTION_NAME_REGEX = re.compile(
1✔
69
    "^(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_.]+)(:(\\$LATEST(\\.PUBLISHED)?|[a-zA-Z0-9-_]+))?$"
70
)
71

72
# Pattern for extracting various attributes from a full or partial ARN or just a function name.
73
FUNCTION_NAME_REGEX = re.compile(
1✔
74
    r"(arn:(aws[a-zA-Z-]*):lambda:)?((?P<region>[a-z]{2}(-gov)?-[a-z]+-\d{1}):)?(?:(?P<account>\d{12}):)?(function:)?(?P<name>[a-zA-Z0-9-_\.]+)(:(?P<qualifier>\$LATEST(\.PUBLISHED)?|[a-zA-Z0-9-_]+))?"
75
)  # also length 1-170 incl.
76
# Pattern for a lambda function handler
77
HANDLER_REGEX = re.compile(r"[^\s]+")
1✔
78
# Pattern for a valid kms key
79
KMS_KEY_ARN_REGEX = re.compile(r"(arn:(aws[a-zA-Z-]*)?:[a-z0-9-.]+:.*)|()")
1✔
80
# Pattern for a valid IAM role assumed by a lambda function
81
ROLE_REGEX = re.compile(r"arn:(aws[a-zA-Z-]*)?:iam::\d{12}:role/?[a-zA-Z_0-9+=,.@\-_/]+")
1✔
82
# Pattern for a valid AWS account
83
AWS_ACCOUNT_REGEX = re.compile(r"\d{12}")
1✔
84
# Pattern for a signing job arn
85
SIGNING_JOB_ARN_REGEX = re.compile(
1✔
86
    r"arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\-])+:([a-z]{2}(-gov)?-[a-z]+-\d{1})?:(\d{12})?:(.*)"
87
)
88
# Pattern for a signing profiler version arn
89
SIGNING_PROFILE_VERSION_ARN_REGEX = re.compile(
1✔
90
    r"arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\-])+:([a-z]{2}(-gov)?-[a-z]+-\d{1})?:(\d{12})?:(.*)"
91
)
92
# Combined pattern for alias and version based on AWS error using "\\$(LATEST(\\.PUBLISHED)?)|[a-zA-Z0-9-_$]+"
93
# This regex is based on the snapshotted validation message, just removing the double \\ before $LATEST
94
QUALIFIER_REGEX = re.compile(r"^\$(LATEST(\\.PUBLISHED)?)|[a-zA-Z0-9-_$]+$")
1✔
95
# Pattern for a version qualifier
96
# TODO: do we need to consider $LATEST.PUBLISHED here?
97
VERSION_REGEX = re.compile(r"^[0-9]+$")
1✔
98
# Pattern for an alias qualifier
99
# Rules: https://docs.aws.amazon.com/lambda/latest/dg/API_CreateAlias.html#SSS-CreateAlias-request-Name
100
# The original regex from AWS misses ^ and $ in the second regex, which allowed for partial substring matches
101
ALIAS_REGEX = re.compile(r"(?!^[0-9]+$)(^[a-zA-Z0-9-_]+$)")
1✔
102
# Permission statement id
103
STATEMENT_ID_REGEX = re.compile(r"^[a-zA-Z0-9-_]+$")
1✔
104
# Pattern for a valid SubnetId
105
SUBNET_ID_REGEX = re.compile(r"^subnet-[0-9a-z]*$")
1✔
106

107

108
URL_CHAR_SET = string.ascii_lowercase + string.digits
1✔
109
# Date format as returned by the lambda service
110
LAMBDA_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%f+0000"
1✔
111

112
# An unordered list of all Lambda CPU architectures supported by LocalStack.
113
ARCHITECTURES = [Architecture.arm64, Architecture.x86_64]
1✔
114

115
# ARN patterns returned in validation exception messages
116
ARN_NAME_PATTERN_GET = r"(arn:(aws[a-zA-Z-]*)?:lambda:)?((eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_\.]+)(:(\$LATEST(\.PUBLISHED)?|[a-zA-Z0-9-_]+))?"
1✔
117
ARN_NAME_PATTERN_CREATE = r"(arn:(aws[a-zA-Z-]*)?:lambda:)?((eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\$LATEST|[a-zA-Z0-9-_]+))?"
1✔
118

119
# AWS response when invalid ARNs are used in Tag operations.
120
TAGGABLE_RESOURCE_ARN_PATTERN = "arn:(aws[a-zA-Z-]*):lambda:(eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:\\d{12}:(function:[a-zA-Z0-9-_]+(:(\\$LATEST|[a-zA-Z0-9-_]+))?|layer:([a-zA-Z0-9-_]+)|code-signing-config:csc-[a-z0-9]{17}|event-source-mapping:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|capacity-provider:[a-zA-Z0-9-_]+)"
1✔
121

122

123
def validate_function_name(function_name_or_arn: str, operation_type: str):
1✔
124
    function_name, *_ = function_locators_from_arn(function_name_or_arn)
1✔
125
    arn_name_pattern = ARN_NAME_PATTERN_CREATE
1✔
126
    max_length = 170
1✔
127

128
    if operation_type == "GetFunction" or operation_type == "Invoke":
1✔
129
        arn_name_pattern = ARN_NAME_PATTERN_GET
1✔
130
    elif operation_type == "CreateFunction":
1✔
131
        # https://docs.aws.amazon.com/lambda/latest/api/API_CreateFunction.html#lambda-CreateFunction-request-FunctionName
132
        if function_name == function_name_or_arn:  # only a function name
1✔
133
            max_length = 64
1✔
134
        else:  # full or partial ARN
135
            max_length = 140
1✔
136
    elif operation_type == "DeleteFunction":
1✔
137
        max_length = 140
1✔
138
        arn_name_pattern = ARN_NAME_PATTERN_GET
1✔
139

140
    validations = []
1✔
141
    if not AWS_FUNCTION_NAME_REGEX.match(function_name_or_arn) or not function_name:
1✔
142
        constraint = f"Member must satisfy regular expression pattern: {arn_name_pattern}"
1✔
143
        validation_msg = f"Value '{function_name_or_arn}' at 'functionName' failed to satisfy constraint: {constraint}"
1✔
144
        validations.append(validation_msg)
1✔
145
        if not operation_type == "CreateFunction":
1✔
146
            # Immediately raises rather than summarizing all validations, except for CreateFunction
147
            return validations
1✔
148

149
    if len(function_name_or_arn) > max_length:
1✔
150
        constraint = f"Member must have length less than or equal to {max_length}"
1✔
151
        validation_msg = f"Value '{function_name_or_arn}' at 'functionName' failed to satisfy constraint: {constraint}"
1✔
152
        validations.append(validation_msg)
1✔
153

154
    return validations
1✔
155

156

157
def validate_qualifier(qualifier: str):
1✔
158
    validations = []
1✔
159

160
    if len(qualifier) > 128:
1✔
161
        constraint = "Member must have length less than or equal to 128"
1✔
162
        validation_msg = (
1✔
163
            f"Value '{qualifier}' at 'qualifier' failed to satisfy constraint: {constraint}"
164
        )
165
        validations.append(validation_msg)
1✔
166

167
    if not QUALIFIER_REGEX.match(qualifier):
1✔
168
        constraint = "Member must satisfy regular expression pattern: \\$(LATEST(\\.PUBLISHED)?)|[a-zA-Z0-9-_$]+"
1✔
169
        validation_msg = (
1✔
170
            f"Value '{qualifier}' at 'qualifier' failed to satisfy constraint: {constraint}"
171
        )
172
        validations.append(validation_msg)
1✔
173

174
    return validations
1✔
175

176

177
def construct_validation_exception_message(validation_errors):
1✔
178
    if validation_errors:
1✔
179
        return f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {'; '.join(validation_errors)}"
1✔
180

181
    return None
×
182

183

184
def map_function_url_config(model: "FunctionUrlConfig") -> api_spec.FunctionUrlConfig:
1✔
185
    return api_spec.FunctionUrlConfig(
1✔
186
        FunctionUrl=model.url,
187
        FunctionArn=model.function_arn,
188
        CreationTime=model.creation_time,
189
        LastModifiedTime=model.last_modified_time,
190
        Cors=model.cors,
191
        AuthType=model.auth_type,
192
        InvokeMode=model.invoke_mode,
193
    )
194

195

196
def map_csc(model: "CodeSigningConfig") -> api_spec.CodeSigningConfig:
1✔
197
    return api_spec.CodeSigningConfig(
1✔
198
        CodeSigningConfigId=model.csc_id,
199
        CodeSigningConfigArn=model.arn,
200
        Description=model.description,
201
        AllowedPublishers=model.allowed_publishers,
202
        CodeSigningPolicies=model.policies,
203
        LastModified=model.last_modified,
204
    )
205

206

207
def get_config_for_url(store: "LambdaStore", url_id: str) -> "FunctionUrlConfig | None":
1✔
208
    """
209
    Get a config object when resolving a URL
210

211
    :param store: Lambda Store
212
    :param url_id: unique url ID (prefixed domain when calling the function)
213
    :return: FunctionUrlConfig that belongs to this ID
214

215
    # TODO: quite inefficient: optimize
216
    """
217
    for fn_name, fn in store.functions.items():
×
218
        for qualifier, fn_url_config in fn.function_url_configs.items():
×
219
            if fn_url_config.url_id == url_id:
×
220
                return fn_url_config
×
221
    return None
×
222

223

224
def is_qualifier_expression(qualifier: str) -> bool:
1✔
225
    """Checks if a given qualifier is a syntactically accepted expression.
226
    It is not necessarily a valid alias or version.
227

228
    :param qualifier: Qualifier to check
229
    :return True if syntactically accepted qualifier expression, false otherwise
230
    """
231
    return bool(QUALIFIER_REGEX.match(qualifier))
1✔
232

233

234
def qualifier_is_version(qualifier: str) -> bool:
1✔
235
    """
236
    Checks if a given qualifier represents a version
237

238
    :param qualifier: Qualifier to check
239
    :return: True if it matches a version, false otherwise
240
    """
241
    return bool(VERSION_REGEX.match(qualifier))
1✔
242

243

244
def qualifier_is_alias(qualifier: str) -> bool:
1✔
245
    """
246
    Checks if a given qualifier represents an alias
247

248
    :param qualifier: Qualifier to check
249
    :return: True if it matches an alias, false otherwise
250
    """
251
    return bool(ALIAS_REGEX.match(qualifier))
1✔
252

253

254
def get_function_name(function_arn_or_name: str, context: RequestContext) -> str:
1✔
255
    """
256
    Return function name from a given arn.
257
    Will check if the context region matches the arn region in the arn, if an arn is provided.
258

259
    :param function_arn_or_name: Function arn or only name
260
    :return: function name
261
    """
262
    name, _ = get_name_and_qualifier(function_arn_or_name, qualifier=None, context=context)
1✔
263
    return name
1✔
264

265

266
def function_locators_from_arn(arn: str) -> tuple[str | None, str | None, str | None, str | None]:
1✔
267
    """
268
    Takes a full or partial arn, or a name
269

270
    :param arn: Given arn (or name)
271
    :return: tuple with (name, qualifier, account, region). Qualifier and region are none if missing
272
    """
273

274
    if matched := FUNCTION_NAME_REGEX.match(arn):
1✔
275
        name = matched.group("name")
1✔
276
        qualifier = matched.group("qualifier")
1✔
277
        account = matched.group("account")
1✔
278
        region = matched.group("region")
1✔
279
        return (name, qualifier, account, region)
1✔
280

281
    return None, None, None, None
1✔
282

283

284
def get_account_and_region(function_arn_or_name: str, context: RequestContext) -> tuple[str, str]:
1✔
285
    """
286
    Takes a full ARN, partial ARN or a name. Returns account ID and region from ARN if available, else
287
    falls back to context account ID and region.
288

289
    Lambda allows cross-account access. This function should be used to resolve the correct Store based on the ARN.
290
    """
291
    _, _, account_id, region = function_locators_from_arn(function_arn_or_name)
1✔
292
    return account_id or context.account_id, region or context.region
1✔
293

294

295
def get_name_and_qualifier(
1✔
296
    function_arn_or_name: str, qualifier: str | None, context: RequestContext
297
) -> tuple[str, str | None]:
298
    """
299
    Takes a full or partial arn, or a name and a qualifier.
300

301
    :param function_arn_or_name: Given arn (or name)
302
    :param qualifier: A qualifier for the function (or None)
303
    :param context: Request context
304
    :return: tuple with (name, qualifier). Qualifier is none if missing
305
    :raises: `ResourceNotFoundException` when the context's region differs from the ARN's region
306
    :raises: `AccessDeniedException` when the context's account ID differs from the ARN's account ID
307
    :raises: `ValidationExcpetion` when a function ARN/name or qualifier fails validation checks
308
    :raises: `InvalidParameterValueException` when a qualified arn is provided and the qualifier does not match (but is given)
309
    """
310
    function_name, arn_qualifier, account, region = function_locators_from_arn(function_arn_or_name)
1✔
311
    operation_type = context.operation.name
1✔
312

313
    if operation_type not in _supported_resource_based_operations:
1✔
314
        if account and account != context.account_id:
1✔
315
            raise AccessDeniedException(None)
1✔
316

317
    # TODO: should this only run if operation type is unsupported?
318
    if region and region != context.region:
1✔
319
        raise ResourceNotFoundException(
1✔
320
            f"Functions from '{region}' are not reachable in this region ('{context.region}')",
321
            Type="User",
322
        )
323

324
    validation_errors = []
1✔
325
    if function_arn_or_name:
1✔
326
        validation_errors.extend(validate_function_name(function_arn_or_name, operation_type))
1✔
327

328
    if qualifier:
1✔
329
        validation_errors.extend(validate_qualifier(qualifier))
1✔
330

331
    is_only_function_name = function_arn_or_name == function_name
1✔
332
    if validation_errors:
1✔
333
        message = construct_validation_exception_message(validation_errors)
1✔
334
        # Edge-case where the error type is not ValidationException
335
        if (
1✔
336
            operation_type == "CreateFunction"
337
            and is_only_function_name
338
            and arn_qualifier is None
339
            and region is None
340
        ):  # just name OR partial
341
            raise InvalidParameterValueException(message=message, Type="User")
1✔
342
        raise CommonServiceException(message=message, code="ValidationException")
1✔
343

344
    if qualifier and arn_qualifier and arn_qualifier != qualifier:
1✔
345
        raise InvalidParameterValueException(
1✔
346
            "The derived qualifier from the function name does not match the specified qualifier.",
347
            Type="User",
348
        )
349

350
    qualifier = qualifier or arn_qualifier
1✔
351
    return function_name, qualifier
1✔
352

353

354
def build_statement(
1✔
355
    partition: str,
356
    resource_arn: str,
357
    statement_id: str,
358
    action: str,
359
    principal: str,
360
    source_arn: str | None = None,
361
    source_account: str | None = None,
362
    principal_org_id: str | None = None,
363
    event_source_token: str | None = None,
364
    auth_type: FunctionUrlAuthType | None = None,
365
) -> dict[str, Any]:
366
    statement = {
1✔
367
        "Sid": statement_id,
368
        "Effect": "Allow",
369
        "Action": action,
370
        "Resource": resource_arn,
371
    }
372

373
    # See AWS service principals for comprehensive docs:
374
    # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html
375
    # TODO: validate against actual list of IAM-supported AWS services (e.g., lambda.amazonaws.com)
376
    if principal.endswith(".amazonaws.com"):
1✔
377
        statement["Principal"] = {"Service": principal}
1✔
378
    elif is_aws_account(principal):
1✔
379
        statement["Principal"] = {"AWS": f"arn:{partition}:iam::{principal}:root"}
1✔
380
    # TODO: potentially validate against IAM?
381
    elif re.match(f"{ARN_PARTITION_REGEX}:iam:", principal):
1✔
382
        statement["Principal"] = {"AWS": principal}
1✔
383
    elif principal == "*":
1✔
384
        statement["Principal"] = principal
1✔
385
    # TODO: unclear whether above matching is complete?
386
    else:
387
        raise InvalidParameterValueException(
1✔
388
            "The provided principal was invalid. Please check the principal and try again.",
389
            Type="User",
390
        )
391

392
    condition = {}
1✔
393
    if auth_type:
1✔
394
        update = {"StringEquals": {"lambda:FunctionUrlAuthType": auth_type}}
1✔
395
        condition = merge_recursive(condition, update)
1✔
396

397
    if principal_org_id:
1✔
398
        update = {"StringEquals": {"aws:PrincipalOrgID": principal_org_id}}
1✔
399
        condition = merge_recursive(condition, update)
1✔
400

401
    if source_account:
1✔
402
        update = {"StringEquals": {"AWS:SourceAccount": source_account}}
1✔
403
        condition = merge_recursive(condition, update)
1✔
404

405
    if event_source_token:
1✔
406
        update = {"StringEquals": {"lambda:EventSourceToken": event_source_token}}
1✔
407
        condition = merge_recursive(condition, update)
1✔
408

409
    if source_arn:
1✔
410
        update = {"ArnLike": {"AWS:SourceArn": source_arn}}
1✔
411
        condition = merge_recursive(condition, update)
1✔
412

413
    if condition:
1✔
414
        statement["Condition"] = condition
1✔
415

416
    return statement
1✔
417

418

419
def generate_random_url_id() -> str:
1✔
420
    """
421
    32 characters [0-9a-z] url ID
422
    """
423

424
    return "".join(random.choices(URL_CHAR_SET, k=32))
1✔
425

426

427
def unqualified_lambda_arn(function_name: str, account: str, region: str):
1✔
428
    """
429
    Generate an unqualified lambda arn
430

431
    :param function_name: Function name (not an arn!)
432
    :param account: Account ID
433
    :param region: Region
434
    :return: Unqualified lambda arn
435
    """
436
    return f"arn:{get_partition(region)}:lambda:{region}:{account}:function:{function_name}"
1✔
437

438

439
def qualified_lambda_arn(
1✔
440
    function_name: str, qualifier: str | None, account: str, region: str
441
) -> str:
442
    """
443
    Generate a qualified lambda arn
444

445
    :param function_name: Function name (not an arn!)
446
    :param qualifier: qualifier (will be set to $LATEST if not present)
447
    :param account: Account ID
448
    :param region: Region
449
    :return: Qualified lambda arn
450
    """
451
    qualifier = qualifier or "$LATEST"
1✔
452
    return f"{unqualified_lambda_arn(function_name=function_name, account=account, region=region)}:{qualifier}"
1✔
453

454

455
def lambda_arn(function_name: str, qualifier: str | None, account: str, region: str) -> str:
1✔
456
    """
457
    Return the lambda arn for the given parameters, with a qualifier if supplied, without otherwise
458

459
    :param function_name: Function name
460
    :param qualifier: Qualifier. May be left out, then the returning arn does not have one either
461
    :param account: Account ID
462
    :param region: Region of the Lambda
463
    :return: Lambda Arn with or without qualifier
464
    """
465
    if qualifier:
1✔
466
        return qualified_lambda_arn(
1✔
467
            function_name=function_name, qualifier=qualifier, account=account, region=region
468
        )
469
    else:
470
        return unqualified_lambda_arn(function_name=function_name, account=account, region=region)
1✔
471

472

473
def is_role_arn(role_arn: str) -> bool:
1✔
474
    """
475
    Returns true if the provided string is a role arn, false otherwise
476

477
    :param role_arn: Potential role arn
478
    :return: Boolean indicating if input is a role arn
479
    """
480
    return bool(ROLE_REGEX.match(role_arn))
1✔
481

482

483
def is_aws_account(aws_account: str) -> bool:
1✔
484
    """
485
    Returns true if the provided string is an AWS account, false otherwise
486

487
    :param role_arn: Potential AWS account
488
    :return: Boolean indicating if input is an AWS account
489
    """
490
    return bool(AWS_ACCOUNT_REGEX.match(aws_account))
1✔
491

492

493
def format_lambda_date(date_to_format: datetime.datetime) -> str:
1✔
494
    """Format a given datetime to a string generated with the lambda date format"""
495
    return date_to_format.strftime(LAMBDA_DATE_FORMAT)
1✔
496

497

498
def generate_lambda_date() -> str:
1✔
499
    """Get the current date as string generated with the lambda date format"""
500
    return format_lambda_date(datetime.datetime.now())
1✔
501

502

503
def map_update_status_config(version: "FunctionVersion") -> dict[str, str]:
1✔
504
    """Map version model to dict output"""
505
    result = {}
1✔
506
    if version.config.last_update:
1✔
507
        if version.config.last_update.status:
1✔
508
            result["LastUpdateStatus"] = version.config.last_update.status
1✔
509
        if version.config.last_update.code:
1✔
510
            result["LastUpdateStatusReasonCode"] = version.config.last_update.code
1✔
511
        if version.config.last_update.reason:
1✔
512
            result["LastUpdateStatusReason"] = version.config.last_update.reason
1✔
513
    return result
1✔
514

515

516
def map_state_config(version: "FunctionVersion") -> dict[str, str]:
1✔
517
    """Map version state to dict output"""
518
    result = {}
1✔
519
    if version_state := version.config.state:
1✔
520
        if version_state.state:
1✔
521
            result["State"] = version_state.state
1✔
522
        if version_state.reason:
1✔
523
            result["StateReason"] = version_state.reason
1✔
524
        if version_state.code:
1✔
525
            result["StateReasonCode"] = version_state.code
1✔
526
    return result
1✔
527

528

529
def map_config_out(
1✔
530
    version: "FunctionVersion",
531
    return_qualified_arn: bool = False,
532
    return_update_status: bool = True,
533
    alias_name: str | None = None,
534
) -> FunctionConfiguration:
535
    """map function version to function configuration"""
536

537
    # handle optional entries that shouldn't be rendered at all if not present
538
    optional_kwargs = {}
1✔
539
    if return_update_status:
1✔
540
        optional_kwargs.update(map_update_status_config(version))
1✔
541
    optional_kwargs.update(map_state_config(version))
1✔
542

543
    if version.config.architectures:
1✔
544
        optional_kwargs["Architectures"] = version.config.architectures
1✔
545

546
    if version.config.dead_letter_arn:
1✔
547
        optional_kwargs["DeadLetterConfig"] = DeadLetterConfig(
1✔
548
            TargetArn=version.config.dead_letter_arn
549
        )
550

551
    if version.config.vpc_config:
1✔
552
        optional_kwargs["VpcConfig"] = VpcConfigResponse(
1✔
553
            VpcId=version.config.vpc_config.vpc_id,
554
            SubnetIds=version.config.vpc_config.subnet_ids,
555
            SecurityGroupIds=version.config.vpc_config.security_group_ids,
556
        )
557

558
    if version.config.environment is not None:
1✔
559
        optional_kwargs["Environment"] = EnvironmentResponse(
1✔
560
            Variables=version.config.environment
561
        )  # TODO: Errors key?
562

563
    if version.config.layers:
1✔
564
        optional_kwargs["Layers"] = [
1✔
565
            {"Arn": layer.layer_version_arn, "CodeSize": layer.code.code_size}
566
            for layer in version.config.layers
567
        ]
568
    if version.config.image_config:
1✔
569
        image_config = ImageConfig()
1✔
570
        if version.config.image_config.command:
1✔
571
            image_config["Command"] = version.config.image_config.command
1✔
572
        if version.config.image_config.entrypoint:
1✔
573
            image_config["EntryPoint"] = version.config.image_config.entrypoint
1✔
574
        if version.config.image_config.working_directory:
1✔
575
            image_config["WorkingDirectory"] = version.config.image_config.working_directory
1✔
576
        if image_config:
1✔
577
            optional_kwargs["ImageConfigResponse"] = ImageConfigResponse(ImageConfig=image_config)
1✔
578
    if version.config.code:
1✔
579
        optional_kwargs["CodeSize"] = version.config.code.code_size
1✔
580
        optional_kwargs["CodeSha256"] = version.config.code.code_sha256
1✔
581
    elif version.config.image:
1✔
582
        optional_kwargs["CodeSize"] = 0
1✔
583
        optional_kwargs["CodeSha256"] = version.config.image.code_sha256
1✔
584

585
    if version.config.CapacityProviderConfig:
1✔
NEW
586
        optional_kwargs["CapacityProviderConfig"] = version.config.CapacityProviderConfig
×
NEW
587
        data = json.dumps(version.config.CapacityProviderConfig, sort_keys=True).encode("utf-8")
×
NEW
588
        config_sha_256 = hashlib.sha256(data).hexdigest()
×
NEW
589
        optional_kwargs["ConfigSha256"] = config_sha_256
×
590

591
    # output for an alias qualifier is completely the same except for the returned ARN
592
    if alias_name:
1✔
593
        function_arn = f"{':'.join(version.id.qualified_arn().split(':')[:-1])}:{alias_name}"
1✔
594
    else:
595
        function_arn = (
1✔
596
            version.id.qualified_arn() if return_qualified_arn else version.id.unqualified_arn()
597
        )
598

599
    func_conf = FunctionConfiguration(
1✔
600
        RevisionId=version.config.revision_id,
601
        FunctionName=version.id.function_name,
602
        FunctionArn=function_arn,
603
        LastModified=version.config.last_modified,
604
        Version=version.id.qualifier,
605
        Description=version.config.description,
606
        Role=version.config.role,
607
        Timeout=version.config.timeout,
608
        Runtime=version.config.runtime,
609
        Handler=version.config.handler,
610
        MemorySize=version.config.memory_size,
611
        PackageType=version.config.package_type,
612
        TracingConfig=TracingConfig(Mode=version.config.tracing_config_mode),
613
        EphemeralStorage=EphemeralStorage(Size=version.config.ephemeral_storage.size),
614
        SnapStart=version.config.snap_start,
615
        RuntimeVersionConfig=version.config.runtime_version_config,
616
        LoggingConfig=version.config.logging_config,
617
        **optional_kwargs,
618
    )
619
    return func_conf
1✔
620

621

622
def map_to_list_response(config: FunctionConfiguration) -> FunctionConfiguration:
1✔
623
    """remove values not usually presented in list operations from function config output"""
624
    shallow_copy = config.copy()
1✔
625
    for k in [
1✔
626
        "State",
627
        "StateReason",
628
        "StateReasonCode",
629
        "LastUpdateStatus",
630
        "LastUpdateStatusReason",
631
        "LastUpdateStatusReasonCode",
632
        "RuntimeVersionConfig",
633
    ]:
634
        shallow_copy.pop(k, None)
1✔
635
    return shallow_copy
1✔
636

637

638
def map_alias_out(alias: "VersionAlias", function: "Function") -> AliasConfiguration:
1✔
639
    """map alias model to alias configuration output"""
640
    alias_arn = f"{function.latest().id.unqualified_arn()}:{alias.name}"
1✔
641
    optional_kwargs = {}
1✔
642
    if alias.routing_configuration:
1✔
643
        optional_kwargs |= {
1✔
644
            "RoutingConfig": {
645
                "AdditionalVersionWeights": alias.routing_configuration.version_weights
646
            }
647
        }
648
    return AliasConfiguration(
1✔
649
        AliasArn=alias_arn,
650
        Description=alias.description,
651
        FunctionVersion=alias.function_version,
652
        Name=alias.name,
653
        RevisionId=alias.revision_id,
654
        **optional_kwargs,
655
    )
656

657

658
def validate_and_set_batch_size(service: str, batch_size: int | None = None) -> int:
1✔
659
    min_batch_size = 1
1✔
660

661
    BATCH_SIZE_RANGES = {
1✔
662
        "kafka": (100, 10_000),
663
        "kinesis": (100, 10_000),
664
        "dynamodb": (100, 10_000),
665
        "sqs-fifo": (10, 10),
666
        "sqs": (10, 10_000),
667
        "mq": (100, 10_000),
668
    }
669
    svc_range = BATCH_SIZE_RANGES.get(service)
1✔
670

671
    if svc_range:
1✔
672
        default_batch_size, max_batch_size = svc_range
1✔
673

674
        if batch_size is None:
1✔
675
            batch_size = default_batch_size
1✔
676

677
        if batch_size < min_batch_size or batch_size > max_batch_size:
1✔
678
            raise InvalidParameterValueException("out of bounds todo", Type="User")  # TODO: test
×
679

680
    return batch_size
1✔
681

682

683
def map_layer_out(layer_version: "LayerVersion") -> PublishLayerVersionResponse:
1✔
684
    return PublishLayerVersionResponse(
1✔
685
        Content=LayerVersionContentOutput(
686
            Location=layer_version.code.generate_presigned_url(
687
                endpoint_url=config.external_service_url()
688
            ),
689
            CodeSha256=layer_version.code.code_sha256,
690
            CodeSize=layer_version.code.code_size,
691
            # SigningProfileVersionArn="", # same as in function configuration
692
            # SigningJobArn="" # same as in function configuration
693
        ),
694
        LicenseInfo=layer_version.license_info,
695
        Description=layer_version.description,
696
        CompatibleArchitectures=layer_version.compatible_architectures,
697
        CompatibleRuntimes=layer_version.compatible_runtimes,
698
        CreatedDate=layer_version.created,
699
        LayerArn=layer_version.layer_arn,
700
        LayerVersionArn=layer_version.layer_version_arn,
701
        Version=layer_version.version,
702
    )
703

704

705
def layer_arn(layer_name: str, account: str, region: str):
1✔
706
    return f"arn:{get_partition(region)}:lambda:{region}:{account}:layer:{layer_name}"
1✔
707

708

709
def layer_version_arn(layer_name: str, account: str, region: str, version: str):
1✔
710
    return f"arn:{get_partition(region)}:lambda:{region}:{account}:layer:{layer_name}:{version}"
1✔
711

712

713
def parse_layer_arn(layer_version_arn: str) -> tuple[str, str, str, str]:
1✔
714
    return LAYER_VERSION_ARN_PATTERN.match(layer_version_arn).group(
1✔
715
        "region_name", "account_id", "layer_name", "layer_version"
716
    )
717

718

719
def validate_layer_runtime(compatible_runtime: str) -> str | None:
1✔
720
    if compatible_runtime is not None and compatible_runtime not in ALL_RUNTIMES:
1✔
721
        return f"Value '{compatible_runtime}' at 'compatibleRuntime' failed to satisfy constraint: Member must satisfy enum value set: {VALID_LAYER_RUNTIMES}"
1✔
722
    return None
1✔
723

724

725
def validate_layer_architecture(compatible_architecture: str) -> str | None:
1✔
726
    if compatible_architecture is not None and compatible_architecture not in ARCHITECTURES:
1✔
727
        return f"Value '{compatible_architecture}' at 'compatibleArchitecture' failed to satisfy constraint: Member must satisfy enum value set: [x86_64, arm64]"
1✔
728
    return None
1✔
729

730

731
def validate_layer_runtimes_and_architectures(
1✔
732
    compatible_runtimes: list[str], compatible_architectures: list[str]
733
):
734
    validations = []
1✔
735

736
    if compatible_runtimes and set(compatible_runtimes).difference(ALL_RUNTIMES):
1✔
737
        constraint = f"Member must satisfy enum value set: {VALID_RUNTIMES}"
1✔
738
        validation_msg = f"Value '[{', '.join(list(compatible_runtimes))}]' at 'compatibleRuntimes' failed to satisfy constraint: {constraint}"
1✔
739
        validations.append(validation_msg)
1✔
740

741
    if compatible_architectures and set(compatible_architectures).difference(ARCHITECTURES):
1✔
742
        constraint = (
1✔
743
            "[Member must satisfy enum value set: [x86_64, arm64], Member must not be null]"
744
        )
745
        validation_msg = f"Value '[{', '.join(list(compatible_architectures))}]' at 'compatibleArchitectures' failed to satisfy constraint: Member must satisfy constraint: {constraint}"
1✔
746
        validations.append(validation_msg)
1✔
747

748
    return validations
1✔
749

750

751
def is_layer_arn(layer_name: str) -> bool:
1✔
752
    return LAYER_VERSION_ARN_PATTERN.match(layer_name) is not None
1✔
753

754

755
# See Lambda API actions that support resource-based IAM policies
756
# https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html#permissions-resource-api
757
_supported_resource_based_operations = {
1✔
758
    "CreateAlias",
759
    "DeleteAlias",
760
    "DeleteFunction",
761
    "DeleteFunctionConcurrency",
762
    "DeleteFunctionEventInvokeConfig",
763
    "DeleteProvisionedConcurrencyConfig",
764
    "GetAlias",
765
    "GetFunction",
766
    "GetFunctionConcurrency",
767
    "GetFunctionConfiguration",
768
    "GetFunctionEventInvokeConfig",
769
    "GetPolicy",
770
    "GetProvisionedConcurrencyConfig",
771
    "Invoke",
772
    "ListAliases",
773
    "ListFunctionEventInvokeConfigs",
774
    "ListProvisionedConcurrencyConfigs",
775
    "ListTags",
776
    "ListVersionsByFunction",
777
    "PublishVersion",
778
    "PutFunctionConcurrency",
779
    "PutFunctionEventInvokeConfig",
780
    "PutProvisionedConcurrencyConfig",
781
    "TagResource",
782
    "UntagResource",
783
    "UpdateAlias",
784
    "UpdateFunctionCode",
785
    "UpdateFunctionEventInvokeConfig",
786
}
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