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

nordisk/monetize

crates/monetize-server/src/estate_order/adopt.rs

//! **The ONE box ADOPTS a kit-installed operator site** (owner 2026-10-17: Gunnar sites are
//! upgraded by the ONE box's monetize, not by kit scripts). gunnar.rs was installed by its
//! kit (private-gunnar-ops): the ONE box knows it only as a site that STANDS (its boxes call
//! home) and holds no run for it, so `EstateOrder.UpdateEstate` has nothing to update.
//!
//! `EstateOrder.AdoptEstate` (the operator's alone; `monetize estate adopt <site> …` through
//! the box's door) makes the run an update needs, and nothing else:
//!
//! * the declared operator site's run (`site_order`, its label as the cloud names an estate,
//!   [`super::site::company_of`]) in the delivered phase a laid ready site has ([`phase::CAPTURED`]),
//!   its `laid` the machines the operator names — each one READ BACK by its id through the
//!   shop's cloud ([`monetize_cloud::Cloud::status`]: the provider lists only what
//!   carries a monetize label, and a kit's machines carry none, so their addresses and disks
//!   come from the kit's own record and only their standing is the provider's word);
//! * its client key minted through the custody path a lay uses ([`crate::custody::Custody::mint`],
//!   the hand-off's `estate-client.key`), its public half on the run, and the custody then
//!   forgotten — the shop keeps the public half alone, as after a lay's hand-off;
//! * the front's feed lines answered ONCE to the operator: `shop_door.toml` (the door, the
//!   company, the shop's key, the client key's secret name) and `secret.shop_client_key` (the
//!   seed, hex). Never logged, never in the run file.
//!
//! **What an adopted run never does** ([`EstateRun::adopted_at_ms`]): the shop did not lay its
//! machines and they carry no label of it, so nothing here lays, re-lays, fences, gives back
//! or bills them — the guardian keeps waiting for the operator's kit for a lost box
//! ([`crate::fleet::ShopWorld`]), `Recharge` and `Terminate` are refused by name, the twin does
//! not grow by itself, the site's join record keeps keying its boxes, and its `Upgrade`
//! is refused by name unless the operator opened an update on that box (every estate alike). It never lapses (a site).

use std::collections::BTreeMap;

use monetize_cloud::lay::{company_name_refusal, reference_of, LaidEstate, LaidMachine};
use monetize_cloud::{CloudError, ResourceStatus, Zone};
use monetize_pay::Money;

use super::lifecycle::{listed, Lifecycle};
use super::{phase, price, site, EstateRun, EstateSizes, Quote, StartRefusal, Transition};
use crate::state::AppState;

/// The note an adoption leaves in the run's history (what a console and the tests look for).
pub const ADOPTED: &str = "adopted";

/// **What the operator asks to adopt.**
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AdoptAsk {
    /// The declared operator site's label (`gunnar-rs`).
    pub estate: String,
    /// The zone its machines stand in; empty: the shop's.
    pub zone: String,
    /// front, main and twin: the server ids (required) and what the kit recorded of them.
    pub machines: Vec<LaidMachine>,
    /// Role → the release it runs, when known; absent: unknown (an update updates it).
    pub release_of: BTreeMap<String, String>,
    /// Adopt an adopted site again: a NEW client key (the old one stops opening the shop).
    pub again: bool,
    /// Who asked (the operator's principal, or `operator`).
    pub by: String,
}

/// **What an adoption answers** — once.
pub struct Adopted {
    pub run: EstateRun,
    /// **The front's feed lines**: `shop_door.toml` and `secret.shop_client_key`. A secret as
    /// a whole: answered once, to the operator, never logged and never kept here.
    pub feed: BTreeMap<String, String>,
    pub words: String,
}

impl std::fmt::Debug for Adopted {
    /// Never a value of the feed.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Adopted")
            .field("company", &self.run.company)
            .field("feed", &self.feed.keys().collect::<Vec<_>>())
            .field("words", &self.words)
            .finish()
    }
}

/// The front's feed key its shop door is fed under (gunnar-front `recharge::FEED_SHOP_DOOR`,
/// monetize-shop-pairing `contract::FEED_SHOP_DOOR`).
pub const FEED_SHOP_DOOR: &str = "shop_door.toml";

