use std::collections::{BTreeMap, BTreeSet};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
use tokio_postgres::error::SqlState;
use tokio_postgres::types::ToSql;
use tokio_postgres::{Client, Config, Row, Transaction};
use super::hydration::{self, HydrationLimits};
use super::rows;
use super::schema::{self, Baseline, MINIMUM_SERVER_VERSION_NUM, SchemaStatus};
use super::{ControlPlaneError, ControlPlaneStore, StatusProbeAdmission};
#[cfg(test)]
use crate::availability::EvidenceClear;
use crate::availability::store::{
self as availability_store, EvidenceWrite, ObservationSlot, ObservationStore, StoredObservation,
};
use crate::availability::{AvailabilityKey, DiscoveryObservation, ScopeRef, TargetRef};
use crate::backends::{Capabilities, Capability};
use crate::desired_state::{
AccessDenial, Action, AuditEvent, Credential, DenialPage, DenialReason, Directory,
IntegrityError, LoadedRevision, Mutation, ProjectId, ResourceRef, ResourceVersion,
ResourceVersionNumber, RevisionCandidate, RevisionId, RevisionManifest, SerializerVersion,
Surface, Tenancy, TenantId, Uuid7Generator,
};
const BACKEND: &str = "postgres";
#[derive(Debug, Clone)]
pub struct ControlPlaneSettings {
pub schema: Option<String>,
pub migrate: bool,
pub connect_timeout: Duration,
pub operation_timeout: Duration,
pub idempotency_retention: Duration,
pub hydration: HydrationLimits,
}
impl ControlPlaneSettings {
pub fn from_config(control_plane: &crate::config::ControlPlane) -> Self {
Self {
schema: control_plane
.schema
.as_deref()
.map(str::trim)
.filter(|schema| !schema.is_empty())
.map(str::to_owned),
migrate: control_plane.migrate,
connect_timeout: Duration::from_millis(control_plane.connect_timeout_ms),
operation_timeout: Duration::from_millis(control_plane.operation_timeout_ms),
..Self::default()
}
}
pub fn for_maintenance(control_plane: &crate::config::ControlPlane) -> Self {
Self {
migrate: false,
..Self::from_config(control_plane)
}
}
pub(crate) fn status_probe_timeout(&self, queued_operations: usize) -> Duration {
let operations = u32::try_from(queued_operations.saturating_add(1)).unwrap_or(u32::MAX);
self.operation_timeout
.saturating_mul(operations)
.saturating_add(self.connect_timeout)
.max(Duration::from_secs(2))
}
}
impl Default for ControlPlaneSettings {
fn default() -> Self {
Self {
schema: None,
migrate: true,
connect_timeout: Duration::from_secs(10),
operation_timeout: Duration::from_secs(30),
idempotency_retention: Duration::from_secs(24 * 60 * 60),
hydration: HydrationLimits::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Adoption {
Recorded {
versions: Vec<i32>,
status: SchemaStatus,
},
AlreadyRecorded { status: SchemaStatus },
}
pub struct PostgresControlPlane {
config: Config,
settings: ControlPlaneSettings,
search_path: Option<String>,
ids: Uuid7Generator,
client: tokio::sync::Mutex<Option<Client>>,
pending_operations: Arc<AtomicUsize>,
}
impl std::fmt::Debug for PostgresControlPlane {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PostgresControlPlane")
.field("schema", &self.search_path)
.field("migrate", &self.settings.migrate)
.finish_non_exhaustive()
}
}
impl PostgresControlPlane {
pub async fn connect(
dsn: &str,
settings: ControlPlaneSettings,
) -> Result<Self, ControlPlaneError> {
let (store, mut client) = Self::open(dsn, settings).await?;
store.prepare_schema(&mut client).await?;
*store.client.lock().await = Some(client);
Ok(store)
}
pub async fn connect_for_maintenance(
dsn: &str,
settings: ControlPlaneSettings,
) -> Result<Self, ControlPlaneError> {
let (store, client) = Self::open(dsn, settings).await?;
*store.client.lock().await = Some(client);
Ok(store)
}
async fn open(
dsn: &str,
settings: ControlPlaneSettings,
) -> Result<(Self, Client), ControlPlaneError> {
let mut config: Config = dsn.parse().map_err(|error| {
denied(format!(
"the control-plane DSN could not be parsed: {error}"
))
})?;
config.connect_timeout(settings.connect_timeout);
config.application_name(crate::telemetry::SERVICE_NAME);
let search_path = settings
.schema
.as_deref()
.map(|schema| {
crate::usage::validate_table_name(schema).map_err(denied)?;
if schema.contains('.') {
return Err(denied(format!(
"`{schema}` is not a single unqualified schema name"
)));
}
Ok(schema.to_owned())
})
.transpose()?;
let store = Self {
config,
settings,
search_path,
ids: Uuid7Generator::new(),
client: tokio::sync::Mutex::new(None),
pending_operations: Arc::new(AtomicUsize::new(0)),
};
let client = tokio::time::timeout(store.settings.connect_timeout, store.connect_client())
.await
.map_err(|_| ControlPlaneError::Unavailable {
backend: BACKEND,
message: "connection timed out".to_owned(),
})?
.map_err(|error| unavailable("connect", &error))?;
store.check_server_version(&client).await?;
Ok((store, client))
}
pub async fn apply_migrations(&self) -> Result<Vec<i32>, ControlPlaneError> {
self.run(None, |client| {
Box::pin(async move {
let transaction = client
.build_transaction()
.isolation_level(tokio_postgres::IsolationLevel::ReadCommitted)
.start()
.await
.map_err(|error| unavailable("begin schema transaction", &error))?;
transaction
.query_one("SELECT pg_advisory_xact_lock($1::bigint)", &[&SCHEMA_LOCK])
.await
.map_err(|error| unavailable("acquire schema lock", &error))?;
let status = schema::status(&transaction)
.await
.map_err(|error| unavailable("read schema status", &error))?;
if status.is_current() {
return Ok(Vec::new());
}
if !status.is_migratable() {
return Err(denied(status.to_string()));
}
let pending = schema::pending(&status);
schema::migrate(&transaction, &status)
.await
.map_err(|error| migration_refused_or_unavailable(&error))?;
let migrated = schema::status(&transaction)
.await
.map_err(|error| unavailable("re-read schema status", &error))?;
if !migrated.is_current() {
return Err(denied(format!(
"migrations were applied but the schema is still not current: {migrated}"
)));
}
transaction
.commit()
.await
.map_err(|error| unavailable("commit schema transaction", &error))?;
Ok(pending)
})
})
.await
}
pub async fn adopt_ledger(&self) -> Result<Adoption, ControlPlaneError> {
self.run(None, |client| {
Box::pin(async move {
let transaction = client
.build_transaction()
.isolation_level(tokio_postgres::IsolationLevel::ReadCommitted)
.start()
.await
.map_err(|error| unavailable("begin schema transaction", &error))?;
transaction
.query_one("SELECT pg_advisory_xact_lock($1::bigint)", &[&SCHEMA_LOCK])
.await
.map_err(|error| unavailable("acquire schema lock", &error))?;
let status = schema::status(&transaction)
.await
.map_err(|error| unavailable("read schema status", &error))?;
match status {
SchemaStatus::Current { .. } | SchemaStatus::Behind { .. } => {
let _ = transaction.rollback().await;
return Ok(Adoption::AlreadyRecorded { status });
}
SchemaStatus::Unrecorded => {}
refused => {
return Err(denied(format!(
"adoption reconciles an existing but empty migration ledger, and this \
database has something else: {refused}"
)));
}
}
let baseline = schema::baseline(&transaction).await.map_err(|error| {
refused_or_unavailable("reconciling the empty ledger", &error)
})?;
let versions = match baseline {
Baseline::Applied { versions } => versions,
Baseline::Nothing => {
let searched: String = transaction
.query_one("SELECT current_schema()", &[])
.await
.map_err(|error| {
refused_or_unavailable("reading the current schema", &error)
})?
.get(0);
return Err(denied(format!(
"no shipped migration's tables are present in schema `{searched}`, so \
this database has no applied schema to adopt there; drop the empty \
`{}` table and run `axond migrate apply`",
schema::MIGRATION_TABLE
)));
}
Baseline::Inconsistent { message } => return Err(denied(message)),
};
schema::record_baseline(&transaction, &versions)
.await
.map_err(|error| {
refused_or_unavailable("recording the adopted baseline", &error)
})?;
let adopted = schema::status(&transaction)
.await
.map_err(|error| unavailable("re-read schema status", &error))?;
if !matches!(
adopted,
SchemaStatus::Current { .. } | SchemaStatus::Behind { .. }
) {
return Err(denied(format!(
"a baseline was recorded but the schema is still not one this build can \
extend: {adopted}"
)));
}
transaction
.commit()
.await
.map_err(|error| unavailable("commit schema transaction", &error))?;
Ok(Adoption::Recorded {
versions,
status: adopted,
})
})
})
.await
}
pub async fn schema_status(&self) -> Result<SchemaStatus, ControlPlaneError> {
self.run(None, |client| {
Box::pin(async move {
let transaction = client
.build_transaction()
.read_only(true)
.start()
.await
.map_err(|error| unavailable("begin read-only schema read", &error))?;
let status = schema::status(&transaction)
.await
.map_err(|error| unavailable("read schema status", &error))?;
let _ = transaction.rollback().await;
Ok(status)
})
})
.await
}
async fn check_server_version(&self, client: &Client) -> Result<(), ControlPlaneError> {
let reported: String = client
.query_one("SELECT current_setting('server_version_num')", &[])
.await
.map_err(|error| unavailable("read server version", &error))?
.get(0);
let version: i32 = reported.parse().map_err(|_| {
denied(format!(
"the server reported version `{reported}`, which is not a number"
))
})?;
if version < MINIMUM_SERVER_VERSION_NUM {
return Err(denied(format!(
"the control-plane journal requires PostgreSQL {}, but the server is {}",
MINIMUM_SERVER_VERSION_NUM / 10_000,
version / 10_000
)));
}
Ok(())
}
async fn prepare_schema(&self, client: &mut Client) -> Result<(), ControlPlaneError> {
let transaction = client
.build_transaction()
.isolation_level(tokio_postgres::IsolationLevel::ReadCommitted)
.start()
.await
.map_err(|error| unavailable("begin schema transaction", &error))?;
transaction
.query_one("SELECT pg_advisory_xact_lock($1::bigint)", &[&SCHEMA_LOCK])
.await
.map_err(|error| unavailable("acquire schema lock", &error))?;
let status = schema::status(&transaction)
.await
.map_err(|error| unavailable("read schema status", &error))?;
if !status.is_current() {
if !status.is_migratable() {
return Err(denied(status.to_string()));
}
if !self.settings.migrate {
return Err(denied(format!(
"{status}, and this store is configured not to migrate"
)));
}
schema::migrate(&transaction, &status)
.await
.map_err(|error| migration_refused_or_unavailable(&error))?;
let migrated = schema::status(&transaction)
.await
.map_err(|error| unavailable("re-read schema status", &error))?;
if !migrated.is_current() {
return Err(denied(format!(
"migrations were applied but the schema is still not current: {migrated}"
)));
}
}
transaction
.commit()
.await
.map_err(|error| unavailable("commit schema transaction", &error))?;
Ok(())
}
async fn connect_client(&self) -> Result<Client, tokio_postgres::Error> {
let (client, connection) = self.config.connect(crate::usage::tls_connector()).await?;
tokio::spawn(async move {
if let Err(error) = connection.await {
tracing::warn!(%error, "postgres control-plane connection closed");
}
});
if let Some(schema) = &self.search_path {
client
.batch_execute(&format!("SET search_path TO {schema}"))
.await?;
}
Ok(client)
}
async fn run<T>(
&self,
admission: Option<StatusProbeAdmission>,
operation: impl for<'a> FnOnce(
&'a mut Client,
) -> Pin<
Box<dyn Future<Output = Result<T, ControlPlaneError>> + Send + 'a>,
>,
) -> Result<T, ControlPlaneError> {
let _pending = admission
.unwrap_or_else(|| StatusProbeAdmission::pending(Arc::clone(&self.pending_operations)));
let mut guard = self.client.lock().await;
if guard.as_ref().is_none_or(Client::is_closed) {
*guard = Some(
self.connect_client()
.await
.map_err(|error| unavailable("reconnect", &error))?,
);
}
let result = tokio::time::timeout(
self.settings.operation_timeout,
operation(guard.as_mut().expect("connected")),
)
.await
.map_err(|_| ControlPlaneError::Unavailable {
backend: BACKEND,
message: "operation timed out".to_owned(),
})
.and_then(|result| result);
if matches!(result, Err(ControlPlaneError::Unavailable { .. })) {
*guard = None;
}
result
}
}
const SCHEMA_LOCK: i64 = 0x1a20_de5c_0de5_1a11u64 as i64;
fn denied(message: impl Into<String>) -> ControlPlaneError {
ControlPlaneError::Denied {
backend: BACKEND,
message: message.into(),
}
}
fn migration_refused_or_unavailable(error: &tokio_postgres::Error) -> ControlPlaneError {
refused_or_unavailable("applying migrations", error)
}
fn refused_or_unavailable(operation: &str, error: &tokio_postgres::Error) -> ControlPlaneError {
const TRANSIENT: [&str; 6] = ["08", "40", "53", "55", "57", "58"];
let Some(db) = error.as_db_error() else {
return unavailable(operation, error);
};
let code = db.code().code();
if code
.get(..2)
.is_some_and(|class| TRANSIENT.contains(&class))
{
return unavailable(operation, error);
}
denied(format!(
"{operation} failed: {} (SQLSTATE {code}); the server rejected the statement, so no retry \
clears it — check that the configured schema exists and that the role may read and \
create objects in it",
db.message()
))
}
fn observation_write_failure(operation: &str, error: &tokio_postgres::Error) -> ControlPlaneError {
let Some(db) = error.as_db_error() else {
return unavailable(operation, error);
};
if is_permanent_observation_sqlstate(db.code()) {
return denied(format!(
"{operation} was permanently refused: {} (SQLSTATE {}); no retry of the same evidence clears it",
db.message(),
db.code().code()
));
}
unavailable(operation, error)
}
fn is_permanent_observation_sqlstate(code: &SqlState) -> bool {
matches!(code.code().get(..2), Some("22" | "23"))
}
pub(super) fn unavailable(operation: &str, error: &tokio_postgres::Error) -> ControlPlaneError {
let message = match error.as_db_error() {
Some(db) => format!(
"{operation} failed: {} (SQLSTATE {})",
db.message(),
db.code().code()
),
None => format!("{operation} failed: {error}"),
};
ControlPlaneError::Unavailable {
backend: BACKEND,
message,
}
}
pub(super) fn corrupt_storage(detail: impl Into<String>) -> ControlPlaneError {
ControlPlaneError::CorruptStorage {
detail: detail.into(),
}
}
fn journal_now() -> SystemTime {
let now = SystemTime::now();
match now.duration_since(UNIX_EPOCH) {
Ok(since) => UNIX_EPOCH + Duration::from_micros(since.as_micros() as u64),
Err(_) => now,
}
}
fn version_text(value: ResourceVersionNumber) -> i64 {
i64::try_from(value.get()).unwrap_or(i64::MAX)
}
fn size_bytes(value: u64) -> i64 {
i64::try_from(value).unwrap_or(i64::MAX)
}
#[async_trait]
impl ControlPlaneStore for PostgresControlPlane {
fn name(&self) -> &'static str {
BACKEND
}
fn capabilities(&self) -> Capabilities {
Capabilities::new(&[
Capability::TransactionalWrites,
Capability::OptimisticConcurrency,
Capability::IdempotentWrites,
Capability::TransactionalAudit,
])
}
fn status_probe_admission(&self) -> Option<StatusProbeAdmission> {
let queued_operations = self.pending_operations.fetch_add(1, Ordering::AcqRel);
Some(StatusProbeAdmission::new(
self.settings.status_probe_timeout(queued_operations),
Arc::clone(&self.pending_operations),
))
}
async fn health(&self) -> Result<(), ControlPlaneError> {
self.health_with_status_probe(None).await
}
async fn health_with_status_probe(
&self,
admission: Option<StatusProbeAdmission>,
) -> Result<(), ControlPlaneError> {
self.run(admission, |client| {
Box::pin(async move {
client
.query_one("SELECT 1", &[])
.await
.map(|_| ())
.map_err(|error| unavailable("health check", &error))
})
})
.await
}
async fn desired_revision(&self) -> Result<Option<RevisionId>, ControlPlaneError> {
self.run(None, |client| {
Box::pin(async move {
let row = client
.query_opt(
"SELECT revision_id FROM axond_cp_head WHERE singleton",
&[],
)
.await
.map_err(|error| unavailable("read desired revision", &error))?;
let Some(row) = row else {
return Err(corrupt_storage(
"the control-plane head row is missing; the schema was modified out of band",
));
};
let id: Option<String> = row.get(0);
id.map(|text| {
rows::revision_id(&text).map_err(|error| {
corrupt_storage(format!("the desired revision is unreadable: {error}"))
})
})
.transpose()
})
})
.await
}
async fn load_manifest(&self, id: RevisionId) -> Result<RevisionManifest, ControlPlaneError> {
let limits = self.settings.hydration;
self.run(None, move |client| {
Box::pin(async move {
let transaction = client
.transaction()
.await
.map_err(|error| unavailable("begin manifest read", &error))?;
let manifest = hydration::manifest(&transaction, id, &limits).await;
let _ = transaction.rollback().await;
manifest
})
})
.await
}
async fn load_revision(&self, id: RevisionId) -> Result<LoadedRevision, ControlPlaneError> {
let limits = self.settings.hydration;
self.run(None, move |client| {
Box::pin(async move {
let transaction = client
.transaction()
.await
.map_err(|error| unavailable("begin revision read", &error))?;
let loaded = hydration::revision(&transaction, id, &limits).await;
let _ = transaction.rollback().await;
loaded
})
})
.await
}
async fn load_desired_revision(&self) -> Result<Option<LoadedRevision>, ControlPlaneError> {
let limits = self.settings.hydration;
self.run(None, move |client| {
Box::pin(async move {
let transaction = client
.transaction()
.await
.map_err(|error| unavailable("begin desired-revision read", &error))?;
let loaded = hydration::desired(&transaction, &limits).await;
let _ = transaction.rollback().await;
loaded
})
})
.await
}
async fn publish_revision(
&self,
candidate: RevisionCandidate,
) -> Result<RevisionManifest, ControlPlaneError> {
let checksum = candidate.validated_checksum()?;
let caller_scope = rows::caller_scope(&candidate.mutation.actor)
.map_err(|error| ControlPlaneError::Invalid(error.into()))?;
let retention = self.settings.idempotency_retention;
let limits = self.settings.hydration;
let id = RevisionId::new(self.ids.next());
self.run(None, move |client| {
Box::pin(async move {
let transaction = client
.transaction()
.await
.map_err(|error| unavailable("begin publication", &error))?;
match publish(
&transaction,
id,
&candidate,
&checksum.to_string(),
&caller_scope,
retention,
&limits,
)
.await
{
Ok(Published::Manifest(manifest)) => {
transaction
.commit()
.await
.map_err(|error| unavailable("commit publication", &error))?;
Ok(manifest)
}
Ok(Published::Replayed(manifest)) => {
transaction
.commit()
.await
.map_err(|error| unavailable("commit replay", &error))?;
Ok(manifest)
}
Err(error) => {
let _ = transaction.rollback().await;
Err(error)
}
}
})
})
.await
}
async fn audit_trail(&self, id: RevisionId) -> Result<Vec<AuditEvent>, ControlPlaneError> {
self.run(None, move |client| {
Box::pin(async move {
let revision = client
.query_opt(
"SELECT 1 FROM axond_cp_revision WHERE revision_id = $1",
&[&id.to_string()],
)
.await
.map_err(|error| unavailable("read revision", &error))?;
if revision.is_none() {
return Err(ControlPlaneError::RevisionNotFound(id));
}
let rows = client
.query(
"SELECT audit_event_id, mutation_id, actor_kind, actor_issuer, \
actor_subject, actor_component, event_kind, target_kind, target_id, \
target_version, summary, recorded_at, actor_tenant_id, \
actor_principal_id \
FROM axond_cp_audit_event WHERE revision_id = $1 \
ORDER BY recorded_at DESC, audit_event_id DESC",
&[&id.to_string()],
)
.await
.map_err(|error| unavailable("read audit trail", &error))?;
rows.iter()
.map(|row| {
audit_event(row).map_err(|error| ControlPlaneError::corrupt(id, error))
})
.collect()
})
})
.await
}
async fn record_denial(&self, denial: &AccessDenial) -> Result<(), ControlPlaneError> {
let denial = denial.clone();
self.run(None, move |client| {
Box::pin(async move {
let actor = rows::actor_columns(&denial.actor);
let scope = rows::scope_columns(&denial.scope);
client
.execute(
"INSERT INTO axond_cp_access_denial \
(denial_id, actor_kind, actor_issuer, actor_subject, actor_component, \
actor_tenant_id, actor_principal_id, surface, action, scope_kind, \
tenant_id, project_id, reason, recorded_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) \
ON CONFLICT (denial_id) DO NOTHING",
&[
&denial.id.to_string(),
&actor.kind,
&actor.issuer,
&actor.subject,
&actor.component,
&actor.tenant,
&actor.principal,
&denial.surface.as_str(),
&denial.action.as_str(),
&scope.kind,
&scope.tenant,
&scope.project,
&denial.reason.as_str(),
&denial.recorded_at,
],
)
.await
.map_err(|error| unavailable("record denied action", &error))?;
Ok(())
})
})
.await
}
async fn denials(
&self,
page: &DenialPage,
limit: usize,
) -> Result<Vec<AccessDenial>, ControlPlaneError> {
let tenant = page.tenant().map(|tenant| tenant.to_string());
let limit = i64::try_from(limit.clamp(1, 1_000)).unwrap_or(1_000);
self.run(None, move |client| {
Box::pin(async move {
let rows = client
.query(
"SELECT denial_id, actor_kind, actor_issuer, actor_subject, \
actor_component, actor_tenant_id, actor_principal_id, surface, action, \
scope_kind, tenant_id, project_id, reason, recorded_at \
FROM axond_cp_access_denial \
WHERE tenant_id IS NOT DISTINCT FROM $1 \
ORDER BY recorded_at DESC, denial_id DESC LIMIT $2",
&[&tenant, &limit],
)
.await
.map_err(|error| unavailable("read denied actions", &error))?;
rows.iter()
.map(|row| {
access_denial(row).map_err(|error| {
corrupt_storage(format!("a denied action is unreadable: {error}"))
})
})
.collect()
})
})
.await
}
}
#[async_trait]
impl ObservationStore for PostgresControlPlane {
async fn load(
&self,
scope: Option<ScopeRef>,
) -> Result<Vec<StoredObservation>, ControlPlaneError> {
let tenant = scope.map(|scope| scope.tenant.to_string());
let project = scope.and_then(|scope| scope.project.map(|project| project.to_string()));
self.run(None, move |client| {
Box::pin(async move {
let rows = client
.query(
"SELECT tenant_id, project_id, provider, model, slot, result, \
completeness, source, observed_at, expires_at, definitive_at \
FROM axond_cp_availability_observation \
WHERE ($1::text IS NULL OR tenant_id = $1) \
AND ($1::text IS NULL OR project_id IS NOT DISTINCT FROM $2) \
ORDER BY tenant_id, project_id, provider, model, slot",
&[&tenant, &project],
)
.await
.map_err(|error| unavailable("read discovery observations", &error))?;
rows.iter().map(observation_row).collect()
})
})
.await
}
async fn save(&self, write: &EvidenceWrite) -> Result<(), ControlPlaneError> {
if write.is_empty() {
return Ok(());
}
let rows = write.rows().to_vec();
let cleared = write.cleared().to_vec();
self.run(None, move |client| {
Box::pin(async move {
let transaction = client
.transaction()
.await
.map_err(|error| unavailable("begin an observation write", &error))?;
let mut replaced: BTreeMap<AvailabilityKey, SystemTime> = BTreeMap::new();
for row in &rows {
replaced
.entry(row.key.clone())
.and_modify(|before| *before = (*before).max(row.observation.observed_at))
.or_insert(row.observation.observed_at);
}
for clear in &cleared {
replaced
.entry(clear.key.clone())
.and_modify(|before| *before = (*before).max(clear.before))
.or_insert(clear.before);
}
let mut behind: BTreeSet<AvailabilityKey> = BTreeSet::new();
for (key, before) in &replaced {
let tenant = key.scope.tenant.to_string();
let project = key.scope.project.map(|project| project.to_string());
let provider = key.target.provider.as_str();
let model = key.target.model.as_str();
let held = transaction
.query(
"SELECT observed_at FROM axond_cp_availability_observation \
WHERE tenant_id = $1 AND project_id IS NOT DISTINCT FROM $2 \
AND provider = $3 AND model = $4 FOR UPDATE",
&[&tenant, &project, &provider, &model],
)
.await
.map_err(|error| {
observation_write_failure("read evidence being replaced", &error)
})?;
let newest = held
.iter()
.map(|row| row.get::<_, SystemTime>(0))
.max()
.unwrap_or(SystemTime::UNIX_EPOCH);
if newest > *before {
behind.insert(key.clone());
continue;
}
transaction
.execute(
"DELETE FROM axond_cp_availability_observation \
WHERE tenant_id = $1 AND project_id IS NOT DISTINCT FROM $2 \
AND provider = $3 AND model = $4 AND observed_at <= $5",
&[&tenant, &project, &provider, &model, before],
)
.await
.map_err(|error| {
observation_write_failure("replace discovery evidence", &error)
})?;
}
for row in rows.iter().filter(|row| !behind.contains(&row.key)) {
transaction
.execute(
"INSERT INTO axond_cp_availability_observation \
(tenant_id, project_id, provider, model, slot, result, \
completeness, source, observed_at, expires_at, definitive_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) \
ON CONFLICT DO NOTHING",
&[
&row.key.scope.tenant.to_string(),
&row.key.scope.project.map(|project| project.to_string()),
&row.key.target.provider.as_str(),
&row.key.target.model.as_str(),
&row.slot.as_str(),
&row.observation.result.as_str(),
&row.observation.completeness.as_str(),
&row.observation.source.as_str(),
&row.observation.observed_at,
&row.observation.expires_at,
&row.definitive_at,
],
)
.await
.map_err(|error| {
observation_write_failure("record discovery evidence", &error)
})?;
}
transaction
.commit()
.await
.map_err(|error| unavailable("commit discovery evidence", &error))?;
Ok(())
})
})
.await
}
}
fn observation_row(row: &Row) -> Result<StoredObservation, ControlPlaneError> {
let tenant: String = row.get(0);
let project: Option<String> = row.get(1);
let provider: String = row.get(2);
let model: String = row.get(3);
let slot: String = row.get(4);
let result: String = row.get(5);
let completeness: String = row.get(6);
let source: String = row.get(7);
let tenant = TenantId::parse(&tenant)
.map_err(|error| corrupt_storage(format!("an observation names no tenant: {error}")))?;
let project = project
.map(|project| ProjectId::parse(&project))
.transpose()
.map_err(|error| corrupt_storage(format!("an observation names no project: {error}")))?;
let target = TargetRef::parse(&provider, &model)
.map_err(|error| corrupt_storage(format!("an observation names no target: {error}")))?;
let scope = match project {
None => ScopeRef::tenant(tenant),
Some(project) => ScopeRef::project(tenant, project),
};
let key = AvailabilityKey::new(scope, target.clone());
let slot = ObservationSlot::parse(&slot)
.ok_or_else(|| corrupt_storage(format!("`{slot}` is not an observation slot")))?;
let result = availability_store::parse_result(&result)
.ok_or_else(|| corrupt_storage(format!("`{result}` is not a discovery result")))?;
let completeness = availability_store::parse_completeness(&completeness).ok_or_else(|| {
corrupt_storage(format!("`{completeness}` is not a discovery completeness"))
})?;
let source = availability_store::parse_source(&source)
.ok_or_else(|| corrupt_storage(format!("`{source}` is not a discovery source")))?;
let observation = DiscoveryObservation {
scope,
target,
result,
completeness,
source,
observed_at: row.get(8),
expires_at: row.get(9),
detail: None,
};
Ok(StoredObservation {
key,
slot,
observation,
definitive_at: row.get(10),
})
}
enum Published {
Manifest(RevisionManifest),
Replayed(RevisionManifest),
}
async fn publish(
transaction: &Transaction<'_>,
id: RevisionId,
candidate: &RevisionCandidate,
checksum: &str,
caller_scope: &str,
retention: Duration,
limits: &HydrationLimits,
) -> Result<Published, ControlPlaneError> {
let head = transaction
.query_opt(
"SELECT revision_id FROM axond_cp_head WHERE singleton FOR UPDATE",
&[],
)
.await
.map_err(|error| unavailable("lock the head", &error))?
.ok_or_else(|| {
corrupt_storage("the control-plane head row is missing; publication has no anchor")
})?;
let head: Option<String> = head.get(0);
let head = head
.map(|text| {
rows::revision_id(&text).map_err(|error| {
corrupt_storage(format!("the desired revision is unreadable: {error}"))
})
})
.transpose()?;
transaction
.execute(
"DELETE FROM axond_cp_idempotency WHERE expires_at <= now()",
&[],
)
.await
.map_err(|error| unavailable("prune expired idempotency records", &error))?;
let key = candidate.mutation.idempotency_key.as_str().to_owned();
if let Some(record) = transaction
.query_opt(
"SELECT state_checksum, revision_id FROM axond_cp_idempotency \
WHERE caller_scope = $1 AND idempotency_key = $2",
&[&caller_scope, &key],
)
.await
.map_err(|error| unavailable("read idempotency record", &error))?
{
let recorded_checksum: String = record.get(0);
let recorded_revision: String = record.get(1);
let published = rows::revision_id(&recorded_revision).map_err(|error| {
corrupt_storage(format!("an idempotency record is unreadable: {error}"))
})?;
if recorded_checksum != checksum {
return Err(ControlPlaneError::IdempotencyKeyReused {
key: candidate.mutation.idempotency_key.clone(),
published,
});
}
return hydration::manifest(transaction, published, limits)
.await
.map(Published::Replayed);
}
if !candidate.expected.matches(head) {
return Err(ControlPlaneError::Conflict {
expected: candidate.expected,
actual: head,
});
}
for resource in candidate.state.resources() {
assert_version_is_immutable(transaction, resource).await?;
}
let manifest = RevisionManifest::of(id, head, journal_now(), candidate)?;
for blob in candidate.state.blobs() {
transaction
.execute(
"INSERT INTO axond_cp_blob (blob_kind, digest, size_bytes) VALUES ($1, $2, $3) \
ON CONFLICT (blob_kind, digest) DO NOTHING",
&[
&blob.kind.as_str(),
&blob.digest.to_string(),
&size_bytes(blob.size_bytes),
],
)
.await
.map_err(|error| unavailable("write blob reference", &error))?;
}
project_tenancy(transaction, id, candidate).await?;
for resource in candidate.state.resources() {
insert_resource_version(transaction, resource).await?;
}
insert_mutation(transaction, &candidate.mutation).await?;
transaction
.execute(
"INSERT INTO axond_cp_revision \
(revision_id, parent_id, mutation_id, serializer, state_checksum, created_at) \
VALUES ($1, $2, $3, $4, $5, $6)",
&[
&id.to_string(),
&manifest.parent.map(|parent| parent.to_string()),
&manifest.mutation.to_string(),
&manifest.serializer.as_str(),
&checksum,
&manifest.created_at,
],
)
.await
.map_err(|error| {
if is_unique_violation(&error) {
ControlPlaneError::Conflict {
expected: candidate.expected,
actual: head,
}
} else {
unavailable("write revision", &error)
}
})?;
for entry in &manifest.entries {
transaction
.execute(
"INSERT INTO axond_cp_revision_entry \
(revision_id, resource_kind, resource_id, version) VALUES ($1, $2, $3, $4)",
&[
&id.to_string(),
&entry.reference.kind.as_str(),
&entry.reference.id.to_string(),
&version_text(entry.reference.version),
],
)
.await
.map_err(|error| unavailable("write manifest entry", &error))?;
}
for blob in &manifest.blobs {
transaction
.execute(
"INSERT INTO axond_cp_revision_blob (revision_id, blob_kind, digest) \
VALUES ($1, $2, $3)",
&[
&id.to_string(),
&blob.kind.as_str(),
&blob.digest.to_string(),
],
)
.await
.map_err(|error| unavailable("write revision blob", &error))?;
}
insert_audit_event(transaction, id, &candidate.audit).await?;
let expires_at = journal_now() + retention;
let replaced = transaction
.execute(
"INSERT INTO axond_cp_idempotency \
(caller_scope, idempotency_key, state_checksum, revision_id, mutation_id, expires_at) \
VALUES ($1, $2, $3, $4, $5, $6) \
ON CONFLICT (caller_scope, idempotency_key) DO UPDATE SET \
state_checksum = EXCLUDED.state_checksum, revision_id = EXCLUDED.revision_id, \
mutation_id = EXCLUDED.mutation_id, recorded_at = now(), \
expires_at = EXCLUDED.expires_at",
&[
&caller_scope,
&key,
&checksum,
&id.to_string(),
&candidate.mutation.id.to_string(),
&expires_at,
],
)
.await
.map_err(|error| unavailable("write idempotency record", &error))?;
if replaced != 1 {
return Err(corrupt_storage(
"the idempotency record for this caller and key was neither written nor replayed",
));
}
transaction
.execute(
"UPDATE axond_cp_head SET revision_id = $1, updated_at = now() WHERE singleton",
&[&id.to_string()],
)
.await
.map_err(|error| unavailable("advance the head", &error))?;
Ok(Published::Manifest(manifest))
}
async fn project_tenancy(
transaction: &Transaction<'_>,
revision: RevisionId,
candidate: &RevisionCandidate,
) -> Result<(), ControlPlaneError> {
let tenancy =
Tenancy::of(&candidate.state).map_err(|error| ControlPlaneError::Invalid(error.into()))?;
let directory = Directory::of(&candidate.state, &tenancy)
.map_err(|error| ControlPlaneError::Invalid(error.into()))?;
let revision = revision.to_string();
let declared_tenants: Vec<String> = tenancy
.tenants()
.map(|tenant| tenant.body.tenant().to_string())
.collect();
if !declared_tenants.is_empty() {
transaction
.execute(
"UPDATE axond_cp_tenant SET lifecycle = 'deleted', revision_id = $2, \
updated_at = now() \
WHERE NOT (tenant_id = ANY($1)) AND lifecycle <> 'deleted'",
&[&declared_tenants, &revision],
)
.await
.map_err(|error| unavailable("retire undeclared tenants", &error))?;
}
for tenant in tenancy.tenants() {
transaction
.execute(
"INSERT INTO axond_cp_tenant (tenant_id, slug, lifecycle, revision_id) \
VALUES ($1, $2, $3, $4) \
ON CONFLICT (tenant_id) DO UPDATE SET slug = EXCLUDED.slug, \
lifecycle = EXCLUDED.lifecycle, revision_id = EXCLUDED.revision_id, \
updated_at = now()",
&[
&tenant.body.tenant().to_string(),
&tenant.slug.as_str(),
&tenant.body.lifecycle().as_str(),
&revision,
],
)
.await
.map_err(|error| {
projection_failure("project tenant", "tenant", tenant.slug.as_str(), &error)
})?;
}
for project in tenancy.projects() {
transaction
.execute(
"INSERT INTO axond_cp_project (project_id, tenant_id, slug, revision_id) \
VALUES ($1, $2, $3, $4) \
ON CONFLICT (project_id) DO UPDATE SET tenant_id = EXCLUDED.tenant_id, \
slug = EXCLUDED.slug, revision_id = EXCLUDED.revision_id, updated_at = now()",
&[
&project.body.project().to_string(),
&project.body.tenant().to_string(),
&project.slug.as_str(),
&revision,
],
)
.await
.map_err(|error| {
projection_failure("project project", "project", project.slug.as_str(), &error)
})?;
}
let declared: Vec<String> = directory
.principals()
.map(|principal| principal.body.principal().to_string())
.collect();
transaction
.execute(
"DELETE FROM axond_cp_principal WHERE NOT (principal_id = ANY($1))",
&[&declared],
)
.await
.map_err(|error| unavailable("revoke undeclared principals", &error))?;
for principal in directory.principals() {
let scope = rows::scope_columns(&principal.scope);
let (issuer, subject, digest) = match principal.body.credential() {
Credential::Oidc { issuer, subject } => {
(Some(issuer.clone()), Some(subject.clone()), None)
}
Credential::MintedKey { digest } => {
(None, None, digest.map(|digest| digest.to_string()))
}
};
transaction
.execute(
"INSERT INTO axond_cp_principal \
(principal_id, resource_id, identity_kind, scope_kind, tenant_id, project_id, \
slug, display_name, issuer, subject, key_digest, revision_id) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) \
ON CONFLICT (principal_id) DO UPDATE SET \
resource_id = EXCLUDED.resource_id, identity_kind = EXCLUDED.identity_kind, \
scope_kind = EXCLUDED.scope_kind, tenant_id = EXCLUDED.tenant_id, \
project_id = EXCLUDED.project_id, slug = EXCLUDED.slug, \
display_name = EXCLUDED.display_name, issuer = EXCLUDED.issuer, \
subject = EXCLUDED.subject, key_digest = EXCLUDED.key_digest, \
revision_id = EXCLUDED.revision_id, updated_at = now()",
&[
&principal.body.principal().to_string(),
&principal.reference.id.to_string(),
&principal.body.kind().as_str(),
&scope.kind,
&scope.tenant,
&scope.project,
&principal.slug.as_str(),
&principal.body.display_name().as_str(),
&issuer,
&subject,
&digest,
&revision,
],
)
.await
.map_err(|error| {
projection_failure(
"project principal",
"principal",
principal.slug.as_str(),
&error,
)
})?;
transaction
.execute(
"DELETE FROM axond_cp_principal_role WHERE principal_id = $1",
&[&principal.body.principal().to_string()],
)
.await
.map_err(|error| unavailable("revoke roles", &error))?;
for role in principal.body.roles() {
transaction
.execute(
"INSERT INTO axond_cp_principal_role (principal_id, role) VALUES ($1, $2)",
&[&principal.body.principal().to_string(), &role.as_str()],
)
.await
.map_err(|error| unavailable("grant role", &error))?;
}
}
transaction
.execute("SET CONSTRAINTS ALL IMMEDIATE", &[])
.await
.map_err(|error| projection_failure("settle names and identities", "name", "", &error))?;
record_referenced_tenants(transaction, &revision, candidate).await?;
Ok(())
}
async fn record_referenced_tenants(
transaction: &Transaction<'_>,
revision: &str,
candidate: &RevisionCandidate,
) -> Result<(), ControlPlaneError> {
let mut referenced: Vec<String> = candidate
.state
.resources()
.filter_map(|resource| rows::scope_columns(&resource.scope).tenant)
.collect();
referenced.sort_unstable();
referenced.dedup();
if referenced.is_empty() {
return Ok(());
}
transaction
.execute(
"INSERT INTO axond_cp_tenant (tenant_id, slug, lifecycle, revision_id) \
SELECT referenced, referenced, 'deleted', $2 \
FROM unnest($1::text[]) AS referenced \
ON CONFLICT (tenant_id) DO NOTHING",
&[&referenced, &revision],
)
.await
.map_err(|error| projection_failure("record referenced tenants", "tenant", "", &error))?;
Ok(())
}
async fn assert_version_is_immutable(
transaction: &Transaction<'_>,
resource: &ResourceVersion,
) -> Result<(), ControlPlaneError> {
let stored = transaction
.query_opt(
"SELECT content_checksum FROM axond_cp_resource_version \
WHERE resource_kind = $1 AND resource_id = $2 AND version = $3",
&[
&resource.reference.kind.as_str(),
&resource.reference.id.to_string(),
&version_text(resource.reference.version),
],
)
.await
.map_err(|error| unavailable("read resource version", &error))?;
let Some(stored) = stored else {
return Ok(());
};
let stored: String = stored.get(0);
let candidate = resource
.content_checksum()
.map_err(|error| ControlPlaneError::Invalid(error.into()))?;
if stored != candidate.to_string() {
return Err(ControlPlaneError::ImmutableResourceVersion {
reference: resource.reference,
});
}
Ok(())
}
async fn insert_resource_version(
transaction: &Transaction<'_>,
resource: &ResourceVersion,
) -> Result<(), ControlPlaneError> {
let scope = rows::scope_columns(&resource.scope);
let body = rows::body_columns(&resource.body).map_err(|error| {
corrupt_storage(format!(
"{} could not be encoded for storage: {error}",
resource.reference
))
})?;
let checksum = resource
.content_checksum()
.map_err(|error| ControlPlaneError::Invalid(error.into()))?;
let kind = resource.reference.kind.as_str();
let id = resource.reference.id.to_string();
let version = version_text(resource.reference.version);
let slug = resource.slug.as_str();
let checksum = checksum.to_string();
let serializer = SerializerVersion::default().as_str();
let parameters: [&(dyn ToSql + Sync); 13] = [
&kind,
&id,
&version,
&scope.kind,
&scope.tenant,
&scope.project,
&slug,
&body.form,
&body.inline,
&body.blob_kind,
&body.blob_digest,
&checksum,
&serializer,
];
transaction
.execute(
"INSERT INTO axond_cp_resource_version \
(resource_kind, resource_id, version, scope_kind, tenant_id, project_id, slug, \
body_form, body_inline, body_blob_kind, body_blob_digest, content_checksum, \
serializer) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) \
ON CONFLICT (resource_kind, resource_id, version) DO NOTHING",
¶meters,
)
.await
.map_err(|error| journal_write_failure("write resource version", &error))?;
for dependency in &resource.depends_on {
transaction
.execute(
"INSERT INTO axond_cp_resource_dependency \
(resource_kind, resource_id, version, depends_on_kind, depends_on_id, \
depends_on_version) VALUES ($1, $2, $3, $4, $5, $6) \
ON CONFLICT DO NOTHING",
&[
&kind,
&id,
&version,
&dependency.kind.as_str(),
&dependency.id.to_string(),
&version_text(dependency.version),
],
)
.await
.map_err(|error| unavailable("write resource dependency", &error))?;
}
Ok(())
}
async fn insert_mutation(
transaction: &Transaction<'_>,
mutation: &Mutation,
) -> Result<(), ControlPlaneError> {
let actor = rows::actor_columns(&mutation.actor);
let scope = rows::scope_columns(&mutation.scope);
transaction
.execute(
"INSERT INTO axond_cp_mutation \
(mutation_id, actor_kind, actor_issuer, actor_subject, actor_component, \
mutation_kind, scope_kind, tenant_id, project_id, idempotency_key, submitted_at, \
actor_tenant_id, actor_principal_id) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)",
&[
&mutation.id.to_string(),
&actor.kind,
&actor.issuer,
&actor.subject,
&actor.component,
&mutation.kind.as_str(),
&scope.kind,
&scope.tenant,
&scope.project,
&mutation.idempotency_key.as_str(),
&mutation.submitted_at,
&actor.tenant,
&actor.principal,
],
)
.await
.map_err(|error| {
if is_unique_violation(&error) {
denied(format!(
"mutation {} is already recorded; a mutation id names one change",
mutation.id
))
} else {
journal_write_failure("write mutation", &error)
}
})?;
Ok(())
}
async fn insert_audit_event(
transaction: &Transaction<'_>,
revision: RevisionId,
audit: &AuditEvent,
) -> Result<(), ControlPlaneError> {
let actor = rows::actor_columns(&audit.actor);
let target = audit.target;
transaction
.execute(
"INSERT INTO axond_cp_audit_event \
(audit_event_id, revision_id, mutation_id, actor_kind, actor_issuer, actor_subject, \
actor_component, event_kind, target_kind, target_id, target_version, summary, \
recorded_at, actor_tenant_id, actor_principal_id) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)",
&[
&audit.id.to_string(),
&revision.to_string(),
&audit.mutation.to_string(),
&actor.kind,
&actor.issuer,
&actor.subject,
&actor.component,
&audit.kind.as_str(),
&target.map(|target| target.kind.as_str()),
&target.map(|target| target.id.to_string()),
&target.map(|target| version_text(target.version)),
&audit.summary,
&audit.recorded_at,
&actor.tenant,
&actor.principal,
],
)
.await
.map_err(|error| {
if is_unique_violation(&error) {
denied(format!(
"audit event {} is already recorded; an audit event is written once",
audit.id
))
} else {
journal_write_failure("write audit event", &error)
}
})?;
Ok(())
}
fn journal_write_failure(operation: &str, error: &tokio_postgres::Error) -> ControlPlaneError {
let Some(db) = error
.as_db_error()
.filter(|db| *db.code() == SqlState::FOREIGN_KEY_VIOLATION)
else {
return unavailable(operation, error);
};
tracing::warn!(
constraint = db.constraint().unwrap_or("unnamed"),
detail = db.detail().unwrap_or(""),
message = db.message(),
"a journal row named a tenant this deployment has no row for"
);
denied(journal_ownership_refusal(db.constraint()))
}
fn journal_ownership_refusal(constraint: Option<&str>) -> String {
match constraint {
Some(constraint) => format!(
"the journal row names a tenant this deployment has no row for \
(constraint `{constraint}`); publish a revision that declares it, or \
upgrade the publisher that records the owners history names"
),
None => "the journal row names a tenant this deployment has no row for".to_owned(),
}
}
fn projection_failure(
operation: &str,
noun: &'static str,
slug: &str,
error: &tokio_postgres::Error,
) -> ControlPlaneError {
let Some(db) = error.as_db_error().filter(|db| is_projection_refusal(db)) else {
return unavailable(operation, error);
};
if is_name_conflict(db) {
let taken = if slug.is_empty() {
colliding_value(db).unwrap_or_else(|| slug.to_owned())
} else {
slug.to_owned()
};
return ControlPlaneError::NameTaken {
noun,
name: taken,
holder: db.constraint().map(ToOwned::to_owned),
};
}
tracing::warn!(
constraint = db.constraint().unwrap_or("unnamed"),
detail = db.detail().unwrap_or(""),
message = db.message(),
"a projected row was refused by state this deployment already holds"
);
denied(projection_refusal(
noun,
slug,
*db.code() == SqlState::FOREIGN_KEY_VIOLATION,
db.constraint(),
))
}
fn projection_refusal(noun: &str, slug: &str, ownership: bool, constraint: Option<&str>) -> String {
let subject = if slug.is_empty() {
format!("projecting the {noun}s of this revision")
} else {
format!("projecting the {noun} `{slug}`")
};
let conflict = if ownership {
"contradicts an ownership row this deployment already holds"
} else {
"collides with a row this deployment already holds"
};
format!(
"{subject} {conflict}{}; the conflicting row stands until a revision changes it, so \
no retry clears this",
constraint.map_or_else(String::new, |name| format!(" ({name})")),
)
}
fn is_unique_violation(error: &tokio_postgres::Error) -> bool {
error
.as_db_error()
.is_some_and(|db| db.code() == &SqlState::UNIQUE_VIOLATION)
}
fn is_projection_refusal(db: &tokio_postgres::error::DbError) -> bool {
matches!(
*db.code(),
SqlState::UNIQUE_VIOLATION
| SqlState::EXCLUSION_VIOLATION
| SqlState::FOREIGN_KEY_VIOLATION
)
}
fn is_name_conflict(db: &tokio_postgres::error::DbError) -> bool {
db.constraint()
.is_some_and(|name| name.ends_with("_slug_unique"))
}
fn colliding_value(db: &tokio_postgres::error::DbError) -> Option<String> {
let detail = db.detail()?;
let values = detail.split_once(")=(")?.1;
let values = values.split_once(')')?.0;
values
.rsplit(", ")
.next()
.map(|value| value.trim().to_owned())
.filter(|value| !value.is_empty())
}
fn audit_event(row: &Row) -> Result<AuditEvent, IntegrityError> {
let id: String = row.get(0);
let mutation: String = row.get(1);
let actor_kind: String = row.get(2);
let issuer: Option<String> = row.get(3);
let subject: Option<String> = row.get(4);
let component: Option<String> = row.get(5);
let event_kind: String = row.get(6);
let target_kind: Option<String> = row.get(7);
let target_id: Option<String> = row.get(8);
let target_version: Option<i64> = row.get(9);
let summary: String = row.get(10);
let recorded_at: SystemTime = row.get(11);
let actor_tenant: Option<String> = row.get(12);
let actor_principal: Option<String> = row.get(13);
let target = match (target_kind, target_id, target_version) {
(None, None, None) => None,
(Some(kind), Some(id), Some(version)) => Some(ResourceRef::new(
rows::resource_kind(&kind)?,
rows::resource_id(&id)?,
rows::version_number(version)?,
)),
_ => {
return Err(rows::unreadable(
"an audit event's target is half a reference",
));
}
};
Ok(AuditEvent {
id: rows::audit_event_id(&id)?,
mutation: rows::mutation_id(&mutation)?,
actor: rows::actor(
&actor_kind,
issuer.as_deref(),
subject.as_deref(),
component.as_deref(),
actor_tenant.as_deref(),
actor_principal.as_deref(),
)?,
kind: rows::mutation_kind(&event_kind)?,
target,
summary,
recorded_at,
})
}
fn access_denial(row: &Row) -> Result<AccessDenial, IntegrityError> {
let id: String = row.get(0);
let actor_kind: String = row.get(1);
let issuer: Option<String> = row.get(2);
let subject: Option<String> = row.get(3);
let component: Option<String> = row.get(4);
let actor_tenant: Option<String> = row.get(5);
let actor_principal: Option<String> = row.get(6);
let surface: String = row.get(7);
let action: String = row.get(8);
let scope_kind: String = row.get(9);
let tenant: Option<String> = row.get(10);
let project: Option<String> = row.get(11);
let reason: String = row.get(12);
let recorded_at: SystemTime = row.get(13);
Ok(AccessDenial {
id: rows::audit_event_id(&id)?,
actor: rows::actor(
&actor_kind,
issuer.as_deref(),
subject.as_deref(),
component.as_deref(),
actor_tenant.as_deref(),
actor_principal.as_deref(),
)?,
surface: Surface::parse(&surface).ok_or_else(|| {
rows::unreadable(format!("`{surface}` is not an administrative surface"))
})?,
action: Action::parse(&action)
.ok_or_else(|| rows::unreadable(format!("`{action}` is not an action")))?,
scope: rows::scope(&scope_kind, tenant.as_deref(), project.as_deref())?,
reason: DenialReason::parse(&reason)
.ok_or_else(|| rows::unreadable(format!("`{reason}` is not a denial reason")))?,
recorded_at,
})
}
#[cfg(test)]
mod tests {
use std::time::UNIX_EPOCH;
use super::super::hydration::HydrationLimit;
use super::*;
use crate::availability::discovery::{DiscoveryCompleteness, DiscoveryResult, DiscoverySource};
use crate::availability::{AvailabilityIndex, AvailabilityRecord};
use crate::backends::{BackendFailure, FailureCategory};
use crate::desired_state::fixtures::{
DESIRED_STATE_RESOURCES, candidate, human, principal_id, project, project_alias,
project_body, project_id, reference, state, state_a_pre_tenancy_build_published,
state_with_directory, state_with_renamed_alias, state_with_revoked_workload,
state_with_second_tenant, state_with_two_blobs, tenant, tenant_body, tenant_id,
two_tenant_directory_state, workload_key,
};
use crate::desired_state::{
Actor, AuditEventId, Checksum, DesiredState, DisplayName, ExpectedRevision, IdentityBody,
MutationId, PrincipalId, ResourceKind, ResourceScope, ResourceVersionNumber, Role, Slug,
TenantLifecycle, Uuid7, ValidationError, WorkloadKey, oracle::InMemoryControlPlane,
};
async fn journal() -> Option<(PostgresControlPlane, String, String)> {
let dsn = crate::test_services::postgres_dsn()?;
let schema = format!(
"cp_{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_nanos()
);
let mut config: Config = dsn.parse().expect("test dsn");
config.connect_timeout(Duration::from_secs(5));
let (client, connection) = config
.connect(crate::usage::tls_connector())
.await
.expect("connect to create the test schema");
tokio::spawn(async move {
let _ = connection.await;
});
client
.batch_execute(&format!("CREATE SCHEMA {schema}"))
.await
.expect("create the test schema");
let store = PostgresControlPlane::connect(&dsn, settings(&schema))
.await
.expect("boot against a fresh schema");
Some((store, dsn, schema))
}
fn settings(schema: &str) -> ControlPlaneSettings {
ControlPlaneSettings {
schema: Some(schema.to_owned()),
operation_timeout: Duration::from_secs(10),
connect_timeout: Duration::from_secs(5),
..ControlPlaneSettings::default()
}
}
#[test]
fn pending_operations_count_waiters_and_release_on_drop() {
let count = Arc::new(AtomicUsize::new(0));
let first = StatusProbeAdmission::pending(Arc::clone(&count));
assert_eq!(count.load(Ordering::Acquire), 1);
let second = StatusProbeAdmission::pending(Arc::clone(&count));
assert_eq!(count.load(Ordering::Acquire), 2);
drop(second);
assert_eq!(count.load(Ordering::Acquire), 1);
drop(first);
assert_eq!(count.load(Ordering::Acquire), 0);
}
#[test]
fn status_probe_reservation_is_visible_to_a_concurrent_operation() {
let count = Arc::new(AtomicUsize::new(0));
count.fetch_add(1, Ordering::AcqRel);
let probe = StatusProbeAdmission::new(Duration::from_secs(65), Arc::clone(&count));
let start = Arc::new(std::sync::Barrier::new(2));
let observed = Arc::new(AtomicUsize::new(0));
let worker_count = Arc::clone(&count);
let worker_start = Arc::clone(&start);
let worker_observed = Arc::clone(&observed);
let worker = std::thread::spawn(move || {
worker_start.wait();
let _admin = StatusProbeAdmission::pending(worker_count.clone());
worker_observed.store(worker_count.load(Ordering::Acquire), Ordering::Release);
});
start.wait();
worker
.join()
.expect("the concurrent operation does not panic");
assert_eq!(count.load(Ordering::Acquire), 1);
assert_eq!(observed.load(Ordering::Acquire), 2);
drop(probe);
assert_eq!(count.load(Ordering::Acquire), 0);
}
async fn second_store(dsn: &str, schema: &str) -> PostgresControlPlane {
PostgresControlPlane::connect(
dsn,
ControlPlaneSettings {
migrate: false,
..settings(schema)
},
)
.await
.expect("boot against a current schema")
}
async fn bounded_store(
dsn: &str,
schema: &str,
hydration: HydrationLimits,
) -> PostgresControlPlane {
PostgresControlPlane::connect(
dsn,
ControlPlaneSettings {
migrate: false,
hydration,
..settings(schema)
},
)
.await
.expect("boot against a current schema")
}
impl PostgresControlPlane {
async fn corrupt_with(&self, statement: &str) {
let sql = statement.to_owned();
self.run(None, move |client| {
let sql = sql.clone();
Box::pin(async move {
client
.batch_execute(&sql)
.await
.map_err(|error| unavailable("corrupt the journal", &error))
})
})
.await
.expect("the corrupting statement itself must succeed");
}
async fn count(&self, table: &str) -> i64 {
let sql = format!("SELECT count(*) FROM {table}");
self.run(None, move |client| {
Box::pin(async move {
client
.query_one(&sql, &[])
.await
.map(|row| row.get(0))
.map_err(|error| unavailable("count", &error))
})
})
.await
.expect("count")
}
}
fn uuid(seed: u64) -> Uuid7 {
Uuid7::from_parts(seed, 0, seed).expect("seed in range")
}
fn candidate_with_mutation(
expected: ExpectedRevision,
key: &str,
state: crate::desired_state::DesiredState,
seed: u64,
) -> RevisionCandidate {
let mut candidate = candidate(expected, key, state);
let mutation = MutationId::new(uuid(seed));
candidate.mutation.id = mutation;
candidate.audit.mutation = mutation;
candidate.audit.id = AuditEventId::new(uuid(seed + 1));
candidate
}
#[tokio::test]
async fn boot_migrates_a_fresh_database_and_reports_the_schema_current() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
assert_eq!(
store.schema_status().await.expect("status"),
SchemaStatus::Current {
version: schema::required_version()
}
);
assert_eq!(store.name(), "postgres");
assert!(store.health().await.is_ok());
assert_eq!(store.desired_revision().await.expect("head"), None);
let second = second_store(&dsn, &schema).await;
assert!(second.schema_status().await.expect("status").is_current());
let bare = format!("{schema}_bare");
store
.run(None, move |client| {
let sql = format!("CREATE SCHEMA {bare}");
Box::pin(async move {
client
.batch_execute(&sql)
.await
.map_err(|error| unavailable("create schema", &error))
})
})
.await
.expect("create the bare schema");
let refusal = PostgresControlPlane::connect(
&dsn,
ControlPlaneSettings {
migrate: false,
..settings(&format!("{schema}_bare"))
},
)
.await
.expect_err("an unmigrated schema must not be served");
assert_eq!(refusal.category(), FailureCategory::Denied);
assert!(refusal.to_string().contains("not present"), "{refusal}");
}
#[tokio::test]
async fn a_database_a_newer_build_owns_is_refused_rather_than_migrated_backwards() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
store
.run(None, |client| {
Box::pin(async move {
client
.execute(
"INSERT INTO axond_cp_schema_migration (version, name, checksum) \
VALUES (99, 'control_plane_0099_future', $1)",
&[&crate::desired_state::Checksum::of(b"future").to_string()],
)
.await
.map(|_| ())
.map_err(|error| unavailable("record a future migration", &error))
})
})
.await
.expect("record a future migration");
let status = store.schema_status().await.expect("status");
assert!(
matches!(status, SchemaStatus::Ahead { applied: 99, .. }),
"{status:?}"
);
let refusal = PostgresControlPlane::connect(&dsn, settings(&schema))
.await
.expect_err("a newer schema must not be served");
assert_eq!(refusal.category(), FailureCategory::Denied);
assert!(refusal.to_string().contains("newer gateway"), "{refusal}");
}
#[tokio::test]
async fn publication_is_a_chain_whose_history_stays_loadable() {
let Some((store, _, _)) = journal().await else {
return;
};
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "first", state()))
.await
.expect("first publication");
assert_eq!(first.parent, None);
assert_eq!(
store.desired_revision().await.expect("head"),
Some(first.id)
);
let second = store
.publish_revision(candidate(
ExpectedRevision::Exactly(first.id),
"second",
state_with_renamed_alias(),
))
.await
.expect("second publication");
assert_eq!(second.parent, Some(first.id));
assert_ne!(second.checksum, first.checksum);
assert_eq!(
store.desired_revision().await.expect("head"),
Some(second.id)
);
assert_eq!(
store.load_manifest(first.id).await.expect("manifest"),
first
);
assert_eq!(
store.load_manifest(second.id).await.expect("manifest"),
second
);
let loaded = store.load_revision(first.id).await.expect("hydrate");
assert_eq!(loaded.manifest(), &first);
assert_eq!(loaded.state().len(), DESIRED_STATE_RESOURCES);
assert_eq!(loaded.state(), &state());
let trail = store.audit_trail(first.id).await.expect("audit trail");
assert_eq!(trail.len(), 1);
assert_eq!(trail[0].summary, "applied first");
assert_eq!(store.count("axond_cp_resource_version").await, 6);
assert_eq!(store.count("axond_cp_blob").await, 1);
assert_eq!(store.count("axond_cp_revision").await, 2);
let missing = RevisionId::new(uuid(9_999));
assert!(matches!(
store.load_manifest(missing).await,
Err(ControlPlaneError::RevisionNotFound(_))
));
assert!(matches!(
store.audit_trail(missing).await,
Err(ControlPlaneError::RevisionNotFound(_))
));
}
#[tokio::test]
async fn concurrent_writers_agree_that_exactly_one_commit_wins() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "first", state()))
.await
.expect("first publication");
let other = second_store(&dsn, &schema).await;
let expected = ExpectedRevision::Exactly(first.id);
let (left, right) = tokio::join!(
store.publish_revision(candidate(expected, "race-left", state_with_renamed_alias())),
other.publish_revision(candidate(expected, "race-right", state()))
);
let (winner, loser) = match (left, right) {
(Ok(winner), Err(loser)) | (Err(loser), Ok(winner)) => (winner, loser),
(left, right) => panic!(
"exactly one writer must win an expected-revision race, got {left:?} and {right:?}"
),
};
assert!(matches!(
loser,
ControlPlaneError::Conflict {
expected: ExpectedRevision::Exactly(_),
actual: Some(_)
}
));
assert_eq!(loser.category(), FailureCategory::Conflict);
assert!(loser.retryable() || !loser.retryable());
assert_eq!(winner.parent, Some(first.id));
assert_eq!(
store.desired_revision().await.expect("head"),
Some(winner.id)
);
assert_eq!(store.count("axond_cp_revision").await, 2);
assert_eq!(store.count("axond_cp_mutation").await, 2);
assert_eq!(store.count("axond_cp_audit_event").await, 2);
}
#[tokio::test]
async fn a_failed_publication_rolls_back_every_row_it_had_written() {
let Some((store, _, _)) = journal().await else {
return;
};
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "first", state()))
.await
.expect("first publication");
let recorded = store.audit_trail(first.id).await.expect("audit trail");
let mut doomed_state = state();
doomed_state
.insert(tenant(9, "rolled-back"))
.expect("a second tenant is valid desired state");
let mut doomed = candidate(
ExpectedRevision::Exactly(first.id),
"rollback",
doomed_state,
);
doomed.mutation.id = first.mutation;
doomed.audit.mutation = first.mutation;
let error = store
.publish_revision(doomed)
.await
.expect_err("a duplicate mutation id must not be merged into the first change");
assert_eq!(error.category(), FailureCategory::Denied);
assert_eq!(store.count("axond_cp_revision").await, 1);
assert_eq!(store.count("axond_cp_audit_event").await, 1);
assert_eq!(
store.count("axond_cp_resource_version").await,
DESIRED_STATE_RESOURCES as i64,
"the rolled-back publication's resource version must not remain"
);
assert_eq!(
store.desired_revision().await.expect("head"),
Some(first.id)
);
assert_eq!(store.audit_trail(first.id).await.expect("trail"), recorded);
}
#[tokio::test]
async fn an_immutable_version_cannot_be_redefined_and_leaves_nothing_behind() {
let Some((store, _, _)) = journal().await else {
return;
};
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "first", state()))
.await
.expect("first publication");
let mut redefined = DesiredState::new();
for resource in state().resources() {
let resource = if resource.reference.kind == ResourceKind::Tenant {
let mut renamed = resource.clone();
renamed.slug = Slug::parse("renamed").expect("slug");
renamed
} else {
resource.clone()
};
redefined.insert(resource).expect("valid state");
}
for blob in state().blobs() {
redefined.declare_blob(*blob);
}
let error = store
.publish_revision(candidate(
ExpectedRevision::Exactly(first.id),
"redefine",
redefined,
))
.await
.expect_err("an immutable version must not be redefined");
assert!(
matches!(error, ControlPlaneError::ImmutableResourceVersion { .. }),
"{error:?}"
);
assert_eq!(store.count("axond_cp_revision").await, 1);
assert_eq!(store.count("axond_cp_audit_event").await, 1);
assert_eq!(
store.desired_revision().await.expect("head"),
Some(first.id)
);
}
#[tokio::test]
async fn a_repeated_key_replays_its_outcome_and_a_reused_one_is_refused() {
let Some((store, _, _)) = journal().await else {
return;
};
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "retry-1", state()))
.await
.expect("first publication");
let replayed = store
.publish_revision(candidate(ExpectedRevision::Empty, "retry-1", state()))
.await
.expect("a retry of the same state replays");
assert_eq!(replayed, first);
assert_eq!(store.count("axond_cp_revision").await, 1);
assert_eq!(store.count("axond_cp_mutation").await, 1);
assert_eq!(store.count("axond_cp_audit_event").await, 1);
assert_eq!(store.audit_trail(first.id).await.expect("trail").len(), 1);
let error = store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"retry-1",
state_with_renamed_alias(),
4_242,
))
.await
.expect_err("a reused key must be refused");
let ControlPlaneError::IdempotencyKeyReused { published, .. } = error else {
panic!("expected a reused-key refusal, got {error:?}");
};
assert_eq!(published, first.id);
assert_eq!(store.count("axond_cp_revision").await, 1);
}
#[tokio::test]
async fn one_callers_key_neither_replays_nor_blocks_anothers() {
let Some((store, _, _)) = journal().await else {
return;
};
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "retry-1", state()))
.await
.expect("first publication");
let mut other = candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"retry-1",
state(),
7_777,
);
let system = Actor::System {
component: "catalog-refresh".to_owned(),
};
other.mutation.actor = system.clone();
other.audit.actor = system;
let second = store
.publish_revision(other)
.await
.expect("another caller's identical key is another write");
assert_ne!(second.id, first.id);
assert_eq!(second.parent, Some(first.id));
assert_eq!(store.count("axond_cp_idempotency").await, 2);
}
#[tokio::test]
async fn an_expired_retry_window_closes_without_touching_the_revision() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
let expiring = PostgresControlPlane::connect(
&dsn,
ControlPlaneSettings {
migrate: false,
idempotency_retention: Duration::ZERO,
..settings(&schema)
},
)
.await
.expect("boot");
let first = expiring
.publish_revision(candidate(ExpectedRevision::Empty, "retry-1", state()))
.await
.expect("first publication");
let second = store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"retry-1",
state(),
8_888,
))
.await
.expect("an expired record is not a replay");
assert_ne!(second.id, first.id);
assert_eq!(
store.load_manifest(first.id).await.expect("manifest"),
first
);
assert_eq!(store.audit_trail(first.id).await.expect("trail").len(), 1);
assert_eq!(store.count("axond_cp_revision").await, 2);
}
#[tokio::test]
async fn an_outage_is_an_outage_and_changes_nothing_a_replica_holds() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "first", state()))
.await
.expect("first publication");
let held = store.load_revision(first.id).await.expect("hydrate");
let stalled = PostgresControlPlane::connect(
&dsn,
ControlPlaneSettings {
migrate: false,
operation_timeout: Duration::from_nanos(1),
..settings(&schema)
},
)
.await
.expect("boot");
let error = stalled
.publish_revision(candidate(
ExpectedRevision::Exactly(first.id),
"stalled",
state_with_renamed_alias(),
))
.await
.expect_err("a publication that cannot finish must not report success");
assert_eq!(error.category(), FailureCategory::Unavailable);
assert!(error.retryable());
assert_eq!(
store.desired_revision().await.expect("head"),
Some(first.id)
);
assert_eq!(store.count("axond_cp_revision").await, 1);
assert_eq!(store.count("axond_cp_audit_event").await, 1);
assert_eq!(held.manifest(), &first);
assert_eq!(held.state(), &state());
assert!(store.health().await.is_ok());
}
#[tokio::test]
async fn the_journal_answers_the_contract_the_way_the_oracle_does() {
let Some((store, _, _)) = journal().await else {
return;
};
let oracle = InMemoryControlPlane::new();
let stores: [&dyn ControlPlaneStore; 2] = [&store, &oracle];
let mut heads = Vec::new();
for store in stores {
assert_eq!(store.desired_revision().await.expect("head"), None);
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "first", state()))
.await
.expect("first publication");
let stale = store
.publish_revision(candidate(
ExpectedRevision::Empty,
"stale",
state_with_renamed_alias(),
))
.await
.expect_err("a stale expectation conflicts");
assert_eq!(stale.category(), FailureCategory::Conflict);
let replay = store
.publish_revision(candidate(ExpectedRevision::Empty, "first", state()))
.await
.expect("a retry replays");
assert_eq!(replay, first);
let loaded = store.load_revision(first.id).await.expect("hydrate");
assert_eq!(loaded.state(), &state());
assert_eq!(
store.audit_trail(first.id).await.expect("trail").len(),
1,
"one mutation is one audit event"
);
heads.push(store.desired_revision().await.expect("head").is_some());
}
assert_eq!(heads, vec![true, true]);
}
#[tokio::test]
async fn a_boot_migration_the_server_rejects_is_denied_rather_than_an_outage() {
let Some(dsn) = crate::test_services::postgres_dsn() else {
return;
};
let missing = format!(
"cp_absent_{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_nanos()
);
let error = PostgresControlPlane::connect(
&dsn,
ControlPlaneSettings {
migrate: true,
..settings(&missing)
},
)
.await
.expect_err("a schema that does not exist cannot be migrated into");
let ControlPlaneError::Denied { message, .. } = &error else {
panic!("a rejected statement is not an outage: {error:?}");
};
assert!(
message.contains("schema exists"),
"the refusal names what to fix: {message}"
);
}
#[tokio::test]
async fn replicas_booting_together_migrate_once_whatever_the_server_default_is() {
let Some(dsn) = crate::test_services::postgres_dsn() else {
return;
};
let schema = format!(
"cp_rr_{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_nanos()
);
let mut config: Config = dsn.parse().expect("test dsn");
config.connect_timeout(Duration::from_secs(5));
let (client, connection) = config
.connect(crate::usage::tls_connector())
.await
.expect("connect to create the test schema");
tokio::spawn(async move {
let _ = connection.await;
});
client
.batch_execute(&format!("CREATE SCHEMA {schema}"))
.await
.expect("create the test schema");
let separator = if dsn.contains('?') { "&" } else { "?" };
let strict = format!(
"{dsn}{separator}options=-c%20default_transaction_isolation%3Drepeatable%5C%20read"
);
let booting = || async {
PostgresControlPlane::connect(
&strict,
ControlPlaneSettings {
migrate: true,
..settings(&schema)
},
)
.await
};
let (first, second) = tokio::join!(booting(), booting());
let first = first.expect("a replica migrating an empty database boots");
second.expect("the replica that waited for the lock boots too");
assert!(
first.schema_status().await.expect("status").is_current(),
"one migration, not two"
);
}
#[tokio::test]
async fn requested_tls_is_not_silently_downgraded() {
let Some(dsn) = crate::test_services::postgres_dsn() else {
return;
};
let separator = if dsn.contains('?') { "&" } else { "?" };
let error = PostgresControlPlane::connect(
&format!("{dsn}{separator}sslmode=require"),
ControlPlaneSettings {
migrate: false,
..ControlPlaneSettings::default()
},
)
.await
.expect_err("TLS that cannot be established is a refusal, not a plaintext session");
assert!(
matches!(
error,
ControlPlaneError::Unavailable { .. } | ControlPlaneError::Denied { .. }
),
"{error:?}"
);
}
async fn three_revisions(
store: &PostgresControlPlane,
) -> (RevisionManifest, RevisionManifest, RevisionManifest) {
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "first", state()))
.await
.expect("first publication");
let second = store
.publish_revision(candidate(
ExpectedRevision::Exactly(first.id),
"second",
state_with_renamed_alias(),
))
.await
.expect("second publication");
let third = store
.publish_revision(candidate(
ExpectedRevision::Exactly(second.id),
"third",
state_with_second_tenant(),
))
.await
.expect("third publication");
(first, second, third)
}
#[tokio::test]
async fn a_historical_revision_hydrates_as_the_state_it_published() {
let Some((store, _, _)) = journal().await else {
return;
};
let (first, second, third) = three_revisions(&store).await;
for (manifest, published) in [
(&first, state()),
(&second, state_with_renamed_alias()),
(&third, state_with_second_tenant()),
] {
let loaded = store.load_revision(manifest.id).await.expect("hydrate");
assert_eq!(loaded.state(), &published);
assert_eq!(loaded.manifest(), manifest);
assert_eq!(
loaded.state().checksum().expect("canonical"),
manifest.checksum
);
assert_eq!(loaded.manifest().entries.len(), loaded.state().len());
}
let once = store.load_revision(first.id).await.expect("hydrate");
let twice = store.load_revision(first.id).await.expect("hydrate");
assert_eq!(once.state(), twice.state());
assert_eq!(once.manifest(), twice.manifest());
}
#[tokio::test]
async fn a_revision_an_older_build_published_still_hydrates() {
let Some((store, _, _)) = journal().await else {
return;
};
let published = state_a_pre_tenancy_build_published();
let manifest = store
.publish_revision(candidate(
ExpectedRevision::Empty,
"pre-tenancy",
published.clone(),
))
.await
.expect("a revision without owner rows publishes");
let loaded = store
.load_revision(manifest.id)
.await
.expect("and hydrates on a build that reads typed bodies");
assert_eq!(loaded.state(), &published);
let owner = tenant_id(1);
let mut owned = state();
owned
.insert(project_alias(&owner, &project_id(2), 6, "inner"))
.expect("a project's own alias is valid");
let second = store
.publish_revision(candidate(
ExpectedRevision::Exactly(manifest.id),
"owned",
owned.clone(),
))
.await
.expect("a declared owner is not a new requirement");
assert_eq!(
store
.load_revision(second.id)
.await
.expect("hydrate")
.state(),
&owned
);
let stranger = tenant_id(11);
store
.corrupt_with(&format!(
"INSERT INTO axond_cp_tenant (tenant_id, slug, lifecycle, revision_id) \
VALUES ('{stranger}', 'stranger', 'active', '{}') \
ON CONFLICT (tenant_id) DO NOTHING; \
UPDATE axond_cp_resource_version SET tenant_id = '{stranger}' \
WHERE scope_kind = 'project'",
second.id
))
.await;
let error = store
.load_revision(second.id)
.await
.expect_err("a resource in another tenant's project is not hydratable");
assert_eq!(error.category(), FailureCategory::Corrupt);
}
#[tokio::test]
async fn two_revisions_share_immutable_resources_without_sharing_a_value() {
let Some((store, _, _)) = journal().await else {
return;
};
let (first, second, _) = three_revisions(&store).await;
assert_eq!(
store.count("axond_cp_resource_version").await,
i64::try_from(DESIRED_STATE_RESOURCES + 1 + 3).expect("small"),
);
assert_eq!(store.count("axond_cp_blob").await, 1);
assert_eq!(store.count("axond_cp_revision_blob").await, 3);
let older = store.load_revision(first.id).await.expect("hydrate");
let newer = store.load_revision(second.id).await.expect("hydrate");
let credential = reference(ResourceKind::ProviderCredential, 3);
assert_eq!(
older.state().get(&credential),
newer.state().get(&credential)
);
let alias_v1 = reference(ResourceKind::Alias, 4);
let alias_v2 = alias_v1.at(ResourceVersionNumber::FIRST.next());
assert!(older.state().get(&alias_v1).is_some());
assert!(older.state().get(&alias_v2).is_none());
assert!(newer.state().get(&alias_v2).is_some());
assert!(newer.state().get(&alias_v1).is_none());
assert_ne!(older.state(), newer.state());
let blobs: Vec<_> = older.state().blobs().collect();
assert_eq!(blobs, newer.state().blobs().collect::<Vec<_>>());
assert_eq!(blobs.len(), 1);
}
#[tokio::test]
async fn the_desired_revision_and_its_hydration_are_one_read() {
let Some((store, _, _)) = journal().await else {
return;
};
assert!(
store
.load_desired_revision()
.await
.expect("an unpublished journal is not a failure")
.is_none()
);
let (_, _, third) = three_revisions(&store).await;
let desired = store
.load_desired_revision()
.await
.expect("hydrate the head")
.expect("a head exists");
assert_eq!(desired.manifest(), &third);
assert_eq!(desired.state(), &state_with_second_tenant());
let oracle = InMemoryControlPlane::new();
assert!(
oracle
.load_desired_revision()
.await
.expect("empty")
.is_none()
);
let published = oracle
.publish_revision(candidate(ExpectedRevision::Empty, "first", state()))
.await
.expect("publication");
let loaded = oracle
.load_desired_revision()
.await
.expect("hydrate the head")
.expect("a head exists");
assert_eq!(loaded.manifest(), &published);
assert_eq!(loaded.state(), &state());
}
#[tokio::test]
async fn a_manifest_reference_whose_version_is_gone_is_named_not_dropped() {
let Some((store, _, _)) = journal().await else {
return;
};
let (first, _, _) = three_revisions(&store).await;
let project = reference(ResourceKind::Project, 2);
store
.corrupt_with(
"DO $$ DECLARE name text; BEGIN \
SELECT conname INTO name FROM pg_constraint \
WHERE conrelid = 'axond_cp_revision_entry'::regclass AND contype = 'f' \
AND conname LIKE '%resource%'; \
EXECUTE format('ALTER TABLE axond_cp_revision_entry DROP CONSTRAINT %I', name); \
END $$;",
)
.await;
store
.corrupt_with(&format!(
"DELETE FROM axond_cp_resource_version WHERE resource_id = '{}'",
project.id
))
.await;
for error in [
store
.load_manifest(first.id)
.await
.expect_err("a manifest missing a version is not a manifest"),
store
.load_revision(first.id)
.await
.expect_err("a revision missing a version is not hydratable"),
] {
assert_eq!(error.category(), FailureCategory::Corrupt);
let ControlPlaneError::Corrupt { source, .. } = &error else {
panic!("expected corruption, got {error:?}");
};
assert_eq!(
**source,
IntegrityError::MissingResource { reference: project },
"{error:?}"
);
}
}
#[tokio::test]
async fn a_declared_blob_whose_record_is_gone_is_named_not_dropped() {
let Some((store, _, _)) = journal().await else {
return;
};
let (first, _, _) = three_revisions(&store).await;
store
.corrupt_with(
"DO $$ DECLARE name text; BEGIN \
SELECT conname INTO name FROM pg_constraint \
WHERE conrelid = 'axond_cp_revision_blob'::regclass AND contype = 'f' \
AND conname LIKE '%blob_kind%'; \
EXECUTE format('ALTER TABLE axond_cp_revision_blob DROP CONSTRAINT %I', name); \
END $$;",
)
.await;
store
.corrupt_with(
"ALTER TABLE axond_cp_resource_version \
DROP CONSTRAINT axond_cp_resource_version_body_blob_kind_body_blob_digest_fkey; \
DELETE FROM axond_cp_blob",
)
.await;
for error in [
store
.load_manifest(first.id)
.await
.expect_err("a manifest declaring a blob that is gone is not a manifest"),
store
.load_revision(first.id)
.await
.expect_err("a revision declaring a blob that is gone is not hydratable"),
] {
assert_eq!(error.category(), FailureCategory::Corrupt);
let ControlPlaneError::Corrupt { source, .. } = &error else {
panic!("expected corruption, got {error:?}");
};
let IntegrityError::Unreadable { detail } = &**source else {
panic!("expected an unreadable row, got {error:?}");
};
assert!(detail.contains("no blob record"), "{detail}");
}
}
#[tokio::test]
async fn a_dependency_edge_that_leaves_the_revision_is_refused() {
let Some((store, _, _)) = journal().await else {
return;
};
let (first, _, _) = three_revisions(&store).await;
let alias = reference(ResourceKind::Alias, 4);
store
.corrupt_with(&format!(
"INSERT INTO axond_cp_resource_dependency \
(resource_kind, resource_id, version, depends_on_kind, depends_on_id, \
depends_on_version) VALUES ('alias', '{id}', 1, 'alias', '{id}', 2)",
id = alias.id
))
.await;
let error = store
.load_revision(first.id)
.await
.expect_err("a reference that leaves the revision is not hydratable");
let ControlPlaneError::Corrupt { source, .. } = &error else {
panic!("expected corruption, got {error:?}");
};
assert!(
matches!(
**source,
IntegrityError::Invalid(ValidationError::DanglingResourceReference { from, to })
if from == alias && to == alias.at(ResourceVersionNumber::FIRST.next())
),
"{error:?}"
);
}
#[tokio::test]
async fn an_edge_across_a_tenant_boundary_is_refused_by_the_reference_layer() {
let Some((store, _, _)) = journal().await else {
return;
};
let (_, _, third) = three_revisions(&store).await;
let alias = reference(ResourceKind::Alias, 14);
let credential = reference(ResourceKind::ProviderCredential, 3);
store
.corrupt_with(&format!(
"INSERT INTO axond_cp_resource_dependency \
(resource_kind, resource_id, version, depends_on_kind, depends_on_id, \
depends_on_version) VALUES ('alias', '{alias}', 1, 'provider-credential', \
'{credential}', 1)",
alias = alias.id,
credential = credential.id
))
.await;
let error = store
.load_revision(third.id)
.await
.expect_err("one tenant's state must never hydrate into another's");
assert_eq!(error.category(), FailureCategory::Corrupt);
let ControlPlaneError::Corrupt { source, .. } = &error else {
panic!("expected corruption, got {error:?}");
};
assert!(
matches!(
**source,
IntegrityError::Invalid(ValidationError::CrossTenantReference { from, to })
if from == alias && to == credential
),
"{error:?}"
);
}
#[tokio::test]
async fn a_deployment_scoped_version_may_not_depend_on_one_tenants_state() {
let Some((store, _, _)) = journal().await else {
return;
};
let (first, _, _) = three_revisions(&store).await;
let catalog = reference(ResourceKind::CatalogModel, 5);
let credential = reference(ResourceKind::ProviderCredential, 3);
store
.corrupt_with(&format!(
"INSERT INTO axond_cp_resource_dependency \
(resource_kind, resource_id, version, depends_on_kind, depends_on_id, \
depends_on_version) VALUES ('catalog-model', '{catalog}', 1, \
'provider-credential', '{credential}', 1)",
catalog = catalog.id,
credential = credential.id
))
.await;
let error = store
.load_revision(first.id)
.await
.expect_err("deployment-wide state must not depend on one tenant's");
let ControlPlaneError::Corrupt { source, .. } = &error else {
panic!("expected corruption, got {error:?}");
};
assert!(
matches!(
**source,
IntegrityError::Invalid(ValidationError::TenantScopedDependency { from, to })
if from == catalog && to == credential
),
"{error:?}"
);
}
#[tokio::test]
async fn a_state_checksum_that_no_longer_matches_refuses_the_revision() {
let Some((store, _, _)) = journal().await else {
return;
};
let (first, _, _) = three_revisions(&store).await;
let foreign = state_with_renamed_alias().checksum().expect("canonical");
store
.corrupt_with(&format!(
"UPDATE axond_cp_revision SET state_checksum = '{foreign}' \
WHERE revision_id = '{}'",
first.id
))
.await;
let error = store
.load_revision(first.id)
.await
.expect_err("a state that hashes to something else is not the state");
let ControlPlaneError::Corrupt { source, .. } = &error else {
panic!("expected corruption, got {error:?}");
};
assert!(
matches!(
**source,
IntegrityError::ChecksumMismatch { expected, .. } if expected == foreign
),
"{error:?}"
);
}
#[tokio::test]
async fn a_resource_whose_content_checksum_no_longer_matches_is_refused() {
let Some((store, _, _)) = journal().await else {
return;
};
let (first, _, _) = three_revisions(&store).await;
let project = reference(ResourceKind::Project, 2);
store
.corrupt_with(&format!(
"UPDATE axond_cp_resource_version SET slug = 'edited' \
WHERE resource_id = '{}'",
project.id
))
.await;
let error = store
.load_revision(first.id)
.await
.expect_err("a resource that is not its own content checksum is not hydratable");
let ControlPlaneError::Corrupt { source, .. } = &error else {
panic!("expected corruption, got {error:?}");
};
assert!(
matches!(
**source,
IntegrityError::ContentMismatch { reference, .. } if reference == project
),
"{error:?}"
);
}
#[tokio::test]
async fn a_body_that_cannot_be_decoded_is_corruption_and_not_an_outage() {
let Some((store, _, _)) = journal().await else {
return;
};
let (first, _, _) = three_revisions(&store).await;
let project = reference(ResourceKind::Project, 2);
store
.corrupt_with(&format!(
"UPDATE axond_cp_resource_version SET body_inline = '\\xdeadbeef'::bytea \
WHERE resource_id = '{}'",
project.id
))
.await;
store
.load_manifest(first.id)
.await
.expect("a manifest hydrates no body");
let error = store
.load_revision(first.id)
.await
.expect_err("a body this build cannot decode is not hydratable");
assert_eq!(error.category(), FailureCategory::Corrupt);
assert!(!error.retryable(), "corruption is not cleared by retrying");
let ControlPlaneError::Corrupt { source, .. } = &error else {
panic!("expected corruption, got {error:?}");
};
assert!(
matches!(**source, IntegrityError::Unreadable { .. }),
"{error:?}"
);
}
#[tokio::test]
async fn a_row_written_by_a_serializer_this_build_does_not_read_is_refused() {
let Some((store, _, _)) = journal().await else {
return;
};
let (first, _, _) = three_revisions(&store).await;
store
.corrupt_with(
"UPDATE axond_cp_resource_version SET serializer = 'axond.desired-state.v99'",
)
.await;
let error = store
.load_revision(first.id)
.await
.expect_err("a row this build cannot read is not hydratable");
assert_ne!(error.category(), FailureCategory::Corrupt);
let ControlPlaneError::Incompatible { source, .. } = &error else {
panic!("expected an incompatibility, got {error:?}");
};
assert!(
matches!(**source, IntegrityError::UnknownSerializer { .. }),
"{error:?}"
);
store
.corrupt_with("UPDATE axond_cp_resource_version SET serializer = 'json'")
.await;
let error = store
.load_revision(first.id)
.await
.expect_err("a column no build ever wrote is not hydratable");
assert_eq!(error.category(), FailureCategory::Corrupt);
let ControlPlaneError::Corrupt { source, .. } = &error else {
panic!("expected corruption, got {error:?}");
};
assert!(
matches!(**source, IntegrityError::Unreadable { .. }),
"{error:?}"
);
}
#[tokio::test]
async fn a_version_number_the_domain_cannot_hold_is_refused() {
let Some((store, _, _)) = journal().await else {
return;
};
let (first, _, _) = three_revisions(&store).await;
let alias = reference(ResourceKind::Alias, 4);
store
.corrupt_with(
"DO $$ DECLARE name text; BEGIN \
SELECT conname INTO name FROM pg_constraint \
WHERE conrelid = 'axond_cp_revision_entry'::regclass AND contype = 'f' \
AND conname LIKE '%resource%'; \
EXECUTE format('ALTER TABLE axond_cp_revision_entry DROP CONSTRAINT %I', name); \
END $$;",
)
.await;
store
.corrupt_with(&format!(
"UPDATE axond_cp_revision_entry SET version = 0 \
WHERE resource_id = '{}' AND revision_id = '{revision}'",
alias.id,
revision = first.id
))
.await;
let error = store
.load_revision(first.id)
.await
.expect_err("a version number that is not one is not a version number");
assert_eq!(error.category(), FailureCategory::Corrupt);
let ControlPlaneError::Corrupt { source, .. } = &error else {
panic!("expected corruption, got {error:?}");
};
let IntegrityError::Unreadable { detail } = &**source else {
panic!("expected an unreadable row, got {error:?}");
};
assert!(detail.contains("resource version 0"), "{detail}");
}
#[tokio::test]
async fn a_revision_larger_than_the_bound_is_refused_whole() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
let (first, _, third) = three_revisions(&store).await;
let cases: [(HydrationLimits, &str); 5] = [
(
HydrationLimits {
max_entries: 2,
..HydrationLimits::default()
},
"resource versions",
),
(
HydrationLimits {
max_blobs: 0,
..HydrationLimits::default()
},
"blobs",
),
(
HydrationLimits {
max_blob_bytes: 8,
..HydrationLimits::default()
},
"bytes",
),
(
HydrationLimits {
max_dependency_edges: 1,
..HydrationLimits::default()
},
"dependency edges",
),
(
HydrationLimits {
max_inline_body_bytes: 4,
..HydrationLimits::default()
},
"inline body",
),
];
for (limits, expected) in cases {
let bounded = bounded_store(&dsn, &schema, limits).await;
let error = bounded
.load_revision(first.id)
.await
.expect_err("a revision past a bound must not hydrate");
assert_eq!(error.category(), FailureCategory::Denied);
assert!(!error.retryable(), "a bound is not cleared by retrying");
assert!(
matches!(error, ControlPlaneError::TooLarge { revision, .. } if revision == first.id),
"{error:?}"
);
assert!(error.to_string().contains(expected), "{error}");
}
let two_blobs = state_with_two_blobs();
let fourth = store
.publish_revision(candidate(
ExpectedRevision::Exactly(third.id),
"fourth",
two_blobs.clone(),
))
.await
.expect("fourth publication");
let sizes: Vec<u64> = two_blobs.blobs().map(|blob| blob.size_bytes).collect();
assert_eq!(sizes.len(), 2, "the fixture must declare two blobs");
let total = sizes.iter().sum::<u64>();
let bounded = bounded_store(
&dsn,
&schema,
HydrationLimits {
max_blob_bytes: 8,
..HydrationLimits::default()
},
)
.await;
let error = bounded
.load_manifest(fourth.id)
.await
.expect_err("a revision past a bound must not hydrate");
assert!(error.to_string().contains(&total.to_string()), "{error}");
for size in sizes {
assert!(
!error.to_string().contains(&size.to_string()),
"reported a partial sum: {error}"
);
}
let bounded = bounded_store(
&dsn,
&schema,
HydrationLimits {
max_state_bytes: 64,
..HydrationLimits::default()
},
)
.await;
let error = bounded
.load_revision(first.id)
.await
.expect_err("a candidate past the bound must not be returned");
assert!(
matches!(
error,
ControlPlaneError::TooLarge {
limit: HydrationLimit::StateBytes { .. },
..
}
),
"{error:?}"
);
assert_eq!(
store
.load_revision(first.id)
.await
.expect("hydrate")
.state(),
&state()
);
}
impl PostgresControlPlane {
async fn column(&self, sql: &str) -> Vec<String> {
let sql = sql.to_owned();
self.run(None, move |client| {
let sql = sql.clone();
Box::pin(async move {
client
.query(&sql, &[])
.await
.map(|rows| {
rows.iter()
.map(|row| {
row.try_get::<_, Option<String>>(0)
.expect("a text column")
.unwrap_or_else(|| "<null>".to_owned())
})
.collect()
})
.map_err(|error| unavailable("read a projected column", &error))
})
})
.await
.expect("the read itself must succeed")
}
async fn attempt(&self, statement: &str) -> Result<(), ControlPlaneError> {
let sql = statement.to_owned();
self.run(None, move |client| {
let sql = sql.clone();
Box::pin(async move {
client
.batch_execute(&sql)
.await
.map_err(|error| unavailable("attempt a write", &error))
})
})
.await
}
}
const DIGEST: &str =
"'sha256:0000000000000000000000000000000000000000000000000000000000000000'";
fn denial(seed: u64, scope: ResourceScope, reason: DenialReason) -> AccessDenial {
AccessDenial {
id: AuditEventId::new(uuid(seed)),
actor: Actor::Human {
issuer: "https://idp.example".to_owned(),
subject: "dev".to_owned(),
},
surface: Surface::Credential,
action: Action::Rotate,
scope,
reason,
recorded_at: journal_now() + Duration::from_secs(seed),
}
}
#[tokio::test]
async fn publishing_a_revision_projects_the_owners_and_the_directory_it_declares() {
let Some((store, _, _)) = journal().await else {
return;
};
let first = store
.publish_revision(candidate(
ExpectedRevision::Empty,
"directory",
state_with_directory(),
))
.await
.expect("a revision declaring a directory publishes");
assert_eq!(
store
.column("SELECT slug FROM axond_cp_tenant ORDER BY slug")
.await,
vec!["acme"]
);
assert_eq!(
store.column("SELECT lifecycle FROM axond_cp_tenant").await,
vec!["active"]
);
assert_eq!(
store
.column("SELECT slug FROM axond_cp_project ORDER BY slug")
.await,
vec!["core"]
);
assert_eq!(
store
.column("SELECT slug FROM axond_cp_principal ORDER BY slug")
.await,
vec!["admin", "deployer", "dev", "root"]
);
assert_eq!(
store
.column("SELECT role FROM axond_cp_principal_role ORDER BY role")
.await,
vec!["developer", "operator", "platform-admin", "tenant-admin"]
);
assert_eq!(
store
.column("SELECT DISTINCT revision_id FROM axond_cp_principal")
.await,
vec![first.id.to_string()]
);
let digests = store
.column("SELECT key_digest FROM axond_cp_principal ORDER BY slug")
.await;
assert_eq!(
digests.iter().filter(|digest| *digest != "<null>").count(),
1
);
for digest in &digests {
assert!(
!digest.contains(WorkloadKey::PREFIX),
"a key reached the database: {digest}"
);
}
assert!(
digests
.iter()
.any(|digest| *digest == Checksum::of(workload_key(0xd0).as_bytes()).to_string()),
"{digests:?}"
);
store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"revoked",
state_with_revoked_workload(),
71,
))
.await
.expect("a revision that drops a principal publishes");
assert_eq!(
store
.column("SELECT slug FROM axond_cp_principal ORDER BY slug")
.await,
vec!["admin", "dev", "root"]
);
assert_eq!(
store
.column("SELECT count(*)::text FROM axond_cp_principal_role")
.await,
vec!["3"]
);
assert_eq!(
store.column("SELECT slug FROM axond_cp_tenant").await,
vec!["acme"]
);
}
#[tokio::test]
async fn a_revision_declaring_no_identity_revokes_every_principal_and_keeps_its_tenants() {
let Some((store, _, _)) = journal().await else {
return;
};
let first = store
.publish_revision(candidate(
ExpectedRevision::Empty,
"directory",
state_with_directory(),
))
.await
.expect("a directory publishes");
assert_eq!(store.count("axond_cp_principal").await, 4);
store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"no directory",
state(),
75,
))
.await
.expect("a revision declaring no identity publishes");
assert_eq!(
store.count("axond_cp_principal").await,
0,
"an undeclared principal is a revoked one, and all of them were undeclared"
);
assert_eq!(
store.count("axond_cp_principal_role").await,
0,
"a grant does not outlive the principal that held it"
);
assert_eq!(
store.column("SELECT slug FROM axond_cp_tenant").await,
vec!["acme"],
"revoking a directory is not deleting a tenant"
);
}
#[tokio::test]
async fn a_retired_tenants_name_is_reusable_and_a_retained_projects_is_refused() {
let Some((store, _, _)) = journal().await else {
return;
};
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "acme", state()))
.await
.expect("the first tenant publishes");
let mut reused_project = DesiredState::new();
reused_project
.insert(tenant(1, "acme"))
.and_then(|state| state.insert(project(&tenant_id(1), 5, "core")))
.expect("a tenant and one project of it is valid");
let error = store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"project reuse",
reused_project,
77,
))
.await
.expect_err("a name a projected project row holds cannot be taken");
assert_eq!(
error.category(),
FailureCategory::Conflict,
"a taken name is a conflict the caller resolves by renaming: {error}"
);
assert!(
!error.retryable(),
"replaying the same name can never succeed: {error}"
);
assert!(
matches!(error, ControlPlaneError::NameTaken { .. }),
"{error}"
);
assert!(error.to_string().contains("core"), "{error}");
assert_eq!(
store.desired_revision().await.expect("head"),
Some(first.id),
"a refused publication is not a published one"
);
let mut reused = DesiredState::new();
reused
.insert(tenant(21, "acme"))
.expect("a state declaring one tenant is valid");
store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"reuse",
reused,
79,
))
.await
.expect("a retired tenant's name is not held against its successor");
assert_eq!(
store
.column(
"SELECT lifecycle FROM axond_cp_tenant WHERE slug = 'acme' \
ORDER BY tenant_id"
)
.await,
vec!["deleted", "active"],
"the name is held by the declared tenant, and kept by the retired row"
);
assert_eq!(
store.count("axond_cp_tenant").await,
2,
"the retired tenant's row is retained, not deleted"
);
}
#[tokio::test]
async fn a_projects_name_is_reclaimed_by_renaming_the_project_that_holds_it() {
let Some((store, _, _)) = journal().await else {
return;
};
let tenant = tenant_id(1);
let mut before = state();
before
.insert(project(&tenant, 3, "edge"))
.expect("a tenant with a project");
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "acme", before.clone()))
.await
.expect("the original project publishes");
let mut reclaimed = before;
reclaimed
.supersede(project_body(3, 1, "Edge Retired").version_at(
Slug::parse("edge-retired").expect("a slug"),
ResourceVersionNumber::FIRST.next(),
))
.and_then(|state| state.insert(project(&tenant, 4, "edge")))
.expect("renaming one project and naming another is valid");
store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"reclaim",
reclaimed,
78,
))
.await
.expect("a name its holder gave up in the same revision is free");
assert_eq!(
store
.column(&format!(
"SELECT project_id FROM axond_cp_project WHERE slug = 'edge' \
AND tenant_id = '{tenant}'"
))
.await,
vec![project_id(4).to_string()],
"the new project holds the name"
);
}
#[tokio::test]
async fn a_revision_a_workload_published_is_attributed_to_its_principal() {
let Some((store, _, _)) = journal().await else {
return;
};
let actor = Actor::Workload {
tenant: tenant_id(1),
principal: principal_id(32),
};
let mut candidate = candidate(ExpectedRevision::Empty, "by-workload", state());
candidate.mutation.actor = actor.clone();
candidate.audit.actor = actor.clone();
let published = store
.publish_revision(candidate)
.await
.expect("a workload's revision publishes");
let trail = store.audit_trail(published.id).await.expect("audit trail");
assert_eq!(trail.len(), 1);
assert_eq!(trail[0].actor, actor);
assert_eq!(
store
.column("SELECT actor_principal_id FROM axond_cp_mutation")
.await,
vec![principal_id(32).to_string()],
"the mutation carries the workload it was made by"
);
}
#[tokio::test]
async fn two_owners_may_trade_names_in_one_revision() {
let Some((store, _, _)) = journal().await else {
return;
};
let slug = |slug: &str| Slug::parse(slug).expect("a slug");
let mut before = state();
before
.insert(tenant(11, "globex"))
.and_then(|state| state.insert(project(&tenant_id(1), 3, "edge")))
.expect("two tenants and two projects of one of them are valid");
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "named", before.clone()))
.await
.expect("the original names publish");
let second = ResourceVersionNumber::FIRST.next();
let mut swapped = before;
swapped
.supersede(tenant_body(1, "Acme").version_at(slug("globex"), second))
.and_then(|state| {
state.supersede(tenant_body(11, "Globex").version_at(slug("acme"), second))
})
.and_then(|state| {
state.supersede(project_body(2, 1, "Core").version_at(slug("edge"), second))
})
.and_then(|state| {
state.supersede(project_body(3, 1, "Edge").version_at(slug("core"), second))
})
.expect("a state in which names are exchanged is valid");
store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"swap",
swapped,
90,
))
.await
.expect("names that are exchanged rather than duplicated must publish");
assert_eq!(
store
.column("SELECT slug FROM axond_cp_tenant ORDER BY tenant_id")
.await,
vec!["globex", "acme"],
"each tenant holds the name the other gave up"
);
assert_eq!(
store
.column("SELECT slug FROM axond_cp_project ORDER BY project_id")
.await,
vec!["edge", "core"]
);
}
#[tokio::test]
async fn an_upgraded_deployment_keeps_no_rule_the_projection_cannot_defer() {
let Some((store, _, _)) = journal().await else {
return;
};
store
.attempt(
"CREATE UNIQUE INDEX axond_cp_tenant_slug_idx ON axond_cp_tenant (slug) \
WHERE lifecycle <> 'deleted'",
)
.await
.expect("the rule 0002 left behind");
let forward = schema::MIGRATIONS
.iter()
.find(|migration| migration.name.contains("tenancy_constraints"))
.expect("the forward tenancy migration ships");
store
.attempt(forward.sql)
.await
.expect("applying the forward migration must not fail");
assert_eq!(
store
.column(
"SELECT indexname::text FROM pg_indexes \
WHERE schemaname = current_schema() \
AND indexname = 'axond_cp_tenant_slug_idx'"
)
.await,
Vec::<String>::new(),
"an index the projection cannot defer outlived the migration that replaced it"
);
let slug = |slug: &str| Slug::parse(slug).expect("a slug");
let mut before = state();
before
.insert(tenant(11, "globex"))
.expect("two tenants are valid");
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "named", before.clone()))
.await
.expect("the original names publish");
let second = ResourceVersionNumber::FIRST.next();
let mut swapped = before;
swapped
.supersede(tenant_body(1, "Acme").version_at(slug("globex"), second))
.and_then(|state| {
state.supersede(tenant_body(11, "Globex").version_at(slug("acme"), second))
})
.expect("a state in which names are exchanged is valid");
store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"swap",
swapped,
91,
))
.await
.expect("an upgraded deployment must accept a traded name too");
}
#[tokio::test]
async fn two_principals_may_trade_identities_in_one_revision() {
let Some((store, _, _)) = journal().await else {
return;
};
let tenant = tenant_id(1);
let identity = |seed: u64, slug: &str, credential: Credential, version| {
IdentityBody::new(
principal_id(seed),
DisplayName::parse(slug).expect("a display name"),
credential,
[Role::TenantAdmin],
)
.expect("an identity granting a role")
.version_at(
ResourceScope::Tenant(tenant),
Slug::parse(slug).expect("a slug"),
version,
)
};
let oidc = |subject: &str| Credential::Oidc {
issuer: "https://idp.example".to_owned(),
subject: subject.to_owned(),
};
let key = |seed: u8| Credential::MintedKey {
digest: Some(Checksum::of(workload_key(seed).as_bytes())),
};
let first_version = ResourceVersionNumber::FIRST;
let mut before = state();
before
.insert(identity(40, "ada", oidc("ada"), first_version))
.and_then(|state| state.insert(identity(41, "grace", oidc("grace"), first_version)))
.and_then(|state| state.insert(identity(42, "builder", key(0xa1), first_version)))
.and_then(|state| state.insert(identity(43, "deployer", key(0xa2), first_version)))
.expect("two humans and two workloads of one tenant are valid");
let first = store
.publish_revision(candidate(
ExpectedRevision::Empty,
"directory",
before.clone(),
))
.await
.expect("the original directory publishes");
let second = first_version.next();
let mut swapped = before;
swapped
.supersede(identity(40, "ada", oidc("grace"), second))
.and_then(|state| state.supersede(identity(41, "grace", oidc("ada"), second)))
.and_then(|state| state.supersede(identity(42, "builder", key(0xa2), second)))
.and_then(|state| state.supersede(identity(43, "deployer", key(0xa1), second)))
.expect("a state in which identities are exchanged is valid");
store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"reassign",
swapped,
91,
))
.await
.expect("identities that are exchanged rather than shared must publish");
assert_eq!(
store
.column(
"SELECT subject FROM axond_cp_principal WHERE subject IS NOT NULL \
ORDER BY principal_id"
)
.await,
vec!["grace", "ada"],
"each administrator signs in as the subject the other gave up"
);
assert_eq!(
store
.column(
"SELECT key_digest FROM axond_cp_principal WHERE key_digest IS NOT NULL \
ORDER BY principal_id"
)
.await,
vec![
Checksum::of(workload_key(0xa2).as_bytes()).to_string(),
Checksum::of(workload_key(0xa1).as_bytes()).to_string(),
],
"and each workload authenticates with the key the other gave up"
);
}
#[tokio::test]
async fn a_project_may_move_to_another_tenant_with_what_is_scoped_into_it() {
let Some((store, _, _)) = journal().await else {
return;
};
let scoped = |tenant: u64, version| {
IdentityBody::new(
principal_id(44),
DisplayName::parse("Builder").expect("a display name"),
Credential::Oidc {
issuer: "https://idp.example".to_owned(),
subject: "builder".to_owned(),
},
[Role::Developer],
)
.expect("an identity granting a role")
.version_at(
ResourceScope::Project {
tenant: tenant_id(tenant),
project: project_id(3),
},
Slug::parse("builder").expect("a slug"),
version,
)
};
let first_version = ResourceVersionNumber::FIRST;
let mut before = state();
before
.insert(tenant(11, "globex"))
.and_then(|state| state.insert(project(&tenant_id(1), 3, "edge")))
.and_then(|state| state.insert(scoped(1, first_version)))
.expect("two tenants, a project of one, and a developer in that project");
let first = store
.publish_revision(candidate(ExpectedRevision::Empty, "owned", before.clone()))
.await
.expect("the original ownership publishes");
let second = first_version.next();
let mut moved = before;
moved
.supersede(
project_body(3, 11, "Edge")
.version_at(Slug::parse("edge").expect("a slug"), second),
)
.and_then(|state| state.supersede(scoped(11, second)))
.expect("a project and its developer moving together is valid");
store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(first.id),
"move",
moved,
92,
))
.await
.expect("a reassignment the state declares consistently must publish");
assert_eq!(
store
.column(&format!(
"SELECT tenant_id FROM axond_cp_project WHERE project_id = '{}'",
project_id(3)
))
.await,
vec![tenant_id(11).to_string()]
);
assert_eq!(
store
.column(&format!(
"SELECT tenant_id FROM axond_cp_principal WHERE principal_id = '{}'",
principal_id(44)
))
.await,
vec![tenant_id(11).to_string()],
"the principal moved with the project it is scoped into"
);
}
#[tokio::test]
async fn a_tenant_lifecycle_transition_is_a_row_update_and_never_a_delete() {
let Some((store, _, _)) = journal().await else {
return;
};
let mut previous = store
.publish_revision(candidate(ExpectedRevision::Empty, "active", state()))
.await
.expect("an active tenant publishes")
.id;
for (index, lifecycle) in [TenantLifecycle::Disabled, TenantLifecycle::Deleted]
.into_iter()
.enumerate()
{
let mut state = state();
let mut version = ResourceVersionNumber::FIRST.next();
for _ in 0..index {
version = version.next();
}
state
.supersede(
tenant_body(1, "Acme")
.in_lifecycle(lifecycle)
.version_at(Slug::parse("acme").expect("a slug"), version),
)
.expect("a later version of the same tenant");
previous = store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(previous),
lifecycle.as_str(),
state,
80 + u64::try_from(index).expect("two transitions") * 2,
))
.await
.expect("a lifecycle transition publishes")
.id;
assert_eq!(
store.column("SELECT lifecycle FROM axond_cp_tenant").await,
vec![lifecycle.as_str()]
);
assert_eq!(
store.column("SELECT slug FROM axond_cp_project").await,
vec!["core"],
"a tenant's projects are not erased by its lifecycle"
);
}
assert!(
store.count("axond_cp_audit_event").await >= 3,
"the trail of a deleted tenant is retained"
);
}
#[tokio::test]
async fn a_retired_tenants_slug_is_reusable_and_a_restore_retires_who_took_it() {
let Some((store, _, _)) = journal().await else {
return;
};
let acme = Slug::parse("acme").expect("a slug");
let mut head = store
.publish_revision(candidate(ExpectedRevision::Empty, "acme", state()))
.await
.expect("an active tenant publishes")
.id;
let mut deleted = state();
deleted
.supersede(
tenant_body(1, "Acme")
.in_lifecycle(TenantLifecycle::Deleted)
.version_at(acme.clone(), ResourceVersionNumber::FIRST.next()),
)
.expect("a later version of the same tenant");
head = store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(head),
"delete",
deleted,
90,
))
.await
.expect("a deletion publishes")
.id;
let mut reused = DesiredState::new();
reused
.insert(tenant(21, "acme"))
.expect("a state declaring one tenant is valid");
head = store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(head),
"reuse",
reused,
94,
))
.await
.expect("a deleted tenant's slug is free for another tenant")
.id;
assert_eq!(
store
.column("SELECT lifecycle FROM axond_cp_tenant ORDER BY slug, lifecycle")
.await,
vec!["active", "deleted"],
"both rows are retained; only one is live under the name"
);
let mut restored = DesiredState::new();
restored
.insert(
tenant_body(1, "Acme").version_at(acme, ResourceVersionNumber::FIRST.next().next()),
)
.expect("a state declaring one tenant is valid");
store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(head),
"restore",
restored,
98,
))
.await
.expect("the revision that restores the tenant retires the one that took its name");
assert_eq!(
store
.column(
"SELECT lifecycle FROM axond_cp_tenant WHERE slug = 'acme' \
ORDER BY lifecycle"
)
.await,
vec!["active", "deleted"],
"one live tenant holds the name; the retired row keeps the name it held"
);
assert_eq!(
store
.column("SELECT tenant_id FROM axond_cp_tenant WHERE lifecycle = 'active'")
.await
.len(),
1,
"exactly one tenant is live, and the restore is it"
);
}
#[tokio::test]
async fn a_tenant_a_revision_stops_declaring_is_retired_and_keeps_its_history() {
let Some((store, _, _)) = journal().await else {
return;
};
let both = store
.publish_revision(candidate(
ExpectedRevision::Empty,
"two tenants",
state_with_second_tenant(),
))
.await
.expect("two tenants publish");
assert_eq!(
store
.column("SELECT lifecycle FROM axond_cp_tenant ORDER BY tenant_id")
.await,
vec!["active", "active"]
);
let dropped = store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(both.id),
"one tenant",
state(),
120,
))
.await
.expect("a revision declaring one of them publishes");
assert_eq!(
store
.column("SELECT lifecycle FROM axond_cp_tenant ORDER BY tenant_id")
.await,
vec!["active", "deleted"],
"an undeclared tenant is still recorded as servable"
);
let published = store
.load_revision(dropped.id)
.await
.expect("the revision it just published hydrates");
let tenancy = Tenancy::of(published.state()).expect("the published tenancy resolves");
assert!(
!tenancy.is_served(tenant_id(11)) && tenancy.tenant(tenant_id(11)).is_none(),
"the projection and the published snapshot disagree about who is served"
);
assert!(
tenancy.is_served(tenant_id(1)),
"retiring one tenant retired another"
);
assert_eq!(
store
.column("SELECT slug FROM axond_cp_project ORDER BY slug")
.await,
vec!["core"],
"the surviving tenant's project is untouched"
);
assert!(
store.count("axond_cp_mutation").await >= 2
&& store.count("axond_cp_audit_event").await >= 2,
"the retired tenant's history is retained"
);
store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(dropped.id),
"two tenants again",
state_with_second_tenant(),
124,
))
.await
.expect("re-declaring the tenant publishes");
assert_eq!(
store
.column("SELECT lifecycle FROM axond_cp_tenant ORDER BY tenant_id")
.await,
vec!["active", "active"],
"a re-declared tenant is still retired"
);
}
#[tokio::test]
async fn a_revision_that_declares_no_tenancy_retires_no_tenant() {
let Some((store, _, _)) = journal().await else {
return;
};
let both = store
.publish_revision(candidate(
ExpectedRevision::Empty,
"two tenants",
state_with_second_tenant(),
))
.await
.expect("two tenants publish");
let rolled_back = store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(both.id),
"pre-tenancy",
state_a_pre_tenancy_build_published(),
130,
))
.await
.expect("a pre-tenancy revision publishes on a tenancy-aware build");
assert_eq!(
store
.column("SELECT lifecycle FROM axond_cp_tenant ORDER BY tenant_id")
.await,
vec!["active", "active"],
"a rollback to a pre-tenancy revision deleted the deployment's tenants"
);
assert_eq!(
store
.column("SELECT slug FROM axond_cp_project ORDER BY slug")
.await,
vec!["core"],
"and their projects"
);
store
.publish_revision(candidate_with_mutation(
ExpectedRevision::Exactly(rolled_back.id),
"one tenant",
state(),
131,
))
.await
.expect("a revision declaring one tenant publishes");
assert_eq!(
store
.column("SELECT lifecycle FROM axond_cp_tenant ORDER BY tenant_id")
.await,
vec!["active", "deleted"],
"an authoritative snapshot stopped reconciling what it omits"
);
}
#[tokio::test]
async fn a_projected_row_cannot_name_an_absent_tenant_or_another_tenants_project() {
let Some((store, _, _)) = journal().await else {
return;
};
store
.publish_revision(candidate(
ExpectedRevision::Empty,
"directory",
state_with_directory(),
))
.await
.expect("a revision declaring a directory publishes");
let tenant = tenant_id(1);
let project = project_id(2);
let stranger = tenant_id(11);
let refusal = store
.attempt(&format!(
"INSERT INTO axond_cp_project (project_id, tenant_id, slug, revision_id) \
VALUES ('{}', '{stranger}', 'smuggled', 'rev_x')",
project_id(12)
))
.await
.expect_err("a project needs an existing tenant");
assert!(refusal.to_string().contains("foreign key"), "{refusal}");
let refusal = store
.attempt(&format!(
"INSERT INTO axond_cp_principal (principal_id, resource_id, identity_kind, \
scope_kind, tenant_id, project_id, slug, display_name, key_digest, revision_id) \
VALUES ('{}', 'res_x', 'workload', 'project', '{stranger}', '{project}', \
'confused', 'Confused', {DIGEST}, 'rev_x')",
PrincipalId::new(uuid(44))
))
.await
.expect_err("a project belongs to exactly one tenant");
assert!(refusal.to_string().contains("foreign key"), "{refusal}");
for (kind, columns, values) in [
(
"human",
"issuer, subject, key_digest",
format!("'https://idp.example', 'smuggled', {DIGEST}"),
),
(
"workload",
"issuer, subject",
"'https://idp.example', 's'".to_owned(),
),
] {
let refusal = store
.attempt(&format!(
"INSERT INTO axond_cp_principal (principal_id, resource_id, identity_kind, \
scope_kind, tenant_id, slug, display_name, {columns}, revision_id) \
VALUES ('{}', 'res_x', '{kind}', 'tenant', '{tenant}', 'mixed', 'Mixed', \
{values}, 'rev_x')",
PrincipalId::new(uuid(45))
))
.await
.expect_err("one identity kind, one credential shape");
assert!(refusal.to_string().contains("check"), "{refusal}");
}
let refusal = store
.attempt(&format!(
"INSERT INTO axond_cp_principal_role (principal_id, role) VALUES ('{}', 'root')",
PrincipalId::new(uuid(33))
))
.await
.expect_err("the role vocabulary is closed");
assert!(refusal.to_string().contains("check"), "{refusal}");
}
#[tokio::test]
async fn a_journal_row_cannot_name_a_tenant_this_deployment_has_no_row_for() {
let Some((store, _, _)) = journal().await else {
return;
};
store
.publish_revision(candidate(
ExpectedRevision::Empty,
"directory",
state_with_directory(),
))
.await
.expect("a revision declaring a directory publishes");
assert_eq!(
store
.column(
"SELECT conname::text FROM pg_constraint \
WHERE contype = 'f' AND NOT convalidated \
AND connamespace = current_schema()::regnamespace \
ORDER BY conname"
)
.await,
vec![
"axond_cp_audit_event_actor_tenant_fkey",
"axond_cp_mutation_actor_tenant_fkey",
"axond_cp_mutation_tenant_fkey",
"axond_cp_resource_version_tenant_fkey",
],
"the unvalidated keys are the journal's, and validation is not pending on others"
);
let stranger = tenant_id(11);
let refusal = store
.attempt(&format!(
"INSERT INTO axond_cp_resource_version \
(resource_kind, resource_id, version, scope_kind, tenant_id, slug, body_form, \
body_inline, content_checksum, serializer) \
VALUES ('alias', '{}', 1, 'tenant', '{stranger}', 'smuggled', 'inline', \
'\\x7b7d'::bytea, {DIGEST}, 'json')",
crate::desired_state::ResourceId::new(uuid(46))
))
.await
.expect_err("a stored version needs a tenant this deployment has a row for");
assert!(
refusal
.to_string()
.contains("axond_cp_resource_version_tenant_fkey"),
"{refusal}"
);
let refusal = store
.attempt(&format!(
"INSERT INTO axond_cp_mutation \
(mutation_id, actor_kind, actor_tenant_id, actor_principal_id, mutation_kind, \
scope_kind, tenant_id, idempotency_key, submitted_at) \
VALUES ('{}', 'workload', '{stranger}', '{}', 'publish', 'tenant', '{}', \
'smuggled', now())",
MutationId::new(uuid(47)),
PrincipalId::new(uuid(48)),
tenant_id(1)
))
.await
.expect_err("attribution names a tenant that exists");
assert!(
refusal.to_string().contains("actor_tenant_fkey"),
"{refusal}"
);
}
#[tokio::test]
async fn a_revision_naming_an_undeclared_tenant_records_that_owner_as_deleted() {
let Some((store, _, _)) = journal().await else {
return;
};
let owner = tenant_id(1);
let first = store
.publish_revision(candidate(
ExpectedRevision::Empty,
"pre-tenancy",
state_a_pre_tenancy_build_published(),
))
.await
.expect("a revision without owner rows publishes");
assert_eq!(
store
.column("SELECT lifecycle FROM axond_cp_tenant ORDER BY tenant_id")
.await,
vec!["deleted"],
"an owner history names is not a tenant anybody granted"
);
assert_eq!(
store
.column("SELECT slug FROM axond_cp_tenant ORDER BY tenant_id")
.await,
vec![owner.to_string()],
"a recorded owner holds no name a declared tenant could want, being deleted"
);
let second = store
.publish_revision(candidate(
ExpectedRevision::Exactly(first.id),
"declared",
state(),
))
.await
.expect("declaring the tenant the history named publishes");
assert_eq!(
store
.column("SELECT lifecycle || ' ' || slug FROM axond_cp_tenant")
.await,
vec!["active acme"],
"declaring a recorded owner is what turns it into a tenant"
);
store
.publish_revision(candidate(
ExpectedRevision::Exactly(second.id),
"rolled back",
state_a_pre_tenancy_build_published(),
))
.await
.expect("a rollback to the pre-tenancy revision publishes");
assert_eq!(
store
.column("SELECT lifecycle || ' ' || slug FROM axond_cp_tenant")
.await,
vec!["active acme"],
"recording an owner rewrote a live tenant, which a rollback must never do"
);
}
#[tokio::test]
async fn recording_historys_owners_does_not_make_an_undeclared_tenants_admin_storable() {
let Some((store, _, _)) = journal().await else {
return;
};
let undeclared = tenant_id(1);
let mut state = state_a_pre_tenancy_build_published();
state
.insert(human(
41,
"ghost-admin",
ResourceScope::Tenant(undeclared),
&[Role::TenantAdmin],
))
.expect("an identity in an undeclared tenant is well-formed desired state");
let refusal = store
.publish_revision(candidate(ExpectedRevision::Empty, "ghost admin", state))
.await
.expect_err("an administrator in a tenant no revision declares is refused");
assert!(
matches!(refusal, ControlPlaneError::Invalid(_)),
"and refused as damaged state rather than as an outage: {refusal:?}"
);
assert!(
store
.column("SELECT tenant_id FROM axond_cp_tenant")
.await
.is_empty(),
"a refused publication stores nothing, owner rows included"
);
}
#[tokio::test]
async fn a_publication_cannot_leave_an_owner_row_behind_by_attributing_itself_to_one() {
let Some((store, _, _)) = journal().await else {
return;
};
let stranger = tenant_id(11);
let mut candidate = candidate(
ExpectedRevision::Empty,
"borrowed attribution",
state_with_directory(),
);
candidate.mutation.actor = Actor::Workload {
tenant: stranger,
principal: principal_id(49),
};
candidate.audit.actor = candidate.mutation.actor.clone();
let refusal = store.publish_revision(candidate).await.expect_err(
"a change attributed to a tenant this deployment has no row for is refused",
);
assert!(
matches!(refusal, ControlPlaneError::Denied { .. }),
"and refused permanently rather than retried as an outage: {refusal:?}"
);
assert!(
!store
.column("SELECT tenant_id FROM axond_cp_tenant")
.await
.contains(&stranger.to_string()),
"and leaves no owner row for the tenant it named"
);
}
#[test]
fn a_projection_refusal_names_the_constraint_and_not_the_row_it_hit() {
let message = projection_refusal(
"principal",
"",
false,
Some("axond_cp_principal_key_digest_unique"),
);
assert!(
message.contains("axond_cp_principal_key_digest_unique"),
"the refusal names the rule it broke: {message}"
);
assert!(
message.contains("no retry clears this"),
"and that it is not an outage: {message}"
);
for leaked in ["sha256:", "Key (", "issuer", "subject", "=("] {
assert!(
!message.contains(leaked),
"the refusal could carry the row it collided with ({leaked}): {message}"
);
}
assert!(
projection_refusal(
"project",
"edge",
true,
Some("axond_cp_principal_project_fkey")
)
.contains("contradicts an ownership row"),
"an ownership contradiction is described as one"
);
}
#[test]
fn observation_integrity_sqlstates_are_permanent() {
for code in [
&SqlState::FOREIGN_KEY_VIOLATION,
&SqlState::UNIQUE_VIOLATION,
&SqlState::CHECK_VIOLATION,
&SqlState::NOT_NULL_VIOLATION,
&SqlState::EXCLUSION_VIOLATION,
] {
assert!(
is_permanent_observation_sqlstate(code),
"{code:?} must not be classified as an outage"
);
}
assert!(!is_permanent_observation_sqlstate(
&SqlState::CONNECTION_FAILURE
));
assert!(!is_permanent_observation_sqlstate(
&SqlState::T_R_SERIALIZATION_FAILURE
));
}
#[test]
fn a_journal_ownership_refusal_names_the_key_and_not_the_tenant_it_refused() {
let message = journal_ownership_refusal(Some("axond_cp_mutation_actor_tenant_fkey"));
assert!(
message.contains("axond_cp_mutation_actor_tenant_fkey"),
"the refusal names the rule it broke: {message}"
);
assert!(
message.contains("publish a revision that declares it"),
"and the remedy an operator has: {message}"
);
for leaked in ["Key (", "=(", "tenant_id)=", "sha256:"] {
assert!(
!message.contains(leaked),
"the refusal could carry the key it refused ({leaked}): {message}"
);
}
assert!(
!journal_ownership_refusal(None).is_empty(),
"an unnamed constraint still says what happened"
);
}
#[tokio::test]
async fn denied_actions_are_recorded_and_read_back_per_tenant_newest_first() {
let Some((store, _, _)) = journal().await else {
return;
};
let tenant = tenant_id(1);
let other = tenant_id(11);
let mine = [
denial(90, ResourceScope::Tenant(tenant), DenialReason::OutOfScope),
denial(
91,
ResourceScope::Project {
tenant,
project: project_id(2),
},
DenialReason::RoleLacksAction,
),
];
let theirs = denial(92, ResourceScope::Tenant(other), DenialReason::CrossTenant);
let deployment = denial(
93,
ResourceScope::Deployment,
DenialReason::TenantNotAdministrable,
);
for denial in mine.iter().chain([&theirs, &deployment]) {
store.record_denial(denial).await.expect("record a refusal");
}
store
.record_denial(&mine[0])
.await
.expect("a retry is not a second attempt");
let read = store
.denials(&DenialPage::for_scope(Some(tenant)), 10)
.await
.expect("read");
assert_eq!(read, vec![mine[1].clone(), mine[0].clone()]);
assert_eq!(
store
.denials(&DenialPage::for_scope(Some(other)), 10)
.await
.expect("read"),
vec![theirs.clone()],
"one tenant's refusals are not another's"
);
assert_eq!(
store
.denials(&DenialPage::for_scope(None), 10)
.await
.expect("read"),
vec![deployment]
);
assert_eq!(
store
.denials(&DenialPage::for_scope(Some(tenant)), 1)
.await
.expect("read"),
vec![mine[1].clone()]
);
assert!(
store
.denials(&DenialPage::for_scope(Some(tenant)), 0)
.await
.expect("read")
.len()
== 1,
"a zero limit is clamped to a page rather than refused"
);
let mut workload = denial(94, ResourceScope::Tenant(tenant), DenialReason::CrossTenant);
workload.actor = Actor::Workload {
tenant,
principal: PrincipalId::new(uuid(33)),
};
store.record_denial(&workload).await.expect("record");
assert_eq!(
store
.denials(&DenialPage::for_scope(Some(tenant)), 1)
.await
.expect("read")
.first()
.map(|denial| denial.actor.clone()),
Some(workload.actor.clone())
);
let mut intruder = denial(95, ResourceScope::Tenant(tenant), DenialReason::CrossTenant);
intruder.actor = Actor::Workload {
tenant: other,
principal: PrincipalId::new(uuid(36)),
};
store.record_denial(&intruder).await.expect("record");
assert!(
store
.denials(&DenialPage::for_scope(Some(tenant)), 10)
.await
.expect("read")
.contains(&intruder),
"a refusal against this tenant is unreadable by anyone"
);
assert!(
!store
.denials(&DenialPage::for_scope(Some(other)), 10)
.await
.expect("read")
.contains(&intruder),
"the attempting tenant's page is not where a refusal against another one belongs"
);
assert!(
!store
.denials(&DenialPage::for_scope(None), 10)
.await
.expect("read")
.contains(&intruder),
"the deployment page is the rows that named no tenant"
);
}
#[tokio::test]
async fn the_tenancy_migration_is_idempotent_when_an_operator_reapplies_it() {
let Some((store, _, _)) = journal().await else {
return;
};
let upgrade: Vec<_> = schema::MIGRATIONS
.iter()
.filter(|migration| migration.version > 1)
.collect();
assert_eq!(upgrade.len(), 4, "the upgrade migrations ship");
for migration in &upgrade {
store
.attempt(migration.sql)
.await
.expect("re-applying a tenancy migration must not fail");
}
assert_eq!(
store
.column(
"SELECT relname::text FROM pg_class \
WHERE relnamespace = current_schema()::regnamespace \
AND relrowsecurity AND NOT relforcerowsecurity \
ORDER BY relname"
)
.await,
Vec::<String>::new(),
"a table left enabled but unforced by the second application"
);
store
.publish_revision(candidate(
ExpectedRevision::Empty,
"after a re-apply",
state_with_directory(),
))
.await
.expect("a revision publishes against a twice-migrated schema");
}
#[tokio::test]
async fn the_tables_outside_the_wall_are_the_ones_the_migration_names() {
let Some((store, _, _)) = journal().await else {
return;
};
assert_eq!(
store
.column(
"SELECT relname::text FROM pg_class \
WHERE relnamespace = current_schema()::regnamespace \
AND relkind = 'r' AND NOT relrowsecurity \
ORDER BY relname"
)
.await,
vec!["axond_cp_schema_migration".to_owned()],
"the unwalled tables are not the ones 0002 and the runbook name"
);
}
#[tokio::test]
async fn a_session_pinned_to_one_tenant_reads_no_other_tenants_rows() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
let first = store
.publish_revision(candidate(
ExpectedRevision::Empty,
"two tenants",
two_tenant_directory_state(),
))
.await
.expect("two tenants publish");
let mut theirs = candidate(
ExpectedRevision::Exactly(first.id),
"their change",
two_tenant_directory_state(),
);
theirs.mutation.scope = ResourceScope::Tenant(tenant_id(11));
store
.publish_revision(theirs)
.await
.expect("the other tenant's change publishes");
store
.record_denial(&denial(
95,
ResourceScope::Tenant(tenant_id(11)),
DenialReason::CrossTenant,
))
.await
.expect("record a refusal against the other tenant");
let mut theirs = denial(96, ResourceScope::Deployment, DenialReason::OutOfScope);
theirs.actor = Actor::Workload {
tenant: tenant_id(11),
principal: principal_id(36),
};
store
.record_denial(&theirs)
.await
.expect("record a deployment-scoped refusal by another tenant's workload");
let role = format!("{schema}_reader");
store
.attempt(&format!(
"CREATE ROLE {role} LOGIN PASSWORD 'reader'; \
GRANT USAGE ON SCHEMA {schema} TO {role}; \
GRANT SELECT ON ALL TABLES IN SCHEMA {schema} TO {role}"
))
.await
.expect("create the reading role");
let mut config: Config = dsn.parse().expect("test dsn");
config.user(&role).password("reader");
config.connect_timeout(Duration::from_secs(5));
let (client, connection) = config
.connect(crate::usage::tls_connector())
.await
.expect("connect as the reading role");
tokio::spawn(async move {
let _ = connection.await;
});
client
.batch_execute(&format!(
"SET search_path TO {schema}; SET axond.tenant_id = '{}'",
tenant_id(1)
))
.await
.expect("pin the session to one tenant");
for (table, expected) in [
("axond_cp_tenant", vec![tenant_id(1).to_string()]),
("axond_cp_project", vec![tenant_id(1).to_string()]),
] {
let rows: Vec<String> = client
.query(&format!("SELECT tenant_id FROM {table}"), &[])
.await
.expect("read as the pinned session")
.iter()
.map(|row| row.get(0))
.collect();
assert_eq!(rows, expected, "{table} leaked another tenant's rows");
}
let denials: i64 = client
.query_one("SELECT count(*) FROM axond_cp_access_denial", &[])
.await
.expect("read")
.get(0);
assert_eq!(
denials, 0,
"another tenant's refusals — including a deployment-scoped one its workload \
attempted — are not visible"
);
let mut aimed_here = denial(
97,
ResourceScope::Tenant(tenant_id(1)),
DenialReason::CrossTenant,
);
aimed_here.actor = Actor::Workload {
tenant: tenant_id(11),
principal: principal_id(37),
};
store
.record_denial(&aimed_here)
.await
.expect("record a refusal against the pinned tenant by another tenant's workload");
let readable: Vec<String> = client
.query(
"SELECT denial_id FROM axond_cp_access_denial ORDER BY denial_id",
&[],
)
.await
.expect("read")
.iter()
.map(|row| row.get(0))
.collect();
assert_eq!(
readable,
vec![aimed_here.id.to_string()],
"the tenant a cross-tenant attempt targeted cannot read that it happened"
);
let granted: Vec<String> = client
.query(
"SELECT DISTINCT p.slug FROM axond_cp_principal_role AS r \
JOIN axond_cp_principal AS p USING (principal_id) ORDER BY p.slug",
&[],
)
.await
.expect("read")
.iter()
.map(|row| row.get(0))
.collect();
assert_eq!(
granted,
["admin", "deployer", "dev", "root"],
"the grant table leaked another tenant's roles"
);
let orphaned: i64 = client
.query_one("SELECT count(*) FROM axond_cp_principal_role", &[])
.await
.expect("read")
.get(0);
assert_eq!(
orphaned, 4,
"a grant whose principal this session cannot see is a grant it cannot see"
);
let journal: Vec<Option<String>> = client
.query(
"SELECT DISTINCT tenant_id FROM axond_cp_mutation ORDER BY tenant_id",
&[],
)
.await
.expect("read")
.iter()
.map(|row| row.get(0))
.collect();
assert_eq!(
journal,
vec![Some(tenant_id(1).to_string())],
"the mutation journal leaked another tenant's changes"
);
store
.attempt(&format!(
"INSERT INTO axond_cp_mutation (mutation_id, actor_kind, actor_tenant_id, actor_principal_id, mutation_kind, scope_kind, tenant_id, idempotency_key, submitted_at) VALUES ('mut_00000000-0000-7000-8000-00000000f00d', 'workload', '{}', '{}', 'publish', 'tenant', '{}', 'theirs-against-ours', now())",
tenant_id(11),
principal_id(38),
tenant_id(1),
))
.await
.expect("record a change against this tenant by another tenant's workload");
let attributed: Vec<String> = client
.query(
"SELECT mutation_id FROM axond_cp_mutation WHERE actor_kind = 'workload' ORDER BY mutation_id",
&[],
)
.await
.expect("read")
.iter()
.map(|row| row.get(0))
.collect();
assert_eq!(
attributed,
vec!["mut_00000000-0000-7000-8000-00000000f00d".to_owned()],
"a tenant cannot read a change recorded against it by another tenant's workload"
);
let events: i64 = client
.query_one("SELECT count(*) FROM axond_cp_audit_event", &[])
.await
.expect("read")
.get(0);
assert_eq!(
events, 1,
"an audit event is visible exactly when the mutation it describes is"
);
let scoped: Vec<String> = client
.query(
"SELECT DISTINCT tenant_id FROM axond_cp_resource_version \
WHERE tenant_id IS NOT NULL",
&[],
)
.await
.expect("read")
.iter()
.map(|row| row.get(0))
.collect();
assert_eq!(scoped, vec![tenant_id(1).to_string()]);
let deployment: i64 = client
.query_one(
"SELECT count(*) FROM axond_cp_resource_version WHERE tenant_id IS NULL",
&[],
)
.await
.expect("read")
.get(0);
assert!(deployment > 0, "shared state is not hidden from a tenant");
for table in [
"axond_cp_head",
"axond_cp_revision",
"axond_cp_revision_entry",
"axond_cp_revision_blob",
"axond_cp_blob",
"axond_cp_resource_dependency",
] {
let visible: i64 = client
.query_one(&format!("SELECT count(*) FROM {table}"), &[])
.await
.expect("read as the pinned session")
.get(0);
assert_eq!(visible, 0, "{table} leaked the publication chain");
}
let keys: i64 = client
.query_one("SELECT count(*) FROM axond_cp_idempotency", &[])
.await
.expect("read as the pinned session")
.get(0);
assert_eq!(
keys, 1,
"an idempotency key is visible exactly when the mutation it records is"
);
assert_eq!(
store
.column("SELECT tenant_id FROM axond_cp_tenant ORDER BY tenant_id")
.await
.len(),
2
);
}
fn look(
scope: ScopeRef,
result: DiscoveryResult,
completeness: DiscoveryCompleteness,
observed_at: SystemTime,
) -> DiscoveryObservation {
DiscoveryObservation::new(
scope,
observation_target(),
result,
completeness,
DiscoverySource::ProviderListing,
observed_at,
)
}
fn observation_target() -> TargetRef {
TargetRef::parse("openai", "gpt-4o-mini").expect("a well-formed target")
}
fn instant(seconds: u64) -> SystemTime {
UNIX_EPOCH + Duration::from_secs(seconds)
}
#[tokio::test]
async fn discovery_evidence_survives_a_restart_without_the_probes_own_words() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
store
.publish_revision(candidate(ExpectedRevision::Empty, "state", state()))
.await
.expect("a tenant and a project exist to own evidence");
let scope = ScopeRef::project(tenant_id(1), project_id(2));
let key = AvailabilityKey::new(scope, observation_target());
let current = look(
scope,
DiscoveryResult::Indeterminate,
DiscoveryCompleteness::Partial,
instant(200),
)
.detailed("HTTP 500 from https://api.example.test/v1/models?key=sk-live-secret");
let retained = look(
scope,
DiscoveryResult::Present,
DiscoveryCompleteness::Complete,
instant(100),
)
.expiring_at(instant(900));
store
.save(&EvidenceWrite::of_rows(vec![
StoredObservation {
key: key.clone(),
slot: ObservationSlot::Current,
observation: current.clone(),
definitive_at: Some(instant(100)),
},
StoredObservation {
key: key.clone(),
slot: ObservationSlot::LastKnownGood,
observation: retained.clone(),
definitive_at: Some(instant(100)),
},
]))
.await
.expect("evidence is written");
let restarted = second_store(&dsn, &schema).await;
let read = restarted.load(None).await.expect("evidence is read back");
assert_eq!(read.len(), 2, "both slots survive");
assert_eq!(read[0].slot, ObservationSlot::Current);
assert_eq!(read[1].slot, ObservationSlot::LastKnownGood);
assert_eq!(read[0].definitive_at, Some(instant(100)));
assert!(
read.iter().all(|row| row.observation.detail.is_none()),
"a probe's detail must not be durable"
);
assert!(
read[0].observation.is_same_look(¤t),
"the current look is the same evidence it was written as"
);
assert!(read[1].observation.is_same_look(&retained));
assert_eq!(read[1].observation.expires_at, Some(instant(900)));
assert_eq!(
store
.load(Some(ScopeRef::tenant(tenant_id(1))))
.await
.expect("read")
.len(),
0,
"a project's evidence is not the tenant's"
);
}
#[tokio::test]
async fn a_looks_instant_survives_the_column_it_is_stored_in() {
let Some((store, _dsn, _schema)) = journal().await else {
return;
};
store
.publish_revision(candidate(ExpectedRevision::Empty, "state", state()))
.await
.expect("a tenant exists to own evidence");
let scope = ScopeRef::tenant(tenant_id(1));
let key = AvailabilityKey::new(scope, observation_target());
let taken = UNIX_EPOCH + Duration::new(1_700_000_000, 123_456_789);
let observation = look(
scope,
DiscoveryResult::Present,
DiscoveryCompleteness::Complete,
taken,
)
.expiring_at(taken + Duration::from_secs(600));
store
.save(&EvidenceWrite::of_rows(vec![StoredObservation {
key,
slot: ObservationSlot::Current,
observation: observation.clone(),
definitive_at: Some(observation.observed_at),
}]))
.await
.expect("evidence is written");
let read = store.load(None).await.expect("evidence is read back");
assert_eq!(read.len(), 1);
assert!(
read[0].observation.is_same_look(&observation),
"a stored look is the look that was stored"
);
assert_eq!(read[0].definitive_at, Some(observation.observed_at));
}
#[tokio::test]
async fn saving_a_record_replaces_the_evidence_it_held() {
let Some((store, _dsn, _schema)) = journal().await else {
return;
};
store
.publish_revision(candidate(ExpectedRevision::Empty, "state", state()))
.await
.expect("a tenant exists to own evidence");
let scope = ScopeRef::tenant(tenant_id(1));
let key = AvailabilityKey::new(scope, observation_target());
let positive = look(
scope,
DiscoveryResult::Present,
DiscoveryCompleteness::Complete,
instant(100),
);
store
.save(&EvidenceWrite::of_rows(vec![
StoredObservation {
key: key.clone(),
slot: ObservationSlot::Current,
observation: positive.clone(),
definitive_at: Some(instant(100)),
},
StoredObservation {
key: key.clone(),
slot: ObservationSlot::LastKnownGood,
observation: positive,
definitive_at: Some(instant(100)),
},
]))
.await
.expect("write");
let dropped = look(
scope,
DiscoveryResult::Absent,
DiscoveryCompleteness::Complete,
instant(300),
);
store
.save(&EvidenceWrite::of_rows(vec![StoredObservation {
key: key.clone(),
slot: ObservationSlot::Current,
observation: dropped.clone(),
definitive_at: Some(instant(300)),
}]))
.await
.expect("write the conclusion");
let read = store.load(Some(scope)).await.expect("read");
assert_eq!(read.len(), 1, "the discredited fallback is gone");
assert_eq!(read[0].slot, ObservationSlot::Current);
assert!(read[0].observation.is_same_look(&dropped));
assert_eq!(read[0].definitive_at, Some(instant(300)));
}
#[tokio::test]
async fn a_key_that_holds_no_evidence_holds_none_in_storage_either() {
let Some((store, _dsn, _schema)) = journal().await else {
return;
};
store
.publish_revision(candidate(ExpectedRevision::Empty, "state", state()))
.await
.expect("a tenant exists to own evidence");
let scope = ScopeRef::tenant(tenant_id(1));
let key = AvailabilityKey::new(scope, observation_target());
store
.save(&EvidenceWrite::of_rows(vec![StoredObservation {
key: key.clone(),
slot: ObservationSlot::Current,
observation: look(
scope,
DiscoveryResult::Present,
DiscoveryCompleteness::Complete,
instant(100),
),
definitive_at: Some(instant(100)),
}]))
.await
.expect("evidence is written");
store
.save(
&EvidenceWrite::default().clearing([EvidenceClear::new(key.clone(), instant(100))]),
)
.await
.expect("the key is cleared");
assert!(
store.load(None).await.expect("read").is_empty(),
"a cleared key does not survive the process that cleared it"
);
let write = EvidenceWrite::of_index(
&AvailabilityIndex::builder()
.record(
key.clone(),
AvailabilityRecord {
definitive_at: Some(instant(100)),
..AvailabilityRecord::enabled()
},
)
.build(),
);
assert!(write.rows().is_empty());
assert_eq!(write.cleared(), &[EvidenceClear::new(key, instant(100))]);
}
#[tokio::test]
async fn an_empty_replica_write_does_not_clear_another_replicas_evidence() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
store
.publish_revision(candidate(ExpectedRevision::Empty, "state", state()))
.await
.expect("a tenant exists to own evidence");
let scope = ScopeRef::tenant(tenant_id(1));
let key = AvailabilityKey::new(scope, observation_target());
store
.save(&EvidenceWrite::of_rows(vec![StoredObservation {
key: key.clone(),
slot: ObservationSlot::Current,
observation: look(
scope,
DiscoveryResult::Present,
DiscoveryCompleteness::Complete,
instant(100),
),
definitive_at: Some(instant(100)),
}]))
.await
.expect("replica A writes its look");
let replica_b = second_store(&dsn, &schema).await;
let empty_projection = AvailabilityIndex::builder()
.record(key.clone(), AvailabilityRecord::enabled())
.build();
replica_b
.save(&EvidenceWrite::of_index(&empty_projection))
.await
.expect("replica B has no owned evidence to replace");
let rows = store.load(Some(scope)).await.expect("read evidence");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].key, key);
assert_eq!(rows[0].observation.result, DiscoveryResult::Present);
}
#[tokio::test]
async fn a_stale_replica_write_does_not_replace_newer_evidence() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
store
.publish_revision(candidate(ExpectedRevision::Empty, "state", state()))
.await
.expect("a tenant exists to own evidence");
let scope = ScopeRef::tenant(tenant_id(1));
let key = AvailabilityKey::new(scope, observation_target());
let replica_b = second_store(&dsn, &schema).await;
let newer = look(
scope,
DiscoveryResult::Present,
DiscoveryCompleteness::Complete,
instant(200),
);
store
.save(&EvidenceWrite::of_rows(vec![StoredObservation {
key: key.clone(),
slot: ObservationSlot::Current,
observation: newer.clone(),
definitive_at: Some(instant(200)),
}]))
.await
.expect("replica A writes the newer look");
let stale = look(
scope,
DiscoveryResult::Absent,
DiscoveryCompleteness::Complete,
instant(100),
);
replica_b
.save(&EvidenceWrite::of_rows(vec![StoredObservation {
key: key.clone(),
slot: ObservationSlot::Current,
observation: stale,
definitive_at: Some(instant(100)),
}]))
.await
.expect("replica B writes its stale view");
replica_b
.save(
&EvidenceWrite::default().clearing([EvidenceClear::new(key.clone(), instant(100))]),
)
.await
.expect("replica B clears only evidence it knows about");
let rows = store.load(Some(scope)).await.expect("read evidence");
assert_eq!(rows.len(), 1);
assert!(rows[0].observation.is_same_look(&newer));
}
#[tokio::test]
async fn a_behind_replicas_write_does_not_erase_a_newer_records_fallback() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
store
.publish_revision(candidate(ExpectedRevision::Empty, "state", state()))
.await
.expect("a tenant exists to own evidence");
let scope = ScopeRef::tenant(tenant_id(1));
let key = AvailabilityKey::new(scope, observation_target());
let current = look(
scope,
DiscoveryResult::Absent,
DiscoveryCompleteness::Complete,
instant(300),
);
let fallback = look(
scope,
DiscoveryResult::Present,
DiscoveryCompleteness::Complete,
instant(200),
);
store
.save(&EvidenceWrite::of_rows(vec![
StoredObservation {
key: key.clone(),
slot: ObservationSlot::Current,
observation: current.clone(),
definitive_at: Some(instant(300)),
},
StoredObservation {
key: key.clone(),
slot: ObservationSlot::LastKnownGood,
observation: fallback.clone(),
definitive_at: Some(instant(200)),
},
]))
.await
.expect("replica A writes both slots");
let replica_b = second_store(&dsn, &schema).await;
replica_b
.save(&EvidenceWrite::of_rows(vec![StoredObservation {
key: key.clone(),
slot: ObservationSlot::Current,
observation: look(
scope,
DiscoveryResult::Absent,
DiscoveryCompleteness::Complete,
instant(250),
),
definitive_at: Some(instant(250)),
}]))
.await
.expect("replica B writes the only look it holds");
let rows = store.load(Some(scope)).await.expect("read evidence");
assert_eq!(
rows.len(),
2,
"a behind replica replaces neither slot of a record it knows less about"
);
let held: Vec<(ObservationSlot, SystemTime)> = rows
.iter()
.map(|row| (row.slot, row.observation.observed_at))
.collect();
assert_eq!(
held,
vec![
(ObservationSlot::Current, instant(300)),
(ObservationSlot::LastKnownGood, instant(200)),
]
);
}
#[tokio::test]
async fn an_observation_foreign_key_failure_is_permanent_not_unavailable() {
let Some((store, _dsn, _schema)) = journal().await else {
return;
};
let scope = ScopeRef::tenant(tenant_id(999));
let error = store
.save(&EvidenceWrite::of_rows(vec![StoredObservation {
key: AvailabilityKey::new(scope, observation_target()),
slot: ObservationSlot::Current,
observation: look(
scope,
DiscoveryResult::Present,
DiscoveryCompleteness::Complete,
instant(100),
),
definitive_at: Some(instant(100)),
}]))
.await
.expect_err("the observation FK refuses a missing tenant");
assert!(matches!(error, ControlPlaneError::Denied { .. }));
assert!(
!error.retryable(),
"an FK violation is not cleared by retrying"
);
}
#[tokio::test]
async fn one_tenants_discovery_evidence_is_not_another_tenants() {
let Some((store, dsn, schema)) = journal().await else {
return;
};
store
.publish_revision(candidate(
ExpectedRevision::Empty,
"two tenants",
two_tenant_directory_state(),
))
.await
.expect("two tenants publish");
for tenant in [tenant_id(1), tenant_id(11)] {
let scope = ScopeRef::tenant(tenant);
store
.save(&EvidenceWrite::of_rows(vec![StoredObservation {
key: AvailabilityKey::new(scope, observation_target()),
slot: ObservationSlot::Current,
observation: look(
scope,
DiscoveryResult::Present,
DiscoveryCompleteness::Complete,
instant(100),
),
definitive_at: Some(instant(100)),
}]))
.await
.expect("write");
}
let mine = store
.load(Some(ScopeRef::tenant(tenant_id(1))))
.await
.expect("read");
assert_eq!(mine.len(), 1);
assert_eq!(mine[0].key.scope.tenant, tenant_id(1));
assert_eq!(store.load(None).await.expect("read").len(), 2);
let role = format!("{schema}_availability_reader");
store
.attempt(&format!(
"CREATE ROLE {role} LOGIN PASSWORD 'reader'; \
GRANT USAGE ON SCHEMA {schema} TO {role}; \
GRANT SELECT ON ALL TABLES IN SCHEMA {schema} TO {role}"
))
.await
.expect("create the reading role");
let mut config: Config = dsn.parse().expect("test dsn");
config.user(&role).password("reader");
config.connect_timeout(Duration::from_secs(5));
let (client, connection) = config
.connect(crate::usage::tls_connector())
.await
.expect("connect as the reading role");
tokio::spawn(async move {
let _ = connection.await;
});
client
.batch_execute(&format!(
"SET search_path TO {schema}; SET axond.tenant_id = '{}'",
tenant_id(11)
))
.await
.expect("pin the session to one tenant");
let rows: Vec<String> = client
.query(
"SELECT tenant_id FROM axond_cp_availability_observation",
&[],
)
.await
.expect("read as the pinned session")
.iter()
.map(|row| row.get(0))
.collect();
assert_eq!(
rows,
vec![tenant_id(11).to_string()],
"the observation table leaked another tenant's evidence"
);
}
#[test]
fn a_journal_timestamp_survives_a_round_trip_through_the_column_it_is_stored_in() {
let now = journal_now();
let since = now.duration_since(UNIX_EPOCH).expect("after the epoch");
assert_eq!(
since.subsec_nanos() % 1_000,
0,
"a timestamp with sub-microsecond precision cannot be read back as itself"
);
}
}