waco 0.0.1

Command line tool to manage WildFly containers.
use crate::constants::{
    BOOTSTRAP_OPERATIONS_VARIABLE, LABEL_NAME, SERVERS_VARIABLE, WILDFLY_ADMIN_CONTAINER,
};
use crate::wildfly::ServerType::{DomainController, Standalone};
use crate::wildfly::{ContainerInstance, Ports, Server, ServerType};
use anyhow::{Context, Error, bail};
use futures::future::join_all;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::process::Command;
use which::which;
use wildfly_container_versions::WildFlyContainer;

pub async fn create_network() -> anyhow::Result<()> {
    let mut network_command = Command::new("podman");
    network_command
        .arg("network")
        .arg("create")
        .arg("--ignore")
        .arg(WILDFLY_ADMIN_CONTAINER);
    let network_child = network_command
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("Unable to run podman-network.");
    network_child.wait_with_output().await?;
    Ok(())
}

pub fn podman_run(name: &str, ports: Option<&Ports>, operations: Vec<String>) -> Command {
    let mut command = Command::new("podman");
    command
        .arg("run")
        .arg("--rm")
        .arg("--detach")
        .arg("--name")
        .arg(name);
    if let Some(ports) = ports {
        command
            .arg("--publish")
            .arg(format!("{}:8080", ports.http))
            .arg("--publish")
            .arg(format!("{}:9990", ports.management));
    }
    if !operations.is_empty() {
        command.arg("--env").arg(format!(
            "{}={}",
            BOOTSTRAP_OPERATIONS_VARIABLE,
            operations.join(",")
        ));
    }
    command
}

pub fn podman_stop(name: &str) -> Command {
    let mut command = Command::new("podman");
    command.arg("stop").arg(name);
    command
}

pub fn add_servers(mut command: Command, hostname: &str, servers: Vec<Server>) -> Command {
    if !servers.is_empty() {
        let server_ops = servers
            .iter()
            .map(|server| server.add_server_op(hostname))
            .collect::<Vec<String>>();
        command
            .arg("--env")
            .arg(format!("{}={}", SERVERS_VARIABLE, server_ops.join(",")));
    }
    command
}

pub async fn get_instance(
    wildfly_containers: Option<&Vec<WildFlyContainer>>,
    name: Option<&str>,
) -> anyhow::Result<ContainerInstance> {
    let instances = podman_ps(
        vec![Standalone, DomainController],
        wildfly_containers,
        name,
        true,
    )
    .await?;
    if instances.len() == 0 || instances.len() > 1 {
        let what = if instances.len() == 0 {
            "No container"
        } else {
            "Multiple containers"
        };
        let why = if let (Some(name), Some(wildfly_containers)) = (name, wildfly_containers) {
            format!(
                "for name '{}' and version '{}'",
                name,
                wildfly_containers
                    .iter()
                    .map(|x| x.short_version.as_str())
                    .collect::<Vec<&str>>()
                    .join(", ")
            )
        } else if let (Some(name), None) = (name, wildfly_containers) {
            format!("for name '{}'", name)
        } else if let (None, Some(wildfly_containers)) = (name, wildfly_containers) {
            format!(
                "for version '{}'",
                wildfly_containers
                    .iter()
                    .map(|x| x.short_version.as_str())
                    .collect::<Vec<&str>>()
                    .join(", ")
            )
        } else {
            "".to_string()
        };
        if instances.len() == 0 {}
        bail!("{} found {}", what, why)
    }
    let mci = &mut instances[0].clone();
    podman_ports(mci).await?;
    Ok(mci.clone())
}

pub async fn podman_ps(
    server_types: Vec<ServerType>,
    wildfly_containers: Option<&Vec<WildFlyContainer>>,
    name: Option<&str>,
    resolve_ports: bool,
) -> anyhow::Result<Vec<ContainerInstance>> {
    let mut instances: Vec<ContainerInstance> = vec![];
    let mut command = Command::new("podman");
    command
        .arg("ps")
        .arg("--filter")
        .arg(format!("label={}", LABEL_NAME))
        .arg("--format")
        .arg(format!(
            "{{{{.ID}}}}|{{{{index .Labels \"{}\"}}}}|{{{{.Names}}}}|{{{{.Status}}}}",
            LABEL_NAME
        ));
    let child = command
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;
    let output = child.wait_with_output().await?;
    let output = String::from_utf8(output.stdout)?;
    for line in output.lines() {
        let parts: Vec<&str> = line.split("|").collect();
        if parts.len() == 4 {
            let container_id = parts[0];
            let identifier = parts[1];
            let name = parts[2];
            let status = parts[3];
            if let Ok(instance) = ContainerInstance::new(identifier, container_id, name, status) {
                instances.push(instance);
            }
        }
    }

    instances.retain(|instance| {
        let server_type_match = server_types.contains(&instance.admin_container.server_type);
        let version_match = if let Some(versions) = &wildfly_containers {
            versions.contains(&instance.admin_container.wildfly_container)
        } else {
            true
        };
        let name_match = if let Some(name) = name {
            name == instance.name
        } else {
            true
        };
        server_type_match && version_match && name_match
    });

    if resolve_ports {
        let futures = instances.iter_mut().map(move |x| podman_ports(x));
        join_all(futures).await;
    }
    Ok(instances)
}

async fn podman_ports(container_instance: &mut ContainerInstance) -> anyhow::Result<()> {
    let mut command = Command::new("podman");
    command.arg("inspect")
            .arg("--format")
            .arg("{{ (index (index .NetworkSettings.Ports \"8080/tcp\") 0).HostPort }}|{{ (index (index .NetworkSettings.Ports \"9990/tcp\") 0).HostPort }}")
            .arg(container_instance.container_id.as_str());
    let child = command
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;
    let output = child.wait_with_output().await?;
    if output.status.success() {
        let output = String::from_utf8(output.stdout)?;
        let ports = output.trim().split("|").collect::<Vec<&str>>();
        if ports.len() == 2 {
            let http = ports[0].parse::<u16>()?;
            let management = ports[1].parse::<u16>()?;
            container_instance.ports = Some(Ports { http, management });
        }
    } else {
        container_instance.ports = None;
    }
    Ok(())
}

pub fn verify_podman() -> Result<PathBuf, Error> {
    which("podman").with_context(|| "podman not found")
}