Skip to content

Commit

Permalink
status: Rework human readable output
Browse files Browse the repository at this point in the history
- Add a circle like rpm-ostree
- Add a whitespace line between entries
- Drop not-present entries
- Rework timestamp+version combo; if there's no version, then
  show the column as `Timestamp`; if there's a version but no
  timestamp, just omit the timestamp
- Align columns

Closes: #900

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed Nov 18, 2024
1 parent c1ac763 commit 5543053
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 51 deletions.
36 changes: 36 additions & 0 deletions lib/src/glyph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Special Unicode characters used for display with ASCII fallbacks
//! in case we're not in a UTF-8 locale.

use std::fmt::Display;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) enum Glyph {
BlackCircle,
}

impl Glyph {
// TODO: Add support for non-Unicode output
#[allow(dead_code)]
pub(crate) fn as_ascii(&self) -> &'static str {
match self {
Glyph::BlackCircle => "*",
}
}

pub(crate) fn as_utf8(&self) -> &'static str {
match self {
Glyph::BlackCircle => "●",
}
}
}

impl Display for Glyph {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_utf8())
}
}

#[test]
fn test_glyph() {
assert_eq!(Glyph::BlackCircle.as_utf8(), "●");
}
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ pub mod spec;

#[cfg(feature = "docgen")]
mod docgen;
mod glyph;
mod imgstorage;
153 changes: 102 additions & 51 deletions lib/src/status.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::borrow::Cow;
use std::collections::VecDeque;
use std::io::IsTerminal;
use std::io::Read;
use std::io::Write;

use anyhow::{Context, Result};
Expand Down Expand Up @@ -325,10 +326,37 @@ pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> {
Ok(())
}

#[derive(Debug)]
enum Slot {
Staged,
Booted,
Rollback,
}

impl std::fmt::Display for Slot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Slot::Staged => "staged",
Slot::Booted => "booted",
Slot::Rollback => "rollback",
};
f.write_str(s)
}
}

/// Output a row title, prefixed by spaces
fn write_row_name(mut out: impl Write, s: &str, prefix_len: usize) -> Result<()> {
let n = prefix_len.saturating_sub(s.chars().count());
let mut spaces = std::io::repeat(b' ').take(n as u64);
std::io::copy(&mut spaces, &mut out)?;
write!(out, "{s}: ")?;
Ok(())
}

/// Write the data for a container image based status.
fn human_render_imagestatus(
mut out: impl Write,
slot_name: &str,
slot: Slot,
image: &crate::spec::ImageStatus,
) -> Result<()> {
let transport = &image.image.transport;
Expand All @@ -340,46 +368,70 @@ fn human_render_imagestatus(
// But for non-registry we include the transport
Cow::Owned(format!("{transport}:{imagename}"))
};
writeln!(out, "Current {slot_name} image: {imageref}")?;

let version = image
.version
.as_deref()
.unwrap_or("No image version defined");
let timestamp = image
.timestamp
.as_ref()
.map(|t| t.to_string())
.unwrap_or_else(|| "No timestamp present".to_owned());
let prefix = match slot {
Slot::Staged => " Staged image".into(),
Slot::Booted => format!("{} Booted image", crate::glyph::Glyph::BlackCircle),
Slot::Rollback => " Rollback image".into(),
};
let prefix_len = prefix.chars().count();
writeln!(out, "{prefix}: {imageref}")?;

write_row_name(&mut out, "Digest", prefix_len)?;
let digest = &image.image_digest;
writeln!(out, "{digest}")?;

let timestamp = image.timestamp.as_ref();
// If we have a version, combine with timestamp
if let Some(version) = image.version.as_deref() {
write_row_name(&mut out, "Version", prefix_len)?;
if let Some(timestamp) = timestamp {
writeln!(out, "{version} ({timestamp})")?;
} else {
writeln!(out, "{version}")?;
}
} else if let Some(timestamp) = timestamp.as_deref() {
// Otherwise just output timestamp
write_row_name(&mut out, "Timestamp", prefix_len)?;
writeln!(out, "{timestamp}")?;
}

writeln!(out, " Image version: {version} ({timestamp})")?;
writeln!(out, " Image digest: {digest}")?;
Ok(())
}

