103 lines
2.8 KiB
Go
103 lines
2.8 KiB
Go
// Package admin serves the operator's view of crossbar: the health table and the routes as JSON,
|
|
// mounted at /_crossbar/ on the same listener as the proxy. The shape of /_crossbar/hosts is
|
|
// fixed so operators can read why a request went where it went.
|
|
package admin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"git.wntrmute.dev/kyle/crossbar/internal/config"
|
|
"git.wntrmute.dev/kyle/crossbar/internal/health"
|
|
)
|
|
|
|
// Hosts is what the admin handler needs from the health table.
|
|
type Hosts interface {
|
|
All() map[string]health.Status
|
|
}
|
|
|
|
type HostView struct {
|
|
Healthy bool `json:"healthy"`
|
|
Loaded []string `json:"loaded"` // never null: an empty slice when nothing is loaded
|
|
LastOK string `json:"last_ok"` // time.RFC3339 in UTC, or "" if never
|
|
LastErr string `json:"last_err"`
|
|
}
|
|
|
|
type RouteView struct {
|
|
Hosts []string `json:"hosts"`
|
|
DefaultModel string `json:"default_model"`
|
|
}
|
|
|
|
// Handler serves GET /_crossbar/hosts and GET /_crossbar/routes. Any other method on those paths is
|
|
// a 405 with an Allow: GET header; anything else under the handler is a 404.
|
|
func Handler(cfg *config.Config, h Hosts) http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/_crossbar/hosts", hostsHandler(h))
|
|
mux.HandleFunc("/_crossbar/routes", routesHandler(cfg))
|
|
mux.HandleFunc("/", notFound)
|
|
return mux
|
|
}
|
|
|
|
func hostsHandler(h Hosts) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
wrongMethod(w)
|
|
return
|
|
}
|
|
views := make(map[string]HostView, len(h.All()))
|
|
for name, s := range h.All() {
|
|
views[name] = hostView(s)
|
|
}
|
|
writeJSON(w, http.StatusOK, views)
|
|
}
|
|
}
|
|
|
|
func routesHandler(cfg *config.Config) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
wrongMethod(w)
|
|
return
|
|
}
|
|
views := make(map[string]RouteView, len(cfg.Routes))
|
|
for name, route := range cfg.Routes {
|
|
hosts := make([]string, len(route.Hosts))
|
|
copy(hosts, route.Hosts)
|
|
views[name] = RouteView{Hosts: hosts, DefaultModel: route.DefaultModel}
|
|
}
|
|
writeJSON(w, http.StatusOK, views)
|
|
}
|
|
}
|
|
|
|
func hostView(s health.Status) HostView {
|
|
loaded := s.Loaded
|
|
if loaded == nil {
|
|
loaded = []string{}
|
|
}
|
|
lastOK := ""
|
|
if !s.LastOK.IsZero() {
|
|
lastOK = s.LastOK.UTC().Format(time.RFC3339)
|
|
}
|
|
return HostView{
|
|
Healthy: s.Healthy,
|
|
Loaded: loaded,
|
|
LastOK: lastOK,
|
|
LastErr: s.LastErr,
|
|
}
|
|
}
|
|
|
|
func wrongMethod(w http.ResponseWriter) {
|
|
w.Header().Set("Allow", "GET")
|
|
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
|
}
|
|
|
|
func notFound(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|