Skip to content

feat: add property to update lambda version when lambda layer is updated #3661

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Nov 6, 2024
Merged
5 changes: 5 additions & 0 deletions docs/globals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Currently, the following resources and properties are being supported:
EphemeralStorage:
RuntimeManagementConfig:
LoggingConfig:
FileSystemConfigs:

Api:
# Properties of AWS::Serverless::Api
Expand Down Expand Up @@ -113,6 +114,10 @@ Currently, the following resources and properties are being supported:
# Properties of AWS::Serverless::SimpleTable
SSESpecification:

LayerVersion:
# Properties of AWS::Serverless::LayerVersion
PublishLambdaVersion:

Implicit APIs
~~~~~~~~~~~~~

Expand Down
36 changes: 36 additions & 0 deletions integration/combination/test_function_with_alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,42 @@ def test_alias_with_event_sources_get_correct_permissions(self):
function_policy = json.loads(function_policy_str)
self.assertEqual(len(function_policy["Statement"]), len(permission_resources))

def test_function_with_alias_and_layer_version(self):
self.create_and_verify_stack("combination/function_with_alias_all_properties_and_layer_version")
alias_name = "Live"
function_name = self.get_physical_id_by_type("AWS::Lambda::Function")
version_ids = self.get_function_version_by_name(function_name)
self.assertEqual(["1"], version_ids)

alias = self.get_alias(function_name, alias_name)
self.assertEqual("1", alias["FunctionVersion"])

# Changing Description in the LayerVersion should create a new version, and leave the existing version intact
self.set_template_resource_property("MyLayer", "Description", "test123")
self.update_stack()

version_ids = self.get_function_version_by_name(function_name)
self.assertEqual(["1", "2"], version_ids)

alias = self.get_alias(function_name, alias_name)
self.assertEqual("2", alias["FunctionVersion"])

# Changing ContentUri in LayerVersion should create a new version, and leave the existing version intact
self.set_template_resource_property("MyLayer", "ContentUri", self.file_to_s3_uri_map["layer2.zip"]["uri"])
self.update_stack()

version_ids = self.get_function_version_by_name(function_name)
self.assertEqual(["1", "2", "3"], version_ids)

alias = self.get_alias(function_name, alias_name)
self.assertEqual("3", alias["FunctionVersion"])

# Make sure the stack has only One Version & One Alias resource
alias = self.get_stack_resources("AWS::Lambda::Alias")
versions = self.get_stack_resources("AWS::Lambda::Version")
self.assertEqual(len(alias), 1)
self.assertEqual(len(versions), 1)

