1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6
7use use_oci_hook::OciHook;
8use use_oci_namespace::NamespaceKind;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum RuntimeError {
13 Empty,
14 InvalidMount,
15 InvalidResource,
16}
17
18impl fmt::Display for RuntimeError {
19 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Self::Empty => formatter.write_str("OCI runtime value cannot be empty"),
22 Self::InvalidMount => formatter.write_str("invalid OCI mount metadata"),
23 Self::InvalidResource => formatter.write_str("invalid OCI resource metadata"),
24 }
25 }
26}
27
28impl Error for RuntimeError {}
29
30macro_rules! text_value {
31 ($name:ident) => {
32 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33 pub struct $name(String);
34
35 impl $name {
36 pub fn new(value: impl AsRef<str>) -> Result<Self, RuntimeError> {
38 let trimmed = value.as_ref().trim();
39 if trimmed.is_empty() {
40 return Err(RuntimeError::Empty);
41 }
42 if trimmed.contains('\0') {
43 return Err(RuntimeError::InvalidMount);
44 }
45 Ok(Self(trimmed.to_string()))
46 }
47
48 #[must_use]
50 pub fn as_str(&self) -> &str {
51 &self.0
52 }
53 }
54
55 impl AsRef<str> for $name {
56 fn as_ref(&self) -> &str {
57 self.as_str()
58 }
59 }
60
61 impl fmt::Display for $name {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 formatter.write_str(self.as_str())
64 }
65 }
66 };
67}
68
69text_value!(ProcessArg);
70text_value!(RuntimeEnv);
71text_value!(Cwd);
72text_value!(Capability);
73text_value!(RootFilesystem);
74
75#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
77pub enum MountKind {
78 Bind,
79 Tmpfs,
80 Proc,
81 Sysfs,
82 Cgroup,
83 Custom,
84}
85
86#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
88pub struct Mount {
89 kind: MountKind,
90 source: String,
91 destination: String,
92 options: Vec<String>,
93}
94
95impl Mount {
96 pub fn new(
98 kind: MountKind,
99 source: impl AsRef<str>,
100 destination: impl AsRef<str>,
101 ) -> Result<Self, RuntimeError> {
102 let source = non_empty(source.as_ref(), RuntimeError::InvalidMount)?;
103 let destination = non_empty(destination.as_ref(), RuntimeError::InvalidMount)?;
104 Ok(Self {
105 kind,
106 source: source.to_string(),
107 destination: destination.to_string(),
108 options: Vec::new(),
109 })
110 }
111
112 #[must_use]
114 pub fn with_option(mut self, option: impl Into<String>) -> Self {
115 self.options.push(option.into());
116 self
117 }
118
119 #[must_use]
121 pub const fn kind(&self) -> MountKind {
122 self.kind
123 }
124
125 #[must_use]
127 pub fn destination(&self) -> &str {
128 &self.destination
129 }
130}
131
132#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
134pub struct ResourceLimit {
135 name: String,
136 value: u64,
137}
138
139impl ResourceLimit {
140 pub fn new(name: impl AsRef<str>, value: u64) -> Result<Self, RuntimeError> {
142 let name = non_empty(name.as_ref(), RuntimeError::InvalidResource)?;
143 Ok(Self {
144 name: name.to_string(),
145 value,
146 })
147 }
148
149 #[must_use]
151 pub fn name(&self) -> &str {
152 &self.name
153 }
154
155 #[must_use]
157 pub const fn value(&self) -> u64 {
158 self.value
159 }
160}
161
162#[derive(Clone, Debug, Eq, PartialEq)]
164pub struct RuntimeSpec {
165 root: RootFilesystem,
166 args: Vec<ProcessArg>,
167 env: Vec<RuntimeEnv>,
168 cwd: Option<Cwd>,
169 mounts: Vec<Mount>,
170 hooks: Vec<OciHook>,
171 namespaces: Vec<NamespaceKind>,
172 capabilities: Vec<Capability>,
173 resources: Vec<ResourceLimit>,
174}
175
176impl RuntimeSpec {
177 #[must_use]
179 pub fn new(root: RootFilesystem) -> Self {
180 Self {
181 root,
182 args: Vec::new(),
183 env: Vec::new(),
184 cwd: None,
185 mounts: Vec::new(),
186 hooks: Vec::new(),
187 namespaces: Vec::new(),
188 capabilities: Vec::new(),
189 resources: Vec::new(),
190 }
191 }
192
193 #[must_use]
195 pub fn with_arg(mut self, arg: ProcessArg) -> Self {
196 self.args.push(arg);
197 self
198 }
199
200 #[must_use]
202 pub fn with_mount(mut self, mount: Mount) -> Self {
203 self.mounts.push(mount);
204 self
205 }
206
207 #[must_use]
209 pub fn with_hook(mut self, hook: OciHook) -> Self {
210 self.hooks.push(hook);
211 self
212 }
213
214 #[must_use]
216 pub fn with_namespace(mut self, namespace: NamespaceKind) -> Self {
217 self.namespaces.push(namespace);
218 self
219 }
220
221 #[must_use]
223 pub const fn root(&self) -> &RootFilesystem {
224 &self.root
225 }
226
227 #[must_use]
229 pub fn namespaces(&self) -> &[NamespaceKind] {
230 &self.namespaces
231 }
232
233 #[must_use]
235 pub fn hooks(&self) -> &[OciHook] {
236 &self.hooks
237 }
238}
239
240fn non_empty(value: &str, error: RuntimeError) -> Result<&str, RuntimeError> {
241 let trimmed = value.trim();
242 if trimmed.is_empty() || trimmed.contains('\0') {
243 Err(error)
244 } else {
245 Ok(trimmed)
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::{Mount, MountKind, ProcessArg, RootFilesystem, RuntimeSpec};
252 use use_oci_hook::{HookKind, HookPath, OciHook};
253 use use_oci_namespace::NamespaceKind;
254
255 #[test]
256 fn models_runtime_metadata_without_execution() -> Result<(), Box<dyn std::error::Error>> {
257 let hook = OciHook::new(HookKind::Prestart, HookPath::new("/bin/check")?);
258 let spec = RuntimeSpec::new(RootFilesystem::new("rootfs")?)
259 .with_arg(ProcessArg::new("/bin/sh")?)
260 .with_mount(Mount::new(MountKind::Bind, "/host", "/container")?)
261 .with_namespace(NamespaceKind::Pid)
262 .with_hook(hook);
263
264 assert_eq!(spec.root().as_str(), "rootfs");
265 assert_eq!(spec.namespaces(), &[NamespaceKind::Pid]);
266 assert_eq!(spec.hooks().len(), 1);
267 Ok(())
268 }
269}