use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use crate::error::{OciError, Result};
use crate::hooks::Hooks;
use super::{Linux, Mount, Process, Root};
pub const OCI_VERSION: &str = "1.2.0";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Spec {
pub oci_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub root: Option<Root>,
#[serde(skip_serializing_if = "Option::is_none")]
pub process: Option<Process>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hostname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domainname: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mounts: Vec<Mount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<Hooks>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub annotations: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub linux: Option<Linux>,
}
impl Spec {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref())?;
Self::from_json(&content)
}
pub fn from_json(json: &str) -> Result<Self> {
let spec: Self = serde_json::from_str(json)?;
spec.validate()?;
Ok(spec)
}
pub fn to_json(&self) -> Result<String> {
Ok(serde_json::to_string_pretty(self)?)
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let json = self.to_json()?;
std::fs::write(path, json)?;
Ok(())
}
pub fn validate(&self) -> Result<()> {
if self.oci_version.is_empty() {
return Err(OciError::MissingField("ociVersion"));
}
if self.oci_version.split('.').count() < 2 {
return Err(OciError::InvalidVersion(self.oci_version.clone()));
}
if let Some(ref root) = self.root {
if root.path.is_empty() {
return Err(OciError::InvalidConfig(
"root.path cannot be empty".to_string(),
));
}
}
if let Some(ref process) = self.process {
if process.cwd.is_empty() {
return Err(OciError::MissingField("process.cwd"));
}
if !process.cwd.starts_with('/') {
return Err(OciError::InvalidConfig(
"process.cwd must be an absolute path".to_string(),
));
}
}
for (i, mount) in self.mounts.iter().enumerate() {
if mount.destination.is_empty() {
return Err(OciError::InvalidConfig(format!(
"mounts[{i}].destination cannot be empty"
)));
}
if !mount.destination.starts_with('/') {
return Err(OciError::InvalidConfig(format!(
"mounts[{i}].destination must be an absolute path"
)));
}
}
Ok(())
}
#[must_use]
pub fn default_linux() -> Self {
Self {
oci_version: OCI_VERSION.to_string(),
root: Some(Root {
path: "rootfs".to_string(),
readonly: false,
}),
process: Some(Process::default()),
hostname: None,
domainname: None,
mounts: Self::default_mounts(),
hooks: None,
annotations: HashMap::new(),
linux: Some(Linux::default()),
}
}
fn default_mounts() -> Vec<Mount> {
vec![
Mount {
destination: "/proc".to_string(),
source: Some("proc".to_string()),
mount_type: Some("proc".to_string()),
options: Some(vec![
"nosuid".to_string(),
"noexec".to_string(),
"nodev".to_string(),
]),
..Default::default()
},
Mount {
destination: "/dev".to_string(),
source: Some("tmpfs".to_string()),
mount_type: Some("tmpfs".to_string()),
options: Some(vec![
"nosuid".to_string(),
"strictatime".to_string(),
"mode=755".to_string(),
"size=65536k".to_string(),
]),
..Default::default()
},
Mount {
destination: "/dev/pts".to_string(),
source: Some("devpts".to_string()),
mount_type: Some("devpts".to_string()),
options: Some(vec![
"nosuid".to_string(),
"noexec".to_string(),
"newinstance".to_string(),
"ptmxmode=0666".to_string(),
"mode=0620".to_string(),
]),
..Default::default()
},
Mount {
destination: "/dev/shm".to_string(),
source: Some("shm".to_string()),
mount_type: Some("tmpfs".to_string()),
options: Some(vec![
"nosuid".to_string(),
"noexec".to_string(),
"nodev".to_string(),
"mode=1777".to_string(),
"size=65536k".to_string(),
]),
..Default::default()
},
Mount {
destination: "/sys".to_string(),
source: Some("sysfs".to_string()),
mount_type: Some("sysfs".to_string()),
options: Some(vec![
"nosuid".to_string(),
"noexec".to_string(),
"nodev".to_string(),
"ro".to_string(),
]),
..Default::default()
},
]
}
}