From 4d84aa8595ed6c288fd767a8732145e7a6870652 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 24 Oct 2024 19:32:57 +0200 Subject: [PATCH 01/11] Adding podman support --- exegol/utils/DockerUtils.py | 146 ++++++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 50 deletions(-) diff --git a/exegol/utils/DockerUtils.py b/exegol/utils/DockerUtils.py index 6f446d2c..d422e61e 100644 --- a/exegol/utils/DockerUtils.py +++ b/exegol/utils/DockerUtils.py @@ -10,6 +10,10 @@ from docker.models.volumes import Volume from requests import ReadTimeout +import podman +from podman import PodmanClient +from podman.errors import APIError as PodmanAPIError, DockerException as PodmanException, NotFound as PodmanNotFound, ImageNotFound as PodmanImageNotFound + from exegol.config.ConstantConfig import ConstantConfig from exegol.config.DataCache import DataCache from exegol.config.EnvInfo import EnvInfo @@ -32,35 +36,75 @@ class DockerUtils(metaclass=MetaSingleton): def __init__(self): - """Utility class between exegol and the Docker SDK""" + """Utility class to manage interactions between exegol and Docker or Podman.""" + self.__client = None + self.__daemon_info = None + self.container_runtime = None # Will be set to either 'docker' or 'podman' + + # List of exceptions that could be raised by both Docker and Podman + connection_exceptions = (DockerException, PodmanException) + try: - # Connect Docker SDK to the local docker instance. - # Docker connection setting is loaded from the user environment variables. - self.__client: DockerClient = docker.from_env() + # Attempt to connect to Docker + self.__client = self.__connect_to_docker() + + if not self.__client: + # If Docker fails, attempt to connect to Podman + self.__client = self.__connect_to_podman() + + if not self.__client: + raise RuntimeError("Failed to connect to both Docker and Podman.") + # Check if the docker daemon is serving linux container self.__daemon_info = self.__client.info() if self.__daemon_info.get("OSType", "linux").lower() != "linux": logger.critical( f"Docker daemon is not serving linux container ! Docker OS Type is: {self.__daemon_info.get('OSType', 'linux')}") EnvInfo.initData(self.__daemon_info) - except DockerException as err: - if 'ConnectionRefusedError' in str(err): - logger.critical(f"Unable to connect to docker (from env config). Is docker running on your machine? Exiting.{os.linesep}" - f" Check documentation for help: https://exegol.readthedocs.io/en/latest/getting-started/faq.html#unable-to-connect-to-docker") - elif 'FileNotFoundError' in str(err): - logger.critical(f"Unable to connect to docker. Is docker installed on your machine? Exiting.{os.linesep}" - f" Check documentation for help: https://exegol.readthedocs.io/en/latest/getting-started/faq.html#unable-to-connect-to-docker") - elif 'PermissionError' in str(err): - logger.critical(f"Docker is installed on your host but you don't have the permission to interact with it. Exiting.{os.linesep}" - f" Check documentation for help: https://exegol.readthedocs.io/en/latest/getting-started/install.html#optional-run-exegol-with-appropriate-privileges") - else: - logger.error(err) - logger.critical( - "Unable to connect to docker (from env config). Is docker operational and accessible? on your machine? " - "Exiting.") + except connection_exceptions as err: + self.__handle_connection_error(err) + except Exception as err: + logger.error(f"Unexpected error: {err}") + self.__images: Optional[List[ExegolImage]] = None self.__containers: Optional[List[ExegolContainer]] = None + def __connect_to_docker(self): + """Attempts to connect to Docker.""" + try: + client = docker.from_env() + self.container_runtime = "docker" + logger.info("Connected to Docker.") + return client + except DockerException as err: + logger.warning(f"Unable to connect to Docker: {err}") + return None + + def __connect_to_podman(self): + """Attempts to connect to Podman.""" + try: + client = podman.from_env() + self.container_runtime = "podman" + logger.info("Connected to Podman.") + return client + except PodmanException as err: + logger.warning(f"Unable to connect to Podman: {err}") + return None + + def __handle_connection_error(self, err): + """Handles connection errors for both Docker and Podman.""" + if 'ConnectionRefusedError' in str(err): + logger.critical(f"Unable to connect to {self.container_runtime}. Is it running on your machine? Exiting.{os.linesep}" + f" Check documentation for help: https://exegol.readthedocs.io/en/latest/getting-started/faq.html#unable-to-connect-to-docker") + elif 'FileNotFoundError' in str(err): + logger.critical(f"Unable to connect to {self.container_runtime}. Is it installed on your machine? Exiting.{os.linesep}" + f" Check documentation for help: https://exegol.readthedocs.io/en/latest/getting-started/faq.html#unable-to-connect-to-docker") + elif 'PermissionError' in str(err): + logger.critical(f"{self.container_runtime.capitalize()} is installed on your host but you don't have permission to interact with it. Exiting.{os.linesep}" + f" Check documentation for help: https://exegol.readthedocs.io/en/latest/getting-started/install.html#optional-run-exegol-with-appropriate-privileges") + else: + logger.critical(f"Unable to connect to {self.container_runtime}. Is it operational and accessible? Exiting.") + def clearCache(self): """Remove class's images and containers data cache Only needed if the list has to be updated in the same runtime at a later moment""" @@ -80,7 +124,7 @@ def listContainers(self) -> List[ExegolContainer]: self.__containers = [] try: docker_containers = self.__client.containers.list(all=True, filters={"name": "exegol-"}) - except APIError as err: + except (APIError, PodmanAPIError) as err: logger.debug(err) logger.critical(err.explanation) # Not reachable, critical logging will exit @@ -99,9 +143,9 @@ def createContainer(self, model: ExegolContainerTemplate, temporary: bool = Fals model.prepare() logger.debug(model) # Preload docker volume before container creation - for volume in model.config.getVolumes(): + for volume in model.config.gets(): if volume.get('Type', '?') == "volume": - docker_volume = self.__loadDockerVolume(volume_path=volume['Source'], volume_name=volume['Target']) + docker_volume = self.__loadDocker(volume_path=volume['Source'], volume_name=volume['Target']) if docker_volume is None: logger.warning(f"Error while creating docker volume '{volume['Target']}'") entrypoint, command = model.config.getEntrypointCommand() @@ -128,7 +172,7 @@ def createContainer(self, model: ExegolContainerTemplate, temporary: bool = Fals "shm_size": model.config.shm_size, "stdin_open": model.config.interactive, "tty": model.config.tty, - "mounts": model.config.getVolumes(), + "mounts": model.config.gets(), "working_dir": model.config.getWorkingDir()} if temporary: # Only the 'run' function support the "remove" parameter @@ -137,7 +181,7 @@ def createContainer(self, model: ExegolContainerTemplate, temporary: bool = Fals docker_args["auto_remove"] = temporary try: container = docker_create_function(**docker_args) - except APIError as err: + except (APIError, PodmanAPIError) as err: message = err.explanation.decode('utf-8').replace('[', '\\[') if type(err.explanation) is bytes else err.explanation if message is not None: message = message.replace('[', '\\[') @@ -169,7 +213,7 @@ def getContainer(self, tag: str) -> ExegolContainer: try: # Fetch potential container match from DockerSDK container = self.__client.containers.list(all=True, filters={"name": f"exegol-{tag}"}) - except APIError as err: + except (APIError, PodmanAPIError) as err: logger.debug(err) logger.critical(err.explanation) # Not reachable, critical logging will exit @@ -194,9 +238,9 @@ def getContainer(self, tag: str) -> ExegolContainer: # In this case, ObjectNotFound is raised raise ObjectNotFound - # # # Volumes Section # # # + # # # s Section # # # - def __loadDockerVolume(self, volume_path: str, volume_name: str) -> Volume: + def __loadDocker(self, volume_path: str, volume_name: str) -> : """Load or create a docker volume for exegol containers (must be created before the container, SDK limitation) Return the docker volume object""" @@ -212,19 +256,19 @@ def __loadDockerVolume(self, volume_path: str, volume_name: str) -> Volume: if path != volume_path: try: self.__client.api.remove_volume(name=volume_name) - raise NotFound('Volume must be reloaded') - except APIError as e: + raise NotFound(' must be reloaded') + except (APIError, PodmanAPIError) as e: if e.status_code == 409: logger.warning("The path of the volume specified by the user is not the same as in the existing docker volume. " "The user path will be [red]ignored[/red] as long as the docker volume already exists.") logger.verbose("The volume is already used by some container and cannot be automatically removed.") logger.debug(e.explanation) else: - raise NotFound('Volume must be reloaded') + raise NotFound(' must be reloaded') except ReadTimeout: - logger.error(f"Received a timeout error, Docker is busy... Volume {volume_name} cannot be automatically removed. Please, retry later the following command:{os.linesep}" + logger.error(f"Received a timeout error, Docker is busy... {volume_name} cannot be automatically removed. Please, retry later the following command:{os.linesep}" f" [orange3]docker volume rm {volume_name}[/orange3]") - except NotFound: + except (NotFound, PodmanNotFound): try: # Creating a docker volume bind to a host path # Docker volume are more easily shared by container @@ -233,15 +277,15 @@ def __loadDockerVolume(self, volume_path: str, volume_name: str) -> Volume: driver_opts={'o': 'bind', 'device': volume_path, 'type': 'none'}) - except APIError as err: + except (APIError, PodmanAPIError) as err: logger.error(f"Error while creating docker volume '{volume_name}'.") logger.debug(err) logger.critical(err.explanation) return None # type: ignore except ReadTimeout: - logger.critical(f"Received a timeout error, Docker is busy... Volume {volume_name} cannot be created.") + logger.critical(f"Received a timeout error, Docker is busy... {volume_name} cannot be created.") return # type: ignore - except APIError as err: + except (APIError, PodmanAPIError) as err: logger.critical(f"Unexpected error by Docker SDK : {err}") return None # type: ignore except ReadTimeout: @@ -304,7 +348,7 @@ def getInstalledImage(self, tag: str) -> ExegolImage: try: docker_local_image = self.__client.images.get(f"{ConstantConfig.IMAGE_NAME}:{tag}") # DockerSDK image get is an exact matching, no need to add more check - except APIError as err: + except (APIError, PodmanAPIError) as err: if err.status_code == 404: # try to find it in recovery mode logger.verbose("Unable to find your image. Trying to find in recovery mode.") @@ -342,8 +386,8 @@ def __listLocalImages(self, tag: Optional[str] = None) -> List[Image]: logger.debug("Fetching local image tags, digests (and other attributes)") try: image_name = ConstantConfig.IMAGE_NAME + ("" if tag is None else f":{tag}") - images = self.__client.images.list(image_name, filters={"dangling": False}) - except APIError as err: + images = self.__client.images.list(name=image_name, filters={"dangling": False}) + except (APIError, PodmanAPIError) as err: logger.debug(err) logger.critical(err.explanation) # Not reachable, critical logging will exit @@ -382,7 +426,7 @@ def __findLocalRecoveryImages(self, include_untag: bool = False) -> List[Image]: recovery_images = self.__client.images.list(filters={"dangling": True}) if include_untag: recovery_images += self.__client.images.list(ConstantConfig.IMAGE_NAME, filters={"dangling": False}) - except APIError as err: + except (APIError, PodmanAPIError) as err: logger.debug(f"Error occurred in recovery mode: {err}") return [] except ReadTimeout: @@ -445,7 +489,7 @@ def __findImageMatch(self, remote_image: ExegolImage): remote_id = remote_image.getRemoteId() try: docker_image = self.__client.images.get(f"{ConstantConfig.IMAGE_NAME}@{remote_id}") - except ImageNotFound: + except (ImageNotFound, PodmanImageNotFound): raise ObjectNotFound except ReadTimeout: logger.critical("Received a timeout error, Docker is busy... Unable to find a specific image, retry later.") @@ -468,17 +512,19 @@ def downloadImage(self, image: ExegolImage, install_mode: bool = False) -> bool: logger.debug(f"Downloading {ConstantConfig.IMAGE_NAME}:{name} ({image.getArch()})") try: ExegolTUI.downloadDockerLayer( - self.__client.api.pull(repository=ConstantConfig.IMAGE_NAME, - tag=name, - stream=True, - decode=True, - platform="linux/" + image.getArch())) + self.__client.images.pull( + repository=ConstantConfig.IMAGE_NAME, # Image name from config + tag=name, # Tag (e.g., 'latest') + stream=True, # Stream the output + decode=True, # Stream the output + platform="linux/" + image.getArch(), # Platform (e.g., 'linux/amd64') + )) logger.success(f"Image successfully {'installed' if install_mode else 'updated'}") # Remove old image if not install_mode and image.isInstall() and UserConfig().auto_remove_images: self.removeImage(image, upgrade_mode=not install_mode) return True - except APIError as err: + except (APIError, PodmanAPIError) as err: if err.status_code == 500: logger.error(f"Error: {err.explanation}") logger.error(f"Error while contacting docker registry. Aborting.") @@ -501,7 +547,7 @@ def downloadVersionTag(self, image: ExegolImage) -> Union[ExegolImage, str]: tag=image.getLatestVersionName(), platform="linux/" + image.getArch()) return ExegolImage(docker_image=image, isUpToDate=True) - except APIError as err: + except (APIError, PodmanAPIError) as err: if err.status_code == 500: return f"error while contacting docker registry: {err.explanation}" elif err.status_code == 404: @@ -531,7 +577,7 @@ def removeImage(self, image: ExegolImage, upgrade_mode: bool = False) -> bool: logger.verbose(f"Removing {'previous ' if upgrade_mode else ''}image [green]{image.getName()}[/green]...") logger.success(f"{'Previous d' if upgrade_mode else 'D'}ocker image successfully removed.") return True - except APIError as err: + except (APIError, PodmanAPIError) as err: # Handle docker API error code logger.verbose(err.explanation) if err.status_code == 409: @@ -564,7 +610,7 @@ def __remove_image(self, image_name: str) -> bool: try: _ = self.__client.images.get(image_name) # DockerSDK image getter is an exact matching, no need to add more check - except APIError as err: + except (APIError, PodmanAPIError) as err: if err.status_code == 404: return True else: @@ -607,7 +653,7 @@ def buildImage(self, tag: str, build_profile: Optional[str] = None, build_docker pull=True, decode=True)) logger.success(f"Exegol image successfully built") - except APIError as err: + except (APIError, PodmanAPIError) as err: logger.debug(f"Error: {err}") if err.status_code == 500: logger.error(f"Error: {err.explanation}") From 7520b5aa006131d872e43e13cb7cf9a43dc171d4 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 24 Oct 2024 19:48:32 +0200 Subject: [PATCH 02/11] Update ExegolContainer.py --- exegol/model/ExegolContainer.py | 37 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/exegol/model/ExegolContainer.py b/exegol/model/ExegolContainer.py index e49f4800..4a3d9238 100644 --- a/exegol/model/ExegolContainer.py +++ b/exegol/model/ExegolContainer.py @@ -4,7 +4,10 @@ from typing import Optional, Dict, Sequence, Tuple, Union from docker.errors import NotFound, ImageNotFound, APIError -from docker.models.containers import Container +from docker.models.containers import Container as DockerContainer + +from podman.errors import NotFound as PodmanNotFound, ImageNotFound as PodmanImageNotFound, APIError as PodmanAPIError +from podman.domain.containers import Container as PodmanContainer from exegol.config.EnvInfo import EnvInfo from exegol.console.ExegolPrompt import Confirm @@ -21,36 +24,36 @@ class ExegolContainer(ExegolContainerTemplate, SelectableInterface): """Class of an exegol container already create in docker""" - def __init__(self, docker_container: Container, model: Optional[ExegolContainerTemplate] = None): - logger.debug(f"Loading container: {docker_container.name}") - self.__container: Container = docker_container - self.__id: str = docker_container.id + def __init__(self, container_obj: Union[DockerContainer, PodmanContainer], model: Optional[ExegolContainerTemplate] = None): + logger.debug(f"Loading container: {container_obj.name}") + self.__container = container_obj + self.__id: str = container_obj.id self.__xhost_applied = False if model is None: image_name = "" try: # Try to find the attached docker image - docker_image = docker_container.image - except ImageNotFound: + docker_image = container_obj.image + except (ImageNotFound, PodmanImageNotFound): # If it is not found, the user has probably forcibly deleted it manually logger.warning(f"Some images were forcibly removed by docker when they were used by existing containers!") - logger.error(f"The '{docker_container.name}' containers might not work properly anymore and should also be deleted and recreated with a new image.") + logger.error(f"The '{container_obj.name}' containers might not work properly anymore and should also be deleted and recreated with a new image.") docker_image = None image_name = "[red bold]BROKEN[/red bold]" # Create Exegol container from an existing docker container - super().__init__(docker_container.name, - config=ContainerConfig(docker_container), + super().__init__(container_obj.name, + config=ContainerConfig(container_obj), image=ExegolImage(name=image_name, docker_image=docker_image), - hostname=docker_container.attrs.get('Config', {}).get('Hostname'), + hostname=container_obj.attrs.get('Config', {}).get('Hostname'), new_container=False) - self.image.syncContainerData(docker_container) + self.image.syncContainerData(container_obj) # At this stage, the container image object has an unknown status because no synchronization with a registry has been done. # This could be done afterwards (with container.image.autoLoad()) if necessary because it takes time. self.__new_container = False else: # Create Exegol container from a newly created docker container with its object template. - super().__init__(docker_container.name, - config=ContainerConfig(docker_container), + super().__init__(container_obj.name, + config=ContainerConfig(container_obj), # Rebuild config from docker object to update workspace path image=model.image, hostname=model.config.hostname, @@ -118,7 +121,7 @@ def __start_container(self): start_date = datetime.utcnow() try: self.__container.start() - except APIError as e: + except (APIError, PodmanAPIError) as e: logger.debug(e) logger.critical(f"Docker raise a critical error when starting the container [green]{self.name}[/green], error message is: {e.explanation}") if not self.config.legacy_entrypoint: # TODO improve startup compatibility check @@ -225,7 +228,7 @@ def remove(self): try: self.__container.remove() logger.success(f"Container {self.name} successfully removed.") - except NotFound: + except (NotFound, PodmanNotFound): logger.error( f"The container {self.name} has already been removed (probably created as a temporary container).") @@ -314,7 +317,7 @@ def postCreateSetup(self, is_temporary: bool = False): self.__start_container() try: self.__updatePasswd() - except APIError as e: + except (APIError, PodmanAPIError) as e: if "is not running" in e.explanation: logger.critical("An unexpected error occurred. Exegol cannot start the container after its creation...") From 705830b03fee82070d6bab3bed21787c6e06db06 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 24 Oct 2024 19:52:10 +0200 Subject: [PATCH 03/11] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index cf72eb36..084cb7e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ docker~=7.1.0 +podman~=5.2.0 requests~=2.32.3 rich~=13.7.1 GitPython~=3.1.43 From 43aa93609586cb1bff45fb0ad32c7e9406a74ff8 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 24 Oct 2024 19:54:03 +0200 Subject: [PATCH 04/11] Update setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 4cb8c89a..03739f5a 100644 --- a/setup.py +++ b/setup.py @@ -56,6 +56,7 @@ ], install_requires=[ 'docker~=7.1.0', + 'podman~=5.2.0', 'requests~=2.32.3', 'rich~=13.7.1', 'GitPython~=3.1.43', From 80cb0258fe905aee5c11512d08fe6328209b4961 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 24 Oct 2024 20:12:03 +0200 Subject: [PATCH 05/11] Update DockerUtils.py --- exegol/utils/DockerUtils.py | 43 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/exegol/utils/DockerUtils.py b/exegol/utils/DockerUtils.py index d422e61e..ce9536fe 100644 --- a/exegol/utils/DockerUtils.py +++ b/exegol/utils/DockerUtils.py @@ -6,13 +6,16 @@ import docker from docker import DockerClient from docker.errors import APIError, DockerException, NotFound, ImageNotFound -from docker.models.images import Image -from docker.models.volumes import Volume -from requests import ReadTimeout +from docker.models.images import Image as DockerImage +from docker.models.volumes import Volume as DockerVolume import podman from podman import PodmanClient from podman.errors import APIError as PodmanAPIError, DockerException as PodmanException, NotFound as PodmanNotFound, ImageNotFound as PodmanImageNotFound +from podman.domain.images import Image as PodmanImage +from podman.domain.volumes import Volume as PodmanVolume + +from requests import ReadTimeout from exegol.config.ConstantConfig import ConstantConfig from exegol.config.DataCache import DataCache @@ -143,9 +146,9 @@ def createContainer(self, model: ExegolContainerTemplate, temporary: bool = Fals model.prepare() logger.debug(model) # Preload docker volume before container creation - for volume in model.config.gets(): + for volume in model.config.getVolumes(): if volume.get('Type', '?') == "volume": - docker_volume = self.__loadDocker(volume_path=volume['Source'], volume_name=volume['Target']) + docker_volume = self.__loadDockerVolume(volume_path=volume['Source'], volume_name=volume['Target']) if docker_volume is None: logger.warning(f"Error while creating docker volume '{volume['Target']}'") entrypoint, command = model.config.getEntrypointCommand() @@ -172,7 +175,7 @@ def createContainer(self, model: ExegolContainerTemplate, temporary: bool = Fals "shm_size": model.config.shm_size, "stdin_open": model.config.interactive, "tty": model.config.tty, - "mounts": model.config.gets(), + "mounts": model.config.getVolumes(), "working_dir": model.config.getWorkingDir()} if temporary: # Only the 'run' function support the "remove" parameter @@ -238,9 +241,9 @@ def getContainer(self, tag: str) -> ExegolContainer: # In this case, ObjectNotFound is raised raise ObjectNotFound - # # # s Section # # # + # # # Volumes Section # # # - def __loadDocker(self, volume_path: str, volume_name: str) -> : + def __loadDockerVolume(self, volume_path: str, volume_name: str) -> Union[DockerVolume, PodmanVolume]: """Load or create a docker volume for exegol containers (must be created before the container, SDK limitation) Return the docker volume object""" @@ -256,7 +259,7 @@ def __loadDocker(self, volume_path: str, volume_name: str) -> : if path != volume_path: try: self.__client.api.remove_volume(name=volume_name) - raise NotFound(' must be reloaded') + raise NotFound('Volume must be reloaded') except (APIError, PodmanAPIError) as e: if e.status_code == 409: logger.warning("The path of the volume specified by the user is not the same as in the existing docker volume. " @@ -264,9 +267,9 @@ def __loadDocker(self, volume_path: str, volume_name: str) -> : logger.verbose("The volume is already used by some container and cannot be automatically removed.") logger.debug(e.explanation) else: - raise NotFound(' must be reloaded') + raise NotFound('Volume must be reloaded') except ReadTimeout: - logger.error(f"Received a timeout error, Docker is busy... {volume_name} cannot be automatically removed. Please, retry later the following command:{os.linesep}" + logger.error(f"Received a timeout error, Docker is busy... Volume {volume_name} cannot be automatically removed. Please, retry later the following command:{os.linesep}" f" [orange3]docker volume rm {volume_name}[/orange3]") except (NotFound, PodmanNotFound): try: @@ -283,7 +286,7 @@ def __loadDocker(self, volume_path: str, volume_name: str) -> : logger.critical(err.explanation) return None # type: ignore except ReadTimeout: - logger.critical(f"Received a timeout error, Docker is busy... {volume_name} cannot be created.") + logger.critical(f"Received a timeout error, Docker is busy... Volume {volume_name} cannot be created.") return # type: ignore except (APIError, PodmanAPIError) as err: logger.critical(f"Unexpected error by Docker SDK : {err}") @@ -380,7 +383,7 @@ def getInstalledImage(self, tag: str) -> ExegolImage: logger.critical(f"The desired image is not installed or do not exist ({ConstantConfig.IMAGE_NAME}:{tag}). Exiting.") return # type: ignore - def __listLocalImages(self, tag: Optional[str] = None) -> List[Image]: + def __listLocalImages(self, tag: Optional[str] = None) -> List[Union[DockerImage, PodmanImage]]: """List local docker images already installed. Return a list of docker images objects""" logger.debug("Fetching local image tags, digests (and other attributes)") @@ -418,7 +421,7 @@ def __listLocalImages(self, tag: Optional[str] = None) -> List[Image]: ids.add(img.id) return result - def __findLocalRecoveryImages(self, include_untag: bool = False) -> List[Image]: + def __findLocalRecoveryImages(self, include_untag: bool = False) -> List[Union[DockerImage, PodmanImage]]: """This method try to recovery untagged docker images. Set include_untag option to recover images with a valid RepoDigest (no not dangling) but without tag.""" try: @@ -512,13 +515,11 @@ def downloadImage(self, image: ExegolImage, install_mode: bool = False) -> bool: logger.debug(f"Downloading {ConstantConfig.IMAGE_NAME}:{name} ({image.getArch()})") try: ExegolTUI.downloadDockerLayer( - self.__client.images.pull( - repository=ConstantConfig.IMAGE_NAME, # Image name from config - tag=name, # Tag (e.g., 'latest') - stream=True, # Stream the output - decode=True, # Stream the output - platform="linux/" + image.getArch(), # Platform (e.g., 'linux/amd64') - )) + self.__client.api.pull(repository=ConstantConfig.IMAGE_NAME, + tag=name, + stream=True, + decode=True, + platform="linux/" + image.getArch())) logger.success(f"Image successfully {'installed' if install_mode else 'updated'}") # Remove old image if not install_mode and image.isInstall() and UserConfig().auto_remove_images: From 1ba8d0e760ec6b931e4288baaf277dfbedf0ad6b Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 24 Oct 2024 22:36:17 +0200 Subject: [PATCH 06/11] Update ContainerConfig.py --- exegol/model/ContainerConfig.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index c34ba06b..5f5f1027 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -10,8 +10,11 @@ from pathlib import Path, PurePath from typing import Optional, List, Dict, Union, Tuple, cast -from docker.models.containers import Container +from docker.models.containers import Container as DockerContainer from docker.types import Mount + +from podman.domain.containers import Container as PodmanContainer + from rich.prompt import Prompt from exegol.config.ConstantConfig import ConstantConfig @@ -81,7 +84,7 @@ class ExegolEnv(Enum): ExegolMetadata.comment.value: ["setComment", "getComment"], ExegolMetadata.password.value: ["setPasswd", "getPasswd"]} - def __init__(self, container: Optional[Container] = None): + def __init__(self, container: Optional[Union[DockerContainer, PodmanContainer]] = None): """Container config default value""" self.hostname = "" self.__enable_gui: bool = False @@ -132,7 +135,7 @@ def __init__(self, container: Optional[Container] = None): # ===== Config parsing section ===== - def __parseContainerConfig(self, container: Container): + def __parseContainerConfig(self, container: Union[DockerContainer, PodmanContainer]): """Parse Docker object to setup self configuration""" # Reset default attributes self.__passwd = None From bbb0603b306a42cea3ba510cc219d6286f0b9aa9 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 24 Oct 2024 22:57:10 +0200 Subject: [PATCH 07/11] Update ExegolImage.py --- exegol/model/ExegolImage.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/exegol/model/ExegolImage.py b/exegol/model/ExegolImage.py index 5bb3c1d3..e2331908 100644 --- a/exegol/model/ExegolImage.py +++ b/exegol/model/ExegolImage.py @@ -1,8 +1,11 @@ from datetime import datetime from typing import Optional, List, Dict, Any, Union -from docker.models.containers import Container -from docker.models.images import Image +from docker.models.containers import Container as DockerContainer +from docker.models.images import Image as DockerImage + +from podman.domain.containers import Container as PodmanContainer +from podman.domain.images import Image as PodmanImage from exegol.config.DataCache import DataCache from exegol.console import ConsoleFormat @@ -23,7 +26,7 @@ def __init__(self, dockerhub_data: Optional[Dict[str, Any]] = None, meta_img: Optional[MetaImages] = None, image_id: Optional[str] = None, - docker_image: Optional[Image] = None, + docker_image: Optional[Union[DockerImage, PodmanImage]] = None, isUpToDate: bool = False): """Docker image default value""" # Prepare parameters @@ -35,7 +38,7 @@ def __init__(self, version_parsed = MetaImages.tagNameParsing(name) self.__version_specific = bool(version_parsed) # Init attributes - self.__image: Optional[Image] = docker_image + self.__image: Optional[Union[DockerImage, PodmanImage]] = docker_image self.__name: str = name self.__alt_name: str = '' self.__arch = "" @@ -148,7 +151,7 @@ def resetDockerImage(self): self.__build_date = "[bright_black]N/A[/bright_black]" self.__disk_size = "[bright_black]N/A[/bright_black]" - def setDockerObject(self, docker_image: Image): + def setDockerObject(self, docker_image: Union[DockerImage, PodmanImage]): """Docker object setter. Parse object to set up self configuration.""" self.__image = docker_image # When a docker image exist, image is locally installed @@ -226,7 +229,7 @@ def __labelVersionParsing(self): self.__profile_version = self.__image_version @classmethod - def parseAliasTagName(cls, image: Image) -> str: + def parseAliasTagName(cls, image: Union[DockerImage, PodmanImage]) -> str: """Create a tag name alias from labels when image's tag is lost""" return image.labels.get("org.exegol.tag", "") + "-" + image.labels.get("org.exegol.version", "v?") @@ -243,7 +246,7 @@ def syncStatus(self): else: self.__custom_status = "" - def syncContainerData(self, container: Container): + def syncContainerData(self, container: Union[DockerContainer, PodmanContainer]): """Synchronization between the container and the image. If the image has been updated, the tag is lost, but it is saved in the properties of the container that still uses it.""" @@ -346,7 +349,7 @@ def __mergeMetaImages(cls, images: List[MetaImages]): pass @classmethod - def mergeImages(cls, remote_images: List[MetaImages], local_images: List[Image]) -> List['ExegolImage']: + def mergeImages(cls, remote_images: List[MetaImages], local_images: List[Union[DockerImage, PodmanImage]]) -> List['ExegolImage']: """Compare and merge local images and remote images. Use case to process : - up-to-date : "Version specific" image can use exact digest_id matching. Latest image must match corresponding tag @@ -531,7 +534,7 @@ def __setDigest(self, digest: Optional[str]): self.__digest = digest @staticmethod - def __parseDigest(docker_image: Image) -> str: + def __parseDigest(docker_image: Union[DockerImage, PodmanImage]) -> str: """Parse the remote image digest ID. Return digest id from the docker object.""" for digest_id in docker_image.attrs["RepoDigests"]: From 0c59551e10b72077a2303c9bf40ee267925bcc84 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 24 Oct 2024 23:03:31 +0200 Subject: [PATCH 08/11] Update MetaImages.py --- exegol/model/MetaImages.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/exegol/model/MetaImages.py b/exegol/model/MetaImages.py index 779fac5d..fc73ce74 100644 --- a/exegol/model/MetaImages.py +++ b/exegol/model/MetaImages.py @@ -1,6 +1,7 @@ from typing import Optional, Set, Union -from docker.models.images import Image +from docker.models.images import Image as DockerImage +from podman.domain.images import Image as PodmanImage from exegol.utils.ExeLog import logger from exegol.utils.WebUtils import WebUtils @@ -58,13 +59,13 @@ def tagNameParsing(tag_name: str) -> str: return version @staticmethod - def parseArch(docker_image: Union[dict, Image]) -> str: + def parseArch(docker_image: Union[dict, DockerImage, PodmanImage]) -> str: """Parse and format arch in dockerhub style from registry dict struct. Return arch in format 'arch/variant'.""" arch_key = "architecture" variant_key = "variant" - # Support Docker image struct with specific dict key - if type(docker_image) is Image: + # Support Docker and Podman image struct with specific dict key + if isinstance(docker_image, (DockerImage, PodmanImage)): docker_image = docker_image.attrs arch_key = "Architecture" variant_key = "Variant" From 48794f2b05d7d5c58f1e4803a7a6e0b52faf1866 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 24 Oct 2024 23:09:29 +0200 Subject: [PATCH 09/11] Update ContainerLogStream.py --- exegol/utils/ContainerLogStream.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/exegol/utils/ContainerLogStream.py b/exegol/utils/ContainerLogStream.py index 51b278f8..e5570a82 100644 --- a/exegol/utils/ContainerLogStream.py +++ b/exegol/utils/ContainerLogStream.py @@ -5,15 +5,16 @@ from datetime import datetime, timedelta from typing import Union, List, Any, Optional -from docker.models.containers import Container -from docker.types import CancellableStream +from docker.models.containers import Container as DockerContainer + +from podman.domain.containers import Container as PodmanContainer from exegol.utils.ExeLog import logger class ContainerLogStream: - def __init__(self, container: Container, start_date: Optional[datetime] = None, timeout: int = 5): + def __init__(self, container: Union[DockerContainer, PodmanContainer], start_date: Optional[datetime] = None, timeout: int = 5): # Container to extract logs from self.__container = container # Fetch more logs from this datetime From c4c21fdc27baeb944335c9f28b713409584dcee1 Mon Sep 17 00:00:00 2001 From: Antonio Date: Fri, 25 Oct 2024 01:09:11 +0200 Subject: [PATCH 10/11] Update DockerUtils.py --- exegol/utils/DockerUtils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exegol/utils/DockerUtils.py b/exegol/utils/DockerUtils.py index ce9536fe..d9a39f21 100644 --- a/exegol/utils/DockerUtils.py +++ b/exegol/utils/DockerUtils.py @@ -404,7 +404,7 @@ def __listLocalImages(self, tag: Optional[str] = None) -> List[Union[DockerImage for img in images: # len tags = 0 handle exegol images (nightly image lost their tag after update) if len(img.attrs.get('RepoTags', [])) == 0 or \ - ConstantConfig.IMAGE_NAME in [repo_tag.split(':')[0] for repo_tag in img.attrs.get("RepoTags", [])]: + any(ConstantConfig.IMAGE_NAME in repo_tag.split(':')[0] for repo_tag in img.attrs.get("RepoTags", [])): result.append(img) ids.add(img.id) From 95abd54f1fd6e4f7cbe4e66f64d63e6ab2f1ec48 Mon Sep 17 00:00:00 2001 From: Antonio Date: Fri, 25 Oct 2024 01:09:32 +0200 Subject: [PATCH 11/11] Update ExegolImage.py --- exegol/model/ExegolImage.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/exegol/model/ExegolImage.py b/exegol/model/ExegolImage.py index e2331908..279b52d7 100644 --- a/exegol/model/ExegolImage.py +++ b/exegol/model/ExegolImage.py @@ -538,7 +538,7 @@ def __parseDigest(docker_image: Union[DockerImage, PodmanImage]) -> str: """Parse the remote image digest ID. Return digest id from the docker object.""" for digest_id in docker_image.attrs["RepoDigests"]: - if digest_id.startswith(ConstantConfig.IMAGE_NAME): # Find digest id from the right repository + if ConstantConfig.IMAGE_NAME in digest_id: # Find digest id from the right repository return digest_id.split('@')[1] return "" @@ -555,9 +555,14 @@ def getLatestRemoteId(self) -> str: return self.__profile_digest def __setImageId(self, image_id: Optional[str]): - """Local image id setter""" + """Local image id setter for both Docker and Podman""" if image_id is not None: - self.__image_id = image_id.split(":")[1][:12] + # Check if the image_id contains a colon (as in Docker's format) + if ":" in image_id: + self.__image_id = image_id.split(":")[1][:12] + else: + # For Podman, where image_id does not contain the 'sha256:' prefix + self.__image_id = image_id[:12] def getLocalId(self) -> str: """Local id getter"""