update agent
This commit is contained in:
@@ -38,6 +38,7 @@ class Settings:
|
||||
robot_package_base_url: str
|
||||
allowed_origins: list[str]
|
||||
allowed_download_hosts: list[str]
|
||||
allowed_apt_packages: list[str]
|
||||
allowed_docker_registries: list[str]
|
||||
cache_dir: Path
|
||||
app_dir: Path
|
||||
@@ -83,6 +84,10 @@ def get_settings() -> Settings:
|
||||
os.getenv("ALLOWED_DOWNLOAD_HOSTS"),
|
||||
_default_allowed_download_hosts(robot_package_base_url),
|
||||
),
|
||||
allowed_apt_packages=_csv(
|
||||
os.getenv("ALLOWED_APT_PACKAGES"),
|
||||
["postgresql"],
|
||||
),
|
||||
allowed_docker_registries=_csv_with_defaults(
|
||||
os.getenv("ALLOWED_DOCKER_REGISTRIES"),
|
||||
["registry.robot.package", "docker.io"],
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.command_runner import CommandRunner
|
||||
from app.core.command_runner import CommandError, CommandRunner
|
||||
|
||||
|
||||
APT_DPKG_OPTIONS = [
|
||||
@@ -101,3 +102,42 @@ class DebInstaller:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class AptInstaller(DebInstaller):
|
||||
def update_package_index(self) -> None:
|
||||
self.command_runner.run(
|
||||
["apt-get", "update"],
|
||||
timeout=600,
|
||||
env=APT_NONINTERACTIVE_ENV,
|
||||
)
|
||||
|
||||
def install_package(self, package_name: str) -> None:
|
||||
self.command_runner.run(
|
||||
[
|
||||
"apt-get",
|
||||
*APT_DPKG_OPTIONS,
|
||||
"install",
|
||||
"--yes",
|
||||
package_name,
|
||||
],
|
||||
timeout=1200,
|
||||
env=APT_NONINTERACTIVE_ENV,
|
||||
)
|
||||
|
||||
def wait_for_postgresql(
|
||||
self,
|
||||
attempts: int = 6,
|
||||
delay_seconds: float = 2.0,
|
||||
) -> None:
|
||||
last_error: CommandError | None = None
|
||||
for attempt in range(1, attempts + 1):
|
||||
try:
|
||||
self.command_runner.run(["pg_isready", "--timeout=5"], timeout=10)
|
||||
return
|
||||
except CommandError as error:
|
||||
last_error = error
|
||||
if attempt < attempts:
|
||||
time.sleep(delay_seconds)
|
||||
|
||||
raise RuntimeError("PostgreSQL did not become ready after installation") from last_error
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.config import settings
|
||||
from app.models.schemas import AppManifest, DebComponent, DockerComponent
|
||||
from app.models.schemas import AptComponent, AppManifest, DebComponent, DockerComponent
|
||||
from app.utils.validators import validate_docker_registry, validate_url_host
|
||||
|
||||
|
||||
@@ -15,6 +15,14 @@ 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 == "apt":
|
||||
component = AptComponent.model_validate(raw_component).model_dump(by_alias=True)
|
||||
allowed_packages = set(settings.allowed_apt_packages)
|
||||
if component["packageName"] not in allowed_packages:
|
||||
raise ValueError(
|
||||
f"APT package is not allowed: {component['packageName']}"
|
||||
)
|
||||
components.append(component)
|
||||
elif component_type == "docker":
|
||||
if not settings.allow_docker:
|
||||
raise ValueError("Docker components are not enabled on this Agent")
|
||||
|
||||
@@ -25,6 +25,11 @@ class ServiceManager:
|
||||
def reset_failed(self, service_name: str) -> None:
|
||||
self.command_runner.run(["systemctl", "reset-failed", service_name])
|
||||
|
||||
def assert_service_active(self, service_name: str) -> None:
|
||||
result = self.command_runner.run(["systemctl", "is-active", service_name])
|
||||
if result.stdout.strip() != "active":
|
||||
raise RuntimeError(f"Service is not active: {service_name}")
|
||||
|
||||
def get_service_status(self, service_name: str) -> dict[str, object]:
|
||||
active = self._query(["systemctl", "is-active", service_name]) == "active"
|
||||
enabled = self._query(["systemctl", "is-enabled", service_name]) == "enabled"
|
||||
|
||||
@@ -10,7 +10,7 @@ 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.installer import AptInstaller, DebInstaller
|
||||
from app.core.manifest_client import ManifestClient
|
||||
from app.core.manifest_validator import ManifestValidator
|
||||
from app.core.service_manager import ServiceManager
|
||||
@@ -86,7 +86,7 @@ class TaskRunner:
|
||||
|
||||
ordered = sorted(components, key=lambda item: item["install_order"], reverse=True)
|
||||
total = len(ordered)
|
||||
removed_deb_package = False
|
||||
removed_apt_package = False
|
||||
for index, component in enumerate(ordered, start=1):
|
||||
progress = int((index - 1) / total * 80) + 10
|
||||
component_id = component["component_id"]
|
||||
@@ -103,13 +103,14 @@ class TaskRunner:
|
||||
self._best_effort(task_id, f"disable service {service_name}", lambda: services.disable_service(service_name))
|
||||
|
||||
package_name = component.get("package_name")
|
||||
if component["type"] == "deb" and package_name:
|
||||
if component["type"] in {"deb", "apt"} and package_name:
|
||||
self.repository.add_log(task_id, "info", f"Removing package {package_name}")
|
||||
installer.remove_package(package_name, purge=effective_purge)
|
||||
self._clean_cached_package_files(task_id, package_name, component_id)
|
||||
if component["type"] == "deb":
|
||||
self._clean_cached_package_files(task_id, package_name, component_id)
|
||||
if service_name:
|
||||
self._best_effort(task_id, f"reset failed state for {service_name}", lambda: services.reset_failed(service_name))
|
||||
removed_deb_package = True
|
||||
removed_apt_package = True
|
||||
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}")
|
||||
@@ -127,7 +128,7 @@ class TaskRunner:
|
||||
else:
|
||||
raise ValueError(f"Unsupported installed component type: {component['type']}")
|
||||
|
||||
if removed_deb_package:
|
||||
if removed_apt_package:
|
||||
self.repository.update_task(task_id, progress=92, current_step="cleaning package leftovers")
|
||||
self._best_effort(
|
||||
task_id,
|
||||
@@ -210,6 +211,8 @@ class TaskRunner:
|
||||
|
||||
if component["type"] == "deb":
|
||||
self._install_deb_component(task_id, manifest["appId"], component)
|
||||
elif component["type"] == "apt":
|
||||
self._install_apt_component(task_id, manifest["appId"], component)
|
||||
elif component["type"] == "docker":
|
||||
self._install_docker_component(task_id, manifest["appId"], component)
|
||||
else:
|
||||
@@ -286,6 +289,73 @@ class TaskRunner:
|
||||
|
||||
self.repository.upsert_installed_component(app_id, component)
|
||||
|
||||
def _install_apt_component(self, task_id: str, app_id: str, component: dict[str, Any]) -> None:
|
||||
component_id = component["componentId"]
|
||||
package_name = component["packageName"]
|
||||
command_runner = CommandRunner(self.repository, task_id)
|
||||
installer = AptInstaller(command_runner)
|
||||
services = ServiceManager(command_runner)
|
||||
|
||||
self.repository.update_task_component(
|
||||
task_id,
|
||||
component_id,
|
||||
progress=10,
|
||||
current_step="refreshing APT package index",
|
||||
)
|
||||
self.repository.add_log(task_id, "info", "Refreshing APT package index")
|
||||
installer.update_package_index()
|
||||
|
||||
self.repository.update_task_component(
|
||||
task_id,
|
||||
component_id,
|
||||
progress=35,
|
||||
current_step=f"installing APT package {package_name}",
|
||||
)
|
||||
self.repository.add_log(task_id, "info", f"Installing trusted APT package {package_name}")
|
||||
installer.install_package(package_name)
|
||||
|
||||
self.repository.update_task_component(
|
||||
task_id,
|
||||
component_id,
|
||||
progress=70,
|
||||
current_step="verifying installed package",
|
||||
)
|
||||
installed_version = installer.get_package_version(package_name)
|
||||
if not installed_version:
|
||||
raise RuntimeError(f"APT package was not installed: {package_name}")
|
||||
self.repository.add_log(
|
||||
task_id,
|
||||
"info",
|
||||
f"APT package {package_name} installed with version {installed_version}",
|
||||
)
|
||||
|
||||
installed_component = dict(component)
|
||||
installed_component["version"] = installed_version
|
||||
|
||||
if package_name == "postgresql":
|
||||
service_name = "postgresql.service"
|
||||
self.repository.update_task_component(
|
||||
task_id,
|
||||
component_id,
|
||||
progress=85,
|
||||
current_step="starting PostgreSQL service",
|
||||
)
|
||||
services.enable_service(service_name)
|
||||
services.start_service(service_name)
|
||||
services.assert_service_active(service_name)
|
||||
|
||||
self.repository.update_task_component(
|
||||
task_id,
|
||||
component_id,
|
||||
progress=95,
|
||||
current_step="checking PostgreSQL readiness",
|
||||
)
|
||||
installer.wait_for_postgresql()
|
||||
self.repository.add_log(task_id, "info", "PostgreSQL service is active and accepting connections")
|
||||
installed_component["serviceName"] = service_name
|
||||
|
||||
self.repository.upsert_installed_component(app_id, installed_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"]
|
||||
|
||||
@@ -23,7 +23,7 @@ from app.utils.validators import (
|
||||
|
||||
TaskType = Literal["install", "update", "remove"]
|
||||
TaskStatus = Literal["queued", "running", "success", "failed", "cancelled"]
|
||||
ComponentType = Literal["deb", "docker", "docker_compose"]
|
||||
ComponentType = Literal["deb", "apt", "docker", "docker_compose"]
|
||||
|
||||
|
||||
class CamelModel(BaseModel):
|
||||
@@ -176,6 +176,30 @@ class DebComponent(CamelModel):
|
||||
return validate_service_name(value)
|
||||
|
||||
|
||||
class AptComponent(CamelModel):
|
||||
component_id: str = Field(alias="componentId")
|
||||
type: Literal["apt"] = "apt"
|
||||
install_order: int = Field(default=10, alias="installOrder")
|
||||
required: bool = True
|
||||
package_name: str = Field(alias="packageName")
|
||||
version: str | None = None
|
||||
|
||||
@field_validator("component_id")
|
||||
@classmethod
|
||||
def _component_id(cls, value: str) -> str:
|
||||
return validate_app_id(value)
|
||||
|
||||
@field_validator("package_name")
|
||||
@classmethod
|
||||
def _package_name(cls, value: str) -> str:
|
||||
return validate_package_name(value)
|
||||
|
||||
@field_validator("version")
|
||||
@classmethod
|
||||
def _version(cls, value: str | None) -> str | None:
|
||||
return validate_version(value) if value else None
|
||||
|
||||
|
||||
class DockerComponent(CamelModel):
|
||||
component_id: str = Field(alias="componentId")
|
||||
type: Literal["docker"] = "docker"
|
||||
|
||||
Reference in New Issue
Block a user