feat(deploy): add versioned Core channel upgrade v2

This commit is contained in:
Codex
2026-08-12 09:48:51 +03:00
parent 020d4dbad7
commit 9fa74a1243
4 changed files with 633 additions and 17 deletions
@@ -318,6 +318,196 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
"public-ipv4-standard-https-tcp-443-only",
)
def test_edge_core_channel_upgrade_v2_is_core_only_and_pins_upgrade_019(self):
patch_id = "device-edge-core-channel-upgrade-v2-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_V2_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-v2.json",
names,
)
descriptor = (
RUNNER.expected_device_plane_edge_core_channel_upgrade_v2_descriptor(
patch_id
)
)
self.assertEqual(descriptor["action"], "upgrade")
self.assertEqual(
descriptor["upgradePredecessor"]["patchId"],
"device-edge-core-channel-upgrade-20260812-019",
)
self.assertEqual(
descriptor["upgradePredecessor"]["artifactSha256"],
"8e9a220275959f378c1c4b00be5c7192e79afe2134eaab808a64e515870a8438",
)
self.assertEqual(descriptor["edgeRegistrations"], "preserved")
self.assertTrue(
RUNNER.is_device_plane_edge_core_channel_upgrade_slice(
"device-plane",
entries,
)
)
self.assertTrue(
RUNNER.is_device_plane_edge_core_channel_upgrade_v2_slice(
"device-plane",
entries,
)
)
def test_edge_core_channel_upgrade_v2_rejects_installed_marker(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-device-edge-upgrade-v2-installed-",
) as directory:
root = Path(directory)
marker = root / RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL
marker.parent.mkdir(parents=True)
marker.write_text("{}\n", encoding="utf-8")
descriptor = (
RUNNER.expected_device_plane_edge_core_channel_upgrade_v2_descriptor(
"device-edge-core-channel-upgrade-v2-unit-002"
)
)
with (
mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", root),
mock.patch.object(
RUNNER,
"validate_device_plane_edge_core_channel_upgrade_v2_payload",
return_value=descriptor,
),
):
with self.assertRaisesRegex(
RUNNER.DeployError,
"upgrade v2 is already installed",
):
RUNNER.validate_device_plane_edge_core_channel_upgrade_v2_predecessor(
root / "payload"
)
def test_edge_core_channel_upgrade_v2_accepts_exact_applied_019(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-device-edge-upgrade-v2-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_V2_PREDECESSOR_PATCH_ID
)
predecessor_sha = (
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_ARTIFACT_SHA256
)
artifact_name = f"nodedc-device-plane-{predecessor_patch}.tgz"
artifact = applied / artifact_name
artifact.write_bytes(b"reviewed-upgrade-019")
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",
)
bootstrap = (
RUNNER.expected_device_plane_edge_core_channel_bootstrap_descriptor(
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID
)
)
descriptor = (
RUNNER.expected_device_plane_edge_core_channel_upgrade_v2_descriptor(
"device-edge-core-channel-upgrade-v2-unit-003"
)
)
expected_source = {"source": "upgrade-019"}
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_v2_payload",
return_value=descriptor,
),
mock.patch.object(
RUNNER,
"sha256_file",
return_value=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_ENTRIES,
root / "predecessor-payload",
),
),
mock.patch.object(
RUNNER,
"validate_device_plane_edge_core_channel_upgrade_payload",
) as validate_predecessor,
mock.patch.object(
RUNNER,
"collect_exact_files",
side_effect=(expected_source, expected_source),
),
mock.patch.object(
RUNNER,
"read_strict_json",
return_value=bootstrap,
),
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_v2_predecessor(
root / "payload"
)
)
self.assertEqual(result["mode"], "edge-core-channel-forward-upgrade-v2")
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",
@@ -539,6 +729,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_V2_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")
@@ -571,6 +779,40 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
)
runtime_acceptance.assert_called_once_with()
def test_upgrade_v2_apply_gate_checks_preserved_runtime_and_edge_contract(self):
entries = RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_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-",
@@ -961,6 +1203,77 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
f"source+runtime-restored:{len(entries)}",
)
def test_edge_upgrade_v2_rollback_restores_upgrade_019_core_runtime(self):
entries = RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES
missing = {RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL}
existing = [entry for entry in entries if entry not in missing]
with tempfile.TemporaryDirectory(
prefix="nodedc-device-edge-upgrade-v2-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(
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL + "\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)