Coverage for dlabel/api.py: 65%
118 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 23:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 23:07 +0000
1import io
2import json
3import tarfile
4import tempfile
5import time
6from abc import ABCMeta, abstractmethod
7from logging import getLogger
8from typing import Any
10import crossplane
11import docker
12import jsonpointer
13from fastapi import APIRouter, HTTPException
14from fastapi.responses import JSONResponse, PlainTextResponse, StreamingResponse
16from .compose import compose
17from .dockerfile import get_dockerfile
18from .traefik import traefik2nginx, traefik_dump
19from .traefik_conf import TraefikConfig
21_log = getLogger(__name__)
24class CommonRoute(metaclass=ABCMeta):
25 def __init__(self, client: docker.DockerClient):
26 self.client = client
27 self.router = APIRouter()
28 kwargs = {
29 "response_model_exclude_none": True,
30 "response_model_exclude_unset": True,
31 }
32 self.router.add_api_route("/", self.getroot, methods=["GET"], **kwargs)
33 self.router.add_api_route(
34 "/{path:path}", self.getsub, methods=["GET"], **kwargs
35 )
37 @abstractmethod
38 def getroot(self, **kwargs) -> dict:
39 raise NotImplementedError("GET /: not implemented")
41 def subpath(self, obj: dict, path: str) -> Any:
42 try:
43 res = jsonpointer.resolve_pointer(obj, "/" + path)
44 if isinstance(res, (int, str)):
45 return PlainTextResponse(content=str(res))
46 return JSONResponse(content=res)
47 except jsonpointer.JsonPointerException as e:
48 raise HTTPException(
49 status_code=404, detail={"path": path, "message": e.args[0]}
50 )
52 @abstractmethod
53 def getsub(self, path: str, **kwargs) -> Any:
54 return self.subpath(self.getroot(**kwargs), path=path)
57class ComposeRoute(CommonRoute):
58 def getroot(self, project: str | None = None) -> dict:
59 try:
60 g = compose(self.client, project=project, volume=False)
61 while True:
62 _ = next(g)
63 except StopIteration as e:
64 return e.value
66 def getsub(self, path: str, project: str | None = None) -> Any:
67 if path == "_tar": 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true
68 return self.getarchive(project)
69 return super().getsub(path, project=project)
71 def getarchive(self, project: str | None = None):
72 def arc():
73 yield "hello"
75 return StreamingResponse(arc, media_type="application/x-tar")
78class DockerfileRoute:
79 def __init__(self, client: docker.DockerClient):
80 self.client = client
81 self.router = APIRouter()
82 kwargs = {
83 "response_model_exclude_none": True,
84 "response_model_exclude_unset": True,
85 }
86 self.router.add_api_route("/", self.getroot, methods=["GET"], **kwargs)
87 self.router.add_api_route(
88 "/{container:path}/Dockerfile",
89 self.get_dockerfile,
90 methods=["GET"],
91 response_class=PlainTextResponse,
92 **kwargs,
93 )
94 self.router.add_api_route(
95 "/{container:path}/archive.tar",
96 self.get_archive,
97 methods=["GET"],
98 response_class=StreamingResponse,
99 responses={
100 200: {
101 "content": {"application/x-tar": {}},
102 }
103 },
104 **kwargs,
105 )
107 def getroot(self) -> list[str]:
108 # list containers
109 return [x.name for x in self.client.containers.list()]
111 def get_dockerfile(
112 self, container: str, ignore: list[str] | None = None, labels: bool = False
113 ) -> Any:
114 # get dockerfile
115 ctn = self.client.containers.get(container)
116 for _, bin in get_dockerfile(ctn, ignore or [], labels, do_output=False):
117 return PlainTextResponse(bin.decode())
119 def get_archive(
120 self, container: str, ignore: list[str] | None = None, labels: bool = False
121 ):
122 ctn = self.client.containers.get(container)
124 def arc():
125 ofp = io.BytesIO()
126 osk = ofp.tell()
127 with tarfile.open(mode="w", fileobj=ofp, format=tarfile.GNU_FORMAT) as tf:
128 for name, bin in get_dockerfile(
129 ctn, ignore or [], labels, do_output=True
130 ):
131 ti = tarfile.TarInfo(name)
132 ti.mode = 0o644
133 ti.mtime = time.time()
134 ti.size = len(bin)
135 tf.addfile(ti, io.BytesIO(bin))
136 _log.info("addfile %s, size=%s", name, len(bin))
137 if osk != ofp.tell():
138 ofp.seek(osk)
139 yield ofp.read()
140 osk = ofp.tell()
141 if osk != ofp.tell():
142 ofp.seek(osk)
143 yield ofp.read()
144 _log.info("finished: %s", container)
146 return StreamingResponse(arc(), media_type="application/x-tar")
149class TraefikRoute(CommonRoute):
150 def getroot(self) -> TraefikConfig:
151 return traefik_dump(self.client)
153 def getsub(self, path: str) -> Any:
154 return self.subpath(self.getroot().to_dict(), path=path)
157class NginxRoute(CommonRoute):
158 base_url = "http://localhost/"
160 def __init__(self, client: docker.DockerClient):
161 self.client = client
162 self.router = APIRouter()
163 self.router.add_api_route(
164 "/", self.getroot, methods=["GET"], response_class=PlainTextResponse
165 )
166 self.router.add_api_route("/json", self.getplane, methods=["GET"])
167 self.router.add_api_route(
168 "/json/{path:path}", self.getplanesub, methods=["GET"]
169 )
171 def getroot(self, ipaddr: bool = True) -> PlainTextResponse:
172 tmp = io.StringIO()
173 traefik2nginx(
174 traefik_dump(self.client),
175 tmp,
176 baseconf=None,
177 server_url=self.base_url,
178 ipaddr=ipaddr,
179 )
180 return PlainTextResponse(tmp.getvalue())
182 def getplane(self, ipaddr: bool = True) -> dict:
183 with tempfile.NamedTemporaryFile("r+") as tf:
184 traefik2nginx(
185 traefik_dump(self.client),
186 tf,
187 baseconf=None,
188 server_url=self.base_url,
189 ipaddr=ipaddr,
190 )
191 tf.flush()
192 res = crossplane.parse(tf.name, combine=True)
193 return json.loads(
194 json.dumps(res).replace('"' + tf.name + '"', '"nginx.conf"')
195 )
197 def getplanesub(self, path: str, ipaddr: bool = True) -> dict:
198 return self.subpath(self.getplane(ipaddr), path)
200 def getsub(self, path):
201 pass