huskies: merge 1138 story In-container huskies self-update — huskies upgrade pulls a fresh binary without docker rebuild

This commit is contained in:
dave
2026-05-18 13:28:53 +00:00
parent d10634c7d6
commit 0ec5c05de8
6 changed files with 590 additions and 2 deletions
+60
View File
@@ -49,6 +49,8 @@ pub mod sled_uplink;
mod startup;
mod state;
mod store;
/// In-container binary self-update — fetch, atomic replace, and re-exec.
pub mod upgrade;
/// Validated input layer — transport-agnostic newtypes and request structs for all MCP write tools.
pub mod validation;
mod workflow;
@@ -72,6 +74,19 @@ mod cli;
use cli::{parse_cli_args, resolve_path_arg};
/// Convert a WebSocket gateway URL into the binary download HTTP URL.
///
/// `ws://gateway:3000/api/sled-uplink?token=x` → `http://gateway:3000/api/huskies-binary`
fn derive_binary_url_from_ws(ws_url: &str) -> Option<String> {
let http = ws_url
.strip_prefix("wss://")
.map(|s| format!("https://{s}"))
.or_else(|| ws_url.strip_prefix("ws://").map(|s| format!("http://{s}")))?;
// Strip any path and query string, then append the binary endpoint.
let base = http.split('/').take(3).collect::<Vec<_>>().join("/");
Some(format!("{base}/api/huskies-binary"))
}
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
// Reap zombie grandchildren on Unix (for native deployments without tini/init).
@@ -145,6 +160,27 @@ async fn main() -> Result<(), std::io::Error> {
}
}
// ── Upgrade mode: fetch new binary, replace, exit ───────────────────────
if cli.upgrade {
let source = cli
.upgrade_source
.clone()
.or_else(|| std::env::var("HUSKIES_BINARY_SOURCE").ok())
.unwrap_or_else(|| {
// Derive from HUSKIES_UPSTREAM_GATEWAY: ws://host:port/... → http://host:port/api/huskies-binary
std::env::var("HUSKIES_UPSTREAM_GATEWAY")
.ok()
.and_then(|ws| derive_binary_url_from_ws(&ws))
.unwrap_or_else(|| "http://gateway:3000/api/huskies-binary".to_string())
});
let target = upgrade::resolve_target_path();
if let Err(e) = upgrade::run_cli_upgrade(&source, &target).await {
eprintln!("error: {e}");
std::process::exit(1);
}
return Ok(());
}
// ── Gateway mode: multi-project proxy ────────────────────────────────────
if is_gateway {
let config_dir = explicit_path.unwrap_or_else(|| cwd.clone());
@@ -464,4 +500,28 @@ name = "coder"
config::ProjectConfig::load(tmp.path())
.unwrap_or_else(|e| panic!("Invalid project.toml: {e}"));
}
#[test]
fn derive_binary_url_strips_ws_scheme_and_path() {
let url = derive_binary_url_from_ws("ws://gateway:3000/api/sled-uplink?token=abc");
assert_eq!(
url.as_deref(),
Some("http://gateway:3000/api/huskies-binary")
);
}
#[test]
fn derive_binary_url_handles_wss_scheme() {
let url = derive_binary_url_from_ws("wss://myhost:443/path");
assert_eq!(
url.as_deref(),
Some("https://myhost:443/api/huskies-binary")
);
}
#[test]
fn derive_binary_url_invalid_scheme_returns_none() {
let url = derive_binary_url_from_ws("http://not-a-ws-url");
assert!(url.is_none());
}
}