mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
Warn users if the minimum version of Docker Desktop is not met
This only happens on Windows and macOS. Fixes #693
This commit is contained in:
parent
c407e2ff84
commit
3d5cacfffb
4 changed files with 217 additions and 2 deletions
|
@ -124,6 +124,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
|
||||||
self.setWindowTitle("Dangerzone")
|
self.setWindowTitle("Dangerzone")
|
||||||
self.setWindowIcon(self.dangerzone.get_window_icon())
|
self.setWindowIcon(self.dangerzone.get_window_icon())
|
||||||
|
self.alert: Optional[Alert] = None
|
||||||
|
|
||||||
self.setMinimumWidth(600)
|
self.setMinimumWidth(600)
|
||||||
if platform.system() == "Darwin":
|
if platform.system() == "Darwin":
|
||||||
|
@ -226,6 +227,13 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
# This allows us to make QSS rules conditional on the OS color mode.
|
# This allows us to make QSS rules conditional on the OS color mode.
|
||||||
self.setProperty("OSColorMode", self.dangerzone.app.os_color_mode.value)
|
self.setProperty("OSColorMode", self.dangerzone.app.os_color_mode.value)
|
||||||
|
|
||||||
|
if hasattr(self.dangerzone.isolation_provider, "check_docker_desktop_version"):
|
||||||
|
is_version_valid, version = (
|
||||||
|
self.dangerzone.isolation_provider.check_docker_desktop_version()
|
||||||
|
)
|
||||||
|
if not is_version_valid:
|
||||||
|
self.handle_docker_desktop_version_check(is_version_valid, version)
|
||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def show_update_success(self) -> None:
|
def show_update_success(self) -> None:
|
||||||
|
@ -279,6 +287,46 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
self.dangerzone.settings.set("updater_check", check)
|
self.dangerzone.settings.set("updater_check", check)
|
||||||
self.dangerzone.settings.save()
|
self.dangerzone.settings.save()
|
||||||
|
|
||||||
|
def handle_docker_desktop_version_check(
|
||||||
|
self, is_version_valid: bool, version: str
|
||||||
|
) -> None:
|
||||||
|
hamburger_menu = self.hamburger_button.menu()
|
||||||
|
sep = hamburger_menu.insertSeparator(hamburger_menu.actions()[0])
|
||||||
|
upgrade_action = QAction("Docker Desktop should be upgraded", hamburger_menu)
|
||||||
|
upgrade_action.setIcon(
|
||||||
|
QtGui.QIcon(
|
||||||
|
load_svg_image(
|
||||||
|
"hamburger_menu_update_dot_error.svg", width=64, height=64
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
message = """
|
||||||
|
<p>A new version of Docker Desktop is available. Please upgrade your system.</p>
|
||||||
|
<p>Visit the <a href="https://www.docker.com/products/docker-desktop">Docker Desktop website</a> to download the latest version.</p>
|
||||||
|
<em>Keeping Docker Desktop up to date allows you to have more confidence that your documents are processed safely.</em>
|
||||||
|
"""
|
||||||
|
self.alert = Alert(
|
||||||
|
self.dangerzone,
|
||||||
|
title="Upgrade Docker Desktop",
|
||||||
|
message=message,
|
||||||
|
ok_text="Ok",
|
||||||
|
has_cancel=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _launch_alert() -> None:
|
||||||
|
if self.alert:
|
||||||
|
self.alert.launch()
|
||||||
|
|
||||||
|
upgrade_action.triggered.connect(_launch_alert)
|
||||||
|
hamburger_menu.insertAction(sep, upgrade_action)
|
||||||
|
|
||||||
|
self.hamburger_button.setIcon(
|
||||||
|
QtGui.QIcon(
|
||||||
|
load_svg_image("hamburger_menu_update_error.svg", width=64, height=64)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def handle_updates(self, report: UpdateReport) -> None:
|
def handle_updates(self, report: UpdateReport) -> None:
|
||||||
"""Handle update reports from the update checker thread.
|
"""Handle update reports from the update checker thread.
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import os
|
||||||
import platform
|
import platform
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import List
|
from typing import List, Tuple
|
||||||
|
|
||||||
from .. import container_utils, errors
|
from .. import container_utils, errors
|
||||||
from ..document import Document
|
from ..document import Document
|
||||||
|
@ -11,7 +11,10 @@ from ..util import get_resource_path, get_subprocess_startupinfo
|
||||||
from .base import IsolationProvider, terminate_process_group
|
from .base import IsolationProvider, terminate_process_group
|
||||||
|
|
||||||
TIMEOUT_KILL = 5 # Timeout in seconds until the kill command returns.
|
TIMEOUT_KILL = 5 # Timeout in seconds until the kill command returns.
|
||||||
|
MINIMUM_DOCKER_DESKTOP = {
|
||||||
|
"Darwin": "4.36.0",
|
||||||
|
"Windows": "4.36.0",
|
||||||
|
}
|
||||||
|
|
||||||
# Define startupinfo for subprocesses
|
# Define startupinfo for subprocesses
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
|
@ -121,6 +124,7 @@ class Container(IsolationProvider):
|
||||||
def is_available() -> bool:
|
def is_available() -> bool:
|
||||||
container_runtime = container_utils.get_runtime()
|
container_runtime = container_utils.get_runtime()
|
||||||
runtime_name = container_utils.get_runtime_name()
|
runtime_name = container_utils.get_runtime_name()
|
||||||
|
|
||||||
# Can we run `docker/podman image ls` without an error
|
# Can we run `docker/podman image ls` without an error
|
||||||
with subprocess.Popen(
|
with subprocess.Popen(
|
||||||
[container_runtime, "image", "ls"],
|
[container_runtime, "image", "ls"],
|
||||||
|
@ -135,6 +139,28 @@ class Container(IsolationProvider):
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def check_docker_desktop_version(self) -> Tuple[bool, str]:
|
||||||
|
# On windows and darwin, check that the minimum version is met
|
||||||
|
version = ""
|
||||||
|
if platform.system() != "Linux":
|
||||||
|
with subprocess.Popen(
|
||||||
|
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
|
) as p:
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
if p.returncode != 0:
|
||||||
|
# When an error occurs, consider that the check went
|
||||||
|
# through, as we're checking for installation compatibiliy
|
||||||
|
# somewhere else already
|
||||||
|
return True, version
|
||||||
|
# The output is like "Docker Desktop 4.35.1 (173168)"
|
||||||
|
version = stdout.decode().replace("Docker Desktop", "").split()[0]
|
||||||
|
if version < MINIMUM_DOCKER_DESKTOP[platform.system()]:
|
||||||
|
return False, version
|
||||||
|
return True, version
|
||||||
|
|
||||||
def doc_to_pixels_container_name(self, document: Document) -> str:
|
def doc_to_pixels_container_name(self, document: Document) -> str:
|
||||||
"""Unique container name for the doc-to-pixels phase."""
|
"""Unique container name for the doc-to-pixels phase."""
|
||||||
return f"dangerzone-doc-to-pixels-{document.id}"
|
return f"dangerzone-doc-to-pixels-{document.id}"
|
||||||
|
|
|
@ -587,3 +587,57 @@ def test_installation_failure_return_false(qtbot: QtBot, mocker: MockerFixture)
|
||||||
|
|
||||||
assert "the following error occured" in widget.label.text()
|
assert "the following error occured" in widget.label.text()
|
||||||
assert "The image cannot be found" in widget.traceback.toPlainText()
|
assert "The image cannot be found" in widget.traceback.toPlainText()
|
||||||
|
|
||||||
|
|
||||||
|
def test_up_to_date_docker_desktop_does_nothing(
|
||||||
|
qtbot: QtBot, mocker: MockerFixture
|
||||||
|
) -> None:
|
||||||
|
# Setup install to return False
|
||||||
|
mock_app = mocker.MagicMock()
|
||||||
|
dummy = mocker.MagicMock(spec=Container)
|
||||||
|
dummy.check_docker_desktop_version.return_value = (True, "1.0.0")
|
||||||
|
dz = DangerzoneGui(mock_app, dummy)
|
||||||
|
|
||||||
|
window = MainWindow(dz)
|
||||||
|
qtbot.addWidget(window)
|
||||||
|
|
||||||
|
menu_actions = window.hamburger_button.menu().actions()
|
||||||
|
assert "Docker Desktop should be upgraded" not in [
|
||||||
|
a.toolTip() for a in menu_actions
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_outdated_docker_desktop_displays_warning(
|
||||||
|
qtbot: QtBot, mocker: MockerFixture
|
||||||
|
) -> None:
|
||||||
|
# Setup install to return False
|
||||||
|
mock_app = mocker.MagicMock()
|
||||||
|
dummy = mocker.MagicMock(spec=Container)
|
||||||
|
dummy.check_docker_desktop_version.return_value = (False, "1.0.0")
|
||||||
|
|
||||||
|
dz = DangerzoneGui(mock_app, dummy)
|
||||||
|
|
||||||
|
load_svg_spy = mocker.spy(main_window_module, "load_svg_image")
|
||||||
|
|
||||||
|
window = MainWindow(dz)
|
||||||
|
qtbot.addWidget(window)
|
||||||
|
|
||||||
|
menu_actions = window.hamburger_button.menu().actions()
|
||||||
|
assert menu_actions[0].toolTip() == "Docker Desktop should be upgraded"
|
||||||
|
|
||||||
|
# Check that the hamburger icon has changed with the expected SVG image.
|
||||||
|
assert load_svg_spy.call_count == 4
|
||||||
|
assert (
|
||||||
|
load_svg_spy.call_args_list[2].args[0] == "hamburger_menu_update_dot_error.svg"
|
||||||
|
)
|
||||||
|
|
||||||
|
alert_spy = mocker.spy(window.alert, "launch")
|
||||||
|
|
||||||
|
# Clicking the menu item should open a warning message
|
||||||
|
def _check_alert_displayed() -> None:
|
||||||
|
alert_spy.assert_any_call()
|
||||||
|
if window.alert:
|
||||||
|
window.alert.close()
|
||||||
|
|
||||||
|
QtCore.QTimer.singleShot(0, _check_alert_displayed)
|
||||||
|
menu_actions[0].trigger()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
@ -108,6 +109,92 @@ class TestContainer(IsolationProviderTest):
|
||||||
with pytest.raises(errors.ImageNotPresentException):
|
with pytest.raises(errors.ImageNotPresentException):
|
||||||
provider.install()
|
provider.install()
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
platform.system() not in ("Windows", "Darwin"),
|
||||||
|
reason="macOS and Windows specific",
|
||||||
|
)
|
||||||
|
def test_old_docker_desktop_version_is_detected(
|
||||||
|
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
|
||||||
|
) -> None:
|
||||||
|
fp.register_subprocess(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"version",
|
||||||
|
"--format",
|
||||||
|
"{{.Server.Platform.Name}}",
|
||||||
|
],
|
||||||
|
stdout="Docker Desktop 1.0.0 (173100)",
|
||||||
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"dangerzone.isolation_provider.container.MINIMUM_DOCKER_DESKTOP",
|
||||||
|
{"Darwin": "1.0.1", "Windows": "1.0.1"},
|
||||||
|
)
|
||||||
|
assert (False, "1.0.0") == provider.check_docker_desktop_version()
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
platform.system() not in ("Windows", "Darwin"),
|
||||||
|
reason="macOS and Windows specific",
|
||||||
|
)
|
||||||
|
def test_up_to_date_docker_desktop_version_is_detected(
|
||||||
|
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
|
||||||
|
) -> None:
|
||||||
|
fp.register_subprocess(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"version",
|
||||||
|
"--format",
|
||||||
|
"{{.Server.Platform.Name}}",
|
||||||
|
],
|
||||||
|
stdout="Docker Desktop 1.0.1 (173100)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Require version 1.0.1
|
||||||
|
mocker.patch(
|
||||||
|
"dangerzone.isolation_provider.container.MINIMUM_DOCKER_DESKTOP",
|
||||||
|
{"Darwin": "1.0.1", "Windows": "1.0.1"},
|
||||||
|
)
|
||||||
|
assert (True, "1.0.1") == provider.check_docker_desktop_version()
|
||||||
|
|
||||||
|
fp.register_subprocess(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"version",
|
||||||
|
"--format",
|
||||||
|
"{{.Server.Platform.Name}}",
|
||||||
|
],
|
||||||
|
stdout="Docker Desktop 2.0.0 (173100)",
|
||||||
|
)
|
||||||
|
assert (True, "2.0.0") == provider.check_docker_desktop_version()
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
platform.system() not in ("Windows", "Darwin"),
|
||||||
|
reason="macOS and Windows specific",
|
||||||
|
)
|
||||||
|
def test_docker_desktop_version_failure_returns_true(
|
||||||
|
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
|
||||||
|
) -> None:
|
||||||
|
fp.register_subprocess(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"version",
|
||||||
|
"--format",
|
||||||
|
"{{.Server.Platform.Name}}",
|
||||||
|
],
|
||||||
|
stderr="Oopsie",
|
||||||
|
returncode=1,
|
||||||
|
)
|
||||||
|
assert provider.check_docker_desktop_version() == (True, "")
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
platform.system() != "Linux",
|
||||||
|
reason="Linux specific",
|
||||||
|
)
|
||||||
|
def test_linux_skips_desktop_version_check_returns_true(
|
||||||
|
self, mocker: MockerFixture, provider: Container
|
||||||
|
) -> None:
|
||||||
|
assert (True, "") == provider.check_docker_desktop_version()
|
||||||
|
|
||||||
|
|
||||||
class TestContainerTermination(IsolationProviderTermination):
|
class TestContainerTermination(IsolationProviderTermination):
|
||||||
pass
|
pass
|
||||||
|
|
Loading…
Reference in a new issue