Files
InstallerRobot/agent/app/models/schemas.py
2026-05-28 14:26:02 +07:00

269 lines
8.3 KiB
Python

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)