177 lines
6.7 KiB
Rust
177 lines
6.7 KiB
Rust
//! End-to-end test of the shared, content-addressed pack extraction
|
|
//! (`extract_sidecar_shared`) against a *real* `.smolmachine` sidecar — real
|
|
//! zstd stream, real tar, real footer/checksum — on a real filesystem.
|
|
//!
|
|
//! Linux-only: the shared store backs the Linux fleet's per-VM idmapped bind
|
|
//! mount (kernel ≥5.12); macOS uses a per-machine case-sensitive sparse image,
|
|
//! so the whole module compiles to nothing elsewhere. This validates the
|
|
//! *extraction half* (extract-once, content-addressed, locked down); the mount
|
|
//! half is validated by the root crate's `tests/idmap_mount.rs`.
|
|
#![cfg(target_os = "linux")]
|
|
|
|
use std::fs;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::path::Path;
|
|
|
|
use smolvm_pack::assets::AssetCollector;
|
|
use smolvm_pack::extract::{extract_sidecar, extract_sidecar_shared, shared_extract_enabled};
|
|
use smolvm_pack::format::PackManifest;
|
|
use smolvm_pack::{read_footer_from_sidecar, sidecar_path_for, Packer};
|
|
|
|
/// Build a tiny but *valid* OCI-style layer: a raw tar archive carrying one
|
|
/// file. `post_process_extraction` untars every `layers/*.tar`, so the payload
|
|
/// must be a real tar (a raw byte blob would fail with "failed to read entire
|
|
/// block" — exactly what a real OCI layer never does).
|
|
fn layer_tar(filename: &str, content: &[u8]) -> Vec<u8> {
|
|
let mut builder = tar::Builder::new(Vec::new());
|
|
let mut header = tar::Header::new_gnu();
|
|
header.set_entry_type(tar::EntryType::Regular);
|
|
header.set_size(content.len() as u64);
|
|
header.set_mode(0o644);
|
|
header.set_cksum();
|
|
builder.append_data(&mut header, filename, content).unwrap();
|
|
builder.into_inner().unwrap()
|
|
}
|
|
|
|
/// Build a real sidecar `.smolmachine` carrying two OCI layers, returning the
|
|
/// sidecar path (under `dir`).
|
|
fn build_real_sidecar(dir: &Path) -> std::path::PathBuf {
|
|
let stub_path = dir.join("stub");
|
|
fs::write(&stub_path, b"#!/bin/sh\necho stub").unwrap();
|
|
|
|
let mut collector = AssetCollector::new(dir.join("staging")).unwrap();
|
|
collector
|
|
.add_layer(
|
|
"sha256:aaa111aaa111bbb222",
|
|
&layer_tar("etc/base.conf", b"base layer file"),
|
|
)
|
|
.unwrap();
|
|
collector
|
|
.add_layer(
|
|
"sha256:ccc333ccc333ddd444",
|
|
&layer_tar("etc/top.conf", b"top layer file"),
|
|
)
|
|
.unwrap();
|
|
|
|
let manifest = PackManifest::new(
|
|
"test:latest".to_string(),
|
|
"sha256:test".to_string(),
|
|
"linux/x86_64".to_string(),
|
|
"linux/x86_64".to_string(),
|
|
);
|
|
|
|
let output = dir.join("packed");
|
|
Packer::new(manifest)
|
|
.with_stub(&stub_path)
|
|
.with_assets(collector)
|
|
.pack(&output)
|
|
.unwrap();
|
|
|
|
sidecar_path_for(&output)
|
|
}
|
|
|
|
/// Recursively collect (relative-path -> file bytes) for every regular file in a
|
|
/// tree, so the shared extraction can be compared byte-for-byte to a plain one.
|
|
fn file_map(root: &Path) -> std::collections::BTreeMap<String, Vec<u8>> {
|
|
let mut out = std::collections::BTreeMap::new();
|
|
let mut stack = vec![root.to_path_buf()];
|
|
while let Some(d) = stack.pop() {
|
|
for entry in fs::read_dir(&d).unwrap() {
|
|
let entry = entry.unwrap();
|
|
let ft = entry.file_type().unwrap();
|
|
let p = entry.path();
|
|
if ft.is_dir() {
|
|
stack.push(p);
|
|
} else if ft.is_file() {
|
|
let rel = p.strip_prefix(root).unwrap().to_string_lossy().to_string();
|
|
out.insert(rel, fs::read(&p).unwrap());
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
fn shared_extraction_is_content_addressed_idempotent_and_locked_down() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let sidecar = build_real_sidecar(tmp.path());
|
|
let footer = read_footer_from_sidecar(&sidecar).unwrap();
|
|
|
|
let shared_root = tmp.path().join("_shared");
|
|
let pack_plain = tmp.path().join("vm-plain/pack");
|
|
|
|
// First machine extracts into the shared store; a plain per-machine
|
|
// extraction is the byte-for-byte reference.
|
|
let shared_dir = extract_sidecar_shared(&sidecar, &shared_root, &footer, false).unwrap();
|
|
extract_sidecar(&sidecar, &pack_plain, &footer, false, false).unwrap();
|
|
|
|
// 1) Content-addressed: the shared copy lives at `_shared/<checksum>`.
|
|
assert_eq!(
|
|
shared_dir,
|
|
shared_root.join(format!("{:08x}", footer.checksum)),
|
|
"shared dir must be keyed by the footer checksum"
|
|
);
|
|
assert!(shared_dir.is_dir(), "shared extraction dir must exist");
|
|
|
|
// 2) The shared view is byte-identical to a plain per-machine extraction —
|
|
// the idmapped mount only remaps ownership, never content.
|
|
let reference = file_map(&pack_plain);
|
|
assert!(!reference.is_empty(), "extraction produced no files");
|
|
assert_eq!(
|
|
file_map(&shared_dir),
|
|
reference,
|
|
"shared content must match a plain extraction byte-for-byte"
|
|
);
|
|
|
|
// 3) Idempotent reuse: a second machine with the same checksum reuses the one
|
|
// extracted tree — no second decode, no second store dir. This is the
|
|
// cold-start tax the shared store removes.
|
|
let shared_dir2 = extract_sidecar_shared(&sidecar, &shared_root, &footer, false).unwrap();
|
|
assert_eq!(
|
|
shared_dir2, shared_dir,
|
|
"second machine must reuse the copy"
|
|
);
|
|
let store_dirs: Vec<_> = fs::read_dir(&shared_root)
|
|
.unwrap()
|
|
.filter_map(|e| e.ok())
|
|
.filter(|e| e.path().is_dir())
|
|
.collect();
|
|
assert_eq!(
|
|
store_dirs.len(),
|
|
1,
|
|
"only one content-addressed extraction may exist for one checksum"
|
|
);
|
|
|
|
// 4) Locked down to 0700: the store root and the checksum dir are owner-only,
|
|
// so a sibling VM dropped to a *different* uid cannot read the shared copy
|
|
// directly — it must go through its own idmapped mount (#456 isolation).
|
|
for dir in [&shared_root, &shared_dir] {
|
|
let mode = fs::metadata(dir).unwrap().permissions().mode() & 0o777;
|
|
assert_eq!(
|
|
mode,
|
|
0o700,
|
|
"{} must be 0700, got {:o}",
|
|
dir.display(),
|
|
mode
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn kill_switch_disables_the_shared_store() {
|
|
// The handler consults `shared_extract_enabled()` to choose the shared vs
|
|
// per-machine path; `SMOLVM_DISABLE_SHARED_EXTRACT` forces the fallback
|
|
// without a redeploy. (Run as its own test fn so the env mutation cannot race
|
|
// the content test on cargo's parallel test threads.)
|
|
assert!(
|
|
shared_extract_enabled(),
|
|
"shared store should be enabled by default on Linux"
|
|
);
|
|
std::env::set_var("SMOLVM_DISABLE_SHARED_EXTRACT", "1");
|
|
assert!(
|
|
!shared_extract_enabled(),
|
|
"kill-switch must disable the shared store"
|
|
);
|
|
std::env::remove_var("SMOLVM_DISABLE_SHARED_EXTRACT");
|
|
}
|