Add `hivectl gateway {create-user,delete-user,list-users}` subcommands for
managing htpasswd files used by gateway Basic auth. Pure Rust bcrypt
(cost 12, $2y$ prefix nginx accepts). No external htpasswd binary required.
Also fix the NixOS module assertion: `cfg.auth ? htpasswdFile` is always
true in the module system (declared options always exist as keys); switch
to `nullOr path; default = null` + `!= null` check so the assertion
actually fires with a useful error when enable=true but no file is set.
Guard bind-mount and nginx config against null to prevent eval errors.
Update docs/gateway.md to show hivectl commands instead of raw htpasswd.
67 lines
2.3 KiB
Rust
67 lines
2.3 KiB
Rust
//! Host-side vacuum of every per-agent turn-stats.sqlite. The harness
|
|
//! writes to `/agents/<name>/harness/hyperhive-turn-stats.sqlite`
|
|
//! (bind-mounted from `/var/lib/hyperhive/agents/<name>/harness/`); we open the same file
|
|
//! from the host every hour and delete rows older than `KEEP_SECS`.
|
|
//! Mirrors `events_vacuum` in structure — host-side so the harness
|
|
//! can't disable it, age-only so a chatty burst doesn't evict old
|
|
//! rows sooner than expected. 90-day retention keeps enough history
|
|
//! for trend analysis without unbounded growth.
|
|
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
use rusqlite::{Connection, Result, params};
|
|
|
|
use crate::coordinator::Coordinator;
|
|
|
|
const VACUUM_INTERVAL: Duration = Duration::from_hours(1);
|
|
const KEEP_SECS: i64 = 90 * 24 * 3600;
|
|
|
|
/// Background loop: sweep every existing agent state dir hourly, run
|
|
/// the vacuum SQL against its turn-stats.sqlite if present. Errors
|
|
/// are logged but don't tear the loop down.
|
|
pub fn spawn(coord: &Arc<Coordinator>) {
|
|
let mut shutdown = coord.shutdown_rx();
|
|
tokio::spawn(async move {
|
|
loop {
|
|
sweep_once();
|
|
tokio::select! {
|
|
() = tokio::time::sleep(VACUUM_INTERVAL) => {}
|
|
_ = shutdown.changed() => {
|
|
tracing::info!("stats vacuum: shutdown signal received");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn sweep_once() {
|
|
for name in Coordinator::kept_state_names() {
|
|
let path = Coordinator::agent_harness_dir(&name).join("hyperhive-turn-stats.sqlite");
|
|
if !path.exists() {
|
|
continue;
|
|
}
|
|
match vacuum_file(&path) {
|
|
Ok(0) => {}
|
|
Ok(n) => tracing::info!(agent = %name, removed = n, "turn-stats vacuum"),
|
|
Err(e) => tracing::warn!(agent = %name, error = ?e, "turn-stats vacuum failed"),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn vacuum_file(path: &Path) -> Result<u64> {
|
|
let conn = Connection::open(path)?;
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.ok()
|
|
.and_then(|d| i64::try_from(d.as_secs()).ok())
|
|
.unwrap_or(0);
|
|
let cutoff = now - KEEP_SECS;
|
|
let removed = conn.execute(
|
|
"DELETE FROM turn_stats WHERE started_at < ?1",
|
|
params![cutoff],
|
|
)?;
|
|
Ok(u64::try_from(removed).unwrap_or(0))
|
|
}
|