fix(device-plane): split edge channel bootstrap

This commit is contained in:
Codex
2026-08-11 23:27:16 +03:00
parent bb0724c52c
commit d641744dca
8 changed files with 1298 additions and 113 deletions
@@ -0,0 +1,33 @@
{
"schemaVersion": "nodedc.device-plane.device-edge-core-channel-bootstrap.v1",
"transitionId": "__PATCH_ID__",
"action": "activate",
"managerPredecessor": {
"patchId": "device-manager-release-20260811-010",
"artifactSha256": "d4132993216eb674967dc6fc65d9670cfc2a9efdf46186ca019030f259de2d0e"
},
"failedPredecessor": {
"patchId": "device-manager-release-20260811-016",
"artifactSha256": "590405821b95b54088f926e0d3b2cdf9c704b339f6749da500c2bb64fe0e952d",
"backupId": "device-plane-device-manager-release-20260811-016-20260811-215941",
"invalidCoreCertificateSha256Fingerprint": "56:16:E0:3A:F4:03:85:FD:42:86:85:AF:2A:AF:1E:90:16:C8:F7:91:7C:AD:02:7D:B7:C2:ED:07:06:56:81:F6"
},
"service": "device-control-core",
"composeActivation": "dedicated-additive-override",
"identity": "runner-managed-host-local-private-key-public-certificate-export",
"identityRecovery": "exact-invalid-unexported-failed-predecessor-only",
"tlsPurpose": "clientAuth",
"direction": "core-initiated",
"publicIngress": "none-on-synology",
"edgeRegistrations": "preserved",
"commandTransport": "disabled",
"gelios": "untouched",
"preservedServices": [
"device-manager",
"device-gateway",
"device-postgres",
"device-backhaul-target"
],
"healthGate": "bounded-container-grace+core-edge-contract",
"rollback": "restore-source-and-preapply-core-runtime"
}
@@ -0,0 +1,36 @@
services:
device-control-core:
environment:
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
DEVICE_EDGE_CHANNEL_MAX_EDGES: "32"
DEVICE_EDGE_CHANNEL_RECONCILE_INTERVAL_MS: "15000"
volumes:
- type: bind
source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-private-key.pem
target: /run/nodedc-secrets/device-edge-channel/core-private-key.pem
read_only: true
bind:
create_host_path: false
- type: bind
source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-certificate.pem
target: /run/nodedc-secrets/device-edge-channel/core-certificate.pem
read_only: true
bind:
create_host_path: false
- type: bind
source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/peers
target: /run/nodedc-secrets/device-edge-channel/peers
read_only: true
bind:
create_host_path: false
networks:
- device-plane-egress
networks:
device-plane-egress:
name: nodedc-device-plane-egress
driver: bridge
internal: false
@@ -3,12 +3,6 @@ services:
environment:
DEVICE_MANAGEMENT_API_ENABLED: "true"
DEVICE_MANAGEMENT_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
DEVICE_EDGE_CHANNEL_MAX_EDGES: "32"
DEVICE_EDGE_CHANNEL_RECONCILE_INTERVAL_MS: "15000"
volumes:
- type: bind
source: /volume1/docker/nodedc-device-plane/secrets/management-core-token
@@ -16,26 +10,6 @@ services:
read_only: true
bind:
create_host_path: false
- type: bind
source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-private-key.pem
target: /run/nodedc-secrets/device-edge-channel/core-private-key.pem
read_only: true
bind:
create_host_path: false
- type: bind
source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-certificate.pem
target: /run/nodedc-secrets/device-edge-channel/core-certificate.pem
read_only: true
bind:
create_host_path: false
- type: bind
source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/peers
target: /run/nodedc-secrets/device-edge-channel/peers
read_only: true
bind:
create_host_path: false
networks:
- device-plane-egress
device-manager:
image: nodedc/device-manager:local
@@ -97,10 +71,6 @@ services:
start_period: 10s
networks:
device-plane-egress:
name: nodedc-device-plane-egress
driver: bridge
internal: false
platform-edge:
external: true
name: nodedc-platform_edge
@@ -0,0 +1,197 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { cp, lstat, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const platformRoot = resolve(scriptDir, "../..");
const devicePlaneRoot = resolve(platformRoot, "device-plane");
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
const [patchId = "device-edge-core-channel-bootstrap-20260811-017", ...extra] = process.argv.slice(2);
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
throw new Error("usage: build-device-edge-core-channel-bootstrap-artifact.mjs [patch-id]");
}
const descriptorPath = "deployment/device-edge-core-channel-bootstrap-v1.json";
const entries = [
".dockerignore",
"package.json",
"package-lock.json",
"docker-compose.device-edge-core-channel.yml",
"packages/device-protocol-contract",
"packages/device-edge-channel-contract",
"services/device-control-core",
descriptorPath,
];
const stage = await mkdtemp(join(tmpdir(), "nodedc-device-edge-core-channel-"));
const payload = join(stage, "payload");
const target = join(artifactDir, `nodedc-device-plane-${patchId}.tgz`);
try {
await mkdir(payload, { recursive: true });
for (const entry of entries) {
if (entry === descriptorPath) {
const descriptor = JSON.parse(await readFile(resolve(devicePlaneRoot, entry), "utf8"));
if (descriptor.transitionId !== "__PATCH_ID__") {
throw new Error("device_edge_core_channel_template_id_mismatch");
}
descriptor.transitionId = patchId;
const destination = join(payload, entry);
await mkdir(dirname(destination), { recursive: true });
await writeFile(destination, `${JSON.stringify(descriptor, null, 2)}\n`, "utf8");
continue;
}
await copySafe(resolve(devicePlaneRoot, entry), join(payload, entry), devicePlaneRoot);
}
await validateDockerCopySources(
join(payload, "services/device-control-core/Dockerfile"),
payload,
);
for (const modulePath of [
"services/device-control-core/src/sensitive-reference-management.mjs",
"services/device-control-core/src/device-gateway-core-runtime.mjs",
"packages/device-edge-channel-contract/src/index.mjs",
]) {
const imported = spawnSync(
process.execPath,
["--input-type=module", "--eval", `import(${JSON.stringify(pathToFileURL(join(payload, modulePath)).href)})`],
{ cwd: payload, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
);
if (imported.status !== 0) {
throw new Error(`device_edge_core_channel_staged_module_import_failed:${modulePath}:${imported.stderr || imported.stdout}`);
}
}
const compose = await readFile(join(payload, "docker-compose.device-edge-core-channel.yml"), "utf8");
for (const required of [
"device-control-core:",
"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",
]) {
if (!compose.includes(required)) {
throw new Error(`device_edge_core_channel_compose_contract_missing:${required}`);
}
}
for (const forbidden of [
"device-manager:",
"device-gateway:",
"device-postgres:",
"PRIVATE KEY",
"ports:",
"network_mode:",
"privileged:",
]) {
if (compose.includes(forbidden)) {
throw new Error(`device_edge_core_channel_compose_boundary_violation:${forbidden}`);
}
}
const descriptor = JSON.parse(await readFile(join(payload, descriptorPath), "utf8"));
if (
descriptor.schemaVersion !== "nodedc.device-plane.device-edge-core-channel-bootstrap.v1"
|| descriptor.transitionId !== patchId
|| descriptor.action !== "activate"
|| descriptor.service !== "device-control-core"
|| descriptor.composeActivation !== "dedicated-additive-override"
|| descriptor.identityRecovery !== "exact-invalid-unexported-failed-predecessor-only"
|| descriptor.tlsPurpose !== "clientAuth"
|| descriptor.direction !== "core-initiated"
|| descriptor.publicIngress !== "none-on-synology"
|| descriptor.commandTransport !== "disabled"
|| descriptor.gelios !== "untouched"
) {
throw new Error("device_edge_core_channel_bootstrap_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");
await mkdir(artifactDir, { recursive: true });
const tar = spawnSync("python3", ["-c", canonicalTarScript(), target, stage], {
encoding: "utf8",
maxBuffer: 128 * 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,
services: ["device-control-core"],
preserved: ["device-manager", "device-gateway", "device-postgres", "device-backhaul-target", "Gelios"],
}, null, 2));
} finally {
await rm(stage, { recursive: true, force: true });
}
async function copySafe(source, destination, sourceBoundary) {
const sourceStat = await lstat(source);
if (sourceStat.isSymbolicLink()) throw new Error(`source_symlink_rejected:${relative(sourceBoundary, source)}`);
if (sourceStat.isFile()) {
if (source.endsWith(".test.mjs") || source.endsWith(".map")) return;
await mkdir(dirname(destination), { recursive: true });
await cp(source, destination, { force: true, verbatimSymlinks: true });
return;
}
if (!sourceStat.isDirectory()) throw new Error(`source_type_rejected:${source}`);
await mkdir(destination, { recursive: true });
for (const entry of await readdir(source, { withFileTypes: true })) {
if ([".DS_Store", ".git", "node_modules", "test"].includes(entry.name) || entry.name.startsWith(".env")) continue;
await copySafe(join(source, entry.name), join(destination, entry.name), sourceBoundary);
}
}
async function validateDockerCopySources(dockerfilePath, buildContext) {
const dockerfile = await readFile(dockerfilePath, "utf8");
for (const [index, rawLine] of dockerfile.split("\n").entries()) {
const line = rawLine.trim();
if (!/^COPY\s+/i.test(line)) continue;
if (line.endsWith("\\") || /^COPY\s+\[/i.test(line)) {
throw new Error(`unsupported_docker_copy_syntax:${dockerfilePath}:${index + 1}`);
}
const tokens = line.split(/\s+/).slice(1);
while (tokens[0]?.startsWith("--")) tokens.shift();
if (tokens.length < 2) throw new Error(`invalid_docker_copy:${dockerfilePath}:${index + 1}`);
for (const source of tokens.slice(0, -1)) {
if (/[*?[\]{}]/.test(source)) throw new Error(`docker_copy_glob_rejected:${dockerfilePath}:${index + 1}:${source}`);
const resolvedSource = resolve(buildContext, source);
const relativeSource = relative(buildContext, resolvedSource);
if (!relativeSource || relativeSource.startsWith("..") || resolve(buildContext, relativeSource) !== resolvedSource) {
throw new Error(`docker_copy_source_outside_context:${dockerfilePath}:${index + 1}:${source}`);
}
try {
await lstat(resolvedSource);
} catch (error) {
if (error?.code === "ENOENT") throw new Error(`docker_copy_source_missing:${dockerfilePath}:${index + 1}:${source}`);
throw error;
}
}
}
}
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");
}
@@ -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-016", ...extra] = process.argv.slice(2);
const [patchId = "device-manager-release-20260811-018", ...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 = [
@@ -27,7 +27,7 @@ const entries = [
"services/device-gateway/package.json",
"services/device-edge-relay/package.json",
"services/device-manager",
"deployment/device-manager-release-v2.json",
"deployment/device-manager-release-v1.json",
];
const stage = await mkdtemp(join(tmpdir(), "nodedc-device-manager-control-plane-"));
const payload = join(stage, "payload");
@@ -41,7 +41,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-v2.json") {
if (entry === "deployment/device-manager-release-v1.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");
@@ -100,20 +100,15 @@ try {
"DEVICE_MANAGEMENT_API_ENABLED: \"true\"",
"NODEDC_LAUNCHER_INTERNAL_TOKEN_FILE: /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 (!compose.includes(required)) throw new Error(`device_manager_compose_contract_missing:${required}`);
for (const forbidden of [
"NODEDC_INTERNAL_ACCESS_TOKEN:",
"NODEDC_PLATFORM_SERVICE_TOKEN:",
"PRIVATE KEY",
"DEVICE_EDGE_CHANNEL_",
"device-edge-channel/",
"nodedc-device-plane-egress",
"0.0.0.0:18122",
"0.0.0.0:9921:9921",
"- \"9921:9921\"",
@@ -121,12 +116,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-v2.json"),
join(payload, "deployment/device-manager-release-v1.json"),
"utf8",
));
const predecessor = descriptor.predecessor;
if (
descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v2"
descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v1"
|| descriptor.releaseId !== patchId
|| !["activate", "upgrade"].includes(descriptor.action)
|| !predecessor
@@ -134,9 +129,6 @@ try {
|| !/^[A-Za-z0-9._-]{1,96}$/.test(predecessor.patchId || "")
|| !/^[a-f0-9]{64}$/.test(predecessor.artifactSha256 || "")
|| (descriptor.action === "activate") !== (predecessor.kind === "reconciliation")
|| descriptor.edgeChannel !== "core-initiated-pinned-mtls-enabled-zero-or-more-registered-edges"
|| descriptor.edgeChannelIdentity !== "runner-managed-host-local-private-key-public-certificate-export"
|| descriptor.edgeChannelEgress !== "dedicated-core-only-bridge-no-host-ingress-public-ipv4-tcp-8443-registration-policy"
|| descriptor.healthGate !== "bounded-container-grace+core-contract"
|| descriptor.rollback !== "restore-preapply-snapshot"
) throw new Error("device_manager_activation_successor_contract_mismatch");
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
import importlib.machinery
import importlib.util
import os
import shutil
import tempfile
import unittest
from pathlib import Path
from unittest import mock
SCRIPT_DIR = Path(__file__).resolve().parent
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
def load_runner():
loader = importlib.machinery.SourceFileLoader(
"nodedc_device_edge_core_channel_deploy_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 DeviceEdgeCoreChannelBootstrapTest(unittest.TestCase):
def test_identity_generation_ignores_synology_global_ca_extensions(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-device-edge-openssl-",
) as directory:
root = Path(directory)
malicious = root / "synology-openssl.cnf"
malicious.write_text(
"""[ req ]
prompt = no
distinguished_name = dn
x509_extensions = v3_ca
[ dn ]
CN = synology-global-default
[ v3_ca ]
basicConstraints = critical,CA:TRUE
keyUsage = critical,keyCertSign,cRLSign
""",
encoding="ascii",
)
private_key = root / "core-private-key.pem"
certificate = root / "core-certificate.pem"
with (
mock.patch.dict(
os.environ,
{"OPENSSL_CONF": str(malicious)},
),
mock.patch.object(
RUNNER,
"resolve_openssl_binary",
return_value=Path(shutil.which("openssl")),
),
):
RUNNER.generate_device_edge_channel_core_identity(
private_key,
certificate,
)
self.assertEqual(
RUNNER.validate_device_edge_channel_certificate_extensions(
certificate
),
"exact-clientAuth",
)
text = RUNNER.device_edge_channel_certificate_text(certificate)
self.assertEqual(
text.count("X509v3 Basic Constraints: critical"),
1,
)
self.assertIn("CA:FALSE", text)
self.assertNotIn("CA:TRUE", text)
RUNNER.run_openssl(
[
"verify",
"-purpose",
"sslclient",
"-CAfile",
str(certificate),
str(certificate),
],
"unit Device Edge client certificate",
)
def test_bootstrap_acceptance_is_core_only_and_preserves_manager(self):
entries = RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES
services = ("device-control-core",)
with (
mock.patch.object(
RUNNER,
"healthcheck_compose_service_with_grace",
) as service_health,
mock.patch.object(RUNNER, "healthcheck_url") as url_health,
mock.patch.object(
RUNNER,
"validate_device_manager_control_plane_runtime",
) as runtime_acceptance,
):
RUNNER.run_healthchecks("device-plane", entries, services)
self.assertEqual(
[call.args for call in service_health.call_args_list],
[
("device-plane", "device-control-core"),
("device-plane", "device-manager"),
("device-plane", "device-gateway"),
("device-plane", "device-postgres"),
],
)
url_health.assert_called_once_with(
RUNNER.component_healthchecks(
"device-plane",
entries,
services,
)[0]
)
runtime_acceptance.assert_called_once_with(
require_edge_channel=True
)
def test_exact_failed_016_recovery_is_required_for_replacement(self):
with (
mock.patch.object(
RUNNER,
"device_edge_channel_invalid_identity_is_exact_recoverable",
return_value=False,
),
):
with self.assertRaisesRegex(
RUNNER.DeployError,
"does not match the exact unexported failed-016",
):
RUNNER.recover_invalid_device_edge_channel_core_identity()
if __name__ == "__main__":
unittest.main()
@@ -185,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_RELEASE_V2_ENTRIES,
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES,
)
self.assertEqual(manifest["component"], "device-plane")
self.assertEqual(
@@ -242,19 +242,53 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
compose = (
PLATFORM_ROOT / "device-plane/docker-compose.device-manager.yml"
).read_text(encoding="utf-8")
for required in (
'DEVICE_EDGE_CHANNEL_ENABLED: "true"',
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE: ",
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE: ",
"DEVICE_EDGE_CHANNEL_TRUST_ROOT: ",
for forbidden in (
"DEVICE_EDGE_CHANNEL_",
"device-edge-channel/",
"name: nodedc-device-plane-egress",
):
self.assertIn(required, compose)
self.assertNotIn(forbidden, compose)
self.assertNotIn("PRIVATE KEY", compose)
checks = RUNNER.component_healthchecks("device-plane", entries, tuple(result["services"]))
self.assertEqual(checks[0]["expected_json"]["managementApi"], "enabled")
self.assertEqual(checks[0]["expected_json"]["discoveryIngest"], "enabled")
def test_edge_core_channel_bootstrap_is_core_only_and_secret_free(self):
manifest, entries, names, result = self.assert_deterministic_artifact(
"build-device-edge-core-channel-bootstrap-artifact.mjs",
"device-edge-core-channel-bootstrap-unit-001",
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES,
)
self.assertEqual(manifest["component"], "device-plane")
self.assertEqual(
RUNNER.component_services("device-plane", entries),
("device-control-core",),
)
builds = RUNNER.component_builds("device-plane", entries)
self.assertEqual(len(builds), 1)
self.assertIn(RUNNER.DEVICE_PLANE_CONTROL_CORE_IMAGE, builds[0][1])
self.assertEqual(result["services"], ["device-control-core"])
self.assertFalse(any(
name.startswith("payload/services/device-manager/")
or name.startswith("payload/services/device-gateway/")
for name in names
))
self.assertIn(
"payload/packages/device-edge-channel-contract/src/index.mjs",
names,
)
self.assertIn(
"payload/docker-compose.device-edge-core-channel.yml",
names,
)
checks = RUNNER.component_healthchecks(
"device-plane",
entries,
tuple(result["services"]),
)
self.assertEqual(checks[0]["expected_json"]["managementApi"], "enabled")
self.assertEqual(checks[0]["expected_json"]["commandTransport"], "disabled")
def test_release_v2_keeps_release_v1_predecessor_contract_immutable(self):
predecessor = device_manager_release_v1_descriptor(
release_id="device-manager-release-20260811-010",
@@ -417,7 +451,7 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
):
RUNNER.prepare_component_runtime(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES,
)
self.assertEqual(
[call.args[0] for call in ensure.call_args_list],
@@ -429,10 +463,37 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
RUNNER.PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE,
],
)
ensure_edge_identity.assert_called_once_with()
ensure_edge_identity.assert_not_called()
with (
mock.patch.object(
RUNNER,
"ensure_platform_runtime_secret",
) as ensure,
mock.patch.object(
RUNNER,
"ensure_device_edge_channel_core_identity",
) as ensure_edge_identity,
):
RUNNER.prepare_component_runtime(
"device-plane",
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES,
)
self.assertEqual(
[call.args[0] for call in ensure.call_args_list],
[
RUNNER.DEVICE_PLANE_POSTGRES_PASSWORD_FILE,
RUNNER.DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE,
RUNNER.DEVICE_PLANE_IDENTIFIER_PEPPER_FILE,
RUNNER.DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE,
],
)
ensure_edge_identity.assert_called_once_with(
allow_invalid_unexported_recovery=True
)
def test_apply_gate_checks_exact_services_core_contract_and_runtime_boundary(self):
entries = RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES
entries = RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES
services = ("device-control-core", "device-manager")
with (
mock.patch.object(