From 8701c631577e104ef523094da4433e5686110f25 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sat, 1 Jul 2023 05:07:50 -0400 Subject: [PATCH] WIP host system state --- lib/Cargo.toml | 4 + lib/src/cli.rs | 212 ++++++++++++++++++++++++++++---------------- lib/src/lib.rs | 1 + lib/src/spec.rs | 101 +++++++++++++++++++++ lib/src/status.rs | 217 +++++++++++++++++++++++++++++----------------- lib/src/utils.rs | 28 ------ 6 files changed, 381 insertions(+), 182 deletions(-) create mode 100644 lib/src/spec.rs diff --git a/lib/Cargo.toml b/lib/Cargo.toml index eaf785e7..04189139 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 6413911c..23fea4ea 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -30,6 +30,13 @@ pub(crate) struct UpgradeOpts { pub(crate) touch_if_changed: Option, } +/// Interactively change desired system state +#[derive(Debug, Parser)] +pub(crate) struct EditOpts { + /// Input path to new d + pub(crate) filename: String, +} + /// Perform an upgrade operation #[derive(Debug, Parser)] pub(crate) struct SwitchOpts { @@ -102,6 +109,8 @@ pub(crate) enum Opt { /// Look for updates to the booted container image. #[clap(alias = "update")] Upgrade(UpgradeOpts), + /// Change desired host system state + Edit(EditOpts), /// Target a new container image reference to boot. Switch(SwitchOpts), /// Display status @@ -263,36 +272,37 @@ 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 { - 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 fetched = pull(repo, &imgref, opts.quiet).await?; - - if fetched.merge_commit.as_str() == commit.as_str() { - println!("Already queued: {digest}"); - return Ok(()); - } - - stage(sysroot, &osname, &imgref, fetched, &origin).await?; - - if let Some(path) = opts.touch_if_changed { - std::fs::write(&path, "").with_context(|| format!("Writing {path}"))?; - } + todo!(); + // 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 { + // 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 fetched = pull(repo, &imgref, opts.quiet).await?; + + // if fetched.merge_commit.as_str() == commit.as_str() { + // println!("Already queued: {digest}"); + // return Ok(()); + // } + + // stage(sysroot, &osname, &imgref, fetched, &origin).await?; + + // if let Some(path) = opts.touch_if_changed { + // std::fs::write(&path, "").with_context(|| format!("Writing {path}"))?; + // } Ok(()) } @@ -302,51 +312,104 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> { 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 repo = &sysroot.repo(); - - let transport = ostree_container::Transport::try_from(opts.transport.as_str())?; - let imgref = ostree_container::ImageReference { - transport, - name: opts.target.to_string(), - }; - let sigverify = if opts.no_signature_verification { - SignatureSource::ContainerPolicyAllowInsecure - } else if let Some(remote) = opts.ostree_remote.as_ref() { - SignatureSource::OstreeRemote(remote.to_string()) - } else { - SignatureSource::ContainerPolicy - }; - let target = ostree_container::OstreeImageReference { sigverify, imgref }; - - 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)?; - } - } + todo!() + // 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().unwrap(); + // let repo = &sysroot.repo().unwrap(); + + // let transport = ostree_container::Transport::try_from(opts.transport.as_str())?; + // let imgref = ostree_container::ImageReference { + // transport, + // name: opts.target.to_string(), + // }; + // let sigverify = if opts.no_signature_verification { + // SignatureSource::ContainerPolicyAllowInsecure + // } else if let Some(remote) = opts.ostree_remote.as_ref() { + // SignatureSource::OstreeRemote(remote.to_string()) + // } else { + // SignatureSource::ContainerPolicy + // }; + // let target = ostree_container::OstreeImageReference { sigverify, imgref }; + + // 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)?; + // } + // } + + // // 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?; + + // Ok(()) +} - // 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?; +/// Implementation of the `bootc edit` CLI command. +#[context("Editing state")] +async fn edit(_opts: EditOpts) -> Result<()> { + prepare_for_write().await?; + todo!(); + // 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 repo = &sysroot.repo(); + + // let transport = ostree_container::Transport::try_from(opts.transport.as_str())?; + // let imgref = ostree_container::ImageReference { + // transport, + // name: opts.target.to_string(), + // }; + // let sigverify = if opts.no_signature_verification { + // SignatureSource::ContainerPolicyAllowInsecure + // } else if let Some(remote) = opts.ostree_remote.as_ref() { + // SignatureSource::OstreeRemote(remote.to_string()) + // } else { + // SignatureSource::ContainerPolicy + // }; + // let target = ostree_container::OstreeImageReference { sigverify, imgref }; + + // 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)?; + // } + // } + + // // 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?; Ok(()) } @@ -371,6 +434,7 @@ where let opt = Opt::parse_from(args); match opt { Opt::Upgrade(opts) => upgrade(opts).await, + Opt::Edit(opts) => edit(opts).await, Opt::Switch(opts) => switch(opts).await, Opt::UsrOverlay => usroverlay().await, #[cfg(feature = "install")] diff --git a/lib/src/lib.rs b/lib/src/lib.rs index d1f03152..58f27a3d 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/spec.rs b/lib/src/spec.rs new file mode 100644 index 00000000..fc2ef441 --- /dev/null +++ b/lib/src/spec.rs @@ -0,0 +1,101 @@ +//! The definition for host system state. + +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +fn default_transport() -> String { + "registry".into() +} + +/// 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" +)] +pub struct HostDef {} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] +/// An image signature +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, JsonSchema)] +pub struct ImageReference { + /// The container image reference + pub image: String, + /// The container image transport + #[serde(default = "default_transport")] + pub transport: String, + /// Disable signature verification + pub signature: ImageSignature, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] +/// Specification for host system +pub struct HostSpec { + /// The host image + pub image: ImageReference, + // TODO enable this + // pub retain_rollback: bool, +} + +/// The status of the booted image +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] +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, JsonSchema)] +pub struct BootEntryOstree { + /// The ostree commit checksum + pub checksum: String, + pub deploy_serial: u32, +} + +/// A bootable entry +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct BootEntry { + /// The image reference + pub image: Option, + /// Whether this boot entry supports changes via bootc + pub compatible: 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, Deserialize, PartialEq, JsonSchema)] +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 10d9f7f7..09ad0a83 100644 --- a/lib/src/status.rs +++ b/lib/src/status.rs @@ -1,136 +1,193 @@ use std::borrow::Cow; +use std::collections::VecDeque; +use crate::spec::{BootEntry, Host, HostDef, 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}; +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, + } + } +} -/// 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 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, + } + } } -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, + signature: imgref.sigverify.into(), + transport: imgref.imgref.transport.to_string(), image: imgref.imgref.name.clone(), } } } -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 { + staged: Option, + rollback: Option, + 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, compatible) = 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 supported = !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, + }), + supported, + ) + } else { + (None, false) + } + } else { + (None, false) + }; + let r = BootEntry { + image, + compatible, + 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( +fn get_status( sysroot: &SysrootLock, booted_deployment: Option<&ostree::Deployment>, - booted_only: bool, -) -> Result> { +) -> Result<(Deployments, HostDef)> { + let stateroot = booted_deployment.as_ref().map(|d| d.osname()); let deployment_is_booted = |d: &ostree::Deployment| -> bool { booted_deployment.as_ref().map_or(false, |b| d.equal(b)) }; - sysroot + 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)) - }) - .collect() + .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 status = HostStatus { + staged, + booted, + rollback, + is_container, + }; + let hostdef = Host { + metadata: todo!(), + spec: todo!(), + status: todo!(), + }; + // let spec = Host::new("bootc", ) + + todo!() } /// 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 (deployments, host) = get_status(&sysroot, booted_deployment.as_ref())?; let sysroot = super::cli::get_locked_sysroot().await?; let repo = &sysroot.repo(); let booted_deployment = sysroot.booted_deployment(); - 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. 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")?; + serde_json::to_writer(&mut out, &host).context("Writing to stdout")?; return Ok(()); } diff --git a/lib/src/utils.rs b/lib/src/utils.rs index 67ae9f7f..ba96eeef 100644 --- a/lib/src/utils.rs +++ b/lib/src/utils.rs @@ -1,4 +1,3 @@ -use std::fmt::Display; use std::process::Command; use anyhow::{Context, Result}; @@ -7,23 +6,6 @@ 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 +20,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 {