From 3d7e02251d948623cefe18e126dbe54bf8800844 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Tue, 3 Oct 2023 23:15:57 +0200 Subject: [PATCH 1/3] Check the image is outdated and pop warning to pull the latest --- aiidalab_launch/__main__.py | 15 ++++++++++++++- aiidalab_launch/util.py | 23 +++++++++++++++++++++++ tests/test_util.py | 14 +++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/aiidalab_launch/__main__.py b/aiidalab_launch/__main__.py index a41fe6d..cd06be3 100644 --- a/aiidalab_launch/__main__.py +++ b/aiidalab_launch/__main__.py @@ -21,7 +21,13 @@ from .core import LOGGER from .instance import AiidaLabInstance from .profile import DEFAULT_IMAGE, DEFAULT_PORT, Profile -from .util import confirm_with_value, get_latest_version, spinner, webbrowser_available +from .util import ( + confirm_with_value, + get_latest_version, + image_is_latest, + spinner, + webbrowser_available, +) from .version import __version__ MSG_MOUNT_POINT_CONFLICT = """Warning: There is at least one other running @@ -350,6 +356,13 @@ async def _async_start( # use local image msg = f"Using local image '{profile.image}'." + # check if local image is outdated and pull latest version if so + if not image_is_latest(instance.client, profile.image): + click.secho( + "Warning! Local image is outdated, please run with --pull to update.", + fg="yellow", + ) + if instance.image is None: raise click.ClickException( f"Unable to find image '{profile.image}'. " diff --git a/aiidalab_launch/util.py b/aiidalab_launch/util.py index 2ca6423..20b738b 100644 --- a/aiidalab_launch/util.py +++ b/aiidalab_launch/util.py @@ -225,3 +225,26 @@ def get_docker_env(container: docker.models.containers.Container, env_name: str) except KeyError: pass raise KeyError(env_name) + + +def image_is_latest(docker_client, image: str): + """Check if the local image has the same digest as the image + on remote registry. + """ + try: + local_image = docker_client.images.get(image) + except docker.errors.ImageNotFound: + return False + + try: + remote_image = docker_client.images.get_registry_data(image) + except docker.errors.APIError: + return False + + # There is no need to check creation date of the image, since the once + # there is a new image with the same tag, the id will be different. + # We can not use image id, see https://windsock.io/explaining-docker-image-ids/ + local_digest = local_image.attrs.get("RepoDigests")[0].split("@")[-1] + remote_digest = remote_image.attrs.get("Descriptor", {}).get("digest") + + return local_digest == remote_digest diff --git a/tests/test_util.py b/tests/test_util.py index d2ea15d..1f2434a 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,8 +1,9 @@ from time import time +import pytest from packaging.version import parse -from aiidalab_launch.util import get_latest_version +from aiidalab_launch.util import get_latest_version, image_is_latest def test_get_latest_version(mock_pypi_request): @@ -13,3 +14,14 @@ def test_get_latest_version_timeout(mock_pypi_request_timeout): start = time() assert get_latest_version() is None assert (time() - start) < 0.5 + + +@pytest.mark.usefixtures("enable_docker_pull") +def test_image_is_latest(docker_client): + """Test that the latest version is identified correctly.""" + # download the alpine image for testing + image_name = "alpine:latest" + docker_client.images.pull(image_name) + + # check that the image is identified as latest + assert image_is_latest(docker_client, image_name) From 6dbb0c477ab4a877489986db3464259e3ac68246 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Tue, 3 Oct 2023 23:53:41 +0200 Subject: [PATCH 2/3] monkeypatch and test the warning pop --- aiidalab_launch/__main__.py | 12 ++++-------- tests/test_cli.py | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/aiidalab_launch/__main__.py b/aiidalab_launch/__main__.py index cd06be3..d0374ce 100644 --- a/aiidalab_launch/__main__.py +++ b/aiidalab_launch/__main__.py @@ -21,13 +21,7 @@ from .core import LOGGER from .instance import AiidaLabInstance from .profile import DEFAULT_IMAGE, DEFAULT_PORT, Profile -from .util import ( - confirm_with_value, - get_latest_version, - image_is_latest, - spinner, - webbrowser_available, -) +from .util import confirm_with_value, get_latest_version, spinner, webbrowser_available from .version import __version__ MSG_MOUNT_POINT_CONFLICT = """Warning: There is at least one other running @@ -353,11 +347,13 @@ async def _async_start( with spinner(msg): instance.pull() else: + from aiidalab_launch import util + # use local image msg = f"Using local image '{profile.image}'." # check if local image is outdated and pull latest version if so - if not image_is_latest(instance.client, profile.image): + if not util.image_is_latest(instance.client, profile.image): click.secho( "Warning! Local image is outdated, please run with --pull to update.", fg="yellow", diff --git a/tests/test_cli.py b/tests/test_cli.py index 3bd7697..ebe8fa3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -208,7 +208,7 @@ def test_remove_running_profile(self): @pytest.mark.slow @pytest.mark.trylast class TestInstanceLifecycle: - def test_start_stop_reset(self, instance, docker_client, caplog): + def test_start_stop_reset(self, instance, docker_client, caplog, monkeypatch): caplog.set_level(logging.DEBUG) def get_volume(volume_name): @@ -260,6 +260,20 @@ def assert_status_down(): assert result.exit_code == 0 assert_status_up() + # test the warning message of image not the latest is not raised + assert "Warning!" not in result.output.strip() + + # Then by monkeypatching the image_is_latest function, we can test that + # the warning message is raised + def image_is_latest(docker_client, image_name): + return False + + monkeypatch.setattr("aiidalab_launch.util.image_is_latest", image_is_latest) + result: Result = runner.invoke( + cli.cli, ["start", "--no-browser", "--no-pull", "--wait=300"] + ) + assert "Warning!" in result.output.strip() + # Restart instance. # TODO: This test is currently disabled, because it is too flaky. For # a currently unknown reason, the docker client will not be able to From 42f086787c8d6502000939c8779c453c6bd965ed Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Fri, 6 Oct 2023 10:25:32 +0200 Subject: [PATCH 3/3] f-d --- aiidalab_launch/__main__.py | 6 ------ tests/conftest.py | 13 +++++++++++++ tests/test_util.py | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/aiidalab_launch/__main__.py b/aiidalab_launch/__main__.py index d0374ce..bf15c63 100644 --- a/aiidalab_launch/__main__.py +++ b/aiidalab_launch/__main__.py @@ -359,12 +359,6 @@ async def _async_start( fg="yellow", ) - if instance.image is None: - raise click.ClickException( - f"Unable to find image '{profile.image}'. " - "Try to use '--pull' to pull the image prior to start." - ) - # Check if the container configuration has changed. if instance.container: configuration_changed = any(instance.configuration_changes()) diff --git a/tests/conftest.py b/tests/conftest.py index 93c1cc4..0474614 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,6 +57,19 @@ def docker_client(): pytest.skip("docker not available") +@pytest.fixture(scope="function") +def remove_created_images(docker_client): + """Remove all images created by the tests.""" + images = docker_client.images.list() + yield + for image in docker_client.images.list(): + if image not in images: + try: + image.remove() + except docker.errors.APIError: + pass + + @pytest.fixture(autouse=True) def _select_default_image(monkeypatch_session, pytestconfig): _default_image = pytestconfig.getoption("default_image") diff --git a/tests/test_util.py b/tests/test_util.py index 1f2434a..60ee483 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -17,6 +17,7 @@ def test_get_latest_version_timeout(mock_pypi_request_timeout): @pytest.mark.usefixtures("enable_docker_pull") +@pytest.mark.usefixtures("remove_created_images") def test_image_is_latest(docker_client): """Test that the latest version is identified correctly.""" # download the alpine image for testing @@ -25,3 +26,19 @@ def test_image_is_latest(docker_client): # check that the image is identified as latest assert image_is_latest(docker_client, image_name) + + +@pytest.mark.usefixtures("enable_docker_pull") +@pytest.mark.usefixtures("remove_created_images") +def test_image_is_not_latest(docker_client): + """Test that the outdate version is identified correctly and will ask for pull the latest.""" + # download the alpine image for testing + old_image_name = "alpine:2.6" + latest_image_name = "alpine:latest" + + # pull the old image and retag it as latest to mock the outdated image + old_image = docker_client.images.pull(old_image_name) + old_image.tag(latest_image_name) + + # check that the image is identified as latest + assert not image_is_latest(docker_client, latest_image_name)