diff --git a/python/ray/experimental/sandbox/_internal/image_utils.py b/python/ray/experimental/sandbox/_internal/image_utils.py index 8fa53bcb1618..de2a803a4776 100644 --- a/python/ray/experimental/sandbox/_internal/image_utils.py +++ b/python/ray/experimental/sandbox/_internal/image_utils.py @@ -180,6 +180,7 @@ def extract_tar_layer( else: tar_fileobj = tar_input + dir_mtimes = [] with tarfile.open(fileobj=tar_fileobj, mode="r:*") as tar: for member in tar.getmembers(): name = member.name.lstrip("/") @@ -261,8 +262,22 @@ def extract_tar_layer( shutil.copyfileobj(f_in, f_out) if member.mode: os.chmod(target_path, member.mode) + # Preserve the archived mtime: tools inside the sandbox rely + # on it (apt revalidates its package lists with + # If-Modified-Since from the file mtime, and a reset-to-now + # mtime makes mirrors answer 304 for stale baked lists). + # Best-effort, like the directory pass below. + try: + os.utime(target_path, (member.mtime, member.mtime)) + except OSError: + pass elif member.isdir(): os.makedirs(target_path, exist_ok=True) + # Applied after the loop: extracting children would bump it. + # Skip preserved symlinks (UsrMerge /bin -> usr/bin): utime + # would follow them and stamp the target with the wrong time. + if not os.path.islink(target_path): + dir_mtimes.append((target_path, member.mtime)) elif member.issym(): os.makedirs(parent_dir, exist_ok=True) try: @@ -280,6 +295,12 @@ def extract_tar_layer( except OSError: pass + for dir_path, mtime in dir_mtimes: + try: + os.utime(dir_path, (mtime, mtime)) + except OSError: + pass + def pull_and_extract_container_image( image: str, diff --git a/python/ray/experimental/sandbox/tests/test_image_manager.py b/python/ray/experimental/sandbox/tests/test_image_manager.py index b88280e41b8f..9b68c8cf4c4b 100644 --- a/python/ray/experimental/sandbox/tests/test_image_manager.py +++ b/python/ray/experimental/sandbox/tests/test_image_manager.py @@ -510,5 +510,37 @@ def test_prepare_oci_bundle_no_resolv_without_host_side_networking(tmp_path): assert _prepare(mgr, tmp_path, network=network) is None +def test_extract_tar_layer_preserves_mtimes(tmp_path): + """Archived mtimes survive extraction: apt inside the sandbox validates + its package lists with If-Modified-Since from the file mtime, so a + reset-to-extraction-time mtime makes mirrors answer 304 for stale + image-baked lists.""" + import io + import os + import tarfile + + from ray.experimental.sandbox._internal.image_utils import extract_tar_layer + + archived_mtime = 1_600_000_000 # 2020-09-13, clearly not "now" + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w") as tar: + dir_info = tarfile.TarInfo("etc") + dir_info.type = tarfile.DIRTYPE + dir_info.mtime = archived_mtime + tar.addfile(dir_info) + file_info = tarfile.TarInfo("etc/os-release") + data = b"ID=debian\n" + file_info.size = len(data) + file_info.mtime = archived_mtime + tar.addfile(file_info, io.BytesIO(data)) + + dest = tmp_path / "rootfs" + dest.mkdir() + extract_tar_layer(buf.getvalue(), str(dest)) + + assert int(os.path.getmtime(dest / "etc" / "os-release")) == archived_mtime + assert int(os.path.getmtime(dest / "etc")) == archived_mtime + + if __name__ == "__main__": sys.exit(pytest.main(["-v", __file__]))