runk: Align oci spec with oci-spec-rs

Utilized oci-spec-rs to align OCI Spec structures
and data representations in runk with the OCI Spec.

Fixes #9766

Signed-off-by: Alex Lyn <alex.lyn@antgroup.com>
This commit is contained in:
Alex Lyn 2024-07-16 16:16:20 +08:00
parent b3eab5ffea
commit bf813f85f2
11 changed files with 1207 additions and 780 deletions

1650
src/tools/runk/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,8 @@ edition = "2018"
[dependencies]
libcontainer = { path = "./libcontainer" }
rustjail = { path = "../../agent/rustjail", features = ["standard-oci-runtime"] }
oci = { path = "../../libs/oci" }
runtime-spec = { path = "../../libs/runtime-spec" }
oci-spec = { version = "0.6.8", features = ["runtime"] }
logging = { path = "../../libs/logging" }
liboci-cli = "0.0.4"
clap = { version = "3.0.6", features = ["derive", "cargo"] }

View File

@ -8,7 +8,8 @@ edition = "2018"
[dependencies]
rustjail = { path = "../../../agent/rustjail", features = ["standard-oci-runtime"] }
oci = { path = "../../../libs/oci" }
runtime-spec = { path = "../../../libs/runtime-spec" }
oci-spec = { version = "0.6.8", features = ["runtime"] }
kata-sys-util = { path = "../../../libs/kata-sys-util" }
logging = { path = "../../../libs/logging" }
derive_builder = "0.10.2"
@ -26,3 +27,4 @@ procfs = "0.14.0"
[dev-dependencies]
tempfile = "3.3.0"
test-utils = { path = "../../../libs/test-utils" }
protocols = { path ="../../../libs/protocols" }

View File

