podbox/quadlet_install/
preflight.rs1use anyhow::{Context, Result};
7
8use crate::config::Config;
9
10pub(crate) fn preflight_check(config: &Config) -> Result<()> {
12 let name = &config.container.name;
13
14 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 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 let is_running = crate::podman::query_state(name)
63 .map(|state| state == crate::podman::ContainerState::Running)
64 .unwrap_or(false);
65
66 if is_running {
68 println!(
69 " Note: container '{name}' is running. Skipping port conflict checks for upgrade."
70 );
71 return Ok(());
72 }
73
74 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(config)?;
90
91 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 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}