From 96e64deae750d25769af7ff42e72854c9962151c Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Mon, 2 Dec 2024 18:12:16 +0200 Subject: [PATCH 01/15] Move container-specific method from base class Move the `is_runtime_available()` method from the base `IsolationProvider` class, and into the `Dummy` provider class. This method was originally defined in the base class, in order to be mocked in our tests for the `Dummy` provider. There's no reason for the `Qubes` class to have it though, so we can just move it to the `Dummy` provider. --- dangerzone/gui/main_window.py | 1 + dangerzone/isolation_provider/base.py | 4 ---- dangerzone/isolation_provider/dummy.py | 4 ++++ tests/gui/test_main_window.py | 7 ++++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/dangerzone/gui/main_window.py b/dangerzone/gui/main_window.py index e292ff0..788ad6a 100644 --- a/dangerzone/gui/main_window.py +++ b/dangerzone/gui/main_window.py @@ -500,6 +500,7 @@ class WaitingWidgetContainer(WaitingWidget): error: Optional[str] = None try: + assert isinstance(self.dangerzone.isolation_provider, (Dummy, Container)) self.dangerzone.isolation_provider.is_runtime_available() except NoContainerTechException as e: log.error(str(e)) diff --git a/dangerzone/isolation_provider/base.py b/dangerzone/isolation_provider/base.py index 6a55a20..9404cee 100644 --- a/dangerzone/isolation_provider/base.py +++ b/dangerzone/isolation_provider/base.py @@ -93,10 +93,6 @@ class IsolationProvider(ABC): else: self.proc_stderr = subprocess.DEVNULL - @staticmethod - def is_runtime_available() -> bool: - return True - @abstractmethod def install(self) -> bool: pass diff --git a/dangerzone/isolation_provider/dummy.py b/dangerzone/isolation_provider/dummy.py index 9ebc345..b8e3b87 100644 --- a/dangerzone/isolation_provider/dummy.py +++ b/dangerzone/isolation_provider/dummy.py @@ -39,6 +39,10 @@ class Dummy(IsolationProvider): def install(self) -> bool: return True + @staticmethod + def is_runtime_available() -> bool: + return True + def start_doc_to_pixels_proc(self, document: Document) -> subprocess.Popen: cmd = [ sys.executable, diff --git a/tests/gui/test_main_window.py b/tests/gui/test_main_window.py index ff45075..7e96d22 100644 --- a/tests/gui/test_main_window.py +++ b/tests/gui/test_main_window.py @@ -30,6 +30,7 @@ from dangerzone.isolation_provider.container import ( NoContainerTechException, NotAvailableContainerTechException, ) +from dangerzone.isolation_provider.dummy import Dummy from .test_updater import assert_report_equal, default_updater_settings @@ -510,9 +511,9 @@ def test_not_available_container_tech_exception( ) -> None: # Setup mock_app = mocker.MagicMock() - dummy = mocker.MagicMock() - - dummy.is_runtime_available.side_effect = NotAvailableContainerTechException( + dummy = Dummy() + fn = mocker.patch.object(dummy, "is_runtime_available") + fn.side_effect = NotAvailableContainerTechException( "podman", "podman image ls logs" ) From f31fbfefc66375becbcb1d4766e461c482484b3f Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Mon, 2 Dec 2024 16:12:56 +0200 Subject: [PATCH 02/15] container: Manipulate Dangerzone image tags Add the following methods that allow the `Container` isolation provider to work with tags for the Dangerzone image: * `list_image_tag()` * `delete_image_tag()` * `add_image_tag()` --- dangerzone/isolation_provider/container.py | 70 +++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index 94f894d..3f855e0 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -1,11 +1,12 @@ import gzip +import json import logging import os import platform import shlex import shutil import subprocess -from typing import List, Tuple +from typing import Dict, List, Tuple from ..document import Document from ..util import get_resource_path, get_subprocess_startupinfo @@ -154,6 +155,73 @@ class Container(IsolationProvider): return security_args + @staticmethod + def list_image_tags() -> Dict[str, str]: + """Get the tags of all loaded Dangerzone images. + + This method returns a mapping of image tags to image IDs, for all Dangerzone + 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. + """ + images = json.loads( + subprocess.check_output( + [ + Container.get_runtime(), + "image", + "list", + "--format", + "json", + Container.CONTAINER_NAME, + ], + text=True, + startupinfo=get_subprocess_startupinfo(), + ) + ) + + # Grab every image name and associate it with an image ID. + tags = {} + for image in images: + for name in image["Names"]: + tag = name.split(":")[1] + tags[tag] = image["Id"] + + return tags + + @staticmethod + def delete_image_tag(tag: str) -> None: + """Delete a Dangerzone image tag.""" + name = Container.CONTAINER_NAME + ":" + tag + log.warning(f"Deleting old container image: {name}") + try: + subprocess.check_output( + [Container.get_runtime(), "rmi", "--force", name], + startupinfo=get_subprocess_startupinfo(), + ) + except Exception as e: + log.warning( + f"Couldn't delete old container image '{name}', so leaving it there." + f" Original error: {e}" + ) + + @staticmethod + def add_image_tag(cur_tag: str, new_tag: str) -> None: + """Add a tag to an existing Dangerzone image.""" + cur_image_name = Container.CONTAINER_NAME + ":" + cur_tag + new_image_name = Container.CONTAINER_NAME + ":" + new_tag + subprocess.check_output( + [ + Container.get_runtime(), + "tag", + cur_image_name, + new_image_name, + ], + startupinfo=get_subprocess_startupinfo(), + ) + + log.info( + f"Successfully tagged container image '{cur_image_name}' as '{new_image_name}'" + ) + @staticmethod def install() -> bool: """ From 7f7fe43711d3bed3e640d58d125279d944e80585 Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Mon, 2 Dec 2024 16:30:43 +0200 Subject: [PATCH 03/15] container: Factor out loading an image tarball --- dangerzone/isolation_provider/container.py | 26 +++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index 3f855e0..0954a53 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -223,16 +223,14 @@ class Container(IsolationProvider): ) @staticmethod - def install() -> bool: - """ - Make sure the podman container is installed. Linux only. - """ - if Container.is_container_installed(): - return True + 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: + return f.read.strip() - # Load the container into podman + @staticmethod + def load_image_tarball() -> None: log.info("Installing Dangerzone container image...") - p = subprocess.Popen( [Container.get_runtime(), "load"], stdin=subprocess.PIPE, @@ -259,6 +257,18 @@ class Container(IsolationProvider): f"Could not install container image: {error}" ) + log.info("Successfully installed container image from") + + @staticmethod + def install() -> bool: + """ + Make sure the podman container is installed. Linux only. + """ + if Container.is_container_installed(): + return True + + Container.load_image_tarball() + if not Container.is_container_installed(raise_on_error=True): return False From 53214d33d8fc9b59f61d07c971ae8aef50454018 Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Mon, 2 Dec 2024 16:51:26 +0200 Subject: [PATCH 04/15] Build and tag Dangerzone images Build Dangerzone images and tag them with a unique ID that stems from the Git reop. Note that using tags as image IDs instead of regular image IDs breaks the current Dangerzone expectations, but this will be addressed in subsequent commits. --- .github/workflows/build.yml | 2 ++ .github/workflows/ci.yml | 4 ++- .github/workflows/scan.yml | 2 ++ install/common/build-image.py | 52 ++++++++++++++++++++++------------- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 92440e7..1a270a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,6 +74,8 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Get current date id: date diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c72dd1..e694f31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,8 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Get current date id: date @@ -245,7 +247,7 @@ jobs: install-deb: name: "install-deb (${{ matrix.distro }} ${{ matrix.version }})" runs-on: ubuntu-latest - needs: + needs: - build-deb strategy: matrix: diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index d9f397b..3080476 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install container build dependencies run: sudo apt install pipx && pipx install poetry - name: Build container image diff --git a/install/common/build-image.py b/install/common/build-image.py index 9f2dcc8..921a520 100644 --- a/install/common/build-image.py +++ b/install/common/build-image.py @@ -2,12 +2,13 @@ import argparse import gzip import os import platform +import secrets import subprocess import sys from pathlib import Path BUILD_CONTEXT = "dangerzone/" -TAG = "dangerzone.rocks/dangerzone:latest" +IMAGE_NAME = "dangerzone.rocks/dangerzone" REQUIREMENTS_TXT = "container-pip-requirements.txt" if platform.system() in ["Darwin", "Windows"]: CONTAINER_RUNTIME = "docker" @@ -44,8 +45,31 @@ def main(): ) args = parser.parse_args() + tarball_path = Path("share") / "container.tar.gz" + image_id_path = Path("share") / "image-id.txt" + print(f"Building for architecture '{ARCH}'") + # Designate a unique tag for this image, depending on the Git commit it was created + # from: + # 1. If created from a Git tag (e.g., 0.8.0), the image tag will be `0.8.0`. + # 2. If created from a commit, it will be something like `0.8.0-31-g6bdaa7a`. + # 3. If the contents of the Git repo are dirty, we will append a unique identifier + # for this run, something like `0.8.0-31-g6bdaa7a-fdcb` or `0.8.0-fdcb`. + dirty_ident = secrets.token_hex(2) + tag = ( + subprocess.check_output( + ["git", "describe", "--first-parent", f"--dirty=-{dirty_ident}"], + ) + .decode() + .strip()[1:] # remove the "v" prefix of the tag. + ) + image_name_tagged = IMAGE_NAME + ":" + tag + + print(f"Will tag the container image as '{image_name_tagged}'") + with open(image_id_path, "w") as f: + f.write(tag) + print("Exporting container pip dependencies") with ContainerPipDependencies(): if not args.use_cache: @@ -59,8 +83,11 @@ def main(): check=True, ) + # Build the container image, and tag it with two tags; the one we calculated + # above, and the "latest" tag. print("Building container image") cache_args = [] if args.use_cache else ["--no-cache"] + image_name_latest = IMAGE_NAME + ":latest" subprocess.run( [ args.runtime, @@ -74,7 +101,9 @@ def main(): "-f", "Dockerfile", "--tag", - TAG, + image_name_latest, + "--tag", + image_name_tagged, ], check=True, ) @@ -85,7 +114,7 @@ def main(): [ CONTAINER_RUNTIME, "save", - TAG, + image_name_tagged, ], stdout=subprocess.PIPE, ) @@ -93,7 +122,7 @@ def main(): print("Compressing container image") chunk_size = 4 << 20 with gzip.open( - "share/container.tar.gz", + tarball_path, "wb", compresslevel=args.compress_level, ) as gzip_f: @@ -105,21 +134,6 @@ def main(): break cmd.wait(5) - print("Looking up the image id") - image_id = subprocess.check_output( - [ - args.runtime, - "image", - "list", - "--format", - "{{.ID}}", - TAG, - ], - text=True, - ) - with open("share/image-id.txt", "w") as f: - f.write(image_id) - class ContainerPipDependencies: """Generates PIP dependencies within container""" From 5b1fe4d7ad12876ddf187efdaf04e24f05bdb844 Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Mon, 2 Dec 2024 17:05:38 +0200 Subject: [PATCH 05/15] container: Revamp container image installation Revamp the container image installation process in a way that does not involve using image IDs. We don't want to rely on image IDs anymore, since they are brittle (see https://github.com/freedomofpress/dangerzone/issues/933). Instead, we use image tags, as provided in the `image-id.txt` file. This allows us to check fast if an image is up to date, and we no longer need to maintain multiple image IDs from various container runtimes. Refs #933 Refs #988 Fixes #1020 --- dangerzone/isolation_provider/container.py | 98 +++++++++------------- tests/isolation_provider/test_container.py | 6 +- 2 files changed, 43 insertions(+), 61 deletions(-) diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index 0954a53..36376de 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -226,7 +226,7 @@ class Container(IsolationProvider): 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: - return f.read.strip() + return f.read().strip() @staticmethod def load_image_tarball() -> None: @@ -261,18 +261,50 @@ class Container(IsolationProvider): @staticmethod def install() -> bool: + """Install the container image tarball, or verify that it's already installed. + + Perform the following actions: + 1. Get the tags of any locally available images that match Dangerzone's image + name. + 2. Get the expected image tag from the image-id.txt file. + - If this tag is present in the local images, and that image is also tagged + as "latest", then we can return. + - Else, prune the older container images and continue. + 3. Load the image tarball and make sure it matches the expected tag. + 4. Tag that image as "latest", and mark the installation as finished. """ - Make sure the podman container is installed. Linux only. - """ - if Container.is_container_installed(): + old_tags = Container.list_image_tags() + expected_tag = Container.get_expected_tag() + + if expected_tag not in old_tags: + # Prune older container images. + log.info( + f"Could not find a Dangerzone container image with tag '{expected_tag}'" + ) + for tag in old_tags.keys(): + Container.delete_image_tag(tag) + elif old_tags[expected_tag] != old_tags.get("latest"): + log.info(f"The expected tag '{expected_tag}' is not the latest one") + Container.add_image_tag(expected_tag, "latest") + return True + else: return True + # Load the image tarball into the container runtime. Container.load_image_tarball() - if not Container.is_container_installed(raise_on_error=True): - return False + # Check that the container image has the expected image tag. + # See https://github.com/freedomofpress/dangerzone/issues/988 for an example + # where this was not the case. + new_tags = Container.list_image_tags() + if expected_tag not in new_tags: + raise ImageNotPresentException( + f"Could not find expected tag '{expected_tag}' after loading the" + " container image tarball" + ) - log.info("Container image installed") + # Mark the expected tag as "latest". + Container.add_image_tag(expected_tag, "latest") return True @staticmethod @@ -291,58 +323,6 @@ class Container(IsolationProvider): raise NotAvailableContainerTechException(runtime_name, stderr.decode()) return True - @staticmethod - def is_container_installed(raise_on_error: bool = False) -> bool: - """ - See if the container is installed. - """ - # Get the image id - with open(get_resource_path("image-id.txt")) as f: - expected_image_ids = f.read().strip().split() - - # See if this image is already installed - installed = False - found_image_id = subprocess.check_output( - [ - Container.get_runtime(), - "image", - "list", - "--format", - "{{.ID}}", - Container.CONTAINER_NAME, - ], - text=True, - startupinfo=get_subprocess_startupinfo(), - ) - found_image_id = found_image_id.strip() - - if found_image_id in expected_image_ids: - installed = True - elif found_image_id == "": - if raise_on_error: - raise ImageNotPresentException( - "Image is not listed after installation. Bailing out." - ) - else: - msg = ( - f"{Container.CONTAINER_NAME} images found, but IDs do not match." - f" Found: {found_image_id}, Expected: {','.join(expected_image_ids)}" - ) - if raise_on_error: - raise ImageNotPresentException(msg) - log.info(msg) - log.info("Deleting old dangerzone container image") - - try: - subprocess.check_output( - [Container.get_runtime(), "rmi", "--force", found_image_id], - startupinfo=get_subprocess_startupinfo(), - ) - except Exception: - log.warning("Couldn't delete old container image, so leaving it there") - - return installed - def doc_to_pixels_container_name(self, document: Document) -> str: """Unique container name for the doc-to-pixels phase.""" return f"dangerzone-doc-to-pixels-{document.id}" diff --git a/tests/isolation_provider/test_container.py b/tests/isolation_provider/test_container.py index 3fb3243..a1a844d 100644 --- a/tests/isolation_provider/test_container.py +++ b/tests/isolation_provider/test_container.py @@ -69,10 +69,11 @@ class TestContainer(IsolationProviderTest): "image", "list", "--format", - "{{.ID}}", + "json", "dangerzone.rocks/dangerzone", ], occurrences=2, + stdout="{}", ) # Make podman load fail @@ -102,10 +103,11 @@ class TestContainer(IsolationProviderTest): "image", "list", "--format", - "{{.ID}}", + "json", "dangerzone.rocks/dangerzone", ], occurrences=2, + stdout="{}", ) # Patch gzip.open and podman load so that it works From e51407ef502a87e10916ec7628b4338dd0a1f3c0 Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Mon, 2 Dec 2024 18:38:33 +0200 Subject: [PATCH 06/15] Update our release instructions --- QA.md | 6 ++++-- RELEASE.md | 5 ----- dev_scripts/qa.py | 6 ++++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/QA.md b/QA.md index dff7780..5302f92 100644 --- a/QA.md +++ b/QA.md @@ -107,9 +107,10 @@ Close the Dangerzone application and get the container image for that version. For example: ``` -$ docker images dangerzone.rocks/dangerzone:latest +$ docker images dangerzone.rocks/dangerzone REPOSITORY TAG IMAGE ID CREATED SIZE dangerzone.rocks/dangerzone latest +dangerzone.rocks/dangerzone ``` Then run the version under QA and ensure that the settings remain changed. @@ -118,9 +119,10 @@ Afterwards check that new docker image was installed by running the same command and seeing the following differences: ``` -$ docker images dangerzone.rocks/dangerzone:latest +$ docker images dangerzone.rocks/dangerzone REPOSITORY TAG IMAGE ID CREATED SIZE dangerzone.rocks/dangerzone latest +dangerzone.rocks/dangerzone ``` #### 4. Dangerzone successfully installs the container image diff --git a/RELEASE.md b/RELEASE.md index 8d4c0ba..c6f0c96 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -142,8 +142,6 @@ Here is what you need to do: poetry run ./install/macos/build-app.py ``` -- [ ] Make sure that the build application works with the containerd graph - driver (see [#933](https://github.com/freedomofpress/dangerzone/issues/933)) - [ ] Sign the application bundle, and notarize it You need to run this command as the account that has access to the code signing certificate @@ -212,9 +210,6 @@ The Windows release is performed in a Windows 11 virtual machine (as opposed to - [ ] Copy the container image into the VM > [!IMPORTANT] > Instead of running `python .\install\windows\build-image.py` in the VM, run the build image script on the host (making sure to build for `linux/amd64`). Copy `share/container.tar.gz` and `share/image-id.txt` from the host into the `share` folder in the VM. - > Also, don't forget to add the supplementary image ID (see - > [#933](https://github.com/freedomofpress/dangerzone/issues/933)) in - > `share/image-id.txt`) - [ ] Run `poetry run .\install\windows\build-app.bat` - [ ] When you're done you will have `dist\Dangerzone.msi` diff --git a/dev_scripts/qa.py b/dev_scripts/qa.py index 5039bbd..ebd6099 100755 --- a/dev_scripts/qa.py +++ b/dev_scripts/qa.py @@ -127,9 +127,10 @@ Close the Dangerzone application and get the container image for that version. For example: ``` -$ docker images dangerzone.rocks/dangerzone:latest +$ docker images dangerzone.rocks/dangerzone REPOSITORY TAG IMAGE ID CREATED SIZE dangerzone.rocks/dangerzone latest +dangerzone.rocks/dangerzone ``` Then run the version under QA and ensure that the settings remain changed. @@ -138,9 +139,10 @@ Afterwards check that new docker image was installed by running the same command and seeing the following differences: ``` -$ docker images dangerzone.rocks/dangerzone:latest +$ docker images dangerzone.rocks/dangerzone REPOSITORY TAG IMAGE ID CREATED SIZE dangerzone.rocks/dangerzone latest +dangerzone.rocks/dangerzone ``` #### 4. Dangerzone successfully installs the container image From ca63d571c76a649bcb571fb21b4811d55e39def9 Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Wed, 4 Dec 2024 16:37:35 +0200 Subject: [PATCH 07/15] Fix minor typos in our docs --- RELEASE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index c6f0c96..39f9564 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -126,7 +126,7 @@ Here is what you need to do: ``` - [ ] Build the container image and the OCR language data - + ```bash poetry run ./install/common/build-image.py poetry run ./install/common/download-tessdata.py @@ -143,9 +143,9 @@ Here is what you need to do: ``` - [ ] Sign the application bundle, and notarize it - + You need to run this command as the account that has access to the code signing certificate - + This command assumes that you have created, and stored in the Keychain, an application password associated with your Apple Developer ID, which will be used specifically for `notarytool`. @@ -264,7 +264,7 @@ or create your own locally with: ./dev_scripts/env.py --distro fedora --version 41 build-dev # Build the latest container (skip if already built): -./dev_scripts/env.py --distro fedora --version 41 run --dev bash -c "cd dangerzone && poetry run ./install/common/build-image.py" +./dev_scripts/env.py --distro fedora --version 41 run --dev bash -c "cd dangerzone && poetry run ./install/common/build-image.py" # Create a .rpm: ./dev_scripts/env.py --distro fedora --version 41 run --dev bash -c "cd dangerzone && ./install/linux/build-rpm.py" From 9b244b8d83634508bb63c16efb45113f26ce9628 Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Wed, 4 Dec 2024 16:50:27 +0200 Subject: [PATCH 08/15] Extend the interface of the isolation provider Add the following two methods in the isolation provider: 1. `.is_available()`: Mainly used for the Container isolation provider, it specifies whether the container runtime is up and running. May be used in the future by other similar providers. 2. `.should_wait_install()`: Whether the isolation provider takes a while to be installed. Should be `True` only for the Container isolation provider, for the time being. --- dangerzone/gui/main_window.py | 14 ++++---------- dangerzone/isolation_provider/base.py | 10 ++++++++++ dangerzone/isolation_provider/container.py | 6 +++++- dangerzone/isolation_provider/dummy.py | 6 +++++- dangerzone/isolation_provider/qubes.py | 8 ++++++++ tests/gui/test_main_window.py | 4 ++-- tests/isolation_provider/test_container.py | 12 ++++-------- 7 files changed, 38 insertions(+), 22 deletions(-) diff --git a/dangerzone/gui/main_window.py b/dangerzone/gui/main_window.py index 788ad6a..da8e11e 100644 --- a/dangerzone/gui/main_window.py +++ b/dangerzone/gui/main_window.py @@ -26,12 +26,10 @@ else: from .. import errors from ..document import SAFE_EXTENSION, Document from ..isolation_provider.container import ( - Container, NoContainerTechException, NotAvailableContainerTechException, ) -from ..isolation_provider.dummy import Dummy -from ..isolation_provider.qubes import Qubes, is_qubes_native_conversion +from ..isolation_provider.qubes import is_qubes_native_conversion from ..util import format_exception, get_resource_path, get_version from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog from .updater import UpdateReport @@ -197,14 +195,11 @@ class MainWindow(QtWidgets.QMainWindow): header_layout.addWidget(self.hamburger_button) header_layout.addSpacing(15) - if isinstance(self.dangerzone.isolation_provider, Container): + if self.dangerzone.isolation_provider.should_wait_install(): # Waiting widget replaces content widget while container runtime isn't available self.waiting_widget: WaitingWidget = WaitingWidgetContainer(self.dangerzone) self.waiting_widget.finished.connect(self.waiting_finished) - - elif isinstance(self.dangerzone.isolation_provider, Dummy) or isinstance( - self.dangerzone.isolation_provider, Qubes - ): + else: # Don't wait with dummy converter and on Qubes. self.waiting_widget = WaitingWidget() self.dangerzone.is_waiting_finished = True @@ -500,8 +495,7 @@ class WaitingWidgetContainer(WaitingWidget): error: Optional[str] = None try: - assert isinstance(self.dangerzone.isolation_provider, (Dummy, Container)) - self.dangerzone.isolation_provider.is_runtime_available() + self.dangerzone.isolation_provider.is_available() except NoContainerTechException as e: log.error(str(e)) state = "not_installed" diff --git a/dangerzone/isolation_provider/base.py b/dangerzone/isolation_provider/base.py index 9404cee..fd1bd6a 100644 --- a/dangerzone/isolation_provider/base.py +++ b/dangerzone/isolation_provider/base.py @@ -254,6 +254,16 @@ class IsolationProvider(ABC): ) return errors.exception_from_error_code(error_code) + @abstractmethod + def should_wait_install(self) -> bool: + """Whether this isolation provider takes a lot of time to install.""" + pass + + @abstractmethod + def is_available(self) -> bool: + """Whether the backing implementation of the isolation provider is available.""" + pass + @abstractmethod def get_max_parallel_conversions(self) -> int: pass diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index 36376de..6116dbf 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -308,7 +308,11 @@ class Container(IsolationProvider): return True @staticmethod - def is_runtime_available() -> bool: + def should_wait_install() -> bool: + return True + + @staticmethod + def is_available() -> bool: container_runtime = Container.get_runtime() runtime_name = Container.get_runtime_name() # Can we run `docker/podman image ls` without an error diff --git a/dangerzone/isolation_provider/dummy.py b/dangerzone/isolation_provider/dummy.py index b8e3b87..fac973f 100644 --- a/dangerzone/isolation_provider/dummy.py +++ b/dangerzone/isolation_provider/dummy.py @@ -40,9 +40,13 @@ class Dummy(IsolationProvider): return True @staticmethod - def is_runtime_available() -> bool: + def is_available() -> bool: return True + @staticmethod + def should_wait_install() -> bool: + return False + def start_doc_to_pixels_proc(self, document: Document) -> subprocess.Popen: cmd = [ sys.executable, diff --git a/dangerzone/isolation_provider/qubes.py b/dangerzone/isolation_provider/qubes.py index 61a7c8d..02f8002 100644 --- a/dangerzone/isolation_provider/qubes.py +++ b/dangerzone/isolation_provider/qubes.py @@ -21,6 +21,14 @@ class Qubes(IsolationProvider): def install(self) -> bool: return True + @staticmethod + def is_available() -> bool: + return True + + @staticmethod + def should_wait_install() -> bool: + return False + def get_max_parallel_conversions(self) -> int: return 1 diff --git a/tests/gui/test_main_window.py b/tests/gui/test_main_window.py index 7e96d22..f8060b0 100644 --- a/tests/gui/test_main_window.py +++ b/tests/gui/test_main_window.py @@ -512,7 +512,7 @@ def test_not_available_container_tech_exception( # Setup mock_app = mocker.MagicMock() dummy = Dummy() - fn = mocker.patch.object(dummy, "is_runtime_available") + fn = mocker.patch.object(dummy, "is_available") fn.side_effect = NotAvailableContainerTechException( "podman", "podman image ls logs" ) @@ -536,7 +536,7 @@ def test_no_container_tech_exception(qtbot: QtBot, mocker: MockerFixture) -> Non dummy = mocker.MagicMock() # Raise - dummy.is_runtime_available.side_effect = NoContainerTechException("podman") + dummy.is_available.side_effect = NoContainerTechException("podman") dz = DangerzoneGui(mock_app, dummy) widget = WaitingWidgetContainer(dz) diff --git a/tests/isolation_provider/test_container.py b/tests/isolation_provider/test_container.py index a1a844d..ad5dc6b 100644 --- a/tests/isolation_provider/test_container.py +++ b/tests/isolation_provider/test_container.py @@ -27,9 +27,7 @@ def provider() -> Container: class TestContainer(IsolationProviderTest): - def test_is_runtime_available_raises( - self, provider: Container, fp: FakeProcess - ) -> None: + def test_is_available_raises(self, provider: Container, fp: FakeProcess) -> None: """ NotAvailableContainerTechException should be raised when the "podman image ls" command fails. @@ -40,18 +38,16 @@ class TestContainer(IsolationProviderTest): stderr="podman image ls logs", ) with pytest.raises(NotAvailableContainerTechException): - provider.is_runtime_available() + provider.is_available() - def test_is_runtime_available_works( - self, provider: Container, fp: FakeProcess - ) -> None: + def test_is_available_works(self, provider: Container, fp: FakeProcess) -> None: """ No exception should be raised when the "podman image ls" can return properly. """ fp.register_subprocess( [provider.get_runtime(), "image", "ls"], ) - provider.is_runtime_available() + provider.is_available() def test_install_raise_if_image_cant_be_installed( self, mocker: MockerFixture, provider: Container, fp: FakeProcess From e7cd6e3138b2a94da5742e452d82d0fa0a45aff1 Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Wed, 4 Dec 2024 17:30:34 +0200 Subject: [PATCH 09/15] Factor out container utilities to separate module --- dangerzone/container_utils.py | 176 ++++++++++++++++ dangerzone/errors.py | 23 ++ dangerzone/gui/main_window.py | 8 +- dangerzone/isolation_provider/container.py | 232 +++------------------ tests/gui/test_main_window.py | 11 +- tests/isolation_provider/test_container.py | 30 ++- 6 files changed, 242 insertions(+), 238 deletions(-) create mode 100644 dangerzone/container_utils.py diff --git a/dangerzone/container_utils.py b/dangerzone/container_utils.py new file mode 100644 index 0000000..6d1e48f --- /dev/null +++ b/dangerzone/container_utils.py @@ -0,0 +1,176 @@ +import gzip +import json +import logging +import platform +import shutil +import subprocess +from typing import Dict, Tuple + +from .util import get_resource_path, get_subprocess_startupinfo +from . import errors + +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 + + +def get_runtime_version() -> 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 + that are not available across all of our platforms. In order to have a proper + fallback, we need to know the Podman version. More specifically, we're fine with + just knowing the major and minor version, since writing/installing a full-blown + semver parser is an overkill. + """ + # Get the Docker/Podman version, using a Go template. + runtime = get_runtime_name() + if runtime == "podman": + query = "{{.Client.Version}}" + else: + query = "{{.Server.Version}}" + + cmd = [runtime, "version", "-f", query] + try: + version = subprocess.run( + cmd, + startupinfo=get_subprocess_startupinfo(), + capture_output=True, + check=True, + ).stdout.decode() + except Exception as e: + msg = f"Could not get the version of the {runtime.capitalize()} tool: {e}" + raise RuntimeError(msg) from e + + # Parse this version and return the major/minor parts, since we don't need the + # rest. + try: + major, minor, _ = version.split(".", 3) + return (int(major), int(minor)) + except Exception as e: + msg = ( + f"Could not parse the version of the {runtime.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() -> Dict[str, str]: + """Get the tags of all loaded Dangerzone images. + + This method returns a mapping of image tags to image IDs, for all Dangerzone + 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. + """ + images = json.loads( + subprocess.check_output( + [ + get_runtime(), + "image", + "list", + "--format", + "json", + CONTAINER_NAME, + ], + text=True, + startupinfo=get_subprocess_startupinfo(), + ) + ) + + # Grab every image name and associate it with an image ID. + tags = {} + for image in images: + for name in image["Names"]: + tag = name.split(":")[1] + tags[tag] = image["Id"] + + return tags + + +def delete_image_tag(tag: str) -> None: + """Delete a Dangerzone image tag.""" + name = CONTAINER_NAME + ":" + tag + log.warning(f"Deleting old container image: {name}") + try: + subprocess.check_output( + [get_runtime(), "rmi", "--force", name], + startupinfo=get_subprocess_startupinfo(), + ) + except Exception as e: + log.warning( + f"Couldn't delete old container image '{name}', so leaving it there." + f" Original error: {e}" + ) + + +def add_image_tag(cur_tag: str, new_tag: str) -> None: + """Add a tag to an existing Dangerzone image.""" + cur_image_name = CONTAINER_NAME + ":" + cur_tag + new_image_name = CONTAINER_NAME + ":" + new_tag + subprocess.check_output( + [ + get_runtime(), + "tag", + cur_image_name, + new_image_name, + ], + startupinfo=get_subprocess_startupinfo(), + ) + + log.info( + f"Successfully tagged container image '{cur_image_name}' as '{new_image_name}'" + ) + + +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: + return f.read().strip() + + +def load_image_tarball() -> None: + log.info("Installing Dangerzone container image...") + p = subprocess.Popen( + [get_runtime(), "load"], + stdin=subprocess.PIPE, + startupinfo=get_subprocess_startupinfo(), + ) + + chunk_size = 4 << 20 + compressed_container_path = get_resource_path("container.tar.gz") + with gzip.open(compressed_container_path) as f: + while True: + chunk = f.read(chunk_size) + if len(chunk) > 0: + if p.stdin: + p.stdin.write(chunk) + else: + break + _, err = p.communicate() + if p.returncode < 0: + if err: + error = err.decode() + else: + error = "No output" + raise errors.ImageInstallationException( + f"Could not install container image: {error}" + ) + + log.info("Successfully installed container image from") diff --git a/dangerzone/errors.py b/dangerzone/errors.py index a55f508..d8e1759 100644 --- a/dangerzone/errors.py +++ b/dangerzone/errors.py @@ -117,3 +117,26 @@ def handle_document_errors(func: F) -> F: sys.exit(1) return cast(F, wrapper) + + +#### Container-related errors + + +class ImageNotPresentException(Exception): + pass + + +class ImageInstallationException(Exception): + pass + + +class NoContainerTechException(Exception): + def __init__(self, container_tech: str) -> None: + super().__init__(f"{container_tech} is not installed") + + +class NotAvailableContainerTechException(Exception): + def __init__(self, container_tech: str, error: str) -> None: + self.error = error + self.container_tech = container_tech + super().__init__(f"{container_tech} is not available") diff --git a/dangerzone/gui/main_window.py b/dangerzone/gui/main_window.py index da8e11e..d03300a 100644 --- a/dangerzone/gui/main_window.py +++ b/dangerzone/gui/main_window.py @@ -25,10 +25,6 @@ else: from .. import errors from ..document import SAFE_EXTENSION, Document -from ..isolation_provider.container import ( - NoContainerTechException, - NotAvailableContainerTechException, -) from ..isolation_provider.qubes import is_qubes_native_conversion from ..util import format_exception, get_resource_path, get_version from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog @@ -496,10 +492,10 @@ class WaitingWidgetContainer(WaitingWidget): try: self.dangerzone.isolation_provider.is_available() - except NoContainerTechException as e: + except errors.NoContainerTechException as e: log.error(str(e)) state = "not_installed" - except NotAvailableContainerTechException as e: + except errors.NotAvailableContainerTechException as e: log.error(str(e)) state = "not_running" error = e.error diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index 6116dbf..878c7d3 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -1,13 +1,11 @@ -import gzip -import json import logging import os import platform import shlex -import shutil import subprocess -from typing import Dict, List, Tuple +from typing import List +from .. import container_utils, errors from ..document import Document from ..util import get_resource_path, get_subprocess_startupinfo from .base import IsolationProvider, terminate_process_group @@ -26,88 +24,8 @@ else: log = logging.getLogger(__name__) -class NoContainerTechException(Exception): - def __init__(self, container_tech: str) -> None: - super().__init__(f"{container_tech} is not installed") - - -class NotAvailableContainerTechException(Exception): - def __init__(self, container_tech: str, error: str) -> None: - self.error = error - self.container_tech = container_tech - super().__init__(f"{container_tech} is not available") - - -class ImageNotPresentException(Exception): - pass - - -class ImageInstallationException(Exception): - pass - - class Container(IsolationProvider): # Name of the dangerzone container - CONTAINER_NAME = "dangerzone.rocks/dangerzone" - - @staticmethod - 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 - - @staticmethod - def get_runtime_version() -> 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 - that are not available across all of our platforms. In order to have a proper - fallback, we need to know the Podman version. More specifically, we're fine with - just knowing the major and minor version, since writing/installing a full-blown - semver parser is an overkill. - """ - # Get the Docker/Podman version, using a Go template. - runtime = Container.get_runtime_name() - if runtime == "podman": - query = "{{.Client.Version}}" - else: - query = "{{.Server.Version}}" - - cmd = [runtime, "version", "-f", query] - try: - version = subprocess.run( - cmd, - startupinfo=get_subprocess_startupinfo(), - capture_output=True, - check=True, - ).stdout.decode() - except Exception as e: - msg = f"Could not get the version of the {runtime.capitalize()} tool: {e}" - raise RuntimeError(msg) from e - - # Parse this version and return the major/minor parts, since we don't need the - # rest. - try: - major, minor, _ = version.split(".", 3) - return (int(major), int(minor)) - except Exception as e: - msg = ( - f"Could not parse the version of the {runtime.capitalize()} tool" - f" (found: '{version}') due to the following error: {e}" - ) - raise RuntimeError(msg) - - @staticmethod - def get_runtime() -> str: - container_tech = Container.get_runtime_name() - runtime = shutil.which(container_tech) - if runtime is None: - raise NoContainerTechException(container_tech) - return runtime - @staticmethod def get_runtime_security_args() -> List[str]: """Security options applicable to the outer Dangerzone container. @@ -131,7 +49,7 @@ class Container(IsolationProvider): - This particular argument is specified in `start_doc_to_pixels_proc()`, but should move here once #748 is merged. """ - if Container.get_runtime_name() == "podman": + if container_utils.get_runtime_name() == "podman": security_args = ["--log-driver", "none"] security_args += ["--security-opt", "no-new-privileges"] else: @@ -155,110 +73,6 @@ class Container(IsolationProvider): return security_args - @staticmethod - def list_image_tags() -> Dict[str, str]: - """Get the tags of all loaded Dangerzone images. - - This method returns a mapping of image tags to image IDs, for all Dangerzone - 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. - """ - images = json.loads( - subprocess.check_output( - [ - Container.get_runtime(), - "image", - "list", - "--format", - "json", - Container.CONTAINER_NAME, - ], - text=True, - startupinfo=get_subprocess_startupinfo(), - ) - ) - - # Grab every image name and associate it with an image ID. - tags = {} - for image in images: - for name in image["Names"]: - tag = name.split(":")[1] - tags[tag] = image["Id"] - - return tags - - @staticmethod - def delete_image_tag(tag: str) -> None: - """Delete a Dangerzone image tag.""" - name = Container.CONTAINER_NAME + ":" + tag - log.warning(f"Deleting old container image: {name}") - try: - subprocess.check_output( - [Container.get_runtime(), "rmi", "--force", name], - startupinfo=get_subprocess_startupinfo(), - ) - except Exception as e: - log.warning( - f"Couldn't delete old container image '{name}', so leaving it there." - f" Original error: {e}" - ) - - @staticmethod - def add_image_tag(cur_tag: str, new_tag: str) -> None: - """Add a tag to an existing Dangerzone image.""" - cur_image_name = Container.CONTAINER_NAME + ":" + cur_tag - new_image_name = Container.CONTAINER_NAME + ":" + new_tag - subprocess.check_output( - [ - Container.get_runtime(), - "tag", - cur_image_name, - new_image_name, - ], - startupinfo=get_subprocess_startupinfo(), - ) - - log.info( - f"Successfully tagged container image '{cur_image_name}' as '{new_image_name}'" - ) - - @staticmethod - 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: - return f.read().strip() - - @staticmethod - def load_image_tarball() -> None: - log.info("Installing Dangerzone container image...") - p = subprocess.Popen( - [Container.get_runtime(), "load"], - stdin=subprocess.PIPE, - startupinfo=get_subprocess_startupinfo(), - ) - - chunk_size = 4 << 20 - compressed_container_path = get_resource_path("container.tar.gz") - with gzip.open(compressed_container_path) as f: - while True: - chunk = f.read(chunk_size) - if len(chunk) > 0: - if p.stdin: - p.stdin.write(chunk) - else: - break - _, err = p.communicate() - if p.returncode < 0: - if err: - error = err.decode() - else: - error = "No output" - raise ImageInstallationException( - f"Could not install container image: {error}" - ) - - log.info("Successfully installed container image from") - @staticmethod def install() -> bool: """Install the container image tarball, or verify that it's already installed. @@ -273,8 +87,8 @@ class Container(IsolationProvider): 3. Load the image tarball and make sure it matches the expected tag. 4. Tag that image as "latest", and mark the installation as finished. """ - old_tags = Container.list_image_tags() - expected_tag = Container.get_expected_tag() + old_tags = container_utils.list_image_tags() + expected_tag = container_utils.get_expected_tag() if expected_tag not in old_tags: # Prune older container images. @@ -282,29 +96,29 @@ class Container(IsolationProvider): f"Could not find a Dangerzone container image with tag '{expected_tag}'" ) for tag in old_tags.keys(): - Container.delete_image_tag(tag) + container_utils.delete_image_tag(tag) elif old_tags[expected_tag] != old_tags.get("latest"): log.info(f"The expected tag '{expected_tag}' is not the latest one") - Container.add_image_tag(expected_tag, "latest") + container_utils.add_image_tag(expected_tag, "latest") return True else: return True # Load the image tarball into the container runtime. - Container.load_image_tarball() + container_utils.load_image_tarball() # Check that the container image has the expected image tag. # See https://github.com/freedomofpress/dangerzone/issues/988 for an example # where this was not the case. - new_tags = Container.list_image_tags() + new_tags = container_utils.list_image_tags() if expected_tag not in new_tags: - raise ImageNotPresentException( + raise errors.ImageNotPresentException( f"Could not find expected tag '{expected_tag}' after loading the" " container image tarball" ) # Mark the expected tag as "latest". - Container.add_image_tag(expected_tag, "latest") + container_utils.add_image_tag(expected_tag, "latest") return True @staticmethod @@ -313,8 +127,8 @@ class Container(IsolationProvider): @staticmethod def is_available() -> bool: - container_runtime = Container.get_runtime() - runtime_name = Container.get_runtime_name() + container_runtime = container_utils.get_runtime() + runtime_name = container_utils.get_runtime_name() # Can we run `docker/podman image ls` without an error with subprocess.Popen( [container_runtime, "image", "ls"], @@ -324,7 +138,9 @@ class Container(IsolationProvider): ) as p: _, stderr = p.communicate() if p.returncode != 0: - raise NotAvailableContainerTechException(runtime_name, stderr.decode()) + raise errors.NotAvailableContainerTechException( + runtime_name, stderr.decode() + ) return True def doc_to_pixels_container_name(self, document: Document) -> str: @@ -359,7 +175,7 @@ class Container(IsolationProvider): name: str, extra_args: List[str] = [], ) -> subprocess.Popen: - container_runtime = self.get_runtime() + container_runtime = container_utils.get_runtime() security_args = self.get_runtime_security_args() enable_stdin = ["-i"] set_name = ["--name", name] @@ -371,7 +187,7 @@ class Container(IsolationProvider): + enable_stdin + set_name + extra_args - + [self.CONTAINER_NAME] + + [container_utils.CONTAINER_NAME] + command ) args = [container_runtime] + args @@ -387,7 +203,7 @@ class Container(IsolationProvider): connected to the Docker daemon, and killing it will just close the associated standard streams. """ - container_runtime = self.get_runtime() + container_runtime = container_utils.get_runtime() cmd = [container_runtime, "kill", name] try: # We do not check the exit code of the process here, since the container may @@ -423,8 +239,8 @@ class Container(IsolationProvider): # NOTE: Using `--userns nomap` is available only on Podman >= 4.1.0. # XXX: Move this under `get_runtime_security_args()` once #748 is merged. extra_args = [] - if Container.get_runtime_name() == "podman": - if Container.get_runtime_version() >= (4, 1): + if container_utils.get_runtime_name() == "podman": + if container_utils.get_runtime_version() >= (4, 1): extra_args += ["--userns", "nomap"] name = self.doc_to_pixels_container_name(document) @@ -451,7 +267,7 @@ 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 = self.get_runtime() + container_runtime = container_utils.get_runtime() name = self.doc_to_pixels_container_name(document) all_containers = subprocess.run( [container_runtime, "ps", "-a"], @@ -473,11 +289,11 @@ class Container(IsolationProvider): if cpu_count is not None: n_cpu = cpu_count - elif self.get_runtime_name() == "docker": + elif container_utils.get_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( - [self.get_runtime(), "info", "--format", "{{.NCPU}}"], + [container_utils.get_runtime(), "info", "--format", "{{.NCPU}}"], text=True, startupinfo=get_subprocess_startupinfo(), ) diff --git a/tests/gui/test_main_window.py b/tests/gui/test_main_window.py index f8060b0..03b78a8 100644 --- a/tests/gui/test_main_window.py +++ b/tests/gui/test_main_window.py @@ -10,6 +10,7 @@ from pytest_mock import MockerFixture from pytest_subprocess import FakeProcess from pytestqt.qtbot import QtBot +from dangerzone import errors from dangerzone.document import Document from dangerzone.gui import MainWindow from dangerzone.gui import main_window as main_window_module @@ -25,11 +26,7 @@ from dangerzone.gui.main_window import ( WaitingWidgetContainer, ) from dangerzone.gui.updater import UpdateReport, UpdaterThread -from dangerzone.isolation_provider.container import ( - Container, - NoContainerTechException, - NotAvailableContainerTechException, -) +from dangerzone.isolation_provider.container import Container from dangerzone.isolation_provider.dummy import Dummy from .test_updater import assert_report_equal, default_updater_settings @@ -513,7 +510,7 @@ def test_not_available_container_tech_exception( mock_app = mocker.MagicMock() dummy = Dummy() fn = mocker.patch.object(dummy, "is_available") - fn.side_effect = NotAvailableContainerTechException( + fn.side_effect = errors.NotAvailableContainerTechException( "podman", "podman image ls logs" ) @@ -536,7 +533,7 @@ def test_no_container_tech_exception(qtbot: QtBot, mocker: MockerFixture) -> Non dummy = mocker.MagicMock() # Raise - dummy.is_available.side_effect = NoContainerTechException("podman") + dummy.is_available.side_effect = errors.NoContainerTechException("podman") dz = DangerzoneGui(mock_app, dummy) widget = WaitingWidgetContainer(dz) diff --git a/tests/isolation_provider/test_container.py b/tests/isolation_provider/test_container.py index ad5dc6b..76a797c 100644 --- a/tests/isolation_provider/test_container.py +++ b/tests/isolation_provider/test_container.py @@ -4,12 +4,8 @@ import pytest from pytest_mock import MockerFixture from pytest_subprocess import FakeProcess -from dangerzone.isolation_provider.container import ( - Container, - ImageInstallationException, - ImageNotPresentException, - NotAvailableContainerTechException, -) +from dangerzone import container_utils, errors +from dangerzone.isolation_provider.container import Container from dangerzone.isolation_provider.qubes import is_qubes_native_conversion from .base import IsolationProviderTermination, IsolationProviderTest @@ -33,11 +29,11 @@ class TestContainer(IsolationProviderTest): the "podman image ls" command fails. """ fp.register_subprocess( - [provider.get_runtime(), "image", "ls"], + [container_utils.get_runtime(), "image", "ls"], returncode=-1, stderr="podman image ls logs", ) - with pytest.raises(NotAvailableContainerTechException): + with pytest.raises(errors.NotAvailableContainerTechException): provider.is_available() def test_is_available_works(self, provider: Container, fp: FakeProcess) -> None: @@ -45,7 +41,7 @@ class TestContainer(IsolationProviderTest): No exception should be raised when the "podman image ls" can return properly. """ fp.register_subprocess( - [provider.get_runtime(), "image", "ls"], + [container_utils.get_runtime(), "image", "ls"], ) provider.is_available() @@ -55,13 +51,13 @@ class TestContainer(IsolationProviderTest): """When an image installation fails, an exception should be raised""" fp.register_subprocess( - [provider.get_runtime(), "image", "ls"], + [container_utils.get_runtime(), "image", "ls"], ) # First check should return nothing. fp.register_subprocess( [ - provider.get_runtime(), + container_utils.get_runtime(), "image", "list", "--format", @@ -76,11 +72,11 @@ class TestContainer(IsolationProviderTest): mocker.patch("gzip.open", mocker.mock_open(read_data="")) fp.register_subprocess( - [provider.get_runtime(), "load"], + [container_utils.get_runtime(), "load"], returncode=-1, ) - with pytest.raises(ImageInstallationException): + with pytest.raises(errors.ImageInstallationException): provider.install() def test_install_raises_if_still_not_installed( @@ -89,13 +85,13 @@ class TestContainer(IsolationProviderTest): """When an image keep being not installed, it should return False""" fp.register_subprocess( - [provider.get_runtime(), "image", "ls"], + [container_utils.get_runtime(), "image", "ls"], ) # First check should return nothing. fp.register_subprocess( [ - provider.get_runtime(), + container_utils.get_runtime(), "image", "list", "--format", @@ -109,9 +105,9 @@ class TestContainer(IsolationProviderTest): # Patch gzip.open and podman load so that it works mocker.patch("gzip.open", mocker.mock_open(read_data="")) fp.register_subprocess( - [provider.get_runtime(), "load"], + [container_utils.get_runtime(), "load"], ) - with pytest.raises(ImageNotPresentException): + with pytest.raises(errors.ImageNotPresentException): provider.install() From eefe7c15ce081b69b0ce2c80452577a63b2e610d Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Wed, 4 Dec 2024 17:34:19 +0200 Subject: [PATCH 10/15] Move container security arg to proper place Now that #748 has been merged, we can move the `--userns nomap` argument to the list with the rest of our security arguments. --- dangerzone/isolation_provider/container.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index 878c7d3..72da7ae 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -46,12 +46,12 @@ class Container(IsolationProvider): * Do not log the container's output. * Do not map the host user to the container, with `--userns nomap` (available from Podman 4.1 onwards) - - This particular argument is specified in `start_doc_to_pixels_proc()`, but - should move here once #748 is merged. """ if container_utils.get_runtime_name() == "podman": security_args = ["--log-driver", "none"] security_args += ["--security-opt", "no-new-privileges"] + if container_utils.get_runtime_version() >= (4, 1): + security_args += ["--userns", "nomap"] else: security_args = ["--security-opt=no-new-privileges:true"] @@ -173,7 +173,6 @@ class Container(IsolationProvider): self, command: List[str], name: str, - extra_args: List[str] = [], ) -> subprocess.Popen: container_runtime = container_utils.get_runtime() security_args = self.get_runtime_security_args() @@ -186,7 +185,6 @@ class Container(IsolationProvider): + prevent_leakage_args + enable_stdin + set_name - + extra_args + [container_utils.CONTAINER_NAME] + command ) @@ -236,15 +234,8 @@ class Container(IsolationProvider): "-m", "dangerzone.conversion.doc_to_pixels", ] - # NOTE: Using `--userns nomap` is available only on Podman >= 4.1.0. - # XXX: Move this under `get_runtime_security_args()` once #748 is merged. - extra_args = [] - if container_utils.get_runtime_name() == "podman": - if container_utils.get_runtime_version() >= (4, 1): - extra_args += ["--userns", "nomap"] - name = self.doc_to_pixels_container_name(document) - return self.exec_container(command, name=name, extra_args=extra_args) + return self.exec_container(command, name=name) def terminate_doc_to_pixels_proc( self, document: Document, p: subprocess.Popen From 2f438c09f11a98edbadca89f02f426c8f4967b46 Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Wed, 4 Dec 2024 17:37:45 +0200 Subject: [PATCH 11/15] FIXUP: Use longer tag description, so that the commit is always shown --- install/common/build-image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/common/build-image.py b/install/common/build-image.py index 921a520..dfa32b4 100644 --- a/install/common/build-image.py +++ b/install/common/build-image.py @@ -59,7 +59,7 @@ def main(): dirty_ident = secrets.token_hex(2) tag = ( subprocess.check_output( - ["git", "describe", "--first-parent", f"--dirty=-{dirty_ident}"], + ["git", "describe", "--long", "--first-parent", f"--dirty=-{dirty_ident}"], ) .decode() .strip()[1:] # remove the "v" prefix of the tag. From c0fa32b6b81a98c72cfe3f967977d5764573789e Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Wed, 4 Dec 2024 18:11:31 +0200 Subject: [PATCH 12/15] FIXUP: Don't require a 'latest' image tag anymore --- QA.md | 2 -- dangerzone/container_utils.py | 40 ++++++++-------------- dangerzone/isolation_provider/container.py | 15 +++----- dev_scripts/qa.py | 2 -- install/common/build-image.py | 6 +--- tests/isolation_provider/test_container.py | 6 ++-- 6 files changed, 21 insertions(+), 50 deletions(-) diff --git a/QA.md b/QA.md index 5302f92..0f0a760 100644 --- a/QA.md +++ b/QA.md @@ -109,7 +109,6 @@ version. For example: ``` $ docker images dangerzone.rocks/dangerzone REPOSITORY TAG IMAGE ID CREATED SIZE -dangerzone.rocks/dangerzone latest dangerzone.rocks/dangerzone ``` @@ -121,7 +120,6 @@ and seeing the following differences: ``` $ docker images dangerzone.rocks/dangerzone REPOSITORY TAG IMAGE ID CREATED SIZE -dangerzone.rocks/dangerzone latest dangerzone.rocks/dangerzone ``` diff --git a/dangerzone/container_utils.py b/dangerzone/container_utils.py index 6d1e48f..5f9172e 100644 --- a/dangerzone/container_utils.py +++ b/dangerzone/container_utils.py @@ -1,10 +1,9 @@ import gzip -import json import logging import platform import shutil import subprocess -from typing import Dict, Tuple +from typing import List, Tuple from .util import get_resource_path, get_subprocess_startupinfo from . import errors @@ -72,36 +71,25 @@ def get_runtime() -> str: return runtime -def list_image_tags() -> Dict[str, str]: +def list_image_tags() -> List[str]: """Get the tags of all loaded Dangerzone images. This method returns a mapping of image tags to image IDs, for all Dangerzone 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. """ - images = json.loads( - subprocess.check_output( - [ - get_runtime(), - "image", - "list", - "--format", - "json", - CONTAINER_NAME, - ], - text=True, - startupinfo=get_subprocess_startupinfo(), - ) - ) - - # Grab every image name and associate it with an image ID. - tags = {} - for image in images: - for name in image["Names"]: - tag = name.split(":")[1] - tags[tag] = image["Id"] - - return tags + return subprocess.check_output( + [ + get_runtime(), + "image", + "list", + "--format", + "{{ .Tag }}", + CONTAINER_NAME, + ], + text=True, + startupinfo=get_subprocess_startupinfo(), + ).strip().split() def delete_image_tag(tag: str) -> None: diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index 72da7ae..e757c4a 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -81,11 +81,9 @@ class Container(IsolationProvider): 1. Get the tags of any locally available images that match Dangerzone's image name. 2. Get the expected image tag from the image-id.txt file. - - If this tag is present in the local images, and that image is also tagged - as "latest", then we can return. + - If this tag is present in the local images, then we can return. - Else, prune the older container images and continue. 3. Load the image tarball and make sure it matches the expected tag. - 4. Tag that image as "latest", and mark the installation as finished. """ old_tags = container_utils.list_image_tags() expected_tag = container_utils.get_expected_tag() @@ -95,12 +93,8 @@ class Container(IsolationProvider): log.info( f"Could not find a Dangerzone container image with tag '{expected_tag}'" ) - for tag in old_tags.keys(): + for tag in old_tags: container_utils.delete_image_tag(tag) - elif old_tags[expected_tag] != old_tags.get("latest"): - log.info(f"The expected tag '{expected_tag}' is not the latest one") - container_utils.add_image_tag(expected_tag, "latest") - return True else: return True @@ -117,8 +111,6 @@ class Container(IsolationProvider): " container image tarball" ) - # Mark the expected tag as "latest". - container_utils.add_image_tag(expected_tag, "latest") return True @staticmethod @@ -179,13 +171,14 @@ class Container(IsolationProvider): enable_stdin = ["-i"] set_name = ["--name", name] prevent_leakage_args = ["--rm"] + image_name = [container_utils.CONTAINER_NAME + ":" + container_utils.get_expected_tag()] args = ( ["run"] + security_args + prevent_leakage_args + enable_stdin + set_name - + [container_utils.CONTAINER_NAME] + + image_name + command ) args = [container_runtime] + args diff --git a/dev_scripts/qa.py b/dev_scripts/qa.py index ebd6099..dfc352b 100755 --- a/dev_scripts/qa.py +++ b/dev_scripts/qa.py @@ -129,7 +129,6 @@ version. For example: ``` $ docker images dangerzone.rocks/dangerzone REPOSITORY TAG IMAGE ID CREATED SIZE -dangerzone.rocks/dangerzone latest dangerzone.rocks/dangerzone ``` @@ -141,7 +140,6 @@ and seeing the following differences: ``` $ docker images dangerzone.rocks/dangerzone REPOSITORY TAG IMAGE ID CREATED SIZE -dangerzone.rocks/dangerzone latest dangerzone.rocks/dangerzone ``` diff --git a/install/common/build-image.py b/install/common/build-image.py index dfa32b4..3e2ab71 100644 --- a/install/common/build-image.py +++ b/install/common/build-image.py @@ -83,11 +83,9 @@ def main(): check=True, ) - # Build the container image, and tag it with two tags; the one we calculated - # above, and the "latest" tag. + # Build the container image, and tag it with the calculated tag print("Building container image") cache_args = [] if args.use_cache else ["--no-cache"] - image_name_latest = IMAGE_NAME + ":latest" subprocess.run( [ args.runtime, @@ -101,8 +99,6 @@ def main(): "-f", "Dockerfile", "--tag", - image_name_latest, - "--tag", image_name_tagged, ], check=True, diff --git a/tests/isolation_provider/test_container.py b/tests/isolation_provider/test_container.py index 76a797c..15a393f 100644 --- a/tests/isolation_provider/test_container.py +++ b/tests/isolation_provider/test_container.py @@ -61,11 +61,10 @@ class TestContainer(IsolationProviderTest): "image", "list", "--format", - "json", + "{{ .Tag }}", "dangerzone.rocks/dangerzone", ], occurrences=2, - stdout="{}", ) # Make podman load fail @@ -95,11 +94,10 @@ class TestContainer(IsolationProviderTest): "image", "list", "--format", - "json", + "{{ .Tag }}", "dangerzone.rocks/dangerzone", ], occurrences=2, - stdout="{}", ) # Patch gzip.open and podman load so that it works From bd72b6a93bc50ab3ae30e1c1dfc286741dfce162 Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Wed, 4 Dec 2024 18:04:43 +0200 Subject: [PATCH 13/15] ci: Work with image tarballs that are not tagged as 'latest' Now that our image tarball is not tagged as 'latest', we must first grab the image tag first, and then refer to it. We can grab the tag either from `share/image-id.txt` (if available) or with: docker load dangerzone.rocks/dangerzone --format {{ .Tag }} --- .github/workflows/build.yml | 5 +++-- .github/workflows/ci.yml | 8 ++++---- .github/workflows/scan.yml | 10 ++++++++-- .github/workflows/scan_released.yml | 10 ++++++++-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a270a8..e5e60c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,7 +85,7 @@ jobs: id: cache-container-image uses: actions/cache@v4 with: - key: v2-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }} + key: v3-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }} path: | share/container.tar.gz share/image-id.txt @@ -97,6 +97,7 @@ jobs: python3 ./install/common/build-image.py echo ${{ github.token }} | podman login ghcr.io -u USERNAME --password-stdin gunzip -c share/container.tar.gz | podman load + tag=$(cat share/image-id.txt) podman push \ dangerzone.rocks/dangerzone \ - ${{ env.IMAGE_REGISTRY }}/dangerzone/dangerzone + ${{ env.IMAGE_REGISTRY }}/dangerzone/dangerzone:$tag diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e694f31..4ecbf29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: id: cache-container-image uses: actions/cache@v4 with: - key: v2-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }} + key: v3-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }} path: |- share/container.tar.gz share/image-id.txt @@ -223,7 +223,7 @@ jobs: - name: Restore container cache uses: actions/cache/restore@v4 with: - key: v2-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }} + key: v3-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }} path: |- share/container.tar.gz share/image-id.txt @@ -330,7 +330,7 @@ jobs: - name: Restore container image uses: actions/cache/restore@v4 with: - key: v2-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }} + key: v3-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }} path: |- share/container.tar.gz share/image-id.txt @@ -425,7 +425,7 @@ jobs: - name: Restore container image uses: actions/cache/restore@v4 with: - key: v2-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }} + key: v3-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }} path: |- share/container.tar.gz share/image-id.txt diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 3080476..0087ac4 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -20,13 +20,18 @@ jobs: run: sudo apt install pipx && pipx install poetry - name: Build container image run: python3 ./install/common/build-image.py --runtime docker --no-save + - name: Get image tag + id: tag + run: | + tag=$(docker images dangerzone.rocks/dangerzone --format '{{ .Tag }}') + echo "tag=$tag" >> $GITHUB_OUTPUT # NOTE: Scan first without failing, else we won't be able to read the scan # report. - name: Scan container image (no fail) uses: anchore/scan-action@v5 id: scan_container with: - image: "dangerzone.rocks/dangerzone:latest" + image: "dangerzone.rocks/dangerzone:{{ steps.tag.outputs.tag }}" fail-build: false only-fixed: false severity-cutoff: critical @@ -40,7 +45,8 @@ jobs: - name: Scan container image uses: anchore/scan-action@v5 with: - image: "dangerzone.rocks/dangerzone:latest" + image: "dangerzone.rocks/dangerzone:{{ steps.tag.outputs.tag }}" + fail-build: false fail-build: true only-fixed: false severity-cutoff: critical diff --git a/.github/workflows/scan_released.yml b/.github/workflows/scan_released.yml index 2bba78c..9a353c0 100644 --- a/.github/workflows/scan_released.yml +++ b/.github/workflows/scan_released.yml @@ -24,13 +24,19 @@ jobs: CONTAINER_FILENAME=container-${VERSION:1}-${{ matrix.arch }}.tar.gz wget https://github.com/freedomofpress/dangerzone/releases/download/${VERSION}/${CONTAINER_FILENAME} -O ${CONTAINER_FILENAME} docker load -i ${CONTAINER_FILENAME} + - name: Get image tag + id: tag + run: | + tag=$(docker images dangerzone.rocks/dangerzone --format '{{ .Tag }}') + echo "tag=$tag" >> $GITHUB_OUTPUT # NOTE: Scan first without failing, else we won't be able to read the scan # report. - name: Scan container image (no fail) uses: anchore/scan-action@v5 id: scan_container with: - image: "dangerzone.rocks/dangerzone:latest" + image: "dangerzone.rocks/dangerzone:{{ steps.tag.outputs.tag }}" + fail-build: false fail-build: false only-fixed: false severity-cutoff: critical @@ -44,7 +50,7 @@ jobs: - name: Scan container image uses: anchore/scan-action@v5 with: - image: "dangerzone.rocks/dangerzone:latest" + image: "dangerzone.rocks/dangerzone:{{ steps.tag.outputs.tag }}" fail-build: true only-fixed: false severity-cutoff: critical From 767617d21ca65cf9b4b46f49969e16251ef5ea65 Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Wed, 4 Dec 2024 18:14:23 +0200 Subject: [PATCH 14/15] FIXUP: Formatting --- dangerzone/container_utils.py | 30 ++++++++++++---------- dangerzone/isolation_provider/container.py | 4 ++- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/dangerzone/container_utils.py b/dangerzone/container_utils.py index 5f9172e..66d25b5 100644 --- a/dangerzone/container_utils.py +++ b/dangerzone/container_utils.py @@ -5,8 +5,8 @@ import shutil import subprocess from typing import List, Tuple -from .util import get_resource_path, get_subprocess_startupinfo from . import errors +from .util import get_resource_path, get_subprocess_startupinfo CONTAINER_NAME = "dangerzone.rocks/dangerzone" @@ -78,18 +78,22 @@ 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. """ - return subprocess.check_output( - [ - get_runtime(), - "image", - "list", - "--format", - "{{ .Tag }}", - CONTAINER_NAME, - ], - text=True, - startupinfo=get_subprocess_startupinfo(), - ).strip().split() + return ( + subprocess.check_output( + [ + get_runtime(), + "image", + "list", + "--format", + "{{ .Tag }}", + CONTAINER_NAME, + ], + text=True, + startupinfo=get_subprocess_startupinfo(), + ) + .strip() + .split() + ) def delete_image_tag(tag: str) -> None: diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index e757c4a..1a08385 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -171,7 +171,9 @@ class Container(IsolationProvider): enable_stdin = ["-i"] set_name = ["--name", name] prevent_leakage_args = ["--rm"] - image_name = [container_utils.CONTAINER_NAME + ":" + container_utils.get_expected_tag()] + image_name = [ + container_utils.CONTAINER_NAME + ":" + container_utils.get_expected_tag() + ] args = ( ["run"] + security_args From 7b1d1756401c11ea840952baa69f0ce07d9c9016 Mon Sep 17 00:00:00 2001 From: Alex Pyrgiotis Date: Wed, 4 Dec 2024 18:27:19 +0200 Subject: [PATCH 15/15] fixup! ci: Work with image tarballs that are not tagged as 'latest' --- .github/workflows/build.yml | 4 ++-- .github/workflows/scan.yml | 5 ++--- .github/workflows/scan_released.yml | 5 ++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5e60c7..35f9597 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,5 +99,5 @@ jobs: gunzip -c share/container.tar.gz | podman load tag=$(cat share/image-id.txt) podman push \ - dangerzone.rocks/dangerzone \ - ${{ env.IMAGE_REGISTRY }}/dangerzone/dangerzone:$tag + dangerzone.rocks/dangerzone:$tag \ + ${{ env.IMAGE_REGISTRY }}/dangerzone/dangerzone:tag diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 0087ac4..d985109 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -31,7 +31,7 @@ jobs: uses: anchore/scan-action@v5 id: scan_container with: - image: "dangerzone.rocks/dangerzone:{{ steps.tag.outputs.tag }}" + image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}" fail-build: false only-fixed: false severity-cutoff: critical @@ -45,8 +45,7 @@ jobs: - name: Scan container image uses: anchore/scan-action@v5 with: - image: "dangerzone.rocks/dangerzone:{{ steps.tag.outputs.tag }}" - fail-build: false + image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}" fail-build: true only-fixed: false severity-cutoff: critical diff --git a/.github/workflows/scan_released.yml b/.github/workflows/scan_released.yml index 9a353c0..0333e49 100644 --- a/.github/workflows/scan_released.yml +++ b/.github/workflows/scan_released.yml @@ -35,8 +35,7 @@ jobs: uses: anchore/scan-action@v5 id: scan_container with: - image: "dangerzone.rocks/dangerzone:{{ steps.tag.outputs.tag }}" - fail-build: false + image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}" fail-build: false only-fixed: false severity-cutoff: critical @@ -50,7 +49,7 @@ jobs: - name: Scan container image uses: anchore/scan-action@v5 with: - image: "dangerzone.rocks/dangerzone:{{ steps.tag.outputs.tag }}" + image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}" fail-build: true only-fixed: false severity-cutoff: critical