use std::fmt::Write as _;
use crate::config::{BudgetBackend, Config, RateLimitBackend};
use crate::desired_state::policy::{
Fenced, PolicyFence, PolicyGeneration, PolicyScope, PolicyTransition, TransitionClass,
TransitionReason,
};
use super::view::PolicyView;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BackendSupport {
budget: BudgetBackend,
namespace_scope: bool,
rate_limit: RateLimitBackend,
}
impl BackendSupport {
pub fn of(config: &Config) -> Self {
Self {
budget: config.budget.backend,
namespace_scope: config.budget.enforces_namespace_scope(),
rate_limit: config.rate_limit.backend,
}
}
const fn shares_spend(self) -> bool {
matches!(self.budget, BudgetBackend::Redis | BudgetBackend::Postgres)
}
const fn shares_concurrency(self) -> bool {
matches!(self.rate_limit, RateLimitBackend::Redis)
}
const fn denies_without_a_document(self) -> bool {
self.shares_spend() || self.shares_concurrency()
}
pub const fn namespace_scope(self) -> bool {
self.namespace_scope
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ActivationRefusal {
#[error("{scope} cannot be enforced by this deployment's backends: {detail}")]
Unsupported { scope: PolicyScope, detail: String },
#[error("{scope} needs a backend migration before it can be activated: {detail}")]
Migration { scope: PolicyScope, detail: String },
#[error("{scope} is not a policy transition this build performs: {detail}")]
Refused { scope: PolicyScope, detail: String },
#[error(
"{scope} governs namespace `{namespace}`, which this candidate still serves without a \
policy document: withdrawing a document from a served namespace would leave it with no \
enforceable cap. Delete the namespace, or publish a document for it"
)]
Withdrawn {
scope: PolicyScope,
namespace: String,
},
#[error(
"namespace `{namespace}` is served by this candidate with no policy document governing \
it, so every request to it would be denied. Publish a tenant-level document as the \
floor before projecting the namespace, or remove it"
)]
Ungoverned { namespace: String },
#[error(
"{scope} states `{field}` as zero, which denies every request for the scope rather than \
capping it. Republish the document with a cap of at least 1, or close the scope through \
tenancy (withdraw the projection, revoke its credentials)"
)]
InvalidCap {
scope: PolicyScope,
field: &'static str,
},
}
impl ActivationRefusal {
#[cfg_attr(not(test), allow(dead_code))]
pub const REASONS: &'static [&'static str] = &[
"unsupported",
"migration",
"refused",
"withdrawn",
"ungoverned",
"invalid_policy",
];
pub const fn reason(&self) -> &'static str {
match self {
Self::Unsupported { .. } => "unsupported",
Self::Migration { .. } => "migration",
Self::Refused { .. } => "refused",
Self::Withdrawn { .. } => "withdrawn",
Self::Ungoverned { .. } => "ungoverned",
Self::InvalidCap { .. } => "invalid_policy",
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub const fn scope(&self) -> Option<PolicyScope> {
match self {
Self::Unsupported { scope, .. }
| Self::Migration { scope, .. }
| Self::Refused { scope, .. }
| Self::Withdrawn { scope, .. }
| Self::InvalidCap { scope, .. } => Some(*scope),
Self::Ungoverned { .. } => None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Activation {
live: Vec<PolicyScope>,
draining: Vec<(PolicyScope, Vec<TransitionReason>)>,
withdrawn: Vec<PolicyScope>,
}
impl Activation {
pub(super) const fn forced() -> Self {
Self {
live: Vec::new(),
draining: Vec::new(),
withdrawn: Vec::new(),
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn live(&self) -> &[PolicyScope] {
&self.live
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn draining(&self) -> &[(PolicyScope, Vec<TransitionReason>)] {
&self.draining
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn withdrawn(&self) -> &[PolicyScope] {
&self.withdrawn
}
pub fn is_noop(&self) -> bool {
self.live.is_empty() && self.draining.is_empty() && self.withdrawn.is_empty()
}
pub(super) fn log(&self, outstanding: &[(PolicyGeneration, u64)]) {
if self.is_noop() && outstanding.is_empty() {
return;
}
let mut draining = String::new();
for (scope, reasons) in &self.draining {
let _ = write!(draining, "{scope} (");
for (index, reason) in reasons.iter().enumerate() {
let _ = write!(draining, "{}{reason:?}", if index == 0 { "" } else { ", " });
}
draining.push_str(") ");
}
tracing::info!(
live = self.live.len(),
draining = self.draining.len(),
withdrawn = self.withdrawn.len(),
draining_scopes = %draining.trim_end(),
outstanding_generations = outstanding.len(),
outstanding_holds = outstanding.iter().map(|(_, count)| count).sum::<u64>(),
"policy activated; holds admitted under a replaced generation keep the terms they \
were granted"
);
}
}
pub(super) fn plan(
active: &PolicyView,
candidate: &PolicyView,
support: BackendSupport,
) -> Result<Activation, ActivationRefusal> {
let mut activation = Activation::default();
for (scope, published) in candidate.published() {
if let Some(bound) = published.body.budget().unenforceable_cap() {
return Err(ActivationRefusal::InvalidCap {
scope: *scope,
field: bound.document_field(),
});
}
supportable(
*scope,
published.body.budget().namespace_limit_microdollars(),
support,
)?;
let Some(current) = active.published().get(scope) else {
let handover = handover(
*scope,
published
.namespaces
.iter()
.filter_map(|namespace| active.governing(namespace))
.map(|inherited| inherited.displaced_by(&published.body)),
)?;
match handover {
Some(reasons) => activation.draining.push((*scope, reasons)),
None => activation.live.push(*scope),
}
continue;
};
if current.body == published.body && current.generation.same_policy(&published.generation) {
if let Some(reasons) = handover(
*scope,
published
.namespaces
.iter()
.filter_map(|namespace| active.governing(namespace))
.filter(|inherited| **inherited != published.body)
.map(|inherited| inherited.displaced_by(&published.body)),
)? {
activation.draining.push((*scope, reasons));
}
continue;
}
let transition = current.body.transition(&published.body);
match transition.class() {
class @ (TransitionClass::Live | TransitionClass::Drain) => {
let mut reasons = match class {
TransitionClass::Drain => transition.reasons().to_vec(),
_ => Vec::new(),
};
if let Some(inherited) = handover(
*scope,
published
.namespaces
.iter()
.filter_map(|namespace| active.governing(namespace))
.filter(|inherited| **inherited != current.body)
.map(|inherited| inherited.displaced_by(&published.body)),
)? {
reasons.extend(inherited);
}
if reasons.is_empty() {
activation.live.push(*scope);
} else {
reasons.sort_unstable();
reasons.dedup();
activation.draining.push((*scope, reasons));
}
}
TransitionClass::MigrationRequired => {
return Err(ActivationRefusal::Migration {
scope: *scope,
detail: format!(
"{} changes the shape of what is stored, not only its values. Drain the \
fleet's holds for this scope, run the backend migration for the new \
layout, restart the fleet on a bootstrap that declares it, and publish \
the document again",
reasons(&transition)
),
});
}
TransitionClass::Refused => {
return Err(ActivationRefusal::Refused {
scope: *scope,
detail: format!(
"{}{}",
reasons(&transition),
fenced(current.generation, published.generation)
),
});
}
}
}
for (scope, current) in active.published() {
if candidate.published().contains_key(scope) {
continue;
}
if let Some(served) = current
.namespaces
.iter()
.find(|namespace| candidate.ungoverned(namespace))
{
return Err(ActivationRefusal::Withdrawn {
scope: *scope,
namespace: served.clone(),
});
}
let handover = handover(
*scope,
current
.namespaces
.iter()
.filter_map(|namespace| candidate.governing(namespace))
.map(|inheriting| current.body.displaced_by(inheriting)),
)?;
match handover {
Some(reasons) => activation.draining.push((*scope, reasons)),
None => activation.withdrawn.push(*scope),
}
}
if let Some(namespace) = candidate
.unenforceable()
.next()
.filter(|_| support.denies_without_a_document())
{
return Err(ActivationRefusal::Ungoverned {
namespace: namespace.to_owned(),
});
}
Ok(activation)
}
fn handover(
scope: PolicyScope,
transitions: impl Iterator<Item = PolicyTransition>,
) -> Result<Option<Vec<TransitionReason>>, ActivationRefusal> {
let mut drain: Vec<TransitionReason> = Vec::new();
for transition in transitions {
match transition.class() {
TransitionClass::Live => {}
TransitionClass::Drain => drain.extend_from_slice(transition.reasons()),
TransitionClass::MigrationRequired => {
return Err(ActivationRefusal::Migration {
scope,
detail: format!(
"{} would take the namespace over from another scope's document and \
change the shape of what is stored with it. Migrate the ledgers and \
restart the fleet on a bootstrap that declares the new layout first",
reasons(&transition)
),
});
}
TransitionClass::Refused => {
return Err(ActivationRefusal::Refused {
scope,
detail: format!(
"{} against the document the namespace is governed by today. Changing \
which scope governs a namespace does not make a change this model \
refuses performable: publish the value the current document enforces, \
or higher",
reasons(&transition)
),
});
}
}
}
drain.sort_unstable();
drain.dedup();
Ok((!drain.is_empty()).then_some(drain))
}
fn supportable(
scope: PolicyScope,
namespace_limit: Option<u64>,
support: BackendSupport,
) -> Result<(), ActivationRefusal> {
if !support.shares_spend() {
return Err(ActivationRefusal::Unsupported {
scope,
detail: format!(
"a published spend cap is a fleet-wide statement, and `[budget] backend = \"{}\"` \
cannot make one: it enforces nothing, or enforces a separate copy per replica. \
Select `redis` or `postgres` in the bootstrap file and restart",
support.budget.as_str()
),
});
}
if !support.shares_concurrency() {
return Err(ActivationRefusal::Unsupported {
scope,
detail: format!(
"every `axond.policy.v1` document states a concurrency ceiling \
(`max_in_flight_per_subject` and `lease_ttl_seconds` are required fields, ADR \
0036), so there is no spend-only policy for a deployment without shared leases \
to enforce it with, and `[rate_limit] backend = \"{}\"` has none. Select \
`redis` in the bootstrap file and restart",
support.rate_limit.as_str()
),
});
}
match (namespace_limit.is_some(), support.namespace_scope()) {
(true, false) => Err(ActivationRefusal::Migration {
scope,
detail: "the document sets a scope-wide cap, but this deployment's budget store is \
laid out without one. Stop the fleet, set `[budget] namespace_scope = true`, \
migrate the ledgers (`axond budget migrate-redis`, or \
`ops/postgres/budget_v2.sql`), restart on that bootstrap, and publish the \
document again"
.to_owned(),
}),
(false, true) => Err(ActivationRefusal::Migration {
scope,
detail: "this deployment's budget store is laid out to carry a scope-wide cap, and \
the document states none. Serving it would leave the scope-wide ledgers \
accumulating against nothing. Publish a document with \
`namespace_limit_microdollars`, or migrate the store back and restart the \
fleet without `[budget] namespace_scope`"
.to_owned(),
}),
_ => Ok(()),
}
}
fn reasons(transition: &PolicyTransition) -> String {
let mut rendered = String::new();
for (index, reason) in transition.reasons().iter().enumerate() {
let _ = write!(rendered, "{}{reason:?}", if index == 0 { "" } else { ", " });
}
if rendered.is_empty() {
rendered.push_str("no classified change");
}
rendered
}
fn fenced(active: PolicyGeneration, candidate: PolicyGeneration) -> String {
match PolicyFence::new(active).admit(candidate) {
Ok(()) => String::new(),
Err(error) => {
let hint = match error {
Fenced::Stale(_) => {
" Roll back by publishing the previous values forward under a higher epoch, \
not by repointing the fleet at an older revision"
}
Fenced::Forked(_) => {
" Two documents claim one epoch, which a restored backup or a second control \
plane produces. Reconcile them before publishing"
}
Fenced::Ahead(_) | Fenced::OtherScope(_) => "",
};
format!(" ({error}).{hint}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::NamespacePolicy;
use crate::desired_state::fixtures::tenant_id;
use crate::policy::fixtures::{body, detailed, generation, stored_zero_cap};
use crate::policy::view::tests::{governed, stateful_config, stateless_config};
fn scope() -> PolicyScope {
PolicyScope::Tenant(tenant_id(1))
}
fn shared() -> BackendSupport {
let mut config = stateful_config();
config.rate_limit.backend = RateLimitBackend::Redis;
config.rate_limit.dsn_env = Some("GW_RATE_LIMIT_REDIS".to_owned());
BackendSupport::of(&config)
}
fn view(document: &crate::desired_state::policy::PolicyBody, revision: u64) -> PolicyView {
PolicyView::of(&governed(
"acme/core",
NamespacePolicy {
body: *document,
generation: generation(document, revision),
},
))
}
fn empty() -> PolicyView {
PolicyView::of(&stateful_config())
}
#[test]
fn a_first_document_binds_live() {
let document = body(scope(), 1, 1_000);
let activation = plan(&empty(), &view(&document, 1), shared()).expect("enforceable");
assert_eq!(activation.live(), [scope()]);
assert!(activation.draining().is_empty());
}
#[test]
fn raising_a_cap_is_live_and_lowering_one_drains() {
let first = body(scope(), 1, 1_000);
let raised = body(scope(), 2, 5_000);
let lowered = body(scope(), 3, 100);
let activation =
plan(&view(&first, 1), &view(&raised, 2), shared()).expect("a raise is live");
assert_eq!(activation.live(), [scope()]);
let activation =
plan(&view(&raised, 2), &view(&lowered, 3), shared()).expect("a lowering drains");
assert_eq!(activation.draining().len(), 1);
assert_eq!(
activation.draining()[0].1,
[TransitionReason::BudgetLowered]
);
}
#[test]
fn a_rollback_is_published_forward_and_a_reversed_epoch_is_refused() {
let old = body(scope(), 4, 1_000);
let new = body(scope(), 5, 9_000);
let rolled_back = body(scope(), 6, 1_000);
plan(&view(&new, 2), &view(&rolled_back, 3), shared())
.expect("restoring the old values under a higher epoch is a drain");
let refusal = plan(&view(&new, 2), &view(&old, 1), shared())
.expect_err("repointing at an older revision is refused");
assert_eq!(refusal.reason(), "refused");
assert!(refusal.to_string().contains("EpochRegressed"), "{refusal}");
assert!(
refusal.to_string().contains("higher epoch"),
"the refusal names the supported rollback: {refusal}"
);
}
#[test]
fn a_forked_epoch_is_refused_and_named() {
let mine = body(scope(), 7, 1_000);
let theirs = body(scope(), 7, 2_000);
let refusal = plan(&view(&mine, 1), &view(&theirs, 2), shared())
.expect_err("one epoch, two policies");
assert_eq!(refusal.reason(), "refused");
assert_eq!(
refusal.scope(),
Some(scope()),
"the refusal names the document it is about"
);
assert!(
refusal.to_string().contains("EpochNotAdvanced"),
"{refusal}"
);
assert!(
refusal.to_string().contains("claims the epoch"),
"the fence's reading is included: {refusal}"
);
}
#[test]
fn a_stored_cap_of_zero_is_refused_at_activation_and_leaves_the_last_good_policy() {
let good = body(scope(), 1, 1_000);
let active = view(&good, 1);
for (zero, field) in [
(
stored_zero_cap(scope(), 2, 0, None),
"budget_limit_microdollars",
),
(
stored_zero_cap(scope(), 2, 1_000, Some(0)),
"namespace_budget_limit_microdollars",
),
] {
let refusal = plan(&active, &view(&zero, 2), shared())
.expect_err("a cap of zero is not a cap this replica starts enforcing");
assert_eq!(refusal.reason(), "invalid_policy");
assert_eq!(refusal.scope(), Some(scope()));
assert!(
refusal.to_string().contains(field),
"the refusal names the field an operator edits: {refusal}"
);
}
let corrected = body(scope(), 2, 900);
let activation =
plan(&active, &view(&corrected, 2), shared()).expect("the corrected cap activates");
assert_eq!(activation.draining().len(), 1);
}
#[test]
fn turning_a_scope_wide_cap_on_needs_a_migration_not_a_publication() {
let flat = body(scope(), 1, 1_000);
let scoped = detailed(scope(), 2, 1_000, Some(10_000), 300, 8, 60, 0);
let refusal = plan(&view(&flat, 1), &view(&scoped, 2), shared())
.expect_err("a layout change is not a publication");
assert_eq!(refusal.reason(), "migration");
assert!(refusal.to_string().contains("namespace_scope"), "{refusal}");
}
#[test]
fn a_per_replica_or_absent_backend_cannot_enforce_a_published_document() {
let document = body(scope(), 1, 1_000);
let mut config = stateful_config();
config.budget.backend = BudgetBackend::InMemory;
let refusal = plan(&empty(), &view(&document, 1), BackendSupport::of(&config))
.expect_err("a per-replica cap is not a fleet-wide cap");
assert_eq!(refusal.reason(), "unsupported");
assert!(refusal.to_string().contains("fleet-wide"), "{refusal}");
let refusal = plan(
&empty(),
&view(&document, 1),
BackendSupport::of(&stateful_config()),
)
.expect_err("no shared lease store, no published ceiling");
assert_eq!(refusal.reason(), "unsupported");
assert!(refusal.to_string().contains("concurrency"), "{refusal}");
}
#[test]
fn a_document_withdrawn_from_a_namespace_that_is_still_served_is_refused() {
let document = body(scope(), 1, 1_000);
let mut without = stateful_config();
without.namespace.push(crate::config::Namespace {
id: "acme/core".to_owned(),
default: true,
allow_platform_fallback: false,
project: None,
policy: None,
});
let refusal = plan(&view(&document, 1), &PolicyView::of(&without), shared())
.expect_err("a served namespace cannot lose its policy");
assert_eq!(refusal.reason(), "withdrawn");
let activation = plan(&view(&document, 1), &empty(), shared())
.expect("a deleted namespace is not a gap");
assert_eq!(activation.withdrawn(), [scope()]);
}
#[test]
fn a_tenant_document_is_only_withdrawable_when_every_namespace_it_governs_is_gone() {
let document = body(scope(), 1, 1_000);
let policy = NamespacePolicy {
body: document,
generation: generation(&document, 1),
};
let mut both = stateful_config();
both.namespace.push(crate::policy::view::tests::projected(
"acme/core",
Some(policy),
));
both.namespace.push(crate::policy::view::tests::projected(
"acme/edge",
Some(policy),
));
let active = PolicyView::of(&both);
let mut survivor = stateful_config();
survivor
.namespace
.push(crate::policy::view::tests::projected("acme/edge", None));
let refusal = plan(&active, &PolicyView::of(&survivor), shared())
.expect_err("a sibling namespace is still governed by this document");
assert_eq!(refusal.reason(), "withdrawn");
assert!(refusal.to_string().contains("acme/edge"), "{refusal}");
let activation = plan(&active, &empty(), shared())
.expect("every namespace the document governed is gone");
assert_eq!(activation.withdrawn(), [scope()]);
}
#[test]
fn a_scope_handover_is_not_a_withdrawal_in_either_direction() {
let project = PolicyScope::Project {
tenant: tenant_id(1),
project: crate::desired_state::fixtures::project_id(1),
};
let tenants = view(&body(scope(), 1, 1_000), 1);
let projects = PolicyView::of(&governed(
"acme/core",
NamespacePolicy {
body: body(project, 1, 1_000),
generation: generation(&body(project, 1, 1_000), 2),
},
));
plan(&tenants, &projects, shared())
.expect("a project publishing its own document still caps the namespace");
plan(&projects, &tenants, shared())
.expect("dropping it hands the namespace back to its tenant's document");
}
#[test]
fn a_first_project_document_is_classified_against_what_it_displaces() {
let project = PolicyScope::Project {
tenant: tenant_id(1),
project: crate::desired_state::fixtures::project_id(1),
};
let tighter = body(project, 1, 500);
let looser = body(project, 1, 5_000);
let tenants = view(&body(scope(), 1, 1_000), 1);
let handover = |document: &crate::desired_state::policy::PolicyBody| {
PolicyView::of(&governed(
"acme/core",
NamespacePolicy {
body: *document,
generation: generation(document, 2),
},
))
};
let activation = plan(&tenants, &handover(&tighter), shared())
.expect("a project may cap itself below its tenant");
assert_eq!(
activation.draining(),
[
(project, vec![TransitionReason::BudgetLowered]),
(scope(), vec![TransitionReason::BudgetLowered]),
],
"cutting the cap a namespace was enforcing is a drain, whichever \
document states it — reported for the scope taking over and the \
one handing it off"
);
let activation = plan(&tenants, &handover(&looser), shared())
.expect("raising the cap binds immediately");
assert!(activation.draining().is_empty());
assert_eq!(activation.live(), [project]);
}
#[test]
fn an_unchanged_document_taking_a_namespace_over_still_drains() {
let tenant = body(scope(), 1, 1_000);
let projects = body(project(), 1, 500);
let with = |core: &crate::desired_state::policy::PolicyBody| {
let mut config = stateful_config();
config.namespace.push(crate::policy::view::tests::projected(
"acme/edge",
Some(NamespacePolicy {
body: projects,
generation: generation(&projects, 2),
}),
));
config.namespace.push(crate::policy::view::tests::projected(
"acme/core",
Some(NamespacePolicy {
body: *core,
generation: generation(core, 2),
}),
));
PolicyView::of(&config)
};
let activation = plan(&with(&tenant), &with(&projects), shared())
.expect("taking a namespace over is not a refusal");
assert!(
activation
.draining()
.contains(&(project(), vec![TransitionReason::BudgetLowered])),
"the document did not move, but the namespace it took over was \
enforcing more: {activation:?}"
);
let activation = plan(&with(&projects), &with(&projects), shared())
.expect("republishing changes nothing");
assert!(activation.is_noop(), "{activation:?}");
}
#[test]
fn a_document_that_is_edited_while_taking_a_namespace_over_drains_for_both() {
let tenant = body(scope(), 1, 10_000);
let before = body(project(), 1, 500);
let raised = body(project(), 2, 900);
let with = |projects: &crate::desired_state::policy::PolicyBody,
core: &crate::desired_state::policy::PolicyBody| {
let mut config = stateful_config();
config.namespace.push(crate::policy::view::tests::projected(
"acme/edge",
Some(NamespacePolicy {
body: *projects,
generation: generation(projects, 2),
}),
));
config.namespace.push(crate::policy::view::tests::projected(
"acme/core",
Some(NamespacePolicy {
body: *core,
generation: generation(core, 2),
}),
));
PolicyView::of(&config)
};
let activation = plan(&with(&before, &tenant), &with(&raised, &raised), shared())
.expect("raising a cap and taking a namespace over is not a refusal");
assert!(
activation
.draining()
.contains(&(project(), vec![TransitionReason::BudgetLowered])),
"the edit raised the cap, but the namespace it took over was \
enforcing 10_000: {activation:?}"
);
assert!(
!activation.live().contains(&project()),
"a scope that stranded a hold is not live: {activation:?}"
);
}
fn project() -> PolicyScope {
PolicyScope::Project {
tenant: tenant_id(1),
project: crate::desired_state::fixtures::project_id(1),
}
}
fn other_project() -> PolicyScope {
PolicyScope::Project {
tenant: tenant_id(1),
project: crate::desired_state::fixtures::project_id(2),
}
}
#[test]
fn a_candidate_serving_an_ungoverned_namespace_is_refused_before_it_is_published() {
let document = body(scope(), 1, 1_000);
let mut candidate = governed(
"acme/core",
NamespacePolicy {
body: document,
generation: generation(&document, 1),
},
);
candidate
.namespace
.push(crate::policy::view::tests::projected("acme/new", None));
let refusal = plan(&empty(), &PolicyView::of(&candidate), shared())
.expect_err("a namespace with no document would deny every request to it");
assert_eq!(refusal.reason(), "ungoverned");
assert_eq!(refusal.scope(), None, "no document, no scope to name");
assert!(refusal.to_string().contains("acme/new"), "{refusal}");
plan(&empty(), &view(&document, 1), shared())
.expect("the same candidate without the ungoverned namespace activates");
let mut unenforcing = stateful_config();
unenforcing.budget.backend = BudgetBackend::None;
unenforcing.budget.dsn_env = None;
plan(
&empty(),
&PolicyView::of(&candidate),
BackendSupport::of(&unenforcing),
)
.expect_err("a document still needs a store that can enforce it");
candidate
.namespace
.retain(|namespace| namespace.id != "acme/core" || namespace.policy.is_none());
plan(
&empty(),
&PolicyView::of(&candidate),
BackendSupport::of(&unenforcing),
)
.expect("a deployment that enforces no caps converges on a revision with no document");
}
#[test]
fn a_handover_cannot_lower_the_token_floor() {
let floor = |scope, epoch, token_epoch| {
detailed(scope, epoch, 1_000, None, 300, 8, 60, token_epoch)
};
let revoked = floor(scope(), 2, 500);
let tenants = view(&revoked, 1);
let projects = |body: &crate::desired_state::policy::PolicyBody| {
PolicyView::of(&governed(
"acme/core",
NamespacePolicy {
body: *body,
generation: generation(body, 2),
},
))
};
let lower = projects(&floor(project(), 1, 100));
let refusal = plan(&tenants, &lower, shared())
.expect_err("a project cannot un-revoke its tenant's tokens by publishing over it");
assert_eq!(refusal.reason(), "refused");
assert!(
refusal.to_string().contains("TokenFloorLowered"),
"{refusal}"
);
let refusal = plan(&projects(&floor(project(), 1, 900)), &tenants, shared())
.expect_err("dropping a project document cannot lower the floor either");
assert_eq!(refusal.reason(), "refused");
assert!(
refusal.to_string().contains("TokenFloorLowered"),
"{refusal}"
);
plan(&tenants, &projects(&floor(project(), 1, 500)), shared())
.expect("a handover that keeps the floor is a handover");
}
#[test]
fn dropping_a_project_document_for_a_tighter_tenant_one_drains() {
let tenants = body(scope(), 1, 1_000);
let mut both = governed(
"acme/core",
NamespacePolicy {
body: body(project(), 1, 5_000),
generation: generation(&body(project(), 1, 5_000), 2),
},
);
both.namespace.push(crate::policy::view::tests::projected(
"acme/edge",
Some(NamespacePolicy {
body: tenants,
generation: generation(&tenants, 2),
}),
));
let mut handed_back = stateful_config();
for namespace in ["acme/core", "acme/edge"] {
handed_back
.namespace
.push(crate::policy::view::tests::projected(
namespace,
Some(NamespacePolicy {
body: tenants,
generation: generation(&tenants, 3),
}),
));
}
let activation = plan(
&PolicyView::of(&both),
&PolicyView::of(&handed_back),
shared(),
)
.expect("the namespace stays governed, by its tenant's document");
assert_eq!(
activation.draining(),
[
(scope(), vec![TransitionReason::BudgetLowered]),
(project(), vec![TransitionReason::BudgetLowered]),
],
"a tighter document taking the namespace over is a drain, not a silent withdrawal — \
reported for the scope handing it off and, though its own document never moved, for \
the one taking it over"
);
assert!(activation.withdrawn().is_empty());
}
#[test]
fn a_scope_handing_over_several_namespaces_reports_every_reason_once() {
let held = detailed(project(), 1, 5_000, None, 300, 8, 60, 0);
let mut governs_both = governed(
"acme/core",
NamespacePolicy {
body: held,
generation: generation(&held, 1),
},
);
governs_both
.namespace
.push(crate::policy::view::tests::projected(
"acme/edge",
Some(NamespacePolicy {
body: held,
generation: generation(&held, 1),
}),
));
let cheaper = detailed(scope(), 1, 1_000, None, 300, 8, 60, 0);
let narrower = detailed(other_project(), 1, 5_000, None, 300, 4, 60, 0);
let mut handed_over = governed(
"acme/core",
NamespacePolicy {
body: cheaper,
generation: generation(&cheaper, 2),
},
);
handed_over
.namespace
.push(crate::policy::view::tests::projected(
"acme/edge",
Some(NamespacePolicy {
body: narrower,
generation: generation(&narrower, 2),
}),
));
let activation = plan(
&PolicyView::of(&governs_both),
&PolicyView::of(&handed_over),
shared(),
)
.expect("both namespaces stay governed");
let handover = activation
.draining()
.iter()
.find(|(scope, _)| *scope == project())
.expect("the scope handing both namespaces over drains");
assert_eq!(
handover.1,
vec![
TransitionReason::BudgetLowered,
TransitionReason::ConcurrencyLowered
],
"the union of what each namespace's new document strands, not the last one's"
);
}
#[test]
fn a_document_restated_by_a_later_revision_is_not_a_transition() {
let document = body(scope(), 1, 1_000);
let activation = plan(&view(&document, 1), &view(&document, 2), shared())
.expect("a restatement is enforceable");
assert!(
activation.is_noop(),
"the same policy, published again: {activation:?}"
);
}
#[test]
fn every_refusal_reason_is_a_catalogued_revision_label() {
let refusals = [
ActivationRefusal::Unsupported {
scope: scope(),
detail: String::new(),
},
ActivationRefusal::Migration {
scope: scope(),
detail: String::new(),
},
ActivationRefusal::Refused {
scope: scope(),
detail: String::new(),
},
ActivationRefusal::Withdrawn {
scope: scope(),
namespace: "acme/core".to_owned(),
},
ActivationRefusal::Ungoverned {
namespace: "acme/core".to_owned(),
},
ActivationRefusal::InvalidCap {
scope: scope(),
field: "budget_limit_microdollars",
},
];
for refusal in &refusals {
let reason = refusal.reason();
assert!(
ActivationRefusal::REASONS.contains(&reason),
"`{reason}` is declared where the compile vocabulary reads it from"
);
assert!(
crate::convergence::reconciler::REVISION_REASONS.contains(&reason),
"`{reason}` is a label a refused publication produces"
);
assert_eq!(
crate::status::StatusReason::from_revision_reason(reason),
crate::status::StatusReason::PolicyRejected,
"`{reason}` reaches the status contract as a decided code"
);
}
for reason in ActivationRefusal::REASONS {
assert!(
refusals.iter().any(|refusal| &refusal.reason() == reason),
"`{reason}` is declared but no refusal produces it"
);
}
}
#[test]
fn a_stateless_deployment_has_nothing_to_activate() {
let view = PolicyView::of(&stateless_config());
let activation = plan(&view, &view, BackendSupport::of(&stateless_config()))
.expect("a file is not a publication");
assert!(activation.is_noop());
}
}