use std::collections::HashMap;
use std::fmt::Display;
use std::fs;
use std::fs::File;
use std::io::{BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use oci_spec::OciSpecError;
use oci_spec::runtime::{
ContainerState as OciContainerState, State as OciState, StateBuilder as OciStateBuilder,
VERSION as OCI_RUNTIME_VERSION,
};
use serde::{Deserialize, Serialize};
use tracing::instrument;
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub enum ContainerStatus {
#[default]
Creating,
Created,
Running,
Stopped,
Paused,
}
impl ContainerStatus {
pub fn can_start(&self) -> bool {
matches!(self, ContainerStatus::Created)
}
pub fn can_kill(&self) -> bool {
use ContainerStatus::*;
match self {
Creating | Stopped => false,
Created | Running | Paused => true,
}
}
pub fn can_delete(&self) -> bool {
matches!(self, ContainerStatus::Stopped)
}
pub fn can_pause(&self) -> bool {
matches!(self, ContainerStatus::Running)
}
pub fn can_resume(&self) -> bool {
matches!(self, ContainerStatus::Paused)
}
}
impl Display for ContainerStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let print = match *self {
Self::Creating => "Creating",
Self::Created => "Created",
Self::Running => "Running",
Self::Stopped => "Stopped",
Self::Paused => "Paused",
};
write!(f, "{print}")
}
}
#[derive(Debug, thiserror::Error)]
pub enum StateError {
#[error("failed to open container state file {state_file_path:?}")]
OpenStateFile {
state_file_path: PathBuf,
source: std::io::Error,
},
#[error("failed to parse container state file {state_file_path:?}")]
ParseStateFile {
state_file_path: PathBuf,
source: serde_json::Error,
},
#[error("failed to write container state file {state_file_path:?}")]
WriteStateFile {
state_file_path: PathBuf,
source: std::io::Error,
},
}
type Result<T> = std::result::Result<T, StateError>;
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct State {
pub oci_version: String,
pub id: String,
pub status: ContainerStatus,
#[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<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub creator: Option<u32>,
pub use_systemd: bool,
pub clean_up_intel_rdt_subdirectory: Option<bool>,
}
impl State {
const STATE_FILE_PATH: &'static str = "state.json";
pub fn new(
container_id: &str,
status: ContainerStatus,
pid: Option<i32>,
bundle: PathBuf,
) -> Self {
Self {
oci_version: OCI_RUNTIME_VERSION.to_string(),
id: container_id.to_string(),
status,
pid,
bundle,
annotations: Some(HashMap::default()),
created: None,
creator: None,
use_systemd: false,
clean_up_intel_rdt_subdirectory: None,
}
}
#[instrument(level = "trace")]
pub fn save(&self, container_root: &Path) -> Result<()> {
let state_file_path = Self::file_path(container_root);
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.append(false)
.create(true)
.truncate(true)
.open(&state_file_path)
.map_err(|err| {
tracing::error!(
state_file_path = ?state_file_path,
err = %err,
"failed to open container state file",
);
StateError::OpenStateFile {
state_file_path: state_file_path.to_owned(),
source: err,
}
})?;
let mut writer = BufWriter::new(file);
serde_json::to_writer(&mut writer, self).map_err(|err| {
tracing::error!(
?state_file_path,
%err,
"failed to parse container state file",
);
StateError::ParseStateFile {
state_file_path: state_file_path.to_owned(),
source: err,
}
})?;
writer.flush().map_err(|err| {
tracing::error!(
?state_file_path,
%err,
"failed to write container state file",
);
StateError::WriteStateFile {
state_file_path: state_file_path.to_owned(),
source: err,
}
})?;
Ok(())
}
pub fn load(container_root: &Path) -> Result<Self> {
let state_file_path = Self::file_path(container_root);
let state_file = File::open(&state_file_path).map_err(|err| {
tracing::error!(
?state_file_path,
%err,
"failed to open container state file",
);
StateError::OpenStateFile {
state_file_path: state_file_path.to_owned(),
source: err,
}
})?;
let state: Self = serde_json::from_reader(BufReader::new(state_file)).map_err(|err| {
tracing::error!(
?state_file_path,
%err,
"failed to parse container state file",
);
StateError::ParseStateFile {
state_file_path: state_file_path.to_owned(),
source: err,
}
})?;
Ok(state)
}
pub fn file_path(container_root: &Path) -> PathBuf {
container_root.join(Self::STATE_FILE_PATH)
}
}
#[derive(Debug, thiserror::Error)]
pub enum StateConversionError {
#[error("failed to build OCI state: {0}")]
OciStateBuild(#[from] OciSpecError),
#[error("invalid container status for OCI conversion: {0}")]
InvalidStatus(ContainerStatus),
}
impl TryFrom<&State> for OciState {
type Error = StateConversionError;
fn try_from(state: &State) -> std::result::Result<Self, Self::Error> {
let status = OciContainerState::try_from(state.status)?;
let mut builder = OciStateBuilder::default()
.version(state.oci_version.clone())
.id(state.id.clone())
.status(status)
.bundle(state.bundle.clone());
if let Some(annotations) = &state.annotations {
builder = builder.annotations(annotations.clone());
}
if let Some(pid) = state.pid {
builder = builder.pid(pid);
}
Ok(builder.build()?)
}
}
impl TryFrom<ContainerStatus> for OciContainerState {
type Error = StateConversionError;
fn try_from(status: ContainerStatus) -> std::result::Result<Self, Self::Error> {
match status {
ContainerStatus::Creating => Ok(OciContainerState::Creating),
ContainerStatus::Created => Ok(OciContainerState::Created),
ContainerStatus::Running => Ok(OciContainerState::Running),
ContainerStatus::Stopped => Ok(OciContainerState::Stopped),
ContainerStatus::Paused => Err(StateConversionError::InvalidStatus(status)),
}
}
}
#[deprecated(
since = "0.6.0",
note = "Use oci_spec::runtime::ContainerProcessState instead"
)]
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct ContainerProcessState {
pub oci_version: String,
pub fds: Vec<String>,
pub pid: i32,
pub metadata: String,
pub state: State,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_creating_status() {
let cstatus = ContainerStatus::default();
assert!(!cstatus.can_start());
assert!(!cstatus.can_delete());
assert!(!cstatus.can_kill());
assert!(!cstatus.can_pause());
assert!(!cstatus.can_resume());
}
#[test]
fn test_create_status() {
let cstatus = ContainerStatus::Created;
assert!(cstatus.can_start());
assert!(!cstatus.can_delete());
assert!(cstatus.can_kill());
assert!(!cstatus.can_pause());
assert!(!cstatus.can_resume());
}
#[test]
fn test_running_status() {
let cstatus = ContainerStatus::Running;
assert!(!cstatus.can_start());
assert!(!cstatus.can_delete());
assert!(cstatus.can_kill());
assert!(cstatus.can_pause());
assert!(!cstatus.can_resume());
}
#[test]
fn test_stopped_status() {
let cstatus = ContainerStatus::Stopped;
assert!(!cstatus.can_start());
assert!(cstatus.can_delete());
assert!(!cstatus.can_kill());
assert!(!cstatus.can_pause());
assert!(!cstatus.can_resume());
}
#[test]
fn test_paused_status() {
let cstatus = ContainerStatus::Paused;
assert!(!cstatus.can_start());
assert!(!cstatus.can_delete());
assert!(cstatus.can_kill());
assert!(!cstatus.can_pause());
assert!(cstatus.can_resume());
}
}