fix(deploy): allow repeatable typed core releases
This commit is contained in:
@@ -36,6 +36,10 @@ if (
|
||||
}
|
||||
|
||||
const isV2 = patchId.startsWith("device-control-core-release-v2-");
|
||||
const expectedV2Predecessor = Object.freeze({
|
||||
patchId: predecessorPatchId ?? "device-control-core-release-20260812-024",
|
||||
artifactSha256: predecessorSha256 ?? "a289e909283109642e6bba3d9822a31f63423cfe0bbcd52705979681bd2bc793",
|
||||
});
|
||||
const descriptorPath = isV2
|
||||
? "deployment/device-control-core-release-v2.json"
|
||||
: "deployment/device-control-core-release-v1.json";
|
||||
@@ -117,8 +121,8 @@ try {
|
||||
&& (
|
||||
descriptor.commandCatalog !== "allowlisted-adapter-typed-commands-only"
|
||||
|| descriptor.credentialBoundary !== "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned"
|
||||
|| descriptor.predecessor?.patchId !== "device-control-core-release-20260812-024"
|
||||
|| descriptor.predecessor?.artifactSha256 !== "a289e909283109642e6bba3d9822a31f63423cfe0bbcd52705979681bd2bc793"
|
||||
|| descriptor.predecessor?.patchId !== expectedV2Predecessor.patchId
|
||||
|| descriptor.predecessor?.artifactSha256 !== expectedV2Predecessor.artifactSha256
|
||||
)
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -12874,7 +12874,12 @@ def validate_device_plane_control_core_release_predecessor(payload_dir):
|
||||
):
|
||||
die("Device Control Core release predecessor journal mismatch")
|
||||
|
||||
installed_release = DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_REL
|
||||
installed_v1_release = (
|
||||
DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_REL
|
||||
)
|
||||
installed_v2_release = (
|
||||
DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL
|
||||
)
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="device-control-core-release-predecessor-",
|
||||
dir=TMP_DIR,
|
||||
@@ -12903,10 +12908,21 @@ def validate_device_plane_control_core_release_predecessor(payload_dir):
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
||||
)
|
||||
)
|
||||
if installed_release.exists() or installed_release.is_symlink():
|
||||
if (
|
||||
installed_v1_release.exists()
|
||||
or installed_v1_release.is_symlink()
|
||||
or installed_v2_release.exists()
|
||||
or installed_v2_release.is_symlink()
|
||||
):
|
||||
die("Device Control Core first release is already installed")
|
||||
else:
|
||||
if tuple(entries) != DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES:
|
||||
if tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES:
|
||||
installed_release = installed_v1_release
|
||||
source_entries = DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES
|
||||
elif tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES:
|
||||
installed_release = installed_v2_release
|
||||
source_entries = DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES
|
||||
else:
|
||||
die("Device Control Core release predecessor type mismatch")
|
||||
expected_installed = validate_device_plane_control_core_release_payload(
|
||||
predecessor_payload,
|
||||
@@ -12919,7 +12935,6 @@ def validate_device_plane_control_core_release_predecessor(payload_dir):
|
||||
)
|
||||
if installed_descriptor != expected_installed:
|
||||
die("Device Control Core release predecessor is not current")
|
||||
source_entries = DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES
|
||||
expected_source = collect_exact_files(
|
||||
predecessor_payload,
|
||||
source_entries,
|
||||
|
||||
@@ -691,6 +691,167 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_control_core_release_v2_builder_supports_v2_release_predecessor(self):
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="nodedc-control-core-v2-successor-",
|
||||
) as directory:
|
||||
artifact_dir = Path(directory) / "successor"
|
||||
environment = os.environ.copy()
|
||||
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
||||
predecessor_id = "device-control-core-release-v2-unit-001"
|
||||
predecessor_sha = "b" * 64
|
||||
completed = subprocess.run(
|
||||
[
|
||||
"node",
|
||||
str(
|
||||
SCRIPT_DIR
|
||||
/ "build-device-control-core-release-artifact.mjs"
|
||||
),
|
||||
"device-control-core-release-v2-unit-002",
|
||||
predecessor_id,
|
||||
predecessor_sha,
|
||||
],
|
||||
cwd=PLATFORM_ROOT,
|
||||
env=environment,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
successor = json.loads(completed.stdout)
|
||||
extracted = Path(directory) / "extracted-successor"
|
||||
extracted.mkdir()
|
||||
_manifest, entries, payload = RUNNER.load_artifact(
|
||||
Path(successor["artifact"]),
|
||||
extracted,
|
||||
)
|
||||
self.assertEqual(
|
||||
tuple(entries),
|
||||
RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
|
||||
)
|
||||
descriptor = json.loads(
|
||||
(
|
||||
payload / RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(
|
||||
descriptor["predecessor"],
|
||||
{
|
||||
"kind": "release",
|
||||
"patchId": predecessor_id,
|
||||
"artifactSha256": predecessor_sha,
|
||||
},
|
||||
)
|
||||
|
||||
def test_control_core_release_v2_accepts_current_v2_release_predecessor(self):
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="nodedc-control-core-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_id = "device-control-core-release-v2-unit-001"
|
||||
predecessor_sha = "b" * 64
|
||||
artifact_name = f"nodedc-device-plane-{predecessor_id}.tgz"
|
||||
(applied / artifact_name).write_bytes(b"reviewed-v2-predecessor")
|
||||
predecessor = {
|
||||
"kind": "release",
|
||||
"patchId": predecessor_id,
|
||||
"artifactSha256": predecessor_sha,
|
||||
}
|
||||
predecessor_descriptor = (
|
||||
RUNNER.expected_device_plane_control_core_release_descriptor(
|
||||
predecessor_id,
|
||||
{
|
||||
"kind": "release",
|
||||
"patchId": "device-control-core-release-20260812-024",
|
||||
"artifactSha256": "a" * 64,
|
||||
},
|
||||
schema_version="v2",
|
||||
)
|
||||
)
|
||||
candidate_descriptor = (
|
||||
RUNNER.expected_device_plane_control_core_release_descriptor(
|
||||
"device-control-core-release-v2-unit-002",
|
||||
predecessor,
|
||||
schema_version="v2",
|
||||
)
|
||||
)
|
||||
installed = (
|
||||
device_plane / RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL
|
||||
)
|
||||
installed.write_text(
|
||||
json.dumps(predecessor_descriptor),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with (
|
||||
mock.patch.object(RUNNER, "APPLIED_DIR", applied),
|
||||
mock.patch.object(RUNNER, "TMP_DIR", temporary),
|
||||
mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", device_plane),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_plane_control_core_release_payload",
|
||||
side_effect=(candidate_descriptor, predecessor_descriptor),
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"load_artifact",
|
||||
return_value=(
|
||||
{
|
||||
"id": predecessor_id,
|
||||
"component": "device-plane",
|
||||
"type": "app-overlay",
|
||||
},
|
||||
RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
|
||||
root / "predecessor-payload",
|
||||
),
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"load_state",
|
||||
return_value=[{
|
||||
"id": predecessor_id,
|
||||
"artifact": artifact_name,
|
||||
"component": "device-plane",
|
||||
"sha256": predecessor_sha,
|
||||
"status": "ok",
|
||||
}],
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"sha256_file",
|
||||
return_value=predecessor_sha,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"collect_exact_files",
|
||||
return_value={"same": "source"},
|
||||
),
|
||||
mock.patch.object(RUNNER, "component_compose_files"),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"inspect_device_edge_channel_core_identity_state",
|
||||
return_value="valid-reuse-at-apply",
|
||||
),
|
||||
mock.patch.object(RUNNER, "healthcheck_compose_service"),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_manager_control_plane_runtime",
|
||||
),
|
||||
):
|
||||
result = (
|
||||
RUNNER.validate_device_plane_control_core_release_predecessor(
|
||||
root / "candidate-payload"
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
result["mode"],
|
||||
"active-device-control-core-forward-release",
|
||||
)
|
||||
|
||||
def test_control_core_release_rejects_direct_legacy_predecessor(self):
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="nodedc-control-core-release-invalid-",
|
||||
|
||||
Reference in New Issue
Block a user