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>
112 lines
3.5 KiB
Python
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
|