use crate::config::Config; use crate::domain::NodeId; use crate::events::EventTx; use crate::topology::{Node, Topology}; 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>; /// The in-flight build pipeline: its abort handle plus the `build_runs` row it /// drives. A newer `/rebuild` aborts the handle and settles the row `aborted`. pub struct ActiveBuild { pub handle: AbortHandle, pub run_id: crate::domain::RunId, } #[derive(Clone)] pub struct AppState { pub pool: SqlitePool, pub topo: Arc, pub cfg: Arc, /// Single-slot guard for the build pipeline. A new /rebuild aborts any /// in-flight build (cargo + gates) so the latest push always wins. Carries /// the run id alongside the handle so the aborting `/rebuild` can settle /// the superseded `build_runs` row as `aborted`. pub active_build: Arc>>, /// Serializes the deploy mutators (`/promote`, `/rollback`) so their /// check -> deploy -> advance sequences never interleave. Without it two /// concurrent promotes (or a promote racing a rollback) could deploy mixed /// versions to a node and clobber each other's `tier_state` advance /// (CF3). Held for the whole handler. pub deploy_lock: 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, /// Bearer token required on the deploy-mutating routes (promote, rollback, /// rebuild, confirm, backup/fetch). Sourced from `SANDO_API_TOKEN` (the /// systemd EnvironmentFile). `None` = unauthenticated, which main() permits /// only on a loopback bind (CF2). pub api_token: Option>, } /// 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() }