bootc-docs/internals/bootc-internals-architecture.md
robojerk 526f1c1afd Initial commit: Comprehensive Debian bootc documentation
- Complete documentation for all bootc commands and subcommands
- Debian-specific adaptations and workarounds
- Manual installation methods to bypass bootc reliability issues
- Technical guides with Rust source code analysis
- Flowcharts and external command references
- Hidden command documentation (bootc internals, state, etc.)
- Composefs integration analysis
- Base image creation guides (with and without bootc binary)
- Management scripts and automation
- Comprehensive troubleshooting and examples
2025-09-15 14:02:28 -07:00

18 KiB

bootc internals - Architecture and Implementation

Overview

This document provides a deep dive into the architecture and implementation details of the bootc internals system, covering the Rust code structure, design patterns, and integration points.

Architecture Overview

The bootc internals system is built on a modular architecture that provides internal-only operations for system maintenance, debugging, and integration. The system is designed to be:

  • Hidden: Commands are not visible in regular help output
  • Internal: Intended for system administrators and developers
  • Modular: Each command is self-contained with clear responsibilities
  • Integrated: Seamlessly integrates with existing bootc infrastructure

Core Components

1. Command Structure

The internals system is built around the InternalsOpts enum, which defines all available internal commands:

#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum InternalsOpts {
    SystemdGenerator {
        normal_dir: Utf8PathBuf,
        early_dir: Option<Utf8PathBuf>,
        late_dir: Option<Utf8PathBuf>,
    },
    FixupEtcFstab,
    PrintJsonSchema {
        #[clap(long)]
        of: SchemaType,
    },
    #[clap(subcommand)]
    Fsverity(FsverityOpts),
    Fsck,
    Cleanup,
    Relabel {
        #[clap(long)]
        as_path: Option<Utf8PathBuf>,
        path: Utf8PathBuf,
    },
    OstreeExt { args: Vec<OsString> },
    Cfs { args: Vec<OsString> },
    OstreeContainer { args: Vec<OsString> },
    TestComposefs,
    LoopbackCleanupHelper { device: String },
    AllocateCleanupLoopback { file_path: Utf8PathBuf },
    BootcInstallCompletion { sysroot: Utf8PathBuf, stateroot: String },
    Reboot,
    #[cfg(feature = "rhsm")]
    PublishRhsmFacts,
    #[cfg(feature = "docgen")]
    DumpCliJson,
    DirDiff {
        pristine_etc: Utf8PathBuf,
        current_etc: Utf8PathBuf,
        new_etc: Utf8PathBuf,
        perform_merge: bool,
    },
}

2. Command Routing

Commands are routed through the main CLI dispatcher in cli.rs:

match opt {
    Opt::Internals(opts) => match opts {
        InternalsOpts::SystemdGenerator { normal_dir, early_dir, late_dir } => {
            let unit_dir = &Dir::open_ambient_dir(normal_dir, cap_std::ambient_authority())?;
            crate::generator::generator(root, unit_dir)
        }
        InternalsOpts::Fsck => {
            let sysroot = &get_storage().await?;
            crate::fsck::fsck(&sysroot, std::io::stdout().lock()).await?;
            Ok(())
        }
        // ... other commands
    }
}

Module Architecture

1. Generator Module (generator.rs)

Purpose: Systemd integration and unit generation

Key Functions:

  • fstab_generator_impl() - Generate fstab editor service
  • generate_fstab_editor() - Create systemd unit file
  • generator() - Main generator entry point

Implementation:

pub(crate) fn fstab_generator_impl(root: &Dir, unit_dir: &Dir) -> Result<bool> {
    // Check if system is ostree-booted
    if !is_ostree_booted_in(root)? {
        return Ok(false);
    }

    // Check /etc/fstab for anaconda stamp
    if let Some(fd) = root.open_optional("etc/fstab")? {
        let mut from_anaconda = false;
        for line in fd.lines() {
            let line = line?;
            if line.contains(BOOTC_EDITED_STAMP) {
                return Ok(false); // Already processed
            }
            if line.contains(FSTAB_ANACONDA_STAMP) {
                from_anaconda = true;
            }
        }
        
        if from_anaconda {
            generate_fstab_editor(unit_dir)?;
            return Ok(true);
        }
    }
    Ok(false)
}

Dependencies:

  • ostree_ext::container_utils::is_ostree_booted_in
  • cap_std::fs::Dir
  • rustix::fs::StatVfsMountFlags

2. Filesystem Check Module (fsck.rs)

Purpose: System consistency checking

Key Functions:

  • fsck() - Main consistency check entry point
  • fsck_ok() - Return success result
  • fsck_err() - Return error result

Implementation:

