Skip to content

Commit 60a6f0b

Browse files
committed
List logically bound images
Solves the second part of #846 The (hidden) image list command now has a `--type` flag to allow users to list only logical images, only host images, or all images. Also the command has been adjusted so that it can run even when not booted off of a bootc system. In that case, it will only list logical images. If a user tries to list host images without a booted system, an error will be thrown. The command also has a `--format` flag to allow users to choose between a human-readable table format and a JSON format. Signed-off-by: Omer Tuchfeld <[email protected]>
1 parent fde4cca commit 60a6f0b

File tree

5 files changed

+263
-24
lines changed

5 files changed

+263
-24
lines changed

Cargo.lock

Lines changed: 74 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ toml = "0.8.12"
4646
xshell = { version = "0.2.6", optional = true }
4747
uuid = { version = "1.8.0", features = ["v4"] }
4848
tini = "1.3.0"
49+
comfy-table = "7.1.1"
4950

5051
[dev-dependencies]
5152
indoc = { workspace = true }

lib/src/cli.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use ostree_ext::container as ostree_container;
2121
use ostree_ext::keyfileext::KeyFileExt;
2222
use ostree_ext::ostree;
2323
use schemars::schema_for;
24+
use serde::{Deserialize, Serialize};
2425

2526
use crate::deploy::RequiredHostSpec;
2627
use crate::lints;
@@ -235,13 +236,54 @@ pub(crate) enum ImageCmdOpts {
235236
},
236237
}
237238

