Publish curated Unraid-AppOps-Gateway source from 4f99d8f6ea4fb8ae105b83570344b25fa05c31fd
This commit is contained in:
1 parent
27a33107a3
commit
f50d6e8c2c
14 files changed
+1178
-21
No files matched your search
@@ -1,3 +1,3 @@
|
||||
# Public source export
|
||||
|
||||
This source snapshot was generated from the private canonical repository at revision `f8f5bf0a29159bd69accd7e58f6427ec32140640`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
|
||||
This source snapshot was generated from the private canonical repository at revision `4f99d8f6ea4fb8ae105b83570344b25fa05c31fd`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
|
||||
+37
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"sourceRevision": "f8f5bf0a29159bd69accd7e58f6427ec32140640",
|
||||
"sourceRevision": "4f99d8f6ea4fb8ae105b83570344b25fa05c31fd",
|
||||
"files": [
|
||||
{
|
||||
"path": ".dockerignore",
|
||||
@@ -124,8 +124,8 @@
|
||||
},
|
||||
{
|
||||
"path": "backend/app/api/routes_deployments.py",
|
||||
"sha256": "8a7a2503c38fd1edea002cbb780844b6907830edf88bd74e9f4c6bbbdf94a43e",
|
||||
"bytes": 4241
|
||||
"sha256": "0441f74d508dde2ca0641cae32b8a7f386496c8f6f0f2df80ec619a6409bc9b3",
|
||||
"bytes": 4955
|
||||
},
|
||||
{
|
||||
"path": "backend/app/api/routes_enrollments.py",
|
||||
@@ -404,8 +404,8 @@
|
||||
},
|
||||
{
|
||||
"path": "backend/app/mcp_server.py",
|
||||
"sha256": "8eb186a7264fd602b019a06e49eae99d91dcce157ae538b30afc7e48320f8df8",
|
||||
"bytes": 34436
|
||||
"sha256": "75779ae0a366cca415c04fc6fe24f38d99c338939874acf17be6616cc12b4f25",
|
||||
"bytes": 35237
|
||||
},
|
||||
{
|
||||
"path": "backend/app/schemas/ai_remediation.py",
|
||||
@@ -432,6 +432,11 @@
|
||||
"sha256": "cb81e13c924626cfd8d6c91a240d6299b92798e0f760a19aa906d021736e1f0b",
|
||||
"bytes": 334
|
||||
},
|
||||
{
|
||||
"path": "backend/app/schemas/forge_bootstrap.py",
|
||||
"sha256": "12db5aa43594bbcc9e4306a173769afdeabb86d364e0939e637b80775ea73629",
|
||||
"bytes": 1208
|
||||
},
|
||||
{
|
||||
"path": "backend/app/services/__init__.py",
|
||||
"sha256": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b",
|
||||
@@ -489,8 +494,8 @@
|
||||
},
|
||||
{
|
||||
"path": "backend/app/services/controller_artifact_promoter.py",
|
||||
"sha256": "a193123dd59de03f19cc14e51e9551e0ab316b741cc8b417ed530f86c539dc13",
|
||||
"bytes": 16370
|
||||
"sha256": "6e740205449af2adc1fbbf9adf15989532fc6a92e252c8dcc81e7b4d644e58cd",
|
||||
"bytes": 16389
|
||||
},
|
||||
{
|
||||
"path": "backend/app/services/controller_bootstrap_import.py",
|
||||
@@ -499,8 +504,8 @@
|
||||
},
|
||||
{
|
||||
"path": "backend/app/services/deployment_broker.py",
|
||||
"sha256": "c8e58f76b1f245553c1a419e33ad919e3366cc54b7f9bdcc8698d46abbff0c6b",
|
||||
"bytes": 22786
|
||||
"sha256": "2feb43b65684f3f6a2dba688a47c18117d907ca34426919270f67e4de42db453",
|
||||
"bytes": 23266
|
||||
},
|
||||
{
|
||||
"path": "backend/app/services/deployment_control_plane.py",
|
||||
@@ -532,6 +537,11 @@
|
||||
"sha256": "36cf235e02e4289b7b1851bb6240584bf8d6f6fbb9b2ae22ac521ec8012ba025",
|
||||
"bytes": 2538
|
||||
},
|
||||
{
|
||||
"path": "backend/app/services/forge_bootstrap_preflight.py",
|
||||
"sha256": "b5ced0a85cc1585386fec21cc1cfa5bc88b5ccecf211beff93f99d79736f413f",
|
||||
"bytes": 14808
|
||||
},
|
||||
{
|
||||
"path": "backend/app/services/gitea_project_auto_registration.py",
|
||||
"sha256": "0da4469d2b75d1d6c74704b9c402348ce246f3325c1726bd50dcf13b378e060b",
|
||||
@@ -819,8 +829,8 @@
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_controller_artifact_promoter.py",
|
||||
"sha256": "590a7155052851e767a048d15f7f6624070a68c020e90b32ed8801ceab11e972",
|
||||
"bytes": 3672
|
||||
"sha256": "671f62d61ec1281644d0b49500141dd4a50ef75b7bf82accbe80dd0bf16d04ec",
|
||||
"bytes": 3922
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_controller_bootstrap_import_host.py",
|
||||
@@ -834,13 +844,13 @@
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_controller_hub_release_volume.py",
|
||||
"sha256": "0dddc78484ebc4a09bc75e3e71cdca5f48cb3b6afba91c3b36aa16703266d6e0",
|
||||
"bytes": 21814
|
||||
"sha256": "d527b5ac287a02bc5e9f4c89ddf69638b6f8a12c5a34ad2e65db2721b8e05303",
|
||||
"bytes": 28619
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_controller_root_procedure_contract.py",
|
||||
"sha256": "1552a883e13a66e0f154481c6f3cf2b162ad867d6bbaaed7a84cedd45c5935ac",
|
||||
"bytes": 2793
|
||||
"sha256": "ff2c7b80c3bc71b1ab06fc5637fce0c2c73df78e6d6123a47a34b3036f3caa8c",
|
||||
"bytes": 3299
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_controller_state_reconciliation.py",
|
||||
@@ -897,6 +907,16 @@
|
||||
"sha256": "ddd08c246eb0719556f307691ca231093584dc4b85e78bb849bee83ccbde8b1f",
|
||||
"bytes": 8374
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_forge_bootstrap_controller.py",
|
||||
"sha256": "442a57109bc2dc0e4d4b9eb50c84ae8bac5685f94c9ec41fcfdadd0911352e44",
|
||||
"bytes": 10829
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_forge_bootstrap_preflight.py",
|
||||
"sha256": "b9ef73cd20bcdb99eb4afe42d26604397e82258f24f4b5dbc25d182c1c6a95da",
|
||||
"bytes": 8518
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_frontend_file_boundary.py",
|
||||
"sha256": "beee4046574b89f6543d7a07040b983cfd7a1e561dbd9db4f81c9cd66ab77c32",
|
||||
@@ -979,8 +999,8 @@
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_mcp_rag_n8n_integrations.py",
|
||||
"sha256": "0b2891cd4b545ced2e737e4caa1506ceedecb4600247223b3490749213348fba",
|
||||
"bytes": 48408
|
||||
"sha256": "89f557049d6e2c65fada94321503df65bf7c82bd567038241ea3bdad7c866ed1",
|
||||
"bytes": 50584
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_media_codec_dashboard_deployment_reconciliation.py",
|
||||
|
||||
@@ -15,11 +15,12 @@ from sqlmodel import Session
|
||||
from ..core.security import require_operator_request
|
||||
from ..db.session import get_session
|
||||
from ..schemas.deployment import DeploymentPlanV1
|
||||
from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
|
||||
from ..services.deployment_broker import DeploymentBrokerClient
|
||||
from ..services.deployment_control_plane import (
|
||||
DeploymentControlError,
|
||||
DeploymentControlPlane,
|
||||
)
|
||||
from ..services.deployment_broker import DeploymentBrokerClient
|
||||
|
||||
|
||||
def _require_deployment_operator(request: Request) -> str:
|
||||
@@ -95,6 +96,27 @@ def preflight_workmesh_promotion(
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/forge-bootstrap/preflight")
|
||||
def preflight_forge_bootstrap(
|
||||
payload: ForgeBootstrapPreflightV1,
|
||||
session: DeploymentSession,
|
||||
idempotency_key: DeploymentIdempotencyKey = None,
|
||||
):
|
||||
from ..services.forge_bootstrap_preflight import (
|
||||
ForgeBootstrapPreflightError,
|
||||
ForgeBootstrapPreflightService,
|
||||
)
|
||||
|
||||
try:
|
||||
return ForgeBootstrapPreflightService().preflight(
|
||||
session,
|
||||
payload,
|
||||
idempotency_key=_idempotency_header(idempotency_key),
|
||||
)
|
||||
except ForgeBootstrapPreflightError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/workmesh/promotions/{promotion_id}")
|
||||
def get_workmesh_promotion(promotion_id: str, session: DeploymentSession):
|
||||
from ..services.workmesh_promotion import WorkMeshPromotionService
|
||||
|
||||
@@ -27,6 +27,7 @@ from .db.models import AppSetting, AuditEvent
|
||||
from .db.session import engine
|
||||
from .schemas.deployment import DeploymentPlanV1
|
||||
from .schemas.enrollment import EnrollmentPlanV1
|
||||
from .schemas.forge_bootstrap import ForgeBootstrapPreflightV1
|
||||
from .services import knowledge_service, mcp_operations, safety_center_service
|
||||
from .services.action_execution import mcp_execution_supported
|
||||
from .services.action_registry import ACTION_RISK, ACTIONS, action_capability
|
||||
@@ -653,8 +654,26 @@ def cancel_job(jobId: str) -> dict[str, Any]:
|
||||
@appops_mcp.tool(name="deployment.preflight", annotations=_READ_ONLY_TOOL)
|
||||
@_tracked("deployment.preflight")
|
||||
def deployment_preflight(plan: dict[str, Any], idempotencyKey: str) -> dict[str, Any]:
|
||||
"""Validate one strict DeploymentPlanV1 without touching application runtime state."""
|
||||
"""Validate a strict V1 plan or fixed Forge bootstrap profile read-only."""
|
||||
try:
|
||||
if plan.get("api_version") == "itworx.forge-bootstrap-preflight/v1":
|
||||
from .services.forge_bootstrap_preflight import (
|
||||
ForgeBootstrapPreflightService,
|
||||
)
|
||||
|
||||
profile = ForgeBootstrapPreflightV1.model_validate(plan)
|
||||
with Session(engine) as session:
|
||||
return _json(
|
||||
ForgeBootstrapPreflightService().preflight(
|
||||
session, profile, idempotency_key=idempotencyKey
|
||||
)
|
||||
)
|
||||
if "api_version" in plan:
|
||||
return {
|
||||
"status": "invalid",
|
||||
"executed": False,
|
||||
"reason": "Unsupported deployment.preflight profile discriminator.",
|
||||
}
|
||||
typed_plan = DeploymentPlanV1.model_validate(plan)
|
||||
with Session(engine) as session:
|
||||
return _json(
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
_REVISION = re.compile(r"^[0-9a-f]{40}$")
|
||||
_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
|
||||
|
||||
class ForgeBootstrapPreflightV1(BaseModel):
|
||||
"""Fixed read-only input for Forge production bootstrap readiness."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", strict=True)
|
||||
|
||||
api_version: Literal["itworx.forge-bootstrap-preflight/v1"]
|
||||
revision: str = Field(min_length=40, max_length=40)
|
||||
workspace_image_digest: str = Field(min_length=71, max_length=71)
|
||||
frontdoor_image_digest: str = Field(min_length=71, max_length=71)
|
||||
|
||||
@field_validator("revision")
|
||||
@classmethod
|
||||
def full_lowercase_revision(cls, value: str) -> str:
|
||||
if not _REVISION.fullmatch(value):
|
||||
raise ValueError("revision must be a full lowercase Git SHA")
|
||||
return value
|
||||
|
||||
@field_validator("workspace_image_digest", "frontdoor_image_digest")
|
||||
@classmethod
|
||||
def immutable_oci_digest(cls, value: str) -> str:
|
||||
if not _DIGEST.fullmatch(value):
|
||||
raise ValueError("image digest must be a lowercase sha256 OCI digest")
|
||||
return value
|
||||
@@ -60,6 +60,7 @@ git --git-dir="$repo" archive --format=tar "$revision" \
|
||||
scripts/reconcile_controller_state.py \
|
||||
scripts/run_repopulse_adoption.py \
|
||||
scripts/prepare_controller_hub_release_volume.py \
|
||||
scripts/vendor \
|
||||
scripts/upgrade_deployment_controller.sh > "$work/controller.tar"
|
||||
chmod 0600 "$work/controller.tar"
|
||||
archive_sha="$(sha256sum "$work/controller.tar" | cut -d ' ' -f 1)"
|
||||
|
||||
@@ -26,6 +26,7 @@ import httpx
|
||||
from ..core.config import settings
|
||||
from ..core.redaction import redact_message
|
||||
from ..schemas.deployment import DeploymentPlanV1
|
||||
from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
|
||||
|
||||
_REQUEST_ID = re.compile(r"^[0-9a-f]{64}$")
|
||||
_MAX_DELIVERY_STATUS_BYTES = 512 * 1024
|
||||
@@ -302,6 +303,17 @@ class DeploymentBrokerClient:
|
||||
payload={"enrollment": enrollment},
|
||||
)
|
||||
|
||||
def forge_bootstrap_preflight(
|
||||
self, profile: ForgeBootstrapPreflightV1, *, secret: str
|
||||
) -> dict[str, Any]:
|
||||
"""Read fixed Forge source and host prerequisites through bootstrap HMAC."""
|
||||
return self._request(
|
||||
"POST",
|
||||
"/v1/appops/forge/bootstrap/preflight",
|
||||
secret=secret,
|
||||
payload={"profile": profile.model_dump(mode="json")},
|
||||
)
|
||||
|
||||
def enrollment_capabilities(self, *, secret: str) -> dict[str, Any]:
|
||||
"""Read the bootstrap-protected enrollment controller capability."""
|
||||
return self._request(
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from ..core.time import utc_now
|
||||
from ..db.models import DeploymentJob, Policy
|
||||
from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
|
||||
from .audit_logger import log_event
|
||||
from .deployment_broker import DeploymentBrokerClient
|
||||
from .enrollment_plane import EnrollmentPlane
|
||||
from .policy_engine import evaluate_policy
|
||||
from .secret_store import get_secret, has_secret
|
||||
|
||||
_API_VERSION = "itworx.forge-bootstrap-preflight/v1"
|
||||
_APPLICATION_ID = "itworx-forge"
|
||||
_REPOSITORY = "jens/itworx-forge"
|
||||
_IDEMPOTENCY = re.compile(r"^[A-Za-z0-9._:-]{8,128}$")
|
||||
_CONTROLLER_CHECKS = {
|
||||
"exact_main", "source_manifest", "managed_appdata", "proxy_network",
|
||||
"resource_collisions", "push_validation", "image_registry", "oidc",
|
||||
"secret_delivery",
|
||||
}
|
||||
_DETAILS = {
|
||||
"deployment_apply_policy": "An explicit allowed exact-target deployment policy is required before apply.",
|
||||
"controller_trust": "Stored controller trust and bootstrap secret are required for host checks.",
|
||||
"managed_appdata_root_selection": "The managed Docker-root backup acknowledgement and storage selection are required before setup.",
|
||||
"exact_main": "Requested Forge revision must equal the current main head.",
|
||||
"source_manifest": "The source manifest must match the fixed Forge six-service topology.",
|
||||
"managed_appdata": "The fixed managed PostgreSQL directory must have safe owner-only metadata.",
|
||||
"proxy_network": "The required external proxy network must be present.",
|
||||
"resource_collisions": "Fixed Forge container names must be free for first installation.",
|
||||
"push_validation": "Exact-main CI validation cannot be verified from trusted controller configuration.",
|
||||
"image_registry": "Registry publication and pull authorization require external verification.",
|
||||
"oidc": "Dedicated Forge Authentik provider, application, and role groups require external verification.",
|
||||
"secret_delivery": "Forge database and application secret files require external verification.",
|
||||
}
|
||||
_STATUS_DETAILS = {
|
||||
"exact_main": {
|
||||
"passed": "Requested revision equals the current protected Gitea main head.",
|
||||
"blocked": "Requested revision is not the current protected Gitea main head.",
|
||||
"unknown": "Current protected Gitea main head could not be verified.",
|
||||
},
|
||||
"source_manifest": {
|
||||
"passed": "Committed Forge manifest matches the pinned production topology contract.",
|
||||
"blocked": "Committed Forge manifest differs from the pinned production topology contract.",
|
||||
"unknown": "Committed Forge manifest could not be read from the controller mirror.",
|
||||
},
|
||||
"managed_appdata": {
|
||||
"passed": "Selected managed root and fixed PostgreSQL directory have safe metadata.",
|
||||
"blocked": "Managed root or fixed PostgreSQL directory is missing or unsafe.",
|
||||
"unknown": "Managed storage metadata could not be verified.",
|
||||
},
|
||||
"proxy_network": {
|
||||
"passed": "The required external proxy network exists.",
|
||||
"blocked": "The required external proxy network is missing.",
|
||||
"unknown": "Docker network inventory is unavailable.",
|
||||
},
|
||||
"resource_collisions": {
|
||||
"passed": "No fixed Forge container-name collisions were found.",
|
||||
"blocked": "One or more fixed Forge container names already exist.",
|
||||
"unknown": "Docker container inventory is unavailable.",
|
||||
},
|
||||
"push_validation": {
|
||||
"passed": "Configured required push validation contexts passed for this exact commit.",
|
||||
"blocked": "A configured required push validation context rejected this exact commit.",
|
||||
"unknown": "Exact-commit push validation is not configured or cannot be verified.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ForgeBootstrapPreflightError(ValueError):
|
||||
"""A fixed-target Forge preflight request failed closed."""
|
||||
|
||||
|
||||
def _canonical_digest(value: dict[str, Any]) -> str:
|
||||
return hashlib.sha256(
|
||||
json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def _idempotency_digest(value: str) -> str:
|
||||
normalized = value.strip()
|
||||
if not _IDEMPOTENCY.fullmatch(normalized):
|
||||
raise ForgeBootstrapPreflightError("Idempotency-Key must contain 8 to 128 safe characters.")
|
||||
return hashlib.sha256(f"forge-bootstrap-preflight:{normalized}".encode()).hexdigest()
|
||||
|
||||
|
||||
def _check(name: str, status: str) -> dict[str, str]:
|
||||
detail = _STATUS_DETAILS.get(name, {}).get(status, _DETAILS[name])
|
||||
return {"name": name, "status": status, "detail": detail}
|
||||
|
||||
|
||||
def _checked_controller_result(
|
||||
result: object, profile: ForgeBootstrapPreflightV1
|
||||
) -> tuple[list[dict[str, str]], dict[str, Any]] | None:
|
||||
if not isinstance(result, dict) or (
|
||||
result.get("apiVersion") != _API_VERSION
|
||||
or result.get("revision") != profile.revision
|
||||
or result.get("workspaceImageDigest") != profile.workspace_image_digest
|
||||
or result.get("frontdoorImageDigest") != profile.frontdoor_image_digest
|
||||
):
|
||||
return None
|
||||
raw_checks = result.get("checks")
|
||||
if not isinstance(raw_checks, dict) or set(raw_checks) != _CONTROLLER_CHECKS:
|
||||
return None
|
||||
checks: list[dict[str, str]] = []
|
||||
for name in sorted(_CONTROLLER_CHECKS):
|
||||
raw = raw_checks.get(name)
|
||||
if not isinstance(raw, dict) or set(raw) != {"status"} or raw.get("status") not in {"passed", "blocked", "unknown"}:
|
||||
return None
|
||||
# CI, registry access, OIDC and secret delivery are intentionally
|
||||
# unknown until a configured, source-authenticated proof exists.
|
||||
if name in {"image_registry", "oidc", "secret_delivery"} and raw["status"] != "unknown":
|
||||
return None
|
||||
checks.append(_check(name, raw["status"]))
|
||||
statuses = [item["status"] for item in checks]
|
||||
expected_status = "preflight_blocked" if "blocked" in statuses else "preflight_unknown" if "unknown" in statuses else "preflight_ok"
|
||||
if result.get("status") != expected_status:
|
||||
return None
|
||||
raw_evidence = result.get("evidence", {})
|
||||
if not isinstance(raw_evidence, dict) or set(raw_evidence) - {
|
||||
"mainRevision", "sourceManifestSha256", "resourceCollisionCount",
|
||||
"workspaceImagePresent", "frontdoorImagePresent",
|
||||
}:
|
||||
return None
|
||||
evidence: dict[str, Any] = {}
|
||||
for name in ("mainRevision", "sourceManifestSha256"):
|
||||
value = raw_evidence.get(name)
|
||||
if value is not None:
|
||||
pattern = r"^[0-9a-f]{40}$" if name == "mainRevision" else r"^[0-9a-f]{64}$"
|
||||
if not isinstance(value, str) or not re.fullmatch(pattern, value):
|
||||
return None
|
||||
evidence[name] = value
|
||||
count = raw_evidence.get("resourceCollisionCount")
|
||||
if count is not None:
|
||||
if isinstance(count, bool) or not isinstance(count, int) or not 0 <= count <= 1000:
|
||||
return None
|
||||
evidence["resourceCollisionCount"] = count
|
||||
for name in ("workspaceImagePresent", "frontdoorImagePresent"):
|
||||
value = raw_evidence.get(name)
|
||||
if value is not None:
|
||||
if not isinstance(value, bool):
|
||||
return None
|
||||
evidence[name] = value
|
||||
return checks, evidence
|
||||
|
||||
|
||||
class ForgeBootstrapPreflightService:
|
||||
"""Read-only fixed Forge checks; this profile has no apply or rollback path."""
|
||||
|
||||
def __init__(self, *, broker: DeploymentBrokerClient | None = None):
|
||||
self.broker = broker or DeploymentBrokerClient()
|
||||
|
||||
def preflight(
|
||||
self,
|
||||
session: Session,
|
||||
profile: ForgeBootstrapPreflightV1,
|
||||
*,
|
||||
idempotency_key: str,
|
||||
) -> dict[str, Any]:
|
||||
payload = profile.model_dump(mode="json")
|
||||
plan_digest = _canonical_digest(payload)
|
||||
key_digest = _idempotency_digest(idempotency_key)
|
||||
existing = session.exec(
|
||||
select(DeploymentJob).where(DeploymentJob.idempotency_sha256 == key_digest)
|
||||
).first()
|
||||
if existing:
|
||||
if existing.plan_sha256 != plan_digest:
|
||||
raise ForgeBootstrapPreflightError(
|
||||
"Idempotency-Key was already used for a different Forge profile."
|
||||
)
|
||||
return self.serialize_job(existing, replay=True)
|
||||
|
||||
exact = session.exec(
|
||||
select(Policy).where(
|
||||
(Policy.action == "deployment.apply") & (Policy.target == _APPLICATION_ID)
|
||||
)
|
||||
).all()
|
||||
decision = evaluate_policy(
|
||||
session, action="deployment.apply", target=_APPLICATION_ID, source="manual"
|
||||
)
|
||||
policy_ready = bool(
|
||||
len(exact) == 1
|
||||
and exact[0].manual == "allowed"
|
||||
and exact[0].voice == "blocked"
|
||||
and exact[0].risk == "high"
|
||||
and decision.get("decision") == "allowed"
|
||||
and decision.get("policy_id") == exact[0].id
|
||||
)
|
||||
checks = [_check("deployment_apply_policy", "passed" if policy_ready else "blocked")]
|
||||
|
||||
trust = EnrollmentPlane.get_trust_config(session)
|
||||
expected_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
||||
secret = None
|
||||
if trust is not None:
|
||||
try:
|
||||
if has_secret(session, trust.bootstrap_secret_ref):
|
||||
secret = get_secret(session, trust.bootstrap_secret_ref, touch=False)
|
||||
except Exception: # noqa: BLE001 - fail closed without exposing secret-store errors.
|
||||
secret = None
|
||||
bootstrap_ready = bool(
|
||||
trust
|
||||
and isinstance(secret, str)
|
||||
and len(secret) >= 32
|
||||
and secret.isascii()
|
||||
and secret.isprintable()
|
||||
)
|
||||
checks.append(_check("controller_trust", "passed" if bootstrap_ready else "blocked"))
|
||||
root_ready = bool(
|
||||
trust
|
||||
and trust.trusted_appdata_root == expected_root
|
||||
and trust.managed_appdata_backup_acknowledged
|
||||
)
|
||||
checks.append(
|
||||
_check("managed_appdata_root_selection", "passed" if root_ready else "blocked")
|
||||
)
|
||||
|
||||
controller_result = None
|
||||
if bootstrap_ready:
|
||||
try:
|
||||
controller_result = self.broker.forge_bootstrap_preflight(
|
||||
profile, secret=secret or ""
|
||||
)
|
||||
except Exception: # noqa: BLE001 - transport details stay out of the preflight receipt.
|
||||
controller_result = None
|
||||
parsed_controller = _checked_controller_result(controller_result, profile)
|
||||
if parsed_controller is None:
|
||||
host_checks = [_check(name, "unknown") for name in sorted(_CONTROLLER_CHECKS)]
|
||||
controller_evidence: dict[str, Any] = {}
|
||||
else:
|
||||
host_checks, controller_evidence = parsed_controller
|
||||
checks.extend(host_checks)
|
||||
blockers = [item["name"] for item in checks if item["status"] == "blocked"]
|
||||
unknowns = [item["name"] for item in checks if item["status"] == "unknown"]
|
||||
outcome = "blocked" if blockers else "unknown" if unknowns else "passed"
|
||||
job_status = "preflight_succeeded" if outcome == "passed" else "preflight_failed"
|
||||
now = utc_now()
|
||||
job = DeploymentJob(
|
||||
id=f"forge-preflight-{uuid.uuid4().hex[:16]}",
|
||||
application_id=_APPLICATION_ID,
|
||||
repository=_REPOSITORY,
|
||||
revision=profile.revision,
|
||||
plan_sha256=plan_digest,
|
||||
idempotency_sha256=key_digest,
|
||||
status=job_status,
|
||||
stage="preflight_complete" if outcome == "passed" else "preflight_incomplete",
|
||||
plan_json=payload,
|
||||
preflight_json={
|
||||
"apiVersion": _API_VERSION,
|
||||
"status": outcome,
|
||||
"blockers": blockers,
|
||||
"unknowns": unknowns,
|
||||
"checks": checks,
|
||||
},
|
||||
evidence_json={
|
||||
"profile": "forge-production-bootstrap",
|
||||
"workspaceImageDigest": profile.workspace_image_digest,
|
||||
"frontdoorImageDigest": profile.frontdoor_image_digest,
|
||||
"controller": controller_evidence,
|
||||
},
|
||||
result_summary=(
|
||||
"Forge bootstrap checks passed; production apply remains separately gated."
|
||||
if outcome == "passed"
|
||||
else "Forge bootstrap preflight is incomplete; see blocked and unknown checks."
|
||||
),
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
completed_at=now,
|
||||
)
|
||||
session.add(job)
|
||||
session.commit()
|
||||
session.refresh(job)
|
||||
log_event(
|
||||
session,
|
||||
source="deployment-control-plane",
|
||||
identity="operator",
|
||||
action_id="deployment.preflight",
|
||||
target=_APPLICATION_ID,
|
||||
target_type="deployment",
|
||||
risk="high",
|
||||
policy_decision="allowed" if policy_ready else "blocked",
|
||||
reason="Fixed Forge bootstrap preflight recorded without requesting a deployment action.",
|
||||
result="success" if outcome == "passed" else "blocked",
|
||||
execution_plan={"planSha256": plan_digest, "profile": "forge-production-bootstrap"},
|
||||
post_checks={"preflight": job.preflight_json},
|
||||
)
|
||||
return self.serialize_job(job)
|
||||
|
||||
@staticmethod
|
||||
def serialize_job(job: DeploymentJob, *, replay: bool = False) -> dict[str, Any]:
|
||||
return {
|
||||
"id": job.id,
|
||||
"applicationId": job.application_id,
|
||||
"repository": job.repository,
|
||||
"revision": job.revision,
|
||||
"status": job.status,
|
||||
"stage": job.stage,
|
||||
"preflight": job.preflight_json,
|
||||
"evidence": job.evidence_json,
|
||||
"result": job.result_summary,
|
||||
"brokerRequestId": job.broker_request_id,
|
||||
"createdAt": job.created_at.isoformat(),
|
||||
"updatedAt": job.updated_at.isoformat(),
|
||||
"completedAt": job.completed_at.isoformat() if job.completed_at else None,
|
||||
"idempotentReplay": replay,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def refresh(session: Session, job_id: str) -> dict[str, Any] | None:
|
||||
job = session.get(DeploymentJob, job_id)
|
||||
if not job or not job.id.startswith("forge-preflight-"):
|
||||
return None
|
||||
return ForgeBootstrapPreflightService.serialize_job(job)
|
||||
|
||||
@staticmethod
|
||||
def list_jobs(session: Session, *, limit: int = 100) -> list[dict[str, Any]]:
|
||||
rows = session.exec(
|
||||
select(DeploymentJob)
|
||||
.where(DeploymentJob.id.startswith("forge-preflight-"))
|
||||
.order_by(DeploymentJob.created_at.desc())
|
||||
.limit(max(1, min(limit, 250)))
|
||||
).all()
|
||||
return [ForgeBootstrapPreflightService.serialize_job(row) for row in rows]
|
||||
@@ -29,6 +29,7 @@ def test_controller_promoter_helper_spec_is_fixed_and_bounded():
|
||||
spec = helper_container_spec(revision=revision, image_id=image_id)
|
||||
|
||||
assert HELPER_NAME == "itworx-controller-artifact-promoter"
|
||||
assert CONTROLLER_VERSION == "2.1.13"
|
||||
assert re.fullmatch(r"2\.1\.\d+", CONTROLLER_VERSION)
|
||||
assert spec["Image"] == image_id
|
||||
assert spec["HostConfig"]["Privileged"] is False
|
||||
@@ -53,6 +54,9 @@ def test_controller_promoter_helper_spec_is_fixed_and_bounded():
|
||||
assert "rm -rf -- \"$work\"" in command
|
||||
assert "chmod 0600 \"$work/controller.tar\"" in command
|
||||
assert "scripts/prepare_controller_hub_release_volume.py" in command
|
||||
archive_start = command.index('git --git-dir="$repo" archive')
|
||||
archive_end = command.index('> "$work/controller.tar"', archive_start)
|
||||
assert "scripts/vendor" in command[archive_start:archive_end]
|
||||
assert "scripts/upgrade_deployment_controller.sh > \"$work/controller.tar\"" in command
|
||||
assert f"--image-id '{image_id}'" in command
|
||||
|
||||
|
||||
@@ -264,6 +264,107 @@ def test_previous_private_tls_is_validated_from_the_attested_config_snapshot(
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_staged_config_checks_run_new_artifact_with_copied_config_and_bounded_output(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
stage_root = tmp_path / "source"
|
||||
stage_root.mkdir(mode=0o700)
|
||||
release_tree = (
|
||||
{
|
||||
"status_server.py": (b"new status server", 0o644),
|
||||
"controller-release.json": (b"new release identity", 0o644),
|
||||
},
|
||||
{"": 0o755},
|
||||
)
|
||||
config_tree = (
|
||||
{
|
||||
"projects.json": (b"{\"repo\":{}}", 0o600),
|
||||
"broker-token": (b"private broker token", 0o600),
|
||||
"gitea-validation.json": (b"{}", 0o600),
|
||||
"gitea-validation-token": (b"private validation token", 0o600),
|
||||
"private-tls.json": (b"private TLS config", 0o600),
|
||||
"webhook-secrets/repo": (b"private webhook secret", 0o600),
|
||||
},
|
||||
{"": 0o755, "webhook-secrets": 0o700},
|
||||
)
|
||||
calls = []
|
||||
validation_root = stage_root / "validation"
|
||||
|
||||
def run(arguments, *, data=None, max_output=module.MAX_BYTES):
|
||||
calls.append((arguments, max_output))
|
||||
assert (validation_root / "status_server.py").read_bytes() == b"new status server"
|
||||
assert (validation_root / "projects.json").read_bytes() == b"{\"repo\":{}}"
|
||||
assert (validation_root / "broker-token").read_bytes() == b"private broker token"
|
||||
assert (validation_root / "webhook-secrets" / "repo").read_bytes() == b"private webhook secret"
|
||||
return b"validated"
|
||||
|
||||
monkeypatch.setattr(module, "_write_private_tree", _write_test_tree)
|
||||
monkeypatch.setattr(module, "_run", run)
|
||||
|
||||
validator = getattr(module, "_validate_staged_configuration", None)
|
||||
assert callable(validator), "staged configuration validation helper is required"
|
||||
validator(stage_root, release_tree, config_tree)
|
||||
|
||||
assert calls == [
|
||||
(["python3", str(validation_root / "status_server.py"), "--check-artifact"], 4096),
|
||||
(["python3", str(validation_root / "status_server.py"), "--check-config"], 4096),
|
||||
(["python3", str(validation_root / "status_server.py"), "--check-validation-config"], 4096),
|
||||
(["python3", str(validation_root / "status_server.py"), "--check-validation-api"], 4096),
|
||||
(["python3", str(validation_root / "status_server.py"), "--check-private-tls-config"], 4096),
|
||||
]
|
||||
assert not validation_root.exists()
|
||||
|
||||
|
||||
def test_staged_config_check_failure_is_sanitized_and_cleans_private_validation_tree(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
stage_root = tmp_path / "source"
|
||||
stage_root.mkdir(mode=0o700)
|
||||
release_tree = (
|
||||
{
|
||||
"status_server.py": (b"new status server", 0o644),
|
||||
"controller-release.json": (b"new release identity", 0o644),
|
||||
},
|
||||
{"": 0o755},
|
||||
)
|
||||
config_tree = (
|
||||
{
|
||||
"projects.json": (b"{}", 0o600),
|
||||
"broker-token": (b"top-secret-broker", 0o600),
|
||||
"gitea-validation.json": (b"{}", 0o600),
|
||||
"gitea-validation-token": (b"top-secret-validation", 0o600),
|
||||
},
|
||||
{"": 0o755},
|
||||
)
|
||||
|
||||
class FailedProcess:
|
||||
returncode = 1
|
||||
stdout = b"token=top-secret-validation"
|
||||
stderr = b"secret=top-secret-broker"
|
||||
|
||||
monkeypatch.setattr(module, "_write_private_tree", _write_test_tree)
|
||||
monkeypatch.setattr(module.subprocess, "run", lambda *args, **kwargs: FailedProcess())
|
||||
|
||||
validator = getattr(module, "_validate_staged_configuration", None)
|
||||
assert callable(validator), "staged configuration validation helper is required"
|
||||
with pytest.raises(module.VolumeTransitionError, match="command_failed") as error:
|
||||
validator(stage_root, release_tree, config_tree)
|
||||
|
||||
assert "top-secret" not in str(error.value)
|
||||
assert not (stage_root / "validation").exists()
|
||||
|
||||
|
||||
def _write_test_tree(path, entries, directories):
|
||||
path.mkdir(mode=0o700)
|
||||
for relative, _mode in sorted(directories.items(), key=lambda item: item[0].count("/")):
|
||||
if relative:
|
||||
(path / relative).mkdir(parents=True, exist_ok=True)
|
||||
for relative, (contents, _mode) in entries.items():
|
||||
destination = path / relative
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_bytes(contents)
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="private TLS snapshot guard requires POSIX symlink semantics")
|
||||
def test_previous_private_tls_rejects_symlinked_snapshot(tmp_path):
|
||||
previous = tmp_path / "previous-controller"
|
||||
@@ -298,12 +399,67 @@ def test_promoted_compose_actions_use_only_stdin_and_fixed_service(compose):
|
||||
module.compose_action(rendered, "stop")
|
||||
module.compose_action(rendered, "up")
|
||||
assert calls[-2][0][-2:] == ["stop", module.SERVICE]
|
||||
assert calls[-1][0][-5:] == ["up", "-d", "--force-recreate", "--pull", "never", module.SERVICE][-5:]
|
||||
up_index = calls[-1][0].index("up")
|
||||
assert calls[-1][0][up_index:] == [
|
||||
"up", "-d", "--force-recreate", "--pull", "never", "--no-deps", module.SERVICE
|
||||
]
|
||||
assert calls[-2][1] == rendered
|
||||
assert calls[-1][1] == rendered
|
||||
assert baseline["name"] == "gitea-deploy-runners"
|
||||
|
||||
|
||||
def test_rollback_uses_no_dependencies_when_recreating_prior_controller(compose, tmp_path, monkeypatch):
|
||||
_baseline, calls = compose
|
||||
previous = REVISION
|
||||
target = "c" * 40
|
||||
previous_compose = module.rendered_model(ORIGINAL_COMPOSE, previous, IMAGE_ID)
|
||||
target_compose = module.rendered_model(ORIGINAL_COMPOSE, target, IMAGE_ID)
|
||||
receipt = {
|
||||
"phase": "activated",
|
||||
"imageId": IMAGE_ID,
|
||||
"previousRevision": previous,
|
||||
"sourceRevision": target,
|
||||
}
|
||||
values = {
|
||||
"controller-before.tar": module._release_tar_bytes(
|
||||
previous,
|
||||
({"controller-release.json": (b"previous release", 0o644)}, {"": 0o755}),
|
||||
),
|
||||
"controller-after.tar": b"unused by isolated rollback test",
|
||||
"mirror-before.tar": b"unused by isolated rollback test",
|
||||
"compose-rollback.json": previous_compose,
|
||||
"compose-after.json": target_compose,
|
||||
}
|
||||
current = {
|
||||
"Mounts": [{
|
||||
"Destination": module.EXECUTABLE_TARGET,
|
||||
"Source": module._expected_revision_path(previous).as_posix(),
|
||||
}]
|
||||
}
|
||||
monkeypatch.setattr(module, "_load_transaction", lambda _backup: (1, receipt, values))
|
||||
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
|
||||
monkeypatch.setattr(module, "_controller_image_inspect", lambda *args, **kwargs: current)
|
||||
monkeypatch.setattr(module, "_verify_release_leaf", lambda *args, **kwargs: True)
|
||||
monkeypatch.setattr(module, "_stop_current_controller", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(module, "_sync_controller_mirror", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(module, "_ensure_root_compose", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(module, "_verify_compose_image", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(module, "_verify_active_mount", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(module, "_set_promotion_phase", lambda *args, **kwargs: None)
|
||||
|
||||
module.rollback_promotion(tmp_path / "backup")
|
||||
|
||||
up_calls = [
|
||||
arguments for arguments, _data in calls
|
||||
if arguments[:2] == ["docker", "compose"] and "up" in arguments
|
||||
]
|
||||
assert len(up_calls) == 1
|
||||
up_index = up_calls[0].index("up")
|
||||
assert up_calls[0][up_index:] == [
|
||||
"up", "-d", "--force-recreate", "--pull", "never", "--no-deps", module.SERVICE
|
||||
]
|
||||
|
||||
|
||||
def test_installer_has_no_shared_executable_stage_or_compose_path():
|
||||
source = (ROOT / "scripts/upgrade_deployment_controller.sh").read_text(encoding="utf-8")
|
||||
assert "mktemp -d -p /tmp \"itworx-controller-upgrade-" in source
|
||||
@@ -476,6 +632,11 @@ def test_source_compose_preflight_rejects_unresolvable_alias_keys():
|
||||
module._preflight_source_compose(source)
|
||||
|
||||
|
||||
def test_source_compose_preflight_bounds_recursive_list_aliases():
|
||||
source = b"services: {app: {image: fixed}}\nx-loop: &a [*a]\n"
|
||||
module._preflight_source_compose(source)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source",
|
||||
[
|
||||
|
||||
@@ -53,3 +53,14 @@ def test_windows_wrapper_supplies_private_archive_and_exact_image_identity():
|
||||
assert "--image-id '$imageId'" in wrapper
|
||||
assert "--image-id) image_id=\"$2\"" in installer
|
||||
assert '[[ "$archive_parent" =~ ^/tmp/itworx-controller-artifact-promoter-' in installer
|
||||
|
||||
|
||||
def test_state_reconciliation_is_committed_only_after_volume_verification():
|
||||
installer = (ROOT / "scripts/upgrade_deployment_controller.sh").read_text(encoding="utf-8")
|
||||
verify = installer.index('python3 "${work}/${PREPARER_NAME}" verify --backup "$backup"')
|
||||
commit = installer.index(
|
||||
'python3 "${work}/reconcile_controller_state.py" --journal-path "$migration_journal" --commit'
|
||||
)
|
||||
|
||||
assert verify < commit
|
||||
assert installer.index("reconciliation_active=0", commit) > commit
|
||||
@@ -0,0 +1,277 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import http.client
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
STATUS_SERVER = ROOT / "ops" / "gitea-deploy-broker" / "status_server.py"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def status_module():
|
||||
if "fcntl" not in sys.modules:
|
||||
fcntl = types.ModuleType("fcntl")
|
||||
fcntl.LOCK_EX = 2
|
||||
fcntl.LOCK_UN = 8
|
||||
fcntl.flock = lambda *_args: None
|
||||
sys.modules["fcntl"] = fcntl
|
||||
spec = importlib.util.spec_from_file_location("forge_bootstrap_status_test", STATUS_SERVER)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _request(module, server, body: bytes, *, nonce: str, secret: bytes, signature: str | None = None):
|
||||
path = module.FORGE_BOOTSTRAP_PREFLIGHT_PATH
|
||||
timestamp = str(int(module.time.time()))
|
||||
signature = signature or module._request_signature(
|
||||
secret,
|
||||
method="POST",
|
||||
path=path,
|
||||
timestamp=timestamp,
|
||||
nonce=nonce,
|
||||
raw_body=body,
|
||||
)
|
||||
connection = http.client.HTTPConnection(*server.server_address, timeout=2)
|
||||
connection.request(
|
||||
"POST",
|
||||
path,
|
||||
body=body,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": str(len(body)),
|
||||
"X-ITWorx-Deployment-Timestamp": timestamp,
|
||||
"X-ITWorx-Deployment-Nonce": nonce,
|
||||
"X-ITWorx-Deployment-Signature": signature,
|
||||
},
|
||||
)
|
||||
response = connection.getresponse()
|
||||
status, payload = response.status, json.loads(response.read())
|
||||
connection.close()
|
||||
return status, payload
|
||||
|
||||
|
||||
def test_forge_preflight_http_is_signed_strict_replay_safe_and_read_only(
|
||||
status_module, tmp_path, monkeypatch
|
||||
):
|
||||
secret = b"forge-bootstrap-test-secret-" + b"x" * 40
|
||||
secret_path = tmp_path / "bootstrap"
|
||||
secret_path.write_bytes(secret)
|
||||
secret_path.chmod(0o600)
|
||||
nonce_root = tmp_path / "nonces"
|
||||
monkeypatch.setattr(status_module, "APPOPS_ENROLLMENT_BOOTSTRAP_SECRET_FILE", secret_path)
|
||||
monkeypatch.setattr(status_module, "APPOPS_NONCE_ROOT", nonce_root)
|
||||
calls = []
|
||||
response = {
|
||||
"apiVersion": status_module.FORGE_BOOTSTRAP_PREFLIGHT_VERSION,
|
||||
"status": "preflight_unknown",
|
||||
"revision": "a" * 40,
|
||||
"workspaceImageDigest": "sha256:" + "b" * 64,
|
||||
"frontdoorImageDigest": "sha256:" + "c" * 64,
|
||||
"checks": {"exact_main": {"status": "unknown"}},
|
||||
"evidence": {},
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
status_module,
|
||||
"_forge_bootstrap_runtime_checks",
|
||||
lambda profile: calls.append(profile) or response,
|
||||
)
|
||||
server = status_module.BoundedThreadingHTTPServer(
|
||||
("127.0.0.1", 0), status_module.Handler
|
||||
)
|
||||
serving = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
serving.start()
|
||||
profile = {
|
||||
"api_version": status_module.FORGE_BOOTSTRAP_PREFLIGHT_VERSION,
|
||||
"revision": "a" * 40,
|
||||
"workspace_image_digest": "sha256:" + "b" * 64,
|
||||
"frontdoor_image_digest": "sha256:" + "c" * 64,
|
||||
}
|
||||
body = json.dumps({"profile": profile}, sort_keys=True, separators=(",", ":")).encode()
|
||||
try:
|
||||
status, payload = _request(
|
||||
status_module, server, body, nonce="a" * 32, secret=secret
|
||||
)
|
||||
assert status == 200
|
||||
assert payload == response
|
||||
assert calls == [profile]
|
||||
assert secret.decode() not in json.dumps(payload)
|
||||
assert len(list(nonce_root.iterdir())) == 1
|
||||
|
||||
replay_status, replay_payload = _request(
|
||||
status_module, server, body, nonce="a" * 32, secret=secret
|
||||
)
|
||||
assert replay_status == 403
|
||||
assert replay_payload == {"status": "authentication_rejected"}
|
||||
assert calls == [profile]
|
||||
|
||||
hostile = dict(profile, host_path="/etc", endpoint="https://attacker.invalid")
|
||||
hostile_body = json.dumps(
|
||||
{"profile": hostile}, sort_keys=True, separators=(",", ":")
|
||||
).encode()
|
||||
hostile_status, hostile_payload = _request(
|
||||
status_module, server, hostile_body, nonce="b" * 32, secret=secret
|
||||
)
|
||||
assert hostile_status == 400
|
||||
assert hostile_payload == {"status": "invalid_forge_preflight_contract"}
|
||||
assert calls == [profile]
|
||||
assert len(list(nonce_root.iterdir())) == 1
|
||||
|
||||
bad_signature_status, _ = _request(
|
||||
status_module,
|
||||
server,
|
||||
body,
|
||||
nonce="c" * 32,
|
||||
secret=secret,
|
||||
signature="0" * 64,
|
||||
)
|
||||
assert bad_signature_status == 403
|
||||
assert calls == [profile]
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
serving.join(timeout=2)
|
||||
|
||||
|
||||
def test_forge_host_preflight_compares_exact_main_and_reports_fixed_runtime_checks(
|
||||
status_module, tmp_path, monkeypatch
|
||||
):
|
||||
revision = "a" * 40
|
||||
manifest = b"fixed Forge compose candidate"
|
||||
managed_root = tmp_path / "managed-root"
|
||||
postgres_dir = managed_root / "itworx-forge" / "postgres"
|
||||
postgres_dir.mkdir(parents=True)
|
||||
(managed_root / "itworx-forge").chmod(0o700)
|
||||
postgres_dir.chmod(0o700)
|
||||
app_dir = managed_root / "itworx-forge"
|
||||
monkeypatch.setattr(status_module, "MANAGED_APPDATA_ROOT", managed_root)
|
||||
monkeypatch.setattr(status_module, "_refresh_appdata_root_selection", lambda: managed_root)
|
||||
monkeypatch.setattr(status_module, "appdata_root_is_safe", lambda _root: True)
|
||||
monkeypatch.setattr(
|
||||
status_module, "os", types.SimpleNamespace(name="posix", geteuid=lambda: 0)
|
||||
)
|
||||
original_lstat = Path.lstat
|
||||
|
||||
def lstat_with_fixed_service_owners(path):
|
||||
info = original_lstat(path)
|
||||
if path == app_dir:
|
||||
return types.SimpleNamespace(
|
||||
st_mode=status_module.stat.S_IFDIR | 0o700, st_uid=0, st_gid=0
|
||||
)
|
||||
if path == postgres_dir:
|
||||
return types.SimpleNamespace(
|
||||
st_mode=status_module.stat.S_IFDIR | 0o700, st_uid=70, st_gid=70
|
||||
)
|
||||
return info
|
||||
|
||||
monkeypatch.setattr(Path, "lstat", lstat_with_fixed_service_owners)
|
||||
monkeypatch.setattr(status_module, "_forge_gitea_main_revision", lambda: revision)
|
||||
monkeypatch.setattr(status_module, "_forge_push_validation", lambda _revision: "unknown")
|
||||
monkeypatch.setattr(
|
||||
status_module, "FORGE_COMPOSE_SHA256", hashlib.sha256(manifest).hexdigest()
|
||||
)
|
||||
commands = []
|
||||
|
||||
def run(arguments, **_kwargs):
|
||||
commands.append(arguments)
|
||||
if arguments[:2] == ["git", "--git-dir"]:
|
||||
if "rev-parse" in arguments:
|
||||
return subprocess.CompletedProcess(arguments, 0, stdout=revision, stderr="")
|
||||
if "show" in arguments:
|
||||
return subprocess.CompletedProcess(arguments, 0, stdout=manifest, stderr=b"")
|
||||
if arguments[:3] == ["docker", "network", "ls"]:
|
||||
return subprocess.CompletedProcess(
|
||||
arguments, 0, stdout="itworx-mcp-hub-npm\n", stderr=""
|
||||
)
|
||||
if arguments[:3] == ["docker", "network", "inspect"]:
|
||||
return subprocess.CompletedProcess(
|
||||
arguments,
|
||||
0,
|
||||
stdout=json.dumps(
|
||||
{"Name": "itworx-mcp-hub-npm", "Driver": "bridge", "Scope": "local"}
|
||||
),
|
||||
stderr="",
|
||||
)
|
||||
if arguments[:3] == ["docker", "ps", "-a"]:
|
||||
return subprocess.CompletedProcess(arguments, 0, stdout="", stderr="")
|
||||
if arguments[:3] == ["docker", "image", "inspect"]:
|
||||
return subprocess.CompletedProcess(
|
||||
arguments, 0, stdout="sha256:" + "d" * 64, stderr=""
|
||||
)
|
||||
raise AssertionError(f"unexpected fixed read-only command: {arguments!r}")
|
||||
|
||||
monkeypatch.setattr(status_module.subprocess, "run", run)
|
||||
profile = {
|
||||
"revision": revision,
|
||||
"workspace_image_digest": "sha256:" + "b" * 64,
|
||||
"frontdoor_image_digest": "sha256:" + "c" * 64,
|
||||
}
|
||||
result = status_module._forge_bootstrap_runtime_checks(profile)
|
||||
assert result["status"] == "preflight_unknown"
|
||||
assert result["checks"]["exact_main"] == {"status": "passed"}
|
||||
assert result["checks"]["source_manifest"] == {"status": "passed"}
|
||||
assert result["checks"]["managed_appdata"] == {"status": "passed"}
|
||||
assert result["checks"]["proxy_network"] == {"status": "passed"}
|
||||
assert result["checks"]["resource_collisions"] == {"status": "passed"}
|
||||
assert result["checks"]["push_validation"] == {"status": "unknown"}
|
||||
assert result["evidence"]["mainRevision"] == revision
|
||||
assert result["evidence"]["sourceManifestSha256"] == hashlib.sha256(manifest).hexdigest()
|
||||
assert result["evidence"]["workspaceImagePresent"] is True
|
||||
assert result["evidence"]["frontdoorImagePresent"] is True
|
||||
assert all(isinstance(command, list) for command in commands)
|
||||
assert all("shell" not in " ".join(command).lower() for command in commands)
|
||||
|
||||
commands.clear()
|
||||
non_head = dict(profile, revision="b" * 40)
|
||||
result = status_module._forge_bootstrap_runtime_checks(non_head)
|
||||
assert result["checks"]["exact_main"] == {"status": "blocked"}
|
||||
assert result["status"] == "preflight_blocked"
|
||||
|
||||
postgres_dir.rmdir()
|
||||
postgres_dir.parent.rmdir()
|
||||
managed_root.rmdir()
|
||||
result = status_module._forge_bootstrap_runtime_checks(profile)
|
||||
assert result["checks"]["managed_appdata"] == {"status": "blocked"}
|
||||
assert not managed_root.exists()
|
||||
|
||||
|
||||
def test_forge_appdata_parent_requires_root_only_metadata(status_module, monkeypatch):
|
||||
monkeypatch.setattr(status_module, "os", types.SimpleNamespace(name="posix"))
|
||||
safe = types.SimpleNamespace(
|
||||
st_mode=status_module.stat.S_IFDIR | 0o700, st_uid=0, st_gid=0
|
||||
)
|
||||
group_writable = types.SimpleNamespace(
|
||||
st_mode=status_module.stat.S_IFDIR | 0o770, st_uid=0, st_gid=0
|
||||
)
|
||||
wrong_owner = types.SimpleNamespace(
|
||||
st_mode=status_module.stat.S_IFDIR | 0o700, st_uid=1000, st_gid=0
|
||||
)
|
||||
assert status_module._forge_app_dir_metadata_is_safe(safe) is True
|
||||
assert status_module._forge_app_dir_metadata_is_safe(group_writable) is False
|
||||
assert status_module._forge_app_dir_metadata_is_safe(wrong_owner) is False
|
||||
|
||||
|
||||
def test_forge_local_image_query_failure_is_unknown(status_module, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
status_module.subprocess,
|
||||
"run",
|
||||
lambda *_args, **_kwargs: subprocess.CompletedProcess(
|
||||
[], 1, stdout="", stderr="daemon unavailable"
|
||||
),
|
||||
)
|
||||
assert (
|
||||
status_module._forge_local_image_present(
|
||||
"gitea.example.com/jens/itworx-forge@sha256:" + "a" * 64
|
||||
)
|
||||
is None
|
||||
)
|
||||
@@ -0,0 +1,214 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
|
||||
from backend.app.db.models import DeploymentJob
|
||||
from backend.app.schemas.enrollment import EnrollmentTrustConfigV1
|
||||
from backend.app.schemas.forge_bootstrap import ForgeBootstrapPreflightV1
|
||||
from backend.app.services.enrollment_plane import EnrollmentPlane
|
||||
from backend.app.services.forge_bootstrap_preflight import (
|
||||
ForgeBootstrapPreflightService,
|
||||
)
|
||||
|
||||
|
||||
def _profile(**overrides):
|
||||
value = {
|
||||
"api_version": "itworx.forge-bootstrap-preflight/v1",
|
||||
"revision": "a" * 40,
|
||||
"workspace_image_digest": "sha256:" + "b" * 64,
|
||||
"frontdoor_image_digest": "sha256:" + "c" * 64,
|
||||
}
|
||||
value.update(overrides)
|
||||
return ForgeBootstrapPreflightV1.model_validate(value)
|
||||
|
||||
|
||||
def _session(tmp_path):
|
||||
engine = create_engine(f"sqlite:///{tmp_path / 'forge-preflight.db'}")
|
||||
SQLModel.metadata.create_all(engine)
|
||||
return Session(engine)
|
||||
|
||||
|
||||
def test_forge_profile_is_strict_and_cannot_select_runtime_inputs():
|
||||
_profile()
|
||||
for key, value in (
|
||||
("host_path", "/etc"),
|
||||
("endpoint", "https://attacker.invalid"),
|
||||
("secret_ref", "OTHER_SECRET"),
|
||||
("command", "docker compose up"),
|
||||
("repository", "other/repo"),
|
||||
("target", "other-host"),
|
||||
):
|
||||
try:
|
||||
_profile(**{key: value})
|
||||
except ValueError:
|
||||
pass
|
||||
else: # pragma: no cover - assertion clarity
|
||||
raise AssertionError(f"caller-controlled {key} must be rejected")
|
||||
|
||||
for field, value in (
|
||||
("revision", "a" * 39),
|
||||
("workspace_image_digest", "latest"),
|
||||
("frontdoor_image_digest", "sha256:" + "D" * 64),
|
||||
):
|
||||
try:
|
||||
_profile(**{field: value})
|
||||
except ValueError:
|
||||
pass
|
||||
else: # pragma: no cover - assertion clarity
|
||||
raise AssertionError(f"invalid {field} must be rejected")
|
||||
|
||||
|
||||
class _Broker:
|
||||
def __init__(self, result):
|
||||
self.result = result
|
||||
self.calls = []
|
||||
|
||||
def forge_bootstrap_preflight(self, profile, *, secret):
|
||||
self.calls.append((profile.model_dump(mode="json"), secret))
|
||||
return self.result
|
||||
|
||||
|
||||
def _trust(session, *, root="/var/lib/docker/volumes/itworx-managed-appdata/_data"):
|
||||
EnrollmentPlane.set_trust_config(
|
||||
session,
|
||||
EnrollmentTrustConfigV1(
|
||||
controller_identity="tower-deployment-controller",
|
||||
bootstrap_secret_ref="CONTROLLER_BOOTSTRAP",
|
||||
trusted_appdata_root=root,
|
||||
managed_appdata_backup_acknowledged=(
|
||||
root == "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
||||
),
|
||||
approved_networks=("itworx-mcp-hub-npm",),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_forge_preflight_uses_stored_trust_and_preserves_unknowns(tmp_path, monkeypatch):
|
||||
from backend.app.services import forge_bootstrap_preflight as service_module
|
||||
|
||||
broker = _Broker(
|
||||
{
|
||||
"apiVersion": "itworx.forge-bootstrap-preflight/v1",
|
||||
"status": "preflight_blocked",
|
||||
"revision": "a" * 40,
|
||||
"workspaceImageDigest": "sha256:" + "b" * 64,
|
||||
"frontdoorImageDigest": "sha256:" + "c" * 64,
|
||||
"checks": {
|
||||
"exact_main": {"status": "passed"},
|
||||
"source_manifest": {"status": "passed"},
|
||||
"managed_appdata": {"status": "blocked"},
|
||||
"proxy_network": {"status": "passed"},
|
||||
"resource_collisions": {"status": "passed"},
|
||||
"push_validation": {"status": "unknown"},
|
||||
"image_registry": {"status": "unknown"},
|
||||
"oidc": {"status": "unknown"},
|
||||
"secret_delivery": {"status": "unknown"},
|
||||
},
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(service_module, "get_secret", lambda *_args, **_kwargs: "s" * 48)
|
||||
monkeypatch.setattr(service_module, "has_secret", lambda *_args, **_kwargs: True)
|
||||
|
||||
with _session(tmp_path) as session:
|
||||
_trust(session)
|
||||
result = ForgeBootstrapPreflightService(broker=broker).preflight(
|
||||
session, _profile(), idempotency_key="forge-preflight-test-0001"
|
||||
)
|
||||
|
||||
assert result["status"] == "preflight_failed"
|
||||
assert result["preflight"]["status"] == "blocked"
|
||||
assert "push_validation" in {
|
||||
item["name"] for item in result["preflight"]["checks"]
|
||||
if item["status"] == "unknown"
|
||||
}
|
||||
assert broker.calls == [(_profile().model_dump(mode="json"), "s" * 48)]
|
||||
job = session.get(DeploymentJob, result["id"])
|
||||
assert job is not None
|
||||
assert job.status == "preflight_failed"
|
||||
assert ForgeBootstrapPreflightService().refresh(session, job.id)["id"] == job.id
|
||||
assert any(item["id"] == job.id for item in ForgeBootstrapPreflightService.list_jobs(session))
|
||||
|
||||
|
||||
def test_forge_preflight_idempotency_rejects_profile_change(tmp_path, monkeypatch):
|
||||
from backend.app.services import forge_bootstrap_preflight as service_module
|
||||
|
||||
broker = _Broker(
|
||||
{
|
||||
"apiVersion": "itworx.forge-bootstrap-preflight/v1",
|
||||
"status": "preflight_unknown",
|
||||
"revision": "a" * 40,
|
||||
"workspaceImageDigest": "sha256:" + "b" * 64,
|
||||
"frontdoorImageDigest": "sha256:" + "c" * 64,
|
||||
"checks": {
|
||||
"exact_main": {"status": "unknown"},
|
||||
"source_manifest": {"status": "unknown"},
|
||||
"managed_appdata": {"status": "unknown"},
|
||||
"proxy_network": {"status": "unknown"},
|
||||
"resource_collisions": {"status": "unknown"},
|
||||
"push_validation": {"status": "unknown"},
|
||||
"image_registry": {"status": "unknown"},
|
||||
"oidc": {"status": "unknown"},
|
||||
"secret_delivery": {"status": "unknown"},
|
||||
},
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(service_module, "get_secret", lambda *_args, **_kwargs: "s" * 48)
|
||||
monkeypatch.setattr(service_module, "has_secret", lambda *_args, **_kwargs: True)
|
||||
|
||||
with _session(tmp_path) as session:
|
||||
_trust(session)
|
||||
service = ForgeBootstrapPreflightService(broker=broker)
|
||||
service.preflight(session, _profile(), idempotency_key="forge-preflight-test-0002")
|
||||
replay = service.preflight(session, _profile(), idempotency_key="forge-preflight-test-0002")
|
||||
assert replay["idempotentReplay"] is True
|
||||
assert len(broker.calls) == 1
|
||||
try:
|
||||
service.preflight(
|
||||
session,
|
||||
_profile(revision="d" * 40),
|
||||
idempotency_key="forge-preflight-test-0002",
|
||||
)
|
||||
except ValueError as exc:
|
||||
assert "different Forge profile" in str(exc)
|
||||
else: # pragma: no cover
|
||||
raise AssertionError("idempotency key reuse with changed Forge profile must fail")
|
||||
|
||||
|
||||
def test_legacy_storage_trust_still_collects_read_only_controller_blockers(tmp_path, monkeypatch):
|
||||
from backend.app.services import forge_bootstrap_preflight as service_module
|
||||
|
||||
checks = {
|
||||
name: {"status": "unknown"}
|
||||
for name in (
|
||||
"exact_main", "source_manifest", "managed_appdata", "proxy_network",
|
||||
"resource_collisions", "push_validation", "image_registry", "oidc",
|
||||
"secret_delivery",
|
||||
)
|
||||
}
|
||||
checks["managed_appdata"] = {"status": "blocked"}
|
||||
broker = _Broker(
|
||||
{
|
||||
"apiVersion": "itworx.forge-bootstrap-preflight/v1",
|
||||
"status": "preflight_blocked",
|
||||
"revision": "a" * 40,
|
||||
"workspaceImageDigest": "sha256:" + "b" * 64,
|
||||
"frontdoorImageDigest": "sha256:" + "c" * 64,
|
||||
"checks": checks,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(service_module, "get_secret", lambda *_args, **_kwargs: "s" * 48)
|
||||
monkeypatch.setattr(service_module, "has_secret", lambda *_args, **_kwargs: True)
|
||||
|
||||
with _session(tmp_path) as session:
|
||||
_trust(session, root="/srv/example/appdata")
|
||||
result = ForgeBootstrapPreflightService(broker=broker).preflight(
|
||||
session, _profile(), idempotency_key="forge-legacy-trust-0001"
|
||||
)
|
||||
|
||||
assert broker.calls == [(_profile().model_dump(mode="json"), "s" * 48)]
|
||||
assert "managed_appdata_root_selection" in result["preflight"]["blockers"]
|
||||
controller_checks = {
|
||||
item["name"]: item["status"] for item in result["preflight"]["checks"]
|
||||
}
|
||||
assert controller_checks["managed_appdata"] == "blocked"
|
||||
assert controller_checks["proxy_network"] == "unknown"
|
||||
@@ -231,6 +231,59 @@ def test_n8n_uses_the_shared_grounded_knowledge_service(integration_client):
|
||||
)
|
||||
|
||||
|
||||
def test_forge_bootstrap_preflight_is_read_only_and_rejects_generic_apply(integration_client):
|
||||
client, _engine = integration_client
|
||||
from backend.app import mcp_server
|
||||
|
||||
profile = {
|
||||
"api_version": "itworx.forge-bootstrap-preflight/v1",
|
||||
"revision": "a" * 40,
|
||||
"workspace_image_digest": "sha256:" + "b" * 64,
|
||||
"frontdoor_image_digest": "sha256:" + "c" * 64,
|
||||
}
|
||||
response = client.post(
|
||||
"/api/deployments/forge-bootstrap/preflight",
|
||||
headers={**human_headers(client), "Idempotency-Key": "forge-api-preflight-0001"},
|
||||
json=profile,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["status"] == "preflight_failed"
|
||||
assert result["preflight"]["status"] == "blocked"
|
||||
assert "deployment_apply_policy" in result["preflight"]["blockers"]
|
||||
assert "controller_trust" in result["preflight"]["blockers"]
|
||||
|
||||
refreshed = client.get(
|
||||
f"/api/deployments/{result['id']}", headers=human_headers(client)
|
||||
)
|
||||
assert refreshed.status_code == 200
|
||||
assert refreshed.json()["preflight"]["status"] == "blocked"
|
||||
listed = client.get("/api/deployments", headers=human_headers(client))
|
||||
assert any(item["id"] == result["id"] for item in listed.json()["items"])
|
||||
|
||||
generic_apply = client.post(
|
||||
"/api/deployments",
|
||||
headers={**human_headers(client), "Idempotency-Key": "forge-generic-apply-0001"},
|
||||
json={"operation": "apply", "plan": profile},
|
||||
)
|
||||
assert generic_apply.status_code == 422
|
||||
|
||||
async def exercise_mcp():
|
||||
async with Client(mcp_server.appops_mcp) as mcp_client:
|
||||
preflight = await mcp_client.call_tool(
|
||||
"deployment.preflight",
|
||||
{"plan": profile, "idempotencyKey": "forge-mcp-preflight-0001"},
|
||||
)
|
||||
assert preflight.structured_content["preflight"]["status"] == "blocked"
|
||||
apply = await mcp_client.call_tool(
|
||||
"deployment.apply",
|
||||
{"plan": profile, "idempotencyKey": "forge-mcp-apply-0001"},
|
||||
)
|
||||
assert apply.structured_content["status"] == "invalid"
|
||||
|
||||
asyncio.run(exercise_mcp())
|
||||
|
||||
|
||||
def test_local_rag_indexes_allowlisted_docs_and_redacts_secrets(tmp_path):
|
||||
from backend.app.services import knowledge_service
|
||||
|
||||
|
||||
Reference in new issue
Block a user