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

1"""AgentOS deployment — Docker and orchestration helpers.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass, field 

6from pathlib import Path 

7from typing import Optional 

8 

9# ── Dockerfile generator ────────────────────────────────────────────────────── 

10 

11 

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" 

23 

24 

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) 

29 

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 ] 

54 

55 if cfg.extra_packages: 

56 lines.append(f"RUN pip install --no-cache-dir {' '.join(cfg.extra_packages)}") 

57 lines.append("") 

58 

59 for k, v in cfg.env_vars.items(): 

60 lines.append(f"ENV {k}={v}") 

61 if cfg.env_vars: 

62 lines.append("") 

63 

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 ] 

76 

77 return "\n".join(lines) 

78 

79 

80# ── docker-compose generator ────────────────────────────────────────────────── 

81 

82 

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 = "" 

93 

94 

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" 

101 

102 

103def generate_docker_compose(config: ComposeConfig) -> str: 

104 """Generate a docker-compose.yml for an AgentOS project.""" 

105 lines = [f'version: "3.9"', "", "services:"] 

106 

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("") 

127 

128 lines += [ 

129 "networks:", 

130 f" default:", 

131 f" name: {config.network_name}", 

132 ] 

133 

134 return "\n".join(lines) 

135 

136 

137# ── Deployment helper ───────────────────────────────────────────────────────── 

138 

139 

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] = [] 

149 

150 df_path = out / "Dockerfile" 

151 df_path.write_text(generate_dockerfile(docker_config)) 

152 written.append(df_path) 

153 

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) 

160 

161 return written