feat(15A): probe ingest endpoint + Redis store (tsk_rBPr0Xuy10bz)

Add `server/internal/scheduler/probe/` package:

* types.go   – frozen ReportRequest/NodeReport/L1-L3 schema (15B/15C contract)
* store.go   – Redis read/write: probe:{nodeId}:{vantage} TTL 30min,
               probe:hb:{probeId} TTL 15min; missing key = no data, not failure;
               SnapshotsByNode + AliveProbes read interfaces for 15D
* ingest.go  – POST /probe/report handler: per-probe HMAC-SHA256 auth
               (X-Probe-Id / X-Probe-Ts / X-Probe-Sign), ±300s time window,
               constant-time comparison, idempotent replay via Redis SetNX

Wire route in cmd/server/main.go (opt-in via PROBE_SECRETS env var).

18/18 tests pass (go test ./internal/scheduler/probe/... -count=1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 11:56:34 +08:00
parent 787151245e
commit fb3389a94b
5 changed files with 1164 additions and 0 deletions
+37
View File
@@ -9,6 +9,8 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/wangjia/pangolin/server/internal/redisutil"
"github.com/wangjia/pangolin/server/internal/scheduler/probe"
)
func main() {
@@ -33,8 +35,43 @@ func main() {
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// --- Probe ingest route (optional) ---
// Requires:
// PROBE_SECRETS JSON object mapping probeId → HMAC secret, e.g.
// '{"probe-sg-01":"s3cr3t1","probe-jp-01":"s3cr3t2"}'
// REDIS_ADDR Redis address, default 127.0.0.1:6379
// REDIS_PASSWORD Redis password (optional)
//
// If PROBE_SECRETS is empty the route is not registered and the server
// starts normally without the probe ingest endpoint.
probeSecretsJSON := os.Getenv("PROBE_SECRETS")
if probeSecretsJSON != "" {
redisAddr := getenvDefault("REDIS_ADDR", "127.0.0.1:6379")
rdb, err := redisutil.New(redisAddr, os.Getenv("REDIS_PASSWORD"), 0)
if err != nil {
log.Printf("probe: redis connect failed (%v) probe route disabled", err)
} else {
var secretMap map[string]string
if err := json.Unmarshal([]byte(probeSecretsJSON), &secretMap); err != nil {
log.Fatalf("probe: invalid PROBE_SECRETS JSON: %v", err)
}
reg := probe.NewMapRegistry(secretMap)
st := probe.NewStore(rdb)
h := probe.NewIngestHandler(reg, st)
r.Post("/probe/report", h.ServeHTTP)
log.Printf("probe ingest route registered (%d probe(s))", len(secretMap))
}
}
log.Printf("pangolin server listening on %s", *addr)
if err := http.ListenAndServe(*addr, r); err != nil {
log.Fatalf("server error: %v", err)
}
}
func getenvDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}