From d140d791550f5b7922c2ff9905e6ef24c423830b Mon Sep 17 00:00:00 2001 From: Dramelac Date: Sat, 20 Apr 2024 19:48:38 +0200 Subject: [PATCH 01/14] Update error message --- exegol/model/ContainerConfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index 103a90cf..4463ff3f 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -1005,8 +1005,8 @@ def addVolume(self, match = True break if not match: - logger.critical(f"Bind volume from {host_path} is not possible, Docker Desktop configuration is incorrect. " - f"You need to modify the config to share a parent directory in [magenta]Docker Desktop > Preferences > Resources > File Sharing[/magenta].") + logger.error(f"Bind volume from {host_path} is not possible, Docker Desktop configuration is [red]incorrect[/red].") + logger.critical(f"You need to modify the [green]Docker Desktop[/green] config and [green]add[/green] this path (or the root directory) in [magenta]Docker Desktop > Preferences > Resources > File Sharing[/magenta] configuration.") # Choose to update fs directory perms if available and depending on user choice # if force_sticky_group is set, user choice is bypassed, fs will be updated. execute_update_fs = force_sticky_group or (enable_sticky_group and (UserConfig().auto_update_workspace_fs ^ ParametersManager().update_fs_perms)) From 5cd92178229fb0e3b05e845387872822ba74aec7 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Wed, 1 May 2024 13:56:22 +0200 Subject: [PATCH 02/14] Handle none config parser --- exegol/utils/DataFileUtils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/exegol/utils/DataFileUtils.py b/exegol/utils/DataFileUtils.py index cfedc5d4..c5bd4031 100644 --- a/exegol/utils/DataFileUtils.py +++ b/exegol/utils/DataFileUtils.py @@ -87,8 +87,12 @@ def _parse_config(self): logger.error("Error while parsing exegol config file ! Check for syntax error.") except JSONDecodeError: logger.error(f"Error while parsing exegol data file {self._file_path} ! Check for syntax error.") - self._raw_data = data - self._process_data() + if data is None: + logger.warning(f"Exegol was unable to load the file {self._file_path}. Restoring it to its original state.") + self._create_config_file() + else: + self._raw_data = data + self._process_data() def __load_config(self, data: dict, config_name: str, default: Union[bool, str], choices: Optional[Set[str]] = None) -> Union[bool, str]: From 0748f8614a4d83474d717fb4b9affb8f7884eefb Mon Sep 17 00:00:00 2001 From: Dramelac Date: Wed, 8 May 2024 19:41:12 +0200 Subject: [PATCH 03/14] Fix git branch name --- exegol/utils/GitUtils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exegol/utils/GitUtils.py b/exegol/utils/GitUtils.py index 868d7b56..a924da5e 100644 --- a/exegol/utils/GitUtils.py +++ b/exegol/utils/GitUtils.py @@ -162,7 +162,7 @@ def listBranch(self) -> List[str]: logger.warning(f"Branch name is not correct: {branch.name}") result.append(branch.name) else: - result.append(branch_parts[1]) + result.append('/'.join(branch_parts[1:])) return result def safeCheck(self) -> bool: From ff6d29a7c46029d7b2d4a76857899ed8ac631739 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Wed, 8 May 2024 19:47:38 +0200 Subject: [PATCH 04/14] Fix push pip only on tag --- .github/workflows/entrypoint_nightly.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/entrypoint_nightly.yml b/.github/workflows/entrypoint_nightly.yml index 74c45fe3..ad37f42c 100644 --- a/.github/workflows/entrypoint_nightly.yml +++ b/.github/workflows/entrypoint_nightly.yml @@ -36,6 +36,8 @@ jobs: run: python -m build --sdist --outdir dist/ . - name: Publish distribution 📦 to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') with: repository-url: https://test.pypi.org/legacy/ skip-existing: true + verbose: true From 8ba3474335130f616301274fbf006a06a9831489 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Wed, 8 May 2024 20:09:24 +0200 Subject: [PATCH 05/14] New beta version + filter release tag --- .github/workflows/entrypoint_release.yml | 2 +- exegol/config/ConstantConfig.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/entrypoint_release.yml b/.github/workflows/entrypoint_release.yml index f290f02f..36e59027 100644 --- a/.github/workflows/entrypoint_release.yml +++ b/.github/workflows/entrypoint_release.yml @@ -3,7 +3,7 @@ name: Release on: push: tags: - - '*' + - '[0-9]+.[0-9]+.[0-9]+' jobs: test: diff --git a/exegol/config/ConstantConfig.py b/exegol/config/ConstantConfig.py index 684fb282..3110d10e 100644 --- a/exegol/config/ConstantConfig.py +++ b/exegol/config/ConstantConfig.py @@ -5,7 +5,7 @@ class ConstantConfig: """Constant parameters information""" # Exegol Version - version: str = "4.3.2" + version: str = "4.3.3b1" # Exegol documentation link documentation: str = "https://exegol.rtfd.io/" From c3a3e4554ba10e9c4bcd306236ee2958385260f6 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Sun, 12 May 2024 19:39:18 +0200 Subject: [PATCH 06/14] Refacto dockerutils to singleton --- exegol/console/cli/ExegolCompleter.py | 2 +- exegol/manager/ExegolController.py | 6 + exegol/manager/ExegolManager.py | 41 ++--- exegol/manager/UpdateManager.py | 16 +- exegol/utils/DockerUtils.py | 238 ++++++++++++-------------- exegol/utils/MetaSingleton.py | 2 +- 6 files changed, 145 insertions(+), 160 deletions(-) diff --git a/exegol/console/cli/ExegolCompleter.py b/exegol/console/cli/ExegolCompleter.py index e2beca5c..81d790e0 100644 --- a/exegol/console/cli/ExegolCompleter.py +++ b/exegol/console/cli/ExegolCompleter.py @@ -11,7 +11,7 @@ def ContainerCompleter(prefix: str, parsed_args: Namespace, **kwargs) -> Tuple[str, ...]: """Function to dynamically load a container list for CLI autocompletion purpose""" - data = [c.name for c in DockerUtils.listContainers()] + data = [c.name for c in DockerUtils().listContainers()] for obj in data: # filter data if needed if prefix and not obj.lower().startswith(prefix.lower()): diff --git a/exegol/manager/ExegolController.py b/exegol/manager/ExegolController.py index 169594b1..87c95fe2 100644 --- a/exegol/manager/ExegolController.py +++ b/exegol/manager/ExegolController.py @@ -1,3 +1,6 @@ +from exegol.manager.ExegolManager import ExegolManager +from exegol.utils.DockerUtils import DockerUtils + try: import docker import requests @@ -32,6 +35,9 @@ class ExegolController: def call_action(cls): """Dynamically retrieve the main function corresponding to the action selected by the user and execute it on the main thread""" + ExegolManager.print_version() + DockerUtils() # Init dockerutils + ExegolManager.print_debug_banner() # Check for missing parameters missing_params = cls.__action.check_parameters() if len(missing_params) == 0: diff --git a/exegol/manager/ExegolManager.py b/exegol/manager/ExegolManager.py index 6745eb68..5b72e8da 100644 --- a/exegol/manager/ExegolManager.py +++ b/exegol/manager/ExegolManager.py @@ -37,7 +37,6 @@ class ExegolManager: @classmethod def info(cls): """Print a list of available images and containers on the current host""" - ExegolManager.print_version() if logger.isEnabledFor(ExeLog.VERBOSE): logger.verbose("Listing user configurations") ExegolTUI.printTable(UserConfig().get_configs(), title="[not italic]:brain: [/not italic][gold3][g]User configurations[/g][/gold3]") @@ -53,8 +52,8 @@ def info(cls): else: # Without any parameter, show all images and containers info # Fetch data - images = DockerUtils.listImages(include_version_tag=False) - containers = DockerUtils.listContainers() + images = DockerUtils().listImages(include_version_tag=False) + containers = DockerUtils().listContainers() # List and print images color = ConsoleFormat.getArchColor(ParametersManager().arch) logger.verbose(f"Listing local and remote Exegol images (filtering for architecture [{color}]{ParametersManager().arch}[/{color}])") @@ -68,7 +67,6 @@ def info(cls): @classmethod def start(cls): """Create and/or start an exegol container to finally spawn an interactive shell""" - ExegolManager.print_version() logger.info("Starting exegol") # Check if the first positional parameter have been supplied cls.__interactive_mode = not bool(ParametersManager().containertag) @@ -86,7 +84,6 @@ def start(cls): def exec(cls): """Create and/or start an exegol container to execute a specific command. The execution can be seen in console output or be relayed in the background as a daemon.""" - ExegolManager.print_version() logger.info("Starting exegol") if ParametersManager().tmp: container = cls.__createTmpContainer(ParametersManager().selector) @@ -103,7 +100,6 @@ def exec(cls): @classmethod def stop(cls): """Stop an exegol container""" - ExegolManager.print_version() logger.info("Stopping exegol") container = cls.__loadOrCreateContainer(multiple=True, must_exist=True) assert container is not None and type(container) is list @@ -113,7 +109,6 @@ def stop(cls): @classmethod def restart(cls): """Stop and start an exegol container""" - ExegolManager.print_version() container = cast(ExegolContainer, cls.__loadOrCreateContainer(must_exist=True)) if container: container.stop(timeout=5) @@ -124,7 +119,6 @@ def restart(cls): @classmethod def install(cls): """Pull or build a docker exegol image""" - ExegolManager.print_version() try: if not ExegolModules().isExegolResourcesReady(): raise CancelOperation @@ -136,7 +130,6 @@ def install(cls): @classmethod def update(cls): """Update python wrapper (git installation required) and Pull a docker exegol image""" - ExegolManager.print_version() if ParametersManager().offline_mode: logger.critical("It's not possible to update Exegol in offline mode. Please retry later with an internet connection.") if not ParametersManager().skip_git: @@ -149,7 +142,6 @@ def update(cls): @classmethod def uninstall(cls): """Remove an exegol image""" - ExegolManager.print_version() logger.info("Uninstalling an exegol image") # Set log level to verbose in order to show every image installed including the outdated. if not logger.isEnabledFor(ExeLog.VERBOSE): @@ -165,12 +157,11 @@ def uninstall(cls): logger.error("Aborting operation.") return for img in images: - DockerUtils.removeImage(img) + DockerUtils().removeImage(img) @classmethod def remove(cls): """Remove an exegol container""" - ExegolManager.print_version() logger.info("Removing an exegol container") containers = cls.__loadOrCreateContainer(multiple=True, must_exist=True) assert type(containers) is list @@ -187,7 +178,7 @@ def remove(cls): c.remove() # If the image used is deprecated, it must be deleted after the removal of its container if c.image.isLocked() and UserConfig().auto_remove_images: - DockerUtils.removeImage(c.image, upgrade_mode=True) + DockerUtils().removeImage(c.image, upgrade_mode=True) @classmethod def print_version(cls): @@ -210,6 +201,10 @@ def print_version(cls): elif 'b' in ConstantConfig.version: logger.empty_line() logger.warning("You are currently using a [orange3]Beta[/orange3] version of Exegol, which may be unstable.") + + @classmethod + def print_debug_banner(cls): + """Print header debug info""" logger.debug(f"Pip installation: {boolFormatter(ConstantConfig.pip_installed)}") logger.debug(f"Git source installation: {boolFormatter(ConstantConfig.git_source_installation)}") logger.debug(f"Host OS: {EnvInfo.getHostOs().value} [bright_black]({EnvInfo.getDockerEngine().value})[/bright_black]") @@ -265,9 +260,9 @@ def __loadOrInstallImage(cls, if multiple: image_selection = [] for image_tag in image_tags: - image_selection.append(DockerUtils.getInstalledImage(image_tag)) + image_selection.append(DockerUtils().getInstalledImage(image_tag)) else: - image_selection = DockerUtils.getInstalledImage(image_tag) + image_selection = DockerUtils().getInstalledImage(image_tag) except ObjectNotFound: # ObjectNotFound is raised when the image_tag provided by the user does not match any existing image. if image_tag is not None: @@ -342,9 +337,9 @@ def __checkImageInstallationStatus(cls, # Check if the selected image is installed and install it logger.warning("The selected image is not installed.") # Download remote image - if DockerUtils.downloadImage(check_img[i], install_mode=True): + if DockerUtils().downloadImage(check_img[i], install_mode=True): # Select installed image - check_img[i] = DockerUtils.getInstalledImage(check_img[i].getName()) + check_img[i] = DockerUtils().getInstalledImage(check_img[i].getName()) else: logger.error("This image cannot be installed.") return False, None @@ -381,7 +376,7 @@ def __loadOrCreateContainer(cls, # test each user tag for container_tag in container_tags: try: - cls.__container.append(DockerUtils.getContainer(container_tag)) + cls.__container.append(DockerUtils().getContainer(container_tag)) except ObjectNotFound: # on multi select, an object not found is not critical if must_exist: @@ -393,7 +388,7 @@ def __loadOrCreateContainer(cls, raise NotImplemented else: assert container_tag is not None - cls.__container = DockerUtils.getContainer(container_tag) + cls.__container = DockerUtils().getContainer(container_tag) except (ObjectNotFound, IndexError): # ObjectNotFound is raised when the container_tag provided by the user does not match any existing container. # IndexError is raise when no container exist (raised from TUI interactive selection) @@ -417,10 +412,10 @@ def __interactiveSelection(cls, # Object listing depending on the type if object_type is ExegolContainer: # List all images available - object_list = DockerUtils.listContainers() + object_list = DockerUtils().listContainers() elif object_type is ExegolImage: # List all images available - object_list = DockerUtils.listInstalledImages() if must_exist else DockerUtils.listImages() + object_list = DockerUtils().listInstalledImages() if must_exist else DockerUtils().listImages() else: logger.critical("Unknown object type during interactive selection. Exiting.") raise Exception @@ -517,7 +512,7 @@ def __createContainer(cls, name: Optional[str]) -> ExegolContainer: logger.info("To use exegol [orange3]without interaction[/orange3], " "read CLI options with [green]exegol start -h[/green]") - container = DockerUtils.createContainer(model) + container = DockerUtils().createContainer(model) container.postCreateSetup() return container @@ -543,7 +538,7 @@ def __createTmpContainer(cls, image_name: Optional[str] = None) -> ExegolContain # Mount entrypoint as a volume (because in tmp mode the container is created with run instead of create method) model.config.addVolume(str(ConstantConfig.entrypoint_context_path_obj), "/.exegol/entrypoint.sh", must_exist=True, read_only=True) - container = DockerUtils.createContainer(model, temporary=True) + container = DockerUtils().createContainer(model, temporary=True) container.postCreateSetup(is_temporary=True) return container diff --git a/exegol/manager/UpdateManager.py b/exegol/manager/UpdateManager.py index 42a26629..55626948 100644 --- a/exegol/manager/UpdateManager.py +++ b/exegol/manager/UpdateManager.py @@ -33,9 +33,9 @@ def updateImage(cls, tag: Optional[str] = None, install_mode: bool = False) -> O if tag is None: # Filter for updatable images if install_mode: - available_images = [i for i in DockerUtils.listImages() if not i.isLocked()] + available_images = [i for i in DockerUtils().listImages() if not i.isLocked()] else: - available_images = [i for i in DockerUtils.listImages() if i.isInstall() and not i.isUpToDate() and not i.isLocked()] + available_images = [i for i in DockerUtils().listImages() if i.isInstall() and not i.isUpToDate() and not i.isLocked()] if len(available_images) == 0: logger.success("All images already installed are up to date!") return None @@ -55,7 +55,7 @@ def updateImage(cls, tag: Optional[str] = None, install_mode: bool = False) -> O else: try: # Find image by name - selected_image = DockerUtils.getImage(tag) + selected_image = DockerUtils().getImage(tag) except ObjectNotFound: # If the image do not exist, ask to build it if install_mode: @@ -66,13 +66,13 @@ def updateImage(cls, tag: Optional[str] = None, install_mode: bool = False) -> O if selected_image is not None and type(selected_image) is ExegolImage: # Update existing ExegolImage - if DockerUtils.downloadImage(selected_image, install_mode): + if DockerUtils().downloadImage(selected_image, install_mode): sync_result = None # Name comparison allow detecting images without version tag if not selected_image.isVersionSpecific() and selected_image.getName() != selected_image.getLatestVersionName(): with console.status(f"Synchronizing version tag information. Please wait.", spinner_style="blue"): # Download associated version tag. - sync_result = DockerUtils.downloadVersionTag(selected_image) + sync_result = DockerUtils().downloadVersionTag(selected_image) # Detect if an error have been triggered during the download if type(sync_result) is str: logger.error(f"Error while downloading version tag, {sync_result}") @@ -80,7 +80,7 @@ def updateImage(cls, tag: Optional[str] = None, install_mode: bool = False) -> O # if version tag have been successfully download, returning ExegolImage from docker response if sync_result is not None and type(sync_result) is ExegolImage: return sync_result - return DockerUtils.getInstalledImage(selected_image.getName()) + return DockerUtils().getInstalledImage(selected_image.getName()) elif type(selected_image) is str: # Build a new image using TUI selected name, confirmation has already been requested by TUI return cls.buildAndLoad(selected_image) @@ -368,14 +368,14 @@ def __buildSource(cls, build_name: Optional[str] = None) -> str: title="[not italic]:dog: [/not italic][gold3]Profile[/gold3]")) logger.debug(f"Using {build_profile} build profile ({build_dockerfile})") # Docker Build - DockerUtils.buildImage(tag=build_name, build_profile=build_profile, build_dockerfile=build_dockerfile, dockerfile_path=build_path.as_posix()) + DockerUtils().buildImage(tag=build_name, build_profile=build_profile, build_dockerfile=build_dockerfile, dockerfile_path=build_path.as_posix()) return build_name @classmethod def buildAndLoad(cls, tag: str): """Build an image and load it""" build_name = cls.__buildSource(tag) - return DockerUtils.getInstalledImage(build_name) + return DockerUtils().getInstalledImage(build_name) @classmethod def listBuildProfiles(cls, profiles_path: Path = ConstantConfig.build_context_path_obj) -> Dict: diff --git a/exegol/utils/DockerUtils.py b/exegol/utils/DockerUtils.py index dac38be9..60c21aca 100644 --- a/exegol/utils/DockerUtils.py +++ b/exegol/utils/DockerUtils.py @@ -22,61 +22,61 @@ from exegol.model.ExegolImage import ExegolImage from exegol.model.MetaImages import MetaImages from exegol.utils.ExeLog import logger, console, ExeLog +from exegol.utils.MetaSingleton import MetaSingleton from exegol.utils.WebUtils import WebUtils # SDK Documentation : https://docker-py.readthedocs.io/en/stable/index.html -class DockerUtils: - """Utility class between exegol and the Docker SDK""" - try: - # Connect Docker SDK to the local docker instance. - # Docker connection setting is loaded from the user environment variables. - __client: DockerClient = docker.from_env() - # Check if the docker daemon is serving linux container - __daemon_info = __client.info() - if __daemon_info.get("OSType", "linux").lower() != "linux": - logger.critical( - f"Docker daemon is not serving linux container ! Docker OS Type is: {__daemon_info.get('OSType', 'linux')}") - EnvInfo.initData(__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") - else: - logger.error(err) - logger.critical( - "Unable to connect to docker (from env config). Is docker operational and accessible? on your machine? " - "Exiting.") - __images: Optional[List[ExegolImage]] = None - __containers: Optional[List[ExegolContainer]] = None +class DockerUtils(metaclass=MetaSingleton): + + def __init__(self): + """Utility class between exegol and the Docker SDK""" + 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() + # 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") + else: + logger.error(err) + logger.critical( + "Unable to connect to docker (from env config). Is docker operational and accessible? on your machine? " + "Exiting.") + self.__images: Optional[List[ExegolImage]] = None + self.__containers: Optional[List[ExegolContainer]] = None - @classmethod - def clearCache(cls): + 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""" - cls.__containers = None - cls.__images = None + self.__containers = None + self.__images = None - @classmethod - def getDockerInfo(cls) -> dict: + def getDockerInfo(self) -> dict: """Fetch info from docker daemon""" - return cls.__daemon_info + return self.__daemon_info # # # Container Section # # # - @classmethod - def listContainers(cls) -> List[ExegolContainer]: + def listContainers(self) -> List[ExegolContainer]: """List available docker containers. Return a list of ExegolContainer""" - if cls.__containers is None: - cls.__containers = [] + if self.__containers is None: + self.__containers = [] try: - docker_containers = cls.__client.containers.list(all=True, filters={"name": "exegol-"}) + docker_containers = self.__client.containers.list(all=True, filters={"name": "exegol-"}) except APIError as err: logger.debug(err) logger.critical(err.explanation) @@ -86,11 +86,10 @@ def listContainers(cls) -> List[ExegolContainer]: logger.critical("Received a timeout error, Docker is busy... Unable to list containers, retry later.") return # type: ignore for container in docker_containers: - cls.__containers.append(ExegolContainer(container)) - return cls.__containers + self.__containers.append(ExegolContainer(container)) + return self.__containers - @classmethod - def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False) -> ExegolContainer: + def createContainer(self, model: ExegolContainerTemplate, temporary: bool = False) -> ExegolContainer: """Create an Exegol container from an ExegolContainerTemplate configuration. Return an ExegolContainer if the creation was successful.""" logger.info("Creating new exegol container") @@ -99,7 +98,7 @@ def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False # Preload docker volume before container creation for volume in model.config.getVolumes(): if volume.get('Type', '?') == "volume": - docker_volume = cls.__loadDockerVolume(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() @@ -107,7 +106,7 @@ def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False logger.debug(f"Cmd: {command}") # The 'create' function must be called to create a container without starting it # in order to hot patch the entrypoint.sh with wrapper features (the container will be started after postCreateSetup) - docker_create_function = cls.__client.containers.create + docker_create_function = self.__client.containers.create docker_args = {"image": model.image.getDockerRef(), "entrypoint": entrypoint, "command": command, @@ -130,7 +129,7 @@ def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False "working_dir": model.config.getWorkingDir()} if temporary: # Only the 'run' function support the "remove" parameter - docker_create_function = cls.__client.containers.run + docker_create_function = self.__client.containers.run docker_args["remove"] = temporary docker_args["auto_remove"] = temporary try: @@ -143,7 +142,7 @@ def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False logger.debug(err) model.rollback() try: - container = cls.__client.containers.list(all=True, filters={"name": model.container_name}) + container = self.__client.containers.list(all=True, filters={"name": model.container_name}) if container is not None and len(container) > 0: for c in container: if c.name == model.container_name: # Search for exact match @@ -162,12 +161,11 @@ def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False return # type: ignore return ExegolContainer(container, model) - @classmethod - def getContainer(cls, tag: str) -> ExegolContainer: + def getContainer(self, tag: str) -> ExegolContainer: """Get an ExegolContainer from tag name.""" try: # Fetch potential container match from DockerSDK - container = cls.__client.containers.list(all=True, filters={"name": f"exegol-{tag}"}) + container = self.__client.containers.list(all=True, filters={"name": f"exegol-{tag}"}) except APIError as err: logger.debug(err) logger.critical(err.explanation) @@ -181,7 +179,7 @@ def getContainer(cls, tag: str) -> ExegolContainer: # If the user's input didn't match any container, try to force the name in lowercase if not already tried lowered_tag = tag.lower() if lowered_tag != tag: - return cls.getContainer(lowered_tag) + return self.getContainer(lowered_tag) raise ObjectNotFound # Filter results with exact name matching for c in container: @@ -195,8 +193,7 @@ def getContainer(cls, tag: str) -> ExegolContainer: # # # Volumes Section # # # - @classmethod - def __loadDockerVolume(cls, volume_path: str, volume_name: str) -> Volume: + def __loadDockerVolume(self, volume_path: str, volume_name: str) -> Volume: """Load or create a docker volume for exegol containers (must be created before the container, SDK limitation) Return the docker volume object""" @@ -207,11 +204,11 @@ def __loadDockerVolume(cls, volume_path: str, volume_name: str) -> Volume: logger.critical(f"Insufficient permission to create the folder: {volume_path}") try: # Check if volume already exist - volume = cls.__client.volumes.get(volume_name) + volume = self.__client.volumes.get(volume_name) path = volume.attrs.get('Options', {}).get('device', '') if path != volume_path: try: - cls.__client.api.remove_volume(name=volume_name) + self.__client.api.remove_volume(name=volume_name) raise NotFound('Volume must be reloaded') except APIError as e: if e.status_code == 409: @@ -229,10 +226,10 @@ def __loadDockerVolume(cls, volume_path: str, volume_name: str) -> Volume: # Creating a docker volume bind to a host path # Docker volume are more easily shared by container # Docker volume can load data from container image on host's folder creation - volume = cls.__client.volumes.create(volume_name, driver="local", - driver_opts={'o': 'bind', - 'device': volume_path, - 'type': 'none'}) + volume = self.__client.volumes.create(volume_name, driver="local", + driver_opts={'o': 'bind', + 'device': volume_path, + 'type': 'none'}) except APIError as err: logger.error(f"Error while creating docker volume '{volume_name}'.") logger.debug(err) @@ -251,15 +248,14 @@ def __loadDockerVolume(cls, volume_path: str, volume_name: str) -> Volume: # # # Image Section # # # - @classmethod - def listImages(cls, include_version_tag: bool = False, include_locked: bool = False) -> List[ExegolImage]: + def listImages(self, include_version_tag: bool = False, include_locked: bool = False) -> List[ExegolImage]: """List available docker images. Return a list of ExegolImage""" - if cls.__images is None: - remote_images = cls.__listRemoteImages() - local_images = cls.__listLocalImages() - cls.__images = ExegolImage.mergeImages(remote_images, local_images) - result = cls.__images + if self.__images is None: + remote_images = self.__listRemoteImages() + local_images = self.__listLocalImages() + self.__images = ExegolImage.mergeImages(remote_images, local_images) + result = self.__images assert result is not None # Caching latest images DataCache().update_image_cache([img for img in result if not img.isVersionSpecific()]) @@ -271,19 +267,17 @@ def listImages(cls, include_version_tag: bool = False, include_locked: bool = Fa result = [img for img in result if not img.isVersionSpecific() or img.isInstall()] return result - @classmethod - def listInstalledImages(cls) -> List[ExegolImage]: + def listInstalledImages(self) -> List[ExegolImage]: """List installed docker images. Return a list of ExegolImage""" - images = cls.listImages() + images = self.listImages() # Selecting only installed image return [img for img in images if img.isInstall()] - @classmethod - def getImage(cls, tag: str) -> ExegolImage: + def getImage(self, tag: str) -> ExegolImage: """Get an ExegolImage from tag name.""" # Fetch every images available - images = cls.listImages(include_version_tag=True, include_locked=True) + images = self.listImages(include_version_tag=True, include_locked=True) match: Optional[ExegolImage] = None # Find a match for i in images: @@ -300,19 +294,18 @@ def getImage(cls, tag: str) -> ExegolImage: # If there is no match at all, raise ObjectNotFound to handle the error raise ObjectNotFound - @classmethod - def getInstalledImage(cls, tag: str) -> ExegolImage: + def getInstalledImage(self, tag: str) -> ExegolImage: """Get an already installed ExegolImage from tag name.""" try: - if cls.__images is None: + if self.__images is None: try: - docker_local_image = cls.__client.images.get(f"{ConstantConfig.IMAGE_NAME}:{tag}") + 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: 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.") - recovery_images = cls.__findLocalRecoveryImages(include_untag=True) + recovery_images = self.__findLocalRecoveryImages(include_untag=True) match = [] for img in recovery_images: if ExegolImage.parseAliasTagName(img) == tag: @@ -330,24 +323,23 @@ def getInstalledImage(cls, tag: str) -> ExegolImage: return # type: ignore return ExegolImage(docker_image=docker_local_image).autoLoad() else: - for img in cls.__images: + for img in self.__images: if img.getName() == tag: if not img.isInstall() or not img.isUpToDate(): # Refresh local image status in case of installation/upgrade operations - cls.__findImageMatch(img) + self.__findImageMatch(img) return img except ObjectNotFound: logger.critical(f"The desired image is not installed or do not exist ({ConstantConfig.IMAGE_NAME}:{tag}). Exiting.") return # type: ignore - @classmethod - def __listLocalImages(cls, tag: Optional[str] = None) -> List[Image]: + def __listLocalImages(self, tag: Optional[str] = None) -> List[Image]: """List local docker images already installed. Return a list of docker images objects""" logger.debug("Fetching local image tags, digests (and other attributes)") try: image_name = ConstantConfig.IMAGE_NAME + ("" if tag is None else f":{tag}") - images = cls.__client.images.list(image_name, filters={"dangling": False}) + images = self.__client.images.list(image_name, filters={"dangling": False}) except APIError as err: logger.debug(err) logger.critical(err.explanation) @@ -367,7 +359,7 @@ def __listLocalImages(cls, tag: Optional[str] = None) -> List[Image]: ids.add(img.id) # Try to find lost Exegol images - recovery_images = cls.__findLocalRecoveryImages() + recovery_images = self.__findLocalRecoveryImages() for img in recovery_images: # Docker can keep track of 2 images maximum with RepoTag or RepoDigests, after it's hard to track origin without labels, so this recovery option is "best effort" if img.id in ids: @@ -379,15 +371,14 @@ def __listLocalImages(cls, tag: Optional[str] = None) -> List[Image]: ids.add(img.id) return result - @classmethod - def __findLocalRecoveryImages(cls, include_untag: bool = False) -> List[Image]: + def __findLocalRecoveryImages(self, include_untag: bool = False) -> List[Image]: """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: # Try to find lost Exegol images - recovery_images = cls.__client.images.list(filters={"dangling": True}) + recovery_images = self.__client.images.list(filters={"dangling": True}) if include_untag: - recovery_images += cls.__client.images.list(ConstantConfig.IMAGE_NAME, filters={"dangling": False}) + recovery_images += self.__client.images.list(ConstantConfig.IMAGE_NAME, filters={"dangling": False}) except APIError as err: logger.debug(f"Error occurred in recovery mode: {err}") return [] @@ -408,8 +399,7 @@ def __findLocalRecoveryImages(cls, include_untag: bool = False) -> List[Image]: id_list.add(img.id) return result - @classmethod - def __listRemoteImages(cls) -> List[MetaImages]: + def __listRemoteImages(self) -> List[MetaImages]: """List remote dockerhub images available. Return a list of ExegolImage""" logger.debug("Fetching remote image tags, digests and sizes") @@ -443,8 +433,7 @@ def __listRemoteImages(cls) -> List[MetaImages]: # Remove duplication (version specific / latest release) return remote_results - @classmethod - def __findImageMatch(cls, remote_image: ExegolImage): + def __findImageMatch(self, remote_image: ExegolImage): """From a Remote ExegolImage, try to find a local match (using Remote DigestID). This method is useful if the image repository name is also lost""" remote_id = remote_image.getLatestRemoteId() @@ -452,7 +441,7 @@ def __findImageMatch(cls, remote_image: ExegolImage): logger.debug("Latest remote id is not available... Falling back to the current remote id.") remote_id = remote_image.getRemoteId() try: - docker_image = cls.__client.images.get(f"{ConstantConfig.IMAGE_NAME}@{remote_id}") + docker_image = self.__client.images.get(f"{ConstantConfig.IMAGE_NAME}@{remote_id}") except ImageNotFound: raise ObjectNotFound except ReadTimeout: @@ -461,8 +450,7 @@ def __findImageMatch(cls, remote_image: ExegolImage): remote_image.resetDockerImage() remote_image.setDockerObject(docker_image) - @classmethod - def downloadImage(cls, image: ExegolImage, install_mode: bool = False) -> bool: + def downloadImage(self, image: ExegolImage, install_mode: bool = False) -> bool: """Download/pull an ExegolImage""" if ParametersManager().offline_mode: logger.critical("It's not possible to download a docker image in offline mode ...") @@ -477,15 +465,15 @@ def downloadImage(cls, image: ExegolImage, install_mode: bool = False) -> bool: logger.debug(f"Downloading {ConstantConfig.IMAGE_NAME}:{name} ({image.getArch()})") try: ExegolTUI.downloadDockerLayer( - cls.__client.api.pull(repository=ConstantConfig.IMAGE_NAME, - tag=name, - stream=True, - decode=True, - platform="linux/" + image.getArch())) + 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: - cls.removeImage(image, upgrade_mode=not install_mode) + self.removeImage(image, upgrade_mode=not install_mode) return True except APIError as err: if err.status_code == 500: @@ -500,16 +488,15 @@ def downloadImage(cls, image: ExegolImage, install_mode: bool = False) -> bool: logger.critical(f"Received a timeout error, Docker is busy... Unable to download {name} image, retry later.") return False - @classmethod - def downloadVersionTag(cls, image: ExegolImage) -> Union[ExegolImage, str]: + def downloadVersionTag(self, image: ExegolImage) -> Union[ExegolImage, str]: """Pull a docker image for a specific version tag and return the corresponding ExegolImage""" if ParametersManager().offline_mode: logger.critical("It's not possible to download a docker image in offline mode ...") return "" try: - image = cls.__client.images.pull(repository=ConstantConfig.IMAGE_NAME, - tag=image.getLatestVersionName(), - platform="linux/" + image.getArch()) + image = self.__client.images.pull(repository=ConstantConfig.IMAGE_NAME, + tag=image.getLatestVersionName(), + platform="linux/" + image.getArch()) return ExegolImage(docker_image=image, isUpToDate=True) except APIError as err: if err.status_code == 500: @@ -524,8 +511,7 @@ def downloadVersionTag(cls, image: ExegolImage) -> Union[ExegolImage, str]: f" [orange3]docker pull --platform linux/{image.getArch()} {ConstantConfig.IMAGE_NAME}:{image.getLatestVersionName()}[/orange3].") return # type: ignore - @classmethod - def removeImage(cls, image: ExegolImage, upgrade_mode: bool = False) -> bool: + def removeImage(self, image: ExegolImage, upgrade_mode: bool = False) -> bool: """Remove an ExegolImage from disk""" tag = image.removeCheck() if tag is None: # Skip removal if image is not installed locally. @@ -535,10 +521,10 @@ def removeImage(cls, image: ExegolImage, upgrade_mode: bool = False) -> bool: if not image.isVersionSpecific() and image.getInstalledVersionName() != image.getName() and not upgrade_mode: # Docker can't remove multiple images at the same tag, version specific tag must be remove first logger.debug(f"Removing image {image.getFullVersionName()}") - if not cls.__remove_image(image.getFullVersionName()): + if not self.__remove_image(image.getFullVersionName()): logger.critical(f"An error occurred while removing this image : {image.getFullVersionName()}") logger.debug(f"Removing image {image.getLocalId()} ({image.getFullVersionName() if upgrade_mode else image.getFullName()})") - if cls.__remove_image(image.getLocalId()): + if self.__remove_image(image.getLocalId()): 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 @@ -558,15 +544,14 @@ def removeImage(cls, image: ExegolImage, upgrade_mode: bool = False) -> bool: logger.critical(f"An error occurred while removing this image : {err}") return False - @classmethod - def __remove_image(cls, image_name: str) -> bool: + def __remove_image(self, image_name: str) -> bool: """ Handle docker image removal with timeout support :param image_name: Name of the docker image to remove :return: True is removal successful and False otherwise """ try: - cls.__client.images.remove(image_name, force=False, noprune=False) + self.__client.images.remove(image_name, force=False, noprune=False) return True except ReadTimeout: logger.warning("The deletion of the image has timeout. Docker is still processing the removal, please wait.") @@ -574,7 +559,7 @@ def __remove_image(cls, image_name: str) -> bool: wait_time = 5 for i in range(5): try: - _ = cls.__client.images.get(image_name) + _ = self.__client.images.get(image_name) # DockerSDK image getter is an exact matching, no need to add more check except APIError as err: if err.status_code == 404: @@ -582,14 +567,13 @@ def __remove_image(cls, image_name: str) -> bool: else: logger.debug(f"Unexpected error after timeout: {err}") except ReadTimeout: - wait_time = wait_time + wait_time*i - logger.info(f"Docker timeout again ({i+1}/{max_retry}). Next retry in {wait_time} seconds...") + wait_time = wait_time + wait_time * i + logger.info(f"Docker timeout again ({i + 1}/{max_retry}). Next retry in {wait_time} seconds...") sleep(wait_time) # Wait x seconds before retry logger.error(f"The deletion of the image '{image_name}' has timeout, the deletion may be incomplete.") return False - @classmethod - def buildImage(cls, tag: str, build_profile: Optional[str] = None, build_dockerfile: Optional[str] = None, dockerfile_path: str = ConstantConfig.build_context_path): + def buildImage(self, tag: str, build_profile: Optional[str] = None, build_dockerfile: Optional[str] = None, dockerfile_path: str = ConstantConfig.build_context_path): """Build a docker image from source""" if ParametersManager().offline_mode: logger.critical("It's not possible to build a docker image in offline mode. The build process need access to internet ...") @@ -608,17 +592,17 @@ def buildImage(cls, tag: str, build_profile: Optional[str] = None, build_dockerf # tag is the name of the final build # dockerfile is the Dockerfile filename ExegolTUI.buildDockerImage( - cls.__client.api.build(path=dockerfile_path, - dockerfile=build_dockerfile, - tag=f"{ConstantConfig.IMAGE_NAME}:{tag}", - buildargs={"TAG": f"{build_profile}", - "VERSION": "local", - "BUILD_DATE": datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')}, - platform="linux/" + ParametersManager().arch, - rm=True, - forcerm=True, - pull=True, - decode=True)) + self.__client.api.build(path=dockerfile_path, + dockerfile=build_dockerfile, + tag=f"{ConstantConfig.IMAGE_NAME}:{tag}", + buildargs={"TAG": f"{build_profile}", + "VERSION": "local", + "BUILD_DATE": datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')}, + platform="linux/" + ParametersManager().arch, + rm=True, + forcerm=True, + pull=True, + decode=True)) logger.success(f"Exegol image successfully built") except APIError as err: logger.debug(f"Error: {err}") diff --git a/exegol/utils/MetaSingleton.py b/exegol/utils/MetaSingleton.py index c5b10eed..7360cf4e 100644 --- a/exegol/utils/MetaSingleton.py +++ b/exegol/utils/MetaSingleton.py @@ -1,7 +1,7 @@ -# Generic singleton class from typing import Dict +# Generic singleton class class MetaSingleton(type): """Metaclass to create a singleton class""" __instances: Dict[type, object] = {} From 6623ad65d6a36b1ac4f0c22c4a523fb59eeb5f5d Mon Sep 17 00:00:00 2001 From: Dramelac Date: Sat, 11 May 2024 20:50:42 +0200 Subject: [PATCH 07/14] Update docker permission error message --- exegol/utils/DockerUtils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/exegol/utils/DockerUtils.py b/exegol/utils/DockerUtils.py index 60c21aca..6f446d2c 100644 --- a/exegol/utils/DockerUtils.py +++ b/exegol/utils/DockerUtils.py @@ -50,6 +50,9 @@ def __init__(self): 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( From 56fd8ea29b1cb733d2f72bd3326e90ffd6954337 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Tue, 14 May 2024 17:06:37 +0200 Subject: [PATCH 08/14] Fix exegol global import Signed-off-by: Dramelac --- exegol/manager/ExegolController.py | 5 ++--- exegol/model/ContainerConfig.py | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/exegol/manager/ExegolController.py b/exegol/manager/ExegolController.py index 87c95fe2..ad36116a 100644 --- a/exegol/manager/ExegolController.py +++ b/exegol/manager/ExegolController.py @@ -1,14 +1,13 @@ -from exegol.manager.ExegolManager import ExegolManager -from exegol.utils.DockerUtils import DockerUtils - try: import docker import requests import git from exegol.utils.ExeLog import logger, ExeLog, console + from exegol.utils.DockerUtils import DockerUtils from exegol.console.cli.ParametersManager import ParametersManager from exegol.console.cli.actions.ExegolParameters import Command + from exegol.manager.ExegolManager import ExegolManager except ModuleNotFoundError as e: print("Mandatory dependencies are missing:", e) print("Please install them with python3 -m pip install --upgrade -r requirements.txt") diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index 4463ff3f..b15d7a7c 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -1455,10 +1455,10 @@ def __str__(self): f"Ports: {self.__ports}{os.linesep}" \ f"Share timezone: {self.__share_timezone}{os.linesep}" \ f"Common resources: {self.__my_resources}{os.linesep}" \ - f"Envs ({len(self.__envs)}): {self.__envs}{os.linesep}" \ - f"Labels ({len(self.__labels)}): {self.__labels}{os.linesep}" \ - f"Shares ({len(self.__mounts)}): {self.__mounts}{os.linesep}" \ - f"Devices ({len(self.__devices)}): {self.__devices}{os.linesep}" \ + f"Envs ({len(self.__envs)}): {os.linesep.join(self.__envs)}{os.linesep}" \ + f"Labels ({len(self.__labels)}): {os.linesep.join(self.__labels)}{os.linesep}" \ + f"Shares ({len(self.__mounts)}): {os.linesep.join([str(x) for x in self.__mounts])}{os.linesep}" \ + f"Devices ({len(self.__devices)}): {os.linesep.join(self.__devices)}{os.linesep}" \ f"VPN: {self.getVpnName()}" def printConfig(self): From 5f980bf108e3c1c1291ca9f614c32eb6c809f084 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Tue, 14 May 2024 17:20:49 +0200 Subject: [PATCH 09/14] Support docker desktop beta Host network Signed-off-by: Dramelac --- exegol/config/ConstantConfig.py | 3 ++- exegol/config/EnvInfo.py | 40 ++++++++++++++++++++++++--------- exegol/model/ContainerConfig.py | 11 +++++---- exegol/utils/GuiUtils.py | 1 + 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/exegol/config/ConstantConfig.py b/exegol/config/ConstantConfig.py index 3110d10e..3a59e536 100644 --- a/exegol/config/ConstantConfig.py +++ b/exegol/config/ConstantConfig.py @@ -23,7 +23,8 @@ class ConstantConfig: exegol_config_path: Path = Path().home() / ".exegol" # Docker Desktop for mac config file docker_desktop_mac_config_path = Path().home() / "Library/Group Containers/group.com.docker/settings.json" - docker_desktop_windows_config_path = Path().home() / "AppData/Roaming/Docker/settings.json" + docker_desktop_windows_config_short_path = "AppData/Roaming/Docker/settings.json" + docker_desktop_windows_config_path = Path().home() / docker_desktop_windows_config_short_path # Install mode, check if Exegol has been git cloned or installed using pip package git_source_installation: bool = (src_root_path_obj / '.git').is_dir() pip_installed: bool = src_root_path_obj.name == "site-packages" diff --git a/exegol/config/EnvInfo.py b/exegol/config/EnvInfo.py index 65200301..135d806d 100644 --- a/exegol/config/EnvInfo.py +++ b/exegol/config/EnvInfo.py @@ -3,7 +3,7 @@ import platform from enum import Enum from pathlib import Path -from typing import Optional, Any, List +from typing import Optional, List, Dict from exegol.config.ConstantConfig import ConstantConfig from exegol.utils.ExeLog import logger @@ -149,6 +149,11 @@ def isMacHost(cls) -> bool: """Return true if macOS is detected on the host""" return cls.getHostOs() == cls.HostOs.MAC + @classmethod + def isLinuxHost(cls) -> bool: + """Return true if Linux is detected on the host""" + return cls.getHostOs() == cls.HostOs.LINUX + @classmethod def isWaylandAvailable(cls) -> bool: """Return true if wayland is detected on the host""" @@ -185,7 +190,7 @@ def getShellType(cls): return "Unknown" @classmethod - def getDockerDesktopSettings(cls) -> Optional[Any]: + def getDockerDesktopSettings(cls) -> Dict: """Applicable only for docker desktop on macos""" if cls.isDockerDesktop(): if cls.__docker_desktop_resource_config is None: @@ -194,20 +199,35 @@ def getDockerDesktopSettings(cls) -> Optional[Any]: elif cls.is_windows_shell: path = ConstantConfig.docker_desktop_windows_config_path else: - return None - # TODO support from WSL shell + # Find docker desktop config + path = None + for i in Path("/mnt/c/Users").glob(f"*/{ConstantConfig.docker_desktop_windows_config_short_path}"): + path = i + logger.debug(f"Docker desktop config found at {path}") + break + if path is None: + return {} try: with open(path, 'r') as docker_desktop_config: cls.__docker_desktop_resource_config = json.load(docker_desktop_config) except FileNotFoundError: logger.warning(f"Docker Desktop configuration file not found: '{path}'") - return None + return {} return cls.__docker_desktop_resource_config - return None + return {} @classmethod def getDockerDesktopResources(cls) -> List[str]: - config = cls.getDockerDesktopSettings() - if config: - return config.get('filesharingDirectories', []) - return [] + return cls.getDockerDesktopSettings().get('filesharingDirectories', []) + + @classmethod + def isHostNetworkAvailable(cls) -> bool: + if cls.isLinuxHost(): + return True + elif cls.isOrbstack(): + return True + elif cls.isDockerDesktop(): + res = cls.getDockerDesktopSettings().get('hostNetworkingEnabled', False) + return res if res is not None else False + logger.warning("Unknown or not supported environment for host network mode.") + return False diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index b15d7a7c..2f6de105 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -838,10 +838,13 @@ def setNetworkMode(self, host_mode: Optional[bool]): logger.warning("Host mode cannot be set with NAT ports configured. Disabling the shared network mode.") host_mode = False if EnvInfo.isDockerDesktop() and host_mode: - logger.warning("Docker desktop (Windows & macOS) does not support sharing of host network interfaces.") - logger.verbose("Official doc: https://docs.docker.com/network/host/") - logger.info("To share network ports between the host and exegol, use the [bright_blue]--port[/bright_blue] parameter.") - host_mode = False + if not EnvInfo.isHostNetworkAvailable(): + logger.warning("Host network mode for Docker desktop (Windows & macOS) is not available.") + logger.verbose("Official doc: https://docs.docker.com/network/drivers/host/#docker-desktop") + logger.info("To share network ports between the host and exegol, use the [bright_blue]--port[/bright_blue] parameter.") + host_mode = False + else: + logger.warning("Docker desktop host network mode is enabled but in beta. Everything might not work as you expect.") self.__network_host = host_mode def setPrivileged(self, status: bool = True): diff --git a/exegol/utils/GuiUtils.py b/exegol/utils/GuiUtils.py index 52a7aca0..607a912e 100644 --- a/exegol/utils/GuiUtils.py +++ b/exegol/utils/GuiUtils.py @@ -154,6 +154,7 @@ def __checkDockerDesktopResourcesConfig() -> bool: mount /tmp/.X11-unix for display sharing. Return True if the configuration is correct and /tmp is part of the whitelisted resources """ + # Function not used for now because the X11 socket cannot be used for now with Docker Desktop docker_config = EnvInfo.getDockerDesktopResources() logger.debug(f"Docker Desktop configuration filesharingDirectories: {docker_config}") return '/tmp' in docker_config From bb3e91ed71671d02e0784078b843af6ea9d7da04 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Tue, 14 May 2024 18:18:49 +0200 Subject: [PATCH 10/14] Fix none type Signed-off-by: Dramelac --- exegol/config/EnvInfo.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/exegol/config/EnvInfo.py b/exegol/config/EnvInfo.py index 135d806d..8b9842fb 100644 --- a/exegol/config/EnvInfo.py +++ b/exegol/config/EnvInfo.py @@ -200,13 +200,12 @@ def getDockerDesktopSettings(cls) -> Dict: path = ConstantConfig.docker_desktop_windows_config_path else: # Find docker desktop config - path = None - for i in Path("/mnt/c/Users").glob(f"*/{ConstantConfig.docker_desktop_windows_config_short_path}"): - path = i - logger.debug(f"Docker desktop config found at {path}") - break - if path is None: + config_file = list(Path("/mnt/c/Users").glob(f"*/{ConstantConfig.docker_desktop_windows_config_short_path}")) + if len(config_file) == 0: return {} + else: + path = config_file[0] + logger.debug(f"Docker desktop config found at {path}") try: with open(path, 'r') as docker_desktop_config: cls.__docker_desktop_resource_config = json.load(docker_desktop_config) From 3e22efc3b41c7617268ce39327533a3fad8a9021 Mon Sep 17 00:00:00 2001 From: Shutdown <40902872+ShutdownRepo@users.noreply.github.com> Date: Wed, 15 May 2024 15:22:41 +0200 Subject: [PATCH 11/14] Changing warning for Timezone support on Mac --- exegol/model/ContainerConfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index 2f6de105..e2f71c5f 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -423,7 +423,7 @@ def enableSharedTimezone(self): # On Orbstack /etc cannot be shared + we should test how Orbstack handle symlink # With docker desktop, symlink are resolved as full path on container creation. When tzdata is updated on the host, the container can no longer be started because the files of the previous package version are missing. # TODO Test if env var can be used as replacement - logger.warning("Timezone sharing on Mac isn't supported for instability issues. Skipping.") + logger.warning("Timezone sharing on Mac is not supported (for stability reasons). Skipping.") return if not self.__share_timezone: logger.verbose("Config: Enabling host timezones") From a046eb0d669f55a75cef490aa72eae242bf0e808 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Wed, 15 May 2024 19:40:40 +0200 Subject: [PATCH 12/14] Support relative path custom volume --- exegol/manager/ExegolManager.py | 2 +- exegol/model/ContainerConfig.py | 41 ++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/exegol/manager/ExegolManager.py b/exegol/manager/ExegolManager.py index 5b72e8da..ab93d90f 100644 --- a/exegol/manager/ExegolManager.py +++ b/exegol/manager/ExegolManager.py @@ -536,7 +536,7 @@ def __createTmpContainer(cls, image_name: Optional[str] = None) -> ExegolContain model = ExegolContainerTemplate(name, config, image, hostname=ParametersManager().hostname) # Mount entrypoint as a volume (because in tmp mode the container is created with run instead of create method) - model.config.addVolume(str(ConstantConfig.entrypoint_context_path_obj), "/.exegol/entrypoint.sh", must_exist=True, read_only=True) + model.config.addVolume(ConstantConfig.entrypoint_context_path_obj, "/.exegol/entrypoint.sh", must_exist=True, read_only=True) container = DockerUtils().createContainer(model, temporary=True) container.postCreateSetup(is_temporary=True) diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index e2f71c5f..d360d0f0 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -388,7 +388,7 @@ def enableGUI(self): try: host_path = GuiUtils.getWaylandSocketPath() if host_path is not None: - self.addVolume(host_path.as_posix(), f"/tmp/{host_path.name}", must_exist=True) + self.addVolume(host_path, f"/tmp/{host_path.name}", must_exist=True) self.addEnv("XDG_SESSION_TYPE", "wayland") self.addEnv("XDG_RUNTIME_DIR", "/tmp") self.addEnv("WAYLAND_DISPLAY", GuiUtils.getWaylandEnv()) @@ -459,12 +459,11 @@ def __disableSharedTimezone(self): def enableMyResources(self): """Procedure to enable shared volume feature""" - # TODO test my resources cross shell source (WSL / PSH) on Windows if not self.__my_resources: logger.verbose("Config: Enabling my-resources volume") self.__my_resources = True # Adding volume config - self.addVolume(str(UserConfig().my_resources_path), '/opt/my-resources', enable_sticky_group=True, force_sticky_group=True) + self.addVolume(UserConfig().my_resources_path, '/opt/my-resources', enable_sticky_group=True, force_sticky_group=True) def __disableMyResources(self): """Procedure to disable shared volume feature (Only for interactive config)""" @@ -487,7 +486,7 @@ def enableExegolResources(self) -> bool: logger.verbose("Config: Enabling exegol resources volume") self.__exegol_resources = True # Adding volume config - self.addVolume(str(UserConfig().exegol_resources_path), '/opt/resources') + self.addVolume(UserConfig().exegol_resources_path, '/opt/resources') return True def disableExegolResources(self): @@ -682,13 +681,13 @@ def __prepareVpnVolumes(self, config_path: Optional[str]) -> Optional[str]: if vpn_path.is_file(): self.__checkVPNConfigDNS(vpn_path) # Configure VPN with single file - self.addVolume(str(vpn_path.absolute()), "/.exegol/vpn/config/client.ovpn", read_only=True) + self.addVolume(vpn_path, "/.exegol/vpn/config/client.ovpn", read_only=True) ovpn_parameters.append("--config /.exegol/vpn/config/client.ovpn") else: # Configure VPN with directory logger.verbose("Folder detected for VPN configuration. " "Only the first *.ovpn file will be automatically launched when the container starts.") - self.addVolume(str(vpn_path.absolute()), "/.exegol/vpn/config", read_only=True) + self.addVolume(vpn_path, "/.exegol/vpn/config", read_only=True) vpn_filename = None # Try to find the config file in order to configure the autostart command of the container for file in vpn_path.glob('*.ovpn'): @@ -712,7 +711,7 @@ def __prepareVpnVolumes(self, config_path: Optional[str]) -> Optional[str]: if vpn_auth is not None: if vpn_auth.is_file(): logger.info(f"Adding VPN credentials from: {str(vpn_auth.absolute())}") - self.addVolume(str(vpn_auth.absolute()), "/.exegol/vpn/auth/creds.txt", read_only=True) + self.addVolume(vpn_auth, "/.exegol/vpn/auth/creds.txt", read_only=True) ovpn_parameters.append("--auth-user-pass /.exegol/vpn/auth/creds.txt") else: # Supply a directory instead of a file for VPN authentication is not supported. @@ -971,7 +970,7 @@ def isWorkspaceCustom(self) -> bool: return bool(self.__workspace_custom_path) def addVolume(self, - host_path: str, + host_path: Union[str, Path], container_path: str, must_exist: bool = False, read_only: bool = False, @@ -984,12 +983,14 @@ def addVolume(self, Otherwise, a folder will attempt to be created at the specified path. if set_sticky_group is set (on a Linux host), the permission setgid will be added to every folder on the volume.""" # The creation of the directory is ignored when it is a path to the remote drive - if volume_type == 'bind' and not host_path.startswith("\\\\"): - path = Path(host_path) - # TODO extend to docker desktop Windows + if volume_type == 'bind' and not (type(host_path) is str and host_path.startswith("\\\\")): + path = Path(host_path).absolute() if type(host_path) is str else host_path.absolute() + host_path = path.as_posix() + # Docker Desktop for Windows based on WSL2 don't have filesystem limitation if EnvInfo.isMacHost(): # Add support for /etc - path_match = str(path) + # TODO check if path_match + replace really useful , path_match rever used + path_match = host_path if path_match.startswith("/opt/") and EnvInfo.isOrbstack(): msg = f"{EnvInfo.getDockerEngine().value} cannot mount directory from [magenta]/opt/[/magenta] host path." if path_match.endswith("entrypoint.sh") or path_match.endswith("spawn.sh"): @@ -1036,7 +1037,7 @@ def addVolume(self, # If user choose not to update, print tips logger.warning(f"The file sharing permissions between the container and the host will not be applied automatically by Exegol. (" f"{'Currently enabled by default according to the user config' if UserConfig().auto_update_workspace_fs else 'Use the --update-fs option to enable the feature'})") - mount = Mount(container_path, host_path, read_only=read_only, type=volume_type) + mount = Mount(container_path, str(host_path), read_only=read_only, type=volume_type) self.__mounts.append(mount) def removeVolume(self, host_path: Optional[str] = None, container_path: Optional[str] = None) -> bool: @@ -1233,11 +1234,12 @@ def getUsername(self) -> str: def addRawVolume(self, volume_string): """Add a volume to the container configuration from raw text input. - Expected format is: /source/path:/target/mount:rw""" + Expected format is one of: + /source/path:/target/mount:rw + C:\source\path:/target/mount:ro + ./relative/path:target/mount""" logger.debug(f"Parsing raw volume config: {volume_string}") - # TODO support relative path - parsing = re.match(r'^((\w:)?([\\/][\w .,:\-|()&;]*)+):(([\\/][\w .,\-|()&;]*)+)(:(ro|rw))?$', - volume_string) + parsing = re.match(r'^((\w:|\.|~)?([\\/][\w .,:\-|()&;]*)+):(([\\/][\w .,\-|()&;]*)+)(:(ro|rw))?$', volume_string) if parsing: host_path = parsing.group(1) container_path = parsing.group(4) @@ -1249,10 +1251,11 @@ def addRawVolume(self, volume_string): else: logger.error(f"Error on volume config, mode: {mode} not recognized.") readonly = False + full_host_path = Path(host_path).expanduser() logger.debug( - f"Adding a volume from '{host_path}' to '{container_path}' as {'readonly' if readonly else 'read/write'}") + f"Adding a volume from '{full_host_path.as_posix()}' to '{container_path}' as {'readonly' if readonly else 'read/write'}") try: - self.addVolume(host_path, container_path, read_only=readonly) + self.addVolume(full_host_path, container_path, read_only=readonly) except CancelOperation as e: logger.error(f"The following volume couldn't be created [magenta]{volume_string}[/magenta]. {e}") if not Confirm("Do you want to continue without this volume ?", False): From d14d2f8d02b340d752cab5b5e944e7f5c4730ba6 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Thu, 16 May 2024 00:40:33 +0200 Subject: [PATCH 13/14] Ready for release 4.3.3 --- exegol/config/ConstantConfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exegol/config/ConstantConfig.py b/exegol/config/ConstantConfig.py index 3a59e536..ceb04844 100644 --- a/exegol/config/ConstantConfig.py +++ b/exegol/config/ConstantConfig.py @@ -5,7 +5,7 @@ class ConstantConfig: """Constant parameters information""" # Exegol Version - version: str = "4.3.3b1" + version: str = "4.3.3" # Exegol documentation link documentation: str = "https://exegol.rtfd.io/" From 895547156414799080f79a42f1a566e5e9331e63 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Thu, 16 May 2024 00:45:03 +0200 Subject: [PATCH 14/14] Fix mypy path type --- exegol/model/ContainerConfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index d360d0f0..58d9c494 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -984,7 +984,7 @@ def addVolume(self, if set_sticky_group is set (on a Linux host), the permission setgid will be added to every folder on the volume.""" # The creation of the directory is ignored when it is a path to the remote drive if volume_type == 'bind' and not (type(host_path) is str and host_path.startswith("\\\\")): - path = Path(host_path).absolute() if type(host_path) is str else host_path.absolute() + path: Path = host_path.absolute() if type(host_path) is Path else Path(host_path).absolute() host_path = path.as_posix() # Docker Desktop for Windows based on WSL2 don't have filesystem limitation if EnvInfo.isMacHost(): @@ -1236,7 +1236,7 @@ def addRawVolume(self, volume_string): """Add a volume to the container configuration from raw text input. Expected format is one of: /source/path:/target/mount:rw - C:\source\path:/target/mount:ro + C:\\source\\path:/target/mount:ro ./relative/path:target/mount""" logger.debug(f"Parsing raw volume config: {volume_string}") parsing = re.match(r'^((\w:|\.|~)?([\\/][\w .,:\-|()&;]*)+):(([\\/][\w .,\-|()&;]*)+)(:(ro|rw))?$', volume_string)