1use sentinel_common::ids::{QualifiedId, Scope};
17use sentinel_common::limits::Limits;
18use std::collections::HashMap;
19
20use crate::{
21 AgentConfig, Config, FilterConfig, ListenerConfig, RouteConfig, UpstreamConfig,
22};
23
24#[derive(Debug, Clone)]
33pub struct FlattenedConfig {
34 pub upstreams: HashMap<QualifiedId, UpstreamConfig>,
36
37 pub routes: Vec<(QualifiedId, RouteConfig)>,
39
40 pub agents: HashMap<QualifiedId, AgentConfig>,
42
43 pub filters: HashMap<QualifiedId, FilterConfig>,
45
46 pub listeners: Vec<(QualifiedId, ListenerConfig)>,
48
49 pub scope_limits: HashMap<Scope, Limits>,
51
52 pub exported_upstreams: HashMap<String, QualifiedId>,
54
55 pub exported_agents: HashMap<String, QualifiedId>,
57
58 pub exported_filters: HashMap<String, QualifiedId>,
60}
61
62impl FlattenedConfig {
63 pub fn new() -> Self {
65 Self {
66 upstreams: HashMap::new(),
67 routes: Vec::new(),
68 agents: HashMap::new(),
69 filters: HashMap::new(),
70 listeners: Vec::new(),
71 scope_limits: HashMap::new(),
72 exported_upstreams: HashMap::new(),
73 exported_agents: HashMap::new(),
74 exported_filters: HashMap::new(),
75 }
76 }
77
78 pub fn get_upstream(&self, qid: &QualifiedId) -> Option<&UpstreamConfig> {
80 self.upstreams.get(qid)
81 }
82
83 pub fn get_upstream_by_canonical(&self, canonical: &str) -> Option<&UpstreamConfig> {
85 self.upstreams.get(&QualifiedId::parse(canonical))
86 }
87
88 pub fn get_agent(&self, qid: &QualifiedId) -> Option<&AgentConfig> {
90 self.agents.get(qid)
91 }
92
93 pub fn get_filter(&self, qid: &QualifiedId) -> Option<&FilterConfig> {
95 self.filters.get(qid)
96 }
97
98 pub fn get_limits(&self, scope: &Scope) -> Option<&Limits> {
103 self.scope_limits.get(scope)
104 }
105
106 pub fn get_effective_limits(&self, scope: &Scope) -> Option<&Limits> {
110 for s in scope.chain() {
111 if let Some(limits) = self.scope_limits.get(&s) {
112 return Some(limits);
113 }
114 }
115 None
116 }
117
118 pub fn routes_in_scope<'a>(&'a self, scope: &'a Scope) -> impl Iterator<Item = &'a (QualifiedId, RouteConfig)> {
120 self.routes.iter().filter(move |(qid, _)| &qid.scope == scope)
121 }
122
123 pub fn listeners_in_scope<'a>(&'a self, scope: &'a Scope) -> impl Iterator<Item = &'a (QualifiedId, ListenerConfig)> {
125 self.listeners.iter().filter(move |(qid, _)| &qid.scope == scope)
126 }
127
128 pub fn is_upstream_exported(&self, name: &str) -> bool {
130 self.exported_upstreams.contains_key(name)
131 }
132
133 pub fn get_exported_upstream_qid(&self, name: &str) -> Option<&QualifiedId> {
135 self.exported_upstreams.get(name)
136 }
137}
138
139impl Default for FlattenedConfig {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145impl Config {
150 pub fn flatten(&self) -> FlattenedConfig {
155 let mut flat = FlattenedConfig::new();
156
157 flat.scope_limits.insert(Scope::Global, self.limits.clone());
159
160 self.flatten_global(&mut flat);
162
163 for ns in &self.namespaces {
165 self.flatten_namespace(ns, &mut flat);
166 }
167
168 flat
169 }
170
171 fn flatten_global(&self, flat: &mut FlattenedConfig) {
172 for (id, upstream) in &self.upstreams {
174 flat.upstreams.insert(QualifiedId::global(id), upstream.clone());
175 }
176
177 for route in &self.routes {
179 flat.routes.push((QualifiedId::global(&route.id), route.clone()));
180 }
181
182 for agent in &self.agents {
184 flat.agents.insert(QualifiedId::global(&agent.id), agent.clone());
185 }
186
187 for (id, filter) in &self.filters {
189 flat.filters.insert(QualifiedId::global(id), filter.clone());
190 }
191
192 for listener in &self.listeners {
194 flat.listeners.push((QualifiedId::global(&listener.id), listener.clone()));
195 }
196 }
197
198 fn flatten_namespace(&self, ns: &crate::NamespaceConfig, flat: &mut FlattenedConfig) {
199 let ns_scope = Scope::Namespace(ns.id.clone());
200
201 if let Some(ref limits) = ns.limits {
203 flat.scope_limits.insert(ns_scope.clone(), limits.clone());
204 }
205
206 for (id, upstream) in &ns.upstreams {
208 let qid = QualifiedId::namespaced(&ns.id, id);
209 flat.upstreams.insert(qid.clone(), upstream.clone());
210
211 if ns.exports.upstreams.contains(id) {
213 flat.exported_upstreams.insert(id.clone(), qid);
214 }
215 }
216
217 for route in &ns.routes {
219 flat.routes.push((
220 QualifiedId::namespaced(&ns.id, &route.id),
221 route.clone(),
222 ));
223 }
224
225 for agent in &ns.agents {
227 let qid = QualifiedId::namespaced(&ns.id, &agent.id);
228 flat.agents.insert(qid.clone(), agent.clone());
229
230 if ns.exports.agents.contains(&agent.id) {
232 flat.exported_agents.insert(agent.id.clone(), qid);
233 }
234 }
235
236 for (id, filter) in &ns.filters {
238 let qid = QualifiedId::namespaced(&ns.id, id);
239 flat.filters.insert(qid.clone(), filter.clone());
240
241 if ns.exports.filters.contains(id) {
243 flat.exported_filters.insert(id.clone(), qid);
244 }
245 }
246
247 for listener in &ns.listeners {
249 flat.listeners.push((
250 QualifiedId::namespaced(&ns.id, &listener.id),
251 listener.clone(),
252 ));
253 }
254
255 for svc in &ns.services {
257 self.flatten_service(&ns.id, svc, flat);
258 }
259 }
260
261 fn flatten_service(
262 &self,
263 ns_id: &str,
264 svc: &crate::ServiceConfig,
265 flat: &mut FlattenedConfig,
266 ) {
267 let svc_scope = Scope::Service {
268 namespace: ns_id.to_string(),
269 service: svc.id.clone(),
270 };
271
272 if let Some(ref limits) = svc.limits {
274 flat.scope_limits.insert(svc_scope.clone(), limits.clone());
275 }
276
277 for (id, upstream) in &svc.upstreams {
279 flat.upstreams.insert(
280 QualifiedId::in_service(ns_id, &svc.id, id),
281 upstream.clone(),
282 );
283 }
284
285 for route in &svc.routes {
287 flat.routes.push((
288 QualifiedId::in_service(ns_id, &svc.id, &route.id),
289 route.clone(),
290 ));
291 }
292
293 for agent in &svc.agents {
295 flat.agents.insert(
296 QualifiedId::in_service(ns_id, &svc.id, &agent.id),
297 agent.clone(),
298 );
299 }
300
301 for (id, filter) in &svc.filters {
303 flat.filters.insert(
304 QualifiedId::in_service(ns_id, &svc.id, id),
305 filter.clone(),
306 );
307 }
308
309 if let Some(ref listener) = svc.listener {
311 flat.listeners.push((
312 QualifiedId::in_service(ns_id, &svc.id, &listener.id),
313 listener.clone(),
314 ));
315 }
316 }
317}
318
319#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::{
327 namespace::{ExportConfig, NamespaceConfig, ServiceConfig},
328 ConnectionPoolConfig, HttpVersionConfig, UpstreamTarget, UpstreamTimeouts,
329 };
330 use sentinel_common::types::LoadBalancingAlgorithm;
331
332 fn test_upstream(id: &str) -> UpstreamConfig {
333 UpstreamConfig {
334 id: id.to_string(),
335 targets: vec![UpstreamTarget {
336 address: "127.0.0.1:8080".to_string(),
337 weight: 1,
338 max_requests: None,
339 metadata: HashMap::new(),
340 }],
341 load_balancing: LoadBalancingAlgorithm::RoundRobin,
342 health_check: None,
343 connection_pool: ConnectionPoolConfig::default(),
344 timeouts: UpstreamTimeouts::default(),
345 tls: None,
346 http_version: HttpVersionConfig::default(),
347 }
348 }
349
350 fn test_config() -> Config {
351 let mut config = Config::default_for_testing();
352
353 config.upstreams.insert("global-backend".to_string(), test_upstream("global-backend"));
355
356 let mut ns = NamespaceConfig::new("api");
358 ns.upstreams.insert("ns-backend".to_string(), test_upstream("ns-backend"));
359 ns.upstreams.insert("shared-backend".to_string(), test_upstream("shared-backend"));
360 ns.exports = ExportConfig {
361 upstreams: vec!["shared-backend".to_string()],
362 agents: vec![],
363 filters: vec![],
364 };
365
366 let mut svc = ServiceConfig::new("payments");
368 svc.upstreams.insert("svc-backend".to_string(), test_upstream("svc-backend"));
369 ns.services.push(svc);
370
371 config.namespaces.push(ns);
372 config
373 }
374
375 #[test]
376 fn test_flatten_global_upstreams() {
377 let config = test_config();
378 let flat = config.flatten();
379
380 let qid = QualifiedId::global("global-backend");
382 assert!(flat.upstreams.contains_key(&qid));
383 assert_eq!(flat.get_upstream(&qid).unwrap().id, "global-backend");
384 }
385
386 #[test]
387 fn test_flatten_namespace_upstreams() {
388 let config = test_config();
389 let flat = config.flatten();
390
391 let qid = QualifiedId::namespaced("api", "ns-backend");
393 assert!(flat.upstreams.contains_key(&qid));
394 assert_eq!(flat.get_upstream(&qid).unwrap().id, "ns-backend");
395 }
396
397 #[test]
398 fn test_flatten_service_upstreams() {
399 let config = test_config();
400 let flat = config.flatten();
401
402 let qid = QualifiedId::in_service("api", "payments", "svc-backend");
404 assert!(flat.upstreams.contains_key(&qid));
405 assert_eq!(flat.get_upstream(&qid).unwrap().id, "svc-backend");
406 }
407
408 #[test]
409 fn test_flatten_exported_upstreams() {
410 let config = test_config();
411 let flat = config.flatten();
412
413 assert!(flat.is_upstream_exported("shared-backend"));
415 assert!(!flat.is_upstream_exported("ns-backend"));
416
417 let exported_qid = flat.get_exported_upstream_qid("shared-backend").unwrap();
418 assert_eq!(exported_qid.canonical(), "api:shared-backend");
419 }
420
421 #[test]
422 fn test_get_upstream_by_canonical() {
423 let config = test_config();
424 let flat = config.flatten();
425
426 let upstream = flat.get_upstream_by_canonical("api:ns-backend").unwrap();
428 assert_eq!(upstream.id, "ns-backend");
429
430 let service_upstream = flat.get_upstream_by_canonical("api:payments:svc-backend").unwrap();
431 assert_eq!(service_upstream.id, "svc-backend");
432 }
433
434 #[test]
435 fn test_flatten_scope_limits() {
436 let mut config = test_config();
437
438 let ns = config.namespaces.get_mut(0).unwrap();
440 ns.limits = Some(Limits::for_testing());
441
442 let flat = config.flatten();
443
444 assert!(flat.scope_limits.contains_key(&Scope::Global));
446
447 assert!(flat.scope_limits.contains_key(&Scope::Namespace("api".to_string())));
449 }
450
451 #[test]
452 fn test_get_effective_limits() {
453 let mut config = test_config();
454
455 let ns = config.namespaces.get_mut(0).unwrap();
457 ns.limits = Some(Limits::for_testing());
458
459 let flat = config.flatten();
460
461 let svc_scope = Scope::Service {
463 namespace: "api".to_string(),
464 service: "payments".to_string(),
465 };
466 let limits = flat.get_effective_limits(&svc_scope);
467 assert!(limits.is_some());
468 }
469
470 #[test]
471 fn test_routes_in_scope() {
472 let config = test_config();
473 let flat = config.flatten();
474
475 let global_routes: Vec<_> = flat.routes_in_scope(&Scope::Global).collect();
477 assert!(!global_routes.is_empty());
478 }
479
480 #[test]
481 fn test_flatten_preserves_route_order() {
482 let config = test_config();
483 let flat = config.flatten();
484
485 let route_ids: Vec<_> = flat.routes.iter().map(|(qid, _)| qid.canonical()).collect();
487 assert!(!route_ids.is_empty());
488 }
489}