handlers.rs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  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. // FLARE has no docker container to tail: its log is the orbit session on
  26. // the controller (`flare orbit attach`).
  27. let recipe_obj = load_recipe(recipe_name, &config.recipe_sources).await?;
  28. if recipe_obj.engine == "flare" {
  29. let head = cluster
  30. .nodes
  31. .iter()
  32. .find(|n| n.rank == 0)
  33. .ok_or_else(|| anyhow::anyhow!("no head node (rank 0) in cluster"))?;
  34. let orbit = crate::native::flare::orbit_cluster_for(config, &cluster.name);
  35. let cmd = crate::native::flare::attach_cmd(config, &recipe_obj, &orbit)?;
  36. crate::ssh::run_ssh(config, head, cmd, dry_run).await?;
  37. return Ok(());
  38. }
  39. docker::show_logs(
  40. config,
  41. &cluster,
  42. recipe_name,
  43. follow,
  44. node.as_deref(),
  45. dry_run,
  46. )
  47. .await?;
  48. Ok(())
  49. }
  50. /// Handle `health` command: check cluster health or show host key fingerprints.
  51. pub(crate) async fn handle_health(
  52. config: &Config,
  53. cluster_name: Option<String>,
  54. fingerprint: bool,
  55. format: OutputFormat,
  56. ) -> Result<()> {
  57. if fingerprint {
  58. let clusters_to_check: Vec<Cluster> = match &cluster_name {
  59. Some(name) => vec![select_cluster_by_name(config, name)?],
  60. None => config.clusters.clone(),
  61. };
  62. for cluster in &clusters_to_check {
  63. println!(
  64. "\n{}",
  65. fluent_i18n::t!("cluster-section", { "name" => &cluster.name })
  66. );
  67. for node in &cluster.nodes {
  68. match fetch_host_key(config, node).await {
  69. Ok(fp) => println!(
  70. "{}",
  71. fluent_i18n::t!("host-key-line", { "name" => &node.name, "ip" => &node.ssh_ip, "key" => &fp })
  72. ),
  73. Err(e) => println!(
  74. "{}",
  75. fluent_i18n::t!("host-key-error", { "name" => &node.name, "ip" => &node.ssh_ip, "error" => &e.to_string() })
  76. ),
  77. }
  78. }
  79. }
  80. } else {
  81. match cluster_name {
  82. Some(ref name) => {
  83. let cluster = select_cluster_by_name(config, name)?;
  84. docker::health::health_check(config, &cluster, false, format).await?;
  85. }
  86. None => {
  87. let fleet = docker::health::collect(config, None, false).await?;
  88. docker::health::print_fleet(&fleet, format);
  89. }
  90. }
  91. }
  92. Ok(())
  93. }
  94. /// Handle `exec` command: run a command on cluster nodes via SSH.
  95. pub(crate) async fn handle_exec(
  96. config: &Config,
  97. cluster: Option<String>,
  98. recipe: Option<String>,
  99. node: Option<String>,
  100. alias: Option<String>,
  101. command: Vec<String>,
  102. dry_run: bool,
  103. ) -> Result<()> {
  104. let (cluster, _recipe) =
  105. exec::resolve_target(config, cluster.as_deref(), recipe.as_deref()).await?;
  106. let cmd_str = exec::resolve_command(config, alias.as_deref(), &command)?;
  107. exec::run(config, &cluster, node.as_deref(), &cmd_str, dry_run).await?;
  108. Ok(())
  109. }
  110. /// Handle `power` command: power on/off/switch nodes.
  111. pub(crate) async fn handle_power(
  112. config: &Config,
  113. action: PowerAction,
  114. dry_run: bool,
  115. ) -> Result<()> {
  116. match action {
  117. PowerAction::Off { cluster, node } => {
  118. let cluster = select_cluster_by_name(config, &cluster)?;
  119. let nodes = node.into_iter().collect::<Vec<_>>();
  120. power::power_off(config, &cluster, &nodes, dry_run).await?;
  121. }
  122. PowerAction::On { cluster, node } => {
  123. let cluster = select_cluster_by_name(config, &cluster)?;
  124. let nodes = node.into_iter().collect::<Vec<_>>();
  125. let targets = power::select_targets(&cluster, &nodes, "power on")?;
  126. let stale = if dry_run {
  127. Vec::new()
  128. } else {
  129. power::stale_nodes(&targets).await
  130. };
  131. let report = power::power_on(config, &cluster, &nodes, dry_run, &stale).await?;
  132. for line in report {
  133. info!("{}", line);
  134. }
  135. }
  136. PowerAction::Switch { from, to } => {
  137. let from = select_cluster_by_name(config, &from)?;
  138. let to = select_cluster_by_name(config, &to)?;
  139. power::power_switch(config, &from, &to, dry_run).await?;
  140. }
  141. }
  142. Ok(())
  143. }
  144. /// Resolve a cluster for container operations (stop/logs).
  145. /// When `auto`, tries live container label lookup first, then falls back
  146. /// to config-based auto-selection (for dry-run / offline scenarios).
  147. pub(crate) async fn resolve_cluster_for_op(
  148. config: &Config,
  149. recipe_name: &str,
  150. cluster_name: &str,
  151. ) -> Result<Cluster> {
  152. if cluster_name != "auto" {
  153. let recipe = load_recipe(recipe_name, &config.recipe_sources).await?;
  154. let (cluster, _) = resolve_cluster(config, &recipe, cluster_name, &HashSet::new())?;
  155. return Ok(cluster);
  156. }
  157. // Try live container label lookup first
  158. if let Ok(c) = docker::resolve_cluster_by_container_label(config, recipe_name).await {
  159. return Ok(c.clone());
  160. }
  161. // Fall back to config-based auto (dry-run / containers not running)
  162. let recipe = load_recipe(recipe_name, &config.recipe_sources).await?;
  163. let (cluster, _) = resolve_cluster(config, &recipe, "auto", &HashSet::new())?;
  164. Ok(cluster)
  165. }