1use std::fs;
2use std::process::Stdio;
3
4use anyhow::{
5 anyhow,
6 bail,
7};
8use sk_api::v1::SimulationHooksConfig;
9use tokio::io::{
10 AsyncWriteExt,
11 BufWriter,
12};
13use tokio::process::Command;
14use tracing::*;
15
16use crate::prelude::*;
17
18#[derive(Debug)]
19pub enum Type {
20 PreStart,
21 PreRun,
22 PostRun,
23 PostStop,
24}
25
26pub fn merge_hooks(maybe_files: &Option<Vec<String>>) -> anyhow::Result<Option<SimulationHooksConfig>> {
27 let Some(files) = maybe_files else {
28 return Ok(None);
29 };
30 if files.is_empty() {
31 return Ok(None);
32 }
33
34 Some(files.iter().try_fold(SimulationHooksConfig::default(), |mut merged_hooks, f| {
35 let next = serde_yaml::from_slice::<SimulationHooksConfig>(
36 &fs::read(f).map_err(|e| anyhow!("error reading hook {f}: {e}"))?,
37 )
38 .map_err(|e| anyhow!("error parsing hook {f}: {e}"))?;
39 merge_vecs(&mut merged_hooks.pre_start_hooks, next.pre_start_hooks);
40 merge_vecs(&mut merged_hooks.pre_run_hooks, next.pre_run_hooks);
41 merge_vecs(&mut merged_hooks.post_run_hooks, next.post_run_hooks);
42 merge_vecs(&mut merged_hooks.post_stop_hooks, next.post_stop_hooks);
43 Ok(merged_hooks)
44 }))
45 .transpose()
46}
47
48pub async fn execute(sim: &Simulation, type_: Type) -> EmptyResult {
49 let maybe_hooks = match &sim.spec.hooks {
50 Some(hooks_config) => match type_ {
51 Type::PreStart => hooks_config.pre_start_hooks.as_ref(),
52 Type::PreRun => hooks_config.pre_run_hooks.as_ref(),
53 Type::PostRun => hooks_config.post_run_hooks.as_ref(),
54 Type::PostStop => hooks_config.post_stop_hooks.as_ref(),
55 },
56 _ => None,
57 };
58
59 if let Some(hooks) = maybe_hooks {
60 info!("Executing {:?} hooks", type_);
61
62 for hook in hooks {
63 info!("Running `{}` with args {:?}", hook.cmd, hook.args);
64 let mut child = Command::new(hook.cmd.clone())
65 .args(hook.args.clone().unwrap_or_default())
66 .stdin(Stdio::piped())
67 .stdout(Stdio::piped())
68 .stderr(Stdio::piped())
69 .spawn()?;
70 if let Some(true) = hook.send_sim {
71 let mut stdin = BufWriter::new(child.stdin.take().ok_or(anyhow!("could not take stdin"))?);
72 stdin.write_all(&serde_json::to_vec(sim)?).await?;
73 stdin.flush().await?;
74 }
75 let output = child.wait_with_output().await?;
76 info!("Hook output: {:?}", output);
77 match hook.ignore_failure {
78 Some(true) => (),
79 _ => {
80 if !output.status.success() {
81 bail!("hook failed");
82 }
83 },
84 }
85 }
86 info!("Done executing {:?} hooks", type_);
87 };
88
89 Ok(())
90}
91
92fn merge_vecs<T>(maybe_v1: &mut Option<Vec<T>>, maybe_v2: Option<Vec<T>>) {
93 if let Some(v2) = maybe_v2 {
94 if let Some(v1) = maybe_v1 {
95 v1.extend(v2)
96 } else {
97 *maybe_v1 = Some(v2)
98 }
99 }
100}
101
102#[cfg(test)]
103#[cfg_attr(coverage, coverage(off))]
104mod test {
105 use assert_fs::prelude::*;
106 use sk_testutils::*;
107
108 use super::*;
109
110 const HOOK1: &str = r#"
111---
112preStartHooks:
113 - cmd: prestart1
114 args:
115 - prestart-arg1
116 - prestart-arg2
117preRunHooks:
118 - cmd: prerun1
119 args:
120 - prerun-arg1
121postRunHooks:
122 - cmd: postrun1
123 args:
124 - postrun-arg1
125"#;
126
127 const HOOK2: &str = r#"
128---
129preStartHooks:
130 - cmd: prestart2
131 - cmd: prestart3
132preRunHooks:
133 - cmd: prerun2
134 args:
135 - prerun-arg2
136postStopHooks:
137 - cmd: poststop1
138 args:
139 - poststop-arg1
140"#;
141
142 const HOOK3: &str = r#"
143---
144preRunHooks:
145 - cmd: prerun3
146 args:
147 - prerun-arg3
148postRunHooks:
149 - cmd: postrun2
150 args:
151 - prerun-arg2
152"#;
153
154 const EXPECTED_MERGED: &str = r#"
155---
156preStartHooks:
157 - cmd: prestart1
158 args:
159 - prestart-arg1
160 - prestart-arg2
161 - cmd: prestart2
162 - cmd: prestart3
163preRunHooks:
164 - cmd: prerun1
165 args:
166 - prerun-arg1
167 - cmd: prerun2
168 args:
169 - prerun-arg2
170 - cmd: prerun3
171 args:
172 - prerun-arg3
173postRunHooks:
174 - cmd: postrun1
175 args:
176 - postrun-arg1
177 - cmd: postrun2
178 args:
179 - prerun-arg2
180postStopHooks:
181 - cmd: poststop1
182 args:
183 - poststop-arg1
184"#;
185
186 #[rstest]
187 fn test_merge_hooks() {
188 let temp = assert_fs::TempDir::new().unwrap();
189 let hook1 = temp.child("hook1.yml");
190 hook1.write_str(HOOK1).unwrap();
191 let hook2 = temp.child("hook2.yml");
192 hook2.write_str(HOOK2).unwrap();
193 let hook3 = temp.child("hook3.yml");
194 hook3.write_str(HOOK3).unwrap();
195
196 let merged_config = merge_hooks(&Some(vec![
197 hook1.path().to_str().unwrap().into(),
198 hook2.path().to_str().unwrap().into(),
199 hook3.path().to_str().unwrap().into(),
200 ]))
201 .unwrap()
202 .unwrap();
203 assert_eq!(merged_config, serde_yaml::from_str(EXPECTED_MERGED).unwrap());
204 }
205
206 #[rstest(tokio::test)]
207 async fn test_execute_hooks(test_sim: Simulation) {
208 let res = execute(&test_sim, Type::PreStart).await;
210 assert!(res.is_ok());
211
212 let res = execute(&test_sim, Type::PostStop).await;
214 assert!(res.is_ok());
215
216 let res = execute(&test_sim, Type::PreRun).await;
218 assert!(res.is_err());
219 }
220}