Skip to main content

nanite_docker/
lib.rs

1//! Nanite docker is an intermediate representation of a Dockerfile.
2//! It intends to provide a strongly typed library for working with Dockerfiles in your code.
3//! > Note: This is intended to be a low level library, intending to provide only the representation, and not any quality of life features.
4//! > if you have a library that provides some of these features, please add a pull request and I will be happy to add those libraries here.
5//!
6//! # Examples
7//! ## Basic Example
8//! Note that this example lacks an assert_eq!, see the specific instruction documentation to see what each instruction renders as
9//! ```rust
10//! use nanite_docker::*;
11//! let dockerfile = Dockerfile {
12//!     config: None,
13//!     comment: Some("Basic Dockerfile Example".into()),
14//!     stages: vec![Stage{
15//!         pre_from_instructions: vec![],
16//!         from: From {
17//!             platform: None,
18//!             image: "ubuntu:latest".into(),
19//!             alias: None,
20//!         },
21//!         instructions: vec![
22//!             Instruction::Cmd(Cmd{
23//!                 argv: vec!["echo".into(), "hello".into()]
24//!             })
25//!         ]
26//!     }]
27//! };
28//!
29//! let built_dockerfile = format!("{dockerfile}");
30//! ```
31//!
32//! ## Long Example (Showcases most features)
33//! ```rust
34//! use nanite_docker::*;
35//! fn main() {
36//!     let dockerfile = Dockerfile {
37//!         config: None,
38//!         comment: Some("Full instruction coverage test scenario".into()),
39//!         stages: vec![Stage {
40//!             // ── Pre-FROM ─────────────────────────────────────────────
41//!             pre_from_instructions: vec![
42//!                 PreFromInstruction::Arg(Arg {
43//!                     name: "BASE_IMAGE".into(),
44//!                     default: Some("ubuntu:24.04".into()),
45//!                 }),
46//!                 PreFromInstruction::Label(Label {
47//!                     key: "prefrom.label.example".into(),
48//!                     value: "true".into(),
49//!                 }),
50//!             ],
51//!
52//!             from: From {
53//!                 platform: None,
54//!                 image: "$BASE_IMAGE".into(),
55//!                 alias: None,
56//!             },
57//!
58//!             // ── Instructions ─────────────────────────────────────────
59//!             instructions: vec![
60//!                 // MAINTAINER (legacy)
61//!                 Instruction::Maintainer(Maintainer {
62//!                     name: "Example Maintainer <maintainer@example.com>".into(),
63//!                 }),
64//!                 // ARG
65//!                 Instruction::Arg(Arg {
66//!                     name: "BUILD_MODE".into(),
67//!                     default: Some("release".into()),
68//!                 }),
69//!                 // ENV
70//!                 Instruction::Env(Env {
71//!                     key: "RUST_LOG".into(),
72//!                     value: "info".into(),
73//!                 }),
74//!                 // WORKDIR
75//!                 Instruction::Workdir(WorkDir {
76//!                     path: "/app".into(),
77//!                 }),
78//!                 // USER (name)
79//!                 Instruction::User(User::ByName {
80//!                     name: "root".into(),
81//!                     group: None,
82//!                 }),
83//!                 // ADD
84//!                 Instruction::Add(Add {
85//!                     opts: vec![AddOpt::Chown {
86//!                         user: "0".into(),
87//!                         group: Some("0".into()),
88//!                     }],
89//!                     src: vec!["./archive.tar.gz".into()],
90//!                     dest: "/app/".into(),
91//!                 }),
92//!                 // COPY
93//!                 Instruction::Copy(Copy {
94//!                     opts: vec![CopyOpt::Chown {
95//!                         user: "0".into(),
96//!                         group: Some("0".into()),
97//!                     }],
98//!                     src: vec!["./src".into()],
99//!                     dest: "/app/src".into(),
100//!                 }),
101//!                 // EXPOSE
102//!                 Instruction::Expose(Expose {
103//!                     port: 8080,
104//!                     protocol: Some(ExposeProtocol::tcp),
105//!                 }),
106//!                 Instruction::Expose(Expose {
107//!                     port: 9090,
108//!                     protocol: Some(ExposeProtocol::udp),
109//!                 }),
110//!                 // LABEL
111//!                 Instruction::Label(Label {
112//!                     key: "app.name".into(),
113//!                     value: "example".into(),
114//!                 }),
115//!                 // VOLUME
116//!                 Instruction::Volume(Volume {
117//!                     volumes: vec!["/var/lib/appdata".into()],
118//!                 }),
119//!                 // SHELL
120//!                 Instruction::Shell(Shell {
121//!                     argv: vec!["/bin/bash".into(), "-c".into()],
122//!                 }),
123//!                 // HEALTHCHECK
124//!                 Instruction::HealthCheck(HealthCheck::Some {
125//!                     opts: vec![
126//!                         HealthCheckOpt::Interval("30s".into()),
127//!                         HealthCheckOpt::Timeout("3s".into()),
128//!                         HealthCheckOpt::Retries(3),
129//!                     ],
130//!                     cmd: Cmd {
131//!                         argv: vec![
132//!                             "curl".into(),
133//!                             "-f".into(),
134//!                             "http://localhost:8080/health".into(),
135//!                             "||".into(),
136//!                             "exit".into(),
137//!                             "1".into(),
138//!                         ],
139//!                     },
140//!                 }),
141//!                 // STOPSIGNAL
142//!                 Instruction::StopSignal(StopSignal::ByName("SIGTERM".into())),
143//!                 // ONBUILD
144//!                 Instruction::OnBuild(OnBuild {
145//!                     instruction: Box::new(Instruction::Run(Run {
146//!                         argv: vec!["echo".into(), "onbuild triggered".into()],
147//!                         mounts: vec![],
148//!                         network: None,
149//!                         security: None,
150//!                     })),
151//!                 }),
152//!                 // RUN (all mount types)
153//!                 Instruction::Run(Run {
154//!                     argv: vec![
155//!                         "bash".into(),
156//!                         "-c".into(),
157//!                         "echo building && make all".into(),
158//!                     ],
159//!                     mounts: vec![
160//!                         RunMount::Bind(RunMountBind {
161//!                             target: "/mnt/bind".into(),
162//!                             opts: vec![RunMountBindOpts::ReadWrite],
163//!                         }),
164//!                         RunMount::Cache(RunMountCache {
165//!                             target: "/mnt/cache".into(),
166//!                             opts: vec![
167//!                                 RunMountCacheOpts::Id("build-cache".into()),
168//!                                 RunMountCacheOpts::Sharing(RunSharing::Shared),
169//!                                 RunMountCacheOpts::Uid(1000),
170//!                                 RunMountCacheOpts::Gid(1000),
171//!                                 RunMountCacheOpts::Mode("0o755".into()),
172//!                             ],
173//!                         }),
174//!                         RunMount::Ssh(RunMountSSH {
175//!                             target: None,
176//!                             opts: vec![
177//!                                 RunMountSSHOpts::Id("default".into()),
178//!                                 RunMountSSHOpts::Required,
179//!                             ],
180//!                         }),
181//!                         RunMount::Secret(RunMountSecret {
182//!                             target: Some("/run/secrets/mysecret".into()),
183//!                             opts: vec![
184//!                                 RunMountSecretOpts::Id("mysecret".into()),
185//!                                 RunMountSecretOpts::Required,
186//!                             ],
187//!                         }),
188//!                         RunMount::Tmpfs(RunMountTmpfs {
189//!                             target: "/mnt/tmpfs".into(),
190//!                             opts: vec![RunMountTmpfsOpts::Size("65536".into())],
191//!                         }),
192//!                     ],
193//!                     network: Some(RunNetwork::Host),
194//!                     security: Some(RunSecurity::Sandbox),
195//!                 }),
196//!                 // RUN (alternate flags)
197//!                 Instruction::Run(Run {
198//!                     argv: vec!["echo".into(), "isolated step".into()],
199//!                     mounts: vec![],
200//!                     network: Some(RunNetwork::None),
201//!                     security: Some(RunSecurity::Insecure),
202//!                 }),
203//!                 // CMD
204//!                 Instruction::Cmd(Cmd {
205//!                     argv: vec!["./app".into()],
206//!                 }),
207//!                 // ENTRYPOINT
208//!                 Instruction::Entrypoint(Entrypoint {
209//!                     argv: vec!["/app/entrypoint.sh".into()],
210//!                 }),
211//!             ],
212//!         }],
213//!     };
214//!
215//!     let dockerfile_built = format!("{dockerfile}");
216//!     assert_eq!(
217//!         dockerfile_built,
218//!         r##"# Full instruction coverage test scenario
219//!
220//! ARG BASE_IMAGE=ubuntu:24.04
221//! LABEL prefrom.label.example=true
222//! FROM $BASE_IMAGE
223//! MAINTAINER Example Maintainer <maintainer@example.com>
224//! ARG BUILD_MODE=release
225//! ENV RUST_LOG=info
226//! WORKDIR /app
227//! USER root
228//! ADD --chown=0:0 "./archive.tar.gz" "/app/"
229//! COPY --chown=0:0 "./src" "/app/src"
230//! EXPOSE 8080/tcp
231//! EXPOSE 9090/udp
232//! LABEL app.name=example
233//! VOLUME ["/var/lib/appdata"]
234//! SHELL ["/bin/bash", "-c"]
235//! HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD ["curl", "-f", "http://localhost:8080/health", "||", "exit", "1"]
236//! STOPSIGNAL SIGTERM
237//! ONBUILD RUN ["echo", "onbuild triggered"]
238//! RUN --mount=type=bind,target=/mnt/bind,readwrite=true --mount=type=cache,target=/mnt/cache,id=build-cache,sharing=shared,uid=1000,gid=1000,mode=0o755 --mount=type=ssh,id=default,required=true --mount=type=secret,source=/run/secrets/mysecret,id=mysecret,required=true --mount=type=tmpfs,target=/mnt/tmpfs,size=65536 --network=host --security=sandbox ["bash", "-c", "echo building && make all"]
239//! RUN --network=none --security=insecure ["echo", "isolated step"]
240//! CMD ["./app"]
241//! ENTRYPOINT ["/app/entrypoint.sh"]
242//! "##
243//!     );
244//! }
245//! ```
246
247#![no_std]
248
249extern crate alloc;
250
251use alloc::string::String;
252use alloc::vec::Vec;
253use core::fmt::{Display, Formatter};
254
255mod from;
256mod instruction;
257
258pub use from::From;
259pub use instruction::*;
260
261#[derive(Clone, Debug)]
262pub struct Dockerfile {
263    pub config: Option<DockerfileConfig>,
264    pub comment: Option<String>,
265    pub stages: Vec<Stage>,
266}
267impl Display for Dockerfile {
268    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
269        match &self.config {
270            Some(config) => writeln!(f, "{config}")?,
271            None => {}
272        }
273
274        match &self.comment {
275            Some(comment) => {
276                for line in comment.lines() {
277                    writeln!(f, "# {line}")?;
278                }
279                writeln!(f, "")?;
280            }
281            None => {}
282        }
283
284        for (e, i) in self.stages.iter().enumerate() {
285            if e != 0 {
286                write!(f, "\n")?;
287            }
288            write!(f, "{i}")?;
289        }
290
291        Ok(())
292    }
293}
294
295#[derive(Clone, Debug)]
296pub struct DockerfileConfig {
297    pub syntax: Option<String>,
298    pub escape: Option<char>,
299    pub check_skips: Option<CheckSkips>,
300    pub check_error: Option<bool>,
301}
302impl Display for DockerfileConfig {
303    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
304        match &self.syntax {
305            Some(syntax) => writeln!(f, "# syntax={syntax}")?,
306            None => {}
307        }
308        match &self.escape {
309            Some(escape) => writeln!(f, "# escape={escape}")?,
310            None => {}
311        }
312        match &self.check_skips {
313            Some(check_skips) => {
314                write!(f, "# check=skip={check_skips}")?;
315
316                match &self.check_error {
317                    Some(check_error) => write!(f, ";error={check_error}\n")?,
318                    None => write!(f, "\n")?,
319                }
320            }
321            None => match &self.check_error {
322                Some(check_error) => writeln!(f, "# check=error={check_error}")?,
323                None => {}
324            },
325        }
326
327        Ok(())
328    }
329}
330
331#[derive(Clone, Debug)]
332pub enum CheckSkips {
333    All,
334    Some(Vec<String>),
335}
336impl Display for CheckSkips {
337    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
338        match self {
339            Self::All => write!(f, "all"),
340            Self::Some(e) => {
341                for (i, check) in e.iter().enumerate() {
342                    if i != 0 {
343                        write!(f, ",")?;
344                    }
345                    write!(f, "{check}")?;
346                }
347                Ok(())
348            }
349        }
350    }
351}
352
353/// The Stage is the unit which sits above the Instruction. It contains a vector of instructions to be placed before and after the FROM command
354#[derive(Clone, Debug)]
355pub struct Stage {
356    pub pre_from_instructions: Vec<PreFromInstruction>,
357    pub from: From,
358    pub instructions: Vec<Instruction>,
359}
360impl Display for Stage {
361    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
362        for i in &self.pre_from_instructions {
363            writeln!(f, "{i}")?;
364        }
365        writeln!(f, "{}", self.from)?;
366        for i in &self.instructions {
367            writeln!(f, "{i}")?;
368        }
369
370        Ok(())
371    }
372}