arcbox_oci/config/
spec.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9
10use crate::error::{OciError, Result};
11use crate::hooks::Hooks;
12
13use super::{Linux, Mount, Process, Root};
14
15pub const OCI_VERSION: &str = "1.2.0";
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct Spec {
25 pub oci_version: String,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub root: Option<Root>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub process: Option<Process>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub hostname: Option<String>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub domainname: Option<String>,
44
45 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub mounts: Vec<Mount>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub hooks: Option<Hooks>,
52
53 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
55 pub annotations: HashMap<String, String>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub linux: Option<Linux>,
60}
61
62impl Spec {
63 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
65 let content = std::fs::read_to_string(path.as_ref())?;
66 Self::from_json(&content)
67 }
68
69 pub fn from_json(json: &str) -> Result<Self> {
71 let spec: Self = serde_json::from_str(json)?;
72 spec.validate()?;
73 Ok(spec)
74 }
75
76 pub fn to_json(&self) -> Result<String> {
78 Ok(serde_json::to_string_pretty(self)?)
79 }
80
81 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
83 let json = self.to_json()?;
84 std::fs::write(path, json)?;
85 Ok(())
86 }
87
88 pub fn validate(&self) -> Result<()> {
90 if self.oci_version.is_empty() {
92 return Err(OciError::MissingField("ociVersion"));
93 }
94
95 if self.oci_version.split('.').count() < 2 {
97 return Err(OciError::InvalidVersion(self.oci_version.clone()));
98 }
99
100 if let Some(ref root) = self.root {
102 if root.path.is_empty() {
103 return Err(OciError::InvalidConfig(
104 "root.path cannot be empty".to_string(),
105 ));
106 }
107 }
108
109 if let Some(ref process) = self.process {
111 if process.cwd.is_empty() {
112 return Err(OciError::MissingField("process.cwd"));
113 }
114 if !process.cwd.starts_with('/') {
115 return Err(OciError::InvalidConfig(
116 "process.cwd must be an absolute path".to_string(),
117 ));
118 }
119 }
120
121 for (i, mount) in self.mounts.iter().enumerate() {
123 if mount.destination.is_empty() {
124 return Err(OciError::InvalidConfig(format!(
125 "mounts[{i}].destination cannot be empty"
126 )));
127 }
128 if !mount.destination.starts_with('/') {
129 return Err(OciError::InvalidConfig(format!(
130 "mounts[{i}].destination must be an absolute path"
131 )));
132 }
133 }
134
135 Ok(())
136 }
137
138 #[must_use]
140 pub fn default_linux() -> Self {
141 Self {
142 oci_version: OCI_VERSION.to_string(),
143 root: Some(Root {
144 path: "rootfs".to_string(),
145 readonly: false,
146 }),
147 process: Some(Process::default()),
148 hostname: None,
149 domainname: None,
150 mounts: Self::default_mounts(),
151 hooks: None,
152 annotations: HashMap::new(),
153 linux: Some(Linux::default()),
154 }
155 }
156
157 fn default_mounts() -> Vec<Mount> {
159 vec![
160 Mount {
161 destination: "/proc".to_string(),
162 source: Some("proc".to_string()),
163 mount_type: Some("proc".to_string()),
164 options: Some(vec![
165 "nosuid".to_string(),
166 "noexec".to_string(),
167 "nodev".to_string(),
168 ]),
169 ..Default::default()
170 },
171 Mount {
172 destination: "/dev".to_string(),
173 source: Some("tmpfs".to_string()),
174 mount_type: Some("tmpfs".to_string()),
175 options: Some(vec![
176 "nosuid".to_string(),
177 "strictatime".to_string(),
178 "mode=755".to_string(),
179 "size=65536k".to_string(),
180 ]),
181 ..Default::default()
182 },
183 Mount {
184 destination: "/dev/pts".to_string(),
185 source: Some("devpts".to_string()),
186 mount_type: Some("devpts".to_string()),
187 options: Some(vec![
188 "nosuid".to_string(),
189 "noexec".to_string(),
190 "newinstance".to_string(),
191 "ptmxmode=0666".to_string(),
192 "mode=0620".to_string(),
193 ]),
194 ..Default::default()
195 },
196 Mount {
197 destination: "/dev/shm".to_string(),
198 source: Some("shm".to_string()),
199 mount_type: Some("tmpfs".to_string()),
200 options: Some(vec![
201 "nosuid".to_string(),
202 "noexec".to_string(),
203 "nodev".to_string(),
204 "mode=1777".to_string(),
205 "size=65536k".to_string(),
206 ]),
207 ..Default::default()
208 },
209 Mount {
210 destination: "/sys".to_string(),
211 source: Some("sysfs".to_string()),
212 mount_type: Some("sysfs".to_string()),
213 options: Some(vec![
214 "nosuid".to_string(),
215 "noexec".to_string(),
216 "nodev".to_string(),
217 "ro".to_string(),
218 ]),
219 ..Default::default()
220 },
221 ]
222 }
223}