Files

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:

  1. GET /_crossbar/hosts → 200, Content-Type: application/json, a JSON object mapping host name to HostView built from h.All().
  2. GET /_crossbar/routes → 200, JSON object mapping route name to RouteView (copy the hosts slice; do not hand out the config's).
  3. Any other method on those two paths → 405 with an Allow: GET header and a JSON {"error":"method not allowed"} body. Any other path under the handler → 404 JSON {"error":"not found"}. (An http.ServeMux with the two exact paths plus a / fallback does this.)

Produces cmd/crossbar/main.go, package main:

  • Flag -config (default crossbar.toml). config.Load; on error print crossbar: <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 with go table.Run(ctx) where ctx comes from signal.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" with addr before ListenAndServe. On signal, log "shutting down" and srv.Shutdown with a 10 s timeout; exit 0. A ListenAndServe error other than http.ErrServerClosed exits 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.go and cmd/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 gate prints gate: ok.
  • cmp cmd/fakeupstream/main.go docs/plans/v0/_files/cmd/fakeupstream/main.go and the same for example.toml and internal/admin/admin_test.go print nothing.

Stop and report if

  • bin/crossbar does not exit 0 on SIGTERM within the timeout.