1use std::path::PathBuf;
10
11use thiserror::Error;
12use xianyun_backends::kubectl::{self, Backend as KubectlBackend};
13use xianyun_backends::kustomize::{self as kustomizebackend, Backend as KustomizeBackend};
14use xianyun_core::config::container::Container;
15use xianyun_core::config::latest::{CaproniConfig, DeployerConfig, KustomizeConfig};
16
17use crate::deployer::{State, Status};
18
19use super::Cleanup;
20use super::builtin::{self, BuiltinError};
21use super::camaterial::{self, CaMaterial, CaMaterialError, CaProvider, PkiCaProvider};
22use super::files::{self, FilesError};
23use super::hostname::{self, HostnameError};
24
25pub const DEPLOYER_TYPE: &str = "kustomize";
27
28#[derive(Debug, Error)]
30pub enum KustomizeError {
31 #[error("deployer is not a kustomize deployer")]
34 NotKustomizeDeployer,
35 #[error("kustomize.path is required")]
37 MissingPath,
38 #[error("kustomization path not found: {0}")]
40 PathNotFound(String),
41 #[error("unsupported url scheme in kustomize path: {0:?} uses scheme {1:?}")]
44 UnsupportedScheme(String, String),
45 #[error(transparent)]
47 Builtin(#[from] BuiltinError),
48 #[error(transparent)]
50 Files(#[from] FilesError),
51 #[error(transparent)]
53 Hostname(#[from] HostnameError),
54 #[error("loading CA material: {0}")]
56 CaMaterial(#[source] CaMaterialError),
57 #[error("checking kustomization path {0}: {1}")]
60 Stat(String, #[source] std::io::Error),
61 #[error("building kustomization {0:?}: {1}")]
63 Build(String, #[source] kustomizebackend::KustomizeError),
64 #[error("applying kustomize manifests from {0:?}: {1}")]
66 Apply(String, #[source] kubectl::KubectlError),
67 #[error("deleting kustomize manifests from {0:?}: {1}")]
69 Delete(String, #[source] kubectl::KubectlError),
70}
71
72pub struct Manager {
74 kustomize: Box<dyn KustomizeBackend + Send + Sync>,
75 kubectl: Box<dyn KubectlBackend + Send + Sync>,
76 ca_provider: Box<dyn CaProvider>,
77}
78
79impl Manager {
80 #[must_use]
83 pub fn new(
84 kustomize_backend: Box<dyn KustomizeBackend + Send + Sync>,
85 kubectl_backend: Box<dyn KubectlBackend + Send + Sync>,
86 ) -> Self {
87 Self::with_ca_provider(kustomize_backend, kubectl_backend, Box::new(PkiCaProvider))
88 }
89
90 #[must_use]
94 pub fn with_ca_provider(
95 kustomize_backend: Box<dyn KustomizeBackend + Send + Sync>,
96 kubectl_backend: Box<dyn KubectlBackend + Send + Sync>,
97 ca_provider: Box<dyn CaProvider>,
98 ) -> Self {
99 Self {
100 kustomize: kustomize_backend,
101 kubectl: kubectl_backend,
102 ca_provider,
103 }
104 }
105
106 #[must_use]
113 pub fn new_from_options(
114 kustomize_backend: Option<Box<dyn KustomizeBackend + Send + Sync>>,
115 kubectl_backend: Option<Box<dyn KubectlBackend + Send + Sync>>,
116 ) -> Self {
117 let kustomize_backend =
118 kustomize_backend.unwrap_or_else(|| panic!("kustomize backend cannot be nil"));
119 let kubectl_backend =
120 kubectl_backend.unwrap_or_else(|| panic!("kubectl backend cannot be nil"));
121
122 Self::new(kustomize_backend, kubectl_backend)
123 }
124
125 #[must_use]
128 pub fn deployer_type(&self) -> &'static str {
129 DEPLOYER_TYPE
130 }
131
132 pub fn validate(
143 &self,
144 deployer_config: &DeployerConfig,
145 caproni_config: &CaproniConfig,
146 ) -> Result<(), KustomizeError> {
147 self.validate_config(deployer_config)?;
148
149 let kc = deployer_config
151 .kustomize
152 .as_ref()
153 .expect("kustomize config present");
154
155 if builtin::is_builtin_path(&kc.path) {
159 return builtin::validate_builtin_path(&kc.path).map_err(KustomizeError::from);
160 }
161
162 if kustomizebackend::remote::is_remote_target(&kc.path) {
167 if !kustomizebackend::remote::has_pinned_ref(&kc.path) {
168 tracing::warn!(
169 path = %kustomizebackend::remote::redact_credentials(&kc.path),
170 "remote kustomize target has no ?ref= pin; the build tracks the repository's default branch and is not reproducible"
171 );
172 }
173
174 return Ok(());
175 }
176
177 let scheme = kustomizebackend::remote::url_scheme(&kc.path);
181 if !scheme.is_empty() {
182 return Err(KustomizeError::UnsupportedScheme(
183 kustomizebackend::remote::redact_credentials(&kc.path),
184 scheme,
185 ));
186 }
187
188 let path = caproni_config.resolve_path(&kc.path);
189
190 match std::fs::metadata(&path) {
191 Ok(info) => {
192 if info.is_dir() {
193 Ok(())
194 } else {
195 Err(KustomizeError::PathNotFound(path))
196 }
197 }
198 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
199 Err(KustomizeError::PathNotFound(path))
200 }
201 Err(e) => Err(KustomizeError::Stat(path, e)),
202 }
203 }
204
205 pub fn deploy(
213 &self,
214 deployer_config: &DeployerConfig,
215 cfg: &Container,
216 ) -> Result<(), KustomizeError> {
217 self.validate_config(deployer_config)?;
218
219 let kc = deployer_config
220 .kustomize
221 .as_ref()
222 .expect("kustomize config present");
223
224 let material = self.load_ca_material(&kc.path, &cfg.global.profile_name)?;
228
229 let (manifests, path, namespace) =
230 self.build_manifests(deployer_config, &cfg.caproni, material.as_ref())?;
231
232 tracing::info!(path = %path, namespace = %namespace, "applying kustomize manifests");
233
234 self.kubectl
235 .apply(&manifests, &namespace)
236 .map_err(|e| KustomizeError::Apply(path.clone(), e))?;
237
238 tracing::info!(path = %path, "kustomize manifests applied successfully");
239
240 Ok(())
241 }
242
243 fn load_ca_material(
247 &self,
248 path: &str,
249 profile_name: &str,
250 ) -> Result<Option<CaMaterial>, KustomizeError> {
251 if !builtin::overlay_needs_ca(path) {
252 return Ok(None);
253 }
254
255 let (cert_pem, key_pem) = self
256 .ca_provider
257 .load(profile_name)
258 .map_err(KustomizeError::CaMaterial)?;
259
260 Ok(Some(CaMaterial { cert_pem, key_pem }))
261 }
262
263 pub fn undeploy(
271 &self,
272 deployer_config: &DeployerConfig,
273 caproni_config: &CaproniConfig,
274 ) -> Result<(), KustomizeError> {
275 let (manifests, path, namespace) =
280 self.build_manifests(deployer_config, caproni_config, None)?;
281
282 tracing::info!(path = %path, namespace = %namespace, "deleting kustomize manifests");
283
284 self.kubectl
285 .delete_manifest(&manifests, &namespace)
286 .map_err(|e| KustomizeError::Delete(path.clone(), e))?;
287
288 Ok(())
289 }
290
291 pub fn status(
306 &self,
307 deployer_config: &DeployerConfig,
308 caproni_config: &CaproniConfig,
309 ) -> Result<Status, KustomizeError> {
310 self.validate_config(deployer_config)?;
311
312 let kc = deployer_config
313 .kustomize
314 .as_ref()
315 .expect("kustomize config present");
316
317 if kustomizebackend::remote::is_remote_target(&kc.path) {
324 return Ok(Status {
325 state: State::Deployed,
326 deployer_type: DEPLOYER_TYPE.to_string(),
327 });
328 }
329
330 let (build_dir, cleanup) = resolve_build_source(kc, caproni_config, None)?;
333
334 let build_result = self.kustomize.build(build_dir.to_string_lossy().as_ref());
335
336 cleanup();
337
338 match build_result {
339 Ok(_) => Ok(Status {
340 state: State::Deployed,
341 deployer_type: DEPLOYER_TYPE.to_string(),
342 }),
343 Err(e) => {
344 tracing::debug!(path = %kc.path, error = %e, "kustomize build failed, treating as not deployed");
345
346 Ok(Status {
347 state: State::NotDeployed,
348 deployer_type: DEPLOYER_TYPE.to_string(),
349 })
350 }
351 }
352 }
353
354 fn validate_config(&self, deployer_config: &DeployerConfig) -> Result<(), KustomizeError> {
357 if deployer_config.deployer_type != DEPLOYER_TYPE || deployer_config.kustomize.is_none() {
358 return Err(KustomizeError::NotKustomizeDeployer);
359 }
360
361 let kc = deployer_config.kustomize.as_ref().expect("checked above");
362
363 if kc.path.is_empty() {
364 return Err(KustomizeError::MissingPath);
365 }
366
367 files::validate_files(kc)?;
368
369 Ok(())
370 }
371
372 fn build_manifests(
379 &self,
380 deployer_config: &DeployerConfig,
381 caproni_config: &CaproniConfig,
382 material: Option<&CaMaterial>,
383 ) -> Result<(Vec<u8>, String, String), KustomizeError> {
384 self.validate_config(deployer_config)?;
385
386 let kc = deployer_config.kustomize.as_ref().expect("checked above");
387
388 let namespace = kc.namespace.clone();
389
390 let path = kustomizebackend::remote::redact_credentials(&kc.path);
393
394 let (build_dir, cleanup) = resolve_build_source(kc, caproni_config, material)?;
395
396 let build_result = self.kustomize.build(build_dir.to_string_lossy().as_ref());
397
398 cleanup();
399
400 let manifests = build_result.map_err(|e| KustomizeError::Build(path.clone(), e))?;
401
402 Ok((manifests, path, namespace))
403 }
404}
405
406fn resolve_build_source(
415 kc: &KustomizeConfig,
416 caproni_config: &CaproniConfig,
417 material: Option<&CaMaterial>,
418) -> Result<(PathBuf, Cleanup), KustomizeError> {
419 let path = &kc.path;
420
421 if kustomizebackend::remote::is_remote_target(path) {
425 return Ok((PathBuf::from(path), no_cleanup()));
426 }
427
428 if !builtin::is_builtin_path(path) {
429 let src = caproni_config.resolve_path(path);
430
431 if !kc.files.is_empty() {
432 let (dir, cleanup) = files::materialize_with_files(&src, &kc.files)?;
433 return Ok((dir, cleanup));
434 }
435
436 return Ok((PathBuf::from(src), no_cleanup()));
437 }
438
439 let (dir, cleanup) = builtin::materialize_builtin_overlay(path)?;
440
441 if builtin::overlay_needs_ca(path)
444 && let Err(e) = camaterial::write_ca_material(&dir, material)
445 {
446 cleanup();
447 return Err(KustomizeError::CaMaterial(e));
448 }
449
450 if builtin::overlay_needs_hostname(path) {
451 let primary_hostname = hostname::primary_hostname(caproni_config);
452 if let Err(e) = hostname::write_overlay_hostname(&dir, &primary_hostname) {
453 cleanup();
454 return Err(KustomizeError::from(e));
455 }
456 }
457
458 Ok((dir, cleanup))
459}
460
461fn no_cleanup() -> Cleanup {
462 Box::new(|| {})
463}
464
465impl crate::deployer::Manager for Manager {
467 fn deployer_type(&self) -> String {
468 DEPLOYER_TYPE.to_string()
469 }
470
471 fn deploy(
472 &self,
473 deployer_config: &DeployerConfig,
474 cfg: Option<&Container>,
475 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
476 let cfg = cfg.ok_or("kustomize deploy needs the resolved configuration")?;
477
478 Manager::deploy(self, deployer_config, cfg).map_err(Into::into)
479 }
480
481 fn undeploy(
482 &self,
483 deployer_config: &DeployerConfig,
484 caproni_config: &CaproniConfig,
485 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
486 Manager::undeploy(self, deployer_config, caproni_config).map_err(Into::into)
487 }
488
489 fn status(
490 &self,
491 deployer_config: &DeployerConfig,
492 caproni_config: &CaproniConfig,
493 ) -> Result<Status, Box<dyn std::error::Error + Send + Sync>> {
494 Manager::status(self, deployer_config, caproni_config).map_err(Into::into)
495 }
496
497 fn validate(
498 &self,
499 deployer_config: &DeployerConfig,
500 caproni_config: &CaproniConfig,
501 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
502 Manager::validate(self, deployer_config, caproni_config).map_err(Into::into)
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use std::path::Path;
509 use std::sync::{Arc, Mutex};
510
511 use xianyun_backends::kubectl::KubectlError;
512 use xianyun_backends::kustomize::KustomizeError as BackendKustomizeError;
513 use xianyun_core::config::container::{Container, new_container};
514 use xianyun_core::config::global::GlobalConfig;
515 use xianyun_core::config::latest::{CaproniConfig, DeployerConfig, KustomizeConfig};
516 use xianyun_core::config::types::NetworkConfig;
517
518 use super::*;
519 use crate::deployer::kustomize::camaterial::CaMaterialError;
520 use crate::deployer::kustomize::test_support::{MockKubectlBackend, MockKustomizeBackend};
521
522 const RENDERED_MANIFESTS: &[u8] =
523 b"apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: example\n";
524
525 #[derive(Default)]
526 struct FakeCaProvider {
527 cert_pem: Vec<u8>,
528 key_pem: Vec<u8>,
529 err: Option<String>,
530 }
531
532 impl CaProvider for FakeCaProvider {
533 fn load(&self, _profile_name: &str) -> Result<(Vec<u8>, Vec<u8>), CaMaterialError> {
534 if let Some(msg) = &self.err {
535 return Err(CaMaterialError::Write(std::io::Error::other(msg.clone())));
536 }
537
538 Ok((self.cert_pem.clone(), self.key_pem.clone()))
539 }
540 }
541
542 fn backend_test_error() -> BackendKustomizeError {
543 BackendKustomizeError::BuildFailed("test".to_string(), "backend failed".to_string())
544 }
545
546 fn kubectl_test_error() -> KubectlError {
547 KubectlError::JobFailed("backend failed".to_string())
548 }
549
550 fn new_config(config_dir: &str) -> CaproniConfig {
551 CaproniConfig {
552 config_dir: config_dir.to_string(),
553 cluster: xianyun_core::config::types::ClusterConfig {
554 network: Some(NetworkConfig {
555 hostname: "gitlab.test".to_string(),
556 additional_hostnames: Vec::new(),
557 cluster_domain: String::new(),
558 }),
559 ..Default::default()
560 },
561 ..Default::default()
562 }
563 }
564
565 fn new_test_container(config_dir: &str) -> Container {
566 new_container(GlobalConfig::default(), new_config(config_dir))
567 }
568
569 fn kustomize_deployer(path: &str, namespace: &str) -> DeployerConfig {
570 DeployerConfig {
571 deployer_type: DEPLOYER_TYPE.to_string(),
572 kustomize: Some(KustomizeConfig {
573 path: path.to_string(),
574 namespace: namespace.to_string(),
575 ..Default::default()
576 }),
577 ..Default::default()
578 }
579 }
580
581 fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
582 if let Some(s) = payload.downcast_ref::<&str>() {
583 (*s).to_string()
584 } else if let Some(s) = payload.downcast_ref::<String>() {
585 s.clone()
586 } else {
587 "unknown panic payload".to_string()
588 }
589 }
590
591 #[test]
593 fn test_type() {
594 let mgr = Manager::new(
595 Box::new(MockKustomizeBackend::new()),
596 Box::new(MockKubectlBackend::new()),
597 );
598 assert_eq!(mgr.deployer_type(), "kustomize");
599 }
600
601 #[test]
603 fn test_new_manager_nil_backends() {
604 let result = std::panic::catch_unwind(|| {
605 let _ = Manager::new_from_options(None, Some(Box::new(MockKubectlBackend::new())));
606 });
607 let payload = result.expect_err("expected a panic for a nil kustomize backend");
608 assert_eq!(panic_message(&*payload), "kustomize backend cannot be nil");
609
610 let result = std::panic::catch_unwind(|| {
611 let _ = Manager::new_from_options(Some(Box::new(MockKustomizeBackend::new())), None);
612 });
613 let payload = result.expect_err("expected a panic for a nil kubectl backend");
614 assert_eq!(panic_message(&*payload), "kubectl backend cannot be nil");
615 }
616
617 #[test]
619 fn test_validate() {
620 let tmp = tempfile::tempdir().unwrap();
621 let dir = tmp.path();
622
623 let not_a_dir = dir.join("kustomization.yaml");
624 std::fs::write(¬_a_dir, "kind: Kustomization\n").unwrap();
625
626 struct Case {
627 name: &'static str,
628 cfg: DeployerConfig,
629 want_err: Option<&'static str>,
630 }
631
632 let cases = vec![
633 Case {
634 name: "valid existing directory",
635 cfg: kustomize_deployer(dir.to_str().unwrap(), ""),
636 want_err: None,
637 },
638 Case {
639 name: "wrong type",
640 cfg: DeployerConfig {
641 deployer_type: "helm".to_string(),
642 kustomize: Some(KustomizeConfig {
643 path: dir.to_str().unwrap().to_string(),
644 ..Default::default()
645 }),
646 ..Default::default()
647 },
648 want_err: Some("not_kustomize"),
649 },
650 Case {
651 name: "missing kustomize block",
652 cfg: DeployerConfig {
653 deployer_type: DEPLOYER_TYPE.to_string(),
654 ..Default::default()
655 },
656 want_err: Some("not_kustomize"),
657 },
658 Case {
659 name: "empty path",
660 cfg: kustomize_deployer("", ""),
661 want_err: Some("missing_path"),
662 },
663 Case {
664 name: "path does not exist",
665 cfg: kustomize_deployer(dir.join("missing").to_str().unwrap(), ""),
666 want_err: Some("path_not_found"),
667 },
668 Case {
669 name: "path is a file",
670 cfg: kustomize_deployer(not_a_dir.to_str().unwrap(), ""),
671 want_err: Some("path_not_found"),
672 },
673 Case {
674 name: "remote target with pinned ref",
675 cfg: kustomize_deployer(
676 "https://gitlab.com/org/repo.git//overlays/dev?ref=v1.2.0",
677 "",
678 ),
679 want_err: None,
680 },
681 Case {
682 name: "remote target with version synonym",
683 cfg: kustomize_deployer(
684 "https://github.com/owner/repo//overlays/dev?version=v2.0.0",
685 "",
686 ),
687 want_err: None,
688 },
689 Case {
690 name: "remote target without ref is allowed but unreproducible",
691 cfg: kustomize_deployer("https://gitlab.com/org/repo.git//overlays/dev", ""),
692 want_err: None,
693 },
694 Case {
695 name: "unsupported url scheme",
696 cfg: kustomize_deployer("s3://bucket/overlays/dev", ""),
697 want_err: Some("unsupported_scheme"),
698 },
699 Case {
700 name: "relative path with colon is a local path",
701 cfg: kustomize_deployer("overlays:v2/dev", ""),
702 want_err: Some("path_not_found"),
703 },
704 Case {
705 name: "go-getter git prefix is an unsupported scheme",
706 cfg: kustomize_deployer("git::https://example.com/org/repo", ""),
707 want_err: Some("unsupported_scheme"),
708 },
709 Case {
710 name: "remote target with empty ref is allowed but unreproducible",
711 cfg: kustomize_deployer("https://github.com/owner/repo?ref=", ""),
712 want_err: None,
713 },
714 ];
715
716 for case in cases {
717 let mgr = Manager::new(
718 Box::new(MockKustomizeBackend::new()),
719 Box::new(MockKubectlBackend::new()),
720 );
721
722 let result = mgr.validate(&case.cfg, &new_config(""));
723
724 match case.want_err {
725 None => {
726 result.unwrap_or_else(|e| panic!("{}: unexpected error: {e}", case.name));
727 }
728 Some("not_kustomize") => assert!(
729 matches!(result, Err(KustomizeError::NotKustomizeDeployer)),
730 "{}: expected NotKustomizeDeployer, got {result:?}",
731 case.name
732 ),
733 Some("missing_path") => assert!(
734 matches!(result, Err(KustomizeError::MissingPath)),
735 "{}: expected MissingPath, got {result:?}",
736 case.name
737 ),
738 Some("path_not_found") => assert!(
739 matches!(result, Err(KustomizeError::PathNotFound(_))),
740 "{}: expected PathNotFound, got {result:?}",
741 case.name
742 ),
743 Some("unsupported_scheme") => assert!(
744 matches!(result, Err(KustomizeError::UnsupportedScheme(_, _))),
745 "{}: expected UnsupportedScheme, got {result:?}",
746 case.name
747 ),
748 Some(other) => unreachable!("unknown want_err case: {other}"),
749 }
750 }
751 }
752
753 #[test]
755 fn test_deploy() {
756 {
758 let path = Path::new("/repo").join("manifests");
759 let path_str = path.to_str().unwrap().to_string();
760 let expected_path = path_str.clone();
761
762 let kbackend = MockKustomizeBackend::new().expect_build(move |dir| {
763 assert_eq!(dir, expected_path);
764 Ok(RENDERED_MANIFESTS.to_vec())
765 });
766 let kctl = MockKubectlBackend::new().expect_apply(|manifests, namespace| {
767 assert_eq!(manifests, RENDERED_MANIFESTS);
768 assert_eq!(namespace, "gitlab");
769 Ok(())
770 });
771
772 let mgr = Manager::new(Box::new(kbackend), Box::new(kctl));
773
774 mgr.deploy(
775 &kustomize_deployer(&path_str, "gitlab"),
776 &new_test_container(""),
777 )
778 .unwrap();
779 }
780
781 {
783 let target = "https://gitlab.com/org/repo.git//overlays/dev?ref=v1.2.0";
784
785 let kbackend = MockKustomizeBackend::new().expect_build(move |dir| {
786 assert_eq!(dir, target);
787 Ok(RENDERED_MANIFESTS.to_vec())
788 });
789 let kctl = MockKubectlBackend::new().expect_apply(|manifests, namespace| {
790 assert_eq!(manifests, RENDERED_MANIFESTS);
791 assert_eq!(namespace, "gitlab");
792 Ok(())
793 });
794
795 let mgr = Manager::new(Box::new(kbackend), Box::new(kctl));
796
797 mgr.deploy(
798 &kustomize_deployer(target, "gitlab"),
799 &new_test_container("/some/config/dir"),
800 )
801 .unwrap();
802 }
803
804 {
806 let resolved = Path::new("/conf").join("infra");
807 let resolved_str = resolved.to_str().unwrap().to_string();
808 let expected = resolved_str.clone();
809
810 let kbackend = MockKustomizeBackend::new().expect_build(move |dir| {
811 assert_eq!(dir, expected);
812 Ok(RENDERED_MANIFESTS.to_vec())
813 });
814 let kctl = MockKubectlBackend::new().expect_apply(|manifests, namespace| {
815 assert_eq!(manifests, RENDERED_MANIFESTS);
816 assert_eq!(namespace, "");
817 Ok(())
818 });
819
820 let mgr = Manager::new(Box::new(kbackend), Box::new(kctl));
821
822 mgr.deploy(
823 &kustomize_deployer("infra", ""),
824 &new_test_container("/conf"),
825 )
826 .unwrap();
827 }
828
829 {
831 let mgr = Manager::new(
832 Box::new(MockKustomizeBackend::new()),
833 Box::new(MockKubectlBackend::new()),
834 );
835
836 let cfg = DeployerConfig {
837 deployer_type: "helm".to_string(),
838 kustomize: Some(KustomizeConfig {
839 path: "x".to_string(),
840 ..Default::default()
841 }),
842 ..Default::default()
843 };
844
845 let err = mgr.deploy(&cfg, &new_test_container("")).unwrap_err();
846 assert!(matches!(err, KustomizeError::NotKustomizeDeployer));
847 }
848
849 {
851 let kbackend =
852 MockKustomizeBackend::new().expect_build(|_dir| Err(backend_test_error()));
853 let kctl = MockKubectlBackend::new();
854
855 let mgr = Manager::new(Box::new(kbackend), Box::new(kctl));
856
857 let err = mgr
858 .deploy(&kustomize_deployer("/repo", ""), &new_test_container(""))
859 .unwrap_err();
860 assert!(matches!(err, KustomizeError::Build(_, _)));
861 }
862
863 {
865 let kbackend =
866 MockKustomizeBackend::new().expect_build(|_dir| Ok(RENDERED_MANIFESTS.to_vec()));
867 let kctl = MockKubectlBackend::new()
868 .expect_apply(|_manifests, _namespace| Err(kubectl_test_error()));
869
870 let mgr = Manager::new(Box::new(kbackend), Box::new(kctl));
871
872 let err = mgr
873 .deploy(&kustomize_deployer("/repo", ""), &new_test_container(""))
874 .unwrap_err();
875 assert!(matches!(err, KustomizeError::Apply(_, _)));
876 }
877 }
878
879 #[test]
881 fn test_deploy_builtin_dev_ca() {
882 let cert = b"test-cert-pem".to_vec();
883 let key = b"test-key-pem".to_vec();
884
885 {
887 let got: Arc<Mutex<(Vec<u8>, Vec<u8>)>> =
888 Arc::new(Mutex::new((Vec::new(), Vec::new())));
889 let got_cap = got.clone();
890
891 let kbackend = MockKustomizeBackend::new().expect_build(move |dir| {
892 let cert = std::fs::read(Path::new(dir).join("tls.crt")).unwrap_or_default();
893 let key = std::fs::read(Path::new(dir).join("tls.key")).unwrap_or_default();
894 *got_cap.lock().unwrap() = (cert, key);
895
896 Ok(RENDERED_MANIFESTS.to_vec())
897 });
898 let kctl = MockKubectlBackend::new().expect_apply(|manifests, namespace| {
899 assert_eq!(manifests, RENDERED_MANIFESTS);
900 assert_eq!(namespace, "cert-manager");
901 Ok(())
902 });
903
904 let ca_provider = FakeCaProvider {
905 cert_pem: cert.clone(),
906 key_pem: key.clone(),
907 err: None,
908 };
909
910 let mgr = Manager::with_ca_provider(
911 Box::new(kbackend),
912 Box::new(kctl),
913 Box::new(ca_provider),
914 );
915
916 mgr.deploy(
917 &kustomize_deployer("builtin:dev-ca", "cert-manager"),
918 &new_test_container(""),
919 )
920 .unwrap();
921
922 let (got_cert, got_key) = got.lock().unwrap().clone();
923 assert_eq!(got_cert, cert, "tls.crt must contain the runtime CA cert");
924 assert_eq!(got_key, key, "tls.key must contain the runtime CA key");
925 }
926
927 {
929 let ca_provider = FakeCaProvider {
930 cert_pem: Vec::new(),
931 key_pem: Vec::new(),
932 err: Some("backend failed".to_string()),
933 };
934
935 let mgr = Manager::with_ca_provider(
936 Box::new(MockKustomizeBackend::new()),
937 Box::new(MockKubectlBackend::new()),
938 Box::new(ca_provider),
939 );
940
941 let err = mgr
942 .deploy(
943 &kustomize_deployer("builtin:dev-ca", "cert-manager"),
944 &new_test_container(""),
945 )
946 .unwrap_err();
947 assert!(matches!(err, KustomizeError::CaMaterial(_)));
948 }
949 }
950
951 #[test]
953 fn test_undeploy_builtin_dev_ca_writes_placeholders() {
954 let present: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
955 let present_cap = present.clone();
956
957 let kbackend = MockKustomizeBackend::new().expect_build(move |dir| {
960 *present_cap.lock().unwrap() = Path::new(dir).join("tls.crt").is_file();
961 Ok(RENDERED_MANIFESTS.to_vec())
962 });
963 let kctl = MockKubectlBackend::new().expect_delete_manifest(|manifests, namespace| {
964 assert_eq!(manifests, RENDERED_MANIFESTS);
965 assert_eq!(namespace, "cert-manager");
966 Ok(())
967 });
968
969 let mgr = Manager::with_ca_provider(
970 Box::new(kbackend),
971 Box::new(kctl),
972 Box::new(FakeCaProvider::default()),
973 );
974
975 mgr.undeploy(
976 &kustomize_deployer("builtin:dev-ca", "cert-manager"),
977 &new_config(""),
978 )
979 .unwrap();
980
981 assert!(
982 *present.lock().unwrap(),
983 "placeholder tls.crt must exist so kustomize can build during undeploy"
984 );
985 }
986
987 #[test]
989 fn test_undeploy() {
990 {
992 let kbackend = MockKustomizeBackend::new().expect_build(|dir| {
993 assert_eq!(dir, "/repo");
994 Ok(RENDERED_MANIFESTS.to_vec())
995 });
996 let kctl = MockKubectlBackend::new().expect_delete_manifest(|manifests, namespace| {
997 assert_eq!(manifests, RENDERED_MANIFESTS);
998 assert_eq!(namespace, "gitlab");
999 Ok(())
1000 });
1001
1002 let mgr = Manager::new(Box::new(kbackend), Box::new(kctl));
1003
1004 mgr.undeploy(&kustomize_deployer("/repo", "gitlab"), &new_config(""))
1005 .unwrap();
1006 }
1007
1008 {
1010 let mgr = Manager::new(
1011 Box::new(MockKustomizeBackend::new()),
1012 Box::new(MockKubectlBackend::new()),
1013 );
1014
1015 let cfg = DeployerConfig {
1016 deployer_type: "helm".to_string(),
1017 kustomize: Some(KustomizeConfig {
1018 path: "x".to_string(),
1019 ..Default::default()
1020 }),
1021 ..Default::default()
1022 };
1023
1024 let err = mgr.undeploy(&cfg, &new_config("")).unwrap_err();
1025 assert!(matches!(err, KustomizeError::NotKustomizeDeployer));
1026 }
1027
1028 {
1030 let kbackend =
1031 MockKustomizeBackend::new().expect_build(|_dir| Ok(RENDERED_MANIFESTS.to_vec()));
1032 let kctl = MockKubectlBackend::new()
1033 .expect_delete_manifest(|_manifests, _namespace| Err(kubectl_test_error()));
1034
1035 let mgr = Manager::new(Box::new(kbackend), Box::new(kctl));
1036
1037 let err = mgr
1038 .undeploy(&kustomize_deployer("/repo", ""), &new_config(""))
1039 .unwrap_err();
1040 assert!(matches!(err, KustomizeError::Delete(_, _)));
1041 }
1042 }
1043
1044 #[test]
1046 fn test_status() {
1047 {
1049 let kbackend = MockKustomizeBackend::new().expect_build(|dir| {
1050 assert_eq!(dir, "/repo");
1051 Ok(RENDERED_MANIFESTS.to_vec())
1052 });
1053
1054 let mgr = Manager::new(Box::new(kbackend), Box::new(MockKubectlBackend::new()));
1055
1056 let status = mgr
1057 .status(&kustomize_deployer("/repo", ""), &new_config(""))
1058 .unwrap();
1059 assert_eq!(status.state, State::Deployed);
1060 assert_eq!(status.deployer_type, "kustomize");
1061 }
1062
1063 {
1065 let kbackend =
1066 MockKustomizeBackend::new().expect_build(|_dir| Err(backend_test_error()));
1067
1068 let mgr = Manager::new(Box::new(kbackend), Box::new(MockKubectlBackend::new()));
1069
1070 let status = mgr
1071 .status(&kustomize_deployer("/repo", ""), &new_config(""))
1072 .unwrap();
1073 assert_eq!(status.state, State::NotDeployed);
1074 }
1075
1076 {
1078 let mgr = Manager::new(
1082 Box::new(MockKustomizeBackend::new()),
1083 Box::new(MockKubectlBackend::new()),
1084 );
1085
1086 let target = "https://gitlab.com/org/repo.git//overlays/dev?ref=v1.2.0";
1087
1088 let status = mgr
1089 .status(&kustomize_deployer(target, ""), &new_config(""))
1090 .unwrap();
1091 assert_eq!(status.state, State::Deployed);
1092 assert_eq!(status.deployer_type, "kustomize");
1093 }
1094
1095 {
1097 let mgr = Manager::new(
1098 Box::new(MockKustomizeBackend::new()),
1099 Box::new(MockKubectlBackend::new()),
1100 );
1101
1102 let cfg = DeployerConfig {
1103 deployer_type: "helm".to_string(),
1104 kustomize: Some(KustomizeConfig {
1105 path: "x".to_string(),
1106 ..Default::default()
1107 }),
1108 ..Default::default()
1109 };
1110
1111 let err = mgr.status(&cfg, &new_config("")).unwrap_err();
1112 assert!(matches!(err, KustomizeError::NotKustomizeDeployer));
1113 }
1114 }
1115}