fix(device-edge): route Core through dedicated egress

This commit is contained in:
Codex
2026-08-12 10:41:51 +03:00
parent 9fa74a1243
commit 20ef5894cc
5 changed files with 741 additions and 29 deletions
@@ -15,19 +15,25 @@ 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 upgradeV2 = patchId.startsWith("device-edge-core-channel-upgrade-v2-");
const upgradeV1 = !upgradeV2 && patchId.startsWith("device-edge-core-channel-upgrade-");
const upgrade = upgradeV1 || upgradeV2;
const descriptorPath = upgradeV2
? "deployment/device-edge-core-channel-upgrade-v2.json"
const upgradeV3 = patchId.startsWith("device-edge-core-channel-upgrade-v3-");
const upgradeV2 = !upgradeV3 && patchId.startsWith("device-edge-core-channel-upgrade-v2-");
const upgradeV1 = !upgradeV3 && !upgradeV2 && patchId.startsWith("device-edge-core-channel-upgrade-");
const upgrade = upgradeV1 || upgradeV2 || upgradeV3;
const descriptorPath = upgradeV3
? "deployment/device-edge-core-channel-upgrade-v3.json"
: upgradeV2
? "deployment/device-edge-core-channel-upgrade-v2.json"
: upgradeV1
? "deployment/device-edge-core-channel-upgrade-v1.json"
: "deployment/device-edge-core-channel-bootstrap-v1.json";
const composePath = upgradeV3
? "docker-compose.device-plane.yml"
: "docker-compose.device-edge-core-channel.yml";
const entries = [
".dockerignore",
"package.json",
"package-lock.json",
"docker-compose.device-edge-core-channel.yml",
composePath,
"packages/device-protocol-contract",
"packages/device-edge-channel-contract",
"services/device-control-core",
@@ -73,39 +79,79 @@ try {
}
}
const compose = await readFile(join(payload, "docker-compose.device-edge-core-channel.yml"), "utf8");
for (const required of [
const compose = await readFile(join(payload, composePath), "utf8");
const commonComposeRequired = [
"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",
]) {
];
const composeRequired = upgradeV3
? [
...commonComposeRequired,
" - device-plane-private",
" - device-plane-control",
]
: [
...commonComposeRequired,
"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 (const required of composeRequired) {
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 (upgradeV3) {
const coreBlock = compose.split("\n device-control-core:", 2)[1]?.split("\n device-gateway:", 1)[0] || "";
const gatewayBlock = compose.split("\n device-gateway:", 2)[1]?.split("\nnetworks:", 1)[0] || "";
if (
!coreBlock.includes(" - device-plane-private")
|| coreBlock.includes(" - device-plane-control")
|| !gatewayBlock.includes(" - device-plane-private")
|| !gatewayBlock.includes(" - device-plane-control")
) {
throw new Error("device_edge_core_channel_v3_network_boundary_mismatch");
}
}
const composeForbidden = upgradeV3
? ["gw_priority:", "network_mode:", "privileged:"]
: [
"device-manager:",
"device-gateway:",
"device-postgres:",
"PRIVATE KEY",
"ports:",
"network_mode:",
"privileged:",
];
for (const forbidden of composeForbidden) {
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"));
const descriptorContractMatches = upgradeV2
const descriptorContractMatches = upgradeV3
? (
descriptor.schemaVersion === "nodedc.device-plane.device-edge-core-channel-upgrade.v3"
&& descriptor.action === "upgrade"
&& descriptor.composeActivation === "replace-core-network-membership-with-private-plus-egress"
&& descriptor.identityRecovery === "forbidden-valid-existing-identity-required"
&& descriptor.endpointPolicy === "public-ipv4-standard-https-tcp-443-only"
&& descriptor.upgradePredecessor?.patchId === "device-edge-core-channel-upgrade-v2-20260812-021"
&& descriptor.upgradePredecessor?.artifactSha256 === "e40a6fd24edfecac09e42cd82635a77850541bcf047788db3e9c55d2b9e58867"
&& JSON.stringify(descriptor.coreNetworks) === JSON.stringify(["device-plane-private", "device-plane-egress"])
&& descriptor.removedCoreNetwork === "device-plane-control"
&& descriptor.composeCompatibility === "synology-compose-v2.20-no-gw-priority"
&& descriptor.edgeRegistrations === "preserved"
&& descriptor.rollback === "restore-upgrade-v2-021-source-and-preapply-core-runtime"
)
: upgradeV2
? (
descriptor.schemaVersion === "nodedc.device-plane.device-edge-core-channel-upgrade.v2"
&& descriptor.action === "upgrade"
&& descriptor.composeActivation === "preserve-dedicated-additive-override"
+312 -1
View File
@@ -256,12 +256,21 @@ DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL = (
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL = (
"deployment/device-edge-core-channel-upgrade-v2.json"
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_REL = (
"deployment/device-edge-core-channel-upgrade-v3.json"
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL = (
"docker-compose.device-edge-core-channel.yml"
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_SHA256 = (
"cac1e06b21202d8d96f5694b1adb1e67c0a3cdd31fcfca56cb7e19839c8516d8"
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_PREDECESSOR_BASE_COMPOSE_SHA256 = (
"eb1018cc0ffeaa0c019944d8810e2daa01eeca4c06289efff175785fb16457e7"
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_BASE_COMPOSE_SHA256 = (
"6a4c08313cc97bbfd86c4e133c6b2e55ba18a499a3bb6f267c7059754f83e8cf"
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES = (
".dockerignore",
"package.json",
@@ -292,6 +301,16 @@ DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES = (
"services/device-control-core",
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL,
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_ENTRIES = (
".dockerignore",
"package.json",
"package-lock.json",
"docker-compose.device-plane.yml",
"packages/device-protocol-contract",
"packages/device-edge-channel-contract",
"services/device-control-core",
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_REL,
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID = (
"device-edge-core-channel-bootstrap-20260812-018"
)
@@ -304,6 +323,12 @@ DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_PATCH_ID = (
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_ARTIFACT_SHA256 = (
"8e9a220275959f378c1c4b00be5c7192e79afe2134eaab808a64e515870a8438"
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_PREDECESSOR_PATCH_ID = (
"device-edge-core-channel-upgrade-v2-20260812-021"
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_PREDECESSOR_ARTIFACT_SHA256 = (
"e40a6fd24edfecac09e42cd82635a77850541bcf047788db3e9c55d2b9e58867"
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_PATCH_ID = (
"device-manager-release-20260811-010"
)
@@ -3835,6 +3860,7 @@ def allowed_payload_path(component, rel):
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_REL,
DEVICE_PLANE_MANAGER_RECONCILIATION_REL,
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL,
"packages/device-protocol-contract",
@@ -8891,7 +8917,15 @@ def load_artifact(artifact, work_dir):
manifest["component"],
entries,
):
if is_device_plane_edge_core_channel_upgrade_v2_slice(
if is_device_plane_edge_core_channel_upgrade_v3_slice(
manifest["component"],
entries,
):
validate_device_plane_edge_core_channel_upgrade_v3_payload(
payload_dir,
expected_transition_id=manifest["id"],
)
elif is_device_plane_edge_core_channel_upgrade_v2_slice(
manifest["component"],
entries,
):
@@ -9404,6 +9438,7 @@ def is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_ENTRIES,
)
)
@@ -9415,6 +9450,7 @@ def is_device_plane_edge_core_channel_upgrade_slice(component, entries):
and tuple(entries) in (
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_ENTRIES,
)
)
@@ -9427,6 +9463,14 @@ def is_device_plane_edge_core_channel_upgrade_v2_slice(component, entries):
)
def is_device_plane_edge_core_channel_upgrade_v3_slice(component, entries):
return (
component == "device-plane"
and entries is not None
and tuple(entries) == DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_ENTRIES
)
def is_device_plane_manager_v2_control_plane_slice(component, entries):
return (
component == "device-plane"
@@ -9661,6 +9705,58 @@ def expected_device_plane_edge_core_channel_upgrade_v2_descriptor(
}
def expected_device_plane_edge_core_channel_upgrade_v3_descriptor(
transition_id,
):
return {
"schemaVersion": (
"nodedc.device-plane.device-edge-core-channel-upgrade.v3"
),
"transitionId": transition_id,
"action": "upgrade",
"upgradePredecessor": {
"patchId": (
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_PREDECESSOR_PATCH_ID
),
"artifactSha256": (
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_PREDECESSOR_ARTIFACT_SHA256
),
},
"service": "device-control-core",
"composeActivation": (
"replace-core-network-membership-with-private-plus-egress"
),
"identity": (
"reuse-existing-runner-managed-host-local-private-key-"
"public-certificate-export"
),
"identityRecovery": "forbidden-valid-existing-identity-required",
"tlsPurpose": "clientAuth",
"direction": "core-initiated",
"endpointPolicy": "public-ipv4-standard-https-tcp-443-only",
"coreNetworks": [
"device-plane-private",
"device-plane-egress",
],
"removedCoreNetwork": "device-plane-control",
"composeCompatibility": "synology-compose-v2.20-no-gw-priority",
"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-upgrade-v2-021-source-and-preapply-core-runtime"
),
}
def expected_device_plane_manager_failed_control_plane_descriptor():
return {
"schemaVersion": (
@@ -10048,6 +10144,36 @@ def validate_device_plane_edge_core_channel_upgrade_v2_payload(
return descriptor
def validate_device_plane_edge_core_channel_upgrade_v3_payload(
payload_dir,
*,
expected_transition_id=None,
):
descriptor = read_strict_json(
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_REL,
"Device Edge Core channel upgrade v3 descriptor",
max_bytes=16 * 1024,
)
transition_id = descriptor.get("transitionId")
if (
not isinstance(transition_id, str)
or not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", transition_id)
or (
expected_transition_id is not None
and transition_id != expected_transition_id
)
or descriptor
!= expected_device_plane_edge_core_channel_upgrade_v3_descriptor(
transition_id
)
):
die("Device Edge Core channel upgrade v3 descriptor mismatch")
validate_device_plane_edge_core_channel_upgrade_v3_base_compose(
payload_dir
)
return descriptor
def validate_device_plane_edge_core_channel_compose(payload_dir):
compose = payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL
if sha256_file(compose) != DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_SHA256:
@@ -10087,6 +10213,39 @@ def validate_device_plane_edge_core_channel_compose(payload_dir):
return compose
def validate_device_plane_edge_core_channel_upgrade_v3_base_compose(
payload_dir,
):
compose = payload_dir / "docker-compose.device-plane.yml"
if (
sha256_file(compose)
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_BASE_COMPOSE_SHA256
):
die("Device Edge Core channel v3 base Compose mismatch")
compose_text = compose.read_text(encoding="utf-8")
try:
core_block = compose_text.split(
"\n device-control-core:", 1
)[1].split("\n device-gateway:", 1)[0]
gateway_block = compose_text.split(
"\n device-gateway:", 1
)[1].split("\nnetworks:", 1)[0]
except IndexError:
die("Device Edge Core channel v3 service topology is incomplete")
if (
" - device-plane-private" not in core_block
or " - device-plane-control" in core_block
or " - device-plane-private" not in gateway_block
or " - device-plane-control" not in gateway_block
or "gw_priority:" in compose_text
or "network_mode:" in compose_text
or "privileged:" in compose_text
):
die("Device Edge Core channel v3 network boundary mismatch")
return compose
return compose
def installed_device_plane_manager_compose_sha256():
root = DEVICE_PLANE_ROOT
v1 = root / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
@@ -11777,6 +11936,12 @@ def inspect_device_edge_channel_core_identity_state():
def validate_device_plane_edge_core_channel_bootstrap_predecessor(
payload_dir,
):
if (
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_REL
).exists():
return validate_device_plane_edge_core_channel_upgrade_v3_predecessor(
payload_dir
)
if (
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL
).exists():
@@ -12075,6 +12240,96 @@ def validate_device_plane_edge_core_channel_upgrade_v2_predecessor(
}
def validate_device_plane_edge_core_channel_upgrade_v3_predecessor(
payload_dir,
):
descriptor = validate_device_plane_edge_core_channel_upgrade_v3_payload(
payload_dir
)
installed_upgrade = (
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_REL
)
if installed_upgrade.exists() or installed_upgrade.is_symlink():
die("Device Edge Core channel upgrade v3 is already installed")
predecessor = descriptor["upgradePredecessor"]
artifact_name = f"nodedc-device-plane-{predecessor['patchId']}.tgz"
artifact = APPLIED_DIR / artifact_name
if (
not artifact.is_file()
or artifact.is_symlink()
or sha256_file(artifact) != predecessor["artifactSha256"]
):
die("Device Edge Core channel upgrade v3 predecessor mismatch")
records = [
row for row in load_state(STATE_FILE)
if row.get("id") == predecessor["patchId"]
and row.get("sha256") == predecessor["artifactSha256"]
]
if (
len(records) != 1
or records[0].get("status") != "ok"
or records[0].get("component") != "device-plane"
or records[0].get("artifact") != artifact_name
):
die("Device Edge Core channel upgrade v3 predecessor journal mismatch")
with tempfile.TemporaryDirectory(
prefix="device-edge-core-channel-upgrade-v3-predecessor-",
dir=TMP_DIR,
) as directory:
manifest, entries, predecessor_payload = load_artifact(
artifact,
Path(directory),
)
if (
manifest.get("id") != predecessor["patchId"]
or manifest.get("component") != "device-plane"
or manifest.get("type") != "app-overlay"
or tuple(entries)
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES
):
die("Device Edge Core channel upgrade v3 predecessor type mismatch")
validate_device_plane_edge_core_channel_upgrade_v2_payload(
predecessor_payload,
expected_transition_id=predecessor["patchId"],
)
expected_source = collect_exact_files(
predecessor_payload,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
"Device Edge Core channel upgrade v3 predecessor source",
)
actual_source = collect_exact_files(
DEVICE_PLANE_ROOT,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
"installed Device Edge Core channel upgrade v2 source",
)
if actual_source != expected_source:
die("installed Device Edge Core channel upgrade v2 source drift detected")
installed_base_compose = DEVICE_PLANE_ROOT / "docker-compose.device-plane.yml"
if (
installed_base_compose.is_symlink()
or not installed_base_compose.is_file()
or sha256_file(installed_base_compose)
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_PREDECESSOR_BASE_COMPOSE_SHA256
):
die("installed Device Edge Core channel v3 base predecessor drift detected")
identity_state = inspect_device_edge_channel_core_identity_state()
if identity_state != "valid-reuse-at-apply":
die("Device Edge Core channel upgrade v3 requires the valid active identity")
for service in (
"device-control-core",
"device-manager",
"device-gateway",
"device-postgres",
):
healthcheck_compose_service("device-plane", service)
return {
"mode": "edge-core-channel-private-plus-egress-upgrade-v3",
"descriptor": descriptor,
"identityState": identity_state,
"upgradeArtifact": artifact,
}
def validate_device_plane_manager_v2_reconciliation_backup():
backup_dir = (
BACKUPS_DIR / DEVICE_PLANE_MANAGER_V2_RECONCILIATION_BACKUP_ID
@@ -15883,6 +16138,9 @@ def component_compose_files(
edge_upgrade_v2_descriptor = (
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL
)
edge_upgrade_v3_descriptor = (
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_REL
)
edge_override = (
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL
)
@@ -15897,6 +16155,10 @@ def component_compose_files(
edge_upgrade_v2_descriptor.exists()
or edge_upgrade_v2_descriptor.is_symlink()
)
edge_upgrade_v3_descriptor_exists = (
edge_upgrade_v3_descriptor.exists()
or edge_upgrade_v3_descriptor.is_symlink()
)
edge_override_exists = edge_override.exists() or edge_override.is_symlink()
if (
edge_override_exists
@@ -15904,6 +16166,7 @@ def component_compose_files(
edge_descriptor_exists
or edge_upgrade_descriptor_exists
or edge_upgrade_v2_descriptor_exists
or edge_upgrade_v3_descriptor_exists
)
or edge_upgrade_descriptor_exists
and not edge_descriptor_exists
@@ -15911,6 +16174,12 @@ def component_compose_files(
and not (
edge_descriptor_exists and edge_upgrade_descriptor_exists
)
or edge_upgrade_v3_descriptor_exists
and not (
edge_descriptor_exists
and edge_upgrade_descriptor_exists
and edge_upgrade_v2_descriptor_exists
)
):
die("installed Device Edge Core channel source is incomplete")
if edge_descriptor_exists:
@@ -15988,6 +16257,35 @@ def component_compose_files(
)
):
die("installed Device Edge Core channel upgrade v2 descriptor drift detected")
if edge_upgrade_v3_descriptor_exists:
if (
upgrade_v2_id
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_PREDECESSOR_PATCH_ID
or edge_upgrade_v3_descriptor.is_symlink()
or not edge_upgrade_v3_descriptor.is_file()
or sha256_file(
DEVICE_PLANE_ROOT / "docker-compose.device-plane.yml"
)
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_BASE_COMPOSE_SHA256
):
die("installed Device Edge Core channel upgrade v3 predecessor drift detected")
upgrade_v3 = read_strict_json(
edge_upgrade_v3_descriptor,
"installed Device Edge Core channel upgrade v3 descriptor",
max_bytes=16 * 1024,
)
upgrade_v3_id = upgrade_v3.get("transitionId")
if (
not isinstance(upgrade_v3_id, str)
or not re.fullmatch(
r"[A-Za-z0-9._-]{1,96}", upgrade_v3_id
)
or upgrade_v3
!= expected_device_plane_edge_core_channel_upgrade_v3_descriptor(
upgrade_v3_id
)
):
die("installed Device Edge Core channel upgrade v3 descriptor drift detected")
files = (*files, edge_override)
overlay = DEVICE_PLANE_ROOT / DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL
if overlay.exists() or overlay.is_symlink():
@@ -19983,6 +20281,19 @@ def plan_artifact(artifact):
f"{predecessor['artifactSha256']}"
)
print("device_edge_channel_endpoint_policy=public-ipv4-standard-https-tcp-443-only")
if "coreNetworks" in edge_descriptor:
print(
"device_edge_channel_core_networks="
f"{','.join(edge_descriptor['coreNetworks'])}"
)
print(
"device_edge_channel_removed_core_network="
f"{edge_descriptor['removedCoreNetwork']}"
)
print(
"device_edge_channel_compose_compatibility="
f"{edge_descriptor['composeCompatibility']}"
)
else:
print(
"device_plane_predecessor_patch="
@@ -364,6 +364,89 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
)
)
def test_edge_core_channel_upgrade_v3_is_core_only_and_pins_upgrade_021(self):
patch_id = "device-edge-core-channel-upgrade-v3-unit-001"
manifest, entries, names, result = self.assert_deterministic_artifact(
"build-device-edge-core-channel-bootstrap-artifact.mjs",
patch_id,
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_ENTRIES,
)
self.assertEqual(manifest["component"], "device-plane")
self.assertEqual(
RUNNER.component_services("device-plane", entries),
("device-control-core",),
)
self.assertEqual(len(RUNNER.component_builds("device-plane", entries)), 1)
self.assertEqual(result["services"], ["device-control-core"])
self.assertIn(
"payload/deployment/device-edge-core-channel-upgrade-v3.json",
names,
)
self.assertIn(
"payload/docker-compose.device-plane.yml",
names,
)
descriptor = (
RUNNER.expected_device_plane_edge_core_channel_upgrade_v3_descriptor(
patch_id
)
)
self.assertEqual(
descriptor["upgradePredecessor"]["patchId"],
"device-edge-core-channel-upgrade-v2-20260812-021",
)
self.assertEqual(
descriptor["upgradePredecessor"]["artifactSha256"],
"e40a6fd24edfecac09e42cd82635a77850541bcf047788db3e9c55d2b9e58867",
)
self.assertEqual(
descriptor["coreNetworks"],
["device-plane-private", "device-plane-egress"],
)
self.assertEqual(
descriptor["removedCoreNetwork"],
"device-plane-control",
)
self.assertEqual(
descriptor["composeCompatibility"],
"synology-compose-v2.20-no-gw-priority",
)
self.assertTrue(
RUNNER.is_device_plane_edge_core_channel_upgrade_v3_slice(
"device-plane",
entries,
)
)
def test_edge_core_channel_upgrade_v3_rejects_installed_marker(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-device-edge-upgrade-v3-installed-",
) as directory:
root = Path(directory)
marker = root / RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_REL
marker.parent.mkdir(parents=True)
marker.write_text("{}\n", encoding="utf-8")
descriptor = (
RUNNER.expected_device_plane_edge_core_channel_upgrade_v3_descriptor(
"device-edge-core-channel-upgrade-v3-unit-002"
)
)
with (
mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", root),
mock.patch.object(
RUNNER,
"validate_device_plane_edge_core_channel_upgrade_v3_payload",
return_value=descriptor,
),
):
with self.assertRaisesRegex(
RUNNER.DeployError,
"upgrade v3 is already installed",
):
RUNNER.validate_device_plane_edge_core_channel_upgrade_v3_predecessor(
root / "payload"
)
def test_edge_core_channel_upgrade_v2_rejects_installed_marker(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-device-edge-upgrade-v2-installed-",
@@ -508,6 +591,120 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
],
)
def test_edge_core_channel_upgrade_v3_accepts_exact_applied_021(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-device-edge-upgrade-v3-predecessor-",
) as directory:
root = Path(directory)
applied = root / "applied"
temporary = root / "tmp"
device_plane = root / "device-plane"
for path in (applied, temporary, device_plane / "deployment"):
path.mkdir(parents=True)
predecessor_patch = (
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_PREDECESSOR_PATCH_ID
)
predecessor_sha = (
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_PREDECESSOR_ARTIFACT_SHA256
)
artifact_name = f"nodedc-device-plane-{predecessor_patch}.tgz"
artifact = applied / artifact_name
artifact.write_bytes(b"reviewed-upgrade-021")
installed_base = device_plane / "docker-compose.device-plane.yml"
installed_base.write_text("services: {}\n", encoding="utf-8")
state_file = root / "applied.jsonl"
state_file.write_text(
json.dumps({
"id": predecessor_patch,
"artifact": artifact_name,
"component": "device-plane",
"sha256": predecessor_sha,
"status": "ok",
}) + "\n",
encoding="utf-8",
)
descriptor = (
RUNNER.expected_device_plane_edge_core_channel_upgrade_v3_descriptor(
"device-edge-core-channel-upgrade-v3-unit-003"
)
)
expected_source = {"source": "upgrade-021"}
with (
mock.patch.object(RUNNER, "APPLIED_DIR", applied),
mock.patch.object(RUNNER, "TMP_DIR", temporary),
mock.patch.object(RUNNER, "STATE_FILE", state_file),
mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", device_plane),
mock.patch.object(
RUNNER,
"validate_device_plane_edge_core_channel_upgrade_v3_payload",
return_value=descriptor,
),
mock.patch.object(
RUNNER,
"sha256_file",
side_effect=lambda path: (
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_PREDECESSOR_BASE_COMPOSE_SHA256
if Path(path) == installed_base
else predecessor_sha
),
),
mock.patch.object(
RUNNER,
"load_artifact",
return_value=(
{
"id": predecessor_patch,
"component": "device-plane",
"type": "app-overlay",
},
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
root / "predecessor-payload",
),
),
mock.patch.object(
RUNNER,
"validate_device_plane_edge_core_channel_upgrade_v2_payload",
) as validate_predecessor,
mock.patch.object(
RUNNER,
"collect_exact_files",
side_effect=(expected_source, expected_source),
),
mock.patch.object(
RUNNER,
"inspect_device_edge_channel_core_identity_state",
return_value="valid-reuse-at-apply",
),
mock.patch.object(
RUNNER,
"healthcheck_compose_service",
) as health,
):
result = (
RUNNER.validate_device_plane_edge_core_channel_upgrade_v3_predecessor(
root / "payload"
)
)
self.assertEqual(
result["mode"],
"edge-core-channel-private-plus-egress-upgrade-v3",
)
self.assertEqual(result["upgradeArtifact"], artifact)
validate_predecessor.assert_called_once_with(
root / "predecessor-payload",
expected_transition_id=predecessor_patch,
)
self.assertEqual(
[call.args for call in health.call_args_list],
[
("device-plane", "device-control-core"),
("device-plane", "device-manager"),
("device-plane", "device-gateway"),
("device-plane", "device-postgres"),
],
)
def test_release_v2_keeps_release_v1_predecessor_contract_immutable(self):
predecessor = device_manager_release_v1_descriptor(
release_id="device-manager-release-20260811-010",
@@ -747,6 +944,24 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
allow_invalid_unexported_recovery=False
)
with (
mock.patch.object(
RUNNER,
"ensure_platform_runtime_secret",
),
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_UPGRADE_V3_ENTRIES,
)
ensure_edge_identity.assert_called_once_with(
allow_invalid_unexported_recovery=False
)
def test_apply_gate_checks_exact_services_core_contract_and_runtime_boundary(self):
entries = RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES
services = ("device-control-core", "device-manager")
@@ -813,6 +1028,40 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
)
runtime_acceptance.assert_called_once_with(require_edge_channel=True)
def test_upgrade_v3_apply_gate_checks_preserved_runtime_and_edge_contract(self):
entries = RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_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_activation_resolves_predecessor_from_descriptor_and_journal(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-device-manager-release-predecessor-",
@@ -1274,6 +1523,79 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
runtime_acceptance.assert_called_once_with(require_edge_channel=True)
self.assertEqual(result, f"source+runtime-restored:{len(entries)}")
def test_edge_upgrade_v3_rollback_restores_upgrade_021_core_runtime(self):
entries = RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_ENTRIES
missing = {
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V3_REL,
}
existing = [entry for entry in entries if entry not in missing]
with tempfile.TemporaryDirectory(
prefix="nodedc-device-edge-upgrade-v3-rollback-",
) as directory:
backup = Path(directory) / "backup"
backup.mkdir()
(backup / "existing-files.txt").write_text(
"\n".join(existing) + "\n",
encoding="utf-8",
)
(backup / "missing-files.txt").write_text(
"\n".join(sorted(missing)) + "\n",
encoding="utf-8",
)
(backup / "runtime-before.json").write_text(
json.dumps(healthy_device_plane_inventory(include_manager=True)),
encoding="utf-8",
)
with (
mock.patch.object(
RUNNER,
"stop_and_remove_compose_services",
) as stop,
mock.patch.object(
RUNNER,
"restore_platform_overlay",
return_value=len(entries),
),
mock.patch.object(
RUNNER,
"run_component_runtime",
) as restore_runtime,
mock.patch.object(
RUNNER,
"healthcheck_compose_service_with_grace",
) as restore_health,
mock.patch.object(
RUNNER,
"validate_device_manager_control_plane_runtime",
) as runtime_acceptance,
):
result = RUNNER.rollback_device_plane_apply(
Path(directory) / "live",
backup,
entries,
"test-stamp",
True,
("device-control-core",),
)
stop.assert_not_called()
restore_runtime.assert_called_once_with(
"device-plane",
existing,
("device-control-core",),
)
self.assertEqual(
[call.args for call in restore_health.call_args_list],
[
("device-plane", "device-control-core"),
("device-plane", "device-manager"),
("device-plane", "device-gateway"),
("device-plane", "device-postgres"),
],
)
runtime_acceptance.assert_called_once_with(require_edge_channel=True)
self.assertEqual(result, f"source+runtime-restored:{len(entries)}")
if __name__ == "__main__":
unittest.main(verbosity=2)