Skip to main content

podbox/quadlet_install/
preflight.rs

1//! Pre-install validation: mount paths, port conflicts, and the admin
2//! capability-preset confirmation.
3//!
4//! Extracted verbatim from `quadlet_install.rs`.
5
6use anyhow::{Context, Result};
7
8use crate::config::Config;
9
10/// Validate that mount paths referenced in extra mounts exist on the host.
11pub(crate) fn preflight_check(config: &Config) -> Result<()> {
12    let name = &config.container.name;
13
14    // Check home directory
15    if !config.container.home.exists() {
16        eprintln!(
17            "  Note: home directory '{}' will be created (does not exist yet).",
18            config.container.home.display()
19        );
20    }
21
22    // Parse extra mounts and check host paths
23    for mount in &config.container.mounts.extra {
24        let host_path = match mount.split_once(':') {
25            Some((host, _)) => host,
26            None => mount,
27        };
28        let path = std::path::Path::new(host_path);
29        if !path.exists() {
30            if crate::codegen::distros::is_tty() {
31                let prompt = format!(
32                    "Mount path '{}' does not exist on the host. Create it?",
33                    path.display()
34                );
35                let create =
36                    dialoguer::Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
37                        .with_prompt(prompt)
38                        .default(true)
39                        .interact_opt()?;
40                if create == Some(true) {
41                    std::fs::create_dir_all(path).with_context(|| {
42                        format!("failed to create mount directory '{}'", path.display())
43                    })?;
44                    println!("✓ Directory '{}' created.", path.display());
45                } else {
46                    eprintln!(
47                        "Warning: mount path '{}' does not exist on the host. This may cause the container to fail to load.",
48                        path.display()
49                    );
50                }
51            } else {
52                eprintln!(
53                    "Warning: mount path '{}' does not exist on the host (container '{}').",
54                    path.display(),
55                    name
56                );
57            }
58        }
59    }
60
61    // Intelligently check if container is running
62    let is_running = crate::podman::query_state(name)
63        .map(|state| state == crate::podman::ContainerState::Running)
64        .unwrap_or(false);
65
66    // Only run port bind tests if the container is stopped
67    if is_running {
68        println!(
69            "  Note: container '{name}' is running. Skipping port conflict checks for upgrade."
70        );
71        return Ok(());
72    }
73
74    // Check for port conflicts (IPv4 + IPv6, TCP + UDP)
75    let conflicts = crate::ports::check_host_ports(&config.network.ports);
76    if !conflicts.is_empty() {
77        let listed = conflicts
78            .iter()
79            .map(ToString::to_string)
80            .collect::<Vec<_>>()
81            .join(", ");
82        anyhow::bail!(
83            "Port conflict: already in use on the host — {listed}. \
84             Find the process with: `ss -ltnp 'sport = :<port>'`"
85        );
86    }
87
88    // Check declared secrets exist in podman store
89    check_declared_secrets(config)?;
90
91    // Check admin cap_preset
92    if config.security.cap_preset == crate::config::CapPreset::Admin {
93        if crate::codegen::distros::is_tty() {
94            let caps = config.security.cap_preset.caps().join(", ");
95            let confirmed = dialoguer::Confirm::with_theme(
96                &dialoguer::theme::ColorfulTheme::default(),
97            )
98            .with_prompt(format!(
99                "WARNING: CapPreset::Admin grants {caps}. Only proceed if you fully trust this container. Continue?"
100            ))
101            .default(false)
102            .interact()?;
103            if !confirmed {
104                anyhow::bail!(
105                    "Aborted — set cap_preset to a lower level or use cap_add for specific caps"
106                );
107            }
108        } else {
109            let caps = config.security.cap_preset.caps().join(", ");
110            eprintln!(
111                "Note: cap_preset = \"admin\" grants {caps}. Non-interactive mode, continuing without confirmation."
112            );
113        }
114    }
115
116    Ok(())
117}
118
119pub(crate) fn check_declared_secrets(config: &Config) -> Result<()> {
120    use std::collections::HashSet;
121
122    use crate::config::{SecretEntry, SecretSource};
123
124    if config.security.secrets.is_empty() {
125        return Ok(());
126    }
127
128    let output = std::process::Command::new("podman")
129        .args(["secret", "ls", "--format", "{{.Name}}"])
130        .output();
131
132    let available: HashSet<String> = match output {
133        Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
134            .lines()
135            .map(str::trim)
136            .filter(|s| !s.is_empty())
137            .map(String::from)
138            .collect(),
139        Ok(o) => {
140            // podman secret ls failed — treat as empty and warn
141            eprintln!(
142                "Warning: `podman secret ls` failed ({}): {}",
143                o.status,
144                String::from_utf8_lossy(&o.stderr).trim()
145            );
146            HashSet::new()
147        }
148        Err(e) => {
149            eprintln!("Warning: failed to run `podman secret ls`: {e}");
150            HashSet::new()
151        }
152    };
153
154    for secret in &config.security.secrets {
155        let (name, source) = match secret {
156            SecretEntry::Simple(n) => (n.as_str(), SecretSource::Podman),
157            SecretEntry::Detailed { name, source, .. } => (name.as_str(), *source),
158        };
159        if source == SecretSource::Podman && !available.contains(name) {
160            anyhow::bail!(
161                "Required secret '{name}' not found in Podman secret store.\n\
162                 Create it with: `podman secret create {name} -`"
163            );
164        }
165    }
166    Ok(())
167}