Skip to content

Commit e474360

Browse files
authored
Merge branch 'main' into fix-orchestrator-dns-names
2 parents 1951b21 + bce82b9 commit e474360

File tree

4 files changed

+136
-52
lines changed

4 files changed

+136
-52
lines changed

.github/workflows/docker-build.yml

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,24 @@ env:
1212
REGISTRY: ghcr.io
1313

1414
jobs:
15+
get-version:
16+
runs-on: ubuntu-latest
17+
outputs:
18+
version: ${{ steps.version.outputs.version }}
19+
steps:
20+
- name: Checkout repository
21+
uses: actions/checkout@v5
22+
23+
- name: Extract version from pyproject.toml
24+
id: version
25+
run: |
26+
VERSION=$(grep '^version' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
27+
echo "version=$VERSION" >> $GITHUB_OUTPUT
28+
echo "Extracted version: $VERSION"
29+
1530
build-orchestrator:
1631
runs-on: ${{ matrix.runner }}
32+
needs: [get-version]
1733
permissions:
1834
contents: read
1935
packages: write
@@ -63,6 +79,7 @@ jobs:
6379
type=sha,prefix=git-
6480
type=semver,pattern={{version}}
6581
type=semver,pattern={{major}}.{{minor}}
82+
type=raw,value=${{ needs.get-version.outputs.version }}
6683
flavor: |
6784
latest=${{ github.ref == 'refs/heads/main' }}
6885
@@ -109,6 +126,7 @@ jobs:
109126

110127
build-operator:
111128
runs-on: ${{ matrix.runner }}
129+
needs: [get-version]
112130
permissions:
113131
contents: read
114132
packages: write
@@ -158,6 +176,7 @@ jobs:
158176
type=sha,prefix=git-
159177
type=semver,pattern={{version}}
160178
type=semver,pattern={{major}}.{{minor}}
179+
type=raw,value=${{ needs.get-version.outputs.version }}
161180
flavor: |
162181
latest=${{ github.ref == 'refs/heads/main' }}
163182
@@ -204,7 +223,7 @@ jobs:
204223

205224
merge-orchestrator-images:
206225
runs-on: ubuntu-latest
207-
needs: [build-orchestrator]
226+
needs: [get-version, build-orchestrator]
208227
permissions:
209228
contents: read
210229
packages: write
@@ -241,6 +260,7 @@ jobs:
241260
type=sha,prefix=git-
242261
type=semver,pattern={{version}}
243262
type=semver,pattern={{major}}.{{minor}}
263+
type=raw,value=${{ needs.get-version.outputs.version }}
244264
flavor: |
245265
latest=${{ github.ref == 'refs/heads/main' }}
246266
@@ -252,11 +272,11 @@ jobs:
252272
253273
- name: Inspect image
254274
run: |
255-
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
275+
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ needs.get-version.outputs.version }}
256276
257277
merge-operator-images:
258278
runs-on: ubuntu-latest
259-
needs: [build-operator]
279+
needs: [get-version, build-operator]
260280
permissions:
261281
contents: read
262282
packages: write
@@ -293,6 +313,7 @@ jobs:
293313
type=sha,prefix=git-
294314
type=semver,pattern={{version}}
295315
type=semver,pattern={{major}}.{{minor}}
316+
type=raw,value=${{ needs.get-version.outputs.version }}
296317
flavor: |
297318
latest=${{ github.ref == 'refs/heads/main' }}
298319
@@ -304,4 +325,33 @@ jobs:
304325
305326
- name: Inspect image
306327
run: |
307-
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
328+
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ needs.get-version.outputs.version }}
329+
330+
create-release:
331+
runs-on: ubuntu-latest
332+
if: github.ref == 'refs/heads/main'
333+
needs: [get-version, merge-orchestrator-images, merge-operator-images]
334+
permissions:
335+
contents: write
336+
steps:
337+
- name: Checkout repository
338+
uses: actions/checkout@v5
339+
340+
- name: Extract changelog for version
341+
id: changelog
342+
run: |
343+
VERSION="${{ needs.get-version.outputs.version }}"
344+
# Extract the section for this version from CHANGELOG.md
345+
NOTES=$(awk "/^## \\[$VERSION\\]/{found=1; next} /^## \\[/{if(found) exit} found" CHANGELOG.md)
346+
# Write to file to preserve newlines
347+
echo "$NOTES" > /tmp/release-notes.md
348+
echo "Extracted release notes for v$VERSION"
349+
cat /tmp/release-notes.md
350+
351+
- name: Create GitHub Release
352+
uses: softprops/action-gh-release@v2
353+
with:
354+
tag_name: v${{ needs.get-version.outputs.version }}
355+
name: v${{ needs.get-version.outputs.version }}
356+
body_path: /tmp/release-notes.md
357+
make_latest: true

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.0.1] - 2026-04-02
9+
10+
### Added
11+
- Multi-tenant terminal orchestrator with Docker and Kubernetes backends.
12+
- Kubernetes operator for terminal custom resource management.
13+
- CLI interface for managing terminals.
14+
- Docker build workflows for orchestrator and operator images (multi-arch: amd64/arm64).

