From 44e83f6ae723d5607d2a3fa288017a201cd3789f Mon Sep 17 00:00:00 2001 From: Alexis Facques Date: Tue, 29 Jun 2021 22:56:20 +0200 Subject: [PATCH] fix: Skip build of Docker image if ImageUri is a valid ECR URL (#2934) (#2935) --- samcli/commands/deploy/guided_context.py | 2 +- samcli/lib/package/ecr_utils.py | 2 +- samcli/lib/providers/sam_base_provider.py | 28 +++ samcli/lib/providers/sam_function_provider.py | 18 ++ .../local/lib/test_sam_function_provider.py | 169 ++++++++++++++---- 5 files changed, 183 insertions(+), 36 deletions(-) diff --git a/samcli/commands/deploy/guided_context.py b/samcli/commands/deploy/guided_context.py index 18b6cb7be1..a2ab1f958d 100644 --- a/samcli/commands/deploy/guided_context.py +++ b/samcli/commands/deploy/guided_context.py @@ -337,7 +337,7 @@ def prompt_image_repository(self, stacks: List[Stack]): if isinstance(self.image_repositories, dict) else "" or self.image_repository, ) - if not is_ecr_url(image_repositories.get(resource_id)): + if resource_id not in image_repositories or not is_ecr_url(str(image_repositories[resource_id])): raise GuidedDeployFailedError( f"Invalid Image Repository ECR URI: {image_repositories.get(resource_id)}" ) diff --git a/samcli/lib/package/ecr_utils.py b/samcli/lib/package/ecr_utils.py index 6186d24099..f4bedc4a27 100644 --- a/samcli/lib/package/ecr_utils.py +++ b/samcli/lib/package/ecr_utils.py @@ -6,5 +6,5 @@ from samcli.lib.package.regexpr import ECR_URL -def is_ecr_url(url): +def is_ecr_url(url: str) -> bool: return bool(re.match(ECR_URL, url)) if url else False diff --git a/samcli/lib/providers/sam_base_provider.py b/samcli/lib/providers/sam_base_provider.py index 29623adb18..a135cbd8f5 100644 --- a/samcli/lib/providers/sam_base_provider.py +++ b/samcli/lib/providers/sam_base_provider.py @@ -11,6 +11,8 @@ from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable from samcli.lib.samlib.resource_metadata_normalizer import ResourceMetadataNormalizer from samcli.lib.samlib.wrapper import SamTranslatorWrapper +from samcli.lib.package.ecr_utils import is_ecr_url + LOG = logging.getLogger(__name__) @@ -35,6 +37,11 @@ class SamBaseProvider: SERVERLESS_LAYER: "ContentUri", } + IMAGE_PROPERTY_KEYS = { + LAMBDA_FUNCTION: "Code", + SERVERLESS_FUNCTION: "ImageUri", + } + def get(self, name: str) -> Optional[Any]: """ Given name of the function, this method must return the Function object @@ -89,6 +96,17 @@ def _is_s3_location(location: Optional[Union[str, Dict]]) -> bool: isinstance(location, str) and location.startswith("s3://") ) + @staticmethod + def _is_ecr_uri(location: Optional[Union[str, Dict]]) -> bool: + """ + the input could be: + - ImageUri of Serverless::Function + - Code of Lambda::Function + """ + return location is not None and is_ecr_url( + str(location.get("ImageUri", "")) if isinstance(location, dict) else location + ) + @staticmethod def _warn_code_extraction(resource_type: str, resource_name: str, code_property: str) -> None: LOG.warning( @@ -99,6 +117,16 @@ def _warn_code_extraction(resource_type: str, resource_name: str, code_property: code_property, ) + @staticmethod + def _warn_imageuri_extraction(resource_type: str, resource_name: str, image_property: str) -> None: + LOG.warning( + "The resource %s '%s' has specified ECR registry image for %s. " + "It will not be built and SAM CLI does not support invoking it locally.", + resource_type, + resource_name, + image_property, + ) + @staticmethod def _extract_lambda_function_imageuri(resource_properties: Dict, code_property_key: str) -> Optional[str]: """ diff --git a/samcli/lib/providers/sam_function_provider.py b/samcli/lib/providers/sam_function_provider.py index e53782fead..0cec8b5ac0 100644 --- a/samcli/lib/providers/sam_function_provider.py +++ b/samcli/lib/providers/sam_function_provider.py @@ -130,7 +130,17 @@ def _extract_functions( resource_properties["Metadata"] = resource_metadata if resource_type in [SamFunctionProvider.SERVERLESS_FUNCTION, SamFunctionProvider.LAMBDA_FUNCTION]: + resource_package_type = resource_properties.get("PackageType", ZIP) + code_property_key = SamBaseProvider.CODE_PROPERTY_KEYS[resource_type] + image_property_key = SamBaseProvider.IMAGE_PROPERTY_KEYS[resource_type] + + assets = resource.assets or [] + code_asset_uri = None + for asset in assets: + if isinstance(asset, S3Asset) and asset.source_property == code_property_key: + code_asset_uri = asset.source_path + break assets = resource.assets or [] code_asset_uri = None for asset in assets: @@ -143,6 +153,14 @@ def _extract_functions( SamFunctionProvider._warn_code_extraction(resource_type, name, code_property_key) continue + if resource_package_type == IMAGE and SamBaseProvider._is_ecr_uri( + resource_properties.get(image_property_key) + ): + # ImageUri can be an ECR uri, which is not supported + if not ignore_code_extraction_warnings: + SamFunctionProvider._warn_imageuri_extraction(resource_type, name, image_property_key) + continue + if resource_type == SamFunctionProvider.SERVERLESS_FUNCTION: layers = SamFunctionProvider._parse_layer_info( stack, diff --git a/tests/unit/commands/local/lib/test_sam_function_provider.py b/tests/unit/commands/local/lib/test_sam_function_provider.py index 89558c6227..19b33a41ce 100644 --- a/tests/unit/commands/local/lib/test_sam_function_provider.py +++ b/tests/unit/commands/local/lib/test_sam_function_provider.py @@ -64,10 +64,6 @@ class TestSamFunctionProviderEndToEnd(TestCase): "Handler": "index.handler", }, }, - "SamFunc4": { - "Type": "AWS::Serverless::Function", - "Properties": {"ImageUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myrepo", "PackageType": IMAGE}, - }, "SamFuncWithFunctionNameOverride": { "Type": "AWS::Serverless::Function", "Properties": { @@ -77,6 +73,29 @@ class TestSamFunctionProviderEndToEnd(TestCase): "Handler": "index.handler", }, }, + "SamFuncWithImage1": { + "Type": "AWS::Serverless::Function", + "Properties": { + "PackageType": IMAGE, + }, + "Metadata": {"DockerTag": "tag", "DockerContext": "./image", "Dockerfile": "Dockerfile"}, + }, + "SamFuncWithImage2": { + "Type": "AWS::Serverless::Function", + "Properties": { + "ImageUri": "image:tag", + "PackageType": IMAGE, + }, + "Metadata": {"DockerTag": "tag", "DockerContext": "./image", "Dockerfile": "Dockerfile"}, + }, + "SamFuncWithImage3": { + # ImageUri is unsupported ECR location + "Type": "AWS::Serverless::Function", + "Properties": { + "ImageUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myrepo:myimage", + "PackageType": IMAGE, + }, + }, "LambdaFunc1": { "Type": "AWS::Lambda::Function", "Properties": { @@ -85,21 +104,37 @@ class TestSamFunctionProviderEndToEnd(TestCase): "Handler": "index.handler", }, }, - "LambdaFuncWithInlineCode": { + "LambdaFuncWithImage1": { "Type": "AWS::Lambda::Function", "Properties": { - "Code": {"ZipFile": "testcode"}, - "Runtime": "nodejs4.3", - "Handler": "index.handler", + "PackageType": IMAGE, + }, + "Metadata": {"DockerTag": "tag", "DockerContext": "./image", "Dockerfile": "Dockerfile"}, + }, + "LambdaFuncWithImage2": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": {"ImageUri": "image:tag"}, + "PackageType": IMAGE, }, + "Metadata": {"DockerTag": "tag", "DockerContext": "./image", "Dockerfile": "Dockerfile"}, }, - "LambdaFunc2": { + "LambdaFuncWithImage3": { + # ImageUri is unsupported ECR location "Type": "AWS::Lambda::Function", "Properties": { "Code": {"ImageUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myrepo"}, "PackageType": IMAGE, }, }, + "LambdaFuncWithInlineCode": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": {"ZipFile": "testcode"}, + "Runtime": "nodejs4.3", + "Handler": "index.handler", + }, + }, "LambdaFuncWithLocalPath": { "Type": "AWS::Lambda::Function", "Properties": {"Code": "./some/path/to/code", "Runtime": "nodejs4.3", "Handler": "index.handler"}, @@ -251,11 +286,11 @@ def setUp(self): ("SamFunc2", None), # codeuri is a s3 location, ignored ("SamFunc3", None), # codeuri is a s3 location, ignored ( - "SamFunc4", + "SamFuncWithImage1", Function( - function_id="SamFunc4", - name="SamFunc4", - functionname="SamFunc4", + function_id="SamFuncWithImage1", + name="SamFuncWithImage1", + functionname="SamFuncWithImage1", runtime=None, handler=None, codeuri=".", @@ -266,14 +301,46 @@ def setUp(self): layers=[], events=None, inlinecode=None, - imageuri="123456789012.dkr.ecr.us-east-1.amazonaws.com/myrepo", + imageuri=None, imageconfig=None, packagetype=IMAGE, - metadata=None, + metadata={ + "DockerTag": "tag", + "DockerContext": os.path.join("image"), + "Dockerfile": "Dockerfile", + }, codesign_config_arn=None, stack_path="", ), ), + ( + "SamFuncWithImage2", + Function( + name="SamFuncWithImage2", + functionname="SamFuncWithImage2", + runtime=None, + handler=None, + codeuri=".", + memory=None, + timeout=None, + environment=None, + rolearn=None, + layers=[], + events=None, + inlinecode=None, + imageuri="image:tag", + imageconfig=None, + packagetype=IMAGE, + metadata={ + "DockerTag": "tag", + "DockerContext": os.path.join("image"), + "Dockerfile": "Dockerfile", + }, + codesign_config_arn=None, + stack_path="", + ), + ), + ("SamFuncWithImage3", None), # imageuri is ecr location, ignored ( "SamFuncWithFunctionNameOverride-x", Function( @@ -300,35 +367,39 @@ def setUp(self): ), ("LambdaFunc1", None), # codeuri is a s3 location, ignored ( - "LambdaFuncWithInlineCode", + "LambdaFuncWithImage1", Function( - function_id="LambdaFuncWithInlineCode", - name="LambdaFuncWithInlineCode", - functionname="LambdaFuncWithInlineCode", - runtime="nodejs4.3", - handler="index.handler", - codeuri=None, + function_id="LambdaFuncWithImage1", + name="LambdaFuncWithImage1", + functionname="LambdaFuncWithImage1", + runtime=None, + handler=None, + codeuri=".", memory=None, timeout=None, environment=None, rolearn=None, layers=[], events=None, - metadata=None, - inlinecode="testcode", - codesign_config_arn=None, + metadata={ + "DockerTag": "tag", + "DockerContext": os.path.join("image"), + "Dockerfile": "Dockerfile", + }, + inlinecode=None, imageuri=None, imageconfig=None, - packagetype=ZIP, + packagetype=IMAGE, + codesign_config_arn=None, stack_path="", ), ), ( - "LambdaFunc2", + "LambdaFuncWithImage2", Function( - function_id="LambdaFunc2", - name="LambdaFunc2", - functionname="LambdaFunc2", + function_id="LambdaFuncWithImage2", + name="LambdaFuncWithImage2", + functionname="LambdaFuncWithImage2", runtime=None, handler=None, codeuri=".", @@ -338,15 +409,43 @@ def setUp(self): rolearn=None, layers=[], events=None, - metadata=None, + metadata={ + "DockerTag": "tag", + "DockerContext": os.path.join("image"), + "Dockerfile": "Dockerfile", + }, inlinecode=None, - imageuri="123456789012.dkr.ecr.us-east-1.amazonaws.com/myrepo", + imageuri="image:tag", imageconfig=None, packagetype=IMAGE, codesign_config_arn=None, stack_path="", ), ), + ("LambdaFuncWithImage3", None), # imageuri is a ecr location, ignored + ( + "LambdaFuncWithInlineCode", + Function( + name="LambdaFuncWithInlineCode", + functionname="LambdaFuncWithInlineCode", + runtime="nodejs4.3", + handler="index.handler", + codeuri=None, + memory=None, + timeout=None, + environment=None, + rolearn=None, + layers=[], + events=None, + metadata=None, + inlinecode="testcode", + codesign_config_arn=None, + imageuri=None, + imageconfig=None, + packagetype=ZIP, + stack_path="", + ), + ), ( "LambdaFuncWithLocalPath", Function( @@ -507,11 +606,13 @@ def test_get_all_must_return_all_functions(self): result = {posixpath.join(f.stack_path, f.name) for f in self.provider.get_all()} expected = { "SamFunctions", + "SamFuncWithImage1", + "SamFuncWithImage2", "SamFuncWithInlineCode", - "SamFunc4", "SamFuncWithFunctionNameOverride", + "LambdaFuncWithImage1", + "LambdaFuncWithImage2", "LambdaFuncWithInlineCode", - "LambdaFunc2", "LambdaFuncWithLocalPath", "LambdaFuncWithFunctionNameOverride", "LambdaFuncWithCodeSignConfig",