Skip to content

Commit

Permalink
kaps: Implement simple OCI container state specification
Browse files Browse the repository at this point in the history
Signed-off-by: kalil <[email protected]>
  • Loading branch information
kalil-pelissier authored and sameo committed Apr 23, 2022
1 parent a3ac95b commit 51c76fa
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 6 deletions.
4 changes: 2 additions & 2 deletions container/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ authors = ["Polytech Montpellier - DevOps"]
lazy_static = "1.4.0"
oci-spec = "0.5.3"
unshare = { git = "https://github.com/virt-do/unshare", branch = "main" }
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"

[dev-dependencies]
proc-mounts = "0.3.0"
tempdir = "0.3.7"
tempdir = "0.3.7"
39 changes: 35 additions & 4 deletions container/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
extern crate core;

use std::path::PathBuf;

use oci_spec::runtime::Spec;
Expand All @@ -6,12 +8,14 @@ use command::Command;
use environment::Environment;
use mounts::Mounts;
use namespaces::Namespaces;
use state::{ContainerState, Status};

mod command;
mod environment;
mod mounts;
mod namespaces;
pub mod spec;
mod state;

/// Containers related errors
#[derive(Debug)]
Expand All @@ -21,7 +25,23 @@ pub enum Error {
ContainerSpawnCommand(unshare::Error),
ContainerWaitCommand(std::io::Error),
ContainerExit(i32),
/// Fail to create container due to existing container with the same id.
ContainerExists(String),
Unmount(std::io::Error),
/// Fail to read container state file.
WriteStateFile(serde_json::error::Error),
/// Fail to save container state file.
ReadStateFile(std::io::Error),
/// Fail to serialize/deserialize file.
SerializeError(serde_json::error::Error),
/// Fail to open container state file.
OpenStateFile(std::io::Error),
/// Fail to create container state file.
CreateStateFile(std::io::Error),
/// Fail to remove container state file.
RemoveStateFile(std::io::Error),
/// Fail to acquire lock for the container status
StatusLockPoisoned(String),
}

/// A common result type for our container module.
Expand All @@ -45,11 +65,13 @@ pub struct Container {
environment: Environment,
/// The command entrypoint
command: Command,
/// The container state
state: ContainerState,
}

