package meta import ( "context" "os" "os/exec" "syscall" "nadir/internal/config" "nadir/internal/oscmd" "github.com/danielgtaylor/huma/v2" ) // RegisterUpdate wires POST /api/update. It runs the equivalent of // `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. // // 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. // // 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. func RegisterUpdate(api huma.API, configPath string) { 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"}, Errors: []int{400, 401, 403, 500}, DefaultStatus: 202, }, func(ctx context.Context, _ *struct{}) (*oscmd.StatusOutput, error) { if configPath != "" { cfg, err := config.Load(configPath) if err != nil { return nil, huma.Error500InternalServerError("config load failed", err) } if cfg.Server.ReleaseRepo == "" { return nil, huma.Error400BadRequest("server.release_repo not set in " + configPath) } } 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 }) }