mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
Display the {podman,docker} pull
progress when installing a new image
The progressbars we see when using this same commands on the command line doesn't seem to be passed to the python process here, unfortunately.
This commit is contained in:
parent
bdceee53d0
commit
8d7e965553
5 changed files with 84 additions and 31 deletions
|
@ -4,14 +4,16 @@ import platform
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import IO, Callable, List, Optional, Tuple
|
||||
|
||||
from . import errors
|
||||
from .settings import Settings
|
||||
from .util import get_resource_path, get_subprocess_startupinfo
|
||||
|
||||
OLD_CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
||||
CONTAINER_NAME = "ghcr.io/freedomofpress/dangerzone/dangerzone"
|
||||
CONTAINER_NAME = (
|
||||
"ghcr.io/almet/dangerzone/dangerzone"
|
||||
) # FIXME: Change this to the correct container name
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -228,16 +230,27 @@ def get_image_id_by_digest(digest: str) -> str:
|
|||
return process.stdout.decode().strip().split("\n")[0]
|
||||
|
||||
|
||||
def container_pull(image: str, manifest_digest: str):
|
||||
def container_pull(image: str, manifest_digest: str, callback: Callable):
|
||||
"""Pull a container image from a registry."""
|
||||
runtime = Runtime()
|
||||
cmd = [str(runtime.path), "pull", f"{image}@sha256:{manifest_digest}"]
|
||||
try:
|
||||
subprocess_run(cmd, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
for line in process.stdout: # type: ignore
|
||||
callback(line)
|
||||
|
||||
process.wait()
|
||||
if process.returncode != 0:
|
||||
raise errors.ContainerPullException(
|
||||
f"Could not pull the container image: {e}"
|
||||
) from e
|
||||
f"Could not pull the container image: {process.returncode}"
|
||||
)
|
||||
|
||||
|
||||
def get_local_image_digest(image: str) -> str:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import io
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
|
@ -5,22 +6,24 @@ import tempfile
|
|||
import typing
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
|
||||
if typing.TYPE_CHECKING:
|
||||
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QTextCursor
|
||||
from PySide2.QtWidgets import QAction, QTextEdit
|
||||
else:
|
||||
try:
|
||||
from PySide6 import QtCore, QtGui, QtSvg, QtWidgets
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QAction
|
||||
from PySide6.QtGui import QAction, QTextCursor
|
||||
from PySide6.QtWidgets import QTextEdit
|
||||
except ImportError:
|
||||
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QTextCursor
|
||||
from PySide2.QtWidgets import QAction, QTextEdit
|
||||
|
||||
from .. import errors
|
||||
|
@ -436,15 +439,21 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
class InstallContainerThread(QtCore.QThread):
|
||||
finished = QtCore.Signal(str)
|
||||
process_stdout = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, dangerzone: DangerzoneGui) -> None:
|
||||
def __init__(
|
||||
self, dangerzone: DangerzoneGui, callback: Optional[Callable] = None
|
||||
) -> None:
|
||||
super(InstallContainerThread, self).__init__()
|
||||
self.dangerzone = dangerzone
|
||||
|
||||
def run(self) -> None:
|
||||
error = None
|
||||
try:
|
||||
installed = self.dangerzone.isolation_provider.install()
|
||||
should_upgrade = self.dangerzone.settings.get("updater_check_all")
|
||||
installed = self.dangerzone.isolation_provider.install(
|
||||
should_upgrade=bool(should_upgrade), callback=self.process_stdout.emit
|
||||
)
|
||||
except Exception as e:
|
||||
log.error("Container installation problem")
|
||||
error = format_exception(e)
|
||||
|
@ -479,11 +488,20 @@ class TracebackWidget(QTextEdit):
|
|||
# Enable copying
|
||||
self.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||
|
||||
self.current_output = ""
|
||||
|
||||
def set_content(self, error: Optional[str] = None) -> None:
|
||||
if error:
|
||||
self.setPlainText(error)
|
||||
self.setVisible(True)
|
||||
|
||||
def process_output(self, line):
|
||||
self.current_output += line
|
||||
self.setText(self.current_output)
|
||||
cursor = self.textCursor()
|
||||
cursor.movePosition(QTextCursor.MoveOperation.End)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
|
||||
class WaitingWidgetContainer(WaitingWidget):
|
||||
# These are the possible states that the WaitingWidget can show.
|
||||
|
@ -623,8 +641,14 @@ class WaitingWidgetContainer(WaitingWidget):
|
|||
"Installing the Dangerzone container image.<br><br>"
|
||||
"This might take a few minutes..."
|
||||
)
|
||||
self.traceback.setVisible(True)
|
||||
|
||||
self.install_container_t = InstallContainerThread(self.dangerzone)
|
||||
self.install_container_t.finished.connect(self.installation_finished)
|
||||
|
||||
self.install_container_t.process_stdout.connect(
|
||||
self.traceback.process_output
|
||||
)
|
||||
self.install_container_t.start()
|
||||
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ class IsolationProvider(ABC):
|
|||
return self.debug or getattr(sys, "dangerzone_dev", False)
|
||||
|
||||
@abstractmethod
|
||||
def install(self) -> bool:
|
||||
def install(self, should_upgrade: bool, callback: Callable) -> bool:
|
||||
pass
|
||||
|
||||
def convert(
|
||||
|
|
|
@ -3,7 +3,7 @@ import os
|
|||
import platform
|
||||
import shlex
|
||||
import subprocess
|
||||
from typing import List, Tuple
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
from .. import container_utils, errors, updater
|
||||
from ..container_utils import Runtime
|
||||
|
@ -94,27 +94,38 @@ class Container(IsolationProvider):
|
|||
return security_args
|
||||
|
||||
@staticmethod
|
||||
def install() -> bool:
|
||||
def install(
|
||||
should_upgrade: bool, callback: Callable, last_try: bool = False
|
||||
) -> bool:
|
||||
"""Check if an update is available and install it if necessary."""
|
||||
# XXX Do this only if users have opted in to auto-updates
|
||||
if False: # Comment this for now, just as an exemple of this can be implemented
|
||||
# # Load the image tarball into the container runtime.
|
||||
if not should_upgrade:
|
||||
log.debug("Skipping container upgrade check as requested by the settings")
|
||||
else:
|
||||
update_available, image_digest = updater.is_update_available(
|
||||
container_utils.CONTAINER_NAME
|
||||
container_utils.CONTAINER_NAME,
|
||||
updater.DEFAULT_PUBKEY_LOCATION,
|
||||
)
|
||||
if update_available and image_digest:
|
||||
log.debug("Upgrading container image to %s", image_digest)
|
||||
updater.upgrade_container_image(
|
||||
container_utils.CONTAINER_NAME,
|
||||
image_digest,
|
||||
updater.DEFAULT_PUBKEY_LOCATION,
|
||||
callback=callback,
|
||||
)
|
||||
for tag in old_tags:
|
||||
tag = container_utils.CONTAINER_NAME + ":" + tag
|
||||
container_utils.delete_image_tag(tag)
|
||||
|
||||
updater.verify_local_image(
|
||||
container_utils.CONTAINER_NAME, updater.DEFAULT_PUBKEY_LOCATION
|
||||
)
|
||||
else:
|
||||
log.debug("No update available for the container")
|
||||
try:
|
||||
updater.verify_local_image(
|
||||
container_utils.CONTAINER_NAME, updater.DEFAULT_PUBKEY_LOCATION
|
||||
)
|
||||
except errors.ImageNotPresentException:
|
||||
if last_try:
|
||||
raise
|
||||
log.debug("Container image not found, trying to install it.")
|
||||
return Container.install(
|
||||
should_upgrade=should_upgrade, callback=callback, last_try=True
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from hashlib import sha256
|
|||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from .. import container_utils as runtime
|
||||
from .. import errors as dzerrors
|
||||
|
@ -370,7 +370,9 @@ def load_and_verify_signatures(
|
|||
return signatures
|
||||
|
||||
|
||||
def store_signatures(signatures: list[Dict], image_digest: str, pubkey: str) -> None:
|
||||
def store_signatures(
|
||||
signatures: list[Dict], image_digest: str, pubkey: str, update_logindex: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Store signatures locally in the SIGNATURE_PATH folder, like this:
|
||||
|
||||
|
@ -415,7 +417,8 @@ def store_signatures(signatures: list[Dict], image_digest: str, pubkey: str) ->
|
|||
)
|
||||
json.dump(signatures, f)
|
||||
|
||||
write_log_index(get_log_index_from_signatures(signatures))
|
||||
if update_logindex:
|
||||
write_log_index(get_log_index_from_signatures(signatures))
|
||||
|
||||
|
||||
def verify_local_image(image: str, pubkey: str) -> bool:
|
||||
|
@ -479,14 +482,16 @@ def prepare_airgapped_archive(image_name: str, destination: str) -> None:
|
|||
archive.add(tmpdir, arcname=".")
|
||||
|
||||
|
||||
def upgrade_container_image(image: str, manifest_digest: str, pubkey: str) -> str:
|
||||
def upgrade_container_image(
|
||||
image: str, manifest_digest: str, pubkey: str, callback: Callable
|
||||
) -> str:
|
||||
"""Verify and upgrade the image to the latest, if signed."""
|
||||
update_available, remote_digest = registry.is_new_remote_image_available(image)
|
||||
if not update_available:
|
||||
raise errors.ImageAlreadyUpToDate("The image is already up to date")
|
||||
|
||||
signatures = check_signatures_and_logindex(image, remote_digest, pubkey)
|
||||
runtime.container_pull(image, manifest_digest)
|
||||
runtime.container_pull(image, manifest_digest, callback=callback)
|
||||
|
||||
# Store the signatures just now to avoid storing them unverified
|
||||
store_signatures(signatures, manifest_digest, pubkey)
|
||||
|
|
Loading…
Reference in a new issue