Skip to content

Commit c77a63c

Browse files
jmarrerocgwalters
andcommitted
install: add progress-fd for install
Co-authored-by: Colin Walters <[email protected]> Assisted by: Claude Code Signed-off-by: Joseph Marrero Corchado <[email protected]>
1 parent 03fa72b commit c77a63c

File tree

3 files changed

+134
-17
lines changed

3 files changed

+134
-17
lines changed

crates/lib/src/cli.rs

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ use crate::spec::ImageReference;
3434
use crate::utils::sigpolicy_from_opt;
3535

3636
/// Shared progress options
37-
#[derive(Debug, Parser, PartialEq, Eq)]
37+
#[derive(Debug, Parser, PartialEq, Eq, Clone, Serialize, Deserialize)]
3838
pub(crate) struct ProgressOptions {
3939
/// File descriptor number which must refer to an open pipe (anonymous or named).
4040
///
4141
/// Interactive progress will be written to this file descriptor as "JSON lines"
4242
/// format, where each value is separated by a newline.
4343
#[clap(long, hide = true)]
44+
#[serde(default)]
4445
pub(crate) progress_fd: Option<RawProgressFd>,
4546
}
4647

@@ -1422,4 +1423,86 @@ mod tests {
14221423
]));
14231424
assert_eq!(args.as_slice(), ["container", "image", "pull"]);
14241425
}
1426+
1427+
#[test]
1428+
fn test_progress_fd_install_parsing() {
1429+
// Test install to-disk with progress-fd
1430+
let opts = Opt::try_parse_from([
1431+
"bootc",
1432+
"install",
1433+
"to-disk",
1434+
"--progress-fd",
1435+
"3",
1436+
"/dev/sda",
1437+
])
1438+
.unwrap();
1439+
1440+
if let Opt::Install(crate::cli::InstallOpts::ToDisk(install_opts)) = opts {
1441+
assert_eq!(install_opts.progress.progress_fd.unwrap().get_raw_fd(), 3);
1442+
} else {
1443+
panic!("Expected install to-disk command");
1444+
}
1445+
1446+
// Test install to-filesystem with progress-fd
1447+
let opts = Opt::try_parse_from([
1448+
"bootc",
1449+
"install",
1450+
"to-filesystem",
1451+
"--progress-fd",
1452+
"4",
1453+
"/mnt/root",
1454+
])
1455+
.unwrap();
1456+
1457+
if let Opt::Install(crate::cli::InstallOpts::ToFilesystem(install_opts)) = opts {
1458+
assert_eq!(install_opts.progress.progress_fd.unwrap().get_raw_fd(), 4);
1459+
} else {
1460+
panic!("Expected install to-filesystem command");
1461+
}
1462+
1463+
// Test install to-existing-root with progress-fd
1464+
let opts =
1465+
Opt::try_parse_from(["bootc", "install", "to-existing-root", "--progress-fd", "5"])
1466+
.unwrap();
1467+
1468+
if let Opt::Install(crate::cli::InstallOpts::ToExistingRoot(install_opts)) = opts {
1469+
assert_eq!(install_opts.progress.progress_fd.unwrap().get_raw_fd(), 5);
1470+
} else {
1471+
panic!("Expected install to-existing-root command");
1472+
}
1473+
}
1474+
1475+
#[test]
1476+
fn test_progress_fd_validation() {
1477+
// Test that invalid FD values are rejected
1478+
let result = Opt::try_parse_from([
1479+
"bootc",
1480+
"install",
1481+
"to-disk",
1482+
"--progress-fd",
1483+
"1",
1484+
"/dev/sda",
1485+
]);
1486+
assert!(result.is_err());
1487+
1488+
let result = Opt::try_parse_from([
1489+
"bootc",
1490+
"install",
1491+
"to-disk",
1492+
"--progress-fd",
1493+
"2",
1494+
"/dev/sda",
1495+
]);
1496+
assert!(result.is_err());
1497+
1498+
let result = Opt::try_parse_from([
1499+
"bootc",
1500+
"install",
1501+
"to-disk",
1502+
"--progress-fd",
1503+
"invalid",
1504+
"/dev/sda",
1505+
]);
1506+
assert!(result.is_err());
1507+
}
14251508
}

