Skip to content

Commit d5dd1af

Browse files
committed
container inspect: Add human-readable and yaml output formats
The container inspect command previously only supported JSON output. This extends it to support human-readable output (now the default) and YAML, matching the output format options available in other bootc commands like status. The --json flag provides backward compatibility for scripts that expect JSON output, while --format allows explicit selection of any supported format. Assisted-by: OpenCode (Sonnet 4) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent fd83c65 commit d5dd1af

File tree

7 files changed

+181
-33
lines changed

7 files changed

+181
-33
lines changed

crates/lib/src/cli.rs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -319,11 +319,22 @@ pub(crate) enum InstallOpts {
319319
/// Subcommands which can be executed as part of a container build.
320320
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
321321
pub(crate) enum ContainerOpts {
322-
/// Output JSON to stdout containing the container image metadata.
322+
/// Output information about the container image.
323+
///
324+
/// By default, a human-readable summary is output. Use --json or --format
325+
/// to change the output format.
323326
Inspect {
324327
/// Operate on the provided rootfs.
325328
#[clap(long, default_value = "/")]
326329
rootfs: Utf8PathBuf,
330+
331+
/// Output in JSON format.
332+
#[clap(long)]
333+
json: bool,
334+
335+
/// The output format.
336+
#[clap(long, conflicts_with = "json")]
337+
format: Option<OutputFormat>,
327338
},
328339
/// Perform relatively inexpensive static analysis checks as part of a container
329340
/// build.
@@ -1473,15 +1484,11 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
14731484
}
14741485
}
14751486
Opt::Container(opts) => match opts {
1476-
ContainerOpts::Inspect { rootfs } => {
1477-
let root = &Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority())?;
1478-
let kargs = crate::bootc_kargs::get_kargs_in_root(root, std::env::consts::ARCH)?;
1479-
let kargs: Vec<String> = kargs.iter_str().map(|s| s.to_owned()).collect();
1480-
let kernel = crate::kernel::find_kernel(root)?;
1481-
let inspect = crate::spec::ContainerInspect { kargs, kernel };
1482-
serde_json::to_writer_pretty(std::io::stdout().lock(), &inspect)?;
1483-
Ok(())
1484-
}
1487+
ContainerOpts::Inspect {
1488+
rootfs,
1489+
json,
1490+
format,
1491+
} => crate::status::container_inspect(&rootfs, json, format),
14851492
ContainerOpts::Lint {
14861493
rootfs,
14871494
fatal_warnings,

crates/lib/src/kernel.rs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,33 @@ pub(crate) struct Kernel {
2727

2828
/// Find the kernel in a container image root directory.
2929
///
30-
/// This function first attempts to find a traditional kernel layout with
31-
/// `/usr/lib/modules/<version>/vmlinuz`. If that doesn't exist, it falls back
32-
/// to looking for a UKI in `/boot/EFI/Linux/*.efi`.
30+
/// This function first attempts to find a UKI in `/boot/EFI/Linux/*.efi`.
31+
/// If that doesn't exist, it falls back to looking for a traditional kernel
32+
/// layout with `/usr/lib/modules/<version>/vmlinuz`.
3333
///
3434
/// Returns `None` if no kernel is found.
3535
pub(crate) fn find_kernel(root: &Dir) -> Result<Option<Kernel>> {
36-
// First, try to find a traditional kernel via ostree_ext
37-
if let Some(kernel_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
38-
let version = kernel_dir
39-
.file_name()
40-
.ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {kernel_dir}"))?
36+
// First, try to find a UKI
37+
if let Some(uki_filename) = find_uki_filename(root)? {
38+
let version = uki_filename
39+
.strip_suffix(".efi")
40+
.unwrap_or(&uki_filename)
4141
.to_owned();
4242
return Ok(Some(Kernel {
4343
version,
44-
unified: false,
44+
unified: true,
4545
}));
4646
}
4747

48-
// Fall back to checking for a UKI
49-
if let Some(uki_filename) = find_uki_filename(root)? {
50-
let version = uki_filename
51-
.strip_suffix(".efi")
52-
.unwrap_or(&uki_filename)
48+
// Fall back to checking for a traditional kernel via ostree_ext
49+
if let Some(kernel_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
50+
let version = kernel_dir
51+
.file_name()
52+
.ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {kernel_dir}"))?
5353
.to_owned();
5454
return Ok(Some(Kernel {
5555
version,
56-
unified: true,
56+
unified: false,
5757
}));
5858
}
5959

@@ -130,7 +130,7 @@ mod tests {
130130
}
131131

132132
#[test]
133-
fn test_find_kernel_traditional_takes_precedence() -> Result<()> {
133+
fn test_find_kernel_uki_takes_precedence() -> Result<()> {
134134
let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
135135
// Both traditional and UKI exist
136136
tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
@@ -142,9 +142,9 @@ mod tests {
142142
tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
143143

144144
let kernel = find_kernel(&tempdir)?.expect("should find kernel");
145-
// Traditional kernel should take precedence
146-
assert_eq!(kernel.version, "6.12.0-100.fc41.x86_64");
147-
assert!(!kernel.unified);
145+
// UKI should take precedence
146+
assert_eq!(kernel.version, "fedora-6.12.0");
147+
assert!(kernel.unified);
148148
Ok(())
149149
}
150150

crates/lib/src/spec.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! The definition for host system state.
22
33
use std::fmt::Display;
4+
45
use std::str::FromStr;
56

67
use anyhow::Result;

crates/lib/src/status.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,77 @@ fn human_readable_output(mut out: impl Write, host: &Host, verbose: bool) -> Res
808808
Ok(())
809809
}
810810

811+
/// Output container inspection in human-readable format
812+
fn container_inspect_print_human(
813+
inspect: &crate::spec::ContainerInspect,
814+
mut out: impl Write,
815+
) -> Result<()> {
816+
// Collect rows to determine the max label width
817+
let mut rows: Vec<(&str, String)> = Vec::new();
818+
819+
if let Some(kernel) = &inspect.kernel {
820+
rows.push(("Kernel", kernel.version.clone()));
821+
let kernel_type = if kernel.unified { "UKI" } else { "vmlinuz" };
822+
rows.push(("Type", kernel_type.to_string()));
823+
} else {
824+
rows.push(("Kernel", "<none>".to_string()));
825+
}
826+
827+
let kargs = if inspect.kargs.is_empty() {
828+
"<none>".to_string()
829+
} else {
830+
inspect.kargs.join(" ")
831+
};
832+
rows.push(("Kargs", kargs));
833+
834+
// Find the max label width for right-alignment
835+
let max_label_len = rows.iter().map(|(label, _)| label.len()).max().unwrap_or(0);
836+
837+
for (label, value) in rows {
838+
write_row_name(&mut out, label, max_label_len)?;
839+
writeln!(out, "{value}")?;
840+
}
841+
842+
Ok(())
843+
}
844+
845+
/// Inspect a container image and output information about it.
846+
pub(crate) fn container_inspect(
847+
rootfs: &camino::Utf8Path,
848+
json: bool,
849+
format: Option<OutputFormat>,
850+
) -> Result<()> {
851+
let root = cap_std_ext::cap_std::fs::Dir::open_ambient_dir(
852+
rootfs,
853+
cap_std_ext::cap_std::ambient_authority(),
854+
)?;
855+
let kargs = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?;
856+
let kargs: Vec<String> = kargs.iter_str().map(|s| s.to_owned()).collect();
857+
let kernel = crate::kernel::find_kernel(&root)?;
858+
let inspect = crate::spec::ContainerInspect { kargs, kernel };
859+
860+
// Determine output format: explicit --format wins, then --json, then default to human-readable
861+
let format = format.unwrap_or(if json {
862+
OutputFormat::Json
863+
} else {
864+
OutputFormat::HumanReadable
865+
});
866+
867+
let mut out = std::io::stdout().lock();
868+
match format {
869+
OutputFormat::Json => {
870+
serde_json::to_writer_pretty(&mut out, &inspect)?;
871+
}
872+
OutputFormat::Yaml => {
873+
serde_yaml::to_writer(&mut out, &inspect)?;
874+
}
875+
OutputFormat::HumanReadable => {
876+
container_inspect_print_human(&inspect, &mut out)?;
877+
}
878+
}
879+
Ok(())
880+
}
881+
811882
#[cfg(test)]
812883
mod tests {
813884
use super::*;
@@ -1014,4 +1085,60 @@ mod tests {
10141085
// Verbose output should include download-only status as "no" for normal staged deployments
10151086
assert!(w.contains("Download-only: no"));
10161087
}
1088+
1089+
#[test]
1090+
fn test_container_inspect_human_readable() {
1091+
let inspect = crate::spec::ContainerInspect {
1092+
kargs: vec!["console=ttyS0".into(), "quiet".into()],
1093+
kernel: Some(crate::kernel::Kernel {
1094+
version: "6.12.0-100.fc41.x86_64".into(),
1095+
unified: false,
1096+
}),
1097+
};
1098+
let mut w = Vec::new();
1099+
container_inspect_print_human(&inspect, &mut w).unwrap();
1100+
let output = String::from_utf8(w).unwrap();
1101+
let expected = indoc::indoc! { r"
1102+
Kernel: 6.12.0-100.fc41.x86_64
1103+
Type: vmlinuz
1104+
Kargs: console=ttyS0 quiet
1105+
"};
1106+
similar_asserts::assert_eq!(output, expected);
1107+
}
1108+
1109+
#[test]
1110+
fn test_container_inspect_human_readable_uki() {
1111+
let inspect = crate::spec::ContainerInspect {
1112+
kargs: vec![],
1113+
kernel: Some(crate::kernel::Kernel {
1114+
version: "6.12.0-100.fc41.x86_64".into(),
1115+
unified: true,
1116+
}),
1117+
};
1118+
let mut w = Vec::new();
1119+
container_inspect_print_human(&inspect, &mut w).unwrap();
1120+
let output = String::from_utf8(w).unwrap();
1121+
let expected = indoc::indoc! { r"
1122+
Kernel: 6.12.0-100.fc41.x86_64
1123+
Type: UKI
1124+
Kargs: <none>
1125+
"};
1126+
similar_asserts::assert_eq!(output, expected);
1127+
}
1128+
1129+
#[test]
1130+
fn test_container_inspect_human_readable_no_kernel() {
1131+
let inspect = crate::spec::ContainerInspect {
1132+
kargs: vec!["console=ttyS0".into()],
1133+
kernel: None,
1134+
};
1135+
let mut w = Vec::new();
1136+
container_inspect_print_human(&inspect, &mut w).unwrap();
1137+
let output = String::from_utf8(w).unwrap();
1138+
let expected = indoc::indoc! { r"
1139+
Kernel: <none>
1140+
Kargs: console=ttyS0
1141+
"};
1142+
similar_asserts::assert_eq!(output, expected);
1143+
}
10171144
}

crates/tests-integration/src/container.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub(crate) fn test_bootc_status() -> Result<()> {
2424
pub(crate) fn test_bootc_container_inspect() -> Result<()> {
2525
let sh = Shell::new()?;
2626
let inspect: serde_json::Value =
27-
serde_json::from_str(&cmd!(sh, "bootc container inspect").read()?)?;
27+
serde_json::from_str(&cmd!(sh, "bootc container inspect --json").read()?)?;
2828

2929
// check kargs processing
3030
let kargs = inspect.get("kargs").unwrap().as_array().unwrap();

docs/src/man/bootc-container-inspect.8.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The command outputs a JSON object with the following fields:
1616

1717
- `kargs`: An array of kernel arguments embedded in the container image.
1818
- `kernel`: An object containing kernel information (or `null` if no kernel is found):
19-
- `version`: The kernel version identifier. For traditional kernels, this is derived from the `/usr/lib/modules/<version>` directory name (equivalent to `uname -r`). For UKI images, this is is the UKI filename without the `.efi` extension - which should usually be the same as the uname.
19+
- `version`: The kernel version identifier. For vmlinuz kernels, this is derived from the `/usr/lib/modules/<version>` directory name (equivalent to `uname -r`). For UKI images, this is the UKI filename without the `.efi` extension - which should usually be the same as the uname.
2020
- `unified`: A boolean indicating whether the kernel is packaged as a UKI (Unified Kernel Image).
2121

2222
# OPTIONS
@@ -28,6 +28,19 @@ The command outputs a JSON object with the following fields:
2828

2929
Default: /
3030

31+
**--json**
32+
33+
Output in JSON format
34+
35+
**--format**=*FORMAT*
36+
37+
The output format
38+
39+
Possible values:
40+
- humanreadable
41+
- yaml
42+
- json
43+
3144
<!-- END GENERATED OPTIONS -->
3245

3346
# EXAMPLES
@@ -36,7 +49,7 @@ Inspect container image metadata:
3649

3750
bootc container inspect
3851

39-
Example output (traditional kernel):
52+
Example output (vmlinuz kernel):
4053

4154
```json
4255
{

docs/src/man/bootc-container.8.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Operations which can be executed as part of a container build
1919
<!-- BEGIN GENERATED SUBCOMMANDS -->
2020
| Command | Description |
2121
|---------|-------------|
22-
| **bootc container inspect** | Output JSON to stdout containing the container image metadata |
22+
| **bootc container inspect** | Output information about the container image |
2323
| **bootc container lint** | Perform relatively inexpensive static analysis checks as part of a container build |
2424

2525
<!-- END GENERATED SUBCOMMANDS -->

0 commit comments

Comments
 (0)