|
4 | 4 |
|
5 | 5 | use std::collections::HashSet; |
6 | 6 | use std::io::{BufRead, Write}; |
| 7 | +use std::os::fd::AsFd; |
7 | 8 | use std::process::Command; |
8 | 9 |
|
9 | 10 | use anyhow::{Context, Result, anyhow}; |
@@ -361,6 +362,55 @@ pub(crate) async fn prune_container_store(sysroot: &Storage) -> Result<()> { |
361 | 362 | Ok(()) |
362 | 363 | } |
363 | 364 |
|
| 365 | +/// Core disk space check: verify that `bytes_to_fetch` fits within available space, |
| 366 | +/// leaving at least `min_free` bytes reserved. |
| 367 | +fn check_disk_space_inner( |
| 368 | + fd: impl AsFd, |
| 369 | + bytes_to_fetch: u64, |
| 370 | + min_free: u64, |
| 371 | + imgref: &ImageReference, |
| 372 | +) -> Result<()> { |
| 373 | + let stat = rustix::fs::fstatvfs(fd)?; |
| 374 | + let bytes_avail = stat.f_bsize.checked_mul(stat.f_bavail).unwrap_or(u64::MAX); |
| 375 | + let usable = bytes_avail.saturating_sub(min_free); |
| 376 | + tracing::trace!("bytes_avail: {bytes_avail} min_free: {min_free} usable: {usable}"); |
| 377 | + |
| 378 | + if bytes_to_fetch > usable { |
| 379 | + anyhow::bail!( |
| 380 | + "Insufficient free space for {image} (available: {available} required: {required})", |
| 381 | + available = ostree_ext::glib::format_size(usable), |
| 382 | + required = ostree_ext::glib::format_size(bytes_to_fetch), |
| 383 | + image = imgref.image, |
| 384 | + ); |
| 385 | + } |
| 386 | + Ok(()) |
| 387 | +} |
| 388 | + |
| 389 | +/// Verify there is sufficient disk space to pull an image into the ostree repo. |
| 390 | +/// Respects the repository's configured min-free-space threshold. |
| 391 | +pub(crate) fn check_disk_space_ostree( |
| 392 | + repo: &ostree::Repo, |
| 393 | + image_meta: &PreparedImportMeta, |
| 394 | + imgref: &ImageReference, |
| 395 | +) -> Result<()> { |
| 396 | + let min_free = repo.min_free_space_bytes().unwrap_or(0); |
| 397 | + check_disk_space_inner( |
| 398 | + repo.dfd_borrow(), |
| 399 | + image_meta.bytes_to_fetch, |
| 400 | + min_free, |
| 401 | + imgref, |
| 402 | + ) |
| 403 | +} |
| 404 | + |
| 405 | +/// Verify there is sufficient disk space to pull an image into the composefs store. |
| 406 | +pub(crate) fn check_disk_space_composefs( |
| 407 | + cfs: &crate::store::ComposefsRepository, |
| 408 | + image_meta: &PreparedImportMeta, |
| 409 | + imgref: &ImageReference, |
| 410 | +) -> Result<()> { |
| 411 | + check_disk_space_inner(cfs.objects_dir()?, image_meta.bytes_to_fetch, 0, imgref) |
| 412 | +} |
| 413 | + |
364 | 414 | pub(crate) struct PreparedImportMeta { |
365 | 415 | pub imp: ImageImporter, |
366 | 416 | pub prep: Box<PreparedImport>, |
@@ -550,6 +600,11 @@ pub(crate) async fn pull_unified( |
550 | 600 | Ok(existing) |
551 | 601 | } |
552 | 602 | PreparedPullResult::Ready(prepared_image_meta) => { |
| 603 | + check_disk_space_composefs( |
| 604 | + store.get_ensure_composefs()?.as_ref(), |
| 605 | + &prepared_image_meta, |
| 606 | + imgref, |
| 607 | + )?; |
553 | 608 | // To avoid duplicate success logs, pass a containers-storage imgref to the importer |
554 | 609 | let cs_imgref = ImageReference { |
555 | 610 | transport: "containers-storage".to_string(), |
@@ -658,6 +713,8 @@ pub(crate) async fn pull( |
658 | 713 | Ok(existing) |
659 | 714 | } |
660 | 715 | PreparedPullResult::Ready(prepared_image_meta) => { |
| 716 | + // Check disk space before attempting to pull |
| 717 | + check_disk_space_ostree(repo, &prepared_image_meta, imgref)?; |
661 | 718 | // Log that we're pulling a new image |
662 | 719 | const PULLING_NEW_IMAGE_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0"; |
663 | 720 | tracing::info!( |
@@ -1368,4 +1425,24 @@ UUID=6907-17CA /boot/efi vfat umask=0077,shortname=win |
1368 | 1425 | assert_eq!(tempdir.read_to_string("etc/fstab")?, modified); |
1369 | 1426 | Ok(()) |
1370 | 1427 | } |
| 1428 | + #[test] |
| 1429 | + fn test_check_disk_space_inner() -> Result<()> { |
| 1430 | + let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?; |
| 1431 | + let imgref = ImageReference { |
| 1432 | + image: "quay.io/exampleos/exampleos:latest".into(), |
| 1433 | + transport: "registry".into(), |
| 1434 | + signature: None, |
| 1435 | + }; |
| 1436 | + |
| 1437 | + // 0 bytes needed always passes |
| 1438 | + check_disk_space_inner(&*td, 0, 0, &imgref)?; |
| 1439 | + |
| 1440 | + // u64::MAX bytes needed always fails |
| 1441 | + assert!(check_disk_space_inner(&*td, u64::MAX, 0, &imgref).is_err()); |
| 1442 | + |
| 1443 | + // With min_free consuming all usable space, even a tiny fetch fails |
| 1444 | + assert!(check_disk_space_inner(&*td, 1, u64::MAX, &imgref).is_err()); |
| 1445 | + |
| 1446 | + Ok(()) |
| 1447 | + } |
1371 | 1448 | } |
0 commit comments