5.3 KiB
v0 task 04: admin endpoints, the crossbar binary, the fake upstream
Branch: v0 (run git switch v0; git status --short must be empty, otherwise stop)
Commit subject: Add the admin endpoints, the crossbar binary and the fake upstream
Goal
Make crossbar runnable: internal/admin shows the health table and the routes as JSON,
cmd/crossbar wires config, health, proxy and admin into one HTTP server with graceful shutdown,
and the given cmd/fakeupstream stands in for a router so the whole thing can be exercised
without a real model (task 05 does that).
Context
Operators read /_crossbar/hosts to see why a request went where it went, so its shape is
fixed: every host, healthy, loaded (always a JSON array, never null), last_ok as RFC 3339
in UTC or "", last_err. The admin handler is mounted at /_crossbar/ on the same listener as
the proxy; the proxy already refuses /{route}/_crossbar/… (task 03).
Files
- Copy (never edit):
cmd/fakeupstream/main.go,example.toml,internal/admin/admin_test.go - Create:
internal/admin/admin.go,cmd/crossbar/main.go - Modify:
docs/implementer-log.md
Interfaces
Produces, in internal/admin/admin.go, package admin:
// 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.
func Handler(cfg *config.Config, h Hosts) http.Handler
Rules the tests check:
GET /_crossbar/hosts→ 200,Content-Type: application/json, a JSON object mapping host name toHostViewbuilt fromh.All().GET /_crossbar/routes→ 200, JSON object mapping route name toRouteView(copy the hosts slice; do not hand out the config's).- Any other method on those two paths → 405 with an
Allow: GETheader and a JSON{"error":"method not allowed"}body. Any other path under the handler → 404 JSON{"error":"not found"}. (Anhttp.ServeMuxwith the two exact paths plus a/fallback does this.)
Produces cmd/crossbar/main.go, package main:
- Flag
-config(defaultcrossbar.toml).config.Load; on error printcrossbar: <err>to stderr and exit 1. - Logger:
slog.New(slog.NewTextHandler(os.Stderr, nil)). health.New(<name -> BaseURL from cfg.Hosts>, cfg.PollInterval.Duration, nil); run it withgo table.Run(ctx)wherectxcomes fromsignal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM).http.ServeMux:mux.Handle("/_crossbar/", admin.Handler(cfg, table)),mux.Handle("/", proxy.New(cfg, table, log)).&http.Server{Addr: cfg.Listen, Handler: mux, ReadHeaderTimeout: 10 * time.Second}. Log"listening"withaddrbeforeListenAndServe. On signal, log"shutting down"andsrv.Shutdownwith a 10 s timeout; exit 0. AListenAndServeerror other thanhttp.ErrServerClosedexits 1 with the message.
cmd/fakeupstream/main.go is given; read its package comment so you know what it does, and do
not change it.
Steps
- 1. Copy.
git switch v0
mkdir -p cmd/fakeupstream cmd/crossbar internal/admin
cp docs/plans/v0/_files/cmd/fakeupstream/main.go cmd/fakeupstream/
cp docs/plans/v0/_files/example.toml .
cp docs/plans/v0/_files/internal/admin/admin_test.go internal/admin/
- 2. See the test fail.
go test ./internal/admin/. Expected: it does not compile. - 3. Write
internal/admin/admin.goandcmd/crossbar/main.go.gofmt -w . - 4. See the test pass.
go test -race -count=1 ./internal/admin/. Expected:ok. - 5. Build and run for three seconds.
make build
timeout --preserve-status --signal=TERM 3 bin/crossbar -config example.toml; echo "exit=$?"
Expected on stderr: a line containing listening and addr=127.0.0.1:17777, then
shutting down; then exit=0 (--preserve-status makes timeout report crossbar's own exit code; without it GNU timeout prints 124 for any child it had to signal). (example.toml names two upstreams that are not running; the
health table simply records them unhealthy — that is fine here.)
bin/crossbar -config /nonexistent.toml; echo "exit=$?"
Expected: crossbar: config: open /nonexistent.toml: no such file or directory and exit=1.
- 6. Run the gate.
make gate. Expected last line:gate: ok. - 7. Log and commit. Row
v0/04-admin-main.bin/is build output: do not add it.
git add internal/admin cmd/crossbar cmd/fakeupstream example.toml docs/implementer-log.md
git commit
Done when
- The admin test passes, the three-second run exits 0 with both log lines, the missing-config
run exits 1,
make gateprintsgate: ok. cmp cmd/fakeupstream/main.go docs/plans/v0/_files/cmd/fakeupstream/main.goand the same forexample.tomlandinternal/admin/admin_test.goprint nothing.
Stop and report if
bin/crossbardoes not exit 0 on SIGTERM within the timeout.