Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Router for Docker routes 

3""" 

4 

5import logging 

6from typing import Dict, List 

7 

8from docker import DockerClient 

9from docker.errors import APIError, ImageNotFound, NotFound 

10from docker.models.containers import Container, Image 

11from docker.types.daemon import CancellableStream 

12from fastapi import APIRouter, HTTPException, status 

13from fastapi.params import Depends 

14from fastapi.responses import StreamingResponse 

15 

16from serverctl_deployd.dependencies import get_docker_client 

17from serverctl_deployd.models.docker import (ContainerDetails, DeleteRequest, 

18 ImageTagRequest, LogsResponse, 

19 PruneRequest, PruneResponse) 

20from serverctl_deployd.models.exceptions import GenericError 

21 

22router = APIRouter( 

23 prefix="/docker", 

24 tags=["docker"] 

25) 

26 

27 

28@router.get( 

29 "/containers/{container_id}", 

30 response_model=ContainerDetails, 

31 responses={ 

32 status.HTTP_404_NOT_FOUND: {"model": GenericError}, 

33 status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": GenericError} 

34 } 

35) 

36async def get_container_details( 

37 container_id: str, 

38 docker_client: DockerClient = Depends(get_docker_client) 

39) -> ContainerDetails: 

40 """ 

41 Get container details 

42 """ 

43 try: 

44 container: Container = docker_client.containers.get(container_id) 

45 container_response: ContainerDetails = ContainerDetails( 

46 id=container.id, 

47 status=container.status, 

48 image=container.image.tags, 

49 name=container.name, 

50 ports=container.ports, 

51 created=container.attrs['Created']) 

52 

53 except NotFound as not_found_exception: 

54 raise HTTPException( 

55 status_code=status.HTTP_404_NOT_FOUND, 

56 detail="Container not found" 

57 ) from not_found_exception 

58 except APIError as api_error_exception: 

59 raise HTTPException( 

60 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

61 detail="Internal server error" 

62 ) from api_error_exception 

63 return container_response 

64 

65 

66@router.post( 

67 "/containers/delete", 

68 responses={ 

69 status.HTTP_404_NOT_FOUND: {"model": GenericError}, 

70 status.HTTP_403_FORBIDDEN: {"model": GenericError}, 

71 status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": GenericError} 

72 } 

73) 

74async def delete_container( 

75 delete_request: DeleteRequest, 

76 docker_client: DockerClient = Depends(get_docker_client) 

77) -> Dict[str, str]: 

78 """ 

79 Delete container 

80 """ 

81 container = Container() 

82 try: 

83 container = docker_client.containers.get(delete_request.container_id) 

84 container.remove(force=delete_request.force, v=delete_request.v) 

85 except NotFound as not_found_exception: 

86 raise HTTPException( 

87 status_code=status.HTTP_404_NOT_FOUND, 

88 detail="Container not found") from not_found_exception 

89 except APIError as api_error_exception: 

90 if container.status == "running": 

91 raise HTTPException( 

92 status_code=status.HTTP_403_FORBIDDEN, 

93 detail="Cannot remove running containers, try forcing") from api_error_exception 

94 logging.exception( 

95 "Error deleting the container %s", 

96 delete_request.container_id) 

97 raise HTTPException( 

98 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

99 detail="Internal server error" 

100 ) from api_error_exception 

101 

102 return {"message": f"Container {delete_request.container_id} deleted"} 

103 

104 

105@router.post( 

106 "/containers/{container_id}/start", 

107 responses={ 

108 status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": GenericError} 

109 } 

110) 

111async def start_container( 

112 container_id: str, 

113 docker_client: DockerClient = Depends(get_docker_client) 

114) -> Dict[str, str]: 

115 """ 

116 Start container 

117 """ 

118 try: 

119 container: Container = docker_client.containers.get(container_id) 

120 container.start() 

121 except NotFound as not_found_exception: 

122 raise HTTPException( 

123 status_code=status.HTTP_404_NOT_FOUND, 

124 detail="Container not found") from not_found_exception 

125 except APIError as api_error_exception: 

126 logging.exception("Error starting the container %s", container_id) 

127 raise HTTPException( 

128 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

129 detail="Internal server error" 

130 ) from api_error_exception 

131 

132 return {"message": f"Container {container_id} started"} 

133 

134 

135@router.post( 

136 "/containers/{container_id}/stop", 

137 responses={ 

138 status.HTTP_404_NOT_FOUND: {"model": GenericError}, 

139 status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": GenericError} 

140 }) 

141async def stop_container( 

142 container_id: str, 

143 docker_client: DockerClient = Depends(get_docker_client) 

144) -> Dict[str, str]: 

145 """ 

