Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

container: Revamp container image installation #1021

Merged
merged 12 commits into from
Dec 10, 2024
9 changes: 6 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
almet marked this conversation as resolved.
Show resolved Hide resolved

- name: Get current date
id: date
Expand All @@ -83,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
Expand All @@ -95,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
dangerzone.rocks/dangerzone:$tag \
${{ env.IMAGE_REGISTRY }}/dangerzone/dangerzone:tag
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -57,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
Expand Down Expand Up @@ -225,7 +227,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
Expand All @@ -249,7 +251,7 @@ jobs:
install-deb:
name: "install-deb (${{ matrix.distro }} ${{ matrix.version }})"
runs-on: ubuntu-latest
needs:
needs:
- build-deb
strategy:
matrix:
Expand Down Expand Up @@ -332,7 +334,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
Expand Down Expand Up @@ -427,7 +429,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
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,24 @@ 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
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
Expand All @@ -38,7 +45,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
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/scan_released.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@ 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
only-fixed: false
severity-cutoff: critical
Expand All @@ -44,7 +49,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
Expand Down
8 changes: 4 additions & 4 deletions QA.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ 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 <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.
Expand All @@ -118,9 +118,9 @@ 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 <different ID> <newer date> <different size>
dangerzone.rocks/dangerzone <other tag> <different ID> <newer date> <different size>
```

#### 4. Dangerzone successfully installs the container image
Expand Down
13 changes: 4 additions & 9 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -142,12 +142,10 @@ 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

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`.
Expand Down Expand Up @@ -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`

Expand Down Expand Up @@ -269,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"
Expand Down
149 changes: 149 additions & 0 deletions dangerzone/container_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import gzip
apyrgio marked this conversation as resolved.
Show resolved Hide resolved
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 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")
23 changes: 23 additions & 0 deletions dangerzone/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,26 @@ def wrapper(*args, **kwargs): # type: ignore
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")
Loading
Loading