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

Refactoring code for podman support #234

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
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/manager/ExegolController.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

try:
import docker
import podman
import git
import requests
import urllib3
Expand Down Expand Up @@ -85,5 +86,5 @@ def main():
logger.critical(f"A critical error occurred while running this git command: {' '.join(git_error.command)}")
except Exception:
print_exception_banner()
console.print_exception(show_locals=True, suppress=[docker, requests, git, urllib3, http])
console.print_exception(show_locals=True, suppress=[docker, podman, requests, git, urllib3, http])
exit(1)
6 changes: 4 additions & 2 deletions exegol/manager/ExegolManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ def start(cls):
if not container.isNew():
# Check and warn user if some parameters don't apply to the current session
cls.__checkUselessParameters()
docker_utils_instance = DockerUtils()
container.start()
container.spawnShell()
container.spawnShell(docker_utils_instance.get_container_runtime())

@classmethod
def exec(cls):
Expand Down Expand Up @@ -114,7 +115,8 @@ def restart(cls):
container.stop(timeout=5)
container.start()
logger.success(f"Container [green]{container.name}[/green] successfully restarted!")
container.spawnShell()
docker_utils_instance = DockerUtils()
container.spawnShell(docker_utils_instance.get_container_runtime())

@classmethod
def install(cls):
Expand Down
7 changes: 4 additions & 3 deletions exegol/model/ContainerConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from pathlib import Path, PurePath
from typing import Optional, List, Dict, Union, Tuple, cast

from docker.models.containers import Container
from docker.models.containers import Container as DockerContainer
from docker.types import Mount
from podman.domain.containers import Container as PodmanContainer
from rich.prompt import Prompt

from exegol.config.ConstantConfig import ConstantConfig
Expand Down Expand Up @@ -81,7 +82,7 @@ class ExegolEnv(Enum):
ExegolMetadata.comment.value: ["setComment", "getComment"],
ExegolMetadata.password.value: ["setPasswd", "getPasswd"]}

def __init__(self, container: Optional[Container] = None):
def __init__(self, container: Optional[Union[DockerContainer, PodmanContainer]] = None):
"""Container config default value"""
self.hostname = ""
self.__enable_gui: bool = False
Expand Down Expand Up @@ -132,7 +133,7 @@ def __init__(self, container: Optional[Container] = None):

# ===== Config parsing section =====

def __parseContainerConfig(self, container: Container):
def __parseContainerConfig(self, container: Union[DockerContainer, PodmanContainer]):
"""Parse Docker object to setup self configuration"""
# Reset default attributes
self.__passwd = None
Expand Down
41 changes: 22 additions & 19 deletions exegol/model/ExegolContainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
from typing import Optional, Dict, Sequence, Tuple, Union

from docker.errors import NotFound, ImageNotFound, APIError
from docker.models.containers import Container
from docker.models.containers import Container as DockerContainer

from podman.errors import NotFound as PodmanNotFound, ImageNotFound as PodmanImageNotFound, APIError as PodmanAPIError
from podman.domain.containers import Container as PodmanContainer

from exegol.config.EnvInfo import EnvInfo
from exegol.console.ExegolPrompt import Confirm
Expand All @@ -24,36 +27,36 @@
class ExegolContainer(ExegolContainerTemplate, SelectableInterface):
"""Class of an exegol container already create in docker"""

