Skip to main content

micro_proxy/
cli.rs

1//! 命令行接口模块
2//!
3//! 负责提供命令行交互接口
4
5use 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/// micro_proxy - 微应用管理工具
20#[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    /// 配置文件路径
27    #[arg(short, long, default_value = "./proxy-config.yml")]
28    config: PathBuf,
29
30    /// 显示详细日志
31    #[arg(short, long)]
32    verbose: bool,
33
34    /// 子命令
35    #[command(subcommand)]
36    command: Commands,
37}
38
39/// 子命令
40#[derive(Subcommand, Debug)]
41enum Commands {
42    /// 启动所有微应用
43    Start {
44        /// 强制重新构建所有镜像
45        #[arg(long)]
46        force_rebuild: bool,
47    },
48    /// 停止所有微应用
49    Stop,
50    /// 清理所有微应用
51    Clean {
52        /// 强制清理,不询问确认
53        #[arg(long)]
54        force: bool,
55        /// 同时清理Docker网络
56        #[arg(long)]
57        network: bool,
58    },
59    /// 查看状态
60    Status,
61    /// 查看网络地址
62    Network {
63        /// 指定输出文件路径(覆盖配置文件中的设置)
64        #[arg(short, long)]
65        output: Option<PathBuf>,
66    },
67}
68
69/// 运行CLI
70///
71/// # 参数
72/// - `args`: 命令行参数
73///
74/// # 返回
75/// 返回运行结果
76pub fn run(args: &[String]) -> Result<()> {
77    let cli = Cli::parse_from(args);
78
79    // 初始化日志
80    let log_level = if cli.verbose { "debug" } else { "info" };
81
82    // 使用dumbo_log初始化日志系统,同时输出到文件和控制台
83    // 日志文件名与Cargo.toml中的包名称保持一致
84    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    // 读取配置
95    let config = ProxyConfig::from_file(&cli.config)?;
96
97    // 执行子命令
98    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
119/// 执行docker-compose命令
120///
121/// 优先使用 `docker compose`(新版本),如果失败则尝试 `docker-compose`(旧版本)
122///
123/// # 参数
124/// - `args`: 命令参数
125///
126/// # 返回
127/// 返回命令执行结果
128fn run_docker_compose(args: &[&str]) -> Result<()> {
129    log::debug!("尝试执行 docker compose 命令: {:?}", args);
130
131    // 首先尝试使用 docker compose(新版本)
132    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            // 尝试使用 docker-compose(旧版本)
149            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
173/// 获取微应用信息
174///
175/// 对于 Static 和 Api 类型,从扫描结果中查找
176/// 对于 Internal 类型,从配置的 path 创建微应用信息
177///
178/// # 参数
179/// - `app_config`: 应用配置
180/// - `micro_apps`: 扫描发现的微应用列表
181///
182/// # 返回
183/// 返回微应用信息
184fn 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            // 从扫描结果中查找
191            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            // 从配置的 path 创建微应用信息
199            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
238/// 计算相对路径
239///
240/// 计算目标路径相对于基准路径的相对路径
241///
242/// # 参数
243/// - `base_path`: 基准路径(通常是当前工作目录)
244/// - `target_path`: 目标路径
245///
246/// # 返回
247/// 返回相对路径字符串
248fn calculate_relative_path(base_path: &PathBuf, target_path: &PathBuf) -> Result<String> {
249    log::debug!("计算相对路径: 基准={:?}, 目标={:?}", base_path, target_path);
250
251    // 获取绝对路径
252    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    // 计算相对路径
263    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
285/// 执行启动命令
286fn execute_start(config: &ProxyConfig, force_rebuild: bool) -> Result<()> {
287    log::info!("开始启动微应用...");
288
289    // 1. 扫描微应用
290    let micro_apps = discover_micro_apps(&config.scan_dirs)?;
291    let discovered_names = get_micro_app_names(&micro_apps);
292
293    // 2. 验证配置
294    config.validate(&discovered_names)?;
295
296    // 3. 创建Docker网络
297    log::info!("创建Docker网络: {}", config.network_name);
298    crate::network::create_network(&config.network_name)?;
299
300    // 4. 初始化状态管理器
301    let mut state_manager = StateManager::new(&config.state_file_path);
302    state_manager.load()?;
303
304    // 5. 处理每个配置的应用
305    let mut network_infos = Vec::new();
306    let mut env_files = HashMap::new();
307
308    // 获取当前工作目录(docker-compose.yml 所在目录)
309    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        // 获取微应用信息
320        let micro_app = get_micro_app_info(app_config, &micro_apps)?;
321
322        // 解析Dockerfile
323        let dockerfile_info = dockerfile::parse_dockerfile(&micro_app.dockerfile)?;
324        if dockerfile_info.exposed_ports.is_empty() {
325            log::warn!("应用 '{}' 的Dockerfile中没有EXPOSE指令", app_config.name);
326        }
327
328        // 计算目录hash
329        let current_hash = calculate_directory_hash(&micro_app.path)?;
330
331        // 判断是否需要重新构建
332        let needs_rebuild =
333            force_rebuild || state_manager.needs_rebuild(&app_config.name, &current_hash);
334
335        if needs_rebuild {
336            log::info!("应用 '{}' 需要重新构建", app_config.name);
337
338            // 执行setup脚本
339            if let Some(ref setup_script) = micro_app.setup_script {
340                log::info!("执行setup脚本: {:?}", setup_script);
341                script::execute_setup_script(setup_script, &micro_app.path)?;
342            }
343
344            // 构建镜像
345            let image_name = format!("{}:latest", app_config.name);
346            builder::build_image(
347                &image_name,
348                &micro_app.dockerfile,
349                &micro_app.path,
350                Some(&micro_app.env_file),
351            )?;
352
353            // 更新状态
354            state_manager.update_state(&app_config.name, current_hash, true);
355        } else {
356            log::info!("应用 '{}' 无需重新构建", app_config.name);
357        }
358
359        // 收集环境变量文件路径(如果存在)
360        if micro_app.env_file.exists() {
361            log::debug!(
362                "应用 '{}' 的 .env 文件存在: {:?}",
363                app_config.name,
364                micro_app.env_file
365            );
366            // 计算相对于当前工作目录的相对路径
367            let relative_env_path = calculate_relative_path(&current_dir, &micro_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        // 创建网络地址信息
379        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    // 6. 生成nginx配置
391    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    // 7. 生成docker-compose配置
396    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    // 8. 生成网络地址列表
406    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    // 9. 保存状态
415    state_manager.save()?;
416
417    // 10. 停止并删除现有容器(确保使用最新配置)
418    log::info!("停止并删除现有容器...");
419    let down_args = vec!["-f", &config.compose_config_path, "down"];
420    // 忽略down命令的错误,因为可能容器不存在
421    let _ = run_docker_compose(&down_args);
422
423    // 11. 启动容器
424    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
434/// 执行停止命令
435fn 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
445/// 执行清理命令
446fn execute_clean(config: &ProxyConfig, force: bool, clean_network: bool) -> Result<()> {
447    log::info!("清理所有微应用...");
448
449    // 如果不是强制清理,询问确认
450    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    // 停止并删除容器
469    log::info!("停止并删除容器...");
470    let compose_args = vec!["-f", &config.compose_config_path, "down"];
471    run_docker_compose(&compose_args)?;
472
473    // 删除镜像
474    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    // 执行clean脚本
481    let micro_apps = discover_micro_apps(&config.scan_dirs)?;
482    for app_config in &config.apps {
483        // 获取微应用信息(包括 Internal 类型)
484        let micro_app = get_micro_app_info(app_config, &micro_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, &micro_app.path) {
489                log::warn!("执行clean脚本失败: {}", e);
490            }
491        }
492    }
493
494    // 删除状态文件
495    log::info!("删除状态文件...");
496    if std::fs::remove_file(&config.state_file_path).is_ok() {
497        log::info!("状态文件已删除");
498    }
499
500    // 删除网络
501    if clean_network {
502        log::info!("删除Docker网络...");
503        crate::network::remove_network(&config.network_name)?;
504    }
505
506    log::info!("清理完成");
507    Ok(())
508}
509
510/// 执行状态查看命令
511fn execute_status(config: &ProxyConfig) -> Result<()> {
512    log::info!("查看微应用状态...");
513
514    println!("=== 微应用状态 ===\n");
515
516    // 检查容器状态
517    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    // 检查镜像状态
529    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
543/// 执行网络地址查看命令
544fn execute_network(config: &ProxyConfig, output: Option<PathBuf>) -> Result<()> {
545    log::info!("查看网络地址...");
546
547    // 扫描微应用
548    let micro_apps = discover_micro_apps(&config.scan_dirs)?;
549    let discovered_names = get_micro_app_names(&micro_apps);
550
551    // 验证配置
552    config.validate(&discovered_names)?;
553
554    // 生成网络地址信息
555    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    // 确定输出路径
569    let output_path = output
570        .map(|p| p.to_string_lossy().to_string())
571        .unwrap_or_else(|| config.network_list_path.clone());
572
573    // 生成网络地址列表
574    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    // 同时打印到控制台
584    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}