impl Container {
/// Build a new container with the bundle provided in parameters.
pub fn new(bundle_path: &str) -> Result<Self> {
pub fn new(bundle_path: &str, id: &str) -> Result<Self> {
let bundle = PathBuf::from(bundle_path);

// Load the specification from the file
Expand All @@ -73,17 +95,21 @@ impl Container {
Namespaces::from(linux.namespaces())
});

// Set the state of the container
let state = ContainerState::new(id, bundle_path)?;

Ok(Container {
environment: Environment::from(spec.process()),
command: Command::from(spec.process()),
namespaces,
rootfs,
state,
..Default::default()
})
}

/// Run the container.
pub fn run(&self) -> Result<()> {
pub fn run(&mut self) -> Result<()> {
let mounts = self.mounts.clone();
let code = unsafe {
let mut child = match unshare::Command::from(&self.command)
Expand All @@ -98,10 +124,15 @@ impl Container {
return self.mounts.cleanup(self.rootfs.clone());
}
};

self.state.pid = child.pid();
self.state.set_status(Status::Running)?;

child.wait().map_err(Error::ContainerWaitCommand)?.code()
};

self.mounts.cleanup(self.rootfs.clone())?;
self.state.remove()?;

if let Some(code) = code {
if code != 0 {
Expand Down Expand Up @@ -131,7 +162,7 @@ mod tests {
)?;

let host_mounts_before_run_fail = MountList::new().unwrap();
let container = Container::new(test_folder_path).unwrap();
let mut container = Container::new(test_folder_path, "test_folder").unwrap();
assert!(container.run().is_err());

let host_mounts_after_run_fail = MountList::new().unwrap();
Expand All @@ -142,7 +173,7 @@ mod tests {

#[test]
fn test_create_container_with_invalid_oci_runtime_spec_file() -> Result<(), Error> {
assert!(Container::new("invalid_spec_file").is_err());
assert!(Container::new("invalid_spec_file", "invalid_spec").is_err());
Ok(())
}
}
208 changes: 208 additions & 0 deletions container/src/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::fs::{File, OpenOptions};
use std::path::PathBuf;
use std::sync::{Arc, RwLock};

const KAPS_ROOT_PATH: &str = "/var/run/kaps/containers";
const OCI_VERSION: &str = "0.2.0";
const STATE_FILE: &str = "state.json";

/// Container runtime status
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
pub enum Status {
#[serde(rename = "creating")]
Creating,
#[serde(rename = "created")]
Created,
#[serde(rename = "running")]
Running,
#[serde(rename = "stopped")]
Stopped,
}

impl Default for Status {
fn default() -> Self {
Status::Creating
}
}

/// Represent the state of the running container.
#[derive(Serialize, Deserialize, Debug)]
pub struct ContainerState {
id: String,
/// OCI version.
oci_version: String,
/// Runtime state of the container.
pub status: Arc<RwLock<Status>>,
/// ID of the container process.
pub pid: i32,
/// Path to the bundle.
bundle: PathBuf,
}

impl Default for ContainerState {
fn default() -> Self {
ContainerState {
oci_version: OCI_VERSION.to_string(),
id: String::default(),
status: Arc::new(RwLock::new(Status::default())),
pid: 0,
bundle: PathBuf::default(),
}
}
}

impl ContainerState {
pub fn new(id: &str, bundle_path: &str) -> Result<Self> {
ContainerState::_new(id, bundle_path, KAPS_ROOT_PATH)
}

fn _new(id: &str, bundle_path: &str, container_dir: &str) -> Result<Self> {
let bundle = PathBuf::from(bundle_path);
let container_path = PathBuf::from(container_dir).join(id);

if container_path.as_path().exists() {
return Err(Error::ContainerExists(format!(
"A container with the id '{}' already exists",
id
)));
}

// create the container directory
fs::create_dir_all(&container_path).map_err(Error::CreateStateFile)?;

// create the `state.json` file of the container
File::create(container_path.join(STATE_FILE)).map_err(Error::CreateStateFile)?;

let container_state = ContainerState {
id: id.to_string(),
bundle,
..Default::default()
};

container_state.save(container_dir)?;

Ok(container_state)
}

/// Get the current runtime status of the container.
///
/// As The container status is a RwLock,
/// calling this function results in acquiring a read lock on the status.
fn _status(&self) -> Result<Status> {
let container_status = Arc::clone(&self.status);
let container_status = container_status
.read()
.map_err(|e| Error::StatusLockPoisoned(e.to_string()))?;

Ok(*container_status)
}

/// Save the container state.
///
/// The container state file must already have been created.
fn save(&self, container_dir: &str) -> Result<()> {
let container_path = PathBuf::from(container_dir).join(&self.id);

let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(container_path.join(STATE_FILE))
.map_err(Error::OpenStateFile)?;

serde_json::to_writer_pretty(file, &self).map_err(Error::WriteStateFile)
}

/// Update runtime status of container.
pub fn set_status(&mut self, status: Status) -> Result<()> {
self._set_status(status, KAPS_ROOT_PATH)
}

fn _set_status(&mut self, status: Status, container_dir: &str) -> Result<()> {
let container_status = Arc::clone(&self.status);

let mut container_status = container_status
.write()
.map_err(|e| Error::StatusLockPoisoned(e.to_string()))?;

*container_status = status;
drop(container_status);

self.save(container_dir)
}

/// Remove the container state file.
pub fn remove(&self) -> Result<()> {
self._remove(KAPS_ROOT_PATH)
}

fn _remove(&self, container_dir: &str) -> Result<()> {
let container_path = PathBuf::from(container_dir).join(&self.id);

fs::remove_dir_all(container_path).map_err(Error::RemoveStateFile)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{Error, Result};
use std::path::Path;

const KAPS_TEST_ROOT_PATH: &str = "/tmp/kaps";

#[test]
fn should_create_state_file() -> Result<()> {
let container_id = "test1";

let state = ContainerState::_new(container_id, "fake/path/to/bundle", KAPS_TEST_ROOT_PATH)?;

assert!(Path::new(KAPS_TEST_ROOT_PATH).join(container_id).exists());

let _ = state._remove(KAPS_TEST_ROOT_PATH)?;

Ok(())
}

#[test]
fn should_remove_state_file() -> Result<()> {
let container_id = "test2";

let state = ContainerState::_new(container_id, "fake/path/to/bundle", KAPS_TEST_ROOT_PATH)?;

let _ = state._remove(KAPS_TEST_ROOT_PATH)?;

assert!(!Path::new(KAPS_TEST_ROOT_PATH).join(container_id).exists());

Ok(())
}

#[test]
fn should_update_runtime_status() -> Result<()> {
let container_id = "test3";
let container_path = PathBuf::from(KAPS_TEST_ROOT_PATH).join(container_id);

let mut state =
ContainerState::_new(container_id, "fake/path/to/bundle", KAPS_TEST_ROOT_PATH)?;

state._set_status(Status::Stopped, KAPS_TEST_ROOT_PATH)?;

let file_state =
fs::read_to_string(container_path.join(STATE_FILE)).map_err(Error::ReadStateFile)?;

let file_state: ContainerState =
serde_json::from_str(&file_state).map_err(Error::SerializeError)?;

let file_status = file_state._status()?;

let status = state._status()?;

assert_eq!(status, file_status);

let _ = state._remove(KAPS_TEST_ROOT_PATH)?;

Ok(())
}
}

0 comments on commit 51c76fa

Please sign in to comment.