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:
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! The definition for host system state.
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user