cargo_pack_docker/
docker.rs

1use 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
31// assuming single bin.
32pub 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                // should be safe but not confident
55                .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        // FIXME: take from user
162        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            // TODO: warn non existing tags
214            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// mktmpdir
275// cp files to tmpdir
276// output Dockerfile
277// docker build -f Dockerfile ./