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/tests/standing_disks.rs

//! ★ **A plan change that GROWS A STANDING MACHINE'S DISK** (monetize v2.0 A4-FACTS, item 2).
//!
//! New numbers can grow the disks of machines that already stand: the flock's `node_gib` is each
//! node's data disk (the Lake's `store_gib`, spread over its backends, is the same kind of
//! number). Before A4-FACTS such a change was refused by name ("not taken yet"). Now the disks
//! are grown at the cloud by the shop itself, once paid: each one detached, resized and put back
//! at its own address on its own server, the provider's sizes read back into the run. What the
//! product's grow rule keeps (`never_shrinks node_gib`) never shrinks.
//!
//! ```text
//!   ChangePlan {node_gib: 20 → 40} ─▶ node and node-2 stand: their data disks GROW
//!     ─▶ priced whole, the difference for the days left HELD ─▶ paid
//!     ─▶ resizing: each node's data set grown at the cloud (grow_estate_set), read back
//!     ─▶ the boxes call home ─▶ CAPTURED ─▶ done; the run's price and laid sizes are the new ones
//!   ChangePlan {node_gib: 40 → 30} ─▶ refused by name: node_gib never shrinks, nothing held
//! ```

use super::*;
use crate::testing::flock;
use monetize::testing::GrowScript;

fn flock_cfg() -> EstateOrderConfig {
    flock::install();
    EstateOrderConfig {
        lifecycle: flock::KIND.into(),
        ..cfg()
    }
}

fn data_gib(r: &Rig, role: &str) -> Vec<u64> {
    r.state
        .estates
        .get("acme")
        .unwrap()
        .laid
        .unwrap()
        .machine(role)
        .unwrap()
        .disks
        .iter()
        .skip(1)
        .map(|d| d.gib)
        .collect()
}

/// ★ **New numbers grow the disks of the machines that stand, at the cloud, once paid.**
#[test]
fn a_change_that_grows_the_standing_nodes_disks_grows_them_at_the_cloud_and_they_never_shrink() {
    let r = rig_with("standing-disks", flock_cfg(), "PASS");
    start_for(
        &r.state,
        "acme",
        "buyer@example.com",
        "",
        "alice",
        &flock::sizes(true, 2, 20),
    )
    .expect("ordered");
    r.pay.authorize(&handle_of(&r, "acme"));
    drive(&r.state, "acme");
    let before = r.state.estates.get("acme").unwrap();
    assert_eq!(before.phase, phase::CAPTURED, "{:#?}", before.history);
    assert_eq!(
        (data_gib(&r, "node"), data_gib(&r, "node-2")),
        (vec![20], vec![20])
    );
    // The provider's data sets: each node's one disk at 20 GiB.
    r.cloud.grows(GrowScript {
        current_gib: 20,
        ..GrowScript::default()
    });

    let c = change::change(&r.state, "acme", &flock::sizes(true, 2, 40))
        .unwrap_or_else(|e| panic!("new numbers that grow standing disks are taken: {e:?}"));
    assert_eq!(
        c.grows
            .iter()
            .map(|g| (g.role.as_str(), g.from_gib, g.to_gib))
            .collect::<Vec<_>>(),
        [("node", 20, 40), ("node-2", 20, 40)],
        "{c:#?}"
    );
    assert!(
        c.adds.is_empty() && c.removes.is_empty() && c.steps.is_empty(),
        "{c:#?}"
    );
    // The rig's cloud prices every resource flat (a bigger disk costs what a small one does), so
    // what is held is exactly the price's rise, here none; the trials hold it at UpCloud's prices.
    assert!(
        c.quote.monthly.minor >= before.quote.monthly.minor,
        "the bigger disks are priced in"
    );
    assert_eq!(
        c.amount,
        change::prorated(
            before.quote.monthly,
            c.quote.monthly,
            r.state.now(),
            before.paid_until_ms
        ),
        "the difference for the days left is held"
    );
    assert_eq!(
        c.status,
        if c.amount.minor > 0 {
            change::status::WAITING_FOR_PAYMENT
        } else {
            change::status::QUEUED
        }
    );
    assert!(
        c.words.contains("node's data disk grows from 20 to 40 GiB"),
        "{}",
        c.words
    );
    assert!(
        !r.cloud
            .calls()
            .iter()
            .any(|x| matches!(x, monetize::testing::CloudCall::Grow(_))),
        "nothing grows before the money is held"
    );

    if let Some(h) = &c.handle {
        r.pay.authorize(&h.id);
    }
    drive(&r.state, "acme");
    drive(&r.state, "acme");
    let run = r.state.estates.get("acme").unwrap();
    assert!(
        run.change.is_none(),
        "{:#?}\n{:#?}",
        run.change,
        run.history
    );
    assert_eq!(
        run.changes.last().unwrap().status,
        change::status::DONE,
        "{:#?}",
        run.changes.last()
    );
    assert!(
        r.cloud
            .calls()
            .iter()
            .any(|x| matches!(x, monetize::testing::CloudCall::Grow(40))),
        "grown at the cloud: {:?}",
        r.cloud.calls()
    );
    assert_eq!(
        (data_gib(&r, "node"), data_gib(&r, "node-2")),
        (vec![40], vec![40]),
        "the provider's sizes are read back into the run"
    );
    assert_eq!(
        r.pay.captures(),
        1 + u64::from(c.handle.is_some()),
        "a hold is captured only once grown and heard"
    );
    assert_eq!(run.quote.monthly, c.quote.monthly, "the bill follows");
    assert!(
        r.mails.last().body.contains("node's data disk is 40 GiB"),
        "{}",
        r.mails.last().body
    );
    // Main's data disks were not touched: a numbers change grows only what its numbers grow.
    assert!(
        data_gib(&r, "main").iter().all(|g| *g == 10),
        "{:?}",
        data_gib(&r, "main")
    );

    // Data never shrinks: the flock's grow rule keeps node_gib.
    let held = r.pay.0.requests.lock().unwrap().len();
    let e = change::change(&r.state, "acme", &flock::sizes(true, 2, 30))
        .expect_err("smaller is refused");
    assert!(
        format!("{e:?}").contains("data never shrinks: node_gib is 40 now, and 30 was asked"),
        "{e:?}"
    );
    assert_eq!(r.pay.0.requests.lock().unwrap().len(), held, "nothing held");
}