omdev 0.0.0.dev222__py3-none-any.whl → 0.0.0.dev224__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- omdev/ci/cache.py +148 -23
- omdev/ci/ci.py +50 -110
- omdev/ci/cli.py +24 -23
- omdev/ci/docker/__init__.py +0 -0
- omdev/ci/docker/buildcaching.py +69 -0
- omdev/ci/docker/cache.py +57 -0
- omdev/ci/docker/cacheserved.py +262 -0
- omdev/ci/{docker.py → docker/cmds.py} +1 -44
- omdev/ci/docker/dataserver.py +204 -0
- omdev/ci/docker/imagepulling.py +65 -0
- omdev/ci/docker/inject.py +37 -0
- omdev/ci/docker/packing.py +72 -0
- omdev/ci/docker/repositories.py +40 -0
- omdev/ci/docker/utils.py +48 -0
- omdev/ci/github/cache.py +35 -6
- omdev/ci/github/client.py +9 -2
- omdev/ci/github/inject.py +30 -0
- omdev/ci/inject.py +61 -0
- omdev/ci/utils.py +0 -49
- omdev/dataserver/__init__.py +1 -0
- omdev/dataserver/handlers.py +198 -0
- omdev/dataserver/http.py +69 -0
- omdev/dataserver/routes.py +49 -0
- omdev/dataserver/server.py +90 -0
- omdev/dataserver/targets.py +121 -0
- omdev/oci/building.py +107 -9
- omdev/oci/compression.py +8 -0
- omdev/oci/data.py +43 -0
- omdev/oci/datarefs.py +90 -50
- omdev/oci/dataserver.py +64 -0
- omdev/oci/loading.py +20 -0
- omdev/oci/media.py +20 -0
- omdev/oci/pack/__init__.py +0 -0
- omdev/oci/pack/packing.py +185 -0
- omdev/oci/pack/repositories.py +162 -0
- omdev/oci/pack/unpacking.py +204 -0
- omdev/oci/repositories.py +84 -2
- omdev/oci/tars.py +144 -0
- omdev/pyproject/resources/python.sh +1 -1
- omdev/scripts/ci.py +2137 -512
- omdev/scripts/interp.py +119 -22
- omdev/scripts/pyproject.py +141 -28
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/RECORD +48 -23
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/top_level.txt +0 -0
omdev/ci/docker/cache.py
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
from omlish.os.temp import temp_file_context
|
6
|
+
|
7
|
+
from ..cache import FileCache
|
8
|
+
from ..shell import ShellCmd
|
9
|
+
from .cmds import load_docker_tar_cmd
|
10
|
+
from .cmds import save_docker_tar_cmd
|
11
|
+
|
12
|
+
|
13
|
+
##
|
14
|
+
|
15
|
+
|
16
|
+
class DockerCache(abc.ABC):
|
17
|
+
@abc.abstractmethod
|
18
|
+
def load_cache_docker_image(self, key: str) -> ta.Awaitable[ta.Optional[str]]:
|
19
|
+
raise NotImplementedError
|
20
|
+
|
21
|
+
@abc.abstractmethod
|
22
|
+
def save_cache_docker_image(self, key: str, image: str) -> ta.Awaitable[None]:
|
23
|
+
raise NotImplementedError
|
24
|
+
|
25
|
+
|
26
|
+
class DockerCacheImpl(DockerCache):
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
*,
|
30
|
+
file_cache: ta.Optional[FileCache] = None,
|
31
|
+
) -> None:
|
32
|
+
super().__init__()
|
33
|
+
|
34
|
+
self._file_cache = file_cache
|
35
|
+
|
36
|
+
async def load_cache_docker_image(self, key: str) -> ta.Optional[str]:
|
37
|
+
if self._file_cache is None:
|
38
|
+
return None
|
39
|
+
|
40
|
+
cache_file = await self._file_cache.get_file(key)
|
41
|
+
if cache_file is None:
|
42
|
+
return None
|
43
|
+
|
44
|
+
get_cache_cmd = ShellCmd(f'cat {cache_file} | zstd -cd --long')
|
45
|
+
|
46
|
+
return await load_docker_tar_cmd(get_cache_cmd)
|
47
|
+
|
48
|
+
async def save_cache_docker_image(self, key: str, image: str) -> None:
|
49
|
+
if self._file_cache is None:
|
50
|
+
return
|
51
|
+
|
52
|
+
with temp_file_context() as tmp_file:
|
53
|
+
write_tmp_cmd = ShellCmd(f'zstd > {tmp_file}')
|
54
|
+
|
55
|
+
await save_docker_tar_cmd(image, write_tmp_cmd)
|
56
|
+
|
57
|
+
await self._file_cache.put_file(key, tmp_file, steal=True)
|
@@ -0,0 +1,262 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import asyncio
|
4
|
+
import contextlib
|
5
|
+
import dataclasses as dc
|
6
|
+
import json
|
7
|
+
import os.path
|
8
|
+
import typing as ta
|
9
|
+
|
10
|
+
from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
|
11
|
+
from omlish.lite.check import check
|
12
|
+
from omlish.lite.json import json_dumps_compact
|
13
|
+
from omlish.lite.logs import log
|
14
|
+
from omlish.lite.marshal import marshal_obj
|
15
|
+
from omlish.lite.marshal import unmarshal_obj
|
16
|
+
|
17
|
+
from ...dataserver.routes import DataServerRoute
|
18
|
+
from ...dataserver.server import DataServer
|
19
|
+
from ...dataserver.targets import BytesDataServerTarget
|
20
|
+
from ...dataserver.targets import DataServerTarget
|
21
|
+
from ...dataserver.targets import FileDataServerTarget
|
22
|
+
from ...oci.building import build_oci_index_repository
|
23
|
+
from ...oci.data import get_single_leaf_oci_image_index
|
24
|
+
from ...oci.dataserver import build_oci_repository_data_server_routes
|
25
|
+
from ...oci.loading import read_oci_repository_root_index
|
26
|
+
from ...oci.pack.repositories import OciPackedRepositoryBuilder
|
27
|
+
from ...oci.repositories import OciRepository
|
28
|
+
from ..cache import DataCache
|
29
|
+
from ..cache import read_data_cache_data
|
30
|
+
from .cache import DockerCache
|
31
|
+
from .dataserver import DockerDataServer
|
32
|
+
from .repositories import DockerImageRepositoryOpener
|
33
|
+
|
34
|
+
|
35
|
+
##
|
36
|
+
|
37
|
+
|
38
|
+
@dc.dataclass(frozen=True)
|
39
|
+
class CacheServedDockerImageManifest:
|
40
|
+
@dc.dataclass(frozen=True)
|
41
|
+
class Route:
|
42
|
+
paths: ta.Sequence[str]
|
43
|
+
|
44
|
+
content_type: str
|
45
|
+
content_length: int
|
46
|
+
|
47
|
+
@dc.dataclass(frozen=True)
|
48
|
+
class Target(abc.ABC): # noqa
|
49
|
+
pass
|
50
|
+
|
51
|
+
@dc.dataclass(frozen=True)
|
52
|
+
class BytesTarget(Target):
|
53
|
+
data: bytes
|
54
|
+
|
55
|
+
@dc.dataclass(frozen=True)
|
56
|
+
class CacheKeyTarget(Target):
|
57
|
+
key: str
|
58
|
+
|
59
|
+
target: Target
|
60
|
+
|
61
|
+
def __post_init__(self) -> None:
|
62
|
+
check.not_isinstance(self.paths, str)
|
63
|
+
|
64
|
+
routes: ta.Sequence[Route]
|
65
|
+
|
66
|
+
|
67
|
+
#
|
68
|
+
|
69
|
+
|
70
|
+
async def build_cache_served_docker_image_manifest(
|
71
|
+
data_server_routes: ta.Iterable[DataServerRoute],
|
72
|
+
make_file_cache_key: ta.Callable[[str], ta.Awaitable[str]],
|
73
|
+
) -> CacheServedDockerImageManifest:
|
74
|
+
routes: ta.List[CacheServedDockerImageManifest.Route] = []
|
75
|
+
|
76
|
+
for data_server_route in data_server_routes:
|
77
|
+
content_length: int
|
78
|
+
|
79
|
+
data_server_target = data_server_route.target
|
80
|
+
target: CacheServedDockerImageManifest.Route.Target
|
81
|
+
if isinstance(data_server_target, BytesDataServerTarget):
|
82
|
+
bytes_data = check.isinstance(data_server_target.data, bytes)
|
83
|
+
content_length = len(bytes_data)
|
84
|
+
target = CacheServedDockerImageManifest.Route.BytesTarget(bytes_data)
|
85
|
+
|
86
|
+
elif isinstance(data_server_target, FileDataServerTarget):
|
87
|
+
file_path = check.non_empty_str(data_server_target.file_path)
|
88
|
+
content_length = os.path.getsize(file_path)
|
89
|
+
cache_key = await make_file_cache_key(file_path)
|
90
|
+
target = CacheServedDockerImageManifest.Route.CacheKeyTarget(cache_key)
|
91
|
+
|
92
|
+
else:
|
93
|
+
raise TypeError(data_server_target)
|
94
|
+
|
95
|
+
routes.append(CacheServedDockerImageManifest.Route(
|
96
|
+
paths=data_server_route.paths,
|
97
|
+
|
98
|
+
content_type=check.non_empty_str(data_server_target.content_type),
|
99
|
+
content_length=content_length,
|
100
|
+
|
101
|
+
target=target,
|
102
|
+
))
|
103
|
+
|
104
|
+
return CacheServedDockerImageManifest(
|
105
|
+
routes=routes,
|
106
|
+
)
|
107
|
+
|
108
|
+
|
109
|
+
#
|
110
|
+
|
111
|
+
|
112
|
+
async def build_cache_served_docker_image_data_server_routes(
|
113
|
+
manifest: CacheServedDockerImageManifest,
|
114
|
+
make_cache_key_target: ta.Callable[..., ta.Awaitable[DataServerTarget]],
|
115
|
+
) -> ta.List[DataServerRoute]:
|
116
|
+
routes: ta.List[DataServerRoute] = []
|
117
|
+
|
118
|
+
for manifest_route in manifest.routes:
|
119
|
+
manifest_target = manifest_route.target
|
120
|
+
|
121
|
+
target_kwargs: dict = dict(
|
122
|
+
content_type=manifest_route.content_type,
|
123
|
+
content_length=manifest_route.content_length,
|
124
|
+
)
|
125
|
+
|
126
|
+
target: DataServerTarget
|
127
|
+
|
128
|
+
if isinstance(manifest_target, CacheServedDockerImageManifest.Route.BytesTarget):
|
129
|
+
target = DataServerTarget.of(manifest_target.data, **target_kwargs)
|
130
|
+
|
131
|
+
elif isinstance(manifest_target, CacheServedDockerImageManifest.Route.CacheKeyTarget):
|
132
|
+
target = await make_cache_key_target(manifest_target.key, **target_kwargs)
|
133
|
+
|
134
|
+
else:
|
135
|
+
raise TypeError(manifest_target)
|
136
|
+
|
137
|
+
routes.append(DataServerRoute(
|
138
|
+
paths=manifest_route.paths,
|
139
|
+
target=target,
|
140
|
+
))
|
141
|
+
|
142
|
+
return routes
|
143
|
+
|
144
|
+
|
145
|
+
##
|
146
|
+
|
147
|
+
|
148
|
+
class CacheServedDockerCache(DockerCache):
|
149
|
+
@dc.dataclass(frozen=True)
|
150
|
+
class Config:
|
151
|
+
port: int = 5021
|
152
|
+
|
153
|
+
repack: bool = True
|
154
|
+
|
155
|
+
def __init__(
|
156
|
+
self,
|
157
|
+
*,
|
158
|
+
config: Config = Config(),
|
159
|
+
|
160
|
+
image_repo_opener: DockerImageRepositoryOpener,
|
161
|
+
data_cache: DataCache,
|
162
|
+
) -> None:
|
163
|
+
super().__init__()
|
164
|
+
|
165
|
+
self._config = config
|
166
|
+
|
167
|
+
self._image_repo_opener = image_repo_opener
|
168
|
+
self._data_cache = data_cache
|
169
|
+
|
170
|
+
async def load_cache_docker_image(self, key: str) -> ta.Optional[str]:
|
171
|
+
if (manifest_data := await self._data_cache.get_data(key)) is None:
|
172
|
+
return None
|
173
|
+
|
174
|
+
manifest_bytes = await read_data_cache_data(manifest_data)
|
175
|
+
|
176
|
+
manifest: CacheServedDockerImageManifest = unmarshal_obj(
|
177
|
+
json.loads(manifest_bytes.decode('utf-8')),
|
178
|
+
CacheServedDockerImageManifest,
|
179
|
+
)
|
180
|
+
|
181
|
+
async def make_cache_key_target(target_cache_key: str, **target_kwargs: ta.Any) -> DataServerTarget: # noqa
|
182
|
+
# FIXME: url
|
183
|
+
cache_data = check.not_none(await self._data_cache.get_data(target_cache_key))
|
184
|
+
file_path = check.isinstance(cache_data, DataCache.FileData).file_path
|
185
|
+
return DataServerTarget.of(
|
186
|
+
file_path=file_path,
|
187
|
+
**target_kwargs,
|
188
|
+
)
|
189
|
+
|
190
|
+
data_server_routes = await build_cache_served_docker_image_data_server_routes(
|
191
|
+
manifest,
|
192
|
+
make_cache_key_target,
|
193
|
+
)
|
194
|
+
|
195
|
+
data_server = DataServer(DataServer.HandlerRoute.of_(*data_server_routes))
|
196
|
+
|
197
|
+
image_url = f'localhost:{self._config.port}/{key}'
|
198
|
+
|
199
|
+
async with DockerDataServer(
|
200
|
+
self._config.port,
|
201
|
+
data_server,
|
202
|
+
handler_log=log,
|
203
|
+
) as dds:
|
204
|
+
dds_run_task = asyncio.create_task(dds.run())
|
205
|
+
try:
|
206
|
+
# FIXME: lol
|
207
|
+
await asyncio.sleep(3.)
|
208
|
+
|
209
|
+
await asyncio_subprocesses.check_call(
|
210
|
+
'docker',
|
211
|
+
'pull',
|
212
|
+
image_url,
|
213
|
+
)
|
214
|
+
|
215
|
+
finally:
|
216
|
+
dds.stop_event.set()
|
217
|
+
await dds_run_task
|
218
|
+
|
219
|
+
return image_url
|
220
|
+
|
221
|
+
async def save_cache_docker_image(self, key: str, image: str) -> None:
|
222
|
+
async with contextlib.AsyncExitStack() as es:
|
223
|
+
image_repo: OciRepository = await es.enter_async_context(
|
224
|
+
self._image_repo_opener.open_docker_image_repository(image),
|
225
|
+
)
|
226
|
+
|
227
|
+
root_image_index = read_oci_repository_root_index(image_repo)
|
228
|
+
image_index = get_single_leaf_oci_image_index(root_image_index)
|
229
|
+
|
230
|
+
if self._config.repack:
|
231
|
+
prb: OciPackedRepositoryBuilder = es.enter_context(OciPackedRepositoryBuilder(
|
232
|
+
image_repo,
|
233
|
+
))
|
234
|
+
built_repo = await asyncio.get_running_loop().run_in_executor(None, prb.build)
|
235
|
+
|
236
|
+
else:
|
237
|
+
built_repo = build_oci_index_repository(image_index)
|
238
|
+
|
239
|
+
data_server_routes = build_oci_repository_data_server_routes(
|
240
|
+
key,
|
241
|
+
built_repo,
|
242
|
+
)
|
243
|
+
|
244
|
+
async def make_file_cache_key(file_path: str) -> str:
|
245
|
+
target_cache_key = f'{key}--{os.path.basename(file_path).split(".")[0]}'
|
246
|
+
await self._data_cache.put_data(
|
247
|
+
target_cache_key,
|
248
|
+
DataCache.FileData(file_path),
|
249
|
+
)
|
250
|
+
return target_cache_key
|
251
|
+
|
252
|
+
cache_served_manifest = await build_cache_served_docker_image_manifest(
|
253
|
+
data_server_routes,
|
254
|
+
make_file_cache_key,
|
255
|
+
)
|
256
|
+
|
257
|
+
manifest_data = json_dumps_compact(marshal_obj(cache_served_manifest)).encode('utf-8')
|
258
|
+
|
259
|
+
await self._data_cache.put_data(
|
260
|
+
key,
|
261
|
+
DataCache.BytesData(manifest_data),
|
262
|
+
)
|
@@ -1,58 +1,15 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
|
-
"""
|
3
|
-
TODO:
|
4
|
-
- some less stupid Dockerfile hash
|
5
|
-
- doesn't change too much though
|
6
|
-
"""
|
7
|
-
import contextlib
|
8
2
|
import dataclasses as dc
|
9
3
|
import json
|
10
4
|
import os.path
|
11
5
|
import shlex
|
12
|
-
import tarfile
|
13
6
|
import typing as ta
|
14
7
|
|
15
8
|
from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
|
16
9
|
from omlish.lite.check import check
|
17
10
|
from omlish.os.temp import temp_file_context
|
18
11
|
|
19
|
-
from
|
20
|
-
from .utils import sha256_str
|
21
|
-
|
22
|
-
|
23
|
-
##
|
24
|
-
|
25
|
-
|
26
|
-
def build_docker_file_hash(docker_file: str) -> str:
|
27
|
-
with open(docker_file) as f:
|
28
|
-
contents = f.read()
|
29
|
-
|
30
|
-
return sha256_str(contents)
|
31
|
-
|
32
|
-
|
33
|
-
##
|
34
|
-
|
35
|
-
|
36
|
-
def read_docker_tar_image_tag(tar_file: str) -> str:
|
37
|
-
with tarfile.open(tar_file) as tf:
|
38
|
-
with contextlib.closing(check.not_none(tf.extractfile('manifest.json'))) as mf:
|
39
|
-
m = mf.read()
|
40
|
-
|
41
|
-
manifests = json.loads(m.decode('utf-8'))
|
42
|
-
manifest = check.single(manifests)
|
43
|
-
tag = check.non_empty_str(check.single(manifest['RepoTags']))
|
44
|
-
return tag
|
45
|
-
|
46
|
-
|
47
|
-
def read_docker_tar_image_id(tar_file: str) -> str:
|
48
|
-
with tarfile.open(tar_file) as tf:
|
49
|
-
with contextlib.closing(check.not_none(tf.extractfile('index.json'))) as mf:
|
50
|
-
i = mf.read()
|
51
|
-
|
52
|
-
index = json.loads(i.decode('utf-8'))
|
53
|
-
manifest = check.single(index['manifests'])
|
54
|
-
image_id = check.non_empty_str(manifest['digest'])
|
55
|
-
return image_id
|
12
|
+
from ..shell import ShellCmd
|
56
13
|
|
57
14
|
|
58
15
|
##
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import asyncio
|
3
|
+
import contextlib
|
4
|
+
import logging
|
5
|
+
import ssl
|
6
|
+
import sys
|
7
|
+
import threading
|
8
|
+
import typing as ta
|
9
|
+
|
10
|
+
from omlish.docker.portrelay import DockerPortRelay
|
11
|
+
from omlish.http.coro.simple import make_simple_http_server
|
12
|
+
from omlish.http.handlers import HttpHandler
|
13
|
+
from omlish.http.handlers import LoggingHttpHandler
|
14
|
+
from omlish.lite.cached import cached_nullary
|
15
|
+
from omlish.lite.check import check
|
16
|
+
from omlish.lite.contextmanagers import AsyncExitStacked
|
17
|
+
from omlish.secrets.tempssl import generate_temp_localhost_ssl_cert
|
18
|
+
from omlish.sockets.server.server import SocketServer
|
19
|
+
|
20
|
+
from ...dataserver.http import DataServerHttpHandler
|
21
|
+
from ...dataserver.server import DataServer
|
22
|
+
|
23
|
+
|
24
|
+
##
|
25
|
+
|
26
|
+
|
27
|
+
@contextlib.asynccontextmanager
|
28
|
+
async def start_docker_port_relay(
|
29
|
+
docker_port: int,
|
30
|
+
host_port: int,
|
31
|
+
**kwargs: ta.Any,
|
32
|
+
) -> ta.AsyncGenerator[None, None]:
|
33
|
+
proc = await asyncio.create_subprocess_exec(*DockerPortRelay(
|
34
|
+
docker_port,
|
35
|
+
host_port,
|
36
|
+
**kwargs,
|
37
|
+
).run_cmd())
|
38
|
+
|
39
|
+
try:
|
40
|
+
yield
|
41
|
+
|
42
|
+
finally:
|
43
|
+
try:
|
44
|
+
proc.kill()
|
45
|
+
except ProcessLookupError:
|
46
|
+
pass
|
47
|
+
await proc.wait()
|
48
|
+
|
49
|
+
|
50
|
+
##
|
51
|
+
|
52
|
+
|
53
|
+
class AsyncioManagedSimpleHttpServer(AsyncExitStacked):
|
54
|
+
def __init__(
|
55
|
+
self,
|
56
|
+
port: int,
|
57
|
+
handler: HttpHandler,
|
58
|
+
*,
|
59
|
+
temp_ssl: bool = False,
|
60
|
+
) -> None:
|
61
|
+
super().__init__()
|
62
|
+
|
63
|
+
self._port = port
|
64
|
+
self._handler = handler
|
65
|
+
|
66
|
+
self._temp_ssl = temp_ssl
|
67
|
+
|
68
|
+
self._lock = threading.RLock()
|
69
|
+
|
70
|
+
self._loop: ta.Optional[asyncio.AbstractEventLoop] = None
|
71
|
+
self._thread: ta.Optional[threading.Thread] = None
|
72
|
+
self._thread_exit_event = asyncio.Event()
|
73
|
+
self._server: ta.Optional[SocketServer] = None
|
74
|
+
|
75
|
+
@cached_nullary
|
76
|
+
def _ssl_context(self) -> ta.Optional['ssl.SSLContext']:
|
77
|
+
if not self._temp_ssl:
|
78
|
+
return None
|
79
|
+
|
80
|
+
ssl_cert = generate_temp_localhost_ssl_cert().cert # FIXME: async blocking
|
81
|
+
|
82
|
+
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
83
|
+
ssl_context.load_cert_chain(
|
84
|
+
keyfile=ssl_cert.key_file,
|
85
|
+
certfile=ssl_cert.cert_file,
|
86
|
+
)
|
87
|
+
|
88
|
+
return ssl_context
|
89
|
+
|
90
|
+
@contextlib.contextmanager
|
91
|
+
def _make_server(self) -> ta.Iterator[SocketServer]:
|
92
|
+
with make_simple_http_server(
|
93
|
+
self._port,
|
94
|
+
self._handler,
|
95
|
+
ssl_context=self._ssl_context(),
|
96
|
+
use_threads=True,
|
97
|
+
) as server:
|
98
|
+
yield server
|
99
|
+
|
100
|
+
def _thread_main(self) -> None:
|
101
|
+
try:
|
102
|
+
check.none(self._server)
|
103
|
+
with self._make_server() as server:
|
104
|
+
self._server = server
|
105
|
+
|
106
|
+
server.run()
|
107
|
+
|
108
|
+
finally:
|
109
|
+
check.not_none(self._loop).call_soon_threadsafe(self._thread_exit_event.set)
|
110
|
+
|
111
|
+
def is_running(self) -> bool:
|
112
|
+
return self._server is not None
|
113
|
+
|
114
|
+
def shutdown(self) -> None:
|
115
|
+
if (server := self._server) is not None:
|
116
|
+
server.shutdown(block=False)
|
117
|
+
|
118
|
+
async def run(self) -> None:
|
119
|
+
with self._lock:
|
120
|
+
check.none(self._loop)
|
121
|
+
check.none(self._thread)
|
122
|
+
check.state(not self._thread_exit_event.is_set())
|
123
|
+
|
124
|
+
if self._temp_ssl:
|
125
|
+
# Hit the ExitStack from this thread
|
126
|
+
self._ssl_context()
|
127
|
+
|
128
|
+
self._loop = check.not_none(asyncio.get_running_loop())
|
129
|
+
self._thread = threading.Thread(
|
130
|
+
target=self._thread_main,
|
131
|
+
daemon=True,
|
132
|
+
)
|
133
|
+
self._thread.start()
|
134
|
+
|
135
|
+
await self._thread_exit_event.wait()
|
136
|
+
|
137
|
+
|
138
|
+
##
|
139
|
+
|
140
|
+
|
141
|
+
class DockerDataServer(AsyncExitStacked):
|
142
|
+
def __init__(
|
143
|
+
self,
|
144
|
+
port: int,
|
145
|
+
data_server: DataServer,
|
146
|
+
*,
|
147
|
+
handler_log: ta.Optional[logging.Logger] = None,
|
148
|
+
stop_event: ta.Optional[asyncio.Event] = None,
|
149
|
+
) -> None:
|
150
|
+
super().__init__()
|
151
|
+
|
152
|
+
self._port = port
|
153
|
+
self._data_server = data_server
|
154
|
+
|
155
|
+
self._handler_log = handler_log
|
156
|
+
|
157
|
+
if stop_event is None:
|
158
|
+
stop_event = asyncio.Event()
|
159
|
+
self._stop_event = stop_event
|
160
|
+
|
161
|
+
@property
|
162
|
+
def stop_event(self) -> asyncio.Event:
|
163
|
+
return self._stop_event
|
164
|
+
|
165
|
+
async def run(self) -> None:
|
166
|
+
relay_port: ta.Optional[int] = None
|
167
|
+
if sys.platform == 'darwin':
|
168
|
+
relay_port = self._port
|
169
|
+
server_port = self._port + 1
|
170
|
+
else:
|
171
|
+
server_port = self._port
|
172
|
+
|
173
|
+
#
|
174
|
+
|
175
|
+
handler: HttpHandler = DataServerHttpHandler(self._data_server)
|
176
|
+
|
177
|
+
if self._handler_log is not None:
|
178
|
+
handler = LoggingHttpHandler(
|
179
|
+
handler,
|
180
|
+
self._handler_log,
|
181
|
+
)
|
182
|
+
|
183
|
+
#
|
184
|
+
|
185
|
+
async with contextlib.AsyncExitStack() as es:
|
186
|
+
if relay_port is not None:
|
187
|
+
await es.enter_async_context(start_docker_port_relay( # noqa
|
188
|
+
relay_port,
|
189
|
+
server_port,
|
190
|
+
intermediate_port=server_port + 1,
|
191
|
+
))
|
192
|
+
|
193
|
+
async with AsyncioManagedSimpleHttpServer(
|
194
|
+
server_port,
|
195
|
+
handler,
|
196
|
+
temp_ssl=True,
|
197
|
+
) as server:
|
198
|
+
server_run_task = asyncio.create_task(server.run())
|
199
|
+
try:
|
200
|
+
await self._stop_event.wait()
|
201
|
+
|
202
|
+
finally:
|
203
|
+
server.shutdown()
|
204
|
+
await server_run_task
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import dataclasses as dc
|
4
|
+
import typing as ta
|
5
|
+
|
6
|
+
from omlish.lite.timing import log_timing_context
|
7
|
+
|
8
|
+
from ..cache import FileCache
|
9
|
+
from .cache import DockerCache
|
10
|
+
from .cmds import is_docker_image_present
|
11
|
+
from .cmds import pull_docker_image
|
12
|
+
|
13
|
+
|
14
|
+
##
|
15
|
+
|
16
|
+
|
17
|
+
class DockerImagePulling(abc.ABC):
|
18
|
+
@abc.abstractmethod
|
19
|
+
def pull_docker_image(self, image: str) -> ta.Awaitable[None]:
|
20
|
+
raise NotImplementedError
|
21
|
+
|
22
|
+
|
23
|
+
class DockerImagePullingImpl(DockerImagePulling):
|
24
|
+
@dc.dataclass(frozen=True)
|
25
|
+
class Config:
|
26
|
+
always_pull: bool = False
|
27
|
+
|
28
|
+
def __init__(
|
29
|
+
self,
|
30
|
+
*,
|
31
|
+
config: Config = Config(),
|
32
|
+
|
33
|
+
file_cache: ta.Optional[FileCache] = None,
|
34
|
+
docker_cache: ta.Optional[DockerCache] = None,
|
35
|
+
) -> None:
|
36
|
+
super().__init__()
|
37
|
+
|
38
|
+
self._config = config
|
39
|
+
|
40
|
+
self._file_cache = file_cache
|
41
|
+
self._docker_cache = docker_cache
|
42
|
+
|
43
|
+
async def _pull_docker_image(self, image: str) -> None:
|
44
|
+
if not self._config.always_pull and (await is_docker_image_present(image)):
|
45
|
+
return
|
46
|
+
|
47
|
+
dep_suffix = image
|
48
|
+
for c in '/:.-_':
|
49
|
+
dep_suffix = dep_suffix.replace(c, '-')
|
50
|
+
|
51
|
+
cache_key = f'docker-{dep_suffix}'
|
52
|
+
if (
|
53
|
+
self._docker_cache is not None and
|
54
|
+
(await self._docker_cache.load_cache_docker_image(cache_key)) is not None
|
55
|
+
):
|
56
|
+
return
|
57
|
+
|
58
|
+
await pull_docker_image(image)
|
59
|
+
|
60
|
+
if self._docker_cache is not None:
|
61
|
+
await self._docker_cache.save_cache_docker_image(cache_key, image)
|
62
|
+
|
63
|
+
async def pull_docker_image(self, image: str) -> None:
|
64
|
+
with log_timing_context(f'Load docker image: {image}'):
|
65
|
+
await self._pull_docker_image(image)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
from omlish.lite.inject import InjectorBindingOrBindings
|
5
|
+
from omlish.lite.inject import InjectorBindings
|
6
|
+
from omlish.lite.inject import inj
|
7
|
+
|
8
|
+
from .buildcaching import DockerBuildCaching
|
9
|
+
from .buildcaching import DockerBuildCachingImpl
|
10
|
+
from .cache import DockerCache
|
11
|
+
from .cache import DockerCacheImpl
|
12
|
+
from .imagepulling import DockerImagePulling
|
13
|
+
from .imagepulling import DockerImagePullingImpl
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
|
18
|
+
|
19
|
+
def bind_docker(
|
20
|
+
*,
|
21
|
+
build_caching_config: DockerBuildCachingImpl.Config,
|
22
|
+
image_pulling_config: DockerImagePullingImpl.Config = DockerImagePullingImpl.Config(),
|
23
|
+
) -> InjectorBindings:
|
24
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
25
|
+
inj.bind(build_caching_config),
|
26
|
+
inj.bind(DockerBuildCachingImpl, singleton=True),
|
27
|
+
inj.bind(DockerBuildCaching, to_key=DockerBuildCachingImpl),
|
28
|
+
|
29
|
+
inj.bind(DockerCacheImpl, singleton=True),
|
30
|
+
inj.bind(DockerCache, to_key=DockerCacheImpl),
|
31
|
+
|
32
|
+
inj.bind(image_pulling_config),
|
33
|
+
inj.bind(DockerImagePullingImpl, singleton=True),
|
34
|
+
inj.bind(DockerImagePulling, to_key=DockerImagePullingImpl),
|
35
|
+
]
|
36
|
+
|
37
|
+
return inj.as_bindings(*lst)
|