use std::path::{Path, PathBuf};
use keel_core_api::policy::{JournalLocation, Policy};
use keel_journal::{DailyStats, DiscoveryStore, SystemClock, TargetStats};
pub fn keel_toml(project: &Path) -> PathBuf {
project.join("keel.toml")
}
pub fn discovery_db(project: &Path) -> PathBuf {
project.join(".keel").join("discovery.db")
}
pub fn journal_db(project: &Path) -> PathBuf {
project.join(".keel").join("journal.db")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JournalBackendKind {
Sqlite,
Postgres,
}
impl JournalBackendKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Sqlite => "sqlite",
Self::Postgres => "postgres",
}
}
}
#[derive(Debug, Clone)]
pub struct ResolvedJournal {
pub backend: JournalBackendKind,
pub path: PathBuf,
pub display: String,
pub from_policy: bool,
}
pub fn resolved_journal(project: &Path) -> ResolvedJournal {
policy_journal(project).map_or_else(
|| ResolvedJournal {
backend: JournalBackendKind::Sqlite,
path: journal_db(project),
display: ".keel/journal.db".to_owned(),
from_policy: false,
},
|location| from_location(project, &location),
)
}
fn from_location(project: &Path, location: &JournalLocation) -> ResolvedJournal {
match location.0.strip_prefix("file:") {
Some(raw) => {
let file = Path::new(raw);
let path = if file.is_absolute() {
file.to_owned()
} else {
project.join(file)
};
ResolvedJournal {
backend: JournalBackendKind::Sqlite,
path,
display: raw.to_owned(),
from_policy: true,
}
}
None => ResolvedJournal {
backend: JournalBackendKind::Postgres,
path: journal_db(project),
display: redact_postgres(&location.0),
from_policy: true,
},
}
}
pub(crate) fn load_policy(project: &Path) -> Option<Policy> {
let text = std::fs::read_to_string(keel_toml(project)).ok()?;
let toml_value: toml::Value = text.parse().ok()?;
let json = serde_json::to_value(&toml_value).ok()?;
serde_json::from_value(json).ok()
}
fn policy_journal(project: &Path) -> Option<JournalLocation> {
load_policy(project)?.journal
}
fn redact_postgres(url: &str) -> String {
let Some(rest) = url.strip_prefix("postgres://") else {
return url.to_owned();
};
let path_start = rest.find('/').unwrap_or(rest.len());
let (authority, tail) = rest.split_at(path_start);
match authority.rfind('@') {
Some(at) => format!("postgres://\u{2026}@{}{tail}", &authority[at + 1..]),
None => url.to_owned(),
}
}
pub fn read_discovery(project: &Path) -> Result<Vec<TargetStats>, String> {
let path = discovery_db(project);
if !path.exists() {
return Ok(Vec::new());
}
let store = DiscoveryStore::open_readonly(&path, SystemClock)
.map_err(|e| format!("could not open {}: {e}", path.display()))?;
store
.snapshot()
.map_err(|e| format!("could not read {}: {e}", path.display()))
}
pub fn read_discovery_daily(project: &Path) -> Result<Vec<DailyStats>, String> {
let path = discovery_db(project);
if !path.exists() {
return Ok(Vec::new());
}
let store = DiscoveryStore::open_readonly(&path, SystemClock)
.map_err(|e| format!("could not open {}: {e}", path.display()))?;
store
.daily_snapshot()
.map_err(|e| format!("could not read {}: {e}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
fn project_with_policy(toml: &str) -> tempfile::TempDir {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join("keel.toml"), toml).unwrap();
dir
}
#[test]
fn no_policy_resolves_to_the_default() {
let dir = tempfile::TempDir::new().unwrap();
let r = resolved_journal(dir.path());
assert_eq!(r.backend, JournalBackendKind::Sqlite);
assert_eq!(r.path, dir.path().join(".keel").join("journal.db"));
assert_eq!(r.display, ".keel/journal.db");
assert!(!r.from_policy);
}
#[test]
fn relative_file_location_resolves_against_the_project() {
let dir = project_with_policy("journal = \"file:custom/j.db\"\n");
let r = resolved_journal(dir.path());
assert_eq!(r.backend, JournalBackendKind::Sqlite);
assert_eq!(r.path, dir.path().join("custom").join("j.db"));
assert_eq!(r.display, "custom/j.db");
assert!(r.from_policy);
}
#[test]
fn absolute_file_location_passes_through() {
let target = tempfile::TempDir::new().unwrap();
let abs = target.path().join("j.db");
let dir = project_with_policy(&format!("journal = \"file:{}\"\n", abs.display()));
let r = resolved_journal(dir.path());
assert_eq!(r.path, abs);
assert_eq!(r.display, abs.display().to_string());
}
#[test]
fn postgres_location_is_reported_with_credentials_redacted() {
let dir =
project_with_policy("journal = \"postgres://keel:sekrit@db.internal:5432/keel\"\n");
let r = resolved_journal(dir.path());
assert_eq!(r.backend, JournalBackendKind::Postgres);
assert!(r.from_policy);
assert_eq!(r.display, "postgres://\u{2026}@db.internal:5432/keel");
assert!(!r.display.contains("sekrit"), "credentials never printed");
assert_eq!(r.path, dir.path().join(".keel").join("journal.db"));
}
#[test]
fn a_password_containing_at_is_fully_redacted() {
let dir =
project_with_policy("journal = \"postgres://user:p@ssw0rd@db.internal:5432/keel\"\n");
let r = resolved_journal(dir.path());
assert_eq!(r.display, "postgres://\u{2026}@db.internal:5432/keel");
assert!(
!r.display.contains("ssw0rd"),
"no credential fragment leaks"
);
assert!(!r.display.contains("p@ssw0rd"));
}
#[test]
fn a_broken_policy_falls_back_to_the_default() {
let dir = project_with_policy("journal = [this is not toml\n");
let r = resolved_journal(dir.path());
assert_eq!(r.backend, JournalBackendKind::Sqlite);
assert!(!r.from_policy);
assert_eq!(r.display, ".keel/journal.db");
}
}