Skip to content
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
19 changes: 19 additions & 0 deletions crates/lib/src/bootc_kargs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ use crate::store::Storage;
/// The relative path to the kernel arguments which may be embedded in an image.
const KARGS_PATH: &str = "usr/lib/bootc/kargs.d";

/// The default root filesystem mount specification.
pub(crate) const ROOT: &str = "root=";
/// This is used by dracut.
pub(crate) const INITRD_ARG_PREFIX: &str = "rd.";
/// The kernel argument for configuring the rootfs flags.
pub(crate) const ROOTFLAGS: &str = "rootflags=";

/// The kargs.d configuration file.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
Expand Down Expand Up @@ -54,6 +61,18 @@ pub(crate) fn get_kargs_in_root(d: &Dir, sys_arch: &str) -> Result<Vec<String>>
Ok(ret)
}

pub(crate) fn root_args_from_cmdline<'a>(cmdline: &'a [&str]) -> Vec<&'a str> {
cmdline
.iter()
.filter(|arg| {
arg.starts_with(ROOT)
|| arg.starts_with(ROOTFLAGS)
|| arg.starts_with(INITRD_ARG_PREFIX)
})
.copied()
.collect()
}

/// Load kargs.d files from the target ostree commit root
pub(crate) fn get_kargs_from_ostree_root(
repo: &ostree::Repo,
Expand Down
20 changes: 15 additions & 5 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use crate::bootc_composefs::{
finalize::composefs_native_finalize, rollback::composefs_rollback, status::composefs_booted,
switch::switch_composefs, update::upgrade_composefs,
};
use crate::deploy::RequiredHostSpec;
use crate::deploy::{MergeState, RequiredHostSpec};
use crate::lints;
use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
use crate::spec::Host;
Expand Down Expand Up @@ -252,6 +252,12 @@ 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),
/// Nondestructively create a fresh installation state inside an existing bootc system.
///
/// This is a nondestructive variant of `install to-existing-root` that works only inside
/// an existing bootc system.
#[clap(hide = true)]
Reset(crate::install::InstallResetOpts),
/// Execute this as the penultimate step of an installation using `install to-filesystem`.
///
Finalize {
Expand Down Expand Up @@ -943,8 +949,9 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
} else if booted_unchanged {
println!("No update available.")
} else {
let osname = booted_deployment.osname();
crate::deploy::stage(sysroot, &osname, &fetched, &spec, prog.clone()).await?;
let stateroot = booted_deployment.osname();
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;
changed = true;
if let Some(prev) = booted_image.as_ref() {
if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
Expand Down Expand Up @@ -1063,7 +1070,8 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
}

let stateroot = booted_deployment.osname();
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;

sysroot.update_mtime()?;

Expand Down Expand Up @@ -1142,7 +1150,8 @@ async fn edit(opts: EditOpts) -> Result<()> {
// TODO gc old layers here

let stateroot = booted_deployment.osname();
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;

sysroot.update_mtime()?;

Expand Down Expand Up @@ -1363,6 +1372,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
InstallOpts::ToExistingRoot(opts) => {
crate::install::install_to_existing_root(opts).await
}
InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
InstallOpts::PrintConfiguration => crate::install::print_configuration(),
InstallOpts::EnsureCompletion {} => {
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
Expand Down
81 changes: 57 additions & 24 deletions crates/lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,25 +577,26 @@ pub(crate) fn get_base_commit(repo: &ostree::Repo, commit: &str) -> Result<Optio
#[context("Writing deployment")]
async fn deploy(
sysroot: &Storage,
merge_deployment: Option<&Deployment>,
stateroot: &str,
from: MergeState,
image: &ImageState,
origin: &glib::KeyFile,
) -> Result<Deployment> {
// Compute the kernel argument overrides. In practice today this API is always expecting
// a merge deployment. The kargs code also always looks at the booted root (which
// is a distinct minor issue, but not super important as right now the install path
// doesn't use this API).
let override_kargs = if let Some(deployment) = merge_deployment {
Some(crate::bootc_kargs::get_kargs(sysroot, &deployment, image)?)
} else {
None
let (stateroot, override_kargs) = match &from {
MergeState::MergeDeployment(deployment) => {
let kargs = crate::bootc_kargs::get_kargs(sysroot, &deployment, image)?;
(deployment.stateroot().into(), kargs)
}
MergeState::Reset { stateroot, kargs } => (stateroot.clone(), kargs.clone()),
};
// Clone all the things to move to worker thread
let ostree = sysroot.get_ostree_cloned()?;
// ostree::Deployment is incorrectly !Send 😢 so convert it to an integer
let merge_deployment = from.as_merge_deployment();
let merge_deployment = merge_deployment.map(|d| d.index() as usize);
let stateroot = stateroot.to_string();
let ostree_commit = image.ostree_commit.to_string();
// GKeyFile also isn't Send! So we serialize that as a string...
let origin_data = origin.to_data();
Expand All @@ -609,11 +610,10 @@ async fn deploy(
// Because the C API expects a Vec<&str>, we need to generate a new Vec<>
// that borrows.
let override_kargs = override_kargs
.as_deref()
.map(|v| v.iter().map(|s| s.as_str()).collect::<Vec<_>>());
if let Some(kargs) = override_kargs.as_deref() {
opts.override_kernel_argv = Some(&kargs);
}
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>();
opts.override_kernel_argv = Some(&override_kargs);
let deployments = ostree.deployments();
let merge_deployment = merge_deployment.map(|m| &deployments[m]);
let origin = glib::KeyFile::new();
Expand All @@ -634,6 +634,18 @@ async fn deploy(
let ostree = sysroot.get_ostree()?;
let staged = ostree.staged_deployment().unwrap();
assert_eq!(staged.index(), r);

// This file is used to signal to other CLI commands that this deployment is a factory reset
// (e.g. bootc status)
if matches!(from, MergeState::Reset { .. }) {
let deployment_dir = ostree.deployment_dirpath(&staged);
let deployment_dir = std::path::Path::new(deployment_dir.as_str());
if deployment_dir.exists() {
let marker_path = deployment_dir.join(".bootc-factory-reset");
Copy link
Collaborator

@cgwalters cgwalters Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have clear const for things like this with an explanation there.

But backing up a second, in the ostree side we have "deployment backing" directories already where we can put metadata.

Though of course we should also be thinking about how this would work with the composefs backend.

Hmmm hmm. Here's an idea, how about we use the deployment's etc directory for state like this instead? I think we could save e.g. system.bootc.merged_from xattr or so?

Basically I lean towards extended attributes over "stamp files" for metadata.

BTW though do note the intersection here with etc.transient - I wonder if we should make that more visible in status output too. If that's enabled each upgrade is already like a reset of /etc.

std::fs::write(&marker_path, b"")?;
}
}

Ok(staged)
}

Expand All @@ -649,11 +661,42 @@ fn origin_from_imageref(imgref: &ImageReference) -> Result<glib::KeyFile> {
Ok(origin)
}

/// The source of data for staging a new deployment
#[derive(Debug)]
pub(crate) enum MergeState {
/// Use the provided merge deployment
MergeDeployment(Deployment),
/// Don't use a merge deployment, but only this
/// provided initial state.
Reset {
stateroot: String,
kargs: Vec<String>,
},
}
impl MergeState {
/// Initialize using the default merge deployment for the given stateroot.
pub(crate) fn from_stateroot(sysroot: &Storage, stateroot: &str) -> Result<Self> {
let ostree = sysroot.get_ostree()?;
let merge_deployment = ostree.merge_deployment(Some(stateroot)).ok_or_else(|| {
anyhow::anyhow!("No merge deployment found for stateroot {stateroot}")
})?;
Ok(Self::MergeDeployment(merge_deployment))
}

/// Cast this to a merge deployment case.
pub(crate) fn as_merge_deployment(&self) -> Option<&Deployment> {
match self {
Self::MergeDeployment(d) => Some(d),
Self::Reset { .. } => None,
}
}
}

/// Stage (queue deployment of) a fetched container image.
#[context("Staging")]
pub(crate) async fn stage(
sysroot: &Storage,
stateroot: &str,
from: MergeState,
image: &ImageState,
spec: &RequiredHostSpec<'_>,
prog: ProgressWriter,
Expand All @@ -666,13 +709,11 @@ pub(crate) async fn stage(
bootc.image.reference = &spec.image.image,
bootc.image.transport = &spec.image.transport,
bootc.manifest_digest = image.manifest_digest.as_ref(),
bootc.stateroot = stateroot,
"Staging image for deployment: {} (digest: {})",
spec.image,
image.manifest_digest
);

let ostree = sysroot.get_ostree()?;
let mut subtask = SubTaskStep {
subtask: "merging".into(),
description: "Merging Image".into(),
Expand All @@ -694,7 +735,6 @@ pub(crate) async fn stage(
.collect(),
})
.await;
let merge_deployment = ostree.merge_deployment(Some(stateroot));

subtask.completed = true;
subtasks.push(subtask.clone());
Expand All @@ -717,14 +757,7 @@ pub(crate) async fn stage(
})
.await;
let origin = origin_from_imageref(spec.image)?;
let deployment = crate::deploy::deploy(
sysroot,
merge_deployment.as_ref(),
stateroot,
image,
&origin,
)
.await?;
let deployment = crate::deploy::deploy(sysroot, from, image, &origin).await?;

subtask.completed = true;
subtasks.push(subtask.clone());
Expand Down
Loading
Loading