handlers.rs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. //! Per-command handlers extracted from main.rs (the 500-line cap).
  2. //! `main.rs` keeps the clap dispatch; each `handle_*` does the command work.
  3. use crate::cli::{OutputFormat, PowerAction};
  4. use crate::config::Config;
  5. use crate::config::load_recipe;
  6. use crate::config::resolve_cluster;
  7. use crate::config::{Cluster, select_cluster_by_name};
  8. use crate::docker;
  9. use crate::exec;
  10. use crate::power;
  11. use crate::ssh::fetch_host_key;
  12. use anyhow::Result;
  13. use std::collections::HashSet;
  14. use tracing::info;
  15. /// Handle `logs` command: show container logs.
  16. pub(crate) async fn handle_logs(
  17. config: &Config,
  18. recipe_name: &str,
  19. cluster: &str,
  20. follow: bool,
  21. node: Option<String>,
  22. dry_run: bool,
  23. ) -> Result<()> {
  24. let cluster = resolve_cluster_for_op(config, recipe_name, cluster).await?;
  25. docker::show_logs(
  26. config,
  27. &cluster,
  28. recipe_name,
  29. follow,
  30. node.as_deref(),
  31. dry_run,
  32. )
  33. .await?;
  34. Ok(())
  35. }
  36. /// Handle `health` command: check cluster health or show host key fingerprints.
  37. pub(crate) async fn handle_health(
  38. config: &Config,
  39. cluster_name: Option<String>,
  40. fingerprint: bool,
  41. format: OutputFormat,
  42. ) -> Result<()> {
  43. if fingerprint {
  44. let clusters_to_check: Vec<Cluster> = match &cluster_name {
  45. Some(name) => vec![select_cluster_by_name(config, name)?],
  46. None => config.clusters.clone(),
  47. };
  48. for cluster in &clusters_to_check {
  49. println!(
  50. "\n{}",
  51. fluent_i18n::t!("cluster-section", { "name" => &cluster.name })
  52. );
  53. for node in &cluster.nodes {
  54. match fetch_host_key(config, node).await {
  55. Ok(fp) => println!(
  56. "{}",
  57. fluent_i18n::t!("host-key-line", { "name" => &node.name, "ip" => &node.ssh_ip, "key" => &fp })
  58. ),
  59. Err(e) => println!(
  60. "{}",
  61. fluent_i18n::t!("host-key-error", { "name" => &node.name, "ip" => &node.ssh_ip, "error" => &e.to_string() })
  62. ),
  63. }
  64. }
  65. }
  66. } else {
  67. match cluster_name {
  68. Some(ref name) => {
  69. let cluster = select_cluster_by_name(config, name)?;
  70. docker::health::health_check(config, &cluster, false, format).await?;
  71. }
  72. None => {
  73. let fleet = docker::health::collect(config, None, false).await?;
  74. docker::health::print_fleet(&fleet, format);
  75. }
  76. }
  77. }
  78. Ok(())
  79. }
  80. /// Handle `exec` command: run a command on cluster nodes via SSH.
  81. pub(crate) async fn handle_exec(
  82. config: &Config,
  83. cluster: Option<String>,
  84. recipe: Option<String>,
  85. node: Option<String>,
  86. alias: Option<String>,
  87. command: Vec<String>,
  88. dry_run: bool,
  89. ) -> Result<()> {
  90. let (cluster, _recipe) =
  91. exec::resolve_target(config, cluster.as_deref(), recipe.as_deref()).await?;
  92. let cmd_str = exec::resolve_command(config, alias.as_deref(), &command)?;
  93. exec::run(config, &cluster, node.as_deref(), &cmd_str, dry_run).await?;
  94. Ok(())
  95. }
  96. /// Handle `power` command: power on/off/switch nodes.
  97. pub(crate) async fn handle_power(
  98. config: &Config,
  99. action: PowerAction,
  100. dry_run: bool,
  101. ) -> Result<()> {
  102. match action {
  103. PowerAction::Off { cluster, node } => {
  104. let cluster = select_cluster_by_name(config, &cluster)?;
  105. let nodes = node.into_iter().collect::<Vec<_>>();
  106. power::power_off(config, &cluster, &nodes, dry_run).await?;
  107. }
  108. PowerAction::On { cluster, node } => {
  109. let cluster = select_cluster_by_name(config, &cluster)?;
  110. let nodes = node.into_iter().collect::<Vec<_>>();
  111. let targets = power::select_targets(&cluster, &nodes, "power on")?;
  112. let stale = if dry_run {
  113. Vec::new()
  114. } else {
  115. power::stale_nodes(&targets).await
  116. };
  117. let report = power::power_on(config, &cluster, &nodes, dry_run, &stale).await?;
  118. for line in report {
  119. info!("{}", line);
  120. }
  121. }
  122. PowerAction::Switch { from, to } => {
  123. let from = select_cluster_by_name(config, &from)?;
  124. let to = select_cluster_by_name(config, &to)?;
  125. power::power_switch(config, &from, &to, dry_run).await?;
  126. }
  127. }
  128. Ok(())
  129. }
  130. /// Resolve a cluster for container operations (stop/logs).
  131. /// When `auto`, tries live container label lookup first, then falls back
  132. /// to config-based auto-selection (for dry-run / offline scenarios).
  133. pub(crate) async fn resolve_cluster_for_op(
  134. config: &Config,
  135. recipe_name: &str,
  136. cluster_name: &str,
  137. ) -> Result<Cluster> {
  138. if cluster_name != "auto" {
  139. let recipe = load_recipe(recipe_name, &config.recipe_sources).await?;
  140. let (cluster, _) = resolve_cluster(config, &recipe, cluster_name, &HashSet::new())?;
  141. return Ok(cluster);
  142. }
  143. // Try live container label lookup first
  144. if let Ok(c) = docker::resolve_cluster_by_container_label(config, recipe_name).await {
  145. return Ok(c.clone());
  146. }
  147. // Fall back to config-based auto (dry-run / containers not running)
  148. let recipe = load_recipe(recipe_name, &config.recipe_sources).await?;
  149. let (cluster, _) = resolve_cluster(config, &recipe, "auto", &HashSet::new())?;
  150. Ok(cluster)
  151. }