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

nordisk/monetize

crates/monetize-cloud/src/factory.rs

//! **The registration seam for clouds** — the twin of
//! `monetize_pay::factory`, with two things a vendor's does not have: the
//! spend ceiling the server read from the block (so a declared price table can
//! borrow its currency), and the linked products (so the `server` cloud can
//! measure through the one whose disk it sells).
//!
//! And one flag that decides whether the server demands a ceiling at all:
//! [`CloudFactory::spends`]. A provider that can buy is wrapped in
//! `cap::SpendCap` and refused without a `cap`; one that cannot is wrapped in
//! `cap::CannotSpend`, which holds it to that. The flag is a claim; the wrapper
//! is what makes the claim safe to act on.

use std::path::Path;
use std::sync::Arc;

pub use monetize_pay::factory::Settings;
use monetize_product::Product;

use crate::cap::Ceiling;
use crate::Cloud;

/// What a cloud is built from.
pub struct CloudBuild<'a> {
    /// The configured name — the factory's own, echoed so an error can say
    /// `[clouds.<name>]`.
    pub name: &'a str,
    /// The server's data root: the pool ledger, the spend file, the sandbox's
    /// iron all live beside the ledger.
    pub data_dir: &'a Path,
    /// The `[clouds.<name>]` block minus `enabled` and `cap`.
    pub settings: Settings<'a>,
    /// The ceiling the server will wrap this provider in, if the block declared
    /// one. `None` for a provider that cannot spend, or a misconfigured one the
    /// server is about to refuse.
    pub ceiling: Option<Ceiling>,
    /// Every product linked into the server, in config order.
    pub products: &'a [Arc<dyn Product>],
}

impl CloudBuild<'_> {
    /// `[clouds.<name>] <what>` — the prefix every refusal from a build wears.
    pub fn err(&self, what: impl std::fmt::Display) -> String {
        format!("[clouds.{}] {what}", self.name)
    }
}

pub type CloudBuilder = fn(&CloudBuild<'_>) -> Result<Box<dyn Cloud>, String>;

/// One linkable cloud: its name, its secrets' environment names, whether it
/// can spend, its constructor.
#[derive(Clone, Copy)]
pub struct CloudFactory {
    pub name: &'static str,
    /// Every environment name `build` may read; forwarded into the unit's 0600
    /// env file by the install. Empty for a provider with no account.
    pub config_envs: &'static [&'static str],
    /// **Can this provider ever buy?** `true` ⇒ the server demands a `cap` and
    /// wraps it in `SpendCap`; `false` ⇒ no ceiling, and `CannotSpend` destroys
    /// and refuses any `Provisioned` answer it ever gives.
    pub spends: bool,
    pub build: CloudBuilder,
}

impl std::fmt::Debug for CloudFactory {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CloudFactory").field("name", &self.name).field("spends", &self.spends).field("config_envs", &self.config_envs).finish_non_exhaustive()
    }
}

/// **Register a factory, replacing one of the same name.** Last writer wins:
/// the vetra build registers a `sandbox` that prices off UpCloud's real list
/// over the open `sandbox` that only knows a declared table.
pub fn register(list: &mut Vec<CloudFactory>, factory: CloudFactory) {
    match list.iter_mut().find(|f| f.name == factory.name) {
        Some(slot) => *slot = factory,
        None => list.push(factory),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn register_replaces_by_name_and_the_flag_travels_with_the_replacement() {
        fn never(_: &CloudBuild<'_>) -> Result<Box<dyn Cloud>, String> {
            Err("never".into())
        }
        let open = CloudFactory { name: "sandbox", config_envs: &["A"], spends: true, build: never };
        let richer = CloudFactory { name: "sandbox", config_envs: &["A", "B"], spends: true, build: never };
        let iron = CloudFactory { name: "server", config_envs: &[], spends: false, build: never };
        let mut list = Vec::new();
        register(&mut list, open);
        register(&mut list, iron);
        register(&mut list, richer);
        assert_eq!(list.iter().map(|f| (f.name, f.spends)).collect::<Vec<_>>(), vec![("sandbox", true), ("server", false)]);
        assert_eq!(list[0].config_envs, &["A", "B"]);
        let v = serde_json::json!({});
        let ctx = CloudBuild { name: "x", data_dir: Path::new("/tmp"), settings: Settings(&v), ceiling: None, products: &[] };
        assert_eq!(ctx.err("no"), "[clouds.x] no");
    }
}