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.
Files changed (32) hide show
  1. {docker_squash-1.2.1/docker_squash.egg-info → docker_squash-1.3.0}/PKG-INFO +14 -4
  2. {docker_squash-1.2.1 → 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.1 → docker_squash-1.3.0}/docker_squash/cli.py +6 -3
  5. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/image.py +154 -59
  6. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/lib/common.py +4 -1
  7. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/squash.py +32 -20
  8. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/v1_image.py +1 -1
  9. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/v2_image.py +34 -10
  10. docker_squash-1.3.0/docker_squash/version.py +1 -0
  11. {docker_squash-1.2.1 → docker_squash-1.3.0/docker_squash.egg-info}/PKG-INFO +14 -4
  12. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash.egg-info/SOURCES.txt +2 -0
  13. {docker_squash-1.2.1 → docker_squash-1.3.0}/setup.cfg +3 -0
  14. {docker_squash-1.2.1 → docker_squash-1.3.0}/setup.py +3 -3
  15. {docker_squash-1.2.1 → docker_squash-1.3.0}/tests/test_integ_squash.py +168 -175
  16. {docker_squash-1.2.1 → 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.1 → docker_squash-1.3.0}/tests/test_unit_v1_image.py +107 -0
  20. docker_squash-1.2.1/docker_squash/__init__.py +0 -3
  21. docker_squash-1.2.1/docker_squash/version.py +0 -1
  22. {docker_squash-1.2.1 → docker_squash-1.3.0}/LICENSE +0 -0
  23. {docker_squash-1.2.1 → docker_squash-1.3.0}/MANIFEST.in +0 -0
  24. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/errors.py +0 -0
  25. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash/lib/__init__.py +0 -0
  26. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash.egg-info/dependency_links.txt +0 -0
  27. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash.egg-info/entry_points.txt +0 -0
  28. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash.egg-info/requires.txt +0 -0
  29. {docker_squash-1.2.1 → docker_squash-1.3.0}/docker_squash.egg-info/top_level.txt +0 -0
  30. {docker_squash-1.2.1 → docker_squash-1.3.0}/requirements.txt +0 -0
  31. {docker_squash-1.2.1 → docker_squash-1.3.0}/tests/test_unit_squash.py +0 -0
  32. {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
1
+ Metadata-Version: 2.4
2
2
  Name: docker-squash
3
- Version: 1.2.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.2.1.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,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("We detected number of layers as the argument to squash")
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
- 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)
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
- def _files_in_layers(self, layers):
293
+ return image_id
294
+
295
+ def _files_in_layers(self, layers: List[str]) -> Dict[str, Set[str]]:
286
296
  """
287
- Prepare a list of files in all layers
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] = [self._normalize_path(x) for x in tar.getnames()]
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
- 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:
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(self, layers, from_layer):
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(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:
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
- shutil.move(os.path.join(src, layer_id), dest)
514
-
515
- 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:
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 = [self._normalize_path(x) for x in tar.getnames()]
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.append(normalized_file)
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.append(normalized_name)
720
+ squashed_files.add(normalized_name)
659
721
  squashed_tar.addfile(member)
660
722
 
661
- 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:
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.append(normalized_name)
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.append(normalized_name)
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(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:
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