Skip to content

Commit

Permalink
Merge pull request #240 from ThePorgs/dev
Browse files Browse the repository at this point in the history
 Release 4.3.8
  • Loading branch information
Dramelac authored Nov 9, 2024
2 parents 28e2ce5 + 9d12b3b commit ada0861
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 45 deletions.
3 changes: 2 additions & 1 deletion exegol/config/ConstantConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
class ConstantConfig:
"""Constant parameters information"""
# Exegol Version
version: str = "4.3.7"
version: str = "4.3.8"

# Exegol documentation link
documentation: str = "https://exegol.rtfd.io/"
Expand All @@ -28,6 +28,7 @@ class ConstantConfig:
# 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"
pipx_installed: bool = "/pipx/venvs/" in src_root_path_obj.as_posix()
# Dockerhub Exegol images repository
DOCKER_HUB: str = "hub.docker.com" # Don't handle docker login operations
DOCKER_REGISTRY: str = "registry-1.docker.io" # Don't handle docker login operations
Expand Down
7 changes: 4 additions & 3 deletions exegol/manager/ExegolManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ def print_version(cls):
@classmethod
def print_debug_banner(cls):
"""Print header debug info"""
logger.debug(f"Pip installation: {boolFormatter(ConstantConfig.pip_installed)}")
logger.debug(f"Pip installation: {boolFormatter(ConstantConfig.pip_installed)}"
f"{'[bright_black](pipx)[/bright_black]' if ConstantConfig.pipx_installed else ''}")
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]")
logger.debug(f"Arch: {EnvInfo.arch}")
Expand All @@ -217,9 +218,9 @@ def print_debug_banner(cls):
logger.debug(f"Docker engine: {EnvInfo.getDockerEngine().value}")
logger.debug(f"Docker desktop: {boolFormatter(EnvInfo.isDockerDesktop())}")
logger.debug(f"Shell type: {EnvInfo.getShellType().value}")
if not UpdateManager.isUpdateTag() and UserConfig().auto_check_updates:
if UserConfig().auto_check_updates:
UpdateManager.checkForWrapperUpdate()
if UpdateManager.isUpdateTag():
if UpdateManager.isUpdateAvailable():
logger.empty_line()
if Confirm(f"An [green]Exegol[/green] update is [orange3]available[/orange3] ({UpdateManager.display_current_version()} -> {UpdateManager.display_latest_version()}), do you want to update ?", default=True):
UpdateManager.updateWrapper()
Expand Down
2 changes: 1 addition & 1 deletion exegol/manager/UpdateManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ def __tagUpdateAvailable(cls, latest_version, current_version=None):
DataCache().get_wrapper_data().current_version = cls.__get_current_version() if current_version is None else current_version

@classmethod
def isUpdateTag(cls) -> bool:
def isUpdateAvailable(cls) -> bool:
"""Check if the cache file is present to announce an available update of the exegol wrapper."""
current_version = cls.__get_current_version()
wrapper_data = DataCache().get_wrapper_data()
Expand Down
6 changes: 3 additions & 3 deletions exegol/model/ExegolContainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ def start(self):
"""Start the docker container"""
if not self.isRunning():
logger.info(f"Starting container {self.name}")
self.__preStartSetup()
self.__start_container()
self.__postStartSetup()

