Skip to content

Commit

Permalink
Initial support for container image updates
Browse files Browse the repository at this point in the history
This is part of coreos/fedora-coreos-tracker#1263

If we're booted into a container image, then instead of looking
for the special `fedora-coreos.stream` ostree commit metadata,
we can do the much more obvious and natural thing of looking at the
container image tag.
  • Loading branch information
cgwalters committed Nov 18, 2022
1 parent a503193 commit 03eef81
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 6 deletions.
42 changes: 36 additions & 6 deletions src/rpm_ostree/cli_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -48,6 +51,7 @@ pub struct StatusJson {
#[serde(rename_all = "kebab-case")]
pub struct DeploymentJson {
booted: bool,
container_image_reference: Option<String>,
base_checksum: Option<String>,
#[serde(rename = "base-commit-meta")]
base_metadata: BaseCommitMetaJson,
Expand All @@ -62,7 +66,7 @@ pub struct DeploymentJson {
#[derive(Clone, Debug, Deserialize)]
struct BaseCommitMetaJson {
#[serde(rename = "fedora-coreos.stream")]
stream: String,
stream: Option<String>,
}

impl DeploymentJson {
Expand All @@ -89,11 +93,37 @@ pub fn parse_booted(status: &StatusJson) -> Result<Release> {
Ok(json.into_release())
}

fn fedora_coreos_stream_from_deployment(deploy: &DeploymentJson) -> Result<String> {
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<String> {
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.
Expand All @@ -105,8 +135,7 @@ pub fn parse_pending_deployment(status: &StatusJson) -> Result<Option<(Release,
match staged {
None => 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)))
}
Expand Down Expand Up @@ -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");
}
}
207 changes: 207 additions & 0 deletions src/rpm_ostree/imageref.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<Self> {
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> {
Self::try_from(s)
}
}

impl TryFrom<&str> for SignatureSource {
type Error = anyhow::Error;

fn try_from(value: &str) -> Result<Self> {
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> {
Self::try_from(s)
}
}

impl TryFrom<&str> for OstreeImageReference {
type Error = anyhow::Error;

fn try_from(value: &str) -> Result<Self> {
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> {
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)
}
}
}
}
2 changes: 2 additions & 0 deletions src/rpm_ostree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub use actor::{
QueryPendingDeploymentStream, RegisterAsDriver, RpmOstreeClient, StageDeployment,
};

mod imageref;

#[cfg(test)]
mod mock_tests;

Expand Down

0 comments on commit 03eef81

Please sign in to comment.