Skip to content

Commit eca5538

Browse files
committed
Introduce AutoPublishCodeSha256 to allow overriding the publish version resource identifier
1 parent eabe27d commit eca5538

8 files changed

+168
-14
lines changed

docs/cloudformation_compatibility.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ DeadLetterQueue All
6262
DeploymentPreference All
6363
Layers All
6464
AutoPublishAlias Ref of a CloudFormation Parameter Alias resources created by SAM uses a LocicalId <FunctionLogicalId+AliasName>. So SAM either needs a string for alias name, or a Ref to template Parameter that SAM can resolve into a string.
65+
AutoPublishCodeSha256 All
6566
ReservedConcurrentExecutions All
6667
EventInvokeConfig All
6768
============================ ================================== ========================

docs/safe_lambda_deployments.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ This will:
6262

6363
- Create an Alias with ``<alias-name>``
6464
- Create & publish a Lambda version with the latest code & configuration
65-
derived from the ``CodeUri`` property
65+
derived from the ``CodeUri`` property. Optionally it is possible to specify
66+
property `AutoPublishCodeSha256` that will override the hash computed for
67+
Lambda ``CodeUri`` property.
6668
- Point the Alias to the latest published version
6769
- Point all event sources to the Alias & not to the function
6870
- When the ``CodeUri`` property of ``AWS::Serverless::Function`` changes,

samtranslator/model/sam_resources.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class SamFunction(SamResourceMacro):
7070
"EventInvokeConfig": PropertyType(False, is_type(dict)),
7171
# Intrinsic functions in value of Alias property are not supported, yet
7272
"AutoPublishAlias": PropertyType(False, one_of(is_str())),
73+
"AutoPublishCodeSha256": PropertyType(False, one_of(is_str())),
7374
"VersionDescription": PropertyType(False, is_str()),
7475
"ProvisionedConcurrencyConfig": PropertyType(False, is_type(dict)),
7576
}
@@ -132,7 +133,10 @@ def to_cloudformation(self, **kwargs):
132133
alias_name = ""
133134
if self.AutoPublishAlias:
134135
alias_name = self._get_resolved_alias_name("AutoPublishAlias", self.AutoPublishAlias, intrinsics_resolver)
135-
lambda_version = self._construct_version(lambda_function, intrinsics_resolver=intrinsics_resolver)
136+
code_sha256 = self.AutoPublishCodeSha256
137+
lambda_version = self._construct_version(
138+
lambda_function, intrinsics_resolver=intrinsics_resolver, code_sha256=code_sha256
139+
)
136140
lambda_alias = self._construct_alias(alias_name, lambda_function, lambda_version)
137141
resources.append(lambda_version)
138142
resources.append(lambda_alias)
@@ -596,14 +600,15 @@ def _construct_code_dict(self):
596600
else:
597601
raise InvalidResourceException(self.logical_id, "Either 'InlineCode' or 'CodeUri' must be set")
598602

599-
def _construct_version(self, function, intrinsics_resolver):
603+
def _construct_version(self, function, intrinsics_resolver, code_sha256=None):
600604
"""Constructs a Lambda Version resource that will be auto-published when CodeUri of the function changes.
601605
Old versions will not be deleted without a direct reference from the CloudFormation template.
602606
603607
:param model.lambda_.LambdaFunction function: Lambda function object that is being connected to a version
604608
:param model.intrinsics.resolver.IntrinsicsResolver intrinsics_resolver: Class that can help resolve
605609
references to parameters present in CodeUri. It is a common usecase to set S3Key of Code to be a
606610
template parameter. Need to resolve the values otherwise we will never detect a change in Code dict
611+
:param str code_sha256: User predefined hash of the Lambda function code
607612
:return: Lambda function Version resource
608613
"""
609614
code_dict = function.Code
@@ -635,7 +640,7 @@ def _construct_version(self, function, intrinsics_resolver):
635640
# SHA Collisions: For purposes of triggering a new update, we are concerned about just the difference previous
636641
# and next hashes. The chances that two subsequent hashes collide is fairly low.
637642
prefix = "{id}Version".format(id=self.logical_id)
638-
logical_id = logical_id_generator.LogicalIdGenerator(prefix, code_dict).gen()
643+
logical_id = logical_id_generator.LogicalIdGenerator(prefix, code_dict, code_sha256).gen()
639644

640645
attributes = self.get_passthrough_resource_attributes()
641646
if attributes is None:

samtranslator/translator/logical_id_generator.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class LogicalIdGenerator(object):
1010
# given by this class
1111
HASH_LENGTH = 10
1212

