1use crate::models::ComponentStatus;
2use crate::ui::PremiumUI;
3use anyhow::anyhow;
4use anyhow::{Context, Result};
5use console::style;
6use dialoguer::{Confirm, Input, Select};
7use flate2::write::GzEncoder;
8use flate2::Compression;
9use ignore::WalkBuilder;
10use pathdiff;
11use reqwest::multipart::{Form, Part};
12use serde::Deserialize;
13use serde::Serialize;
14use std::path::PathBuf;
15use std::{fs::File, path::Path};
16use std::{thread, time::Duration};
17use tabled::Table;
18use tar::Builder;
19use tempfile::env::temp_dir;
20use tokio::{fs, task};
21
22#[derive(Debug, Serialize, Deserialize)]
23pub struct DeployPermissions {
24 max_file_count: u64,
25}
26
27impl PremiumUI {
28 pub async fn deploy_interactive(&self) -> Result<()> {
29 let project_path: String = Input::with_theme(&self.theme)
31 .with_prompt("Enter project path")
32 .default(".".into())
33 .interact_text()?;
34 let project_path = PathBuf::from(project_path);
35 let project_path = project_path.canonicalize().context("Failed to canonicalize project path")?;
36
37 if !Path::new(&project_path).exists() {
39 println!("{}", style("Error: Project path does not exist.").red());
40 return Ok(());
41 }
42
43 let environments = vec!["Development", "Staging", "Production"];
45 let env_selection = Select::with_theme(&self.theme)
46 .with_prompt("Select deployment environment")
47 .items(&environments)
48 .default(0)
49 .interact()?;
50
51 if environments[env_selection] == "Production" {
53 let confirm = Confirm::with_theme(&self.theme)
54 .with_prompt("⚠️ You're deploying to production. Are you sure?")
55 .default(false)
56 .interact()?;
57 if !confirm {
58 println!("{}", style("Deployment cancelled.").yellow());
59 return Ok(());
60 }
61 }
62
63 println!("\n{}", style("🚀 Initializing deployment...").cyan().bold());
64 println!("{}", style("🗜️ Creating tarball...").cyan().bold());
66 let tarball_path = self
67 .create_tarball(&project_path.to_string_lossy())
68 .await
69 .context("Failed to create tarball")?;
70 println!("{}", style("🗜️ uploading").cyan().bold());
71 let path = Path::new(&project_path);
72 if !path.is_dir() {
73 print!("{}", style("Error: Not a directory").red());
74 return Err(anyhow!("Invalid project path"));
75 }
76 let project_path = Path::new(&project_path)
77 .canonicalize()
78 .expect("Failed to canonicalize path");
79 let project_name: String = project_path
80 .file_name()
81 .and_then(|s| s.to_str())
82 .map(String::from)
83 .expect("Unable to determine folder name"); self.upload_tarball(
85 &tarball_path,
86 environments[env_selection],
87 project_name.as_str(),
88 )
89 .await
90 .context("Failed to upload tarball")?;
91
92 fs::remove_file(&tarball_path)
94 .await
95 .context("Failed to clean up tarball")?;
96
97 let steps = [
98 ("Analyzing project", 20),
99 ("Building containers", 40),
100 ("Pushing to registry", 30),
101 ("Configuring services", 25),
102 ("Starting components", 35),
103 ];
104
105 for (step, duration) in steps.iter() {
106 let pb = self.create_progress_bar(*duration, step);
107 for i in 0..*duration {
108 pb.inc(1);
109 thread::sleep(Duration::from_millis(100));
110
111 match i {
112 5 => pb.set_message(format!("{} (scanning dependencies)", step)),
113 15 => pb.set_message(format!("{} (optimizing)", step)),
114 25 => pb.set_message(format!("{} (finalizing)", step)),
115 _ => {}
116 }
117 }
118 pb.finish_with_message(format!("{} ✓", step));
119 }
120
121 let status_table = Table::new(vec![
122 ComponentStatus {
123 name: "Web Frontend".into(),
124 status: "Running".into(),
125 replicas: "3/3".into(),
126 cpu: "150m".into(),
127 memory: "256Mi".into(),
128 },
129 ComponentStatus {
130 name: "API Backend".into(),
131 status: "Running".into(),
132 replicas: "2/2".into(),
133 cpu: "200m".into(),
134 memory: "512Mi".into(),
135 },
136 ComponentStatus {
137 name: "Database".into(),
138 status: "Running".into(),
139 replicas: "1/1".into(),
140 cpu: "500m".into(),
141 memory: "1Gi".into(),
142 },
143 ])
144 .to_string();
145
146 println!("\n{}", style("📊 Deployment Status").cyan().bold());
147 println!("{}", status_table);
148 println!("\n{}", style("🌍 Application Endpoints").cyan().bold());
149 println!("Frontend: {}", style("https://app.example.com").green());
150 println!("API: {}", style("https://api.example.com").green());
151 println!("Metrics: {}", style("https://metrics.example.com").green());
152 println!(
153 "\n{}",
154 style("✨ Deployment completed successfully!")
155 .green()
156 .bold()
157 );
158 println!(
159 "{}",
160 style("Run 'omni status' to monitor your deployment.").dim()
161 );
162 Ok(())
163 }
164
165 async fn create_tarball(&self, project_path: &str) -> Result<String> {
166 let project_path = fs::canonicalize(project_path)
168 .await
169 .context("Failed to resolve project path")?;
170 let absolute_path = project_path.clone();
171 let project_name = absolute_path
173 .file_name()
174 .and_then(|name| name.to_str())
175 .unwrap_or_else(|| {
176 project_path
177 .components()
178 .last()
179 .and_then(|comp| comp.as_os_str().to_str())
180 .unwrap_or("project")
181 })
182 .to_string();
183
184 let temp_dir = temp_dir();
186 let tar_gz_path = temp_dir.join(format!("{}.tar.gz", project_name));
187
188 let tar_gz = File::create(&tar_gz_path)?;
190 let enc = GzEncoder::new(tar_gz, Compression::default());
191 let builder = std::sync::Arc::new(std::sync::Mutex::new(Builder::new(enc)));
192
193 let mut total_files = 0;
195 let walker = WalkBuilder::new(&project_path)
196 .hidden(false)
197 .git_ignore(true)
198 .git_global(true)
199 .git_exclude(true)
200 .build();
201
202 for entry in walker.filter_map(|e| e.ok()) {
203 if entry.file_type().map_or(false, |ft| ft.is_file()) {
204 total_files += 1;
205 }
206 }
207
208 let permissions_url = self.api_client.base_url.clone() + "/deploy/permissions";
210 let max_file_count = self.api_client.get::<DeployPermissions>("/deploy/permissions").await;
211
212 match max_file_count {
213 Ok(permissions) => {
214 if total_files > permissions.max_file_count {
215 let too_many_files: i64 =
216 total_files as i64 - permissions.max_file_count as i64;
217 println!("{}",style(format!("The server had denied your deployment request. Your project contains {} too many files. ({}/{})",too_many_files,total_files,permissions.max_file_count)).red());
218 std::process::exit(0);
219 }
220 },
221 Err(e) => {
222 eprintln!("{}", style(format!("Deployment failed: {e}",)).red().bold());
223 std::process::exit(0);
224 }
225 }
226
227 if total_files > 5000 {
228 let path_str = format!("{}", project_path.display());
229 let current_path_str = style(format!(
230 "You are about to upload the entire of {}",
231 path_str
232 ))
233 .yellow()
234 .bold()
235 .underlined();
236 let prompt = format!("Your project contains more than 5000 files.
237Are you sure you would like to deploy it? This make take significant amounts of time and space on your machine.\n{}",
238 current_path_str);
239 let confirm = dialoguer::Confirm::with_theme(&self.theme)
240 .default(false)
241 .with_prompt(prompt)
242 .report(false)
243 .show_default(true)
244 .interact()?;
245 if !confirm {
246 println!("{}", style("Canceling upload operation").bold().blue());
247 std::process::exit(0)
248 }
249 }
250
251 let pb = self.create_progress_bar(total_files, "Creating tarball");
252 pb.set_message("Initializing tarball creation");
253
254 let mut files_processed = 0;
256 let walker = WalkBuilder::new(&project_path)
257 .hidden(false)
258 .git_ignore(true)
259 .git_global(true)
260 .git_exclude(true)
261 .build();
262
263 for entry in walker.filter_map(|e| e.ok()) {
264 if let Some(file_type) = entry.file_type() {
265 let entry_path = entry.path().to_path_buf();
266
267 let relative_path = pathdiff::diff_paths(&entry_path, &project_path)
269 .ok_or_else(|| anyhow::anyhow!("Failed to compute relative path"))?;
270
271 if relative_path.as_os_str().is_empty() {
273 continue;
274 }
275
276 if file_type.is_dir() {
277 pb.set_message(format!("Adding directory: {}", relative_path.display()));
278
279 let builder = std::sync::Arc::clone(&builder);
280 let relative_path = relative_path.clone();
281
282 task::spawn_blocking(move || -> Result<()> {
283 let mut builder = builder.lock().unwrap();
284 let mut header = tar::Header::new_ustar();
285 header.set_entry_type(tar::EntryType::Directory);
286 header.set_mode(0o755);
287 header.set_size(0);
288 builder.append_data(&mut header, relative_path, &[][..])?;
289 Ok(())
290 })
291 .await??;
292 } else if file_type.is_file() {
293 let file_contents = fs::read(&entry_path)
294 .await
295 .with_context(|| format!("Failed to read file: {:?}", entry_path))?;
296
297 let builder = std::sync::Arc::clone(&builder);
298 let relative_path_clone = relative_path.clone();
299
300 task::spawn_blocking(move || -> Result<()> {
301 let mut builder = builder.lock().unwrap();
302 let mut header = tar::Header::new_ustar();
303 header.set_size(file_contents.len() as u64);
304 header.set_mode(0o644);
305 builder.append_data(
306 &mut header,
307 relative_path_clone,
308 &file_contents[..],
309 )?;
310 Ok(())
311 })
312 .await??;
313
314 files_processed += 1;
315 pb.set_position(files_processed);
316 pb.set_message(format!("Adding file: {}", relative_path.display()));
317 }
318
319 tokio::time::sleep(Duration::from_millis(1)).await;
320 }
321 }
322
323 pb.set_message("Finalizing tarball");
325
326 task::spawn_blocking(move || -> Result<()> {
327 let mut builder = builder.lock().unwrap();
328 builder.finish()?;
329 Ok(())
330 })
331 .await??;
332
333 pb.finish_with_message("Tarball created successfully ✓");
334
335 Ok(tar_gz_path.to_string_lossy().into_owned())
336 }
337
338 async fn upload_tarball(
339 &self,
340 tarball_path: &str,
341 environment: &str,
342 name: &str,
343 ) -> Result<()> {
344 let path = PathBuf::from(tarball_path);
345 if !path.is_file() {
346 return Err(anyhow!("Path is not a file"));
347 }
348 let uuid = uuid::Uuid::new_v4();
349 let uuid_str = format!("u-{}", uuid.to_string());
350
351 let api_url = format!("{}/apps/{}/releases/{}/upload",
353 self.api_client.base_url, name, uuid_str);
354
355 let file_content = fs::read(tarball_path).await?;
356
357 let part = Part::bytes(file_content)
359 .file_name(name.to_string())
360 .mime_str("application/gzip")?;
361
362 let form = Form::new()
364 .part("media", part)
365 .text("environment", environment.to_string());
366
367 let pb = self.create_progress_bar(100, "Uploading project");
368
369 let response = self.api_client.client
371 .post(&api_url)
372 .headers(self.api_client.headers.clone())
373 .multipart(form)
374 .send()
375 .await?;
376
377 if !response.status().is_success() {
378 pb.abandon_with_message("Upload failed!");
379 anyhow::bail!(
380 "Failed to upload tarball: {} - {}",
381 response.status(),
382 response
383 .text()
384 .await
385 .unwrap_or_else(|_| "No error message".to_string())
386 );
387 }
388
389 pb.finish_with_message("Upload completed successfully ✓");
390 Ok(())
391 }
392
393
394 async fn test_api_connection(&self) -> Result<()> {
395 let mut spinner = self.create_spinner("Testing API connection...");
396
397 match self.api_client.get::<serde_json::Value>("/health").await {
399 Ok(_) => {
400 spinner.stop_with_message("✅ Connection successful!".to_string());
401 Ok(())
402 },
403 Err(err) => {
404 spinner.stop_with_message(format!("❌ Connection failed: {}", err));
405 Err(err)
406 }
407 }
408 }
409}