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}