ADR-0409: Kollect UI deployment¶
Ship the read-only Kollect console as a separate static SPA container (
kollect-ui) with its own Helm subchart, optional ingress, and post-MVP oauth2-proxy auth offload — decoupled from the operator controller lifecycle.
Theme: 04 · Export & sinks (read side) · Status: Current (accepted 2026-06-05)
Context¶
ADR-0408 locks a read-only SPA that consumes the Read API for inventory rows. Maintainer decision OQ-1 rejects embedding the UI in the operator binary for MVP: UI release cadence, static asset serving, and CSP hardening should not ride on controller upgrades.
Operators still need a predictable install path: enable UI alongside the operator, point the SPA at the Read API, and optionally expose both through ingress with OIDC — without putting inventory HTTP on ingress by default (OQ-10).
Decision¶
1. Container image¶
| Property | Value |
|---|---|
| Image | ghcr.io/konih/kollect-ui (alongside ghcr.io/konih/kollect) |
| Contents | Vite-built static assets from monorepo ui/dist |
| Server | nginx or distroless static file server with security headers |
| Build | task build-ui → Docker multi-stage (Node build + minimal runtime) |
| SBOM | Syft on ui/dist + image (ADR-0705) |
The operator image does not embed or serve the SPA in MVP.
2. Helm subchart charts/kollect-ui/¶
Parent chart charts/kollect optionally depends on the UI subchart (umbrella pattern):
charts/kollect-ui/
Chart.yaml
values.yaml
templates/
deployment.yaml # static SPA server
service.yaml
ingress.yaml # optional — disabled by default
configmap.yaml # runtime config (Read API base URL)
| Resource | Purpose |
|---|---|
| Deployment | Serves static files; liveness/readiness on / or /healthz |
| Service | ClusterIP; port 8080 (conventional) |
| Ingress | Disabled by default; enable when exposing UI outside cluster |
| ConfigMap / env | READ_API_BASE_URL — cluster Service URL of operator Read API |
Feature gate wiring (parent charts/kollect/values.yaml):
ui:
enabled: false # default off — matches conservative feature gates ([ADR-0704](0704-helm-chart-crd-lifecycle.md))
image:
repository: ghcr.io/konih/kollect-ui
tag: "" # defaults to chart appVersion
ingress:
enabled: false
readApi:
# In-cluster default: http://kollect-manager.kollect-system.svc:8081
baseUrl: ""
portalMode: false # v0.3+ — postgres adapter hints for SPA
When ui.enabled: true, parent chart renders the subchart and documents required companion gates:
featureGates.inventoryHttp.enabled: true for Read API access.
3. Relationship to operator Read API¶
flowchart LR
Browser[Browser SPA] --> UI[kollect-ui Service]
UI -->|static assets| Browser
Browser -->|GET inventory JSON| ReadAPI[Operator Read API<br/>/v1alpha1/…]
Browser -.->|conditions only| K8s[Kubernetes API<br/>CRD get/list]
| Concern | Pattern |
|---|---|
| Backend URL | SPA reads READ_API_BASE_URL at runtime (injected ConfigMap/env); build-time default for dev |
| Same-origin vs cross-origin | In-cluster: UI and Read API may be different Services — configure CORS on Read API deny by default; allow UI origin only when cross-origin (ADR-0404) |
| Auth (MVP) | No auth in frontend — assume cluster-network, port-forward, or dev SA token in sessionStorage; Read API SAR enforced when bearer present |
| Auth (post-MVP) | oauth2-proxy at ingress (below) — session cookie; Read API still validates forwarded identity or bearer |
| Inventory HTTP ingress | Off by default (OQ-10) — enable Read API ingress only with explicit values; UI ingress independent |
Dev loop: kubectl port-forward svc/kollect-ui 8080:8080 + port-forward Read API or in-cluster curl.
4. oauth2-proxy — post-MVP ingress overlay (OQ-2)¶
MVP ships without oauth2-proxy templates. Production browser auth is documented as an ingress overlay pattern for v0.3+:
flowchart LR
User[Human browser] -->|HTTPS| Ingress[Ingress]
Ingress --> O2P[oauth2-proxy<br/>OIDC session]
O2P --> UI[kollect-ui]
O2P -->|Bearer or trusted header| ReadAPI[Read API]
SA[Automation SA] -->|direct Bearer| ReadAPI
Pattern (not shipped in MVP chart):
- Deploy oauth2-proxy as Ingress backend or sidecar in front of
kollect-ui+ Read API paths. - Configure OIDC provider; oauth2-proxy sets httpOnly session cookie (
SameSite=Lax). - Service accounts and in-cluster clients bypass oauth2-proxy — connect directly to Read API Service with Kubernetes bearer tokens (ADR-0404).
- Optional thin BFF (Phase 3) if cookie model must hide kube tokens from the browser entirely (ADR-0408).
Helm values stub in parent chart (oauth2Proxy.enabled: false) remains documentation-only until
post-MVP implementation.
5. Security¶
| Control | Requirement |
|---|---|
| No secrets in static bundle | No sink DSNs, secretRef contents, or production tokens in Vite env baked into dist/ |
| CSP | Enforced by static server — default-src 'self'; no unsafe-eval; see ADR-0410 |
| SRI | Subresource Integrity on hashed asset URLs in container image |
| Separate lifecycle | UI Deployment rolls independently of operator; UI outage does not block collection/export |
| frame-ancestors | 'none' — no clickjacking |
| Rate limiting | Read API middleware (operator) — optional Phase 2 |
6. Optional co-location (v0.3+)¶
When Read API splits to kollect-server (ADR-0504),
kollect-ui may share a namespace and ingress host with kollect-server — still separate Deployments
and images.
Consequences¶
Positive¶
- UI and controller release independently; static surface area isolated from reconciler.
- Feature gate defaults keep clusters without UI/HTTP exposure conservative.
- oauth2-proxy documented without blocking MVP UI development.
Negative¶
- Two images and Deployments to version, scan, and document.
- Cross-origin CORS configuration required when UI and Read API Services differ.
- MVP assumes cluster-network access or port-forward — not internet-facing without post-MVP auth.