239+
#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
240+
#[serde(rename_all = "kebab-case")]
241+
pub(crate) enum ImageListType {
242+
/// List all images
243+
#[default]
244+
All,
245+
/// List only logically bound images
246+
Logical,
247+
/// List only host images
248+
Host,
249+
}
250+
251+
impl std::fmt::Display for ImageListType {
252+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253+
self.to_possible_value().unwrap().get_name().fmt(f)
254+
}
255+
}
256+
257+
#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
258+
#[serde(rename_all = "kebab-case")]
259+
pub(crate) enum ImageListFormat {
260+
/// Human readable table format
261+
#[default]
262+
Table,
263+
/// JSON format
264+
Json,
265+
}
266+
impl std::fmt::Display for ImageListFormat {
267+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268+
self.to_possible_value().unwrap().get_name().fmt(f)
269+
}
270+
}
271+
238272
/// Subcommands which operate on images.
239273
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
240274
pub(crate) enum ImageOpts {
241275
/// List fetched images stored in the bootc storage.
242276
///
243277
/// Note that these are distinct from images stored via e.g. `podman`.
244-
List,
278+
List {
279+
/// Type of image to list
280+
#[clap(long = "type")]
281+
#[arg(default_value_t)]
282+
list_type: ImageListType,
283+
#[clap(long = "format")]
284+
#[arg(default_value_t)]
285+
list_format: ImageListFormat,
286+
},
245287
/// Copy a container image from the bootc storage to `containers-storage:`.
246288
///
247289
/// The source and target are both optional; if both are left unspecified,
@@ -886,7 +928,10 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
886928
}
887929
},
888930
Opt::Image(opts) => match opts {
889-
ImageOpts::List => crate::image::list_entrypoint().await,
931+
ImageOpts::List {
932+
list_type,
933+
list_format,
934+
} => crate::image::list_entrypoint(list_type, list_format).await,
890935
ImageOpts::CopyToStorage { source, target } => {
891936
crate::image::push_entrypoint(source.as_deref(), target.as_deref()).await
892937
}

lib/src/image.rs

Lines changed: 105 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,122 @@
22
//!
33
//! APIs for operating on container images in the bootc storage.
44
5-
use anyhow::{Context, Result};
5+
use anyhow::{bail, Context, Result};
66
use bootc_utils::CommandRunExt;
7+
use cap_std_ext::cap_std::{self, fs::Dir};
8+
use clap::ValueEnum;
9+
use comfy_table::{presets::NOTHING, Table};
710
use fn_error_context::context;
811
use ostree_ext::container::{ImageReference, Transport};
12+
use serde::Serialize;
913

10-
use crate::imgstorage::Storage;
14+
use crate::{
15+
boundimage::query_bound_images,
16+
cli::{ImageListFormat, ImageListType},
17+
};
1118

1219
/// The name of the image we push to containers-storage if nothing is specified.
1320
const IMAGE_DEFAULT: &str = "localhost/bootc";
1421

22+
#[derive(Clone, Serialize, ValueEnum)]
23+
enum ImageListTypeColumn {
24+
Host,
25+
Logical,
26+
}
27+
28+
impl std::fmt::Display for ImageListTypeColumn {
29+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30+
self.to_possible_value().unwrap().get_name().fmt(f)
31+
}
32+
}
33+
34+
#[derive(Serialize)]
35+
struct ImageOutput {
36+
image_type: ImageListTypeColumn,
37+
image: String,
38+
// TODO: Add hash, size, etc? Difficult because [`ostree_ext::container::store::list_images`]
39+
// only gives us the pullspec.
40+
}
41+
42+
#[context("Listing host images")]
43+
fn list_host_images(sysroot: &crate::store::Storage) -> Result<Vec<ImageOutput>> {
44+
let repo = sysroot.repo();
45+
let images = ostree_ext::container::store::list_images(&repo).context("Querying images")?;
46+
47+
Ok(images
48+
.into_iter()
49+
.map(|image| ImageOutput {
50+
image,
51+
image_type: ImageListTypeColumn::Host,
52+
})
53+
.collect())
54+
}
55+
56+
#[context("Listing logical images")]
57+
fn list_logical_images(root: &Dir) -> Result<Vec<ImageOutput>> {
58+
let bound = query_bound_images(root)?;
59+
60+
Ok(bound
61+
.into_iter()
62+
.map(|image| ImageOutput {
63+
image: image.image,
64+
image_type: ImageListTypeColumn::Logical,
65+
})
66+
.collect())
67+
}
68+
69+
async fn list_images(list_type: ImageListType) -> Result<Vec<ImageOutput>> {
70+
let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
71+
.context("Opening /")?;
72+
73+
let sysroot: Option<crate::store::Storage> =
74+
if ostree_ext::container_utils::running_in_container() {
75+
None
76+
} else {
77+
Some(crate::cli::get_storage().await?)
78+
};
79+
80+
Ok(match (list_type, sysroot) {
81+
// TODO: Should we list just logical images silently here, or error?
82+
(ImageListType::All, None) => list_logical_images(&rootfs)?,
83+
(ImageListType::All, Some(sysroot)) => list_host_images(&sysroot)?
84+
.into_iter()
85+
.chain(list_logical_images(&rootfs)?)
86+
.collect(),
87+
(ImageListType::Logical, _) => list_logical_images(&rootfs)?,
88+
(ImageListType::Host, None) => {
89+
bail!("Listing host images requires a booted bootc system")
90+
}
91+
(ImageListType::Host, Some(sysroot)) => list_host_images(&sysroot)?,
92+
})
93+
}
94+
1595
#[context("Listing images")]
16-
pub(crate) async fn list_entrypoint() -> Result<()> {
17-
let sysroot = crate::cli::get_storage().await?;
18-
let repo = &sysroot.repo();
96+
pub(crate) async fn list_entrypoint(
97+
list_type: ImageListType,
98+
list_format: ImageListFormat,
99+
) -> Result<()> {
100+
let images = list_images(list_type).await?;
19101

20-
let images = ostree_ext::container::store::list_images(repo).context("Querying images")?;
102+
match list_format {
103+
ImageListFormat::Table => {
104+
let mut table = Table::new();
21105

22-
println!("# Host images");
23-
for image in images {
24-
println!("{image}");
25-
}
26-
println!();
106+
table
107+
.load_preset(NOTHING)
108+
.set_header(vec!["REPOSITORY", "TYPE"]);
109+
110+
for image in images {
111+
table.add_row(vec![image.image, image.image_type.to_string()]);
112+
}
27113

28-
println!("# Logically bound images");
29-
let mut listcmd = sysroot.get_ensure_imgstore()?.new_image_cmd()?;
30-
listcmd.arg("list");
31-
listcmd.run()?;
114+
println!("{table}");
115+
}
116+
ImageListFormat::Json => {
117+
let mut stdout = std::io::stdout();
118+
serde_json::to_writer_pretty(&mut stdout, &images)?;
119+
}
120+
}
32121

33122
Ok(())
34123
}
@@ -79,7 +168,7 @@ pub(crate) async fn push_entrypoint(source: Option<&str>, target: Option<&str>)
79168
/// Thin wrapper for invoking `podman image <X>` but set up for our internal
80169
/// image store (as distinct from /var/lib/containers default).
81170
pub(crate) async fn imgcmd_entrypoint(
82-
storage: &Storage,
171+
storage: &crate::imgstorage::Storage,
83172
arg: &str,
84173
args: &[std::ffi::OsString],
85174
) -> std::result::Result<(), anyhow::Error> {
Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,41 @@
11
use std assert
22
use tap.nu
33

4-
let images = podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage images --format {{.Repository}} | from csv --noheaders
5-
print "IMAGES:"
6-
podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage images # for debugging
7-
assert ($images | any {|item| $item.column1 == "quay.io/curl/curl"})
8-
assert ($images | any {|item| $item.column1 == "quay.io/curl/curl-base"})
9-
assert ($images | any {|item| $item.column1 == "registry.access.redhat.com/ubi9/podman"}) # this image is signed
4+
# This list reflects the LBIs specified in bootc/tests/containerfiles/lbi/usr/share/containers/systemd
5+
let expected_images = [
6+
"quay.io/curl/curl:latest",
7+
"quay.io/curl/curl-base:latest",
8+
"registry.access.redhat.com/ubi9/podman:latest" # this image is signed
9+
]
10+
11+
def validate_images [images: table] {
12+
print $"Validating images ($images)"
13+
for expected in $expected_images {
14+
assert ($images | any {|item| $item.image == $expected})
15+
}
16+
}
17+
18+
# This test checks that bootc actually populated the bootc storage with the LBI images
19+
def test_logically_bound_images_in_storage [] {
20+
# Use podman to list the images in the bootc storage
21+
let images = podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage images --format {{.Repository}}:{{.Tag}} | from csv --noheaders | rename --column { column1: image }
22+
23+
# Debug print
24+
print "IMAGES:"
25+
podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage images
26+
27+
validate_images $images
28+
}
29+
30+
# This test makes sure that bootc itself knows how to list the LBI images in the bootc storage
31+
def test_bootc_image_list [] {
32+
# Use bootc to list the images in the bootc storage
33+
let images = bootc image list --type logical --format json | from json
34+
35+
validate_images $images
36+
}
37+
38+
test_logically_bound_images_in_storage
39+
test_bootc_image_list
1040

1141
tap ok

0 commit comments

Comments
 (0)