oci_spec/image/config.rs
1use super::{Arch, Os};
2use crate::{
3 error::{OciSpecError, Result},
4 from_file, from_reader, to_file, to_string, to_writer,
5};
6use derive_builder::Builder;
7use getset::{CopyGetters, Getters, MutGetters, Setters};
8use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
9#[cfg(test)]
10use std::collections::BTreeMap;
11use std::{
12 collections::HashMap,
13 fmt::Display,
14 io::{Read, Write},
15 path::Path,
16};
17
18/// In theory, this key is not standard. In practice, it's used by at least the
19/// RHEL UBI images for a long time.
20pub const LABEL_VERSION: &str = "version";
21
22#[derive(
23 Builder,
24 Clone,
25 Debug,
26 Default,
27 Deserialize,
28 Eq,
29 Getters,
30 MutGetters,
31 Setters,
32 PartialEq,
33 Serialize,
34)]
35#[builder(
36 default,
37 pattern = "owned",
38 setter(into, strip_option),
39 build_fn(error = "OciSpecError")
40)]
41#[getset(get = "pub", set = "pub")]
42/// The image configuration is associated with an image and describes some
43/// basic information about the image such as date created, author, as
44/// well as execution/runtime configuration like its entrypoint, default
45/// arguments, networking, and volumes.
46pub struct ImageConfiguration {
47 /// An combined date and time at which the image was created,
48 /// formatted as defined by [RFC 3339, section 5.6.](https://tools.ietf.org/html/rfc3339#section-5.6)
49 #[serde(skip_serializing_if = "Option::is_none")]
50 created: Option<String>,
51 /// Gives the name and/or email address of the person or entity
52 /// which created and is responsible for maintaining the image.
53 #[serde(skip_serializing_if = "Option::is_none")]
54 author: Option<String>,
55 /// The CPU architecture which the binaries in this
56 /// image are built to run on. Configurations SHOULD use, and
57 /// implementations SHOULD understand, values listed in the Go
58 /// Language document for [GOARCH](https://golang.org/doc/install/source#environment).
59 architecture: Arch,
60 /// The name of the operating system which the image is built to run on.
61 /// Configurations SHOULD use, and implementations SHOULD understand,
62 /// values listed in the Go Language document for [GOOS](https://golang.org/doc/install/source#environment).
63 os: Os,
64 /// This OPTIONAL property specifies the version of the operating
65 /// system targeted by the referenced blob. Implementations MAY refuse
66 /// to use manifests where os.version is not known to work with
67 /// the host OS version. Valid values are
68 /// implementation-defined. e.g. 10.0.14393.1066 on windows.
69 #[serde(rename = "os.version", skip_serializing_if = "Option::is_none")]
70 os_version: Option<String>,
71 /// This OPTIONAL property specifies an array of strings,
72 /// each specifying a mandatory OS feature. When os is windows, image
73 /// indexes SHOULD use, and implementations SHOULD understand
74 /// the following values:
75 /// - win32k: image requires win32k.sys on the host (Note: win32k.sys is
76 /// missing on Nano Server)
77 #[serde(rename = "os.features", skip_serializing_if = "Option::is_none")]
78 os_features: Option<Vec<String>>,
79 /// The variant of the specified CPU architecture. Configurations SHOULD
80 /// use, and implementations SHOULD understand, variant values
81 /// listed in the [Platform Variants](https://github.com/opencontainers/image-spec/blob/main/image-index.md#platform-variants) table.
82 #[serde(skip_serializing_if = "Option::is_none")]
83 variant: Option<String>,
84 /// The execution parameters which SHOULD be used as a base when
85 /// running a container using the image. This field can be None, in
86 /// which case any execution parameters should be specified at
87 /// creation of the container.
88 #[serde(skip_serializing_if = "Option::is_none")]
89 config: Option<Config>,
90 /// The rootfs key references the layer content addresses used by the
91 /// image. This makes the image config hash depend on the
92 /// filesystem hash.
93 #[getset(get_mut = "pub", get = "pub", set = "pub")]
94 rootfs: RootFs,
95 /// Describes the history of each layer. The array is ordered from first
96 /// to last.
97 #[getset(get_mut = "pub", get = "pub", set = "pub")]
98 history: Vec<History>,
99}
100
101impl ImageConfiguration {
102 /// Attempts to load an image configuration from a file.
103 /// # Errors
104 /// This function will return an [OciSpecError::Io](crate::OciSpecError::Io)
105 /// if the file does not exist or an
106 /// [OciSpecError::SerDe](crate::OciSpecError::SerDe) if the image configuration
107 /// cannot be deserialized.
108 /// # Example
109 /// ``` no_run
110 /// use oci_spec::image::ImageConfiguration;
111 ///
112 /// let image_index = ImageConfiguration::from_file("config.json").unwrap();
113 /// ```
114 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<ImageConfiguration> {
115 from_file(path)
116 }
117
118 /// Attempts to load an image configuration from a stream.
119 /// # Errors
120 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe)
121 /// if the image configuration cannot be deserialized.
122 /// # Example
123 /// ``` no_run
124 /// use oci_spec::image::ImageConfiguration;
125 /// use std::fs::File;
126 ///
127 /// let reader = File::open("config.json").unwrap();
128 /// let image_index = ImageConfiguration::from_reader(reader).unwrap();
129 /// ```
130 pub fn from_reader<R: Read>(reader: R) -> Result<ImageConfiguration> {
131 from_reader(reader)
132 }
133
134 /// Attempts to write an image configuration to a file as JSON. If the file already exists, it
135 /// will be overwritten.
136 /// # Errors
137 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
138 /// the image configuration cannot be serialized.
139 /// # Example
140 /// ``` no_run
141 /// use oci_spec::image::ImageConfiguration;
142 ///
143 /// let image_index = ImageConfiguration::from_file("config.json").unwrap();
144 /// image_index.to_file("my-config.json").unwrap();
145 /// ```
146 pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
147 to_file(&self, path, false)
148 }
149
150 /// Attempts to write an image configuration to a file as pretty printed JSON. If the file
151 /// already exists, it will be overwritten.
152 /// # Errors
153 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
154 /// the image configuration cannot be serialized.
155 /// # Example
156 /// ``` no_run
157 /// use oci_spec::image::ImageConfiguration;
158 ///
159 /// let image_index = ImageConfiguration::from_file("config.json").unwrap();
160 /// image_index.to_file_pretty("my-config.json").unwrap();
161 /// ```
162 pub fn to_file_pretty<P: AsRef<Path>>(&self, path: P) -> Result<()> {
163 to_file(&self, path, true)
164 }
165
166 /// Attempts to write an image configuration to a stream as JSON.
167 /// # Errors
168 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
169 /// the image configuration cannot be serialized.
170 /// # Example
171 /// ``` no_run
172 /// use oci_spec::image::ImageConfiguration;
173 ///
174 /// let image_index = ImageConfiguration::from_file("config.json").unwrap();
175 /// let mut writer = Vec::new();
176 /// image_index.to_writer(&mut writer);
177 /// ```
178 pub fn to_writer<W: Write>(&self, writer: &mut W) -> Result<()> {
179 to_writer(&self, writer, false)
180 }
181
182 /// Attempts to write an image configuration to a stream as pretty printed JSON.
183 /// # Errors
184 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
185 /// the image configuration cannot be serialized.
186 /// # Example
187 /// ``` no_run
188 /// use oci_spec::image::ImageConfiguration;
189 ///
190 /// let image_index = ImageConfiguration::from_file("config.json").unwrap();
191 /// let mut writer = Vec::new();
192 /// image_index.to_writer_pretty(&mut writer);
193 /// ```
194 pub fn to_writer_pretty<W: Write>(&self, writer: &mut W) -> Result<()> {
195 to_writer(&self, writer, true)
196 }
197
198 /// Attempts to write an image configuration to a string as JSON.
199 /// # Errors
200 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
201 /// the image configuration cannot be serialized.
202 /// # Example
203 /// ``` no_run
204 /// use oci_spec::image::ImageConfiguration;
205 ///
206 /// let image_configuration = ImageConfiguration::from_file("config.json").unwrap();
207 /// let json_str = image_configuration.to_string().unwrap();
208 /// ```
209 pub fn to_string(&self) -> Result<String> {
210 to_string(&self, false)
211 }
212
213 /// Attempts to write an image configuration to a string as pretty printed JSON.
214 /// # Errors
215 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
216 /// the image configuration cannot be serialized.
217 /// # Example
218 /// ``` no_run
219 /// use oci_spec::image::ImageConfiguration;
220 ///
221 /// let image_configuration = ImageConfiguration::from_file("config.json").unwrap();
222 /// let json_str = image_configuration.to_string_pretty().unwrap();
223 /// ```
224 pub fn to_string_pretty(&self) -> Result<String> {
225 to_string(&self, true)
226 }
227
228 /// Extract the labels of the configuration, if present.
229 pub fn labels_of_config(&self) -> Option<&HashMap<String, String>> {
230 self.config().as_ref().and_then(|c| c.labels().as_ref())
231 }
232
233 /// Retrieve the version number associated with this configuration. This will try
234 /// to use several well-known label keys.
235 pub fn version(&self) -> Option<&str> {
236 let labels = self.labels_of_config();
237 if let Some(labels) = labels {
238 for k in [super::ANNOTATION_VERSION, LABEL_VERSION] {
239 if let Some(v) = labels.get(k) {
240 return Some(v.as_str());
241 }
242 }
243 }
244 None
245 }
246
247 /// Extract the value of a given annotation on the configuration, if present.
248 pub fn get_config_annotation(&self, key: &str) -> Option<&str> {
249 self.labels_of_config()
250 .and_then(|v| v.get(key).map(|s| s.as_str()))
251 }
252}
253
254/// This ToString trait is automatically implemented for any type which implements the Display trait.
255/// As such, ToString shouldn’t be implemented directly: Display should be implemented instead,
256/// and you get the ToString implementation for free.
257impl Display for ImageConfiguration {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 // Serde seralization never fails since this is
260 // a combination of String and enums.
261 write!(
262 f,
263 "{}",
264 self.to_string_pretty()
265 .expect("ImageConfiguration JSON convertion failed")
266 )
267 }
268}
269
270#[derive(
271 Builder,
272 Clone,
273 Debug,
274 Default,
275 Deserialize,
276 Eq,
277 Getters,
278 MutGetters,
279 Setters,
280 PartialEq,
281 Serialize,
282)]
283#[serde(rename_all = "PascalCase")]
284#[builder(
285 default,
286 pattern = "owned",
287 setter(into, strip_option),
288 build_fn(error = "OciSpecError")
289)]
290#[getset(get = "pub", set = "pub")]
291/// The execution parameters which SHOULD be used as a base when
292/// running a container using the image.
293pub struct Config {
294 /// The username or UID which is a platform-specific
295 /// structure that allows specific control over which
296 /// user the process run as. This acts as a default
297 /// value to use when the value is not specified when
298 /// creating a container. For Linux based systems, all
299 /// of the following are valid: user, uid, user:group,
300 /// uid:gid, uid:group, user:gid. If group/gid is not
301 /// specified, the default group and supplementary
302 /// groups of the given user/uid in /etc/passwd from
303 /// the container are applied.
304 #[serde(skip_serializing_if = "Option::is_none")]
305 user: Option<String>,
306 /// A set of ports to expose from a container running this
307 /// image. Its keys can be in the format of: port/tcp, port/udp,
308 /// port with the default protocol being tcp if not specified.
309 /// These values act as defaults and are merged with any
310 /// specified when creating a container.
311 #[serde(
312 default,
313 skip_serializing_if = "Option::is_none",
314 deserialize_with = "deserialize_as_vec",
315 serialize_with = "serialize_as_map"
316 )]
317 exposed_ports: Option<Vec<String>>,
318 /// Entries are in the format of VARNAME=VARVALUE. These
319 /// values act as defaults and are merged with any
320 /// specified when creating a container.
321 #[serde(skip_serializing_if = "Option::is_none")]
322 env: Option<Vec<String>>,
323 /// A list of arguments to use as the command to execute
324 /// when the container starts. These values act as defaults
325 /// and may be replaced by an entrypoint specified when
326 /// creating a container.
327 #[serde(skip_serializing_if = "Option::is_none")]
328 entrypoint: Option<Vec<String>>,
329 /// Default arguments to the entrypoint of the container.
330 /// These values act as defaults and may be replaced by any
331 /// specified when creating a container. If an Entrypoint
332 /// value is not specified, then the first entry of the Cmd
333 /// array SHOULD be interpreted as the executable to run.
334 #[serde(skip_serializing_if = "Option::is_none")]
335 cmd: Option<Vec<String>>,
336 /// A set of directories describing where the process is
337 /// likely to write data specific to a container instance.
338 #[serde(
339 default,
340 skip_serializing_if = "Option::is_none",
341 deserialize_with = "deserialize_as_vec",
342 serialize_with = "serialize_as_map"
343 )]
344 volumes: Option<Vec<String>>,
345 /// Sets the current working directory of the entrypoint process
346 /// in the container. This value acts as a default and may be
347 /// replaced by a working directory specified when creating
348 /// a container.
349 #[serde(skip_serializing_if = "Option::is_none")]
350 working_dir: Option<String>,
351 /// The field contains arbitrary metadata for the container.
352 /// This property MUST use the annotation rules.
353 #[serde(skip_serializing_if = "Option::is_none")]
354 #[getset(get_mut = "pub", get = "pub", set = "pub")]
355 labels: Option<HashMap<String, String>>,
356 /// The field contains the system call signal that will be
357 /// sent to the container to exit. The signal can be a signal
358 /// name in the format SIGNAME, for instance SIGKILL or SIGRTMIN+3.
359 #[serde(skip_serializing_if = "Option::is_none")]
360 stop_signal: Option<String>,
361}
362
363// Some fields of the image configuration are a json serialization of a
364// Go map[string]struct{} leading to the following json:
365// {
366// "ExposedPorts": {
367// "8080/tcp": {},
368// "443/tcp": {},
369// }
370// }
371// Instead we treat this as a list
372#[derive(Deserialize, Serialize)]
373struct GoMapSerde {}
374
375fn deserialize_as_vec<'de, D>(deserializer: D) -> std::result::Result<Option<Vec<String>>, D::Error>
376where
377 D: Deserializer<'de>,
378{
379 // ensure stable order of keys in json document for comparison between expected and actual
380 #[cfg(test)]
381 let opt = Option::<BTreeMap<String, GoMapSerde>>::deserialize(deserializer)?;
382 #[cfg(not(test))]
383 let opt = Option::<HashMap<String, GoMapSerde>>::deserialize(deserializer)?;
384
385 if let Some(data) = opt {
386 let vec: Vec<String> = data.keys().cloned().collect();
387 return Ok(Some(vec));
388 }
389
390 Ok(None)
391}
392
393fn serialize_as_map<S>(
394 target: &Option<Vec<String>>,
395 serializer: S,
396) -> std::result::Result<S::Ok, S::Error>
397where
398 S: Serializer,
399{
400 match target {
401 Some(values) => {
402 // ensure stable order of keys in json document for comparison between expected and actual
403 #[cfg(test)]
404 let map: BTreeMap<_, _> = values.iter().map(|v| (v, GoMapSerde {})).collect();
405 #[cfg(not(test))]
406 let map: HashMap<_, _> = values.iter().map(|v| (v, GoMapSerde {})).collect();
407
408 let mut map_ser = serializer.serialize_map(Some(map.len()))?;
409 for (key, value) in map {
410 map_ser.serialize_entry(key, &value)?;
411 }
412 map_ser.end()
413 }
414 _ => unreachable!(),
415 }
416}
417
418#[derive(
419 Builder, Clone, Debug, Deserialize, Eq, Getters, MutGetters, Setters, PartialEq, Serialize,
420)]
421#[builder(
422 default,
423 pattern = "owned",
424 setter(into, strip_option),
425 build_fn(error = "OciSpecError")
426)]
427#[getset(get = "pub", set = "pub")]
428/// RootFs references the layer content addresses used by the image.
429pub struct RootFs {
430 /// MUST be set to layers.
431 #[serde(rename = "type")]
432 typ: String,
433 /// An array of layer content hashes (DiffIDs), in order
434 /// from first to last.
435 #[getset(get_mut = "pub", get = "pub", set = "pub")]
436 diff_ids: Vec<String>,
437}
438
439impl Default for RootFs {
440 fn default() -> Self {
441 Self {
442 typ: "layers".to_owned(),
443 diff_ids: Default::default(),
444 }
445 }
446}
447
448#[derive(
449 Builder,
450 Clone,
451 Debug,
452 Default,
453 Deserialize,
454 Eq,
455 CopyGetters,
456 Getters,
457 Setters,
458 PartialEq,
459 Serialize,
460)]
461#[builder(
462 default,
463 pattern = "owned",
464 setter(into, strip_option),
465 build_fn(error = "OciSpecError")
466)]
467/// Describes the history of a layer.
468pub struct History {
469 /// A combined date and time at which the layer was created,
470 /// formatted as defined by [RFC 3339, section 5.6.](https://tools.ietf.org/html/rfc3339#section-5.6).
471 #[serde(skip_serializing_if = "Option::is_none")]
472 #[getset(get = "pub", set = "pub")]
473 created: Option<String>,
474 /// The author of the build point.
475 #[serde(skip_serializing_if = "Option::is_none")]
476 #[getset(get = "pub", set = "pub")]
477 author: Option<String>,
478 /// The command which created the layer.
479 #[serde(skip_serializing_if = "Option::is_none")]
480 #[getset(get = "pub", set = "pub")]
481 created_by: Option<String>,
482 /// A custom message set when creating the layer.
483 #[serde(skip_serializing_if = "Option::is_none")]
484 #[getset(get = "pub", set = "pub")]
485 comment: Option<String>,
486 /// This field is used to mark if the history item created
487 /// a filesystem diff. It is set to true if this history item
488 /// doesn't correspond to an actual layer in the rootfs section
489 #[serde(skip_serializing_if = "Option::is_none")]
490 #[getset(get_copy = "pub", set = "pub")]
491 empty_layer: Option<bool>,
492}
493
494#[cfg(test)]
495mod tests {
496 use std::{fs, path::PathBuf};
497
498 use super::*;
499 use crate::image::{ANNOTATION_CREATED, ANNOTATION_VERSION};
500
501 fn create_base_config() -> ConfigBuilder {
502 ConfigBuilder::default()
503 .user("alice".to_owned())
504 .exposed_ports(vec!["8080/tcp".to_owned()])
505 .env(vec![
506 "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_owned(),
507 "FOO=oci_is_a".to_owned(),
508 "BAR=well_written_spec".to_owned(),
509 ])
510 .entrypoint(vec!["/bin/my-app-binary".to_owned()])
511 .cmd(vec![
512 "--foreground".to_owned(),
513 "--config".to_owned(),
514 "/etc/my-app.d/default.cfg".to_owned(),
515 ])
516 .volumes(vec![
517 "/var/job-result-data".to_owned(),
518 "/var/log/my-app-logs".to_owned(),
519 ])
520 .working_dir("/home/alice".to_owned())
521 }
522
523 fn create_base_imgconfig(conf: Config) -> ImageConfigurationBuilder {
524 ImageConfigurationBuilder::default()
525 .created("2015-10-31T22:22:56.015925234Z".to_owned())
526 .author("Alyssa P. Hacker <alyspdev@example.com>".to_owned())
527 .architecture(Arch::Amd64)
528 .os(Os::Linux)
529 .config(conf
530 )
531 .rootfs(RootFsBuilder::default()
532 .diff_ids(vec![
533 "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1".to_owned(),
534 "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef".to_owned(),
535 ])
536 .build()
537 .expect("build rootfs"))
538 .history(vec![
539 HistoryBuilder::default()
540 .created("2015-10-31T22:22:54.690851953Z".to_owned())
541 .created_by("/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /".to_owned())
542 .build()
543 .expect("build history"),
544 HistoryBuilder::default()
545 .created("2015-10-31T22:22:55.613815829Z".to_owned())
546 .created_by("/bin/sh -c #(nop) CMD [\"sh\"]".to_owned())
547 .empty_layer(true)
548 .build()
549 .expect("build history"),
550 ])
551 }
552
553 fn create_config() -> ImageConfiguration {
554 create_base_imgconfig(create_base_config().build().expect("config"))
555 .build()
556 .expect("build configuration")
557 }
558
559 /// A config with some additions (labels)
560 fn create_imgconfig_v1() -> ImageConfiguration {
561 let labels = [
562 (ANNOTATION_CREATED, "2023-09-16T19:22:18.014Z"),
563 (ANNOTATION_VERSION, "42.27"),
564 ]
565 .into_iter()
566 .map(|(k, v)| (k.to_owned(), v.to_owned()));
567 let config = create_base_config()
568 .labels(labels.collect::<HashMap<_, _>>())
569 .build()
570 .unwrap();
571 create_base_imgconfig(config).build().unwrap()
572 }
573
574 fn get_config_path() -> PathBuf {
575 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test/data/config.json")
576 }
577
578 #[test]
579 fn load_configuration_from_file() {
580 // arrange
581 let config_path = get_config_path();
582 let expected = create_config();
583
584 // act
585 let actual = ImageConfiguration::from_file(config_path).expect("from file");
586
587 // assert
588 assert_eq!(actual, expected);
589 }
590
591 #[test]
592 fn test_helpers() {
593 let config = create_imgconfig_v1();
594 assert_eq!(config.labels_of_config().unwrap().len(), 2);
595 assert_eq!(
596 config.get_config_annotation(ANNOTATION_CREATED).unwrap(),
597 "2023-09-16T19:22:18.014Z"
598 );
599 }
600
601 #[test]
602 fn load_configuration_from_reader() {
603 // arrange
604 let reader = fs::read(get_config_path()).expect("read config");
605
606 // act
607 let actual = ImageConfiguration::from_reader(&*reader).expect("from reader");
608 println!("{actual:#?}");
609
610 // assert
611 let expected = create_config();
612 println!("{expected:#?}");
613
614 assert_eq!(actual, expected);
615 }
616
617 #[test]
618 fn save_config_to_file() {
619 // arrange
620 let tmp = std::env::temp_dir().join("save_config_to_file");
621 fs::create_dir_all(&tmp).expect("create test directory");
622 let config = create_config();
623 let config_path = tmp.join("config.json");
624
625 // act
626 config
627 .to_file_pretty(&config_path)
628 .expect("write config to file");
629
630 // assert
631 let actual = fs::read_to_string(config_path).expect("read actual");
632 let expected = fs::read_to_string(get_config_path()).expect("read expected");
633 assert_eq!(actual, expected);
634 }
635
636 #[test]
637 fn save_config_to_writer() {
638 // arrange
639 let config = create_config();
640 let mut actual = Vec::new();
641
642 // act
643 config.to_writer_pretty(&mut actual).expect("to writer");
644 let actual = String::from_utf8(actual).unwrap();
645
646 // assert
647 let expected = fs::read_to_string(get_config_path()).expect("read expected");
648 assert_eq!(actual, expected);
649 }
650
651 #[test]
652 fn save_config_to_string() {
653 // arrange
654 let config = create_config();
655
656 // act
657 let actual = config.to_string_pretty().expect("to string");
658
659 // assert
660 let expected = fs::read_to_string(get_config_path()).expect("read expected");
661 assert_eq!(actual, expected);
662 }
663}