Skip to content

Composefs backend #1314

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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use fn_error_context::context;
use indoc::indoc;
use ostree::gio;
use ostree_container::store::PrepareResult;
use ostree_ext::composefs::fsverity;
use ostree_ext::composefs::fsverity::{self, FsVerityHashValue};
use ostree_ext::container as ostree_container;
use ostree_ext::container_utils::ostree_booted;
use ostree_ext::keyfileext::KeyFileExt;
Expand Down Expand Up @@ -1199,7 +1199,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
let fd =
std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
let digest: fsverity::Sha256HashValue = fsverity::measure_verity(&fd)?;
let digest = hex::encode(digest);
let digest = digest.to_hex();
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this should be squashed with the previous commit, and it may make sense to have a "bump composefs-rs" prep PR split out right?

Copy link
Author

Choose a reason for hiding this comment

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

Right, that makes sense. I'll put this in a separate PR

println!("{digest}");
Ok(())
}
Expand Down
174 changes: 147 additions & 27 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod osbuild;
pub(crate) mod osconfig;

use std::collections::HashMap;
use std::fs::create_dir_all;
use std::io::Write;
use std::os::fd::{AsFd, AsRawFd};
use std::os::unix::process::CommandExt;
Expand All @@ -38,12 +39,22 @@ use chrono::prelude::*;
use clap::ValueEnum;
use fn_error_context::context;
use ostree::gio;
use ostree_ext::composefs::{
fsverity::{FsVerityHashValue, Sha256HashValue},
oci::image::create_filesystem as create_composefs_filesystem,
oci::pull as composefs_oci_pull,
repository::Repository as ComposefsRepository,
util::Sha256Digest,
write_boot::write_boot_simple as composefs_write_boot_simple,
};
use ostree_ext::oci_spec;
use ostree_ext::ostree;
use ostree_ext::ostree_prepareroot::{ComposefsState, Tristate};
use ostree_ext::prelude::Cast;
use ostree_ext::sysroot::SysrootLock;
use ostree_ext::{container as ostree_container, ostree_prepareroot};
use ostree_ext::{
container as ostree_container, container::ImageReference as OstreeExtImgRef, ostree_prepareroot,
};
#[cfg(feature = "install-to-disk")]
use rustix::fs::FileTypeExt;
use rustix::fs::MetadataExt as _;
Expand Down Expand Up @@ -241,6 +252,9 @@ pub(crate) struct InstallToDiskOpts {
#[clap(long)]
#[serde(default)]
pub(crate) via_loopback: bool,

#[clap(long)]
pub(crate) composefs: bool,
}

#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -363,6 +377,7 @@ pub(crate) struct SourceInfo {
}

// Shared read-only global state
#[derive(Debug)]
pub(crate) struct State {
pub(crate) source: SourceInfo,
/// Force SELinux off in target system
Expand Down Expand Up @@ -1425,10 +1440,102 @@ impl BoundImages {
}
}

fn open_composefs_repo(rootfs_dir: &Dir) -> Result<ComposefsRepository<Sha256HashValue>> {
ComposefsRepository::open_path(rootfs_dir, "composefs")
.context("Failed to open composefs repository")
}

async fn initialize_composefs_repository(
state: &State,
root_setup: &RootSetup,
) -> Result<(Sha256Digest, impl FsVerityHashValue)> {
let rootfs_dir = &root_setup.physical_root;

rootfs_dir
.create_dir_all("composefs")
.context("Creating dir 'composefs'")?;

tracing::warn!("STATE: {state:#?}");
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd recommend dbg! for this type of stuff; it will build locally but not pass CI so it can't be accidentally merged.

Copy link
Author

Choose a reason for hiding this comment

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

I intend to remove all these logs. They were only for debugging purposes

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yep that makes sense, what I meant is that use dbg! locally. (Or use tracing::debug! and keep the tracing in there and just use the tracing selector to ensure you see what you want...we are really chatty with a big env RUST_LOG=debug unfortunately today, mostly in container fetches)


let repo = open_composefs_repo(rootfs_dir)?;

let OstreeExtImgRef { transport, name } = &state.target_imgref.imgref;

// transport's display is already of type "<transport_type>:"
composefs_oci_pull(&Arc::new(repo), &format!("{transport}{name}",), None).await
}

#[context("Setting up composefs boot")]
fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) -> Result<()> {
let boot_uuid = root_setup
.get_boot_uuid()?
.or(root_setup.rootfs_uuid.as_deref())
.ok_or_else(|| anyhow!("No uuid for boot/root"))?;

if cfg!(target_arch = "s390x") {
// TODO: Integrate s390x support into install_via_bootupd
crate::bootloader::install_via_zipl(&root_setup.device_info, boot_uuid)?;
} else {
crate::bootloader::install_via_bootupd(
&root_setup.device_info,
&root_setup.physical_root_path,
&state.config_opts,
)?;
}

let repo = open_composefs_repo(&root_setup.physical_root)?;

let mut fs = create_composefs_filesystem(&repo, image_id, None)?;

let entries = fs.transform_for_boot(&repo)?;
let id = fs.commit_image(&repo, None)?;

println!("{entries:#?}");

let Some(entry) = entries.into_iter().next() else {
anyhow::bail!("No boot entries!");
};

let rootfs_uuid = match &root_setup.rootfs_uuid {
Some(u) => u,
None => anyhow::bail!("Expected rootfs to have a UUID by now"),
};

let cmdline_refs = [
"console=ttyS0,115200",
&format!("root=UUID={rootfs_uuid}"),
"rw",
];

let boot_dir = root_setup.physical_root_path.join("boot");
create_dir_all(&boot_dir).context("Failed to create boot dir")?;

composefs_write_boot_simple(
&repo,
entry,
&id,
boot_dir.as_std_path(),
Some(&format!("{}", id.to_hex())),
Some("/boot"),
&cmdline_refs,
)?;

let state_path = root_setup
.physical_root_path
.join(format!("state/{}", id.to_hex()));

create_dir_all(state_path.join("var"))?;
create_dir_all(state_path.join("etc/upper"))?;
create_dir_all(state_path.join("etc/work"))?;

Ok(())
}

