use std::collections::BTreeMap;
use std::fmt;
use std::path::{Path, PathBuf};
const KERNEL: &str = "kernels/default.kernel-arm64";
const INDEX: &str = "state.json";
const CONTAINERS: &str = "containers";
macro_rules! initfs_version {
() => {
"0.45.0"
};
}
macro_rules! kernel_version {
() => {
"3.17.0"
};
}
pub const INITFS_VERSION: &str = initfs_version!();
pub const INITFS_REFERENCE: &str = concat!("ghcr.io/apple/containerization/vminit:", initfs_version!());
pub const KERNEL_VERSION: &str = kernel_version!();
#[cfg(target_os = "macos")]
pub const KERNEL_URL: &str = concat!(
"https://github.com/kata-containers/kata-containers/releases/download/",
kernel_version!(),
"/kata-static-",
kernel_version!(),
"-arm64.tar.xz"
);
#[cfg(target_os = "macos")]
pub const KERNEL_IN_ARCHIVE: &str = "opt/kata/share/kata-containers/vmlinux.container";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Store {
root: PathBuf,
}
#[derive(Debug, Eq, PartialEq)]
pub enum StoreError {
Missing(PathBuf),
Incomplete { root: PathBuf, missing: String },
Unreadable { path: PathBuf, source: String },
}
impl fmt::Display for StoreError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Missing(root) => write!(formatter, "no image store at {}", root.display()),
Self::Incomplete { root, missing } => {
write!(formatter, "the image store at {} has no {missing}", root.display())
}
Self::Unreadable { path, source } => write!(formatter, "cannot read {}: {source}", path.display()),
}
}
}
impl std::error::Error for StoreError {}
impl Store {
pub fn at(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
pub fn ready(&self) -> Result<(), StoreError> {
if !self.root.is_dir() {
return Err(StoreError::Missing(self.root.clone()));
}
if !self.kernel().is_file() {
return Err(StoreError::Incomplete {
root: self.root.clone(),
missing: format!("kernel at {KERNEL}"),
});
}
if !self.holds(INITFS_REFERENCE) {
return Err(StoreError::Incomplete {
root: self.root.clone(),
missing: format!("{INITFS_REFERENCE} in {INDEX}"),
});
}
Ok(())
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn kernel(&self) -> PathBuf {
self.root.join(KERNEL)
}
pub fn container_dir(&self, name: &str) -> PathBuf {
self.root.join(CONTAINERS).join(name)
}
pub fn images(&self) -> Result<Vec<String>, StoreError> {
let index = self.root.join(INDEX);
if !index.exists() {
return Ok(Vec::new());
}
let unreadable = |source: String| StoreError::Unreadable {
path: index.clone(),
source,
};
let text = std::fs::read_to_string(&index).map_err(|source| unreadable(source.to_string()))?;
let references: BTreeMap<String, serde_json::Value> =
serde_json::from_str(&text).map_err(|source| unreadable(source.to_string()))?;
Ok(references.into_keys().collect())
}
pub fn holds(&self, reference: &str) -> bool {
self
.images()
.is_ok_and(|references| references.iter().any(|held| held == reference))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_not_ready_before_anything_has_provisioned_it() {
assert_eq!(
Store::at("/nonexistent/images").ready(),
Err(StoreError::Missing(PathBuf::from("/nonexistent/images")))
);
}
#[test]
fn names_the_reference_it_could_not_find() {
let root = tempfile::tempdir().expect("a temp dir");
std::fs::create_dir_all(root.path().join("kernels")).expect("kernels");
std::fs::write(root.path().join(KERNEL), "").expect("kernel");
std::fs::write(root.path().join(INDEX), "{}").expect("index");
let error = Store::at(root.path())
.ready()
.expect_err("an incomplete store");
assert!(
error.to_string().contains(INITFS_REFERENCE),
"error should name the missing image: {error}"
);
}
#[test]
fn holds_no_images_before_the_first_build() {
let root = tempfile::tempdir().expect("a temp dir");
assert_eq!(
Store::at(root.path())
.images()
.expect("an empty store is readable"),
Vec::<String>::new()
);
}
fn index_holding(reference: &str) -> String {
format!("{{\"{}\":{{}}}}", reference.replace('/', "\\/"))
}
#[test]
fn lists_the_references_the_index_names() {
let root = tempfile::tempdir().expect("a temp dir");
std::fs::write(
root.path().join(INDEX),
r#"{"example\/base:latest":{"digest":"sha256:aa","annotations":{"org.opencontainers.image.ref.name":"latest"}},"docker.io\/library\/debian:stable-slim":{"digest":"sha256:bb"}}"#,
)
.expect("index");
let store = Store {
root: root.path().to_path_buf(),
};
assert_eq!(
store.images().expect("the index should be readable"),
["docker.io/library/debian:stable-slim", "example/base:latest"]
);
}
#[test]
fn names_the_index_it_could_not_parse() {
let root = tempfile::tempdir().expect("a temp dir");
std::fs::write(root.path().join(INDEX), "{\"truncated").expect("index");
let store = Store::at(root.path());
assert!(matches!(
store.images(),
Err(StoreError::Unreadable { path, .. }) if path == root.path().join(INDEX)
));
assert!(!store.holds(INITFS_REFERENCE));
}
#[test]
fn finds_a_reference_whose_slashes_are_escaped() {
let root = tempfile::tempdir().expect("a temp dir");
std::fs::write(root.path().join(INDEX), index_holding(INITFS_REFERENCE)).expect("index");
let store = Store {
root: root.path().to_path_buf(),
};
assert!(store.holds(INITFS_REFERENCE));
assert!(!store.holds("ghcr.io/apple/containerization/vminit:0.0.0"));
}
#[test]
fn is_ready_when_it_holds_the_kernel_and_the_initfs() {
let root = tempfile::tempdir().expect("a temp dir");
std::fs::create_dir_all(root.path().join("kernels")).expect("kernels");
std::fs::write(root.path().join(KERNEL), "").expect("kernel");
std::fs::write(root.path().join(INDEX), index_holding(INITFS_REFERENCE)).expect("index");
let store = Store::at(root.path());
store.ready().expect("a complete store");
assert_eq!(store.root(), root.path());
assert_eq!(store.kernel(), root.path().join(KERNEL));
assert_eq!(
store.container_dir("session-cb"),
root.path().join("containers/session-cb")
);
}
}