use std::collections::BTreeMap;
use std::time::Duration;
use crate::config::{Config, Mode};
use crate::desired_state::policy::{PolicyBody, PolicyGeneration, PolicyScope};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BudgetCaps {
pub subject_microdollars: u64,
pub namespace_microdollars: Option<u64>,
pub reservation_ttl: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConcurrencyCaps {
pub max_in_flight_per_subject: u64,
pub lease_ttl: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ActivePolicy {
pub budget: Option<BudgetCaps>,
pub concurrency: Option<ConcurrencyCaps>,
pub generation: Option<PolicyGeneration>,
}
impl ActivePolicy {
pub(super) const fn unenforceable(&self) -> bool {
self.budget.is_none()
}
fn bootstrap(config: &Config) -> Self {
Self {
budget: Some(BudgetCaps {
subject_microdollars: config.budget.limit_microdollars,
namespace_microdollars: config.budget.namespace_limit_microdollars,
reservation_ttl: Duration::from_secs(config.budget.reservation_ttl_seconds),
}),
concurrency: Some(ConcurrencyCaps {
max_in_flight_per_subject: config.rate_limit.max_in_flight_per_subject as u64,
lease_ttl: Duration::from_secs(config.rate_limit.lease_ttl_seconds),
}),
generation: None,
}
}
fn published(body: &PolicyBody, generation: PolicyGeneration) -> Self {
Self {
budget: Some(BudgetCaps {
subject_microdollars: body.budget().subject_limit_microdollars(),
namespace_microdollars: body.budget().namespace_limit_microdollars(),
reservation_ttl: Duration::from_secs(body.budget().reservation_ttl_seconds()),
}),
concurrency: Some(ConcurrencyCaps {
max_in_flight_per_subject: body.concurrency().max_in_flight_per_subject(),
lease_ttl: Duration::from_secs(body.concurrency().lease_ttl_seconds()),
}),
generation: Some(generation),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Published {
pub(super) body: PolicyBody,
pub(super) generation: PolicyGeneration,
pub(super) namespaces: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyView {
default: ActivePolicy,
by_namespace: BTreeMap<String, ActivePolicy>,
published: BTreeMap<PolicyScope, Published>,
}
impl PolicyView {
pub fn of(config: &Config) -> Self {
let stateful = config.mode == Mode::Stateful;
let bootstrap = ActivePolicy::bootstrap(config);
let mut by_namespace = BTreeMap::new();
let mut published = BTreeMap::new();
for namespace in &config.namespace {
let policy = match &namespace.policy {
Some(policy) => {
published
.entry(policy.body.scope())
.or_insert_with(|| Published {
body: policy.body,
generation: policy.generation,
namespaces: Vec::new(),
})
.namespaces
.push(namespace.id.clone());
ActivePolicy::published(&policy.body, policy.generation)
}
None if stateful => ActivePolicy::default(),
None => bootstrap,
};
by_namespace.insert(namespace.id.clone(), policy);
}
Self {
default: if stateful {
ActivePolicy::default()
} else {
bootstrap
},
by_namespace,
published,
}
}
pub fn policy(&self, namespace: &str) -> ActivePolicy {
self.by_namespace
.get(namespace)
.copied()
.unwrap_or(self.default)
}
pub fn enforces(&self, generation: PolicyGeneration) -> bool {
self.by_namespace.values().any(|policy| {
policy
.generation
.is_some_and(|active| active.same_policy(&generation))
})
}
pub(super) fn published(&self) -> &BTreeMap<PolicyScope, Published> {
&self.published
}
pub(super) fn ungoverned(&self, namespace: &str) -> bool {
self.by_namespace
.get(namespace)
.is_some_and(ActivePolicy::unenforceable)
}
pub(super) fn unenforceable(&self) -> impl Iterator<Item = &str> {
self.by_namespace
.iter()
.filter(|(_, policy)| policy.unenforceable())
.map(|(namespace, _)| namespace.as_str())
}
pub(super) fn governing(&self, namespace: &str) -> Option<&PolicyBody> {
self.published
.values()
.find(|published| {
published
.namespaces
.iter()
.any(|governed| governed == namespace)
})
.map(|published| &published.body)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::config::NamespacePolicy;
use crate::desired_state::fixtures::tenant_id;
use crate::policy::fixtures::{body, generation};
pub(crate) fn stateless_config() -> Config {
Config::from_toml_str(
r#"
[[namespace]]
id = "platform"
default = true
[[provider]]
id = "openai"
kind = "openai"
base_url = "https://api.openai.com/v1"
[[gateway_key]]
env = "STATIC_KEY"
namespace = "platform"
[budget]
backend = "in-memory"
limit_microdollars = 5_000
reservation_ttl_seconds = 120
[rate_limit]
max_in_flight_per_subject = 4
"#,
)
.expect("a valid stateless config")
}
pub(crate) fn stateful_config() -> Config {
Config::from_toml_str(
r#"
mode = "stateful"
[control_plane]
dsn_env = "GW_CONTROL_PLANE_DSN"
[secret_store]
kek_env = "GW_SECRET_STORE_KEK"
[[admin_breakglass]]
env = "GW_ADMIN_BREAKGLASS"
[budget]
backend = "redis"
dsn_env = "GW_BUDGET_REDIS"
"#,
)
.expect("a valid stateful bootstrap")
}
pub(crate) fn projected(
namespace: &str,
policy: Option<NamespacePolicy>,
) -> crate::config::Namespace {
crate::config::Namespace {
id: namespace.to_owned(),
default: true,
allow_platform_fallback: false,
project: Some(crate::config::ProjectIdentity {
tenant: tenant_id(1),
project: crate::desired_state::fixtures::project_id(1),
}),
policy,
}
}
pub(crate) fn governed(namespace: &str, policy: NamespacePolicy) -> Config {
let mut config = stateful_config();
config.namespace.push(projected(namespace, Some(policy)));
config
}
#[test]
fn a_stateless_deployment_enforces_the_file_for_every_namespace() {
let view = PolicyView::of(&stateless_config());
let policy = view.policy("platform");
assert_eq!(
policy.budget,
Some(BudgetCaps {
subject_microdollars: 5_000,
namespace_microdollars: None,
reservation_ttl: Duration::from_secs(120),
})
);
assert_eq!(
policy.concurrency.map(|c| c.max_in_flight_per_subject),
Some(4)
);
assert_eq!(policy.generation, None, "a file has no generation");
assert_eq!(
view.policy("a-namespace-the-file-never-named"),
policy,
"the file governs the deployment, not a list of names"
);
}
#[test]
fn a_stateful_namespace_with_no_document_has_no_policy_at_all() {
let mut config = stateful_config();
config.namespace.push(projected("acme/core", None));
let view = PolicyView::of(&config);
assert_eq!(view.policy("acme/core"), ActivePolicy::default());
assert!(view.policy("acme/core").budget.is_none());
}
#[test]
fn a_published_document_governs_its_namespace_and_carries_its_generation() {
let scope = PolicyScope::Tenant(tenant_id(1));
let document = body(scope, 3, 9_000);
let generation = generation(&document, 7);
let view = PolicyView::of(&governed(
"acme/core",
NamespacePolicy {
body: document,
generation,
},
));
let policy = view.policy("acme/core");
assert_eq!(
policy.budget.expect("published").subject_microdollars,
9_000
);
assert_eq!(policy.generation, Some(generation));
assert!(view.enforces(generation));
assert!(!view.ungoverned("acme/core"));
let restated = crate::policy::fixtures::generation(&document, 8);
assert_ne!(restated, generation, "a new revision, a new generation");
assert!(view.enforces(restated));
}
}