pub async fn fsck(sysroot: &Storage, mut out: impl Write) -> Result<()> {
    let ostree = sysroot.get_ostree()?;
    let repo = ostree.repo();
    
    // Run all registered fsck checks
    for check in FSCK_CHECKS {
        match check(sysroot).await {
            Ok(Ok(())) => writeln!(out, "✓ {}", check.name())?,
            Ok(Err(e)) => writeln!(out, "✗ {}: {}", check.name(), e)?,
            Err(e) => writeln!(out, "✗ {}: {}", check.name(), e)?,
        }
    }
    Ok(())
}

Dependencies:

  • linkme::distributed_slice - For check registration
  • ostree_ext::composefs - For composefs operations
  • ostree_ext::ostree - For OSTree operations

3. Composefs Control Module (cfsctl.rs)

Purpose: Composefs repository management

Key Functions:

  • run_from_iter() - Parse and execute cfsctl commands
  • App::parse() - Parse command line arguments
  • Command enum - Define available commands

Implementation:

pub async fn run_from_iter<I>(args: I) -> Result<()>
where
    I: IntoIterator<Item = impl AsRef<OsStr>>,
{
    let app = App::parse_from(args);
    let repo = if app.user {
        Repository::open_user()?
    } else if app.system {
        Repository::open_system()?
    } else if let Some(repo_path) = app.repo {
        Repository::open(repo_path)?
    } else {
        Repository::open_default()?
    };

    match app.cmd {
        Command::Oci(oci_cmd) => match oci_cmd {
            OciCommand::ImportLayer { sha256, name } => {
                // Import layer implementation
            }
            OciCommand::LsLayer { name } => {
                // List layer implementation
            }
            // ... other OCI commands
        }
        Command::CreateFs { name, output } => {
            // Create filesystem implementation
        }
        Command::WriteBoot { name, output } => {
            // Write boot entries implementation
        }
    }
}

Dependencies:

  • composefs::repository::Repository
  • composefs::fsverity::FsVerityHashValue
  • composefs_boot::write_boot

4. Deploy Module (deploy.rs)

Purpose: Deployment operations and cleanup

Key Functions:

  • fixup_etc_fstab() - Fix /etc/fstab for composefs
  • cleanup() - Perform system cleanup
  • switch_origin_inplace() - In-place origin switching

Implementation:

pub(crate) fn fixup_etc_fstab(root: &Dir) -> Result<()> {
    let fstab_path = root.open("etc/fstab")?;
    let mut fstab_content = String::new();
    fstab_path.read_to_string(&mut fstab_content)?;
    
    let mut lines = fstab_content.lines().collect::<Vec<_>>();
    let mut modified = false;
    
    for line in lines.iter_mut() {
        if line.contains("ro") && !line.contains("ro,") {
            *line = line.replace("ro", "ro,");
            modified = true;
        }
    }
    
    if modified {
        let mut fstab_file = root.create("etc/fstab")?;
        for line in lines {
            writeln!(fstab_file, "{}", line)?;
        }
        writeln!(fstab_file, "# {}", BOOTC_EDITED_STAMP)?;
    }
    
    Ok(())
}

Dependencies:

  • cap_std::fs::Dir
  • std::io::Write
  • ostree_ext::ostree

5. Block Device Module (blockdev.rs)

Purpose: Loopback device management

Key Functions:

  • LoopbackDevice::new() - Create loopback device
  • run_loopback_cleanup_helper() - Cleanup helper
  • run_allocate_cleanup_loopback() - Test allocation

Implementation:

pub struct LoopbackDevice {
    device: String,
    file: PathBuf,
}

impl LoopbackDevice {
    pub fn new(file_path: &Path) -> Result<Self> {
        let output = Command::new("losetup")
            .args(["-f", "--show", file_path.to_str().unwrap()])
            .output()?;
        
        if !output.status.success() {
            return Err(anyhow::anyhow!("Failed to create loopback device"));
        }
        
        let device = String::from_utf8(output.stdout)?;
        let device = device.trim().to_string();
        
        Ok(Self {
            device,
            file: file_path.to_path_buf(),
        })
    }
    
    pub fn path(&self) -> &str {
        &self.device
    }
}

impl Drop for LoopbackDevice {
    fn drop(&mut self) {
        let _ = Command::new("losetup")
            .args(["-d", &self.device])
            .output();
    }
}

Dependencies:

  • std::process::Command
  • std::path::Path
  • anyhow::Result

Design Patterns

1. Command Pattern

Each internals command follows the command pattern, encapsulating a request as an object:

trait InternalCommand {
    fn execute(&self) -> Result<()>;
}

