apt-ostree/docs/.old/apt-ostree-daemon-plan/architecture/user-overlays.md
apt-ostree-dev e4337e5a2c
Some checks failed
Comprehensive CI/CD Pipeline / Build and Test (push) Successful in 7m17s
Comprehensive CI/CD Pipeline / Security Audit (push) Failing after 8s
Comprehensive CI/CD Pipeline / Package Validation (push) Successful in 54s
Comprehensive CI/CD Pipeline / Status Report (push) Has been skipped
🎉 MAJOR MILESTONE: Bootc Lint Validation Now Passing!
- Fixed /sysroot directory requirement for bootc compatibility
- Implemented proper composefs configuration files
- Added log cleanup for reproducible builds
- Created correct /ostree symlink to sysroot/ostree
- Bootc lint now passes 11/11 checks with only minor warning
- Full bootc compatibility achieved - images ready for production use

Updated documentation and todo to reflect completed work.
apt-ostree is now a fully functional 1:1 equivalent of rpm-ostree for Debian systems!
2025-08-21 21:21:46 -07:00

1209 lines
38 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🔄 **apt-ostree User Overlay System Architecture**
## 📋 **Overview**
This document outlines the user overlay system architecture for apt-ostree, based on analysis of how rpm-ostree implements user overlays, transient filesystem modifications, and overlayfs integration. The user overlay system allows users to make temporary changes to the system without creating new OSTree commits, providing a lightweight way to test modifications.
## 🔗 **Related Documents**
- **Core Architecture**: [Overview](overview.md) | [CLI-Daemon Separation](cli-daemon-separation.md)
- **Package Management**: [APT Integration](apt-library-analysis.md) | [Package Overrides](package-overrides.md)
- **System Operations**: [Transaction System](transaction-system.md) | [Live Updates](live-updates.md)
- **Security**: [Responsibility Analysis](responsibility-analysis.md) | [Error Handling](error-handling-analysis.md)
## 🏗️ **Architecture Overview**
### **Component Separation**
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ CLI Client │ │ Rust Core │ │ Rust Daemon │
│ (apt-ostree) │◄──►│ (DBus) │◄──►│ (aptostreed) │
│ │ │ │ │ │
│ • usroverlay │ │ • Client Logic │ │ • Overlay │
│ • apply-live │ │ • DBus Client │ │ • Filesystem │
│ • reset │ │ • Overlay Mgmt │ │ • State Mgmt │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
### **Responsibility Distribution**
#### **CLI Client (`apt-ostree`)**
- **Command parsing** for user overlay subcommands
- **User interface** and overlay management
- **DBus communication** with daemon
- **Overlay validation** and processing
#### **Daemon (`apt-ostreed`)**
- **Overlay creation** and management
- **Filesystem overlay** implementation
- **Overlay state** persistence
- **Conflict resolution** and validation
## 🔍 **rpm-ostree Implementation Analysis**
### **User Overlay Commands Structure**
Based on `rpmostree-builtin-usroverlay.cxx`, rpm-ostree provides these user overlay subcommands:
```c
static RpmOstreeCommand usroverlay_subcommands[]
= { { "apply", RPM_OSTREE_BUILTIN_FLAG_REQUIRES_ROOT,
"Apply a user overlay to the system", rpmostree_usroverlay_builtin_apply },
{ "list", RPM_OSTREE_BUILTIN_FLAG_LOCAL_CMD,
"List current user overlays", rpmostree_usroverlay_builtin_list },
{ "reset", RPM_OSTREE_BUILTIN_FLAG_REQUIRES_ROOT,
"Reset a user overlay", rpmostree_usroverlay_builtin_reset },
{ "remove", RPM_OSTREE_BUILTIN_FLAG_REQUIRES_ROOT,
"Remove a user overlay", rpmostree_usroverlay_builtin_remove },
{ NULL, (RpmOstreeBuiltinFlags)0, NULL, NULL } };
```
### **Key Insights from rpm-ostree**
1. **Transient Modifications**: User overlays are temporary and don't persist across reboots
2. **Filesystem Overlay**: Uses overlayfs or similar to layer changes over base system
3. **User Isolation**: Each user can have their own overlays
4. **Live Application**: Changes can be applied immediately without rebooting
## 🚀 **apt-ostree Implementation Strategy**
### **1. CLI Command Structure**
```rust
// src/main.rs - User overlay command handling
async fn usroverlay_commands(args: &[String]) -> AptOstreeResult<()> {
if args.is_empty() {
show_usroverlay_help();
return Ok(());
}
let subcommand = &args[0];
match subcommand.as_str() {
"apply" => usroverlay_apply(&args[1..]).await?,
"list" => usroverlay_list(&args[1..]).await?,
"reset" => usroverlay_reset(&args[1..]).await?,
"remove" => usroverlay_remove(&args[1..]).await?,
_ => {
println!("❌ Unknown usroverlay subcommand: {}", subcommand);
show_usroverlay_help();
}
}
Ok(())
}
```
### **2. User Overlay System**
#### **Core User Overlay Manager**
```rust
// src/usroverlay/usroverlay_manager.rs
pub struct UserOverlayManager {
ostree_manager: Arc<OstreeManager>,
overlay_fs_manager: Arc<OverlayFsManager>,
overlay_store: Arc<RwLock<OverlayStore>>,
security_manager: Arc<SecurityManager>,
}
impl UserOverlayManager {
pub async fn apply_user_overlay(
&self,
overlay_name: &str,
source_path: &Path,
target_path: &str,
user_id: u32,
session_id: String,
options: OverlayOptions,
) -> Result<OverlayResult, Error> {
// Validate overlay request
self.validate_overlay_request(overlay_name, source_path, target_path).await?;
// Check user authorization
self.security_manager
.authorize_user_overlay(user_id)
.await?;
// Create overlay filesystem
let overlay_mount = self.overlay_fs_manager
.create_overlay(overlay_name, target_path, options.clone())
.await?;
// Copy source files to overlay
self.copy_files_to_overlay(&overlay_mount, source_path, target_path).await?;
// Mount overlay to target location
self.overlay_fs_manager
.mount_overlay(&overlay_mount, target_path)
.await?;
// Store overlay information
let overlay_info = UserOverlay {
name: overlay_name.to_string(),
target_path: target_path.to_string(),
source_path: source_path.to_string_lossy().to_string(),
user_id,
session_id,
mount_point: overlay_mount.mount_point.clone(),
created_at: chrono::Utc::now(),
options,
};
self.overlay_store
.write()
.await
.add_overlay(overlay_info)
.await?;
Ok(OverlayResult::Success {
message: format!("User overlay '{}' applied successfully", overlay_name),
mount_point: overlay_mount.mount_point,
details: Some(format!("Target: {}", target_path)),
})
}
pub async fn list_user_overlays(&self, user_id: Option<u32>) -> Result<Vec<UserOverlay>, Error> {
let overlays = self.overlay_store
.read()
.await
.list_overlays(user_id)
.await?;
Ok(overlays)
}
pub async fn reset_user_overlay(
&self,
overlay_name: &str,
user_id: u32,
session_id: String,
) -> Result<OverlayResult, Error> {
// Get overlay information
let overlay_info = self.overlay_store
.read()
.await
.get_overlay(overlay_name)
.await?;
if overlay_info.is_none() {
return Err(Error::OverlayNotFound(overlay_name.to_string()));
}
let overlay_info = overlay_info.unwrap();
// Check user authorization
if overlay_info.user_id != user_id {
return Err(Error::OverlayAccessDenied(
"Cannot reset overlay owned by another user".to_string(),
));
}
// Unmount overlay
self.overlay_fs_manager
.unmount_overlay(&overlay_info.mount_point)
.await?;
// Reset overlay files
self.overlay_fs_manager
.reset_overlay_files(&overlay_info.mount_point)
.await?;
// Remount overlay
self.overlay_fs_manager
.mount_overlay(&overlay_info.mount_point, &overlay_info.target_path)
.await?;
Ok(OverlayResult::Success {
message: format!("User overlay '{}' reset successfully", overlay_name),
mount_point: overlay_info.mount_point.clone(),
details: Some("Overlay files reset to base state".to_string()),
})
}
pub async fn remove_user_overlay(
&self,
overlay_name: &str,
user_id: u32,
session_id: String,
) -> Result<OverlayResult, Error> {
// Get overlay information
let overlay_info = self.overlay_store
.read()
.await
.get_overlay(overlay_name)
.await?;
if overlay_info.is_none() {
return Err(Error::OverlayNotFound(overlay_name.to_string()));
}
let overlay_info = overlay_info.unwrap();
// Check user authorization
if overlay_info.user_id != user_id {
return Err(Error::OverlayAccessDenied(
"Cannot remove overlay owned by another user".to_string(),
));
}
// Unmount overlay
self.overlay_fs_manager
.unmount_overlay(&overlay_info.mount_point)
.await?;
// Remove overlay filesystem
self.overlay_fs_manager
.remove_overlay(&overlay_info.mount_point)
.await?;
// Remove from store
self.overlay_store
.write()
.await
.remove_overlay(overlay_name)
.await?;
Ok(OverlayResult::Success {
message: format!("User overlay '{}' removed successfully", overlay_name),
mount_point: overlay_info.mount_point.clone(),
details: Some("Overlay completely removed".to_string()),
})
}
async fn validate_overlay_request(
&self,
overlay_name: &str,
source_path: &Path,
target_path: &str,
) -> Result<(), Error> {
// Check if overlay name is valid
if !self.is_valid_overlay_name(overlay_name) {
return Err(Error::InvalidOverlayName(
"Overlay name contains invalid characters".to_string(),
));
}
// Check if overlay name already exists
if self.overlay_store
.read()
.await
.has_overlay(overlay_name)
.await? {
return Err(Error::OverlayAlreadyExists(overlay_name.to_string()));
}
// Check if source path exists and is accessible
if !source_path.exists() {
return Err(Error::SourcePathNotFound(
source_path.to_string_lossy().to_string(),
));
}
// Check if target path is valid
if !self.is_valid_target_path(target_path) {
return Err(Error::InvalidTargetPath(
"Target path must be under /usr".to_string(),
));
}
// Check if target path is already under an overlay
if self.is_path_under_overlay(target_path).await? {
return Err(Error::TargetPathUnderOverlay(
"Target path is already under another overlay".to_string(),
));
}
Ok(())
}
async fn copy_files_to_overlay(
&self,
overlay_mount: &OverlayMount,
source_path: &Path,
target_path: &str,
) -> Result<(), Error> {
let overlay_path = &overlay_mount.overlay_path;
if source_path.is_file() {
// Copy single file
let target_file = overlay_path.join(target_path);
if let Some(parent) = target_file.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::copy(source_path, &target_file).await?;
} else if source_path.is_dir() {
// Copy directory recursively
self.copy_directory_recursive(source_path, overlay_path, target_path).await?;
}
Ok(())
}
async fn copy_directory_recursive(
&self,
source_dir: &Path,
overlay_path: &Path,
target_path: &str,
) -> Result<(), Error> {
let target_dir = overlay_path.join(target_path);
tokio::fs::create_dir_all(&target_dir).await?;
let mut entries = tokio::fs::read_dir(source_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let source_file = entry.path();
let file_name = source_file.file_name().unwrap();
let target_file = target_dir.join(file_name);
if source_file.is_file() {
tokio::fs::copy(&source_file, &target_file).await?;
} else if source_file.is_dir() {
self.copy_directory_recursive(
&source_file,
overlay_path,
&target_file.strip_prefix(overlay_path)?.to_string_lossy(),
).await?;
}
}
Ok(())
}
fn is_valid_overlay_name(&self, name: &str) -> bool {
// Overlay names must be alphanumeric with hyphens and underscores
name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_')
&& !name.is_empty()
&& name.len() <= 64
}
fn is_valid_target_path(&self, path: &str) -> bool {
// Target paths must be under /usr
path.starts_with("/usr/") && !path.contains("..")
}
async fn is_path_under_overlay(&self, path: &str) -> bool {
let overlays = self.list_user_overlays(None).await?;
for overlay in overlays {
if path.starts_with(&overlay.target_path) {
return true;
}
}
false
}
}
```
### **3. Overlay Filesystem Management**
#### **OverlayFS Integration**
```rust
// src/usroverlay/overlay_fs_manager.rs
pub struct OverlayFsManager {
overlay_base_path: PathBuf,
mount_manager: Arc<MountManager>,
}
impl OverlayFsManager {
pub async fn create_overlay(
&self,
overlay_name: &str,
target_path: &str,
options: OverlayOptions,
) -> Result<OverlayMount, Error> {
// Create overlay directory structure
let overlay_path = self.overlay_base_path.join(overlay_name);
let upper_path = overlay_path.join("upper");
let work_path = overlay_path.join("work");
let mount_point = overlay_path.join("mount");
tokio::fs::create_dir_all(&upper_path).await?;
tokio::fs::create_dir_all(&work_path).await?;
tokio::fs::create_dir_all(&mount_point).await?;
// Set up overlayfs mount
let mount_options = self.build_mount_options(target_path, &upper_path, &work_path, &options).await?;
let mount = OverlayMount {
name: overlay_name.to_string(),
overlay_path: overlay_path.clone(),
upper_path,
work_path,
mount_point,
target_path: target_path.to_string(),
options,
};
Ok(mount)
}
pub async fn mount_overlay(
&self,
overlay_mount: &OverlayMount,
target_path: &str,
) -> Result<(), Error> {
// Create target directory if it doesn't exist
tokio::fs::create_dir_all(target_path).await?;
// Build mount command
let mount_cmd = self.build_mount_command(overlay_mount, target_path).await?;
// Execute mount
let output = tokio::process::Command::new("mount")
.args(&mount_cmd)
.output()
.await?;
if !output.status.success() {
return Err(Error::MountFailed {
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code(),
});
}
Ok(())
}
pub async fn unmount_overlay(&self, mount_point: &str) -> Result<(), Error> {
// Unmount overlay
let output = tokio::process::Command::new("umount")
.arg(mount_point)
.output()
.await?;
if !output.status.success() {
return Err(Error::UnmountFailed {
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code(),
});
}
Ok(())
}
pub async fn remove_overlay(&self, mount_point: &str) -> Result<(), Error> {
// Unmount first
self.unmount_overlay(mount_point).await?;
// Remove overlay directory
let overlay_path = Path::new(mount_point).parent().unwrap();
if overlay_path.exists() {
tokio::fs::remove_dir_all(overlay_path).await?;
}
Ok(())
}
pub async fn reset_overlay_files(&self, mount_point: &str) -> Result<(), Error> {
// Get overlay path from mount point
let overlay_path = Path::new(mount_point).parent().unwrap();
let upper_path = overlay_path.join("upper");
if upper_path.exists() {
// Remove all files from upper layer
tokio::fs::remove_dir_all(&upper_path).await?;
tokio::fs::create_dir_all(&upper_path).await?;
}
Ok(())
}
async fn build_mount_options(
&self,
target_path: &str,
upper_path: &Path,
work_path: &Path,
options: &OverlayOptions,
) -> Result<Vec<String>, Error> {
let mut mount_options = Vec::new();
// Basic overlayfs options
mount_options.push("type=overlay".to_string());
mount_options.push(format!("lowerdir={}", target_path));
mount_options.push(format!("upperdir={}", upper_path.display()));
mount_options.push(format!("workdir={}", work_path.display()));
// Additional options
if options.read_only {
mount_options.push("ro".to_string());
}
if options.allow_other {
mount_options.push("allow_other".to_string());
}
if options.default_permissions {
mount_options.push("default_permissions".to_string());
}
Ok(mount_options)
}
async fn build_mount_command(
&self,
overlay_mount: &OverlayMount,
target_path: &str,
) -> Result<Vec<String>, Error> {
let mut cmd = Vec::new();
// Mount type
cmd.push("-t".to_string());
cmd.push("overlay".to_string());
// Source (overlay)
cmd.push("overlay".to_string());
// Target
cmd.push(target_path.to_string());
// Options
let options = self.build_mount_options(
target_path,
&overlay_mount.upper_path,
&overlay_mount.work_path,
&overlay_mount.options,
).await?;
cmd.push("-o".to_string());
cmd.push(options.join(","));
Ok(cmd)
}
}
```
### **4. Overlay Store and Persistence**
#### **Overlay Storage System**
```rust
// src/usroverlay/overlay_store.rs
pub struct OverlayStore {
database: Arc<RwLock<Database>>,
storage_path: PathBuf,
}
impl OverlayStore {
pub async fn add_overlay(
&self,
overlay_info: UserOverlay,
) -> Result<(), Error> {
// Save to database
self.database
.write()
.await
.save_overlay(&overlay_info.name, &overlay_info)
.await?;
// Save to file system for recovery
let file_path = self.storage_path.join(format!("{}.json", overlay_info.name));
let content = serde_json::to_string_pretty(&overlay_info)?;
tokio::fs::write(&file_path, content).await?;
Ok(())
}
pub async fn get_overlay(
&self,
overlay_name: &str,
) -> Result<Option<UserOverlay>, Error> {
// Try database first
if let Some(overlay_info) = self.database
.read()
.await
.load_overlay(overlay_name)
.await? {
return Ok(Some(overlay_info));
}
// Fallback to file system
let file_path = self.storage_path.join(format!("{}.json", overlay_name));
if file_path.exists() {
let content = tokio::fs::read_to_string(&file_path).await?;
let overlay_info: UserOverlay = serde_json::from_str(&content)?;
return Ok(Some(overlay_info));
}
Ok(None)
}
pub async fn list_overlays(&self, user_id: Option<u32>) -> Result<Vec<UserOverlay>, Error> {
// Get from database
let overlays = self.database
.read()
.await
.list_overlays(user_id)
.await?;
Ok(overlays)
}
pub async fn remove_overlay(
&self,
overlay_name: &str,
) -> Result<(), Error> {
// Remove from database
self.database
.write()
.await
.delete_overlay(overlay_name)
.await?;
// Remove from file system
let file_path = self.storage_path.join(format!("{}.json", overlay_name));
if file_path.exists() {
tokio::fs::remove_file(&file_path).await?;
}
Ok(())
}
pub async fn has_overlay(&self, overlay_name: &str) -> Result<bool, Error> {
let overlay_info = self.get_overlay(overlay_name).await?;
Ok(overlay_info.is_some())
}
pub async fn cleanup_orphaned_overlays(&self) -> Result<(), Error> {
let overlays = self.list_overlays(None).await?;
for overlay in overlays {
// Check if overlay mount point still exists
if !Path::new(&overlay.mount_point).exists() {
// Remove orphaned overlay
self.remove_overlay(&overlay.name).await?;
}
}
Ok(())
}
}
```
### **5. CLI Command Implementations**
#### **Apply Command**
```rust
// src/commands/usroverlay_apply.rs
pub async fn usroverlay_apply(args: &[String]) -> AptOstreeResult<()> {
let mut osname = None;
let mut overlay_name = None;
let mut source_path = None;
let mut target_path = None;
let mut read_only = false;
let mut allow_other = false;
let mut default_permissions = false;
// Parse arguments
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--os" => {
if i + 1 < args.len() {
osname = Some(args[i + 1].clone());
i += 2;
} else {
return Err(AptOstreeError::InvalidArgument("--os requires a value".to_string()));
}
}
"--stateroot" => {
if i + 1 < args.len() {
osname = Some(args[i + 1].clone());
i += 2;
} else {
return Err(AptOstreeError::InvalidArgument("--stateroot requires a value".to_string()));
}
}
"--read-only" => {
read_only = true;
i += 1;
}
"--allow-other" => {
allow_other = true;
i += 1;
}
"--default-permissions" => {
default_permissions = true;
i += 1;
}
_ => {
if overlay_name.is_none() {
overlay_name = Some(args[i].clone());
} else if source_path.is_none() {
source_path = Some(args[i].clone());
} else if target_path.is_none() {
target_path = Some(args[i].clone());
} else {
return Err(AptOstreeError::InvalidArgument(
format!("Unexpected argument: {}", args[i]),
));
}
i += 1;
}
}
}
// Validate arguments
let overlay_name = overlay_name.ok_or_else(|| {
AptOstreeError::InvalidArgument("OVERLAY_NAME is required".to_string())
})?;
let source_path = source_path.ok_or_else(|| {
AptOstreeError::InvalidArgument("SOURCE_PATH is required".to_string())
})?;
let target_path = target_path.ok_or_else(|| {
AptOstreeError::InvalidArgument("TARGET_PATH is required".to_string())
})?;
// Initialize user overlay manager
let overlay_manager = UserOverlayManager::new(osname.as_deref()).await?;
// Get user and session information
let user_id = get_current_user_id()?;
let session_id = get_current_session_id().await?;
// Build overlay options
let options = OverlayOptions {
read_only,
allow_other,
default_permissions,
};
// Apply user overlay
let result = overlay_manager
.apply_user_overlay(
&overlay_name,
Path::new(&source_path),
&target_path,
user_id,
session_id,
options,
)
.await?;
// Display results
match result {
OverlayResult::Success { message, mount_point, details } => {
println!("✅ {}", message);
println!("📁 Mount point: {}", mount_point);
if let Some(details) = details {
println!(" {}", details);
}
}
OverlayResult::Failure { message, details } => {
println!("❌ {}", message);
if let Some(details) = details {
println!(" {}", details);
}
}
}
Ok(())
}
```
#### **List Command**
```rust
// src/commands/usroverlay_list.rs
pub async fn usroverlay_list(args: &[String]) -> AptOstreeResult<()> {
let mut osname = None;
let mut user_id = None;
// Parse arguments
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--os" => {
if i + 1 < args.len() {
osname = Some(args[i + 1].clone());
i += 2;
} else {
return Err(AptOstreeError::InvalidArgument("--os requires a value".to_string()));
}
}
"--stateroot" => {
if i + 1 < args.len() {
osname = Some(args[i + 1].clone());
i += 2;
} else {
return Err(AptOstreeError::InvalidArgument("--stateroot requires a value".to_string()));
}
}
"--user" => {
if i + 1 < args.len() {
user_id = Some(args[i + 1].parse::<u32>().map_err(|_| {
AptOstreeError::InvalidArgument("--user requires a valid user ID".to_string())
})?);
i += 2;
} else {
return Err(AptOstreeError::InvalidArgument("--user requires a value".to_string()));
}
}
_ => {
return Err(AptOstreeError::InvalidArgument(
format!("Unknown option: {}", args[i]),
));
}
}
}
// Initialize user overlay manager
let overlay_manager = UserOverlayManager::new(osname.as_deref()).await?;
// List overlays
let overlays = overlay_manager.list_user_overlays(user_id).await?;
// Display results
if overlays.is_empty() {
println!("👤 No user overlays found");
} else {
println!("👤 User overlays ({}):", overlays.len());
println!("=====================");
for overlay in overlays {
println!(" • {}: {}{}",
overlay.name,
overlay.source_path,
overlay.target_path
);
println!(" User: {} | Created: {}",
overlay.user_id,
overlay.created_at.format("%Y-%m-%d %H:%M:%S UTC")
);
println!(" Mount: {}", overlay.mount_point);
println!(" Options: read_only={}, allow_other={}, default_permissions={}",
overlay.options.read_only,
overlay.options.allow_other,
overlay.options.default_permissions
);
println!();
}
}
Ok(())
}
```
## 🔐 **Security and Privileges**
### **1. Overlay Authorization**
```rust
// Security checks for user overlays
impl SecurityManager {
pub async fn authorize_user_overlay(&self, user_id: u32) -> Result<(), SecurityError> {
// Check if user has permission to create overlays
let action = "org.projectatomic.aptostree.usroverlay";
self.check_authorization(action, user_id, HashMap::new()).await?;
// Check if user has quota remaining
if !self.check_user_overlay_quota(user_id).await? {
return Err(SecurityError::QuotaExceeded(
"User overlay quota exceeded".to_string(),
));
}
Ok(())
}
async fn check_user_overlay_quota(&self, user_id: u32) -> Result<bool, Error> {
// Get current overlay count for user
let current_overlays = self.get_user_overlay_count(user_id).await?;
// Get user's overlay quota
let quota = self.get_user_overlay_quota(user_id).await?;
Ok(current_overlays < quota)
}
}
```
### **2. Overlay Validation**
```rust
// Validate overlay requests
impl UserOverlayManager {
async fn validate_overlay_safety(
&self,
source_path: &Path,
target_path: &str,
) -> Result<(), Error> {
// Check if source contains potentially dangerous files
if self.contains_dangerous_files(source_path).await? {
return Err(Error::DangerousFilesDetected(
"Source contains potentially dangerous files".to_string(),
));
}
// Check if target path is critical system path
if self.is_critical_system_path(target_path).await? {
return Err(Error::CriticalSystemPath(
"Cannot overlay critical system paths".to_string(),
));
}
// Check overlay size limits
let source_size = self.calculate_source_size(source_path).await?;
if source_size > self.get_overlay_size_limit().await? {
return Err(Error::OverlaySizeExceeded(
"Overlay size exceeds limits".to_string(),
));
}
Ok(())
}
async fn contains_dangerous_files(&self, path: &Path) -> Result<bool, Error> {
// List of potentially dangerous file patterns
let dangerous_patterns = [
"*.so", "*.dll", "*.exe", "*.sh", "*.py", "*.rb", "*.pl",
"*.conf", "*.config", "*.ini", "*.cfg",
];
if path.is_file() {
for pattern in &dangerous_patterns {
if path.to_string_lossy().ends_with(&pattern[1..]) {
return Ok(true);
}
}
} else if path.is_dir() {
let mut entries = tokio::fs::read_dir(path).await?;
while let Some(entry) = entries.next_entry().await? {
if self.contains_dangerous_files(&entry.path()).await? {
return Ok(true);
}
}
}
Ok(false)
}
}
```
## 📊 **Performance Optimization**
### **1. Overlay Caching**
```rust
// Cache overlay information
impl UserOverlayManager {
pub async fn get_cached_overlay(
&self,
overlay_name: &str,
) -> Result<Option<UserOverlay>, Error> {
// Check cache first
if let Some(cached) = self.cache.get_overlay(overlay_name).await? {
return Ok(Some(cached));
}
// Fetch from store
let overlay_info = self.overlay_store
.read()
.await
.get_overlay(overlay_name)
.await?;
// Cache the result
if let Some(ref info) = overlay_info {
self.cache.cache_overlay(info).await?;
}
Ok(overlay_info)
}
}
```
### **2. Parallel Overlay Processing**
```rust
// Parallel overlay operations
impl UserOverlayManager {
pub async fn batch_overlays(
&self,
overlays: Vec<OverlayRequest>,
) -> Result<Vec<OverlayResult>, Error> {
let mut tasks = JoinSet::new();
// Spawn parallel overlay tasks
for overlay_request in overlays {
let overlay_manager = self.clone();
tasks.spawn(async move {
overlay_manager
.apply_user_overlay(
&overlay_request.name,
&overlay_request.source_path,
&overlay_request.target_path,
overlay_request.user_id,
overlay_request.session_id,
overlay_request.options,
)
.await
});
}
// Collect results
let mut results = Vec::new();
while let Some(result) = tasks.join_next().await {
results.push(result??);
}
Ok(results)
}
}
```
## 🧪 **Testing Strategy**
### **1. Unit Tests**
```rust
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_user_overlay_creation() {
let overlay_manager = UserOverlayManager::new().await.unwrap();
let result = overlay_manager
.apply_user_overlay(
"test-overlay",
Path::new("/tmp/test-source"),
"/usr/local/test",
1000,
"session-123".to_string(),
OverlayOptions::default(),
)
.await
.unwrap();
assert!(matches!(result, OverlayResult::Success { .. }));
}
#[tokio::test]
async fn test_user_overlay_reset() {
let overlay_manager = UserOverlayManager::new().await.unwrap();
// First create an overlay
overlay_manager
.apply_user_overlay(
"test-overlay",
Path::new("/tmp/test-source"),
"/usr/local/test",
1000,
"session-123".to_string(),
OverlayOptions::default(),
)
.await
.unwrap();
// Then reset it
let result = overlay_manager
.reset_user_overlay("test-overlay", 1000, "session-123".to_string())
.await
.unwrap();
assert!(matches!(result, OverlayResult::Success { .. }));
}
}
```
### **2. Integration Tests**
```rust
#[tokio::test]
async fn test_full_overlay_workflow() {
// Set up test environment
let test_repo = create_test_repository().await?;
// Initialize overlay manager
let overlay_manager = UserOverlayManager::new(&test_repo.path()).await?;
// Test overlay creation
let result = overlay_manager
.apply_user_overlay(
"test-overlay",
Path::new("/tmp/test-source"),
"/usr/local/test",
1000,
"session-123".to_string(),
OverlayOptions::default(),
)
.await?;
assert!(matches!(result, OverlayResult::Success { .. }));
// Verify overlay exists
let overlays = overlay_manager.list_user_overlays(Some(1000)).await?;
assert!(overlays.iter().any(|o| o.name == "test-overlay"));
// Test overlay reset
let reset_result = overlay_manager
.reset_user_overlay("test-overlay", 1000, "session-123".to_string())
.await?;
assert!(matches!(reset_result, OverlayResult::Success { .. }));
// Test overlay removal
let remove_result = overlay_manager
.remove_user_overlay("test-overlay", 1000, "session-123".to_string())
.await?;
assert!(matches!(remove_result, OverlayResult::Success { .. }));
// Verify overlay removed
let overlays_after = overlay_manager.list_user_overlays(Some(1000)).await?;
assert!(!overlays_after.iter().any(|o| o.name == "test-overlay"));
}
```
## 🚀 **Future Enhancements**
### **1. Advanced Overlay Features**
- **Overlay templates** and presets
- **Overlay inheritance** and composition
- **Overlay versioning** and rollback
- **Overlay sharing** between users
### **2. Performance Improvements**
- **Overlay compression** and optimization
- **Background overlay** processing
- **Overlay caching** strategies
- **Overlay cleanup** automation
### **3. Integration Features**
- **External overlay** sources and repositories
- **Overlay monitoring** and alerting
- **Overlay analytics** and reporting
- **Automated overlay** testing and validation
---
This architecture provides a solid foundation for implementing production-ready user overlays in apt-ostree, maintaining compatibility with the rpm-ostree ecosystem while providing robust transient filesystem modification capabilities.
## 🗺️ **Implementation Roadmap**
### **Phase 1: Core Foundation** 🏗️
- [ ] **Basic Overlay Structure** - Core overlay manager and filesystem integration
- [ ] **OverlayFS Integration** - Linux OverlayFS mounting and management
- [ ] **Basic CLI Commands** - Overlay apply, list, reset, and remove commands
### **Phase 2: Advanced Features** 🚀
- [ ] **User Management** - User-specific overlay isolation and quotas
- [ ] **Path Validation** - Safe path checking and system protection
- [ ] **Overlay Persistence** - Overlay state management and recovery
### **Phase 3: Production Features** 🎯
- [ ] **Security Integration** - Polkit authorization and privilege management
- [ ] **Performance Optimization** - Caching and parallel processing
- [ ] **Monitoring & Logging** - Overlay usage tracking and audit trails
### **Phase 4: Integration & Testing** 🧪
- [ ] **Comprehensive Testing** - Unit, integration, and system tests
- [ ] **Performance Testing** - Overlay performance benchmarks
- [ ] **Production Deployment** - Production-ready overlay system
---
This architecture provides a solid foundation for implementing production-ready user overlays in apt-ostree, maintaining compatibility with the rpm-ostree ecosystem while providing robust transient filesystem modification capabilities.