Skip to content

Commit 48616c7

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 72f9013 commit 48616c7

File tree

5 files changed

+197
-1
lines changed

5 files changed

+197
-1
lines changed

lib/Cargo.toml

Lines changed: 1 addition & 1 deletion
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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use fn_error_context::context;
1717
use ostree::gio;
1818
use ostree_container::store::PrepareResult;
1919
use ostree_ext::container as ostree_container;
20+
use ostree_ext::container::Transport;
2021
use ostree_ext::keyfileext::KeyFileExt;
2122
use ostree_ext::ostree;
2223

@@ -191,6 +192,59 @@ pub(crate) enum ContainerOpts {
191192
Lint,
192193
}
193194

195+
/// Subcommands which operate on images.
196+
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
197+
pub(crate) enum ImageOpts {
198+
/// List fetched images stored in the bootc storage.
199+
///
200+
/// Note that these are distinct from images stored via e.g. `podman`.
201+
List,
202+
/// Copy a container image from the bootc storage to a target.
203+
///
204+
/// ## Copying the booted container image to containers-storage: (podman)
205+
///
206+
/// The source and target are both optional; if both are left unspecified,
207+
/// via a simple invocation of `bootc image push`, then the default is to
208+
/// push the currently booted image to `containers-storage` (as used by podman, etc.)
209+
/// and tagged with the image name `localhost/bootc`,
210+
///
211+
/// ## Copying the booted container image to a remote registry
212+
///
213+
/// Aside from the special case above, default transport is `registry`. This
214+
/// means that an invocation of
215+
///
216+
/// `bootc image push --target quay.io/example/someimage:latest` will push the
217+
/// booted container image to the target registry. This will be done via an
218+
/// invocation equivalent to `skopeo copy`, and hence the defaults for that apply.
219+
/// For example, the default registry authentication file applies.
220+
///
221+
/// ## Copying a non-default container image
222+
///
223+
/// It is also possible to copy an image other than the currently booted one by
224+
/// specifying `--source`.
225+
///
226+
/// ## Pulling images
227+
///
228+
/// At the current time there is no explicit support for pulling images other than indirectly
229+
/// via e.g. `bootc switch` or `bootc upgrade`.
230+
Push {
231+
/// The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry`.
232+
///
233+
/// For more information, see `man containers-transports`.
234+
#[clap(long, default_value = "registry")]
235+
transport: String,
236+
237+
#[clap(long)]
238+
/// The source image; if not specified, the booted image will be used.
239+
source: Option<String>,
240+
241+
#[clap(long)]
242+
/// The destination; if not specified, then the default is to push to `containers-storage:localhost/bootc`;
243+
/// this will make the image accessible via e.g. `podman run localhost/bootc` and for builds.
244+
target: Option<String>,
245+
},
246+
}
247+
194248
/// Hidden, internal only options
195249
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
196250
pub(crate) enum InternalsOpts {
@@ -321,6 +375,12 @@ pub(crate) enum Opt {
321375
/// Operations which can be executed as part of a container build.
322376
#[clap(subcommand)]
323377
Container(ContainerOpts),
378+
/// Operations on container images
379+
///
380+
/// Stability: This interface is not declared stable and may change or be removed
381+
/// at any point in the future.
382+
#[clap(subcommand, hide = true)]
383+
Image(ImageOpts),
324384
/// Execute the given command in the host mount namespace
325385
#[cfg(feature = "install")]
326386
#[clap(hide = true)]
@@ -732,6 +792,17 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
732792
Ok(())
733793
}
734794
},
795+
Opt::Image(opts) => match opts {
796+
ImageOpts::List => crate::image::list_entrypoint().await,
797+
ImageOpts::Push {
798+
transport,
799+
source,
800+
target,
801+
} => {
802+
let transport = Transport::try_from(transport.as_str())?;
803+
crate::image::push_entrypoint(transport, source.as_deref(), target.as_deref()).await
804+
}
805+
},
735806
#[cfg(feature = "install")]
736807
Opt::Install(opts) => match opts {
737808
InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,

lib/src/image.rs

Lines changed: 68 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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;
Lines changed: 56 additions & 0 deletions
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)