1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use super::origin::OriginSpec;
6use super::profile_spec::{EncryptionConstraint, ManagedFileSpec, PackagesSpec, SecretSpec};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase", deny_unknown_fields)]
12pub struct SourceSpec {
13 pub name: String,
14 pub origin: OriginSpec,
15 #[serde(default)]
16 pub subscription: SubscriptionSpec,
17 #[serde(default)]
18 pub sync: SourceSyncSpec,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase", deny_unknown_fields)]
23pub struct SubscriptionSpec {
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub profile: Option<String>,
26 #[serde(default = "default_source_priority")]
27 pub priority: u32,
28 #[serde(default)]
29 pub accept_recommended: bool,
30 #[serde(default)]
31 pub opt_in: Vec<String>,
32 #[serde(default)]
33 pub overrides: serde_yaml::Value,
34 #[serde(default)]
35 pub reject: serde_yaml::Value,
36}
37
38impl Default for SubscriptionSpec {
39 fn default() -> Self {
40 Self {
41 profile: None,
42 priority: default_source_priority(),
43 accept_recommended: false,
44 opt_in: Vec::new(),
45 overrides: serde_yaml::Value::Null,
46 reject: serde_yaml::Value::Null,
47 }
48 }
49}
50
51fn default_source_priority() -> u32 {
52 500
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase", deny_unknown_fields)]
57pub struct SourceSyncSpec {
58 #[serde(default = "default_sync_interval")]
59 pub interval: String,
60 #[serde(default)]
61 pub auto_apply: bool,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub pin_version: Option<String>,
64}
65
66impl Default for SourceSyncSpec {
67 fn default() -> Self {
68 Self {
69 interval: default_sync_interval(),
70 auto_apply: false,
71 pin_version: None,
72 }
73 }
74}
75
76pub(super) fn default_sync_interval() -> String {
77 "1h".to_string()
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct ConfigSourceDocument {
85 pub api_version: String,
86 pub kind: String,
87 pub metadata: ConfigSourceMetadata,
88 pub spec: ConfigSourceSpec,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase", deny_unknown_fields)]
93pub struct ConfigSourceMetadata {
94 pub name: String,
95 #[serde(default)]
96 pub version: Option<String>,
97 #[serde(default)]
98 pub description: Option<String>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase", deny_unknown_fields)]
103pub struct ConfigSourceSpec {
104 #[serde(default)]
105 pub provides: ConfigSourceProvides,
106 #[serde(default)]
107 pub policy: ConfigSourcePolicy,
108}
109
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase", deny_unknown_fields)]
112pub struct ConfigSourceProvides {
113 #[serde(default)]
114 pub profiles: Vec<String>,
115 #[serde(default)]
116 pub profile_details: Vec<ConfigSourceProfileEntry>,
117 #[serde(default)]
118 pub platform_profiles: HashMap<String, String>,
119 #[serde(default)]
120 pub modules: Vec<String>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase", deny_unknown_fields)]
127pub struct ConfigSourceProfileEntry {
128 pub name: String,
129 #[serde(default)]
130 pub description: Option<String>,
131 #[serde(default)]
132 pub path: Option<String>,
133 #[serde(default)]
134 pub inherits: Vec<String>,
135}
136
137#[derive(Debug, Clone, Default, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase", deny_unknown_fields)]
139pub struct ConfigSourcePolicy {
140 #[serde(default)]
141 pub required: PolicyItems,
142 #[serde(default)]
143 pub recommended: PolicyItems,
144 #[serde(default)]
145 pub optional: PolicyItems,
146 #[serde(default)]
147 pub locked: PolicyItems,
148 #[serde(default)]
149 pub constraints: SourceConstraints,
150}
151
152#[derive(Debug, Clone, Serialize, PartialEq)]
153#[serde(rename_all = "camelCase")]
154pub struct EnvVar {
155 pub name: String,
156 pub value: String,
157}
158
159impl<'de> Deserialize<'de> for EnvVar {
160 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
161 where
162 D: serde::Deserializer<'de>,
163 {
164 #[derive(Deserialize)]
165 #[serde(rename_all = "camelCase", deny_unknown_fields)]
166 struct Raw {
167 name: String,
168 value: String,
169 }
170 let raw = Raw::deserialize(deserializer)?;
171 crate::validate_env_var_user_name(&raw.name).map_err(serde::de::Error::custom)?;
172 Ok(EnvVar {
173 name: raw.name,
174 value: raw.value,
175 })
176 }
177}
178
179#[derive(Debug, Clone, Serialize, PartialEq)]
180#[serde(rename_all = "camelCase")]
181pub struct ShellAlias {
182 pub name: String,
183 pub command: String,
184}
185
186impl<'de> Deserialize<'de> for ShellAlias {
187 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
188 where
189 D: serde::Deserializer<'de>,
190 {
191 #[derive(Deserialize)]
192 #[serde(rename_all = "camelCase", deny_unknown_fields)]
193 struct Raw {
194 name: String,
195 command: String,
196 }
197 let raw = Raw::deserialize(deserializer)?;
198 crate::validate_alias_name(&raw.name).map_err(serde::de::Error::custom)?;
199 Ok(ShellAlias {
200 name: raw.name,
201 command: raw.command,
202 })
203 }
204}
205
206#[derive(Debug, Clone, Default, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase", deny_unknown_fields)]
208pub struct PolicyItems {
209 #[serde(default)]
210 pub packages: Option<PackagesSpec>,
211 #[serde(default)]
212 pub files: Vec<ManagedFileSpec>,
213 #[serde(default)]
214 pub env: Vec<EnvVar>,
215 #[serde(default)]
216 pub aliases: Vec<ShellAlias>,
217 #[serde(default)]
218 pub system: HashMap<String, serde_yaml::Value>,
219 #[serde(default)]
220 pub profiles: Vec<String>,
221 #[serde(default)]
222 pub modules: Vec<String>,
223 #[serde(default)]
224 pub secrets: Vec<SecretSpec>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase", deny_unknown_fields)]
229pub struct SourceConstraints {
230 #[serde(default = "default_true")]
231 pub no_scripts: bool,
232 #[serde(default = "default_true")]
233 pub no_secrets_read: bool,
234 #[serde(default)]
235 pub allowed_target_paths: Vec<String>,
236 #[serde(default)]
237 pub allow_system_changes: bool,
238 #[serde(default)]
241 pub require_signed_commits: bool,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub encryption: Option<EncryptionConstraint>,
245}
246
247impl Default for SourceConstraints {
248 fn default() -> Self {
249 Self {
250 no_scripts: true,
251 no_secrets_read: true,
252 allowed_target_paths: Vec::new(),
253 allow_system_changes: false,
254 require_signed_commits: false,
255 encryption: None,
256 }
257 }
258}
259
260pub(super) fn default_true() -> bool {
261 true
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn source_spec_rejects_unknown_field() {
270 let yaml = r#"name: team
272origin:
273 type: Git
274 url: https://example.com/x.git
275bogusField: 1
276"#;
277 let err = serde_yaml::from_str::<SourceSpec>(yaml)
278 .expect_err("expected deny_unknown_fields to reject bogusField");
279 let msg = format!("{}", err);
280 assert!(
281 msg.contains("unknown field") && msg.contains("bogusField"),
282 "expected unknown-field error mentioning bogusField, got: {msg}"
283 );
284 }
285
286 #[test]
287 fn subscription_spec_rejects_unknown_field() {
288 let yaml = "priority: 100\nautoApply: true\n";
289 let err = serde_yaml::from_str::<SubscriptionSpec>(yaml)
290 .expect_err("expected deny_unknown_fields to reject autoApply (belongs on sync)");
291 assert!(format!("{}", err).contains("unknown field"));
292 }
293
294 #[test]
295 fn env_var_rejects_cfgd_prefix_at_parse_time() {
296 let yaml = r#"
297- name: CFGD_FOO
298 value: bar
299"#;
300 let err = serde_yaml::from_str::<Vec<EnvVar>>(yaml)
301 .expect_err("CFGD_* env var names must be rejected");
302 let msg = format!("{err}");
303 assert!(
304 msg.contains("reserved"),
305 "error should mention 'reserved': {msg}"
306 );
307 assert!(
308 msg.contains("CFGD_FOO"),
309 "error should name the offending var: {msg}"
310 );
311 }
312
313 #[test]
314 fn env_var_accepts_normal_names() {
315 let yaml = r#"
316- name: MY_APP_KEY
317 value: hello
318- name: PATH
319 value: /usr/bin
320"#;
321 let vars: Vec<EnvVar> = serde_yaml::from_str(yaml).unwrap();
322 assert_eq!(vars.len(), 2);
323 assert_eq!(vars[0].name, "MY_APP_KEY");
324 assert_eq!(vars[1].name, "PATH");
325 }
326}