dockerfile_rs/
builder.rs

1use crate::{
2    Add, Arg, Cmd, Comment, Copy, EntryPoint, Env, Expose, From, HealthCheck, Label, Maintainer,
3    OnBuild, Run, Shell, StopSignal, StorageInstruction, User, Volume, WorkDir,
4};
5use std::fmt::{self, Display};
6
7/// `Dockerfile` generator
8/// # Example
9/// ```rust,no_run
10/// # use std::io::Result;
11/// # fn main() -> Result<()> {
12/// use std::{io::Write, fs::File};
13/// use dockerfile_rs::{DockerFile, Copy, FROM};
14///
15/// let docker_file = DockerFile::from(FROM!(nginx:latest))
16///     .comment("open port for server")
17///     .expose(80)
18///     .copy(Copy {
19///         src: ".".to_string(),
20///         dst: ".".to_string(),
21///         from: None,
22///         chown: None,
23///     })
24///     .cmd(vec!["echo", "Hello from container!"]);
25///
26/// // write into file
27/// let mut file = File::create("nginx.Dockerfile")?;
28/// write!(&mut file, "{}", docker_file)?;
29/// # Ok(())
30/// # }
31/// ```
32pub struct DockerFile {
33    from: From,
34    maintainer: Option<Maintainer>,
35    entry_point: Option<EntryPoint>,
36    cmd: Option<Cmd>,
37    instructions: Vec<Box<StorageInstruction>>,
38    on_builds: Vec<OnBuild>,
39}
40
41impl DockerFile {
42    pub fn from(from: From) -> Self {
43        Self {
44            from,
45            maintainer: None,
46            entry_point: None,
47            cmd: None,
48            instructions: Vec::new(),
49            on_builds: Vec::new(),
50        }
51    }
52
53    /// Can be defined just once, only last function call will have effect
54    /// Deprecated, use [`label`] with `maintainer` key instead
55    ///
56    /// [`label`]: struct.DockerFile.html#method.label
57    pub fn maintainer<T: Into<Maintainer> + 'static>(mut self, maintainer: T) -> Self {
58        self.maintainer = Some(maintainer.into());
59        self
60    }
61
62    fn instruction<T: StorageInstruction + 'static>(mut self, t: T) -> Self {
63        self.instructions.push(Box::new(t));
64        self
65    }
66
67    /// Can be defined just once, only last function call will have effect
68    pub fn entry_point<T: Into<EntryPoint> + 'static>(mut self, entry_point: T) -> Self {
69        self.entry_point = Some(entry_point.into());
70        self
71    }
72
73    /// Can be defined just once, only last function call will have effect
74    pub fn cmd<T: Into<Cmd> + 'static>(mut self, cmd: T) -> Self {
75        self.cmd = Some(cmd.into());
76        self
77    }
78
79    pub fn run<T: Into<Run> + 'static>(self, run: T) -> Self {
80        self.instruction(run.into())
81    }
82
83    pub fn label<T: Into<Label> + 'static>(self, label: T) -> Self {
84        self.instruction(label.into())
85    }
86
87    pub fn expose<T: Into<Expose> + 'static>(self, expose: T) -> Self {
88        self.instruction(expose.into())
89    }
90
91    pub fn env<T: Into<Env> + 'static>(self, env: T) -> Self {
92        self.instruction(env.into())
93    }
94
95    #[allow(clippy::should_implement_trait)]
96    pub fn add(self, add: Add) -> Self {
97        self.instruction(add)
98    }
99
100    pub fn copy(self, copy: Copy) -> Self {
101        self.instruction(copy)
102    }
103
104    pub fn volume<T: Into<Volume> + 'static>(self, volume: T) -> Self {
105        self.instruction(volume.into())
106    }
107
108    pub fn user(self, user: User) -> Self {
109        self.instruction(user)
110    }
111
112    pub fn work_dir<T: Into<WorkDir> + 'static>(self, work_dir: T) -> Self {
113        self.instruction(work_dir.into())
114    }
115
116    pub fn arg<T: Into<Arg> + 'static>(self, arg: T) -> Self {
117        self.instruction(arg.into())
118    }
119
120    pub fn stop_signal<T: Into<StopSignal> + 'static>(self, stop_signal: T) -> Self {
121        self.instruction(stop_signal.into())
122    }
123
124    pub fn health_check(self, health_check: HealthCheck) -> Self {
125        self.instruction(health_check)
126    }
127
128    pub fn shell<T: Into<Shell> + 'static>(self, shell: T) -> Self {
129        self.instruction(shell.into())
130    }
131
132    pub fn comment<T: Into<Comment> + 'static>(self, comment: T) -> Self {
133        self.instruction(comment.into())
134    }
135
136    pub fn on_build<T: Into<OnBuild> + 'static>(mut self, on_build: T) -> Self {
137        self.on_builds.push(on_build.into());
138        self
139    }
140}
141
142impl Display for DockerFile {
143    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
144        writeln!(f, "{}", self.from)?;
145
146        if let Some(maintainer) = &self.maintainer {
147            writeln!(f)?;
148            writeln!(f, "{}", maintainer)?;
149        }
150
151        if !self.instructions.is_empty() {
152            writeln!(f)?;
153            for instruction in &self.instructions {
154                writeln!(f, "{}", instruction)?;
155            }
156        }
157
158        if !self.on_builds.is_empty() {
159            writeln!(f)?;
160            for on_build in &self.on_builds {
161                writeln!(f, "{}", on_build)?;
162            }
163        }
164
165        match (&self.entry_point, &self.cmd) {
166            (Some(entry_point), Some(cmd)) => {
167                writeln!(f)?;
168                writeln!(f, "{}", entry_point)?;
169                writeln!(f, "{}", cmd)?;
170            }
171            (Some(entry_point), None) => {
172                writeln!(f)?;
173                writeln!(f, "{}", entry_point)?;
174            }
175            (None, Some(cmd)) => {
176                writeln!(f)?;
177                writeln!(f, "{}", cmd)?;
178            }
179            (None, None) => {}
180        }
181
182        Ok(())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::Tag;
190
191    #[test]
192    fn builder() {
193        let content = DockerFile::from(From {
194            image: String::from("rust"),
195            tag_or_digest: Some(Tag("latest".to_string())),
196            name: None,
197        })
198        .maintainer("lead rustacean")
199        .comment("Hello, world!")
200        .run(vec!["/bin/bash", "-c", "echo"])
201        .label(("key", "value"))
202        .expose(80)
203        .env(("RUST", "1.0.0"))
204        .add(Add {
205            src: "/var/run".to_string(),
206            dst: "/home".to_string(),
207            chown: None,
208        })
209        .copy(Copy {
210            src: "/var/run".to_string(),
211            dst: "/home".to_string(),
212            from: None,
213            chown: None,
214        })
215        .volume(vec!["/var/run", "/var/www"])
216        .user(User {
217            user: "rustacean".to_string(),
218            group: None,
219        })
220        .work_dir("/home/rustacean")
221        .arg(("build", "yes"))
222        .stop_signal("SIGKILL")
223        .health_check(HealthCheck::None)
224        .shell(vec!["/bin/bash", "-c"])
225        .on_build(OnBuild::from(Cmd::from(vec![
226            "echo",
227            "This is the ONBUILD command",
228        ])))
229        .entry_point(vec!["cargo", "check"])
230        .cmd(vec!["echo", "Hi!"])
231        .to_string();
232        assert_eq!(
233            content,
234            r#"FROM rust:latest
235
236MAINTAINER lead rustacean
237
238# Hello, world!
239RUN ["/bin/bash", "-c", "echo"]
240LABEL key="value"
241EXPOSE 80
242ENV RUST="1.0.0"
243ADD "/var/run" "/home"
244COPY "/var/run" "/home"
245VOLUME ["/var/run", "/var/www"]
246USER rustacean
247WORKDIR "/home/rustacean"
248ARG build="yes"
249STOPSIGNAL SIGKILL
250HEALTHCHECK NONE
251SHELL ["/bin/bash", "-c"]
252
253ONBUILD CMD ["echo", "This is the ONBUILD command"]
254
255ENTRYPOINT ["cargo", "check"]
256CMD ["echo", "Hi!"]
257"#
258        );
259    }
260}