mirror of
https://github.com/containers/youki
synced 2024-09-27 22:49:57 +02:00
refactor
This commit is contained in:
parent
23b6eb6658
commit
13a09ad602
164
src/hook.rs
Normal file
164
src/hook.rs
Normal file
@ -0,0 +1,164 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use nix::{sys::signal, unistd::Pid};
|
||||
use oci_spec::Hook;
|
||||
use std::{collections::HashMap, fmt, process, thread, time};
|
||||
|
||||
use crate::{container::Container, utils};
|
||||
// A special error used to signal a timeout. We want to differenciate between a
|
||||
// timeout vs. other error.
|
||||
#[derive(Debug)]
|
||||
pub struct HookTimeoutError;
|
||||
impl std::error::Error for HookTimeoutError {}
|
||||
impl fmt::Display for HookTimeoutError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
"hook command timeout".fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_hooks(hooks: Option<Vec<Hook>>, container: Option<Container>) -> Result<()> {
|
||||
if let Some(hooks) = hooks {
|
||||
for hook in hooks {
|
||||
let envs: HashMap<String, String> = if let Some(env) = hook.env {
|
||||
utils::parse_env(env)
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
let mut hook_command = process::Command::new(hook.path)
|
||||
.args(hook.args.unwrap_or_default())
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.stdin(if container.is_some() {
|
||||
process::Stdio::piped()
|
||||
} else {
|
||||
process::Stdio::null()
|
||||
})
|
||||
.stdout(process::Stdio::null())
|
||||
.stderr(process::Stdio::null())
|
||||
.spawn()
|
||||
.with_context(|| "Failed to execute hook")?;
|
||||
let hook_command_pid = Pid::from_raw(hook_command.id() as i32);
|
||||
// Based on the OCI spec, we need to pipe the container state into
|
||||
// the hook command through stdin.
|
||||
if hook_command.stdin.is_some() {
|
||||
let stdin = hook_command.stdin.take().unwrap();
|
||||
if let Some(container) = &container {
|
||||
serde_json::to_writer(stdin, &container.state)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(timeout_sec) = hook.timeout {
|
||||
// Rust does not make it easy to handle executing a command and
|
||||
// timeout. Here we decided to wait for the command in a
|
||||
// different thread, so the main thread is not blocked. We use a
|
||||
// channel shared between main thread and the wait thread, since
|
||||
// the channel has timeout functions out of the box. Rust won't
|
||||
// let us copy the Command structure, so we can't share it
|
||||
// between the wait thread and main thread. Therefore, we will
|
||||
// use pid to identify the process and send a kill signal. This
|
||||
// is what the Command.kill() does under the hood anyway. When
|
||||
// timeout, we have to kill the process and clean up properly.
|
||||
let (s, r) = crossbeam_channel::unbounded();
|
||||
thread::spawn(move || {
|
||||
let res = hook_command.wait();
|
||||
let _ = s.send(res);
|
||||
});
|
||||
match r.recv_timeout(time::Duration::from_secs(timeout_sec as u64)) {
|
||||
Ok(res) => {
|
||||
match res {
|
||||
Ok(exit_status) => {
|
||||
if !exit_status.success() {
|
||||
bail!("Failed to execute hook command. Non-zero return code. {:?}", exit_status);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
bail!("Failed to execute hook command: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(crossbeam_channel::RecvTimeoutError::Timeout) => {
|
||||
// Kill the process. There is no need to further clean
|
||||
// up because we will be error out.
|
||||
let _ = signal::kill(hook_command_pid, signal::Signal::SIGKILL);
|
||||
return Err(HookTimeoutError.into());
|
||||
}
|
||||
Err(_) => {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hook_command.wait()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use anyhow::{bail, Result};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_run_hook() -> Result<()> {
|
||||
run_hooks(None, None)?;
|
||||
|
||||
{
|
||||
let default_container: Container = Default::default();
|
||||
let hook = Hook {
|
||||
path: PathBuf::from("/bin/true"),
|
||||
args: None,
|
||||
env: None,
|
||||
timeout: None,
|
||||
};
|
||||
let hooks = Some(vec![hook]);
|
||||
run_hooks(hooks, Some(default_container))?;
|
||||
}
|
||||
|
||||
{
|
||||
// Use `printenv` to make sure the environment is set correctly.
|
||||
let default_container: Container = Default::default();
|
||||
let hook = Hook {
|
||||
path: PathBuf::from("/bin/printenv"),
|
||||
args: Some(vec!["key".to_string()]),
|
||||
env: Some(vec!["key=value".to_string()]),
|
||||
timeout: None,
|
||||
};
|
||||
let hooks = Some(vec![hook]);
|
||||
run_hooks(hooks, Some(default_container))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
// This will test executing hook with a timeout. Since the timeout is set in
|
||||
// secs, minimally, the test will run for 1 second to trigger the timeout.
|
||||
// Therefore, we leave this test in the normal execution.
|
||||
fn test_run_hook_timeout() -> Result<()> {
|
||||
// We use `/bin/cat` here to simulate a hook command that hangs.
|
||||
let hook = Hook {
|
||||
path: PathBuf::from("tail"),
|
||||
args: Some(vec![String::from("-f"), String::from("/dev/null")]),
|
||||
env: None,
|
||||
timeout: Some(1),
|
||||
};
|
||||
let hooks = Some(vec![hook]);
|
||||
match run_hooks(hooks, None) {
|
||||
Ok(_) => {
|
||||
bail!("The test expects the hook to error out with timeout. Should not execute cleanly");
|
||||
}
|
||||
Err(err) => {
|
||||
// We want to make sure the error returned is indeed timeout
|
||||
// error. All other errors are considered failure.
|
||||
if !err.is::<HookTimeoutError>() {
|
||||
bail!("Failed to execute hook: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ pub mod capabilities;
|
||||
pub mod commands;
|
||||
pub mod container;
|
||||
pub mod dbus;
|
||||
pub mod hook;
|
||||
pub mod logger;
|
||||
pub mod namespaces;
|
||||
pub mod notify_socket;
|
||||
|
@ -4,10 +4,9 @@ use nix::mount::MsFlags;
|
||||
use crossbeam_channel::RecvTimeoutError;
|
||||
use nix::{
|
||||
fcntl, sched,
|
||||
sys::{signal, statfs},
|
||||
unistd::{Gid, Pid, Uid},
|
||||
sys::statfs,
|
||||
unistd::{Gid, Uid},
|
||||
};
|
||||
use oci_spec::Hook;
|
||||
use oci_spec::Spec;
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
@ -24,6 +23,7 @@ use std::{
|
||||
use crate::{
|
||||
capabilities,
|
||||
container::Container,
|
||||
hook,
|
||||
namespaces::Namespaces,
|
||||
notify_socket::NotifyListener,
|
||||
process::child,
|
||||
@ -32,111 +32,6 @@ use crate::{
|
||||
tty, utils,
|
||||
};
|
||||
|
||||
// A special error used to signal a timeout. We want to differenciate between a
|
||||
// timeout vs. other error.
|
||||
#[derive(Debug)]
|
||||
struct HookTimeoutError;
|
||||
impl std::error::Error for HookTimeoutError {}
|
||||
impl fmt::Display for HookTimeoutError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
"hook command timeout".fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_env(envs: Vec<String>) -> HashMap<String, String> {
|
||||
envs.iter()
|
||||
.filter_map(|e| {
|
||||
let mut split = e.split('=');
|
||||
|
||||
if let Some(key) = split.next() {
|
||||
let value: String = split.collect::<Vec<&str>>().join("=");
|
||||
Some((String::from(key), value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn run_hooks(hooks: Option<Vec<Hook>>, container: Option<Container>) -> Result<()> {
|
||||
if let Some(hooks) = hooks {
|
||||
for hook in hooks {
|
||||
let envs: HashMap<String, String> = if let Some(env) = hook.env {
|
||||
parse_env(env)
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
let mut hook_command = process::Command::new(hook.path)
|
||||
.args(hook.args.unwrap_or_default())
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.stdin(if container.is_some() {
|
||||
process::Stdio::piped()
|
||||
} else {
|
||||
process::Stdio::null()
|
||||
})
|
||||
.stdout(process::Stdio::null())
|
||||
.stderr(process::Stdio::null())
|
||||
.spawn()
|
||||
.with_context(|| "Failed to execute hook")?;
|
||||
let hook_command_pid = Pid::from_raw(hook_command.id() as i32);
|
||||
// Based on the OCI spec, we need to pipe the container state into
|
||||
// the hook command through stdin.
|
||||
if hook_command.stdin.is_some() {
|
||||
let stdin = hook_command.stdin.take().unwrap();
|
||||
if let Some(container) = &container {
|
||||
serde_json::to_writer(stdin, &container.state)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(timeout_sec) = hook.timeout {
|
||||
// Rust does not make it easy to handle executing a command and
|
||||
// timeout. Here we decided to wait for the command in a
|
||||
// different thread, so the main thread is not blocked. We use a
|
||||
// channel shared between main thread and the wait thread, since
|
||||
// the channel has timeout functions out of the box. Rust won't
|
||||
// let us copy the Command structure, so we can't share it
|
||||
// between the wait thread and main thread. Therefore, we will
|
||||
// use pid to identify the process and send a kill signal. This
|
||||
// is what the Command.kill() does under the hood anyway. When
|
||||
// timeout, we have to kill the process and clean up properly.
|
||||
let (s, r) = crossbeam_channel::unbounded();
|
||||
thread::spawn(move || {
|
||||
let res = hook_command.wait();
|
||||
let _ = s.send(res);
|
||||
});
|
||||
match r.recv_timeout(time::Duration::from_secs(timeout_sec as u64)) {
|
||||
Ok(res) => {
|
||||
match res {
|
||||
Ok(exit_status) => {
|
||||
if !exit_status.success() {
|
||||
bail!("Failed to execute hook command. Non-zero return code. {:?}", exit_status);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
bail!("Failed to execute hook command: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
// Kill the process. There is no need to further clean
|
||||
// up because we will be error out.
|
||||
let _ = signal::kill(hook_command_pid, signal::Signal::SIGKILL);
|
||||
return Err(HookTimeoutError.into());
|
||||
}
|
||||
Err(_) => {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hook_command.wait()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Make sure a given path is on procfs. This is to avoid the security risk that
|
||||
// /proc path is mounted over. Ref: CVE-2019-16884
|
||||
fn ensure_procfs(path: &Path) -> Result<()> {
|
||||
@ -298,10 +193,10 @@ pub fn container_init(args: ContainerInitArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
if args.init {
|
||||
// create_runtime hook needs to be called after the namespace setup, but
|
||||
// before pivot_root is called.
|
||||
// create_container hook needs to be called after the namespace setup, but
|
||||
// before pivot_root is called. This runs in the container namespaces.
|
||||
if let Some(hooks) = hooks {
|
||||
run_hooks(hooks.create_runtime, args.container)?
|
||||
hook::run_hooks(hooks.create_container, args.container)?
|
||||
}
|
||||
rootfs::prepare_rootfs(spec, rootfs, bind_service)
|
||||
.with_context(|| "Failed to prepare rootfs")?;
|
||||
@ -378,6 +273,11 @@ pub fn container_init(args: ContainerInitArgs) -> Result<()> {
|
||||
// listing on the notify socket for container start command
|
||||
notify_socket.wait_for_container_start()?;
|
||||
|
||||
// create_container hook needs to be called after the namespace setup, but
|
||||
// before pivot_root is called. This runs in the container namespaces.
|
||||
// if let Some(hooks) = hooks {
|
||||
// hook::run_hooks(hooks.start_container, args.container)?
|
||||
// }
|
||||
if let Some(args) = proc.args.as_ref() {
|
||||
utils::do_exec(&args[0], args, &envs)?;
|
||||
} else {
|
||||
@ -476,82 +376,4 @@ mod tests {
|
||||
unistd::close(fd)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_env() -> Result<()> {
|
||||
let key = "key".to_string();
|
||||
let value = "value".to_string();
|
||||
let env_input = vec![format!("{}={}", key, value)];
|
||||
let env_output = parse_env(env_input);
|
||||
assert_eq!(
|
||||
env_output.len(),
|
||||
1,
|
||||
"There should be exactly one entry inside"
|
||||
);
|
||||
assert_eq!(env_output.get_key_value(&key), Some((&key, &value)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_hook() -> Result<()> {
|
||||
run_hooks(None, None)?;
|
||||
|
||||
{
|
||||
let default_container: Container = Default::default();
|
||||
let hook = Hook {
|
||||
path: PathBuf::from("/bin/true"),
|
||||
args: None,
|
||||
env: None,
|
||||
timeout: None,
|
||||
};
|
||||
let hooks = Some(vec![hook]);
|
||||
run_hooks(hooks, Some(default_container))?;
|
||||
}
|
||||
|
||||
{
|
||||
// Use `printenv` to make sure the environment is set correctly.
|
||||
let default_container: Container = Default::default();
|
||||
let hook = Hook {
|
||||
path: PathBuf::from("/bin/printenv"),
|
||||
args: Some(vec!["key".to_string()]),
|
||||
env: Some(vec!["key=value".to_string()]),
|
||||
timeout: None,
|
||||
};
|
||||
let hooks = Some(vec![hook]);
|
||||
run_hooks(hooks, Some(default_container))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
// This will test executing hook with a timeout. Since the timeout is set in
|
||||
// secs, minimally, the test will run for 1 second to trigger the timeout.
|
||||
// Therefore, we leave this test in the normal execution.
|
||||
fn test_run_hook_timeout() -> Result<()> {
|
||||
// We use `/bin/cat` here to simulate a hook command that hangs.
|
||||
let hook = Hook {
|
||||
path: PathBuf::from("tail"),
|
||||
args: Some(vec![String::from("-f"), String::from("/dev/null")]),
|
||||
env: None,
|
||||
timeout: Some(1),
|
||||
};
|
||||
let hooks = Some(vec![hook]);
|
||||
match run_hooks(hooks, None) {
|
||||
Ok(_) => {
|
||||
bail!("The test expects the hook to error out with timeout. Should not execute cleanly");
|
||||
}
|
||||
Err(err) => {
|
||||
// We want to make sure the error returned is indeed timeout
|
||||
// error. All other errors are considered failure.
|
||||
if !err.is::<HookTimeoutError>() {
|
||||
bail!("Failed to execute hook: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
31
src/utils.rs
31
src/utils.rs
@ -1,5 +1,6 @@
|
||||
//! Utility functionality
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ffi::CString;
|
||||
use std::fs::{self, DirBuilder, File};
|
||||
@ -40,6 +41,21 @@ impl PathBufExt for PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_env(envs: Vec<String>) -> HashMap<String, String> {
|
||||
envs.iter()
|
||||
.filter_map(|e| {
|
||||
let mut split = e.split('=');
|
||||
|
||||
if let Some(key) = split.next() {
|
||||
let value: String = split.collect::<Vec<&str>>().join("=");
|
||||
Some((String::from(key), value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn do_exec(path: impl AsRef<Path>, args: &[String], envs: &[String]) -> Result<()> {
|
||||
let p = CString::new(path.as_ref().to_string_lossy().to_string())?;
|
||||
let a: Vec<CString> = args
|
||||
@ -233,4 +249,19 @@ mod tests {
|
||||
PathBuf::from("/youki")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_env() -> Result<()> {
|
||||
let key = "key".to_string();
|
||||
let value = "value".to_string();
|
||||
let env_input = vec![format!("{}={}", key, value)];
|
||||
let env_output = parse_env(env_input);
|
||||
assert_eq!(
|
||||
env_output.len(),
|
||||
1,
|
||||
"There should be exactly one entry inside"
|
||||
);
|
||||
assert_eq!(env_output.get_key_value(&key), Some((&key, &value)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user