Files
orca/internal/daemon/server.go
T
Jon Chery fc94326b0e feat(P00): deprecation sweep + bash tooling gate + render contract + doc banners (v0.9 P00)
P00 — Re-architecture Foundation (deprecation/migration/test-infra/persona/docs).

Deprecation sweep (REQ-068, REQ-072, REQ-089):
- Add // Deprecated: doc comments to internal/daemon (R-001), internal/transport
  (REQ-073), internal/security/ca.go+csr.go (D-101/REQ-076), internal/engine/
  dispatcher.go+peer.go (CLI-side scheduler), internal/cli/daemon.go.
- orca daemon emits slog.Warn deprecation banner on every run (ungated); fires
  R-001 + v0.10-P05 drain-and-stop + v0.10-P14 deletion.
- orca cert and orca node join (mTLS path) emit deprecation warnings; proxmox
  SSH path (the v0.9 replacement) does not warn.
- Add --no-deprecation-warnings global flag on root command (PersistentPreRunE)
  for orca upgrade migrations.
- 12 new daemon/cert/node deprecation tests in internal/cli/daemon_test.go
  (cli coverage 81.9%, warnDeprecated 100%).
- Add DEPRECATED banners to v0.8 sections of ARCHITECTURE.md (verified the
  v0.9 supersession section + Supersession Table from prior turn are present).

Bash tooling gate (grill C-06, C-15, C-16, C-17, C-18):
- scripts/tests/test_helper.bash + example_test.bash — bats framework + helpers.
- scripts/lib/orca-log.sh — slog-compatible JSON logging to syslog (C-17).
- scripts/orca-verify-render.sh — render-contract validator skeleton (C-16).
- scripts/tests/orca-log_test.bash + orca-verify-render_test.bash — 20 bats
  tests total (happy + failure paths per C-15).
- .shellcheckrc — project shellcheck config.
- Makefile: test-bash + lint-bash targets (graceful skip if tools missing);
  wired into test + lint targets.
- internal/emit/contract.go + contract_test.go — versioned JSON render
  contract (orca.emit/v1) between Go emitters and bash appliers (C-16).
- .ciagent/BASH_CAPABILITY_MAP_v0.9.md — maps shipped internal/transport
  capabilities to bash-side equivalents or accepted drops (C-18).
- D-186 recorded in PROJECT.md: bash exempt from Go coverage gate; compensating
  control is bats + shellcheck + shfmt (C-06).

verify-reqs: 90 requirements consistent. Build/test/lint/fmt all green.
20 bats tests pass. Go tests pass. No v0.8 code deleted — only marked deprecated
(deletion deferred to v0.10-P14 per REQ-090 dual-write window).

---ci---
project: orca
phase: P00
milestone: v0.9
status: execute
---/ci---
2026-08-05 16:26:26 +00:00

178 lines
5.6 KiB
Go

