From aff7263afd83ba8e47968a7a85d69e1b01414245 Mon Sep 17 00:00:00 2001 From: SamiAhmed7777 Date: Sun, 9 Mar 2025 23:29:06 -0700 Subject: [PATCH] feat: Add container management plugin for OpenWebUI - Add monitoring, update, and restart capabilities --- .../custom/components/ContainerStatus.svelte | 35 +++ custom/README.md | 77 ++++++ custom/api/container_routes.py | 43 ++++ custom/components/ContainerSettings.svelte | 121 ++++++++++ custom/container_manager.py | 228 ++++++++++++++++++ custom/container_settings.py | 66 +++++ custom/lib/apis/functions.ts | 26 ++ custom/plugin.py | 23 ++ custom/types/container.ts | 14 ++ 9 files changed, 633 insertions(+) create mode 100644 container-status-test/custom/components/ContainerStatus.svelte create mode 100644 custom/README.md create mode 100644 custom/api/container_routes.py create mode 100644 custom/components/ContainerSettings.svelte create mode 100644 custom/container_manager.py create mode 100644 custom/container_settings.py create mode 100644 custom/lib/apis/functions.ts create mode 100644 custom/plugin.py create mode 100644 custom/types/container.ts diff --git a/container-status-test/custom/components/ContainerStatus.svelte b/container-status-test/custom/components/ContainerStatus.svelte new file mode 100644 index 000000000..70f492a03 --- /dev/null +++ b/container-status-test/custom/components/ContainerStatus.svelte @@ -0,0 +1,35 @@ + + +
+ {#if error} +

{error}

+ {:else} +

Container Status: {containerStatus}

+ {/if} +
+ + diff --git a/custom/README.md b/custom/README.md new file mode 100644 index 000000000..ab32f85f0 --- /dev/null +++ b/custom/README.md @@ -0,0 +1,77 @@ +# OpenWebUI Container Management Plugin + +This plugin adds container management capabilities to OpenWebUI, allowing you to monitor, update, and restart the container directly from the web interface. + +## Features + +- View container status and version +- Update container to the latest version +- Restart container when needed +- User-friendly interface integrated into OpenWebUI settings + +## Installation + +### Installing in Docker-based OpenWebUI + +1. If your OpenWebUI is running in Docker, mount the plugin directory into the container by adding this volume to your docker-compose.yml: + ```yaml + services: + openwebui: + volumes: + - ./custom:/app/custom # Mount the plugin directory + ``` + +2. Copy the `custom` directory to your OpenWebUI installation directory (where your docker-compose.yml is located) + +3. Restart your OpenWebUI container: + ```bash + docker-compose restart openwebui + ``` + +The plugin will be automatically detected and loaded by OpenWebUI when it starts. + +### Dependencies + +The plugin requires the following Python packages which are already included in the OpenWebUI Docker image: +- fastapi +- docker-py + +## Usage + +1. Navigate to the OpenWebUI settings page +2. Look for the "Container Management" section +3. You can: + - View the current container status and version + - Click "Update Container" to update to the latest version + - Click "Restart Container" to restart the container + +## API Endpoints + +The plugin exposes the following API endpoints: + +- `GET /api/container/status` - Get container status and version +- `POST /api/container/update` - Update the container +- `POST /api/container/restart` - Restart the container + +## Development + +To modify or extend the plugin: + +1. Frontend components are in `custom/components/` +2. Backend API routes are in `custom/api/` +3. Container management logic is in `custom/container_manager.py` +4. Plugin registration is handled in `custom/plugin.py` + +### Development Workflow + +1. Make changes to the plugin code +2. The changes will be reflected immediately in the OpenWebUI container due to the volume mount +3. If you modify Python files, you'll need to restart the OpenWebUI container: + ```bash + docker-compose restart openwebui + ``` +4. Frontend changes (Svelte components) will be hot-reloaded automatically + +## Contributing + +Feel free to submit issues and pull requests to improve the plugin. \ No newline at end of file diff --git a/custom/api/container_routes.py b/custom/api/container_routes.py new file mode 100644 index 000000000..f0545a7ba --- /dev/null +++ b/custom/api/container_routes.py @@ -0,0 +1,43 @@ +from fastapi import APIRouter, HTTPException +from typing import Dict +from ..container_manager import ContainerManager + +router = APIRouter() +container_manager = ContainerManager() + +@router.get("/status") +async def get_container_status() -> Dict: + """Get the current status and version of the container.""" + try: + status = container_manager.get_status() + version = container_manager.get_version() + return { + "status": status, + "version": version + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/update") +async def update_container() -> Dict: + """Update the container to the latest version.""" + try: + success = container_manager.update() + return { + "success": success, + "message": "Container updated successfully" if success else "Update failed" + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/restart") +async def restart_container() -> Dict: + """Restart the container.""" + try: + success = container_manager.restart() + return { + "success": success, + "message": "Container restarted successfully" if success else "Restart failed" + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/custom/components/ContainerSettings.svelte b/custom/components/ContainerSettings.svelte new file mode 100644 index 000000000..2fb8b5d0f --- /dev/null +++ b/custom/components/ContainerSettings.svelte @@ -0,0 +1,121 @@ +{/* ContainerSettings.svelte */} + + + +

Container Management

+ +
+
+ + +
+ +
+ + +
+ +
+ + + +
+
+
\ No newline at end of file diff --git a/custom/container_manager.py b/custom/container_manager.py new file mode 100644 index 000000000..e34aacbb2 --- /dev/null +++ b/custom/container_manager.py @@ -0,0 +1,228 @@ +""" +type: action +name: Container Manager +description: Manage chat.Sami container updates +version: 1.0.0 +author: chat.Sami +""" + +import os +import subprocess +import json +import requests +from typing import Dict, Any + +class Action: + def __init__(self): + self.actions = [ + { + "id": "update_container", + "name": "Update Container", + "description": "Update chat.Sami to the latest version" + }, + { + "id": "container_status", + "name": "Container Status", + "description": "Get current container status and version" + }, + { + "id": "check_updates", + "name": "Check Updates", + "description": "Check for available updates" + } + ] + + def execute(self, action_id: str, params: Dict[str, Any] = None) -> Dict[str, Any]: + if action_id == "update_container": + return self._update_container() + elif action_id == "container_status": + return self._get_container_status() + elif action_id == "check_updates": + return self._check_updates() + else: + raise ValueError(f"Unknown action: {action_id}") + + def _get_container_config(self) -> Dict[str, Any]: + """Get current container configuration""" + try: + result = subprocess.run( + ["docker", "inspect", "chat-sami"], + capture_output=True, + text=True, + check=True + ) + container_info = json.loads(result.stdout)[0] + + # Extract current configuration + config = { + "port": None, + "volumes": [], + "env": {} + } + + # Get port mapping + ports = container_info["HostConfig"]["PortBindings"] + if "8080/tcp" in ports: + port_info = ports["8080/tcp"][0] + config["port"] = port_info["HostPort"] + + # Get volume mappings + mounts = container_info["HostConfig"]["Mounts"] + for mount in mounts: + config["volumes"].append({ + "source": mount["Source"], + "target": mount["Target"] + }) + + # Get environment variables + env_list = container_info["Config"]["Env"] + for env in env_list: + if "=" in env: + key, value = env.split("=", 1) + config["env"][key] = value + + return config + except Exception as e: + return None + + def _get_current_version(self) -> str: + try: + result = subprocess.run( + ["docker", "inspect", "chat-sami"], + capture_output=True, + text=True, + check=True + ) + container_info = json.loads(result.stdout)[0] + image_tag = container_info["Config"]["Image"].split(":")[-1] + return image_tag + except: + return "unknown" + + def _get_latest_version(self) -> str: + try: + response = requests.get( + "https://api.github.com/repos/open-webui/open-webui/releases/latest", + timeout=5 + ) + if response.status_code == 200: + return response.json()["tag_name"].lstrip('v') + return "unknown" + except: + return "unknown" + + def _check_updates(self) -> Dict[str, Any]: + current = self._get_current_version() + latest = self._get_latest_version() + + return { + "success": True, + "data": { + "current_version": current, + "latest_version": latest, + "update_available": latest != "unknown" and current != latest + } + } + + def _update_container(self) -> Dict[str, Any]: + try: + # Get current container configuration + config = self._get_container_config() + if not config: + raise Exception("Failed to get current container configuration") + + # Pull the latest image + subprocess.run( + ["docker", "pull", "ghcr.io/open-webui/open-webui:main"], + check=True, + capture_output=True + ) + + # Stop the current container + subprocess.run( + ["docker", "stop", "chat-sami"], + check=True, + capture_output=True + ) + + # Remove the old container but keep the volumes + subprocess.run( + ["docker", "rm", "chat-sami"], + check=True, + capture_output=True + ) + + # Create new container with same configuration + cmd = ["docker", "run", "-d", "--name", "chat-sami"] + + # Add port mapping + if config["port"]: + cmd.extend(["-p", f"{config['port']}:8080"]) + + # Add volume mappings + for volume in config["volumes"]: + cmd.extend(["-v", f"{volume['source']}:{volume['target']}"]) + + # Add environment variables + for key, value in config["env"].items(): + cmd.extend(["-e", f"{key}={value}"]) + + # Add image name + cmd.append("ghcr.io/open-webui/open-webui:main") + + # Run the new container + subprocess.run(cmd, check=True, capture_output=True) + + return { + "success": True, + "message": "Updated to latest version. The page will refresh in a few moments." + } + except subprocess.CalledProcessError as e: + return { + "success": False, + "message": f"Update failed: {e.stderr.decode() if e.stderr else str(e)}" + } + except Exception as e: + return { + "success": False, + "message": f"Error during update: {str(e)}" + } + + def _get_container_status(self) -> Dict[str, Any]: + try: + # Get container info + result = subprocess.run( + ["docker", "inspect", "chat-sami"], + capture_output=True, + text=True, + check=True + ) + container_info = json.loads(result.stdout)[0] + + # Get update status + update_info = self._check_updates() + current_version = update_info["data"]["current_version"] + latest_version = update_info["data"]["latest_version"] + update_available = update_info["data"]["update_available"] + + return { + "success": True, + "data": { + "status": container_info["State"]["Status"], + "health": container_info["State"]["Health"]["Status"] if "Health" in container_info["State"] else "N/A", + "current_version": current_version, + "latest_version": latest_version, + "update_available": update_available, + "created": container_info["Created"] + } + } + except subprocess.CalledProcessError as e: + return { + "success": False, + "message": f"Failed to get container status: {e.stderr.decode() if e.stderr else str(e)}" + } + except Exception as e: + return { + "success": False, + "message": f"Error: {str(e)}" + } \ No newline at end of file diff --git a/custom/container_settings.py b/custom/container_settings.py new file mode 100644 index 000000000..92d94a01c --- /dev/null +++ b/custom/container_settings.py @@ -0,0 +1,66 @@ +""" +type: settings +name: Container Settings +description: Manage chat.Sami container settings +version: 1.0.0 +author: chat.Sami +""" + +from typing import Dict, Any +import json +from fastapi import APIRouter +from pydantic import BaseModel + +router = APIRouter() + +class ContainerSettings(BaseModel): + """Container management settings.""" + enabled: bool = True + +@router.get("/settings/container") +async def get_container_settings() -> Dict[str, Any]: + """Get container management settings.""" + return { + "id": "container", + "title": "Container Management", + "description": "Manage the chat.Sami container", + "component": "ContainerStatus", + "settings": ContainerSettings().dict() + } + +class Settings: + def __init__(self): + self.settings = { + "container": { + "title": "Container Management", + "description": "Manage your chat.Sami container", + "type": "object", + "properties": { + "status": { + "type": "custom", + "component": "ContainerStatus", + "action": "container_status", + "refresh_interval": 30, # Refresh every 30 seconds + "title": "Container Status" + }, + "update": { + "type": "button", + "title": "Update Container", + "description": "Update chat.Sami to the latest version", + "action": "update_container", + "buttonText": "Update Now", + "confirmText": "Are you sure you want to update the container? The service will restart briefly." + } + } + } + } + + def get_settings(self) -> Dict[str, Any]: + return self.settings + + def update_settings(self, settings: Dict[str, Any]) -> Dict[str, Any]: + # This is a read-only settings panel + return { + "success": True, + "message": "Settings updated" + } \ No newline at end of file diff --git a/custom/lib/apis/functions.ts b/custom/lib/apis/functions.ts new file mode 100644 index 000000000..f4ae23b33 --- /dev/null +++ b/custom/lib/apis/functions.ts @@ -0,0 +1,26 @@ +export async function executeAction(actionId: string, params?: Record): Promise { + // This is a mock implementation for testing + const mockResponses = { + container_status: { + success: true, + data: { + status: "running", + health: "healthy", + current_version: "1.0.0", + latest_version: "1.1.0", + update_available: true, + created: new Date().toISOString() + } + }, + update_container: { + success: true, + message: "Update initiated" + } + }; + + return new Promise((resolve) => { + setTimeout(() => { + resolve(mockResponses[actionId as keyof typeof mockResponses] as T); + }, 500); + }); +} \ No newline at end of file diff --git a/custom/plugin.py b/custom/plugin.py new file mode 100644 index 000000000..565348438 --- /dev/null +++ b/custom/plugin.py @@ -0,0 +1,23 @@ +from fastapi import FastAPI +from .api.container_routes import router as container_router + +def register_plugin(app: FastAPI) -> None: + """Register the container management plugin with OpenWebUI.""" + # Register API routes + app.include_router( + container_router, + prefix="/api/container", + tags=["container"] + ) + + # Register frontend components + app.state.plugin_components = app.state.plugin_components or {} + app.state.plugin_components["container_settings"] = { + "name": "ContainerSettings", + "path": "custom/components/ContainerSettings.svelte", + "settings": { + "title": "Container Management", + "icon": "container", + "description": "Manage container updates and status" + } + } \ No newline at end of file diff --git a/custom/types/container.ts b/custom/types/container.ts new file mode 100644 index 000000000..9ce6aba58 --- /dev/null +++ b/custom/types/container.ts @@ -0,0 +1,14 @@ +export interface ContainerStatus { + status: string; + health: string; + current_version: string; + latest_version: string; + update_available: boolean; + created: string; +} + +export interface ContainerResponse { + success: boolean; + data?: ContainerStatus; + message?: string; +} \ No newline at end of file