Skip to content

Commit 71b87ba

Browse files
authored
Merge pull request #620 from cgwalters/container-push
cli: Add a new bootc image subcommand
2 parents 72f9013 + 83a6a15 commit 71b87ba

File tree

5 files changed

+245
-1
lines changed

5 files changed

+245
-1
lines changed

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

+48
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,41 @@ 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 `containers-storage:`.
203+
///
204+
/// The source and target are both optional; if both are left unspecified,
205+
/// via a simple invocation of `bootc image copy-to-storage`, then the default is to
206+
/// push the currently booted image to `containers-storage` (as used by podman, etc.)
207+
/// and tagged with the image name `localhost/bootc`,
208+
///
209+
/// ## Copying a non-default container image
210+
///
211+
/// It is also possible to copy an image other than the currently booted one by
212+
/// specifying `--source`.
213+
///
214+
/// ## Pulling images
215+
///
216+
/// At the current time there is no explicit support for pulling images other than indirectly
217+
/// via e.g. `bootc switch` or `bootc upgrade`.
218+
CopyToStorage {
219+
#[clap(long)]
220+
/// The source image; if not specified, the booted image will be used.
221+
source: Option<String>,
222+
223+
#[clap(long)]
224+
/// The destination; if not specified, then the default is to push to `containers-storage:localhost/bootc`;
225+
/// this will make the image accessible via e.g. `podman run localhost/bootc` and for builds.
226+
target: Option<String>,
227+
},
228+
}
229+
194230
/// Hidden, internal only options
195231
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
196232
pub(crate) enum InternalsOpts {
@@ -321,6 +357,12 @@ pub(crate) enum Opt {
321357
/// Operations which can be executed as part of a container build.
322358
#[clap(subcommand)]
323359
Container(ContainerOpts),
360+
/// Operations on container images
361+
///
362+
/// Stability: This interface is not declared stable and may change or be removed
363+
/// at any point in the future.
364+
#[clap(subcommand, hide = true)]
365+
Image(ImageOpts),
324366
/// Execute the given command in the host mount namespace
325367
#[cfg(feature = "install")]
326368
#[clap(hide = true)]
@@ -732,6 +774,12 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
732774
Ok(())
733775
}
734776
},
777+
Opt::Image(opts) => match opts {
778+
ImageOpts::List => crate::image::list_entrypoint().await,
779+
ImageOpts::CopyToStorage { source, target } => {
780+
crate::image::push_entrypoint(source.as_deref(), target.as_deref()).await
781+
}
782+
},
735783
#[cfg(feature = "install")]
736784
Opt::Install(opts) => match opts {
737785
InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,

lib/src/image.rs

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
/// Implementation of `bootc image push-to-storage`.
26+
#[context("Pushing image")]
27+
pub(crate) async fn push_entrypoint(source: Option<&str>, target: Option<&str>) -> Result<()> {
28+
let transport = Transport::ContainerStorage;
29+
let sysroot = crate::cli::get_locked_sysroot().await?;
30+
31+
let repo = &sysroot.repo();
32+
33+
// If the target isn't specified, push to containers-storage + our default image
34+
let target = if let Some(target) = target {
35+
ImageReference {
36+
transport,
37+
name: target.to_owned(),
38+
}
39+
} else {
40+
ImageReference {
41+
transport: Transport::ContainerStorage,
42+
name: IMAGE_DEFAULT.to_string(),
43+
}
44+
};
45+
46+
// If the source isn't specified, we use the booted image
47+
let source = if let Some(source) = source {
48+
ImageReference::try_from(source).context("Parsing source image")?
49+
} else {
50+
let status = crate::status::get_status_require_booted(&sysroot)?;
51+
// SAFETY: We know it's booted
52+
let booted = status.2.status.booted.unwrap();
53+
let booted_image = booted.image.unwrap().image;
54+
ImageReference {
55+
transport: Transport::try_from(booted_image.transport.as_str()).unwrap(),
56+
name: booted_image.image,
57+
}
58+
};
59+
let mut opts = ostree_ext::container::store::ExportToOCIOpts::default();
60+
opts.progress_to_stdout = true;
61+
println!("Copying local image {source} to {target} ...");
62+
let r = ostree_ext::container::store::export(repo, &source, &target, Some(opts)).await?;
63+
64+
println!("Pushed: {target} {r}");
65+
Ok(())
66+
}

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,129 @@
1+
# This test does:
2+
# bootc image copy-to-storage
3+
# podman build <from that image>
4+
# bootc switch <to the local image>
5+
# <verify booted state>
6+
# Then another build, and reboot into verifying that
7+
use std assert
8+
use tap.nu
9+
10+
const kargsv0 = ["testarg=foo", "othertestkarg", "thirdkarg=bar"]
11+
const kargsv1 = ["testarg=foo", "thirdkarg=baz"]
12+
let removed = ($kargsv0 | filter { not ($in in $kargsv1) })
13+
14+
# This code runs on *each* boot.
15+
# Here we just capture information.
16+
bootc status
17+
let st = bootc status --json | from json
18+
let booted = $st.status.booted.image
19+
20+
# Parse the kernel commandline into a list.
21+
# This is not a proper parser, but good enough
22+
# for what we need here.
23+
def parse_cmdline [] {
24+
open /proc/cmdline | str trim | split row " "
25+
}
26+
27+
# Run on the first boot
28+
def initial_build [] {
29+
tap begin "local image push + pull + upgrade"
30+
31+
let td = mktemp -d
32+
cd $td
33+
34+
do --ignore-errors { podman image rm localhost/bootc o+e>| ignore }
35+
bootc image copy-to-storage
36+
let img = podman image inspect localhost/bootc | from json
37+
38+
mkdir usr/lib/bootc/kargs.d
39+
{ kargs: $kargsv0 } | to toml | save usr/lib/bootc/kargs.d/05-testkargs.toml
40+
# A simple derived container that adds a file, but also injects some kargs
41+
"FROM localhost/bootc
42+
COPY usr/ /usr/
43+
RUN echo test content > /usr/share/blah.txt
44+
" | save Dockerfile
45+
# Build it
46+
podman build -t localhost/bootc-derived .
47+
# Just sanity check it
48+
let v = podman run --rm localhost/bootc-derived cat /usr/share/blah.txt | str trim
49+
assert equal $v "test content"
50+
# Now, fetch it back into the bootc storage!
51+
bootc switch --transport containers-storage localhost/bootc-derived
52+
# And reboot into it
53+
tmt-reboot
54+
}
55+
56+
# The second boot; verify we're in the derived image
57+
def second_boot [] {
58+
print "verifying second boot"
59+
# booted from the local container storage and image
60+
assert equal $booted.image.transport containers-storage
61+
assert equal $booted.image.image localhost/bootc-derived
62+
# We wrote this file
63+
let t = open /usr/share/blah.txt | str trim
64+
assert equal $t "test content"
65+
66+
# Verify we have updated kargs
67+
let cmdline = parse_cmdline
68+
print $"cmdline=($cmdline)"
69+
for x in $kargsv0 {
70+
print $"verifying karg: ($x)"
71+
assert ($x in $cmdline)
72+
}
73+
74+
# Now do another build where we drop one of the kargs
75+
let td = mktemp -d
76+
cd $td
77+
78+
mkdir usr/lib/bootc/kargs.d
79+
{ kargs: $kargsv1 } | to toml | save usr/lib/bootc/kargs.d/05-testkargs.toml
80+
"FROM localhost/bootc
81+
COPY usr/ /usr/
82+
RUN echo test content2 > /usr/share/blah.txt
83+
" | save Dockerfile
84+
# Build it
85+
podman build -t localhost/bootc-derived .
86+
let booted_digest = $booted.imageDigest
87+
print booted_digest = $booted_digest
88+
# We should already be fetching updates from container storage
89+
bootc upgrade
90+
# Verify we staged an update
91+
let st = bootc status --json | from json
92+
let staged_digest = $st.status.staged.image.imageDigest
93+
assert ($booted_digest != $staged_digest)
94+
# And reboot into the upgrade
95+
tmt-reboot
96+
}
97+
98+
# Check we have the updated kargs
99+
def third_boot [] {
100+
print "verifying third boot"
101+
assert equal $booted.image.transport containers-storage
102+
assert equal $booted.image.image localhost/bootc-derived
103+
let t = open /usr/share/blah.txt | str trim
104+
assert equal $t "test content2"
105+
106+
# Verify we have updated kargs
107+
let cmdline = parse_cmdline
108+
print $"cmdline=($cmdline)"
109+
for x in $kargsv1 {
110+
print $"Verifying karg ($x)"
111+
assert ($x in $cmdline)
112+
}
113+
# And the kargs that should be removed are gone
114+
for x in $removed {
115+
assert not ($removed in $cmdline)
116+
}
117+
118+
tap ok
119+
}
120+
121+
def main [] {
122+
# See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test
123+
match $env.TMT_REBOOT_COUNT? {
124+
null | "0" => initial_build,
125+
"1" => second_boot,
126+
"2" => third_boot,
127+
$o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } },
128+
}
129+
}

0 commit comments

Comments
 (0)