|
@@ -45,6 +45,37 @@ pub fn remote_command(recipe_name: &str, cluster_name: &str, rank: u32, wait: bo
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/// Find the cluster containing all named nodes (for `--node` without
|
|
|
|
|
+/// `--cluster`): the only cluster whose node set is a superset of the given
|
|
|
|
|
+/// names. Ambiguous (nodes spread across several clusters) → error naming
|
|
|
|
|
+/// the candidates.
|
|
|
|
|
+fn cluster_for_node(
|
|
|
|
|
+ config: &Config,
|
|
|
|
|
+ nodes: &[String],
|
|
|
|
|
+) -> anyhow::Result<Cluster> {
|
|
|
|
|
+ let mut matches: Vec<&Cluster> = config
|
|
|
|
|
+ .clusters
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .filter(|c| nodes.iter().all(|n| c.nodes.iter().any(|cn| cn.name == *n)))
|
|
|
|
|
+ .collect();
|
|
|
|
|
+ match matches.len() {
|
|
|
|
|
+ 0 => anyhow::bail!(
|
|
|
|
|
+ "Node(s) {} not found in any cluster",
|
|
|
|
|
+ nodes.join(", ")
|
|
|
|
|
+ ),
|
|
|
|
|
+ 1 => Ok(matches.remove(0).clone()),
|
|
|
|
|
+ n => anyhow::bail!(
|
|
|
|
|
+ "Nodes {} are in {} clusters ({}); add --cluster",
|
|
|
|
|
+ nodes.join(", "),
|
|
|
|
|
+ n,
|
|
|
|
|
+ matches
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(|c| c.name.as_str())
|
|
|
|
|
+ .collect::<Vec<_>>()
|
|
|
|
|
+ .join(", ")
|
|
|
|
|
+ ),
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
/// Stop the recipe's containers across `nodes` (all cluster nodes when the
|
|
/// Stop the recipe's containers across `nodes` (all cluster nodes when the
|
|
|
/// list is empty) and verify they are really gone on EVERY node: each node
|
|
/// list is empty) and verify they are really gone on EVERY node: each node
|
|
|
/// runs the verifying remote command, all nodes are attempted even when a
|
|
/// runs the verifying remote command, all nodes are attempted even when a
|
|
@@ -123,8 +154,14 @@ pub async fn handle(
|
|
|
power_off: bool,
|
|
power_off: bool,
|
|
|
dry_run: bool,
|
|
dry_run: bool,
|
|
|
) -> Result<(), anyhow::Error> {
|
|
) -> Result<(), anyhow::Error> {
|
|
|
- let (cluster, nodes): (crate::config::Cluster, Vec<String>) = if !nodes.is_empty() {
|
|
|
|
|
- let cluster = crate::resolve_cluster_for_op(config, recipe_name, cluster_name).await?;
|
|
|
|
|
|
|
+ let (cluster, nodes): (crate::config::Cluster, Vec<String>) = if !cluster_name.is_empty() && cluster_name != "auto" {
|
|
|
|
|
+ // Explicit cluster selection (exact or prefix): stop that cluster's
|
|
|
|
|
+ // deployment directly — no discovery, no multi-deployment ambiguity.
|
|
|
|
|
+ let cluster = crate::config::select_cluster_by_name(config, cluster_name)?;
|
|
|
|
|
+ (cluster, nodes.to_vec())
|
|
|
|
|
+ } else if !nodes.is_empty() {
|
|
|
|
|
+ // --node without --cluster: find the cluster that contains the node.
|
|
|
|
|
+ let cluster = cluster_for_node(config, nodes)?;
|
|
|
(cluster, nodes.to_vec())
|
|
(cluster, nodes.to_vec())
|
|
|
} else {
|
|
} else {
|
|
|
let variants =
|
|
let variants =
|