2026-06-22 18:24:59 +02:00
package meta
import (
"context"
"os"
"os/exec"
"syscall"
2026-06-22 19:15:06 +02:00
"nadir/internal/config"
2026-06-22 18:24:59 +02:00
"nadir/internal/oscmd"
"github.com/danielgtaylor/huma/v2"
)
2026-06-22 22:22:36 +02:00
// RegisterUpdate wires POST /api/update. It runs the equivalent of
2026-06-22 18:24:59 +02:00
// `sudo nadir update` in a detached session and returns 202 immediately; the
// systemctl restart that ends the updater drops in-flight connections, so the
// caller should poll /api/health to confirm the new version is up.
//
2026-06-22 22:22:36 +02:00
// configPath is re-read by the handler so a missing release_repo (or any other
// config error) surfaces as 4xx/5xx to the caller, not as stderr only.
//
2026-06-22 18:24:59 +02:00
// Authorization: requires (meta, root). Only roles with a wildcard grant
// (the default admin role) match, since "meta" isn't a real module with a
// declared permission vocabulary.
2026-06-22 22:22:36 +02:00
func RegisterUpdate ( api huma . API , configPath string ) {
2026-06-22 18:24:59 +02:00
huma . Register ( api , huma . Operation {
OperationID : "meta-update" ,
Method : "POST" ,
Path : "/api/update" ,
Summary : "Update nadir to the latest release" ,
Description : "Equivalent to running `sudo nadir update` on the host: queries server.release_repo for the latest release, downloads the binary matching the host's architecture, atomically replaces the running binary, and restarts the systemd unit. Returns 202 immediately; the service restart drops in-flight connections, so poll /api/health to confirm the new version is up. Requires the wildcard admin role." ,
Tags : [] string { "Meta" },
Metadata : map [ string ] any { "module" : "meta" , "permission" : "root" },
2026-06-22 19:15:06 +02:00
Errors : [] int { 400 , 401 , 403 , 500 },
2026-06-22 18:24:59 +02:00
DefaultStatus : 202 ,
}, func ( ctx context . Context , _ * struct {}) ( * oscmd . StatusOutput , error ) {
2026-06-22 22:22:36 +02:00
if configPath != "" {
cfg , err := config . Load ( configPath )
2026-06-22 19:15:06 +02:00
if err != nil {
return nil , huma . Error500InternalServerError ( "config load failed" , err )
}
if cfg . Server . ReleaseRepo == "" {
2026-06-22 22:22:36 +02:00
return nil , huma . Error400BadRequest ( "server.release_repo not set in " + configPath )
2026-06-22 19:15:06 +02:00
}
}
2026-06-22 18:24:59 +02:00
exe , err := os . Executable ()
if err != nil {
return nil , huma . Error500InternalServerError ( "could not resolve own binary path" , err )
}
cmd := exec . Command ( exe , "update" )
// Detach from the server's process group so `systemctl restart nadir`
// (the final step of `nadir update`) doesn't kill its own updater.
cmd . SysProcAttr = & syscall . SysProcAttr { Setsid : true }
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
if err := cmd . Start (); err != nil {
return nil , huma . Error500InternalServerError ( "could not start updater" , err )
}
return oscmd . OK (), nil
})
}