impl InternalCommand for SystemdGenerator {
    fn execute(&self) -> Result<()> {
        let unit_dir = &Dir::open_ambient_dir(&self.normal_dir, cap_std::ambient_authority())?;
        crate::generator::generator(&self.root, unit_dir)
    }
}

2. Strategy Pattern

Different commands use different strategies for execution:

enum ExecutionStrategy {
    Direct(DirectCommand),
    Proxy(ProxyCommand),
    Generator(GeneratorCommand),
    Test(TestCommand),
}

impl ExecutionStrategy {
    fn execute(&self) -> Result<()> {
        match self {
            ExecutionStrategy::Direct(cmd) => cmd.execute_direct(),
            ExecutionStrategy::Proxy(cmd) => cmd.execute_proxy(),
            ExecutionStrategy::Generator(cmd) => cmd.execute_generator(),
            ExecutionStrategy::Test(cmd) => cmd.execute_test(),
        }
    }
}

3. Factory Pattern

Commands are created using a factory pattern:

struct CommandFactory;

impl CommandFactory {
    fn create_command(opts: &InternalsOpts) -> Box<dyn InternalCommand> {
        match opts {
            InternalsOpts::SystemdGenerator { .. } => {
                Box::new(SystemdGenerator::new(opts))
            }
            InternalsOpts::Fsck => {
                Box::new(FsckCommand::new())
            }
            // ... other commands
        }
    }
}

Error Handling

1. Error Types

The system uses a hierarchical error structure:

#[derive(thiserror::Error, Debug)]
pub enum InternalError {
    #[error("System error: {0}")]
    System(String),
    
    #[error("Permission error: {0}")]
    Permission(String),
    
    #[error("Resource error: {0}")]
    Resource(String),
    
    #[error("Configuration error: {0}")]
    Configuration(String),
}

impl From<std::io::Error> for InternalError {
    fn from(err: std::io::Error) -> Self {
        InternalError::System(err.to_string())
    }
}

2. Error Context

All operations use error context for better debugging:

#[context("Systemd generator")]
pub(crate) fn generator(root: &Dir, unit_dir: &Dir) -> Result<()> {
    // Implementation with automatic error context
}

#[context("Filesystem check")]
pub async fn fsck(sysroot: &Storage, out: impl Write) -> Result<()> {
    // Implementation with automatic error context
}

3. Error Recovery

The system implements error recovery strategies:

pub async fn execute_with_retry<F, T>(mut operation: F, max_retries: usize) -> Result<T>
where
    F: FnMut() -> Result<T>,
{
    let mut last_error = None;
    
    for attempt in 0..max_retries {
        match operation() {
            Ok(result) => return Ok(result),
            Err(e) => {
                last_error = Some(e);
                if attempt < max_retries - 1 {
                    tokio::time::sleep(Duration::from_millis(100 * (attempt + 1) as u64)).await;
                }
            }
        }
    }
    
    Err(last_error.unwrap())
}

Security Considerations

1. Privilege Escalation

All internals commands require root privileges:

pub(crate) fn require_root(is_container: bool) -> Result<()> {
    ensure!(
        rustix::process::getuid().is_root(),
        if is_container {
            "The user inside the container from which you are running this command must be root"
        } else {
            "This command must be executed as the root user"
        }
    );
    Ok(())
}

2. Input Validation

All inputs are validated before processing:

pub(crate) fn validate_path(path: &Path) -> Result<()> {
    // Check for path traversal
    if path.components().any(|c| c == Component::ParentDir) {
        return Err(anyhow::anyhow!("Path traversal detected"));
    }
    
    // Check for absolute paths
    if !path.is_absolute() {
        return Err(anyhow::anyhow!("Path must be absolute"));
    }
    
    Ok(())
}

3. Resource Limits

Operations are limited to prevent resource exhaustion:

pub(crate) fn with_resource_limits<F, T>(operation: F) -> Result<T>
where
    F: FnOnce() -> Result<T>,
{
    // Set memory limit
    let memory_limit = 1024 * 1024 * 1024; // 1GB
    let mut limits = rlimit::ResourceLimits::default();
    limits.set_memory_limit(memory_limit)?;
    limits.apply()?;
    
    // Execute operation
    operation()
}

Performance Optimization

1. Async Operations

All I/O operations are asynchronous:

pub async fn cleanup_async(sysroot: &Storage) -> Result<()> {
    let ostree = sysroot.get_ostree()?;
    let repo = ostree.repo();
    
    // Async repository operations
    let deployments = repo.list_deployments().await?;
    let unused_deployments = find_unused_deployments(deployments).await?;
    
    for deployment in unused_deployments {
        repo.remove_deployment(deployment).await?;
    }
    
    // Async garbage collection
    repo.garbage_collect().await?;
    
    Ok(())
}