/// The front's feed key of the client key's seed: `secret.<client_key_secret>`.
pub fn client_key_feed() -> String {
    format!("secret.{}", crate::shop_cloud::CLIENT_KEY_SECRET)
}

/// Pure: **the front's `shop_door.toml`** — the shop's door, the company its calls name, the
/// shop's key, and the name of the fed secret its client key is (as a lay's pairing feeds it).
pub fn shop_door_toml(door: &str, company: &str, shop_pubkey: &str) -> Result<String, String> {
    let mut t = toml::Table::new();
    t.insert("door".into(), toml::Value::String(door.trim().to_string()));
    t.insert("company".into(), toml::Value::String(company.to_string()));
    t.insert(
        "shop_pubkey".into(),
        toml::Value::String(shop_pubkey.trim().to_string()),
    );
    t.insert(
        "client_key_secret".into(),
        toml::Value::String(crate::shop_cloud::CLIENT_KEY_SECRET.into()),
    );
    toml::to_string(&t).map_err(|e| e.to_string())
}

/// Pure: **why `machines` are not an estate of `life`'s product to adopt**, or `None`: its
/// roles ([`Lifecycle::roles`]; Gunnar's front, main and twin), each once, each with its
/// server id, no id twice, every address given an IPv4 one.
pub fn machines_refusal(life: &Lifecycle, machines: &[LaidMachine]) -> Option<String> {
    let roles = life.roles();
    for role in &roles {
        match machines.iter().filter(|m| m.role == *role).count() {
            0 => {
                return Some(format!(
                    "no {role} named: an adopted estate is {}, each by its server id",
                    listed(&roles, "and")
                ))
            }
            1 => {}
            _ => return Some(format!("the {role} is named twice")),
        }
    }
    if let Some(m) = machines.iter().find(|m| !roles.contains(&m.role)) {
        return Some(format!(
            "{:?} is no role of an estate ({})",
            m.role,
            roles.join(", ")
        ));
    }
    for m in machines {
        if m.server.trim().is_empty() {
            return Some(format!("the {} names no server id", m.role));
        }
        if m.server.chars().any(|c| c.is_whitespace() || c == '/') {
            return Some(format!(
                "the {}'s server id {:?} is not one",
                m.role, m.server
            ));
        }
        for (what, ip) in [("public", &m.public_ipv4), ("utility", &m.utility_ipv4)] {
            if !ip.trim().is_empty() && ip.trim().parse::<std::net::Ipv4Addr>().is_err() {
                return Some(format!(
                    "the {}'s {what} address {ip:?} is not an IPv4 address",
                    m.role
                ));
            }
        }
    }
    let mut ids: Vec<&str> = machines.iter().map(|m| m.server.trim()).collect();
    ids.sort_unstable();
    if ids.windows(2).any(|w| w[0] == w[1]) {
        return Some("two roles name the same server".into());
    }
    None
}

/// Pure: **an adopted estate's quote** — what was laid, priced at nothing: the shop bought none
/// of it and bills none of it (its ledger row is the operator's own, never revenue). The sizes
/// are read off `life`'s front, its primary and the machine mirroring the primary (Gunnar's
/// front, main and twin).
pub fn quote_of(
    life: &Lifecycle,
    machines: &[LaidMachine],
    currency: monetize_pay::Currency,
) -> Quote {
    let zero = Money { minor: 0, currency };
    let plan = |role: &str| {
        machines
            .iter()
            .find(|m| m.role == role)
            .map(|m| m.plan.clone())
            .unwrap_or_default()
    };
    let primary = life.primary_role();
    let mirror = life
        .data_roles()
        .into_iter()
        .find(|r| r.mirrors_primary)
        .map(|r| r.role)
        .unwrap_or_default();
    let main_data_gib = machines
        .iter()
        .find(|m| m.role == primary)
        .and_then(|m| m.disks.iter().skip(1).map(|d| d.gib).min())
        .unwrap_or(0);
    Quote {
        sizes: EstateSizes {
            front_plan: plan(&life.update_plan().front_role),
            main_plan: plan(&primary),
            main_data_gib,
            ..Default::default()
        },
        twin_plan: plan(&mirror),
        machines: machines
            .iter()
            .map(|m| price::MachineLine {
                role: m.role.clone(),
                plan: m.plan.clone(),
                cores: 0,
                memory_mib: 0,
                disks_gib: m.disks.iter().map(|d| d.gib).collect(),
            })
            .collect(),
        cost: monetize_cloud::Price {
            minor_per_month: 0,
            currency,
        },
        cost_in_sale: zero,
        monthly: zero,
        margin_bp: 0,
        term_months: 0,
        term_days: 0,
        total: zero,
        offered: Vec::new(),
    }
}

