Skip to main content

a3s_box_runtime/rootfs/
provider.rs

1//! Rootfs provider — abstracts how a rootfs directory is prepared for a box.
2//!
3//! Two built-in providers:
4//! - `CopyProvider` — full recursive copy (works everywhere, current default)
5//! - `OverlayProvider` — Linux overlayfs mount (near-instant, CoW)
6
7use std::path::{Path, PathBuf};
8
9use a3s_box_core::error::{BoxError, Result};
10
11/// Abstracts how a rootfs directory is prepared for a box from a cached lower layer.
12pub trait RootfsProvider: Send + Sync {
13    /// Prepare a rootfs at `box_dir` from the cached read-only layer at `cache_dir`.
14    /// Returns the path to use as `InstanceSpec.rootfs_path`.
15    fn prepare(&self, box_dir: &Path, cache_dir: &Path) -> Result<PathBuf>;
16
17    /// Cleanup after box stops.
18    ///
19    /// When `persistent` is true, the writable layer (overlay upper dir or copy
20    /// rootfs) is preserved on disk so changes survive the next start.
21    /// When false, the writable layer is wiped for a clean slate.
22    fn cleanup(&self, box_dir: &Path, persistent: bool) -> Result<()>;
23
24    /// Human-readable name for logging.
25    fn name(&self) -> &'static str;
26}
27
28/// Full recursive copy provider — works on all platforms.
29///
30/// This is the original behavior: copies the entire cached rootfs into
31/// `box_dir/rootfs/`. Safe but slow for large images.
32pub struct CopyProvider;
33
34impl RootfsProvider for CopyProvider {
35    fn prepare(&self, box_dir: &Path, cache_dir: &Path) -> Result<PathBuf> {
36        let rootfs = box_dir.join("rootfs");
37        // Reuse existing rootfs when persistent and already populated
38        if rootfs.exists() {
39            tracing::info!(path = %rootfs.display(), "Reusing persistent rootfs");
40            return Ok(rootfs);
41        }
42        crate::cache::layer_cache::copy_dir_recursive(cache_dir, &rootfs)?;
43        Ok(rootfs)
44    }
45
46    fn cleanup(&self, box_dir: &Path, persistent: bool) -> Result<()> {
47        if persistent {
48            tracing::info!("Persistent box: keeping rootfs on disk");
49            return Ok(());
50        }
51        let rootfs = box_dir.join("rootfs");
52        if rootfs.exists() {
53            std::fs::remove_dir_all(&rootfs).map_err(|e| {
54                BoxError::BuildError(format!(
55                    "Failed to remove rootfs {}: {}",
56                    rootfs.display(),
57                    e
58                ))
59            })?;
60        }
61        Ok(())
62    }
63
64    fn name(&self) -> &'static str {
65        "copy"
66    }
67}
68
69/// Overlayfs provider — near-instant CoW mounts (Linux only).
70///
71/// Layout:
72/// ```text
73/// cache_dir/           ← lower (read-only, shared across boxes)
74/// box_dir/upper/       ← upper (per-box writes)
75/// box_dir/work/        ← overlayfs workdir
76/// box_dir/merged/      ← merged view → InstanceSpec.rootfs_path
77/// ```
78pub struct OverlayProvider;
79
80impl RootfsProvider for OverlayProvider {
81    fn prepare(&self, box_dir: &Path, cache_dir: &Path) -> Result<PathBuf> {
82        let upper = box_dir.join("upper");
83        let work = box_dir.join("work");
84        let merged = box_dir.join("merged");
85
86        for dir in [&upper, &work, &merged] {
87            std::fs::create_dir_all(dir).map_err(|e| {
88                BoxError::BuildError(format!(
89                    "Failed to create overlay dir {}: {}",
90                    dir.display(),
91                    e
92                ))
93            })?;
94        }
95
96        // Idempotent: a restart re-runs prepare(); without this guard each call
97        // stacks another overlay on `merged` (the leaked double/triple mounts).
98        if super::is_mountpoint(&merged) {
99            tracing::debug!(merged = %merged.display(), "Overlay already mounted; reusing");
100            return Ok(merged);
101        }
102
103        super::overlay::overlay_mount(cache_dir, &upper, &work, &merged)?;
104
105        tracing::info!(
106            lower = %cache_dir.display(),
107            merged = %merged.display(),
108            "Overlay mount ready"
109        );
110
111        Ok(merged)
112    }
113
114    fn cleanup(&self, box_dir: &Path, persistent: bool) -> Result<()> {
115        let merged = box_dir.join("merged");
116        // Bounded unmount-retry rather than a single attempt: a transient EBUSY
117        // must not leave the overlay mounted, or the remove_dir_all below would
118        // recurse into the live mount and leak it. Mirrors the cleanup paths in
119        // cleanup_stopped_box/cleanup_removed_box.
120        super::unmount_box_overlay(&merged);
121
122        if persistent {
123            // Keep upper (writes) and remove only merged/work (not needed at rest)
124            tracing::info!("Persistent box: keeping overlay upper layer on disk");
125            for dir_name in &["merged", "work"] {
126                let dir = box_dir.join(dir_name);
127                if dir.exists() {
128                    if let Err(e) = std::fs::remove_dir_all(&dir) {
129                        tracing::warn!(path = %dir.display(), error = %e, "Failed to remove overlay dir");
130                    }
131                }
132            }
133            return Ok(());
134        }
135
136        for dir_name in &["upper", "work", "merged"] {
137            let dir = box_dir.join(dir_name);
138            if dir.exists() {
139                if let Err(e) = std::fs::remove_dir_all(&dir) {
140                    tracing::warn!(
141                        path = %dir.display(),
142                        error = %e,
143                        "Failed to remove overlay dir"
144                    );
145                }
146            }
147        }
148
149        Ok(())
150    }
151
152    fn name(&self) -> &'static str {
153        "overlay"
154    }
155}
156
157/// Auto-detect the best available rootfs provider for the current platform.
158pub fn default_provider() -> Box<dyn RootfsProvider> {
159    if super::overlay::is_overlay_supported() {
160        tracing::info!("Using overlayfs rootfs provider");
161        return Box::new(OverlayProvider);
162    }
163
164    tracing::info!("Overlayfs not available, using copy provider");
165    Box::new(CopyProvider)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use tempfile::TempDir;
172
173    fn make_sample_rootfs(dir: &Path) {
174        std::fs::create_dir_all(dir.join("etc")).unwrap();
175        std::fs::create_dir_all(dir.join("bin")).unwrap();
176        std::fs::write(dir.join("etc/hostname"), "testbox").unwrap();
177        std::fs::write(dir.join("bin/hello"), "#!/bin/sh\necho hi").unwrap();
178    }
179
180    #[test]
181    fn test_copy_provider_prepare() {
182        let tmp = TempDir::new().unwrap();
183        let cache_dir = tmp.path().join("cache");
184        let box_dir = tmp.path().join("box");
185        std::fs::create_dir_all(&cache_dir).unwrap();
186        std::fs::create_dir_all(&box_dir).unwrap();
187        make_sample_rootfs(&cache_dir);
188
189        let provider = CopyProvider;
190        let rootfs = provider.prepare(&box_dir, &cache_dir).unwrap();
191
192        assert_eq!(rootfs, box_dir.join("rootfs"));
193        assert!(rootfs.join("etc/hostname").exists());
194        assert_eq!(
195            std::fs::read_to_string(rootfs.join("etc/hostname")).unwrap(),
196            "testbox"
197        );
198        assert!(rootfs.join("bin/hello").exists());
199    }
200
201    #[test]
202    fn test_copy_provider_cleanup() {
203        let tmp = TempDir::new().unwrap();
204        let cache_dir = tmp.path().join("cache");
205        let box_dir = tmp.path().join("box");
206        std::fs::create_dir_all(&cache_dir).unwrap();
207        std::fs::create_dir_all(&box_dir).unwrap();
208        make_sample_rootfs(&cache_dir);
209
210        let provider = CopyProvider;
211        let rootfs = provider.prepare(&box_dir, &cache_dir).unwrap();
212        assert!(rootfs.exists());
213
214        provider.cleanup(&box_dir, false).unwrap();
215        assert!(!rootfs.exists());
216    }
217
218    #[test]
219    fn test_copy_provider_cleanup_nonexistent() {
220        let tmp = TempDir::new().unwrap();
221        let provider = CopyProvider;
222        // Should not error on missing dir
223        provider.cleanup(tmp.path(), false).unwrap();
224    }
225
226    #[test]
227    fn test_copy_provider_name() {
228        assert_eq!(CopyProvider.name(), "copy");
229    }
230
231    #[test]
232    fn test_overlay_provider_name() {
233        assert_eq!(OverlayProvider.name(), "overlay");
234    }
235
236    #[test]
237    fn test_default_provider_returns_something() {
238        let provider = default_provider();
239        // On any platform, we should get a provider
240        assert!(!provider.name().is_empty());
241    }
242
243    #[cfg(target_os = "linux")]
244    #[test]
245    fn test_overlay_provider_prepare_and_cleanup() {
246        if !super::super::overlay::is_overlay_supported() {
247            // Skip if overlay not available (e.g., in container without privileges)
248            return;
249        }
250
251        let tmp = TempDir::new().unwrap();
252        let cache_dir = tmp.path().join("cache");
253        let box_dir = tmp.path().join("box");
254        std::fs::create_dir_all(&cache_dir).unwrap();
255        std::fs::create_dir_all(&box_dir).unwrap();
256        make_sample_rootfs(&cache_dir);
257
258        let provider = OverlayProvider;
259        let merged = provider.prepare(&box_dir, &cache_dir).unwrap();
260
261        assert_eq!(merged, box_dir.join("merged"));
262        assert!(merged.join("etc/hostname").exists());
263        assert_eq!(
264            std::fs::read_to_string(merged.join("etc/hostname")).unwrap(),
265            "testbox"
266        );
267
268        // Write to merged — should go to upper
269        std::fs::write(merged.join("etc/newfile"), "overlay write").unwrap();
270        assert!(box_dir.join("upper/etc/newfile").exists());
271
272        provider.cleanup(&box_dir, false).unwrap();
273        assert!(!box_dir.join("merged").exists());
274        assert!(!box_dir.join("upper").exists());
275        assert!(!box_dir.join("work").exists());
276    }
277}