# Extractor admin UI

Read-only pages over the [shot bridge](<https://homelab-apps.wlkr.ch/shot-bridge/index.md>)'s diagnostics: the shots recorded with the app's diagnostics switch on, their curves, merged timelines and stop legs, the uploaded logs, and the calibration runs. It has no login of its own and reads the bridge server side with the bridge's read-only token, which never reaches a browser. The source, the image and the chart live in [somuch-cloud/extractor.cafe](<https://github.com/somuch-cloud/extractor.cafe/tree/main/admin>); the chart is published to `ghcr.io/somuch-cloud/charts`, per [Conventions → Official upstream sources only](<https://homelab-apps.wlkr.ch/conventions/#2-official-upstream-sources-only>).

## At a glance

|  |  |
| --- | --- |
| URL | [extractor-admin.k8s.wlkr.ch](<https://extractor-admin.k8s.wlkr.ch>) |
| Authentication | The Authentik outpost in front, per [Conventions → Authentication is Authentik's](<https://homelab-apps.wlkr.ch/conventions/#6-authentication-is-authentiks-not-the-applications>) |
| Storage | None; everything is read from the bridge on each request |
| Dials | The bridge's pods in `shot-bridge`, nothing outside the cluster |
| Files | [`extractor-admin/`](<https://github.com/JanWelker/homelab-apps/tree/main/extractor-admin>) |

## Configuration

| Setting | Why |
| --- | --- |
| Own chart, values in `application.yaml` | The chart ships the Deployment, Service, `HTTPRoute` and `CiliumNetworkPolicy`; this directory adds the namespace and the secret |
| `HTTPRoute` targets `authentik-server` | The admin UI has no login; the chart refuses a route to its own Service. The platform repository's provider forwards to `extractor-admin.extractor-admin.svc` after the login |
| Policy admits the Authentik server pods only | No request can skip the outpost. Egress is the bridge's pods on `bridge.port`, plus a DNS rule the chart renders itself, which repeats the platform's cluster-wide one ([Conventions → rule 5](<https://homelab-apps.wlkr.ch/conventions/#5-ship-a-ciliumnetworkpolicy>)) and cannot be switched off |
| The bridge's `admin.enabled: true` | Passes `BRIDGE_ADMIN_READ_KEY` to the bridge and lets this namespace's pods in on 8080 |

### Secrets

| Path | Keys | Written by |
| --- | --- | --- |
| `kv/extractor-admin/config` | `admin-read-key` | By hand, as a copy of the bridge's property in `kv/shot-bridge/config`, which the `PushSecret` in [`shot-bridge/secrets.yaml`](<https://github.com/JanWelker/homelab-apps/blob/main/shot-bridge/secrets.yaml>) generates — see [Copying the read key](<#copying-the-read-key>) |

A copy, because the `openbao-apps` store confines each namespace to its own `kv/<namespace>/` ([Conventions → Real secrets come from OpenBao](<https://homelab-apps.wlkr.ch/conventions/#7-real-secrets-come-from-openbao>)). The token may only read the bridge's debug routes and the logs; every other bridge route answers 403.

## Usage

### Copying the read key

Once before the first sync, and again whenever `kv/shot-bridge/config` is rotated. Run from the platform repository's checkout.

1. Log in as the `operator` user, whose policy covers `kv/`; the root token no longer exists after `make bao-init`. The port-forward and the login are [Authenticating locally](<https://homelab.wlkr.ch/platform/openbao/#authenticating-locally>).
2. Copy the property; it is alphanumeric, so it needs no JSON escaping:

   ```bash
   key="$(bao kv get -mount=kv -field=admin-read-key shot-bridge/config)"
   printf '{"admin-read-key":"%s"}' "$key" | bao kv put -mount=kv extractor-admin/config -
   unset key
   ```
3. Drop the login's token, which the short TTL would otherwise leave in `~/.bao-token` until it expires:

   ```bash
   bao token revoke -self && rm -f ~/.bao-token
   ```
4. After a rotation, push the value through and restart the UI, which reads it into an environment variable:

   ```bash
   kubectl -n extractor-admin annotate externalsecret extractor-admin force-sync="$(date +%s)" --overwrite
   kubectl -n extractor-admin rollout restart deploy/extractor-admin
   ```

## Health check

```bash
kubectl -n extractor-admin get pods,externalsecret,httproute
kubectl -n extractor-admin logs deploy/extractor-admin    # view GET /shots user=<you> status=200
```

The hostname answers with Authentik's login first, then the shot list.

## Pitfalls

- **The image and chart have to be public on GHCR**, as for the bridge: the cluster has no pull secret for `ghcr.io/somuch-cloud`.
- **A rotated bridge key is not copied for you.** `admin-read-key` in `kv/extractor-admin/config` is a hand copy of the bridge's, and nothing writes it: the `openbao-apps` store confines a `PushSecret` to its own `kv/<namespace>/`, so the bridge's namespace cannot push it here. After a [rotation](<https://homelab.wlkr.ch/platform/openbao/#rotating-a-credential>) of `kv/shot-bridge/config` the UI answers 401 or 403 until the key is [copied again](<#copying-the-read-key>); the same two codes with no rotation mean the bridge's `admin.enabled` is off.
- **Only shots recorded with diagnostics on appear.** The token cannot list the other shots, by design.
