"""Vaultwarden password manager deployment playbook. Defines the steps required to set up Vaultwarden on a tenant server (ENV_UPDATE + DOCKER_RELOAD). No Playwright setup needed - Vaultwarden uses a web-based registration flow that doesn't require automation. Tenant servers must have stacks and env templates under /opt/letsbe. """ import uuid from typing import Any 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 VAULTWARDEN_ENV_PATH = "/opt/letsbe/env/vaultwarden.env" VAULTWARDEN_STACK_DIR = "/opt/letsbe/stacks/vaultwarden" def build_vaultwarden_setup_steps( *, domain: str, admin_token: str, signups_allowed: bool = True, ) -> list[CompositeStep]: """ Build the sequence of steps required to set up Vaultwarden. Assumes the env file already exists at /opt/letsbe/env/vaultwarden.env (created by provisioning/env_setup.sh). Args: domain: The domain for Vaultwarden (e.g., "vault.example.com") admin_token: Admin panel access token signups_allowed: Whether new user registration is allowed Returns: List of 2 CompositeStep objects: 1. ENV_UPDATE - patches DOMAIN, ADMIN_TOKEN, SIGNUPS_ALLOWED 2. DOCKER_RELOAD - restarts the vaultwarden stack with pull=True """ steps = [ # Step 1: Update environment variables CompositeStep( type="ENV_UPDATE", payload={ "path": VAULTWARDEN_ENV_PATH, "updates": { "DOMAIN": f"https://{domain}", "ADMIN_TOKEN": admin_token, "SIGNUPS_ALLOWED": str(signups_allowed).lower(), }, }, ), # Step 2: Reload Docker stack CompositeStep( type="DOCKER_RELOAD", payload={ "compose_dir": VAULTWARDEN_STACK_DIR, "pull": True, }, ), ] return steps async def create_vaultwarden_setup_task( *, db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID | None, domain: str, admin_token: str, signups_allowed: bool = True, ) -> Task: """ Create and persist a COMPOSITE task for Vaultwarden 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 Vaultwarden admin_token: Admin panel access token signups_allowed: Whether new user registration is allowed Returns: The created Task object with type="COMPOSITE" """ steps = build_vaultwarden_setup_steps( domain=domain, admin_token=admin_token, signups_allowed=signups_allowed, ) 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