Skip to content

feat(api): fleet observability endpoints — read-only Slice B surface (14/14 ACs, 100%)#427

Merged
remyluslosius merged 1 commit into
mainfrom
feat/api-fleet-observability
May 29, 2026
Merged

feat(api): fleet observability endpoints — read-only Slice B surface (14/14 ACs, 100%)#427
remyluslosius merged 1 commit into
mainfrom
feat/api-fleet-observability

Conversation

@remyluslosius
Copy link
Copy Markdown
Contributor

Summary

Closes the visibility gap identified after Slice B landed: the
fleetrollup package was a Go-level API with no HTTP surface. This PR
wraps it with five read-only endpoints.

Endpoint Returns
`GET /api/v1/fleet/score` passing/total compliance fraction
`GET /api/v1/fleet/liveness` 4-bucket reachability counts
`GET /api/v1/fleet/top-failing-rules` rules with most failing hosts
`GET /api/v1/fleet/top-failing-hosts` hosts with most failing rules
`GET /api/v1/fleet/recent-changes` transactions DESC, optional `?since` cursor

Spec

`app/specs/api/fleet-observability.spec.yaml` (status: approved) — 14 ACs across 6 constraints, all at 100%.

RBAC + pagination

  • Every endpoint requires `system:read` (in RoleViewer and above)
  • Anonymous returns 403 `authz.permission_denied` (per system-rbac AC-09)
  • Paginated endpoints default to `limit=50`, max 1000
  • Out-of-range `?limit` returns 400 `pagination.limit_exceeded`
  • Malformed `?since` returns 400 (oapi-codegen InvalidParamFormatError)

Architectural invariant (AC-13)

`fleet_handlers.go` and `fleet_helpers.go` contain no SQL, no direct `h.pool` access, and must reference `h.fleet` — enforced by regex source inspection. All data access goes through `internal/fleetrollup.Service`.

Verification

```
go vet ./... clean
go test -race -count=1 PASS (9.1s with Postgres)
specter parse PASS (api-fleet-observability@1.0.0)
specter check PASS
specter coverage api-fleet-observability 14/14 (100%)
```

Test plan

  • CI Quality + security gates pass (vet, lint, vuln, test-race, specter sync)
  • Confirm `api-fleet-observability` reaches 100% in `specter coverage`
  • Manual smoke after merge: `curl -H "Cookie: session=..." .../api/v1/fleet/score`

…(14/14 ACs)

Closes the visibility gap identified after Slice B landed: the Go
rebuild had Slice B's fleetrollup package as a Go-level API but
exposed nothing over HTTP. This PR adds five read-only endpoints
that wrap the existing fleetrollup.Service methods.

Spec
  New: app/specs/api/fleet-observability.spec.yaml (status: approved).
  14 ACs across 6 constraints.

Endpoints
  GET /api/v1/fleet/score              passing/total compliance score
  GET /api/v1/fleet/liveness           host counts by reachability
  GET /api/v1/fleet/top-failing-rules  ranked by failing host count
  GET /api/v1/fleet/top-failing-hosts  ranked by failing rule count
  GET /api/v1/fleet/recent-changes     transactions DESC, ?since cursor

Every endpoint requires system:read (in RoleViewer and above);
anonymous returns 403 authz.permission_denied per system-rbac AC-09.
Paginated endpoints default to limit=50, max 1000; out-of-range
returns 400 pagination.limit_exceeded.

Implementation
  - api/openapi.yaml: five GET routes + five typed response schemas
    (FleetScore, FleetLiveness, FleetTopFailingRules,
    FleetTopFailingHosts, FleetRecentChanges + entry types).
    Re-ran make generate-api to refresh server.gen.go.
  - internal/server/fleet_handlers.go: five handlers. Each is a thin
    wrapper: RBAC gate -> parse + validate -> delegate to
    h.fleet.<method> -> JSON-encode. Zero SQL, zero pool access,
    zero aggregation in the handler layer (AC-13 enforces).
  - internal/server/fleet_helpers.go: validatePaginatedLimit clamps
    caller input to [1, fleetrollup.MaxLimit]; nilOrTime handles
    optional ?since cursor.
  - internal/server/handlers.go: fleetrollup.Service wired into the
    handlers struct.
  - internal/server/api_helpers_test.go: added TRUNCATE for the
    Slice B tables so fresh-API-server fixtures start clean.
  - .secrets.baseline: refreshed for the new high-entropy lines in
    server.gen.go (base64-encoded embedded OpenAPI spec chunks).

Tests (14/14 ACs, all under -race)
  api_fleet_test.go covers AC-01..AC-12 + AC-14: score math,
                       empty-fleet zeros, four-bucket liveness,
                       top-failing-rules/hosts ordering and limit cap,
                       recent-changes ordering + since cursor,
                       malformed since 400, limit overflow / negative
                       400 with pagination.limit_exceeded, anonymous
                       403 authz.permission_denied, viewer happy
                       path on all 5 endpoints, POST returns 405.
  fleet_source_test.go covers AC-13: regex scan of fleet_handlers.go
                       + fleet_helpers.go confirms no SQL keywords,
                       no h.pool access, and h.fleet is referenced.

Verification
  go vet ./...                  clean
  go test -race -count=1        PASS (9.1s with Postgres)
  specter parse                 PASS (api-fleet-observability@1.0.0)
  specter check                 PASS
  specter coverage              api-fleet-observability 14/14 (100%)

Spec: app/specs/api/fleet-observability.spec.yaml
@remyluslosius remyluslosius enabled auto-merge (squash) May 29, 2026 22:05
@remyluslosius remyluslosius merged commit 34ac97e into main May 29, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant