mirror of
https://github.com/marcoallegretti/karapace.git
synced 2026-03-26 21:43:09 +00:00
- RuntimeBackend trait: resolve, build, enter, exec, destroy, status - Namespace backend: unshare + fuse-overlayfs + chroot (unprivileged) - OCI backend: crun/runc/youki support - Mock backend: deterministic test backend with configurable resolution - Image downloading from images.linuxcontainers.org with blake3 verification - Sandbox script generation with POSIX shell-quote injection prevention - Host integration: Wayland, X11, PipeWire, PulseAudio, D-Bus, GPU, audio, SSH agent - Desktop app export as .desktop files on the host - SecurityPolicy: mount whitelist, device policy, env var allow/deny, resource limits - Prerequisite detection with distro-specific install instructions - OSC 777 terminal markers for container-aware terminals
255 lines
7.5 KiB
Rust
255 lines
7.5 KiB
Rust
use crate::backend::{RuntimeBackend, RuntimeSpec, RuntimeStatus};
|
|
use crate::RuntimeError;
|
|
use karapace_schema::{ResolutionResult, ResolvedPackage};
|
|
use std::collections::HashMap;
|
|
use std::sync::Mutex;
|
|
|
|
pub struct MockBackend {
|
|
state: Mutex<HashMap<String, bool>>,
|
|
}
|
|
|
|
impl Default for MockBackend {
|
|
fn default() -> Self {
|
|
Self {
|
|
state: Mutex::new(HashMap::new()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MockBackend {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
}
|
|
|
|
impl RuntimeBackend for MockBackend {
|
|
fn name(&self) -> &'static str {
|
|
"mock"
|
|
}
|
|
|
|
fn available(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn resolve(&self, spec: &RuntimeSpec) -> Result<ResolutionResult, RuntimeError> {
|
|
// Mock resolution: deterministic digest from image name,
|
|
// packages get version "0.0.0-mock" for deterministic identity.
|
|
let base_image_digest =
|
|
blake3::hash(format!("mock-image:{}", spec.manifest.base_image).as_bytes())
|
|
.to_hex()
|
|
.to_string();
|
|
|
|
let resolved_packages = spec
|
|
.manifest
|
|
.system_packages
|
|
.iter()
|
|
.map(|name| ResolvedPackage {
|
|
name: name.clone(),
|
|
version: "0.0.0-mock".to_owned(),
|
|
})
|
|
.collect();
|
|
|
|
Ok(ResolutionResult {
|
|
base_image_digest,
|
|
resolved_packages,
|
|
})
|
|
}
|
|
|
|
fn build(&self, spec: &RuntimeSpec) -> Result<(), RuntimeError> {
|
|
let mut state = self
|
|
.state
|
|
.lock()
|
|
.map_err(|e| RuntimeError::ExecFailed(format!("mutex poisoned: {e}")))?;
|
|
state.insert(spec.env_id.clone(), false);
|
|
|
|
let root = std::path::Path::new(&spec.root_path);
|
|
if !root.exists() {
|
|
std::fs::create_dir_all(root)?;
|
|
}
|
|
let overlay = std::path::Path::new(&spec.overlay_path);
|
|
if !overlay.exists() {
|
|
std::fs::create_dir_all(overlay)?;
|
|
}
|
|
|
|
// Create upper dir with mock filesystem content so engine tests
|
|
// exercise the real layer capture path (pack_layer on upper dir).
|
|
let upper = overlay.join("upper");
|
|
std::fs::create_dir_all(&upper)?;
|
|
std::fs::write(
|
|
upper.join(".karapace-mock"),
|
|
format!("mock-env:{}", spec.env_id),
|
|
)?;
|
|
for pkg in &spec.manifest.system_packages {
|
|
std::fs::write(
|
|
upper.join(format!(".pkg-{pkg}")),
|
|
format!("{pkg}@0.0.0-mock"),
|
|
)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn enter(&self, spec: &RuntimeSpec) -> Result<(), RuntimeError> {
|
|
let mut state = self
|
|
.state
|
|
.lock()
|
|
.map_err(|e| RuntimeError::ExecFailed(format!("mutex poisoned: {e}")))?;
|
|
if state.get(&spec.env_id) == Some(&true) {
|
|
return Err(RuntimeError::AlreadyRunning(spec.env_id.clone()));
|
|
}
|
|
state.insert(spec.env_id.clone(), true);
|
|
Ok(())
|
|
}
|
|
|
|
fn exec(
|
|
&self,
|
|
_spec: &RuntimeSpec,
|
|
command: &[String],
|
|
) -> Result<std::process::Output, RuntimeError> {
|
|
let stdout = format!("mock-exec: {}\n", command.join(" "));
|
|
// Create a real success ExitStatus portably
|
|
let success_status = std::process::Command::new("true")
|
|
.status()
|
|
.unwrap_or_else(|_| {
|
|
std::process::Command::new("/bin/true")
|
|
.status()
|
|
.expect("cannot execute /bin/true")
|
|
});
|
|
Ok(std::process::Output {
|
|
status: success_status,
|
|
stdout: stdout.into_bytes(),
|
|
stderr: Vec::new(),
|
|
})
|
|
}
|
|
|
|
fn destroy(&self, spec: &RuntimeSpec) -> Result<(), RuntimeError> {
|
|
let mut state = self
|
|
.state
|
|
.lock()
|
|
.map_err(|e| RuntimeError::ExecFailed(format!("mutex poisoned: {e}")))?;
|
|
state.remove(&spec.env_id);
|
|
|
|
let overlay = std::path::Path::new(&spec.overlay_path);
|
|
if overlay.exists() {
|
|
std::fs::remove_dir_all(overlay)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn status(&self, env_id: &str) -> Result<RuntimeStatus, RuntimeError> {
|
|
let state = self
|
|
.state
|
|
.lock()
|
|
.map_err(|e| RuntimeError::ExecFailed(format!("mutex poisoned: {e}")))?;
|
|
let running = state.get(env_id).copied().unwrap_or(false);
|
|
Ok(RuntimeStatus {
|
|
env_id: env_id.to_owned(),
|
|
running,
|
|
pid: if running { Some(99999) } else { None },
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use karapace_schema::parse_manifest_str;
|
|
|
|
fn test_spec(dir: &std::path::Path) -> RuntimeSpec {
|
|
let manifest = parse_manifest_str(
|
|
r#"
|
|
manifest_version = 1
|
|
[base]
|
|
image = "rolling"
|
|
"#,
|
|
)
|
|
.unwrap()
|
|
.normalize()
|
|
.unwrap();
|
|
|
|
RuntimeSpec {
|
|
env_id: "mock-test".to_owned(),
|
|
root_path: dir.join("root").to_string_lossy().to_string(),
|
|
overlay_path: dir.join("overlay").to_string_lossy().to_string(),
|
|
store_root: dir.to_string_lossy().to_string(),
|
|
manifest,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mock_resolve_determinism() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let backend = MockBackend::new();
|
|
let spec = test_spec(dir.path());
|
|
|
|
let r1 = backend.resolve(&spec).unwrap();
|
|
let r2 = backend.resolve(&spec).unwrap();
|
|
|
|
assert_eq!(r1.base_image_digest, r2.base_image_digest);
|
|
assert_eq!(r1.resolved_packages.len(), r2.resolved_packages.len());
|
|
for (a, b) in r1.resolved_packages.iter().zip(r2.resolved_packages.iter()) {
|
|
assert_eq!(a.name, b.name);
|
|
assert_eq!(a.version, b.version);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mock_resolve_with_packages() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let manifest = parse_manifest_str(
|
|
r#"
|
|
manifest_version = 1
|
|
[base]
|
|
image = "rolling"
|
|
[system]
|
|
packages = ["git", "clang", "cmake"]
|
|
[runtime]
|
|
backend = "mock"
|
|
"#,
|
|
)
|
|
.unwrap()
|
|
.normalize()
|
|
.unwrap();
|
|
|
|
let spec = RuntimeSpec {
|
|
env_id: "mock-pkg-test".to_owned(),
|
|
root_path: dir.path().join("root").to_string_lossy().to_string(),
|
|
overlay_path: dir.path().join("overlay").to_string_lossy().to_string(),
|
|
store_root: dir.path().to_string_lossy().to_string(),
|
|
manifest,
|
|
};
|
|
|
|
let backend = MockBackend::new();
|
|
let result = backend.resolve(&spec).unwrap();
|
|
|
|
assert_eq!(result.resolved_packages.len(), 3);
|
|
assert!(result
|
|
.resolved_packages
|
|
.iter()
|
|
.all(|p| p.version == "0.0.0-mock"));
|
|
assert!(result.resolved_packages.iter().any(|p| p.name == "git"));
|
|
assert!(!result.base_image_digest.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn mock_lifecycle() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let backend = MockBackend::new();
|
|
let spec = test_spec(dir.path());
|
|
|
|
backend.build(&spec).unwrap();
|
|
let status = backend.status(&spec.env_id).unwrap();
|
|
assert!(!status.running);
|
|
|
|
backend.enter(&spec).unwrap();
|
|
let status = backend.status(&spec.env_id).unwrap();
|
|
assert!(status.running);
|
|
assert_eq!(status.pid, Some(99999));
|
|
|
|
assert!(backend.enter(&spec).is_err());
|
|
|
|
backend.destroy(&spec).unwrap();
|
|
let status = backend.status(&spec.env_id).unwrap();
|
|
assert!(!status.running);
|
|
}
|
|
}
|