|
| 1 | +--- |
| 2 | +name: deploy |
| 3 | +description: > |
| 4 | + Deploy, update manifests, and troubleshoot the ambient-ui component. |
| 5 | + Use when creating or modifying deployment manifests, adding ambient-ui |
| 6 | + to a new overlay, building images, or debugging cluster issues. |
| 7 | +--- |
| 8 | + |
| 9 | +# Ambient UI Deployment |
| 10 | + |
| 11 | +Instructions for deploying the ambient-ui component across environments. |
| 12 | + |
| 13 | +## User Input |
| 14 | + |
| 15 | +```text |
| 16 | +$ARGUMENTS |
| 17 | +``` |
| 18 | + |
| 19 | +## Architecture |
| 20 | + |
| 21 | +The ambient-ui is a Next.js BFF (Backend-for-Frontend) that handles OIDC |
| 22 | +authentication as a confidential client, manages server-side sessions, |
| 23 | +and proxies API requests to the ambient-api-server with JWT relay. |
| 24 | + |
| 25 | +``` |
| 26 | +Route (port 443, TLS edge termination) |
| 27 | + → Service (port 3000) |
| 28 | + → Next.js BFF (port 3000) |
| 29 | + ├── OIDC auth (Authorization Code Flow + PKCE against Red Hat SSO) |
| 30 | + ├── Server-side session (iron-session, httpOnly cookie) |
| 31 | + ├── JWT relay to ambient-api-server (Authorization: Bearer <jwt>) |
| 32 | + └── ambient-api-server (port 8000, HTTPS, validates JWT against same issuer) |
| 33 | +``` |
| 34 | + |
| 35 | +No sidecar proxy. The BFF IS the confidential OIDC client. The browser |
| 36 | +never receives a raw JWT — only an httpOnly session cookie. |
| 37 | + |
| 38 | +In local/kind environments, `AUTH_MODE=none` disables auth entirely. |
| 39 | + |
| 40 | +## Manifest File Map |
| 41 | + |
| 42 | +| File | Purpose | |
| 43 | +|------|---------| |
| 44 | +| `components/manifests/base/core/ambient-ui-deployment.yaml` | Base Deployment, ServiceAccount, Service | |
| 45 | +| `components/manifests/overlays/kind/kustomization.yaml` | Kind overlay (Quay images, no auth) | |
| 46 | +| `components/manifests/overlays/kind-local/kustomization.yaml` | Kind-local overlay (localhost images) | |
| 47 | +| `components/ambient-ui/Dockerfile` | Multi-stage Docker build | |
| 48 | +| `.github/workflows/components-build-deploy.yml` | CI build matrix entry | |
| 49 | + |
| 50 | +Production overlay files exist (`ambient-ui-oauth-patch.yaml`, etc.) but are |
| 51 | +disabled — they used origin-oauth-proxy which can't produce JWTs. See Auth section. |
| 52 | + |
| 53 | +## Docker Build |
| 54 | + |
| 55 | +**Build context** is `./components` (not `./components/ambient-ui`), because |
| 56 | +the Dockerfile references `ambient-sdk/ts-sdk` as a sibling. |
| 57 | + |
| 58 | +### Critical details |
| 59 | + |
| 60 | +1. **SDK dist/ is gitignored.** The Dockerfile must build it: |
| 61 | + ```dockerfile |
| 62 | + COPY ambient-sdk/ts-sdk /ambient-sdk/ts-sdk |
| 63 | + RUN cd /ambient-sdk/ts-sdk && npm install --ignore-scripts && npm run build |
| 64 | + ``` |
| 65 | + |
| 66 | +2. **Standalone output path.** `outputFileTracingRoot: ../..` in next.config.js |
| 67 | + resolves to `/` in Docker (WORKDIR `/app`), so standalone nests under `app/`: |
| 68 | + ```dockerfile |
| 69 | + cp -r .next/standalone/app/. /app-output/ |
| 70 | + ``` |
| 71 | + Locally it nests under `components/ambient-ui/`. Always verify with: |
| 72 | + ```bash |
| 73 | + find .next/standalone -name server.js -not -path '*/node_modules/*' |
| 74 | + ``` |
| 75 | + |
| 76 | +3. **Webpack, not Turbopack.** The build script is `next build --webpack`. |
| 77 | + Turbopack cannot resolve `file:` linked dependencies. |
| 78 | + |
| 79 | +4. **OpenShift permissions.** The builder stage must run: |
| 80 | + ```dockerfile |
| 81 | + chmod -R g=u /app-output && chgrp -R 0 /app-output |
| 82 | + ``` |
| 83 | + The runner image (Red Hat Hardened Image) is distroless — no shell at runtime. |
| 84 | + |
| 85 | +### Local build & test |
| 86 | + |
| 87 | +```bash |
| 88 | +podman build -t ambient-ui-test -f components/ambient-ui/Dockerfile components/ |
| 89 | +podman run --rm ambient-ui-test node -e 'require("fs").statSync("/app/server.js"); console.log("server.js found")' |
| 90 | +podman run -d --name ambient-ui-test -e HOSTNAME=0.0.0.0 ambient-ui-test |
| 91 | +podman exec ambient-ui-test node -e "require('http').get('http://localhost:3000/', r => { console.log(r.statusCode); r.on('data',()=>{}); r.on('end',()=>process.exit(0)); })" |
| 92 | +podman stop ambient-ui-test && podman rm ambient-ui-test |
| 93 | +``` |
| 94 | + |
| 95 | +## Environment Configuration |
| 96 | + |
| 97 | +| Env Var | kind / local | Production | |
| 98 | +|---------|-------------|------------| |
| 99 | +| `AUTH_MODE` | `none` | `native-sso` | |
| 100 | +| `API_SERVER_URL` | `http://ambient-api-server:8000` | `https://ambient-api-server:8000` | |
| 101 | +| `NODE_EXTRA_CA_CERTS` | (unset) | `/etc/ssl/service-ca/service-ca.crt` (from service-ca ConfigMap) | |
| 102 | +| `SSO_ISSUER_URL` | (unset) | `https://sso.redhat.com/auth/realms/redhat-external` | |
| 103 | +| `SSO_CLIENT_ID` | (unset) | OIDC client ID | |
| 104 | +| `SSO_CLIENT_SECRET` | (unset) | OIDC client secret (from Secret) | |
| 105 | +| `SESSION_SECRET` | (unset) | Random 32+ byte string (from Secret) | |
| 106 | +| `NEXT_PUBLIC_PREVIEW_ALLOWED_HOSTS` | (unset, defaults `localhost:*`) | target domains | |
| 107 | + |
| 108 | +## Authentication |
| 109 | + |
| 110 | +### Native SSO (target architecture) |
| 111 | + |
| 112 | +The BFF handles OIDC directly using Authorization Code Flow with PKCE: |
| 113 | + |
| 114 | +1. User visits ambient-ui → BFF redirects to SSO issuer authorize endpoint |
| 115 | +2. User authenticates at Red Hat SSO → redirected back with auth code |
| 116 | +3. BFF exchanges code for tokens (access token = JWT, refresh token, ID token) |
| 117 | +4. BFF stores tokens in an iron-session encrypted httpOnly cookie |
| 118 | +5. On API requests, BFF extracts JWT from session and forwards as `Authorization: Bearer` |
| 119 | +6. ambient-api-server validates JWT against `sso.redhat.com` JWKS endpoint |
| 120 | + |
| 121 | +This is already implemented in: |
| 122 | +- `src/lib/oidc.ts` — OIDC discovery, auth URL, code exchange, token refresh |
| 123 | +- `src/lib/session.ts` — iron-session storage, token expiry, auto-refresh |
| 124 | +- `src/lib/auth.ts` — `resolveAccessToken()` extracts JWT from session |
| 125 | +- `src/app/api/auth/sso/login/route.ts` — initiates OIDC flow |
| 126 | +- `src/app/api/auth/sso/callback/route.ts` — handles callback, stores tokens |
| 127 | +- `src/app/api/auth/sso/logout/route.ts` — destroys session |
| 128 | + |
| 129 | +### What's needed to enable native SSO |
| 130 | + |
| 131 | +An OIDC confidential client on `sso.redhat.com/auth/realms/redhat-external`: |
| 132 | +- Client protocol: `openid-connect`, access type: `confidential` |
| 133 | +- Valid redirect URI: `https://<ambient-ui-route>/api/auth/sso/callback` |
| 134 | +- Scopes: `openid`, `email`, `profile` |
| 135 | + |
| 136 | +Then set the env vars: `SSO_ISSUER_URL`, `SSO_CLIENT_ID`, `SSO_CLIENT_SECRET`, |
| 137 | +`SESSION_SECRET`, and `AUTH_MODE=native-sso`. |
| 138 | + |
| 139 | +### Why origin-oauth-proxy doesn't work |
| 140 | + |
| 141 | +The OpenShift `origin-oauth-proxy` issues opaque `sha256~` tokens, not JWTs. |
| 142 | +The ambient-api-server requires Red Hat SSO JWTs (`--enable-jwt=true`). |
| 143 | +Confirmed via source code audit: origin-oauth-proxy has zero JWT support. |
| 144 | +The upstream OIDC id_token is decoded for claims and permanently discarded |
| 145 | +by the OpenShift OAuth server. No configuration can change this. |
| 146 | + |
| 147 | +Production overlay files for oauth-proxy exist as reference but are disabled. |
| 148 | + |
| 149 | +## Prerequisites (Manual Steps) |
| 150 | + |
| 151 | +### 1. Quay.io repository |
| 152 | + |
| 153 | +`quay.io/ambient_code/vteam_ambient_ui` with push access for `vteamrobbit`. |
| 154 | + |
| 155 | +### 2. OIDC client (REQUIRED for production) |
| 156 | + |
| 157 | +Create on `sso.redhat.com/auth/realms/redhat-external`: |
| 158 | +- Client protocol: `openid-connect`, access type: `confidential` |
| 159 | +- Redirect URI: `https://<route>/api/auth/sso/callback` |
| 160 | +- Scopes: `openid`, `email`, `profile` |
| 161 | + |
| 162 | +### 3. Secrets |
| 163 | + |
| 164 | +```bash |
| 165 | +oc create secret generic ambient-ui-config \ |
| 166 | + --from-literal=sso-client-id=<client-id> \ |
| 167 | + --from-literal=sso-client-secret=<client-secret> \ |
| 168 | + --from-literal=session-secret="$(openssl rand -base64 32)" |
| 169 | +``` |
| 170 | + |
| 171 | +### 4. Route + TLS |
| 172 | + |
| 173 | +OpenShift Route with TLS edge termination, targeting the ambient-ui Service on port 3000. |
| 174 | + |
| 175 | +## Adding ambient-ui to a New Overlay |
| 176 | + |
| 177 | +1. Include base: `resources: [../../base]` (ambient-ui-deployment.yaml is in base/core) |
| 178 | +2. Add image entries for `quay.io/ambient_code/vteam_ambient_ui` |
| 179 | +3. Apply environment patches (auth mode, API URL, SSO config) |
| 180 | +4. If auth: create OIDC client and secrets per Prerequisites |
| 181 | +5. If OpenShift with HTTPS to ambient-api-server: trust the service-ca bundle. |
| 182 | + The API server TLS cert is signed by OpenShift service-ca, not the default |
| 183 | + SA CA. Create a ConfigMap with inject-cabundle annotation, mount it into the |
| 184 | + ambient-ui container, and set NODE_EXTRA_CA_CERTS=/etc/ssl/service-ca/service-ca.crt |
| 185 | +6. Add a Route if external access is needed |
| 186 | +7. Verify: `kustomize build overlays/<target> | grep -A5 "name: ambient-ui"` |
| 187 | + |
| 188 | +## Troubleshooting |
| 189 | + |
| 190 | +| Symptom | Fix | |
| 191 | +|---------|-----| |
| 192 | +| `Cannot find module '/app/server.js'` | Copy from `.next/standalone/app/` | |
| 193 | +| `Can't resolve 'ambient-sdk'` | Build SDK dist in Dockerfile deps stage | |
| 194 | +| Liveness probe 401 | Use `/api/healthz`, not `/` | |
| 195 | +| `Client sent HTTP request to HTTPS server` | Use `https://` in API_SERVER_URL | |
| 196 | +| 401 `text/plain` from API server | Token format mismatch — ensure JWT, not opaque token | |
| 197 | +| Turbopack build fails | Use `next build --webpack` | |
| 198 | + |
| 199 | +## Verification |
| 200 | + |
| 201 | +- [ ] `kustomize build overlays/<target>` valid YAML |
| 202 | +- [ ] Image references consistent across kustomization, CI, Makefile |
| 203 | +- [ ] Pod starts with 0 restarts |
| 204 | +- [ ] `/api/healthz` returns 200 |
| 205 | +- [ ] API proxy returns data |
0 commit comments