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"