refactor(deploy): make device manager releases declarative
This commit is contained in:
@@ -217,7 +217,7 @@ DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_REL = (
|
||||
"deployment/device-manager-control-plane-v2.json"
|
||||
)
|
||||
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL = (
|
||||
"deployment/device-manager-control-plane-v3.json"
|
||||
"deployment/device-manager-release-v1.json"
|
||||
)
|
||||
DEVICE_PLANE_MANAGER_COMPOSE_REL = "docker-compose.device-manager.yml"
|
||||
DEVICE_PLANE_MANAGER_COMPOSE_SHA256 = (
|
||||
@@ -350,17 +350,6 @@ DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256 = (
|
||||
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID = (
|
||||
"device-manager-control-plane-v2-reconciliation-20260811-004"
|
||||
)
|
||||
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT = (
|
||||
"nodedc-device-plane-device-manager-control-plane-v2-reconciliation-"
|
||||
"20260811-004.tgz"
|
||||
)
|
||||
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256 = (
|
||||
"09600bfd99717314b43936e17ffaaf6b6fca1f2fd008beddddc68480cdf3d284"
|
||||
)
|
||||
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_APPLY_BACKUP_ID = (
|
||||
"device-plane-device-manager-control-plane-v2-reconciliation-"
|
||||
"20260811-004-20260811-100533"
|
||||
)
|
||||
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_BACKUP_ID = (
|
||||
"device-plane-device-manager-control-plane-20260811-003-"
|
||||
"20260811-012505"
|
||||
@@ -8332,7 +8321,10 @@ def load_artifact(artifact, work_dir):
|
||||
manifest["component"],
|
||||
entries,
|
||||
):
|
||||
validate_device_plane_manager_control_plane_payload(payload_dir)
|
||||
validate_device_plane_manager_control_plane_payload(
|
||||
payload_dir,
|
||||
expected_release_id=manifest["id"],
|
||||
)
|
||||
if is_device_plane_manager_failed_control_plane_slice(
|
||||
manifest["component"],
|
||||
entries,
|
||||
@@ -8652,6 +8644,34 @@ def state_has_patch_id(patch_id):
|
||||
return any(row.get("id") == patch_id for row in load_state(STATE_FILE))
|
||||
|
||||
|
||||
def failed_state_has_sha(sha):
|
||||
return any(
|
||||
row.get("sha256") == sha
|
||||
for row in load_state(FAILED_STATE_FILE)
|
||||
)
|
||||
|
||||
|
||||
def failed_state_has_patch_id(patch_id):
|
||||
return any(
|
||||
row.get("id") == patch_id
|
||||
for row in load_state(FAILED_STATE_FILE)
|
||||
)
|
||||
|
||||
|
||||
def reject_failed_artifact_replay(manifest, sha256):
|
||||
patch_id = manifest.get("id")
|
||||
if failed_state_has_sha(sha256):
|
||||
die(
|
||||
"artifact SHA is terminal failed and cannot be replayed; "
|
||||
"build a new immutable artifact"
|
||||
)
|
||||
if patch_id and failed_state_has_patch_id(patch_id):
|
||||
die(
|
||||
"patch id is terminal failed and cannot be reused; "
|
||||
"build a new patch id"
|
||||
)
|
||||
|
||||
|
||||
def reject_terminal_engine_l2_failed_artifact(manifest, sha256):
|
||||
if (
|
||||
manifest.get("id") == ENGINE_L2_CLOSED_LOOP_FAILED_PATCH_ID
|
||||
@@ -8712,7 +8732,7 @@ def reject_terminal_device_plane_manager_artifact(
|
||||
die(
|
||||
"Device Manager control-plane 003 is terminal failed; "
|
||||
"use the exact v2 reconciliation successor followed by the "
|
||||
"exact v3 activation successor"
|
||||
"declarative Device Manager release successor"
|
||||
)
|
||||
|
||||
|
||||
@@ -8833,19 +8853,8 @@ def expected_platform_device_manager_public_route_descriptor():
|
||||
}
|
||||
|
||||
|
||||
def expected_device_plane_manager_control_plane_descriptor():
|
||||
def expected_device_plane_manager_release_boundaries():
|
||||
return {
|
||||
"schemaVersion": (
|
||||
"nodedc.device-plane.device-manager-control-plane.v3"
|
||||
),
|
||||
"action": "activate",
|
||||
"predecessor": {
|
||||
"patchId": DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID,
|
||||
"artifactSha256": (
|
||||
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256
|
||||
),
|
||||
"mode": "failed-v2-control-plane-baseline-adoption",
|
||||
},
|
||||
"service": "device-manager",
|
||||
"publicIngress": "reverse-proxy-only",
|
||||
"deviceCoreManagementApi": "file-token-authenticated",
|
||||
@@ -8853,7 +8862,7 @@ def expected_device_plane_manager_control_plane_descriptor():
|
||||
"healthGate": "bounded-container-grace+core-contract",
|
||||
"commandTransport": "disabled",
|
||||
"gelios": "untouched",
|
||||
"rollback": "restore-v2-reconciled-baseline",
|
||||
"rollback": "restore-preapply-snapshot",
|
||||
}
|
||||
|
||||
|
||||
@@ -8982,14 +8991,62 @@ def validate_platform_device_manager_public_route_payload(payload_dir):
|
||||
return descriptor
|
||||
|
||||
|
||||
def validate_device_plane_manager_control_plane_payload(payload_dir):
|
||||
def validate_device_plane_manager_control_plane_payload(
|
||||
payload_dir,
|
||||
*,
|
||||
expected_release_id=None,
|
||||
):
|
||||
descriptor = read_strict_json(
|
||||
payload_dir / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
||||
"Device Manager control-plane descriptor",
|
||||
"Device Manager release descriptor",
|
||||
max_bytes=16 * 1024,
|
||||
)
|
||||
if descriptor != expected_device_plane_manager_control_plane_descriptor():
|
||||
die("Device Manager control-plane descriptor mismatch")
|
||||
required_keys = {
|
||||
"schemaVersion",
|
||||
"releaseId",
|
||||
"action",
|
||||
"predecessor",
|
||||
*expected_device_plane_manager_release_boundaries(),
|
||||
}
|
||||
if set(descriptor) != required_keys:
|
||||
die("Device Manager release descriptor key set mismatch")
|
||||
if (
|
||||
descriptor.get("schemaVersion")
|
||||
!= "nodedc.device-plane.device-manager-release.v1"
|
||||
):
|
||||
die("Device Manager release descriptor schema mismatch")
|
||||
release_id = descriptor.get("releaseId")
|
||||
if (
|
||||
not isinstance(release_id, str)
|
||||
or not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", release_id)
|
||||
or (expected_release_id is not None and release_id != expected_release_id)
|
||||
):
|
||||
die("Device Manager release id mismatch")
|
||||
action = descriptor.get("action")
|
||||
predecessor = descriptor.get("predecessor")
|
||||
if (
|
||||
action not in ("activate", "upgrade")
|
||||
or not isinstance(predecessor, dict)
|
||||
or set(predecessor) != {"kind", "patchId", "artifactSha256"}
|
||||
or predecessor.get("kind") not in ("reconciliation", "release")
|
||||
or not isinstance(predecessor.get("patchId"), str)
|
||||
or not re.fullmatch(
|
||||
r"[A-Za-z0-9._-]{1,96}",
|
||||
predecessor["patchId"],
|
||||
)
|
||||
or predecessor["patchId"] == release_id
|
||||
or not isinstance(predecessor.get("artifactSha256"), str)
|
||||
or not re.fullmatch(
|
||||
r"[a-f0-9]{64}",
|
||||
predecessor["artifactSha256"],
|
||||
)
|
||||
or (action == "activate")
|
||||
!= (predecessor["kind"] == "reconciliation")
|
||||
):
|
||||
die("Device Manager release predecessor mismatch")
|
||||
boundaries = expected_device_plane_manager_release_boundaries()
|
||||
if any(descriptor.get(key) != value for key, value in boundaries.items()):
|
||||
die("Device Manager release security boundary mismatch")
|
||||
compose_path = payload_dir / DEVICE_PLANE_MANAGER_COMPOSE_REL
|
||||
if sha256_file(compose_path) != DEVICE_PLANE_MANAGER_COMPOSE_SHA256:
|
||||
die("Device Manager control-plane Compose mismatch")
|
||||
@@ -10494,52 +10551,136 @@ def validate_device_plane_manager_activation_predecessor(payload_dir):
|
||||
descriptor = validate_device_plane_manager_control_plane_payload(
|
||||
payload_dir
|
||||
)
|
||||
reconciliation_artifact = (
|
||||
APPLIED_DIR / DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT
|
||||
)
|
||||
predecessor = descriptor["predecessor"]
|
||||
patch_id = predecessor["patchId"]
|
||||
sha256 = predecessor["artifactSha256"]
|
||||
artifact_name = f"nodedc-device-plane-{patch_id}.tgz"
|
||||
artifact = APPLIED_DIR / artifact_name
|
||||
try:
|
||||
artifact_stat = reconciliation_artifact.lstat()
|
||||
artifact_stat = artifact.lstat()
|
||||
except FileNotFoundError:
|
||||
die("Device Manager reconciliation applied artifact is missing")
|
||||
die("Device Manager predecessor applied artifact is missing")
|
||||
if (
|
||||
stat.S_ISLNK(artifact_stat.st_mode)
|
||||
or not stat.S_ISREG(artifact_stat.st_mode)
|
||||
or sha256_file(reconciliation_artifact)
|
||||
!= DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256
|
||||
or sha256_file(artifact) != sha256
|
||||
):
|
||||
die("Device Manager reconciliation applied artifact mismatch")
|
||||
die("Device Manager predecessor applied artifact mismatch")
|
||||
|
||||
records = [
|
||||
id_records = [
|
||||
value
|
||||
for value in load_state(STATE_FILE)
|
||||
if value.get("id")
|
||||
== DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID
|
||||
if value.get("id") == patch_id
|
||||
]
|
||||
sha_records = [
|
||||
value
|
||||
for value in load_state(STATE_FILE)
|
||||
if value.get("sha256") == sha256
|
||||
]
|
||||
if len(records) != 1:
|
||||
die("Device Manager reconciliation applied journal count mismatch")
|
||||
record = records[0]
|
||||
if (
|
||||
record.get("artifact")
|
||||
!= DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT
|
||||
or record.get("backup_id")
|
||||
!= DEVICE_PLANE_MANAGER_V2_RECONCILIATION_APPLY_BACKUP_ID
|
||||
or record.get("component") != "device-plane"
|
||||
or record.get("sha256")
|
||||
!= DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256
|
||||
or record.get("status") != "ok"
|
||||
len(id_records) != 1
|
||||
or len(sha_records) != 1
|
||||
or id_records[0] != sha_records[0]
|
||||
):
|
||||
die("Device Manager reconciliation applied journal mismatch")
|
||||
die("Device Manager predecessor applied journal identity mismatch")
|
||||
record = id_records[0]
|
||||
backup_id = record.get("backup_id")
|
||||
if (
|
||||
record.get("artifact") != artifact_name
|
||||
or record.get("component") != "device-plane"
|
||||
or record.get("id") != patch_id
|
||||
or record.get("sha256") != sha256
|
||||
or record.get("status") != "ok"
|
||||
or not isinstance(backup_id, str)
|
||||
or not backup_id
|
||||
or safe_name(backup_id) != backup_id
|
||||
):
|
||||
die("Device Manager predecessor applied journal mismatch")
|
||||
backup_dir = BACKUPS_DIR / backup_id
|
||||
try:
|
||||
backup_stat = backup_dir.lstat()
|
||||
except FileNotFoundError:
|
||||
die("Device Manager predecessor backup is missing")
|
||||
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(
|
||||
backup_stat.st_mode
|
||||
):
|
||||
die("Device Manager predecessor backup is unsafe")
|
||||
|
||||
backup_dir = validate_device_plane_manager_v2_reconciliation_backup()
|
||||
runtime = validate_device_plane_manager_v2_reconciled_baseline(
|
||||
backup_dir,
|
||||
marker_installed=True,
|
||||
)
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="device-manager-applied-predecessor-",
|
||||
dir=TMP_DIR,
|
||||
) as directory:
|
||||
predecessor_manifest, predecessor_entries, predecessor_payload = (
|
||||
load_artifact(artifact, Path(directory))
|
||||
)
|
||||
if (
|
||||
predecessor_manifest.get("id") != patch_id
|
||||
or predecessor_manifest.get("component") != "device-plane"
|
||||
or predecessor_manifest.get("type") != "app-overlay"
|
||||
):
|
||||
die("Device Manager predecessor artifact manifest mismatch")
|
||||
root = component_root("device-plane")
|
||||
if predecessor["kind"] == "reconciliation":
|
||||
if (
|
||||
descriptor["action"] != "activate"
|
||||
or tuple(predecessor_entries)
|
||||
!= DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ENTRIES
|
||||
):
|
||||
die("Device Manager reconciliation predecessor type mismatch")
|
||||
predecessor_descriptor = (
|
||||
validate_device_plane_manager_v2_reconciliation_payload(
|
||||
predecessor_payload
|
||||
)
|
||||
)
|
||||
installed_descriptor = read_strict_json(
|
||||
root / DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL,
|
||||
"installed Device Manager reconciliation predecessor",
|
||||
max_bytes=16 * 1024,
|
||||
)
|
||||
if installed_descriptor != predecessor_descriptor:
|
||||
die("Device Manager reconciliation predecessor is not current")
|
||||
baseline_backup = (
|
||||
validate_device_plane_manager_v2_reconciliation_backup()
|
||||
)
|
||||
runtime = validate_device_plane_manager_v2_reconciled_baseline(
|
||||
baseline_backup,
|
||||
marker_installed=True,
|
||||
)
|
||||
mode = "reconciled-manager-forward-activation"
|
||||
else:
|
||||
if (
|
||||
descriptor["action"] != "upgrade"
|
||||
or tuple(predecessor_entries)
|
||||
!= DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES
|
||||
):
|
||||
die("Device Manager release predecessor type mismatch")
|
||||
predecessor_descriptor = (
|
||||
validate_device_plane_manager_control_plane_payload(
|
||||
predecessor_payload,
|
||||
expected_release_id=patch_id,
|
||||
)
|
||||
)
|
||||
installed_descriptor = read_strict_json(
|
||||
root / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
||||
"installed Device Manager release predecessor",
|
||||
max_bytes=16 * 1024,
|
||||
)
|
||||
if installed_descriptor != predecessor_descriptor:
|
||||
die("Device Manager release predecessor is not current")
|
||||
for service in (
|
||||
"device-control-core",
|
||||
"device-manager",
|
||||
"device-postgres",
|
||||
):
|
||||
healthcheck_compose_service("device-plane", service)
|
||||
runtime = {"accepted": True}
|
||||
mode = "active-manager-forward-upgrade"
|
||||
return {
|
||||
"mode": "reconciled-v2-manager-forward-activation",
|
||||
"mode": mode,
|
||||
"descriptor": descriptor,
|
||||
"reconciliationArtifact": reconciliation_artifact,
|
||||
"reconciliationRecord": record,
|
||||
"predecessorArtifact": artifact,
|
||||
"predecessorRecord": record,
|
||||
"predecessorBackup": backup_dir,
|
||||
"runtime": runtime,
|
||||
}
|
||||
|
||||
@@ -10828,7 +10969,10 @@ def device_plane_inventory_service_names(inventory):
|
||||
"health",
|
||||
"restartCount",
|
||||
}
|
||||
or item.get("service") not in DEVICE_PLANE_RUNTIME_SERVICES
|
||||
or item.get("service") not in (
|
||||
*DEVICE_PLANE_RUNTIME_SERVICES,
|
||||
"device-manager",
|
||||
)
|
||||
or not isinstance(item.get("containerId"), str)
|
||||
or not re.fullmatch(r"[a-f0-9]{64}", item["containerId"])
|
||||
or not isinstance(item.get("imageId"), str)
|
||||
@@ -16391,6 +16535,7 @@ def plan_artifact(artifact):
|
||||
device_plane_foundation_recovery_preflight = None
|
||||
with tempfile.TemporaryDirectory(prefix="plan-", dir=TMP_DIR) as tmp:
|
||||
manifest, entries, payload_dir = load_artifact(artifact, Path(tmp))
|
||||
reject_failed_artifact_replay(manifest, sha)
|
||||
reject_terminal_engine_l2_failed_artifact(manifest, sha)
|
||||
reject_terminal_device_plane_foundation_artifact(manifest, sha)
|
||||
reject_terminal_device_plane_manager_artifact(
|
||||
@@ -18162,11 +18307,15 @@ def plan_artifact(artifact):
|
||||
)
|
||||
print(
|
||||
"device_plane_predecessor_patch="
|
||||
f"{DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID}"
|
||||
f"{device_plane_manager_activation_preflight['descriptor']['predecessor']['patchId']}"
|
||||
)
|
||||
print(
|
||||
"device_plane_predecessor_artifact_sha256="
|
||||
f"{DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ARTIFACT_SHA256}"
|
||||
f"{device_plane_manager_activation_preflight['descriptor']['predecessor']['artifactSha256']}"
|
||||
)
|
||||
print(
|
||||
"device_plane_predecessor_kind="
|
||||
f"{device_plane_manager_activation_preflight['descriptor']['predecessor']['kind']}"
|
||||
)
|
||||
print(
|
||||
"device_plane_runtime_mutation="
|
||||
@@ -21469,6 +21618,7 @@ def apply_artifact(artifact):
|
||||
with tempfile.TemporaryDirectory(prefix=f"apply-{current_stamp}-", dir=TMP_DIR) as tmp:
|
||||
work = Path(tmp)
|
||||
manifest, entries, payload_dir = load_artifact(artifact, work)
|
||||
reject_failed_artifact_replay(manifest, sha)
|
||||
reject_terminal_engine_l2_failed_artifact(manifest, sha)
|
||||
reject_terminal_device_plane_foundation_artifact(
|
||||
manifest,
|
||||
@@ -21848,9 +21998,18 @@ def apply_artifact(artifact):
|
||||
if not artifact_only and not DOCKER.is_file():
|
||||
die(f"docker not found: {DOCKER}")
|
||||
if component == "device-plane":
|
||||
inventory_services = DEVICE_PLANE_RUNTIME_SERVICES
|
||||
if is_device_plane_manager_control_plane_slice(
|
||||
component,
|
||||
entries,
|
||||
):
|
||||
inventory_services = (
|
||||
*inventory_services,
|
||||
"device-manager",
|
||||
)
|
||||
device_plane_runtime_before = (
|
||||
device_plane_runtime_inventory(
|
||||
DEVICE_PLANE_RUNTIME_SERVICES
|
||||
inventory_services
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user