Skip to content

Commit 4efcf49

Browse files
committed
WIP: Use own container-storage for host images if the refspec
On install add flag to enable unified-storage Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>
1 parent 1f0a312 commit 4efcf49

File tree

8 files changed

+330
-18
lines changed

8 files changed

+330
-18
lines changed

crates/lib/src/cli.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,11 @@ pub(crate) enum ImageOpts {
439439
/// this will make the image accessible via e.g. `podman run localhost/bootc` and for builds.
440440
target: Option<String>,
441441
},
442+
/// Re-pull the currently booted image into the bootc-owned container storage.
443+
///
444+
/// This onboards the system to the unified storage path so that future
445+
/// upgrade/switch operations can read from the bootc storage directly.
446+
SetUnified,
442447
/// Copy a container image from the default `containers-storage:` to the bootc-owned container storage.
443448
PullFromDefaultStorage {
444449
/// The image to pull
@@ -937,7 +942,22 @@ async fn upgrade(
937942
}
938943
}
939944
} else {
940-
let fetched = crate::deploy::pull(repo, imgref, None, opts.quiet, prog.clone()).await?;
945+
// Check if image exists in bootc storage (/usr/lib/bootc/storage)
946+
let imgstore = sysroot.get_ensure_imgstore()?;
947+
let use_unified = match imgstore.exists(&format!("{imgref:#}")).await {
948+
Ok(v) => v,
949+
Err(e) => {
950+
tracing::warn!("Failed to check bootc storage for image: {e}; falling back to standard pull");
951+
false
952+
}
953+
};
954+
955+
let fetched = if use_unified {
956+
crate::deploy::pull_unified(repo, imgref, None, opts.quiet, prog.clone(), sysroot)
957+
.await?
958+
} else {
959+
crate::deploy::pull(repo, imgref, None, opts.quiet, prog.clone()).await?
960+
};
941961
let staged_digest = staged_image.map(|s| s.digest().expect("valid digest in status"));
942962
let fetched_digest = &fetched.manifest_digest;
943963
tracing::debug!("staged: {staged_digest:?}");
@@ -1051,7 +1071,21 @@ async fn switch_ostree(
10511071

10521072
let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
10531073

1054-
let fetched = crate::deploy::pull(repo, &target, None, opts.quiet, prog.clone()).await?;
1074+
// Check if image exists in bootc storage (/usr/lib/bootc/storage)
1075+
let imgstore = sysroot.get_ensure_imgstore()?;
1076+
let use_unified = match imgstore.exists(&format!("{target:#}")).await {
1077+
Ok(v) => v,
1078+
Err(e) => {
1079+
tracing::warn!("Failed to check bootc storage for image: {e}; falling back to standard pull");
1080+
false
1081+
}
1082+
};
1083+
1084+
let fetched = if use_unified {
1085+
crate::deploy::pull_unified(repo, &target, None, opts.quiet, prog.clone(), sysroot).await?
1086+
} else {
1087+
crate::deploy::pull(repo, &target, None, opts.quiet, prog.clone()).await?
1088+
};
10551089

10561090
if !opts.retain {
10571091
// By default, we prune the previous ostree ref so it will go away after later upgrades
@@ -1439,6 +1473,9 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
14391473
ImageOpts::CopyToStorage { source, target } => {
14401474
crate::image::push_entrypoint(source.as_deref(), target.as_deref()).await
14411475
}
1476+
ImageOpts::SetUnified => {
1477+
crate::image::set_unified_entrypoint().await
1478+
}
14421479
ImageOpts::PullFromDefaultStorage { image } => {
14431480
let storage = get_storage().await?;
14441481
storage
@@ -1518,7 +1555,10 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
15181555
let mut w = SplitStreamWriter::new(&cfs, None, Some(testdata_digest));
15191556
w.write_inline(testdata);
15201557
let object = cfs.write_stream(w, Some("testobject"))?.to_hex();
1521-
assert_eq!(object, "5d94ceb0b2bb3a78237e0a74bc030a262239ab5f47754a5eb2e42941056b64cb21035d64a8f7c2f156e34b820802fa51884de2b1f7dc3a41b9878fc543cd9b07");
1558+
assert_eq!(
1559+
object,
1560+
"5d94ceb0b2bb3a78237e0a74bc030a262239ab5f47754a5eb2e42941056b64cb21035d64a8f7c2f156e34b820802fa51884de2b1f7dc3a41b9878fc543cd9b07"
1561+
);
15221562
Ok(())
15231563
}
15241564
// We don't depend on fsverity-utils today, so re-expose some helpful CLI tools.

crates/lib/src/deploy.rs

Lines changed: 130 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,118 @@ pub(crate) async fn prepare_for_pull(
381381
Ok(PreparedPullResult::Ready(Box::new(prepared_image)))
382382
}
383383

384+
/// Unified approach: Use bootc's CStorage to pull the image, then prepare from containers-storage.
385+
/// This reuses the same infrastructure as LBIs.
386+
pub(crate) async fn prepare_for_pull_unified(
387+
repo: &ostree::Repo,
388+
imgref: &ImageReference,
389+
target_imgref: Option<&OstreeImageReference>,
390+
store: &Storage,
391+
) -> Result<PreparedPullResult> {
392+
// Get or initialize the bootc container storage (same as used for LBIs)
393+
let imgstore = store.get_ensure_imgstore()?;
394+
395+
let image_ref_str = format!("{imgref:#}");
396+
397+
// Log the original transport being used for the pull
398+
tracing::info!(
399+
"Unified pull: pulling from transport '{}' to bootc storage",
400+
&imgref.transport
401+
);
402+
403+
// Pull the image to bootc storage using the same method as LBIs
404+
imgstore
405+
.pull(&image_ref_str, crate::podstorage::PullMode::Always)
406+
.await?;
407+
408+
// Now create a containers-storage reference to read from bootc storage
409+
tracing::info!("Unified pull: now importing from containers-storage transport");
410+
let containers_storage_imgref = ImageReference {
411+
transport: "containers-storage".to_string(),
412+
image: imgref.image.clone(),
413+
signature: imgref.signature.clone(),
414+
};
415+
let ostree_imgref = OstreeImageReference::from(containers_storage_imgref);
416+
417+
// Use the standard preparation flow but reading from containers-storage
418+
let mut imp = new_importer(repo, &ostree_imgref).await?;
419+
if let Some(target) = target_imgref {
420+
imp.set_target(target);
421+
}
422+
let prep = match imp.prepare().await? {
423+
PrepareResult::AlreadyPresent(c) => {
424+
println!("No changes in {imgref:#} => {}", c.manifest_digest);
425+
return Ok(PreparedPullResult::AlreadyPresent(Box::new((*c).into())));
426+
}
427+
PrepareResult::Ready(p) => p,
428+
};
429+
check_bootc_label(&prep.config);
430+
if let Some(warning) = prep.deprecated_warning() {
431+
ostree_ext::cli::print_deprecated_warning(warning).await;
432+
}
433+
ostree_ext::cli::print_layer_status(&prep);
434+
let layers_to_fetch = prep.layers_to_fetch().collect::<Result<Vec<_>>>()?;
435+
436+
// Log that we're importing a new image from containers-storage
437+
const PULLING_NEW_IMAGE_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0";
438+
tracing::info!(
439+
message_id = PULLING_NEW_IMAGE_ID,
440+
bootc.image.reference = &imgref.image,
441+
bootc.image.transport = "containers-storage",
442+
bootc.original_transport = &imgref.transport,
443+
bootc.status = "importing_from_storage",
444+
"Importing image from bootc storage: {}",
445+
ostree_imgref
446+
);
447+
448+
let prepared_image = PreparedImportMeta {
449+
imp,
450+
n_layers_to_fetch: layers_to_fetch.len(),
451+
layers_total: prep.all_layers().count(),
452+
bytes_to_fetch: layers_to_fetch.iter().map(|(l, _)| l.layer.size()).sum(),
453+
bytes_total: prep.all_layers().map(|l| l.layer.size()).sum(),
454+
digest: prep.manifest_digest.clone(),
455+
prep,
456+
};
457+
458+
Ok(PreparedPullResult::Ready(Box::new(prepared_image)))
459+
}
460+
461+
/// Unified pull: Use podman to pull to containers-storage, then read from there
462+
pub(crate) async fn pull_unified(
463+
repo: &ostree::Repo,
464+
imgref: &ImageReference,
465+
target_imgref: Option<&OstreeImageReference>,
466+
quiet: bool,
467+
prog: ProgressWriter,
468+
store: &Storage,
469+
) -> Result<Box<ImageState>> {
470+
match prepare_for_pull_unified(repo, imgref, target_imgref, store).await? {
471+
PreparedPullResult::AlreadyPresent(existing) => {
472+
// Log that the image was already present (Debug level since it's not actionable)
473+
const IMAGE_ALREADY_PRESENT_ID: &str = "5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9";
474+
tracing::debug!(
475+
message_id = IMAGE_ALREADY_PRESENT_ID,
476+
bootc.image.reference = &imgref.image,
477+
bootc.image.transport = &imgref.transport,
478+
bootc.status = "already_present",
479+
"Image already present: {}",
480+
imgref
481+
);
482+
Ok(existing)
483+
}
484+
PreparedPullResult::Ready(prepared_image_meta) => {
485+
// To avoid duplicate success logs, pass a containers-storage imgref to the importer
486+
let cs_imgref = ImageReference {
487+
transport: "containers-storage".to_string(),
488+
image: imgref.image.clone(),
489+
signature: imgref.signature.clone(),
490+
};
491+
pull_from_prepared(&cs_imgref, quiet, prog, *prepared_image_meta).await
492+
}
493+
}
494+
}
495+
384496
#[context("Pulling")]
385497
pub(crate) async fn pull_from_prepared(
386498
imgref: &ImageReference,
@@ -430,18 +542,21 @@ pub(crate) async fn pull_from_prepared(
430542
let imgref_canonicalized = imgref.clone().canonicalize()?;
431543
tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}");
432544

433-
// Log successful import completion
434-
const IMPORT_COMPLETE_JOURNAL_ID: &str = "4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8";
435-
436-
tracing::info!(
437-
message_id = IMPORT_COMPLETE_JOURNAL_ID,
438-
bootc.image.reference = &imgref.image,
439-
bootc.image.transport = &imgref.transport,
440-
bootc.manifest_digest = import.manifest_digest.as_ref(),
441-
bootc.ostree_commit = &import.merge_commit,
442-
"Successfully imported image: {}",
443-
imgref
444-
);
545+
// Log successful import completion (skip if using unified storage to avoid double logging)
546+
let is_unified_path = imgref.transport == "containers-storage";
547+
if !is_unified_path {
548+
const IMPORT_COMPLETE_JOURNAL_ID: &str = "4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8";
549+
550+
tracing::info!(
551+
message_id = IMPORT_COMPLETE_JOURNAL_ID,
552+
bootc.image.reference = &imgref.image,
553+
bootc.image.transport = &imgref.transport,
554+
bootc.manifest_digest = import.manifest_digest.as_ref(),
555+
bootc.ostree_commit = &import.merge_commit,
556+
"Successfully imported image: {}",
557+
imgref
558+
);
559+
}
445560

446561
if let Some(msg) =
447562
ostree_container::store::image_filtered_content_warning(&import.filtered_files)
@@ -490,6 +605,9 @@ pub(crate) async fn pull(
490605
}
491606
}
492607

608+
/// Pull selecting unified vs standard path based on persistent storage config.
609+
// pull_auto was reverted per request; keep explicit callers branching.
610+
493611
pub(crate) async fn wipe_ostree(sysroot: Sysroot) -> Result<()> {
494612
tokio::task::spawn_blocking(move || {
495613
sysroot

crates/lib/src/image.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,59 @@ pub(crate) async fn imgcmd_entrypoint(
181181
cmd.args(args);
182182
cmd.run_capture_stderr()
183183
}
184+
185+
/// Re-pull the currently booted image into the bootc-owned container storage.
186+
///
187+
/// This onboards the system to unified storage for host images so that
188+
/// upgrade/switch can use the unified path automatically when the image is present.
189+
#[context("Setting unified storage for booted image")]
190+
pub(crate) async fn set_unified_entrypoint() -> Result<()> {
191+
let sysroot = crate::cli::get_storage().await?;
192+
let ostree = sysroot.get_ostree()?;
193+
let repo = &ostree.repo();
194+
195+
// Discover the currently booted image reference
196+
let (_booted_deployment, _deployments, host) =
197+
crate::status::get_status_require_booted(ostree)?;
198+
let imgref = host
199+
.spec
200+
.image
201+
.as_ref()
202+
.ok_or_else(|| anyhow::anyhow!("No image source specified in host spec"))?;
203+
204+
// Canonicalize for pull display only, but we want to preserve original pullspec
205+
let imgref_display = imgref.clone().canonicalize()?;
206+
207+
// Pull the image from its original source into bootc storage using LBI machinery
208+
let imgstore = sysroot.get_ensure_imgstore()?;
209+
let img_string = format!("{:#}", imgref);
210+
const SET_UNIFIED_JOURNAL_ID: &str = "1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d";
211+
tracing::info!(
212+
message_id = SET_UNIFIED_JOURNAL_ID,
213+
bootc.image.reference = &imgref_display.image,
214+
bootc.image.transport = &imgref_display.transport,
215+
"Re-pulling booted image into bootc storage via unified path: {}",
216+
imgref_display
217+
);
218+
imgstore
219+
.pull(&img_string, crate::podstorage::PullMode::Always)
220+
.await?;
221+
222+
// Optionally verify we can import from containers-storage by preparing in a temp importer
223+
// without actually importing into the main repo; this is a lightweight validation.
224+
let containers_storage_imgref = crate::spec::ImageReference {
225+
transport: "containers-storage".to_string(),
226+
image: imgref.image.clone(),
227+
signature: imgref.signature.clone(),
228+
};
229+
let ostree_imgref = ostree_ext::container::OstreeImageReference::from(containers_storage_imgref);
230+
let _ = ostree_ext::container::store::ImageImporter::new(repo, &ostree_imgref, Default::default())
231+
.await?;
232+
233+
tracing::info!(
234+
message_id = SET_UNIFIED_JOURNAL_ID,
235+
bootc.status = "set_unified_complete",
236+
"Unified storage set for current image. Future upgrade/switch will use it automatically."
237+
);
238+
Ok(())
239+
}

crates/lib/src/install.rs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@ pub(crate) struct InstallTargetOpts {
147147
#[clap(long)]
148148
#[serde(default)]
149149
pub(crate) skip_fetch_check: bool,
150+
151+
/// Use unified storage path to pull images (experimental)
152+
///
153+
/// When enabled, this uses bootc's container storage (/usr/lib/bootc/storage) to pull
154+
/// the image first, then imports it from there. This is the same approach used for
155+
/// logically bound images.
156+
#[clap(long = "experimental-unified-storage")]
157+
#[serde(default)]
158+
pub(crate) unified_storage_exp: bool,
150159
}
151160

152161
#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -458,6 +467,7 @@ pub(crate) struct State {
458467
pub(crate) selinux_state: SELinuxFinalState,
459468
#[allow(dead_code)]
460469
pub(crate) config_opts: InstallConfigOpts,
470+
pub(crate) target_opts: InstallTargetOpts,
461471
pub(crate) target_imgref: ostree_container::OstreeImageReference,
462472
#[allow(dead_code)]
463473
pub(crate) prepareroot_config: HashMap<String, String>,
@@ -857,6 +867,7 @@ async fn install_container(
857867
state: &State,
858868
root_setup: &RootSetup,
859869
sysroot: &ostree::Sysroot,
870+
storage: &Storage,
860871
has_ostree: bool,
861872
) -> Result<(ostree::Deployment, InstallAleph)> {
862873
let sepolicy = state.load_policy()?;
@@ -897,9 +908,38 @@ async fn install_container(
897908
let repo = &sysroot.repo();
898909
repo.set_disable_fsync(true);
899910

900-
let pulled_image = match prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref))
911+
// Determine whether to use unified storage path
912+
let use_unified = if state.target_opts.unified_storage_exp {
913+
// Explicit flag always uses unified path
914+
true
915+
} else {
916+
// Auto-detect: check if image exists in bootc storage (same as upgrade/switch)
917+
let imgstore = storage.get_ensure_imgstore()?;
918+
imgstore
919+
.exists(&format!("{spec_imgref:#}"))
920+
.await
921+
.unwrap_or_else(|e| {
922+
tracing::warn!(
923+
"Failed to check bootc storage for image: {e}; falling back to standard pull"
924+
);
925+
false
926+
})
927+
};
928+
929+
let prepared = if use_unified {
930+
tracing::info!("Using unified storage path for installation");
931+
crate::deploy::prepare_for_pull_unified(
932+
repo,
933+
&spec_imgref,
934+
Some(&state.target_imgref),
935+
storage,
936+
)
901937
.await?
902-
{
938+
} else {
939+
prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref)).await?
940+
};
941+
942+
let pulled_image = match prepared {
903943
PreparedPullResult::AlreadyPresent(existing) => existing,
904944
PreparedPullResult::Ready(image_meta) => {
905945
check_disk_space(root_setup.physical_root.as_fd(), &image_meta, &spec_imgref)?;
@@ -1468,6 +1508,7 @@ async fn prepare_install(
14681508
selinux_state,
14691509
source,
14701510
config_opts,
1511+
target_opts,
14711512
target_imgref,
14721513
install_config,
14731514
prepareroot_config,
@@ -1500,7 +1541,7 @@ async fn install_with_sysroot(
15001541

15011542
// And actually set up the container in that root, returning a deployment and
15021543
// the aleph state (see below).
1503-
let (deployment, aleph) = install_container(state, rootfs, ostree, has_ostree).await?;
1544+
let (deployment, aleph) = install_container(state, rootfs, ostree, storage, has_ostree).await?;
15041545
// Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
15051546
aleph.write_to(&rootfs.physical_root)?;
15061547

0 commit comments

Comments
 (0)