1use anyhow::{Context, Result, bail};
17use bootc_utils::CommandRunExt;
18use cap_std_ext::cap_std::{self, fs::Dir};
19use clap::ValueEnum;
20use comfy_table::{Table, presets::NOTHING};
21use fn_error_context::context;
22use ostree_ext::container::{ImageReference, Transport};
23use serde::Serialize;
24
25use crate::{
26 boundimage::query_bound_images,
27 cli::{ImageListFormat, ImageListType},
28 podstorage::CStorage,
29 spec::Host,
30 store::Storage,
31 utils::async_task_with_spinner,
32};
33
34pub(crate) const IMAGE_DEFAULT: &str = "localhost/bootc";
36
37async fn image_exists_in_host_storage(image: &str) -> Result<bool> {
43 use tokio::process::Command as AsyncCommand;
44 let mut cmd = AsyncCommand::new(bootc_utils::podman_bin());
45 cmd.args(["image", "exists", image]);
46 Ok(cmd.status().await?.success())
47}
48
49#[derive(Clone, Serialize, ValueEnum)]
50#[serde(rename_all = "lowercase")]
51enum ImageListTypeColumn {
52 Host,
53 Unified,
54 Logical,
55}
56
57impl std::fmt::Display for ImageListTypeColumn {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 self.to_possible_value().unwrap().get_name().fmt(f)
60 }
61}
62
63#[derive(Serialize)]
64struct ImageOutput {
65 image_type: ImageListTypeColumn,
66 image: String,
67 }
70
71#[context("Listing host images")]
72async fn list_host_images(sysroot: &crate::store::Storage) -> Result<Vec<ImageOutput>> {
73 let mut result = Vec::new();
74 if let Ok(ostree) = sysroot.get_ostree() {
75 let repo = ostree.repo();
76 let images = ostree_ext::container::store::list_images(&repo).context("Querying images")?;
77 result.extend(images.into_iter().map(|image| ImageOutput {
78 image,
79 image_type: ImageListTypeColumn::Host,
80 }));
81 }
82 result.extend(list_host_images_composefs(sysroot).await?);
87 Ok(result)
88}
89
90#[context("Listing host images from containers-storage")]
91async fn list_host_images_composefs(sysroot: &crate::store::Storage) -> Result<Vec<ImageOutput>> {
92 let sysroot_dir = &sysroot.physical_root;
93 let subpath = CStorage::subpath();
94 if !sysroot_dir.try_exists(&subpath).unwrap_or(false) {
95 return Ok(Vec::new());
96 }
97 let run = Dir::open_ambient_dir("/run", cap_std::ambient_authority())?;
98 let imgstore = CStorage::create(sysroot_dir, &run, None)?;
99 let images = imgstore
100 .list_images()
101 .await
102 .context("Listing containers-storage images")?;
103 Ok(images
104 .into_iter()
105 .flat_map(|entry| {
106 entry
107 .names
108 .unwrap_or_default()
109 .into_iter()
110 .map(|name| ImageOutput {
111 image: name,
112 image_type: ImageListTypeColumn::Unified,
113 })
114 })
115 .collect())
116}
117
118#[context("Listing logical images")]
119fn list_logical_images(root: &Dir) -> Result<Vec<ImageOutput>> {
120 let bound = query_bound_images(root)?;
121
122 Ok(bound
123 .into_iter()
124 .map(|image| ImageOutput {
125 image: image.image,
126 image_type: ImageListTypeColumn::Logical,
127 })
128 .collect())
129}
130
131async fn list_images(list_type: ImageListType) -> Result<Vec<ImageOutput>> {
132 let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
133 .context("Opening /")?;
134
135 let sysroot: Option<crate::store::BootedStorage> =
136 if ostree_ext::container_utils::running_in_container() {
137 None
138 } else {
139 Some(crate::cli::get_storage().await?)
140 };
141
142 Ok(match (list_type, sysroot) {
143 (ImageListType::All, None) => list_logical_images(&rootfs)?,
145 (ImageListType::All, Some(sysroot)) => list_host_images(&sysroot)
146 .await?
147 .into_iter()
148 .chain(list_logical_images(&rootfs)?)
149 .collect(),
150 (ImageListType::Logical, _) => list_logical_images(&rootfs)?,
151 (ImageListType::Host, None) => {
152 bail!("Listing host images requires a booted bootc system")
153 }
154 (ImageListType::Host, Some(sysroot)) => list_host_images(&sysroot).await?,
155 })
156}
157
158#[context("Listing images")]
159pub(crate) async fn list_entrypoint(
160 list_type: ImageListType,
161 list_format: ImageListFormat,
162) -> Result<()> {
163 let images = list_images(list_type).await?;
164
165 match list_format {
166 ImageListFormat::Table => {
167 let mut table = Table::new();
168
169 table
170 .load_preset(NOTHING)
171 .set_content_arrangement(comfy_table::ContentArrangement::Dynamic)
172 .set_header(["REPOSITORY", "TYPE"]);
173
174 for image in images {
175 table.add_row([image.image, image.image_type.to_string()]);
176 }
177
178 println!("{table}");
179 }
180 ImageListFormat::Json => {
181 let mut stdout = std::io::stdout();
182 serde_json::to_writer_pretty(&mut stdout, &images)?;
183 }
184 }
185
186 Ok(())
187}
188
189pub(crate) async fn get_imgrefs_for_copy(
193 host: &Host,
194 source: Option<&str>,
195 target: Option<&str>,
196) -> Result<(ImageReference, ImageReference)> {
197 crate::podstorage::ensure_floating_c_storage_initialized();
199
200 let dest_imgref = match target {
202 Some(target) => ostree_ext::container::ImageReference {
203 transport: Transport::ContainerStorage,
204 name: target.to_owned(),
205 },
206 None => ostree_ext::container::ImageReference {
207 transport: Transport::ContainerStorage,
208 name: IMAGE_DEFAULT.into(),
209 },
210 };
211
212 let src_imgref = match source {
214 Some(source) => ostree_ext::container::ImageReference::try_from(source)
215 .context("Parsing source image")?,
216
217 None => {
218 let booted = host
219 .status
220 .booted
221 .as_ref()
222 .ok_or_else(|| anyhow::anyhow!("Booted deployment not found"))?;
223
224 let booted_image = &booted.image.as_ref().unwrap().image;
225
226 ImageReference {
227 transport: Transport::try_from(booted_image.transport.as_str()).unwrap(),
228 name: booted_image.image.clone(),
229 }
230 }
231 };
232
233 return Ok((src_imgref, dest_imgref));
234}
235
236#[context("Pushing image")]
238pub(crate) async fn push_entrypoint(
239 storage: &Storage,
240 host: &Host,
241 source: Option<&str>,
242 target: Option<&str>,
243) -> Result<()> {
244 let (source, target) = get_imgrefs_for_copy(host, source, target).await?;
245
246 let ostree = storage.get_ostree()?;
247 let repo = &ostree.repo();
248
249 let mut opts = ostree_ext::container::store::ExportToOCIOpts::default();
250 opts.progress_to_stdout = true;
251 println!("Copying local image {source} to {target} ...");
252 let r = ostree_ext::container::store::export(repo, &source, &target, Some(opts)).await?;
253
254 println!("Pushed: {target} {r}");
255 Ok(())
256}
257
258pub(crate) async fn imgcmd_entrypoint(
261 storage: &CStorage,
262 arg: &str,
263 args: &[std::ffi::OsString],
264) -> std::result::Result<(), anyhow::Error> {
265 let mut cmd = storage.new_image_cmd()?;
266 cmd.arg(arg);
267 cmd.args(args);
268 cmd.run_capture_stderr()
269}
270
271#[context("Setting unified storage for booted image")]
276pub(crate) async fn set_unified_entrypoint() -> Result<()> {
277 let storage = crate::cli::get_storage().await?;
278
279 if let crate::store::BootedStorageKind::Composefs(booted_cfs) = storage.kind()? {
280 return set_unified_composefs(&storage, &booted_cfs).await;
281 }
282
283 crate::podstorage::ensure_floating_c_storage_initialized();
285
286 set_unified(&storage).await
287}
288
289#[context("Setting unified storage for composefs")]
293async fn set_unified_composefs(
294 storage: &crate::store::Storage,
295 booted_cfs: &crate::store::BootedComposefs,
296) -> Result<()> {
297 use crate::bootc_composefs::status::get_composefs_status;
298
299 const SET_UNIFIED_CFS_JOURNAL_ID: &str = "2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e";
300
301 let host = get_composefs_status(storage, booted_cfs)
302 .await
303 .context("Getting composefs deployment status")?;
304
305 let imgref = host
306 .spec
307 .image
308 .as_ref()
309 .ok_or_else(|| anyhow::anyhow!("No image source specified for booted deployment"))?;
310
311 tracing::info!(
312 message_id = SET_UNIFIED_CFS_JOURNAL_ID,
313 bootc.image.reference = &imgref.image,
314 bootc.image.transport = &imgref.transport,
315 "Pulling booted image into bootc containers-storage for unified storage: {}",
316 imgref,
317 );
318
319 let imgstore = storage.get_ensure_imgstore()?;
320
321 let img_transport = imgref.to_transport_image()?;
323 if imgstore.exists(&img_transport).await? {
324 println!("Image {} is already in bootc storage.", imgref.image);
325 tracing::info!(
326 message_id = SET_UNIFIED_CFS_JOURNAL_ID,
327 bootc.status = "already_unified",
328 "Image already present in bootc containers-storage",
329 );
330 return Ok(());
331 }
332
333 let image_in_host = image_exists_in_host_storage(&imgref.image).await?;
338
339 if image_in_host {
340 tracing::info!(
341 "Image {} found in host containers-storage; copying to bootc storage",
342 &imgref.image
343 );
344 let image_name = imgref.image.clone();
345 let copy_msg = format!("Copying {} to bootc storage", &image_name);
346 async_task_with_spinner(©_msg, async move {
347 imgstore.pull_from_host_storage(&image_name).await
348 })
349 .await?;
350 } else {
351 let pull_ref = img_transport;
352 let pull_msg = format!("Pulling {} to bootc storage", &pull_ref);
353 async_task_with_spinner(&pull_msg, async move {
354 imgstore.pull_with_progress(&pull_ref).await
355 })
356 .await?;
357 }
358
359 let imgstore = storage.get_ensure_imgstore()?;
361 let img_transport = imgref.to_transport_image()?;
362 if !imgstore.exists(&img_transport).await? {
363 anyhow::bail!(
364 "Image was pulled but not found in bootc storage: {}",
365 &imgref.image
366 );
367 }
368
369 tracing::info!(
370 message_id = SET_UNIFIED_CFS_JOURNAL_ID,
371 bootc.status = "set_unified_complete",
372 "Unified storage set. Future upgrade/switch will use zero-copy path automatically.",
373 );
374 println!("Unified storage enabled for {}.", imgref.image);
375 Ok(())
376}
377
378#[context("Setting unified storage for booted image")]
380pub(crate) async fn set_unified(sysroot: &crate::store::Storage) -> Result<()> {
381 let ostree = sysroot.get_ostree()?;
382 let repo = &ostree.repo();
383
384 let (_booted_ostree, _deployments, host) = crate::status::get_status_require_booted(ostree)?;
387
388 let booted_entry = host
391 .status
392 .booted
393 .as_ref()
394 .ok_or_else(|| anyhow::anyhow!("No booted deployment found"))?;
395 let image_status = booted_entry
396 .image
397 .as_ref()
398 .ok_or_else(|| anyhow::anyhow!("Booted deployment is not from a container image"))?;
399
400 let imgref = &image_status.image;
402
403 let imgref_display = imgref.clone().canonicalize()?;
405
406 let imgstore = sysroot.get_ensure_imgstore()?;
408
409 const SET_UNIFIED_JOURNAL_ID: &str = "1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d";
410 tracing::info!(
411 message_id = SET_UNIFIED_JOURNAL_ID,
412 bootc.image.reference = &imgref_display.image,
413 bootc.image.transport = &imgref_display.transport,
414 "Re-pulling booted image into bootc storage via unified path: {}",
415 imgref_display
416 );
417
418 let is_containers_storage = imgref.transport()? == Transport::ContainerStorage;
425
426 if is_containers_storage {
427 tracing::info!(
428 "Source transport is containers-storage; checking if image exists in host storage"
429 );
430
431 let image_exists = image_exists_in_host_storage(&imgref.image).await?;
435
436 if image_exists {
437 tracing::info!(
438 "Image {} already exists in containers-storage, skipping ostree export",
439 &imgref.image
440 );
441 } else {
442 tracing::info!("Image not found in containers-storage; exporting from ostree");
445 let source = ImageReference {
447 transport: Transport::try_from(imgref.transport.as_str())?,
448 name: imgref.image.clone(),
449 };
450 let target = ImageReference {
451 transport: Transport::ContainerStorage,
452 name: imgref.image.clone(),
453 };
454
455 let mut opts = ostree_ext::container::store::ExportToOCIOpts::default();
456 opts.progress_to_stdout = true;
458 tracing::info!(
459 "Exporting ostree deployment to default containers-storage: {}",
460 &imgref.image
461 );
462 ostree_ext::container::store::export(repo, &source, &target, Some(opts)).await?;
463 }
464
465 tracing::info!(
467 "Copying from default containers-storage to bootc storage: {}",
468 &imgref.image
469 );
470 let image_name = imgref.image.clone();
471 let copy_msg = format!("Copying {} to bootc storage", &image_name);
472 async_task_with_spinner(©_msg, async move {
473 imgstore.pull_from_host_storage(&image_name).await
474 })
475 .await?;
476 } else {
477 let image_in_host = image_exists_in_host_storage(&imgref.image).await?;
483
484 if image_in_host {
485 tracing::info!(
486 "Image {} found in host container storage; copying to bootc storage",
487 &imgref.image
488 );
489 let image_name = imgref.image.clone();
490 let copy_msg = format!("Copying {} to bootc storage", &image_name);
491 async_task_with_spinner(©_msg, async move {
492 imgstore.pull_from_host_storage(&image_name).await
493 })
494 .await?;
495 } else {
496 let img_string = imgref.to_transport_image()?;
497 let pull_msg = format!("Pulling {} to bootc storage", &img_string);
498 async_task_with_spinner(&pull_msg, async move {
499 imgstore
500 .pull(&img_string, crate::podstorage::PullMode::Always)
501 .await
502 })
503 .await?;
504 }
505 }
506
507 let imgstore = sysroot.get_ensure_imgstore()?;
509 if !imgstore.exists(&imgref.image).await? {
510 anyhow::bail!(
511 "Image was pushed to bootc storage but not found: {}. \
512 This may indicate a storage configuration issue.",
513 &imgref.image
514 );
515 }
516 tracing::info!("Image verified in bootc storage: {}", &imgref.image);
517
518 let containers_storage_imgref = crate::spec::ImageReference {
521 transport: "containers-storage".to_string(),
522 image: imgref.image.clone(),
523 signature: imgref.signature.clone(),
524 };
525 let ostree_imgref =
526 ostree_ext::container::OstreeImageReference::from(containers_storage_imgref);
527 let _ =
528 ostree_ext::container::store::ImageImporter::new(repo, &ostree_imgref, Default::default())
529 .await?;
530
531 tracing::info!(
532 message_id = SET_UNIFIED_JOURNAL_ID,
533 bootc.status = "set_unified_complete",
534 "Unified storage set for current image. Future upgrade/switch will use it automatically."
535 );
536 Ok(())
537}