omdev 0.0.0.dev223__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 +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)
|