@ -8,7 +8,9 @@ use crate::status::Status;
use crate::utils::validate_spec;
use anyhow::{anyhow, Result};
use derive_builder::Builder;
use oci::{ContainerState, Process as OCIProcess, Spec};
use oci::{Process as OCIProcess, Spec};
use oci_spec::runtime as oci;
use runtime_spec::ContainerState;
use rustjail::container::update_namespaces;
use slog::{debug, Logger};
use std::fs::File;
@ -105,8 +107,8 @@ impl ActivatedContainer {
// If with --process, load process from file.
// Otherwise, update process with args and other options.
if let Some(process_path) = self.process.as_ref() {
spec.process = Some(Self::get_process(process_path)?);
} else if let Some(process) = spec.process.as_mut() {
spec.set_process(Some(Self::get_process(process_path)?));
} else if let Some(process) = spec.process_mut().as_mut() {
self.update_process(process)?;
} else {
return Err(anyhow!("process is empty in spec"));
@ -118,15 +120,15 @@ impl ActivatedContainer {
/// Update process with args and other options.
fn update_process(&self, process: &mut OCIProcess) -> Result<()> {
process.args = self.args.clone();
process.no_new_privileges = self.no_new_privs;
process.terminal = self.tty;
process.set_args(Some(self.args.clone()));
process.set_no_new_privileges(Some(self.no_new_privs));
process.set_terminal(Some(self.tty));
if let Some(cwd) = self.cwd.as_ref() {
process.cwd = cwd.as_path().display().to_string();
process.set_cwd(cwd.as_path().to_path_buf());
}
if let Some(process_env) = process.env_mut() {
process_env.extend(self.env.iter().map(|kv| format!("{}={}", kv.0, kv.1)));
}
process
.env
.extend(self.env.iter().map(|kv| format!("{}={}", kv.0, kv.1)));
Ok(())
}
@ -143,7 +145,7 @@ mod tests {
use crate::status::Status;
use crate::utils::test_utils::*;
use nix::unistd::getpid;
use oci::{Linux, LinuxNamespace, User};
use oci_spec::runtime::{LinuxBuilder, LinuxNamespaceBuilder, ProcessBuilder, User};
use rustjail::container::TYPETONAME;
use scopeguard::defer;
use slog::o;
@ -193,11 +195,10 @@ mod tests {
let pid = getpid().as_raw();
let mut spec = create_dummy_spec();
spec.root.as_mut().unwrap().path = bundle_dir
.path()
.join(TEST_ROOTFS_PATH)
.to_string_lossy()
.to_string();
spec.root_mut()
.as_mut()
.unwrap()
.set_path(bundle_dir.path().join(TEST_ROOTFS_PATH));
let status = create_custom_dummy_status(&id, pid, root.path(), &spec);
status.save().unwrap();
@ -223,36 +224,39 @@ mod tests {
.build()
.unwrap();
let linux = Linux {
namespaces: TYPETONAME
.iter()
.filter(|&(_, &name)| name != "user")
.map(|ns| LinuxNamespace {
r#type: ns.0.to_string(),
path: format!("/proc/{}/ns/{}", pid, ns.1),
})
.collect(),
..Default::default()
};
spec.linux = Some(linux);
spec.process = Some(OCIProcess {
terminal: result.tty,
console_size: None,
user: User::default(),
args: result.args.clone(),
cwd: result.cwd.clone().unwrap().to_string_lossy().to_string(),
env: vec![
let linux = LinuxBuilder::default()
.namespaces(
TYPETONAME
.iter()
.filter(|&(_, &name)| name != "user")
.map(|ns| {
LinuxNamespaceBuilder::default()
.typ(ns.0.clone())
.path(PathBuf::from(&format!("/proc/{}/ns/{}", pid, ns.1)))
.build()
.unwrap()
})
.collect::<Vec<_>>(),
)
.build()
.unwrap();
spec.set_linux(Some(linux));
let process = ProcessBuilder::default()
.terminal(result.tty)
.user(User::default())
.args(result.args.clone())
.cwd(result.cwd.clone().unwrap().to_string_lossy().to_string())
.env(vec![
"PATH=/bin:/usr/bin".to_string(),
"K1=V1".to_string(),
"K2=V2".to_string(),
],
capabilities: None,
rlimits: Vec::new(),
no_new_privileges: result.no_new_privs,
apparmor_profile: "".to_string(),
oom_score_adj: None,
selinux_label: "".to_string(),
});
])
.no_new_privileges(result.no_new_privs)
.build()
.unwrap();
spec.set_process(Some(process));
let launcher = result.clone().create_launcher(&logger).unwrap();
assert!(!launcher.init);
assert_eq!(launcher.runner.config.spec.unwrap(), spec);
@ -269,11 +273,11 @@ mod tests {
skip_if_not_root!();
let bundle_dir = tempdir().unwrap();
let process_file = bundle_dir.path().join(TEST_PROCESS_FILE_NAME);
let process_template = OCIProcess {
args: vec!["sleep".to_string(), "10".to_string()],
cwd: "/".to_string(),
..Default::default()
};
let mut process_template = OCIProcess::default();
process_template.set_args(Some(vec!["sleep".to_string(), "10".to_string()]));
process_template.set_cwd(PathBuf::from("/"));
let file = File::create(process_file.clone()).unwrap();
serde_json::to_writer(&file, &process_template).unwrap();
@ -284,11 +288,10 @@ mod tests {
let id = "test_activated_container_create_with_process".to_string();
let pid = getpid().as_raw();
let mut spec = create_dummy_spec();
spec.root.as_mut().unwrap().path = bundle_dir
.path()
.join(TEST_ROOTFS_PATH)
.to_string_lossy()
.to_string();
spec.root_mut()
.as_mut()
.unwrap()
.set_path(bundle_dir.path().join(TEST_ROOTFS_PATH));
create_activated_dirs(root.path(), &id, bundle_dir.path());
let status = create_custom_dummy_status(&id, pid, root.path(), &spec);
@ -300,7 +303,7 @@ mod tests {
let launcher = ActivatedContainerBuilder::default()
.id(id)
.root(root.into_path())
.console_socket(None)
.console_socket(Some(PathBuf::from(TEST_CONSOLE_SOCKET_PATH)))
.pid_file(None)
.tty(true)
.cwd(Some(PathBuf::from(TEST_BUNDLE_PATH)))
@ -319,7 +322,14 @@ mod tests {
assert!(!launcher.init);
assert_eq!(
launcher.runner.config.spec.unwrap().process.unwrap(),
launcher
.runner
.config
.spec
.unwrap()
.process()
.clone()
.unwrap(),
process_template
);
}

View File

@ -15,8 +15,8 @@ use nix::{
sys::signal::SIGKILL,
unistd::{chdir, unlink, Pid},
};
use oci::{ContainerState, State as OCIState};
use procfs;
use runtime_spec::{ContainerState, State as OCIState};
use rustjail::cgroups::fs::Manager as CgroupManager;
use rustjail::{
container::{BaseContainer, LinuxContainer, EXEC_FIFO_FILENAME},
@ -59,15 +59,18 @@ impl Container {
.as_ref()
.ok_or_else(|| anyhow!("spec config was not present"))?;
let linux = spec
.linux
.linux()
.as_ref()
.ok_or_else(|| anyhow!("linux config was not present"))?;
let cpath = if linux.cgroups_path.is_empty() {
let cpath = if linux.cgroups_path().is_none() {
id.to_string()
} else {
linux
.cgroups_path
.cgroups_path()
.clone()
.unwrap_or_default()
.display()
.to_string()
.trim_start_matches('/')
.to_string()
};
@ -142,13 +145,16 @@ impl Container {
.to_str()
.ok_or_else(|| anyhow!("invalid bundle path"))?
.to_string(),
annotations: spec.annotations.clone(),
annotations: spec.annotations().clone().unwrap_or_default(),
};
if let Some(hooks) = spec.hooks.as_ref() {
if let Some(hooks) = spec.hooks().as_ref() {
info!(&logger, "Poststop Hooks");
let mut poststop_hookstates = HookStates::new();
poststop_hookstates.execute_hooks(&hooks.poststop, Some(oci_state.clone()))?;
poststop_hookstates.execute_hooks(
&hooks.poststop().clone().unwrap_or_default(),
Some(oci_state.clone()),
)?;
}
match oci_state.status {
@ -281,12 +287,10 @@ impl ContainerLauncher {
/// Generate rustjail::Process from OCI::Process
fn get_process(&self, logger: &Logger) -> Result<Process> {
let spec = self.runner.config.spec.as_ref().unwrap();
if spec.process.is_some() {
if spec.process().is_some() {
Ok(Process::new(
logger,
spec.process
.as_ref()
.ok_or_else(|| anyhow!("process config was not present in the spec file"))?,
spec.process().as_ref().unwrap(),
// rustjail::LinuxContainer use the exec_id to identify processes in a container,
// so we can get the spawned process by ctr.get_process(exec_id) later.
// Since LinuxContainer is temporarily created to spawn one process in each runk invocation,

View File

@ -6,7 +6,7 @@
use crate::container::{load_linux_container, Container, ContainerLauncher};
use anyhow::{anyhow, Result};
use derive_builder::Builder;
use oci::ContainerState;
use runtime_spec::ContainerState;
use slog::{debug, Logger};
use std::path::PathBuf;
@ -113,11 +113,10 @@ mod tests {
let pid = getpid().as_raw();
let mut spec = create_dummy_spec();
spec.root.as_mut().unwrap().path = bundle_dir
.path()
.join(TEST_ROOTFS_PATH)
.to_string_lossy()
.to_string();
spec.root_mut()
.as_mut()
.unwrap()
.set_path(bundle_dir.path().join(TEST_ROOTFS_PATH));
let status = create_custom_dummy_status(&id, pid, root.path(), &spec);
status.save().unwrap();

View File

@ -8,7 +8,7 @@ use crate::status::Status;
use crate::utils::{canonicalize_spec_root, validate_spec};
use anyhow::{anyhow, Result};
use derive_builder::Builder;
use oci::Spec;
use oci_spec::runtime::Spec;
use rustjail::specconv::CreateOpts;
use slog::{debug, Logger};
use std::path::PathBuf;
@ -91,11 +91,11 @@ mod tests {
use super::*;
use crate::container::CONFIG_FILE_NAME;
use crate::utils::test_utils::*;
use oci_spec::runtime::Process;
use slog::o;
use std::fs::{create_dir, File};
use std::path::Path;
use tempfile::tempdir;
use test_utils::skip_if_not_root;
#[test]
fn test_init_container_validate() {
@ -126,11 +126,10 @@ mod tests {
let file = File::create(config_file).unwrap();
serde_json::to_writer(&file, &spec).unwrap();
spec.root.as_mut().unwrap().path = bundle_dir
.path()
.join(TEST_ROOTFS_PATH)
.to_string_lossy()
.to_string();
spec.root_mut()
.as_mut()
.unwrap()
.set_path(bundle_dir.path().join(TEST_ROOTFS_PATH));
let test_data = TestContainerData {
// Since tests are executed concurrently, container_id must be unique in tests with cgroup.
// Or the cgroup directory may be removed by other tests in advance.
@ -179,11 +178,12 @@ mod tests {
let bundle_dir = tempdir().unwrap();
let config_file = bundle_dir.path().join(CONFIG_FILE_NAME);
let mut spec = oci::Spec {
process: Some(oci::Process::default()),
..Default::default()
};
spec.process.as_mut().unwrap().terminal = true;
let mut spec = Spec::default();
spec.set_process(Some(Process::default()));
spec.process_mut()
.as_mut()
.unwrap()
.set_terminal(Some(true));
let file = File::create(config_file).unwrap();
serde_json::to_writer(&file, &spec).unwrap();

View File

@ -14,8 +14,8 @@ use nix::{
sys::{signal::kill, stat::Mode},
unistd::Pid,
};
use oci::{ContainerState, State as OCIState};
use procfs::process::ProcState;
use runtime_spec::{ContainerState, State as OCIState};
use rustjail::{cgroups::fs::Manager as CgroupManager, specconv::CreateOpts};
use serde::{Deserialize, Serialize};
use std::{
@ -60,10 +60,10 @@ impl Status {
.clone()
.spec
.ok_or_else(|| anyhow!("spec config was not present"))?
.root
.root()
.as_ref()
.ok_or_else(|| anyhow!("root config was not present in the spec"))?
.path
.path()
.clone();
Ok(Self {
@ -72,7 +72,7 @@ impl Status {
pid: oci_state.pid,
root: root.to_path_buf(),
bundle: bundle.to_path_buf(),
rootfs,
rootfs: rootfs.display().to_string(),
process_start_time,
created,
cgroup_manager: cgroup_mg,
@ -187,7 +187,7 @@ mod tests {
use ::test_utils::skip_if_not_root;
use chrono::{DateTime, Utc};
use nix::unistd::getpid;
use oci::ContainerState;
use runtime_spec::ContainerState;
use rustjail::cgroups::fs::Manager as CgroupManager;
use scopeguard::defer;
use std::path::Path;

View File

@ -5,7 +5,7 @@
use anyhow::{anyhow, Result};
use nix::sys::stat::Mode;
use oci::{Process, Spec};
use oci_spec::runtime::{Process, Spec};
use std::{
fs::{DirBuilder, File},
io::{prelude::*, BufReader},
@ -37,28 +37,24 @@ pub fn create_dir_with_mode<P: AsRef<Path>>(path: P, mode: Mode, recursive: bool
/// If root in spec is a relative path, make it absolute.
pub fn canonicalize_spec_root(spec: &mut Spec, bundle_canon: &Path) -> Result<()> {
let spec_root = spec
.root
.root_mut()
.as_mut()
.ok_or_else(|| anyhow!("root config was not present in the spec file"))?;
let rootfs_path = Path::new(&spec_root.path);
let rootfs_path = &spec_root.path();
if !rootfs_path.is_absolute() {
spec_root.path = bundle_canon
.join(rootfs_path)
.canonicalize()?
.to_str()
.map(|s| s.to_string())
.ok_or_else(|| anyhow!("failed to convert a rootfs path into a canonical path"))?;
let bundle_canon_path = bundle_canon.join(rootfs_path).canonicalize()?;
spec_root.set_path(bundle_canon_path);
}
Ok(())
}
/// Check whether spec is valid. Now runk only support detach mode.
pub fn validate_spec(spec: &Spec, console_socket: &Option<PathBuf>) -> Result<()> {
validate_process_spec(&spec.process)?;
if let Some(process) = spec.process.as_ref() {
validate_process_spec(spec.process())?;
if let Some(process) = spec.process().as_ref() {
// runk always launches containers with detached mode, so users have to
// use a console socket with run or create operation when a terminal is used.
if process.terminal && console_socket.is_none() {
if process.terminal().is_some() && console_socket.is_none() {
return Err(anyhow!(
"cannot allocate a pseudo-TTY without setting a console socket"
));
@ -72,14 +68,14 @@ pub fn validate_process_spec(process: &Option<Process>) -> Result<()> {
let process = process
.as_ref()
.ok_or_else(|| anyhow!("process property must not be empty"))?;
if process.cwd.is_empty() {
if process.cwd().as_os_str().is_empty() {
return Err(anyhow!("cwd property must not be empty"));
}
let cwd = Path::new(process.cwd.as_str());
let cwd = process.cwd();
if !cwd.is_absolute() {
return Err(anyhow!("cwd must be an absolute path"));
}
if process.args.is_empty() {
if process.args().is_none() {
return Err(anyhow!("args must not be empty"));
}
Ok(())
@ -91,7 +87,9 @@ pub(crate) mod test_utils {
use crate::status::Status;
use chrono::DateTime;
use nix::unistd::getpid;
use oci::{ContainerState, LinuxNamespace, Process, Root, Spec, State as OCIState};
use oci::{LinuxBuilder, LinuxNamespaceBuilder, Process, Root, Spec};
use oci_spec::runtime as oci;
use runtime_spec::{ContainerState, State as OCIState};
use rustjail::{
cgroups::fs::Manager as CgroupManager, container::TYPETONAME, specconv::CreateOpts,
};
@ -129,40 +127,46 @@ pub(crate) mod test_utils {
}
pub fn create_dummy_spec() -> Spec {
let linux = oci::Linux {
namespaces: TYPETONAME
.iter()
.filter(|&(_, &name)| name != "user")
.map(|ns| LinuxNamespace {
r#type: ns.0.to_string(),
path: "".to_string(),
})
.collect(),
..Default::default()
};
Spec {
version: TEST_OCI_SPEC_VERSION.to_string(),
process: Some(Process {
args: vec!["sleep".to_string(), "10".to_string()],
env: vec!["PATH=/bin:/usr/bin".to_string()],
cwd: "/".to_string(),
..Default::default()
}),
hostname: TEST_HOST_NAME.to_string(),
root: Some(Root {
path: TEST_ROOTFS_PATH.to_string(),
readonly: false,
}),
linux: Some(linux),
..Default::default()
}
let linux = LinuxBuilder::default()
.namespaces(
TYPETONAME
.iter()
.filter(|&(_, &name)| name != "user")
.map(|ns| {
LinuxNamespaceBuilder::default()
.typ(ns.0.clone())
.path(PathBuf::from(""))
.build()
.unwrap()
})
.collect::<Vec<_>>(),
)
.build()
.unwrap();
let mut process = Process::default();
process.set_args(Some(vec!["sleep".to_string(), "10".to_string()]));
process.set_env(Some(vec!["PATH=/bin:/usr/bin".to_string()]));
process.set_cwd(PathBuf::from("/"));
let mut root = Root::default();
root.set_path(PathBuf::from(TEST_ROOTFS_PATH));
root.set_readonly(Some(false));
let mut spec = Spec::default();
spec.set_version(TEST_OCI_SPEC_VERSION.to_string());
spec.set_process(Some(process));
spec.set_hostname(Some(TEST_HOST_NAME.to_string()));
spec.set_root(Some(root));
spec.set_linux(Some(linux));
spec
}
pub fn create_dummy_opts() -> CreateOpts {
let spec = Spec {
root: Some(Root::default()),
..Default::default()
};
let mut spec = Spec::default();
spec.set_root(Some(Root::default()));
CreateOpts {
cgroup_name: "".to_string(),
use_systemd_cgroup: false,
@ -219,7 +223,7 @@ pub(crate) mod test_utils {
.unwrap()
.starttime;
Status {
oci_version: spec.version.clone(),
oci_version: spec.version().clone(),
id: id.to_string(),
pid,
root: root.to_path_buf(),
@ -247,13 +251,13 @@ pub(crate) mod test_utils {
#[test]
fn test_canonicalize_spec_root() {
let gen_spec = |p: &str| -> Spec {
Spec {
root: Some(Root {
path: p.to_string(),
readonly: false,
}),
..Default::default()
}
let mut root = Root::default();
root.set_path(PathBuf::from(p));
root.set_readonly(Some(false));
let mut spec = Spec::default();
spec.set_root(Some(root));
spec
};
let rootfs_name = TEST_ROOTFS_PATH;
@ -263,29 +267,28 @@ pub(crate) mod test_utils {
create_dir_all(abs_root.clone()).unwrap();
let mut spec = gen_spec(abs_root.to_str().unwrap());
assert!(canonicalize_spec_root(&mut spec, bundle_dir).is_ok());
assert_eq!(spec.root.unwrap().path, abs_root.to_str().unwrap());
assert_eq!(spec.root_mut().clone().unwrap().path(), &abs_root);
let mut spec = gen_spec(rootfs_name);
assert!(canonicalize_spec_root(&mut spec, bundle_dir).is_ok());
assert_eq!(spec.root.unwrap().path, abs_root.to_str().unwrap());
assert_eq!(spec.root().clone().unwrap().path(), &abs_root);
}
#[test]
pub fn test_validate_process_spec() {
let valid_process = Process {
args: vec!["test".to_string()],
cwd: "/".to_string(),
..Default::default()
};
let mut valid_process = Process::default();
valid_process.set_args(Some(vec!["test".to_string()]));
valid_process.set_cwd(PathBuf::from("/"));
assert!(validate_process_spec(&None).is_err());
assert!(validate_process_spec(&Some(valid_process.clone())).is_ok());
let mut invalid_process = valid_process.clone();
invalid_process.args = vec![];
invalid_process.set_args(None);
assert!(validate_process_spec(&Some(invalid_process)).is_err());
let mut invalid_process = valid_process.clone();
invalid_process.cwd = "".to_string();
invalid_process.set_cwd(PathBuf::from(""));
assert!(validate_process_spec(&Some(invalid_process)).is_err());
let mut invalid_process = valid_process;
invalid_process.cwd = "test/".to_string();
invalid_process.set_cwd(PathBuf::from("test/"));
assert!(validate_process_spec(&Some(invalid_process)).is_err());
}
}

View File

@ -7,7 +7,7 @@ use super::state::get_container_state_name;
use anyhow::Result;
use libcontainer::container::Container;
use liboci_cli::List;
use oci::ContainerState;
use runtime_spec::ContainerState;
use slog::{info, Logger};
use std::fmt::Write as _;
use std::{fs, os::unix::prelude::MetadataExt, path::Path};

View File

@ -7,7 +7,7 @@ use anyhow::Result;
use chrono::{DateTime, Utc};
use libcontainer::{container::Container, status::Status};
use liboci_cli::State;
use oci::ContainerState;
use runtime_spec::ContainerState;
use serde::{Deserialize, Serialize};
use slog::{info, Logger};
use std::path::{Path, PathBuf};
@ -62,7 +62,7 @@ pub fn get_container_state_name(state: ContainerState) -> String {
#[cfg(test)]
mod tests {
use super::*;
use oci::ContainerState;
use runtime_spec::ContainerState;
#[test]
fn test_get_container_state_name() {