use crate::bootstrap::state::AppState;
use crate::features::iac::manifest::ProjectManifest;
use shared::{
ActionVerb, ApplyReport, ResourceAction, ResourceActionKind, Response as RpResponse,
ServiceSource,
};
use std::collections::BTreeMap;
use tracing::{debug, info, warn};
pub async fn handle(
state: AppState,
manifests: Vec<String>,
prune: bool,
deploy: bool,
) -> RpResponse {
info!(
count = manifests.len(),
prune, deploy, "manifest_apply: reconciliando manifestos"
);
let mut report = ApplyReport::default();
let mut changed: Vec<(String, String)> = Vec::new();
let provider_ids: BTreeMap<String, String> =
match crate::store::git_providers::list(&state.db).await {
Ok(list) => list.into_iter().map(|p| (p.name, p.id)).collect(),
Err(e) => {
tracing::error!(error = %e, "manifest_apply: erro ao listar git providers");
return RpResponse::err("DatabaseError", e.to_string());
}
};
info!(
?provider_ids,
"manifest_apply: git providers disponíveis pra resolução (nome -> id)"
);
for yaml in manifests {
let manifest: ProjectManifest = match serde_yaml::from_str(&yaml) {
Ok(m) => m,
Err(e) => return RpResponse::err("InvalidManifest", e.to_string()),
};
if let Err(resp) = apply_one(
&state,
manifest,
prune,
&provider_ids,
&mut report,
&mut changed,
)
.await
{
return resp;
}
}
if deploy {
for (service_id, label) in &changed {
match crate::features::deploy::start::handle(state.clone(), service_id.clone()).await {
RpResponse::Err { code, message } => {
warn!(%label, %code, %message, "manifest_apply: deploy falhou ao iniciar");
}
_ => report.deployed.push(label.clone()),
}
}
}
info!(
actions = report.actions.len(),
deployed = report.deployed.len(),
"manifest_apply: concluído"
);
RpResponse::ManifestReport(report)
}
async fn apply_one(
state: &AppState,
manifest: ProjectManifest,
prune: bool,
provider_ids: &BTreeMap<String, String>,
report: &mut ApplyReport,
changed: &mut Vec<(String, String)>,
) -> Result<(), RpResponse> {
let (name, description, env_vars) = manifest.project_fields();
if name.trim().is_empty() {
return Err(RpResponse::err(
"InvalidManifest",
"projeto sem nome no manifesto",
));
}
let existing = crate::store::projects::list(&state.db)
.await
.map_err(db_err)?
.into_iter()
.find(|p| p.name == name);
let (project_id, project_action) = match existing {
Some(p) if p.description == description => {
if !env_vars.is_empty() && p.env_vars != env_vars {
crate::store::projects::update(&state.db, &p.id, name.clone(), description)
.await
.map_err(db_err)?;
crate::store::projects::update_env_vars(&state.db, &p.id, env_vars, Vec::new())
.await
.map_err(db_err)?;
(p.id, ActionVerb::Updated)
} else {
(p.id, ActionVerb::Unchanged)
}
}
Some(p) => {
crate::store::projects::update(&state.db, &p.id, name.clone(), description)
.await
.map_err(db_err)?;
if !env_vars.is_empty() {
crate::store::projects::update_env_vars(&state.db, &p.id, env_vars, Vec::new())
.await
.map_err(db_err)?;
}
(p.id, ActionVerb::Updated)
}
None => {
let created = crate::store::projects::create(&state.db, name.clone(), description)
.await
.map_err(db_err)?;
if !env_vars.is_empty() {
crate::store::projects::update_env_vars(
&state.db,
&created.id,
env_vars,
Vec::new(),
)
.await
.map_err(db_err)?;
}
(created.id, ActionVerb::Created)
}
};
report.actions.push(ResourceAction {
kind: ResourceActionKind::Project,
name: name.clone(),
action: project_action,
});
let existing_services = crate::store::services::list(&state.db, &project_id)
.await
.map_err(db_err)?;
let mut specs = manifest.service_specs(&project_id, provider_ids);
for spec in &mut specs {
if spec.host_port != Some(crate::features::ingress::external_ports::AUTO_PORT) {
continue;
}
let existing_port = existing_services
.iter()
.find(|s| s.spec.name == spec.name)
.and_then(|s| s.spec.host_port)
.filter(|p| *p != crate::features::ingress::external_ports::AUTO_PORT);
match existing_port {
Some(p) => spec.host_port = Some(p),
None => {
crate::features::ingress::external_ports::resolve_host_port(&state.db, spec, None)
.await
.map_err(|e| RpResponse::err("PortAllocationError", e))?;
if let Some(p) = spec.host_port {
crate::features::ingress::firewall::ensure_allowed_bg(p);
}
}
}
}
for spec in &specs {
let svc_name = spec.name.clone();
let label = format!("{name}/{svc_name}");
if let ServiceSource::Git(g) = &spec.source {
info!(
service = %label,
provider_id = ?g.provider_id,
url = %g.url,
"manifest_apply: fonte git resolvida a partir do manifesto"
);
}
let existing_match = existing_services.iter().find(|s| s.spec.name == svc_name);
if let Some(s) = existing_match
&& let (ServiceSource::Git(old_g), ServiceSource::Git(new_g)) =
(&s.spec.source, &spec.source)
&& old_g.provider_id != new_g.provider_id
{
warn!(
service = %label,
old_provider_id = ?old_g.provider_id,
new_provider_id = ?new_g.provider_id,
"manifest_apply: provider_id do serviço git VAI MUDAR nesta atualização"
);
}
let (action, changed_id) = match existing_match {
Some(s) if &s.spec == spec => {
debug!(service = %label, "manifest_apply: spec idêntico ao existente, Unchanged");
(ActionVerb::Unchanged, None)
}
Some(s) => {
let mut effective_spec = spec.clone();
if effective_spec.env_vars.is_empty() && !s.spec.env_vars.is_empty() {
effective_spec.env_vars = s.spec.env_vars.clone();
}
crate::store::services::update_spec(&state.db, &s.id, effective_spec.clone())
.await
.map_err(db_err)?;
crate::features::services::update::sync_firewall(
state,
&s.id,
s.spec.host_port,
effective_spec.host_port,
)
.await;
debug!(service = %label, service_id = %s.id, "manifest_apply: serviço atualizado (Updated)");
(ActionVerb::Updated, Some(s.id.clone()))
}
None => {
let created = crate::store::services::create(&state.db, spec.clone())
.await
.map_err(db_err)?;
debug!(service = %label, service_id = %created.id, "manifest_apply: serviço criado (Created)");
(ActionVerb::Created, Some(created.id))
}
};
if let Some(id) = changed_id {
changed.push((id, label.clone()));
}
report.actions.push(ResourceAction {
kind: ResourceActionKind::Service,
name: label,
action,
});
}
if prune {
let wanted: std::collections::HashSet<&str> =
specs.iter().map(|s| s.name.as_str()).collect();
for s in &existing_services {
if wanted.contains(s.spec.name.as_str()) {
continue;
}
let label = format!("{name}/{}", s.spec.name);
match crate::features::services::delete::handle(state.clone(), s.id.clone()).await {
RpResponse::Err { code, message } => {
warn!(%label, %code, %message, "manifest_apply: prune falhou");
}
_ => {
info!(%label, "manifest_apply: serviço removido (prune)");
report.actions.push(ResourceAction {
kind: ResourceActionKind::Service,
name: label,
action: ActionVerb::Deleted,
});
}
}
}
}
Ok(())
}
fn db_err(e: anyhow::Error) -> RpResponse {
tracing::error!(error = %e, "manifest_apply: erro de banco");
RpResponse::err("DatabaseError", e.to_string())
}