2. Parallel Processing

Operations that can be parallelized are:

pub async fn parallel_checks(sysroot: &Storage) -> Result<()> {
    let checks = vec![
        check_ostree_repo(sysroot),
        check_composefs_repo(sysroot),
        check_deployments(sysroot),
        check_configuration(sysroot),
    ];
    
    let results = futures::future::join_all(checks).await;
    
    for result in results {
        result?;
    }
    
    Ok(())
}

3. Caching

Frequently accessed data is cached:

pub struct Cache {
    deployments: Option<Vec<Deployment>>,
    images: Option<Vec<Image>>,
    last_update: Option<Instant>,
}

impl Cache {
    pub async fn get_deployments(&mut self, sysroot: &Storage) -> Result<&Vec<Deployment>> {
        if self.deployments.is_none() || self.should_refresh() {
            self.deployments = Some(sysroot.get_deployments().await?);
            self.last_update = Some(Instant::now());
        }
        
        Ok(self.deployments.as_ref().unwrap())
    }
    
    fn should_refresh(&self) -> bool {
        self.last_update
            .map(|last| last.elapsed() > Duration::from_secs(60))
            .unwrap_or(true)
    }
}

Testing Strategy

1. Unit Tests

Each module has comprehensive unit tests:

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_systemd_generator() {
        let temp_dir = tempfile::tempdir().unwrap();
        let unit_dir = Dir::open_ambient_dir(temp_dir.path(), cap_std::ambient_authority()).unwrap();
        
        // Test generator functionality
        let result = fstab_generator_impl(&root, &unit_dir);
        assert!(result.is_ok());
    }
    
    #[test]
    fn test_fsck_operations() {
        let temp_sysroot = create_test_sysroot().unwrap();
        
        // Test fsck functionality
        let result = fsck(&temp_sysroot, Vec::new()).await;
        assert!(result.is_ok());
    }
}

2. Integration Tests

End-to-end integration tests:

#[tokio::test]
async fn test_internals_integration() {
    // Setup test environment
    let test_env = TestEnvironment::new().await.unwrap();
    
    // Test systemd generator
    let result = test_env.run_internals_command("systemd-generator", &["/tmp"]).await;
    assert!(result.is_ok());
    
    // Test fsck
    let result = test_env.run_internals_command("fsck", &[]).await;
    assert!(result.is_ok());
    
    // Test cleanup
    let result = test_env.run_internals_command("cleanup", &[]).await;
    assert!(result.is_ok());
}

3. Performance Tests

Performance benchmarks:

#[bench]
fn bench_fsck_operations(b: &mut Bencher) {
    let sysroot = create_test_sysroot().unwrap();
    
    b.iter(|| {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(fsck(&sysroot, Vec::new()))
    });
}

Future Enhancements

1. Plugin System

A plugin system for extending internals functionality:

pub trait InternalPlugin {
    fn name(&self) -> &str;
    fn execute(&self, args: &[String]) -> Result<()>;
    fn help(&self) -> &str;
}

pub struct PluginManager {
    plugins: Vec<Box<dyn InternalPlugin>>,
}

impl PluginManager {
    pub fn register_plugin(&mut self, plugin: Box<dyn InternalPlugin>) {
        self.plugins.push(plugin);
    }
    
    pub fn execute_plugin(&self, name: &str, args: &[String]) -> Result<()> {
        let plugin = self.plugins.iter()
            .find(|p| p.name() == name)
            .ok_or_else(|| anyhow::anyhow!("Plugin not found: {}", name))?;
        
        plugin.execute(args)
    }
}

2. Configuration Management

Centralized configuration for internals commands:

#[derive(Debug, Serialize, Deserialize)]
pub struct InternalsConfig {
    pub fsck: FsckConfig,
    pub cleanup: CleanupConfig,
    pub generator: GeneratorConfig,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct FsckConfig {
    pub enabled_checks: Vec<String>,
    pub timeout_seconds: u64,
    pub parallel_checks: bool,
}

3. Monitoring Integration

Integration with monitoring systems:

pub trait MetricsCollector {
    fn record_command_execution(&self, command: &str, duration: Duration, success: bool);
    fn record_error(&self, command: &str, error: &str);
    fn record_resource_usage(&self, command: &str, memory: u64, cpu: f64);
}

pub struct PrometheusCollector {
    registry: Registry,
}

impl MetricsCollector for PrometheusCollector {
    fn record_command_execution(&self, command: &str, duration: Duration, success: bool) {
        // Record metrics in Prometheus format
    }
}

This architecture document provides a comprehensive understanding of the bootc internals system's design, implementation, and future direction.