fix(device-plane): activate manager from v2 reconciliation

This commit is contained in:
Codex
2026-08-11 10:14:24 +03:00
parent 37bdbebb4e
commit 1adbabefcf
5 changed files with 116 additions and 39 deletions
@@ -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"
}
@@ -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 designRoot = resolve(process.env.NODEDC_DEVICE_MANAGER_SOURCE_ROOT || resolve(platformRoot, "../NODEDC_DESIGN_GUIDELINE"));
const managerRoot = resolve(designRoot, "apps/device-manager"); const managerRoot = resolve(designRoot, "apps/device-manager");
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts")); 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]"); 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 = [ const entries = [
@@ -26,7 +26,7 @@ const entries = [
"services/device-gateway/package.json", "services/device-gateway/package.json",
"services/device-edge-relay/package.json", "services/device-edge-relay/package.json",
"services/device-manager", "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 stage = await mkdtemp(join(tmpdir(), "nodedc-device-manager-control-plane-"));
const payload = join(stage, "payload"); const payload = join(stage, "payload");
@@ -89,15 +89,16 @@ try {
if (compose.includes(forbidden)) throw new Error(`device_manager_compose_boundary_violation:${forbidden}`); if (compose.includes(forbidden)) throw new Error(`device_manager_compose_boundary_violation:${forbidden}`);
} }
const descriptor = JSON.parse(await readFile( 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", "utf8",
)); ));
if ( if (
descriptor.schemaVersion !== "nodedc.device-plane.device-manager-control-plane.v2" descriptor.schemaVersion !== "nodedc.device-plane.device-manager-control-plane.v3"
|| descriptor.predecessor?.patchId !== "device-manager-control-plane-reconciliation-20260811-002" || descriptor.predecessor?.patchId !== "device-manager-control-plane-v2-reconciliation-20260811-004"
|| descriptor.predecessor?.artifactSha256 !== "dd86dd58e4f649db0981db5089e003caf3961356179f2abb514662351487e1e6" || descriptor.predecessor?.artifactSha256 !== "09600bfd99717314b43936e17ffaaf6b6fca1f2fd008beddddc68480cdf3d284"
|| descriptor.predecessor?.mode !== "failed-v2-control-plane-baseline-adoption"
|| descriptor.healthGate !== "bounded-container-grace+core-contract" || 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"); ) 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, "manifest.env"), `id=${patchId}\ncomponent=device-plane\ntype=app-overlay\n`, "utf8");
await writeFile(join(stage, "files.txt"), `${entries.join("\n")}\n`, "utf8"); await writeFile(join(stage, "files.txt"), `${entries.join("\n")}\n`, "utf8");
+65 -22
View File
@@ -213,9 +213,12 @@ DEVICE_PLANE_FOUNDATION_ENTRIES = (
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL = ( DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL = (
"deployment/device-manager-control-plane-v1.json" "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" "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_REL = "docker-compose.device-manager.yml"
DEVICE_PLANE_MANAGER_COMPOSE_SHA256 = ( DEVICE_PLANE_MANAGER_COMPOSE_SHA256 = (
"4954120aaddc999798b64c304d8cf692b79714feb727d873117bd1f3434e865e" "4954120aaddc999798b64c304d8cf692b79714feb727d873117bd1f3434e865e"
@@ -233,6 +236,19 @@ DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_ENTRIES = (
"services/device-manager", "services/device-manager",
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL, 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 = ( DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES = (
".dockerignore", ".dockerignore",
"package.json", "package.json",
@@ -334,6 +350,17 @@ DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256 = (
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID = ( DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID = (
"device-manager-control-plane-v2-reconciliation-20260811-004" "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_MANAGER_V2_RECONCILIATION_BACKUP_ID = (
"device-plane-device-manager-control-plane-20260811-003-" "device-plane-device-manager-control-plane-20260811-003-"
"20260811-012505" "20260811-012505"
@@ -371,7 +398,7 @@ DEVICE_PLANE_MANAGER_V2_RECONCILIATION_EXISTING = (
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_MISSING = ( DEVICE_PLANE_MANAGER_V2_RECONCILIATION_MISSING = (
DEVICE_PLANE_MANAGER_COMPOSE_REL, DEVICE_PLANE_MANAGER_COMPOSE_REL,
"services/device-manager", "services/device-manager",
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL, DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_REL,
) )
DEVICE_PLANE_FOUNDATION_RECOVERY_REL = ( DEVICE_PLANE_FOUNDATION_RECOVERY_REL = (
"deployment/device-plane-foundation-recovery-v1.json" "deployment/device-plane-foundation-recovery-v1.json"
@@ -8677,10 +8704,15 @@ def reject_terminal_device_plane_manager_artifact(
if ( if (
manifest.get("id") == DEVICE_PLANE_MANAGER_V2_FAILED_PATCH_ID manifest.get("id") == DEVICE_PLANE_MANAGER_V2_FAILED_PATCH_ID
or sha256 == DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256 or sha256 == DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256
or is_device_plane_manager_v2_control_plane_slice(
manifest.get("component"),
entries,
)
): ):
die( die(
"Device Manager control-plane 003 is terminal failed; " "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): def is_device_plane_manager_failed_control_plane_slice(component, entries):
return ( return (
component == "device-plane" component == "device-plane"
@@ -8796,15 +8836,15 @@ def expected_platform_device_manager_public_route_descriptor():
def expected_device_plane_manager_control_plane_descriptor(): def expected_device_plane_manager_control_plane_descriptor():
return { return {
"schemaVersion": ( "schemaVersion": (
"nodedc.device-plane.device-manager-control-plane.v2" "nodedc.device-plane.device-manager-control-plane.v3"
), ),
"action": "activate", "action": "activate",
"predecessor": { "predecessor": {
"patchId": DEVICE_PLANE_MANAGER_RECONCILIATION_PATCH_ID, "patchId": DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID,
"artifactSha256": ( "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", "service": "device-manager",
"publicIngress": "reverse-proxy-only", "publicIngress": "reverse-proxy-only",
@@ -8813,7 +8853,7 @@ def expected_device_plane_manager_control_plane_descriptor():
"healthGate": "bounded-container-grace+core-contract", "healthGate": "bounded-container-grace+core-contract",
"commandTransport": "disabled", "commandTransport": "disabled",
"gelios": "untouched", "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 payload_dir
) )
reconciliation_artifact = ( reconciliation_artifact = (
APPLIED_DIR / DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT APPLIED_DIR / DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT
) )
try: try:
artifact_stat = reconciliation_artifact.lstat() 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) stat.S_ISLNK(artifact_stat.st_mode)
or not stat.S_ISREG(artifact_stat.st_mode) or not stat.S_ISREG(artifact_stat.st_mode)
or sha256_file(reconciliation_artifact) 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") die("Device Manager reconciliation applied artifact mismatch")
@@ -10473,30 +10513,30 @@ def validate_device_plane_manager_activation_predecessor(payload_dir):
value value
for value in load_state(STATE_FILE) for value in load_state(STATE_FILE)
if value.get("id") if value.get("id")
== DEVICE_PLANE_MANAGER_RECONCILIATION_PATCH_ID == DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID
] ]
if len(records) != 1: if len(records) != 1:
die("Device Manager reconciliation applied journal count mismatch") die("Device Manager reconciliation applied journal count mismatch")
record = records[0] record = records[0]
if ( if (
record.get("artifact") record.get("artifact")
!= DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT != DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT
or record.get("backup_id") 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("component") != "device-plane"
or record.get("sha256") or record.get("sha256")
!= DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT_SHA256 != DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256
or record.get("status") != "ok" or record.get("status") != "ok"
): ):
die("Device Manager reconciliation applied journal mismatch") die("Device Manager reconciliation applied journal mismatch")
backup_dir = validate_device_plane_manager_reconciliation_backup() backup_dir = validate_device_plane_manager_v2_reconciliation_backup()
runtime = validate_device_plane_manager_reconciled_baseline( runtime = validate_device_plane_manager_v2_reconciled_baseline(
backup_dir, backup_dir,
marker_installed=True, marker_installed=True,
) )
return { return {
"mode": "reconciled-manager-forward-activation", "mode": "reconciled-v2-manager-forward-activation",
"descriptor": descriptor, "descriptor": descriptor,
"reconciliationArtifact": reconciliation_artifact, "reconciliationArtifact": reconciliation_artifact,
"reconciliationRecord": record, "reconciliationRecord": record,
@@ -10538,7 +10578,7 @@ def validate_device_plane_manager_v2_reconciliation_backup():
backup_dir / "missing-files.txt" backup_dir / "missing-files.txt"
)) ))
validate_backup_partition( validate_backup_partition(
DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES, DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_ENTRIES,
existing, existing,
missing, missing,
"Device Manager v2 reconciliation", "Device Manager v2 reconciliation",
@@ -10584,7 +10624,10 @@ def validate_device_plane_manager_v2_reconciled_baseline(
) )
if live_source != backup_source: if live_source != backup_source:
die("Device Manager v2 rollback source does not match backup") 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 path = root / rel
if path.exists() or path.is_symlink(): if path.exists() or path.is_symlink():
die( 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("component") != "device-plane"
or failed_manifest.get("type") != "app-overlay" or failed_manifest.get("type") != "app-overlay"
or tuple(failed_entries) 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") die("Device Manager v2 failed artifact contract mismatch")
@@ -18119,11 +18162,11 @@ def plan_artifact(artifact):
) )
print( print(
"device_plane_predecessor_patch=" "device_plane_predecessor_patch="
f"{DEVICE_PLANE_MANAGER_RECONCILIATION_PATCH_ID}" f"{DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID}"
) )
print( print(
"device_plane_predecessor_artifact_sha256=" "device_plane_predecessor_artifact_sha256="
f"{DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT_SHA256}" f"{DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256}"
) )
print( print(
"device_plane_runtime_mutation=" "device_plane_runtime_mutation="
@@ -239,21 +239,28 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
) )
runtime_acceptance.assert_called_once_with() 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( with tempfile.TemporaryDirectory(
prefix="nodedc-device-manager-v2-predecessor-", prefix="nodedc-device-manager-v3-predecessor-",
) as directory: ) as directory:
root = Path(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_bytes = b"reviewed-reconciliation-artifact"
artifact.write_bytes(artifact_bytes) artifact.write_bytes(artifact_bytes)
state_file = root / "applied.jsonl" state_file = root / "applied.jsonl"
state_file.write_text( state_file.write_text(
json.dumps({ json.dumps({
"id": RUNNER.DEVICE_PLANE_MANAGER_RECONCILIATION_PATCH_ID, "id": (
"artifact": RUNNER.DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT, RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID
),
"artifact": (
RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT
),
"backup_id": ( "backup_id": (
RUNNER.DEVICE_PLANE_MANAGER_RECONCILIATION_APPLY_BACKUP_ID RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_APPLY_BACKUP_ID
), ),
"component": "device-plane", "component": "device-plane",
"sha256": hashlib.sha256(artifact_bytes).hexdigest(), "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, "STATE_FILE", state_file),
mock.patch.object( mock.patch.object(
RUNNER, RUNNER,
"DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT_SHA256", "DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256",
hashlib.sha256(artifact_bytes).hexdigest(), hashlib.sha256(artifact_bytes).hexdigest(),
), ),
mock.patch.object( mock.patch.object(
@@ -279,12 +286,12 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
) as payload, ) as payload,
mock.patch.object( mock.patch.object(
RUNNER, RUNNER,
"validate_device_plane_manager_reconciliation_backup", "validate_device_plane_manager_v2_reconciliation_backup",
return_value=backup, return_value=backup,
), ),
mock.patch.object( mock.patch.object(
RUNNER, RUNNER,
"validate_device_plane_manager_reconciled_baseline", "validate_device_plane_manager_v2_reconciled_baseline",
return_value={"accepted": True}, return_value={"accepted": True},
) as baseline, ) as baseline,
): ):
@@ -296,7 +303,7 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
result["mode"], result["mode"],
"reconciled-manager-forward-activation", "reconciled-v2-manager-forward-activation",
) )
payload.assert_called_once_with(root / "payload") payload.assert_called_once_with(root / "payload")
baseline.assert_called_once_with(backup, marker_installed=True) baseline.assert_called_once_with(backup, marker_installed=True)
@@ -221,6 +221,15 @@ class DeviceManagerControlPlaneReconciliationArtifactTest(unittest.TestCase):
{"id": "different"}, {"id": "different"},
RUNNER.DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256, 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): def test_health_grace_waits_through_unhealthy_and_exited(self):
results = [ results = [