fix(deploy): version device manager edge release contract

This commit is contained in:
Codex
2026-08-11 20:43:07 +03:00
parent 724e745628
commit 0f44cca27d
5 changed files with 283 additions and 59 deletions
@@ -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-release-20260811-011", ...extra] = process.argv.slice(2);
const [patchId = "device-manager-release-20260811-012", ...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-release-v1.json",
"deployment/device-manager-release-v2.json",
];
const stage = await mkdtemp(join(tmpdir(), "nodedc-device-manager-control-plane-"));
const payload = join(stage, "payload");
@@ -40,7 +40,7 @@ try {
if (build.status !== 0) throw new Error(`device_manager_build_failed:${build.stderr || build.stdout}`);
await mkdir(payload, { recursive: true });
for (const entry of entries) {
if (entry === "deployment/device-manager-release-v1.json") {
if (entry === "deployment/device-manager-release-v2.json") {
const descriptor = JSON.parse(await readFile(resolve(devicePlaneRoot, entry), "utf8"));
if (descriptor.releaseId !== "__PATCH_ID__") {
throw new Error("device_manager_release_template_id_mismatch");
@@ -109,12 +109,12 @@ 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-release-v1.json"),
join(payload, "deployment/device-manager-release-v2.json"),
"utf8",
));
const predecessor = descriptor.predecessor;
if (
descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v1"
descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v2"
|| descriptor.releaseId !== patchId
|| !["activate", "upgrade"].includes(descriptor.action)
|| !predecessor
+177 -44
View File
@@ -240,10 +240,19 @@ DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_REL = (
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL = (
"deployment/device-manager-release-v1.json"
)
DEVICE_PLANE_MANAGER_RELEASE_V2_REL = (
"deployment/device-manager-release-v2.json"
)
DEVICE_PLANE_MANAGER_COMPOSE_REL = "docker-compose.device-manager.yml"
DEVICE_PLANE_MANAGER_COMPOSE_SHA256 = (
DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256 = (
"4954120aaddc999798b64c304d8cf692b79714feb727d873117bd1f3434e865e"
)
DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256 = (
"369a2acf9c1a1030b9e1c6c366144b1eaf8900aef0ee59bb6bf23250b7b371b9"
)
DEVICE_PLANE_MANAGER_COMPOSE_SHA256 = (
DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256
)
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_ENTRIES = (
".dockerignore",
"package.json",
@@ -283,6 +292,19 @@ DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES = (
"services/device-manager",
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
)
DEVICE_PLANE_MANAGER_RELEASE_V2_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_RELEASE_V2_REL,
)
DEVICE_PLANE_MANAGER_RECONCILIATION_REL = (
"deployment/device-manager-control-plane-reconciliation-v1.json"
)
@@ -3505,6 +3527,7 @@ def allowed_payload_path(component, rel):
DEVICE_PLANE_BACKHAUL_TARGET_REL,
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL,
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
DEVICE_PLANE_MANAGER_RECONCILIATION_REL,
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL,
"packages/device-protocol-contract",
@@ -8551,7 +8574,7 @@ def load_artifact(artifact, work_dir):
manifest["component"],
entries,
):
validate_device_plane_manager_control_plane_payload(
validate_device_plane_manager_release_payload(
payload_dir,
expected_release_id=manifest["id"],
)
@@ -9023,7 +9046,18 @@ def is_device_plane_manager_control_plane_slice(component, entries):
return (
component == "device-plane"
and entries is not None
and tuple(entries) == DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES
and tuple(entries) in (
DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
)
)
def is_device_plane_manager_release_v2_slice(component, entries):
return (
component == "device-plane"
and entries is not None
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES
)
@@ -9083,12 +9117,22 @@ def expected_platform_device_manager_public_route_descriptor():
}
def expected_device_plane_manager_release_boundaries():
def expected_device_plane_manager_release_v1_boundaries():
return {
"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-preapply-snapshot",
}
def expected_device_plane_manager_release_v2_boundaries():
return {
**expected_device_plane_manager_release_v1_boundaries(),
"edgeChannel": (
"core-initiated-pinned-mtls-enabled-zero-or-more-registered-edges"
),
@@ -9099,13 +9143,15 @@ def expected_device_plane_manager_release_boundaries():
"dedicated-core-only-bridge-no-host-ingress-"
"public-ipv4-tcp-8443-registration-policy"
),
"healthGate": "bounded-container-grace+core-contract",
"commandTransport": "disabled",
"gelios": "untouched",
"rollback": "restore-preapply-snapshot",
}
def expected_device_plane_manager_release_boundaries():
# Compatibility name for the current release builder/tests. Immutable v1
# predecessors always use expected_device_plane_manager_release_v1_boundaries.
return expected_device_plane_manager_release_v2_boundaries()
def expected_device_plane_manager_failed_control_plane_descriptor():
return {
"schemaVersion": (
@@ -9231,29 +9277,28 @@ def validate_platform_device_manager_public_route_payload(payload_dir):
return descriptor
def validate_device_plane_manager_control_plane_payload(
payload_dir,
def validate_device_plane_manager_release_descriptor(
descriptor,
*,
schema_version,
boundaries,
expected_release_id=None,
):
descriptor = read_strict_json(
payload_dir / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
"Device Manager release descriptor",
max_bytes=16 * 1024,
)
required_keys = {
"schemaVersion",
"releaseId",
"action",
"predecessor",
*expected_device_plane_manager_release_boundaries(),
*boundaries,
}
if set(descriptor) != required_keys:
die("Device Manager release descriptor key set mismatch")
if (
descriptor.get("schemaVersion")
!= "nodedc.device-plane.device-manager-release.v1"
):
missing = ",".join(sorted(required_keys - set(descriptor))) or "none"
extra = ",".join(sorted(set(descriptor) - required_keys)) or "none"
die(
"Device Manager release descriptor key set mismatch: "
f"schema={schema_version} missing={missing} extra={extra}"
)
if descriptor.get("schemaVersion") != schema_version:
die("Device Manager release descriptor schema mismatch")
release_id = descriptor.get("releaseId")
if (
@@ -9284,16 +9329,39 @@ def validate_device_plane_manager_control_plane_payload(
!= (predecessor["kind"] == "reconciliation")
):
die("Device Manager release predecessor mismatch")
boundaries = expected_device_plane_manager_release_boundaries()
if any(descriptor.get(key) != value for key, value in boundaries.items()):
die("Device Manager release security boundary mismatch")
return descriptor
def validate_device_plane_manager_release_payload_contract(
payload_dir,
*,
descriptor_rel,
schema_version,
boundaries,
compose_sha256,
edge_channel,
expected_release_id=None,
):
descriptor = read_strict_json(
payload_dir / descriptor_rel,
"Device Manager release descriptor",
max_bytes=16 * 1024,
)
validate_device_plane_manager_release_descriptor(
descriptor,
schema_version=schema_version,
boundaries=boundaries,
expected_release_id=expected_release_id,
)
compose_path = payload_dir / DEVICE_PLANE_MANAGER_COMPOSE_REL
if sha256_file(compose_path) != DEVICE_PLANE_MANAGER_COMPOSE_SHA256:
if sha256_file(compose_path) != compose_sha256:
die("Device Manager control-plane Compose mismatch")
compose = compose_path.read_text(
encoding="utf-8"
)
for required in (
required_compose = [
"device-manager:",
'DEVICE_MANAGEMENT_API_ENABLED: "true"',
"DEVICE_MANAGEMENT_CORE_TOKEN_FILE: "
@@ -9302,22 +9370,26 @@ def validate_device_plane_manager_control_plane_payload(
"/run/nodedc-secrets/device-core-internal-token",
"NODEDC_DEVICE_CORE_TOKEN_FILE: "
"/run/nodedc-secrets/management-core-token",
'DEVICE_EDGE_CHANNEL_ENABLED: "true"',
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE: "
"/run/nodedc-secrets/device-edge-channel/core-private-key.pem",
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE: "
"/run/nodedc-secrets/device-edge-channel/core-certificate.pem",
"DEVICE_EDGE_CHANNEL_TRUST_ROOT: "
"/run/nodedc-secrets/device-edge-channel/peers",
"source: /volume1/docker/nodedc-device-plane/secrets/"
"device-edge-channel/core-private-key.pem",
"source: /volume1/docker/nodedc-device-plane/secrets/"
"device-edge-channel/core-certificate.pem",
"source: /volume1/docker/nodedc-device-plane/secrets/"
"device-edge-channel/peers",
"name: nodedc-device-plane-egress",
"name: nodedc-platform_edge",
):
]
if edge_channel:
required_compose.extend((
'DEVICE_EDGE_CHANNEL_ENABLED: "true"',
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE: "
"/run/nodedc-secrets/device-edge-channel/core-private-key.pem",
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE: "
"/run/nodedc-secrets/device-edge-channel/core-certificate.pem",
"DEVICE_EDGE_CHANNEL_TRUST_ROOT: "
"/run/nodedc-secrets/device-edge-channel/peers",
"source: /volume1/docker/nodedc-device-plane/secrets/"
"device-edge-channel/core-private-key.pem",
"source: /volume1/docker/nodedc-device-plane/secrets/"
"device-edge-channel/core-certificate.pem",
"source: /volume1/docker/nodedc-device-plane/secrets/"
"device-edge-channel/peers",
"name: nodedc-device-plane-egress",
))
for required in required_compose:
if required not in compose:
die(f"Device Manager control-plane boundary missing: {required}")
for forbidden in (
@@ -9330,6 +9402,59 @@ def validate_device_plane_manager_control_plane_payload(
return descriptor
def validate_device_plane_manager_control_plane_payload(
payload_dir,
*,
expected_release_id=None,
):
return validate_device_plane_manager_release_payload_contract(
payload_dir,
descriptor_rel=DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
schema_version="nodedc.device-plane.device-manager-release.v1",
boundaries=expected_device_plane_manager_release_v1_boundaries(),
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256,
edge_channel=False,
expected_release_id=expected_release_id,
)
def validate_device_plane_manager_release_v2_payload(
payload_dir,
*,
expected_release_id=None,
):
return validate_device_plane_manager_release_payload_contract(
payload_dir,
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
schema_version="nodedc.device-plane.device-manager-release.v2",
boundaries=expected_device_plane_manager_release_v2_boundaries(),
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256,
edge_channel=True,
expected_release_id=expected_release_id,
)
def validate_device_plane_manager_release_payload(
payload_dir,
*,
expected_release_id=None,
):
v1 = payload_dir / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
v2 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V2_REL
present = [path for path in (v1, v2) if path.exists() or path.is_symlink()]
if len(present) != 1:
die("Device Manager release descriptor cardinality mismatch")
if present[0] == v2:
return validate_device_plane_manager_release_v2_payload(
payload_dir,
expected_release_id=expected_release_id,
)
return validate_device_plane_manager_control_plane_payload(
payload_dir,
expected_release_id=expected_release_id,
)
def validate_device_plane_manager_failed_control_plane_payload(payload_dir):
descriptor = read_strict_json(
payload_dir / DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL,
@@ -10802,7 +10927,7 @@ def validate_device_plane_manager_reconciliation_evidence(payload_dir):
def validate_device_plane_manager_activation_predecessor(payload_dir):
descriptor = validate_device_plane_manager_control_plane_payload(
descriptor = validate_device_plane_manager_release_payload(
payload_dir
)
predecessor = descriptor["predecessor"]
@@ -10904,18 +11029,26 @@ def validate_device_plane_manager_activation_predecessor(payload_dir):
else:
if (
descriptor["action"] != "upgrade"
or tuple(predecessor_entries)
!= DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES
or tuple(predecessor_entries) not in (
DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
)
):
die("Device Manager release predecessor type mismatch")
predecessor_descriptor = (
validate_device_plane_manager_control_plane_payload(
validate_device_plane_manager_release_payload(
predecessor_payload,
expected_release_id=patch_id,
)
)
predecessor_descriptor_rel = (
DEVICE_PLANE_MANAGER_RELEASE_V2_REL
if tuple(predecessor_entries)
== DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES
else DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
)
installed_descriptor = read_strict_json(
root / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
root / predecessor_descriptor_rel,
"installed Device Manager release predecessor",
max_bytes=16 * 1024,
)
@@ -69,6 +69,27 @@ def device_manager_release_descriptor(
predecessor_kind="reconciliation",
predecessor_patch="device-manager-reconciliation-unit-001",
predecessor_sha="a" * 64,
):
return {
"schemaVersion": "nodedc.device-plane.device-manager-release.v2",
"releaseId": release_id,
"action": action,
"predecessor": {
"kind": predecessor_kind,
"patchId": predecessor_patch,
"artifactSha256": predecessor_sha,
},
**RUNNER.expected_device_plane_manager_release_v2_boundaries(),
}
def device_manager_release_v1_descriptor(
release_id="device-manager-release-v1-unit-001",
*,
action="upgrade",
predecessor_kind="release",
predecessor_patch="device-manager-release-v1-unit-000",
predecessor_sha="a" * 64,
):
return {
"schemaVersion": "nodedc.device-plane.device-manager-release.v1",
@@ -79,7 +100,7 @@ def device_manager_release_descriptor(
"patchId": predecessor_patch,
"artifactSha256": predecessor_sha,
},
**RUNNER.expected_device_plane_manager_release_boundaries(),
**RUNNER.expected_device_plane_manager_release_v1_boundaries(),
}
@@ -164,7 +185,7 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
manifest, entries, names, result = self.assert_deterministic_artifact(
"build-device-manager-control-plane-artifact.mjs",
"device-manager-control-plane-unit-001",
RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
)
self.assertEqual(manifest["component"], "device-plane")
self.assertEqual(
@@ -205,6 +226,58 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
self.assertEqual(checks[0]["expected_json"]["managementApi"], "enabled")
self.assertEqual(checks[0]["expected_json"]["discoveryIngest"], "enabled")
def test_release_v2_keeps_release_v1_predecessor_contract_immutable(self):
predecessor = device_manager_release_v1_descriptor(
release_id="device-manager-release-20260811-010",
)
candidate = device_manager_release_descriptor(
release_id="device-manager-release-20260811-012",
action="upgrade",
predecessor_kind="release",
predecessor_patch="device-manager-release-20260811-010",
predecessor_sha="d" * 64,
)
self.assertIs(
RUNNER.validate_device_plane_manager_release_descriptor(
predecessor,
schema_version=(
"nodedc.device-plane.device-manager-release.v1"
),
boundaries=(
RUNNER.expected_device_plane_manager_release_v1_boundaries()
),
expected_release_id="device-manager-release-20260811-010",
),
predecessor,
)
self.assertIs(
RUNNER.validate_device_plane_manager_release_descriptor(
candidate,
schema_version=(
"nodedc.device-plane.device-manager-release.v2"
),
boundaries=(
RUNNER.expected_device_plane_manager_release_v2_boundaries()
),
expected_release_id="device-manager-release-20260811-012",
),
candidate,
)
with self.assertRaisesRegex(
RUNNER.DeployError,
r"schema=.*release\.v2 missing=edgeChannel,edgeChannelEgress,"
r"edgeChannelIdentity extra=none",
):
RUNNER.validate_device_plane_manager_release_descriptor(
predecessor,
schema_version=(
"nodedc.device-plane.device-manager-release.v2"
),
boundaries=(
RUNNER.expected_device_plane_manager_release_v2_boundaries()
),
)
def test_public_route_artifact_is_last_and_proxy_only(self):
manifest, entries, _names, result = self.assert_deterministic_artifact(
"build-platform-device-manager-route-artifact.mjs",
@@ -239,7 +312,7 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
):
RUNNER.prepare_component_runtime(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
)
self.assertEqual(
[call.args[0] for call in ensure.call_args_list],
@@ -254,7 +327,7 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
ensure_edge_identity.assert_called_once_with()
def test_apply_gate_checks_exact_services_core_contract_and_runtime_boundary(self):
entries = RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES
entries = RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES
services = ("device-control-core", "device-manager")
with (
mock.patch.object(
@@ -341,7 +414,7 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
),
mock.patch.object(
RUNNER,
"validate_device_plane_manager_control_plane_payload",
"validate_device_plane_manager_release_payload",
return_value=descriptor,
) as payload,
mock.patch.object(
@@ -441,7 +514,7 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
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(
predecessor_descriptor = device_manager_release_v1_descriptor(
release_id=predecessor_patch,
)
candidate_descriptor = device_manager_release_descriptor(
@@ -483,7 +556,7 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
),
mock.patch.object(
RUNNER,
"validate_device_plane_manager_control_plane_payload",
"validate_device_plane_manager_release_payload",
side_effect=(
candidate_descriptor,
predecessor_descriptor,