From cbf954beadc2db534986fee778c762ac8c218700 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Mon, 23 Feb 2026 18:28:10 +0100 Subject: [PATCH] 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. --- crates/karapace-runtime/src/backend.rs | 2 ++ crates/karapace-runtime/src/image.rs | 13 +++++++++++++ crates/karapace-runtime/src/mock.rs | 2 ++ crates/karapace-runtime/src/namespace.rs | 19 +++++++++++++++---- crates/karapace-runtime/src/oci.rs | 19 +++++++++++++++---- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/crates/karapace-runtime/src/backend.rs b/crates/karapace-runtime/src/backend.rs index 23cc528..d0114de 100644 --- a/crates/karapace-runtime/src/backend.rs +++ b/crates/karapace-runtime/src/backend.rs @@ -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)] diff --git a/crates/karapace-runtime/src/image.rs b/crates/karapace-runtime/src/image.rs index d302c77..a0451f5 100644 --- a/crates/karapace-runtime/src/image.rs +++ b/crates/karapace-runtime/src/image.rs @@ -21,6 +21,11 @@ pub struct ResolvedImage { pub display_name: String, } +pub fn resolve_pinned_image_url(name: &str) -> Result { + let resolved = resolve_image(name)?; + download_url(&resolved.source) +} + #[allow(clippy::too_many_lines)] pub fn resolve_image(name: &str) -> Result { let name = name.trim().to_lowercase(); @@ -248,6 +253,7 @@ impl ImageCache { &self, resolved: &ResolvedImage, progress: &dyn Fn(&str), + offline: bool, ) -> Result { 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!( diff --git a/crates/karapace-runtime/src/mock.rs b/crates/karapace-runtime/src/mock.rs index a4ac979..7176b29 100644 --- a/crates/karapace-runtime/src/mock.rs +++ b/crates/karapace-runtime/src/mock.rs @@ -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(); diff --git a/crates/karapace-runtime/src/namespace.rs b/crates/karapace-runtime/src/namespace.rs index 267bd6c..c5443b6 100644 --- a/crates/karapace-runtime/src/namespace.rs +++ b/crates/karapace-runtime/src/namespace.rs @@ -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.) diff --git a/crates/karapace-runtime/src/oci.rs b/crates/karapace-runtime/src/oci.rs index dc33550..833b081 100644 --- a/crates/karapace-runtime/src/oci.rs +++ b/crates/karapace-runtime/src/oci.rs @@ -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);