README.md

Lines changed: 67 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,21 @@
55
66
Per-user [Open Terminal](https://github.com/open-webui/open-terminal) orchestration for Docker and Kubernetes.
77

8-
Giving terminal access to users on Open WebUI requires per-user isolation: separate containers, each with their own credentials and resource constraints. Terminals handles the full lifecycle: provisioning containers on demand, proxying traffic per user, enforcing resource and network policies, validating Open WebUI JWTs natively, and cleaning up idle instances.
9-
10-
| Capability | |
11-
|---|---|
12-
| **Backends** | Docker, Kubernetes, K8s Operator |
13-
| **Provisioning** | On-demand per user, transparent to the client |
14-
| **Policies** | Per-environment image, CPU, memory, network, env vars via REST API |
15-
| **Auth** | Open WebUI JWT validation or static API key |
16-
| **Hard caps** | Admin-enforced limits on CPU, memory, storage, and allowed images |
17-
| **Multi-environment** | Named policies with routing via `/p/{policy_id}/` |
18-
| **Network control** | Egress filtering via `env.OPEN_TERMINAL_ALLOWED_DOMAINS` |
19-
| **Idle cleanup** | Automatic teardown of inactive instances |
20-
| **Runtime changes** | Update policies via API without redeployment |
8+
Terminals gives every Open WebUI user their own isolated container — with separate credentials, resource limits, and network rules. It handles the full lifecycle automatically: spinning up containers when a user connects, proxying traffic, enforcing limits, and cleaning up when they're done.
9+
10+
```
11+
Open WebUI → Terminals service → per-user containers
12+
(this project) (Open Terminal images)
13+
```
2114

2215
> [!IMPORTANT]
2316
> **Production use requires an [Open WebUI Enterprise License](LICENSE) with Terminals access.** Contact the Open WebUI team to get started.
2417
25-
2618
## Quick Start
2719

28-
```bash
29-
pip install -e .
30-
terminals serve
31-
```
20+
The fastest way to get running is with Docker. Terminals will manage sibling containers through the Docker socket.
3221

33-
Or with Docker:
22+
### Docker (recommended for single-node)
3423

3524
```bash
3625
docker run -p 3000:3000 \
@@ -39,24 +28,61 @@ docker run -p 3000:3000 \
3928
terminals
4029
```
4130

31+
**Prerequisites:** Docker running on the host.
32+
33+
### Kubernetes Operator (recommended for clusters)
34+
35+
For Kubernetes deployments, the operator manages `Terminal` custom resources automatically — handling pod creation, storage, and cleanup through CRDs.
36+
37+
```bash
38+
# Install the CRD and operator
39+
kubectl apply -f manifests/terminal-crd.yaml
40+
kubectl apply -f manifests/operator-deployment.yaml
41+
```
42+
43+
Set `TERMINALS_BACKEND=kubernetes-operator` when deploying the Terminals service.
44+
45+
### From source (development)
46+
47+
```bash
48+
pip install -e .
49+
terminals serve
50+
```
51+
52+
## Choosing a Backend
53+
54+
| Backend | Best for | How it works |
55+
|---------|----------|-------------|
56+
| `docker` | Single-node, local dev | One container per user via Docker socket |
57+
| `kubernetes-operator` | Production K8s clusters | Operator watches `Terminal` CRDs for automated lifecycle |
58+
| `kubernetes` | K8s without CRDs | Direct Pod + PVC + Service per user (you manage resources) |
59+
60+
Set the backend with `TERMINALS_BACKEND` (defaults to `docker`).
61+
4262
## Policies
4363

44-
Policies define per-environment configuration. Manage via REST API:
64+
Policies let you define different environments — for example, a "data-science" environment with extra CPU and specific Python packages, or a "sandbox" environment with restricted network access.
65+
66+
Without any policies, Terminals uses the defaults from your configuration. Once you're ready to customize, manage policies through the REST API:
4567

4668
```bash
69+
# Create a "data-science" policy
4770
curl -X PUT http://localhost:3000/api/v1/policies/data-science \
4871
-H "Authorization: Bearer $API_KEY" \
4972
-H "Content-Type: application/json" \
5073
-d '{
5174
"image": "ghcr.io/open-webui/open-terminal:python-ds",
5275
"cpu_limit": "2",
5376
"memory_limit": "4Gi",
54-
"env": {"OPENAI_API_KEY": "sk-proj-...", "OPEN_TERMINAL_ALLOWED_DOMAINS": "*.pypi.org,github.com"},
77+
"env": {
78+
"OPENAI_API_KEY": "sk-proj-...",
79+
"OPEN_TERMINAL_ALLOWED_DOMAINS": "*.pypi.org,github.com"
80+
},
5581
"idle_timeout_minutes": 30
5682
}'
5783
```
5884

59-
Route requests through a policy via `/p/{policy_id}/`:
85+
Route requests through a policy by adding `/p/{policy_id}/` to the URL:
6086

6187
```bash
6288
curl -X POST http://localhost:3000/p/data-science/execute \
@@ -65,47 +91,41 @@ curl -X POST http://localhost:3000/p/data-science/execute \
6591
-d '{"command": "echo hello"}'
6692
```
6793

94+
### Policy fields
95+
6896
| Field | Type | Description |
6997
|-------|------|-------------|
70-
| `image` | string | Container image |
71-
| `env` | dict | Environment variables |
98+
| `image` | string | Container image to use |
99+
| `env` | dict | Environment variables passed to the container |
72100
| `cpu_limit` | string | Max CPU (e.g. `"2"`) |
73101
| `memory_limit` | string | Max memory (e.g. `"4Gi"`) |
74-
| `storage` | string | Persistent volume size (absent = ephemeral) |
75-
| `storage_mode` | string | `per-user`, `shared`, `shared-rwo` (absent = global default) |
76-
| `idle_timeout_minutes` | int | Idle timeout before cleanup |
102+
| `storage` | string | Persistent volume size (omit for ephemeral storage) |
103+
| `storage_mode` | string | `per-user`, `shared`, or `shared-rwo` |
104+
| `idle_timeout_minutes` | int | Minutes of inactivity before the container is cleaned up |
77105

78106
## Configuration
79107

80-
Environment variables prefixed with `TERMINALS_` (or `.env` file).
108+
All settings are configured through environment variables prefixed with `TERMINALS_`, or via a `.env` file.
81109

82110
| Variable | Default | Description |
83111
|----------|---------|-------------|
84-
| `TERMINALS_BACKEND` | `docker` | `docker`, `kubernetes`, `kubernetes-operator` |
85-
| `TERMINALS_API_KEY` | *(auto)* | Bearer token for API auth |
86-
| `TERMINALS_OPEN_WEBUI_URL` | | Open WebUI URL for JWT auth |
112+
| `TERMINALS_BACKEND` | `docker` | `docker`, `kubernetes`, or `kubernetes-operator` |
113+
| `TERMINALS_API_KEY` | *(auto-generated)* | Bearer token for API auth |
87114
| `TERMINALS_IMAGE` | `ghcr.io/open-webui/open-terminal:latest` | Default container image |
88-
| `TERMINALS_MAX_CPU` | | Hard cap on CPU |
89-
| `TERMINALS_MAX_MEMORY` | | Hard cap on memory |
90-
| `TERMINALS_MAX_STORAGE` | | Hard cap on storage |
91-
| `TERMINALS_ALLOWED_IMAGES` | | Comma-separated image globs |
92-
| `TERMINALS_KUBERNETES_STORAGE_MODE` | `per-user` | `per-user`, `shared`, `shared-rwo` |
115+
| `TERMINALS_MAX_CPU` | | Hard cap on CPU per container |
116+
| `TERMINALS_MAX_MEMORY` | | Hard cap on memory per container |
117+
| `TERMINALS_MAX_STORAGE` | | Hard cap on storage per container |
118+
| `TERMINALS_ALLOWED_IMAGES` | | Comma-separated list of allowed image patterns |
119+
| `TERMINALS_KUBERNETES_STORAGE_MODE` | `per-user` | `per-user`, `shared`, or `shared-rwo` |
93120

94121
See [`config.py`](terminals/config.py) for the full list.
95122

96123
## Authentication
97124

98-
| Mode | Trigger |
99-
|------|---------|
100-
| **Open WebUI JWT** | Set `TERMINALS_OPEN_WEBUI_URL` |
101-
| **API Key** | Set `TERMINALS_API_KEY` |
102-
| **Open** | Neither set (dev only) |
103-
104-
## Backends
105-
106-
- **`docker`** – One container per user via Docker socket
107-
- **`kubernetes`** – Pod + PVC + Service per user
108-
- **`kubernetes-operator`** – Kopf operator watching `Terminal` CRDs
125+
| Mode | How to enable |
126+
|------|---------------|
127+
| **API Key** | Set `TERMINALS_API_KEY` to a static token |
128+
| **Open (dev only)** | Leave unset — no auth, for local development only |
109129

110130
## License
111131

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "terminals"
3-
version = "0.1.1"
3+
version = "0.0.2"
44
description = "Multi-tenant terminal orchestrator for Open Terminal."
55
readme = "README.md"
66
authors = [

0 commit comments

Comments
 (0)