omdev 0.0.0.dev222__py3-none-any.whl → 0.0.0.dev224__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- omdev/ci/cache.py +148 -23
- omdev/ci/ci.py +50 -110
- omdev/ci/cli.py +24 -23
- omdev/ci/docker/__init__.py +0 -0
- omdev/ci/docker/buildcaching.py +69 -0
- omdev/ci/docker/cache.py +57 -0
- omdev/ci/docker/cacheserved.py +262 -0
- omdev/ci/{docker.py → docker/cmds.py} +1 -44
- omdev/ci/docker/dataserver.py +204 -0
- omdev/ci/docker/imagepulling.py +65 -0
- omdev/ci/docker/inject.py +37 -0
- omdev/ci/docker/packing.py +72 -0
- omdev/ci/docker/repositories.py +40 -0
- omdev/ci/docker/utils.py +48 -0
- omdev/ci/github/cache.py +35 -6
- omdev/ci/github/client.py +9 -2
- omdev/ci/github/inject.py +30 -0
- omdev/ci/inject.py +61 -0
- omdev/ci/utils.py +0 -49
- omdev/dataserver/__init__.py +1 -0
- omdev/dataserver/handlers.py +198 -0
- omdev/dataserver/http.py +69 -0
- omdev/dataserver/routes.py +49 -0
- omdev/dataserver/server.py +90 -0
- omdev/dataserver/targets.py +121 -0
- omdev/oci/building.py +107 -9
- omdev/oci/compression.py +8 -0
- omdev/oci/data.py +43 -0
- omdev/oci/datarefs.py +90 -50
- omdev/oci/dataserver.py +64 -0
- omdev/oci/loading.py +20 -0
- omdev/oci/media.py +20 -0
- omdev/oci/pack/__init__.py +0 -0
- omdev/oci/pack/packing.py +185 -0
- omdev/oci/pack/repositories.py +162 -0
- omdev/oci/pack/unpacking.py +204 -0
- omdev/oci/repositories.py +84 -2
- omdev/oci/tars.py +144 -0
- omdev/pyproject/resources/python.sh +1 -1
- omdev/scripts/ci.py +2137 -512
- omdev/scripts/interp.py +119 -22
- omdev/scripts/pyproject.py +141 -28
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/RECORD +48 -23
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/top_level.txt +0 -0
omdev/oci/dataserver.py
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
from omlish.lite.check import check
|
5
|
+
|
6
|
+
from ..dataserver.routes import DataServerRoute
|
7
|
+
from ..dataserver.targets import DataServerTarget
|
8
|
+
from .building import BuiltOciImageIndexRepository
|
9
|
+
from .building import OciRepositoryBuilder
|
10
|
+
from .datarefs import BytesOciDataRef
|
11
|
+
from .datarefs import FileOciDataRef
|
12
|
+
from .datarefs import open_oci_data_ref
|
13
|
+
from .media import OCI_MANIFEST_MEDIA_TYPES
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
|
18
|
+
|
19
|
+
def build_oci_repository_data_server_routes(
|
20
|
+
repo_name: str,
|
21
|
+
built_repo: BuiltOciImageIndexRepository,
|
22
|
+
) -> ta.List[DataServerRoute]:
|
23
|
+
base_url_path = f'/v2/{repo_name}'
|
24
|
+
|
25
|
+
repo_contents: ta.Dict[str, OciRepositoryBuilder.Blob] = {}
|
26
|
+
|
27
|
+
repo_contents[f'{base_url_path}/manifests/latest'] = built_repo.blobs[built_repo.media_index_descriptor.digest]
|
28
|
+
|
29
|
+
for blob in built_repo.blobs.values():
|
30
|
+
repo_contents['/'.join([
|
31
|
+
base_url_path,
|
32
|
+
'manifests' if blob.media_type in OCI_MANIFEST_MEDIA_TYPES else 'blobs',
|
33
|
+
blob.digest,
|
34
|
+
])] = blob
|
35
|
+
|
36
|
+
#
|
37
|
+
|
38
|
+
def build_blob_target(blob: OciRepositoryBuilder.Blob) -> ta.Optional[DataServerTarget]: # noqa
|
39
|
+
kw: dict = dict(
|
40
|
+
content_type=check.non_empty_str(blob.media_type),
|
41
|
+
)
|
42
|
+
|
43
|
+
if isinstance(blob.data, BytesOciDataRef):
|
44
|
+
return DataServerTarget.of(blob.data.data, **kw)
|
45
|
+
|
46
|
+
elif isinstance(blob.data, FileOciDataRef):
|
47
|
+
return DataServerTarget.of(file_path=blob.data.path, **kw)
|
48
|
+
|
49
|
+
else:
|
50
|
+
with open_oci_data_ref(blob.data) as f:
|
51
|
+
data = f.read()
|
52
|
+
|
53
|
+
return DataServerTarget.of(data, **kw)
|
54
|
+
|
55
|
+
#
|
56
|
+
|
57
|
+
return [
|
58
|
+
DataServerRoute(
|
59
|
+
paths=[path],
|
60
|
+
target=target,
|
61
|
+
)
|
62
|
+
for path, blob in repo_contents.items()
|
63
|
+
if (target := build_blob_target(blob)) is not None
|
64
|
+
]
|
omdev/oci/loading.py
CHANGED
@@ -18,6 +18,7 @@ from .media import OciMediaImageConfig
|
|
18
18
|
from .media import OciMediaImageIndex
|
19
19
|
from .media import OciMediaImageManifest
|
20
20
|
from .media import unmarshal_oci_media_dataclass
|
21
|
+
from .repositories import FileOciRepository
|
21
22
|
from .repositories import OciRepository
|
22
23
|
|
23
24
|
|
@@ -120,3 +121,22 @@ class OciRepositoryLoader:
|
|
120
121
|
|
121
122
|
else:
|
122
123
|
raise TypeError(obj)
|
124
|
+
|
125
|
+
|
126
|
+
##
|
127
|
+
|
128
|
+
|
129
|
+
def read_oci_repository_root_index(
|
130
|
+
obj: ta.Any,
|
131
|
+
*,
|
132
|
+
file_name: str = 'index.json',
|
133
|
+
) -> OciImageIndex:
|
134
|
+
file_repo = check.isinstance(OciRepository.of(obj), FileOciRepository)
|
135
|
+
|
136
|
+
repo_ldr = OciRepositoryLoader(file_repo)
|
137
|
+
|
138
|
+
media_image_idx = repo_ldr.load_object(file_repo.read_file(file_name), OciMediaImageIndex)
|
139
|
+
|
140
|
+
image_idx = repo_ldr.from_media(media_image_idx)
|
141
|
+
|
142
|
+
return check.isinstance(image_idx, OciImageIndex)
|
omdev/oci/media.py
CHANGED
@@ -25,8 +25,19 @@ OCI_MEDIA_FIELDS: ta.Collection[str] = frozenset([
|
|
25
25
|
@dc.dataclass()
|
26
26
|
class OciMediaDataclass(abc.ABC): # noqa
|
27
27
|
SCHEMA_VERSION: ta.ClassVar[int]
|
28
|
+
|
29
|
+
@property
|
30
|
+
def schema_version(self) -> int:
|
31
|
+
raise TypeError
|
32
|
+
|
28
33
|
MEDIA_TYPE: ta.ClassVar[str]
|
29
34
|
|
35
|
+
@property
|
36
|
+
def media_type(self) -> str:
|
37
|
+
raise TypeError
|
38
|
+
|
39
|
+
#
|
40
|
+
|
30
41
|
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
31
42
|
super().__init_subclass__(**kwargs)
|
32
43
|
for a in OCI_MEDIA_FIELDS:
|
@@ -157,3 +168,12 @@ class OciMediaImageConfig(OciImageConfig, OciMediaDataclass):
|
|
157
168
|
|
158
169
|
MEDIA_TYPE: ta.ClassVar[str] = 'application/vnd.oci.image.config.v1+json'
|
159
170
|
media_type: str = dc.field(default=MEDIA_TYPE, metadata={OBJ_MARSHALER_FIELD_KEY: 'mediaType'})
|
171
|
+
|
172
|
+
|
173
|
+
##
|
174
|
+
|
175
|
+
|
176
|
+
OCI_MANIFEST_MEDIA_TYPES: ta.AbstractSet[str] = frozenset([
|
177
|
+
OciMediaImageIndex.MEDIA_TYPE,
|
178
|
+
OciMediaImageManifest.MEDIA_TYPE,
|
179
|
+
])
|
File without changes
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import heapq
|
4
|
+
import tarfile
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
from omlish.lite.cached import cached_nullary
|
8
|
+
from omlish.lite.check import check
|
9
|
+
from omlish.lite.contextmanagers import ExitStacked
|
10
|
+
|
11
|
+
from ..compression import OciCompression
|
12
|
+
from ..tars import OciDataTarWriter
|
13
|
+
from ..tars import WrittenOciDataTarFileInfo
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
|
18
|
+
|
19
|
+
class OciLayerPacker(ExitStacked):
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
input_file_path: str,
|
23
|
+
output_file_paths: ta.Sequence[str],
|
24
|
+
*,
|
25
|
+
compression: ta.Optional[OciCompression] = None,
|
26
|
+
) -> None:
|
27
|
+
super().__init__()
|
28
|
+
|
29
|
+
self._input_file_path = input_file_path
|
30
|
+
self._output_file_paths = list(output_file_paths)
|
31
|
+
self._compression = compression
|
32
|
+
|
33
|
+
self._output_file_indexes_by_name: ta.Dict[str, int] = {}
|
34
|
+
|
35
|
+
#
|
36
|
+
|
37
|
+
@cached_nullary
|
38
|
+
def _input_tar_file(self) -> tarfile.TarFile:
|
39
|
+
# FIXME: check uncompressed
|
40
|
+
return self._enter_context(tarfile.open(self._input_file_path))
|
41
|
+
|
42
|
+
#
|
43
|
+
|
44
|
+
@cached_nullary
|
45
|
+
def _entries_by_name(self) -> ta.Mapping[str, tarfile.TarInfo]:
|
46
|
+
return {
|
47
|
+
info.name: info
|
48
|
+
for info in self._input_tar_file().getmembers()
|
49
|
+
}
|
50
|
+
|
51
|
+
#
|
52
|
+
|
53
|
+
class _CategorizedEntries(ta.NamedTuple):
|
54
|
+
files_by_name: ta.Mapping[str, tarfile.TarInfo]
|
55
|
+
non_files_by_name: ta.Mapping[str, tarfile.TarInfo]
|
56
|
+
links_by_name: ta.Mapping[str, tarfile.TarInfo]
|
57
|
+
|
58
|
+
@cached_nullary
|
59
|
+
def _categorized_entries(self) -> _CategorizedEntries:
|
60
|
+
files_by_name: ta.Dict[str, tarfile.TarInfo] = {}
|
61
|
+
non_files_by_name: ta.Dict[str, tarfile.TarInfo] = {}
|
62
|
+
links_by_name: ta.Dict[str, tarfile.TarInfo] = {}
|
63
|
+
|
64
|
+
for name, info in self._entries_by_name().items():
|
65
|
+
if info.type in tarfile.REGULAR_TYPES:
|
66
|
+
files_by_name[name] = info
|
67
|
+
elif info.type in (tarfile.LNKTYPE, tarfile.GNUTYPE_LONGLINK):
|
68
|
+
links_by_name[name] = info
|
69
|
+
else:
|
70
|
+
non_files_by_name[name] = info
|
71
|
+
|
72
|
+
return self._CategorizedEntries(
|
73
|
+
files_by_name=files_by_name,
|
74
|
+
non_files_by_name=non_files_by_name,
|
75
|
+
links_by_name=links_by_name,
|
76
|
+
)
|
77
|
+
|
78
|
+
#
|
79
|
+
|
80
|
+
@cached_nullary
|
81
|
+
def _non_files_sorted_by_name(self) -> ta.Sequence[tarfile.TarInfo]:
|
82
|
+
return sorted(
|
83
|
+
self._categorized_entries().non_files_by_name.values(),
|
84
|
+
key=lambda info: info.name,
|
85
|
+
)
|
86
|
+
|
87
|
+
@cached_nullary
|
88
|
+
def _files_descending_by_size(self) -> ta.Sequence[tarfile.TarInfo]:
|
89
|
+
return sorted(
|
90
|
+
self._categorized_entries().files_by_name.values(),
|
91
|
+
key=lambda info: -check.isinstance(info.size, int),
|
92
|
+
)
|
93
|
+
|
94
|
+
#
|
95
|
+
|
96
|
+
@cached_nullary
|
97
|
+
def _output_files(self) -> ta.Sequence[ta.BinaryIO]:
|
98
|
+
return [
|
99
|
+
self._enter_context(open(output_file_path, 'wb'))
|
100
|
+
for output_file_path in self._output_file_paths
|
101
|
+
]
|
102
|
+
|
103
|
+
@cached_nullary
|
104
|
+
def _output_tar_writers(self) -> ta.Sequence[OciDataTarWriter]:
|
105
|
+
return [
|
106
|
+
self._enter_context(
|
107
|
+
OciDataTarWriter(
|
108
|
+
output_file,
|
109
|
+
compression=self._compression,
|
110
|
+
),
|
111
|
+
)
|
112
|
+
for output_file in self._output_files()
|
113
|
+
]
|
114
|
+
|
115
|
+
#
|
116
|
+
|
117
|
+
def _write_entry(
|
118
|
+
self,
|
119
|
+
info: tarfile.TarInfo,
|
120
|
+
output_file_idx: int,
|
121
|
+
) -> None:
|
122
|
+
check.not_in(info.name, self._output_file_indexes_by_name)
|
123
|
+
|
124
|
+
writer = self._output_tar_writers()[output_file_idx]
|
125
|
+
|
126
|
+
if info.type in tarfile.REGULAR_TYPES:
|
127
|
+
with check.not_none(self._input_tar_file().extractfile(info)) as f:
|
128
|
+
writer.add_file(info, f) # type: ignore
|
129
|
+
|
130
|
+
else:
|
131
|
+
writer.add_file(info)
|
132
|
+
|
133
|
+
self._output_file_indexes_by_name[info.name] = output_file_idx
|
134
|
+
|
135
|
+
@cached_nullary
|
136
|
+
def _write_non_files(self) -> None:
|
137
|
+
for non_file in self._non_files_sorted_by_name():
|
138
|
+
self._write_entry(non_file, 0)
|
139
|
+
|
140
|
+
@cached_nullary
|
141
|
+
def _write_files(self) -> None:
|
142
|
+
writers = self._output_tar_writers()
|
143
|
+
|
144
|
+
bins = [
|
145
|
+
(writer.info().compressed_sz, i)
|
146
|
+
for i, writer in enumerate(writers)
|
147
|
+
]
|
148
|
+
|
149
|
+
heapq.heapify(bins)
|
150
|
+
|
151
|
+
for file in self._files_descending_by_size():
|
152
|
+
_, bin_index = heapq.heappop(bins)
|
153
|
+
|
154
|
+
writer = writers[bin_index]
|
155
|
+
|
156
|
+
self._write_entry(file, bin_index)
|
157
|
+
|
158
|
+
bin_size = writer.info().compressed_sz
|
159
|
+
|
160
|
+
heapq.heappush(bins, (bin_size, bin_index))
|
161
|
+
|
162
|
+
@cached_nullary
|
163
|
+
def _write_links(self) -> None:
|
164
|
+
for link in self._categorized_entries().links_by_name.values():
|
165
|
+
link_name = check.non_empty_str(link.linkname)
|
166
|
+
|
167
|
+
output_file_idx = self._output_file_indexes_by_name[link_name]
|
168
|
+
|
169
|
+
self._write_entry(link, output_file_idx)
|
170
|
+
|
171
|
+
@cached_nullary
|
172
|
+
def write(self) -> ta.Mapping[str, WrittenOciDataTarFileInfo]:
|
173
|
+
writers = self._output_tar_writers()
|
174
|
+
|
175
|
+
self._write_non_files()
|
176
|
+
self._write_files()
|
177
|
+
self._write_links()
|
178
|
+
|
179
|
+
for output_tar_writer in writers:
|
180
|
+
output_tar_writer.tar_file().close()
|
181
|
+
|
182
|
+
return {
|
183
|
+
output_file_path: output_tar_writer.info()
|
184
|
+
for output_file_path, output_tar_writer in zip(self._output_file_paths, writers)
|
185
|
+
}
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import copy
|
4
|
+
import os.path
|
5
|
+
import shutil
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from omlish.lite.cached import cached_nullary
|
9
|
+
from omlish.lite.check import check
|
10
|
+
from omlish.lite.contextmanagers import ExitStacked
|
11
|
+
from omlish.lite.timing import log_timing_context
|
12
|
+
from omlish.os.temp import temp_dir_context
|
13
|
+
|
14
|
+
from ...oci.tars import WrittenOciDataTarFileInfo
|
15
|
+
from ..building import BuiltOciImageIndexRepository
|
16
|
+
from ..building import build_oci_index_repository
|
17
|
+
from ..compression import OciCompression
|
18
|
+
from ..data import OciImageIndex
|
19
|
+
from ..data import OciImageLayer
|
20
|
+
from ..data import OciImageManifest
|
21
|
+
from ..data import get_single_leaf_oci_image_index
|
22
|
+
from ..data import get_single_oci_image_manifest
|
23
|
+
from ..datarefs import FileOciDataRef
|
24
|
+
from ..datarefs import open_oci_data_ref
|
25
|
+
from ..loading import read_oci_repository_root_index
|
26
|
+
from ..repositories import OciRepository
|
27
|
+
from .packing import OciLayerPacker
|
28
|
+
from .unpacking import OciLayerUnpacker
|
29
|
+
|
30
|
+
|
31
|
+
##
|
32
|
+
|
33
|
+
|
34
|
+
class OciPackedRepositoryBuilder(ExitStacked):
|
35
|
+
def __init__(
|
36
|
+
self,
|
37
|
+
source_repo: OciRepository,
|
38
|
+
*,
|
39
|
+
temp_dir: ta.Optional[str] = None,
|
40
|
+
|
41
|
+
num_packed_files: int = 3, # GH actions have this set to 3, the default
|
42
|
+
packed_compression: ta.Optional[OciCompression] = OciCompression.ZSTD,
|
43
|
+
) -> None:
|
44
|
+
super().__init__()
|
45
|
+
|
46
|
+
self._source_repo = source_repo
|
47
|
+
|
48
|
+
self._given_temp_dir = temp_dir
|
49
|
+
|
50
|
+
check.arg(num_packed_files > 0)
|
51
|
+
self._num_packed_files = num_packed_files
|
52
|
+
|
53
|
+
self._packed_compression = packed_compression
|
54
|
+
|
55
|
+
@cached_nullary
|
56
|
+
def _temp_dir(self) -> str:
|
57
|
+
if (given := self._given_temp_dir) is not None:
|
58
|
+
return given
|
59
|
+
else:
|
60
|
+
return self._enter_context(temp_dir_context()) # noqa
|
61
|
+
|
62
|
+
#
|
63
|
+
|
64
|
+
@cached_nullary
|
65
|
+
def _source_image_index(self) -> OciImageIndex:
|
66
|
+
image_index = read_oci_repository_root_index(self._source_repo)
|
67
|
+
return get_single_leaf_oci_image_index(image_index)
|
68
|
+
|
69
|
+
@cached_nullary
|
70
|
+
def _source_image_manifest(self) -> OciImageManifest:
|
71
|
+
return get_single_oci_image_manifest(self._source_image_index())
|
72
|
+
|
73
|
+
#
|
74
|
+
|
75
|
+
@cached_nullary
|
76
|
+
def _extracted_layer_tar_files(self) -> ta.List[str]:
|
77
|
+
image = self._source_image_manifest()
|
78
|
+
|
79
|
+
layer_tar_files = []
|
80
|
+
|
81
|
+
for i, layer in enumerate(image.layers):
|
82
|
+
if isinstance(layer.data, FileOciDataRef):
|
83
|
+
input_file_path = layer.data.path
|
84
|
+
|
85
|
+
else:
|
86
|
+
input_file_path = os.path.join(self._temp_dir(), f'save-layer-{i}.tar')
|
87
|
+
with open(input_file_path, 'wb') as input_file: # noqa
|
88
|
+
with open_oci_data_ref(layer.data) as layer_file:
|
89
|
+
shutil.copyfileobj(layer_file, input_file, length=1024 * 1024) # noqa
|
90
|
+
|
91
|
+
layer_tar_files.append(input_file_path)
|
92
|
+
|
93
|
+
return layer_tar_files
|
94
|
+
|
95
|
+
#
|
96
|
+
|
97
|
+
@cached_nullary
|
98
|
+
def _unpacked_tar_file(self) -> str:
|
99
|
+
layer_tar_files = self._extracted_layer_tar_files()
|
100
|
+
unpacked_file = os.path.join(self._temp_dir(), 'unpacked.tar')
|
101
|
+
|
102
|
+
with log_timing_context(f'Unpacking docker image {self._source_repo}'):
|
103
|
+
with OciLayerUnpacker(
|
104
|
+
layer_tar_files,
|
105
|
+
unpacked_file,
|
106
|
+
) as lu:
|
107
|
+
lu.write()
|
108
|
+
|
109
|
+
return unpacked_file
|
110
|
+
|
111
|
+
#
|
112
|
+
|
113
|
+
@cached_nullary
|
114
|
+
def _packed_tar_files(self) -> ta.Mapping[str, WrittenOciDataTarFileInfo]:
|
115
|
+
unpacked_tar_file = self._unpacked_tar_file()
|
116
|
+
|
117
|
+
packed_tar_files = [
|
118
|
+
os.path.join(self._temp_dir(), f'packed-{i}.tar')
|
119
|
+
for i in range(self._num_packed_files)
|
120
|
+
]
|
121
|
+
|
122
|
+
with log_timing_context(f'Packing docker image {self._source_repo}'):
|
123
|
+
with OciLayerPacker(
|
124
|
+
unpacked_tar_file,
|
125
|
+
packed_tar_files,
|
126
|
+
compression=self._packed_compression,
|
127
|
+
) as lp:
|
128
|
+
return lp.write()
|
129
|
+
|
130
|
+
#
|
131
|
+
|
132
|
+
@cached_nullary
|
133
|
+
def _packed_image_index(self) -> OciImageIndex:
|
134
|
+
image_index = copy.deepcopy(self._source_image_index())
|
135
|
+
|
136
|
+
image = get_single_oci_image_manifest(image_index)
|
137
|
+
|
138
|
+
image.config.history = None
|
139
|
+
|
140
|
+
written = self._packed_tar_files()
|
141
|
+
|
142
|
+
# FIXME: use prebuilt sha256
|
143
|
+
image.layers = [
|
144
|
+
OciImageLayer(
|
145
|
+
kind=OciImageLayer.Kind.from_compression(self._packed_compression),
|
146
|
+
data=FileOciDataRef(output_file),
|
147
|
+
)
|
148
|
+
for output_file, output_file_info in written.items()
|
149
|
+
]
|
150
|
+
|
151
|
+
image.config.rootfs.diff_ids = [
|
152
|
+
f'sha256:{output_file_info.tar_sha256}'
|
153
|
+
for output_file_info in written.values()
|
154
|
+
]
|
155
|
+
|
156
|
+
return image_index
|
157
|
+
|
158
|
+
#
|
159
|
+
|
160
|
+
@cached_nullary
|
161
|
+
def build(self) -> BuiltOciImageIndexRepository:
|
162
|
+
return build_oci_index_repository(self._packed_image_index())
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import contextlib
|
4
|
+
import os.path
|
5
|
+
import tarfile
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from omlish.lite.cached import cached_nullary
|
9
|
+
from omlish.lite.check import check
|
10
|
+
from omlish.lite.contextmanagers import ExitStacked
|
11
|
+
|
12
|
+
|
13
|
+
##
|
14
|
+
|
15
|
+
|
16
|
+
class OciLayerUnpacker(ExitStacked):
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
input_files: ta.Sequence[ta.Union[str, tarfile.TarFile]],
|
20
|
+
output_file_path: str,
|
21
|
+
) -> None:
|
22
|
+
super().__init__()
|
23
|
+
|
24
|
+
self._input_files = list(input_files)
|
25
|
+
self._output_file_path = output_file_path
|
26
|
+
|
27
|
+
#
|
28
|
+
|
29
|
+
@contextlib.contextmanager
|
30
|
+
def _open_input_file(self, input_file: ta.Union[str, tarfile.TarFile]) -> ta.Iterator[tarfile.TarFile]:
|
31
|
+
if isinstance(input_file, tarfile.TarFile):
|
32
|
+
yield input_file
|
33
|
+
|
34
|
+
elif isinstance(input_file, str):
|
35
|
+
with tarfile.open(input_file) as tar_file:
|
36
|
+
yield tar_file
|
37
|
+
|
38
|
+
else:
|
39
|
+
raise TypeError(input_file)
|
40
|
+
|
41
|
+
#
|
42
|
+
|
43
|
+
class _Entry(ta.NamedTuple):
|
44
|
+
file: ta.Union[str, tarfile.TarFile]
|
45
|
+
info: tarfile.TarInfo
|
46
|
+
|
47
|
+
def _build_input_file_sorted_entries(self, input_file: ta.Union[str, tarfile.TarFile]) -> ta.Sequence[_Entry]:
|
48
|
+
dct: ta.Dict[str, OciLayerUnpacker._Entry] = {}
|
49
|
+
|
50
|
+
with self._open_input_file(input_file) as input_tar_file:
|
51
|
+
for info in input_tar_file.getmembers():
|
52
|
+
check.not_in(info.name, dct)
|
53
|
+
dct[info.name] = self._Entry(
|
54
|
+
file=input_file,
|
55
|
+
info=info,
|
56
|
+
)
|
57
|
+
|
58
|
+
return sorted(dct.values(), key=lambda entry: entry.info.name)
|
59
|
+
|
60
|
+
@cached_nullary
|
61
|
+
def _entries_by_name(self) -> ta.Mapping[str, _Entry]:
|
62
|
+
root: dict = {}
|
63
|
+
|
64
|
+
def find_dir(dir_name: str) -> dict: # noqa
|
65
|
+
if dir_name:
|
66
|
+
dir_parts = dir_name.split('/')
|
67
|
+
else:
|
68
|
+
dir_parts = []
|
69
|
+
|
70
|
+
cur = root # noqa
|
71
|
+
for dir_part in dir_parts:
|
72
|
+
cur = cur[dir_part] # noqa
|
73
|
+
|
74
|
+
return check.isinstance(cur, dict)
|
75
|
+
|
76
|
+
#
|
77
|
+
|
78
|
+
for input_file in self._input_files:
|
79
|
+
sorted_entries = self._build_input_file_sorted_entries(input_file)
|
80
|
+
|
81
|
+
wh_names = set()
|
82
|
+
wh_opaques = set()
|
83
|
+
|
84
|
+
#
|
85
|
+
|
86
|
+
for entry in sorted_entries:
|
87
|
+
info = entry.info
|
88
|
+
name = check.non_empty_str(info.name)
|
89
|
+
base_name = os.path.basename(name)
|
90
|
+
dir_name = os.path.dirname(name)
|
91
|
+
|
92
|
+
if base_name == '.wh..wh..opq':
|
93
|
+
wh_opaques.add(dir_name)
|
94
|
+
continue
|
95
|
+
|
96
|
+
if base_name.startswith('.wh.'):
|
97
|
+
wh_base_name = os.path.basename(base_name[4:])
|
98
|
+
wh_name = os.path.join(dir_name, wh_base_name)
|
99
|
+
wh_names.add(wh_name)
|
100
|
+
continue
|
101
|
+
|
102
|
+
cur = find_dir(dir_name)
|
103
|
+
|
104
|
+
if info.type == tarfile.DIRTYPE:
|
105
|
+
try:
|
106
|
+
ex = cur[base_name]
|
107
|
+
except KeyError:
|
108
|
+
cur[base_name] = {'': entry}
|
109
|
+
else:
|
110
|
+
ex[''] = entry
|
111
|
+
|
112
|
+
else:
|
113
|
+
cur[base_name] = entry
|
114
|
+
|
115
|
+
#
|
116
|
+
|
117
|
+
for wh_name in reversed(sorted(wh_names)): # noqa
|
118
|
+
wh_dir_name = os.path.dirname(wh_name)
|
119
|
+
wh_base_name = os.path.basename(wh_name)
|
120
|
+
|
121
|
+
cur = find_dir(wh_dir_name)
|
122
|
+
rm = cur[wh_base_name]
|
123
|
+
|
124
|
+
if isinstance(rm, dict):
|
125
|
+
# Whiteouts wipe out whole directory:
|
126
|
+
# https://github.com/containerd/containerd/blob/59c8cf6ea5f4175ad512914dd5ce554942bf144f/pkg/archive/tar_test.go#L648
|
127
|
+
# check.equal(set(rm), '')
|
128
|
+
del cur[wh_base_name]
|
129
|
+
|
130
|
+
elif isinstance(rm, self._Entry):
|
131
|
+
del cur[wh_base_name]
|
132
|
+
|
133
|
+
else:
|
134
|
+
raise TypeError(rm)
|
135
|
+
|
136
|
+
if wh_opaques:
|
137
|
+
raise NotImplementedError
|
138
|
+
|
139
|
+
#
|
140
|
+
|
141
|
+
out: ta.Dict[str, OciLayerUnpacker._Entry] = {}
|
142
|
+
|
143
|
+
def rec(cur): # noqa
|
144
|
+
for _, child in sorted(cur.items(), key=lambda t: t[0]):
|
145
|
+
if isinstance(child, dict):
|
146
|
+
rec(child)
|
147
|
+
|
148
|
+
elif isinstance(child, self._Entry):
|
149
|
+
check.not_in(child.info.name, out)
|
150
|
+
out[child.info.name] = child
|
151
|
+
|
152
|
+
else:
|
153
|
+
raise TypeError(child)
|
154
|
+
|
155
|
+
rec(root)
|
156
|
+
|
157
|
+
return out
|
158
|
+
|
159
|
+
#
|
160
|
+
|
161
|
+
@cached_nullary
|
162
|
+
def _output_tar_file(self) -> tarfile.TarFile:
|
163
|
+
return self._enter_context(tarfile.open(self._output_file_path, 'w'))
|
164
|
+
|
165
|
+
#
|
166
|
+
|
167
|
+
def _add_unpacked_entry(
|
168
|
+
self,
|
169
|
+
input_tar_file: tarfile.TarFile,
|
170
|
+
info: tarfile.TarInfo,
|
171
|
+
) -> None:
|
172
|
+
base_name = os.path.basename(info.name)
|
173
|
+
check.state(not base_name.startswith('.wh.'))
|
174
|
+
|
175
|
+
if info.type in tarfile.REGULAR_TYPES:
|
176
|
+
with check.not_none(input_tar_file.extractfile(info)) as f:
|
177
|
+
self._output_tar_file().addfile(info, f)
|
178
|
+
|
179
|
+
else:
|
180
|
+
self._output_tar_file().addfile(info)
|
181
|
+
|
182
|
+
def _unpack_file(
|
183
|
+
self,
|
184
|
+
input_file: ta.Union[str, tarfile.TarFile],
|
185
|
+
) -> None:
|
186
|
+
entries_by_name = self._entries_by_name()
|
187
|
+
|
188
|
+
with self._open_input_file(input_file) as input_tar_file:
|
189
|
+
info: tarfile.TarInfo
|
190
|
+
for info in input_tar_file.getmembers():
|
191
|
+
try:
|
192
|
+
entry = entries_by_name[info.name]
|
193
|
+
except KeyError:
|
194
|
+
continue
|
195
|
+
|
196
|
+
if entry.file != input_file:
|
197
|
+
continue
|
198
|
+
|
199
|
+
self._add_unpacked_entry(input_tar_file, info)
|
200
|
+
|
201
|
+
@cached_nullary
|
202
|
+
def write(self) -> None:
|
203
|
+
for input_file in self._input_files:
|
204
|
+
self._unpack_file(input_file)
|