fn human_render_ostree(mut out: impl Write, slot_name: &str, _ostree_commit: &str) -> Result<()> {
fn human_render_ostree(mut out: impl Write, slot: Slot, ostree_commit: &str) -> Result<()> {
// TODO consider rendering more ostree stuff here like rpm-ostree status does
writeln!(out, "Current {slot_name} state is native ostree")?;
let prefix = match slot {
Slot::Staged => " Staged ostree".into(),
Slot::Booted => format!("{} Booted ostree", crate::glyph::Glyph::BlackCircle),
Slot::Rollback => " Rollback ostree".into(),
};
let prefix_len = prefix.len();
writeln!(out, "{prefix}")?;
write_row_name(&mut out, "Commit", prefix_len)?;
writeln!(out, "{ostree_commit}")?;
Ok(())
}

fn human_readable_output_booted(mut out: impl Write, host: &Host) -> Result<()> {
let mut first = true;
for (slot_name, status) in [
("staged", &host.status.staged),
("booted", &host.status.booted),
("rollback", &host.status.rollback),
(Slot::Staged, &host.status.staged),
(Slot::Booted, &host.status.booted),
(Slot::Rollback, &host.status.rollback),
] {
if let Some(host_status) = status {
if first {
first = false;
} else {
writeln!(out)?;
}
if let Some(image) = &host_status.image {
human_render_imagestatus(&mut out, slot_name, image)?;
} else if let Some(ostree) = host_status.ostree.as_ref() {
human_render_ostree(&mut out, slot_name, &ostree.checksum)?;
} else {
writeln!(out, "Current {slot_name} state is unknown")?;
}
} else {
writeln!(out, "No {slot_name} image present")?;
}
}
Ok(())
Expand Down Expand Up @@ -413,14 +465,14 @@ mod tests {
let w = human_status_from_spec_fixture(include_str!("fixtures/spec-staged-booted.yaml"))
.expect("No spec found");
let expected = indoc::indoc! { r"
Current staged image: quay.io/example/someimage:latest
Image version: nightly (2023-10-14 19:22:15 UTC)
Image digest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566
Current booted image: quay.io/example/someimage:latest
Image version: nightly (2023-09-30 19:22:16 UTC)
Image digest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34
No rollback image present
"};
Staged image: quay.io/example/someimage:latest
Digest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566
Version: nightly (2023-10-14 19:22:15 UTC)
● Booted image: quay.io/example/someimage:latest
Digest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34
Version: nightly (2023-09-30 19:22:16 UTC)
"};
similar_asserts::assert_eq!(w, expected);
}

Expand All @@ -432,10 +484,12 @@ mod tests {
))
.expect("No spec found");
let expected = indoc::indoc! { r"
Current staged state is native ostree
Current booted state is native ostree
No rollback image present
"};
Staged ostree
Commit: 1c24260fdd1be20f72a4a97a75c582834ee3431fbb0fa8e4f482bb219d633a45
● Booted ostree
Commit: f9fa3a553ceaaaf30cf85bfe7eed46a822f7b8fd7e14c1e3389cbc3f6d27f791
"};
similar_asserts::assert_eq!(w, expected);
}

Expand All @@ -445,12 +499,13 @@ mod tests {
let w = human_status_from_spec_fixture(include_str!("fixtures/spec-ostree-to-bootc.yaml"))
.expect("No spec found");
let expected = indoc::indoc! { r"
Current staged image: quay.io/centos-bootc/centos-bootc:stream9
Image version: stream9.20240807.0 (No timestamp present)
Image digest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38
Current booted state is native ostree
No rollback image present
"};
Staged image: quay.io/centos-bootc/centos-bootc:stream9
Digest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38
Version: stream9.20240807.0
● Booted ostree
Commit: f9fa3a553ceaaaf30cf85bfe7eed46a822f7b8fd7e14c1e3389cbc3f6d27f791
"};
similar_asserts::assert_eq!(w, expected);
}

Expand All @@ -460,12 +515,10 @@ mod tests {
let w = human_status_from_spec_fixture(include_str!("fixtures/spec-only-booted.yaml"))
.expect("No spec found");
let expected = indoc::indoc! { r"
No staged image present
Current booted image: quay.io/centos-bootc/centos-bootc:stream9
Image version: stream9.20240807.0 (No timestamp present)
Image digest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38
No rollback image present
"};
● Booted image: quay.io/centos-bootc/centos-bootc:stream9
Digest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38
Version: stream9.20240807.0
"};
similar_asserts::assert_eq!(w, expected);
}

Expand All @@ -483,12 +536,10 @@ mod tests {
let w = human_status_from_spec_fixture(include_str!("fixtures/spec-via-local-oci.yaml"))
.unwrap();
let expected = indoc::indoc! { r"
No staged image present
Current booted image: oci:/var/mnt/osupdate
Image version: stream9.20240807.0 (No timestamp present)
Image digest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38
No rollback image present
"};
● Booted image: oci:/var/mnt/osupdate
Digest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38
Version: stream9.20240807.0
"};
similar_asserts::assert_eq!(w, expected);
}

Expand Down

0 comments on commit 5543053

Please sign in to comment.