crates/monetize-estate-side/src/pump/config.rs
//! **Where this box reports, and as whom** (moved from Holger's `holger-fleet`, P4a). The
//! installer's feed (the same keys every product's boxes read: `fleet`, `fleet_instance`, `fleet_at`, `fleet_ca`, `monetize_pubkey`, plain
//! or offered by a VM as `offer.<vm>.<key>`), else what an enrolment kept at
//! [`CONFIG_REL`].
use std::collections::BTreeMap;
use std::path::Path;
use std::time::Duration;
use serde::{Deserialize, Serialize};
/// What an enrolment keeps under the data root.
pub const CONFIG_REL: &str = "fleet/fleet.json";
pub const FEED_FLEET: &str = "fleet";
pub const FEED_FLEET_INSTANCE: &str = "fleet_instance";
pub const FEED_FLEET_AT: &str = "fleet_at";
pub const FEED_FLEET_CA: &str = "fleet_ca";
pub const FEED_MONETIZE: &str = "monetize_pubkey";
/// The longest pause between reports when monetize names none.
pub const DEFAULT_EVERY: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct FleetConfig {
/// The ONE monetize's `Fleet` door (`https://monetize.ignalina.dk`).
pub url: String,
/// Where to connect (`host:port`) when the URL's host is not what this box dials.
pub at: Option<String>,
/// PEM the door's certificate is checked against; none = the public roots.
pub ca_pem: Option<String>,
/// The estate this box reports for.
pub instance: String,
/// `main` | `twin`; empty = what the box is.
pub role: String,
/// The key orders verify under (hex).
pub monetize_pubkey: String,
}
impl FleetConfig {
/// From a feed; `None` when it names no door or no estate.
pub fn from_feed(feed: &BTreeMap<String, String>) -> Option<FleetConfig> {
let get = |k: &str| {
feed.get(k)
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.or_else(|| {
feed.iter()
.find(|(key, v)| {
key.starts_with("offer.")
&& key.rsplit('.').next() == Some(k)
&& !v.trim().is_empty()
})
.map(|(_, v)| v.trim().to_string())
})
};
let url = get(FEED_FLEET)?;
let instance = get(FEED_FLEET_INSTANCE)?;
Some(FleetConfig {
url,
at: get(FEED_FLEET_AT),
ca_pem: get(FEED_FLEET_CA),
instance,
role: String::new(),
monetize_pubkey: get(FEED_MONETIZE).unwrap_or_default(),
})
}
/// What an enrolment kept, if any.
pub fn load(root: &Path) -> std::io::Result<Option<FleetConfig>> {
match std::fs::read(root.join(CONFIG_REL)) {
Ok(b) => serde_json::from_slice(&b)
.map(Some)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
pub fn save(&self, root: &Path) -> std::io::Result<()> {
let bytes = serde_json::to_vec_pretty(self).map_err(std::io::Error::other)?;
crate::write_atomic(&root.join(CONFIG_REL), &bytes, true)
}
/// The key orders verify under, when it is one.
pub fn key(&self) -> Option<[u8; 32]> {
crate::key32(&self.monetize_pubkey)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_feed_names_the_door_the_estate_and_the_key_plain_or_offered() {
let k = "11".repeat(32);
let feed: BTreeMap<String, String> = [
("offer.front.fleet", "https://monetize.ignalina.dk"),
("offer.front.fleet_instance", "acme"),
("monetize_pubkey", k.as_str()),
]
.iter()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect();
let c = FleetConfig::from_feed(&feed).unwrap();
assert_eq!(
(c.url.as_str(), c.instance.as_str()),
("https://monetize.ignalina.dk", "acme")
);
assert_eq!(c.key(), Some([0x11; 32]));
assert_eq!(FleetConfig::from_feed(&BTreeMap::new()), None);
let dir = tempfile::tempdir().unwrap();
c.save(dir.path()).unwrap();
assert_eq!(FleetConfig::load(dir.path()).unwrap(), Some(c));
}
}