use std::fmt;
use lgwks_std::hash::Hasher;
use crate::cap::{Cap, Deficit};
use crate::effect::RunId;
use crate::gate::GrantSet;
use super::ledger::TicketStamp;
pub const MAX_TICKET_NEEDS: usize = 64;
#[derive(Debug)]
#[non_exhaustive]
pub enum RepairError {
ForeignTenant {
ticket: String,
host: String,
},
UnknownRun {
run: String,
},
StaleEpoch {
current: u64,
offered: u64,
},
AlreadyApplied,
NotAuthorized {
missing: Vec<Cap>,
},
OverWide {
beyond: Vec<Cap>,
},
BudgetSpent {
attempts: u64,
max_attempts: u64,
},
LedgerForeignTenant {
owner: String,
asked: String,
},
NoLedger,
Store {
cause: super::StoreError,
},
}
impl fmt::Display for RepairError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::ForeignTenant {
ref ticket,
ref host,
} => write!(
formatter,
"the repair ticket names tenant {ticket:?}, not {host:?}; \
refusing to repair another tenant's run"
),
Self::UnknownRun { ref run } => write!(
formatter,
"no repair ledger for run {run} under this tenant; refusing a repair whose \
budget and epoch cannot be attributed"
),
Self::StaleEpoch { current, offered } => write!(
formatter,
"repair epoch {offered} is not this run's current epoch {current}; \
refusing a stale repair ticket"
),
Self::AlreadyApplied => formatter.write_str(
"this repair ticket was already applied to this run; refusing to widen authority \
or repeat an applied repair",
),
Self::NotAuthorized { ref missing } => write!(
formatter,
"the repair grant is missing {} capability the ticket names ({}); leaving the run blocked",
missing.len(),
render(missing),
),
Self::OverWide { ref beyond } => write!(
formatter,
"the repair grant names {} capability the ticket never asked for ({}); \
refusing to widen authority beyond the request",
beyond.len(),
render(beyond),
),
Self::BudgetSpent {
attempts,
max_attempts,
} => write!(
formatter,
"root budget spent: {attempts} attempts against a ceiling of {max_attempts}; \
an authorized repair does not refill it"
),
Self::LedgerForeignTenant {
ref owner,
ref asked,
} => write!(
formatter,
"repair ledger says the run belongs to {owner:?}, not {asked:?}"
),
Self::NoLedger => formatter.write_str(
"this host has no repair ledger, so a repair has no budget, epoch or applied \
tickets to be decided against; refusing it unchecked",
),
Self::Store { ref cause } => write!(formatter, "{cause}"),
}
}
}
impl std::error::Error for RepairError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
Self::Store { ref cause } => Some(cause),
_ => None,
}
}
}
impl From<super::LeaseRefusal> for RepairError {
fn from(cause: super::LeaseRefusal) -> Self {
match cause {
super::LeaseRefusal::ForeignTenant { owner, asked } => {
Self::LedgerForeignTenant { owner, asked }
}
super::LeaseRefusal::StaleEpoch { current, offered } => {
Self::StaleEpoch { current, offered }
}
super::LeaseRefusal::AlreadyApplied => Self::AlreadyApplied,
super::LeaseRefusal::BudgetSpent {
attempts,
max_attempts,
} => Self::BudgetSpent {
attempts,
max_attempts,
},
}
}
}
fn render(caps: &[Cap]) -> String {
caps.iter().map(Cap::as_str).collect::<Vec<_>>().join(", ")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepairTicket {
run: RunId,
tenant: String,
epoch: u64,
needs: Vec<Cap>,
}
impl RepairTicket {
pub(crate) fn of(run: RunId, tenant: &str, epoch: u64, needs: Vec<Cap>) -> Self {
Self {
run,
tenant: tenant.to_owned(),
epoch,
needs,
}
}
pub(crate) fn of_deficit(run: RunId, tenant: &str, epoch: u64, deficit: &Deficit) -> Self {
Self::of(
run,
tenant,
epoch,
deficit
.shortages()
.map(|shortage| shortage.required().clone())
.collect(),
)
}
#[must_use]
pub const fn run(&self) -> RunId {
self.run
}
#[must_use]
pub fn tenant(&self) -> &str {
&self.tenant
}
#[must_use]
pub const fn epoch(&self) -> u64 {
self.epoch
}
#[must_use]
pub fn needs(&self) -> &[Cap] {
&self.needs
}
pub(crate) fn stamp(&self) -> TicketStamp {
let mut sorted = self.needs.clone();
sorted.sort();
let mut hasher = Hasher::new();
hasher.write_framed(b"lgwks-bot/repair-ticket/v1");
hasher.write_framed(self.run.id().to_hex().as_bytes());
hasher.write_framed(self.tenant.as_bytes());
hasher.write_framed(&self.epoch.to_le_bytes());
for cap in &sorted {
hasher.write_framed(cap.as_str().as_bytes());
}
TicketStamp {
identity: hasher.finalize().as_bytes().to_vec(),
epoch: self.epoch,
}
}
pub(crate) fn missing_from(&self, grant: &GrantSet) -> Vec<Cap> {
self.needs
.iter()
.filter(|cap| !grant.grants(cap))
.cloned()
.collect()
}
pub(crate) fn beyond(&self, grant: &GrantSet) -> Vec<Cap> {
let mut beyond: Vec<Cap> = grant
.caps()
.filter(|cap| !self.needs.contains(cap))
.cloned()
.collect();
beyond.sort();
beyond
}
pub(crate) fn delta(&self) -> GrantSet {
self.needs
.iter()
.fold(GrantSet::empty(), |set, cap| set.grant(cap.clone()))
}
pub(crate) fn check_grant(&self, grant: &GrantSet) -> Result<(), RepairError> {
let missing = self.missing_from(grant);
if !missing.is_empty() {
let refusal = Err(RepairError::NotAuthorized { missing });
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "check_grant: returning an error to the caller");
return refusal;
}
let beyond = self.beyond(grant);
if !beyond.is_empty() {
let refusal = Err(RepairError::OverWide { beyond });
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "check_grant: returning an error to the caller");
return refusal;
}
Ok(())
}
}