from __future__ import annotations from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from app.utils.validators import ( validate_app_id, validate_container_name, validate_docker_digest, validate_docker_image, validate_docker_tag, validate_env_name, validate_package_name, validate_port_mapping, validate_restart_policy, validate_service_name, validate_sha256, validate_volume_mapping, validate_version, ) TaskType = Literal["install", "update", "remove"] TaskStatus = Literal["queued", "running", "success", "failed", "cancelled"] ComponentType = Literal["deb", "docker", "docker_compose"] class CamelModel(BaseModel): model_config = ConfigDict(populate_by_name=True, extra="forbid") class HealthResponse(CamelModel): status: str agent_version: str = Field(alias="agentVersion") hostname: str os: str architecture: str class SystemInfoResponse(CamelModel): hostname: str os: str kernel: str architecture: str disk_free: str = Field(alias="diskFree") memory_total: str = Field(alias="memoryTotal") class InstallRequest(CamelModel): app_id: str = Field(alias="appId") version: str | None = None app_name: str | None = Field(default=None, alias="appName") package_name: str | None = Field(default=None, alias="packageName") service_name: str | None = Field(default=None, alias="serviceName") download_url: str | None = Field(default=None, alias="downloadUrl") checksum: str | None = None sha256: str | None = None @field_validator("app_id") @classmethod def _app_id(cls, value: str) -> str: return validate_app_id(value) @field_validator("version") @classmethod def _version(cls, value: str | None) -> str | None: return validate_version(value) if value else None @field_validator("package_name") @classmethod def _package_name(cls, value: str | None) -> str | None: return validate_package_name(value) if value else None @field_validator("service_name") @classmethod def _service_name(cls, value: str | None) -> str | None: return validate_service_name(value) @model_validator(mode="after") def _direct_package_fields(self) -> "InstallRequest": digest = self.sha256 or self.checksum if digest: normalized = validate_sha256(digest) self.sha256 = normalized self.checksum = normalized has_direct_package = any([self.download_url, self.package_name, self.checksum, self.sha256]) if has_direct_package and not all([self.download_url, self.package_name, self.checksum or self.sha256]): raise ValueError("direct install requires packageName, downloadUrl, and checksum/sha256") return self class UpdateRequest(InstallRequest): current_version: str | None = Field(default=None, alias="currentVersion") target_version: str | None = Field(default=None, alias="targetVersion") @model_validator(mode="after") def _normalize_target(self) -> "UpdateRequest": if self.target_version: self.target_version = validate_version(self.target_version) self.version = self.target_version return self class RemoveRequest(CamelModel): app_id: str = Field(alias="appId") package_name: str | None = Field(default=None, alias="packageName") service_name: str | None = Field(default=None, alias="serviceName") purge: bool = False @field_validator("app_id") @classmethod def _app_id(cls, value: str) -> str: return validate_app_id(value) @field_validator("package_name") @classmethod def _package_name(cls, value: str | None) -> str | None: return validate_package_name(value) if value else None @field_validator("service_name") @classmethod def _service_name(cls, value: str | None) -> str | None: return validate_service_name(value) class ServiceRequest(CamelModel): service_name: str = Field(alias="serviceName") @field_validator("service_name") @classmethod def _service_name(cls, value: str) -> str: return validate_service_name(value) or value class TaskQueuedResponse(CamelModel): task_id: str = Field(alias="taskId") status: str class DebComponent(CamelModel): component_id: str = Field(alias="componentId") type: Literal["deb"] = "deb" install_order: int = Field(default=10, alias="installOrder") required: bool = True package_name: str = Field(alias="packageName") version: str download_url: str = Field(alias="downloadUrl") sha256: str service_name: str | None = Field(default=None, alias="serviceName") @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) -> str: return validate_version(value) @field_validator("sha256") @classmethod def _sha256(cls, value: str) -> str: return validate_sha256(value) @field_validator("service_name") @classmethod def _service_name(cls, value: str | None) -> str | None: return validate_service_name(value) class DockerComponent(CamelModel): component_id: str = Field(alias="componentId") type: Literal["docker"] = "docker" install_order: int = Field(default=10, alias="installOrder") required: bool = True image: str tag: str | None = None digest: str | None = None container_name: str | None = Field(default=None, alias="containerName") restart_policy: str = Field(default="unless-stopped", alias="restartPolicy") ports: list[str] = Field(default_factory=list) volumes: list[str] = Field(default_factory=list) env: dict[str, str] = Field(default_factory=dict) @field_validator("component_id") @classmethod def _component_id(cls, value: str) -> str: return validate_app_id(value) @field_validator("image") @classmethod def _image(cls, value: str) -> str: return validate_docker_image(value) @field_validator("tag") @classmethod def _tag(cls, value: str | None) -> str | None: return validate_docker_tag(value) @field_validator("digest") @classmethod def _digest(cls, value: str | None) -> str | None: return validate_docker_digest(value) @field_validator("restart_policy") @classmethod def _restart_policy(cls, value: str | None) -> str: return validate_restart_policy(value) @field_validator("ports") @classmethod def _ports(cls, values: list[str]) -> list[str]: return [validate_port_mapping(value) for value in values] @field_validator("volumes") @classmethod def _volumes(cls, values: list[str]) -> list[str]: return [validate_volume_mapping(value) for value in values] @field_validator("env") @classmethod def _env(cls, values: dict[str, Any]) -> dict[str, str]: normalized: dict[str, str] = {} for key, value in values.items(): normalized[validate_env_name(str(key))] = str(value) return normalized @model_validator(mode="after") def _container_name(self) -> "DockerComponent": self.container_name = validate_container_name(self.container_name or self.component_id) return self class RawComponent(CamelModel): component_id: str = Field(alias="componentId") type: ComponentType install_order: int = Field(default=10, alias="installOrder") required: bool = True payload: dict[str, Any] = Field(default_factory=dict) class AppManifest(CamelModel): schema_version: str = Field(default="1.0", alias="schemaVersion") app_id: str = Field(alias="appId") app_name: str = Field(alias="appName") version: str open_url: str | None = Field(default=None, alias="openUrl") architecture: str = "amd64" components: list[dict[str, Any]] signature: str | None = None @field_validator("app_id") @classmethod def _app_id(cls, value: str) -> str: return validate_app_id(value) @field_validator("version") @classmethod def _version(cls, value: str) -> str: return validate_version(value)