Publish curated Unraid-AppOps-Gateway source from a506550ef83ef2d81651e37c98a89f7ee330c730
This commit is contained in:
1 parent
2facb8de34
commit
767e66921e
68 files changed
+15924
-313
No files matched your search
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
@@ -53,7 +54,10 @@ def _project():
|
||||
"containers": ["Unraid-AppOps-Gateway"],
|
||||
"compose_project": "unraid-appops-gateway",
|
||||
"compose_files": ["docker-compose.yml"],
|
||||
"environment": {"APPOPS_DATA_PATH": "/srv/example/appdata/unraid-appops-gateway-1216"},
|
||||
"environment": {
|
||||
"APPOPS_DATA_PATH": "/srv/example/appdata/unraid-appops-gateway-1216",
|
||||
"APPOPS_PUBLISH_HOST": "198.18.10.150",
|
||||
},
|
||||
"verify_oci_revision": True,
|
||||
"validation": {
|
||||
"validation_gate_version": 1, "required": True,
|
||||
@@ -71,6 +75,9 @@ def _ready(controller, monkeypatch):
|
||||
monkeypatch.setattr(controller, "_fetch_gitea_validation", lambda repository, revision, policy: ("pass", []))
|
||||
monkeypatch.setattr(controller, "_self_upgrade_runtime", lambda: {
|
||||
"revision": "a" * 40, "imageId": "sha256:" + "b" * 64,
|
||||
"containerId": "c" * 64,
|
||||
"imageRef": "unraid-appops-gateway:production",
|
||||
"sourceTree": "d" * 40,
|
||||
"dataPath": "/srv/example/appdata/unraid-appops-gateway-1216",
|
||||
})
|
||||
monkeypatch.setattr(controller.shutil, "disk_usage", lambda path: SimpleNamespace(free=50 * 1024**3))
|
||||
@@ -150,24 +157,65 @@ def test_self_upgrade_preflight_rejects_non_sha_and_conflicting_upgrade(controll
|
||||
|
||||
|
||||
def test_runtime_requires_exact_container_mount_port_and_health(controller, monkeypatch):
|
||||
monkeypatch.setattr(controller, "_gitea_validation_settings", lambda: ("https://gitea.example.com/api/v1", b"t" * 32))
|
||||
payload = [{
|
||||
"State": {"Running": True, "Health": {"Status": "healthy"}},
|
||||
"Mounts": [{"Destination": "/data", "Source": "/srv/example/appdata/unraid-appops-gateway-1216", "RW": True}],
|
||||
"Config": {"Labels": {"org.opencontainers.image.revision": "a" * 40,
|
||||
"org.opencontainers.image.source": "jens/unraid-appops-gateway"}},
|
||||
"NetworkSettings": {"Ports": {"1216/tcp": [{"HostPort": "1216"}]}},
|
||||
"Id": "1" * 64,
|
||||
"Mounts": [
|
||||
{"Type": "bind", "Destination": "/data", "Source": "/srv/example/appdata/unraid-appops-gateway-1216", "RW": True},
|
||||
{"Type": "bind", "Destination": "/mnt/privatemuse-comfyui-models", "Source": "/srv/example/appdata/comfyui-nvidia/basedir/models", "RW": True},
|
||||
],
|
||||
"Config": {
|
||||
"Image": "unraid-appops-gateway:platform-5553c04ce497",
|
||||
"Hostname": "unraid-appops-gateway",
|
||||
"Labels": {
|
||||
"com.docker.compose.project": "unraid-appops-gateway",
|
||||
"com.docker.compose.service": "appops-gateway",
|
||||
"com.docker.compose.project.working_dir": "/srv/example/appdata/unraid-appops-gateway/source",
|
||||
"com.docker.compose.project.config_files": "/srv/example/appdata/unraid-appops-gateway/source/docker-compose.yml",
|
||||
"org.opencontainers.image.revision": "5553c04ce497c08a35ed301052dc5e0b02e5d9b4",
|
||||
"org.opencontainers.image.source": "https://gitea.example.com/Jens/Unraid-AppOps-Gateway",
|
||||
"org.opencontainers.image.source-tree": "1624e1748053f998d8427c8c8a4203277d22f13c",
|
||||
"org.opencontainers.image.version": "1.8.0-platform-5553c04ce497",
|
||||
"org.opencontainers.image.created": "2026-10-01T16:32:01Z",
|
||||
},
|
||||
},
|
||||
"HostConfig": {"RestartPolicy": {"Name": "unless-stopped"}},
|
||||
"NetworkSettings": {
|
||||
"Ports": {"1216/tcp": [{"HostIp": "198.18.10.150", "HostPort": "1216"}]},
|
||||
"Networks": {"itworx-mcp-hub-npm": {}, "unraid-appops-gateway_default": {}},
|
||||
},
|
||||
"Image": "sha256:" + "b" * 64,
|
||||
}]
|
||||
image = [{"Id": payload[0]["Image"], "Config": {"Labels": {
|
||||
key: value for key, value in payload[0]["Config"]["Labels"].items()
|
||||
if key.startswith("org.opencontainers.image.")
|
||||
}}}]
|
||||
names = ["Unraid-AppOps-Gateway"]
|
||||
|
||||
def fake_run(args, **kwargs):
|
||||
return subprocess.CompletedProcess(args, 0, json.dumps(payload) if args[1] == "inspect" else "\n".join(names), "")
|
||||
if args[1] == "inspect":
|
||||
result = payload
|
||||
elif args[1] == "image":
|
||||
result = image
|
||||
else:
|
||||
result = "\n".join(names)
|
||||
return subprocess.CompletedProcess(args, 0, json.dumps(result) if isinstance(result, list) else result, "")
|
||||
|
||||
monkeypatch.setattr(controller.subprocess, "run", fake_run)
|
||||
assert controller._self_upgrade_runtime()["revision"] == "a" * 40
|
||||
runtime = controller._self_upgrade_runtime()
|
||||
assert runtime["revision"] == "5553c04ce497c08a35ed301052dc5e0b02e5d9b4"
|
||||
assert runtime["imageRef"] == "unraid-appops-gateway:platform-5553c04ce497"
|
||||
assert runtime["sourceTree"] == "1624e1748053f998d8427c8c8a4203277d22f13c"
|
||||
payload[0]["Mounts"][0]["Source"] = "/wrong"
|
||||
assert controller._self_upgrade_runtime() is None
|
||||
payload[0]["Mounts"][0]["Source"] = "/srv/example/appdata/unraid-appops-gateway-1216"
|
||||
payload[0]["NetworkSettings"]["Ports"]["1216/tcp"][0]["HostIp"] = "127.0.0.1"
|
||||
assert controller._self_upgrade_runtime() is None
|
||||
payload[0]["NetworkSettings"]["Ports"]["1216/tcp"][0]["HostIp"] = "198.18.10.150"
|
||||
image[0]["Id"] = "sha256:" + "0" * 64
|
||||
assert controller._self_upgrade_runtime() is None
|
||||
image[0]["Id"] = payload[0]["Image"]
|
||||
names.append("Unraid-AppOps-Gateway-copy")
|
||||
assert controller._self_upgrade_runtime() is None
|
||||
|
||||
@@ -184,30 +232,13 @@ def test_runtime_requires_exact_container_mount_port_and_health(controller, monk
|
||||
("http://gitea.example.com/Jens/Unraid-AppOps-Gateway", False),
|
||||
],
|
||||
)
|
||||
def test_runtime_accepts_only_fixed_appops_oci_sources(controller, monkeypatch, source, accepted):
|
||||
def test_source_validation_accepts_only_fixed_appops_oci_sources(controller, monkeypatch, source, accepted):
|
||||
monkeypatch.setattr(
|
||||
controller,
|
||||
"_gitea_validation_settings",
|
||||
lambda: ("https://gitea.example.com/api/v1", b"t" * 32),
|
||||
)
|
||||
payload = [{
|
||||
"State": {"Running": True, "Health": {"Status": "healthy"}},
|
||||
"Mounts": [{"Destination": "/data", "Source": "/srv/example/appdata/unraid-appops-gateway-1216", "RW": True}],
|
||||
"Config": {"Labels": {
|
||||
"org.opencontainers.image.revision": "a" * 40,
|
||||
"org.opencontainers.image.source": source,
|
||||
}},
|
||||
"NetworkSettings": {"Ports": {"1216/tcp": [{"HostPort": "1216"}]}},
|
||||
"Image": "sha256:" + "b" * 64,
|
||||
}]
|
||||
|
||||
def fake_run(args, **kwargs):
|
||||
stdout = json.dumps(payload) if args[1] == "inspect" else "Unraid-AppOps-Gateway"
|
||||
return subprocess.CompletedProcess(args, 0, stdout, "")
|
||||
|
||||
monkeypatch.setattr(controller.subprocess, "run", fake_run)
|
||||
|
||||
assert (controller._self_upgrade_runtime() is not None) is accepted
|
||||
assert controller._self_upgrade_source_is_valid(source) is accepted
|
||||
|
||||
|
||||
def test_mcp_self_upgrade_rejects_arbitrary_repository(monkeypatch):
|
||||
@@ -219,12 +250,16 @@ def test_mcp_self_upgrade_rejects_arbitrary_repository(monkeypatch):
|
||||
mcp_server.appops_self_upgrade_apply("other/repo", "a" * 40, "test reason", "b" * 64, "key-12345")
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == 'win32', reason='Linux controller pinned directory descriptors')
|
||||
def test_self_upgrade_signature_binds_endpoint_and_rejects_replay(controller, monkeypatch, tmp_path):
|
||||
secret = b"fixed-test-self-upgrade-capability-0123456789"
|
||||
token_file = tmp_path / "mcp-token"
|
||||
token_file.write_bytes(secret + b"\n")
|
||||
token_file.chmod(0o600)
|
||||
monkeypatch.setattr(controller, "SELF_UPGRADE_TOKEN_FILE", token_file)
|
||||
import runner_statistics_binding
|
||||
monkeypatch.setattr(runner_statistics_binding, 'APPOPS_DATA', tmp_path)
|
||||
monkeypatch.setattr(runner_statistics_binding, 'ADMIN_TOKEN_FILE', token_file)
|
||||
monkeypatch.setattr(runner_statistics_binding, 'ROOT_UID', os.getuid())
|
||||
monkeypatch.setattr(controller, "APPOPS_NONCE_ROOT", tmp_path / "nonces")
|
||||
stamp = str(int(time.time()))
|
||||
nonce = "a" * 32
|
||||
@@ -245,6 +280,7 @@ def test_self_upgrade_signature_binds_endpoint_and_rejects_replay(controller, mo
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == 'win32', reason='Linux controller pinned directory descriptors')
|
||||
def test_self_upgrade_http_contract_is_typed_idempotent_and_durable(controller, monkeypatch, tmp_path):
|
||||
_ready(controller, monkeypatch)
|
||||
monkeypatch.setattr(controller, "SELF_UPGRADE_REQUEST_ROOT", tmp_path / "requests")
|
||||
@@ -254,7 +290,10 @@ def test_self_upgrade_http_contract_is_typed_idempotent_and_durable(controller,
|
||||
token_file = tmp_path / "mcp-token"
|
||||
token_file.write_bytes(secret + b"\n")
|
||||
token_file.chmod(0o600)
|
||||
monkeypatch.setattr(controller, "SELF_UPGRADE_TOKEN_FILE", token_file)
|
||||
import runner_statistics_binding
|
||||
monkeypatch.setattr(runner_statistics_binding, 'APPOPS_DATA', tmp_path)
|
||||
monkeypatch.setattr(runner_statistics_binding, 'ADMIN_TOKEN_FILE', token_file)
|
||||
monkeypatch.setattr(runner_statistics_binding, 'ROOT_UID', os.getuid())
|
||||
monkeypatch.setattr(controller.Handler, "log_message", lambda *args: None)
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), controller.Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
@@ -333,6 +372,9 @@ def test_self_upgrade_http_contract_is_typed_idempotent_and_durable(controller,
|
||||
)
|
||||
monkeypatch.setattr(controller, "_self_upgrade_runtime", lambda: {
|
||||
"revision": "c" * 40, "imageId": "sha256:" + "b" * 64,
|
||||
"containerId": "d" * 64,
|
||||
"imageRef": "unraid-appops-gateway:production",
|
||||
"sourceTree": "e" * 40,
|
||||
"dataPath": "/srv/example/appdata/unraid-appops-gateway-1216",
|
||||
})
|
||||
monkeypatch.setattr(controller, "_deployment_state", lambda slug: {})
|
||||
@@ -389,7 +431,10 @@ def test_appops_backup_is_fixed_and_digest_verified(deploy_module, monkeypatch,
|
||||
archive = Path(receipt["archive"])
|
||||
assert hashlib.sha256(archive.read_bytes()).hexdigest() == receipt["sha256"]
|
||||
assert receipt["previousRevision"] == "a" * 40
|
||||
assert json.loads((archive.parent / "receipt.json").read_text()) == receipt
|
||||
stored_receipt = json.loads((archive.parent / "receipt.json").read_text())
|
||||
assert {**stored_receipt, "receiptSha256": hashlib.sha256(
|
||||
json.dumps(stored_receipt, sort_keys=True, separators=(",", ":")).encode()
|
||||
).hexdigest()} == receipt
|
||||
with tarfile.open(archive, "r:gz") as backup:
|
||||
assert backup.extractfile("./state.txt").read() == b"preserved"
|
||||
|
||||
|
||||
Reference in new issue
Block a user