1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 06:45:13 +01:00

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>
This commit is contained in:
Colin Walters
2025-12-18 16:44:33 -05:00
parent fd83c659a8
commit d5dd1af815
7 changed files with 182 additions and 34 deletions

View File

@@ -319,11 +319,22 @@ pub(crate) enum InstallOpts {
/// Subcommands which can be executed as part of a container build.
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum ContainerOpts {
/// Output JSON to stdout containing the container image metadata.
/// Output information about the container image.
///
/// By default, a human-readable summary is output. Use --json or --format
/// to change the output format.
Inspect {
/// Operate on the provided rootfs.
#[clap(long, default_value = "/")]
rootfs: Utf8PathBuf,
/// Output in JSON format.
#[clap(long)]
json: bool,
/// The output format.
#[clap(long, conflicts_with = "json")]
format: Option<OutputFormat>,
},
/// Perform relatively inexpensive static analysis checks as part of a container
/// build.
@@ -1473,15 +1484,11 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
}
}
Opt::Container(opts) => match opts {
ContainerOpts::Inspect { rootfs } => {
let root = &Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority())?;
let kargs = crate::bootc_kargs::get_kargs_in_root(root, std::env::consts::ARCH)?;
let kargs: Vec<String> = kargs.iter_str().map(|s| s.to_owned()).collect();
let kernel = crate::kernel::find_kernel(root)?;
let inspect = crate::spec::ContainerInspect { kargs, kernel };
serde_json::to_writer_pretty(std::io::stdout().lock(), &inspect)?;
Ok(())
}
ContainerOpts::Inspect {
rootfs,
json,
format,
} => crate::status::container_inspect(&rootfs, json, format),
ContainerOpts::Lint {
rootfs,
fatal_warnings,

View File

@@ -27,25 +27,13 @@ pub(crate) struct Kernel {
/// Find the kernel in a container image root directory.
///
/// This function first attempts to find a traditional kernel layout with
/// `/usr/lib/modules/<version>/vmlinuz`. If that doesn't exist, it falls back
/// to looking for a UKI in `/boot/EFI/Linux/*.efi`.
/// This function first attempts to find a UKI in `/boot/EFI/Linux/*.efi`.
/// If that doesn't exist, it falls back to looking for a traditional kernel
/// layout with `/usr/lib/modules/<version>/vmlinuz`.
///
/// Returns `None` if no kernel is found.
pub(crate) fn find_kernel(root: &Dir) -> Result<Option<Kernel>> {
// First, try to find a traditional kernel via ostree_ext
if let Some(kernel_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
let version = kernel_dir
.file_name()
.ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {kernel_dir}"))?
.to_owned();
return Ok(Some(Kernel {
version,
unified: false,
}));
}
// Fall back to checking for a UKI
// First, try to find a UKI
if let Some(uki_filename) = find_uki_filename(root)? {
let version = uki_filename
.strip_suffix(".efi")
@@ -57,6 +45,18 @@ pub(crate) fn find_kernel(root: &Dir) -> Result<Option<Kernel>> {
}));
}
// Fall back to checking for a traditional kernel via ostree_ext
if let Some(kernel_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
let version = kernel_dir
.file_name()
.ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {kernel_dir}"))?
.to_owned();
return Ok(Some(Kernel {
version,
unified: false,
}));
}
Ok(None)
}
@@ -130,7 +130,7 @@ mod tests {
}
#[test]
fn test_find_kernel_traditional_takes_precedence() -> Result<()> {
fn test_find_kernel_uki_takes_precedence() -> Result<()> {
let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
// Both traditional and UKI exist
tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
@@ -142,9 +142,9 @@ mod tests {
tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
let kernel = find_kernel(&tempdir)?.expect("should find kernel");
// Traditional kernel should take precedence
assert_eq!(kernel.version, "6.12.0-100.fc41.x86_64");
assert!(!kernel.unified);
// UKI should take precedence
assert_eq!(kernel.version, "fedora-6.12.0");
assert!(kernel.unified);
Ok(())
}

View File

@@ -1,6 +1,7 @@
//! The definition for host system state.
use std::fmt::Display;
use std::str::FromStr;
use anyhow::Result;

View File

@@ -808,6 +808,77 @@ fn human_readable_output(mut out: impl Write, host: &Host, verbose: bool) -> Res
Ok(())
}
/// Output container inspection in human-readable format
fn container_inspect_print_human(
inspect: &crate::spec::ContainerInspect,
mut out: impl Write,
) -> Result<()> {
// Collect rows to determine the max label width
let mut rows: Vec<(&str, String)> = Vec::new();
if let Some(kernel) = &inspect.kernel {
rows.push(("Kernel", kernel.version.clone()));
let kernel_type = if kernel.unified { "UKI" } else { "vmlinuz" };
rows.push(("Type", kernel_type.to_string()));
} else {
rows.push(("Kernel", "<none>".to_string()));
}
let kargs = if inspect.kargs.is_empty() {
"<none>".to_string()
} else {
inspect.kargs.join(" ")
};
rows.push(("Kargs", kargs));
// Find the max label width for right-alignment
let max_label_len = rows.iter().map(|(label, _)| label.len()).max().unwrap_or(0);
for (label, value) in rows {
write_row_name(&mut out, label, max_label_len)?;
writeln!(out, "{value}")?;
}
Ok(())
}
/// Inspect a container image and output information about it.
pub(crate) fn container_inspect(
rootfs: &camino::Utf8Path,
json: bool,
format: Option<OutputFormat>,
) -> Result<()> {
let root = cap_std_ext::cap_std::fs::Dir::open_ambient_dir(
rootfs,
cap_std_ext::cap_std::ambient_authority(),
)?;
let kargs = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?;
let kargs: Vec<String> = kargs.iter_str().map(|s| s.to_owned()).collect();
let kernel = crate::kernel::find_kernel(&root)?;
let inspect = crate::spec::ContainerInspect { kargs, kernel };
// Determine output format: explicit --format wins, then --json, then default to human-readable
let format = format.unwrap_or(if json {
OutputFormat::Json
} else {
OutputFormat::HumanReadable
});
let mut out = std::io::stdout().lock();
match format {
OutputFormat::Json => {
serde_json::to_writer_pretty(&mut out, &inspect)?;
}
OutputFormat::Yaml => {
serde_yaml::to_writer(&mut out, &inspect)?;
}
OutputFormat::HumanReadable => {
container_inspect_print_human(&inspect, &mut out)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1014,4 +1085,60 @@ mod tests {
// Verbose output should include download-only status as "no" for normal staged deployments
assert!(w.contains("Download-only: no"));
}
#[test]
fn test_container_inspect_human_readable() {
let inspect = crate::spec::ContainerInspect {
kargs: vec!["console=ttyS0".into(), "quiet".into()],
kernel: Some(crate::kernel::Kernel {
version: "6.12.0-100.fc41.x86_64".into(),
unified: false,
}),
};
let mut w = Vec::new();
container_inspect_print_human(&inspect, &mut w).unwrap();
let output = String::from_utf8(w).unwrap();
let expected = indoc::indoc! { r"
Kernel: 6.12.0-100.fc41.x86_64
Type: vmlinuz
Kargs: console=ttyS0 quiet
"};
similar_asserts::assert_eq!(output, expected);
}
#[test]
fn test_container_inspect_human_readable_uki() {
let inspect = crate::spec::ContainerInspect {
kargs: vec![],
kernel: Some(crate::kernel::Kernel {
version: "6.12.0-100.fc41.x86_64".into(),
unified: true,
}),
};
let mut w = Vec::new();
container_inspect_print_human(&inspect, &mut w).unwrap();
let output = String::from_utf8(w).unwrap();
let expected = indoc::indoc! { r"
Kernel: 6.12.0-100.fc41.x86_64
Type: UKI
Kargs: <none>
"};
similar_asserts::assert_eq!(output, expected);
}
#[test]
fn test_container_inspect_human_readable_no_kernel() {
let inspect = crate::spec::ContainerInspect {
kargs: vec!["console=ttyS0".into()],
kernel: None,
};
let mut w = Vec::new();
container_inspect_print_human(&inspect, &mut w).unwrap();
let output = String::from_utf8(w).unwrap();
let expected = indoc::indoc! { r"
Kernel: <none>
Kargs: console=ttyS0
"};
similar_asserts::assert_eq!(output, expected);
}
}

View File

@@ -24,7 +24,7 @@ pub(crate) fn test_bootc_status() -> Result<()> {
pub(crate) fn test_bootc_container_inspect() -> Result<()> {
let sh = Shell::new()?;
let inspect: serde_json::Value =
serde_json::from_str(&cmd!(sh, "bootc container inspect").read()?)?;
serde_json::from_str(&cmd!(sh, "bootc container inspect --json").read()?)?;
// check kargs processing
let kargs = inspect.get("kargs").unwrap().as_array().unwrap();

View File

@@ -16,7 +16,7 @@ The command outputs a JSON object with the following fields:
- `kargs`: An array of kernel arguments embedded in the container image.
- `kernel`: An object containing kernel information (or `null` if no kernel is found):
- `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.
- `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.
- `unified`: A boolean indicating whether the kernel is packaged as a UKI (Unified Kernel Image).
# OPTIONS
@@ -28,6 +28,19 @@ The command outputs a JSON object with the following fields:
Default: /
**--json**
Output in JSON format
**--format**=*FORMAT*
The output format
Possible values:
- humanreadable
- yaml
- json
<!-- END GENERATED OPTIONS -->
# EXAMPLES
@@ -36,7 +49,7 @@ Inspect container image metadata:
bootc container inspect
Example output (traditional kernel):
Example output (vmlinuz kernel):
```json
{

View File

@@ -19,7 +19,7 @@ Operations which can be executed as part of a container build
<!-- BEGIN GENERATED SUBCOMMANDS -->
| Command | Description |
|---------|-------------|
| **bootc container inspect** | Output JSON to stdout containing the container image metadata |
| **bootc container inspect** | Output information about the container image |
| **bootc container lint** | Perform relatively inexpensive static analysis checks as part of a container build |
<!-- END GENERATED SUBCOMMANDS -->