Skip to main content

yeti_types/
node.rs

1//! Canonical node identity.
2//!
3//! A structured
4//! `{provider}-{region}-{node}-{deployment_hash}-{count}` id so multi-deployment
5//! hosting — and replicas of one deployment on one host — don't collide, and so
6//! placement can read a server's region straight off its id.
7//!
8//! `provider`, `node`, and `deployment_hash` are dash-free tokens; `region` may
9//! contain dashes (`us-east-1`); `count` is a non-negative integer. The parser
10//! peels from both ends — `count` and `deployment_hash` off the tail, `provider`
11//! off the head — so the dashes left in the middle all belong to `region`.
12
13use serde::{Deserialize, Serialize};
14
15/// A parsed canonical node id.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct NodeId {
18    /// Cloud provider (e.g. `linode`, `gcp`). Dash-free.
19    pub provider: String,
20    /// Region (e.g. `us-ord`, `us-east-1`). May contain dashes.
21    pub region: String,
22    /// Host identifier within the region. Dash-free.
23    pub node: String,
24    /// The deployment this node serves. Dash-free (hex hash).
25    pub deployment_hash: String,
26    /// Replica ordinal for this deployment on this host (0-based).
27    pub count: u32,
28}
29
30impl NodeId {
31    /// Render the canonical `{provider}-{region}-{node}-{hash}-{count}` form.
32    #[must_use]
33    pub fn to_canonical(&self) -> String {
34        format!(
35            "{}-{}-{}-{}-{}",
36            self.provider, self.region, self.node, self.deployment_hash, self.count
37        )
38    }
39
40    /// Parse a canonical node id. Returns `None` for the bare `"local"` dev
41    /// fallback or any string that isn't the five-field shape.
42    #[must_use]
43    pub fn parse(s: &str) -> Option<Self> {
44        // Peel from the tail: count, then deployment_hash.
45        let (head, count) = s.rsplit_once('-')?;
46        let count: u32 = count.parse().ok()?;
47        let (head, deployment_hash) = head.rsplit_once('-')?;
48        // Peel provider off the head; what remains is `region-node`.
49        let (provider, rest) = head.split_once('-')?;
50        let (region, node) = rest.rsplit_once('-')?;
51        if provider.is_empty() || region.is_empty() || node.is_empty() || deployment_hash.is_empty()
52        {
53            return None;
54        }
55        Some(Self {
56            provider: provider.to_owned(),
57            region: region.to_owned(),
58            node: node.to_owned(),
59            deployment_hash: deployment_hash.to_owned(),
60            count,
61        })
62    }
63
64    /// The region of a node id, or `None` if it isn't canonical (e.g. `"local"`).
65    /// What placement reads to honor region constraints.
66    #[must_use]
67    pub fn region_of(node_id: &str) -> Option<String> {
68        Self::parse(node_id).map(|n| n.region)
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn round_trips_a_simple_id() {
78        let id = NodeId {
79            provider: "linode".to_owned(),
80            region: "ord".to_owned(),
81            node: "n1".to_owned(),
82            deployment_hash: "deadbeef".to_owned(),
83            count: 0,
84        };
85        let s = id.to_canonical();
86        assert_eq!(s, "linode-ord-n1-deadbeef-0");
87        assert_eq!(NodeId::parse(&s), Some(id));
88    }
89
90    #[test]
91    fn region_may_contain_dashes() {
92        let id = NodeId::parse("aws-us-east-1-host7-cafef00d-2").unwrap();
93        assert_eq!(id.provider, "aws");
94        assert_eq!(id.region, "us-east-1");
95        assert_eq!(id.node, "host7");
96        assert_eq!(id.deployment_hash, "cafef00d");
97        assert_eq!(id.count, 2);
98        // Round-trips.
99        assert_eq!(id.to_canonical(), "aws-us-east-1-host7-cafef00d-2");
100    }
101
102    #[test]
103    fn region_of_extracts_just_the_region() {
104        assert_eq!(
105            NodeId::region_of("aws-us-east-1-host7-cafef00d-2").as_deref(),
106            Some("us-east-1")
107        );
108    }
109
110    #[test]
111    fn non_canonical_ids_are_none() {
112        assert_eq!(NodeId::parse("local"), None);
113        assert_eq!(NodeId::parse(""), None);
114        // Missing the count.
115        assert_eq!(NodeId::parse("linode-ord-n1-deadbeef"), None);
116        // Non-numeric count.
117        assert_eq!(NodeId::parse("linode-ord-n1-deadbeef-x"), None);
118        assert_eq!(NodeId::region_of("local"), None);
119    }
120}