Skip to main content

zlayer_types/
overlay.rs

1//! Overlay-network configuration types shared across `ZLayer` crates.
2//!
3//! Mode status:
4//! - [`OverlayMode::Shared`] is fully implemented (single cluster `WireGuard`
5//!   interface carrying multiple service subnets via multi-CIDR `AllowedIPs`,
6//!   per-node Linux bridges per service).
7//! - [`OverlayMode::Dedicated`] is implemented: each service gets its own
8//!   per-service `WireGuard` transport with an isolated crypto context.
9//! - [`OverlayMode::Auto`] is the default. It resolves to `Shared` — there is
10//!   no telemetry heuristic yet (bandwidth budgets, NIC capabilities, per-node
11//!   interface caps) to pick `Dedicated` automatically. A future round will
12//!   wire a real heuristic.
13//!
14//! Every consumer of `OverlayMode` MUST go through [`OverlayMode::resolve`]
15//! before acting on the value, so the resolution surface is uniform.
16
17use serde::{Deserialize, Serialize};
18
19/// How the daemon places a service's overlay attachment.
20///
21/// See module docs for the implementation status of each variant.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, utoipa::ToSchema)]
23#[serde(rename_all = "lowercase")]
24pub enum OverlayMode {
25    /// Daemon picks. Resolves to [`OverlayMode::Shared`] — there is no
26    /// telemetry heuristic yet to pick [`OverlayMode::Dedicated`] on its own.
27    #[default]
28    Auto,
29    /// Single cluster `WireGuard` interface carries every service subnet via
30    /// multi-CIDR `AllowedIPs`; each service has a per-node Linux bridge
31    /// for container attachment. Lowest interface count; one shared crypto
32    /// context (bandwidth ceiling shared across all service traffic).
33    Shared,
34    /// Per-service `WireGuard` transport with its own isolated crypto context,
35    /// for services that need their own bandwidth ceiling. Implemented.
36    Dedicated,
37}
38
39impl OverlayMode {
40    /// Resolve to the actually-implemented mode. `Auto` resolves to `Shared`
41    /// (no telemetry heuristic yet to choose `Dedicated` automatically);
42    /// `Shared` and `Dedicated` resolve to themselves. Every code path that
43    /// consumes an `OverlayMode` value MUST go through this funnel so the
44    /// resolution surface is uniform.
45    #[must_use]
46    pub fn resolve(self) -> OverlayMode {
47        match self {
48            OverlayMode::Auto | OverlayMode::Shared => OverlayMode::Shared,
49            OverlayMode::Dedicated => OverlayMode::Dedicated,
50        }
51    }
52}
53
54/// Per-service overlay configuration, populated from the service spec.
55///
56/// `parent` names another overlay this one should nest under. In v0.51 only
57/// `None` / `Some("cluster")` is honored; any other value triggers a warn-
58/// and-fallback (treated as `None`). Future rounds may allow service-of-
59/// service nesting.
60#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
61pub struct OverlayConfig {
62    #[serde(default)]
63    pub mode: OverlayMode,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub parent: Option<String>,
66}
67
68impl OverlayConfig {
69    /// Returns the resolved parent name for v0.51: `None` if the parent is
70    /// `None` or `Some("cluster")`; logs a warning and falls back to `None`
71    /// for any other value.
72    #[must_use]
73    pub fn resolved_parent_v0_51(&self) -> Option<&str> {
74        match self.parent.as_deref() {
75            None | Some("cluster") => None,
76            Some(other) => {
77                tracing::warn!(
78                    parent = other,
79                    "OverlayConfig.parent only supports `cluster` (or unset) in v0.51; \
80                     service-of-service nesting is reserved for a future round. \
81                     Falling back to cluster parent.",
82                );
83                None
84            }
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn overlay_mode_default_is_auto() {
95        assert_eq!(OverlayMode::default(), OverlayMode::Auto);
96    }
97
98    #[test]
99    fn overlay_mode_resolve() {
100        assert_eq!(OverlayMode::Auto.resolve(), OverlayMode::Shared);
101        assert_eq!(OverlayMode::Shared.resolve(), OverlayMode::Shared);
102        assert_eq!(OverlayMode::Dedicated.resolve(), OverlayMode::Dedicated);
103    }
104
105    #[test]
106    fn overlay_mode_serde_lowercase() {
107        assert_eq!(
108            serde_json::to_string(&OverlayMode::Auto).unwrap(),
109            "\"auto\""
110        );
111        assert_eq!(
112            serde_json::to_string(&OverlayMode::Shared).unwrap(),
113            "\"shared\""
114        );
115        assert_eq!(
116            serde_json::to_string(&OverlayMode::Dedicated).unwrap(),
117            "\"dedicated\""
118        );
119        assert_eq!(
120            serde_json::from_str::<OverlayMode>("\"shared\"").unwrap(),
121            OverlayMode::Shared,
122        );
123    }
124
125    #[test]
126    fn overlay_config_default_is_auto_no_parent() {
127        let cfg = OverlayConfig::default();
128        assert_eq!(cfg.mode, OverlayMode::Auto);
129        assert_eq!(cfg.parent, None);
130        assert_eq!(cfg.resolved_parent_v0_51(), None);
131    }
132
133    #[test]
134    fn overlay_config_cluster_parent_is_none() {
135        let cfg = OverlayConfig {
136            mode: OverlayMode::Shared,
137            parent: Some("cluster".to_string()),
138        };
139        assert_eq!(cfg.resolved_parent_v0_51(), None);
140    }
141
142    #[test]
143    fn overlay_config_other_parent_warns_and_returns_none() {
144        let cfg = OverlayConfig {
145            mode: OverlayMode::Shared,
146            parent: Some("svc-other".to_string()),
147        };
148        assert_eq!(cfg.resolved_parent_v0_51(), None);
149    }
150}