1use crate::config::{AppType, ProxyConfig};
6use crate::container;
7use crate::discovery::{discover_micro_apps, get_micro_app_names, MicroApp};
8use crate::dockerfile;
9use crate::network::{generate_network_list, NetworkAddressInfo};
10use crate::nginx;
11use crate::script;
12use crate::state::{calculate_directory_hash, StateManager};
13use crate::{builder, compose, Error, Result};
14use clap::{Parser, Subcommand};
15use std::collections::HashMap;
16use std::path::PathBuf;
17use std::process::Command;
18
19#[derive(Parser, Debug)]
21#[command(name = "micro_proxy")]
22#[command(author = "Your Name <your.email@example.com>")]
23#[command(version = crate::VERSION)]
24#[command(about = "用于管理微应用的工具", long_about = None)]
25struct Cli {
26 #[arg(short, long, default_value = "./proxy-config.yml")]
28 config: PathBuf,
29
30 #[arg(short, long)]
32 verbose: bool,
33
34 #[command(subcommand)]
36 command: Commands,
37}
38
39#[derive(Subcommand, Debug)]
41enum Commands {
42 Start {
44 #[arg(long)]
46 force_rebuild: bool,
47 },
48 Stop,
50 Clean {
52 #[arg(long)]
54 force: bool,
55 #[arg(long)]
57 network: bool,
58 },
59 Status,
61 Network {
63 #[arg(short, long)]
65 output: Option<PathBuf>,
66 },
67}
68
69pub fn run(args: &[String]) -> Result<()> {
77 let cli = Cli::parse_from(args);
78
79 let log_level = if cli.verbose { "debug" } else { "info" };
81
82 let log_file_name = format!("{}.log", env!("CARGO_PKG_NAME"));
85 let log_path = PathBuf::from(&log_file_name);
86 if let Err(e) = dumbo_log::init_log_with_console(&log_path, None, true) {
87 log::error!("初始化日志系统失败: {}", e);
88 return Err(Error::Config(format!("初始化日志系统失败: {}", e)));
89 }
90
91 log::info!("micro_proxy v{} 启动", crate::VERSION);
92 log::debug!("配置文件: {:?}", cli.config);
93
94 let config = ProxyConfig::from_file(&cli.config)?;
96
97 match cli.command {
99 Commands::Start { force_rebuild } => {
100 execute_start(&config, force_rebuild)?;
101 }
102 Commands::Stop => {
103 execute_stop(&config)?;
104 }
105 Commands::Clean { force, network } => {
106 execute_clean(&config, force, network)?;
107 }
108 Commands::Status => {
109 execute_status(&config)?;
110 }
111 Commands::Network { output } => {
112 execute_network(&config, output)?;
113 }
114 }
115
116 Ok(())
117}
118
119fn run_docker_compose(args: &[&str]) -> Result<()> {
129 log::debug!("尝试执行 docker compose 命令: {:?}", args);
130
131 let result = Command::new("docker").arg("compose").args(args).status();
133
134 match result {
135 Ok(status) => {
136 if status.success() {
137 log::debug!("docker compose 命令执行成功");
138 Ok(())
139 } else {
140 let error = format!("docker compose 命令执行失败,退出码: {:?}", status.code());
141 log::error!("{}", error);
142 Err(Error::Container(error))
143 }
144 }
145 Err(e) => {
146 log::warn!("docker compose 命令不可用,尝试使用 docker-compose: {}", e);
147
148 let result = Command::new("docker-compose").args(args).status();
150
151 match result {
152 Ok(status) => {
153 if status.success() {
154 log::debug!("docker-compose 命令执行成功");
155 Ok(())
156 } else {
157 let error =
158 format!("docker-compose 命令执行失败,退出码: {:?}", status.code());
159 log::error!("{}", error);
160 Err(Error::Container(error))
161 }
162 }
163 Err(e) => {
164 let error = format!("docker-compose 命令也不可用: {}", e);
165 log::error!("{}", error);
166 Err(Error::Container(error))
167 }
168 }
169 }
170 }
171}
172
173fn get_micro_app_info(
185 app_config: &crate::config::AppConfig,
186 micro_apps: &[MicroApp],
187) -> Result<MicroApp> {
188 match app_config.app_type {
189 AppType::Static | AppType::Api => {
190 micro_apps
192 .iter()
193 .find(|app| app.name == app_config.name)
194 .cloned()
195 .ok_or_else(|| Error::Config(format!("未找到微应用: {}", app_config.name)))
196 }
197 AppType::Internal => {
198 let path = app_config.path.as_ref().ok_or_else(|| {
200 Error::Config(format!(
201 "Internal 应用 '{}' 必须配置 path 字段",
202 app_config.name
203 ))
204 })?;
205
206 let path_buf = PathBuf::from(path);
207 let dockerfile = path_buf.join("Dockerfile");
208 let env_file = path_buf.join(".env");
209 let setup_script = {
210 let script_path = path_buf.join("setup.sh");
211 if script_path.exists() {
212 Some(script_path)
213 } else {
214 None
215 }
216 };
217 let clean_script = {
218 let script_path = path_buf.join("clean.sh");
219 if script_path.exists() {
220 Some(script_path)
221 } else {
222 None
223 }
224 };
225
226 Ok(MicroApp {
227 name: app_config.name.clone(),
228 path: path_buf,
229 env_file,
230 dockerfile,
231 setup_script,
232 clean_script,
233 })
234 }
235 }
236}
237
238fn calculate_relative_path(base_path: &PathBuf, target_path: &PathBuf) -> Result<String> {
249 log::debug!("计算相对路径: 基准={:?}, 目标={:?}", base_path, target_path);
250
251 let base_abs = base_path.canonicalize().map_err(|e| {
253 log::error!("获取基准路径的绝对路径失败: {:?}, 错误: {}", base_path, e);
254 Error::Config(format!("获取基准路径的绝对路径失败: {}", e))
255 })?;
256
257 let target_abs = target_path.canonicalize().map_err(|e| {
258 log::error!("获取目标路径的绝对路径失败: {:?}, 错误: {}", target_path, e);
259 Error::Config(format!("获取目标路径的绝对路径失败: {}", e))
260 })?;
261
262 let relative_path = pathdiff::diff_paths(&target_abs, &base_abs).ok_or_else(|| {
264 log::error!(
265 "无法计算相对路径: 基准={:?}, 目标={:?}",
266 base_abs,
267 target_abs
268 );
269 Error::Config("无法计算相对路径".to_string())
270 })?;
271
272 let relative_str = relative_path
273 .to_str()
274 .ok_or_else(|| {
275 log::error!("相对路径包含无效字符: {:?}", relative_path);
276 Error::Config("相对路径包含无效字符".to_string())
277 })?
278 .to_string();
279
280 log::debug!("计算得到的相对路径: {}", relative_str);
281
282 Ok(relative_str)
283}
284
285fn execute_start(config: &ProxyConfig, force_rebuild: bool) -> Result<()> {
287 log::info!("开始启动微应用...");
288
289 let micro_apps = discover_micro_apps(&config.scan_dirs)?;
291 let discovered_names = get_micro_app_names(µ_apps);
292
293 config.validate(&discovered_names)?;
295
296 log::info!("创建Docker网络: {}", config.network_name);
298 crate::network::create_network(&config.network_name)?;
299
300 let mut state_manager = StateManager::new(&config.state_file_path);
302 state_manager.load()?;
303
304 let mut network_infos = Vec::new();
306 let mut env_files = HashMap::new();
307
308 let current_dir = std::env::current_dir().map_err(|e| {
310 log::error!("获取当前工作目录失败: {}", e);
311 Error::Config(format!("获取当前工作目录失败: {}", e))
312 })?;
313
314 log::debug!("当前工作目录: {:?}", current_dir);
315
316 for app_config in &config.apps {
317 log::info!("处理应用: {} ({:?})", app_config.name, app_config.app_type);
318
319 let micro_app = get_micro_app_info(app_config, µ_apps)?;
321
322 let dockerfile_info = dockerfile::parse_dockerfile(µ_app.dockerfile)?;
324 if dockerfile_info.exposed_ports.is_empty() {
325 log::warn!("应用 '{}' 的Dockerfile中没有EXPOSE指令", app_config.name);
326 }
327
328 let current_hash = calculate_directory_hash(µ_app.path)?;
330
331 let needs_rebuild =
333 force_rebuild || state_manager.needs_rebuild(&app_config.name, ¤t_hash);
334
335 if needs_rebuild {
336 log::info!("应用 '{}' 需要重新构建", app_config.name);
337
338 if let Some(ref setup_script) = micro_app.setup_script {
340 log::info!("执行setup脚本: {:?}", setup_script);
341 script::execute_setup_script(setup_script, µ_app.path)?;
342 }
343
344 let image_name = format!("{}:latest", app_config.name);
346 builder::build_image(
347 &image_name,
348 µ_app.dockerfile,
349 µ_app.path,
350 Some(µ_app.env_file),
351 )?;
352
353 state_manager.update_state(&app_config.name, current_hash, true);
355 } else {
356 log::info!("应用 '{}' 无需重新构建", app_config.name);
357 }
358
359 if micro_app.env_file.exists() {
361 log::debug!(
362 "应用 '{}' 的 .env 文件存在: {:?}",
363 app_config.name,
364 micro_app.env_file
365 );
366 let relative_env_path = calculate_relative_path(¤t_dir, µ_app.env_file)?;
368 env_files.insert(app_config.name.clone(), relative_env_path);
369 log::info!(
370 "为应用 '{}' 添加环境变量文件: {}",
371 app_config.name,
372 env_files.get(&app_config.name).unwrap()
373 );
374 } else {
375 log::debug!("应用 '{}' 的 .env 文件不存在", app_config.name);
376 }
377
378 let network_info = NetworkAddressInfo::new(
380 app_config.name.clone(),
381 app_config.container_name.clone(),
382 app_config.container_port,
383 &app_config.routes,
384 config.nginx_host_port,
385 &app_config.app_type,
386 );
387 network_infos.push(network_info);
388 }
389
390 log::info!("生成nginx配置...");
392 let nginx_config = nginx::generate_nginx_config(&config.apps, config.nginx_host_port)?;
393 nginx::save_nginx_config(&nginx_config, &config.nginx_config_path)?;
394
395 log::info!("生成docker-compose配置...");
397 let compose_config = compose::generate_compose_config(
398 &config.apps,
399 &config.network_name,
400 config.nginx_host_port,
401 &env_files,
402 )?;
403 compose::save_compose_config(&compose_config, &config.compose_config_path)?;
404
405 log::info!("生成网络地址列表...");
407 generate_network_list(
408 &network_infos,
409 &config.network_name,
410 config.nginx_host_port,
411 &config.network_list_path,
412 )?;
413
414 state_manager.save()?;
416
417 log::info!("停止并删除现有容器...");
419 let down_args = vec!["-f", &config.compose_config_path, "down"];
420 let _ = run_docker_compose(&down_args);
422
423 log::info!("启动容器...");
425 let compose_args = vec!["-f", &config.compose_config_path, "up", "-d"];
426 run_docker_compose(&compose_args)?;
427
428 log::info!("所有微应用启动成功!");
429 log::info!("Nginx统一入口: http://localhost:{}", config.nginx_host_port);
430
431 Ok(())
432}
433
434fn execute_stop(config: &ProxyConfig) -> Result<()> {
436 log::info!("停止所有微应用...");
437
438 let compose_args = vec!["-f", &config.compose_config_path, "stop"];
439 run_docker_compose(&compose_args)?;
440
441 log::info!("所有微应用已停止");
442 Ok(())
443}
444
445fn execute_clean(config: &ProxyConfig, force: bool, clean_network: bool) -> Result<()> {
447 log::info!("清理所有微应用...");
448
449 if !force {
451 println!("确定要清理所有微应用吗?这将删除所有容器和镜像。");
452 print!("输入 'yes' 确认: ");
453 use std::io::Write;
454 std::io::stdout().flush().unwrap();
455
456 let mut input = String::new();
457 std::io::stdin().read_line(&mut input).map_err(|e| {
458 log::error!("读取输入失败: {}", e);
459 Error::Config(format!("读取输入失败: {}", e))
460 })?;
461
462 if input.trim() != "yes" {
463 log::info!("取消清理操作");
464 return Ok(());
465 }
466 }
467
468 log::info!("停止并删除容器...");
470 let compose_args = vec!["-f", &config.compose_config_path, "down"];
471 run_docker_compose(&compose_args)?;
472
473 log::info!("删除镜像...");
475 for app_config in &config.apps {
476 let image_name = format!("{}:latest", app_config.name);
477 builder::remove_image(&image_name)?;
478 }
479
480 let micro_apps = discover_micro_apps(&config.scan_dirs)?;
482 for app_config in &config.apps {
483 let micro_app = get_micro_app_info(app_config, µ_apps)?;
485
486 if let Some(ref clean_script) = micro_app.clean_script {
487 log::info!("执行clean脚本: {:?}", clean_script);
488 if let Err(e) = script::execute_clean_script(clean_script, µ_app.path) {
489 log::warn!("执行clean脚本失败: {}", e);
490 }
491 }
492 }
493
494 log::info!("删除状态文件...");
496 if std::fs::remove_file(&config.state_file_path).is_ok() {
497 log::info!("状态文件已删除");
498 }
499
500 if clean_network {
502 log::info!("删除Docker网络...");
503 crate::network::remove_network(&config.network_name)?;
504 }
505
506 log::info!("清理完成");
507 Ok(())
508}
509
510fn execute_status(config: &ProxyConfig) -> Result<()> {
512 log::info!("查看微应用状态...");
513
514 println!("=== 微应用状态 ===\n");
515
516 for app_config in &config.apps {
518 let status = container::get_container_status(&app_config.container_name)?;
519 let running = container::is_container_running(&app_config.container_name)?;
520
521 println!("应用: {} ({:?})", app_config.name, app_config.app_type);
522 println!(" 容器: {}", app_config.container_name);
523 println!(" 状态: {:?}", status);
524 println!(" 运行中: {}", running);
525 println!();
526 }
527
528 println!("=== 镜像状态 ===\n");
530 for app_config in &config.apps {
531 let image_name = format!("{}:latest", app_config.name);
532 let exists = builder::image_exists(&image_name)?;
533 println!(
534 "镜像: {} - {}",
535 image_name,
536 if exists { "存在" } else { "不存在" }
537 );
538 }
539
540 Ok(())
541}
542
543fn execute_network(config: &ProxyConfig, output: Option<PathBuf>) -> Result<()> {
545 log::info!("查看网络地址...");
546
547 let micro_apps = discover_micro_apps(&config.scan_dirs)?;
549 let discovered_names = get_micro_app_names(µ_apps);
550
551 config.validate(&discovered_names)?;
553
554 let mut network_infos = Vec::new();
556 for app_config in &config.apps {
557 let network_info = NetworkAddressInfo::new(
558 app_config.name.clone(),
559 app_config.container_name.clone(),
560 app_config.container_port,
561 &app_config.routes,
562 config.nginx_host_port,
563 &app_config.app_type,
564 );
565 network_infos.push(network_info);
566 }
567
568 let output_path = output
570 .map(|p| p.to_string_lossy().to_string())
571 .unwrap_or_else(|| config.network_list_path.clone());
572
573 generate_network_list(
575 &network_infos,
576 &config.network_name,
577 config.nginx_host_port,
578 &output_path,
579 )?;
580
581 println!("网络地址列表已生成: {}", output_path);
582
583 println!("\n=== 网络地址信息 ===\n");
585 for info in &network_infos {
586 println!("{}", info.format());
587 }
588
589 Ok(())
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 #[test]
597 fn test_cli_parse() {
598 let args = vec![
599 "micro_proxy".to_string(),
600 "--config".to_string(),
601 "./test.yml".to_string(),
602 "start".to_string(),
603 ];
604
605 let cli = Cli::parse_from(&args);
606 assert_eq!(cli.config, PathBuf::from("./test.yml"));
607 assert!(matches!(cli.command, Commands::Start { .. }));
608 }
609
610 #[test]
611 fn test_cli_parse_verbose() {
612 let args = vec![
613 "micro_proxy".to_string(),
614 "-v".to_string(),
615 "status".to_string(),
616 ];
617
618 let cli = Cli::parse_from(&args);
619 assert!(cli.verbose);
620 assert!(matches!(cli.command, Commands::Status));
621 }
622}