1use std::collections::{HashMap, HashSet};
6use std::path::PathBuf;
7
8use configparser::ini::Ini;
9use thiserror::Error;
10
11#[derive(Debug, Error)]
12pub enum GroupSourceError {
13 #[error("no upcall configured for group source")]
14 GroupSourceNoUpcall,
15
16 #[error("group source query failed: {0}")]
17 GroupSourceQueryFailed(String),
18}
19
20#[derive(Debug, Error)]
21pub enum GroupResolverSourceError {
22 #[error("source not found: {0}")]
23 SourceNotFound(String),
24
25 #[error("source error: {0}")]
26 SourceError(#[from] GroupSourceError),
27}
28
29#[derive(Debug, Error)]
30pub enum GroupResolverConfigError {
31 #[error("config file not found: {0}")]
32 FileNotFound(PathBuf),
33
34 #[error("config parsing error: {0}")]
35 ParseError(String),
36}
37
38#[derive(Debug, Error)]
39pub enum GroupError {
40 #[error("group source error: {0}")]
41 SourceError(#[from] GroupSourceError),
42
43 #[error("group resolver error: {0}")]
44 ResolverError(#[from] GroupResolverSourceError),
45
46 #[error("group resolver config error: {0}")]
47 ConfigError(#[from] GroupResolverConfigError),
48}
49
50pub type Result<T> = std::result::Result<T, GroupError>;
51
52pub trait GroupSource: Send + Sync {
57 fn resolve_map(&self, group: &str) -> Result<String>;
61
62 fn resolve_all(&self) -> Result<String> {
66 Err(GroupSourceError::GroupSourceNoUpcall.into())
67 }
68
69 fn resolve_list(&self) -> Result<Vec<String>> {
71 Err(GroupSourceError::GroupSourceNoUpcall.into())
72 }
73
74 fn resolve_reverse(&self, _node: &str) -> Result<Vec<String>> {
76 Err(GroupSourceError::GroupSourceNoUpcall.into())
77 }
78
79 fn has_reverse(&self) -> bool {
81 false
82 }
83
84 fn cache_time(&self) -> Option<f64> {
86 None
87 }
88}
89
90pub struct StaticGroupSource {
95 map: HashMap<String, String>,
96 all: Option<String>,
97 list: Option<Vec<String>>,
98 reverse: Option<HashMap<String, Vec<String>>>,
99}
100
101impl StaticGroupSource {
102 pub fn new(map: HashMap<String, String>) -> Self {
104 Self {
105 map,
106 all: None,
107 list: None,
108 reverse: None,
109 }
110 }
111
112 pub fn with_all(mut self, all: impl Into<String>) -> Self {
114 self.all = Some(all.into());
115 self
116 }
117
118 pub fn with_list(mut self, list: Vec<String>) -> Self {
120 self.list = Some(list);
121 self
122 }
123
124 pub fn with_reverse(mut self, reverse: HashMap<String, Vec<String>>) -> Self {
126 self.reverse = Some(reverse);
127 self
128 }
129}
130
131impl GroupSource for StaticGroupSource {
132 fn resolve_map(&self, group: &str) -> Result<String> {
133 self.map
134 .get(group)
135 .cloned()
136 .ok_or(GroupSourceError::GroupSourceNoUpcall.into())
137 }
138
139 fn resolve_all(&self) -> Result<String> {
140 self.all
141 .clone()
142 .ok_or(GroupSourceError::GroupSourceNoUpcall.into())
143 }
144
145 fn resolve_list(&self) -> Result<Vec<String>> {
146 self.list
147 .clone()
148 .ok_or(GroupSourceError::GroupSourceNoUpcall.into())
149 }
150
151 fn resolve_reverse(&self, node: &str) -> Result<Vec<String>> {
152 self.reverse
153 .as_ref()
154 .and_then(|r| r.get(node).cloned())
155 .ok_or(GroupSourceError::GroupSourceNoUpcall.into())
156 }
157
158 fn has_reverse(&self) -> bool {
159 self.reverse.is_some()
160 }
161}
162
163pub struct UpcallGroupSource {
167 map_upcall: Option<String>,
168 all_upcall: Option<String>,
169 list_upcall: Option<String>,
170 reverse_upcall: Option<String>,
171}
172
173impl UpcallGroupSource {
174 pub fn new(
178 map_upcall: Option<String>,
179 all_upcall: Option<String>,
180 list_upcall: Option<String>,
181 reverse_upcall: Option<String>,
182 ) -> Self {
183 Self {
184 map_upcall,
185 all_upcall,
186 list_upcall,
187 reverse_upcall,
188 }
189 }
190
191 fn run_command(&self, template: &str, arg: &str) -> Result<String> {
192 let command = template.replace("%s", arg);
193 let output = std::process::Command::new("sh")
194 .arg("-c")
195 .arg(&command)
196 .env("GROUP", arg)
197 .output()
198 .map_err(|e| GroupSourceError::GroupSourceQueryFailed(e.to_string()))?;
199
200 if output.status.success() {
201 Ok(String::from_utf8(output.stdout)
202 .map_err(|e| GroupSourceError::GroupSourceQueryFailed(e.to_string()))?
203 .trim()
204 .to_string())
205 } else {
206 let stderr = String::from_utf8_lossy(&output.stderr);
207 Err(GroupSourceError::GroupSourceQueryFailed(stderr.to_string()).into())
208 }
209 }
210}
211
212impl GroupSource for UpcallGroupSource {
213 fn resolve_map(&self, group: &str) -> Result<String> {
214 match self.map_upcall.as_ref() {
215 Some(t) => self.run_command(t, group),
216 None => Err(GroupSourceError::GroupSourceNoUpcall.into()),
217 }
218 }
219
220 fn resolve_all(&self) -> Result<String> {
221 match self.all_upcall.as_ref() {
222 Some(t) => self.run_command(t, ""),
223 None => Err(GroupSourceError::GroupSourceNoUpcall.into()),
224 }
225 }
226
227 fn resolve_list(&self) -> Result<Vec<String>> {
228 match self.list_upcall.as_ref() {
229 Some(t) => {
230 let s = self.run_command(t, "")?;
231 Ok(s.split(|c: char| c == ',' || c.is_whitespace())
232 .filter(|s| !s.is_empty())
233 .map(|s| s.to_string())
234 .collect())
235 }
236 None => Err(GroupSourceError::GroupSourceNoUpcall.into()),
237 }
238 }
239
240 fn resolve_reverse(&self, node: &str) -> Result<Vec<String>> {
241 match self.reverse_upcall.as_ref() {
242 Some(t) => {
243 let s = self.run_command(t, node)?;
244 Ok(s.split(|c: char| c == ',' || c.is_whitespace())
245 .filter(|s| !s.is_empty())
246 .map(|s| s.to_string())
247 .collect())
248 }
249 None => Err(GroupSourceError::GroupSourceNoUpcall.into()),
250 }
251 }
252
253 fn has_reverse(&self) -> bool {
254 self.reverse_upcall.is_some()
255 }
256}
257
258pub struct GroupResolver {
262 sources: HashMap<String, Box<dyn GroupSource>>,
263 default_source_name: Option<String>,
264 illegal_chars: HashSet<char>,
265}
266
267impl GroupResolver {
268 pub fn new() -> Self {
270 Self {
271 sources: HashMap::new(),
272 default_source_name: None,
273 illegal_chars: HashSet::new(),
274 }
275 }
276
277 pub fn add_source(&mut self, name: impl Into<String>, source: Box<dyn GroupSource>) {
279 self.sources.insert(name.into(), source);
280 }
281
282 pub fn set_default(&mut self, name: impl Into<String>) {
284 self.default_source_name = Some(name.into());
285 }
286
287 fn get_source(&self, namespace: Option<&str>) -> Result<&dyn GroupSource> {
289 let name = match namespace {
290 Some(n) => n,
291 None => self.default_source_name.as_ref().ok_or(
292 GroupResolverSourceError::SourceNotFound("no default source set".to_string()),
293 )?,
294 };
295
296 self.sources
297 .get(name)
298 .map(|s| s.as_ref())
299 .ok_or(GroupResolverSourceError::SourceNotFound(name.to_string()).into())
300 }
301
302 pub fn group_nodes(&self, group: &str, namespace: Option<&str>) -> Result<Vec<String>> {
306 let source = self.get_source(namespace)?;
307 let pattern = source.resolve_map(group)?;
308 Ok(pattern
309 .split(|c: char| c == ',' || c.is_whitespace())
310 .filter(|s| !s.is_empty())
311 .map(|s| s.to_string())
312 .collect())
313 }
314
315 pub fn all_nodes(&self, namespace: Option<&str>) -> Result<Vec<String>> {
317 let source = self.get_source(namespace)?;
318 let all = source.resolve_all()?;
319 Ok(all
320 .split(|c: char| c == ',' || c.is_whitespace())
321 .filter(|s| !s.is_empty())
322 .map(|s| s.to_string())
323 .collect())
324 }
325
326 pub fn grouplist(&self, namespace: Option<&str>) -> Result<Vec<String>> {
328 let source = self.get_source(namespace)?;
329 source.resolve_list()
330 }
331
332 pub fn node_groups(&self, node: &str, namespace: Option<&str>) -> Result<Vec<String>> {
334 let source = self.get_source(namespace)?;
335 source.resolve_reverse(node)
336 }
337
338 pub fn has_node_groups(&self, namespace: Option<&str>) -> bool {
340 self.get_source(namespace)
341 .map(|s| s.has_reverse())
342 .unwrap_or(false)
343 }
344
345 pub fn set_illegal_chars(&mut self, chars: HashSet<char>) {
347 self.illegal_chars = chars;
348 }
349
350 pub fn is_valid_group_name(&self, name: &str) -> Result<()> {
352 for c in name.chars() {
353 if self.illegal_chars.contains(&c) {
354 return Err(GroupResolverSourceError::SourceError(
355 GroupSourceError::GroupSourceQueryFailed(format!(
356 "group name contains illegal character: '{}'",
357 c
358 )),
359 )
360 .into());
361 }
362 }
363 Ok(())
364 }
365}
366
367impl Default for GroupResolver {
368 fn default() -> Self {
369 Self::new()
370 }
371}
372
373pub struct GroupResolverConfig {
379 filenames: Vec<PathBuf>,
380 illegal_chars: HashSet<char>,
381 initialized: bool,
382 resolver: Option<GroupResolver>,
383}
384
385impl GroupResolverConfig {
386 pub fn new(filenames: Vec<PathBuf>, illegal_chars: HashSet<char>) -> Self {
388 Self {
389 filenames,
390 illegal_chars,
391 initialized: false,
392 resolver: None,
393 }
394 }
395
396 fn ensure_initialized(&mut self) -> Result<&mut GroupResolver> {
398 if !self.initialized {
399 self.initialize()?;
400 self.initialized = true;
401 }
402 Ok(self.resolver.as_mut().unwrap())
403 }
404
405 fn initialize(&mut self) -> Result<()> {
407 let mut resolver = GroupResolver::new();
408 resolver.set_illegal_chars(self.illegal_chars.clone());
409
410 for filename in &self.filenames {
411 if !filename.exists() {
412 return Err(GroupResolverConfigError::FileNotFound(filename.clone()).into());
413 }
414
415 let mut ini = Ini::new();
416 ini.load(filename.to_string_lossy().as_ref())
418 .map_err(|e| GroupResolverConfigError::ParseError(e.to_string()))?;
419
420 if let Some(default) = ini.get("main", "default") {
422 resolver.set_default(default);
423 }
424
425 for section_name in ini.sections() {
427 if section_name == "main" || section_name == "default" {
428 continue;
429 }
430
431 let map_upcall = ini.get(§ion_name, "map");
432 let all_upcall = ini.get(§ion_name, "all");
433 let list_upcall = ini.get(§ion_name, "list");
434 let reverse_upcall = ini.get(§ion_name, "reverse");
435
436 if map_upcall.is_some() {
438 let source =
439 UpcallGroupSource::new(map_upcall, all_upcall, list_upcall, reverse_upcall);
440 resolver.add_source(section_name.clone(), Box::new(source));
441 }
442 }
443 }
444
445 self.resolver = Some(resolver);
446 Ok(())
447 }
448
449 pub fn resolver(&mut self) -> Result<&mut GroupResolver> {
451 self.ensure_initialized()
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use std::collections::HashSet;
459
460 #[test]
461 fn test_static_group_source_basic_resolve() {
462 let mut map = HashMap::new();
463 map.insert("web".to_string(), "web1,web2,web3".to_string());
464 map.insert("db".to_string(), "db1,db2".to_string());
465
466 let source = StaticGroupSource::new(map);
467
468 assert_eq!(source.resolve_map("web").unwrap(), "web1,web2,web3");
469 assert_eq!(source.resolve_map("db").unwrap(), "db1,db2");
470 }
471
472 #[test]
473 fn test_static_group_source_missing_group() {
474 let source = StaticGroupSource::new(HashMap::new());
475 assert!(source.resolve_map("nonexistent").is_err());
476 }
477
478 #[test]
479 fn test_static_group_source_with_all_and_list() {
480 let mut map = HashMap::new();
481 map.insert("web".to_string(), "web1,web2".to_string());
482 map.insert("db".to_string(), "db1,db2".to_string());
483
484 let source = StaticGroupSource::new(map)
485 .with_all("web1,web2,db1,db2".to_string())
486 .with_list(vec!["web".to_string(), "db".to_string()]);
487
488 assert_eq!(source.resolve_all().unwrap(), "web1,web2,db1,db2");
489 assert_eq!(source.resolve_list().unwrap(), vec!["web", "db"]);
490 }
491
492 #[test]
493 fn test_static_group_source_reverse_lookup() {
494 let mut map = HashMap::new();
495 map.insert("web".to_string(), "web1,web2".to_string());
496 map.insert("db".to_string(), "db1,db2".to_string());
497
498 let mut reverse = HashMap::new();
499 reverse.insert("web1".to_string(), vec!["web".to_string()]);
500 reverse.insert("web2".to_string(), vec!["web".to_string()]);
501 reverse.insert("db1".to_string(), vec!["db".to_string()]);
502 reverse.insert("db2".to_string(), vec!["db".to_string()]);
503
504 let source = StaticGroupSource::new(map).with_reverse(reverse);
505
506 assert!(source.has_reverse());
507 assert_eq!(
508 source.resolve_reverse("web1").unwrap(),
509 vec!["web".to_string()]
510 );
511 assert_eq!(
512 source.resolve_reverse("db2").unwrap(),
513 vec!["db".to_string()]
514 );
515 }
516
517 #[test]
518 fn test_group_resolver_with_multiple_sources() {
519 let mut resolver = GroupResolver::new();
520
521 let mut map1 = HashMap::new();
522 map1.insert("web".to_string(), "web1,web2".to_string());
523 resolver.add_source("cluster1", Box::new(StaticGroupSource::new(map1)));
524
525 let mut map2 = HashMap::new();
526 map2.insert("db".to_string(), "db1,db2".to_string());
527 resolver.add_source("cluster2", Box::new(StaticGroupSource::new(map2)));
528
529 let web_nodes = resolver.group_nodes("web", Some("cluster1")).unwrap();
530 assert_eq!(web_nodes, vec!["web1", "web2"]);
531
532 let db_nodes = resolver.group_nodes("db", Some("cluster2")).unwrap();
533 assert_eq!(db_nodes, vec!["db1", "db2"]);
534 }
535
536 #[test]
537 fn test_group_resolver_default_source() {
538 let mut resolver = GroupResolver::new();
539
540 let mut map1 = HashMap::new();
541 map1.insert("web".to_string(), "web1,web2".to_string());
542 resolver.add_source("default", Box::new(StaticGroupSource::new(map1)));
543
544 resolver.set_default("default");
545
546 let web_nodes = resolver.group_nodes("web", None).unwrap();
548 assert_eq!(web_nodes, vec!["web1", "web2"]);
549 }
550
551 #[test]
552 fn test_group_resolver_missing_source() {
553 let resolver = GroupResolver::new();
554 assert!(resolver.group_nodes("web", Some("nonexistent")).is_err());
555 }
556
557 #[test]
558 fn test_group_resolver_missing_default() {
559 let resolver = GroupResolver::new();
560 assert!(resolver.group_nodes("web", None).is_err());
561 }
562
563 #[test]
564 fn test_illegal_chars_validation() {
565 let mut resolver = GroupResolver::new();
566 let mut illegal = HashSet::new();
567 illegal.insert('$');
568 illegal.insert(' ');
569
570 resolver.set_illegal_chars(illegal);
571
572 assert!(resolver.is_valid_group_name("web").is_ok());
573 assert!(resolver.is_valid_group_name("web-group").is_ok());
574 assert!(resolver.is_valid_group_name("web_group").is_ok());
575 assert!(resolver.is_valid_group_name("web$group").is_err());
576 assert!(resolver.is_valid_group_name("web group").is_err());
577 }
578
579 #[test]
580 fn test_upcall_source_no_upcall() {
581 let source = UpcallGroupSource::new(None, None, None, None);
582 assert!(source.resolve_map("test").is_err());
583 assert!(source.resolve_all().is_err());
584 assert!(source.resolve_list().is_err());
585 assert!(source.resolve_reverse("node").is_err());
586 assert!(!source.has_reverse());
587 }
588
589 #[test]
590 fn test_upcall_source_with_map() {
591 let source = UpcallGroupSource::new(Some("echo web1,web2".to_string()), None, None, None);
592 assert_eq!(source.resolve_map("test").unwrap(), "web1,web2");
593 }
594
595 #[test]
596 fn test_group_resolver_sources_from_ini() {
597 use std::io::Write;
598 use tempfile::NamedTempFile;
599
600 let mut file = NamedTempFile::new().unwrap();
601 writeln!(
602 file,
603 r#"[Main]
604default = cluster1
605
606[cluster1]
607map = echo web1,web2
608
609[cluster2]
610map = echo db1,db2
611list = echo web db
612all = echo web1,web2,db1,db2
613reverse = echo web
614"#
615 )
616 .unwrap();
617
618 let mut config = GroupResolverConfig::new(vec![file.path().to_path_buf()], HashSet::new());
619 let resolver = config.resolver().unwrap();
620
621 let web_nodes = resolver.group_nodes("test", Some("cluster1")).unwrap();
622 assert_eq!(web_nodes, vec!["web1", "web2"]);
623
624 let db_nodes = resolver.group_nodes("test", Some("cluster2")).unwrap();
625 assert_eq!(db_nodes, vec!["db1", "db2"]);
626
627 let list = resolver.grouplist(Some("cluster2")).unwrap();
628 assert_eq!(list, vec!["web", "db"]);
629 }
630
631 #[test]
632 fn test_group_resolver_config_file_not_found() {
633 let mut config = GroupResolverConfig::new(
634 vec![PathBuf::from("/nonexistent/path/groups.conf")],
635 HashSet::new(),
636 );
637 assert!(config.resolver().is_err());
638 }
639
640 #[test]
641 fn test_whitespace_splitting() {
642 let mut map = HashMap::new();
643 map.insert(
645 "web".to_string(),
646 "web1, web2 ,web3\tweb4\nweb5".to_string(),
647 );
648
649 let source = StaticGroupSource::new(map);
650 let nodes = source.resolve_map("web").unwrap();
651
652 let resolver = GroupResolver::new();
653 let parsed = nodes
654 .split(|c: char| c == ',' || c.is_whitespace())
655 .filter(|s| !s.is_empty())
656 .collect::<Vec<_>>();
657
658 assert_eq!(parsed, vec!["web1", "web2", "web3", "web4", "web5"]);
659 }
660}