def __start_container(self):
"""
Expand Down Expand Up @@ -281,9 +281,9 @@ def __removeVolume(self):
return
logger.success("Private workspace volume removed successfully")

def __preStartSetup(self):
def __postStartSetup(self):
"""
Operation to be performed before starting a container
Operation to be performed after starting a container
:return:
"""
self.__applyX11ACLs()
Expand Down
16 changes: 11 additions & 5 deletions exegol/model/ExegolImage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from docker.models.containers import Container
from docker.models.images import Image
from rich.status import Status

from exegol.config.DataCache import DataCache
from exegol.console import ConsoleFormat
Expand Down Expand Up @@ -79,7 +80,8 @@ def __init__(self,
if meta_img and meta_img.meta_id is not None:
self.__setDigest(meta_img.meta_id)
self.__setLatestRemoteId(meta_img.meta_id) # Meta id is always the latest one
logger.debug(f"└── {self.__name}\t→ ({self.getType()}) {self.__digest}")
# Debug every Exegol image init
# logger.debug(f"└── {self.__name}\t→ ({self.getType()}) {self.__digest}")

def __initFromDockerImage(self):
"""Parse Docker object to set up self configuration on creation."""
Expand Down Expand Up @@ -178,7 +180,7 @@ def setDockerObject(self, docker_image: Image):
# backup plan: Use label to retrieve image version
self.__labelVersionParsing()

def setMetaImage(self, meta: MetaImages):
def setMetaImage(self, meta: MetaImages, status: Status):
dockerhub_data = meta.getDockerhubImageForArch(self.getArch())
self.__is_remote = True
if self.__version_specific:
Expand All @@ -188,6 +190,8 @@ def setMetaImage(self, meta: MetaImages):
self.__outdated = self.__version_specific
elif not meta.version:
# nightly image don't have a version in their tag. The latest version must be fetch from label on the registry directly
logger.verbose(f"Fetch latest [green]{self.getName()}[/green] image version")
status.update(status=f"Retrieving latest [green]{self.getName()}[/green] image version")
fetch_version = WebUtils.getRemoteVersion(self.__name)
if fetch_version:
meta.version = fetch_version
Expand Down Expand Up @@ -268,7 +272,7 @@ def autoLoad(self, from_cache: bool = True) -> 'ExegolImage':
logger.debug(f"Auto-load remote version for the specific image '{self.__name}'")
# Find remote metadata for the specific current image
with console.status(f"Synchronization of the [green]{self.__name}[/green] image status...",
spinner_style="blue"):
spinner_style="blue") as s:
remote_digest = None
version = None
if from_cache:
Expand All @@ -282,6 +286,8 @@ def autoLoad(self, from_cache: bool = True) -> 'ExegolImage':
break
if not from_cache or version is None:
remote_digest = WebUtils.getMetaDigestId(self.__name)
logger.verbose(f"Fetch latest [green]{self.getName()}[/green] image version")
s.update(status=f"Retrieving latest [green]{self.getName()}[/green] image version")
version = WebUtils.getRemoteVersion(self.__name)
if remote_digest is not None:
self.__setLatestRemoteId(remote_digest)
Expand Down Expand Up @@ -346,7 +352,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[Image], status: Status) -> 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
Expand Down Expand Up @@ -399,7 +405,7 @@ def mergeImages(cls, remote_images: List[MetaImages], local_images: List[Image])
selected = default
if selected:
# Remote match found
current_local_img.setMetaImage(selected)
current_local_img.setMetaImage(selected, status)
else:
if len(remote_images) > 0:
# If there is some result from internet but no match and this is not a local image, this image is probably discontinued or the remote image is too old (relative to other images)
Expand Down
60 changes: 33 additions & 27 deletions exegol/utils/DockerUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from docker.models.images import Image
from docker.models.volumes import Volume
from requests import ReadTimeout
from rich.status import Status

from exegol.config.ConstantConfig import ConstantConfig
from exegol.config.DataCache import DataCache
Expand Down Expand Up @@ -258,9 +259,14 @@ def listImages(self, include_version_tag: bool = False, include_locked: bool = F
"""List available docker images.
Return a list of ExegolImage"""
if self.__images is None:
remote_images = self.__listRemoteImages()
local_images = self.__listLocalImages()
self.__images = ExegolImage.mergeImages(remote_images, local_images)
logger.verbose("Loading every Exegol images")
with console.status(f"Loading Exegol images from registry", spinner_style="blue") as s:
remote_images = self.__listRemoteImages(s)
logger.verbose("Retrieve [green]local[/green] Exegol images")
s.update(status=f"Retrieving [green]local[/green] Exegol images")
local_images = self.__listLocalImages()
self.__images = ExegolImage.mergeImages(remote_images, local_images, s)
logger.verbose("Images fetched")
result = self.__images
assert result is not None
# Caching latest images
Expand Down Expand Up @@ -405,37 +411,37 @@ def __findLocalRecoveryImages(self, include_untag: bool = False) -> List[Image]:
id_list.add(img.id)
return result

def __listRemoteImages(self) -> List[MetaImages]:
@staticmethod
def __listRemoteImages(status: Status) -> List[MetaImages]:
"""List remote dockerhub images available.
Return a list of ExegolImage"""
logger.debug("Fetching remote image tags, digests and sizes")
remote_results = []
# Define max number of tags to download from dockerhub (in order to limit download time and discard historical versions)
page_size = 20
page_max = 2
current_page = 0
url: Optional[str] = f"https://{ConstantConfig.DOCKER_HUB}/v2/repositories/{ConstantConfig.IMAGE_NAME}/tags?page_size={page_size}"
page_max = 3
current_page = 1
url: Optional[str] = f"https://{ConstantConfig.DOCKER_HUB}/v2/repositories/{ConstantConfig.IMAGE_NAME}/tags?page=1&page_size={page_size}"
# Handle multi-page tags from registry
with console.status(f"Loading registry information from [green]{url}[/green]", spinner_style="blue") as s:
while url is not None:
if current_page == page_max:
logger.debug("Max page limit reached. In non-verbose mode, downloads will stop there.")
if not logger.isEnabledFor(ExeLog.VERBOSE):
break
current_page += 1
logger.debug(f"Fetching information from: {url}")
s.update(status=f"Fetching registry information from [green]{url}[/green]")
docker_repo_response = WebUtils.runJsonRequest(url, "Dockerhub")
if docker_repo_response is None:
logger.warning("Skipping online queries.")
return []
error_message = docker_repo_response.get("message")
if error_message:
logger.error(f"Dockerhub send an error message: {error_message}")
for docker_images in docker_repo_response.get("results", []):
meta_image = MetaImages(docker_images)
remote_results.append(meta_image)
url = docker_repo_response.get("next") # handle multiple page tags
while url is not None:
if current_page > page_max:
logger.debug("Max page limit reached. In non-verbose mode, downloads will stop there.")
if not logger.isEnabledFor(ExeLog.VERBOSE):
break
current_page += 1
if logger.isEnabledFor(ExeLog.VERBOSE):
status.update(status=f"Fetching registry information from [green]{url}[/green]")
docker_repo_response = WebUtils.runJsonRequest(url, "Dockerhub")
if docker_repo_response is None:
logger.warning("Skipping online queries.")
return []
error_message = docker_repo_response.get("message")
if error_message:
logger.error(f"Dockerhub send an error message: {error_message}")
for docker_images in docker_repo_response.get("results", []):
meta_image = MetaImages(docker_images)
remote_results.append(meta_image)
url = docker_repo_response.get("next") # handle multiple page tags
# Remove duplication (version specific / latest release)
return remote_results

Expand Down
5 changes: 4 additions & 1 deletion exegol/utils/GitUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ def __init__(self,
except ReferenceError:
if self.__git_name == "wrapper":
logger.warning("Exegol has [red]not[/red] been installed via git clone. Skipping wrapper auto-update operation.")
if ConstantConfig.pip_installed:
if ConstantConfig.pipx_installed:
logger.info("If you have installed Exegol with pipx, check for an update with the command "
"[green]pipx upgrade exegol[/green]")
elif ConstantConfig.pip_installed:
logger.info("If you have installed Exegol with pip, check for an update with the command "
"[green]pip3 install exegol --upgrade[/green]")
abort_loading = True
Expand Down
46 changes: 42 additions & 4 deletions exegol/utils/WebUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,47 @@ def getRemoteVersion(cls, tag: str) -> Optional[str]:
version: Optional[str] = None
if response is not None and response.status_code == 200:
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
version = metadata.get("config", {}).get("Labels", {}).get("org.exegol.version", "")
received_media_type = data.get("mediaType")
if received_media_type == "application/vnd.docker.distribution.manifest.v1+json":
# Get image version from legacy v1 manifest (faster)
# Parse metadata of the current image from v1 schema
metadata = json.loads(data.get("history", [])[0]['v1Compatibility'])
# Find version label and extract data
version = metadata.get("config", {}).get("Labels", {}).get("org.exegol.version", "")

# Convert image list to a specific image
elif received_media_type == "application/vnd.docker.distribution.manifest.list.v2+json":
# Get image version from v2 manifest list (slower)
# Retrieve image digest id from manifest image list
manifest = data.get("manifests")
# Get first image manifest
# Handle application/vnd.docker.distribution.manifest.list.v2+json spec
if type(manifest) is list and len(manifest) > 0:
# Get Image digest
first_digest = manifest[0].get("digest")
# Retrieve specific image detail from first image digest (architecture not sensitive)
manifest_headers["Accept"] = "application/vnd.docker.distribution.manifest.v2+json"
url = f"https://{ConstantConfig.DOCKER_REGISTRY}/v2/{ConstantConfig.IMAGE_NAME}/manifests/{first_digest}"
response = cls.__runRequest(url, service_name="Docker Registry", headers=manifest_headers, method="GET")
if response is not None and response.status_code == 200:
data = json.loads(response.content.decode("utf-8"))
# Update received media type to ba handle later
received_media_type = data.get("mediaType")
# Try to extract version tag from a specific image
if received_media_type == "application/vnd.docker.distribution.manifest.v2+json":
# Get image version from v2 manifest (slower)
# Retrieve config detail from config digest
config_digest: Optional[str] = data.get("config", {}).get('digest')
if config_digest is not None:
manifest_headers["Accept"] = "application/json"
url = f"https://{ConstantConfig.DOCKER_REGISTRY}/v2/{ConstantConfig.IMAGE_NAME}/blobs/{config_digest}"
response = cls.__runRequest(url, service_name="Docker Registry", headers=manifest_headers, method="GET")
if response is not None and response.status_code == 200:
data = json.loads(response.content.decode("utf-8"))
# Find version label and extract data
version = data.get("config", {}).get("Labels", {}).get("org.exegol.version")
else:
logger.debug(f"WARNING: Docker API not supported: {received_media_type}")
return version

@classmethod
Expand Down Expand Up @@ -135,6 +172,7 @@ def __runRequest(cls, url: str, service_name: str, headers: Optional[Dict] = Non
no_proxy = os.environ.get('NO_PROXY') or os.environ.get('no_proxy')
if no_proxy:
proxies['no_proxy'] = no_proxy
logger.debug(f"Fetching information from {url}")
response = requests.request(method=method, url=url, timeout=(10, 20), verify=ParametersManager().verify, headers=headers, data=data, proxies=proxies if len(proxies) > 0 else None)
return response
except requests.exceptions.HTTPError as e:
Expand Down

0 comments on commit ada0861

Please sign in to comment.