1 Commits

Author SHA1 Message Date
urania fff43a5ab6 fix: auto-updates
build-and-release / release (push) Successful in 2m6s
2026-06-22 18:24:59 +02:00
5 changed files with 90 additions and 2 deletions
+15
View File
@@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Bash(go get *)",
"Bash(go build *)",
"Bash(go vet *)",
"Read(//usr/lib/**)",
"Read(//proc/**)",
"Bash(systemctl show *)",
"Bash(echo \"exit=$?\")",
"Bash(systemctl list-units *)",
"Bash(go test *)"
]
}
}
+1
View File
@@ -232,6 +232,7 @@ func runServer() {
meta.Register(api, mods) meta.Register(api, mods)
meta.RegisterHealth(api, sessions) meta.RegisterHealth(api, sessions)
meta.RegisterWhoami(api, sessions, roles, mods) meta.RegisterWhoami(api, sessions, roles, mods)
meta.RegisterUpdate(api)
auth.RegisterLogin(api, sessions, auditStore, cfg.SecureCookie()) auth.RegisterLogin(api, sessions, auditStore, cfg.SecureCookie())
auth.RegisterLogout(api, sessions, cfg.SecureCookie()) auth.RegisterLogout(api, sessions, cfg.SecureCookie())
+2 -1
View File
@@ -425,7 +425,8 @@ Usage:
nadir token add <name> Mint a machine credential (Bearer token), shown once nadir token add <name> Mint a machine credential (Bearer token), shown once
nadir token rm <name> Revoke a token (effective immediately, no restart) nadir token rm <name> Revoke a token (effective immediately, no restart)
nadir token ls List token names and when they were created nadir token ls List token names and when they were created
nadir update Fetch the latest release from server.release_repo and restart nadir update [--check|--force] Fetch the latest release from server.release_repo and restart
(--check: report only; --force: re-download when already current)
nadir help Show this help nadir help Show this help
Most commands need root. Config path is specified via -f/--config or CONFIG_PATH (default ~/.config/config.yaml). Most commands need root. Config path is specified via -f/--config or CONFIG_PATH (default ~/.config/config.yaml).
+23 -1
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"flag"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -12,6 +13,7 @@ import (
"strings" "strings"
"time" "time"
"nadir"
"nadir/internal/config" "nadir/internal/config"
) )
@@ -19,7 +21,12 @@ import (
// releases/latest, pick the asset for the host's GOARCH, atomically replace // releases/latest, pick the asset for the host's GOARCH, atomically replace
// /usr/local/bin/nadir (or wherever the running binary lives), and restart // /usr/local/bin/nadir (or wherever the running binary lives), and restart
// the systemd unit so the new code takes effect. // the systemd unit so the new code takes effect.
func updateCmd(_ []string) error { func updateCmd(args []string) error {
fs := flag.NewFlagSet("update", flag.ExitOnError)
check := fs.Bool("check", false, "report the latest version without downloading")
force := fs.Bool("force", false, "re-download even when already at the latest version")
fs.Parse(args)
configPath, err := resolveConfigPath() configPath, err := resolveConfigPath()
if err != nil { if err != nil {
return err return err
@@ -44,6 +51,21 @@ func updateCmd(_ []string) error {
return err return err
} }
fmt.Printf("current: %s\nlatest: %s\n", nadir.Version, rel.TagName)
upToDate := nadir.Version == rel.TagName
switch {
case *check:
if upToDate {
fmt.Println("already up to date.")
} else {
fmt.Println("update available; run `nadir update` to install.")
}
return nil
case upToDate && !*force:
fmt.Println("already up to date; pass --force to re-download.")
return nil
}
var assetURL, assetName string var assetURL, assetName string
for _, a := range rel.Assets { for _, a := range rel.Assets {
if strings.HasSuffix(a.Name, suffix) { if strings.HasSuffix(a.Name, suffix) {
+49
View File
@@ -0,0 +1,49 @@
package meta
import (
"context"
"os"
"os/exec"
"syscall"
"nadir/internal/oscmd"
"github.com/danielgtaylor/huma/v2"
)
// RegisterUpdate wires POST /api/meta/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.
//
// 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) {
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{401, 403, 500},
DefaultStatus: 202,
}, func(ctx context.Context, _ *struct{}) (*oscmd.StatusOutput, error) {
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
})
}