"""Playbook endpoints for triggering infrastructure automation.""" import uuid from fastapi import APIRouter, HTTPException, status from pydantic import BaseModel, Field from sqlalchemy import select from app.db import AsyncSessionDep from app.models.agent import Agent from app.models.task import Task from app.models.tenant import Tenant from app.playbooks.chatwoot import create_chatwoot_setup_task from app.playbooks.nextcloud import create_nextcloud_set_domain_task from app.schemas.task import TaskResponse router = APIRouter(prefix="/tenants/{tenant_id}", tags=["Playbooks"]) class ChatwootSetupRequest(BaseModel): """Request body for Chatwoot setup playbook.""" agent_id: uuid.UUID | None = Field( None, description="Optional agent UUID to assign the task to" ) domain: str = Field( ..., min_length=1, description="Domain for Chatwoot (e.g., support.example.com)" ) class NextcloudSetDomainRequest(BaseModel): """Request body for Nextcloud set-domain playbook.""" public_url: str = Field( ..., min_length=1, description="Public URL for Nextcloud (e.g., https://cloud.example.com)", ) pull: bool = Field( False, description="Whether to pull images before reloading the stack", ) # --- Helper functions --- async def get_tenant_by_id(db: AsyncSessionDep, tenant_id: uuid.UUID) -> Tenant | None: """Retrieve a tenant by ID.""" result = await db.execute(select(Tenant).where(Tenant.id == tenant_id)) return result.scalar_one_or_none() async def get_agent_by_id(db: AsyncSessionDep, agent_id: uuid.UUID) -> Agent | None: """Retrieve an agent by ID.""" result = await db.execute(select(Agent).where(Agent.id == agent_id)) return result.scalar_one_or_none() async def get_online_agent_for_tenant( db: AsyncSessionDep, tenant_id: uuid.UUID ) -> Agent | None: """Get the first online agent for a tenant.""" result = await db.execute( select(Agent) .where(Agent.tenant_id == tenant_id) .where(Agent.status == "online") .limit(1) ) return result.scalar_one_or_none() # --- Route handlers --- @router.post( "/chatwoot/setup", response_model=TaskResponse, status_code=status.HTTP_201_CREATED, ) async def setup_chatwoot( tenant_id: uuid.UUID, request: ChatwootSetupRequest, db: AsyncSessionDep, ) -> Task: """ Trigger Chatwoot setup playbook for a tenant. Creates a COMPOSITE task with the following steps: 1. **ENV_UPDATE**: Set FRONTEND_URL and BACKEND_URL in chatwoot.env 2. **DOCKER_RELOAD**: Restart the Chatwoot stack with pull=True ## Request Body - **agent_id**: Optional agent UUID to assign the task to immediately - **domain**: The domain for Chatwoot (e.g., "support.example.com") ## Response Returns the created Task with type="COMPOSITE" and status="pending". The SysAdmin Agent will pick up this task and execute the steps in sequence. """ # Validate tenant exists tenant = await get_tenant_by_id(db, tenant_id) if tenant is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Tenant {tenant_id} not found", ) # Validate agent exists if provided if request.agent_id is not None: agent = await get_agent_by_id(db, request.agent_id) if agent is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Agent {request.agent_id} not found", ) # Create the COMPOSITE task task = await create_chatwoot_setup_task( db=db, tenant_id=tenant_id, agent_id=request.agent_id, domain=request.domain, ) return task @router.post( "/nextcloud/set-domain", response_model=TaskResponse, status_code=status.HTTP_201_CREATED, ) async def nextcloud_set_domain( tenant_id: uuid.UUID, request: NextcloudSetDomainRequest, db: AsyncSessionDep, ) -> Task: """ Set Nextcloud domain for a tenant (v1 - reload only). Creates a COMPOSITE task with a single DOCKER_RELOAD step. Auto-resolves the agent from the tenant (first online agent). **Note:** This is v1 of the playbook. It only reloads the Nextcloud stack. Future versions will add occ commands to configure config.php. ## Request Body - **public_url**: The public URL for Nextcloud (e.g., "https://cloud.example.com") - **pull**: Whether to pull images before reloading (default: false) ## Response Returns the created Task with type="COMPOSITE" and status="pending". The SysAdmin Agent will pick up this task and execute the steps in sequence. """ # Validate tenant exists tenant = await get_tenant_by_id(db, tenant_id) if tenant is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Tenant {tenant_id} not found", ) # Auto-resolve agent (first online agent for tenant) agent = await get_online_agent_for_tenant(db, tenant_id) if agent is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"No online agent found for tenant {tenant_id}", ) # Create the COMPOSITE task task = await create_nextcloud_set_domain_task( db=db, tenant_id=tenant_id, agent_id=agent.id, public_url=request.public_url, pull=request.pull, ) return task