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
+70
View File
@@ -104,6 +104,10 @@ pub fn build_routes(
route = route.at("/api/events", get(events::events_handler).data(buf));
}
route = route
.at("/api/upgrade", post(upgrade_trigger_handler))
.at("/api/huskies-binary", get(serve_binary_handler));
if let Some(wa_ctx) = whatsapp_ctx {
route = route.at(
"/webhook/whatsapp",
@@ -209,6 +213,72 @@ pub fn debug_crdt_handler(req: &poem::Request) -> poem::Response {
.body(serde_json::to_string_pretty(&body).unwrap_or_default())
}
/// `POST /api/upgrade` — trigger a self-update on the running sled.
///
/// Accepts `{"source_url": "http://gateway:3000/api/huskies-binary"}` and
/// spawns the upgrade task in the background, returning 202 immediately.
/// The connection will be dropped when `exec()` replaces the process.
#[poem::handler]
pub async fn upgrade_trigger_handler(
body: poem::web::Json<serde_json::Value>,
ctx: poem::web::Data<&std::sync::Arc<AppContext>>,
) -> poem::Response {
let source_url = match body
.0
.get("source_url")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
{
Some(u) => u,
None => {
return poem::Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("Missing required field: source_url");
}
};
let project_root = ctx.state.get_project_root().unwrap_or_default();
// Spawn upgrade in background so we can return 202 before exec() fires.
tokio::spawn(async move {
if let Err(e) = crate::upgrade::upgrade_and_reexec(&source_url, &project_root).await {
crate::slog!("[upgrade] Upgrade failed: {e}");
}
});
poem::Response::builder()
.status(StatusCode::ACCEPTED)
.body("Upgrade triggered. The sled will re-exec momentarily.")
}
/// `GET /api/huskies-binary` — serve the running binary so peer sleds can download it.
///
/// Streams `current_exe()` (the binary that is currently running) as an
/// `application/octet-stream` download. Returns 500 if the path cannot be
/// resolved or read.
#[poem::handler]
pub async fn serve_binary_handler() -> poem::Response {
let exe = match std::env::current_exe() {
Ok(p) => p,
Err(e) => {
return poem::Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(format!("Cannot resolve current executable: {e}"));
}
};
match tokio::fs::read(&exe).await {
Ok(bytes) => poem::Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/octet-stream")
.header("Content-Disposition", "attachment; filename=\"huskies\"")
.body(bytes),
Err(e) => poem::Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(format!("Cannot read binary at {}: {e}", exe.display())),
}
}
#[cfg(test)]
mod tests {
use super::*;