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