Coverage for agentos/deployment/docker.py: 40%
77 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
1"""AgentOS deployment — Docker and orchestration helpers."""
3from __future__ import annotations
5from dataclasses import dataclass, field
6from pathlib import Path
7from typing import Optional
9# ── Dockerfile generator ──────────────────────────────────────────────────────
12@dataclass
13class DockerConfig:
14 """Docker 部署配置。"""
15 python_version: str = "3.11"
16 base_image: str = "python:{python_version}-slim"
17 workdir: str = "/app"
18 port: int = 8000
19 entry_module: str = "agentos.cli.serve"
20 extra_packages: list[str] = field(default_factory=list)
21 env_vars: dict[str, str] = field(default_factory=dict)
22 user: str = "appuser"
25def generate_dockerfile(config: Optional[DockerConfig] = None) -> str:
26 """Generate a production-ready Dockerfile for an AgentOS project."""
27 cfg = config or DockerConfig()
28 base = cfg.base_image.format(python_version=cfg.python_version)
30 lines = [
31 f"FROM {base}",
32 "",
33 "LABEL org.opencontainers.image.source=\"https://github.com/agentos/agentos\"",
34 "",
35 "# System dependencies",
36 "RUN apt-get update && apt-get install -y --no-install-recommends \\",
37 " curl ca-certificates \\",
38 " && rm -rf /var/lib/apt/lists/*",
39 "",
40 "# Create non-root user",
41 f"RUN groupadd -r {cfg.user} && useradd -r -g {cfg.user} {cfg.user}",
42 "",
43 f"WORKDIR {cfg.workdir}",
44 "",
45 "# Install Python dependencies",
46 "COPY requirements.txt .",
47 f"RUN pip install --no-cache-dir -r requirements.txt",
48 "",
49 "# Copy application",
50 "COPY . .",
51 f"RUN pip install --no-cache-dir -e .",
52 "",
53 ]
55 if cfg.extra_packages:
56 lines.append(f"RUN pip install --no-cache-dir {' '.join(cfg.extra_packages)}")
57 lines.append("")
59 for k, v in cfg.env_vars.items():
60 lines.append(f"ENV {k}={v}")
61 if cfg.env_vars:
62 lines.append("")
64 lines += [
65 "# Security: drop root",
66 f"USER {cfg.user}",
67 "",
68 "# Health check",
69 "HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \\",
70 f" CMD curl -f http://localhost:{cfg.port}/health || exit 1",
71 "",
72 f"EXPOSE {cfg.port}",
73 "",
74 f'ENTRYPOINT ["python", "-m", "{cfg.entry_module}"]',
75 ]
77 return "\n".join(lines)
80# ── docker-compose generator ──────────────────────────────────────────────────
83@dataclass
84class ComposeService:
85 """Compose 服务定义。"""
86 name: str
87 build_context: str = "."
88 port: int = 8000
89 env_file: str = ".env"
90 volumes: list[str] = field(default_factory=list)
91 depends_on: list[str] = field(default_factory=list)
92 command: str = ""
95@dataclass
96class ComposeConfig:
97 """Compose 编排配置。"""
98 services: list[ComposeService] = field(default_factory=list)
99 project_name: str = "agentos"
100 network_name: str = "agentos-net"
103def generate_docker_compose(config: ComposeConfig) -> str:
104 """Generate a docker-compose.yml for an AgentOS project."""
105 lines = [f'version: "3.9"', "", "services:"]
107 for svc in config.services:
108 lines.append(f" {svc.name}:")
109 lines.append(f" build:")
110 lines.append(f" context: {svc.build_context}")
111 lines.append(f" ports:")
112 lines.append(f' - "{svc.port}:{svc.port}"')
113 if svc.env_file:
114 lines.append(f" env_file:")
115 lines.append(f" - {svc.env_file}")
116 if svc.volumes:
117 lines.append(f" volumes:")
118 for v in svc.volumes:
119 lines.append(f" - {v}")
120 if svc.depends_on:
121 lines.append(f" depends_on:")
122 for d in svc.depends_on:
123 lines.append(f" - {d}")
124 if svc.command:
125 lines.append(f" command: {svc.command}")
126 lines.append("")
128 lines += [
129 "networks:",
130 f" default:",
131 f" name: {config.network_name}",
132 ]
134 return "\n".join(lines)
137# ── Deployment helper ─────────────────────────────────────────────────────────
140def write_deployment_files(
141 output_dir: str | Path,
142 docker_config: Optional[DockerConfig] = None,
143 compose_config: Optional[ComposeConfig] = None,
144) -> list[Path]:
145 """Write Dockerfile and docker-compose.yml to output_dir. Returns written paths."""
146 out = Path(output_dir)
147 out.mkdir(parents=True, exist_ok=True)
148 written: list[Path] = []
150 df_path = out / "Dockerfile"
151 df_path.write_text(generate_dockerfile(docker_config))
152 written.append(df_path)
154 compose = compose_config or ComposeConfig(
155 services=[ComposeService(name="agentos")]
156 )
157 dc_path = out / "docker-compose.yml"
158 dc_path.write_text(generate_docker_compose(compose))
159 written.append(dc_path)
161 return written