Skip to content

Commit 5b0cdac

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 29e5c9a commit 5b0cdac

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
@@ -942,7 +947,22 @@ async fn upgrade(
942947
}
943948
}
944949
} else {
945-
let fetched = crate::deploy::pull(repo, imgref, None, opts.quiet, prog.clone()).await?;
950+
// Check if image exists in bootc storage (/usr/lib/bootc/storage)
951+
let imgstore = sysroot.get_ensure_imgstore()?;
952+
let use_unified = match imgstore.exists(&format!("{imgref:#}")).await {
953+
Ok(v) => v,
954+
Err(e) => {
955+
tracing::warn!("Failed to check bootc storage for image: {e}; falling back to standard pull");
956+
false
957+
}
958+
};
959+
960+
let fetched = if use_unified {
961+
crate::deploy::pull_unified(repo, imgref, None, opts.quiet, prog.clone(), sysroot)
962+
.await?
963+
} else {
964+
crate::deploy::pull(repo, imgref, None, opts.quiet, prog.clone()).await?
965+
};
946966
let staged_digest = staged_image.map(|s| s.digest().expect("valid digest in status"));
947967
let fetched_digest = &fetched.manifest_digest;
948968
tracing::debug!("staged: {staged_digest:?}");
@@ -1056,7 +1076,21 @@ async fn switch_ostree(
10561076

10571077
let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
10581078

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

10611095
if !opts.retain {
10621096
// By default, we prune the previous ostree ref so it will go away after later upgrades
@@ -1446,6 +1480,9 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
14461480
ImageOpts::CopyToStorage { source, target } => {
14471481
crate::image::push_entrypoint(source.as_deref(), target.as_deref()).await
14481482
}
1483+
ImageOpts::SetUnified => {
1484+
crate::image::set_unified_entrypoint().await
1485+
}
14491486
ImageOpts::PullFromDefaultStorage { image } => {
14501487
let storage = get_storage().await?;
14511488
storage
@@ -1525,7 +1562,10 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
15251562
let mut w = SplitStreamWriter::new(&cfs, None, Some(testdata_digest));
15261563
w.write_inline(testdata);
15271564
let object = cfs.write_stream(w, Some("testobject"))?.to_hex();
1528-
assert_eq!(object, "5d94ceb0b2bb3a78237e0a74bc030a262239ab5f47754a5eb2e42941056b64cb21035d64a8f7c2f156e34b820802fa51884de2b1f7dc3a41b9878fc543cd9b07");
1565+
assert_eq!(
1566+
object,
1567+
"5d94ceb0b2bb3a78237e0a74bc030a262239ab5f47754a5eb2e42941056b64cb21035d64a8f7c2f156e34b820802fa51884de2b1f7dc3a41b9878fc543cd9b07"
1568+
);
15291569
Ok(())
15301570
}
15311571
// 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>,
@@ -861,6 +871,7 @@ async fn install_container(
861871
state: &State,
862872
root_setup: &RootSetup,
863873
sysroot: &ostree::Sysroot,
874+
storage: &Storage,
864875
has_ostree: bool,
865876
) -> Result<(ostree::Deployment, InstallAleph)> {
866877
let sepolicy = state.load_policy()?;
@@ -901,9 +912,38 @@ async fn install_container(
901912
let repo = &sysroot.repo();
902913
repo.set_disable_fsync(true);
903914

904-
let pulled_image = match prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref))
915+
// Determine whether to use unified storage path
916+
let use_unified = if state.target_opts.unified_storage_exp {
917+
// Explicit flag always uses unified path
918+
true
919+
} else {
920+
// Auto-detect: check if image exists in bootc storage (same as upgrade/switch)
921+
let imgstore = storage.get_ensure_imgstore()?;
922+
imgstore
923+
.exists(&format!("{spec_imgref:#}"))
924+
.await
925+
.unwrap_or_else(|e| {
926+
tracing::warn!(
927+
"Failed to check bootc storage for image: {e}; falling back to standard pull"
928+
);
929+
false
930+
})
931+
};
932+
933+
let prepared = if use_unified {
934+
tracing::info!("Using unified storage path for installation");
935+
crate::deploy::prepare_for_pull_unified(
936+
repo,
937+
&spec_imgref,
938+
Some(&state.target_imgref),
939+
storage,
940+
)
905941
.await?
906-
{
942+
} else {
943+
prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref)).await?
944+
};
945+
946+
let pulled_image = match prepared {
907947
PreparedPullResult::AlreadyPresent(existing) => existing,
908948
PreparedPullResult::Ready(image_meta) => {
909949
check_disk_space(root_setup.physical_root.as_fd(), &image_meta, &spec_imgref)?;
@@ -1464,6 +1504,7 @@ async fn prepare_install(
14641504
selinux_state,
14651505
source,
14661506
config_opts,
1507+
target_opts,
14671508
target_imgref,
14681509
install_config,
14691510
prepareroot_config,
@@ -1518,7 +1559,7 @@ async fn install_with_sysroot(
15181559

15191560
// And actually set up the container in that root, returning a deployment and
15201561
// the aleph state (see below).
1521-
let (deployment, aleph) = install_container(state, rootfs, ostree, has_ostree).await?;
1562+
let (deployment, aleph) = install_container(state, rootfs, ostree, storage, has_ostree).await?;
15221563
// Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
15231564
aleph.write_to(&rootfs.physical_root)?;
15241565

0 commit comments

Comments
 (0)