Source code for minitrino.core.docker.wrappers

"""Minitrino resource management.

This module provides classes and functions to manage Docker resources (containers,
volumes, images, networks). All Docker objects are associated with a cluster name
**except** for images, which are global.
"""

from abc import ABC, abstractmethod

from docker.models.containers import Container
from docker.models.images import Image
from docker.models.networks import Network
from docker.models.volumes import Volume

from minitrino.settings import COMPOSE_LABEL_KEY


[docs] class MinitrinoDockerObjectMixin(ABC): """Abstract base mixin for Minitrino Docker objects. Parameters ---------- cluster_name : Optional[str] The name of the cluster this Docker object belongs to. """ def __init__(self, cluster_name: str | None = None): self._cluster_name = cluster_name @property def cluster_name(self) -> str | None: """Return the cluster name associated with this object.""" return self._cluster_name def __repr__(self): """Return a string representation of the object.""" # Try to show name if available, else just id name = getattr(self, "name", None) if name and name != "<unknown>": return ( f"<{self.kind} name={name} id={self.id} cluster={self._cluster_name}>" ) return f"<{self.kind} id={self.id} cluster={self._cluster_name}>" @property def labels(self) -> dict[str, str]: """Retrieve Docker labels from the underlying Docker object.""" base = getattr(self, "_base", None) if base is None: return getattr(self, "labels", {}) or {} attrs = getattr(base, "attrs", None) if isinstance(attrs, dict): if isinstance(self, MinitrinoContainer) and "Config" in attrs: return attrs["Config"].get("Labels", {}) or {} return attrs.get("Labels", {}) or {} return getattr(base, "labels", {}) or {} @property @abstractmethod def id(self) -> str: """The unique identifier of the Docker object. Returns ------- str The object ID string. """ pass @property @abstractmethod def kind(self) -> str: """The kind of the Docker object. Returns ------- str The string identifier of the object kind (e.g., "container"). """ pass
[docs] class MinitrinoContainer(MinitrinoDockerObjectMixin, Container): """Minitrino-wrapped container object. Parameters ---------- base : Container The base Docker container to wrap. cluster_name : str The name of the cluster this container belongs to. """ def __init__(self, base: Container, cluster_name: str | None = None): super().__init__(cluster_name) self._base = base self.__dict__.update(base.__dict__) @property def id(self) -> str: """ID of the container.""" return str(getattr(self._base, "id", "<unknown>") or "<unknown>") @property def name(self) -> str: """Name of the container.""" return str(getattr(self._base, "name", "<unknown>") or "<unknown>") @property def kind(self) -> str: """Kind of object.""" return "container" @property def cluster_name(self) -> str: """Cluster name associated with the container.""" from minitrino.settings import COMPOSE_LABEL_KEY labels = self.labels project = labels.get(COMPOSE_LABEL_KEY) if labels else None if project and project.startswith("minitrino-"): return project.split("minitrino-")[1] return str(getattr(self, "_cluster_name", None)) or str(super().cluster_name)
[docs] def ports_and_host_endpoints(self) -> tuple[list[str], list[str]]: """Get published and exposed ports and host endpoints. Returns ------- tuple[list[str], list[str]] - ports: Published ports as 'host_port:container_port', exposed-only as 'container_port' - host_endpoints: Published host endpoints as 'localhost:host_port' """ ports_dict = self.attrs.get("NetworkSettings", {}).get("Ports", {}) exposed_ports_dict = self.attrs.get("Config", {}).get("ExposedPorts", {}) port_mappings = set() host_endpoints = set() published_container_ports = set() # Published ports for container_port, mappings in (ports_dict or {}).items(): port_num = ( container_port.split("/")[0] if "/" in container_port else container_port ) if mappings: for mapping in mappings: host_port = mapping.get("HostPort") if host_port: port_mappings.add(f"{host_port}:{port_num}") host_endpoints.add(f"localhost:{host_port}") published_container_ports.add(port_num) # Exposed-only ports (not published) for exposed_port in exposed_ports_dict or {}: port_num = ( exposed_port.split("/")[0] if "/" in exposed_port else exposed_port ) if port_num not in published_container_ports: port_mappings.add(f"{port_num}") return ( sorted(port_mappings, key=lambda x: (x.count(":"), x)), sorted(host_endpoints), )
[docs] class MinitrinoVolume(MinitrinoDockerObjectMixin, Volume): """Minitrino-wrapped volume object. Parameters ---------- base : Volume The base Docker volume to wrap. cluster_name : str The name of the cluster this volume belongs to. """ def __init__(self, base: Volume, cluster_name: str | None = None): super().__init__(cluster_name) self._base = base self.__dict__.update(base.__dict__) @property def id(self) -> str: """ID of the volume.""" return str(getattr(self._base, "id", "<unknown>") or "<unknown>") @property def name(self) -> str: """Name of the volume.""" return str(getattr(self._base, "name", "<unknown>") or "<unknown>") @property def kind(self) -> str: """Kind of object.""" return "volume" @property def cluster_name(self) -> str: """Cluster name associated with the volume.""" from minitrino.settings import COMPOSE_LABEL_KEY labels = self.labels project = labels.get(COMPOSE_LABEL_KEY) if labels else None if project and project.startswith("minitrino-"): return project.split("minitrino-")[1] return str(getattr(self, "_cluster_name", None)) or str(super().cluster_name)
[docs] class MinitrinoNetwork(MinitrinoDockerObjectMixin, Network): """Minitrino-wrapped network object. Parameters ---------- base : Network The base Docker network to wrap. cluster_name : str The name of the cluster this network belongs to. """ def __init__(self, base: Network, cluster_name: str | None = None): super().__init__(cluster_name) self._base = base self.__dict__.update(base.__dict__) @property def id(self) -> str: """ID of the network.""" return str(getattr(self._base, "id", "<unknown>") or "<unknown>") @property def name(self) -> str: """Name of the network.""" return str(getattr(self._base, "name", "<unknown>") or "<unknown>") @property def kind(self) -> str: """Kind of object.""" return "network" @property def cluster_name(self) -> str: """Cluster name associated with the network.""" labels = self.labels project = labels.get(COMPOSE_LABEL_KEY) if labels else None if project and project.startswith("minitrino-"): return project.split("minitrino-")[1] return str(getattr(self, "_cluster_name", None)) or str(super().cluster_name)
[docs] class MinitrinoImage(MinitrinoDockerObjectMixin, Image): """Minitrino-wrapped image object. Unlike other Docker resources, images are global and not tied to a specific cluster. For consistency, they are assigned a special cluster name: "images". Parameters ---------- base : Image The base Docker image to wrap. """ def __init__(self, base: Image): MinitrinoDockerObjectMixin.__init__(self, "images") self._base = base self.__dict__.update(base.__dict__) @property def id(self) -> str: """ID of the image.""" return getattr(self._base, "id", "<unknown>") @property def name(self) -> str: """Name of the image.""" # Images may not always have a 'name', but try tags or repo tags = getattr(self._base, "tags", None) if tags and len(tags) > 0: return tags[0] return getattr(self._base, "short_id", "<unknown>") @property def kind(self) -> str: """Kind of object.""" return "image" @property def cluster_name(self) -> str: """Cluster name associated with the image (always "images").""" return "images"
MinitrinoDockerObject = ( MinitrinoContainer | MinitrinoVolume | MinitrinoNetwork | MinitrinoImage )