soma/
docker.rs

1use std::collections::HashMap;
2
3use bollard::container::{
4    APIContainers, Config, CreateContainerOptions, HostConfig, ListContainersOptions, PortBinding,
5    PruneContainersOptions, RemoveContainerOptions, StartContainerOptions, StopContainerOptions,
6};
7use bollard::image::{
8    APIImages, BuildImageOptions, ListImagesOptions, PruneImagesOptions, RemoveImageOptions,
9};
10use bollard::Docker;
11use failure::Error;
12use futures::{Future, Stream};
13use hyper::client::connect::Connect;
14
15use crate::prelude::*;
16use crate::problem::Problem;
17use crate::{Environment, Printer, VERSION};
18
19const LABEL_KEY_VERSION: &str = "soma.version";
20const LABEL_KEY_USERNAME: &str = "soma.username";
21const LABEL_KEY_REPOSITORY: &str = "soma.repository";
22const LABEL_KEY_PROBLEM: &str = "soma.problem";
23
24type DockerLabel<'a> = HashMap<&'a str, &'a str>;
25type SomaFilter = HashMap<String, Vec<String>>;
26
27#[cfg(windows)]
28pub fn connect_default() -> SomaResult<Docker<impl Connect>> {
29    Docker::connect_with_named_pipe("npipe:////./pipe/docker_engine", 600)
30}
31
32#[cfg(unix)]
33pub fn connect_default() -> SomaResult<Docker<impl Connect>> {
34    Docker::connect_with_unix("unix:///var/run/docker.sock", 600)
35}
36
37#[derive(Clone, Copy, Debug)]
38pub enum VersionStatus {
39    Normal,
40    VersionMismatch,
41    NoVersionFound,
42}
43
44#[derive(Debug)]
45pub struct SomaImage {
46    repo_name: String,
47    prob_name: String,
48    image: APIImages,
49    status: VersionStatus,
50}
51
52impl SomaImage {
53    pub fn new(
54        repo_name: String,
55        prob_name: String,
56        image: APIImages,
57        status: VersionStatus,
58    ) -> SomaImage {
59        SomaImage {
60            repo_name,
61            prob_name,
62            image,
63            status,
64        }
65    }
66
67    pub fn repo_name(&self) -> &String {
68        &self.repo_name
69    }
70
71    pub fn prob_name(&self) -> &String {
72        &self.prob_name
73    }
74
75    pub fn image(&self) -> &APIImages {
76        &self.image
77    }
78
79    pub fn status(&self) -> VersionStatus {
80        self.status
81    }
82}
83
84struct SomaFilterBuilder {
85    label_filter: Vec<String>,
86}
87
88impl SomaFilterBuilder {
89    fn new() -> SomaFilterBuilder {
90        SomaFilterBuilder {
91            label_filter: vec![],
92        }
93    }
94
95    fn append_filter(mut self, key: String, value: String) -> SomaFilterBuilder {
96        self.label_filter.push(format!("{}={}", key, value));
97        self
98    }
99
100    pub fn append_user(self, username: &str) -> SomaFilterBuilder {
101        self.append_filter(LABEL_KEY_USERNAME.to_owned(), username.to_owned())
102    }
103
104    pub fn append_prob(self, problem: &Problem) -> SomaFilterBuilder {
105        self.append_filter(
106            LABEL_KEY_REPOSITORY.to_owned(),
107            problem.repo_name().to_string(),
108        )
109        .append_filter(
110            LABEL_KEY_PROBLEM.to_owned(),
111            problem.prob_name().to_string(),
112        )
113    }
114
115    pub fn build(self) -> SomaFilter {
116        let mut filter = SomaFilter::new();
117        filter.insert("label".to_owned(), self.label_filter);
118        filter
119    }
120}
121
122pub fn image_exists(images: &[SomaImage], image_name: &str) -> bool {
123    images.iter().any(|image| match &image.image().repo_tags {
124        Some(tags) => tags
125            .iter()
126            .any(|tag| tag.starts_with(format!("{}:", image_name).as_str())),
127        None => false,
128    })
129}
130
131pub fn image_from_repo_exists(images: &[SomaImage], repo_name: &str) -> bool {
132    images.iter().any(|image| image.repo_name() == repo_name)
133}
134
135pub fn image_from_prob_exists(images: &[SomaImage], problem: &Problem) -> bool {
136    images.iter().any(|image| {
137        image.repo_name() == problem.repo_name() && image.prob_name() == problem.prob_name()
138    })
139}
140
141pub fn image_from_repo_and_prob_exists(
142    images: &[SomaImage],
143    repo_name: &str,
144    prob_name: &str,
145) -> bool {
146    images
147        .iter()
148        .any(|image| image.repo_name() == repo_name && image.prob_name() == prob_name)
149}
150
151#[derive(Debug)]
152pub struct SomaContainer {
153    repo_name: String,
154    prob_name: String,
155    container: APIContainers,
156    status: VersionStatus,
157}
158
159impl SomaContainer {
160    pub fn new(
161        repo_name: String,
162        prob_name: String,
163        container: APIContainers,
164        status: VersionStatus,
165    ) -> SomaContainer {
166        SomaContainer {
167            repo_name,
168            prob_name,
169            container,
170            status,
171        }
172    }
173
174    pub fn repo_name(&self) -> &String {
175        &self.repo_name
176    }
177
178    pub fn prob_name(&self) -> &String {
179        &self.prob_name
180    }
181
182    pub fn container(&self) -> &APIContainers {
183        &self.container
184    }
185
186    pub fn status(&self) -> VersionStatus {
187        self.status
188    }
189}
190
191pub fn container_exists(containers: &[SomaContainer], container_id: &str) -> bool {
192    containers
193        .iter()
194        .any(|container| container.container().id == container_id)
195}
196
197pub fn container_from_prob_exists(containers: &[SomaContainer], problem: &Problem) -> bool {
198    containers.iter().any(|container| {
199        container.repo_name() == problem.repo_name() && container.prob_name() == problem.prob_name()
200    })
201}
202
203pub fn container_from_prob_running(containers: &[SomaContainer], problem: &Problem) -> bool {
204    containers.iter().any(|container| {
205        container.repo_name() == problem.repo_name()
206            && container.prob_name() == problem.prob_name()
207            && container.container().state == "running"
208    })
209}
210
211pub fn containers_from_prob(
212    containers: Vec<SomaContainer>,
213    problem: &Problem,
214) -> Vec<SomaContainer> {
215    containers
216        .into_iter()
217        .filter(|container| {
218            container.repo_name() == problem.repo_name()
219                && container.prob_name() == problem.prob_name()
220        })
221        .collect()
222}
223
224pub fn list_containers(
225    env: &Environment<impl Connect, impl Printer>,
226) -> impl Future<Item = Vec<SomaContainer>, Error = Error> {
227    let soma_filter = SomaFilterBuilder::new().append_user(env.username()).build();
228    env.docker
229        .list_containers(Some(ListContainersOptions::<String> {
230            all: true,
231            filters: soma_filter,
232            ..Default::default()
233        }))
234        .map(move |containers| -> Vec<SomaContainer> {
235            containers
236                .into_iter()
237                .filter_map(|container| {
238                    let labels = &container.labels;
239                    if let (Some(repo_name), Some(prob_name)) = (
240                        labels.get(LABEL_KEY_REPOSITORY),
241                        labels.get(LABEL_KEY_PROBLEM),
242                    ) {
243                        let status = match labels.get(LABEL_KEY_VERSION) {
244                            Some(version) if version == VERSION => VersionStatus::Normal,
245                            Some(_) => VersionStatus::VersionMismatch,
246                            None => VersionStatus::NoVersionFound,
247                        };
248                        Some(SomaContainer::new(
249                            repo_name.to_owned(),
250                            prob_name.to_owned(),
251                            container,
252                            status,
253                        ))
254                    } else {
255                        None
256                    }
257                })
258                .collect()
259        })
260}
261
262pub fn list_images(
263    env: &Environment<impl Connect, impl Printer>,
264) -> impl Future<Item = Vec<SomaImage>, Error = Error> {
265    let soma_filter = SomaFilterBuilder::new().append_user(env.username()).build();
266    env.docker
267        .list_images(Some(ListImagesOptions::<String> {
268            filters: soma_filter,
269            ..Default::default()
270        }))
271        .map(move |images| -> Vec<SomaImage> {
272            images
273                .into_iter()
274                .filter_map(|image| {
275                    // soma_filter guarantees label existence
276                    let labels = image.labels.as_ref().unwrap();
277                    if let (Some(repo_name), Some(prob_name)) = (
278                        labels.get(LABEL_KEY_REPOSITORY),
279                        labels.get(LABEL_KEY_PROBLEM),
280                    ) {
281                        let status = match labels.get(LABEL_KEY_VERSION) {
282                            Some(version) if version == VERSION => VersionStatus::Normal,
283                            Some(_) => VersionStatus::VersionMismatch,
284                            None => VersionStatus::NoVersionFound,
285                        };
286                        Some(SomaImage::new(
287                            repo_name.to_owned(),
288                            prob_name.to_owned(),
289                            image,
290                            status,
291                        ))
292                    } else {
293                        None
294                    }
295                })
296                .collect()
297        })
298}
299
300pub fn build<'a>(
301    env: &'a Environment<impl Connect, impl Printer>,
302    labels: DockerLabel<'a>,
303    image_name: &'a str,
304    build_context: Vec<u8>,
305) -> impl Future<Item = (), Error = Error> + 'a {
306    let build_options = BuildImageOptions {
307        t: image_name,
308        pull: true,
309        forcerm: true,
310        labels,
311        ..Default::default()
312    };
313
314    env.docker
315        .build_image(build_options, None, Some(build_context.into()))
316        .fold((), move |_, build_image_result| {
317            use bollard::image::BuildImageResults::*;
318            match build_image_result {
319                BuildImageStream { stream } => {
320                    let message = stream.trim();
321                    if message != "" {
322                        env.printer().write_line(message)
323                    }
324                    Ok(())
325                }
326                BuildImageError { error, .. } => {
327                    env.printer().write_line(error.trim());
328                    Err(SomaError::DockerBuildFailed)
329                }
330                _ => Ok(()),
331            }
332        })
333}
334
335pub fn docker_labels<'a>(
336    env: &'a Environment<impl Connect, impl Printer>,
337    problem: &'a Problem,
338) -> DockerLabel<'a> {
339    vec![
340        (LABEL_KEY_VERSION, VERSION),
341        (LABEL_KEY_USERNAME, env.username()),
342        (LABEL_KEY_REPOSITORY, problem.repo_name()),
343        (LABEL_KEY_PROBLEM, problem.prob_name()),
344    ]
345    .into_iter()
346    .collect()
347}
348
349pub fn create<'a>(
350    env: &'a Environment<impl Connect, impl Printer>,
351    labels: DockerLabel<'a>,
352    image_name: &'a str,
353    port_str: &'a str,
354) -> impl Future<Item = String, Error = Error> + 'a {
355    let mut port_bindings = HashMap::new();
356    port_bindings.insert(
357        "1337/tcp",
358        vec![PortBinding {
359            host_ip: "",
360            host_port: port_str,
361        }],
362    );
363
364    let host_config = HostConfig {
365        port_bindings: Some(port_bindings),
366        ..Default::default()
367    };
368
369    env.docker
370        .create_container(
371            None::<CreateContainerOptions<String>>,
372            Config {
373                image: Some(image_name),
374                labels: Some(labels),
375                host_config: Some(host_config),
376                ..Default::default()
377            },
378        )
379        .map(|container_results| container_results.id)
380}
381
382pub fn remove_image(
383    env: &Environment<impl Connect, impl Printer>,
384    image_name: &str,
385) -> impl Future<Item = (), Error = Error> {
386    env.docker
387        .remove_image(image_name, None::<RemoveImageOptions>, None)
388        .map(|_| ())
389}
390
391pub fn remove_container(
392    env: &Environment<impl Connect, impl Printer>,
393    container_id: &str,
394) -> impl Future<Item = (), Error = Error> {
395    env.docker
396        .remove_container(container_id, None::<RemoveContainerOptions>)
397}
398
399pub fn prune_images_from_prob(
400    env: &Environment<impl Connect, impl Printer>,
401    problem: &Problem,
402) -> impl Future<Item = (), Error = Error> {
403    let soma_filter = SomaFilterBuilder::new()
404        .append_user(env.username())
405        .append_prob(problem)
406        .build();
407    env.docker
408        .prune_images(Some(PruneImagesOptions {
409            filters: soma_filter,
410        }))
411        .map(|_| ())
412}
413
414pub fn prune_containers_from_prob(
415    env: &Environment<impl Connect, impl Printer>,
416    problem: &Problem,
417) -> impl Future<Item = (), Error = Error> {
418    let soma_filter = SomaFilterBuilder::new()
419        .append_user(env.username())
420        .append_prob(problem)
421        .build();
422    env.docker
423        .prune_containers(Some(PruneContainersOptions {
424            filters: soma_filter,
425        }))
426        .map(|_| ())
427}
428
429pub fn start(
430    env: &Environment<impl Connect, impl Printer>,
431    container_id: &str,
432) -> impl Future<Item = (), Error = Error> {
433    env.docker
434        .start_container(container_id, None::<StartContainerOptions<String>>)
435}
436
437pub fn stop(
438    env: &Environment<impl Connect, impl Printer>,
439    container_id: &str,
440) -> impl Future<Item = (), Error = Error> {
441    env.docker
442        .stop_container(container_id, None::<StopContainerOptions>)
443}