1use anyhow::{Context, Result, bail};
6use bootc_utils::CommandRunExt;
7use cap_std_ext::cap_std::{self, fs::Dir};
8use clap::ValueEnum;
9use comfy_table::{Table, presets::NOTHING};
10use fn_error_context::context;
11use ostree_ext::container::{ImageReference, Transport};
12use serde::Serialize;
13
14use crate::{
15 boundimage::query_bound_images,
16 cli::{ImageListFormat, ImageListType},
17 podstorage::CStorage,
18 spec::Host,
19 store::Storage,
20 utils::async_task_with_spinner,
21};
22
23pub(crate) const IMAGE_DEFAULT: &str = "localhost/bootc";
25
26async fn image_exists_in_host_storage(image: &str) -> Result<bool> {
33 use tokio::process::Command as AsyncCommand;
34 let mut cmd = AsyncCommand::new("podman");
35 cmd.args(["image", "exists", image]);
36 Ok(cmd.status().await?.success())
37}
38
39#[derive(Clone, Serialize, ValueEnum)]
40enum ImageListTypeColumn {
41 Host,
42 Logical,
43}
44
45impl std::fmt::Display for ImageListTypeColumn {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 self.to_possible_value().unwrap().get_name().fmt(f)
48 }
49}
50
51#[derive(Serialize)]
52struct ImageOutput {
53 image_type: ImageListTypeColumn,
54 image: String,
55 }
58
59#[context("Listing host images")]
60fn list_host_images(sysroot: &crate::store::Storage) -> Result<Vec<ImageOutput>> {
61 let ostree = sysroot.get_ostree()?;
62 let repo = ostree.repo();
63 let images = ostree_ext::container::store::list_images(&repo).context("Querying images")?;
64
65 Ok(images
66 .into_iter()
67 .map(|image| ImageOutput {
68 image,
69 image_type: ImageListTypeColumn::Host,
70 })
71 .collect())
72}
73
74#[context("Listing logical images")]
75fn list_logical_images(root: &Dir) -> Result<Vec<ImageOutput>> {
76 let bound = query_bound_images(root)?;
77
78 Ok(bound
79 .into_iter()
80 .map(|image| ImageOutput {
81 image: image.image,
82 image_type: ImageListTypeColumn::Logical,
83 })
84 .collect())
85}
86
87async fn list_images(list_type: ImageListType) -> Result<Vec<ImageOutput>> {
88 let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
89 .context("Opening /")?;
90
91 let sysroot: Option<crate::store::BootedStorage> =
92 if ostree_ext::container_utils::running_in_container() {
93 None
94 } else {
95 Some(crate::cli::get_storage().await?)
96 };
97
98 Ok(match (list_type, sysroot) {
99 (ImageListType::All, None) => list_logical_images(&rootfs)?,
101 (ImageListType::All, Some(sysroot)) => list_host_images(&sysroot)?
102 .into_iter()
103 .chain(list_logical_images(&rootfs)?)
104 .collect(),
105 (ImageListType::Logical, _) => list_logical_images(&rootfs)?,
106 (ImageListType::Host, None) => {
107 bail!("Listing host images requires a booted bootc system")
108 }
109 (ImageListType::Host, Some(sysroot)) => list_host_images(&sysroot)?,
110 })
111}
112
113#[context("Listing images")]
114pub(crate) async fn list_entrypoint(
115 list_type: ImageListType,
116 list_format: ImageListFormat,
117) -> Result<()> {
118 let images = list_images(list_type).await?;
119
120 match list_format {
121 ImageListFormat::Table => {
122 let mut table = Table::new();
123
124 table
125 .load_preset(NOTHING)
126 .set_content_arrangement(comfy_table::ContentArrangement::Dynamic)
127 .set_header(["REPOSITORY", "TYPE"]);
128
129 for image in images {
130 table.add_row([image.image, image.image_type.to_string()]);
131 }
132
133 println!("{table}");
134 }
135 ImageListFormat::Json => {
136 let mut stdout = std::io::stdout();
137 serde_json::to_writer_pretty(&mut stdout, &images)?;
138 }
139 }
140
141 Ok(())
142}
143
144pub(crate) async fn get_imgrefs_for_copy(
148 host: &Host,
149 source: Option<&str>,
150 target: Option<&str>,
151) -> Result<(ImageReference, ImageReference)> {
152 crate::podstorage::ensure_floating_c_storage_initialized();
154
155 let dest_imgref = match target {
157 Some(target) => ostree_ext::container::ImageReference {
158 transport: Transport::ContainerStorage,
159 name: target.to_owned(),
160 },
161 None => ostree_ext::container::ImageReference {
162 transport: Transport::ContainerStorage,
163 name: IMAGE_DEFAULT.into(),
164 },
165 };
166
167 let src_imgref = match source {
169 Some(source) => ostree_ext::container::ImageReference::try_from(source)
170 .context("Parsing source image")?,
171
172 None => {
173 let booted = host
174 .status
175 .booted
176 .as_ref()
177 .ok_or_else(|| anyhow::anyhow!("Booted deployment not found"))?;
178
179 let booted_image = &booted.image.as_ref().unwrap().image;
180
181 ImageReference {
182 transport: Transport::try_from(booted_image.transport.as_str()).unwrap(),
183 name: booted_image.image.clone(),
184 }
185 }
186 };
187
188 return Ok((src_imgref, dest_imgref));
189}
190
191#[context("Pushing image")]
193pub(crate) async fn push_entrypoint(
194 storage: &Storage,
195 host: &Host,
196 source: Option<&str>,
197 target: Option<&str>,
198) -> Result<()> {
199 let (source, target) = get_imgrefs_for_copy(host, source, target).await?;
200
201 let ostree = storage.get_ostree()?;
202 let repo = &ostree.repo();
203
204 let mut opts = ostree_ext::container::store::ExportToOCIOpts::default();
205 opts.progress_to_stdout = true;
206 println!("Copying local image {source} to {target} ...");
207 let r = ostree_ext::container::store::export(repo, &source, &target, Some(opts)).await?;
208
209 println!("Pushed: {target} {r}");
210 Ok(())
211}
212
213pub(crate) async fn imgcmd_entrypoint(
216 storage: &CStorage,
217 arg: &str,
218 args: &[std::ffi::OsString],
219) -> std::result::Result<(), anyhow::Error> {
220 let mut cmd = storage.new_image_cmd()?;
221 cmd.arg(arg);
222 cmd.args(args);
223 cmd.run_capture_stderr()
224}
225
226#[context("Setting unified storage for booted image")]
231pub(crate) async fn set_unified_entrypoint() -> Result<()> {
232 crate::podstorage::ensure_floating_c_storage_initialized();
234
235 let sysroot = crate::cli::get_storage().await?;
236 set_unified(&sysroot).await
237}
238
239#[context("Setting unified storage for booted image")]
241pub(crate) async fn set_unified(sysroot: &crate::store::Storage) -> Result<()> {
242 let ostree = sysroot.get_ostree()?;
243 let repo = &ostree.repo();
244
245 let (_booted_ostree, _deployments, host) = crate::status::get_status_require_booted(ostree)?;
248
249 let booted_entry = host
252 .status
253 .booted
254 .as_ref()
255 .ok_or_else(|| anyhow::anyhow!("No booted deployment found"))?;
256 let image_status = booted_entry
257 .image
258 .as_ref()
259 .ok_or_else(|| anyhow::anyhow!("Booted deployment is not from a container image"))?;
260
261 let imgref = &image_status.image;
263
264 let imgref_display = imgref.clone().canonicalize()?;
266
267 let imgstore = sysroot.get_ensure_imgstore()?;
269
270 const SET_UNIFIED_JOURNAL_ID: &str = "1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d";
271 tracing::info!(
272 message_id = SET_UNIFIED_JOURNAL_ID,
273 bootc.image.reference = &imgref_display.image,
274 bootc.image.transport = &imgref_display.transport,
275 "Re-pulling booted image into bootc storage via unified path: {}",
276 imgref_display
277 );
278
279 let is_containers_storage = imgref.transport()? == Transport::ContainerStorage;
286
287 if is_containers_storage {
288 tracing::info!(
289 "Source transport is containers-storage; checking if image exists in host storage"
290 );
291
292 let image_exists = image_exists_in_host_storage(&imgref.image).await?;
296
297 if image_exists {
298 tracing::info!(
299 "Image {} already exists in containers-storage, skipping ostree export",
300 &imgref.image
301 );
302 } else {
303 tracing::info!("Image not found in containers-storage; exporting from ostree");
306 let source = ImageReference {
308 transport: Transport::try_from(imgref.transport.as_str())?,
309 name: imgref.image.clone(),
310 };
311 let target = ImageReference {
312 transport: Transport::ContainerStorage,
313 name: imgref.image.clone(),
314 };
315
316 let mut opts = ostree_ext::container::store::ExportToOCIOpts::default();
317 opts.progress_to_stdout = true;
319 tracing::info!(
320 "Exporting ostree deployment to default containers-storage: {}",
321 &imgref.image
322 );
323 ostree_ext::container::store::export(repo, &source, &target, Some(opts)).await?;
324 }
325
326 tracing::info!(
328 "Copying from default containers-storage to bootc storage: {}",
329 &imgref.image
330 );
331 let image_name = imgref.image.clone();
332 let copy_msg = format!("Copying {} to bootc storage", &image_name);
333 async_task_with_spinner(©_msg, async move {
334 imgstore.pull_from_host_storage(&image_name).await
335 })
336 .await?;
337 } else {
338 let image_in_host = image_exists_in_host_storage(&imgref.image).await?;
344
345 if image_in_host {
346 tracing::info!(
347 "Image {} found in host container storage; copying to bootc storage",
348 &imgref.image
349 );
350 let image_name = imgref.image.clone();
351 let copy_msg = format!("Copying {} to bootc storage", &image_name);
352 async_task_with_spinner(©_msg, async move {
353 imgstore.pull_from_host_storage(&image_name).await
354 })
355 .await?;
356 } else {
357 let img_string = imgref.to_transport_image()?;
358 let pull_msg = format!("Pulling {} to bootc storage", &img_string);
359 async_task_with_spinner(&pull_msg, async move {
360 imgstore
361 .pull(&img_string, crate::podstorage::PullMode::Always)
362 .await
363 })
364 .await?;
365 }
366 }
367
368 let imgstore = sysroot.get_ensure_imgstore()?;
370 if !imgstore.exists(&imgref.image).await? {
371 anyhow::bail!(
372 "Image was pushed to bootc storage but not found: {}. \
373 This may indicate a storage configuration issue.",
374 &imgref.image
375 );
376 }
377 tracing::info!("Image verified in bootc storage: {}", &imgref.image);
378
379 let containers_storage_imgref = crate::spec::ImageReference {
382 transport: "containers-storage".to_string(),
383 image: imgref.image.clone(),
384 signature: imgref.signature.clone(),
385 };
386 let ostree_imgref =
387 ostree_ext::container::OstreeImageReference::from(containers_storage_imgref);
388 let _ =
389 ostree_ext::container::store::ImageImporter::new(repo, &ostree_imgref, Default::default())
390 .await?;
391
392 tracing::info!(
393 message_id = SET_UNIFIED_JOURNAL_ID,
394 bootc.status = "set_unified_complete",
395 "Unified storage set for current image. Future upgrade/switch will use it automatically."
396 );
397 Ok(())
398}