diff --git a/README.md b/README.md
index d39af743..2a906778 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,9 @@
+
+
+
diff --git a/exegol-docker-build b/exegol-docker-build
index 4b8aa946..ad28264b 160000
--- a/exegol-docker-build
+++ b/exegol-docker-build
@@ -1 +1 @@
-Subproject commit 4b8aa9464301674e20195876f31ccc90284d97cc
+Subproject commit ad28264bd1698f45d522866d0033fb53942dbd98
diff --git a/exegol/config/ConstantConfig.py b/exegol/config/ConstantConfig.py
index 0c7b194a..c5c00b3c 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.2.4"
+ version: str = "4.2.5"
# Exegol documentation link
documentation: str = "https://exegol.rtfd.io/"
diff --git a/exegol/config/DataCache.py b/exegol/config/DataCache.py
index 87c60474..cee1309f 100644
--- a/exegol/config/DataCache.py
+++ b/exegol/config/DataCache.py
@@ -8,7 +8,7 @@
class DataCache(DataFileUtils, metaclass=MetaSingleton):
"""This class allows loading cached information defined configurations
- Exemple of data:
+ Example of data:
{
wrapper: {
update: {
diff --git a/exegol/console/TUI.py b/exegol/console/TUI.py
index 1d70bb51..70b43b0b 100644
--- a/exegol/console/TUI.py
+++ b/exegol/console/TUI.py
@@ -422,7 +422,7 @@ def printContainerRecap(cls, container: ExegolContainerTemplate):
container_info_header += f" - v.{container.image.getImageVersion()}"
if "Unknown" not in container.image.getStatus():
container_info_header += f" ({container.image.getStatus(include_version=False)})"
- if container.image.getArch() != EnvInfo.arch or logger.isEnabledFor(ExeLog.VERBOSE):
+ if container.image.getArch().split('/')[0] != EnvInfo.arch or logger.isEnabledFor(ExeLog.VERBOSE):
color = ConsoleFormat.getArchColor(container.image.getArch())
container_info_header += f" [{color}]({container.image.getArch()})[/{color}]"
recap.add_column(container_info_header)
diff --git a/exegol/manager/UpdateManager.py b/exegol/manager/UpdateManager.py
index f802be36..aa6df1f2 100644
--- a/exegol/manager/UpdateManager.py
+++ b/exegol/manager/UpdateManager.py
@@ -143,7 +143,7 @@ def __updateGit(gitUtils: GitUtils) -> bool:
if current_branch is None:
logger.warning("HEAD is detached. Please checkout to an existing branch.")
current_branch = "unknown"
- if logger.isEnabledFor(ExeLog.VERBOSE) or current_branch not in ["master", "main"]:
+ if logger.isEnabledFor(ExeLog.VERBOSE):
available_branches = gitUtils.listBranch()
# Ask to checkout only if there is more than one branch available
if len(available_branches) > 1:
@@ -289,7 +289,10 @@ def isUpdateTag(cls) -> bool:
@classmethod
def display_latest_version(cls) -> str:
- return f"[blue]v{DataCache().get_wrapper_data().last_version}[/blue]"
+ last_version = DataCache().get_wrapper_data().last_version
+ if len(last_version) == 8 and '.' not in last_version:
+ return f"[bright_black]\[{last_version}][/bright_black]"
+ return f"[blue]v{last_version}[/blue]"
@classmethod
def __untagUpdateAvailable(cls, current_version: Optional[str] = None):
diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py
index 2357ab50..1164a584 100644
--- a/exegol/model/ContainerConfig.py
+++ b/exegol/model/ContainerConfig.py
@@ -588,10 +588,19 @@ def prepareShare(self, share_name: str):
# Skip default volume workspace if disabled
return
else:
- # Add shared-data-volumes private workspace bind volume
+ # Add dedicated private workspace bind volume
volume_path = str(UserConfig().private_volume_path.joinpath(share_name))
self.addVolume(volume_path, '/workspace', enable_sticky_group=True)
+ def rollback_preparation(self, share_name: str):
+ """Undo preparation in case of container creation failure"""
+ if self.__workspace_custom_path is None and not self.__disable_workspace:
+ # Remove dedicated workspace volume
+ logger.info("Rollback: removing dedicated workspace directory")
+ directory_path = UserConfig().private_volume_path.joinpath(share_name)
+ if directory_path.is_dir():
+ directory_path.rmdir()
+
def setNetworkMode(self, host_mode: Optional[bool]):
"""Set container's network mode, true for host, false for bridge"""
if host_mode is None:
@@ -852,7 +861,7 @@ def removeVolume(self, host_path: Optional[str] = None, container_path: Optional
"""Remove a volume from the container configuration (Only before container creation)"""
if host_path is None and container_path is None:
# This is a dev problem
- raise ReferenceError('At least one parameter must be set')
+ raise ValueError('At least one parameter must be set')
for i in range(len(self.__mounts)):
# For each Mount object compare the host_path if supplied or the container_path si supplied
if host_path is not None and self.__mounts[i].get("Source") == host_path:
diff --git a/exegol/model/ExegolContainer.py b/exegol/model/ExegolContainer.py
index d2cfccf6..05563be1 100644
--- a/exegol/model/ExegolContainer.py
+++ b/exegol/model/ExegolContainer.py
@@ -212,6 +212,9 @@ def __removeVolume(self):
list_files = []
else:
return
+ except FileNotFoundError:
+ logger.debug("This workspace has already been removed.")
+ return
try:
if len(list_files) > 0:
# Directory is not empty
diff --git a/exegol/model/ExegolContainerTemplate.py b/exegol/model/ExegolContainerTemplate.py
index e39260c1..213f4ae3 100644
--- a/exegol/model/ExegolContainerTemplate.py
+++ b/exegol/model/ExegolContainerTemplate.py
@@ -3,6 +3,7 @@
from rich.prompt import Prompt
+from exegol.config.EnvInfo import EnvInfo
from exegol.model.ContainerConfig import ContainerConfig
from exegol.model.ExegolImage import ExegolImage
@@ -14,6 +15,9 @@ def __init__(self, name: Optional[str], config: ContainerConfig, image: ExegolIm
if name is None:
name = Prompt.ask("[bold blue][?][/bold blue] Enter the name of your new exegol container", default="default")
assert name is not None
+ if (EnvInfo.isWindowsHost() or EnvInfo.isMacHost()) and not name.startswith("exegol-"):
+ # Force container as lowercase because the filesystem of windows / mac are case-insensitive => https://github.com/ThePorgs/Exegol/issues/167
+ name = name.lower()
self.container_name: str = name if name.startswith("exegol-") else f'exegol-{name}'
self.name: str = name.replace('exegol-', '')
if hostname:
@@ -31,6 +35,10 @@ def prepare(self):
"""Prepare the model before creating the docker container"""
self.config.prepareShare(self.name)
+ def rollback(self):
+ """Rollback change in case of container creation fail."""
+ self.config.rollback_preparation(self.name)
+
def getDisplayName(self) -> str:
"""Getter of the container's name for TUI purpose"""
if self.container_name != self.hostname:
diff --git a/exegol/model/ExegolImage.py b/exegol/model/ExegolImage.py
index f5c46728..ffa67df0 100644
--- a/exegol/model/ExegolImage.py
+++ b/exegol/model/ExegolImage.py
@@ -616,7 +616,7 @@ def getName(self) -> str:
def getDisplayName(self) -> str:
"""Image's display name getter"""
result = self.__alt_name if self.__alt_name else self.__name
- if self.getArch() != ParametersManager().arch or logger.isEnabledFor(ExeLog.VERBOSE):
+ if self.getArch().split('/')[0] != ParametersManager().arch or logger.isEnabledFor(ExeLog.VERBOSE):
color = ConsoleFormat.getArchColor(self.getArch())
result += f" [{color}]({self.getArch()})[/{color}]"
return result
diff --git a/exegol/utils/DataFileUtils.py b/exegol/utils/DataFileUtils.py
index 4c3a541f..cfedc5d4 100644
--- a/exegol/utils/DataFileUtils.py
+++ b/exegol/utils/DataFileUtils.py
@@ -62,16 +62,18 @@ def _build_file_content(self) -> str:
This fonction build the default file content. Called when the file doesn't exist yet or have been upgrade and need to be updated.
:return:
"""
- raise NotImplementedError(
- f"The '_build_default_file' method hasn't been implemented in the '{self.__class__}' class.")
+ raise NotImplementedError(f"The '_build_default_file' method hasn't been implemented in the '{self.__class__}' class.")
def _create_config_file(self):
"""
Create or overwrite the file content to the default / current value depending on the '_build_default_file' that must be redefined in child class.
:return:
"""
- with open(self._file_path, 'w') as file:
- file.write(self._build_file_content())
+ try:
+ with open(self._file_path, 'w') as file:
+ file.write(self._build_file_content())
+ except PermissionError as e:
+ logger.critical(f"Unable to open the file '{self._file_path}' ({e}). Please fix your file permissions or run exegol with the correct rights.")
def _parse_config(self):
data: Dict = {}
diff --git a/exegol/utils/DockerUtils.py b/exegol/utils/DockerUtils.py
index a65f3fec..ae2752c5 100644
--- a/exegol/utils/DockerUtils.py
+++ b/exegol/utils/DockerUtils.py
@@ -125,8 +125,20 @@ def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False
auto_remove=temporary,
working_dir=model.config.getWorkingDir())
except APIError as err:
- logger.error(err.explanation.decode('utf-8') if type(err.explanation) is bytes else err.explanation)
+ message = err.explanation.decode('utf-8').replace('[', '\\[') if type(err.explanation) is bytes else err.explanation
+ message = message.replace('[', '\\[')
+ logger.error(message)
logger.debug(err)
+ model.rollback()
+ try:
+ container = cls.__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
+ container[0].remove()
+ logger.debug("Container removed")
+ except Exception:
+ pass
logger.critical("Error while creating exegol container. Exiting.")
# Not reachable, critical logging will exit
return # type: ignore
@@ -151,6 +163,13 @@ def getContainer(cls, tag: str) -> ExegolContainer:
return # type: ignore
# Check if there is at least 1 result. If no container was found, raise ObjectNotFound.
if container is None or len(container) == 0:
+ # Handle case-insensitive OS
+ if EnvInfo.isWindowsHost() or EnvInfo.isMacHost():
+ # First try to fetch the container as-is (for retroactive support with old container with uppercase characters)
+ # 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)
raise ObjectNotFound
# Filter results with exact name matching
for c in container:
diff --git a/exegol/utils/GuiUtils.py b/exegol/utils/GuiUtils.py
index c385fc1a..a597428e 100644
--- a/exegol/utils/GuiUtils.py
+++ b/exegol/utils/GuiUtils.py
@@ -2,6 +2,7 @@
import os
import shutil
import subprocess
+import sys
import time
from pathlib import Path
from typing import Optional
@@ -92,6 +93,9 @@ def __macGuiChecks(cls) -> bool:
# Notify user to change configuration
logger.error("XQuartz does not allow network connections. "
"You need to manually change the configuration to 'Allow connections from network clients'")
+ # Add sys.platform check to exclude windows env (fix for mypy static code analysis)
+ if sys.platform != "win32" and os.getuid() == 0:
+ logger.warning("You are running exegol as [red]root[/red]! The root user cannot check in the user context whether XQuartz is properly configured or not.")
return False
# Check if XQuartz is started, check is dir exist and if there is at least one socket
diff --git a/exegol/utils/WebUtils.py b/exegol/utils/WebUtils.py
index 7aabee7a..e43a437c 100644
--- a/exegol/utils/WebUtils.py
+++ b/exegol/utils/WebUtils.py
@@ -92,7 +92,7 @@ def getRemoteVersion(cls, tag: str) -> Optional[str]:
response = cls.__runRequest(url, service_name="Docker Registry", headers=manifest_headers, method="GET")
version: Optional[str] = None
if response is not None and response.status_code == 200:
- data = json.loads(response.text)
+ data = json.loads(response.content.decode("utf-8"))
# Parse metadata of the current image from v1 schema
metadata = json.loads(data.get("history", [])[0]['v1Compatibility'])
# Find version label and extract data
@@ -106,7 +106,7 @@ def runJsonRequest(cls, url: str, service_name: str, headers: Optional[Dict] = N
return None
data = cls.__runRequest(url, service_name, headers, method, data, retry_count)
if data is not None and data.status_code == 200:
- data = json.loads(data.text)
+ data = json.loads(data.content.decode("utf-8"))
elif data is not None:
logger.error(f"Error during web request to {service_name} ({data.status_code}) on {url}")
if data.status_code == 404 and service_name == "Dockerhub":
@@ -127,7 +127,7 @@ def __runRequest(cls, url: str, service_name: str, headers: Optional[Dict] = Non
response = requests.request(method=method, url=url, timeout=(5, 10), verify=ParametersManager().verify, headers=headers, data=data)
return response
except requests.exceptions.HTTPError as e:
- logger.error(f"Response error: {e.response.text}")
+ logger.error(f"Response error: {e.response.content.decode('utf-8')}")
except requests.exceptions.ConnectionError as err:
logger.debug(f"Error: {err}")
error_re = re.search(r"\[Errno [-\d]+]\s?([^']*)('\))+\)*", str(err))
diff --git a/tests/test_exegol.py b/tests/test_exegol.py
index b98740f9..1687cd35 100644
--- a/tests/test_exegol.py
+++ b/tests/test_exegol.py
@@ -2,4 +2,4 @@
def test_version():
- assert __version__ == '4.2.4'
+ assert __version__ == '4.2.5'