Parcourir la source

fix: stop --cluster narrows multi-deployment (no more 'use --node' when --cluster given); --node without --cluster resolves via node containment. prebuilt images (no docker_image_dir) are presence-checked + node-to-node synced before docker run — no more silent Docker Hub pull + false 'started'

Alexander Korolev il y a 3 semaines
Parent
commit
89c21cc236
2 fichiers modifiés avec 56 ajouts et 2 suppressions
  1. 17 0
      src/docker/mod.rs
  2. 39 2
      src/docker/stop.rs

+ 17 - 0
src/docker/mod.rs

@@ -256,6 +256,23 @@ pub async fn serve(
         let source = built_on.as_deref().or(opts.sync_image_source.as_deref());
         self::sync::sync_image_to_nodes(config, &cluster, &recipe.docker_image, source).await?;
     }
+    // Prebuilt external images (no `docker_image_dir`) are not built by the
+    // launcher — verify the image exists on the launch nodes BEFORE any
+    // `docker run`: a missing image would make docker silently pull it from
+    // the registry over the internet, and the SSH timeout would kill the
+    // pull while the serve still reports "started" and waits out a 30-min
+    // readiness timeout for a container that never loads. Sync it node-to-
+    // node from a peer that has it; if no node has it, fail with a hint.
+    if !opts.dry_run && recipe.docker_image_dir.is_none() {
+        self::sync::sync_image_to_nodes(config, &cluster, &recipe.docker_image, None)
+            .await
+            .map_err(|e| {
+                anyhow::anyhow!(
+                    "image '{}' not available on the launch nodes: {e} \u{2014} pull it on one node first (or build it with --build-node)",
+                    recipe.docker_image
+                )
+            })?;
+    }
 
     // Drop page cache on all nodes right before starting containers.
     // Must run after image sync (docker load fills page cache).

+ 39 - 2
src/docker/stop.rs

@@ -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
 /// 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
@@ -123,8 +154,14 @@ pub async fn handle(
     power_off: bool,
     dry_run: bool,
 ) -> 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())
     } else {
         let variants =