Skip to content
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
83 changes: 50 additions & 33 deletions crates/lib/src/bootc_composefs/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,27 @@ use composefs_ctl::composefs;
use composefs_ctl::composefs_oci;
use composefs_oci::open_config;
use ocidir::{OciDir, oci_spec::image::Platform};
use ostree_ext::container::ImageReference;
use ostree_ext::container::Transport;
use ostree_ext::container::skopeo;
use tar::EntryType;

use crate::image::get_imgrefs_for_copy;
use crate::{
bootc_composefs::status::{get_composefs_status, get_imginfo},
store::{BootedComposefs, Storage},
bootc_composefs::status::{ImgConfigManifest, get_composefs_status, get_imginfo},
store::{BootedComposefs, ComposefsRepository, Storage},
};

/// Exports a composefs repository to a container image in containers-storage:
pub async fn export_repo_to_image(
storage: &Storage,
booted_cfs: &BootedComposefs,
source: Option<&str>,
target: Option<&str>,
/// Streams a composefs OCI image out to a destination image reference.
///
/// Given a composefs repository handle and image metadata (manifest + config),
/// reconstructs the container image by reading layer data from the composefs
/// splitstreams and copies the assembled OCI image to `dest_imgref` via skopeo.
pub(crate) async fn export_composefs_to_dest(
composefs_repo: &ComposefsRepository,
imginfo: &ImgConfigManifest,
dest_imgref: &ImageReference,
) -> Result<()> {
let host = get_composefs_status(storage, booted_cfs).await?;

let (source, dest_imgref) = get_imgrefs_for_copy(&host, source, target).await?;

let mut depl_verity = None;

for depl in host.list_deployments() {
let img = &depl.image.as_ref().unwrap().image;

// Not checking transport here as we'll be pulling from the repo anyway
// So, image name is all we need
if img.image == source.name {
depl_verity = Some(depl.require_composefs()?.verity.clone());
break;
}
}

let depl_verity = depl_verity.ok_or_else(|| anyhow::anyhow!("Image {source} not found"))?;

let imginfo = get_imginfo(storage, &depl_verity)?;

let config_digest = imginfo.manifest.config().digest().clone();

let var_tmp =
Expand All @@ -54,7 +37,7 @@ pub async fn export_repo_to_image(
let oci_dir = OciDir::ensure(tmpdir.try_clone()?).context("Opening OCI")?;

// Use composefs_oci::open_config to get the config and layer map
let open = open_config(&*booted_cfs.repo, &config_digest, None).context("Opening config")?;
let open = open_config(composefs_repo, &config_digest, None).context("Opening config")?;
let config = open.config;
let layer_map = open.layer_refs;

Expand All @@ -77,7 +60,7 @@ pub async fn export_repo_to_image(
.get(old_diff_id.as_str())
.ok_or_else(|| anyhow::anyhow!("Layer {old_diff_id} not found in config"))?;

let mut layer_stream = booted_cfs.repo.open_stream("", Some(layer_verity), None)?;
let mut layer_stream = composefs_repo.open_stream("", Some(layer_verity), None)?;

let mut layer_writer = oci_dir.create_layer(None)?;
layer_writer.follow_symlinks(false);
Expand Down Expand Up @@ -113,7 +96,7 @@ pub async fn export_repo_to_image(
match layer_stream.read_exact(size as usize, ((size as usize) + 511) & !511)? {
SplitStreamData::External(obj_id) => match header.entry_type() {
EntryType::Regular | EntryType::Continuous => {
let file = File::from(booted_cfs.repo.open_object(&obj_id)?);
let file = File::from(composefs_repo.open_object(&obj_id)?);

layer_writer
.append(&header, file)
Expand Down Expand Up @@ -196,7 +179,7 @@ pub async fn export_repo_to_image(

skopeo::copy(
&tempoci,
&dest_imgref,
dest_imgref,
None,
Some((
std::sync::Arc::new(tmpdir.try_clone()?.into()),
Expand All @@ -208,3 +191,37 @@ pub async fn export_repo_to_image(

Ok(())
}

/// Exports a composefs repository to a container image in containers-storage:
pub async fn export_repo_to_image(
storage: &Storage,
booted_cfs: &BootedComposefs,
source: Option<&str>,
target: Option<&str>,
) -> Result<()> {
let host = get_composefs_status(storage, booted_cfs).await?;

let (source, dest_imgref) = get_imgrefs_for_copy(&host, source, target).await?;

let mut depl_verity = None;

for depl in host.list_deployments() {
let img = &depl.image.as_ref().unwrap().image;

// Not checking transport here as we'll be pulling from the repo anyway
// So, image name is all we need
if img.image == source.name {
depl_verity = Some(depl.require_composefs()?.verity.clone());
break;
}
}

let depl_verity = depl_verity.ok_or_else(|| anyhow::anyhow!("Image {source} not found"))?;

let imginfo = get_imginfo(storage, &depl_verity)?;

println!("Copying local image {source} to {dest_imgref} ...");
export_composefs_to_dest(&booted_cfs.repo, &imginfo, &dest_imgref).await?;
println!("Pushed: {dest_imgref}");
Ok(())
}
7 changes: 4 additions & 3 deletions crates/lib/src/bootc_composefs/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,12 @@ async fn pull_composefs_unified(

// Stage 1: get the image into bootc-owned containers-storage.
if imgref.transport == containers_image_proxy::Transport::ContainerStorage {
// The image is in the default containers-storage (/var/lib/containers/storage).
// Copy it into bootc-owned storage.
// The image is in a containers-storage instance — either the default
// /var/lib/containers/storage or an additional image store advertised
// via STORAGE_OPTS (e.g. the bcvk virtiofs mount).
tracing::info!("Unified pull: copying {image} from host containers-storage");
imgstore
.pull_from_host_storage(image)
.pull_from_containers_storage(image)
.await
.context("Copying image from host containers-storage into bootc storage")?;
} else {
Expand Down
22 changes: 5 additions & 17 deletions crates/lib/src/bootc_composefs/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,11 @@ pub(crate) async fn switch_composefs(
let repo = &*booted_cfs.repo;
let (image, img_config) = is_image_pulled(repo, &target_imgref).await?;

// Use unified storage if explicitly requested, or auto-detect: either the
// target image is already in bootc-owned containers-storage, OR the booted
// image is — which means the user has opted into unified storage and all
// subsequent operations (including switch to a new image) should use it.
let use_unified = if opts.unified_storage_exp {
true
} else {
let booted_imgref = host.spec.image.as_ref();
let booted_unified = if let Some(booted) = booted_imgref {
crate::deploy::image_exists_in_unified_storage(storage, booted).await?
} else {
false
};
let target_unified =
crate::deploy::image_exists_in_unified_storage(storage, &target_imgref).await?;
booted_unified || target_unified
};
// Use unified storage if explicitly requested via flag, or if the
// composefs/bootc.json marker says unified storage is enabled on this system.
let use_unified = opts.unified_storage_exp
|| crate::deploy::unified_storage_enabled(storage)
.context("Checking unified storage flag")?;

let do_upgrade_opts = DoUpgradeOpts {
soft_reboot: opts.soft_reboot,
Expand Down
16 changes: 5 additions & 11 deletions crates/lib/src/bootc_composefs/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,17 +416,11 @@ pub(crate) async fn upgrade_composefs(
let imgref = derived_image.as_ref().or(current_image);
let mut booted_imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;

// Auto-detect unified storage: use the unified path if the target image is
// already in bootc-owned containers-storage, OR if the booted image is —
// the latter means the user has opted into unified storage and all
// subsequent operations should use it.
let current_unified = if let Some(current) = current_image {
crate::deploy::image_exists_in_unified_storage(storage, current).await?
} else {
false
};
do_upgrade_opts.use_unified = current_unified
|| crate::deploy::image_exists_in_unified_storage(storage, booted_imgref).await?;
// Use unified storage if the composefs/bootc.json marker says it is enabled
// on this system (written by `bootc image set-unified` or by install with
// `--experimental-unified-storage`).
do_upgrade_opts.use_unified = crate::deploy::unified_storage_enabled(storage)
.context("Checking unified storage flag")?;

let repo = &*composefs.repo;

Expand Down
110 changes: 76 additions & 34 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,21 @@ pub(crate) enum FsverityOpts {
},
}

/// Subcommands for `bootc internals fsck`.
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum FsckCheck {
/// Check image store metadata consistency.
///
/// Verifies that every image bootc has committed to (i.e. has a composefs
/// GC tag) is also present in bootc's containers-storage. This is a
/// metadata-level check only — it does not walk composefs objects or verify
/// content integrity. Exits non-zero if any image is partially imported.
///
/// LBIs (logically bound images) are not subject to this check; only images
/// that went through the unified storage pipeline are checked.
Images,
}

/// Hidden, internal only options
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum InternalsOpts {
Expand All @@ -638,7 +653,23 @@ pub(crate) enum InternalsOpts {
#[clap(subcommand)]
Fsverity(FsverityOpts),
/// Perform consistency checking.
Fsck,
///
/// Without a subcommand, runs all checks. Use a subcommand to run a
/// specific subset.
Fsck {
#[clap(subcommand)]
check: Option<FsckCheck>,
/// Attempt to repair detected inconsistencies.
///
/// Currently handles: images present in the composefs repo (with a bootc
/// GC tag) that are missing from bootc containers-storage (copies them
/// back from composefs).
///
/// Does NOT attempt to repair images in containers-storage that are
/// missing from composefs; those require re-running `bootc upgrade`.
#[clap(long)]
repair: bool,
},
/// Perform cleanup actions
Cleanup,
Relabel {
Expand Down Expand Up @@ -1212,10 +1243,8 @@ async fn upgrade(
return Ok(());
}

// Ensure the bootc storage directory is initialized; the --check path
// needs this for update_mtime() and the non-check path needs it for
// unified pull detection.
let use_unified = crate::deploy::image_exists_in_unified_storage(storage, imgref).await?;
// Check whether the composefs/bootc.json flag says unified storage is enabled.
let use_unified = crate::deploy::unified_storage_enabled(storage)?;

if opts.check {
let ostree_imgref = imgref.clone().into();
Expand Down Expand Up @@ -1243,16 +1272,7 @@ async fn upgrade(
}
} else {
let fetched = if use_unified {
crate::deploy::pull_unified(
repo,
imgref,
None,
opts.quiet,
prog.clone(),
storage,
Some(&booted_ostree.deployment),
)
.await?
crate::deploy::pull_via_composefs(repo, imgref, storage).await?
} else {
crate::deploy::pull(
repo,
Expand Down Expand Up @@ -1414,24 +1434,12 @@ async fn switch_ostree(

// Determine whether to use unified storage path.
// If explicitly requested via flag, use unified storage directly.
// Otherwise, auto-detect based on whether the image exists in bootc storage.
let use_unified = if opts.unified_storage_exp {
true
} else {
crate::deploy::image_exists_in_unified_storage(storage, &target).await?
};
// Otherwise, check the composefs/bootc.json flag.
let use_unified = opts.unified_storage_exp
|| crate::deploy::unified_storage_enabled(storage)?;

let fetched = if use_unified {
crate::deploy::pull_unified(
repo,
&target,
None,
opts.quiet,
prog.clone(),
storage,
Some(&booted_ostree.deployment),
)
.await?
crate::deploy::pull_via_composefs(repo, &target, storage).await?
} else {
crate::deploy::pull(
repo,
Expand Down Expand Up @@ -2089,11 +2097,45 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
Ok(())
}
},
InternalsOpts::Cfs { args } => composefs_ctl::run_from_iter(args.iter()).await,
InternalsOpts::Cfs { args } => {
// Inject `--system` (which resolves to /sysroot/composefs) unless the
// caller has already specified a repo location via --repo, --user, or
// --system. This ensures `bootc internals cfsctl oci images` (and
// similar subcommands) operate on the booted composefs repository by
// default rather than falling back to cfsctl's own heuristic.
let has_repo_flag = args.iter().any(|a| {
let s = a.to_string_lossy();
s == "--repo" || s.starts_with("--repo=") || s == "--user" || s == "--system"
});
Comment thread
cgwalters marked this conversation as resolved.
if has_repo_flag {
composefs_ctl::run_from_iter(args.iter()).await
} else {
composefs_ctl::run_from_iter(
std::iter::once(OsString::from("--system")).chain(args.into_iter()),
)
.await
}
}
InternalsOpts::Reboot => crate::reboot::reboot(),
InternalsOpts::Fsck => {
InternalsOpts::Fsck { check, repair } => {
let storage = &get_storage().await?;
crate::fsck::fsck(&storage, std::io::stdout().lock()).await?;
match check {
// `bootc internals fsck images` — metadata-level image check only.
Some(FsckCheck::Images) => {
let ok = crate::fsck::fsck_images(storage, repair).await?;
if !ok {
std::process::exit(1);
}
}
// `bootc internals fsck` with no subcommand — run everything.
None => {
crate::fsck::fsck(storage, std::io::stdout().lock()).await?;
let ok = crate::fsck::fsck_images(storage, repair).await?;
if !ok {
std::process::exit(1);
}
}
}
Ok(())
}
InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
Expand Down
Loading
Loading