Files
InstallerRobot/agent/app/core/docker_installer.py
2026-06-08 10:54:52 +07:00

191 lines
6.5 KiB
Python

from __future__ import annotations
import shutil
from app.core.command_runner import CommandError, CommandRunner
DOCKER_REQUIRED_MESSAGE = "Docker runtime is required"
def image_reference(component: dict) -> str:
image = component["image"]
digest = component.get("digest")
tag = component.get("tag")
if "@" in image:
return image
if digest:
return f"{_image_without_tag(image)}@{digest}"
if _image_has_tag(image) or not tag:
return image
return f"{image}:{tag}"
def _image_has_tag(image: str) -> bool:
last_slash = image.rfind("/")
last_colon = image.rfind(":")
return last_colon > last_slash
def _image_without_tag(image: str) -> str:
if not _image_has_tag(image):
return image
return image[: image.rfind(":")]
class DockerInstaller:
def __init__(self, command_runner: CommandRunner) -> None:
self.command_runner = command_runner
def ensure_runtime(self, auto_install: bool = False) -> None:
if not shutil.which("docker"):
if not auto_install:
raise RuntimeError(DOCKER_REQUIRED_MESSAGE)
self.install_runtime()
try:
self._verify_runtime()
return
except CommandError as error:
self._log("warning", self._runtime_error_message(error))
self._start_runtime()
try:
self._verify_runtime()
except CommandError as error:
raise RuntimeError(self._runtime_error_message(error)) from error
def install_runtime(self) -> None:
if not shutil.which("apt-get"):
raise RuntimeError(
f"{DOCKER_REQUIRED_MESSAGE}. Install Docker Engine on this client or disable Docker packages."
)
self._log("info", "Docker runtime not found; installing docker.io")
try:
self.command_runner.run(["apt-get", "update"], timeout=600)
self.command_runner.run(
["env", "DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y", "docker.io"],
timeout=1200,
)
except CommandError as error:
raise RuntimeError(
f"{DOCKER_REQUIRED_MESSAGE}: failed to install docker.io with apt-get: "
f"{self._command_error_detail(error)}"
) from error
self._start_runtime()
def _verify_runtime(self) -> None:
self.command_runner.run(["docker", "version", "--format", "{{.Server.Version}}"])
def _start_runtime(self) -> None:
if shutil.which("systemctl"):
try:
self.command_runner.run(["systemctl", "enable", "--now", "docker"], timeout=120)
return
except CommandError as error:
self._log("warning", f"Could not start Docker with systemctl: {self._command_error_detail(error)}")
if shutil.which("service"):
try:
self.command_runner.run(["service", "docker", "start"], timeout=120)
except CommandError as error:
self._log("warning", f"Could not start Docker service: {self._command_error_detail(error)}")
def _runtime_error_message(self, error: CommandError) -> str:
detail = self._command_error_detail(error)
return f"{DOCKER_REQUIRED_MESSAGE}: {detail}" if detail else DOCKER_REQUIRED_MESSAGE
def _command_error_detail(self, error: CommandError) -> str:
output = (error.stderr or error.stdout or "").strip()
if not output:
return str(error)
return output.splitlines()[-1].strip()
def _log(self, level: str, message: str) -> None:
if self.command_runner.task_id:
self.command_runner.repository.add_log(self.command_runner.task_id, level, message)
def pull_image(self, reference: str) -> None:
self.command_runner.run(["docker", "pull", reference])
def recreate_container(self, app_id: str, component: dict) -> None:
container_name = component["containerName"]
reference = image_reference(component)
self.remove_container(container_name)
command = [
"docker",
"run",
"-d",
"--name",
container_name,
"--restart",
component.get("restartPolicy", "unless-stopped"),
"--label",
f"local-installer-agent.app-id={app_id}",
"--label",
f"local-installer-agent.component-id={component['componentId']}",
]
for name, value in sorted((component.get("env") or {}).items()):
command.extend(["-e", f"{name}={value}"])
for port in component.get("ports") or []:
command.extend(["-p", port])
for volume in component.get("volumes") or []:
command.extend(["-v", volume])
command.append(reference)
self.command_runner.run(command)
def remove_container(self, container_name: str, remove_volumes: bool = False) -> None:
result = self.command_runner.run([
"docker",
"ps",
"-aq",
"--filter",
f"name=^/{container_name}$",
])
if result.stdout.strip():
command = ["docker", "rm", "-f"]
if remove_volumes:
command.append("-v")
command.append(container_name)
self.command_runner.run(command)
def remove_labeled_containers(self, app_id: str, component_id: str, remove_volumes: bool = False) -> None:
result = self.command_runner.run([
"docker",
"ps",
"-aq",
"--filter",
f"label=local-installer-agent.app-id={app_id}",
"--filter",
f"label=local-installer-agent.component-id={component_id}",
])
container_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()]
if not container_ids:
return
command = ["docker", "rm", "-f"]
if remove_volumes:
command.append("-v")
command.extend(container_ids)
self.command_runner.run(command)
def remove_image(self, reference: str) -> None:
self.command_runner.run(["docker", "image", "rm", reference])
def assert_container_running(self, container_name: str) -> None:
result = self.command_runner.run([
"docker",
"inspect",
"-f",
"{{.State.Running}}",
container_name,
])
if result.stdout.strip().lower() != "true":
raise RuntimeError(f"Docker container is not running: {container_name}")