fix: make web updates survive container restart
Docker Release / Check web and api (push) Waiting to run
Docker Release / Resolve release tag (push) Blocked by required conditions
Docker Release / Build and publish all-in-one (push) Blocked by required conditions
Docker Release / Build and publish api (push) Blocked by required conditions
Docker Release / Build and publish web (push) Blocked by required conditions
Docker Release / Build and publish dovecot (push) Blocked by required conditions
Docker Release / Build and publish postfix (push) Blocked by required conditions
Docker Release / Build and publish rspamd (push) Blocked by required conditions
Docker Release / Create GitHub release (push) Blocked by required conditions
Docker Release / Check web and api (push) Waiting to run
Docker Release / Resolve release tag (push) Blocked by required conditions
Docker Release / Build and publish all-in-one (push) Blocked by required conditions
Docker Release / Build and publish api (push) Blocked by required conditions
Docker Release / Build and publish web (push) Blocked by required conditions
Docker Release / Build and publish dovecot (push) Blocked by required conditions
Docker Release / Build and publish postfix (push) Blocked by required conditions
Docker Release / Build and publish rspamd (push) Blocked by required conditions
Docker Release / Create GitHub release (push) Blocked by required conditions
This commit is contained in:
@@ -81,12 +81,6 @@ func (a *App) handleSystemUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
respondError(w, http.StatusInternalServerError, "failed to back up database")
|
||||
return
|
||||
}
|
||||
if err := a.triggerUpdateService(r.Context()); err != nil {
|
||||
a.log.Error("trigger system update", "error", err)
|
||||
respondError(w, http.StatusBadGateway, "failed to start update")
|
||||
return
|
||||
}
|
||||
|
||||
a.log.Info("system update requested", "user", user.ID, "from", info.CurrentVersion, "to", info.LatestVersion, "backup", backupPath)
|
||||
respondJSON(w, http.StatusAccepted, map[string]any{
|
||||
"ok": true,
|
||||
@@ -94,6 +88,7 @@ func (a *App) handleSystemUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
"targetVersion": info.LatestVersion,
|
||||
"message": "更新已启动,服务会在完成后自动恢复",
|
||||
})
|
||||
a.scheduleUpdateService(info.CurrentVersion, info.LatestVersion)
|
||||
}
|
||||
|
||||
func (a *App) systemVersion(ctx context.Context) (systemVersionInfo, error) {
|
||||
@@ -175,7 +170,7 @@ func (a *App) triggerUpdateService(ctx context.Context) error {
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(a.config().UpdateServiceToken))
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Timeout: 10 * time.Minute,
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
@@ -192,6 +187,18 @@ func (a *App) triggerUpdateService(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) scheduleUpdateService(currentVersion, targetVersion string) {
|
||||
go func() {
|
||||
// Let the accepted response reach the browser before Watchtower replaces this container.
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
if err := a.triggerUpdateService(ctx); err != nil {
|
||||
a.log.Error("run scheduled system update", "error", err, "from", currentVersion, "to", targetVersion)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) backupDatabaseBeforeUpdate(ctx context.Context) (string, error) {
|
||||
backupDir := filepath.Join(a.config().DataDir, "backups")
|
||||
if err := os.MkdirAll(backupDir, 0o700); err != nil {
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSystemVersionAndUpdate(t *testing.T) {
|
||||
@@ -22,6 +24,11 @@ func TestSystemVersionAndUpdate(t *testing.T) {
|
||||
defer releaseServer.Close()
|
||||
|
||||
var updateRequests atomic.Int32
|
||||
updateStarted := make(chan struct{}, 1)
|
||||
releaseUpdate := make(chan struct{})
|
||||
var releaseUpdateOnce sync.Once
|
||||
releaseBlockedUpdate := func() { releaseUpdateOnce.Do(func() { close(releaseUpdate) }) }
|
||||
defer releaseBlockedUpdate()
|
||||
updateServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("update method = %s", r.Method)
|
||||
@@ -30,6 +37,8 @@ func TestSystemVersionAndUpdate(t *testing.T) {
|
||||
t.Errorf("authorization = %q", got)
|
||||
}
|
||||
updateRequests.Add(1)
|
||||
updateStarted <- struct{}{}
|
||||
<-releaseUpdate
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer updateServer.Close()
|
||||
@@ -66,12 +75,44 @@ func TestSystemVersionAndUpdate(t *testing.T) {
|
||||
t.Fatalf("unexpected version response: %+v", version)
|
||||
}
|
||||
|
||||
var update map[string]any
|
||||
if code := admin.do("POST", "/api/admin/system/update", nil, &update); code != http.StatusAccepted {
|
||||
t.Fatalf("update code=%d response=%v", code, update)
|
||||
type updateResponse struct {
|
||||
code int
|
||||
err error
|
||||
}
|
||||
if updateRequests.Load() != 1 {
|
||||
t.Fatalf("update requests=%d", updateRequests.Load())
|
||||
response := make(chan updateResponse, 1)
|
||||
go func() {
|
||||
req, err := http.NewRequest(http.MethodPost, ts.URL+"/api/admin/system/update", nil)
|
||||
if err != nil {
|
||||
response <- updateResponse{err: err}
|
||||
return
|
||||
}
|
||||
req.AddCookie(admin.cookie)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
response <- updateResponse{err: err}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
response <- updateResponse{code: resp.StatusCode}
|
||||
}()
|
||||
select {
|
||||
case result := <-response:
|
||||
if result.err != nil || result.code != http.StatusAccepted {
|
||||
t.Fatalf("update response=%+v", result)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
releaseBlockedUpdate()
|
||||
t.Fatal("update response waited for container replacement")
|
||||
}
|
||||
select {
|
||||
case <-updateStarted:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("scheduled update request did not start")
|
||||
}
|
||||
releaseBlockedUpdate()
|
||||
if got := updateRequests.Load(); got != 1 {
|
||||
t.Fatalf("update requests=%d", got)
|
||||
}
|
||||
backups, err := filepath.Glob(filepath.Join(dir, "backups", "pre-update-*.db"))
|
||||
if err != nil || len(backups) != 1 {
|
||||
|
||||
Reference in New Issue
Block a user