use crate::platform::config::RustployConfig;
use crate::platform::{event_bus::EventBus, log_sink::LogSink};
use anyhow::{Result, anyhow};
use bollard::{Docker, volume::CreateVolumeOptions};
use chrono::Utc;
use shared::Event;
use std::path::Path;
use std::sync::Arc;
use tokio::fs::{File, remove_file};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tracing::info;
pub(super) const PROJECT_NET_ALIAS: &str = "rp_project_net";
pub(super) async fn registry_login(token: &str) -> Result<()> {
let port = RustployConfig::global().registry.port;
let mut child = crate::engine::cli()
.args([
"login",
&format!("127.0.0.1:{port}"),
"-u",
"rp-internal",
"--password-stdin",
])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| anyhow!("falha ao iniciar docker login: {e}"))?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(token.as_bytes()).await.ok();
stdin.shutdown().await.ok();
}
let output = child
.wait_with_output()
.await
.map_err(|e| anyhow!("falha ao aguardar docker login: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("docker login falhou: {stderr}"));
}
Ok(())
}
pub(super) async fn registry_logout() -> Result<()> {
let port = RustployConfig::global().registry.port;
let status = crate::engine::cli()
.args(["logout", &format!("127.0.0.1:{port}")])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.map_err(|e| anyhow!("falha ao iniciar docker logout: {e}"))?;
if !status.success() {
return Err(anyhow!("docker logout falhou"));
}
Ok(())
}
pub fn inject_project_network(content: &str, network_name: &str) -> Result<String> {
use serde_yaml::Value;
let mut doc: Value =
serde_yaml::from_str(content).map_err(|e| anyhow!("compose YAML inválido: {e}"))?;
if doc.is_null() {
doc = Value::Mapping(serde_yaml::Mapping::new());
}
let root = doc
.as_mapping_mut()
.ok_or_else(|| anyhow!("compose YAML não é um mapping no nível raiz"))?;
let alias_key = Value::String(PROJECT_NET_ALIAS.to_string());
let networks_key = Value::String("networks".to_string());
if !root.contains_key(&networks_key) {
root.insert(
networks_key.clone(),
Value::Mapping(serde_yaml::Mapping::new()),
);
}
let networks = root
.get_mut(&networks_key)
.and_then(Value::as_mapping_mut)
.ok_or_else(|| anyhow!("`networks:` no compose não é um mapping"))?;
let already_present = networks.contains_key(&alias_key)
|| networks.values().any(|v| {
v.as_mapping()
.and_then(|m| m.get(Value::String("name".to_string())))
.and_then(Value::as_str)
== Some(network_name)
});
if !already_present {
let mut entry = serde_yaml::Mapping::new();
entry.insert(Value::String("external".to_string()), Value::Bool(true));
entry.insert(
Value::String("name".to_string()),
Value::String(network_name.to_string()),
);
networks.insert(alias_key.clone(), Value::Mapping(entry));
}
let services_key = Value::String("services".to_string());
if let Some(services_val) = root.get_mut(&services_key) {
let services = services_val
.as_mapping_mut()
.ok_or_else(|| anyhow!("`services:` no compose não é um mapping"))?;
let svc_net_key = Value::String("networks".to_string());
for (_, svc) in services.iter_mut() {
let Some(svc_map) = svc.as_mapping_mut() else {
continue;
};
match svc_map.get_mut(&svc_net_key) {
Some(Value::Sequence(seq)) => {
if !seq.iter().any(|v| v.as_str() == Some(PROJECT_NET_ALIAS)) {
seq.push(Value::String(PROJECT_NET_ALIAS.to_string()));
}
}
Some(Value::Mapping(map)) => {
if !map.contains_key(&alias_key) {
map.insert(
alias_key.clone(),
Value::Mapping(serde_yaml::Mapping::new()),
);
}
}
Some(other) if other.is_null() => {
*other = Value::Sequence(vec![Value::String(PROJECT_NET_ALIAS.to_string())]);
}
Some(_) => {}
None => {
svc_map.insert(
svc_net_key.clone(),
Value::Sequence(vec![Value::String(PROJECT_NET_ALIAS.to_string())]),
);
}
}
}
}
serde_yaml::to_string(&doc).map_err(|e| anyhow!("falha ao serializar compose YAML: {e}"))
}
pub async fn ensure_external_volumes(docker: &Docker, content: &str) -> Result<()> {
use serde_yaml::Value;
let doc: Value =
serde_yaml::from_str(content).map_err(|e| anyhow!("compose YAML inválido: {e}"))?;
let Some(volumes) = doc.get("volumes").and_then(Value::as_mapping) else {
return Ok(());
};
for (key, def) in volumes {
let Some(map) = def.as_mapping() else {
continue;
};
let is_external = map
.get(Value::String("external".to_string()))
.and_then(Value::as_bool)
.unwrap_or(false);
if !is_external {
continue;
}
let name = map
.get(Value::String("name".to_string()))
.and_then(Value::as_str)
.map(str::to_string)
.or_else(|| key.as_str().map(str::to_string));
let Some(name) = name else {
continue;
};
if docker.inspect_volume(&name).await.is_ok() {
info!(volume = %name, "compose::ensure_external_volumes: volume já existe");
continue;
}
info!(volume = %name, "compose::ensure_external_volumes: criando volume externo ausente");
docker
.create_volume(CreateVolumeOptions {
name: name.clone(),
..Default::default()
})
.await?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn up(
docker: &Docker,
content: &str,
project_name: &str,
service_id: &str,
deployment_id: &str,
network_name: &str,
bus: &Arc<EventBus>,
db: &Arc<impl LogSink>,
env_vars: &[(String, String)],
build_dir: &Path,
registry_internal_token: Option<Arc<str>>,
) -> Result<()> {
info!(project = %project_name, "compose_up: iniciando docker compose up");
let content = inject_project_network(content, network_name)?;
ensure_external_volumes(docker, &content).await?;
tokio::fs::create_dir_all(build_dir).await?;
let env_file_path = build_dir.join(".env");
let compose_file_path = build_dir.join("docker-compose.yml");
{
let mut env_file = File::create(&env_file_path).await?;
for (k, v) in env_vars {
env_file
.write_all(format!("{}={}\n", k, v).as_bytes())
.await?;
}
env_file.flush().await?;
}
let mut compose_file = File::create(&compose_file_path).await?;
compose_file.write_all(content.as_bytes()).await?;
compose_file.flush().await?;
drop(compose_file);
if let Some(token) = ®istry_internal_token
&& let Err(e) = registry_login(token).await
{
tracing::warn!(error = %e, project = %project_name, "compose_up: falha ao autenticar no registry embutido, pull vai falhar se a imagem for de lá");
}
let mut child = crate::engine::cli()
.args([
"compose",
"-p",
project_name,
"-f",
"docker-compose.yml",
"--env-file",
".env",
"up",
"-d",
"--build",
"--remove-orphans",
])
.current_dir(build_dir)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()
.map_err(|e| anyhow!("falha ao iniciar docker compose: {e}"))?;
let stdout = BufReader::new(child.stdout.take().unwrap());
let stderr = BufReader::new(child.stderr.take().unwrap());
let bus_s = bus.clone();
let db_s = db.clone();
let sid = service_id.to_string();
let did = deployment_id.to_string();
let read_stdout = async move {
let mut lines = stdout.lines();
while let Ok(Some(line)) = lines.next_line().await {
if line.trim().is_empty() {
continue;
}
let ts = Utc::now();
bus_s.publish(Event::BuildLog {
deployment_id: did.clone(),
service_id: sid.clone(),
line: line.clone(),
timestamp: ts,
});
let _ = db_s.append_build_line(&did, &line, ts).await;
}
};
let bus_e = bus.clone();
let db_e = db.clone();
let sid_e = service_id.to_string();
let did_e = deployment_id.to_string();
let read_stderr = async move {
let mut lines = stderr.lines();
while let Ok(Some(line)) = lines.next_line().await {
if line.trim().is_empty() {
continue;
}
let ts = Utc::now();
bus_e.publish(Event::BuildLog {
deployment_id: did_e.clone(),
service_id: sid_e.clone(),
line: line.clone(),
timestamp: ts,
});
let _ = db_e.append_build_line(&did_e, &line, ts).await;
}
};
tokio::join!(read_stdout, read_stderr);
let status = child
.wait()
.await
.map_err(|e| anyhow!("falha ao aguardar: {e}"))?;
if registry_internal_token.is_some()
&& let Err(e) = registry_logout().await
{
tracing::warn!(error = %e, project = %project_name, "compose_up: falha ao deslogar do registry embutido (best-effort)");
}
let _ = remove_file(&env_file_path).await;
let _ = remove_file(&compose_file_path).await;
if !status.success() {
return Err(anyhow!("docker compose up falhou"));
}
Ok(())
}