1use std::path::Path;
6
7use thiserror::Error;
8use xianyun_backends::kubectl::Backend as KubectlBackend;
9use xianyun_core::config::latest::{CaproniConfig, ReloaderConfig};
10use xianyun_executor::exec::streamed::ExecStreamedOpts;
11use xianyun_executor::exec::{CommandRunner, ExecError};
12
13use crate::reloader::{State, Status};
14
15pub const RELOADER_TYPE: &str = "skaffold";
17
18#[derive(Debug, Error)]
20pub enum SkaffoldError {
21 #[error("reloader type is not 'skaffold'")]
23 InvalidReloaderType,
24 #[error("skaffold configuration is missing")]
26 SkaffoldConfigMissing,
27 #[error("config_file not configured")]
29 ConfigFileNotConfigured,
30 #[error("repository not found: {0}")]
32 RepositoryNotFound(String),
33 #[error("skaffold config file not found: {0}")]
35 ConfigFileNotFound(String),
36 #[error("skaffold version too old: {1}. Please upgrade skaffold")]
38 SkaffoldVersionTooOld(String, #[source] xianyun_update::version::VersionError),
39 #[error("checking repository directory: {0}")]
42 CheckRepositoryDir(#[source] std::io::Error),
43 #[error("running skaffold dev: {0}")]
45 RunSkaffoldDev(#[source] ExecError),
46}
47
48pub struct Manager {
50 #[allow(dead_code)]
51 kubectl: Box<dyn KubectlBackend + Send + Sync>,
52}
53
54impl Manager {
55 #[must_use]
57 pub fn new(kubectl_backend: Box<dyn KubectlBackend + Send + Sync>) -> Self {
58 Self {
59 kubectl: kubectl_backend,
60 }
61 }
62
63 #[must_use]
65 pub fn reloader_type(&self) -> &'static str {
66 RELOADER_TYPE
67 }
68
69 pub fn check_preconditions(
80 &self,
81 executor: &dyn CommandRunner,
82 _reloader_config: &ReloaderConfig,
83 _caproni_config: &CaproniConfig,
84 get_tool_version: &dyn Fn(&str) -> Option<String>,
85 ) -> Result<(), SkaffoldError> {
86 self.check_skaffold_version(executor, get_tool_version)
87 }
88
89 fn check_skaffold_version(
90 &self,
91 executor: &dyn CommandRunner,
92 get_tool_version: &dyn Fn(&str) -> Option<String>,
93 ) -> Result<(), SkaffoldError> {
94 let Some(required_version) = get_tool_version("GL_ASDF_SKAFFOLD_VERSION") else {
95 return Ok(());
96 };
97
98 let Ok(installed_version) = executor.read_stdout("skaffold", &["version".to_string()])
101 else {
102 return Ok(());
103 };
104
105 xianyun_update::version::check_minimum_version(&installed_version, &required_version)
106 .map_err(|e| SkaffoldError::SkaffoldVersionTooOld(installed_version.clone(), e))
107 }
108
109 pub fn run(
118 &self,
119 executor: &dyn CommandRunner,
120 reloader_config: &ReloaderConfig,
121 caproni_config: &CaproniConfig,
122 kubeconfig_path: &str,
123 ) -> Result<(), SkaffoldError> {
124 if reloader_config.reloader_type != RELOADER_TYPE {
125 return Err(SkaffoldError::InvalidReloaderType);
126 }
127
128 let Some(skaffold) = reloader_config.skaffold.as_ref() else {
129 return Err(SkaffoldError::SkaffoldConfigMissing);
130 };
131
132 let repo_config = caproni_config
133 .repositories
134 .as_ref()
135 .and_then(|repos| repos.get(&reloader_config.repository))
136 .ok_or_else(|| SkaffoldError::RepositoryNotFound(reloader_config.repository.clone()))?;
137
138 if skaffold.config_file.is_empty() {
139 return Err(SkaffoldError::ConfigFileNotConfigured);
140 }
141
142 let mut dev_args = vec![
143 "dev".to_string(),
144 "--filename".to_string(),
145 skaffold.config_file.clone(),
146 ];
147 dev_args.extend(self.build_skaffold_args(reloader_config, kubeconfig_path));
148
149 tracing::info!(
150 reloader = %reloader_config.repository,
151 namespace = %skaffold.namespace,
152 config_file = %skaffold.config_file,
153 repository = %repo_config.directory,
154 "starting skaffold dev"
155 );
156
157 let repo_dir = caproni_config.resolve_path(&repo_config.directory);
158
159 let opts = ExecStreamedOpts {
160 use_scrolling_area: false,
161 process_descriptive_name: Some("skaffold".to_string()),
162 ..Default::default()
163 };
164
165 match executor.run_in_repository(&repo_dir, "skaffold", &dev_args, Some(opts)) {
166 Ok(()) => Ok(()),
167 Err(ExecError::Cancelled) => {
172 tracing::info!(
173 reloader = %reloader_config.repository,
174 "skaffold dev stopped due to context cancellation"
175 );
176 Ok(())
177 }
178 Err(e) => Err(SkaffoldError::RunSkaffoldDev(e)),
179 }
180 }
181
182 pub fn status(
192 &self,
193 reloader_config: &ReloaderConfig,
194 caproni_config: &CaproniConfig,
195 ) -> Result<Status, SkaffoldError> {
196 if reloader_config.reloader_type != RELOADER_TYPE {
197 return Err(SkaffoldError::InvalidReloaderType);
198 }
199
200 if reloader_config.skaffold.is_none() {
201 return Err(SkaffoldError::SkaffoldConfigMissing);
202 }
203
204 let Some(repo_config) = caproni_config
206 .repositories
207 .as_ref()
208 .and_then(|repos| repos.get(&reloader_config.repository))
209 else {
210 return Err(SkaffoldError::RepositoryNotFound(
211 reloader_config.repository.clone(),
212 ));
213 };
214
215 let repo_dir = caproni_config.resolve_path(&repo_config.directory);
216
217 match std::fs::metadata(&repo_dir) {
218 Ok(_) => Ok(Status {
219 state: State::Ready,
220 reloader_type: RELOADER_TYPE.to_string(),
221 }),
222 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Status {
223 state: State::NotReady,
224 reloader_type: RELOADER_TYPE.to_string(),
225 }),
226 Err(e) => Err(SkaffoldError::CheckRepositoryDir(e)),
227 }
228 }
229
230 fn build_skaffold_args(
236 &self,
237 reloader_config: &ReloaderConfig,
238 kubeconfig_path: &str,
239 ) -> Vec<String> {
240 let skaffold = reloader_config
241 .skaffold
242 .as_ref()
243 .expect("caller has already checked skaffold config is present");
244
245 let mut args = vec!["--namespace".to_string(), skaffold.namespace.clone()];
246
247 if !kubeconfig_path.is_empty() {
248 args.push("--kubeconfig".to_string());
249 args.push(kubeconfig_path.to_string());
250 }
251
252 args
253 }
254
255 pub fn validate(
264 &self,
265 reloader_config: &ReloaderConfig,
266 caproni_config: &CaproniConfig,
267 ) -> Result<(), SkaffoldError> {
268 if reloader_config.reloader_type != RELOADER_TYPE {
269 return Err(SkaffoldError::InvalidReloaderType);
270 }
271
272 let Some(skaffold) = reloader_config.skaffold.as_ref() else {
273 return Err(SkaffoldError::SkaffoldConfigMissing);
274 };
275
276 if skaffold.config_file.is_empty() {
277 return Err(SkaffoldError::ConfigFileNotConfigured);
278 }
279
280 let repo_config = caproni_config
281 .repositories
282 .as_ref()
283 .and_then(|repos| repos.get(&reloader_config.repository))
284 .ok_or_else(|| SkaffoldError::RepositoryNotFound(reloader_config.repository.clone()))?;
285
286 let repo_dir = caproni_config.resolve_path(&repo_config.directory);
287
288 if std::fs::metadata(&repo_dir).is_err() {
291 return Ok(());
292 }
293
294 let config_file_path = Path::new(&repo_dir).join(&skaffold.config_file);
295
296 if std::fs::metadata(&config_file_path).is_err() {
297 return Err(SkaffoldError::ConfigFileNotFound(
298 config_file_path.to_string_lossy().into_owned(),
299 ));
300 }
301
302 Ok(())
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use xianyun_core::config::latest::{
309 CaproniConfig, ReloaderConfig, RepositoryConfig, SkaffoldConfig,
310 };
311 use xianyun_core::orderedmap::OrderedMap;
312 use xianyun_executor::exec::ExecError;
313 use xianyun_testutil::MockCommandRunner;
314
315 use super::*;
316 use crate::reloader::mirror::test_support::MockKubectlBackend;
317
318 #[test]
320 fn test_type() {
321 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
322 assert_eq!(mgr.reloader_type(), "skaffold");
323 }
324
325 #[test]
327 fn test_check_preconditions_skaffold_version_check() {
328 struct Case {
329 name: &'static str,
330 required_version: &'static str,
331 installed_output: &'static str,
332 installed_err: bool,
333 want_too_old: bool,
334 want_err_contains: &'static str,
335 }
336
337 let cases = vec![
338 Case {
339 name: "version meets minimum",
340 required_version: "2.17.3",
341 installed_output: "v2.17.3",
342 installed_err: false,
343 want_too_old: false,
344 want_err_contains: "",
345 },
346 Case {
347 name: "version exceeds minimum",
348 required_version: "2.17.3",
349 installed_output: "v2.20.0",
350 installed_err: false,
351 want_too_old: false,
352 want_err_contains: "",
353 },
354 Case {
355 name: "version too old",
356 required_version: "2.17.3",
357 installed_output: "v1.39.0",
358 installed_err: false,
359 want_too_old: true,
360 want_err_contains: "v1.39.0",
361 },
362 Case {
363 name: "minor version too old",
364 required_version: "2.17.3",
365 installed_output: "v2.10.0",
366 installed_err: false,
367 want_too_old: true,
368 want_err_contains: "minimum required v2.17",
369 },
370 Case {
371 name: "skaffold not installed",
372 required_version: "2.17.3",
373 installed_output: "",
374 installed_err: true,
375 want_too_old: false,
376 want_err_contains: "",
377 },
378 Case {
379 name: "version key not configured",
380 required_version: "",
381 installed_output: "",
382 installed_err: false,
383 want_too_old: false,
384 want_err_contains: "",
385 },
386 ];
387
388 for case in cases {
389 let mock_exec = MockCommandRunner::new();
390
391 if !case.required_version.is_empty() {
392 if case.installed_err {
393 mock_exec.expect_read_stdout(
394 "skaffold",
395 &["version"],
396 Err(ExecError::CommandFailed(1)),
397 );
398 } else {
399 mock_exec.expect_read_stdout(
400 "skaffold",
401 &["version"],
402 Ok(case.installed_output.to_string()),
403 );
404 }
405 }
406
407 let get_tool_version = move |_: &str| {
408 if case.required_version.is_empty() {
409 None
410 } else {
411 Some(case.required_version.to_string())
412 }
413 };
414
415 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
416
417 let result = mgr.check_preconditions(
418 &mock_exec,
419 &ReloaderConfig::default(),
420 &CaproniConfig::default(),
421 &get_tool_version,
422 );
423
424 if case.want_too_old {
425 let err = result.unwrap_err();
426 assert!(
427 matches!(err, SkaffoldError::SkaffoldVersionTooOld(_, _)),
428 "{}: expected SkaffoldVersionTooOld, got {err:?}",
429 case.name
430 );
431 assert!(
432 err.to_string().contains(case.want_err_contains),
433 "{}: {err}",
434 case.name
435 );
436 } else {
437 result.unwrap_or_else(|e| panic!("{}: unexpected error: {e}", case.name));
438 }
439 }
440 }
441
442 #[test]
444 fn test_run_invalid_reloader_type() {
445 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
446 let mock_exec = MockCommandRunner::new();
447
448 let reloader_config = ReloaderConfig {
449 reloader_type: "mirror".to_string(),
450 ..Default::default()
451 };
452
453 let err = mgr
454 .run(&mock_exec, &reloader_config, &CaproniConfig::default(), "")
455 .unwrap_err();
456 assert!(matches!(err, SkaffoldError::InvalidReloaderType));
457 }
458
459 #[test]
461 fn test_run_missing_skaffold_config() {
462 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
463 let mock_exec = MockCommandRunner::new();
464
465 let reloader_config = ReloaderConfig {
466 reloader_type: RELOADER_TYPE.to_string(),
467 skaffold: None,
468 ..Default::default()
469 };
470
471 let err = mgr
472 .run(&mock_exec, &reloader_config, &CaproniConfig::default(), "")
473 .unwrap_err();
474 assert!(matches!(err, SkaffoldError::SkaffoldConfigMissing));
475 }
476
477 #[test]
479 fn test_run_missing_repository() {
480 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
481 let mock_exec = MockCommandRunner::new();
482
483 let reloader_config = ReloaderConfig {
484 reloader_type: RELOADER_TYPE.to_string(),
485 repository: "nonexistent".to_string(),
486 skaffold: Some(SkaffoldConfig {
487 namespace: "gitlab".to_string(),
488 config_file: "skaffold.yaml".to_string(),
489 }),
490 ..Default::default()
491 };
492
493 let caproni_config = CaproniConfig {
494 repositories: Some(OrderedMap::new()),
495 ..Default::default()
496 };
497
498 let err = mgr
499 .run(&mock_exec, &reloader_config, &caproni_config, "")
500 .unwrap_err();
501 assert!(matches!(err, SkaffoldError::RepositoryNotFound(_)));
502 }
503
504 #[test]
506 fn test_run_config_file_not_configured() {
507 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
508 let mock_exec = MockCommandRunner::new();
509
510 let mut repos = OrderedMap::new();
511 repos.set(
512 "gitlab".to_string(),
513 RepositoryConfig {
514 repository: "https://gitlab.com/gitlab-org/gitlab.git".to_string(),
515 directory: "./gitlab".to_string(),
516 ..Default::default()
517 },
518 );
519
520 let reloader_config = ReloaderConfig {
521 reloader_type: RELOADER_TYPE.to_string(),
522 repository: "gitlab".to_string(),
523 skaffold: Some(SkaffoldConfig {
524 namespace: "gitlab".to_string(),
525 config_file: String::new(),
526 }),
527 ..Default::default()
528 };
529
530 let caproni_config = CaproniConfig {
531 repositories: Some(repos),
532 ..Default::default()
533 };
534
535 let err = mgr
536 .run(&mock_exec, &reloader_config, &caproni_config, "")
537 .unwrap_err();
538 assert!(matches!(err, SkaffoldError::ConfigFileNotConfigured));
539 }
540
541 #[test]
543 fn test_run_success() {
544 let mock_exec = MockCommandRunner::new();
545 mock_exec.expect_run_in_repository_matching(
546 "skaffold",
547 |args| {
548 args.len() == 6
549 && args[1..].iter().map(String::as_str).collect::<Vec<_>>()
550 == [
551 "dev",
552 "--filename",
553 "skaffold.yaml",
554 "--namespace",
555 "gitlab",
556 ]
557 },
558 Ok(()),
559 );
560
561 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
562
563 let mut repos = OrderedMap::new();
564 repos.set(
565 "gitlab".to_string(),
566 RepositoryConfig {
567 repository: "https://gitlab.com/gitlab-org/gitlab.git".to_string(),
568 directory: "./gitlab".to_string(),
569 ..Default::default()
570 },
571 );
572
573 let reloader_config = ReloaderConfig {
574 reloader_type: RELOADER_TYPE.to_string(),
575 repository: "gitlab".to_string(),
576 skaffold: Some(SkaffoldConfig {
577 namespace: "gitlab".to_string(),
578 config_file: "skaffold.yaml".to_string(),
579 }),
580 ..Default::default()
581 };
582
583 let caproni_config = CaproniConfig {
584 repositories: Some(repos),
585 config_dir: "/tmp".to_string(),
586 ..Default::default()
587 };
588
589 mgr.run(&mock_exec, &reloader_config, &caproni_config, "")
590 .unwrap();
591 }
592
593 #[test]
595 fn test_run_pins_kubeconfig_via_flag() {
596 let mock_exec = MockCommandRunner::new();
597 mock_exec.expect_run_in_repository_matching(
598 "skaffold",
599 |args| {
600 args.len() == 8
601 && args[1..].iter().map(String::as_str).collect::<Vec<_>>()
602 == [
603 "dev",
604 "--filename",
605 "skaffold.yaml",
606 "--namespace",
607 "gitlab",
608 "--kubeconfig",
609 "/tmp/caproni-kubeconfig-123",
610 ]
611 },
612 Ok(()),
613 );
614
615 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
616
617 let mut repos = OrderedMap::new();
618 repos.set(
619 "gitlab".to_string(),
620 RepositoryConfig {
621 repository: "https://gitlab.com/gitlab-org/gitlab.git".to_string(),
622 directory: "./gitlab".to_string(),
623 ..Default::default()
624 },
625 );
626
627 let reloader_config = ReloaderConfig {
628 reloader_type: RELOADER_TYPE.to_string(),
629 repository: "gitlab".to_string(),
630 skaffold: Some(SkaffoldConfig {
631 namespace: "gitlab".to_string(),
632 config_file: "skaffold.yaml".to_string(),
633 }),
634 ..Default::default()
635 };
636
637 let caproni_config = CaproniConfig {
638 repositories: Some(repos),
639 config_dir: "/tmp".to_string(),
640 ..Default::default()
641 };
642
643 mgr.run(
644 &mock_exec,
645 &reloader_config,
646 &caproni_config,
647 "/tmp/caproni-kubeconfig-123",
648 )
649 .unwrap();
650 }
651
652 #[test]
654 fn test_run_context_cancellation() {
655 let mock_exec = MockCommandRunner::new();
656 mock_exec.expect_run_in_repository_matching(
657 "skaffold",
658 |_args| true,
659 Err(ExecError::Cancelled),
660 );
661
662 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
663
664 let mut repos = OrderedMap::new();
665 repos.set(
666 "gitlab".to_string(),
667 RepositoryConfig {
668 repository: "https://gitlab.com/gitlab-org/gitlab.git".to_string(),
669 directory: "./gitlab".to_string(),
670 ..Default::default()
671 },
672 );
673
674 let reloader_config = ReloaderConfig {
675 reloader_type: RELOADER_TYPE.to_string(),
676 repository: "gitlab".to_string(),
677 skaffold: Some(SkaffoldConfig {
678 namespace: "gitlab".to_string(),
679 config_file: "skaffold.yaml".to_string(),
680 }),
681 ..Default::default()
682 };
683
684 let caproni_config = CaproniConfig {
685 repositories: Some(repos),
686 config_dir: "/tmp".to_string(),
687 ..Default::default()
688 };
689
690 mgr.run(&mock_exec, &reloader_config, &caproni_config, "")
692 .unwrap();
693 }
694
695 #[test]
697 fn test_status_invalid_reloader_type() {
698 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
699
700 let reloader_config = ReloaderConfig {
701 reloader_type: "mirror".to_string(),
702 ..Default::default()
703 };
704
705 let err = mgr
706 .status(&reloader_config, &CaproniConfig::default())
707 .unwrap_err();
708 assert!(matches!(err, SkaffoldError::InvalidReloaderType));
709 }
710
711 #[test]
713 fn test_status_missing_skaffold_config() {
714 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
715
716 let reloader_config = ReloaderConfig {
717 reloader_type: RELOADER_TYPE.to_string(),
718 skaffold: None,
719 ..Default::default()
720 };
721
722 let err = mgr
723 .status(&reloader_config, &CaproniConfig::default())
724 .unwrap_err();
725 assert!(matches!(err, SkaffoldError::SkaffoldConfigMissing));
726 }
727
728 #[test]
730 fn test_status_repository_not_found() {
731 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
732
733 let reloader_config = ReloaderConfig {
734 reloader_type: RELOADER_TYPE.to_string(),
735 repository: "nonexistent".to_string(),
736 skaffold: Some(SkaffoldConfig {
737 namespace: "gitlab".to_string(),
738 config_file: "skaffold.yaml".to_string(),
739 }),
740 ..Default::default()
741 };
742
743 let caproni_config = CaproniConfig {
744 repositories: Some(OrderedMap::new()),
745 ..Default::default()
746 };
747
748 let err = mgr.status(&reloader_config, &caproni_config).unwrap_err();
749 assert!(matches!(err, SkaffoldError::RepositoryNotFound(_)));
750 }
751
752 #[test]
754 fn test_status_repository_not_cloned() {
755 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
756
757 let mut repos = OrderedMap::new();
758 repos.set(
759 "gitlab".to_string(),
760 RepositoryConfig {
761 repository: "https://gitlab.com/gitlab-org/gitlab.git".to_string(),
762 directory: "./nonexistent-repo".to_string(),
763 ..Default::default()
764 },
765 );
766
767 let reloader_config = ReloaderConfig {
768 reloader_type: RELOADER_TYPE.to_string(),
769 repository: "gitlab".to_string(),
770 skaffold: Some(SkaffoldConfig {
771 namespace: "gitlab".to_string(),
772 config_file: "skaffold.yaml".to_string(),
773 }),
774 ..Default::default()
775 };
776
777 let caproni_config = CaproniConfig {
778 repositories: Some(repos),
779 config_dir: "/tmp".to_string(),
780 ..Default::default()
781 };
782
783 let status = mgr.status(&reloader_config, &caproni_config).unwrap();
784 assert_eq!(status.state, State::NotReady);
785 assert_eq!(status.reloader_type, "skaffold");
786 }
787
788 #[test]
790 fn test_status_ready() {
791 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
792
793 let tmp_dir = tempfile::tempdir().unwrap();
794 let tmp_dir_str = tmp_dir.path().to_str().unwrap().to_string();
795
796 let mut repos = OrderedMap::new();
797 repos.set(
798 "gitlab".to_string(),
799 RepositoryConfig {
800 repository: "https://gitlab.com/gitlab-org/gitlab.git".to_string(),
801 directory: tmp_dir_str.clone(),
802 ..Default::default()
803 },
804 );
805
806 let reloader_config = ReloaderConfig {
807 reloader_type: RELOADER_TYPE.to_string(),
808 repository: "gitlab".to_string(),
809 skaffold: Some(SkaffoldConfig {
810 namespace: "gitlab".to_string(),
811 config_file: "skaffold.yaml".to_string(),
812 }),
813 ..Default::default()
814 };
815
816 let caproni_config = CaproniConfig {
817 repositories: Some(repos),
818 config_dir: tmp_dir_str,
819 ..Default::default()
820 };
821
822 let status = mgr.status(&reloader_config, &caproni_config).unwrap();
823 assert_eq!(status.state, State::Ready);
824 assert_eq!(status.reloader_type, "skaffold");
825 }
826
827 #[test]
829 fn test_validate_invalid_reloader_type() {
830 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
831
832 let reloader_config = ReloaderConfig {
833 reloader_type: "mirror".to_string(),
834 ..Default::default()
835 };
836
837 let err = mgr
838 .validate(&reloader_config, &CaproniConfig::default())
839 .unwrap_err();
840 assert!(matches!(err, SkaffoldError::InvalidReloaderType));
841 }
842
843 #[test]
845 fn test_validate_missing_skaffold_config() {
846 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
847
848 let reloader_config = ReloaderConfig {
849 reloader_type: RELOADER_TYPE.to_string(),
850 skaffold: None,
851 ..Default::default()
852 };
853
854 let err = mgr
855 .validate(&reloader_config, &CaproniConfig::default())
856 .unwrap_err();
857 assert!(matches!(err, SkaffoldError::SkaffoldConfigMissing));
858 }
859
860 #[test]
862 fn test_validate_config_file_not_configured() {
863 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
864
865 let reloader_config = ReloaderConfig {
866 reloader_type: RELOADER_TYPE.to_string(),
867 skaffold: Some(SkaffoldConfig {
868 namespace: "gitlab".to_string(),
869 config_file: String::new(),
870 }),
871 ..Default::default()
872 };
873
874 let err = mgr
875 .validate(&reloader_config, &CaproniConfig::default())
876 .unwrap_err();
877 assert!(matches!(err, SkaffoldError::ConfigFileNotConfigured));
878 }
879
880 #[test]
882 fn test_validate_repository_not_found() {
883 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
884
885 let reloader_config = ReloaderConfig {
886 reloader_type: RELOADER_TYPE.to_string(),
887 repository: "nonexistent".to_string(),
888 skaffold: Some(SkaffoldConfig {
889 namespace: "gitlab".to_string(),
890 config_file: "skaffold.yaml".to_string(),
891 }),
892 ..Default::default()
893 };
894
895 let caproni_config = CaproniConfig {
896 repositories: Some(OrderedMap::new()),
897 ..Default::default()
898 };
899
900 let err = mgr.validate(&reloader_config, &caproni_config).unwrap_err();
901 assert!(matches!(err, SkaffoldError::RepositoryNotFound(_)));
902 }
903
904 #[test]
906 fn test_validate_repository_not_cloned() {
907 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
908
909 let mut repos = OrderedMap::new();
910 repos.set(
911 "gitlab".to_string(),
912 RepositoryConfig {
913 repository: "https://gitlab.com/gitlab-org/gitlab.git".to_string(),
914 directory: "./nonexistent-repo".to_string(),
915 ..Default::default()
916 },
917 );
918
919 let reloader_config = ReloaderConfig {
920 reloader_type: RELOADER_TYPE.to_string(),
921 repository: "gitlab".to_string(),
922 skaffold: Some(SkaffoldConfig {
923 namespace: "gitlab".to_string(),
924 config_file: "skaffold.yaml".to_string(),
925 }),
926 ..Default::default()
927 };
928
929 let caproni_config = CaproniConfig {
930 repositories: Some(repos),
931 config_dir: "/tmp".to_string(),
932 ..Default::default()
933 };
934
935 mgr.validate(&reloader_config, &caproni_config).unwrap();
937 }
938
939 #[test]
941 fn test_validate_config_file_not_found() {
942 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
943
944 let tmp_dir = tempfile::tempdir().unwrap();
945 let tmp_dir_str = tmp_dir.path().to_str().unwrap().to_string();
946
947 let mut repos = OrderedMap::new();
948 repos.set(
949 "gitlab".to_string(),
950 RepositoryConfig {
951 repository: "https://gitlab.com/gitlab-org/gitlab.git".to_string(),
952 directory: tmp_dir_str.clone(),
953 ..Default::default()
954 },
955 );
956
957 let reloader_config = ReloaderConfig {
958 reloader_type: RELOADER_TYPE.to_string(),
959 repository: "gitlab".to_string(),
960 skaffold: Some(SkaffoldConfig {
961 namespace: "gitlab".to_string(),
962 config_file: "nonexistent-skaffold.yaml".to_string(),
963 }),
964 ..Default::default()
965 };
966
967 let caproni_config = CaproniConfig {
968 repositories: Some(repos),
969 config_dir: tmp_dir_str,
970 ..Default::default()
971 };
972
973 let err = mgr.validate(&reloader_config, &caproni_config).unwrap_err();
974 assert!(matches!(err, SkaffoldError::ConfigFileNotFound(_)));
975 }
976
977 #[test]
979 fn test_validate_success() {
980 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
981
982 let tmp_dir = tempfile::tempdir().unwrap();
983 let tmp_dir_str = tmp_dir.path().to_str().unwrap().to_string();
984
985 std::fs::write(
986 tmp_dir.path().join("skaffold.yaml"),
987 "apiVersion: skaffold/v2beta28\nkind: Config\n",
988 )
989 .unwrap();
990
991 let mut repos = OrderedMap::new();
992 repos.set(
993 "gitlab".to_string(),
994 RepositoryConfig {
995 repository: "https://gitlab.com/gitlab-org/gitlab.git".to_string(),
996 directory: tmp_dir_str.clone(),
997 ..Default::default()
998 },
999 );
1000
1001 let reloader_config = ReloaderConfig {
1002 reloader_type: RELOADER_TYPE.to_string(),
1003 repository: "gitlab".to_string(),
1004 skaffold: Some(SkaffoldConfig {
1005 namespace: "gitlab".to_string(),
1006 config_file: "skaffold.yaml".to_string(),
1007 }),
1008 ..Default::default()
1009 };
1010
1011 let caproni_config = CaproniConfig {
1012 repositories: Some(repos),
1013 config_dir: tmp_dir_str,
1014 ..Default::default()
1015 };
1016
1017 mgr.validate(&reloader_config, &caproni_config).unwrap();
1018 }
1019
1020 #[test]
1022 fn test_build_skaffold_args() {
1023 struct Case {
1024 name: &'static str,
1025 kubeconfig_path: &'static str,
1026 want: Vec<&'static str>,
1027 }
1028
1029 let cases = vec![
1030 Case {
1031 name: "without kubeconfig",
1032 kubeconfig_path: "",
1033 want: vec!["--namespace", "test-namespace"],
1034 },
1035 Case {
1036 name: "with kubeconfig",
1037 kubeconfig_path: "/tmp/caproni-kubeconfig-123",
1038 want: vec![
1039 "--namespace",
1040 "test-namespace",
1041 "--kubeconfig",
1042 "/tmp/caproni-kubeconfig-123",
1043 ],
1044 },
1045 ];
1046
1047 for case in cases {
1048 let mgr = Manager::new(Box::new(MockKubectlBackend::new()));
1049
1050 let reloader_config = ReloaderConfig {
1051 skaffold: Some(SkaffoldConfig {
1052 namespace: "test-namespace".to_string(),
1053 config_file: String::new(),
1054 }),
1055 ..Default::default()
1056 };
1057
1058 let args = mgr.build_skaffold_args(&reloader_config, case.kubeconfig_path);
1059 let want: Vec<String> = case.want.into_iter().map(str::to_string).collect();
1060 assert_eq!(args, want, "{}", case.name);
1061 }
1062 }
1063}