Skip to main content

max / makenotwork

4.9 KB · 130 lines History Blame Raw
1 use crate::config::AppConfig;
2 use crate::domain::{AppId, NodeId};
3 use crate::events::EventTx;
4 use crate::topology::{Node, Topology};
5 use ops_exec::{CapabilitySet, Executor, LocalExec, SshExec};
6 use sqlx::SqlitePool;
7 use std::collections::HashMap;
8 use std::sync::Arc;
9 use tokio::sync::Mutex;
10 use tokio::task::AbortHandle;
11
12 /// Per-node executors keyed by node id, built once from the topology at
13 /// startup. The deploy path looks a node's executor up here instead of
14 /// constructing ssh/rsync invocations inline.
15 pub type ExecutorMap = HashMap<NodeId, Arc<dyn Executor>>;
16
17 /// The in-flight build pipeline: its abort handle plus the `build_runs` row it
18 /// drives. A newer `/rebuild` aborts the handle and settles the row `aborted`.
19 pub struct ActiveBuild {
20 pub handle: AbortHandle,
21 pub run_id: crate::domain::RunId,
22 }
23
24 /// One product, fully resolved: its pipeline config, its tiers and nodes, and
25 /// the executors for those nodes.
26 ///
27 /// Everything a handler needs to act on a product, reachable from its
28 /// [`AppId`] alone. Bundled rather than left as three parallel maps because the
29 /// three are only ever correct together — an executor map built from another
30 /// product's topology would address the wrong machines.
31 pub struct App {
32 pub cfg: Arc<AppConfig>,
33 pub topo: Arc<Topology>,
34 pub executors: Arc<ExecutorMap>,
35 }
36
37 /// Every product this daemon ships, by id.
38 pub type AppMap = std::collections::BTreeMap<AppId, Arc<App>>;
39
40 #[derive(Clone)]
41 pub struct AppState {
42 pub pool: SqlitePool,
43 /// Every configured product. The authority; the three fields below are one
44 /// product's view of it.
45 pub apps: Arc<AppMap>,
46 /// The product a request that names none is about, and the only one a
47 /// pre-multi-app config declares.
48 pub default_app: AppId,
49 pub topo: Arc<Topology>,
50 pub cfg: Arc<AppConfig>,
51 /// Single-slot guard for the build pipeline. A new /rebuild aborts any
52 /// in-flight build (cargo + gates) so the latest push always wins. Carries
53 /// the run id alongside the handle so the aborting `/rebuild` can settle
54 /// the superseded `build_runs` row as `aborted`.
55 pub active_build: Arc<Mutex<Option<ActiveBuild>>>,
56 /// Serializes the deploy mutators (`/promote`, `/rollback`) so their
57 /// check -> deploy -> advance sequences never interleave. Without it two
58 /// concurrent promotes (or a promote racing a rollback) could deploy mixed
59 /// versions to a node and clobber each other's `tier_state` advance
60 /// (CF3). Held for the whole handler.
61 pub deploy_lock: Arc<Mutex<()>>,
62 /// Broadcast bus for live operator events. WS /events subscribes; all
63 /// build/gate/deploy code sites emit on this.
64 pub events: EventTx,
65 /// One capability-scoped [`Executor`] per node, built from the topology.
66 pub executors: Arc<ExecutorMap>,
67 /// Bearer token required on the deploy-mutating routes (promote, rollback,
68 /// rebuild, confirm, backup/fetch). Sourced from `SANDO_API_TOKEN` (the
69 /// systemd EnvironmentFile). `None` = unauthenticated, which main() permits
70 /// only on a loopback bind (CF2).
71 pub api_token: Option<Arc<str>>,
72 }
73
74 /// The `apps` map and default id for a daemon shipping exactly one product.
75 ///
76 /// The common case. Kept as a constructor so the single-product path builds the
77 /// same map the multi-product path does, rather than a special case that can
78 /// drift from it.
79 pub fn one_app(
80 cfg: Arc<AppConfig>,
81 topo: Arc<Topology>,
82 executors: Arc<ExecutorMap>,
83 ) -> (Arc<AppMap>, AppId) {
84 let id = cfg.id.clone();
85 let mut apps = AppMap::new();
86 apps.insert(
87 id.clone(),
88 Arc::new(App {
89 cfg,
90 topo,
91 executors,
92 }),
93 );
94 (Arc::new(apps), id)
95 }
96
97 impl AppState {
98 /// The product `id` names, or `None` if this daemon does not ship it.
99 pub fn app(&self, id: &AppId) -> Option<&Arc<App>> {
100 self.apps.get(id)
101 }
102
103 /// Every product id, in declared order. Stable, because it keys the
104 /// operator-facing surface.
105 pub fn app_ids(&self) -> Vec<AppId> {
106 self.apps.keys().cloned().collect()
107 }
108 }
109
110 /// Build the executor for one node: a `LocalExec` for the `local` fast-path, an
111 /// `SshExec` otherwise, each granted the node's declared capabilities (which
112 /// default to deploy+restart / observe health).
113 pub fn build_executor(node: &Node) -> Arc<dyn Executor> {
114 let caps = CapabilitySet::from_tokens(&node.actuate, &node.observe);
115 if node.ssh_target == "local" || node.ssh_target.is_empty() {
116 Arc::new(LocalExec::new(caps))
117 } else {
118 Arc::new(SshExec::new(node.ssh_target.clone(), caps))
119 }
120 }
121
122 /// Build the full node → executor map from every tier's nodes.
123 pub fn build_executors(topo: &Topology) -> ExecutorMap {
124 topo.tiers
125 .iter()
126 .flat_map(|t| t.nodes.iter())
127 .map(|node| (node.name.clone(), build_executor(node)))
128 .collect()
129 }
130