NODEDC_PLATFORM/infra/deploy-runner/test_device_plane_registry.py

409 lines
14 KiB
Python

#!/usr/bin/env python3
import importlib.machinery
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path
from unittest import mock
SCRIPT_DIR = Path(__file__).resolve().parent
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
def load_runner():
loader = importlib.machinery.SourceFileLoader(
"nodedc_device_plane_deploy_under_test",
str(RUNNER_PATH),
)
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module
RUNNER = load_runner()
class DevicePlaneRegistryTest(unittest.TestCase):
def test_registry_has_exact_roots_project_and_stateless_services(self):
component = RUNNER.COMPONENTS["device-plane"]
root = Path("/volume1/docker/nodedc-device-plane")
self.assertEqual(component["payload_root"], root)
self.assertEqual(component["compose_root"], root)
self.assertEqual(component["compose_project"], "nodedc-device-plane")
self.assertEqual(
component["compose_files"],
(root / "docker-compose.device-plane.yml",),
)
self.assertTrue(component["bootstrap_root"])
self.assertTrue(component["compose_no_deps"])
self.assertEqual(
component["services"],
("device-control-core", "device-gateway"),
)
self.assertNotIn("device-postgres", component["services"])
self.assertIsNone(component.get("compose_env_file"))
def test_files_select_only_the_affected_stateless_services(self):
self.assertEqual(
RUNNER.component_services(
"device-plane",
("services/device-control-core/src/server.mjs",),
),
("device-control-core",),
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
("services/device-gateway/src/server.mjs",),
),
("device-gateway",),
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
("packages/arusnavi-b2-adapter",),
),
("device-control-core", "device-gateway"),
)
services = RUNNER.component_services(
"device-plane",
("docker-compose.device-plane.yml",),
)
self.assertEqual(
services,
("device-control-core", "device-gateway"),
)
self.assertNotIn("device-postgres", services)
def test_build_selection_is_exact_and_database_free(self):
builds = RUNNER.component_builds(
"device-plane",
("services/device-gateway",),
)
self.assertEqual(len(builds), 1)
self.assertEqual(builds[0][0], RUNNER.DEVICE_PLANE_ROOT)
self.assertIn(
"services/device-gateway/Dockerfile",
builds[0][1],
)
self.assertIn(RUNNER.DEVICE_PLANE_GATEWAY_IMAGE, builds[0][1])
all_builds = RUNNER.component_builds(
"device-plane",
("package-lock.json",),
)
self.assertEqual(len(all_builds), 2)
self.assertNotIn("device-postgres", " ".join(
argument
for _root, arguments in all_builds
for argument in arguments
))
def test_payload_allowlist_rejects_runtime_secrets_and_broad_paths(self):
for allowed in (
".dockerignore",
"docker-compose.device-plane.yml",
"packages/device-protocol-contract/src/index.mjs",
"services/device-control-core/Dockerfile",
"services/device-gateway/src/runtime.mjs",
):
self.assertTrue(
RUNNER.allowed_payload_path("device-plane", allowed),
)
for rejected in (
".env",
"secrets/postgres-password",
"runtime/postgres/data",
"services/unknown/server.mjs",
"docker-compose.yml",
"services/device-control-core/start.sh",
"services/device-control-core/test/app.test.mjs",
):
with self.assertRaises(RUNNER.DeployError):
RUNNER.allowed_payload_path("device-plane", rejected)
def test_healthchecks_are_service_scoped_and_fail_closed(self):
core_checks = RUNNER.component_healthchecks(
"device-plane",
("services/device-control-core",),
("device-control-core",),
)
self.assertEqual(len(core_checks), 1)
self.assertEqual(
core_checks[0]["expected_json"]["commandTransport"],
"disabled",
)
self.assertEqual(
core_checks[0]["expected_json"]["discoveryIngest"],
"disabled",
)
gateway_checks = RUNNER.component_healthchecks(
"device-plane",
("services/device-gateway",),
("device-gateway",),
)
self.assertEqual(len(gateway_checks), 1)
self.assertEqual(
gateway_checks[0]["expected_json"]["publicIngress"],
"disabled",
)
self.assertEqual(
gateway_checks[0]["expected_json"]["tcpListener"],
"disabled",
)
def test_runtime_secrets_are_runner_owned_and_not_manifest_selected(self):
with mock.patch.object(
RUNNER,
"ensure_platform_runtime_secret",
) as ensure:
RUNNER.prepare_component_runtime(
"device-plane",
("services/device-control-core",),
)
self.assertEqual(
[call.args[0] for call in ensure.call_args_list],
[
RUNNER.DEVICE_PLANE_POSTGRES_PASSWORD_FILE,
RUNNER.DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE,
RUNNER.DEVICE_PLANE_IDENTIFIER_PEPPER_FILE,
],
)
def test_compose_runtime_uses_no_deps_and_never_selects_database(self):
with mock.patch.object(RUNNER.subprocess, "run") as run:
RUNNER.run_compose(
"device-plane",
("device-control-core", "device-gateway"),
("docker-compose.device-plane.yml",),
)
command = run.call_args_list[0].args[0]
self.assertIn("--no-deps", command)
self.assertNotIn("device-postgres", command)
self.assertEqual(
command[-2:],
["device-control-core", "device-gateway"],
)
def test_initial_candidate_rollback_removes_only_stateless_services(self):
entries = (
"docker-compose.device-plane.yml",
"services/device-control-core",
"services/device-gateway",
)
with tempfile.TemporaryDirectory(
prefix="nodedc-device-plane-rollback-",
) as directory:
root = Path(directory) / "live"
backup = Path(directory) / "backup"
root.mkdir()
backup.mkdir()
(backup / "existing-files.txt").write_text("", encoding="utf-8")
(backup / "missing-files.txt").write_text(
"\n".join(entries) + "\n",
encoding="utf-8",
)
(backup / "runtime-before.json").write_text(
json.dumps({
"schemaVersion":
"nodedc.device-plane.runtime-inventory.v1",
"composeProject": "nodedc-device-plane",
"services": [],
}),
encoding="utf-8",
)
with (
mock.patch.object(
RUNNER,
"stop_and_remove_compose_services",
) as stop,
mock.patch.object(
RUNNER,
"restore_platform_overlay",
return_value=3,
) as restore,
):
result = RUNNER.rollback_device_plane_apply(
root,
backup,
entries,
"test-stamp",
True,
("device-control-core", "device-gateway"),
)
stop.assert_called_once_with(
"device-plane",
("device-control-core", "device-gateway"),
)
restore.assert_called_once()
self.assertEqual(result, "source-restored-runtime-unchanged:3")
def test_db_bootstrap_compose_does_not_invent_stateless_baseline(self):
entries = (
"docker-compose.device-plane.yml",
"services/device-control-core",
"services/device-gateway",
)
with tempfile.TemporaryDirectory(
prefix="nodedc-device-plane-compose-predecessor-",
) as directory:
root = Path(directory) / "live"
backup = Path(directory) / "backup"
root.mkdir()
backup.mkdir()
(backup / "existing-files.txt").write_text(
"docker-compose.device-plane.yml\n",
encoding="utf-8",
)
(backup / "missing-files.txt").write_text(
"services/device-control-core\n"
"services/device-gateway\n",
encoding="utf-8",
)
(backup / "runtime-before.json").write_text(
json.dumps({
"schemaVersion":
"nodedc.device-plane.runtime-inventory.v1",
"composeProject": "nodedc-device-plane",
"services": [{
"service": "device-postgres",
"containerId": "a" * 64,
"imageId": "sha256:" + "b" * 64,
"status": "running",
"running": True,
"health": "healthy",
"restartCount": 0,
}],
}),
encoding="utf-8",
)
with (
mock.patch.object(
RUNNER,
"stop_and_remove_compose_services",
) as stop,
mock.patch.object(
RUNNER,
"restore_platform_overlay",
return_value=3,
),
mock.patch.object(
RUNNER,
"run_component_runtime",
) as run_runtime,
):
result = RUNNER.rollback_device_plane_apply(
root,
backup,
entries,
"test-stamp",
True,
("device-control-core", "device-gateway"),
)
stop.assert_called_once_with(
"device-plane",
("device-control-core", "device-gateway"),
)
run_runtime.assert_not_called()
self.assertEqual(result, "source-restored-runtime-unchanged:3")
def test_installed_stateless_baseline_is_restored_from_runtime_inventory(
self,
):
entries = (
"docker-compose.device-plane.yml",
"services/device-control-core",
"services/device-gateway",
)
with tempfile.TemporaryDirectory(
prefix="nodedc-device-plane-installed-predecessor-",
) as directory:
root = Path(directory) / "live"
backup = Path(directory) / "backup"
root.mkdir()
backup.mkdir()
(backup / "existing-files.txt").write_text(
"\n".join(entries) + "\n",
encoding="utf-8",
)
(backup / "missing-files.txt").write_text(
"",
encoding="utf-8",
)
(backup / "runtime-before.json").write_text(
json.dumps({
"schemaVersion":
"nodedc.device-plane.runtime-inventory.v1",
"composeProject": "nodedc-device-plane",
"services": [
{
"service": "device-control-core",
"containerId": "a" * 64,
"imageId": "sha256:" + "b" * 64,
"status": "running",
"running": True,
"health": "healthy",
"restartCount": 0,
},
{
"service": "device-gateway",
"containerId": "c" * 64,
"imageId": "sha256:" + "d" * 64,
"status": "running",
"running": True,
"health": "healthy",
"restartCount": 0,
},
],
}),
encoding="utf-8",
)
with (
mock.patch.object(
RUNNER,
"stop_and_remove_compose_services",
) as stop,
mock.patch.object(
RUNNER,
"restore_platform_overlay",
return_value=3,
),
mock.patch.object(
RUNNER,
"run_component_runtime",
) as run_runtime,
mock.patch.object(RUNNER, "run_healthchecks") as healthchecks,
):
result = RUNNER.rollback_device_plane_apply(
root,
backup,
entries,
"test-stamp",
True,
("device-control-core", "device-gateway"),
)
stop.assert_not_called()
run_runtime.assert_called_once_with(
"device-plane",
list(entries),
("device-control-core", "device-gateway"),
)
healthchecks.assert_called_once_with(
"device-plane",
list(entries),
("device-control-core", "device-gateway"),
)
self.assertEqual(result, "source+runtime-restored:3")
if __name__ == "__main__":
unittest.main(verbosity=2)