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