puu-installer 0.2.8

Standalone installer for bootc-based OSs
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) Opinsys Oy 2026

use anyhow::{Context, Result};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;

#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct Config {
    pub partitions: Partitions,
    pub labels: Labels,
    pub image: Image,
    pub identity: Identity,
    pub flatpak: Flatpak,
    pub k3s: K3s,
    pub homed: Homed,
    pub serial_console: HashMap<String, String>,
    pub kernel: Kernel,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Partitions {
    pub efi: String,
    pub root_min: String,
    pub home: String,
}

impl Default for Partitions {
    fn default() -> Self {
        Self {
            efi: "512M".into(),
            root_min: "64G".into(),
            home: "256G".into(),
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Labels {
    pub root: String,
    pub home: String,
}

impl Default for Labels {
    fn default() -> Self {
        Self {
            root: "root".into(),
            home: "home".into(),
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Image {
    pub target_img_ref: String,
    pub source_label: String,
    pub source_mount: String,
}

impl Default for Image {
    fn default() -> Self {
        Self {
            target_img_ref: "quay.io/puu/puu:latest".into(),
            source_label: "PAYLOAD".into(),
            source_mount: "/run/puu-installer/payload".into(),
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Identity {
    pub keymap: String,
    pub locale: String,
    pub timezone: String,
    pub hostname: String,
}

impl Default for Identity {
    fn default() -> Self {
        Self {
            keymap: "us".into(),
            locale: "C.UTF-8".into(),
            timezone: "UTC".into(),
            hostname: "puu".into(),
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Flatpak {
    pub manifest_path: String,
    pub sync_helper: String,
}

impl Default for Flatpak {
    fn default() -> Self {
        Self {
            manifest_path: "/usr/share/puu/flatpaks.list".into(),
            sync_helper: "/usr/libexec/puu/flatpak-sync".into(),
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct K3s {
    pub helper: String,
    pub role: String,
    pub server_url: String,
    pub node_name: String,
}

impl Default for K3s {
    fn default() -> Self {
        Self {
            helper: "/usr/libexec/puu/configure-k3s-cluster".into(),
            role: "server".into(),
            server_url: String::new(),
            node_name: String::new(),
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Homed {
    pub groups: Vec<String>,
}

impl Default for Homed {
    fn default() -> Self {
        Self {
            groups: vec![
                "adm".into(),
                "audio".into(),
                "cdrom".into(),
                "dip".into(),
                "input".into(),
                "lpadmin".into(),
                "netdev".into(),
                "plugdev".into(),
                "render".into(),
                "sudo".into(),
                "video".into(),
                "wheel".into(),
                "realtime".into(),
                "uucp".into(),
            ],
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Kernel {
    pub extra_arm64_args: Vec<String>,
}

impl Default for Kernel {
    fn default() -> Self {
        Self {
            extra_arm64_args: vec!["cma=256M".into()],
        }
    }
}

impl Config {
    pub fn load(path: &Path) -> Result<Self> {
        let text = std::fs::read_to_string(path)
            .with_context(|| format!("failed to read config {}", path.display()))?;
        let cfg: Config = serde_norway::from_str(&text)
            .with_context(|| format!("failed to parse config {}", path.display()))?;
        Ok(cfg)
    }
}