use gunnar_proto::usage_server::Usage;
use gunnar_proto::{
ActivityRequest, ActivityResponse, AdmitPlanRequest, AdmitPlanResponse, Allowance,
GrantLevel, NamespaceUsage, ReadAllUsageRequest, ReadAllUsageResponse, ReadUsageRequest,
ReadUsageResponse, Remaining,
};
use tonic::{Request, Response, Status};
use crate::auth::Access;
use crate::service::UsageService;
#[cfg(feature = "metered")]
pub fn allowance_of(caps: &std::collections::BTreeMap<String, u64>) -> Allowance {
let cap = |meter: &str| caps.get(meter).copied().filter(|cap| *cap > 0);
Allowance {
enforcement: crate::service::meters::enforcement(),
pack_bytes: cap("pack_bytes"),
cache_bytes: cap("cache_bytes"),
cold_bytes: cap("cold_bytes"),
lfs_bytes: cap("lfs_bytes"),
tombstoned_bytes: cap("tombstoned_bytes"),
repos: cap("repos"),
objects: cap("objects"),
}
}
fn uncapped() -> Allowance {
Allowance {
enforcement: crate::service::meters::enforcement(),
..Allowance::default()
}
}
pub fn remaining_of(usage: &NamespaceUsage, allowance: &Allowance) -> Remaining {
let left = |cap: Option<u64>, held: u64| cap.map(|cap| cap.saturating_sub(held));
Remaining {
pack_bytes: left(allowance.pack_bytes, usage.pack_bytes),
cache_bytes: left(allowance.cache_bytes, usage.cache_bytes),
cold_bytes: left(allowance.cold_bytes, usage.cold_bytes),
lfs_bytes: left(allowance.lfs_bytes, usage.lfs_bytes),
tombstoned_bytes: left(allowance.tombstoned_bytes, usage.tombstoned_bytes),
repos: left(allowance.repos, usage.repos),
objects: left(allowance.objects, usage.objects),
}
}
impl UsageService {
fn allowance(&self, namespace: &str) -> Result<Allowance, Status> {
#[cfg(feature = "metered")]
{
use crate::seam::ControlError;
let clans = self.ctx.control.clans();
if let Some(clan) = clans.map(|p| p.clan_get(namespace)).transpose()?.flatten() {
let room = clan.room_in_force_bytes;
let (pack, lfs) = self
.ctx
.control
.usage(Some(namespace))
.ok()
.and_then(|rows| rows.into_iter().find(|row| row.namespace == namespace))
.map_or((0, 0), |row| (row.pack_bytes, row.lfs_bytes));
return Ok(Allowance {
pack_bytes: Some(room.saturating_sub(lfs)),
lfs_bytes: Some(room.saturating_sub(pack)),
..uncapped()
});
}
match self.ctx.control.entitlement_get(namespace) {
Ok(Some(fact)) => {
let mut allowance = allowance_of(&fact.caps);
if let (Some(cap), Some(plane)) = (allowance.pack_bytes, clans) {
allowance.pack_bytes = Some(cap.saturating_sub(plane.clan_given(namespace)?));
}
return Ok(allowance);
}
Ok(None) => {}
Err(ControlError::FailedPrecondition(_)) => {}
Err(err) => return Err(err.into()),
}
}
#[cfg(not(feature = "metered"))]
let _ = namespace;
Ok(uncapped())
}
}
#[tonic::async_trait]
impl Usage for UsageService {
async fn read(
&self,
request: Request<ReadUsageRequest>,
) -> Result<Response<ReadUsageResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let namespace = request.into_inner().namespace;
if namespace.is_empty() {
return Err(Status::invalid_argument("namespace is required"));
}
self.ctx.require(
&caller,
Access::namespace(&namespace),
GrantLevel::Read,
"Usage.Read",
)?;
let mut rows = self.ctx.control.usage(Some(&namespace))?;
let usage = rows.pop().ok_or_else(|| {
Status::not_found(format!("namespace {namespace} is not in the catalog"))
})?;
let allowance = self.allowance(&namespace)?;
let remaining = remaining_of(&usage, &allowance);
let repos = match self.ctx.control.repo_usage(&namespace) {
Ok(rows) => rows,
Err(crate::seam::ControlError::Unimplemented(_)) => Vec::new(),
Err(err) => return Err(err.into()),
};
Ok(Response::new(ReadUsageResponse {
usage: Some(usage),
allowance: Some(allowance),
remaining: Some(remaining),
repos,
}))
}
async fn read_all(
&self,
request: Request<ReadAllUsageRequest>,
) -> Result<Response<ReadAllUsageResponse>, Status> {
let caller = self
.ctx
.authorize(&request, Access::Server, GrantLevel::Read, "Usage.ReadAll")?;
let usages = self.ctx.control.usage(None)?;
let usages = self.ctx.retain_unshielded(&caller, usages, |row| {
row.namespace.contains('/').then(|| row.namespace.clone())
})?;
Ok(Response::new(ReadAllUsageResponse { usages }))
}
async fn activity(
&self,
request: Request<ActivityRequest>,
) -> Result<Response<ActivityResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
if request.namespace.is_empty() {
return Err(Status::invalid_argument("namespace is required"));
}
self.ctx.require(
&caller,
Access::namespace(&request.namespace),
GrantLevel::Read,
"Usage.Activity",
)?;
let (days, retained_days) = self.ctx.control.activity(
&request.namespace,
request.since_unix_ms,
request.until_unix_ms,
)?;
Ok(Response::new(ActivityResponse {
days,
retained_days,
}))
}
async fn admit_plan(
&self,
request: Request<AdmitPlanRequest>,
) -> Result<Response<AdmitPlanResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
if request.namespace.is_empty() {
return Err(Status::invalid_argument("namespace is required"));
}
self.ctx.require(
&caller,
Access::namespace(&request.namespace),
GrantLevel::Read,
"Usage.AdmitPlan",
)?;
Ok(Response::new(self.ctx.control.admit_plan(&request.namespace)?))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn held() -> NamespaceUsage {
NamespaceUsage {
namespace: "acme".into(),
pack_bytes: 700,
cache_bytes: 50,
lfs_bytes: 0,
repos: 3,
..Default::default()
}
}
#[test]
fn what_is_left_is_cap_minus_held_clamped_at_zero_and_absent_where_uncapped() {
let allowance = Allowance {
pack_bytes: Some(1_000),
cache_bytes: Some(40),
lfs_bytes: Some(9),
repos: None,
..Allowance::default()
};
let left = remaining_of(&held(), &allowance);
assert_eq!(left.pack_bytes, Some(300), "under the cap");
assert_eq!(left.cache_bytes, Some(0), "over the cap is 0, not a wrap");
assert_eq!(left.lfs_bytes, Some(9), "nothing held: the whole cap");
assert_eq!(left.repos, None, "uncapped is absent, never a number");
assert_eq!(left.cold_bytes, None);
assert_eq!(left.tombstoned_bytes, None);
assert_eq!(left.objects, None);
}
}