13-
def __init__(self, prefix, data_obj=None):
13+
def __init__(self, prefix, data_obj=None, data_hash=None):
1414
"""
1515
Generate logical IDs for resources that are stable, deterministic and platform independent
1616
@@ -24,6 +24,7 @@ def __init__(self, prefix, data_obj=None):
2424

2525
self._prefix = prefix
2626
self.data_str = data_str
27+
self.data_hash = data_hash
2728

2829
def gen(self):
2930
"""
@@ -54,6 +55,9 @@ def get_hash(self, length=HASH_LENGTH):
5455
:rtype string
5556
"""
5657

58+
if self.data_hash:
59+
return self.data_hash[:length]
60+
5761
data_hash = ""
5862
if not self.data_str:
5963
return data_hash
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Resources:
2+
MinimalFunction:
3+
Type: 'AWS::Serverless::Function'
4+
Properties:
5+
CodeUri: s3://sam-demo-bucket/hello.zip
6+
Handler: hello.handler
7+
Runtime: python2.7
8+
AutoPublishAlias: live
9+
AutoPublishCodeSha256: 6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b
10+
VersionDescription: sam-testing
11+
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"Resources": {
3+
"MinimalFunctionVersion6b86b273ff": {
4+
"DeletionPolicy": "Retain",
5+
"Type": "AWS::Lambda::Version",
6+
"Properties": {
7+
"Description": "sam-testing",
8+
"FunctionName": {
9+
"Ref": "MinimalFunction"
10+
}
11+
}
12+
},
13+
"MinimalFunctionAliaslive": {
14+
"Type": "AWS::Lambda::Alias",
15+
"Properties": {
16+
"FunctionVersion": {
17+
"Fn::GetAtt": [
18+
"MinimalFunctionVersion6b86b273ff",
19+
"Version"
20+
]
21+
},
22+
"FunctionName": {
23+
"Ref": "MinimalFunction"
24+
},
25+
"Name": "live"
26+
}
27+
},
28+
"MinimalFunction": {
29+
"Type": "AWS::Lambda::Function",
30+
"Properties": {
31+
"Code": {
32+
"S3Bucket": "sam-demo-bucket",
33+
"S3Key": "hello.zip"
34+
},
35+
"Handler": "hello.handler",
36+
"Role": {
37+
"Fn::GetAtt": [
38+
"MinimalFunctionRole",
39+
"Arn"
40+
]
41+
},
42+
"Runtime": "python2.7",
43+
"Tags": [
44+
{
45+
"Value": "SAM",
46+
"Key": "lambda:createdBy"
47+
}
48+
]
49+
}
50+
},
51+
"MinimalFunctionRole": {
52+
"Type": "AWS::IAM::Role",
53+
"Properties": {
54+
"ManagedPolicyArns": [
55+
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
56+
],
57+
"Tags": [
58+
{
59+
"Value": "SAM",
60+
"Key": "lambda:createdBy"
61+
}
62+
],
63+
"AssumeRolePolicyDocument": {
64+
"Version": "2012-10-17",
65+
"Statement": [
66+
{
67+
"Action": [
68+
"sts:AssumeRole"
69+
],
70+
"Effect": "Allow",
71+
"Principal": {
72+
"Service": [
73+
"lambda.amazonaws.com"
74+
]
75+
}
76+
}
77+
]
78+
}
79+
}
80+
}
81+
}
82+
}

tests/translator/test_function_resources.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,29 @@ def test_version_creation(self, LogicalIdGeneratorMock):
379379
self.assertEqual(version.get_resource_attribute("DeletionPolicy"), "Retain")
380380

381381
expected_prefix = self.sam_func.logical_id + "Version"
382-
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
382+
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, None)
383+
generator_mock.gen.assert_called_once_with()
384+
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)
385+
386+
@patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
387+
def test_version_creation_with_code_sha(self, LogicalIdGeneratorMock):
388+
generator_mock = LogicalIdGeneratorMock.return_value
389+
prefix = "SomeLogicalId"
390+
hash_code = "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"
391+
id_val = "{}{}".format(prefix, hash_code[:10])
392+
generator_mock.gen.return_value = id_val
393+
394+
self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = self.lambda_func.Code
395+
self.sam_func.AutoPublishCodeSha256 = hash_code
396+
version = self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock, hash_code)
397+
398+
self.assertEqual(version.logical_id, id_val)
399+
self.assertEqual(version.Description, None)
400+
self.assertEqual(version.FunctionName, {"Ref": self.lambda_func.logical_id})
401+
self.assertEqual(version.get_resource_attribute("DeletionPolicy"), "Retain")
402+
403+
expected_prefix = self.sam_func.logical_id + "Version"
404+
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, hash_code)
383405
generator_mock.gen.assert_called_once_with()
384406
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)
385407

@@ -397,7 +419,7 @@ def test_version_creation_without_s3_object_version(self, LogicalIdGeneratorMock
397419
self.assertEqual(version.logical_id, id_val)
398420

399421
expected_prefix = self.sam_func.logical_id + "Version"
400-
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
422+
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, None)
401423
generator_mock.gen.assert_called_once_with()
402424
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)
403425

@@ -421,7 +443,7 @@ def test_version_creation_intrinsic_function_in_code_s3key(self, LogicalIdGenera
421443
self.assertEqual(version.logical_id, id_val)
422444

423445
expected_prefix = self.sam_func.logical_id + "Version"
424-
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
446+
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, None)
425447
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)
426448

427449
@patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
@@ -437,7 +459,7 @@ def test_version_creation_intrinsic_function_in_code_s3bucket(self, LogicalIdGen
437459
self.assertEqual(version.logical_id, id_val)
438460

439461
expected_prefix = self.sam_func.logical_id + "Version"
440-
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
462+
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, None)
441463
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)
442464

443465
@patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
@@ -453,7 +475,7 @@ def test_version_creation_intrinsic_function_in_code_s3version(self, LogicalIdGe
453475
self.assertEqual(version.logical_id, id_val)
454476

455477
expected_prefix = self.sam_func.logical_id + "Version"
456-
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
478+
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, None)
457479
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)
458480

459481
@patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
@@ -467,15 +489,15 @@ def test_version_logical_id_changes(self, LogicalIdGeneratorMock):
467489
self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = self.lambda_func.Code
468490
self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)
469491

470-
LogicalIdGeneratorMock.assert_called_once_with(prefix, self.lambda_func.Code)
492+
LogicalIdGeneratorMock.assert_called_once_with(prefix, self.lambda_func.Code, None)
471493
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(self.lambda_func.Code)
472494

473495
# Modify Code of the lambda function
474496
self.lambda_func.Code["S3ObjectVersion"] = "new object version"
475497
new_code = self.lambda_func.Code.copy()
476498
self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = new_code
477499
self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)
478-
LogicalIdGeneratorMock.assert_called_with(prefix, new_code)
500+
LogicalIdGeneratorMock.assert_called_with(prefix, new_code, None)
479501
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(new_code)
480502

481503
@patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
@@ -490,14 +512,14 @@ def test_version_logical_id_changes_with_intrinsic_functions(self, LogicalIdGene
490512
self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = self.lambda_func.Code
491513
self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)
492514

493-
LogicalIdGeneratorMock.assert_called_once_with(prefix, self.lambda_func.Code)
515+
LogicalIdGeneratorMock.assert_called_once_with(prefix, self.lambda_func.Code, None)
494516
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(self.lambda_func.Code)
495517

496518
# Now, just let the intrinsics resolver return a different value. Let's make sure the new value gets wired up properly
497519
new_code = {"S3Bucket": "bucket", "S3Key": "some new value"}
498520
self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = new_code
499521
self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)
500-
LogicalIdGeneratorMock.assert_called_with(prefix, new_code)
522+
LogicalIdGeneratorMock.assert_called_with(prefix, new_code, None)
501523
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(self.lambda_func.Code)
502524

503525
def test_alias_creation(self):

tests/translator/test_logical_id_generator.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,33 @@ def test_gen_dict_data(self, stringify_mock, get_hash_mock):
4444

4545
self.assertEqual(generator.gen(), generator.gen())
4646

47+
@patch.object(LogicalIdGenerator, "_stringify")
48+
def test_gen_hash_data_override(self, stringify_mock):
49+
data = {"foo": "bar"}
50+
stringified_data = "stringified data"
51+
hash_value = "6b86b273ff"
52+
stringify_mock.return_value = stringified_data
53+
54+
generator = LogicalIdGenerator(self.prefix, data_obj=data, data_hash=hash_value)
55+
56+
expected = "{}{}".format(self.prefix, hash_value)
57+
self.assertEqual(expected, generator.gen())
58+
stringify_mock.assert_called_once_with(data)
59+
60+
self.assertEqual(generator.gen(), generator.gen())
61+
62+
@patch.object(LogicalIdGenerator, "_stringify")
63+
def test_gen_hash_data_empty(self, stringify_mock):
64+
data = {"foo": "bar"}
65+
stringified_data = "stringified data"
66+
hash_value = ""
67+
stringify_mock.return_value = stringified_data
68+
69+
generator = LogicalIdGenerator(self.prefix, data_obj=data, data_hash=hash_value)
70+
71+
stringify_mock.assert_called_once_with(data)
72+
self.assertEqual(generator.gen(), generator.gen())
73+
4774
def test_gen_stability_with_copy(self):
4875
data = {"foo": "bar", "a": "b"}
4976
generator = LogicalIdGenerator(self.prefix, data_obj=data)

0 commit comments

Comments
 (0)