Remove Uptime Kuma integration
- Delete kuma_client.py and all Kuma-related code - Remove /infrastructure/monitors endpoints - Update service start/stop to use Portainer only - Simplify service control widget to show container status - Remove Kuma config and credentials - Delete stale memory tests (moved to core-ai service) - Add test infrastructure with pytest - Add tests for service_groups, config, and health endpoints - Bump version to 1.1.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,6 @@ from pydantic import BaseModel, field_validator
|
||||
from src.controllers.base import BaseController
|
||||
from src.clients.portainer_client import get_portainer_client
|
||||
from src.clients.npm_client import get_npm_client
|
||||
from src.clients.kuma_client import get_kuma_client
|
||||
from src.logging_config import get_logger
|
||||
from src import service_groups
|
||||
from src.auth.oidc import get_admin_user, get_forward_auth_admin
|
||||
@@ -963,7 +962,7 @@ class InfrastructureController(BaseController):
|
||||
"/services/{name}/stop",
|
||||
response_model=OperationResult,
|
||||
summary="Stop a service or service group",
|
||||
description="Stop a service or service group by pausing monitors and stopping containers. Requires admin authentication when accessed externally via api.schweitz.net."
|
||||
description="Stop a service or service group by stopping containers. Requires admin authentication when accessed externally via api.schweitz.net."
|
||||
)
|
||||
async def stop_service(
|
||||
name: str,
|
||||
@@ -974,8 +973,7 @@ class InfrastructureController(BaseController):
|
||||
|
||||
This will:
|
||||
1. Validate service can be stopped (not always-on)
|
||||
2. Pause Uptime Kuma monitors for all services in group
|
||||
3. Stop the Portainer stack(s)
|
||||
2. Stop the Portainer stack containers
|
||||
|
||||
Args:
|
||||
name: Service or group name
|
||||
@@ -984,7 +982,6 @@ class InfrastructureController(BaseController):
|
||||
Operation result with details
|
||||
"""
|
||||
portainer = get_portainer_client()
|
||||
kuma = get_kuma_client()
|
||||
|
||||
try:
|
||||
# Get all services in the group
|
||||
@@ -997,24 +994,13 @@ class InfrastructureController(BaseController):
|
||||
|
||||
results = {
|
||||
"stopped_services": [],
|
||||
"paused_monitors": [],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
# Stop each service
|
||||
for service_name in services:
|
||||
try:
|
||||
# 1. Pause Uptime Kuma monitor
|
||||
try:
|
||||
monitor_paused = await kuma.pause_monitor_by_name(service_name)
|
||||
if monitor_paused:
|
||||
results["paused_monitors"].append(service_name)
|
||||
logger.info(f"Paused Kuma monitor for {service_name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to pause Kuma monitor for {service_name}: {e}")
|
||||
results["errors"].append(f"Kuma pause failed for {service_name}: {str(e)}")
|
||||
|
||||
# 2. Stop Portainer stack
|
||||
# Stop Portainer stack containers
|
||||
stacks = await portainer.get_stacks()
|
||||
stack = next(
|
||||
(s for s in stacks if s.get("Name", "").lower() == service_name.lower()),
|
||||
@@ -1025,9 +1011,6 @@ class InfrastructureController(BaseController):
|
||||
stack_id = stack.get("Id")
|
||||
endpoint_id = stack.get("EndpointId")
|
||||
|
||||
# Stop stack by deleting it (Portainer doesn't have a "stop" operation)
|
||||
# Note: This is destructive. For a gentler approach, we'd need to use docker compose stop
|
||||
# Let's use docker API instead
|
||||
logger.info(f"Stopping containers for stack: {service_name}")
|
||||
|
||||
# Get containers for this stack
|
||||
@@ -1078,7 +1061,7 @@ class InfrastructureController(BaseController):
|
||||
"/services/{name}/start",
|
||||
response_model=OperationResult,
|
||||
summary="Start a service or service group",
|
||||
description="Start a service or service group by starting containers and resuming monitors. Requires admin authentication when accessed externally via api.schweitz.net."
|
||||
description="Start a service or service group by starting containers. Requires admin authentication when accessed externally via api.schweitz.net."
|
||||
)
|
||||
async def start_service(
|
||||
name: str,
|
||||
@@ -1088,8 +1071,7 @@ class InfrastructureController(BaseController):
|
||||
Start a service or service group
|
||||
|
||||
This will:
|
||||
1. Start the Portainer stack(s)
|
||||
2. Resume Uptime Kuma monitors for all services in group
|
||||
1. Start the Portainer stack containers
|
||||
|
||||
Args:
|
||||
name: Service or group name
|
||||
@@ -1098,7 +1080,6 @@ class InfrastructureController(BaseController):
|
||||
Operation result with details
|
||||
"""
|
||||
portainer = get_portainer_client()
|
||||
kuma = get_kuma_client()
|
||||
|
||||
try:
|
||||
# Get all services in the group
|
||||
@@ -1106,14 +1087,13 @@ class InfrastructureController(BaseController):
|
||||
|
||||
results = {
|
||||
"started_services": [],
|
||||
"resumed_monitors": [],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
# Start each service
|
||||
for service_name in services:
|
||||
try:
|
||||
# 1. Start Portainer stack (start containers)
|
||||
# Start Portainer stack containers
|
||||
stacks = await portainer.get_stacks()
|
||||
stack = next(
|
||||
(s for s in stacks if s.get("Name", "").lower() == service_name.lower()),
|
||||
@@ -1147,16 +1127,6 @@ class InfrastructureController(BaseController):
|
||||
})
|
||||
logger.info(f"Started service: {service_name}")
|
||||
|
||||
# 2. Resume Uptime Kuma monitor
|
||||
try:
|
||||
monitor_resumed = await kuma.resume_monitor_by_name(service_name)
|
||||
if monitor_resumed:
|
||||
results["resumed_monitors"].append(service_name)
|
||||
logger.info(f"Resumed Kuma monitor for {service_name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to resume Kuma monitor for {service_name}: {e}")
|
||||
results["errors"].append(f"Kuma resume failed for {service_name}: {str(e)}")
|
||||
|
||||
else:
|
||||
results["errors"].append(f"Stack not found: {service_name}")
|
||||
|
||||
@@ -1181,8 +1151,6 @@ class InfrastructureController(BaseController):
|
||||
logger.error(f"Failed to start service group '{name}': {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# ===== Monitoring Endpoints =====
|
||||
|
||||
@router.get(
|
||||
"/widget-data",
|
||||
summary="Get combined data for service control widget",
|
||||
@@ -1190,11 +1158,10 @@ class InfrastructureController(BaseController):
|
||||
)
|
||||
async def get_widget_data():
|
||||
"""
|
||||
Get combined service and monitor data for the widget
|
||||
Get combined service data for the widget
|
||||
|
||||
Returns all data needed by service-control widget in a single call:
|
||||
- Service list with status and container counts
|
||||
- Monitor list with uptime percentages
|
||||
- Service groups and always-on list
|
||||
|
||||
This endpoint is designed for browser-based widgets to avoid
|
||||
@@ -1202,7 +1169,6 @@ class InfrastructureController(BaseController):
|
||||
"""
|
||||
try:
|
||||
portainer = get_portainer_client()
|
||||
kuma = get_kuma_client()
|
||||
npm = get_npm_client()
|
||||
|
||||
# Fetch services (same logic as /services endpoint)
|
||||
@@ -1252,38 +1218,9 @@ class InfrastructureController(BaseController):
|
||||
"containers_total": containers_total
|
||||
})
|
||||
|
||||
# Fetch monitors with real-time status from metrics endpoint
|
||||
monitors_list = []
|
||||
try:
|
||||
# Get real-time status from Prometheus metrics
|
||||
metrics_data = await kuma.get_metrics_status()
|
||||
|
||||
for monitor_name, monitor_info in metrics_data.items():
|
||||
# Status: 1=UP, 0=DOWN, 2=PENDING, 3=MAINTENANCE
|
||||
status = monitor_info.get('status', 0)
|
||||
|
||||
# Convert status to simple up/down for widget
|
||||
# Treat UP (1) as 100%, anything else as 0%
|
||||
status_percentage = 100.0 if status == 1 else 0.0
|
||||
|
||||
monitors_list.append({
|
||||
"id": None, # Not available from metrics
|
||||
"name": monitor_name,
|
||||
"uptime_24h": status_percentage, # Current status as percentage
|
||||
"active": True, # Assume active if in metrics
|
||||
"status": status, # 1=UP, 0=DOWN, 2=PENDING, 3=MAINTENANCE
|
||||
"response_time": monitor_info.get('response_time', 0)
|
||||
})
|
||||
|
||||
logger.info(f"Fetched status for {len(monitors_list)} monitors from metrics")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch monitors: {e}")
|
||||
# Continue without monitor data rather than failing
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"services": services,
|
||||
"monitors": monitors_list,
|
||||
"service_groups": {
|
||||
"groups": service_groups.list_service_groups(),
|
||||
"always_on": list(service_groups.ALWAYS_ON_SERVICES),
|
||||
@@ -1295,169 +1232,6 @@ class InfrastructureController(BaseController):
|
||||
logger.error(f"Failed to fetch widget data: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch widget data: {str(e)}")
|
||||
|
||||
@router.get(
|
||||
"/monitors",
|
||||
summary="List all monitors",
|
||||
response_model=Dict[str, Any]
|
||||
)
|
||||
async def list_monitors():
|
||||
"""
|
||||
List all Uptime Kuma monitors
|
||||
|
||||
Returns:
|
||||
List of monitors with their configurations
|
||||
"""
|
||||
try:
|
||||
kuma = get_kuma_client()
|
||||
monitors = await kuma.get_monitors()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"monitors": monitors,
|
||||
"total": len(monitors)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list monitors: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to list monitors: {str(e)}")
|
||||
|
||||
@router.post(
|
||||
"/monitors",
|
||||
summary="Create a new monitor",
|
||||
description="Create a new Uptime Kuma monitor. Requires admin authentication.",
|
||||
response_model=Dict[str, Any]
|
||||
)
|
||||
async def create_monitor(
|
||||
monitor_config: Dict[str, Any],
|
||||
user: Dict = Depends(get_admin_user)
|
||||
):
|
||||
"""
|
||||
Create a new Uptime Kuma monitor
|
||||
|
||||
Args:
|
||||
monitor_config: Monitor configuration (name, type, hostname, port, etc.)
|
||||
|
||||
Returns:
|
||||
Created monitor details including ID
|
||||
"""
|
||||
try:
|
||||
kuma = get_kuma_client()
|
||||
created_monitor = await kuma.add_monitor(monitor_config)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Monitor '{monitor_config.get('name')}' created successfully",
|
||||
"monitor": created_monitor
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create monitor: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create monitor: {str(e)}")
|
||||
|
||||
@router.get(
|
||||
"/monitors/{monitor_id}",
|
||||
summary="Get monitor details",
|
||||
response_model=Dict[str, Any]
|
||||
)
|
||||
async def get_monitor(monitor_id: int):
|
||||
"""
|
||||
Get details of a specific monitor
|
||||
|
||||
Args:
|
||||
monitor_id: Monitor identifier
|
||||
|
||||
Returns:
|
||||
Monitor configuration and status
|
||||
"""
|
||||
try:
|
||||
kuma = get_kuma_client()
|
||||
monitor = await kuma.get_monitor(monitor_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"monitor": monitor
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get monitor {monitor_id}: {e}")
|
||||
raise HTTPException(status_code=404, detail=f"Monitor {monitor_id} not found: {str(e)}")
|
||||
|
||||
@router.put(
|
||||
"/monitors/{monitor_id}",
|
||||
summary="Update a monitor",
|
||||
description="Update an existing Uptime Kuma monitor. Requires admin authentication.",
|
||||
response_model=Dict[str, Any]
|
||||
)
|
||||
async def update_monitor(
|
||||
monitor_id: int,
|
||||
updates: Dict[str, Any],
|
||||
user: Dict = Depends(get_admin_user)
|
||||
):
|
||||
"""
|
||||
Update an existing monitor
|
||||
|
||||
Args:
|
||||
monitor_id: Monitor identifier
|
||||
updates: Fields to update
|
||||
|
||||
Returns:
|
||||
Updated monitor details
|
||||
"""
|
||||
try:
|
||||
kuma = get_kuma_client()
|
||||
|
||||
# Get existing monitor
|
||||
existing = await kuma.get_monitor(monitor_id)
|
||||
|
||||
# Merge updates
|
||||
monitor_config = existing.copy()
|
||||
monitor_config.update(updates)
|
||||
|
||||
# Update monitor
|
||||
updated_monitor = await kuma.update_monitor(monitor_id, monitor_config)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Monitor {monitor_id} updated successfully",
|
||||
"monitor": updated_monitor
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update monitor {monitor_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update monitor: {str(e)}")
|
||||
|
||||
@router.delete(
|
||||
"/monitors/{monitor_id}",
|
||||
summary="Delete a monitor",
|
||||
description="Delete an Uptime Kuma monitor. Requires admin authentication.",
|
||||
response_model=Dict[str, Any]
|
||||
)
|
||||
async def delete_monitor(
|
||||
monitor_id: int,
|
||||
user: Dict = Depends(get_admin_user)
|
||||
):
|
||||
"""
|
||||
Delete a monitor
|
||||
|
||||
Args:
|
||||
monitor_id: Monitor identifier
|
||||
|
||||
Returns:
|
||||
Success confirmation
|
||||
"""
|
||||
try:
|
||||
kuma = get_kuma_client()
|
||||
await kuma.delete_monitor(monitor_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Monitor {monitor_id} deleted successfully"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete monitor {monitor_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete monitor: {str(e)}")
|
||||
|
||||
# ========================================================================
|
||||
# Container Management Endpoints (for core-ai infrastructure tools)
|
||||
# ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user