From 757eddaa922159d3f809ac17187d423ade07bf9a Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Mon, 11 Dec 2023 12:04:10 -0800 Subject: [PATCH 1/6] Revert "chore: Upgrade Mac installer to python3.11 (#6424)" This reverts commit ec451f2be7e4f9a8290f32f4c7a51ff19f757fce. --- .github/workflows/validate_pyinstaller.yml | 2 +- Makefile | 2 +- installer/pyinstaller/build-mac.sh | 6 +-- requirements/reproducible-mac.txt | 43 +++++++++++++++++++++- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate_pyinstaller.yml b/.github/workflows/validate_pyinstaller.yml index f520675225..4904668fd7 100644 --- a/.github/workflows/validate_pyinstaller.yml +++ b/.github/workflows/validate_pyinstaller.yml @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.7" - name: Set up Go uses: actions/setup-go@v4 with: diff --git a/Makefile b/Makefile index 429effc02d..a5314e30e1 100644 --- a/Makefile +++ b/Makefile @@ -70,7 +70,7 @@ update-reproducible-linux-reqs: venv-update-reproducible-linux/bin/pip-compile --generate-hashes --allow-unsafe -o requirements/reproducible-linux.txt update-reproducible-mac-reqs: - python3.11 -m venv venv-update-reproducible-mac + python3.8 -m venv venv-update-reproducible-mac venv-update-reproducible-mac/bin/pip install --upgrade pip-tools pip venv-update-reproducible-mac/bin/pip install -r requirements/base.txt venv-update-reproducible-mac/bin/pip-compile --generate-hashes --allow-unsafe -o requirements/reproducible-mac.txt diff --git a/installer/pyinstaller/build-mac.sh b/installer/pyinstaller/build-mac.sh index 382991f412..f003274cae 100755 --- a/installer/pyinstaller/build-mac.sh +++ b/installer/pyinstaller/build-mac.sh @@ -30,11 +30,11 @@ if [ "$python_library_zip_filename" = "" ]; then fi if [ "$openssl_version" = "" ]; then - openssl_version="1.1.1t"; + openssl_version="1.1.1o"; fi if [ "$python_version" = "" ]; then - python_version="3.11.3"; + python_version="3.8.13"; fi if ! [ "$build_binary_name" = "" ]; then @@ -97,7 +97,7 @@ sudo make install cd .. echo "Installing Python Libraries" -/usr/local/bin/python3.11 -m venv venv +/usr/local/bin/python3.8 -m venv venv ./venv/bin/pip install --upgrade pip ./venv/bin/pip install -r src/requirements/reproducible-mac.txt diff --git a/requirements/reproducible-mac.txt b/requirements/reproducible-mac.txt index 15cd29e1a9..a1b7412c67 100644 --- a/requirements/reproducible-mac.txt +++ b/requirements/reproducible-mac.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/reproducible-mac.txt @@ -30,6 +30,24 @@ aws-sam-translator==1.82.0 \ # via # aws-sam-cli (setup.py) # cfn-lint +backports-zoneinfo==0.2.1 \ + --hash=sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf \ + --hash=sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328 \ + --hash=sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546 \ + --hash=sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6 \ + --hash=sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570 \ + --hash=sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9 \ + --hash=sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7 \ + --hash=sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987 \ + --hash=sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722 \ + --hash=sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582 \ + --hash=sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc \ + --hash=sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b \ + --hash=sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1 \ + --hash=sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08 \ + --hash=sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac \ + --hash=sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2 + # via tzlocal binaryornot==0.4.4 \ --hash=sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061 \ --hash=sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4 @@ -274,6 +292,16 @@ idna==3.6 \ --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f # via requests +importlib-metadata==6.8.0 \ + --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ + --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 + # via flask +importlib-resources==6.1.1 \ + --hash=sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a \ + --hash=sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6 + # via + # jsonschema + # jsonschema-specifications itsdangerous==2.1.2 \ --hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44 \ --hash=sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a @@ -458,6 +486,10 @@ pbr==6.0.0 \ # via # jschema-to-python # sarif-om +pkgutil-resolve-name==1.3.10 \ + --hash=sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174 \ + --hash=sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e + # via jsonschema pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 @@ -954,9 +986,11 @@ typing-extensions==4.8.0 \ --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef # via + # annotated-types # aws-sam-cli (setup.py) # aws-sam-translator # boto3-stubs + # botocore-stubs # mypy-boto3-apigateway # mypy-boto3-cloudformation # mypy-boto3-ecr @@ -973,6 +1007,7 @@ typing-extensions==4.8.0 \ # mypy-boto3-xray # pydantic # pydantic-core + # rich tzlocal==5.2 \ --hash=sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8 \ --hash=sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e @@ -1027,6 +1062,12 @@ wheel==0.42.0 \ --hash=sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d \ --hash=sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8 # via aws-lambda-builders +zipp==3.17.0 \ + --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ + --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 + # via + # importlib-metadata + # importlib-resources # The following packages are considered to be unsafe in a requirements file: setuptools==69.0.2 \ From 899b8b36998a2ab260ca363ea18b1751a32d2321 Mon Sep 17 00:00:00 2001 From: Daniel Mil Date: Mon, 11 Dec 2023 12:51:30 -0800 Subject: [PATCH 2/6] Update GH action python version to 3.8 --- .github/workflows/validate_pyinstaller.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate_pyinstaller.yml b/.github/workflows/validate_pyinstaller.yml index 4904668fd7..503c67fb5f 100644 --- a/.github/workflows/validate_pyinstaller.yml +++ b/.github/workflows/validate_pyinstaller.yml @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.8" - name: Set up Go uses: actions/setup-go@v4 with: From 7adf8ec2576523255bea3621f3d08b1dfec8ad21 Mon Sep 17 00:00:00 2001 From: Daniel Mil Date: Wed, 20 Dec 2023 13:36:26 -0800 Subject: [PATCH 3/6] fix: Explicitly exit when container is out-of-memory --- samcli/local/docker/container.py | 2 +- samcli/local/docker/container_analyzer.py | 48 +++++++++++++++++++++++ samcli/local/docker/exceptions.py | 7 ++++ samcli/local/docker/manager.py | 20 ++++++++++ samcli/local/lambdafn/runtime.py | 24 ++++++++++++ 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 samcli/local/docker/container_analyzer.py diff --git a/samcli/local/docker/container.py b/samcli/local/docker/container.py index 7450f25d44..6e54ed2531 100644 --- a/samcli/local/docker/container.py +++ b/samcli/local/docker/container.py @@ -120,7 +120,7 @@ def __init__( self.docker_client = docker_client or docker.from_env(version=DOCKER_MIN_API_VERSION) # Runtime properties of the container. They won't have value until container is created or started - self.id = None + self.id: Optional[str] = None # aws-lambda-rie defaults to 8080 as the port, however that's a common port. A port is chosen by # selecting the first free port in a range that's not ephemeral. diff --git a/samcli/local/docker/container_analyzer.py b/samcli/local/docker/container_analyzer.py new file mode 100644 index 0000000000..02916f870a --- /dev/null +++ b/samcli/local/docker/container_analyzer.py @@ -0,0 +1,48 @@ +""" +Class for handling the analysis and inspection of Docker containers +""" +import logging +from dataclasses import dataclass + +from samcli.local.docker.container import Container +from samcli.local.docker.manager import ContainerManager + +LOG = logging.getLogger(__name__) + +DEFAULT_OUT_OF_MEMORY = False + + +@dataclass +class ContainerState: + out_of_memory: bool + + +class ContainerAnalyzer: + def __init__(self, container_manager: ContainerManager, container: Container): + self.container_manager = container_manager + self.container = container + + def inspect(self) -> ContainerState: + """ + Inspect the state of a container by calling the "inspect()" API that Docker provides. + Extract relevant information into a ContainerState object. + + Returns + ------- + ContainerState: + Returns a ContainerState object with relevant container data + """ + if not self.container.id: + LOG.debug("Container ID not defined, unable to fetch container state") + return ContainerState(DEFAULT_OUT_OF_MEMORY) + + state = self.container_manager.inspect(self.container.id) + + if isinstance(state, bool): + LOG.debug("Unable to fetch container state") + return ContainerState(DEFAULT_OUT_OF_MEMORY) + + container_state = ContainerState(state.get("State", {}).get("OOMKilled", DEFAULT_OUT_OF_MEMORY)) + LOG.debug("[Container state] OOMKilled %s", container_state.out_of_memory) + + return container_state diff --git a/samcli/local/docker/exceptions.py b/samcli/local/docker/exceptions.py index 5aee68e9cc..388041e0ff 100644 --- a/samcli/local/docker/exceptions.py +++ b/samcli/local/docker/exceptions.py @@ -1,6 +1,7 @@ """ Docker container related exceptions """ +from samcli.commands.exceptions import UserException class ContainerNotStartableException(Exception): @@ -17,3 +18,9 @@ class PortAlreadyInUse(Exception): """ Exception to raise when the provided port is not available for use. """ + + +class ContainerFailureError(UserException): + """ + Raised when the invoke container fails execution + """ diff --git a/samcli/local/docker/manager.py b/samcli/local/docker/manager.py index 6975828cd1..851d499afc 100644 --- a/samcli/local/docker/manager.py +++ b/samcli/local/docker/manager.py @@ -5,6 +5,7 @@ import logging import sys import threading +from typing import Union, cast import docker @@ -193,6 +194,25 @@ def has_image(self, image_name): except docker.errors.ImageNotFound: return False + def inspect(self, container: str) -> Union[bool, dict]: + """ + Low-level Docker API for inspecting the container state + + Parameters + ---------- + container: str + ID of the container + + Returns + ------- + Union[bool, dict] + Container inspection state if successful, False otherwise + """ + try: + return cast(dict, self.docker_client.api.inspect_container(container)) + except (docker.errors.APIError, docker.errors.NullResource): + return False + class DockerImagePullFailedException(Exception): pass diff --git a/samcli/local/lambdafn/runtime.py b/samcli/local/lambdafn/runtime.py index 0272604656..ba406d06ea 100644 --- a/samcli/local/lambdafn/runtime.py +++ b/samcli/local/lambdafn/runtime.py @@ -13,6 +13,9 @@ from samcli.lib.telemetry.metric import capture_parameter from samcli.lib.utils.file_observer import LambdaFunctionObserver from samcli.lib.utils.packagetype import ZIP +from samcli.local.docker.container import Container +from samcli.local.docker.container_analyzer import ContainerAnalyzer +from samcli.local.docker.exceptions import ContainerFailureError from samcli.local.docker.lambda_container import LambdaContainer from ...lib.providers.provider import LayerVersion @@ -223,9 +226,30 @@ def _on_invoke_done(self, container): The current running container """ if container: + self._check_exit_state(container) self._container_manager.stop(container) self._clean_decompressed_paths() + def _check_exit_state(self, container: Container): + """ + Check and validate the exit state of the invoke container. + + Parameters + ---------- + container: Container + Docker container to be checked + + Raises + ------- + ContainerFailureError + If the exit reason is due to out-of-memory, return exit code 1 + + """ + container_analyzer = ContainerAnalyzer(self._container_manager, container) + exit_state = container_analyzer.inspect() + if exit_state.out_of_memory: + raise ContainerFailureError("Container invocation failed due to maximum memory usage") + def _configure_interrupt(self, function_full_path, timeout, container, is_debugging): """ When a Lambda function is executing, we setup certain interrupt handlers to stop the execution. From 0bb9f44f98c7ebf8779acb7f21d2bda6ec246381 Mon Sep 17 00:00:00 2001 From: Daniel Mil Date: Wed, 20 Dec 2023 13:36:26 -0800 Subject: [PATCH 4/6] fix: Explicitly exit when container is out-of-memory --- samcli/local/docker/container.py | 2 +- samcli/local/docker/container_analyzer.py | 48 +++++++++++++++++++++++ samcli/local/docker/exceptions.py | 7 ++++ samcli/local/docker/manager.py | 20 ++++++++++ samcli/local/lambdafn/runtime.py | 24 ++++++++++++ 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 samcli/local/docker/container_analyzer.py diff --git a/samcli/local/docker/container.py b/samcli/local/docker/container.py index 7450f25d44..6e54ed2531 100644 --- a/samcli/local/docker/container.py +++ b/samcli/local/docker/container.py @@ -120,7 +120,7 @@ def __init__( self.docker_client = docker_client or docker.from_env(version=DOCKER_MIN_API_VERSION) # Runtime properties of the container. They won't have value until container is created or started - self.id = None + self.id: Optional[str] = None # aws-lambda-rie defaults to 8080 as the port, however that's a common port. A port is chosen by # selecting the first free port in a range that's not ephemeral. diff --git a/samcli/local/docker/container_analyzer.py b/samcli/local/docker/container_analyzer.py new file mode 100644 index 0000000000..02916f870a --- /dev/null +++ b/samcli/local/docker/container_analyzer.py @@ -0,0 +1,48 @@ +""" +Class for handling the analysis and inspection of Docker containers +""" +import logging +from dataclasses import dataclass + +from samcli.local.docker.container import Container +from samcli.local.docker.manager import ContainerManager + +LOG = logging.getLogger(__name__) + +DEFAULT_OUT_OF_MEMORY = False + + +@dataclass +class ContainerState: + out_of_memory: bool + + +class ContainerAnalyzer: + def __init__(self, container_manager: ContainerManager, container: Container): + self.container_manager = container_manager + self.container = container + + def inspect(self) -> ContainerState: + """ + Inspect the state of a container by calling the "inspect()" API that Docker provides. + Extract relevant information into a ContainerState object. + + Returns + ------- + ContainerState: + Returns a ContainerState object with relevant container data + """ + if not self.container.id: + LOG.debug("Container ID not defined, unable to fetch container state") + return ContainerState(DEFAULT_OUT_OF_MEMORY) + + state = self.container_manager.inspect(self.container.id) + + if isinstance(state, bool): + LOG.debug("Unable to fetch container state") + return ContainerState(DEFAULT_OUT_OF_MEMORY) + + container_state = ContainerState(state.get("State", {}).get("OOMKilled", DEFAULT_OUT_OF_MEMORY)) + LOG.debug("[Container state] OOMKilled %s", container_state.out_of_memory) + + return container_state diff --git a/samcli/local/docker/exceptions.py b/samcli/local/docker/exceptions.py index 5aee68e9cc..388041e0ff 100644 --- a/samcli/local/docker/exceptions.py +++ b/samcli/local/docker/exceptions.py @@ -1,6 +1,7 @@ """ Docker container related exceptions """ +from samcli.commands.exceptions import UserException class ContainerNotStartableException(Exception): @@ -17,3 +18,9 @@ class PortAlreadyInUse(Exception): """ Exception to raise when the provided port is not available for use. """ + + +class ContainerFailureError(UserException): + """ + Raised when the invoke container fails execution + """ diff --git a/samcli/local/docker/manager.py b/samcli/local/docker/manager.py index 6975828cd1..851d499afc 100644 --- a/samcli/local/docker/manager.py +++ b/samcli/local/docker/manager.py @@ -5,6 +5,7 @@ import logging import sys import threading +from typing import Union, cast import docker @@ -193,6 +194,25 @@ def has_image(self, image_name): except docker.errors.ImageNotFound: return False + def inspect(self, container: str) -> Union[bool, dict]: + """ + Low-level Docker API for inspecting the container state + + Parameters + ---------- + container: str + ID of the container + + Returns + ------- + Union[bool, dict] + Container inspection state if successful, False otherwise + """ + try: + return cast(dict, self.docker_client.api.inspect_container(container)) + except (docker.errors.APIError, docker.errors.NullResource): + return False + class DockerImagePullFailedException(Exception): pass diff --git a/samcli/local/lambdafn/runtime.py b/samcli/local/lambdafn/runtime.py index 0272604656..ba406d06ea 100644 --- a/samcli/local/lambdafn/runtime.py +++ b/samcli/local/lambdafn/runtime.py @@ -13,6 +13,9 @@ from samcli.lib.telemetry.metric import capture_parameter from samcli.lib.utils.file_observer import LambdaFunctionObserver from samcli.lib.utils.packagetype import ZIP +from samcli.local.docker.container import Container +from samcli.local.docker.container_analyzer import ContainerAnalyzer +from samcli.local.docker.exceptions import ContainerFailureError from samcli.local.docker.lambda_container import LambdaContainer from ...lib.providers.provider import LayerVersion @@ -223,9 +226,30 @@ def _on_invoke_done(self, container): The current running container """ if container: + self._check_exit_state(container) self._container_manager.stop(container) self._clean_decompressed_paths() + def _check_exit_state(self, container: Container): + """ + Check and validate the exit state of the invoke container. + + Parameters + ---------- + container: Container + Docker container to be checked + + Raises + ------- + ContainerFailureError + If the exit reason is due to out-of-memory, return exit code 1 + + """ + container_analyzer = ContainerAnalyzer(self._container_manager, container) + exit_state = container_analyzer.inspect() + if exit_state.out_of_memory: + raise ContainerFailureError("Container invocation failed due to maximum memory usage") + def _configure_interrupt(self, function_full_path, timeout, container, is_debugging): """ When a Lambda function is executing, we setup certain interrupt handlers to stop the execution. From f160cc64229b9a903c1495af732925f3d7aa594d Mon Sep 17 00:00:00 2001 From: Daniel Mil Date: Tue, 2 Jan 2024 11:27:17 -0800 Subject: [PATCH 5/6] Add log, add unit tests --- samcli/local/docker/manager.py | 3 +- .../local/docker/test_container_analyzer.py | 73 +++++++++++++++++++ tests/unit/local/docker/test_manager.py | 24 ++++++ tests/unit/local/lambdafn/test_runtime.py | 6 ++ 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 tests/unit/local/docker/test_container_analyzer.py diff --git a/samcli/local/docker/manager.py b/samcli/local/docker/manager.py index 851d499afc..ceda43ce8e 100644 --- a/samcli/local/docker/manager.py +++ b/samcli/local/docker/manager.py @@ -210,7 +210,8 @@ def inspect(self, container: str) -> Union[bool, dict]: """ try: return cast(dict, self.docker_client.api.inspect_container(container)) - except (docker.errors.APIError, docker.errors.NullResource): + except (docker.errors.APIError, docker.errors.NullResource) as ex: + LOG.debug("Failed to call Docker inspect: %s", str(ex)) return False diff --git a/tests/unit/local/docker/test_container_analyzer.py b/tests/unit/local/docker/test_container_analyzer.py new file mode 100644 index 0000000000..8464fb61a1 --- /dev/null +++ b/tests/unit/local/docker/test_container_analyzer.py @@ -0,0 +1,73 @@ +from unittest import TestCase +from unittest.mock import Mock, patch + +from samcli.lib.utils.packagetype import IMAGE +from samcli.local.docker.container import Container +from samcli.local.docker.container_analyzer import ContainerAnalyzer, ContainerState +from samcli.local.docker.manager import ContainerManager + + +class TestContainerAnalyzer(TestCase): + def setUp(self) -> None: + self.image = IMAGE + self.cmd = "cmd" + self.working_dir = "working_dir" + self.host_dir = "host_dir" + self.memory_mb = 123 + self.exposed_ports = {123: 123} + self.entrypoint = ["a", "b", "c"] + self.env_vars = {"key": "value"} + + self.mock_docker_client = Mock() + + self.container = Container( + self.image, + self.cmd, + self.working_dir, + self.host_dir, + self.memory_mb, + self.exposed_ports, + self.entrypoint, + self.env_vars, + self.mock_docker_client, + ) + + @patch("samcli.local.docker.container_analyzer.LOG") + def test_inspect_returns_container_state(self, mock_log): + self.container.id = "id" + manager = ContainerManager() + manager.inspect = Mock() + manager.inspect.return_value = { + "State": { + "OOMKilled": True + } + } + + analyzer = ContainerAnalyzer(container_manager=manager, container=self.container) + state = analyzer.inspect() + + manager.inspect.assert_called_once_with("id") + mock_log.debug.assert_called_once_with("[Container state] OOMKilled %s", True) + self.assertEqual(state, ContainerState(out_of_memory=True)) + + def test_inspect_no_container_id(self): + manager = ContainerManager() + manager.inspect = Mock() + + analyzer = ContainerAnalyzer(container_manager=manager, container=self.container) + state = analyzer.inspect() + + manager.inspect.assert_not_called() + self.assertEqual(state, ContainerState(out_of_memory=False)) + + def test_inspect_docker_call_fails(self): + self.container.id = "id" + manager = ContainerManager() + manager.inspect = Mock() + manager.inspect.return_value = False + + analyzer = ContainerAnalyzer(container_manager=manager, container=self.container) + state = analyzer.inspect() + + manager.inspect.assert_called_once_with("id") + self.assertEqual(state, ContainerState(out_of_memory=False)) diff --git a/tests/unit/local/docker/test_manager.py b/tests/unit/local/docker/test_manager.py index 4cb42bbd02..87b6af154d 100644 --- a/tests/unit/local/docker/test_manager.py +++ b/tests/unit/local/docker/test_manager.py @@ -7,6 +7,8 @@ import requests from docker.errors import APIError, ImageNotFound + +import docker from samcli.local.docker.manager import ContainerManager, DockerImagePullFailedException from samcli.local.docker.lambda_image import RAPID_IMAGE_TAG_PREFIX @@ -384,3 +386,25 @@ def test_must_call_delete_on_container(self): manager.stop(container) container.delete.assert_called_with() + + +class TestContainerManager_inspect(TestCase): + def test_must_call_inspect_on_container(self): + manager = ContainerManager() + manager.docker_client = Mock() + + container = "container_id" + + manager.inspect(container) + manager.docker_client.docker_client.api.inspect_container(container) + + @patch("samcli.local.docker.manager.LOG") + def test_must_fail_with_error_message(self, mock_log): + manager = ContainerManager() + manager.docker_client.api.inspect_container = Mock() + manager.docker_client.api.inspect_container.side_effect = [docker.errors.APIError("Failed")] + + return_val = manager.inspect("container_id") + + self.assertEqual(return_val, False) + mock_log.debug.assert_called_once_with("Failed to call Docker inspect: %s", "Failed") diff --git a/tests/unit/local/lambdafn/test_runtime.py b/tests/unit/local/lambdafn/test_runtime.py index 42087ebd00..cc5753869a 100644 --- a/tests/unit/local/lambdafn/test_runtime.py +++ b/tests/unit/local/lambdafn/test_runtime.py @@ -310,6 +310,8 @@ def test_must_run_container_and_wait_for_result(self, LambdaContainerMock): self.runtime._configure_interrupt = Mock() self.runtime._configure_interrupt.return_value = start_timer + self.runtime._check_exit_state = Mock() + LambdaContainerMock.return_value = container container.is_running.return_value = False @@ -369,6 +371,8 @@ def test_exception_from_run_must_trigger_cleanup(self, LambdaContainerMock): self.runtime._configure_interrupt = Mock() self.runtime._configure_interrupt.return_value = start_timer + self.runtime._check_exit_state = Mock() + LambdaContainerMock.return_value = container container.is_running.return_value = False @@ -404,6 +408,7 @@ def test_exception_from_wait_for_result_must_trigger_cleanup(self, LambdaContain self.runtime._get_code_dir.return_value = code_dir self.runtime._configure_interrupt = Mock() self.runtime._configure_interrupt.return_value = timer + self.runtime._check_exit_state = Mock() LambdaContainerMock.return_value = container container.is_running.return_value = False @@ -437,6 +442,7 @@ def test_keyboard_interrupt_must_not_raise(self, LambdaContainerMock): self.runtime._get_code_dir = MagicMock() self.runtime._get_code_dir.return_value = code_dir self.runtime._configure_interrupt = Mock() + self.runtime._check_exit_state = Mock() LambdaContainerMock.return_value = container container.is_running.return_value = False From c531b5a1fe0950e12d42518645261d8f2f039d30 Mon Sep 17 00:00:00 2001 From: Daniel Mil Date: Tue, 2 Jan 2024 11:40:32 -0800 Subject: [PATCH 6/6] Format files --- tests/unit/local/docker/test_container_analyzer.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/unit/local/docker/test_container_analyzer.py b/tests/unit/local/docker/test_container_analyzer.py index 8464fb61a1..9832bd83ad 100644 --- a/tests/unit/local/docker/test_container_analyzer.py +++ b/tests/unit/local/docker/test_container_analyzer.py @@ -37,11 +37,7 @@ def test_inspect_returns_container_state(self, mock_log): self.container.id = "id" manager = ContainerManager() manager.inspect = Mock() - manager.inspect.return_value = { - "State": { - "OOMKilled": True - } - } + manager.inspect.return_value = {"State": {"OOMKilled": True}} analyzer = ContainerAnalyzer(container_manager=manager, container=self.container) state = analyzer.inspect()