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 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
|
const descriptorPath = isV2
|
||||||
? "deployment/device-control-core-release-v2.json"
|
? "deployment/device-control-core-release-v2.json"
|
||||||
: "deployment/device-control-core-release-v1.json";
|
: "deployment/device-control-core-release-v1.json";
|
||||||
@@ -117,8 +121,8 @@ try {
|
|||||||
&& (
|
&& (
|
||||||
descriptor.commandCatalog !== "allowlisted-adapter-typed-commands-only"
|
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.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?.patchId !== expectedV2Predecessor.patchId
|
||||||
|| descriptor.predecessor?.artifactSha256 !== "a289e909283109642e6bba3d9822a31f63423cfe0bbcd52705979681bd2bc793"
|
|| 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")
|
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(
|
with tempfile.TemporaryDirectory(
|
||||||
prefix="device-control-core-release-predecessor-",
|
prefix="device-control-core-release-predecessor-",
|
||||||
dir=TMP_DIR,
|
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,
|
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")
|
die("Device Control Core first release is already installed")
|
||||||
else:
|
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")
|
die("Device Control Core release predecessor type mismatch")
|
||||||
expected_installed = validate_device_plane_control_core_release_payload(
|
expected_installed = validate_device_plane_control_core_release_payload(
|
||||||
predecessor_payload,
|
predecessor_payload,
|
||||||
@@ -12919,7 +12935,6 @@ def validate_device_plane_control_core_release_predecessor(payload_dir):
|
|||||||
)
|
)
|
||||||
if installed_descriptor != expected_installed:
|
if installed_descriptor != expected_installed:
|
||||||
die("Device Control Core release predecessor is not current")
|
die("Device Control Core release predecessor is not current")
|
||||||
source_entries = DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES
|
|
||||||
expected_source = collect_exact_files(
|
expected_source = collect_exact_files(
|
||||||
predecessor_payload,
|
predecessor_payload,
|
||||||
source_entries,
|
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):
|
def test_control_core_release_rejects_direct_legacy_predecessor(self):
|
||||||
with tempfile.TemporaryDirectory(
|
with tempfile.TemporaryDirectory(
|
||||||
prefix="nodedc-control-core-release-invalid-",
|
prefix="nodedc-control-core-release-invalid-",
|
||||||
|
|||||||
Reference in New Issue
Block a user