omdev 0.0.0.dev222__py3-none-any.whl → 0.0.0.dev224__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omdev/ci/cache.py +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)
|