podbox-cli 0.6.7

Declarative Podman-native container environment manager. Define an environment as a TOML file and let systemd own its lifecycle.
Documentation
use std::collections::HashMap;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

use crate::config::defaults::{
    default_network_mode, default_package_manager, default_pull_retry, default_pull_retry_delay,
    default_shell, default_true, is_default_gpu, is_default_host_exec, is_default_mounts,
    is_default_packages, is_default_pkg_mgr, is_default_pull_retry, is_default_pull_retry_delay,
    is_default_run, is_default_shell, is_empty_hashmap, is_false, is_true,
};
use crate::config::enums::{CapPreset, GpuMode, ImageSource, OnStop, PackageManager, XdgDirValue};
use crate::config::expand_tilde;

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ImageConfig {
    pub base: String,
    pub name: String,
    #[serde(rename = "image", default)]
    pub image_ref: Option<String>,
    #[serde(
        default = "default_pull_retry",
        skip_serializing_if = "is_default_pull_retry"
    )]
    pub pull_retry: u32,
    #[serde(
        default = "default_pull_retry_delay",
        skip_serializing_if = "is_default_pull_retry_delay"
    )]
    pub pull_retry_delay: String,
    #[serde(default, skip_serializing_if = "is_default_packages")]
    pub packages: PackageConfig,
    #[serde(default, skip_serializing_if = "is_default_run")]
    pub run: RunConfig,
}

impl ImageConfig {
    pub fn source(&self) -> ImageSource {
        match &self.image_ref {
            Some(ref_str) => ImageSource::Prebuilt {
                ref_str: ref_str.clone(),
            },
            None => ImageSource::Build {
                base: self.base.clone(),
            },
        }
    }
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PackageConfig {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub install: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub remove: Vec<String>,
    #[serde(
        default = "default_package_manager",
        skip_serializing_if = "is_default_pkg_mgr"
    )]
    pub manager: PackageManager,
}

impl Default for PackageConfig {
    fn default() -> Self {
        Self {
            install: Vec::new(),
            remove: Vec::new(),
            manager: default_package_manager(),
        }
    }
}

#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct RunConfig {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub commands: Vec<String>,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ContainerConfig {
    pub name: String,
    #[serde(deserialize_with = "deserialize_home")]
    pub home: PathBuf,
    #[serde(default = "default_shell", skip_serializing_if = "is_default_shell")]
    pub shell: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub memory: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cpus: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reload_cmd: Option<String>,
    #[serde(default, skip_serializing_if = "is_default_mounts")]
    pub mounts: MountConfig,
    #[serde(default, skip_serializing_if = "is_empty_hashmap")]
    pub env: HashMap<String, String>,
}

#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct MountConfig {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub extra: Vec<String>,
}

fn deserialize_home<'de, D>(deserializer: D) -> std::result::Result<PathBuf, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let path = String::deserialize(deserializer)?;
    Ok(expand_tilde(&path))
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HostExecConfig {
    #[serde(default)]
    pub enabled: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub allowlist: Option<std::collections::HashMap<String, String>>,
}

impl HostExecConfig {
    pub fn resolve<'a>(&'a self, cmd: &'a str) -> Option<&'a str> {
        if !self.enabled {
            return None;
        }
        match &self.allowlist {
            Some(map) => map.get(cmd).map(|s| s.as_str()),
            None => Some(cmd),
        }
    }
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct IntegrationConfig {
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub wayland: bool,
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub audio: bool,
    #[serde(default, skip_serializing_if = "is_default_gpu")]
    pub gpu: GpuMode,
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub dbus: bool,
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub notify: bool,
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub xdg_open: bool,
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub clipboard: bool,
    #[serde(default, skip_serializing_if = "is_default_host_exec")]
    pub host_exec: HostExecConfig,
    #[serde(default, skip_serializing_if = "is_false")]
    pub ssh_agent: bool,
    #[serde(default, skip_serializing_if = "is_false")]
    pub gpg_agent: bool,
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub sync_fonts: bool,
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub sync_icons: bool,
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub sync_themes: bool,
    #[serde(default)]
    pub xdg_dirs: XdgDirConfig,
    #[serde(default)]
    pub export: ExportConfig,
}

impl Default for IntegrationConfig {
    fn default() -> Self {
        IntegrationConfig {
            wayland: true,
            audio: true,
            gpu: GpuMode::Auto,
            dbus: true,
            notify: true,
            xdg_open: true,
            clipboard: true,
            host_exec: HostExecConfig::default(),
            ssh_agent: false,
            gpg_agent: false,
            sync_fonts: true,
            sync_icons: true,
            sync_themes: true,
            xdg_dirs: XdgDirConfig::default(),
            export: ExportConfig::default(),
        }
    }
}

#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct XdgDirConfig {
    #[serde(default)]
    pub documents: XdgDirValue,
    #[serde(default)]
    pub downloads: XdgDirValue,
    #[serde(default)]
    pub pictures: XdgDirValue,
    #[serde(default)]
    pub music: XdgDirValue,
    #[serde(default)]
    pub videos: XdgDirValue,
    #[serde(default)]
    pub desktop: XdgDirValue,
    #[serde(default)]
    pub projects: XdgDirValue,
}

