Source code for pi_dashboard.docker_container_handler

"""Docker container management handler."""

import logging

import docker
from docker.errors import APIError

from pi_dashboard.models import DockerContainer

logger = logging.getLogger(__name__)


[docs] class DockerContainerHandler: """Handler for Docker container operations."""
[docs] def __init__(self) -> None: """Initialize the DockerContainerHandler.""" try: self.client = docker.from_env() self.client.ping() logger.info("Successfully connected to Docker daemon") except Exception: logger.exception("Failed to connect to Docker daemon") self.client = None
@staticmethod def _extract_primary_port(ports: dict) -> str | None: """Extract the primary (first) port from Docker container. :param dict ports: Docker ports dictionary :return str | None: The first host port as a string, or None if no ports """ if not ports: return None for host_bindings in ports.values(): if host_bindings is None: continue for binding in host_bindings: host_port = binding.get("HostPort", "") if host_port: return str(host_port) return None def _check_docker_available(self) -> None: """Check if Docker daemon is available. :raises APIError: If Docker daemon is not available """ if not self.client: msg = "Docker daemon not available" logger.error(msg) raise APIError(msg)
[docs] def list_containers(self) -> list[DockerContainer]: """List all Docker containers. :return list[DockerContainer]: List of containers """ self._check_docker_available() containers = self.client.containers.list(all=True) docker_containers: list[DockerContainer] = [] for container in containers: image_name = container.image.tags[0] if container.image.tags else container.image.id[:12] primary_port = DockerContainerHandler._extract_primary_port(container.ports) docker_containers.append( DockerContainer( container_id=container.short_id, name=container.name, image=image_name, status=container.status, port=primary_port, ) ) return docker_containers
[docs] def start_container(self, container_id: str) -> str: """Start a Docker container. :param str container_id: The container ID to start :return str: The name of the started container """ self._check_docker_available() container = self.client.containers.get(container_id) container.start() logger.info("Started container: %s (%s)", container.name, container_id) return str(container.name)
[docs] def stop_container(self, container_id: str, timeout: int) -> str: """Stop a Docker container. :param str container_id: The container ID to stop :param int timeout: Timeout in seconds before forcefully killing the container :return str: The name of the stopped container """ self._check_docker_available() container = self.client.containers.get(container_id) container.stop(timeout=timeout) logger.info("Stopped container: %s (%s)", container.name, container_id) return str(container.name)
[docs] def restart_container(self, container_id: str, timeout: int) -> str: """Restart a Docker container. :param str container_id: The container ID to restart :param int timeout: Timeout in seconds before forcefully killing the container :return str: The name of the restarted container """ self._check_docker_available() container = self.client.containers.get(container_id) container.restart(timeout=timeout) logger.info("Restarted container: %s (%s)", container.name, container_id) return str(container.name)
[docs] def update_container(self, container_id: str, timeout: int) -> tuple[str, str]: """Update a Docker container by pulling latest image and recreating it. :param str container_id: The container ID to update :param int timeout: Timeout in seconds before forcefully killing the container :return tuple[str, str]: Tuple of (container_name, new_container_id) """ self._check_docker_available() container = self.client.containers.get(container_id) container_name = container.name # Get image information image_tags = container.image.tags if not image_tags: msg = f"Container {container_name} has an image with no tags; cannot update." logger.error(msg) raise APIError(msg) image_name = image_tags[0] # Get container configuration config = container.attrs["Config"] host_config = container.attrs["HostConfig"] logger.info("Pulling latest image: %s", image_name) self.client.images.pull(image_name) # Stop and remove old container logger.info("Stopping and removing container: %s", container_name) container.stop(timeout=timeout) container.remove() logger.info("Creating new container with updated image: %s", container_name) # Create new container with same configuration new_container = self.client.containers.run( image=image_name, name=container_name, ports=host_config.get("PortBindings"), volumes=host_config.get("Binds"), environment=config.get("Env"), network_mode=host_config.get("NetworkMode"), restart_policy=host_config.get("RestartPolicy"), detach=True, ) logger.info("Container updated successfully: %s (%s)", container_name, new_container.short_id) return container_name, new_container.short_id
[docs] def get_container_logs(self, container_id: str, lines: int) -> list[str]: """Get logs for a Docker container. :param str container_id: The container ID :param int lines: Number of log lines to retrieve (tail) :return list[str]: List of log lines """ self._check_docker_available() container = self.client.containers.get(container_id) raw_logs: bytes = container.logs(tail=lines, timestamps=False, stream=False) decoded = raw_logs.decode("utf-8", errors="replace") return [line for line in decoded.splitlines() if line]