From 9f1923597f3b23cce431cde5c0fa8dd91e383dae Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sat, 1 Jul 2023 05:07:50 -0400 Subject: [PATCH] Rework API to use a Kubernetes CRD I was working on the configmap support slowly, but a big issue it raised was how we expose the ability to mutate things. Our current "API" is just an imperative CLI. This would require higher level tooling to manage Kubernetes style declarative system state. Instead, let's use the really nice Rust `kube` crate to define a CRD; this is what is output (in YAML form) via `bootc status` now. We translate the imperative CLI verbs into changes to the `spec` field. However, things become more compelling when we offer a `bootc edit` CLI verb that allows arbitrary changes to the spec. I think this will become the *only* way to manage attached configmaps, instead of having imperative CLI verbs like `bootc configmap add` etc. At least to start. Signed-off-by: Colin Walters --- lib/Cargo.toml | 4 + lib/src/cli.rs | 134 +++++++++++-------- lib/src/lib.rs | 1 + lib/src/privtests.rs | 7 +- lib/src/spec.rs | 98 ++++++++++++++ lib/src/status.rs | 307 ++++++++++++++++++++++++------------------- lib/src/utils.rs | 32 ----- tests/kolainst/basic | 2 +- 8 files changed, 362 insertions(+), 223 deletions(-) create mode 100644 lib/src/spec.rs diff --git a/lib/Cargo.toml b/lib/Cargo.toml index eaf785e74..04189139c 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -19,6 +19,8 @@ hex = "^0.4" fn-error-context = "0.2.0" gvariant = "0.4.0" indicatif = "0.17.0" +k8s-openapi = { version = "0.18.0", features = ["v1_25"] } +kube = { version = "0.83.0", features = ["runtime", "derive"] } libc = "^0.2" liboverdrop = "0.1.0" once_cell = "1.9" @@ -26,8 +28,10 @@ openssl = "^0.10" nix = ">= 0.24, < 0.26" regex = "1.7.1" rustix = { "version" = "0.37", features = ["thread", "process"] } +schemars = "0.8.6" serde = { features = ["derive"], version = "1.0.125" } serde_json = "1.0.64" +serde_yaml = "0.9.17" serde_with = ">= 1.9.4, < 2" tokio = { features = ["io-std", "time", "process", "rt", "net"], version = ">= 1.13.0" } tokio-util = { features = ["io-util"], version = "0.7" } diff --git a/lib/src/cli.rs b/lib/src/cli.rs index b1a252432..58db41b15 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -19,6 +19,9 @@ use std::ffi::OsString; use std::os::unix::process::CommandExt; use std::process::Command; +use crate::spec::HostSpec; +use crate::spec::ImageReference; + /// Perform an upgrade operation #[derive(Debug, Parser)] pub(crate) struct UpgradeOpts { @@ -174,9 +177,10 @@ pub(crate) async fn get_locked_sysroot() -> Result Result> { + let imgref = &OstreeImageReference::from(imgref.clone()); let config = Default::default(); let mut imp = ostree_container::store::ImageImporter::new(repo, imgref, config).await?; let prep = match imp.prepare().await? { @@ -215,22 +219,35 @@ async fn pull( async fn stage( sysroot: &SysrootLock, stateroot: &str, - imgref: &ostree_container::OstreeImageReference, image: Box, - origin: &glib::KeyFile, + spec: &HostSpec, ) -> Result<()> { let cancellable = gio::Cancellable::NONE; let stateroot = Some(stateroot); let merge_deployment = sysroot.merge_deployment(stateroot); + let origin = glib::KeyFile::new(); + let ostree_imgref = spec + .image + .as_ref() + .map(|imgref| OstreeImageReference::from(imgref.clone())); + if let Some(imgref) = ostree_imgref.as_ref() { + origin.set_string( + "origin", + ostree_container::deploy::ORIGIN_CONTAINER, + imgref.to_string().as_str(), + ); + } let _new_deployment = sysroot.stage_tree_with_options( stateroot, image.merge_commit.as_str(), - Some(origin), + Some(&origin), merge_deployment.as_ref(), &Default::default(), cancellable, )?; - println!("Queued for next boot: {imgref}"); + if let Some(imgref) = ostree_imgref.as_ref() { + println!("Queued for next boot: {imgref}"); + } Ok(()) } @@ -266,30 +283,30 @@ async fn prepare_for_write() -> Result<()> { async fn upgrade(opts: UpgradeOpts) -> Result<()> { prepare_for_write().await?; let sysroot = &get_locked_sysroot().await?; - let repo = &sysroot.repo(); let booted_deployment = &sysroot.require_booted_deployment()?; - let status = crate::status::DeploymentStatus::from_deployment(booted_deployment, true)?; - let osname = booted_deployment.osname(); - let origin = booted_deployment - .origin() - .ok_or_else(|| anyhow::anyhow!("Deployment is missing an origin"))?; - let imgref = status - .image - .ok_or_else(|| anyhow::anyhow!("Booted deployment is not container image based"))?; - let imgref: OstreeImageReference = imgref.into(); - if !status.supported { + let (_deployments, host) = crate::status::get_status(sysroot, Some(booted_deployment))?; + // SAFETY: There must be a status if we have a booted deployment + let status = host.status.unwrap(); + let imgref = host.spec.image.as_ref(); + // If there's no specified image, let's be nice and check if the booted system is using rpm-ostree + if imgref.is_none() && status.booted.map_or(false, |b| b.incompatible) { return Err(anyhow::anyhow!( "Booted deployment contains local rpm-ostree modifications; cannot upgrade via bootc" )); } - let commit = booted_deployment.csum(); - let state = ostree_container::store::query_image_commit(repo, &commit)?; - let digest = state.manifest_digest.as_str(); - + let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?; + // Find the currently queued digest, if any before we pull + let queued_digest = status + .staged + .as_ref() + .and_then(|e| e.image.as_ref()) + .map(|img| img.image_digest.as_str()); if opts.check { // pull the image manifest without the layers let config = Default::default(); - let mut imp = ostree_container::store::ImageImporter::new(repo, &imgref, config).await?; + let imgref = &OstreeImageReference::from(imgref.clone()); + let mut imp = + ostree_container::store::ImageImporter::new(&sysroot.repo(), imgref, config).await?; match imp.prepare().await? { PrepareResult::AlreadyPresent(c) => { println!( @@ -298,24 +315,27 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> { ); return Ok(()); } - PrepareResult::Ready(p) => { + PrepareResult::Ready(r) => { + // TODO show a diff println!( - "New manifest available for {}. Digest {}", - imgref, p.manifest_digest + "New image available for {imgref}. Digest {}", + r.manifest_digest ); + // Note here we'll fall through to handling the --touch-if-changed below } } } else { - let fetched = pull(repo, &imgref, opts.quiet).await?; - - if fetched.merge_commit.as_str() == commit.as_str() { - println!("Already queued: {digest}"); - return Ok(()); + let fetched = pull(&sysroot.repo(), imgref, opts.quiet).await?; + if let Some(queued_digest) = queued_digest { + if fetched.merge_commit.as_str() == queued_digest { + println!("Already queued: {queued_digest}"); + return Ok(()); + } } - stage(sysroot, &osname, &imgref, fetched, &origin).await?; + let osname = booted_deployment.osname(); + stage(sysroot, &osname, fetched, &host.spec).await?; } - if let Some(path) = opts.touch_if_changed { std::fs::write(&path, "").with_context(|| format!("Writing {path}"))?; } @@ -327,14 +347,14 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> { #[context("Switching")] async fn switch(opts: SwitchOpts) -> Result<()> { prepare_for_write().await?; - let cancellable = gio::Cancellable::NONE; - let sysroot = get_locked_sysroot().await?; - let booted_deployment = &sysroot.require_booted_deployment()?; - let (origin, booted_image) = crate::utils::get_image_origin(booted_deployment)?; - let booted_refspec = origin.optional_string("origin", "refspec")?; - let osname = booted_deployment.osname(); + + let sysroot = &get_locked_sysroot().await?; let repo = &sysroot.repo(); + let booted_deployment = &sysroot.require_booted_deployment()?; + let (_deployments, host) = crate::status::get_status(sysroot, Some(booted_deployment))?; + // SAFETY: There must be a status if we have a booted deployment + let status = host.status.unwrap(); let transport = ostree_container::Transport::try_from(opts.transport.as_str())?; let imgref = ostree_container::ImageReference { @@ -349,30 +369,38 @@ async fn switch(opts: SwitchOpts) -> Result<()> { SignatureSource::ContainerPolicy }; let target = ostree_container::OstreeImageReference { sigverify, imgref }; + let target = ImageReference::from(target); + + let new_spec = { + let mut new_spec = host.spec.clone(); + new_spec.image = Some(target.clone()); + new_spec + }; + + if new_spec == host.spec { + anyhow::bail!("No changes in current host spec"); + } let fetched = pull(repo, &target, opts.quiet).await?; if !opts.retain { // By default, we prune the previous ostree ref or container image - if let Some(ostree_ref) = booted_refspec { - let (remote, ostree_ref) = - ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?; - repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?; - origin.remove_key("origin", "refspec")?; - } else if let Some(booted_image) = booted_image.as_ref() { - ostree_container::store::remove_image(repo, &booted_image.imgref)?; - let _nlayers: u32 = ostree_container::store::gc_image_layers(repo)?; + if let Some(booted_origin) = booted_deployment.origin() { + if let Some(ostree_ref) = booted_origin.optional_string("origin", "refspec")? { + let (remote, ostree_ref) = + ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?; + repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?; + } else if let Some(booted_image) = status.booted.as_ref().and_then(|b| b.image.as_ref()) + { + let imgref = OstreeImageReference::from(booted_image.image.clone()); + ostree_container::store::remove_image(repo, &imgref.imgref)?; + let _nlayers: u32 = ostree_container::store::gc_image_layers(repo)?; + } } } - // We always make a fresh origin to toss out old state. - let origin = glib::KeyFile::new(); - origin.set_string( - "origin", - ostree_container::deploy::ORIGIN_CONTAINER, - target.to_string().as_str(), - ); - stage(&sysroot, &osname, &target, fetched, &origin).await?; + let stateroot = booted_deployment.osname(); + stage(sysroot, &stateroot, fetched, &new_spec).await?; Ok(()) } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index d1f031520..58f27a3d3 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -36,6 +36,7 @@ mod install; pub(crate) mod mount; #[cfg(feature = "install")] mod podman; +pub mod spec; #[cfg(feature = "install")] mod task; diff --git a/lib/src/privtests.rs b/lib/src/privtests.rs index 04510a385..d1c996a1f 100644 --- a/lib/src/privtests.rs +++ b/lib/src/privtests.rs @@ -7,6 +7,7 @@ use rustix::fd::AsFd; use xshell::{cmd, Shell}; use super::cli::TestingOpts; +use super::spec::Host; const IMGSIZE: u64 = 20 * 1024 * 1024 * 1024; @@ -101,9 +102,9 @@ pub(crate) fn impl_run_host() -> Result<()> { pub(crate) fn impl_run_container() -> Result<()> { assert!(ostree_ext::container_utils::is_ostree_container()?); let sh = Shell::new()?; - let stout = cmd!(sh, "bootc status").read()?; - assert!(stout.contains("Running in a container (ostree base).")); - drop(stout); + let host: Host = serde_yaml::from_str(&cmd!(sh, "bootc status").read()?)?; + let status = host.status.unwrap(); + assert!(status.is_container); for c in ["upgrade", "update"] { let o = Command::new("bootc").arg(c).output()?; let st = o.status; diff --git a/lib/src/spec.rs b/lib/src/spec.rs new file mode 100644 index 000000000..e5c40e499 --- /dev/null +++ b/lib/src/spec.rs @@ -0,0 +1,98 @@ +//! The definition for host system state. + +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Representation of a bootc host system +#[derive( + CustomResource, Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, JsonSchema, +)] +#[kube( + group = "org.containers.bootc", + version = "v1alpha1", + kind = "BootcHost", + struct = "Host", + namespaced, + status = "HostStatus", + derive = "PartialEq", + derive = "Default" +)] +#[serde(rename_all = "camelCase")] +pub struct HostSpec { + /// The host image + pub image: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +/// An image signature +#[serde(rename_all = "camelCase")] +pub enum ImageSignature { + /// Fetches will use the named ostree remote for signature verification of the ostree commit. + OstreeRemote(String), + /// Fetches will defer to the `containers-policy.json`, but we make a best effort to reject `default: insecureAcceptAnything` policy. + ContainerPolicy, + /// No signature verification will be performed + Insecure, +} + +/// A container image reference with attached transport and signature verification +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ImageReference { + /// The container image reference + pub image: String, + /// The container image transport + pub transport: String, + /// Disable signature verification + pub signature: ImageSignature, +} + +/// The status of the booted image +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ImageStatus { + /// The currently booted image + pub image: ImageReference, + /// The digest of the fetched image (e.g. sha256:a0...); + pub image_digest: String, +} + +/// A bootable entry +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct BootEntryOstree { + /// The ostree commit checksum + pub checksum: String, + /// The deployment serial + pub deploy_serial: u32, +} + +/// A bootable entry +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct BootEntry { + /// The image reference + pub image: Option, + /// Whether this boot entry is not compatible (has origin changes bootc does not understand) + pub incompatible: bool, + /// Whether this entry will be subject to garbage collection + pub pinned: bool, + /// If this boot entry is ostree based, the corresponding state + pub ostree: Option, +} + +/// The status of the host system +#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct HostStatus { + /// The staged image for the next boot + pub staged: Option, + /// The booted image; this will be unset if the host is not bootc compatible. + pub booted: Option, + /// The previously booted image + pub rollback: Option, + + /// Whether or not the current system state is an ostree-based container + pub is_container: bool, +} diff --git a/lib/src/status.rs b/lib/src/status.rs index 10d9f7f73..31d59ed9b 100644 --- a/lib/src/status.rs +++ b/lib/src/status.rs @@ -1,180 +1,219 @@ -use std::borrow::Cow; +use std::collections::VecDeque; +use crate::spec::{BootEntry, Host, HostSpec, HostStatus, ImageStatus}; +use crate::spec::{ImageReference, ImageSignature}; use anyhow::{Context, Result}; +use ostree::glib; use ostree_container::OstreeImageReference; use ostree_ext::container as ostree_container; +use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::ostree; use ostree_ext::sysroot::SysrootLock; -use crate::utils::{get_image_origin, ser_with_display}; +const OBJECT_NAME: &str = "host"; -/// Representation of a container image reference suitable for serialization to e.g. JSON. -#[derive(Debug, Clone, serde::Serialize)] -pub(crate) struct Image { - #[serde(serialize_with = "ser_with_display")] - pub(crate) verification: ostree_container::SignatureSource, - #[serde(serialize_with = "ser_with_display")] - pub(crate) transport: ostree_container::Transport, - pub(crate) image: String, +impl From for ImageSignature { + fn from(sig: ostree_container::SignatureSource) -> Self { + use ostree_container::SignatureSource; + match sig { + SignatureSource::OstreeRemote(r) => Self::OstreeRemote(r), + SignatureSource::ContainerPolicy => Self::ContainerPolicy, + SignatureSource::ContainerPolicyAllowInsecure => Self::Insecure, + } + } +} + +impl From for ostree_container::SignatureSource { + fn from(sig: ImageSignature) -> Self { + use ostree_container::SignatureSource; + match sig { + ImageSignature::OstreeRemote(r) => SignatureSource::OstreeRemote(r), + ImageSignature::ContainerPolicy => Self::ContainerPolicy, + ImageSignature::Insecure => Self::ContainerPolicyAllowInsecure, + } + } +} + +/// Fixme lower serializability into ostree-ext +fn transport_to_string(transport: ostree_container::Transport) -> String { + match transport { + // Canonicalize to registry for our own use + ostree_container::Transport::Registry => "registry".to_string(), + o => { + let mut s = o.to_string(); + s.truncate(s.rfind(':').unwrap()); + s + } + } } -impl From<&OstreeImageReference> for Image { - fn from(imgref: &OstreeImageReference) -> Self { +impl From for ImageReference { + fn from(imgref: OstreeImageReference) -> Self { Self { - verification: imgref.sigverify.clone(), - transport: imgref.imgref.transport, - image: imgref.imgref.name.clone(), + signature: imgref.sigverify.into(), + transport: transport_to_string(imgref.imgref.transport), + image: imgref.imgref.name, } } } -impl From for OstreeImageReference { - fn from(img: Image) -> OstreeImageReference { - OstreeImageReference { - sigverify: img.verification, +impl From for OstreeImageReference { + fn from(img: ImageReference) -> Self { + Self { + sigverify: img.signature.into(), imgref: ostree_container::ImageReference { - transport: img.transport, + /// SAFETY: We validated the schema in kube-rs + transport: img.transport.as_str().try_into().unwrap(), name: img.image, }, } } } -/// Representation of a deployment suitable for serialization to e.g. JSON. -#[derive(serde::Serialize)] -pub(crate) struct DeploymentStatus { - pub(crate) pinned: bool, - pub(crate) booted: bool, - pub(crate) staged: bool, - pub(crate) supported: bool, - pub(crate) image: Option, - pub(crate) checksum: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) deploy_serial: Option, +/// Parse an ostree origin file (a keyfile) and extract the targeted +/// container image reference. +fn get_image_origin(origin: &glib::KeyFile) -> Result> { + origin + .optional_string("origin", ostree_container::deploy::ORIGIN_CONTAINER) + .context("Failed to load container image from origin")? + .map(|v| ostree_container::OstreeImageReference::try_from(v.as_str())) + .transpose() } -/// This struct is serialized when we're running in a container image. -#[derive(serde::Serialize)] -pub(crate) struct StatusInContainer { - pub(crate) is_container: bool, +pub(crate) struct Deployments { + pub(crate) staged: Option, + pub(crate) rollback: Option, + #[allow(dead_code)] + pub(crate) other: VecDeque, } -impl DeploymentStatus { - /// Gather metadata from an ostree deployment into a Rust structure - pub(crate) fn from_deployment(deployment: &ostree::Deployment, booted: bool) -> Result { - let staged = deployment.is_staged(); - let pinned = deployment.is_pinned(); - let image = get_image_origin(deployment)?.1; - let checksum = deployment.csum().to_string(); - let deploy_serial = (!staged).then(|| deployment.bootserial().try_into().unwrap()); - let supported = deployment - .origin() - .map(|o| !crate::utils::origin_has_rpmostree_stuff(&o)) - .unwrap_or_default(); - - Ok(DeploymentStatus { - staged, - pinned, - booted, - supported, - image: image.as_ref().map(Into::into), - checksum, - deploy_serial, - }) - } +fn boot_entry_from_deployment( + sysroot: &SysrootLock, + deployment: &ostree::Deployment, +) -> Result { + let repo = &sysroot.repo(); + let (image, incompatible) = if let Some(origin) = deployment.origin().as_ref() { + if let Some(image) = get_image_origin(origin)? { + let image = ImageReference::from(image); + let csum = deployment.csum(); + let incompatible = crate::utils::origin_has_rpmostree_stuff(origin); + let imgstate = ostree_container::store::query_image_commit(repo, &csum)?; + ( + Some(ImageStatus { + image, + image_digest: imgstate.manifest_digest, + }), + incompatible, + ) + } else { + (None, false) + } + } else { + (None, false) + }; + let r = BootEntry { + image, + incompatible, + pinned: deployment.is_pinned(), + ostree: Some(crate::spec::BootEntryOstree { + checksum: deployment.csum().into(), + // SAFETY: The deployserial is really unsigned + deploy_serial: deployment.deployserial().try_into().unwrap(), + }), + }; + Ok(r) } /// Gather the ostree deployment objects, but also extract metadata from them into /// a more native Rust structure. -fn get_deployments( +pub(crate) fn get_status( sysroot: &SysrootLock, booted_deployment: Option<&ostree::Deployment>, - booted_only: bool, -) -> Result> { - let deployment_is_booted = |d: &ostree::Deployment| -> bool { - booted_deployment.as_ref().map_or(false, |b| d.equal(b)) - }; - sysroot +) -> Result<(Deployments, Host)> { + let stateroot = booted_deployment.as_ref().map(|d| d.osname()); + let (mut related_deployments, other_deployments) = sysroot .deployments() .into_iter() - .filter(|deployment| !booted_only || deployment_is_booted(deployment)) - .map(|deployment| -> Result<_> { - let booted = deployment_is_booted(&deployment); - let status = DeploymentStatus::from_deployment(&deployment, booted)?; - Ok((deployment, status)) + .partition::, _>(|d| Some(d.osname()) == stateroot); + let staged = related_deployments + .iter() + .position(|d| d.is_staged()) + .map(|i| related_deployments.remove(i).unwrap()); + // Filter out the booted, the caller already found that + if let Some(booted) = booted_deployment.as_ref() { + related_deployments.retain(|f| !f.equal(booted)); + } + let rollback = related_deployments.pop_front(); + let other = { + related_deployments.extend(other_deployments); + related_deployments + }; + let deployments = Deployments { + staged, + rollback, + other, + }; + + let is_container = ostree_ext::container_utils::is_ostree_container()?; + + let staged = deployments + .staged + .as_ref() + .map(|d| boot_entry_from_deployment(sysroot, d)) + .transpose()?; + let booted = booted_deployment + .as_ref() + .map(|d| boot_entry_from_deployment(sysroot, d)) + .transpose()?; + let rollback = deployments + .rollback + .as_ref() + .map(|d| boot_entry_from_deployment(sysroot, d)) + .transpose()?; + let spec = staged + .as_ref() + .or(booted.as_ref()) + .and_then(|entry| entry.image.as_ref()) + .map(|img| HostSpec { + image: Some(img.image.clone()), }) - .collect() + .unwrap_or_default(); + let mut host = Host::new(OBJECT_NAME, spec); + host.status = Some(HostStatus { + staged, + booted, + rollback, + is_container, + }); + Ok((deployments, host)) } /// Implementation of the `bootc status` CLI command. pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> { - if ostree_ext::container_utils::is_ostree_container()? { - if opts.json { - let mut stdout = std::io::stdout().lock(); - serde_json::to_writer(&mut stdout, &StatusInContainer { is_container: true }) - .context("Serializing status")?; - } else { - println!("Running in a container (ostree base)."); - } - return Ok(()); - } - let sysroot = super::cli::get_locked_sysroot().await?; - let repo = &sysroot.repo(); - let booted_deployment = sysroot.booted_deployment(); + let host = if ostree_ext::container_utils::is_ostree_container()? { + let status = HostStatus { + is_container: true, + ..Default::default() + }; + let mut r = Host::new(OBJECT_NAME, HostSpec { image: None }); + r.status = Some(status); + r + } else { + let sysroot = super::cli::get_locked_sysroot().await?; + let booted_deployment = sysroot.booted_deployment(); + let (_deployments, host) = get_status(&sysroot, booted_deployment.as_ref())?; + host + }; - let deployments = get_deployments(&sysroot, booted_deployment.as_ref(), opts.booted)?; // If we're in JSON mode, then convert the ostree data into Rust-native // structures that can be serialized. + // Filter to just the serializable status structures. + let out = std::io::stdout(); + let mut out = out.lock(); if opts.json { - // Filter to just the serializable status structures. - let deployments = deployments.into_iter().map(|e| e.1).collect::>(); - let out = std::io::stdout(); - let mut out = out.lock(); - serde_json::to_writer(&mut out, &deployments).context("Writing to stdout")?; - return Ok(()); - } - - // We're not writing to JSON; iterate over and print. - for (deployment, info) in deployments { - let booted_display = if info.booted { "* " } else { " " }; - let image: Option = info.image.as_ref().map(|i| i.clone().into()); - - let commit = info.checksum; - if let Some(image) = image.as_ref() { - println!("{booted_display} {image}"); - if !info.supported { - println!(" Origin contains rpm-ostree machine-local changes"); - } else { - let state = ostree_container::store::query_image_commit(repo, &commit)?; - println!(" Digest: {}", state.manifest_digest.as_str()); - let version = state - .configuration - .as_ref() - .and_then(ostree_container::version_for_config); - if let Some(version) = version { - println!(" Version: {version}"); - } - } - } else { - let deployinfo = if let Some(serial) = info.deploy_serial { - Cow::Owned(format!("{commit}.{serial}")) - } else { - Cow::Borrowed(&commit) - }; - println!("{booted_display} {deployinfo}"); - println!(" (Non-container origin type)"); - println!(); - } - println!(" Backend: ostree"); - if deployment.is_pinned() { - println!(" Pinned: yes") - } - if info.booted { - println!(" Booted: yes") - } else if deployment.is_staged() { - println!(" Staged: yes"); - } - println!(); + serde_json::to_writer(&mut out, &host).context("Writing to stdout")?; + } else { + serde_yaml::to_writer(&mut out, &host).context("Writing to stdout")?; } Ok(()) diff --git a/lib/src/utils.rs b/lib/src/utils.rs index 67ae9f7f1..e0a524aa5 100644 --- a/lib/src/utils.rs +++ b/lib/src/utils.rs @@ -1,29 +1,7 @@ -use std::fmt::Display; use std::process::Command; -use anyhow::{Context, Result}; use ostree::glib; -use ostree_container::OstreeImageReference; -use ostree_ext::container as ostree_container; -use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::ostree; -use serde::Serializer; - -/// Parse an ostree origin file (a keyfile) and extract the targeted -/// container image reference. -pub(crate) fn get_image_origin( - deployment: &ostree::Deployment, -) -> Result<(glib::KeyFile, Option)> { - let origin = deployment - .origin() - .ok_or_else(|| anyhow::anyhow!("Missing origin"))?; - let imgref = origin - .optional_string("origin", ostree_container::deploy::ORIGIN_CONTAINER) - .context("Failed to load container image from origin")? - .map(|v| ostree_container::OstreeImageReference::try_from(v.as_str())) - .transpose()?; - Ok((origin, imgref)) -} /// Try to look for keys injected by e.g. rpm-ostree requesting machine-local /// changes; if any are present, return `true`. @@ -38,16 +16,6 @@ pub(crate) fn origin_has_rpmostree_stuff(kf: &glib::KeyFile) -> bool { false } -/// Implement the `Serialize` trait for types that are `Display`. -/// https://stackoverflow.com/questions/58103801/serialize-using-the-display-trait -pub(crate) fn ser_with_display(value: &T, serializer: S) -> Result -where - T: Display, - S: Serializer, -{ - serializer.collect_str(value) -} - /// Run a command in the host mount namespace #[allow(dead_code)] pub(crate) fn run_in_host_mountns(cmd: &str) -> Command { diff --git a/tests/kolainst/basic b/tests/kolainst/basic index ce4469b44..fdf6a9ce7 100755 --- a/tests/kolainst/basic +++ b/tests/kolainst/basic @@ -15,7 +15,7 @@ case "${AUTOPKGTEST_REBOOT_MARK:-}" in bootc status > status.txt grep 'Version:' status.txt bootc status --json > status.json - image=$(jq -r '.[0].image.image' < status.json) + image=$(jq -r '.status.booted.image.image' < status.json) echo "booted into $image" echo "ok status test"