cargo_pack_docker/
docker.rs1use crate::error::*;
2use cargo_pack::CargoPack;
3use copy_dir;
4use failure::format_err;
5use handlebars::{no_escape, Handlebars};
6use log::debug;
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::fs::File;
10use std::io::{BufWriter, Write};
11use std::path::Path;
12use std::process::Command;
13use tempdir::TempDir;
14
15#[derive(Deserialize, Debug)]
16#[serde(rename_all = "kebab-case")]
17pub struct PackDocker {
18 entrypoint: Option<Vec<String>>,
19 cmd: Option<Vec<String>>,
20 base_image: String,
21 bin: Option<String>,
22 inject: Option<String>,
23 tag: Option<String>,
24}
25
26#[derive(Deserialize, Debug)]
27pub struct PackDockerConfig {
28 docker: Vec<PackDocker>,
29}
30
31pub struct Docker {
33 config: PackDockerConfig,
34 pack: CargoPack,
35 tags: Vec<String>,
36 is_release: bool,
37}
38
39#[derive(Deserialize, Serialize, Debug)]
40pub struct DockerfileConfig {
41 entrypoint: Option<String>,
42 cmd: Option<String>,
43 baseimage: String,
44 files: Vec<String>,
45 bin: String,
46 inject: String,
47}
48
49impl PackDocker {
50 fn base_name(&self, docker: &Docker) -> Result<String> {
51 self.tag(docker).map(|name| {
52 name.rsplitn(2, ':')
53 .last()
54 .unwrap()
56 .to_string()
57 })
58 }
59
60 fn bin_name<'a>(&'a self, docker: &'a Docker) -> Result<&'a str> {
61 let bins = docker
62 .pack
63 .package()?
64 .targets
65 .iter()
66 .filter(|t| t.kind.contains(&"bin".to_string()))
67 .map(|t| &t.name)
68 .collect::<Vec<_>>();
69
70 if let Some(name) = self.bin.as_ref() {
71 if bins.contains(&name) {
72 return Ok(name);
73 } else {
74 return Err(Error::BinNotFound(name.clone()).into());
75 }
76 }
77 match bins.len() {
78 0 => Err(Error::NoBins.into()),
79 1 => Ok(bins.get(0).unwrap()),
80 _ => Err(Error::AmbiguousBinName(bins.into_iter().map(Into::into).collect()).into()),
81 }
82 }
83
84 fn tag(&self, docker: &Docker) -> Result<String> {
85 if let Some(ref tag) = self.tag {
86 Ok(tag.to_string())
87 } else {
88 let bin_name = self.bin_name(docker)?;
89 let package = docker.pack.package().unwrap();
90 let version = if docker.is_release {
91 package.version.to_string()
92 } else {
93 "latest".to_string()
94 };
95 Ok(format!("{}:{}", bin_name, version))
96 }
97 }
98}
99
100impl<'cfg> Docker {
101 pub fn new(
102 config: PackDockerConfig,
103 pack: CargoPack,
104 tags: Vec<String>,
105 is_release: bool,
106 ) -> Self {
107 Docker {
108 config,
109 pack,
110 tags,
111 is_release,
112 }
113 }
114
115 pub fn pack(&self) -> Result<()> {
116 debug!("tags: {:?}, config: {:?}", self.tags, self.config);
117 debug!("workspace: {:?}", self.pack.package());
118 debug!("preparing");
119 for pack_docker in self.targets() {
120 let tmpdir = self.prepare(pack_docker)?;
121 debug!("building a image");
122 self.build(tmpdir, pack_docker)?;
123 }
124 Ok(())
125 }
126
127 fn prepare(&self, pack_docker: &PackDocker) -> Result<TempDir> {
128 let tmp = TempDir::new("cargo-pack-docker")?;
129 debug!("created: {:?}", tmp);
130 self.copy_files(&tmp)?;
131 let bin = self.add_bin(&tmp, pack_docker)?;
132 let data = DockerfileConfig {
133 entrypoint: pack_docker.entrypoint.as_ref().map(|e| {
134 e.iter()
135 .map(|s| format!("\"{}\"", s))
136 .collect::<Vec<_>>()
137 .join(", ")
138 }),
139 cmd: pack_docker.cmd.as_ref().map(|c| {
140 c.iter()
141 .map(|s| format!("\"{}\"", s))
142 .collect::<Vec<_>>()
143 .join(", ")
144 }),
145 baseimage: pack_docker.base_image.clone(),
146 files: self.pack.files().into(),
147 bin: bin,
148 inject: pack_docker
149 .inject
150 .as_ref()
151 .map(|s| s.as_ref())
152 .unwrap_or("")
153 .to_string(),
154 };
155 self.gen_dockerfile(&tmp, &data)?;
156 Ok(tmp)
157 }
158
159 fn build<P: AsRef<Path>>(&self, path: P, pack_docker: &PackDocker) -> Result<()> {
160 let image_tag = pack_docker.tag(self)?;
161 let dockerbin = ::which::which("docker")?;
163 let status = Command::new(dockerbin)
164 .current_dir(&path)
165 .arg("build")
166 .arg(path.as_ref().to_str().unwrap())
167 .args(&["-t", image_tag.as_str()])
168 .spawn()?
169 .wait()?;
170
171 if status.success() {
172 Ok(())
173 } else {
174 Err(format_err!("docker command faild"))
175 }
176 }
177
178 fn copy_files<P: AsRef<Path>>(&self, path: P) -> Result<()> {
179 for file in self.pack.files() {
180 let to = path.as_ref().join(file);
181 debug!("copying file: from {:?} to {:?}", file, to);
182 copy_dir::copy_dir(file, to)?;
183 }
184 Ok(())
185 }
186
187 fn add_bin<P: AsRef<Path>>(&self, path: P, pack_docker: &PackDocker) -> Result<String> {
188 let name = pack_docker.bin_name(self)?;
189 let from = if self.is_release {
190 self.pack
191 .metadata()
192 .target_directory
193 .join("release")
194 .join(&name)
195 } else {
196 self.pack
197 .metadata()
198 .target_directory
199 .join("debug")
200 .join(&name)
201 };
202
203 let to = path.as_ref().join(&name);
204 debug!("copying file: from {:?} to {:?}", from, to);
205 fs::copy(from, to)?;
206 Ok(name.into())
207 }
208
209 fn targets(&self) -> Vec<&PackDocker> {
210 if self.tags.len() == 0 {
211 self.config.docker.iter().collect()
212 } else {
213 self.config
215 .docker
216 .iter()
217 .filter(|p| {
218 p.base_name(&self)
219 .map(|name| self.tags.contains(&name))
220 .unwrap_or(false)
221 })
222 .collect()
223 }
224 }
225
226 fn gen_dockerfile<P: AsRef<Path>>(&self, path: P, data: &DockerfileConfig) -> Result<()> {
227 let dockerfile = path.as_ref().join("Dockerfile");
228 debug!("generating {:?}", dockerfile);
229 let file = File::create(dockerfile)?;
230 debug!("Dockerfile creation succeeded.");
231 debug!("templating with {:?}", data);
232 let mut buf = BufWriter::new(file);
233 let template = r#"
234FROM {{ baseimage }}
235
236RUN mkdir -p /opt/app/bin
237{{#each files as |file| ~}}
238 COPY {{ file }} /opt/app
239{{/each~}}
240COPY {{bin}} /opt/app/bin
241WORKDIR /opt/app
242
243{{inject}}
244
245{{#if entrypoint ~}}
246ENTRYPOINT [{{entrypoint}}]
247{{else ~}}
248ENTRYPOINT ["/opt/app/bin/{{bin}}"]
249{{/if ~}}
250{{#if cmd ~}}
251CMD [{{cmd}}]
252{{/if}}
253"#;
254 let mut handlebars = Handlebars::new();
255
256 handlebars.register_escape_fn(no_escape);
257 handlebars
258 .register_template_string("dockerfile", template)
259 .expect("internal error: illegal template");
260
261 handlebars
262 .render_to_write("dockerfile", data, &mut buf)
263 .unwrap();
264 debug!("templating done");
265 let _ = buf.flush()?;
266 debug!(
267 "content:{}",
268 fs::read_to_string(path.as_ref().join("Dockerfile"))?
269 );
270
271 Ok(())
272 }
273}
274