done fix
This commit is contained in:
163
agent/app/core/docker_installer.py
Normal file
163
agent/app/core/docker_installer.py
Normal file
@@ -0,0 +1,163 @@
|
||||
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) -> None:
|
||||
result = self.command_runner.run([
|
||||
"docker",
|
||||
"ps",
|
||||
"-aq",
|
||||
"--filter",
|
||||
f"name=^/{container_name}$",
|
||||
])
|
||||
if result.stdout.strip():
|
||||
self.command_runner.run(["docker", "rm", "-f", container_name])
|
||||
|
||||
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}")
|
||||
@@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.config import settings
|
||||
from app.models.schemas import AppManifest, DebComponent
|
||||
from app.utils.validators import validate_url_host
|
||||
from app.models.schemas import AppManifest, DebComponent, DockerComponent
|
||||
from app.utils.validators import validate_docker_registry, validate_url_host
|
||||
|
||||
|
||||
class ManifestValidator:
|
||||
@@ -15,12 +15,17 @@ class ManifestValidator:
|
||||
component = DebComponent.model_validate(raw_component).model_dump(by_alias=True)
|
||||
validate_url_host(component["downloadUrl"], settings.allowed_download_hosts)
|
||||
components.append(component)
|
||||
elif component_type == "docker" and not settings.allow_docker:
|
||||
raise ValueError("Docker components are not enabled on this Agent")
|
||||
elif component_type == "docker_compose" and not settings.allow_docker_compose:
|
||||
raise ValueError("Docker Compose components are not enabled on this Agent")
|
||||
elif component_type == "docker":
|
||||
if not settings.allow_docker:
|
||||
raise ValueError("Docker components are not enabled on this Agent")
|
||||
component = DockerComponent.model_validate(raw_component).model_dump(by_alias=True)
|
||||
validate_docker_registry(component["image"], settings.allowed_docker_registries)
|
||||
components.append(component)
|
||||
elif component_type == "docker_compose":
|
||||
if not settings.allow_docker_compose:
|
||||
raise ValueError("Docker Compose components are not enabled on this Agent")
|
||||
raise ValueError("Docker Compose components are not implemented on this Agent")
|
||||
else:
|
||||
raise ValueError(f"Unsupported component type: {component_type}")
|
||||
manifest["components"] = sorted(components, key=lambda item: item.get("installOrder", 10))
|
||||
return manifest
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from app.config import settings
|
||||
from app.core.checksum import sha256_file
|
||||
from app.core.command_runner import CommandRunner
|
||||
from app.core.downloader import Downloader
|
||||
from app.core.docker_installer import DockerInstaller, image_reference
|
||||
from app.core.installer import DebInstaller
|
||||
from app.core.manifest_client import ManifestClient
|
||||
from app.core.manifest_validator import ManifestValidator
|
||||
@@ -99,6 +100,12 @@ class TaskRunner:
|
||||
if component["type"] == "deb" and package_name:
|
||||
self.repository.add_log(task_id, "info", f"Removing package {package_name}")
|
||||
installer.remove_package(package_name, purge=request.purge)
|
||||
elif component["type"] == "docker":
|
||||
container_name = component.get("container_name") or component_id
|
||||
self.repository.add_log(task_id, "info", f"Removing Docker container {container_name}")
|
||||
docker_installer = DockerInstaller(command_runner)
|
||||
docker_installer.ensure_runtime(auto_install=settings.auto_install_docker)
|
||||
docker_installer.remove_container(container_name)
|
||||
else:
|
||||
raise ValueError(f"Unsupported installed component type: {component['type']}")
|
||||
|
||||
@@ -171,14 +178,16 @@ class TaskRunner:
|
||||
component_id,
|
||||
status="running",
|
||||
progress=5,
|
||||
current_step="downloading",
|
||||
current_step="preparing",
|
||||
started_at=utc_now(),
|
||||
)
|
||||
|
||||
if component["type"] != "deb":
|
||||
raise ValueError(f"Unsupported component type in MVP: {component['type']}")
|
||||
|
||||
self._install_deb_component(task_id, manifest["appId"], component)
|
||||
if component["type"] == "deb":
|
||||
self._install_deb_component(task_id, manifest["appId"], component)
|
||||
elif component["type"] == "docker":
|
||||
self._install_docker_component(task_id, manifest["appId"], component)
|
||||
else:
|
||||
raise ValueError(f"Unsupported component type: {component['type']}")
|
||||
|
||||
self.repository.update_task_component(
|
||||
task_id,
|
||||
@@ -196,6 +205,7 @@ class TaskRunner:
|
||||
installer = DebInstaller(command_runner)
|
||||
services = ServiceManager(command_runner)
|
||||
|
||||
self.repository.update_task_component(task_id, component_id, progress=10, current_step="downloading")
|
||||
package_path = downloader.download(component["downloadUrl"])
|
||||
self.repository.update_task_component(task_id, component_id, progress=35, current_step="verifying checksum")
|
||||
actual_sha256 = sha256_file(package_path)
|
||||
@@ -250,6 +260,32 @@ class TaskRunner:
|
||||
|
||||
self.repository.upsert_installed_component(app_id, component)
|
||||
|
||||
def _install_docker_component(self, task_id: str, app_id: str, component: dict[str, Any]) -> None:
|
||||
component_id = component["componentId"]
|
||||
container_name = component["containerName"]
|
||||
reference = image_reference(component)
|
||||
command_runner = CommandRunner(self.repository, task_id)
|
||||
installer = DockerInstaller(command_runner)
|
||||
|
||||
self.repository.update_task_component(task_id, component_id, progress=15, current_step="checking Docker runtime")
|
||||
installer.ensure_runtime(auto_install=settings.auto_install_docker)
|
||||
|
||||
self.repository.update_task_component(task_id, component_id, progress=35, current_step="pulling image")
|
||||
self.repository.add_log(task_id, "info", f"Pulling Docker image {reference}")
|
||||
installer.pull_image(reference)
|
||||
|
||||
self.repository.update_task_component(task_id, component_id, progress=70, current_step="recreating container")
|
||||
self.repository.add_log(task_id, "info", f"Recreating Docker container {container_name}")
|
||||
installer.recreate_container(app_id, component)
|
||||
|
||||
self.repository.update_task_component(task_id, component_id, progress=90, current_step="verifying container")
|
||||
installer.assert_container_running(container_name)
|
||||
self.repository.add_log(task_id, "info", f"Docker container {container_name} is running")
|
||||
|
||||
installed_component = dict(component)
|
||||
installed_component["image"] = reference
|
||||
self.repository.upsert_installed_component(app_id, installed_component)
|
||||
|
||||
def _mark_started(self, task_id: str, step: str) -> None:
|
||||
self.repository.update_task(
|
||||
task_id,
|
||||
@@ -261,12 +297,24 @@ class TaskRunner:
|
||||
self.repository.add_log(task_id, "info", step)
|
||||
|
||||
def _fail_task(self, task_id: str, error: Exception) -> None:
|
||||
task = self.repository.get_task(task_id)
|
||||
component_id = task.get("current_component_id") if task else None
|
||||
finished_at = utc_now()
|
||||
if component_id:
|
||||
self.repository.update_task_component(
|
||||
task_id,
|
||||
component_id,
|
||||
status="failed",
|
||||
current_step="failed",
|
||||
error_message=str(error),
|
||||
finished_at=finished_at,
|
||||
)
|
||||
self.repository.update_task(
|
||||
task_id,
|
||||
status="failed",
|
||||
current_step="failed",
|
||||
error_message=str(error),
|
||||
finished_at=utc_now(),
|
||||
finished_at=finished_at,
|
||||
)
|
||||
self.repository.add_log(task_id, "error", str(error))
|
||||
self.repository.add_log(task_id, "debug", traceback.format_exc())
|
||||
|
||||
Reference in New Issue
Block a user