# -*- coding: utf-8 -*-
import typing as T
import enum
from datetime import datetime, timezone, timedelta
import botocore.exceptions
from func_args.api import OPT, remove_optional
from .model import (
LayerVersion,
)
from .client import (
list_layer_versions,
)
if T.TYPE_CHECKING: # pragma: no cover
from mypy_boto3_lambda.client import LambdaClient
from mypy_boto3_lambda.type_defs import (
AddLayerVersionPermissionResponseTypeDef,
)
[docs]
def get_latest_layer_version(
lambda_client: "LambdaClient",
layer_name: str,
compatible_runtime: str = OPT,
compatible_architecture: str = OPT,
_sort_descending: bool = False,
) -> LayerVersion | None:
"""
Call the AWS Lambda Layer API to retrieve the latest deployed layer version.
If it returns ``None``, it indicates that no layer has been deployed yet.
Example: if layer has version 1, 2, 3, then this function return 3.
If there's no layer version created yet, then this function returns None.
Reference:
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda.html#Lambda.Client.list_layer_versions
"""
if _sort_descending:
max_items = 10
else:
max_items = 1
layer_versions = list_layer_versions(
lambda_client=lambda_client,
layer_name=layer_name,
compatible_runtime=compatible_runtime,
compatible_architecture=compatible_architecture,
max_items=max_items,
_sort_descending=_sort_descending,
).all()
if len(layer_versions) == 0:
return None
else:
return layer_versions[0]
[docs]
def cleanup_old_layer_versions(
lambda_client: "LambdaClient",
layer_name: str,
keep_last_n_versions: int = 5,
keep_versions_newer_than_seconds: int = 90 * 24 * 60 * 60,
real_run: bool = False,
_sort_descending: bool = False,
) -> list[int]:
"""
Delete old Lambda layer versions based on retention policy.
Keeps layer versions if they meet ANY of these conditions:
- Among the last N versions (most recent)
- Created within the last N seconds
:param lambda_client: AWS Lambda client
:param layer_name: Name of the Lambda layer
:param keep_last_n_versions: Number of most recent versions to keep
:param keep_versions_newer_than_seconds: Keep versions newer than this many seconds
:param real_run: If True, actually delete versions. If False, only return what would be deleted
:returns: List of version numbers that were deleted (or would be deleted in simulation mode)
Ref:
- `delete_layer_version <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda/client/delete_layer_version.html>`_
"""
# Get all layer versions
all_versions = list_layer_versions(
lambda_client=lambda_client,
layer_name=layer_name,
max_items=9999,
_sort_descending=True,
).all()
# Only exam versions beyond the last N to keep
other_versions = all_versions[keep_last_n_versions:]
if len(other_versions) == 0:
return []
# Calculate cutoff date
delta = timedelta(seconds=keep_versions_newer_than_seconds)
cutoff_date = datetime.now(timezone.utc) - delta
versions_to_delete = []
for version in other_versions:
# Keep if it's newer than cutoff date
if version.created_datetime > cutoff_date: # pragma: no cover
continue
# This version should be deleted
versions_to_delete.append(version.version)
# Delete the versions (if real_run is True)
deleted_versions = []
for version_number in versions_to_delete:
deleted_versions.append(version_number)
if real_run:
try:
lambda_client.delete_layer_version(
LayerName=layer_name,
VersionNumber=version_number,
)
except botocore.exceptions.ClientError: # pragma: no cover
# Continue with other versions even if one fails
pass
return deleted_versions
[docs]
class LambdaPermissionActionEnum(str, enum.Enum):
"""
Enum for different Lambda layer permission actions.
See: https://docs.aws.amazon.com/lambda/latest/dg/permissions-layer-cross-account.html
"""
get_layer_version = "lambda:GetLayerVersion"
list_layer_versions = "lambda:ListLayerVersions"
# Map action to a more human friendly name for statement id naming convention
action_to_name_mapper: T.Dict[str, str] = {
LambdaPermissionActionEnum.get_layer_version.value: "GetLayerVersion",
LambdaPermissionActionEnum.list_layer_versions.value: "ListLayerVersions",
}
[docs]
class LayerPrincipalTypeEnum(str, enum.Enum):
"""
Enum for different types of layer principals.
Based on this AWS doc https://docs.aws.amazon.com/lambda/latest/dg/permissions-layer-cross-account.html
There are only three cross account Lambda layer permission patterns
The grant_aws_account_or_aws_organization_lambda_layer_version_access
and revoke_aws_account_or_aws_organization_lambda_layer_version_access
recipes only support these three patterns.
"""
public = "public"
aws_account = "aws_account"
aws_organization = "aws_organization"
[docs]
def identify_principal_type(principal: str) -> LayerPrincipalTypeEnum:
"""
Identify the type of principal based on its format.
:param principal: The principal string to identify:
- "*" for public access
- "123456789012" for specific AWS account (12-digit account ID)
- "o-example123456" for AWS organization ID
:returns: The identified LayerPrincipalTypeEnum
"""
if principal == "*":
return LayerPrincipalTypeEnum.public
elif principal.isdigit() and len(principal) == 12:
return LayerPrincipalTypeEnum.aws_account
elif principal.startswith("o-"):
return LayerPrincipalTypeEnum.aws_organization
else: # pragma: no cover
raise ValueError(f"Invalid principal format: {principal}")
[docs]
def get_layer_permission_statement_id(
action: str,
principal: str,
) -> str:
"""
Encode the statement ID for Lambda layer permission based on action and principal.
"""
name = action_to_name_mapper[action]
return f"allow-{principal}-{name}"
[docs]
def grant_aws_account_or_aws_organization_lambda_layer_version_access(
lambda_client: "LambdaClient",
layer_name: str,
version_number: int,
principal: str,
):
"""
Grant other AWS accounts Lambda layer access to a specific layer version.
Idempotent version of the AWS Lambda add_layer_version_permission API that
automatically handles statement ID generation and manages conflicts by
allowing existing permissions to remain.
Grants both GetLayerVersion and ListLayerVersions permissions to the specified
principal (AWS account, AWS organization, or public access).
:param lambda_client: AWS Lambda client for the account that owns the layer
:param layer_name: Name of the Lambda layer
:param version_number: Version number of the layer to grant access to
:param principal: Principal to grant access to:
- "*" for public access
- "123456789012" for specific AWS account (12-digit account ID)
- "o-example123456" for AWS organization ID
Ref:
- `add_layer_version_permission <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda/client/add_layer_version_permission.html>`_
"""
layer_principal_type = identify_principal_type(principal)
if layer_principal_type == LayerPrincipalTypeEnum.public: # pragma: no cover
kwargs = {"Principal": principal}
elif layer_principal_type == LayerPrincipalTypeEnum.aws_account: # pragma: no cover
kwargs = {"Principal": principal}
elif (
layer_principal_type == LayerPrincipalTypeEnum.aws_organization
): # pragma: no cover
kwargs = {
"Principal": "*",
"OrganizationId": principal,
}
else: # pragma: no cover
raise ValueError(f"Unsupported principal type: {principal}")
def add_layer_version_permission(action: str):
statement_id = get_layer_permission_statement_id(
action=action,
principal=principal,
)
try:
lambda_client.add_layer_version_permission(
LayerName=layer_name,
VersionNumber=version_number,
StatementId=statement_id,
Action=action,
**kwargs,
)
except botocore.exceptions.ClientError as e: # pragma: no cover
if e.response["Error"]["Code"] == "ResourceConflictException":
pass
else:
raise
# Currently, AWS Lambda only supports granting GetLayerVersion permission
add_layer_version_permission(LambdaPermissionActionEnum.get_layer_version.value)
[docs]
def revoke_aws_account_or_aws_organization_lambda_layer_version_access(
lambda_client: "LambdaClient",
layer_name: str,
version_number: int,
principal: str,
):
"""
Revoke AWS accounts Lambda layer access from a specific layer version.
Idempotent version of the AWS Lambda remove_layer_version_permission API that
automatically handles statement ID generation and gracefully handles cases
where permissions don't exist.
Removes both GetLayerVersion and ListLayerVersions permissions from the
specified principal (AWS account, AWS organization, or public access).
:param lambda_client: AWS Lambda client for the account that owns the layer
:param layer_name: Name of the Lambda layer
:param version_number: Version number of the layer to revoke access from
:param principal: Principal to revoke access from:
- "*" for public access
- "123456789012" for specific AWS account (12-digit account ID)
- "o-example123456" for AWS organization ID
Ref:
- `remove_layer_version_permission <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda/client/remove_layer_version_permission.html>`_
"""
identify_principal_type(principal)
def remove_layer_version_permission(action: str):
statement_id = get_layer_permission_statement_id(
action=action,
principal=principal,
)
try:
lambda_client.remove_layer_version_permission(
LayerName=layer_name,
VersionNumber=version_number,
StatementId=statement_id,
)
except botocore.exceptions.ClientError as e: # pragma: no cover
if e.response["Error"]["Code"] == "ResourceNotFoundException":
pass
else:
raise
# Currently, AWS Lambda only supports granting GetLayerVersion permission
remove_layer_version_permission(LambdaPermissionActionEnum.get_layer_version.value)