1use std::collections::{HashMap, HashSet};
16use std::path::Path;
17
18use serde_json::Value;
19
20use super::errors::ConfigError;
21use super::global::GlobalConfig;
22use super::latest::CaproniConfig;
23use super::merge::{apply_local_override, resolve_extends, resolve_local_override};
24use super::registry::{LATEST_VERSION, load_and_upgrade, supported_versions};
25use super::validate::validate_semantics;
26use crate::naming;
27use crate::variables::expand::expand_map;
28use crate::variables::predefined::Options;
29use crate::variables::resolver::{Lookup, ResolverLookup, new_resolver};
30
31const CLUSTER_KEY: &str = "cluster";
32const DRIVER_KEY: &str = "driver";
33const K3D_KEY: &str = "k3d";
34const COLIMA_KEY: &str = "colima";
35const K3S_KEY: &str = "k3s";
36const DISABLE_KEY: &str = "disable";
37
38fn validate_schema_v2(raw_config: &Value) -> Result<(), ConfigError> {
47 const SCHEMA_JSON: &str = include_str!("../../../../schema/config.v2.schema.json");
48
49 let schema_doc: Value = serde_json::from_str(SCHEMA_JSON)
50 .map_err(|err| ConfigError::SchemaValidation(format!("parsing schema: {err}")))?;
51
52 let compiled = jsonschema::validator_for(&schema_doc)
53 .map_err(|err| ConfigError::SchemaValidation(format!("compiling schema: {err}")))?;
54
55 let errors: Vec<String> = compiled
61 .iter_errors(raw_config)
62 .map(|e| {
63 let path = e.instance_path.to_string();
64 if path.is_empty() {
65 e.to_string()
66 } else {
67 format!("{path}: {e}")
68 }
69 })
70 .collect();
71
72 if errors.is_empty() {
73 Ok(())
74 } else {
75 Err(ConfigError::SchemaValidation(errors.join("; ")))
76 }
77}
78
79fn detect_config_version(raw_config: &Value) -> Result<i64, ConfigError> {
84 let map = raw_config.as_object().ok_or(ConfigError::MissingVersion)?;
85 let version = map.get("version").ok_or(ConfigError::MissingVersion)?;
86 version.as_i64().ok_or(ConfigError::InvalidVersion)
87}
88
89pub fn load_caproni_config(gc: &GlobalConfig) -> Result<CaproniConfig, ConfigError> {
95 load_caproni_config_with_fetcher(gc, &super::merge::UreqFetcher::new())
96}
97
98pub fn load_caproni_config_with_fetcher(
102 gc: &GlobalConfig,
103 fetcher: &dyn super::merge::RemoteFetcher,
104) -> Result<CaproniConfig, ConfigError> {
105 let mut gc = gc.clone();
106 let path = gc.config_file.clone();
107
108 let data = std::fs::read(&path).map_err(|_| ConfigError::ConfigFileNotFound(path.clone()))?;
110
111 let mut raw_config: Value =
114 serde_yaml::from_slice(&data).map_err(|err| ConfigError::InvalidYAML(err.to_string()))?;
115
116 let abs_path = std::fs::canonicalize(&path)
118 .or_else(|_| std::path::absolute(&path))
119 .map_err(|err| ConfigError::ConfigFileNotFound(format!("{path}: {err}")))?;
120
121 let mut used_fragments = HashMap::new();
122 let mut element_sources = HashMap::new();
125
126 if raw_config.is_object() {
128 let mut visited = HashSet::new();
129 visited.insert(abs_path.clone());
130
131 raw_config = resolve_extends(
132 &abs_path,
133 raw_config,
134 false,
135 &mut visited,
136 &mut used_fragments,
137 fetcher,
138 Some(&mut element_sources),
139 )?;
140 }
141
142 if let Some(local_override) = resolve_local_override(
144 &abs_path,
145 &mut used_fragments,
146 fetcher,
147 Some(&mut element_sources),
148 )? {
149 raw_config = apply_local_override(raw_config, local_override)?;
150 }
151
152 let version = detect_config_version(&raw_config)?;
154
155 if version_from_i64(version) != LATEST_VERSION {
162 return Err(ConfigError::SemanticValidation(format!(
163 "unsupported config version: {version} (supported versions: {})",
164 supported_versions()
165 )));
166 }
167
168 let config_dir = abs_path
172 .parent()
173 .unwrap_or_else(|| Path::new("."))
174 .to_path_buf();
175 raw_config = super::merge::apply_caproni_project(raw_config, &config_dir)?;
176
177 let data = serde_json::to_vec(&raw_config)
178 .map_err(|err| ConfigError::InvalidYAML(format!("re-marshaling config: {err}")))?;
179
180 let mut seed = decode_seed_config(version, &data, &abs_path)?;
183
184 apply_resolved_cluster_domain(&mut seed, &gc);
185
186 if gc.profile_name.is_empty() {
187 gc.profile_name = resolve_profile_name(&seed)?;
188 }
189
190 let def_unresolved =
193 super::definitions::resolve_variable_definitions(&mut gc, &mut seed, &mut raw_config)?;
194
195 let static_vars: HashMap<String, String> = seed
198 .variables
199 .as_ref()
200 .map(|vars| {
201 vars.iter()
202 .map(|(name, vc)| (name.clone(), vc.value.clone()))
203 .collect()
204 })
205 .unwrap_or_default();
206
207 let resolver = new_resolver(
208 Options {
209 profile_name: gc.profile_name.clone(),
210 cluster: Some(seed.cluster.clone()),
211 },
212 Some(static_vars),
213 Some(gc.tool_versions()),
214 );
215 let lookup_adapter = ResolverLookup(&*resolver);
216 let lookup: &dyn Lookup = &lookup_adapter;
217
218 if let Value::Object(base_map) = &mut raw_config {
219 validate_repository_ref_variables(base_map, lookup)?;
222
223 let unresolved = expand_map(&mut raw_config, lookup, Some(&skip_variable_expansion_path))
224 .map_err(|err| {
225 ConfigError::SemanticValidation(format!("resolving config variables: {err}"))
226 })?;
227
228 let _ = (def_unresolved, unresolved); if let Value::Object(base_map) = &raw_config {
233 validate_literal_cluster_fields(base_map)?;
234 }
235 }
236
237 let data = serde_json::to_vec(&raw_config)
238 .map_err(|err| ConfigError::InvalidYAML(format!("re-marshaling config: {err}")))?;
239
240 let declared = version_from_i64(version);
244
245 validate_schema_v2(&raw_config)?;
246
247 let mut cfg = load_and_upgrade(declared, &data, true)?;
248
249 cfg.config_file = abs_path.to_string_lossy().into_owned();
250 cfg.config_dir = abs_path
251 .parent()
252 .map(|p| p.to_string_lossy().into_owned())
253 .unwrap_or_default();
254
255 super::merge::fetch_deployer_files(&mut cfg, &element_sources, fetcher)?;
256
257 validate_semantics(&cfg).map_err(|err| ConfigError::SemanticValidation(err.to_string()))?;
258
259 apply_resolved_cluster_domain(&mut cfg, &gc);
260 apply_version_defaults(&mut cfg, &mut gc);
261
262 Ok(cfg)
263}
264
265fn version_from_i64(v: i64) -> super::version::ConfigVersion {
266 match v {
267 2 => super::version::ConfigVersion::V2,
268 _ => super::version::ConfigVersion::Unknown,
269 }
270}
271
272fn decode_seed_config(
277 version: i64,
278 data: &[u8],
279 abs_path: &Path,
280) -> Result<CaproniConfig, ConfigError> {
281 let mut cfg = load_and_upgrade(version_from_i64(version), data, false)?;
282 cfg.config_dir = abs_path
283 .parent()
284 .map(|p| p.to_string_lossy().into_owned())
285 .unwrap_or_default();
286 Ok(cfg)
287}
288
289fn resolve_profile_name(cfg: &CaproniConfig) -> Result<String, ConfigError> {
293 let strategy = cfg
294 .cluster
295 .name
296 .as_ref()
297 .map(|n| n.strategy.as_str())
298 .unwrap_or_default();
299
300 naming::resolve(strategy, &cfg.config_dir)
301 .map_err(|err| ConfigError::SemanticValidation(format!("resolving cluster name: {err}")))
302}
303
304fn apply_resolved_cluster_domain(cfg: &mut CaproniConfig, gc: &GlobalConfig) {
309 if gc.cluster_domain.is_empty() {
310 return;
311 }
312
313 let Some(network) = cfg.cluster.network.as_mut() else {
314 return;
315 };
316
317 if network.cluster_domain.is_empty() {
318 network.cluster_domain = gc.cluster_domain.clone();
319 }
320}
321
322fn validate_repository_ref_variables(
326 root: &serde_json::Map<String, Value>,
327 lookup: &dyn Lookup,
328) -> Result<(), ConfigError> {
329 let Some(repos) = root.get("repositories").and_then(Value::as_object) else {
330 return Ok(());
331 };
332
333 for (name, raw) in repos {
334 let Some(repo) = raw.as_object() else {
335 continue;
336 };
337 let Some(ref_val) = repo.get("ref").and_then(Value::as_str) else {
338 continue;
339 };
340 if ref_val.is_empty() {
341 continue;
342 }
343
344 let (_, missing) =
345 crate::variables::expand::expand_definition(ref_val, lookup).map_err(|err| {
346 ConfigError::SemanticValidation(format!("repository {name:?} ref: {err}"))
347 })?;
348
349 if !missing.is_empty() {
350 return Err(ConfigError::UnresolvedRepositoryRefVariable);
351 }
352 }
353
354 Ok(())
355}
356
357pub fn skip_variable_expansion(path: &[String]) -> bool {
361 is_literal_regex_field(path) || is_literal_cluster_field(path)
362}
363
364fn skip_variable_expansion_path(path: &[String]) -> bool {
365 skip_variable_expansion(path)
366}
367
368fn is_literal_cluster_field(path: &[String]) -> bool {
372 if path.is_empty() || path[0] != CLUSTER_KEY {
373 return false;
374 }
375
376 if path.len() == 2 && path[1] == DRIVER_KEY {
377 return true;
378 }
379
380 path.len() >= 4
381 && (path[1] == K3D_KEY || path[1] == COLIMA_KEY)
382 && path[2] == K3S_KEY
383 && path[3] == DISABLE_KEY
384}
385
386fn is_literal_regex_field(path: &[String]) -> bool {
389 if path.len() != 5 || path[0] != "reloaders" || path[2] != "mirror" || path[3] != "filesystem" {
390 return false;
391 }
392
393 matches!(path[4].as_str(), "mapping" | "read_only" | "read_write")
394}
395
396fn validate_literal_cluster_fields(
399 root: &serde_json::Map<String, Value>,
400) -> Result<(), ConfigError> {
401 let Some(cluster) = root.get(CLUSTER_KEY).and_then(Value::as_object) else {
402 return Ok(());
403 };
404
405 if let Some(driver) = cluster.get(DRIVER_KEY).and_then(Value::as_str)
406 && driver.contains('$')
407 {
408 return Err(ConfigError::DriverNotLiteral(driver.to_string()));
409 }
410
411 for driver in [K3D_KEY, COLIMA_KEY] {
412 for component in k3s_disable_list(cluster, driver) {
413 if component.contains('$') {
414 return Err(ConfigError::K3sDisableNotLiteral(component));
415 }
416 }
417 }
418
419 Ok(())
420}
421
422fn k3s_disable_list(cluster: &serde_json::Map<String, Value>, driver: &str) -> Vec<String> {
423 cluster
424 .get(driver)
425 .and_then(Value::as_object)
426 .and_then(|d| d.get(K3S_KEY))
427 .and_then(Value::as_object)
428 .and_then(|k| k.get(DISABLE_KEY))
429 .and_then(Value::as_array)
430 .map(|entries| {
431 entries
432 .iter()
433 .filter_map(|e| e.as_str().map(String::from))
434 .collect()
435 })
436 .unwrap_or_default()
437}
438
439pub fn load_from_context(
442 ctx: &super::global::Context,
443) -> Result<super::container::Container, ConfigError> {
444 let mut global = super::global::from_context(ctx).ok_or_else(|| {
445 ConfigError::ConfigFileNotFound("no global config in context".to_string())
446 })?;
447
448 let cfg = load_caproni_config(&global)?;
449
450 if global.profile_name.is_empty() {
456 global.profile_name = resolve_profile_name(&cfg)?;
457 }
458
459 Ok(super::container::new_container(global, cfg))
460}
461
462pub fn apply_version_defaults(cfg: &mut CaproniConfig, gc: &mut GlobalConfig) {
464 if let Some(image) = gc.get_tool_version("GL_COPY_SLEEP_IMAGE") {
465 cfg.copy_sleep_image = image;
466 }
467
468 if let Some(colima) = cfg.cluster.colima.as_mut()
469 && colima.k3s_version.is_empty()
470 && let Some(version) = gc.get_tool_version("GL_K3S_VERSION")
471 {
472 colima.k3s_version = version;
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::config::global::GlobalConfig;
480 use crate::config::types::ColimaConfig;
481 use std::collections::HashMap;
482 use std::fs;
483
484 fn write_test_config(content: &str) -> (tempfile::TempDir, String) {
487 let dir = tempfile::tempdir().unwrap();
488 let path = dir.path().join("caproni.yaml");
489 fs::write(&path, content).unwrap();
490 (dir, path.to_string_lossy().into_owned())
491 }
492
493 fn load(path: String) -> Result<CaproniConfig, ConfigError> {
494 let gc = GlobalConfig {
495 config_file: path,
496 ..Default::default()
497 };
498 load_caproni_config(&gc)
499 }
500
501 #[test]
503 fn load_valid_config() {
504 let yaml = "
505version: 2
506cluster:
507 driver: colima
508 network:
509 hostname: gitlab.test
510 colima:
511 memory: 8GiB
512 cores: 4
513
514repositories:
515 gitlab:
516 repository: https://gitlab.com/gitlab-org/gitlab.git
517 directory: ./gitlab/
518 ref: v1.2.3
519
520reloaders:
521 gitlab:
522 type: mirror
523 repository: gitlab
524 edit_mode: true
525 mirror:
526 target:
527 namespace: gitlab
528 kubernetes_type: deployment
529 name: gitlab-webservice-default
530 edit_mode_processes:
531 puma:
532 command: bin/start-puma
533";
534 let (_dir, path) = write_test_config(yaml);
535 let cfg = load(path).expect("load should succeed");
536
537 assert_eq!(cfg.cluster.driver, "colima");
538 let colima = cfg.cluster.colima.expect("colima config present");
539 assert_eq!(colima.memory, "8GiB");
540 assert_eq!(colima.cores, 4);
541
542 let repos = cfg.repositories.expect("repositories present");
543 assert_eq!(repos.len(), 1);
544 assert!(repos.has(&"gitlab".to_string()));
545 let repo = repos.get(&"gitlab".to_string()).unwrap();
546 assert_eq!(repo.repository, "https://gitlab.com/gitlab-org/gitlab.git");
547 assert_eq!(repo.directory, "./gitlab/");
548 assert_eq!(repo.ref_, "v1.2.3");
549
550 let reloaders = cfg.reloaders.expect("reloaders present");
551 assert_eq!(reloaders.len(), 1);
552 let reloader = reloaders.get(&"gitlab".to_string()).unwrap();
553 assert_eq!(reloader.reloader_type, "mirror");
554 assert_eq!(reloader.repository, "gitlab");
555 assert!(reloader.edit_mode);
556 let mirror = reloader.mirror.as_ref().expect("mirror present");
557 let target = mirror.target.as_ref().expect("target present");
558 assert_eq!(target.kubernetes_type, "deployment");
559 assert_eq!(target.name, "gitlab-webservice-default");
560 }
561
562 #[test]
564 fn load_ref_unresolved_variable() {
565 let yaml = "
566version: 2
567cluster:
568 driver: mock
569 network:
570 hostname: gitlab.test
571repositories:
572 gitlab:
573 repository: https://gitlab.com/gitlab-org/gitlab.git
574 directory: ./gitlab/
575 ref: ${CAPRONI_TEST_UNSET_REF_VAR}
576";
577 let (_dir, path) = write_test_config(yaml);
578 let err = load(path).unwrap_err();
579 assert!(matches!(err, ConfigError::UnresolvedRepositoryRefVariable));
580 }
581
582 #[test]
584 fn load_no_repositories() {
585 let yaml = "version: 2\ncluster:\n driver: auto\n network:\n hostname: foo\n k3d: {}\n colima:\n memory: 8GiB\n cores: 4\n";
586 let (_dir, path) = write_test_config(yaml);
587 let cfg = load(path).expect("load should succeed");
588
589 let repos = cfg
590 .repositories
591 .expect("repositories should be initialized even when omitted from config");
592 assert_eq!(repos.len(), 0);
593 }
594
595 #[test]
597 fn load_registry_mirror_schema_cases() {
598 struct Case {
599 name: &'static str,
600 endpoint: &'static str,
601 upstream: &'static str,
602 want_err: bool,
603 }
604
605 let cases = vec![
606 Case {
607 name: "valid http endpoint and host",
608 endpoint: "http://host.docker.internal:5000",
609 upstream: "docker.io",
610 want_err: false,
611 },
612 Case {
613 name: "valid registry.gitlab.com upstream",
614 endpoint: "http://host.k3d.internal:5000",
615 upstream: "registry.gitlab.com",
616 want_err: false,
617 },
618 Case {
619 name: "host:port upstream is allowed",
620 endpoint: "http://host.k3d.internal:5000",
621 upstream: "localhost:5000",
622 want_err: false,
623 },
624 Case {
625 name: "https endpoint rejected",
626 endpoint: "https://my-cache.lan:5443",
627 upstream: "docker.io",
628 want_err: true,
629 },
630 Case {
631 name: "endpoint without host rejected",
632 endpoint: "http://:5000",
633 upstream: "docker.io",
634 want_err: true,
635 },
636 Case {
637 name: "endpoint without port rejected",
638 endpoint: "http://host.docker.internal",
639 upstream: "docker.io",
640 want_err: true,
641 },
642 Case {
643 name: "endpoint without scheme rejected",
644 endpoint: "host.docker.internal:5000",
645 upstream: "docker.io",
646 want_err: true,
647 },
648 Case {
649 name: "upstream path traversal rejected",
650 endpoint: "http://host.docker.internal:5000",
651 upstream: "../evil",
652 want_err: true,
653 },
654 Case {
655 name: "upstream with slash rejected",
656 endpoint: "http://host.docker.internal:5000",
657 upstream: "a/b",
658 want_err: true,
659 },
660 ];
661
662 for case in cases {
663 let yaml = format!(
664 "version: 2\ncluster:\n driver: colima\n network:\n hostname: gitlab.test\n colima:\n memory: 8GiB\n cores: 4\n registry_mirror:\n endpoint: {}\n upstreams: [{}]\nrepositories: {{}}\n",
665 case.endpoint, case.upstream
666 );
667 let (_dir, path) = write_test_config(&yaml);
668 let result = load(path);
669
670 if case.want_err {
671 assert!(
672 matches!(result.unwrap_err(), ConfigError::SchemaValidation(_)),
673 "{}",
674 case.name
675 );
676 continue;
677 }
678
679 let cfg = result.unwrap_or_else(|e| panic!("{}: {e}", case.name));
680 assert_eq!(
681 cfg.cluster.registry_mirror.unwrap().endpoint,
682 case.endpoint,
683 "{}",
684 case.name
685 );
686 }
687 }
688
689 #[test]
691 fn load_registry_mirror_managed_schema_cases() {
692 struct Case {
693 name: &'static str,
694 mirror: &'static str,
695 want_err: bool,
696 }
697
698 let cases = vec![
699 Case {
700 name: "managed true without endpoint is valid",
701 mirror: " registry_mirror:\n managed: true\n upstreams: [docker.io]",
702 want_err: false,
703 },
704 Case {
705 name: "managed true with empty upstreams is valid",
706 mirror: " registry_mirror:\n managed: true",
707 want_err: false,
708 },
709 Case {
710 name: "managed true with endpoint is rejected",
711 mirror: " registry_mirror:\n managed: true\n endpoint: http://host.k3d.internal:5000\n upstreams: [docker.io]",
712 want_err: true,
713 },
714 Case {
715 name: "unmanaged without endpoint is rejected",
716 mirror: " registry_mirror:\n upstreams: [docker.io]",
717 want_err: true,
718 },
719 ];
720
721 for case in cases {
722 let yaml = format!(
723 "version: 2\ncluster:\n driver: colima\n network:\n hostname: gitlab.test\n colima:\n memory: 8GiB\n cores: 4\n{}\nrepositories: {{}}\n",
724 case.mirror
725 );
726 let (_dir, path) = write_test_config(&yaml);
727 let result = load(path);
728
729 if case.want_err {
730 assert!(
731 matches!(result.unwrap_err(), ConfigError::SchemaValidation(_)),
732 "{}",
733 case.name
734 );
735 } else {
736 result.unwrap_or_else(|e| panic!("{}: {e}", case.name));
737 }
738 }
739 }
740
741 #[test]
743 fn load_helm_repository() {
744 let yaml = "
745version: 2
746cluster:
747 driver: colima
748 network:
749 hostname: gitlab.test
750 colima:
751 memory: 10GiB
752 cores: 4
753
754repositories:
755 charts:
756 repository: https://gitlab.com/gitlab-org/charts/gitlab.git
757 directory: ./charts/
758
759deployers:
760 charts:
761 type: helm
762 repository: charts
763 edit_mode: true
764 helm:
765 namespace: gitlab
766 values_file: ./cng-values.yaml
767 repository:
768 name: gitlab
769 url: https://charts.gitlab.io
770 release:
771 name: gitlab
772 chart: gitlab/gitlab
773";
774 let (dir, path) = write_test_config(yaml);
775 fs::write(dir.path().join("cng-values.yaml"), "key: value").unwrap();
776
777 let cfg = load(path).expect("load should succeed");
778
779 let repos = cfg.repositories.expect("repositories present");
780 assert_eq!(repos.len(), 1);
781 let repo = repos.get(&"charts".to_string()).unwrap();
782 assert_eq!(
783 repo.repository,
784 "https://gitlab.com/gitlab-org/charts/gitlab.git"
785 );
786 assert_eq!(repo.directory, "./charts/");
787
788 let deployers = cfg.deployers.expect("deployers present");
789 assert_eq!(deployers.len(), 1);
790 let deployer = deployers.get(&"charts".to_string()).unwrap();
791 assert_eq!(deployer.deployer_type, "helm");
792 assert_eq!(deployer.repository.as_deref(), Some("charts"));
793 assert!(deployer.edit_mode);
794 let helm = deployer.helm.as_ref().expect("helm present");
795 assert_eq!(helm.namespace, "gitlab");
796 assert_eq!(helm.values_file, "./cng-values.yaml");
797 }
798
799 #[test]
801 fn load_namespace_deployer() {
802 let yaml = "
803version: 2
804cluster:
805 driver: colima
806 network:
807 hostname: gitlab.test
808 colima:
809 memory: 4GiB
810 cores: 2
811
812repositories: {}
813
814deployers:
815 prerequisites:
816 type: namespace
817 namespace:
818 namespaces:
819 - gitlab
820 - monitoring
821 delete_on_undeploy: true
822";
823 let (_dir, path) = write_test_config(yaml);
824 let cfg = load(path).expect("load should succeed");
825
826 let deployers = cfg.deployers.expect("deployers present");
827 assert_eq!(deployers.len(), 1);
828 let deployer = deployers.get(&"prerequisites".to_string()).unwrap();
829 assert_eq!(deployer.deployer_type, "namespace");
830 let ns = deployer.namespace.as_ref().expect("namespace present");
831 assert_eq!(
832 ns.namespaces,
833 vec!["gitlab".to_string(), "monitoring".to_string()]
834 );
835 assert!(ns.delete_on_undeploy);
836 assert!(deployer.helm.is_none());
837 }
838
839 #[test]
841 fn load_composes_no_infrastructure() {
842 let yaml = "
843version: 2
844cluster:
845 driver: colima
846 network:
847 hostname: test.local
848 colima:
849 memory: 8GiB
850 cores: 4
851deployers:
852 my-app:
853 type: namespace
854 namespace:
855 namespaces: [my-app]
856";
857 let (_dir, path) = write_test_config(yaml);
858 let cfg = load(path).expect("load should succeed");
859
860 let deployers = cfg.deployers.expect("deployers present");
861 assert_eq!(
862 deployers.len(),
863 1,
864 "caproni must compose no deployers of its own"
865 );
866 assert!(!deployers.has(&"cert-manager".to_string()));
867 }
868
869 #[test]
871 fn load_needs_on_absent_deployer_is_reported() {
872 let yaml = "
873version: 2
874cluster:
875 driver: colima
876 network:
877 hostname: test.local
878 colima:
879 memory: 8GiB
880 cores: 4
881deployers:
882 my-app:
883 type: namespace
884 needs: [cert-manager]
885 namespace:
886 namespaces: [my-app]
887";
888 let (_dir, path) = write_test_config(yaml);
889 let err = load(path).unwrap_err();
890 assert!(err.to_string().contains("cert-manager"));
891 }
892
893 #[test]
895 fn load_multiple_repositories() {
896 let yaml = "
897version: 2
898cluster:
899 driver: colima
900 network:
901 hostname: gitlab.test
902 colima:
903 memory: 10GiB
904 cores: 4
905
906repositories:
907 charts:
908 repository: https://gitlab.com/gitlab-org/charts/gitlab.git
909 directory: ./charts/
910 gitlab:
911 repository: https://gitlab.com/gitlab-org/gitlab.git
912 directory: ./gitlab/
913
914deployers:
915 charts:
916 type: helm
917 repository: charts
918 helm:
919 namespace: gitlab
920 values_file: ./values.yaml
921 repository:
922 name: gitlab
923 url: https://charts.gitlab.io
924 release:
925 name: gitlab
926 chart: gitlab/gitlab
927
928reloaders:
929 gitlab:
930 type: mirror
931 repository: gitlab
932 edit_mode: true
933 mirror:
934 target:
935 namespace: gitlab
936 kubernetes_type: deployment
937 name: gitlab-webservice-default
938 edit_mode_processes:
939 puma:
940 command: bin/start-puma
941 port: 8080
942 sidekiq:
943 command: bin/sidekiq
944";
945 let (dir, path) = write_test_config(yaml);
946 fs::write(dir.path().join("values.yaml"), "key: value").unwrap();
947
948 let cfg = load(path).expect("load should succeed");
949
950 let repos = cfg.repositories.expect("repositories present");
951 assert_eq!(repos.len(), 2);
952
953 let deployers = cfg.deployers.expect("deployers present");
954 assert_eq!(deployers.len(), 1);
955 let deployer = deployers.get(&"charts".to_string()).unwrap();
956 assert_eq!(deployer.repository.as_deref(), Some("charts"));
957 assert!(!deployer.edit_mode);
958
959 let reloaders = cfg.reloaders.expect("reloaders present");
960 let reloader = reloaders.get(&"gitlab".to_string()).unwrap();
961 let mirror = reloader.mirror.as_ref().unwrap();
962 let processes = mirror.edit_mode_processes.as_ref().unwrap();
963 assert_eq!(processes.len(), 2);
964 let puma = processes.get(&"puma".to_string()).unwrap();
965 assert_eq!(puma.command, "bin/start-puma");
966 assert_eq!(puma.port, 8080);
967 let sidekiq = processes.get(&"sidekiq".to_string()).unwrap();
968 assert_eq!(sidekiq.port, 0, "not set");
969 }
970
971 #[test]
973 fn load_lifecycle_hooks() {
974 let yaml = "
975version: 2
976cluster:
977 driver: colima
978 network:
979 hostname: gitlab.test
980 colima:
981 memory: 8GiB
982 cores: 2
983
984repositories:
985 gitlab:
986 repository: https://gitlab.com/gitlab-org/gitlab.git
987 directory: ./gitlab/
988
989reloaders:
990 gitlab:
991 type: mirror
992 repository: gitlab
993 mirror:
994 target:
995 namespace: gitlab
996 kubernetes_type: deployment
997 name: gitlab-webservice-default
998 edit_mode_processes:
999 puma:
1000 command: bin/start-puma
1001 lifecycle_hooks:
1002 - type: edit_mode_prepare
1003 script: ./scripts/prepare-dev-env.sh
1004 - type: edit_mode_start
1005 mirror_script: rake migrate:db
1006 - type: edit_mode_stop
1007 job: cleanup-job.yaml
1008";
1009 let (_dir, path) = write_test_config(yaml);
1010 let cfg = load(path).expect("load should succeed");
1011
1012 let reloaders = cfg.reloaders.expect("reloaders present");
1013 let reloader = reloaders.get(&"gitlab".to_string()).unwrap();
1014 let hooks = &reloader.mirror.as_ref().unwrap().lifecycle_hooks;
1015 assert_eq!(hooks.len(), 3);
1016
1017 assert_eq!(hooks[0].hook_type, "edit_mode_prepare");
1018 assert_eq!(hooks[0].script, "./scripts/prepare-dev-env.sh");
1019 assert!(hooks[0].mirror_script.is_empty());
1020
1021 assert_eq!(hooks[1].hook_type, "edit_mode_start");
1022 assert_eq!(hooks[1].mirror_script, "rake migrate:db");
1023
1024 assert_eq!(hooks[2].hook_type, "edit_mode_stop");
1025 assert_eq!(hooks[2].job, "cleanup-job.yaml");
1026 }
1027
1028 #[test]
1030 fn load_filesystem() {
1031 let yaml = "
1032version: 2
1033cluster:
1034 driver: colima
1035 network:
1036 hostname: gitlab.test
1037 colima:
1038 memory: 8GiB
1039 cores: 2
1040
1041repositories:
1042 gitlab:
1043 repository: https://gitlab.com/gitlab-org/gitlab.git
1044 directory: ./gitlab/
1045
1046reloaders:
1047 gitlab:
1048 type: mirror
1049 repository: gitlab
1050 mirror:
1051 target:
1052 namespace: gitlab
1053 kubernetes_type: pod
1054 name: gitlab-gitaly-0
1055 edit_mode_processes:
1056 puma:
1057 command: bin/start-puma
1058 filesystem:
1059 read_write:
1060 - \"^/home/git/repositories\"
1061 read_only:
1062 - \"^/etc/gitaly\"
1063 - \"^/srv/gitlab-shell\"
1064 - \"^/etc/gitlab-secrets\"
1065";
1066 let (_dir, path) = write_test_config(yaml);
1067 let cfg = load(path).expect("load should succeed");
1068
1069 let reloaders = cfg.reloaders.expect("reloaders present");
1070 let reloader = reloaders.get(&"gitlab".to_string()).unwrap();
1071 let fs_cfg = reloader
1072 .mirror
1073 .as_ref()
1074 .unwrap()
1075 .filesystem
1076 .as_ref()
1077 .expect("filesystem present");
1078
1079 assert_eq!(
1080 fs_cfg.read_write,
1081 vec!["^/home/git/repositories".to_string()]
1082 );
1083 assert_eq!(
1084 fs_cfg.read_only,
1085 vec![
1086 "^/etc/gitaly".to_string(),
1087 "^/srv/gitlab-shell".to_string(),
1088 "^/etc/gitlab-secrets".to_string()
1089 ]
1090 );
1091 }
1092
1093 #[test]
1095 fn load_defaults() {
1096 let yaml = "
1097version: 2
1098cluster:
1099 driver: colima
1100 network:
1101 hostname: gitlab.test
1102 colima:
1103 memory: 8GiB
1104 cores: 4
1105
1106repositories:
1107 gitlab:
1108 repository: https://gitlab.com/gitlab-org/gitlab.git
1109 directory: ./gitlab/
1110
1111reloaders:
1112 gitlab:
1113 type: mirror
1114 repository: gitlab
1115 mirror:
1116 target:
1117 namespace: gitlab
1118 kubernetes_type: deployment
1119 name: gitlab-webservice-default
1120 edit_mode_processes:
1121 puma:
1122 command: bin/start-puma
1123";
1124 let (_dir, path) = write_test_config(yaml);
1125 let cfg = load(path).expect("load should succeed");
1126
1127 let reloaders = cfg.reloaders.expect("reloaders present");
1128 let reloader = reloaders.get(&"gitlab".to_string()).unwrap();
1129 assert!(!reloader.edit_mode, "default is false");
1130 }
1131
1132 macro_rules! stub_test {
1141 ($fn_name:ident, $go_name:literal) => {
1142 #[test]
1143 #[ignore] fn $fn_name() {
1145 unimplemented!(concat!("port of ", $go_name))
1146 }
1147 };
1148 }
1149
1150 #[test]
1152 fn reloader_name() {
1153 let yaml = "
1154version: 2
1155cluster:
1156 driver: colima
1157 network:
1158 hostname: gitlab.test
1159 colima:
1160 memory: 8GiB
1161 cores: 4
1162repositories:
1163 gitlab:
1164 repository: https://gitlab.com/gitlab-org/gitlab.git
1165 directory: ./gitlab/
1166reloaders:
1167 test-reloader:
1168 type: mirror
1169 repository: gitlab
1170 mirror:
1171 target:
1172 namespace: gitlab
1173 kubernetes_type: deployment
1174 name: gitlab-webservice-default
1175 edit_mode_processes:
1176 puma:
1177 command: bin/start-puma
1178";
1179 let (_dir, path) = write_test_config(yaml);
1180 let cfg = load(path).expect("load should succeed");
1181 let reloader = cfg
1182 .reloaders
1183 .unwrap()
1184 .get(&"test-reloader".to_string())
1185 .cloned()
1186 .unwrap();
1187 assert_eq!(reloader.name, "test-reloader");
1188 }
1189
1190 #[test]
1192 fn empty_deployers_and_reloaders() {
1193 let yaml = "
1194version: 2
1195cluster:
1196 driver: colima
1197 network:
1198 hostname: gitlab.test
1199 colima:
1200 memory: 8GiB
1201 cores: 4
1202
1203repositories: {}
1204";
1205 let (_dir, path) = write_test_config(yaml);
1206 let cfg = load(path).expect("load should succeed");
1207
1208 assert_eq!(cfg.repositories.unwrap().len(), 0);
1209 assert_eq!(cfg.deployers.unwrap().len(), 0);
1210 assert_eq!(cfg.reloaders.unwrap().len(), 0);
1211 }
1212
1213 #[test]
1215 fn schema_validation() {
1216 struct Case {
1217 name: &'static str,
1218 yaml: &'static str,
1219 want_err: bool,
1220 err_contains: &'static str,
1221 }
1222
1223 let cases = vec![
1224 Case {
1225 name: "valid config",
1226 yaml: "
1227version: 2
1228cluster:
1229 driver: colima
1230 network:
1231 hostname: gitlab.test
1232 colima:
1233 memory: 10GiB
1234 cores: 4
1235repositories:
1236 gitlab:
1237 repository: https://gitlab.com/gitlab-org/gitlab.git
1238 directory: ./gitlab/
1239reloaders:
1240 gitlab:
1241 type: mirror
1242 repository: gitlab
1243 mirror:
1244 target:
1245 namespace: gitlab
1246 kubernetes_type: deployment
1247 name: gitlab-webservice-default
1248 edit_mode_processes:
1249 puma:
1250 command: bin/start-puma
1251",
1252 want_err: false,
1253 err_contains: "",
1254 },
1255 Case {
1256 name: "invalid driver",
1257 yaml: "
1258version: 2
1259cluster:
1260 driver: invalid
1261 network:
1262 hostname: gitlab.test
1263 colima:
1264 memory: 10GiB
1265 cores: 4
1266repositories: {}
1267",
1268 want_err: true,
1269 err_contains: "not valid under any of the schemas",
1274 },
1275 Case {
1276 name: "invalid memory format",
1277 yaml: "
1278version: 2
1279cluster:
1280 driver: colima
1281 network:
1282 hostname: gitlab.test
1283 colima:
1284 memory: 10GB2
1285 cores: 4
1286repositories: {}
1287",
1288 want_err: true,
1289 err_contains: "memory",
1290 },
1291 Case {
1292 name: "missing required repositories",
1293 yaml: "
1294version: 2
1295cluster:
1296 driver: colima
1297 network:
1298 hostname: gitlab.test
1299 colima:
1300 memory: 10GiB
1301 cores: 4
1302repositories: {}
1303deployers:
1304 charts:
1305 type: helm
1306 repository: charts
1307 edit_mode: true
1308 helm:
1309 namespace: gitlab
1310",
1311 want_err: true,
1312 err_contains: "repository \"charts\" not defined in repositories section",
1313 },
1314 Case {
1315 name: "missing required cluster",
1316 yaml: "
1317version: 2
1318repositories:
1319 gitlab:
1320 repository: https://gitlab.com/gitlab-org/gitlab.git
1321 directory: ./gitlab/
1322",
1323 want_err: true,
1324 err_contains: "cluster",
1325 },
1326 Case {
1327 name: "invalid cores (less than 1)",
1328 yaml: "
1329version: 2
1330cluster:
1331 driver: colima
1332 network:
1333 hostname: gitlab.test
1334 colima:
1335 memory: 10GiB
1336 cores: 0
1337repositories: {}
1338",
1339 want_err: true,
1340 err_contains: "cores",
1341 },
1342 Case {
1343 name: "invalid kubernetes_type",
1344 yaml: "
1345version: 2
1346cluster:
1347 driver: colima
1348 network:
1349 hostname: gitlab.test
1350 colima:
1351 memory: 10GiB
1352 cores: 4
1353repositories:
1354 gitlab:
1355 repository: https://gitlab.com/gitlab-org/gitlab.git
1356 directory: ./gitlab/
1357reloaders:
1358 gitlab:
1359 type: mirror
1360 repository: gitlab
1361 mirror:
1362 target:
1363 namespace: gitlab
1364 kubernetes_type: invalid
1365 name: gitlab-webservice-default
1366 edit_mode_processes:
1367 puma:
1368 command: bin/start-puma
1369",
1370 want_err: true,
1371 err_contains: "kubernetes_type",
1372 },
1373 Case {
1374 name: "helm repo without namespace",
1375 yaml: "
1376version: 2
1377cluster:
1378 driver: colima
1379 network:
1380 hostname: gitlab.test
1381 colima:
1382 memory: 10GiB
1383 cores: 4
1384repositories:
1385 charts:
1386 repository: https://gitlab.com/gitlab-org/charts/gitlab.git
1387 directory: ./charts/
1388deployers:
1389 charts:
1390 type: helm
1391 repository: charts
1392 helm:
1393 repository:
1394 name: gitlab
1395 url: https://charts.gitlab.io
1396 release:
1397 name: gitlab
1398 chart: gitlab/gitlab
1399",
1400 want_err: true,
1401 err_contains: "not valid under any of the schemas",
1405 },
1406 Case {
1407 name: "helm repo without values_file",
1409 yaml: "
1410version: 2
1411cluster:
1412 driver: colima
1413 network:
1414 hostname: gitlab.test
1415 colima:
1416 memory: 10GiB
1417 cores: 4
1418repositories:
1419 charts:
1420 repository: https://gitlab.com/gitlab-org/charts/gitlab.git
1421 directory: ./charts/
1422deployers:
1423 charts:
1424 type: helm
1425 repository: charts
1426 helm:
1427 namespace: gitlab
1428 repository:
1429 name: gitlab
1430 url: https://charts.gitlab.io
1431 release:
1432 name: gitlab
1433 chart: gitlab/gitlab
1434",
1435 want_err: false,
1436 err_contains: "",
1437 },
1438 Case {
1439 name: "app repo without kubernetes_type",
1440 yaml: "
1441version: 2
1442cluster:
1443 driver: colima
1444 network:
1445 hostname: gitlab.test
1446 colima:
1447 memory: 10GiB
1448 cores: 4
1449repositories:
1450 gitlab:
1451 repository: https://gitlab.com/gitlab-org/gitlab.git
1452 directory: ./gitlab/
1453reloaders:
1454 gitlab:
1455 type: mirror
1456 repository: gitlab
1457 mirror:
1458 target:
1459 namespace: gitlab
1460 name: gitlab-webservice-default
1461 edit_mode_processes:
1462 puma:
1463 command: bin/start-puma
1464",
1465 want_err: true,
1466 err_contains: "not valid under any of the schemas",
1471 },
1472 Case {
1473 name: "invalid namespace format",
1474 yaml: "
1475version: 2
1476cluster:
1477 driver: colima
1478 network:
1479 hostname: gitlab.test
1480 colima:
1481 memory: 10GiB
1482 cores: 4
1483repositories:
1484 charts:
1485 repository: https://gitlab.com/gitlab-org/charts/gitlab.git
1486 directory: ./charts/
1487deployers:
1488 charts:
1489 type: helm
1490 repository: charts
1491 helm:
1492 namespace: Invalid_Namespace
1493 repository:
1494 name: gitlab
1495 url: https://charts.gitlab.io
1496 release:
1497 name: gitlab
1498 chart: gitlab/gitlab
1499",
1500 want_err: true,
1501 err_contains: "namespace",
1502 },
1503 Case {
1504 name: "lifecycle hook without action",
1505 yaml: "
1506version: 2
1507cluster:
1508 driver: colima
1509 network:
1510 hostname: gitlab.test
1511 colima:
1512 memory: 10GiB
1513 cores: 4
1514repositories:
1515 gitlab:
1516 repository: https://gitlab.com/gitlab-org/gitlab.git
1517 directory: ./gitlab/
1518reloaders:
1519 gitlab:
1520 type: mirror
1521 repository: gitlab
1522 mirror:
1523 target:
1524 namespace: gitlab
1525 kubernetes_type: deployment
1526 name: gitlab-webservice-default
1527 edit_mode_processes:
1528 puma:
1529 command: bin/start-puma
1530 lifecycle_hooks:
1531 - type: edit_mode_prepare
1532",
1533 want_err: true,
1534 err_contains: "oneOf",
1535 },
1536 Case {
1537 name: "process without command",
1538 yaml: "
1539version: 2
1540cluster:
1541 driver: colima
1542 network:
1543 hostname: gitlab.test
1544 colima:
1545 memory: 10GiB
1546 cores: 4
1547repositories:
1548 gitlab:
1549 repository: https://gitlab.com/gitlab-org/gitlab.git
1550 directory: ./gitlab/
1551reloaders:
1552 gitlab:
1553 type: mirror
1554 repository: gitlab
1555 mirror:
1556 target:
1557 namespace: gitlab
1558 kubernetes_type: deployment
1559 name: gitlab-webservice-default
1560 edit_mode_processes:
1561 puma:
1562 port: 8080
1563",
1564 want_err: true,
1565 err_contains: "not valid under any of the schemas",
1567 },
1568 ];
1569
1570 for case in cases {
1571 let (_dir, path) = write_test_config(case.yaml);
1572 let result = load(path);
1573
1574 if case.want_err {
1575 let err = result.expect_err(case.name);
1576 if !case.err_contains.is_empty() {
1577 assert!(
1578 err.to_string().contains(case.err_contains),
1579 "{}: expected error to contain {:?}, got {:?}",
1580 case.name,
1581 case.err_contains,
1582 err.to_string()
1583 );
1584 }
1585 } else {
1586 result.unwrap_or_else(|e| panic!("{}: expected success, got {e}", case.name));
1587 }
1588 }
1589 }
1590
1591 #[test]
1594 fn schema_validation_inline_override() {
1595 const BASE: &str = "
1596version: 2
1597cluster:
1598 driver: colima
1599 network:
1600 hostname: gitlab.test
1601 colima:
1602 memory: 10GiB
1603 cores: 4
1604repositories:
1605 charts:
1606 repository: https://gitlab.com/gitlab-org/charts/gitlab.git
1607 directory: ./charts/
1608deployers:
1609 charts:
1610 type: helm
1611 repository: charts
1612 helm:
1613 namespace: gitlab
1614 repository:
1615 name: gitlab
1616 url: https://charts.gitlab.io
1617 release:
1618 name: gitlab
1619 chart: gitlab/gitlab
1620 dynamic_overrides:
1621";
1622
1623 struct Case {
1624 name: &'static str,
1625 overrides: &'static str,
1626 want_err: bool,
1627 err_contains: &'static str,
1628 }
1629
1630 let cases = vec![
1631 Case {
1632 name: "inline override with nested lists and maps of scalars",
1633 overrides: " - type: inline
1634 value_map:
1635 global.extraArgs:
1636 - --foo
1637 - --bar
1638 global.appConfig:
1639 enabled: true
1640 limits:
1641 cpu: \"500m\"
1642 count: 2
1643",
1644 want_err: false,
1645 err_contains: "",
1646 },
1647 Case {
1656 name: "inline override with nested date value (accepted, see divergence note)",
1657 overrides: " - type: inline
1658 value_map:
1659 global.appConfig:
1660 expiry: 2024-01-01
1661",
1662 want_err: false,
1663 err_contains: "",
1664 },
1665 Case {
1666 name: "inline_file override with string paths",
1667 overrides: " - type: inline_file
1668 value_map:
1669 global.certificate: ./tls.crt
1670",
1671 want_err: false,
1672 err_contains: "",
1673 },
1674 Case {
1675 name: "inline_file override with non-string value",
1676 overrides: " - type: inline_file
1677 value_map:
1678 global.certificate:
1679 - ./tls.crt
1680",
1681 want_err: true,
1682 err_contains: "value_map",
1683 },
1684 Case {
1685 name: "override dropping a key with null",
1686 overrides: " - type: inline
1687 value_map:
1688 global.edition: ~
1689",
1690 want_err: false,
1691 err_contains: "",
1692 },
1693 Case {
1694 name: "inline_file override dropping a key with null",
1695 overrides: " - type: inline_file
1696 value_map:
1697 global.certificate: ~
1698",
1699 want_err: false,
1700 err_contains: "",
1701 },
1702 Case {
1703 name: "inline override dropping a nested key with null",
1704 overrides: " - type: inline
1705 value_map:
1706 global.appConfig:
1707 enabled: ~
1708 limits:
1709 cpu: \"500m\"
1710",
1711 want_err: false,
1712 err_contains: "",
1713 },
1714 Case {
1715 name: "inline override with null inside a list",
1716 overrides: " - type: inline
1717 value_map:
1718 global.extraArgs:
1719 - --foo
1720 - ~
1721",
1722 want_err: true,
1723 err_contains: "value_map",
1724 },
1725 ];
1726
1727 for case in cases {
1728 let yaml = format!("{BASE}{}", case.overrides);
1729 let (_dir, path) = write_test_config(&yaml);
1730 let result = load(path);
1731
1732 if case.want_err {
1733 let err = result.expect_err(case.name);
1734 if !case.err_contains.is_empty() {
1735 assert!(
1736 err.to_string().contains(case.err_contains),
1737 "{}: expected error to contain {:?}, got {:?}",
1738 case.name,
1739 case.err_contains,
1740 err.to_string()
1741 );
1742 }
1743 } else {
1744 result.unwrap_or_else(|e| panic!("{}: expected success, got {e}", case.name));
1745 }
1746 }
1747 }
1748
1749 #[test]
1751 fn file_not_found() {
1752 let gc = GlobalConfig {
1753 config_file: "/nonexistent/caproni.yaml".to_string(),
1754 ..Default::default()
1755 };
1756 let err = load_caproni_config(&gc).unwrap_err();
1757 assert!(matches!(err, ConfigError::ConfigFileNotFound(_)));
1758 }
1759
1760 #[test]
1762 fn invalid_yaml() {
1763 let yaml = "
1764cluster:
1765 driver: colima
1766 network:
1767 hostname: gitlab.test
1768 colima:
1769 memory: 10GiB
1770 cores: 4
1771repositories: [invalid yaml structure
1772";
1773 let (_dir, path) = write_test_config(yaml);
1774 let err = load(path).unwrap_err();
1775 assert!(matches!(err, ConfigError::InvalidYAML(_)));
1776 }
1777
1778 stub_test!(compile_schema_lazy_loading, "TestCompileSchema_LazyLoading");
1784
1785 #[test]
1788 fn schema_validation_namespace_deployer() {
1789 const BASE: &str = "
1790version: 2
1791cluster:
1792 driver: colima
1793 network:
1794 hostname: gitlab.test
1795 colima:
1796 memory: 4GiB
1797 cores: 2
1798
1799repositories: {}
1800
1801deployers:
1802 prerequisites:
1803 type: namespace
1804";
1805
1806 struct Case {
1807 name: &'static str,
1808 deployer: &'static str,
1809 want_err: bool,
1810 err_contains: &'static str,
1811 }
1812
1813 let cases = vec![
1814 Case {
1815 name: "minimal",
1816 deployer: " namespace:
1817 namespaces:
1818 - gitlab
1819",
1820 want_err: false,
1821 err_contains: "",
1822 },
1823 Case {
1824 name: "with delete_on_undeploy",
1825 deployer: " namespace:
1826 namespaces:
1827 - gitlab
1828 - monitoring
1829 delete_on_undeploy: true
1830",
1831 want_err: false,
1832 err_contains: "",
1833 },
1834 Case {
1835 name: "empty namespaces rejected",
1836 deployer: " namespace:
1837 namespaces: []
1838",
1839 want_err: true,
1840 err_contains: "not valid under any of the schemas",
1843 },
1844 Case {
1845 name: "invalid dns name rejected",
1846 deployer: " namespace:
1847 namespaces:
1848 - Not_A_Valid_NS
1849",
1850 want_err: true,
1851 err_contains: "not valid under any of the schemas",
1853 },
1854 Case {
1855 name: "name longer than 63 chars rejected",
1856 deployer: " namespace:
1857 namespaces:
1858 - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
1859",
1860 want_err: true,
1861 err_contains: "not valid under any of the schemas",
1863 },
1864 Case {
1865 name: "mixed with helm rejected",
1866 deployer: " namespace:
1867 namespaces:
1868 - gitlab
1869 helm:
1870 namespace: gitlab
1871",
1872 want_err: true,
1873 err_contains: "not valid under any of the schemas",
1875 },
1876 ];
1877
1878 for case in cases {
1879 let yaml = format!("{BASE}{}", case.deployer);
1880 let (_dir, path) = write_test_config(&yaml);
1881 let result = load(path);
1882
1883 if case.want_err {
1884 let err = result.expect_err(case.name);
1885 if !case.err_contains.is_empty() {
1886 assert!(
1887 err.to_string().contains(case.err_contains),
1888 "{}: expected error to contain {:?}, got {:?}",
1889 case.name,
1890 case.err_contains,
1891 err.to_string()
1892 );
1893 }
1894 } else {
1895 result.unwrap_or_else(|e| panic!("{}: expected success, got {e}", case.name));
1896 }
1897 }
1898 }
1899
1900 #[test]
1907 fn schema_validation_kustomize_deployer() {
1908 const BASE: &str = "
1909version: 2
1910cluster:
1911 driver: colima
1912 network:
1913 hostname: gitlab.test
1914 colima:
1915 memory: 4GiB
1916 cores: 2
1917
1918repositories: {}
1919
1920deployers:
1921 supporting-manifests:
1922 type: kustomize
1923";
1924
1925 struct Case {
1926 name: &'static str,
1927 deployer: &'static str,
1928 want_err: bool,
1929 err_contains: &'static str,
1930 }
1931
1932 let cases = vec![
1933 Case {
1934 name: "minimal",
1935 deployer: " kustomize:
1936 path: ./manifests
1937",
1938 want_err: false,
1939 err_contains: "",
1940 },
1941 Case {
1942 name: "with namespace",
1943 deployer: " kustomize:
1944 path: ./manifests
1945 namespace: gitlab
1946",
1947 want_err: false,
1948 err_contains: "",
1949 },
1950 Case {
1951 name: "missing path rejected",
1952 deployer: " kustomize:
1953 namespace: gitlab
1954",
1955 want_err: true,
1956 err_contains: "not valid under any of the schemas",
1961 },
1962 Case {
1963 name: "unknown property rejected",
1964 deployer: " kustomize:
1965 path: ./manifests
1966 overlay: production
1967",
1968 want_err: true,
1969 err_contains: "not valid under any of the schemas",
1971 },
1972 Case {
1973 name: "mixed with helm rejected",
1974 deployer: " kustomize:
1975 path: ./manifests
1976 helm:
1977 namespace: gitlab
1978",
1979 want_err: true,
1980 err_contains: "not valid under any of the schemas",
1982 },
1983 Case {
1984 name: "ca_material rejected",
1985 deployer: " kustomize:
1986 path: ./manifests
1987 ca_material:
1988 cert_file: tls.crt
1989 key_file: tls.key
1990",
1991 want_err: true,
1992 err_contains: "not valid under any of the schemas",
1994 },
1995 ];
1996
1997 for case in cases {
1998 let yaml = format!("{BASE}{}", case.deployer);
1999 let (dir, path) = write_test_config(&yaml);
2000 std::fs::create_dir_all(dir.path().join("manifests")).unwrap();
2001 let result = load(path);
2002
2003 if case.want_err {
2004 let err = result.expect_err(case.name);
2005 if !case.err_contains.is_empty() {
2006 assert!(
2007 err.to_string().contains(case.err_contains),
2008 "{}: expected error to contain {:?}, got {:?}",
2009 case.name,
2010 case.err_contains,
2011 err.to_string()
2012 );
2013 }
2014 } else {
2015 result.unwrap_or_else(|e| panic!("{}: expected success, got {e}", case.name));
2016 }
2017 }
2018 }
2019
2020 #[test]
2022 fn load_from_context_test() {
2023 let err = match load_from_context(&super::super::global::Context::default()) {
2025 Err(e) => e,
2026 Ok(_) => panic!("expected error"),
2027 };
2028 assert!(matches!(err, ConfigError::ConfigFileNotFound(_)));
2029
2030 let yaml = "version: 2\ncluster:\n driver: colima\n network:\n hostname: woof.dog\n colima:\n memory: 8GiB\n cores: 4\nrepositories: {}";
2032 let (_dir, path) = write_test_config(yaml);
2033 let ctx = super::super::global::with_context(
2034 super::super::global::Context::default(),
2035 GlobalConfig {
2036 config_file: path,
2037 ..Default::default()
2038 },
2039 );
2040 let container = load_from_context(&ctx).expect("load should succeed");
2041 assert_eq!(container.global.profile_name, "xianyun");
2042 assert_eq!(
2043 container.caproni.cluster.network.unwrap().hostname,
2044 "woof.dog"
2045 );
2046
2047 let (_dir2, path2) = write_test_config("version: -1");
2049 let ctx2 = super::super::global::with_context(
2050 super::super::global::Context::default(),
2051 GlobalConfig {
2052 config_file: path2,
2053 ..Default::default()
2054 },
2055 );
2056 let err = match load_from_context(&ctx2) {
2057 Err(e) => e,
2058 Ok(_) => panic!("expected error"),
2059 };
2060 assert!(err.to_string().contains("unsupported config version"));
2061 }
2062
2063 #[test]
2065 fn resolves_variables() {
2066 unsafe {
2068 std::env::set_var("CAPRONI_ENV_SUFFIX", "prod");
2069 }
2070
2071 let yaml = "version: 2\ncluster:\n driver: colima\n network:\n hostname: ${CLUSTER}.example.com\n additional_hostnames:\n - api-${CAPRONI_ENV_SUFFIX}.example.com\n colima:\n memory: 8GiB\n cores: 4\nvariables:\n CLUSTER: demo\n";
2072 let (_dir, path) = write_test_config(yaml);
2073 let cfg = load(path).expect("load should succeed");
2074
2075 let network = cfg.cluster.network.unwrap();
2076 assert_eq!(network.hostname, "demo.example.com");
2077 assert_eq!(
2078 network.additional_hostnames,
2079 vec!["api-prod.example.com".to_string()]
2080 );
2081
2082 let v = cfg
2083 .variables
2084 .unwrap()
2085 .get(&"CLUSTER".to_string())
2086 .cloned()
2087 .unwrap();
2088 assert_eq!(v.value, "demo");
2089 }
2090
2091 #[test]
2093 fn unknown_variable_resolves_empty() {
2094 let yaml = "version: 2\ncluster:\n driver: colima\n network:\n hostname: gitlab.example.com\n additional_hostnames:\n - prefix${DOES_NOT_EXIST}.example.com\n colima:\n memory: 8GiB\n cores: 4\n";
2095 let (_dir, path) = write_test_config(yaml);
2096 let cfg = load(path).expect("load should succeed");
2097
2098 assert_eq!(
2099 cfg.cluster.network.unwrap().additional_hostnames,
2100 vec!["prefix.example.com".to_string()]
2101 );
2102 }
2103
2104 #[test]
2106 fn preserves_mirror_regex_fields() {
2107 let yaml = "version: 2\ncluster:\n driver: k3d\n network:\n hostname: gitlab.example.com\n k3d:\n servers: 1\nrepositories:\n gitlab:\n repository: https://gitlab.com/gitlab-org/gitlab.git\n directory: ./gitlab\nreloaders:\n gitlab:\n type: mirror\n repository: gitlab\n edit_mode: true\n mirror:\n target:\n namespace: gitlab\n kubernetes_type: deployment\n name: gitlab-webservice-default\n filesystem:\n mode: localwithoverrides\n read_only:\n - \"^/etc/gitlab/.*\"\n mapping:\n \"^/srv/gitlab/log/(?<file>.+)\": \"./log/$file\"\n \"^(?<path>/home/.*/config/)gitlab.yml\": \"${path}/gitlab-sidekiq.yml\"\n edit_mode_processes:\n puma:\n command: echo hi\n";
2108 let (_dir, path) = write_test_config(yaml);
2109 let cfg = load(path).expect("load should succeed");
2110
2111 let reloader = cfg
2112 .reloaders
2113 .unwrap()
2114 .get(&"gitlab".to_string())
2115 .cloned()
2116 .unwrap();
2117 let fs_cfg = reloader.mirror.unwrap().filesystem.unwrap();
2118 let mapping = fs_cfg.mapping.unwrap();
2119
2120 assert_eq!(
2121 mapping
2122 .get(&"^/srv/gitlab/log/(?<file>.+)".to_string())
2123 .cloned(),
2124 Some("./log/$file".to_string())
2125 );
2126 assert_eq!(
2127 mapping
2128 .get(&"^(?<path>/home/.*/config/)gitlab.yml".to_string())
2129 .cloned(),
2130 Some("${path}/gitlab-sidekiq.yml".to_string())
2131 );
2132 assert_eq!(fs_cfg.read_only, vec!["^/etc/gitlab/.*".to_string()]);
2133 }
2134
2135 #[test]
2137 fn rejects_variable_in_cluster_driver() {
2138 unsafe {
2140 std::env::set_var("CAPRONI_TEST_DRIVER", "colima");
2141 }
2142
2143 struct Case {
2144 driver: &'static str,
2145 extra: &'static str,
2146 }
2147
2148 let cases = vec![
2149 Case {
2150 driver: "${DRV}",
2151 extra: "variables:\n DRV: colima\n",
2152 },
2153 Case {
2154 driver: "$DRV",
2155 extra: "variables:\n DRV: colima\n",
2156 },
2157 Case {
2158 driver: "${CAPRONI_TEST_DRIVER}",
2159 extra: "",
2160 },
2161 Case {
2162 driver: "${DOES_NOT_EXIST}",
2163 extra: "",
2164 },
2165 Case {
2166 driver: "col${SUFFIX}ima",
2167 extra: "variables:\n SUFFIX: \"\"\n",
2168 },
2169 ];
2170
2171 for case in cases {
2172 let content = format!(
2173 "version: 2\ncluster:\n driver: {}\n network:\n hostname: gitlab.example.com\n colima:\n memory: 8GiB\n cores: 4\n{}",
2174 case.driver, case.extra
2175 );
2176 let (_dir, path) = write_test_config(&content);
2177 let err = load(path).unwrap_err();
2178 assert!(
2179 matches!(&err, ConfigError::DriverNotLiteral(d) if d == case.driver),
2180 "{}: {err}",
2181 case.driver
2182 );
2183 }
2184 }
2185
2186 #[test]
2188 fn literal_cluster_fields_rejects_variable_in_k3s_disable() {
2189 let cases = [
2190 "\nversion: 2\ncluster:\n driver: k3d\n network:\n hostname: gitlab.test\n k3d:\n k3s:\n disable: [\"${DISABLED_COMPONENT}\"]\nrepositories: {}\n",
2191 "\nversion: 2\ncluster:\n driver: colima\n network:\n hostname: gitlab.test\n colima:\n memory: 8GiB\n cores: 4\n k3s:\n disable: [\"${DISABLED_COMPONENT}\"]\nrepositories: {}\n",
2192 ];
2193
2194 for config in cases {
2195 let (_dir, path) = write_test_config(config);
2196 let err = load(path).unwrap_err();
2197 assert!(matches!(err, ConfigError::K3sDisableNotLiteral(_)));
2198 }
2199 }
2200
2201 #[test]
2203 fn literal_cluster_fields_keeps_k3s_disable_verbatim() {
2204 let k3d_yaml = "\nversion: 2\ncluster:\n driver: k3d\n network:\n hostname: gitlab.test\n k3d:\n k3s:\n disable: [traefik, servicelb]\nrepositories: {}\n";
2205 let (_dir, path) = write_test_config(k3d_yaml);
2206 let cfg = load(path).expect("load should succeed");
2207 assert_eq!(
2208 cfg.cluster.k3d.unwrap().k3s.unwrap().disable,
2209 vec!["traefik".to_string(), "servicelb".to_string()]
2210 );
2211
2212 let colima_yaml = "\nversion: 2\ncluster:\n driver: colima\n network:\n hostname: gitlab.test\n colima:\n memory: 8GiB\n cores: 4\n k3s:\n disable: [traefik, servicelb]\nrepositories: {}\n";
2213 let (_dir2, path2) = write_test_config(colima_yaml);
2214 let cfg2 = load(path2).expect("load should succeed");
2215 assert_eq!(
2216 cfg2.cluster.colima.unwrap().k3s.unwrap().disable,
2217 vec!["traefik".to_string(), "servicelb".to_string()]
2218 );
2219 }
2220
2221 #[test]
2223 fn skip_variable_expansion_cases() {
2224 struct Case {
2225 path: &'static [&'static str],
2226 want: bool,
2227 }
2228
2229 let cases = vec![
2230 Case {
2231 path: &["cluster", "driver"],
2232 want: true,
2233 },
2234 Case {
2235 path: &["cluster", "k3d", "k3s", "disable"],
2236 want: true,
2237 },
2238 Case {
2239 path: &["cluster", "k3d", "k3s", "disable", "0"],
2240 want: true,
2241 },
2242 Case {
2243 path: &["cluster", "colima", "k3s", "disable"],
2244 want: true,
2245 },
2246 Case {
2247 path: &["cluster", "colima", "k3s", "disable", "0"],
2248 want: true,
2249 },
2250 Case {
2251 path: &["cluster", "k3d", "extra_args"],
2252 want: false,
2253 },
2254 Case {
2255 path: &["cluster", "k3d", "k3s"],
2256 want: false,
2257 },
2258 Case {
2259 path: &["cluster", "k3d", "k3s", "other"],
2260 want: false,
2261 },
2262 Case {
2263 path: &["cluster", "network", "k3s", "disable"],
2264 want: false,
2265 },
2266 Case {
2267 path: &["reloaders", "gitlab", "mirror", "filesystem", "mapping"],
2268 want: true,
2269 },
2270 Case {
2271 path: &["cluster", "network", "hostname"],
2272 want: false,
2273 },
2274 Case {
2275 path: &["cluster", "colima", "memory"],
2276 want: false,
2277 },
2278 Case {
2279 path: &["deployers", "cluster", "driver"],
2280 want: false,
2281 },
2282 Case {
2283 path: &["cluster"],
2284 want: false,
2285 },
2286 Case {
2287 path: &["repositories", "driver"],
2288 want: false,
2289 },
2290 ];
2291
2292 for case in cases {
2293 let path: Vec<String> = case.path.iter().map(|s| s.to_string()).collect();
2294 assert_eq!(skip_variable_expansion(&path), case.want, "{:?}", case.path);
2295 }
2296 }
2297
2298 #[test]
2300 fn variables_schema() {
2301 let yaml = "version: 2\ncluster:\n driver: colima\n network:\n hostname: gitlab.example.com\n colima:\n memory: 8GiB\n cores: 4\nrepositories: {}\nvariables:\n SHORTHAND: literal-value\n";
2302 let (_dir, path) = write_test_config(yaml);
2303 let cfg = load(path).expect("load should succeed");
2304
2305 let sh = cfg
2306 .variables
2307 .unwrap()
2308 .get(&"SHORTHAND".to_string())
2309 .cloned()
2310 .unwrap();
2311 assert_eq!(sh.value, "literal-value");
2312 }
2313
2314 #[test]
2316 fn vcluster_driver_is_valid() {
2317 let yaml = "
2318version: 2
2319cluster:
2320 driver: vcluster
2321 network:
2322 hostname: gitlab.test
2323 vcluster:
2324 host_context: my-host
2325 namespace: caproni-vc
2326repositories: {}
2327";
2328 let (_dir, path) = write_test_config(yaml);
2329 let ctx = super::super::global::with_context(
2330 super::super::global::Context::default(),
2331 GlobalConfig {
2332 config_file: path,
2333 ..Default::default()
2334 },
2335 );
2336 let container = load_from_context(&ctx).expect("load should succeed");
2337
2338 assert_eq!(container.caproni.cluster.driver, "vcluster");
2339 let vcluster = container.caproni.cluster.vcluster.unwrap();
2340 assert_eq!(vcluster.host_context, "my-host");
2341 assert_eq!(vcluster.namespace, "caproni-vc");
2342 assert_eq!(vcluster.manage_namespace, Some(true));
2343 }
2344
2345 #[test]
2347 fn vcluster_driver_requires_host_context() {
2348 let yaml = "
2349version: 2
2350cluster:
2351 driver: vcluster
2352 network:
2353 hostname: gitlab.test
2354 vcluster:
2355 namespace: caproni-vc
2356repositories: {}
2357";
2358 let (_dir, path) = write_test_config(yaml);
2359 let ctx = super::super::global::with_context(
2360 super::super::global::Context::default(),
2361 GlobalConfig {
2362 config_file: path,
2363 ..Default::default()
2364 },
2365 );
2366 let err = match load_from_context(&ctx) {
2367 Err(e) => e,
2368 Ok(_) => panic!("expected error"),
2369 };
2370 assert!(err.to_string().contains("host_context"));
2371 }
2372
2373 #[test]
2375 fn helm_release_version_resolves_from_tool_versions() {
2376 let yaml = "
2377version: 2
2378cluster:
2379 driver: colima
2380 network:
2381 hostname: gitlab.test
2382 colima:
2383 memory: 8GiB
2384 cores: 4
2385repositories: {}
2386deployers:
2387 traefik:
2388 type: helm
2389 helm:
2390 namespace: kube-system
2391 repository:
2392 name: traefik
2393 url: https://traefik.github.io/charts
2394 release:
2395 name: traefik
2396 chart: traefik/traefik
2397 version: ${GL_TRAEFIK_VERSION}
2398";
2399 let (_dir, path) = write_test_config(yaml);
2400 let versions = HashMap::from([(
2401 ".gitlab-ci-other-versions.yml".to_string(),
2402 "variables:\n GL_TRAEFIK_VERSION: tf-test\n".to_string(),
2403 )]);
2404 let gc = GlobalConfig {
2405 config_file: path,
2406 ..GlobalConfig::new(Some(versions))
2407 };
2408 let cfg = load_caproni_config(&gc).expect("load should succeed");
2409
2410 let dep = cfg
2411 .deployers
2412 .unwrap()
2413 .get(&"traefik".to_string())
2414 .cloned()
2415 .unwrap();
2416 assert_eq!(dep.helm.unwrap().release.unwrap().version, "tf-test");
2417 }
2418
2419 #[test]
2421 fn cluster_domain_cases() {
2422 struct Case {
2423 name: &'static str,
2424 network: &'static str,
2425 want: &'static str,
2426 }
2427
2428 let cases = vec![
2429 Case {
2430 name: "unset",
2431 network: "",
2432 want: "",
2433 },
2434 Case {
2435 name: "configured",
2436 network: " cluster_domain: corp.example",
2437 want: "corp.example",
2438 },
2439 ];
2440
2441 for case in cases {
2442 let yaml = format!(
2443 "version: 2\ncluster:\n driver: external\n network:\n hostname: gitlab.example.com\n{}\n",
2444 case.network
2445 );
2446 let (_dir, path) = write_test_config(&yaml);
2447 let cfg = load(path).unwrap_or_else(|e| panic!("{}: {e}", case.name));
2448 assert_eq!(
2449 cfg.cluster.network.unwrap().cluster_domain,
2450 case.want,
2451 "{}",
2452 case.name
2453 );
2454 }
2455 }
2456
2457 #[test]
2459 fn resolved_cluster_domain_cases() {
2460 struct Case {
2461 name: &'static str,
2462 network: &'static str,
2463 resolved: &'static str,
2464 want: &'static str,
2465 want_host: &'static str,
2466 }
2467
2468 let cases = vec![
2469 Case {
2470 name: "unset takes the resolved domain",
2471 network: "",
2472 resolved: "corp.example",
2473 want: "corp.example",
2474 want_host: "postgres.gitlab.svc.corp.example",
2475 },
2476 Case {
2477 name: "configured key wins",
2478 network: " cluster_domain: other.example",
2479 resolved: "corp.example",
2480 want: "other.example",
2481 want_host: "postgres.gitlab.svc.other.example",
2482 },
2483 Case {
2484 name: "nothing resolved leaves the key unset",
2485 network: "",
2486 resolved: "",
2487 want: "",
2488 want_host: "postgres.gitlab.svc",
2489 },
2490 ];
2491
2492 for case in cases {
2493 let yaml = format!(
2494 "version: 2\ncluster:\n driver: external\n network:\n hostname: gitlab.example.com\n{}\nvariables:\n PG_HOST: postgres.gitlab.svc${{CAPRONI_CLUSTER_DOMAIN:+.${{CAPRONI_CLUSTER_DOMAIN}}}}\n",
2495 case.network
2496 );
2497 let (_dir, path) = write_test_config(&yaml);
2498 let gc = GlobalConfig {
2499 config_file: path,
2500 cluster_domain: case.resolved.to_string(),
2501 ..Default::default()
2502 };
2503 let cfg = load_caproni_config(&gc).unwrap_or_else(|e| panic!("{}: {e}", case.name));
2504
2505 assert_eq!(
2506 cfg.cluster.network.as_ref().unwrap().cluster_domain,
2507 case.want,
2508 "{}",
2509 case.name
2510 );
2511 let def = cfg
2512 .variables
2513 .unwrap()
2514 .get(&"PG_HOST".to_string())
2515 .cloned()
2516 .unwrap();
2517 assert_eq!(def.value, case.want_host, "{}", case.name);
2518 }
2519 }
2520
2521 #[test]
2523 fn cluster_domain_variable_cases() {
2524 struct Case {
2525 name: &'static str,
2526 network: &'static str,
2527 want: &'static str,
2528 want_pg_host: &'static str,
2529 }
2530
2531 let cases = vec![
2532 Case {
2533 name: "unset leaves the variable undefined",
2534 network: "",
2535 want: "",
2536 want_pg_host: "postgres.gitlab.svc",
2537 },
2538 Case {
2539 name: "configured expands",
2540 network: " cluster_domain: corp.example",
2541 want: "corp.example",
2542 want_pg_host: "postgres.gitlab.svc.corp.example",
2543 },
2544 ];
2545
2546 for case in cases {
2547 let yaml = format!(
2548 "version: 2\ncluster:\n driver: external\n network:\n hostname: gitlab.example.com\n{}\nvariables:\n PG_HOST: postgres.gitlab.svc${{CAPRONI_CLUSTER_DOMAIN:+.${{CAPRONI_CLUSTER_DOMAIN}}}}\n",
2549 case.network
2550 );
2551 let (_dir, path) = write_test_config(&yaml);
2552 let gc = GlobalConfig {
2553 config_file: path,
2554 ..Default::default()
2555 };
2556 let cfg = load_caproni_config(&gc).unwrap_or_else(|e| panic!("{}: {e}", case.name));
2557
2558 assert_eq!(
2559 cfg.cluster.network.as_ref().unwrap().cluster_domain,
2560 case.want,
2561 "{}",
2562 case.name
2563 );
2564 let def = cfg
2565 .variables
2566 .as_ref()
2567 .unwrap()
2568 .get(&"PG_HOST".to_string())
2569 .cloned()
2570 .unwrap();
2571 assert_eq!(def.value, case.want_pg_host, "{}", case.name);
2572
2573 let container = crate::config::container::new_container(gc, cfg);
2574 let known = container
2575 .resolver
2576 .lookup("CAPRONI_CLUSTER_DOMAIN")
2577 .is_some();
2578 assert_eq!(known, !case.want.is_empty(), "{}", case.name);
2579 }
2580 }
2581
2582 #[test]
2584 fn apply_version_defaults_k3s_version_cases() {
2585 const EMBEDDED: &str = "v.yml";
2586
2587 struct Case {
2588 name: &'static str,
2589 colima: Option<ColimaConfig>,
2590 versions: HashMap<String, String>,
2591 want: &'static str,
2592 }
2593
2594 let cases = vec![
2595 Case {
2596 name: "defaults from embedded when empty",
2597 colima: Some(ColimaConfig {
2598 memory: "8GiB".into(),
2599 cores: 4,
2600 ..Default::default()
2601 }),
2602 versions: HashMap::from([(
2603 EMBEDDED.to_string(),
2604 "variables:\n GL_K3S_VERSION: v1.36.1+k3s1\n".to_string(),
2605 )]),
2606 want: "v1.36.1+k3s1",
2607 },
2608 Case {
2609 name: "caproni.yaml value wins",
2610 colima: Some(ColimaConfig {
2611 memory: "8GiB".into(),
2612 cores: 4,
2613 k3s_version: "v1.30.0+k3s1".into(),
2614 ..Default::default()
2615 }),
2616 versions: HashMap::from([(
2617 EMBEDDED.to_string(),
2618 "variables:\n GL_K3S_VERSION: v1.36.1+k3s1\n".to_string(),
2619 )]),
2620 want: "v1.30.0+k3s1",
2621 },
2622 Case {
2623 name: "missing key leaves empty",
2624 colima: Some(ColimaConfig {
2625 memory: "8GiB".into(),
2626 cores: 4,
2627 ..Default::default()
2628 }),
2629 versions: HashMap::new(),
2630 want: "",
2631 },
2632 Case {
2633 name: "no colima config is a no-op",
2634 colima: None,
2635 versions: HashMap::from([(
2636 EMBEDDED.to_string(),
2637 "variables:\n GL_K3S_VERSION: v1.36.1+k3s1\n".to_string(),
2638 )]),
2639 want: "",
2640 },
2641 ];
2642
2643 for case in cases {
2644 let mut cfg = CaproniConfig {
2645 cluster: crate::config::types::ClusterConfig {
2646 driver: "colima".into(),
2647 colima: case.colima.clone(),
2648 ..Default::default()
2649 },
2650 ..Default::default()
2651 };
2652 let mut gc = GlobalConfig::new(Some(case.versions));
2653
2654 apply_version_defaults(&mut cfg, &mut gc);
2655
2656 if case.colima.is_none() {
2657 assert!(cfg.cluster.colima.is_none(), "{}", case.name);
2658 continue;
2659 }
2660
2661 assert_eq!(
2662 cfg.cluster.colima.unwrap().k3s_version,
2663 case.want,
2664 "{}",
2665 case.name
2666 );
2667 }
2668 }
2669}