buildkit_frontend/
oci.rs

1use std::collections::BTreeMap;
2use std::convert::TryFrom;
3use std::path::PathBuf;
4
5use chrono::prelude::*;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9// https://github.com/opencontainers/image-spec/blob/v1.0.1/config.md
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct ImageSpecification {
13    /// An combined date and time at which the image was created.
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub created: Option<DateTime<Utc>>,
16
17    /// Gives the name and/or email address of the person or entity which created and is responsible for maintaining the image.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub author: Option<String>,
20
21    /// The CPU architecture which the binaries in this image are built to run on.
22    pub architecture: Architecture,
23
24    /// The name of the operating system which the image is built to run on.
25    pub os: OperatingSystem,
26
27    /// The execution parameters which should be used as a base when running a container using the image.
28    /// This field can be `None`, in which case any execution parameters should be specified at creation of the container.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub config: Option<ImageConfig>,
31
32    /// The rootfs key references the layer content addresses used by the image.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub rootfs: Option<ImageRootfs>,
35
36    /// Describes the history of each layer.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub history: Option<Vec<LayerHistoryItem>>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum Architecture {
44    /// 64-bit x86, the most mature port
45    Amd64,
46
47    /// 32-bit x86
48    I386,
49
50    /// 32-bit ARM
51    ARM,
52
53    /// 64-bit ARM
54    ARM64,
55
56    /// PowerPC 64-bit, little-endian
57    PPC64le,
58
59    /// PowerPC 64-bit, big-endian
60    PPC64,
61
62    /// MIPS 64-bit, little-endian
63    Mips64le,
64
65    /// MIPS 64-bit, big-endian
66    Mips64,
67
68    /// MIPS 32-bit, little-endian
69    Mipsle,
70
71    /// MIPS 32-bit, big-endian
72    Mips,
73
74    /// IBM System z 64-bit, big-endian
75    S390x,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
79#[serde(rename_all = "lowercase")]
80pub enum OperatingSystem {
81    Darwin,
82    Dragonfly,
83    Freebsd,
84    Linux,
85    Netbsd,
86    Openbsd,
87    Plan9,
88    Solaris,
89    Windows,
90}
91
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93#[serde(from = "RawImageConfig")]
94#[serde(into = "RawImageConfig")]
95pub struct ImageConfig {
96    /// The username or UID which is a platform-specific structure that allows specific control over which user the process run as.
97    pub user: Option<String>,
98
99    /// A set of ports to expose from a container running this image.
100    pub exposed_ports: Option<Vec<ExposedPort>>,
101
102    /// Environment variables for the process to run with.
103    pub env: Option<BTreeMap<String, String>>,
104
105    /// A list of arguments to use as the command to execute when the container starts.
106    pub entrypoint: Option<Vec<String>>,
107
108    /// Default arguments to the entrypoint of the container.
109    pub cmd: Option<Vec<String>>,
110
111    /// A set of directories describing where the process is likely write data specific to a container instance.
112    pub volumes: Option<Vec<PathBuf>>,
113
114    /// Sets the current working directory of the entrypoint process in the container.
115    pub working_dir: Option<PathBuf>,
116
117    /// The field contains arbitrary metadata for the container.
118    pub labels: Option<BTreeMap<String, String>>,
119
120    /// The field contains the system call signal that will be sent to the container to exit.
121    pub stop_signal: Option<Signal>,
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
125#[serde(rename_all = "PascalCase")]
126struct RawImageConfig {
127    #[serde(skip_serializing_if = "Option::is_none")]
128    user: Option<String>,
129
130    #[serde(skip_serializing_if = "Option::is_none")]
131    exposed_ports: Option<BTreeMap<ExposedPort, Value>>,
132
133    #[serde(skip_serializing_if = "Option::is_none")]
134    env: Option<Vec<String>>,
135
136    #[serde(skip_serializing_if = "Option::is_none")]
137    entrypoint: Option<Vec<String>>,
138
139    #[serde(skip_serializing_if = "Option::is_none")]
140    cmd: Option<Vec<String>>,
141
142    #[serde(skip_serializing_if = "Option::is_none")]
143    volumes: Option<BTreeMap<PathBuf, Value>>,
144
145    #[serde(skip_serializing_if = "Option::is_none")]
146    working_dir: Option<PathBuf>,
147
148    #[serde(skip_serializing_if = "Option::is_none")]
149    labels: Option<BTreeMap<String, String>>,
150
151    #[serde(skip_serializing_if = "Option::is_none")]
152    stop_signal: Option<Signal>,
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct ImageRootfs {
157    /// Must be set to `RootfsType::Layers`.
158    #[serde(rename = "type")]
159    pub diff_type: RootfsType,
160
161    /// An array of layer content hashes (DiffIDs), in order from first to last.
162    pub diff_ids: Vec<String>,
163}
164
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166pub struct LayerHistoryItem {
167    /// A combined date and time at which the layer was created.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub created: Option<DateTime<Utc>>,
170
171    /// The author of the build point.
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub author: Option<String>,
174
175    /// The command which created the layer.
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub created_by: Option<String>,
178
179    /// A custom message set when creating the layer.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub comment: Option<String>,
182
183    /// This field is used to mark if the history item created a filesystem diff.
184    /// It is set to true if this history item doesn't correspond to an actual layer in the rootfs section
185    /// (for example, Dockerfile's ENV command results in no change to the filesystem).
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub empty_layer: Option<bool>,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize)]
191#[serde(try_from = "String")]
192#[serde(into = "String")]
193pub enum ExposedPort {
194    Tcp(u16),
195    Udp(u16),
196}
197
198impl TryFrom<String> for ExposedPort {
199    type Error = std::num::ParseIntError;
200
201    fn try_from(value: String) -> Result<Self, Self::Error> {
202        let postfix_len = value.len() - 4;
203
204        match &value[postfix_len..] {
205            "/tcp" => Ok(ExposedPort::Tcp(value[..postfix_len].parse()?)),
206            "/udp" => Ok(ExposedPort::Udp(value[..postfix_len].parse()?)),
207
208            _ => Ok(ExposedPort::Tcp(value.parse()?)),
209        }
210    }
211}
212
213impl Into<String> for ExposedPort {
214    fn into(self) -> String {
215        match self {
216            ExposedPort::Tcp(port) => format!("{}/tcp", port),
217            ExposedPort::Udp(port) => format!("{}/udp", port),
218        }
219    }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
223#[serde(rename_all = "lowercase")]
224pub enum RootfsType {
225    Layers,
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
229pub enum Signal {
230    SIGHUP,
231    SIGINT,
232    SIGQUIT,
233    SIGILL,
234    SIGTRAP,
235    SIGABRT,
236    SIGBUS,
237    SIGFPE,
238    SIGKILL,
239    SIGUSR1,
240    SIGSEGV,
241    SIGUSR2,
242    SIGPIPE,
243    SIGALRM,
244    SIGTERM,
245    SIGSTKFLT,
246    SIGCHLD,
247    SIGCONT,
248    SIGSTOP,
249    SIGTSTP,
250    SIGTTIN,
251    SIGTTOU,
252    SIGURG,
253    SIGXCPU,
254    SIGXFSZ,
255    SIGVTALRM,
256    SIGPROF,
257    SIGWINCH,
258    SIGIO,
259    SIGPWR,
260    SIGSYS,
261    SIGEMT,
262    SIGINFO,
263}
264
265impl From<RawImageConfig> for ImageConfig {
266    fn from(raw: RawImageConfig) -> Self {
267        Self {
268            user: raw.user,
269            entrypoint: raw.entrypoint,
270            cmd: raw.cmd,
271            working_dir: raw.working_dir,
272            labels: raw.labels,
273            stop_signal: raw.stop_signal,
274
275            env: raw.env.map(|inner| {
276                inner
277                    .into_iter()
278                    .map(|mut pair| match pair.find('=') {
279                        Some(pos) => {
280                            let value = pair.split_off(pos + 1);
281                            let mut name = pair;
282                            name.pop();
283
284                            (name, value)
285                        }
286
287                        None => (pair, String::with_capacity(0)),
288                    })
289                    .collect()
290            }),
291
292            exposed_ports: raw
293                .exposed_ports
294                .map(|inner| inner.into_iter().map(|(port, _)| port).collect()),
295
296            volumes: raw
297                .volumes
298                .map(|inner| inner.into_iter().map(|(volume, _)| volume).collect()),
299        }
300    }
301}
302
303impl Into<RawImageConfig> for ImageConfig {
304    fn into(self) -> RawImageConfig {
305        RawImageConfig {
306            user: self.user,
307            entrypoint: self.entrypoint,
308            cmd: self.cmd,
309            working_dir: self.working_dir,
310            labels: self.labels,
311            stop_signal: self.stop_signal,
312
313            env: self.env.map(|inner| {
314                inner
315                    .into_iter()
316                    .map(|(key, value)| format!("{}={}", key, value))
317                    .collect()
318            }),
319
320            exposed_ports: self.exposed_ports.map(|inner| {
321                inner
322                    .into_iter()
323                    .map(|port| (port, Value::Object(Default::default())))
324                    .collect()
325            }),
326
327            volumes: self.volumes.map(|inner| {
328                inner
329                    .into_iter()
330                    .map(|volume| (volume, Value::Object(Default::default())))
331                    .collect()
332            }),
333        }
334    }
335}
336
337#[test]
338fn serialization() {
339    use pretty_assertions::assert_eq;
340
341    let ref_json = include_str!("../tests/oci-image-spec.json");
342    let ref_spec = ImageSpecification {
343        created: Some("2015-10-31T22:22:56.015925234Z".parse().unwrap()),
344        author: Some("Alyssa P. Hacker <alyspdev@example.com>".into()),
345        architecture: Architecture::Amd64,
346        os: OperatingSystem::Linux,
347        rootfs: Some(ImageRootfs {
348            diff_type: RootfsType::Layers,
349            diff_ids: vec![
350                "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1".into(),
351                "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef".into(),
352            ],
353        }),
354        history: Some(vec![
355            LayerHistoryItem {
356                created: Some("2015-10-31T22:22:54.690851953Z".parse().unwrap()),
357                created_by: Some("/bin/sh -c #(nop) ADD file in /".into()),
358                author: None,
359                comment: None,
360                empty_layer: None,
361            },
362            LayerHistoryItem {
363                created: Some("2015-10-31T22:22:55.613815829Z".parse().unwrap()),
364                created_by: Some("/bin/sh -c #(nop) CMD [\"sh\"]".into()),
365                author: None,
366                comment: None,
367                empty_layer: Some(true),
368            },
369        ]),
370
371        config: Some(ImageConfig {
372            user: Some("alice".into()),
373            exposed_ports: Some(vec![ExposedPort::Tcp(8080), ExposedPort::Udp(8081)]),
374            env: Some(
375                vec![(
376                    String::from("PATH"),
377                    String::from("/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"),
378                )]
379                .into_iter()
380                .collect(),
381            ),
382            entrypoint: Some(vec!["/bin/my-app-binary".into()]),
383            cmd: Some(vec![
384                "--foreground".into(),
385                "--config".into(),
386                "/etc/my-app.d/default.cfg".into(),
387            ]),
388            volumes: Some(vec![
389                "/var/job-result-data".into(),
390                "/var/log/my-app-logs".into(),
391            ]),
392            working_dir: Some("/home/alice".into()),
393            labels: Some(
394                vec![(
395                    String::from("com.example.project.git.url"),
396                    String::from("https://example.com/project.git"),
397                )]
398                .into_iter()
399                .collect(),
400            ),
401            stop_signal: Some(Signal::SIGKILL),
402        }),
403    };
404
405    assert_eq!(serde_json::to_string_pretty(&ref_spec).unwrap(), ref_json);
406    assert_eq!(
407        serde_json::from_str::<ImageSpecification>(ref_json).unwrap(),
408        ref_spec
409    );
410}
411
412#[test]
413fn min_serialization() {
414    use pretty_assertions::assert_eq;
415
416    let ref_json = include_str!("../tests/oci-image-spec-min.json");
417    let ref_spec = ImageSpecification {
418        created: None,
419        author: None,
420
421        architecture: Architecture::Amd64,
422        os: OperatingSystem::Linux,
423        rootfs: Some(ImageRootfs {
424            diff_type: RootfsType::Layers,
425            diff_ids: vec![
426                "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1".into(),
427                "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef".into(),
428            ],
429        }),
430
431        history: None,
432        config: None,
433    };
434
435    assert_eq!(serde_json::to_string_pretty(&ref_spec).unwrap(), ref_json);
436    assert_eq!(
437        serde_json::from_str::<ImageSpecification>(ref_json).unwrap(),
438        ref_spec
439    );
440}