rustploy 0.1.35

Rustploy PaaS daemon — lightweight self-hosted deployment platform
//! Iniciar um deploy (`Command::DeployStart`): valida a fonte do serviço, cria o deployment e o
//! enfileira na fila global (quem roda é `deploy::queue::run_worker`).

use crate::features::deploy::DeployContext;
use crate::store::services;
use shared::{Event, Response as RpResponse, ServiceSource, ServiceStatus};
use tracing::{error, info, warn};

pub async fn handle(state: DeployContext, service_id: String) -> RpResponse {
    info!(service_id = %service_id, "deploy_start: solicitação recebida");

    let svc = match services::get(&state.db, &service_id).await {
        Ok(Some(s)) => s,
        Ok(None) => {
            warn!(service_id = %service_id, "deploy_start: serviço não encontrado");
            return RpResponse::err("NotFound", "service not found");
        }
        Err(e) => {
            error!(service_id = %service_id, error = %e, "deploy_start: erro ao buscar serviço");
            return RpResponse::err("DatabaseError", e.to_string());
        }
    };

    info!(
        service_id = %service_id,
        name = %svc.spec.name,
        status = %svc.status,
        source = match &svc.spec.source {
            ServiceSource::Registry { image } => format!("registry:{image}"),
            ServiceSource::Git(g) => format!("git:{}", g.url),
            ServiceSource::Archive(a) => format!("archive:{}", a.archive_id),
            ServiceSource::Compose(c) => format!("compose:{}", c.content),
        },
        port = svc.spec.port,
        "deploy_start: serviço encontrado"
    );

    if matches!(svc.status, ServiceStatus::Deploying | ServiceStatus::Queued) {
        warn!(service_id = %service_id, "deploy_start: deploy já em andamento/na fila, rejeitando");
        return RpResponse::err(
            "ServiceAlreadyDeploying",
            "deploy already in progress or queued",
        );
    }

    // Valida que a fonte tem conteúdo configurado
    match &svc.spec.source {
        ServiceSource::Registry { image } if image.is_empty() => {
            warn!(service_id = %service_id, "deploy_start: imagem registry vazia");
            return RpResponse::err(
                "InvalidSource",
                "registry image is empty — configure a imagem antes de fazer deploy",
            );
        }
        ServiceSource::Git(g) if g.url.is_empty() => {
            warn!(service_id = %service_id, "deploy_start: URL git vazia");
            return RpResponse::err(
                "InvalidSource",
                "git URL is empty — configure o repositório antes de fazer deploy",
            );
        }
        ServiceSource::Archive(a) if a.archive_id.is_empty() => {
            warn!(service_id = %service_id, "deploy_start: archive vazio");
            return RpResponse::err(
                "InvalidSource",
                "zip archive is empty — envie um zip antes de fazer deploy",
            );
        }
        ServiceSource::Compose(c) if c.content.is_empty() => {
            warn!(service_id = %service_id, "deploy_start: compose file vazio");
            return RpResponse::err(
                "InvalidSource",
                "compose file is empty — configure o caminho do docker-compose.yml",
            );
        }
        _ => {}
    }

    let image = match &svc.spec.source {
        ServiceSource::Registry { image } => {
            info!(service_id = %service_id, image = %image, "deploy_start: fonte registry, imagem resolvida");
            image.clone()
        }
        ServiceSource::Git(_) => {
            let tag = format!("rp_{}", svc.spec.safe_name());
            info!(service_id = %service_id, tag = %tag, "deploy_start: fonte git, tag de build resolvida");
            tag
        }
        ServiceSource::Archive(_) => {
            let tag = format!("rp_{}", svc.spec.safe_name());
            info!(service_id = %service_id, tag = %tag, "deploy_start: fonte archive, tag de build resolvida");
            tag
        }
        ServiceSource::Compose(c) => {
            let tag = format!("compose:{}", c.content);
            info!(service_id = %service_id, tag = %tag, "deploy_start: fonte compose, referência resolvida");
            tag
        }
    };

    let dep = match crate::store::deployments::create(&state.db, &service_id, &image).await {
        Ok(d) => {
            info!(
                service_id = %service_id,
                deployment_id = %d.id,
                image = %d.image,
                "deploy_start: deployment criado no banco"
            );
            d
        }
        Err(e) => {
            error!(service_id = %service_id, error = %e, "deploy_start: falha ao criar deployment");
            return RpResponse::err("DatabaseError", e.to_string());
        }
    };

    info!(service_id = %service_id, "deploy_start: atualizando status para Queued");
    let _ =
        crate::store::services::update_status(&state.db, &service_id, &ServiceStatus::Queued, None)
            .await;

    // Iniciar também é uma decisão do usuário: o serviço passa a ser desejado no ar, e o autostart
    // do boot o religa se o servidor reiniciar.
    if let Err(e) = crate::store::services::set_desired_state(
        &state.db,
        &service_id,
        crate::store::services::DesiredState::Running,
    )
    .await
    {
        warn!(service_id = %service_id, error = %e, "deploy_start: não gravei desired_state=Running");
    }

    state.bus.publish(Event::ServiceStatusChanged {
        service_id: service_id.clone(),
        status: ServiceStatus::Queued,
    });
    info!(service_id = %service_id, "deploy_start: evento ServiceStatusChanged(Queued) publicado");

    // Um deploy faz o que está rodando alcançar o declarado: some a marca de "mudança pendente de
    // deploy" deixada por um commit externo (Fase 7 do plano de estado em git).
    if let Err(e) = crate::store::state_git_marks::clear_pending(&state.db, &service_id).await {
        warn!(service_id = %service_id, error = %e, "deploy_start: não limpei a marca de deploy pendente");
    }

    // Auto-cria webhook token na primeira vez para serviços Application (não Compose)
    if !matches!(svc.spec.source, ServiceSource::Compose(_))
        && let Ok(None) =
            crate::store::webhook_tokens::get(&state.db, &state.secrets, &service_id).await
    {
        let token = crate::store::webhook_tokens::generate_token();
        let _ =
            crate::store::webhook_tokens::upsert(&state.db, &state.secrets, &service_id, &token)
                .await;
    }

    // Enfileira na fila global (um por vez). O worker
    // (`crate::features::deploy::queue::run_worker`) puxa serialmente, marca o serviço
    // como `Deploying` e roda o `DeployExecutor`.
    info!(deployment_id = %dep.id, "deploy_start: enfileirando deploy");
    state.deploy_queue.enqueue(dep.id.clone());
    state.bus.publish(Event::DeployQueueChanged);

    RpResponse::Deployment(Box::new(dep))
}