/// **`EstateOrder.AdoptEstate`** — see the module. Refused by name, before anything is minted:
/// a site that is not a declared operator site or names no site, machines that are not front,
/// main and twin or that the provider does not hold, a site that already has a run here (an
/// adopted one: unless `again`), a shop with no custody or no door.
pub fn adopt(state: &AppState, ask: &AdoptAsk) -> Result<Adopted, StartRefusal> {
    let base = state.estates.cfg.as_ref().ok_or_else(|| {
        StartRefusal::NotSold(
            "this monetize lays no estates ([estate_orders] is absent): nothing is adopted".into(),
        )
    })?;
    let estate = ask.estate.trim();
    let Some(admin) = state.registry.operator_admin_of(estate) else {
        return Err(StartRefusal::NotSold(format!("{estate:?} is not a declared operator site ([estates.<label>] operator_site = true with its owner): only the operator's own sites are adopted")));
    };
    if state.registry.site_of(estate).is_none() {
        return Err(StartRefusal::NotSold(format!(
            "[estates.{estate}] names no site: an adopted site is the site's own apex"
        )));
    }
    let company = site::company_of(estate);
    if let Some(why) = company_name_refusal(&company) {
        return Err(StartRefusal::NotSold(format!("the site {estate} cannot be adopted: as the cloud names an estate it is {company:?}, and {why}")));
    }
    let mut terms = super::terms_on(state, base, Some(estate)).map_err(StartRefusal::NotSold)?;
    terms.site_itself = company.clone();
    // The site's product's roles, words and sizes: its lifecycle's (Gunnar's front, main, twin).
    let life = super::lifecycle::of(&terms).map_err(StartRefusal::NotSold)?;
    let roles = life.roles();
    if let Some(why) = machines_refusal(&life, &ask.machines) {
        return Err(StartRefusal::BadName(why));
    }
    if let Some(role) = ask.release_of.keys().find(|r| !roles.contains(r)) {
        return Err(StartRefusal::BadName(format!(
            "{role:?} is no role of an estate: a release is named for {}",
            listed(&roles, "or")
        )));
    }
    let existing = state
        .estates
        .get(&company)
        .filter(|r| !phase::is_gone(&r.phase));
    match &existing {
        Some(r) if r.adopted_at_ms > 0 && !ask.again => return Err(StartRefusal::Taken(format!("{estate} is adopted already ({}): `--again` adopts it again with a NEW client key — its front must then be fed the new one", super::iso_date(r.adopted_at_ms)))),
        Some(r) if r.adopted_at_ms == 0 => return Err(StartRefusal::Taken(format!("{estate} has a run here already ({}, {}): this box laid it, nothing is adopted", r.reference, r.phase))),
        _ => {}
    }
    let custody = state.estates.custody.as_ref().ok_or_else(|| {
        StartRefusal::NotSold("this box holds no custody: no client key can be minted".into())
    })?;
    let door = terms.shop_door().map_err(StartRefusal::NotNow)?;
    let product = terms.sold_product().map_err(StartRefusal::NotNow)?;
    let shop_pubkey: String = state
        .signer_for(product)
        .public_key()
        .iter()
        .map(|b| format!("{b:02x}"))
        .collect();
    // Each machine stands at the provider, by its own id.
    let cloud = state.registry.find_cloud(&terms.cloud).ok_or_else(|| {
        StartRefusal::NotSold(format!(
            "the cloud {:?} is not linked here: the machines cannot be read back",
            terms.cloud
        ))
    })?;
    for m in &ask.machines {
        let id = monetize_cloud::ResourceId {
            provider: cloud.name().into(),
            id: m.server.trim().to_string(),
        };
        match monetize_cloud::ask::status(&cloud, &id) {
            Ok(ResourceStatus::Active)
            | Ok(ResourceStatus::Provisioning)
            | Ok(ResourceStatus::Stopped) => {}
            Ok(ResourceStatus::Gone) | Err(CloudError::UnknownResource(_)) => {
                return Err(StartRefusal::NotFound(format!(
                    "the {} {} is not found at the provider ({}): nothing is adopted",
                    m.role, m.server, terms.cloud
                )))
            }
            Err(e) => {
                return Err(StartRefusal::NotNow(format!(
                    "the provider could not say whether the {} {} stands: {e}; nothing is adopted",
                    m.role, m.server
                )))
            }
        }
    }
    let _pen = state.estates.pen_for_a_call(&terms, &company)?;
    let now = state.now();
    let dir = state
        .estates
        .run_dir(&company)
        .ok_or_else(|| StartRefusal::NotSold("no estate-orders directory".into()))?;
    // **The client key, minted through the lay's custody path**: a fresh seal (an earlier
    // adoption's or a gone run's is forgotten first), its public half named, its seed read from
    // the hand-off — then forgotten: the shop keeps the public half alone.
    let minted = (|| -> Result<(String, String), String> {
        custody.forget(&company)?;
        custody.mint(&company)?;
        let public = custody
            .estate_client_public(&company)?
            .ok_or("the custody holds no client key for the estate")?;
        let handoff = custody.hand_off(&company, &dir.join("adopt-handoff"))?;
        let seed =
            std::fs::read_to_string(handoff.dir().join(crate::custody::handoff::ESTATE_CLIENT))
                .map_err(|e| format!("the hand-off's client key: {e}"))?;
        drop(handoff);
        custody.forget(&company)?;
        Ok((public, seed.trim().to_string()))
    })();
    let (public, seed) = match minted {
        Ok(k) => k,
        Err(e) => {
            // What was minted is forgotten; a custody that cannot forget it is said (A28 M16),
            // and the next adopt of the estate forgets it first.
            let left = match custody.forget(&company) {
                Ok(_) => String::new(),
                Err(f) => format!("; the custody still holds what was minted for it ({f}), and the next adopt forgets it first"),
            };
            return Err(StartRefusal::NotNow(format!(
                "the client key could not be minted: {e}; nothing is adopted{left}"
            )));
        }
    };
    let feed = BTreeMap::from([
        (
            FEED_SHOP_DOOR.to_string(),
            shop_door_toml(&door, &company, &shop_pubkey).map_err(StartRefusal::NotNow)?,
        ),
        (client_key_feed(), seed),
    ]);
    let zone = if ask.zone.trim().is_empty() {
        terms.zone.clone()
    } else {
        ask.zone.trim().to_string()
    };
    let mut machines: Vec<LaidMachine> = roles
        .iter()
        .filter_map(|r| ask.machines.iter().find(|m| m.role == *r).cloned())
        .collect();
    for m in &mut machines {
        m.server = m.server.trim().to_string();
        m.medium_clone.clear();
    }
    let laid = LaidEstate {
        reference: reference_of(&company),
        company: company.clone(),
        zone: Zone(zone),
        machines: machines.clone(),
        narrate_from: String::new(),
        shape: None,
    };
    // MAP-SEG 4: the machines join the labelled listing from now on — labels only, nothing
    // else is written; a cloud that cannot label is said, and the inventory looks them up by uuid.
    let labelled = match monetize_cloud::ask::label_adopted(&cloud, &laid) {
        Ok(ids) => format!(
            "; {} of its servers and disks labelled {}",
            ids.len(),
            laid.reference
        ),
        Err(e) => format!("; NOT labelled ({e}): the inventory looks its machines up by uuid"),
    };
    let named = |m: &LaidMachine| {
        format!(
            "{} {}{}",
            m.role,
            m.server,
            if m.public_ipv4.is_empty() {
                String::new()
            } else {
                format!(" ({} / {})", m.public_ipv4, m.utility_ipv4)
            }
        )
    };
    let note = format!(
        "{ADOPTED} by {}: the kit-installed site {estate} is this box's to update — {}; its client key minted (public {}…), its front to be fed it",
        if ask.by.trim().is_empty() { "the operator" } else { ask.by.trim() },
        machines.iter().map(named).collect::<Vec<_>>().join(", "),
        &public[..public.len().min(12)]
    ) + &labelled;
    let run = match existing {
        Some(mut run) => {
            run.laid = Some(laid);
            run.client_key = public;
            if !ask.release_of.is_empty() {
                run.release_of = ask.release_of.clone();
            }
            run.adopted_at_ms = now.max(1);
            run.secrets_forgotten_at_ms = now.max(1);
            run.history.push(Transition {
                phase: String::new(),
                at_ms: now,
                note: format!("{note} (again: the earlier client key opens nothing any more)"),
            });
            run
        }
        None => {
            let currency = price::sale_currency(&terms).unwrap_or(monetize_pay::Currency::Eur);
            let quote = quote_of(&life, &machines, currency);
            let zero = Money { minor: 0, currency };
            EstateRun {
                company: company.clone(),
                reference: reference_of(&company),
                email: String::new(),
                estate: estate.to_string(),
                account: admin.clone(),
                phase: phase::CAPTURED.into(),
                ordered_at_ms: now,
                held_at_ms: 0,
                amount: zero,
                quote,
                domain: String::new(),
                proof: String::new(),
                dns_waiting_since_ms: 0,
                dns_verified_at_ms: 0,
                captured_at_ms: now,
                paid_until_ms: 0,
                handle: None,
                checkout_url: String::new(),
                laid: Some(laid),
                dns: Vec::new(),
                dns_writer: String::new(),
                product: terms.product.trim().to_string(),
                stopped_why: String::new(),
                stopped_at_ms: 0,
                receipt: None,
                why: String::new(),
                history: vec![Transition {
                    phase: phase::CAPTURED.into(),
                    at_ms: now,
                    note,
                }],
                // Nobody is mailed: the site ran before this box knew it.
                told: true,
                given_back: Vec::new(),
                give_back_failures: 0,
                give_back_retry_at_ms: 0,
                peers: Vec::new(),
                laid_cost: zero,
                spend_cap: Money {
                    minor: terms.spend_cap_monthly_minor,
                    currency,
                },
                renewal: None,
                renewals: Vec::new(),
                reminded: Vec::new(),
                delete_on_ms: 0,
                suspended_at_ms: 0,
                stopped: Vec::new(),
                change: None,
                changes: Vec::new(),
                twin_floor: String::new(),
                twin_changed_at_ms: 0,
                sizes_read_at_ms: 0,
                client_key: public,
                growth: None,
                cap_waiting_since_ms: 0,
                cap_cleared_at_ms: 0,
                queue_looked_at_ms: 0,
                queue_told_at_ms: 0,
                owed_told_at_ms: 0,
                queue_why: String::new(),
                failures: 0,
                cancelled_at_ms: 0,
                told_tried_at_ms: now,
                // The custody was forgotten above: the shop keeps the public client key alone.
                secrets_forgotten_at_ms: now.max(1),
                // No release clone was laid: nothing to give back.
                media_forgotten_at_ms: now.max(1),
                release_of: ask.release_of.clone(),
                material_taken_at_ms: 0,
                sign_in_again_at_ms: 0,
                sale_checked_at_ms: 0,
                seam_looked_at_ms: 0,
                test: false,
                operator_test: super::is_operator_buy(state, estate, &admin),
                site_order: true,
                site_bearer_sha256: String::new(),
                site_bearer_at_ms: 0,
                site_bearer_until_ms: 0,
                site_ticket_key: String::new(),
                site_seed_key: String::new(),
                update: None,
                updates: Vec::new(),
                live_update_off: false,
                adopted_at_ms: now.max(1),
                update_taken_at_ms: 0,
                new_account: None,
                reversals: Vec::new(),
                reversed_at_ms: 0,
                unreleased: Vec::new(),
            }
        }
    };
    super::save(state, &run).map_err(|e| {
        StartRefusal::NotNow(format!("the run could not be written: {e}; adopt it again"))
    })?;
    let words = format!("{estate} is adopted as {} ({}): this box updates it from now on (`estate update {estate}`). Feed its FRONT the two lines answered here — {} and {} — then put its installer material in its primary's slot.", run.company, run.reference, FEED_SHOP_DOOR, client_key_feed());
    // The log names the run, never the feed.
    eprintln!(
        "monetize-server: estate order {}: {}",
        run.reference,
        run.history
            .last()
            .map(|t| t.note.as_str())
            .unwrap_or(ADOPTED)
    );
    Ok(Adopted { run, feed, words })
}

