Skip to content

Commit

Permalink
cli: Add a new bootc image subcommand
Browse files Browse the repository at this point in the history
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 <walters@verbum.org>
  • Loading branch information
cgwalters committed Jun 22, 2024
1 parent 79d318f commit 655ee1e
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 3 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
43 changes: 43 additions & 0 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String>,

#[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<String>,
},
}

/// Hidden, internal only options
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum InternalsOpts {
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
Expand Down
68 changes: 68 additions & 0 deletions lib/src/image.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
56 changes: 56 additions & 0 deletions tests/booted/002-test-image-pushpull-upgrade.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# This test does:
# bootc image push
# podman build <from that image>
# bootc switch <to the local image>
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)" } },
}
}

0 comments on commit 655ee1e

Please sign in to comment.