use std::collections::BTreeMap;
use std::fmt;
use std::path::{Path, PathBuf};
const INDEX: &str = "state.json";
const CONTAINERS: &str = "containers";
macro_rules! initfs_version {
() => {
"0.47.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!();
pub const KERNEL_URL: &str = concat!(
"https://github.com/kata-containers/kata-containers/releases/download/",
kernel_version!(),
"/kata-static-",
kernel_version!(),
"-arm64.tar.xz"
);
pub const KERNEL_IN_ARCHIVE: &str = "opt/kata/share/kata-containers/vmlinux.container";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Store {
root: PathBuf,
kernel: PathBuf,
initfs_reference: String,
initfs: 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>,
kernel: impl Into<PathBuf>,
initfs_reference: impl Into<String>,
initfs: impl Into<PathBuf>,
) -> Self {
Self {
root: root.into(),
kernel: kernel.into(),
initfs_reference: initfs_reference.into(),
initfs: initfs.into(),
}
}
pub fn kernel(&self) -> &Path {
&self.kernel
}
pub fn initfs_reference(&self) -> &str {
&self.initfs_reference
}
pub fn initfs(&self) -> &Path {
&self.initfs
}
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 {}", self.kernel.display()),
});
}
if !self.holds(&self.initfs_reference) {
return Err(StoreError::Incomplete {
root: self.root.clone(),
missing: format!("{} in {INDEX}", self.initfs_reference),
});
}
Ok(())
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn container_dir(&self, name: &str) -> PathBuf {
self.root.join(CONTAINERS).join(name)
}
pub fn images(&self) -> Result<Vec<String>, StoreError> {
Ok(self.references()?.into_keys().collect())
}
pub fn holds(&self, reference: &str) -> bool {
self
.references()
.is_ok_and(|references| references.contains_key(reference))
}
fn references(&self) -> Result<BTreeMap<String, serde::de::IgnoredAny>, StoreError> {
let index = self.root.join(INDEX);
if !index.exists() {
return Ok(BTreeMap::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()))?;
serde_json::from_str(&text).map_err(|source| unreadable(source.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
const CUSTOM_INIT_IMAGE: &str = "docker.io/example/vminit:0.47.0-runc";
fn store_at(root: &Path) -> Store {
Store::at(root, root.join("vmlinux"), INITFS_REFERENCE, root.join("vminit.ext4"))
}
fn place_kernel(store: &Store) {
std::fs::write(store.kernel(), "").expect("kernel");
}
fn index_holding(reference: &str) -> String {
format!("{{\"{}\":{{}}}}", reference.replace('/', "\\/"))
}
#[test]
fn boots_the_kernel_and_init_image_it_is_given() {
let store = Store::at("/store", "/cache/vmlinux", CUSTOM_INIT_IMAGE, "/cache/vminit-runc.ext4");
assert_eq!(store.root(), Path::new("/store"));
assert_eq!(store.kernel(), Path::new("/cache/vmlinux"));
assert_eq!(store.initfs_reference(), CUSTOM_INIT_IMAGE);
assert_eq!(store.initfs(), Path::new("/cache/vminit-runc.ext4"));
}
#[test]
fn is_not_ready_before_anything_has_provisioned_it() {
assert_eq!(
store_at(Path::new("/nonexistent/images")).ready(),
Err(StoreError::Missing(PathBuf::from("/nonexistent/images")))
);
}
#[test]
fn names_the_kernel_it_could_not_find() {
let root = tempfile::tempdir().expect("a temp dir");
let store = store_at(root.path());
let error = store.ready().expect_err("a store with no kernel");
assert!(
error
.to_string()
.contains(&store.kernel().display().to_string()),
"error should name the kernel's path: {error}"
);
}
#[test]
fn names_the_reference_it_could_not_find() {
let root = tempfile::tempdir().expect("a temp dir");
let store = store_at(root.path());
place_kernel(&store);
std::fs::write(root.path().join(INDEX), "{}").expect("index");
let error = store.ready().expect_err("an incomplete store");
assert!(
error.to_string().contains(INITFS_REFERENCE),
"error should name the missing image: {error}"
);
}
#[test]
fn is_ready_only_with_the_init_image_it_boots() {
let root = tempfile::tempdir().expect("a temp dir");
let store = Store::at(
root.path(),
root.path().join("vmlinux"),
CUSTOM_INIT_IMAGE,
root.path().join("runc.ext4"),
);
place_kernel(&store);
std::fs::write(root.path().join(INDEX), index_holding(INITFS_REFERENCE)).expect("index");
let error = store
.ready()
.expect_err("a store holding another init image");
assert!(
error.to_string().contains(CUSTOM_INIT_IMAGE),
"error should name the missing image: {error}"
);
}
#[test]
fn is_ready_when_it_holds_the_kernel_and_the_initfs() {
let root = tempfile::tempdir().expect("a temp dir");
let store = store_at(root.path());
place_kernel(&store);
std::fs::write(root.path().join(INDEX), index_holding(INITFS_REFERENCE)).expect("index");
store.ready().expect("a complete store");
assert_eq!(
store.container_dir("session-cb"),
root.path().join("containers/session-cb")
);
}
#[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()
);
}
#[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");
assert_eq!(
store_at(root.path())
.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_at(root.path());
assert!(store.holds(INITFS_REFERENCE));
assert!(!store.holds("ghcr.io/apple/containerization/vminit:0.0.0"));
}
}