Repository navigation
dashboard: read-only incident API (Pro only) - #579
Merged
Merged
Conversation
Add Dashboard config section mirroring HealthCheck, gated behind Pro via feature.Limits.Dashboard. Dashboard runs on a separate listener (default port 8061), off by default. Validates port range and no collision with healthCheck.port.
Add internal/dashboard package with a separate HTTP listener serving
GET /api/incidents, GET /api/incidents/{id}, and GET /api/status.
Bearer token auth with constant-time compare and per-IP rate limiting
on failures. Pro only via feature.Limits.Dashboard. Add
Manager.Incident(id) for single-incident lookup.
Create the dashboard server in bootstrap (Pro only, off by default), open its listener in generation (optional, non-fatal), serve it as an optional component in serve, stop it on shutdown, and set its incident reader and status source from the pipeline. Bearer token read from KWATCH_DASHBOARD_TOKEN env var.
Add dashboard values, schema, Service, Secret, NetworkPolicy rule, and config catalog entries. Dashboard is Pro only and off by default; the bearer token lives in the kwatch-dashboard Secret (auto-generated when no existingSecret is set).
Add Facts []string to incident.Record and Incident, bounded in count and length. Bump storage SchemaVersion from 2 to 3 with reset path. Pipeline stores investigation evidence on each decision so the dashboard detail page shows log lines after a restart.
Add last success/last error timestamps to ProviderHealth, wire delivery health into /api/status, and add a redaction test verifying secrets never appear in API output.
Fix architecture violations: inject clock.Clock into dashboard auth rate limiter and use delivery manager's now() instead of time.Now() in provider health. Bound dashboard API responses at 64 KiB. Log dashboard API access with remote addr and endpoint (never the token).
Extract bootstrap struct construction into newBootstrapDeps so newBootstrap stays under the 50-line funlen limit after adding the dashboard server wiring.
Deadcode check flags them as unreachable. Remove rather than adding to the allowlist (project guidance: keep allowlist empty). Re-add when token rotation or server error reporting is actually needed.
… list limit, keep Helm token
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add a separate, read-only incident dashboard API served on its own listener, Pro only and off by default.
What this adds
dashboard.enabled(default false) anddashboard.port(default 8061), validated against ahealthCheck.portcollision. Flows through RuntimeConfig and the config catalog.Dashboard: trueinProEntitlementsonly. On Community the server is never built, so the dashboard port is never opened (no listener, no 402 responses). Gating fails closed: a nil entitlement is Community.internal/dashboard/): separate listener, not on the health/metrics mux. All responses are JSON; errors are{"error": "..."}.GET /api/incidents?state=active|resolved&namespace=&limit=: newest first (by first seen). Response{"incidents": [...], "total": N, "truncated": bool}.limitdefaults to 100, is capped at 500; a list that would exceed 64 KiB is cut to fit and markedtruncated: trueinstead of failing. Unknownstateor a non-positivelimitanswers 400.GET /api/incidents/{id}: list fields plusrootCause,suggestedFix,timeline(most recent 100 entries),capturedLogLines(at most 25 lines of 512 bytes),docsLink. Unknown id answers 404.GET /api/status: version, edition, last scan,watchedNamespaces(["*"]when every namespace is watched),excludedNamespaces(when configured), per-channel delivery health.Allow: GET.{"error":"unavailable"}. Sources are held in atomic pointers, so handlers never race the pipeline wiring.KWATCH_DASHBOARD_TOKEN(Secretkwatch-dashboard, keytoken), constant-time compare, 401 with no detail. Failed attempts are rate limited per client host (port ignored), 10 per minute, then 429. Enabled with an empty token logs a clear warning at startup and every call answers 401.X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Cache-Control: no-store,Referrer-Policy: no-referrer. No CORS.internal/redactagain before it is written.titleis the headline of the last message kwatch composed for the incident (same wording as the alert), falling back to the root object name before the first message.docsLinkis the configured runbook link (runbooks:config) the message named; omitted when none is configured.capturedLogLinesare the application output and investigation evidence of each decision, deduplicated and cut on a rune boundary.kubernetesEventsanddeliveryAttemptswere removed from the response: kwatch does not keep them per incident, so they would always be empty. They can come back when that data is held.Facts,HeadlineandDocsLinkare additiveomitemptyfields onincident.Record.SchemaVersionstays at 2, so upgrading does not reset the store; older records restore without them (tested).ProviderHealthkeeps bothLastSuccessandLastError(one never clears the other) and a shortLastErrorMessage: the fixed reason plus the error text with URLs removed and credentials redacted, at most 160 characters. A webhook URL or token in a provider error never appears (tested)./healthis unchanged and still carries only the fixed reason.dashboard.enabled/port/existingSecret, a generated token Secret that keeps its token acrosshelm upgrade(lookupof the existing Secret), ClusterIP Service, NetworkPolicy ingress rule. Default render: no dashboard port, Service or Secret.Tests
go test -race).LastErrorMessage.E2E: not extended in this PR. Exercising the dashboard in kind needs the e2e install to enable it, provision the token Secret and port-forward a second port, which is a larger harness change; it is better done with the Phase 2 UI. The unit tests above cover the behaviour (including restore of captured lines across a restart via export/restore).
Out of scope (Phase 2+)
UI, installer menu, Ingress/TLS sharing, sessions/cookies, write endpoints, config editing.
No RBAC changes (read-only, reads existing state). No disk config writes.
Ready for review. Do not merge until CI is green.