"""n8n workflow automation deployment playbook. Defines the steps required to: 1. Set up n8n on a tenant server (ENV_UPDATE + DOCKER_RELOAD) 2. Perform initial setup via Playwright automation (create owner account) Tenant servers must have stacks and env templates under /opt/letsbe. """ import uuid from typing import Any from urllib.parse import urlparse from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.models.task import Task, TaskStatus class CompositeStep(BaseModel): """A single step in a composite playbook.""" type: str = Field(..., description="Task type (e.g., ENV_UPDATE, DOCKER_RELOAD)") payload: dict[str, Any] = Field( default_factory=dict, description="Payload for this step" ) # LetsBe standard paths N8N_ENV_PATH = "/opt/letsbe/env/n8n.env" N8N_STACK_DIR = "/opt/letsbe/stacks/n8n" def build_n8n_setup_steps(*, domain: str) -> list[CompositeStep]: """ Build the sequence of steps required to set up n8n. Assumes the env file already exists at /opt/letsbe/env/n8n.env (created by provisioning/env_setup.sh). Args: domain: The domain for n8n (e.g., "n8n.example.com") Returns: List of 2 CompositeStep objects: 1. ENV_UPDATE - patches N8N_HOST, N8N_PROTOCOL, WEBHOOK_URL 2. DOCKER_RELOAD - restarts the n8n stack with pull=True """ steps = [ # Step 1: Update environment variables CompositeStep( type="ENV_UPDATE", payload={ "path": N8N_ENV_PATH, "updates": { "N8N_HOST": domain, "N8N_PROTOCOL": "https", "WEBHOOK_URL": f"https://{domain}/", }, }, ), # Step 2: Reload Docker stack CompositeStep( type="DOCKER_RELOAD", payload={ "compose_dir": N8N_STACK_DIR, "pull": True, }, ), ] return steps async def create_n8n_setup_task( *, db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID | None, domain: str, ) -> Task: """ Create and persist a COMPOSITE task for n8n setup. Args: db: Async database session tenant_id: UUID of the tenant agent_id: Optional UUID of the agent to assign the task to domain: The domain for n8n Returns: The created Task object with type="COMPOSITE" """ steps = build_n8n_setup_steps(domain=domain) task = Task( tenant_id=tenant_id, agent_id=agent_id, type="COMPOSITE", payload={"steps": [step.model_dump() for step in steps]}, status=TaskStatus.PENDING.value, ) db.add(task) await db.commit() await db.refresh(task) return task # ============================================================================= # Initial Setup via Playwright # ============================================================================= def build_n8n_initial_setup_step( *, base_url: str, admin_email: str, admin_password: str | None = None, admin_first_name: str = "Admin", admin_last_name: str = "User", ) -> dict[str, Any]: """ Build a PLAYWRIGHT task payload for n8n initial setup. This creates the owner account on a fresh n8n installation. Args: base_url: The base URL for n8n (e.g., "https://n8n.example.com") admin_email: Email address for the owner account admin_password: Password for owner (auto-generated if None) admin_first_name: First name for the owner account admin_last_name: Last name for the owner account Returns: Task payload dict with type="PLAYWRIGHT" """ parsed = urlparse(base_url) allowed_domain = parsed.netloc inputs: dict[str, Any] = { "base_url": base_url, "admin_email": admin_email, "admin_first_name": admin_first_name, "admin_last_name": admin_last_name, } if admin_password: inputs["admin_password"] = admin_password return { "scenario": "n8n_initial_setup", "inputs": inputs, "options": { "allowed_domains": [allowed_domain], }, "timeout": 120, } async def create_n8n_initial_setup_task( *, db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID, base_url: str, admin_email: str, admin_password: str | None = None, admin_first_name: str = "Admin", admin_last_name: str = "User", ) -> Task: """ Create and persist a PLAYWRIGHT task for n8n initial setup. Args: db: Async database session tenant_id: UUID of the tenant agent_id: UUID of the agent to assign the task to base_url: The base URL for n8n admin_email: Email address for the owner account admin_password: Password for owner (auto-generated if None) admin_first_name: First name for the owner account admin_last_name: Last name for the owner account Returns: The created Task object with type="PLAYWRIGHT" """ payload = build_n8n_initial_setup_step( base_url=base_url, admin_email=admin_email, admin_password=admin_password, admin_first_name=admin_first_name, admin_last_name=admin_last_name, ) task = Task( tenant_id=tenant_id, agent_id=agent_id, type="PLAYWRIGHT", payload=payload, status=TaskStatus.PENDING.value, ) db.add(task) await db.commit() await db.refresh(task) return task