[PR #2] [MERGED] Make Kubernetes operator backend production-ready #36

Closed
opened 2026-10-18 21:13:41 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/open-webui/terminals/pull/2
Author: @james-westbrook
Created: 3/8/2026
Status: ✅ Merged
Merged: 3/8/2026
Merged by: @tjbck

Base: main ← Head: k8s-improvements


📝 Commits (1)

  • f4c0563 fix: make k8s operator more production ready

📊 Changes

11 files changed (+1110 additions, -529 deletions)

View changed files

📝 manifests/terminal-crd.yaml (+82 -22)
📝 operator/Dockerfile (+3 -1)
📝 operator/handler.py (+470 -249)
📝 pyproject.toml (+0 -5)
📝 terminals/backends/base.py (+35 -0)
📝 terminals/backends/kubernetes_operator.py (+346 -85)
📝 terminals/config.py (+9 -1)
📝 terminals/db/session.py (+15 -11)
📝 terminals/main.py (+7 -23)
➕ terminals/routers/auth.py (+97 -0)
📝 terminals/routers/proxy.py (+46 -132)

📄 Description

Problem

The Kubernetes operator backend had several issues preventing reliable end-to-end operation with Open WebUI:

  • Database dependency was unnecessary — the operator backend stores all state in Kubernetes CRDs, but the orchestrator still required a SQL database for tenant tracking, creating redundant state and operational complexity.
  • Route prefix mismatch — proxy routes were prefixed with terminals (e.g. /terminals/openapi.json, /terminals/{path:path}), but Open WebUI strips the server ID from the URL before forwarding, so all requests returned 404.
  • OpenAPI spec conflicts — FastAPI's built-in /openapi.json endpoint returned the orchestrator's own auto-generated schema (with X-User-Id header parameters), shadowing the open-terminal spec. Additionally, the open-terminal spec included securitySchemes that caused models to ask users for Bearer tokens instead of calling tools.
  • Finalizer race conditions — when resuming an idle terminal (delete CR → re-create), the kopf finalizer could still be running on the old CR when the new create arrived, causing 409 conflicts that weren't handled.
  • CRD schema gaps — the Terminal CRD was missing fields for resource limits, persistence, idle timeout configuration, and proper status reporting (serviceUrl, apiKeySecret, lastActivityAt).
  • touch_activity content-type error — the status sub-resource patch used the wrong content type, causing 400 errors from the API server.

Changes

Orchestrator (terminals)

  • Removed database dependency: _resolve_tenant now calls backend.ensure_terminal() / backend.touch_activity() directly against CRDs instead of going through SQL. Removed tenants_router import, lifecycle loop, and DB init/close from the app lifespan. Made db/session.py conditional so the engine is only created when database_url is set.
  • Fixed route prefixes: Removed terminals prefix from all proxy routes (/openapi.json, /{path:path}, /api/terminals/{session_id}).
  • Fixed OpenAPI serving: Set openapi_url=None on FastAPI to disable the built-in spec endpoint. The proxy router's /openapi.json now fetches the spec from the open-terminal pod, strips security and securitySchemes from the response, and caches it for 5 minutes.
  • Moved /health before the catch-all router so it's matched before /{path:path}.
    Extracted auth module (routers/auth.py): verify_api_key, verify_user_id, and validate_token are now in a shared module used by the proxy router.
  • Removed inline audit logging from the proxy — audit events were tightly coupled to the DB-backed tenant model. Audit can be re-added at the operator level where it belongs.
  • Added touch_activity() to the Backend ABC as an optional method with a default no-op.
  • Added resource configuration settings to config.py: CPU/memory requests and limits, idle timeout, and persistence size.

Operator (operator/handler.py)

  • Expanded CRD schema: Added resources (requests/limits), idleTimeoutMinutes, packages, pipPackages, persistence (enabled/size/storageClass) fields to the Terminal spec.
  • Status fields: The operator now sets serviceUrl, apiKeySecret, and lastActivityAt on the CR status, which the orchestrator reads directly instead of guessing hostnames.
  • Idle/resume lifecycle: Proper handling of the Idle→delete→re-create flow with finalizer-aware waiting.

Kubernetes operator backend (backends/kubernetes_operator.py)

  • ensure_terminal(): New DB-free get-or-create flow — checks CRD phase (Running → return immediately, Idle → delete and re-provision, Pending → wait for ready, Missing → create).
  • _create_terminal_cr(): Handles 409 conflicts by checking for deletionTimestamp and waiting for the finalizer to complete before retrying.
  • _delete_terminal_cr(): Polls until 404 so subsequent creates don't collide with pending finalizers.
  • _wait_for_ready(): Polls until status.phase == Running AND serviceUrl + apiKeySecret are set, then reads the API key from the referenced Secret.
  • touch_activity(): Patches status.lastActivityAt using application/merge-patch+json content type.
  • get_terminal_info(): Read-only lookup of existing terminal without creating one.

Testing

Tested end-to-end in a Kind cluster with the stock Open WebUI v0.8.8 image:

  • Terminal file browser works (directory listing, file read/write)
  • Model successfully calls terminal tools (list_files, execute, etc.) via the chat completions API
  • Idle→resume flow works without finalizer race conditions
  • WebSocket terminal proxy connects correctly
  • Multi-user pattern works correctly, with each new user getting their own terminal

Contributor License Agreement

Note

Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/open-webui/terminals/pull/2 **Author:** [@james-westbrook](https://github.com/james-westbrook) **Created:** 3/8/2026 **Status:** ✅ Merged **Merged:** 3/8/2026 **Merged by:** [@tjbck](https://github.com/tjbck) **Base:** `main` ← **Head:** `k8s-improvements` --- ### 📝 Commits (1) - [`f4c0563`](https://github.com/open-webui/terminals/commit/f4c0563067d57f8419c544122159c470904e52e2) fix: make k8s operator more production ready ### 📊 Changes **11 files changed** (+1110 additions, -529 deletions) <details> <summary>View changed files</summary> 📝 `manifests/terminal-crd.yaml` (+82 -22) 📝 `operator/Dockerfile` (+3 -1) 📝 `operator/handler.py` (+470 -249) 📝 `pyproject.toml` (+0 -5) 📝 `terminals/backends/base.py` (+35 -0) 📝 `terminals/backends/kubernetes_operator.py` (+346 -85) 📝 `terminals/config.py` (+9 -1) 📝 `terminals/db/session.py` (+15 -11) 📝 `terminals/main.py` (+7 -23) ➕ `terminals/routers/auth.py` (+97 -0) 📝 `terminals/routers/proxy.py` (+46 -132) </details> ### 📄 Description # Problem The Kubernetes operator backend had several issues preventing reliable end-to-end operation with Open WebUI: - Database dependency was unnecessary — the operator backend stores all state in Kubernetes CRDs, but the orchestrator still required a SQL database for tenant tracking, creating redundant state and operational complexity. - Route prefix mismatch — proxy routes were prefixed with terminals (e.g. /terminals/openapi.json, /terminals/{path:path}), but Open WebUI strips the server ID from the URL before forwarding, so all requests returned 404. - OpenAPI spec conflicts — FastAPI's built-in /openapi.json endpoint returned the orchestrator's own auto-generated schema (with X-User-Id header parameters), shadowing the open-terminal spec. Additionally, the open-terminal spec included securitySchemes that caused models to ask users for Bearer tokens instead of calling tools. - Finalizer race conditions — when resuming an idle terminal (delete CR → re-create), the kopf finalizer could still be running on the old CR when the new create arrived, causing 409 conflicts that weren't handled. - CRD schema gaps — the Terminal CRD was missing fields for resource limits, persistence, idle timeout configuration, and proper status reporting (serviceUrl, apiKeySecret, lastActivityAt). - touch_activity content-type error — the status sub-resource patch used the wrong content type, causing 400 errors from the API server. # Changes ## Orchestrator (terminals) - Removed database dependency: _resolve_tenant now calls backend.ensure_terminal() / backend.touch_activity() directly against CRDs instead of going through SQL. Removed tenants_router import, lifecycle loop, and DB init/close from the app lifespan. Made db/session.py conditional so the engine is only created when database_url is set. - Fixed route prefixes: Removed terminals prefix from all proxy routes (/openapi.json, /{path:path}, /api/terminals/{session_id}). - Fixed OpenAPI serving: Set openapi_url=None on FastAPI to disable the built-in spec endpoint. The proxy router's /openapi.json now fetches the spec from the open-terminal pod, strips security and securitySchemes from the response, and caches it for 5 minutes. - Moved /health before the catch-all router so it's matched before /{path:path}. Extracted auth module (routers/auth.py): verify_api_key, verify_user_id, and validate_token are now in a shared module used by the proxy router. - Removed inline audit logging from the proxy — audit events were tightly coupled to the DB-backed tenant model. Audit can be re-added at the operator level where it belongs. - Added touch_activity() to the Backend ABC as an optional method with a default no-op. - Added resource configuration settings to config.py: CPU/memory requests and limits, idle timeout, and persistence size. ## Operator (operator/handler.py) - Expanded CRD schema: Added resources (requests/limits), idleTimeoutMinutes, packages, pipPackages, persistence (enabled/size/storageClass) fields to the Terminal spec. - Status fields: The operator now sets serviceUrl, apiKeySecret, and lastActivityAt on the CR status, which the orchestrator reads directly instead of guessing hostnames. - Idle/resume lifecycle: Proper handling of the Idle→delete→re-create flow with finalizer-aware waiting. ## Kubernetes operator backend (backends/kubernetes_operator.py) - ensure_terminal(): New DB-free get-or-create flow — checks CRD phase (Running → return immediately, Idle → delete and re-provision, Pending → wait for ready, Missing → create). - _create_terminal_cr(): Handles 409 conflicts by checking for deletionTimestamp and waiting for the finalizer to complete before retrying. - _delete_terminal_cr(): Polls until 404 so subsequent creates don't collide with pending finalizers. - _wait_for_ready(): Polls until status.phase == Running AND serviceUrl + apiKeySecret are set, then reads the API key from the referenced Secret. - touch_activity(): Patches status.lastActivityAt using application/merge-patch+json content type. - get_terminal_info(): Read-only lookup of existing terminal without creating one. # Testing Tested end-to-end in a Kind cluster with the stock Open WebUI v0.8.8 image: - Terminal file browser works (directory listing, file read/write) - Model successfully calls terminal tools (list_files, execute, etc.) via the chat completions API - Idle→resume flow works without finalizer race conditions - WebSocket terminal proxy connects correctly - Multi-user pattern works correctly, with each new user getting their own terminal ## Contributor License Agreement <!-- 🚨 DO NOT DELETE THE TEXT BELOW 🚨 Keep the "Contributor License Agreement" confirmation text intact. Deleting it will trigger the CLA-Bot to INVALIDATE your PR. Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA. --> - [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](./CLA.md), and I am providing my contributions under its terms. > [!NOTE] > Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-18 21:13:41 +00:00
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
open-webui/terminals#36
No description provided.