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}