docker-diff 0.0.1__py3-none-any.whl
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.
@@ -0,0 +1,33 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: docker-diff
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Docker Image Comparison Database Manager
|
5
|
+
License: This is free and unencumbered software released into the public domain.
|
6
|
+
|
7
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
8
|
+
distribute this software, either in source code form or as a compiled
|
9
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
10
|
+
means.
|
11
|
+
|
12
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
13
|
+
of this software dedicate any and all copyright interest in the
|
14
|
+
software to the public domain. We make this dedication for the benefit
|
15
|
+
of the public at large and to the detriment of our heirs and
|
16
|
+
successors. We intend this dedication to be an overt act of
|
17
|
+
relinquishment in perpetuity of all present and future rights to this
|
18
|
+
software under copyright law.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
23
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
24
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
25
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
26
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
For more information, please refer to <https://unlicense.org>
|
29
|
+
|
30
|
+
Project-URL: Repository, https://github.com/eapolinario/docker-diff
|
31
|
+
Requires-Python: >=3.10
|
32
|
+
License-File: LICENSE
|
33
|
+
Dynamic: license-file
|
@@ -0,0 +1,7 @@
|
|
1
|
+
docker_diff.py,sha256=_Jt20fhQqZQGac-PacGyV9IU10FXNoCrc6lqlZe4Gps,15516
|
2
|
+
docker_diff-0.0.1.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
|
3
|
+
docker_diff-0.0.1.dist-info/METADATA,sha256=XnO5IBtAKuZq0xQ8C45jYB445OD91_xdzrTnsfh0Ktc,1654
|
4
|
+
docker_diff-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
5
|
+
docker_diff-0.0.1.dist-info/entry_points.txt,sha256=Hknxh2UZBgBH5Lwe55BK5Gh813R1qHZSPIYQ9dzKQXk,49
|
6
|
+
docker_diff-0.0.1.dist-info/top_level.txt,sha256=HSDBG4y8znoDTP5jcUCH2gZZ-qiZW0_6AsIeXry71Ew,12
|
7
|
+
docker_diff-0.0.1.dist-info/RECORD,,
|
@@ -0,0 +1,24 @@
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
4
|
+
distribute this software, either in source code form or as a compiled
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
6
|
+
means.
|
7
|
+
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
9
|
+
of this software dedicate any and all copyright interest in the
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
11
|
+
of the public at large and to the detriment of our heirs and
|
12
|
+
successors. We intend this dedication to be an overt act of
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
14
|
+
software under copyright law.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
@@ -0,0 +1 @@
|
|
1
|
+
docker_diff
|
docker_diff.py
ADDED
@@ -0,0 +1,403 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Docker Image Comparison Database Manager
|
4
|
+
Provides functions to store and query Docker image file comparison results in SQLite
|
5
|
+
"""
|
6
|
+
|
7
|
+
import sqlite3
|
8
|
+
import json
|
9
|
+
import subprocess
|
10
|
+
import sys
|
11
|
+
from datetime import datetime
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import List, Dict, Optional, Tuple
|
14
|
+
|
15
|
+
class DockerImageDB:
|
16
|
+
def __init__(self, db_path: str = "docker_images.db"):
|
17
|
+
self.db_path = db_path
|
18
|
+
self.init_database()
|
19
|
+
|
20
|
+
def init_database(self):
|
21
|
+
"""Initialize the database with schema"""
|
22
|
+
with sqlite3.connect(self.db_path) as conn:
|
23
|
+
# Read and execute schema
|
24
|
+
schema_path = Path(__file__).parent / "schema.sql"
|
25
|
+
with open(schema_path) as f:
|
26
|
+
conn.executescript(f.read())
|
27
|
+
|
28
|
+
def add_image(self, name: str, digest: str = None, size_bytes: int = None) -> int:
|
29
|
+
"""Add an image to the database, return image_id"""
|
30
|
+
with sqlite3.connect(self.db_path) as conn:
|
31
|
+
cursor = conn.cursor()
|
32
|
+
cursor.execute("""
|
33
|
+
INSERT OR IGNORE INTO images (name, digest, size_bytes)
|
34
|
+
VALUES (?, ?, ?)
|
35
|
+
""", (name, digest, size_bytes))
|
36
|
+
|
37
|
+
# Get the image ID
|
38
|
+
cursor.execute("SELECT id FROM images WHERE name = ?", (name,))
|
39
|
+
return cursor.fetchone()[0]
|
40
|
+
|
41
|
+
def add_files_for_image(self, image_id: int, files: List[Dict]):
|
42
|
+
"""Add file listings for an image"""
|
43
|
+
with sqlite3.connect(self.db_path) as conn:
|
44
|
+
cursor = conn.cursor()
|
45
|
+
|
46
|
+
# Clear existing files for this image
|
47
|
+
cursor.execute("DELETE FROM files WHERE image_id = ?", (image_id,))
|
48
|
+
|
49
|
+
# Insert new files
|
50
|
+
file_data = []
|
51
|
+
for file_info in files:
|
52
|
+
file_data.append((
|
53
|
+
image_id,
|
54
|
+
file_info['path'],
|
55
|
+
file_info.get('size', 0),
|
56
|
+
file_info.get('mode'),
|
57
|
+
file_info.get('mtime'),
|
58
|
+
file_info.get('type', 'file'),
|
59
|
+
file_info.get('checksum')
|
60
|
+
))
|
61
|
+
|
62
|
+
cursor.executemany("""
|
63
|
+
INSERT INTO files
|
64
|
+
(image_id, file_path, file_size, file_mode, modified_time, file_type, checksum)
|
65
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
66
|
+
""", file_data)
|
67
|
+
|
68
|
+
def scan_image(self, image_name: str) -> int:
|
69
|
+
"""Scan a Docker image and store its files"""
|
70
|
+
print(f"Scanning {image_name}...")
|
71
|
+
|
72
|
+
# Add image to database
|
73
|
+
image_id = self.add_image(image_name)
|
74
|
+
|
75
|
+
# Get file listing using docker run
|
76
|
+
try:
|
77
|
+
result = subprocess.run([
|
78
|
+
'docker', 'run', '--rm', image_name,
|
79
|
+
'find', '/', '-type', 'f', '-exec', 'stat', '-c', '%n|%s|%Y', '{}', ';'
|
80
|
+
], capture_output=True, text=True)
|
81
|
+
|
82
|
+
files = []
|
83
|
+
for line in result.stdout.strip().split('\n'):
|
84
|
+
if '|' in line:
|
85
|
+
parts = line.split('|')
|
86
|
+
if len(parts) >= 3:
|
87
|
+
files.append({
|
88
|
+
'path': parts[0],
|
89
|
+
'size': int(parts[1]) if parts[1].isdigit() else 0,
|
90
|
+
'mtime': int(parts[2]) if parts[2].isdigit() else None
|
91
|
+
})
|
92
|
+
|
93
|
+
self.add_files_for_image(image_id, files)
|
94
|
+
print(f" Stored {len(files)} files for {image_name}")
|
95
|
+
return image_id
|
96
|
+
|
97
|
+
except subprocess.SubprocessError as e:
|
98
|
+
print(f"Error scanning {image_name}: {e}")
|
99
|
+
return image_id
|
100
|
+
|
101
|
+
def create_comparison(self, name: str, description: str = None) -> int:
|
102
|
+
"""Create a new comparison session"""
|
103
|
+
with sqlite3.connect(self.db_path) as conn:
|
104
|
+
cursor = conn.cursor()
|
105
|
+
cursor.execute("""
|
106
|
+
INSERT INTO comparisons (name, description)
|
107
|
+
VALUES (?, ?)
|
108
|
+
""", (name, description))
|
109
|
+
return cursor.lastrowid
|
110
|
+
|
111
|
+
def add_images_to_comparison(self, comparison_id: int, image_ids: List[int]):
|
112
|
+
"""Add images to a comparison"""
|
113
|
+
with sqlite3.connect(self.db_path) as conn:
|
114
|
+
cursor = conn.cursor()
|
115
|
+
for image_id in image_ids:
|
116
|
+
cursor.execute("""
|
117
|
+
INSERT OR IGNORE INTO comparison_images (comparison_id, image_id)
|
118
|
+
VALUES (?, ?)
|
119
|
+
""", (comparison_id, image_id))
|
120
|
+
|
121
|
+
def compare_images(self, image_names: List[str], comparison_name: str = None) -> int:
|
122
|
+
"""Compare multiple images and store results"""
|
123
|
+
if not comparison_name:
|
124
|
+
comparison_name = f"Comparison_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
125
|
+
|
126
|
+
# Scan all images
|
127
|
+
image_ids = []
|
128
|
+
for image_name in image_names:
|
129
|
+
image_id = self.scan_image(image_name)
|
130
|
+
image_ids.append(image_id)
|
131
|
+
|
132
|
+
# Create comparison
|
133
|
+
comparison_id = self.create_comparison(
|
134
|
+
comparison_name,
|
135
|
+
f"Comparing: {', '.join(image_names)}"
|
136
|
+
)
|
137
|
+
|
138
|
+
# Add images to comparison
|
139
|
+
self.add_images_to_comparison(comparison_id, image_ids)
|
140
|
+
|
141
|
+
# Generate file differences
|
142
|
+
self._generate_file_differences(comparison_id)
|
143
|
+
|
144
|
+
return comparison_id
|
145
|
+
|
146
|
+
def _generate_file_differences(self, comparison_id: int):
|
147
|
+
"""Generate file difference records for a comparison"""
|
148
|
+
with sqlite3.connect(self.db_path) as conn:
|
149
|
+
conn.execute("""
|
150
|
+
INSERT INTO file_differences
|
151
|
+
(comparison_id, file_path, difference_type, source_image_id, target_image_id, old_size, new_size, size_change)
|
152
|
+
SELECT
|
153
|
+
? as comparison_id,
|
154
|
+
f1.file_path,
|
155
|
+
CASE
|
156
|
+
WHEN f2.file_path IS NULL THEN 'only_in_first'
|
157
|
+
WHEN f1.file_size != f2.file_size THEN 'changed'
|
158
|
+
ELSE 'common'
|
159
|
+
END as difference_type,
|
160
|
+
f1.image_id as source_image_id,
|
161
|
+
f2.image_id as target_image_id,
|
162
|
+
f1.file_size as old_size,
|
163
|
+
f2.file_size as new_size,
|
164
|
+
COALESCE(f2.file_size - f1.file_size, -f1.file_size) as size_change
|
165
|
+
FROM files f1
|
166
|
+
JOIN comparison_images ci1 ON f1.image_id = ci1.image_id
|
167
|
+
LEFT JOIN files f2 ON f1.file_path = f2.file_path
|
168
|
+
AND f2.image_id IN (
|
169
|
+
SELECT ci2.image_id FROM comparison_images ci2
|
170
|
+
WHERE ci2.comparison_id = ? AND ci2.image_id != f1.image_id
|
171
|
+
)
|
172
|
+
WHERE ci1.comparison_id = ?
|
173
|
+
""", (comparison_id, comparison_id, comparison_id))
|
174
|
+
|
175
|
+
def get_comparison_summary(self, comparison_id: int) -> Dict:
|
176
|
+
"""Get summary statistics for a comparison"""
|
177
|
+
with sqlite3.connect(self.db_path) as conn:
|
178
|
+
cursor = conn.cursor()
|
179
|
+
|
180
|
+
# Get basic info
|
181
|
+
cursor.execute("""
|
182
|
+
SELECT c.name, c.description, c.created_at,
|
183
|
+
GROUP_CONCAT(i.name, ', ') as images
|
184
|
+
FROM comparisons c
|
185
|
+
JOIN comparison_images ci ON c.id = ci.comparison_id
|
186
|
+
JOIN images i ON ci.image_id = i.id
|
187
|
+
WHERE c.id = ?
|
188
|
+
GROUP BY c.id
|
189
|
+
""", (comparison_id,))
|
190
|
+
|
191
|
+
basic_info = cursor.fetchone()
|
192
|
+
if not basic_info:
|
193
|
+
return {}
|
194
|
+
|
195
|
+
# Get difference counts
|
196
|
+
cursor.execute("""
|
197
|
+
SELECT difference_type, COUNT(*) as count
|
198
|
+
FROM file_differences
|
199
|
+
WHERE comparison_id = ?
|
200
|
+
GROUP BY difference_type
|
201
|
+
""", (comparison_id,))
|
202
|
+
|
203
|
+
diff_counts = dict(cursor.fetchall())
|
204
|
+
|
205
|
+
return {
|
206
|
+
'name': basic_info[0],
|
207
|
+
'description': basic_info[1],
|
208
|
+
'created_at': basic_info[2],
|
209
|
+
'images': basic_info[3].split(', '),
|
210
|
+
'differences': diff_counts,
|
211
|
+
'total_differences': sum(diff_counts.values())
|
212
|
+
}
|
213
|
+
|
214
|
+
def query_unique_files(self, comparison_id: int) -> List[Tuple]:
|
215
|
+
"""Get files unique to each image in comparison"""
|
216
|
+
with sqlite3.connect(self.db_path) as conn:
|
217
|
+
cursor = conn.cursor()
|
218
|
+
cursor.execute("""
|
219
|
+
SELECT i.name as image_name, f.file_path, f.file_size
|
220
|
+
FROM unique_files uf
|
221
|
+
JOIN comparisons c ON uf.comparison_name = c.name
|
222
|
+
JOIN images i ON uf.image_name = i.name
|
223
|
+
JOIN files f ON i.id = f.image_id AND uf.file_path = f.file_path
|
224
|
+
WHERE c.id = ?
|
225
|
+
ORDER BY i.name, f.file_size DESC
|
226
|
+
""", (comparison_id,))
|
227
|
+
return cursor.fetchall()
|
228
|
+
|
229
|
+
|
230
|
+
def print_comparison_summary(db: DockerImageDB, comparison_id: int):
|
231
|
+
"""Print detailed comparison summary"""
|
232
|
+
summary = db.get_comparison_summary(comparison_id)
|
233
|
+
if not summary:
|
234
|
+
print(f"No comparison found with ID {comparison_id}")
|
235
|
+
return
|
236
|
+
|
237
|
+
print(f"\n{'='*60}")
|
238
|
+
print(f"COMPARISON SUMMARY")
|
239
|
+
print(f"{'='*60}")
|
240
|
+
print(f"Name: {summary['name']}")
|
241
|
+
print(f"Description: {summary.get('description', 'N/A')}")
|
242
|
+
print(f"Created: {summary['created_at']}")
|
243
|
+
print(f"Images: {', '.join(summary['images'])}")
|
244
|
+
print(f"\nDifference Summary:")
|
245
|
+
|
246
|
+
diff_counts = summary.get('differences', {})
|
247
|
+
total = summary.get('total_differences', 0)
|
248
|
+
|
249
|
+
if total == 0:
|
250
|
+
print(" No differences found")
|
251
|
+
else:
|
252
|
+
for diff_type, count in diff_counts.items():
|
253
|
+
percentage = (count / total) * 100
|
254
|
+
print(f" {diff_type.capitalize()}: {count} ({percentage:.1f}%)")
|
255
|
+
print(f" Total: {total}")
|
256
|
+
|
257
|
+
|
258
|
+
def list_comparisons(db: DockerImageDB):
|
259
|
+
"""List all comparisons in the database"""
|
260
|
+
with sqlite3.connect(db.db_path) as conn:
|
261
|
+
cursor = conn.cursor()
|
262
|
+
cursor.execute("""
|
263
|
+
SELECT
|
264
|
+
c.id,
|
265
|
+
c.name,
|
266
|
+
c.created_at,
|
267
|
+
COUNT(DISTINCT ci.image_id) as image_count,
|
268
|
+
GROUP_CONCAT(i.name, ', ') as images
|
269
|
+
FROM comparisons c
|
270
|
+
JOIN comparison_images ci ON c.id = ci.comparison_id
|
271
|
+
JOIN images i ON ci.image_id = i.id
|
272
|
+
GROUP BY c.id
|
273
|
+
ORDER BY c.created_at DESC
|
274
|
+
""")
|
275
|
+
|
276
|
+
comparisons = cursor.fetchall()
|
277
|
+
|
278
|
+
if not comparisons:
|
279
|
+
print("No comparisons found in database.")
|
280
|
+
return
|
281
|
+
|
282
|
+
print(f"\n{'ID':<4} {'Name':<25} {'Images':<8} {'Date':<20} {'Image Names'}")
|
283
|
+
print("-" * 80)
|
284
|
+
|
285
|
+
for comp_id, name, created, img_count, img_names in comparisons:
|
286
|
+
# Truncate long image names
|
287
|
+
if len(img_names) > 35:
|
288
|
+
img_names = img_names[:32] + "..."
|
289
|
+
print(f"{comp_id:<4} {name[:24]:<25} {img_count:<8} {created[:19]:<20} {img_names}")
|
290
|
+
|
291
|
+
|
292
|
+
def list_images(db: DockerImageDB):
|
293
|
+
"""List all images in the database"""
|
294
|
+
with sqlite3.connect(db.db_path) as conn:
|
295
|
+
cursor = conn.cursor()
|
296
|
+
cursor.execute("""
|
297
|
+
SELECT
|
298
|
+
i.id,
|
299
|
+
i.name,
|
300
|
+
i.scanned_at,
|
301
|
+
COUNT(f.id) as file_count,
|
302
|
+
COALESCE(SUM(f.file_size), 0) as total_size
|
303
|
+
FROM images i
|
304
|
+
LEFT JOIN files f ON i.id = f.image_id
|
305
|
+
GROUP BY i.id
|
306
|
+
ORDER BY i.scanned_at DESC
|
307
|
+
""")
|
308
|
+
|
309
|
+
images = cursor.fetchall()
|
310
|
+
|
311
|
+
if not images:
|
312
|
+
print("No images found in database.")
|
313
|
+
return
|
314
|
+
|
315
|
+
print(f"\n{'ID':<4} {'Image Name':<30} {'Files':<8} {'Size (MB)':<12} {'Scanned'}")
|
316
|
+
print("-" * 80)
|
317
|
+
|
318
|
+
for img_id, name, scanned, file_count, total_size in images:
|
319
|
+
size_mb = total_size / (1024 * 1024) if total_size else 0
|
320
|
+
print(f"{img_id:<4} {name[:29]:<30} {file_count:<8} {size_mb:<12.2f} {scanned[:19] if scanned else 'Never'}")
|
321
|
+
|
322
|
+
|
323
|
+
def show_unique_files(db: DockerImageDB, comparison_id: int, limit: int = 20):
|
324
|
+
"""Show files unique to each image in a comparison"""
|
325
|
+
unique_files = db.query_unique_files(comparison_id)
|
326
|
+
|
327
|
+
if not unique_files:
|
328
|
+
print(f"No unique files found for comparison {comparison_id}")
|
329
|
+
return
|
330
|
+
|
331
|
+
print(f"\nUnique Files (showing first {limit}):")
|
332
|
+
print(f"{'Image':<25} {'Size (KB)':<12} {'File Path'}")
|
333
|
+
print("-" * 80)
|
334
|
+
|
335
|
+
for i, (image_name, file_path, file_size) in enumerate(unique_files[:limit]):
|
336
|
+
size_kb = file_size / 1024 if file_size else 0
|
337
|
+
print(f"{image_name[:24]:<25} {size_kb:<12.2f} {file_path}")
|
338
|
+
|
339
|
+
|
340
|
+
# ---- CLI entry point ----
|
341
|
+
|
342
|
+
def _cmd_scan(db: DockerImageDB, args):
|
343
|
+
for image in args.images:
|
344
|
+
db.scan_image(image)
|
345
|
+
|
346
|
+
def _cmd_compare(db: DockerImageDB, args):
|
347
|
+
comparison_id = db.compare_images(args.images, args.name)
|
348
|
+
print_comparison_summary(db, comparison_id)
|
349
|
+
|
350
|
+
def _cmd_list_images(db: DockerImageDB, args):
|
351
|
+
list_images(db)
|
352
|
+
|
353
|
+
def _cmd_list_comparisons(db: DockerImageDB, args):
|
354
|
+
list_comparisons(db)
|
355
|
+
|
356
|
+
def _cmd_summary(db: DockerImageDB, args):
|
357
|
+
print_comparison_summary(db, args.id)
|
358
|
+
|
359
|
+
def _cmd_unique(db: DockerImageDB, args):
|
360
|
+
show_unique_files(db, args.id, args.limit)
|
361
|
+
|
362
|
+
|
363
|
+
def main():
|
364
|
+
"""docker-diff command line interface"""
|
365
|
+
import argparse
|
366
|
+
|
367
|
+
parser = argparse.ArgumentParser(prog="docker-diff", description="Docker image file comparison and database manager")
|
368
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
369
|
+
|
370
|
+
p_scan = sub.add_parser("scan", help="Scan one or more images and store file listings")
|
371
|
+
p_scan.add_argument("images", nargs="+", help="Docker image names (e.g., ubuntu:22.04)")
|
372
|
+
p_scan.set_defaults(func=_cmd_scan)
|
373
|
+
|
374
|
+
p_compare = sub.add_parser("compare", help="Compare images and store results")
|
375
|
+
p_compare.add_argument("images", nargs="+", help="Docker image names to compare")
|
376
|
+
p_compare.add_argument("--name", help="Optional comparison name")
|
377
|
+
p_compare.set_defaults(func=_cmd_compare)
|
378
|
+
|
379
|
+
p_list = sub.add_parser("list", help="List images or comparisons")
|
380
|
+
sub_list = p_list.add_subparsers(dest="what", required=True)
|
381
|
+
|
382
|
+
p_list_images = sub_list.add_parser("images", help="List scanned images")
|
383
|
+
p_list_images.set_defaults(func=_cmd_list_images)
|
384
|
+
|
385
|
+
p_list_comparisons = sub_list.add_parser("comparisons", help="List comparisons")
|
386
|
+
p_list_comparisons.set_defaults(func=_cmd_list_comparisons)
|
387
|
+
|
388
|
+
p_summary = sub.add_parser("summary", help="Show summary for a comparison")
|
389
|
+
p_summary.add_argument("id", type=int, help="Comparison ID")
|
390
|
+
p_summary.set_defaults(func=_cmd_summary)
|
391
|
+
|
392
|
+
p_unique = sub.add_parser("unique", help="Show files unique to each image in a comparison")
|
393
|
+
p_unique.add_argument("id", type=int, help="Comparison ID")
|
394
|
+
p_unique.add_argument("--limit", type=int, default=20, help="Max rows to display")
|
395
|
+
p_unique.set_defaults(func=_cmd_unique)
|
396
|
+
|
397
|
+
args = parser.parse_args()
|
398
|
+
db = DockerImageDB()
|
399
|
+
args.func(db, args)
|
400
|
+
|
401
|
+
|
402
|
+
if __name__ == "__main__":
|
403
|
+
main()
|