From d8c0e9f3b1431eccc2211d1d5a0cbfbe8dbd8373 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 12 Aug 2026 01:43:09 +0300 Subject: [PATCH] feat(deploy): add Core channel forward upgrade --- .../device-edge-core-channel-upgrade-v1.json | 28 ++ ...e-edge-core-channel-bootstrap-artifact.mjs | 26 +- infra/deploy-runner/nodedc-deploy | 331 ++++++++++++++++-- ..._device_manager_control_plane_artifacts.py | 47 +++ 4 files changed, 396 insertions(+), 36 deletions(-) create mode 100644 device-plane/deployment/device-edge-core-channel-upgrade-v1.json diff --git a/device-plane/deployment/device-edge-core-channel-upgrade-v1.json b/device-plane/deployment/device-edge-core-channel-upgrade-v1.json new file mode 100644 index 0000000..a013b86 --- /dev/null +++ b/device-plane/deployment/device-edge-core-channel-upgrade-v1.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": "nodedc.device-plane.device-edge-core-channel-upgrade.v1", + "transitionId": "__PATCH_ID__", + "action": "upgrade", + "bootstrapPredecessor": { + "patchId": "device-edge-core-channel-bootstrap-20260812-018", + "artifactSha256": "5598b7388b491fe524ab46038ce476482a93a6cf07d8ca5e00206c69ded02931" + }, + "service": "device-control-core", + "composeActivation": "preserve-dedicated-additive-override", + "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", + "publicIngress": "none-on-synology", + "edgeRegistrations": "preserved-requires-explicit-443-reconciliation", + "commandTransport": "disabled", + "gelios": "untouched", + "preservedServices": [ + "device-manager", + "device-gateway", + "device-postgres", + "device-backhaul-target" + ], + "healthGate": "bounded-container-grace+core-edge-contract", + "rollback": "restore-bootstrap-018-source-and-preapply-core-runtime" +} diff --git a/infra/deploy-runner/build-device-edge-core-channel-bootstrap-artifact.mjs b/infra/deploy-runner/build-device-edge-core-channel-bootstrap-artifact.mjs index 9b58028..89a239a 100644 --- a/infra/deploy-runner/build-device-edge-core-channel-bootstrap-artifact.mjs +++ b/infra/deploy-runner/build-device-edge-core-channel-bootstrap-artifact.mjs @@ -15,7 +15,10 @@ 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 upgrade = patchId.startsWith("device-edge-core-channel-upgrade-"); +const descriptorPath = upgrade + ? "deployment/device-edge-core-channel-upgrade-v1.json" + : "deployment/device-edge-core-channel-bootstrap-v1.json"; const entries = [ ".dockerignore", "package.json", @@ -97,13 +100,26 @@ try { } const descriptor = JSON.parse(await readFile(join(payload, descriptorPath), "utf8")); + const descriptorContractMatches = upgrade + ? ( + descriptor.schemaVersion === "nodedc.device-plane.device-edge-core-channel-upgrade.v1" + && descriptor.action === "upgrade" + && descriptor.composeActivation === "preserve-dedicated-additive-override" + && descriptor.identityRecovery === "forbidden-valid-existing-identity-required" + && descriptor.endpointPolicy === "public-ipv4-standard-https-tcp-443-only" + && descriptor.bootstrapPredecessor?.patchId === "device-edge-core-channel-bootstrap-20260812-018" + && descriptor.bootstrapPredecessor?.artifactSha256 === "5598b7388b491fe524ab46038ce476482a93a6cf07d8ca5e00206c69ded02931" + ) + : ( + descriptor.schemaVersion === "nodedc.device-plane.device-edge-core-channel-bootstrap.v1" + && descriptor.action === "activate" + && descriptor.composeActivation === "dedicated-additive-override" + && descriptor.identityRecovery === "exact-invalid-unexported-failed-predecessor-only" + ); if ( - descriptor.schemaVersion !== "nodedc.device-plane.device-edge-core-channel-bootstrap.v1" + !descriptorContractMatches || 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" diff --git a/infra/deploy-runner/nodedc-deploy b/infra/deploy-runner/nodedc-deploy index 060f095..5d95a56 100755 --- a/infra/deploy-runner/nodedc-deploy +++ b/infra/deploy-runner/nodedc-deploy @@ -250,6 +250,9 @@ DEVICE_PLANE_MANAGER_COMPOSE_REL = "docker-compose.device-manager.yml" DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL = ( "deployment/device-edge-core-channel-bootstrap-v1.json" ) +DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL = ( + "deployment/device-edge-core-channel-upgrade-v1.json" +) DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL = ( "docker-compose.device-edge-core-channel.yml" ) @@ -266,6 +269,22 @@ DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES = ( "services/device-control-core", DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL, ) +DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES = ( + ".dockerignore", + "package.json", + "package-lock.json", + DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL, + "packages/device-protocol-contract", + "packages/device-edge-channel-contract", + "services/device-control-core", + DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL, +) +DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID = ( + "device-edge-core-channel-bootstrap-20260812-018" +) +DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_ARTIFACT_SHA256 = ( + "5598b7388b491fe524ab46038ce476482a93a6cf07d8ca5e00206c69ded02931" +) DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_PATCH_ID = ( "device-manager-release-20260811-010" ) @@ -3795,6 +3814,7 @@ def allowed_payload_path(component, rel): DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL, DEVICE_PLANE_MANAGER_RELEASE_V2_REL, DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL, + DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL, DEVICE_PLANE_MANAGER_RECONCILIATION_REL, DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL, "packages/device-protocol-contract", @@ -8851,10 +8871,19 @@ def load_artifact(artifact, work_dir): manifest["component"], entries, ): - validate_device_plane_edge_core_channel_bootstrap_payload( - payload_dir, - expected_transition_id=manifest["id"], - ) + if is_device_plane_edge_core_channel_upgrade_slice( + manifest["component"], + entries, + ): + validate_device_plane_edge_core_channel_upgrade_payload( + payload_dir, + expected_transition_id=manifest["id"], + ) + else: + validate_device_plane_edge_core_channel_bootstrap_payload( + payload_dir, + expected_transition_id=manifest["id"], + ) if is_device_plane_manager_failed_control_plane_slice( manifest["component"], entries, @@ -9343,8 +9372,18 @@ def is_device_plane_edge_core_channel_bootstrap_slice(component, entries): return ( component == "device-plane" and entries is not None - and tuple(entries) - == DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES + and tuple(entries) in ( + DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES, + DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES, + ) + ) + + +def is_device_plane_edge_core_channel_upgrade_slice(component, entries): + return ( + component == "device-plane" + and entries is not None + and tuple(entries) == DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES ) @@ -9492,6 +9531,52 @@ def expected_device_plane_edge_core_channel_bootstrap_descriptor( } +def expected_device_plane_edge_core_channel_upgrade_descriptor( + transition_id, +): + return { + "schemaVersion": ( + "nodedc.device-plane.device-edge-core-channel-upgrade.v1" + ), + "transitionId": transition_id, + "action": "upgrade", + "bootstrapPredecessor": { + "patchId": ( + DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID + ), + "artifactSha256": ( + DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_ARTIFACT_SHA256 + ), + }, + "service": "device-control-core", + "composeActivation": "preserve-dedicated-additive-override", + "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", + "publicIngress": "none-on-synology", + "edgeRegistrations": ( + "preserved-requires-explicit-443-reconciliation" + ), + "commandTransport": "disabled", + "gelios": "untouched", + "preservedServices": [ + "device-manager", + "device-gateway", + "device-postgres", + "device-backhaul-target", + ], + "healthGate": "bounded-container-grace+core-edge-contract", + "rollback": ( + "restore-bootstrap-018-source-and-preapply-core-runtime" + ), + } + + def expected_device_plane_manager_failed_control_plane_descriptor(): return { "schemaVersion": ( @@ -9819,6 +9904,39 @@ def validate_device_plane_edge_core_channel_bootstrap_payload( ) ): die("Device Edge Core channel bootstrap descriptor mismatch") + validate_device_plane_edge_core_channel_compose(payload_dir) + return descriptor + + +def validate_device_plane_edge_core_channel_upgrade_payload( + payload_dir, + *, + expected_transition_id=None, +): + descriptor = read_strict_json( + payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL, + "Device Edge Core channel upgrade 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_descriptor( + transition_id + ) + ): + die("Device Edge Core channel upgrade descriptor mismatch") + validate_device_plane_edge_core_channel_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: die("Device Edge Core channel Compose mismatch") @@ -9849,11 +9967,12 @@ def validate_device_plane_edge_core_channel_bootstrap_payload( "ports:", "PRIVATE KEY", "0.0.0.0:8443", + "0.0.0.0:443", "9921:9921", ) if any(value in compose_text for value in forbidden): die("Device Edge Core channel Compose boundary violation") - return descriptor + return compose def installed_device_plane_manager_compose_sha256(): @@ -11546,6 +11665,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_REL + ).exists(): + return validate_device_plane_edge_core_channel_upgrade_predecessor( + payload_dir + ) descriptor = validate_device_plane_edge_core_channel_bootstrap_payload( payload_dir ) @@ -11657,6 +11782,88 @@ def validate_device_plane_edge_core_channel_bootstrap_predecessor( } +def validate_device_plane_edge_core_channel_upgrade_predecessor( + payload_dir, +): + descriptor = validate_device_plane_edge_core_channel_upgrade_payload( + payload_dir + ) + installed_upgrade = ( + DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL + ) + if installed_upgrade.exists() or installed_upgrade.is_symlink(): + die("Device Edge Core channel upgrade is already installed") + predecessor = descriptor["bootstrapPredecessor"] + 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 bootstrap 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 bootstrap journal mismatch") + with tempfile.TemporaryDirectory( + prefix="device-edge-core-channel-upgrade-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_BOOTSTRAP_ENTRIES + ): + die("Device Edge Core channel bootstrap artifact type mismatch") + validate_device_plane_edge_core_channel_bootstrap_payload( + predecessor_payload, + expected_transition_id=predecessor["patchId"], + ) + expected_source = collect_exact_files( + predecessor_payload, + DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES, + "Device Edge Core channel bootstrap predecessor source", + ) + actual_source = collect_exact_files( + DEVICE_PLANE_ROOT, + DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES, + "installed Device Edge Core channel bootstrap source", + ) + if actual_source != expected_source: + die("installed Device Edge Core channel bootstrap source drift detected") + identity_state = inspect_device_edge_channel_core_identity_state() + if identity_state != "valid-reuse-at-apply": + die("Device Edge Core channel upgrade 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-standard-https-443-upgrade", + "descriptor": descriptor, + "identityState": identity_state, + "bootstrapArtifact": artifact, + } + + def validate_device_plane_manager_v2_reconciliation_backup(): backup_dir = ( BACKUPS_DIR / DEVICE_PLANE_MANAGER_V2_RECONCILIATION_BACKUP_ID @@ -15459,14 +15666,26 @@ def component_compose_files( edge_descriptor = ( DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL ) + edge_upgrade_descriptor = ( + DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL + ) edge_override = ( DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL ) edge_descriptor_exists = ( edge_descriptor.exists() or edge_descriptor.is_symlink() ) + edge_upgrade_descriptor_exists = ( + edge_upgrade_descriptor.exists() + or edge_upgrade_descriptor.is_symlink() + ) edge_override_exists = edge_override.exists() or edge_override.is_symlink() - if edge_descriptor_exists != edge_override_exists: + if ( + edge_override_exists + != (edge_descriptor_exists or edge_upgrade_descriptor_exists) + or edge_upgrade_descriptor_exists + and not edge_descriptor_exists + ): die("installed Device Edge Core channel source is incomplete") if edge_descriptor_exists: if ( @@ -15494,6 +15713,29 @@ def component_compose_files( ) ): die("installed Device Edge Core channel descriptor drift detected") + if edge_upgrade_descriptor_exists: + if ( + transition_id + != DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID + or edge_upgrade_descriptor.is_symlink() + or not edge_upgrade_descriptor.is_file() + ): + die("installed Device Edge Core channel upgrade predecessor drift detected") + upgrade = read_strict_json( + edge_upgrade_descriptor, + "installed Device Edge Core channel upgrade descriptor", + max_bytes=16 * 1024, + ) + upgrade_id = upgrade.get("transitionId") + if ( + not isinstance(upgrade_id, str) + or not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", upgrade_id) + or upgrade + != expected_device_plane_edge_core_channel_upgrade_descriptor( + upgrade_id + ) + ): + die("installed Device Edge Core channel upgrade descriptor drift detected") files = (*files, edge_override) overlay = DEVICE_PLANE_ROOT / DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL if overlay.exists() or overlay.is_symlink(): @@ -19471,26 +19713,41 @@ def plan_artifact(artifact): "device_plane_transition=" f"{device_plane_edge_core_channel_preflight['mode']}" ) - print( - "device_plane_predecessor_patch=" - f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_PATCH_ID}" - ) - print( - "device_plane_predecessor_artifact_sha256=" - f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_ARTIFACT_SHA256}" - ) - print( - "failed_patch=" - f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_PATCH_ID}" - ) - print( - "failed_artifact_sha256=" - f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_ARTIFACT_SHA256}" - ) - print( - "recovery_backup=" - f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_BACKUP_ID}" + upgrade = ( + device_plane_edge_core_channel_preflight["mode"] + == "edge-core-channel-standard-https-443-upgrade" ) + if upgrade: + print( + "device_plane_predecessor_patch=" + f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID}" + ) + print( + "device_plane_predecessor_artifact_sha256=" + f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_ARTIFACT_SHA256}" + ) + print("device_edge_channel_endpoint_policy=public-ipv4-standard-https-tcp-443-only") + else: + print( + "device_plane_predecessor_patch=" + f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_PATCH_ID}" + ) + print( + "device_plane_predecessor_artifact_sha256=" + f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_ARTIFACT_SHA256}" + ) + print( + "failed_patch=" + f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_PATCH_ID}" + ) + print( + "failed_artifact_sha256=" + f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_ARTIFACT_SHA256}" + ) + print( + "recovery_backup=" + f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_BACKUP_ID}" + ) print( "device_edge_channel_identity_preflight=" f"{device_plane_edge_core_channel_preflight['identityState']}" @@ -19504,8 +19761,12 @@ def plan_artifact(artifact): print("device_edge_channel=enabled:core-initiated:pinned-mtls:registered-edges-only") print("device_edge_channel_egress=dedicated-core-only-bridge:no-host-ingress") print("device_edge_channel_identity=host-local-private-key:public-certificate-export-only") - print("device_edge_channel_invalid_failed_016=recover-exact-unexported-only") - print("device_edge_channel_registrations=preserved") + if upgrade: + print("device_edge_channel_identity_recovery=forbidden:reuse-valid-only") + print("device_edge_channel_registrations=preserved:explicit-443-reconciliation-required") + else: + print("device_edge_channel_invalid_failed_016=recover-exact-unexported-only") + print("device_edge_channel_registrations=preserved") print("device_edge_channel_commands=disabled") print("device_manager=preserved:active") print("device_manager_public_route=unchanged:active") @@ -20207,7 +20468,10 @@ def rollback_device_plane_apply( service, ) validate_device_manager_control_plane_runtime( - require_edge_channel=False + require_edge_channel=is_device_plane_edge_core_channel_upgrade_slice( + "device-plane", + entries, + ) ) elif is_device_plane_manager_control_plane_slice("device-plane", entries): for service in baseline_services: @@ -21024,7 +21288,12 @@ def prepare_component_runtime(component, entries=None): "device plane management to Core", ) ensure_device_edge_channel_core_identity( - allow_invalid_unexported_recovery=True + allow_invalid_unexported_recovery=( + not is_device_plane_edge_core_channel_upgrade_slice( + component, + entries, + ) + ) ) if is_device_plane_backhaul_vps_enrollment_slice( component, diff --git a/infra/deploy-runner/test_device_manager_control_plane_artifacts.py b/infra/deploy-runner/test_device_manager_control_plane_artifacts.py index e7b79e4..f692bfb 100644 --- a/infra/deploy-runner/test_device_manager_control_plane_artifacts.py +++ b/infra/deploy-runner/test_device_manager_control_plane_artifacts.py @@ -289,6 +289,35 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase): self.assertEqual(checks[0]["expected_json"]["managementApi"], "enabled") self.assertEqual(checks[0]["expected_json"]["commandTransport"], "disabled") + def test_edge_core_channel_upgrade_is_core_only_and_pins_bootstrap_018(self): + manifest, entries, names, result = self.assert_deterministic_artifact( + "build-device-edge-core-channel-bootstrap-artifact.mjs", + "device-edge-core-channel-upgrade-unit-001", + RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES, + ) + self.assertEqual(manifest["component"], "device-plane") + self.assertEqual( + RUNNER.component_services("device-plane", entries), + ("device-control-core",), + ) + self.assertEqual(result["services"], ["device-control-core"]) + self.assertIn( + "payload/deployment/device-edge-core-channel-upgrade-v1.json", + names, + ) + descriptor = RUNNER.expected_device_plane_edge_core_channel_upgrade_descriptor( + "device-edge-core-channel-upgrade-unit-001" + ) + self.assertEqual(descriptor["action"], "upgrade") + self.assertEqual( + descriptor["bootstrapPredecessor"]["patchId"], + "device-edge-core-channel-bootstrap-20260812-018", + ) + self.assertEqual( + descriptor["endpointPolicy"], + "public-ipv4-standard-https-tcp-443-only", + ) + def test_release_v2_keeps_release_v1_predecessor_contract_immutable(self): predecessor = device_manager_release_v1_descriptor( release_id="device-manager-release-20260811-010", @@ -492,6 +521,24 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase): allow_invalid_unexported_recovery=True ) + 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_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")