Browse Source

fix: sync_image_to_nodes selects most recent image (by creation timestamp)

Alexander Korolev 3 months ago
parent
commit
e89fe5d869
1 changed files with 42 additions and 12 deletions
  1. 42 12
      src/docker/sync.rs

+ 42 - 12
src/docker/sync.rs

@@ -1,8 +1,8 @@
 use crate::config::{Cluster, Config, Node};
 use crate::ssh::{run_ssh, run_ssh_long_running};
 use anyhow::Result;
-
 use tracing::info;
+
 /// Get the Docker image digest (short ID) from a node.
 async fn get_image_digest(config: &Config, node: &Node, image: &str) -> Result<Option<String>> {
     let cmd = format!("docker image ls --format '{{{{.ID}}}}' {image}");
@@ -15,20 +15,38 @@ async fn get_image_digest(config: &Config, node: &Node, image: &str) -> Result<O
     }
 }
 
+/// Get the Docker image creation timestamp.
+async fn get_image_created(config: &Config, node: &Node, image: &str) -> Option<String> {
+    let cmd = format!("docker image inspect --format '{{{{.Created}}}}' {image} 2>/dev/null");
+    run_ssh(config, node, cmd, false)
+        .await
+        .ok()
+        .map(|c| c.trim().to_string())
+}
+
 /// Sync Docker image across cluster nodes.
-/// Probes all nodes, finds a source (explicit or first with image), then pipes
-/// `docker save | ssh docker load` to every node missing or with a different digest.
+/// Finds the node with the *most recently built* image (by creation timestamp),
+/// then pipes `docker save | ssh docker load` to nodes missing or with a different digest.
 pub async fn sync_image_to_nodes(
     config: &Config,
     cluster: &Cluster,
     image: &str,
     explicit_source: Option<&str>,
 ) -> Result<()> {
+    // Collect digests and creation timestamps per node.
     let mut digests: Vec<(&Node, Option<String>)> = Vec::new();
+    let mut created: Vec<Option<String>> = Vec::new();
     for node in &cluster.nodes {
         let d = get_image_digest(config, node, image).await?;
+        let c = if d.is_some() {
+            get_image_created(config, node, image).await
+        } else {
+            None
+        };
         digests.push((node, d));
+        created.push(c);
     }
+
     let source_idx = if let Some(src_name) = explicit_source {
         digests
             .iter()
@@ -37,19 +55,31 @@ pub async fn sync_image_to_nodes(
                 anyhow::anyhow!("sync-image-source '{}' not found in cluster", src_name)
             })?
     } else {
-        digests
+        let mut indexed: Vec<usize> = digests
             .iter()
-            .position(|(_, d)| d.is_some())
-            .ok_or_else(|| anyhow::anyhow!("Image '{}' not found on any cluster node", image))?
+            .enumerate()
+            .filter(|(_, (_, d))| d.is_some())
+            .map(|(i, _)| i)
+            .collect();
+        indexed.sort_by(|&a, &b| {
+            created[b]
+                .as_deref()
+                .unwrap_or("")
+                .cmp(&created[a].as_deref().unwrap_or(""))
+        });
+        *indexed
+            .first()
+            .ok_or_else(|| anyhow::anyhow!("Image '{}' not found on any node", image))?
     };
-    let (source, source_digest_opt) = &digests[source_idx];
-    let source_digest = source_digest_opt.as_ref().unwrap();
+
+    let (src_node, src_digest_opt) = &digests[source_idx];
+    let source_digest = src_digest_opt.as_ref().unwrap();
     info!(
-        "Syncing image '{}' from '{}' to peers...",
-        image, source.name
+        "Syncing image '{}' from '{}' (most recent build) to peers...",
+        image, src_node.name
     );
     for (target, target_digest) in &digests {
-        if target.name == source.name {
+        if target.name == src_node.name {
             continue;
         }
         if target_digest.as_deref() == Some(source_digest) {
@@ -64,7 +94,7 @@ pub async fn sync_image_to_nodes(
             config.ssh_user,
             target.ssh_ip
         );
-        run_ssh_long_running(config, source, pipe_cmd, false).await?;
+        run_ssh_long_running(config, src_node, pipe_cmd, false).await?;
         let verify = get_image_digest(config, target, image).await?;
         if verify.as_deref() != Some(source_digest) {
             anyhow::bail!(