def __init__(self, docker_container: Container, model: Optional[ExegolContainerTemplate] = None):
logger.debug(f"Loading container: {docker_container.name}")
self.__container: Container = docker_container
self.__id: str = docker_container.id
def __init__(self, container_obj: Union[DockerContainer, PodmanContainer], model: Optional[ExegolContainerTemplate] = None):
logger.debug(f"Loading container: {container_obj.name}")
self.__container: Container = container_obj
self.__id: str = container_obj.id
self.__xhost_applied = False
if model is None:
image_name = ""
try:
# Try to find the attached docker image
docker_image = docker_container.image
except ImageNotFound:
docker_image = container_obj.image
except (ImageNotFound, PodmanImageNotFound):
# If it is not found, the user has probably forcibly deleted it manually
logger.warning(f"Some images were forcibly removed by docker when they were used by existing containers!")
logger.error(f"The '{docker_container.name}' containers might not work properly anymore and should also be deleted and recreated with a new image.")
logger.error(f"The '{container_obj.name}' containers might not work properly anymore and should also be deleted and recreated with a new image.")
docker_image = None
image_name = "[red bold]BROKEN[/red bold]"
# Create Exegol container from an existing docker container
super().__init__(docker_container.name,
config=ContainerConfig(docker_container),
super().__init__(container_obj.name,
config=ContainerConfig(container_obj),
image=ExegolImage(name=image_name, docker_image=docker_image),
hostname=docker_container.attrs.get('Config', {}).get('Hostname'),
hostname=container_obj.attrs.get('Config', {}).get('Hostname'),
new_container=False)
self.image.syncContainerData(docker_container)
self.image.syncContainerData(container_obj)
# At this stage, the container image object has an unknown status because no synchronization with a registry has been done.
# This could be done afterwards (with container.image.autoLoad()) if necessary because it takes time.
self.__new_container = False
else:
# Create Exegol container from a newly created docker container with its object template.
super().__init__(docker_container.name,
config=ContainerConfig(docker_container),
super().__init__(container_obj.name,
config=ContainerConfig(container_obj),
# Rebuild config from docker object to update workspace path
image=model.image,
hostname=model.config.hostname,
Expand Down Expand Up @@ -121,7 +124,7 @@ def __start_container(self):
start_date = datetime.now()
try:
self.__container.start()
except APIError as e:
except (APIError, PodmanAPIError) as e:
logger.debug(e)
logger.critical(f"Docker raise a critical error when starting the container [green]{self.name}[/green], error message is: {e.explanation}")
if not self.config.legacy_entrypoint: # TODO improve startup compatibility check
Expand Down Expand Up @@ -151,7 +154,7 @@ def stop(self, timeout: int = 10):
with console.status(f"Waiting to stop ({timeout}s timeout)", spinner_style="blue"):
self.__container.stop(timeout=timeout)

def spawnShell(self):
def spawnShell(self, container_runtime: str = None):
"""Spawn a shell on the docker container"""
self.__check_start_version()
logger.info(f"Location of the exegol workspace on the host : {self.config.getHostWorkspacePath()}")
Expand All @@ -165,7 +168,7 @@ def spawnShell(self):
options = ""
if len(envs) > 0:
options += f" -e {' -e '.join(envs)}"
cmd = f"docker exec{options} -ti {self.getFullId()} {self.config.getShellCommand()}"
cmd = f"{container_runtime} exec{options} -ti {self.getFullId()} {self.config.getShellCommand()}"
logger.debug(f"Opening shell with: {cmd}")
os.system(cmd)
# Docker SDK doesn't support (yet) stdin properly
Expand Down Expand Up @@ -230,7 +233,7 @@ def remove(self):
try:
self.__container.remove()
logger.success(f"Container {self.name} successfully removed.")
except NotFound:
except (NotFound, PodmanNotFound):
logger.error(
f"The container {self.name} has already been removed (probably created as a temporary container).")

Expand Down Expand Up @@ -319,7 +322,7 @@ def postCreateSetup(self, is_temporary: bool = False):
self.__start_container()
try:
self.__updatePasswd()
except APIError as e:
except (APIError, PodmanAPIError) as e:
if "is not running" in e.explanation:
logger.critical("An unexpected error occurred. Exegol cannot start the container after its creation...")

Expand Down
62 changes: 38 additions & 24 deletions exegol/model/ExegolImage.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from datetime import datetime
from typing import Optional, List, Dict, Any, Union

from docker.models.containers import Container
from docker.models.images import Image
from docker.models.containers import Container as DockerContainer
from docker.models.images import Image as DockerImage
from podman.domain.containers import Container as PodmanContainer
from podman.domain.images import Image as PodmanImage
from rich.status import Status

from exegol.config.DataCache import DataCache
Expand All @@ -24,7 +26,7 @@ def __init__(self,
dockerhub_data: Optional[Dict[str, Any]] = None,
meta_img: Optional[MetaImages] = None,
image_id: Optional[str] = None,
docker_image: Optional[Image] = None,
docker_image: Optional[Union[DockerImage, PodmanImage]] = None,
isUpToDate: bool = False):
"""Docker image default value"""
# Prepare parameters
Expand All @@ -36,7 +38,7 @@ def __init__(self,
version_parsed = MetaImages.tagNameParsing(name)
self.__version_specific = bool(version_parsed)
# Init attributes
self.__image: Optional[Image] = docker_image
self.__image: Optional[Union[DockerImage, PodmanImage]] = docker_image
self.__name: str = name
self.__alt_name: str = ''
self.__arch = ""
Expand Down Expand Up @@ -150,7 +152,7 @@ def resetDockerImage(self):
self.__build_date = "[bright_black]N/A[/bright_black]"
self.__disk_size = "[bright_black]N/A[/bright_black]"

def setDockerObject(self, docker_image: Image):
def setDockerObject(self, docker_image: Union[DockerImage, PodmanImage]):
"""Docker object setter. Parse object to set up self configuration."""
self.__image = docker_image
# When a docker image exist, image is locally installed
Expand All @@ -164,9 +166,9 @@ def setDockerObject(self, docker_image: Image):
self.__build_date = self.__image.labels.get('org.exegol.build_date', '[bright_black]N/A[/bright_black]')
# Check if local image is sync with remote digest id (check up-to-date status)
if self.__profile_digest:
self.__is_update = self.__profile_digest == self.__parseDigest(docker_image)
self.__is_update = self.__profile_digest in self.__parseDigest(docker_image) # unlike docker, podman associates multiple digests to an image
else:
self.__is_update = self.__digest == self.__parseDigest(docker_image)
self.__is_update = self.__digest in self.__parseDigest(docker_image) # unlike docker, podman associates multiple digests to an image
# If this image is remote, set digest ID
self.__is_remote = not (len(self.__image.attrs["RepoDigests"]) == 0 and self.__checkLocalLabel())
if self.__is_remote:
Expand Down Expand Up @@ -202,7 +204,7 @@ def setMetaImage(self, meta: MetaImages, status: Status):
if meta.meta_id:
self.__setLatestRemoteId(meta.meta_id)
# Check if local image is sync with remote digest id (check up-to-date status)
self.__is_update = self.__digest == self.__profile_digest
self.__is_update = self.__profile_digest in self.__digest # unlike docker, podman associates multiple digests to an image
if not self.__digest and meta.is_latest and meta.meta_id:
# If the digest is lost (multiple same image installed locally) fallback to meta id (only if latest)
self.__setDigest(meta.meta_id)
Expand Down Expand Up @@ -230,7 +232,7 @@ def __labelVersionParsing(self):
self.__profile_version = self.__image_version

@classmethod
def parseAliasTagName(cls, image: Image) -> str:
def parseAliasTagName(cls, image: Union[DockerImage, PodmanImage]) -> str:
"""Create a tag name alias from labels when image's tag is lost"""
return image.labels.get("org.exegol.tag", "<none>") + "-" + image.labels.get("org.exegol.version", "v?")

Expand All @@ -247,7 +249,7 @@ def syncStatus(self):
else:
self.__custom_status = ""

def syncContainerData(self, container: Container):
def syncContainerData(self, container: Union[DockerImage, PodmanImage]):
"""Synchronization between the container and the image.
If the image has been updated, the tag is lost,
but it is saved in the properties of the container that still uses it."""
Expand Down Expand Up @@ -293,7 +295,7 @@ def autoLoad(self, from_cache: bool = True) -> 'ExegolImage':
self.__setLatestRemoteId(remote_digest)
if self.__digest:
# Compare current and remote latest digest for up-to-date status
self.__is_update = self.__digest == self.__profile_digest
self.__is_update = self.__profile_digest in self.__digest # unlike docker, podman associates multiple digests to an image
if version is not None:
# Set latest remote version
self.__setLatestVersion(version)
Expand Down Expand Up @@ -352,7 +354,7 @@ def __mergeMetaImages(cls, images: List[MetaImages]):
pass

@classmethod
def mergeImages(cls, remote_images: List[MetaImages], local_images: List[Image], status: Status) -> List['ExegolImage']:
def mergeImages(cls, remote_images: List[MetaImages], local_images: List[Union[DockerImage, PodmanImage]], 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 @@ -479,7 +481,7 @@ def __eq__(self, other):
"""Operation == overloading for ExegolImage object"""
# How to compare two ExegolImage
if type(other) is ExegolImage:
return self.__name == other.__name and self.__digest == other.__digest and self.__arch == other.__arch
return self.__name == other.__name and other.__digest in self.__digest and self.__arch == other.__arch
# How to compare ExegolImage with str
elif type(other) is str:
return self.__name == other
Expand Down Expand Up @@ -531,19 +533,26 @@ def getType(self) -> str:
"""Image type getter"""
return "remote" if self.__is_remote else "local"

def __setDigest(self, digest: Optional[str]):
def __setDigest(self, digests: Optional[List[str]]):
"""Remote image digest setter"""
if digest is not None:
self.__digest = digest
if digests is not None and isinstance(digests, list):
self.__digest = digests # Store the entire list
elif isinstance(digests, str): # Handle backward compatibility
self.__digest = [digests] # Convert single digest to a list
else:
self.__digest = None # No digest

@staticmethod
def __parseDigest(docker_image: Image) -> str:
"""Parse the remote image digest ID.
Return digest id from the docker object."""
def __parseDigest(docker_image: Union[DockerImage, PodmanImage]) -> List[str]:
"""Parse the remote image digest IDs.
Return a list of digest IDs from the docker object.
Note that a list is returned because Podman allows
multiple digests to be associated with an image. """
digests = []
for digest_id in docker_image.attrs["RepoDigests"]:
if digest_id.startswith(ConstantConfig.IMAGE_NAME): # Find digest id from the right repository
return digest_id.split('@')[1]
return ""
if ConstantConfig.IMAGE_NAME in digest_id:
digests.append(digest_id.split('@')[1])
return digests

def getRemoteId(self) -> str:
"""Remote digest getter"""
Expand All @@ -558,9 +567,14 @@ def getLatestRemoteId(self) -> str:
return self.__profile_digest

def __setImageId(self, image_id: Optional[str]):
"""Local image id setter"""
"""Local image id setter for both Docker and Podman"""
if image_id is not None:
self.__image_id = image_id.split(":")[1][:12]
# Check if the image_id contains a colon (as in Docker's format)
if ":" in image_id:
self.__image_id = image_id.split(":")[1][:12]
else:
# For Podman, where image_id does not contain the 'sha256:' prefix
self.__image_id = image_id[:12]

def getLocalId(self) -> str:
"""Local id getter"""
Expand Down
9 changes: 5 additions & 4 deletions exegol/model/MetaImages.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional, Set, Union

from docker.models.images import Image
from docker.models.images import Image as DockerImage
from podman.domain.images import Image as PodmanImage

from exegol.utils.ExeLog import logger
from exegol.utils.WebUtils import WebUtils
Expand Down Expand Up @@ -58,13 +59,13 @@ def tagNameParsing(tag_name: str) -> str:
return version

@staticmethod
def parseArch(docker_image: Union[dict, Image]) -> str:
def parseArch(docker_image: Union[dict, DockerImage, PodmanImage]) -> str:
"""Parse and format arch in dockerhub style from registry dict struct.
Return arch in format 'arch/variant'."""
arch_key = "architecture"
variant_key = "variant"
# Support Docker image struct with specific dict key
if type(docker_image) is Image:
# Support Docker and Podman image struct with specific dict key
if isinstance(docker_image, (DockerImage, PodmanImage)):
docker_image = docker_image.attrs
arch_key = "Architecture"
variant_key = "Variant"
Expand Down
7 changes: 4 additions & 3 deletions exegol/utils/ContainerLogStream.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import time
from datetime import datetime, timedelta
from typing import Optional
from typing import Optional, Union

from docker.models.containers import Container
from docker.models.containers import Container as DockerContainer
from podman.domain.containers import Container as PodmanContainer

from exegol.utils.ExeLog import logger


class ContainerLogStream:

def __init__(self, container: Container, start_date: Optional[datetime] = None, timeout: int = 5):
def __init__(self, container: Union[DockerContainer, PodmanContainer], start_date: Optional[datetime] = None, timeout: int = 5):
# Container to extract logs from
self.__container = container
# Fetch more logs from this datetime
Expand Down
Loading