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 11 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
9 changes: 6 additions & 3 deletions exegol/model/ContainerConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
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 +84,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 +135,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
37 changes: 20 additions & 17 deletions exegol/model/ExegolContainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,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 @@ -21,36 +24,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_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 @@ -118,7 +121,7 @@ def __start_container(self):
start_date = datetime.utcnow()
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 @@ -225,7 +228,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 @@ -314,7 +317,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
32 changes: 20 additions & 12 deletions exegol/model/ExegolImage.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
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 exegol.config.DataCache import DataCache
from exegol.console import ConsoleFormat
Expand All @@ -23,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 @@ -35,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 @@ -148,7 +151,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 Down Expand Up @@ -226,7 +229,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 @@ -243,7 +246,7 @@ def syncStatus(self):
else:
self.__custom_status = ""

def syncContainerData(self, container: Container):
def syncContainerData(self, container: Union[DockerContainer, PodmanContainer]):
"""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 @@ -346,7 +349,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[Union[DockerImage, PodmanImage]]) -> 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 @@ -531,11 +534,11 @@ def __setDigest(self, digest: Optional[str]):
self.__digest = digest

@staticmethod
def __parseDigest(docker_image: Image) -> str:
def __parseDigest(docker_image: Union[DockerImage, PodmanImage]) -> str:
"""Parse the remote image digest ID.
Return digest id from the docker object."""
for digest_id in docker_image.attrs["RepoDigests"]:
if digest_id.startswith(ConstantConfig.IMAGE_NAME): # Find digest id from the right repository
if ConstantConfig.IMAGE_NAME in digest_id: # Find digest id from the right repository
return digest_id.split('@')[1]
return ""

Expand All @@ -552,9 +555,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
Expand Up @@ -5,15 +5,16 @@
from datetime import datetime, timedelta
from typing import Union, List, Any, Optional

from docker.models.containers import Container
from docker.types import CancellableStream
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