Skip to content

Commit 101fa43

Browse files
authored
feat(images): additions to SAM spec (#1845)
New properties in AWS::Serverless::Function ImageUri -> Code: ImageUri -> AWS::Lambda::Function ImageConfig -> Passthrough -> AWS::Lambda::Function PackageType -> Passthrough -> AWS::Lambda::Function
1 parent 2707bb8 commit 101fa43

20 files changed

+1518
-19
lines changed

samtranslator/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.31.0"
1+
__version__ = "1.32.0"

samtranslator/model/lambda_.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ class LambdaFunction(Resource):
77
resource_type = "AWS::Lambda::Function"
88
property_types = {
99
"Code": PropertyType(True, is_type(dict)),
10+
"PackageType": PropertyType(False, is_str()),
1011
"DeadLetterConfig": PropertyType(False, is_type(dict)),
1112
"Description": PropertyType(False, is_str()),
1213
"FunctionName": PropertyType(False, is_str()),
13-
"Handler": PropertyType(True, is_str()),
14+
"Handler": PropertyType(False, is_str()),
1415
"MemorySize": PropertyType(False, is_type(int)),
1516
"Role": PropertyType(False, is_str()),
1617
"Runtime": PropertyType(False, is_str()),
@@ -24,6 +25,7 @@ class LambdaFunction(Resource):
2425
"ReservedConcurrentExecutions": PropertyType(False, any_type()),
2526
"FileSystemConfigs": PropertyType(False, list_of(is_type(dict))),
2627
"CodeSigningConfigArn": PropertyType(False, is_str()),
28+
"ImageConfig": PropertyType(False, is_type(dict)),
2729
}
2830

2931
runtime_attrs = {"name": lambda self: ref(self.logical_id), "arn": lambda self: fnGetAtt(self.logical_id, "Arn")}

samtranslator/model/packagetype.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
IMAGE = "Image"
2+
ZIP = "Zip"

samtranslator/model/s3_utils/uri_parser.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ def to_s3_uri(code_dict):
4545
return uri
4646

4747

48+
def construct_image_code_object(image_uri, logical_id, property_name):
49+
"""Constructs a Lambda `Code` or `Content` property, from the SAM `ImageUri` property.
50+
This follows the current scheme for Lambda Functions.
51+
52+
:param string image_uri: string
53+
:param string logical_id: logical_id of the resource calling this function
54+
:param string property_name: name of the property which is used as an input to this function.
55+
:returns: a Code dict, containing the ImageUri.
56+
:rtype: dict
57+
"""
58+
if not image_uri:
59+
raise InvalidResourceException(
60+
logical_id, "'{}' requires that a image hosted at a registry be specified.".format(property_name)
61+
)
62+
63+
return {"ImageUri": image_uri}
64+
65+
4866
def construct_s3_location_object(location_uri, logical_id, property_name):
4967
"""Constructs a Lambda `Code` or `Content` property, from the SAM `CodeUri` or `ContentUri` property.
5068
This follows the current scheme for Lambda Functions and LayerVersions.

samtranslator/model/sam_resources.py

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import samtranslator.model.eventsources.cloudwatchlogs
88
from .api.api_generator import ApiGenerator
99
from .api.http_api_generator import HttpApiGenerator
10-
from .s3_utils.uri_parser import construct_s3_location_object
10+
from .packagetype import ZIP, IMAGE
11+
from .s3_utils.uri_parser import construct_s3_location_object, construct_image_code_object
1112
from .tags.resource_tagging import get_tag_list
1213
from samtranslator.model import PropertyType, SamResourceMacro, ResourceTypeResolver
1314
from samtranslator.model.apigateway import (
@@ -54,9 +55,11 @@ class SamFunction(SamResourceMacro):
5455
resource_type = "AWS::Serverless::Function"
5556
property_types = {
5657
"FunctionName": PropertyType(False, one_of(is_str(), is_type(dict))),
57-
"Handler": PropertyType(True, is_str()),
58-
"Runtime": PropertyType(True, is_str()),
58+
"Handler": PropertyType(False, is_str()),
59+
"Runtime": PropertyType(False, is_str()),
5960
"CodeUri": PropertyType(False, one_of(is_str(), is_type(dict))),
61+
"ImageUri": PropertyType(False, is_str()),
62+
"PackageType": PropertyType(False, is_str()),
6063
"InlineCode": PropertyType(False, one_of(is_str(), is_type(dict))),
6164
"DeadLetterQueue": PropertyType(False, is_type(dict)),
6265
"Description": PropertyType(False, is_str()),
@@ -82,6 +85,7 @@ class SamFunction(SamResourceMacro):
8285
"VersionDescription": PropertyType(False, is_str()),
8386
"ProvisionedConcurrencyConfig": PropertyType(False, is_type(dict)),
8487
"FileSystemConfigs": PropertyType(False, list_of(is_type(dict))),
88+
"ImageConfig": PropertyType(False, is_type(dict)),
8589
"CodeSigningConfigArn": PropertyType(False, is_str()),
8690
}
8791
event_resolver = ResourceTypeResolver(
@@ -406,6 +410,8 @@ def _construct_lambda_function(self):
406410
lambda_function.Tags = self._construct_tag_list(self.Tags)
407411
lambda_function.Layers = self.Layers
408412
lambda_function.FileSystemConfigs = self.FileSystemConfigs
413+
lambda_function.ImageConfig = self.ImageConfig
414+
lambda_function.PackageType = self.PackageType
409415

410416
if self.Tracing:
411417
lambda_function.TracingConfig = {"Mode": self.Tracing}
@@ -415,6 +421,7 @@ def _construct_lambda_function(self):
415421

416422
lambda_function.CodeSigningConfigArn = self.CodeSigningConfigArn
417423

424+
self._validate_package_type(lambda_function)
418425
return lambda_function
419426

420427
def _add_event_invoke_managed_policy(self, dest_config, logical_id, condition, dest_arn):
@@ -491,6 +498,50 @@ def _construct_role(self, managed_policy_map, event_invoke_policies):
491498
)
492499
return execution_role
493500

501+
def _validate_package_type(self, lambda_function):
502+
"""
503+
Validates Function based on the existence of Package type
504+
"""
505+
packagetype = lambda_function.PackageType or ZIP
506+
507+
if packagetype not in [ZIP, IMAGE]:
508+
raise InvalidResourceException(
509+
lambda_function.logical_id,
510+
"PackageType needs to be `{zip}` or `{image}`".format(zip=ZIP, image=IMAGE),
511+
)
512+
513+
def _validate_package_type_zip():
514+
if not all([lambda_function.Runtime, lambda_function.Handler]):
515+
raise InvalidResourceException(
516+
lambda_function.logical_id,
517+
"Runtime and Handler needs to be present when PackageType is of type `{zip}`".format(zip=ZIP),
518+
)
519+
520+
if any([lambda_function.Code.get("ImageUri", False), lambda_function.ImageConfig]):
521+
raise InvalidResourceException(
522+
lambda_function.logical_id,
523+
"ImageUri or ImageConfig cannot be present when PackageType is of type `{zip}`".format(zip=ZIP),
524+
)
525+
526+
def _validate_package_type_image():
527+
if any([lambda_function.Handler, lambda_function.Runtime, lambda_function.Layers]):
528+
raise InvalidResourceException(
529+
lambda_function.logical_id,
530+
"Runtime, Handler, Layers cannot be present when PackageType is of type `{image}`".format(
531+
image=IMAGE
532+
),
533+
)
534+
if not lambda_function.Code.get("ImageUri"):
535+
raise InvalidResourceException(
536+
lambda_function.logical_id,
537+
"ImageUri needs to be present when PackageType is of type `{image}`".format(image=IMAGE),
538+
)
539+
540+
_validate_per_package_type = {ZIP: _validate_package_type_zip, IMAGE: _validate_package_type_image}
541+
542+
# Call appropriate validation function based on the package type.
543+
return _validate_per_package_type[packagetype]()
544+
494545
def _validate_dlq(self):
495546
"""Validates whether the DeadLetterQueue LogicalId is validation
496547
:raise: InvalidResourceException
@@ -577,12 +628,59 @@ def _generate_event_resources(
577628
return resources
578629

579630
def _construct_code_dict(self):
580-
if self.InlineCode:
631+
"""Constructs Lambda Code Dictionary based on the accepted SAM artifact properties such
632+
as `InlineCode`, `CodeUri` and `ImageUri` and also raises errors if more than one of them is
633+
defined. `PackageType` determines which artifacts are considered.
634+
635+
:raises InvalidResourceException when conditions on the SAM artifact properties are not met.
636+
"""
637+
# list of accepted artifacts
638+
packagetype = self.PackageType or ZIP
639+
artifacts = {}
640+
641+
if packagetype == ZIP:
642+
artifacts = {"InlineCode": self.InlineCode, "CodeUri": self.CodeUri}
643+
elif packagetype == IMAGE:
644+
artifacts = {"ImageUri": self.ImageUri}
645+
646+
if packagetype not in [ZIP, IMAGE]:
647+
raise InvalidResourceException(self.logical_id, "invalid 'PackageType' : {}".format(packagetype))
648+
649+
# Inline function for transformation of inline code.
650+
# It accepts arbitrary argumemnts, because the arguments do not matter for the result.
651+
def _construct_inline_code(*args, **kwargs):
581652
return {"ZipFile": self.InlineCode}
582-
elif self.CodeUri:
583-
return construct_s3_location_object(self.CodeUri, self.logical_id, "CodeUri")
653+
654+
# dispatch mechanism per artifact on how it needs to be transformed.
655+
artifact_dispatch = {
656+
"InlineCode": _construct_inline_code,
657+
"CodeUri": construct_s3_location_object,
658+
"ImageUri": construct_image_code_object,
659+
}
660+
661+
filtered_artifacts = dict(filter(lambda x: x[1] != None, artifacts.items()))
662+
# There are more than one allowed artifact types present, raise an Error.
663+
# There are no valid artifact types present, also raise an Error.
664+
if len(filtered_artifacts) > 1 or len(filtered_artifacts) == 0:
665+
if packagetype == ZIP and len(filtered_artifacts) == 0:
666+
raise InvalidResourceException(self.logical_id, "Only one of 'InlineCode' or 'CodeUri' can be set.")
667+
elif packagetype == IMAGE:
668+
raise InvalidResourceException(self.logical_id, "'ImageUri' must be set.")
669+
670+
filtered_keys = [key for key in filtered_artifacts.keys()]
671+
# NOTE(sriram-mv): This precedence order is important. It is protect against python2 vs python3
672+
# dictionary ordering when getting the key values with .keys() on a dictionary.
673+
# Do not change this precedence order.
674+
if "InlineCode" in filtered_keys:
675+
filtered_key = "InlineCode"
676+
elif "CodeUri" in filtered_keys:
677+
filtered_key = "CodeUri"
678+
elif "ImageUri" in filtered_keys:
679+
filtered_key = "ImageUri"
584680
else:
585-
raise InvalidResourceException(self.logical_id, "Either 'InlineCode' or 'CodeUri' must be set")
681+
raise InvalidResourceException(self.logical_id, "Either 'InlineCode' or 'CodeUri' must be set.")
682+
dispatch_function = artifact_dispatch[filtered_key]
683+
return dispatch_function(artifacts[filtered_key], self.logical_id, filtered_key)
586684

587685
def _construct_version(self, function, intrinsics_resolver, code_sha256=None):
588686
"""Constructs a Lambda Version resource that will be auto-published when CodeUri of the function changes.

tests/model/test_sam_resources.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
from samtranslator.model import InvalidResourceException
77
from samtranslator.model.apigatewayv2 import ApiGatewayV2HttpApi
88
from samtranslator.model.lambda_ import LambdaFunction, LambdaVersion
9-
from samtranslator.model.apigateway import ApiGatewayRestApi
10-
from samtranslator.model.apigateway import ApiGatewayDeployment
9+
from samtranslator.model.apigateway import ApiGatewayDeployment, ApiGatewayRestApi
1110
from samtranslator.model.apigateway import ApiGatewayStage
1211
from samtranslator.model.iam import IAMRole
12+
from samtranslator.model.packagetype import IMAGE, ZIP
1313
from samtranslator.model.sam_resources import SamFunction, SamApi, SamHttpApi
1414

1515

16-
class TestCodeUri(TestCase):
16+
class TestCodeUriandImageUri(TestCase):
1717
kwargs = {
1818
"intrinsics_resolver": IntrinsicsResolver({}),
1919
"event_resources": [],
@@ -24,6 +24,8 @@ class TestCodeUri(TestCase):
2424
def test_with_code_uri(self):
2525
function = SamFunction("foo")
2626
function.CodeUri = "s3://foobar/foo.zip"
27+
function.Runtime = "foo"
28+
function.Handler = "bar"
2729

2830
cfnResources = function.to_cloudformation(**self.kwargs)
2931
generatedFunctionList = [x for x in cfnResources if isinstance(x, LambdaFunction)]
@@ -34,14 +36,62 @@ def test_with_code_uri(self):
3436
def test_with_zip_file(self):
3537
function = SamFunction("foo")
3638
function.InlineCode = "hello world"
39+
function.Runtime = "foo"
40+
function.Handler = "bar"
3741

3842
cfnResources = function.to_cloudformation(**self.kwargs)
3943
generatedFunctionList = [x for x in cfnResources if isinstance(x, LambdaFunction)]
4044
self.assertEqual(generatedFunctionList.__len__(), 1)
4145
self.assertEqual(generatedFunctionList[0].Code, {"ZipFile": "hello world"})
4246

43-
def test_with_no_code_uri_or_zipfile(self):
47+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
48+
def test_with_no_code_uri_or_zipfile_or_no_image_uri(self):
49+
function = SamFunction("foo")
50+
with pytest.raises(InvalidResourceException):
51+
function.to_cloudformation(**self.kwargs)
52+
53+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
54+
def test_with_image_uri(self):
55+
function = SamFunction("foo")
56+
function.ImageUri = "123456789.dkr.ecr.us-east-1.amazonaws.com/myimage:latest"
57+
function.PackageType = IMAGE
58+
cfnResources = function.to_cloudformation(**self.kwargs)
59+
generatedFunctionList = [x for x in cfnResources if isinstance(x, LambdaFunction)]
60+
self.assertEqual(generatedFunctionList.__len__(), 1)
61+
self.assertEqual(generatedFunctionList[0].Code, {"ImageUri": function.ImageUri})
62+
63+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
64+
def test_with_image_uri_layers_runtime_handler(self):
65+
function = SamFunction("foo")
66+
function.ImageUri = "123456789.dkr.ecr.us-east-1.amazonaws.com/myimage:latest"
67+
function.Layers = ["Layer1"]
68+
function.Runtime = "foo"
69+
function.Handler = "bar"
70+
function.PackageType = IMAGE
71+
with pytest.raises(InvalidResourceException):
72+
function.to_cloudformation(**self.kwargs)
73+
74+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
75+
def test_with_image_uri_package_type_zip(self):
76+
function = SamFunction("foo")
77+
function.ImageUri = "123456789.dkr.ecr.us-east-1.amazonaws.com/myimage:latest"
78+
function.PackageType = ZIP
79+
with pytest.raises(InvalidResourceException):
80+
function.to_cloudformation(**self.kwargs)
81+
82+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
83+
def test_with_image_uri_invalid_package_type(self):
4484
function = SamFunction("foo")
85+
function.ImageUri = "123456789.dkr.ecr.us-east-1.amazonaws.com/myimage:latest"
86+
function.PackageType = "fake"
87+
with pytest.raises(InvalidResourceException):
88+
function.to_cloudformation(**self.kwargs)
89+
90+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
91+
def test_with_image_uri_and_code_uri(self):
92+
function = SamFunction("foo")
93+
function.ImageUri = "123456789.dkr.ecr.us-east-1.amazonaws.com/myimage:latest"
94+
function.CodeUri = "s3://foobar/foo.zip"
4595
with pytest.raises(InvalidResourceException):
4696
function.to_cloudformation(**self.kwargs)
4797

@@ -57,6 +107,8 @@ class TestAssumeRolePolicyDocument(TestCase):
57107
def test_with_assume_role_policy_document(self):
58108
function = SamFunction("foo")
59109
function.CodeUri = "s3://foobar/foo.zip"
110+
function.Runtime = "foo"
111+
function.Handler = "bar"
60112

61113
assume_role_policy_document = {
62114
"Version": "2012-10-17",
@@ -79,6 +131,8 @@ def test_with_assume_role_policy_document(self):
79131
def test_without_assume_role_policy_document(self):
80132
function = SamFunction("foo")
81133
function.CodeUri = "s3://foobar/foo.zip"
134+
function.Runtime = "foo"
135+
function.Handler = "bar"
82136

83137
assume_role_policy_document = {
84138
"Version": "2012-10-17",
@@ -104,6 +158,8 @@ def test_with_version_description(self):
104158
function = SamFunction("foo")
105159
test_description = "foobar"
106160

161+
function.Runtime = "foo"
162+
function.Handler = "bar"
107163
function.CodeUri = "s3://foobar/foo.zip"
108164
function.VersionDescription = test_description
109165
function.AutoPublishAlias = "live"

0 commit comments

Comments
 (0)