use anyhow::{Context, Result};
use crate::config::Config;
pub(crate) fn preflight_check(config: &Config) -> Result<()> {
let name = &config.container.name;
if !config.container.home.exists() {
eprintln!(
" Note: home directory '{}' will be created (does not exist yet).",
config.container.home.display()
);
}
for mount in &config.container.mounts.extra {
let host_path = match mount.split_once(':') {
Some((host, _)) => host,
None => mount,
};
let path = std::path::Path::new(host_path);
if !path.exists() {
if crate::codegen::distros::is_tty() {
let prompt = format!(
"Mount path '{}' does not exist on the host. Create it?",
path.display()
);
let create =
dialoguer::Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
.with_prompt(prompt)
.default(true)
.interact_opt()?;
if create == Some(true) {
std::fs::create_dir_all(path).with_context(|| {
format!("failed to create mount directory '{}'", path.display())
})?;
println!("✓ Directory '{}' created.", path.display());
} else {
eprintln!(
"Warning: mount path '{}' does not exist on the host. This may cause the container to fail to load.",
path.display()
);
}
} else {
eprintln!(
"Warning: mount path '{}' does not exist on the host (container '{}').",
path.display(),
name
);
}
}
}
let is_running = crate::podman::query_state(name)
.map(|state| state == crate::podman::ContainerState::Running)
.unwrap_or(false);
if is_running {
println!(
" Note: container '{name}' is running. Skipping port conflict checks for upgrade."
);
return Ok(());
}
let conflicts = crate::ports::check_host_ports(&config.network.ports);
if !conflicts.is_empty() {
let listed = conflicts
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
anyhow::bail!(
"Port conflict: already in use on the host — {listed}. \
Find the process with: `ss -ltnp 'sport = :<port>'`"
);
}
check_declared_secrets(config)?;
if config.security.cap_preset == crate::config::CapPreset::Admin {
if crate::codegen::distros::is_tty() {
let caps = config.security.cap_preset.caps().join(", ");
let confirmed = dialoguer::Confirm::with_theme(
&dialoguer::theme::ColorfulTheme::default(),
)
.with_prompt(format!(
"WARNING: CapPreset::Admin grants {caps}. Only proceed if you fully trust this container. Continue?"
))
.default(false)
.interact()?;
if !confirmed {
anyhow::bail!(
"Aborted — set cap_preset to a lower level or use cap_add for specific caps"
);
}
} else {
let caps = config.security.cap_preset.caps().join(", ");
eprintln!(
"Note: cap_preset = \"admin\" grants {caps}. Non-interactive mode, continuing without confirmation."
);
}
}
Ok(())
}
pub(crate) fn check_declared_secrets(config: &Config) -> Result<()> {
use std::collections::HashSet;
use crate::config::{SecretEntry, SecretSource};
if config.security.secrets.is_empty() {
return Ok(());
}
let output = std::process::Command::new("podman")
.args(["secret", "ls", "--format", "{{.Name}}"])
.output();
let available: HashSet<String> = match output {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
.lines()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect(),
Ok(o) => {
eprintln!(
"Warning: `podman secret ls` failed ({}): {}",
o.status,
String::from_utf8_lossy(&o.stderr).trim()
);
HashSet::new()
}
Err(e) => {
eprintln!("Warning: failed to run `podman secret ls`: {e}");
HashSet::new()
}
};
for secret in &config.security.secrets {
let (name, source) = match secret {
SecretEntry::Simple(n) => (n.as_str(), SecretSource::Podman),
SecretEntry::Detailed { name, source, .. } => (name.as_str(), *source),
};
if source == SecretSource::Podman && !available.contains(name) {
anyhow::bail!(
"Required secret '{name}' not found in Podman secret store.\n\
Create it with: `podman secret create {name} -`"
);
}
}
Ok(())
}