1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9use crate::error::{OciError, Result};
10use crate::state::State;
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct Hooks {
23 #[deprecated(note = "Use createRuntime instead")]
25 #[serde(default, skip_serializing_if = "Vec::is_empty")]
26 pub prestart: Vec<Hook>,
27
28 #[serde(default, skip_serializing_if = "Vec::is_empty")]
30 pub create_runtime: Vec<Hook>,
31
32 #[serde(default, skip_serializing_if = "Vec::is_empty")]
34 pub create_container: Vec<Hook>,
35
36 #[serde(default, skip_serializing_if = "Vec::is_empty")]
38 pub start_container: Vec<Hook>,
39
40 #[serde(default, skip_serializing_if = "Vec::is_empty")]
42 pub poststart: Vec<Hook>,
43
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub poststop: Vec<Hook>,
47}
48
49impl Hooks {
50 #[must_use]
52 pub fn new() -> Self {
53 Self::default()
54 }
55
56 #[allow(deprecated)]
58 #[must_use]
59 pub fn is_empty(&self) -> bool {
60 self.prestart.is_empty()
61 && self.create_runtime.is_empty()
62 && self.create_container.is_empty()
63 && self.start_container.is_empty()
64 && self.poststart.is_empty()
65 && self.poststop.is_empty()
66 }
67
68 pub fn validate(&self) -> Result<()> {
70 #[allow(deprecated)]
71 for hook in &self.prestart {
72 hook.validate()?;
73 }
74 for hook in &self.create_runtime {
75 hook.validate()?;
76 }
77 for hook in &self.create_container {
78 hook.validate()?;
79 }
80 for hook in &self.start_container {
81 hook.validate()?;
82 }
83 for hook in &self.poststart {
84 hook.validate()?;
85 }
86 for hook in &self.poststop {
87 hook.validate()?;
88 }
89 Ok(())
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct Hook {
98 pub path: PathBuf,
101
102 #[serde(default, skip_serializing_if = "Vec::is_empty")]
104 pub args: Vec<String>,
105
106 #[serde(default, skip_serializing_if = "Vec::is_empty")]
108 pub env: Vec<String>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub timeout: Option<u32>,
113}
114
115impl Hook {
116 #[must_use]
118 pub fn new<P: Into<PathBuf>>(path: P) -> Self {
119 Self {
120 path: path.into(),
121 args: Vec::new(),
122 env: Vec::new(),
123 timeout: None,
124 }
125 }
126
127 #[must_use]
129 pub fn with_args(mut self, args: Vec<String>) -> Self {
130 self.args = args;
131 self
132 }
133
134 #[must_use]
136 pub fn with_env(mut self, env: Vec<String>) -> Self {
137 self.env = env;
138 self
139 }
140
141 #[must_use]
143 pub const fn with_timeout(mut self, timeout: u32) -> Self {
144 self.timeout = Some(timeout);
145 self
146 }
147
148 pub fn validate(&self) -> Result<()> {
150 if !self.path.is_absolute() {
152 return Err(OciError::InvalidConfig(format!(
153 "hook path must be absolute: {}",
154 self.path.display()
155 )));
156 }
157
158 if let Some(timeout) = self.timeout {
160 if timeout == 0 {
161 return Err(OciError::InvalidConfig(
162 "hook timeout must be greater than 0".to_string(),
163 ));
164 }
165 }
166
167 Ok(())
168 }
169}
170
171#[derive(Debug, Clone)]
176pub struct HookContext {
177 pub state: State,
179 pub bundle: PathBuf,
181}
182
183impl HookContext {
184 #[must_use]
186 pub const fn new(state: State, bundle: PathBuf) -> Self {
187 Self { state, bundle }
188 }
189
190 pub fn state_json(&self) -> Result<String> {
192 Ok(serde_json::to_string(&self.state)?)
193 }
194}
195
196#[derive(Debug, Clone)]
198pub struct HookResult {
199 pub exit_code: i32,
201 pub stdout: String,
203 pub stderr: String,
205}
206
207impl HookResult {
208 #[must_use]
210 pub const fn success(&self) -> bool {
211 self.exit_code == 0
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum HookType {
218 Prestart,
220 CreateRuntime,
222 CreateContainer,
224 StartContainer,
226 Poststart,
228 Poststop,
230}
231
232impl HookType {
233 #[must_use]
235 pub const fn as_str(&self) -> &'static str {
236 match self {
237 Self::Prestart => "prestart",
238 Self::CreateRuntime => "createRuntime",
239 Self::CreateContainer => "createContainer",
240 Self::StartContainer => "startContainer",
241 Self::Poststart => "poststart",
242 Self::Poststop => "poststop",
243 }
244 }
245}
246
247impl std::fmt::Display for HookType {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 write!(f, "{}", self.as_str())
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_hook_validation_absolute_path() {
259 let hook = Hook::new("/usr/bin/hook");
260 assert!(hook.validate().is_ok());
261 }
262
263 #[test]
264 fn test_hook_validation_relative_path() {
265 let hook = Hook::new("relative/path");
266 assert!(hook.validate().is_err());
267 }
268
269 #[test]
270 fn test_hook_validation_zero_timeout() {
271 let hook = Hook::new("/usr/bin/hook").with_timeout(0);
272 assert!(hook.validate().is_err());
273 }
274
275 #[test]
276 fn test_hook_builder() {
277 let hook = Hook::new("/usr/bin/hook")
278 .with_args(vec!["hook".to_string(), "--config".to_string()])
279 .with_env(vec!["FOO=bar".to_string()])
280 .with_timeout(30);
281
282 assert_eq!(hook.path.to_str().unwrap(), "/usr/bin/hook");
283 assert_eq!(hook.args.len(), 2);
284 assert_eq!(hook.env.len(), 1);
285 assert_eq!(hook.timeout, Some(30));
286 }
287
288 #[test]
289 fn test_hooks_empty() {
290 let hooks = Hooks::new();
291 assert!(hooks.is_empty());
292 }
293
294 #[test]
295 fn test_parse_hooks() {
296 let json = r#"{
297 "createRuntime": [
298 {
299 "path": "/usr/bin/setup-network",
300 "args": ["setup-network", "--type=bridge"],
301 "timeout": 30
302 }
303 ],
304 "poststart": [
305 {
306 "path": "/usr/bin/notify",
307 "env": ["NOTIFY_SOCKET=/run/notify.sock"]
308 }
309 ]
310 }"#;
311
312 let hooks: Hooks = serde_json::from_str(json).unwrap();
313 assert_eq!(hooks.create_runtime.len(), 1);
314 assert_eq!(hooks.poststart.len(), 1);
315 assert!(hooks.poststop.is_empty());
316 }
317
318 #[test]
319 fn test_hooks_not_empty() {
320 let mut hooks = Hooks::new();
321 assert!(hooks.is_empty());
322
323 hooks.create_runtime.push(Hook::new("/usr/bin/hook"));
324 assert!(!hooks.is_empty());
325 }
326
327 #[test]
328 fn test_hooks_validate_all_valid() {
329 let hooks = Hooks {
330 create_runtime: vec![Hook::new("/usr/bin/hook1")],
331 create_container: vec![Hook::new("/usr/bin/hook2")],
332 start_container: vec![Hook::new("/usr/bin/hook3")],
333 poststart: vec![Hook::new("/usr/bin/hook4")],
334 poststop: vec![Hook::new("/usr/bin/hook5")],
335 ..Default::default()
336 };
337
338 assert!(hooks.validate().is_ok());
339 }
340
341 #[test]
342 fn test_hooks_validate_invalid_in_create_runtime() {
343 let hooks = Hooks {
344 create_runtime: vec![Hook::new("relative/path")],
345 ..Default::default()
346 };
347
348 assert!(hooks.validate().is_err());
349 }
350
351 #[test]
352 fn test_hooks_validate_invalid_in_poststart() {
353 let hooks = Hooks {
354 poststart: vec![Hook::new("relative/path")],
355 ..Default::default()
356 };
357
358 assert!(hooks.validate().is_err());
359 }
360
361 #[test]
362 fn test_hooks_validate_invalid_in_poststop() {
363 let hooks = Hooks {
364 poststop: vec![Hook::new("relative/path")],
365 ..Default::default()
366 };
367
368 assert!(hooks.validate().is_err());
369 }
370
371 #[test]
372 fn test_hook_type_as_str() {
373 assert_eq!(HookType::Prestart.as_str(), "prestart");
374 assert_eq!(HookType::CreateRuntime.as_str(), "createRuntime");
375 assert_eq!(HookType::CreateContainer.as_str(), "createContainer");
376 assert_eq!(HookType::StartContainer.as_str(), "startContainer");
377 assert_eq!(HookType::Poststart.as_str(), "poststart");
378 assert_eq!(HookType::Poststop.as_str(), "poststop");
379 }
380
381 #[test]
382 fn test_hook_type_display() {
383 assert_eq!(HookType::Prestart.to_string(), "prestart");
384 assert_eq!(HookType::CreateRuntime.to_string(), "createRuntime");
385 assert_eq!(HookType::Poststart.to_string(), "poststart");
386 }
387
388 #[test]
389 fn test_hook_result_success() {
390 let success = HookResult {
391 exit_code: 0,
392 stdout: "output".to_string(),
393 stderr: String::new(),
394 };
395 assert!(success.success());
396
397 let failure = HookResult {
398 exit_code: 1,
399 stdout: String::new(),
400 stderr: "error".to_string(),
401 };
402 assert!(!failure.success());
403 }
404
405 #[test]
406 fn test_hook_context_new() {
407 let state = State::new("test".to_string(), std::path::PathBuf::from("/bundle"));
408 let context = HookContext::new(state, std::path::PathBuf::from("/bundle"));
409
410 assert_eq!(context.state.id, "test");
411 assert_eq!(context.bundle, std::path::PathBuf::from("/bundle"));
412 }
413
414 #[test]
415 fn test_hook_context_state_json() {
416 let state = State::new(
417 "test-container".to_string(),
418 std::path::PathBuf::from("/bundle"),
419 );
420 let context = HookContext::new(state, std::path::PathBuf::from("/bundle"));
421
422 let json = context.state_json().unwrap();
423 assert!(json.contains("test-container"));
424 assert!(json.contains("creating"));
425 }
426
427 #[test]
428 fn test_hook_serialization() {
429 let hook = Hook::new("/usr/bin/test")
430 .with_args(vec!["test".to_string(), "--flag".to_string()])
431 .with_env(vec!["VAR=value".to_string()])
432 .with_timeout(60);
433
434 let json = serde_json::to_string(&hook).unwrap();
435 assert!(json.contains("/usr/bin/test"));
436 assert!(json.contains("--flag"));
437 assert!(json.contains("VAR=value"));
438 assert!(json.contains("60"));
439
440 let parsed: Hook = serde_json::from_str(&json).unwrap();
442 assert_eq!(parsed.path, hook.path);
443 assert_eq!(parsed.args, hook.args);
444 assert_eq!(parsed.env, hook.env);
445 assert_eq!(parsed.timeout, hook.timeout);
446 }
447
448 #[test]
449 fn test_hooks_serialization_roundtrip() {
450 let hooks = Hooks {
451 create_runtime: vec![Hook::new("/usr/bin/setup").with_timeout(30)],
452 poststart: vec![
453 Hook::new("/usr/bin/notify")
454 .with_args(vec!["notify".to_string()])
455 .with_env(vec!["SOCKET=/run/notify.sock".to_string()]),
456 ],
457 poststop: vec![Hook::new("/usr/bin/cleanup")],
458 ..Default::default()
459 };
460
461 let json = serde_json::to_string(&hooks).unwrap();
462 let parsed: Hooks = serde_json::from_str(&json).unwrap();
463
464 assert_eq!(parsed.create_runtime.len(), 1);
465 assert_eq!(parsed.poststart.len(), 1);
466 assert_eq!(parsed.poststop.len(), 1);
467 }
468
469 #[test]
470 fn test_parse_all_hook_types() {
471 let json = r#"{
472 "createRuntime": [{"path": "/bin/cr"}],
473 "createContainer": [{"path": "/bin/cc"}],
474 "startContainer": [{"path": "/bin/sc"}],
475 "poststart": [{"path": "/bin/ps"}],
476 "poststop": [{"path": "/bin/pp"}]
477 }"#;
478
479 let hooks: Hooks = serde_json::from_str(json).unwrap();
480 assert_eq!(hooks.create_runtime.len(), 1);
481 assert_eq!(hooks.create_container.len(), 1);
482 assert_eq!(hooks.start_container.len(), 1);
483 assert_eq!(hooks.poststart.len(), 1);
484 assert_eq!(hooks.poststop.len(), 1);
485 }
486
487 #[test]
488 fn test_hook_with_valid_timeout() {
489 let hook = Hook::new("/usr/bin/hook").with_timeout(1);
490 assert!(hook.validate().is_ok());
491
492 let hook = Hook::new("/usr/bin/hook").with_timeout(3600);
493 assert!(hook.validate().is_ok());
494 }
495
496 #[test]
497 fn test_hook_no_timeout() {
498 let hook = Hook::new("/usr/bin/hook");
499 assert!(hook.timeout.is_none());
500 assert!(hook.validate().is_ok());
501 }
502
503 #[test]
504 fn test_hook_empty_args_and_env() {
505 let hook = Hook::new("/usr/bin/hook");
506 assert!(hook.args.is_empty());
507 assert!(hook.env.is_empty());
508 }
509
510 #[test]
511 fn test_hook_type_equality() {
512 assert_eq!(HookType::Prestart, HookType::Prestart);
513 assert_ne!(HookType::Prestart, HookType::Poststart);
514 }
515
516 #[test]
517 fn test_hooks_default() {
518 let hooks = Hooks::default();
519 assert!(hooks.is_empty());
520 assert!(hooks.create_runtime.is_empty());
521 assert!(hooks.create_container.is_empty());
522 assert!(hooks.start_container.is_empty());
523 assert!(hooks.poststart.is_empty());
524 assert!(hooks.poststop.is_empty());
525 }
526
527 #[test]
528 fn test_multiple_hooks_per_type() {
529 let hooks = Hooks {
530 poststart: vec![
531 Hook::new("/usr/bin/hook1"),
532 Hook::new("/usr/bin/hook2"),
533 Hook::new("/usr/bin/hook3"),
534 ],
535 ..Default::default()
536 };
537
538 assert_eq!(hooks.poststart.len(), 3);
539 assert!(hooks.validate().is_ok());
540 }
541}