Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions crates/agent/src/agent_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use super::*;
use crate::config::DockerConfig;
use jiff::SignedDuration;
use skym_server::api::{AppState, router};
use skym_server::config::{Customer, HostEntry, ServerConfig, sha256_hex};
use skym_server::config::{HostEntry, ServerConfig, sha256_hex};
use skym_server::db;
use skym_server::store::{Store, hosts};
use std::net::SocketAddr;
Expand All @@ -18,13 +18,7 @@ const TOKEN: &str = "host-x-token";

fn state() -> AppState {
let cfg = ServerConfig {
customers: vec![Customer { id: "acme".into(), name: "Acme".into() }],
hosts: vec![HostEntry {
id: "x".into(),
customer: None,
token_sha256: sha256_hex(TOKEN),
tags: vec![],
}],
hosts: vec![HostEntry { id: "x".into(), token_sha256: sha256_hex(TOKEN), tags: vec![] }],
..ServerConfig::default()
};
AppState::new(Store::new(db::open_in_memory().unwrap()), cfg, Timestamp::now())
Expand Down
1 change: 0 additions & 1 deletion crates/agent/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ pub fn host_view(report: &Report, mut findings: Vec<Finding>) -> HostView {
.collect();
HostView {
id: report.host.clone(),
customer: None,
status: rollup(&incidents),
last_report_ago: None,
observed_since: None,
Expand Down
1 change: 0 additions & 1 deletion crates/core/src/subject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use std::{borrow::Cow, fmt, str::FromStr};

pub type HostId = String;
pub type CustomerId = String;

/// Stable identity of a workload across container recreation.
///
Expand Down
17 changes: 1 addition & 16 deletions crates/core/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::model::{
WorkloadState,
};
use crate::rules::{IncidentCode, Severity};
use crate::subject::{AppKey, CustomerId, HostId, Subject, WorkloadKey};
use crate::subject::{AppKey, HostId, Subject, WorkloadKey};
use crate::time::Timestamp;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -124,7 +124,6 @@ pub struct WorkloadSummary {
#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, PartialEq)]
pub struct HostView {
pub id: HostId,
pub customer: Option<CustomerId>,
pub status: Status,
pub last_report_ago: Option<String>,
/// When skym first heard from the host: incidents open about that long may be older.
Expand Down Expand Up @@ -247,25 +246,11 @@ pub struct EndpointOverview {
pub app: Option<AppKey>,
}

#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, PartialEq)]
pub struct CustomerOverview {
pub id: CustomerId,
pub name: String,
pub status: Status,
#[serde(default)]
pub hosts: Vec<HostOverview>,
#[serde(default)]
pub endpoints: Vec<EndpointOverview>,
}

/// `GET /api/overview`: where is something wrong right now.
#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, PartialEq)]
pub struct Overview {
pub ts: Timestamp,
pub status: Status,
/// Grouped by customer, for views older than `problems` and `hosts`; to be removed.
#[serde(default)]
pub customers: Vec<CustomerOverview>,
#[serde(default)]
pub muted_count: u32,
/// Open, unmuted incidents of every host and application.
Expand Down
15 changes: 10 additions & 5 deletions crates/core/tests/compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,17 +331,22 @@ fn workloads_belong_to_their_project_or_stand_alone() {

#[test]
fn an_overview_from_an_older_server_parses() {
// Its hosts were grouped under `customers`, which no longer exists.
let json = r#"{
"ts": "2026-10-01T00:00:00Z", "status": "warn",
"customers": [{ "id": "acme", "name": "Acme", "status": "warn", "hosts": [{
"id": "x", "status": "warn", "last_report_ago": "5s",
"incidents": [{ "subject": "host:x", "code": "OOM_KILLED", "severity": "warn",
"detail": "d", "opened_at": null, "open_for": null, "resolved_at": null,
"mute_reason": null }] }] }]
"id": "x", "status": "warn", "last_report_ago": "5s", "incidents": [] }] }]
}"#;
let o: skym_core::view::Overview = serde_json::from_str(json).unwrap();
assert!(o.problems.is_empty() && o.hosts.is_empty(), "added later, absent before");
let host = &o.customers[0].hosts[0];
assert_eq!(o.status, skym_core::view::Status::Warn);
let host: skym_core::view::HostOverview = serde_json::from_str(
r#"{ "id": "x", "status": "warn", "last_report_ago": "5s",
"incidents": [{ "subject": "host:x", "code": "OOM_KILLED", "severity": "warn",
"detail": "d", "opened_at": null, "open_for": null, "resolved_at": null,
"mute_reason": null }] }"#,
)
.unwrap();
assert_eq!((host.load_1m, host.disks.len(), host.apps), (None, 0, 0));
let incident = &host.incidents[0];
assert_eq!((incident.app.clone(), incident.observed_since), (None, None));
Expand Down
29 changes: 3 additions & 26 deletions crates/server/src/api/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ use skym_core::rules::{IncidentCode, Severity};
use skym_core::subject::{AppKey, HostId, Subject, WorkloadKey, encode};
use skym_core::time::format_duration;
use skym_core::view::{
AppSummary, CustomerOverview, DiskUse, EndpointOverview, HostOverview, HostView, IncidentView,
Overview, Status, Timeline, TimelineEntry, TimelineKind, WorkloadSummary, WorkloadView, rollup,
workload_summary,
AppSummary, DiskUse, EndpointOverview, HostOverview, HostView, IncidentView, Overview, Status,
Timeline, TimelineEntry, TimelineKind, WorkloadSummary, WorkloadView, rollup, workload_summary,
};
use std::collections::BTreeMap;

Expand Down Expand Up @@ -149,7 +148,6 @@ pub fn host(
ip: row.as_ref().and_then(|r| r.remote_addr.clone()),
state: row.map(|r| r.state),
id,
customer: None, // customers are gone; the field stays for older readers
workloads,
incidents,
apps,
Expand Down Expand Up @@ -266,7 +264,7 @@ pub fn hosts(
}

/// Where something is wrong: every open, unmuted problem, worst and oldest first, and every
/// host. `customers` groups the hosts for views older than this.
/// host.
pub fn overview(
cfg: &ServerConfig,
rows: &[HostRow],
Expand All @@ -278,32 +276,11 @@ pub fn overview(
let mut problems: Vec<IncidentView> = open.iter().filter(|i| !i.muted).cloned().collect();
problems
.sort_by(|a, b| b.severity.cmp(&a.severity).then_with(|| a.opened_at.cmp(&b.opened_at)));
let customers = cfg
.customers
.iter()
.map(|c| {
let theirs: Vec<HostOverview> = hosts
.iter()
.filter(|h| {
cfg.hosts.iter().any(|e| e.id == h.id && e.customer.as_ref() == Some(&c.id))
})
.cloned()
.collect();
CustomerOverview {
id: c.id.clone(),
name: c.name.clone(),
status: theirs.iter().map(|h| h.status).max().unwrap_or(Status::Ok),
hosts: theirs,
endpoints: Vec::new(),
}
})
.collect();
let worst = hosts.iter().map(|h| h.status).chain(std::iter::once(rollup(&problems)));
Overview {
ts: now,
status: worst.max().unwrap_or(Status::Ok),
muted_count: open.iter().filter(|i| i.muted).count() as u32,
customers,
problems,
hosts,
}
Expand Down
14 changes: 3 additions & 11 deletions crates/server/src/api/views_tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::*;
use crate::config::{Customer, Endpoint, Headers, HostEntry};
use crate::config::{Endpoint, Headers, HostEntry};
use crate::probe::Probe;
use skym_core::model::EventKind;
use skym_core::rules::{IncidentCode, Severity};
Expand Down Expand Up @@ -178,14 +178,9 @@ fn summary(key: &str, status: Status) -> AppSummary {

#[test]
fn the_overview_lists_problems_flat_and_hosts_by_urgency() {
let host = |id: &str, customer: Option<&str>| HostEntry {
id: id.into(),
customer: customer.map(String::from),
..HostEntry::default()
};
let host = |id: &str| HostEntry { id: id.into(), ..HostEntry::default() };
let cfg = ServerConfig {
customers: vec![Customer { id: "acme".into(), name: "Acme".into() }],
hosts: vec![host("a", Some("acme")), host("b", None), host("c", None), host("d", None)],
hosts: vec![host("a"), host("b"), host("c"), host("d")],
..ServerConfig::default()
};
let rows = [row("a", 0), row("b", 0), row("c", 0)];
Expand Down Expand Up @@ -232,9 +227,6 @@ fn the_overview_lists_problems_flat_and_hosts_by_urgency() {
(1, 1),
"info listed, not counted"
);
assert_eq!(o.customers.len(), 1, "older views still get their grouping");
let acme: Vec<&str> = o.customers[0].hosts.iter().map(|h| h.id.as_str()).collect();
assert_eq!(acme, ["a"]);
}

#[test]
Expand Down
85 changes: 44 additions & 41 deletions crates/server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use reqwest::header::{HeaderName, HeaderValue};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use skym_core::rules::IncidentCode;
use skym_core::subject::{AppKey, CustomerId, EXTERNAL, HostId, Subject};
use skym_core::subject::{AppKey, EXTERNAL, HostId, Subject};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::net::SocketAddr;
Expand All @@ -19,8 +19,6 @@ pub struct ServerConfig {
pub database: PathBuf,
/// Must match the agents' interval: heartbeat timeout is three of them.
pub report_interval: SignedDuration,
/// Optional, kept for the per-customer grouping older views read; tags will replace it.
pub customers: Vec<Customer>,
pub hosts: Vec<HostEntry>,
pub readers: Vec<Reader>,
pub mute: Vec<Mute>,
Expand All @@ -35,7 +33,6 @@ impl Default for ServerConfig {
listen: ([127, 0, 0, 1], 7280).into(),
database: "/var/lib/skym-server/skym.db".into(),
report_interval: SignedDuration::from_secs(60),
customers: Vec::new(),
hosts: Vec::new(),
readers: Vec::new(),
mute: Vec::new(),
Expand All @@ -45,17 +42,9 @@ impl Default for ServerConfig {
}
}

#[derive(Deserialize, Debug, Clone)]
pub struct Customer {
pub id: CustomerId,
pub name: String,
}

#[derive(Deserialize, Debug, Clone, Default)]
pub struct HostEntry {
pub id: HostId,
#[serde(default)]
pub customer: Option<CustomerId>,
pub token_sha256: String,
/// Tags for filtering and grouping; its applications inherit them.
#[serde(default)]
Expand Down Expand Up @@ -171,10 +160,26 @@ pub fn parse(text: &str) -> anyhow::Result<ServerConfig> {
let line = e.span().map(|s| text[..s.start].lines().count().max(1));
anyhow::anyhow!("{} (line {})", e.message(), line.map_or("?".into(), |l| l.to_string()))
})?;
reject_customers(text)?;
validate(&cfg)?;
Ok(cfg)
}

/// Customers gave way to tags: a configuration that still has them says so, instead of
/// silently losing its grouping.
fn reject_customers(text: &str) -> anyhow::Result<()> {
let Ok(table) = text.parse::<toml::Table>() else { return Ok(()) };
let hosts = table.get("hosts").and_then(|h| h.as_array()).into_iter().flatten();
let in_a_host = hosts.filter_map(|h| h.get("customer")).next().is_some();
if table.contains_key("customers") || in_a_host {
bail!(
"[[customers]] and a host's customer are no longer read: tag the hosts instead \
(tags = [\"<customer>\"])"
);
}
Ok(())
}

fn validate(cfg: &ServerConfig) -> anyhow::Result<()> {
if !cfg.report_interval.is_positive() {
bail!("report_interval must be positive");
Expand All @@ -185,15 +190,11 @@ fn validate(cfg: &ServerConfig) -> anyhow::Result<()> {
application (an external application, id = \"external/<name>\", for third-party URLs)"
);
}
let customers: BTreeSet<&str> = cfg.customers.iter().map(|c| c.id.as_str()).collect();
let mut ids = BTreeSet::new();
for h in &cfg.hosts {
if h.id.is_empty() || h.id.contains(['/', ':']) || h.id == EXTERNAL || !ids.insert(&h.id) {
bail!("host id {:?} is empty, contains / or :, is \"external\" or is repeated", h.id);
}
if let Some(c) = h.customer.as_ref().filter(|c| !customers.contains(c.as_str())) {
bail!("host {:?} names unknown customer {c:?}", h.id);
}
}
let hashes = cfg
.hosts
Expand Down Expand Up @@ -290,20 +291,16 @@ mod tests {
c.to_string().repeat(64)
}

fn config(hosts: &str) -> String {
format!("[[customers]]\nid = \"acme\"\nname = \"Acme\"\n{hosts}")
}

fn host(id: &str, customer: &str, h: &str) -> String {
format!("[[hosts]]\nid = \"{id}\"\ncustomer = \"{customer}\"\ntoken_sha256 = \"{h}\"\n")
fn host(id: &str, h: &str) -> String {
format!("[[hosts]]\nid = \"{id}\"\ntoken_sha256 = \"{h}\"\n")
}

#[test]
fn valid_configuration_parses_with_defaults() {
let reader = format!("[[readers]]\nname = \"ops\"\ntoken_sha256 = \"{}\"\n", hash('b'));
let mute =
"[[mute]]\nsubject = \"workload:x/legacy/worker\"\ncode = \"WORKLOAD_UNHEALTHY\"\n";
let cfg = parse(&config(&(host("x", "acme", &hash('a')) + &reader + mute))).unwrap();
let cfg = parse(&(host("x", &hash('a')) + &reader + mute)).unwrap();
assert_eq!(cfg.report_interval, SignedDuration::from_secs(60));
assert_eq!((cfg.hosts.len(), cfg.readers.len(), cfg.mute.len()), (1, 1, 1));
}
Expand All @@ -313,23 +310,22 @@ mod tests {
let reader_same =
format!("[[readers]]\nname = \"ops\"\ntoken_sha256 = \"{}\"\n", hash('a'));
let cases = [
config(&(host("x", "acme", &hash('a')) + &host("x", "acme", &hash('b')))),
config(&host("a/b", "acme", &hash('a'))),
config(&host("x", "nobody", &hash('a'))),
config(&host("x", "acme", "short")),
config(&host("x", "acme", &hash('A'))),
config(&(host("x", "acme", &hash('a')) + &reader_same)),
config("[[mute]]\nsubject = \"nonsense\"\ncode = \"WORKLOAD_DOWN\"\n"),
config("[[mute]]\nsubject = \"host:x\"\ncode = \"NOT_A_CODE\"\n"),
format!("report_interval = \"0s\"\n{}", config("")),
(host("x", &hash('a')) + &host("x", &hash('b'))),
host("a/b", &hash('a')),
host("x", "short"),
host("x", &hash('A')),
(host("x", &hash('a')) + &reader_same),
"[[mute]]\nsubject = \"nonsense\"\ncode = \"WORKLOAD_DOWN\"\n".to_string(),
"[[mute]]\nsubject = \"host:x\"\ncode = \"NOT_A_CODE\"\n".to_string(),
"report_interval = \"0s\"\n".to_string(),
];
for (i, text) in cases.iter().enumerate() {
assert!(parse(text).is_err(), "case {i} should be rejected");
}
}

fn with_app(app: &str) -> String {
config(&(host("x", "acme", &hash('a')) + app))
host("x", &hash('a')) + app
}

/// One probe of app x/shop with these fields.
Expand Down Expand Up @@ -367,10 +363,18 @@ mod tests {
}

#[test]
fn customers_are_optional_and_endpoints_point_to_apps() {
let bare = format!("[[hosts]]\nid = \"x\"\ntoken_sha256 = \"{}\"\n", hash('a'));
assert!(parse(&bare).unwrap().hosts[0].customer.is_none());
let old = with_app("[[endpoints]]\nurl = \"https://a.example/\"\ncustomer = \"acme\"\n");
fn customers_and_endpoints_from_older_versions_are_named() {
for old in [
(format!("[[customers]]\nid = \"acme\"\nname = \"Acme\"\n{}", host("x", &hash('a')))),
format!(
"[[hosts]]\nid = \"x\"\ncustomer = \"acme\"\ntoken_sha256 = \"{}\"\n",
hash('a')
),
] {
let err = format!("{:#}", parse(&old).unwrap_err());
assert!(err.contains("customers") && err.contains("tags"), "{err}");
}
let old = with_app("[[endpoints]]\nurl = \"https://a.example/\"\n");
let err = format!("{:#}", parse(&old).unwrap_err());
assert!(err.contains("[[apps.probes]]") && err.contains("external"), "{err}");
}
Expand Down Expand Up @@ -404,8 +408,7 @@ mod tests {
with_app("[[apps]]\nid = \"y/shop\"\n"),
with_app("[[apps]]\nid = \"x/shop\"\n[[apps]]\nid = \"x/shop\"\n"),
with_app("[[apps]]\nid = \"external/partner\"\n"),
config(&host("external", "acme", &hash('a'))),
config(&host("x", "nobody", &hash('a'))),
host("external", &hash('a')),
with_app("[[mute]]\nsubject = \"queue:mail\"\ncode = \"WORKLOAD_DOWN\"\n"),
probe("url = \"not a url\""),
probe("url = \"ftp://shop.example.com/\""),
Expand Down Expand Up @@ -440,9 +443,9 @@ mod tests {
fn load_reads_and_validates_a_file() {
let path =
std::env::temp_dir().join(format!("skym-server-test-{}.toml", std::process::id()));
std::fs::write(&path, config(&host("x", "acme", &hash('a')))).unwrap();
std::fs::write(&path, host("x", &hash('a'))).unwrap();
assert_eq!(load(&path).unwrap().hosts[0].id, "x");
std::fs::write(&path, config(&host("x", "nobody", &hash('a')))).unwrap();
std::fs::write(&path, host("a/b", &hash('a'))).unwrap();
assert!(load(&path).is_err());
std::fs::remove_file(&path).unwrap();
assert!(load(&path).is_err(), "a missing file is an error");
Expand Down
Loading
Loading