Skip to main content

max / makenotwork

6.3 KB · 154 lines History Blame Raw
1 use anyhow::Result;
2 use bento_daemon::{config, db, events, metrics, ota, routes, state, topology};
3 use std::collections::HashMap;
4 use std::net::SocketAddr;
5 use std::sync::Arc;
6
7 /// MNW base URL the `tauri-mnw` OTA backend publishes to. Overridable so a
8 /// staging host can point elsewhere; defaults to production.
9 fn mnw_base_url() -> String {
10 std::env::var("BENTO_MNW_BASE_URL").unwrap_or_else(|_| "https://makenot.work".into())
11 }
12
13 #[tokio::main]
14 async fn main() -> Result<()> {
15 // `--check-config`: load the daemon config + topology (which reads every
16 // app's in-repo manifest) and exit 0, or fail with the parse error and a
17 // non-zero code. Run before swapping in a freshly built bentod so a schema
18 // change that can't parse the live config fails the install instead of
19 // bricking the daemon — a per-app-config move once left it down for 20h.
20 // Mirrors `sandod --check-config`.
21 if std::env::args().skip(1).any(|a| a == "--check-config") {
22 let cfg = config::Config::load()?;
23 let topo = topology::Topology::load(&cfg.topology_path)?;
24 // Cross-document: every service reaches its host exactly one way, either
25 // its own [[deploy]] or a [handoff] to Sando. Neither builds and archives
26 // into nothing; both leaves two systems installing the same service.
27 topo.validate_delivery(&cfg)?;
28 println!(
29 "bentod config OK: {} host(s), {} app(s) from {}",
30 topo.hosts.len(),
31 topo.app.len(),
32 cfg.topology_path.display()
33 );
34 return Ok(());
35 }
36
37 tracing_subscriber::fmt()
38 .with_writer(std::io::stderr)
39 .with_env_filter(
40 tracing_subscriber::EnvFilter::try_from_default_env()
41 .unwrap_or_else(|_| "bento_daemon=info,bentod=info,tower_http=info".into()),
42 )
43 .init();
44
45 let cfg = Arc::new(config::Config::load()?);
46 let topo = Arc::new(topology::Topology::load(&cfg.topology_path)?);
47 topo.validate_delivery(&cfg)?;
48 tokio::fs::create_dir_all(&cfg.dist_root).await?;
49 tokio::fs::create_dir_all(&cfg.logs_root).await?;
50 let pool = db::open(&cfg.db_path).await?;
51 // Reconcile any builds left `running` by a previous process (the finalizer
52 // and target tasks don't survive a restart) so they don't orphan forever.
53 match db::recover_orphaned_running(&pool).await {
54 Ok(0) => {}
55 Ok(n) => tracing::warn!(
56 reconciled = n,
57 "marked orphaned `running` builds failed after restart"
58 ),
59 Err(e) => tracing::error!(error = %e, "failed to reconcile orphaned running builds"),
60 }
61 tracing::info!(
62 hosts = topo.hosts.len(),
63 apps = topo.app.len(),
64 "topology loaded"
65 );
66
67 let prom = metrics::init();
68 let addr: SocketAddr = cfg.listen.parse()?;
69
70 // Build-API auth (CF2). Token from BENTO_API_TOKEN (systemd EnvironmentFile);
71 // refuse to expose build triggers unauthenticated on a non-loopback bind.
72 let api_token: Option<Arc<str>> = std::env::var("BENTO_API_TOKEN")
73 .ok()
74 .map(|s| s.trim().to_string())
75 .filter(|s| !s.is_empty())
76 .map(|s| Arc::from(s.as_str()));
77 if api_token.is_none() && !addr.ip().is_loopback() {
78 anyhow::bail!(
79 "BENTO_API_TOKEN is unset but listen={addr} is not loopback; refusing to expose build \
80 triggers unauthenticated. Set BENTO_API_TOKEN (via EnvironmentFile) or bind 127.0.0.1."
81 );
82 }
83 match &api_token {
84 Some(_) => tracing::info!("build endpoints require a bearer token"),
85 None => {
86 tracing::warn!(%addr, "BENTO_API_TOKEN unset; build endpoints are UNAUTHENTICATED (loopback bind)");
87 }
88 }
89
90 let executors = Arc::new(state::build_executors(&topo));
91 let syncs = Arc::new(state::build_syncs(&topo));
92 let host_locks = state::build_host_locks(&topo);
93 let mnw_base = mnw_base_url();
94 let app_state = state::AppState {
95 pool,
96 topo,
97 cfg,
98 prom,
99 events: events::channel(),
100 ota: Arc::new(ota::OtaRegistry::standard(mnw_base.clone())),
101 executors,
102 syncs,
103 active: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
104 api_token,
105 host_locks,
106 distribution: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
107 // Short timeout: an unreachable MNW must make the board say "unknown"
108 // promptly, not stall the whole poll behind a hanging connect.
109 http: bento_daemon::tls::builder()
110 .timeout(std::time::Duration::from_secs(5))
111 .build()
112 .expect("build http client"),
113 mnw_base_url: mnw_base.into(),
114 };
115 // Disk retention: prune logs_root + dist_root to the newest N versions per
116 // app, at startup and every 6h, so neither root grows without bound. Skips
117 // any (app, version) that must survive: one an in-flight build is writing
118 // to, or one a published release still points at (so a slow-adopting
119 // release that has fallen outside the keep window is never deleted out from
120 // under clients still fetching it).
121 {
122 let cfg = app_state.cfg.clone();
123 let pool = app_state.pool.clone();
124 tokio::spawn(async move {
125 let mut tick = tokio::time::interval(std::time::Duration::from_hours(6));
126 loop {
127 tick.tick().await; // fires immediately, then every 6h
128 let protected: bento_daemon::retention::ProtectedVersions =
129 sqlx::query_as::<_, (String, String)>(
130 "SELECT DISTINCT app, version FROM target_runs WHERE status = 'running' \
131 UNION \
132 SELECT DISTINCT app, version FROM releases",
133 )
134 .fetch_all(&pool)
135 .await
136 .unwrap_or_default()
137 .into_iter()
138 .collect();
139 let cfg = cfg.clone();
140 let _ = tokio::task::spawn_blocking(move || {
141 bento_daemon::retention::prune_once(&cfg, &protected);
142 })
143 .await;
144 }
145 });
146 }
147
148 let app = routes::router(app_state);
149 tracing::info!(%addr, "bento daemon listening");
150 let listener = tokio::net::TcpListener::bind(addr).await?;
151 axum::serve(listener, app).await?;
152 Ok(())
153 }
154