chore(deploy): add plan-only Core incident audit

This commit is contained in:
DCCONSTRUCTIONS
2026-08-22 20:44:15 +03:00
parent 6dad30c565
commit 65c99613e5
3 changed files with 287 additions and 0 deletions
@@ -0,0 +1,36 @@
{
"schemaVersion": "nodedc.device-plane.device-control-core-incident-audit.v1",
"mode": "double-rollback-failed-read-only-audit",
"allowedOperation": "canonical-plan-only",
"applyAllowed": false,
"failedAttempts": [
{
"patchId": "device-control-core-release-v3-20260822-040",
"artifactSha256": "08448a56cdf391076f92c5242e368fd0033b420167874645eebfa1f22184ee92",
"backupId": "device-plane-device-control-core-release-v3-20260822-040-20260822-184245"
},
{
"patchId": "device-control-core-release-v3-reconciliation-20260822-042",
"artifactSha256": "54ab243439bce724fa0a0872b76cc32e0052ea5127153214d92872f02ae831cf",
"backupId": "device-plane-device-control-core-release-v3-reconciliation-20260822-042-20260822-195448"
}
],
"readOnlyEvidence": [
"device-control-core-runtime-inventory",
"device-control-core-bounded-container-logs",
"device-postgres-schema-presence",
"device-postgres-wait-activity"
],
"runtimeMutation": "none",
"sourceMutation": "none",
"databaseMutation": "none",
"networkMutation": "none",
"secretRead": "none",
"preservedServices": [
"device-control-core",
"device-manager",
"device-gateway",
"device-postgres",
"device-backhaul-target"
]
}
@@ -0,0 +1,92 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const sourceRoot = resolve(scriptDir, "../..");
const artifactDir = resolve(
process.env.NODEDC_DEPLOY_ARTIFACT_DIR
|| resolve(scriptDir, "../deploy-artifacts"),
);
const [
patchId = "device-control-core-incident-audit-20260822-043",
...extra
] = process.argv.slice(2);
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
throw new Error("usage: build-device-control-core-incident-audit-artifact.mjs [patch-id]");
}
const entry = "deployment/device-control-core-incident-audit-v1.json";
const stage = await mkdtemp(join(tmpdir(), "nodedc-control-core-incident-audit-"));
const payload = join(stage, "payload");
const target = resolve(artifactDir, `nodedc-device-plane-${patchId}.tgz`);
try {
const descriptor = JSON.parse(await readFile(resolve(sourceRoot, entry), "utf8"));
if (
descriptor.schemaVersion
!== "nodedc.device-plane.device-control-core-incident-audit.v1"
|| descriptor.mode !== "double-rollback-failed-read-only-audit"
|| descriptor.allowedOperation !== "canonical-plan-only"
|| descriptor.applyAllowed !== false
|| descriptor.runtimeMutation !== "none"
|| descriptor.sourceMutation !== "none"
|| descriptor.databaseMutation !== "none"
|| descriptor.networkMutation !== "none"
|| descriptor.secretRead !== "none"
) {
throw new Error("device_control_core_incident_audit_descriptor_mismatch");
}
await mkdir(dirname(join(payload, entry)), { recursive: true });
await cp(resolve(sourceRoot, entry), join(payload, entry), { force: true });
await writeFile(
join(stage, "manifest.env"),
`id=${patchId}\ncomponent=device-plane\ntype=app-overlay\n`,
"utf8",
);
await writeFile(join(stage, "files.txt"), `${entry}\n`, "utf8");
await mkdir(artifactDir, { recursive: true });
const tar = spawnSync(
"python3",
["-c", canonicalTarScript(), target, stage],
{ encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
);
if (tar.status !== 0) throw new Error(`tar_failed:${tar.stderr || tar.stdout}`);
const sha256 = createHash("sha256")
.update(await readFile(target))
.digest("hex");
console.log(JSON.stringify({
ok: true,
patchId,
component: "device-plane",
artifact: target,
sha256,
entries: [entry],
build: [],
services: [],
allowedOperation: descriptor.allowedOperation,
runtimeMutation: descriptor.runtimeMutation,
}, null, 2));
} finally {
await rm(stage, { recursive: true, force: true });
}
function canonicalTarScript() {
return [
"import gzip,io,pathlib,sys,tarfile",
"root=pathlib.Path(sys.argv[2])",
"with open(sys.argv[1],'wb') as out:",
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
" for top in ('manifest.env','files.txt','payload'):",
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
" for x in paths:",
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix()); info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)",
].join("\n");
}
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
import hashlib
import importlib.machinery
import importlib.util
import inspect
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 = Path(
os.environ.get(
"NODEDC_DEPLOY_RUNNER",
SCRIPT_DIR.parents[2] / "platform/infra/deploy-runner/nodedc-deploy",
)
)
BUILDER = SCRIPT_DIR / "build-device-control-core-incident-audit-artifact.mjs"
def load_runner():
loader = importlib.machinery.SourceFileLoader(
"nodedc_control_core_incident_audit_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 DeviceControlCoreIncidentAuditArtifactTest(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_artifact_is_deterministic_plan_only_audit(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-control-core-incident-audit-artifact-",
) as directory:
artifact_dir = Path(directory)
patch_id = "device-control-core-incident-audit-unit-043"
first = self.build(artifact_dir, patch_id)
first_bytes = Path(first["artifact"]).read_bytes()
second = self.build(artifact_dir, patch_id)
second_bytes = Path(second["artifact"]).read_bytes()
self.assertEqual(first_bytes, second_bytes)
self.assertEqual(
first["sha256"],
hashlib.sha256(first_bytes).hexdigest(),
)
self.assertEqual(first["build"], [])
self.assertEqual(first["services"], [])
self.assertEqual(first["allowedOperation"], "canonical-plan-only")
self.assertEqual(first["runtimeMutation"], "none")
with tarfile.open(first["artifact"], "r:gz") as archive:
descriptor = json.loads(
archive.extractfile(
"payload/"
+ RUNNER.DEVICE_PLANE_CONTROL_CORE_INCIDENT_AUDIT_REL
).read().decode("utf-8")
)
self.assertEqual(
descriptor,
RUNNER.expected_device_plane_control_core_incident_audit_descriptor(),
)
entries = RUNNER.DEVICE_PLANE_CONTROL_CORE_INCIDENT_AUDIT_ENTRIES
self.assertEqual(RUNNER.component_services("device-plane", entries), ())
self.assertEqual(RUNNER.component_builds("device-plane", entries), ())
def test_descriptor_pins_both_rollback_failures(self):
descriptor = (
RUNNER.expected_device_plane_control_core_incident_audit_descriptor()
)
self.assertFalse(descriptor["applyAllowed"])
self.assertEqual(descriptor["runtimeMutation"], "none")
self.assertEqual(
[item["artifactSha256"] for item in descriptor["failedAttempts"]],
[
"08448a56cdf391076f92c5242e368fd0033b420167874645eebfa1f22184ee92",
"54ab243439bce724fa0a0872b76cc32e0052ea5127153214d92872f02ae831cf",
],
)
def test_runtime_audit_reads_logs_and_database_without_mutation(self):
log_result = mock.Mock(
returncode=0,
stdout="Error: device_database_migration_blocked\n",
stderr="at PostgresDeviceRepository.migrate (postgres-repository.mjs:110)\n",
)
database_result = mock.Mock(
returncode=0,
stdout=(
"activity\t42,active,Lock,relation,schema-ddl,181\n"
"schema\thost-telemetry-table-present\n"
),
stderr="",
)
with mock.patch.object(
RUNNER,
"device_plane_service_container_ids",
side_effect=[("c" * 64,), ("p" * 64,)],
), mock.patch.object(
RUNNER.subprocess,
"run",
side_effect=[log_result, database_result],
) as run:
evidence = RUNNER.collect_device_plane_control_core_incident_audit()
self.assertEqual(evidence["coreContainerId"], "c" * 64)
self.assertIn(
"Error: device_database_migration_blocked",
evidence["logErrors"],
)
self.assertEqual(
evidence["database"],
[
"activity\t42,active,Lock,relation,schema-ddl,181",
"schema\thost-telemetry-table-present",
],
)
self.assertEqual(run.call_count, 2)
log_command = run.call_args_list[0].args[0]
self.assertEqual(log_command[1:4], ["logs", "--tail", "240"])
database_command = run.call_args_list[1].args[0]
self.assertEqual(database_command[1], "exec")
query = database_command[database_command.index("-c") + 1]
self.assertTrue(query.lstrip().startswith("select 'schema'"))
self.assertNotIn(";", query)
def test_apply_path_rejects_plan_only_audit_before_state_mutation(self):
source = inspect.getsource(RUNNER.apply_artifact)
self.assertIn("canonical-plan-only", source)
self.assertIn("apply is forbidden", source)
self.assertLess(
source.index("canonical-plan-only"),
source.index("state_has_sha"),
)
if __name__ == "__main__":
unittest.main()