omdev 0.0.0.dev221__py3-none-any.whl → 0.0.0.dev223__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 +40 -23
- omdev/ci/ci.py +49 -109
- 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.py → docker/cmds.py} +1 -44
- omdev/ci/docker/imagepulling.py +64 -0
- omdev/ci/docker/inject.py +37 -0
- omdev/ci/docker/utils.py +48 -0
- omdev/ci/github/cache.py +15 -5
- omdev/ci/github/inject.py +30 -0
- omdev/ci/inject.py +61 -0
- 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 +89 -0
- omdev/oci/__init__.py +0 -0
- omdev/oci/building.py +221 -0
- omdev/oci/compression.py +8 -0
- omdev/oci/data.py +151 -0
- omdev/oci/datarefs.py +138 -0
- omdev/oci/dataserver.py +61 -0
- omdev/oci/loading.py +142 -0
- omdev/oci/media.py +179 -0
- omdev/oci/packing.py +381 -0
- omdev/oci/repositories.py +159 -0
- omdev/oci/tars.py +144 -0
- omdev/pyproject/resources/python.sh +1 -1
- omdev/scripts/ci.py +1841 -384
- omdev/scripts/interp.py +100 -22
- omdev/scripts/pyproject.py +122 -28
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/RECORD +40 -15
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev221.dist-info → omdev-0.0.0.dev223.dist-info}/top_level.txt +0 -0
omdev/ci/cache.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import abc
|
3
|
+
import dataclasses as dc
|
3
4
|
import os.path
|
4
5
|
import shutil
|
5
6
|
import typing as ta
|
@@ -11,24 +12,30 @@ from omlish.lite.logs import log
|
|
11
12
|
from .consts import CI_CACHE_VERSION
|
12
13
|
|
13
14
|
|
15
|
+
CacheVersion = ta.NewType('CacheVersion', int)
|
16
|
+
|
17
|
+
|
14
18
|
##
|
15
19
|
|
16
20
|
|
17
|
-
@abc.abstractmethod
|
18
21
|
class FileCache(abc.ABC):
|
22
|
+
DEFAULT_CACHE_VERSION: ta.ClassVar[CacheVersion] = CacheVersion(CI_CACHE_VERSION)
|
23
|
+
|
19
24
|
def __init__(
|
20
25
|
self,
|
21
26
|
*,
|
22
|
-
version:
|
27
|
+
version: ta.Optional[CacheVersion] = None,
|
23
28
|
) -> None:
|
24
29
|
super().__init__()
|
25
30
|
|
31
|
+
if version is None:
|
32
|
+
version = self.DEFAULT_CACHE_VERSION
|
26
33
|
check.isinstance(version, int)
|
27
34
|
check.arg(version >= 0)
|
28
|
-
self._version = version
|
35
|
+
self._version: CacheVersion = version
|
29
36
|
|
30
37
|
@property
|
31
|
-
def version(self) ->
|
38
|
+
def version(self) -> CacheVersion:
|
32
39
|
return self._version
|
33
40
|
|
34
41
|
#
|
@@ -52,19 +59,28 @@ class FileCache(abc.ABC):
|
|
52
59
|
|
53
60
|
|
54
61
|
class DirectoryFileCache(FileCache):
|
62
|
+
@dc.dataclass(frozen=True)
|
63
|
+
class Config:
|
64
|
+
dir: str
|
65
|
+
|
66
|
+
no_create: bool = False
|
67
|
+
no_purge: bool = False
|
68
|
+
|
55
69
|
def __init__(
|
56
70
|
self,
|
57
|
-
|
71
|
+
config: Config,
|
58
72
|
*,
|
59
|
-
|
60
|
-
no_purge: bool = False,
|
61
|
-
**kwargs: ta.Any,
|
73
|
+
version: ta.Optional[CacheVersion] = None,
|
62
74
|
) -> None: # noqa
|
63
|
-
super().__init__(
|
75
|
+
super().__init__(
|
76
|
+
version=version,
|
77
|
+
)
|
78
|
+
|
79
|
+
self._config = config
|
64
80
|
|
65
|
-
|
66
|
-
|
67
|
-
self.
|
81
|
+
@property
|
82
|
+
def dir(self) -> str:
|
83
|
+
return self._config.dir
|
68
84
|
|
69
85
|
#
|
70
86
|
|
@@ -72,37 +88,38 @@ class DirectoryFileCache(FileCache):
|
|
72
88
|
|
73
89
|
@cached_nullary
|
74
90
|
def setup_dir(self) -> None:
|
75
|
-
version_file = os.path.join(self.
|
91
|
+
version_file = os.path.join(self.dir, self.VERSION_FILE_NAME)
|
76
92
|
|
77
|
-
if self.
|
78
|
-
check.state(os.path.isdir(self.
|
93
|
+
if self._config.no_create:
|
94
|
+
check.state(os.path.isdir(self.dir))
|
79
95
|
|
80
|
-
elif not os.path.isdir(self.
|
81
|
-
os.makedirs(self.
|
96
|
+
elif not os.path.isdir(self.dir):
|
97
|
+
os.makedirs(self.dir)
|
82
98
|
with open(version_file, 'w') as f:
|
83
99
|
f.write(str(self._version))
|
84
100
|
return
|
85
101
|
|
102
|
+
# NOTE: intentionally raises FileNotFoundError to refuse to use an existing non-cache dir as a cache dir.
|
86
103
|
with open(version_file) as f:
|
87
104
|
dir_version = int(f.read().strip())
|
88
105
|
|
89
106
|
if dir_version == self._version:
|
90
107
|
return
|
91
108
|
|
92
|
-
if self.
|
109
|
+
if self._config.no_purge:
|
93
110
|
raise RuntimeError(f'{dir_version=} != {self._version=}')
|
94
111
|
|
95
|
-
dirs = [n for n in sorted(os.listdir(self.
|
112
|
+
dirs = [n for n in sorted(os.listdir(self.dir)) if os.path.isdir(os.path.join(self.dir, n))]
|
96
113
|
if dirs:
|
97
114
|
raise RuntimeError(
|
98
|
-
f'Refusing to remove stale cache dir {self.
|
115
|
+
f'Refusing to remove stale cache dir {self.dir!r} '
|
99
116
|
f'due to present directories: {", ".join(dirs)}',
|
100
117
|
)
|
101
118
|
|
102
|
-
for n in sorted(os.listdir(self.
|
119
|
+
for n in sorted(os.listdir(self.dir)):
|
103
120
|
if n.startswith('.'):
|
104
121
|
continue
|
105
|
-
fp = os.path.join(self.
|
122
|
+
fp = os.path.join(self.dir, n)
|
106
123
|
check.state(os.path.isfile(fp))
|
107
124
|
log.debug('Purging stale cache file: %s', fp)
|
108
125
|
os.unlink(fp)
|
@@ -119,7 +136,7 @@ class DirectoryFileCache(FileCache):
|
|
119
136
|
key: str,
|
120
137
|
) -> str:
|
121
138
|
self.setup_dir()
|
122
|
-
return os.path.join(self.
|
139
|
+
return os.path.join(self.dir, key)
|
123
140
|
|
124
141
|
def format_incomplete_file(self, f: str) -> str:
|
125
142
|
return os.path.join(os.path.dirname(f), f'_{os.path.basename(f)}.incomplete')
|
omdev/ci/ci.py
CHANGED
@@ -9,21 +9,20 @@ from omlish.lite.check import check
|
|
9
9
|
from omlish.lite.contextmanagers import AsyncExitStacked
|
10
10
|
from omlish.os.temp import temp_file_context
|
11
11
|
|
12
|
-
from .cache import FileCache
|
13
12
|
from .compose import DockerComposeRun
|
14
13
|
from .compose import get_compose_service_dependencies
|
15
|
-
from .docker import
|
16
|
-
from .docker import build_docker_image
|
17
|
-
from .docker import
|
18
|
-
from .docker import
|
19
|
-
from .docker import pull_docker_image
|
20
|
-
from .docker import save_docker_tar_cmd
|
21
|
-
from .docker import tag_docker_image
|
14
|
+
from .docker.buildcaching import DockerBuildCaching
|
15
|
+
from .docker.cmds import build_docker_image
|
16
|
+
from .docker.imagepulling import DockerImagePulling
|
17
|
+
from .docker.utils import build_docker_file_hash
|
22
18
|
from .requirements import build_requirements_hash
|
23
19
|
from .shell import ShellCmd
|
24
20
|
from .utils import log_timing_context
|
25
21
|
|
26
22
|
|
23
|
+
##
|
24
|
+
|
25
|
+
|
27
26
|
class Ci(AsyncExitStacked):
|
28
27
|
KEY_HASH_LEN = 16
|
29
28
|
|
@@ -56,104 +55,40 @@ class Ci(AsyncExitStacked):
|
|
56
55
|
|
57
56
|
def __init__(
|
58
57
|
self,
|
59
|
-
|
58
|
+
config: Config,
|
60
59
|
*,
|
61
|
-
|
60
|
+
docker_build_caching: DockerBuildCaching,
|
61
|
+
docker_image_pulling: DockerImagePulling,
|
62
62
|
) -> None:
|
63
63
|
super().__init__()
|
64
64
|
|
65
|
-
self.
|
66
|
-
self._file_cache = file_cache
|
67
|
-
|
68
|
-
#
|
69
|
-
|
70
|
-
async def _load_docker_image(self, image: str) -> None:
|
71
|
-
if not self._cfg.always_pull and (await is_docker_image_present(image)):
|
72
|
-
return
|
73
|
-
|
74
|
-
dep_suffix = image
|
75
|
-
for c in '/:.-_':
|
76
|
-
dep_suffix = dep_suffix.replace(c, '-')
|
77
|
-
|
78
|
-
cache_key = f'docker-{dep_suffix}'
|
79
|
-
if (await self._load_cache_docker_image(cache_key)) is not None:
|
80
|
-
return
|
81
|
-
|
82
|
-
await pull_docker_image(image)
|
83
|
-
|
84
|
-
await self._save_cache_docker_image(cache_key, image)
|
65
|
+
self._config = config
|
85
66
|
|
86
|
-
|
87
|
-
|
88
|
-
await self._load_docker_image(image)
|
89
|
-
|
90
|
-
#
|
91
|
-
|
92
|
-
async def _load_cache_docker_image(self, key: str) -> ta.Optional[str]:
|
93
|
-
if self._file_cache is None:
|
94
|
-
return None
|
95
|
-
|
96
|
-
cache_file = await self._file_cache.get_file(key)
|
97
|
-
if cache_file is None:
|
98
|
-
return None
|
99
|
-
|
100
|
-
get_cache_cmd = ShellCmd(f'cat {cache_file} | zstd -cd --long')
|
101
|
-
|
102
|
-
return await load_docker_tar_cmd(get_cache_cmd)
|
103
|
-
|
104
|
-
async def _save_cache_docker_image(self, key: str, image: str) -> None:
|
105
|
-
if self._file_cache is None:
|
106
|
-
return
|
107
|
-
|
108
|
-
with temp_file_context() as tmp_file:
|
109
|
-
write_tmp_cmd = ShellCmd(f'zstd > {tmp_file}')
|
110
|
-
|
111
|
-
await save_docker_tar_cmd(image, write_tmp_cmd)
|
112
|
-
|
113
|
-
await self._file_cache.put_file(key, tmp_file, steal=True)
|
114
|
-
|
115
|
-
#
|
116
|
-
|
117
|
-
async def _resolve_docker_image(
|
118
|
-
self,
|
119
|
-
cache_key: str,
|
120
|
-
build_and_tag: ta.Callable[[str], ta.Awaitable[str]],
|
121
|
-
) -> str:
|
122
|
-
image_tag = f'{self._cfg.service}:{cache_key}'
|
123
|
-
|
124
|
-
if not self._cfg.always_build and (await is_docker_image_present(image_tag)):
|
125
|
-
return image_tag
|
126
|
-
|
127
|
-
if (cache_image_id := await self._load_cache_docker_image(cache_key)) is not None:
|
128
|
-
await tag_docker_image(
|
129
|
-
cache_image_id,
|
130
|
-
image_tag,
|
131
|
-
)
|
132
|
-
return image_tag
|
133
|
-
|
134
|
-
image_id = await build_and_tag(image_tag)
|
135
|
-
|
136
|
-
await self._save_cache_docker_image(cache_key, image_id)
|
137
|
-
|
138
|
-
return image_tag
|
67
|
+
self._docker_build_caching = docker_build_caching
|
68
|
+
self._docker_image_pulling = docker_image_pulling
|
139
69
|
|
140
70
|
#
|
141
71
|
|
142
72
|
@cached_nullary
|
143
73
|
def docker_file_hash(self) -> str:
|
144
|
-
return build_docker_file_hash(self.
|
74
|
+
return build_docker_file_hash(self._config.docker_file)[:self.KEY_HASH_LEN]
|
75
|
+
|
76
|
+
@cached_nullary
|
77
|
+
def ci_base_image_cache_key(self) -> str:
|
78
|
+
return f'ci-base-{self.docker_file_hash()}'
|
145
79
|
|
146
80
|
async def _resolve_ci_base_image(self) -> str:
|
147
81
|
async def build_and_tag(image_tag: str) -> str:
|
148
82
|
return await build_docker_image(
|
149
|
-
self.
|
83
|
+
self._config.docker_file,
|
150
84
|
tag=image_tag,
|
151
|
-
cwd=self.
|
85
|
+
cwd=self._config.project_dir,
|
152
86
|
)
|
153
87
|
|
154
|
-
|
155
|
-
|
156
|
-
|
88
|
+
return await self._docker_build_caching.cached_build_docker_image(
|
89
|
+
self.ci_base_image_cache_key(),
|
90
|
+
build_and_tag,
|
91
|
+
)
|
157
92
|
|
158
93
|
@async_cached_nullary
|
159
94
|
async def resolve_ci_base_image(self) -> str:
|
@@ -167,14 +102,18 @@ class Ci(AsyncExitStacked):
|
|
167
102
|
@cached_nullary
|
168
103
|
def requirements_txts(self) -> ta.Sequence[str]:
|
169
104
|
return [
|
170
|
-
os.path.join(self.
|
171
|
-
for rf in check.not_none(self.
|
105
|
+
os.path.join(self._config.project_dir, rf)
|
106
|
+
for rf in check.not_none(self._config.requirements_txts)
|
172
107
|
]
|
173
108
|
|
174
109
|
@cached_nullary
|
175
110
|
def requirements_hash(self) -> str:
|
176
111
|
return build_requirements_hash(self.requirements_txts())[:self.KEY_HASH_LEN]
|
177
112
|
|
113
|
+
@cached_nullary
|
114
|
+
def ci_image_cache_key(self) -> str:
|
115
|
+
return f'ci-{self.docker_file_hash()}-{self.requirements_hash()}'
|
116
|
+
|
178
117
|
async def _resolve_ci_image(self) -> str:
|
179
118
|
async def build_and_tag(image_tag: str) -> str:
|
180
119
|
base_image = await self.resolve_ci_base_image()
|
@@ -191,7 +130,7 @@ class Ci(AsyncExitStacked):
|
|
191
130
|
'--no-cache',
|
192
131
|
'--index-strategy unsafe-best-match',
|
193
132
|
'--system',
|
194
|
-
*[f'-r /project/{rf}' for rf in self.
|
133
|
+
*[f'-r /project/{rf}' for rf in self._config.requirements_txts or []],
|
195
134
|
]),
|
196
135
|
]
|
197
136
|
setup_cmd = ' && '.join(setup_cmds)
|
@@ -199,7 +138,7 @@ class Ci(AsyncExitStacked):
|
|
199
138
|
docker_file_lines = [
|
200
139
|
f'FROM {base_image}',
|
201
140
|
'RUN mkdir /project',
|
202
|
-
*[f'COPY {rf} /project/{rf}' for rf in self.
|
141
|
+
*[f'COPY {rf} /project/{rf}' for rf in self._config.requirements_txts or []],
|
203
142
|
f'RUN {setup_cmd}',
|
204
143
|
'RUN rm /project/*',
|
205
144
|
'WORKDIR /project',
|
@@ -212,12 +151,13 @@ class Ci(AsyncExitStacked):
|
|
212
151
|
return await build_docker_image(
|
213
152
|
docker_file,
|
214
153
|
tag=image_tag,
|
215
|
-
cwd=self.
|
154
|
+
cwd=self._config.project_dir,
|
216
155
|
)
|
217
156
|
|
218
|
-
|
219
|
-
|
220
|
-
|
157
|
+
return await self._docker_build_caching.cached_build_docker_image(
|
158
|
+
self.ci_image_cache_key(),
|
159
|
+
build_and_tag,
|
160
|
+
)
|
221
161
|
|
222
162
|
@async_cached_nullary
|
223
163
|
async def resolve_ci_image(self) -> str:
|
@@ -229,34 +169,34 @@ class Ci(AsyncExitStacked):
|
|
229
169
|
#
|
230
170
|
|
231
171
|
@async_cached_nullary
|
232
|
-
async def
|
172
|
+
async def pull_dependencies(self) -> None:
|
233
173
|
deps = get_compose_service_dependencies(
|
234
|
-
self.
|
235
|
-
self.
|
174
|
+
self._config.compose_file,
|
175
|
+
self._config.service,
|
236
176
|
)
|
237
177
|
|
238
178
|
for dep_image in deps.values():
|
239
|
-
await self.
|
179
|
+
await self._docker_image_pulling.pull_docker_image(dep_image)
|
240
180
|
|
241
181
|
#
|
242
182
|
|
243
183
|
async def _run_compose_(self) -> None:
|
244
184
|
async with DockerComposeRun(DockerComposeRun.Config(
|
245
|
-
compose_file=self.
|
246
|
-
service=self.
|
185
|
+
compose_file=self._config.compose_file,
|
186
|
+
service=self._config.service,
|
247
187
|
|
248
188
|
image=await self.resolve_ci_image(),
|
249
189
|
|
250
|
-
cmd=self.
|
190
|
+
cmd=self._config.cmd,
|
251
191
|
|
252
192
|
run_options=[
|
253
|
-
'-v', f'{os.path.abspath(self.
|
254
|
-
*(self.
|
193
|
+
'-v', f'{os.path.abspath(self._config.project_dir)}:/project',
|
194
|
+
*(self._config.run_options or []),
|
255
195
|
],
|
256
196
|
|
257
|
-
cwd=self.
|
197
|
+
cwd=self._config.project_dir,
|
258
198
|
|
259
|
-
no_dependencies=self.
|
199
|
+
no_dependencies=self._config.no_dependencies,
|
260
200
|
)) as ci_compose_run:
|
261
201
|
await ci_compose_run.run()
|
262
202
|
|
@@ -269,6 +209,6 @@ class Ci(AsyncExitStacked):
|
|
269
209
|
async def run(self) -> None:
|
270
210
|
await self.resolve_ci_image()
|
271
211
|
|
272
|
-
await self.
|
212
|
+
await self.pull_dependencies()
|
273
213
|
|
274
214
|
await self._run_compose()
|
omdev/ci/cli.py
CHANGED
@@ -21,16 +21,15 @@ from omlish.argparse.cli import ArgparseCli
|
|
21
21
|
from omlish.argparse.cli import argparse_arg
|
22
22
|
from omlish.argparse.cli import argparse_cmd
|
23
23
|
from omlish.lite.check import check
|
24
|
+
from omlish.lite.inject import inj
|
24
25
|
from omlish.lite.logs import log
|
25
26
|
from omlish.logs.standard import configure_standard_logging
|
26
27
|
|
27
|
-
from .cache import DirectoryFileCache
|
28
|
-
from .cache import FileCache
|
29
28
|
from .ci import Ci
|
30
29
|
from .compose import get_compose_service_dependencies
|
31
30
|
from .github.bootstrap import is_in_github_actions
|
32
|
-
from .github.cache import GithubFileCache
|
33
31
|
from .github.cli import GithubCli
|
32
|
+
from .inject import bind_ci
|
34
33
|
from .requirements import build_requirements_hash
|
35
34
|
from .shell import ShellCmd
|
36
35
|
|
@@ -165,14 +164,9 @@ class CiCli(ArgparseCli):
|
|
165
164
|
|
166
165
|
#
|
167
166
|
|
168
|
-
file_cache: ta.Optional[FileCache] = None
|
169
167
|
if cache_dir is not None:
|
170
168
|
cache_dir = os.path.abspath(cache_dir)
|
171
169
|
log.debug('Using cache dir %s', cache_dir)
|
172
|
-
if github:
|
173
|
-
file_cache = GithubFileCache(cache_dir)
|
174
|
-
else:
|
175
|
-
file_cache = DirectoryFileCache(cache_dir)
|
176
170
|
|
177
171
|
#
|
178
172
|
|
@@ -188,28 +182,35 @@ class CiCli(ArgparseCli):
|
|
188
182
|
|
189
183
|
#
|
190
184
|
|
191
|
-
|
192
|
-
|
193
|
-
|
185
|
+
config = Ci.Config(
|
186
|
+
project_dir=project_dir,
|
187
|
+
|
188
|
+
docker_file=docker_file,
|
189
|
+
|
190
|
+
compose_file=compose_file,
|
191
|
+
service=self.args.service,
|
194
192
|
|
195
|
-
|
193
|
+
requirements_txts=requirements_txts,
|
196
194
|
|
197
|
-
|
198
|
-
service=self.args.service,
|
195
|
+
cmd=ShellCmd(cmd),
|
199
196
|
|
200
|
-
|
197
|
+
always_pull=self.args.always_pull,
|
198
|
+
always_build=self.args.always_build,
|
201
199
|
|
202
|
-
|
200
|
+
no_dependencies=self.args.no_dependencies,
|
203
201
|
|
204
|
-
|
205
|
-
|
202
|
+
run_options=run_options,
|
203
|
+
)
|
206
204
|
|
207
|
-
|
205
|
+
injector = inj.create_injector(bind_ci(
|
206
|
+
config=config,
|
207
|
+
|
208
|
+
github=github,
|
209
|
+
|
210
|
+
cache_dir=cache_dir,
|
211
|
+
))
|
208
212
|
|
209
|
-
|
210
|
-
),
|
211
|
-
file_cache=file_cache,
|
212
|
-
) as ci:
|
213
|
+
async with injector[Ci] as ci:
|
213
214
|
await ci.run()
|
214
215
|
|
215
216
|
|
File without changes
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import dataclasses as dc
|
4
|
+
import typing as ta
|
5
|
+
|
6
|
+
from .cache import DockerCache
|
7
|
+
from .cmds import is_docker_image_present
|
8
|
+
from .cmds import tag_docker_image
|
9
|
+
|
10
|
+
|
11
|
+
##
|
12
|
+
|
13
|
+
|
14
|
+
class DockerBuildCaching(abc.ABC):
|
15
|
+
@abc.abstractmethod
|
16
|
+
def cached_build_docker_image(
|
17
|
+
self,
|
18
|
+
cache_key: str,
|
19
|
+
build_and_tag: ta.Callable[[str], ta.Awaitable[str]], # image_tag -> image_id
|
20
|
+
) -> ta.Awaitable[str]:
|
21
|
+
raise NotImplementedError
|
22
|
+
|
23
|
+
|
24
|
+
class DockerBuildCachingImpl(DockerBuildCaching):
|
25
|
+
@dc.dataclass(frozen=True)
|
26
|
+
class Config:
|
27
|
+
service: str
|
28
|
+
|
29
|
+
always_build: bool = False
|
30
|
+
|
31
|
+
def __init__(
|
32
|
+
self,
|
33
|
+
*,
|
34
|
+
config: Config,
|
35
|
+
|
36
|
+
docker_cache: ta.Optional[DockerCache] = None,
|
37
|
+
) -> None:
|
38
|
+
super().__init__()
|
39
|
+
|
40
|
+
self._config = config
|
41
|
+
|
42
|
+
self._docker_cache = docker_cache
|
43
|
+
|
44
|
+
async def cached_build_docker_image(
|
45
|
+
self,
|
46
|
+
cache_key: str,
|
47
|
+
build_and_tag: ta.Callable[[str], ta.Awaitable[str]],
|
48
|
+
) -> str:
|
49
|
+
image_tag = f'{self._config.service}:{cache_key}'
|
50
|
+
|
51
|
+
if not self._config.always_build and (await is_docker_image_present(image_tag)):
|
52
|
+
return image_tag
|
53
|
+
|
54
|
+
if (
|
55
|
+
self._docker_cache is not None and
|
56
|
+
(cache_image_id := await self._docker_cache.load_cache_docker_image(cache_key)) is not None
|
57
|
+
):
|
58
|
+
await tag_docker_image(
|
59
|
+
cache_image_id,
|
60
|
+
image_tag,
|
61
|
+
)
|
62
|
+
return image_tag
|
63
|
+
|
64
|
+
image_id = await build_and_tag(image_tag)
|
65
|
+
|
66
|
+
if self._docker_cache is not None:
|
67
|
+
await self._docker_cache.save_cache_docker_image(cache_key, image_id)
|
68
|
+
|
69
|
+
return image_tag
|
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)
|
@@ -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
|
##
|