runtime: propagate offline mode

Add RuntimeSpec.offline and thread it through OCI/namespace backends.

Offline mode requires cached base images, forces sandbox network isolation,
and fails fast when system package resolution/installation would require
network access.
This commit is contained in:
Marco Allegretti 2026-02-23 18:28:10 +01:00
parent 32296bd75a
commit cbf954bead
5 changed files with 47 additions and 8 deletions

View file

@ -9,6 +9,8 @@ pub struct RuntimeSpec {
pub overlay_path: String,
pub store_root: String,
pub manifest: NormalizedManifest,
#[serde(default)]
pub offline: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]

View file

@ -21,6 +21,11 @@ pub struct ResolvedImage {
pub display_name: String,
}
pub fn resolve_pinned_image_url(name: &str) -> Result<String, RuntimeError> {
let resolved = resolve_image(name)?;
download_url(&resolved.source)
}
#[allow(clippy::too_many_lines)]
pub fn resolve_image(name: &str) -> Result<ResolvedImage, RuntimeError> {
let name = name.trim().to_lowercase();
@ -248,6 +253,7 @@ impl ImageCache {
&self,
resolved: &ResolvedImage,
progress: &dyn Fn(&str),
offline: bool,
) -> Result<PathBuf, RuntimeError> {
let rootfs = self.rootfs_path(&resolved.cache_key);
if self.is_cached(&resolved.cache_key) {
@ -255,6 +261,13 @@ impl ImageCache {
return Ok(rootfs);
}
if offline {
return Err(RuntimeError::ExecFailed(format!(
"offline mode: base image '{}' is not cached",
resolved.display_name
)));
}
std::fs::create_dir_all(&rootfs)?;
progress(&format!(

View file

@ -173,6 +173,7 @@ image = "rolling"
overlay_path: dir.join("overlay").to_string_lossy().to_string(),
store_root: dir.to_string_lossy().to_string(),
manifest,
offline: false,
}
}
@ -217,6 +218,7 @@ backend = "mock"
overlay_path: dir.path().join("overlay").to_string_lossy().to_string(),
store_root: dir.path().to_string_lossy().to_string(),
manifest,
offline: false,
};
let backend = MockBackend::new();

View file

@ -62,11 +62,17 @@ impl RuntimeBackend for NamespaceBackend {
// Download/cache the base image
let resolved = resolve_image(&spec.manifest.base_image)?;
let image_cache = ImageCache::new(&self.store_root);
let rootfs = image_cache.ensure_image(&resolved, &progress)?;
let rootfs = image_cache.ensure_image(&resolved, &progress, spec.offline)?;
// Compute content digest of the base image
let base_image_digest = compute_image_digest(&rootfs)?;
if spec.offline && !spec.manifest.system_packages.is_empty() {
return Err(RuntimeError::ExecFailed(
"offline mode: cannot resolve system packages".to_owned(),
));
}
// If there are packages to resolve, set up a temporary overlay
// and install+query to get exact versions
let resolved_packages = if spec.manifest.system_packages.is_empty() {
@ -145,11 +151,11 @@ impl RuntimeBackend for NamespaceBackend {
// Resolve and download the base image
let resolved = resolve_image(&spec.manifest.base_image)?;
let image_cache = ImageCache::new(&self.store_root);
let rootfs = image_cache.ensure_image(&resolved, &progress)?;
let rootfs = image_cache.ensure_image(&resolved, &progress, spec.offline)?;
// Set up overlay filesystem
let mut sandbox = SandboxConfig::new(rootfs.clone(), &spec.env_id, &env_dir);
sandbox.isolate_network = spec.manifest.network_isolation;
sandbox.isolate_network = spec.offline || spec.manifest.network_isolation;
mount_overlay(&sandbox)?;
@ -158,6 +164,11 @@ impl RuntimeBackend for NamespaceBackend {
// Install system packages if any
if !spec.manifest.system_packages.is_empty() {
if spec.offline {
return Err(RuntimeError::ExecFailed(
"offline mode: cannot install system packages".to_owned(),
));
}
let pkg_mgr = detect_package_manager(&sandbox.overlay_merged)
.or_else(|| detect_package_manager(&rootfs))
.ok_or_else(|| {
@ -216,7 +227,7 @@ impl RuntimeBackend for NamespaceBackend {
// Create sandbox config
let mut sandbox = SandboxConfig::new(rootfs, &spec.env_id, &env_dir);
sandbox.isolate_network = spec.manifest.network_isolation;
sandbox.isolate_network = spec.offline || spec.manifest.network_isolation;
sandbox.hostname = format!("karapace-{}", &spec.env_id[..12.min(spec.env_id.len())]);
// Compute host integration (Wayland, PipeWire, GPU, etc.)

View file

@ -173,9 +173,15 @@ impl RuntimeBackend for OciBackend {
let resolved = resolve_image(&spec.manifest.base_image)?;
let image_cache = ImageCache::new(&self.store_root);
let rootfs = image_cache.ensure_image(&resolved, &progress)?;
let rootfs = image_cache.ensure_image(&resolved, &progress, spec.offline)?;
let base_image_digest = compute_image_digest(&rootfs)?;
if spec.offline && !spec.manifest.system_packages.is_empty() {
return Err(RuntimeError::ExecFailed(
"offline mode: cannot resolve system packages".to_owned(),
));
}
let resolved_packages = if spec.manifest.system_packages.is_empty() {
Vec::new()
} else {
@ -250,15 +256,20 @@ impl RuntimeBackend for OciBackend {
let resolved = resolve_image(&spec.manifest.base_image)?;
let image_cache = ImageCache::new(&self.store_root);
let rootfs = image_cache.ensure_image(&resolved, &progress)?;
let rootfs = image_cache.ensure_image(&resolved, &progress, spec.offline)?;
let mut sandbox = SandboxConfig::new(rootfs.clone(), &spec.env_id, &env_dir);
sandbox.isolate_network = spec.manifest.network_isolation;
sandbox.isolate_network = spec.offline || spec.manifest.network_isolation;
mount_overlay(&sandbox)?;
setup_container_rootfs(&sandbox)?;
if !spec.manifest.system_packages.is_empty() {
if spec.offline {
return Err(RuntimeError::ExecFailed(
"offline mode: cannot install system packages".to_owned(),
));
}
let pkg_mgr = detect_package_manager(&sandbox.overlay_merged)
.or_else(|| detect_package_manager(&rootfs))
.ok_or_else(|| {
@ -319,7 +330,7 @@ impl RuntimeBackend for OciBackend {
let rootfs = image_cache.rootfs_path(&resolved.cache_key);
let mut sandbox = SandboxConfig::new(rootfs, &spec.env_id, &env_dir);
sandbox.isolate_network = spec.manifest.network_isolation;
sandbox.isolate_network = spec.offline || spec.manifest.network_isolation;
let host = compute_host_integration(&spec.manifest);
sandbox.bind_mounts.extend(host.bind_mounts);