1use thiserror::Error;
6use xianyun_core::config::container::Container;
7
8use super::order::{self, OrderError};
9use crate::deployer::{self, DeployerError};
10
11pub type DeployerFilterFunc<'a> = dyn Fn(&str) -> bool + 'a;
15
16#[derive(Debug, Error)]
18pub enum OrchestratorError {
19 #[error("listing deployers: {0}")]
21 ListDeployers(#[from] OrderError),
22 #[error("creating deployer manager for {0}: {1}")]
24 CreateManager(String, #[source] DeployerError),
25 #[error("deploying {0}: {1}")]
27 Deploy(String, #[source] Box<dyn std::error::Error + Send + Sync>),
28}
29
30#[derive(Debug, Default)]
32pub struct Orchestrator {
33 }
35
36impl Orchestrator {
37 #[must_use]
39 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn deploy_all(
60 &self,
61 cfg: &Container,
62 filter: Option<&DeployerFilterFunc<'_>>,
63 kubeconfig: &str,
64 ) -> Result<(), OrchestratorError> {
65 let default_filter = default_deployer_filter();
66 let filter: &DeployerFilterFunc<'_> = filter.unwrap_or(&default_filter);
67
68 let deployers = order::list_deployers_in_order(cfg)?;
69
70 for name in deployers {
71 if !filter(&name) {
72 continue;
73 }
74
75 let deployer_config = cfg
77 .caproni
78 .deployers
79 .as_ref()
80 .and_then(|d| d.get(&name))
81 .expect("listed deployer must exist in config");
82
83 tracing::info!(
84 deployer = %name,
85 deployer_type = %deployer_config.deployer_type,
86 "deploying"
87 );
88
89 let deployer_mgr =
90 deployer::registry::new_manager(&deployer_config.deployer_type, kubeconfig)
91 .map_err(|e| OrchestratorError::CreateManager(name.clone(), e))?;
92
93 deployer_mgr
94 .deploy(deployer_config, Some(cfg))
95 .map_err(|e| OrchestratorError::Deploy(name.clone(), e))?;
96
97 tracing::info!(deployer = %name, "deployed successfully");
98 }
99
100 Ok(())
101 }
102}
103
104fn default_deployer_filter() -> impl Fn(&str) -> bool {
106 |_deployer_name: &str| true
107}
108
109#[cfg(test)]
110mod tests {
111 use std::sync::Arc;
112 use std::sync::atomic::{AtomicUsize, Ordering};
113
114 use xianyun_core::config::container::new_container;
115 use xianyun_core::config::global::GlobalConfig;
116 use xianyun_core::config::latest::{CaproniConfig, DeployerConfig};
117 use xianyun_core::config::types::NetworkConfig;
118 use xianyun_core::orderedmap::OrderedMap;
119
120 use super::*;
121 use crate::deployer::{Manager, State, Status, registry};
122
123 fn new_test_config(mut caproni_config: CaproniConfig) -> Container {
126 if caproni_config.cluster.network.is_none() {
127 caproni_config.cluster.network = Some(NetworkConfig {
128 hostname: "test.example.com".to_string(),
129 additional_hostnames: Vec::new(),
130 cluster_domain: String::new(),
131 });
132 }
133
134 new_container(GlobalConfig::default(), caproni_config)
135 }
136
137 fn config_from(deployers: std::collections::HashMap<String, DeployerConfig>) -> CaproniConfig {
138 CaproniConfig {
139 deployers: Some(OrderedMap::from_map(deployers)),
140 ..Default::default()
141 }
142 }
143
144 struct MockManager<F> {
147 deployer_type: String,
148 on_deploy: F,
149 }
150
151 impl<F> Manager for MockManager<F>
152 where
153 F: Fn() -> Result<(), Box<dyn std::error::Error + Send + Sync>> + Send + Sync,
154 {
155 fn deployer_type(&self) -> String {
156 self.deployer_type.clone()
157 }
158
159 fn deploy(
160 &self,
161 _deployer_config: &DeployerConfig,
162 _cfg: Option<&Container>,
163 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
164 (self.on_deploy)()
165 }
166
167 fn undeploy(
168 &self,
169 _deployer_config: &DeployerConfig,
170 _caproni_config: &CaproniConfig,
171 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
172 Ok(())
173 }
174
175 fn status(
176 &self,
177 _deployer_config: &DeployerConfig,
178 _caproni_config: &CaproniConfig,
179 ) -> Result<Status, Box<dyn std::error::Error + Send + Sync>> {
180 Ok(Status {
181 state: State::NotDeployed,
182 deployer_type: self.deployer_type.clone(),
183 })
184 }
185
186 fn validate(
187 &self,
188 _deployer_config: &DeployerConfig,
189 _caproni_config: &CaproniConfig,
190 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
191 Ok(())
192 }
193 }
194
195 fn register_counting_mock(mock_type: &str) -> Arc<AtomicUsize> {
196 let count = Arc::new(AtomicUsize::new(0));
197 let counted = count.clone();
198 let deployer_type = mock_type.to_string();
199
200 registry::register(
201 mock_type,
202 Box::new(move |_kubeconfig| {
203 let count = counted.clone();
204 Box::new(MockManager {
205 deployer_type: deployer_type.clone(),
206 on_deploy: move || {
207 count.fetch_add(1, Ordering::SeqCst);
208 Ok(())
209 },
210 })
211 }),
212 );
213
214 count
215 }
216
217 #[test]
219 fn test_default_deployer_filter() {
220 let filter = default_deployer_filter();
221
222 assert!(filter("charts"));
223 assert!(filter("gitlab"));
224 assert!(filter("anything"));
225 assert!(filter(""));
226 }
227
228 #[test]
230 fn test_new_orchestrator() {
231 let _orch = Orchestrator::new();
232 }
233
234 #[test]
236 fn test_orchestrator_deploy_all_empty_config() {
237 let orch = Orchestrator::new();
238
239 let cfg = new_test_config(CaproniConfig::default());
240
241 orch.deploy_all(&cfg, None, "").unwrap();
242 }
243
244 #[test]
246 fn test_orchestrator_deploy_all_with_mock_deployer() {
247 let mock_type = "test-deploy-all-mock";
248 let count = register_counting_mock(mock_type);
249
250 let orch = Orchestrator::new();
251
252 let mut deployers = std::collections::HashMap::new();
253 deployers.insert(
254 "test-deployer".to_string(),
255 DeployerConfig {
256 deployer_type: mock_type.to_string(),
257 ..Default::default()
258 },
259 );
260
261 let cfg = new_test_config(config_from(deployers));
262
263 orch.deploy_all(&cfg, None, "").unwrap();
264
265 assert_eq!(count.load(Ordering::SeqCst), 1);
266 }
267
268 #[test]
270 fn test_orchestrator_deploy_all_with_filter() {
271 let mock_type = "test-deploy-filter";
272 let count = register_counting_mock(mock_type);
273
274 let orch = Orchestrator::new();
275
276 let mut deployers = std::collections::HashMap::new();
277 for name in ["deployer1", "deployer2", "deployer3"] {
278 deployers.insert(
279 name.to_string(),
280 DeployerConfig {
281 deployer_type: mock_type.to_string(),
282 ..Default::default()
283 },
284 );
285 }
286
287 let cfg = new_test_config(config_from(deployers));
288
289 let filter: &DeployerFilterFunc<'_> = &|name: &str| name == "deployer2";
290
291 orch.deploy_all(&cfg, Some(filter), "").unwrap();
292
293 assert_eq!(count.load(Ordering::SeqCst), 1);
294 }
295
296 #[test]
298 fn test_orchestrator_deploy_all_error_handling() {
299 let mock_type = "test-deploy-error";
300 let call_count = Arc::new(AtomicUsize::new(0));
301 let counted = call_count.clone();
302 let deployer_type = mock_type.to_string();
303
304 registry::register(
305 mock_type,
306 Box::new(move |_kubeconfig| {
307 let count = counted.clone();
308 Box::new(MockManager {
309 deployer_type: deployer_type.clone(),
310 on_deploy: move || {
311 let n = count.fetch_add(1, Ordering::SeqCst) + 1;
312 if n == 2 {
313 Err(Box::from("deployment failed")
314 as Box<dyn std::error::Error + Send + Sync>)
315 } else {
316 Ok(())
317 }
318 },
319 })
320 }),
321 );
322
323 let orch = Orchestrator::new();
324
325 let mut deployers = std::collections::HashMap::new();
326 for name in ["deployer1", "deployer2", "deployer3"] {
327 deployers.insert(
328 name.to_string(),
329 DeployerConfig {
330 deployer_type: mock_type.to_string(),
331 ..Default::default()
332 },
333 );
334 }
335
336 let cfg = new_test_config(config_from(deployers));
337
338 let err = orch.deploy_all(&cfg, None, "").unwrap_err();
339
340 assert!(err.to_string().contains("deployment failed"));
341 assert_eq!(call_count.load(Ordering::SeqCst), 2);
342 }
343
344 #[test]
346 fn test_orchestrator_deploy_all_unknown_deployer_type() {
347 let orch = Orchestrator::new();
348
349 let mut deployers = std::collections::HashMap::new();
350 deployers.insert(
351 "unknown".to_string(),
352 DeployerConfig {
353 deployer_type: "unknown-deployer-type-xyz".to_string(),
354 ..Default::default()
355 },
356 );
357
358 let cfg = new_test_config(config_from(deployers));
359
360 let err = orch.deploy_all(&cfg, None, "").unwrap_err();
361
362 assert!(err.to_string().contains("creating deployer manager"));
363 assert!(err.to_string().contains("unknown"));
364 }
365
366 #[test]
368 fn test_orchestrator_deploy_all_multiple_deployers() {
369 let mock_type1 = "test-multi-1";
370 let mock_type2 = "test-multi-2";
371 let count1 = register_counting_mock(mock_type1);
372 let count2 = register_counting_mock(mock_type2);
373
374 let orch = Orchestrator::new();
375
376 let mut deployers = std::collections::HashMap::new();
377 deployers.insert(
378 "first".to_string(),
379 DeployerConfig {
380 deployer_type: mock_type1.to_string(),
381 ..Default::default()
382 },
383 );
384 deployers.insert(
385 "second".to_string(),
386 DeployerConfig {
387 deployer_type: mock_type2.to_string(),
388 ..Default::default()
389 },
390 );
391 deployers.insert(
392 "third".to_string(),
393 DeployerConfig {
394 deployer_type: mock_type1.to_string(),
395 ..Default::default()
396 },
397 );
398
399 let cfg = new_test_config(config_from(deployers));
400
401 orch.deploy_all(&cfg, None, "").unwrap();
402
403 assert_eq!(count1.load(Ordering::SeqCst), 2);
404 assert_eq!(count2.load(Ordering::SeqCst), 1);
405 }
406
407 #[test]
409 fn test_orchestrator_deploy_all_nil_filter() {
410 let mock_type = "test-nil-filter";
411 let count = register_counting_mock(mock_type);
412
413 let orch = Orchestrator::new();
414
415 let mut deployers = std::collections::HashMap::new();
416 deployers.insert(
417 "deployer1".to_string(),
418 DeployerConfig {
419 deployer_type: mock_type.to_string(),
420 ..Default::default()
421 },
422 );
423 deployers.insert(
424 "deployer2".to_string(),
425 DeployerConfig {
426 deployer_type: mock_type.to_string(),
427 ..Default::default()
428 },
429 );
430
431 let cfg = new_test_config(config_from(deployers));
432
433 orch.deploy_all(&cfg, None, "").unwrap();
434
435 assert_eq!(count.load(Ordering::SeqCst), 2);
436 }
437
438 #[test]
440 fn test_deploy_all_deploys_every_declared_deployer() {
441 let mock_type = "test-deploys-every-declared-deployer";
442 let count = register_counting_mock(mock_type);
443
444 let orch = Orchestrator::new();
445
446 let mut deployers = std::collections::HashMap::new();
447 for name in ["cert-manager", "traefik", "my-app"] {
450 deployers.insert(
451 name.to_string(),
452 DeployerConfig {
453 deployer_type: mock_type.to_string(),
454 ..Default::default()
455 },
456 );
457 }
458
459 let cfg = new_test_config(config_from(deployers));
460
461 orch.deploy_all(&cfg, None, "").unwrap();
462
463 assert_eq!(count.load(Ordering::SeqCst), 3);
464 }
465}