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 shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Tuple
|
from typing import IO, Callable, List, Optional, Tuple
|
||||||
|
|
||||||
from . import errors
|
from . import errors
|
||||||
from .settings import Settings
|
from .settings import Settings
|
||||||
from .util import get_resource_path, get_subprocess_startupinfo
|
from .util import get_resource_path, get_subprocess_startupinfo
|
||||||
|
|
||||||
OLD_CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
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__)
|
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]
|
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."""
|
"""Pull a container image from a registry."""
|
||||||
runtime = Runtime()
|
runtime = Runtime()
|
||||||
cmd = [str(runtime.path), "pull", f"{image}@sha256:{manifest_digest}"]
|
cmd = [str(runtime.path), "pull", f"{image}@sha256:{manifest_digest}"]
|
||||||
try:
|
process = subprocess.Popen(
|
||||||
subprocess_run(cmd, check=True)
|
cmd,
|
||||||
except subprocess.CalledProcessError as e:
|
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(
|
raise errors.ContainerPullException(
|
||||||
f"Could not pull the container image: {e}"
|
f"Could not pull the container image: {process.returncode}"
|
||||||
) from e
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_local_image_digest(image: str) -> str:
|
def get_local_image_digest(image: str) -> str:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
@ -5,22 +6,24 @@ import tempfile
|
||||||
import typing
|
import typing
|
||||||
from multiprocessing.pool import ThreadPool
|
from multiprocessing.pool import ThreadPool
|
||||||
from pathlib import Path
|
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.
|
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
|
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
|
||||||
from PySide2.QtCore import Qt
|
from PySide2.QtCore import Qt
|
||||||
|
from PySide2.QtGui import QTextCursor
|
||||||
from PySide2.QtWidgets import QAction, QTextEdit
|
from PySide2.QtWidgets import QAction, QTextEdit
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
from PySide6 import QtCore, QtGui, QtSvg, QtWidgets
|
from PySide6 import QtCore, QtGui, QtSvg, QtWidgets
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtGui import QAction
|
from PySide6.QtGui import QAction, QTextCursor
|
||||||
from PySide6.QtWidgets import QTextEdit
|
from PySide6.QtWidgets import QTextEdit
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
|
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
|
||||||
from PySide2.QtCore import Qt
|
from PySide2.QtCore import Qt
|
||||||
|
from PySide2.QtGui import QTextCursor
|
||||||
from PySide2.QtWidgets import QAction, QTextEdit
|
from PySide2.QtWidgets import QAction, QTextEdit
|
||||||
|
|
||||||
from .. import errors
|
from .. import errors
|
||||||
|
@ -436,15 +439,21 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
|
||||||
class InstallContainerThread(QtCore.QThread):
|
class InstallContainerThread(QtCore.QThread):
|
||||||
finished = QtCore.Signal(str)
|
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__()
|
super(InstallContainerThread, self).__init__()
|
||||||
self.dangerzone = dangerzone
|
self.dangerzone = dangerzone
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
error = None
|
error = None
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
log.error("Container installation problem")
|
log.error("Container installation problem")
|
||||||
error = format_exception(e)
|
error = format_exception(e)
|
||||||
|
@ -479,11 +488,20 @@ class TracebackWidget(QTextEdit):
|
||||||
# Enable copying
|
# Enable copying
|
||||||
self.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
self.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||||
|
|
||||||
|
self.current_output = ""
|
||||||
|
|
||||||
def set_content(self, error: Optional[str] = None) -> None:
|
def set_content(self, error: Optional[str] = None) -> None:
|
||||||
if error:
|
if error:
|
||||||
self.setPlainText(error)
|
self.setPlainText(error)
|
||||||
self.setVisible(True)
|
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):
|
class WaitingWidgetContainer(WaitingWidget):
|
||||||
# These are the possible states that the WaitingWidget can show.
|
# These are the possible states that the WaitingWidget can show.
|
||||||
|
@ -623,8 +641,14 @@ class WaitingWidgetContainer(WaitingWidget):
|
||||||
"Installing the Dangerzone container image.<br><br>"
|
"Installing the Dangerzone container image.<br><br>"
|
||||||
"This might take a few minutes..."
|
"This might take a few minutes..."
|
||||||
)
|
)
|
||||||
|
self.traceback.setVisible(True)
|
||||||
|
|
||||||
self.install_container_t = InstallContainerThread(self.dangerzone)
|
self.install_container_t = InstallContainerThread(self.dangerzone)
|
||||||
self.install_container_t.finished.connect(self.installation_finished)
|
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()
|
self.install_container_t.start()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ class IsolationProvider(ABC):
|
||||||
return self.debug or getattr(sys, "dangerzone_dev", False)
|
return self.debug or getattr(sys, "dangerzone_dev", False)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def install(self) -> bool:
|
def install(self, should_upgrade: bool, callback: Callable) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def convert(
|
def convert(
|
||||||
|
|
|
@ -3,7 +3,7 @@ import os
|
||||||
import platform
|
import platform
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import List, Tuple
|
from typing import Callable, List, Tuple
|
||||||
|
|
||||||
from .. import container_utils, errors, updater
|
from .. import container_utils, errors, updater
|
||||||
from ..container_utils import Runtime
|
from ..container_utils import Runtime
|
||||||
|
@ -94,27 +94,38 @@ class Container(IsolationProvider):
|
||||||
return security_args
|
return security_args
|
||||||
|
|
||||||
@staticmethod
|
@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."""
|
"""Check if an update is available and install it if necessary."""
|
||||||
# XXX Do this only if users have opted in to auto-updates
|
if not should_upgrade:
|
||||||
if False: # Comment this for now, just as an exemple of this can be implemented
|
log.debug("Skipping container upgrade check as requested by the settings")
|
||||||
# # Load the image tarball into the container runtime.
|
else:
|
||||||
update_available, image_digest = updater.is_update_available(
|
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:
|
if update_available and image_digest:
|
||||||
|
log.debug("Upgrading container image to %s", image_digest)
|
||||||
updater.upgrade_container_image(
|
updater.upgrade_container_image(
|
||||||
container_utils.CONTAINER_NAME,
|
container_utils.CONTAINER_NAME,
|
||||||
image_digest,
|
image_digest,
|
||||||
updater.DEFAULT_PUBKEY_LOCATION,
|
updater.DEFAULT_PUBKEY_LOCATION,
|
||||||
|
callback=callback,
|
||||||
)
|
)
|
||||||
for tag in old_tags:
|
else:
|
||||||
tag = container_utils.CONTAINER_NAME + ":" + tag
|
log.debug("No update available for the container")
|
||||||
container_utils.delete_image_tag(tag)
|
try:
|
||||||
|
|
||||||
updater.verify_local_image(
|
updater.verify_local_image(
|
||||||
container_utils.CONTAINER_NAME, updater.DEFAULT_PUBKEY_LOCATION
|
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
|
return True
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ from hashlib import sha256
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
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 container_utils as runtime
|
||||||
from .. import errors as dzerrors
|
from .. import errors as dzerrors
|
||||||
|
@ -370,7 +370,9 @@ def load_and_verify_signatures(
|
||||||
return 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:
|
Store signatures locally in the SIGNATURE_PATH folder, like this:
|
||||||
|
|
||||||
|
@ -415,6 +417,7 @@ def store_signatures(signatures: list[Dict], image_digest: str, pubkey: str) ->
|
||||||
)
|
)
|
||||||
json.dump(signatures, f)
|
json.dump(signatures, f)
|
||||||
|
|
||||||
|
if update_logindex:
|
||||||
write_log_index(get_log_index_from_signatures(signatures))
|
write_log_index(get_log_index_from_signatures(signatures))
|
||||||
|
|
||||||
|
|
||||||
|
@ -479,14 +482,16 @@ def prepare_airgapped_archive(image_name: str, destination: str) -> None:
|
||||||
archive.add(tmpdir, arcname=".")
|
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."""
|
"""Verify and upgrade the image to the latest, if signed."""
|
||||||
update_available, remote_digest = registry.is_new_remote_image_available(image)
|
update_available, remote_digest = registry.is_new_remote_image_available(image)
|
||||||
if not update_available:
|
if not update_available:
|
||||||
raise errors.ImageAlreadyUpToDate("The image is already up to date")
|
raise errors.ImageAlreadyUpToDate("The image is already up to date")
|
||||||
|
|
||||||
signatures = check_signatures_and_logindex(image, remote_digest, pubkey)
|
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 the signatures just now to avoid storing them unverified
|
||||||
store_signatures(signatures, manifest_digest, pubkey)
|
store_signatures(signatures, manifest_digest, pubkey)
|
||||||
|
|
Loading…
Reference in a new issue