Compare commits

..

7 commits

Author SHA1 Message Date
Alex Pyrgiotis
2f4fc93a06
Merge 57973b8a2e into fdc27c4d3b 2024-12-02 19:16:00 +00:00
Alex Pyrgiotis
57973b8a2e
Update our release instructions 2024-12-02 21:15:34 +02:00
Alex Pyrgiotis
ab6d9ba1fd
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
2024-12-02 21:15:34 +02:00
Alex Pyrgiotis
8793871ffb
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.
2024-12-02 21:15:34 +02:00
Alex Pyrgiotis
eb560b8e7f
container: Factor out loading an image tarball 2024-12-02 21:15:34 +02:00
Alex Pyrgiotis
587cb6d40e
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()`
2024-12-02 21:15:34 +02:00
Alex Pyrgiotis
5ac1288277
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.
2024-12-02 21:15:34 +02:00
18 changed files with 567 additions and 722 deletions

View file

@ -85,7 +85,7 @@ jobs:
id: cache-container-image id: cache-container-image
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
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') }} 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') }}
path: | path: |
share/container.tar.gz share/container.tar.gz
share/image-id.txt share/image-id.txt
@ -97,7 +97,6 @@ jobs:
python3 ./install/common/build-image.py python3 ./install/common/build-image.py
echo ${{ github.token }} | podman login ghcr.io -u USERNAME --password-stdin echo ${{ github.token }} | podman login ghcr.io -u USERNAME --password-stdin
gunzip -c share/container.tar.gz | podman load gunzip -c share/container.tar.gz | podman load
tag=$(cat share/image-id.txt)
podman push \ podman push \
dangerzone.rocks/dangerzone:$tag \ dangerzone.rocks/dangerzone \
${{ env.IMAGE_REGISTRY }}/dangerzone/dangerzone:tag ${{ env.IMAGE_REGISTRY }}/dangerzone/dangerzone

View file

@ -59,7 +59,7 @@ jobs:
id: cache-container-image id: cache-container-image
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
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') }} 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') }}
path: |- path: |-
share/container.tar.gz share/container.tar.gz
share/image-id.txt share/image-id.txt
@ -223,7 +223,7 @@ jobs:
- name: Restore container cache - name: Restore container cache
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
with: with:
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') }} 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') }}
path: |- path: |-
share/container.tar.gz share/container.tar.gz
share/image-id.txt share/image-id.txt
@ -330,7 +330,7 @@ jobs:
- name: Restore container image - name: Restore container image
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
with: with:
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') }} 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') }}
path: |- path: |-
share/container.tar.gz share/container.tar.gz
share/image-id.txt share/image-id.txt
@ -425,7 +425,7 @@ jobs:
- name: Restore container image - name: Restore container image
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
with: with:
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') }} 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') }}
path: |- path: |-
share/container.tar.gz share/container.tar.gz
share/image-id.txt share/image-id.txt

View file

@ -20,18 +20,13 @@ jobs:
run: sudo apt install pipx && pipx install poetry run: sudo apt install pipx && pipx install poetry
- name: Build container image - name: Build container image
run: python3 ./install/common/build-image.py --runtime docker --no-save 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 # NOTE: Scan first without failing, else we won't be able to read the scan
# report. # report.
- name: Scan container image (no fail) - name: Scan container image (no fail)
uses: anchore/scan-action@v5 uses: anchore/scan-action@v5
id: scan_container id: scan_container
with: with:
image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}" image: "dangerzone.rocks/dangerzone:latest"
fail-build: false fail-build: false
only-fixed: false only-fixed: false
severity-cutoff: critical severity-cutoff: critical
@ -45,7 +40,7 @@ jobs:
- name: Scan container image - name: Scan container image
uses: anchore/scan-action@v5 uses: anchore/scan-action@v5
with: with:
image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}" image: "dangerzone.rocks/dangerzone:latest"
fail-build: true fail-build: true
only-fixed: false only-fixed: false
severity-cutoff: critical severity-cutoff: critical

View file

@ -24,18 +24,13 @@ jobs:
CONTAINER_FILENAME=container-${VERSION:1}-${{ matrix.arch }}.tar.gz CONTAINER_FILENAME=container-${VERSION:1}-${{ matrix.arch }}.tar.gz
wget https://github.com/freedomofpress/dangerzone/releases/download/${VERSION}/${CONTAINER_FILENAME} -O ${CONTAINER_FILENAME} wget https://github.com/freedomofpress/dangerzone/releases/download/${VERSION}/${CONTAINER_FILENAME} -O ${CONTAINER_FILENAME}
docker load -i ${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 # NOTE: Scan first without failing, else we won't be able to read the scan
# report. # report.
- name: Scan container image (no fail) - name: Scan container image (no fail)
uses: anchore/scan-action@v5 uses: anchore/scan-action@v5
id: scan_container id: scan_container
with: with:
image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}" image: "dangerzone.rocks/dangerzone:latest"
fail-build: false fail-build: false
only-fixed: false only-fixed: false
severity-cutoff: critical severity-cutoff: critical
@ -49,7 +44,7 @@ jobs:
- name: Scan container image - name: Scan container image
uses: anchore/scan-action@v5 uses: anchore/scan-action@v5
with: with:
image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}" image: "dangerzone.rocks/dangerzone:latest"
fail-build: true fail-build: true
only-fixed: false only-fixed: false
severity-cutoff: critical severity-cutoff: critical

197
QA.md
View file

@ -1,197 +0,0 @@
## QA
To ensure that new releases do not introduce regressions, and support existing
and newer platforms, we have to test that the produced packages work as expected.
Check the following:
- [ ] Make sure that the tip of the `main` branch passes the CI tests.
- [ ] Make sure that the Apple account has a valid application password and has
agreed to the latest Apple terms (see [macOS release](#macos-release)
section).
Because it is repetitive, we wrote a script to help with the QA.
It can run the tasks for you, pausing when it needs manual intervention.
You can run it with a command like:
```bash
poetry run ./dev_scripts/qa.py {distro}-{version}
```
### The checklist
- [ ] Create a test build in Windows and make sure it works:
- [ ] Check if the suggested Python version is still supported.
- [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses
the new image.
- [ ] Download the OCR language data using `./install/common/download-tessdata.py`
- [ ] Run the Dangerzone tests.
- [ ] Build and run the Dangerzone .exe
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
- [ ] Create a test build in macOS (Intel CPU) and make sure it works:
- [ ] Check if the suggested Python version is still supported.
- [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses
the new image.
- [ ] Download the OCR language data using `./install/common/download-tessdata.py`
- [ ] Run the Dangerzone tests.
- [ ] Create and run an app bundle.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
- [ ] Create a test build in macOS (M1/2 CPU) and make sure it works:
- [ ] Check if the suggested Python version is still supported.
- [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses
the new image.
- [ ] Download the OCR language data using `./install/common/download-tessdata.py`
- [ ] Run the Dangerzone tests.
- [ ] Create and run an app bundle.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
- [ ] Create a test build in the most recent Ubuntu LTS platform (Ubuntu 24.04
as of writing this) and make sure it works:
- [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses
the new image.
- [ ] Download the OCR language data using `./install/common/download-tessdata.py`
- [ ] Run the Dangerzone tests.
- [ ] Create a .deb package and install it system-wide.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
- [ ] Create a test build in the most recent Fedora platform (Fedora 41 as of
writing this) and make sure it works:
- [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses
the new image.
- [ ] Download the OCR language data using `./install/common/download-tessdata.py`
- [ ] Run the Dangerzone tests.
- [ ] Create an .rpm package and install it system-wide.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
- [ ] Create a test build in the most recent Qubes Fedora template (Fedora 40 as
of writing this) and make sure it works:
- [ ] Create a new development environment with Poetry.
- [ ] Run the Dangerzone tests.
- [ ] Create a Qubes .rpm package and install it system-wide.
- [ ] Ensure that the Dangerzone application appears in the "Applications"
tab.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below) and make sure
they spawn disposable qubes.
### Scenarios
#### 1. Dangerzone correctly identifies that Docker/Podman is not installed
_(Only for MacOS / Windows)_
Temporarily hide the Docker/Podman binaries, e.g., rename the `docker` /
`podman` binaries to something else. Then run Dangerzone. Dangerzone should
prompt the user to install Docker/Podman.
#### 2. Dangerzone correctly identifies that Docker is not running
_(Only for MacOS / Windows)_
Stop the Docker Desktop application. Then run Dangerzone. Dangerzone should
prompt the user to start Docker Desktop.
#### 3. Updating Dangerzone handles external state correctly.
_(Applies to Windows/MacOS)_
Install the previous version of Dangerzone, downloaded from the website.
Open the Dangerzone application and enable some non-default settings.
**If there are new settings, make sure to change those as well**.
Close the Dangerzone application and get the container image for that
version. For example:
```
$ docker images dangerzone.rocks/dangerzone
REPOSITORY TAG IMAGE ID CREATED SIZE
dangerzone.rocks/dangerzone <tag> <image ID> <date> <size>
```
Then run the version under QA and ensure that the settings remain changed.
Afterwards check that new docker image was installed by running the same command
and seeing the following differences:
```
$ docker images dangerzone.rocks/dangerzone
REPOSITORY TAG IMAGE ID CREATED SIZE
dangerzone.rocks/dangerzone <other tag> <different ID> <newer date> <different size>
```
#### 4. Dangerzone successfully installs the container image
_(Only for Linux)_
Remove the Dangerzone container image from Docker/Podman. Then run Dangerzone.
Dangerzone should install the container image successfully.
#### 5. Dangerzone retains the settings of previous runs
Run Dangerzone and make some changes in the settings (e.g., change the OCR
language, toggle whether to open the document after conversion, etc.). Restart
Dangerzone. Dangerzone should show the settings that the user chose.
#### 6. Dangerzone reports failed conversions
Run Dangerzone and convert the `tests/test_docs/sample_bad_pdf.pdf` document.
Dangerzone should fail gracefully, by reporting that the operation failed, and
showing the following error message:
> The document format is not supported
#### 7. Dangerzone succeeds in converting multiple documents
Run Dangerzone against a list of documents, and tick all options. Ensure that:
* Conversions take place sequentially.
* Attempting to close the window while converting asks the user if they want to
abort the conversions.
* Conversions are completed successfully.
* Conversions show individual progress in real-time (double-check for Qubes).
* _(Only for Linux)_ The resulting files open with the PDF viewer of our choice.
* OCR seems to have detected characters in the PDF files.
* The resulting files have been saved with the proper suffix, in the proper
location.
* The original files have been saved in the `unsafe/` directory.
#### 8. Dangerzone is able to handle drag-n-drop
Run Dangerzone against a set of documents that you drag-n-drop. Files should be
added and conversion should run without issue.
> [!TIP]
> On our end-user container environments for Linux, we can start a file manager
> with `thunar &`.
#### 9. Dangerzone CLI succeeds in converting multiple documents
_(Only for Windows and Linux)_
Run Dangerzone CLI against a list of documents. Ensure that conversions happen
sequentially, are completed successfully, and we see their progress.
#### 10. Dangerzone can open a document for conversion via right-click -> "Open With"
_(Only for Windows, MacOS and Qubes)_
Go to a directory with office documents, right-click on one, and click on "Open
With". We should be able to open the file with Dangerzone, and then convert it.
#### 11. Dangerzone shows helpful errors for setup issues on Qubes
_(Only for Qubes)_
Check what errors does Dangerzone throw in the following scenarios. The errors
should point the user to the Qubes notifications in the top-right corner:
1. The `dz-dvm` template does not exist. We can trigger this scenario by
temporarily renaming this template.
2. The Dangerzone RPC policy does not exist. We can trigger this scenario by
temporarily renaming the `dz.Convert` policy.
3. The `dz-dvm` disposable Qube cannot start due to insufficient resources. We
can trigger this scenario by temporarily increasing the minimum required RAM
of the `dz-dvm` template to more than the available amount.

View file

@ -1,13 +1,12 @@
# Release instructions # Release instructions
This section documents how we currently release Dangerzone for the different distributions we support. This section documents the release process. Unless you're a dangerzone developer making a release, you'll probably never need to follow it.
## Pre-release ## Pre-release
Here is a list of tasks that should be done before issuing the release: Before making a release, all of these should be complete:
- [ ] Create a new issue named **QA and Release for version \<VERSION\>**, to track the general progress. - [ ] Copy the checkboxes from these instructions onto a new issue and call it **QA and Release version \<VERSION\>**
You can generate its content with the the `poetry run ./dev_scripts/generate-release-tasks.py` command.
- [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-platforms-and-remove-obsolete-ones) - [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-platforms-and-remove-obsolete-ones)
- [ ] Bump the Python dependencies using `poetry lock` - [ ] Bump the Python dependencies using `poetry lock`
- [ ] Update `version` in `pyproject.toml` - [ ] Update `version` in `pyproject.toml`
@ -16,8 +15,6 @@ Here is a list of tasks that should be done before issuing the release:
- [ ] Bump the Debian version by adding a new changelog entry in `debian/changelog` - [ ] Bump the Debian version by adding a new changelog entry in `debian/changelog`
- [ ] Update screenshot in `README.md`, if necessary - [ ] Update screenshot in `README.md`, if necessary
- [ ] CHANGELOG.md should be updated to include a list of all major changes since the last release - [ ] CHANGELOG.md should be updated to include a list of all major changes since the last release
- [ ] A draft release should be created. Copy the release notes text from the template at [`docs/templates/release-notes`](https://github.com/freedomofpress/dangerzone/tree/main/docs/templates/)
- [ ] Do the QA tasks
## Add new Linux platforms and remove obsolete ones ## Add new Linux platforms and remove obsolete ones
@ -40,7 +37,7 @@ In case of a new version (beta, RC, or official release):
`BUILD.md` files where necessary. `BUILD.md` files where necessary.
4. Send a PR with the above changes. 4. Send a PR with the above changes.
In case of the removal of a version: In case of an EOL version:
1. Remove any mention to this version from our repo. 1. Remove any mention to this version from our repo.
* Consult the previous paragraph, but also `grep` your way around. * Consult the previous paragraph, but also `grep` your way around.
@ -54,13 +51,194 @@ Follow the instructions in `docs/developer/TESTING.md` to run the tests.
These tests will identify any regressions or progression in terms of document coverage. These tests will identify any regressions or progression in terms of document coverage.
## QA
To ensure that new releases do not introduce regressions, and support existing
and newer platforms, we have to do the following:
- [ ] Make sure that the tip of the `main` branch passes the CI tests.
- [ ] Make sure that the Apple account has a valid application password and has
agreed to the latest Apple terms (see [macOS release](#macos-release)
section).
- [ ] Create a test build in Windows and make sure it works:
- [ ] Check if the suggested Python version is still supported.
- [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses
the new image.
- [ ] Run the Dangerzone tests.
- [ ] Build and run the Dangerzone .exe
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
- [ ] Create a test build in macOS (Intel CPU) and make sure it works:
- [ ] Check if the suggested Python version is still supported.
- [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses
the new image.
- [ ] Run the Dangerzone tests.
- [ ] Create and run an app bundle.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
- [ ] Create a test build in macOS (M1/2 CPU) and make sure it works:
- [ ] Check if the suggested Python version is still supported.
- [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses
the new image.
- [ ] Run the Dangerzone tests.
- [ ] Create and run an app bundle.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
- [ ] Create a test build in the most recent Ubuntu LTS platform (Ubuntu 24.04
as of writing this) and make sure it works:
- [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses
the new image.
- [ ] Run the Dangerzone tests.
- [ ] Create a .deb package and install it system-wide.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
- [ ] Create a test build in the most recent Fedora platform (Fedora 41 as of
writing this) and make sure it works:
- [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses
the new image.
- [ ] Run the Dangerzone tests.
- [ ] Create an .rpm package and install it system-wide.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
- [ ] Create a test build in the most recent Qubes Fedora template (Fedora 40 as
of writing this) and make sure it works:
- [ ] Create a new development environment with Poetry.
- [ ] Run the Dangerzone tests.
- [ ] Create a Qubes .rpm package and install it system-wide.
- [ ] Ensure that the Dangerzone application appears in the "Applications"
tab.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below) and make sure
they spawn disposable qubes.
### Scenarios
#### 1. Dangerzone correctly identifies that Docker/Podman is not installed
_(Only for MacOS / Windows)_
Temporarily hide the Docker/Podman binaries, e.g., rename the `docker` /
`podman` binaries to something else. Then run Dangerzone. Dangerzone should
prompt the user to install Docker/Podman.
#### 2. Dangerzone correctly identifies that Docker is not running
_(Only for MacOS / Windows)_
Stop the Docker Desktop application. Then run Dangerzone. Dangerzone should
prompt the user to start Docker Desktop.
#### 3. Updating Dangerzone handles external state correctly.
_(Applies to Windows/MacOS)_
Install the previous version of Dangerzone, downloaded from the website.
Open the Dangerzone application and enable some non-default settings.
**If there are new settings, make sure to change those as well**.
Close the Dangerzone application and get the container image for that
version. For example:
```
$ docker images dangerzone.rocks/dangerzone
REPOSITORY TAG IMAGE ID CREATED SIZE
dangerzone.rocks/dangerzone latest <image ID> <date> <size>
dangerzone.rocks/dangerzone <tag> <image ID> <date> <size>
```
Then run the version under QA and ensure that the settings remain changed.
Afterwards check that new docker image was installed by running the same command
and seeing the following differences:
```
$ docker images dangerzone.rocks/dangerzone
REPOSITORY TAG IMAGE ID CREATED SIZE
dangerzone.rocks/dangerzone latest <different ID> <newer date> <different size>
dangerzone.rocks/dangerzone <other tag> <different ID> <newer date> <different size>
```
#### 4. Dangerzone successfully installs the container image
_(Only for Linux)_
Remove the Dangerzone container image from Docker/Podman. Then run Dangerzone.
Dangerzone should install the container image successfully.
#### 5. Dangerzone retains the settings of previous runs
Run Dangerzone and make some changes in the settings (e.g., change the OCR
language, toggle whether to open the document after conversion, etc.). Restart
Dangerzone. Dangerzone should show the settings that the user chose.
#### 6. Dangerzone reports failed conversions
Run Dangerzone and convert the `tests/test_docs/sample_bad_pdf.pdf` document.
Dangerzone should fail gracefully, by reporting that the operation failed, and
showing the following error message:
> The document format is not supported
#### 7. Dangerzone succeeds in converting multiple documents
Run Dangerzone against a list of documents, and tick all options. Ensure that:
* Conversions take place sequentially.
* Attempting to close the window while converting asks the user if they want to
abort the conversions.
* Conversions are completed successfully.
* Conversions show individual progress in real-time (double-check for Qubes).
* _(Only for Linux)_ The resulting files open with the PDF viewer of our choice.
* OCR seems to have detected characters in the PDF files.
* The resulting files have been saved with the proper suffix, in the proper
location.
* The original files have been saved in the `unsafe/` directory.
#### 8. Dangerzone is able to handle drag-n-drop
Run Dangerzone against a set of documents that you drag-n-drop. Files should be
added and conversion should run without issue.
> [!TIP]
> On our end-user container environments for Linux, we can start a file manager
> with `thunar &`.
#### 9. Dangerzone CLI succeeds in converting multiple documents
_(Only for Windows and Linux)_
Run Dangerzone CLI against a list of documents. Ensure that conversions happen
sequentially, are completed successfully, and we see their progress.
#### 10. Dangerzone can open a document for conversion via right-click -> "Open With"
_(Only for Windows, MacOS and Qubes)_
Go to a directory with office documents, right-click on one, and click on "Open
With". We should be able to open the file with Dangerzone, and then convert it.
#### 11. Dangerzone shows helpful errors for setup issues on Qubes
_(Only for Qubes)_
Check what errors does Dangerzone throw in the following scenarios. The errors
should point the user to the Qubes notifications in the top-right corner:
1. The `dz-dvm` template does not exist. We can trigger this scenario by
temporarily renaming this template.
2. The Dangerzone RPC policy does not exist. We can trigger this scenario by
temporarily renaming the `dz.Convert` policy.
3. The `dz-dvm` disposable Qube cannot start due to insufficient resources. We
can trigger this scenario by temporarily increasing the minimum required RAM
of the `dz-dvm` template to more than the available amount.
## Release ## Release
Once we are confident that the release will be out shortly, and doesn't need any more changes: Once we are confident that the release will be out shortly, and doesn't need any more changes:
- [ ] Create a PGP-signed git tag for the version, e.g., for dangerzone `v0.1.0`: - [ ] Create a PGP-signed git tag for the version, e.g., for dangerzone `v0.1.0`:
```bash ```
git tag -s v0.1.0 git tag -s v0.1.0
git push origin v0.1.0 git push origin v0.1.0
``` ```
@ -76,8 +254,6 @@ Once we are confident that the release will be out shortly, and doesn't need any
### macOS Release ### macOS Release
This needs to happen for both Silicon and Intel chipsets.
#### Initial Setup #### Initial Setup
- Build machine must have: - Build machine must have:
@ -92,83 +268,48 @@ This needs to happen for both Silicon and Intel chipsets.
#### Releasing and Signing #### Releasing and Signing
Here is what you need to do:
- [ ] Verify and install the latest supported Python version from - [ ] Verify and install the latest supported Python version from
[python.org](https://www.python.org/downloads/macos/) (do not use the one from [python.org](https://www.python.org/downloads/macos/) (do not use the one from
brew as it is known to [cause issues](https://github.com/freedomofpress/dangerzone/issues/471)) brew as it is known to [cause issues](https://github.com/freedomofpress/dangerzone/issues/471))
* In case of a new Python installation or minor version upgrade, e.g., from
- [ ] Checkout the dependencies, and clean your local copy: 3.11 to 3.12 , reinstall Poetry with `python3 -m pip install poetry`
* You can verify the correct Python version is used with `poetry debug info`
```bash - [ ] Verify and checkout the git tag for this release
- [ ] Run `poetry install --sync`
# In case of a new Python installation or minor version upgrade, e.g., from - [ ] On the silicon mac, build the container image:
# 3.11 to 3.12, reinstall Poetry
python3 -m pip install poetry
# You can verify the correct Python version is used
poetry debug info
# Replace with the actual version
export DZ_VERSION=$(cat share/version.txt)
# Verify and checkout the git tag for this release:
git checkout -f v$VERSION
# Clean the git repository
git clean -df
# Clean up the environment
poetry env remove --all
# Install the dependencies
poetry install --sync
``` ```
python3 ./install/common/build-image.py
- [ ] Build the container image and the OCR language data
```bash
poetry run ./install/common/build-image.py
poetry run ./install/common/download-tessdata.py
# Copy the container image to the assets folder
cp share/container.tar.gz ~dz/release-assets/$VERSION/dangerzone-$VERSION-arm64.tar.gz
cp share/image-id.txt ~dz/release-assets/$VERSION/.
``` ```
Then copy the `share/container.tar.gz` to the assets folder on `dangerzone-$VERSION-arm64.tar.gz`, along with the `share/image-id.txt` file.
- [ ] Run `poetry run ./install/macos/build-app.py`; this will make `dist/Dangerzone.app`
- [ ] Make sure that the built application works with the containerd graph
driver (see [#933](https://github.com/freedomofpress/dangerzone/issues/933))
- [ ] Run `poetry run ./install/macos/build-app.py --only-codesign`; this will make `dist/Dangerzone.dmg`
* You need to run this command as the account that has access to the code signing certificate
* You must run this command from the MacOS UI, from a terminal application.
- [ ] Notarize it: `xcrun notarytool submit --wait --apple-id "<email>" --keychain-profile "dz-notarytool-release-key" dist/Dangerzone.dmg`
* You need to change the `<email>` in the above command with the email
associated with the Apple Developer ID.
* 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`.
- [ ] Wait for it to get approved:
* If it gets rejected, you should be able to see why with the same command
(or use the `log` option for a more verbose JSON output)
* You will also receive an update in your email.
- [ ] After it's approved, staple the ticket: `xcrun stapler staple dist/Dangerzone.dmg`
- [ ] Build the app bundle This process ends up with the final file:
```bash ```
poetry run ./install/macos/build-app.py dist/Dangerzone.dmg
``` ```
- [ ] Sign the application bundle, and notarize it Rename `Dangerzone.dmg` to `Dangerzone-$VERSION.dmg`.
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`.
```bash
# Sign the .App and make it a .dmg
poetry run ./install/macos/build-app.py --only-codesign
# Notarize it. You must run this command from the MacOS UI
# from a terminal application.
xcrun notarytool submit ./dist/Dangerzone.dmg --apple-id $APPLE_ID --keychain-profile "dz-notarytool-release-key" --wait && xcrun stapler staple dist/Dangerzone.dmg
# Copy the .dmg to the assets folder
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
ARCH="i686"
fi
cp dist/Dangerzone.dmg ~dz/release-assets/$VERSION/Dangerzone-$VERSION-$ARCH.dmg
```
### Windows Release ### Windows Release
The Windows release is performed in a Windows 11 virtual machine (as opposed to a physical one). The Windows release is performed in a Windows 11 virtual machine as opposed to a physical one.
#### Initial Setup #### Initial Setup
@ -182,31 +323,8 @@ The Windows release is performed in a Windows 11 virtual machine (as opposed to
#### Releasing and Signing #### Releasing and Signing
- [ ] Checkout the dependencies, and clean your local copy: - [ ] Verify and checkout the git tag for this release
```bash - [ ] Run `poetry install --sync`
# In case of a new Python installation or minor version upgrade, e.g., from
# 3.11 to 3.12, reinstall Poetry
python3 -m pip install poetry
# You can verify the correct Python version is used
poetry debug info
# Replace with the actual version
export DZ_VERSION=$(cat share/version.txt)
# Verify and checkout the git tag for this release:
git checkout -f v$VERSION
# Clean the git repository
git clean -df
# Clean up the environment
poetry env remove --all
# Install the dependencies
poetry install --sync
```
- [ ] Copy the container image into the VM - [ ] Copy the container image into the VM
> [!IMPORTANT] > [!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. > 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.
@ -235,15 +353,21 @@ instructions in our build section](https://github.com/freedomofpress/dangerzone/
or create your own locally with: or create your own locally with:
```sh ```sh
# Create and run debian bookworm development environment
./dev_scripts/env.py --distro debian --version bookworm build-dev ./dev_scripts/env.py --distro debian --version bookworm build-dev
./dev_scripts/env.py --distro debian --version bookworm run --dev bash ./dev_scripts/env.py --distro debian --version bookworm run --dev bash
cd dangerzone
```
# Build the latest container Build the latest container:
./dev_scripts/env.py --distro debian --version bookworm run --dev bash -c "cd dangerzone && poetry run ./install/common/build-image.py"
# Create a .deb ```sh
./dev_scripts/env.py --distro debian --version bookworm run --dev bash -c "cd dangerzone && ./install/linux/build-deb.py" python3 ./install/common/build-image.py
```
Create a .deb:
```sh
./install/linux/build-deb.py
``` ```
Publish the .deb under `./deb_dist` to the Publish the .deb under `./deb_dist` to the
@ -262,12 +386,22 @@ or create your own locally with:
```sh ```sh
./dev_scripts/env.py --distro fedora --version 41 build-dev ./dev_scripts/env.py --distro fedora --version 41 build-dev
./dev_scripts/env.py --distro fedora --version 41 run --dev bash
cd dangerzone
```
# Build the latest container (skip if already built): Build the latest container:
./dev_scripts/env.py --distro fedora --version 41 run --dev bash -c "cd dangerzone && poetry run ./install/common/build-image.py"
# Create a .rpm: ```sh
./dev_scripts/env.py --distro fedora --version 41 run --dev bash -c "cd dangerzone && ./install/linux/build-rpm.py" python3 ./install/common/build-image.py
```
Copy the container image to the assets folder on `dangerzone-$VERSION-i686.tar.gz`.
Create a .rpm:
```sh
./install/linux/build-rpm.py
``` ```
Publish the .rpm under `./dist` to the Publish the .rpm under `./dist` to the
@ -278,7 +412,7 @@ Publish the .rpm under `./dist` to the
Create a .rpm for Qubes: Create a .rpm for Qubes:
```sh ```sh
./dev_scripts/env.py --distro fedora --version 41 run --dev bash -c "cd dangerzone && ./install/linux/build-rpm.py --qubes" ./install/linux/build-rpm.py --qubes
``` ```
and similarly publish it to the [`freedomofpress/yum-tools-prod`](https://github.com/freedomofpress/yum-tools-prod) and similarly publish it to the [`freedomofpress/yum-tools-prod`](https://github.com/freedomofpress/yum-tools-prod)
@ -286,37 +420,36 @@ repo.
## Publishing the Release ## Publishing the Release
To publish the release, you can follow these steps: To publish the release:
- [ ] Create an archive of the Dangerzone source in `tar.gz` format: - [ ] Create an archive of the Dangerzone source in `tar.gz` format:
```bash * You can use the following command:
export VERSION=$(cat share/version.txt)
git archive --format=tar.gz -o dangerzone-${VERSION:?}.tar.gz --prefix=dangerzone/ v${VERSION:?} ```
``` export DZ_VERSION=$(cat share/version.txt)
git archive --format=tar.gz -o dangerzone-${DZ_VERSION:?}.tar.gz --prefix=dangerzone/ v${DZ_VERSION:?}
```
- [ ] Run container scan on the produced container images (some time may have passed since the artifacts were built) - [ ] Run container scan on the produced container images (some time may have passed since the artifacts were built)
```bash ```
gunzip --keep -c ./share/container.tar.gz > /tmp/container.tar gunzip --keep -c ./share/container.tar.gz > /tmp/container.tar
docker pull anchore/grype:latest docker pull anchore/grype:latest
docker run --rm -v /tmp/container.tar:/container.tar anchore/grype:latest /container.tar docker run --rm -v /tmp/container.tar:/container.tar anchore/grype:latest /container.tar
``` ```
- [ ] Collect the assets in a single directory, calculate their SHA-256 hashes, and sign them. - [ ] Collect the assets in a single directory, calculate their SHA-256 hashes, and sign them.
There is an `./dev_scripts/sign-assets.py` script to automate this task. * You can use `./dev_scripts/sign-assets.py`, if you want to automate this
task.
- [ ] Create a new **draft** release on GitHub and upload the macOS and Windows installers.
* Copy the release notes text from the template at [`docs/templates/release-notes`](https://github.com/freedomofpress/dangerzone/tree/main/docs/templates/)
* You can use `./dev_scripts/upload-asset.py`, if you want to upload an asset
using an access token.
- [ ] Upload the `container-$VERSION-i686.tar.gz` and `container-$VERSION-arm64.tar.gz` images that were created in the previous step
**Important:** Before running the script, make sure that it's the same container images as **Important:** Make sure that it's the same container images as the ones that
the ones that are shipped in other platforms (see our [Pre-release](#Pre-release) section) are shipped in other platforms (see our [Pre-release](#Pre-release) section)
```bash
# Sign all the assets
./dev_scripts/sign-assets.py ~/release-assets/$VERSION/github --version $VERSION
```
- [ ] Upload all the assets to the draft release on GitHub.
```bash
find ~/release-assets/$VERSION/github | xargs -n1 ./dev_scripts/upload-asset.py --token ~/token --draft
```
- [ ] Upload the detached signatures (.asc) and checksum file.
- [ ] Update the [Dangerzone website](https://github.com/freedomofpress/dangerzone.rocks) to link to the new installers. - [ ] Update the [Dangerzone website](https://github.com/freedomofpress/dangerzone.rocks) to link to the new installers.
- [ ] Update the brew cask release of Dangerzone with a [PR like this one](https://github.com/Homebrew/homebrew-cask/pull/116319) - [ ] Update the brew cask release of Dangerzone with a [PR like this one](https://github.com/Homebrew/homebrew-cask/pull/116319)
- [ ] Update version and download links in `README.md` - [ ] Update version and download links in `README.md`

View file

@ -1,168 +0,0 @@
import gzip
import logging
import platform
import shutil
import subprocess
from typing import List, Tuple
from . import errors
from .util import get_resource_path, get_subprocess_startupinfo
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() -> 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.
"""
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:
"""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")

View file

@ -117,26 +117,3 @@ def handle_document_errors(func: F) -> F:
sys.exit(1) sys.exit(1)
return cast(F, wrapper) 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")

View file

@ -25,7 +25,13 @@ else:
from .. import errors from .. import errors
from ..document import SAFE_EXTENSION, Document from ..document import SAFE_EXTENSION, Document
from ..isolation_provider.qubes import is_qubes_native_conversion 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 ..util import format_exception, get_resource_path, get_version from ..util import format_exception, get_resource_path, get_version
from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog
from .updater import UpdateReport from .updater import UpdateReport
@ -191,11 +197,14 @@ class MainWindow(QtWidgets.QMainWindow):
header_layout.addWidget(self.hamburger_button) header_layout.addWidget(self.hamburger_button)
header_layout.addSpacing(15) header_layout.addSpacing(15)
if self.dangerzone.isolation_provider.should_wait_install(): if isinstance(self.dangerzone.isolation_provider, Container):
# Waiting widget replaces content widget while container runtime isn't available # Waiting widget replaces content widget while container runtime isn't available
self.waiting_widget: WaitingWidget = WaitingWidgetContainer(self.dangerzone) self.waiting_widget: WaitingWidget = WaitingWidgetContainer(self.dangerzone)
self.waiting_widget.finished.connect(self.waiting_finished) self.waiting_widget.finished.connect(self.waiting_finished)
else:
elif isinstance(self.dangerzone.isolation_provider, Dummy) or isinstance(
self.dangerzone.isolation_provider, Qubes
):
# Don't wait with dummy converter and on Qubes. # Don't wait with dummy converter and on Qubes.
self.waiting_widget = WaitingWidget() self.waiting_widget = WaitingWidget()
self.dangerzone.is_waiting_finished = True self.dangerzone.is_waiting_finished = True
@ -491,11 +500,12 @@ class WaitingWidgetContainer(WaitingWidget):
error: Optional[str] = None error: Optional[str] = None
try: try:
self.dangerzone.isolation_provider.is_available() assert isinstance(self.dangerzone.isolation_provider, (Dummy, Container))
except errors.NoContainerTechException as e: self.dangerzone.isolation_provider.is_runtime_available()
except NoContainerTechException as e:
log.error(str(e)) log.error(str(e))
state = "not_installed" state = "not_installed"
except errors.NotAvailableContainerTechException as e: except NotAvailableContainerTechException as e:
log.error(str(e)) log.error(str(e))
state = "not_running" state = "not_running"
error = e.error error = e.error

View file

@ -254,16 +254,6 @@ class IsolationProvider(ABC):
) )
return errors.exception_from_error_code(error_code) 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 @abstractmethod
def get_max_parallel_conversions(self) -> int: def get_max_parallel_conversions(self) -> int:
pass pass

View file

@ -1,11 +1,13 @@
import gzip
import json
import logging import logging
import os import os
import platform import platform
import shlex import shlex
import shutil
import subprocess import subprocess
from typing import List from typing import Dict, List, Tuple
from .. import container_utils, errors
from ..document import Document from ..document import Document
from ..util import get_resource_path, get_subprocess_startupinfo from ..util import get_resource_path, get_subprocess_startupinfo
from .base import IsolationProvider, terminate_process_group from .base import IsolationProvider, terminate_process_group
@ -24,8 +26,88 @@ else:
log = logging.getLogger(__name__) 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): class Container(IsolationProvider):
# Name of the dangerzone container # 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 @staticmethod
def get_runtime_security_args() -> List[str]: def get_runtime_security_args() -> List[str]:
"""Security options applicable to the outer Dangerzone container. """Security options applicable to the outer Dangerzone container.
@ -46,12 +128,12 @@ class Container(IsolationProvider):
* Do not log the container's output. * Do not log the container's output.
* Do not map the host user to the container, with `--userns nomap` (available * Do not map the host user to the container, with `--userns nomap` (available
from Podman 4.1 onwards) 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": if Container.get_runtime_name() == "podman":
security_args = ["--log-driver", "none"] security_args = ["--log-driver", "none"]
security_args += ["--security-opt", "no-new-privileges"] security_args += ["--security-opt", "no-new-privileges"]
if container_utils.get_runtime_version() >= (4, 1):
security_args += ["--userns", "nomap"]
else: else:
security_args = ["--security-opt=no-new-privileges:true"] security_args = ["--security-opt=no-new-privileges:true"]
@ -73,6 +155,110 @@ class Container(IsolationProvider):
return security_args 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 @staticmethod
def install() -> bool: def install() -> bool:
"""Install the container image tarball, or verify that it's already installed. """Install the container image tarball, or verify that it's already installed.
@ -81,46 +267,50 @@ class Container(IsolationProvider):
1. Get the tags of any locally available images that match Dangerzone's image 1. Get the tags of any locally available images that match Dangerzone's image
name. name.
2. Get the expected image tag from the image-id.txt file. 2. Get the expected image tag from the image-id.txt file.
- If this tag is present in the local images, then we can return. - 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. - Else, prune the older container images and continue.
3. Load the image tarball and make sure it matches the expected tag. 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() old_tags = Container.list_image_tags()
expected_tag = container_utils.get_expected_tag() expected_tag = Container.get_expected_tag()
if expected_tag not in old_tags: if expected_tag not in old_tags:
# Prune older container images. # Prune older container images.
log.info( log.info(
f"Could not find a Dangerzone container image with tag '{expected_tag}'" f"Could not find a Dangerzone container image with tag '{expected_tag}'"
) )
for tag in old_tags: for tag in old_tags.keys():
container_utils.delete_image_tag(tag) 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: else:
return True return True
# Load the image tarball into the container runtime. # Load the image tarball into the container runtime.
container_utils.load_image_tarball() Container.load_image_tarball()
# Check that the container image has the expected image tag. # Check that the container image has the expected image tag.
# See https://github.com/freedomofpress/dangerzone/issues/988 for an example # See https://github.com/freedomofpress/dangerzone/issues/988 for an example
# where this was not the case. # where this was not the case.
new_tags = container_utils.list_image_tags() new_tags = Container.list_image_tags()
if expected_tag not in new_tags: if expected_tag not in new_tags:
raise errors.ImageNotPresentException( raise ImageNotPresentException(
f"Could not find expected tag '{expected_tag}' after loading the" f"Could not find expected tag '{expected_tag}' after loading the"
" container image tarball" " container image tarball"
) )
# Mark the expected tag as "latest".
Container.add_image_tag(expected_tag, "latest")
return True return True
@staticmethod @staticmethod
def should_wait_install() -> bool: def is_runtime_available() -> bool:
return True container_runtime = Container.get_runtime()
runtime_name = Container.get_runtime_name()
@staticmethod
def is_available() -> bool:
container_runtime = container_utils.get_runtime()
runtime_name = container_utils.get_runtime_name()
# Can we run `docker/podman image ls` without an error # Can we run `docker/podman image ls` without an error
with subprocess.Popen( with subprocess.Popen(
[container_runtime, "image", "ls"], [container_runtime, "image", "ls"],
@ -130,9 +320,7 @@ class Container(IsolationProvider):
) as p: ) as p:
_, stderr = p.communicate() _, stderr = p.communicate()
if p.returncode != 0: if p.returncode != 0:
raise errors.NotAvailableContainerTechException( raise NotAvailableContainerTechException(runtime_name, stderr.decode())
runtime_name, stderr.decode()
)
return True return True
def doc_to_pixels_container_name(self, document: Document) -> str: def doc_to_pixels_container_name(self, document: Document) -> str:
@ -165,22 +353,21 @@ class Container(IsolationProvider):
self, self,
command: List[str], command: List[str],
name: str, name: str,
extra_args: List[str] = [],
) -> subprocess.Popen: ) -> subprocess.Popen:
container_runtime = container_utils.get_runtime() container_runtime = self.get_runtime()
security_args = self.get_runtime_security_args() security_args = self.get_runtime_security_args()
enable_stdin = ["-i"] enable_stdin = ["-i"]
set_name = ["--name", name] set_name = ["--name", name]
prevent_leakage_args = ["--rm"] prevent_leakage_args = ["--rm"]
image_name = [
container_utils.CONTAINER_NAME + ":" + container_utils.get_expected_tag()
]
args = ( args = (
["run"] ["run"]
+ security_args + security_args
+ prevent_leakage_args + prevent_leakage_args
+ enable_stdin + enable_stdin
+ set_name + set_name
+ image_name + extra_args
+ [self.CONTAINER_NAME]
+ command + command
) )
args = [container_runtime] + args args = [container_runtime] + args
@ -196,7 +383,7 @@ class Container(IsolationProvider):
connected to the Docker daemon, and killing it will just close the associated connected to the Docker daemon, and killing it will just close the associated
standard streams. standard streams.
""" """
container_runtime = container_utils.get_runtime() container_runtime = self.get_runtime()
cmd = [container_runtime, "kill", name] cmd = [container_runtime, "kill", name]
try: try:
# We do not check the exit code of the process here, since the container may # We do not check the exit code of the process here, since the container may
@ -229,8 +416,15 @@ class Container(IsolationProvider):
"-m", "-m",
"dangerzone.conversion.doc_to_pixels", "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.get_runtime_name() == "podman":
if Container.get_runtime_version() >= (4, 1):
extra_args += ["--userns", "nomap"]
name = self.doc_to_pixels_container_name(document) name = self.doc_to_pixels_container_name(document)
return self.exec_container(command, name=name) return self.exec_container(command, name=name, extra_args=extra_args)
def terminate_doc_to_pixels_proc( def terminate_doc_to_pixels_proc(
self, document: Document, p: subprocess.Popen self, document: Document, p: subprocess.Popen
@ -253,7 +447,7 @@ class Container(IsolationProvider):
# after a podman kill / docker kill invocation, this will likely be the case, # 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 # else the container runtime (Docker/Podman) has experienced a problem, and we
# should report it. # should report it.
container_runtime = container_utils.get_runtime() container_runtime = self.get_runtime()
name = self.doc_to_pixels_container_name(document) name = self.doc_to_pixels_container_name(document)
all_containers = subprocess.run( all_containers = subprocess.run(
[container_runtime, "ps", "-a"], [container_runtime, "ps", "-a"],
@ -275,11 +469,11 @@ class Container(IsolationProvider):
if cpu_count is not None: if cpu_count is not None:
n_cpu = cpu_count n_cpu = cpu_count
elif container_utils.get_runtime_name() == "docker": elif self.get_runtime_name() == "docker":
# For Windows and MacOS containers run in VM # For Windows and MacOS containers run in VM
# So we obtain the CPU count for the VM # So we obtain the CPU count for the VM
n_cpu_str = subprocess.check_output( n_cpu_str = subprocess.check_output(
[container_utils.get_runtime(), "info", "--format", "{{.NCPU}}"], [self.get_runtime(), "info", "--format", "{{.NCPU}}"],
text=True, text=True,
startupinfo=get_subprocess_startupinfo(), startupinfo=get_subprocess_startupinfo(),
) )

View file

@ -40,13 +40,9 @@ class Dummy(IsolationProvider):
return True return True
@staticmethod @staticmethod
def is_available() -> bool: def is_runtime_available() -> bool:
return True return True
@staticmethod
def should_wait_install() -> bool:
return False
def start_doc_to_pixels_proc(self, document: Document) -> subprocess.Popen: def start_doc_to_pixels_proc(self, document: Document) -> subprocess.Popen:
cmd = [ cmd = [
sys.executable, sys.executable,

View file

@ -21,14 +21,6 @@ class Qubes(IsolationProvider):
def install(self) -> bool: def install(self) -> bool:
return True return True
@staticmethod
def is_available() -> bool:
return True
@staticmethod
def should_wait_install() -> bool:
return False
def get_max_parallel_conversions(self) -> int: def get_max_parallel_conversions(self) -> int:
return 1 return 1

View file

@ -1,67 +0,0 @@
#!/usr/bin/env python3
import pathlib
import subprocess
RELEASE_FILE = "RELEASE.md"
QA_FILE = "QA.md"
def git_root():
"""Get the root directory of the Git repo."""
# FIXME: Use a Git Python binding for this.
# FIXME: Make this work if called outside the repo.
path = (
subprocess.run(
["git", "rev-parse", "--show-toplevel"],
check=True,
stdout=subprocess.PIPE,
)
.stdout.decode()
.strip("\n")
)
return pathlib.Path(path)
def extract_checkboxes(filename):
headers = []
result = []
with open(filename, "r") as f:
lines = f.readlines()
current_level = 0
for line in lines:
line = line.rstrip()
# If it's a header, store it
if line.startswith("#"):
# Count number of # to determine header level
level = len(line) - len(line.lstrip("#"))
if level < current_level or not current_level:
headers.extend(["", line, ""])
current_level = level
elif level > current_level:
continue
else:
headers = ["", line, ""]
# If it's a checkbox
elif "- [ ]" in line or "- [x]" in line or "- [X]" in line:
# Print the last header if we haven't already
if headers:
result.extend(headers)
headers = []
current_level = 0
# If this is the "Do the QA tasks" line, recursively get QA tasks
if "Do the QA tasks" in line:
result.append(line)
qa_tasks = extract_checkboxes(git_root() / QA_FILE)
result.append(qa_tasks)
else:
result.append(line)
return "\n".join(result)
if __name__ == "__main__":
print(extract_checkboxes(git_root() / RELEASE_FILE))

View file

@ -20,32 +20,17 @@ EOL_PYTHON_URL = "https://endoflife.date/api/python.json"
CONTENT_QA = r"""## QA CONTENT_QA = r"""## QA
To ensure that new releases do not introduce regressions, and support existing To ensure that new releases do not introduce regressions, and support existing
and newer platforms, we have to test that the produced packages work as expected. and newer platforms, we have to do the following:
Check the following:
- [ ] Make sure that the tip of the `main` branch passes the CI tests. - [ ] Make sure that the tip of the `main` branch passes the CI tests.
- [ ] Make sure that the Apple account has a valid application password and has - [ ] Make sure that the Apple account has a valid application password and has
agreed to the latest Apple terms (see [macOS release](#macos-release) agreed to the latest Apple terms (see [macOS release](#macos-release)
section). section).
Because it is repetitive, we wrote a script to help with the QA.
It can run the tasks for you, pausing when it needs manual intervention.
You can run it with a command like:
```bash
poetry run ./dev_scripts/qa.py {distro}-{version}
```
### The checklist
- [ ] Create a test build in Windows and make sure it works: - [ ] Create a test build in Windows and make sure it works:
- [ ] Check if the suggested Python version is still supported. - [ ] Check if the suggested Python version is still supported.
- [ ] Create a new development environment with Poetry. - [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses - [ ] Build the container image and ensure the development environment uses
the new image. the new image.
- [ ] Download the OCR language data using `./install/common/download-tessdata.py`
- [ ] Run the Dangerzone tests. - [ ] Run the Dangerzone tests.
- [ ] Build and run the Dangerzone .exe - [ ] Build and run the Dangerzone .exe
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below). - [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
@ -54,7 +39,6 @@ poetry run ./dev_scripts/qa.py {distro}-{version}
- [ ] Create a new development environment with Poetry. - [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses - [ ] Build the container image and ensure the development environment uses
the new image. the new image.
- [ ] Download the OCR language data using `./install/common/download-tessdata.py`
- [ ] Run the Dangerzone tests. - [ ] Run the Dangerzone tests.
- [ ] Create and run an app bundle. - [ ] Create and run an app bundle.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below). - [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
@ -63,7 +47,6 @@ poetry run ./dev_scripts/qa.py {distro}-{version}
- [ ] Create a new development environment with Poetry. - [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses - [ ] Build the container image and ensure the development environment uses
the new image. the new image.
- [ ] Download the OCR language data using `./install/common/download-tessdata.py`
- [ ] Run the Dangerzone tests. - [ ] Run the Dangerzone tests.
- [ ] Create and run an app bundle. - [ ] Create and run an app bundle.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below). - [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
@ -72,7 +55,6 @@ poetry run ./dev_scripts/qa.py {distro}-{version}
- [ ] Create a new development environment with Poetry. - [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses - [ ] Build the container image and ensure the development environment uses
the new image. the new image.
- [ ] Download the OCR language data using `./install/common/download-tessdata.py`
- [ ] Run the Dangerzone tests. - [ ] Run the Dangerzone tests.
- [ ] Create a .deb package and install it system-wide. - [ ] Create a .deb package and install it system-wide.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below). - [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
@ -81,7 +63,6 @@ poetry run ./dev_scripts/qa.py {distro}-{version}
- [ ] Create a new development environment with Poetry. - [ ] Create a new development environment with Poetry.
- [ ] Build the container image and ensure the development environment uses - [ ] Build the container image and ensure the development environment uses
the new image. the new image.
- [ ] Download the OCR language data using `./install/common/download-tessdata.py`
- [ ] Run the Dangerzone tests. - [ ] Run the Dangerzone tests.
- [ ] Create an .rpm package and install it system-wide. - [ ] Create an .rpm package and install it system-wide.
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below). - [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
@ -129,6 +110,7 @@ version. For example:
``` ```
$ docker images dangerzone.rocks/dangerzone $ docker images dangerzone.rocks/dangerzone
REPOSITORY TAG IMAGE ID CREATED SIZE REPOSITORY TAG IMAGE ID CREATED SIZE
dangerzone.rocks/dangerzone latest <image ID> <date> <size>
dangerzone.rocks/dangerzone <tag> <image ID> <date> <size> dangerzone.rocks/dangerzone <tag> <image ID> <date> <size>
``` ```
@ -140,6 +122,7 @@ and seeing the following differences:
``` ```
$ docker images dangerzone.rocks/dangerzone $ docker images dangerzone.rocks/dangerzone
REPOSITORY TAG IMAGE ID CREATED SIZE REPOSITORY TAG IMAGE ID CREATED SIZE
dangerzone.rocks/dangerzone latest <different ID> <newer date> <different size>
dangerzone.rocks/dangerzone <other tag> <different ID> <newer date> <different size> dangerzone.rocks/dangerzone <other tag> <different ID> <newer date> <different size>
``` ```
@ -572,7 +555,7 @@ class Reference:
# Convert spaces to dashes # Convert spaces to dashes
anchor = anchor.replace(" ", "-") anchor = anchor.replace(" ", "-")
# Remove non-alphanumeric (except dash and underscore) # Remove non-alphanumeric (except dash and underscore)
anchor = re.sub("[^a-zA-Z-_]", "", anchor) anchor = re.sub("[^a-zA-Z\-_]", "", anchor)
return anchor return anchor
@ -591,8 +574,8 @@ class QABase(abc.ABC):
platforms = {} platforms = {}
REF_QA = Reference("QA.md", content=CONTENT_QA) REF_QA = Reference("RELEASE.md", content=CONTENT_QA)
REF_QA_SCENARIOS = Reference("QA.md", content=CONTENT_QA_SCENARIOS) REF_QA_SCENARIOS = Reference("RELEASE.md", content=CONTENT_QA_SCENARIOS)
# The following class method is available since Python 3.6. For more details, see: # The following class method is available since Python 3.6. For more details, see:
# https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-simpler-customization-of-class-creation # https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-simpler-customization-of-class-creation
@ -1070,10 +1053,6 @@ class QAFedora(QALinux):
) )
class QAFedora41(QAFedora):
VERSION = "41"
class QAFedora40(QAFedora): class QAFedora40(QAFedora):
VERSION = "40" VERSION = "40"

View file

@ -59,7 +59,7 @@ def main():
dirty_ident = secrets.token_hex(2) dirty_ident = secrets.token_hex(2)
tag = ( tag = (
subprocess.check_output( subprocess.check_output(
["git", "describe", "--long", "--first-parent", f"--dirty=-{dirty_ident}"], ["git", "describe", "--first-parent", f"--dirty=-{dirty_ident}"],
) )
.decode() .decode()
.strip()[1:] # remove the "v" prefix of the tag. .strip()[1:] # remove the "v" prefix of the tag.
@ -83,9 +83,11 @@ def main():
check=True, check=True,
) )
# Build the container image, and tag it with the calculated tag # Build the container image, and tag it with two tags; the one we calculated
# above, and the "latest" tag.
print("Building container image") print("Building container image")
cache_args = [] if args.use_cache else ["--no-cache"] cache_args = [] if args.use_cache else ["--no-cache"]
image_name_latest = IMAGE_NAME + ":latest"
subprocess.run( subprocess.run(
[ [
args.runtime, args.runtime,
@ -99,6 +101,8 @@ def main():
"-f", "-f",
"Dockerfile", "Dockerfile",
"--tag", "--tag",
image_name_latest,
"--tag",
image_name_tagged, image_name_tagged,
], ],
check=True, check=True,

View file

@ -10,7 +10,6 @@ from pytest_mock import MockerFixture
from pytest_subprocess import FakeProcess from pytest_subprocess import FakeProcess
from pytestqt.qtbot import QtBot from pytestqt.qtbot import QtBot
from dangerzone import errors
from dangerzone.document import Document from dangerzone.document import Document
from dangerzone.gui import MainWindow from dangerzone.gui import MainWindow
from dangerzone.gui import main_window as main_window_module from dangerzone.gui import main_window as main_window_module
@ -26,7 +25,11 @@ from dangerzone.gui.main_window import (
WaitingWidgetContainer, WaitingWidgetContainer,
) )
from dangerzone.gui.updater import UpdateReport, UpdaterThread from dangerzone.gui.updater import UpdateReport, UpdaterThread
from dangerzone.isolation_provider.container import Container from dangerzone.isolation_provider.container import (
Container,
NoContainerTechException,
NotAvailableContainerTechException,
)
from dangerzone.isolation_provider.dummy import Dummy from dangerzone.isolation_provider.dummy import Dummy
from .test_updater import assert_report_equal, default_updater_settings from .test_updater import assert_report_equal, default_updater_settings
@ -509,8 +512,8 @@ def test_not_available_container_tech_exception(
# Setup # Setup
mock_app = mocker.MagicMock() mock_app = mocker.MagicMock()
dummy = Dummy() dummy = Dummy()
fn = mocker.patch.object(dummy, "is_available") fn = mocker.patch.object(dummy, "is_runtime_available")
fn.side_effect = errors.NotAvailableContainerTechException( fn.side_effect = NotAvailableContainerTechException(
"podman", "podman image ls logs" "podman", "podman image ls logs"
) )
@ -533,7 +536,7 @@ def test_no_container_tech_exception(qtbot: QtBot, mocker: MockerFixture) -> Non
dummy = mocker.MagicMock() dummy = mocker.MagicMock()
# Raise # Raise
dummy.is_available.side_effect = errors.NoContainerTechException("podman") dummy.is_runtime_available.side_effect = NoContainerTechException("podman")
dz = DangerzoneGui(mock_app, dummy) dz = DangerzoneGui(mock_app, dummy)
widget = WaitingWidgetContainer(dz) widget = WaitingWidgetContainer(dz)

View file

@ -4,8 +4,12 @@ import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from pytest_subprocess import FakeProcess from pytest_subprocess import FakeProcess
from dangerzone import container_utils, errors from dangerzone.isolation_provider.container import (
from dangerzone.isolation_provider.container import Container Container,
ImageInstallationException,
ImageNotPresentException,
NotAvailableContainerTechException,
)
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
from .base import IsolationProviderTermination, IsolationProviderTest from .base import IsolationProviderTermination, IsolationProviderTest
@ -23,27 +27,31 @@ def provider() -> Container:
class TestContainer(IsolationProviderTest): class TestContainer(IsolationProviderTest):
def test_is_available_raises(self, provider: Container, fp: FakeProcess) -> None: def test_is_runtime_available_raises(
self, provider: Container, fp: FakeProcess
) -> None:
""" """
NotAvailableContainerTechException should be raised when NotAvailableContainerTechException should be raised when
the "podman image ls" command fails. the "podman image ls" command fails.
""" """
fp.register_subprocess( fp.register_subprocess(
[container_utils.get_runtime(), "image", "ls"], [provider.get_runtime(), "image", "ls"],
returncode=-1, returncode=-1,
stderr="podman image ls logs", stderr="podman image ls logs",
) )
with pytest.raises(errors.NotAvailableContainerTechException): with pytest.raises(NotAvailableContainerTechException):
provider.is_available() provider.is_runtime_available()
def test_is_available_works(self, provider: Container, fp: FakeProcess) -> None: def test_is_runtime_available_works(
self, provider: Container, fp: FakeProcess
) -> None:
""" """
No exception should be raised when the "podman image ls" can return properly. No exception should be raised when the "podman image ls" can return properly.
""" """
fp.register_subprocess( fp.register_subprocess(
[container_utils.get_runtime(), "image", "ls"], [provider.get_runtime(), "image", "ls"],
) )
provider.is_available() provider.is_runtime_available()
def test_install_raise_if_image_cant_be_installed( def test_install_raise_if_image_cant_be_installed(
self, mocker: MockerFixture, provider: Container, fp: FakeProcess self, mocker: MockerFixture, provider: Container, fp: FakeProcess
@ -51,31 +59,32 @@ class TestContainer(IsolationProviderTest):
"""When an image installation fails, an exception should be raised""" """When an image installation fails, an exception should be raised"""
fp.register_subprocess( fp.register_subprocess(
[container_utils.get_runtime(), "image", "ls"], [provider.get_runtime(), "image", "ls"],
) )
# First check should return nothing. # First check should return nothing.
fp.register_subprocess( fp.register_subprocess(
[ [
container_utils.get_runtime(), provider.get_runtime(),
"image", "image",
"list", "list",
"--format", "--format",
"{{ .Tag }}", "json",
"dangerzone.rocks/dangerzone", "dangerzone.rocks/dangerzone",
], ],
occurrences=2, occurrences=2,
stdout="{}",
) )
# Make podman load fail # Make podman load fail
mocker.patch("gzip.open", mocker.mock_open(read_data="")) mocker.patch("gzip.open", mocker.mock_open(read_data=""))
fp.register_subprocess( fp.register_subprocess(
[container_utils.get_runtime(), "load"], [provider.get_runtime(), "load"],
returncode=-1, returncode=-1,
) )
with pytest.raises(errors.ImageInstallationException): with pytest.raises(ImageInstallationException):
provider.install() provider.install()
def test_install_raises_if_still_not_installed( def test_install_raises_if_still_not_installed(
@ -84,28 +93,29 @@ class TestContainer(IsolationProviderTest):
"""When an image keep being not installed, it should return False""" """When an image keep being not installed, it should return False"""
fp.register_subprocess( fp.register_subprocess(
[container_utils.get_runtime(), "image", "ls"], [provider.get_runtime(), "image", "ls"],
) )
# First check should return nothing. # First check should return nothing.
fp.register_subprocess( fp.register_subprocess(
[ [
container_utils.get_runtime(), provider.get_runtime(),
"image", "image",
"list", "list",
"--format", "--format",
"{{ .Tag }}", "json",
"dangerzone.rocks/dangerzone", "dangerzone.rocks/dangerzone",
], ],
occurrences=2, occurrences=2,
stdout="{}",
) )
# Patch gzip.open and podman load so that it works # Patch gzip.open and podman load so that it works
mocker.patch("gzip.open", mocker.mock_open(read_data="")) mocker.patch("gzip.open", mocker.mock_open(read_data=""))
fp.register_subprocess( fp.register_subprocess(
[container_utils.get_runtime(), "load"], [provider.get_runtime(), "load"],
) )
with pytest.raises(errors.ImageNotPresentException): with pytest.raises(ImageNotPresentException):
provider.install() provider.install()