diff --git a/.icons/openwebui.svg b/.icons/openwebui.svg new file mode 100644 index 000000000..06f676458 --- /dev/null +++ b/.icons/openwebui.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/registry/coder-labs/.images/openwebui.png b/registry/coder-labs/.images/openwebui.png new file mode 100644 index 000000000..709d3e5f3 Binary files /dev/null and b/registry/coder-labs/.images/openwebui.png differ diff --git a/registry/coder-labs/modules/open-webui/README.md b/registry/coder-labs/modules/open-webui/README.md new file mode 100644 index 000000000..1fa0092a1 --- /dev/null +++ b/registry/coder-labs/modules/open-webui/README.md @@ -0,0 +1,64 @@ +--- +display_name: Open WebUI +description: A self-hosted AI chat interface supporting various LLM providers +icon: ../../../../.icons/openwebui.svg +verified: false +tags: [ai, llm, chat, web, python] +--- + +# Open WebUI + +Open WebUI is a user-friendly web interface for interacting with Large Language Models. It provides a ChatGPT-like interface that can connect to various LLM providers including OpenAI, Ollama, and more. + +```tf +module "open-webui" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder-labs/open-webui/coder" + version = "1.0.0" + agent_id = coder_agent.main.id +} +``` + +![Open WebUI](../../.images/openwebui.png) + +## Prerequisites + +- **Python 3.11 or higher** must be installed in your image (with `venv` module) +- Port 7800 (default) or your custom port must be available + +For Ubuntu/Debian, you can install Python 3.11 from [deadsnakes PPA](https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa): + +```shell +sudo add-apt-repository -y ppa:deadsnakes/ppa +sudo apt-get update +sudo apt-get install -y python3.11 python3.11-venv +``` + +## Examples + +### With OpenAI API Key + +```tf +module "open-webui" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder-labs/open-webui/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + + openai_api_key = var.openai_api_key +} +``` + +### Custom Port and Data Directory + +```tf +module "open-webui" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder-labs/open-webui/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + + http_server_port = 8080 + data_dir = "/home/coder/open-webui-data" +} +``` diff --git a/registry/coder-labs/modules/open-webui/main.tf b/registry/coder-labs/modules/open-webui/main.tf new file mode 100644 index 000000000..2e3190b6e --- /dev/null +++ b/registry/coder-labs/modules/open-webui/main.tf @@ -0,0 +1,94 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "http_server_log_path" { + type = string + description = "The path to log Open WebUI to." + default = "/tmp/open-webui.log" +} + +variable "http_server_port" { + type = number + description = "The port to run Open WebUI on." + default = 7800 +} + +variable "open_webui_version" { + type = string + description = "The version of Open WebUI to install" + default = "latest" +} + +variable "data_dir" { + type = string + description = "The directory where Open WebUI stores its data (database, uploads, vector_db, cache)." + default = ".open-webui" +} + +variable "openai_api_key" { + type = string + description = "OpenAI API key for accessing OpenAI models. If not provided, OpenAI integration will need to be configured manually in the UI." + default = "" + sensitive = true +} + +variable "share" { + type = string + description = "The sharing level for the Open WebUI app. Set to 'owner' for private access, 'authenticated' for access by any authenticated user, or 'public' for public access." + default = "owner" + validation { + condition = var.share == "owner" || var.share == "authenticated" || var.share == "public" + error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'." + } +} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +variable "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +resource "coder_script" "open-webui" { + agent_id = var.agent_id + display_name = "open-webui" + icon = "/icon/openwebui.svg" + script = templatefile("${path.module}/run.sh", { + HTTP_SERVER_LOG_PATH : var.http_server_log_path, + HTTP_SERVER_PORT : var.http_server_port, + VERSION : var.open_webui_version, + DATA_DIR : var.data_dir, + OPENAI_API_KEY : var.openai_api_key, + }) + run_on_start = true +} + +resource "coder_app" "open-webui" { + agent_id = var.agent_id + slug = "open-webui" + display_name = "Open WebUI" + url = "http://localhost:${var.http_server_port}" + icon = "/icon/openwebui.svg" + subdomain = true + share = var.share + order = var.order + group = var.group +} diff --git a/registry/coder-labs/modules/open-webui/main.tftest.hcl b/registry/coder-labs/modules/open-webui/main.tftest.hcl new file mode 100644 index 000000000..cd0099729 --- /dev/null +++ b/registry/coder-labs/modules/open-webui/main.tftest.hcl @@ -0,0 +1,188 @@ +mock_provider "coder" {} + +run "test_defaults" { + command = plan + + variables { + agent_id = "test-agent-123" + } + + assert { + condition = var.http_server_port == 7800 + error_message = "Default port should be 7800" + } + + assert { + condition = var.http_server_log_path == "/tmp/open-webui.log" + error_message = "Default log path should be /tmp/open-webui.log" + } + + assert { + condition = var.share == "owner" + error_message = "Default share should be 'owner'" + } + + assert { + condition = var.open_webui_version == "latest" + error_message = "Default version should be 'latest'" + } + + assert { + condition = coder_app.open-webui.subdomain == true + error_message = "App should use subdomain" + } + + assert { + condition = coder_app.open-webui.display_name == "Open WebUI" + error_message = "App display name should be 'Open WebUI'" + } +} + +run "test_custom_port" { + command = plan + + variables { + agent_id = "test-agent-456" + http_server_port = 9000 + } + + assert { + condition = var.http_server_port == 9000 + error_message = "Custom port should be 9000" + } + + assert { + condition = coder_app.open-webui.url == "http://localhost:9000" + error_message = "App URL should use custom port" + } +} + +run "test_custom_log_path" { + command = plan + + variables { + agent_id = "test-agent-789" + http_server_log_path = "/var/log/open-webui.log" + } + + assert { + condition = var.http_server_log_path == "/var/log/open-webui.log" + error_message = "Custom log path should be set" + } +} + +run "test_share_authenticated" { + command = plan + + variables { + agent_id = "test-agent-auth" + share = "authenticated" + } + + assert { + condition = coder_app.open-webui.share == "authenticated" + error_message = "Share should be 'authenticated'" + } +} + +run "test_share_public" { + command = plan + + variables { + agent_id = "test-agent-public" + share = "public" + } + + assert { + condition = coder_app.open-webui.share == "public" + error_message = "Share should be 'public'" + } +} + +run "test_order_and_group" { + command = plan + + variables { + agent_id = "test-agent-order" + order = 10 + group = "AI Tools" + } + + assert { + condition = coder_app.open-webui.order == 10 + error_message = "Order should be 10" + } + + assert { + condition = coder_app.open-webui.group == "AI Tools" + error_message = "Group should be 'AI Tools'" + } +} + +run "test_custom_version" { + command = plan + + variables { + agent_id = "test-agent-version" + open_webui_version = "0.5.0" + } + + assert { + condition = var.open_webui_version == "0.5.0" + error_message = "Custom version should be '0.5.0'" + } +} + +run "test_custom_data_dir" { + command = plan + + variables { + agent_id = "test-agent-data" + data_dir = "/home/coder/open-webui-data" + } + + assert { + condition = var.data_dir == "/home/coder/open-webui-data" + error_message = "Custom data_dir should be set" + } +} + +run "test_default_data_dir" { + command = plan + + variables { + agent_id = "test-agent-data-default" + } + + assert { + condition = var.data_dir == ".open-webui" + error_message = "Default data_dir should be '.open-webui'" + } +} + +run "test_openai_api_key" { + command = plan + + variables { + agent_id = "test-agent-openai" + openai_api_key = "sk-test-key-123" + } + + assert { + condition = var.openai_api_key == "sk-test-key-123" + error_message = "OpenAI API key should be set" + } +} + +run "test_default_openai_api_key" { + command = plan + + variables { + agent_id = "test-agent-openai-default" + } + + assert { + condition = var.openai_api_key == "" + error_message = "Default OpenAI API key should be empty" + } +} diff --git a/registry/coder-labs/modules/open-webui/run.sh b/registry/coder-labs/modules/open-webui/run.sh new file mode 100755 index 000000000..647269eac --- /dev/null +++ b/registry/coder-labs/modules/open-webui/run.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env sh + +set -eu + +printf '\033[0;1mInstalling Open WebUI %s...\n\n' "${VERSION}" + +check_python_version() { + python_cmd="$1" + if command -v "$python_cmd" > /dev/null 2>&1; then + version=$("$python_cmd" --version 2>&1 | awk '{print $2}') + major=$(echo "$version" | cut -d. -f1) + minor=$(echo "$version" | cut -d. -f2) + if [ "$major" -eq 3 ] && [ "$minor" -ge 11 ]; then + echo "$python_cmd" + return 0 + fi + fi + return 1 +} + +PYTHON_CMD="" +for cmd in python3.13 python3.12 python3.11 python3 python; do + if result=$(check_python_version "$cmd"); then + PYTHON_CMD="$result" + echo "✅ Found suitable Python: $PYTHON_CMD ($($PYTHON_CMD --version 2>&1))" + break + fi +done + +if [ -z "$PYTHON_CMD" ]; then + echo "❌ Python 3.11 or higher is required but not found." + echo "" + echo "Please install Python 3.11+ in your image. For example on Ubuntu/Debian:" + echo " sudo add-apt-repository -y ppa:deadsnakes/ppa" + echo " sudo apt-get update" + echo " sudo apt-get install -y python3.11 python3.11-venv" + exit 1 +fi + +VENV_DIR="$HOME/.open-webui-venv" +if [ ! -d "$VENV_DIR" ]; then + echo "📦 Creating virtual environment..." + "$PYTHON_CMD" -m venv "$VENV_DIR" +fi +. "$VENV_DIR/bin/activate" + +if ! pip show open-webui > /dev/null 2>&1; then + echo "📦 Installing Open WebUI version ${VERSION}..." + if [ "${VERSION}" = "latest" ]; then + pip install open-webui + else + pip install "open-webui==${VERSION}" + fi + echo "🥳 Open WebUI has been installed" +else + echo "✅ Open WebUI is already installed" +fi + +echo "👷 Starting Open WebUI in background..." +echo "Check logs at ${HTTP_SERVER_LOG_PATH}" + +DATA_DIR="${DATA_DIR}" \ + OPENAI_API_KEY="${OPENAI_API_KEY}" \ + open-webui serve --host 0.0.0.0 --port "${HTTP_SERVER_PORT}" > "${HTTP_SERVER_LOG_PATH}" 2>&1 & + +echo "🥳 Open WebUI is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}"