Skip to content

Commit 6cc374d

Browse files
committed
DRAFT: List logically bound images
For now this is just the implementation, but it does not contain the important bug fix for #846 which is to allow this command to run inside a container and not just on a bootc booted host Signed-off-by: Omer Tuchfeld <[email protected]>
1 parent ad95463 commit 6cc374d

File tree

5 files changed

+265
-25
lines changed

5 files changed

+265
-25
lines changed

Cargo.lock

+84-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/Cargo.toml

+1
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

+47-2
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,
@@ -876,7 +918,10 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
876918
}
877919
},
878920
Opt::Image(opts) => match opts {
879-
ImageOpts::List => crate::image::list_entrypoint().await,
921+
ImageOpts::List {
922+
list_type,
923+
list_format,
924+
} => crate::image::list_entrypoint(list_type, list_format).await,
880925
ImageOpts::CopyToStorage { source, target } => {
881926
crate::image::push_entrypoint(source.as_deref(), target.as_deref()).await
882927
}

lib/src/image.rs

+97-16
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,114 @@
22
//!
33
//! APIs for operating on container images in the bootc storage.
44
5-
use anyhow::{Context, Result};
5+
use anyhow::{ensure, Context, Result};
66
use bootc_utils::CommandRunExt;
7+
use clap::ValueEnum;
8+
use comfy_table::{presets::NOTHING, Table};
79
use fn_error_context::context;
810
use ostree_ext::container::{ImageReference, Transport};
11+
use serde::Serialize;
912

10-
use crate::imgstorage::Storage;
13+
use crate::cli::{ImageListFormat, ImageListType};
1114

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

18+
#[derive(Clone, Serialize, ValueEnum)]
19+
enum ImageListTypeColumn {
20+
Host,
21+
Logical,
22+
}
23+
24+
impl std::fmt::Display for ImageListTypeColumn {
25+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26+
self.to_possible_value().unwrap().get_name().fmt(f)
27+
}
28+
}
29+
30+
#[derive(Serialize)]
31+
struct ImageOutput {
32+
image_type: ImageListTypeColumn,
33+
image: String,
34+
// TODO: Add hash, size, etc? Difficult because [`ostree_ext::container::store::list_images`]
35+
// only gives us the pullspec.
36+
}
37+
38+
#[context("Listing host images")]
39+
fn list_host_images(sysroot: &crate::store::Storage) -> Result<Vec<ImageOutput>> {
40+
let repo = sysroot.repo();
41+
let images = ostree_ext::container::store::list_images(&repo).context("Querying images")?;
42+
43+
Ok(images
44+
.iter()
45+
.map(|x| ImageOutput {
46+
image: x.to_string(),
47+
image_type: ImageListTypeColumn::Host,
48+
})
49+
.collect())
50+
}
51+
52+
#[context("Listing logical images")]
53+
fn list_logical_images(sysroot: &crate::store::Storage) -> Result<Vec<ImageOutput>> {
54+
let stdout = {
55+
let mut output_bufread = sysroot
56+
.get_ensure_imgstore()?
57+
.new_image_cmd()?
58+
.arg("list")
59+
.arg("--format={{.Repository}}:{{.Tag}}")
60+
.run_get_output()?;
61+
let mut output_buf = vec![];
62+
output_bufread.read_to_end(&mut output_buf)?;
63+
String::from_utf8(output_buf)?
64+
};
65+
66+
let images = stdout
67+
.lines()
68+
.map(|x| ImageOutput {
69+
image: x.to_string(),
70+
image_type: ImageListTypeColumn::Logical,
71+
})
72+
.collect();
73+
74+
Ok(images)
75+
}
76+
1577
#[context("Listing images")]
16-
pub(crate) async fn list_entrypoint() -> Result<()> {
17-
let sysroot = crate::cli::get_storage().await?;
18-
let repo = &sysroot.repo();
78+
pub(crate) async fn list_entrypoint(
79+
list_type: ImageListType,
80+
list_format: ImageListFormat,
81+
) -> Result<()> {
82+
// TODO: Get the storage from the container image, not the booted storage
83+
let sysroot: crate::store::Storage = crate::cli::get_storage().await?;
84+
85+
let images = match list_type {
86+
ImageListType::All => list_host_images(&sysroot)?
87+
.into_iter()
88+
.chain(list_logical_images(&sysroot)?)
89+
.collect(),
90+
ImageListType::Host => list_host_images(&sysroot)?,
91+
ImageListType::Logical => list_logical_images(&sysroot)?,
92+
};
1993

20-
let images = ostree_ext::container::store::list_images(repo).context("Querying images")?;
94+
match list_format {
95+
ImageListFormat::Table => {
96+
let mut table = Table::new();
2197

22-
println!("# Host images");
23-
for image in images {
24-
println!("{image}");
25-
}
26-
println!();
98+
table
99+
.load_preset(NOTHING)
100+
.set_header(vec!["REPOSITORY", "TYPE"]);
101+
102+
for image in images {
103+
table.add_row(vec![image.image, image.image_type.to_string()]);
104+
}
27105

28-
println!("# Logically bound images");
29-
let mut listcmd = sysroot.get_ensure_imgstore()?.new_image_cmd()?;
30-
listcmd.arg("list");
31-
listcmd.run()?;
106+
println!("{table}");
107+
}
108+
ImageListFormat::Json => {
109+
let mut stdout = std::io::stdout();
110+
serde_json::to_writer_pretty(&mut stdout, &images)?;
111+
}
112+
}
32113

33114
Ok(())
34115
}
@@ -79,7 +160,7 @@ pub(crate) async fn push_entrypoint(source: Option<&str>, target: Option<&str>)
79160
/// Thin wrapper for invoking `podman image <X>` but set up for our internal
80161
/// image store (as distinct from /var/lib/containers default).
81162
pub(crate) async fn imgcmd_entrypoint(
82-
storage: &Storage,
163+
storage: &crate::imgstorage::Storage,
83164
arg: &str,
84165
args: &[std::ffi::OsString],
85166
) -> std::result::Result<(), anyhow::Error> {

0 commit comments

Comments
 (0)