Coverage for agentos/deployment/docker.py: 39%
76 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 21:26 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 21:26 +0800
1"""AgentOS deployment — Docker and orchestration helpers."""
3from __future__ import annotations
5from dataclasses import dataclass, field
6from pathlib import Path
8# ── Dockerfile generator ──────────────────────────────────────────────────────
11@dataclass
12class DockerConfig:
13 """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: DockerConfig | None = 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 "RUN pip install --no-cache-dir -r requirements.txt",
48 "",
49 "# Copy application",
50 "COPY . .",
51 "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 服务定义。"""
87 name: str
88 build_context: str = "."
89 port: int = 8000
90 env_file: str = ".env"
91 volumes: list[str] = field(default_factory=list)
92 depends_on: list[str] = field(default_factory=list)
93 command: str = ""
96@dataclass
97class ComposeConfig:
98 """Compose 编排配置。"""
100 services: list[ComposeService] = field(default_factory=list)
101 project_name: str = "agentos"
102 network_name: str = "agentos-net"
105def generate_docker_compose(config: ComposeConfig) -> str:
106 """Generate a docker-compose.yml for an AgentOS project."""
107 lines = ['version: "3.9"', "", "services:"]
109 for svc in config.services:
110 lines.append(f" {svc.name}:")
111 lines.append(" build:")
112 lines.append(f" context: {svc.build_context}")
113 lines.append(" ports:")
114 lines.append(f' - "{svc.port}:{svc.port}"')
115 if svc.env_file:
116 lines.append(" env_file:")
117 lines.append(f" - {svc.env_file}")
118 if svc.volumes:
119 lines.append(" volumes:")
120 for v in svc.volumes:
121 lines.append(f" - {v}")
122 if svc.depends_on:
123 lines.append(" depends_on:")
124 for d in svc.depends_on:
125 lines.append(f" - {d}")
126 if svc.command:
127 lines.append(f" command: {svc.command}")
128 lines.append("")
130 lines += [
131 "networks:",
132 " default:",
133 f" name: {config.network_name}",
134 ]
136 return "\n".join(lines)
139# ── Deployment helper ─────────────────────────────────────────────────────────
142def write_deployment_files(
143 output_dir: str | Path,
144 docker_config: DockerConfig | None = None,
145 compose_config: ComposeConfig | None = None,
146) -> list[Path]:
147 """Write Dockerfile and docker-compose.yml to output_dir. Returns written paths."""
148 out = Path(output_dir)
149 out.mkdir(parents=True, exist_ok=True)
150 written: list[Path] = []
152 df_path = out / "Dockerfile"
153 df_path.write_text(generate_dockerfile(docker_config))
154 written.append(df_path)
156 compose = compose_config or ComposeConfig(services=[ComposeService(name="agentos")])
157 dc_path = out / "docker-compose.yml"
158 dc_path.write_text(generate_docker_compose(compose))
159 written.append(dc_path)
161 return written