Skip to main content

xianyun_core/config/
version.rs

1//! Port of `internal/config/version.go`: configuration version detection.
2
3use thiserror::Error;
4
5#[derive(Debug, Error, PartialEq, Eq)]
6pub enum VersionError {
7    #[error("unsupported config version")]
8    Unsupported,
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
12pub enum ConfigVersion {
13    #[default]
14    Unknown,
15    V2,
16}
17
18impl std::fmt::Display for ConfigVersion {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            ConfigVersion::Unknown => write!(f, "unknown"),
22            ConfigVersion::V2 => write!(f, "v2"),
23        }
24    }
25}
26
27/// Detects the configuration version from a file path.
28///
29/// Port of `DetectVersionFromFile`.
30pub fn detect_version_from_file(path: &str) -> Result<ConfigVersion, super::errors::ConfigError> {
31    let data = std::fs::read(path)
32        .map_err(|_| super::errors::ConfigError::ConfigFileNotFound(path.to_string()))?;
33    detect_version_from_bytes(&data)
34}
35
36/// Detects the configuration version from raw YAML bytes.
37///
38/// Port of `DetectVersionFromBytes`.
39pub fn detect_version_from_bytes(data: &[u8]) -> Result<ConfigVersion, super::errors::ConfigError> {
40    let raw: serde_yaml::Value = serde_yaml::from_slice(data)
41        .map_err(|e| super::errors::ConfigError::InvalidYAML(e.to_string()))?;
42
43    let version = detect_config_version(&raw)?;
44    match version {
45        2 => Ok(ConfigVersion::V2),
46        _ => Ok(ConfigVersion::Unknown),
47    }
48}
49
50/// Reads the required `version` field from raw YAML. The field is mandatory:
51/// a missing field is an error, not a defaulted version.
52///
53/// Port of `detectConfigVersion` (`internal/config/caproni.go`).
54fn detect_config_version(raw: &serde_yaml::Value) -> Result<i64, super::errors::ConfigError> {
55    let map = raw
56        .as_mapping()
57        .ok_or(super::errors::ConfigError::MissingVersion)?;
58
59    let version = map
60        .get(serde_yaml::Value::String("version".to_string()))
61        .ok_or(super::errors::ConfigError::MissingVersion)?;
62
63    version
64        .as_i64()
65        .ok_or(super::errors::ConfigError::InvalidVersion)
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::config::errors::ConfigError;
72    use crate::config::global::GlobalConfig;
73    use std::fs;
74
75    fn write_temp_config(content: &str) -> (tempfile::TempDir, String) {
76        let dir = tempfile::tempdir().unwrap();
77        let path = dir.path().join("caproni.yaml");
78        fs::write(&path, content).unwrap();
79        let path_str = path.to_string_lossy().into_owned();
80        (dir, path_str)
81    }
82
83    // Port of TestVersionDetection_V2.
84    #[test]
85    fn version_detection_v2() {
86        let v2_config = r#"
87version: 2
88cluster:
89  driver: mock
90  network:
91    hostname: gitlab.test
92repositories:
93  test-repo:
94    repository: https://example.com/test.git
95    directory: ./test
96deployers:
97  test-deployer:
98    type: helm
99    helm:
100      namespace: default
101      repository:
102        name: test
103        url: https://charts.example.com
104      release:
105        name: test
106        chart: test/test
107reloaders:
108  test-reloader:
109    type: mirror
110    repository: test-repo
111    mirror:
112      target:
113        namespace: default
114        kubernetes_type: pod
115        name: test-pod
116      edit_mode_processes:
117        test-process:
118          command: echo test
119"#;
120        let (_dir, path) = write_temp_config(v2_config);
121
122        let gc = GlobalConfig {
123            config_file: path,
124            ..Default::default()
125        };
126        let cfg = crate::config::caproni::load_caproni_config(&gc).expect("load should succeed");
127
128        assert_eq!(cfg.version, 2);
129        assert_eq!(cfg.repositories.map(|r| r.len()).unwrap_or_default(), 1);
130        assert_eq!(cfg.deployers.map(|d| d.len()).unwrap_or_default(), 1);
131        assert_eq!(cfg.reloaders.map(|r| r.len()).unwrap_or_default(), 1);
132    }
133
134    // Port of TestVersionDetection_UnsupportedVersion.
135    #[test]
136    fn version_detection_unsupported_version() {
137        let invalid_config = "\nversion: 99\ncluster:\n  driver: mock\n  network:\n    hostname: gitlab.test\nrepositories: {}\n";
138        let (_dir, path) = write_temp_config(invalid_config);
139
140        let gc = GlobalConfig {
141            config_file: path,
142            ..Default::default()
143        };
144        let err = crate::config::caproni::load_caproni_config(&gc).unwrap_err();
145        assert!(err.to_string().contains("unsupported config version: 99"));
146    }
147
148    // Port of TestDetectVersionFromFile.
149    #[test]
150    fn detect_version_from_file_errors_when_version_field_missing() {
151        let no_version_config = "cluster:\n  driver: colima\n\nrepositories:\n  test-repo:\n    repository: https://example.com/test.git\n    directory: ./test/\n";
152        let (_dir, path) = write_temp_config(no_version_config);
153
154        let err = detect_version_from_file(&path).unwrap_err();
155        assert_eq!(err, ConfigError::MissingVersion);
156    }
157
158    #[test]
159    fn detect_version_from_file_detects_v2_with_version_field() {
160        let v2_config = "version: 2\ncluster:\n  driver: colima\n\nrepositories:\n  test-repo:\n    repository: https://example.com/test.git\n    directory: ./test/\n";
161        let (_dir, path) = write_temp_config(v2_config);
162
163        let version = detect_version_from_file(&path).unwrap();
164        assert_eq!(version, ConfigVersion::V2);
165    }
166
167    #[test]
168    fn detect_version_from_file_returns_error_for_nonexistent_file() {
169        let err = detect_version_from_file("/nonexistent/caproni.yaml");
170        assert!(err.is_err());
171    }
172
173    #[test]
174    fn detect_version_from_file_returns_error_for_invalid_yaml() {
175        let (_dir, path) = write_temp_config("invalid: yaml: content: [[[");
176        assert!(detect_version_from_file(&path).is_err());
177    }
178
179    // Port of TestDetectVersionFromBytes.
180    #[test]
181    fn detect_version_from_bytes_errors_when_version_field_missing() {
182        let no_version_config = b"cluster:\n  driver: colima\n\nrepositories:\n  test-repo:\n    repository: https://example.com/test.git\n    directory: ./test/\n";
183        let err = detect_version_from_bytes(no_version_config).unwrap_err();
184        assert_eq!(err, ConfigError::MissingVersion);
185    }
186
187    #[test]
188    fn detect_version_from_bytes_detects_v2() {
189        let v2_config = b"version: 2\ncluster:\n  driver: colima\n";
190        let version = detect_version_from_bytes(v2_config).unwrap();
191        assert_eq!(version, ConfigVersion::V2);
192    }
193
194    #[test]
195    fn detect_version_from_bytes_returns_error_for_invalid_yaml() {
196        assert!(detect_version_from_bytes(b"invalid: yaml: [[[").is_err());
197    }
198
199    // Port of TestConfigVersionString.
200    #[test]
201    fn config_version_string_cases() {
202        assert_eq!(ConfigVersion::Unknown.to_string(), "unknown");
203        assert_eq!(ConfigVersion::V2.to_string(), "v2");
204    }
205}