diff --git a/craft_providers/actions/snap_installer.py b/craft_providers/actions/snap_installer.py index abcae1a1..e61c51b8 100644 --- a/craft_providers/actions/snap_installer.py +++ b/craft_providers/actions/snap_installer.py @@ -340,7 +340,15 @@ 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 for %r from host", host_snap_base, snap_name + ) + 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, diff --git a/tests/unit/actions/test_snap_installer.py b/tests/unit/actions/test_snap_installer.py index 8adb0239..6ee7ed59 100644 --- a/tests/unit/actions/test_snap_installer.py +++ b/tests/unit/actions/test_snap_installer.py @@ -264,6 +264,100 @@ def test_inject_from_host_snap_name( } +def test_inject_from_host_snap_name_with_base( + config_fixture, + mock_requests, + fake_executor, + fake_process, + logs, + tmp_path, + mocker, +): + """Inject a snap and its base installed locally with the `--name` parameter.""" + # register 'snap known' calls + fake_process.register_subprocess( + ["snap", "known", fake_process.any()], occurrences=8 + ) + fake_process.register_subprocess( + ["fake-executor", "snap", "ack", "/tmp/coreXX.assert"] + ) + fake_process.register_subprocess( + ["fake-executor", "snap", "ack", "/tmp/test-name.assert"] + ) + fake_process.register_subprocess( + ["fake-executor", "snap", "install", "/tmp/coreXX.snap"] + ) + fake_process.register_subprocess( + ["fake-executor", "snap", "install", "/tmp/test-name.snap"] + ) + + mocker.patch( + "craft_providers.actions.snap_installer.get_host_snap_info", + side_effect=[ + { + "id": "", + "name": "test-name", + "type": "app", + "base": "coreXX", + "version": "0.1", + "channel": "", + "revision": "2", + "publisher": {"id": ""}, + "confinement": "classic", + }, + { + "id": "", + "name": "coreXX", + "type": "base", + "version": "0.1", + "channel": "latest/stable", + "revision": "1", + "publisher": {"id": ""}, + "confinement": "strict", + }, + { + "id": "", + "name": "coreXX", + "type": "base", + "version": "0.1", + "channel": "latest/stable", + "revision": "1", + "publisher": {"id": ""}, + "confinement": "strict", + }, + { + "id": "", + "name": "test-name", + "type": "app", + "base": "coreXX", + "version": "0.1", + "channel": "", + "revision": "2", + "publisher": {"id": ""}, + "confinement": "classic", + }, + ], + ) + + snap_installer.inject_from_host( + executor=fake_executor, snap_name="test-name_suffix", classic=False + ) + + mock_requests.get.assert_called_with( + "http+unix://%2Frun%2Fsnapd.socket/v2/snaps/test-name_suffix/file" + ) + assert len(fake_process.calls) == 12 + assert ( + "Installing base snap 'coreXX' for 'test-name_suffix' from host" in logs.debug + ) + assert ( + Exact( + "Installing snap 'test-name_suffix' from host as 'test-name' in instance (classic=False)." + ) + in logs.debug + ) + + @pytest.mark.parametrize("mock_get_host_snap_info", [{"revision": "x3"}], indirect=True) def test_inject_from_host_dangerous( config_fixture, @@ -432,10 +526,25 @@ def test_inject_from_host_no_snapd(mock_get_host_snap_info, fake_executor): ) -def test_inject_from_host_push_error(mock_requests, fake_executor): +def test_inject_from_host_push_error(mock_requests, fake_executor, mocker): mock_executor = mock.Mock(spec=fake_executor, wraps=fake_executor) mock_executor.push_file.side_effect = ProviderError(brief="foo") + mocker.patch( + "craft_providers.actions.snap_installer.get_host_snap_info", + side_effect=[ + { + "id": "", + "name": "test-name", + "type": "app", + "version": "0.1", + "channel": "", + "revision": "x1", + "confinement": "classic", + }, + ], + ) + with pytest.raises(snap_installer.SnapInstallationError) as exc_info: snap_installer.inject_from_host( executor=mock_executor, snap_name="test-name", classic=False @@ -448,6 +557,47 @@ def test_inject_from_host_push_error(mock_requests, fake_executor): assert exc_info.value.__cause__ is not None +def test_inject_from_host_with_base_push_error(mock_requests, fake_executor, mocker): + mock_executor = mock.Mock(spec=fake_executor, wraps=fake_executor) + mock_executor.push_file.side_effect = ProviderError(brief="foo") + + mocker.patch( + "craft_providers.actions.snap_installer.get_host_snap_info", + side_effect=[ + { + "id": "", + "name": "test-name", + "type": "app", + "base": "coreXX", + "version": "0.1", + "channel": "", + "revision": "x1", + "confinement": "classic", + }, + { + "id": "", + "name": "coreXX", + "type": "base", + "version": "0.1", + "channel": "", + "revision": "x1", + "confinement": "strict", + }, + ], + ) + + with pytest.raises(snap_installer.SnapInstallationError) as exc_info: + snap_installer.inject_from_host( + executor=mock_executor, snap_name="test-name", classic=False + ) + + assert exc_info.value == snap_installer.SnapInstallationError( + brief="failed to copy snap file for snap 'coreXX'", + details="error copying snap file into target environment", + ) + assert exc_info.value.__cause__ is not None + + def test_inject_from_host_snapd_connection_error_using_pack_fallback( mock_get_host_snap_info, mock_requests, @@ -545,7 +695,9 @@ def test_inject_from_host_snapd_http_error_using_pack_fallback( assert len(fake_process.calls) == 7 -def test_inject_from_host_install_failure(mock_requests, fake_executor, fake_process): +def test_inject_from_host_install_failure( + mock_requests, fake_executor, fake_process, mocker +): fake_process.register_subprocess( [ "fake-executor", @@ -559,6 +711,21 @@ def test_inject_from_host_install_failure(mock_requests, fake_executor, fake_pro returncode=1, ) + mocker.patch( + "craft_providers.actions.snap_installer.get_host_snap_info", + side_effect=[ + { + "id": "", + "name": "test-name", + "type": "app", + "version": "0.1", + "channel": "", + "revision": "x1", + "confinement": "classic", + }, + ], + ) + with pytest.raises(snap_installer.SnapInstallationError) as exc_info: snap_installer.inject_from_host( executor=fake_executor, snap_name="test-name", classic=False