Skip to content
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

Introduce AutoPublishCodeSha256 to allow overriding the publish versi… #1376

Merged
merged 1 commit into from
Jan 27, 2020
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
1 change: 1 addition & 0 deletions docs/cloudformation_compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ DeadLetterQueue All
DeploymentPreference All
Layers All
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.
AutoPublishCodeSha256 All
ReservedConcurrentExecutions All
EventInvokeConfig All
============================ ================================== ========================
Expand Down
4 changes: 3 additions & 1 deletion docs/safe_lambda_deployments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ This will:

- Create an Alias with ``<alias-name>``
- Create & publish a Lambda version with the latest code & configuration
derived from the ``CodeUri`` property
derived from the ``CodeUri`` property. Optionally it is possible to specify
property `AutoPublishCodeSha256` that will override the hash computed for
Lambda ``CodeUri`` property.
- Point the Alias to the latest published version
- Point all event sources to the Alias & not to the function
- When the ``CodeUri`` property of ``AWS::Serverless::Function`` changes,
Expand Down
11 changes: 8 additions & 3 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class SamFunction(SamResourceMacro):
"EventInvokeConfig": PropertyType(False, is_type(dict)),
# Intrinsic functions in value of Alias property are not supported, yet
"AutoPublishAlias": PropertyType(False, one_of(is_str())),
"AutoPublishCodeSha256": PropertyType(False, one_of(is_str())),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Just is_str() instead of one_of(is_str())

"VersionDescription": PropertyType(False, is_str()),
"ProvisionedConcurrencyConfig": PropertyType(False, is_type(dict)),
}
Expand Down Expand Up @@ -132,7 +133,10 @@ def to_cloudformation(self, **kwargs):
alias_name = ""
if self.AutoPublishAlias:
alias_name = self._get_resolved_alias_name("AutoPublishAlias", self.AutoPublishAlias, intrinsics_resolver)
lambda_version = self._construct_version(lambda_function, intrinsics_resolver=intrinsics_resolver)
code_sha256 = self.AutoPublishCodeSha256
lambda_version = self._construct_version(
lambda_function, intrinsics_resolver=intrinsics_resolver, code_sha256=code_sha256
)
lambda_alias = self._construct_alias(alias_name, lambda_function, lambda_version)
resources.append(lambda_version)
resources.append(lambda_alias)
Expand Down Expand Up @@ -596,14 +600,15 @@ def _construct_code_dict(self):
else:
raise InvalidResourceException(self.logical_id, "Either 'InlineCode' or 'CodeUri' must be set")

def _construct_version(self, function, intrinsics_resolver):
def _construct_version(self, function, intrinsics_resolver, code_sha256=None):
"""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.

:param model.lambda_.LambdaFunction function: Lambda function object that is being connected to a version
:param model.intrinsics.resolver.IntrinsicsResolver intrinsics_resolver: Class that can help resolve
references to parameters present in CodeUri. It is a common usecase to set S3Key of Code to be a
template parameter. Need to resolve the values otherwise we will never detect a change in Code dict
:param str code_sha256: User predefined hash of the Lambda function code
:return: Lambda function Version resource
"""
code_dict = function.Code
Expand Down Expand Up @@ -635,7 +640,7 @@ def _construct_version(self, function, intrinsics_resolver):
# SHA Collisions: For purposes of triggering a new update, we are concerned about just the difference previous
# and next hashes. The chances that two subsequent hashes collide is fairly low.
prefix = "{id}Version".format(id=self.logical_id)
logical_id = logical_id_generator.LogicalIdGenerator(prefix, code_dict).gen()
logical_id = logical_id_generator.LogicalIdGenerator(prefix, code_dict, code_sha256).gen()

attributes = self.get_passthrough_resource_attributes()
if attributes is None:
Expand Down
6 changes: 5 additions & 1 deletion samtranslator/translator/logical_id_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class LogicalIdGenerator(object):
# given by this class
HASH_LENGTH = 10

