Skip to main content

xianyun_deploy/deployer/namespace/
manager.rs

1//! A deployer that pre-creates Kubernetes namespaces idempotently so
2//! that other deployers can write into them.
3//!
4//! Ported from `internal/deployer/namespace/{manager,errors}.go`.
5
6use std::time::Duration;
7
8use thiserror::Error;
9use xianyun_backends::kubectl::{self, KubectlError};
10use xianyun_core::config::latest::{CaproniConfig, DeployerConfig};
11
12const DEPLOYER_TYPE: &str = "namespace";
13
14/// The DNS-1123 label limit. Kubernetes enforces this on every
15/// namespace name.
16const MAX_NAMESPACE_NAME_LENGTH: usize = 63;
17
18/// The per-namespace wait budget for phase=Active plus the default
19/// `ServiceAccount`. Kubernetes normally settles in well under a
20/// second; 60s is comfortably past any expected propagation delay and
21/// short enough that a genuinely broken cluster fails fast.
22const NAMESPACE_READY_TIMEOUT: Duration = Duration::from_secs(60);
23
24/// Errors from the namespace deployer.
25#[derive(Debug, Error)]
26pub enum NamespaceError {
27    /// A manager method received a deployer config whose type is not
28    /// `"namespace"`.
29    #[error("deployer is not a namespace deployer")]
30    NotNamespaceDeployer,
31    /// A namespace deployer's `namespaces` list is empty.
32    #[error("namespaces list must not be empty")]
33    EmptyNamespaces,
34    /// A namespace name does not match the DNS-1123 label syntax.
35    #[error("invalid namespace name: {0:?}")]
36    InvalidNamespaceName(String),
37    /// A namespace name exceeds the 63-character DNS-1123 label
38    /// limit. Kubernetes will reject any longer name at apply time,
39    /// so this is caught during validation with a clearer message.
40    #[error("namespace name exceeds 63 characters: {0:?}")]
41    NamespaceNameTooLong(String),
42    /// Deploying a namespace failed.
43    #[error("deploying namespace {0:?}: {1}")]
44    Deploy(String, #[source] KubectlError),
45    /// Undeploying a namespace failed.
46    #[error("undeploying namespace {0:?}: {1}")]
47    Undeploy(String, #[source] KubectlError),
48    /// Checking a namespace's existence failed.
49    #[error("checking namespace {0:?}: {1}")]
50    Status(String, #[source] KubectlError),
51}
52
53/// Matches a valid DNS-1123 label (`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`),
54/// used for namespace names. Length is enforced separately via
55/// [`MAX_NAMESPACE_NAME_LENGTH`].
56fn is_valid_dns1123_label(name: &str) -> bool {
57    let bytes = name.as_bytes();
58
59    if bytes.is_empty() {
60        return false;
61    }
62
63    let is_label_char = |c: u8| c.is_ascii_lowercase() || c.is_ascii_digit();
64
65    if !is_label_char(bytes[0]) || !is_label_char(*bytes.last().unwrap()) {
66        return false;
67    }
68
69    bytes.iter().all(|&c| is_label_char(c) || c == b'-')
70}
71
72/// Implements the namespace deployer.
73pub struct Manager {
74    backend: Box<dyn kubectl::Backend + Send + Sync>,
75}
76
77impl Manager {
78    /// Creates a new namespace deployer manager.
79    #[must_use]
80    pub fn new(backend: Box<dyn kubectl::Backend + Send + Sync>) -> Self {
81        Self { backend }
82    }
83
84    /// Returns the deployer type identifier (`"namespace"`).
85    #[must_use]
86    pub fn deployer_type(&self) -> &'static str {
87        DEPLOYER_TYPE
88    }
89
90    /// Validates the namespace deployer configuration.
91    pub fn validate(
92        &self,
93        deployer_config: &DeployerConfig,
94        _caproni_config: &CaproniConfig,
95    ) -> Result<(), NamespaceError> {
96        if deployer_config.deployer_type != DEPLOYER_TYPE {
97            return Err(NamespaceError::NotNamespaceDeployer);
98        }
99
100        let namespaces = configured_namespaces(deployer_config)?;
101
102        for name in namespaces {
103            if name.len() > MAX_NAMESPACE_NAME_LENGTH {
104                return Err(NamespaceError::NamespaceNameTooLong(name.clone()));
105            }
106
107            if !is_valid_dns1123_label(name) {
108                return Err(NamespaceError::InvalidNamespaceName(name.clone()));
109            }
110        }
111
112        Ok(())
113    }
114
115    /// Creates each configured namespace and waits for it to be ready
116    /// before continuing to the next one.
117    ///
118    /// A missing or empty namespace block is rejected with
119    /// [`NamespaceError::EmptyNamespaces`]. [`Manager::validate`]
120    /// catches this at config-load time, but a programmatic caller
121    /// that bypasses validation must not see `deploy` silently
122    /// succeed while `status` reports [`super::super::State`]
123    /// not-deployed.
124    pub fn deploy(&self, deployer_config: &DeployerConfig) -> Result<(), NamespaceError> {
125        if deployer_config.deployer_type != DEPLOYER_TYPE {
126            return Err(NamespaceError::NotNamespaceDeployer);
127        }
128
129        let namespaces = configured_namespaces(deployer_config)?;
130
131        for name in namespaces {
132            self.backend
133                .ensure_namespace(name)
134                .map_err(|e| NamespaceError::Deploy(name.clone(), e))?;
135
136            self.backend
137                .wait_for_namespace_ready(name, NAMESPACE_READY_TIMEOUT)
138                .map_err(|e| NamespaceError::Deploy(name.clone(), e))?;
139        }
140
141        Ok(())
142    }
143
144    /// Deletes the configured namespaces when `delete_on_undeploy` is
145    /// set. Otherwise this is a no-op -- the namespaces are treated
146    /// as shared infrastructure that may still be in use by sibling
147    /// deployers.
148    pub fn undeploy(
149        &self,
150        deployer_config: &DeployerConfig,
151        _caproni_config: &CaproniConfig,
152    ) -> Result<(), NamespaceError> {
153        if deployer_config.deployer_type != DEPLOYER_TYPE {
154            return Err(NamespaceError::NotNamespaceDeployer);
155        }
156
157        let Some(namespace) = &deployer_config.namespace else {
158            return Ok(());
159        };
160
161        if !namespace.delete_on_undeploy {
162            return Ok(());
163        }
164
165        for name in &namespace.namespaces {
166            self.backend
167                .delete_namespace(name)
168                .map_err(|e| NamespaceError::Undeploy(name.clone(), e))?;
169        }
170
171        Ok(())
172    }
173
174    /// Returns [`super::super::State::Deployed`] only when every
175    /// configured namespace exists. Any backend error (other than
176    /// "namespace not found", which the backend handles as a `false`
177    /// result) propagates to the caller.
178    ///
179    /// A missing or empty namespace block is reported as
180    /// [`super::super::State::NotDeployed`] rather than vacuously
181    /// "deployed".
182    pub fn status(
183        &self,
184        deployer_config: &DeployerConfig,
185        _caproni_config: &CaproniConfig,
186    ) -> Result<super::super::Status, NamespaceError> {
187        if deployer_config.deployer_type != DEPLOYER_TYPE {
188            return Err(NamespaceError::NotNamespaceDeployer);
189        }
190
191        let not_deployed = || super::super::Status {
192            state: super::super::State::NotDeployed,
193            deployer_type: DEPLOYER_TYPE.to_string(),
194        };
195
196        let Some(namespace) = &deployer_config.namespace else {
197            return Ok(not_deployed());
198        };
199
200        if namespace.namespaces.is_empty() {
201            return Ok(not_deployed());
202        }
203
204        for name in &namespace.namespaces {
205            let exists = self
206                .backend
207                .namespace_exists(name)
208                .map_err(|e| NamespaceError::Status(name.clone(), e))?;
209
210            if !exists {
211                return Ok(not_deployed());
212            }
213        }
214
215        Ok(super::super::Status {
216            state: super::super::State::Deployed,
217            deployer_type: DEPLOYER_TYPE.to_string(),
218        })
219    }
220}
221
222fn configured_namespaces(deployer_config: &DeployerConfig) -> Result<&Vec<String>, NamespaceError> {
223    match &deployer_config.namespace {
224        Some(ns) if !ns.namespaces.is_empty() => Ok(&ns.namespaces),
225        _ => Err(NamespaceError::EmptyNamespaces),
226    }
227}
228
229/// Lets the orchestrator drive a namespace [`Manager`] through the registry.
230impl crate::deployer::Manager for Manager {
231    fn deployer_type(&self) -> String {
232        DEPLOYER_TYPE.to_string()
233    }
234
235    fn deploy(
236        &self,
237        deployer_config: &DeployerConfig,
238        _cfg: Option<&xianyun_core::config::container::Container>,
239    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
240        Manager::deploy(self, deployer_config).map_err(Into::into)
241    }
242
243    fn undeploy(
244        &self,
245        deployer_config: &DeployerConfig,
246        caproni_config: &CaproniConfig,
247    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
248        Manager::undeploy(self, deployer_config, caproni_config).map_err(Into::into)
249    }
250
251    fn status(
252        &self,
253        deployer_config: &DeployerConfig,
254        caproni_config: &CaproniConfig,
255    ) -> Result<crate::deployer::Status, Box<dyn std::error::Error + Send + Sync>> {
256        Manager::status(self, deployer_config, caproni_config).map_err(Into::into)
257    }
258
259    fn validate(
260        &self,
261        deployer_config: &DeployerConfig,
262        caproni_config: &CaproniConfig,
263    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
264        Manager::validate(self, deployer_config, caproni_config).map_err(Into::into)
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use std::sync::Mutex;
271
272    use super::*;
273    use xianyun_core::config::latest::NamespaceConfig;
274
275    /// A minimal hand-rolled mock for `kubectl::Backend`. Only the
276    /// four namespace-lifecycle methods are exercised by this
277    /// module's tests; every other method panics if called, since no
278    /// `xianyun-backends`-side mock of the whole `Backend` trait
279    /// exists yet (only its `CommandRunner` dependency has one, via
280    /// `xianyun-testutil`). Expectations are matched FIFO per method,
281    /// mirroring `xianyun-testutil::MockCommandRunner`'s model.
282    type WaitExpectation = (String, Duration, Result<(), KubectlError>);
283
284    #[derive(Default)]
285    struct MockKubectlBackend {
286        ensure_namespace: Mutex<Vec<(String, Result<(), KubectlError>)>>,
287        delete_namespace: Mutex<Vec<(String, Result<(), KubectlError>)>>,
288        namespace_exists: Mutex<Vec<(String, Result<bool, KubectlError>)>>,
289        wait_for_namespace_ready: Mutex<Vec<WaitExpectation>>,
290    }
291
292    impl MockKubectlBackend {
293        fn new() -> Self {
294            Self::default()
295        }
296
297        fn expect_ensure_namespace(&self, name: &str, result: Result<(), KubectlError>) {
298            self.ensure_namespace
299                .lock()
300                .unwrap()
301                .push((name.to_string(), result));
302        }
303
304        fn expect_delete_namespace(&self, name: &str, result: Result<(), KubectlError>) {
305            self.delete_namespace
306                .lock()
307                .unwrap()
308                .push((name.to_string(), result));
309        }
310
311        fn expect_namespace_exists(&self, name: &str, result: Result<bool, KubectlError>) {
312            self.namespace_exists
313                .lock()
314                .unwrap()
315                .push((name.to_string(), result));
316        }
317
318        fn expect_wait_for_namespace_ready(
319            &self,
320            name: &str,
321            timeout: Duration,
322            result: Result<(), KubectlError>,
323        ) {
324            self.wait_for_namespace_ready
325                .lock()
326                .unwrap()
327                .push((name.to_string(), timeout, result));
328        }
329    }
330
331    fn err_test_backend() -> KubectlError {
332        KubectlError::Other("kubectl backend failed".to_string())
333    }
334
335    impl kubectl::Backend for MockKubectlBackend {
336        fn get(
337            &self,
338            _resource_type: &str,
339            _name: &str,
340            _namespace: &str,
341        ) -> Result<String, KubectlError> {
342            unimplemented!("not exercised by namespace manager tests")
343        }
344
345        fn get_raw(&self, _path: &str) -> Result<String, KubectlError> {
346            unimplemented!("not exercised by namespace manager tests")
347        }
348
349        fn delete_pods_by_label(
350            &self,
351            _namespace: &str,
352            _label_selector: &str,
353        ) -> Result<usize, KubectlError> {
354            unimplemented!("not exercised by namespace manager tests")
355        }
356
357        fn get_version(
358            &self,
359            _timeout: Duration,
360            _interval: Duration,
361        ) -> Result<kubectl::VersionResponse, KubectlError> {
362            unimplemented!("not exercised by namespace manager tests")
363        }
364
365        fn scale(
366            &self,
367            _resource_type: &str,
368            _name: &str,
369            _namespace: &str,
370            _replicas: i64,
371        ) -> Result<(), KubectlError> {
372            unimplemented!("not exercised by namespace manager tests")
373        }
374
375        fn apply(&self, _json_data: &[u8], _namespace: &str) -> Result<(), KubectlError> {
376            unimplemented!("not exercised by namespace manager tests")
377        }
378
379        fn delete_manifest(&self, _data: &[u8], _namespace: &str) -> Result<(), KubectlError> {
380            unimplemented!("not exercised by namespace manager tests")
381        }
382
383        fn delete(
384            &self,
385            _resource_type: &str,
386            _name: &str,
387            _namespace: &str,
388        ) -> Result<(), KubectlError> {
389            unimplemented!("not exercised by namespace manager tests")
390        }
391
392        fn wait(
393            &self,
394            _resource_type: &str,
395            _name: &str,
396            _namespace: &str,
397            _condition: &str,
398            _timeout: Duration,
399        ) -> Result<(), KubectlError> {
400            unimplemented!("not exercised by namespace manager tests")
401        }
402
403        fn wait_for_rollout_status(
404            &self,
405            _resource_type: &str,
406            _name: &str,
407            _namespace: &str,
408            _timeout: Duration,
409        ) -> Result<(), KubectlError> {
410            unimplemented!("not exercised by namespace manager tests")
411        }
412
413        fn wait_for_pod_ready_by_label(
414            &self,
415            _namespace: &str,
416            _label_selector: &str,
417            _timeout: Duration,
418        ) -> Result<(), KubectlError> {
419            unimplemented!("not exercised by namespace manager tests")
420        }
421
422        fn wait_for_job_complete(
423            &self,
424            _name: &str,
425            _namespace: &str,
426            _timeout: Duration,
427        ) -> Result<(), KubectlError> {
428            unimplemented!("not exercised by namespace manager tests")
429        }
430
431        fn wait_for_job_created(
432            &self,
433            _name: &str,
434            _namespace: &str,
435            _timeout: Duration,
436        ) -> Result<(), KubectlError> {
437            unimplemented!("not exercised by namespace manager tests")
438        }
439
440        fn list_resource_names(
441            &self,
442            _kind: &str,
443            _namespace: &str,
444            _label_selector: &str,
445        ) -> Result<Vec<String>, KubectlError> {
446            unimplemented!("not exercised by namespace manager tests")
447        }
448
449        fn ensure_namespace(&self, name: &str) -> Result<(), KubectlError> {
450            let mut queue = self.ensure_namespace.lock().unwrap();
451            let pos = queue
452                .iter()
453                .position(|(n, _)| n == name)
454                .unwrap_or_else(|| panic!("unexpected ensure_namespace({name:?}) call"));
455            queue.remove(pos).1
456        }
457
458        fn delete_namespace(&self, name: &str) -> Result<(), KubectlError> {
459            let mut queue = self.delete_namespace.lock().unwrap();
460            let pos = queue
461                .iter()
462                .position(|(n, _)| n == name)
463                .unwrap_or_else(|| panic!("unexpected delete_namespace({name:?}) call"));
464            queue.remove(pos).1
465        }
466
467        fn namespace_exists(&self, name: &str) -> Result<bool, KubectlError> {
468            let mut queue = self.namespace_exists.lock().unwrap();
469            let pos = queue
470                .iter()
471                .position(|(n, _)| n == name)
472                .unwrap_or_else(|| panic!("unexpected namespace_exists({name:?}) call"));
473            queue.remove(pos).1
474        }
475
476        fn wait_for_namespace_ready(
477            &self,
478            name: &str,
479            timeout: Duration,
480        ) -> Result<(), KubectlError> {
481            let mut queue = self.wait_for_namespace_ready.lock().unwrap();
482            let pos = queue
483                .iter()
484                .position(|(n, t, _)| n == name && *t == timeout)
485                .unwrap_or_else(|| {
486                    panic!("unexpected wait_for_namespace_ready({name:?}, {timeout:?}) call")
487                });
488            queue.remove(pos).2
489        }
490
491        fn run_job(
492            &self,
493            _image: &str,
494            _namespace: &str,
495            _timeout: Duration,
496            _command: &[&str],
497        ) -> Result<String, KubectlError> {
498            unimplemented!("not exercised by namespace manager tests")
499        }
500    }
501
502    fn deployer_config(namespaces: &[&str]) -> DeployerConfig {
503        DeployerConfig {
504            deployer_type: DEPLOYER_TYPE.to_string(),
505            namespace: Some(NamespaceConfig {
506                namespaces: namespaces.iter().map(|s| s.to_string()).collect(),
507                delete_on_undeploy: false,
508            }),
509            ..Default::default()
510        }
511    }
512
513    // Ported from internal/deployer/namespace/manager_test.go:18 (TestType)
514    #[test]
515    fn test_type() {
516        let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
517        assert_eq!(mgr.deployer_type(), "namespace");
518    }
519
520    // Ported from internal/deployer/namespace/manager_test.go:25 (TestValidate)
521    #[test]
522    fn test_validate() {
523        let long_valid = format!("a{}c", "b".repeat(61)); // 63 chars
524        let too_long = format!("a{}c", "b".repeat(62)); // 64 chars
525
526        let cases: Vec<(&str, DeployerConfig, Option<&str>)> = vec![
527            ("valid single namespace", deployer_config(&["gitlab"]), None),
528            (
529                "valid multiple namespaces",
530                deployer_config(&["gitlab", "monitoring"]),
531                None,
532            ),
533            (
534                "wrong type",
535                DeployerConfig {
536                    deployer_type: "helm".to_string(),
537                    namespace: Some(NamespaceConfig {
538                        namespaces: vec!["gitlab".to_string()],
539                        ..Default::default()
540                    }),
541                    ..Default::default()
542                },
543                Some("not_namespace_deployer"),
544            ),
545            (
546                "missing namespace block",
547                DeployerConfig {
548                    deployer_type: DEPLOYER_TYPE.to_string(),
549                    ..Default::default()
550                },
551                Some("empty_namespaces"),
552            ),
553            (
554                "empty namespaces",
555                deployer_config(&[]),
556                Some("empty_namespaces"),
557            ),
558            (
559                "invalid dns name",
560                deployer_config(&["Not_Valid"]),
561                Some("invalid_namespace_name"),
562            ),
563            (
564                "name at 63 char limit accepted",
565                deployer_config(&[&long_valid]),
566                None,
567            ),
568            (
569                "name longer than 63 chars rejected",
570                deployer_config(&[&too_long]),
571                Some("namespace_name_too_long"),
572            ),
573        ];
574
575        for (name, cfg, want_err) in cases {
576            let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
577            let result = mgr.validate(&cfg, &CaproniConfig::default());
578
579            match want_err {
580                None => assert!(result.is_ok(), "case {name}: expected Ok, got {result:?}"),
581                Some("not_namespace_deployer") => {
582                    assert!(
583                        matches!(result, Err(NamespaceError::NotNamespaceDeployer)),
584                        "case {name}"
585                    );
586                }
587                Some("empty_namespaces") => {
588                    assert!(
589                        matches!(result, Err(NamespaceError::EmptyNamespaces)),
590                        "case {name}"
591                    );
592                }
593                Some("invalid_namespace_name") => {
594                    assert!(
595                        matches!(result, Err(NamespaceError::InvalidNamespaceName(_))),
596                        "case {name}"
597                    );
598                }
599                Some("namespace_name_too_long") => {
600                    assert!(
601                        matches!(result, Err(NamespaceError::NamespaceNameTooLong(_))),
602                        "case {name}"
603                    );
604                }
605                Some(other) => panic!("unknown expectation {other}"),
606            }
607        }
608    }
609
610    // Ported from internal/deployer/namespace/manager_test.go:120 (TestDeploy)
611    #[test]
612    fn test_deploy() {
613        // single namespace
614        {
615            let backend = MockKubectlBackend::new();
616            backend.expect_ensure_namespace("gitlab", Ok(()));
617            backend.expect_wait_for_namespace_ready("gitlab", NAMESPACE_READY_TIMEOUT, Ok(()));
618
619            let mgr = Manager::new(Box::new(backend));
620            mgr.deploy(&deployer_config(&["gitlab"])).unwrap();
621        }
622
623        // multiple namespaces in order
624        {
625            let backend = MockKubectlBackend::new();
626            backend.expect_ensure_namespace("gitlab", Ok(()));
627            backend.expect_wait_for_namespace_ready("gitlab", NAMESPACE_READY_TIMEOUT, Ok(()));
628            backend.expect_ensure_namespace("monitoring", Ok(()));
629            backend.expect_wait_for_namespace_ready("monitoring", NAMESPACE_READY_TIMEOUT, Ok(()));
630
631            let mgr = Manager::new(Box::new(backend));
632            mgr.deploy(&deployer_config(&["gitlab", "monitoring"]))
633                .unwrap();
634        }
635
636        // ensure error propagates
637        {
638            let backend = MockKubectlBackend::new();
639            backend.expect_ensure_namespace("gitlab", Err(err_test_backend()));
640
641            let mgr = Manager::new(Box::new(backend));
642            let err = mgr.deploy(&deployer_config(&["gitlab"])).unwrap_err();
643            assert!(matches!(err, NamespaceError::Deploy(ref n, _) if n == "gitlab"));
644            assert!(err.to_string().contains("gitlab"));
645            assert!(err.to_string().contains("kubectl backend failed"));
646        }
647
648        // wait error propagates and stops further namespaces
649        {
650            let backend = MockKubectlBackend::new();
651            backend.expect_ensure_namespace("gitlab", Ok(()));
652            backend.expect_wait_for_namespace_ready(
653                "gitlab",
654                NAMESPACE_READY_TIMEOUT,
655                Err(err_test_backend()),
656            );
657
658            let mgr = Manager::new(Box::new(backend));
659            let err = mgr
660                .deploy(&deployer_config(&["gitlab", "monitoring"]))
661                .unwrap_err();
662            assert!(matches!(err, NamespaceError::Deploy(ref n, _) if n == "gitlab"));
663            assert!(err.to_string().contains("gitlab"));
664        }
665
666        // wrong type rejected
667        {
668            let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
669            let cfg = DeployerConfig {
670                deployer_type: "helm".to_string(),
671                namespace: Some(NamespaceConfig {
672                    namespaces: vec!["gitlab".to_string()],
673                    ..Default::default()
674                }),
675                ..Default::default()
676            };
677            let err = mgr.deploy(&cfg).unwrap_err();
678            assert!(matches!(err, NamespaceError::NotNamespaceDeployer));
679        }
680
681        // missing namespace block rejected
682        {
683            let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
684            let cfg = DeployerConfig {
685                deployer_type: DEPLOYER_TYPE.to_string(),
686                ..Default::default()
687            };
688            let err = mgr.deploy(&cfg).unwrap_err();
689            assert!(matches!(err, NamespaceError::EmptyNamespaces));
690        }
691    }
692
693    // Ported from internal/deployer/namespace/manager_test.go:216 (TestUndeploy)
694    #[test]
695    fn test_undeploy() {
696        // default no-op
697        {
698            let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
699            mgr.undeploy(
700                &deployer_config(&["gitlab", "monitoring"]),
701                &CaproniConfig::default(),
702            )
703            .unwrap();
704        }
705
706        // delete_on_undeploy true deletes all
707        {
708            let backend = MockKubectlBackend::new();
709            backend.expect_delete_namespace("gitlab", Ok(()));
710            backend.expect_delete_namespace("monitoring", Ok(()));
711
712            let mgr = Manager::new(Box::new(backend));
713            let cfg = DeployerConfig {
714                deployer_type: DEPLOYER_TYPE.to_string(),
715                namespace: Some(NamespaceConfig {
716                    namespaces: vec!["gitlab".to_string(), "monitoring".to_string()],
717                    delete_on_undeploy: true,
718                }),
719                ..Default::default()
720            };
721            mgr.undeploy(&cfg, &CaproniConfig::default()).unwrap();
722        }
723
724        // delete error propagates
725        {
726            let backend = MockKubectlBackend::new();
727            backend.expect_delete_namespace("gitlab", Err(err_test_backend()));
728
729            let mgr = Manager::new(Box::new(backend));
730            let cfg = DeployerConfig {
731                deployer_type: DEPLOYER_TYPE.to_string(),
732                namespace: Some(NamespaceConfig {
733                    namespaces: vec!["gitlab".to_string(), "monitoring".to_string()],
734                    delete_on_undeploy: true,
735                }),
736                ..Default::default()
737            };
738            let err = mgr.undeploy(&cfg, &CaproniConfig::default()).unwrap_err();
739            assert!(matches!(err, NamespaceError::Undeploy(ref n, _) if n == "gitlab"));
740            assert!(err.to_string().contains("gitlab"));
741        }
742
743        // wrong type rejected
744        {
745            let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
746            let cfg = DeployerConfig {
747                deployer_type: "helm".to_string(),
748                namespace: Some(NamespaceConfig {
749                    namespaces: vec!["gitlab".to_string()],
750                    delete_on_undeploy: true,
751                }),
752                ..Default::default()
753            };
754            let err = mgr.undeploy(&cfg, &CaproniConfig::default()).unwrap_err();
755            assert!(matches!(err, NamespaceError::NotNamespaceDeployer));
756        }
757    }
758
759    // Ported from internal/deployer/namespace/manager_test.go:286 (TestStatus)
760    #[test]
761    fn test_status() {
762        // all exist
763        {
764            let backend = MockKubectlBackend::new();
765            backend.expect_namespace_exists("gitlab", Ok(true));
766            backend.expect_namespace_exists("monitoring", Ok(true));
767
768            let mgr = Manager::new(Box::new(backend));
769            let status = mgr
770                .status(
771                    &deployer_config(&["gitlab", "monitoring"]),
772                    &CaproniConfig::default(),
773                )
774                .unwrap();
775            assert_eq!(status.state, super::super::super::State::Deployed);
776            assert_eq!(status.deployer_type, "namespace");
777        }
778
779        // one missing
780        {
781            let backend = MockKubectlBackend::new();
782            backend.expect_namespace_exists("gitlab", Ok(true));
783            backend.expect_namespace_exists("monitoring", Ok(false));
784
785            let mgr = Manager::new(Box::new(backend));
786            let status = mgr
787                .status(
788                    &deployer_config(&["gitlab", "monitoring"]),
789                    &CaproniConfig::default(),
790                )
791                .unwrap();
792            assert_eq!(status.state, super::super::super::State::NotDeployed);
793        }
794
795        // none exist
796        {
797            let backend = MockKubectlBackend::new();
798            backend.expect_namespace_exists("gitlab", Ok(false));
799
800            let mgr = Manager::new(Box::new(backend));
801            let status = mgr
802                .status(&deployer_config(&["gitlab"]), &CaproniConfig::default())
803                .unwrap();
804            assert_eq!(status.state, super::super::super::State::NotDeployed);
805        }
806
807        // backend error propagates
808        {
809            let backend = MockKubectlBackend::new();
810            backend.expect_namespace_exists("gitlab", Err(err_test_backend()));
811
812            let mgr = Manager::new(Box::new(backend));
813            let err = mgr
814                .status(&deployer_config(&["gitlab"]), &CaproniConfig::default())
815                .unwrap_err();
816            assert!(matches!(err, NamespaceError::Status(ref n, _) if n == "gitlab"));
817        }
818
819        // wrong type rejected
820        {
821            let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
822            let err = mgr
823                .status(
824                    &DeployerConfig {
825                        deployer_type: "helm".to_string(),
826                        namespace: Some(NamespaceConfig {
827                            namespaces: vec!["gitlab".to_string()],
828                            ..Default::default()
829                        }),
830                        ..Default::default()
831                    },
832                    &CaproniConfig::default(),
833                )
834                .unwrap_err();
835            assert!(matches!(err, NamespaceError::NotNamespaceDeployer));
836        }
837
838        // missing namespace block returns StateNotDeployed
839        {
840            let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
841            let status = mgr
842                .status(
843                    &DeployerConfig {
844                        deployer_type: DEPLOYER_TYPE.to_string(),
845                        ..Default::default()
846                    },
847                    &CaproniConfig::default(),
848                )
849                .unwrap();
850            assert_eq!(status.state, super::super::super::State::NotDeployed);
851            assert_eq!(status.deployer_type, "namespace");
852        }
853    }
854}