Skip to content

Commit 670a460

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 07593a0 commit 670a460

File tree

4 files changed

+184
-18
lines changed

4 files changed

+184
-18
lines changed

Cargo.lock

Lines changed: 41 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+
tabled = "0.16.0"
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)]
281+
#[arg(default_value_t)]
282+
list_type: ImageListType,
283+
#[clap(long)]
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

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,112 @@
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;
78
use fn_error_context::context;
89
use ostree_ext::container::{ImageReference, Transport};
10+
use serde::Serialize;
11+
use tabled::{settings::Style, Tabled};
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, Tabled, 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(Tabled, Serialize)]
31+
struct ImageOutput {
32+
#[tabled(rename = "IMAGE TYPE")]
33+
image_type: ImageListTypeColumn,
34+
35+
#[tabled(rename = "IMAGE")]
36+
image: String,
37+
// TODO: Add hash, size, etc? Difficult because [`ostree_ext::container::store::list_images`]
38+
// only gives us the pullspec.
39+
}
40+
41+
#[context("Listing host images")]
42+
fn list_host_images(sysroot: &crate::store::Storage) -> Result<Vec<ImageOutput>> {
43+
let repo = sysroot.repo();
44+
let images = ostree_ext::container::store::list_images(&repo).context("Querying images")?;
45+
46+
Ok(images
47+
.iter()
48+
.map(|x| ImageOutput {
49+
image: x.to_string(),
50+
image_type: ImageListTypeColumn::Host,
51+
})
52+
.collect())
53+
}
54+
55+
#[context("Listing logical images")]
56+
fn list_logical_images(sysroot: &crate::store::Storage) -> Result<Vec<ImageOutput>> {
57+
let output = sysroot
58+
.get_ensure_imgstore()?
59+
.new_image_cmd()?
60+
.arg("list")
61+
.arg("--format={{.Repository}}:{{.Tag}}")
62+
.log_debug()
63+
.output()?;
64+
65+
ensure!(
66+
output.status.success(),
67+
"Failed to list images: {}",
68+
String::from_utf8_lossy(&output.stderr)
69+
);
70+
71+
let stdout = String::from_utf8(output.stdout)?;
72+
73+
let images = stdout
74+
.lines()
75+
.map(|x| ImageOutput {
76+
image: x.to_string(),
77+
image_type: ImageListTypeColumn::Logical,
78+
})
79+
.collect();
80+
81+
Ok(images)
82+
}
83+
1584
#[context("Listing images")]
16-
pub(crate) async fn list_entrypoint() -> Result<()> {
17-
let sysroot = crate::cli::get_storage().await?;
18-
let repo = &sysroot.repo();
85+
pub(crate) async fn list_entrypoint(
86+
list_type: ImageListType,
87+
list_format: ImageListFormat,
88+
) -> Result<()> {
89+
// TODO: Get the storage from the container image, not the booted storage
90+
let sysroot: crate::store::Storage = crate::cli::get_storage().await?;
1991

20-
let images = ostree_ext::container::store::list_images(repo).context("Querying images")?;
92+
let images = match list_type {
93+
ImageListType::All => {
94+
let images = list_host_images(&sysroot)?;
95+
let images = images.into_iter().chain(list_logical_images(&sysroot)?);
96+
images.collect()
97+
}
98+
ImageListType::Host => list_host_images(&sysroot)?,
99+
ImageListType::Logical => list_logical_images(&sysroot)?,
100+
};
21101

22-
println!("# Host images");
23-
for image in images {
24-
println!("{image}");
102+
match list_format {
103+
ImageListFormat::Table => {
104+
println!("{}", tabled::Table::new(images).with(Style::blank()));
105+
}
106+
ImageListFormat::Json => {
107+
let json = serde_json::to_string_pretty(&images)?;
108+
println!("{}", json);
109+
}
25110
}
26-
println!();
27-
28-
println!("# Logically bound images");
29-
let mut listcmd = sysroot.get_ensure_imgstore()?.new_image_cmd()?;
30-
listcmd.arg("list");
31-
listcmd.run()?;
32111

33112
Ok(())
34113
}
@@ -79,7 +158,7 @@ pub(crate) async fn push_entrypoint(source: Option<&str>, target: Option<&str>)
79158
/// Thin wrapper for invoking `podman image <X>` but set up for our internal
80159
/// image store (as distinct from /var/lib/containers default).
81160
pub(crate) async fn imgcmd_entrypoint(
82-
storage: &Storage,
161+
storage: &crate::imgstorage::Storage,
83162
arg: &str,
84163
args: &[std::ffi::OsString],
85164
) -> std::result::Result<(), anyhow::Error> {

0 commit comments

Comments
 (0)