soma/
ops.rs

1use std::fs;
2use std::path::Path;
3
4use flate2::write::GzEncoder;
5use flate2::Compression;
6use fs_extra::{dir, file};
7use futures::Future;
8use handlebars::Handlebars;
9use hyper::client::connect::Connect;
10use tempfile::tempdir;
11use tokio::runtime::current_thread::Runtime;
12
13use crate::docker;
14use crate::prelude::*;
15use crate::problem::configs::SolidBinaryConfig;
16use crate::problem::Problem;
17use crate::repository::backend;
18use crate::template::{HandleBarsExt, Templates};
19use crate::{Environment, Printer};
20
21pub fn add(
22    env: &mut Environment<impl Connect, impl Printer>,
23    repo_location: &str,
24    repo_name: Option<&str>,
25) -> SomaResult<()> {
26    let (resolved_repo_name, backend) = backend::location_to_backend(repo_location)?;
27    let repo_name = match repo_name {
28        Some(repo_name) => repo_name,
29        None => &resolved_repo_name,
30    };
31
32    env.repo_manager_mut().add_repo(repo_name, backend)?;
33
34    let mut repository = env.repo_manager().get_repo(repo_name)?;
35    repository.update(&[])?;
36
37    env.printer()
38        .write_line(&format!("Repository added: '{}'", repo_name));
39
40    Ok(())
41}
42
43pub fn fetch(
44    env: &Environment<impl Connect, impl Printer>,
45    prob_query: &str,
46    cwd: impl AsRef<Path>,
47) -> SomaResult<()> {
48    let problem = env.repo_manager().search_prob(prob_query)?;
49    let manifest = problem.load_manifest()?;
50
51    manifest
52        .public_files()
53        .into_iter()
54        .try_for_each(|public_file_path| {
55            let file_path = problem.path().join(public_file_path);
56            let file_name = file_path.file_name().ok_or(SomaError::FileNameNotFound)?;
57
58            env.printer()
59                .write_line(&format!("Fetching '{}'...", file_name.to_string_lossy()));
60            fs::copy(&file_path, cwd.as_ref().join(file_name))?;
61            Ok(())
62        })
63}
64
65pub fn build(
66    env: &Environment<impl Connect, impl Printer>,
67    prob_query: &str,
68    runtime: &mut Runtime,
69) -> SomaResult<()> {
70    let problem = env.repo_manager().search_prob(prob_query)?;
71
72    runtime.block_on(docker::prune_images_from_prob(&env, &problem))?;
73    build_image(&env, &problem, runtime)?;
74    env.printer().write_line(&format!(
75        "Built image for problem: '{}'",
76        problem.fully_qualified_name()
77    ));
78    Ok(())
79}
80
81fn construct_image_root(
82    image_root: impl AsRef<Path>,
83    problem_dir: impl AsRef<Path>,
84    binary_config: &SolidBinaryConfig,
85) -> SomaResult<()> {
86    let mut dir_copy_options = dir::CopyOptions::new();
87    dir_copy_options.copy_inside = true;
88    // Latter entry has higher priority
89    dir_copy_options.overwrite = true;
90    let mut file_copy_options = file::CopyOptions::new();
91    file_copy_options.overwrite = true;
92
93    for (local_path, target_path) in binary_config.path_maps() {
94        let local_path = problem_dir.as_ref().join(local_path);
95        let destination = image_root.as_ref().join(target_path.strip_prefix("/")?);
96        // TODO: more descriptive error
97        fs::create_dir_all(destination.parent().ok_or(SomaError::InvalidManifest)?)?;
98        if local_path.is_dir() {
99            if destination.exists() {
100                unimplemented!("Handling copy of nested or duplicate directory");
101            }
102            dir::copy(local_path, destination, &dir_copy_options)?;
103        } else if local_path.is_file() {
104            file::copy(local_path, destination, &file_copy_options)?;
105        } else {
106            Err(SomaError::FileUnreachable)?;
107        }
108    }
109    Ok(())
110}
111
112fn encode_context(path: impl AsRef<Path>) -> SomaResult<Vec<u8>> {
113    let compressor = GzEncoder::new(Vec::new(), Compression::default());
114    let mut tar = tar::Builder::new(compressor);
115    tar.append_dir_all("", path)?;
116    tar.finish()?;
117    let compressor = tar.into_inner()?;
118    Ok(compressor.finish()?)
119}
120
121fn build_image(
122    env: &Environment<impl Connect, impl Printer>,
123    problem: &Problem,
124    runtime: &mut Runtime,
125) -> SomaResult<()> {
126    let image_name = problem.docker_image_name(env.username());
127
128    env.printer().write_line("Preparing build context...");
129    let context = tempdir()?;
130    let context_path = context.path();
131
132    env.printer().write_line("Loading manifest...");
133    let manifest = problem.load_manifest()?.solidify()?;
134
135    env.printer().write_line("Constructing image root...");
136    let image_root = context_path.join("image-root");
137    let problem_dir = problem.path();
138    fs::create_dir(&image_root)?;
139    let binary_config = manifest.binary();
140    construct_image_root(image_root, problem_dir, binary_config)?;
141
142    env.printer().write_line("Rendering build files...");
143    fs::create_dir(context_path.join(".soma"))?;
144    Handlebars::new().render_templates(Templates::Binary, &manifest, context_path)?;
145
146    env.printer().write_line("Encoding build context...");
147    let build_context = encode_context(context_path)?;
148
149    context.close()?;
150    env.printer().write_line("Building image...");
151    let labels = docker::docker_labels(&env, &problem);
152    runtime.block_on(docker::build(&env, labels, &image_name, build_context))?;
153    Ok(())
154}
155
156pub fn run(
157    env: &Environment<impl Connect, impl Printer>,
158    prob_query: &str,
159    port: u32,
160    runtime: &mut Runtime,
161) -> SomaResult<String> {
162    let problem = env.repo_manager().search_prob(prob_query)?;
163    let image_name = problem.docker_image_name(env.username());
164    let port_str = &port.to_string();
165
166    let containers = runtime.block_on(docker::list_containers(&env))?;
167    if docker::container_from_prob_running(&containers, &problem) {
168        Err(SomaError::ProblemAlreadyRunning)?
169    }
170
171    runtime.block_on(docker::prune_containers_from_prob(&env, &problem))?;
172
173    let labels = docker::docker_labels(env, &problem);
174    let container_run =
175        docker::create(env, labels, &image_name, port_str).and_then(|container_name| {
176            env.printer().write_line("Starting container...");
177            docker::start(env, &container_name).map(|_| container_name)
178        });
179
180    env.printer().write_line(&format!(
181        "Creating container for problem: '{}'",
182        problem.fully_qualified_name()
183    ));
184    let container_name = runtime.block_on(container_run)?;
185    env.printer()
186        .write_line(&format!("Container started: '{}'", &container_name));
187
188    Ok(container_name)
189}
190
191pub fn remove(
192    env: &mut Environment<impl Connect, impl Printer>,
193    repo_name: &str,
194    runtime: &mut Runtime,
195) -> SomaResult<()> {
196    let image_list = runtime.block_on(docker::list_images(env))?;
197    if docker::image_from_repo_exists(&image_list, repo_name) {
198        Err(SomaError::RepositoryInUse)?;
199    }
200
201    env.repo_manager_mut().remove_repo(repo_name)?;
202    env.printer()
203        .write_line(&format!("Repository removed: '{}'", &repo_name));
204
205    Ok(())
206}
207
208pub fn clean(
209    env: &Environment<impl Connect, impl Printer>,
210    prob_query: &str,
211    runtime: &mut Runtime,
212) -> SomaResult<()> {
213    let problem = env.repo_manager().search_prob(prob_query)?;
214
215    let container_list = runtime.block_on(docker::list_containers(env))?;
216    if docker::container_from_prob_exists(&container_list, &problem) {
217        Err(SomaError::RepositoryInUse)?;
218    }
219
220    runtime.block_on(docker::remove_image(
221        env,
222        &problem.docker_image_name(env.username()),
223    ))?;
224    env.printer().write_line(&format!(
225        "Problem image cleaned: '{}'",
226        problem.fully_qualified_name()
227    ));
228
229    Ok(())
230}
231
232pub fn stop(
233    env: &Environment<impl Connect, impl Printer>,
234    prob_query: &str,
235    runtime: &mut Runtime,
236) -> SomaResult<()> {
237    let problem = env.repo_manager().search_prob(prob_query)?;
238
239    let container_list = runtime.block_on(docker::list_containers(env))?;
240    if !docker::container_from_prob_exists(&container_list, &problem) {
241        Err(SomaError::ProblemNotRunning)?;
242    }
243
244    let container_list = docker::containers_from_prob(container_list, &problem);
245    let states_to_stop = &["paused", "restarting", "running"];
246
247    let containers_to_stop = container_list
248        .iter()
249        .filter(|container| states_to_stop.contains(&container.container().state.as_str()));
250
251    for container in containers_to_stop {
252        runtime.block_on(docker::stop(env, &container.container().id))?;
253    }
254
255    for container in container_list {
256        runtime.block_on(docker::remove_container(env, &container.container().id))?;
257    }
258
259    env.printer().write_line(&format!(
260        "Problem stopped: '{}'",
261        problem.fully_qualified_name()
262    ));
263
264    Ok(())
265}
266
267pub fn update(
268    env: &Environment<impl Connect, impl Printer>,
269    repo_name: &str,
270    runtime: &mut Runtime,
271) -> SomaResult<()> {
272    let mut repository = env.repo_manager().get_repo(repo_name)?;
273    repository.update(&runtime.block_on(docker::list_images(env))?)?;
274    env.printer()
275        .write_line(&format!("Repository updated: '{}'", repo_name));
276
277    Ok(())
278}