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}