feat(update): 支持 Docker Compose 页面一键升级并发布 v1.3.0

This commit is contained in:
2026-09-27 18:43:34 +08:00
parent 32dfe3b6de
commit 163df0c5b0
29 changed files with 1541 additions and 10 deletions
+2
View File
@@ -17,6 +17,7 @@ import (
"github.com/fonu/fonu/internal/service"
"github.com/fonu/fonu/internal/settings"
"github.com/fonu/fonu/internal/traffic"
"github.com/fonu/fonu/internal/updater"
)
type Deps struct {
@@ -34,6 +35,7 @@ type Deps struct {
FRPMulti *frp.Service
Traffic *traffic.Collector
Notify *notify.Service
Updater *updater.Client
StaticFS fs.FS
StartedAt string
}
+2 -1
View File
@@ -74,7 +74,8 @@ func tokenDeniedPath(path string) bool {
if path == "/api/auth/password" || path == "/api/auth/logout" {
return true
}
return strings.HasPrefix(path, "/api/settings/tokens")
return strings.HasPrefix(path, "/api/settings/tokens") ||
strings.HasPrefix(path, "/api/system/update")
}
func sessionIDFromContext(ctx context.Context) string {
+4
View File
@@ -27,6 +27,7 @@ func NewRouter(deps Deps) http.Handler {
metricsHandler: NewMetricsHandler(deps.Metrics),
discoveryHandler: NewDiscoveryHandler(deps.Discovery),
frpmultiHandler: NewFRPMultiHandler(deps.FRPMulti),
updateHandler: NewUpdateHandler(deps.Updater),
authSvc: deps.Auth,
staticFS: deps.StaticFS,
}
@@ -94,6 +95,8 @@ func NewRouter(deps Deps) http.Handler {
protect("GET /api/logs/system", r.logsHandler.System)
protect("GET /api/metrics/history", r.metricsHandler.History)
protect("GET /api/logs/stream", r.logsHandler.Stream)
protect("GET /api/system/update/status", r.updateHandler.Status)
protect("POST /api/system/update/apply", r.updateHandler.Apply)
protect("GET /api/backup/export", r.backupHandler.Export)
protect("POST /api/backup/restore", r.backupHandler.Restore)
protect("GET /api/backup/archives", r.backupHandler.ListArchives)
@@ -190,6 +193,7 @@ type Router struct {
metricsHandler *MetricsHandler
discoveryHandler *DiscoveryHandler
frpmultiHandler *FRPMultiHandler
updateHandler *UpdateHandler
authSvc *auth.Service
staticFS fs.FS
}
+45
View File
@@ -0,0 +1,45 @@
package api
import (
"net/http"
"github.com/fonu/fonu/internal/updater"
)
type UpdateHandler struct {
client *updater.Client
}
func NewUpdateHandler(client *updater.Client) *UpdateHandler {
return &UpdateHandler{client: client}
}
func (h *UpdateHandler) Status(w http.ResponseWriter, r *http.Request) {
if h.client == nil || !h.client.Enabled() {
writeJSON(w, http.StatusOK, updater.Status{
Enabled: false,
Phase: updater.PhaseIdle,
Message: "当前部署未启用一键升级",
})
return
}
status, err := h.client.Status(r.Context())
if err != nil {
writeError(r, w, http.StatusBadGateway, "读取升级状态失败", err)
return
}
writeJSON(w, http.StatusOK, status)
}
func (h *UpdateHandler) Apply(w http.ResponseWriter, r *http.Request) {
if h.client == nil || !h.client.Enabled() {
writeError(r, w, http.StatusServiceUnavailable, "当前部署未启用一键升级")
return
}
status, err := h.client.Apply(r.Context())
if err != nil {
writeError(r, w, http.StatusBadGateway, "启动升级失败", err)
return
}
writeJSON(w, http.StatusAccepted, status)
}
+29
View File
@@ -0,0 +1,29 @@
package api
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestUpdateHandlerDisabled(t *testing.T) {
handler := NewUpdateHandler(nil)
statusRecorder := httptest.NewRecorder()
statusReq := httptest.NewRequest(http.MethodGet, "/api/system/update/status", nil)
handler.Status(statusRecorder, statusReq)
if statusRecorder.Code != http.StatusOK {
t.Fatalf("状态接口期望 200,实际 %d", statusRecorder.Code)
}
if body := statusRecorder.Body.String(); !strings.Contains(body, `"enabled":false`) {
t.Fatalf("未返回 enabled=false: %s", body)
}
applyRecorder := httptest.NewRecorder()
applyReq := httptest.NewRequest(http.MethodPost, "/api/system/update/apply", nil)
handler.Apply(applyRecorder, applyReq)
if applyRecorder.Code != http.StatusServiceUnavailable {
t.Fatalf("升级接口期望 503,实际 %d", applyRecorder.Code)
}
}