Просмотр исходного кода

feat: remote build via tmux (disconnect-safe, polling)

Alexander Korolev 3 месяцев назад
Родитель
Сommit
c04454ce6d
2 измененных файлов с 85 добавлено и 17 удалено
  1. 81 0
      src/docker/build.rs
  2. 4 17
      src/docker/mod.rs

+ 81 - 0
src/docker/build.rs

@@ -0,0 +1,81 @@
+use crate::config::{Config, Node};
+use crate::ssh::run_ssh;
+use anyhow::Result;
+use tracing::info;
+
+/// Start a Docker image build on a remote node in a tmux session.
+/// Polls every 30s, streams log output, and returns when the build completes.
+pub async fn build_image_on_node(
+    config: &Config,
+    node: &Node,
+    local_dir: &std::path::Path,
+    recipe_name: &str,
+    image_tag: &str,
+) -> Result<()> {
+    let session_name = format!("glm52-build-{}", recipe_name);
+    let logfile = format!("/tmp/{}-build.log", recipe_name);
+    let remote_dir = format!("/tmp/{}-build", recipe_name);
+
+    info!(
+        "Building image '{}' on '{}' (tmux: {})...",
+        image_tag, node.name, session_name
+    );
+    crate::ssh::upload_dir(config, node, local_dir, &remote_dir, false).await?;
+
+    let tmux_cmd = format!(
+        "tmux new-session -d -s '{}' -c {} \"TAG={} bash build.sh 2>&1 | tee {}\"",
+        session_name, remote_dir, image_tag, logfile
+    );
+    run_ssh(config, node, tmux_cmd, false).await?;
+
+    let start = std::time::Instant::now();
+    loop {
+        tokio::time::sleep(std::time::Duration::from_secs(30)).await;
+        let elapsed = start.elapsed();
+
+        let alive = run_ssh(
+            config,
+            node,
+            format!("tmux has-session -t '{}' 2>&1 || echo DEAD", session_name),
+            false,
+        )
+        .await?;
+
+        if let Ok(logs) = run_ssh(
+            config,
+            node,
+            format!("tail -10 {} 2>/dev/null || echo '(no log yet)'", logfile),
+            false,
+        )
+        .await
+        {
+            for line in logs.lines().filter(|l| !l.is_empty()) {
+                info!("  [build] {}", line.trim());
+            }
+        }
+
+        if alive.trim().contains("DEAD") || alive.trim().is_empty() {
+            let exit_log = run_ssh(config, node, format!("tail -5 {}", logfile), false)
+                .await
+                .unwrap_or_default();
+            if exit_log.contains(&format!("Done: {}", image_tag)) {
+                info!(
+                    "Image '{}' built on '{}' ({}m)",
+                    image_tag,
+                    node.name,
+                    elapsed.as_secs() / 60
+                );
+            } else {
+                anyhow::bail!(
+                    "Build on '{}' failed after {}m.\n{}",
+                    node.name,
+                    elapsed.as_secs() / 60,
+                    exit_log
+                );
+            }
+            break;
+        }
+        info!("  Build running for {}m...", elapsed.as_secs() / 60);
+    }
+    Ok(())
+}

+ 4 - 17
src/docker/mod.rs

@@ -1,3 +1,4 @@
+pub mod build;
 pub mod deploy;
 pub mod deploy;
 pub mod health;
 pub mod health;
 pub mod logs;
 pub mod logs;
@@ -112,28 +113,14 @@ pub async fn serve(
             );
             );
             let present = run_ssh(config, build_node, check, false).await?;
             let present = run_ssh(config, build_node, check, false).await?;
             if present.trim().is_empty() {
             if present.trim().is_empty() {
-                info!(
-                    "Building image '{}' on '{}'...",
-                    recipe.docker_image, build_node.name
-                );
-                // Upload build directory.
-                crate::ssh::upload_dir(
+                crate::docker::build::build_image_on_node(
                     config,
                     config,
                     build_node,
                     build_node,
                     local_dir.as_path(),
                     local_dir.as_path(),
-                    &format!("/tmp/{}-build", recipe_name),
-                    false,
+                    recipe_name,
+                    &recipe.docker_image,
                 )
                 )
                 .await?;
                 .await?;
-                let build_cmd = format!(
-                    "cd /tmp/{}-build && TAG={} bash build.sh 2>&1",
-                    recipe_name, recipe.docker_image
-                );
-                crate::ssh::run_ssh_long_running(config, build_node, build_cmd, false).await?;
-                info!(
-                    "Image '{}' built on '{}'",
-                    recipe.docker_image, build_node.name
-                );
             } else {
             } else {
                 info!(
                 info!(
                     "Image '{}' already present on '{}'",
                     "Image '{}' already present on '{}'",