mirror of
				https://github.com/kata-containers/kata-containers.git
				synced 2025-10-31 09:26:52 +00:00 
			
		
		
		
	libs/sys-util: provide functions to execute hooks
Provide functions to execute OCI hooks. Signed-off-by: Liu Jiang <gerry@linux.alibaba.com> Signed-off-by: Bin Liu <bin@hyper.sh> Signed-off-by: Huamin Tang <huamin.thm@alibaba-inc.com> Signed-off-by: Lei Wang <wllenyj@linux.alibaba.com> Signed-off-by: Quanwei Zhou <quanweiZhou@linux.alibaba.com>
This commit is contained in:
		
							
								
								
									
										13
									
								
								src/libs/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								src/libs/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -360,10 +360,13 @@ dependencies = [ | |||||||
|  "libc", |  "libc", | ||||||
|  "nix 0.23.1", |  "nix 0.23.1", | ||||||
|  "num_cpus", |  "num_cpus", | ||||||
|  |  "oci", | ||||||
|  "once_cell", |  "once_cell", | ||||||
|  |  "serde_json", | ||||||
|  "serial_test", |  "serial_test", | ||||||
|  "slog", |  "slog", | ||||||
|  "slog-scope", |  "slog-scope", | ||||||
|  |  "subprocess", | ||||||
|  "tempfile", |  "tempfile", | ||||||
|  "thiserror", |  "thiserror", | ||||||
| ] | ] | ||||||
| @@ -916,6 +919,16 @@ dependencies = [ | |||||||
|  "winapi", |  "winapi", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "subprocess" | ||||||
|  | version = "0.2.9" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" | ||||||
|  | dependencies = [ | ||||||
|  |  "libc", | ||||||
|  |  "winapi", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "syn" | name = "syn" | ||||||
| version = "1.0.91" | version = "1.0.91" | ||||||
|   | |||||||
| @@ -19,12 +19,14 @@ lazy_static = "1.4.0" | |||||||
| libc = "0.2.100" | libc = "0.2.100" | ||||||
| nix = "0.23.0" | nix = "0.23.0" | ||||||
| once_cell = "1.9.0" | once_cell = "1.9.0" | ||||||
|  | serde_json = "1.0.73" | ||||||
| slog = "2.5.2" | slog = "2.5.2" | ||||||
| slog-scope = "4.4.0" | slog-scope = "4.4.0" | ||||||
|  | subprocess = "0.2.8" | ||||||
| thiserror = "1.0.30" | thiserror = "1.0.30" | ||||||
|  |  | ||||||
| kata-types = { path = "../kata-types" } | kata-types = { path = "../kata-types" } | ||||||
| oci = { path = "../../agent/oci" } | oci = { path = "../oci" } | ||||||
|  |  | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| num_cpus = "1.13.1" | num_cpus = "1.13.1" | ||||||
|   | |||||||
							
								
								
									
										541
									
								
								src/libs/kata-sys-util/src/hooks.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										541
									
								
								src/libs/kata-sys-util/src/hooks.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,541 @@ | |||||||
|  | // Copyright (c) 2019-2021 Alibaba Cloud | ||||||
|  | // Copyright (c) 2019-2021 Ant Group | ||||||
|  | // | ||||||
|  | // SPDX-License-Identifier: Apache-2.0 | ||||||
|  | // | ||||||
|  |  | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use std::ffi::OsString; | ||||||
|  | use std::hash::{Hash, Hasher}; | ||||||
|  | use std::io::{self, Read, Result}; | ||||||
|  | use std::path::Path; | ||||||
|  | use std::time::Duration; | ||||||
|  |  | ||||||
|  | use subprocess::{ExitStatus, Popen, PopenConfig, PopenError, Redirection}; | ||||||
|  |  | ||||||
|  | use crate::{eother, sl}; | ||||||
|  |  | ||||||
|  | const DEFAULT_HOOK_TIMEOUT_SEC: i32 = 10; | ||||||
|  |  | ||||||
|  | /// A simple wrapper over `oci::Hook` to provide `Hash, Eq`. | ||||||
|  | /// | ||||||
|  | /// The `oci::Hook` is auto-generated from protobuf source file, which doesn't implement `Hash, Eq`. | ||||||
|  | #[derive(Debug, Default, Clone)] | ||||||
|  | struct HookKey(oci::Hook); | ||||||
|  |  | ||||||
|  | impl From<&oci::Hook> for HookKey { | ||||||
|  |     fn from(hook: &oci::Hook) -> Self { | ||||||
|  |         HookKey(hook.clone()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl PartialEq for HookKey { | ||||||
|  |     fn eq(&self, other: &Self) -> bool { | ||||||
|  |         self.0 == other.0 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Eq for HookKey {} | ||||||
|  |  | ||||||
|  | impl Hash for HookKey { | ||||||
|  |     fn hash<H: Hasher>(&self, state: &mut H) { | ||||||
|  |         self.0.path.hash(state); | ||||||
|  |         self.0.args.hash(state); | ||||||
|  |         self.0.env.hash(state); | ||||||
|  |         self.0.timeout.hash(state); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Execution state of OCI hooks. | ||||||
|  | #[derive(Clone, Copy, Debug, Eq, PartialEq)] | ||||||
|  | pub enum HookState { | ||||||
|  |     /// Hook is pending for executing/retry. | ||||||
|  |     Pending, | ||||||
|  |     /// Hook has been successfully executed. | ||||||
|  |     Done, | ||||||
|  |     /// Hook has been marked as ignore. | ||||||
|  |     Ignored, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Structure to maintain state for hooks. | ||||||
|  | #[derive(Default)] | ||||||
|  | pub struct HookStates { | ||||||
|  |     states: HashMap<HookKey, HookState>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl HookStates { | ||||||
|  |     /// Create a new instance of [`HookStates`]. | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         Self::default() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Get execution state of a hook. | ||||||
|  |     pub fn get(&self, hook: &oci::Hook) -> HookState { | ||||||
|  |         self.states | ||||||
|  |             .get(&hook.into()) | ||||||
|  |             .copied() | ||||||
|  |             .unwrap_or(HookState::Pending) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Update execution state of a hook. | ||||||
|  |     pub fn update(&mut self, hook: &oci::Hook, state: HookState) { | ||||||
|  |         self.states.insert(hook.into(), state); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Remove an execution state of a hook. | ||||||
|  |     pub fn remove(&mut self, hook: &oci::Hook) { | ||||||
|  |         self.states.remove(&hook.into()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Check whether some hooks are still pending and should retry execution. | ||||||
|  |     pub fn should_retry(&self) -> bool { | ||||||
|  |         for state in self.states.values() { | ||||||
|  |             if *state == HookState::Pending { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Execute an OCI hook. | ||||||
|  |     /// | ||||||
|  |     /// If `state` is valid, it will be sent to subprocess' STDIN. | ||||||
|  |     /// | ||||||
|  |     /// The [OCI Runtime specification 1.0.0](https://github.com/opencontainers/runtime-spec/releases/download/v1.0.0/oci-runtime-spec-v1.0.0.pdf) | ||||||
|  |     /// states: | ||||||
|  |     /// - path (string, REQUIRED) with similar semantics to IEEE Std 1003.1-2008 execv's path. | ||||||
|  |     ///   This specification extends the IEEE standard in that path MUST be absolute. | ||||||
|  |     /// - args (array of strings, OPTIONAL) with the same semantics as IEEE Std 1003.1-2008 execv's | ||||||
|  |     ///   argv. | ||||||
|  |     /// - env (array of strings, OPTIONAL) with the same semantics as IEEE Std 1003.1-2008's environ. | ||||||
|  |     /// - timeout (int, OPTIONAL) is the number of seconds before aborting the hook. If set, timeout | ||||||
|  |     ///   MUST be greater than zero. | ||||||
|  |     /// | ||||||
|  |     /// The OCI spec also defines the context to invoke hooks, caller needs to take the responsibility | ||||||
|  |     /// to setup execution context, such as namespace etc. | ||||||
|  |     pub fn execute_hook(&mut self, hook: &oci::Hook, state: Option<oci::State>) -> Result<()> { | ||||||
|  |         if self.get(hook) != HookState::Pending { | ||||||
|  |             return Ok(()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         fail::fail_point!("execute_hook", |_| { | ||||||
|  |             Err(eother!("execute hook fail point injection")) | ||||||
|  |         }); | ||||||
|  |         info!(sl!(), "execute hook {:?}", hook); | ||||||
|  |  | ||||||
|  |         self.states.insert(hook.into(), HookState::Pending); | ||||||
|  |  | ||||||
|  |         let mut executor = HookExecutor::new(hook)?; | ||||||
|  |         let stdin = if state.is_some() { | ||||||
|  |             Redirection::Pipe | ||||||
|  |         } else { | ||||||
|  |             Redirection::None | ||||||
|  |         }; | ||||||
|  |         let mut popen = Popen::create( | ||||||
|  |             &executor.args, | ||||||
|  |             PopenConfig { | ||||||
|  |                 stdin, | ||||||
|  |                 stdout: Redirection::Pipe, | ||||||
|  |                 stderr: Redirection::Pipe, | ||||||
|  |                 executable: executor.executable.to_owned(), | ||||||
|  |                 detached: true, | ||||||
|  |                 env: Some(executor.envs.clone()), | ||||||
|  |                 ..Default::default() | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         .map_err(|e| eother!("failed to create subprocess for hook {:?}: {}", hook, e))?; | ||||||
|  |  | ||||||
|  |         if let Some(state) = state { | ||||||
|  |             executor.execute_with_input(&mut popen, state)?; | ||||||
|  |         } | ||||||
|  |         executor.execute_and_wait(&mut popen)?; | ||||||
|  |         info!(sl!(), "hook {} finished", hook.path); | ||||||
|  |         self.states.insert(hook.into(), HookState::Done); | ||||||
|  |  | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Try to execute hooks and remember execution result. | ||||||
|  |     /// | ||||||
|  |     /// The `execute_hooks()` will be called multiple times. | ||||||
|  |     /// It will first be called before creating the VMM when creating the sandbox, so hooks could be | ||||||
|  |     /// used to setup environment for the VMM, such as creating tap device etc. | ||||||
|  |     /// It will also be called during starting containers, to setup environment for those containers. | ||||||
|  |     /// | ||||||
|  |     /// The execution result will be recorded for each hook. Once a hook returns success, it will not | ||||||
|  |     /// be invoked anymore. | ||||||
|  |     pub fn execute_hooks(&mut self, hooks: &[oci::Hook], state: Option<oci::State>) -> Result<()> { | ||||||
|  |         for hook in hooks.iter() { | ||||||
|  |             if let Err(e) = self.execute_hook(hook, state.clone()) { | ||||||
|  |                 // Ignore error and try next hook, the caller should retry. | ||||||
|  |                 error!(sl!(), "hook {} failed: {}", hook.path, e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct HookExecutor<'a> { | ||||||
|  |     hook: &'a oci::Hook, | ||||||
|  |     executable: Option<OsString>, | ||||||
|  |     args: Vec<String>, | ||||||
|  |     envs: Vec<(OsString, OsString)>, | ||||||
|  |     timeout: u64, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<'a> HookExecutor<'a> { | ||||||
|  |     fn new(hook: &'a oci::Hook) -> Result<Self> { | ||||||
|  |         // Ensure Hook.path is present and is an absolute path. | ||||||
|  |         let executable = if hook.path.is_empty() { | ||||||
|  |             return Err(eother!("path of hook {:?} is empty", hook)); | ||||||
|  |         } else { | ||||||
|  |             let path = Path::new(&hook.path); | ||||||
|  |             if !path.is_absolute() { | ||||||
|  |                 return Err(eother!("path of hook {:?} is not absolute", hook)); | ||||||
|  |             } | ||||||
|  |             Some(path.as_os_str().to_os_string()) | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         // Hook.args is optional, use Hook.path as arg0 if Hook.args is empty. | ||||||
|  |         let args = if hook.args.is_empty() { | ||||||
|  |             vec![hook.path.clone()] | ||||||
|  |         } else { | ||||||
|  |             hook.args.clone() | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         let mut envs: Vec<(OsString, OsString)> = Vec::new(); | ||||||
|  |         for e in hook.env.iter() { | ||||||
|  |             match e.split_once('=') { | ||||||
|  |                 Some((key, value)) => envs.push((OsString::from(key), OsString::from(value))), | ||||||
|  |                 None => warn!(sl!(), "env {} of hook {:?} is invalid", e, hook), | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Use Hook.timeout if it's valid, otherwise default to 10s. | ||||||
|  |         let mut timeout = DEFAULT_HOOK_TIMEOUT_SEC as u64; | ||||||
|  |         if let Some(t) = hook.timeout { | ||||||
|  |             if t > 0 { | ||||||
|  |                 timeout = t as u64; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Ok(HookExecutor { | ||||||
|  |             hook, | ||||||
|  |             executable, | ||||||
|  |             args, | ||||||
|  |             envs, | ||||||
|  |             timeout, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn execute_with_input(&mut self, popen: &mut Popen, state: oci::State) -> Result<()> { | ||||||
|  |         let st = serde_json::to_string(&state)?; | ||||||
|  |         let (stdout, stderr) = popen | ||||||
|  |             .communicate_start(Some(st.as_bytes().to_vec())) | ||||||
|  |             .limit_time(Duration::from_secs(self.timeout)) | ||||||
|  |             .read_string() | ||||||
|  |             .map_err(|e| e.error)?; | ||||||
|  |         if let Some(err) = stderr { | ||||||
|  |             if !err.is_empty() { | ||||||
|  |                 error!(sl!(), "hook {} exec failed: {}", self.hook.path, err); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if let Some(out) = stdout { | ||||||
|  |             if !out.is_empty() { | ||||||
|  |                 info!(sl!(), "hook {} exec stdout: {}", self.hook.path, out); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         // Give a grace period for `execute_and_wait()`. | ||||||
|  |         self.timeout = 1; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn execute_and_wait(&mut self, popen: &mut Popen) -> Result<()> { | ||||||
|  |         match popen.wait_timeout(Duration::from_secs(self.timeout)) { | ||||||
|  |             Ok(v) => self.handle_exit_status(v, popen), | ||||||
|  |             Err(e) => self.handle_popen_wait_error(e, popen), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn handle_exit_status(&mut self, result: Option<ExitStatus>, popen: &mut Popen) -> Result<()> { | ||||||
|  |         if let Some(exit_status) = result { | ||||||
|  |             // the process has finished | ||||||
|  |             info!( | ||||||
|  |                 sl!(), | ||||||
|  |                 "exit status of hook {:?} : {:?}", self.hook, exit_status | ||||||
|  |             ); | ||||||
|  |             self.print_result(popen); | ||||||
|  |             match exit_status { | ||||||
|  |                 subprocess::ExitStatus::Exited(code) => { | ||||||
|  |                     if code == 0 { | ||||||
|  |                         info!(sl!(), "hook {:?} succeeds", self.hook); | ||||||
|  |                         Ok(()) | ||||||
|  |                     } else { | ||||||
|  |                         warn!(sl!(), "hook {:?} exit status with {}", self.hook, code,); | ||||||
|  |                         Err(eother!("hook {:?} exit status with {}", self.hook, code)) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 _ => { | ||||||
|  |                     error!( | ||||||
|  |                         sl!(), | ||||||
|  |                         "no exit code for hook {:?}: {:?}", self.hook, exit_status | ||||||
|  |                     ); | ||||||
|  |                     Err(eother!( | ||||||
|  |                         "no exit code for hook {:?}: {:?}", | ||||||
|  |                         self.hook, | ||||||
|  |                         exit_status | ||||||
|  |                     )) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             // may be timeout | ||||||
|  |             error!(sl!(), "hook poll failed, kill it"); | ||||||
|  |             // it is still running, kill it | ||||||
|  |             popen.kill()?; | ||||||
|  |             let _ = popen.wait(); | ||||||
|  |             self.print_result(popen); | ||||||
|  |             Err(io::Error::from(io::ErrorKind::TimedOut)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn handle_popen_wait_error(&mut self, e: PopenError, popen: &mut Popen) -> Result<()> { | ||||||
|  |         self.print_result(popen); | ||||||
|  |         error!(sl!(), "wait_timeout for hook {:?} failed: {}", self.hook, e); | ||||||
|  |         Err(eother!( | ||||||
|  |             "wait_timeout for hook {:?} failed: {}", | ||||||
|  |             self.hook, | ||||||
|  |             e | ||||||
|  |         )) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn print_result(&mut self, popen: &mut Popen) { | ||||||
|  |         if let Some(file) = popen.stdout.as_mut() { | ||||||
|  |             let mut buffer = String::new(); | ||||||
|  |             file.read_to_string(&mut buffer).ok(); | ||||||
|  |             if !buffer.is_empty() { | ||||||
|  |                 info!(sl!(), "hook stdout: {}", buffer); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if let Some(file) = popen.stderr.as_mut() { | ||||||
|  |             let mut buffer = String::new(); | ||||||
|  |             file.read_to_string(&mut buffer).ok(); | ||||||
|  |             if !buffer.is_empty() { | ||||||
|  |                 info!(sl!(), "hook stderr: {}", buffer); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  |     use std::fs::{self, set_permissions, File, Permissions}; | ||||||
|  |     use std::io::Write; | ||||||
|  |     use std::os::unix::fs::PermissionsExt; | ||||||
|  |     use std::time::Instant; | ||||||
|  |  | ||||||
|  |     fn test_hook_eq(hook1: &oci::Hook, hook2: &oci::Hook, expected: bool) { | ||||||
|  |         let key1 = HookKey::from(hook1); | ||||||
|  |         let key2 = HookKey::from(hook2); | ||||||
|  |  | ||||||
|  |         assert_eq!(key1 == key2, expected); | ||||||
|  |     } | ||||||
|  |     #[test] | ||||||
|  |     fn test_hook_key() { | ||||||
|  |         let hook = oci::Hook { | ||||||
|  |             path: "1".to_string(), | ||||||
|  |             args: vec!["2".to_string(), "3".to_string()], | ||||||
|  |             env: vec![], | ||||||
|  |             timeout: Some(0), | ||||||
|  |         }; | ||||||
|  |         let cases = [ | ||||||
|  |             ( | ||||||
|  |                 oci::Hook { | ||||||
|  |                     path: "1000".to_string(), | ||||||
|  |                     args: vec!["2".to_string(), "3".to_string()], | ||||||
|  |                     env: vec![], | ||||||
|  |                     timeout: Some(0), | ||||||
|  |                 }, | ||||||
|  |                 false, | ||||||
|  |             ), | ||||||
|  |             ( | ||||||
|  |                 oci::Hook { | ||||||
|  |                     path: "1".to_string(), | ||||||
|  |                     args: vec!["2".to_string(), "4".to_string()], | ||||||
|  |                     env: vec![], | ||||||
|  |                     timeout: Some(0), | ||||||
|  |                 }, | ||||||
|  |                 false, | ||||||
|  |             ), | ||||||
|  |             ( | ||||||
|  |                 oci::Hook { | ||||||
|  |                     path: "1".to_string(), | ||||||
|  |                     args: vec!["2".to_string()], | ||||||
|  |                     env: vec![], | ||||||
|  |                     timeout: Some(0), | ||||||
|  |                 }, | ||||||
|  |                 false, | ||||||
|  |             ), | ||||||
|  |             ( | ||||||
|  |                 oci::Hook { | ||||||
|  |                     path: "1".to_string(), | ||||||
|  |                     args: vec!["2".to_string(), "3".to_string()], | ||||||
|  |                     env: vec!["5".to_string()], | ||||||
|  |                     timeout: Some(0), | ||||||
|  |                 }, | ||||||
|  |                 false, | ||||||
|  |             ), | ||||||
|  |             ( | ||||||
|  |                 oci::Hook { | ||||||
|  |                     path: "1".to_string(), | ||||||
|  |                     args: vec!["2".to_string(), "3".to_string()], | ||||||
|  |                     env: vec![], | ||||||
|  |                     timeout: Some(6), | ||||||
|  |                 }, | ||||||
|  |                 false, | ||||||
|  |             ), | ||||||
|  |             ( | ||||||
|  |                 oci::Hook { | ||||||
|  |                     path: "1".to_string(), | ||||||
|  |                     args: vec!["2".to_string(), "3".to_string()], | ||||||
|  |                     env: vec![], | ||||||
|  |                     timeout: None, | ||||||
|  |                 }, | ||||||
|  |                 false, | ||||||
|  |             ), | ||||||
|  |             ( | ||||||
|  |                 oci::Hook { | ||||||
|  |                     path: "1".to_string(), | ||||||
|  |                     args: vec!["2".to_string(), "3".to_string()], | ||||||
|  |                     env: vec![], | ||||||
|  |                     timeout: Some(0), | ||||||
|  |                 }, | ||||||
|  |                 true, | ||||||
|  |             ), | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         for case in cases.iter() { | ||||||
|  |             test_hook_eq(&hook, &case.0, case.1); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn test_execute_hook() { | ||||||
|  |         // test need root permission | ||||||
|  |         if !nix::unistd::getuid().is_root() { | ||||||
|  |             println!("test need root permission"); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let tmpdir = tempfile::tempdir().unwrap(); | ||||||
|  |         let file = tmpdir.path().join("data"); | ||||||
|  |         let file_str = file.to_string_lossy(); | ||||||
|  |         let mut states = HookStates::new(); | ||||||
|  |  | ||||||
|  |         // test case 1: normal | ||||||
|  |         // execute hook | ||||||
|  |         let hook = oci::Hook { | ||||||
|  |             path: "/bin/touch".to_string(), | ||||||
|  |             args: vec!["touch".to_string(), file_str.to_string()], | ||||||
|  |             env: vec![], | ||||||
|  |             timeout: Some(0), | ||||||
|  |         }; | ||||||
|  |         let ret = states.execute_hook(&hook, None); | ||||||
|  |         assert!(ret.is_ok()); | ||||||
|  |         assert!(fs::metadata(&file).is_ok()); | ||||||
|  |         assert!(!states.should_retry()); | ||||||
|  |  | ||||||
|  |         // test case 2: timeout in 10s | ||||||
|  |         let hook = oci::Hook { | ||||||
|  |             path: "/bin/sleep".to_string(), | ||||||
|  |             args: vec!["sleep".to_string(), "3600".to_string()], | ||||||
|  |             env: vec![], | ||||||
|  |             timeout: Some(0), // default timeout is 10 seconds | ||||||
|  |         }; | ||||||
|  |         let start = Instant::now(); | ||||||
|  |         let ret = states.execute_hook(&hook, None).unwrap_err(); | ||||||
|  |         let duration = start.elapsed(); | ||||||
|  |         let used = duration.as_secs(); | ||||||
|  |         assert!((10..12u64).contains(&used)); | ||||||
|  |         assert_eq!(ret.kind(), io::ErrorKind::TimedOut); | ||||||
|  |         assert_eq!(states.get(&hook), HookState::Pending); | ||||||
|  |         assert!(states.should_retry()); | ||||||
|  |         states.remove(&hook); | ||||||
|  |  | ||||||
|  |         // test case 3: timeout in 5s | ||||||
|  |         let hook = oci::Hook { | ||||||
|  |             path: "/bin/sleep".to_string(), | ||||||
|  |             args: vec!["sleep".to_string(), "3600".to_string()], | ||||||
|  |             env: vec![], | ||||||
|  |             timeout: Some(5), // timeout is set to 5 seconds | ||||||
|  |         }; | ||||||
|  |         let start = Instant::now(); | ||||||
|  |         let ret = states.execute_hook(&hook, None).unwrap_err(); | ||||||
|  |         let duration = start.elapsed(); | ||||||
|  |         let used = duration.as_secs(); | ||||||
|  |         assert!((5..7u64).contains(&used)); | ||||||
|  |         assert_eq!(ret.kind(), io::ErrorKind::TimedOut); | ||||||
|  |         assert_eq!(states.get(&hook), HookState::Pending); | ||||||
|  |         assert!(states.should_retry()); | ||||||
|  |         states.remove(&hook); | ||||||
|  |  | ||||||
|  |         // test case 4: with envs | ||||||
|  |         let create_shell = |shell_path: &str, data_path: &str| -> Result<()> { | ||||||
|  |             let shell = format!( | ||||||
|  |                 r#"#!/bin/sh | ||||||
|  | echo -n "K1=${{K1}}" > {} | ||||||
|  | "#, | ||||||
|  |                 data_path | ||||||
|  |             ); | ||||||
|  |             let mut output = File::create(shell_path)?; | ||||||
|  |             output.write_all(shell.as_bytes())?; | ||||||
|  |  | ||||||
|  |             // set to executable | ||||||
|  |             let permissions = Permissions::from_mode(0o755); | ||||||
|  |             set_permissions(shell_path, permissions)?; | ||||||
|  |  | ||||||
|  |             Ok(()) | ||||||
|  |         }; | ||||||
|  |         let shell_path = format!("{}/test.sh", tmpdir.path().to_string_lossy()); | ||||||
|  |         let ret = create_shell(&shell_path, file_str.as_ref()); | ||||||
|  |         assert!(ret.is_ok()); | ||||||
|  |         let hook = oci::Hook { | ||||||
|  |             path: shell_path, | ||||||
|  |             args: vec![], | ||||||
|  |             env: vec!["K1=V1".to_string()], | ||||||
|  |             timeout: Some(5), | ||||||
|  |         }; | ||||||
|  |         let ret = states.execute_hook(&hook, None); | ||||||
|  |         assert!(ret.is_ok()); | ||||||
|  |         assert!(!states.should_retry()); | ||||||
|  |         let contents = fs::read_to_string(file); | ||||||
|  |         match contents { | ||||||
|  |             Err(e) => panic!("got error {}", e), | ||||||
|  |             Ok(s) => assert_eq!(s, "K1=V1"), | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // test case 5: timeout in 5s with state | ||||||
|  |         let hook = oci::Hook { | ||||||
|  |             path: "/bin/sleep".to_string(), | ||||||
|  |             args: vec!["sleep".to_string(), "3600".to_string()], | ||||||
|  |             env: vec![], | ||||||
|  |             timeout: Some(6), // timeout is set to 5 seconds | ||||||
|  |         }; | ||||||
|  |         let state = oci::State { | ||||||
|  |             version: "".to_string(), | ||||||
|  |             id: "".to_string(), | ||||||
|  |             status: oci::ContainerState::Creating, | ||||||
|  |             pid: 10, | ||||||
|  |             bundle: "nouse".to_string(), | ||||||
|  |             annotations: Default::default(), | ||||||
|  |         }; | ||||||
|  |         let start = Instant::now(); | ||||||
|  |         let ret = states.execute_hook(&hook, Some(state)).unwrap_err(); | ||||||
|  |         let duration = start.elapsed(); | ||||||
|  |         let used = duration.as_secs(); | ||||||
|  |         assert!((6..8u64).contains(&used)); | ||||||
|  |         assert_eq!(ret.kind(), io::ErrorKind::TimedOut); | ||||||
|  |         assert!(states.should_retry()); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -9,6 +9,7 @@ extern crate slog; | |||||||
| pub mod cgroup; | pub mod cgroup; | ||||||
| pub mod device; | pub mod device; | ||||||
| pub mod fs; | pub mod fs; | ||||||
|  | pub mod hooks; | ||||||
| pub mod k8s; | pub mod k8s; | ||||||
| pub mod mount; | pub mod mount; | ||||||
| pub mod numa; | pub mod numa; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user