// Package httpserver exposes health and authenticated task-management endpoints. package httpserver import ( "context" "encoding/json" "errors" "net/http" "sync/atomic" "time" "git.yuntuo.hk/k2safe/orbit/internal/buildinfo" "git.yuntuo.hk/k2safe/orbit/internal/consoleauth" "git.yuntuo.hk/k2safe/orbit/internal/observability" "git.yuntuo.hk/k2safe/orbit/pulse/service" ) type Checker func(context.Context) error // Readiness 将数据库/结构检查与进程排空状态合并为一个就绪门控。 // 收到终止信号后先调用 BeginDrain,再停止接收新请求;存量请求仍由 // http.Server.Shutdown 在限定时间内完成。这样编排平台有机会先看到 503。 type Readiness struct { checker Checker draining atomic.Bool } // NewReadiness 创建可在优雅下线期间切换为未就绪的门控。 func NewReadiness(checker Checker) *Readiness { return &Readiness{checker: checker} } // Check 执行就绪检查;排空开始后始终返回内部错误,由 HTTP 层统一隐藏细节。 func (r *Readiness) Check(ctx context.Context) error { if r == nil || r.draining.Load() { return errors.New("server is draining") } if r.checker == nil { return errors.New("readiness checker is unavailable") } return r.checker(ctx) } // BeginDrain 先于 http.Server.Shutdown 调用,阻止排空窗口内的新流量。 func (r *Readiness) BeginDrain() { if r != nil { r.draining.Store(true) } } func New(addr string, ready Checker, tasks ...*service.Service) *http.Server { return newServer(addr, ready, nil, tasks...) } // NewWithConsoleAuth 创建带数据库控制台登录会话的 HTTP 服务。 // 保留 New 供只测试 API 的调用方使用;生产入口应传入 ConsoleAuthStore。 func NewWithConsoleAuth(addr string, ready Checker, consoleAuth consoleauth.Store, tasks ...*service.Service) *http.Server { return newServer(addr, ready, consoleAuth, tasks...) } func newServer(addr string, ready Checker, consoleAuth consoleauth.Store, tasks ...*service.Service) *http.Server { mux := http.NewServeMux() write := func(w http.ResponseWriter, status int, body map[string]string) { // 探针响应不能被代理或浏览器缓存,否则下线节点可能继续被路由流量。 w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) } mux.HandleFunc("GET /health/live", func(w http.ResponseWriter, r *http.Request) { write(w, http.StatusOK, map[string]string{"status": "alive"}) }) mux.HandleFunc("GET /health/ready", func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() if ready == nil || ready(ctx) != nil { // 给负载均衡器一个明确的短暂重试提示;真正的重试节奏仍由探针策略控制。 w.Header().Set("Retry-After", "1") write(w, http.StatusServiceUnavailable, map[string]string{"status": "not_ready"}) return } write(w, http.StatusOK, map[string]string{"status": "ready", "scope": "database_and_schema"}) }) mux.HandleFunc("GET /version", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(buildinfo.Current()) }) if len(tasks) > 0 && tasks[0] != nil { registerTasks(mux, tasks[0]) registerHistory(mux, tasks[0]) } server := &http.Server{Addr: addr, Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 16 << 10} RegisterConsole(server, consoleAuth) return server } // WrapConsoleAuth 必须在所有 Register* 路由完成后调用,因为路由注册器需要 // 直接访问底层 ServeMux;包装完成后,浏览器会话才会转换为内部 Bearer 请求。 func WrapConsoleAuth(server *http.Server, consoleAuth consoleauth.Store, scopes ...consoleauth.Scopes) { if server == nil || consoleAuth == nil { return } server.Handler = ConsoleSessionMiddleware(server.Handler, consoleAuth, scopes...) } // WrapMetrics must run after all Register* functions because those functions // intentionally register routes on the underlying ServeMux. func WrapMetrics(server *http.Server, metrics *observability.Registry) { if server == nil || metrics == nil { return } server.Handler = metrics.Middleware(server.Handler) } // NewMetricsServer uses a distinct listener so public API routing cannot expose // operational aggregates. It does not provide business or database endpoints. func NewMetricsServer(addr string, metrics *observability.Registry) *http.Server { mux := http.NewServeMux() if metrics != nil { mux.Handle("GET /metrics", metrics) } return &http.Server{Addr: addr, Handler: mux, ReadHeaderTimeout: 2 * time.Second, ReadTimeout: 3 * time.Second, WriteTimeout: 5 * time.Second, IdleTimeout: 15 * time.Second, MaxHeaderBytes: 8 << 10} }