Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fc02b5734 | |||
| d7e3946e11 |
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
**MCP Hub** — a Python MCP (Model Context Protocol) server that manages multiple self-hosted services through a unified plugin architecture. Supports 10 plugin types (WordPress, WooCommerce, WordPress Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus, Coolify) with 597 tools total. The tool count stays constant regardless of how many sites are configured.
|
**MCP Hub** — a Python MCP (Model Context Protocol) server that manages multiple self-hosted services through a unified plugin architecture. Supports 10 plugin types (WordPress, WooCommerce, WordPress Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus, Coolify) with 633 tools total. The tool count stays constant regardless of how many sites are configured.
|
||||||
|
|
||||||
## Quick Setup
|
## Quick Setup
|
||||||
|
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -13,8 +13,8 @@ Connect your sites, stores, repos, and databases — manage them all through Cla
|
|||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
[](https://pypi.org/project/mcphub-server/)
|
[](https://pypi.org/project/mcphub-server/)
|
||||||
[](https://hub.docker.com/r/airano/mcphub)
|
[](https://hub.docker.com/r/airano/mcphub)
|
||||||
[]()
|
[]()
|
||||||
[]()
|
[]()
|
||||||
[](https://github.com/airano-ir/mcphub/actions/workflows/ci.yml)
|
[](https://github.com/airano-ir/mcphub/actions/workflows/ci.yml)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +25,7 @@ Connect your sites, stores, repos, and databases — manage them all through Cla
|
|||||||
|
|
||||||
WordPress powers 43% of the web. WooCommerce runs 36% of online stores. Yet **no MCP server existed** for managing them through AI — until now.
|
WordPress powers 43% of the web. WooCommerce runs 36% of online stores. Yet **no MCP server existed** for managing them through AI — until now.
|
||||||
|
|
||||||
MCP Hub is the first MCP server that lets you manage WordPress, WooCommerce, and 8 other self-hosted services through any AI assistant. Instead of clicking through dashboards, just tell your AI what to do:
|
MCP Hub is the first MCP server that lets you manage WordPress, WooCommerce, and 7 other self-hosted services through any AI assistant. Instead of clicking through dashboards, just tell your AI what to do:
|
||||||
|
|
||||||
> *"Update the SEO meta description for all WooCommerce products that don't have one"*
|
> *"Update the SEO meta description for all WooCommerce products that don't have one"*
|
||||||
>
|
>
|
||||||
@@ -62,9 +62,8 @@ MCP Hub is the first MCP server that lets you manage WordPress, WooCommerce, and
|
|||||||
| **OpenPanel** | 42 | Events, export, insights, profiles, projects, system |
|
| **OpenPanel** | 42 | Events, export, insights, profiles, projects, system |
|
||||||
| **Appwrite** | 100 | Databases, auth, storage, functions, teams, messaging |
|
| **Appwrite** | 100 | Databases, auth, storage, functions, teams, messaging |
|
||||||
| **Directus** | 100 | Collections, items, users, files, flows, permissions |
|
| **Directus** | 100 | Collections, items, users, files, flows, permissions |
|
||||||
| **Coolify** | 30 | Applications, deployments, servers, env vars, logs |
|
|
||||||
| **System** | 24 | Health monitoring, API keys, OAuth management, audit |
|
| **System** | 24 | Health monitoring, API keys, OAuth management, audit |
|
||||||
| **Total** | **596** | Constant count — scales to unlimited sites |
|
| **Total** | **565** | Constant count — scales to unlimited sites |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -165,7 +164,6 @@ MASTER_API_KEY=your-secure-key-here
|
|||||||
| OpenPanel | URL, Client ID, Client Secret | OpenPanel Dashboard → Project Settings |
|
| OpenPanel | URL, Client ID, Client Secret | OpenPanel Dashboard → Project Settings |
|
||||||
| Appwrite | URL, API Key, Project ID | Appwrite Console → Settings → API Keys |
|
| Appwrite | URL, API Key, Project ID | Appwrite Console → Settings → API Keys |
|
||||||
| Directus | URL, Static Token | Directus Admin → Settings |
|
| Directus | URL, Static Token | Directus Admin → Settings |
|
||||||
| Coolify | URL, API Token | Coolify → Keys & Tokens → API tokens |
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -267,7 +265,7 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
/mcp → Admin endpoint (all 565 tools)
|
/mcp → Admin endpoint (all 633 tools)
|
||||||
/system/mcp → System tools only (24 tools)
|
/system/mcp → System tools only (24 tools)
|
||||||
/wordpress/mcp → WordPress tools (67 tools)
|
/wordpress/mcp → WordPress tools (67 tools)
|
||||||
/woocommerce/mcp → WooCommerce tools (28 tools)
|
/woocommerce/mcp → WooCommerce tools (28 tools)
|
||||||
@@ -277,13 +275,13 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au
|
|||||||
/supabase/mcp → Supabase tools (70 tools)
|
/supabase/mcp → Supabase tools (70 tools)
|
||||||
/openpanel/mcp → OpenPanel tools (42 tools)
|
/openpanel/mcp → OpenPanel tools (42 tools)
|
||||||
/appwrite/mcp → Appwrite tools (100 tools)
|
/appwrite/mcp → Appwrite tools (100 tools)
|
||||||
|
/coolify/mcp → Coolify tools (67 tools)
|
||||||
/directus/mcp → Directus tools (100 tools)
|
/directus/mcp → Directus tools (100 tools)
|
||||||
/coolify/mcp → Coolify tools (30 tools)
|
|
||||||
/project/{alias}/mcp → Per-project endpoint (auto-injects site)
|
/project/{alias}/mcp → Per-project endpoint (auto-injects site)
|
||||||
/u/{user_id}/{alias}/mcp → Per-user endpoint (hosted/OAuth users)
|
/u/{user_id}/{alias}/mcp → Per-user endpoint (hosted/OAuth users)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Recommendation**: Use plugin-specific endpoints instead of `/mcp` (596 tools) to minimize token usage.
|
**Recommendation**: Use plugin-specific endpoints instead of `/mcp` (633 tools) to minimize token usage.
|
||||||
|
|
||||||
| Endpoint | Use Case | Tools |
|
| Endpoint | Use Case | Tools |
|
||||||
|----------|----------|------:|
|
|----------|----------|------:|
|
||||||
|
|||||||
115
docs/prompts/next-session-f17-phase3.md
Normal file
115
docs/prompts/next-session-f17-phase3.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# Next Session — F.17 Phase 3: Projects + Phase 2: Databases & Services
|
||||||
|
|
||||||
|
> Session prompt. Copy and paste into a new Claude Code conversation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt:
|
||||||
|
|
||||||
|
```
|
||||||
|
از مهارت project-ops برای آشنایی با محیط استفاده کن. حافظه و فایلهای مرجع را بررسی کن.
|
||||||
|
|
||||||
|
## فایلهای مرجع
|
||||||
|
- docs/plans/2026-04-02-coolify-mcp-plugin-design.md (mcphub-internal) ← طرح کامل پلاگین
|
||||||
|
- docs/plans/2026-03-25-v4-development-cycle.md (mcphub-internal) ← Phase F.17 آپدیت شده
|
||||||
|
- plugins/coolify/ (mcphub-internal) ← پلاگین Phase 1 (الگوی اصلی)
|
||||||
|
- plugins/gitea/ (mcphub-internal) ← الگوی مرجع معماری
|
||||||
|
- CLAUDE.md (mcphub-internal)
|
||||||
|
|
||||||
|
## وضعیت فعلی
|
||||||
|
- MCPHub: v3.7.0 — 596 ابزار، 10 پلاگین
|
||||||
|
- Coolify Phase 1: ✅ 30 ابزار (17 app + 5 deploy + 8 server) — deployed, tested, synced
|
||||||
|
- MCP endpoint فعال: mcphub-coolify در Claude Code (30 ابزار لود شده)
|
||||||
|
- CI سبز، 718 تست (internal), 686 تست (public)
|
||||||
|
|
||||||
|
## ریپازیتوری
|
||||||
|
- MCPHub (internal): `/config/workspace/mcphub-internal` (branch Phase-1)
|
||||||
|
|
||||||
|
## هدف session: F.17 Phase 3 + Phase 2
|
||||||
|
|
||||||
|
### بخش اول: Phase 3 — Projects & Environments (8 ابزار)
|
||||||
|
> اولویت بالا — بدون project_uuid ساخت app/db/service بلاک است
|
||||||
|
|
||||||
|
#### مرحله ۱: Projects Handler
|
||||||
|
- [ ] `plugins/coolify/handlers/projects.py`
|
||||||
|
- [ ] ابزارها:
|
||||||
|
- list_projects (GET /projects) — read
|
||||||
|
- get_project (GET /projects/{uuid}) — read
|
||||||
|
- create_project (POST /projects) — write
|
||||||
|
- update_project (PATCH /projects/{uuid}) — write
|
||||||
|
- delete_project (DELETE /projects/{uuid}) — admin
|
||||||
|
- list_environments (GET /projects/{uuid}/environments) — read
|
||||||
|
- get_environment (GET /projects/{uuid}/environments/{name}) — read
|
||||||
|
- create_environment (POST /projects/{uuid}/environments) — write
|
||||||
|
- [ ] متدهای client در `client.py` اضافه شود
|
||||||
|
- [ ] در `handlers/__init__.py` ایمپورت projects اضافه شود
|
||||||
|
- [ ] در `plugin.py` — specs و __getattr__ آپدیت شود
|
||||||
|
|
||||||
|
#### مرحله ۲: تست و دیپلوی Phase 3
|
||||||
|
- [ ] `tests/test_coolify_projects.py` — Unit tests با mocked HTTP
|
||||||
|
- [ ] `pytest tests/test_coolify*.py -v`
|
||||||
|
- [ ] Commit: `feat(F.17): add projects handler — Phase 3 (8 tools)`
|
||||||
|
- [ ] Push و درخواست redeploy
|
||||||
|
- [ ] تست live: list_projects → پیدا کردن project_uuid
|
||||||
|
- [ ] تست ساخت container: create_application_docker_image با project_uuid واقعی → دیپلوی nginx → تأیید → حذف
|
||||||
|
|
||||||
|
### بخش دوم: Phase 2 — Databases (16 ابزار)
|
||||||
|
- [ ] `plugins/coolify/handlers/databases.py`
|
||||||
|
- [ ] ابزارها:
|
||||||
|
- list_databases, get_database — read
|
||||||
|
- update_database — write
|
||||||
|
- delete_database — admin
|
||||||
|
- start_database, stop_database, restart_database — write
|
||||||
|
- create_postgresql, create_mysql, create_mariadb — write
|
||||||
|
- create_mongodb, create_redis, create_clickhouse — write
|
||||||
|
- get_database_backups — read
|
||||||
|
- create_database_backup — write
|
||||||
|
- list_backup_executions — read
|
||||||
|
- [ ] متدهای client اضافه شود
|
||||||
|
- [ ] تست: `tests/test_coolify_databases.py`
|
||||||
|
|
||||||
|
### بخش سوم: Phase 2 — Services (13 ابزار)
|
||||||
|
- [ ] `plugins/coolify/handlers/services.py`
|
||||||
|
- [ ] ابزارها:
|
||||||
|
- list_services, get_service — read
|
||||||
|
- create_service — write
|
||||||
|
- update_service — write
|
||||||
|
- delete_service — admin
|
||||||
|
- start_service, stop_service, restart_service — write
|
||||||
|
- list_service_envs — read
|
||||||
|
- create_service_env — write
|
||||||
|
- update_service_env — write
|
||||||
|
- update_service_envs_bulk — write
|
||||||
|
- delete_service_env — write
|
||||||
|
- [ ] متدهای client اضافه شود
|
||||||
|
- [ ] تست: `tests/test_coolify_services.py`
|
||||||
|
|
||||||
|
### بخش چهارم: ثبت و تست نهایی
|
||||||
|
- [ ] handlers/__init__.py آپدیت (projects, databases, services)
|
||||||
|
- [ ] plugin.py آپدیت (specs + __getattr__)
|
||||||
|
- [ ] server.py — نیازی به تغییر ندارد (coolify قبلا ثبت شده)
|
||||||
|
- [ ] `uvx --python 3.12 black .`
|
||||||
|
- [ ] `uvx ruff check --fix .`
|
||||||
|
- [ ] `pytest` (همه تستها سبز)
|
||||||
|
- [ ] Commit: `feat(F.17): add databases + services handlers — Phase 2 (29 tools)`
|
||||||
|
- [ ] Push و درخواست redeploy
|
||||||
|
- [ ] تست live: list_databases, list_services
|
||||||
|
- [ ] آپدیت ورژن به v3.8.0 (اگر تأیید شد)
|
||||||
|
|
||||||
|
### بخش پنجم: Sync و داکیومنت
|
||||||
|
- [ ] Sync به نسخه عمومی: `python3.11 scripts/community-build/sync.py --output ../mcphub/`
|
||||||
|
- [ ] black + ruff + pytest در نسخه عمومی
|
||||||
|
- [ ] README آپدیت (تعداد ابزار، جدول Coolify)
|
||||||
|
- [ ] Commit و push نسخه عمومی
|
||||||
|
- [ ] mcp-skills/skills/coolify/SKILL.md آپدیت (67 ابزار)
|
||||||
|
- [ ] حافظه آپدیت شود
|
||||||
|
- [ ] پلن آپدیت شود (Phase 2+3 complete)
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
- server.py نیازی به تغییر ندارد — coolify قبلا register شده و generate_tools خودکار specs جدید رو میخونه
|
||||||
|
- site_api.py نیازی به تغییر ندارد — credential fields و display name قبلا اضافه شده
|
||||||
|
- الگوی handler: از applications.py کپی کن (آخرین و تمیزترین)
|
||||||
|
- هر بخش (Phase 3, databases, services) جداگانه commit شود
|
||||||
|
- قبل از هر عملیات write در تست live از کاربر تأیید بگیر
|
||||||
|
- ایمیل git داخلی: mcphub.dev@gmail.com
|
||||||
|
```
|
||||||
@@ -267,3 +267,163 @@ class CoolifyClient:
|
|||||||
async def validate_server(self, uuid: str) -> dict:
|
async def validate_server(self, uuid: str) -> dict:
|
||||||
"""Validate server connectivity and configuration."""
|
"""Validate server connectivity and configuration."""
|
||||||
return await self.request("GET", f"servers/{uuid}/validate")
|
return await self.request("GET", f"servers/{uuid}/validate")
|
||||||
|
|
||||||
|
# --- Projects ---
|
||||||
|
|
||||||
|
async def list_projects(self) -> list[dict]:
|
||||||
|
"""List all projects."""
|
||||||
|
return await self.request("GET", "projects")
|
||||||
|
|
||||||
|
async def get_project(self, uuid: str) -> dict:
|
||||||
|
"""Get project by UUID."""
|
||||||
|
return await self.request("GET", f"projects/{uuid}")
|
||||||
|
|
||||||
|
async def create_project(self, data: dict) -> dict:
|
||||||
|
"""Create a new project."""
|
||||||
|
return await self.request("POST", "projects", json_data=data)
|
||||||
|
|
||||||
|
async def update_project(self, uuid: str, data: dict) -> dict:
|
||||||
|
"""Update project by UUID."""
|
||||||
|
return await self.request("PATCH", f"projects/{uuid}", json_data=data)
|
||||||
|
|
||||||
|
async def delete_project(self, uuid: str) -> dict:
|
||||||
|
"""Delete project by UUID."""
|
||||||
|
return await self.request("DELETE", f"projects/{uuid}")
|
||||||
|
|
||||||
|
# --- Environments ---
|
||||||
|
|
||||||
|
async def list_environments(self, project_uuid: str) -> list[dict]:
|
||||||
|
"""List environments in a project."""
|
||||||
|
return await self.request("GET", f"projects/{project_uuid}/environments")
|
||||||
|
|
||||||
|
async def get_environment(self, project_uuid: str, environment_name: str) -> dict:
|
||||||
|
"""Get environment by name."""
|
||||||
|
return await self.request("GET", f"projects/{project_uuid}/environments/{environment_name}")
|
||||||
|
|
||||||
|
async def create_environment(self, project_uuid: str, data: dict) -> dict:
|
||||||
|
"""Create environment in a project."""
|
||||||
|
return await self.request("POST", f"projects/{project_uuid}/environments", json_data=data)
|
||||||
|
|
||||||
|
# --- Databases ---
|
||||||
|
|
||||||
|
async def list_databases(self) -> list[dict]:
|
||||||
|
"""List all databases."""
|
||||||
|
return await self.request("GET", "databases")
|
||||||
|
|
||||||
|
async def get_database(self, uuid: str) -> dict:
|
||||||
|
"""Get database by UUID."""
|
||||||
|
return await self.request("GET", f"databases/{uuid}")
|
||||||
|
|
||||||
|
async def update_database(self, uuid: str, data: dict) -> dict:
|
||||||
|
"""Update database by UUID."""
|
||||||
|
return await self.request("PATCH", f"databases/{uuid}", json_data=data)
|
||||||
|
|
||||||
|
async def delete_database(
|
||||||
|
self,
|
||||||
|
uuid: str,
|
||||||
|
delete_configurations: bool = True,
|
||||||
|
delete_volumes: bool = True,
|
||||||
|
docker_cleanup: bool = True,
|
||||||
|
) -> dict:
|
||||||
|
"""Delete database by UUID."""
|
||||||
|
params = {
|
||||||
|
"delete_configurations": str(delete_configurations).lower(),
|
||||||
|
"delete_volumes": str(delete_volumes).lower(),
|
||||||
|
"docker_cleanup": str(docker_cleanup).lower(),
|
||||||
|
}
|
||||||
|
return await self.request("DELETE", f"databases/{uuid}", params=params)
|
||||||
|
|
||||||
|
async def start_database(self, uuid: str) -> dict:
|
||||||
|
"""Start database."""
|
||||||
|
return await self.request("GET", f"databases/{uuid}/start")
|
||||||
|
|
||||||
|
async def stop_database(self, uuid: str) -> dict:
|
||||||
|
"""Stop database."""
|
||||||
|
return await self.request("GET", f"databases/{uuid}/stop")
|
||||||
|
|
||||||
|
async def restart_database(self, uuid: str) -> dict:
|
||||||
|
"""Restart database."""
|
||||||
|
return await self.request("GET", f"databases/{uuid}/restart")
|
||||||
|
|
||||||
|
async def create_database(self, db_type: str, data: dict) -> dict:
|
||||||
|
"""Create a database of given type (postgresql, mysql, mariadb, mongodb, redis, clickhouse)."""
|
||||||
|
return await self.request("POST", f"databases/{db_type}", json_data=data)
|
||||||
|
|
||||||
|
async def get_database_backups(self, uuid: str) -> dict:
|
||||||
|
"""Get database backups."""
|
||||||
|
return await self.request("GET", f"databases/{uuid}/backups")
|
||||||
|
|
||||||
|
async def create_database_backup(self, uuid: str) -> dict:
|
||||||
|
"""Create a manual database backup."""
|
||||||
|
return await self.request("POST", f"databases/{uuid}/backups")
|
||||||
|
|
||||||
|
async def list_backup_executions(self) -> list[dict]:
|
||||||
|
"""List all backup executions."""
|
||||||
|
return await self.request("GET", "databases/backup-executions")
|
||||||
|
|
||||||
|
# --- Services ---
|
||||||
|
|
||||||
|
async def list_services(self) -> list[dict]:
|
||||||
|
"""List all services."""
|
||||||
|
return await self.request("GET", "services")
|
||||||
|
|
||||||
|
async def get_service(self, uuid: str) -> dict:
|
||||||
|
"""Get service by UUID."""
|
||||||
|
return await self.request("GET", f"services/{uuid}")
|
||||||
|
|
||||||
|
async def create_service(self, data: dict) -> dict:
|
||||||
|
"""Create a service from template."""
|
||||||
|
return await self.request("POST", "services", json_data=data)
|
||||||
|
|
||||||
|
async def update_service(self, uuid: str, data: dict) -> dict:
|
||||||
|
"""Update service by UUID."""
|
||||||
|
return await self.request("PATCH", f"services/{uuid}", json_data=data)
|
||||||
|
|
||||||
|
async def delete_service(
|
||||||
|
self,
|
||||||
|
uuid: str,
|
||||||
|
delete_configurations: bool = True,
|
||||||
|
delete_volumes: bool = True,
|
||||||
|
docker_cleanup: bool = True,
|
||||||
|
) -> dict:
|
||||||
|
"""Delete service by UUID."""
|
||||||
|
params = {
|
||||||
|
"delete_configurations": str(delete_configurations).lower(),
|
||||||
|
"delete_volumes": str(delete_volumes).lower(),
|
||||||
|
"docker_cleanup": str(docker_cleanup).lower(),
|
||||||
|
}
|
||||||
|
return await self.request("DELETE", f"services/{uuid}", params=params)
|
||||||
|
|
||||||
|
async def start_service(self, uuid: str) -> dict:
|
||||||
|
"""Start service."""
|
||||||
|
return await self.request("GET", f"services/{uuid}/start")
|
||||||
|
|
||||||
|
async def stop_service(self, uuid: str) -> dict:
|
||||||
|
"""Stop service."""
|
||||||
|
return await self.request("GET", f"services/{uuid}/stop")
|
||||||
|
|
||||||
|
async def restart_service(self, uuid: str) -> dict:
|
||||||
|
"""Restart service."""
|
||||||
|
return await self.request("GET", f"services/{uuid}/restart")
|
||||||
|
|
||||||
|
# --- Service Environment Variables ---
|
||||||
|
|
||||||
|
async def list_service_envs(self, uuid: str) -> list[dict]:
|
||||||
|
"""List service environment variables."""
|
||||||
|
return await self.request("GET", f"services/{uuid}/envs")
|
||||||
|
|
||||||
|
async def create_service_env(self, uuid: str, data: dict) -> dict:
|
||||||
|
"""Create service environment variable."""
|
||||||
|
return await self.request("POST", f"services/{uuid}/envs", json_data=data)
|
||||||
|
|
||||||
|
async def update_service_env(self, uuid: str, data: dict) -> dict:
|
||||||
|
"""Update service environment variable."""
|
||||||
|
return await self.request("PATCH", f"services/{uuid}/envs", json_data=data)
|
||||||
|
|
||||||
|
async def update_service_envs_bulk(self, uuid: str, data: list[dict]) -> dict:
|
||||||
|
"""Bulk update service environment variables."""
|
||||||
|
return await self.request("PATCH", f"services/{uuid}/envs/bulk", json_data={"data": data})
|
||||||
|
|
||||||
|
async def delete_service_env(self, uuid: str, env_uuid: str) -> dict:
|
||||||
|
"""Delete service environment variable."""
|
||||||
|
return await self.request("DELETE", f"services/{uuid}/envs/{env_uuid}")
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ Coolify Plugin Handlers
|
|||||||
All tool handlers for Coolify operations.
|
All tool handlers for Coolify operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import applications, deployments, servers
|
from . import applications, databases, deployments, projects, servers, services
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"applications",
|
"applications",
|
||||||
|
"databases",
|
||||||
"deployments",
|
"deployments",
|
||||||
|
"projects",
|
||||||
"servers",
|
"servers",
|
||||||
|
"services",
|
||||||
]
|
]
|
||||||
|
|||||||
405
plugins/coolify/handlers/databases.py
Normal file
405
plugins/coolify/handlers/databases.py
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
"""Database Handler — manages Coolify databases, lifecycle, and backups."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from plugins.coolify.client import CoolifyClient
|
||||||
|
|
||||||
|
|
||||||
|
def _create_db_spec(db_type: str, description: str) -> dict[str, Any]:
|
||||||
|
"""Generate a create_* tool spec for a database type."""
|
||||||
|
return {
|
||||||
|
"name": f"create_{db_type}",
|
||||||
|
"method_name": f"create_{db_type}",
|
||||||
|
"description": description,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"server_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"environment_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment name (e.g., 'production')",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Database name",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Database description",
|
||||||
|
},
|
||||||
|
"instant_deploy": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Deploy immediately after creation",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["project_uuid", "server_uuid", "environment_name"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
|
"""Return tool specifications for ToolGenerator."""
|
||||||
|
specs = [
|
||||||
|
{
|
||||||
|
"name": "list_databases",
|
||||||
|
"method_name": "list_databases",
|
||||||
|
"description": "List all Coolify databases.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_database",
|
||||||
|
"method_name": "get_database",
|
||||||
|
"description": "Get details of a specific Coolify database by UUID.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_database",
|
||||||
|
"method_name": "update_database",
|
||||||
|
"description": "Update a Coolify database settings.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Database name",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Database description",
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Docker image",
|
||||||
|
},
|
||||||
|
"is_public": {
|
||||||
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||||
|
"description": "Make database publicly accessible",
|
||||||
|
},
|
||||||
|
"public_port": {
|
||||||
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||||
|
"description": "Public port number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete_database",
|
||||||
|
"method_name": "delete_database",
|
||||||
|
"description": (
|
||||||
|
"Delete a Coolify database permanently. "
|
||||||
|
"Optionally clean up volumes and Docker resources."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"delete_configurations": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Delete configurations",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
"delete_volumes": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Delete volumes",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
"docker_cleanup": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Run Docker cleanup",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "admin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "start_database",
|
||||||
|
"method_name": "start_database",
|
||||||
|
"description": "Start a Coolify database.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stop_database",
|
||||||
|
"method_name": "stop_database",
|
||||||
|
"description": "Stop a running Coolify database.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "restart_database",
|
||||||
|
"method_name": "restart_database",
|
||||||
|
"description": "Restart a Coolify database.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add create specs for each database type
|
||||||
|
db_types = [
|
||||||
|
("postgresql", "Create a PostgreSQL database on Coolify."),
|
||||||
|
("mysql", "Create a MySQL database on Coolify."),
|
||||||
|
("mariadb", "Create a MariaDB database on Coolify."),
|
||||||
|
("mongodb", "Create a MongoDB database on Coolify."),
|
||||||
|
("redis", "Create a Redis database on Coolify."),
|
||||||
|
("clickhouse", "Create a ClickHouse database on Coolify."),
|
||||||
|
]
|
||||||
|
for db_type, desc in db_types:
|
||||||
|
specs.append(_create_db_spec(db_type, desc))
|
||||||
|
|
||||||
|
# Backup tools
|
||||||
|
specs.extend(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "get_database_backups",
|
||||||
|
"method_name": "get_database_backups",
|
||||||
|
"description": "Get backup configuration and history for a Coolify database.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_database_backup",
|
||||||
|
"method_name": "create_database_backup",
|
||||||
|
"description": "Create a manual backup of a Coolify database.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "list_backup_executions",
|
||||||
|
"method_name": "list_backup_executions",
|
||||||
|
"description": "List all backup executions across all databases.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return specs
|
||||||
|
|
||||||
|
|
||||||
|
# --- Handler Functions ---
|
||||||
|
|
||||||
|
|
||||||
|
async def list_databases(client: CoolifyClient) -> str:
|
||||||
|
"""List all databases."""
|
||||||
|
databases = await client.list_databases()
|
||||||
|
result = {"success": True, "count": len(databases), "databases": databases}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_database(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Get database details."""
|
||||||
|
db = await client.get_database(uuid)
|
||||||
|
result = {"success": True, "database": db}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_database(client: CoolifyClient, uuid: str, **kwargs) -> str:
|
||||||
|
"""Update database settings."""
|
||||||
|
data = {k: v for k, v in kwargs.items() if v is not None and k != "uuid"}
|
||||||
|
db = await client.update_database(uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Database '{uuid}' updated successfully",
|
||||||
|
"database": db,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_database(
|
||||||
|
client: CoolifyClient,
|
||||||
|
uuid: str,
|
||||||
|
delete_configurations: bool = True,
|
||||||
|
delete_volumes: bool = True,
|
||||||
|
docker_cleanup: bool = True,
|
||||||
|
) -> str:
|
||||||
|
"""Delete a database."""
|
||||||
|
result_data = await client.delete_database(
|
||||||
|
uuid,
|
||||||
|
delete_configurations=delete_configurations,
|
||||||
|
delete_volumes=delete_volumes,
|
||||||
|
docker_cleanup=docker_cleanup,
|
||||||
|
)
|
||||||
|
result = {"success": True, "message": f"Database '{uuid}' deleted", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def start_database(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Start a database."""
|
||||||
|
result_data = await client.start_database(uuid)
|
||||||
|
result = {"success": True, "message": f"Database '{uuid}' starting", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def stop_database(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Stop a database."""
|
||||||
|
result_data = await client.stop_database(uuid)
|
||||||
|
result = {"success": True, "message": f"Database '{uuid}' stopping", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def restart_database(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Restart a database."""
|
||||||
|
result_data = await client.restart_database(uuid)
|
||||||
|
result = {"success": True, "message": f"Database '{uuid}' restarting", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_database(client: CoolifyClient, db_type: str, **kwargs) -> str:
|
||||||
|
"""Create a database of given type."""
|
||||||
|
data = {k: v for k, v in kwargs.items() if v is not None}
|
||||||
|
db = await client.create_database(db_type, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"{db_type.title()} database created successfully",
|
||||||
|
"database": db,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_postgresql(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create a PostgreSQL database."""
|
||||||
|
return await _create_database(client, "postgresql", **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_mysql(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create a MySQL database."""
|
||||||
|
return await _create_database(client, "mysql", **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_mariadb(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create a MariaDB database."""
|
||||||
|
return await _create_database(client, "mariadb", **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_mongodb(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create a MongoDB database."""
|
||||||
|
return await _create_database(client, "mongodb", **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_redis(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create a Redis database."""
|
||||||
|
return await _create_database(client, "redis", **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_clickhouse(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create a ClickHouse database."""
|
||||||
|
return await _create_database(client, "clickhouse", **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_database_backups(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Get database backups."""
|
||||||
|
backups = await client.get_database_backups(uuid)
|
||||||
|
result = {"success": True, "backups": backups}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_database_backup(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Create a manual database backup."""
|
||||||
|
result_data = await client.create_database_backup(uuid)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Backup created for database '{uuid}'",
|
||||||
|
"data": result_data,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_backup_executions(client: CoolifyClient) -> str:
|
||||||
|
"""List all backup executions."""
|
||||||
|
executions = await client.list_backup_executions()
|
||||||
|
result = {"success": True, "count": len(executions), "executions": executions}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
265
plugins/coolify/handlers/projects.py
Normal file
265
plugins/coolify/handlers/projects.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
"""Project & Environment Handler — manages Coolify projects and environments."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from plugins.coolify.client import CoolifyClient
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
|
"""Return tool specifications for ToolGenerator."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "list_projects",
|
||||||
|
"method_name": "list_projects",
|
||||||
|
"description": "List all Coolify projects.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_project",
|
||||||
|
"method_name": "get_project",
|
||||||
|
"description": "Get details of a specific Coolify project by UUID.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_project",
|
||||||
|
"method_name": "create_project",
|
||||||
|
"description": (
|
||||||
|
"Create a new Coolify project. "
|
||||||
|
"Projects group applications, databases, and services."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project name",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Project description",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["name"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_project",
|
||||||
|
"method_name": "update_project",
|
||||||
|
"description": "Update a Coolify project name or description.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "New project name",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "New project description",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete_project",
|
||||||
|
"method_name": "delete_project",
|
||||||
|
"description": (
|
||||||
|
"Delete a Coolify project permanently. "
|
||||||
|
"All resources in the project will be removed."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "admin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "list_environments",
|
||||||
|
"method_name": "list_environments",
|
||||||
|
"description": "List all environments in a Coolify project.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["project_uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_environment",
|
||||||
|
"method_name": "get_environment",
|
||||||
|
"description": ("Get details of a specific environment in a Coolify project by name."),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"environment_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment name (e.g., 'production')",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["project_uuid", "environment_name"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_environment",
|
||||||
|
"method_name": "create_environment",
|
||||||
|
"description": "Create a new environment in a Coolify project.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment name",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Environment description",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["project_uuid", "name"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Handler Functions ---
|
||||||
|
|
||||||
|
|
||||||
|
async def list_projects(client: CoolifyClient) -> str:
|
||||||
|
"""List all projects."""
|
||||||
|
projects = await client.list_projects()
|
||||||
|
result = {"success": True, "count": len(projects), "projects": projects}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_project(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Get project details."""
|
||||||
|
project = await client.get_project(uuid)
|
||||||
|
result = {"success": True, "project": project}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_project(client: CoolifyClient, name: str, description: str | None = None) -> str:
|
||||||
|
"""Create a new project."""
|
||||||
|
data = {"name": name}
|
||||||
|
if description is not None:
|
||||||
|
data["description"] = description
|
||||||
|
project = await client.create_project(data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": "Project created successfully",
|
||||||
|
"project": project,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_project(
|
||||||
|
client: CoolifyClient,
|
||||||
|
uuid: str,
|
||||||
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Update project settings."""
|
||||||
|
data = {}
|
||||||
|
if name is not None:
|
||||||
|
data["name"] = name
|
||||||
|
if description is not None:
|
||||||
|
data["description"] = description
|
||||||
|
project = await client.update_project(uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Project '{uuid}' updated successfully",
|
||||||
|
"project": project,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_project(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Delete a project."""
|
||||||
|
result_data = await client.delete_project(uuid)
|
||||||
|
result = {"success": True, "message": f"Project '{uuid}' deleted", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_environments(client: CoolifyClient, project_uuid: str) -> str:
|
||||||
|
"""List environments in a project."""
|
||||||
|
envs = await client.list_environments(project_uuid)
|
||||||
|
result = {"success": True, "count": len(envs), "environments": envs}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_environment(client: CoolifyClient, project_uuid: str, environment_name: str) -> str:
|
||||||
|
"""Get environment details."""
|
||||||
|
env = await client.get_environment(project_uuid, environment_name)
|
||||||
|
result = {"success": True, "environment": env}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_environment(
|
||||||
|
client: CoolifyClient,
|
||||||
|
project_uuid: str,
|
||||||
|
name: str,
|
||||||
|
description: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Create a new environment in a project."""
|
||||||
|
data = {"name": name}
|
||||||
|
if description is not None:
|
||||||
|
data["description"] = description
|
||||||
|
env = await client.create_environment(project_uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Environment '{name}' created",
|
||||||
|
"environment": env,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
483
plugins/coolify/handlers/services.py
Normal file
483
plugins/coolify/handlers/services.py
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
"""Service Handler — manages Coolify services (Docker Compose), lifecycle, and env vars."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from plugins.coolify.client import CoolifyClient
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
|
"""Return tool specifications for ToolGenerator."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "list_services",
|
||||||
|
"method_name": "list_services",
|
||||||
|
"description": "List all Coolify services.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_service",
|
||||||
|
"method_name": "get_service",
|
||||||
|
"description": "Get details of a specific Coolify service by UUID.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_service",
|
||||||
|
"method_name": "create_service",
|
||||||
|
"description": (
|
||||||
|
"Create a Coolify service from a predefined template. "
|
||||||
|
"Requires project_uuid, server_uuid, environment, and service type."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"server_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"environment_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment name (e.g., 'production')",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"Service type from Coolify templates "
|
||||||
|
"(e.g., 'plausible-analytics', 'minio', 'grafana')"
|
||||||
|
),
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Service name",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Service description",
|
||||||
|
},
|
||||||
|
"instant_deploy": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Deploy immediately after creation",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"project_uuid",
|
||||||
|
"server_uuid",
|
||||||
|
"environment_name",
|
||||||
|
"type",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_service",
|
||||||
|
"method_name": "update_service",
|
||||||
|
"description": "Update a Coolify service settings.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Service name",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Service description",
|
||||||
|
},
|
||||||
|
"docker_compose_raw": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Raw Docker Compose YAML content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete_service",
|
||||||
|
"method_name": "delete_service",
|
||||||
|
"description": (
|
||||||
|
"Delete a Coolify service permanently. "
|
||||||
|
"Optionally clean up volumes and Docker resources."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"delete_configurations": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Delete configurations",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
"delete_volumes": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Delete volumes",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
"docker_cleanup": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Run Docker cleanup",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "admin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "start_service",
|
||||||
|
"method_name": "start_service",
|
||||||
|
"description": "Start a Coolify service.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stop_service",
|
||||||
|
"method_name": "stop_service",
|
||||||
|
"description": "Stop a running Coolify service.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "restart_service",
|
||||||
|
"method_name": "restart_service",
|
||||||
|
"description": "Restart a Coolify service.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "list_service_envs",
|
||||||
|
"method_name": "list_service_envs",
|
||||||
|
"description": "List environment variables for a Coolify service.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_service_env",
|
||||||
|
"method_name": "create_service_env",
|
||||||
|
"description": "Create an environment variable for a Coolify service.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment variable key",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment variable value",
|
||||||
|
},
|
||||||
|
"is_preview": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Preview deployment only",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid", "key", "value"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_service_env",
|
||||||
|
"method_name": "update_service_env",
|
||||||
|
"description": "Update an environment variable for a Coolify service.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment variable key",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "New value",
|
||||||
|
},
|
||||||
|
"is_preview": {
|
||||||
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||||
|
"description": "Preview deployment only",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid", "key", "value"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_service_envs_bulk",
|
||||||
|
"method_name": "update_service_envs_bulk",
|
||||||
|
"description": "Bulk update environment variables for a Coolify service.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of env var objects with key and value",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"key": {"type": "string"},
|
||||||
|
"value": {"type": "string"},
|
||||||
|
"is_preview": {"type": "boolean"},
|
||||||
|
},
|
||||||
|
"required": ["key", "value"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid", "data"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete_service_env",
|
||||||
|
"method_name": "delete_service_env",
|
||||||
|
"description": "Delete an environment variable from a Coolify service.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"env_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment variable UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid", "env_uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Handler Functions ---
|
||||||
|
|
||||||
|
|
||||||
|
async def list_services(client: CoolifyClient) -> str:
|
||||||
|
"""List all services."""
|
||||||
|
services = await client.list_services()
|
||||||
|
result = {"success": True, "count": len(services), "services": services}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_service(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Get service details."""
|
||||||
|
service = await client.get_service(uuid)
|
||||||
|
result = {"success": True, "service": service}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_service(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create a service from template."""
|
||||||
|
data = {k: v for k, v in kwargs.items() if v is not None}
|
||||||
|
service = await client.create_service(data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": "Service created successfully",
|
||||||
|
"service": service,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_service(client: CoolifyClient, uuid: str, **kwargs) -> str:
|
||||||
|
"""Update service settings."""
|
||||||
|
data = {k: v for k, v in kwargs.items() if v is not None and k != "uuid"}
|
||||||
|
service = await client.update_service(uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Service '{uuid}' updated successfully",
|
||||||
|
"service": service,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_service(
|
||||||
|
client: CoolifyClient,
|
||||||
|
uuid: str,
|
||||||
|
delete_configurations: bool = True,
|
||||||
|
delete_volumes: bool = True,
|
||||||
|
docker_cleanup: bool = True,
|
||||||
|
) -> str:
|
||||||
|
"""Delete a service."""
|
||||||
|
result_data = await client.delete_service(
|
||||||
|
uuid,
|
||||||
|
delete_configurations=delete_configurations,
|
||||||
|
delete_volumes=delete_volumes,
|
||||||
|
docker_cleanup=docker_cleanup,
|
||||||
|
)
|
||||||
|
result = {"success": True, "message": f"Service '{uuid}' deleted", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def start_service(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Start a service."""
|
||||||
|
result_data = await client.start_service(uuid)
|
||||||
|
result = {"success": True, "message": f"Service '{uuid}' starting", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def stop_service(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Stop a service."""
|
||||||
|
result_data = await client.stop_service(uuid)
|
||||||
|
result = {"success": True, "message": f"Service '{uuid}' stopping", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def restart_service(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Restart a service."""
|
||||||
|
result_data = await client.restart_service(uuid)
|
||||||
|
result = {"success": True, "message": f"Service '{uuid}' restarting", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_service_envs(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""List service environment variables."""
|
||||||
|
envs = await client.list_service_envs(uuid)
|
||||||
|
result = {"success": True, "count": len(envs), "envs": envs}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_service_env(
|
||||||
|
client: CoolifyClient,
|
||||||
|
uuid: str,
|
||||||
|
key: str,
|
||||||
|
value: str,
|
||||||
|
is_preview: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""Create service environment variable."""
|
||||||
|
data = {"key": key, "value": value, "is_preview": is_preview}
|
||||||
|
result_data = await client.create_service_env(uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Env var '{key}' created",
|
||||||
|
"data": result_data,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_service_env(
|
||||||
|
client: CoolifyClient,
|
||||||
|
uuid: str,
|
||||||
|
key: str,
|
||||||
|
value: str,
|
||||||
|
is_preview: bool | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Update service environment variable."""
|
||||||
|
data = {"key": key, "value": value}
|
||||||
|
if is_preview is not None:
|
||||||
|
data["is_preview"] = is_preview
|
||||||
|
result_data = await client.update_service_env(uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Env var '{key}' updated",
|
||||||
|
"data": result_data,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_service_envs_bulk(client: CoolifyClient, uuid: str, data: list[dict]) -> str:
|
||||||
|
"""Bulk update service environment variables."""
|
||||||
|
result_data = await client.update_service_envs_bulk(uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Bulk update {len(data)} env vars",
|
||||||
|
"data": result_data,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_service_env(client: CoolifyClient, uuid: str, env_uuid: str) -> str:
|
||||||
|
"""Delete service environment variable."""
|
||||||
|
await client.delete_service_env(uuid, env_uuid)
|
||||||
|
result = {"success": True, "message": f"Env var '{env_uuid}' deleted"}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
@@ -18,8 +18,11 @@ class CoolifyPlugin(BasePlugin):
|
|||||||
|
|
||||||
Provides Coolify deployment management capabilities including:
|
Provides Coolify deployment management capabilities including:
|
||||||
- Application management (CRUD, lifecycle, env vars, logs)
|
- Application management (CRUD, lifecycle, env vars, logs)
|
||||||
|
- Database management (PostgreSQL, MySQL, MariaDB, MongoDB, Redis, ClickHouse, backups)
|
||||||
- Deployment control (list, cancel, deploy by tag/UUID)
|
- Deployment control (list, cancel, deploy by tag/UUID)
|
||||||
|
- Project & environment management (CRUD)
|
||||||
- Server management (CRUD, resources, domains, validation)
|
- Server management (CRUD, resources, domains, validation)
|
||||||
|
- Service management (CRUD, lifecycle, env vars)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -60,8 +63,11 @@ class CoolifyPlugin(BasePlugin):
|
|||||||
specs = []
|
specs = []
|
||||||
|
|
||||||
specs.extend(handlers.applications.get_tool_specifications())
|
specs.extend(handlers.applications.get_tool_specifications())
|
||||||
|
specs.extend(handlers.databases.get_tool_specifications())
|
||||||
specs.extend(handlers.deployments.get_tool_specifications())
|
specs.extend(handlers.deployments.get_tool_specifications())
|
||||||
|
specs.extend(handlers.projects.get_tool_specifications())
|
||||||
specs.extend(handlers.servers.get_tool_specifications())
|
specs.extend(handlers.servers.get_tool_specifications())
|
||||||
|
specs.extend(handlers.services.get_tool_specifications())
|
||||||
|
|
||||||
return specs
|
return specs
|
||||||
|
|
||||||
@@ -77,8 +83,11 @@ class CoolifyPlugin(BasePlugin):
|
|||||||
"""
|
"""
|
||||||
handler_modules = [
|
handler_modules = [
|
||||||
handlers.applications,
|
handlers.applications,
|
||||||
|
handlers.databases,
|
||||||
handlers.deployments,
|
handlers.deployments,
|
||||||
|
handlers.projects,
|
||||||
handlers.servers,
|
handlers.servers,
|
||||||
|
handlers.services,
|
||||||
]
|
]
|
||||||
|
|
||||||
for module in handler_modules:
|
for module in handler_modules:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mcphub-server"
|
name = "mcphub-server"
|
||||||
version = "3.7.0"
|
version = "3.8.0"
|
||||||
description = "AI-native management hub for WordPress, WooCommerce, and self-hosted services via Model Context Protocol (MCP)"
|
description = "AI-native management hub for WordPress, WooCommerce, and self-hosted services via Model Context Protocol (MCP)"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "MCP Hub", email = "contact@mcphub.dev"}
|
{name = "MCP Hub", email = "contact@mcphub.dev"}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class TestCoolifyPlugin:
|
|||||||
def test_tool_specifications(self):
|
def test_tool_specifications(self):
|
||||||
"""Test that tool specifications are returned."""
|
"""Test that tool specifications are returned."""
|
||||||
specs = CoolifyPlugin.get_tool_specifications()
|
specs = CoolifyPlugin.get_tool_specifications()
|
||||||
assert len(specs) == 30 # 17 apps + 5 deployments + 8 servers
|
assert len(specs) == 67 # 17 apps + 16 dbs + 5 deploys + 8 projects + 8 servers + 13 svcs
|
||||||
|
|
||||||
# Check all specs have required fields
|
# Check all specs have required fields
|
||||||
for spec in specs:
|
for spec in specs:
|
||||||
|
|||||||
237
tests/test_coolify_databases.py
Normal file
237
tests/test_coolify_databases.py
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
"""
|
||||||
|
Tests for Coolify Databases Handler
|
||||||
|
|
||||||
|
Unit tests with mocked HTTP responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from plugins.coolify.client import CoolifyClient
|
||||||
|
from plugins.coolify.handlers import databases
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
"""Create a CoolifyClient instance for testing."""
|
||||||
|
return CoolifyClient(site_url="https://coolify.test.com", token="test-token-123")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDatabaseToolSpecs:
|
||||||
|
"""Tests for database tool specifications."""
|
||||||
|
|
||||||
|
def test_database_tool_count(self):
|
||||||
|
"""Test database tool specification count."""
|
||||||
|
specs = databases.get_tool_specifications()
|
||||||
|
assert len(specs) == 16
|
||||||
|
|
||||||
|
def test_tool_specs_have_required_fields(self):
|
||||||
|
"""Test all specs have required fields."""
|
||||||
|
for spec in databases.get_tool_specifications():
|
||||||
|
assert "name" in spec
|
||||||
|
assert "method_name" in spec
|
||||||
|
assert "description" in spec
|
||||||
|
assert "schema" in spec
|
||||||
|
assert "scope" in spec
|
||||||
|
assert spec["scope"] in ("read", "write", "admin")
|
||||||
|
|
||||||
|
def test_tool_names_unique(self):
|
||||||
|
"""Test that all tool names are unique."""
|
||||||
|
specs = databases.get_tool_specifications()
|
||||||
|
names = [s["name"] for s in specs]
|
||||||
|
assert len(names) == len(set(names))
|
||||||
|
|
||||||
|
def test_all_db_types_present(self):
|
||||||
|
"""Test all 6 database creation tools exist."""
|
||||||
|
specs = databases.get_tool_specifications()
|
||||||
|
names = [s["name"] for s in specs]
|
||||||
|
for db_type in ["postgresql", "mysql", "mariadb", "mongodb", "redis", "clickhouse"]:
|
||||||
|
assert f"create_{db_type}" in names
|
||||||
|
|
||||||
|
def test_scope_distribution(self):
|
||||||
|
"""Test correct scope assignments."""
|
||||||
|
specs = databases.get_tool_specifications()
|
||||||
|
by_scope = {}
|
||||||
|
for s in specs:
|
||||||
|
by_scope.setdefault(s["scope"], []).append(s["name"])
|
||||||
|
assert "list_databases" in by_scope["read"]
|
||||||
|
assert "delete_database" in by_scope["admin"]
|
||||||
|
assert "start_database" in by_scope["write"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestDatabaseHandlers:
|
||||||
|
"""Tests for database handler functions."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_databases(self, client):
|
||||||
|
"""Test list_databases handler."""
|
||||||
|
mock_dbs = [
|
||||||
|
{"uuid": "db-1", "name": "postgres-main", "type": "postgresql", "status": "running"},
|
||||||
|
{"uuid": "db-2", "name": "redis-cache", "type": "redis", "status": "running"},
|
||||||
|
]
|
||||||
|
client.list_databases = AsyncMock(return_value=mock_dbs)
|
||||||
|
|
||||||
|
result = await databases.list_databases(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 2
|
||||||
|
client.list_databases.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_database(self, client):
|
||||||
|
"""Test get_database handler."""
|
||||||
|
mock_db = {"uuid": "db-1", "name": "postgres-main", "type": "postgresql"}
|
||||||
|
client.get_database = AsyncMock(return_value=mock_db)
|
||||||
|
|
||||||
|
result = await databases.get_database(client, uuid="db-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["database"]["name"] == "postgres-main"
|
||||||
|
client.get_database.assert_called_once_with("db-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_database(self, client):
|
||||||
|
"""Test update_database handler."""
|
||||||
|
mock_response = {"uuid": "db-1"}
|
||||||
|
client.update_database = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await databases.update_database(client, uuid="db-1", name="renamed-db")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "updated" in data["message"].lower()
|
||||||
|
client.update_database.assert_called_once_with("db-1", {"name": "renamed-db"})
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_database(self, client):
|
||||||
|
"""Test delete_database handler."""
|
||||||
|
mock_response = {"message": "Database deleted."}
|
||||||
|
client.delete_database = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await databases.delete_database(client, uuid="db-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "deleted" in data["message"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_database(self, client):
|
||||||
|
"""Test start_database handler."""
|
||||||
|
mock_response = {"message": "Starting."}
|
||||||
|
client.start_database = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await databases.start_database(client, uuid="db-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.start_database.assert_called_once_with("db-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_stop_database(self, client):
|
||||||
|
"""Test stop_database handler."""
|
||||||
|
mock_response = {"message": "Stopping."}
|
||||||
|
client.stop_database = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await databases.stop_database(client, uuid="db-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.stop_database.assert_called_once_with("db-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_restart_database(self, client):
|
||||||
|
"""Test restart_database handler."""
|
||||||
|
mock_response = {"message": "Restarting."}
|
||||||
|
client.restart_database = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await databases.restart_database(client, uuid="db-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.restart_database.assert_called_once_with("db-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_postgresql(self, client):
|
||||||
|
"""Test create_postgresql handler."""
|
||||||
|
mock_response = {"uuid": "db-new", "type": "postgresql"}
|
||||||
|
client.create_database = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await databases.create_postgresql(
|
||||||
|
client,
|
||||||
|
project_uuid="proj-1",
|
||||||
|
server_uuid="srv-1",
|
||||||
|
environment_name="production",
|
||||||
|
)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "postgresql" in data["message"].lower()
|
||||||
|
client.create_database.assert_called_once_with(
|
||||||
|
"postgresql",
|
||||||
|
{
|
||||||
|
"project_uuid": "proj-1",
|
||||||
|
"server_uuid": "srv-1",
|
||||||
|
"environment_name": "production",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_redis(self, client):
|
||||||
|
"""Test create_redis handler."""
|
||||||
|
mock_response = {"uuid": "db-new", "type": "redis"}
|
||||||
|
client.create_database = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await databases.create_redis(
|
||||||
|
client,
|
||||||
|
project_uuid="proj-1",
|
||||||
|
server_uuid="srv-1",
|
||||||
|
environment_name="production",
|
||||||
|
name="my-cache",
|
||||||
|
)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "redis" in data["message"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_database_backups(self, client):
|
||||||
|
"""Test get_database_backups handler."""
|
||||||
|
mock_backups = {"enabled": True, "frequency": "0 0 * * *", "backups": []}
|
||||||
|
client.get_database_backups = AsyncMock(return_value=mock_backups)
|
||||||
|
|
||||||
|
result = await databases.get_database_backups(client, uuid="db-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.get_database_backups.assert_called_once_with("db-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_database_backup(self, client):
|
||||||
|
"""Test create_database_backup handler."""
|
||||||
|
mock_response = {"message": "Backup started."}
|
||||||
|
client.create_database_backup = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await databases.create_database_backup(client, uuid="db-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "backup" in data["message"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_backup_executions(self, client):
|
||||||
|
"""Test list_backup_executions handler."""
|
||||||
|
mock_executions = [
|
||||||
|
{"id": 1, "database_uuid": "db-1", "status": "success"},
|
||||||
|
{"id": 2, "database_uuid": "db-1", "status": "failed"},
|
||||||
|
]
|
||||||
|
client.list_backup_executions = AsyncMock(return_value=mock_executions)
|
||||||
|
|
||||||
|
result = await databases.list_backup_executions(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 2
|
||||||
214
tests/test_coolify_projects.py
Normal file
214
tests/test_coolify_projects.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
"""
|
||||||
|
Tests for Coolify Projects & Environments Handler
|
||||||
|
|
||||||
|
Unit tests with mocked HTTP responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from plugins.coolify.client import CoolifyClient
|
||||||
|
from plugins.coolify.handlers import projects
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
"""Create a CoolifyClient instance for testing."""
|
||||||
|
return CoolifyClient(site_url="https://coolify.test.com", token="test-token-123")
|
||||||
|
|
||||||
|
|
||||||
|
class TestProjectToolSpecs:
|
||||||
|
"""Tests for project tool specifications."""
|
||||||
|
|
||||||
|
def test_project_tool_count(self):
|
||||||
|
"""Test project tool specification count."""
|
||||||
|
specs = projects.get_tool_specifications()
|
||||||
|
assert len(specs) == 8
|
||||||
|
|
||||||
|
def test_tool_specs_have_required_fields(self):
|
||||||
|
"""Test all specs have required fields."""
|
||||||
|
for spec in projects.get_tool_specifications():
|
||||||
|
assert "name" in spec
|
||||||
|
assert "method_name" in spec
|
||||||
|
assert "description" in spec
|
||||||
|
assert "schema" in spec
|
||||||
|
assert "scope" in spec
|
||||||
|
assert spec["scope"] in ("read", "write", "admin")
|
||||||
|
|
||||||
|
def test_tool_names_unique(self):
|
||||||
|
"""Test that all tool names are unique."""
|
||||||
|
specs = projects.get_tool_specifications()
|
||||||
|
names = [s["name"] for s in specs]
|
||||||
|
assert len(names) == len(set(names))
|
||||||
|
|
||||||
|
def test_scope_distribution(self):
|
||||||
|
"""Test correct scope assignments."""
|
||||||
|
specs = projects.get_tool_specifications()
|
||||||
|
by_scope = {}
|
||||||
|
for s in specs:
|
||||||
|
by_scope.setdefault(s["scope"], []).append(s["name"])
|
||||||
|
assert "list_projects" in by_scope["read"]
|
||||||
|
assert "get_project" in by_scope["read"]
|
||||||
|
assert "create_project" in by_scope["write"]
|
||||||
|
assert "delete_project" in by_scope["admin"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestProjectHandlers:
|
||||||
|
"""Tests for project handler functions."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_projects(self, client):
|
||||||
|
"""Test list_projects handler."""
|
||||||
|
mock_projects = [
|
||||||
|
{"uuid": "proj-1", "name": "mcphub", "description": "MCP Hub project"},
|
||||||
|
{"uuid": "proj-2", "name": "blog", "description": "Blog project"},
|
||||||
|
]
|
||||||
|
client.list_projects = AsyncMock(return_value=mock_projects)
|
||||||
|
|
||||||
|
result = await projects.list_projects(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 2
|
||||||
|
assert len(data["projects"]) == 2
|
||||||
|
client.list_projects.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_project(self, client):
|
||||||
|
"""Test get_project handler."""
|
||||||
|
mock_project = {
|
||||||
|
"uuid": "proj-1",
|
||||||
|
"name": "mcphub",
|
||||||
|
"description": "MCP Hub project",
|
||||||
|
"environments": [{"name": "production"}],
|
||||||
|
}
|
||||||
|
client.get_project = AsyncMock(return_value=mock_project)
|
||||||
|
|
||||||
|
result = await projects.get_project(client, uuid="proj-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["project"]["name"] == "mcphub"
|
||||||
|
client.get_project.assert_called_once_with("proj-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_project(self, client):
|
||||||
|
"""Test create_project handler."""
|
||||||
|
mock_response = {"uuid": "proj-new", "name": "new-project"}
|
||||||
|
client.create_project = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await projects.create_project(
|
||||||
|
client, name="new-project", description="A test project"
|
||||||
|
)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "created" in data["message"].lower()
|
||||||
|
client.create_project.assert_called_once_with(
|
||||||
|
{"name": "new-project", "description": "A test project"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_project_no_description(self, client):
|
||||||
|
"""Test create_project without description."""
|
||||||
|
mock_response = {"uuid": "proj-new"}
|
||||||
|
client.create_project = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await projects.create_project(client, name="minimal")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.create_project.assert_called_once_with({"name": "minimal"})
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_project(self, client):
|
||||||
|
"""Test update_project handler."""
|
||||||
|
mock_response = {"uuid": "proj-1"}
|
||||||
|
client.update_project = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await projects.update_project(client, uuid="proj-1", name="renamed")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "updated" in data["message"].lower()
|
||||||
|
client.update_project.assert_called_once_with("proj-1", {"name": "renamed"})
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_project(self, client):
|
||||||
|
"""Test delete_project handler."""
|
||||||
|
mock_response = {"message": "Project deleted."}
|
||||||
|
client.delete_project = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await projects.delete_project(client, uuid="proj-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "deleted" in data["message"].lower()
|
||||||
|
client.delete_project.assert_called_once_with("proj-1")
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnvironmentHandlers:
|
||||||
|
"""Tests for environment handler functions."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_environments(self, client):
|
||||||
|
"""Test list_environments handler."""
|
||||||
|
mock_envs = [
|
||||||
|
{"name": "production", "id": 1},
|
||||||
|
{"name": "staging", "id": 2},
|
||||||
|
]
|
||||||
|
client.list_environments = AsyncMock(return_value=mock_envs)
|
||||||
|
|
||||||
|
result = await projects.list_environments(client, project_uuid="proj-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 2
|
||||||
|
assert len(data["environments"]) == 2
|
||||||
|
client.list_environments.assert_called_once_with("proj-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_environment(self, client):
|
||||||
|
"""Test get_environment handler."""
|
||||||
|
mock_env = {"name": "production", "id": 1, "project_id": 5}
|
||||||
|
client.get_environment = AsyncMock(return_value=mock_env)
|
||||||
|
|
||||||
|
result = await projects.get_environment(
|
||||||
|
client, project_uuid="proj-1", environment_name="production"
|
||||||
|
)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["environment"]["name"] == "production"
|
||||||
|
client.get_environment.assert_called_once_with("proj-1", "production")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_environment(self, client):
|
||||||
|
"""Test create_environment handler."""
|
||||||
|
mock_response = {"name": "staging", "id": 3}
|
||||||
|
client.create_environment = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await projects.create_environment(
|
||||||
|
client, project_uuid="proj-1", name="staging", description="Staging env"
|
||||||
|
)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "staging" in data["message"]
|
||||||
|
client.create_environment.assert_called_once_with(
|
||||||
|
"proj-1", {"name": "staging", "description": "Staging env"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_environment_no_description(self, client):
|
||||||
|
"""Test create_environment without description."""
|
||||||
|
mock_response = {"name": "test"}
|
||||||
|
client.create_environment = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await projects.create_environment(client, project_uuid="proj-1", name="test")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.create_environment.assert_called_once_with("proj-1", {"name": "test"})
|
||||||
240
tests/test_coolify_services.py
Normal file
240
tests/test_coolify_services.py
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
"""
|
||||||
|
Tests for Coolify Services Handler
|
||||||
|
|
||||||
|
Unit tests with mocked HTTP responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from plugins.coolify.client import CoolifyClient
|
||||||
|
from plugins.coolify.handlers import services
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
"""Create a CoolifyClient instance for testing."""
|
||||||
|
return CoolifyClient(site_url="https://coolify.test.com", token="test-token-123")
|
||||||
|
|
||||||
|
|
||||||
|
class TestServiceToolSpecs:
|
||||||
|
"""Tests for service tool specifications."""
|
||||||
|
|
||||||
|
def test_service_tool_count(self):
|
||||||
|
"""Test service tool specification count."""
|
||||||
|
specs = services.get_tool_specifications()
|
||||||
|
assert len(specs) == 13
|
||||||
|
|
||||||
|
def test_tool_specs_have_required_fields(self):
|
||||||
|
"""Test all specs have required fields."""
|
||||||
|
for spec in services.get_tool_specifications():
|
||||||
|
assert "name" in spec
|
||||||
|
assert "method_name" in spec
|
||||||
|
assert "description" in spec
|
||||||
|
assert "schema" in spec
|
||||||
|
assert "scope" in spec
|
||||||
|
assert spec["scope"] in ("read", "write", "admin")
|
||||||
|
|
||||||
|
def test_tool_names_unique(self):
|
||||||
|
"""Test that all tool names are unique."""
|
||||||
|
specs = services.get_tool_specifications()
|
||||||
|
names = [s["name"] for s in specs]
|
||||||
|
assert len(names) == len(set(names))
|
||||||
|
|
||||||
|
def test_scope_distribution(self):
|
||||||
|
"""Test correct scope assignments."""
|
||||||
|
specs = services.get_tool_specifications()
|
||||||
|
by_scope = {}
|
||||||
|
for s in specs:
|
||||||
|
by_scope.setdefault(s["scope"], []).append(s["name"])
|
||||||
|
assert "list_services" in by_scope["read"]
|
||||||
|
assert "list_service_envs" in by_scope["read"]
|
||||||
|
assert "delete_service" in by_scope["admin"]
|
||||||
|
assert "start_service" in by_scope["write"]
|
||||||
|
assert "create_service_env" in by_scope["write"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestServiceHandlers:
|
||||||
|
"""Tests for service handler functions."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_services(self, client):
|
||||||
|
"""Test list_services handler."""
|
||||||
|
mock_services = [
|
||||||
|
{"uuid": "svc-1", "name": "plausible", "status": "running"},
|
||||||
|
{"uuid": "svc-2", "name": "minio", "status": "stopped"},
|
||||||
|
]
|
||||||
|
client.list_services = AsyncMock(return_value=mock_services)
|
||||||
|
|
||||||
|
result = await services.list_services(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 2
|
||||||
|
client.list_services.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_service(self, client):
|
||||||
|
"""Test get_service handler."""
|
||||||
|
mock_service = {"uuid": "svc-1", "name": "plausible", "status": "running"}
|
||||||
|
client.get_service = AsyncMock(return_value=mock_service)
|
||||||
|
|
||||||
|
result = await services.get_service(client, uuid="svc-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["service"]["name"] == "plausible"
|
||||||
|
client.get_service.assert_called_once_with("svc-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_service(self, client):
|
||||||
|
"""Test create_service handler."""
|
||||||
|
mock_response = {"uuid": "svc-new"}
|
||||||
|
client.create_service = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await services.create_service(
|
||||||
|
client,
|
||||||
|
project_uuid="proj-1",
|
||||||
|
server_uuid="srv-1",
|
||||||
|
environment_name="production",
|
||||||
|
type="plausible-analytics",
|
||||||
|
name="my-plausible",
|
||||||
|
)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "created" in data["message"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_service(self, client):
|
||||||
|
"""Test update_service handler."""
|
||||||
|
mock_response = {"uuid": "svc-1"}
|
||||||
|
client.update_service = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await services.update_service(client, uuid="svc-1", name="renamed")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "updated" in data["message"].lower()
|
||||||
|
client.update_service.assert_called_once_with("svc-1", {"name": "renamed"})
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_service(self, client):
|
||||||
|
"""Test delete_service handler."""
|
||||||
|
mock_response = {"message": "Service deleted."}
|
||||||
|
client.delete_service = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await services.delete_service(client, uuid="svc-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "deleted" in data["message"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_service(self, client):
|
||||||
|
"""Test start_service handler."""
|
||||||
|
mock_response = {"message": "Starting."}
|
||||||
|
client.start_service = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await services.start_service(client, uuid="svc-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.start_service.assert_called_once_with("svc-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_stop_service(self, client):
|
||||||
|
"""Test stop_service handler."""
|
||||||
|
mock_response = {"message": "Stopping."}
|
||||||
|
client.stop_service = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await services.stop_service(client, uuid="svc-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.stop_service.assert_called_once_with("svc-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_restart_service(self, client):
|
||||||
|
"""Test restart_service handler."""
|
||||||
|
mock_response = {"message": "Restarting."}
|
||||||
|
client.restart_service = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await services.restart_service(client, uuid="svc-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.restart_service.assert_called_once_with("svc-1")
|
||||||
|
|
||||||
|
|
||||||
|
class TestServiceEnvHandlers:
|
||||||
|
"""Tests for service environment variable handlers."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_service_envs(self, client):
|
||||||
|
"""Test list_service_envs handler."""
|
||||||
|
mock_envs = [
|
||||||
|
{"key": "DATABASE_URL", "value": "postgres://..."},
|
||||||
|
{"key": "SECRET", "value": "***"},
|
||||||
|
]
|
||||||
|
client.list_service_envs = AsyncMock(return_value=mock_envs)
|
||||||
|
|
||||||
|
result = await services.list_service_envs(client, uuid="svc-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_service_env(self, client):
|
||||||
|
"""Test create_service_env handler."""
|
||||||
|
mock_response = {"uuid": "env-123"}
|
||||||
|
client.create_service_env = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await services.create_service_env(
|
||||||
|
client, uuid="svc-1", key="NEW_VAR", value="test"
|
||||||
|
)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "NEW_VAR" in data["message"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_service_env(self, client):
|
||||||
|
"""Test update_service_env handler."""
|
||||||
|
mock_response = {"uuid": "env-123"}
|
||||||
|
client.update_service_env = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await services.update_service_env(
|
||||||
|
client, uuid="svc-1", key="EXISTING_VAR", value="new_val"
|
||||||
|
)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "EXISTING_VAR" in data["message"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_service_envs_bulk(self, client):
|
||||||
|
"""Test update_service_envs_bulk handler."""
|
||||||
|
mock_response = {"message": "Updated."}
|
||||||
|
client.update_service_envs_bulk = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
env_data = [{"key": "A", "value": "1"}, {"key": "B", "value": "2"}]
|
||||||
|
result = await services.update_service_envs_bulk(client, uuid="svc-1", data=env_data)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "2" in data["message"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_service_env(self, client):
|
||||||
|
"""Test delete_service_env handler."""
|
||||||
|
client.delete_service_env = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
result = await services.delete_service_env(client, uuid="svc-1", env_uuid="env-123")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.delete_service_env.assert_called_once_with("svc-1", "env-123")
|
||||||
Reference in New Issue
Block a user