refactor(deploy): make device manager releases declarative

This commit is contained in:
Codex
2026-08-11 10:37:13 +03:00
parent 1adbabefcf
commit 72b4846b32
4 changed files with 546 additions and 110 deletions
@@ -36,7 +36,14 @@ LAUNCHER_HUB_SERVICE_TRUST_UI_ENTRIES = (
)
def healthy_device_plane_inventory():
def healthy_device_plane_inventory(*, include_manager=False):
services = [
("device-control-core", "a"),
("device-gateway", "b"),
("device-postgres", "c"),
]
if include_manager:
services.append(("device-manager", "d"))
return {
"schemaVersion": "nodedc.device-plane.runtime-inventory.v1",
"composeProject": "nodedc-device-plane",
@@ -50,15 +57,32 @@ def healthy_device_plane_inventory():
"health": "healthy",
"restartCount": 0,
}
for service, character in (
("device-control-core", "a"),
("device-gateway", "b"),
("device-postgres", "c"),
)
for service, character in services
],
}
def device_manager_release_descriptor(
release_id="device-manager-release-unit-001",
*,
action="activate",
predecessor_kind="reconciliation",
predecessor_patch="device-manager-reconciliation-unit-001",
predecessor_sha="a" * 64,
):
return {
"schemaVersion": "nodedc.device-plane.device-manager-release.v1",
"releaseId": release_id,
"action": action,
"predecessor": {
"kind": predecessor_kind,
"patchId": predecessor_patch,
"artifactSha256": predecessor_sha,
},
**RUNNER.expected_device_plane_manager_release_boundaries(),
}
class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
def build(self, script, patch_id, artifact_dir):
environment = os.environ.copy()
@@ -239,55 +263,87 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
)
runtime_acceptance.assert_called_once_with()
def test_v3_activation_requires_exact_applied_v2_reconciliation(self):
def test_activation_resolves_predecessor_from_descriptor_and_journal(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-device-manager-v3-predecessor-",
prefix="nodedc-device-manager-release-predecessor-",
) as directory:
root = Path(directory)
artifact = (
root
/ RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT
)
applied = root / "applied"
backups = root / "backups"
temporary = root / "tmp"
device_plane = root / "device-plane"
for path in (applied, backups, temporary, device_plane / "deployment"):
path.mkdir(parents=True)
predecessor_patch = "device-manager-reconciliation-unit-004"
artifact_name = f"nodedc-device-plane-{predecessor_patch}.tgz"
artifact = applied / artifact_name
artifact_bytes = b"reviewed-reconciliation-artifact"
artifact.write_bytes(artifact_bytes)
artifact_sha = hashlib.sha256(artifact_bytes).hexdigest()
descriptor = device_manager_release_descriptor(
predecessor_patch=predecessor_patch,
predecessor_sha=artifact_sha,
)
state_file = root / "applied.jsonl"
state_file.write_text(
json.dumps({
"id": (
RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID
),
"artifact": (
RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT
),
"backup_id": (
RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_APPLY_BACKUP_ID
),
"id": predecessor_patch,
"artifact": artifact_name,
"backup_id": "predecessor-backup",
"component": "device-plane",
"sha256": hashlib.sha256(artifact_bytes).hexdigest(),
"sha256": artifact_sha,
"status": "ok",
}) + "\n",
encoding="utf-8",
)
backup = root / "failed-backup"
(backups / "predecessor-backup").mkdir()
baseline_backup = root / "failed-backup"
predecessor_descriptor = {"marker": "exact"}
marker = (
device_plane
/ RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL
)
marker.write_text(
json.dumps(predecessor_descriptor),
encoding="utf-8",
)
with (
mock.patch.object(RUNNER, "APPLIED_DIR", root),
mock.patch.object(RUNNER, "APPLIED_DIR", applied),
mock.patch.object(RUNNER, "BACKUPS_DIR", backups),
mock.patch.object(RUNNER, "TMP_DIR", temporary),
mock.patch.object(RUNNER, "STATE_FILE", state_file),
mock.patch.object(
RUNNER,
"DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256",
hashlib.sha256(artifact_bytes).hexdigest(),
"component_root",
return_value=device_plane,
),
mock.patch.object(
RUNNER,
"validate_device_plane_manager_control_plane_payload",
return_value=(
RUNNER.expected_device_plane_manager_control_plane_descriptor()
),
return_value=descriptor,
) as payload,
mock.patch.object(
RUNNER,
"load_artifact",
return_value=(
{
"id": predecessor_patch,
"component": "device-plane",
"type": "app-overlay",
},
RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ENTRIES,
root / "predecessor-payload",
),
),
mock.patch.object(
RUNNER,
"validate_device_plane_manager_v2_reconciliation_payload",
return_value=predecessor_descriptor,
),
mock.patch.object(
RUNNER,
"validate_device_plane_manager_v2_reconciliation_backup",
return_value=backup,
return_value=baseline_backup,
),
mock.patch.object(
RUNNER,
@@ -303,10 +359,147 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
self.assertEqual(
result["mode"],
"reconciled-v2-manager-forward-activation",
"reconciled-manager-forward-activation",
)
payload.assert_called_once_with(root / "payload")
baseline.assert_called_once_with(backup, marker_installed=True)
baseline.assert_called_once_with(
baseline_backup,
marker_installed=True,
)
def test_failed_patch_id_and_sha_are_globally_non_replayable(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-device-manager-failed-replay-",
) as directory:
failed_state = Path(directory) / "failed.jsonl"
failed_state.write_text(
json.dumps({
"id": "device-manager-release-failed-001",
"sha256": "b" * 64,
"status": "failed",
}) + "\n",
encoding="utf-8",
)
with mock.patch.object(
RUNNER,
"FAILED_STATE_FILE",
failed_state,
):
with self.assertRaisesRegex(
RUNNER.DeployError,
"SHA is terminal failed",
):
RUNNER.reject_failed_artifact_replay(
{"id": "different"},
"b" * 64,
)
with self.assertRaisesRegex(
RUNNER.DeployError,
"patch id is terminal failed",
):
RUNNER.reject_failed_artifact_replay(
{"id": "device-manager-release-failed-001"},
"c" * 64,
)
def test_upgrade_accepts_any_current_release_without_runner_patch_ids(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-device-manager-release-upgrade-",
) as directory:
root = Path(directory)
applied = root / "applied"
backups = root / "backups"
temporary = root / "tmp"
device_plane = root / "device-plane"
for path in (applied, backups, temporary, device_plane / "deployment"):
path.mkdir(parents=True)
predecessor_patch = "device-manager-release-arbitrary-041"
artifact_name = f"nodedc-device-plane-{predecessor_patch}.tgz"
artifact = applied / artifact_name
artifact_bytes = b"arbitrary-reviewed-device-manager-release"
artifact.write_bytes(artifact_bytes)
artifact_sha = hashlib.sha256(artifact_bytes).hexdigest()
predecessor_descriptor = device_manager_release_descriptor(
release_id=predecessor_patch,
)
candidate_descriptor = device_manager_release_descriptor(
release_id="device-manager-release-arbitrary-042",
action="upgrade",
predecessor_kind="release",
predecessor_patch=predecessor_patch,
predecessor_sha=artifact_sha,
)
installed = (
device_plane / RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
)
installed.write_text(
json.dumps(predecessor_descriptor),
encoding="utf-8",
)
state_file = root / "applied.jsonl"
state_file.write_text(
json.dumps({
"id": predecessor_patch,
"artifact": artifact_name,
"backup_id": "arbitrary-release-backup",
"component": "device-plane",
"sha256": artifact_sha,
"status": "ok",
}) + "\n",
encoding="utf-8",
)
(backups / "arbitrary-release-backup").mkdir()
with (
mock.patch.object(RUNNER, "APPLIED_DIR", applied),
mock.patch.object(RUNNER, "BACKUPS_DIR", backups),
mock.patch.object(RUNNER, "TMP_DIR", temporary),
mock.patch.object(RUNNER, "STATE_FILE", state_file),
mock.patch.object(
RUNNER,
"component_root",
return_value=device_plane,
),
mock.patch.object(
RUNNER,
"validate_device_plane_manager_control_plane_payload",
side_effect=(
candidate_descriptor,
predecessor_descriptor,
),
),
mock.patch.object(
RUNNER,
"load_artifact",
return_value=(
{
"id": predecessor_patch,
"component": "device-plane",
"type": "app-overlay",
},
RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
root / "predecessor-payload",
),
),
mock.patch.object(
RUNNER,
"healthcheck_compose_service",
) as health,
):
result = (
RUNNER.validate_device_plane_manager_activation_predecessor(
root / "candidate-payload"
)
)
self.assertEqual(result["mode"], "active-manager-forward-upgrade")
self.assertEqual(
[call.args for call in health.call_args_list],
[
("device-plane", "device-control-core"),
("device-plane", "device-manager"),
("device-plane", "device-postgres"),
],
)
def test_initial_install_rollback_removes_manager_and_restores_core_only(self):
entries = RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES
@@ -393,6 +586,73 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
f"source+runtime-restored:{len(entries)}",
)
def test_upgrade_rollback_restores_previous_core_and_manager_release(self):
entries = RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES
with tempfile.TemporaryDirectory(
prefix="nodedc-device-manager-upgrade-rollback-",
) as directory:
backup = Path(directory) / "backup"
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(healthy_device_plane_inventory(include_manager=True)),
encoding="utf-8",
)
with (
mock.patch.object(
RUNNER,
"stop_and_remove_compose_services",
) as stop,
mock.patch.object(
RUNNER,
"restore_platform_overlay",
return_value=len(entries),
),
mock.patch.object(
RUNNER,
"run_component_runtime",
) as restore_runtime,
mock.patch.object(
RUNNER,
"healthcheck_compose_service_with_grace",
) as restore_health,
mock.patch.object(
RUNNER,
"component_healthchecks",
return_value=(),
),
):
result = RUNNER.rollback_device_plane_apply(
Path(directory) / "live",
backup,
entries,
"test-stamp",
True,
("device-control-core", "device-manager"),
)
stop.assert_not_called()
restore_runtime.assert_called_once_with(
"device-plane",
list(entries),
("device-control-core", "device-manager"),
)
self.assertEqual(
[call.args for call in restore_health.call_args_list],
[
("device-plane", "device-control-core"),
("device-plane", "device-manager"),
],
)
self.assertEqual(
result,
f"source+runtime-restored:{len(entries)}",
)
if __name__ == "__main__":
unittest.main(verbosity=2)