use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum OverlayMode {
#[default]
Auto,
Shared,
Dedicated,
Isolated,
}
impl OverlayMode {
#[must_use]
pub fn resolve(self) -> OverlayMode {
self
}
#[must_use]
pub fn uses_shared_bridge(self) -> bool {
matches!(self, OverlayMode::Shared)
}
#[must_use]
pub fn uses_per_service_wg(self) -> bool {
matches!(self, OverlayMode::Dedicated)
}
#[must_use]
pub fn uses_isolation_scope(self) -> bool {
matches!(self, OverlayMode::Isolated)
}
}
pub const ISOLATION_NETWORK_LABEL: &str = "com.zlayer.isolation_network";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayConfig {
#[serde(default)]
pub mode: OverlayMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
}
impl OverlayConfig {
#[must_use]
pub fn resolved_parent_v0_51(&self) -> Option<&str> {
match self.parent.as_deref() {
None | Some("cluster") => None,
Some(other) => {
tracing::warn!(
parent = other,
"OverlayConfig.parent only supports `cluster` (or unset) in v0.51; \
service-of-service nesting is reserved for a future round. \
Falling back to cluster parent.",
);
None
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EgressPolicy {
None,
Full,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NetworkIsolation {
pub scope: Option<String>,
pub fenced: bool,
pub allow_host: bool,
pub egress: EgressPolicy,
}
#[must_use]
pub fn resolve_network_isolation(
mode: OverlayMode,
network_mode_is_none: bool,
network_mode_is_host: bool,
service: &str,
explicit_isolation_label: Option<&str>,
) -> NetworkIsolation {
if network_mode_is_none {
return NetworkIsolation {
scope: None,
fenced: true,
allow_host: false,
egress: EgressPolicy::None,
};
}
if network_mode_is_host {
return NetworkIsolation {
scope: None,
fenced: false,
allow_host: true,
egress: EgressPolicy::Full,
};
}
let fenced = mode.uses_isolation_scope() || mode.uses_per_service_wg();
let scope = if fenced {
Some(explicit_isolation_label.map_or_else(|| service.to_string(), str::to_string))
} else {
explicit_isolation_label.map(str::to_string)
};
NetworkIsolation {
scope,
fenced,
allow_host: true,
egress: EgressPolicy::Full,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overlay_mode_default_is_auto() {
assert_eq!(OverlayMode::default(), OverlayMode::Auto);
}
#[test]
fn isolation_network_label_is_canonical() {
assert_eq!(ISOLATION_NETWORK_LABEL, "com.zlayer.isolation_network");
}
#[test]
fn overlay_mode_resolve() {
assert_eq!(OverlayMode::Auto.resolve(), OverlayMode::Auto);
assert_eq!(OverlayMode::Shared.resolve(), OverlayMode::Shared);
assert_eq!(OverlayMode::Dedicated.resolve(), OverlayMode::Dedicated);
assert_eq!(OverlayMode::Isolated.resolve(), OverlayMode::Isolated);
}
#[test]
fn overlay_mode_predicates() {
assert_eq!(
(
OverlayMode::Auto.uses_shared_bridge(),
OverlayMode::Auto.uses_per_service_wg()
),
(false, false)
);
assert_eq!(
(
OverlayMode::Dedicated.uses_shared_bridge(),
OverlayMode::Dedicated.uses_per_service_wg()
),
(false, true)
);
assert_eq!(
(
OverlayMode::Shared.uses_shared_bridge(),
OverlayMode::Shared.uses_per_service_wg()
),
(true, false)
);
}
#[test]
fn overlay_mode_isolated_predicates() {
assert!(OverlayMode::Isolated.uses_isolation_scope());
assert!(!OverlayMode::Isolated.uses_shared_bridge());
assert!(!OverlayMode::Isolated.uses_per_service_wg());
assert!(!OverlayMode::Auto.uses_isolation_scope());
assert!(!OverlayMode::Shared.uses_isolation_scope());
assert!(!OverlayMode::Dedicated.uses_isolation_scope());
}
#[test]
fn overlay_mode_serde_lowercase() {
assert_eq!(
serde_json::to_string(&OverlayMode::Auto).unwrap(),
"\"auto\""
);
assert_eq!(
serde_json::to_string(&OverlayMode::Shared).unwrap(),
"\"shared\""
);
assert_eq!(
serde_json::to_string(&OverlayMode::Dedicated).unwrap(),
"\"dedicated\""
);
assert_eq!(
serde_json::to_string(&OverlayMode::Isolated).unwrap(),
"\"isolated\""
);
assert_eq!(
serde_json::from_str::<OverlayMode>("\"shared\"").unwrap(),
OverlayMode::Shared,
);
assert_eq!(
serde_json::from_str::<OverlayMode>("\"isolated\"").unwrap(),
OverlayMode::Isolated,
);
}
#[test]
fn overlay_config_default_is_auto_no_parent() {
let cfg = OverlayConfig::default();
assert_eq!(cfg.mode, OverlayMode::Auto);
assert_eq!(cfg.parent, None);
assert_eq!(cfg.resolved_parent_v0_51(), None);
}
#[test]
fn overlay_config_cluster_parent_is_none() {
let cfg = OverlayConfig {
mode: OverlayMode::Shared,
parent: Some("cluster".to_string()),
};
assert_eq!(cfg.resolved_parent_v0_51(), None);
}
#[test]
fn overlay_config_other_parent_warns_and_returns_none() {
let cfg = OverlayConfig {
mode: OverlayMode::Shared,
parent: Some("svc-other".to_string()),
};
assert_eq!(cfg.resolved_parent_v0_51(), None);
}
#[test]
fn resolve_isolation_auto_is_unfenced_full_and_host() {
let iso = resolve_network_isolation(OverlayMode::Auto, false, false, "svc", None);
assert_eq!(
iso,
NetworkIsolation {
scope: None,
fenced: false,
allow_host: true,
egress: EgressPolicy::Full,
}
);
}
#[test]
fn resolve_isolation_shared_is_unfenced() {
let iso = resolve_network_isolation(OverlayMode::Shared, false, false, "svc", None);
assert!(!iso.fenced);
assert_eq!(iso.scope, None);
assert_eq!(iso.egress, EgressPolicy::Full);
assert!(iso.allow_host);
}
#[test]
fn resolve_isolation_isolated_is_fenced_to_service() {
let iso = resolve_network_isolation(OverlayMode::Isolated, false, false, "svc", None);
assert!(iso.fenced);
assert_eq!(iso.scope.as_deref(), Some("svc"));
assert!(iso.allow_host);
assert_eq!(iso.egress, EgressPolicy::Full);
}
#[test]
fn resolve_isolation_isolated_explicit_label_overrides_service() {
let iso =
resolve_network_isolation(OverlayMode::Isolated, false, false, "svc", Some("netA"));
assert!(iso.fenced);
assert_eq!(iso.scope.as_deref(), Some("netA"));
}
#[test]
fn resolve_isolation_dedicated_is_fenced() {
let iso = resolve_network_isolation(OverlayMode::Dedicated, false, false, "svc", None);
assert!(iso.fenced);
assert_eq!(iso.scope.as_deref(), Some("svc"));
assert_eq!(iso.egress, EgressPolicy::Full);
}
#[test]
fn resolve_isolation_unfenced_label_still_sets_scope() {
let iso = resolve_network_isolation(OverlayMode::Auto, false, false, "svc", Some("netA"));
assert!(!iso.fenced);
assert_eq!(iso.scope.as_deref(), Some("netA"));
}
#[test]
fn resolve_isolation_network_none_has_no_net() {
let iso = resolve_network_isolation(OverlayMode::Auto, true, false, "svc", Some("netA"));
assert_eq!(
iso,
NetworkIsolation {
scope: None,
fenced: true,
allow_host: false,
egress: EgressPolicy::None,
}
);
}
#[test]
fn resolve_isolation_network_host_is_unfenced_full() {
let iso =
resolve_network_isolation(OverlayMode::Isolated, false, true, "svc", Some("netA"));
assert_eq!(
iso,
NetworkIsolation {
scope: None,
fenced: false,
allow_host: true,
egress: EgressPolicy::Full,
}
);
}
}