Skip to content

Commit 2b53d68

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 2b53d68

File tree

6 files changed

+264
-25
lines changed

6 files changed

+264
-25
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, ensure, 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+
.iter()
49+
.map(|x| ImageOutput {
50+
image: x.to_string(),
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+
.iter()
62+
.map(|x| ImageOutput {
63+
image: x.image.clone(),
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> {

tests-integration/src/container.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub(crate) fn test_bootc_upgrade() -> Result<()> {
2323
assert!(!st.success());
2424
let stderr = String::from_utf8(o.stderr)?;
2525
assert!(
26-
stderr.contains("this command requires a booted host system"),
26+
stderr.contains("This command requires a booted host system"),
2727
"stderr: {stderr}",
2828
);
2929
}

0 commit comments

Comments
 (0)