Skip to content

Commit c5852ad

Browse files
committed
install: Add ensure-completion verb, wire up ostree-deploy → bootc
When bootc was created, it started to become a superset of ostree; in particular things like `/usr/lib/bootc/kargs.d` and logically bound images. However...Anaconda today is still invoking `ostree container image deploy`. Main fix -------- When bootc takes over the `/usr/libexec/ostree/ext/ostree-container` entrypoint, make the existing `ostree container image deploy` CLI actually just call back into bootc to fix things up. No additional work required other than getting an updated bootc in the Anaconda ISO. Old Anaconda ISOs ----------------- But, a further problem here is that Anaconda is only updated once per OS major+minor - e.g. there won't be an update to it for the lifetime of RHEL 9.5 or Fedora 41. We want the ability to ship new features and bugfixes in those OSes (especially RHEL9.5). So given that we have a newer bootc in the target container, we can do this: ``` %post --erroronfail bootc install ensure-completion %end ``` And will fix things up. Of course there's fun $details here...the way Anaconda implements `%post` is via a hand-augmented `chroot` i.e. a degenerate container, and we need to escape that and fix some things up (such as a missing cgroupfs mount). Summmary -------- - With a newer bootc in the ISO, everything just works - For older ISOs, one can add the `%post` above as a workaround. Implementation details: Cross-linking bootc and ostree-rs-ext ------------------------------------------------------------- This whole thing is very confusing because now, the linkage between bootc and ostree-rs-ext is bidirectional. In the case of `bootc install to-filesystem`, we end up calling into ostree-rs-ext, and we *must not* recurse back into bootc, because at least for kernel arguments we might end up applying them *twice*. We do this by passing a CLI argument. The second problem is the crate-level dependency; right now they're independent crates so we can't have ostree-rs-ext actually call into bootc directly, as convenient as that would be. So we end up forking ourselves as a subprocess. But that's not too bad because we need to carry a subprocess-based entrypoint *anyways* for the Anaconda `%post` case. Implementation details: /etc/resolv.conf ---------------------------------------- There's some surprising stuff going on in how Anaconda handles `/etc/resolv.conf` in the target root that I got burned by. In Fedora it's trying to query if systemd-resolved is enabled in the target or something? I ended up writing some code to just try to paper over this to ensure we have networking in the `%post` where we need it to fetch LBIs. Signed-off-by: Colin Walters <[email protected]>
1 parent d866f5c commit c5852ad

File tree

5 files changed

+378
-11
lines changed

5 files changed

+378
-11
lines changed

