plugin/guest-allocation/src/lib.rs
//! **`holger-plugin-allocation`** — the open Meter guest.
//!
//! It declares holger's storage meters, answers how much storage there IS and
//! how much of it is still servable, and runs a selftest that checks the number
//! it just reported rather than asserting it.
//!
//! # What it does not do
//!
//! It has **no refusal path**. [`AllocationMeter::evaluate`] returns
//! [`WireVerdict::Allow`] unconditionally, and
//! `tests/allow_is_total.rs` asserts the function is total over the whole
//! context space it can be handed. Nothing in this crate can bound anything.
//! It declines `accept_fact` with
//! [`UNSUPPORTED`]: it holds no key and verifies nothing, and its manifest says
//! so in [`MeterFacts::accepts_facts`] so nothing has to infer that from an
//! error string.
//!
//! # Its capabilities, and the one correction to the design
//!
//! It requires `usage.read` and `store.list`. Nothing else, and that is visible
//! in its manifest.
//!
//! The design (`.nornir/stinky/06-monetize-plugin.md` §6.1) says the selftest
//! *"writes a probe, measures the delta, compares"*. That is not compatible with
//! the capability list in the same paragraph: writing a probe needs
//! `store.write`, which this guest does not ask for and must not, since a meter
//! that can write into the store it measures is a meter that can move its own
//! number. **Corrected here:** the selftest is a read-only cross-check — it sums
//! the store listing itself and compares that total against the `used_bytes` the
//! host reported through a different code path, and prints both numbers. It
//! measures a delta between two independent answers to the same question, which
//! is a stronger check than a write-probe against the same counter would be, and
//! it needs no capability the guest did not already declare.
use holger_plugin_abi::v2::{
meters, AbiRange, Capability, Enforcement, GuestKind, Meter, MeterFacts, MeterHost, MeterScope,
ReadingValue, WireCallContext, WireCapacity, WireFactSummary, WireManifest2, WireMeterDecl,
WireMeterReading, WireSelftest, WireVerdict, UNSUPPORTED,
};
use holger_plugin_abi::ModuleIdentity;
/// This guest's name, as it appears in the module census and in the grant file.
pub const NAME: &str = "allocation";
/// The anti-rollback floor's lane is `(SUPPLIER, NAME)`; the version compared
/// against it is this crate's own, so a release of the guest IS a floor step.
pub const SUPPLIER: &str = "nordisk";
/// What this guest asks for. One spelling, read by the manifest and asserted
/// against the wasm shim's import list by `tests/`.
pub const REQUIRES: &[Capability] = &[Capability::UsageRead, Capability::StoreList];
#[derive(Debug, Default, Clone, Copy)]
pub struct AllocationMeter;
impl AllocationMeter {
pub fn manifest_const() -> WireManifest2 {
WireManifest2 {
abi: AbiRange::new(2, 2),
kind: GuestKind::Meter,
name: NAME.to_string(),
requires: REQUIRES.to_vec(),
handler: None,
meter: Some(MeterFacts {
accepts_facts: false,
}),
identity: Some(ModuleIdentity::new(
SUPPLIER,
NAME,
env!("CARGO_PKG_VERSION"),
)),
unknown_optional: Vec::new(),
}
}
/// Pick one reading out of a host answer by meter name.
fn pick<'a>(rows: &'a [WireMeterReading], meter: &str) -> Option<&'a WireMeterReading> {
rows.iter().find(|r| r.meter == meter)
}
/// A number from the host, or a refusal that says which meter went missing —
/// never a zero the guest invented.
fn number(rows: &[WireMeterReading], meter: &str) -> ReadingValue {
match Self::pick(rows, meter) {
Some(r) => r.value.clone(),
None => ReadingValue::Refused {
reason: format!("the host reported no `{meter}` reading"),
},
}
}
}
impl Meter for AllocationMeter {
fn manifest(&self) -> WireManifest2 {
Self::manifest_const()
}
fn meters(&self) -> Result<Vec<WireMeterDecl>, String> {
// Every one of these is `Measured`. That is not modesty: a bound may be
// attached only to an `Enforced` meter, so declaring `Measured` here is
// what makes a bound against these meters refusable up front.
Ok(vec![
WireMeterDecl {
meter: meters::ARTIFACT_BYTES.into(),
unit: "bytes".into(),
scope: MeterScope::Tenant,
enforcement: Enforcement::Measured,
description: "bytes held by the artifacts in one repository".into(),
},
WireMeterDecl {
meter: meters::ARTIFACTS.into(),
unit: "artifacts".into(),
scope: MeterScope::Tenant,
enforcement: Enforcement::Measured,
description:
"artifacts held in one repository; REFUSED on a backend that cannot enumerate"
.into(),
},
WireMeterDecl {
meter: meters::ARCHIVE_BYTES.into(),
unit: "bytes".into(),
scope: MeterScope::Tenant,
enforcement: Enforcement::Measured,
description: "bytes inside one repository's archive, when it has one".into(),
},
WireMeterDecl {
meter: meters::ARCHIVE_FILES.into(),
unit: "files".into(),
scope: MeterScope::Tenant,
enforcement: Enforcement::Measured,
description: "files inside one repository's archive, when it has one".into(),
},
WireMeterDecl {
meter: meters::STORAGE_TOTAL_BYTES.into(),
unit: "bytes".into(),
scope: MeterScope::Product,
enforcement: Enforcement::Measured,
description: "how much storage this holger has".into(),
},
WireMeterDecl {
meter: meters::STORAGE_SERVABLE_BYTES.into(),
unit: "bytes".into(),
scope: MeterScope::Product,
enforcement: Enforcement::Measured,
description: "how much of it holger can still write into".into(),
},
WireMeterDecl {
meter: meters::STORAGE_USED_BYTES.into(),
unit: "bytes".into(),
scope: MeterScope::Product,
enforcement: Enforcement::Measured,
description: "how much of it is in use".into(),
},
])
}
fn read(
&self,
host: &dyn MeterHost,
repository: &str,
) -> Result<Vec<WireMeterReading>, String> {
// The host counts; this guest never substitutes a number of its own. A
// meter the host refused stays refused, with the host's reason — that is
// the whole point of `ReadingValue`.
host.usage_read(repository)
}
fn capacity(&self, host: &dyn MeterHost) -> Result<WireCapacity, String> {
// The empty repository name asks for the PRODUCT-scope readings: how
// much storage there is, rather than what one tenant holds.
let rows = host.usage_read(meters::PRODUCT_SCOPE)?;
let total = Self::number(&rows, meters::STORAGE_TOTAL_BYTES);
let servable = Self::number(&rows, meters::STORAGE_SERVABLE_BYTES);
let used = Self::number(&rows, meters::STORAGE_USED_BYTES);
// If any of the three was refused, the three numbers below are not to be
// believed and `source` says which one and why. A `WireCapacity` whose
// `source` is `Refused` is not a capacity of zero.
let source = match (&total, &servable, &used) {
(ReadingValue::Counted(_), ReadingValue::Counted(_), ReadingValue::Counted(_)) => {
ReadingValue::Counted(3)
}
_ => {
let mut missing = Vec::new();
for (name, v) in [
(meters::STORAGE_TOTAL_BYTES, &total),
(meters::STORAGE_SERVABLE_BYTES, &servable),
(meters::STORAGE_USED_BYTES, &used),
] {
if let ReadingValue::Refused { reason } = v {
missing.push(format!("{name}: {reason}"));
}
}
ReadingValue::Refused {
reason: missing.join("; "),
}
}
};
Ok(WireCapacity {
total_bytes: total.counted().unwrap_or(0),
servable_bytes: servable.counted().unwrap_or(0),
used_bytes: used.counted().unwrap_or(0),
source,
})
}
fn selftest(&self, host: &dyn MeterHost) -> Result<WireSelftest, String> {
let reported = self.capacity(host)?;
if let ReadingValue::Refused { reason } = &reported.source {
return Ok(WireSelftest {
name: "storage-figure-cross-check".into(),
passed: false,
detail: format!(
"the host could not report the storage figure, so there was nothing to \
check: {reason}"
),
});
}
// The independent answer: sum the store listing this guest can read for
// itself. Two code paths, one question.
let listing = host.store_list("")?;
let counted: u64 = listing.iter().map(|(_, size)| *size).sum();
let keys = listing.len();
// The two numbers do not have to be equal — the store listing covers the
// blobs, the host figure covers the filesystem those blobs sit on, which
// also carries the audit chain and the filesystem's
// own overhead. So the check is DIRECTIONAL: the blobs cannot be more than
// what is in use, and what is in use cannot be more than what there is.
let blobs_fit = counted <= reported.used_bytes;
let used_fits = reported.used_bytes <= reported.total_bytes;
let servable_fits = reported.servable_bytes <= reported.total_bytes;
let passed = blobs_fit && used_fits && servable_fits;
let detail = format!(
"summed {keys} stored key(s) to {counted} bytes; the host reports \
{used} bytes used of {total} bytes total, {servable} bytes still servable. \
Checked: blobs <= used ({blobs_fit}), used <= total ({used_fits}), \
servable <= total ({servable_fits}).",
used = reported.used_bytes,
total = reported.total_bytes,
servable = reported.servable_bytes,
);
Ok(WireSelftest {
name: "storage-figure-cross-check".into(),
passed,
detail,
})
}
/// **Total, and unconditional.** There is no branch here and there is not
/// going to be one: the open build stays unlimited. `tests/allow_is_total.rs`
/// asserts it over every verb and every shape of context.
fn evaluate(
&self,
_host: &dyn MeterHost,
_ctx: &WireCallContext,
) -> Result<WireVerdict, String> {
Ok(WireVerdict::Allow)
}
/// Declined. This guest holds no key and verifies nothing.
fn accept_fact(
&self,
_host: &dyn MeterHost,
_signed: &[u8],
) -> Result<WireFactSummary, String> {
Err(UNSUPPORTED.to_string())
}
fn describe_fact(
&self,
_host: &dyn MeterHost,
_repository: &str,
) -> Result<WireFactSummary, String> {
Err(UNSUPPORTED.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use holger_plugin_abi::v2::{check_grants, WireAuditRecord, WireInstant};
/// A host that counts, so the guest can be exercised without a server.
#[derive(Default)]
struct FakeHost {
rows: Vec<WireMeterReading>,
listing: Vec<(String, u64)>,
}
impl MeterHost for FakeHost {
fn usage_read(&self, _repository: &str) -> Result<Vec<WireMeterReading>, String> {
Ok(self.rows.clone())
}
fn store_list(&self, _prefix: &str) -> Result<Vec<(String, u64)>, String> {
Ok(self.listing.clone())
}
}
fn counted(meter: &str, v: u64) -> WireMeterReading {
WireMeterReading {
meter: meter.into(),
scope: MeterScope::Product,
enforcement: Enforcement::Measured,
value: ReadingValue::Counted(v),
}
}
fn full_host() -> FakeHost {
FakeHost {
rows: vec![
counted(meters::STORAGE_TOTAL_BYTES, 1000),
counted(meters::STORAGE_SERVABLE_BYTES, 700),
counted(meters::STORAGE_USED_BYTES, 300),
],
listing: vec![("a".into(), 100), ("b".into(), 100)],
}
}
#[test]
fn the_manifest_asks_for_two_capabilities_and_no_more() {
let m = AllocationMeter::manifest_const();
assert_eq!(
m.requires,
vec![Capability::UsageRead, Capability::StoreList]
);
assert_eq!(m.kind, GuestKind::Meter);
assert!(!m.meter.unwrap().accepts_facts);
}
#[test]
fn its_imports_are_exactly_what_its_manifest_unlocks() {
// The wasm shim emits only the imports its capability list unlocks, so
// the real import list of the built module is this set. Gate B is
// checked here against the same two statements the host will compare.
let m = AllocationMeter::manifest_const();
let real: Vec<String> = REQUIRES
.iter()
.flat_map(|c| c.imports().iter().map(|s| s.to_string()))
.collect();
let unlocked = check_grants(NAME, &m, &real, REQUIRES).unwrap();
assert_eq!(unlocked.len(), real.len());
for imp in &unlocked {
assert!(real.contains(&imp.to_string()));
}
}
#[test]
fn a_grant_that_stops_short_is_refused_by_name() {
let m = AllocationMeter::manifest_const();
let err = check_grants(NAME, &m, &[], &[Capability::UsageRead]).unwrap_err();
assert!(err.to_string().contains("store.list"), "{err}");
}
#[test]
fn capacity_reports_what_the_host_counted() {
let c = AllocationMeter.capacity(&full_host()).unwrap();
assert_eq!(c.total_bytes, 1000);
assert_eq!(c.servable_bytes, 700);
assert_eq!(c.used_bytes, 300);
assert!(matches!(c.source, ReadingValue::Counted(3)));
}
#[test]
fn a_missing_host_reading_is_a_refusal_naming_the_meter_not_a_zero() {
let host = FakeHost {
rows: vec![counted(meters::STORAGE_TOTAL_BYTES, 1000)],
..Default::default()
};
let c = AllocationMeter.capacity(&host).unwrap();
match c.source {
ReadingValue::Refused { reason } => {
assert!(reason.contains(meters::STORAGE_USED_BYTES), "{reason}");
assert!(reason.contains(meters::STORAGE_SERVABLE_BYTES), "{reason}");
}
other => panic!("expected Refused, got {other:?}"),
}
}
#[test]
fn the_selftest_checks_the_figure_it_reported_and_says_both_numbers() {
let t = AllocationMeter.selftest(&full_host()).unwrap();
assert!(t.passed, "{}", t.detail);
assert!(t.detail.contains("200 bytes"), "{}", t.detail);
assert!(t.detail.contains("300 bytes used"), "{}", t.detail);
assert!(t.detail.contains("1000 bytes total"), "{}", t.detail);
}
#[test]
fn the_selftest_fails_loudly_when_the_numbers_cannot_hold() {
let host = FakeHost {
rows: vec![
counted(meters::STORAGE_TOTAL_BYTES, 100),
counted(meters::STORAGE_SERVABLE_BYTES, 50),
// more used than exists
counted(meters::STORAGE_USED_BYTES, 500),
],
listing: vec![],
};
let t = AllocationMeter.selftest(&host).unwrap();
assert!(!t.passed);
assert!(t.detail.contains("used <= total (false)"), "{}", t.detail);
}
#[test]
fn a_host_that_cannot_say_produces_a_failed_selftest_not_a_pass_on_zeroes() {
let t = AllocationMeter.selftest(&FakeHost::default()).unwrap();
assert!(!t.passed);
assert!(t.detail.contains("nothing to check"), "{}", t.detail);
}
#[test]
fn read_passes_the_hosts_refusal_through_unchanged() {
let host = FakeHost {
rows: vec![WireMeterReading {
meter: meters::ARTIFACTS.into(),
scope: MeterScope::Tenant,
enforcement: Enforcement::Measured,
value: ReadingValue::Refused {
reason: "znippy-go cannot enumerate".into(),
},
}],
..Default::default()
};
let rows = AllocationMeter.read(&host, "releases").unwrap();
assert_eq!(
rows[0].value,
ReadingValue::Refused {
reason: "znippy-go cannot enumerate".into()
}
);
assert_eq!(rows[0].value.counted(), None);
}
#[test]
fn every_declared_meter_is_measured_so_nothing_can_be_bounded_against_one() {
for d in AllocationMeter.meters().unwrap() {
assert_eq!(
d.enforcement,
Enforcement::Measured,
"{} is not Measured",
d.meter
);
}
}
#[test]
fn accept_fact_is_declined_and_says_so_by_name() {
assert_eq!(
AllocationMeter
.accept_fact(&FakeHost::default(), &[])
.unwrap_err(),
UNSUPPORTED
);
assert_eq!(
AllocationMeter
.describe_fact(&FakeHost::default(), "releases")
.unwrap_err(),
UNSUPPORTED
);
}
#[test]
fn the_defaults_of_an_ungranted_capability_are_named_refusals_not_stubs() {
let host = FakeHost::default();
// This guest never calls these; if a future edit did, it gets an Err
// naming the capability rather than a zero.
assert!(host.store_get("k").unwrap_err().contains("store.read"));
assert!(host
.store_put("k", b"v")
.unwrap_err()
.contains("store.write"));
assert!(host
.audit_write(&WireAuditRecord::default())
.unwrap_err()
.contains("audit.write"));
let _: Result<WireInstant, String> = host.clock();
assert!(host.clock().unwrap_err().contains("clock.read"));
}
}