Skip to content

Commit 139db98

Browse files
authored
Merge pull request #871 from omertuc/imagelist
List logically bound images
2 parents 3604dbb + 60a6f0b commit 139db98

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)