Skip to main content

xianyun_deploy/deploy/
orchestrator.rs

1//! Orchestrates deployment operations across every declared deployer.
2//!
3//! Ported from `internal/deploy/orchestrator.go`.
4
5use thiserror::Error;
6use xianyun_core::config::container::Container;
7
8use super::order::{self, OrderError};
9use crate::deployer::{self, DeployerError};
10
11/// A predicate function that determines whether a deployer should be
12/// processed. It receives the deployer name and returns `true` if the
13/// deployer should be processed, `false` to skip.
14pub type DeployerFilterFunc<'a> = dyn Fn(&str) -> bool + 'a;
15
16/// Errors from [`Orchestrator::deploy_all`].
17#[derive(Debug, Error)]
18pub enum OrchestratorError {
19    /// Failed to order the declared deployers.
20    #[error("listing deployers: {0}")]
21    ListDeployers(#[from] OrderError),
22    /// Failed to create a deployer manager for a deployer's type.
23    #[error("creating deployer manager for {0}: {1}")]
24    CreateManager(String, #[source] DeployerError),
25    /// A deployer's `deploy` call failed.
26    #[error("deploying {0}: {1}")]
27    Deploy(String, #[source] Box<dyn std::error::Error + Send + Sync>),
28}
29
30/// Orchestrates deployment operations.
31#[derive(Debug, Default)]
32pub struct Orchestrator {
33    // Future: inject deployer backends here for testing
34}
35
36impl Orchestrator {
37    /// Creates a new deployment orchestrator.
38    #[must_use]
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Deploys all deployers matching the filter predicate.
44    ///
45    /// If `filter` is `None`, all deployers are deployed. Aborts on the
46    /// first deployment failure.
47    ///
48    /// # Errors
49    ///
50    /// Returns [`OrchestratorError`] if ordering the deployers fails,
51    /// creating a deployer manager for a deployer's type fails, or a
52    /// deployer's `deploy` call fails.
53    ///
54    /// # Panics
55    ///
56    /// Panics if a deployer named by [`order::list_deployers_in_order`] is
57    /// absent from `cfg`, which cannot happen because that function only
58    /// returns names it read from `cfg` itself.
59    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            // must exist because list_deployers_in_order returns only existing ones
76            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
104/// Returns a filter that accepts all deployers.
105fn 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    /// Wraps a [`CaproniConfig`] in a [`Container`] for use in tests,
124    /// mirroring the Go test helper `newTestConfig`.
125    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    /// A mock [`Manager`] that records how many times `deploy` is called
145    /// and delegates to a caller-provided closure for the return value.
146    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    // Ported from internal/deploy/orchestrator_test.go:33 (TestDefaultDeployerFilter)
218    #[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    // Ported from internal/deploy/orchestrator_test.go:44 (TestNewOrchestrator)
229    #[test]
230    fn test_new_orchestrator() {
231        let _orch = Orchestrator::new();
232    }
233
234    // Ported from internal/deploy/orchestrator_test.go:52 (TestOrchestrator_DeployAll_EmptyConfig)
235    #[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    // Ported from internal/deploy/orchestrator_test.go:67 (TestOrchestrator_DeployAll_WithMockDeployer)
245    #[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    // Ported from internal/deploy/orchestrator_test.go:97 (TestOrchestrator_DeployAll_WithFilter)
269    #[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    // Ported from internal/deploy/orchestrator_test.go:131 (TestOrchestrator_DeployAll_ErrorHandling)
297    #[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    // Ported from internal/deploy/orchestrator_test.go:171 (TestOrchestrator_DeployAll_UnknownDeployerType)
345    #[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    // Ported from internal/deploy/orchestrator_test.go:191 (TestOrchestrator_DeployAll_MultipleDeployers)
367    #[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    // Ported from internal/deploy/orchestrator_test.go:233 (TestOrchestrator_DeployAll_NilFilter)
408    #[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    // Ported from internal/deploy/orchestrator_test.go:266 (TestDeployAllDeploysEveryDeclaredDeployer)
439    #[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        // Names once carried by caproni's bundled infrastructure get no
448        // special treatment; they are ordinary deployers now.
449        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}