crates/lib/src/install.rs

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ use serde::{Deserialize, Serialize};
5353
#[cfg(feature = "install-to-disk")]
5454
use self::baseline::InstallBlockDeviceOpts;
5555
use crate::boundimage::{BoundImage, ResolvedBoundImage};
56+
use crate::cli::ProgressOptions;
5657
use crate::containerenv::ContainerExecutionInfo;
5758
use crate::deploy::{prepare_for_pull, pull_from_prepared, PreparedImportMeta, PreparedPullResult};
5859
use crate::lsm;
@@ -242,6 +243,10 @@ pub(crate) struct InstallToDiskOpts {
242243
#[clap(long)]
243244
#[serde(default)]
244245
pub(crate) via_loopback: bool,
246+
247+
#[clap(flatten)]
248+
#[serde(flatten)]
249+
pub(crate) progress: ProgressOptions,
245250
}
246251

247252
#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -317,6 +322,9 @@ pub(crate) struct InstallToFilesystemOpts {
317322

318323
#[clap(flatten)]
319324
pub(crate) config_opts: InstallConfigOpts,
325+
326+
#[clap(flatten)]
327+
pub(crate) progress: ProgressOptions,
320328
}
321329

322330
#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
@@ -348,6 +356,9 @@ pub(crate) struct InstallToExistingRootOpts {
348356
/// via e.g. `-v /:/target`.
349357
#[clap(default_value = ALONGSIDE_ROOT_MOUNT)]
350358
pub(crate) root_path: Utf8PathBuf,
359+
360+
#[clap(flatten)]
361+
pub(crate) progress: ProgressOptions,
351362
}
352363

353364
/// Global state captured from the container.
@@ -755,6 +766,7 @@ async fn install_container(
755766
root_setup: &RootSetup,
756767
sysroot: &ostree::Sysroot,
757768
has_ostree: bool,
769+
prog: ProgressWriter,
758770
) -> Result<(ostree::Deployment, InstallAleph)> {
759771
let sepolicy = state.load_policy()?;
760772
let sepolicy = sepolicy.as_ref();
@@ -793,15 +805,14 @@ async fn install_container(
793805
let repo = &sysroot.repo();
794806
repo.set_disable_fsync(true);
795807

796-
let pulled_image = match prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref))
797-
.await?
798-
{
799-
PreparedPullResult::AlreadyPresent(existing) => existing,
800-
PreparedPullResult::Ready(image_meta) => {
801-
check_disk_space(root_setup.physical_root.as_fd(), &image_meta, &spec_imgref)?;
802-
pull_from_prepared(&spec_imgref, false, ProgressWriter::default(), image_meta).await?
803-
}
804-
};
808+
let pulled_image =
809+
match prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref)).await? {
810+
PreparedPullResult::AlreadyPresent(existing) => existing,
811+
PreparedPullResult::Ready(image_meta) => {
812+
check_disk_space(root_setup.physical_root.as_fd(), &image_meta, &spec_imgref)?;
813+
pull_from_prepared(&spec_imgref, false, prog, image_meta).await?
814+
}
815+
};
805816

806817
repo.set_disable_fsync(false);
807818

