mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-05-07 22:11:50 +02:00
Compare commits
4 commits
7542a341f6
...
b3c0f76060
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b3c0f76060 | ||
![]() |
15e5fa7de8 | ||
![]() |
d14b209e79 | ||
![]() |
0a4140b713 |
9 changed files with 97 additions and 53 deletions
|
@ -1,10 +1,12 @@
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
from . import errors
|
from . import errors
|
||||||
|
from .settings import Settings
|
||||||
from .util import get_resource_path, get_subprocess_startupinfo
|
from .util import get_resource_path, get_subprocess_startupinfo
|
||||||
|
|
||||||
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
||||||
|
@ -13,14 +15,27 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_name() -> str:
|
def get_runtime_name() -> str:
|
||||||
if platform.system() == "Linux":
|
settings = Settings()
|
||||||
runtime_name = "podman"
|
try:
|
||||||
else:
|
runtime_name = settings.get("container_runtime")
|
||||||
# Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually
|
except KeyError:
|
||||||
runtime_name = "docker"
|
return "podman" if platform.system() == "Linux" else "docker"
|
||||||
return runtime_name
|
return runtime_name
|
||||||
|
|
||||||
|
|
||||||
|
def get_runtime() -> str:
|
||||||
|
container_tech = get_runtime_name()
|
||||||
|
runtime = shutil.which(container_tech)
|
||||||
|
if runtime is None:
|
||||||
|
# Fallback to the container runtime path from the settings
|
||||||
|
settings = Settings()
|
||||||
|
runtime_path = settings.get("container_runtime_path")
|
||||||
|
if os.path.exists(runtime_path):
|
||||||
|
return runtime_path
|
||||||
|
raise errors.NoContainerTechException(container_tech)
|
||||||
|
return runtime
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_version() -> Tuple[int, int]:
|
def get_runtime_version() -> Tuple[int, int]:
|
||||||
"""Get the major/minor parts of the Docker/Podman version.
|
"""Get the major/minor parts of the Docker/Podman version.
|
||||||
|
|
||||||
|
@ -31,13 +46,14 @@ def get_runtime_version() -> Tuple[int, int]:
|
||||||
semver parser is an overkill.
|
semver parser is an overkill.
|
||||||
"""
|
"""
|
||||||
# Get the Docker/Podman version, using a Go template.
|
# Get the Docker/Podman version, using a Go template.
|
||||||
runtime = get_runtime_name()
|
runtime_name = get_runtime_name()
|
||||||
if runtime == "podman":
|
|
||||||
|
if runtime_name == "podman":
|
||||||
query = "{{.Client.Version}}"
|
query = "{{.Client.Version}}"
|
||||||
else:
|
else:
|
||||||
query = "{{.Server.Version}}"
|
query = "{{.Server.Version}}"
|
||||||
|
|
||||||
cmd = [runtime, "version", "-f", query]
|
cmd = [get_runtime(), "version", "-f", query]
|
||||||
try:
|
try:
|
||||||
version = subprocess.run(
|
version = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
|
@ -46,7 +62,7 @@ def get_runtime_version() -> Tuple[int, int]:
|
||||||
check=True,
|
check=True,
|
||||||
).stdout.decode()
|
).stdout.decode()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = f"Could not get the version of the {runtime.capitalize()} tool: {e}"
|
msg = f"Could not get the version of the {runtime_name.capitalize()} tool: {e}"
|
||||||
raise RuntimeError(msg) from e
|
raise RuntimeError(msg) from e
|
||||||
|
|
||||||
# Parse this version and return the major/minor parts, since we don't need the
|
# Parse this version and return the major/minor parts, since we don't need the
|
||||||
|
@ -56,20 +72,12 @@ def get_runtime_version() -> Tuple[int, int]:
|
||||||
return (int(major), int(minor))
|
return (int(major), int(minor))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = (
|
msg = (
|
||||||
f"Could not parse the version of the {runtime.capitalize()} tool"
|
f"Could not parse the version of the {runtime_name.capitalize()} tool"
|
||||||
f" (found: '{version}') due to the following error: {e}"
|
f" (found: '{version}') due to the following error: {e}"
|
||||||
)
|
)
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
|
||||||
def get_runtime() -> str:
|
|
||||||
container_tech = get_runtime_name()
|
|
||||||
runtime = shutil.which(container_tech)
|
|
||||||
if runtime is None:
|
|
||||||
raise errors.NoContainerTechException(container_tech)
|
|
||||||
return runtime
|
|
||||||
|
|
||||||
|
|
||||||
def list_image_tags() -> List[str]:
|
def list_image_tags() -> List[str]:
|
||||||
"""Get the tags of all loaded Dangerzone images.
|
"""Get the tags of all loaded Dangerzone images.
|
||||||
|
|
||||||
|
|
|
@ -574,8 +574,15 @@ class WaitingWidgetContainer(WaitingWidget):
|
||||||
self.finished.emit()
|
self.finished.emit()
|
||||||
|
|
||||||
def state_change(self, state: str, error: Optional[str] = None) -> None:
|
def state_change(self, state: str, error: Optional[str] = None) -> None:
|
||||||
|
custom_runtime = self.dangerzone.settings.custom_runtime_specified()
|
||||||
|
|
||||||
if state == "not_installed":
|
if state == "not_installed":
|
||||||
if platform.system() == "Linux":
|
if custom_runtime:
|
||||||
|
self.show_error(
|
||||||
|
"<strong>We could not find the container runtime defined in your settings</strong><br><br>"
|
||||||
|
"Please check your settings, install it if needed, and retry."
|
||||||
|
)
|
||||||
|
elif platform.system() == "Linux":
|
||||||
self.show_error(
|
self.show_error(
|
||||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||||
"Install it and retry."
|
"Install it and retry."
|
||||||
|
@ -588,7 +595,12 @@ class WaitingWidgetContainer(WaitingWidget):
|
||||||
)
|
)
|
||||||
|
|
||||||
elif state == "not_running":
|
elif state == "not_running":
|
||||||
if platform.system() == "Linux":
|
if custom_runtime:
|
||||||
|
self.show_error(
|
||||||
|
"<strong>We were unable to start the container runtime defined in your settings</strong><br><br>"
|
||||||
|
"Please check your settings, install it if needed, and retry."
|
||||||
|
)
|
||||||
|
elif platform.system() == "Linux":
|
||||||
# "not_running" here means that the `podman image ls` command failed.
|
# "not_running" here means that the `podman image ls` command failed.
|
||||||
message = (
|
message = (
|
||||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||||
|
|
|
@ -143,7 +143,10 @@ class Container(IsolationProvider):
|
||||||
def check_docker_desktop_version(self) -> Tuple[bool, str]:
|
def check_docker_desktop_version(self) -> Tuple[bool, str]:
|
||||||
# On windows and darwin, check that the minimum version is met
|
# On windows and darwin, check that the minimum version is met
|
||||||
version = ""
|
version = ""
|
||||||
if platform.system() != "Linux":
|
runtime_is_docker = container_utils.get_runtime_name() == "docker"
|
||||||
|
platform_is_not_linux = platform.system() != "Linux"
|
||||||
|
|
||||||
|
if runtime_is_docker and platform_is_not_linux:
|
||||||
with subprocess.Popen(
|
with subprocess.Popen(
|
||||||
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
|
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
|
|
|
@ -39,6 +39,9 @@ class Settings:
|
||||||
"updater_errors": 0,
|
"updater_errors": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def custom_runtime_specified(self) -> bool:
|
||||||
|
return "container_runtime" in self.settings
|
||||||
|
|
||||||
def get(self, key: str) -> Any:
|
def get(self, key: str) -> Any:
|
||||||
return self.settings[key]
|
return self.settings[key]
|
||||||
|
|
||||||
|
|
|
@ -21,34 +21,25 @@ def get_qt_app() -> Application:
|
||||||
|
|
||||||
def generate_isolated_updater(
|
def generate_isolated_updater(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
monkeypatch: MonkeyPatch,
|
mocker: MockerFixture,
|
||||||
app_mocker: Optional[MockerFixture] = None,
|
mock_app: bool = False,
|
||||||
) -> UpdaterThread:
|
) -> UpdaterThread:
|
||||||
"""Generate an Updater class with its own settings."""
|
"""Generate an Updater class with its own settings."""
|
||||||
if app_mocker:
|
app = mocker.MagicMock() if mock_app else get_qt_app()
|
||||||
app = app_mocker.MagicMock()
|
|
||||||
else:
|
|
||||||
app = get_qt_app()
|
|
||||||
|
|
||||||
dummy = Dummy()
|
dummy = Dummy()
|
||||||
# XXX: We can monkey-patch global state without wrapping it in a context manager, or
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
# worrying that it will leak between tests, for two reasons:
|
|
||||||
#
|
|
||||||
# 1. Parallel tests in PyTest take place in different processes.
|
|
||||||
# 2. The monkeypatch fixture tears down the monkey-patch after each test ends.
|
|
||||||
monkeypatch.setattr(util, "get_config_dir", lambda: tmp_path)
|
|
||||||
dangerzone = DangerzoneGui(app, isolation_provider=dummy)
|
dangerzone = DangerzoneGui(app, isolation_provider=dummy)
|
||||||
updater = UpdaterThread(dangerzone)
|
updater = UpdaterThread(dangerzone)
|
||||||
return updater
|
return updater
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def updater(
|
def updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
return generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||||
) -> UpdaterThread:
|
|
||||||
return generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def qt_updater(tmp_path: Path, monkeypatch: MonkeyPatch) -> UpdaterThread:
|
def qt_updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||||
return generate_isolated_updater(tmp_path, monkeypatch)
|
return generate_isolated_updater(tmp_path, mocker, mock_app=False)
|
||||||
|
|
|
@ -48,9 +48,7 @@ def test_default_updater_settings(updater: UpdaterThread) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_pre_0_4_2_settings(
|
def test_pre_0_4_2_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
|
||||||
) -> None:
|
|
||||||
"""Check settings of installations prior to 0.4.2.
|
"""Check settings of installations prior to 0.4.2.
|
||||||
|
|
||||||
Check that installations that have been upgraded from a version < 0.4.2 to >= 0.4.2
|
Check that installations that have been upgraded from a version < 0.4.2 to >= 0.4.2
|
||||||
|
@ -58,7 +56,7 @@ def test_pre_0_4_2_settings(
|
||||||
in their settings.json file.
|
in their settings.json file.
|
||||||
"""
|
"""
|
||||||
save_settings(tmp_path, default_settings_0_4_1())
|
save_settings(tmp_path, default_settings_0_4_1())
|
||||||
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
updater = generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||||
assert (
|
assert (
|
||||||
updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
|
updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
|
||||||
)
|
)
|
||||||
|
@ -83,12 +81,10 @@ def test_post_0_4_2_settings(
|
||||||
# version is 0.4.3.
|
# version is 0.4.3.
|
||||||
expected_settings = default_updater_settings()
|
expected_settings = default_updater_settings()
|
||||||
expected_settings["updater_latest_version"] = "0.4.3"
|
expected_settings["updater_latest_version"] = "0.4.3"
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(settings, "get_version", lambda: "0.4.3")
|
||||||
settings, "get_version", lambda: expected_settings["updater_latest_version"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ensure that the Settings class will correct the latest version field to 0.4.3.
|
# Ensure that the Settings class will correct the latest version field to 0.4.3.
|
||||||
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
updater = generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||||
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
# Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4).
|
# Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4).
|
||||||
|
@ -118,9 +114,7 @@ def test_linux_no_check(updater: UpdaterThread, monkeypatch: MonkeyPatch) -> Non
|
||||||
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
|
|
||||||
def test_user_prompts(
|
def test_user_prompts(updater: UpdaterThread, mocker: MockerFixture) -> None:
|
||||||
updater: UpdaterThread, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
|
||||||
) -> None:
|
|
||||||
"""Test prompting users to ask them if they want to enable update checks."""
|
"""Test prompting users to ask them if they want to enable update checks."""
|
||||||
# First run
|
# First run
|
||||||
#
|
#
|
||||||
|
@ -370,8 +364,6 @@ def test_update_errors(
|
||||||
def test_update_check_prompt(
|
def test_update_check_prompt(
|
||||||
qtbot: QtBot,
|
qtbot: QtBot,
|
||||||
qt_updater: UpdaterThread,
|
qt_updater: UpdaterThread,
|
||||||
monkeypatch: MonkeyPatch,
|
|
||||||
mocker: MockerFixture,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that the prompt to enable update checks works properly."""
|
"""Test that the prompt to enable update checks works properly."""
|
||||||
# Force Dangerzone to check immediately for updates
|
# Force Dangerzone to check immediately for updates
|
||||||
|
|
|
@ -87,7 +87,7 @@ class TestContainer(IsolationProviderTest):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""When an image keep being not installed, it should return False"""
|
"""When an image keep being not installed, it should return False"""
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
["podman", "version", "-f", "{{.Client.Version}}"],
|
[container_utils.get_runtime(), "version", "-f", "{{.Client.Version}}"],
|
||||||
stdout="4.0.0",
|
stdout="4.0.0",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
28
tests/test_container_utils.py
Normal file
28
tests/test_container_utils.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from dangerzone.container_utils import get_runtime_name
|
||||||
|
from dangerzone.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_runtime_name_from_settings(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||||
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
settings.set("container_runtime", "new-kid-on-the-block", autosave=True)
|
||||||
|
|
||||||
|
assert get_runtime_name() == "new-kid-on-the-block"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_runtime_name_linux(mocker: MockerFixture) -> None:
|
||||||
|
mocker.patch("platform.system", return_value="Linux")
|
||||||
|
assert get_runtime_name() == "podman"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_runtime_name_non_linux(mocker: MockerFixture) -> None:
|
||||||
|
mocker.patch("platform.system", return_value="Windows")
|
||||||
|
assert get_runtime_name() == "docker"
|
||||||
|
|
||||||
|
mocker.patch("platform.system", return_value="Something else")
|
||||||
|
assert get_runtime_name() == "docker"
|
|
@ -21,6 +21,13 @@ def default_settings_0_4_1() -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def save_settings(tmp_path: Path, settings: dict) -> None:
|
||||||
|
"""Mimic the way Settings save a dictionary to a settings.json file."""
|
||||||
|
settings_filename = tmp_path / "settings.json"
|
||||||
|
with open(settings_filename, "w") as settings_file:
|
||||||
|
json.dump(settings, settings_file, indent=4)
|
||||||
|
|
||||||
|
|
||||||
def test_no_settings_file_creates_new_one(
|
def test_no_settings_file_creates_new_one(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
mocker: MockerFixture,
|
mocker: MockerFixture,
|
||||||
|
|
Loading…
Reference in a new issue