#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct ExportConfig {
    #[serde(default)]
    pub apps: Vec<String>,
    #[serde(default)]
    pub bins: Vec<String>,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct LifecycleConfig {
    #[serde(default)]
    pub quadlet: bool,
    #[serde(default)]
    pub autostart: bool,
    #[serde(default)]
    pub on_stop: OnStop,
    #[serde(default)]
    pub auto_update: bool,
    #[serde(default = "crate::config::defaults::default_idle_timeout")]
    pub idle_timeout: String,
}

impl Default for LifecycleConfig {
    fn default() -> Self {
        LifecycleConfig {
            quadlet: false,
            autostart: false,
            on_stop: OnStop::Keep,
            auto_update: false,
            idle_timeout: crate::config::defaults::default_idle_timeout(),
        }
    }
}

#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct SystemdConfig {
    #[serde(default)]
    pub requires: Vec<String>,
    #[serde(default)]
    pub after: Vec<String>,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct WaylandConfig {
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub firewall: bool,
    #[serde(default)]
    pub blocked_interfaces: Vec<String>,
}

impl Default for WaylandConfig {
    fn default() -> Self {
        Self {
            firewall: true,
            blocked_interfaces: vec![
                "zwlr_screencopy_manager_v1".into(),
                "ext_image_copy_capture_v1".into(),
                "ext_foreign_toplevel_list_v1".into(),
                "zwlr_virtual_pointer_manager_v1".into(),
                "zwlr_virtual_pointer_unstable_v1".into(),
                "zwp_virtual_keyboard_manager_v1".into(),
                "zwp_input_method_v1".into(),
                "zwp_input_method_v2".into(),
                "ext_input_method_v1".into(),
                "org_kde_kwin_fake_input".into(),
            ],
        }
    }
}

#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct DbusConfig {
    #[serde(default)]
    pub preset: String,
    #[serde(default)]
    pub talk: Vec<String>,
    #[serde(default)]
    pub own: Vec<String>,
}

impl DbusConfig {
    pub fn effective_talk(&self) -> Vec<String> {
        let mut result = self.talk.clone();
        if !self.preset.is_empty() && self.preset != "none" {
            for svc in dbus_preset_talk(&self.preset) {
                if !result.contains(&svc.to_string()) {
                    result.push(svc.to_string());
                }
            }
        }
        result
    }
}

pub fn dbus_preset_talk(preset: &str) -> &[&str] {
    match preset {
        "flatpak" => &["org.freedesktop.Flatpak", "org.freedesktop.Flatpak.*"],
        "gnome" => &[
            "org.gnome.Shell",
            "org.gnome.Shell.*",
            "org.gnome.ScreenSaver",
            "org.gnome.Mutter.*",
            "org.gnome.keyring.*",
        ],
        "kde" => &["org.kde.*"],
        _ => &[],
    }
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct NetworkConfig {
    #[serde(default = "default_network_mode")]
    pub mode: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub ports: Vec<String>,
}

impl Default for NetworkConfig {
    fn default() -> Self {
        Self {
            mode: default_network_mode(),
            ports: Vec::new(),
        }
    }
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct SecurityConfig {
    #[serde(default)]
    pub apparmor: Option<String>,
    #[serde(default)]
    pub seccomp: Option<String>,
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub security_label_disable: bool,
    #[serde(default = "default_true", skip_serializing_if = "is_true")]
    pub no_new_privileges: bool,
    #[serde(default, skip_serializing_if = "is_false")]
    pub read_only_rootfs: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub userns: Option<String>,
    #[serde(default, skip_serializing_if = "is_default_cap_preset")]
    pub cap_preset: CapPreset,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub cap_add: Vec<String>,
}

fn is_default_cap_preset(v: &CapPreset) -> bool {
    *v == CapPreset::Default
}

impl Default for SecurityConfig {
    fn default() -> Self {
        SecurityConfig {
            apparmor: None,
            seccomp: None,
            security_label_disable: true,
            no_new_privileges: true,
            read_only_rootfs: false,
            userns: None,
            cap_preset: CapPreset::Default,
            cap_add: Vec::new(),
        }
    }
}