Files
lda-wf/src/wf_api/deployments.py
T

230 lines
7.9 KiB
Python

"""Saved deployment operations and dependency validation."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from wf_artifacts import (
AvailableCapability,
AvailableSource,
DependencyDiagnostic,
WorkflowArtifact,
WorkflowDeployment,
validate_deployment_dependencies,
)
from wf_platform import CapabilitySource, hash_json_schema
from .models import (
DeleteDeploymentResult,
DeploymentSummary,
JsonProjector,
ListDeploymentsResult,
SaveDeploymentResult,
ValidateDeploymentResult,
WorkflowDeploymentPayload,
)
from .next_actions import NextActions
from .operation_context import WorkflowOperationContext
from .saved_subgraphs import resolve_saved_subgraph_tree, validate_saved_subgraph_tree
_PROJECT_DEPLOYMENT = JsonProjector(WorkflowDeploymentPayload)
_PROJECT_DEPLOYMENT_VALIDATION = JsonProjector(ValidateDeploymentResult)
class WorkflowDeploymentApi:
"""Saved deployment operations and dependency validation."""
def __init__(self, context: WorkflowOperationContext) -> None:
self.context = context
def _artifact_store(self):
if self.context.artifact_store is None:
raise KeyError("workflow artifact store is not configured")
return self.context.artifact_store
async def list_deployments(self) -> ListDeploymentsResult:
if self.context.artifact_store is None:
return {"deployments": []}
return {
"deployments": [
_deployment_summary(deployment)
for deployment in self.context.artifact_store.list_deployments()
]
}
async def inspect_deployment(
self, *, deployment_id: str
) -> WorkflowDeploymentPayload:
return _PROJECT_DEPLOYMENT(
self._artifact_store()
.get_deployment(deployment_id)
.model_dump(mode="json"),
)
async def save_deployment(self, deployment: dict[str, Any]) -> SaveDeploymentResult:
workflow_deployment = WorkflowDeployment.model_validate(deployment)
self._artifact_store().save_deployment(workflow_deployment)
self.context.events.record_workflow_event(
"workflow_deployment_saved",
capability_id=f"deployment.{workflow_deployment.id}",
payload={
"deployment_id": workflow_deployment.id,
"artifact_id": workflow_deployment.artifact_id,
"artifact_version": workflow_deployment.artifact_version,
},
)
return {
"deployment_id": workflow_deployment.id,
"artifact_id": workflow_deployment.artifact_id,
"artifact_version": workflow_deployment.artifact_version,
"saved": True,
}
async def delete_deployment(self, *, deployment_id: str) -> DeleteDeploymentResult:
"""Delete one mutable deployment environment binding."""
self._artifact_store().delete_deployment(deployment_id)
self.context.events.record_workflow_event(
"workflow_deployment_deleted",
capability_id=f"deployment.{deployment_id}",
payload={"deployment_id": deployment_id},
)
return {"deployment_id": deployment_id, "deleted": True}
async def validate_deployment(
self,
*,
deployment_id: str,
live_check: bool = False,
) -> ValidateDeploymentResult:
deployment, artifact, diagnostics, tree = self.deployment_validation(
deployment_id
)
if live_check and self.context.live_sources is not None:
diagnostics.extend(
await self.context.live_sources.deployment_diagnostics(
deployment=deployment,
artifacts=[artifact, *tree.artifacts_by_ref.values()],
)
)
result = {
"deployment_id": deployment.id,
"artifact_id": artifact.id,
"artifact_version": artifact.version,
"status": "unrunnable" if diagnostics else "runnable",
"diagnostics": [
diagnostic.model_dump(mode="json") for diagnostic in diagnostics
],
"next_actions": NextActions.from_deployment_validation(
deployment_id=deployment.id,
diagnostics=diagnostics,
).model_dump(mode="json"),
}
return _PROJECT_DEPLOYMENT_VALIDATION(result)
def deployment_validation(
self,
deployment_id: str,
) -> tuple[
WorkflowDeployment,
WorkflowArtifact,
list[DependencyDiagnostic],
Any, # SavedSubgraphTree
]:
store = self._artifact_store()
deployment = store.get_deployment(deployment_id)
artifact = store.get_artifact(
deployment.artifact_id,
deployment.artifact_version,
)
available_sources = _available_sources(self.context.specs.capability_sources)
diagnostics = validate_deployment_dependencies(
artifact=artifact,
deployment=deployment,
sources=available_sources,
)
tree = resolve_saved_subgraph_tree(
root_artifact=artifact,
artifact_store=store,
)
diagnostics.extend(
validate_saved_subgraph_tree(
tree=tree,
deployment=deployment,
sources=available_sources,
)
)
return deployment, artifact, diagnostics, tree
def _available_sources(
capability_sources: Mapping[str, CapabilitySource],
) -> list[AvailableSource]:
"""Convert broker capability sources into artifact validation snapshots."""
sources: list[AvailableSource] = []
for source in capability_sources.values():
node_spec_details = {
detail.name: detail
for detail in source.as_inventory().capabilities.node_spec_details
}
capabilities = {
capability_name: AvailableCapability(
name=capability_name,
kind="node_spec",
input_schema_hash=hash_json_schema(detail.input_schema),
output_schema_hash=hash_json_schema(detail.output_schema),
)
for spec in source.capabilities.node_specs.values()
if (capability_name := _capability_name(spec.name)) is not None
if (detail := node_spec_details.get(spec.name)) is not None
}
capabilities.update(
{
capability_name: AvailableCapability(
name=capability_name,
kind="reducer",
)
for reducer in source.capabilities.reducers.values()
if (capability_name := _capability_name(reducer.name)) is not None
}
)
sources.append(
AvailableSource(
id=source.id,
enabled=source.enabled,
platform=source.policy.platform,
capabilities=capabilities,
)
)
return sources
def _capability_name(qualified_name: str) -> str | None:
"""Return the local name of one qualified capability ref if it is valid."""
from wf_api.refs import parse_workflow_surface_capability_id
from wf_artifacts import WorkflowCapabilityRef
try:
parsed = parse_workflow_surface_capability_id(qualified_name)
except ValueError:
return None
if isinstance(parsed, WorkflowCapabilityRef):
return None
return parsed.name
def _deployment_summary(deployment: WorkflowDeployment) -> DeploymentSummary:
"""Return compact deployment metadata for progressive list responses."""
return {
"id": deployment.id,
"artifact_id": deployment.artifact_id,
"artifact_version": deployment.artifact_version,
"binding_count": len(deployment.binding_map()),
"drift_policy": deployment.drift_policy.value,
}
__all__ = [
"WorkflowDeploymentApi",
]