1use serde::de::Error;
41use serde::{Deserialize, Serialize};
42
43const MAJOR: u64 = 1;
44const MINOR: u64 = 1;
45const PATCH: u64 = 0;
46
47const SEM_VERSION: semver::Version = semver::Version::new(MAJOR, MINOR, PATCH);
48
49pub const VERSION: &str = const_format::concatcp!(MAJOR, ".", MINOR, ".", PATCH);
51
52#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
54pub struct EnvVar {
55 pub key: String,
56 pub value: String,
57}
58
59#[derive(Clone, Debug, Deserialize, Serialize)]
61pub struct Mount {
62 pub source: String,
63 pub target: String,
64 pub fstype: Option<String>,
65 pub flags: Option<u64>,
66 pub data: Option<String>,
67}
68
69impl Mount {
70 pub fn virtio9p(source: String, target: String) -> Self {
74 Self {
75 source,
76 target,
77 fstype: Some("9p".to_string()),
78 flags: None,
79 data: Some("trans=virtio,version=9p2000.L".to_string()),
80 }
81 }
82}
83
84#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
86#[serde(tag = "arch")]
87pub enum DebugExit {
88 #[serde(rename = "x86")]
92 X86 {
93 iobase: u16,
94 iosize: u16,
97 #[serde(rename = "success-code")]
100 success_code: u32,
101 },
102}
103
104impl DebugExit {
105 pub const fn default_x86() -> Self {
106 Self::X86 {
107 iobase: 0xf4,
108 iosize: 0x4,
109 success_code: 0x3,
110 }
111 }
112}
113
114fn true_value() -> bool {
115 true
116}
117
118fn deserialize_version<'de, D>(deserializer: D) -> Result<String, D::Error>
119where
120 D: serde::Deserializer<'de>,
121{
122 let mut version = String::deserialize(deserializer)?;
123 let split = version.split('.').collect::<Vec<_>>();
125 match split.len() {
126 1 => {
127 version.push_str(".0.0");
128 }
129 2 => {
130 version.push_str(".0");
131 }
132 3 => {}
133 _ => {
134 return Err(D::Error::custom(
135 "Gevulot runtime config: invalid version string",
136 ));
137 }
138 }
139 let semversion = semver::Version::parse(&version).map_err(|err| {
141 D::Error::custom(format!(
142 "Gevulot runtime config: failed to parse version: {}",
143 err
144 ))
145 })?;
146 if semversion.major != SEM_VERSION.major || semversion > SEM_VERSION {
147 return Err(D::Error::custom(
148 "Gevulot runtime config: unsupported version",
149 ));
150 }
151 Ok(version)
152}
153
154#[derive(Clone, Debug, Default, Deserialize, Serialize)]
158#[serde(deny_unknown_fields, rename_all = "kebab-case")]
159pub struct RuntimeConfig {
160 #[serde(deserialize_with = "deserialize_version")]
162 pub version: String,
163
164 pub command: Option<String>,
166
167 #[serde(default)]
169 pub args: Vec<String>,
170
171 #[serde(default)]
173 pub env: Vec<EnvVar>,
174
175 pub working_dir: Option<String>,
177
178 #[serde(default)]
180 pub mounts: Vec<Mount>,
181
182 #[serde(default = "true_value")]
188 pub default_mounts: bool,
189
190 #[serde(default)]
192 pub kernel_modules: Vec<String>,
193
194 pub debug_exit: Option<DebugExit>,
198
199 #[serde(default)]
203 pub bootcmd: Vec<Vec<String>>,
204
205 pub follow_config: Option<String>,
207}
208
209#[cfg(test)]
218mod tests {
219 use super::{DebugExit, EnvVar, RuntimeConfig};
220
221 #[test]
222 fn test_deserialize_version_ok() {
223 let source = "
224 version: 1
225 command: echo
226 ";
227 let result = serde_yaml::from_str::<RuntimeConfig>(source);
228 result.expect("deserialization should succeed");
229 }
230
231 #[test]
232 fn test_deserialize_version_fail_1() {
233 let source = "
234 version: 0
235 commands: echo
236 ";
237 let result = serde_yaml::from_str::<RuntimeConfig>(source);
238 assert!(result.is_err());
239 let err = result.err().unwrap();
240 assert_eq!(
241 err.to_string(),
242 "Gevulot runtime config: unsupported version at line 2 column 9".to_string()
243 );
244 }
245
246 #[test]
247 fn test_deserialize_version_fail_2() {
248 let source = "
249 abracadabra: 0
250 version: 123
251 ";
252 let result = serde_yaml::from_str::<RuntimeConfig>(source);
253 assert!(result.is_err());
254 }
256
257 const EXAMPLE_CONFIG: &str = "
258 version: 1
259 working-dir: /
260 command: prover
261 args: [--log, info]
262 env:
263 - key: TMPDIR
264 value: /tmp
265 mounts:
266 - source: input-1
267 target: /input/1
268 default-mounts: true
269 kernel-modules:
270 - nvidia
271 debug-exit:
272 arch: x86
273 iobase: 0xf4
274 iosize: 0x4
275 success-code: 0x3
276 bootcmd:
277 - [echo, booting]
278 follow-config: /my/local/config.yaml
279 ";
280
281 #[test]
282 fn test_deserialization_example_config() {
283 let result = serde_yaml::from_str::<RuntimeConfig>(EXAMPLE_CONFIG)
284 .expect("deserialization should succeed");
285 assert_eq!(
286 &result.command.expect("command should be present"),
287 "prover"
288 );
289 assert_eq!(result.args, vec!["--log".to_string(), "info".to_string()]);
290 assert_eq!(result.env.len(), 1);
291 assert_eq!(
292 result.env[0],
293 EnvVar {
294 key: "TMPDIR".to_string(),
295 value: "/tmp".to_string()
296 }
297 );
298 assert_eq!(
299 &result.working_dir.expect("working dir should be present"),
300 "/"
301 );
302 assert_eq!(result.mounts.len(), 1);
303 assert_eq!(result.mounts[0].source, "input-1".to_string());
304 assert_eq!(result.mounts[0].target, "/input/1".to_string());
305 assert_eq!(result.mounts[0].fstype, None);
306 assert_eq!(result.mounts[0].flags, None);
307 assert_eq!(result.mounts[0].data, None);
308 assert!(result.default_mounts);
309 assert_eq!(result.kernel_modules, vec!["nvidia".to_string()]);
310 assert_eq!(result.debug_exit, Some(DebugExit::default_x86()));
311 assert_eq!(result.bootcmd, vec![vec!["echo", "booting"]]);
312 assert_eq!(
313 &result
314 .follow_config
315 .expect("follow config should be present"),
316 "/my/local/config.yaml"
317 );
318 }
319}