patches on sxsw ai working api key working container hardened plus many more

This commit is contained in:
Dorian
2026-03-12 22:19:04 +00:00
parent 73e0a1b74d
commit 5e6aaa74aa
14 changed files with 625 additions and 46 deletions

View File

@@ -1234,8 +1234,23 @@ fn get_app_capabilities(app_id: &str) -> Vec<String> {
"--cap-add=SETGID".to_string(),
"--cap-add=NET_BIND_SERVICE".to_string(),
],
// Bitcoin and Lightning need file ownership ops
// Bitcoin and Lightning need file ownership ops + DAC_OVERRIDE for data dir access
"bitcoin" | "bitcoin-core" | "bitcoin-knots" | "lnd" | "fedimint" | "fedimint-gateway" => vec![
"--cap-add=CHOWN".to_string(),
"--cap-add=FOWNER".to_string(),
"--cap-add=SETUID".to_string(),
"--cap-add=SETGID".to_string(),
"--cap-add=DAC_OVERRIDE".to_string(),
],
// Vaultwarden needs file ownership + NET_BIND_SERVICE (binds port 80 internally)
"vaultwarden" => vec![
"--cap-add=CHOWN".to_string(),
"--cap-add=SETUID".to_string(),
"--cap-add=SETGID".to_string(),
"--cap-add=NET_BIND_SERVICE".to_string(),
],
// PhotoPrism uses s6-overlay which needs privilege ops
"photoprism" => vec![
"--cap-add=CHOWN".to_string(),
"--cap-add=SETUID".to_string(),
"--cap-add=SETGID".to_string(),
@@ -1246,7 +1261,14 @@ fn get_app_capabilities(app_id: &str) -> Vec<String> {
"--cap-add=SETUID".to_string(),
"--cap-add=SETGID".to_string(),
],
// Minimal apps (searxng, filebrowser, uptime-kuma, etc.) need no extra caps
// Uptime-kuma startup script needs chown/fowner for /app/data ownership
"uptime-kuma" => vec![
"--cap-add=CHOWN".to_string(),
"--cap-add=FOWNER".to_string(),
"--cap-add=SETUID".to_string(),
"--cap-add=SETGID".to_string(),
],
// Minimal apps (searxng, filebrowser, etc.) need no extra caps
_ => vec![],
}
}
@@ -1258,10 +1280,7 @@ fn is_readonly_compatible(app_id: &str) -> bool {
app_id,
"searxng"
| "grafana"
| "uptime-kuma"
| "filebrowser"
| "photoprism"
| "vaultwarden"
| "mempool-electrs"
| "electrs"
| "nostr-rs-relay"

View File

@@ -241,7 +241,7 @@ impl DataModel {
Self {
server_info: ServerInfo {
id: uuid::Uuid::new_v4().to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
version: format!("{}-alpha", env!("CARGO_PKG_VERSION")),
name: Some("Archipelago".to_string()),
pubkey: String::new(),
status_info: StatusInfo {

View File

@@ -4,7 +4,9 @@
use crate::data_model::{Notification, NotificationLevel};
use crate::state::StateManager;
use crate::webhooks::{self, WebhookEvent};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{debug, info, warn};
@@ -134,7 +136,7 @@ async fn restart_container(name: &str) -> bool {
}
/// Spawn the health monitor background task.
pub fn spawn_health_monitor(state: Arc<StateManager>) {
pub fn spawn_health_monitor(state: Arc<StateManager>, data_dir: PathBuf) {
tokio::spawn(async move {
// Wait 2 minutes for containers to start up
tokio::time::sleep(std::time::Duration::from_secs(120)).await;
@@ -145,6 +147,14 @@ pub fn spawn_health_monitor(state: Arc<StateManager>) {
loop {
interval.tick().await;
// Check webhook config — if webhooks are disabled or ContainerCrash
// isn't subscribed, skip all health monitoring (no restarts, no notifications)
let webhook_config = webhooks::load_config(&data_dir).await.unwrap_or_default();
if !webhook_config.enabled || !webhook_config.events.contains(&WebhookEvent::ContainerCrash) {
debug!("Health monitor: skipping — webhooks disabled or ContainerCrash not subscribed");
continue;
}
let containers = check_containers().await;
if containers.is_empty() {
continue;

View File

@@ -2,11 +2,14 @@ pub mod collector;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
const ALERT_RULES_FILE: &str = "alert-rules.json";
/// Maximum entries at 1-minute resolution (24 hours = 1440 minutes)
const MAX_1MIN_ENTRIES: usize = 1440;
@@ -132,6 +135,7 @@ pub struct MetricsStore {
ws_connections: AtomicU32,
alert_rules: RwLock<Vec<AlertRule>>,
fired_alerts: RwLock<VecDeque<FiredAlert>>,
data_dir: Option<PathBuf>,
}
impl MetricsStore {
@@ -144,6 +148,22 @@ impl MetricsStore {
ws_connections: AtomicU32::new(0),
alert_rules: RwLock::new(AlertRule::default_rules()),
fired_alerts: RwLock::new(VecDeque::with_capacity(MAX_ALERT_HISTORY)),
data_dir: None,
}
}
/// Create a MetricsStore that persists alert rules to disk.
pub fn with_data_dir(data_dir: PathBuf) -> Self {
let rules = load_alert_rules_sync(&data_dir);
Self {
minute_data: RwLock::new(VecDeque::with_capacity(MAX_1MIN_ENTRIES)),
quarter_hour_data: RwLock::new(VecDeque::with_capacity(MAX_15MIN_ENTRIES)),
minute_count: RwLock::new(0),
rpc_latency: RwLock::new((0.0, 0)),
ws_connections: AtomicU32::new(0),
alert_rules: RwLock::new(rules),
fired_alerts: RwLock::new(VecDeque::with_capacity(MAX_ALERT_HISTORY)),
data_dir: Some(data_dir),
}
}
@@ -229,7 +249,7 @@ impl MetricsStore {
self.alert_rules.read().await.clone()
}
/// Update an alert rule by kind.
/// Update an alert rule by kind and persist to disk.
pub async fn update_alert_rule(&self, kind: &AlertRuleKind, enabled: Option<bool>, threshold: Option<f64>) {
let mut rules = self.alert_rules.write().await;
if let Some(rule) = rules.iter_mut().find(|r| &r.kind == kind) {
@@ -240,6 +260,12 @@ impl MetricsStore {
rule.threshold = t;
}
}
// Persist to disk so changes survive restarts
if let Some(ref dir) = self.data_dir {
if let Err(e) = save_alert_rules(dir, &rules).await {
warn!("Failed to persist alert rules: {}", e);
}
}
}
/// Get fired alert history.
@@ -409,6 +435,47 @@ impl MetricsStore {
}
}
/// Load alert rules from disk, falling back to defaults if file missing or corrupt.
fn load_alert_rules_sync(data_dir: &std::path::Path) -> Vec<AlertRule> {
let path = data_dir.join(ALERT_RULES_FILE);
match std::fs::read_to_string(&path) {
Ok(content) => match serde_json::from_str::<Vec<AlertRule>>(&content) {
Ok(saved) => {
// Merge with defaults: use saved enabled/threshold, add any new rule kinds
let defaults = AlertRule::default_rules();
let mut merged = Vec::new();
for default in &defaults {
if let Some(saved_rule) = saved.iter().find(|r| r.kind == default.kind) {
merged.push(AlertRule {
kind: default.kind.clone(),
threshold: saved_rule.threshold,
enabled: saved_rule.enabled,
description: default.description.clone(),
});
} else {
merged.push(default.clone());
}
}
info!("Loaded alert rules from {}", path.display());
merged
}
Err(e) => {
warn!("Failed to parse alert rules ({}), using defaults", e);
AlertRule::default_rules()
}
},
Err(_) => AlertRule::default_rules(),
}
}
/// Save alert rules to disk.
async fn save_alert_rules(data_dir: &std::path::Path, rules: &[AlertRule]) -> anyhow::Result<()> {
tokio::fs::create_dir_all(data_dir).await?;
let content = serde_json::to_string_pretty(rules)?;
tokio::fs::write(data_dir.join(ALERT_RULES_FILE), content).await?;
Ok(())
}
/// Spawn the background metrics collector (runs every 60 seconds).
/// Also evaluates alert rules on each snapshot and pushes notifications.
pub fn spawn_metrics_collector(

View File

@@ -76,7 +76,7 @@ impl Server {
let identity = Arc::new(identity);
// Create metrics store and spawn background collector
let metrics_store = Arc::new(MetricsStore::new());
let metrics_store = Arc::new(MetricsStore::with_data_dir(config.data_dir.clone()));
crate::monitoring::spawn_metrics_collector(metrics_store.clone(), Some(state_manager.clone()));
let api_handler = Arc::new(
@@ -139,7 +139,8 @@ impl Server {
}
// Container health monitoring — auto-restart unhealthy containers
crate::health_monitor::spawn_health_monitor(state_manager.clone());
// Respects webhook config: skips when disabled or ContainerCrash not subscribed
crate::health_monitor::spawn_health_monitor(state_manager.clone(), config.data_dir.clone());
Ok(Self {
_config: config,

View File

@@ -94,8 +94,8 @@ mod tests {
let sm = StateManager::new();
let (data, rev) = sm.get_snapshot().await;
assert_eq!(rev, 0);
// DataModel::new() sets version from CARGO_PKG_VERSION
assert_eq!(data.server_info.version, env!("CARGO_PKG_VERSION"));
// DataModel::new() sets version from CARGO_PKG_VERSION with alpha suffix
assert_eq!(data.server_info.version, format!("{}-alpha", env!("CARGO_PKG_VERSION")));
assert!(data.package_data.is_empty());
assert!(data.notifications.is_empty());
}