Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 4.3.8 #240

Merged
merged 6 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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