docker-squash 1.2.2__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.2/docker_squash.egg-info → docker_squash-1.3.0}/PKG-INFO +14 -4
- {docker_squash-1.2.2 → docker_squash-1.3.0}/README.rst +1 -1
- docker_squash-1.3.0/docker_squash/__init__.py +1 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/cli.py +6 -3
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/image.py +150 -56
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/lib/common.py +4 -1
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/squash.py +26 -17
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/v1_image.py +1 -1
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/v2_image.py +32 -7
- docker_squash-1.3.0/docker_squash/version.py +1 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0/docker_squash.egg-info}/PKG-INFO +14 -4
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash.egg-info/SOURCES.txt +2 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/setup.cfg +3 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/setup.py +3 -3
- {docker_squash-1.2.2 → docker_squash-1.3.0}/tests/test_integ_squash.py +168 -175
- {docker_squash-1.2.2 → 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.2 → docker_squash-1.3.0}/tests/test_unit_v1_image.py +107 -0
- docker_squash-1.2.2/docker_squash/__init__.py +0 -3
- docker_squash-1.2.2/docker_squash/version.py +0 -1
- {docker_squash-1.2.2 → docker_squash-1.3.0}/LICENSE +0 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/MANIFEST.in +0 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/errors.py +0 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/lib/__init__.py +0 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash.egg-info/dependency_links.txt +0 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash.egg-info/entry_points.txt +0 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash.egg-info/requires.txt +0 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash.egg-info/top_level.txt +0 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/requirements.txt +0 -0
- {docker_squash-1.2.2 → docker_squash-1.3.0}/tests/test_unit_squash.py +0 -0
- {docker_squash-1.2.2 → 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,7 +183,7 @@ 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)
|
|
@@ -255,7 +257,7 @@ class Image(object):
|
|
|
255
257
|
% float(((size_before_mb - size_after_mb) / size_before_mb) * 100)
|
|
256
258
|
)
|
|
257
259
|
|
|
258
|
-
def _dir_size(self, directory):
|
|
260
|
+
def _dir_size(self, directory: str) -> int:
|
|
259
261
|
size = 0
|
|
260
262
|
|
|
261
263
|
for path, dirs, files in os.walk(directory):
|
|
@@ -274,8 +276,13 @@ class Image(object):
|
|
|
274
276
|
self._tar_image(target_tar_file, self.new_image_dir)
|
|
275
277
|
self.log.info("Image available at '%s'" % target_tar_file)
|
|
276
278
|
|
|
277
|
-
def load_squashed_image(self):
|
|
278
|
-
|
|
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)
|
|
279
286
|
|
|
280
287
|
if self.tag:
|
|
281
288
|
self.log.info(
|
|
@@ -283,22 +290,24 @@ class Image(object):
|
|
|
283
290
|
% (self.image_name, self.image_tag)
|
|
284
291
|
)
|
|
285
292
|
|
|
286
|
-
|
|
293
|
+
return image_id
|
|
294
|
+
|
|
295
|
+
def _files_in_layers(self, layers: List[str]) -> Dict[str, Set[str]]:
|
|
287
296
|
"""
|
|
288
|
-
Prepare a
|
|
297
|
+
Prepare a set of files for each of the layers
|
|
289
298
|
"""
|
|
290
|
-
files = {}
|
|
299
|
+
files: Dict[str, Set[str]] = {}
|
|
291
300
|
|
|
292
301
|
for layer in layers:
|
|
293
302
|
self.log.debug("Generating list of files in layer '%s'..." % layer)
|
|
294
303
|
tar_file = self._extract_tar_name(layer)
|
|
295
304
|
with tarfile.open(tar_file, "r", format=tarfile.PAX_FORMAT) as tar:
|
|
296
|
-
files[layer] =
|
|
305
|
+
files[layer] = {self._normalize_path(x) for x in tar.getnames()}
|
|
297
306
|
self.log.debug("Done, found %s files" % len(files[layer]))
|
|
298
307
|
|
|
299
308
|
return files
|
|
300
309
|
|
|
301
|
-
def _prepare_tmp_directory(self, tmp_dir: str) -> str:
|
|
310
|
+
def _prepare_tmp_directory(self, tmp_dir: Optional[str]) -> str:
|
|
302
311
|
"""Creates temporary directory that is used to work on layers"""
|
|
303
312
|
|
|
304
313
|
if tmp_dir:
|
|
@@ -314,19 +323,59 @@ class Image(object):
|
|
|
314
323
|
|
|
315
324
|
return tmp_dir
|
|
316
325
|
|
|
317
|
-
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
|
|
318
329
|
tar_file = os.path.join(self.tmp_dir, "image.tar")
|
|
319
330
|
|
|
320
331
|
self._tar_image(tar_file, directory)
|
|
321
332
|
|
|
333
|
+
loaded_id = None
|
|
334
|
+
loaded_ref = None
|
|
335
|
+
|
|
322
336
|
with open(tar_file, "rb") as f:
|
|
323
337
|
self.log.debug("Loading squashed image...")
|
|
324
|
-
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
|
+
|
|
325
361
|
self.log.debug("Image loaded!")
|
|
326
362
|
|
|
327
363
|
os.remove(tar_file)
|
|
328
364
|
|
|
329
|
-
|
|
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:
|
|
330
379
|
with tarfile.open(target_tar_file, "w", format=tarfile.PAX_FORMAT) as tar:
|
|
331
380
|
self.log.debug("Generating tar archive for the squashed image...")
|
|
332
381
|
with Chdir(directory):
|
|
@@ -341,10 +390,12 @@ class Image(object):
|
|
|
341
390
|
tar.add(f)
|
|
342
391
|
self.log.debug("Archive generated")
|
|
343
392
|
|
|
344
|
-
def _layers_to_squash(
|
|
393
|
+
def _layers_to_squash(
|
|
394
|
+
self, layers: List[str], from_layer: Optional[str]
|
|
395
|
+
) -> Tuple[List[str], List[str]]:
|
|
345
396
|
"""Prepares a list of layer IDs that should be squashed"""
|
|
346
|
-
to_squash = []
|
|
347
|
-
to_leave = []
|
|
397
|
+
to_squash: List[str] = []
|
|
398
|
+
to_leave: List[str] = []
|
|
348
399
|
should_squash = True
|
|
349
400
|
|
|
350
401
|
for reversed_layer in reversed(layers):
|
|
@@ -414,7 +465,7 @@ class Image(object):
|
|
|
414
465
|
|
|
415
466
|
raise SquashError(f"Couldn't save {image_id} image!")
|
|
416
467
|
|
|
417
|
-
def _unpack(self, tar_file, directory):
|
|
468
|
+
def _unpack(self, tar_file: str, directory: str) -> None:
|
|
418
469
|
"""Unpacks tar archive to selected directory"""
|
|
419
470
|
|
|
420
471
|
self.log.info("Unpacking %s tar file to %s directory" % (tar_file, directory))
|
|
@@ -445,7 +496,7 @@ class Image(object):
|
|
|
445
496
|
|
|
446
497
|
return (image_name, image_tag)
|
|
447
498
|
|
|
448
|
-
def _dump_json(self, data, new_line=False):
|
|
499
|
+
def _dump_json(self, data: object, new_line: bool = False) -> Tuple[str, str]:
|
|
449
500
|
"""
|
|
450
501
|
Helper function to marshal object into JSON string.
|
|
451
502
|
Additionally a sha256sum of the created JSON string is generated.
|
|
@@ -462,7 +513,13 @@ class Image(object):
|
|
|
462
513
|
|
|
463
514
|
return json_data, sha
|
|
464
515
|
|
|
465
|
-
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:
|
|
466
523
|
if not image_id:
|
|
467
524
|
raise SquashError("Provided image id cannot be null")
|
|
468
525
|
|
|
@@ -472,7 +529,7 @@ class Image(object):
|
|
|
472
529
|
)
|
|
473
530
|
return
|
|
474
531
|
|
|
475
|
-
repos = {}
|
|
532
|
+
repos: Dict[Optional[str], Dict[Optional[str], str]] = {}
|
|
476
533
|
repos[name] = {}
|
|
477
534
|
repos[name][tag] = image_id
|
|
478
535
|
|
|
@@ -482,13 +539,13 @@ class Image(object):
|
|
|
482
539
|
f.write(data)
|
|
483
540
|
f.write("\n")
|
|
484
541
|
|
|
485
|
-
def _write_version_file(self, squashed_dir):
|
|
542
|
+
def _write_version_file(self, squashed_dir: str) -> None:
|
|
486
543
|
version_file = os.path.join(squashed_dir, "VERSION")
|
|
487
544
|
|
|
488
545
|
with open(version_file, "w") as f:
|
|
489
546
|
f.write("1.0")
|
|
490
547
|
|
|
491
|
-
def _write_json_metadata(self, metadata, metadata_file):
|
|
548
|
+
def _write_json_metadata(self, metadata: str, metadata_file: str) -> None:
|
|
492
549
|
with open(metadata_file, "w") as f:
|
|
493
550
|
f.write(metadata)
|
|
494
551
|
|
|
@@ -511,9 +568,15 @@ class Image(object):
|
|
|
511
568
|
layer_id = layer.replace("sha256:", "")
|
|
512
569
|
|
|
513
570
|
self.log.debug("Moving unmodified layer '%s'..." % layer_id)
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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:
|
|
517
580
|
# file_paths is now array of array with files to be skipped.
|
|
518
581
|
# First level are layers, second are files in these layers.
|
|
519
582
|
layer_nb = 1
|
|
@@ -565,7 +628,7 @@ class Image(object):
|
|
|
565
628
|
# Some tar archives do have the filenames prefixed with './'
|
|
566
629
|
# which does not have any effect when we unpack the tar achive,
|
|
567
630
|
# but when processing tar content - we see this.
|
|
568
|
-
tar_files =
|
|
631
|
+
tar_files = {self._normalize_path(x) for x in tar.getnames()}
|
|
569
632
|
|
|
570
633
|
for marker, marker_file in markers.items():
|
|
571
634
|
actual_file = marker.name.replace(".wh.", "")
|
|
@@ -606,13 +669,11 @@ class Image(object):
|
|
|
606
669
|
tar.addfile(tarfile.TarInfo(name=marker.name), marker_file)
|
|
607
670
|
# Add the file name to the list too to avoid re-reading all files
|
|
608
671
|
# in tar archive
|
|
609
|
-
tar_files.
|
|
672
|
+
tar_files.add(normalized_file)
|
|
610
673
|
else:
|
|
611
674
|
self.log.debug("Skipping '%s' marker file..." % marker.name)
|
|
612
675
|
|
|
613
|
-
def _normalize_path(
|
|
614
|
-
self, path: Union[str, pathlib.Path]
|
|
615
|
-
) -> Union[str, pathlib.Path]:
|
|
676
|
+
def _normalize_path(self, path: Union[str, pathlib.Path]) -> str:
|
|
616
677
|
return os.path.normpath(os.path.join("/", path))
|
|
617
678
|
|
|
618
679
|
def _add_hardlinks(self, squashed_tar, squashed_files, to_skip, skipped_hard_links):
|
|
@@ -656,10 +717,17 @@ class Image(object):
|
|
|
656
717
|
% (normalized_name, normalized_linkname)
|
|
657
718
|
)
|
|
658
719
|
|
|
659
|
-
squashed_files.
|
|
720
|
+
squashed_files.add(normalized_name)
|
|
660
721
|
squashed_tar.addfile(member)
|
|
661
722
|
|
|
662
|
-
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:
|
|
663
731
|
normalized_name = self._normalize_path(member.name)
|
|
664
732
|
|
|
665
733
|
if normalized_name in squashed_files:
|
|
@@ -683,10 +751,10 @@ class Image(object):
|
|
|
683
751
|
squashed_tar.addfile(member)
|
|
684
752
|
|
|
685
753
|
# We added a file to the squashed tar, so let's note it
|
|
686
|
-
squashed_files.
|
|
754
|
+
squashed_files.add(normalized_name)
|
|
687
755
|
|
|
688
756
|
def _add_symlinks(self, squashed_tar, squashed_files, to_skip, skipped_sym_links):
|
|
689
|
-
added_symlinks = []
|
|
757
|
+
added_symlinks: List[List[str]] = []
|
|
690
758
|
for layer, symlinks_in_layer in enumerate(skipped_sym_links):
|
|
691
759
|
# We need to start from 1, that's why we bump it here
|
|
692
760
|
current_layer = layer + 1
|
|
@@ -741,12 +809,14 @@ class Image(object):
|
|
|
741
809
|
|
|
742
810
|
added_symlinks.append([normalized_name])
|
|
743
811
|
|
|
744
|
-
squashed_files.
|
|
812
|
+
squashed_files.add(normalized_name)
|
|
745
813
|
squashed_tar.addfile(member)
|
|
746
814
|
|
|
747
815
|
return added_symlinks
|
|
748
816
|
|
|
749
|
-
def _squash_layers(
|
|
817
|
+
def _squash_layers(
|
|
818
|
+
self, layers_to_squash: List[str], layers_to_move: List[str]
|
|
819
|
+
) -> None:
|
|
750
820
|
self.log.info(f"Starting squashing for {self.squashed_tar}...")
|
|
751
821
|
|
|
752
822
|
# Reverse the layers to squash - we begin with the newest one
|
|
@@ -765,9 +835,14 @@ class Image(object):
|
|
|
765
835
|
skipped_sym_links = []
|
|
766
836
|
skipped_files = []
|
|
767
837
|
# List of filenames in the squashed archive
|
|
768
|
-
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()
|
|
769
844
|
# List of opaque directories in the image
|
|
770
|
-
opaque_dirs = []
|
|
845
|
+
opaque_dirs: List[str] = []
|
|
771
846
|
reading_layers: List[tarfile.TarFile] = []
|
|
772
847
|
|
|
773
848
|
for layer_id in layers_to_squash:
|
|
@@ -785,13 +860,13 @@ class Image(object):
|
|
|
785
860
|
members = layer_tar.getmembers()
|
|
786
861
|
markers = self._marker_files(layer_tar, members)
|
|
787
862
|
|
|
788
|
-
skipped_sym_link_files = {}
|
|
789
|
-
skipped_hard_link_files = {}
|
|
863
|
+
skipped_sym_link_files: Dict[str, tarfile.TarInfo] = {}
|
|
864
|
+
skipped_hard_link_files: Dict[str, tarfile.TarInfo] = {}
|
|
790
865
|
skipped_files_in_layer = {}
|
|
791
866
|
|
|
792
|
-
files_to_skip = []
|
|
867
|
+
files_to_skip: List[str] = []
|
|
793
868
|
# List of opaque directories found in this layer
|
|
794
|
-
layer_opaque_dirs = []
|
|
869
|
+
layer_opaque_dirs: List[str] = []
|
|
795
870
|
|
|
796
871
|
# Add it as early as possible, we will be populating
|
|
797
872
|
# 'skipped_sym_link_files' array later
|
|
@@ -876,6 +951,16 @@ class Image(object):
|
|
|
876
951
|
)
|
|
877
952
|
continue
|
|
878
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
|
+
|
|
879
964
|
# Hard links are processed after everything else
|
|
880
965
|
if member.islnk():
|
|
881
966
|
skipped_hard_link_files[normalized_name] = member
|
|
@@ -889,6 +974,8 @@ class Image(object):
|
|
|
889
974
|
self._add_file(
|
|
890
975
|
member, content, squashed_tar, squashed_files, to_skip
|
|
891
976
|
)
|
|
977
|
+
if not member.isdir() and normalized_name in squashed_files:
|
|
978
|
+
non_directories.add(normalized_name)
|
|
892
979
|
|
|
893
980
|
skipped_hard_links.append(skipped_hard_link_files)
|
|
894
981
|
skipped_files.append(skipped_files_in_layer)
|
|
@@ -907,6 +994,13 @@ class Image(object):
|
|
|
907
994
|
member, content, squashed_tar, squashed_files, added_symlinks
|
|
908
995
|
)
|
|
909
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
|
+
|
|
910
1004
|
if files_in_layers_to_move:
|
|
911
1005
|
self._reduce(skipped_markers)
|
|
912
1006
|
|
|
@@ -922,7 +1016,7 @@ class Image(object):
|
|
|
922
1016
|
tar.close()
|
|
923
1017
|
self.log.info("Squashing finished!")
|
|
924
1018
|
|
|
925
|
-
def _is_in_opaque_dir(self, member, dirs):
|
|
1019
|
+
def _is_in_opaque_dir(self, member: tarfile.TarInfo, dirs: List[str]) -> bool:
|
|
926
1020
|
"""
|
|
927
1021
|
If the member we investigate is an opaque directory
|
|
928
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
|
|
|
@@ -5,6 +5,7 @@ from logging import Logger
|
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
7
|
import docker.errors as docker_errors
|
|
8
|
+
from docker.api.client import APIClient
|
|
8
9
|
from packaging import version as packaging_version
|
|
9
10
|
|
|
10
11
|
from docker_squash.errors import SquashError
|
|
@@ -12,7 +13,7 @@ from docker_squash.image import Image
|
|
|
12
13
|
from docker_squash.lib import common
|
|
13
14
|
from docker_squash.v1_image import V1Image
|
|
14
15
|
from docker_squash.v2_image import V2Image
|
|
15
|
-
from docker_squash.version import
|
|
16
|
+
from docker_squash.version import __version__
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class Squash(object):
|
|
@@ -20,7 +21,7 @@ class Squash(object):
|
|
|
20
21
|
self,
|
|
21
22
|
log,
|
|
22
23
|
image,
|
|
23
|
-
docker=None,
|
|
24
|
+
docker: Optional[APIClient] = None,
|
|
24
25
|
from_layer: Optional[str] = None,
|
|
25
26
|
tag: Optional[str] = None,
|
|
26
27
|
comment: Optional[str] = "",
|
|
@@ -30,15 +31,15 @@ class Squash(object):
|
|
|
30
31
|
cleanup: Optional[bool] = False,
|
|
31
32
|
):
|
|
32
33
|
self.log: Logger = log
|
|
33
|
-
self.docker = docker
|
|
34
|
+
self.docker: APIClient = docker if docker else common.docker_client(self.log)
|
|
34
35
|
self.image: str = image
|
|
35
|
-
self.from_layer: str = from_layer
|
|
36
|
-
self.tag: str = tag
|
|
37
|
-
self.comment: str = comment
|
|
38
|
-
self.tmp_dir: str = tmp_dir
|
|
39
|
-
self.output_path: str = output_path
|
|
40
|
-
self.load_image: bool = load_image
|
|
41
|
-
self.cleanup: bool = cleanup
|
|
36
|
+
self.from_layer: Optional[str] = from_layer
|
|
37
|
+
self.tag: Optional[str] = tag
|
|
38
|
+
self.comment: Optional[str] = comment
|
|
39
|
+
self.tmp_dir: Optional[str] = tmp_dir
|
|
40
|
+
self.output_path: Optional[str] = output_path
|
|
41
|
+
self.load_image: Optional[bool] = load_image
|
|
42
|
+
self.cleanup: Optional[bool] = cleanup
|
|
42
43
|
self.development = False
|
|
43
44
|
|
|
44
45
|
if tag == image and cleanup:
|
|
@@ -46,14 +47,12 @@ class Squash(object):
|
|
|
46
47
|
self.cleanup = False
|
|
47
48
|
if tmp_dir:
|
|
48
49
|
self.development = True
|
|
49
|
-
if not docker:
|
|
50
|
-
self.docker = common.docker_client(self.log)
|
|
51
50
|
|
|
52
51
|
def run(self):
|
|
53
52
|
docker_version = self.docker.version()
|
|
54
53
|
self.log.info(
|
|
55
54
|
"docker-squash version %s, Docker %s, API %s..."
|
|
56
|
-
% (
|
|
55
|
+
% (__version__, docker_version["Version"], docker_version["ApiVersion"])
|
|
57
56
|
)
|
|
58
57
|
|
|
59
58
|
if self.image is None:
|
|
@@ -71,10 +70,12 @@ class Squash(object):
|
|
|
71
70
|
% self.output_path
|
|
72
71
|
)
|
|
73
72
|
|
|
73
|
+
image: Image
|
|
74
|
+
|
|
74
75
|
if packaging_version.parse(
|
|
75
76
|
docker_version["ApiVersion"]
|
|
76
77
|
) >= packaging_version.parse("1.22"):
|
|
77
|
-
image
|
|
78
|
+
image = V2Image(
|
|
78
79
|
self.log,
|
|
79
80
|
self.docker,
|
|
80
81
|
self.image,
|
|
@@ -84,7 +85,7 @@ class Squash(object):
|
|
|
84
85
|
self.comment,
|
|
85
86
|
)
|
|
86
87
|
else:
|
|
87
|
-
image
|
|
88
|
+
image = V1Image(
|
|
88
89
|
self.log,
|
|
89
90
|
self.docker,
|
|
90
91
|
self.image,
|
|
@@ -140,8 +141,16 @@ class Squash(object):
|
|
|
140
141
|
image.export_tar_archive(self.output_path)
|
|
141
142
|
|
|
142
143
|
if self.load_image:
|
|
143
|
-
# Load squashed image into Docker
|
|
144
|
-
image
|
|
144
|
+
# Load squashed image into Docker; use the ID assigned by the
|
|
145
|
+
# daemon, which differs from the computed one on some image stores
|
|
146
|
+
loaded_image_id = image.load_squashed_image()
|
|
147
|
+
|
|
148
|
+
if loaded_image_id:
|
|
149
|
+
if loaded_image_id != new_image_id:
|
|
150
|
+
self.log.info(
|
|
151
|
+
"Image ID assigned by the Docker daemon is %s" % loaded_image_id
|
|
152
|
+
)
|
|
153
|
+
new_image_id = loaded_image_id
|
|
145
154
|
|
|
146
155
|
# If development mode is not enabled, make sure we clean up the
|
|
147
156
|
# temporary directory
|
|
@@ -27,7 +27,7 @@ class V1Image(Image):
|
|
|
27
27
|
if self.layers_to_move:
|
|
28
28
|
self.squash_id = self.layers_to_move[-1]
|
|
29
29
|
|
|
30
|
-
def _squash(self):
|
|
30
|
+
def _squash(self) -> str:
|
|
31
31
|
# Prepare the directory
|
|
32
32
|
os.makedirs(self.squashed_dir)
|
|
33
33
|
self._squash_layers(self.layers_to_squash, self.layers_to_move)
|