From f94ca4bf670a90dedc21b74d6cb39492315d1d36 Mon Sep 17 00:00:00 2001 From: Sheng Yu Date: Tue, 7 Nov 2023 08:42:55 -0500 Subject: [PATCH] feat(snaps): inject snaps from host if try to install same channel --- craft_providers/actions/snap_installer.py | 70 ++++++++++++++++++- craft_providers/errors.py | 5 ++ .../actions/test_snap_installer.py | 4 ++ tests/unit/actions/test_snap_installer.py | 10 ++- 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/craft_providers/actions/snap_installer.py b/craft_providers/actions/snap_installer.py index abcae1a1..e1847a1c 100644 --- a/craft_providers/actions/snap_installer.py +++ b/craft_providers/actions/snap_installer.py @@ -33,6 +33,7 @@ from craft_providers.const import TIMEOUT_COMPLEX, TIMEOUT_SIMPLE from craft_providers.errors import ( BaseConfigurationError, + IncompatibleSnapError, ProviderError, details_from_called_process_error, ) @@ -340,7 +341,13 @@ def inject_from_host(*, executor: Executor, snap_name: str, classic: bool) -> No classic, ) - host_revision = get_host_snap_info(snap_name)["revision"] + host_snap_info = get_host_snap_info(snap_name) + host_snap_base = host_snap_info.get("base", None) + if host_snap_base: + logger.debug("Installing base snap %r from host", host_snap_base) + inject_from_host(executor=executor, snap_name=host_snap_base, classic=False) + + host_revision = host_snap_info["revision"] target_revision = _get_snap_revision_ensuring_source( snap_name=snap_store_name, source=SNAP_SRC_HOST, @@ -398,8 +405,41 @@ def inject_from_host(*, executor: Executor, snap_name: str, classic: bool) -> No ) +def _try_install_from_host( + *, + executor: Executor, + snap_name: str, + channel: str, + classic: bool, +) -> None: + """Try to install snap from host if has same channel.""" + try: + host_snap_info = get_host_snap_info(snap_name) + except requests.exceptions.HTTPError as error: + logger.debug(f"Host snap {snap_name} not exists, skipping") + raise IncompatibleSnapError(f"Host snap {snap_name} not exists") from error + + if host_snap_info.get("channel", None) != channel: + logger.debug(f"Host snap {snap_name} has different channel, skipping") + raise IncompatibleSnapError(f"Host snap {snap_name} has different channel") + + # find and inject base snap if exists + host_snap_base = host_snap_info.get("base", None) + if host_snap_base: + host_snap_base_info = get_host_snap_info(host_snap_base) + if host_snap_base_info.get("name", None): + inject_from_host(executor=executor, snap_name=host_snap_base, classic=False) + + inject_from_host(executor=executor, snap_name=snap_name, classic=classic) + + def install_from_store( - *, executor: Executor, snap_name: str, channel: str, classic: bool + *, + executor: Executor, + snap_name: str, + channel: str, + classic: bool, + try_local: bool = True, ) -> None: """Install snap from store into target. @@ -409,11 +449,37 @@ def install_from_store( :param snap_name: Name of snap to install. :param channel: Channel to install from. :param classic: Install in classic mode. + :param try_local: Try to install from local snap if available. :raises SnapInstallationError: on unexpected error. """ # trim the `_name` suffix, if present snap_store_name = snap_name.split("_")[0] + + if try_local: + logger.debug( + "Checking snap %r from host snapd (channel=%r, classic=%s).", + snap_name, + channel, + classic, + ) + + try: + _try_install_from_host( + executor=executor, snap_name=snap_name, channel=channel, classic=classic + ) + except IncompatibleSnapError as error: + logger.debug( + "Failed to find snap %r from host snapd (channel=%r, classic=%s).", + snap_name, + channel, + classic, + ) + logger.debug("Error: %s", error) + else: + logger.debug("Snap %r installed from host snapd.", snap_name) + return + if snap_name == snap_store_name: logger.debug( "Installing snap %r from store (channel=%r, classic=%s).", diff --git a/craft_providers/errors.py b/craft_providers/errors.py index 59ac688f..9fd38980 100644 --- a/craft_providers/errors.py +++ b/craft_providers/errors.py @@ -130,3 +130,8 @@ def __init__(self) -> None: "see https://craft-providers.readthedocs.io/ for further reference." ) super().__init__(brief=brief, resolution=resolution) + + +@dataclasses.dataclass +class IncompatibleSnapError(ProviderError, RuntimeError): + """Host snap is incompatible with the target.""" diff --git a/tests/integration/actions/test_snap_installer.py b/tests/integration/actions/test_snap_installer.py index 33414537..8bba2939 100644 --- a/tests/integration/actions/test_snap_installer.py +++ b/tests/integration/actions/test_snap_installer.py @@ -138,6 +138,7 @@ def test_install_from_store_strict(core22_lxd_instance, installed_snap, caplog): snap_name="hello-world", channel="latest/stable", classic=False, + try_local=False, ) core22_lxd_instance.execute_run(["test", "-f", "/snap/bin/hello-world"], check=True) @@ -177,6 +178,7 @@ def test_install_from_store_classic(core22_lxd_instance, installed_snap, caplog) snap_name="charmcraft", channel="latest/stable", classic=True, + try_local=False, ) core22_lxd_instance.execute_run(["test", "-f", "/snap/bin/charmcraft"], check=True) @@ -216,6 +218,7 @@ def test_install_from_store_channel(core22_lxd_instance, installed_snap, caplog) snap_name="go", channel="1.15/stable", classic=True, + try_local=False, ) proc = core22_lxd_instance.execute_run( @@ -263,6 +266,7 @@ def test_install_from_store_snap_name_suffix( snap_name="hello-world_suffix", channel="latest/stable", classic=False, + try_local=False, ) core22_lxd_instance.execute_run(["test", "-f", "/snap/bin/hello-world"], check=True) diff --git a/tests/unit/actions/test_snap_installer.py b/tests/unit/actions/test_snap_installer.py index 8adb0239..15ae053d 100644 --- a/tests/unit/actions/test_snap_installer.py +++ b/tests/unit/actions/test_snap_installer.py @@ -601,6 +601,7 @@ def test_install_from_store_strict( snap_name="test-name_suffix", classic=False, channel="test-chan", + try_local=False, ) assert len(fake_process.calls) == 1 @@ -652,7 +653,11 @@ def test_install_from_store_classic( ) snap_installer.install_from_store( - executor=fake_executor, snap_name="test-name", classic=True, channel="test-chan" + executor=fake_executor, + snap_name="test-name", + classic=True, + channel="test-chan", + try_local=False, ) assert len(fake_process.calls) == 1 @@ -691,6 +696,7 @@ def test_refresh_from_store( snap_name="test-name", classic=False, channel="test-chan", + try_local=False, ) assert len(fake_process.calls) == 1 @@ -741,6 +747,7 @@ def test_install_from_store_failure( snap_name="test-name", classic=True, channel="test-chan", + try_local=False, ) assert exc_info.value == snap_installer.SnapInstallationError( @@ -779,6 +786,7 @@ def test_install_from_store_trim_suffix( snap_name="test-name", classic=False, channel="test-chan", + try_local=False, ) assert len(fake_process.calls) == 1