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