crates/monetize-cloud/tests/release_for.rs
//! **`Give::References`: the tenant's whole footprint, given back.**
//!
//! `monetize::transaction::release_tenant` walks the LEDGER and destroys what
//! it finds. That is the right instrument for a teardown and it is blind to the
//! one class of resource that costs the most: an **orphan** — a disk the
//! provider still bills for that the ledger has no row for.
//! `monetize::inventory` names them and says why they matter:
//!
//! > an orphan — the cloud has it, the ledger does not — is money paid for
//! > something we forgot. It bills forever and nobody notices; found by hand
//! > with curl on 2026-09-05.
//!
//! A "super purge" that returned only what the ledger remembered would leave
//! exactly those behind, and they are the resources most likely to be there,
//! because a run that crashed between buying and recording is what makes one.
//!
//! # Why this is a trait method and not a fold in `monetize`
//!
//! Because the answer to *what belongs to this tenant here* is the provider's
//! and nobody else's. It is read off the `monetize_ref` label the provider
//! itself stamps, out of an account listing only the provider can take, and the
//! rows that must be spared — the box's own dataset, and anything with no label
//! at all — are distinctions only the provider draws. monetize core knows the
//! tenant's reference strings and nothing else about the account.
//!
//! The default implementation is written in terms of `inventory` and `destroy`,
//! both of which every provider already has to implement, so no provider is
//! forced to write anything and every one of them inherits its own `destroy` —
//! UpCloud's detach-then-delete, the sandbox's state file, the pool ledger each
//! of them forgets keys in. A provider that can do better in one API call
//! overrides it.
//!
//! **A default here is not the silence `Product::can_absorb` refuses to have.**
//! That method has none because every constant it could return is a lie about
//! capacity. This one's default is an exact implementation of the operation,
//! built from two required methods; the dangerous default would be `Ok(no rows)`,
//! and that is the one thing it cannot answer, because it asks the provider.
use monetize_cloud::{
Cloud, CloudError, Ensured, Inventory, Owned, OwnedKind, Price, Resource, ResourceId,
ResourceStatus, Zone,
};
use monetize_pay::Currency;
use std::sync::Mutex;
/// A provider holding a fixed account listing, recording every destroy.
struct Account {
owned: Vec<Owned>,
destroyed: Mutex<Vec<String>>,
/// Ids whose destroy fails, by id.
refuse: Vec<String>,
}
impl Account {
fn new(owned: Vec<Owned>) -> Self {
Account {
owned,
destroyed: Mutex::new(Vec::new()),
refuse: Vec::new(),
}
}
fn refusing(mut self, id: &str) -> Self {
self.refuse.push(id.to_owned());
self
}
fn destroyed(&self) -> Vec<String> {
self.destroyed.lock().unwrap().clone()
}
}
fn owned(id: &str, kind: OwnedKind, reference: Option<&str>) -> Owned {
Owned {
id: ResourceId {
provider: "fake".into(),
id: id.into(),
},
kind,
gib: 10,
zone: "se-sto1".into(),
attached_to: None,
reference: reference.map(str::to_owned),
monthly_minor: Some(1_000),
currency: Currency::Sek,
provenance: "fake".into(),
resource: None,
backup: None,
}
}
#[allow(dead_code)]
impl Account {
pub fn provider(&self) -> &'static str {
"fake"
}
pub fn zones(&self) -> &[Zone] {
&[]
}
pub fn price_resource(&self, _resource: &Resource) -> Result<Price, CloudError> {
Err(CloudError::Unavailable("no price list".into()))
}
pub fn ensure(&self, _reference: &str, _resource: &Resource) -> Result<Ensured, CloudError> {
Err(CloudError::Refused("this account never buys".into()))
}
pub fn destroy_id(&self, id: &ResourceId) -> Result<(), CloudError> {
if self.refuse.contains(&id.id) {
return Err(CloudError::Unavailable(format!("{} is wedged", id.id)));
}
self.destroyed.lock().unwrap().push(id.id.clone());
Ok(())
}
pub fn status(&self, _id: &ResourceId) -> Result<ResourceStatus, CloudError> {
Ok(ResourceStatus::Active)
}
pub fn list_owned(&self) -> Result<Inventory, CloudError> {
Ok(Inventory {
owned: self.owned.clone(),
foreign: 3,
currency: Currency::Sek,
})
}
}
#[allow(unused_variables, unreachable_code)]
impl monetize_cloud::cloud::Cloud for Account {
fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
monetize_cloud::cloud::CloudManifest { provider: self.provider().to_string(), zones: self.zones().to_vec(), settings: Vec::new(), grow_refused: Some(monetize_cloud::grow::unsupported(self.provider()).to_string()) }
}
fn catalog(&self, zone: &monetize_cloud::Zone) -> Result<monetize_cloud::cloud::Catalog, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn price(&self, what: &monetize_cloud::cloud::Priced<'_>) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Priced::Resource(r) => self.price_resource(r),
monetize_cloud::cloud::Priced::Plan(p) => monetize_cloud::price_estate_by_rows(p, |r| self.price_resource(r), None),
monetize_cloud::cloud::Priced::Grow(set) => Err(monetize_cloud::grow::unsupported(self.provider())),
}
}
fn lay(&self, plan: &monetize_cloud::lay::EstatePlan, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::lay::LaidEstate, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn change(&self, change: &monetize_cloud::cloud::Change, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::cloud::Changed, monetize_cloud::CloudError> {
Ok(match change {
monetize_cloud::cloud::Change::Plan { server, plan, delta } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "change_plan")),
monetize_cloud::cloud::Change::Grow { set, estate } => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Change::Settle { set } => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Change::ForgetMedia { laid } => monetize_cloud::cloud::Changed::Forgotten(Vec::new()),
monetize_cloud::cloud::Change::Ensure { reference, resource, room } => monetize_cloud::cloud::Changed::Ensured(self.ensure(reference, resource)?),
monetize_cloud::cloud::Change::Retire { reference, machine } => monetize_cloud::cloud::Changed::Retired(monetize_cloud::cloud::retire_by_rows(self, reference, machine)?),
monetize_cloud::cloud::Change::LabelAdopted { .. } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "label_adopted")),
})
}
fn boot(&self, boot: &monetize_cloud::cloud::Boot, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::cloud::Booted, monetize_cloud::CloudError> {
Ok(match boot {
monetize_cloud::cloud::Boot::Keep(k) => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "keep_boot")),
monetize_cloud::cloud::Boot::Power { server, on } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "set_power")),
monetize_cloud::cloud::Boot::Restore(r) => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "restore_from_backup")),
})
}
fn firewall(&self, laid: &monetize_cloud::lay::LaidEstate) -> Result<(), monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported_verb(self.provider(), "set_estate_firewall"))
}
fn destroy(&self, what: &monetize_cloud::cloud::Give<'_>) -> Result<monetize_cloud::Reclaimed, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Give::References(refs) => Ok(monetize_cloud::release_for_by_rows(self.list_owned()?, refs, |id| self.destroy_id(id))),
monetize_cloud::cloud::Give::Resource(id) => {
self.destroy_id(id)?;
Ok(monetize_cloud::Reclaimed { destroyed: vec![(*id).clone()], ..monetize_cloud::Reclaimed::default() })
}
}
}
fn inventory(&self, what: &monetize_cloud::cloud::Listing<'_>) -> Result<monetize_cloud::cloud::Listed, monetize_cloud::CloudError> {
Ok(match what {
monetize_cloud::cloud::Listing::Account => monetize_cloud::cloud::Listed::Account(self.list_owned()?),
monetize_cloud::cloud::Listing::Set(set) => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Listing::Status(id) => monetize_cloud::cloud::Listed::Status(self.status(id)?),
monetize_cloud::cloud::Listing::Under(refs) => monetize_cloud::cloud::Listed::Under(monetize_cloud::cloud::under(self.list_owned()?, refs)),
monetize_cloud::cloud::Listing::Room { delta, room } => monetize_cloud::cloud::Listed::Room(monetize_cloud::cloud::RoomLeft { serve: Ok(()), left_bytes: None, ceilings: Default::default() }),
monetize_cloud::cloud::Listing::Admits(_plan) => monetize_cloud::cloud::Listed::Admitted,
})
}
}
/// **The headline: everything under the tenant's references goes, including the
/// row the ledger never knew about — and nothing else does.**
///
/// The account here is deliberately mixed, because a release that destroyed the
/// whole account would pass any test that only checked the tenant's own rows
/// were gone.
#[test]
fn a_release_takes_the_tenants_rows_including_orphans_and_spares_everything_else() {
let account = Account::new(vec![
// The tenant's, and the ledger knows about this one.
owned("disk-a", OwnedKind::Disk, Some("ord-1")),
// The tenant's, from a second order.
owned("srv-b", OwnedKind::Server, Some("ord-2")),
// **The orphan.** Bought under one of the tenant's references and never
// recorded — the class `release_tenant` can never reach, and the one
// that bills for ever.
owned("disk-orphan", OwnedKind::Disk, Some("ord-2")),
// Somebody else's tenant.
owned("disk-other", OwnedKind::Disk, Some("ord-99")),
// **The box's own data set.** The `server` provider reports one of these
// for the iron monetize itself runs on; destroying it is destroying the
// deployment.
owned("this-box", OwnedKind::Dataset, Some("ord-1")),
// Labelled with nothing at all: not monetize's, whoever's it is.
owned("stranger", OwnedKind::Disk, None),
]);
let released = monetize_cloud::ask::release_for(&account, &["ord-1".to_owned(), "ord-2".to_owned()])
.expect("the release runs");
let mut gone = account.destroyed();
gone.sort();
assert_eq!(
gone,
vec!["disk-a".to_owned(), "disk-orphan".to_owned(), "srv-b".to_owned()],
"asserted on what DESTROY was actually called with, not on the returned list — a release \
that reported ids it never handed back would be green on the report"
);
let mut reported: Vec<String> = released.destroyed.iter().map(|id| id.id.clone()).collect();
reported.sort();
assert_eq!(reported, gone, "the report and the calls must agree");
assert!(released.failures.is_empty(), "{released:?}");
assert_eq!(
released.skipped_other_tenants, 1,
"one row belonged to another tenant and must be COUNTED, not silently ignored: a release \
that scoped itself wrongly and a release that found nothing look identical otherwise"
);
assert_eq!(
released.spared_datasets, 1,
"the box's own data set must be spared and said so; a silent skip would be one line \
between an operator and the deployment"
);
}
/// **A reference nobody bought under releases nothing**, and that is a success.
///
/// A tenant served entirely from the pool owns no provider-side resource at
/// all, and a super purge of one must not look like a failure.
#[test]
fn a_tenant_with_nothing_at_the_provider_releases_nothing_and_that_is_not_an_error() {
let account = Account::new(vec![owned("disk-other", OwnedKind::Disk, Some("ord-99"))]);
let released = monetize_cloud::ask::release_for(&account, &["ord-1".to_owned()])
.expect("the release runs");
assert!(released.destroyed.is_empty());
assert!(released.failures.is_empty());
assert_eq!(released.skipped_other_tenants, 1);
assert!(
account.destroyed().is_empty(),
"nothing of anybody's may be destroyed by a release that matched nothing"
);
}
/// **An empty reference list destroys NOTHING.**
///
/// The single most dangerous input this method can be given: a caller whose
/// ledger read failed, or whose tenant turns out to have no orders. An
/// implementation that read "no filter" as "everything" would empty the whole
/// account — every tenant's, and the box's own — on a caller's bad day.
#[test]
fn an_empty_reference_list_is_not_a_wildcard() {
let account = Account::new(vec![
owned("disk-a", OwnedKind::Disk, Some("ord-1")),
owned("disk-other", OwnedKind::Disk, Some("ord-99")),
owned("this-box", OwnedKind::Dataset, Some("ord-1")),
]);
let released = monetize_cloud::ask::release_for(&account, &[]).expect("the release runs");
assert!(
account.destroyed().is_empty(),
"an empty reference list destroyed {:?}; it must be read as `nothing matches`, never as \
`no filter`",
account.destroyed()
);
assert!(released.destroyed.is_empty());
}
/// **One wedged resource does not stop the others, and is named.**
///
/// The opposite choice from `release_tenant`, which stops at the first failure
/// so a retry finds the rest still held. It can afford that: every id it walks
/// is in the ledger, so nothing is forgotten by stopping. Here the list is the
/// PROVIDER's, and the rows are exactly the ones the ledger may not know —
/// stopping would leave the remaining orphans billing with nothing to remind
/// anybody they exist. So the walk finishes, and what is left is named.
#[test]
fn a_wedged_resource_is_named_and_the_rest_still_go() {
let account = Account::new(vec![
owned("disk-a", OwnedKind::Disk, Some("ord-1")),
owned("disk-wedged", OwnedKind::Disk, Some("ord-1")),
owned("disk-c", OwnedKind::Disk, Some("ord-1")),
])
.refusing("disk-wedged");
let released = monetize_cloud::ask::release_for(&account, &["ord-1".to_owned()])
.expect("a partial release still reports");
let mut gone = account.destroyed();
gone.sort();
assert_eq!(
gone,
vec!["disk-a".to_owned(), "disk-c".to_owned()],
"the walk stopped at the wedged row and left the rest billing"
);
assert_eq!(released.failures.len(), 1, "{released:?}");
assert!(
released.failures[0].contains("disk-wedged"),
"the failure must NAME what the provider still holds, or nobody can go and free it: \
{released:?}"
);
assert!(
released.failures[0].contains("wedged"),
"and it must carry the provider's own words: {released:?}"
);
}
/// A resource already gone at the provider counts as released. A super purge is
/// retried, and the second run finds the first run's work done.
#[test]
fn a_resource_already_gone_counts_as_released() {
struct Vanished(Account);
#[allow(dead_code)]
impl Vanished {
pub fn provider(&self) -> &'static str {
self.0.provider()
}
pub fn zones(&self) -> &[Zone] {
self.0.zones()
}
pub fn price_resource(&self, r: &Resource) -> Result<Price, CloudError> {
monetize_cloud::Cloud::price(&self.0, &monetize_cloud::cloud::Priced::Resource(r))
}
pub fn ensure(&self, reference: &str, r: &Resource) -> Result<Ensured, CloudError> {
self.0.ensure(reference, r)
}
pub fn destroy_id(&self, id: &ResourceId) -> Result<(), CloudError> {
Err(CloudError::UnknownResource(id.clone()))
}
pub fn status(&self, id: &ResourceId) -> Result<ResourceStatus, CloudError> {
self.0.status(id)
}
pub fn list_owned(&self) -> Result<Inventory, CloudError> {
monetize_cloud::ask::account(&self.0)
}
}
#[allow(unused_variables, unreachable_code)]
impl monetize_cloud::cloud::Cloud for Vanished {
fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
monetize_cloud::cloud::CloudManifest { provider: self.provider().to_string(), zones: self.zones().to_vec(), settings: Vec::new(), grow_refused: Some(monetize_cloud::grow::unsupported(self.provider()).to_string()) }
}
fn catalog(&self, zone: &monetize_cloud::Zone) -> Result<monetize_cloud::cloud::Catalog, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn price(&self, what: &monetize_cloud::cloud::Priced<'_>) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Priced::Resource(r) => self.price_resource(r),
monetize_cloud::cloud::Priced::Plan(p) => monetize_cloud::price_estate_by_rows(p, |r| self.price_resource(r), None),
monetize_cloud::cloud::Priced::Grow(set) => Err(monetize_cloud::grow::unsupported(self.provider())),
}
}
fn lay(&self, plan: &monetize_cloud::lay::EstatePlan, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::lay::LaidEstate, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn change(&self, change: &monetize_cloud::cloud::Change, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::cloud::Changed, monetize_cloud::CloudError> {
Ok(match change {
monetize_cloud::cloud::Change::Plan { server, plan, delta } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "change_plan")),
monetize_cloud::cloud::Change::Grow { set, estate } => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Change::Settle { set } => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Change::ForgetMedia { laid } => monetize_cloud::cloud::Changed::Forgotten(Vec::new()),
monetize_cloud::cloud::Change::Ensure { reference, resource, room } => monetize_cloud::cloud::Changed::Ensured(self.ensure(reference, resource)?),
monetize_cloud::cloud::Change::Retire { reference, machine } => monetize_cloud::cloud::Changed::Retired(monetize_cloud::cloud::retire_by_rows(self, reference, machine)?),
monetize_cloud::cloud::Change::LabelAdopted { .. } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "label_adopted")),
})
}
fn boot(&self, boot: &monetize_cloud::cloud::Boot, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::cloud::Booted, monetize_cloud::CloudError> {
Ok(match boot {
monetize_cloud::cloud::Boot::Keep(k) => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "keep_boot")),
monetize_cloud::cloud::Boot::Power { server, on } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "set_power")),
monetize_cloud::cloud::Boot::Restore(r) => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "restore_from_backup")),
})
}
fn firewall(&self, laid: &monetize_cloud::lay::LaidEstate) -> Result<(), monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported_verb(self.provider(), "set_estate_firewall"))
}
fn destroy(&self, what: &monetize_cloud::cloud::Give<'_>) -> Result<monetize_cloud::Reclaimed, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Give::References(refs) => Ok(monetize_cloud::release_for_by_rows(self.list_owned()?, refs, |id| self.destroy_id(id))),
monetize_cloud::cloud::Give::Resource(id) => {
self.destroy_id(id)?;
Ok(monetize_cloud::Reclaimed { destroyed: vec![(*id).clone()], ..monetize_cloud::Reclaimed::default() })
}
}
}
fn inventory(&self, what: &monetize_cloud::cloud::Listing<'_>) -> Result<monetize_cloud::cloud::Listed, monetize_cloud::CloudError> {
Ok(match what {
monetize_cloud::cloud::Listing::Account => monetize_cloud::cloud::Listed::Account(self.list_owned()?),
monetize_cloud::cloud::Listing::Set(set) => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Listing::Status(id) => monetize_cloud::cloud::Listed::Status(self.status(id)?),
monetize_cloud::cloud::Listing::Under(refs) => monetize_cloud::cloud::Listed::Under(monetize_cloud::cloud::under(self.list_owned()?, refs)),
monetize_cloud::cloud::Listing::Room { delta, room } => monetize_cloud::cloud::Listed::Room(monetize_cloud::cloud::RoomLeft { serve: Ok(()), left_bytes: None, ceilings: Default::default() }),
monetize_cloud::cloud::Listing::Admits(_plan) => monetize_cloud::cloud::Listed::Admitted,
})
}
}
let account = Vanished(Account::new(vec![owned(
"disk-a",
OwnedKind::Disk,
Some("ord-1"),
)]));
let released = monetize_cloud::ask::release_for(&account, &["ord-1".to_owned()])
.expect("the release runs");
assert_eq!(
released.destroyed.len(),
1,
"a resource the provider no longer has IS released; reporting it as a failure would make \
every retry of a super purge look broken: {released:?}"
);
assert!(released.failures.is_empty(), "{released:?}");
}
/// **An inventory that cannot be taken is an ERROR, never an empty release.**
///
/// The one answer that must not be optimistic. "I could not look" and "there is
/// nothing there" would be the same green, and a caller would record a tenant's
/// iron as returned while the provider went on billing for it.
#[test]
fn an_unreadable_inventory_is_refused_rather_than_reported_as_nothing_to_do() {
struct Blind;
#[allow(dead_code)]
impl Blind {
pub fn provider(&self) -> &'static str {
"blind"
}
pub fn zones(&self) -> &[Zone] {
&[]
}
pub fn price_resource(&self, _r: &Resource) -> Result<Price, CloudError> {
Err(CloudError::Unavailable("no".into()))
}
pub fn ensure(&self, _reference: &str, _r: &Resource) -> Result<Ensured, CloudError> {
Err(CloudError::Refused("no".into()))
}
pub fn destroy_id(&self, _id: &ResourceId) -> Result<(), CloudError> {
panic!("destroy must not be reached when the account could not be listed");
}
pub fn status(&self, _id: &ResourceId) -> Result<ResourceStatus, CloudError> {
Ok(ResourceStatus::Active)
}
pub fn list_owned(&self) -> Result<Inventory, CloudError> {
Err(CloudError::Unavailable("the API is down".into()))
}
}
#[allow(unused_variables, unreachable_code)]
impl monetize_cloud::cloud::Cloud for Blind {
fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
monetize_cloud::cloud::CloudManifest { provider: self.provider().to_string(), zones: self.zones().to_vec(), settings: Vec::new(), grow_refused: Some(monetize_cloud::grow::unsupported(self.provider()).to_string()) }
}
fn catalog(&self, zone: &monetize_cloud::Zone) -> Result<monetize_cloud::cloud::Catalog, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn price(&self, what: &monetize_cloud::cloud::Priced<'_>) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Priced::Resource(r) => self.price_resource(r),
monetize_cloud::cloud::Priced::Plan(p) => monetize_cloud::price_estate_by_rows(p, |r| self.price_resource(r), None),
monetize_cloud::cloud::Priced::Grow(set) => Err(monetize_cloud::grow::unsupported(self.provider())),
}
}
fn lay(&self, plan: &monetize_cloud::lay::EstatePlan, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::lay::LaidEstate, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn change(&self, change: &monetize_cloud::cloud::Change, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::cloud::Changed, monetize_cloud::CloudError> {
Ok(match change {
monetize_cloud::cloud::Change::Plan { server, plan, delta } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "change_plan")),
monetize_cloud::cloud::Change::Grow { set, estate } => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Change::Settle { set } => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Change::ForgetMedia { laid } => monetize_cloud::cloud::Changed::Forgotten(Vec::new()),
monetize_cloud::cloud::Change::Ensure { reference, resource, room } => monetize_cloud::cloud::Changed::Ensured(self.ensure(reference, resource)?),
monetize_cloud::cloud::Change::Retire { reference, machine } => monetize_cloud::cloud::Changed::Retired(monetize_cloud::cloud::retire_by_rows(self, reference, machine)?),
monetize_cloud::cloud::Change::LabelAdopted { .. } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "label_adopted")),
})
}
fn boot(&self, boot: &monetize_cloud::cloud::Boot, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::cloud::Booted, monetize_cloud::CloudError> {
Ok(match boot {
monetize_cloud::cloud::Boot::Keep(k) => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "keep_boot")),
monetize_cloud::cloud::Boot::Power { server, on } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "set_power")),
monetize_cloud::cloud::Boot::Restore(r) => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "restore_from_backup")),
})
}
fn firewall(&self, laid: &monetize_cloud::lay::LaidEstate) -> Result<(), monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported_verb(self.provider(), "set_estate_firewall"))
}
fn destroy(&self, what: &monetize_cloud::cloud::Give<'_>) -> Result<monetize_cloud::Reclaimed, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Give::References(refs) => Ok(monetize_cloud::release_for_by_rows(self.list_owned()?, refs, |id| self.destroy_id(id))),
monetize_cloud::cloud::Give::Resource(id) => {
self.destroy_id(id)?;
Ok(monetize_cloud::Reclaimed { destroyed: vec![(*id).clone()], ..monetize_cloud::Reclaimed::default() })
}
}
}
fn inventory(&self, what: &monetize_cloud::cloud::Listing<'_>) -> Result<monetize_cloud::cloud::Listed, monetize_cloud::CloudError> {
Ok(match what {
monetize_cloud::cloud::Listing::Account => monetize_cloud::cloud::Listed::Account(self.list_owned()?),
monetize_cloud::cloud::Listing::Set(set) => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Listing::Status(id) => monetize_cloud::cloud::Listed::Status(self.status(id)?),
monetize_cloud::cloud::Listing::Under(refs) => monetize_cloud::cloud::Listed::Under(monetize_cloud::cloud::under(self.list_owned()?, refs)),
monetize_cloud::cloud::Listing::Room { delta, room } => monetize_cloud::cloud::Listed::Room(monetize_cloud::cloud::RoomLeft { serve: Ok(()), left_bytes: None, ceilings: Default::default() }),
monetize_cloud::cloud::Listing::Admits(_plan) => monetize_cloud::cloud::Listed::Admitted,
})
}
}
let err = monetize_cloud::ask::release_for(&Blind, &["ord-1".to_owned()])
.expect_err("an unreadable account must not answer `nothing to release`");
assert!(
format!("{err}").contains("the API is down"),
"the provider's own words must survive: {err}"
);
}
/// **The pointer forms forward it**, so an `Arc<UpCloud>` that overrides
/// `release_for` in one API call is not silently handed the default.
///
/// The forwarding macro is the whole reason this can go wrong quietly: a
/// method added to the trait and not to the macro compiles, ships, and takes
/// the default through every `Arc` and `Box` in the server — which is every
/// provider the registry builds.
#[test]
fn arc_and_box_forward_to_an_overriding_impl() {
struct OneCall;
#[allow(dead_code)]
impl OneCall {
pub fn provider(&self) -> &'static str {
"one-call"
}
pub fn zones(&self) -> &[Zone] {
&[]
}
pub fn price_resource(&self, _r: &Resource) -> Result<Price, CloudError> {
Err(CloudError::Unavailable("no".into()))
}
pub fn ensure(&self, _reference: &str, _r: &Resource) -> Result<Ensured, CloudError> {
Err(CloudError::Refused("no".into()))
}
pub fn destroy_id(&self, _id: &ResourceId) -> Result<(), CloudError> {
Ok(())
}
pub fn status(&self, _id: &ResourceId) -> Result<ResourceStatus, CloudError> {
Ok(ResourceStatus::Active)
}
pub fn list_owned(&self) -> Result<Inventory, CloudError> {
panic!("an overriding release_for must not fall back to the inventory default");
}
pub fn release_for(
&self,
_references: &[String],
) -> Result<monetize_cloud::Reclaimed, CloudError> {
Ok(monetize_cloud::Reclaimed {
destroyed: vec![ResourceId {
provider: "one-call".into(),
id: "everything".into(),
}],
failures: Vec::new(),
skipped_other_tenants: 0,
spared_datasets: 0,
})
}
}
#[allow(unused_variables, unreachable_code)]
impl monetize_cloud::cloud::Cloud for OneCall {
fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
monetize_cloud::cloud::CloudManifest { provider: self.provider().to_string(), zones: self.zones().to_vec(), settings: Vec::new(), grow_refused: Some(monetize_cloud::grow::unsupported(self.provider()).to_string()) }
}
fn catalog(&self, zone: &monetize_cloud::Zone) -> Result<monetize_cloud::cloud::Catalog, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn price(&self, what: &monetize_cloud::cloud::Priced<'_>) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Priced::Resource(r) => self.price_resource(r),
monetize_cloud::cloud::Priced::Plan(p) => monetize_cloud::price_estate_by_rows(p, |r| self.price_resource(r), None),
monetize_cloud::cloud::Priced::Grow(set) => Err(monetize_cloud::grow::unsupported(self.provider())),
}
}
fn lay(&self, plan: &monetize_cloud::lay::EstatePlan, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::lay::LaidEstate, monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported(self.provider()))
}
fn change(&self, change: &monetize_cloud::cloud::Change, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::cloud::Changed, monetize_cloud::CloudError> {
Ok(match change {
monetize_cloud::cloud::Change::Plan { server, plan, delta } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "change_plan")),
monetize_cloud::cloud::Change::Grow { set, estate } => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Change::Settle { set } => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Change::ForgetMedia { laid } => monetize_cloud::cloud::Changed::Forgotten(Vec::new()),
monetize_cloud::cloud::Change::Ensure { reference, resource, room } => monetize_cloud::cloud::Changed::Ensured(self.ensure(reference, resource)?),
monetize_cloud::cloud::Change::Retire { reference, machine } => monetize_cloud::cloud::Changed::Retired(monetize_cloud::cloud::retire_by_rows(self, reference, machine)?),
monetize_cloud::cloud::Change::LabelAdopted { .. } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "label_adopted")),
})
}
fn boot(&self, boot: &monetize_cloud::cloud::Boot, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::cloud::Booted, monetize_cloud::CloudError> {
Ok(match boot {
monetize_cloud::cloud::Boot::Keep(k) => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "keep_boot")),
monetize_cloud::cloud::Boot::Power { server, on } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "set_power")),
monetize_cloud::cloud::Boot::Restore(r) => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "restore_from_backup")),
})
}
fn firewall(&self, laid: &monetize_cloud::lay::LaidEstate) -> Result<(), monetize_cloud::CloudError> {
Err(monetize_cloud::lay::unsupported_verb(self.provider(), "set_estate_firewall"))
}
fn destroy(&self, what: &monetize_cloud::cloud::Give<'_>) -> Result<monetize_cloud::Reclaimed, monetize_cloud::CloudError> {
match what {
monetize_cloud::cloud::Give::References(refs) => self.release_for(refs),
monetize_cloud::cloud::Give::Resource(id) => {
self.destroy_id(id)?;
Ok(monetize_cloud::Reclaimed { destroyed: vec![(*id).clone()], ..monetize_cloud::Reclaimed::default() })
}
}
}
fn inventory(&self, what: &monetize_cloud::cloud::Listing<'_>) -> Result<monetize_cloud::cloud::Listed, monetize_cloud::CloudError> {
Ok(match what {
monetize_cloud::cloud::Listing::Account => monetize_cloud::cloud::Listed::Account(self.list_owned()?),
monetize_cloud::cloud::Listing::Set(set) => return Err(monetize_cloud::grow::unsupported(self.provider())),
monetize_cloud::cloud::Listing::Status(id) => monetize_cloud::cloud::Listed::Status(self.status(id)?),
monetize_cloud::cloud::Listing::Under(refs) => monetize_cloud::cloud::Listed::Under(monetize_cloud::cloud::under(self.list_owned()?, refs)),
monetize_cloud::cloud::Listing::Room { delta, room } => monetize_cloud::cloud::Listed::Room(monetize_cloud::cloud::RoomLeft { serve: Ok(()), left_bytes: None, ceilings: Default::default() }),
monetize_cloud::cloud::Listing::Admits(_plan) => monetize_cloud::cloud::Listed::Admitted,
})
}
}
let boxed: Box<dyn Cloud> = Box::new(OneCall);
assert_eq!(
monetize_cloud::ask::release_for(&boxed, &["ord-1".into()]).expect("boxed").destroyed[0].id,
"everything",
"a Box took the default instead of the impl's own release_for"
);
let shared = std::sync::Arc::new(OneCall);
assert_eq!(
shared.release_for(&["ord-1".into()]).expect("arc").destroyed[0].id,
"everything",
"an Arc took the default instead of the impl's own release_for"
);
}