mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-05-05 05:01:49 +02:00
Compare commits
13 commits
dfcb74b427
...
e8ca12eb11
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e8ca12eb11 | ||
![]() |
491cca6341 | ||
![]() |
0a7b79f61a | ||
![]() |
86eab5d222 | ||
![]() |
ed39c056bb | ||
![]() |
983622fe59 | ||
![]() |
8e99764952 | ||
![]() |
20cd9cfc5c | ||
![]() |
f082641b71 | ||
![]() |
c0215062bc | ||
![]() |
b551a4dec4 | ||
![]() |
5a56a7f055 | ||
![]() |
ab6dd9c01d |
22 changed files with 334 additions and 191 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -16,6 +16,22 @@ since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.or
|
|||
- Document Operating System support [#986](https://github.com/freedomofpress/dangerzone/issues/986)
|
||||
- Tests: Look for regressions when converting PDFs [#321](https://github.com/freedomofpress/dangerzone/issues/321)
|
||||
|
||||
## Added
|
||||
|
||||
- (experimental): It is now possible to specify a custom container runtime in
|
||||
the settings, by using the `container_runtime` key. It should contain the path
|
||||
to the container runtime you want to use. Please note that this doesn't mean
|
||||
we support more container runtimes than Podman and Docker for the time being,
|
||||
but enables you to chose which one you want to use, independently of your
|
||||
platform. ([#925](https://github.com/freedomofpress/dangerzone/issues/925))
|
||||
|
||||
### Changed
|
||||
|
||||
- The `debian` base image is now fetched by digest. As a result, your local
|
||||
container storage will no longer show a tag for this dependency.
|
||||
- The `debian` base image is now referenced with a fully qualified URI,
|
||||
including the registry hostname.
|
||||
|
||||
## [0.8.1](https://github.com/freedomofpress/dangerzone/compare/v0.8.1...0.8.0)
|
||||
|
||||
- Update the container image
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
# Dockerfile args below. For more info about this file, read
|
||||
# docs/developer/reproducibility.md.
|
||||
|
||||
ARG DEBIAN_IMAGE_DATE=20250224
|
||||
ARG DEBIAN_IMAGE_DIGEST=sha256:12c396bd585df7ec21d5679bb6a83d4878bc4415ce926c9e5ea6426d23c60bdc
|
||||
|
||||
FROM debian:bookworm-${DEBIAN_IMAGE_DATE}-slim AS dangerzone-image
|
||||
FROM docker.io/library/debian@${DEBIAN_IMAGE_DIGEST} AS dangerzone-image
|
||||
|
||||
ARG GVISOR_ARCHIVE_DATE=20250217
|
||||
ARG DEBIAN_ARCHIVE_DATE=20250226
|
||||
|
@ -185,8 +185,8 @@ RUN mkdir -p \
|
|||
# Copy the /etc and /var directories under the new root directory. Also,
|
||||
# copy /etc/, /opt, and /usr to the Dangerzone image rootfs.
|
||||
#
|
||||
# NOTE: We also have to remove the resolv.conf file, in order to not leak any DNS
|
||||
# servers added there during image build time.
|
||||
# NOTE: We also have to remove the resolv.conf file, in order to not leak any
|
||||
# DNS servers added there during image build time.
|
||||
RUN cp -r /etc /var /new_root/ \
|
||||
&& rm /new_root/etc/resolv.conf
|
||||
RUN cp -r /etc /opt /usr /new_root/home/dangerzone/dangerzone-image/rootfs \
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
# Can be bumped to the latest date in https://hub.docker.com/_/debian/tags?name=bookworm-
|
||||
DEBIAN_IMAGE_DATE=20250224
|
||||
# Should be the INDEX DIGEST from an image tagged `bookworm-<DATE>-slim`:
|
||||
# https://hub.docker.com/_/debian/tags?name=bookworm-
|
||||
#
|
||||
# Tag for this digest: bookworm-20250224-slim
|
||||
DEBIAN_IMAGE_DIGEST=sha256:12c396bd585df7ec21d5679bb6a83d4878bc4415ce926c9e5ea6426d23c60bdc
|
||||
# Can be bumped to today's date
|
||||
DEBIAN_ARCHIVE_DATE=20250226
|
||||
# Can be bumped to the latest date in https://github.com/google/gvisor/tags
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
# Dockerfile args below. For more info about this file, read
|
||||
# docs/developer/reproducibility.md.
|
||||
|
||||
ARG DEBIAN_IMAGE_DATE={{DEBIAN_IMAGE_DATE}}
|
||||
ARG DEBIAN_IMAGE_DIGEST={{DEBIAN_IMAGE_DIGEST}}
|
||||
|
||||
FROM debian:bookworm-${DEBIAN_IMAGE_DATE}-slim AS dangerzone-image
|
||||
FROM docker.io/library/debian@${DEBIAN_IMAGE_DIGEST} AS dangerzone-image
|
||||
|
||||
ARG GVISOR_ARCHIVE_DATE={{GVISOR_ARCHIVE_DATE}}
|
||||
ARG DEBIAN_ARCHIVE_DATE={{DEBIAN_ARCHIVE_DATE}}
|
||||
|
|
|
@ -11,6 +11,7 @@ from .isolation_provider.container import Container
|
|||
from .isolation_provider.dummy import Dummy
|
||||
from .isolation_provider.qubes import Qubes, is_qubes_native_conversion
|
||||
from .logic import DangerzoneCore
|
||||
from .settings import Settings
|
||||
from .util import get_version, replace_control_chars
|
||||
|
||||
|
||||
|
@ -37,7 +38,7 @@ def print_header(s: str) -> None:
|
|||
)
|
||||
@click.argument(
|
||||
"filenames",
|
||||
required=True,
|
||||
required=False,
|
||||
nargs=-1,
|
||||
type=click.UNPROCESSED,
|
||||
callback=args.validate_input_filenames,
|
||||
|
@ -48,17 +49,33 @@ def print_header(s: str) -> None:
|
|||
flag_value=True,
|
||||
help="Run Dangerzone in debug mode, to get logs from gVisor.",
|
||||
)
|
||||
@click.option(
|
||||
"--set-container-runtime",
|
||||
required=False,
|
||||
help="The path to the container runtime you want to set in the settings",
|
||||
)
|
||||
@click.version_option(version=get_version(), message="%(version)s")
|
||||
@errors.handle_document_errors
|
||||
def cli_main(
|
||||
output_filename: Optional[str],
|
||||
ocr_lang: Optional[str],
|
||||
filenames: List[str],
|
||||
filenames: Optional[List[str]],
|
||||
archive: bool,
|
||||
dummy_conversion: bool,
|
||||
debug: bool,
|
||||
set_container_runtime: Optional[str] = None,
|
||||
) -> None:
|
||||
setup_logging()
|
||||
display_banner()
|
||||
if set_container_runtime:
|
||||
settings = Settings()
|
||||
container_runtime = settings.set_custom_runtime(
|
||||
set_container_runtime, autosave=True
|
||||
)
|
||||
click.echo(f"Set the settings container_runtime to {container_runtime}")
|
||||
sys.exit(0)
|
||||
elif not filenames:
|
||||
raise click.UsageError("Missing argument 'FILENAMES...'")
|
||||
|
||||
if getattr(sys, "dangerzone_dev", False) and dummy_conversion:
|
||||
dangerzone = DangerzoneCore(Dummy())
|
||||
|
@ -67,7 +84,6 @@ def cli_main(
|
|||
else:
|
||||
dangerzone = DangerzoneCore(Container(debug=debug))
|
||||
|
||||
display_banner()
|
||||
if len(filenames) == 1 and output_filename:
|
||||
dangerzone.add_document_from_filename(filenames[0], output_filename, archive)
|
||||
elif len(filenames) > 1 and output_filename:
|
||||
|
@ -320,4 +336,10 @@ def display_banner() -> None:
|
|||
+ Style.DIM
|
||||
+ "│"
|
||||
)
|
||||
print(Back.BLACK + Fore.YELLOW + Style.DIM + "╰──────────────────────────╯")
|
||||
print(
|
||||
Back.BLACK
|
||||
+ Fore.YELLOW
|
||||
+ Style.DIM
|
||||
+ "╰──────────────────────────╯"
|
||||
+ Style.RESET_ALL
|
||||
)
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import List, Tuple
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from . import errors
|
||||
from .settings import Settings
|
||||
from .util import get_resource_path, get_subprocess_startupinfo
|
||||
|
||||
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
||||
|
@ -12,16 +15,47 @@ CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_runtime_name() -> str:
|
||||
if platform.system() == "Linux":
|
||||
runtime_name = "podman"
|
||||
else:
|
||||
# Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually
|
||||
runtime_name = "docker"
|
||||
return runtime_name
|
||||
class Runtime(object):
|
||||
"""Represents the container runtime to use.
|
||||
|
||||
- It can be specified via the settings, using the "container_runtime" key,
|
||||
which should point to the full path of the runtime;
|
||||
- If the runtime is not specified via the settings, it defaults
|
||||
to "podman" on Linux and "docker" on macOS and Windows.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
settings = Settings()
|
||||
|
||||
if settings.custom_runtime_specified():
|
||||
self.path = Path(settings.get("container_runtime"))
|
||||
if not self.path.exists():
|
||||
raise errors.UnsupportedContainerRuntime(self.path)
|
||||
self.name = self.path.stem
|
||||
else:
|
||||
self.name = self.get_default_runtime_name()
|
||||
self.path = Runtime.path_from_name(self.name)
|
||||
|
||||
if self.name not in ("podman", "docker"):
|
||||
raise errors.UnsupportedContainerRuntime(self.name)
|
||||
|
||||
@staticmethod
|
||||
def path_from_name(name: str) -> Path:
|
||||
name_path = Path(name)
|
||||
if name_path.is_file():
|
||||
return name_path
|
||||
else:
|
||||
runtime = shutil.which(name_path)
|
||||
if runtime is None:
|
||||
raise errors.NoContainerTechException(name)
|
||||
return Path(runtime)
|
||||
|
||||
@staticmethod
|
||||
def get_default_runtime_name() -> str:
|
||||
return "podman" if platform.system() == "Linux" else "docker"
|
||||
|
||||
|
||||
def get_runtime_version() -> Tuple[int, int]:
|
||||
def get_runtime_version(runtime: Optional[Runtime] = None) -> Tuple[int, int]:
|
||||
"""Get the major/minor parts of the Docker/Podman version.
|
||||
|
||||
Some of the operations we perform in this module rely on some Podman features
|
||||
|
@ -30,14 +64,15 @@ def get_runtime_version() -> Tuple[int, int]:
|
|||
just knowing the major and minor version, since writing/installing a full-blown
|
||||
semver parser is an overkill.
|
||||
"""
|
||||
runtime = runtime or Runtime()
|
||||
|
||||
# Get the Docker/Podman version, using a Go template.
|
||||
runtime = get_runtime_name()
|
||||
if runtime == "podman":
|
||||
if runtime.name == "podman":
|
||||
query = "{{.Client.Version}}"
|
||||
else:
|
||||
query = "{{.Server.Version}}"
|
||||
|
||||
cmd = [runtime, "version", "-f", query]
|
||||
cmd = [str(runtime.path), "version", "-f", query]
|
||||
try:
|
||||
version = subprocess.run(
|
||||
cmd,
|
||||
|
@ -46,7 +81,7 @@ def get_runtime_version() -> Tuple[int, int]:
|
|||
check=True,
|
||||
).stdout.decode()
|
||||
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
|
||||
|
||||
# Parse this version and return the major/minor parts, since we don't need the
|
||||
|
@ -56,20 +91,12 @@ def get_runtime_version() -> Tuple[int, int]:
|
|||
return (int(major), int(minor))
|
||||
except Exception as e:
|
||||
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}"
|
||||
)
|
||||
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]:
|
||||
"""Get the tags of all loaded Dangerzone images.
|
||||
|
||||
|
@ -77,10 +104,11 @@ def list_image_tags() -> List[str]:
|
|||
images. This can be useful when we want to find which are the local image tags,
|
||||
and which image ID does the "latest" tag point to.
|
||||
"""
|
||||
runtime = Runtime()
|
||||
return (
|
||||
subprocess.check_output(
|
||||
[
|
||||
get_runtime(),
|
||||
str(runtime.path),
|
||||
"image",
|
||||
"list",
|
||||
"--format",
|
||||
|
@ -97,19 +125,21 @@ def list_image_tags() -> List[str]:
|
|||
|
||||
def add_image_tag(image_id: str, new_tag: str) -> None:
|
||||
"""Add a tag to the Dangerzone image."""
|
||||
runtime = Runtime()
|
||||
log.debug(f"Adding tag '{new_tag}' to image '{image_id}'")
|
||||
subprocess.check_output(
|
||||
[get_runtime(), "tag", image_id, new_tag],
|
||||
[str(runtime.path), "tag", image_id, new_tag],
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
|
||||
|
||||
def delete_image_tag(tag: str) -> None:
|
||||
"""Delete a Dangerzone image tag."""
|
||||
runtime = Runtime()
|
||||
log.warning(f"Deleting old container image: {tag}")
|
||||
try:
|
||||
subprocess.check_output(
|
||||
[get_runtime(), "rmi", "--force", tag],
|
||||
[str(runtime.name), "rmi", "--force", tag],
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
except Exception as e:
|
||||
|
@ -121,16 +151,17 @@ def delete_image_tag(tag: str) -> None:
|
|||
|
||||
def get_expected_tag() -> str:
|
||||
"""Get the tag of the Dangerzone image tarball from the image-id.txt file."""
|
||||
with open(get_resource_path("image-id.txt")) as f:
|
||||
with get_resource_path("image-id.txt").open() as f:
|
||||
return f.read().strip()
|
||||
|
||||
|
||||
def load_image_tarball() -> None:
|
||||
runtime = Runtime()
|
||||
log.info("Installing Dangerzone container image...")
|
||||
tarball_path = get_resource_path("container.tar")
|
||||
try:
|
||||
res = subprocess.run(
|
||||
[get_runtime(), "load", "-i", tarball_path],
|
||||
[str(runtime.path), "load", "-i", str(tarball_path)],
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
capture_output=True,
|
||||
check=True,
|
||||
|
@ -155,7 +186,7 @@ def load_image_tarball() -> None:
|
|||
# `share/image-id.txt` and delete the incorrect tag.
|
||||
#
|
||||
# [1] https://github.com/containers/podman/issues/16490
|
||||
if get_runtime_name() == "podman" and get_runtime_version() == (3, 4):
|
||||
if runtime.name == "podman" and get_runtime_version(runtime) == (3, 4):
|
||||
expected_tag = get_expected_tag()
|
||||
bad_tag = f"localhost/{expected_tag}:latest"
|
||||
good_tag = f"{CONTAINER_NAME}:{expected_tag}"
|
||||
|
|
|
@ -140,3 +140,7 @@ class NotAvailableContainerTechException(Exception):
|
|||
self.error = error
|
||||
self.container_tech = container_tech
|
||||
super().__init__(f"{container_tech} is not available")
|
||||
|
||||
|
||||
class UnsupportedContainerRuntime(Exception):
|
||||
pass
|
||||
|
|
|
@ -51,7 +51,7 @@ class Application(QtWidgets.QApplication):
|
|||
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
||||
super(Application, self).__init__(*args, **kwargs)
|
||||
self.setQuitOnLastWindowClosed(False)
|
||||
with open(get_resource_path("dangerzone.css"), "r") as f:
|
||||
with get_resource_path("dangerzone.css").open("r") as f:
|
||||
style = f.read()
|
||||
self.setStyleSheet(style)
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ class DangerzoneGui(DangerzoneCore):
|
|||
path = get_resource_path("dangerzone.ico")
|
||||
else:
|
||||
path = get_resource_path("icon.png")
|
||||
return QtGui.QIcon(path)
|
||||
return QtGui.QIcon(str(path))
|
||||
|
||||
def open_pdf_viewer(self, filename: str) -> None:
|
||||
if platform.system() == "Darwin":
|
||||
|
@ -252,7 +252,7 @@ class Alert(Dialog):
|
|||
def create_layout(self) -> QtWidgets.QBoxLayout:
|
||||
logo = QtWidgets.QLabel()
|
||||
logo.setPixmap(
|
||||
QtGui.QPixmap.fromImage(QtGui.QImage(get_resource_path("icon.png")))
|
||||
QtGui.QPixmap.fromImage(QtGui.QImage(str(get_resource_path("icon.png"))))
|
||||
)
|
||||
|
||||
label = QtWidgets.QLabel()
|
||||
|
|
|
@ -62,7 +62,7 @@ def load_svg_image(filename: str, width: int, height: int) -> QtGui.QPixmap:
|
|||
This answer is basically taken from: https://stackoverflow.com/a/25689790
|
||||
"""
|
||||
path = get_resource_path(filename)
|
||||
svg_renderer = QtSvg.QSvgRenderer(path)
|
||||
svg_renderer = QtSvg.QSvgRenderer(str(path))
|
||||
image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
|
@ -130,9 +130,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
# Header
|
||||
logo = QtWidgets.QLabel()
|
||||
logo.setPixmap(
|
||||
QtGui.QPixmap.fromImage(QtGui.QImage(get_resource_path("icon.png")))
|
||||
)
|
||||
icon_path = str(get_resource_path("icon.png"))
|
||||
logo.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(icon_path)))
|
||||
header_label = QtWidgets.QLabel("Dangerzone")
|
||||
header_label.setFont(self.dangerzone.fixed_font)
|
||||
header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }")
|
||||
|
@ -222,11 +221,14 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
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)
|
||||
try:
|
||||
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)
|
||||
except errors.UnsupportedContainerRuntime as e:
|
||||
pass # It's catched later in the flow.
|
||||
|
||||
self.show()
|
||||
|
||||
|
@ -575,8 +577,15 @@ class WaitingWidgetContainer(WaitingWidget):
|
|||
self.finished.emit()
|
||||
|
||||
def state_change(self, state: str, error: Optional[str] = None) -> None:
|
||||
custom_runtime = self.dangerzone.settings.custom_runtime_specified()
|
||||
|
||||
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(
|
||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||
"Install it and retry."
|
||||
|
@ -589,19 +598,25 @@ class WaitingWidgetContainer(WaitingWidget):
|
|||
)
|
||||
|
||||
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.
|
||||
message = (
|
||||
self.show_error(
|
||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||
"Podman is installed but cannot run properly. See errors below"
|
||||
"Podman is installed but cannot run properly. See errors below",
|
||||
error,
|
||||
)
|
||||
else:
|
||||
message = (
|
||||
self.show_error(
|
||||
"<strong>Dangerzone requires Docker Desktop</strong><br><br>"
|
||||
"Docker is installed but isn't running.<br><br>"
|
||||
"Open Docker and make sure it's running in the background."
|
||||
"Open Docker and make sure it's running in the background.",
|
||||
error,
|
||||
)
|
||||
self.show_error(message, error)
|
||||
else:
|
||||
self.show_message(
|
||||
"Installing the Dangerzone container image.<br><br>"
|
||||
|
@ -1306,7 +1321,7 @@ class DocumentWidget(QtWidgets.QWidget):
|
|||
|
||||
def load_status_image(self, filename: str) -> QtGui.QPixmap:
|
||||
path = get_resource_path(filename)
|
||||
img = QtGui.QImage(path)
|
||||
img = QtGui.QImage(str(path))
|
||||
image = QtGui.QPixmap.fromImage(img)
|
||||
return image.scaled(QtCore.QSize(15, 15))
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import subprocess
|
|||
from typing import List, Tuple
|
||||
|
||||
from .. import container_utils, errors
|
||||
from ..container_utils import Runtime
|
||||
from ..document import Document
|
||||
from ..util import get_resource_path, get_subprocess_startupinfo
|
||||
from .base import IsolationProvider, terminate_process_group
|
||||
|
@ -50,7 +51,8 @@ class Container(IsolationProvider):
|
|||
* Do not map the host user to the container, with `--userns nomap` (available
|
||||
from Podman 4.1 onwards)
|
||||
"""
|
||||
if container_utils.get_runtime_name() == "podman":
|
||||
runtime = Runtime()
|
||||
if runtime.name == "podman":
|
||||
security_args = ["--log-driver", "none"]
|
||||
security_args += ["--security-opt", "no-new-privileges"]
|
||||
if container_utils.get_runtime_version() >= (4, 1):
|
||||
|
@ -64,7 +66,7 @@ class Container(IsolationProvider):
|
|||
#
|
||||
# [1] https://github.com/freedomofpress/dangerzone/issues/846
|
||||
# [2] https://github.com/containers/common/blob/d3283f8401eeeb21f3c59a425b5461f069e199a7/pkg/seccomp/seccomp.json
|
||||
seccomp_json_path = get_resource_path("seccomp.gvisor.json")
|
||||
seccomp_json_path = str(get_resource_path("seccomp.gvisor.json"))
|
||||
security_args += ["--security-opt", f"seccomp={seccomp_json_path}"]
|
||||
|
||||
security_args += ["--cap-drop", "all"]
|
||||
|
@ -123,12 +125,11 @@ class Container(IsolationProvider):
|
|||
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
container_runtime = container_utils.get_runtime()
|
||||
runtime_name = container_utils.get_runtime_name()
|
||||
runtime = Runtime()
|
||||
|
||||
# Can we run `docker/podman image ls` without an error
|
||||
with subprocess.Popen(
|
||||
[container_runtime, "image", "ls"],
|
||||
[str(runtime.path), "image", "ls"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
|
@ -136,14 +137,18 @@ class Container(IsolationProvider):
|
|||
_, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise errors.NotAvailableContainerTechException(
|
||||
runtime_name, stderr.decode()
|
||||
runtime.name, stderr.decode()
|
||||
)
|
||||
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":
|
||||
runtime = Runtime()
|
||||
runtime_is_docker = runtime.name == "docker"
|
||||
platform_is_not_linux = platform.system() != "Linux"
|
||||
|
||||
if runtime_is_docker and platform_is_not_linux:
|
||||
with subprocess.Popen(
|
||||
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
|
||||
stdout=subprocess.PIPE,
|
||||
|
@ -193,7 +198,7 @@ class Container(IsolationProvider):
|
|||
command: List[str],
|
||||
name: str,
|
||||
) -> subprocess.Popen:
|
||||
container_runtime = container_utils.get_runtime()
|
||||
runtime = Runtime()
|
||||
security_args = self.get_runtime_security_args()
|
||||
debug_args = []
|
||||
if self.debug:
|
||||
|
@ -215,7 +220,7 @@ class Container(IsolationProvider):
|
|||
+ image_name
|
||||
+ command
|
||||
)
|
||||
return self.exec([container_runtime] + args)
|
||||
return self.exec([str(runtime.path)] + args)
|
||||
|
||||
def kill_container(self, name: str) -> None:
|
||||
"""Terminate a spawned container.
|
||||
|
@ -227,8 +232,8 @@ class Container(IsolationProvider):
|
|||
connected to the Docker daemon, and killing it will just close the associated
|
||||
standard streams.
|
||||
"""
|
||||
container_runtime = container_utils.get_runtime()
|
||||
cmd = [container_runtime, "kill", name]
|
||||
runtime = Runtime()
|
||||
cmd = [str(runtime.path), "kill", name]
|
||||
try:
|
||||
# We do not check the exit code of the process here, since the container may
|
||||
# have stopped right before invoking this command. In that case, the
|
||||
|
@ -284,10 +289,10 @@ class Container(IsolationProvider):
|
|||
# after a podman kill / docker kill invocation, this will likely be the case,
|
||||
# else the container runtime (Docker/Podman) has experienced a problem, and we
|
||||
# should report it.
|
||||
container_runtime = container_utils.get_runtime()
|
||||
runtime = Runtime()
|
||||
name = self.doc_to_pixels_container_name(document)
|
||||
all_containers = subprocess.run(
|
||||
[container_runtime, "ps", "-a"],
|
||||
[str(runtime.path), "ps", "-a"],
|
||||
capture_output=True,
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
|
@ -298,19 +303,20 @@ class Container(IsolationProvider):
|
|||
# FIXME hardcoded 1 until length conversions are better handled
|
||||
# https://github.com/freedomofpress/dangerzone/issues/257
|
||||
return 1
|
||||
runtime = Runtime() # type: ignore [unreachable]
|
||||
|
||||
n_cpu = 1 # type: ignore [unreachable]
|
||||
n_cpu = 1
|
||||
if platform.system() == "Linux":
|
||||
# if on linux containers run natively
|
||||
cpu_count = os.cpu_count()
|
||||
if cpu_count is not None:
|
||||
n_cpu = cpu_count
|
||||
|
||||
elif container_utils.get_runtime_name() == "docker":
|
||||
elif runtime.name == "docker":
|
||||
# For Windows and MacOS containers run in VM
|
||||
# So we obtain the CPU count for the VM
|
||||
n_cpu_str = subprocess.check_output(
|
||||
[container_utils.get_runtime(), "info", "--format", "{{.NCPU}}"],
|
||||
[str(runtime.path), "info", "--format", "{{.NCPU}}"],
|
||||
text=True,
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
|
|
|
@ -130,7 +130,6 @@ def is_qubes_native_conversion() -> bool:
|
|||
# This disambiguates if it is running a Qubes targetted build or not
|
||||
# (Qubes-specific builds don't ship the container image)
|
||||
|
||||
container_image_path = get_resource_path("container.tar")
|
||||
return not os.path.exists(container_image_path)
|
||||
return not get_resource_path("container.tar").exists()
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -23,16 +23,13 @@ class DangerzoneCore(object):
|
|||
# Initialize terminal colors
|
||||
colorama.init(autoreset=True)
|
||||
|
||||
# App data folder
|
||||
self.appdata_path = util.get_config_dir()
|
||||
|
||||
# Languages supported by tesseract
|
||||
with open(get_resource_path("ocr-languages.json"), "r") as f:
|
||||
with get_resource_path("ocr-languages.json").open("r") as f:
|
||||
unsorted_ocr_languages = json.load(f)
|
||||
self.ocr_languages = dict(sorted(unsorted_ocr_languages.items()))
|
||||
|
||||
# Load settings
|
||||
self.settings = Settings(self)
|
||||
self.settings = Settings()
|
||||
self.documents: List[Document] = []
|
||||
self.isolation_provider = isolation_provider
|
||||
|
||||
|
|
|
@ -1,29 +1,24 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
|
||||
from packaging import version
|
||||
|
||||
from .document import SAFE_EXTENSION
|
||||
from .util import get_version
|
||||
from .util import get_config_dir, get_version
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .logic import DangerzoneCore
|
||||
|
||||
SETTINGS_FILENAME: str = "settings.json"
|
||||
|
||||
|
||||
class Settings:
|
||||
settings: Dict[str, Any]
|
||||
|
||||
def __init__(self, dangerzone: "DangerzoneCore") -> None:
|
||||
self.dangerzone = dangerzone
|
||||
self.settings_filename = os.path.join(
|
||||
self.dangerzone.appdata_path, SETTINGS_FILENAME
|
||||
)
|
||||
def __init__(self) -> None:
|
||||
self.settings_filename = get_config_dir() / SETTINGS_FILENAME
|
||||
self.default_settings: Dict[str, Any] = self.generate_default_settings()
|
||||
self.load()
|
||||
|
||||
|
@ -45,6 +40,18 @@ class Settings:
|
|||
"updater_errors": 0,
|
||||
}
|
||||
|
||||
def custom_runtime_specified(self) -> bool:
|
||||
return "container_runtime" in self.settings
|
||||
|
||||
def set_custom_runtime(self, runtime: str, autosave: bool = False) -> Path:
|
||||
from .container_utils import Runtime # Avoid circular import
|
||||
|
||||
container_runtime = Runtime.path_from_name(runtime)
|
||||
self.settings["container_runtime"] = str(container_runtime)
|
||||
if autosave:
|
||||
self.save()
|
||||
return container_runtime
|
||||
|
||||
def get(self, key: str) -> Any:
|
||||
return self.settings[key]
|
||||
|
||||
|
@ -91,6 +98,6 @@ class Settings:
|
|||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
os.makedirs(self.dangerzone.appdata_path, exist_ok=True)
|
||||
with open(self.settings_filename, "w") as settings_file:
|
||||
self.settings_filename.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self.settings_filename.open("w") as settings_file:
|
||||
json.dump(self.settings, settings_file, indent=4)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import pathlib
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import platformdirs
|
||||
|
@ -11,40 +11,39 @@ except ImportError:
|
|||
import appdirs as platformdirs
|
||||
|
||||
|
||||
def get_config_dir() -> str:
|
||||
return platformdirs.user_config_dir("dangerzone")
|
||||
def get_config_dir() -> Path:
|
||||
return Path(platformdirs.user_config_dir("dangerzone"))
|
||||
|
||||
|
||||
def get_resource_path(filename: str) -> str:
|
||||
def get_resource_path(filename: str) -> Path:
|
||||
if getattr(sys, "dangerzone_dev", False):
|
||||
# Look for resources directory relative to python file
|
||||
project_root = pathlib.Path(__file__).parent.parent
|
||||
project_root = Path(__file__).parent.parent
|
||||
prefix = project_root / "share"
|
||||
else:
|
||||
if platform.system() == "Darwin":
|
||||
bin_path = pathlib.Path(sys.executable)
|
||||
bin_path = Path(sys.executable)
|
||||
app_path = bin_path.parent.parent
|
||||
prefix = app_path / "Resources" / "share"
|
||||
elif platform.system() == "Linux":
|
||||
prefix = pathlib.Path(sys.prefix) / "share" / "dangerzone"
|
||||
prefix = Path(sys.prefix) / "share" / "dangerzone"
|
||||
elif platform.system() == "Windows":
|
||||
exe_path = pathlib.Path(sys.executable)
|
||||
exe_path = Path(sys.executable)
|
||||
dz_install_path = exe_path.parent
|
||||
prefix = dz_install_path / "share"
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported system {platform.system()}")
|
||||
resource_path = prefix / filename
|
||||
return str(resource_path)
|
||||
return prefix / filename
|
||||
|
||||
|
||||
def get_tessdata_dir() -> pathlib.Path:
|
||||
def get_tessdata_dir() -> Path:
|
||||
if getattr(sys, "dangerzone_dev", False) or platform.system() in (
|
||||
"Windows",
|
||||
"Darwin",
|
||||
):
|
||||
# Always use the tessdata path from the Dangerzone ./share directory, for
|
||||
# development builds, or in Windows/macOS platforms.
|
||||
return pathlib.Path(get_resource_path("tessdata"))
|
||||
return get_resource_path("tessdata")
|
||||
|
||||
# In case of Linux systems, grab the Tesseract data from any of the following
|
||||
# locations. We have found some of the locations through trial and error, whereas
|
||||
|
@ -55,11 +54,11 @@ def get_tessdata_dir() -> pathlib.Path:
|
|||
#
|
||||
# [1] https://tesseract-ocr.github.io/tessdoc/Installation.html
|
||||
tessdata_dirs = [
|
||||
pathlib.Path("/usr/share/tessdata/"), # on some Debian
|
||||
pathlib.Path("/usr/share/tesseract/tessdata/"), # on Fedora
|
||||
pathlib.Path("/usr/share/tesseract-ocr/tessdata/"), # ? (documented)
|
||||
pathlib.Path("/usr/share/tesseract-ocr/4.00/tessdata/"), # on Debian Bullseye
|
||||
pathlib.Path("/usr/share/tesseract-ocr/5/tessdata/"), # on Debian Trixie
|
||||
Path("/usr/share/tessdata/"), # on some Debian
|
||||
Path("/usr/share/tesseract/tessdata/"), # on Fedora
|
||||
Path("/usr/share/tesseract-ocr/tessdata/"), # ? (documented)
|
||||
Path("/usr/share/tesseract-ocr/4.00/tessdata/"), # on Debian Bullseye
|
||||
Path("/usr/share/tesseract-ocr/5/tessdata/"), # on Debian Trixie
|
||||
]
|
||||
|
||||
for dir in tessdata_dirs:
|
||||
|
@ -71,7 +70,7 @@ def get_tessdata_dir() -> pathlib.Path:
|
|||
|
||||
def get_version() -> str:
|
||||
try:
|
||||
with open(get_resource_path("version.txt")) as f:
|
||||
with get_resource_path("version.txt").open() as f:
|
||||
version = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
# In dev mode, in Windows, get_resource_path doesn't work properly for the container, but luckily
|
||||
|
|
|
@ -27,7 +27,7 @@ This means that rebuilding the image without updating our Dockerfile will
|
|||
|
||||
Here are the necessary variables that make up our image in the `Dockerfile.env`
|
||||
file:
|
||||
* `DEBIAN_IMAGE_DATE`: The date that the Debian container image was released
|
||||
* `DEBIAN_IMAGE_DIGEST`: The index digest for the Debian container image
|
||||
* `DEBIAN_ARCHIVE_DATE`: The Debian snapshot repo that we want to use
|
||||
* `GVISOR_ARCHIVE_DATE`: The gVisor APT repo that we want to use
|
||||
* `H2ORESTART_CHECKSUM`: The SHA-256 checksum of the H2ORestart plugin
|
||||
|
|
|
@ -21,34 +21,25 @@ def get_qt_app() -> Application:
|
|||
|
||||
def generate_isolated_updater(
|
||||
tmp_path: Path,
|
||||
monkeypatch: MonkeyPatch,
|
||||
app_mocker: Optional[MockerFixture] = None,
|
||||
mocker: MockerFixture,
|
||||
mock_app: bool = False,
|
||||
) -> UpdaterThread:
|
||||
"""Generate an Updater class with its own settings."""
|
||||
if app_mocker:
|
||||
app = app_mocker.MagicMock()
|
||||
else:
|
||||
app = get_qt_app()
|
||||
app = mocker.MagicMock() if mock_app else get_qt_app()
|
||||
|
||||
dummy = Dummy()
|
||||
# XXX: We can monkey-patch global state without wrapping it in a context manager, or
|
||||
# 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)
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
|
||||
dangerzone = DangerzoneGui(app, isolation_provider=dummy)
|
||||
updater = UpdaterThread(dangerzone)
|
||||
return updater
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def updater(
|
||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
||||
) -> UpdaterThread:
|
||||
return generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
||||
def updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||
return generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def qt_updater(tmp_path: Path, monkeypatch: MonkeyPatch) -> UpdaterThread:
|
||||
return generate_isolated_updater(tmp_path, monkeypatch)
|
||||
def qt_updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||
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(
|
||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
||||
) -> None:
|
||||
def test_pre_0_4_2_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||
"""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
|
||||
|
@ -58,7 +56,7 @@ def test_pre_0_4_2_settings(
|
|||
in their settings.json file.
|
||||
"""
|
||||
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 (
|
||||
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.
|
||||
expected_settings = default_updater_settings()
|
||||
expected_settings["updater_latest_version"] = "0.4.3"
|
||||
monkeypatch.setattr(
|
||||
settings, "get_version", lambda: expected_settings["updater_latest_version"]
|
||||
)
|
||||
monkeypatch.setattr(settings, "get_version", lambda: "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
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
def test_user_prompts(
|
||||
updater: UpdaterThread, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
||||
) -> None:
|
||||
def test_user_prompts(updater: UpdaterThread, mocker: MockerFixture) -> None:
|
||||
"""Test prompting users to ask them if they want to enable update checks."""
|
||||
# First run
|
||||
#
|
||||
|
@ -370,8 +364,6 @@ def test_update_errors(
|
|||
def test_update_check_prompt(
|
||||
qtbot: QtBot,
|
||||
qt_updater: UpdaterThread,
|
||||
monkeypatch: MonkeyPatch,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Test that the prompt to enable update checks works properly."""
|
||||
# Force Dangerzone to check immediately for updates
|
||||
|
|
|
@ -5,7 +5,8 @@ import pytest
|
|||
from pytest_mock import MockerFixture
|
||||
from pytest_subprocess import FakeProcess
|
||||
|
||||
from dangerzone import container_utils, errors
|
||||
from dangerzone import errors
|
||||
from dangerzone.container_utils import Runtime
|
||||
from dangerzone.isolation_provider.container import Container
|
||||
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
|
||||
from dangerzone.util import get_resource_path
|
||||
|
@ -24,42 +25,51 @@ def provider() -> Container:
|
|||
return Container()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runtime_path() -> str:
|
||||
return str(Runtime().path)
|
||||
|
||||
|
||||
class TestContainer(IsolationProviderTest):
|
||||
def test_is_available_raises(self, provider: Container, fp: FakeProcess) -> None:
|
||||
def test_is_available_raises(
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
) -> None:
|
||||
"""
|
||||
NotAvailableContainerTechException should be raised when
|
||||
the "podman image ls" command fails.
|
||||
"""
|
||||
fp.register_subprocess(
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
[runtime_path, "image", "ls"],
|
||||
returncode=-1,
|
||||
stderr="podman image ls logs",
|
||||
)
|
||||
with pytest.raises(errors.NotAvailableContainerTechException):
|
||||
provider.is_available()
|
||||
|
||||
def test_is_available_works(self, provider: Container, fp: FakeProcess) -> None:
|
||||
def test_is_available_works(
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
) -> None:
|
||||
"""
|
||||
No exception should be raised when the "podman image ls" can return properly.
|
||||
"""
|
||||
fp.register_subprocess(
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
[runtime_path, "image", "ls"],
|
||||
)
|
||||
provider.is_available()
|
||||
|
||||
def test_install_raise_if_image_cant_be_installed(
|
||||
self, provider: Container, fp: FakeProcess
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
) -> None:
|
||||
"""When an image installation fails, an exception should be raised"""
|
||||
|
||||
fp.register_subprocess(
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
[runtime_path, "image", "ls"],
|
||||
)
|
||||
|
||||
# First check should return nothing.
|
||||
fp.register_subprocess(
|
||||
[
|
||||
container_utils.get_runtime(),
|
||||
runtime_path,
|
||||
"image",
|
||||
"list",
|
||||
"--format",
|
||||
|
@ -71,10 +81,10 @@ class TestContainer(IsolationProviderTest):
|
|||
|
||||
fp.register_subprocess(
|
||||
[
|
||||
container_utils.get_runtime(),
|
||||
runtime_path,
|
||||
"load",
|
||||
"-i",
|
||||
get_resource_path("container.tar"),
|
||||
get_resource_path("container.tar").absolute(),
|
||||
],
|
||||
returncode=-1,
|
||||
)
|
||||
|
@ -83,22 +93,22 @@ class TestContainer(IsolationProviderTest):
|
|||
provider.install()
|
||||
|
||||
def test_install_raises_if_still_not_installed(
|
||||
self, provider: Container, fp: FakeProcess
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
) -> None:
|
||||
"""When an image keep being not installed, it should return False"""
|
||||
fp.register_subprocess(
|
||||
["podman", "version", "-f", "{{.Client.Version}}"],
|
||||
[runtime_path, "version", "-f", "{{.Client.Version}}"],
|
||||
stdout="4.0.0",
|
||||
)
|
||||
|
||||
fp.register_subprocess(
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
[runtime_path, "image", "ls"],
|
||||
)
|
||||
|
||||
# First check should return nothing.
|
||||
fp.register_subprocess(
|
||||
[
|
||||
container_utils.get_runtime(),
|
||||
runtime_path,
|
||||
"image",
|
||||
"list",
|
||||
"--format",
|
||||
|
@ -110,10 +120,10 @@ class TestContainer(IsolationProviderTest):
|
|||
|
||||
fp.register_subprocess(
|
||||
[
|
||||
container_utils.get_runtime(),
|
||||
runtime_path,
|
||||
"load",
|
||||
"-i",
|
||||
get_resource_path("container.tar"),
|
||||
get_resource_path("container.tar").absolute(),
|
||||
],
|
||||
)
|
||||
with pytest.raises(errors.ImageNotPresentException):
|
||||
|
|
60
tests/test_container_utils.py
Normal file
60
tests/test_container_utils.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from dangerzone import errors
|
||||
from dangerzone.container_utils import Runtime
|
||||
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)
|
||||
mocker.patch("dangerzone.container_utils.Path.exists", return_value=True)
|
||||
|
||||
settings = Settings()
|
||||
settings.set("container_runtime", "/opt/somewhere/docker", autosave=True)
|
||||
|
||||
assert Runtime().name == "docker"
|
||||
|
||||
|
||||
def test_get_runtime_name_linux(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
mocker.patch("platform.system", return_value="Linux")
|
||||
mocker.patch(
|
||||
"dangerzone.container_utils.shutil.which", return_value="/usr/bin/podman"
|
||||
)
|
||||
mocker.patch("dangerzone.container_utils.os.path.exists", return_value=True)
|
||||
runtime = Runtime()
|
||||
assert runtime.name == "podman"
|
||||
assert runtime.path == Path("/usr/bin/podman")
|
||||
|
||||
|
||||
def test_get_runtime_name_non_linux(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||
mocker.patch("platform.system", return_value="Windows")
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
mocker.patch(
|
||||
"dangerzone.container_utils.shutil.which", return_value="/usr/bin/docker"
|
||||
)
|
||||
mocker.patch("dangerzone.container_utils.os.path.exists", return_value=True)
|
||||
runtime = Runtime()
|
||||
assert runtime.name == "docker"
|
||||
assert runtime.path == Path("/usr/bin/docker")
|
||||
|
||||
mocker.patch("platform.system", return_value="Something else")
|
||||
|
||||
runtime = Runtime()
|
||||
assert runtime.name == "docker"
|
||||
assert runtime.path == Path("/usr/bin/docker")
|
||||
assert Runtime().name == "docker"
|
||||
|
||||
|
||||
def test_get_unsupported_runtime_name(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
settings = Settings()
|
||||
settings.set(
|
||||
"container_runtime", "/opt/somewhere/new-kid-on-the-block", autosave=True
|
||||
)
|
||||
|
||||
with pytest.raises(errors.UnsupportedContainerRuntime):
|
||||
assert Runtime().name == "new-kid-on-the-block"
|
|
@ -1,5 +1,4 @@
|
|||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
|
@ -22,13 +21,6 @@ def default_settings_0_4_1() -> dict:
|
|||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def settings(tmp_path: Path, mocker: MockerFixture) -> Settings:
|
||||
dz_core = mocker.MagicMock()
|
||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
||||
return Settings(dz_core)
|
||||
|
||||
|
||||
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"
|
||||
|
@ -36,26 +28,31 @@ def save_settings(tmp_path: Path, settings: dict) -> None:
|
|||
json.dump(settings, settings_file, indent=4)
|
||||
|
||||
|
||||
def test_no_settings_file_creates_new_one(settings: Settings) -> None:
|
||||
def test_no_settings_file_creates_new_one(
|
||||
tmp_path: Path,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Default settings file is created on first run"""
|
||||
assert os.path.isfile(settings.settings_filename)
|
||||
new_settings_dict = json.load(open(settings.settings_filename))
|
||||
assert sorted(new_settings_dict.items()) == sorted(
|
||||
settings.generate_default_settings().items()
|
||||
)
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
settings = Settings()
|
||||
|
||||
assert settings.settings_filename.is_file()
|
||||
with settings.settings_filename.open() as settings_file:
|
||||
new_settings_dict = json.load(settings_file)
|
||||
assert sorted(new_settings_dict.items()) == sorted(
|
||||
settings.generate_default_settings().items()
|
||||
)
|
||||
|
||||
|
||||
def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||
# Set some broken settings file
|
||||
corrupt_settings_dict = "{:}"
|
||||
with open(tmp_path / SETTINGS_FILENAME, "w") as settings_file:
|
||||
with (tmp_path / SETTINGS_FILENAME).open("w") as settings_file:
|
||||
settings_file.write(corrupt_settings_dict)
|
||||
|
||||
# Initialize settings
|
||||
dz_core = mocker.MagicMock()
|
||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
||||
settings = Settings(dz_core)
|
||||
assert os.path.isfile(settings.settings_filename)
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
settings = Settings()
|
||||
assert settings.settings_filename.is_file()
|
||||
|
||||
# Check if settings file was reset to the default
|
||||
new_settings_dict = json.load(open(settings.settings_filename))
|
||||
|
@ -66,10 +63,7 @@ def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
|||
|
||||
|
||||
def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||
# Initialize settings
|
||||
dz_core = mocker.MagicMock()
|
||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
||||
settings = Settings(dz_core)
|
||||
settings = Settings()
|
||||
settings.save()
|
||||
|
||||
# Ensure new default setting is imported into settings
|
||||
|
@ -78,15 +72,12 @@ def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
|
|||
return_value={"mock_setting": 1},
|
||||
)
|
||||
|
||||
settings2 = Settings(dz_core)
|
||||
settings2 = Settings()
|
||||
assert settings2.get("mock_setting") == 1
|
||||
|
||||
|
||||
def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||
# Initialize settings
|
||||
dz_core = mocker.MagicMock()
|
||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
||||
settings = Settings(dz_core)
|
||||
settings = Settings()
|
||||
|
||||
# Add new setting
|
||||
settings.set("new_setting_autosaved", 20, autosave=True)
|
||||
|
@ -95,7 +86,7 @@ def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
|
|||
) # XXX has to be afterwards; otherwise this will be saved
|
||||
|
||||
# Simulate new app startup (settings recreation)
|
||||
settings2 = Settings(dz_core)
|
||||
settings2 = Settings()
|
||||
|
||||
# Check if new setting persisted
|
||||
assert 20 == settings2.get("new_setting_autosaved")
|
||||
|
|
|
@ -11,7 +11,7 @@ VERSION_FILE_NAME = "version.txt"
|
|||
|
||||
def test_get_resource_path() -> None:
|
||||
share_dir = Path("share").resolve()
|
||||
resource_path = Path(util.get_resource_path(VERSION_FILE_NAME)).parent
|
||||
resource_path = util.get_resource_path(VERSION_FILE_NAME).parent
|
||||
assert share_dir.samefile(resource_path), (
|
||||
f"{share_dir} is not the same file as {resource_path}"
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue