LetsBeBiz-Redesign/letsbe-orchestrator/app/playbooks/portainer.py

106 lines
2.8 KiB
Python
Raw Permalink Normal View History

"""Portainer container management deployment playbook.
Defines the steps required to set up Portainer on a tenant server
(ENV_UPDATE + DOCKER_RELOAD). No Playwright setup needed - Portainer's
admin account is created via its first-use web UI which is already
handled during provisioning.
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
PORTAINER_ENV_PATH = "/opt/letsbe/env/portainer.env"
PORTAINER_STACK_DIR = "/opt/letsbe/stacks/portainer"
def build_portainer_setup_steps(*, domain: str) -> list[CompositeStep]:
"""
Build the sequence of steps required to set up Portainer.
Assumes the env file already exists at /opt/letsbe/env/portainer.env
(created by provisioning/env_setup.sh).
Args:
domain: The domain for Portainer (e.g., "portainer.example.com")
Returns:
List of 2 CompositeStep objects:
1. ENV_UPDATE - patches PORTAINER_DOMAIN
2. DOCKER_RELOAD - restarts the portainer stack with pull=True
"""
steps = [
# Step 1: Update environment variables
CompositeStep(
type="ENV_UPDATE",
payload={
"path": PORTAINER_ENV_PATH,
"updates": {
"PORTAINER_DOMAIN": domain,
},
},
),
# Step 2: Reload Docker stack
CompositeStep(
type="DOCKER_RELOAD",
payload={
"compose_dir": PORTAINER_STACK_DIR,
"pull": True,
},
),
]
return steps
async def create_portainer_setup_task(
*,
db: AsyncSession,
tenant_id: uuid.UUID,
agent_id: uuid.UUID | None,
domain: str,
) -> Task:
"""
Create and persist a COMPOSITE task for Portainer 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 Portainer
Returns:
The created Task object with type="COMPOSITE"
"""
steps = build_portainer_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