Skip to main content

podbox/config/
types.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::config::defaults::{
7    default_network_mode, default_package_manager, default_pull_retry, default_pull_retry_delay,
8    default_shell, default_true, is_default_gpu, is_default_host_exec, is_default_mounts,
9    is_default_packages, is_default_pkg_mgr, is_default_pull_retry, is_default_pull_retry_delay,
10    is_default_run, is_default_shell, is_empty_hashmap, is_false, is_true,
11};
12use crate::config::enums::{CapPreset, GpuMode, ImageSource, OnStop, PackageManager, XdgDirValue};
13use crate::config::expand_tilde;
14
15#[derive(Debug, Deserialize, Serialize, Clone)]
16pub struct ImageConfig {
17    pub base: String,
18    pub name: String,
19    #[serde(rename = "image", default)]
20    pub image_ref: Option<String>,
21    #[serde(
22        default = "default_pull_retry",
23        skip_serializing_if = "is_default_pull_retry"
24    )]
25    pub pull_retry: u32,
26    #[serde(
27        default = "default_pull_retry_delay",
28        skip_serializing_if = "is_default_pull_retry_delay"
29    )]
30    pub pull_retry_delay: String,
31    #[serde(default, skip_serializing_if = "is_default_packages")]
32    pub packages: PackageConfig,
33    #[serde(default, skip_serializing_if = "is_default_run")]
34    pub run: RunConfig,
35}
36
37impl ImageConfig {
38    pub fn source(&self) -> ImageSource {
39        match &self.image_ref {
40            Some(ref_str) => ImageSource::Prebuilt {
41                ref_str: ref_str.clone(),
42            },
43            None => ImageSource::Build {
44                base: self.base.clone(),
45            },
46        }
47    }
48}
49
50#[derive(Debug, Deserialize, Serialize, Clone)]
51pub struct PackageConfig {
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub install: Vec<String>,
54    #[serde(default, skip_serializing_if = "Vec::is_empty")]
55    pub remove: Vec<String>,
56    #[serde(
57        default = "default_package_manager",
58        skip_serializing_if = "is_default_pkg_mgr"
59    )]
60    pub manager: PackageManager,
61}
62
63impl Default for PackageConfig {
64    fn default() -> Self {
65        Self {
66            install: Vec::new(),
67            remove: Vec::new(),
68            manager: default_package_manager(),
69        }
70    }
71}
72
73#[derive(Debug, Deserialize, Serialize, Clone, Default)]
74pub struct RunConfig {
75    #[serde(default, skip_serializing_if = "Vec::is_empty")]
76    pub commands: Vec<String>,
77}
78
79#[derive(Debug, Deserialize, Serialize, Clone)]
80pub struct ContainerConfig {
81    pub name: String,
82    #[serde(deserialize_with = "deserialize_home")]
83    pub home: PathBuf,
84    #[serde(default = "default_shell", skip_serializing_if = "is_default_shell")]
85    pub shell: String,
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub memory: Option<String>,
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub cpus: Option<String>,
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub reload_cmd: Option<String>,
92    #[serde(default, skip_serializing_if = "is_default_mounts")]
93    pub mounts: MountConfig,
94    #[serde(default, skip_serializing_if = "is_empty_hashmap")]
95    pub env: HashMap<String, String>,
96}
97
98#[derive(Debug, Deserialize, Serialize, Clone, Default)]
99pub struct MountConfig {
100    #[serde(default, skip_serializing_if = "Vec::is_empty")]
101    pub extra: Vec<String>,
102}
103
104fn deserialize_home<'de, D>(deserializer: D) -> std::result::Result<PathBuf, D::Error>
105where
106    D: serde::Deserializer<'de>,
107{
108    let path = String::deserialize(deserializer)?;
109    Ok(expand_tilde(&path))
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(untagged)]
114pub enum HostExecEntry {
115    /// Simple path string (defaults: `filter = true`, `shim = true`).
116    Simple(String),
117    /// Detailed configuration table.
118    Detailed {
119        path: String,
120        #[serde(default = "default_true")]
121        filter: bool,
122        #[serde(default = "default_true")]
123        shim: bool,
124    },
125}
126
127impl HostExecEntry {
128    pub fn path(&self) -> &str {
129        match self {
130            Self::Simple(p) => p.as_str(),
131            Self::Detailed { path, .. } => path.as_str(),
132        }
133    }
134
135    pub fn filter_enabled(&self) -> bool {
136        match self {
137            Self::Simple(_) => true,
138            Self::Detailed { filter, .. } => *filter,
139        }
140    }
141
142    pub fn shim_enabled(&self) -> bool {
143        match self {
144            Self::Simple(_) => true,
145            Self::Detailed { shim, .. } => *shim,
146        }
147    }
148}
149
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151pub struct HostExecConfig {
152    #[serde(default)]
153    pub enabled: bool,
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub allowlist: Option<std::collections::HashMap<String, HostExecEntry>>,
156}
157
158impl HostExecConfig {
159    pub fn resolve(&self, cmd: &str) -> Option<&HostExecEntry> {
160        if !self.enabled {
161            return None;
162        }
163        match &self.allowlist {
164            Some(map) => map.get(cmd),
165            None => None,
166        }
167    }
168
169    /// List of alias names that should have PATH shims generated in the guest.
170    pub fn guest_shims(&self) -> Vec<String> {
171        if !self.enabled {
172            return Vec::new();
173        }
174        self.allowlist
175            .as_ref()
176            .map(|map| {
177                map.iter()
178                    .filter(|(_, entry)| entry.shim_enabled())
179                    .map(|(alias, _)| alias.clone())
180                    .collect()
181            })
182            .unwrap_or_default()
183    }
184}
185
186#[derive(Debug, Deserialize, Serialize, Clone)]
187pub struct IntegrationConfig {
188    #[serde(default = "default_true", skip_serializing_if = "is_true")]
189    pub wayland: bool,
190    #[serde(default = "default_true", skip_serializing_if = "is_true")]
191    pub audio: bool,
192    #[serde(default, skip_serializing_if = "is_default_gpu")]
193    pub gpu: GpuMode,
194    #[serde(default = "default_true", skip_serializing_if = "is_true")]
195    pub dbus: bool,
196    #[serde(default = "default_true", skip_serializing_if = "is_true")]
197    pub notify: bool,
198    #[serde(default = "default_true", skip_serializing_if = "is_true")]
199    pub xdg_open: bool,
200    #[serde(default = "default_true", skip_serializing_if = "is_true")]
201    pub clipboard: bool,
202    #[serde(default, skip_serializing_if = "is_default_host_exec")]
203    pub host_exec: HostExecConfig,
204    #[serde(default, skip_serializing_if = "is_false")]
205    pub ssh_agent: bool,
206    #[serde(default, skip_serializing_if = "is_false")]
207    pub gpg_agent: bool,
208    #[serde(default = "default_true", skip_serializing_if = "is_true")]
209    pub sync_fonts: bool,
210    #[serde(default = "default_true", skip_serializing_if = "is_true")]
211    pub sync_icons: bool,
212    #[serde(default = "default_true", skip_serializing_if = "is_true")]
213    pub sync_themes: bool,
214    #[serde(default)]
215    pub hardware: HardwareConfig,
216    #[serde(default)]
217    pub xdg_dirs: XdgDirConfig,
218    #[serde(default)]
219    pub export: ExportConfig,
220}
221
222impl Default for IntegrationConfig {
223    fn default() -> Self {
224        IntegrationConfig {
225            wayland: true,
226            audio: true,
227            gpu: GpuMode::Auto,
228            dbus: true,
229            notify: true,
230            xdg_open: true,
231            clipboard: true,
232            host_exec: HostExecConfig::default(),
233            ssh_agent: false,
234            gpg_agent: false,
235            sync_fonts: true,
236            sync_icons: true,
237            sync_themes: true,
238            hardware: HardwareConfig::default(),
239            xdg_dirs: XdgDirConfig::default(),
240            export: ExportConfig::default(),
241        }
242    }
243}
244
245#[derive(Debug, Deserialize, Serialize, Clone, Default)]
246pub struct XdgDirConfig {
247    #[serde(default)]
248    pub documents: XdgDirValue,
249    #[serde(default)]
250    pub downloads: XdgDirValue,
251    #[serde(default)]
252    pub pictures: XdgDirValue,
253    #[serde(default)]
254    pub music: XdgDirValue,
255    #[serde(default)]
256    pub videos: XdgDirValue,
257    #[serde(default)]
258    pub desktop: XdgDirValue,
259    #[serde(default)]
260    pub projects: XdgDirValue,
261}
262
263#[derive(Debug, Deserialize, Serialize, Clone, Default)]
264pub struct ExportConfig {
265    #[serde(default)]
266    pub apps: Vec<String>,
267    #[serde(default)]
268    pub bins: Vec<String>,
269}
270
271#[derive(Debug, Deserialize, Serialize, Clone)]
272pub struct LifecycleConfig {
273    #[serde(default)]
274    pub quadlet: bool,
275    #[serde(default)]
276    pub autostart: bool,
277    #[serde(default)]
278    pub on_stop: OnStop,
279    #[serde(default)]
280    pub auto_update: bool,
281    #[serde(default = "crate::config::defaults::default_idle_timeout")]
282    pub idle_timeout: String,
283}
284
285impl Default for LifecycleConfig {
286    fn default() -> Self {
287        LifecycleConfig {
288            quadlet: false,
289            autostart: false,
290            on_stop: OnStop::Keep,
291            auto_update: false,
292            idle_timeout: crate::config::defaults::default_idle_timeout(),
293        }
294    }
295}
296
297#[derive(Debug, Deserialize, Serialize, Clone, Default)]
298pub struct SystemdConfig {
299    #[serde(default)]
300    pub requires: Vec<String>,
301    #[serde(default)]
302    pub after: Vec<String>,
303}
304
305#[derive(Debug, Deserialize, Serialize, Clone)]
306pub struct WaylandConfig {
307    #[serde(default = "default_true", skip_serializing_if = "is_true")]
308    pub firewall: bool,
309    #[serde(default)]
310    pub blocked_interfaces: Vec<String>,
311}
312
313impl Default for WaylandConfig {
314    fn default() -> Self {
315        Self {
316            firewall: true,
317            blocked_interfaces: vec![
318                "zwlr_screencopy_manager_v1".into(),
319                "ext_image_copy_capture_v1".into(),
320                "ext_foreign_toplevel_list_v1".into(),
321                "zwlr_virtual_pointer_manager_v1".into(),
322                "zwlr_virtual_pointer_unstable_v1".into(),
323                "zwp_virtual_keyboard_manager_v1".into(),
324                "zwp_input_method_v1".into(),
325                "zwp_input_method_v2".into(),
326                "ext_input_method_v1".into(),
327                "org_kde_kwin_fake_input".into(),
328            ],
329        }
330    }
331}
332
333#[derive(Debug, Deserialize, Serialize, Clone, Default)]
334pub struct DbusConfig {
335    #[serde(default)]
336    pub preset: String,
337    #[serde(default)]
338    pub talk: Vec<String>,
339    #[serde(default)]
340    pub own: Vec<String>,
341}
342
343impl DbusConfig {
344    pub fn effective_talk(&self) -> Vec<String> {
345        let mut result = self.talk.clone();
346        if !self.preset.is_empty() && self.preset != "none" {
347            for svc in dbus_preset_talk(&self.preset) {
348                if !result.contains(&svc.to_string()) {
349                    result.push(svc.to_string());
350                }
351            }
352        }
353        result
354    }
355}
356
357pub fn dbus_preset_talk(preset: &str) -> &[&str] {
358    match preset {
359        "flatpak" => &["org.freedesktop.Flatpak", "org.freedesktop.Flatpak.*"],
360        "gnome" => &[
361            "org.gnome.Shell",
362            "org.gnome.Shell.*",
363            "org.gnome.ScreenSaver",
364            "org.gnome.Mutter.*",
365            "org.gnome.keyring.*",
366        ],
367        "kde" => &["org.kde.*"],
368        _ => &[],
369    }
370}
371
372#[derive(Debug, Deserialize, Serialize, Clone)]
373pub struct NetworkConfig {
374    #[serde(default = "default_network_mode")]
375    pub mode: String,
376    #[serde(default, skip_serializing_if = "Vec::is_empty")]
377    pub ports: Vec<String>,
378}
379
380impl Default for NetworkConfig {
381    fn default() -> Self {
382        Self {
383            mode: default_network_mode(),
384            ports: Vec::new(),
385        }
386    }
387}
388
389#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
390pub struct HardwareConfig {
391    #[serde(default, skip_serializing_if = "is_false")]
392    pub joystick: bool,
393    #[serde(default, skip_serializing_if = "is_false")]
394    pub webcam: bool,
395    #[serde(default, skip_serializing_if = "is_false")]
396    pub yubikey: bool,
397    #[serde(default, skip_serializing_if = "is_false")]
398    pub serial: bool,
399    #[serde(default, skip_serializing_if = "is_false")]
400    pub kvm: bool,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
404#[serde(untagged)]
405pub enum SecretEntry {
406    Simple(String),
407    Detailed {
408        name: String,
409        #[serde(default = "default_secret_type")]
410        #[serde(rename = "type")]
411        secret_type: SecretType,
412        #[serde(default, skip_serializing_if = "Option::is_none")]
413        target: Option<String>,
414        #[serde(default, skip_serializing_if = "Option::is_none")]
415        mode: Option<String>,
416        #[serde(default = "default_secret_source")]
417        source: SecretSource,
418    },
419}
420
421fn default_secret_type() -> SecretType {
422    SecretType::Env
423}
424fn default_secret_source() -> SecretSource {
425    SecretSource::Podman
426}
427
428#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
429#[serde(rename_all = "lowercase")]
430pub enum SecretType {
431    #[default]
432    Env,
433    Mount,
434}
435
436#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
437#[serde(rename_all = "lowercase")]
438pub enum SecretSource {
439    #[default]
440    Podman,
441    Systemd,
442}
443
444#[derive(Debug, Deserialize, Serialize, Clone)]
445pub struct SecurityConfig {
446    #[serde(default)]
447    pub apparmor: Option<String>,
448    #[serde(default)]
449    pub seccomp: Option<String>,
450    #[serde(default = "default_true", skip_serializing_if = "is_true")]
451    pub security_label_disable: bool,
452    #[serde(default = "default_true", skip_serializing_if = "is_true")]
453    pub no_new_privileges: bool,
454    #[serde(default, skip_serializing_if = "is_false")]
455    pub read_only_rootfs: bool,
456    #[serde(default, skip_serializing_if = "Option::is_none")]
457    pub userns: Option<String>,
458    #[serde(default, skip_serializing_if = "is_default_cap_preset")]
459    pub cap_preset: CapPreset,
460    #[serde(default, skip_serializing_if = "Vec::is_empty")]
461    pub cap_add: Vec<String>,
462    #[serde(default, skip_serializing_if = "Vec::is_empty")]
463    pub secrets: Vec<SecretEntry>,
464}
465
466fn is_default_cap_preset(v: &CapPreset) -> bool {
467    *v == CapPreset::Default
468}
469
470impl Default for SecurityConfig {
471    fn default() -> Self {
472        SecurityConfig {
473            apparmor: None,
474            seccomp: None,
475            security_label_disable: true,
476            no_new_privileges: true,
477            read_only_rootfs: false,
478            userns: None,
479            cap_preset: CapPreset::Default,
480            cap_add: Vec::new(),
481            secrets: Vec::new(),
482        }
483    }
484}