Skip to content

Commit 655ee1e

Browse files
committed
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 <[email protected]>
1 parent 79d318f commit 655ee1e

File tree

6 files changed

+171
-3
lines changed

6 files changed

+171
-3
lines changed

Cargo.lock

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ anstream = "0.6.13"
1616
anstyle = "1.0.6"
1717
anyhow = "1.0.82"
1818
camino = { version = "1.1.6", features = ["serde1"] }
19-
ostree-ext = { version = "0.14.0" }
19+
ostree-ext = { version = "0.14.0" }
2020
chrono = { version = "0.4.38", features = ["serde"] }
2121
clap = { version= "4.5.4", features = ["derive","cargo"] }
2222
clap_mangen = { version = "0.2.20", optional = true }

lib/src/cli.rs

+43
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use fn_error_context::context;
1111
use ostree::gio;
1212
use ostree_container::store::PrepareResult;
1313
use ostree_ext::container as ostree_container;
14+
use ostree_ext::container::Transport;
1415
use ostree_ext::keyfileext::KeyFileExt;
1516
use ostree_ext::ostree;
1617
use std::ffi::OsString;
@@ -174,6 +175,31 @@ pub(crate) enum ContainerOpts {
174175
Lint,
175176
}
176177

178+
/// Subcommands which operate on images.
179+
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
180+
pub(crate) enum ImageOpts {
181+
List,
182+
/// Perform relatively inexpensive static analysis checks as part of a container
183+
/// build.
184+
///
185+
/// This is intended to be invoked via e.g. `RUN bootc container lint` as part
186+
/// of a build process; it will error if any problems are detected.
187+
Push {
188+
/// The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry`.
189+
#[clap(long, default_value = "registry")]
190+
transport: String,
191+
192+
#[clap(long)]
193+
/// The source image; if not specified, the booted image will be used
194+
source: Option<String>,
195+
196+
#[clap(long)]
197+
/// The destination; if not specified, then the default is to push to `containers-storage:localhost/bootc`;
198+
/// this will make the image accessible via e.g. `podman run localhost/bootc` and for builds.
199+
target: Option<String>,
200+
},
201+
}
202+
177203
/// Hidden, internal only options
178204
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
179205
pub(crate) enum InternalsOpts {
@@ -304,6 +330,12 @@ pub(crate) enum Opt {
304330
/// Operations which can be executed as part of a container build.
305331
#[clap(subcommand)]
306332
Container(ContainerOpts),
333+
/// Operations on container images
334+
///
335+
/// Stability: This interface is not declared stable and may change or be removed
336+
/// at any point in the future.
337+
#[clap(subcommand, hide = true)]
338+
Image(ImageOpts),
307339
/// Execute the given command in the host mount namespace
308340
#[cfg(feature = "install")]
309341
#[clap(hide = true)]
@@ -715,6 +747,17 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
715747
Ok(())
716748
}
717749
},
750+
Opt::Image(opts) => match opts {
751+
ImageOpts::List => crate::image::list_entrypoint().await,
752+
ImageOpts::Push {
753+
transport,
754+
source,
755+
target,
756+
} => {
757+
let transport = Transport::try_from(transport.as_str())?;
758+
crate::image::push_entrypoint(transport, source.as_deref(), target.as_deref()).await
759+
}
760+
},
718761
#[cfg(feature = "install")]
719762
Opt::Install(opts) => match opts {
720763
InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,

lib/src/image.rs

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//! # Controlling bootc-managed images
2+
//!
3+
//! APIs for operating on container images in the bootc storage.
4+
5+
use anyhow::{Context, Result};
6+
use fn_error_context::context;
7+
use ostree_ext::container::{ImageReference, Transport};
8+
9+
/// The name of the image we push to containers-storage if nothing is specified.
10+
const IMAGE_DEFAULT: &str = "localhost/bootc";
11+
12+
#[context("Listing images")]
13+
pub(crate) async fn list_entrypoint() -> Result<()> {
14+
let sysroot = crate::cli::get_locked_sysroot().await?;
15+
let repo = &sysroot.repo();
16+
17+
let images = ostree_ext::container::store::list_images(repo).context("Querying images")?;
18+
19+
for image in images {
20+
println!("{image}");
21+
}
22+
Ok(())
23+
}
24+
25+
#[context("Pushing image")]
26+
pub(crate) async fn push_entrypoint(
27+
transport: Transport,
28+
source: Option<&str>,
29+
target: Option<&str>,
30+
) -> Result<()> {
31+
let sysroot = crate::cli::get_locked_sysroot().await?;
32+
33+
let repo = &sysroot.repo();
34+
35+
// If the target isn't specified, push to containers-storage + our default image
36+
let target = if let Some(target) = target {
37+
ImageReference {
38+
transport,
39+
name: target.to_owned(),
40+
}
41+
} else {
42+
ImageReference {
43+
transport: Transport::ContainerStorage,
44+
name: IMAGE_DEFAULT.to_string(),
45+
}
46+
};
47+
48+
// If the source isn't specified, we use the booted image
49+
let source = if let Some(source) = source {
50+
ImageReference::try_from(source).context("Parsing source image")?
51+
} else {
52+
let status = crate::status::get_status_require_booted(&sysroot)?;
53+
// SAFETY: We know it's booted
54+
let booted = status.2.status.booted.unwrap();
55+
let booted_image = booted.image.unwrap().image;
56+
ImageReference {
57+
transport: Transport::try_from(booted_image.transport.as_str()).unwrap(),
58+
name: booted_image.image,
59+
}
60+
};
61+
let mut opts = ostree_ext::container::store::ExportToOCIOpts::default();
62+
opts.progress_to_stdout = true;
63+
println!("Copying local image {source} to {target} ...");
64+
let r = ostree_ext::container::store::export(repo, &source, &target, Some(opts)).await?;
65+
66+
println!("Pushed: {target} {r}");
67+
Ok(())
68+
}

lib/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
pub mod cli;
2121
pub(crate) mod deploy;
2222
pub(crate) mod generator;
23+
mod image;
2324
pub(crate) mod journal;
2425
pub(crate) mod kargs;
2526
mod lints;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# This test does:
2+
# bootc image push
3+
# podman build <from that image>
4+
# bootc switch <to the local image>
5+
use std assert
6+
use tap.nu
7+
8+
# This code runs on *each* boot.
9+
# Here we just capture information.
10+
bootc status
11+
let st = bootc status --json | from json
12+
let booted = $st.status.booted.image.image
13+
14+
# Run on the first boot
15+
def initial_build [] {
16+
tap begin "local image push + pull + upgrade"
17+
18+
let td = mktemp -d
19+
cd $td
20+
21+
do --ignore-errors { podman image rm localhost/bootc o+e>| ignore }
22+
bootc image push
23+
let img = podman image inspect localhost/bootc | from json
24+
25+
# A simple derived container
26+
"FROM localhost/bootc
27+
RUN echo test content > /usr/share/blah.txt
28+
" | save Dockerfile
29+
# Build it
30+
podman build -t localhost/bootc-derived .
31+
# Just sanity check it
32+
let v = podman run --rm localhost/bootc-derived cat /usr/share/blah.txt | str trim
33+
assert equal $v "test content"
34+
# Now, fetch it back into the bootc storage!
35+
bootc switch --transport containers-storage localhost/bootc-derived
36+
# And reboot into it
37+
tmt-reboot
38+
}
39+
40+
# The second boot; verify we're in the derived image
41+
def second_boot [] {
42+
assert equal $booted.transport containers-storage
43+
assert equal $booted.image localhost/bootc-derived
44+
let t = open /usr/share/blah.txt | str trim
45+
assert equal $t "test content"
46+
tap ok
47+
}
48+
49+
def main [] {
50+
# See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test
51+
match $env.TMT_REBOOT_COUNT? {
52+
null | "0" => initial_build,
53+
"1" => second_boot,
54+
$o => { error make {msg: $"Invalid TMT_REBOOT_COUNT ($o)" } },
55+
}
56+
}

0 commit comments

Comments
 (0)