use crate::label::Label;
use crate::neo4j::{Neo4JContainer, Neo4JImage, RunningNeo4JContainer};
use crate::progress::Progress;
use crate::registry::{images_registry, packs_registry};
use anyhow::{Error, bail};
use std::collections::HashSet;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use tokio::process::Command;
use tokio::time::sleep;
use which::which;
use wildfly_meta::{MetaItem, parse_meta_item};
pub fn verify_container_command() -> Result<PathBuf, Error> {
which("podman")
.or_else(|_| which("docker"))
.map_err(|_| anyhow::anyhow!("podman or docker not found"))
}
pub fn container_command() -> anyhow::Result<Command> {
if let Ok(podman_path) = which("podman") {
Ok(Command::new(podman_path))
} else if let Ok(docker_path) = which("docker") {
Ok(Command::new(docker_path))
} else {
bail!("podman or docker not found")
}
}
fn is_podman() -> bool {
which("podman").is_ok()
}
pub fn network_name(item: &MetaItem) -> String {
format!("mgt-network-{}", item.container_name())
}
pub async fn create_network(name: &str) -> anyhow::Result<()> {
let mut cmd = container_command()?;
cmd.arg("network").arg("create");
if is_podman() {
cmd.arg("--ignore");
}
cmd.arg(name).stdout(Stdio::piped()).stderr(Stdio::piped());
let output = cmd.output().await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.contains("already exists") {
bail!("Failed to create network {}: {}", name, stderr);
}
}
Ok(())
}
pub async fn run_container_cmd(args: &[&str], error_context: &str) -> anyhow::Result<()> {
let mut cmd = container_command()?;
for arg in args {
cmd.arg(arg);
}
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
let output = cmd.output().await?;
if !output.status.success() {
bail!(
"{}: {}",
error_context,
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
pub async fn remove_network(name: &str) -> anyhow::Result<()> {
run_container_cmd(
&["network", "rm", name],
&format!("Failed to remove network {name}"),
)
.await
}
pub async fn stop_container(name: &str) -> anyhow::Result<()> {
run_container_cmd(&["stop", name], &format!("Failed to stop {name}")).await
}
pub async fn remove_volume(name: &str) -> anyhow::Result<()> {
run_container_cmd(
&["volume", "rm", name],
&format!("Failed to remove volume {name}"),
)
.await
}
pub async fn remove_container(name: &str) -> anyhow::Result<()> {
run_container_cmd(&["rm", name], &format!("Failed to remove container {name}")).await
}
pub async fn running_neo4j_containers() -> anyhow::Result<Vec<RunningNeo4JContainer>> {
let source_name_label = Label::SourceName;
let mut cmd = container_command()?;
cmd.arg("ps")
.arg("--filter")
.arg(Label::Identifier.filter())
.arg("--format")
.arg(format!(
"{{{{.ID}}}}|{{{{.Names}}}}|{{{{.Status}}}}|{}",
source_name_label.format_expr()
))
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = cmd.output().await?;
if !output.status.success() {
bail!(
"Failed to list containers: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let mut containers: Vec<RunningNeo4JContainer> = String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|l| !l.is_empty())
.filter_map(|line| {
let parts: Vec<&str> = line.splitn(4, '|').collect();
if parts.len() == 4 {
let name = source_name_label.parse_value(parts[3])?;
let item = parse_meta_item(&name, images_registry(), packs_registry()).ok()?;
let image = Neo4JImage::new(&item);
let container = Neo4JContainer::new(image);
Some(RunningNeo4JContainer {
container,
id: parts[0].to_string(),
status: parts[2].to_string(),
})
} else {
None
}
})
.collect();
containers.sort_by(|a, b| {
a.container
.image
.item
.port_offset()
.cmp(&b.container.image.item.port_offset())
});
Ok(containers)
}
pub async fn local_image_names() -> anyhow::Result<HashSet<String>> {
let mut cmd = container_command()?;
cmd.arg("images")
.arg("--format")
.arg("{{.Repository}}:{{.Tag}}")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = cmd.output().await?;
if !output.status.success() {
bail!(
"Failed to list images: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.map(String::from)
.collect())
}
pub async fn pull_image(image: &str, progress: &Progress) -> anyhow::Result<()> {
progress.show_progress(&format!("Checking {}...", image));
if local_image_names().await?.contains(image) {
progress.show_progress(&format!("{} already available", image));
return Ok(());
}
progress.show_progress(&format!("Pulling {}...", image));
let mut cmd = container_command()?;
cmd.arg("pull")
.arg(image)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = cmd.output().await?;
if !output.status.success() {
bail!(
"Failed to pull image {}: {}",
image,
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
const MAX_HEALTHCHECK_ATTEMPTS: u32 = 120;
pub async fn healthcheck(url: &str, progress: &Progress) -> anyhow::Result<()> {
let client = reqwest::Client::new();
for attempt in 1..=MAX_HEALTHCHECK_ATTEMPTS {
progress.show_progress(&format!(
"Healthcheck {}/{}",
attempt, MAX_HEALTHCHECK_ATTEMPTS
));
if let Ok(response) = client.get(url).send().await
&& response.status().is_success()
{
return Ok(());
}
sleep(Duration::from_secs(1)).await;
}
bail!(
"Healthcheck failed after {} attempts: {}",
MAX_HEALTHCHECK_ATTEMPTS,
url
)
}