def __init__(self, prefix, data_obj=None):
def __init__(self, prefix, data_obj=None, data_hash=None):
"""
Generate logical IDs for resources that are stable, deterministic and platform independent

Expand All @@ -24,6 +24,7 @@ def __init__(self, prefix, data_obj=None):

self._prefix = prefix
self.data_str = data_str
self.data_hash = data_hash

def gen(self):
"""
Expand Down Expand Up @@ -54,6 +55,9 @@ def get_hash(self, length=HASH_LENGTH):
:rtype string
"""

if self.data_hash:
return self.data_hash[:length]

data_hash = ""
if not self.data_str:
return data_hash
Expand Down
11 changes: 11 additions & 0 deletions tests/translator/input/function_with_alias_and_code_sha256.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Resources:
MinimalFunction:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: s3://sam-demo-bucket/hello.zip
Handler: hello.handler
Runtime: python2.7
AutoPublishAlias: live
AutoPublishCodeSha256: 6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b
VersionDescription: sam-testing

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"Resources": {
"MinimalFunctionVersion6b86b273ff": {
"DeletionPolicy": "Retain",
"Type": "AWS::Lambda::Version",
"Properties": {
"Description": "sam-testing",
"FunctionName": {
"Ref": "MinimalFunction"
}
}
},
"MinimalFunctionAliaslive": {
"Type": "AWS::Lambda::Alias",
"Properties": {
"FunctionVersion": {
"Fn::GetAtt": [
"MinimalFunctionVersion6b86b273ff",
"Version"
]
},
"FunctionName": {
"Ref": "MinimalFunction"
},
"Name": "live"
}
},
"MinimalFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": "sam-demo-bucket",
"S3Key": "hello.zip"
},
"Handler": "hello.handler",
"Role": {
"Fn::GetAtt": [
"MinimalFunctionRole",
"Arn"
]
},
"Runtime": "python2.7",
"Tags": [
{
"Value": "SAM",
"Key": "lambda:createdBy"
}
]
}
},
"MinimalFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"ManagedPolicyArns": [
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
],
"Tags": [
{
"Value": "SAM",
"Key": "lambda:createdBy"
}
],
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"sts:AssumeRole"
],
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com"
]
}
}
]
}
}
}
}
}
40 changes: 31 additions & 9 deletions tests/translator/test_function_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,29 @@ def test_version_creation(self, LogicalIdGeneratorMock):
self.assertEqual(version.get_resource_attribute("DeletionPolicy"), "Retain")

expected_prefix = self.sam_func.logical_id + "Version"
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, None)
generator_mock.gen.assert_called_once_with()
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)

@patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
def test_version_creation_with_code_sha(self, LogicalIdGeneratorMock):
generator_mock = LogicalIdGeneratorMock.return_value
prefix = "SomeLogicalId"
hash_code = "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"
id_val = "{}{}".format(prefix, hash_code[:10])
generator_mock.gen.return_value = id_val

self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = self.lambda_func.Code
self.sam_func.AutoPublishCodeSha256 = hash_code
version = self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock, hash_code)

self.assertEqual(version.logical_id, id_val)
self.assertEqual(version.Description, None)
self.assertEqual(version.FunctionName, {"Ref": self.lambda_func.logical_id})
self.assertEqual(version.get_resource_attribute("DeletionPolicy"), "Retain")

expected_prefix = self.sam_func.logical_id + "Version"
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, hash_code)
generator_mock.gen.assert_called_once_with()
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)

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

expected_prefix = self.sam_func.logical_id + "Version"
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, None)
generator_mock.gen.assert_called_once_with()
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)

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

expected_prefix = self.sam_func.logical_id + "Version"
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, None)
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)

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

expected_prefix = self.sam_func.logical_id + "Version"
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, None)
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)

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

expected_prefix = self.sam_func.logical_id + "Version"
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code, None)
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)

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

LogicalIdGeneratorMock.assert_called_once_with(prefix, self.lambda_func.Code)
LogicalIdGeneratorMock.assert_called_once_with(prefix, self.lambda_func.Code, None)
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(self.lambda_func.Code)

# Modify Code of the lambda function
self.lambda_func.Code["S3ObjectVersion"] = "new object version"
new_code = self.lambda_func.Code.copy()
self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = new_code
self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)
LogicalIdGeneratorMock.assert_called_with(prefix, new_code)
LogicalIdGeneratorMock.assert_called_with(prefix, new_code, None)
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(new_code)

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

LogicalIdGeneratorMock.assert_called_once_with(prefix, self.lambda_func.Code)
LogicalIdGeneratorMock.assert_called_once_with(prefix, self.lambda_func.Code, None)
self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(self.lambda_func.Code)

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

def test_alias_creation(self):
Expand Down
27 changes: 27 additions & 0 deletions tests/translator/test_logical_id_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,33 @@ def test_gen_dict_data(self, stringify_mock, get_hash_mock):

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

@patch.object(LogicalIdGenerator, "_stringify")
def test_gen_hash_data_override(self, stringify_mock):
data = {"foo": "bar"}
stringified_data = "stringified data"
hash_value = "6b86b273ff"
stringify_mock.return_value = stringified_data

generator = LogicalIdGenerator(self.prefix, data_obj=data, data_hash=hash_value)

expected = "{}{}".format(self.prefix, hash_value)
self.assertEqual(expected, generator.gen())
stringify_mock.assert_called_once_with(data)

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

@patch.object(LogicalIdGenerator, "_stringify")
def test_gen_hash_data_empty(self, stringify_mock):
data = {"foo": "bar"}
stringified_data = "stringified data"
hash_value = ""
stringify_mock.return_value = stringified_data

generator = LogicalIdGenerator(self.prefix, data_obj=data, data_hash=hash_value)

stringify_mock.assert_called_once_with(data)
self.assertEqual(generator.gen(), generator.gen())

def test_gen_stability_with_copy(self):
data = {"foo": "bar", "a": "b"}
generator = LogicalIdGenerator(self.prefix, data_obj=data)
Expand Down