/// Whether `run` was adopted (a kit-installed site the shop did not lay).
pub fn is_adopted(run: &EstateRun) -> bool {
    run.adopted_at_ms > 0
}

/// The refusal a verb that would lay, re-lay or give back an adopted estate's machines answers.
pub fn not_laid_here(run: &EstateRun, what: &str) -> String {
    format!("{} was adopted from its kit's install, not laid by this box: its machines carry no label of this shop, so {what} is its kit's, never this box's", run.estate)
}

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

    fn m(role: &str, server: &str) -> LaidMachine {
        LaidMachine {
            role: role.into(),
            hostname: String::new(),
            title: String::new(),
            plan: "1xCPU-2GB".into(),
            server: server.into(),
            public_ipv4: "198.51.100.1".into(),
            utility_ipv4: "10.0.0.1".into(),
            disks: vec![],
            medium_clone: String::new(),
        }
    }

    #[test]
    fn an_estate_to_adopt_is_front_main_and_twin_each_by_its_own_server() {
        let ok = vec![m("front", "f"), m("main", "a"), m("twin", "t")];
        assert_eq!(
            machines_refusal(&crate::testing::lifecycles::gunnar(), &ok),
            None
        );
        assert!(
            machines_refusal(&crate::testing::lifecycles::gunnar(), &ok[..2])
                .unwrap()
                .contains("no twin")
        );
        assert!(machines_refusal(
            &crate::testing::lifecycles::gunnar(),
            &[m("front", "f"), m("main", "a"), m("twin", "a")]
        )
        .unwrap()
        .contains("same server"));
        assert!(machines_refusal(
            &crate::testing::lifecycles::gunnar(),
            &[m("front", "f"), m("main", ""), m("twin", "t")]
        )
        .unwrap()
        .contains("no server id"));
        assert!(machines_refusal(
            &crate::testing::lifecycles::gunnar(),
            &[
                m("front", "f"),
                m("main", "a"),
                m("twin", "t"),
                m("relay", "r")
            ]
        )
        .unwrap()
        .contains("no role"));
        let mut bad = ok.clone();
        bad[0].public_ipv4 = "gunnar.rs".into();
        assert!(
            machines_refusal(&crate::testing::lifecycles::gunnar(), &bad)
                .unwrap()
                .contains("not an IPv4")
        );
    }

    #[test]
    fn the_shop_door_is_fed_as_a_lays_pairing_feeds_it() {
        let t: toml::Table = toml::from_str(
            &shop_door_toml("https://monetize.ignalina.dk", "gunnarrs", &"ab".repeat(32)).unwrap(),
        )
        .unwrap();
        assert_eq!(t["door"].as_str(), Some("https://monetize.ignalina.dk"));
        assert_eq!(t["company"].as_str(), Some("gunnarrs"));
        assert_eq!(t["client_key_secret"].as_str(), Some("shop_client_key"));
        assert_eq!(client_key_feed(), "secret.shop_client_key");
    }

    #[test]
    fn an_adopted_estate_is_priced_at_nothing_and_quoted_as_it_stands() {
        let mut main = m("main", "a");
        main.disks = vec![
            monetize_cloud::lay::LaidDisk {
                title: "sys".into(),
                id: "d0".into(),
                gib: 40,
                tier: "maxiops".into(),
                address: "virtio:0".into(),
            },
            monetize_cloud::lay::LaidDisk {
                title: "data".into(),
                id: "d1".into(),
                gib: 100,
                tier: "maxiops".into(),
                address: "virtio:1".into(),
            },
        ];
        let q = quote_of(
            &crate::testing::lifecycles::gunnar(),
            &[m("front", "f"), main, m("twin", "t")],
            monetize_pay::Currency::Eur,
        );
        assert_eq!(
            (q.monthly.minor, q.total.minor, q.cost.minor_per_month),
            (0, 0, 0)
        );
        assert_eq!(
            (q.sizes.main_data_gib, q.twin_plan.as_str()),
            (100, "1xCPU-2GB")
        );
    }
}