diff --git a/agents/k8s-agent/agent_k8s_operations.go b/agents/k8s-agent/agent_k8s_operations.go index f33e2cd..a26d38a 100644 --- a/agents/k8s-agent/agent_k8s_operations.go +++ b/agents/k8s-agent/agent_k8s_operations.go @@ -687,25 +687,3 @@ func deletePVC(client *kubernetes.Clientset, namespace, sessionID string) error return nil } -// getTemplateImage returns the container image for a template. -// -// TODO: This should fetch the template from the Control Plane API -// and return the actual image. For now, we use a hardcoded mapping. -func getTemplateImage(templateName string) string { - // Default template images (from LinuxServer.io) - templates := map[string]string{ - "firefox": "lscr.io/linuxserver/firefox:latest", - "chrome": "lscr.io/linuxserver/chromium:latest", - "vscode": "lscr.io/linuxserver/code-server:latest", - "ubuntu": "lscr.io/linuxserver/webtop:ubuntu-mate", - "kali": "lscr.io/linuxserver/kali-linux:latest", - "libreoffice": "lscr.io/linuxserver/libreoffice:latest", - } - - if image, ok := templates[templateName]; ok { - return image - } - - // Default to Firefox if template not found - return "lscr.io/linuxserver/firefox:latest" -} diff --git a/agents/k8s-agent/agent_test.go b/agents/k8s-agent/agent_test.go index 07f4a0b..a771800 100644 --- a/agents/k8s-agent/agent_test.go +++ b/agents/k8s-agent/agent_test.go @@ -217,44 +217,6 @@ func TestHelperFunctions(t *testing.T) { } // TestGetTemplateImage tests template image mapping -func TestGetTemplateImage(t *testing.T) { - tests := []struct { - name string - template string - want string - }{ - { - name: "Firefox template", - template: "firefox", - want: "lscr.io/linuxserver/firefox:latest", - }, - { - name: "Chrome template", - template: "chrome", - want: "lscr.io/linuxserver/chromium:latest", - }, - { - name: "VS Code template", - template: "vscode", - want: "lscr.io/linuxserver/code-server:latest", - }, - { - name: "Unknown template", - template: "unknown", - want: "lscr.io/linuxserver/firefox:latest", // default - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := getTemplateImage(tt.template) - if got != tt.want { - t.Errorf("getTemplateImage() = %v, want %v", got, tt.want) - } - }) - } -} - // TestSessionSpec tests session specification parsing func TestSessionSpec(t *testing.T) { payload := map[string]interface{}{ diff --git a/api/internal/api/handlers.go b/api/internal/api/handlers.go index 0d574f7..2e050cb 100644 --- a/api/internal/api/handlers.go +++ b/api/internal/api/handlers.go @@ -253,122 +253,47 @@ func NewHandler(database *db.Database, publisher *events.Publisher, dispatcher C } } -// detectStreamingProtocol analyzes a template manifest and determines the streaming protocol. -// -// This function examines the template's baseImage and port configuration to determine -// which streaming protocol the session will use: -// - VNC: Traditional VNC servers (port 5900) -// - Selkies: LinuxServer images with WebRTC streaming (port 3000, path /websockify) -// - Guacamole: Apache Guacamole (port 8080) -// - X2Go: X2Go desktop sharing (port 22) -// -// The detection logic: -// 1. Parse manifest JSON to extract baseImage field -// 2. Check image name for known patterns (lscr.io/linuxserver, kasmweb, etc.) -// 3. Check port configuration for known streaming ports -// 4. Return protocol, port, and path (for HTTP-based protocols) +// detectStreamingProtocol resolves the streaming endpoint for a template +// manifest. StreamSpace is Selkies-only, so this only honors the manifest's +// explicit `spec.streamingProtocol` / `spec.ports[]` and otherwise returns the +// Selkies defaults (selkies on port 8080). // // Returns: -// - protocol: "vnc", "selkies", "guacamole", "x2go", etc. -// - port: The streaming service port (5900 for VNC, 3000 for Selkies, etc.) -// - path: URL path for HTTP-based protocols (e.g., "/websockify" for Selkies) +// - protocol: always "selkies" today, kept as a return so the column stays +// forward-compatible if a second protocol is ever added. +// - port: the streaming service port on the session pod (default 8080). +// - path: URL path for sub-resource routing (default empty). func detectStreamingProtocol(manifestJSON []byte) (protocol string, port int, path string) { - // Default to VNC - protocol = "vnc" - port = 5900 + protocol = "selkies" + port = 8080 path = "" - // Parse the template manifest var manifest map[string]interface{} if err := json.Unmarshal(manifestJSON, &manifest); err != nil { log.Printf("Failed to parse template manifest for protocol detection: %v", err) - return // Return defaults + return } - // Extract spec.baseImage spec, ok := manifest["spec"].(map[string]interface{}) if !ok { - return // Return defaults - } - - baseImage, ok := spec["baseImage"].(string) - if !ok { - return // Return defaults - } - - baseImage = strings.ToLower(baseImage) - - // Detection logic based on image name patterns - if strings.Contains(baseImage, "lscr.io/linuxserver/") || - strings.Contains(baseImage, "linuxserver/") { - // LinuxServer images use Selkies (WebRTC streaming via websockify) - protocol = "selkies" - port = 3000 - path = "/websockify" - log.Printf("Detected Selkies protocol from LinuxServer image: %s", baseImage) return } - if strings.Contains(baseImage, "kasmweb/") { - // Kasm images use Selkies-like protocol - protocol = "selkies" - port = 6901 - path = "/websockify" - log.Printf("Detected Selkies protocol from Kasm image: %s", baseImage) - return + // Honor explicit overrides if the template author set them. + if p, ok := spec["streamingProtocol"].(string); ok && p != "" { + protocol = p } - - if strings.Contains(baseImage, "guacamole/") { - // Apache Guacamole - protocol = "guacamole" - port = 8080 - path = "/guacamole" - log.Printf("Detected Guacamole protocol: %s", baseImage) - return - } - - if strings.Contains(baseImage, "x2go") { - // X2Go desktop sharing - protocol = "x2go" - port = 22 - path = "" - log.Printf("Detected X2Go protocol: %s", baseImage) - return - } - - // Check port configuration for protocol hints if ports, ok := spec["ports"].([]interface{}); ok && len(ports) > 0 { if firstPort, ok := ports[0].(map[string]interface{}); ok { - if containerPort, ok := firstPort["containerPort"].(float64); ok { - switch int(containerPort) { - case 3000, 6901: - // HTTP-based streaming (likely Selkies/Kasm) - protocol = "selkies" - port = int(containerPort) - path = "/websockify" - log.Printf("Detected Selkies protocol from port %d", port) - return - case 8080: - // Likely Guacamole - protocol = "guacamole" - port = 8080 - path = "/guacamole" - log.Printf("Detected Guacamole protocol from port 8080") - return - case 5900: - // Traditional VNC - protocol = "vnc" - port = 5900 - path = "" - log.Printf("Detected VNC protocol from port 5900") - return - } + if containerPort, ok := firstPort["containerPort"].(float64); ok && containerPort > 0 { + port = int(containerPort) } } } + if p, ok := spec["streamingPath"].(string); ok { + path = p + } - // Default to VNC if no specific protocol detected - log.Printf("No specific protocol detected for image %s, defaulting to VNC", baseImage) return } @@ -675,12 +600,14 @@ func (h *Handler) CreateSession(c *gin.Context) { "description": template.Description, "category": template.Category, "appType": template.AppType, - // Use a sensible default image for testing if we don't have one - "baseImage": "lscr.io/linuxserver/firefox:latest", + // Default to the bootstrap chrome-selkies image when the + // catalog hasn't supplied a baseImage of its own. + "baseImage": "ghcr.io/streamspace-dev/chrome-selkies:latest", + "streamingProtocol": "selkies", "ports": []map[string]interface{}{ { - "name": "vnc", - "containerPort": 3000, + "name": "selkies", + "containerPort": 8080, "protocol": "TCP", }, }, diff --git a/api/internal/sync/parser.go b/api/internal/sync/parser.go index 4b1e03a..d78b2c1 100644 --- a/api/internal/sync/parser.go +++ b/api/internal/sync/parser.go @@ -122,16 +122,17 @@ type ParsedTemplate struct { // apiVersion: stream.space/v1alpha1 // kind: Template // metadata: -// name: firefox-browser +// name: chrome-selkies // spec: -// displayName: Firefox Web Browser -// description: Modern, privacy-focused web browser +// displayName: Google Chrome +// description: Chrome browser streamed via Selkies-GStreamer // category: Web Browsers -// baseImage: lscr.io/linuxserver/firefox:latest -// vnc: -// enabled: true -// port: 3000 -// tags: [browser, web, privacy] +// baseImage: ghcr.io/streamspace-dev/chrome-selkies:latest +// streamingProtocol: selkies +// ports: +// - name: selkies +// containerPort: 8080 +// tags: [browser, web, selkies] type TemplateManifest struct { APIVersion string `yaml:"apiVersion" json:"apiVersion"` Kind string `yaml:"kind" json:"kind"` diff --git a/images/README.md b/images/README.md deleted file mode 100644 index 7edce51..0000000 --- a/images/README.md +++ /dev/null @@ -1,151 +0,0 @@ -# StreamSpace Container Images - -This directory contains standardized container images for StreamSpace sessions. - -## Image Design Philosophy - -StreamSpace images are designed with the following principles: - -1. **Protocol Standardization**: All images expose streaming on a consistent port -2. **Security First**: Run as non-root, minimal attack surface -3. **Performance Optimized**: Hardware acceleration support, optimized codecs -4. **Kubernetes Ready**: Health checks, resource limits, graceful shutdown - -## Available Images - -### chrome-selkies - -Chrome browser with Selkies-GStreamer WebRTC streaming. - -**Features:** -- Google Chrome stable -- Selkies WebRTC streaming (low latency) -- Hardware acceleration (NVENC, VA-API) -- Audio support -- Clipboard sharing - -**Build:** -```bash -cd chrome-selkies -docker build -t ghcr.io/streamspace-dev/chrome-selkies:latest . -``` - -**Test locally:** -```bash -docker run -p 8080:8080 ghcr.io/streamspace-dev/chrome-selkies:latest -# Open http://localhost:8080 in browser -``` - -## Image Standards - -### Ports - -| Protocol | Port | Description | -|----------|------|-------------| -| Selkies WebRTC | 8080 | Primary streaming port | -| VNC (fallback) | 5900 | VNC protocol | -| noVNC (fallback) | 6080 | Web VNC | - -### Environment Variables - -All images should support these standard variables: - -| Variable | Default | Description | -|----------|---------|-------------| -| DISPLAY_WIDTH | 1920 | Display width | -| DISPLAY_HEIGHT | 1080 | Display height | -| DISPLAY_DPI | 96 | Display DPI | -| PUID | 1000 | User ID | -| PGID | 1000 | Group ID | -| TZ | UTC | Timezone | - -### Labels - -All images should include these OCI labels: - -```dockerfile -LABEL org.opencontainers.image.title="StreamSpace " -LABEL org.opencontainers.image.description="" -LABEL org.opencontainers.image.version="" -LABEL org.opencontainers.image.vendor="StreamSpace" -LABEL org.opencontainers.image.source="https://github.com/streamspace-dev/streamspace" -``` - -### Health Checks - -All images must include a health check: - -```dockerfile -HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD curl -f http://localhost:8080/ || exit 1 -``` - -## Building Images - -### Local Build - -```bash -cd images/ -docker build -t ghcr.io/streamspace-dev/:latest . -``` - -### CI/CD Build - -Images are automatically built and pushed to GHCR on: -- Push to main branch -- Release tags - -## Testing Images - -### Quick Test - -```bash -# Run the image -docker run -d -p 8080:8080 --name test-session ghcr.io/streamspace-dev/:latest - -# Check health -docker inspect --format='{{.State.Health.Status}}' test-session - -# View logs -docker logs test-session - -# Cleanup -docker rm -f test-session -``` - -### Integration Test - -```bash -# Run with StreamSpace agent -./scripts/test-image.sh ghcr.io/streamspace-dev/:latest -``` - -## LinuxServer Compatibility - -For maximum compatibility with LinuxServer images, StreamSpace images can also be built to expose port 3000 with KasmVNC: - -```dockerfile -# Alternative: LinuxServer-compatible base -FROM lscr.io/linuxserver/baseimage-kasmvnc:ubuntujammy -``` - -This provides compatibility with existing LinuxServer catalog images. - -## Creating New Images - -1. Create a new directory under `images/` -2. Copy the template from an existing image -3. Modify the Dockerfile for your application -4. Update the entrypoint script -5. Add to the template catalog in the API -6. Test locally before pushing - -## Future Images - -Planned images for StreamSpace: - -- [ ] `firefox-selkies` - Firefox with Selkies WebRTC -- [ ] `vscode-selkies` - VS Code with Selkies WebRTC -- [ ] `ubuntu-desktop` - Full Ubuntu desktop -- [ ] `blender-selkies` - Blender 3D with GPU acceleration -- [ ] `gimp-selkies` - GIMP image editor diff --git a/images/chrome-selkies/Dockerfile b/images/chrome-selkies/Dockerfile deleted file mode 100644 index 3663dfe..0000000 --- a/images/chrome-selkies/Dockerfile +++ /dev/null @@ -1,109 +0,0 @@ -# StreamSpace Chrome Browser with Selkies WebRTC -# -# This is a standardized browser image for StreamSpace that uses Selkies-GStreamer -# for high-performance WebRTC streaming instead of traditional VNC. -# -# Features: -# - Chrome browser pre-configured -# - Selkies-GStreamer WebRTC streaming on port 8080 -# - Hardware acceleration support (NVENC, VA-API) -# - Clipboard sharing -# - Audio support -# - Optimized for low latency -# -# Build: -# docker build -t ghcr.io/streamspace-dev/chrome-selkies:latest . -# -# Run locally: -# docker run -p 8080:8080 ghcr.io/streamspace-dev/chrome-selkies:latest - -FROM ghcr.io/selkies-project/selkies-gstreamer:24.04 - -# Metadata -LABEL org.opencontainers.image.title="StreamSpace Chrome Browser" -LABEL org.opencontainers.image.description="Chrome browser with Selkies WebRTC streaming for StreamSpace" -LABEL org.opencontainers.image.version="1.0.0" -LABEL org.opencontainers.image.vendor="StreamSpace" -LABEL org.opencontainers.image.source="https://github.com/streamspace-dev/streamspace" - -# Environment variables for Selkies -ENV DISPLAY=:0 -ENV DISPLAY_SIZEW=1920 -ENV DISPLAY_SIZEH=1080 -ENV DISPLAY_DPI=96 -ENV DISPLAY_REFRESH=60 -ENV DISPLAY_CDEPTH=24 - -# Selkies configuration -ENV SELKIES_ENABLE_RESIZE=true -ENV SELKIES_ENABLE_BASIC_AUTH=false -ENV SELKIES_ENCODER=x264enc -ENV SELKIES_ENABLE_AUDIO=true -ENV SELKIES_AUDIO_BITRATE=128000 - -# Chrome-specific settings -ENV CHROME_FLAGS="--no-sandbox --disable-dev-shm-usage --disable-gpu-sandbox" - -# Streaming port -ENV WEBRTC_PORT=8080 -EXPOSE 8080 - -# User configuration -ENV PUID=1000 -ENV PGID=1000 -ENV HOME=/home/user -ENV USER=user - -# Install Chrome -RUN apt-get update && apt-get install -y \ - wget \ - gnupg2 \ - ca-certificates \ - fonts-liberation \ - libasound2 \ - libatk-bridge2.0-0 \ - libatk1.0-0 \ - libatspi2.0-0 \ - libcups2 \ - libdbus-1-3 \ - libdrm2 \ - libgbm1 \ - libgtk-3-0 \ - libnspr4 \ - libnss3 \ - libxcomposite1 \ - libxdamage1 \ - libxfixes3 \ - libxkbcommon0 \ - libxrandr2 \ - xdg-utils \ - && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \ - && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ - && apt-get update \ - && apt-get install -y google-chrome-stable \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Create user and directories -RUN groupadd -g ${PGID} ${USER} || true \ - && useradd -u ${PUID} -g ${PGID} -m -s /bin/bash ${USER} || true \ - && mkdir -p ${HOME}/.config/google-chrome \ - && chown -R ${PUID}:${PGID} ${HOME} - -# Copy startup script -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -# Set working directory -WORKDIR ${HOME} - -# Switch to non-root user -USER ${USER} - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD curl -f http://localhost:${WEBRTC_PORT}/ || exit 1 - -# Start Selkies with Chrome -ENTRYPOINT ["/entrypoint.sh"] -CMD ["google-chrome-stable", "--start-maximized"] diff --git a/images/chrome-selkies/entrypoint.sh b/images/chrome-selkies/entrypoint.sh deleted file mode 100644 index 8a04ba3..0000000 --- a/images/chrome-selkies/entrypoint.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# StreamSpace Chrome Selkies Entrypoint -# -# This script starts the Selkies-GStreamer WebRTC server with Chrome - -set -e - -# Configure display resolution if provided -if [ -n "$DISPLAY_WIDTH" ] && [ -n "$DISPLAY_HEIGHT" ]; then - export DISPLAY_SIZEW=$DISPLAY_WIDTH - export DISPLAY_SIZEH=$DISPLAY_HEIGHT -fi - -# Configure encoder based on available hardware -configure_encoder() { - # Check for NVIDIA GPU - if [ -e /dev/nvidia0 ]; then - echo "NVIDIA GPU detected, using NVENC encoder" - export SELKIES_ENCODER=nvh264enc - export SELKIES_ENABLE_NVFBC=true - return - fi - - # Check for Intel VA-API - if [ -e /dev/dri/renderD128 ]; then - echo "Intel/AMD GPU detected, using VA-API encoder" - export SELKIES_ENCODER=vah264enc - return - fi - - # Fallback to software encoding - echo "No GPU detected, using software x264 encoder" - export SELKIES_ENCODER=x264enc -} - -configure_encoder - -# Print configuration -echo "================================================" -echo "StreamSpace Chrome Selkies Container" -echo "================================================" -echo "Display: ${DISPLAY_SIZEW}x${DISPLAY_SIZEH}@${DISPLAY_REFRESH}Hz" -echo "Encoder: ${SELKIES_ENCODER}" -echo "Audio: ${SELKIES_ENABLE_AUDIO}" -echo "Port: ${WEBRTC_PORT}" -echo "================================================" - -# Start Selkies-GStreamer -exec selkies-gstreamer \ - --enable_audio=${SELKIES_ENABLE_AUDIO} \ - --enable_basic_auth=${SELKIES_ENABLE_BASIC_AUTH:-false} \ - --encoder=${SELKIES_ENCODER} \ - --port=${WEBRTC_PORT} \ - "$@" diff --git a/ui/e2e/fixtures/api.fixture.ts b/ui/e2e/fixtures/api.fixture.ts index f4eb26d..1de738f 100644 --- a/ui/e2e/fixtures/api.fixture.ts +++ b/ui/e2e/fixtures/api.fixture.ts @@ -70,29 +70,15 @@ export const MOCK_SESSIONS = { */ export const MOCK_TEMPLATES = [ { - name: 'chromium', - displayName: 'Chromium Browser', - description: 'Chromium web browser', + name: 'chrome-selkies', + displayName: 'Google Chrome', + description: 'Chrome browser streamed via Selkies-GStreamer (WebRTC)', category: 'browsers', - baseImage: 'lscr.io/linuxserver/chromium:latest', - defaultResources: { memory: '2Gi', cpu: '500m' }, - }, - { - name: 'firefox', - displayName: 'Firefox Browser', - description: 'Firefox web browser', - category: 'browsers', - baseImage: 'lscr.io/linuxserver/firefox:latest', + baseImage: 'ghcr.io/streamspace-dev/chrome-selkies:latest', + streamingProtocol: 'selkies', + streamingPort: 8080, defaultResources: { memory: '2Gi', cpu: '500m' }, }, - { - name: 'vscode', - displayName: 'VS Code', - description: 'Visual Studio Code editor', - category: 'development', - baseImage: 'lscr.io/linuxserver/code-server:latest', - defaultResources: { memory: '4Gi', cpu: '1000m' }, - }, ]; /** diff --git a/ui/src/mocks/handlers.ts b/ui/src/mocks/handlers.ts index 961a10c..d85c1f6 100644 --- a/ui/src/mocks/handlers.ts +++ b/ui/src/mocks/handlers.ts @@ -68,32 +68,16 @@ export const MOCK_SESSIONS = { export const MOCK_TEMPLATES = [ { - name: 'chromium', - displayName: 'Chromium Browser', - description: 'Chromium web browser with Selkies WebRTC streaming', + name: 'chrome-selkies', + displayName: 'Google Chrome', + description: 'Chrome browser streamed via Selkies-GStreamer (WebRTC)', category: 'browsers', - icon: '/icons/chromium.svg', - baseImage: 'lscr.io/linuxserver/chromium:latest', - defaultResources: { memory: '2Gi', cpu: '500m' }, - }, - { - name: 'firefox', - displayName: 'Firefox Browser', - description: 'Firefox web browser', - category: 'browsers', - icon: '/icons/firefox.svg', - baseImage: 'lscr.io/linuxserver/firefox:latest', + icon: '/icons/chrome.svg', + baseImage: 'ghcr.io/streamspace-dev/chrome-selkies:latest', + streamingProtocol: 'selkies', + streamingPort: 8080, defaultResources: { memory: '2Gi', cpu: '500m' }, }, - { - name: 'vscode', - displayName: 'VS Code', - description: 'Visual Studio Code editor', - category: 'development', - icon: '/icons/vscode.svg', - baseImage: 'lscr.io/linuxserver/code-server:latest', - defaultResources: { memory: '4Gi', cpu: '1000m' }, - }, ]; export const MOCK_AGENTS = [