From 1adbabefcf21b4c242bc23a32aaed37e93ca2d8f Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 11 Aug 2026 10:14:24 +0300 Subject: [PATCH] fix(device-plane): activate manager from v2 reconciliation --- .../device-manager-control-plane-v3.json | 17 ++++ ...-device-manager-control-plane-artifact.mjs | 15 ++-- infra/deploy-runner/nodedc-deploy | 87 ++++++++++++++----- ..._device_manager_control_plane_artifacts.py | 27 +++--- ...r_control_plane_reconciliation_artifact.py | 9 ++ 5 files changed, 116 insertions(+), 39 deletions(-) create mode 100644 device-plane/deployment/device-manager-control-plane-v3.json diff --git a/device-plane/deployment/device-manager-control-plane-v3.json b/device-plane/deployment/device-manager-control-plane-v3.json new file mode 100644 index 0000000..69fde1f --- /dev/null +++ b/device-plane/deployment/device-manager-control-plane-v3.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": "nodedc.device-plane.device-manager-control-plane.v3", + "action": "activate", + "predecessor": { + "patchId": "device-manager-control-plane-v2-reconciliation-20260811-004", + "artifactSha256": "09600bfd99717314b43936e17ffaaf6b6fca1f2fd008beddddc68480cdf3d284", + "mode": "failed-v2-control-plane-baseline-adoption" + }, + "service": "device-manager", + "publicIngress": "reverse-proxy-only", + "deviceCoreManagementApi": "file-token-authenticated", + "launcherTrust": "file-token-scoped-to-device-core-handoff", + "healthGate": "bounded-container-grace+core-contract", + "commandTransport": "disabled", + "gelios": "untouched", + "rollback": "restore-v2-reconciled-baseline" +} diff --git a/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs b/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs index b36c5a3..554a86c 100644 --- a/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs +++ b/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs @@ -12,7 +12,7 @@ const devicePlaneRoot = resolve(platformRoot, "device-plane"); const designRoot = resolve(process.env.NODEDC_DEVICE_MANAGER_SOURCE_ROOT || resolve(platformRoot, "../NODEDC_DESIGN_GUIDELINE")); const managerRoot = resolve(designRoot, "apps/device-manager"); const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts")); -const [patchId = "device-manager-control-plane-20260811-003", ...extra] = process.argv.slice(2); +const [patchId = "device-manager-control-plane-v3-20260811-005", ...extra] = process.argv.slice(2); if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) throw new Error("usage: build-device-manager-control-plane-artifact.mjs [patch-id]"); const entries = [ @@ -26,7 +26,7 @@ const entries = [ "services/device-gateway/package.json", "services/device-edge-relay/package.json", "services/device-manager", - "deployment/device-manager-control-plane-v2.json", + "deployment/device-manager-control-plane-v3.json", ]; const stage = await mkdtemp(join(tmpdir(), "nodedc-device-manager-control-plane-")); const payload = join(stage, "payload"); @@ -89,15 +89,16 @@ try { if (compose.includes(forbidden)) throw new Error(`device_manager_compose_boundary_violation:${forbidden}`); } const descriptor = JSON.parse(await readFile( - join(payload, "deployment/device-manager-control-plane-v2.json"), + join(payload, "deployment/device-manager-control-plane-v3.json"), "utf8", )); if ( - descriptor.schemaVersion !== "nodedc.device-plane.device-manager-control-plane.v2" - || descriptor.predecessor?.patchId !== "device-manager-control-plane-reconciliation-20260811-002" - || descriptor.predecessor?.artifactSha256 !== "dd86dd58e4f649db0981db5089e003caf3961356179f2abb514662351487e1e6" + descriptor.schemaVersion !== "nodedc.device-plane.device-manager-control-plane.v3" + || descriptor.predecessor?.patchId !== "device-manager-control-plane-v2-reconciliation-20260811-004" + || descriptor.predecessor?.artifactSha256 !== "09600bfd99717314b43936e17ffaaf6b6fca1f2fd008beddddc68480cdf3d284" + || descriptor.predecessor?.mode !== "failed-v2-control-plane-baseline-adoption" || descriptor.healthGate !== "bounded-container-grace+core-contract" - || descriptor.rollback !== "restore-reconciled-baseline" + || descriptor.rollback !== "restore-v2-reconciled-baseline" ) throw new Error("device_manager_activation_successor_contract_mismatch"); await writeFile(join(stage, "manifest.env"), `id=${patchId}\ncomponent=device-plane\ntype=app-overlay\n`, "utf8"); await writeFile(join(stage, "files.txt"), `${entries.join("\n")}\n`, "utf8"); diff --git a/infra/deploy-runner/nodedc-deploy b/infra/deploy-runner/nodedc-deploy index bafa1b4..b71eb3b 100755 --- a/infra/deploy-runner/nodedc-deploy +++ b/infra/deploy-runner/nodedc-deploy @@ -213,9 +213,12 @@ DEVICE_PLANE_FOUNDATION_ENTRIES = ( DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL = ( "deployment/device-manager-control-plane-v1.json" ) -DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL = ( +DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_REL = ( "deployment/device-manager-control-plane-v2.json" ) +DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL = ( + "deployment/device-manager-control-plane-v3.json" +) DEVICE_PLANE_MANAGER_COMPOSE_REL = "docker-compose.device-manager.yml" DEVICE_PLANE_MANAGER_COMPOSE_SHA256 = ( "4954120aaddc999798b64c304d8cf692b79714feb727d873117bd1f3434e865e" @@ -233,6 +236,19 @@ DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_ENTRIES = ( "services/device-manager", DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL, ) +DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_ENTRIES = ( + ".dockerignore", + "package.json", + "package-lock.json", + DEVICE_PLANE_MANAGER_COMPOSE_REL, + "packages/device-protocol-contract", + "packages/arusnavi-b2-adapter", + "services/device-control-core", + "services/device-gateway/package.json", + "services/device-edge-relay/package.json", + "services/device-manager", + DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_REL, +) DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES = ( ".dockerignore", "package.json", @@ -334,6 +350,17 @@ DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256 = ( DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID = ( "device-manager-control-plane-v2-reconciliation-20260811-004" ) +DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT = ( + "nodedc-device-plane-device-manager-control-plane-v2-reconciliation-" + "20260811-004.tgz" +) +DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256 = ( + "09600bfd99717314b43936e17ffaaf6b6fca1f2fd008beddddc68480cdf3d284" +) +DEVICE_PLANE_MANAGER_V2_RECONCILIATION_APPLY_BACKUP_ID = ( + "device-plane-device-manager-control-plane-v2-reconciliation-" + "20260811-004-20260811-100533" +) DEVICE_PLANE_MANAGER_V2_RECONCILIATION_BACKUP_ID = ( "device-plane-device-manager-control-plane-20260811-003-" "20260811-012505" @@ -371,7 +398,7 @@ DEVICE_PLANE_MANAGER_V2_RECONCILIATION_EXISTING = ( DEVICE_PLANE_MANAGER_V2_RECONCILIATION_MISSING = ( DEVICE_PLANE_MANAGER_COMPOSE_REL, "services/device-manager", - DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL, + DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_REL, ) DEVICE_PLANE_FOUNDATION_RECOVERY_REL = ( "deployment/device-plane-foundation-recovery-v1.json" @@ -8677,10 +8704,15 @@ def reject_terminal_device_plane_manager_artifact( if ( manifest.get("id") == DEVICE_PLANE_MANAGER_V2_FAILED_PATCH_ID or sha256 == DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256 + or is_device_plane_manager_v2_control_plane_slice( + manifest.get("component"), + entries, + ) ): die( "Device Manager control-plane 003 is terminal failed; " - "use the exact v2 reconciliation successor" + "use the exact v2 reconciliation successor followed by the " + "exact v3 activation successor" ) @@ -8745,6 +8777,14 @@ def is_device_plane_manager_control_plane_slice(component, entries): ) +def is_device_plane_manager_v2_control_plane_slice(component, entries): + return ( + component == "device-plane" + and entries is not None + and tuple(entries) == DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_ENTRIES + ) + + def is_device_plane_manager_failed_control_plane_slice(component, entries): return ( component == "device-plane" @@ -8796,15 +8836,15 @@ def expected_platform_device_manager_public_route_descriptor(): def expected_device_plane_manager_control_plane_descriptor(): return { "schemaVersion": ( - "nodedc.device-plane.device-manager-control-plane.v2" + "nodedc.device-plane.device-manager-control-plane.v3" ), "action": "activate", "predecessor": { - "patchId": DEVICE_PLANE_MANAGER_RECONCILIATION_PATCH_ID, + "patchId": DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID, "artifactSha256": ( - DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT_SHA256 + DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256 ), - "mode": "failed-control-plane-baseline-adoption", + "mode": "failed-v2-control-plane-baseline-adoption", }, "service": "device-manager", "publicIngress": "reverse-proxy-only", @@ -8813,7 +8853,7 @@ def expected_device_plane_manager_control_plane_descriptor(): "healthGate": "bounded-container-grace+core-contract", "commandTransport": "disabled", "gelios": "untouched", - "rollback": "restore-reconciled-baseline", + "rollback": "restore-v2-reconciled-baseline", } @@ -10455,7 +10495,7 @@ def validate_device_plane_manager_activation_predecessor(payload_dir): payload_dir ) reconciliation_artifact = ( - APPLIED_DIR / DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT + APPLIED_DIR / DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT ) try: artifact_stat = reconciliation_artifact.lstat() @@ -10465,7 +10505,7 @@ def validate_device_plane_manager_activation_predecessor(payload_dir): stat.S_ISLNK(artifact_stat.st_mode) or not stat.S_ISREG(artifact_stat.st_mode) or sha256_file(reconciliation_artifact) - != DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT_SHA256 + != DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256 ): die("Device Manager reconciliation applied artifact mismatch") @@ -10473,30 +10513,30 @@ def validate_device_plane_manager_activation_predecessor(payload_dir): value for value in load_state(STATE_FILE) if value.get("id") - == DEVICE_PLANE_MANAGER_RECONCILIATION_PATCH_ID + == DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID ] if len(records) != 1: die("Device Manager reconciliation applied journal count mismatch") record = records[0] if ( record.get("artifact") - != DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT + != DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT or record.get("backup_id") - != DEVICE_PLANE_MANAGER_RECONCILIATION_APPLY_BACKUP_ID + != DEVICE_PLANE_MANAGER_V2_RECONCILIATION_APPLY_BACKUP_ID or record.get("component") != "device-plane" or record.get("sha256") - != DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT_SHA256 + != DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256 or record.get("status") != "ok" ): die("Device Manager reconciliation applied journal mismatch") - backup_dir = validate_device_plane_manager_reconciliation_backup() - runtime = validate_device_plane_manager_reconciled_baseline( + backup_dir = validate_device_plane_manager_v2_reconciliation_backup() + runtime = validate_device_plane_manager_v2_reconciled_baseline( backup_dir, marker_installed=True, ) return { - "mode": "reconciled-manager-forward-activation", + "mode": "reconciled-v2-manager-forward-activation", "descriptor": descriptor, "reconciliationArtifact": reconciliation_artifact, "reconciliationRecord": record, @@ -10538,7 +10578,7 @@ def validate_device_plane_manager_v2_reconciliation_backup(): backup_dir / "missing-files.txt" )) validate_backup_partition( - DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES, + DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_ENTRIES, existing, missing, "Device Manager v2 reconciliation", @@ -10584,7 +10624,10 @@ def validate_device_plane_manager_v2_reconciled_baseline( ) if live_source != backup_source: die("Device Manager v2 rollback source does not match backup") - for rel in DEVICE_PLANE_MANAGER_V2_RECONCILIATION_MISSING: + for rel in ( + *DEVICE_PLANE_MANAGER_V2_RECONCILIATION_MISSING, + DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL, + ): path = root / rel if path.exists() or path.is_symlink(): die( @@ -10667,7 +10710,7 @@ def validate_device_plane_manager_v2_reconciliation_evidence(payload_dir): or failed_manifest.get("component") != "device-plane" or failed_manifest.get("type") != "app-overlay" or tuple(failed_entries) - != DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES + != DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_ENTRIES ): die("Device Manager v2 failed artifact contract mismatch") @@ -18119,11 +18162,11 @@ def plan_artifact(artifact): ) print( "device_plane_predecessor_patch=" - f"{DEVICE_PLANE_MANAGER_RECONCILIATION_PATCH_ID}" + f"{DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID}" ) print( "device_plane_predecessor_artifact_sha256=" - f"{DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT_SHA256}" + f"{DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256}" ) print( "device_plane_runtime_mutation=" diff --git a/infra/deploy-runner/test_device_manager_control_plane_artifacts.py b/infra/deploy-runner/test_device_manager_control_plane_artifacts.py index c621977..0ae766d 100644 --- a/infra/deploy-runner/test_device_manager_control_plane_artifacts.py +++ b/infra/deploy-runner/test_device_manager_control_plane_artifacts.py @@ -239,21 +239,28 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase): ) runtime_acceptance.assert_called_once_with() - def test_v2_activation_requires_exact_applied_reconciliation(self): + def test_v3_activation_requires_exact_applied_v2_reconciliation(self): with tempfile.TemporaryDirectory( - prefix="nodedc-device-manager-v2-predecessor-", + prefix="nodedc-device-manager-v3-predecessor-", ) as directory: root = Path(directory) - artifact = root / RUNNER.DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT + artifact = ( + root + / RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT + ) artifact_bytes = b"reviewed-reconciliation-artifact" artifact.write_bytes(artifact_bytes) state_file = root / "applied.jsonl" state_file.write_text( json.dumps({ - "id": RUNNER.DEVICE_PLANE_MANAGER_RECONCILIATION_PATCH_ID, - "artifact": RUNNER.DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT, + "id": ( + RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID + ), + "artifact": ( + RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT + ), "backup_id": ( - RUNNER.DEVICE_PLANE_MANAGER_RECONCILIATION_APPLY_BACKUP_ID + RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_APPLY_BACKUP_ID ), "component": "device-plane", "sha256": hashlib.sha256(artifact_bytes).hexdigest(), @@ -267,7 +274,7 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase): mock.patch.object(RUNNER, "STATE_FILE", state_file), mock.patch.object( RUNNER, - "DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT_SHA256", + "DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256", hashlib.sha256(artifact_bytes).hexdigest(), ), mock.patch.object( @@ -279,12 +286,12 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase): ) as payload, mock.patch.object( RUNNER, - "validate_device_plane_manager_reconciliation_backup", + "validate_device_plane_manager_v2_reconciliation_backup", return_value=backup, ), mock.patch.object( RUNNER, - "validate_device_plane_manager_reconciled_baseline", + "validate_device_plane_manager_v2_reconciled_baseline", return_value={"accepted": True}, ) as baseline, ): @@ -296,7 +303,7 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase): self.assertEqual( result["mode"], - "reconciled-manager-forward-activation", + "reconciled-v2-manager-forward-activation", ) payload.assert_called_once_with(root / "payload") baseline.assert_called_once_with(backup, marker_installed=True) diff --git a/infra/deploy-runner/test_device_manager_control_plane_reconciliation_artifact.py b/infra/deploy-runner/test_device_manager_control_plane_reconciliation_artifact.py index 2f7cbf8..79d03bb 100644 --- a/infra/deploy-runner/test_device_manager_control_plane_reconciliation_artifact.py +++ b/infra/deploy-runner/test_device_manager_control_plane_reconciliation_artifact.py @@ -221,6 +221,15 @@ class DeviceManagerControlPlaneReconciliationArtifactTest(unittest.TestCase): {"id": "different"}, RUNNER.DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256, ) + with self.assertRaisesRegex( + RUNNER.DeployError, + "exact v2 reconciliation successor", + ): + RUNNER.reject_terminal_device_plane_manager_artifact( + {"id": "different", "component": "device-plane"}, + "1" * 64, + RUNNER.DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_ENTRIES, + ) def test_health_grace_waits_through_unhealthy_and_exited(self): results = [