use anyhow::anyhow;
use cap_std_ext::cap_std;
use cap_std_ext::cap_std::fs::Dir;
use containers_image_proxy::oci_spec;
use ostree::glib;
use serde::Serialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Debug;
use std::ops::Deref;
use std::str::FromStr;
pub const OSTREE_COMMIT_LABEL: &str = "ostree.commit";
pub(crate) const CONTENT_ANNOTATION: &str = "ostree.components";
pub(crate) const COMPONENT_SEPARATOR: char = ',';
type Result<T> = anyhow::Result<T>;
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq)]
pub enum Transport {
Registry,
OciDir,
OciArchive,
DockerArchive,
ContainerStorage,
Dir,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct ImageReference {
pub transport: Transport,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SignatureSource {
OstreeRemote(String),
ContainerPolicy,
ContainerPolicyAllowInsecure,
}
pub const LABEL_VERSION: &str = "version";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OstreeImageReference {
pub sigverify: SignatureSource,
pub imgref: ImageReference,
}
impl TryFrom<&str> for Transport {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self> {
Ok(match value {
Self::REGISTRY_STR | "docker" => Self::Registry,
Self::OCI_STR => Self::OciDir,
Self::OCI_ARCHIVE_STR => Self::OciArchive,
Self::DOCKER_ARCHIVE_STR => Self::DockerArchive,
Self::CONTAINERS_STORAGE_STR => Self::ContainerStorage,
Self::LOCAL_DIRECTORY_STR => Self::Dir,
o => return Err(anyhow!("Unknown transport '{}'", o)),
})
}
}
impl Transport {
const OCI_STR: &'static str = "oci";
const OCI_ARCHIVE_STR: &'static str = "oci-archive";
const DOCKER_ARCHIVE_STR: &'static str = "docker-archive";
const CONTAINERS_STORAGE_STR: &'static str = "containers-storage";
const LOCAL_DIRECTORY_STR: &'static str = "dir";
const REGISTRY_STR: &'static str = "registry";
pub fn serializable_name(&self) -> &'static str {
match self {
Transport::Registry => Self::REGISTRY_STR,
Transport::OciDir => Self::OCI_STR,
Transport::OciArchive => Self::OCI_ARCHIVE_STR,
Transport::DockerArchive => Self::DOCKER_ARCHIVE_STR,
Transport::ContainerStorage => Self::CONTAINERS_STORAGE_STR,
Transport::Dir => Self::LOCAL_DIRECTORY_STR,
}
}
}
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),
),
"ostree-unverified-registry" => (
SignatureSource::ContainerPolicyAllowInsecure,
Cow::Owned(format!("registry:{second}")),
),
"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.deref().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 {
Self::Registry => "docker://",
Self::OciArchive => "oci-archive:",
Self::DockerArchive => "docker-archive:",
Self::OciDir => "oci:",
Self::ContainerStorage => "containers-storage:",
Self::Dir => "dir:",
};
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 SignatureSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SignatureSource::OstreeRemote(r) => write!(f, "ostree-remote-image:{r}"),
SignatureSource::ContainerPolicy => write!(f, "ostree-image-signed"),
SignatureSource::ContainerPolicyAllowInsecure => {
write!(f, "ostree-unverified-image")
}
}
}
}
impl std::fmt::Display for OstreeImageReference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (&self.sigverify, &self.imgref) {
(SignatureSource::ContainerPolicyAllowInsecure, imgref)
if imgref.transport == Transport::Registry =>
{
if f.alternate() {
write!(f, "{}", self.imgref)
} else {
write!(f, "ostree-unverified-registry:{}", self.imgref.name)
}
}
(sigverify, imgref) => {
write!(f, "{}:{}", sigverify, imgref)
}
}
}
}
#[derive(Debug, Serialize)]
pub struct ManifestDiff<'a> {
#[serde(skip)]
pub from: &'a oci_spec::image::ImageManifest,
#[serde(skip)]
pub to: &'a oci_spec::image::ImageManifest,
#[serde(skip)]
pub removed: Vec<&'a oci_spec::image::Descriptor>,
#[serde(skip)]
pub added: Vec<&'a oci_spec::image::Descriptor>,
pub total: u64,
pub total_size: u64,
pub n_removed: u64,
pub removed_size: u64,
pub n_added: u64,
pub added_size: u64,
}
impl<'a> ManifestDiff<'a> {
pub fn new(
src: &'a oci_spec::image::ImageManifest,
dest: &'a oci_spec::image::ImageManifest,
) -> Self {
let src_layers = src
.layers()
.iter()
.map(|l| (l.digest(), l))
.collect::<HashMap<_, _>>();
let dest_layers = dest
.layers()
.iter()
.map(|l| (l.digest(), l))
.collect::<HashMap<_, _>>();
let mut removed = Vec::new();
let mut added = Vec::new();
for (blobid, &descriptor) in src_layers.iter() {
if !dest_layers.contains_key(blobid) {
removed.push(descriptor);
}
}
removed.sort_by(|a, b| a.digest().cmp(b.digest()));
for (blobid, &descriptor) in dest_layers.iter() {
if !src_layers.contains_key(blobid) {
added.push(descriptor);
}
}
added.sort_by(|a, b| a.digest().cmp(b.digest()));
fn layersum<'a, I: Iterator<Item = &'a oci_spec::image::Descriptor>>(layers: I) -> u64 {
layers.map(|layer| layer.size() as u64).sum()
}
let total = dest_layers.len() as u64;
let total_size = layersum(dest.layers().iter());
let n_removed = removed.len() as u64;
let n_added = added.len() as u64;
let removed_size = layersum(removed.iter().copied());
let added_size = layersum(added.iter().copied());
ManifestDiff {
from: src,
to: dest,
removed,
added,
total,
total_size,
n_removed,
removed_size,
n_added,
added_size,
}
}
}
impl<'a> ManifestDiff<'a> {
pub fn print(&self) {
let print_total = self.total;
let print_total_size = glib::format_size(self.total_size);
let print_n_removed = self.n_removed;
let print_removed_size = glib::format_size(self.removed_size);
let print_n_added = self.n_added;
let print_added_size = glib::format_size(self.added_size);
println!("Total new layers: {print_total:<4} Size: {print_total_size}");
println!("Removed layers: {print_n_removed:<4} Size: {print_removed_size}");
println!("Added layers: {print_n_added:<4} Size: {print_added_size}");
}
}
pub fn merge_default_container_proxy_opts(
config: &mut containers_image_proxy::ImageProxyConfig,
) -> Result<()> {
let user = rustix::process::getuid()
.is_root()
.then_some(isolation::DEFAULT_UNPRIVILEGED_USER);
merge_default_container_proxy_opts_with_isolation(config, user)
}
pub fn merge_default_container_proxy_opts_with_isolation(
config: &mut containers_image_proxy::ImageProxyConfig,
isolation_user: Option<&str>,
) -> Result<()> {
let auth_specified =
config.auth_anonymous || config.authfile.is_some() || config.auth_data.is_some();
if !auth_specified {
let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
config.auth_data = crate::globals::get_global_authfile(root)?.map(|a| a.1);
if config.auth_data.is_none() {
config.auth_anonymous = true;
}
}
let isolation_user = config
.skopeo_cmd
.is_none()
.then_some(isolation_user.as_ref())
.flatten();
if let Some(user) = isolation_user {
if let Some(authfile) = config.authfile.take() {
config.auth_data = Some(std::fs::File::open(authfile)?);
}
let cmd = crate::isolation::unprivileged_subprocess("skopeo", user);
config.skopeo_cmd = Some(cmd);
}
Ok(())
}
pub(crate) fn labels_of(
config: &oci_spec::image::ImageConfiguration,
) -> Option<&HashMap<String, String>> {
config.config().as_ref().and_then(|c| c.labels().as_ref())
}
pub fn version_for_config(config: &oci_spec::image::ImageConfiguration) -> Option<&str> {
if let Some(labels) = labels_of(config) {
for k in [oci_spec::image::ANNOTATION_VERSION, LABEL_VERSION] {
if let Some(v) = labels.get(k) {
return Some(v.as_str());
}
}
}
None
}
pub mod deploy;
mod encapsulate;
pub use encapsulate::*;
mod unencapsulate;
pub use unencapsulate::*;
mod skopeo;
pub mod store;
mod update_detachedmeta;
pub use update_detachedmeta::*;
use crate::isolation;
#[cfg(test)]
mod tests {
use std::process::Command;
use containers_image_proxy::ImageProxyConfig;
use super::*;
#[test]
fn test_serializable_transport() {
for v in [
Transport::Registry,
Transport::ContainerStorage,
Transport::OciArchive,
Transport::DockerArchive,
Transport::OciDir,
] {
assert_eq!(Transport::try_from(v.serializable_name()).unwrap(), v);
}
}
const INVALID_IRS: &[&str] = &["", "foo://", "docker:blah", "registry:", "foo:bar"];
const VALID_IRS: &[&str] = &[
"containers-storage:localhost/someimage",
"docker://quay.io/exampleos/blah:sometag",
];
#[test]
fn test_imagereference() {
let ir: ImageReference = "registry:quay.io/exampleos/blah".try_into().unwrap();
assert_eq!(ir.transport, Transport::Registry);
assert_eq!(ir.name, "quay.io/exampleos/blah");
assert_eq!(ir.to_string(), "docker://quay.io/exampleos/blah");
for &v in VALID_IRS {
ImageReference::try_from(v).unwrap();
}
for &v in INVALID_IRS {
if ImageReference::try_from(v).is_ok() {
panic!("Should fail to parse: {}", v)
}
}
struct Case {
s: &'static str,
transport: Transport,
name: &'static str,
}
for case in [
Case {
s: "oci:somedir",
transport: Transport::OciDir,
name: "somedir",
},
Case {
s: "dir:/some/dir/blah",
transport: Transport::Dir,
name: "/some/dir/blah",
},
Case {
s: "oci-archive:/path/to/foo.ociarchive",
transport: Transport::OciArchive,
name: "/path/to/foo.ociarchive",
},
Case {
s: "docker-archive:/path/to/foo.dockerarchive",
transport: Transport::DockerArchive,
name: "/path/to/foo.dockerarchive",
},
Case {
s: "containers-storage:localhost/someimage:blah",
transport: Transport::ContainerStorage,
name: "localhost/someimage:blah",
},
] {
let ir: ImageReference = case.s.try_into().unwrap();
assert_eq!(ir.transport, case.transport);
assert_eq!(ir.name, case.name);
let reserialized = ir.to_string();
assert_eq!(case.s, reserialized.as_str());
}
}
#[test]
fn test_ostreeimagereference() {
let ir_s = "ostree-remote-image:myremote:registry:quay.io/exampleos/blah";
let ir_registry = "ostree-remote-registry:myremote:quay.io/exampleos/blah";
for &ir_s in &[ir_s, ir_registry] {
let ir: OstreeImageReference = ir_s.try_into().unwrap();
assert_eq!(
ir.sigverify,
SignatureSource::OstreeRemote("myremote".to_string())
);
assert_eq!(ir.imgref.transport, Transport::Registry);
assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
assert_eq!(
ir.to_string(),
"ostree-remote-image:myremote:docker://quay.io/exampleos/blah"
);
}
let ir: OstreeImageReference = ir_s.try_into().unwrap();
assert_eq!(ir, OstreeImageReference::from_str(ir_s).unwrap());
assert_eq!(&ir, &OstreeImageReference::try_from(ir_registry).unwrap());
let ir_s = "ostree-image-signed:docker://quay.io/exampleos/blah";
let ir: OstreeImageReference = ir_s.try_into().unwrap();
assert_eq!(ir.sigverify, SignatureSource::ContainerPolicy);
assert_eq!(ir.imgref.transport, Transport::Registry);
assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
assert_eq!(ir.to_string(), ir_s);
assert_eq!(format!("{:#}", &ir), ir_s);
let ir_s = "ostree-unverified-image:docker://quay.io/exampleos/blah";
let ir: OstreeImageReference = ir_s.try_into().unwrap();
assert_eq!(ir.sigverify, SignatureSource::ContainerPolicyAllowInsecure);
assert_eq!(ir.imgref.transport, Transport::Registry);
assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
assert_eq!(
ir.to_string(),
"ostree-unverified-registry:quay.io/exampleos/blah"
);
let ir_shorthand =
OstreeImageReference::try_from("ostree-unverified-registry:quay.io/exampleos/blah")
.unwrap();
assert_eq!(&ir_shorthand, &ir);
assert_eq!(format!("{:#}", &ir), "docker://quay.io/exampleos/blah");
}
#[test]
fn test_merge_authopts() {
let mut c = ImageProxyConfig::default();
let authf = std::fs::File::open("/dev/null").unwrap();
c.auth_data = Some(authf);
super::merge_default_container_proxy_opts_with_isolation(&mut c, None).unwrap();
assert!(!c.auth_anonymous);
assert!(c.authfile.is_none());
assert!(c.auth_data.is_some());
assert!(c.skopeo_cmd.is_none());
super::merge_default_container_proxy_opts_with_isolation(&mut c, None).unwrap();
assert!(!c.auth_anonymous);
assert!(c.authfile.is_none());
assert!(c.auth_data.is_some());
assert!(c.skopeo_cmd.is_none());
let mut c = ImageProxyConfig {
skopeo_cmd: Some(Command::new("skopeo")),
..Default::default()
};
super::merge_default_container_proxy_opts_with_isolation(&mut c, Some("foo")).unwrap();
assert_eq!(c.skopeo_cmd.unwrap().get_program(), "skopeo");
}
}