1use zentinel_common::ids::{QualifiedId, Scope};
19
20use crate::{AgentConfig, Config, FilterConfig, NamespaceConfig, ServiceConfig, UpstreamConfig};
21
22pub struct ResourceResolver<'a> {
48 config: &'a Config,
49}
50
51impl<'a> ResourceResolver<'a> {
52 pub fn new(config: &'a Config) -> Self {
54 Self { config }
55 }
56
57 pub fn config(&self) -> &'a Config {
59 self.config
60 }
61
62 pub fn resolve_upstream(
76 &self,
77 reference: &str,
78 from_scope: &Scope,
79 ) -> Option<&'a UpstreamConfig> {
80 if reference.contains(':') {
82 return self.resolve_upstream_qualified(&QualifiedId::parse(reference));
83 }
84
85 match from_scope {
87 Scope::Service { namespace, service } => {
88 if let Some(upstream) = self.find_service_upstream(namespace, service, reference) {
90 return Some(upstream);
91 }
92 if let Some(upstream) = self.find_namespace_upstream(namespace, reference) {
94 return Some(upstream);
95 }
96 if let Some(upstream) = self.find_exported_upstream(reference) {
98 return Some(upstream);
99 }
100 self.config.upstreams.get(reference)
102 }
103 Scope::Namespace(namespace) => {
104 if let Some(upstream) = self.find_namespace_upstream(namespace, reference) {
106 return Some(upstream);
107 }
108 if let Some(upstream) = self.find_exported_upstream(reference) {
110 return Some(upstream);
111 }
112 self.config.upstreams.get(reference)
114 }
115 Scope::Global => {
116 if let Some(upstream) = self.config.upstreams.get(reference) {
118 return Some(upstream);
119 }
120 self.find_exported_upstream(reference)
122 }
123 }
124 }
125
126 fn resolve_upstream_qualified(&self, qid: &QualifiedId) -> Option<&'a UpstreamConfig> {
128 match &qid.scope {
129 Scope::Global => self.config.upstreams.get(&qid.name),
130 Scope::Namespace(ns) => self.find_namespace_upstream(ns, &qid.name),
131 Scope::Service { namespace, service } => {
132 self.find_service_upstream(namespace, service, &qid.name)
133 }
134 }
135 }
136
137 fn find_namespace_upstream(&self, ns_id: &str, name: &str) -> Option<&'a UpstreamConfig> {
138 self.config
139 .namespaces
140 .iter()
141 .find(|ns| ns.id == ns_id)
142 .and_then(|ns| ns.upstreams.get(name))
143 }
144
145 fn find_service_upstream(
146 &self,
147 ns_id: &str,
148 svc_id: &str,
149 name: &str,
150 ) -> Option<&'a UpstreamConfig> {
151 self.config
152 .namespaces
153 .iter()
154 .find(|ns| ns.id == ns_id)
155 .and_then(|ns| ns.services.iter().find(|s| s.id == svc_id))
156 .and_then(|svc| svc.upstreams.get(name))
157 }
158
159 fn find_exported_upstream(&self, name: &str) -> Option<&'a UpstreamConfig> {
160 for ns in &self.config.namespaces {
161 if ns.exports.upstreams.contains(&name.to_string()) {
162 if let Some(upstream) = ns.upstreams.get(name) {
163 return Some(upstream);
164 }
165 }
166 }
167 None
168 }
169
170 pub fn resolve_agent(&self, reference: &str, from_scope: &Scope) -> Option<&'a AgentConfig> {
176 if reference.contains(':') {
178 return self.resolve_agent_qualified(&QualifiedId::parse(reference));
179 }
180
181 match from_scope {
183 Scope::Service { namespace, service } => {
184 if let Some(agent) = self.find_service_agent(namespace, service, reference) {
186 return Some(agent);
187 }
188 if let Some(agent) = self.find_namespace_agent(namespace, reference) {
190 return Some(agent);
191 }
192 if let Some(agent) = self.find_exported_agent(reference) {
194 return Some(agent);
195 }
196 self.config.agents.iter().find(|a| a.id == reference)
198 }
199 Scope::Namespace(namespace) => {
200 if let Some(agent) = self.find_namespace_agent(namespace, reference) {
202 return Some(agent);
203 }
204 if let Some(agent) = self.find_exported_agent(reference) {
206 return Some(agent);
207 }
208 self.config.agents.iter().find(|a| a.id == reference)
210 }
211 Scope::Global => {
212 if let Some(agent) = self.config.agents.iter().find(|a| a.id == reference) {
214 return Some(agent);
215 }
216 self.find_exported_agent(reference)
218 }
219 }
220 }
221
222 fn resolve_agent_qualified(&self, qid: &QualifiedId) -> Option<&'a AgentConfig> {
223 match &qid.scope {
224 Scope::Global => self.config.agents.iter().find(|a| a.id == qid.name),
225 Scope::Namespace(ns) => self.find_namespace_agent(ns, &qid.name),
226 Scope::Service { namespace, service } => {
227 self.find_service_agent(namespace, service, &qid.name)
228 }
229 }
230 }
231
232 fn find_namespace_agent(&self, ns_id: &str, name: &str) -> Option<&'a AgentConfig> {
233 self.config
234 .namespaces
235 .iter()
236 .find(|ns| ns.id == ns_id)
237 .and_then(|ns| ns.agents.iter().find(|a| a.id == name))
238 }
239
240 fn find_service_agent(&self, ns_id: &str, svc_id: &str, name: &str) -> Option<&'a AgentConfig> {
241 self.config
242 .namespaces
243 .iter()
244 .find(|ns| ns.id == ns_id)
245 .and_then(|ns| ns.services.iter().find(|s| s.id == svc_id))
246 .and_then(|svc| svc.agents.iter().find(|a| a.id == name))
247 }
248
249 fn find_exported_agent(&self, name: &str) -> Option<&'a AgentConfig> {
250 for ns in &self.config.namespaces {
251 if ns.exports.agents.contains(&name.to_string()) {
252 if let Some(agent) = ns.agents.iter().find(|a| a.id == name) {
253 return Some(agent);
254 }
255 }
256 }
257 None
258 }
259
260 pub fn resolve_filter(&self, reference: &str, from_scope: &Scope) -> Option<&'a FilterConfig> {
266 if reference.contains(':') {
268 return self.resolve_filter_qualified(&QualifiedId::parse(reference));
269 }
270
271 match from_scope {
273 Scope::Service { namespace, service } => {
274 if let Some(filter) = self.find_service_filter(namespace, service, reference) {
276 return Some(filter);
277 }
278 if let Some(filter) = self.find_namespace_filter(namespace, reference) {
280 return Some(filter);
281 }
282 if let Some(filter) = self.find_exported_filter(reference) {
284 return Some(filter);
285 }
286 self.config.filters.get(reference)
288 }
289 Scope::Namespace(namespace) => {
290 if let Some(filter) = self.find_namespace_filter(namespace, reference) {
292 return Some(filter);
293 }
294 if let Some(filter) = self.find_exported_filter(reference) {
296 return Some(filter);
297 }
298 self.config.filters.get(reference)
300 }
301 Scope::Global => {
302 if let Some(filter) = self.config.filters.get(reference) {
304 return Some(filter);
305 }
306 self.find_exported_filter(reference)
308 }
309 }
310 }
311
312 fn resolve_filter_qualified(&self, qid: &QualifiedId) -> Option<&'a FilterConfig> {
313 match &qid.scope {
314 Scope::Global => self.config.filters.get(&qid.name),
315 Scope::Namespace(ns) => self.find_namespace_filter(ns, &qid.name),
316 Scope::Service { namespace, service } => {
317 self.find_service_filter(namespace, service, &qid.name)
318 }
319 }
320 }
321
322 fn find_namespace_filter(&self, ns_id: &str, name: &str) -> Option<&'a FilterConfig> {
323 self.config
324 .namespaces
325 .iter()
326 .find(|ns| ns.id == ns_id)
327 .and_then(|ns| ns.filters.get(name))
328 }
329
330 fn find_service_filter(
331 &self,
332 ns_id: &str,
333 svc_id: &str,
334 name: &str,
335 ) -> Option<&'a FilterConfig> {
336 self.config
337 .namespaces
338 .iter()
339 .find(|ns| ns.id == ns_id)
340 .and_then(|ns| ns.services.iter().find(|s| s.id == svc_id))
341 .and_then(|svc| svc.filters.get(name))
342 }
343
344 fn find_exported_filter(&self, name: &str) -> Option<&'a FilterConfig> {
345 for ns in &self.config.namespaces {
346 if ns.exports.filters.contains(&name.to_string()) {
347 if let Some(filter) = ns.filters.get(name) {
348 return Some(filter);
349 }
350 }
351 }
352 None
353 }
354
355 pub fn get_namespace(&self, id: &str) -> Option<&'a NamespaceConfig> {
361 self.config.namespaces.iter().find(|ns| ns.id == id)
362 }
363
364 pub fn get_service(&self, namespace: &str, service: &str) -> Option<&'a ServiceConfig> {
366 self.get_namespace(namespace)
367 .and_then(|ns| ns.services.iter().find(|s| s.id == service))
368 }
369
370 pub fn can_resolve_upstream(&self, reference: &str, from_scope: &Scope) -> bool {
372 self.resolve_upstream(reference, from_scope).is_some()
373 }
374
375 pub fn can_resolve_agent(&self, reference: &str, from_scope: &Scope) -> bool {
377 self.resolve_agent(reference, from_scope).is_some()
378 }
379
380 pub fn can_resolve_filter(&self, reference: &str, from_scope: &Scope) -> bool {
382 self.resolve_filter(reference, from_scope).is_some()
383 }
384}
385
386#[cfg(test)]
391mod tests {
392 use super::*;
393 use crate::{
394 namespace::{ExportConfig, NamespaceConfig, ServiceConfig},
395 ConnectionPoolConfig, HttpVersionConfig, UpstreamTarget, UpstreamTimeouts,
396 };
397 use std::collections::HashMap;
398 use zentinel_common::types::LoadBalancingAlgorithm;
399
400 fn test_upstream(id: &str) -> UpstreamConfig {
401 UpstreamConfig {
402 id: id.to_string(),
403 targets: vec![UpstreamTarget {
404 address: "127.0.0.1:8080".to_string(),
405 weight: 1,
406 max_requests: None,
407 metadata: HashMap::new(),
408 }],
409 load_balancing: LoadBalancingAlgorithm::RoundRobin,
410 sticky_session: None,
411 health_check: None,
412 connection_pool: ConnectionPoolConfig::default(),
413 timeouts: UpstreamTimeouts::default(),
414 tls: None,
415 http_version: HttpVersionConfig::default(),
416 }
417 }
418
419 fn test_config() -> Config {
420 let mut config = Config::default_for_testing();
421
422 config.upstreams.insert(
424 "global-backend".to_string(),
425 test_upstream("global-backend"),
426 );
427
428 let mut ns = NamespaceConfig::new("api");
430 ns.upstreams
431 .insert("ns-backend".to_string(), test_upstream("ns-backend"));
432 ns.upstreams.insert(
433 "shared-backend".to_string(),
434 test_upstream("shared-backend"),
435 );
436 ns.exports = ExportConfig {
437 upstreams: vec!["shared-backend".to_string()],
438 agents: vec![],
439 filters: vec![],
440 };
441
442 let mut svc = ServiceConfig::new("payments");
444 svc.upstreams
445 .insert("svc-backend".to_string(), test_upstream("svc-backend"));
446 ns.services.push(svc);
447
448 config.namespaces.push(ns);
449 config
450 }
451
452 #[test]
453 fn test_resolve_global_upstream_from_global() {
454 let config = test_config();
455 let resolver = ResourceResolver::new(&config);
456
457 let result = resolver.resolve_upstream("global-backend", &Scope::Global);
458 assert!(result.is_some());
459 assert_eq!(result.unwrap().id, "global-backend");
460 }
461
462 #[test]
463 fn test_resolve_global_upstream_from_namespace() {
464 let config = test_config();
465 let resolver = ResourceResolver::new(&config);
466
467 let scope = Scope::Namespace("api".to_string());
468 let result = resolver.resolve_upstream("global-backend", &scope);
469 assert!(result.is_some());
470 assert_eq!(result.unwrap().id, "global-backend");
471 }
472
473 #[test]
474 fn test_resolve_namespace_upstream_from_namespace() {
475 let config = test_config();
476 let resolver = ResourceResolver::new(&config);
477
478 let scope = Scope::Namespace("api".to_string());
479 let result = resolver.resolve_upstream("ns-backend", &scope);
480 assert!(result.is_some());
481 assert_eq!(result.unwrap().id, "ns-backend");
482 }
483
484 #[test]
485 fn test_namespace_upstream_not_visible_from_global() {
486 let config = test_config();
487 let resolver = ResourceResolver::new(&config);
488
489 let result = resolver.resolve_upstream("ns-backend", &Scope::Global);
490 assert!(result.is_none());
491 }
492
493 #[test]
494 fn test_resolve_service_upstream_from_service() {
495 let config = test_config();
496 let resolver = ResourceResolver::new(&config);
497
498 let scope = Scope::Service {
499 namespace: "api".to_string(),
500 service: "payments".to_string(),
501 };
502 let result = resolver.resolve_upstream("svc-backend", &scope);
503 assert!(result.is_some());
504 assert_eq!(result.unwrap().id, "svc-backend");
505 }
506
507 #[test]
508 fn test_service_can_access_namespace_upstream() {
509 let config = test_config();
510 let resolver = ResourceResolver::new(&config);
511
512 let scope = Scope::Service {
513 namespace: "api".to_string(),
514 service: "payments".to_string(),
515 };
516 let result = resolver.resolve_upstream("ns-backend", &scope);
517 assert!(result.is_some());
518 assert_eq!(result.unwrap().id, "ns-backend");
519 }
520
521 #[test]
522 fn test_service_can_access_global_upstream() {
523 let config = test_config();
524 let resolver = ResourceResolver::new(&config);
525
526 let scope = Scope::Service {
527 namespace: "api".to_string(),
528 service: "payments".to_string(),
529 };
530 let result = resolver.resolve_upstream("global-backend", &scope);
531 assert!(result.is_some());
532 assert_eq!(result.unwrap().id, "global-backend");
533 }
534
535 #[test]
536 fn test_exported_upstream_visible_globally() {
537 let config = test_config();
538 let resolver = ResourceResolver::new(&config);
539
540 let result = resolver.resolve_upstream("shared-backend", &Scope::Global);
542 assert!(result.is_some());
543 assert_eq!(result.unwrap().id, "shared-backend");
544 }
545
546 #[test]
547 fn test_exported_upstream_visible_from_other_namespace() {
548 let mut config = test_config();
549
550 let other_ns = NamespaceConfig::new("web");
552 config.namespaces.push(other_ns);
553
554 let resolver = ResourceResolver::new(&config);
555
556 let scope = Scope::Namespace("web".to_string());
558 let result = resolver.resolve_upstream("shared-backend", &scope);
559 assert!(result.is_some());
560 assert_eq!(result.unwrap().id, "shared-backend");
561 }
562
563 #[test]
564 fn test_qualified_reference_direct_lookup() {
565 let config = test_config();
566 let resolver = ResourceResolver::new(&config);
567
568 let result = resolver.resolve_upstream("api:ns-backend", &Scope::Global);
570 assert!(result.is_some());
571 assert_eq!(result.unwrap().id, "ns-backend");
572 }
573
574 #[test]
575 fn test_qualified_service_reference() {
576 let config = test_config();
577 let resolver = ResourceResolver::new(&config);
578
579 let result = resolver.resolve_upstream("api:payments:svc-backend", &Scope::Global);
581 assert!(result.is_some());
582 assert_eq!(result.unwrap().id, "svc-backend");
583 }
584
585 #[test]
586 fn test_nonexistent_upstream() {
587 let config = test_config();
588 let resolver = ResourceResolver::new(&config);
589
590 let result = resolver.resolve_upstream("nonexistent", &Scope::Global);
591 assert!(result.is_none());
592 }
593
594 #[test]
595 fn test_can_resolve_upstream() {
596 let config = test_config();
597 let resolver = ResourceResolver::new(&config);
598
599 assert!(resolver.can_resolve_upstream("global-backend", &Scope::Global));
600 assert!(!resolver.can_resolve_upstream("ns-backend", &Scope::Global));
601 assert!(resolver.can_resolve_upstream("ns-backend", &Scope::Namespace("api".to_string())));
602 }
603}