Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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"));
    }
}