@@ -1335,10 +1346,11 @@ async fn install_with_sysroot(
13351346
bound_images: BoundImages,
13361347
has_ostree: bool,
13371348
imgstore: &crate::imgstorage::Storage,
1349+
prog: ProgressWriter,
13381350
) -> Result<()> {
13391351
// And actually set up the container in that root, returning a deployment and
13401352
// the aleph state (see below).
1341-
let (_deployment, aleph) = install_container(state, rootfs, &sysroot, has_ostree).await?;
1353+
let (_deployment, aleph) = install_container(state, rootfs, &sysroot, has_ostree, prog).await?;
13421354
// Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
13431355
rootfs
13441356
.physical_root
@@ -1420,6 +1432,7 @@ async fn install_to_filesystem_impl(
14201432
state: &State,
14211433
rootfs: &mut RootSetup,
14221434
cleanup: Cleanup,
1435+
prog: ProgressWriter,
14231436
) -> Result<()> {
14241437
if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) {
14251438
rootfs.kargs.push("selinux=0".to_string());
@@ -1461,6 +1474,7 @@ async fn install_to_filesystem_impl(
14611474
bound_images,
14621475
has_ostree,
14631476
&imgstore,
1477+
prog,
14641478
)
14651479
.await?;
14661480

@@ -1496,6 +1510,7 @@ fn installation_complete() {
14961510
#[context("Installing to disk")]
14971511
#[cfg(feature = "install-to-disk")]
14981512
pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
1513+
let prog: ProgressWriter = opts.progress.try_into()?;
14991514
let mut block_opts = opts.block_opts;
15001515
let target_blockdev_meta = block_opts
15011516
.device
@@ -1517,7 +1532,12 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
15171532
} else if !target_blockdev_meta.file_type().is_block_device() {
15181533
anyhow::bail!("Not a block device: {}", block_opts.device);
15191534
}
1520-
let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?;
1535+
let state = prepare_install(
1536+
opts.config_opts,
1537+
opts.source_opts,
1538+
opts.target_opts,
1539+
)
1540+
.await?;
15211541

15221542
// This is all blocking stuff
15231543
let (mut rootfs, loopback) = {
@@ -1538,7 +1558,7 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
15381558
(rootfs, loopback_dev)
15391559
};
15401560

1541-
install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip).await?;
1561+
install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip, prog).await?;
15421562

15431563
// Drop all data about the root except the bits we need to ensure any file descriptors etc. are closed.
15441564
let (root_path, luksdev) = rootfs.into_storage();
@@ -1720,12 +1740,18 @@ pub(crate) async fn install_to_filesystem(
17201740
targeting_host_root: bool,
17211741
cleanup: Cleanup,
17221742
) -> Result<()> {
1743+
let prog: ProgressWriter = opts.progress.try_into()?;
17231744
// Gather global state, destructuring the provided options.
17241745
// IMPORTANT: We might re-execute the current process in this function (for SELinux among other things)
17251746
// IMPORTANT: and hence anything that is done before MUST BE IDEMPOTENT.
17261747
// IMPORTANT: In practice, we should only be gathering information before this point,
17271748
// IMPORTANT: and not performing any mutations at all.
1728-
let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?;
1749+
let state = prepare_install(
1750+
opts.config_opts,
1751+
opts.source_opts,
1752+
opts.target_opts,
1753+
)
1754+
.await?;
17291755
// And the last bit of state here is the fsopts, which we also destructure now.
17301756
let mut fsopts = opts.filesystem_opts;
17311757

@@ -1924,7 +1950,7 @@ pub(crate) async fn install_to_filesystem(
19241950
skip_finalize,
19251951
};
19261952

1927-
install_to_filesystem_impl(&state, &mut rootfs, cleanup).await?;
1953+
install_to_filesystem_impl(&state, &mut rootfs, cleanup, prog).await?;
19281954

19291955
// Drop all data about the root except the path to ensure any file descriptors etc. are closed.
19301956
drop(rootfs);
@@ -1952,6 +1978,7 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
19521978
source_opts: opts.source_opts,
19531979
target_opts: opts.target_opts,
19541980
config_opts: opts.config_opts,
1981+
progress: opts.progress,
19551982
};
19561983

19571984
install_to_filesystem(opts, true, cleanup).await

crates/lib/src/progress_jsonl.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use anyhow::Result;
55
use canon_json::CanonJsonSerialize;
66
use schemars::JsonSchema;
7-
use serde::Serialize;
7+
use serde::{Deserialize, Serialize};
88
use std::borrow::Cow;
99
use std::os::fd::{FromRawFd, OwnedFd, RawFd};
1010
use std::str::FromStr;
@@ -137,7 +137,7 @@ pub enum Event<'t> {
137137
},
138138
}
139139

140-
#[derive(Debug, Clone, PartialEq, Eq)]
140+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141141
pub(crate) struct RawProgressFd(RawFd);
142142

143143
impl FromStr for RawProgressFd {
@@ -153,6 +153,13 @@ impl FromStr for RawProgressFd {
153153
}
154154
}
155155

156+
impl RawProgressFd {
157+
#[cfg(test)]
158+
pub(crate) fn get_raw_fd(&self) -> RawFd {
159+
self.0
160+
}
161+
}
162+
156163
#[derive(Debug)]
157164
struct ProgressWriterInner {
158165
/// true if we sent the initial Start message

0 commit comments

Comments
 (0)