pub fn detect_container_cli() -> Option<String> {
if let Ok(cli) = std::env::var("FAKECLOUD_CONTAINER_CLI") {
return if cli_available(&cli) { Some(cli) } else { None };
}
if cli_available("docker") {
Some("docker".to_string())
} else if cli_available("podman") {
Some("podman".to_string())
} else {
None
}
}
pub fn cli_available(cli: &str) -> bool {
std::process::Command::new(cli)
.arg("info")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn is_podman_binary(cli: &str) -> bool {
std::path::Path::new(cli)
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.contains("podman"))
.unwrap_or(false)
}
pub fn detect_bridge_gateway(cli: &str) -> Option<String> {
let output = std::process::Command::new(cli)
.args([
"network",
"inspect",
"bridge",
"--format",
"{{range .IPAM.Config}}{{.Gateway}}{{end}}",
])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let gateway = String::from_utf8_lossy(&output.stdout).trim().to_string();
if gateway.is_empty() || !gateway.contains('.') {
return None;
}
Some(gateway)
}
#[derive(Debug, Clone)]
pub struct HostNetworking {
pub host_alias: String,
pub add_host_arg: Option<String>,
pub sibling_host: String,
}
impl HostNetworking {
pub fn detect(cli: &str) -> Self {
let (host_alias, add_host_arg) = resolve_host_alias(cli);
let sibling_host = resolve_sibling_host(std::env::var("FAKECLOUD_IN_CONTAINER").ok());
Self {
host_alias,
add_host_arg,
sibling_host,
}
}
pub fn push_add_host_args(&self, argv: &mut Vec<String>) {
if let Some(arg) = &self.add_host_arg {
argv.push("--add-host".to_string());
argv.push(arg.clone());
}
}
}
pub fn resolve_host_alias(cli: &str) -> (String, Option<String>) {
if is_podman_binary(cli) {
("host.containers.internal".to_string(), None)
} else if cfg!(target_os = "linux") {
let ip = detect_bridge_gateway(cli).unwrap_or_else(|| "172.17.0.1".to_string());
(
"host.docker.internal".to_string(),
Some(format!("host.docker.internal:{ip}")),
)
} else {
(
"host.docker.internal".to_string(),
Some("host.docker.internal:host-gateway".to_string()),
)
}
}
pub fn resolve_sibling_host(env_value: Option<String>) -> String {
let in_container = env_value
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if in_container {
"host.docker.internal".to_string()
} else {
"127.0.0.1".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_podman_binary_matches_bare_name() {
assert!(is_podman_binary("podman"));
assert!(is_podman_binary("podman-remote"));
}
#[test]
fn is_podman_binary_matches_absolute_path() {
assert!(is_podman_binary("/opt/homebrew/bin/podman"));
assert!(is_podman_binary("/usr/local/bin/podman-remote"));
}
#[test]
fn is_podman_binary_rejects_docker() {
assert!(!is_podman_binary("docker"));
assert!(!is_podman_binary("/usr/local/bin/docker"));
assert!(!is_podman_binary("docker-credential-helper"));
}
#[test]
fn resolve_host_alias_podman_has_no_add_host() {
let (alias, add_host) = resolve_host_alias("podman");
assert_eq!(alias, "host.containers.internal");
assert_eq!(add_host, None);
let (alias, add_host) = resolve_host_alias("/opt/homebrew/bin/podman");
assert_eq!(alias, "host.containers.internal");
assert_eq!(add_host, None);
}
#[test]
fn resolve_host_alias_docker_emits_add_host() {
let (alias, add_host) = resolve_host_alias("docker");
assert_eq!(alias, "host.docker.internal");
assert!(add_host.is_some());
assert!(add_host.unwrap().starts_with("host.docker.internal:"));
}
#[test]
fn resolve_sibling_host_defaults_to_loopback() {
assert_eq!(resolve_sibling_host(None), "127.0.0.1");
assert_eq!(resolve_sibling_host(Some(String::new())), "127.0.0.1");
assert_eq!(resolve_sibling_host(Some("0".to_string())), "127.0.0.1");
assert_eq!(resolve_sibling_host(Some("false".to_string())), "127.0.0.1");
}
#[test]
fn resolve_sibling_host_uses_docker_internal_when_in_container() {
assert_eq!(
resolve_sibling_host(Some("1".to_string())),
"host.docker.internal"
);
assert_eq!(
resolve_sibling_host(Some("true".to_string())),
"host.docker.internal"
);
assert_eq!(
resolve_sibling_host(Some("TRUE".to_string())),
"host.docker.internal"
);
}
#[test]
fn push_add_host_args_noop_for_podman() {
let net = HostNetworking {
host_alias: "host.containers.internal".to_string(),
add_host_arg: None,
sibling_host: "127.0.0.1".to_string(),
};
let mut argv = vec!["create".to_string()];
net.push_add_host_args(&mut argv);
assert_eq!(argv, vec!["create".to_string()]);
}
#[test]
fn push_add_host_args_emits_for_docker() {
let net = HostNetworking {
host_alias: "host.docker.internal".to_string(),
add_host_arg: Some("host.docker.internal:host-gateway".to_string()),
sibling_host: "127.0.0.1".to_string(),
};
let mut argv = vec!["create".to_string()];
net.push_add_host_args(&mut argv);
assert_eq!(
argv,
vec![
"create".to_string(),
"--add-host".to_string(),
"host.docker.internal:host-gateway".to_string(),
]
);
}
}