Publish curated Unraid-AppOps-Gateway source from 2e53f628d284213b90ca2f9c5e121671f5933574
This commit is contained in:
1 parent
4f9c5a1106
commit
2facb8de34
13 files changed
+3868
-1749
No files matched your search
@@ -33,8 +33,7 @@ _TERMINAL = {"preflight_failed", "succeeded", "failed", "rolled_back", "rollback
|
||||
_BROKER_EVIDENCE_FIELDS = {"request", "repository", "revision", "status"}
|
||||
_OIDC_EVIDENCE_FIELDS = {"success", "status", "applicationId", "providerId", "clientSecretCreated"}
|
||||
_PROXY_EVIDENCE_FIELDS = {"success", "status", "hostId", "hostname"}
|
||||
_AUTO_ROLLBACK_REPOSITORY = "jens/itworx_mcp_hub"
|
||||
_AUTO_ROLLBACK_APPLICATION_ID = "itworx-mcp-hub"
|
||||
_CANONICAL_HUB_REPOSITORY = "jens/itworx_mcp_hub"
|
||||
|
||||
|
||||
class DeploymentControlError(ValueError):
|
||||
@@ -312,123 +311,23 @@ class DeploymentControlPlane:
|
||||
**dict(job.evidence_json or {}),
|
||||
"postApplyVerification": {"status": "failed", "detail": message},
|
||||
}
|
||||
if (
|
||||
plan.repository.casefold() != _AUTO_ROLLBACK_REPOSITORY
|
||||
or plan.application_id != _AUTO_ROLLBACK_APPLICATION_ID
|
||||
):
|
||||
job.status = "failed"
|
||||
job.stage = "external_integration_failed"
|
||||
job.result_summary = message
|
||||
job.completed_at = utc_now()
|
||||
return
|
||||
self._attempt_auto_rollback(session, job, plan, reason=message)
|
||||
|
||||
def _attempt_auto_rollback(
|
||||
self,
|
||||
session: Session,
|
||||
job: DeploymentJob,
|
||||
plan: DeploymentPlanV1,
|
||||
*,
|
||||
reason: str,
|
||||
) -> None:
|
||||
"""Restore only the controller-retained prior receipt after verification failure."""
|
||||
policy = evaluate_policy(
|
||||
session,
|
||||
action="deployment.rollback",
|
||||
target=plan.application_id,
|
||||
source="manual",
|
||||
)
|
||||
exact_policies = session.exec(
|
||||
select(Policy).where(
|
||||
(Policy.action == "deployment.rollback")
|
||||
& (Policy.target == plan.application_id)
|
||||
)
|
||||
).all()
|
||||
exact_policy = exact_policies[0] if len(exact_policies) == 1 else None
|
||||
policy_is_exact_and_strict = (
|
||||
exact_policy is not None
|
||||
and exact_policy.manual == "allowed"
|
||||
and exact_policy.voice == "blocked"
|
||||
and exact_policy.risk == "high"
|
||||
and policy.get("policy_id") == exact_policy.id
|
||||
)
|
||||
if policy.get("decision") != "allowed" or not policy_is_exact_and_strict:
|
||||
job.status = "failed"
|
||||
job.stage = "external_integration_failed"
|
||||
if plan.repository.casefold() == _CANONICAL_HUB_REPOSITORY:
|
||||
job.evidence_json = {
|
||||
**dict(job.evidence_json or {}),
|
||||
"autoRollback": {"status": "blocked", "reason": "exact-target-policy"},
|
||||
"recovery": {
|
||||
"status": "manual_required",
|
||||
"reason": "hub_data_aware_recovery_required",
|
||||
},
|
||||
}
|
||||
job.status = "failed"
|
||||
job.stage = "external_integration_failed"
|
||||
job.result_summary = reason
|
||||
job.completed_at = utc_now()
|
||||
return
|
||||
|
||||
secret = get_secret(session, plan.broker_secret_ref)
|
||||
if not secret:
|
||||
job.evidence_json = {
|
||||
**dict(job.evidence_json or {}),
|
||||
"autoRollback": {"status": "blocked", "reason": "controller-secret"},
|
||||
}
|
||||
job.status = "failed"
|
||||
job.stage = "external_integration_failed"
|
||||
job.result_summary = reason
|
||||
job.completed_at = utc_now()
|
||||
return
|
||||
|
||||
rollback_key = f"auto-rollback:{job.id}"
|
||||
rollback = self.broker.rollback(plan, secret=secret, idempotency_key=rollback_key)
|
||||
rollback_status = str(rollback.get("status") or "failed")
|
||||
rollback_request = str(rollback.get("request") or "") or None
|
||||
rollback_evidence = {
|
||||
key: rollback[key]
|
||||
for key in _BROKER_EVIDENCE_FIELDS
|
||||
if key in rollback
|
||||
}
|
||||
rollback_evidence["status"] = rollback_status
|
||||
job.evidence_json = {
|
||||
**dict(job.evidence_json or {}),
|
||||
"autoRollback": rollback_evidence,
|
||||
}
|
||||
if rollback_request:
|
||||
job.broker_request_id = rollback_request
|
||||
|
||||
if rollback_status == "rolled_back":
|
||||
job.status = "rolled_back"
|
||||
job.stage = "auto_rollback_verified"
|
||||
job.result_summary = (
|
||||
"Post-apply verification failed; the controller restored the prior verified deployment."
|
||||
f"{message} Automatic image-only rollback is disabled for MCP Hub; "
|
||||
"data-aware recovery is required."
|
||||
)
|
||||
job.completed_at = utc_now()
|
||||
elif rollback_status in {"accepted", "already_queued", "running"}:
|
||||
if not rollback_request:
|
||||
job.status = "failed"
|
||||
job.stage = "auto_rollback_failed"
|
||||
job.evidence_json = {
|
||||
**dict(job.evidence_json or {}),
|
||||
"autoRollback": {
|
||||
**rollback_evidence,
|
||||
"reason": "missing-controller-request",
|
||||
},
|
||||
}
|
||||
job.result_summary = (
|
||||
"Post-apply verification failed and automatic rollback "
|
||||
"returned no pollable controller request."
|
||||
)
|
||||
job.completed_at = utc_now()
|
||||
return
|
||||
job.status = "rolling_back"
|
||||
job.stage = "auto_rollback_queued"
|
||||
job.result_summary = (
|
||||
"Post-apply verification failed; automatic rollback is in progress."
|
||||
)
|
||||
job.completed_at = None
|
||||
else:
|
||||
job.status = "failed"
|
||||
job.stage = "auto_rollback_failed"
|
||||
job.result_summary = (
|
||||
"Post-apply verification failed and the controller did not verify rollback."
|
||||
)
|
||||
job.completed_at = utc_now()
|
||||
job.result_summary = message
|
||||
job.completed_at = utc_now()
|
||||
|
||||
def execute(
|
||||
self,
|
||||
@@ -438,6 +337,13 @@ class DeploymentControlPlane:
|
||||
idempotency_key: str,
|
||||
operation: Literal["preflight", "apply", "rollback"],
|
||||
) -> dict[str, Any]:
|
||||
if (
|
||||
operation in {"apply", "rollback"}
|
||||
and plan.repository.casefold() == _CANONICAL_HUB_REPOSITORY
|
||||
):
|
||||
raise DeploymentControlError(
|
||||
"MCP Hub apply and rollback require the signed protected push workflow"
|
||||
)
|
||||
job = self.create_job(session, plan=plan, idempotency_key=idempotency_key, operation=operation)
|
||||
if job.status != "queued":
|
||||
return self.serialize_job(job, replay=True)
|
||||
|
||||
Reference in new issue
Block a user