| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359 |
- """PostgreSQL persistence adapters for the DataOps MCP control plane."""
- from __future__ import annotations
- import hashlib
- import json
- import uuid
- from datetime import UTC, datetime
- from zoneinfo import ZoneInfo
- from croniter import croniter
- from sqlalchemy import text
- from app.core.common.identifiers import new_governance_uid
- def _json(value):
- return json.dumps(
- value,
- sort_keys=True,
- separators=(",", ":"),
- ensure_ascii=False,
- )
- def _schedule_hash(schedule_plan):
- return hashlib.sha256(_json(schedule_plan).encode("utf-8")).hexdigest()
- def _required_string(value, label, maximum=500):
- if not isinstance(value, str) or not value.strip():
- raise ValueError(f"{label} is required")
- normalized = value.strip()
- if len(normalized) > maximum:
- raise ValueError(f"{label} exceeds {maximum} characters")
- return normalized
- class PostgresSchedulingPlanStore:
- def __init__(self, engine):
- self.engine = engine
- def create_candidate(self, record):
- dataflow_uid = _required_string(record.get("dataflow_uid"), "dataflow_uid", 100)
- business_domain = _required_string(
- record.get("business_domain"), "business_domain", 200
- )
- environment = _required_string(record.get("environment"), "environment", 20)
- created_by = _required_string(record.get("created_by"), "created_by", 200)
- workflow_spec = dict(record.get("workflow_spec") or {})
- schedule_plan = dict(record.get("schedule_plan") or {})
- workflow_hash = _required_string(
- record.get("workflow_hash"), "workflow_hash", 64
- )
- candidate_id = new_governance_uid()
- binding_id = new_governance_uid()
- schedule_id = new_governance_uid()
- placeholder = f"candidate_{candidate_id.replace('-', '')}"
- metadata = {
- "gateway_status": "candidate",
- "correlation_id": record.get("correlation_id"),
- }
- with self.engine.begin() as connection:
- connection.execute(
- text("SELECT pg_advisory_xact_lock(hashtext(:key))"),
- {"key": f"{dataflow_uid}:{environment}"},
- )
- version_no = connection.execute(
- text(
- "SELECT COALESCE(MAX(version_no), 0) + 1 "
- "FROM public.dataflow_workflow_versions "
- "WHERE dataflow_uid = CAST(:uid AS uuid) "
- "AND environment = :environment"
- ),
- {"uid": dataflow_uid, "environment": environment},
- ).scalar_one()
- connection.execute(
- text(
- "INSERT INTO public.dataflow_workflow_versions "
- "(id, dataflow_uid, environment, version_no, "
- "n8n_workflow_id, n8n_workflow_name, definition_hash, "
- "definition_snapshot, status, created_by, engine_type, "
- "engine_definition_id, engine_revision, "
- "deployment_metadata, workflow_spec, schedule_plan, "
- "business_domain, created_by_subject, write_authorized) "
- "VALUES (CAST(:id AS uuid), CAST(:dataflow_uid AS uuid), "
- ":environment, :version_no, NULL, NULL, :definition_hash, "
- "CAST(:definition_snapshot AS jsonb), 'draft', NULL, "
- "'kestra', :engine_definition_id, NULL, "
- "CAST(:deployment_metadata AS jsonb), "
- "CAST(:workflow_spec AS jsonb), "
- "CAST(:schedule_plan AS jsonb), :business_domain, "
- ":created_by_subject, FALSE)"
- ),
- {
- "id": candidate_id,
- "dataflow_uid": dataflow_uid,
- "environment": environment,
- "version_no": version_no,
- "definition_hash": workflow_hash,
- "definition_snapshot": _json(workflow_spec),
- "engine_definition_id": placeholder,
- "deployment_metadata": _json(metadata),
- "workflow_spec": _json(workflow_spec),
- "schedule_plan": _json(schedule_plan),
- "business_domain": business_domain,
- "created_by_subject": created_by,
- },
- )
- connection.execute(
- text(
- "INSERT INTO public.workflow_engine_bindings "
- "(id, workflow_version_id, dataflow_uid, environment, "
- "engine_type, engine_definition_id, engine_revision, "
- "role, status, metadata) "
- "VALUES (CAST(:id AS uuid), CAST(:version_id AS uuid), "
- "CAST(:dataflow_uid AS uuid), :environment, 'kestra', "
- ":engine_definition_id, NULL, 'standby', 'disabled', "
- "CAST(:metadata AS jsonb))"
- ),
- {
- "id": binding_id,
- "version_id": candidate_id,
- "dataflow_uid": dataflow_uid,
- "environment": environment,
- "engine_definition_id": placeholder,
- "metadata": _json(metadata),
- },
- )
- connection.execute(
- text(
- "INSERT INTO public.workflow_schedules "
- "(id, workflow_version_id, schedule_plan, schedule_hash, "
- "timezone, status, created_by) "
- "VALUES (CAST(:id AS uuid), CAST(:version_id AS uuid), "
- "CAST(:schedule_plan AS jsonb), :schedule_hash, "
- ":timezone, 'draft', NULL)"
- ),
- {
- "id": schedule_id,
- "version_id": candidate_id,
- "schedule_plan": _json(schedule_plan),
- "schedule_hash": _schedule_hash(schedule_plan),
- "timezone": schedule_plan.get("timezone"),
- },
- )
- return {
- **record,
- "candidate_id": candidate_id,
- "version_no": int(version_no),
- "status": "candidate",
- }
- def get_candidate(self, candidate_id):
- with self.engine.connect() as connection:
- row = (
- connection.execute(
- text(
- "SELECT id::text AS candidate_id, "
- "dataflow_uid::text AS dataflow_uid, business_domain, "
- "environment, version_no, workflow_spec, schedule_plan, "
- "definition_hash AS workflow_hash, status, "
- "created_by_subject AS created_by, write_authorized, "
- "deployment_metadata "
- "FROM public.dataflow_workflow_versions "
- "WHERE id = CAST(:id AS uuid) AND engine_type = 'kestra'"
- ),
- {"id": candidate_id},
- )
- .mappings()
- .one_or_none()
- )
- if row is None:
- raise KeyError(candidate_id)
- result = dict(row)
- metadata = dict(result.pop("deployment_metadata") or {})
- result["status"] = metadata.get("gateway_status", result["status"])
- result["trusted_write_authorized"] = bool(result.pop("write_authorized", False))
- return result
- def record_deployment(self, candidate_id, deployment):
- metadata = {
- "gateway_status": deployment["status"],
- "deployment": deployment,
- }
- with self.engine.begin() as connection:
- updated = connection.execute(
- text(
- "UPDATE public.dataflow_workflow_versions "
- "SET engine_definition_id = :flow_id, "
- "engine_revision = :revision, "
- "deployment_metadata = deployment_metadata || "
- "CAST(:metadata AS jsonb), updated_at = CURRENT_TIMESTAMP "
- "WHERE id = CAST(:id AS uuid) AND engine_type = 'kestra'"
- ),
- {
- "id": candidate_id,
- "flow_id": deployment["flow_id"],
- "revision": deployment.get("engine_revision"),
- "metadata": _json(metadata),
- },
- )
- if updated.rowcount != 1:
- raise KeyError(candidate_id)
- connection.execute(
- text(
- "UPDATE public.workflow_engine_bindings "
- "SET engine_definition_id = :flow_id, "
- "engine_revision = :revision, metadata = CAST(:metadata AS jsonb), "
- "updated_at = CURRENT_TIMESTAMP "
- "WHERE workflow_version_id = CAST(:id AS uuid) "
- "AND engine_type = 'kestra'"
- ),
- {
- "id": candidate_id,
- "flow_id": deployment["flow_id"],
- "revision": deployment.get("engine_revision"),
- "metadata": _json(deployment),
- },
- )
- return dict(deployment)
- def get_deployment(self, candidate_id):
- with self.engine.connect() as connection:
- metadata = connection.execute(
- text(
- "SELECT deployment_metadata "
- "FROM public.dataflow_workflow_versions "
- "WHERE id = CAST(:id AS uuid) AND engine_type = 'kestra'"
- ),
- {"id": candidate_id},
- ).scalar_one_or_none()
- if metadata is None or not dict(metadata).get("deployment"):
- raise KeyError(candidate_id)
- return dict(metadata)["deployment"]
- def get_active_deployment(self, candidate_id):
- with self.engine.connect() as connection:
- row = (
- connection.execute(
- text(
- "SELECT active.id::text AS candidate_id, "
- "active.deployment_metadata "
- "FROM public.dataflow_workflow_versions candidate "
- "JOIN public.dataflow_workflow_versions active "
- "ON active.dataflow_uid = candidate.dataflow_uid "
- "AND active.environment = candidate.environment "
- "AND active.engine_type = 'kestra' "
- "AND active.status = 'active' "
- "AND active.id <> candidate.id "
- "WHERE candidate.id = CAST(:id AS uuid) "
- "ORDER BY active.version_no DESC LIMIT 1"
- ),
- {"id": candidate_id},
- )
- .mappings()
- .one_or_none()
- )
- if row is None:
- return None
- deployment = dict(row["deployment_metadata"] or {}).get("deployment")
- if not isinstance(deployment, dict):
- raise ValueError("active deployment is incomplete")
- return {**deployment, "candidate_id": row["candidate_id"]}
- def record_canary_execution(self, candidate_id, evidence):
- evidence_id = new_governance_uid()
- with self.engine.begin() as connection:
- connection.execute(
- text(
- "INSERT INTO public.workflow_canary_evidence "
- "(id, workflow_version_id, engine_execution_id, status, "
- "sample_runs, verification_summary) "
- "VALUES (CAST(:id AS uuid), CAST(:version_id AS uuid), "
- ":execution_id, 'started', 0, '{}'::jsonb)"
- ),
- {
- "id": evidence_id,
- "version_id": candidate_id,
- "execution_id": evidence["execution_id"],
- },
- )
- return dict(evidence)
- def get_canary_evidence(self, candidate_id):
- with self.engine.connect() as connection:
- row = (
- connection.execute(
- text(
- "SELECT engine_execution_id AS execution_id, status, "
- "sample_runs, verified_by, verification_summary "
- "FROM public.workflow_canary_evidence "
- "WHERE workflow_version_id = CAST(:id AS uuid) "
- "ORDER BY created_at DESC LIMIT 1"
- ),
- {"id": candidate_id},
- )
- .mappings()
- .one_or_none()
- )
- return dict(row) if row is not None else None
- def record_canary_verification(self, candidate_id, evidence):
- status = evidence.get("status")
- if status not in {"passed", "failed"}:
- raise ValueError("canary verification must be passed or failed")
- verified_by = _required_string(
- evidence.get("verified_by"), "canary verified_by", 200
- )
- summary = dict(evidence.get("verification_summary") or {})
- with self.engine.begin() as connection:
- updated = connection.execute(
- text(
- "UPDATE public.workflow_canary_evidence "
- "SET status = :status, sample_runs = :sample_runs, "
- "verified_by = :verified_by, "
- "verification_summary = CAST(:summary AS jsonb), "
- "verified_at = CURRENT_TIMESTAMP "
- "WHERE workflow_version_id = CAST(:version_id AS uuid) "
- "AND engine_execution_id = :execution_id "
- "AND status = 'started'"
- ),
- {
- "status": status,
- "sample_runs": int(evidence.get("sample_runs", 0)),
- "verified_by": verified_by,
- "summary": _json(summary),
- "version_id": candidate_id,
- "execution_id": evidence.get("execution_id"),
- },
- )
- if updated.rowcount != 1:
- raise ValueError("canary evidence is already finalized")
- return dict(evidence)
- def get_execution_record(self, execution_id):
- execution_id = _required_string(execution_id, "execution_id", 255)
- with self.engine.connect() as connection:
- row = (
- connection.execute(
- text(
- "SELECT run.id::text AS run_id, "
- "run.workflow_version_id::text AS workflow_version_id, "
- "run.schedule_id::text AS schedule_id, "
- "run.engine_execution_id AS execution_id, "
- "run.trigger_type, run.status, "
- "run.correlation_id::text AS correlation_id, "
- "run.attempt, run.run_metadata, "
- "version.business_domain, version.environment, "
- "(SELECT COUNT(*) FROM public.workflow_runs replay "
- "WHERE replay.parent_run_id = run.id) AS retry_count "
- "FROM public.workflow_runs run "
- "JOIN public.dataflow_workflow_versions version "
- "ON version.id = run.workflow_version_id "
- "WHERE run.engine_type = 'kestra' "
- "AND run.engine_execution_id = :execution_id"
- ),
- {"execution_id": execution_id},
- )
- .mappings()
- .one_or_none()
- )
- if row is None:
- raise KeyError(execution_id)
- return dict(row)
- def record_retry(self, execution_id, result):
- execution_id = _required_string(execution_id, "execution_id", 255)
- replay_execution_id = _required_string(
- (result or {}).get("id"), "replay execution id", 255
- )
- with self.engine.begin() as connection:
- original = (
- connection.execute(
- text(
- "SELECT id::text AS run_id, "
- "workflow_version_id::text AS workflow_version_id, "
- "schedule_id::text AS schedule_id, "
- "correlation_id::text AS correlation_id, attempt "
- "FROM public.workflow_runs "
- "WHERE engine_type = 'kestra' "
- "AND engine_execution_id = :execution_id "
- "FOR UPDATE"
- ),
- {"execution_id": execution_id},
- )
- .mappings()
- .one_or_none()
- )
- if original is None:
- raise KeyError(execution_id)
- next_attempt = connection.execute(
- text(
- "SELECT GREATEST(:original_attempt, "
- "COALESCE(MAX(attempt), 0)) + 1 "
- "FROM public.workflow_runs "
- "WHERE parent_run_id = CAST(:run_id AS uuid)"
- ),
- {
- "original_attempt": original["attempt"],
- "run_id": original["run_id"],
- },
- ).scalar_one()
- connection.execute(
- text(
- "INSERT INTO public.workflow_runs "
- "(id, workflow_version_id, schedule_id, engine_type, "
- "engine_execution_id, trigger_type, status, "
- "correlation_id, parent_run_id, attempt, run_metadata) "
- "VALUES (CAST(:id AS uuid), CAST(:version_id AS uuid), "
- "CAST(:schedule_id AS uuid), 'kestra', :execution_id, "
- "'replay', 'queued', CAST(:correlation_id AS uuid), "
- "CAST(:parent_run_id AS uuid), :attempt, "
- "CAST(:metadata AS jsonb))"
- ),
- {
- "id": new_governance_uid(),
- "version_id": original["workflow_version_id"],
- "schedule_id": original["schedule_id"],
- "execution_id": replay_execution_id,
- "correlation_id": original["correlation_id"],
- "parent_run_id": original["run_id"],
- "attempt": int(next_attempt),
- "metadata": _json({"source_execution_id": execution_id}),
- },
- )
- return dict(result)
- @staticmethod
- def _parse_timestamp(value, label):
- value = _required_string(value, label, 100)
- try:
- parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
- except ValueError as exc:
- raise ValueError(f"{label} must be an ISO 8601 timestamp") from exc
- if parsed.tzinfo is None:
- raise ValueError(f"{label} must include a timezone")
- return parsed
- def estimate_backfill_runs(self, candidate_id, start, end):
- candidate = self.get_candidate(candidate_id)
- schedule_plan = dict(candidate.get("schedule_plan") or {})
- start_at = self._parse_timestamp(start, "backfill start")
- end_at = self._parse_timestamp(end, "backfill end")
- if end_at <= start_at:
- raise ValueError("backfill end must be after start")
- try:
- timezone = ZoneInfo(schedule_plan.get("timezone") or "UTC")
- except (KeyError, ValueError) as exc:
- raise ValueError("schedule timezone is invalid") from exc
- start_local = start_at.astimezone(timezone)
- end_local = end_at.astimezone(timezone)
- total = 0
- for trigger in schedule_plan.get("triggers") or []:
- if trigger.get("type") != "cron":
- continue
- expression = _required_string(
- trigger.get("expression"), "cron expression", 100
- )
- if len(expression.split()) != 5:
- raise ValueError("backfill estimation supports five-field cron only")
- iterator = croniter(expression, start_local)
- occurrence = iterator.get_next(datetime)
- while occurrence <= end_local:
- total += 1
- if total > 1000:
- return total
- occurrence = iterator.get_next(datetime)
- if total == 0:
- raise ValueError("candidate has no cron occurrences in window")
- return total
- def record_backfill(self, candidate_id, result):
- execution_id = _required_string(
- (result or {}).get("id"), "backfill execution id", 255
- )
- estimated_runs = int((result or {}).get("estimated_runs", 0))
- if estimated_runs < 1:
- raise ValueError("estimated_runs must be positive")
- with self.engine.begin() as connection:
- schedule_id = connection.execute(
- text(
- "SELECT id::text FROM public.workflow_schedules "
- "WHERE workflow_version_id = CAST(:id AS uuid) "
- "ORDER BY created_at DESC LIMIT 1"
- ),
- {"id": candidate_id},
- ).scalar_one_or_none()
- if schedule_id is None:
- raise KeyError(candidate_id)
- connection.execute(
- text(
- "INSERT INTO public.workflow_runs "
- "(id, workflow_version_id, schedule_id, engine_type, "
- "engine_execution_id, trigger_type, status, "
- "correlation_id, attempt, run_metadata) "
- "VALUES (CAST(:id AS uuid), CAST(:version_id AS uuid), "
- "CAST(:schedule_id AS uuid), 'kestra', :execution_id, "
- "'backfill', 'queued', CAST(:correlation_id AS uuid), "
- "1, CAST(:metadata AS jsonb))"
- ),
- {
- "id": new_governance_uid(),
- "version_id": candidate_id,
- "schedule_id": schedule_id,
- "execution_id": execution_id,
- "correlation_id": new_governance_uid(),
- "metadata": _json({"estimated_runs": estimated_runs}),
- },
- )
- return dict(result)
- def _claim_operation(
- self,
- action,
- idempotency_key,
- candidate_id,
- *,
- actor_subject,
- correlation_id,
- result,
- ):
- operation_id = new_governance_uid()
- with self.engine.begin() as connection:
- inserted = connection.execute(
- text(
- "INSERT INTO public.workflow_gateway_operations "
- "(id, workflow_version_id, action, idempotency_key, status, "
- "result, actor_subject, correlation_id) "
- "VALUES (CAST(:id AS uuid), CAST(:version_id AS uuid), "
- ":action, :key, 'claimed', CAST(:result AS jsonb), "
- ":actor, CAST(:correlation_id AS uuid)) "
- "ON CONFLICT (action, idempotency_key) DO NOTHING "
- "RETURNING id::text"
- ),
- {
- "id": operation_id,
- "version_id": candidate_id,
- "action": action,
- "key": idempotency_key,
- "result": _json(result),
- "actor": actor_subject,
- "correlation_id": correlation_id,
- },
- ).scalar_one_or_none()
- if inserted is not None:
- return True, result
- previous = (
- connection.execute(
- text(
- "SELECT workflow_version_id::text AS version_id, "
- "result FROM public.workflow_gateway_operations "
- "WHERE action = :action AND idempotency_key = :key"
- ),
- {"action": action, "key": idempotency_key},
- )
- .mappings()
- .one()
- )
- if previous["version_id"] != candidate_id:
- raise ValueError("idempotency key belongs to another workflow version")
- return False, dict(previous["result"])
- def claim_promotion(
- self,
- idempotency_key,
- candidate_id,
- *,
- actor_subject,
- correlation_id,
- ):
- deployment = self.get_deployment(candidate_id)
- result = {
- **deployment,
- "candidate_id": candidate_id,
- "status": "promoted",
- }
- return self._claim_operation(
- "promote_candidate",
- idempotency_key,
- candidate_id,
- actor_subject=actor_subject,
- correlation_id=correlation_id,
- result=result,
- )
- def _complete_operation(
- self, connection, action, idempotency_key, candidate_id, result
- ):
- updated = connection.execute(
- text(
- "UPDATE public.workflow_gateway_operations "
- "SET status = 'succeeded', result = CAST(:result AS jsonb), "
- "safe_error = NULL, updated_at = CURRENT_TIMESTAMP "
- "WHERE action = :action AND idempotency_key = :key "
- "AND workflow_version_id = CAST(:version_id AS uuid) "
- "AND status = 'claimed'"
- ),
- {
- "action": action,
- "key": idempotency_key,
- "version_id": candidate_id,
- "result": _json(result),
- },
- )
- if updated.rowcount != 1:
- raise ValueError("gateway operation is not in claimed state")
- def complete_promotion(self, idempotency_key, candidate_id, result):
- with self.engine.begin() as connection:
- row = connection.execute(
- text(
- "SELECT dataflow_uid::text, environment "
- "FROM public.dataflow_workflow_versions "
- "WHERE id = CAST(:id AS uuid) FOR UPDATE"
- ),
- {"id": candidate_id},
- ).one_or_none()
- if row is None:
- raise KeyError(candidate_id)
- dataflow_uid, environment = row
- connection.execute(
- text("SELECT pg_advisory_xact_lock(hashtext(:key))"),
- {"key": f"{dataflow_uid}:{environment}"},
- )
- connection.execute(
- text(
- "UPDATE public.workflow_engine_bindings "
- "SET role = 'archived', status = 'disabled', "
- "updated_at = CURRENT_TIMESTAMP "
- "WHERE dataflow_uid = CAST(:dataflow_uid AS uuid) "
- "AND environment = :environment "
- "AND role = 'primary' AND status = 'enabled' "
- "AND workflow_version_id <> CAST(:id AS uuid)"
- ),
- {
- "dataflow_uid": dataflow_uid,
- "environment": environment,
- "id": candidate_id,
- },
- )
- connection.execute(
- text(
- "UPDATE public.dataflow_workflow_versions "
- "SET status = 'superseded', updated_at = CURRENT_TIMESTAMP "
- "WHERE dataflow_uid = CAST(:dataflow_uid AS uuid) "
- "AND environment = :environment AND status = 'active' "
- "AND id <> CAST(:id AS uuid)"
- ),
- {
- "dataflow_uid": dataflow_uid,
- "environment": environment,
- "id": candidate_id,
- },
- )
- connection.execute(
- text(
- "UPDATE public.workflow_schedules SET status = 'retired', "
- "updated_at = CURRENT_TIMESTAMP "
- "WHERE workflow_version_id IN ("
- "SELECT id FROM public.dataflow_workflow_versions "
- "WHERE dataflow_uid = CAST(:dataflow_uid AS uuid) "
- "AND environment = :environment "
- "AND id <> CAST(:id AS uuid)) "
- "AND status = 'active'"
- ),
- {
- "dataflow_uid": dataflow_uid,
- "environment": environment,
- "id": candidate_id,
- },
- )
- connection.execute(
- text(
- "UPDATE public.dataflow_workflow_versions "
- "SET status = 'active', "
- "deployment_metadata = deployment_metadata || "
- '\'{"gateway_status":"promoted"}\'::jsonb, '
- "updated_at = CURRENT_TIMESTAMP "
- "WHERE id = CAST(:id AS uuid)"
- ),
- {"id": candidate_id},
- )
- connection.execute(
- text(
- "UPDATE public.workflow_engine_bindings "
- "SET role = 'primary', status = 'enabled', "
- "updated_at = CURRENT_TIMESTAMP "
- "WHERE workflow_version_id = CAST(:id AS uuid)"
- ),
- {"id": candidate_id},
- )
- connection.execute(
- text(
- "UPDATE public.workflow_schedules "
- "SET status = 'active', updated_at = CURRENT_TIMESTAMP "
- "WHERE workflow_version_id = CAST(:id AS uuid)"
- ),
- {"id": candidate_id},
- )
- self._complete_operation(
- connection,
- "promote_candidate",
- idempotency_key,
- candidate_id,
- result,
- )
- return result
- def release_promotion(self, idempotency_key, candidate_id):
- self._release_operation("promote_candidate", idempotency_key, candidate_id)
- def mark_paused(self, candidate_id):
- with self.engine.begin() as connection:
- connection.execute(
- text(
- "UPDATE public.workflow_engine_bindings "
- "SET status = 'disabled', updated_at = CURRENT_TIMESTAMP "
- "WHERE workflow_version_id = CAST(:id AS uuid)"
- ),
- {"id": candidate_id},
- )
- connection.execute(
- text(
- "UPDATE public.workflow_schedules "
- "SET status = 'paused', updated_at = CURRENT_TIMESTAMP "
- "WHERE workflow_version_id = CAST(:id AS uuid)"
- ),
- {"id": candidate_id},
- )
- updated = connection.execute(
- text(
- "UPDATE public.dataflow_workflow_versions "
- "SET deployment_metadata = deployment_metadata || "
- '\'{"gateway_status":"paused"}\'::jsonb, '
- "updated_at = CURRENT_TIMESTAMP "
- "WHERE id = CAST(:id AS uuid)"
- ),
- {"id": candidate_id},
- )
- if updated.rowcount != 1:
- raise KeyError(candidate_id)
- def get_previous_deployment(self, candidate_id):
- with self.engine.connect() as connection:
- row = (
- connection.execute(
- text(
- "SELECT previous.id::text AS candidate_id, "
- "previous.deployment_metadata "
- "FROM public.dataflow_workflow_versions current "
- "JOIN LATERAL ("
- "SELECT id, deployment_metadata "
- "FROM public.dataflow_workflow_versions candidate "
- "WHERE candidate.dataflow_uid = current.dataflow_uid "
- "AND candidate.environment = current.environment "
- "AND candidate.engine_type = 'kestra' "
- "AND candidate.version_no < current.version_no "
- "ORDER BY candidate.version_no DESC LIMIT 1"
- ") previous ON TRUE "
- "WHERE current.id = CAST(:id AS uuid)"
- ),
- {"id": candidate_id},
- )
- .mappings()
- .one_or_none()
- )
- if row is None:
- raise ValueError("previous deployment is not available")
- deployment = dict(row["deployment_metadata"] or {}).get("deployment")
- if not isinstance(deployment, dict):
- raise ValueError("previous deployment is incomplete")
- return {**deployment, "candidate_id": row["candidate_id"]}
- def claim_rollback(
- self,
- idempotency_key,
- candidate_id,
- *,
- actor_subject,
- correlation_id,
- ):
- previous = self.get_previous_deployment(candidate_id)
- result = {
- "candidate_id": candidate_id,
- "status": "rolled_back",
- "active_candidate_id": previous["candidate_id"],
- }
- return self._claim_operation(
- "rollback_to_previous_version",
- idempotency_key,
- candidate_id,
- actor_subject=actor_subject,
- correlation_id=correlation_id,
- result=result,
- )
- def complete_rollback(self, idempotency_key, candidate_id, result):
- previous = self.get_previous_deployment(candidate_id)
- previous_id = previous["candidate_id"]
- with self.engine.begin() as connection:
- connection.execute(
- text(
- "UPDATE public.dataflow_workflow_versions "
- "SET status = 'superseded', "
- "deployment_metadata = deployment_metadata || "
- '\'{"gateway_status":"rolled_back"}\'::jsonb, '
- "updated_at = CURRENT_TIMESTAMP "
- "WHERE id = CAST(:id AS uuid)"
- ),
- {"id": candidate_id},
- )
- connection.execute(
- text(
- "UPDATE public.workflow_engine_bindings "
- "SET role = 'archived', status = 'disabled', "
- "updated_at = CURRENT_TIMESTAMP "
- "WHERE workflow_version_id = CAST(:id AS uuid)"
- ),
- {"id": candidate_id},
- )
- connection.execute(
- text(
- "UPDATE public.workflow_schedules "
- "SET status = 'retired', updated_at = CURRENT_TIMESTAMP "
- "WHERE workflow_version_id = CAST(:id AS uuid)"
- ),
- {"id": candidate_id},
- )
- connection.execute(
- text(
- "UPDATE public.dataflow_workflow_versions "
- "SET status = 'active', "
- "deployment_metadata = deployment_metadata || "
- '\'{"gateway_status":"promoted"}\'::jsonb, '
- "updated_at = CURRENT_TIMESTAMP "
- "WHERE id = CAST(:id AS uuid)"
- ),
- {"id": previous_id},
- )
- connection.execute(
- text(
- "UPDATE public.workflow_engine_bindings "
- "SET role = 'primary', status = 'enabled', "
- "updated_at = CURRENT_TIMESTAMP "
- "WHERE workflow_version_id = CAST(:id AS uuid)"
- ),
- {"id": previous_id},
- )
- connection.execute(
- text(
- "UPDATE public.workflow_schedules "
- "SET status = 'active', updated_at = CURRENT_TIMESTAMP "
- "WHERE workflow_version_id = CAST(:id AS uuid)"
- ),
- {"id": previous_id},
- )
- self._complete_operation(
- connection,
- "rollback_to_previous_version",
- idempotency_key,
- candidate_id,
- result,
- )
- return result
- def release_rollback(self, idempotency_key, candidate_id):
- self._release_operation(
- "rollback_to_previous_version", idempotency_key, candidate_id
- )
- def _release_operation(self, action, idempotency_key, candidate_id):
- with self.engine.begin() as connection:
- connection.execute(
- text(
- "DELETE FROM public.workflow_gateway_operations "
- "WHERE action = :action AND idempotency_key = :key "
- "AND workflow_version_id = CAST(:version_id AS uuid) "
- "AND status = 'claimed'"
- ),
- {
- "action": action,
- "key": idempotency_key,
- "version_id": candidate_id,
- },
- )
- class PostgresContextRepository:
- """Secret-free PostgreSQL read model for the Context MCP server."""
- def __init__(self, engine, *, max_concurrency=10, clock=None):
- self.engine = engine
- self.max_concurrency = int(max_concurrency)
- self.clock = clock or (lambda: datetime.now(UTC))
- if self.max_concurrency < 1 or self.max_concurrency > 1000:
- raise ValueError("context max concurrency is invalid")
- @staticmethod
- def _dataflow_row(row):
- if row is None:
- return None
- value = dict(row)
- spec = dict(value.pop("workflow_spec") or {})
- return {
- "uid": value["uid"],
- "name": spec.get("name"),
- "description": spec.get("description"),
- "business_domain": value.get("business_domain"),
- "status": value.get("status"),
- }
- def list_dataflows(self):
- with self.engine.connect() as connection:
- rows = (
- connection.execute(
- text(
- "SELECT DISTINCT ON (dataflow_uid) "
- "dataflow_uid::text AS uid, business_domain, status, "
- "workflow_spec "
- "FROM public.dataflow_workflow_versions "
- "WHERE engine_type = 'kestra' "
- "ORDER BY dataflow_uid, version_no DESC"
- )
- )
- .mappings()
- .all()
- )
- return [self._dataflow_row(row) for row in rows]
- def describe_dataflow(self, dataflow_uid):
- with self.engine.connect() as connection:
- row = (
- connection.execute(
- text(
- "SELECT dataflow_uid::text AS uid, business_domain, "
- "status, workflow_spec "
- "FROM public.dataflow_workflow_versions "
- "WHERE dataflow_uid = CAST(:uid AS uuid) "
- "AND engine_type = 'kestra' "
- "ORDER BY version_no DESC LIMIT 1"
- ),
- {"uid": dataflow_uid},
- )
- .mappings()
- .one_or_none()
- )
- if row is None:
- raise KeyError(dataflow_uid)
- return self._dataflow_row(row)
- def _latest_spec(self, dataflow_uid):
- with self.engine.connect() as connection:
- row = (
- connection.execute(
- text(
- "SELECT workflow_spec, business_domain "
- "FROM public.dataflow_workflow_versions "
- "WHERE dataflow_uid = CAST(:uid AS uuid) "
- "AND engine_type = 'kestra' "
- "ORDER BY version_no DESC LIMIT 1"
- ),
- {"uid": dataflow_uid},
- )
- .mappings()
- .one_or_none()
- )
- if row is None:
- raise KeyError(dataflow_uid)
- return dict(row["workflow_spec"] or {}), row["business_domain"]
- def get_dataflow_dependencies(self, dataflow_uid):
- spec, domain = self._latest_spec(dataflow_uid)
- dependencies = []
- seen = set()
- for node in spec.get("nodes") or []:
- if node.get("type") != "subflow":
- continue
- dependency_uid = (node.get("config") or {}).get("dataflow_uid")
- if not dependency_uid or dependency_uid in seen:
- continue
- seen.add(dependency_uid)
- try:
- dependency = self.describe_dataflow(dependency_uid)
- except (KeyError, ValueError):
- continue
- dependencies.append(
- {
- "uid": dependency["uid"],
- "name": dependency["name"],
- "relation": "subflow",
- "business_domain": domain,
- }
- )
- return dependencies
- def get_data_lineage(self, dataflow_uid, depth):
- del depth
- spec, _ = self._latest_spec(dataflow_uid)
- nodes = {node.get("id"): node for node in spec.get("nodes") or []}
- return [
- {
- "from_uid": (
- nodes.get(edge.get("from"), {}).get("data_source_uid")
- or f"{dataflow_uid}:{edge.get('from')}"
- ),
- "to_uid": (
- nodes.get(edge.get("to"), {}).get("data_source_uid")
- or f"{dataflow_uid}:{edge.get('to')}"
- ),
- "relation": "workflow_edge",
- "description": edge.get("condition"),
- }
- for edge in spec.get("edges") or []
- ]
- def list_datasource_capabilities(self, business_domain):
- with self.engine.connect() as connection:
- rows = (
- connection.execute(
- text(
- "WITH latest AS ("
- "SELECT DISTINCT ON (dataflow_uid) workflow_spec "
- "FROM public.dataflow_workflow_versions "
- "WHERE engine_type = 'kestra' "
- "AND business_domain = :domain "
- "ORDER BY dataflow_uid, version_no DESC"
- "), nodes AS ("
- "SELECT jsonb_array_elements("
- "COALESCE(workflow_spec->'nodes', '[]'::jsonb)) AS node "
- "FROM latest"
- "), sources AS ("
- "SELECT node->>'data_source_uid' AS uid, "
- "array_agg(DISTINCT COALESCE("
- "node->>'purpose', 'read')) AS purposes "
- "FROM nodes WHERE node ? 'data_source_uid' "
- "GROUP BY node->>'data_source_uid'"
- ") SELECT sources.uid, sources.purposes, "
- "credentials.status AS credential_status "
- "FROM sources LEFT JOIN "
- "public.datasource_credentials credentials "
- "ON credentials.data_source_uid = "
- "CAST(sources.uid AS uuid) "
- "AND credentials.status = 'active'"
- ),
- {"domain": business_domain},
- )
- .mappings()
- .all()
- )
- return [
- {
- "uid": row["uid"],
- "type": "external",
- "purposes": list(row["purposes"] or []),
- "health": (
- "configured"
- if row["credential_status"] == "active"
- else "credential_unavailable"
- ),
- "capacity": {"available": row["credential_status"] == "active"},
- }
- for row in rows
- ]
- def get_datasource_pool_health(self, data_source_uid):
- with self.engine.connect() as connection:
- configured = bool(
- connection.execute(
- text(
- "SELECT EXISTS("
- "SELECT 1 FROM public.datasource_credentials "
- "WHERE data_source_uid = CAST(:uid AS uuid) "
- "AND status = 'active')"
- ),
- {"uid": data_source_uid},
- ).scalar_one()
- )
- return {
- "uid": data_source_uid,
- "health": "configured" if configured else "credential_unavailable",
- "capacity": {"available": configured},
- }
- def get_execution_history(self, dataflow_uid, limit, days):
- with self.engine.connect() as connection:
- rows = (
- connection.execute(
- text(
- "SELECT run.status, run.correlation_id::text "
- "AS correlation_id, run.started_at, run.finished_at, "
- "run.run_metadata->>'safe_error' AS safe_error "
- "FROM public.workflow_runs run "
- "JOIN public.dataflow_workflow_versions version "
- "ON version.id = run.workflow_version_id "
- "WHERE version.dataflow_uid = CAST(:uid AS uuid) "
- "AND run.created_at >= "
- "CURRENT_TIMESTAMP - make_interval(days => :days) "
- "ORDER BY run.created_at DESC LIMIT :limit"
- ),
- {
- "uid": dataflow_uid,
- "days": int(days),
- "limit": int(limit),
- },
- )
- .mappings()
- .all()
- )
- return [
- {
- **dict(row),
- "started_at": (
- row["started_at"].isoformat()
- if row["started_at"] is not None
- else None
- ),
- "finished_at": (
- row["finished_at"].isoformat()
- if row["finished_at"] is not None
- else None
- ),
- }
- for row in rows
- ]
- def get_sla_constraints(self, dataflow_uid):
- with self.engine.connect() as connection:
- plan = connection.execute(
- text(
- "SELECT schedule_plan "
- "FROM public.dataflow_workflow_versions "
- "WHERE dataflow_uid = CAST(:uid AS uuid) "
- "AND engine_type = 'kestra' "
- "ORDER BY version_no DESC LIMIT 1"
- ),
- {"uid": dataflow_uid},
- ).scalar_one_or_none()
- if plan is None:
- raise KeyError(dataflow_uid)
- plan = dict(plan)
- return {
- "timezone": plan.get("timezone"),
- "max_duration_seconds": plan.get("timeout_seconds"),
- }
- def estimate_schedule_capacity(self, business_domain, schedule_plan):
- with self.engine.connect() as connection:
- running = int(
- connection.execute(
- text(
- "SELECT COUNT(*) FROM public.workflow_runs run "
- "JOIN public.dataflow_workflow_versions version "
- "ON version.id = run.workflow_version_id "
- "WHERE version.business_domain = :domain "
- "AND run.status IN ('queued', 'running')"
- ),
- {"domain": business_domain},
- ).scalar_one()
- )
- available = max(0, self.max_concurrency - running)
- required = int(schedule_plan.get("max_concurrency", 1))
- return {
- "feasible": required <= available,
- "required_slots": required,
- "available_slots": available,
- "warnings": (
- []
- if required <= available
- else ["requested concurrency exceeds current capacity"]
- ),
- }
- def simulate_schedule(self, business_domain, schedule_plan):
- del business_domain
- now = self.clock()
- timezone = ZoneInfo(schedule_plan.get("timezone") or "UTC")
- local_now = now.astimezone(timezone)
- next_runs = []
- warnings = []
- for trigger in schedule_plan.get("triggers") or []:
- kind = trigger.get("type")
- if kind == "cron":
- expression = trigger.get("expression", "")
- if len(str(expression).split()) != 5:
- warnings.append("simulation supports five-field cron only")
- continue
- iterator = croniter(expression, local_now)
- next_runs.extend(
- iterator.get_next(datetime).isoformat() for _ in range(5)
- )
- elif kind == "at":
- next_runs.append(str(trigger.get("at")))
- elif kind in {"manual", "event"}:
- warnings.append(f"{kind} trigger has no deterministic next run")
- return {
- "next_runs": sorted(next_runs)[:20],
- "warnings": warnings,
- }
- def compare_execution_results(self, baseline_execution_id, candidate_execution_id):
- with self.engine.connect() as connection:
- rows = (
- connection.execute(
- text(
- "SELECT engine_execution_id, status, started_at, "
- "finished_at, run_metadata "
- "FROM public.workflow_runs "
- "WHERE engine_type = 'kestra' "
- "AND engine_execution_id IN (:baseline, :candidate)"
- ),
- {
- "baseline": baseline_execution_id,
- "candidate": candidate_execution_id,
- },
- )
- .mappings()
- .all()
- )
- by_id = {row["engine_execution_id"]: dict(row) for row in rows}
- if baseline_execution_id not in by_id or candidate_execution_id not in by_id:
- raise KeyError("execution comparison input is unavailable")
- baseline = by_id[baseline_execution_id]
- candidate = by_id[candidate_execution_id]
- def duration(row):
- if row["started_at"] is None or row["finished_at"] is None:
- return None
- return (row["finished_at"] - row["started_at"]).total_seconds()
- baseline_duration = duration(baseline)
- candidate_duration = duration(candidate)
- baseline_metadata = dict(baseline["run_metadata"] or {})
- candidate_metadata = dict(candidate["run_metadata"] or {})
- baseline_hash = baseline_metadata.get("output_hash")
- candidate_hash = candidate_metadata.get("output_hash")
- hash_match = (
- baseline_hash == candidate_hash
- if baseline_hash is not None and candidate_hash is not None
- else None
- )
- duration_delta = (
- candidate_duration - baseline_duration
- if baseline_duration is not None and candidate_duration is not None
- else None
- )
- equivalent = (
- baseline["status"] == "success"
- and candidate["status"] == "success"
- and hash_match is not False
- )
- return {
- "equivalent": equivalent,
- "metrics": {
- "duration_delta_seconds": duration_delta,
- "output_hash_match": hash_match,
- },
- "differences": (
- [] if equivalent else ["execution status or output hash differs"]
- ),
- }
- class PostgresAuditSink:
- def __init__(self, engine):
- self.engine = engine
- def record(self, event):
- detail = dict(event.get("detail") or {})
- candidate_id = detail.get("candidate_id")
- try:
- workflow_version_id = str(uuid.UUID(candidate_id)) if candidate_id else None
- correlation_id = str(uuid.UUID(event["correlation_id"]))
- except (TypeError, ValueError) as exc:
- raise ValueError("audit identifiers must be UUIDs") from exc
- decision = event.get("decision")
- if decision not in {
- "allowed",
- "rejected",
- "failed",
- "recorded",
- "idempotent_replay",
- }:
- raise ValueError("unsupported audit decision")
- bounded_detail = {
- **detail,
- "roles": list(event.get("roles") or [])[:20],
- "business_domain": event.get("business_domain"),
- "environment": event.get("environment"),
- }
- encoded_detail = _json(bounded_detail)
- if len(encoded_detail.encode("utf-8")) > 1048576:
- raise ValueError("audit detail exceeds size limit")
- def optional_string(name, maximum):
- value = event.get(name)
- return (
- _required_string(value, f"audit {name}", maximum)
- if value is not None
- else None
- )
- def optional_hash(value, label):
- if value is None:
- return None
- normalized = _required_string(value, label, 64).lower()
- if len(normalized) != 64 or any(
- character not in "0123456789abcdef" for character in normalized
- ):
- raise ValueError(f"{label} must be a SHA-256 hex digest")
- return normalized
- schema_version = _required_string(
- event.get("schema_version", "v53"),
- "audit schema_version",
- 40,
- )
- context_hash = optional_hash(
- event.get("context_hash"),
- "audit context_hash",
- )
- candidate_hash = optional_hash(
- detail.get("workflow_hash"),
- "audit candidate_hash",
- )
- with self.engine.begin() as connection:
- connection.execute(
- text(
- "INSERT INTO public.workflow_plan_audits "
- "(id, workflow_version_id, actor_uid, actor_subject, action, "
- "model_provider, model_name, prompt_version, schema_version, "
- "context_hash, candidate_hash, decision, decision_detail, "
- "correlation_id) "
- "VALUES (CAST(:id AS uuid), "
- "CAST(:workflow_version_id AS uuid), NULL, :actor_subject, "
- ":action, :model_provider, :model_name, :prompt_version, "
- ":schema_version, :context_hash, :candidate_hash, :decision, "
- "CAST(:detail AS jsonb), CAST(:correlation_id AS uuid))"
- ),
- {
- "id": new_governance_uid(),
- "workflow_version_id": workflow_version_id,
- "actor_subject": _required_string(
- event.get("subject"), "audit subject", 200
- ),
- "action": _required_string(event.get("action"), "audit action", 80),
- "model_provider": optional_string("model_provider", 80),
- "model_name": optional_string("model_name", 120),
- "prompt_version": optional_string("prompt_version", 80),
- "schema_version": schema_version,
- "context_hash": context_hash,
- "candidate_hash": candidate_hash,
- "decision": decision,
- "detail": encoded_detail,
- "correlation_id": correlation_id,
- },
- )
|