146 Stop container 

147 """ 

148 try: 

149 container: Container = docker_client.containers.get(container_id) 

150 container.stop() 

151 except NotFound as not_found_exception: 

152 raise HTTPException( 

153 status_code=status.HTTP_404_NOT_FOUND, 

154 detail="Container not found") from not_found_exception 

155 except APIError as api_error_exception: 

156 logging.exception("Error stopping the container %s", container_id) 

157 raise HTTPException( 

158 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

159 detail="Internal server error" 

160 ) from api_error_exception 

161 

162 return {"message": f"Container {container_id} stopped"} 

163 

164 

165@router.post( 

166 "/containers/{container_id}/restart", 

167 responses={ 

168 status.HTTP_404_NOT_FOUND: {"model": GenericError}, 

169 status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": GenericError} 

170 }) 

171async def restart_container( 

172 container_id: str, 

173 docker_client: DockerClient = Depends(get_docker_client) 

174) -> Dict[str, str]: 

175 """ 

176 Restart container 

177 """ 

178 try: 

179 container: Container = docker_client.containers.get(container_id) 

180 container.restart() 

181 except NotFound as not_found_exception: 

182 raise HTTPException( 

183 status_code=status.HTTP_404_NOT_FOUND, 

184 detail="Container not found") from not_found_exception 

185 except APIError as api_error_exception: 

186 logging.exception("Error restarting the container %s", container_id) 

187 raise HTTPException( 

188 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

189 detail="Internal server error" 

190 ) from api_error_exception 

191 

192 return {"message": f"Container {container_id} restarted"} 

193 

194 

195@router.post( 

196 "/containers/{container_id}/kill", 

197 responses={ 

198 status.HTTP_404_NOT_FOUND: {"model": GenericError}, 

199 status.HTTP_403_FORBIDDEN: {"model": GenericError}, 

200 status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": GenericError} 

201 }) 

202async def kill_container( 

203 container_id: str, 

204 docker_client: DockerClient = Depends(get_docker_client) 

205) -> Dict[str, str]: 

206 """ 

207 Kill container 

208 """ 

209 container = Container() 

210 try: 

211 container = docker_client.containers.get(container_id) 

212 container.kill() 

213 except NotFound as not_found_exception: 

214 raise HTTPException( 

215 status_code=status.HTTP_404_NOT_FOUND, 

216 detail="Container not found") from not_found_exception 

217 except APIError as api_error_exception: 

218 if container.status != "running": 

219 raise HTTPException( 

220 status_code=status.HTTP_403_FORBIDDEN, 

221 detail="Cannot kill containers that are not running") from api_error_exception 

222 logging.exception("Error killing the container %s", container_id) 

223 raise HTTPException( 

224 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

225 detail="Internal server error" 

226 ) from api_error_exception 

227 

228 return {"message": f"Container {container_id} killed"} 

229 

230 

231@router.get( 

232 "/containers/{container_id}/logs", 

233 response_model=LogsResponse, 

234 responses={ 

235 status.HTTP_404_NOT_FOUND: {"model": GenericError}, 

236 status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": GenericError} 

237 }) 

238async def get_logs( 

239 container_id: str, 

240 docker_client: DockerClient = Depends(get_docker_client) 

241) -> LogsResponse: 

242 """ 

243 Get logs 

244 """ 

245 try: 

246 container: Container = docker_client.containers.get(container_id) 

247 logs: str = container.logs() 

248 except NotFound as not_found_exception: 

249 raise HTTPException( 

250 status_code=status.HTTP_404_NOT_FOUND, 

251 detail="Container not found") from not_found_exception 

252 except APIError as api_error_exception: 

253 logging.exception( 

254 "Error getting the logs of the container %s", 

255 container_id) 

256 raise HTTPException( 

257 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

258 detail="Internal server error" 

259 ) from api_error_exception 

260 

261 return LogsResponse(container_id=container_id, logs=logs) 

262 

263 

264@router.get("/containers", 

265 response_model=List[ContainerDetails], 

266 responses={ 

267 status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": GenericError} 

268 } 

269 ) 

270async def get_containers( 

271 docker_client: DockerClient = Depends(get_docker_client) 

272) -> List[ContainerDetails]: 

273 """ 

