Skip to content

Commit b62a648

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 89e9341 commit b62a648

File tree

6 files changed

+162
-14
lines changed

6 files changed

+162
-14
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.
@@ -1457,15 +1468,11 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
14571468
}
14581469
}
14591470
Opt::Container(opts) => match opts {
1460-
ContainerOpts::Inspect { rootfs } => {
1461-
let root = &Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority())?;
1462-
let kargs = crate::bootc_kargs::get_kargs_in_root(root, std::env::consts::ARCH)?;
1463-
let kargs: Vec<String> = kargs.iter_str().map(|s| s.to_owned()).collect();
1464-
let kernel = crate::kernel::find_kernel(root)?;
1465-
let inspect = crate::spec::ContainerInspect { kargs, kernel };
1466-
serde_json::to_writer_pretty(std::io::stdout().lock(), &inspect)?;
1467-
Ok(())
1468-
}
1471+
ContainerOpts::Inspect {
1472+
rootfs,
1473+
json,
1474+
format,
1475+
} => crate::status::container_inspect(&rootfs, json, format),
14691476
ContainerOpts::Lint {
14701477
rootfs,
14711478
fatal_warnings,

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
@@ -801,6 +801,77 @@ fn human_readable_output(mut out: impl Write, host: &Host, verbose: bool) -> Res
801801
Ok(())
802802
}
803803

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

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)