def get_function_version_by_name(self, function_name):
lambda_client = self.client_provider.lambda_client
versions = lambda_client.list_versions_by_function(FunctionName=function_name)["Versions"]
Expand Down
4 changes: 4 additions & 0 deletions integration/config/file_to_s3_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"type": "s3",
"uri": ""
},
"layer2.zip": {
"type": "s3",
"uri": ""
},
"swagger1.json": {
"type": "s3",
"uri": ""
Expand Down
1 change: 1 addition & 0 deletions integration/helpers/file_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"code.zip": {"type": "s3", "uri": ""},
"code2.zip": {"type": "s3", "uri": ""},
"layer1.zip": {"type": "s3", "uri": ""},
"layer2.zip": {"type": "s3", "uri": ""},
"swagger1.json": {"type": "s3", "uri": ""},
"swagger2.json": {"type": "s3", "uri": ""},
"binary-media.zip": {"type": "s3", "uri": ""},
Expand Down
Binary file added integration/resources/code/layer2.zip
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"LogicalResourceId": "MyLambdaFunction",
"ResourceType": "AWS::Lambda::Function"
},
{
"LogicalResourceId": "MyLambdaFunctionRole",
"ResourceType": "AWS::IAM::Role"
},
{
"LogicalResourceId": "MyLambdaFunctionAliasLive",
"ResourceType": "AWS::Lambda::Alias"
},
{
"LogicalResourceId": "MyLambdaFunctionVersion",
"ResourceType": "AWS::Lambda::Version"
},
{
"LogicalResourceId": "MyLayer",
"ResourceType": "AWS::Lambda::LayerVersion"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Resources:
MyLambdaFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ${codeuri}
Handler: index.handler
Runtime: nodejs20.x
AutoPublishAlias: Live
AutoPublishAliasAllProperties: true
Layers:
- !Ref MyLayer

MyLayer:
Type: AWS::Serverless::LayerVersion
Properties:
ContentUri: ${contenturi}
RetentionPolicy: Delete
PublishLambdaVersion: true
Description: test
Metadata:
SamTransformTest: true
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Properties(BaseModel):
"CompatibleRuntimes",
["AWS::Lambda::LayerVersion", "Properties", "CompatibleRuntimes"],
)
PublishLambdaVersion: Optional[bool] # TODO: add docs
ContentUri: Union[str, ContentUri] = properties("ContentUri")
Description: Optional[PassThroughProp] = passthrough_prop(
PROPERTIES_STEM,
Expand All @@ -65,3 +66,7 @@ class Properties(BaseModel):
class Resource(ResourceAttributes):
Type: Literal["AWS::Serverless::LayerVersion"]
Properties: Properties


class Globals(BaseModel):
PublishLambdaVersion: Optional[bool] # TODO: add docs
1 change: 1 addition & 0 deletions samtranslator/internal/schema_source/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Globals(BaseModel):
HttpApi: Optional[aws_serverless_httpapi.Globals]
SimpleTable: Optional[aws_serverless_simpletable.Globals]
StateMachine: Optional[aws_serverless_statemachine.Globals]
LayerVersion: Optional[aws_serverless_layerversion.Globals]


Resources = Union[
Expand Down
37 changes: 34 additions & 3 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
from samtranslator.model.intrinsics import (
fnGetAtt,
fnSub,
get_logical_id_from_intrinsic,
is_intrinsic,
is_intrinsic_if,
is_intrinsic_no_value,
Expand Down Expand Up @@ -265,6 +266,7 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] # noqa: P
"""
resources: List[Any] = []
intrinsics_resolver: IntrinsicsResolver = kwargs["intrinsics_resolver"]
resource_resolver: ResourceResolver = kwargs["resource_resolver"]
mappings_resolver: Optional[IntrinsicsResolver] = kwargs.get("mappings_resolver")
conditions = kwargs.get("conditions", {})
feature_toggle = kwargs.get("feature_toggle")
Expand Down Expand Up @@ -303,7 +305,10 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] # noqa: P
else:
lambda_function.Description = {"Fn::Join": [" ", [description, code_sha256]]}
lambda_version = self._construct_version(
lambda_function, intrinsics_resolver=intrinsics_resolver, code_sha256=code_sha256
lambda_function,
intrinsics_resolver=intrinsics_resolver,
resource_resolver=resource_resolver,
code_sha256=code_sha256,
)
lambda_alias = self._construct_alias(alias_name, lambda_function, lambda_version)
resources.append(lambda_version)
Expand Down Expand Up @@ -882,8 +887,12 @@ def _construct_inline_code(*args: Any, **kwargs: Dict[str, Any]) -> Dict[str, An
dispatch_function: Callable[..., Dict[str, Any]] = artifact_dispatch[filtered_key]
return dispatch_function(artifacts[filtered_key], self.logical_id, filtered_key)

def _construct_version(
self, function: LambdaFunction, intrinsics_resolver: IntrinsicsResolver, code_sha256: Optional[str] = None
def _construct_version( # noqa: PLR0912
self,
function: LambdaFunction,
intrinsics_resolver: IntrinsicsResolver,
resource_resolver: ResourceResolver,
code_sha256: Optional[str] = None,
) -> LambdaVersion:
"""Constructs a Lambda Version resource that will be auto-published when CodeUri of the function changes.
Old versions will not be deleted without a direct reference from the CloudFormation template.
Expand Down Expand Up @@ -929,6 +938,26 @@ def _construct_version(
# property that when set to true would change the lambda version whenever a property in the lambda function changes
if self.AutoPublishAliasAllProperties:
properties = function._generate_resource_dict().get("Properties", {})

# When a Lambda LayerVersion resource is updated, a new Lambda layer is created.
# However, we need the Lambda function to automatically create a new version
# and use the new layer. By setting the `PublishLambdaVersion` property to true,
# a new Lambda function version will be created when the layer version is updated.
if function.Layers:
for layer in function.Layers:
layer_logical_id = get_logical_id_from_intrinsic(layer)
if not layer_logical_id:
continue

layer_resource = resource_resolver.get_resource_by_logical_id(layer_logical_id)
if not layer_resource:
continue

layer_properties = layer_resource.get("Properties", {})
publish_lambda_version = layer_properties.get("PublishLambdaVersion", False)
if publish_lambda_version:
properties.update({layer_logical_id: layer_properties})

logical_dict = properties
else:
with suppress(AttributeError, UnboundLocalError):
Expand Down Expand Up @@ -1596,6 +1625,7 @@ class SamLayerVersion(SamResourceMacro):
property_types = {
"LayerName": PropertyType(False, one_of(IS_STR, IS_DICT)),
"Description": PropertyType(False, IS_STR),
"PublishLambdaVersion": PropertyType(False, IS_BOOL),
"ContentUri": PropertyType(True, one_of(IS_STR, IS_DICT)),
"CompatibleArchitectures": PropertyType(False, list_of(one_of(IS_STR, IS_DICT))),
"CompatibleRuntimes": PropertyType(False, list_of(one_of(IS_STR, IS_DICT))),
Expand All @@ -1605,6 +1635,7 @@ class SamLayerVersion(SamResourceMacro):

LayerName: Optional[Intrinsicable[str]]
Description: Optional[Intrinsicable[str]]
PublishLambdaVersion: Optional[bool]
ContentUri: Dict[str, Any]
CompatibleArchitectures: Optional[List[Any]]
CompatibleRuntimes: Optional[List[Any]]
Expand Down
1 change: 1 addition & 0 deletions samtranslator/plugins/globals/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class Globals:
],
SamResourceType.SimpleTable.value: ["SSESpecification"],
SamResourceType.StateMachine.value: ["PropagateTags"],
SamResourceType.LambdaLayerVersion.value: ["PublishLambdaVersion"],
}
# unreleased_properties *must be* part of supported_properties too
unreleased_properties: Dict[str, List[str]] = {
Expand Down
18 changes: 18 additions & 0 deletions samtranslator/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -277241,6 +277241,9 @@
"HttpApi": {
"$ref": "#/definitions/samtranslator__internal__schema_source__aws_serverless_httpapi__Globals"
},
"LayerVersion": {
"$ref": "#/definitions/samtranslator__internal__schema_source__aws_serverless_layerversion__Globals"
},
"SimpleTable": {
"$ref": "#/definitions/samtranslator__internal__schema_source__aws_serverless_simpletable__Globals"
},
Expand Down Expand Up @@ -280288,6 +280291,17 @@
"title": "Route53",
"type": "object"
},
"samtranslator__internal__schema_source__aws_serverless_layerversion__Globals": {
"additionalProperties": false,
"properties": {
"PublishLambdaVersion": {
"title": "Publishlambdaversion",
"type": "boolean"
}
},
"title": "Globals",
"type": "object"
},
"samtranslator__internal__schema_source__aws_serverless_layerversion__Properties": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -280338,6 +280352,10 @@
"title": "LicenseInfo",
"type": "string"
},
"PublishLambdaVersion": {
"title": "Publishlambdaversion",
"type": "boolean"
},
"RetentionPolicy": {
"anyOf": [
{
Expand Down
18 changes: 18 additions & 0 deletions schema_source/sam.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3413,6 +3413,9 @@
"HttpApi": {
"$ref": "#/definitions/samtranslator__internal__schema_source__aws_serverless_httpapi__Globals"
},
"LayerVersion": {
"$ref": "#/definitions/samtranslator__internal__schema_source__aws_serverless_layerversion__Globals"
},
"SimpleTable": {
"$ref": "#/definitions/samtranslator__internal__schema_source__aws_serverless_simpletable__Globals"
},
Expand Down Expand Up @@ -7212,6 +7215,17 @@
"title": "Route53",
"type": "object"
},
"samtranslator__internal__schema_source__aws_serverless_layerversion__Globals": {
"additionalProperties": false,
"properties": {
"PublishLambdaVersion": {
"title": "Publishlambdaversion",
"type": "boolean"
}
},
"title": "Globals",
"type": "object"
},
"samtranslator__internal__schema_source__aws_serverless_layerversion__Properties": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -7312,6 +7326,10 @@
],
"title": "LicenseInfo"
},
"PublishLambdaVersion": {
"title": "Publishlambdaversion",
"type": "boolean"
},
"RetentionPolicy": {
"anyOf": [
{
Expand Down
5 changes: 5 additions & 0 deletions tests/model/test_sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class TestArchitecture(TestCase):
"intrinsics_resolver": IntrinsicsResolver({}),
"event_resources": [],
"managed_policy_map": {"foo": "bar"},
"resource_resolver": ResourceResolver({}),
}

@patch("boto3.session.Session.region_name", "ap-southeast-1")
Expand Down Expand Up @@ -60,6 +61,7 @@ class TestCodeUriandImageUri(TestCase):
"intrinsics_resolver": IntrinsicsResolver({}),
"event_resources": [],
"managed_policy_map": {"foo": "bar"},
"resource_resolver": ResourceResolver({}),
}

@patch("boto3.session.Session.region_name", "ap-southeast-1")
Expand Down Expand Up @@ -143,6 +145,7 @@ class TestAssumeRolePolicyDocument(TestCase):
"intrinsics_resolver": IntrinsicsResolver({}),
"event_resources": [],
"managed_policy_map": {"foo": "bar"},
"resource_resolver": ResourceResolver({}),
}

@patch("boto3.session.Session.region_name", "ap-southeast-1")
Expand Down Expand Up @@ -193,6 +196,7 @@ class TestVersionDescription(TestCase):
"intrinsics_resolver": IntrinsicsResolver({}),
"event_resources": [],
"managed_policy_map": {"foo": "bar"},
"resource_resolver": ResourceResolver({}),
}

