feat(device-plane): add fail-closed deploy foundation
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
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",
|
||||
)
|
||||
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")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user