Skip to content

Commit fa7549f

Browse files
authored
feat: add property to update lambda version when lambda layer is updated (#3661)
1 parent 51b6994 commit fa7549f

24 files changed

+855
-21
lines changed

docs/globals.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Currently, the following resources and properties are being supported:
7575
EphemeralStorage:
7676
RuntimeManagementConfig:
7777
LoggingConfig:
78+
FileSystemConfigs:
7879
7980
Api:
8081
# Properties of AWS::Serverless::Api
@@ -113,6 +114,10 @@ Currently, the following resources and properties are being supported:
113114
# Properties of AWS::Serverless::SimpleTable
114115
SSESpecification:
115116
117+
LayerVersion:
118+
# Properties of AWS::Serverless::LayerVersion
119+
PublishLambdaVersion:
120+
116121
Implicit APIs
117122
~~~~~~~~~~~~~
118123

integration/combination/test_function_with_alias.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,42 @@ def test_alias_with_event_sources_get_correct_permissions(self):
160160
function_policy = json.loads(function_policy_str)
161161
self.assertEqual(len(function_policy["Statement"]), len(permission_resources))
162162

163+
def test_function_with_alias_and_layer_version(self):
164+
self.create_and_verify_stack("combination/function_with_alias_all_properties_and_layer_version")
165+
alias_name = "Live"
166+
function_name = self.get_physical_id_by_type("AWS::Lambda::Function")
167+
version_ids = self.get_function_version_by_name(function_name)
168+
self.assertEqual(["1"], version_ids)
169+
170+
alias = self.get_alias(function_name, alias_name)
171+
self.assertEqual("1", alias["FunctionVersion"])
172+
173+
# Changing Description in the LayerVersion should create a new version, and leave the existing version intact
174+
self.set_template_resource_property("MyLayer", "Description", "test123")
175+
self.update_stack()
176+
177+
version_ids = self.get_function_version_by_name(function_name)
178+
self.assertEqual(["1", "2"], version_ids)
179+
180+
alias = self.get_alias(function_name, alias_name)
181+
self.assertEqual("2", alias["FunctionVersion"])
182+
183+
# Changing ContentUri in LayerVersion should create a new version, and leave the existing version intact
184+
self.set_template_resource_property("MyLayer", "ContentUri", self.file_to_s3_uri_map["layer2.zip"]["uri"])
185+
self.update_stack()
186+
187+
version_ids = self.get_function_version_by_name(function_name)
188+
self.assertEqual(["1", "2", "3"], version_ids)
189+
190+
alias = self.get_alias(function_name, alias_name)
191+
self.assertEqual("3", alias["FunctionVersion"])
192+
193+
# Make sure the stack has only One Version & One Alias resource
194+
alias = self.get_stack_resources("AWS::Lambda::Alias")
195+
versions = self.get_stack_resources("AWS::Lambda::Version")
196+
self.assertEqual(len(alias), 1)
197+
self.assertEqual(len(versions), 1)
198+
163199
def get_function_version_by_name(self, function_name):
164200
lambda_client = self.client_provider.lambda_client
165201
versions = lambda_client.list_versions_by_function(FunctionName=function_name)["Versions"]

integration/config/file_to_s3_map.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"type": "s3",
2424
"uri": ""
2525
},
26+
"layer2.zip": {
27+
"type": "s3",
28+
"uri": ""
29+
},
2630
"swagger1.json": {
2731
"type": "s3",
2832
"uri": ""

integration/helpers/file_resources.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"code.zip": {"type": "s3", "uri": ""},
33
"code2.zip": {"type": "s3", "uri": ""},
44
"layer1.zip": {"type": "s3", "uri": ""},
5+
"layer2.zip": {"type": "s3", "uri": ""},
56
"swagger1.json": {"type": "s3", "uri": ""},
67
"swagger2.json": {"type": "s3", "uri": ""},
78
"binary-media.zip": {"type": "s3", "uri": ""},

integration/resources/code/layer2.zip

2.12 KB
Binary file not shown.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[
2+
{
3+
"LogicalResourceId": "MyLambdaFunction",
4+
"ResourceType": "AWS::Lambda::Function"
5+
},
6+
{
7+
"LogicalResourceId": "MyLambdaFunctionRole",
8+
"ResourceType": "AWS::IAM::Role"
9+
},
10+
{
11+
"LogicalResourceId": "MyLambdaFunctionAliasLive",
12+
"ResourceType": "AWS::Lambda::Alias"
13+
},
14+
{
15+
"LogicalResourceId": "MyLambdaFunctionVersion",
16+
"ResourceType": "AWS::Lambda::Version"
17+
},
18+
{
19+
"LogicalResourceId": "MyLayer",
20+
"ResourceType": "AWS::Lambda::LayerVersion"
21+
}
22+
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Resources:
2+
MyLambdaFunction:
3+
Type: AWS::Serverless::Function
4+
Properties:
5+
CodeUri: ${codeuri}
6+
Handler: index.handler
7+
Runtime: nodejs20.x
8+
AutoPublishAlias: Live
9+
AutoPublishAliasAllProperties: true
10+
Layers:
11+
- !Ref MyLayer
12+
13+
MyLayer:
14+
Type: AWS::Serverless::LayerVersion
15+
Properties:
16+
ContentUri: ${contenturi}
17+
RetentionPolicy: Delete
18+
PublishLambdaVersion: true
19+
Description: test
20+
Metadata:
21+
SamTransformTest: true

samtranslator/internal/schema_source/aws_serverless_layerversion.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class Properties(BaseModel):
4747
"CompatibleRuntimes",
4848
["AWS::Lambda::LayerVersion", "Properties", "CompatibleRuntimes"],
4949
)
50+
PublishLambdaVersion: Optional[bool] # TODO: add docs
5051
ContentUri: Union[str, ContentUri] = properties("ContentUri")
5152
Description: Optional[PassThroughProp] = passthrough_prop(
5253
PROPERTIES_STEM,
@@ -65,3 +66,7 @@ class Properties(BaseModel):
6566
class Resource(ResourceAttributes):
6667
Type: Literal["AWS::Serverless::LayerVersion"]
6768
Properties: Properties
69+
70+
71+
class Globals(BaseModel):
72+
PublishLambdaVersion: Optional[bool] # TODO: add docs

samtranslator/internal/schema_source/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Globals(BaseModel):
2828
HttpApi: Optional[aws_serverless_httpapi.Globals]
2929
SimpleTable: Optional[aws_serverless_simpletable.Globals]
3030
StateMachine: Optional[aws_serverless_statemachine.Globals]
31+
LayerVersion: Optional[aws_serverless_layerversion.Globals]
3132

3233

3334
Resources = Union[

samtranslator/model/sam_resources.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
from samtranslator.model.intrinsics import (
8282
fnGetAtt,
8383
fnSub,
84+
get_logical_id_from_intrinsic,
8485
is_intrinsic,
8586
is_intrinsic_if,
8687
is_intrinsic_no_value,
@@ -265,6 +266,7 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] # noqa: P
265266
"""
266267
resources: List[Any] = []
267268
intrinsics_resolver: IntrinsicsResolver = kwargs["intrinsics_resolver"]
269+
resource_resolver: ResourceResolver = kwargs["resource_resolver"]
268270
mappings_resolver: Optional[IntrinsicsResolver] = kwargs.get("mappings_resolver")
269271
conditions = kwargs.get("conditions", {})
270272
feature_toggle = kwargs.get("feature_toggle")
@@ -303,7 +305,10 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] # noqa: P
303305
else:
304306
lambda_function.Description = {"Fn::Join": [" ", [description, code_sha256]]}
305307
lambda_version = self._construct_version(
306-
lambda_function, intrinsics_resolver=intrinsics_resolver, code_sha256=code_sha256
308+
lambda_function,
309+
intrinsics_resolver=intrinsics_resolver,
310+
resource_resolver=resource_resolver,
311+
code_sha256=code_sha256,
307312
)
308313
lambda_alias = self._construct_alias(alias_name, lambda_function, lambda_version)
309314
resources.append(lambda_version)
@@ -882,8 +887,12 @@ def _construct_inline_code(*args: Any, **kwargs: Dict[str, Any]) -> Dict[str, An
882887
dispatch_function: Callable[..., Dict[str, Any]] = artifact_dispatch[filtered_key]
883888
return dispatch_function(artifacts[filtered_key], self.logical_id, filtered_key)
884889

885-
def _construct_version(
886-
self, function: LambdaFunction, intrinsics_resolver: IntrinsicsResolver, code_sha256: Optional[str] = None
890+
def _construct_version( # noqa: PLR0912
891+
self,
892+
function: LambdaFunction,
893+
intrinsics_resolver: IntrinsicsResolver,
894+
resource_resolver: ResourceResolver,
895+
code_sha256: Optional[str] = None,
887896
) -> LambdaVersion:
888897
"""Constructs a Lambda Version resource that will be auto-published when CodeUri of the function changes.
889898
Old versions will not be deleted without a direct reference from the CloudFormation template.
@@ -929,6 +938,26 @@ def _construct_version(
929938
# property that when set to true would change the lambda version whenever a property in the lambda function changes
930939
if self.AutoPublishAliasAllProperties:
931940
properties = function._generate_resource_dict().get("Properties", {})
941+
942+
# When a Lambda LayerVersion resource is updated, a new Lambda layer is created.
943+
# However, we need the Lambda function to automatically create a new version
944+
# and use the new layer. By setting the `PublishLambdaVersion` property to true,
945+
# a new Lambda function version will be created when the layer version is updated.
946+
if function.Layers:
947+
for layer in function.Layers:
948+
layer_logical_id = get_logical_id_from_intrinsic(layer)
949+
if not layer_logical_id:
950+
continue
951+
952+
layer_resource = resource_resolver.get_resource_by_logical_id(layer_logical_id)
953+
if not layer_resource:
954+
continue
955+
956+
layer_properties = layer_resource.get("Properties", {})
957+
publish_lambda_version = layer_properties.get("PublishLambdaVersion", False)
958+
if publish_lambda_version:
959+
properties.update({layer_logical_id: layer_properties})
960+
932961
logical_dict = properties
933962
else:
934963
with suppress(AttributeError, UnboundLocalError):
@@ -1596,6 +1625,7 @@ class SamLayerVersion(SamResourceMacro):
15961625
property_types = {
15971626
"LayerName": PropertyType(False, one_of(IS_STR, IS_DICT)),
15981627
"Description": PropertyType(False, IS_STR),
1628+
"PublishLambdaVersion": PropertyType(False, IS_BOOL),
15991629
"ContentUri": PropertyType(True, one_of(IS_STR, IS_DICT)),
16001630
"CompatibleArchitectures": PropertyType(False, list_of(one_of(IS_STR, IS_DICT))),
16011631
"CompatibleRuntimes": PropertyType(False, list_of(one_of(IS_STR, IS_DICT))),
@@ -1605,6 +1635,7 @@ class SamLayerVersion(SamResourceMacro):
16051635

16061636
LayerName: Optional[Intrinsicable[str]]
16071637
Description: Optional[Intrinsicable[str]]
1638+
PublishLambdaVersion: Optional[bool]
16081639
ContentUri: Dict[str, Any]
16091640
CompatibleArchitectures: Optional[List[Any]]
16101641
CompatibleRuntimes: Optional[List[Any]]

0 commit comments

Comments
 (0)