use std::collections::HashMap;
use zentinel_common::ids::{QualifiedId, Scope};
use zentinel_common::limits::Limits;
use crate::{AgentConfig, Config, FilterConfig, ListenerConfig, RouteConfig, UpstreamConfig};
#[derive(Debug, Clone)]
pub struct FlattenedConfig {
pub upstreams: HashMap<QualifiedId, UpstreamConfig>,
pub routes: Vec<(QualifiedId, RouteConfig)>,
pub agents: HashMap<QualifiedId, AgentConfig>,
pub filters: HashMap<QualifiedId, FilterConfig>,
pub listeners: Vec<(QualifiedId, ListenerConfig)>,
pub scope_limits: HashMap<Scope, Limits>,
pub exported_upstreams: HashMap<String, QualifiedId>,
pub exported_agents: HashMap<String, QualifiedId>,
pub exported_filters: HashMap<String, QualifiedId>,
}
impl FlattenedConfig {
pub fn new() -> Self {
Self {
upstreams: HashMap::new(),
routes: Vec::new(),
agents: HashMap::new(),
filters: HashMap::new(),
listeners: Vec::new(),
scope_limits: HashMap::new(),
exported_upstreams: HashMap::new(),
exported_agents: HashMap::new(),
exported_filters: HashMap::new(),
}
}
pub fn get_upstream(&self, qid: &QualifiedId) -> Option<&UpstreamConfig> {
self.upstreams.get(qid)
}
pub fn get_upstream_by_canonical(&self, canonical: &str) -> Option<&UpstreamConfig> {
self.upstreams.get(&QualifiedId::parse(canonical))
}
pub fn get_agent(&self, qid: &QualifiedId) -> Option<&AgentConfig> {
self.agents.get(qid)
}
pub fn get_filter(&self, qid: &QualifiedId) -> Option<&FilterConfig> {
self.filters.get(qid)
}
pub fn get_limits(&self, scope: &Scope) -> Option<&Limits> {
self.scope_limits.get(scope)
}
pub fn get_effective_limits(&self, scope: &Scope) -> Option<&Limits> {
for s in scope.chain() {
if let Some(limits) = self.scope_limits.get(&s) {
return Some(limits);
}
}
None
}
pub fn routes_in_scope<'a>(
&'a self,
scope: &'a Scope,
) -> impl Iterator<Item = &'a (QualifiedId, RouteConfig)> {
self.routes
.iter()
.filter(move |(qid, _)| &qid.scope == scope)
}
pub fn listeners_in_scope<'a>(
&'a self,
scope: &'a Scope,
) -> impl Iterator<Item = &'a (QualifiedId, ListenerConfig)> {
self.listeners
.iter()
.filter(move |(qid, _)| &qid.scope == scope)
}
pub fn is_upstream_exported(&self, name: &str) -> bool {
self.exported_upstreams.contains_key(name)
}
pub fn get_exported_upstream_qid(&self, name: &str) -> Option<&QualifiedId> {
self.exported_upstreams.get(name)
}
}
impl Default for FlattenedConfig {
fn default() -> Self {
Self::new()
}
}
impl Config {
pub fn flatten(&self) -> FlattenedConfig {
let mut flat = FlattenedConfig::new();
flat.scope_limits.insert(Scope::Global, self.limits.clone());
self.flatten_global(&mut flat);
for ns in &self.namespaces {
self.flatten_namespace(ns, &mut flat);
}
flat
}
fn flatten_global(&self, flat: &mut FlattenedConfig) {
for (id, upstream) in &self.upstreams {
flat.upstreams
.insert(QualifiedId::global(id), upstream.clone());
}
for route in &self.routes {
flat.routes
.push((QualifiedId::global(&route.id), route.clone()));
}
for agent in &self.agents {
flat.agents
.insert(QualifiedId::global(&agent.id), agent.clone());
}
for (id, filter) in &self.filters {
flat.filters.insert(QualifiedId::global(id), filter.clone());
}
for listener in &self.listeners {
flat.listeners
.push((QualifiedId::global(&listener.id), listener.clone()));
}
}
fn flatten_namespace(&self, ns: &crate::NamespaceConfig, flat: &mut FlattenedConfig) {
let ns_scope = Scope::Namespace(ns.id.clone());
if let Some(ref limits) = ns.limits {
flat.scope_limits.insert(ns_scope.clone(), limits.clone());
}
for (id, upstream) in &ns.upstreams {
let qid = QualifiedId::namespaced(&ns.id, id);
flat.upstreams.insert(qid.clone(), upstream.clone());
if ns.exports.upstreams.contains(id) {
flat.exported_upstreams.insert(id.clone(), qid);
}
}
for route in &ns.routes {
flat.routes
.push((QualifiedId::namespaced(&ns.id, &route.id), route.clone()));
}
for agent in &ns.agents {
let qid = QualifiedId::namespaced(&ns.id, &agent.id);
flat.agents.insert(qid.clone(), agent.clone());
if ns.exports.agents.contains(&agent.id) {
flat.exported_agents.insert(agent.id.clone(), qid);
}
}
for (id, filter) in &ns.filters {
let qid = QualifiedId::namespaced(&ns.id, id);
flat.filters.insert(qid.clone(), filter.clone());
if ns.exports.filters.contains(id) {
flat.exported_filters.insert(id.clone(), qid);
}
}
for listener in &ns.listeners {
flat.listeners.push((
QualifiedId::namespaced(&ns.id, &listener.id),
listener.clone(),
));
}
for svc in &ns.services {
self.flatten_service(&ns.id, svc, flat);
}
}
fn flatten_service(&self, ns_id: &str, svc: &crate::ServiceConfig, flat: &mut FlattenedConfig) {
let svc_scope = Scope::Service {
namespace: ns_id.to_string(),
service: svc.id.clone(),
};
if let Some(ref limits) = svc.limits {
flat.scope_limits.insert(svc_scope.clone(), limits.clone());
}
for (id, upstream) in &svc.upstreams {
flat.upstreams.insert(
QualifiedId::in_service(ns_id, &svc.id, id),
upstream.clone(),
);
}
for route in &svc.routes {
flat.routes.push((
QualifiedId::in_service(ns_id, &svc.id, &route.id),
route.clone(),
));
}
for agent in &svc.agents {
flat.agents.insert(
QualifiedId::in_service(ns_id, &svc.id, &agent.id),
agent.clone(),
);
}
for (id, filter) in &svc.filters {
flat.filters
.insert(QualifiedId::in_service(ns_id, &svc.id, id), filter.clone());
}
if let Some(ref listener) = svc.listener {
flat.listeners.push((
QualifiedId::in_service(ns_id, &svc.id, &listener.id),
listener.clone(),
));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
namespace::{ExportConfig, NamespaceConfig, ServiceConfig},
ConnectionPoolConfig, HttpVersionConfig, UpstreamTarget, UpstreamTimeouts,
};
use zentinel_common::types::LoadBalancingAlgorithm;
fn test_upstream(id: &str) -> UpstreamConfig {
UpstreamConfig {
id: id.to_string(),
targets: vec![UpstreamTarget {
address: "127.0.0.1:8080".to_string(),
weight: 1,
max_requests: None,
metadata: HashMap::new(),
}],
load_balancing: LoadBalancingAlgorithm::RoundRobin,
sticky_session: None,
health_check: None,
connection_pool: ConnectionPoolConfig::default(),
timeouts: UpstreamTimeouts::default(),
tls: None,
http_version: HttpVersionConfig::default(),
}
}
fn test_config() -> Config {
let mut config = Config::default_for_testing();
config.upstreams.insert(
"global-backend".to_string(),
test_upstream("global-backend"),
);
let mut ns = NamespaceConfig::new("api");
ns.upstreams
.insert("ns-backend".to_string(), test_upstream("ns-backend"));
ns.upstreams.insert(
"shared-backend".to_string(),
test_upstream("shared-backend"),
);
ns.exports = ExportConfig {
upstreams: vec!["shared-backend".to_string()],
agents: vec![],
filters: vec![],
};
let mut svc = ServiceConfig::new("payments");
svc.upstreams
.insert("svc-backend".to_string(), test_upstream("svc-backend"));
ns.services.push(svc);
config.namespaces.push(ns);
config
}
#[test]
fn test_flatten_global_upstreams() {
let config = test_config();
let flat = config.flatten();
let qid = QualifiedId::global("global-backend");
assert!(flat.upstreams.contains_key(&qid));
assert_eq!(flat.get_upstream(&qid).unwrap().id, "global-backend");
}
#[test]
fn test_flatten_namespace_upstreams() {
let config = test_config();
let flat = config.flatten();
let qid = QualifiedId::namespaced("api", "ns-backend");
assert!(flat.upstreams.contains_key(&qid));
assert_eq!(flat.get_upstream(&qid).unwrap().id, "ns-backend");
}
#[test]
fn test_flatten_service_upstreams() {
let config = test_config();
let flat = config.flatten();
let qid = QualifiedId::in_service("api", "payments", "svc-backend");
assert!(flat.upstreams.contains_key(&qid));
assert_eq!(flat.get_upstream(&qid).unwrap().id, "svc-backend");
}
#[test]
fn test_flatten_exported_upstreams() {
let config = test_config();
let flat = config.flatten();
assert!(flat.is_upstream_exported("shared-backend"));
assert!(!flat.is_upstream_exported("ns-backend"));
let exported_qid = flat.get_exported_upstream_qid("shared-backend").unwrap();
assert_eq!(exported_qid.canonical(), "api:shared-backend");
}
#[test]
fn test_get_upstream_by_canonical() {
let config = test_config();
let flat = config.flatten();
let upstream = flat.get_upstream_by_canonical("api:ns-backend").unwrap();
assert_eq!(upstream.id, "ns-backend");
let service_upstream = flat
.get_upstream_by_canonical("api:payments:svc-backend")
.unwrap();
assert_eq!(service_upstream.id, "svc-backend");
}
#[test]
fn test_flatten_scope_limits() {
let mut config = test_config();
let ns = config.namespaces.get_mut(0).unwrap();
ns.limits = Some(Limits::for_testing());
let flat = config.flatten();
assert!(flat.scope_limits.contains_key(&Scope::Global));
assert!(flat
.scope_limits
.contains_key(&Scope::Namespace("api".to_string())));
}
#[test]
fn test_get_effective_limits() {
let mut config = test_config();
let ns = config.namespaces.get_mut(0).unwrap();
ns.limits = Some(Limits::for_testing());
let flat = config.flatten();
let svc_scope = Scope::Service {
namespace: "api".to_string(),
service: "payments".to_string(),
};
let limits = flat.get_effective_limits(&svc_scope);
assert!(limits.is_some());
}
#[test]
fn test_routes_in_scope() {
let config = test_config();
let flat = config.flatten();
let global_routes: Vec<_> = flat.routes_in_scope(&Scope::Global).collect();
assert!(!global_routes.is_empty());
}
#[test]
fn test_flatten_preserves_route_order() {
let config = test_config();
let flat = config.flatten();
let route_ids: Vec<_> = flat.routes.iter().map(|(qid, _)| qid.canonical()).collect();
assert!(!route_ids.is_empty());
}
}