omdev 0.0.0.dev223__py3-none-any.whl → 0.0.0.dev224__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omdev/ci/cache.py +108 -0
- omdev/ci/ci.py +1 -1
- omdev/ci/docker/cacheserved.py +262 -0
- omdev/ci/docker/dataserver.py +204 -0
- omdev/ci/docker/imagepulling.py +2 -1
- omdev/ci/docker/packing.py +72 -0
- omdev/ci/docker/repositories.py +40 -0
- omdev/ci/github/cache.py +20 -1
- omdev/ci/github/client.py +9 -2
- omdev/ci/github/inject.py +4 -4
- omdev/ci/utils.py +0 -49
- omdev/dataserver/targets.py +32 -0
- omdev/oci/data.py +19 -0
- omdev/oci/dataserver.py +4 -1
- omdev/oci/pack/__init__.py +0 -0
- omdev/oci/pack/packing.py +185 -0
- omdev/oci/pack/repositories.py +162 -0
- omdev/oci/{packing.py → pack/unpacking.py} +0 -177
- omdev/oci/repositories.py +6 -0
- omdev/scripts/ci.py +423 -255
- omdev/scripts/interp.py +19 -0
- omdev/scripts/pyproject.py +19 -0
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev224.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev224.dist-info}/RECORD +28 -21
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev224.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev224.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev224.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev224.dist-info}/top_level.txt +0 -0
omdev/ci/cache.py
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import abc
|
3
|
+
import asyncio
|
3
4
|
import dataclasses as dc
|
5
|
+
import functools
|
4
6
|
import os.path
|
5
7
|
import shutil
|
6
8
|
import typing as ta
|
9
|
+
import urllib.request
|
7
10
|
|
8
11
|
from omlish.lite.cached import cached_nullary
|
9
12
|
from omlish.lite.check import check
|
10
13
|
from omlish.lite.logs import log
|
14
|
+
from omlish.os.temp import make_temp_file
|
11
15
|
|
12
16
|
from .consts import CI_CACHE_VERSION
|
13
17
|
|
@@ -162,3 +166,107 @@ class DirectoryFileCache(FileCache):
|
|
162
166
|
else:
|
163
167
|
shutil.copyfile(file_path, cache_file_path)
|
164
168
|
return cache_file_path
|
169
|
+
|
170
|
+
|
171
|
+
##
|
172
|
+
|
173
|
+
|
174
|
+
class DataCache:
|
175
|
+
@dc.dataclass(frozen=True)
|
176
|
+
class Data(abc.ABC): # noqa
|
177
|
+
pass
|
178
|
+
|
179
|
+
@dc.dataclass(frozen=True)
|
180
|
+
class BytesData(Data):
|
181
|
+
data: bytes
|
182
|
+
|
183
|
+
@dc.dataclass(frozen=True)
|
184
|
+
class FileData(Data):
|
185
|
+
file_path: str
|
186
|
+
|
187
|
+
@dc.dataclass(frozen=True)
|
188
|
+
class UrlData(Data):
|
189
|
+
url: str
|
190
|
+
|
191
|
+
#
|
192
|
+
|
193
|
+
@abc.abstractmethod
|
194
|
+
def get_data(self, key: str) -> ta.Awaitable[ta.Optional[Data]]:
|
195
|
+
raise NotImplementedError
|
196
|
+
|
197
|
+
@abc.abstractmethod
|
198
|
+
def put_data(self, key: str, data: Data) -> ta.Awaitable[None]:
|
199
|
+
raise NotImplementedError
|
200
|
+
|
201
|
+
|
202
|
+
#
|
203
|
+
|
204
|
+
|
205
|
+
@functools.singledispatch
|
206
|
+
async def read_data_cache_data(data: DataCache.Data) -> bytes:
|
207
|
+
raise TypeError(data)
|
208
|
+
|
209
|
+
|
210
|
+
@read_data_cache_data.register
|
211
|
+
async def _(data: DataCache.BytesData) -> bytes:
|
212
|
+
return data.data
|
213
|
+
|
214
|
+
|
215
|
+
@read_data_cache_data.register
|
216
|
+
async def _(data: DataCache.FileData) -> bytes:
|
217
|
+
with open(data.file_path, 'rb') as f: # noqa
|
218
|
+
return f.read()
|
219
|
+
|
220
|
+
|
221
|
+
@read_data_cache_data.register
|
222
|
+
async def _(data: DataCache.UrlData) -> bytes:
|
223
|
+
def inner() -> bytes:
|
224
|
+
with urllib.request.urlopen(urllib.request.Request( # noqa
|
225
|
+
data.url,
|
226
|
+
)) as resp:
|
227
|
+
return resp.read()
|
228
|
+
|
229
|
+
return await asyncio.get_running_loop().run_in_executor(None, inner)
|
230
|
+
|
231
|
+
|
232
|
+
#
|
233
|
+
|
234
|
+
|
235
|
+
class FileCacheDataCache(DataCache):
|
236
|
+
def __init__(
|
237
|
+
self,
|
238
|
+
file_cache: FileCache,
|
239
|
+
) -> None:
|
240
|
+
super().__init__()
|
241
|
+
|
242
|
+
self._file_cache = file_cache
|
243
|
+
|
244
|
+
async def get_data(self, key: str) -> ta.Optional[DataCache.Data]:
|
245
|
+
if (file_path := await self._file_cache.get_file(key)) is None:
|
246
|
+
return None
|
247
|
+
|
248
|
+
return DataCache.FileData(file_path)
|
249
|
+
|
250
|
+
async def put_data(self, key: str, data: DataCache.Data) -> None:
|
251
|
+
steal = False
|
252
|
+
|
253
|
+
if isinstance(data, DataCache.BytesData):
|
254
|
+
file_path = make_temp_file()
|
255
|
+
with open(file_path, 'wb') as f: # noqa
|
256
|
+
f.write(data.data)
|
257
|
+
steal = True
|
258
|
+
|
259
|
+
elif isinstance(data, DataCache.FileData):
|
260
|
+
file_path = data.file_path
|
261
|
+
|
262
|
+
elif isinstance(data, DataCache.UrlData):
|
263
|
+
raise NotImplementedError
|
264
|
+
|
265
|
+
else:
|
266
|
+
raise TypeError(data)
|
267
|
+
|
268
|
+
await self._file_cache.put_file(
|
269
|
+
key,
|
270
|
+
file_path,
|
271
|
+
steal=steal,
|
272
|
+
)
|
omdev/ci/ci.py
CHANGED
@@ -7,6 +7,7 @@ from omlish.lite.cached import async_cached_nullary
|
|
7
7
|
from omlish.lite.cached import cached_nullary
|
8
8
|
from omlish.lite.check import check
|
9
9
|
from omlish.lite.contextmanagers import AsyncExitStacked
|
10
|
+
from omlish.lite.timing import log_timing_context
|
10
11
|
from omlish.os.temp import temp_file_context
|
11
12
|
|
12
13
|
from .compose import DockerComposeRun
|
@@ -17,7 +18,6 @@ from .docker.imagepulling import DockerImagePulling
|
|
17
18
|
from .docker.utils import build_docker_file_hash
|
18
19
|
from .requirements import build_requirements_hash
|
19
20
|
from .shell import ShellCmd
|
20
|
-
from .utils import log_timing_context
|
21
21
|
|
22
22
|
|
23
23
|
##
|
@@ -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
|
+
)
|
@@ -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
|
omdev/ci/docker/imagepulling.py
CHANGED
@@ -3,8 +3,9 @@ import abc
|
|
3
3
|
import dataclasses as dc
|
4
4
|
import typing as ta
|
5
5
|
|
6
|
+
from omlish.lite.timing import log_timing_context
|
7
|
+
|
6
8
|
from ..cache import FileCache
|
7
|
-
from ..utils import log_timing_context
|
8
9
|
from .cache import DockerCache
|
9
10
|
from .cmds import is_docker_image_present
|
10
11
|
from .cmds import pull_docker_image
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import asyncio
|
3
|
+
import os.path
|
4
|
+
import shlex
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
|
8
|
+
from omlish.lite.cached import async_cached_nullary
|
9
|
+
from omlish.lite.cached import cached_nullary
|
10
|
+
from omlish.lite.contextmanagers import ExitStacked
|
11
|
+
from omlish.logs.timing import log_timing_context
|
12
|
+
from omlish.os.temp import temp_dir_context
|
13
|
+
|
14
|
+
from ...oci.building import BuiltOciImageIndexRepository
|
15
|
+
from ...oci.pack.repositories import OciPackedRepositoryBuilder
|
16
|
+
from ...oci.repositories import DirectoryOciRepository
|
17
|
+
|
18
|
+
|
19
|
+
##
|
20
|
+
|
21
|
+
|
22
|
+
class PackedDockerImageIndexRepositoryBuilder(ExitStacked):
|
23
|
+
def __init__(
|
24
|
+
self,
|
25
|
+
*,
|
26
|
+
image_id: str,
|
27
|
+
|
28
|
+
temp_dir: ta.Optional[str] = None,
|
29
|
+
) -> None:
|
30
|
+
super().__init__()
|
31
|
+
|
32
|
+
self._image_id = image_id
|
33
|
+
|
34
|
+
self._given_temp_dir = temp_dir
|
35
|
+
|
36
|
+
@cached_nullary
|
37
|
+
def _temp_dir(self) -> str:
|
38
|
+
if (given := self._given_temp_dir) is not None:
|
39
|
+
return given
|
40
|
+
else:
|
41
|
+
return self._enter_context(temp_dir_context()) # noqa
|
42
|
+
|
43
|
+
#
|
44
|
+
|
45
|
+
@async_cached_nullary
|
46
|
+
async def _save_to_dir(self) -> str:
|
47
|
+
save_dir = os.path.join(self._temp_dir(), 'built-image')
|
48
|
+
os.mkdir(save_dir)
|
49
|
+
|
50
|
+
with log_timing_context(f'Saving docker image {self._image_id}'):
|
51
|
+
await asyncio_subprocesses.check_call(
|
52
|
+
' | '.join([
|
53
|
+
f'docker save {shlex.quote(self._image_id)}',
|
54
|
+
f'tar x -C {shlex.quote(save_dir)}',
|
55
|
+
]),
|
56
|
+
shell=True,
|
57
|
+
)
|
58
|
+
|
59
|
+
return save_dir
|
60
|
+
|
61
|
+
#
|
62
|
+
|
63
|
+
@async_cached_nullary
|
64
|
+
async def build(self) -> BuiltOciImageIndexRepository:
|
65
|
+
saved_dir = await self._save_to_dir()
|
66
|
+
|
67
|
+
with OciPackedRepositoryBuilder(
|
68
|
+
DirectoryOciRepository(saved_dir),
|
69
|
+
|
70
|
+
temp_dir=self._temp_dir(),
|
71
|
+
) as prb:
|
72
|
+
return await asyncio.get_running_loop().run_in_executor(None, prb.build)
|