Skip to content

Commit f282db3

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 f282db3

File tree

3 files changed

+137
-17
lines changed

3 files changed

+137
-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: 44 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

@@ -1174,6 +1185,7 @@ async fn prepare_install(
11741185
config_opts: InstallConfigOpts,
11751186
source_opts: InstallSourceOpts,
11761187
target_opts: InstallTargetOpts,
1188+
_prog: ProgressWriter,
11771189
) -> Result<Arc<State>> {
11781190
tracing::trace!("Preparing install");
11791191
let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
@@ -1335,10 +1347,11 @@ async fn install_with_sysroot(
13351347
bound_images: BoundImages,
13361348
has_ostree: bool,
13371349
imgstore: &crate::imgstorage::Storage,
1350+
prog: ProgressWriter,
13381351
) -> Result<()> {
13391352
// And actually set up the container in that root, returning a deployment and
13401353
// the aleph state (see below).
1341-
let (_deployment, aleph) = install_container(state, rootfs, &sysroot, has_ostree).await?;
1354+
let (_deployment, aleph) = install_container(state, rootfs, &sysroot, has_ostree, prog).await?;
13421355
// Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
13431356
rootfs
13441357
.physical_root
@@ -1420,6 +1433,7 @@ async fn install_to_filesystem_impl(
14201433
state: &State,
14211434
rootfs: &mut RootSetup,
14221435
cleanup: Cleanup,
1436+
prog: ProgressWriter,
14231437
) -> Result<()> {
14241438
if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) {
14251439
rootfs.kargs.push("selinux=0".to_string());
@@ -1461,6 +1475,7 @@ async fn install_to_filesystem_impl(
14611475
bound_images,
14621476
has_ostree,
14631477
&imgstore,
1478+
prog,
14641479
)
14651480
.await?;
14661481

@@ -1496,6 +1511,7 @@ fn installation_complete() {
14961511
#[context("Installing to disk")]
14971512
#[cfg(feature = "install-to-disk")]
14981513
pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
1514+
let prog: ProgressWriter = opts.progress.try_into()?;
14991515
let mut block_opts = opts.block_opts;
15001516
let target_blockdev_meta = block_opts
15011517
.device
@@ -1517,7 +1533,13 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
15171533
} else if !target_blockdev_meta.file_type().is_block_device() {
15181534
anyhow::bail!("Not a block device: {}", block_opts.device);
15191535
}
1520-
let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?;
1536+
let state = prepare_install(
1537+
opts.config_opts,
1538+
opts.source_opts,
1539+
opts.target_opts,
1540+
prog.clone(),
1541+
)
1542+
.await?;
15211543

15221544
// This is all blocking stuff
15231545
let (mut rootfs, loopback) = {
@@ -1538,7 +1560,7 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
15381560
(rootfs, loopback_dev)
15391561
};
15401562

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

15431565
// Drop all data about the root except the bits we need to ensure any file descriptors etc. are closed.
15441566
let (root_path, luksdev) = rootfs.into_storage();
@@ -1720,12 +1742,19 @@ pub(crate) async fn install_to_filesystem(
17201742
targeting_host_root: bool,
17211743
cleanup: Cleanup,
17221744
) -> Result<()> {
1745+
let prog: ProgressWriter = opts.progress.try_into()?;
17231746
// Gather global state, destructuring the provided options.
17241747
// IMPORTANT: We might re-execute the current process in this function (for SELinux among other things)
17251748
// IMPORTANT: and hence anything that is done before MUST BE IDEMPOTENT.
17261749
// IMPORTANT: In practice, we should only be gathering information before this point,
17271750
// IMPORTANT: and not performing any mutations at all.
1728-
let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?;
1751+
let state = prepare_install(
1752+
opts.config_opts,
1753+
opts.source_opts,
1754+
opts.target_opts,
1755+
prog.clone(),
1756+
)
1757+
.await?;
17291758
// And the last bit of state here is the fsopts, which we also destructure now.
17301759
let mut fsopts = opts.filesystem_opts;
17311760

@@ -1924,7 +1953,7 @@ pub(crate) async fn install_to_filesystem(
19241953
skip_finalize,
19251954
};
19261955

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

19291958
// Drop all data about the root except the path to ensure any file descriptors etc. are closed.
19301959
drop(rootfs);
@@ -1952,6 +1981,7 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
19521981
source_opts: opts.source_opts,
19531982
target_opts: opts.target_opts,
19541983
config_opts: opts.config_opts,
1984+
progress: opts.progress,
19551985
};
19561986

19571987
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)