Skip to content

Commit b76ab0f

Browse files
jlebondustymabe
authored andcommitted
Add new cosa diff command to diff builds
A lot of times when making major changes to content or how artifacts are built, it's helpful to be able to diff the before and after to make sure only what we expect to change changed. For example, this would've been useful for the `create_disk.sh` to osbuild migration or the more recent tier-x migration (where I ended up doing a lot of comparisons by hand). Now with the move of the live ISO to osbuild, we have a need for it again. Add a new `cosa diff` command for this. The command supports different kinds of diffs and more can easily be added. For now, I've focused on the core ones (RPMs, OSTree content, initramfs) and the live artifacts since those are the needed currently. ``` $ cosa diff -h usage: cmd-diff <snip> options: -h, --help show this help message and exit --from DIFF_FROM First build ID --to DIFF_TO Second build ID --gc Delete cached diff content --rpms Diff RPMs --ostree-ls Diff OSTree contents using 'ostree diff' --ostree Diff OSTree contents using 'git diff' --initrd Diff initramfs contents --live-iso-ls Diff live ISO listings --live-iso Diff live ISO content --live-initrd-ls Diff live initramfs listings --live-initrd Diff live initramfs content --live-rootfs-ls Diff live rootfs listings --live-rootfs Diff live rootfs content --live-squashfs-ls Diff live squashfs listings --live-squashfs Diff live squashfs content ```
1 parent f529f73 commit b76ab0f

File tree

3 files changed

+308
-1
lines changed

3 files changed

+308
-1
lines changed

cmd/coreos-assembler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ var buildCommands = []string{"init", "fetch", "build", "osbuild", "run", "prune"
1616
var advancedBuildCommands = []string{"buildfetch", "buildupload", "oc-adm-release", "push-container"}
1717
var buildextendCommands = []string{"aliyun", "applehv", "aws", "azure", "digitalocean", "exoscale", "extensions-container", "gcp", "hyperv", "ibmcloud", "kubevirt", "live", "metal", "metal4k", "nutanix", "openstack", "qemu", "secex", "virtualbox", "vmware", "vultr"}
1818

19-
var utilityCommands = []string{"aws-replicate", "coreos-prune", "compress", "copy-container", "koji-upload", "kola", "push-container-manifest", "remote-build-container", "remote-session", "sign", "tag", "update-variant"}
19+
var utilityCommands = []string{"aws-replicate", "coreos-prune", "compress", "copy-container", "diff", "koji-upload", "kola", "push-container-manifest", "remote-build-container", "remote-session", "sign", "tag", "update-variant"}
2020
var otherCommands = []string{"shell", "meta"}
2121

2222
func init() {

src/cmd-diff

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import os
5+
import shutil
6+
import subprocess
7+
import sys
8+
import tempfile
9+
10+
from dataclasses import dataclass
11+
from typing import Callable
12+
13+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14+
from cosalib.builds import Builds
15+
from cosalib.cmdlib import runcmd, import_ostree_commit
16+
17+
18+
@dataclass
19+
class DiffBuildTarget:
20+
id: str
21+
dir: str
22+
meta: dict
23+
24+
@staticmethod
25+
def from_build(builds, build):
26+
return DiffBuildTarget(build, builds.get_build_dir(build),
27+
builds.get_build_meta(build))
28+
29+
30+
@dataclass
31+
class Differ:
32+
name: str
33+
description: str
34+
needs_ostree: bool
35+
function: Callable[[DiffBuildTarget, DiffBuildTarget], None]
36+
37+
38+
TMP_REPO = 'tmp/repo'
39+
40+
DIFF_CACHE = 'tmp/diff-cache'
41+
42+
43+
def main():
44+
args = parse_args()
45+
builds = Builds()
46+
47+
latest_build = builds.get_latest()
48+
49+
os.makedirs(DIFF_CACHE, exist_ok=True)
50+
51+
# finalize diff targets
52+
if args.diff_from is None and args.diff_to is None:
53+
# default to previous and current build
54+
args.diff_from = builds.get_previous()
55+
args.diff_to = latest_build
56+
elif args.diff_from is None:
57+
args.diff_from = latest_build
58+
elif args.diff_to is None:
59+
args.diff_to = latest_build
60+
61+
if args.diff_from == 'latest':
62+
args.diff_from = latest_build
63+
if args.diff_to == 'latest':
64+
args.diff_to = latest_build
65+
66+
if args.diff_from == args.diff_to:
67+
raise Exception("from and to builds are the same")
68+
69+
diff_from = DiffBuildTarget.from_build(builds, args.diff_from)
70+
diff_to = DiffBuildTarget.from_build(builds, args.diff_to)
71+
72+
# get activated differs
73+
active_differs = []
74+
for differ in DIFFERS:
75+
if getattr(args, differ.name.replace('-', '_')):
76+
active_differs += [differ]
77+
78+
# ensure commits are imported if we know we'll need them
79+
if any(differ.needs_ostree for differ in active_differs):
80+
for target in [diff_from, diff_to]:
81+
import_ostree_commit('.', target.dir, target.meta, extract_json=0)
82+
83+
# start diff'ing
84+
for differ in active_differs:
85+
differ.function(diff_from, diff_to)
86+
87+
if args.gc:
88+
# some of the dirs in the rootfs are dumb and have "private" bits
89+
runcmd(['find', DIFF_CACHE, '-type', 'd', '-exec', 'chmod', 'u+rwx', '{}', '+'])
90+
shutil.rmtree(DIFF_CACHE)
91+
92+
93+
def parse_args():
94+
# Parse args and dispatch
95+
parser = argparse.ArgumentParser()
96+
parser.add_argument("--from", dest='diff_from', help="First build ID")
97+
parser.add_argument("--to", dest='diff_to', help="Second build ID")
98+
parser.add_argument("--gc", action='store_true', help="Delete cached diff content")
99+
for differ in DIFFERS:
100+
parser.add_argument("--" + differ.name, action='store_true', default=False,
101+
help=differ.description)
102+
return parser.parse_args()
103+
104+
105+
def diff_rpms(diff_from, diff_to):
106+
commit_from = diff_from.meta['ostree-commit']
107+
commit_to = diff_to.meta['ostree-commit']
108+
runcmd(['rpm-ostree', 'db', 'diff', '--repo', TMP_REPO, commit_from, commit_to])
109+
110+
111+
def diff_ostree_ls(diff_from, diff_to):
112+
commit_from = diff_from.meta['ostree-commit']
113+
commit_to = diff_to.meta['ostree-commit']
114+
runcmd(['ostree', 'diff', '--repo', TMP_REPO, commit_from, commit_to])
115+
116+
117+
def diff_ostree(diff_from, diff_to):
118+
commit_from = diff_from.meta['ostree-commit']
119+
commit_to = diff_to.meta['ostree-commit']
120+
checkout_from = os.path.join(cache_dir("ostree"), diff_from.id)
121+
checkout_to = os.path.join(cache_dir("ostree"), diff_to.id)
122+
if not os.path.exists(checkout_from):
123+
runcmd(['ostree', 'checkout', '-U', '--repo', TMP_REPO, commit_from, checkout_from])
124+
if not os.path.exists(checkout_to):
125+
runcmd(['ostree', 'checkout', '-U', '--repo', TMP_REPO, commit_to, checkout_to])
126+
git_diff(checkout_from, checkout_to)
127+
128+
129+
def diff_initrd(diff_from, diff_to):
130+
commit_from = diff_from.meta['ostree-commit']
131+
commit_to = diff_to.meta['ostree-commit']
132+
initrd_from = os.path.join(cache_dir("initrd"), diff_from.id)
133+
initrd_to = os.path.join(cache_dir("initrd"), diff_to.id)
134+
135+
def get_initrd_path(commit):
136+
ls = runcmd(['ostree', 'ls', '--repo', TMP_REPO, commit, "/usr/lib/modules",
137+
"--nul-filenames-only"], capture_output=True).stdout
138+
entries = [entry.decode('utf-8') for entry in ls.strip(b'\0').split(b'\0')]
139+
assert len(entries) == 2 # there should only be the modules/ dir and the kver dir
140+
return os.path.join(entries[1], "initramfs.img")
141+
142+
def extract_initrd(commit, dir):
143+
ostree_path = get_initrd_path(commit)
144+
cat = subprocess.Popen(['ostree', 'cat', '--repo', TMP_REPO, commit, ostree_path], stdout=subprocess.PIPE)
145+
runcmd(['coreos-installer', 'dev', 'extract', 'initrd', '-', '-C', dir], stdin=cat.stdout)
146+
cat.wait()
147+
148+
if not os.path.exists(initrd_from):
149+
extract_initrd(commit_from, initrd_from)
150+
if not os.path.exists(initrd_to):
151+
extract_initrd(commit_to, initrd_to)
152+
git_diff(initrd_from, initrd_to)
153+
154+
155+
def diff_live_iso_tree(diff_from, diff_to):
156+
iso_from = os.path.join(diff_from.dir, diff_from.meta['images']['live-iso']['path'])
157+
iso_to = os.path.join(diff_to.dir, diff_to.meta['images']['live-iso']['path'])
158+
diff_cmd_outputs(['coreos-installer', 'dev', 'show', 'iso'], iso_from, iso_to)
159+
diff_cmd_outputs(['isoinfo', '-R', '-l', '-i'], iso_from, iso_to)
160+
161+
162+
def diff_live_iso(diff_from, diff_to):
163+
iso_from = os.path.join(diff_from.dir, diff_from.meta['images']['live-iso']['path'])
164+
iso_to = os.path.join(diff_to.dir, diff_to.meta['images']['live-iso']['path'])
165+
dir_from = os.path.join(cache_dir("iso"), diff_from.id)
166+
dir_to = os.path.join(cache_dir("iso"), diff_to.id)
167+
168+
def extract_iso(iso, dir):
169+
iso = os.path.abspath(iso)
170+
os.mkdir(dir)
171+
runcmd(['bsdtar', 'xpf', iso], cwd=dir)
172+
173+
if not os.path.exists(dir_from):
174+
extract_iso(iso_from, dir_from)
175+
if not os.path.exists(dir_to):
176+
extract_iso(iso_to, dir_to)
177+
git_diff(dir_from, dir_to)
178+
179+
180+
def diff_live_initrd_tree(diff_from, diff_to):
181+
initramfs_from = os.path.join(diff_from.dir, diff_from.meta['images']['live-initramfs']['path'])
182+
initramfs_to = os.path.join(diff_to.dir, diff_to.meta['images']['live-initramfs']['path'])
183+
diff_cmd_outputs(['coreos-installer', 'dev', 'show', 'initrd'], initramfs_from, initramfs_to)
184+
185+
186+
def diff_live_initrd(diff_from, diff_to):
187+
initramfs_from = os.path.join(diff_from.dir, diff_from.meta['images']['live-initramfs']['path'])
188+
initramfs_to = os.path.join(diff_to.dir, diff_to.meta['images']['live-initramfs']['path'])
189+
dir_from = os.path.join(cache_dir("live-initrd"), diff_from.id)
190+
dir_to = os.path.join(cache_dir("live-initrd"), diff_to.id)
191+
192+
if not os.path.exists(dir_from):
193+
runcmd(['coreos-installer', 'dev', 'extract', 'initrd', initramfs_from, '-C', dir_from])
194+
if not os.path.exists(dir_to):
195+
runcmd(['coreos-installer', 'dev', 'extract', 'initrd', initramfs_to, '-C', dir_to])
196+
git_diff(dir_from, dir_to)
197+
198+
199+
def diff_live_rootfs_tree(diff_from, diff_to):
200+
rootfs_from = os.path.join(diff_from.dir, diff_from.meta['images']['live-rootfs']['path'])
201+
rootfs_to = os.path.join(diff_to.dir, diff_to.meta['images']['live-rootfs']['path'])
202+
diff_cmd_outputs(['coreos-installer', 'dev', 'show', 'initrd'], rootfs_from, rootfs_to)
203+
204+
205+
def ensure_extracted_rootfses(diff_from, diff_to):
206+
rootfs_from = os.path.join(diff_from.dir, diff_from.meta['images']['live-rootfs']['path'])
207+
rootfs_to = os.path.join(diff_to.dir, diff_to.meta['images']['live-rootfs']['path'])
208+
dir_from = os.path.join(cache_dir("live-rootfs"), diff_from.id)
209+
dir_to = os.path.join(cache_dir("live-rootfs"), diff_to.id)
210+
211+
def extract_rootfs(img, dir):
212+
runcmd(['coreos-installer', 'dev', 'extract', 'initrd', img, '-C', dir])
213+
214+
if not os.path.exists(dir_from):
215+
extract_rootfs(rootfs_from, dir_from)
216+
if not os.path.exists(dir_to):
217+
extract_rootfs(rootfs_to, dir_to)
218+
219+
return (dir_from, dir_to)
220+
221+
222+
def diff_live_rootfs(diff_from, diff_to):
223+
(dir_from, dir_to) = ensure_extracted_rootfses(diff_from, diff_to)
224+
git_diff(dir_from, dir_to)
225+
226+
227+
def diff_live_squashfs_tree(diff_from, diff_to):
228+
(dir_from, dir_to) = ensure_extracted_rootfses(diff_from, diff_to)
229+
diff_cmd_outputs(['unsquashfs', '-d', '', '-l', '-excludes', '{}',
230+
'/ostree/deploy', '/ostree/repo/objects'],
231+
os.path.join(dir_from, "root.squashfs"),
232+
os.path.join(dir_to, "root.squashfs"))
233+
234+
235+
def diff_live_squashfs(diff_from, diff_to):
236+
(rootfs_dir_from, rootfs_dir_to) = ensure_extracted_rootfses(diff_from, diff_to)
237+
squashfs_from = os.path.join(rootfs_dir_from, "root.squashfs")
238+
squashfs_to = os.path.join(rootfs_dir_to, "root.squashfs")
239+
dir_from = os.path.join(cache_dir("live-squashfs"), diff_from.id)
240+
dir_to = os.path.join(cache_dir("live-squashfs"), diff_to.id)
241+
242+
if not os.path.exists(dir_from):
243+
runcmd(['unsquashfs', '-d', dir_from, '-no-xattrs', '-excludes', squashfs_from, '/ostree/deploy', '/ostree/repo/objects'])
244+
if not os.path.exists(dir_to):
245+
runcmd(['unsquashfs', '-d', dir_to, '-no-xattrs', '-excludes', squashfs_to, '/ostree/deploy', '/ostree/repo/objects'])
246+
247+
git_diff(dir_from, dir_to)
248+
249+
250+
def diff_cmd_outputs(cmd, file_from, file_to):
251+
with tempfile.NamedTemporaryFile(prefix=cmd[0] + '-') as f_from, \
252+
tempfile.NamedTemporaryFile(prefix=cmd[0] + '-') as f_to:
253+
if '{}' not in cmd:
254+
cmd += ['{}']
255+
idx = cmd.index('{}')
256+
cmd_from = list(cmd)
257+
cmd_from[idx] = file_from
258+
subprocess.run(cmd_from, check=True, stdout=f_from).stdout
259+
cmd_to = list(cmd)
260+
cmd_to[idx] = file_to
261+
subprocess.run(cmd_to, check=True, stdout=f_to).stdout
262+
git_diff(f_from.name, f_to.name)
263+
264+
265+
def git_diff(arg_from, arg_to):
266+
runcmd(['git', 'diff', '--no-index', arg_from, arg_to], check=False)
267+
268+
269+
def cache_dir(dir):
270+
dir = os.path.join(DIFF_CACHE, dir)
271+
os.makedirs(dir, exist_ok=True)
272+
return dir
273+
274+
275+
# unfortunately, this has to come at the end to resolve functions
276+
DIFFERS = [
277+
Differ("rpms", "Diff RPMs", needs_ostree=True, function=diff_rpms),
278+
Differ("ostree-ls", "Diff OSTree contents using 'ostree diff'",
279+
needs_ostree=True, function=diff_ostree_ls),
280+
Differ("ostree", "Diff OSTree contents using 'git diff'",
281+
needs_ostree=True, function=diff_ostree),
282+
Differ("initrd", "Diff initramfs contents",
283+
needs_ostree=True, function=diff_initrd),
284+
Differ("live-iso-ls", "Diff live ISO listings",
285+
needs_ostree=False, function=diff_live_iso_tree),
286+
Differ("live-iso", "Diff live ISO content",
287+
needs_ostree=False, function=diff_live_iso),
288+
Differ("live-initrd-ls", "Diff live initramfs listings",
289+
needs_ostree=False, function=diff_live_initrd_tree),
290+
Differ("live-initrd", "Diff live initramfs content",
291+
needs_ostree=False, function=diff_live_initrd),
292+
Differ("live-rootfs-ls", "Diff live rootfs listings",
293+
needs_ostree=False, function=diff_live_rootfs_tree),
294+
Differ("live-rootfs", "Diff live rootfs content",
295+
needs_ostree=False, function=diff_live_rootfs),
296+
Differ("live-squashfs-ls", "Diff live squashfs listings",
297+
needs_ostree=False, function=diff_live_squashfs_tree),
298+
Differ("live-squashfs", "Diff live squashfs content",
299+
needs_ostree=False, function=diff_live_squashfs),
300+
]
301+
302+
if __name__ == '__main__':
303+
main()

src/cosalib/builds.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ def get_latest(self):
6464
# just let throw if there are none
6565
return self._data['builds'][0]['id']
6666

67+
def get_previous(self):
68+
# just let throw if there are none
69+
return self._data['builds'][1]['id']
70+
6771
def get_latest_for_arch(self, basearch):
6872
for build in self._data['builds']:
6973
if basearch in build['arches']:

0 commit comments

Comments
 (0)