Skip to content

Commit 147a844

Browse files
committed
WIP disk-image in patc
Assisted-by: Cursor AI Signed-off-by: Scott Wickersham <swickers@redhat.com>
1 parent eb74eb6 commit 147a844

4 files changed

Lines changed: 160 additions & 13 deletions

File tree

scripts/python/helpers/compress_artifacts.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
* Pulls signed macOS and Windows OCI artifacts from Quay into a ``signed/`` directory.
66
* Restores supplementary files (readme, license, changelog) that were held during signing.
77
* Compresses each file entry into the final deliverable format:
8-
- macOS / Linux → ``.tar.gz`` (from ``os/arch/`` directory)
8+
- macOS / Linux (non-disk-image) → ``.tar.gz`` (from ``os/arch/`` directory)
9+
- Linux disk images (``.qcow2``, ``.iso``) → copied as-is to ``ready_for_distribution/``
910
- Windows → ``.zip`` (from ``os/arch/`` directory, extension corrected from
1011
``.tar.gz``/``.tar``)
1112
* Updates ``SNAPSHOT_JSON`` to reflect corrected Windows filenames in ``files[]``.
@@ -39,6 +40,10 @@
3940

4041
PROG = "compress_artifacts.py"
4142

43+
# File extensions that identify disk images. These files are copied as-is to
44+
# ready_for_distribution without being wrapped in a tar archive.
45+
_DISK_IMAGE_EXTENSIONS: frozenset[str] = frozenset({".qcow2", ".iso"})
46+
4247
QUAY_SECRET_MOUNT = Path(os.environ.get("QUAY_SECRET_MOUNT", "/mnt/quaySecret"))
4348
CONTENT_DIR = Path(os.environ.get("CONTENT_DIR", "/shared/artifacts"))
4449
SHARED_DIR = Path(os.environ.get("SHARED_DIR", "/shared"))
@@ -111,11 +116,10 @@ def _compress_file_entry(
111116
the archive is created as a ``.zip`` instead of ``.tar.gz``/``.tar``, and the returned
112117
source path reflects the corrected filename so the snapshot can be updated accordingly.
113118
114-
Raises RuntimeError on failure (missing source, unknown OS, or empty arch directory).
119+
Disk images (``.qcow2``, ``.iso``) are copied directly to ``ready_dir`` without
120+
being wrapped in a tarball, since they are already in their final deliverable format.
115121
116-
Note: all files are currently compressed regardless of type. ISOs should be
117-
passed through as-is rather than wrapped in a tarball — this will need to be
118-
addressed before ISO delivery is supported.
122+
Raises RuntimeError on failure (missing source, unknown OS, or empty arch directory).
119123
"""
120124
source = entry.get("source")
121125
if not source:
@@ -139,12 +143,22 @@ def _compress_file_entry(
139143

140144
# macOS and Linux follow the Unix convention of tar.gz archives; Windows uses zip
141145
# because that is the standard expected by Windows users and Developer Portal tooling.
146+
# Disk images are an exception: they are delivered as-is without any archiving.
142147
if os_name in ("darwin", "linux"):
143148
out_path = ready_dir / source_filename
144-
with tarfile.open(str(out_path), "w:gz") as tf:
145-
for item in sorted(arch_dir.rglob("*")):
146-
if item.is_file():
147-
tf.add(str(item), arcname=str(item.relative_to(arch_dir)))
149+
if Path(source_filename).suffix.lower() in _DISK_IMAGE_EXTENSIONS:
150+
files = [f for f in arch_dir.rglob("*") if f.is_file()]
151+
if len(files) != 1:
152+
raise RuntimeError(
153+
f"Expected exactly one disk image file in {arch_dir}, "
154+
f"found {len(files)}"
155+
)
156+
shutil.copy2(str(files[0]), str(out_path))
157+
else:
158+
with tarfile.open(str(out_path), "w:gz") as tf:
159+
for item in sorted(arch_dir.rglob("*")):
160+
if item.is_file():
161+
tf.add(str(item), arcname=str(item.relative_to(arch_dir)))
148162
logger.info(" Created (%s): %s", array_name, source_filename)
149163
return source
150164

scripts/python/helpers/push_unsigned.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
SUPPLEMENTARY_NAMES = {"readme", "license", "changelog"}
4141
SUPPLEMENTARY_EXTS = {".md", ".txt"}
4242

43+
# File extensions that identify disk images. These files are moved directly to
44+
# the target directory without tar extraction.
45+
_DISK_IMAGE_EXTENSIONS: frozenset[str] = frozenset({".qcow2", ".iso"})
46+
4347
logger = logging.getLogger(__name__)
4448

4549

@@ -81,7 +85,11 @@ def move_supplementary_out(src_root: Path, hold_root: Path) -> None:
8185

8286

8387
def _unpack_file_entries(entries: list[dict], component_dir: Path, unsigned_dir: Path) -> None:
84-
"""Extract each archive from entries into its OS/arch subdirectory under unsigned_dir."""
88+
"""Extract each archive from entries into its OS/arch subdirectory under unsigned_dir.
89+
90+
Disk images (.qcow2, .iso) are moved directly to the target directory without
91+
unpacking, since they are not archives.
92+
"""
8593
for entry in entries:
8694
source = entry.get("source", "")
8795
os_name = entry.get("os", "")
@@ -103,9 +111,12 @@ def _unpack_file_entries(entries: list[dict], component_dir: Path, unsigned_dir:
103111
continue
104112

105113
target_dir.mkdir(parents=True, exist_ok=True)
106-
with tarfile.open(str(archive_path)) as tf:
107-
_safe_extract_archive(tf, target_dir, archive_name)
108-
archive_path.unlink()
114+
if archive_path.suffix.lower() in _DISK_IMAGE_EXTENSIONS:
115+
shutil.move(str(archive_path), str(target_dir / archive_name))
116+
else:
117+
with tarfile.open(str(archive_path)) as tf:
118+
_safe_extract_archive(tf, target_dir, archive_name)
119+
archive_path.unlink()
109120

110121

111122
def _safe_extract_archive(tf: tarfile.TarFile, target_dir: Path, archive_name: str) -> None:

scripts/python/helpers/test_compress_artifacts.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,92 @@ def test_compress_file_entry_windows(tmp_path: Path, monkeypatch: pytest.MonkeyP
148148
assert result is not None and result.endswith(".zip")
149149

150150

151+
def test_compress_file_entry_qcow2_passthrough(
152+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
153+
) -> None:
154+
"""A qcow2 disk image is copied directly to ready_for_distribution without archiving."""
155+
monkeypatch.setattr(compress_artifacts, "CONTENT_DIR", tmp_path)
156+
comp_dir = tmp_path / "prod"
157+
ready_dir = comp_dir / "ready_for_distribution"
158+
ready_dir.mkdir(parents=True)
159+
160+
linux_arch_dir = comp_dir / "linux" / "x86_64"
161+
linux_arch_dir.mkdir(parents=True)
162+
(linux_arch_dir / "rhel-10.0-x86_64-kvm.qcow2").write_bytes(b"qcow2 content")
163+
164+
result = compress_artifacts._compress_file_entry(
165+
{
166+
"source": "/releases/rhel-10.0-x86_64-kvm.qcow2",
167+
"os": "linux",
168+
"arch": "x86_64",
169+
},
170+
"files",
171+
comp_dir,
172+
ready_dir,
173+
)
174+
out = ready_dir / "rhel-10.0-x86_64-kvm.qcow2"
175+
assert out.exists()
176+
assert out.read_bytes() == b"qcow2 content"
177+
assert not tarfile.is_tarfile(str(out))
178+
assert result == "/releases/rhel-10.0-x86_64-kvm.qcow2"
179+
180+
181+
def test_compress_file_entry_iso_passthrough(
182+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
183+
) -> None:
184+
"""An iso disk image is copied directly to ready_for_distribution without archiving."""
185+
monkeypatch.setattr(compress_artifacts, "CONTENT_DIR", tmp_path)
186+
comp_dir = tmp_path / "prod"
187+
ready_dir = comp_dir / "ready_for_distribution"
188+
ready_dir.mkdir(parents=True)
189+
190+
linux_arch_dir = comp_dir / "linux" / "x86_64"
191+
linux_arch_dir.mkdir(parents=True)
192+
(linux_arch_dir / "rhel-10.0-x86_64-boot.iso").write_bytes(b"iso content")
193+
194+
result = compress_artifacts._compress_file_entry(
195+
{
196+
"source": "/releases/rhel-10.0-x86_64-boot.iso",
197+
"os": "linux",
198+
"arch": "x86_64",
199+
},
200+
"files",
201+
comp_dir,
202+
ready_dir,
203+
)
204+
out = ready_dir / "rhel-10.0-x86_64-boot.iso"
205+
assert out.exists()
206+
assert out.read_bytes() == b"iso content"
207+
assert result == "/releases/rhel-10.0-x86_64-boot.iso"
208+
209+
210+
def test_compress_file_entry_disk_image_multiple_files_raises(
211+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
212+
) -> None:
213+
"""RuntimeError is raised when a disk image arch dir contains more than one file."""
214+
monkeypatch.setattr(compress_artifacts, "CONTENT_DIR", tmp_path)
215+
comp_dir = tmp_path / "prod"
216+
ready_dir = comp_dir / "ready_for_distribution"
217+
ready_dir.mkdir(parents=True)
218+
219+
linux_arch_dir = comp_dir / "linux" / "x86_64"
220+
linux_arch_dir.mkdir(parents=True)
221+
(linux_arch_dir / "rhel-10.0-x86_64-kvm.qcow2").write_bytes(b"a")
222+
(linux_arch_dir / "extra.qcow2").write_bytes(b"b")
223+
224+
with pytest.raises(RuntimeError, match="exactly one disk image"):
225+
compress_artifacts._compress_file_entry(
226+
{
227+
"source": "/releases/rhel-10.0-x86_64-kvm.qcow2",
228+
"os": "linux",
229+
"arch": "x86_64",
230+
},
231+
"files",
232+
comp_dir,
233+
ready_dir,
234+
)
235+
236+
151237
def test_compress_file_entry_missing_arch_dir_raises(
152238
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
153239
) -> None:

scripts/python/helpers/test_push_unsigned.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,42 @@ def test_unpack_file_entries_skips_missing_fields(tmp_path: Path) -> None:
283283
# no crash
284284

285285

286+
def test_unpack_file_entries_qcow2_passthrough(tmp_path: Path) -> None:
287+
"""A qcow2 disk image is moved directly to the OS/arch dir without tar extraction."""
288+
comp_dir = tmp_path / "prod"
289+
comp_dir.mkdir()
290+
disk_image = comp_dir / "rhel-10.0-x86_64-kvm.qcow2"
291+
disk_image.write_bytes(b"qcow2 raw content")
292+
293+
push_unsigned._unpack_file_entries(
294+
[{"source": "/releases/rhel-10.0-x86_64-kvm.qcow2", "os": "linux", "arch": "x86_64"}],
295+
comp_dir,
296+
comp_dir / "unsigned",
297+
)
298+
dest = comp_dir / "linux" / "x86_64" / "rhel-10.0-x86_64-kvm.qcow2"
299+
assert dest.exists()
300+
assert dest.read_bytes() == b"qcow2 raw content"
301+
assert not disk_image.exists()
302+
303+
304+
def test_unpack_file_entries_iso_passthrough(tmp_path: Path) -> None:
305+
"""An iso disk image is moved directly to the OS/arch dir without tar extraction."""
306+
comp_dir = tmp_path / "prod"
307+
comp_dir.mkdir()
308+
disk_image = comp_dir / "rhel-10.0-x86_64-boot.iso"
309+
disk_image.write_bytes(b"iso raw content")
310+
311+
push_unsigned._unpack_file_entries(
312+
[{"source": "/releases/rhel-10.0-x86_64-boot.iso", "os": "linux", "arch": "x86_64"}],
313+
comp_dir,
314+
comp_dir / "unsigned",
315+
)
316+
dest = comp_dir / "linux" / "x86_64" / "rhel-10.0-x86_64-boot.iso"
317+
assert dest.exists()
318+
assert dest.read_bytes() == b"iso raw content"
319+
assert not disk_image.exists()
320+
321+
286322
def test_unpack_file_entries_rejects_path_traversal(tmp_path: Path) -> None:
287323
"""RuntimeError is raised when a tar entry contains an unsafe path traversal sequence."""
288324
comp_dir = tmp_path / "prod"

0 commit comments

Comments
 (0)