async fn install_to_filesystem_impl(
state: &State,
rootfs: &mut RootSetup,
cleanup: Cleanup,
composefs: bool,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this would make sense as part of state

Copy link
Author

Choose a reason for hiding this comment

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

I was conflicted on where to put this. Thanks for the review. I'll add this to State

) -> Result<()> {
if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) {
rootfs.kargs.push("selinux=0".to_string());
Expand Down Expand Up @@ -1457,34 +1564,47 @@ async fn install_to_filesystem_impl(

let bound_images = BoundImages::from_state(state).await?;

// Initialize the ostree sysroot (repo, stateroot, etc.)
if composefs {
// Load a fd for the mounted target physical root
let (id, verity) = initialize_composefs_repository(state, rootfs).await?;

{
let (sysroot, has_ostree, imgstore) = initialize_ostree_root(state, rootfs).await?;

install_with_sysroot(
state,
rootfs,
&sysroot,
&boot_uuid,
bound_images,
has_ostree,
&imgstore,
)
.await?;
tracing::warn!(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same comment re dbg! or this one could also make sense as a tracing::debug! that gets left in right?

Copy link
Author

Choose a reason for hiding this comment

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

Ah, yes. We can keep this one as debug

"id = {id}, verity = {verity}",
id = hex::encode(id),
verity = verity.to_hex()
);

if matches!(cleanup, Cleanup::TriggerOnNextBoot) {
let sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}");
sysroot_dir.atomic_write(format!("etc/{}", DESTRUCTIVE_CLEANUP), b"")?;
}
setup_composefs_boot(rootfs, state, &hex::encode(id))?;
} else {
// Initialize the ostree sysroot (repo, stateroot, etc.)

{
let (sysroot, has_ostree, imgstore) = initialize_ostree_root(state, rootfs).await?;

install_with_sysroot(
state,
rootfs,
&sysroot,
&boot_uuid,
bound_images,
has_ostree,
&imgstore,
)
.await?;

// We must drop the sysroot here in order to close any open file
// descriptors.
};
if matches!(cleanup, Cleanup::TriggerOnNextBoot) {
let sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}");
sysroot_dir.atomic_write(format!("etc/{}", DESTRUCTIVE_CLEANUP), b"")?;
}

// Run this on every install as the penultimate step
install_finalize(&rootfs.physical_root_path).await?;
// We must drop the sysroot here in order to close any open file
// descriptors.
};

// Run this on every install as the penultimate step
install_finalize(&rootfs.physical_root_path).await?;
}

// Finalize mounted filesystems
if !rootfs.skip_finalize {
Expand Down Expand Up @@ -1547,7 +1667,7 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
(rootfs, loopback_dev)
};

install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip).await?;
install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip, opts.composefs).await?;

// Drop all data about the root except the bits we need to ensure any file descriptors etc. are closed.
let (root_path, luksdev) = rootfs.into_storage();
Expand Down Expand Up @@ -1933,7 +2053,7 @@ pub(crate) async fn install_to_filesystem(
skip_finalize,
};

install_to_filesystem_impl(&state, &mut rootfs, cleanup).await?;
install_to_filesystem_impl(&state, &mut rootfs, cleanup, false).await?;

// Drop all data about the root except the path to ensure any file descriptors etc. are closed.
drop(rootfs);
Expand Down
4 changes: 2 additions & 2 deletions ostree-ext/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ version = "0.15.3"
# Note that we re-export the oci-spec types
# that are exported by this crate, so when bumping
# semver here you must also bump our semver.
containers-image-proxy = "0.7.0"
containers-image-proxy = "0.7.1"
# We re-export this library too.
ostree = { features = ["v2025_1"], version = "0.20.0" }

# Private dependencies
anyhow = { workspace = true }
bootc-utils = { path = "../utils" }
camino = { workspace = true, features = ["serde1"] }
composefs = { git = "https://github.com/containers/composefs-rs", rev = "821eeae93e48f1ee381c49b8cd4d22fda92d27a2" }
composefs = { git = "https://github.com/containers/composefs-rs", rev = "b1bd9c1d253c9b15bbf8e04068ad96edd974ccd0" }
chrono = { workspace = true }
olpc-cjson = "0.1.1"
clap = { workspace = true, features = ["derive","cargo"] }
Expand Down
Loading