FIX - NAS DEPLOY: wait for Foundry runtime health

This commit is contained in:
Codex
2026-08-09 09:44:27 +03:00
parent 4b24c2e457
commit b511e92cf9
4 changed files with 112 additions and 35 deletions
+5
View File
@@ -435,6 +435,11 @@ The check is source-aware: if an apply rolls back to the prior source, rollback
acceptance uses that prior health contract instead of falsely requiring a acceptance uses that prior health contract instead of falsely requiring a
feature which the restored generation does not contain. feature which the restored generation does not contain.
Module Foundry activation and rollback wait for the Compose container health
barrier before strict image/container inventory acceptance. The HTTP health
endpoint may become ready while Docker still reports `starting`; that timing
window is not treated as an application or rollback failure.
The Foundry ↔ Map Gateway signing key is not an application or `.env` setting. The Foundry ↔ Map Gateway signing key is not an application or `.env` setting.
On the first relevant `platform` or `module-foundry` apply, the root-owned On the first relevant `platform` or `module-foundry` apply, the root-owned
runner creates `/volume1/docker/nodedc-platform/secrets/map-gateway-admin-secret` runner creates `/volume1/docker/nodedc-platform/secrets/map-gateway-admin-secret`
@@ -28,7 +28,7 @@ const artifactDir = resolve(
|| resolve(scriptDir, "../deploy-artifacts"), || resolve(scriptDir, "../deploy-artifacts"),
); );
const [ const [
patchId = "module-foundry-map-runtime-recovery-20260809-002", patchId = "module-foundry-map-runtime-recovery-20260809-003",
...extra ...extra
] = process.argv.slice(2); ] = process.argv.slice(2);
+57 -32
View File
@@ -39,7 +39,7 @@ MODULE_FOUNDRY_SERVICE = "nodedc-module-foundry"
MODULE_FOUNDRY_RUNTIME_BEFORE_FILE = "module-foundry-runtime-before.json" MODULE_FOUNDRY_RUNTIME_BEFORE_FILE = "module-foundry-runtime-before.json"
MODULE_FOUNDRY_BUILD_ATTEMPTS = 2 MODULE_FOUNDRY_BUILD_ATTEMPTS = 2
MODULE_FOUNDRY_MAP_RECOVERY_PATCH_ID = ( MODULE_FOUNDRY_MAP_RECOVERY_PATCH_ID = (
"module-foundry-map-runtime-recovery-20260809-002" "module-foundry-map-runtime-recovery-20260809-003"
) )
MODULE_FOUNDRY_MAP_RECOVERY_ENTRIES = ( MODULE_FOUNDRY_MAP_RECOVERY_ENTRIES = (
"package.json", "package.json",
@@ -56,13 +56,7 @@ MODULE_FOUNDRY_MAP_RECOVERY_ENTRIES = (
MODULE_FOUNDRY_MAP_RECOVERY_MISSING_PREDECESSOR_ENTRIES = ( MODULE_FOUNDRY_MAP_RECOVERY_MISSING_PREDECESSOR_ENTRIES = (
"server/map-grid-persistence.test.mjs", "server/map-grid-persistence.test.mjs",
) )
MODULE_FOUNDRY_MAP_RECOVERY_INSTALLED_TREE_SHA256 = ( MODULE_FOUNDRY_MAP_FAILED_INSTALLED_ENTRIES = (
"8513127fb3b875dc3326c0305441723cda6c405525a68cc11030a6f645738d2b"
)
MODULE_FOUNDRY_MAP_RECOVERY_CANDIDATE_TREE_SHA256 = (
"b89392e56fbaf6a8450ab67fcf263199eadba4a74ef7b70b4d8d685a57c2edc3"
)
MODULE_FOUNDRY_SECTOR_FAILED_ENTRIES = (
"apps/catalog/src/MapFixturePreview.tsx", "apps/catalog/src/MapFixturePreview.tsx",
"apps/catalog/src/mapPresentationProfile.ts", "apps/catalog/src/mapPresentationProfile.ts",
"apps/catalog/src/mapSectorGrid.d.mts", "apps/catalog/src/mapSectorGrid.d.mts",
@@ -72,44 +66,47 @@ MODULE_FOUNDRY_SECTOR_FAILED_ENTRIES = (
"scripts/map-presentation-filters.test.mjs", "scripts/map-presentation-filters.test.mjs",
"scripts/map-sector-grid.test.mjs", "scripts/map-sector-grid.test.mjs",
) )
MODULE_FOUNDRY_MAP_RECOVERY_INSTALLED_TREE_SHA256 = (
"8513127fb3b875dc3326c0305441723cda6c405525a68cc11030a6f645738d2b"
)
MODULE_FOUNDRY_MAP_RECOVERY_CANDIDATE_TREE_SHA256 = (
"b89392e56fbaf6a8450ab67fcf263199eadba4a74ef7b70b4d8d685a57c2edc3"
)
MODULE_FOUNDRY_MAP_FAILED_ENTRIES = MODULE_FOUNDRY_MAP_RECOVERY_ENTRIES
MODULE_FOUNDRY_MAP_FAILED_PATCH_ID = ( MODULE_FOUNDRY_MAP_FAILED_PATCH_ID = (
"module-foundry-map-sector-workspace-20260809-001" "module-foundry-map-runtime-recovery-20260809-002"
) )
MODULE_FOUNDRY_MAP_FAILED_ARTIFACT = ( MODULE_FOUNDRY_MAP_FAILED_ARTIFACT = (
"nodedc-module-foundry-map-sector-workspace-20260809-001.tgz." "nodedc-module-foundry-map-runtime-recovery-20260809-002.tgz."
"20260809-022707" "20260809-092439"
) )
MODULE_FOUNDRY_MAP_FAILED_ARTIFACT_SHA256 = ( MODULE_FOUNDRY_MAP_FAILED_ARTIFACT_SHA256 = (
"96f67297efe6033f545e797dc4e341983b2ce9ca6ebf6ff48a00f6ee8b8ea23b" "bcaffc5dc6098a6c16f8f9362e6e6a89f8eb2745b898bf1964db8ba1f921f603"
) )
MODULE_FOUNDRY_MAP_FAILED_BACKUP_ID = ( MODULE_FOUNDRY_MAP_FAILED_BACKUP_ID = (
"module-foundry-module-foundry-map-sector-workspace-20260809-001-" "module-foundry-module-foundry-map-runtime-recovery-20260809-002-"
"20260809-022707" "20260809-092439"
)
MODULE_FOUNDRY_MAP_FAILED_MESSAGE = (
"Command '['/usr/local/bin/docker', 'compose', '-p', "
"'nodedc-module-foundry', '--env-file', "
"'/volume1/docker/nodedc-platform/module-foundry/source/.env', '-f', "
"'/volume1/docker/nodedc-platform/module-foundry/source/infra/"
"docker-compose.module-foundry.yml', 'up', '-d', '--force-recreate', "
"'--build', '--no-deps', 'nodedc-module-foundry']' returned non-zero "
"exit status 17."
) )
MODULE_FOUNDRY_MAP_FAILED_MESSAGE = "Module Foundry runtime inventory mismatch"
MODULE_FOUNDRY_MAP_FAILED_ROLLBACK_STATUS = "failed:DeployError"
MODULE_FOUNDRY_MAP_FAILED_BACKUP_SHA256 = { MODULE_FOUNDRY_MAP_FAILED_BACKUP_SHA256 = {
"existing-files.txt": ( "existing-files.txt": (
"9273d70c9d1fd9a12963f1f66d2994ec7b7c216ebbae27e254931b8fc2a07ea3" "eae18c8687a8b163f5b7bcf362c44c0be53a107c08f7c8799ee4dc6af9fb4e16"
), ),
"files.txt": ( "files.txt": (
"9273d70c9d1fd9a12963f1f66d2994ec7b7c216ebbae27e254931b8fc2a07ea3" "cde91865f65a8d79fee66b3872a8067caa0c988a4a61f75e3dac2904ff814b10"
), ),
"manifest.env": ( "manifest.env": (
"fd1ea43c173d89e358731415e528220aa72b1a7c27b6ee8ba408292d8ddf41a2" "cefce93cb613d771a4814af100f9cda7c2c020143268913905a233324edd96d9"
), ),
"missing-files.txt": ( "missing-files.txt": (
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" "bec2dcad43b34b2f040ad08414117e0654238a1bc051b37257f1b52f288ce689"
),
"module-foundry-runtime-before.json": (
"bf2ec7678af00349c15d6b8dd9d4b0a43e16f427ed92c5adc842c59fab07e4fe"
), ),
"source-before.tgz": ( "source-before.tgz": (
"e3f14d55d5eaff456a9001fb4a1234b5b584afb93cf4d0c929b30312bd7b5668" "4d34654d32fcd8b328bf83d0f9f5f121fed094fe7bc2e9b1e4b322ccd77da8aa"
), ),
} }
MAP_GATEWAY_SECRET_DIR = Path("/volume1/docker/nodedc-platform/secrets") MAP_GATEWAY_SECRET_DIR = Path("/volume1/docker/nodedc-platform/secrets")
@@ -17467,7 +17464,8 @@ def validate_module_foundry_map_recovery_evidence(
or record.get("sha256") or record.get("sha256")
!= MODULE_FOUNDRY_MAP_FAILED_ARTIFACT_SHA256 != MODULE_FOUNDRY_MAP_FAILED_ARTIFACT_SHA256
or record.get("started_apply") is not True or record.get("started_apply") is not True
or record.get("rollback_status") != "not-required" or record.get("rollback_status")
!= MODULE_FOUNDRY_MAP_FAILED_ROLLBACK_STATUS
or record.get("status") != "failed" or record.get("status") != "failed"
or record.get("message") != MODULE_FOUNDRY_MAP_FAILED_MESSAGE or record.get("message") != MODULE_FOUNDRY_MAP_FAILED_MESSAGE
): ):
@@ -17487,7 +17485,7 @@ def validate_module_foundry_map_recovery_evidence(
or failed_manifest.get("component") != "module-foundry" or failed_manifest.get("component") != "module-foundry"
or failed_manifest.get("type") != "app-overlay" or failed_manifest.get("type") != "app-overlay"
or tuple(failed_entries) or tuple(failed_entries)
!= MODULE_FOUNDRY_SECTOR_FAILED_ENTRIES != MODULE_FOUNDRY_MAP_FAILED_ENTRIES
): ):
die("Module Foundry map failed artifact contract mismatch") die("Module Foundry map failed artifact contract mismatch")
failed_payload_sha256 = collect_exact_files( failed_payload_sha256 = collect_exact_files(
@@ -17535,10 +17533,26 @@ def validate_module_foundry_map_recovery_evidence(
!= MODULE_FOUNDRY_MAP_RECOVERY_INSTALLED_TREE_SHA256 != MODULE_FOUNDRY_MAP_RECOVERY_INSTALLED_TREE_SHA256
): ):
die("Module Foundry partial map predecessor drift detected") die("Module Foundry partial map predecessor drift detected")
installed_failed_payload_sha256 = {
rel: digest
for rel, digest in failed_payload_sha256.items()
if any(
rel == installed or rel.startswith(f"{installed}/")
for installed in MODULE_FOUNDRY_MAP_FAILED_INSTALLED_ENTRIES
)
}
if any(
not any(
rel == installed or rel.startswith(f"{installed}/")
for rel in installed_failed_payload_sha256
)
for installed in MODULE_FOUNDRY_MAP_FAILED_INSTALLED_ENTRIES
):
die("Module Foundry failed installed overlap evidence mismatch")
if { if {
rel: installed_payload_sha256.get(rel) rel: installed_payload_sha256.get(rel)
for rel in failed_payload_sha256 for rel in installed_failed_payload_sha256
} != failed_payload_sha256: } != installed_failed_payload_sha256:
die("Module Foundry installed failed map payload drift detected") die("Module Foundry installed failed map payload drift detected")
runtime = module_foundry_runtime_inventory() runtime = module_foundry_runtime_inventory()
@@ -19492,6 +19506,17 @@ def run_healthchecks(component, entries=None, services=None):
# successful health probe. Wait for the Compose health barrier before # successful health probe. Wait for the Compose health barrier before
# the strict immutable-runtime preflight, both on apply and retry. # the strict immutable-runtime preflight, both on apply and retry.
healthcheck_compose_service("engine", "nodedc-backend") healthcheck_compose_service("engine", "nodedc-backend")
if component == "module-foundry":
if tuple(services or ()) != (MODULE_FOUNDRY_SERVICE,):
die("Module Foundry healthcheck service set mismatch")
# The HTTP endpoint can answer before Docker publishes the first
# successful health probe. Inventory acceptance is intentionally
# strict, so wait for the Compose health barrier on both activation
# and rollback before inspecting the immutable runtime identity.
healthcheck_compose_service(
"module-foundry",
MODULE_FOUNDRY_SERVICE,
)
for url in component_healthchecks(component, entries, services): for url in component_healthchecks(component, entries, services):
healthcheck_url(url) healthcheck_url(url)
if ( if (
@@ -44,6 +44,45 @@ def runtime(container_char="a", image_char="b"):
class ModuleFoundryRuntimeRecoveryTest(unittest.TestCase): class ModuleFoundryRuntimeRecoveryTest(unittest.TestCase):
def test_healthchecks_wait_for_compose_health_before_http(self):
events = []
with (
mock.patch.object(
RUNNER,
"healthcheck_compose_service",
side_effect=lambda component, service: events.append(
("compose", component, service)
),
),
mock.patch.object(
RUNNER,
"component_healthchecks",
return_value=("http://foundry/healthz",),
),
mock.patch.object(
RUNNER,
"healthcheck_url",
side_effect=lambda check: events.append(("http", check)),
),
):
RUNNER.run_healthchecks(
"module-foundry",
("package.json",),
(RUNNER.MODULE_FOUNDRY_SERVICE,),
)
self.assertEqual(
events,
[
(
"compose",
"module-foundry",
RUNNER.MODULE_FOUNDRY_SERVICE,
),
("http", "http://foundry/healthz"),
],
)
def test_build_retries_before_any_runtime_recreate(self): def test_build_retries_before_any_runtime_recreate(self):
first_failure = subprocess.CalledProcessError(17, ["docker", "compose"]) first_failure = subprocess.CalledProcessError(17, ["docker", "compose"])
completed = subprocess.CompletedProcess([], 0) completed = subprocess.CompletedProcess([], 0)
@@ -290,6 +329,7 @@ class ModuleFoundryRuntimeRecoveryTest(unittest.TestCase):
failed_entries = ( failed_entries = (
"apps/catalog/src/MapFixturePreview.tsx", "apps/catalog/src/MapFixturePreview.tsx",
"scripts/map-sector-grid.test.mjs", "scripts/map-sector-grid.test.mjs",
"server/map-grid-persistence.test.mjs",
) )
candidate_files = { candidate_files = {
"apps/catalog/src/MapFixturePreview.tsx": "sector-current\n", "apps/catalog/src/MapFixturePreview.tsx": "sector-current\n",
@@ -326,6 +366,7 @@ class ModuleFoundryRuntimeRecoveryTest(unittest.TestCase):
"files.txt", "files.txt",
"manifest.env", "manifest.env",
"missing-files.txt", "missing-files.txt",
"module-foundry-runtime-before.json",
"source-before.tgz", "source-before.tgz",
} }
for name in backup_names: for name in backup_names:
@@ -377,7 +418,7 @@ class ModuleFoundryRuntimeRecoveryTest(unittest.TestCase):
"component": "module-foundry", "component": "module-foundry",
"id": failed_patch_id, "id": failed_patch_id,
"message": failed_message, "message": failed_message,
"rollback_status": "not-required", "rollback_status": "failed:DeployError",
"sha256": failed_sha256, "sha256": failed_sha256,
"started_apply": True, "started_apply": True,
"status": "failed", "status": "failed",
@@ -416,18 +457,24 @@ class ModuleFoundryRuntimeRecoveryTest(unittest.TestCase):
"MODULE_FOUNDRY_MAP_RECOVERY_MISSING_PREDECESSOR_ENTRIES": ( "MODULE_FOUNDRY_MAP_RECOVERY_MISSING_PREDECESSOR_ENTRIES": (
missing_entries missing_entries
), ),
"MODULE_FOUNDRY_MAP_FAILED_INSTALLED_ENTRIES": (
failed_entries[:-1]
),
"MODULE_FOUNDRY_MAP_RECOVERY_INSTALLED_TREE_SHA256": ( "MODULE_FOUNDRY_MAP_RECOVERY_INSTALLED_TREE_SHA256": (
installed_tree installed_tree
), ),
"MODULE_FOUNDRY_MAP_RECOVERY_CANDIDATE_TREE_SHA256": ( "MODULE_FOUNDRY_MAP_RECOVERY_CANDIDATE_TREE_SHA256": (
candidate_tree candidate_tree
), ),
"MODULE_FOUNDRY_SECTOR_FAILED_ENTRIES": failed_entries, "MODULE_FOUNDRY_MAP_FAILED_ENTRIES": failed_entries,
"MODULE_FOUNDRY_MAP_FAILED_PATCH_ID": failed_patch_id, "MODULE_FOUNDRY_MAP_FAILED_PATCH_ID": failed_patch_id,
"MODULE_FOUNDRY_MAP_FAILED_ARTIFACT": failed_artifact_name, "MODULE_FOUNDRY_MAP_FAILED_ARTIFACT": failed_artifact_name,
"MODULE_FOUNDRY_MAP_FAILED_ARTIFACT_SHA256": failed_sha256, "MODULE_FOUNDRY_MAP_FAILED_ARTIFACT_SHA256": failed_sha256,
"MODULE_FOUNDRY_MAP_FAILED_BACKUP_ID": failed_backup_id, "MODULE_FOUNDRY_MAP_FAILED_BACKUP_ID": failed_backup_id,
"MODULE_FOUNDRY_MAP_FAILED_MESSAGE": failed_message, "MODULE_FOUNDRY_MAP_FAILED_MESSAGE": failed_message,
"MODULE_FOUNDRY_MAP_FAILED_ROLLBACK_STATUS": (
"failed:DeployError"
),
"MODULE_FOUNDRY_MAP_FAILED_BACKUP_SHA256": backup_hashes, "MODULE_FOUNDRY_MAP_FAILED_BACKUP_SHA256": backup_hashes,
} }
with ExitStack() as stack: with ExitStack() as stack: