# Calamares Module for bootc install

## Overview

This document outlines a focused plan for creating a single Calamares module that handles `bootc install` operations. This is the simplest and most direct approach, following the official bootc documentation patterns.

## Executive Summary

**Goal**: Create a single Calamares module that executes `bootc install` commands with proper configuration and error handling.

**Timeline**: 2-3 months (focused, single-purpose implementation)
**Complexity**: Low (direct tool integration)
**Target**: Debian 13 (Trixie) with bootc support

## Real-World Analysis

### How bootc install Actually Works

Based on the [official bootc documentation](https://bootc-dev.github.io/bootc//bootc-install.html) and [Fedora's bare metal documentation](https://docs.fedoraproject.org/en-US/bootc/bare-metal/):

#### 1. Core Commands
```bash
bootc install to-disk /dev/sda
bootc install to-filesystem /path/to/mounted/fs
bootc install to-existing-root
```

#### 2. Container Execution Pattern
From [Fedora's official documentation](https://docs.fedoraproject.org/en-US/bootc/bare-metal/):
```bash
podman run \
--rm --privileged \
--pid=host \
-v /dev:/dev \
-v /var/lib/containers:/var/lib/containers \
--security-opt label=type:unconfined_t \
<image> \
bootc install to-disk /path/to/disk
```

#### 3. Key Differences from Anaconda Approach
- **No kickstart files** - Direct container execution
- **No network during install** - Container is already pulled
- **Minimal configuration** - Basic installer built into bootc
- **Live ISO environment** - Typically run from Fedora CoreOS Live ISO

#### 3. Key OSTree Filesystem Nuances

**Critical differences from traditional Linux installations:**

- **Composefs by default**: [Fedora bootc uses composefs](https://docs.fedoraproject.org/en-US/bootc/filesystem/) for the root filesystem
- **Read-only root**: Filesystem is read-only at runtime (like `podman run --read-only`)
- **Special mount points**: `/etc` and `/var` are persistent, mutable bind mounts
- **Kernel location**: Kernel is in `/usr/lib/ostree-boot/` not `/boot/`
- **3-way merge**: `/etc` changes are merged across upgrades
- **Transient mountpoints**: Support for dynamic mountpoints with `transient-ro`

#### 4. Filesystem Layout
```
/usr/lib/ostree-boot/     # Kernel and initrd (not /boot/)
/etc/                     # Persistent, mutable (bind mount)
/var/                     # Persistent, mutable (bind mount)
/usr/                     # Read-only (composefs)
/opt/                     # Read-only (composefs)
```

#### 5. Authentication Patterns
From [Fedora's authentication documentation](https://docs.fedoraproject.org/en-US/bootc/authentication/):
- **Registry auth**: `/etc/ostree/auth.json` for container registries
- **SSH keys**: Via kickstart or bootc-image-builder config
- **User management**: systemd-sysusers for local users
- **nss-altfiles**: Static users in `/usr/lib/passwd` and `/usr/lib/group`

## Implementation Plan

### Phase 1: Core Module Development (Month 1)

#### 1.1 Module Structure
```cpp
class BootcInstallModule : public Calamares::Module
{
public:
    void init() override;
    QList<Calamares::job_ptr> jobs() const override;
    
private:
    QString m_containerUrl;
    QString m_targetDevice;
    QString m_installType;  // "to-disk", "to-filesystem", "to-existing-root"
    bool m_authRequired;
    QString m_authJson;
};
```

#### 1.2 Configuration Loading
```cpp
void BootcInstallModule::init()
{
    auto config = Calamares::ModuleSystem::instance()->moduleConfiguration("bootc-install");
    
    m_containerUrl = config.value("containerUrl").toString();
    m_targetDevice = config.value("targetDevice").toString();
    m_installType = config.value("installType", "to-disk").toString();
    m_authRequired = config.value("authRequired", false).toBool();
    m_authJson = config.value("authJson").toString();
}
```

#### 1.3 Job Implementation
```cpp
QList<Calamares::job_ptr> BootcInstallModule::jobs() const
{
    QList<Calamares::job_ptr> jobs;
    
    // Registry authentication job
    if (m_authRequired) {
        jobs.append(Calamares::job_ptr(new RegistryAuthJob(m_authJson)));
    }
    
    // Bootc installation job
    jobs.append(Calamares::job_ptr(new BootcInstallJob(m_containerUrl, m_targetDevice, m_installType)));
    
    // Post-install configuration job
    jobs.append(Calamares::job_ptr(new BootcPostInstallJob(m_targetDevice, m_sshKey, m_username)));
    
    return jobs;
}
```

### Phase 2: Job Implementations (Month 1-2)

#### 2.1 Registry Authentication Job
```cpp
class RegistryAuthJob : public Calamares::Job
{
public:
    RegistryAuthJob(const QString& authJson) : m_authJson(authJson) {}
    
    QString prettyName() const override { return "Configuring registry authentication"; }
    Calamares::JobResult exec() override;
    
private:
    QString m_authJson;
};

Calamares::JobResult RegistryAuthJob::exec()
{
    // Create /etc/ostree directory (persistent bind mount)
    if (!QDir("/etc/ostree").exists()) {
        if (!QDir().mkpath("/etc/ostree")) {
            return Calamares::JobResult::error("Failed to create /etc/ostree directory");
        }
    }
    
    // Write auth.json (will persist across upgrades due to /etc bind mount)
    QFile authFile("/etc/ostree/auth.json");
    if (!authFile.open(QIODevice::WriteOnly)) {
        return Calamares::JobResult::error("Failed to open /etc/ostree/auth.json for writing");
    }
    
    authFile.write(m_authJson.toUtf8());
    authFile.close();
    
    return Calamares::JobResult::ok();
}
```

#### 2.2 Bootc Install Job
```cpp
class BootcInstallJob : public Calamares::Job
{
public:
    BootcInstallJob(const QString& containerUrl, const QString& targetDevice, const QString& installType)
        : m_containerUrl(containerUrl), m_targetDevice(targetDevice), m_installType(installType) {}
    
    QString prettyName() const override { return "Installing bootc container"; }
    Calamares::JobResult exec() override;
    
private:
    QString m_containerUrl;
    QString m_targetDevice;
    QString m_installType;
};

Calamares::JobResult BootcInstallJob::exec()
{
    // Build podman command with OSTree-specific considerations
    QStringList args;
    args << "run" << "--rm" << "--privileged";
    args << "--pid=host";
    args << "-v" << "/dev:/dev";
    args << "-v" << "/var/lib/containers:/var/lib/containers";
    args << "--security-opt" << "label=type:unconfined_t";
    
    // Add environment variables for OSTree filesystem
    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
    env.insert("OSTREE_NO_SIGNATURE_VERIFICATION", "1");  // For composefs unsigned mode
    env.insert("LIBMOUNT_FORCE_MOUNT2", "always");        // For transient-ro support
    
    args << m_containerUrl;
    args << "bootc" << "install" << m_installType << m_targetDevice;
    
    // Execute podman command
    QProcess process;
    process.setProcessEnvironment(env);
    process.start("podman", args);
    
    if (!process.waitForFinished(-1)) {
        return Calamares::JobResult::error("bootc install command timed out");
    }
    
    if (process.exitCode() != 0) {
        QString error = QString("bootc install failed: %1").arg(process.readAllStandardError());
        return Calamares::JobResult::error(error);
    }
    
    return Calamares::JobResult::ok();
}
```

#### 2.3 Post-Install Configuration Job
```cpp
class BootcPostInstallJob : public Calamares::Job
{
public:
    BootcPostInstallJob(const QString& targetDevice, const QString& sshKey, const QString& username)
        : m_targetDevice(targetDevice), m_sshKey(sshKey), m_username(username) {}
    
    QString prettyName() const override { return "Configuring bootc system"; }
    Calamares::JobResult exec() override;
    
private:
    QString m_targetDevice;
    QString m_sshKey;
    QString m_username;
    
    bool configureBootloader();
    bool createUserAccount();
    bool setupSshKey();
};

Calamares::JobResult BootcPostInstallJob::exec()
{
    // Mount the installed system
    QString mountPoint = "/mnt/bootc-install";
    if (!QDir().mkpath(mountPoint)) {
        return Calamares::JobResult::error("Failed to create mount point");
    }
    
    // Mount the root partition
    QProcess mount;
    mount.start("mount", QStringList() << m_targetDevice << mountPoint);
    if (!mount.waitForFinished() || mount.exitCode() != 0) {
        return Calamares::JobResult::error("Failed to mount installed system");
    }
    
    // Configure bootloader (GRUB2 for OSTree)
    if (!configureBootloader()) {
        return Calamares::JobResult::error("Failed to configure bootloader");
    }
    
    // Create user account
    if (!createUserAccount()) {
        return Calamares::JobResult::error("Failed to create user account");
    }
    
    // Setup SSH key
    if (!setupSshKey()) {
        return Calamares::JobResult::error("Failed to setup SSH key");
    }
    
    // Unmount
    QProcess umount;
    umount.start("umount", QStringList() << mountPoint);
    umount.waitForFinished();
    
    return Calamares::JobResult::ok();
}

bool BootcPostInstallJob::configureBootloader()
{
    // OSTree systems use GRUB2 with specific configuration
    // Kernel is in /usr/lib/ostree-boot/, not /boot/
    QString grubConfig = QString("/mnt/bootc-install/boot/grub2/grub.cfg");
    
    // Check if GRUB2 configuration exists
    if (!QFile::exists(grubConfig)) {
        // Run grub2-mkconfig to generate configuration
        QProcess grubMkconfig;
        grubMkconfig.start("grub2-mkconfig", QStringList() << "-o" << grubConfig);
        if (!grubMkconfig.waitForFinished() || grubMkconfig.exitCode() != 0) {
            return false;
        }
    }
    
    // Install GRUB2 to the target device
    QProcess grubInstall;
    grubInstall.start("grub2-install", QStringList() << "--target=x86_64-efi" << m_targetDevice);
    if (!grubInstall.waitForFinished() || grubInstall.exitCode() != 0) {
        return false;
    }
    
    return true;
}

bool BootcPostInstallJob::createUserAccount()
{
    // OSTree systems use systemd-sysusers for user management
    // Users are defined in /usr/lib/sysusers.d/ or /etc/sysusers.d/
    QString sysusersConfig = "/mnt/bootc-install/etc/sysusers.d/calamares-user.conf";
    
    QString userConfig = QString("u %1 1000 \"%1\" /home/%1\n").arg(m_username);
    userConfig += QString("g %1 1000\n").arg(m_username);
    userConfig += QString("m %1 %1\n").arg(m_username);
    
    QFile file(sysusersConfig);
    if (!file.open(QIODevice::WriteOnly)) {
        return false;
    }
    
    file.write(userConfig.toUtf8());
    file.close();
    
    return true;
}

bool BootcPostInstallJob::setupSshKey()
{
    if (m_sshKey.isEmpty()) return true;
    
    // Create .ssh directory for root
    QString sshDir = "/mnt/bootc-install/root/.ssh";
    if (!QDir().mkpath(sshDir)) {
        return false;
    }
    
    // Write SSH key
    QString keyFile = sshDir + "/authorized_keys";
    QFile file(keyFile);
    if (!file.open(QIODevice::WriteOnly)) {
        return false;
    }
    
    file.write(m_sshKey.toUtf8());
    file.close();
    
    // Set proper permissions
    QFile::setPermissions(keyFile, QFile::ReadOwner | QFile::WriteOwner);
    QFile::setPermissions(sshDir, QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
    
    return true;
}
```

### Phase 3: OSTree Filesystem Considerations (Month 2)

#### 3.1 Filesystem Layout Validation
```cpp
class OstreeFilesystemValidator
{
public:
    static bool validateTargetDevice(const QString& device);
    static bool checkComposefsSupport();
    static bool validateMountPoints();
    
private:
    static bool isOstreeBootPath(const QString& path);
    static bool checkTransientRoSupport();
};

bool OstreeFilesystemValidator::validateTargetDevice(const QString& device)
{
    // Check if target device can support OSTree layout
    // - GPT partition table required
    // - EFI system partition for UEFI
    // - Root partition for OSTree deployment
    // - Boot partition for kernel/initrd (not /boot/ but /usr/lib/ostree-boot/)
    
    QProcess sfdisk;
    sfdisk.start("sfdisk", QStringList() << "-l" << device);
    if (!sfdisk.waitForFinished()) return false;
    
    QString output = sfdisk.readAllStandardOutput();
    return output.contains("GPT") && output.contains("EFI");
}

bool OstreeFilesystemValidator::checkComposefsSupport()
{
    // Check if composefs is available in the container
    QProcess composefs;
    composefs.start("composefs", QStringList() << "--help");
    return composefs.waitForFinished() && composefs.exitCode() == 0;
}
```

#### 3.2 Kernel and Initrd Handling
```cpp
class OstreeBootManager
{
public:
    static QString getKernelPath();
    static QString getInitrdPath();
    static bool setupBootloader(const QString& device);
    static bool regenerateInitramfs(const QString& mountPoint);
    
private:
    static const QString OSTREE_BOOT_PATH;  // "/usr/lib/ostree-boot/"
};

const QString OstreeBootManager::OSTREE_BOOT_PATH = "/usr/lib/ostree-boot/";

QString OstreeBootManager::getKernelPath()
{
    // Kernel is in /usr/lib/ostree-boot/, not /boot/
    return OSTREE_BOOT_PATH + "vmlinuz";
}

QString OstreeBootManager::getInitrdPath()
{
    // Initrd is in /usr/lib/ostree-boot/, not /boot/
    return OSTREE_BOOT_PATH + "initramfs.img";
}

bool OstreeBootManager::regenerateInitramfs(const QString& mountPoint)
{
    // OSTree systems need initramfs regeneration after configuration changes
    // This is critical for filesystem configuration changes
    
    QProcess dracut;
    dracut.start("dracut", QStringList() 
                 << "--force" 
                 << "--hostonly" 
                 << "--kver" << "5.15.0"  // Get actual kernel version
                 << mountPoint + "/boot/initramfs.img");
    
    if (!dracut.waitForFinished()) {
        return false;
    }
    
    return dracut.exitCode() == 0;
}
```

#### 3.3 Persistent State Management
```cpp
class OstreeStateManager
{
public:
    static bool setupEtcBindMount();
    static bool setupVarBindMount();
    static bool configureTransientRo();
    
private:
    static bool createBindMount(const QString& source, const QString& target);
};

bool OstreeStateManager::setupEtcBindMount()
{
    // /etc is a persistent, mutable bind mount
    // Changes here persist across upgrades via 3-way merge
    return createBindMount("/etc", "/etc");
}

bool OstreeStateManager::configureTransientRo()
{
    // Enable transient-ro for dynamic mountpoints
    QString configPath = "/usr/lib/ostree/prepare-root.conf";
    QString config = "[root]\ntransient-ro = true\n";
    
    QFile file(configPath);
    if (!file.open(QIODevice::WriteOnly)) return false;
    
    file.write(config.toUtf8());
    file.close();
    
    // Regenerate initramfs after config change
    QProcess dracut;
    dracut.start("dracut", QStringList() << "--force");
    return dracut.waitForFinished() && dracut.exitCode() == 0;
}
```

### Phase 4: UI Integration (Month 2-3)

#### 4.1 Configuration Page
```cpp
class BootcInstallPage : public QWidget
{
    Q_OBJECT
    
public:
    explicit BootcInstallPage(QWidget* parent = nullptr);
    
    QString containerUrl() const;
    QString targetDevice() const;
    QString installType() const;
    bool authRequired() const;
    QString authJson() const;
    bool enableTransientRo() const;
    
private slots:
    void onContainerUrlChanged();
    void onTargetDeviceChanged();
    void onInstallTypeChanged();
    void onAuthRequiredChanged();
    void validateOstreeRequirements();
    
private:
    QLineEdit* m_containerUrlEdit;
    QLineEdit* m_targetDeviceEdit;
    QComboBox* m_installTypeCombo;
    QCheckBox* m_authRequiredCheck;
    QTextEdit* m_authJsonEdit;
    QCheckBox* m_transientRoCheck;
    QLabel* m_ostreeStatusLabel;
};
```

#### 4.2 OSTree-Aware Validation
```cpp
bool BootcInstallPage::validate()
{
    if (m_containerUrlEdit->text().isEmpty()) {
        Calamares::Branding::instance()->setValidationError("Container URL is required");
        return false;
    }
    
    if (m_targetDeviceEdit->text().isEmpty()) {
        Calamares::Branding::instance()->setValidationError("Target device is required");
        return false;
    }
    
    // Validate OSTree filesystem requirements
    if (!OstreeFilesystemValidator::validateTargetDevice(m_targetDeviceEdit->text())) {
        Calamares::Branding::instance()->setValidationError("Target device must have GPT partition table and EFI support");
        return false;
    }
    
    if (!OstreeFilesystemValidator::checkComposefsSupport()) {
        Calamares::Branding::instance()->setValidationError("Composefs support not available in container");
        return false;
    }
    
    if (m_authRequiredCheck->isChecked() && m_authJsonEdit->toPlainText().isEmpty()) {
        Calamares::Branding::instance()->setValidationError("Authentication JSON is required");
        return false;
    }
    
    return true;
}

void BootcInstallPage::validateOstreeRequirements()
{
    // Real-time validation of OSTree requirements
    bool deviceValid = OstreeFilesystemValidator::validateTargetDevice(m_targetDeviceEdit->text());
    bool composefsValid = OstreeFilesystemValidator::checkComposefsSupport();
    
    QString status;
    if (deviceValid && composefsValid) {
        status = "✓ OSTree requirements satisfied";
        m_ostreeStatusLabel->setStyleSheet("color: green;");
    } else {
        status = "✗ OSTree requirements not met";
        m_ostreeStatusLabel->setStyleSheet("color: red;");
    }
    
    m_ostreeStatusLabel->setText(status);
}
```

### Phase 4: Testing and Polish (Month 2-3)

#### 4.1 Unit Tests
```cpp
class BootcInstallModuleTest : public QObject
{
    Q_OBJECT
    
private slots:
    void testModuleInitialization();
    void testJobCreation();
    void testRegistryAuthJob();
    void testBootcInstallJob();
    void testValidation();
};
```

#### 4.2 Integration Tests
- [ ] Test with real bootc containers
- [ ] Test registry authentication
- [ ] Test different install types
- [ ] Test error handling

## Configuration

### Module Configuration
```yaml
# bootc-install.conf
module: bootc-install
config:
  containerUrl: "quay.io/centos-bootc/centos-bootc:stream9"
  targetDevice: "/dev/sda"
  installType: "to-disk"  # to-disk, to-filesystem, to-existing-root
  authRequired: false
  authJson: ""
  
  # User account configuration
  username: "admin"
  sshKey: ""  # SSH public key for root access
  
  # OSTree-specific configuration
  ostree:
    enableComposefs: true
    enableTransientRo: false
    kernelPath: "/usr/lib/ostree-boot/vmlinuz"
    initrdPath: "/usr/lib/ostree-boot/initramfs.img"
    bootPath: "/usr/lib/ostree-boot/"
    
  # Bootloader configuration
  bootloader:
    type: "grub2"
    target: "x86_64-efi"
    regenerateInitramfs: true
    
  # Filesystem validation
  validation:
    requireGpt: true
    requireEfi: true
    checkComposefs: true
    validateMountPoints: true
```

### Calamares Integration
```yaml
# calamares.conf
modules:
  - bootc-install

bootc-install:
  containerUrl: "quay.io/centos-bootc/centos-bootc:stream9"
  targetDevice: "/dev/sda"
  installType: "to-disk"
  authRequired: false
  
  # OSTree filesystem settings
  ostree:
    enableComposefs: true
    enableTransientRo: false
```

## Technical Architecture

### File Structure
```
calamares-bootc-install/
├── src/
│   ├── BootcInstallModule.cpp
│   ├── BootcInstallJob.cpp
│   ├── RegistryAuthJob.cpp
│   ├── BootcInstallPage.cpp
│   └── BootcInstallPage.ui
├── config/
│   └── bootc-install.conf
├── tests/
│   ├── BootcInstallModuleTest.cpp
│   └── testdata/
└── CMakeLists.txt
```

### Dependencies
- **Calamares**: Module framework
- **Qt**: UI and core functionality
- **podman**: Container runtime
- **bootc**: Container installation tool

## Key Implementation Details

### 1. Simple and Focused
- **Single purpose**: Only handles `bootc install`
- **Direct integration**: Calls podman and bootc directly
- **Minimal complexity**: No pattern switching or hybrid approaches

### 2. Follow Official Patterns
- **Use exact podman command** from Fedora documentation
- **Follow bootc install syntax** from official docs
- **Handle registry auth** via `/etc/ostree/auth.json`

### 3. Error Handling
- **Validate inputs** before execution
- **Handle podman failures** gracefully
- **Provide clear error messages** to users

## Success Metrics

### Technical Metrics
- [ ] Module loads and initializes correctly
- [ ] Jobs execute successfully
- [ ] Registry authentication works
- [ ] bootc install completes successfully

### User Experience Metrics
- [ ] Clear configuration UI
- [ ] Proper validation and error messages
- [ ] Progress reporting during installation
- [ ] Integration with Calamares workflow

## Conclusion

This focused plan creates a single, purpose-built Calamares module for `bootc install` operations that properly accounts for the unique OSTree filesystem characteristics and the specific nuances of direct container installation. By following the [official bootc bare metal documentation](https://docs.fedoraproject.org/en-US/bootc/bare-metal/) exactly and incorporating the specific requirements for bootloader configuration, initramfs handling, and user account creation, we can create a reliable, maintainable solution.

### Key bootc install Considerations Addressed:

1. **Direct Container Execution** - Uses [podman run with privileged mode](https://docs.fedoraproject.org/en-US/bootc/bare-metal/) for installation
2. **Post-Install Configuration** - Handles bootloader setup, user creation, and SSH key configuration
3. **OSTree Filesystem Layout** - Kernel in `/usr/lib/ostree-boot/` not `/boot/`
4. **GRUB2 Configuration** - Proper bootloader setup for OSTree systems
5. **Initramfs Regeneration** - Critical for filesystem configuration changes
6. **User Account Management** - Uses systemd-sysusers for OSTree-compatible user creation
7. **SSH Key Setup** - Proper permissions and directory structure for root access

### Key Advantages:

1. **bootc install Native** - Follows the [official bootc install approach](https://docs.fedoraproject.org/en-US/bootc/bare-metal/) exactly
2. **Complete Installation** - Handles both installation and post-install configuration
3. **OSTree-Aware** - Properly manages filesystem layout and bootloader configuration
4. **User-Friendly** - Provides familiar Calamares interface for bootc installation
5. **Validation** - Real-time checking of OSTree and bootc requirements
6. **Extensibility** - Can be enhanced with additional features over time

This approach ensures that the Calamares module provides a complete, user-friendly interface for `bootc install` operations while properly handling all the OSTree-specific requirements for bootloader configuration, initramfs management, and user account creation.