use crate::config::Config; use crate::domain::NodeId; use crate::events::EventTx; use crate::topology::{Node, Topology}; use metrics_exporter_prometheus::PrometheusHandle; use ops_exec::{CapabilitySet, Executor, LocalExec, SshExec}; use sqlx::SqlitePool; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::Mutex; use tokio::task::AbortHandle; /// Per-node executors keyed by node id, built once from the topology at /// startup. The deploy path looks a node's executor up here instead of /// constructing ssh/rsync invocations inline. pub type ExecutorMap = HashMap>; #[derive(Clone)] pub struct AppState { pub pool: SqlitePool, pub topo: Arc, pub cfg: Arc, pub prom: PrometheusHandle, /// Single-slot guard for the build pipeline. A new /rebuild aborts any /// in-flight build (cargo + gates) so the latest push always wins. pub active_build: Arc>>, /// Broadcast bus for live operator events. WS /events subscribes; all /// build/gate/deploy code sites emit on this. pub events: EventTx, /// One capability-scoped [`Executor`] per node, built from the topology. pub executors: Arc, } /// Build the executor for one node: a `LocalExec` for the `local` fast-path, an /// `SshExec` otherwise, each granted the node's declared capabilities (which /// default to deploy+restart / observe health — the historical behavior). pub fn build_executor(node: &Node) -> Arc { let caps = CapabilitySet::from_tokens(&node.actuate, &node.observe); if node.ssh_target == "local" || node.ssh_target.is_empty() { Arc::new(LocalExec::new(caps)) } else { Arc::new(SshExec::new(node.ssh_target.clone(), caps)) } } /// Build the full node → executor map from every tier's nodes. pub fn build_executors(topo: &Topology) -> ExecutorMap { topo.tiers .iter() .flat_map(|t| t.nodes.iter()) .map(|node| (node.name.clone(), build_executor(node))) .collect() }