patches on sxsw ai working api key working container hardened plus many more
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user