Skip to content

Commit 9c0127f

Browse files
committed
Gateway API Compatibility: A Guide to Container Image Migration
Signed-off-by: Harshad Reddy Nalla <hnalla@redhat.com>
1 parent ced6344 commit 9c0127f

File tree

1 file changed

+389
-0
lines changed

1 file changed

+389
-0
lines changed
Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
# Migrating Workbench Images to Kubernetes Gateway API
2+
3+
## Overview
4+
5+
When migrating from OpenShift Route + oauth-proxy to Kubernetes Gateway API + kube-rbac-proxy, workbench images require nginx configuration updates to properly handle path-based routing.
6+
7+
## The Core Requirement
8+
9+
**Your workbench image must serve all content from the base path `${NB_PREFIX}`.**
10+
11+
Any call from a browser to a different path, for example `/index.html`, `/api/my-endpoint`, or simply `/`, won't be routed to the workbench container. This is because the routing, handled by the Gateway API, is path-based, using the same value as the environment variable `NB_PREFIX` that is injected into the workbench at runtime.
12+
13+
Example `NB_PREFIX`: `/notebook/<namespace>/<workbench-name>`
14+
15+
## Key Architectural Difference
16+
17+
### OpenShift Route (Old)
18+
```
19+
External: /notebook/user/workbench/app/
20+
21+
Route strips prefix
22+
23+
Container receives: /app/
24+
```
25+
26+
**Important**: The prefix stripping isn't automatic - it requires implementation:
27+
- **nginx** strips the prefix via rewrite rules
28+
- **Catch-all redirects** like `location / { return 302 /app; }`
29+
30+
Both approaches work because the Route **forwards all traffic** to the pod regardless of path.
31+
32+
### Gateway API (New)
33+
```
34+
External: /notebook/user/workbench/app/
35+
36+
Gateway preserves full path (path-based routing)
37+
38+
Container receives: /notebook/user/workbench/app/
39+
```
40+
41+
**Critical Difference**: Gateway API uses **path-based routing**. Only requests matching the configured path prefix are forwarded to the pod.
42+
43+
### Why Old Approaches Fail with Gateway API
44+
45+
```
46+
App redirects: /notebook/user/workbench/app → /app
47+
48+
Browser follows redirect to: /app
49+
50+
Gateway routing rule: /notebook/user/workbench/** (doesn't match /app!)
51+
52+
Pod receives NO traffic → 404 or routing failure
53+
```
54+
55+
**The Problem**: If your application redirects to paths outside `${NB_PREFIX}`, the Gateway cannot route those requests back to your pod. The path-based matching at the Gateway level requires all traffic to stay within the configured prefix.
56+
57+
**Critical Change**: Your application (or reverse proxy) must handle the **full path** including the prefix and never redirect outside of it.
58+
59+
---
60+
61+
## Part 1: For All Workbenches - General Requirements
62+
63+
These requirements apply **regardless of whether you use nginx or application-level path handling**.
64+
65+
### 1. Health Check Endpoints
66+
67+
Your workbench **must** respond to health checks at:
68+
69+
```
70+
GET /{NB_PREFIX}/api
71+
```
72+
73+
This endpoint must return an HTTP 200 status for probes to succeed.
74+
75+
**Example for Python Flask**:
76+
```python
77+
from flask import Flask
78+
import os
79+
80+
app = Flask(__name__)
81+
nb_prefix = os.getenv('NB_PREFIX', '')
82+
83+
@app.route(f'{nb_prefix}/api')
84+
def health_check():
85+
return {'status': 'healthy'}, 200
86+
87+
@app.route(f'{nb_prefix}/api/kernels')
88+
def kernels():
89+
# Handle culler endpoint
90+
return {'kernels': []}, 200
91+
92+
@app.route(f'{nb_prefix}/api/terminals')
93+
def terminals():
94+
# Handle culler endpoint
95+
return {'terminals': []}, 200
96+
```
97+
98+
**Example for Node.js Express**:
99+
```javascript
100+
const express = require('express');
101+
const app = express();
102+
const nbPrefix = process.env.NB_PREFIX || '';
103+
104+
app.get(`${nbPrefix}/api`, (req, res) => {
105+
res.json({ status: 'healthy' });
106+
});
107+
108+
app.get(`${nbPrefix}/api/kernels`, (req, res) => {
109+
res.json({ kernels: [] });
110+
});
111+
112+
app.get(`${nbPrefix}/api/terminals`, (req, res) => {
113+
res.json({ terminals: [] });
114+
});
115+
```
116+
117+
### 2. Culler Endpoints
118+
119+
If your workbench supports culling idle workbenches, you must handle:
120+
121+
```
122+
GET /{NB_PREFIX}/api/kernels
123+
GET /{NB_PREFIX}/api/terminals
124+
```
125+
126+
These should return information about active kernels/terminals, or empty arrays if none exist.
127+
128+
### 3. Use Relative URLs in Your Application
129+
130+
**Critical**: Your application must generate relative URLs, not absolute ones.
131+
132+
```html
133+
<!-- ❌ BAD - Hardcoded absolute path -->
134+
<a href="/menu1">Menu 1</a>
135+
<script src="/static/app.js"></script>
136+
<img src="/images/logo.png" />
137+
138+
<!-- ✅ GOOD - Relative URLs -->
139+
<a href="menu1">Menu 1</a>
140+
<script src="static/app.js"></script>
141+
<img src="images/logo.png" />
142+
143+
<!-- ✅ ALSO GOOD - Framework-generated URLs with base path -->
144+
<a href="{{ url_for('menu1') }}">Menu 1</a>
145+
```
146+
147+
**Why**: Hardcoded absolute paths like `/menu1` will not include the `{NB_PREFIX}`, causing 404 errors. Relative URLs or framework-generated URLs will correctly resolve to `/{NB_PREFIX}/menu1`.
148+
149+
### 4. Configure Your Application's Base Path
150+
151+
If your framework supports it, configure the base path using the `NB_PREFIX` environment variable:
152+
153+
**FastAPI**:
154+
```python
155+
from fastapi import FastAPI
156+
import os
157+
158+
app = FastAPI(root_path=os.getenv('NB_PREFIX', ''))
159+
```
160+
161+
**Flask**:
162+
```python
163+
from flask import Flask
164+
import os
165+
166+
app = Flask(__name__)
167+
app.config['APPLICATION_ROOT'] = os.getenv('NB_PREFIX', '')
168+
```
169+
170+
**Express.js**:
171+
```javascript
172+
const express = require('express');
173+
const app = express();
174+
const nbPrefix = process.env.NB_PREFIX || '';
175+
176+
// Mount all routes under the prefix
177+
const router = express.Router();
178+
// ... define routes on router ...
179+
app.use(nbPrefix, router);
180+
```
181+
182+
**Streamlit**:
183+
```toml
184+
# .streamlit/config.toml
185+
[server]
186+
baseUrlPath = "/notebook/namespace/workbench" # Set via NB_PREFIX
187+
```
188+
189+
### 5. Limitations: Applications with Hardcoded Absolute Paths
190+
191+
**If your application has hardcoded absolute paths that cannot be changed**, migration becomes very difficult:
192+
193+
```javascript
194+
// ❌ This cannot work with Gateway API unless rewritten
195+
const menuUrl = "/menu1"; // Hardcoded absolute path
196+
fetch(menuUrl).then(...);
197+
```
198+
199+
**Solutions**:
200+
1. **Modify the application** - Change to relative URLs or configurable base path (preferred)
201+
2. **Use nginx with URL rewriting** - nginx can intercept and rewrite some URLs, but this is limited
202+
3. **HTML/JS post-processing** - Intercept responses and rewrite URLs (complex, not recommended)
203+
204+
**Warning**: nginx can rewrite URLs in redirects and some headers, but it **cannot** rewrite URLs embedded in HTML/JavaScript content without complex content manipulation, which is error-prone and slow.
205+
206+
---
207+
208+
## Part 2: For nginx-based Workbenches - Reverse Proxy Configuration
209+
210+
**Use this section if** your application does not support base path configuration and you need nginx to handle the path translation.
211+
212+
### Required nginx Changes
213+
214+
### 1. Remove Problematic Location Blocks
215+
216+
**REMOVE** any overly broad location blocks that cause infinite redirects:
217+
218+
```nginx
219+
# ❌ REMOVE THIS - Too broad, causes infinite loops
220+
location ${NB_PREFIX}/ {
221+
return 302 $custom_scheme://$http_host/app/;
222+
}
223+
```
224+
225+
**Why**: This matches ALL paths under the prefix, including your application endpoint itself (e.g., `/notebook/user/workbench/app/`), creating redirect loops.
226+
227+
### 2. Update Redirects to Preserve NB_PREFIX
228+
229+
**All redirects must include `${NB_PREFIX}`** to keep requests within the Gateway route:
230+
231+
```nginx
232+
# ❌ BAD - Strips prefix
233+
location = ${NB_PREFIX} {
234+
return 302 $custom_scheme://$http_host/myapp/;
235+
}
236+
237+
# ✅ GOOD - Preserves prefix
238+
location ${NB_PREFIX} {
239+
return 302 $custom_scheme://$http_host${NB_PREFIX}/myapp/;
240+
}
241+
```
242+
243+
**Note**: Use `location ${NB_PREFIX}` (without `=`) to handle both with and without trailing slash.
244+
245+
### 3. Add Prefix-Aware Proxy Location
246+
247+
**Add a location block** that matches the full prefixed path and strips the prefix before proxying:
248+
249+
```nginx
250+
location ${NB_PREFIX}/myapp/ {
251+
# Strip the prefix before proxying to backend
252+
rewrite ^${NB_PREFIX}/myapp/(.*)$ /$1 break;
253+
254+
# Proxy to your application
255+
proxy_pass http://localhost:8080/;
256+
proxy_http_version 1.1;
257+
258+
# Essential for WebSocket support
259+
proxy_set_header Upgrade $http_upgrade;
260+
proxy_set_header Connection $connection_upgrade;
261+
262+
# Long timeout for interactive sessions
263+
proxy_read_timeout 20d;
264+
265+
# Pass through important headers
266+
proxy_set_header Host $http_host;
267+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
268+
proxy_set_header X-Forwarded-Proto $custom_scheme;
269+
}
270+
```
271+
272+
### 4. Update Health Check Endpoints
273+
274+
Health checks must also preserve the prefix:
275+
276+
```nginx
277+
# Health check endpoint
278+
location = ${NB_PREFIX}/api {
279+
return 302 ${NB_PREFIX}/myapp/healthz/;
280+
access_log off;
281+
}
282+
```
283+
284+
### 5. Add Wildcard server_name Fallback
285+
286+
Gateway API uses different hostnames than OpenShift Routes. Add fallback logic:
287+
288+
```bash
289+
# In run-nginx.sh or startup script
290+
export BASE_URL=$(extract_base_url_from_notebook_args)
291+
292+
# If BASE_URL is empty or invalid, use wildcard server_name
293+
if [ -z "$BASE_URL" ] || [ "$BASE_URL" = "$(echo $NB_PREFIX | awk -F/ '{ print $4"-"$3 }')" ]; then
294+
export BASE_URL="_"
295+
fi
296+
```
297+
298+
This sets `server_name _;` which accepts requests from any hostname.
299+
300+
### 6. Update kube-rbac-proxy Configuration
301+
302+
Remove trailing slashes from upstream URLs in pod/statefulset specs:
303+
304+
```yaml
305+
# ❌ BAD
306+
args:
307+
- '--upstream=http://127.0.0.1:8888/'
308+
309+
# ✅ GOOD
310+
args:
311+
- '--upstream=http://127.0.0.1:8888'
312+
```
313+
314+
## HTTPRoute Configuration
315+
316+
Ensure your HTTPRoute matches the full prefix path:
317+
318+
```yaml
319+
apiVersion: gateway.networking.k8s.io/v1
320+
kind: HTTPRoute
321+
metadata:
322+
name: my-workbench
323+
namespace: <user-namespace>
324+
spec:
325+
parentRefs:
326+
- group: gateway.networking.k8s.io
327+
kind: Gateway
328+
name: data-science-gateway
329+
namespace: openshift-ingress
330+
rules:
331+
- backendRefs:
332+
- kind: Service
333+
name: my-workbench-rbac
334+
port: 8443
335+
weight: 1
336+
matches:
337+
- path:
338+
type: PathPrefix
339+
value: /notebook/<namespace>/<workbench-name>
340+
```
341+
342+
**Important**: The `value` must match the `NB_PREFIX` environment variable set in the pod.
343+
344+
## Reference Implementation
345+
346+
See these files for complete examples:
347+
348+
### Code-Server
349+
- **nginx config**: `codeserver/ubi9-python-3.12/nginx/serverconf/proxy.conf.template_nbprefix`
350+
- **startup script**: `codeserver/ubi9-python-3.12/run-nginx.sh`
351+
352+
### RStudio
353+
- **nginx config**: `rstudio/c9s-python-3.11/nginx/serverconf/proxy.conf.template_nbprefix`
354+
- **startup script**: `rstudio/c9s-python-3.11/run-nginx.sh`
355+
356+
## Understanding nginx Location Matching
357+
358+
nginx location blocks have different matching priorities:
359+
360+
```nginx
361+
# 1. Exact match (highest priority)
362+
location = /exact/path {
363+
# Only matches /exact/path (no trailing slash)
364+
}
365+
366+
# 2. Prefix match (evaluated in order of length)
367+
location /prefix {
368+
# Matches /prefix, /prefix/, /prefix/anything
369+
}
370+
371+
# 3. Regex match (not covered here)
372+
```
373+
374+
For Gateway API, you need:
375+
376+
```nginx
377+
# Redirect root to app
378+
location ${NB_PREFIX} {
379+
return 302 $custom_scheme://$http_host${NB_PREFIX}/myapp/;
380+
}
381+
382+
# Proxy app traffic (longer prefix wins)
383+
location ${NB_PREFIX}/myapp/ {
384+
proxy_pass http://localhost:8080/;
385+
}
386+
```
387+
388+
Request `/notebook/ns/wb` → matches first location → redirects
389+
Request `/notebook/ns/wb/myapp/` → matches second location (longer) → proxies

0 commit comments

Comments
 (0)