This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 22:53:33 +02:00
1 parent 174389e5b1
commit e6604e41fc
73 files changed
+1240 -201

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
use super::Store;
use crate::config::Config;
use anyhow::{ensure, Context, Result};
use serde::Serialize;
use sqlx::Row;
pub(super) const CONFIG_HISTORY_CAPACITY: i64 = 512;
#[derive(Debug, Clone, Serialize)]
pub struct ConfigurationRevision {
pub revision: u64,
pub time: i64,
pub actor: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfigurationHistoryDetail {
pub revision: u64,
pub time: i64,
pub actor: String,
pub config: Config,
pub previous_revision: Option<u64>,
pub previous_config: Option<Config>,
}
impl Store {
pub async fn configuration_history(&self, limit: usize) -> Result<Vec<ConfigurationRevision>> {
let limit = i64::try_from(limit.min(CONFIG_HISTORY_CAPACITY as usize))?;
let rows = sqlx::query(
"SELECT revision, time, actor FROM configuration_history ORDER BY revision DESC LIMIT ?",
)
.bind(limit)
.fetch_all(&self.pool)
.await?;
rows.into_iter()
.map(|row| {
Ok(ConfigurationRevision {
revision: row.try_get::<i64, _>("revision")?.try_into()?,
time: row.try_get("time")?,
actor: row.try_get("actor")?,
})
})
.collect()
}
pub async fn configuration_revision(
&self,
revision: u64,
) -> Result<Option<ConfigurationHistoryDetail>> {
let revision_i64: i64 = revision.try_into().context("revision overflow")?;
let Some(row) = sqlx::query(
"SELECT revision, time, actor, document FROM configuration_history WHERE revision = ?",
)
.bind(revision_i64)
.fetch_optional(&self.pool)
.await? else {
return Ok(None);
};
let document: String = row.try_get("document")?;
ensure!(
document.len() <= crate::config::MAX_CONFIG_BYTES,
"stored configuration history exceeds size limit"
);
let previous = sqlx::query(
"SELECT revision, document FROM configuration_history WHERE revision < ? ORDER BY revision DESC LIMIT 1",
)
.bind(revision_i64)
.fetch_optional(&self.pool)
.await?;
let (previous_revision, previous_config) = if let Some(previous) = previous {
let previous_document: String = previous.try_get("document")?;
ensure!(
previous_document.len() <= crate::config::MAX_CONFIG_BYTES,
"stored configuration history exceeds size limit"
);
(
Some(previous.try_get::<i64, _>("revision")?.try_into()?),
Some(serde_json::from_str(&previous_document).context(
"invalid previous configuration history document",
)?),
)
} else {
(None, None)
};
Ok(Some(ConfigurationHistoryDetail {
revision: row.try_get::<i64, _>("revision")?.try_into()?,
time: row.try_get("time")?,
actor: row.try_get("actor")?,
config: serde_json::from_str(&document)
.context("invalid configuration history document")?,
previous_revision,
previous_config,
}))
}
}
+22
View File
@@ -36,6 +36,28 @@ mod tests {
assert_eq!(data.config.transform.max_inflight, 64);
reopened.close().await;
}
#[tokio::test]
async fn configuration_history_keeps_revision_documents_and_actor() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("history.sqlite3");
let store = Store::open(&file, &Config::default()).await.unwrap();
let mut first = Config::default();
first.server_header.value = "first".into();
assert!(store.replace_as(0, &first, "alice").await.unwrap());
let mut second = first.clone();
second.server_header.value = "second".into();
assert!(store.replace_as(1, &second, "bob").await.unwrap());
let history = store.configuration_history(10).await.unwrap();
assert_eq!(history[0].revision, 2);
assert_eq!(history[0].actor, "bob");
assert_eq!(history[1].revision, 1);
let detail = store.configuration_revision(2).await.unwrap().unwrap();
assert_eq!(detail.config.server_header.value, "second");
assert_eq!(detail.previous_revision, Some(1));
assert_eq!(detail.previous_config.unwrap().server_header.value, "first");
store.close().await;
}
#[tokio::test]
async fn certificate_renewal_observations_survive_reopen_in_schema_v3() {
let dir = tempfile::tempdir().unwrap();