211 lines
6.7 KiB
Python
211 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
import hashlib
|
|
import importlib.machinery
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
|
BUILDER = (
|
|
SCRIPT_DIR
|
|
/ "build-device-plane-postgres-bootstrap-artifact.mjs"
|
|
)
|
|
EXPECTED_ENTRIES = [
|
|
"docker-compose.device-plane.yml",
|
|
"deployment/device-postgres-bootstrap-v1.json",
|
|
]
|
|
|
|
|
|
def load_runner():
|
|
loader = importlib.machinery.SourceFileLoader(
|
|
"nodedc_device_plane_postgres_runner_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 DevicePlanePostgresBootstrapTest(unittest.TestCase):
|
|
def build(self, artifact_dir, patch_id):
|
|
environment = os.environ.copy()
|
|
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
|
result = subprocess.run(
|
|
["node", str(BUILDER), patch_id],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
env=environment,
|
|
)
|
|
return json.loads(result.stdout)
|
|
|
|
def test_bootstrap_artifact_is_exact_deterministic_and_runner_accepted(self):
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="nodedc-device-plane-postgres-artifact-",
|
|
) as directory:
|
|
artifact_dir = Path(directory)
|
|
first = self.build(
|
|
artifact_dir,
|
|
"device-plane-postgres-bootstrap-unit-001",
|
|
)
|
|
artifact = Path(first["artifact"])
|
|
first_bytes = artifact.read_bytes()
|
|
second = self.build(
|
|
artifact_dir,
|
|
"device-plane-postgres-bootstrap-unit-001",
|
|
)
|
|
second_bytes = Path(second["artifact"]).read_bytes()
|
|
|
|
self.assertEqual(first["entries"], EXPECTED_ENTRIES)
|
|
self.assertEqual(first["services"], ["device-postgres"])
|
|
self.assertEqual(first["mode"], "create-if-absent")
|
|
self.assertEqual(first["rollbackVolumePolicy"], "preserve")
|
|
self.assertEqual(
|
|
first["sha256"],
|
|
hashlib.sha256(first_bytes).hexdigest(),
|
|
)
|
|
self.assertEqual(first_bytes, second_bytes)
|
|
|
|
with tarfile.open(artifact, "r:gz") as archive:
|
|
names = {member.name for member in archive.getmembers()}
|
|
self.assertEqual(
|
|
archive.extractfile("files.txt")
|
|
.read()
|
|
.decode("utf-8")
|
|
.splitlines(),
|
|
EXPECTED_ENTRIES,
|
|
)
|
|
self.assertEqual(
|
|
names,
|
|
{
|
|
"manifest.env",
|
|
"files.txt",
|
|
"payload",
|
|
"payload/docker-compose.device-plane.yml",
|
|
"payload/deployment",
|
|
"payload/deployment/device-postgres-bootstrap-v1.json",
|
|
},
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="nodedc-device-plane-postgres-load-",
|
|
) as work_directory:
|
|
manifest, entries, _payload = RUNNER.load_artifact(
|
|
artifact,
|
|
Path(work_directory),
|
|
)
|
|
self.assertEqual(manifest["component"], "device-plane")
|
|
self.assertEqual(entries, EXPECTED_ENTRIES)
|
|
self.assertTrue(
|
|
RUNNER.is_device_plane_postgres_bootstrap_slice(
|
|
manifest["component"],
|
|
entries,
|
|
),
|
|
)
|
|
self.assertEqual(
|
|
RUNNER.component_services("device-plane", entries),
|
|
("device-postgres",),
|
|
)
|
|
self.assertEqual(
|
|
RUNNER.component_builds("device-plane", entries),
|
|
(),
|
|
)
|
|
|
|
def test_preflight_accepts_only_absent_container_and_volume(self):
|
|
absent_container = mock.Mock(returncode=0, stdout="")
|
|
absent_volume = mock.Mock(returncode=1)
|
|
with mock.patch.object(
|
|
RUNNER.subprocess,
|
|
"run",
|
|
side_effect=[absent_container, absent_volume],
|
|
):
|
|
self.assertEqual(
|
|
RUNNER.preflight_device_plane_postgres_bootstrap(),
|
|
"absent",
|
|
)
|
|
|
|
existing_container = mock.Mock(
|
|
returncode=0,
|
|
stdout="abc123def456\n",
|
|
)
|
|
with mock.patch.object(
|
|
RUNNER.subprocess,
|
|
"run",
|
|
return_value=existing_container,
|
|
):
|
|
with self.assertRaisesRegex(
|
|
RUNNER.DeployError,
|
|
"container already exists",
|
|
):
|
|
RUNNER.preflight_device_plane_postgres_bootstrap()
|
|
|
|
with mock.patch.object(
|
|
RUNNER.subprocess,
|
|
"run",
|
|
side_effect=[
|
|
absent_container,
|
|
mock.Mock(returncode=0),
|
|
],
|
|
):
|
|
with self.assertRaisesRegex(
|
|
RUNNER.DeployError,
|
|
"volume already exists",
|
|
):
|
|
RUNNER.preflight_device_plane_postgres_bootstrap()
|
|
|
|
def test_plan_selection_is_unambiguous_for_bootstrap_and_application(self):
|
|
self.assertEqual(
|
|
RUNNER.device_plane_postgres_plan_selection(None),
|
|
"preserved-prerequisite:not-selected",
|
|
)
|
|
self.assertEqual(
|
|
RUNNER.device_plane_postgres_plan_selection("absent"),
|
|
"bootstrap-selected:create-if-absent",
|
|
)
|
|
with self.assertRaisesRegex(
|
|
RUNNER.DeployError,
|
|
"plan preflight state is invalid",
|
|
):
|
|
RUNNER.device_plane_postgres_plan_selection("unknown")
|
|
|
|
def test_bootstrap_health_acceptance_is_database_service_only(self):
|
|
with mock.patch.object(
|
|
RUNNER,
|
|
"healthcheck_compose_service",
|
|
) as health:
|
|
RUNNER.run_healthchecks(
|
|
"device-plane",
|
|
EXPECTED_ENTRIES,
|
|
("device-postgres",),
|
|
)
|
|
health.assert_called_once_with(
|
|
"device-plane",
|
|
"device-postgres",
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
RUNNER.DeployError,
|
|
"service set mismatch",
|
|
):
|
|
RUNNER.run_healthchecks(
|
|
"device-plane",
|
|
EXPECTED_ENTRIES,
|
|
("device-control-core",),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|