omdev 0.0.0.dev240__py3-none-any.whl → 0.0.0.dev242__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/.manifests.json +1 -1
- omdev/ci/cache.py +1 -1
- omdev/ci/ci.py +62 -12
- omdev/ci/cli.py +12 -0
- omdev/ci/consts.py +1 -1
- omdev/ci/docker/buildcaching.py +4 -3
- omdev/ci/docker/cache.py +28 -6
- omdev/ci/docker/cacheserved/__init__.py +0 -0
- omdev/ci/docker/cacheserved/cache.py +209 -0
- omdev/ci/docker/cacheserved/manifests.py +122 -0
- omdev/ci/docker/cmds.py +18 -0
- omdev/ci/docker/dataserver.py +9 -1
- omdev/ci/docker/imagepulling.py +4 -4
- omdev/ci/docker/inject.py +36 -7
- omdev/ci/github/client.py +2 -2
- omdev/ci/github/inject.py +2 -0
- omdev/ci/inject.py +16 -1
- omdev/dataserver/http.py +1 -0
- omdev/precheck/lite.py +3 -0
- omdev/scripts/bumpversion.py +4 -0
- omdev/scripts/ci.py +7276 -1321
- omdev/scripts/interp.py +21 -12
- omdev/scripts/pyproject.py +21 -12
- omdev/tools/docker.py +11 -27
- omdev/tools/git/cli.py +1 -0
- {omdev-0.0.0.dev240.dist-info → omdev-0.0.0.dev242.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev240.dist-info → omdev-0.0.0.dev242.dist-info}/RECORD +31 -29
- omdev/ci/docker/cacheserved.py +0 -262
- {omdev-0.0.0.dev240.dist-info → omdev-0.0.0.dev242.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev240.dist-info → omdev-0.0.0.dev242.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev240.dist-info → omdev-0.0.0.dev242.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev240.dist-info → omdev-0.0.0.dev242.dist-info}/top_level.txt +0 -0
omdev/.manifests.json
CHANGED
omdev/ci/cache.py
CHANGED
@@ -118,7 +118,7 @@ class DirectoryFileCache(FileCache):
|
|
118
118
|
elif not os.path.isdir(self.dir):
|
119
119
|
os.makedirs(self.dir)
|
120
120
|
with open(version_file, 'w') as f:
|
121
|
-
f.write(
|
121
|
+
f.write(f'{self._version}\n')
|
122
122
|
return
|
123
123
|
|
124
124
|
# NOTE: intentionally raises FileNotFoundError to refuse to use an existing non-cache dir as a cache dir.
|
omdev/ci/ci.py
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import dataclasses as dc
|
3
|
+
import functools
|
3
4
|
import os.path
|
4
5
|
import typing as ta
|
5
6
|
|
7
|
+
from omlish.asyncs.asyncio.asyncio import asyncio_wait_maybe_concurrent
|
6
8
|
from omlish.lite.cached import async_cached_nullary
|
7
9
|
from omlish.lite.cached import cached_nullary
|
8
10
|
from omlish.lite.check import check
|
@@ -13,7 +15,9 @@ from omlish.os.temp import temp_file_context
|
|
13
15
|
from .compose import DockerComposeRun
|
14
16
|
from .compose import get_compose_service_dependencies
|
15
17
|
from .docker.buildcaching import DockerBuildCaching
|
18
|
+
from .docker.cache import DockerCacheKey
|
16
19
|
from .docker.cmds import build_docker_image
|
20
|
+
from .docker.cmds import ensure_docker_image_setup
|
17
21
|
from .docker.imagepulling import DockerImagePulling
|
18
22
|
from .docker.utils import build_docker_file_hash
|
19
23
|
from .requirements import build_requirements_hash
|
@@ -44,8 +48,12 @@ class Ci(AsyncExitStacked):
|
|
44
48
|
always_pull: bool = False
|
45
49
|
always_build: bool = False
|
46
50
|
|
51
|
+
setup_concurrency: ta.Optional[int] = None
|
52
|
+
|
47
53
|
no_dependencies: bool = False
|
48
54
|
|
55
|
+
setup_only: bool = False
|
56
|
+
|
49
57
|
run_options: ta.Optional[ta.Sequence[str]] = None
|
50
58
|
|
51
59
|
#
|
@@ -74,8 +82,8 @@ class Ci(AsyncExitStacked):
|
|
74
82
|
return build_docker_file_hash(self._config.docker_file)[:self.KEY_HASH_LEN]
|
75
83
|
|
76
84
|
@cached_nullary
|
77
|
-
def ci_base_image_cache_key(self) ->
|
78
|
-
return
|
85
|
+
def ci_base_image_cache_key(self) -> DockerCacheKey:
|
86
|
+
return DockerCacheKey(['ci-base'], self.docker_file_hash())
|
79
87
|
|
80
88
|
async def _resolve_ci_base_image(self) -> str:
|
81
89
|
async def build_and_tag(image_tag: str) -> str:
|
@@ -111,8 +119,8 @@ class Ci(AsyncExitStacked):
|
|
111
119
|
return build_requirements_hash(self.requirements_txts())[:self.KEY_HASH_LEN]
|
112
120
|
|
113
121
|
@cached_nullary
|
114
|
-
def ci_image_cache_key(self) ->
|
115
|
-
return f'
|
122
|
+
def ci_image_cache_key(self) -> DockerCacheKey:
|
123
|
+
return DockerCacheKey(['ci'], f'{self.docker_file_hash()}-{self.requirements_hash()}')
|
116
124
|
|
117
125
|
async def _resolve_ci_image(self) -> str:
|
118
126
|
async def build_and_tag(image_tag: str) -> str:
|
@@ -168,15 +176,40 @@ class Ci(AsyncExitStacked):
|
|
168
176
|
|
169
177
|
#
|
170
178
|
|
171
|
-
@
|
172
|
-
|
179
|
+
@cached_nullary
|
180
|
+
def get_dependency_images(self) -> ta.Sequence[str]:
|
173
181
|
deps = get_compose_service_dependencies(
|
174
182
|
self._config.compose_file,
|
175
183
|
self._config.service,
|
176
184
|
)
|
185
|
+
return sorted(deps.values())
|
186
|
+
|
187
|
+
@cached_nullary
|
188
|
+
def pull_dependencies_funcs(self) -> ta.Sequence[ta.Callable[[], ta.Awaitable]]:
|
189
|
+
return [
|
190
|
+
async_cached_nullary(functools.partial(
|
191
|
+
self._docker_image_pulling.pull_docker_image,
|
192
|
+
dep_image,
|
193
|
+
))
|
194
|
+
for dep_image in self.get_dependency_images()
|
195
|
+
]
|
196
|
+
|
197
|
+
#
|
198
|
+
|
199
|
+
@cached_nullary
|
200
|
+
def setup_funcs(self) -> ta.Sequence[ta.Callable[[], ta.Awaitable]]:
|
201
|
+
return [
|
202
|
+
self.resolve_ci_image,
|
177
203
|
|
178
|
-
|
179
|
-
|
204
|
+
*(self.pull_dependencies_funcs() if not self._config.no_dependencies else []),
|
205
|
+
]
|
206
|
+
|
207
|
+
@async_cached_nullary
|
208
|
+
async def setup(self) -> None:
|
209
|
+
await asyncio_wait_maybe_concurrent(
|
210
|
+
[fn() for fn in self.setup_funcs()],
|
211
|
+
self._config.setup_concurrency,
|
212
|
+
)
|
180
213
|
|
181
214
|
#
|
182
215
|
|
@@ -206,9 +239,26 @@ class Ci(AsyncExitStacked):
|
|
206
239
|
|
207
240
|
#
|
208
241
|
|
209
|
-
async def
|
210
|
-
|
242
|
+
async def _run_setup_only(self) -> None:
|
243
|
+
image_ids = [
|
244
|
+
await self.resolve_ci_image(),
|
245
|
+
|
246
|
+
*(self.get_dependency_images() if not self._config.no_dependencies else []),
|
247
|
+
]
|
211
248
|
|
212
|
-
|
249
|
+
for image_id in image_ids:
|
250
|
+
with log_timing_context(f'Run setup only: {image_id}'):
|
251
|
+
await ensure_docker_image_setup(
|
252
|
+
image_id,
|
253
|
+
cwd=self._config.project_dir,
|
254
|
+
)
|
255
|
+
|
256
|
+
#
|
257
|
+
|
258
|
+
async def run(self) -> None:
|
259
|
+
await self.setup()
|
213
260
|
|
214
|
-
|
261
|
+
if self._config.setup_only:
|
262
|
+
await self._run_setup_only()
|
263
|
+
else:
|
264
|
+
await self._run_compose()
|
omdev/ci/cli.py
CHANGED
@@ -88,11 +88,17 @@ class CiCli(ArgparseCli):
|
|
88
88
|
argparse_arg('--github', action='store_true'),
|
89
89
|
argparse_arg('--github-detect', action='store_true'),
|
90
90
|
|
91
|
+
argparse_arg('--cache-served-docker', action='store_true'),
|
92
|
+
|
93
|
+
argparse_arg('--setup-concurrency', type=int),
|
94
|
+
|
91
95
|
argparse_arg('--always-pull', action='store_true'),
|
92
96
|
argparse_arg('--always-build', action='store_true'),
|
93
97
|
|
94
98
|
argparse_arg('--no-dependencies', action='store_true'),
|
95
99
|
|
100
|
+
argparse_arg('--setup-only', action='store_true'),
|
101
|
+
|
96
102
|
argparse_arg('-e', '--env', action='append'),
|
97
103
|
argparse_arg('-v', '--volume', action='append'),
|
98
104
|
|
@@ -203,8 +209,12 @@ class CiCli(ArgparseCli):
|
|
203
209
|
always_pull=self.args.always_pull,
|
204
210
|
always_build=self.args.always_build,
|
205
211
|
|
212
|
+
setup_concurrency=self.args.setup_concurrency,
|
213
|
+
|
206
214
|
no_dependencies=self.args.no_dependencies,
|
207
215
|
|
216
|
+
setup_only=self.args.setup_only,
|
217
|
+
|
208
218
|
run_options=run_options,
|
209
219
|
)
|
210
220
|
|
@@ -225,6 +235,8 @@ class CiCli(ArgparseCli):
|
|
225
235
|
directory_file_cache_config=directory_file_cache_config,
|
226
236
|
|
227
237
|
github=github,
|
238
|
+
|
239
|
+
cache_served_docker=self.args.cache_served_docker,
|
228
240
|
))
|
229
241
|
|
230
242
|
async with injector[Ci] as ci:
|
omdev/ci/consts.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
CI_CACHE_VERSION =
|
1
|
+
CI_CACHE_VERSION = 2
|
omdev/ci/docker/buildcaching.py
CHANGED
@@ -4,6 +4,7 @@ import dataclasses as dc
|
|
4
4
|
import typing as ta
|
5
5
|
|
6
6
|
from .cache import DockerCache
|
7
|
+
from .cache import DockerCacheKey
|
7
8
|
from .cmds import is_docker_image_present
|
8
9
|
from .cmds import tag_docker_image
|
9
10
|
|
@@ -15,7 +16,7 @@ class DockerBuildCaching(abc.ABC):
|
|
15
16
|
@abc.abstractmethod
|
16
17
|
def cached_build_docker_image(
|
17
18
|
self,
|
18
|
-
cache_key:
|
19
|
+
cache_key: DockerCacheKey,
|
19
20
|
build_and_tag: ta.Callable[[str], ta.Awaitable[str]], # image_tag -> image_id
|
20
21
|
) -> ta.Awaitable[str]:
|
21
22
|
raise NotImplementedError
|
@@ -43,10 +44,10 @@ class DockerBuildCachingImpl(DockerBuildCaching):
|
|
43
44
|
|
44
45
|
async def cached_build_docker_image(
|
45
46
|
self,
|
46
|
-
cache_key:
|
47
|
+
cache_key: DockerCacheKey,
|
47
48
|
build_and_tag: ta.Callable[[str], ta.Awaitable[str]],
|
48
49
|
) -> str:
|
49
|
-
image_tag = f'{self._config.service}:{cache_key}'
|
50
|
+
image_tag = f'{self._config.service}:{cache_key!s}'
|
50
51
|
|
51
52
|
if not self._config.always_build and (await is_docker_image_present(image_tag)):
|
52
53
|
return image_tag
|
omdev/ci/docker/cache.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import abc
|
3
|
+
import dataclasses as dc
|
3
4
|
import typing as ta
|
4
5
|
|
6
|
+
from omlish.lite.check import check
|
5
7
|
from omlish.os.temp import temp_file_context
|
6
8
|
|
7
9
|
from ..cache import FileCache
|
@@ -13,13 +15,33 @@ from .cmds import save_docker_tar_cmd
|
|
13
15
|
##
|
14
16
|
|
15
17
|
|
18
|
+
@dc.dataclass(frozen=True)
|
19
|
+
class DockerCacheKey:
|
20
|
+
prefixes: ta.Sequence[str]
|
21
|
+
content: str
|
22
|
+
|
23
|
+
def __post_init__(self) -> None:
|
24
|
+
check.not_isinstance(self.prefixes, str)
|
25
|
+
|
26
|
+
def append_prefix(self, *prefixes: str) -> 'DockerCacheKey':
|
27
|
+
return dc.replace(self, prefixes=(*self.prefixes, *prefixes))
|
28
|
+
|
29
|
+
SEPARATOR: ta.ClassVar[str] = '--'
|
30
|
+
|
31
|
+
def __str__(self) -> str:
|
32
|
+
return self.SEPARATOR.join([*self.prefixes, self.content])
|
33
|
+
|
34
|
+
|
35
|
+
##
|
36
|
+
|
37
|
+
|
16
38
|
class DockerCache(abc.ABC):
|
17
39
|
@abc.abstractmethod
|
18
|
-
def load_cache_docker_image(self, key:
|
40
|
+
def load_cache_docker_image(self, key: DockerCacheKey) -> ta.Awaitable[ta.Optional[str]]:
|
19
41
|
raise NotImplementedError
|
20
42
|
|
21
43
|
@abc.abstractmethod
|
22
|
-
def save_cache_docker_image(self, key:
|
44
|
+
def save_cache_docker_image(self, key: DockerCacheKey, image: str) -> ta.Awaitable[None]:
|
23
45
|
raise NotImplementedError
|
24
46
|
|
25
47
|
|
@@ -33,11 +55,11 @@ class DockerCacheImpl(DockerCache):
|
|
33
55
|
|
34
56
|
self._file_cache = file_cache
|
35
57
|
|
36
|
-
async def load_cache_docker_image(self, key:
|
58
|
+
async def load_cache_docker_image(self, key: DockerCacheKey) -> ta.Optional[str]:
|
37
59
|
if self._file_cache is None:
|
38
60
|
return None
|
39
61
|
|
40
|
-
cache_file = await self._file_cache.get_file(key)
|
62
|
+
cache_file = await self._file_cache.get_file(str(key))
|
41
63
|
if cache_file is None:
|
42
64
|
return None
|
43
65
|
|
@@ -45,7 +67,7 @@ class DockerCacheImpl(DockerCache):
|
|
45
67
|
|
46
68
|
return await load_docker_tar_cmd(get_cache_cmd)
|
47
69
|
|
48
|
-
async def save_cache_docker_image(self, key:
|
70
|
+
async def save_cache_docker_image(self, key: DockerCacheKey, image: str) -> None:
|
49
71
|
if self._file_cache is None:
|
50
72
|
return
|
51
73
|
|
@@ -54,4 +76,4 @@ class DockerCacheImpl(DockerCache):
|
|
54
76
|
|
55
77
|
await save_docker_tar_cmd(image, write_tmp_cmd)
|
56
78
|
|
57
|
-
await self._file_cache.put_file(key, tmp_file, steal=True)
|
79
|
+
await self._file_cache.put_file(str(key), tmp_file, steal=True)
|
File without changes
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import asyncio
|
3
|
+
import contextlib
|
4
|
+
import dataclasses as dc
|
5
|
+
import json
|
6
|
+
import os.path
|
7
|
+
import typing as ta
|
8
|
+
|
9
|
+
from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
|
10
|
+
from omlish.lite.check import check
|
11
|
+
from omlish.lite.json import json_dumps_compact
|
12
|
+
from omlish.lite.logs import log
|
13
|
+
from omlish.lite.marshal import marshal_obj
|
14
|
+
from omlish.lite.marshal import unmarshal_obj
|
15
|
+
from omlish.lite.timeouts import Timeout
|
16
|
+
from omlish.lite.timeouts import TimeoutLike
|
17
|
+
|
18
|
+
from ....dataserver.server import DataServer
|
19
|
+
from ....dataserver.targets import DataServerTarget
|
20
|
+
from ....oci.building import build_oci_index_repository
|
21
|
+
from ....oci.data import get_single_leaf_oci_image_index
|
22
|
+
from ....oci.dataserver import build_oci_repository_data_server_routes
|
23
|
+
from ....oci.loading import read_oci_repository_root_index
|
24
|
+
from ....oci.pack.repositories import OciPackedRepositoryBuilder
|
25
|
+
from ....oci.repositories import OciRepository
|
26
|
+
from ...cache import DataCache
|
27
|
+
from ...cache import read_data_cache_data
|
28
|
+
from ..cache import DockerCache
|
29
|
+
from ..cache import DockerCacheKey
|
30
|
+
from ..dataserver import DockerDataServer
|
31
|
+
from ..repositories import DockerImageRepositoryOpener
|
32
|
+
from .manifests import CacheServedDockerImageManifest
|
33
|
+
from .manifests import build_cache_served_docker_image_data_server_routes
|
34
|
+
from .manifests import build_cache_served_docker_image_manifest
|
35
|
+
|
36
|
+
|
37
|
+
##
|
38
|
+
|
39
|
+
|
40
|
+
class CacheServedDockerCache(DockerCache):
|
41
|
+
@dc.dataclass(frozen=True)
|
42
|
+
class Config:
|
43
|
+
port: int = 5021
|
44
|
+
|
45
|
+
repack: bool = True
|
46
|
+
|
47
|
+
key_prefix: ta.Optional[str] = 'cs'
|
48
|
+
|
49
|
+
#
|
50
|
+
|
51
|
+
pull_run_cmd: ta.Optional[str] = 'true'
|
52
|
+
|
53
|
+
#
|
54
|
+
|
55
|
+
server_start_timeout: TimeoutLike = 5.
|
56
|
+
server_start_sleep: float = .1
|
57
|
+
|
58
|
+
def __init__(
|
59
|
+
self,
|
60
|
+
*,
|
61
|
+
config: Config = Config(),
|
62
|
+
|
63
|
+
image_repo_opener: DockerImageRepositoryOpener,
|
64
|
+
data_cache: DataCache,
|
65
|
+
) -> None:
|
66
|
+
super().__init__()
|
67
|
+
|
68
|
+
self._config = config
|
69
|
+
|
70
|
+
self._image_repo_opener = image_repo_opener
|
71
|
+
self._data_cache = data_cache
|
72
|
+
|
73
|
+
async def load_cache_docker_image(self, key: DockerCacheKey) -> ta.Optional[str]:
|
74
|
+
if (kp := self._config.key_prefix) is not None:
|
75
|
+
key = key.append_prefix(kp)
|
76
|
+
|
77
|
+
if (manifest_data := await self._data_cache.get_data(str(key))) is None:
|
78
|
+
return None
|
79
|
+
|
80
|
+
manifest_bytes = await read_data_cache_data(manifest_data)
|
81
|
+
|
82
|
+
manifest: CacheServedDockerImageManifest = unmarshal_obj(
|
83
|
+
json.loads(manifest_bytes.decode('utf-8')),
|
84
|
+
CacheServedDockerImageManifest,
|
85
|
+
)
|
86
|
+
|
87
|
+
async def make_cache_key_target(target_cache_key: str, **target_kwargs: ta.Any) -> DataServerTarget: # noqa
|
88
|
+
cache_data = check.not_none(await self._data_cache.get_data(target_cache_key))
|
89
|
+
|
90
|
+
if isinstance(cache_data, DataCache.BytesData):
|
91
|
+
return DataServerTarget.of(
|
92
|
+
cache_data.data,
|
93
|
+
**target_kwargs,
|
94
|
+
)
|
95
|
+
|
96
|
+
elif isinstance(cache_data, DataCache.FileData):
|
97
|
+
return DataServerTarget.of(
|
98
|
+
file_path=cache_data.file_path,
|
99
|
+
**target_kwargs,
|
100
|
+
)
|
101
|
+
|
102
|
+
elif isinstance(cache_data, DataCache.UrlData):
|
103
|
+
return DataServerTarget.of(
|
104
|
+
url=cache_data.url,
|
105
|
+
methods=['GET'],
|
106
|
+
**target_kwargs,
|
107
|
+
)
|
108
|
+
|
109
|
+
else:
|
110
|
+
raise TypeError(cache_data)
|
111
|
+
|
112
|
+
data_server_routes = await build_cache_served_docker_image_data_server_routes(
|
113
|
+
manifest,
|
114
|
+
make_cache_key_target,
|
115
|
+
)
|
116
|
+
|
117
|
+
data_server = DataServer(DataServer.HandlerRoute.of_(*data_server_routes))
|
118
|
+
|
119
|
+
image_url = f'localhost:{self._config.port}/{key!s}'
|
120
|
+
|
121
|
+
async with DockerDataServer(
|
122
|
+
self._config.port,
|
123
|
+
data_server,
|
124
|
+
handler_log=log,
|
125
|
+
) as dds:
|
126
|
+
dds_run_task = asyncio.create_task(dds.run())
|
127
|
+
try:
|
128
|
+
timeout = Timeout.of(self._config.server_start_timeout)
|
129
|
+
while True:
|
130
|
+
timeout()
|
131
|
+
try:
|
132
|
+
reader, writer = await asyncio.open_connection('localhost', self._config.port)
|
133
|
+
except Exception as e: # noqa
|
134
|
+
log.exception('Failed to connect to cache server - will try again')
|
135
|
+
else:
|
136
|
+
writer.close()
|
137
|
+
await asyncio.wait_for(writer.wait_closed(), timeout=timeout.remaining())
|
138
|
+
break
|
139
|
+
await asyncio.sleep(self._config.server_start_sleep)
|
140
|
+
|
141
|
+
if (prc := self._config.pull_run_cmd) is not None:
|
142
|
+
pull_cmd = [
|
143
|
+
'run',
|
144
|
+
'--rm',
|
145
|
+
image_url,
|
146
|
+
prc,
|
147
|
+
]
|
148
|
+
else:
|
149
|
+
pull_cmd = [
|
150
|
+
'pull',
|
151
|
+
image_url,
|
152
|
+
]
|
153
|
+
|
154
|
+
await asyncio_subprocesses.check_call(
|
155
|
+
'docker',
|
156
|
+
*pull_cmd,
|
157
|
+
)
|
158
|
+
|
159
|
+
finally:
|
160
|
+
dds.stop_event.set()
|
161
|
+
await dds_run_task
|
162
|
+
|
163
|
+
return image_url
|
164
|
+
|
165
|
+
async def save_cache_docker_image(self, key: DockerCacheKey, image: str) -> None:
|
166
|
+
if (kp := self._config.key_prefix) is not None:
|
167
|
+
key = key.append_prefix(kp)
|
168
|
+
|
169
|
+
async with contextlib.AsyncExitStack() as es:
|
170
|
+
image_repo: OciRepository = await es.enter_async_context(
|
171
|
+
self._image_repo_opener.open_docker_image_repository(image),
|
172
|
+
)
|
173
|
+
|
174
|
+
root_image_index = read_oci_repository_root_index(image_repo)
|
175
|
+
image_index = get_single_leaf_oci_image_index(root_image_index)
|
176
|
+
|
177
|
+
if self._config.repack:
|
178
|
+
prb: OciPackedRepositoryBuilder = es.enter_context(OciPackedRepositoryBuilder(
|
179
|
+
image_repo,
|
180
|
+
))
|
181
|
+
built_repo = await asyncio.get_running_loop().run_in_executor(None, prb.build) # noqa
|
182
|
+
|
183
|
+
else:
|
184
|
+
built_repo = build_oci_index_repository(image_index)
|
185
|
+
|
186
|
+
data_server_routes = build_oci_repository_data_server_routes(
|
187
|
+
str(key),
|
188
|
+
built_repo,
|
189
|
+
)
|
190
|
+
|
191
|
+
async def make_file_cache_key(file_path: str) -> str:
|
192
|
+
target_cache_key = f'{key!s}--{os.path.basename(file_path).split(".")[0]}'
|
193
|
+
await self._data_cache.put_data(
|
194
|
+
target_cache_key,
|
195
|
+
DataCache.FileData(file_path),
|
196
|
+
)
|
197
|
+
return target_cache_key
|
198
|
+
|
199
|
+
cache_served_manifest = await build_cache_served_docker_image_manifest(
|
200
|
+
data_server_routes,
|
201
|
+
make_file_cache_key,
|
202
|
+
)
|
203
|
+
|
204
|
+
manifest_data = json_dumps_compact(marshal_obj(cache_served_manifest)).encode('utf-8')
|
205
|
+
|
206
|
+
await self._data_cache.put_data(
|
207
|
+
str(key),
|
208
|
+
DataCache.BytesData(manifest_data),
|
209
|
+
)
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import dataclasses as dc
|
4
|
+
import os.path
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
from omlish.lite.check import check
|
8
|
+
|
9
|
+
from ....dataserver.routes import DataServerRoute
|
10
|
+
from ....dataserver.targets import BytesDataServerTarget
|
11
|
+
from ....dataserver.targets import DataServerTarget
|
12
|
+
from ....dataserver.targets import FileDataServerTarget
|
13
|
+
|
14
|
+
|
15
|
+
##
|
16
|
+
|
17
|
+
|
18
|
+
@dc.dataclass(frozen=True)
|
19
|
+
class CacheServedDockerImageManifest:
|
20
|
+
@dc.dataclass(frozen=True)
|
21
|
+
class Route:
|
22
|
+
paths: ta.Sequence[str]
|
23
|
+
|
24
|
+
content_type: str
|
25
|
+
content_length: int
|
26
|
+
|
27
|
+
@dc.dataclass(frozen=True)
|
28
|
+
class Target(abc.ABC): # noqa
|
29
|
+
pass
|
30
|
+
|
31
|
+
@dc.dataclass(frozen=True)
|
32
|
+
class BytesTarget(Target):
|
33
|
+
data: bytes
|
34
|
+
|
35
|
+
@dc.dataclass(frozen=True)
|
36
|
+
class CacheKeyTarget(Target):
|
37
|
+
key: str
|
38
|
+
|
39
|
+
target: Target
|
40
|
+
|
41
|
+
def __post_init__(self) -> None:
|
42
|
+
check.not_isinstance(self.paths, str)
|
43
|
+
|
44
|
+
routes: ta.Sequence[Route]
|
45
|
+
|
46
|
+
|
47
|
+
#
|
48
|
+
|
49
|
+
|
50
|
+
async def build_cache_served_docker_image_manifest(
|
51
|
+
data_server_routes: ta.Iterable[DataServerRoute],
|
52
|
+
make_file_cache_key: ta.Callable[[str], ta.Awaitable[str]],
|
53
|
+
) -> CacheServedDockerImageManifest:
|
54
|
+
routes: ta.List[CacheServedDockerImageManifest.Route] = []
|
55
|
+
|
56
|
+
for data_server_route in data_server_routes:
|
57
|
+
content_length: int
|
58
|
+
|
59
|
+
data_server_target = data_server_route.target
|
60
|
+
target: CacheServedDockerImageManifest.Route.Target
|
61
|
+
if isinstance(data_server_target, BytesDataServerTarget):
|
62
|
+
bytes_data = check.isinstance(data_server_target.data, bytes)
|
63
|
+
content_length = len(bytes_data)
|
64
|
+
target = CacheServedDockerImageManifest.Route.BytesTarget(bytes_data)
|
65
|
+
|
66
|
+
elif isinstance(data_server_target, FileDataServerTarget):
|
67
|
+
file_path = check.non_empty_str(data_server_target.file_path)
|
68
|
+
content_length = os.path.getsize(file_path)
|
69
|
+
cache_key = await make_file_cache_key(file_path)
|
70
|
+
target = CacheServedDockerImageManifest.Route.CacheKeyTarget(cache_key)
|
71
|
+
|
72
|
+
else:
|
73
|
+
raise TypeError(data_server_target)
|
74
|
+
|
75
|
+
routes.append(CacheServedDockerImageManifest.Route(
|
76
|
+
paths=data_server_route.paths,
|
77
|
+
|
78
|
+
content_type=check.non_empty_str(data_server_target.content_type),
|
79
|
+
content_length=content_length,
|
80
|
+
|
81
|
+
target=target,
|
82
|
+
))
|
83
|
+
|
84
|
+
return CacheServedDockerImageManifest(
|
85
|
+
routes=routes,
|
86
|
+
)
|
87
|
+
|
88
|
+
|
89
|
+
#
|
90
|
+
|
91
|
+
|
92
|
+
async def build_cache_served_docker_image_data_server_routes(
|
93
|
+
manifest: CacheServedDockerImageManifest,
|
94
|
+
make_cache_key_target: ta.Callable[..., ta.Awaitable[DataServerTarget]],
|
95
|
+
) -> ta.List[DataServerRoute]:
|
96
|
+
routes: ta.List[DataServerRoute] = []
|
97
|
+
|
98
|
+
for manifest_route in manifest.routes:
|
99
|
+
manifest_target = manifest_route.target
|
100
|
+
|
101
|
+
target_kwargs: dict = dict(
|
102
|
+
content_type=manifest_route.content_type,
|
103
|
+
content_length=manifest_route.content_length,
|
104
|
+
)
|
105
|
+
|
106
|
+
target: DataServerTarget
|
107
|
+
|
108
|
+
if isinstance(manifest_target, CacheServedDockerImageManifest.Route.BytesTarget):
|
109
|
+
target = DataServerTarget.of(manifest_target.data, **target_kwargs)
|
110
|
+
|
111
|
+
elif isinstance(manifest_target, CacheServedDockerImageManifest.Route.CacheKeyTarget):
|
112
|
+
target = await make_cache_key_target(manifest_target.key, **target_kwargs)
|
113
|
+
|
114
|
+
else:
|
115
|
+
raise TypeError(manifest_target)
|
116
|
+
|
117
|
+
routes.append(DataServerRoute(
|
118
|
+
paths=manifest_route.paths,
|
119
|
+
target=target,
|
120
|
+
))
|
121
|
+
|
122
|
+
return routes
|
omdev/ci/docker/cmds.py
CHANGED
@@ -123,3 +123,21 @@ async def load_docker_tar(
|
|
123
123
|
tar_file: str,
|
124
124
|
) -> str:
|
125
125
|
return await load_docker_tar_cmd(ShellCmd(f'cat {shlex.quote(tar_file)}'))
|
126
|
+
|
127
|
+
|
128
|
+
##
|
129
|
+
|
130
|
+
|
131
|
+
async def ensure_docker_image_setup(
|
132
|
+
image: str,
|
133
|
+
*,
|
134
|
+
cwd: ta.Optional[str] = None,
|
135
|
+
) -> None:
|
136
|
+
await asyncio_subprocesses.check_call(
|
137
|
+
'docker',
|
138
|
+
'run',
|
139
|
+
'--rm',
|
140
|
+
'--entrypoint', '/bin/true', # FIXME: lol
|
141
|
+
image,
|
142
|
+
**(dict(cwd=cwd) if cwd is not None else {}),
|
143
|
+
)
|
omdev/ci/docker/dataserver.py
CHANGED
@@ -7,7 +7,7 @@ import sys
|
|
7
7
|
import threading
|
8
8
|
import typing as ta
|
9
9
|
|
10
|
-
from omlish.docker.
|
10
|
+
from omlish.docker.ports import DockerPortRelay
|
11
11
|
from omlish.http.coro.simple import make_simple_http_server
|
12
12
|
from omlish.http.handlers import HttpHandler
|
13
13
|
from omlish.http.handlers import LoggingHttpHandler
|
@@ -93,6 +93,7 @@ class AsyncioManagedSimpleHttpServer(AsyncExitStacked):
|
|
93
93
|
self._port,
|
94
94
|
self._handler,
|
95
95
|
ssl_context=self._ssl_context(),
|
96
|
+
ignore_ssl_errors=True,
|
96
97
|
use_threads=True,
|
97
98
|
) as server:
|
98
99
|
yield server
|
@@ -163,6 +164,13 @@ class DockerDataServer(AsyncExitStacked):
|
|
163
164
|
return self._stop_event
|
164
165
|
|
165
166
|
async def run(self) -> None:
|
167
|
+
# FIXME:
|
168
|
+
# - shared single server with updatable routes
|
169
|
+
# - get docker used ports with ns1
|
170
|
+
# - discover server port with get_available_port
|
171
|
+
# - discover relay port pair with get_available_ports
|
172
|
+
# relay_port: ta.Optional[ta.Tuple[int, int]] = None
|
173
|
+
|
166
174
|
relay_port: ta.Optional[int] = None
|
167
175
|
if sys.platform == 'darwin':
|
168
176
|
relay_port = self._port
|