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
7pub 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 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 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 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}