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.
Files changed (32) hide show
  1. {docker_squash-1.2.2/docker_squash.egg-info → docker_squash-1.3.0}/PKG-INFO +14 -4
  2. {docker_squash-1.2.2 → docker_squash-1.3.0}/README.rst +1 -1
  3. docker_squash-1.3.0/docker_squash/__init__.py +1 -0
  4. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/cli.py +6 -3
  5. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/image.py +150 -56
  6. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/lib/common.py +4 -1
  7. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/squash.py +26 -17
  8. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/v1_image.py +1 -1
  9. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/v2_image.py +32 -7
  10. docker_squash-1.3.0/docker_squash/version.py +1 -0
  11. {docker_squash-1.2.2 → docker_squash-1.3.0/docker_squash.egg-info}/PKG-INFO +14 -4
  12. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash.egg-info/SOURCES.txt +2 -0
  13. {docker_squash-1.2.2 → docker_squash-1.3.0}/setup.cfg +3 -0
  14. {docker_squash-1.2.2 → docker_squash-1.3.0}/setup.py +3 -3
  15. {docker_squash-1.2.2 → docker_squash-1.3.0}/tests/test_integ_squash.py +168 -175
  16. {docker_squash-1.2.2 → docker_squash-1.3.0}/tests/test_qa.py +5 -0
  17. docker_squash-1.3.0/tests/test_unit_load_image.py +121 -0
  18. docker_squash-1.3.0/tests/test_unit_release.py +39 -0
  19. {docker_squash-1.2.2 → docker_squash-1.3.0}/tests/test_unit_v1_image.py +107 -0
  20. docker_squash-1.2.2/docker_squash/__init__.py +0 -3
  21. docker_squash-1.2.2/docker_squash/version.py +0 -1
  22. {docker_squash-1.2.2 → docker_squash-1.3.0}/LICENSE +0 -0
  23. {docker_squash-1.2.2 → docker_squash-1.3.0}/MANIFEST.in +0 -0
  24. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/errors.py +0 -0
  25. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash/lib/__init__.py +0 -0
  26. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash.egg-info/dependency_links.txt +0 -0
  27. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash.egg-info/entry_points.txt +0 -0
  28. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash.egg-info/requires.txt +0 -0
  29. {docker_squash-1.2.2 → docker_squash-1.3.0}/docker_squash.egg-info/top_level.txt +0 -0
  30. {docker_squash-1.2.2 → docker_squash-1.3.0}/requirements.txt +0 -0
  31. {docker_squash-1.2.2 → docker_squash-1.3.0}/tests/test_unit_squash.py +0 -0
  32. {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
1
+ Metadata-Version: 2.4
2
2
  Name: docker-squash
3
- Version: 1.2.2
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.2.2.tar.gz
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.6 and above.
68
+ It is supported on Python 3.7 and above.
59
69
 
60
70
  Usage
61
71
  -----
@@ -41,7 +41,7 @@ From PyPi
41
41
 
42
42
  $ pip install docker-squash
43
43
 
44
- It is supported on Python 3.6 and above.
44
+ It is supported on Python 3.7 and above.
45
45
 
46
46
  Usage
47
47
  -----
@@ -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 version
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", action="version", help="Show version and exit", version=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", version)
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
- pass
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
- shutil.rmtree(self.tmp_dir, ignore_errors=True)
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
- self._load_image(self.new_image_dir)
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
- def _files_in_layers(self, layers):
293
+ return image_id
294
+
295
+ def _files_in_layers(self, layers: List[str]) -> Dict[str, Set[str]]:
287
296
  """
288
- Prepare a list of files in all layers
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] = [self._normalize_path(x) for x in tar.getnames()]
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
- def _tar_image(self, target_tar_file, directory):
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(self, layers, from_layer):
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(self, repositories_file, image_id, name, tag):
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
- shutil.move(os.path.join(src, layer_id), dest)
515
-
516
- def _file_should_be_skipped(self, file_name, file_paths):
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 = [self._normalize_path(x) for x in tar.getnames()]
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.append(normalized_file)
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.append(normalized_name)
720
+ squashed_files.add(normalized_name)
660
721
  squashed_tar.addfile(member)
661
722
 
662
- def _add_file(self, member, content, squashed_tar, squashed_files, to_skip):
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.append(normalized_name)
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.append(normalized_name)
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(self, layers_to_squash: List[str], layers_to_move: List[str]):
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 version
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
- % (version, docker_version["Version"], docker_version["ApiVersion"])
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: Image = V2Image(
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: Image = V1Image(
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.load_squashed_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)