1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
//! [OCI runtime spec](https://github.com/opencontainers/runtime-spec) types and definitions.
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use crate::error::{oci_error, Result};
mod capability;
mod hooks;
mod linux;
mod miscellaneous;
mod process;
mod solaris;
mod test;
mod vm;
mod windows;
// re-export for ease of use
pub use capability::*;
pub use hooks::*;
pub use linux::*;
pub use miscellaneous::*;
pub use process::*;
pub use solaris::*;
pub use vm::*;
pub use windows::*;
make_pub!(
/// Base configuration for the container.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "builder",
derive(derive_builder::Builder, getset::Getters),
builder(default, pattern = "owned", setter(into, strip_option)),
getset(get = "pub")
)]
struct Spec {
#[serde(default, rename = "ociVersion")]
/// MUST be in SemVer v2.0.0 format and specifies the version of the
/// Open Container Initiative Runtime Specification with which
/// the bundle complies. The Open Container Initiative
/// Runtime Specification follows semantic versioning and retains
/// forward and backward compatibility within major versions.
/// For example, if a configuration is compliant with
/// version 1.1 of this specification, it is compatible with all
/// runtimes that support any 1.1 or later release of this
/// specification, but is not compatible with a runtime that supports
/// 1.0 and not 1.1.
version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
/// Specifies the container's root filesystem. On Windows, for Windows
/// Server Containers, this field is REQUIRED. For Hyper-V
/// Containers, this field MUST NOT be set.
///
/// On all other platforms, this field is REQUIRED.
root: Option<Root>,
#[serde(default, skip_serializing_if = "Option::is_none")]
/// Specifies additional mounts beyond `root`. The runtime MUST mount
/// entries in the listed order.
///
/// For Linux, the parameters are as documented in
/// [`mount(2)`](http://man7.org/linux/man-pages/man2/mount.2.html) system call man page. For
/// Solaris, the mount entry corresponds to the 'fs' resource in the
/// [`zonecfg(1M)`](http://docs.oracle.com/cd/E86824_01/html/E54764/zonecfg-1m.html) man page.
mounts: Option<Vec<Mount>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
/// Specifies the container process. This property is REQUIRED when
/// [`start`](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md#start) is
/// called.
process: Option<Process>,
#[serde(default, skip_serializing_if = "Option::is_none")]
/// Specifies the container's hostname as seen by processes running
/// inside the container. On Linux, for example, this will
/// change the hostname in the container [UTS namespace](http://man7.org/linux/man-pages/man7/namespaces.7.html). Depending on your
/// [namespace
/// configuration](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#namespaces),
/// the container UTS namespace may be the runtime UTS namespace.
hostname: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
/// Hooks allow users to specify programs to run before or after various
/// lifecycle events. Hooks MUST be called in the listed order.
/// The state of the container MUST be passed to hooks over
/// stdin so that they may do work appropriate to the current state of
/// the container.
hooks: Option<Hooks>,
#[serde(default, skip_serializing_if = "Option::is_none")]
/// Annotations contains arbitrary metadata for the container. This
/// information MAY be structured or unstructured. Annotations
/// MUST be a key-value map. If there are no annotations then
/// this property MAY either be absent or an empty map.
///
/// Keys MUST be strings. Keys MUST NOT be an empty string. Keys SHOULD
/// be named using a reverse domain notation - e.g.
/// com.example.myKey. Keys using the org.opencontainers
/// namespace are reserved and MUST NOT be used by subsequent
/// specifications. Runtimes MUST handle unknown annotation keys
/// like any other unknown property.
///
/// Values MUST be strings. Values MAY be an empty string.
annotations: Option<HashMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
/// Linux is platform-specific configuration for Linux based containers.
linux: Option<Linux>,
#[serde(default, skip_serializing_if = "Option::is_none")]
/// Solaris is platform-specific configuration for Solaris based
/// containers.
solaris: Option<Solaris>,
#[serde(default, skip_serializing_if = "Option::is_none")]
/// Windows is platform-specific configuration for Windows based
/// containers.
windows: Option<Windows>,
#[serde(default, skip_serializing_if = "Option::is_none")]
/// VM specifies configuration for Virtual Machine based containers.
vm: Option<VM>,
}
);
// This gives a basic boilerplate for Spec that can be used calling
// Default::default(). The values given are similar to the defaults seen in
// docker and runc, it creates a containerized shell! (see respective types
// default impl for more info)
impl Default for Spec {
fn default() -> Self {
Spec {
// Defaults to most current oci version
version: String::from("1.0.2-dev"),
process: Some(Default::default()),
root: Some(Default::default()),
// Defaults hostname as youki
hostname: "youki".to_string().into(),
mounts: get_default_mounts().into(),
// Defaults to empty metadata
annotations: Some(Default::default()),
linux: Some(Default::default()),
hooks: None,
solaris: None,
windows: None,
vm: None,
}
}
}
impl Spec {
/// Load a new `Spec` from the provided JSON file `path`.
/// # Errors
/// This function will return an [OciSpecError::Io](crate::OciSpecError::Io)
/// if the spec does not exist or an
/// [OciSpecError::SerDe](crate::OciSpecError::SerDe) if it is invalid.
/// # Example
/// ``` no_run
/// use oci_spec::runtime::Spec;
///
/// let spec = Spec::load("config.json").unwrap();
/// ```
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let file = fs::File::open(path)?;
let s = serde_json::from_reader(&file)?;
Ok(s)
}
/// Save a `Spec` to the provided JSON file `path`.
/// # Errors
/// This function will return an [OciSpecError::Io](crate::OciSpecError::Io)
/// if a file cannot be created at the provided path or an
/// [OciSpecError::SerDe](crate::OciSpecError::SerDe) if the spec cannot be
/// serialized.
/// # Example
/// ``` no_run
/// use oci_spec::runtime::Spec;
///
/// let mut spec = Spec::load("config.json").unwrap();
/// spec.save("my_config.json").unwrap();
/// ```
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path = path.as_ref();
let file = fs::File::create(path)?;
serde_json::to_writer(&file, self)?;
Ok(())
}
/// Canonicalize the `root.path` of the `Spec` for the provided `bundle`.
pub fn canonicalize_rootfs<P: AsRef<Path>>(&mut self, bundle: P) -> Result<()> {
cfg_if::cfg_if!(
if #[cfg(feature = "builder")] {
let root = self.root.as_ref().ok_or_else(||oci_error("no root path provided for canonicalization"))?;
let path = Self::canonicalize_path(bundle, root.path())?;
self.root = Some(RootBuilder::default().path(path).readonly(root.readonly()).build()
.map_err(|_| oci_error("failed to set canonicalized root"))?);
} else {
let root = self.root.as_mut().ok_or_else(||oci_error("no root path provided for canonicalization"))?;
root.path = Self::canonicalize_path(bundle, &root.path)?;
}
);
Ok(())
}
fn canonicalize_path<B, P>(bundle: B, path: P) -> Result<PathBuf>
where
B: AsRef<Path>,
P: AsRef<Path>,
{
Ok(if path.as_ref().is_absolute() {
fs::canonicalize(path.as_ref())?
} else {
let canonical_bundle_path = fs::canonicalize(&bundle)?;
fs::canonicalize(canonical_bundle_path.join(path.as_ref()))?
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canonicalize_rootfs() {
let rootfs_name = "rootfs";
let bundle = tempfile::tempdir().expect("failed to create tmp test bundle dir");
let rootfs_absolute_path = bundle.path().join(rootfs_name);
assert!(
rootfs_absolute_path.is_absolute(),
"rootfs path is not absolute path"
);
fs::create_dir_all(&rootfs_absolute_path).expect("failed to create the testing rootfs");
{
// Test the case with absolute path
cfg_if::cfg_if!(
if #[cfg(feature = "builder")] {
let mut spec = SpecBuilder::default()
.root(RootBuilder::default()
.path(rootfs_absolute_path.clone())
.build().unwrap())
.build().unwrap();
} else {
let mut spec = Spec {
root: Root {
path: rootfs_absolute_path.clone(),
..Default::default()
}
.into(),
..Default::default()
};
}
);
spec.canonicalize_rootfs(bundle.path())
.expect("failed to canonicalize rootfs");
cfg_if::cfg_if!(
if #[cfg(feature = "builder")] {
assert_eq!(
&rootfs_absolute_path,
spec.root.expect("no root in spec").path()
);
} else {
assert_eq!(
rootfs_absolute_path,
spec.root.expect("no root in spec").path
);
}
);
}
{
// Test the case with relative path
cfg_if::cfg_if!(
if #[cfg(feature = "builder")] {
let mut spec = SpecBuilder::default()
.root(RootBuilder::default()
.path(rootfs_name)
.build().unwrap())
.build().unwrap();
} else {
let mut spec = Spec {
root: Root {
path: PathBuf::from(rootfs_name),
..Default::default()
}
.into(),
..Default::default()
};
}
);
spec.canonicalize_rootfs(bundle.path())
.expect("failed to canonicalize rootfs");
cfg_if::cfg_if!(
if #[cfg(feature = "builder")] {
assert_eq!(
&rootfs_absolute_path,
spec.root.expect("no root in spec").path()
);
} else {
assert_eq!(
rootfs_absolute_path,
spec.root.expect("no root in spec").path
);
}
);
}
}
#[test]
fn test_load_save() {
let spec = Spec {
..Default::default()
};
let test_dir = tempfile::tempdir().expect("failed to create tmp test dir");
let spec_path = test_dir.into_path().join("config.json");
// Test first save the default config, and then load the saved config.
// The before and after should be the same.
spec.save(&spec_path).expect("failed to save spec");
let loaded_spec = Spec::load(&spec_path).expect("failed to load the saved spec.");
assert_eq!(
spec, loaded_spec,
"The saved spec is not the same as the loaded spec"
);
}
}