From 655ee1e85cd0ededc72f32b66b669a027fb0b99e Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 20 Jun 2024 14:29:22 -0400 Subject: [PATCH] cli: Add a new `bootc image` subcommand We have a basic `bootc image list` but more interesting is `bootc image push` which defaults to copying the booted image into the container storage. Signed-off-by: Colin Walters --- Cargo.lock | 4 +- lib/Cargo.toml | 2 +- lib/src/cli.rs | 43 ++++++++++++ lib/src/image.rs | 68 +++++++++++++++++++ lib/src/lib.rs | 1 + .../booted/002-test-image-pushpull-upgrade.nu | 56 +++++++++++++++ 6 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 lib/src/image.rs create mode 100644 tests/booted/002-test-image-pushpull-upgrade.nu diff --git a/Cargo.lock b/Cargo.lock index 5f63bf88..eba2c9cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1347,9 +1347,9 @@ dependencies = [ [[package]] name = "ostree-ext" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945ba054bf4c562f8720668f4963af30ead7621e1b72ffc03638a70d31917124" +checksum = "bb30ee0b43f22ee3cf04f944dab83318ea8202a6e61009ad302cd030824a4438" dependencies = [ "anyhow", "camino", diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 662656e1..90fb77aa 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -16,7 +16,7 @@ anstream = "0.6.13" anstyle = "1.0.6" anyhow = "1.0.82" camino = { version = "1.1.6", features = ["serde1"] } -ostree-ext = { version = "0.14.0" } +ostree-ext = { version = "0.14.0" } chrono = { version = "0.4.38", features = ["serde"] } clap = { version= "4.5.4", features = ["derive","cargo"] } clap_mangen = { version = "0.2.20", optional = true } diff --git a/lib/src/cli.rs b/lib/src/cli.rs index bd00b7aa..ada40e26 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -11,6 +11,7 @@ use fn_error_context::context; use ostree::gio; use ostree_container::store::PrepareResult; use ostree_ext::container as ostree_container; +use ostree_ext::container::Transport; use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::ostree; use std::ffi::OsString; @@ -174,6 +175,31 @@ pub(crate) enum ContainerOpts { Lint, } +/// Subcommands which operate on images. +#[derive(Debug, clap::Subcommand, PartialEq, Eq)] +pub(crate) enum ImageOpts { + List, + /// Perform relatively inexpensive static analysis checks as part of a container + /// build. + /// + /// This is intended to be invoked via e.g. `RUN bootc container lint` as part + /// of a build process; it will error if any problems are detected. + Push { + /// The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry`. + #[clap(long, default_value = "registry")] + transport: String, + + #[clap(long)] + /// The source image; if not specified, the booted image will be used + source: Option, + + #[clap(long)] + /// The destination; if not specified, then the default is to push to `containers-storage:localhost/bootc`; + /// this will make the image accessible via e.g. `podman run localhost/bootc` and for builds. + target: Option, + }, +} + /// Hidden, internal only options #[derive(Debug, clap::Subcommand, PartialEq, Eq)] pub(crate) enum InternalsOpts { @@ -304,6 +330,12 @@ pub(crate) enum Opt { /// Operations which can be executed as part of a container build. #[clap(subcommand)] Container(ContainerOpts), + /// Operations on container images + /// + /// Stability: This interface is not declared stable and may change or be removed + /// at any point in the future. + #[clap(subcommand, hide = true)] + Image(ImageOpts), /// Execute the given command in the host mount namespace #[cfg(feature = "install")] #[clap(hide = true)] @@ -715,6 +747,17 @@ async fn run_from_opt(opt: Opt) -> Result<()> { Ok(()) } }, + Opt::Image(opts) => match opts { + ImageOpts::List => crate::image::list_entrypoint().await, + ImageOpts::Push { + transport, + source, + target, + } => { + let transport = Transport::try_from(transport.as_str())?; + crate::image::push_entrypoint(transport, source.as_deref(), target.as_deref()).await + } + }, #[cfg(feature = "install")] Opt::Install(opts) => match opts { InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await, diff --git a/lib/src/image.rs b/lib/src/image.rs new file mode 100644 index 00000000..d79ed605 --- /dev/null +++ b/lib/src/image.rs @@ -0,0 +1,68 @@ +//! # Controlling bootc-managed images +//! +//! APIs for operating on container images in the bootc storage. + +use anyhow::{Context, Result}; +use fn_error_context::context; +use ostree_ext::container::{ImageReference, Transport}; + +/// The name of the image we push to containers-storage if nothing is specified. +const IMAGE_DEFAULT: &str = "localhost/bootc"; + +#[context("Listing images")] +pub(crate) async fn list_entrypoint() -> Result<()> { + let sysroot = crate::cli::get_locked_sysroot().await?; + let repo = &sysroot.repo(); + + let images = ostree_ext::container::store::list_images(repo).context("Querying images")?; + + for image in images { + println!("{image}"); + } + Ok(()) +} + +#[context("Pushing image")] +pub(crate) async fn push_entrypoint( + transport: Transport, + source: Option<&str>, + target: Option<&str>, +) -> Result<()> { + let sysroot = crate::cli::get_locked_sysroot().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 { + 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(()) +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f2f2c60d..9f8d4ac5 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -20,6 +20,7 @@ pub mod cli; pub(crate) mod deploy; pub(crate) mod generator; +mod image; pub(crate) mod journal; pub(crate) mod kargs; mod lints; diff --git a/tests/booted/002-test-image-pushpull-upgrade.nu b/tests/booted/002-test-image-pushpull-upgrade.nu new file mode 100644 index 00000000..3d9e3bbf --- /dev/null +++ b/tests/booted/002-test-image-pushpull-upgrade.nu @@ -0,0 +1,56 @@ +# This test does: +# bootc image push +# podman build +# bootc switch +use std assert +use tap.nu + +# This code runs on *each* boot. +# Here we just capture information. +bootc status +let st = bootc status --json | from json +let booted = $st.status.booted.image.image + +# Run on the first boot +def initial_build [] { + tap begin "local image push + pull + upgrade" + + let td = mktemp -d + cd $td + + do --ignore-errors { podman image rm localhost/bootc o+e>| ignore } + bootc image push + let img = podman image inspect localhost/bootc | from json + + # A simple derived container + "FROM localhost/bootc +RUN echo test content > /usr/share/blah.txt +" | save Dockerfile + # Build it + podman build -t localhost/bootc-derived . + # Just sanity check it + let v = podman run --rm localhost/bootc-derived cat /usr/share/blah.txt | str trim + assert equal $v "test content" + # Now, fetch it back into the bootc storage! + bootc switch --transport containers-storage localhost/bootc-derived + # And reboot into it + tmt-reboot +} + +# The second boot; verify we're in the derived image +def second_boot [] { + assert equal $booted.transport containers-storage + assert equal $booted.image localhost/bootc-derived + let t = open /usr/share/blah.txt | str trim + assert equal $t "test content" + tap ok +} + +def main [] { + # See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test + match $env.TMT_REBOOT_COUNT? { + null | "0" => initial_build, + "1" => second_boot, + $o => { error make {msg: $"Invalid TMT_REBOOT_COUNT ($o)" } }, + } +}