274 Get all containers 

275 """ 

276 try: 

277 containers: List[Container] = [ContainerDetails( 

278 id=container.id, 

279 status=container.status, 

280 image=container.image.tags, 

281 name=container.name, 

282 ports=container.ports, 

283 created=container.attrs['Created'] 

284 ) 

285 for container in docker_client.containers.list(all=True)] 

286 except APIError as api_error_exception: 

287 raise HTTPException( 

288 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

289 detail="Internal server error" 

290 ) from api_error_exception 

291 return containers 

292 

293 

294@router.post( 

295 "/images/tag", 

296 responses={ 

297 status.HTTP_404_NOT_FOUND: {"model": GenericError}, 

298 status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": GenericError} 

299 }) 

300async def tag_image( 

301 tag_image_request: ImageTagRequest, 

302 docker_client: DockerClient = Depends(get_docker_client) 

303) -> Dict[str, str]: 

304 """ 

305 Tag image 

306 """ 

307 try: 

308 image: Image = docker_client.images.get(tag_image_request.image_id) 

309 image.tag(tag_image_request.tag, "latest") 

310 except ImageNotFound as image_not_found_exception: 

311 raise HTTPException( 

312 status_code=status.HTTP_404_NOT_FOUND, 

313 detail="Image not found" 

314 ) from image_not_found_exception 

315 except APIError as api_error_exception: 

316 raise HTTPException( 

317 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

318 detail="Internal server error" 

319 ) from api_error_exception 

320 return {"message": f"Image {tag_image_request.image_id} tagged"} 

321 

322 

323@router.post( 

324 "/prune", 

325 response_model=PruneResponse, 

326 responses={ 

327 status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": GenericError} 

328 } 

329) 

330async def prune( 

331 prune_request: PruneRequest, 

332 docker_client: DockerClient = Depends(get_docker_client) 

333) -> PruneResponse: 

334 """ 

335 Prune Docker images and containers 

336 """ 

337 try: 

338 prune_response = PruneResponse() 

339 if prune_request.containers or prune_request.all: 

340 prune_response.containers = docker_client.containers.prune() 

341 if prune_request.images or prune_request.all: 

342 prune_response.images = docker_client.images.prune() 

343 if prune_request.volumes or prune_request.all: 

344 prune_response.volumes = docker_client.volumes.prune() 

345 if prune_request.networks or prune_request.all: 

346 prune_response.networks = docker_client.networks.prune() 

347 if prune_request.build_cache or prune_request.all: 

348 prune_response.build_cache = docker_client.api.prune_builds() 

349 except APIError as api_error_exception: 

350 raise HTTPException( 

351 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

352 detail="Internal server error" 

353 ) from api_error_exception 

354 return prune_response 

355 

356 

357@router.get( 

358 "/containers/{container_id}/attach", 

359 responses={ 

360 status.HTTP_404_NOT_FOUND: {"model": GenericError}, 

361 status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": GenericError} 

362 } 

363) 

364async def container_attach( 

365 container_id: str, 

366 docker_client: DockerClient = Depends(get_docker_client) 

367) -> StreamingResponse: 

368 """ 

369 Returns a HTTP Stream for the container's stdout and stderr 

370 """ 

371 try: 

372 container: Container = docker_client.containers.get(container_id) 

373 log_stream: CancellableStream = container.attach(stream=True) 

374 except NotFound as not_found_exception: 

375 raise HTTPException( 

376 status_code=status.HTTP_404_NOT_FOUND, 

377 detail="Container not found" 

378 ) from not_found_exception 

379 except APIError as api_error_exception: 

380 raise HTTPException( 

381 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

382 detail="Internal server error" 

383 ) from api_error_exception 

384 return StreamingResponse( 

385 log_stream, 

386 media_type="text/plain" 

387 )