@patch("boto3.session.Session.region_name", "ap-southeast-1")
Expand Down Expand Up @@ -441,6 +445,7 @@ class TestFunctionUrlConfig(TestCase):
"intrinsics_resolver": IntrinsicsResolver({}),
"event_resources": [],
"managed_policy_map": {"foo": "bar"},
"resource_resolver": ResourceResolver({}),
}

@patch("boto3.session.Session.region_name", "ap-southeast-1")
Expand Down
6 changes: 3 additions & 3 deletions tests/translator/output/error_globals_unsupported_type.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
"Number of errors found: 1. ",
"'Globals' section is invalid. ",
"'NewType' is not supported. ",
"Must be one of the following values - ['Api', 'Function', 'HttpApi', 'SimpleTable', 'StateMachine']"
"Must be one of the following values - ['Api', 'Function', 'HttpApi', 'LayerVersion', 'SimpleTable', 'StateMachine']"
],
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. 'Globals' section is invalid. 'NewType' is not supported. Must be one of the following values - ['Api', 'Function', 'HttpApi', 'SimpleTable', 'StateMachine']",
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. 'Globals' section is invalid. 'NewType' is not supported. Must be one of the following values - ['Api', 'Function', 'HttpApi', 'LayerVersion', 'SimpleTable', 'StateMachine']",
"errors": [
{
"errorMessage": "'Globals' section is invalid. 'NewType' is not supported. Must be one of the following values - ['Api', 'Function', 'HttpApi', 'SimpleTable', 'StateMachine']"
"errorMessage": "'Globals' section is invalid. 'NewType' is not supported. Must be one of the following values - ['Api', 'Function', 'HttpApi', 'LayerVersion', 'SimpleTable', 'StateMachine']"
}
]
}
Loading