1use crate::config::Config;
2use crate::deployment_manager::DeploymentManager;
3use clap::Parser;
4use std::collections::HashMap;
5use tracing::{debug, error, info};
6use tracing_subscriber;
7
8#[derive(Parser)]
9pub struct CLI {
10 #[arg(value_name = "TAG", index = 1)]
11 pub tag: String,
12 #[arg(short, long)]
13 pub name: Option<String>,
14 #[arg(short, long, default_value = "/var/run/docker.sock")]
15 pub socket_path: String,
16 #[arg(short, long)]
17 pub repo_url: Option<String>,
18 #[arg(short, long, help = "Path to clone the config repo into")]
19 pub clone_path: Option<String>,
20 #[arg(
21 long,
22 help = "Target path in the container to mount the config (e.g. /etc/traefik/dynamic)"
23 )]
24 pub mount_path: Option<String>,
25 #[arg(short, long, action = clap::ArgAction::Count, help = "Increase verbosity (-v, -vv, etc.)")]
26 pub verbose: u8,
27 #[arg(long, default_value = "docker-compose.yml")]
28 pub compose_file: String,
29 #[arg(
30 short = 'e',
31 long = "env-file",
32 default_value = ".env",
33 help = "Path to .env file"
34 )]
35 pub env_file: String,
36 #[arg(long, help = "Use Docker Swarm mode")]
37 pub swarm: bool,
38}
39
40pub async fn deploy(mut cli: CLI) {
42 if std::env::var("SKIP_DEPLOY").ok().as_deref() == Some("1") {
44 tracing::info!("Skipping real deployment for test");
45 return;
46 }
47
48 let env_content = match std::fs::read_to_string(&cli.env_file) {
50 Ok(content) => {
51 let mut env_content = HashMap::new();
52 for line in content.lines() {
53 let line = line.trim();
54 if line.is_empty() || line.starts_with('#') {
55 continue;
56 }
57 if let Some((key, value)) = line.split_once('=') {
58 env_content.insert(key.trim().to_string(), value.trim().to_string());
59 }
60 }
61 env_content
62 }
63 Err(e) => {
64 error!("Error reading .env file: {}", e);
65 HashMap::new()
66 }
67 };
68
69 debug!("env_content: {:?}", env_content);
70
71 let name = extract_env_var_from_cli_or_env(&cli.name, &env_content, "NAME", "");
73 let repo_url = extract_env_var_from_cli_or_env(&cli.repo_url, &env_content, "REPO_URL", "");
74 let clone_path =
75 extract_env_var_from_cli_or_env(&cli.clone_path, &env_content, "CLONE_PATH", "/opt/dev");
76 let mount_path =
77 extract_env_var_from_cli_or_env(&cli.mount_path, &env_content, "MOUNT_PATH", "");
78
79 let socket_path = if cli.socket_path == "/var/run/docker.sock" {
81 env_content
82 .get("SOCKET_PATH")
83 .cloned()
84 .unwrap_or_else(|| cli.socket_path.clone())
85 } else {
86 cli.socket_path.clone()
87 };
88 let compose_file = if cli.compose_file == "docker-compose.yml" {
89 env_content
90 .get("COMPOSE_FILE")
91 .cloned()
92 .unwrap_or_else(|| cli.compose_file.clone())
93 } else {
94 cli.compose_file.clone()
95 };
96
97 cli.name = if name.is_empty() { None } else { Some(name) };
99 cli.repo_url = if repo_url.is_empty() {
100 None
101 } else {
102 Some(repo_url)
103 };
104 cli.clone_path = if clone_path.is_empty() {
105 None
106 } else {
107 Some(clone_path)
108 };
109 cli.mount_path = if mount_path.is_empty() {
110 None
111 } else {
112 Some(mount_path)
113 };
114 cli.socket_path = socket_path;
115 cli.compose_file = compose_file;
116
117 if cli.mount_path.is_none() {
119 error!("MOUNT_PATH must be set via --mount-path or in the .env file");
120 eprintln!("MOUNT_PATH must be set via --mount-path or in the .env file");
121 return;
122 }
123 tracing::debug!("Mount path from CLI: {:?}", cli.mount_path);
124
125 let config = match Config::from_env_and_cli(&cli) {
127 Ok(config) => {
128 let filter = match cli.verbose {
130 0 => "warn",
131 1 => "info",
132 2 => "debug",
133 _ => "trace",
134 };
135 tracing_subscriber::fmt()
136 .with_env_filter(tracing_subscriber::EnvFilter::new(filter))
137 .init();
138 info!("Configuration loaded:");
139 info!(" Repository: {}", config.repo_url);
140 info!(" Clone path: {}", config.clone_path);
141 info!(" Mount path: {}", config.mount_path);
142 config
143 }
144 Err(e) => {
145 error!("Configuration error: {}", e);
146 info!("");
147 Config::show_configuration_help();
148 return;
149 }
150 };
151
152 let deployment_manager = DeploymentManager::new(config.clone());
153
154 info!(
155 "Starting deployment for project '{}' with tag '{}'",
156 config.name, cli.tag
157 );
158
159 match deployment_manager.rolling_deploy(&cli.tag, cli.swarm).await {
160 Ok(()) => info!("Rolling deployment successful!"),
161 Err(e) => error!("Rolling deployment failed: {}", e),
162 }
163}
164
165fn extract_env_var_from_cli_or_env<V: ToString + PartialEq + Default>(
167 val: &Option<V>,
168 env_content: &HashMap<String, String>,
169 key: &str,
170 default_value: &str,
171) -> String {
172 let val_str = val.as_ref().unwrap_or(&V::default()).to_string();
173 if val_str != V::default().to_string() && val_str != default_value {
174 val_str
176 } else if let Some(env_val) = env_content.get(key) {
177 env_val.clone()
179 } else {
180 default_value.to_string()
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use std::sync::Once;
189 use tokio::runtime::Runtime;
190
191 static INIT: Once = Once::new();
192
193 fn setup() {
194 INIT.call_once(|| {
195 });
197 }
198
199 #[test]
200 fn test_deploy_missing_name() {
201 setup();
202 let cli = CLI {
203 tag: "v1.0.0".to_string(),
204 name: None,
205 socket_path: "/tmp/docker.sock".to_string(),
206 repo_url: Some("https://example.com/repo.git".to_string()),
207 clone_path: Some("/tmp/mount".to_string()),
208 mount_path: None,
209 verbose: 0,
210 compose_file: "docker-compose.yml".to_string(),
211 env_file: ".env".to_string(),
212 swarm: false,
213 };
214 let rt = Runtime::new().unwrap();
215 rt.block_on(async {
216 deploy(cli).await;
217 });
218 }
220
221 #[test]
222 fn test_extract_env_var_from_cli_or_env_cli_value() {
223 let env_content = std::collections::HashMap::new();
224 let cli_val = Some("cli_val".to_string());
225 let result =
226 super::extract_env_var_from_cli_or_env(&cli_val, &env_content, "KEY", "default");
227 assert_eq!(result, "cli_val");
228 }
229
230 #[test]
231 fn test_extract_env_var_from_cli_or_env_env_value() {
232 let mut env_content = std::collections::HashMap::new();
233 env_content.insert("KEY".to_string(), "env_val".to_string());
234 let cli_val: Option<String> = None;
235 let result =
236 super::extract_env_var_from_cli_or_env(&cli_val, &env_content, "KEY", "default");
237 assert_eq!(result, "env_val");
238 }
239
240 #[test]
241 fn test_extract_env_var_from_cli_or_env_both_values() {
242 let mut env_content = std::collections::HashMap::new();
243 env_content.insert("KEY".to_string(), "env_val".to_string());
244 let cli_val = Some("cli_val".to_string());
245 let result =
246 super::extract_env_var_from_cli_or_env(&cli_val, &env_content, "KEY", "default");
247 assert_eq!(result, "cli_val");
248 }
249
250 #[test]
251 fn test_extract_env_var_from_cli_or_env_default() {
252 let env_content = std::collections::HashMap::new();
253 let cli_val: Option<String> = None;
254 let result =
255 super::extract_env_var_from_cli_or_env(&cli_val, &env_content, "KEY", "default");
256 assert_eq!(result, "default");
257 }
258}