lib/src/boundimage.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
//! pre-pulled (and in the future, pinned) before a new image root
66
//! is considered ready.
77
8-
use std::num::NonZeroUsize;
9-
108
use anyhow::{Context, Result};
119
use camino::Utf8Path;
1210
use cap_std_ext::cap_std::fs::Dir;
@@ -49,7 +47,7 @@ pub(crate) async fn pull_bound_images(sysroot: &Storage, deployment: &Deployment
4947

5048
#[context("Querying bound images")]
5149
pub(crate) fn query_bound_images_for_deployment(
52-
sysroot: &Storage,
50+
sysroot: &ostree_ext::ostree::Sysroot,
5351
deployment: &Deployment,
5452
) -> Result<Vec<BoundImage>> {
5553
let deployment_root = &crate::utils::deployment_fd(sysroot, deployment)?;
@@ -153,15 +151,21 @@ pub(crate) async fn pull_images(
153151
sysroot: &Storage,
154152
bound_images: Vec<crate::boundimage::BoundImage>,
155153
) -> Result<()> {
156-
tracing::debug!("Pulling bound images: {}", bound_images.len());
157-
// Yes, the usage of NonZeroUsize here is...maybe odd looking, but I find
158-
// it an elegant way to divide (empty vector, non empty vector) since
159-
// we want to print the length too below.
160-
let Some(n) = NonZeroUsize::new(bound_images.len()) else {
161-
return Ok(());
162-
};
163154
// Only do work like initializing the image storage if we have images to pull.
155+
if bound_images.is_empty() {
156+
return Ok(());
157+
}
164158
let imgstore = sysroot.get_ensure_imgstore()?;
159+
pull_images_impl(imgstore, bound_images).await
160+
}
161+
162+
#[context("Pulling bound images")]
163+
pub(crate) async fn pull_images_impl(
164+
imgstore: &crate::imgstorage::Storage,
165+
bound_images: Vec<crate::boundimage::BoundImage>,
166+
) -> Result<()> {
167+
let n = bound_images.len();
168+
tracing::debug!("Pulling bound images: {n}");
165169
// TODO: do this in parallel
166170
for bound_image in bound_images {
167171
let image = &bound_image.image;

lib/src/cli.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,15 @@ pub(crate) enum InstallOpts {
182182
/// will be wiped, but the content of the existing root will otherwise be retained, and will
183183
/// need to be cleaned up if desired when rebooted into the new root.
184184
ToExistingRoot(crate::install::InstallToExistingRootOpts),
185+
/// Intended for use in environments that are performing an ostree-based installation, not bootc.
186+
///
187+
/// In this scenario the installation may be missing bootc specific features such as
188+
/// kernel arguments, logically bound images and more. This command can be used to attempt
189+
/// to reconcile. At the current time, the only tested environment is Anaconda using `ostreecontainer`
190+
/// and it is recommended to avoid usage outside of that environment. Instead, ensure your
191+
/// code is using `bootc install to-filesystem` from the start.
192+
#[clap(hide = true)]
193+
EnsureCompletion {},
185194
/// Output JSON to stdout that contains the merged installation configuration
186195
/// as it may be relevant to calling processes using `install to-filesystem`
187196
/// that in particular want to discover the desired root filesystem type from the container image.
@@ -346,6 +355,15 @@ pub(crate) enum InternalsOpts {
346355
#[clap(allow_hyphen_values = true)]
347356
args: Vec<OsString>,
348357
},
358+
#[cfg(feature = "install")]
359+
/// Invoked from ostree-ext to complete an installation.
360+
BootcInstallCompletion {
361+
/// Path to the sysroot
362+
sysroot: Utf8PathBuf,
363+
364+
// The stateroot
365+
stateroot: String,
366+
},
349367
}
350368

351369
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
@@ -989,6 +1007,10 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
9891007
crate::install::install_to_existing_root(opts).await
9901008
}
9911009
InstallOpts::PrintConfiguration => crate::install::print_configuration(),
1010+
InstallOpts::EnsureCompletion {} => {
1011+
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1012+
crate::install::completion::run_from_anaconda(rootfs).await
1013+
}
9921014
},
9931015
#[cfg(feature = "install")]
9941016
Opt::ExecInHostMountNamespace { args } => {
@@ -1026,6 +1048,11 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
10261048
let sysroot = get_storage().await?;
10271049
crate::deploy::cleanup(&sysroot).await
10281050
}
1051+
#[cfg(feature = "install")]
1052+
InternalsOpts::BootcInstallCompletion { sysroot, stateroot } => {
1053+
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1054+
crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await
1055+
}
10291056
},
10301057
#[cfg(feature = "docgen")]
10311058
Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),

lib/src/install.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// This sub-module is the "basic" installer that handles creating basic block device
88
// and filesystem setup.
99
pub(crate) mod baseline;
10+
pub(crate) mod completion;
1011
pub(crate) mod config;
1112
mod osbuild;
1213
pub(crate) mod osconfig;
@@ -762,6 +763,7 @@ async fn install_container(
762763
)?;
763764
let kargsd = kargsd.iter().map(|s| s.as_str());
764765

766+
// Keep this in sync with install/completion.rs for the Anaconda fixups
765767
let install_config_kargs = state
766768
.install_config
767769
.as_ref()
@@ -786,6 +788,7 @@ async fn install_container(
786788
options.kargs = Some(kargs.as_slice());
787789
options.target_imgref = Some(&state.target_imgref);
788790
options.proxy_cfg = proxy_cfg;
791+
options.skip_completion = true; // Must be set to avoid recursion!
789792
options.no_clean = has_ostree;
790793
let imgstate = crate::utils::async_task_with_spinner(
791794
"Deploying container image",
@@ -1383,7 +1386,7 @@ async fn install_with_sysroot(
13831386
}
13841387
}
13851388
BoundImages::Unresolved(bound_images) => {
1386-
crate::boundimage::pull_images(sysroot, bound_images)
1389+
crate::boundimage::pull_images_impl(imgstore, bound_images)
13871390
.await
13881391
.context("pulling bound images")?;
13891392
}

0 commit comments

Comments
 (0)