mirror of
https://github.com/containers/youki
synced 2024-05-07 08:06:18 +02:00
273 lines
9.2 KiB
Rust
273 lines
9.2 KiB
Rust
use super::{generate_uuid, prepare_bundle, set_config};
|
|
///! Contains utility functions for testing
|
|
///! Similar to https://github.com/opencontainers/runtime-tools/blob/master/validation/util/test.go
|
|
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};
|
|
use uuid::Uuid;
|
|
|
|
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<i32>,
|
|
pub bundle: PathBuf,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub annotations: Option<HashMap<String, String>>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub created: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub creator: Option<u32>,
|
|
pub use_systemd: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ContainerData {
|
|
pub id: String,
|
|
pub state: Option<State>,
|
|
pub state_err: String,
|
|
pub create_result: std::io::Result<ExitStatus>,
|
|
}
|
|
|
|
/// Starts the runtime with given directory as root directory
|
|
pub fn create_container<P: AsRef<Path>>(id: &Uuid, dir: P) -> Result<Child> {
|
|
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())
|
|
// set log level to error only, otherwise
|
|
// we get warnings in stderr
|
|
.env("YOUKI_LOG_LEVEL", "error")
|
|
.arg("--root")
|
|
.arg(dir.as_ref().join("runtime"))
|
|
.arg("create")
|
|
.arg(id.to_string())
|
|
.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<P: AsRef<Path>>(id: &Uuid, dir: P) -> Result<Child> {
|
|
let res = Command::new(get_runtime_path())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.arg("--root")
|
|
.arg(dir.as_ref().join("runtime"))
|
|
.arg("kill")
|
|
.arg(id.to_string())
|
|
.arg("9")
|
|
.spawn()
|
|
.context("could not kill container")?;
|
|
Ok(res)
|
|
}
|
|
|
|
pub fn delete_container<P: AsRef<Path>>(id: &Uuid, dir: P) -> Result<Child> {
|
|
let res = Command::new(get_runtime_path())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.arg("--root")
|
|
.arg(dir.as_ref().join("runtime"))
|
|
.arg("delete")
|
|
.arg(id.to_string())
|
|
.spawn()
|
|
.context("could not delete container")?;
|
|
Ok(res)
|
|
}
|
|
|
|
pub fn get_state<P: AsRef<Path>>(id: &Uuid, dir: P) -> Result<(String, String)> {
|
|
sleep(SLEEP_TIME);
|
|
let output = Command::new(get_runtime_path())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.arg("--root")
|
|
.arg(dir.as_ref().join("runtime"))
|
|
.arg("state")
|
|
.arg(id.to_string())
|
|
.spawn()?
|
|
.wait_with_output()?;
|
|
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<P: AsRef<Path>>(id: &Uuid, dir: P) -> Result<Child> {
|
|
let res = Command::new(get_runtime_path())
|
|
.stderr(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.arg("--root")
|
|
.arg(dir.as_ref().join("runtime"))
|
|
.arg("start")
|
|
.arg(id.to_string())
|
|
.spawn()
|
|
.context("could not start container")?;
|
|
Ok(res)
|
|
}
|
|
|
|
pub fn test_outside_container(
|
|
spec: Spec,
|
|
execute_test: &dyn Fn(ContainerData) -> TestResult,
|
|
) -> TestResult {
|
|
let id = generate_uuid();
|
|
let bundle = prepare_bundle(&id).unwrap();
|
|
set_config(&bundle, &spec).unwrap();
|
|
let create_result = create_container(&id, &bundle).unwrap().wait();
|
|
let (out, err) = get_state(&id, &bundle).unwrap();
|
|
let state: Option<State> = 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, &bundle).unwrap().wait().unwrap();
|
|
delete_container(&id, &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 bundle = prepare_bundle(&id).unwrap();
|
|
|
|
// This will do the required setup for the test
|
|
test_result!(setup_for_test(
|
|
&bundle.as_ref().join("bundle").join("rootfs")
|
|
));
|
|
// std::thread::sleep_ms(50000);
|
|
|
|
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, &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, &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 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, &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, &bundle).unwrap().wait().unwrap();
|
|
delete_container(&id, &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)),
|
|
}
|
|
}
|