// Package daemon implements the orca HTTP daemon.
//
// The daemon exposes health endpoints (/healthz, /readyz), a status endpoint
// (/v1/status), and a v1 resource API for jobs, nodes, and tasks. All handlers
// follow the project conventions:
//
// - context.Context propagated to all I/O
// - errors wrapped with %w
// - structured JSON via writeJSON
// - no secrets in logs
// - input validation on path/query/body
//
// Deprecated: v0.9 re-architecture replaces this with SSH-push to bare
// servers (no orca binary on servers) per R-001. The orca daemon is
// repurposed to drain-and-stop in v0.10-P05 and scheduled for deletion
// in v0.10-P14. See .ciagent/PRD_v0.9.md R-001/R-006. The dual-write
// window (REQ-090/REQ-085) keeps this package compiling until v0.10-P14.
package daemon
import (
"context"
"database/sql"
"errors"
"log/slog"
"net/http"
"sync/atomic"
"time"
)
// Server is the orca HTTP daemon. It holds shared dependencies and lifecycle
// state. Construct it with NewServer, then call Start/Shutdown.
type Server struct {
db *sql.DB
log *slog.Logger
addr string
ready atomic.Bool
httpServer *http.Server
pprofServer *http.Server
// mtls is non-nil after StartMTLS has been called; nil otherwise.
// Plaintext HTTP and mTLS are mutually exclusive — a Server is
// either in plaintext mode (default, v0.1 compat) or mTLS mode
// (v0.2 P01 forward).
mtls *MTLSState
// dispatch is the orca.v1.Dispatch service mounted on
// /orca.v1.Dispatch/* (P02). Optional — nil if no Dispatcher
// was registered. P02 wires this via RegisterDispatch.
dispatch *DispatchHandlers
}
// Options configures a new Server.
type Options struct {
DB *sql.DB
Log *slog.Logger
Addr string
Actor string // used for audit logging from API requests
// PprofAddr enables the pprof endpoint on a separate listener
// when non-empty (e.g. "127.0.0.1:6060"). Default "" disables it.
// The pprof listener is unauthenticated and operator-only; never
// expose it publicly (AD-024).
PprofAddr string
}
// NewServer constructs a Server with the default mux and route table.
func NewServer(opts Options) *Server {
if opts.Log == nil {
opts.Log = slog.Default()
}
if opts.Addr == "" {
opts.Addr = ":8080"
}
if opts.Actor == "" {
opts.Actor = "api"
}
s := &Server{
db: opts.DB,
log: opts.Log,
addr: opts.Addr,
}
s.httpServer = &http.Server{
Addr: opts.Addr,
Handler: s.mux(),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
if opts.PprofAddr != "" {
ps, perr := StartPprof(opts.PprofAddr, opts.Log)
if perr != nil {
s.log.Error("pprof start failed", slog.String("component", "daemon"), slog.Any("err", perr))
} else {
s.pprofServer = ps
}
}
return s
}
// Addr returns the configured listen address.
func (s *Server) Addr() string { return s.addr }
// MarkReady flips the readiness flag to true. The /readyz endpoint returns
// 200 only when this flag is set AND the database is reachable.
func (s *Server) MarkReady() { s.ready.Store(true) }
// MarkNotReady flips the readiness flag to false. Called at shutdown start
// so load balancers stop routing traffic.
func (s *Server) MarkNotReady() { s.ready.Store(false) }
// Ready reports the current readiness flag.
func (s *Server) Ready() bool { return s.ready.Load() }
// mux builds the route table. Handlers are split across files:
// - health.go /healthz, /readyz, /v1/status
// - jobs_handler.go /v1/jobs/*
// - nodes_handler.go /v1/nodes/*
// - tasks_handler.go /v1/tasks/*
// - dispatch_handler.go /orca.v1.Dispatch/* (P02; mounted only if
// RegisterDispatch was called)
func (s *Server) mux() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", s.handleHealthz)
mux.HandleFunc("/readyz", s.handleReadyz)
mux.HandleFunc("/v1/status", s.handleStatus)
mux.HandleFunc("/v1/jobs", s.handleJobsCollection)
mux.HandleFunc("/v1/jobs/", s.handleJobsItem)
mux.HandleFunc("/v1/nodes", s.handleNodesCollection)
mux.HandleFunc("/v1/tasks", s.handleTasksCollection)
if s.dispatch != nil {
s.dispatch.Mount(mux)
}
return loggingMiddleware(s.log, mux)
}
// RegisterDispatch attaches the orca.v1.Dispatch service to the
// daemon. Call before Start(). The dispatch routes are mounted at
// /orca.v1.Dispatch/Submit and /orca.v1.Dispatch/Status.
func (s *Server) RegisterDispatch(h *DispatchHandlers) {
if h == nil {
return
}
s.dispatch = h
s.log.Info("dispatch handlers registered",
slog.String("component", "daemon"),
slog.String("submit", "/orca.v1.Dispatch/Submit"),
slog.String("status", "/orca.v1.Dispatch/Status"),
)
}
// Start runs the HTTP server. Returns http.ErrServerClosed on clean shutdown.
func (s *Server) Start() error {
s.log.Info("daemon starting",
slog.String("addr", s.addr),
slog.String("component", "daemon"))
return s.httpServer.ListenAndServe()
}
// Shutdown gracefully stops the server, bounded by ctx. It also flips the
// readiness flag to false so /readyz returns 503 immediately.
func (s *Server) Shutdown(ctx context.Context) error {
s.MarkNotReady()
s.log.Info("daemon shutting down", slog.String("component", "daemon"))
if s.pprofServer != nil {
if perr := s.pprofServer.Shutdown(ctx); perr != nil {
s.log.Error("pprof shutdown failed", slog.String("component", "daemon"), slog.Any("err", perr))
}
}
return s.httpServer.Shutdown(ctx)
}
// IsShutdownErr reports whether err is the expected error from a stopped server.
func IsShutdownErr(err error) bool {
return errors.Is(err, http.ErrServerClosed)
}