use std::collections::BTreeMap;
pub mod factory;
pub use factory::{
body_secret_placeholder, GuestSpec, HttpAuth, ProductBuild, ProductFactory,
PULLED_NEVER_DIALLED,
};
pub mod flow;
pub mod guardian;
pub mod pull;
pub mod pulled;
#[derive(
Clone,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Debug,
serde::Serialize,
serde::Deserialize,
Default,
)]
pub struct TenantId(pub String);
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Enforcement {
Enforced,
Measured,
Provisioned,
}
impl Enforcement {
pub fn describe(self) -> &'static str {
match self {
Enforcement::Enforced => "the product refuses when over",
Enforcement::Measured => "counted and reported; nothing refuses",
Enforcement::Provisioned => {
"provisioned at the cloud provider; not policed by the product"
}
}
}
pub fn is_a_wall(self) -> bool {
matches!(self, Enforcement::Enforced)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Backing {
DiskBytes,
CpuMillicores,
RamBytes,
Units,
}
impl Backing {
pub fn unit(self) -> &'static str {
match self {
Backing::DiskBytes => "disk_gib_month",
Backing::CpuMillicores => "cpu_core_month",
Backing::RamBytes => "ram_gib_month",
Backing::Units => "unit_month",
}
}
pub const ALL: [Backing; 4] = [
Backing::DiskBytes,
Backing::CpuMillicores,
Backing::RamBytes,
Backing::Units,
];
pub fn from_unit(unit: &str) -> Option<Backing> {
Backing::ALL.into_iter().find(|b| b.unit() == unit)
}
pub fn price_month(self, delta: u64, minor_per_unit_month: u64) -> u64 {
const GIB: u64 = 1 << 30;
match self {
Backing::DiskBytes | Backing::RamBytes => {
delta.div_ceil(GIB).saturating_mul(minor_per_unit_month)
}
Backing::CpuMillicores => {
u64::try_from(u128::from(delta) * u128::from(minor_per_unit_month) / 1000)
.unwrap_or(u64::MAX)
}
Backing::Units => delta.saturating_mul(minor_per_unit_month),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Scope {
Tenant,
Product,
}
impl Scope {
pub fn sums_across_tenants(self) -> bool {
matches!(self, Scope::Tenant)
}
pub fn name(self) -> &'static str {
match self {
Scope::Tenant => "tenant",
Scope::Product => "product",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Meter {
pub name: &'static str,
pub meaning: &'static str,
pub enforcement: Enforcement,
pub reports_usage: bool,
pub backing: Option<Backing>,
pub scope: Scope,
}
impl Meter {
pub const fn new(name: &'static str, meaning: &'static str, enforcement: Enforcement) -> Meter {
Meter {
name,
meaning,
enforcement,
reports_usage: true,
backing: None,
scope: Scope::Tenant,
}
}
pub const fn cap_only(
name: &'static str,
meaning: &'static str,
enforcement: Enforcement,
) -> Meter {
Meter {
name,
meaning,
enforcement,
reports_usage: false,
backing: None,
scope: Scope::Tenant,
}
}
pub const fn with_backing(self, backing: Backing) -> Meter {
Meter {
backing: Some(backing),
..self
}
}
pub const fn per_product(self) -> Meter {
Meter {
scope: Scope::Product,
..self
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Growth {
Sealed(String),
OperatorOnly(String),
Unknown(String),
AtRestart(String),
AtRuntime(String),
}
impl Growth {
pub fn name(&self) -> &'static str {
match self {
Growth::Sealed(_) => "sealed",
Growth::OperatorOnly(_) => "operator_only",
Growth::Unknown(_) => "unknown",
Growth::AtRestart(_) => "at_restart",
Growth::AtRuntime(_) => "at_runtime",
}
}
pub fn detail(&self) -> &str {
match self {
Growth::Sealed(s)
| Growth::OperatorOnly(s)
| Growth::Unknown(s)
| Growth::AtRestart(s)
| Growth::AtRuntime(s) => s,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Servable {
Measured {
servable_bytes: u64,
total_bytes: u64,
growth: Growth,
ceilings: BTreeMap<String, u64>,
},
Unmeasured(String),
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Usage {
pub meters: BTreeMap<String, u64>,
pub measured_at_unix_ms: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct TenantOwner {
pub tenant: TenantId,
pub person: bool,
pub seller: bool,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct UsagePageReq {
#[serde(default)]
pub page_token: String,
#[serde(default)]
pub page_size: u32,
#[serde(default)]
pub changed_since: String,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct TenantUsage {
pub tenant: TenantId,
pub usage: Usage,
}
pub const KEEP_BOOT_WITHIN: std::time::Duration = std::time::Duration::from_secs(120 * 60);
pub const ESTATE_MARK: char = '@';
impl TenantId {
pub fn on_estate(estate: &str, namespace: &str) -> Self {
TenantId(format!("{ESTATE_MARK}{estate}/{namespace}"))
}
pub fn estate(&self) -> Option<(&str, &str)> {
let (estate, namespace) = self.0.strip_prefix(ESTATE_MARK)?.split_once('/')?;
(!estate.is_empty() && !namespace.is_empty()).then_some((estate, namespace))
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct UsagePage {
#[serde(default)]
pub rows: Vec<TenantUsage>,
#[serde(default)]
pub next_page_token: String,
#[serde(default)]
pub cursor: String,
#[serde(default)]
pub changed_only: bool,
#[serde(default)]
pub as_of_unix_ms: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct ActivityDay {
pub day_unix_ms: u64,
pub meters: BTreeMap<String, u64>,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Activity {
pub days: Vec<ActivityDay>,
pub retained_days: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct EntitlementFact {
pub tenant: TenantId,
pub plan: String,
pub state: State,
pub paid_until_unix_ms: Option<u64>,
pub caps: BTreeMap<String, u64>,
pub source: String,
pub signature: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issued_unix_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub issued_signature: Vec<u8>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum State {
Free,
Paid,
Grace,
Suspended,
Retention,
}
#[derive(Clone, Debug)]
pub enum ProductError {
Refused(String),
Unavailable(String),
}
impl std::fmt::Display for ProductError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProductError::Refused(r) => write!(f, "product refused: {r}"),
ProductError::Unavailable(r) => write!(f, "product unavailable: {r}"),
}
}
}
impl std::error::Error for ProductError {}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Absorb {
OnHand,
WithIron { note: String },
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum MeteredDoor {
Enforcing(String),
Absent(String),
Unproven(String),
}
impl MeteredDoor {
pub fn name(&self) -> &'static str {
match self {
MeteredDoor::Enforcing(_) => "enforcing",
MeteredDoor::Absent(_) => "absent",
MeteredDoor::Unproven(_) => "unproven",
}
}
pub fn detail(&self) -> &str {
match self {
MeteredDoor::Enforcing(s) | MeteredDoor::Absent(s) | MeteredDoor::Unproven(s) => s,
}
}
pub fn proven(&self) -> bool {
matches!(self, MeteredDoor::Enforcing(_))
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TestAction {
pub id: String,
pub label: String,
pub kind: TestActionKind,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TestActionKind {
HostedPlan { cap_gib: u64 },
PrivateInstance {
#[serde(default)]
front_plan: String,
#[serde(default)]
main_plan: String,
#[serde(default)]
main_data_gib: u64,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct ProductFacts {
pub id: &'static str,
pub meters: &'static [Meter],
pub tickets: bool,
pub test_actions: Vec<TestAction>,
pub guardian: bool,
pub unread: Option<String>,
}
impl ProductFacts {
pub fn new(id: &'static str, meters: &'static [Meter]) -> ProductFacts {
ProductFacts {
id,
meters,
..Default::default()
}
}
pub fn leak(self) -> &'static ProductFacts {
static HELD: std::sync::Mutex<Vec<&'static ProductFacts>> =
std::sync::Mutex::new(Vec::new());
let mut held = HELD.lock().unwrap_or_else(|e| e.into_inner());
if let Some(f) = held.iter().find(|f| ***f == self) {
return f;
}
let f: &'static ProductFacts = Box::leak(Box::new(self));
held.push(f);
f
}
}
#[derive(Clone, Copy)]
pub struct Served<'a> {
facts: &'a ProductFacts,
product: &'a pulled::Pulled,
guardian: Option<&'a dyn guardian::EstateGuardian>,
absorb: Option<AbsorbFn<'a>>,
}
pub type AbsorbFn<'a> =
&'a (dyn Fn(&TenantId, &BTreeMap<String, u64>) -> Result<Absorb, String> + Sync);
impl<'a> Served<'a> {
pub fn new(facts: &'a ProductFacts, product: &'a pulled::Pulled) -> Served<'a> {
Served {
facts,
product,
guardian: None,
absorb: None,
}
}
pub fn with_room(mut self, absorb: AbsorbFn<'a>) -> Served<'a> {
self.absorb = Some(absorb);
self
}
pub fn can_absorb(
&self,
tenant: &TenantId,
caps: &BTreeMap<String, u64>,
) -> Result<Absorb, String> {
match self.absorb {
Some(f) => f(tenant, caps),
None => self.product.can_absorb(tenant, caps),
}
}
pub fn with_guardian(
mut self,
guardian: Option<&'a dyn guardian::EstateGuardian>,
) -> Served<'a> {
self.guardian = guardian;
self
}
pub fn guardian(&self) -> Option<&'a dyn guardian::EstateGuardian> {
self.guardian
}
pub fn id(&self) -> &'static str {
self.facts.id
}
pub fn meters(&self) -> &'static [Meter] {
self.facts.meters
}
pub fn tenant_renewal(&self) -> bool {
self.facts.tickets
}
pub fn test_actions(&self) -> &'a [TestAction] {
&self.facts.test_actions
}
pub fn facts(&self) -> &'a ProductFacts {
self.facts
}
pub fn product(&self) -> &'a pulled::Pulled {
self.product
}
}
impl<'a> std::ops::Deref for Served<'a> {
type Target = pulled::Pulled;
fn deref(&self) -> &Self::Target {
self.product
}
}
impl std::fmt::Debug for Served<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Served")
.field("id", &self.facts.id)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ActorVerdict {
Allowed,
Refused(String),
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct UsersAsk {
pub count: u32,
pub room_bytes: u64,
#[serde(skip_serializing_if = "String::is_empty")]
pub name: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub email: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub invite_origin: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub principal_id: String,
}
impl UsersAsk {
pub fn ticket_caps(&self) -> BTreeMap<String, u64> {
[
("users".to_owned(), u64::from(self.count.max(1))),
("user_room_bytes".to_owned(), self.room_bytes),
]
.into_iter()
.collect()
}
pub fn fingerprint(&self) -> String {
match self.principal_id.as_str() {
"" => format!("{}x{}", self.count.max(1), self.room_bytes),
id => format!("{}x{}:{id}", self.count.max(1), self.room_bytes),
}
}
pub fn without_names(&self) -> UsersAsk {
UsersAsk {
name: String::new(),
email: String::new(),
..self.clone()
}
}
pub fn is_named(&self) -> bool {
!self.name.is_empty() || !self.principal_id.is_empty()
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct UsersAnswer {
pub principal: String,
pub room_bytes: u64,
pub need_bytes: u64,
pub unused_bytes: Option<u64>,
pub shortfall_bytes: u64,
pub seats: u32,
pub seats_open: u32,
pub unchanged: bool,
#[serde(skip_serializing_if = "String::is_empty")]
pub invite_secret: String,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Purged {
pub tenant: TenantId,
pub detail: String,
pub bytes_reclaimed: Option<u64>,
pub still_locked: bool,
pub failures: Vec<String>,
}
pub fn bytes(n: u64) -> String {
const KIB: u64 = 1 << 10;
const MIB: u64 = 1 << 20;
const GIB: u64 = 1 << 30;
const TIB: u64 = 1 << 40;
let (unit, per) = match n {
n if n >= TIB => ("TiB", TIB),
n if n >= GIB => ("GiB", GIB),
n if n >= MIB => ("MiB", MIB),
n if n >= KIB => ("KiB", KIB),
n => return format!("{n} B"),
};
format!("{}.{} {unit}", n / per, ((n % per) * 10) / per)
}
#[cfg(test)]
mod backing_tests {
use super::Backing;
#[test]
fn prices_round_bytes_up_to_the_unit_and_cpu_by_the_millicore() {
const GIB: u64 = 1 << 30;
assert_eq!(
Backing::DiskBytes.price_month(90 * GIB, 2200),
198_000,
"90 GiB × 22.00 SEK"
);
assert_eq!(
Backing::DiskBytes.price_month(90 * GIB + 1, 2200),
200_200,
"one byte over is the 91st GiB"
);
assert_eq!(Backing::DiskBytes.price_month(0, 2200), 0);
assert_eq!(Backing::RamBytes.price_month(3 * GIB, 700), 2100);
assert_eq!(
Backing::CpuMillicores.price_month(1500, 30_000),
45_000,
"1.5 cores at 300.00"
);
assert_eq!(
Backing::CpuMillicores.price_month(1, 30_000),
30,
"one millicore is not free and not a core"
);
assert_eq!(Backing::Units.price_month(5, 20_000), 100_000, "5 × 200.00");
assert_eq!(Backing::Units.price_month(1, 20_000), 20_000);
assert_eq!(Backing::Units.price_month(0, 20_000), 0);
assert_eq!(Backing::Units.unit(), "unit_month");
for b in Backing::ALL {
assert_eq!(b.price_month(u64::MAX / 4, 0), 0, "{b:?}");
}
for b in Backing::ALL {
assert_eq!(Backing::from_unit(b.unit()), Some(b));
}
assert_eq!(Backing::from_unit("moon_month"), None);
}
}
#[cfg(test)]
mod bytes_tests {
use super::bytes;
#[test]
fn only_a_genuine_zero_reads_as_zero() {
for n in [1u64, 512, 1 << 20, 67_108_864, (1 << 30) - 1] {
let s = bytes(n);
assert!(
!s.starts_with("0.0 ") && !s.starts_with("0 "),
"{n} bytes rendered as {s:?}, which reads as nothing"
);
}
assert_eq!(bytes(0), "0 B");
}
#[test]
fn the_unit_scales_and_the_value_truncates() {
assert_eq!(bytes(999), "999 B");
assert_eq!(bytes(1536), "1.5 KiB");
assert_eq!(bytes(67_108_864), "64.0 MiB");
assert_eq!(
bytes((1 << 30) - 1),
"1023.9 MiB",
"truncates, never rounds up past the cap"
);
assert_eq!(bytes(10 << 30), "10.0 GiB");
assert_eq!(bytes(3 << 40), "3.0 TiB");
assert_eq!(
bytes(u64::MAX),
"16777215.9 TiB",
"no overflow at the top of the range"
);
}
}
#[cfg(test)]
mod estate_tenant_tests {
use super::*;
#[test]
fn a_sold_estates_tenant_carries_the_estate_in_its_path_and_no_own_tenant_reads_as_one() {
let t = TenantId::on_estate("acme", "bob");
assert_eq!(t.0, "@acme/bob");
assert_eq!(t.estate(), Some(("acme", "bob")));
assert_eq!(
TenantId::on_estate("acme", "team/sub").estate(),
Some(("acme", "team/sub"))
);
for own in [
"bob",
"team/sub",
"acme/bob",
"",
"@acme",
"@/bob",
"@acme/",
"x@acme/bob",
] {
assert_eq!(TenantId(own.into()).estate(), None, "{own:?}");
}
}
}