//! Contains utility functions for testing //! Similar to https://github.com/opencontainers/runtime-tools/blob/master/validation/util/test.go use super::{generate_uuid, prepare_bundle, set_config}; use super::{get_runtime_path, get_runtimetest_path}; use anyhow::{anyhow, bail, Context, Result}; use oci_spec::runtime::Spec; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::process::{Child, Command, ExitStatus, Stdio}; use std::thread::sleep; use std::time::Duration; use test_framework::{test_result, TestResult}; const SLEEP_TIME: Duration = Duration::from_millis(150); pub const CGROUP_ROOT: &str = "/sys/fs/cgroup"; #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct State { pub oci_version: String, pub id: String, pub status: String, #[serde(skip_serializing_if = "Option::is_none")] pub pid: Option, pub bundle: PathBuf, #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub created: Option, #[serde(skip_serializing_if = "Option::is_none")] pub creator: Option, pub use_systemd: Option, } #[derive(Debug)] pub struct ContainerData { pub id: String, pub state: Option, pub state_err: String, pub create_result: std::io::Result, } /// Starts the runtime with given directory as root directory pub fn create_container>(id: &str, dir: P) -> Result { let res = Command::new(get_runtime_path()) // set stdio so that we can get o/p of runtimetest // in test_inside_container function .stdout(Stdio::piped()) .stderr(Stdio::piped()) .arg("--root") .arg(dir.as_ref().join("runtime")) .arg("create") .arg(id) .arg("--bundle") .arg(dir.as_ref().join("bundle")) .spawn() .context("could not create container")?; Ok(res) } /// Sends a kill command to the given container process pub fn kill_container>(id: &str, dir: P) -> Result { let res = runtime_command(dir) .arg("kill") .arg(id) .arg("9") .spawn() .context("could not kill container")?; Ok(res) } pub fn delete_container>(id: &str, dir: P) -> Result { let res = runtime_command(dir) .arg("delete") .arg(id) .spawn() .context("could not delete container")?; Ok(res) } pub fn get_state>(id: &str, dir: P) -> Result<(String, String)> { sleep(SLEEP_TIME); let output = runtime_command(dir) .arg("state") .arg(id) .spawn() .context("could not get container state")? .wait_with_output() .context("failed while waiting for state command")?; let stderr = String::from_utf8(output.stderr).context("failed to parse std error stream")?; let stdout = String::from_utf8(output.stdout).context("failed to parse std output stream")?; Ok((stdout, stderr)) } pub fn start_container>(id: &str, dir: P) -> Result { let res = runtime_command(dir) .arg("start") .arg(id) .spawn() .context("could not start container")?; Ok(res) } fn runtime_command>(dir: P) -> Command { let mut command = Command::new(get_runtime_path()); command .stdout(Stdio::piped()) .stderr(Stdio::piped()) .arg("--root") .arg(dir.as_ref().join("runtime")); command } pub fn test_outside_container( spec: Spec, execute_test: &dyn Fn(ContainerData) -> TestResult, ) -> TestResult { let id = generate_uuid(); let id_str = id.to_string(); let bundle = prepare_bundle().unwrap(); set_config(&bundle, &spec).unwrap(); let create_result = create_container(&id_str, &bundle).unwrap().wait(); let (out, err) = get_state(&id_str, &bundle).unwrap(); let state: Option = match serde_json::from_str(&out) { Ok(v) => Some(v), Err(_) => None, }; let data = ContainerData { id: id.to_string(), state, state_err: err, create_result, }; let test_result = execute_test(data); kill_container(&id_str, &bundle).unwrap().wait().unwrap(); delete_container(&id_str, &bundle).unwrap().wait().unwrap(); test_result } // mostly needs a name that better expresses what this actually does pub fn test_inside_container( spec: Spec, setup_for_test: &dyn Fn(&Path) -> Result<()>, ) -> TestResult { let id = generate_uuid(); let id_str = id.to_string(); let bundle = prepare_bundle().unwrap(); // This will do the required setup for the test test_result!(setup_for_test( &bundle.as_ref().join("bundle").join("rootfs") )); set_config(&bundle, &spec).unwrap(); // as we have to run runtimetest inside the container, and is expects // the config.json to be at path /config.json we save it there let path = bundle .as_ref() .join("bundle") .join("rootfs") .join("config.json"); spec.save(path).unwrap(); let runtimetest_path = get_runtimetest_path(); // The config will directly use runtime as the command to be run, so we have to // save the runtimetest binary at its /bin std::fs::copy( runtimetest_path, bundle .as_ref() .join("bundle") .join("rootfs") .join("bin") .join("runtimetest"), ) .unwrap(); let create_process = create_container(&id_str, &bundle).unwrap(); // here we do not wait for the process by calling wait() as in the test_outside_container // function because we need the output of the runtimetest. If we call wait, it will return // and we won't have an easy way of getting the stdio of the runtimetest. // Thus to make sure the container is created, we just wait for sometime, and // assume that the create command was successful. If it wasn't we can catch that error // in the start_container, as we can not start a non-created container anyways std::thread::sleep(std::time::Duration::from_millis(1000)); match start_container(&id_str, &bundle) .unwrap() .wait_with_output() { Ok(c) => c, Err(e) => return TestResult::Failed(anyhow!("container start failed : {:?}", e)), }; let create_output = create_process .wait_with_output() .context("getting output after starting the container failed") .unwrap(); let stdout = String::from_utf8_lossy(&create_output.stdout); if !stdout.is_empty() { println!( "{:?}", anyhow!("container stdout was not empty, found : {}", stdout) ) } let stderr = String::from_utf8_lossy(&create_output.stderr); if !stderr.is_empty() { return TestResult::Failed(anyhow!( "container stderr was not empty, found : {}", stderr )); } let (out, err) = get_state(&id_str, &bundle).unwrap(); if !err.is_empty() { return TestResult::Failed(anyhow!( "error in getting state after starting the container : {}", err )); } let state: State = match serde_json::from_str(&out) { Ok(v) => v, Err(e) => return TestResult::Failed(anyhow!("error in parsing state of container after start in test_inside_container : stdout : {}, parse error : {}",out,e)), }; if state.status != "stopped" { return TestResult::Failed(anyhow!("error : unexpected container status in test_inside_runtime : expected stopped, got {}, container state : {:?}",state.status,state)); } kill_container(&id_str, &bundle).unwrap().wait().unwrap(); delete_container(&id_str, &bundle).unwrap().wait().unwrap(); TestResult::Passed } pub fn check_container_created(data: &ContainerData) -> Result<()> { match &data.create_result { Ok(exit_status) => { if !exit_status.success() { bail!( "container creation was not successful. Exit code was {:?}", exit_status.code() ) } if !data.state_err.is_empty() { bail!( "container state could not be retrieved successfully. Error was {}", data.state_err ); } if data.state.is_none() { bail!("container state could not be retrieved"); } let container_state = data.state.as_ref().unwrap(); if container_state.id != data.id { bail!( "container state contains container id {}, but expected was {}", container_state.id, data.id ); } if container_state.status != "created" { bail!( "expected container to be in state created, but was in state {}", container_state.status ); } Ok(()) } Err(e) => Err(anyhow!("{}", e)), } }