Add self-update, including in Docker
Sandbar can install new releases itself using Reef's selfupdate, automatically with SANDBAR_AUTO_UPDATE=1 or from a button on the Account page. In a container the new binary is written to <data>/bin and takes over from the image at start, until the image catches up. Tags now publish a Gitea release with archives and SHA256SUMS as well as the image. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
955e01333d
commit
73f45fb3ae
14 files changed
+610
-20
No files matched your search
@@ -0,0 +1,109 @@
|
||||
package sandbar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersionNewer(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
a, b string
|
||||
want bool
|
||||
}{
|
||||
{"v0.1.4", "v0.1.3", true},
|
||||
{"v0.2.0", "v0.1.9", true},
|
||||
{"v1.0.0", "v0.9.9", true},
|
||||
{"v0.1.3", "v0.1.3", false},
|
||||
{"v0.1.2", "v0.1.3", false},
|
||||
{"v0.1.10", "v0.1.9", true},
|
||||
{"v0.2.0-rc1", "v0.1.0", false}, // pre-releases are never offered
|
||||
{"v0.2.0", "dev", false}, // a development build never updates
|
||||
{"dev", "v0.1.0", false},
|
||||
} {
|
||||
if got := versionNewer(c.a, c.b); got != c.want {
|
||||
t.Errorf("versionNewer(%q, %q) = %v, want %v", c.a, c.b, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The update page asks the release server and reports whether it has something newer.
|
||||
func TestUpdateStatus(t *testing.T) {
|
||||
release := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/repos/Veillax/sandbar/releases/latest" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Write([]byte(`{"tag_name":"v0.2.0","assets":[]}`))
|
||||
}))
|
||||
defer release.Close()
|
||||
|
||||
s, _, _ := newTestServer(t)
|
||||
s.opts.Version, s.opts.UpdateServer = "v0.1.0", release.URL
|
||||
ts := httptest.NewServer(s.Handler())
|
||||
defer ts.Close()
|
||||
c := newAPIClient(t, ts)
|
||||
|
||||
if code := c.do("GET", "/api/v1/system/update", nil, nil); code != 401 {
|
||||
t.Fatalf("signed out: %d", code)
|
||||
}
|
||||
c.setup()
|
||||
var st map[string]any
|
||||
if code := c.do("GET", "/api/v1/system/update", nil, &st); code != 200 {
|
||||
t.Fatalf("status: %d", code)
|
||||
}
|
||||
if st["current"] != "v0.1.0" || st["latest"] != "v0.2.0" || st["newer"] != true {
|
||||
t.Fatalf("status: %v", st)
|
||||
}
|
||||
if st["error"] != "" {
|
||||
t.Fatalf("unexpected error: %v", st["error"])
|
||||
}
|
||||
|
||||
// A development build can look but not install.
|
||||
s.opts.Version = "dev"
|
||||
s.upd.checked = s.upd.checked.AddDate(-1, 0, 0)
|
||||
if code := c.do("POST", "/api/v1/system/update", map[string]string{}, &st); code != 400 {
|
||||
t.Fatalf("a development build must not install: %d %v", code, st)
|
||||
}
|
||||
}
|
||||
|
||||
// A newer binary on the data volume takes over from the image, and a stale one is removed.
|
||||
func TestOverlay(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("self-update is not supported on Windows")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
bin := filepath.Join(dir, "bin")
|
||||
os.MkdirAll(bin, 0o755)
|
||||
overlay := filepath.Join(bin, "sandbar")
|
||||
script := func(version string) {
|
||||
os.WriteFile(overlay, []byte("#!/bin/sh\necho 'sandbar "+version+"'\n"), 0o755)
|
||||
}
|
||||
var logged []string
|
||||
logf := func(f string, a ...any) { logged = append(logged, f) }
|
||||
|
||||
// An older overlay is deleted once the image is a release that has caught up.
|
||||
script("v0.1.0")
|
||||
UseNewerOverlay(dir, "v0.1.5", logf)
|
||||
if _, err := os.Stat(overlay); err == nil {
|
||||
t.Fatal("a stale overlay should be removed")
|
||||
}
|
||||
|
||||
// A development image never deletes or switches to anything.
|
||||
script("v0.9.0")
|
||||
UseNewerOverlay(dir, "dev", logf)
|
||||
if _, err := os.Stat(overlay); err != nil {
|
||||
t.Fatal("a development build must leave the overlay alone")
|
||||
}
|
||||
|
||||
// Something that does not report a version is ignored, not run.
|
||||
os.WriteFile(overlay, []byte("#!/bin/sh\necho hello\n"), 0o755)
|
||||
UseNewerOverlay(dir, "v0.1.5", logf)
|
||||
if len(logged) == 0 || !strings.Contains(logged[len(logged)-1], "did not report a version") {
|
||||
t.Fatalf("log: %v", logged)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user