diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 76b4b88f..0438cc3a 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -120,6 +120,8 @@ pub(crate) struct EditOpts { #[derive(Debug, Clone, ValueEnum, PartialEq, Eq)] #[clap(rename_all = "lowercase")] pub(crate) enum OutputFormat { + /// Output in Human Readable format. + HumanReadable, /// Output in YAML format. Yaml, /// Output in JSON format. diff --git a/lib/src/fixtures/spec-only-booted.yaml b/lib/src/fixtures/spec-only-booted.yaml new file mode 100644 index 00000000..8f5f5736 --- /dev/null +++ b/lib/src/fixtures/spec-only-booted.yaml @@ -0,0 +1,28 @@ +apiVersion: org.containers.bootc/v1alpha1 +kind: BootcHost +metadata: + name: host +spec: + image: + image: quay.io/centos-bootc/centos-bootc:stream9 + transport: registry + bootOrder: default +status: + staged: null + booted: + image: + image: + image: quay.io/centos-bootc/centos-bootc:stream9 + transport: registry + version: stream9.20240807.0 + timestamp: null + imageDigest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38 + cachedUpdate: null + incompatible: false + pinned: false + ostree: + checksum: 439f6bd2e2361bee292c1f31840d798c5ac5ba76483b8021dc9f7b0164ac0f48 + deploySerial: 0 + rollback: null + rollbackQueued: false + type: bootcHost \ No newline at end of file diff --git a/lib/src/fixtures/spec-ostree-to-bootc.yaml b/lib/src/fixtures/spec-ostree-to-bootc.yaml new file mode 100644 index 00000000..3607f99a --- /dev/null +++ b/lib/src/fixtures/spec-ostree-to-bootc.yaml @@ -0,0 +1,37 @@ +apiVersion: org.containers.bootc/v1alpha1 +kind: BootcHost +metadata: + name: host +spec: + image: + image: quay.io/centos-bootc/centos-bootc:stream9 + transport: registry + bootOrder: default +status: + staged: + image: + image: + image: quay.io/centos-bootc/centos-bootc:stream9 + transport: registry + version: stream9.20240807.0 + timestamp: null + imageDigest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38 + cachedUpdate: null + incompatible: false + pinned: false + store: ostreeContainer + ostree: + checksum: 05cbf6dcae32e7a1c5a0774a648a073a5834a305ca92204b53fb6c281fe49db1 + deploySerial: 0 + booted: + image: null + cachedUpdate: null + incompatible: false + pinned: false + store: null + ostree: + checksum: f9fa3a553ceaaaf30cf85bfe7eed46a822f7b8fd7e14c1e3389cbc3f6d27f791 + deploySerial: 0 + rollback: null + rollbackQueued: false + type: null diff --git a/lib/src/fixtures/spec-rfe-ostree-deployment.yaml b/lib/src/fixtures/spec-rfe-ostree-deployment.yaml new file mode 100644 index 00000000..f7557826 --- /dev/null +++ b/lib/src/fixtures/spec-rfe-ostree-deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: org.containers.bootc/v1alpha1 +kind: BootcHost +metadata: + name: host +spec: + image: null + bootOrder: default +status: + staged: + image: null + cachedUpdate: null + incompatible: true + pinned: false + store: null + ostree: + checksum: 1c24260fdd1be20f72a4a97a75c582834ee3431fbb0fa8e4f482bb219d633a45 + deploySerial: 0 + booted: + image: null + cachedUpdate: null + incompatible: false + pinned: false + store: null + ostree: + checksum: f9fa3a553ceaaaf30cf85bfe7eed46a822f7b8fd7e14c1e3389cbc3f6d27f791 + deploySerial: 0 + rollback: null + rollbackQueued: false + type: null diff --git a/lib/src/fixtures/spec-staged-booted.yaml b/lib/src/fixtures/spec-staged-booted.yaml new file mode 100644 index 00000000..6ef439fa --- /dev/null +++ b/lib/src/fixtures/spec-staged-booted.yaml @@ -0,0 +1,40 @@ +apiVersion: org.containers.bootc/v1alpha1 +kind: BootcHost +metadata: + name: host +spec: + image: + image: quay.io/example/someimage:latest + transport: registry + signature: insecure +status: + staged: + image: + image: + image: quay.io/example/someimage:latest + transport: registry + signature: insecure + version: nightly + timestamp: 2023-10-14T19:22:15Z + imageDigest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566 + incompatible: false + pinned: false + ostree: + checksum: 3c6dad657109522e0b2e49bf44b5420f16f0b438b5b9357e5132211cfbad135d + deploySerial: 0 + booted: + image: + image: + image: quay.io/example/someimage:latest + transport: registry + signature: insecure + version: nightly + timestamp: 2023-09-30T19:22:16Z + imageDigest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34 + incompatible: false + pinned: false + ostree: + checksum: 26836632adf6228d64ef07a26fd3efaf177104efd1f341a2cf7909a3e4e2c72c + deploySerial: 0 + rollback: null + isContainer: false diff --git a/lib/src/fixtures/spec-staged-rollback.yaml b/lib/src/fixtures/spec-staged-rollback.yaml new file mode 100644 index 00000000..98354171 --- /dev/null +++ b/lib/src/fixtures/spec-staged-rollback.yaml @@ -0,0 +1,40 @@ +apiVersion: org.containers.bootc/v1alpha1 +kind: BootcHost +metadata: + name: host +spec: + image: + image: quay.io/example/someimage:latest + transport: registry + signature: insecure +status: + staged: + image: + image: + image: quay.io/example/someimage:latest + transport: registry + signature: insecure + version: nightly + timestamp: 2023-10-14T19:22:15Z + imageDigest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566 + incompatible: false + pinned: false + ostree: + checksum: 3c6dad657109522e0b2e49bf44b5420f16f0b438b5b9357e5132211cfbad135d + deploySerial: 0 + booted: null + rollback: + image: + image: + image: quay.io/example/someimage:latest + transport: registry + signature: insecure + version: nightly + timestamp: 2023-09-30T19:22:16Z + imageDigest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34 + incompatible: false + pinned: false + ostree: + checksum: 26836632adf6228d64ef07a26fd3efaf177104efd1f341a2cf7909a3e4e2c72c + deploySerial: 0 + isContainer: false diff --git a/lib/src/status.rs b/lib/src/status.rs index 34377e8e..67f55062 100644 --- a/lib/src/status.rs +++ b/lib/src/status.rs @@ -1,4 +1,6 @@ use std::collections::VecDeque; +use std::io::IsTerminal; +use std::io::Write; use anyhow::{Context, Result}; use camino::Utf8Path; @@ -305,6 +307,8 @@ pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> { let mut out = out.lock(); let legacy_opt = if opts.json { OutputFormat::Json + } else if std::io::stdout().is_terminal() { + OutputFormat::HumanReadable } else { OutputFormat::Yaml }; @@ -312,12 +316,144 @@ pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> { match format { OutputFormat::Json => serde_json::to_writer(&mut out, &host).map_err(anyhow::Error::new), OutputFormat::Yaml => serde_yaml::to_writer(&mut out, &host).map_err(anyhow::Error::new), + OutputFormat::HumanReadable => human_readable_output(&mut out, &host), } .context("Writing to stdout")?; Ok(()) } +fn human_readable_output(mut out: impl Write, host: &Host) -> Result<()> { + for (status_string, status) in [ + ("staged", &host.status.staged), + ("booted", &host.status.booted), + ("rollback", &host.status.rollback), + ] { + if let Some(host_status) = status { + if let Some(image) = &host_status.image { + writeln!( + out, + "Current {} image: {}", + status_string, image.image.image + )?; + + 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 transport = &image.image.transport; + let digest = &image.image_digest; + + writeln!(out, " Image version: {version} ({timestamp})")?; + writeln!(out, " Image transport: {transport}")?; + writeln!(out, " Image digest: {digest}")?; + } else { + writeln!(out, "Current {status_string} state is native ostree")?; + } + } else { + writeln!(out, "No {status_string} image present")?; + } + } + Ok(()) +} + +fn human_status_from_spec_fixture(spec_fixture: &str) -> Result { + let host: Host = serde_yaml::from_str(spec_fixture).unwrap(); + let mut w = Vec::new(); + human_readable_output(&mut w, &host).unwrap(); + let w = String::from_utf8(w).unwrap(); + Ok(w) +} + +#[test] +fn test_human_readable_base_spec() { + // Tests Staged and Booted, null Rollback + 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 transport: registry + Image digest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566 + Current booted image: quay.io/example/someimage:latest + Image version: nightly (2023-09-30 19:22:16 UTC) + Image transport: registry + Image digest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34 + No rollback image present + "}; + similar_asserts::assert_eq!(w, expected); +} + +#[test] +fn test_human_readable_rfe_spec() { + // Basic rhel for edge bootc install with nothing + let w = + human_status_from_spec_fixture(include_str!("fixtures/spec-rfe-ostree-deployment.yaml")) + .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 + "}; + similar_asserts::assert_eq!(w, expected); +} + +#[test] +fn test_human_readable_staged_spec() { + // staged image, no boot/rollback + 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 transport: registry + Image digest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38 + Current booted state is native ostree + No rollback image present + "}; + similar_asserts::assert_eq!(w, expected); +} + +#[test] +fn test_human_readable_booted_spec() { + // booted image, no staged/rollback + 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 transport: registry + Image digest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38 + No rollback image present + "}; + similar_asserts::assert_eq!(w, expected); +} + +#[test] +fn test_human_readable_staged_rollback_spec() { + // staged/rollback image, no booted + let w = human_status_from_spec_fixture(include_str!("fixtures/spec-staged-rollback.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 transport: registry + Image digest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566 + No booted image present + Current rollback image: quay.io/example/someimage:latest + Image version: nightly (2023-09-30 19:22:16 UTC) + Image transport: registry + Image digest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34 + "}; + similar_asserts::assert_eq!(w, expected); +} + #[test] fn test_convert_signatures() { use std::str::FromStr;