diff -Nru edi-1.16.1+u2204/.github/workflows/package-build.yml edi-1.17.0+u2204/.github/workflows/package-build.yml --- edi-1.16.1+u2204/.github/workflows/package-build.yml 2024-03-01 10:21:11.000000000 +0000 +++ edi-1.17.0+u2204/.github/workflows/package-build.yml 2024-03-22 13:43:18.000000000 +0000 @@ -12,9 +12,6 @@ matrix: include: - distribution: debian - distribution_release: buster - repository_type: packagecloud - - distribution: debian distribution_release: bullseye repository_type: packagecloud - distribution: debian diff -Nru edi-1.16.1+u2204/debian/changelog edi-1.17.0+u2204/debian/changelog --- edi-1.16.1+u2204/debian/changelog 2024-03-01 10:21:24.000000000 +0000 +++ edi-1.17.0+u2204/debian/changelog 2024-03-22 13:43:31.000000000 +0000 @@ -1,8 +1,16 @@ -edi (1.16.1+u2204) jammy; urgency=medium +edi (1.17.0+u2204) jammy; urgency=medium * Automatic jammy build. - -- Matthias Lüscher (Launchpad) Fri, 01 Mar 2024 10:21:24 +0000 + -- Matthias Lüscher (Launchpad) Fri, 22 Mar 2024 13:43:31 +0000 + +edi (1.17.0) mantic; urgency=medium + + [ Matthias Luescher ] + * Removed Debian buster build. + * Added handling for podman images within command runner. + + -- Matthias Lüscher (Launchpad) Fri, 22 Mar 2024 14:41:55 +0100 edi (1.16.1) mantic; urgency=medium diff -Nru edi-1.16.1+u2204/debian/compat edi-1.17.0+u2204/debian/compat --- edi-1.16.1+u2204/debian/compat 2024-03-01 10:21:11.000000000 +0000 +++ edi-1.17.0+u2204/debian/compat 2024-03-22 13:43:18.000000000 +0000 @@ -1 +1 @@ -9 +10 diff -Nru edi-1.16.1+u2204/debian/control edi-1.17.0+u2204/debian/control --- edi-1.16.1+u2204/debian/control 2024-03-01 10:21:11.000000000 +0000 +++ edi-1.17.0+u2204/debian/control 2024-03-22 13:43:18.000000000 +0000 @@ -3,7 +3,7 @@ Priority: optional Build-Depends: ansible (>= 2.9), binfmt-support, - debhelper (>= 9), + debhelper (>= 10), debootstrap, dh-python, flake8, @@ -30,7 +30,7 @@ zstd, Maintainer: Matthias Luescher Standards-Version: 3.9.7 -X-Python3-Version: >= 3.5 +X-Python3-Version: >= 3.8 Package: edi Architecture: all diff -Nru edi-1.16.1+u2204/debian/rules edi-1.17.0+u2204/debian/rules --- edi-1.16.1+u2204/debian/rules 2024-03-01 10:21:11.000000000 +0000 +++ edi-1.17.0+u2204/debian/rules 2024-03-22 13:43:18.000000000 +0000 @@ -3,7 +3,7 @@ #DH_VERBOSE=1 %: - dh $@ --with python3,sphinxdoc --buildsystem=pybuild --fail-missing + dh $@ --with python3 --buildsystem=pybuild --fail-missing override_dh_installudev: dh_installudev --priority=81 diff -Nru edi-1.16.1+u2204/docs/conf.py edi-1.17.0+u2204/docs/conf.py --- edi-1.16.1+u2204/docs/conf.py 2024-03-01 10:21:24.000000000 +0000 +++ edi-1.17.0+u2204/docs/conf.py 2024-03-22 13:43:31.000000000 +0000 @@ -55,8 +55,8 @@ # built documents. # # The short X.Y version. -version = '1.16.1+u2204' -release = '1.16.1+u2204' +version = '1.17.0+u2204' +release = '1.17.0+u2204' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff -Nru edi-1.16.1+u2204/edi/lib/buildahhelpers.py edi-1.17.0+u2204/edi/lib/buildahhelpers.py --- edi-1.16.1+u2204/edi/lib/buildahhelpers.py 2024-03-01 10:21:11.000000000 +0000 +++ edi-1.17.0+u2204/edi/lib/buildahhelpers.py 2024-03-22 13:43:18.000000000 +0000 @@ -79,7 +79,6 @@ @require('buildah', buildah_install_hint, BuildahVersion.check) def delete_container(name): - # needs to be stopped first! cmd = [buildah_exec(), "rm", name] run(cmd, log_threshold=logging.INFO) diff -Nru edi-1.16.1+u2204/edi/lib/commandrunner.py edi-1.17.0+u2204/edi/lib/commandrunner.py --- edi-1.16.1+u2204/edi/lib/commandrunner.py 2024-03-01 10:21:11.000000000 +0000 +++ edi-1.17.0+u2204/edi/lib/commandrunner.py 2024-03-22 13:43:18.000000000 +0000 @@ -33,11 +33,14 @@ from edi.lib.shellhelpers import run, safely_remove_artifacts_folder from edi.lib.configurationparser import remove_passwords from edi.lib.yamlhelpers import LiteralString +from edi.lib.podmanhelpers import is_image_existing, try_delete_image, untag_image class ArtifactType(Enum): PATH = 'path' BUILDAH_CONTAINER = 'buildah-container' + PODMAN_IMAGE = 'podman-image' # Owned by non-root user. + PODMAN_IMAGE_ROOT = 'podman-image-root' # Owned by root. Artifact = namedtuple("Artifact", "name, location, type") @@ -114,8 +117,7 @@ commands = self._get_commands() for command in commands: - if (not self._are_all_artifacts_available(command.output_artifacts) - and self._require_real_root(command.config_node.get('require_root', False))): + if self._require_real_root(command.config_node.get('require_root', False)): return True return False @@ -132,7 +134,7 @@ def require_real_root_for_clean(self): for command in self._get_commands(): if (self._require_real_root(command.config_node.get('require_root', False)) - and self._is_an_artifact_a_directory(command.output_artifacts)): + and self._is_root_required_for_removal(command.output_artifacts)): return True return False @@ -158,19 +160,32 @@ commands = self._get_commands() for command in commands: for _, artifact in command.output_artifacts.items(): - if not str(get_workdir()) in str(artifact.location): - raise FatalError(('Output artifact {} is not within the current working directory!' - ).format(artifact.location)) - - if os.path.isfile(artifact.location): - logging.info("Removing '{}'.".format(artifact.location)) - os.remove(artifact.location) - print_success("Removed image file artifact {}.".format(artifact.location)) - elif os.path.isdir(artifact.location): - safely_remove_artifacts_folder(artifact.location, - sudo=self._require_real_root(command.config_node.get('require_root', - False))) - print_success("Removed image directory artifact {}.".format(artifact.location)) + if artifact.type is ArtifactType.PATH: + if not str(get_workdir()) in str(artifact.location): + raise FatalError(('Output artifact {} is not within the current working directory!' + ).format(artifact.location)) + + if os.path.isfile(artifact.location): + logging.info("Removing '{}'.".format(artifact.location)) + os.remove(artifact.location) + print_success("Removed image file artifact {}.".format(artifact.location)) + elif os.path.isdir(artifact.location): + safely_remove_artifacts_folder(artifact.location, + sudo=self._require_real_root( + command.config_node.get('require_root', False))) + print_success("Removed image directory artifact {}.".format(artifact.location)) + elif artifact.type in [ArtifactType.PODMAN_IMAGE, ArtifactType.PODMAN_IMAGE_ROOT]: + image_name = artifact.location + require_sudo = artifact.type is ArtifactType.PODMAN_IMAGE_ROOT + if is_image_existing(image_name, sudo=require_sudo): + if try_delete_image(image_name, sudo=require_sudo): + print_success(f"Removed podman image {artifact.location}.") + else: + logging.info(f"Podman image '{artifact.location}' is still in use, going to untag it.") + untag_image(image_name, sudo=require_sudo) + print_success(f"Untagged podman image {artifact.location}.") + else: + raise FatalError(f"Unhandled removal of artifact type '{artifact.type}'.") def result(self): commands = self._get_commands() @@ -185,7 +200,6 @@ run(cmd, log_threshold=logging.INFO, sudo=CommandRunner._require_real_root(require_root)) def _get_commands(self): - artifact_directory = get_artifact_dir() commands = self.config.get_ordered_path_items(self.config_section) augmented_commands = [] artifacts = dict() @@ -200,15 +214,7 @@ new_artifacts = dict() for artifact_key, artifact_item in output.items(): - if str(artifact_item) != os.path.basename(artifact_item): - raise FatalError((('''The specified output artifact '{}' within the ''' - '''command node '{}' is invalid.\n''' - '''The output shall be a file or a folder (no '/' in string).''') - ).format(artifact_key, name)) - artifact_path = os.path.join(artifact_directory, artifact_item) - new_artifacts[artifact_key] = Artifact(name=artifact_key, - location=str(artifact_path), - type=ArtifactType.PATH) + new_artifacts[artifact_key] = self._get_artifact(name, artifact_key, artifact_item) artifacts.update(new_artifacts) dictionary.update({key: val.location for key, val in artifacts.items()}) @@ -221,17 +227,55 @@ return augmented_commands @staticmethod + def _get_artifact(node_name, artifact_key, artifact_item): + if type(artifact_item) is dict: + artifact_location = artifact_item.get('location', '') + artifact_type_string = artifact_item.get('type', 'path') + try: + artifact_type = ArtifactType(artifact_type_string) + except ValueError: + raise FatalError((f"The specified output artifact '{artifact_key}' within the " + f"command node '{node_name}' has an invalid artifact type '{artifact_type_string}'.")) + else: + artifact_location = artifact_item + artifact_type = ArtifactType.PATH + + if not artifact_location: + raise FatalError((f"The specified output artifact '{artifact_key}' within the " + f"command node '{node_name}' must not be empty.")) + + if artifact_type is ArtifactType.PATH: + if str(artifact_location) != str(os.path.basename(artifact_location)): + raise FatalError((('''The specified output artifact '{}' within the ''' + '''command node '{}' is invalid. ''' + '''The output shall be a file or a folder (no '/' in string).''') + ).format(artifact_key, node_name)) + artifact_location = os.path.join(get_artifact_dir(), artifact_location) + + return Artifact(name=artifact_key, location=str(artifact_location), type=artifact_type) + + @staticmethod def _are_all_artifacts_available(artifacts): for _, artifact in artifacts.items(): - if not os.path.isfile(artifact.location) and not os.path.isdir(artifact.location): - return False + if artifact.type is ArtifactType.PATH: + if not os.path.isfile(artifact.location) and not os.path.isdir(artifact.location): + return False + elif artifact.type in [ArtifactType.PODMAN_IMAGE, ArtifactType.PODMAN_IMAGE_ROOT]: + require_sudo = artifact.type is ArtifactType.PODMAN_IMAGE_ROOT + if not is_image_existing(artifact.location, sudo=require_sudo): + return False + else: + raise FatalError(f"Unhandled presence checking for artifact type '{artifact.type}'.") return True @staticmethod - def _is_an_artifact_a_directory(artifacts): + def _is_root_required_for_removal(artifacts): for _, artifact in artifacts.items(): - if os.path.isdir(artifact.location): + if artifact.type is ArtifactType.PATH and os.path.isdir(artifact.location): + return True + if artifact.type is ArtifactType.PODMAN_IMAGE_ROOT: + # Unable to check presence as we might not be running as root. return True return False @@ -239,11 +283,21 @@ @staticmethod def _post_process_artifacts(command_name, expected_artifacts): for _, artifact in expected_artifacts.items(): - if not os.path.isfile(artifact.location) and not os.path.isdir(artifact.location): - raise FatalError(('''The command '{}' did not generate ''' - '''the specified output artifact '{}'.'''.format(command_name, artifact.location))) - elif os.path.isfile(artifact.location): - chown_to_user(artifact.location) + if artifact.type is ArtifactType.PATH: + if not os.path.isfile(artifact.location) and not os.path.isdir(artifact.location): + raise FatalError(('''The command '{}' did not generate ''' + '''the specified output artifact '{}'.'''.format(command_name, + artifact.location))) + elif os.path.isfile(artifact.location): + chown_to_user(artifact.location) + elif artifact.type in [ArtifactType.PODMAN_IMAGE, ArtifactType.PODMAN_IMAGE_ROOT]: + require_sudo = artifact.type is ArtifactType.PODMAN_IMAGE_ROOT + if not is_image_existing(artifact.location, sudo=require_sudo): + raise FatalError(('''The command '{}' did not generate ''' + '''the specified podman image '{}'.'''.format(command_name, + artifact.location))) + else: + raise FatalError(f"Missing postprocessing rule for artifact type '{artifact.type}'.") @staticmethod def _render_command_file(input_file, dictionary): diff -Nru edi-1.16.1+u2204/edi/lib/podmanhelpers.py edi-1.17.0+u2204/edi/lib/podmanhelpers.py --- edi-1.16.1+u2204/edi/lib/podmanhelpers.py 1970-01-01 00:00:00.000000000 +0000 +++ edi-1.17.0+u2204/edi/lib/podmanhelpers.py 2024-03-22 13:43:18.000000000 +0000 @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2024 Matthias Luescher +# +# Authors: +# Matthias Luescher +# +# This file is part of edi. +# +# edi is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# edi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with edi. If not, see . + +import subprocess +import yaml +import logging +from packaging.version import Version +from edi.lib.helpers import FatalError +from edi.lib.versionhelpers import get_stripped_version +from edi.lib.shellhelpers import run, Executables, require + + +podman_install_hint = "'sudo apt install podman'" + + +def podman_exec(): + return Executables.get('podman') + + +def get_podman_version(): + if not Executables.has('podman'): + return '0.0.0' + + cmd = [Executables.get("podman"), "version", "--format=json"] + result = run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + parsed_result = yaml.safe_load(result.stdout) + return parsed_result.get('Client').get('Version') + + +class PodmanVersion: + """ + Make sure that the podman version is >= 4.3.1. + """ + _check_done = False + _required_minimal_version = '4.3.1' + + def __init__(self, clear_cache=False): + if clear_cache: + PodmanVersion._check_done = False + + @staticmethod + def check(): + if PodmanVersion._check_done: + return + + if Version(get_stripped_version(get_podman_version())) < Version(PodmanVersion._required_minimal_version): + raise FatalError(('The current podman installation ({}) does not meet the minimal requirements (>={}).\n' + 'Please update your podman installation!' + ).format(get_podman_version(), PodmanVersion._required_minimal_version)) + else: + PodmanVersion._check_done = True + + +@require('podman', podman_install_hint, PodmanVersion.check) +def is_image_existing(name, sudo=False): + cmd = [podman_exec(), "image", "exists", name] + result = run(cmd, check=False, stderr=subprocess.PIPE, sudo=sudo) + return result.returncode == 0 + + +@require('podman', podman_install_hint, PodmanVersion.check) +def try_delete_image(name, sudo=False): + cmd = [podman_exec(), "image", "rm", name] + result = run(cmd, check=False, stderr=subprocess.PIPE, sudo=sudo) + return result.returncode == 0 + + +@require('podman', podman_install_hint, PodmanVersion.check) +def untag_image(name, sudo=False): + cmd = [podman_exec(), "image", "untag", name] + run(cmd, log_threshold=logging.INFO, sudo=sudo) diff -Nru edi-1.16.1+u2204/edi/lib/versionhelpers.py edi-1.17.0+u2204/edi/lib/versionhelpers.py --- edi-1.16.1+u2204/edi/lib/versionhelpers.py 2024-03-01 10:21:24.000000000 +0000 +++ edi-1.17.0+u2204/edi/lib/versionhelpers.py 2024-03-22 13:43:31.000000000 +0000 @@ -20,13 +20,13 @@ # along with edi. If not, see . import os -import pkg_resources +import importlib.metadata import re from edi.lib.helpers import FatalError # The do_release script will update this version! # During launchpad debuild neither the git version nor the package version is available. -edi_fallback_version = '1.16.1+u2204' +edi_fallback_version = '1.17.0+u2204' def get_edi_version(): @@ -43,8 +43,8 @@ return get_version(root=project_root) else: try: - return pkg_resources.get_distribution('edi').version - except pkg_resources.DistributionNotFound: + return importlib.metadata.version('edi') + except importlib.metadata.PackageNotFoundError: return edi_fallback_version diff -Nru edi-1.16.1+u2204/setup.py edi-1.17.0+u2204/setup.py --- edi-1.16.1+u2204/setup.py 2024-03-01 10:21:24.000000000 +0000 +++ edi-1.17.0+u2204/setup.py 2024-03-22 13:43:31.000000000 +0000 @@ -38,7 +38,7 @@ setup( name='edi', - version='1.16.1+u2204', + version='1.17.0+u2204', description='Embedded Development Infrastructure - edi', long_description=long_description, diff -Nru edi-1.16.1+u2204/tests/conftest.py edi-1.17.0+u2204/tests/conftest.py --- edi-1.16.1+u2204/tests/conftest.py 2024-03-01 10:21:11.000000000 +0000 +++ edi-1.17.0+u2204/tests/conftest.py 2024-03-22 13:43:18.000000000 +0000 @@ -32,6 +32,8 @@ help="include tests that rely on lxc availability") parser.addoption("--buildah", action="store_true", help="include tests that rely on buildah availability") + parser.addoption("--podman", action="store_true", + help="include tests that rely on podman availability") parser.addoption("--ansible", action="store_true", help="include tests that rely on ansible availability") parser.addoption("--debootstrap", action="store_true", @@ -43,6 +45,7 @@ def pytest_configure(config): config.addinivalue_line("markers", "requires_lxc: mark test as requiring lxc to run") config.addinivalue_line("markers", "requires_buildah: mark test as requiring buildah to run") + config.addinivalue_line("markers", "requires_podman: mark test as requiring podman to run") config.addinivalue_line("markers", "requires_ansible: mark test as requiring ansible to run") config.addinivalue_line("markers", "requires_debootstrap: mark test as requiring debootstrap to run") config.addinivalue_line("markers", "requires_flake8: mark test as requiring flake8 to run") @@ -55,6 +58,8 @@ item.add_marker(pytest.mark.skip(reason="requires --lxc or --all option to run")) if "requires_buildah" in item.keywords and not (config.getoption("--buildah") or config.getoption("--all")): item.add_marker(pytest.mark.skip(reason="requires --buildah or --all option to run")) + if "requires_podman" in item.keywords and not (config.getoption("--podman") or config.getoption("--all")): + item.add_marker(pytest.mark.skip(reason="requires --podman or --all option to run")) if "requires_ansible" in item.keywords and not (config.getoption("--ansible") or config.getoption("--all")): item.add_marker(pytest.mark.skip(reason="requires --ansible or --all option to run")) if "requires_debootstrap" in item.keywords and not (config.getoption("--debootstrap") or @@ -188,7 +193,9 @@ message: "*last step*" require_root: False output: - last_output_file: last.txt + last_output_file: + location: last.txt + type: path documentation_steps: 20_second_step: Binary files /tmp/tmp3fewr4p2/D9UTGqjTR1/edi-1.16.1+u2204/tests/data/test_podmanhelpers/demo_rootfs.tar and /tmp/tmp3fewr4p2/cU6RzH5VOo/edi-1.17.0+u2204/tests/data/test_podmanhelpers/demo_rootfs.tar differ diff -Nru edi-1.16.1+u2204/tests/lib/test_buildahhelpers.py edi-1.17.0+u2204/tests/lib/test_buildahhelpers.py --- edi-1.16.1+u2204/tests/lib/test_buildahhelpers.py 2024-03-01 10:21:11.000000000 +0000 +++ edi-1.17.0+u2204/tests/lib/test_buildahhelpers.py 2024-03-22 13:43:18.000000000 +0000 @@ -182,6 +182,8 @@ assert os.path.isfile(extracted_rootfs_archive) + delete_container(container_name) + @pytest.mark.requires_buildah def test_buildah_container_creation_failure(): diff -Nru edi-1.16.1+u2204/tests/lib/test_commandrunner.py edi-1.17.0+u2204/tests/lib/test_commandrunner.py --- edi-1.16.1+u2204/tests/lib/test_commandrunner.py 2024-03-01 10:21:11.000000000 +0000 +++ edi-1.17.0+u2204/tests/lib/test_commandrunner.py 2024-03-22 13:43:18.000000000 +0000 @@ -24,9 +24,11 @@ from tests.libtesting.contextmanagers.workspace import workspace from tests.libtesting.helpers import get_command, suppress_chown_during_debuild from edi.lib import mockablerun +from edi.lib.helpers import FatalError import subprocess import os from codecs import open +import pytest def test_run_and_clean(config_files, monkeypatch): @@ -115,3 +117,27 @@ location=input_file, type=ArtifactType.PATH)) assert not runner.require_real_root() assert not runner.require_real_root_for_clean() + + +@pytest.mark.parametrize("artifact_item, expected_type, expected_location", [ + ('foo', ArtifactType.PATH, 'foo'), + ({'location': 'bar'}, ArtifactType.PATH, 'bar'), + ({'location': 'bingo:bongo', 'type': 'buildah-container'}, ArtifactType.BUILDAH_CONTAINER, 'bingo:bongo'), +]) +def test_output_node(artifact_item, expected_type, expected_location): + artifact = CommandRunner._get_artifact('some_node', 'some_key', artifact_item) + assert artifact.type is expected_type + assert expected_location in artifact.location + assert artifact.name == 'some_key' + + +@pytest.mark.parametrize("artifact_item, error_message", [ + ('', 'must not be empty'), + ({'location': ''}, 'must not be empty'), + ({'location': 'bingo/bongo'}, 'shall be a file or a folder'), + ({'location': 'bingo:bongo', 'type': 'unknown-artifact'}, 'invalid artifact type'), +]) +def test_output_node_failure(artifact_item, error_message): + with pytest.raises(FatalError) as error: + CommandRunner._get_artifact('some_node', 'some_key', artifact_item) + assert error_message in str(error) diff -Nru edi-1.16.1+u2204/tests/lib/test_podmanhelpers.py edi-1.17.0+u2204/tests/lib/test_podmanhelpers.py --- edi-1.16.1+u2204/tests/lib/test_podmanhelpers.py 1970-01-01 00:00:00.000000000 +0000 +++ edi-1.17.0+u2204/tests/lib/test_podmanhelpers.py 2024-03-22 13:43:18.000000000 +0000 @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2024 Matthias Luescher +# +# Authors: +# Matthias Luescher +# +# This file is part of edi. +# +# edi is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# edi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with edi. If not, see . + + +import pytest +import subprocess +import tempfile +import json +import os +import logging +import shutil +from edi.lib.helpers import FatalError, chown_to_user +from edi.lib.podmanhelpers import (get_podman_version, PodmanVersion, is_image_existing, try_delete_image, untag_image, + podman_exec) +from edi.lib.buildahhelpers import create_container, is_container_existing, delete_container, buildah_exec +from contextlib import contextmanager +from edi.lib.shellhelpers import run, mockablerun +from tests.libtesting.helpers import get_command, get_sub_command, get_random_string +from tests.libtesting.contextmanagers.mocked_executable import mocked_executable + + +@pytest.mark.requires_podman +def test_get_podman_version(): + version = get_podman_version() + assert '.' in version + + +@contextmanager +def clear_podman_version_check_cache(): + try: + PodmanVersion(clear_cache=True) + yield + finally: + PodmanVersion(clear_cache=True) + + +@pytest.mark.requires_podman +def test_podman_version_check(): + with clear_podman_version_check_cache(): + check_method = PodmanVersion.check + check_method() + + +def patch_podman_get_version(monkeypatch, fake_version): + def fake_podman_version_command(*popenargs, **kwargs): + if get_command(popenargs).endswith('podman') and get_sub_command(popenargs) == 'version': + output_json = { + "Client": + { + "APIVersion": fake_version, + "Version": fake_version, + "GoVersion": "go1.21.1", + "GitCommit": "", + "BuiltTime": "Thu Jan 1 01:00:00 1970", + "Built": 0, + "OsArch": "linux/amd64", + "Os": "linux" + } + } + + return subprocess.CompletedProcess("fakerun", 0, + stdout=json.dumps(output_json)) + else: + return subprocess.run(*popenargs, **kwargs) + + monkeypatch.setattr(mockablerun, 'run_mockable', fake_podman_version_command) + + +def test_invalid_podman_version(monkeypatch): + patch_podman_get_version(monkeypatch, '3.1.2') + with mocked_executable('podman', '/here/is/no/podman'): + with clear_podman_version_check_cache(): + check_method = PodmanVersion.check + + with pytest.raises(FatalError) as error: + check_method() + + assert '3.1.2' in error.value.message + assert '>=4.3.1' in error.value.message + + +def test_valid_podman_version(monkeypatch): + patch_podman_get_version(monkeypatch, '16.1.0+bingo') + with mocked_executable('podman', '/here/is/no/podman'): + with clear_podman_version_check_cache(): + PodmanVersion.check() + + +@pytest.mark.requires_buildah +@pytest.mark.requires_podman +def test_buildah_podman_workflow(datadir): + work_dir = os.getcwd() + with tempfile.TemporaryDirectory(dir=work_dir) as tempdir: + chown_to_user(tempdir) + demo_rootfs_archive = os.path.join(tempdir, 'demo_rootfs.tar') + shutil.copyfile(os.path.join(datadir, "demo_rootfs.tar"), demo_rootfs_archive) + + container_name = f'edi-pytest-{get_random_string(6)}' + assert not is_container_existing(container_name) + + create_container(container_name, demo_rootfs_archive) + + image_name = f'edi-pytest-{get_random_string(6)}:test'.lower() + + cmd = [buildah_exec(), "commit", container_name, image_name] + run(cmd, log_threshold=logging.INFO) + + cmd = [podman_exec(), "image", "inspect", "--format", "{{.Id}}", image_name] + image_id = run(cmd, log_threshold=logging.INFO, stdout=subprocess.PIPE).stdout.strip() + + assert is_image_existing(image_name) + + second_container_name = f'edi-pytest-{get_random_string(6)}' + + cmd = [buildah_exec(), "from", "--name", second_container_name, image_name] + run(cmd, log_threshold=logging.INFO) + + assert not try_delete_image(image_name) + assert not try_delete_image(image_id) + untag_image(image_name) + assert not is_image_existing(image_name) + assert is_image_existing(image_id) + delete_container(second_container_name) + assert try_delete_image(image_id) + + delete_container(container_name) diff -Nru edi-1.16.1+u2204/tests/libtesting/contextmanagers/mocked_executable.py edi-1.17.0+u2204/tests/libtesting/contextmanagers/mocked_executable.py --- edi-1.16.1+u2204/tests/libtesting/contextmanagers/mocked_executable.py 2024-03-01 10:21:11.000000000 +0000 +++ edi-1.17.0+u2204/tests/libtesting/contextmanagers/mocked_executable.py 2024-03-22 13:43:18.000000000 +0000 @@ -25,20 +25,21 @@ from edi.lib.shellhelpers import Executables from edi.lib.lxchelpers import LxdVersion from edi.lib.buildahhelpers import BuildahVersion +from edi.lib.podmanhelpers import PodmanVersion @contextmanager -def mocked_executable(executable, mock=str(os.path.join(os.sep, 'bin', 'true'))): +def mocked_executable(executable, executable_mock=str(os.path.join(os.sep, 'bin', 'true'))): """ Mocks away an executable that gets fetched using the Executables class. :param executable: The executable to be mocked. - :param mock: The replacement for the mocked executable. + :param executable_mock: The replacement for the mocked executable. :return: The mock. """ - Executables._cache[executable] = mock + Executables._cache[executable] = executable_mock try: - yield mock + yield executable_mock finally: del Executables._cache[executable] @@ -59,3 +60,12 @@ yield finally: BuildahVersion._check_done = False + + +@contextmanager +def mocked_podman_version_check(): + PodmanVersion._check_done = True + try: + yield + finally: + PodmanVersion._check_done = False