Skip to main content

max / makenotwork

2.8 KB · 72 lines History Blame Raw
1 //! Atomic symlink-swap deploys.
2 //!
3 //! Layout on every target (MM, A nodes, B nodes, ...):
4 //!
5 //! <release_root>/
6 //! releases/
7 //! 0.8.1/
8 //! server <- the binary
9 //! 0.8.2/
10 //! server
11 //! current -> releases/0.8.2
12 //!
13 //! `ln -sfn` makes the swap atomic on Linux. systemd units should point at
14 //! `<release_root>/current/server` so a swap + reload picks up the new binary
15 //! without a window where the unit references a missing path.
16 //!
17 //! v0 only implements local deploys (used for MM and for localhost-dev
18 //! "remote" nodes whose ssh_target is `local`). Real SSH/rsync deploys are
19 //! follow-up work — see the `remote_deploy_stub` branch.
20
21 use crate::topology::Node;
22 use anyhow::{Context, Result};
23 use std::path::{Path, PathBuf};
24 use tokio::process::Command;
25
26 pub async fn deploy_local(release_root: &Path, version: &str, binary: &Path) -> Result<PathBuf> {
27 let release_dir = release_root.join("releases").join(version);
28 tokio::fs::create_dir_all(&release_dir).await?;
29 let dest = release_dir.join("server");
30 tokio::fs::copy(binary, &dest)
31 .await
32 .with_context(|| format!("copy {} -> {}", binary.display(), dest.display()))?;
33
34 let current = release_root.join("current");
35 // ln -sfn is atomic on Linux; on macOS the dev path is non-prod so the
36 // race is irrelevant. We shell out rather than using std::os::unix::fs
37 // symlink + rename because the rename-over-symlink pattern is platform-fussy.
38 let target = format!("releases/{version}");
39 let out = Command::new("ln")
40 .args(["-sfn", &target])
41 .arg(&current)
42 .output()
43 .await?;
44 anyhow::ensure!(
45 out.status.success(),
46 "symlink swap failed: {}",
47 String::from_utf8_lossy(&out.stderr),
48 );
49 Ok(release_dir)
50 }
51
52 pub async fn deploy_node(node: &Node, version: &str, binary: &Path) -> Result<PathBuf> {
53 if node.ssh_target == "local" || node.ssh_target.is_empty() {
54 return deploy_local(Path::new(&node.release_root), version, binary).await;
55 }
56 remote_deploy_stub(node, version, binary).await
57 }
58
59 async fn remote_deploy_stub(node: &Node, version: &str, _binary: &Path) -> Result<PathBuf> {
60 // Real implementation: rsync the binary to <ssh_target>:<release_root>/releases/<version>/server,
61 // then ssh <ssh_target> "ln -sfn releases/<version> current && systemctl reload-or-restart <unit>".
62 // Wiring this up needs a story for systemd unit naming and ssh key/auth conventions; deferring
63 // until the localhost smoke loop is settled and we know which knobs matter.
64 anyhow::bail!(
65 "remote deploy not yet implemented (node {} -> {}); use ssh_target=local for dev",
66 node.name,
67 node.ssh_target,
68 );
69 #[allow(unreachable_code)]
70 Ok(PathBuf::from(&node.release_root).join("releases").join(version))
71 }
72