From f7a9b11391577c451194c5468b8e4695c47e19f5 Mon Sep 17 00:00:00 2001 From: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> Date: Mon, 23 Jan 2023 18:15:41 -0800 Subject: [PATCH] feat: Support Runtime management controls (#420) (#4609) * feat: Support Runtime management controls (#420) * feat: Support Runtime management controls - Modify ECR repo from `/sam/emulation-` to `/lambda/`. - Keep RAPID image up-to-date with the remote environment. - Remove SAM cli version from RAPID image tag locally. - These modifications change the rapid image tag from `python3.7-rapid-1.2.0-x86_64` to `3.7-rapid-x86_64`, since now the runtime name is part of the image *name* and not the tag. - Support a `RuntimeManagementConfig` in SAM templates and show a message if a runtime version is pinned in the template when invoking. - Rename some variables when building the rapid image, for more clarity. * PR feedback. Add placeholder text * Last updates for RuntimeManagementConfiguration - Change from RuntimeVersion to RuntimeVersion Arn - Update the message when user has a RuntimeVersion in their template and wants to invoke locally. * Update with latest tranform changes * update tests with latest transform changes * rollback dev version of SAMT Co-authored-by: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> * Update local_uri_plugin.py * Update local_uri_plugin.py Co-authored-by: Renato Valenzuela <37676028+valerena@users.noreply.github.com> --- samcli/commands/local/lib/local_lambda.py | 1 + samcli/lib/providers/provider.py | 2 + samcli/lib/providers/sam_function_provider.py | 1 + samcli/local/docker/lambda_image.py | 219 ++++++++++--- samcli/local/docker/manager.py | 2 +- samcli/local/lambdafn/config.py | 4 + samcli/local/lambdafn/runtime.py | 11 + .../invoke/test_integration_cli_images.py | 4 +- .../commands/local/lib/test_local_lambda.py | 8 +- .../unit/commands/local/lib/test_provider.py | 1 + .../local/lib/test_sam_function_provider.py | 43 +++ .../local/docker/test_lambda_container.py | 10 +- tests/unit/local/docker/test_lambda_image.py | 302 ++++++++++++++---- tests/unit/local/docker/test_manager.py | 2 +- tests/unit/local/lambdafn/test_config.py | 5 + tests/unit/local/lambdafn/test_runtime.py | 54 +++- 16 files changed, 547 insertions(+), 122 deletions(-) diff --git a/samcli/commands/local/lib/local_lambda.py b/samcli/commands/local/lib/local_lambda.py index 478c32c6a6..82602478d4 100644 --- a/samcli/commands/local/lib/local_lambda.py +++ b/samcli/commands/local/lib/local_lambda.py @@ -210,6 +210,7 @@ def get_invoke_config(self, function: Function) -> FunctionConfig: memory=function.memory, timeout=function_timeout, env_vars=env_vars, + runtime_management_config=function.runtime_management_config, ) def _make_env_vars(self, function: Function) -> EnvironmentVariables: diff --git a/samcli/lib/providers/provider.py b/samcli/lib/providers/provider.py index 8e12772b75..27838a80a7 100644 --- a/samcli/lib/providers/provider.py +++ b/samcli/lib/providers/provider.py @@ -84,6 +84,8 @@ class Function(NamedTuple): function_url_config: Optional[Dict] # The path of the stack relative to the root stack, it is empty for functions in root stack stack_path: str = "" + # Configuration for runtime management. Includes the fields `UpdateRuntimeOn` and `RuntimeVersionArn` (optional). + runtime_management_config: Optional[Dict] = None @property def full_path(self) -> str: diff --git a/samcli/lib/providers/sam_function_provider.py b/samcli/lib/providers/sam_function_provider.py index 3d453be590..31528a20fb 100644 --- a/samcli/lib/providers/sam_function_provider.py +++ b/samcli/lib/providers/sam_function_provider.py @@ -467,6 +467,7 @@ def _build_function_configuration( codesign_config_arn=resource_properties.get("CodeSigningConfigArn", None), architectures=resource_properties.get("Architectures", None), function_url_config=resource_properties.get("FunctionUrlConfig"), + runtime_management_config=resource_properties.get("RuntimeManagementConfig"), ) @staticmethod diff --git a/samcli/local/docker/lambda_image.py b/samcli/local/docker/lambda_image.py index 98fd41855f..0d9ab43e5f 100644 --- a/samcli/local/docker/lambda_image.py +++ b/samcli/local/docker/lambda_image.py @@ -1,6 +1,7 @@ """ Generates a Docker Image to be used for invoking a function locally """ +from typing import Optional import uuid import logging import hashlib @@ -8,6 +9,7 @@ from pathlib import Path import sys +import re import platform import docker @@ -55,10 +57,49 @@ def has_value(cls, value): """ return any(value == item.value for item in cls) + @classmethod + def get_image_name_tag(cls, runtime: str, architecture: str) -> str: + """ + Returns the image name and tag for a particular runtime + + Parameters + ---------- + runtime : str + AWS Lambda runtime + architecture : str + Architecture for the runtime + + Returns + ------- + str + Image name and tag for the runtime's base image, like `python:3.7` or `provided:al2` + """ + runtime_image_tag = "" + if runtime == cls.provided.value: + # There's a special tag for `provided` not al2 (provided:alami) + runtime_image_tag = "provided:alami" + elif runtime.startswith("provided"): + # `provided.al2` becomes `provided:al2`` + runtime_image_tag = runtime.replace(".", ":") + elif runtime.startswith("dotnet"): + # dotnetcore3.1 becomes dotnet:core3.1 and dotnet6 becomes dotnet:6 + runtime_image_tag = runtime.replace("dotnet", "dotnet:") + else: + # This fits most runtimes format: `nameN.M` becomes `name:N.M` (python3.9 -> python:3.9) + runtime_image_tag = re.sub(r"^([a-z]+)([0-9][a-z0-9\.]*)$", r"\1:\2", runtime) + # nodejs14.x, go1.x, etc don't have the `.x` part. + runtime_image_tag = runtime_image_tag.replace(".x", "") + + # Runtime image tags contain the architecture only if more than one is supported for that runtime + if has_runtime_multi_arch_image(runtime): + runtime_image_tag = f"{runtime_image_tag}-{architecture}" + return runtime_image_tag + class LambdaImage: _LAYERS_DIR = "/opt" - _INVOKE_REPO_PREFIX = "public.ecr.aws/sam/emulation" + _INVOKE_REPO_PREFIX = "public.ecr.aws/lambda" + _SAM_INVOKE_REPO_PREFIX = "public.ecr.aws/sam/emulation" _SAM_CLI_REPO_NAME = "samcli/lambda" _RAPID_SOURCE_PATH = Path(__file__).parent.joinpath("..", "rapid").resolve() @@ -108,49 +149,58 @@ def build(self, runtime, packagetype, image, layers, architecture, stream=None, str The image to be used (REPOSITORY:TAG) """ - image_name = None + base_image = None + tag_prefix = "" if packagetype == IMAGE: - image_name = image + base_image = image elif packagetype == ZIP: if self.invoke_images: - image_name = self.invoke_images.get(function_name, self.invoke_images.get(None)) - if not image_name: - tag_name = f"latest-{architecture}" if has_runtime_multi_arch_image(runtime) else "latest" - image_name = f"{self._INVOKE_REPO_PREFIX}-{runtime}:{tag_name}" - - if not image_name: + base_image = self.invoke_images.get(function_name, self.invoke_images.get(None)) + if not base_image: + # Gets the ECR image format like `python:3.7` or `nodejs:16-x86_64` + runtime_image_tag = Runtime.get_image_name_tag(runtime, architecture) + runtime_only_number = re.split("[:-]", runtime_image_tag)[1] + tag_prefix = f"{runtime_only_number}-" + base_image = f"{self._INVOKE_REPO_PREFIX}/{runtime_image_tag}" + + if not base_image: raise InvalidIntermediateImageError(f"Invalid PackageType, PackageType needs to be one of [{ZIP}, {IMAGE}]") if image: self.skip_pull_image = True - # Default image tag to be the base image with a tag of 'rapid' instead of latest. # If the image name had a digest, removing the @ so that a valid image name can be constructed # to use for the local invoke image name. - image_repo = image_name.split(":")[0].replace("@", "") - image_tag = f"{image_repo}:{RAPID_IMAGE_TAG_PREFIX}-{version}-{architecture}" + image_repo = base_image.split(":")[0].replace("@", "") + rapid_image = f"{image_repo}:{tag_prefix}{RAPID_IMAGE_TAG_PREFIX}-{architecture}" downloaded_layers = [] if layers and packagetype == ZIP: downloaded_layers = self.layer_downloader.download_all(layers, self.force_image_build) - docker_image_version = self._generate_docker_image_version(downloaded_layers, runtime, architecture) - image_tag = f"{self._SAM_CLI_REPO_NAME}:{docker_image_version}" + docker_image_version = self._generate_docker_image_version(downloaded_layers, runtime_image_tag) + rapid_image = f"{self._SAM_CLI_REPO_NAME}-{docker_image_version}" image_not_found = False # If we are not using layers, build anyways to ensure any updates to rapid get added try: - self.docker_client.images.get(image_tag) + self.docker_client.images.get(rapid_image) + # Check if the base image is up-to-date locally and modify build/pull parameters accordingly + self._check_base_image_is_current(base_image) except docker.errors.ImageNotFound: - LOG.info("Image was not found.") + LOG.info("Local image was not found.") image_not_found = True - # If building a new rapid image, delete older rapid images of the same repo - if image_not_found and image_tag == f"{image_repo}:{RAPID_IMAGE_TAG_PREFIX}-{version}-{architecture}": - self._remove_rapid_images(image_repo) + # If building a new rapid image, delete older rapid images + if image_not_found and rapid_image == f"{image_repo}:{tag_prefix}{RAPID_IMAGE_TAG_PREFIX}-{architecture}": + if tag_prefix: + # ZIP functions with new RAPID format. Delete images from the old ecr/sam repository + self._remove_rapid_images(f"{self._SAM_INVOKE_REPO_PREFIX}-{runtime}") + else: + self._remove_rapid_images(image_repo) if ( self.force_image_build @@ -162,10 +212,10 @@ def build(self, runtime, packagetype, image, layers, architecture, stream=None, stream_writer.write("Building image...") stream_writer.flush() self._build_image( - image if image else image_name, image_tag, downloaded_layers, architecture, stream=stream_writer + image if image else base_image, rapid_image, downloaded_layers, architecture, stream=stream_writer ) - return image_tag + return rapid_image def get_config(self, image_tag): config = {} @@ -176,7 +226,7 @@ def get_config(self, image_tag): return config @staticmethod - def _generate_docker_image_version(layers, runtime, architecture): + def _generate_docker_image_version(layers, runtime_image_tag): """ Generate the Docker TAG that will be used to create the image @@ -185,11 +235,8 @@ def _generate_docker_image_version(layers, runtime, architecture): layers list(samcli.commands.local.lib.provider.Layer) List of the layers - runtime str - Runtime of the image to create - - architecture str - Architecture type either x86_64 or arm64 on AWS lambda + runtime_image_tag str + Runtime version format to generate image name and tag (including architecture, e.g. "python:3.7-x86_64") Returns ------- @@ -204,9 +251,7 @@ def _generate_docker_image_version(layers, runtime, architecture): # same order), SAM CLI will only produce one image and use this image across both functions for invoke. return ( - runtime - + "-" - + architecture + runtime_image_tag + "-" + hashlib.sha256("-".join([layer.name for layer in layers]).encode("utf-8")).hexdigest()[0:25] ) @@ -219,10 +264,14 @@ def _build_image(self, base_image, docker_tag, layers, architecture, stream=None ---------- base_image str Base Image to use for the new image - docker_tag + docker_tag str Docker tag (REPOSITORY:TAG) to use when building the image layers list(samcli.commands.local.lib.provider.Layer) List of Layers to be use to mount in the image + architecture str + Architecture, either x86_64 or arm64 + stream samcli.lib.utils.stream_writer.StreamWriter + Stream to write the build output Raises ------ @@ -289,9 +338,9 @@ def set_item_permission(tar_info): @staticmethod def _generate_dockerfile(base_image, layers, architecture): """ - FROM amazon/aws-sam-cli-emulation-image-python3.6:latest + FROM public.ecr.aws/lambda/python:3.6-x86_64 - ADD init /var/rapid + ADD aws-lambda-rie /var/rapid ADD layer1 /opt ADD layer2 /opt @@ -334,7 +383,7 @@ def _remove_rapid_images(self, repo: str) -> None: try: for image in self.docker_client.images.list(name=repo): for tag in image.tags: - if self.is_rapid_image(tag) and not self.is_image_current(tag): + if self.is_rapid_image(tag) and not self.is_rapid_image_current(tag): try: self.docker_client.images.remove(image.id) except docker.errors.APIError as ex: @@ -348,20 +397,29 @@ def is_rapid_image(image_name: str) -> bool: """ Is the image tagged as a RAPID clone? - : param string image_name: Name of the image - : return bool: True, if the image name ends with rapid-$SAM_CLI_VERSION. False, otherwise + Parameters + ---------- + image_name : str + Name of the image + + Returns + ------- + bool + True if the image tag starts with the rapid prefix or contains it in between. False, otherwise """ try: - return image_name.split(":")[1].startswith(f"{RAPID_IMAGE_TAG_PREFIX}-") + tag = image_name.split(":")[1] + return tag.startswith(f"{RAPID_IMAGE_TAG_PREFIX}-") or f"-{RAPID_IMAGE_TAG_PREFIX}-" in tag except (IndexError, AttributeError): # split() returned 1 or less items or image_name is None return False @staticmethod - def is_image_current(image_name: str) -> bool: + def is_rapid_image_current(image_name: str) -> bool: """ - Verify if an image is current or the latest image for the version of samcli + Verify if an image has the latest format. + The current format doesn't include the SAM version and has the RAPID prefix between dashes. Parameters ---------- @@ -373,4 +431,85 @@ def is_image_current(image_name: str) -> bool: bool return True if it is current and vice versa """ - return bool(f"-{version}" in image_name) + return f"-{RAPID_IMAGE_TAG_PREFIX}-" in image_name + + def _check_base_image_is_current(self, image_name: str) -> None: + """ + Check if the existing base image is up-to-date and update modifier parameters + (skip_pull_image, force_image_build) accordingly, printing an informative + message depending on the case. + + Parameters + ---------- + image_name : str + Base image name to check + """ + + # No need to check if the overriding parameters are already set + if self.skip_pull_image or self.force_image_build: + return + + if self.is_base_image_current(image_name): + self.skip_pull_image = True + LOG.info("Local image is up-to-date") + else: + self.force_image_build = True + LOG.info( + "Local image is out of date and will be updated to the latest runtime. " + "To skip this, pass in the parameter --skip-pull-image" + ) + + def is_base_image_current(self, image_name: str) -> bool: + """ + Return True if the base image is up-to-date with the remote environment by comparing the image digests + + Parameters + ---------- + image_name : str + Base image name to check + + Returns + ------- + bool + True if local image digest is the same as the remote image digest + """ + return self.get_local_image_digest(image_name) == self.get_remote_image_digest(image_name) + + def get_remote_image_digest(self, image_name: str) -> Optional[str]: + """ + Get the digest of the remote version of an image + + Parameters + ---------- + image_name : str + Name of the image to get the digest + + Returns + ------- + str + Image digest, including `sha256:` prefix + """ + remote_info = self.docker_client.images.get_registry_data(image_name) + digest: Optional[str] = remote_info.attrs.get("Descriptor", {}).get("digest") + return digest + + def get_local_image_digest(self, image_name: str) -> Optional[str]: + """ + Get the digest of the local version of an image + + Parameters + ---------- + image_name : str + Name of the image to get the digest + + Returns + ------- + str + Image digest, including `sha256:` prefix + """ + image_info = self.docker_client.images.get(image_name) + full_digest: str = image_info.attrs.get("RepoDigests", [None])[0] + try: + return full_digest.split("@")[1] + except (AttributeError, IndexError): + return None diff --git a/samcli/local/docker/manager.py b/samcli/local/docker/manager.py index f63fe949ce..7b48bb290c 100644 --- a/samcli/local/docker/manager.py +++ b/samcli/local/docker/manager.py @@ -79,7 +79,7 @@ def create(self, container): if is_image_local and self.skip_pull_image: LOG.info("Requested to skip pulling images ...\n") elif image_name.startswith("samcli/lambda") or (is_image_local and LambdaImage.is_rapid_image(image_name)): - LOG.info("Skip pulling image and use local one: %s.\n", image_name) + LOG.info("Using local image: %s.\n", image_name) else: try: self.pull_image(image_name) diff --git a/samcli/local/lambdafn/config.py b/samcli/local/lambdafn/config.py index 415d6af004..d9e30342fd 100644 --- a/samcli/local/lambdafn/config.py +++ b/samcli/local/lambdafn/config.py @@ -28,6 +28,7 @@ def __init__( architecture, memory=None, timeout=None, + runtime_management_config=None, env_vars=None, ): """ @@ -57,6 +58,8 @@ def __init__( Function memory limit in MB, by default None timeout : int, optional Function timeout in seconds, by default None + runtime_management_config: str, optional + Function's runtime management config env_vars : str, optional Environment variables, by default None If it not provided, this class will generate one for you based on the function properties @@ -80,6 +83,7 @@ def __init__( self.architecture = architecture self.timeout = timeout or self._DEFAULT_TIMEOUT_SECONDS + self.runtime_management_config = runtime_management_config if not isinstance(self.timeout, int): try: diff --git a/samcli/local/lambdafn/runtime.py b/samcli/local/lambdafn/runtime.py index ebb09df01a..f9a43589ae 100644 --- a/samcli/local/lambdafn/runtime.py +++ b/samcli/local/lambdafn/runtime.py @@ -70,6 +70,17 @@ def create(self, function_config, debug_context=None, container_host=None, conta code_dir = self._get_code_dir(function_config.code_abs_path) layers = [self._unarchived_layer(layer) for layer in function_config.layers] + if function_config.runtime_management_config and function_config.runtime_management_config.get( + "RuntimeVersionArn" + ): + sam_accelerate_link = "https://s12d.com/accelerate" + LOG.info( + "This function will be invoked using the latest available runtime, which may differ from your " + "Runtime Management Configuration. To test this function with a pinned runtime, test on AWS with " + "`sam sync -–help`. Learn more here: %s", + sam_accelerate_link, + ) + container = LambdaContainer( function_config.runtime, function_config.imageuri, diff --git a/tests/integration/local/invoke/test_integration_cli_images.py b/tests/integration/local/invoke/test_integration_cli_images.py index 8453f2a040..58e15b632e 100644 --- a/tests/integration/local/invoke/test_integration_cli_images.py +++ b/tests/integration/local/invoke/test_integration_cli_images.py @@ -46,7 +46,7 @@ def setUpClass(cls): def tearDownClass(cls): try: cls.client.api.remove_image(cls.docker_tag) - cls.client.api.remove_image(f"{cls.image_name}:{RAPID_IMAGE_TAG_PREFIX}-{version}") + cls.client.api.remove_image(f"{cls.image_name}:{RAPID_IMAGE_TAG_PREFIX}-{X86_64}") except APIError: pass @@ -454,7 +454,7 @@ def setUp(self): path=self.test_data_invoke_path, dockerfile="Dockerfile", tag=tag, decode=True, nocache=True ): print(log) - self.new_rapid_image_tag = f"{self.repo}:{RAPID_IMAGE_TAG_PREFIX}-{version}-{X86_64}" + self.new_rapid_image_tag = f"{self.repo}:{RAPID_IMAGE_TAG_PREFIX}-{X86_64}" def tearDown(self): for tag in self.old_rapid_image_tags + [self.new_rapid_image_tag] + self.other_repo_tags: diff --git a/tests/unit/commands/local/lib/test_local_lambda.py b/tests/unit/commands/local/lib/test_local_lambda.py index 88dbafde2c..9aa0f0837f 100644 --- a/tests/unit/commands/local/lib/test_local_lambda.py +++ b/tests/unit/commands/local/lib/test_local_lambda.py @@ -18,7 +18,6 @@ OverridesNotWellDefinedError, NoPrivilegeException, InvalidIntermediateImageError, - UnsupportedRuntimeArchitectureError, ) @@ -251,6 +250,7 @@ def test_must_work_with_override_values( architectures=[X86_64], codesign_config_arn=None, function_url_config=None, + runtime_management_config=None, ) self.local_lambda.env_vars_values = env_vars_values @@ -303,6 +303,7 @@ def test_must_not_work_with_invalid_override_values(self, env_vars_values, expec architectures=[X86_64], codesign_config_arn=None, function_url_config=None, + runtime_management_config=None, ) self.local_lambda.env_vars_values = env_vars_values @@ -345,6 +346,7 @@ def test_must_work_with_invalid_environment_variable(self, environment_variable, architectures=[X86_64], codesign_config_arn=None, function_url_config=None, + runtime_management_config=None, ) self.local_lambda.env_vars_values = {} @@ -423,6 +425,7 @@ def test_must_work(self, FunctionConfigMock, is_debugging_mock, resolve_code_pat architectures=[ARM64], codesign_config_arn=None, function_url_config=None, + runtime_management_config=None, ) config = "someconfig" @@ -444,6 +447,7 @@ def test_must_work(self, FunctionConfigMock, is_debugging_mock, resolve_code_pat env_vars=env_vars, architecture=ARM64, full_path=function.full_path, + runtime_management_config=function.runtime_management_config, ) resolve_code_path_patch.assert_called_with(self.cwd, function.codeuri) @@ -489,6 +493,7 @@ def test_timeout_set_to_max_during_debugging( architectures=[X86_64], function_url_config=None, codesign_config_arn=None, + runtime_management_config=None, ) config = "someconfig" @@ -510,6 +515,7 @@ def test_timeout_set_to_max_during_debugging( env_vars=env_vars, architecture=X86_64, full_path=function.full_path, + runtime_management_config=function.runtime_management_config, ) resolve_code_path_patch.assert_called_with(self.cwd, "codeuri") diff --git a/tests/unit/commands/local/lib/test_provider.py b/tests/unit/commands/local/lib/test_provider.py index 6e544566f2..98e4a8099b 100644 --- a/tests/unit/commands/local/lib/test_provider.py +++ b/tests/unit/commands/local/lib/test_provider.py @@ -294,6 +294,7 @@ def setUp(self) -> None: [ARM64], None, "stackpath", + None, ) @parameterized.expand( 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 855fb8f9e9..04d86cc0cc 100644 --- a/tests/unit/commands/local/lib/test_sam_function_provider.py +++ b/tests/unit/commands/local/lib/test_sam_function_provider.py @@ -106,6 +106,18 @@ class TestSamFunctionProviderEndToEnd(TestCase): }, "Metadata": {"DockerTag": "tag", "DockerContext": "./image", "Dockerfile": "Dockerfile"}, }, + "SamFuncWithRuntimeManagementConfig": { + "Type": "AWS::Serverless::Function", + "Properties": { + "CodeUri": "/usr/foo/bar", + "Runtime": "python3.9", + "Handler": "index.handler", + "RuntimeManagementConfig": { + "UpdateRuntimeOn": "Manual", + "RuntimeVersionArn": "arn:aws:lambda:us-east-1::runtime:python3.9::0af1966588ced06e3143ae720245c9b7aeaae213c6921c12c742a166679cc505", + }, + }, + }, "LambdaFunc1": { "Type": "AWS::Lambda::Function", "Properties": { @@ -466,6 +478,36 @@ def setUp(self): stack_path="", ), ), + ( + "SamFuncWithRuntimeManagementConfig", + Function( + function_id="SamFuncWithRuntimeManagementConfig", + name="SamFuncWithRuntimeManagementConfig", + functionname="SamFuncWithRuntimeManagementConfig", + runtime="python3.9", + handler="index.handler", + codeuri="/usr/foo/bar", + memory=None, + timeout=None, + environment=None, + rolearn=None, + layers=[], + events=None, + metadata={"SamResourceId": "SamFuncWithRuntimeManagementConfig"}, + inlinecode=None, + imageuri=None, + imageconfig=None, + packagetype=ZIP, + codesign_config_arn=None, + architectures=None, + function_url_config=None, + stack_path="", + runtime_management_config={ + "UpdateRuntimeOn": "Manual", + "RuntimeVersionArn": "arn:aws:lambda:us-east-1::runtime:python3.9::0af1966588ced06e3143ae720245c9b7aeaae213c6921c12c742a166679cc505", + }, + ), + ), ("LambdaFunc1", None), # codeuri is a s3 location, ignored ( "LambdaFuncWithImage1", @@ -983,6 +1025,7 @@ def test_get_all_must_return_all_functions(self): "SamFuncWithImage4", "SamFuncWithInlineCode", "SamFuncWithFunctionNameOverride", + "SamFuncWithRuntimeManagementConfig", "LambdaFuncWithImage1", "LambdaFuncWithImage2", "LambdaFuncWithImage4", diff --git a/tests/unit/local/docker/test_lambda_container.py b/tests/unit/local/docker/test_lambda_container.py index 3d69c3d6f7..8e01fb01b6 100644 --- a/tests/unit/local/docker/test_lambda_container.py +++ b/tests/unit/local/docker/test_lambda_container.py @@ -395,10 +395,10 @@ def test_must_configure_container_properly_image_with_imageconfig_no_debug( code_dir=self.code_dir, layers=[], lambda_image=image_builder_mock, + architecture="x86_64", env_vars=self.env_var, memory_mb=self.memory_mb, debug_options=self.debug_options, - architecture="x86_64", function_full_path=self.function_name, ) @@ -482,7 +482,7 @@ def test_must_skip_if_port_is_not_given(self): class TestLambdaContainer_get_image(TestCase): def test_must_return_build_image(self): - expected = f"public.ecr.aws/sam/emulation-foo:{RAPID_IMAGE_TAG_PREFIX}-x.y.z" + expected = f"public.ecr.aws/lambda/foo:1.0-{RAPID_IMAGE_TAG_PREFIX}-x.y.z" image_builder = Mock() image_builder.build.return_value = expected @@ -490,17 +490,17 @@ def test_must_return_build_image(self): self.assertEqual( LambdaContainer._get_image( lambda_image=image_builder, - runtime="foo", + runtime="foo1.0", packagetype=ZIP, image=None, layers=[], - architecture="arm64", function_name=None, + architecture="x86_64", ), expected, ) - image_builder.build.assert_called_with("foo", ZIP, None, [], "arm64", function_name=None) + image_builder.build.assert_called_with("foo1.0", ZIP, None, [], "x86_64", function_name=None) class TestLambdaContainer_get_debug_settings(TestCase): diff --git a/tests/unit/local/docker/test_lambda_image.py b/tests/unit/local/docker/test_lambda_image.py index e026e6fdc4..9e627ff4a6 100644 --- a/tests/unit/local/docker/test_lambda_image.py +++ b/tests/unit/local/docker/test_lambda_image.py @@ -3,6 +3,7 @@ from unittest import TestCase from unittest.mock import patch, Mock, mock_open, ANY, call +from parameterized import parameterized from docker.errors import ImageNotFound, BuildError, APIError from parameterized import parameterized @@ -10,11 +11,37 @@ from samcli.commands.local.lib.exceptions import InvalidIntermediateImageError from samcli.lib.utils.packagetype import ZIP, IMAGE from samcli.lib.utils.architecture import ARM64, X86_64 -from samcli.local.docker.lambda_image import LambdaImage, RAPID_IMAGE_TAG_PREFIX +from samcli.local.docker.lambda_image import LambdaImage, RAPID_IMAGE_TAG_PREFIX, Runtime from samcli.commands.local.cli_common.user_exceptions import ImageBuildException from samcli import __version__ as version +class TestRuntime(TestCase): + @parameterized.expand( + [ + ("nodejs12.x", "nodejs:12-x86_64"), + ("nodejs14.x", "nodejs:14-x86_64"), + ("nodejs16.x", "nodejs:16-x86_64"), + ("python2.7", "python:2.7"), + ("python3.6", "python:3.6"), + ("python3.7", "python:3.7"), + ("python3.8", "python:3.8-x86_64"), + ("python3.9", "python:3.9-x86_64"), + ("ruby2.7", "ruby:2.7-x86_64"), + ("java8", "java:8"), + ("java8.al2", "java:8.al2-x86_64"), + ("java11", "java:11-x86_64"), + ("go1.x", "go:1"), + ("dotnet6", "dotnet:6-x86_64"), + ("dotnetcore3.1", "dotnet:core3.1-x86_64"), + ("provided", "provided:alami"), + ("provided.al2", "provided:al2-x86_64"), + ] + ) + def test_image_name_tag(self, runtime, image_tag): + self.assertEqual(Runtime.get_image_name_tag(runtime, "x86_64"), image_tag) + + class TestLambdaImage(TestCase): def setUp(self): self.layer_cache_dir = tempfile.gettempdir() @@ -52,7 +79,7 @@ def test_building_image_with_no_runtime_only_image(self): self.assertEqual( lambda_image.build(None, IMAGE, "mylambdaimage:v1", [], X86_64), - f"mylambdaimage:{RAPID_IMAGE_TAG_PREFIX}-{version}-x86_64", + f"mylambdaimage:{RAPID_IMAGE_TAG_PREFIX}-{X86_64}", ) @patch("samcli.local.docker.lambda_image.LambdaImage._build_image") @@ -74,13 +101,13 @@ def test_building_image_with_no_runtime_only_image_always_build( self.assertEqual( lambda_image.build(None, IMAGE, "mylambdaimage:v1", ["mylayer"], X86_64, function_name="function"), - f"mylambdaimage:{RAPID_IMAGE_TAG_PREFIX}-{version}-x86_64", + f"mylambdaimage:{RAPID_IMAGE_TAG_PREFIX}-x86_64", ) # No layers are added, because runtime is not defined. build_image_patch.assert_called_once_with( "mylambdaimage:v1", - f"mylambdaimage:{RAPID_IMAGE_TAG_PREFIX}-{version}-x86_64", + f"mylambdaimage:{RAPID_IMAGE_TAG_PREFIX}-x86_64", [], X86_64, stream=ANY, @@ -123,24 +150,56 @@ def test_building_image_with_non_accepted_package_type(self): with self.assertRaises(InvalidIntermediateImageError): lambda_image.build("python3.6", None, None, [], X86_64, function_name="function") - def test_building_image_with_no_layers(self): + @patch("samcli.local.docker.lambda_image.LambdaImage.is_base_image_current") + @patch("samcli.local.docker.lambda_image.LambdaImage._build_image") + def test_building_image_with_no_layers(self, build_image_patch, is_base_image_current_patch): + docker_client_mock = Mock() + layer_downloader_mock = Mock() + stream = Mock() + docker_client_mock.api.build.return_value = ["mock"] + + is_base_image_current_patch.return_value = False + + lambda_image = LambdaImage(layer_downloader_mock, False, False, docker_client=docker_client_mock) + + self.assertEqual( + lambda_image.build("python3.6", ZIP, None, [], ARM64, stream=stream), + f"public.ecr.aws/lambda/python:3.6-{RAPID_IMAGE_TAG_PREFIX}-arm64", + ) + + build_image_patch.assert_called_once_with( + "public.ecr.aws/lambda/python:3.6", + f"public.ecr.aws/lambda/python:3.6-{RAPID_IMAGE_TAG_PREFIX}-arm64", + [], + ARM64, + stream=stream, + ) + + @patch("samcli.local.docker.lambda_image.LambdaImage.is_base_image_current") + @patch("samcli.local.docker.lambda_image.LambdaImage._build_image") + def test_not_building_image_with_no_layers_if_up_to_date(self, build_image_patch, is_base_image_current_patch): docker_client_mock = Mock() layer_downloader_mock = Mock() setattr(layer_downloader_mock, "layer_cache", self.layer_cache_dir) docker_client_mock.api.build.return_value = ["mock"] + is_base_image_current_patch.return_value = True + lambda_image = LambdaImage(layer_downloader_mock, False, False, docker_client=docker_client_mock) self.assertEqual( lambda_image.build("python3.6", ZIP, None, [], ARM64, function_name="function"), - f"public.ecr.aws/sam/emulation-python3.6:{RAPID_IMAGE_TAG_PREFIX}-{version}-arm64", + f"public.ecr.aws/lambda/python:3.6-{RAPID_IMAGE_TAG_PREFIX}-{ARM64}", ) - def test_building_image_with_custom_image_uri(self): + @patch("samcli.local.docker.lambda_image.LambdaImage.is_base_image_current") + @patch("samcli.local.docker.lambda_image.LambdaImage._build_image") + def test_building_image_with_custom_image_uri(self, build_image_patch, is_base_image_current_patch): docker_client_mock = Mock() layer_downloader_mock = Mock() setattr(layer_downloader_mock, "layer_cache", self.layer_cache_dir) docker_client_mock.api.build.return_value = ["mock"] + is_base_image_current_patch.return_value = True lambda_image = LambdaImage( layer_downloader_mock, @@ -154,23 +213,28 @@ def test_building_image_with_custom_image_uri(self): ) self.assertEqual( lambda_image.build("python3.6", ZIP, None, [], X86_64, function_name="Function1"), - f"amazon/aws-sam-cli-emulation-image2-python3.6:{RAPID_IMAGE_TAG_PREFIX}-{version}-x86_64", + f"amazon/aws-sam-cli-emulation-image2-python3.6:{RAPID_IMAGE_TAG_PREFIX}-x86_64", ) self.assertEqual( lambda_image.build("python3.6", ZIP, None, [], X86_64, function_name="Function2"), - f"amazon/aws-sam-cli-emulation-image-python3.6:{RAPID_IMAGE_TAG_PREFIX}-{version}-x86_64", + f"amazon/aws-sam-cli-emulation-image-python3.6:{RAPID_IMAGE_TAG_PREFIX}-x86_64", ) + build_image_patch.assert_not_called() + @patch("samcli.local.docker.lambda_image.LambdaImage.is_base_image_current") @patch("samcli.local.docker.lambda_image.LambdaImage._build_image") @patch("samcli.local.docker.lambda_image.LambdaImage._generate_docker_image_version") - def test_not_building_image_that_already_exists(self, generate_docker_image_version_patch, build_image_patch): + def test_not_building_image_that_is_up_to_date( + self, generate_docker_image_version_patch, build_image_patch, is_base_image_current_patch + ): layer_downloader_mock = Mock() layer_mock = Mock() layer_mock.name = "layers1" layer_mock.is_defined_within_template = False layer_downloader_mock.download_all.return_value = [layer_mock] - generate_docker_image_version_patch.return_value = "image-version" + generate_docker_image_version_patch.return_value = "runtime:image-version" + is_base_image_current_patch.return_value = True docker_client_mock = Mock() docker_client_mock.images.get.return_value = Mock() @@ -178,28 +242,28 @@ def test_not_building_image_that_already_exists(self, generate_docker_image_vers lambda_image = LambdaImage(layer_downloader_mock, False, False, docker_client=docker_client_mock) actual_image_id = lambda_image.build("python3.6", ZIP, None, [layer_mock], X86_64, function_name="function") - self.assertEqual(actual_image_id, "samcli/lambda:image-version") + self.assertEqual(actual_image_id, "samcli/lambda-runtime:image-version") layer_downloader_mock.download_all.assert_called_once_with([layer_mock], False) - generate_docker_image_version_patch.assert_called_once_with([layer_mock], "python3.6", X86_64) - docker_client_mock.images.get.assert_called_once_with("samcli/lambda:image-version") + generate_docker_image_version_patch.assert_called_once_with([layer_mock], "python:3.6") + docker_client_mock.images.get.assert_called_once_with("samcli/lambda-runtime:image-version") build_image_patch.assert_not_called() @parameterized.expand( [ - ("python3.6", "public.ecr.aws/sam/emulation-python3.6:latest"), - ("python3.8", "public.ecr.aws/sam/emulation-python3.8:latest-x86_64"), + ("python3.6", "python:3.6", "public.ecr.aws/lambda/python:3.6"), + ("python3.8", "python:3.8-x86_64", "public.ecr.aws/lambda/python:3.8-x86_64"), ] ) @patch("samcli.local.docker.lambda_image.LambdaImage._build_image") @patch("samcli.local.docker.lambda_image.LambdaImage._generate_docker_image_version") def test_force_building_image_that_doesnt_already_exists( - self, runtime, image_name, generate_docker_image_version_patch, build_image_patch + self, runtime, image_suffix, image_name, generate_docker_image_version_patch, build_image_patch ): layer_downloader_mock = Mock() layer_downloader_mock.download_all.return_value = ["layers1"] - generate_docker_image_version_patch.return_value = "image-version" + generate_docker_image_version_patch.return_value = "runtime:image-version" docker_client_mock = Mock() docker_client_mock.images.get.side_effect = ImageNotFound("image not found") @@ -212,14 +276,14 @@ def test_force_building_image_that_doesnt_already_exists( runtime, ZIP, None, ["layers1"], X86_64, stream=stream, function_name="function" ) - self.assertEqual(actual_image_id, "samcli/lambda:image-version") + self.assertEqual(actual_image_id, "samcli/lambda-runtime:image-version") layer_downloader_mock.download_all.assert_called_once_with(["layers1"], True) - generate_docker_image_version_patch.assert_called_once_with(["layers1"], runtime, X86_64) - docker_client_mock.images.get.assert_called_once_with("samcli/lambda:image-version") + generate_docker_image_version_patch.assert_called_once_with(["layers1"], f"{image_suffix}") + docker_client_mock.images.get.assert_called_once_with("samcli/lambda-runtime:image-version") build_image_patch.assert_called_once_with( image_name, - "samcli/lambda:image-version", + "samcli/lambda-runtime:image-version", ["layers1"], X86_64, stream=stream, @@ -227,19 +291,19 @@ def test_force_building_image_that_doesnt_already_exists( @parameterized.expand( [ - ("python3.6", "public.ecr.aws/sam/emulation-python3.6:latest"), - ("python3.8", "public.ecr.aws/sam/emulation-python3.8:latest-arm64"), + ("python3.6", "python:3.6", "public.ecr.aws/lambda/python:3.6"), + ("python3.8", "python:3.8-arm64", "public.ecr.aws/lambda/python:3.8-arm64"), ] ) @patch("samcli.local.docker.lambda_image.LambdaImage._build_image") @patch("samcli.local.docker.lambda_image.LambdaImage._generate_docker_image_version") def test_not_force_building_image_that_doesnt_already_exists( - self, runtime, image_name, generate_docker_image_version_patch, build_image_patch + self, runtime, image_suffix, image_name, generate_docker_image_version_patch, build_image_patch ): layer_downloader_mock = Mock() layer_downloader_mock.download_all.return_value = ["layers1"] - generate_docker_image_version_patch.return_value = "image-version" + generate_docker_image_version_patch.return_value = "runtime:image-version" docker_client_mock = Mock() docker_client_mock.images.get.side_effect = ImageNotFound("image not found") @@ -252,14 +316,14 @@ def test_not_force_building_image_that_doesnt_already_exists( runtime, ZIP, None, ["layers1"], ARM64, stream=stream, function_name="function" ) - self.assertEqual(actual_image_id, "samcli/lambda:image-version") + self.assertEqual(actual_image_id, "samcli/lambda-runtime:image-version") layer_downloader_mock.download_all.assert_called_once_with(["layers1"], False) - generate_docker_image_version_patch.assert_called_once_with(["layers1"], runtime, ARM64) - docker_client_mock.images.get.assert_called_once_with("samcli/lambda:image-version") + generate_docker_image_version_patch.assert_called_once_with(["layers1"], f"{image_suffix}") + docker_client_mock.images.get.assert_called_once_with("samcli/lambda-runtime:image-version") build_image_patch.assert_called_once_with( image_name, - "samcli/lambda:image-version", + "samcli/lambda-runtime:image-version", ["layers1"], ARM64, stream=stream, @@ -274,9 +338,9 @@ def test_generate_docker_image_version(self, hashlib_patch): layer_mock = Mock() layer_mock.name = "layer1" - image_version = LambdaImage._generate_docker_image_version([layer_mock], "runtime", ARM64) + image_version = LambdaImage._generate_docker_image_version([layer_mock], "runtime:1-arm64") - self.assertEqual(image_version, "runtime-arm64-thisisahexdigestofshahash") + self.assertEqual(image_version, "runtime:1-arm64-thisisahexdigestofshahash") hashlib_patch.sha256.assert_called_once_with(b"layer1") @@ -496,16 +560,19 @@ def test_build_image_fails_with_ApiError( docker_full_path_mock.unlink.assert_called_once() def test_building_new_rapid_image_removes_old_rapid_images(self): - repo = "public.ecr.aws/sam/emulation-python3.6" + old_repo = "public.ecr.aws/sam/emulation-python3.8" + repo = "public.ecr.aws/lambda/python:3.8" docker_client_mock = Mock() docker_client_mock.api.build.return_value = ["mock"] docker_client_mock.images.get.side_effect = ImageNotFound("image not found") docker_client_mock.images.list.return_value = [ - Mock(id="old1", tags=[f"{repo}:{RAPID_IMAGE_TAG_PREFIX}-0.00.01-x86_64"]), - Mock(id="old2", tags=[f"{repo}:{RAPID_IMAGE_TAG_PREFIX}-0.00.02-arm64"]), - Mock(id="old3", tags=[f"{repo}:{RAPID_IMAGE_TAG_PREFIX}-{version}-arm64"]), - Mock(id="old4", tags=[f"{repo}:{RAPID_IMAGE_TAG_PREFIX}-{version}"]), - Mock(id="old5", tags=[f"{repo}:{RAPID_IMAGE_TAG_PREFIX}-0.00.05-arm64"]), + Mock(id="old1", tags=[f"{old_repo}:{RAPID_IMAGE_TAG_PREFIX}-0.00.01"]), + Mock(id="old2", tags=[f"{old_repo}:{RAPID_IMAGE_TAG_PREFIX}-0.00.02-arm64"]), + Mock(id="old3", tags=[f"{old_repo}:{RAPID_IMAGE_TAG_PREFIX}-0.00.03-arm64"]), + Mock( + id="old4", + tags=[f"{old_repo}:{RAPID_IMAGE_TAG_PREFIX}-0.00.04-arm64"], + ), ] layer_downloader_mock = Mock() @@ -514,64 +581,157 @@ def test_building_new_rapid_image_removes_old_rapid_images(self): lambda_image = LambdaImage(layer_downloader_mock, False, False, docker_client=docker_client_mock) self.assertEqual( - lambda_image.build("python3.6", ZIP, None, [], X86_64, function_name="function"), - f"{repo}:{RAPID_IMAGE_TAG_PREFIX}-{version}-x86_64", + lambda_image.build("python3.8", ZIP, None, [], X86_64, function_name="function"), + f"{repo}-{RAPID_IMAGE_TAG_PREFIX}-x86_64", ) docker_client_mock.images.remove.assert_has_calls( [ call("old1"), call("old2"), - call("old5"), + call("old3"), + call("old4"), + ] + ) + + def test_building_new_rapid_image_removes_old_rapid_images_for_image_function(self): + image_name = "custom_image_name" + docker_client_mock = Mock() + docker_client_mock.api.build.return_value = ["mock"] + docker_client_mock.images.get.side_effect = ImageNotFound("image not found") + docker_client_mock.images.list.return_value = [ + Mock(id="old1", tags=[f"{image_name}:{RAPID_IMAGE_TAG_PREFIX}-0.00.01"]), + Mock(id="old2", tags=[f"{image_name}:{RAPID_IMAGE_TAG_PREFIX}-0.00.02-x86_64"]), + Mock(id="old3", tags=[f"{image_name}:{RAPID_IMAGE_TAG_PREFIX}-{version}-x86_64"]), + ] + + layer_downloader_mock = Mock() + setattr(layer_downloader_mock, "layer_cache", self.layer_cache_dir) + + lambda_image = LambdaImage(layer_downloader_mock, False, False, docker_client=docker_client_mock) + + self.assertEqual( + lambda_image.build(None, IMAGE, f"{image_name}:image-tag", [], X86_64), + f"{image_name}:{RAPID_IMAGE_TAG_PREFIX}-x86_64", + ) + + docker_client_mock.images.list.assert_called_once() + docker_client_mock.images.remove.assert_has_calls( + [ + call("old1"), + call("old2"), + call("old3"), ] ) def test_building_existing_rapid_image_does_not_remove_old_rapid_images(self): - repo = "public.ecr.aws/sam/emulation-python3.6" + old_repo = "public.ecr.aws/sam/emulation-python3.8" + repo = "public.ecr.aws/lambda/python:3.8" docker_client_mock = Mock() docker_client_mock.api.build.return_value = ["mock"] docker_client_mock.images.list.return_value = [ - Mock(id="old1", tags=[f"{repo}:{RAPID_IMAGE_TAG_PREFIX}-0.00.01-x86_64"]), - Mock(id="old2", tags=[f"{repo}:{RAPID_IMAGE_TAG_PREFIX}-0.00.02-arm64"]), + Mock(id="old1", tags=[f"{old_repo}:{RAPID_IMAGE_TAG_PREFIX}-0.00.01-x86_64"]), + Mock(id="old1", tags=[f"{old_repo}:{RAPID_IMAGE_TAG_PREFIX}-0.00.02-arm64"]), ] layer_downloader_mock = Mock() setattr(layer_downloader_mock, "layer_cache", self.layer_cache_dir) lambda_image = LambdaImage(layer_downloader_mock, False, False, docker_client=docker_client_mock) + lambda_image.is_base_image_current = Mock(return_value=True) self.assertEqual( - lambda_image.build("python3.6", ZIP, None, [], X86_64, function_name="function"), - f"{repo}:{RAPID_IMAGE_TAG_PREFIX}-{version}-x86_64", + lambda_image.build("python3.8", ZIP, None, [], X86_64, function_name="function"), + f"{repo}-{RAPID_IMAGE_TAG_PREFIX}-x86_64", ) docker_client_mock.images.remove.assert_not_called() - def test_is_rapid_image(self): - self.assertFalse(LambdaImage.is_rapid_image(None)) - self.assertFalse(LambdaImage.is_rapid_image("")) - self.assertFalse(LambdaImage.is_rapid_image("my_repo")) - self.assertFalse(LambdaImage.is_rapid_image("my_repo:tag")) - self.assertFalse(LambdaImage.is_rapid_image("public.ecr.aws/lambda/python:3.9")) - self.assertFalse(LambdaImage.is_rapid_image("public.ecr.aws/sam/emulation-python3.6:latest")) - - self.assertTrue(LambdaImage.is_rapid_image("my_repo:rapid-1.29.0")) - self.assertTrue(LambdaImage.is_rapid_image("my_repo:rapid-1.29.0beta")) - self.assertTrue(LambdaImage.is_rapid_image("my_repo:rapid-1.29.0-x86_64")) - self.assertTrue( - LambdaImage.is_rapid_image(f"public.ecr.aws/sam/emulation-python3.6:{RAPID_IMAGE_TAG_PREFIX}-1.23.0") - ) + @parameterized.expand( + [ + (None, False), + ("", False), + ("my_repo", False), + ("my_repo:tag", False), + ("my_repo:rapid-1.29beta", True), + ("public.ecr.aws/lambda/python:3.9", False), + ("public.ecr.aws/sam/emulation-python3.6:latest", False), + ("public.ecr.aws/sam/emulation-python3.6:rapid", False), + ("public.ecr.aws/sam/emulation-python3.6:rapid-1.29.0", True), + ("public.ecr.aws/lambda/python:3.6-rapid-arm64", True), + ("public.ecr.aws/lambda/python:3.8.v1-rapid-x86_64", True), + ("public.ecr.aws/lambda/java:11-rapid-x86_64", True), + ("public.ecr.aws/lambda/python:3.8", False), + ("public.ecr.aws/lambda/latest", False), + ] + ) + def test_is_rapid_image(self, image_name, is_rapid): + self.assertEqual(LambdaImage.is_rapid_image(image_name), is_rapid) - def test_is_image_current(self): - self.assertTrue(LambdaImage.is_image_current(f"my_repo:rapid-{version}")) - self.assertTrue(LambdaImage.is_image_current(f"my_repo:rapid-{version}beta")) - self.assertTrue(LambdaImage.is_image_current(f"my_repo:rapid-{version}-x86_64")) - self.assertTrue( - LambdaImage.is_image_current(f"public.ecr.aws/sam/emulation-python3.6:{RAPID_IMAGE_TAG_PREFIX}-{version}") + @parameterized.expand( + [ + (f"my_repo:rapid-{version}", False), + (f"my_repo:rapid-{version}beta", False), + (f"my_repo:rapid-{version}-x86_64", False), + (f"public.ecr.aws/sam/emulation-python3.6:{RAPID_IMAGE_TAG_PREFIX}", False), + (f"public.ecr.aws/lambda/python:3.9-{RAPID_IMAGE_TAG_PREFIX}-x86_64", True), + ("my_repo:rapid-1.230.0", False), + ("my_repo:rapid-1.204.0beta", False), + ("my_repo:rapid-0.00.02-x86_64", False), + (f"public.ecr.aws/sam/emulation-python3.6:{RAPID_IMAGE_TAG_PREFIX}-0.01.01.01", False), + ] + ) + def test_is_rapid_image_current(self, image_name, is_current): + self.assertEqual(LambdaImage.is_rapid_image_current(image_name), is_current) + + def test_get_remote_image_digest(self): + docker_client_mock = Mock() + registry_data = Mock( + attrs={ + "Descriptor": {"digest": "sha256:remote-digest"}, + }, ) - self.assertFalse(LambdaImage.is_image_current("my_repo:rapid-1.230.0")) - self.assertFalse(LambdaImage.is_image_current("my_repo:rapid-1.204.0beta")) - self.assertFalse(LambdaImage.is_image_current("my_repo:rapid-0.00.02-x86_64")) - self.assertFalse( - LambdaImage.is_image_current(f"public.ecr.aws/sam/emulation-python3.6:{RAPID_IMAGE_TAG_PREFIX}-0.01.01.01") + docker_client_mock.images.get_registry_data.return_value = registry_data + lambda_image = LambdaImage("layer_downloader", False, False, docker_client=docker_client_mock) + self.assertEqual("sha256:remote-digest", lambda_image.get_remote_image_digest("image_name")) + + def test_get_local_image_digest(self): + docker_client_mock = Mock() + local_image_data = Mock( + attrs={ + "RepoDigests": ["image_name@sha256:local-digest"], + }, ) + docker_client_mock.images.get.return_value = local_image_data + lambda_image = LambdaImage("layer_downloader", False, False, docker_client=docker_client_mock) + self.assertEqual("sha256:local-digest", lambda_image.get_local_image_digest("image_name")) + + @parameterized.expand( + [ + ("same-digest", "same-digest", True), + ("one-digest", "another-digest", False), + ] + ) + def test_is_base_image_current(self, local_digest, remote_digest, expected_image_current): + lambda_image = LambdaImage("layer_downloader", False, False, docker_client=Mock()) + lambda_image.get_local_image_digest = Mock(return_value=local_digest) + lambda_image.get_remote_image_digest = Mock(return_value=remote_digest) + self.assertEqual(lambda_image.is_base_image_current("image_name"), expected_image_current) + + @parameterized.expand( + [ + (True, True, False), # It's up-to-date => skip_pull_image: True + (False, False, True), # It needs to be updated => force_image_build: True + ] + ) + def test_check_base_image_is_current( + self, + is_base_image_current, + expected_skip_pull_image, + expected_force_image_build, + ): + lambda_image = LambdaImage("layer_downloader", False, False, docker_client=Mock()) + lambda_image.is_base_image_current = Mock(return_value=is_base_image_current) + lambda_image._check_base_image_is_current("image_name") + self.assertEqual(lambda_image.skip_pull_image, expected_skip_pull_image) + self.assertEqual(lambda_image.force_image_build, expected_force_image_build) diff --git a/tests/unit/local/docker/test_manager.py b/tests/unit/local/docker/test_manager.py index 7e567abfcf..21faebe56e 100644 --- a/tests/unit/local/docker/test_manager.py +++ b/tests/unit/local/docker/test_manager.py @@ -106,7 +106,7 @@ def test_must_not_pull_image_if_image_is_samcli_lambda_image(self): def test_must_not_pull_image_if_image_is_rapid_image(self): input_data = "input data" - rapid_image_name = f"Mock_image_name:{RAPID_IMAGE_TAG_PREFIX}-1.0.0" + rapid_image_name = f"Mock_image_name/python:3.6-{RAPID_IMAGE_TAG_PREFIX}-x86_64" self.manager.has_image = Mock() self.manager.pull_image = Mock() diff --git a/tests/unit/local/lambdafn/test_config.py b/tests/unit/local/lambdafn/test_config.py index 47f644ab4d..68e1fc7805 100644 --- a/tests/unit/local/lambdafn/test_config.py +++ b/tests/unit/local/lambdafn/test_config.py @@ -27,6 +27,7 @@ def setUp(self): self.env_vars_mock = Mock() self.layers = ["layer1"] self.architecture = "arm64" + self.runtime_management_config = {"key": "value"} def test_init_with_env_vars(self): config = FunctionConfig( @@ -43,6 +44,7 @@ def test_init_with_env_vars(self): memory=self.memory, timeout=self.timeout, env_vars=self.env_vars_mock, + runtime_management_config=self.runtime_management_config, ) self.assertEqual(config.name, self.name) @@ -62,6 +64,8 @@ def test_init_with_env_vars(self): self.assertEqual(self.env_vars_mock.memory, self.memory) self.assertEqual(self.env_vars_mock.timeout, self.timeout) + self.assertEqual(config.runtime_management_config, self.runtime_management_config) + def test_init_without_optional_values(self): config = FunctionConfig( self.name, @@ -92,6 +96,7 @@ def test_init_without_optional_values(self): self.assertEqual(config.env_vars.handler, self.handler) self.assertEqual(config.env_vars.memory, self.DEFAULT_MEMORY) self.assertEqual(config.env_vars.timeout, self.DEFAULT_TIMEOUT) + self.assertEqual(config.runtime_management_config, None) def test_init_with_timeout_of_int_string(self): config = FunctionConfig( diff --git a/tests/unit/local/lambdafn/test_runtime.py b/tests/unit/local/lambdafn/test_runtime.py index b7f421edad..9ed73bedd1 100644 --- a/tests/unit/local/lambdafn/test_runtime.py +++ b/tests/unit/local/lambdafn/test_runtime.py @@ -49,8 +49,9 @@ def setUp(self): self.env_var_value = {"a": "b"} self.env_vars.resolve.return_value = self.env_var_value + @patch("samcli.local.lambdafn.runtime.LOG") @patch("samcli.local.lambdafn.runtime.LambdaContainer") - def test_must_create_lambda_container(self, LambdaContainerMock): + def test_must_create_lambda_container(self, LambdaContainerMock, LogMock): code_dir = "some code dir" container = Mock() @@ -67,6 +68,8 @@ def test_must_create_lambda_container(self, LambdaContainerMock): self.runtime.create(self.func_config, debug_context=debug_options) + LogMock.assert_not_called() + # Make sure env-vars get resolved self.env_vars.resolve.assert_called_with() @@ -115,6 +118,55 @@ def test_keyboard_interrupt_must_raise(self, LambdaContainerMock): with self.assertRaises(KeyboardInterrupt): self.runtime.create(self.func_config, debug_context=debug_options) + @patch("samcli.local.lambdafn.runtime.LOG") + @patch("samcli.local.lambdafn.runtime.LambdaContainer") + def test_must_log_if_template_has_runtime_version(self, LambdaContainerMock, LogMock): + code_dir = "some code dir" + + container = Mock() + debug_options = Mock() + lambda_image_mock = Mock() + + self.runtime = LambdaRuntime(self.manager_mock, lambda_image_mock) + + # Using MagicMock to mock the context manager + self.runtime._get_code_dir = MagicMock() + self.runtime._get_code_dir.return_value = code_dir + + LambdaContainerMock.return_value = container + self.func_config.runtime_management_config = dict(RuntimeVersionArn="runtime_version") + self.runtime.create(self.func_config, debug_context=debug_options) + LogMock.info.assert_called_once() + # It shows a warning + self.assertIn("This function will be invoked using the latest available runtime", LogMock.info.call_args[0][0]) + + # Make sure env-vars get resolved + self.env_vars.resolve.assert_called_with() + + # Make sure the context manager is called to return the code directory + self.runtime._get_code_dir.assert_called_with(self.code_path) + + # Make sure the container is created with proper values + LambdaContainerMock.assert_called_with( + self.lang, + self.imageuri, + self.handler, + self.packagetype, + self.imageconfig, + code_dir, + self.layers, + lambda_image_mock, + self.architecture, + debug_options=debug_options, + env_vars=self.env_var_value, + memory_mb=self.DEFAULT_MEMORY, + container_host=None, + container_host_interface=None, + function_full_path=self.full_path, + ) + # Run the container and get results + self.manager_mock.create.assert_called_with(container) + class LambdaRuntime_run(TestCase):