diff --git a/src/rpm_ostree/cli_status.rs b/src/rpm_ostree/cli_status.rs index 179f0a60..694a5f97 100644 --- a/src/rpm_ostree/cli_status.rs +++ b/src/rpm_ostree/cli_status.rs @@ -11,6 +11,9 @@ use std::collections::BTreeSet; use std::fs; use std::rc::Rc; +/// The well-known Fedora CoreOS base image. +const FEDORA_COREOS_CONTAINER: &str = "quay.io/fedora/fedora-coreos"; + /// Path to local OSTree deployments. We use its mtime to check for modifications (e.g. new deployments) /// to local deployments that might warrant querying `rpm-ostree status` again to update our knowledge /// of the current state of deployments. @@ -48,6 +51,7 @@ pub struct StatusJson { #[serde(rename_all = "kebab-case")] pub struct DeploymentJson { booted: bool, + container_image_reference: Option, base_checksum: Option, #[serde(rename = "base-commit-meta")] base_metadata: BaseCommitMetaJson, @@ -62,7 +66,7 @@ pub struct DeploymentJson { #[derive(Clone, Debug, Deserialize)] struct BaseCommitMetaJson { #[serde(rename = "fedora-coreos.stream")] - stream: String, + stream: Option, } impl DeploymentJson { @@ -89,11 +93,37 @@ pub fn parse_booted(status: &StatusJson) -> Result { Ok(json.into_release()) } +fn fedora_coreos_stream_from_deployment(deploy: &DeploymentJson) -> Result { + if let Some(cr) = deploy.container_image_reference.as_deref() { + let cr = super::imageref::OstreeImageReference::try_from(cr) + .with_context(|| format!("Failed to parse container image reference {cr}"))?; + let ir = &cr.imgref; + let tx = ir.transport; + if tx != super::imageref::Transport::Registry { + anyhow::bail!("Unhandled container transport {tx}"); + } + let name = ir.name.as_str(); + let (name, tag) = name + .rsplit_once(':') + .ok_or_else(|| anyhow!("Failed to find tag in {name}"))?; + if name != FEDORA_COREOS_CONTAINER { + anyhow::bail!("Unhandled container image {name}"); + } + ensure!(!tag.is_empty(), "empty tag value"); + Ok(tag.to_string()) + } else { + let stream = deploy.base_metadata.stream.as_deref().ok_or_else(|| { + anyhow!("Failed to find Fedora CoreOS stream metadata from commit object") + })?; + ensure!(!stream.is_empty(), "empty stream value"); + Ok(stream.to_string()) + } +} + /// Parse updates stream for booted deployment from status object. pub fn parse_booted_updates_stream(status: &StatusJson) -> Result { let json = booted_json(status)?; - ensure!(!json.base_metadata.stream.is_empty(), "empty stream value"); - Ok(json.base_metadata.stream) + fedora_coreos_stream_from_deployment(&json) } /// Parse pending deployment from status object. @@ -105,8 +135,7 @@ pub fn parse_pending_deployment(status: &StatusJson) -> Result Ok(None), Some(json) => { - let stream = json.base_metadata.stream.clone(); - ensure!(!stream.is_empty(), "empty stream value"); + let stream = fedora_coreos_stream_from_deployment(&json)?; let release = json.into_release(); Ok(Some((release, stream))) } @@ -239,6 +268,7 @@ mod tests { fn mock_booted_updates_stream() { let status = mock_status("tests/fixtures/rpm-ostree-status.json").unwrap(); let booted = booted_json(&status).unwrap(); - assert_eq!(booted.base_metadata.stream, "testing-devel"); + let stream = fedora_coreos_stream_from_deployment(&booted).unwrap(); + assert_eq!(stream, "testing-devel"); } } diff --git a/src/rpm_ostree/imageref.rs b/src/rpm_ostree/imageref.rs new file mode 100644 index 00000000..c14c6f5e --- /dev/null +++ b/src/rpm_ostree/imageref.rs @@ -0,0 +1,207 @@ +//! This is a copy of code from ostreedev/ostree-rs-ext to avoid +//! depending on that whole library. + +use std::borrow::Cow; +use std::str::FromStr; + +use anyhow::{anyhow, Result}; + +/// A backend/transport for OCI/Docker images. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Transport { + /// A remote Docker/OCI registry (`registry:` or `docker://`) + Registry, + /// A local OCI directory (`oci:`) + OciDir, + /// A local OCI archive tarball (`oci-archive:`) + OciArchive, + /// Local container storage (`containers-storage:`) + ContainerStorage, +} + +/// Combination of a remote image reference and transport. +/// +/// For example, +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImageReference { + /// The storage and transport for the image + pub transport: Transport, + /// The image name (e.g. `quay.io/somerepo/someimage:latest`) + pub name: String, +} + +/// Policy for signature verification. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SignatureSource { + /// 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, + /// NOT RECOMMENDED. Fetches will defer to the `containers-policy.json` default which is usually `insecureAcceptAnything`. + ContainerPolicyAllowInsecure, +} + +/// Combination of a signature verification mechanism, and a standard container image reference. +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OstreeImageReference { + /// The signature verification mechanism. + pub sigverify: SignatureSource, + /// The container image reference. + pub imgref: ImageReference, +} + +impl TryFrom<&str> for Transport { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + Ok(match value { + "registry" | "docker" => Self::Registry, + "oci" => Self::OciDir, + "oci-archive" => Self::OciArchive, + "containers-storage" => Self::ContainerStorage, + o => return Err(anyhow!("Unknown transport '{}'", o)), + }) + } +} + +impl TryFrom<&str> for ImageReference { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + let (transport_name, mut name) = value + .split_once(':') + .ok_or_else(|| anyhow!("Missing ':' in {}", value))?; + let transport: Transport = transport_name.try_into()?; + if name.is_empty() { + return Err(anyhow!("Invalid empty name in {}", value)); + } + if transport_name == "docker" { + name = name + .strip_prefix("//") + .ok_or_else(|| anyhow!("Missing // in docker:// in {}", value))?; + } + Ok(Self { + transport, + name: name.to_string(), + }) + } +} + +impl FromStr for ImageReference { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} + +impl TryFrom<&str> for SignatureSource { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + match value { + "ostree-image-signed" => Ok(Self::ContainerPolicy), + "ostree-unverified-image" => Ok(Self::ContainerPolicyAllowInsecure), + o => match o.strip_prefix("ostree-remote-image:") { + Some(rest) => Ok(Self::OstreeRemote(rest.to_string())), + _ => Err(anyhow!("Invalid signature source: {}", o)), + }, + } + } +} + +impl FromStr for SignatureSource { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} + +impl TryFrom<&str> for OstreeImageReference { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + let (first, second) = value + .split_once(':') + .ok_or_else(|| anyhow!("Missing ':' in {}", value))?; + let (sigverify, rest) = match first { + "ostree-image-signed" => (SignatureSource::ContainerPolicy, Cow::Borrowed(second)), + "ostree-unverified-image" => ( + SignatureSource::ContainerPolicyAllowInsecure, + Cow::Borrowed(second), + ), + // Shorthand for ostree-unverified-image:registry: + "ostree-unverified-registry" => ( + SignatureSource::ContainerPolicyAllowInsecure, + Cow::Owned(format!("registry:{second}")), + ), + // This is a shorthand for ostree-remote-image with registry: + "ostree-remote-registry" => { + let (remote, rest) = second + .split_once(':') + .ok_or_else(|| anyhow!("Missing second ':' in {}", value))?; + ( + SignatureSource::OstreeRemote(remote.to_string()), + Cow::Owned(format!("registry:{rest}")), + ) + } + "ostree-remote-image" => { + let (remote, rest) = second + .split_once(':') + .ok_or_else(|| anyhow!("Missing second ':' in {}", value))?; + ( + SignatureSource::OstreeRemote(remote.to_string()), + Cow::Borrowed(rest), + ) + } + o => { + return Err(anyhow!("Invalid ostree image reference scheme: {}", o)); + } + }; + let imgref = (&*rest).try_into()?; + Ok(Self { sigverify, imgref }) + } +} + +impl FromStr for OstreeImageReference { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} + +impl std::fmt::Display for Transport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + // TODO once skopeo supports this, canonicalize as registry: + Self::Registry => "docker://", + Self::OciArchive => "oci-archive:", + Self::OciDir => "oci:", + Self::ContainerStorage => "containers-storage:", + }; + f.write_str(s) + } +} + +impl std::fmt::Display for ImageReference { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}{}", self.transport, self.name) + } +} + +impl std::fmt::Display for OstreeImageReference { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.sigverify { + SignatureSource::OstreeRemote(r) => { + write!(f, "ostree-remote-image:{}:{}", r, self.imgref) + } + SignatureSource::ContainerPolicy => write!(f, "ostree-image-signed:{}", self.imgref), + SignatureSource::ContainerPolicyAllowInsecure => { + write!(f, "ostree-unverified-image:{}", self.imgref) + } + } + } +} diff --git a/src/rpm_ostree/mod.rs b/src/rpm_ostree/mod.rs index 621882de..6090599f 100644 --- a/src/rpm_ostree/mod.rs +++ b/src/rpm_ostree/mod.rs @@ -9,6 +9,8 @@ pub use actor::{ QueryPendingDeploymentStream, RegisterAsDriver, RpmOstreeClient, StageDeployment, }; +mod imageref; + #[cfg(test)] mod mock_tests;