docs(pilot): dashboard now behind Logto SSO
This commit is contained in:
+128
-107
@@ -4,19 +4,24 @@ A read-only overview + per-repo / per-PR drilldown over the same SQLite
|
|||||||
feedback DB the webhook writes, plus a small form to mutate `.pr-review.json`
|
feedback DB the webhook writes, plus a small form to mutate `.pr-review.json`
|
||||||
on a covered repo via the Gitea contents API. Companion to the
|
on a covered repo via the Gitea contents API. Companion to the
|
||||||
[webhook service](README-webhook.md); reuses the webhook image
|
[webhook service](README-webhook.md); reuses the webhook image
|
||||||
(`pragent-webhook:optin`) — the pilot modules are baked into `/app/pilot/`,
|
(`pragent-webhook:dashboard`) — the pilot modules are baked into `/app/pilot/`,
|
||||||
and the dashboard is just `python3 -m pilot.dashboard`.
|
and the dashboard is just `python3 -m pilot.dashboard`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
Browser (tailnet)
|
Browser
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
NodePort :31540 on 100.74.17.70 (kubernets)
|
Caddy (TLS, wildcard cert via Cloudflare DNS-01)
|
||||||
│
|
│ https://pragent-dashboard.marcospaulo.dev.br → 100.74.17.70:31541
|
||||||
▼
|
▼
|
||||||
Service pragent-dashboard.pragent.svc.cluster.local (NodePort, ns pragent)
|
Service oauth2-proxy-dashboard.pragent.svc.cluster.local (NodePort 31541, ns pragent)
|
||||||
|
│
|
||||||
|
│ oauth2-proxy fronts the dashboard, enforces Logto SSO + email allowlist
|
||||||
|
│ sets X-Forwarded-User / X-Forwarded-Email on accepted requests
|
||||||
|
▼
|
||||||
|
Service pragent-dashboard.pragent.svc.cluster.local (ClusterIP, ns pragent)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
pragent-dashboard pod (uid 10001, /data RO, no subprocess fan-out)
|
pragent-dashboard pod (uid 10001, /data RO, no subprocess fan-out)
|
||||||
@@ -31,6 +36,10 @@ Fail-soft. Nothing is ever written to local disk by the dashboard — the
|
|||||||
SQLite file is read-only and `.pr-review.json` mutations go through Gitea's
|
SQLite file is read-only and `.pr-review.json` mutations go through Gitea's
|
||||||
contents API so the commit history records who changed what.
|
contents API so the commit history records who changed what.
|
||||||
|
|
||||||
|
The dashboard `Service` is **ClusterIP** — only oauth2-proxy can reach it.
|
||||||
|
Public access is gated by Caddy (TLS termination) → oauth2-proxy (Logto SSO
|
||||||
|
+ allowlist) → dashboard.
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
- **Overview** (`GET /`): summary stats across all onboarded repos — total
|
- **Overview** (`GET /`): summary stats across all onboarded repos — total
|
||||||
@@ -50,9 +59,6 @@ contents API so the commit history records who changed what.
|
|||||||
API). This is how repo-local `focus` / `instructions` /
|
API). This is how repo-local `focus` / `instructions` /
|
||||||
`reviewers` / `severity_threshold` get tuned per-repo after seeing
|
`reviewers` / `severity_threshold` get tuned per-repo after seeing
|
||||||
the feedback roll-up.
|
the feedback roll-up.
|
||||||
- **Auth** (`GET /login`, `POST /login`): single-user cookie
|
|
||||||
`pragent_dash=<token>` when `PRAGENT_DASHBOARD_TOKEN` is set in the
|
|
||||||
Secret. Unset = no auth (tailnet-only mode; logged on startup).
|
|
||||||
|
|
||||||
All routes return HTML (or plain text for `/raw`) with the same stylesheet
|
All routes return HTML (or plain text for `/raw`) with the same stylesheet
|
||||||
(`/static/style.css`).
|
(`/static/style.css`).
|
||||||
@@ -62,14 +68,16 @@ All routes return HTML (or plain text for `/raw`) with the same stylesheet
|
|||||||
| method | path | auth | description |
|
| method | path | auth | description |
|
||||||
|--------|-----------------------------------|------|----------------------------------------------|
|
|--------|-----------------------------------|------|----------------------------------------------|
|
||||||
| GET | `/` | yes | Overview |
|
| GET | `/` | yes | Overview |
|
||||||
| GET | `/login` | no | Login form |
|
|
||||||
| POST | `/login` | no | Sets the `pragent_dash` cookie |
|
|
||||||
| GET | `/static/style.css` | no | Stylesheet |
|
| GET | `/static/style.css` | no | Stylesheet |
|
||||||
| GET | `/r/<owner>/<name>` | yes | Repo drilldown |
|
| GET | `/r/<owner>/<name>` | yes | Repo drilldown |
|
||||||
| GET | `/r/<owner>/<name>/<index>` | yes | PR drilldown |
|
| GET | `/r/<owner>/<name>/<index>` | yes | PR drilldown |
|
||||||
| GET | `/r/<owner>/<name>/<index>/raw` | yes | Most recent review body as markdown |
|
| GET | `/r/<owner>/<name>/<index>/raw` | yes | Most recent review body as markdown |
|
||||||
| POST | `/r/<owner>/<name>/edit` | yes | Edit `.pr-review.json` on the default branch |
|
| POST | `/r/<owner>/<name>/edit` | yes | Edit `.pr-review.json` on the default branch |
|
||||||
|
|
||||||
|
Auth is enforced by oauth2-proxy upstream; the dashboard itself only
|
||||||
|
checks the `X-Forwarded-User` header that oauth2-proxy sets after a
|
||||||
|
successful Logto login + email allowlist match.
|
||||||
|
|
||||||
There is no `/health` route — don't add one to the k8s probes without
|
There is no `/health` route — don't add one to the k8s probes without
|
||||||
updating `pilot/dashboard.py` (the handler returns 404 on unknown paths,
|
updating `pilot/dashboard.py` (the handler returns 404 on unknown paths,
|
||||||
so a probe would loop forever).
|
so a probe would loop forever).
|
||||||
@@ -91,51 +99,66 @@ volumeMount in `~/k8s/pragent-dashboard.yaml`): the dashboard never
|
|||||||
writes the SQLite file, only the webhook + the daily cronjob do, and
|
writes the SQLite file, only the webhook + the daily cronjob do, and
|
||||||
keeping it RO means a buggy deploy can't corrupt the harvested feedback.
|
keeping it RO means a buggy deploy can't corrupt the harvested feedback.
|
||||||
|
|
||||||
## Auth (single-user cookie)
|
## Auth (Logto SSO via oauth2-proxy)
|
||||||
|
|
||||||
`PRAGENT_DASHBOARD_TOKEN` in the `pragent-webhook` Secret is the value
|
Authentication is delegated to oauth2-proxy, which fronts the dashboard
|
||||||
the dashboard expects on the `pragent_dash` cookie. The login form
|
in-cluster. The dashboard never sees a cookie or a token — it only
|
||||||
accepts it on POST / login and sets the cookie. Empty / unset env var
|
inspects `X-Forwarded-User` (set by oauth2-proxy after a successful
|
||||||
disables auth (logged on startup: `auth OFF (tailnet-only mode)`).
|
Logto login + email allowlist match). Missing header → 401 with
|
||||||
|
`WWW-Authenticate: Basic realm="pragent-dashboard"`, which lets
|
||||||
|
oauth2-proxy intercept and bounce the browser to Logto.
|
||||||
|
|
||||||
Generate a token once:
|
Email allowlist lives in the ConfigMap `oauth2-proxy-dashboard-emails`
|
||||||
|
in namespace `pragent`:
|
||||||
|
|
||||||
```bash
|
```yaml
|
||||||
openssl rand -hex 16
|
data:
|
||||||
|
authenticated-emails: |
|
||||||
|
marcos.paulodasilva.mp@gmail.com
|
||||||
|
thiago@marcospaulo.dev.br
|
||||||
```
|
```
|
||||||
|
|
||||||
Add to the Secret (the dashboard reads it as `PRAGENT_DASHBOARD_TOKEN`):
|
Edit the ConfigMap to add/remove users; oauth2-proxy hot-reloads the
|
||||||
|
file (it logs `watching ... for updates`), no restart needed. This is
|
||||||
|
the same isolation pattern as the minecraft-sso / code-server
|
||||||
|
allowlists — see `~/.claude/memory/minecraft-sso.md`.
|
||||||
|
|
||||||
```bash
|
The Logto app is `pragent-dashboard` (tenant `default`, type
|
||||||
# base64 the token first
|
`Traditional`), created by direct INSERT into Logto Postgres mirroring
|
||||||
printf '<token>' | base64
|
the proven `minecraft-sso` pattern. Credentials live in
|
||||||
# then JSON-patch the Secret
|
`~/k8s/oauth2-proxy-dashboard-secret.yaml` (mode 600, NOT in git).
|
||||||
microk8s kubectl patch secret pragent-webhook -n pragent --type=json \
|
|
||||||
-p='[{"op":"add","path":"/data/PRAGENT_DASHBOARD_TOKEN","value":"<b64>"}]'
|
|
||||||
```
|
|
||||||
|
|
||||||
The token also lives in `~/.config/pragent/dashboard-token` (mode 600) so
|
Public URL: **https://pragent-dashboard.marcospaulo.dev.br** (Caddy
|
||||||
you can paste it into the login form without re-reading the Secret.
|
TLS termination via wildcard cert → Tailscale → NodePort 31541 →
|
||||||
|
oauth2-proxy → dashboard ClusterIP).
|
||||||
|
|
||||||
|
### Emergency bypass (cookie)
|
||||||
|
|
||||||
|
If Logto goes down and you need to access the dashboard before the
|
||||||
|
oauth2-proxy restart dance (see `~/.claude/memory/logto-fix.md`),
|
||||||
|
`pilot/dashboard.py` can be patched to accept a fallback cookie by
|
||||||
|
re-adding the `PRAGENT_DASHBOARD_TOKEN` env path — the route gate is
|
||||||
|
isolated in `_is_authed` and the logic is straightforward. The current
|
||||||
|
commit intentionally has no bypass because Logto SSO is the single
|
||||||
|
source of truth for "who can touch `.pr-review.json`".
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
The dashboard shares the webhook image, so there's nothing to rebuild.
|
The dashboard shares the webhook image, so there's nothing to rebuild
|
||||||
After editing `pilot/dashboard.py` or `pilot/dashboard_data.py`, redo the
|
beyond what the webhook already does. After editing `pilot/dashboard.py`
|
||||||
webhook image rebuild + containerd import (see `README-webhook.md` § "K8s
|
or `pilot/dashboard_data.py`, redo the webhook image rebuild + containerd
|
||||||
deployment") and roll both deployments.
|
import (see `README-webhook.md` § "K8s deployment") and roll both
|
||||||
|
deployments.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
K="microk8s kubectl"
|
K="microk8s kubectl"
|
||||||
|
|
||||||
# 1. (one-time) generate + persist the auth token
|
# 1. (one-time) create the Logto app + cookie secret + oauth2-proxy
|
||||||
TOKEN=$(openssl rand -hex 16)
|
# See ~/.claude/memory/minecraft-sso.md for the SQL INSERT recipe
|
||||||
printf "%s" "$TOKEN" > ~/.config/pragent/dashboard-token
|
# and ~/k8s/oauth2-proxy-dashboard*.yaml for the manifests.
|
||||||
chmod 600 ~/.config/pragent/dashboard-token
|
|
||||||
printf "%s" "$TOKEN" | base64 \
|
|
||||||
| xargs -I{} $K -n pragent patch secret pragent-webhook --type=json \
|
|
||||||
-p='[{"op":"add","path":"/data/PRAGENT_DASHBOARD_TOKEN","value":"{}"}]'
|
|
||||||
|
|
||||||
# 2. apply the manifest
|
# 2. apply all pragent-dashboard manifests (dashboard + oauth2-proxy)
|
||||||
|
$K apply -f ~/k8s/oauth2-proxy-dashboard.yaml
|
||||||
$K apply -f ~/k8s/pragent-dashboard.yaml
|
$K apply -f ~/k8s/pragent-dashboard.yaml
|
||||||
|
|
||||||
# 3. roll on image / code changes
|
# 3. roll on image / code changes
|
||||||
@@ -144,86 +167,86 @@ $K -n pragent rollout status deploy/pragent-dashboard --timeout=120s
|
|||||||
$K -n pragent logs -f deploy/pragent-dashboard
|
$K -n pragent logs -f deploy/pragent-dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
K8s manifest at `~/k8s/pragent-dashboard.yaml`. Key fields:
|
K8s manifests:
|
||||||
|
|
||||||
- `image: pragent-webhook:optin` + `imagePullPolicy: Never` — local
|
- `~/k8s/pragent-dashboard.yaml` — Deployment + ClusterIP Service.
|
||||||
containerd only, same image as the webhook.
|
- `image: pragent-webhook:dashboard` + `imagePullPolicy: Never` —
|
||||||
- `nodeSelector: kubernetes.io/hostname: kubernets` — pinned to the node
|
local containerd only, same image as the webhook.
|
||||||
holding the `/data` PVC.
|
- `nodeSelector: kubernetes.io/hostname: kubernets` — pinned to the
|
||||||
|
node holding the `/data` PVC.
|
||||||
- `securityContext: runAsNonRoot: true, runAsUser: 10001, runAsGroup:
|
- `securityContext: runAsNonRoot: true, runAsUser: 10001, runAsGroup:
|
||||||
10001, fsGroup: 10001` — matches the image's USER directive; fsGroup
|
10001, fsGroup: 10001` — matches the image's USER directive;
|
||||||
makes the RO hostpath volume readable.
|
fsGroup makes the RO hostpath volume readable.
|
||||||
- `volumeMounts.feedback-data.readOnly: true` — dashboard is read-only
|
- `volumeMounts.feedback-data.readOnly: true` — dashboard is
|
||||||
over `/data`; mutations go through Gitea, not local fs.
|
read-only over `/data`; mutations go through Gitea, not local fs.
|
||||||
- No `readinessProbe` / `livenessProbe` — the dashboard has no `/health`
|
- No `readinessProbe` / `livenessProbe` — the dashboard has no
|
||||||
route. If you add one to `pilot/dashboard.py`, add a probe here too.
|
`/health` route. If you add one to `pilot/dashboard.py`, add a
|
||||||
|
probe here too.
|
||||||
- `resources.requests: {cpu: 100m, memory: 256Mi}` /
|
- `resources.requests: {cpu: 100m, memory: 256Mi}` /
|
||||||
`limits: {cpu: 500m, memory: 512Mi}` — read-heavy + tiny writes, no
|
`limits: {cpu: 500m, memory: 512Mi}` — read-heavy + tiny writes,
|
||||||
opencode subprocess fan-out, much smaller than the webhook.
|
no opencode subprocess fan-out, much smaller than the webhook.
|
||||||
- `Service.type: NodePort`, `nodePort: 31540` — between pages-proxy
|
- `Service.type: ClusterIP` — only oauth2-proxy can reach it.
|
||||||
(30081) and browserless (30100), outside the 30096..30969 media range.
|
|
||||||
|
- `~/k8s/oauth2-proxy-dashboard.yaml` — Deployment + ConfigMap +
|
||||||
|
NodePort Service (`oauth2-proxy-dashboard`, NodePort 31541,
|
||||||
|
namespace `pragent`). Same shape as the code-server /
|
||||||
|
minecraft-sso oauth2-proxy. NodePort 31541 was chosen because
|
||||||
|
31540 was the old dashboard NodePort and the 30096..30969 media
|
||||||
|
range + 30350-30351 (other oauth2-proxy NodePorts) were taken.
|
||||||
|
|
||||||
|
- `~/k8s/oauth2-proxy-dashboard-secret.yaml` — client-id /
|
||||||
|
client-secret / cookie-secret (mode 600, NOT in git).
|
||||||
|
|
||||||
## Smoke test
|
## Smoke test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. port-forward (in-cluster)
|
# 1. anonymous request → 302 redirect to Logto
|
||||||
|
curl -I https://pragent-dashboard.marcospaulo.dev.br/
|
||||||
|
|
||||||
|
# 2. pod logs
|
||||||
|
microk8s kubectl logs -n pragent -l app=oauth2-proxy-dashboard --tail=50
|
||||||
|
microk8s kubectl logs -n pragent -l app=pragent-dashboard --tail=50
|
||||||
|
|
||||||
|
# 3. in-cluster direct probe (should 401 without X-Forwarded-User)
|
||||||
microk8s kubectl port-forward -n pragent svc/pragent-dashboard 8181:80 &
|
microk8s kubectl port-forward -n pragent svc/pragent-dashboard 8181:80 &
|
||||||
sleep 2
|
sleep 2
|
||||||
curl -s -o /tmp/dash-overview.html -w "HTTP %{http_code}\n" \
|
curl -I http://localhost:8181/ # expect 401 + WWW-Authenticate: Basic
|
||||||
http://localhost:8181/
|
curl -I -H "X-Forwarded-User: marcos@example.com" http://localhost:8181/ # expect 200
|
||||||
grep -o "<title>[^<]*</title>" /tmp/dash-overview.html
|
|
||||||
head -50 /tmp/dash-overview.html
|
|
||||||
kill %1
|
kill %1
|
||||||
|
|
||||||
# 2. NodePort (host-side, Tailscale IP — only reachable on 100.74.17.70
|
|
||||||
# or 192.168.1.80; no public DNS yet)
|
|
||||||
curl -s -o /tmp/dash-nodeport.html -w "HTTP %{http_code}\n" \
|
|
||||||
http://100.74.17.70:31540/
|
|
||||||
grep -o "<title>[^<]*</title>" /tmp/dash-nodeport.html
|
|
||||||
|
|
||||||
# 3. auth: POST the login cookie value, capture into a jar
|
|
||||||
curl -sS -c /tmp/dash.jar -X POST \
|
|
||||||
-d "token=$(cat ~/.config/pragent/dashboard-token)" \
|
|
||||||
http://100.74.17.70:31540/login
|
|
||||||
|
|
||||||
# 4. authenticated overview
|
|
||||||
curl -sS -b /tmp/dash.jar -o /tmp/dash-auth.html -w "HTTP %{http_code}\n" \
|
|
||||||
http://100.74.17.70:31540/
|
|
||||||
grep -o "<title>[^<]*</title>" /tmp/dash-auth.html
|
|
||||||
|
|
||||||
# 5. pod logs
|
|
||||||
microk8s kubectl logs -n pragent -l app=pragent-dashboard --tail=50
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The HTML should contain a `<title>` (whatever the dashboard renders) and
|
The HTML returned with a valid `X-Forwarded-User` should contain a
|
||||||
**never** `Traceback` or any Python exception output. A 401 / redirect
|
`<title>` (whatever the dashboard renders) and **never** `Traceback` or
|
||||||
to `/login` on the unauthenticated GETs is expected when
|
any Python exception output. A 401 on the unauthenticated GET is the
|
||||||
`PRAGENT_DASHBOARD_TOKEN` is set.
|
expected behaviour — oauth2-proxy catches it and redirects to Logto.
|
||||||
|
|
||||||
## Threat model / security notes
|
## Threat model / security notes
|
||||||
|
|
||||||
- **Tailnet-only by default.** NodePort 31540 is exposed on the
|
- **Behind Logto SSO.** Anonymous traffic gets 302 → Logto. Allowed
|
||||||
Tailscale / LAN interfaces of kubernets (100.74.17.70, 192.168.1.80).
|
emails (marcos, thiago) reach the dashboard after Logto login; all
|
||||||
No public DNS, no Caddy route yet — keep it that way until the auth
|
others see oauth2-proxy's "not authorized" page. Adding a user is a
|
||||||
story is solid. When you do add a public route, terminate TLS at
|
one-line ConfigMap edit; oauth2-proxy hot-reloads the allowlist.
|
||||||
Caddy (wildcard cert via Cloudflare DNS-01) and rely on the cookie
|
- **`PRAGENT_BOT_TOKEN` is Gitea Write scoped** to onboarded repos, so
|
||||||
auth + Logto SSO gating pattern from the
|
a successful auth bypass on the dashboard is Gitea repo write access,
|
||||||
[code-server / minecraft-sso / livecodes setups](../).
|
not just read. oauth2-proxy's email allowlist is the only
|
||||||
- **`PRAGENT_BOT_TOKEN` is Gitea Write scoped** to onboarded repos, so a
|
authentication factor — there is no second factor. If this becomes a
|
||||||
successful auth bypass on the dashboard is Gitea repo write access,
|
concern, swap oauth2-proxy for an IdP that supports TOTP/WebAuthn
|
||||||
not just read. The cookie is the only auth factor — there is no CSRF
|
and the dashboard needs no further changes (it just reads the
|
||||||
token in the current implementation (single-operator trust model). If
|
forwarded headers).
|
||||||
the dashboard goes public, add a CSRF token to the edit form
|
- **CSRF on the edit form.** Per-process random secret embedded as a
|
||||||
(hidden input + double-submit cookie) before opening it up.
|
hidden input + double-submit via the `X-Forwarded-User` context. An
|
||||||
|
attacker would need to (a) steal the user's Logto session cookie
|
||||||
|
from oauth2-proxy and (b) read the rendered HTML to harvest the
|
||||||
|
CSRF token. Both have to happen in the same browser.
|
||||||
- **Read-only `/data` mount.** The dashboard can't corrupt the
|
- **Read-only `/data` mount.** The dashboard can't corrupt the
|
||||||
harvested SQLite file even if it's compromised. The webhook and the
|
harvested SQLite file even if it's compromised. The webhook and the
|
||||||
daily cronjob are the only writers.
|
daily cronjob are the only writers.
|
||||||
|
- **ClusterIP dashboard Service.** Even if a malicious actor discovered
|
||||||
|
the dashboard's container port, they cannot reach it from outside the
|
||||||
|
cluster — only oauth2-proxy can. NetworkPolicy is the cluster
|
||||||
|
default deny.
|
||||||
- **`uid 10001` + `runAsNonRoot: true`.** No host-level escalation if
|
- **`uid 10001` + `runAsNonRoot: true`.** No host-level escalation if
|
||||||
the dashboard is popped — it has no caps, no `/proc` mounts, no
|
the dashboard is popped — it has no caps, no `/proc` mounts.
|
||||||
NetworkPolicy exemption (cluster default deny applies until you
|
|
||||||
grant egress to `gitea-http.gitea.svc.cluster.local:3000` and
|
|
||||||
`100.74.17.70:8787` if you ever need the headroom proxy — currently
|
|
||||||
the dashboard doesn't).
|
|
||||||
- **No author-controlled input is `eval`-ed.** The edit form parses the
|
- **No author-controlled input is `eval`-ed.** The edit form parses the
|
||||||
JSON, validates types / lengths, and re-serialises before the Gitea
|
JSON, validates types / lengths, and re-serialises before the Gitea
|
||||||
PUT. The review-side hostile-input concerns from `README-webhook.md`
|
PUT. The review-side hostile-input concerns from `README-webhook.md`
|
||||||
@@ -232,12 +255,10 @@ to `/login` on the unauthenticated GETs is expected when
|
|||||||
|
|
||||||
## Known limitations (pilot)
|
## Known limitations (pilot)
|
||||||
|
|
||||||
- Single-user cookie auth (no per-user sessions, no Logto SSO yet).
|
- Logto SSO is the only auth factor — no per-user sessions, no CSRF
|
||||||
- No CSRF protection on the edit form (intentional — single-operator
|
token tied to a per-user identity (the per-process CSRF secret is
|
||||||
trust model; add before going public).
|
global). Adequate for a single-operator dashboard; not adequate for
|
||||||
- Read-only `/data` means the dashboard can't backfill the DB if the
|
multi-tenant.
|
||||||
cronjob is paused; if you turn off feedback harvesting
|
|
||||||
(`PRAGENT_FEEDBACK_DB=` empty in the webhook), the overview is empty.
|
|
||||||
- No `/health` route — if the dashboard process wedges on a Gitea hang,
|
- No `/health` route — if the dashboard process wedges on a Gitea hang,
|
||||||
k8s won't restart it. Add a `/health` route to `pilot/dashboard.py`
|
k8s won't restart it. Add a `/health` route to `pilot/dashboard.py`
|
||||||
+ a probe here before relying on this in production.
|
+ a probe here before relying on this in production.
|
||||||
|
|||||||
Reference in New Issue
Block a user