From 782304012c26b2f178ad7ec9f0a4de2a3dd1d13d Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 18 Mar 2026 23:11:48 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=8A=9F=E8=83=BD]=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E5=81=A5=E5=BA=B7=E4=BA=8B=E4=BB=B6=E6=B8=85?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E8=A7=82=E6=B5=8B=E6=95=B0=E6=8D=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design.md | 1 + docs/development-guidelines.md | 1 + openflare_agent/internal/state/state.go | 3 + openflare_agent/internal/sync/service.go | 85 +- openflare_agent/internal/sync/service_test.go | 165 ++ openflare_server/controller/node.go | 24 + openflare_server/docs/docs.go | 1330 ++++++++++++++--- openflare_server/docs/swagger.json | 1330 ++++++++++++++--- openflare_server/docs/swagger.yaml | 762 +++++++++- openflare_server/model/node_health_event.go | 5 + openflare_server/router/api-router.go | 1 + openflare_server/router/api_phase2_test.go | 29 + .../service/node_observability.go | 20 + openflare_server/service/node_update_test.go | 60 + .../web/features/nodes/api/nodes.ts | 9 + .../nodes/components/node-detail-page.tsx | 81 + 16 files changed, 3473 insertions(+), 433 deletions(-) diff --git a/docs/design.md b/docs/design.md index 56c201a0..70cfd06a 100644 --- a/docs/design.md +++ b/docs/design.md @@ -173,6 +173,7 @@ Origin * 主配置、路由配置、证书与 Lua 资源写入 * 执行 `openresty -t` / `openresty -s reload` * 失败回滚 +* 对已失败并回退的目标版本做本地熔断,直到控制面出现新的激活版本 * 节点观测采集与结果上报 ### 7.3 `openflare_server/web` diff --git a/docs/development-guidelines.md b/docs/development-guidelines.md index dee55f60..b7dd1234 100644 --- a/docs/development-guidelines.md +++ b/docs/development-guidelines.md @@ -192,6 +192,7 @@ Agent 必须满足: * 写入新配置后以运行态恢复为目标执行激活,Docker 模式优先重建容器并确认容器保持运行 * 新配置激活失败时必须先尝试用目标配置恢复运行,再回滚到旧配置并重新拉起 OpenResty * 回滚后 OpenResty 恢复正常时上报警告;回滚后仍无法恢复运行时上报失败 +* 某个目标 `version + checksum` 一旦应用失败并回退,Agent 必须在本地状态中阻断该目标的重复应用;只有远端激活版本或 checksum 发生变化时,才允许再次尝试 ## 6. 测试与交付要求 diff --git a/openflare_agent/internal/state/state.go b/openflare_agent/internal/state/state.go index 55e5d3a6..10bf68ab 100644 --- a/openflare_agent/internal/state/state.go +++ b/openflare_agent/internal/state/state.go @@ -13,6 +13,9 @@ type Snapshot struct { NodeID string `json:"node_id"` CurrentVersion string `json:"current_version"` CurrentChecksum string `json:"current_checksum"` + BlockedVersion string `json:"blocked_version"` + BlockedChecksum string `json:"blocked_checksum"` + BlockedReason string `json:"blocked_reason"` LastError string `json:"last_error"` OpenrestyStatus string `json:"openresty_status"` OpenrestyMessage string `json:"openresty_message"` diff --git a/openflare_agent/internal/sync/service.go b/openflare_agent/internal/sync/service.go index f8b344ee..1ec58ca9 100644 --- a/openflare_agent/internal/sync/service.go +++ b/openflare_agent/internal/sync/service.go @@ -105,13 +105,27 @@ func (s *Service) sync(ctx context.Context, startup bool, target *protocol.Activ } snapshot.CurrentVersion = target.Version snapshot.CurrentChecksum = target.Checksum + clearBlockedTarget(snapshot) snapshot.LastError = "" slog.Debug("sync finished without changes", "mode", mode, "version", target.Version) return s.stateStore.Save(snapshot) } + if isBlockedTarget(snapshot, target.Version, target.Checksum) { + slog.Warn("skipping blocked config version after previous failed apply", "mode", mode, "version", target.Version, "checksum", target.Checksum) + if startup { + if err = s.ensureRuntimeForCurrentConfig(ctx, mode, snapshot, currentChecksum); err != nil { + return err + } + return s.stateStore.Save(snapshot) + } + return nil + } + if hasBlockedTarget(snapshot) { + clearBlockedTarget(snapshot) + } if snapshot.CurrentVersion == target.Version && snapshot.CurrentChecksum == target.Checksum && !startup { slog.Debug("skipping config fetch because state already records target version/checksum", "version", target.Version, "checksum", target.Checksum) - return nil + return s.stateStore.Save(snapshot) } config, err := s.client.GetActiveConfig(ctx) @@ -139,6 +153,7 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool, } snapshot.CurrentVersion = config.Version snapshot.CurrentChecksum = config.Checksum + clearBlockedTarget(snapshot) snapshot.LastError = "" slog.Debug("sync finished without changes", "mode", mode, "version", config.Version) return s.stateStore.Save(snapshot) @@ -146,9 +161,22 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool, if target != nil && (target.Version != config.Version || target.Checksum != config.Checksum) { slog.Warn("active config changed between heartbeat and fetch", "heartbeat_version", target.Version, "heartbeat_checksum", target.Checksum, "fetched_version", config.Version, "fetched_checksum", config.Checksum) } + if isBlockedTarget(snapshot, config.Version, config.Checksum) { + slog.Warn("skipping blocked config after fetch because the same version previously failed", "mode", mode, "version", config.Version, "checksum", config.Checksum) + if startup { + if err := s.ensureRuntimeForCurrentConfig(ctx, mode, snapshot, currentChecksum); err != nil { + return err + } + return s.stateStore.Save(snapshot) + } + return nil + } + if hasBlockedTarget(snapshot) { + clearBlockedTarget(snapshot) + } if snapshot.CurrentVersion == config.Version && snapshot.CurrentChecksum == config.Checksum && !startup { slog.Debug("skipping apply because state already records target version/checksum", "version", config.Version, "checksum", config.Checksum) - return nil + return s.stateStore.Save(snapshot) } routeConfig := config.RouteConfig if routeConfig == "" { @@ -172,6 +200,7 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool, slog.Info("openresty config applied successfully", "mode", mode, "version", config.Version) snapshot.CurrentVersion = config.Version snapshot.CurrentChecksum = config.Checksum + clearBlockedTarget(snapshot) snapshot.LastError = "" snapshot.OpenrestyStatus = protocol.OpenrestyStatusHealthy snapshot.OpenrestyMessage = "" @@ -184,6 +213,7 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool, message = "apply rolled back to previous config" } slog.Warn("openresty config apply rolled back", "mode", mode, "version", config.Version, "message", message) + markBlockedTarget(snapshot, config.Version, config.Checksum, message) snapshot.LastError = message snapshot.OpenrestyStatus = protocol.OpenrestyStatusHealthy snapshot.OpenrestyMessage = message @@ -193,6 +223,7 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool, message = "openresty apply failed" } slog.Error("apply openresty config failed", "mode", mode, "version", config.Version, "message", message) + markBlockedTarget(snapshot, config.Version, config.Checksum, message) snapshot.LastError = message snapshot.OpenrestyStatus = protocol.OpenrestyStatusUnhealthy snapshot.OpenrestyMessage = message @@ -230,6 +261,56 @@ func outcomeError(version string, message string) error { return fmt.Errorf("apply version %s failed: %s", version, trimmed) } +func (s *Service) ensureRuntimeForCurrentConfig(ctx context.Context, mode string, snapshot *state.Snapshot, currentChecksum string) error { + if strings.TrimSpace(currentChecksum) == "" { + slog.Warn("blocked config cannot be retried and no local checksum is available for runtime recovery", "mode", mode, "blocked_version", snapshot.BlockedVersion) + return nil + } + slog.Info("ensuring runtime with current local config while active target remains blocked", "mode", mode, "current_version", snapshot.CurrentVersion, "current_checksum", currentChecksum, "blocked_version", snapshot.BlockedVersion) + if err := s.nginxManager.EnsureRuntime(ctx, true); err != nil { + snapshot.OpenrestyStatus = protocol.OpenrestyStatusUnhealthy + snapshot.OpenrestyMessage = err.Error() + _ = s.stateStore.Save(snapshot) + return err + } + snapshot.OpenrestyStatus = protocol.OpenrestyStatusHealthy + if strings.TrimSpace(snapshot.OpenrestyMessage) == strings.TrimSpace(snapshot.BlockedReason) { + snapshot.OpenrestyMessage = "" + } + return nil +} + +func markBlockedTarget(snapshot *state.Snapshot, version string, checksum string, reason string) { + if snapshot == nil { + return + } + snapshot.BlockedVersion = strings.TrimSpace(version) + snapshot.BlockedChecksum = strings.TrimSpace(checksum) + snapshot.BlockedReason = strings.TrimSpace(reason) +} + +func clearBlockedTarget(snapshot *state.Snapshot) { + if snapshot == nil { + return + } + snapshot.BlockedVersion = "" + snapshot.BlockedChecksum = "" + snapshot.BlockedReason = "" +} + +func hasBlockedTarget(snapshot *state.Snapshot) bool { + return snapshot != nil && (strings.TrimSpace(snapshot.BlockedVersion) != "" || strings.TrimSpace(snapshot.BlockedChecksum) != "") +} + +func isBlockedTarget(snapshot *state.Snapshot, version string, checksum string) bool { + if snapshot == nil { + return false + } + return strings.TrimSpace(snapshot.BlockedVersion) == strings.TrimSpace(version) && + strings.TrimSpace(snapshot.BlockedChecksum) == strings.TrimSpace(checksum) && + (strings.TrimSpace(version) != "" || strings.TrimSpace(checksum) != "") +} + func checksumString(content string) string { sum := sha256.Sum256([]byte(content)) return hex.EncodeToString(sum[:]) diff --git a/openflare_agent/internal/sync/service_test.go b/openflare_agent/internal/sync/service_test.go index 11f28083..9d85df74 100644 --- a/openflare_agent/internal/sync/service_test.go +++ b/openflare_agent/internal/sync/service_test.go @@ -203,6 +203,9 @@ func TestSyncOnceRollbackOnNginxFailure(t *testing.T) { if snapshot.CurrentVersion != "20260309-001" { t.Fatal("expected failed sync not to overwrite current version") } + if snapshot.BlockedVersion != "20260309-002" || snapshot.BlockedChecksum != "checksum-2" { + t.Fatalf("expected failed target version to be blocked, got %+v", snapshot) + } if snapshot.OpenrestyStatus != protocol.OpenrestyStatusUnhealthy { t.Fatalf("expected unhealthy openresty status, got %q", snapshot.OpenrestyStatus) } @@ -267,6 +270,9 @@ func TestSyncOnceReportsWarningWhenRollbackKeepsOpenrestyHealthy(t *testing.T) { if snapshot.CurrentVersion != "20260309-001" || snapshot.CurrentChecksum != "checksum-1" { t.Fatal("expected warning apply to keep previous version state") } + if snapshot.BlockedVersion != "20260309-002" || snapshot.BlockedChecksum != "checksum-2" { + t.Fatalf("expected rolled-back target version to be blocked, got %+v", snapshot) + } if snapshot.OpenrestyStatus != protocol.OpenrestyStatusHealthy { t.Fatalf("expected healthy openresty after rollback, got %q", snapshot.OpenrestyStatus) } @@ -368,6 +374,165 @@ func TestSyncOnStartupRecordsRuntimeFailure(t *testing.T) { } } +func TestSyncOnceSkipsPreviouslyBlockedVersion(t *testing.T) { + client := &fakeClient{ + config: protocol.ActiveConfigResponse{ + Version: "20260309-006", + Checksum: "checksum-6", + MainConfig: "worker_processes 6;", + RouteConfig: "server { listen 86; }", + RenderedConfig: "server { listen 86; }", + CreatedAt: time.Now().Format(time.RFC3339), + }, + } + stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json")) + nodeID, err := stateStore.EnsureNodeID() + if err != nil { + t.Fatalf("EnsureNodeID failed: %v", err) + } + if err = stateStore.Save(&state.Snapshot{ + NodeID: nodeID, + CurrentVersion: "20260309-005", + CurrentChecksum: "checksum-5", + BlockedVersion: "20260309-006", + BlockedChecksum: "checksum-6", + BlockedReason: "apply failed, rolled back to previous config", + LastError: "apply failed, rolled back to previous config", + }); err != nil { + t.Fatalf("failed to seed state: %v", err) + } + + manager := &fakeManager{currentChecksum: "checksum-5"} + service := New(client, manager, stateStore) + if err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{ + Version: "20260309-006", + Checksum: "checksum-6", + }); err != nil { + t.Fatalf("expected blocked version to be skipped, got %v", err) + } + if client.fetchCalls != 0 { + t.Fatalf("expected blocked version to skip fetch, got %d", client.fetchCalls) + } + if len(manager.applyMainContents) != 0 { + t.Fatal("expected blocked version to skip apply") + } + if len(client.reports) != 0 { + t.Fatal("expected blocked version to skip reporting duplicate apply result") + } +} + +func TestSyncOnStartupKeepsBlockedVersionSuppressedUntilNewTargetArrives(t *testing.T) { + client := &fakeClient{ + config: protocol.ActiveConfigResponse{ + Version: "20260309-007", + Checksum: "checksum-7", + MainConfig: "worker_processes 7;", + RouteConfig: "server { listen 87; }", + RenderedConfig: "server { listen 87; }", + CreatedAt: time.Now().Format(time.RFC3339), + }, + } + stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json")) + nodeID, err := stateStore.EnsureNodeID() + if err != nil { + t.Fatalf("EnsureNodeID failed: %v", err) + } + if err = stateStore.Save(&state.Snapshot{ + NodeID: nodeID, + CurrentVersion: "20260309-005", + CurrentChecksum: "checksum-5", + BlockedVersion: "20260309-007", + BlockedChecksum: "checksum-7", + BlockedReason: "apply failed, rolled back to previous config", + OpenrestyStatus: protocol.OpenrestyStatusUnhealthy, + OpenrestyMessage: "apply failed, rolled back to previous config", + LastError: "apply failed, rolled back to previous config", + }); err != nil { + t.Fatalf("failed to seed state: %v", err) + } + + manager := &fakeManager{currentChecksum: "checksum-5"} + service := New(client, manager, stateStore) + if err = service.SyncOnStartup(context.Background(), &protocol.ActiveConfigMeta{ + Version: "20260309-007", + Checksum: "checksum-7", + }); err != nil { + t.Fatalf("expected blocked startup target to be skipped, got %v", err) + } + if len(manager.ensureCalls) != 1 || !manager.ensureCalls[0] { + t.Fatal("expected startup skip to ensure runtime with current local config") + } + if client.fetchCalls != 0 { + t.Fatalf("expected blocked startup target to skip fetch, got %d", client.fetchCalls) + } + if len(client.reports) != 0 { + t.Fatal("expected blocked startup target to skip duplicate apply report") + } + snapshot, err := stateStore.Load() + if err != nil { + t.Fatalf("failed to load state: %v", err) + } + if snapshot.BlockedVersion != "20260309-007" || snapshot.BlockedChecksum != "checksum-7" { + t.Fatalf("expected blocked target to remain recorded, got %+v", snapshot) + } + if snapshot.OpenrestyStatus != protocol.OpenrestyStatusHealthy { + t.Fatalf("expected startup runtime recovery to mark openresty healthy, got %q", snapshot.OpenrestyStatus) + } +} + +func TestSyncOnceClearsBlockedTargetWhenNewVersionArrives(t *testing.T) { + client := &fakeClient{ + config: protocol.ActiveConfigResponse{ + Version: "20260309-008", + Checksum: "checksum-8", + MainConfig: "worker_processes 8;", + RouteConfig: "server { listen 88; }", + RenderedConfig: "server { listen 88; }", + CreatedAt: time.Now().Format(time.RFC3339), + }, + } + stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json")) + nodeID, err := stateStore.EnsureNodeID() + if err != nil { + t.Fatalf("EnsureNodeID failed: %v", err) + } + if err = stateStore.Save(&state.Snapshot{ + NodeID: nodeID, + CurrentVersion: "20260309-005", + CurrentChecksum: "checksum-5", + BlockedVersion: "20260309-007", + BlockedChecksum: "checksum-7", + BlockedReason: "apply failed, rolled back to previous config", + }); err != nil { + t.Fatalf("failed to seed state: %v", err) + } + + manager := &fakeManager{} + service := New(client, manager, stateStore) + if err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{ + Version: "20260309-008", + Checksum: "checksum-8", + }); err != nil { + t.Fatalf("expected new target version to be applied, got %v", err) + } + if client.fetchCalls != 1 { + t.Fatalf("expected new target to trigger fetch, got %d", client.fetchCalls) + } + if len(manager.applyMainContents) != 1 { + t.Fatal("expected new target to trigger apply") + } + snapshot, err := stateStore.Load() + if err != nil { + t.Fatalf("failed to load state: %v", err) + } + if snapshot.BlockedVersion != "" || snapshot.BlockedChecksum != "" { + t.Fatalf("expected blocked target to be cleared after new version succeeds, got %+v", snapshot) + } + if snapshot.CurrentVersion != "20260309-008" || snapshot.CurrentChecksum != "checksum-8" { + t.Fatalf("expected current version to move to new target, got %+v", snapshot) + } +} + func TestSyncOnceSkipsFetchWhenHeartbeatChecksumMatches(t *testing.T) { client := &fakeClient{ config: protocol.ActiveConfigResponse{ diff --git a/openflare_server/controller/node.go b/openflare_server/controller/node.go index 2853967b..f11a0063 100644 --- a/openflare_server/controller/node.go +++ b/openflare_server/controller/node.go @@ -247,3 +247,27 @@ func GetNodeObservability(c *gin.Context) { } respondSuccess(c, view) } + +// CleanupNodeHealthEvents godoc +// @Summary Cleanup node health events +// @Tags Nodes +// @Produce json +// @Security BearerAuth +// @Param id path int true "Node ID" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Router /api/nodes/{id}/observability/cleanup [post] +func CleanupNodeHealthEvents(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil || id == 0 { + respondBadRequest(c, "") + return + } + + result, err := service.CleanupNodeHealthEvents(uint(id)) + if err != nil { + respondFailure(c, err.Error()) + return + } + respondSuccess(c, result) +} diff --git a/openflare_server/docs/docs.go b/openflare_server/docs/docs.go index 6f60bcf8..c32bf8d6 100644 --- a/openflare_server/docs/docs.go +++ b/openflare_server/docs/docs.go @@ -15,6 +15,317 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/api/access-logs/": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AccessLogs" + ], + "summary": "List access logs", + "parameters": [ + { + "type": "string", + "description": "Node ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "Remote address", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "Host", + "name": "host", + "in": "query" + }, + { + "type": "string", + "description": "Path", + "name": "path", + "in": "query" + }, + { + "type": "integer", + "description": "Page index", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Sort by", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Sort order", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/access-logs/cleanup": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AccessLogs" + ], + "summary": "Cleanup access logs by retention days", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/access-logs/folds": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AccessLogs" + ], + "summary": "List folded access logs", + "parameters": [ + { + "type": "string", + "description": "Node ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "Remote address", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "Host", + "name": "host", + "in": "query" + }, + { + "type": "string", + "description": "Path", + "name": "path", + "in": "query" + }, + { + "type": "integer", + "description": "Page index", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Sort by", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Sort order", + "name": "sort_order", + "in": "query" + }, + { + "type": "integer", + "description": "Fold minutes", + "name": "fold_minutes", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/access-logs/ip-summary": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AccessLogs" + ], + "summary": "List access log IP summaries", + "parameters": [ + { + "type": "string", + "description": "Node ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "Remote address", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "Host", + "name": "host", + "in": "query" + }, + { + "type": "integer", + "description": "Page index", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Sort by", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Sort order", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/access-logs/ip-summary/trend": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AccessLogs" + ], + "summary": "Get access log IP trend", + "parameters": [ + { + "type": "string", + "description": "Node ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "Remote address", + "name": "remote_addr", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Host", + "name": "host", + "in": "query" + }, + { + "type": "integer", + "description": "Hours", + "name": "hours", + "in": "query" + }, + { + "type": "integer", + "description": "Bucket minutes", + "name": "bucket_minutes", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/agent/apply-logs": { "post": { "security": [ @@ -211,6 +522,34 @@ const docTemplate = `{ } } }, + "/api/apply-logs/cleanup": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ApplyLogs" + ], + "summary": "Cleanup apply logs", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/config-versions/": { "get": { "security": [ @@ -336,8 +675,49 @@ const docTemplate = `{ } } }, + "/api/config-versions/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ConfigVersions" + ], + "summary": "Get config version detail", + "parameters": [ + { + "type": "integer", + "description": "Version ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/config-versions/{id}/activate": { - "put": { + "post": { "security": [ { "BearerAuth": [] @@ -377,6 +757,38 @@ const docTemplate = `{ } } }, + "/api/dashboard/overview": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Dashboard" + ], + "summary": "Get dashboard overview", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/managed-domains/": { "get": { "security": [ @@ -480,8 +892,49 @@ const docTemplate = `{ } } }, - "/api/managed-domains/{id}": { - "put": { + "/api/managed-domains/{id}/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ManagedDomains" + ], + "summary": "Delete managed domain", + "parameters": [ + { + "type": "integer", + "description": "Managed domain ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/managed-domains/{id}/update": { + "post": { "security": [ { "BearerAuth": [] @@ -531,45 +984,6 @@ const docTemplate = `{ } } } - }, - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "ManagedDomains" - ], - "summary": "Delete managed domain", - "parameters": [ - { - "type": "integer", - "description": "Managed domain ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } } }, "/api/nodes/": { @@ -691,98 +1105,6 @@ const docTemplate = `{ } } }, - "/api/nodes/{id}": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Nodes" - ], - "summary": "Update node", - "parameters": [ - { - "type": "integer", - "description": "Node ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Node payload", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/service.NodeInput" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Nodes" - ], - "summary": "Delete node", - "parameters": [ - { - "type": "integer", - "description": "Node ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - } - }, "/api/nodes/{id}/agent-release": { "get": { "security": [ @@ -871,6 +1193,141 @@ const docTemplate = `{ } } }, + "/api/nodes/{id}/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Nodes" + ], + "summary": "Delete node", + "parameters": [ + { + "type": "integer", + "description": "Node ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/nodes/{id}/observability": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Nodes" + ], + "summary": "Get node observability details", + "parameters": [ + { + "type": "integer", + "description": "Node ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Lookback window in hours", + "name": "hours", + "in": "query" + }, + { + "type": "integer", + "description": "Max records per section", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/nodes/{id}/observability/cleanup": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Nodes" + ], + "summary": "Cleanup node health events", + "parameters": [ + { + "type": "integer", + "description": "Node ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/nodes/{id}/openresty-restart": { "post": { "security": [ @@ -912,6 +1369,59 @@ const docTemplate = `{ } } }, + "/api/nodes/{id}/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Nodes" + ], + "summary": "Update node", + "parameters": [ + { + "type": "integer", + "description": "Node ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Node payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.NodeInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/option/": { "get": { "produces": [ @@ -930,8 +1440,51 @@ const docTemplate = `{ } } } - }, - "put": { + } + }, + "/api/option/geoip/lookup": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Options" + ], + "summary": "Test GeoIP lookup", + "parameters": [ + { + "description": "GeoIP lookup payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.geoIPLookupRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/option/update": { + "post": { "consumes": [ "application/json" ], @@ -1040,8 +1593,49 @@ const docTemplate = `{ } } }, - "/api/proxy-routes/{id}": { - "put": { + "/api/proxy-routes/{id}/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ProxyRoutes" + ], + "summary": "Delete proxy route", + "parameters": [ + { + "type": "integer", + "description": "Route ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/proxy-routes/{id}/update": { + "post": { "security": [ { "BearerAuth": [] @@ -1091,45 +1685,6 @@ const docTemplate = `{ } } } - }, - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "ProxyRoutes" - ], - "summary": "Delete proxy route", - "parameters": [ - { - "type": "integer", - "description": "Route ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } } }, "/api/status": { @@ -1286,7 +1841,89 @@ const docTemplate = `{ } }, "/api/tls-certificates/{id}": { - "delete": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Get TLS certificate detail", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/tls-certificates/{id}/content": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Get TLS certificate PEM content", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/tls-certificates/{id}/delete": { + "post": { "security": [ { "BearerAuth": [] @@ -1326,6 +1963,59 @@ const docTemplate = `{ } } }, + "/api/tls-certificates/{id}/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Update TLS certificate from PEM", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "TLS certificate payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.TLSCertificateInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/update/latest-release": { "get": { "produces": [ @@ -1346,6 +2036,15 @@ const docTemplate = `{ } } }, + "/api/update/logs/ws": { + "get": { + "tags": [ + "Update" + ], + "summary": "Stream server upgrade logs over websocket", + "responses": {} + } + }, "/api/update/manual-upgrade": { "post": { "consumes": [ @@ -1414,6 +2113,17 @@ const docTemplate = `{ } }, "definitions": { + "controller.geoIPLookupRequest": { + "type": "object", + "properties": { + "ip": { + "type": "string" + }, + "provider": { + "type": "string" + } + } + }, "model.Option": { "type": "object", "properties": { @@ -1425,15 +2135,140 @@ const docTemplate = `{ } } }, + "service.AgentBufferedObservabilityRecord": { + "type": "object", + "properties": { + "access_logs": { + "type": "array", + "items": { + "$ref": "#/definitions/service.AgentNodeAccessLog" + } + }, + "snapshot": { + "$ref": "#/definitions/service.AgentNodeMetricSnapshot" + }, + "traffic_report": { + "$ref": "#/definitions/service.AgentNodeTrafficReport" + }, + "window_started_at_unix": { + "type": "integer" + } + } + }, + "service.AgentNodeAccessLog": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "logged_at_unix": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "remote_addr": { + "type": "string" + }, + "status_code": { + "type": "integer" + } + } + }, + "service.AgentNodeHealthEvent": { + "type": "object", + "properties": { + "event_type": { + "type": "string" + }, + "message": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "severity": { + "type": "string" + }, + "triggered_at_unix": { + "type": "integer" + } + } + }, + "service.AgentNodeMetricSnapshot": { + "type": "object", + "properties": { + "captured_at_unix": { + "type": "integer" + }, + "cpu_usage_percent": { + "type": "number" + }, + "disk_read_bytes": { + "type": "integer" + }, + "disk_write_bytes": { + "type": "integer" + }, + "memory_total_bytes": { + "type": "integer" + }, + "memory_used_bytes": { + "type": "integer" + }, + "network_rx_bytes": { + "type": "integer" + }, + "network_tx_bytes": { + "type": "integer" + }, + "openresty_connections": { + "type": "integer" + }, + "openresty_rx_bytes": { + "type": "integer" + }, + "openresty_tx_bytes": { + "type": "integer" + }, + "storage_total_bytes": { + "type": "integer" + }, + "storage_used_bytes": { + "type": "integer" + } + } + }, "service.AgentNodePayload": { "type": "object", "properties": { + "access_logs": { + "type": "array", + "items": { + "$ref": "#/definitions/service.AgentNodeAccessLog" + } + }, "agent_version": { "type": "string" }, + "buffered_observability": { + "type": "array", + "items": { + "$ref": "#/definitions/service.AgentBufferedObservabilityRecord" + } + }, "current_version": { "type": "string" }, + "health_events": { + "type": "array", + "items": { + "$ref": "#/definitions/service.AgentNodeHealthEvent" + } + }, "ip": { "type": "string" }, @@ -1454,12 +2289,106 @@ const docTemplate = `{ }, "openresty_status": { "type": "string" + }, + "profile": { + "$ref": "#/definitions/service.AgentNodeSystemProfile" + }, + "snapshot": { + "$ref": "#/definitions/service.AgentNodeMetricSnapshot" + }, + "traffic_report": { + "$ref": "#/definitions/service.AgentNodeTrafficReport" + } + } + }, + "service.AgentNodeSystemProfile": { + "type": "object", + "properties": { + "architecture": { + "type": "string" + }, + "cpu_cores": { + "type": "integer" + }, + "cpu_model": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "kernel_version": { + "type": "string" + }, + "os_name": { + "type": "string" + }, + "os_version": { + "type": "string" + }, + "reported_at_unix": { + "type": "integer" + }, + "total_disk_bytes": { + "type": "integer" + }, + "total_memory_bytes": { + "type": "integer" + }, + "uptime_seconds": { + "type": "integer" + } + } + }, + "service.AgentNodeTrafficReport": { + "type": "object", + "properties": { + "error_count": { + "type": "integer" + }, + "request_count": { + "type": "integer" + }, + "source_countries": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "status_codes": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "top_domains": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "unique_visitor_count": { + "type": "integer" + }, + "window_ended_at_unix": { + "type": "integer" + }, + "window_started_at_unix": { + "type": "integer" } } }, "service.ApplyLogPayload": { "type": "object", "properties": { + "checksum": { + "type": "string" + }, + "main_config_checksum": { + "type": "string" + }, "message": { "type": "string" }, @@ -1469,6 +2398,12 @@ const docTemplate = `{ "result": { "type": "string" }, + "route_config_checksum": { + "type": "string" + }, + "support_file_count": { + "type": "integer" + }, "version": { "type": "string" } @@ -1497,6 +2432,21 @@ const docTemplate = `{ "auto_update_enabled": { "type": "boolean" }, + "geo_latitude": { + "type": "number" + }, + "geo_longitude": { + "type": "number" + }, + "geo_manual_override": { + "type": "boolean" + }, + "geo_name": { + "type": "string" + }, + "ip": { + "type": "string" + }, "name": { "type": "string" } @@ -1516,6 +2466,18 @@ const docTemplate = `{ "service.ProxyRouteInput": { "type": "object", "properties": { + "cache_enabled": { + "type": "boolean" + }, + "cache_policy": { + "type": "string" + }, + "cache_rules": { + "type": "array", + "items": { + "type": "string" + } + }, "cert_id": { "type": "integer" }, @@ -1534,10 +2496,10 @@ const docTemplate = `{ "enabled": { "type": "boolean" }, - "origin_url": { + "origin_host": { "type": "string" }, - "origin_host": { + "origin_url": { "type": "string" }, "redirect_http": { @@ -1545,6 +2507,12 @@ const docTemplate = `{ }, "remark": { "type": "string" + }, + "upstreams": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -1592,6 +2560,8 @@ var SwaggerInfo = &swag.Spec{ Description: "OpenFlare Server 管理端与 Agent API 文档。", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", } func init() { diff --git a/openflare_server/docs/swagger.json b/openflare_server/docs/swagger.json index 52d2cd0e..203a5093 100644 --- a/openflare_server/docs/swagger.json +++ b/openflare_server/docs/swagger.json @@ -12,6 +12,317 @@ }, "basePath": "/", "paths": { + "/api/access-logs/": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AccessLogs" + ], + "summary": "List access logs", + "parameters": [ + { + "type": "string", + "description": "Node ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "Remote address", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "Host", + "name": "host", + "in": "query" + }, + { + "type": "string", + "description": "Path", + "name": "path", + "in": "query" + }, + { + "type": "integer", + "description": "Page index", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Sort by", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Sort order", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/access-logs/cleanup": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AccessLogs" + ], + "summary": "Cleanup access logs by retention days", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/access-logs/folds": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AccessLogs" + ], + "summary": "List folded access logs", + "parameters": [ + { + "type": "string", + "description": "Node ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "Remote address", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "Host", + "name": "host", + "in": "query" + }, + { + "type": "string", + "description": "Path", + "name": "path", + "in": "query" + }, + { + "type": "integer", + "description": "Page index", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Sort by", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Sort order", + "name": "sort_order", + "in": "query" + }, + { + "type": "integer", + "description": "Fold minutes", + "name": "fold_minutes", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/access-logs/ip-summary": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AccessLogs" + ], + "summary": "List access log IP summaries", + "parameters": [ + { + "type": "string", + "description": "Node ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "Remote address", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "Host", + "name": "host", + "in": "query" + }, + { + "type": "integer", + "description": "Page index", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Sort by", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Sort order", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/access-logs/ip-summary/trend": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AccessLogs" + ], + "summary": "Get access log IP trend", + "parameters": [ + { + "type": "string", + "description": "Node ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "Remote address", + "name": "remote_addr", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Host", + "name": "host", + "in": "query" + }, + { + "type": "integer", + "description": "Hours", + "name": "hours", + "in": "query" + }, + { + "type": "integer", + "description": "Bucket minutes", + "name": "bucket_minutes", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/agent/apply-logs": { "post": { "security": [ @@ -208,6 +519,34 @@ } } }, + "/api/apply-logs/cleanup": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ApplyLogs" + ], + "summary": "Cleanup apply logs", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/config-versions/": { "get": { "security": [ @@ -333,8 +672,49 @@ } } }, + "/api/config-versions/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ConfigVersions" + ], + "summary": "Get config version detail", + "parameters": [ + { + "type": "integer", + "description": "Version ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/config-versions/{id}/activate": { - "put": { + "post": { "security": [ { "BearerAuth": [] @@ -374,6 +754,38 @@ } } }, + "/api/dashboard/overview": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Dashboard" + ], + "summary": "Get dashboard overview", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/managed-domains/": { "get": { "security": [ @@ -477,8 +889,49 @@ } } }, - "/api/managed-domains/{id}": { - "put": { + "/api/managed-domains/{id}/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ManagedDomains" + ], + "summary": "Delete managed domain", + "parameters": [ + { + "type": "integer", + "description": "Managed domain ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/managed-domains/{id}/update": { + "post": { "security": [ { "BearerAuth": [] @@ -528,45 +981,6 @@ } } } - }, - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "ManagedDomains" - ], - "summary": "Delete managed domain", - "parameters": [ - { - "type": "integer", - "description": "Managed domain ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } } }, "/api/nodes/": { @@ -688,98 +1102,6 @@ } } }, - "/api/nodes/{id}": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Nodes" - ], - "summary": "Update node", - "parameters": [ - { - "type": "integer", - "description": "Node ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Node payload", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/service.NodeInput" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Nodes" - ], - "summary": "Delete node", - "parameters": [ - { - "type": "integer", - "description": "Node ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - } - }, "/api/nodes/{id}/agent-release": { "get": { "security": [ @@ -868,6 +1190,141 @@ } } }, + "/api/nodes/{id}/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Nodes" + ], + "summary": "Delete node", + "parameters": [ + { + "type": "integer", + "description": "Node ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/nodes/{id}/observability": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Nodes" + ], + "summary": "Get node observability details", + "parameters": [ + { + "type": "integer", + "description": "Node ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Lookback window in hours", + "name": "hours", + "in": "query" + }, + { + "type": "integer", + "description": "Max records per section", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/nodes/{id}/observability/cleanup": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Nodes" + ], + "summary": "Cleanup node health events", + "parameters": [ + { + "type": "integer", + "description": "Node ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/nodes/{id}/openresty-restart": { "post": { "security": [ @@ -909,6 +1366,59 @@ } } }, + "/api/nodes/{id}/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Nodes" + ], + "summary": "Update node", + "parameters": [ + { + "type": "integer", + "description": "Node ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Node payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.NodeInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/option/": { "get": { "produces": [ @@ -927,8 +1437,51 @@ } } } - }, - "put": { + } + }, + "/api/option/geoip/lookup": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Options" + ], + "summary": "Test GeoIP lookup", + "parameters": [ + { + "description": "GeoIP lookup payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.geoIPLookupRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/option/update": { + "post": { "consumes": [ "application/json" ], @@ -1037,8 +1590,49 @@ } } }, - "/api/proxy-routes/{id}": { - "put": { + "/api/proxy-routes/{id}/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ProxyRoutes" + ], + "summary": "Delete proxy route", + "parameters": [ + { + "type": "integer", + "description": "Route ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/proxy-routes/{id}/update": { + "post": { "security": [ { "BearerAuth": [] @@ -1088,45 +1682,6 @@ } } } - }, - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "ProxyRoutes" - ], - "summary": "Delete proxy route", - "parameters": [ - { - "type": "integer", - "description": "Route ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } } }, "/api/status": { @@ -1283,7 +1838,89 @@ } }, "/api/tls-certificates/{id}": { - "delete": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Get TLS certificate detail", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/tls-certificates/{id}/content": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Get TLS certificate PEM content", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/tls-certificates/{id}/delete": { + "post": { "security": [ { "BearerAuth": [] @@ -1323,6 +1960,59 @@ } } }, + "/api/tls-certificates/{id}/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Update TLS certificate from PEM", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "TLS certificate payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.TLSCertificateInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/update/latest-release": { "get": { "produces": [ @@ -1343,6 +2033,15 @@ } } }, + "/api/update/logs/ws": { + "get": { + "tags": [ + "Update" + ], + "summary": "Stream server upgrade logs over websocket", + "responses": {} + } + }, "/api/update/manual-upgrade": { "post": { "consumes": [ @@ -1411,6 +2110,17 @@ } }, "definitions": { + "controller.geoIPLookupRequest": { + "type": "object", + "properties": { + "ip": { + "type": "string" + }, + "provider": { + "type": "string" + } + } + }, "model.Option": { "type": "object", "properties": { @@ -1422,15 +2132,140 @@ } } }, + "service.AgentBufferedObservabilityRecord": { + "type": "object", + "properties": { + "access_logs": { + "type": "array", + "items": { + "$ref": "#/definitions/service.AgentNodeAccessLog" + } + }, + "snapshot": { + "$ref": "#/definitions/service.AgentNodeMetricSnapshot" + }, + "traffic_report": { + "$ref": "#/definitions/service.AgentNodeTrafficReport" + }, + "window_started_at_unix": { + "type": "integer" + } + } + }, + "service.AgentNodeAccessLog": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "logged_at_unix": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "remote_addr": { + "type": "string" + }, + "status_code": { + "type": "integer" + } + } + }, + "service.AgentNodeHealthEvent": { + "type": "object", + "properties": { + "event_type": { + "type": "string" + }, + "message": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "severity": { + "type": "string" + }, + "triggered_at_unix": { + "type": "integer" + } + } + }, + "service.AgentNodeMetricSnapshot": { + "type": "object", + "properties": { + "captured_at_unix": { + "type": "integer" + }, + "cpu_usage_percent": { + "type": "number" + }, + "disk_read_bytes": { + "type": "integer" + }, + "disk_write_bytes": { + "type": "integer" + }, + "memory_total_bytes": { + "type": "integer" + }, + "memory_used_bytes": { + "type": "integer" + }, + "network_rx_bytes": { + "type": "integer" + }, + "network_tx_bytes": { + "type": "integer" + }, + "openresty_connections": { + "type": "integer" + }, + "openresty_rx_bytes": { + "type": "integer" + }, + "openresty_tx_bytes": { + "type": "integer" + }, + "storage_total_bytes": { + "type": "integer" + }, + "storage_used_bytes": { + "type": "integer" + } + } + }, "service.AgentNodePayload": { "type": "object", "properties": { + "access_logs": { + "type": "array", + "items": { + "$ref": "#/definitions/service.AgentNodeAccessLog" + } + }, "agent_version": { "type": "string" }, + "buffered_observability": { + "type": "array", + "items": { + "$ref": "#/definitions/service.AgentBufferedObservabilityRecord" + } + }, "current_version": { "type": "string" }, + "health_events": { + "type": "array", + "items": { + "$ref": "#/definitions/service.AgentNodeHealthEvent" + } + }, "ip": { "type": "string" }, @@ -1451,12 +2286,106 @@ }, "openresty_status": { "type": "string" + }, + "profile": { + "$ref": "#/definitions/service.AgentNodeSystemProfile" + }, + "snapshot": { + "$ref": "#/definitions/service.AgentNodeMetricSnapshot" + }, + "traffic_report": { + "$ref": "#/definitions/service.AgentNodeTrafficReport" + } + } + }, + "service.AgentNodeSystemProfile": { + "type": "object", + "properties": { + "architecture": { + "type": "string" + }, + "cpu_cores": { + "type": "integer" + }, + "cpu_model": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "kernel_version": { + "type": "string" + }, + "os_name": { + "type": "string" + }, + "os_version": { + "type": "string" + }, + "reported_at_unix": { + "type": "integer" + }, + "total_disk_bytes": { + "type": "integer" + }, + "total_memory_bytes": { + "type": "integer" + }, + "uptime_seconds": { + "type": "integer" + } + } + }, + "service.AgentNodeTrafficReport": { + "type": "object", + "properties": { + "error_count": { + "type": "integer" + }, + "request_count": { + "type": "integer" + }, + "source_countries": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "status_codes": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "top_domains": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "unique_visitor_count": { + "type": "integer" + }, + "window_ended_at_unix": { + "type": "integer" + }, + "window_started_at_unix": { + "type": "integer" } } }, "service.ApplyLogPayload": { "type": "object", "properties": { + "checksum": { + "type": "string" + }, + "main_config_checksum": { + "type": "string" + }, "message": { "type": "string" }, @@ -1466,6 +2395,12 @@ "result": { "type": "string" }, + "route_config_checksum": { + "type": "string" + }, + "support_file_count": { + "type": "integer" + }, "version": { "type": "string" } @@ -1494,6 +2429,21 @@ "auto_update_enabled": { "type": "boolean" }, + "geo_latitude": { + "type": "number" + }, + "geo_longitude": { + "type": "number" + }, + "geo_manual_override": { + "type": "boolean" + }, + "geo_name": { + "type": "string" + }, + "ip": { + "type": "string" + }, "name": { "type": "string" } @@ -1513,6 +2463,18 @@ "service.ProxyRouteInput": { "type": "object", "properties": { + "cache_enabled": { + "type": "boolean" + }, + "cache_policy": { + "type": "string" + }, + "cache_rules": { + "type": "array", + "items": { + "type": "string" + } + }, "cert_id": { "type": "integer" }, @@ -1531,10 +2493,10 @@ "enabled": { "type": "boolean" }, - "origin_url": { + "origin_host": { "type": "string" }, - "origin_host": { + "origin_url": { "type": "string" }, "redirect_http": { @@ -1542,6 +2504,12 @@ }, "remark": { "type": "string" + }, + "upstreams": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -1577,4 +2545,4 @@ "in": "header" } } -} +} \ No newline at end of file diff --git a/openflare_server/docs/swagger.yaml b/openflare_server/docs/swagger.yaml index efb56667..49ebfecf 100644 --- a/openflare_server/docs/swagger.yaml +++ b/openflare_server/docs/swagger.yaml @@ -1,5 +1,12 @@ basePath: / definitions: + controller.geoIPLookupRequest: + properties: + ip: + type: string + provider: + type: string + type: object model.Option: properties: key: @@ -7,12 +14,94 @@ definitions: value: type: string type: object + service.AgentBufferedObservabilityRecord: + properties: + access_logs: + items: + $ref: '#/definitions/service.AgentNodeAccessLog' + type: array + snapshot: + $ref: '#/definitions/service.AgentNodeMetricSnapshot' + traffic_report: + $ref: '#/definitions/service.AgentNodeTrafficReport' + window_started_at_unix: + type: integer + type: object + service.AgentNodeAccessLog: + properties: + host: + type: string + logged_at_unix: + type: integer + path: + type: string + remote_addr: + type: string + status_code: + type: integer + type: object + service.AgentNodeHealthEvent: + properties: + event_type: + type: string + message: + type: string + metadata: + additionalProperties: + type: string + type: object + severity: + type: string + triggered_at_unix: + type: integer + type: object + service.AgentNodeMetricSnapshot: + properties: + captured_at_unix: + type: integer + cpu_usage_percent: + type: number + disk_read_bytes: + type: integer + disk_write_bytes: + type: integer + memory_total_bytes: + type: integer + memory_used_bytes: + type: integer + network_rx_bytes: + type: integer + network_tx_bytes: + type: integer + openresty_connections: + type: integer + openresty_rx_bytes: + type: integer + openresty_tx_bytes: + type: integer + storage_total_bytes: + type: integer + storage_used_bytes: + type: integer + type: object service.AgentNodePayload: properties: + access_logs: + items: + $ref: '#/definitions/service.AgentNodeAccessLog' + type: array agent_version: type: string + buffered_observability: + items: + $ref: '#/definitions/service.AgentBufferedObservabilityRecord' + type: array current_version: type: string + health_events: + items: + $ref: '#/definitions/service.AgentNodeHealthEvent' + type: array ip: type: string last_error: @@ -27,15 +116,82 @@ definitions: type: string openresty_status: type: string + profile: + $ref: '#/definitions/service.AgentNodeSystemProfile' + snapshot: + $ref: '#/definitions/service.AgentNodeMetricSnapshot' + traffic_report: + $ref: '#/definitions/service.AgentNodeTrafficReport' + type: object + service.AgentNodeSystemProfile: + properties: + architecture: + type: string + cpu_cores: + type: integer + cpu_model: + type: string + hostname: + type: string + kernel_version: + type: string + os_name: + type: string + os_version: + type: string + reported_at_unix: + type: integer + total_disk_bytes: + type: integer + total_memory_bytes: + type: integer + uptime_seconds: + type: integer + type: object + service.AgentNodeTrafficReport: + properties: + error_count: + type: integer + request_count: + type: integer + source_countries: + additionalProperties: + format: int64 + type: integer + type: object + status_codes: + additionalProperties: + format: int64 + type: integer + type: object + top_domains: + additionalProperties: + format: int64 + type: integer + type: object + unique_visitor_count: + type: integer + window_ended_at_unix: + type: integer + window_started_at_unix: + type: integer type: object service.ApplyLogPayload: properties: + checksum: + type: string + main_config_checksum: + type: string message: type: string node_id: type: string result: type: string + route_config_checksum: + type: string + support_file_count: + type: integer version: type: string type: object @@ -54,6 +210,16 @@ definitions: properties: auto_update_enabled: type: boolean + geo_latitude: + type: number + geo_longitude: + type: number + geo_manual_override: + type: boolean + geo_name: + type: string + ip: + type: string name: type: string type: object @@ -66,6 +232,14 @@ definitions: type: object service.ProxyRouteInput: properties: + cache_enabled: + type: boolean + cache_policy: + type: string + cache_rules: + items: + type: string + type: array cert_id: type: integer custom_headers: @@ -78,14 +252,18 @@ definitions: type: boolean enabled: type: boolean - origin_url: - type: string origin_host: type: string + origin_url: + type: string redirect_http: type: boolean remark: type: string + upstreams: + items: + type: string + type: array type: object service.TLSCertificateInput: properties: @@ -104,6 +282,204 @@ info: title: OpenFlare Server API version: "3.0" paths: + /api/access-logs/: + get: + parameters: + - description: Node ID + in: query + name: node_id + type: string + - description: Remote address + in: query + name: remote_addr + type: string + - description: Host + in: query + name: host + type: string + - description: Path + in: query + name: path + type: string + - description: Page index + in: query + name: p + type: integer + - description: Page size + in: query + name: page_size + type: integer + - description: Sort by + in: query + name: sort_by + type: string + - description: Sort order + in: query + name: sort_order + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: List access logs + tags: + - AccessLogs + /api/access-logs/cleanup: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Cleanup access logs by retention days + tags: + - AccessLogs + /api/access-logs/folds: + get: + parameters: + - description: Node ID + in: query + name: node_id + type: string + - description: Remote address + in: query + name: remote_addr + type: string + - description: Host + in: query + name: host + type: string + - description: Path + in: query + name: path + type: string + - description: Page index + in: query + name: p + type: integer + - description: Page size + in: query + name: page_size + type: integer + - description: Sort by + in: query + name: sort_by + type: string + - description: Sort order + in: query + name: sort_order + type: string + - description: Fold minutes + in: query + name: fold_minutes + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: List folded access logs + tags: + - AccessLogs + /api/access-logs/ip-summary: + get: + parameters: + - description: Node ID + in: query + name: node_id + type: string + - description: Remote address + in: query + name: remote_addr + type: string + - description: Host + in: query + name: host + type: string + - description: Page index + in: query + name: p + type: integer + - description: Page size + in: query + name: page_size + type: integer + - description: Sort by + in: query + name: sort_by + type: string + - description: Sort order + in: query + name: sort_order + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: List access log IP summaries + tags: + - AccessLogs + /api/access-logs/ip-summary/trend: + get: + parameters: + - description: Node ID + in: query + name: node_id + type: string + - description: Remote address + in: query + name: remote_addr + required: true + type: string + - description: Host + in: query + name: host + type: string + - description: Hours + in: query + name: hours + type: integer + - description: Bucket minutes + in: query + name: bucket_minutes + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Get access log IP trend + tags: + - AccessLogs /api/agent/apply-logs: post: consumes: @@ -226,6 +602,23 @@ paths: summary: List apply logs tags: - ApplyLogs + /api/apply-logs/cleanup: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Cleanup apply logs + tags: + - ApplyLogs /api/config-versions/: get: produces: @@ -241,8 +634,34 @@ paths: summary: List config versions tags: - ConfigVersions + /api/config-versions/{id}: + get: + parameters: + - description: Version ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Get config version detail + tags: + - ConfigVersions /api/config-versions/{id}/activate: - put: + post: parameters: - description: Version ID in: path @@ -327,6 +746,26 @@ paths: summary: Publish a new config version tags: - ConfigVersions + /api/dashboard/overview: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Get dashboard overview + tags: + - Dashboard /api/managed-domains/: get: produces: @@ -370,8 +809,8 @@ paths: summary: Create managed domain tags: - ManagedDomains - /api/managed-domains/{id}: - delete: + /api/managed-domains/{id}/delete: + post: parameters: - description: Managed domain ID in: path @@ -396,7 +835,8 @@ paths: summary: Delete managed domain tags: - ManagedDomains - put: + /api/managed-domains/{id}/update: + post: consumes: - application/json parameters: @@ -493,65 +933,6 @@ paths: summary: Create node tags: - Nodes - /api/nodes/{id}: - delete: - parameters: - - description: Node ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - "400": - description: Bad Request - schema: - additionalProperties: true - type: object - security: - - BearerAuth: [] - summary: Delete node - tags: - - Nodes - put: - consumes: - - application/json - parameters: - - description: Node ID - in: path - name: id - required: true - type: integer - - description: Node payload - in: body - name: payload - required: true - schema: - $ref: '#/definitions/service.NodeInput' - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - "400": - description: Bad Request - schema: - additionalProperties: true - type: object - security: - - BearerAuth: [] - summary: Update node - tags: - - Nodes /api/nodes/{id}/agent-release: get: parameters: @@ -608,6 +989,92 @@ paths: summary: Request agent self-update on node tags: - Nodes + /api/nodes/{id}/delete: + post: + parameters: + - description: Node ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Delete node + tags: + - Nodes + /api/nodes/{id}/observability: + get: + parameters: + - description: Node ID + in: path + name: id + required: true + type: integer + - description: Lookback window in hours + in: query + name: hours + type: integer + - description: Max records per section + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Get node observability details + tags: + - Nodes + /api/nodes/{id}/observability/cleanup: + post: + parameters: + - description: Node ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Cleanup node health events + tags: + - Nodes /api/nodes/{id}/openresty-restart: post: parameters: @@ -634,6 +1101,40 @@ paths: summary: Request openresty restart on node tags: - Nodes + /api/nodes/{id}/update: + post: + consumes: + - application/json + parameters: + - description: Node ID + in: path + name: id + required: true + type: integer + - description: Node payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service.NodeInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Update node + tags: + - Nodes /api/nodes/bootstrap-token: get: produces: @@ -677,7 +1178,35 @@ paths: summary: List editable options tags: - Options - put: + /api/option/geoip/lookup: + post: + consumes: + - application/json + parameters: + - description: GeoIP lookup payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/controller.geoIPLookupRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + summary: Test GeoIP lookup + tags: + - Options + /api/option/update: + post: consumes: - application/json parameters: @@ -746,8 +1275,8 @@ paths: summary: Create proxy route tags: - ProxyRoutes - /api/proxy-routes/{id}: - delete: + /api/proxy-routes/{id}/delete: + post: parameters: - description: Route ID in: path @@ -772,7 +1301,8 @@ paths: summary: Delete proxy route tags: - ProxyRoutes - put: + /api/proxy-routes/{id}/update: + post: consumes: - application/json parameters: @@ -862,7 +1392,59 @@ paths: tags: - TLSCertificates /api/tls-certificates/{id}: - delete: + get: + parameters: + - description: Certificate ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Get TLS certificate detail + tags: + - TLSCertificates + /api/tls-certificates/{id}/content: + get: + parameters: + - description: Certificate ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Get TLS certificate PEM content + tags: + - TLSCertificates + /api/tls-certificates/{id}/delete: + post: parameters: - description: Certificate ID in: path @@ -887,6 +1469,40 @@ paths: summary: Delete TLS certificate tags: - TLSCertificates + /api/tls-certificates/{id}/update: + post: + consumes: + - application/json + parameters: + - description: Certificate ID + in: path + name: id + required: true + type: integer + - description: TLS certificate payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service.TLSCertificateInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Update TLS certificate from PEM + tags: + - TLSCertificates /api/tls-certificates/import-file: post: consumes: @@ -942,6 +1558,12 @@ paths: summary: Get latest GitHub release tags: - Update + /api/update/logs/ws: + get: + responses: {} + summary: Stream server upgrade logs over websocket + tags: + - Update /api/update/manual-upgrade: post: consumes: diff --git a/openflare_server/model/node_health_event.go b/openflare_server/model/node_health_event.go index 0bec5b20..4bc0df63 100644 --- a/openflare_server/model/node_health_event.go +++ b/openflare_server/model/node_health_event.go @@ -40,3 +40,8 @@ func ListActiveNodeHealthEvents() (events []*NodeHealthEvent, err error) { err = DB.Where("status = ?", "active").Order("last_triggered_at desc").Find(&events).Error return events, err } + +func DeleteNodeHealthEvents(nodeID string) (deleted int64, err error) { + result := DB.Where("node_id = ?", nodeID).Delete(&NodeHealthEvent{}) + return result.RowsAffected, result.Error +} diff --git a/openflare_server/router/api-router.go b/openflare_server/router/api-router.go index 304b3a27..797c1733 100644 --- a/openflare_server/router/api-router.go +++ b/openflare_server/router/api-router.go @@ -126,6 +126,7 @@ func SetApiRouter(router *gin.Engine) { nodeRoute.POST("/", controller.CreateNode) nodeRoute.GET("/:id/agent-release", controller.GetNodeAgentRelease) nodeRoute.GET("/:id/observability", controller.GetNodeObservability) + nodeRoute.POST("/:id/observability/cleanup", controller.CleanupNodeHealthEvents) nodeRoute.POST("/:id/agent-update", controller.RequestNodeAgentUpdate) nodeRoute.POST("/:id/openresty-restart", controller.RequestNodeOpenrestyRestart) nodeRoute.POST("/:id/update", controller.UpdateNode) diff --git a/openflare_server/router/api_phase2_test.go b/openflare_server/router/api_phase2_test.go index b17b6faf..1064ac99 100644 --- a/openflare_server/router/api_phase2_test.go +++ b/openflare_server/router/api_phase2_test.go @@ -281,12 +281,41 @@ func TestPhase2AgentLifecycle(t *testing.T) { t.Fatal("expected node list to expose openresty message") } + if err := model.DB.Create(&model.NodeHealthEvent{ + NodeID: createdNode.NodeID, + EventType: "openresty_down", + Severity: service.NodeHealthSeverityCritical, + Status: service.NodeHealthEventStatusActive, + Message: "docker run openresty failed: bind 80 already allocated", + FirstTriggeredAt: time.Now().Add(-2 * time.Minute), + LastTriggeredAt: time.Now().Add(-time.Minute), + ReportedAt: time.Now().Add(-time.Minute), + }).Error; err != nil { + t.Fatalf("failed to insert node health event: %v", err) + } + observabilityResp := performJSONRequest(t, engine, adminToken, http.MethodGet, "/api/nodes/"+toString(createdNode.ID)+"/observability?hours=24&limit=20", nil) var observability service.NodeObservabilityView decodeResponseData(t, observabilityResp, &observability) if observability.NodeID != createdNode.NodeID { t.Fatalf("expected observability response for node %s, got %s", createdNode.NodeID, observability.NodeID) } + if len(observability.HealthEvents) != 1 { + t.Fatalf("expected observability response to include health events, got %+v", observability.HealthEvents) + } + + cleanupHealthResp := performJSONRequest(t, engine, adminToken, http.MethodPost, "/api/nodes/"+toString(createdNode.ID)+"/observability/cleanup", nil) + var cleanupHealthResult service.NodeHealthEventCleanupResult + decodeResponseData(t, cleanupHealthResp, &cleanupHealthResult) + if cleanupHealthResult.NodeID != createdNode.NodeID || cleanupHealthResult.DeletedCount != 1 { + t.Fatalf("unexpected node health cleanup result: %+v", cleanupHealthResult) + } + + observabilityAfterCleanupResp := performJSONRequest(t, engine, adminToken, http.MethodGet, "/api/nodes/"+toString(createdNode.ID)+"/observability?hours=24&limit=20", nil) + decodeResponseData(t, observabilityAfterCleanupResp, &observability) + if len(observability.HealthEvents) != 0 { + t.Fatalf("expected health events to be cleaned up, got %+v", observability.HealthEvents) + } restartResp := performJSONRequest(t, engine, adminToken, http.MethodPost, "/api/nodes/"+toString(createdNode.ID)+"/openresty-restart", nil) decodeResponseData(t, restartResp, &createdNode) diff --git a/openflare_server/service/node_observability.go b/openflare_server/service/node_observability.go index 1d85223c..37e07773 100644 --- a/openflare_server/service/node_observability.go +++ b/openflare_server/service/node_observability.go @@ -42,6 +42,11 @@ type NodeObservabilityTrends struct { DiskIO24h []DiskIOTrendPoint `json:"disk_io_24h"` } +type NodeHealthEventCleanupResult struct { + NodeID string `json:"node_id"` + DeletedCount int64 `json:"deleted_count"` +} + func GetNodeObservability(id uint, query NodeObservabilityQuery) (*NodeObservabilityView, error) { now := time.Now() node, err := model.GetNodeByID(id) @@ -105,6 +110,21 @@ func GetNodeObservability(id uint, query NodeObservabilityQuery) (*NodeObservabi }, nil } +func CleanupNodeHealthEvents(id uint) (*NodeHealthEventCleanupResult, error) { + node, err := model.GetNodeByID(id) + if err != nil { + return nil, err + } + deletedCount, err := model.DeleteNodeHealthEvents(node.NodeID) + if err != nil { + return nil, err + } + return &NodeHealthEventCleanupResult{ + NodeID: node.NodeID, + DeletedCount: deletedCount, + }, nil +} + func latestMetricSnapshot(snapshots []*model.NodeMetricSnapshot) *model.NodeMetricSnapshot { for _, snapshot := range snapshots { if snapshot != nil { diff --git a/openflare_server/service/node_update_test.go b/openflare_server/service/node_update_test.go index d0bae2e4..b4348ce6 100644 --- a/openflare_server/service/node_update_test.go +++ b/openflare_server/service/node_update_test.go @@ -1201,6 +1201,66 @@ func TestGetNodeObservabilityAllowsMissingProfile(t *testing.T) { } } +func TestCleanupNodeHealthEvents(t *testing.T) { + setupServiceTestDB(t) + + node := &model.Node{ + NodeID: "node-health-cleanup", + Name: "health-cleanup-edge", + IP: "10.0.0.72", + AgentToken: "token-health-cleanup", + AgentVersion: "v0.6.0", + NginxVersion: "1.27.1.2", + Status: NodeStatusOnline, + } + if err := node.Insert(); err != nil { + t.Fatalf("failed to insert node: %v", err) + } + + resolvedAt := time.Now().Add(-4 * time.Minute) + if err := model.DB.Create(&model.NodeHealthEvent{ + NodeID: node.NodeID, + EventType: "sync_error", + Severity: NodeHealthSeverityWarning, + Status: NodeHealthEventStatusActive, + Message: "checksum mismatch", + FirstTriggeredAt: time.Now().Add(-2 * time.Minute), + LastTriggeredAt: time.Now().Add(-time.Minute), + ReportedAt: time.Now().Add(-time.Minute), + }).Error; err != nil { + t.Fatalf("failed to insert first node health event: %v", err) + } + if err := model.DB.Create(&model.NodeHealthEvent{ + NodeID: node.NodeID, + EventType: "openresty_down", + Severity: NodeHealthSeverityCritical, + Status: NodeHealthEventStatusResolved, + Message: "openresty exited unexpectedly", + FirstTriggeredAt: time.Now().Add(-10 * time.Minute), + LastTriggeredAt: time.Now().Add(-5 * time.Minute), + ReportedAt: time.Now().Add(-5 * time.Minute), + ResolvedAt: &resolvedAt, + }).Error; err != nil { + t.Fatalf("failed to insert second node health event: %v", err) + } + + result, err := CleanupNodeHealthEvents(node.ID) + if err != nil { + t.Fatalf("CleanupNodeHealthEvents failed: %v", err) + } + if result.NodeID != node.NodeID || result.DeletedCount != 2 { + t.Fatalf("unexpected cleanup result: %+v", result) + } + + events, err := model.ListNodeHealthEvents(node.NodeID, false, 10) + if err != nil { + t.Fatalf("failed to list node health events after cleanup: %v", err) + } + if len(events) != 0 { + t.Fatalf("expected node health events to be removed, got %+v", events) + } +} + func TestGetDashboardOverview(t *testing.T) { setupServiceTestDB(t) diff --git a/openflare_server/web/features/nodes/api/nodes.ts b/openflare_server/web/features/nodes/api/nodes.ts index f8539022..b2ac965a 100644 --- a/openflare_server/web/features/nodes/api/nodes.ts +++ b/openflare_server/web/features/nodes/api/nodes.ts @@ -85,3 +85,12 @@ export function getNodeObservability( `/nodes/${id}/observability${query ? `?${query}` : ''}`, ); } + +export function cleanupNodeHealthEvents(id: number) { + return apiRequest<{ node_id: string; deleted_count: number }>( + `/nodes/${id}/observability/cleanup`, + { + method: 'POST', + }, + ); +} diff --git a/openflare_server/web/features/nodes/components/node-detail-page.tsx b/openflare_server/web/features/nodes/components/node-detail-page.tsx index 646f2e17..de8dd7bf 100644 --- a/openflare_server/web/features/nodes/components/node-detail-page.tsx +++ b/openflare_server/web/features/nodes/components/node-detail-page.tsx @@ -20,6 +20,7 @@ import { ConfigVersionSnapshotModal } from '@/features/config-versions/component import type { ConfigVersionSummary } from '@/features/config-versions/types'; import { getApplyLogs } from '@/features/apply-logs/api/apply-logs'; import { + cleanupNodeHealthEvents, deleteNode, getNodeAgentRelease, getNodeObservability, @@ -250,6 +251,8 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { const [healthEventFilter, setHealthEventFilter] = useState('all'); const [activeTab, setActiveTab] = useState('dashboard'); + const [isHealthEventCleanupModalOpen, setHealthEventCleanupModalOpen] = + useState(false); const nodesQuery = useQuery({ queryKey: nodesQueryKey, @@ -373,6 +376,27 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { }, }); + const cleanupHealthEventsMutation = useMutation({ + mutationFn: () => cleanupNodeHealthEvents(Number(nodeId)), + onSuccess: async (result) => { + setFeedback({ + tone: 'success', + message: + result.deleted_count > 0 + ? `已清理 ${result.deleted_count} 条健康事件日志。` + : '当前没有可清理的健康事件日志。', + }); + setHealthEventCleanupModalOpen(false); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['node-observability', nodeId] }), + queryClient.invalidateQueries({ queryKey: ['dashboard', 'overview'] }), + ]); + }, + onError: (error) => { + setFeedback({ tone: 'danger', message: getErrorMessage(error) }); + }, + }); + const handleDelete = () => { if (!node) { return; @@ -1215,6 +1239,20 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { setHealthEventCleanupModalOpen(true)} + > + {cleanupHealthEventsMutation.isPending + ? '清理中...' + : '清理日志'} + + } > {observability?.health_events.length ? (
@@ -1703,6 +1741,49 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { }} /> + setHealthEventCleanupModalOpen(false)} + title="清理健康事件日志" + description={ + node + ? `确认清理节点“${node.name}”的健康事件时间线吗?已清理的历史记录将立即从当前页面移除,后续只有新的节点上报才会再次出现。` + : '确认清理当前节点的健康事件时间线吗?' + } + footer={ +
+ setHealthEventCleanupModalOpen(false)} + > + 取消 + + { + setFeedback(null); + cleanupHealthEventsMutation.mutate(); + }} + > + {cleanupHealthEventsMutation.isPending ? '清理中...' : '确认清理'} + +
+ } + > + {cleanupHealthEventsMutation.isError ? ( + + ) : ( +
+

该操作会删除当前节点已记录的全部健康事件,包括活动中与已恢复事件。

+

这不会影响节点后续继续上报新的健康事件,但现有时间线与相关摘要会立即刷新。

+
+ )} +
+ setIsAgentUpdateModalOpen(false)}