Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion localstack-core/localstack/services/lambda_/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3606,7 +3606,12 @@ def get_layer_version_by_arn(
)

store = lambda_stores[account_id][region_name]
layer_version = store.layers.get(layer_name, {}).layer_versions.get(layer_version)
if not (layers := store.layers.get(layer_name)):
raise ResourceNotFoundException(
"The resource you requested does not exist.", Type="User"
)

layer_version = layers.layer_versions.get(layer_version)

if not layer_version:
raise ResourceNotFoundException(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
# LocalStack Resource Provider Scaffolding v2
from __future__ import annotations

import logging
from pathlib import Path
from typing import Optional, TypedDict

import localstack.services.cloudformation.provider_utils as util
from localstack.services.cloudformation.resource_provider import (
OperationStatus,
ProgressEvent,
Properties,
ResourceProvider,
ResourceRequest,
)
from localstack.services.lambda_.api_utils import parse_layer_arn
from localstack.utils.strings import short_uid

LOG = logging.getLogger(__name__)


class LambdaLayerVersionProperties(TypedDict):
Content: Optional[Content]
CompatibleArchitectures: Optional[list[str]]
CompatibleRuntimes: Optional[list[str]]
Description: Optional[str]
Id: Optional[str]
LayerName: Optional[str]
LayerVersionArn: Optional[str]
LicenseInfo: Optional[str]


Expand All @@ -45,7 +50,7 @@ def create(
Create a new resource.

Primary identifier fields:
- /properties/Id
- /properties/LayerVersionArn

Required properties:
- Content
Expand All @@ -59,17 +64,20 @@ def create(
- /properties/Content

Read-only properties:
- /properties/Id

- /properties/LayerVersionArn

IAM permissions required:
- lambda:PublishLayerVersion
- s3:GetObject
- s3:GetObjectVersion

"""
model = request.desired_state
lambda_client = request.aws_client_factory.lambda_
if not model.get("LayerName"):
model["LayerName"] = f"layer-{short_uid()}"
response = lambda_client.publish_layer_version(**model)
model["Id"] = response["LayerVersionArn"]
model["LayerVersionArn"] = response["LayerVersionArn"]

return ProgressEvent(
status=OperationStatus.SUCCESS,
Expand All @@ -84,9 +92,61 @@ def read(
"""
Fetch resource information


IAM permissions required:
- lambda:GetLayerVersion
"""
raise NotImplementedError
lambda_client = request.aws_client_factory.lambda_
layer_version_arn = request.desired_state.get("LayerVersionArn")

try:
_, _, layer_name, version = parse_layer_arn(layer_version_arn)
except AttributeError as e:
LOG.info(
"Invalid Arn: '%s', %s",
layer_version_arn,
e,
exc_info=LOG.isEnabledFor(logging.DEBUG),
)
return ProgressEvent(
status=OperationStatus.FAILED,
message="Caught unexpected syntax violation. Consider using ARN.fromString().",
error_code="InternalFailure",
)

if not version:
return ProgressEvent(
status=OperationStatus.FAILED,
message="Invalid request provided: Layer Version ARN contains invalid layer name or version",
error_code="InvalidRequest",
)

try:
response = lambda_client.get_layer_version_by_arn(Arn=layer_version_arn)
except lambda_client.exceptions.ResourceNotFoundException as e:
return ProgressEvent(
status=OperationStatus.FAILED,
message="The resource you requested does not exist. "
f"(Service: Lambda, Status Code: 404, Request ID: {e.response['ResponseMetadata']['RequestId']})",
error_code="NotFound",
)
layer = util.select_attributes(
response,
[
"CompatibleRuntimes",
"Description",
"LayerVersionArn",
"CompatibleArchitectures",
],
)
layer.setdefault("CompatibleRuntimes", [])
layer.setdefault("CompatibleArchitectures", [])
layer.setdefault("LayerName", layer_name)

return ProgressEvent(
status=OperationStatus.SUCCESS,
resource_model=layer,
custom_context=request.custom_context,
)

def delete(
self,
Expand All @@ -95,11 +155,13 @@ def delete(
"""
Delete a resource


IAM permissions required:
- lambda:GetLayerVersion
- lambda:DeleteLayerVersion
"""
model = request.desired_state
lambda_client = request.aws_client_factory.lambda_
version = int(model["Id"].split(":")[-1])
version = int(model["LayerVersionArn"].split(":")[-1])

lambda_client.delete_layer_version(LayerName=model["LayerName"], VersionNumber=version)
return ProgressEvent(
Expand All @@ -118,3 +180,32 @@ def update(

"""
raise NotImplementedError

def list(self, request: ResourceRequest[Properties]) -> ProgressEvent[Properties]:
"""
List resources

IAM permissions required:
- lambda:ListLayerVersions
"""

lambda_client = request.aws_client_factory.lambda_

lambda_layer = request.desired_state.get("LayerName")
if not lambda_layer:
return ProgressEvent(
status=OperationStatus.FAILED,
message="Layer Name cannot be empty",
error_code="InvalidRequest",
)

layer_versions = lambda_client.list_layer_versions(LayerName=lambda_layer)

return ProgressEvent(
status=OperationStatus.SUCCESS,
resource_models=[
LambdaLayerVersionProperties(LayerVersionArn=layer_version["LayerVersionArn"])
for layer_version in layer_versions["LayerVersions"]
],
custom_context=request.custom_context,
)
Original file line number Diff line number Diff line change
@@ -1,59 +1,71 @@
{
"typeName": "AWS::Lambda::LayerVersion",
"description": "Resource Type definition for AWS::Lambda::LayerVersion",
"additionalProperties": false,
"sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-lambda.git",
"definitions": {
"Content": {
"type": "object",
"additionalProperties": false,
"properties": {
"S3ObjectVersion": {
"description": "For versioned objects, the version of the layer archive object to use.",
"type": "string"
},
"S3Bucket": {
"description": "The Amazon S3 bucket of the layer archive.",
"type": "string"
},
"S3Key": {
"description": "The Amazon S3 key of the layer archive.",
"type": "string"
}
},
"required": [
"S3Bucket",
"S3Key"
]
}
},
"properties": {
"CompatibleRuntimes": {
"description": "A list of compatible function runtimes. Used for filtering with ListLayers and ListLayerVersions.",
"type": "array",
"insertionOrder": false,
"uniqueItems": false,
"items": {
"type": "string"
}
},
"LicenseInfo": {
"description": "The layer's software license.",
"type": "string"
},
"Description": {
"description": "The description of the version.",
"type": "string"
},
"LayerName": {
"description": "The name or Amazon Resource Name (ARN) of the layer.",
"type": "string"
},
"Content": {
"description": "The function layer archive.",
"$ref": "#/definitions/Content"
},
"Id": {
"LayerVersionArn": {
"type": "string"
},
"CompatibleArchitectures": {
"description": "A list of compatible instruction set architectures.",
"type": "array",
"insertionOrder": false,
"uniqueItems": false,
"items": {
"type": "string"
}
}
},
"definitions": {
"Content": {
"type": "object",
"additionalProperties": false,
"properties": {
"S3ObjectVersion": {
"type": "string"
},
"S3Bucket": {
"type": "string"
},
"S3Key": {
"type": "string"
}
},
"required": [
"S3Bucket",
"S3Key"
]
}
},
"additionalProperties": false,
"required": [
"Content"
],
Expand All @@ -65,10 +77,44 @@
"/properties/Description",
"/properties/Content"
],
"readOnlyProperties": [
"/properties/LayerVersionArn"
],
"writeOnlyProperties": [
"/properties/Content"
],
"primaryIdentifier": [
"/properties/Id"
"/properties/LayerVersionArn"
],
"readOnlyProperties": [
"/properties/Id"
]
"tagging": {
"taggable": false,
"tagOnCreate": false,
"tagUpdatable": false,
"cloudFormationSystemTags": false
},
"handlers": {
"create": {
"permissions": [
"lambda:PublishLayerVersion",
"s3:GetObject",
"s3:GetObjectVersion"
]
},
"read": {
"permissions": [
"lambda:GetLayerVersion"
]
},
"delete": {
"permissions": [
"lambda:GetLayerVersion",
"lambda:DeleteLayerVersion"
]
},
"list": {
"permissions": [
"lambda:ListLayerVersions"
]
}
}
}
27 changes: 27 additions & 0 deletions tests/aws/services/cloudformation/resources/test_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -1237,3 +1237,30 @@ def check_dlq_message(response: dict):

retry(check_dlq_message, response=response, retries=5, sleep=2.5)
snapshot.match("failed-async-lambda", response)


@markers.aws.validated
def test_lambda_layer_crud(deploy_cfn_template, aws_client, s3_bucket, snapshot):
snapshot.add_transformers_list(
[snapshot.transform.key_value("LambdaName"), snapshot.transform.key_value("layer-name")]
)

layer_name = f"layer-{short_uid()}"
snapshot.match("layer-name", layer_name)

bucket_key = "layer.zip"
zip_file = create_lambda_archive(
"hello",
get_content=True,
runtime=Runtime.python3_12,
file_name="hello.txt",
)
aws_client.s3.upload_fileobj(BytesIO(zip_file), s3_bucket, bucket_key)

deployment = deploy_cfn_template(
template_path=os.path.join(
os.path.dirname(__file__), "../../../templates/lambda_layer_version.yml"
),
parameters={"LayerBucket": s3_bucket, "LayerName": layer_name},
)
snapshot.match("cfn-output", deployment.outputs)
Original file line number Diff line number Diff line change
Expand Up @@ -1594,5 +1594,17 @@
}
}
}
},
"tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_layer_crud": {
"recorded-date": "20-12-2024, 18:23:31",
"recorded-content": {
"layer-name": "<layer-name:1>",
"cfn-output": {
"LambdaArn": "arn:<partition>:lambda:<region>:111111111111:function:<lambda-name:1>",
"LambdaName": "<lambda-name:1>",
"LayerVersionArn": "arn:<partition>:lambda:<region>:111111111111:layer:<layer-name:1>:1",
"LayerVersionRef": "arn:<partition>:lambda:<region>:111111111111:layer:<layer-name:1>:1"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_function_tags": {
"last_validated_date": "2024-10-01T12:52:51+00:00"
},
"tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_layer_crud": {
"last_validated_date": "2024-12-20T18:23:31+00:00"
},
"tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_version": {
"last_validated_date": "2024-04-09T07:21:37+00:00"
},
Expand Down
Loading
Loading