1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 15:45:53 +01:00
Files
bootc/lib/src/image.rs
Colin Walters be2ae679de storage: Ensure global c/storage is initialized via podman
Two of our tests (and an unknown set of users may) run
`bootc image copy-to-storage` which happens to invoke
skopeo to target /var/lib/containers/storage. If this
happens to be the *first* thing to write to that
c/storage instance it triggers a bug around the podman
image network setup.

Signed-off-by: Colin Walters <walters@verbum.org>
2025-02-12 14:58:46 -05:00

183 lines
5.8 KiB
Rust

//! # Controlling bootc-managed images
//!
//! APIs for operating on container images in the bootc storage.
use anyhow::{bail, Context, Result};
use bootc_utils::CommandRunExt;
use cap_std_ext::cap_std::{self, fs::Dir};
use clap::ValueEnum;
use comfy_table::{presets::NOTHING, Table};
use fn_error_context::context;
use ostree_ext::container::{ImageReference, Transport};
use serde::Serialize;
use crate::{
boundimage::query_bound_images,
cli::{ImageListFormat, ImageListType},
imgstorage::ensure_floating_c_storage_initialized,
};
/// The name of the image we push to containers-storage if nothing is specified.
const IMAGE_DEFAULT: &str = "localhost/bootc";
#[derive(Clone, Serialize, ValueEnum)]
enum ImageListTypeColumn {
Host,
Logical,
}
impl std::fmt::Display for ImageListTypeColumn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_possible_value().unwrap().get_name().fmt(f)
}
}
#[derive(Serialize)]
struct ImageOutput {
image_type: ImageListTypeColumn,
image: String,
// TODO: Add hash, size, etc? Difficult because [`ostree_ext::container::store::list_images`]
// only gives us the pullspec.
}
#[context("Listing host images")]
fn list_host_images(sysroot: &crate::store::Storage) -> Result<Vec<ImageOutput>> {
let repo = sysroot.repo();
let images = ostree_ext::container::store::list_images(&repo).context("Querying images")?;
Ok(images
.into_iter()
.map(|image| ImageOutput {
image,
image_type: ImageListTypeColumn::Host,
})
.collect())
}
#[context("Listing logical images")]
fn list_logical_images(root: &Dir) -> Result<Vec<ImageOutput>> {
let bound = query_bound_images(root)?;
Ok(bound
.into_iter()
.map(|image| ImageOutput {
image: image.image,
image_type: ImageListTypeColumn::Logical,
})
.collect())
}
async fn list_images(list_type: ImageListType) -> Result<Vec<ImageOutput>> {
let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
.context("Opening /")?;
let sysroot: Option<crate::store::Storage> =
if ostree_ext::container_utils::running_in_container() {
None
} else {
Some(crate::cli::get_storage().await?)
};
Ok(match (list_type, sysroot) {
// TODO: Should we list just logical images silently here, or error?
(ImageListType::All, None) => list_logical_images(&rootfs)?,
(ImageListType::All, Some(sysroot)) => list_host_images(&sysroot)?
.into_iter()
.chain(list_logical_images(&rootfs)?)
.collect(),
(ImageListType::Logical, _) => list_logical_images(&rootfs)?,
(ImageListType::Host, None) => {
bail!("Listing host images requires a booted bootc system")
}
(ImageListType::Host, Some(sysroot)) => list_host_images(&sysroot)?,
})
}
#[context("Listing images")]
pub(crate) async fn list_entrypoint(
list_type: ImageListType,
list_format: ImageListFormat,
) -> Result<()> {
let images = list_images(list_type).await?;
match list_format {
ImageListFormat::Table => {
let mut table = Table::new();
table
.load_preset(NOTHING)
.set_content_arrangement(comfy_table::ContentArrangement::Dynamic)
.set_header(["REPOSITORY", "TYPE"]);
for image in images {
table.add_row([image.image, image.image_type.to_string()]);
}
println!("{table}");
}
ImageListFormat::Json => {
let mut stdout = std::io::stdout();
serde_json::to_writer_pretty(&mut stdout, &images)?;
}
}
Ok(())
}
/// Implementation of `bootc image push-to-storage`.
#[context("Pushing image")]
pub(crate) async fn push_entrypoint(source: Option<&str>, target: Option<&str>) -> Result<()> {
let transport = Transport::ContainerStorage;
let sysroot = crate::cli::get_storage().await?;
let repo = &sysroot.repo();
// If the target isn't specified, push to containers-storage + our default image
let target = if let Some(target) = target {
ImageReference {
transport,
name: target.to_owned(),
}
} else {
ensure_floating_c_storage_initialized();
ImageReference {
transport: Transport::ContainerStorage,
name: IMAGE_DEFAULT.to_string(),
}
};
// If the source isn't specified, we use the booted image
let source = if let Some(source) = source {
ImageReference::try_from(source).context("Parsing source image")?
} else {
let status = crate::status::get_status_require_booted(&sysroot)?;
// SAFETY: We know it's booted
let booted = status.2.status.booted.unwrap();
let booted_image = booted.image.unwrap().image;
ImageReference {
transport: Transport::try_from(booted_image.transport.as_str()).unwrap(),
name: booted_image.image,
}
};
let mut opts = ostree_ext::container::store::ExportToOCIOpts::default();
opts.progress_to_stdout = true;
println!("Copying local image {source} to {target} ...");
let r = ostree_ext::container::store::export(repo, &source, &target, Some(opts)).await?;
println!("Pushed: {target} {r}");
Ok(())
}
/// Thin wrapper for invoking `podman image <X>` but set up for our internal
/// image store (as distinct from /var/lib/containers default).
pub(crate) async fn imgcmd_entrypoint(
storage: &crate::imgstorage::Storage,
arg: &str,
args: &[std::ffi::OsString],
) -> std::result::Result<(), anyhow::Error> {
let mut cmd = storage.new_image_cmd()?;
cmd.arg(arg);
cmd.args(args);
cmd.run()
}