docker-squash 1.2.1__tar.gz → 1.3.0__tar.gz
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.
- {docker_squash-1.2.1/docker_squash.egg-info → docker_squash-1.3.0}/PKG-INFO +14 -4
- {docker_squash-1.2.1 → docker_squash-1.3.0}/README.rst +1 -1
- docker_squash-1.3.0/docker_squash/__init__.py +1 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/cli.py +6 -3
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/image.py +154 -59
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/lib/common.py +4 -1
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/squash.py +32 -20
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/v1_image.py +1 -1
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/v2_image.py +34 -10
- docker_squash-1.3.0/docker_squash/version.py +1 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0/docker_squash.egg-info}/PKG-INFO +14 -4
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash.egg-info/SOURCES.txt +2 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/setup.cfg +3 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/setup.py +3 -3
- {docker_squash-1.2.1 → docker_squash-1.3.0}/tests/test_integ_squash.py +168 -175
- {docker_squash-1.2.1 → docker_squash-1.3.0}/tests/test_qa.py +5 -0
- docker_squash-1.3.0/tests/test_unit_load_image.py +121 -0
- docker_squash-1.3.0/tests/test_unit_release.py +39 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/tests/test_unit_v1_image.py +107 -0
- docker_squash-1.2.1/docker_squash/__init__.py +0 -3
- docker_squash-1.2.1/docker_squash/version.py +0 -1
- {docker_squash-1.2.1 → docker_squash-1.3.0}/LICENSE +0 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/MANIFEST.in +0 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/errors.py +0 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/lib/__init__.py +0 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash.egg-info/dependency_links.txt +0 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash.egg-info/entry_points.txt +0 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash.egg-info/requires.txt +0 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash.egg-info/top_level.txt +0 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/requirements.txt +0 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/tests/test_unit_squash.py +0 -0
- {docker_squash-1.2.1 → docker_squash-1.3.0}/tests/test_unit_v2_image.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: docker-squash
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Docker layer squashing tool
|
|
5
5
|
Home-page: https://github.com/goldmann/docker-squash
|
|
6
|
-
Download-URL: https://github.com/goldmann/docker-squash/archive/1.
|
|
6
|
+
Download-URL: https://github.com/goldmann/docker-squash/archive/1.3.0.tar.gz
|
|
7
7
|
Author: Marek Goldmann
|
|
8
8
|
Author-email: marek.goldmann@gmail.com
|
|
9
9
|
License: MIT
|
|
@@ -11,6 +11,16 @@ Keywords: docker
|
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Requires-Dist: docker
|
|
13
13
|
Requires-Dist: packaging
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: author-email
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: download-url
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: keywords
|
|
20
|
+
Dynamic: license
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: summary
|
|
14
24
|
|
|
15
25
|
``docker-squash``
|
|
16
26
|
==================
|
|
@@ -55,7 +65,7 @@ From PyPi
|
|
|
55
65
|
|
|
56
66
|
$ pip install docker-squash
|
|
57
67
|
|
|
58
|
-
It is supported on Python 3.
|
|
68
|
+
It is supported on Python 3.7 and above.
|
|
59
69
|
|
|
60
70
|
Usage
|
|
61
71
|
-----
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from docker_squash.version import __version__ # noqa: F401
|
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
|
|
7
7
|
from docker_squash import squash
|
|
8
8
|
from docker_squash.errors import SquashError
|
|
9
|
-
from docker_squash.version import
|
|
9
|
+
from docker_squash.version import __version__
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
# Source: http://stackoverflow.com/questions/1383254/logging-streamhandler-and-standard-streams
|
|
@@ -67,7 +67,10 @@ class CLI(object):
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
parser.add_argument(
|
|
70
|
-
"--version",
|
|
70
|
+
"--version",
|
|
71
|
+
action="version",
|
|
72
|
+
help="Show version and exit",
|
|
73
|
+
version=__version__,
|
|
71
74
|
)
|
|
72
75
|
|
|
73
76
|
parser.add_argument("image", help="Image to be squashed")
|
|
@@ -117,7 +120,7 @@ class CLI(object):
|
|
|
117
120
|
else:
|
|
118
121
|
self.log.setLevel(logging.INFO)
|
|
119
122
|
|
|
120
|
-
self.log.debug("Running version %s",
|
|
123
|
+
self.log.debug("Running version %s", __version__)
|
|
121
124
|
try:
|
|
122
125
|
squash.Squash(
|
|
123
126
|
log=self.log,
|
|
@@ -10,7 +10,7 @@ import shutil
|
|
|
10
10
|
import tarfile
|
|
11
11
|
import tempfile
|
|
12
12
|
import threading
|
|
13
|
-
from typing import List, Optional, Union
|
|
13
|
+
from typing import IO, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
14
14
|
|
|
15
15
|
import docker as docker_library
|
|
16
16
|
|
|
@@ -20,10 +20,10 @@ from docker_squash.errors import SquashError, SquashUnnecessaryError
|
|
|
20
20
|
class Chdir(object):
|
|
21
21
|
"""Context manager for changing the current working directory"""
|
|
22
22
|
|
|
23
|
-
def __init__(self, new_path):
|
|
23
|
+
def __init__(self, new_path: str) -> None:
|
|
24
24
|
self.newPath = os.path.expanduser(new_path)
|
|
25
25
|
|
|
26
|
-
def __enter__(self):
|
|
26
|
+
def __enter__(self) -> None:
|
|
27
27
|
self.savedPath = os.getcwd()
|
|
28
28
|
os.chdir(self.newPath)
|
|
29
29
|
|
|
@@ -39,7 +39,7 @@ class Image(object):
|
|
|
39
39
|
This class should not be used directly.
|
|
40
40
|
"""
|
|
41
41
|
|
|
42
|
-
FORMAT = None
|
|
42
|
+
FORMAT: Optional[str] = None
|
|
43
43
|
""" Image format version """
|
|
44
44
|
|
|
45
45
|
def __init__(
|
|
@@ -56,12 +56,12 @@ class Image(object):
|
|
|
56
56
|
self.debug = self.log.isEnabledFor(logging.DEBUG)
|
|
57
57
|
self.docker = docker
|
|
58
58
|
self.image: str = image
|
|
59
|
-
self.from_layer: str = from_layer
|
|
60
|
-
self.tag: str = tag
|
|
61
|
-
self.comment: str = comment
|
|
59
|
+
self.from_layer: Optional[str] = from_layer
|
|
60
|
+
self.tag: Optional[str] = tag
|
|
61
|
+
self.comment: Optional[str] = comment
|
|
62
62
|
self.image_name = None
|
|
63
63
|
self.image_tag = None
|
|
64
|
-
self.squash_id = None
|
|
64
|
+
self.squash_id: Optional[str] = None
|
|
65
65
|
self.oci_format = False
|
|
66
66
|
|
|
67
67
|
# Workaround for https://play.golang.org/p/sCsWMXYxqy
|
|
@@ -76,24 +76,26 @@ class Image(object):
|
|
|
76
76
|
)
|
|
77
77
|
""" Date used in metadata, already formatted using the `%Y-%m-%dT%H:%M:%S.%fZ` format """
|
|
78
78
|
|
|
79
|
-
self.tmp_dir: str = tmp_dir
|
|
79
|
+
self.tmp_dir: Optional[str] = tmp_dir
|
|
80
80
|
""" Main temporary directory to save all working files. This is the root directory for all other temporary files. """
|
|
81
81
|
|
|
82
|
-
def squash(self):
|
|
82
|
+
def squash(self) -> str:
|
|
83
83
|
self._before_squashing()
|
|
84
84
|
ret = self._squash()
|
|
85
85
|
self._after_squashing()
|
|
86
86
|
|
|
87
87
|
return ret
|
|
88
88
|
|
|
89
|
-
def _squash(self):
|
|
90
|
-
|
|
89
|
+
def _squash(self) -> str:
|
|
90
|
+
"""Squashes the image and returns the new image ID"""
|
|
91
|
+
raise NotImplementedError
|
|
91
92
|
|
|
92
93
|
def cleanup(self):
|
|
93
94
|
"""Cleanup the temporary directory"""
|
|
94
95
|
|
|
95
96
|
self.log.debug("Cleaning up %s temporary directory" % self.tmp_dir)
|
|
96
|
-
|
|
97
|
+
if self.tmp_dir:
|
|
98
|
+
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
|
97
99
|
|
|
98
100
|
def _initialize_directories(self):
|
|
99
101
|
# Prepare temporary directory where all the work will be executed
|
|
@@ -171,7 +173,7 @@ class Image(object):
|
|
|
171
173
|
f"Could not get the image ID to squash, please check provided 'image' argument: {self.image}"
|
|
172
174
|
)
|
|
173
175
|
|
|
174
|
-
self.old_image_layers = []
|
|
176
|
+
self.old_image_layers: List[str] = []
|
|
175
177
|
|
|
176
178
|
# Read all layers in the image
|
|
177
179
|
self._read_layers(self.old_image_layers, self.old_image_id)
|
|
@@ -181,16 +183,17 @@ class Image(object):
|
|
|
181
183
|
|
|
182
184
|
# By default - squash all layers.
|
|
183
185
|
if self.from_layer is None:
|
|
184
|
-
self.from_layer = len(self.old_image_layers)
|
|
186
|
+
self.from_layer = str(len(self.old_image_layers))
|
|
185
187
|
|
|
186
188
|
try:
|
|
187
189
|
number_of_layers = int(self.from_layer)
|
|
188
190
|
|
|
189
|
-
self.log.debug(
|
|
191
|
+
self.log.debug(
|
|
192
|
+
f"We detected number of layers ({number_of_layers}) as the argument to squash"
|
|
193
|
+
)
|
|
190
194
|
except ValueError:
|
|
191
|
-
self.log.debug("We detected layer as the argument to squash")
|
|
192
|
-
|
|
193
195
|
squash_id = self._squash_id(self.from_layer)
|
|
196
|
+
self.log.debug(f"We detected layer ({squash_id}) as the argument to squash")
|
|
194
197
|
|
|
195
198
|
if not squash_id:
|
|
196
199
|
raise SquashError(
|
|
@@ -254,7 +257,7 @@ class Image(object):
|
|
|
254
257
|
% float(((size_before_mb - size_after_mb) / size_before_mb) * 100)
|
|
255
258
|
)
|
|
256
259
|
|
|
257
|
-
def _dir_size(self, directory):
|
|
260
|
+
def _dir_size(self, directory: str) -> int:
|
|
258
261
|
size = 0
|
|
259
262
|
|
|
260
263
|
for path, dirs, files in os.walk(directory):
|
|
@@ -273,8 +276,13 @@ class Image(object):
|
|
|
273
276
|
self._tar_image(target_tar_file, self.new_image_dir)
|
|
274
277
|
self.log.info("Image available at '%s'" % target_tar_file)
|
|
275
278
|
|
|
276
|
-
def load_squashed_image(self):
|
|
277
|
-
|
|
279
|
+
def load_squashed_image(self) -> Optional[str]:
|
|
280
|
+
"""
|
|
281
|
+
Loads the squashed image into the daemon and returns the image ID
|
|
282
|
+
assigned by the daemon (without the 'sha256:' prefix), or None if it
|
|
283
|
+
could not be determined.
|
|
284
|
+
"""
|
|
285
|
+
image_id = self._load_image(self.new_image_dir)
|
|
278
286
|
|
|
279
287
|
if self.tag:
|
|
280
288
|
self.log.info(
|
|
@@ -282,22 +290,24 @@ class Image(object):
|
|
|
282
290
|
% (self.image_name, self.image_tag)
|
|
283
291
|
)
|
|
284
292
|
|
|
285
|
-
|
|
293
|
+
return image_id
|
|
294
|
+
|
|
295
|
+
def _files_in_layers(self, layers: List[str]) -> Dict[str, Set[str]]:
|
|
286
296
|
"""
|
|
287
|
-
Prepare a
|
|
297
|
+
Prepare a set of files for each of the layers
|
|
288
298
|
"""
|
|
289
|
-
files = {}
|
|
299
|
+
files: Dict[str, Set[str]] = {}
|
|
290
300
|
|
|
291
301
|
for layer in layers:
|
|
292
302
|
self.log.debug("Generating list of files in layer '%s'..." % layer)
|
|
293
303
|
tar_file = self._extract_tar_name(layer)
|
|
294
304
|
with tarfile.open(tar_file, "r", format=tarfile.PAX_FORMAT) as tar:
|
|
295
|
-
files[layer] =
|
|
305
|
+
files[layer] = {self._normalize_path(x) for x in tar.getnames()}
|
|
296
306
|
self.log.debug("Done, found %s files" % len(files[layer]))
|
|
297
307
|
|
|
298
308
|
return files
|
|
299
309
|
|
|
300
|
-
def _prepare_tmp_directory(self, tmp_dir: str) -> str:
|
|
310
|
+
def _prepare_tmp_directory(self, tmp_dir: Optional[str]) -> str:
|
|
301
311
|
"""Creates temporary directory that is used to work on layers"""
|
|
302
312
|
|
|
303
313
|
if tmp_dir:
|
|
@@ -313,19 +323,59 @@ class Image(object):
|
|
|
313
323
|
|
|
314
324
|
return tmp_dir
|
|
315
325
|
|
|
316
|
-
def _load_image(self, directory):
|
|
326
|
+
def _load_image(self, directory) -> Optional[str]:
|
|
327
|
+
# Set by _initialize_directories() before squashing starts
|
|
328
|
+
assert self.tmp_dir is not None
|
|
317
329
|
tar_file = os.path.join(self.tmp_dir, "image.tar")
|
|
318
330
|
|
|
319
331
|
self._tar_image(tar_file, directory)
|
|
320
332
|
|
|
333
|
+
loaded_id = None
|
|
334
|
+
loaded_ref = None
|
|
335
|
+
|
|
321
336
|
with open(tar_file, "rb") as f:
|
|
322
337
|
self.log.debug("Loading squashed image...")
|
|
323
|
-
self.docker.load_image(f)
|
|
338
|
+
for message in self.docker.load_image(f) or []:
|
|
339
|
+
self.log.debug("Load reply: %s" % message)
|
|
340
|
+
|
|
341
|
+
if not isinstance(message, dict):
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
if message.get("error") or message.get("errorDetail"):
|
|
345
|
+
raise SquashError(
|
|
346
|
+
"Docker daemon failed to load the squashed image: %s"
|
|
347
|
+
% (message.get("error") or message.get("errorDetail"))
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
stream = message.get("stream", "")
|
|
351
|
+
|
|
352
|
+
match = re.search(r"Loaded image ID: (\S+)", stream)
|
|
353
|
+
if match:
|
|
354
|
+
loaded_id = match.group(1)
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
match = re.search(r"Loaded image: (\S+)", stream)
|
|
358
|
+
if match:
|
|
359
|
+
loaded_ref = match.group(1)
|
|
360
|
+
|
|
324
361
|
self.log.debug("Image loaded!")
|
|
325
362
|
|
|
326
363
|
os.remove(tar_file)
|
|
327
364
|
|
|
328
|
-
|
|
365
|
+
if loaded_id is None and loaded_ref is not None:
|
|
366
|
+
try:
|
|
367
|
+
loaded_id = self.docker.inspect_image(loaded_ref)["Id"]
|
|
368
|
+
except Exception as ex:
|
|
369
|
+
self.log.warning(
|
|
370
|
+
"Could not inspect loaded image %s: %s" % (loaded_ref, ex)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if loaded_id is None:
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
return loaded_id.replace("sha256:", "")
|
|
377
|
+
|
|
378
|
+
def _tar_image(self, target_tar_file: str, directory: str) -> None:
|
|
329
379
|
with tarfile.open(target_tar_file, "w", format=tarfile.PAX_FORMAT) as tar:
|
|
330
380
|
self.log.debug("Generating tar archive for the squashed image...")
|
|
331
381
|
with Chdir(directory):
|
|
@@ -340,10 +390,12 @@ class Image(object):
|
|
|
340
390
|
tar.add(f)
|
|
341
391
|
self.log.debug("Archive generated")
|
|
342
392
|
|
|
343
|
-
def _layers_to_squash(
|
|
393
|
+
def _layers_to_squash(
|
|
394
|
+
self, layers: List[str], from_layer: Optional[str]
|
|
395
|
+
) -> Tuple[List[str], List[str]]:
|
|
344
396
|
"""Prepares a list of layer IDs that should be squashed"""
|
|
345
|
-
to_squash = []
|
|
346
|
-
to_leave = []
|
|
397
|
+
to_squash: List[str] = []
|
|
398
|
+
to_leave: List[str] = []
|
|
347
399
|
should_squash = True
|
|
348
400
|
|
|
349
401
|
for reversed_layer in reversed(layers):
|
|
@@ -413,7 +465,7 @@ class Image(object):
|
|
|
413
465
|
|
|
414
466
|
raise SquashError(f"Couldn't save {image_id} image!")
|
|
415
467
|
|
|
416
|
-
def _unpack(self, tar_file, directory):
|
|
468
|
+
def _unpack(self, tar_file: str, directory: str) -> None:
|
|
417
469
|
"""Unpacks tar archive to selected directory"""
|
|
418
470
|
|
|
419
471
|
self.log.info("Unpacking %s tar file to %s directory" % (tar_file, directory))
|
|
@@ -444,7 +496,7 @@ class Image(object):
|
|
|
444
496
|
|
|
445
497
|
return (image_name, image_tag)
|
|
446
498
|
|
|
447
|
-
def _dump_json(self, data, new_line=False):
|
|
499
|
+
def _dump_json(self, data: object, new_line: bool = False) -> Tuple[str, str]:
|
|
448
500
|
"""
|
|
449
501
|
Helper function to marshal object into JSON string.
|
|
450
502
|
Additionally a sha256sum of the created JSON string is generated.
|
|
@@ -461,7 +513,13 @@ class Image(object):
|
|
|
461
513
|
|
|
462
514
|
return json_data, sha
|
|
463
515
|
|
|
464
|
-
def _generate_repositories_json(
|
|
516
|
+
def _generate_repositories_json(
|
|
517
|
+
self,
|
|
518
|
+
repositories_file: str,
|
|
519
|
+
image_id: str,
|
|
520
|
+
name: Optional[str],
|
|
521
|
+
tag: Optional[str],
|
|
522
|
+
) -> None:
|
|
465
523
|
if not image_id:
|
|
466
524
|
raise SquashError("Provided image id cannot be null")
|
|
467
525
|
|
|
@@ -471,7 +529,7 @@ class Image(object):
|
|
|
471
529
|
)
|
|
472
530
|
return
|
|
473
531
|
|
|
474
|
-
repos = {}
|
|
532
|
+
repos: Dict[Optional[str], Dict[Optional[str], str]] = {}
|
|
475
533
|
repos[name] = {}
|
|
476
534
|
repos[name][tag] = image_id
|
|
477
535
|
|
|
@@ -481,13 +539,13 @@ class Image(object):
|
|
|
481
539
|
f.write(data)
|
|
482
540
|
f.write("\n")
|
|
483
541
|
|
|
484
|
-
def _write_version_file(self, squashed_dir):
|
|
542
|
+
def _write_version_file(self, squashed_dir: str) -> None:
|
|
485
543
|
version_file = os.path.join(squashed_dir, "VERSION")
|
|
486
544
|
|
|
487
545
|
with open(version_file, "w") as f:
|
|
488
546
|
f.write("1.0")
|
|
489
547
|
|
|
490
|
-
def _write_json_metadata(self, metadata, metadata_file):
|
|
548
|
+
def _write_json_metadata(self, metadata: str, metadata_file: str) -> None:
|
|
491
549
|
with open(metadata_file, "w") as f:
|
|
492
550
|
f.write(metadata)
|
|
493
551
|
|
|
@@ -510,9 +568,15 @@ class Image(object):
|
|
|
510
568
|
layer_id = layer.replace("sha256:", "")
|
|
511
569
|
|
|
512
570
|
self.log.debug("Moving unmodified layer '%s'..." % layer_id)
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
571
|
+
# Keep the layer's relative path (for example blobs/sha256/<id>
|
|
572
|
+
# in the OCI archive layout), because manifest.json refers to it.
|
|
573
|
+
target = os.path.join(dest, layer_id)
|
|
574
|
+
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
575
|
+
shutil.move(os.path.join(src, layer_id), target)
|
|
576
|
+
|
|
577
|
+
def _file_should_be_skipped(
|
|
578
|
+
self, file_name: str, file_paths: Iterable[Iterable[str]]
|
|
579
|
+
) -> int:
|
|
516
580
|
# file_paths is now array of array with files to be skipped.
|
|
517
581
|
# First level are layers, second are files in these layers.
|
|
518
582
|
layer_nb = 1
|
|
@@ -564,7 +628,7 @@ class Image(object):
|
|
|
564
628
|
# Some tar archives do have the filenames prefixed with './'
|
|
565
629
|
# which does not have any effect when we unpack the tar achive,
|
|
566
630
|
# but when processing tar content - we see this.
|
|
567
|
-
tar_files =
|
|
631
|
+
tar_files = {self._normalize_path(x) for x in tar.getnames()}
|
|
568
632
|
|
|
569
633
|
for marker, marker_file in markers.items():
|
|
570
634
|
actual_file = marker.name.replace(".wh.", "")
|
|
@@ -605,13 +669,11 @@ class Image(object):
|
|
|
605
669
|
tar.addfile(tarfile.TarInfo(name=marker.name), marker_file)
|
|
606
670
|
# Add the file name to the list too to avoid re-reading all files
|
|
607
671
|
# in tar archive
|
|
608
|
-
tar_files.
|
|
672
|
+
tar_files.add(normalized_file)
|
|
609
673
|
else:
|
|
610
674
|
self.log.debug("Skipping '%s' marker file..." % marker.name)
|
|
611
675
|
|
|
612
|
-
def _normalize_path(
|
|
613
|
-
self, path: Union[str, pathlib.Path]
|
|
614
|
-
) -> Union[str, pathlib.Path]:
|
|
676
|
+
def _normalize_path(self, path: Union[str, pathlib.Path]) -> str:
|
|
615
677
|
return os.path.normpath(os.path.join("/", path))
|
|
616
678
|
|
|
617
679
|
def _add_hardlinks(self, squashed_tar, squashed_files, to_skip, skipped_hard_links):
|
|
@@ -655,10 +717,17 @@ class Image(object):
|
|
|
655
717
|
% (normalized_name, normalized_linkname)
|
|
656
718
|
)
|
|
657
719
|
|
|
658
|
-
squashed_files.
|
|
720
|
+
squashed_files.add(normalized_name)
|
|
659
721
|
squashed_tar.addfile(member)
|
|
660
722
|
|
|
661
|
-
def _add_file(
|
|
723
|
+
def _add_file(
|
|
724
|
+
self,
|
|
725
|
+
member: tarfile.TarInfo,
|
|
726
|
+
content: Optional[IO[bytes]],
|
|
727
|
+
squashed_tar: tarfile.TarFile,
|
|
728
|
+
squashed_files: Set[str],
|
|
729
|
+
to_skip,
|
|
730
|
+
) -> None:
|
|
662
731
|
normalized_name = self._normalize_path(member.name)
|
|
663
732
|
|
|
664
733
|
if normalized_name in squashed_files:
|
|
@@ -682,10 +751,10 @@ class Image(object):
|
|
|
682
751
|
squashed_tar.addfile(member)
|
|
683
752
|
|
|
684
753
|
# We added a file to the squashed tar, so let's note it
|
|
685
|
-
squashed_files.
|
|
754
|
+
squashed_files.add(normalized_name)
|
|
686
755
|
|
|
687
756
|
def _add_symlinks(self, squashed_tar, squashed_files, to_skip, skipped_sym_links):
|
|
688
|
-
added_symlinks = []
|
|
757
|
+
added_symlinks: List[List[str]] = []
|
|
689
758
|
for layer, symlinks_in_layer in enumerate(skipped_sym_links):
|
|
690
759
|
# We need to start from 1, that's why we bump it here
|
|
691
760
|
current_layer = layer + 1
|
|
@@ -740,12 +809,14 @@ class Image(object):
|
|
|
740
809
|
|
|
741
810
|
added_symlinks.append([normalized_name])
|
|
742
811
|
|
|
743
|
-
squashed_files.
|
|
812
|
+
squashed_files.add(normalized_name)
|
|
744
813
|
squashed_tar.addfile(member)
|
|
745
814
|
|
|
746
815
|
return added_symlinks
|
|
747
816
|
|
|
748
|
-
def _squash_layers(
|
|
817
|
+
def _squash_layers(
|
|
818
|
+
self, layers_to_squash: List[str], layers_to_move: List[str]
|
|
819
|
+
) -> None:
|
|
749
820
|
self.log.info(f"Starting squashing for {self.squashed_tar}...")
|
|
750
821
|
|
|
751
822
|
# Reverse the layers to squash - we begin with the newest one
|
|
@@ -764,9 +835,14 @@ class Image(object):
|
|
|
764
835
|
skipped_sym_links = []
|
|
765
836
|
skipped_files = []
|
|
766
837
|
# List of filenames in the squashed archive
|
|
767
|
-
squashed_files =
|
|
838
|
+
squashed_files: Set[str] = set()
|
|
839
|
+
# Paths added to the squashed archive that are not directories.
|
|
840
|
+
# Anything below such a path in an older layer is stale, because
|
|
841
|
+
# the path was replaced by a non-directory in a newer layer.
|
|
842
|
+
# https://github.com/goldmann/docker-squash/issues/253
|
|
843
|
+
non_directories: Set[str] = set()
|
|
768
844
|
# List of opaque directories in the image
|
|
769
|
-
opaque_dirs = []
|
|
845
|
+
opaque_dirs: List[str] = []
|
|
770
846
|
reading_layers: List[tarfile.TarFile] = []
|
|
771
847
|
|
|
772
848
|
for layer_id in layers_to_squash:
|
|
@@ -784,13 +860,13 @@ class Image(object):
|
|
|
784
860
|
members = layer_tar.getmembers()
|
|
785
861
|
markers = self._marker_files(layer_tar, members)
|
|
786
862
|
|
|
787
|
-
skipped_sym_link_files = {}
|
|
788
|
-
skipped_hard_link_files = {}
|
|
863
|
+
skipped_sym_link_files: Dict[str, tarfile.TarInfo] = {}
|
|
864
|
+
skipped_hard_link_files: Dict[str, tarfile.TarInfo] = {}
|
|
789
865
|
skipped_files_in_layer = {}
|
|
790
866
|
|
|
791
|
-
files_to_skip = []
|
|
867
|
+
files_to_skip: List[str] = []
|
|
792
868
|
# List of opaque directories found in this layer
|
|
793
|
-
layer_opaque_dirs = []
|
|
869
|
+
layer_opaque_dirs: List[str] = []
|
|
794
870
|
|
|
795
871
|
# Add it as early as possible, we will be populating
|
|
796
872
|
# 'skipped_sym_link_files' array later
|
|
@@ -875,6 +951,16 @@ class Image(object):
|
|
|
875
951
|
)
|
|
876
952
|
continue
|
|
877
953
|
|
|
954
|
+
if non_directories and any(
|
|
955
|
+
parent in non_directories
|
|
956
|
+
for parent in self._path_hierarchy(normalized_name)
|
|
957
|
+
):
|
|
958
|
+
self.log.debug(
|
|
959
|
+
"Skipping '%s' file because its parent was replaced by a non-directory"
|
|
960
|
+
% normalized_name
|
|
961
|
+
)
|
|
962
|
+
continue
|
|
963
|
+
|
|
878
964
|
# Hard links are processed after everything else
|
|
879
965
|
if member.islnk():
|
|
880
966
|
skipped_hard_link_files[normalized_name] = member
|
|
@@ -888,6 +974,8 @@ class Image(object):
|
|
|
888
974
|
self._add_file(
|
|
889
975
|
member, content, squashed_tar, squashed_files, to_skip
|
|
890
976
|
)
|
|
977
|
+
if not member.isdir() and normalized_name in squashed_files:
|
|
978
|
+
non_directories.add(normalized_name)
|
|
891
979
|
|
|
892
980
|
skipped_hard_links.append(skipped_hard_link_files)
|
|
893
981
|
skipped_files.append(skipped_files_in_layer)
|
|
@@ -906,6 +994,13 @@ class Image(object):
|
|
|
906
994
|
member, content, squashed_tar, squashed_files, added_symlinks
|
|
907
995
|
)
|
|
908
996
|
|
|
997
|
+
# Drop markers located below a path replaced by a non-directory
|
|
998
|
+
if non_directories:
|
|
999
|
+
for marker in list(skipped_markers):
|
|
1000
|
+
marked = self._normalize_path(marker.name.replace(".wh.", ""))
|
|
1001
|
+
if any(p in non_directories for p in self._path_hierarchy(marked)):
|
|
1002
|
+
skipped_markers.pop(marker)
|
|
1003
|
+
|
|
909
1004
|
if files_in_layers_to_move:
|
|
910
1005
|
self._reduce(skipped_markers)
|
|
911
1006
|
|
|
@@ -921,7 +1016,7 @@ class Image(object):
|
|
|
921
1016
|
tar.close()
|
|
922
1017
|
self.log.info("Squashing finished!")
|
|
923
1018
|
|
|
924
|
-
def _is_in_opaque_dir(self, member, dirs):
|
|
1019
|
+
def _is_in_opaque_dir(self, member: tarfile.TarInfo, dirs: List[str]) -> bool:
|
|
925
1020
|
"""
|
|
926
1021
|
If the member we investigate is an opaque directory
|
|
927
1022
|
or if the member is located inside of the opaque directory,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from typing import Any, Dict
|
|
4
5
|
|
|
5
6
|
import docker
|
|
7
|
+
import docker.errors
|
|
8
|
+
import docker.utils
|
|
6
9
|
import requests
|
|
7
10
|
|
|
8
11
|
from docker_squash.errors import Error
|
|
@@ -43,7 +46,7 @@ def docker_client(log):
|
|
|
43
46
|
except KeyError:
|
|
44
47
|
pass
|
|
45
48
|
|
|
46
|
-
params = {"version": "auto"}
|
|
49
|
+
params: Dict[str, Any] = {"version": "auto"}
|
|
47
50
|
params.update(docker.utils.kwargs_from_env())
|
|
48
51
|
params["timeout"] = timeout
|
|
49
52
|
|