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
@@ -0,0 +1,926 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def controller():
|
||||
if "fcntl" not in sys.modules:
|
||||
shim = types.ModuleType("fcntl")
|
||||
shim.LOCK_EX, shim.LOCK_UN, shim.flock = 2, 8, lambda *_args: None
|
||||
sys.modules["fcntl"] = shim
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"hub_candidate_controller", ROOT / "ops/gitea-deploy-broker/deploy.py"
|
||||
)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prepared(controller, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(controller, "STATE_ROOT", tmp_path)
|
||||
monkeypatch.setattr(controller, "_mcp_hub_require_trusted_state_root", lambda: None)
|
||||
monkeypatch.setattr(controller, "_mcp_hub_require_no_posix_acl", lambda _path: None)
|
||||
revision = "a" * 40
|
||||
directory = tmp_path / "mcp-hub-release-candidates" / revision
|
||||
directory.mkdir(parents=True, mode=0o700)
|
||||
directory.parent.chmod(0o700)
|
||||
now = (
|
||||
datetime.now(timezone.utc)
|
||||
.isoformat(timespec="milliseconds")
|
||||
.replace("+00:00", "Z")
|
||||
)
|
||||
metadata = {
|
||||
"id": revision[:12],
|
||||
"repository": controller.MCP_HUB_REPOSITORY,
|
||||
"revision": revision,
|
||||
"tree": "b" * 40,
|
||||
"treeDigest": hashlib.sha256(("git-tree:" + "b" * 40).encode()).hexdigest(),
|
||||
"builtAt": now,
|
||||
}
|
||||
project = {
|
||||
"_mcp_hub_contract": controller.MCP_HUB_TOPOLOGY_CONTRACT,
|
||||
"_repository_path": str(tmp_path / "hub.git"),
|
||||
"_deployment_metadata": metadata,
|
||||
"_mcp_hub_source_date_epoch": "1790888863",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
controller, "deployment_metadata", lambda *_args, **_kwargs: metadata
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller, "mcp_hub_source_date_epoch", lambda *_args: "1790888863"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller, "mcp_hub_stage_release_source", lambda *_args: tmp_path / "release"
|
||||
)
|
||||
checks = []
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"_mcp_hub_check_candidate_exceptions",
|
||||
lambda *_args: checks.append("gate"),
|
||||
)
|
||||
images = {}
|
||||
model = {"services": {}}
|
||||
|
||||
def write(name, value):
|
||||
data = (
|
||||
json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n"
|
||||
).encode()
|
||||
path = directory / name
|
||||
path.write_bytes(data)
|
||||
path.chmod(0o600)
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
for index, service in enumerate(controller.MCP_HUB_SERVICES, 1):
|
||||
image_id = "sha256:" + f"{index:064x}"
|
||||
ref = f"fixture/{service}@sha256:" + f"{index:064x}"
|
||||
if service == "contextforge":
|
||||
ref = f"itworx-mcp-hub/contextforge-schema:{revision}"
|
||||
model["services"][service] = {"image": ref}
|
||||
vuln = {
|
||||
"SchemaVersion": 2,
|
||||
"Trivy": {"Version": "0.73.0"},
|
||||
"CreatedAt": now,
|
||||
"Metadata": {
|
||||
"ImageID": image_id,
|
||||
"ImageConfig": {"architecture": "amd64", "os": "linux"},
|
||||
},
|
||||
"Results": [
|
||||
{"Class": "os-pkgs", "Packages": [{"Name": "fixture", "Version": "1"}]}
|
||||
],
|
||||
}
|
||||
secret = {
|
||||
"ImageID": image_id,
|
||||
"CreatedAt": now,
|
||||
"TrivyVersion": "0.73.0",
|
||||
"FindingCount": 0,
|
||||
"SensitiveFindingDetailsPersisted": False,
|
||||
"Results": [{"FindingCount": 0}],
|
||||
}
|
||||
images[service] = {
|
||||
"imageRef": ref,
|
||||
"imageId": image_id,
|
||||
"vulnerabilitySha256": write(f"vulnerability-{service}.json", vuln),
|
||||
"secretSha256": write(f"secret-{service}.json", secret),
|
||||
}
|
||||
candidate = {
|
||||
"schema": "itworx.mcp-hub-release-candidate/v1",
|
||||
"repository": controller.MCP_HUB_REPOSITORY,
|
||||
"revision": revision,
|
||||
"build": metadata,
|
||||
"sourceDateEpoch": "1790888863",
|
||||
"images": images,
|
||||
"composeSha256": hashlib.sha256(
|
||||
json.dumps(model, sort_keys=True, separators=(",", ":")).encode()
|
||||
).hexdigest(),
|
||||
"databaseMetadataSha256": write(
|
||||
"trivy-db-metadata.json", {"UpdatedAt": now, "DownloadedAt": now}
|
||||
),
|
||||
"summarySha256": write("scan-summary.json", {"unresolvedCriticalOrHigh": []}),
|
||||
"digestsSha256": write(
|
||||
"image-digests.json",
|
||||
{
|
||||
"images": {
|
||||
s: {"imageId": v["imageId"], "tag": v["imageRef"]}
|
||||
for s, v in images.items()
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
candidate_sha = write("candidate.json", candidate)
|
||||
exceptions_sha = write("exceptions.json", {"exceptions": []})
|
||||
write(
|
||||
"approval.json",
|
||||
{
|
||||
"schema": "itworx.mcp-hub-cve-approval/v1",
|
||||
"candidateSha256": candidate_sha,
|
||||
"exceptionsSha256": exceptions_sha,
|
||||
},
|
||||
)
|
||||
labels = {
|
||||
"org.opencontainers.image.source": controller.MCP_HUB_OCI_IMAGE_SOURCE,
|
||||
"org.opencontainers.image.revision": revision,
|
||||
"org.opencontainers.image.source-tree": metadata["tree"],
|
||||
"org.opencontainers.image.version": metadata["id"],
|
||||
"org.opencontainers.image.created": metadata["builtAt"],
|
||||
"org.opencontainers.image.source-date-epoch": "1790888863",
|
||||
}
|
||||
facts = {
|
||||
v["imageRef"]: {
|
||||
"Id": v["imageId"],
|
||||
"Config": {"User": "10001:10001", "Labels": dict(labels)},
|
||||
}
|
||||
for v in images.values()
|
||||
}
|
||||
monkeypatch.setattr(controller, "inspect_image", lambda ref: facts.get(ref))
|
||||
return types.SimpleNamespace(
|
||||
project=project,
|
||||
revision=revision,
|
||||
directory=directory,
|
||||
candidate=candidate,
|
||||
model=model,
|
||||
metadata=metadata,
|
||||
write=write,
|
||||
checks=checks,
|
||||
facts=facts,
|
||||
)
|
||||
|
||||
|
||||
def test_fresh_complete_candidate_freezes_metadata_and_checks_explicit_gate(
|
||||
controller, prepared
|
||||
):
|
||||
result = controller.mcp_hub_load_release_candidate(
|
||||
prepared.project, prepared.revision
|
||||
)
|
||||
assert result["build"] == prepared.metadata
|
||||
assert set(result["images"]) == set(controller.MCP_HUB_SERVICES)
|
||||
assert prepared.checks == ["gate"]
|
||||
controller.mcp_hub_require_candidate_images(
|
||||
prepared.project, prepared.model, prepared.revision
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mutation",
|
||||
[
|
||||
"missing",
|
||||
"extra",
|
||||
"revision",
|
||||
"build",
|
||||
"epoch",
|
||||
"scan_hash",
|
||||
"scan_identity",
|
||||
"scan_empty",
|
||||
"secret",
|
||||
"stale",
|
||||
"db_stale",
|
||||
"approval",
|
||||
"summary",
|
||||
"digests",
|
||||
],
|
||||
)
|
||||
def test_candidate_drift_rejects_before_any_production_action(
|
||||
controller, prepared, mutation
|
||||
):
|
||||
value = copy.deepcopy(prepared.candidate)
|
||||
if mutation == "missing":
|
||||
value["images"].pop("web")
|
||||
elif mutation == "extra":
|
||||
value["images"]["foreign"] = value["images"]["web"]
|
||||
elif mutation == "revision":
|
||||
value["revision"] = "c" * 40
|
||||
elif mutation == "build":
|
||||
value["build"]["tree"] = "c" * 40
|
||||
elif mutation == "epoch":
|
||||
value["sourceDateEpoch"] = "1"
|
||||
elif mutation == "scan_hash":
|
||||
value["images"]["web"]["vulnerabilitySha256"] = "c" * 64
|
||||
elif mutation in {"scan_identity", "scan_empty", "stale"}:
|
||||
path = prepared.directory / "vulnerability-web.json"
|
||||
report = json.loads(path.read_text())
|
||||
if mutation == "scan_identity":
|
||||
report["Metadata"]["ImageID"] = "sha256:" + "c" * 64
|
||||
elif mutation == "scan_empty":
|
||||
report["Results"] = []
|
||||
else:
|
||||
report["CreatedAt"] = "2026-01-01T00:00:00Z"
|
||||
value["images"]["web"]["vulnerabilitySha256"] = prepared.write(
|
||||
path.name, report
|
||||
)
|
||||
elif mutation == "secret":
|
||||
report = json.loads((prepared.directory / "secret-web.json").read_text())
|
||||
report["FindingCount"] = 1
|
||||
value["images"]["web"]["secretSha256"] = prepared.write(
|
||||
"secret-web.json", report
|
||||
)
|
||||
elif mutation == "db_stale":
|
||||
value["databaseMetadataSha256"] = prepared.write(
|
||||
"trivy-db-metadata.json",
|
||||
{
|
||||
"UpdatedAt": "2026-01-01T00:00:00Z",
|
||||
"DownloadedAt": "2026-01-01T00:00:00Z",
|
||||
},
|
||||
)
|
||||
elif mutation == "summary":
|
||||
value["summarySha256"] = prepared.write(
|
||||
"scan-summary.json", {"unresolvedCriticalOrHigh": [{"package": "invented"}]}
|
||||
)
|
||||
elif mutation == "digests":
|
||||
value["digestsSha256"] = prepared.write("image-digests.json", {"images": {}})
|
||||
candidate_sha = prepared.write("candidate.json", value)
|
||||
exceptions_sha = hashlib.sha256(
|
||||
(prepared.directory / "exceptions.json").read_bytes()
|
||||
).hexdigest()
|
||||
prepared.write(
|
||||
"approval.json",
|
||||
{
|
||||
"schema": "itworx.mcp-hub-cve-approval/v1",
|
||||
"candidateSha256": "c" * 64 if mutation == "approval" else candidate_sha,
|
||||
"exceptionsSha256": exceptions_sha,
|
||||
},
|
||||
)
|
||||
with pytest.raises(controller.DeployError):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
assert prepared.checks == []
|
||||
|
||||
|
||||
def test_pending_or_expired_human_exception_gate_cannot_be_substituted(
|
||||
controller, prepared, monkeypatch
|
||||
):
|
||||
def denied(*_args):
|
||||
raise controller.DeployError("CVE exception gate did not pass")
|
||||
|
||||
monkeypatch.setattr(controller, "_mcp_hub_check_candidate_exceptions", denied)
|
||||
with pytest.raises(controller.DeployError, match="CVE exception gate"):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"service", ["web", "contextforge", "postgres", "connector-ingestion"]
|
||||
)
|
||||
def test_retagged_or_changed_candidate_image_is_rejected_before_apply(
|
||||
controller, prepared, service
|
||||
):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
prepared.facts[prepared.candidate["images"][service]["imageRef"]]["Id"] = (
|
||||
"sha256:" + "f" * 64
|
||||
)
|
||||
with pytest.raises(controller.DeployError, match="image identity"):
|
||||
controller.mcp_hub_require_candidate_images(
|
||||
prepared.project, prepared.model, prepared.revision
|
||||
)
|
||||
|
||||
|
||||
def test_changed_rendered_model_is_rejected(controller, prepared):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
prepared.model["services"]["web"]["environment"] = {"unexpected": "change"}
|
||||
with pytest.raises(controller.DeployError, match="Compose"):
|
||||
controller.mcp_hub_require_candidate_images(
|
||||
prepared.project, prepared.model, prepared.revision
|
||||
)
|
||||
|
||||
|
||||
def test_changed_approval_is_rejected_between_stages(controller, prepared):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
prepared.write("exceptions.json", {"exceptions": [{"approvalStatus": "pending"}]})
|
||||
with pytest.raises(controller.DeployError):
|
||||
controller.mcp_hub_require_candidate_images(
|
||||
prepared.project, prepared.model, prepared.revision
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("service", ["web", "contextforge"])
|
||||
@pytest.mark.parametrize(
|
||||
"label",
|
||||
[
|
||||
"org.opencontainers.image.source",
|
||||
"org.opencontainers.image.revision",
|
||||
"org.opencontainers.image.source-tree",
|
||||
"org.opencontainers.image.source-date-epoch",
|
||||
"org.opencontainers.image.created",
|
||||
"org.opencontainers.image.version",
|
||||
],
|
||||
)
|
||||
def test_frozen_image_provenance_is_checked_before_database_intent(
|
||||
controller, prepared, service, label
|
||||
):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
prepared.facts[prepared.candidate["images"][service]["imageRef"]]["Config"][
|
||||
"Labels"
|
||||
][label] = "wrong"
|
||||
with pytest.raises(controller.DeployError, match="provenance"):
|
||||
controller.mcp_hub_require_candidate_images(
|
||||
prepared.project, prepared.model, prepared.revision
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("service", ["web", "contextforge"])
|
||||
@pytest.mark.parametrize("user", ["", "root", "root:10001", "0", "00:10001"])
|
||||
def test_frozen_image_root_user_is_rejected_before_database_intent(
|
||||
controller, prepared, service, user
|
||||
):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
prepared.facts[prepared.candidate["images"][service]["imageRef"]]["Config"][
|
||||
"User"
|
||||
] = user
|
||||
with pytest.raises(controller.DeployError, match="non-root runtime"):
|
||||
controller.mcp_hub_require_candidate_images(
|
||||
prepared.project, prepared.model, prepared.revision
|
||||
)
|
||||
|
||||
|
||||
def test_changed_raw_scan_is_rejected_between_stages(controller, prepared):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
prepared.write("vulnerability-contextforge.json", {"Results": []})
|
||||
with pytest.raises(controller.DeployError, match="hash"):
|
||||
controller.mcp_hub_require_candidate_images(
|
||||
prepared.project, prepared.model, prepared.revision
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("changed", [False, True])
|
||||
def test_staged_start_consumes_no_build_no_pull_and_checks_running_ids(
|
||||
controller, prepared, monkeypatch, changed
|
||||
):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
prepared.project["_mcp_hub_rendered_model"] = prepared.model
|
||||
commands = []
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"compose_command",
|
||||
lambda _p: (prepared.directory, ["docker", "compose"]),
|
||||
)
|
||||
monkeypatch.setattr(controller, "deployment_environment", lambda *_args: {})
|
||||
monkeypatch.setattr(
|
||||
controller, "run", lambda command, **_kwargs: commands.append(command)
|
||||
)
|
||||
monkeypatch.setattr(controller, "mcp_hub_wait_services", lambda *_args: None)
|
||||
expected = prepared.candidate["images"]["postgres"]["imageId"]
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"inspect_container",
|
||||
lambda _name: {"Image": "sha256:" + "f" * 64 if changed else expected},
|
||||
)
|
||||
if changed:
|
||||
with pytest.raises(controller.DeployError, match="exact scanned image"):
|
||||
controller.mcp_hub_up_services(
|
||||
prepared.project,
|
||||
prepared.revision,
|
||||
controller.MCP_HUB_REPOSITORY,
|
||||
("postgres",),
|
||||
)
|
||||
else:
|
||||
controller.mcp_hub_up_services(
|
||||
prepared.project,
|
||||
prepared.revision,
|
||||
controller.MCP_HUB_REPOSITORY,
|
||||
("postgres",),
|
||||
)
|
||||
assert commands == [
|
||||
[
|
||||
"docker",
|
||||
"compose",
|
||||
"up",
|
||||
"--detach",
|
||||
"--no-deps",
|
||||
"--no-build",
|
||||
"--pull",
|
||||
"never",
|
||||
"postgres",
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation", ["start", "migrate"])
|
||||
def test_compose_operations_pin_all_scanned_ids_without_changing_source_model(
|
||||
controller, prepared, monkeypatch, operation
|
||||
):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
prepared.project["_mcp_hub_rendered_model"] = prepared.model
|
||||
original = copy.deepcopy(prepared.model)
|
||||
observed = []
|
||||
|
||||
def compose(project):
|
||||
path = Path(project["_mcp_hub_image_override_file"])
|
||||
assert path.stat().st_mode & 0o777 == 0o600
|
||||
observed.append((path, json.loads(path.read_text())))
|
||||
return prepared.directory, ["docker", "compose", "-f", str(path)]
|
||||
|
||||
commands = []
|
||||
monkeypatch.setattr(controller, "compose_command", compose)
|
||||
monkeypatch.setattr(controller, "deployment_environment", lambda *_args: {})
|
||||
monkeypatch.setattr(controller, "run", lambda cmd, **_kwargs: commands.append(cmd))
|
||||
monkeypatch.setattr(controller, "mcp_hub_wait_services", lambda *_args: None)
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"inspect_container",
|
||||
lambda _name: {"Image": prepared.candidate["images"]["postgres"]["imageId"]},
|
||||
)
|
||||
if operation == "start":
|
||||
controller.mcp_hub_up_services(
|
||||
prepared.project,
|
||||
prepared.revision,
|
||||
controller.MCP_HUB_REPOSITORY,
|
||||
("postgres",),
|
||||
)
|
||||
else:
|
||||
controller.mcp_hub_run_database_migration(
|
||||
prepared.project, prepared.revision, controller.MCP_HUB_REPOSITORY
|
||||
)
|
||||
assert commands[0][-3:] == [
|
||||
"-e",
|
||||
"SERVICE_NAME=database-migrate",
|
||||
"control-api",
|
||||
]
|
||||
assert len(observed) == len(commands) == 1
|
||||
path, payload = observed[0]
|
||||
assert payload == {
|
||||
"services": {
|
||||
service: {"image": record["imageId"]}
|
||||
for service, record in prepared.candidate["images"].items()
|
||||
}
|
||||
}
|
||||
assert "--no-deps" in commands[0]
|
||||
assert commands[0][commands[0].index("--pull") + 1] == "never"
|
||||
assert not path.exists()
|
||||
assert "_mcp_hub_image_override_file" not in prepared.project
|
||||
assert prepared.model == original
|
||||
|
||||
|
||||
def test_migration_rechecks_candidate_before_running(controller, prepared, monkeypatch):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
prepared.project["_mcp_hub_rendered_model"] = prepared.model
|
||||
prepared.write("vulnerability-control-api.json", {"Results": []})
|
||||
monkeypatch.setattr(
|
||||
controller, "run", lambda *_args, **_kwargs: pytest.fail("must not run")
|
||||
)
|
||||
with pytest.raises(controller.DeployError, match="hash"):
|
||||
controller.mcp_hub_run_database_migration(
|
||||
prepared.project, prepared.revision, controller.MCP_HUB_REPOSITORY
|
||||
)
|
||||
|
||||
|
||||
def test_candidate_runtime_verifies_pinned_ids_after_migration(
|
||||
controller, prepared, monkeypatch
|
||||
):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
project = prepared.project
|
||||
project["compose_project"] = controller.MCP_HUB_PROJECT
|
||||
project["slug"] = "itworx-mcp-hub"
|
||||
project["source_dir"] = str(prepared.directory)
|
||||
project["compose_files"] = []
|
||||
project["containers"] = list(controller.MCP_HUB_CONTAINERS)
|
||||
project["_mcp_hub_rendered_model"] = prepared.model
|
||||
project["_mcp_hub_candidate_image_ids"] = (
|
||||
controller.mcp_hub_require_candidate_images(
|
||||
project, prepared.model, prepared.revision
|
||||
)
|
||||
)
|
||||
for service in ("control-api", "worker"):
|
||||
prepared.model["services"][service]["environment"] = {
|
||||
"INGESTION_CONNECTOR_ENABLED": "true"
|
||||
}
|
||||
containers = {}
|
||||
for service, record in prepared.candidate["images"].items():
|
||||
name = f"itworx-mcp-hub-{service}-1"
|
||||
containers[name] = {
|
||||
"Id": "c" * 64,
|
||||
"Image": record["imageId"],
|
||||
"Config": {
|
||||
"Image": record["imageId"],
|
||||
"Env": (
|
||||
["INGESTION_CONNECTOR_ENABLED=true"]
|
||||
if service in {"control-api", "worker"}
|
||||
else []
|
||||
),
|
||||
"Labels": {
|
||||
"com.docker.compose.project": controller.MCP_HUB_PROJECT,
|
||||
"com.docker.compose.service": service,
|
||||
},
|
||||
},
|
||||
"State": {"Running": True, "Health": {"Status": "healthy"}},
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
controller, "compose_project_container_names", lambda _project: set(containers)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller, "inspect_container", lambda name: containers.get(name)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"container_state",
|
||||
lambda _name: (True, "healthy", controller.MCP_HUB_PROJECT),
|
||||
)
|
||||
labels = {
|
||||
"org.opencontainers.image.revision": prepared.revision,
|
||||
"org.opencontainers.image.source": controller.MCP_HUB_OCI_IMAGE_SOURCE,
|
||||
"org.opencontainers.image.source-tree": prepared.metadata["tree"],
|
||||
"org.opencontainers.image.version": prepared.metadata["id"],
|
||||
"org.opencontainers.image.created": prepared.metadata["builtAt"],
|
||||
}
|
||||
image_ids_by_ref = {
|
||||
record["imageRef"]: record["imageId"]
|
||||
for record in prepared.candidate["images"].values()
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"inspect_image",
|
||||
lambda image_ref: {
|
||||
"Id": image_ids_by_ref.get(image_ref, image_ref),
|
||||
"Config": {"Labels": labels},
|
||||
"RepoDigests": [],
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller, "mcp_hub_attested_live_mounts", lambda *_args, **_kwargs: []
|
||||
)
|
||||
assert set(controller.verify_mcp_hub_live_topology(project, prepared.model)) == set(
|
||||
containers
|
||||
)
|
||||
assert set(
|
||||
controller.verify_containers(
|
||||
project, expected_image_ids=project["_mcp_hub_candidate_image_ids"]
|
||||
)
|
||||
) == set(containers)
|
||||
for service in controller.MCP_HUB_APPLICATION_SERVICES:
|
||||
prepared.model["services"][service]["build"] = {"context": "."}
|
||||
manifest = (
|
||||
"CONTEXTFORGE_UPSTREAM_COMMIT=" + "1" * 40 + "\n"
|
||||
"CONTEXTFORGE_ALEMBIC_HEAD=12d4a0c7789c\n"
|
||||
"CONTEXTFORGE_MCP_SDK=1.30.0\n"
|
||||
"CONTEXTFORGE_LOCAL_IMAGE=itworx-mcp-hub/contextforge-schema:12d4a0c7789c\n"
|
||||
)
|
||||
builder = "\n".join(
|
||||
[
|
||||
"--release-candidate",
|
||||
'CANDIDATE_IMAGE="itworx-mcp-hub/contextforge-schema:${APPOPS_SOURCE_REVISION}"',
|
||||
'docker tag "$STAGED_IMAGE" "$CANDIDATE_IMAGE"',
|
||||
'git -C "$SRC" fetch --depth 1 origin "$CONTEXTFORGE_UPSTREAM_COMMIT"',
|
||||
'git -C "$SRC" checkout --detach FETCH_HEAD',
|
||||
"alembic -c mcpgateway/alembic.ini heads",
|
||||
'docker tag "$STAGED_IMAGE" "$CONTEXTFORGE_LOCAL_IMAGE"',
|
||||
]
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"_mcp_hub_git_output",
|
||||
lambda _project, _show, path: manifest
|
||||
if path.endswith("compat-image.env")
|
||||
else builder,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller, "_mcp_hub_dataplane_compat_proven", lambda *_args: True
|
||||
)
|
||||
monkeypatch.setattr(controller, "_mcp_hub_digest_ref_proven", lambda *_args: True)
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"_mcp_hub_live_image_proven",
|
||||
lambda _project, name, item, source_ref, *_args, **_kwargs: {
|
||||
"container_id": item["Id"],
|
||||
"image_id": item["Image"],
|
||||
"image_ref": source_ref,
|
||||
"container_revision": "",
|
||||
"image_revision": prepared.revision,
|
||||
"image_source": controller.MCP_HUB_OCI_IMAGE_SOURCE,
|
||||
"image_tree": prepared.metadata["tree"],
|
||||
"image_version": prepared.metadata["id"],
|
||||
"image_created": prepared.metadata["builtAt"],
|
||||
},
|
||||
)
|
||||
receipt = controller.verify_deployment(
|
||||
project,
|
||||
prepared.metadata,
|
||||
expected_image_ids=project["_mcp_hub_candidate_image_ids"],
|
||||
)
|
||||
for service in controller.MCP_HUB_APPLICATION_SERVICES:
|
||||
name = f"itworx-mcp-hub-{service}-1"
|
||||
assert (
|
||||
receipt["containers"][name]["image_ref"]
|
||||
== receipt["containers"][name]["image_id"]
|
||||
)
|
||||
assert (
|
||||
controller.validate_recorded_receipt(
|
||||
project, receipt, controller.MCP_HUB_REPOSITORY, prepared.revision
|
||||
)
|
||||
== prepared.metadata
|
||||
)
|
||||
state = {}
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"_mcp_hub_read_controller_state_bytes",
|
||||
lambda **_kwargs: state.get("bytes"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"_mcp_hub_write_controller_state_bytes",
|
||||
lambda payload, **_kwargs: state.__setitem__("bytes", payload),
|
||||
)
|
||||
controller.write_state(
|
||||
controller.MCP_HUB_REPOSITORY,
|
||||
project,
|
||||
prepared.revision,
|
||||
"success",
|
||||
"verified",
|
||||
live_receipt=receipt,
|
||||
)
|
||||
saved = json.loads(state["bytes"])
|
||||
assert saved["live_status"] == "healthy"
|
||||
for name in controller.MCP_HUB_CONTAINERS:
|
||||
assert (
|
||||
saved["live_artifacts"]["containers"][name]["image_ref"]
|
||||
== receipt["containers"][name]["image_ref"]
|
||||
)
|
||||
project.pop("_mcp_hub_candidate_image_ids")
|
||||
assert (
|
||||
controller.verified_current_receipt(
|
||||
controller.MCP_HUB_REPOSITORY,
|
||||
project,
|
||||
prepared.revision,
|
||||
prepared.metadata["tree"],
|
||||
)
|
||||
== receipt
|
||||
)
|
||||
assert "_mcp_hub_receipt_image_ids" not in project
|
||||
project["_repository_name"] = controller.MCP_HUB_REPOSITORY
|
||||
rollback_commands = []
|
||||
monkeypatch.setattr(
|
||||
controller, "run", lambda command, **_kwargs: rollback_commands.append(command)
|
||||
)
|
||||
snapshot = controller.capture_compose_rollback(project)
|
||||
assert len(snapshot["containers"]) == len(rollback_commands) == 17
|
||||
for item in snapshot["containers"]:
|
||||
assert item["image_ref"] == receipt["containers"][item["name"]]["image_ref"]
|
||||
cf = prepared.candidate["images"]["contextforge"]
|
||||
assert not controller._mcp_hub_contextforge_compat_proven(
|
||||
project, prepared.revision, cf["imageRef"], "sha256:" + "f" * 64
|
||||
)
|
||||
containers["itworx-mcp-hub-web-1"]["Config"]["Image"] = "web:mutable"
|
||||
with pytest.raises(controller.DeployError, match="identity or health"):
|
||||
controller.verify_mcp_hub_live_topology(project, prepared.model)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("same_revision", [True, False])
|
||||
def test_same_revision_receipt_prepares_exact_source_before_verification(
|
||||
controller, prepared, monkeypatch, same_revision
|
||||
):
|
||||
project = prepared.project
|
||||
project.update({"source_dir": str(prepared.directory), "slug": "itworx-mcp-hub"})
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"read_state",
|
||||
lambda _project: {
|
||||
"live_revision": prepared.revision if same_revision else "b" * 40
|
||||
},
|
||||
)
|
||||
events = []
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"sync_source",
|
||||
lambda *_args, **kwargs: events.append(("sync", kwargs["exact"])),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller, "copy_environment", lambda *_args: events.append("environment")
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"mcp_hub_compose_model",
|
||||
lambda *_args: events.append("model") or prepared.model,
|
||||
)
|
||||
controller.mcp_hub_prepare_current_receipt_model(
|
||||
prepared.directory, project, controller.MCP_HUB_REPOSITORY, prepared.revision
|
||||
)
|
||||
assert events == ([("sync", True), "environment", "model"] if same_revision else [])
|
||||
assert (project.get("_mcp_hub_rendered_model") is prepared.model) is same_revision
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mutation", ["missing", "extra", "tag", "nonmapping"])
|
||||
def test_candidate_compose_override_rejects_malformed_ids(
|
||||
controller, prepared, mutation
|
||||
):
|
||||
ids = {
|
||||
f"itworx-mcp-hub-{service}-1": record["imageId"]
|
||||
for service, record in prepared.candidate["images"].items()
|
||||
}
|
||||
if mutation == "missing":
|
||||
ids.pop("itworx-mcp-hub-web-1")
|
||||
elif mutation == "extra":
|
||||
ids["foreign"] = "sha256:" + "a" * 64
|
||||
elif mutation == "tag":
|
||||
ids["itworx-mcp-hub-web-1"] = "web:latest"
|
||||
else:
|
||||
ids = []
|
||||
prepared.project["_mcp_hub_rendered_model"] = prepared.model
|
||||
prepared.project["_mcp_hub_candidate_image_ids"] = ids
|
||||
with pytest.raises(controller.DeployError, match="candidate image IDs"):
|
||||
with controller._mcp_hub_compose_image_override(prepared.project):
|
||||
pytest.fail("must reject invalid image IDs")
|
||||
|
||||
|
||||
def test_actual_high_findings_are_bound_to_current_service_tags(controller, prepared):
|
||||
report = json.loads((prepared.directory / "vulnerability-web.json").read_text())
|
||||
report["Results"][0]["Vulnerabilities"] = [
|
||||
{
|
||||
"PkgName": "fixture",
|
||||
"VulnerabilityID": "CVE-2026-12345",
|
||||
"InstalledVersion": "1",
|
||||
"Severity": "HIGH",
|
||||
}
|
||||
]
|
||||
prepared.candidate["images"]["web"]["vulnerabilitySha256"] = prepared.write(
|
||||
"vulnerability-web.json", report
|
||||
)
|
||||
summary = {
|
||||
"unresolvedCriticalOrHigh": [
|
||||
{
|
||||
"package": "fixture",
|
||||
"cves": ["CVE-2026-12345"],
|
||||
"installedVersion": "1",
|
||||
"severity": "HIGH",
|
||||
"affectedImages": [prepared.candidate["images"]["web"]["imageRef"]],
|
||||
}
|
||||
]
|
||||
}
|
||||
prepared.candidate["summarySha256"] = prepared.write("scan-summary.json", summary)
|
||||
candidate_sha = prepared.write("candidate.json", prepared.candidate)
|
||||
prepared.write(
|
||||
"approval.json",
|
||||
{
|
||||
"schema": "itworx.mcp-hub-cve-approval/v1",
|
||||
"candidateSha256": candidate_sha,
|
||||
"exceptionsSha256": hashlib.sha256(
|
||||
(prepared.directory / "exceptions.json").read_bytes()
|
||||
).hexdigest(),
|
||||
},
|
||||
)
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
assert prepared.checks == ["gate"]
|
||||
|
||||
|
||||
def test_fixable_high_findings_cannot_be_approved_around(controller, prepared):
|
||||
report = json.loads((prepared.directory / "vulnerability-web.json").read_text())
|
||||
report["Results"][0]["Vulnerabilities"] = [
|
||||
{
|
||||
"PkgName": "fixture",
|
||||
"VulnerabilityID": "CVE-2026-12345",
|
||||
"InstalledVersion": "1",
|
||||
"FixedVersion": "2",
|
||||
"Severity": "HIGH",
|
||||
}
|
||||
]
|
||||
prepared.candidate["images"]["web"]["vulnerabilitySha256"] = prepared.write(
|
||||
"vulnerability-web.json", report
|
||||
)
|
||||
candidate_sha = prepared.write("candidate.json", prepared.candidate)
|
||||
prepared.write(
|
||||
"approval.json",
|
||||
{
|
||||
"schema": "itworx.mcp-hub-cve-approval/v1",
|
||||
"candidateSha256": candidate_sha,
|
||||
"exceptionsSha256": hashlib.sha256(
|
||||
(prepared.directory / "exceptions.json").read_bytes()
|
||||
).hexdigest(),
|
||||
},
|
||||
)
|
||||
with pytest.raises(controller.DeployError, match="fixable critical/high"):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
assert prepared.checks == []
|
||||
|
||||
|
||||
def test_missing_normal_upgrade_volume_baseline_denies_before_intent_or_start(
|
||||
controller, prepared, monkeypatch
|
||||
):
|
||||
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
||||
monkeypatch.setattr(controller, "mcp_hub_require_docker_capacity", lambda: None)
|
||||
monkeypatch.setattr(
|
||||
controller, "mcp_hub_stage_release_source", lambda *_args: prepared.directory
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller, "mcp_hub_compose_model", lambda *_args: prepared.model
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"compose_command",
|
||||
lambda *_args: (prepared.directory, ["docker", "compose"]),
|
||||
)
|
||||
monkeypatch.setattr(controller, "deployment_environment", lambda *_args: {})
|
||||
monkeypatch.setattr(
|
||||
controller.subprocess,
|
||||
"run",
|
||||
lambda *_args, **_kwargs: types.SimpleNamespace(returncode=0),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"_mcp_hub_begin_migration_intent",
|
||||
lambda *_args: pytest.fail(
|
||||
"missing volume identity must precede durable intent"
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"mcp_hub_up_services",
|
||||
lambda *_args: pytest.fail(
|
||||
"missing volume identity must precede Compose mutation"
|
||||
),
|
||||
)
|
||||
with pytest.raises(controller.DeployError, match="volume baseline"):
|
||||
controller.deploy_mcp_hub_staged(
|
||||
prepared.project, prepared.revision, controller.MCP_HUB_REPOSITORY
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, candidate_epoch",
|
||||
[("deploy", "1790888863"), ("deploy", "1"), ("preflight", "1790888863")],
|
||||
)
|
||||
def test_main_hub_path_sets_validated_epoch_before_compose_render(
|
||||
controller, prepared, monkeypatch, mode, candidate_epoch
|
||||
):
|
||||
class ReachedValidatedRender(Exception):
|
||||
pass
|
||||
|
||||
target = {
|
||||
**prepared.project,
|
||||
"branch": "main",
|
||||
"slug": controller.MCP_HUB_PROJECT,
|
||||
"source_dir": str(prepared.directory),
|
||||
}
|
||||
target["compose_project"] = controller.MCP_HUB_PROJECT
|
||||
target.pop("_mcp_hub_source_date_epoch", None)
|
||||
monkeypatch.setattr(
|
||||
controller, "load_projects", lambda: {controller.MCP_HUB_REPOSITORY: target}
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"effective_mcp_hub_project",
|
||||
lambda _repository, project: dict(project),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
controller, "repository_path", lambda *_args: prepared.directory
|
||||
)
|
||||
monkeypatch.setattr(controller, "validate_commit", lambda *_args: prepared.revision)
|
||||
candidate = copy.deepcopy(prepared.candidate)
|
||||
candidate["sourceDateEpoch"] = candidate_epoch
|
||||
monkeypatch.setattr(
|
||||
controller, "mcp_hub_load_release_candidate", lambda *_args: candidate
|
||||
)
|
||||
monkeypatch.setenv("SOURCE_DATE_EPOCH", "untrusted-environment-override")
|
||||
|
||||
def render(_repo, project, revision):
|
||||
if mode == "deploy" and candidate_epoch != "1790888863":
|
||||
pytest.fail(
|
||||
"mismatched candidate epoch must deny before Compose validation"
|
||||
)
|
||||
assert project["_mcp_hub_source_date_epoch"] == "1790888863"
|
||||
assert (
|
||||
controller.deployment_environment(
|
||||
project, revision, controller.MCP_HUB_REPOSITORY
|
||||
)["SOURCE_DATE_EPOCH"]
|
||||
== "1790888863"
|
||||
)
|
||||
raise ReachedValidatedRender()
|
||||
|
||||
monkeypatch.setattr(controller, "validate_project_config", render)
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
["deploy.py", mode, controller.MCP_HUB_REPOSITORY, prepared.revision],
|
||||
)
|
||||
if mode == "deploy" and candidate_epoch != "1790888863":
|
||||
with pytest.raises(controller.DeployError, match="epoch"):
|
||||
controller.main()
|
||||
else:
|
||||
with pytest.raises(ReachedValidatedRender):
|
||||
controller.main()
|
||||
Reference in new issue
Block a user