Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

install: Add ensure-completion verb, wire up ostree-deploy → bootc #915

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions lib/src/boundimage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
//! pre-pulled (and in the future, pinned) before a new image root
//! is considered ready.

use std::num::NonZeroUsize;

use anyhow::{Context, Result};
use camino::Utf8Path;
use cap_std_ext::cap_std::fs::Dir;
Expand Down Expand Up @@ -49,7 +47,7 @@ pub(crate) async fn pull_bound_images(sysroot: &Storage, deployment: &Deployment

#[context("Querying bound images")]
pub(crate) fn query_bound_images_for_deployment(
sysroot: &Storage,
sysroot: &ostree_ext::ostree::Sysroot,
deployment: &Deployment,
) -> Result<Vec<BoundImage>> {
let deployment_root = &crate::utils::deployment_fd(sysroot, deployment)?;
Expand Down Expand Up @@ -153,15 +151,21 @@ pub(crate) async fn pull_images(
sysroot: &Storage,
bound_images: Vec<crate::boundimage::BoundImage>,
) -> Result<()> {
tracing::debug!("Pulling bound images: {}", bound_images.len());
// Yes, the usage of NonZeroUsize here is...maybe odd looking, but I find
// it an elegant way to divide (empty vector, non empty vector) since
// we want to print the length too below.
let Some(n) = NonZeroUsize::new(bound_images.len()) else {
return Ok(());
};
// Only do work like initializing the image storage if we have images to pull.
if bound_images.is_empty() {
return Ok(());
}
let imgstore = sysroot.get_ensure_imgstore()?;
pull_images_impl(imgstore, bound_images).await
}

#[context("Pulling bound images")]
pub(crate) async fn pull_images_impl(
imgstore: &crate::imgstorage::Storage,
bound_images: Vec<crate::boundimage::BoundImage>,
) -> Result<()> {
let n = bound_images.len();
tracing::debug!("Pulling bound images: {n}");
// TODO: do this in parallel
for bound_image in bound_images {
let image = &bound_image.image;
Expand Down
113 changes: 106 additions & 7 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
//!
//! Command line tool to manage bootable ostree-based containers.

use std::ffi::CString;
use std::ffi::OsString;
use std::ffi::{CString, OsStr, OsString};
use std::io::Seek;
use std::os::unix::process::CommandExt;
use std::process::Command;
Expand Down Expand Up @@ -183,6 +182,24 @@ pub(crate) enum InstallOpts {
/// will be wiped, but the content of the existing root will otherwise be retained, and will
/// need to be cleaned up if desired when rebooted into the new root.
ToExistingRoot(crate::install::InstallToExistingRootOpts),
/// Intended for use in environments that are performing an ostree-based installation, not bootc.
///
/// In this scenario the installation may be missing bootc specific features such as
/// kernel arguments, logically bound images and more. This command can be used to attempt
/// to reconcile. At the current time, the only tested environment is Anaconda using `ostreecontainer`
/// and it is recommended to avoid usage outside of that environment. Instead, ensure your
/// code is using `bootc install to-filesystem` from the start.
#[clap(hide = true)]
EnsureCompletion {
/// When provided, we assume that we're being invoked from our own
/// ostree-ext codebase.
#[clap(long)]
sysroot: Option<Utf8PathBuf>,

/// Must be set if sysroot is set
#[clap(long)]
stateroot: Option<String>,
},
/// Output JSON to stdout that contains the merged installation configuration
/// as it may be relevant to calling processes using `install to-filesystem`
/// that in particular want to discover the desired root filesystem type from the container image.
Expand Down Expand Up @@ -879,6 +896,18 @@ where
run_from_opt(Opt::parse_including_static(args)).await
}

/// Find the base binary name from argv0 (without a full path). The empty string
/// is never returned; instead a fallback string is used.
fn callname_from_argv0(argv0: &OsStr) -> &str {
let default = "bootc";
let argv0 = argv0.to_str().filter(|s| !s.is_empty()).unwrap_or(default);
argv0
.rsplit_once('/')
.map(|s| s.1)
.filter(|s| !s.is_empty())
.unwrap_or(argv0)
}

impl Opt {
/// In some cases (e.g. systemd generator) we dispatch specifically on argv0. This
/// requires some special handling in clap.
Expand All @@ -890,12 +919,19 @@ impl Opt {
let mut args = args.into_iter();
let first = if let Some(first) = args.next() {
let first: OsString = first.into();
let argv0 = first.to_str().and_then(|s| s.rsplit_once('/')).map(|s| s.1);
let argv0 = callname_from_argv0(&first);
tracing::debug!("argv0={argv0:?}");
if matches!(argv0, Some(InternalsOpts::GENERATOR_BIN)) {
let base_args = ["bootc", "internals", "systemd-generator"]
.into_iter()
.map(OsString::from);
let mapped = match argv0 {
InternalsOpts::GENERATOR_BIN => {
Some(["bootc", "internals", "systemd-generator"].as_slice())
}
"ostree-container" | "ostree-ima-sign" | "ostree-provisional-repair" => {
Some(["bootc", "internals", "ostree-ext"].as_slice())
}
_ => None,
};
if let Some(base_args) = mapped {
let base_args = base_args.into_iter().map(OsString::from);
return Opt::parse_from(base_args.chain(args.map(|i| i.into())));
}
Some(first)
Expand Down Expand Up @@ -971,6 +1007,17 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
crate::install::install_to_existing_root(opts).await
}
InstallOpts::PrintConfiguration => crate::install::print_configuration(),
InstallOpts::EnsureCompletion { sysroot, stateroot } => {
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
if let Some(sysroot) = sysroot {
let stateroot = stateroot.as_deref().ok_or_else(|| {
anyhow::anyhow!("Expected stateroot when --sysroot is set")
})?;
crate::install::completion::run_from_ostree(rootfs, &sysroot, stateroot).await
} else {
crate::install::completion::run_from_anaconda(rootfs).await
}
}
},
#[cfg(feature = "install")]
Opt::ExecInHostMountNamespace { args } => {
Expand Down Expand Up @@ -1022,6 +1069,31 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
}
}

#[test]
fn test_callname() {
let mapped_cases = [
("", "bootc"),
("/foo/bar", "bar"),
("../foo/bar", "bar"),
("usr/bin/ostree-container", "ostree-container"),
];
for (input, output) in mapped_cases {
assert_eq!(
output,
callname_from_argv0(OsStr::new(input)),
"Handling mapped case {input}"
);
}
let ident_cases = ["foo", "bootc"];
for case in ident_cases {
assert_eq!(
case,
callname_from_argv0(OsStr::new(case)),
"Handling ident case {case}"
);
}
}

#[test]
fn test_parse_install_args() {
// Verify we still process the legacy --target-no-signature-verification
Expand Down Expand Up @@ -1083,4 +1155,31 @@ fn test_parse_ostree_ext() {
Opt::parse_including_static(["bootc", "internals", "ostree-container"]),
Opt::Internals(InternalsOpts::OstreeContainer { .. })
));

fn peel(o: Opt) -> Vec<OsString> {
match o {
Opt::Internals(InternalsOpts::OstreeExt { args }) => args,
o => panic!("unexpected {o:?}"),
}
}
let args = peel(Opt::parse_including_static([
"/usr/libexec/libostree/ext/ostree-ima-sign",
"ima-sign",
"--repo=foo",
"foo",
"bar",
"baz",
]));
assert_eq!(
args.as_slice(),
["ima-sign", "--repo=foo", "foo", "bar", "baz"]
);

let args = peel(Opt::parse_including_static([
"/usr/libexec/libostree/ext/ostree-container",
"container",
"image",
"pull",
]));
assert_eq!(args.as_slice(), ["container", "image", "pull"]);
}
5 changes: 4 additions & 1 deletion lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// This sub-module is the "basic" installer that handles creating basic block device
// and filesystem setup.
pub(crate) mod baseline;
pub(crate) mod completion;
pub(crate) mod config;
mod osbuild;
pub(crate) mod osconfig;
Expand Down Expand Up @@ -762,6 +763,7 @@ async fn install_container(
)?;
let kargsd = kargsd.iter().map(|s| s.as_str());

// Keep this in sync with install/completion.rs for the Anaconda fixups
let install_config_kargs = state
.install_config
.as_ref()
Expand All @@ -786,6 +788,7 @@ async fn install_container(
options.kargs = Some(kargs.as_slice());
options.target_imgref = Some(&state.target_imgref);
options.proxy_cfg = proxy_cfg;
options.skip_completion = true; // Must be set to avoid recursion!
options.no_clean = has_ostree;
let imgstate = crate::utils::async_task_with_spinner(
"Deploying container image",
Expand Down Expand Up @@ -1383,7 +1386,7 @@ async fn install_with_sysroot(
}
}
BoundImages::Unresolved(bound_images) => {
crate::boundimage::pull_images(sysroot, bound_images)
crate::boundimage::pull_images_impl(imgstore, bound_images)
.await
.context("pulling bound images")?;
}
Expand Down
Loading