Skip to content

Commit 81ced8b

Browse files
committed
Add new copy-to-bootc-storage option to bootc image command
This adds a new `bootc image copy-to-bootc-storage` command that copies container images from various sources to the bootc-owned storage location used by Logically Bound Images (LBIs). Usage examples: bootc image copy-to-bootc-storage docker://quay.io/example/image:tag bootc image copy-to-bootc-storage oci-archive:/path/to/image.tar Assisted by Claude Code
1 parent 8cf0971 commit 81ced8b

File tree

3 files changed

+78
-0
lines changed

3 files changed

+78
-0
lines changed

crates/lib/src/cli.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,15 @@ pub(crate) enum ImageOpts {
421421
/// The image to pull
422422
image: String,
423423
},
424+
/// Copy a container image to the bootc-owned container storage.
425+
///
426+
/// This copies a container image from various sources (registries, OCI archives,
427+
/// containers-storage, etc.) to the bootc-owned storage, which is used for
428+
/// Logically Bound Images and other bootc functionality.
429+
CopyToBootcStorage {
430+
/// The source image to copy (supports various transports like registry:, containers-storage:, oci-archive:, etc.)
431+
source: String,
432+
},
424433
/// Wrapper for selected `podman image` subcommands in bootc storage.
425434
#[clap(subcommand)]
426435
Cmd(ImageCmdOpts),
@@ -1351,6 +1360,9 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
13511360
.pull_from_host_storage(&image)
13521361
.await
13531362
}
1363+
ImageOpts::CopyToBootcStorage { source } => {
1364+
crate::image::copy_to_bootc_storage_entrypoint(&source).await
1365+
}
13541366
ImageOpts::Cmd(opt) => {
13551367
let storage = get_storage().await?;
13561368
let imgstore = storage.get_ensure_imgstore()?;

crates/lib/src/image.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,16 @@ pub(crate) async fn push_entrypoint(source: Option<&str>, target: Option<&str>)
169169
Ok(())
170170
}
171171

172+
/// Implementation of `bootc image copy-to-bootc-storage`.
173+
#[context("Copying image to bootc storage")]
174+
pub(crate) async fn copy_to_bootc_storage_entrypoint(source: &str) -> Result<()> {
175+
let sysroot = crate::cli::get_storage().await?;
176+
sysroot
177+
.get_ensure_imgstore()?
178+
.copy_to_storage(source)
179+
.await
180+
}
181+
172182
/// Thin wrapper for invoking `podman image <X>` but set up for our internal
173183
/// image store (as distinct from /var/lib/containers default).
174184
pub(crate) async fn imgcmd_entrypoint(

crates/lib/src/podstorage.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,62 @@ impl CStorage {
376376
Ok(())
377377
}
378378

379+
/// Copy an image from any source to this bootc-owned storage.
380+
/// This is more generic than pull_from_host_storage and supports various transports.
381+
#[context("Copying to bootc storage: {source}")]
382+
pub(crate) async fn copy_to_storage(&self, source: &str) -> Result<()> {
383+
let temp_runroot = TempDir::new(cap_std::ambient_authority())?;
384+
let mut cmd = Command::new("skopeo");
385+
cmd.stdin(Stdio::null());
386+
387+
bind_storage_roots(&mut cmd, &self.storage_root, &temp_runroot)?;
388+
389+
// Parse the source to determine the transport and image name
390+
let source_ref = if source.contains("://") || (source.contains(':') && !source.starts_with('/')) {
391+
// Looks like it has a transport specified or is a registry reference
392+
source.to_string()
393+
} else {
394+
// Assume containers-storage if no transport is specified
395+
format!("containers-storage:{}", source)
396+
};
397+
398+
// Extract the image name (without transport prefix) for the destination
399+
let image_name = if let Some(pos) = source.rfind('/') {
400+
&source[pos + 1..]
401+
} else if let Some(pos) = source.find(':') {
402+
// Handle cases like "containers-storage:imagename" or "registry:imagename"
403+
let after_colon = &source[pos + 1..];
404+
if after_colon.contains('/') {
405+
after_colon.split('/').last().unwrap_or(after_colon)
406+
} else {
407+
after_colon
408+
}
409+
} else {
410+
source
411+
};
412+
413+
// The destination in our bootc-owned storage
414+
let storage_dest = format!(
415+
"containers-storage:[overlay@{STORAGE_ALIAS_DIR}+/proc/self/fd/{STORAGE_RUN_FD}]{image_name}"
416+
);
417+
418+
cmd.args(["copy", &source_ref, &storage_dest]);
419+
420+
// Handle authentication if available
421+
let authfile = ostree_ext::globals::get_global_authfile(&self.sysroot)?
422+
.map(|(authfile, _fd)| authfile);
423+
if let Some(authfile) = authfile {
424+
cmd.args(["--authfile", authfile.as_str()]);
425+
}
426+
427+
println!("Copying {source_ref} to bootc storage...");
428+
let mut cmd = AsyncCommand::from(cmd);
429+
cmd.run().await.context("Failed to copy image")?;
430+
temp_runroot.close()?;
431+
println!("Successfully copied {image_name} to bootc storage");
432+
Ok(())
433+
}
434+
379435
fn subpath() -> Utf8PathBuf {
380436
Utf8Path::new(crate::store::BOOTC_ROOT).join(SUBPATH)
381437
}

0 commit comments

Comments
 (0)