fastapi-gsap/gsap_broker/connectors/orchestrator.py
Tyler J King 2ac5aa3b85 feat(connectors): add OrchestratorConnector base and stubbed Ansible
Multi-step workflow base class with plan/execute lifecycle and
partial-completion reporting. Ansible connector stubbed —
ansible-runner integration in future sprint. Credentials
resolved per-host at runtime via CredentialResolver, never stored.

Signed-off-by: Tyler King <tking@guildhouse.dev>
2026-04-14 06:00:48 -04:00

112 lines
3.5 KiB
Python

# Copyright 2026 Guildhouse Dev
# SPDX-License-Identifier: Apache-2.0
"""Orchestrator connector framework — multi-step workflow execution.
Orchestrator connectors manage workflows that span multiple steps
and potentially multiple targets. Each step may acquire its own
credentials via the CredentialResolver.
Unlike ``SessionConnector`` (single target, single credential,
single command), an ``OrchestratorConnector``:
- Plans a sequence of steps before execution
- Executes steps in order, stopping on required-step failure
- Reports partial results (which steps completed before failure)
- Can target different hosts per step
Rust port note:
``WorkflowStep`` and ``WorkflowPlan`` map to plain structs.
``OrchestratorConnector`` maps to an async trait with
``plan()`` and ``execute_step()`` methods.
"""
from __future__ import annotations
import logging
from abc import abstractmethod
from dataclasses import dataclass, field
from typing import Any
from gsap_broker.connectors.base import ConnectorContext, ConnectorPlugin, ConnectorResult
from gsap_broker.credentials.resolver import CredentialResolver
logger = logging.getLogger(__name__)
@dataclass
class WorkflowStep:
"""A single step in a workflow plan."""
name: str
command: str
targets: list[str] = field(default_factory=list)
required: bool = True
extra_vars: dict[str, Any] = field(default_factory=dict)
@dataclass
class WorkflowPlan:
"""Ordered sequence of steps for a workflow."""
steps: list[WorkflowStep] = field(default_factory=list)
class OrchestratorConnector(ConnectorPlugin):
"""Base for multi-step workflow connectors (Ansible, Terraform, etc).
Subclasses implement ``plan()`` to convert an operation + parameters
into a ``WorkflowPlan``, and ``execute_step()`` to run each step.
The base ``invoke()`` handles:
- Planning the workflow
- Executing steps in order
- Stopping on required-step failure
- Aggregating results with partial-completion reporting
"""
def __init__(self, credential_resolver: CredentialResolver):
self._resolver = credential_resolver
async def invoke(
self, operation: str, parameters: dict[str, Any], context: ConnectorContext
) -> ConnectorResult:
try:
plan = await self.plan(operation, parameters, context)
except Exception as e:
return ConnectorResult(success=False, error=f"Planning failed: {e}")
if not plan.steps:
return ConnectorResult(success=True, data={"steps": []})
results: list[dict[str, Any]] = []
for step in plan.steps:
result = await self.execute_step(step, context)
results.append({"step": step.name, **result})
if not result.get("success") and step.required:
return ConnectorResult(
success=False,
data={"completed": results, "failed_at": step.name},
)
return ConnectorResult(success=True, data={"steps": results})
@abstractmethod
async def plan(
self, operation: str, parameters: dict[str, Any], context: ConnectorContext
) -> WorkflowPlan:
"""Convert an operation into a step-by-step execution plan."""
...
@abstractmethod
async def execute_step(
self, step: WorkflowStep, context: ConnectorContext
) -> dict[str, Any]:
"""Execute a single workflow step.
Returns a dict with at minimum a ``success: bool`` key.
"""
...
def health_check(self) -> bool:
return True