Compare commits

...

1 Commits

9 changed files with 633 additions and 0 deletions
@@ -0,0 +1,35 @@
<script>
import { onMount } from 'svelte';
let containerStatus = '';
let error = '';
onMount(async () => {
try {
const response = await fetch('/api/container/status');
const data = await response.json();
containerStatus = data.status;
} catch (err) {
error = 'Failed to fetch container status';
console.error(err);
}
});
</script>
<div class="container-status">
{#if error}
<p class="error">{error}</p>
{:else}
<p>Container Status: {containerStatus}</p>
{/if}
</div>
<style>
.container-status {
padding: 1rem;
}
.error {
color: red;
}
</style>
+77
View File
@@ -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.
+43
View File
@@ -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))
+121
View File
@@ -0,0 +1,121 @@
{/* ContainerSettings.svelte */}
<script lang="ts">
import { onMount } from 'svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Card } from '$lib/components/ui/card';
import { toast } from '$lib/components/ui/toaster';
let containerStatus = '';
let containerVersion = '';
let isUpdating = false;
let isRestarting = false;
async function fetchContainerStatus() {
try {
const response = await fetch('/api/container/status');
const data = await response.json();
containerStatus = data.status;
containerVersion = data.version;
} catch (error) {
toast({
title: 'Error',
description: 'Failed to fetch container status',
variant: 'destructive'
});
}
}
async function handleUpdate() {
isUpdating = true;
try {
const response = await fetch('/api/container/update', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
toast({
title: 'Success',
description: 'Container updated successfully'
});
await fetchContainerStatus();
} else {
throw new Error(data.message);
}
} catch (error) {
toast({
title: 'Error',
description: 'Failed to update container',
variant: 'destructive'
});
} finally {
isUpdating = false;
}
}
async function handleRestart() {
isRestarting = true;
try {
const response = await fetch('/api/container/restart', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
toast({
title: 'Success',
description: 'Container restarted successfully'
});
await fetchContainerStatus();
} else {
throw new Error(data.message);
}
} catch (error) {
toast({
title: 'Error',
description: 'Failed to restart container',
variant: 'destructive'
});
} finally {
isRestarting = false;
}
}
onMount(() => {
fetchContainerStatus();
});
</script>
<Card class="p-6">
<h2 class="text-2xl font-bold mb-4">Container Management</h2>
<div class="space-y-4">
<div class="flex flex-col gap-2">
<Label>Status</Label>
<Input value={containerStatus} readonly />
</div>
<div class="flex flex-col gap-2">
<Label>Version</Label>
<Input value={containerVersion} readonly />
</div>
<div class="flex gap-4 mt-4">
<Button
variant="primary"
disabled={isUpdating}
on:click={handleUpdate}
>
{isUpdating ? 'Updating...' : 'Update Container'}
</Button>
<Button
variant="secondary"
disabled={isRestarting}
on:click={handleRestart}
>
{isRestarting ? 'Restarting...' : 'Restart Container'}
</Button>
</div>
</div>
</Card>
+228
View File
@@ -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)}"
}
+66
View File
@@ -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"
}
+26
View File
@@ -0,0 +1,26 @@
export async function executeAction<T>(actionId: string, params?: Record<string, unknown>): Promise<T> {
// 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);
});
}
+23
View File
@@ -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"
}
}
+14
View File
@@ -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;
}