99import importlib .resources
1010import json
1111import re
12- import tarfile
13- import tempfile
1412from os import PathLike
1513from pathlib import Path
16- from typing import Optional
14+
15+ from ...constants import (
16+ PODMAN_FS_CHANGE_ADDED ,
17+ PODMAN_FS_CHANGE_DELETED ,
18+ PODMAN_FS_CHANGE_MODIFIED ,
19+ )
20+ from ...oci import Image , Podman , PodmanContext
1721
1822
1923class Comparator (object ):
@@ -29,120 +33,27 @@ class Comparator(object):
2933 Apache License, Version 2.0
3034 """
3135
32- _default_whitelist : list [str ] = []
33-
34- _nightly_whitelist = json .loads (
35- importlib .resources .read_text (__name__ , "nightly_whitelist.json" )
36- )
37-
38- def __init__ (
39- self , nightly : bool = False , whitelist : list [str ] = _default_whitelist
40- ):
36+ def __init__ (self , nightly : bool = False , whitelist : list [str ] = []):
4137 """
4238 Constructor __init__(Comparator)
4339
4440 :param nightly: Flag indicating if the nightlywhitelist should be used
4541 :param whitelst: Additional whitelist
4642
47- :since: 1.0.0
48- """
49- self .whitelist = whitelist
50- if nightly :
51- self .whitelist += self ._nightly_whitelist
52-
53- @staticmethod
54- def _unpack (file : PathLike [str ]) -> tempfile .TemporaryDirectory [str ]:
55- """
56- Unpack a .tar archive or .oci image into a temporary dictionary
57-
58- :param file: .tar or .oci file
59-
60- :return: TemporaryDirectory Temporary directory containing the unpacked file
6143 :since: 1.0.0
6244 """
6345
64- output_dir = tempfile .TemporaryDirectory ()
65- file = Path (file ).resolve ()
66- if file .name .endswith (".oci" ):
67- with tempfile .TemporaryDirectory () as extracted :
68- # Extract .oci file
69- with tarfile .open (file , "r" ) as tar :
70- tar .extractall (
71- path = extracted , filter = "fully_trusted" , members = tar .getmembers ()
72- )
73-
74- layers_dir = Path (extracted ).joinpath ("blobs/sha256" )
75- assert layers_dir .is_dir ()
76-
77- with open (Path (extracted ).joinpath ("index.json" ), "r" ) as f :
78- index = json .load (f )
79-
80- # Only support first manifest
81- manifest = index ["manifests" ][0 ]["digest" ].split (":" )[1 ]
82-
83- with open (layers_dir .joinpath (manifest ), "r" ) as f :
84- manifest = json .load (f )
85-
86- layers = [layer ["digest" ].split (":" )[1 ] for layer in manifest ["layers" ]]
87-
88- # Extract layers in order
89- for layer in layers :
90- layer_path = layers_dir .joinpath (layer )
91- if tarfile .is_tarfile (layer_path ):
92- with tarfile .open (layer_path , "r" ) as tar :
93- for member in tar .getmembers ():
94- try :
95- tar .extract (
96- member ,
97- path = output_dir .name ,
98- filter = "fully_trusted" ,
99- )
100- except tarfile .AbsoluteLinkError :
101- # Convert absolute link to relative link
102- member .linkpath = (
103- "../" * member .path .count ("/" )
104- + member .linkpath [1 :]
105- )
106- tar .extract (
107- member ,
108- path = output_dir .name ,
109- filter = "fully_trusted" ,
110- )
111- except tarfile .TarError as e :
112- print (f"Skipping { member .name } due to error: { e } " )
113- else :
114- with tarfile .open (file , "r" ) as tar :
115- tar .extractall (
116- path = output_dir .name ,
117- filter = "fully_trusted" ,
118- members = tar .getmembers (),
119- )
120-
121- return output_dir
122-
123- def _diff_files (
124- self , cmp : filecmp .dircmp [str ], left_root : Optional [Path ] = None
125- ) -> list [str ]:
126- """
127- Recursively compare files
128-
129- :param cmp: Dircmp to recursively compare
130- :param left_root: Left root to obtain the archive relative path
131-
132- :return: list[Path] List of paths with different content
133- :since: 1.0.0
134- """
135-
136- result = []
137- if not left_root :
138- left_root = Path (cmp .left )
139- for name in cmp .diff_files :
140- result .append (f"/{ Path (cmp .left ).relative_to (left_root ).joinpath (name )} " )
141- for sub_cmp in cmp .subdirs .values ():
142- result += self ._diff_files (sub_cmp , left_root = left_root )
143- return result
46+ self .whitelist = whitelist
14447
145- def generate (self , a : PathLike [str ], b : PathLike [str ]) -> tuple [list [str ], bool ]:
48+ if nightly :
49+ self .whitelist += json .loads (
50+ importlib .resources .read_text (__name__ , "nightly_whitelist.json" )
51+ )
52+
53+ @PodmanContext .wrap
54+ def generate (
55+ self , a : PathLike [str ], b : PathLike [str ], podman : PodmanContext
56+ ) -> tuple [list [str ], bool ]:
14657 """
14758 Compare two .tar/.oci images with each other
14859
@@ -156,16 +67,51 @@ def generate(self, a: PathLike[str], b: PathLike[str]) -> tuple[list[str], bool]
15667 if filecmp .cmp (a , b , shallow = False ):
15768 return [], False
15869
159- with self ._unpack (a ) as unpacked_a , self ._unpack (b ) as unpacked_b :
160- cmp = filecmp .dircmp (unpacked_a , unpacked_b , shallow = False )
161-
162- diff_files = self ._diff_files (cmp )
163-
164- filtered = [
165- file
166- for file in diff_files
167- if not any (re .match (pattern , file ) for pattern in self .whitelist )
168- ]
169- whitelist = len (diff_files ) != len (filtered )
170-
171- return filtered , whitelist
70+ a = Path (a )
71+ a_image_id = None
72+
73+ b = Path (b )
74+ b_image_id = None
75+
76+ differences = []
77+ podman_api = Podman ()
78+
79+ try :
80+ if a .suffix == ".oci" :
81+ a_image_id = podman_api .load_oci_archive (a , podman = podman )
82+ elif a .suffix == ".tar" :
83+ a_image_id = Image .import_plain_tar (a , podman = podman )
84+ else :
85+ raise RuntimeError (f"Unsupported file type for comparison: { a .name } " )
86+
87+ if b .suffix == ".oci" :
88+ b_image_id = podman_api .load_oci_archive (b , podman = podman )
89+ elif b .suffix == ".tar" :
90+ b_image_id = Image .import_plain_tar (b , podman = podman )
91+ else :
92+ raise RuntimeError (f"Unsupported file type for comparison: { b .name } " )
93+
94+ image = podman_api .get_image (a_image_id , podman = podman )
95+
96+ result = image .get_filesystem_changes (
97+ parent_layer_image_id = b_image_id , podman = podman
98+ )
99+
100+ differences = (
101+ result [PODMAN_FS_CHANGE_ADDED ] + result [PODMAN_FS_CHANGE_DELETED ]
102+ )
103+
104+ whitelist = False
105+
106+ for entry in result [PODMAN_FS_CHANGE_MODIFIED ]:
107+ if not any (re .match (pattern , entry ) for pattern in self .whitelist ):
108+ differences .append (entry )
109+ else :
110+ whitelist = True
111+ finally :
112+ if a_image_id is not None :
113+ podman .images .remove (a_image_id )
114+ if b_image_id is not None :
115+ podman .images .remove (b_image_id )
116+
117+ return differences , whitelist
0 commit comments