Coverage for serverctl_deployd/routers/docker.py : 100%
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"""
5import logging
6from typing import Dict, List
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
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
22router = APIRouter(
23 prefix="/docker",
24 tags=["docker"]
25)
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'])
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
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
102 return {"message": f"Container {delete_request.container_id} deleted"}
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
132 return {"message": f"Container {container_id} started"}
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
162 return {"message": f"Container {container_id} stopped"}
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
192 return {"message": f"Container {container_id} restarted"}
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
228 return {"message": f"Container {container_id} killed"}
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
261 return LogsResponse(container_id=container_id, logs=logs)
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
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"}
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
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 )