fix(release): make deployment backup and rollback immutable
This commit is contained in:
@@ -18,17 +18,16 @@ def test_build_identity_does_not_invalidate_dependency_layers() -> None:
|
||||
assert 'io.geointel.ai.enabled="${GEOINTEL_INSTALL_AI}"' in dockerfile
|
||||
|
||||
|
||||
def test_release_deploy_preserves_immutable_and_previous_images() -> None:
|
||||
def test_release_deploy_preserves_immutable_and_backup_specific_rollback_images() -> None:
|
||||
script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert 'GEOINTEL_RELEASE_VARIANT="ai"' in script
|
||||
assert 'GEOINTEL_RELEASE_VARIANT="gis"' in script
|
||||
assert 'GEOINTEL_RELEASE_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:${GEOINTEL_BUILD_SHA}-${GEOINTEL_RELEASE_VARIANT}"' in script
|
||||
assert 'GEOINTEL_PREVIOUS_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:previous"' in script
|
||||
assert 'release_image_id="$(' in script
|
||||
assert '[ "$current_image_id" != "$release_image_id" ]' in script
|
||||
assert 'docker tag "$current_image_id" "$GEOINTEL_PREVIOUS_IMAGE"' in script
|
||||
assert "preserving the existing previous image" in script
|
||||
assert 'GEOINTEL_PREDEPLOY_ROLLBACK_TAG="${GEOINTEL_IMAGE_REPOSITORY}:rollback-${release_id}"' in script
|
||||
assert 'docker tag "$current_image_id" "$GEOINTEL_PREDEPLOY_ROLLBACK_TAG"' in script
|
||||
assert '--rollback-image-tag "$GEOINTEL_PREDEPLOY_ROLLBACK_TAG"' in script
|
||||
assert "GEOINTEL_PREVIOUS_IMAGE" not in script
|
||||
assert 'if docker image inspect "$GEOINTEL_RELEASE_IMAGE"' in script
|
||||
assert "Immutable release tag has conflicting metadata" in script
|
||||
assert "Reusing existing immutable image" in script
|
||||
@@ -36,12 +35,62 @@ def test_release_deploy_preserves_immutable_and_previous_images() -> None:
|
||||
assert "Deployed immutable image" in script
|
||||
|
||||
|
||||
def test_release_creates_verified_backup_before_candidate_migrations() -> None:
|
||||
script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||
|
||||
backup_index = script.index("create_predeploy_backup\n")
|
||||
scan_index = script.index("scan_release_image\n")
|
||||
candidate_start_index = script.index('if ! start_image "$GEOINTEL_RELEASE_IMAGE_ID"')
|
||||
assert scan_index < backup_index
|
||||
assert backup_index < candidate_start_index
|
||||
assert script.index("docker build") < backup_index
|
||||
assert "/mnt/user/appdata/geointel/backups" in script
|
||||
assert "--inventory-mode sha256" in script
|
||||
assert "scripts/verify_release_backup.sh" in script
|
||||
assert "refusing an unbacked migration" in script
|
||||
assert "Quiescing the current backend" in script
|
||||
assert "restarting the unchanged current release" in script
|
||||
assert "preflight_backup_capacity" in script
|
||||
assert "select_verified_link_dest" in script
|
||||
assert "release_backup_snapshot.py" in script
|
||||
|
||||
|
||||
def test_release_starts_only_the_locally_attested_ai_image() -> None:
|
||||
script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert 'GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-true}"' in script
|
||||
assert "Production release deployment requires the gated AI image" in script
|
||||
assert 'bash scripts/generate_container_sbom.sh "$GEOINTEL_RELEASE_IMAGE"' in script
|
||||
assert 'bash scripts/scan_container_image.sh "$GEOINTEL_RELEASE_IMAGE"' in script
|
||||
assert 'running_image_id="$(docker inspect --format \'{{.Image}}\' geointel)"' in script
|
||||
assert 'if [ "$running_image_id" != "$image" ]' in script
|
||||
assert "deployment-attestation.json" in script
|
||||
assert "artifacts/release-evidence/deploy" in script
|
||||
assert "GITEA_COMMIT_SHA" in script
|
||||
assert "GITHUB_SHA" in script
|
||||
assert "must contain one full 40-character Git commit SHA" in script
|
||||
assert 'running_revision" != "$GEOINTEL_BUILD_SHA"' in script
|
||||
assert 'running_ai" != "true"' in script
|
||||
assert '"revision": revision' in script
|
||||
|
||||
|
||||
def test_release_and_container_replacement_are_serialized() -> None:
|
||||
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
|
||||
rollback_script = (ROOT / "deploy" / "unraid" / "rollback-dockerman-container.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
restore_script = (ROOT / "deploy" / "unraid" / "restore-predeploy-database.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "GEOINTEL_DEPLOY_LOCK_FILE" in release_script
|
||||
assert "flock -n 9" in release_script
|
||||
assert "GEOINTEL_DEPLOY_LOCK_FILE" in rollback_script
|
||||
assert "flock -n 9" in rollback_script
|
||||
assert "GEOINTEL_DEPLOY_LOCK_FILE" in restore_script
|
||||
assert "flock -n 9" in restore_script
|
||||
assert "GEOINTEL_DEPLOY_LOCK_HELD=true" in release_script
|
||||
assert "GEOINTEL_CONTAINER_LOCK_FILE" in run_script
|
||||
assert "flock -w 300 8" in run_script
|
||||
assert "GeoIntel container removal did not complete within 60 seconds" in run_script
|
||||
@@ -89,13 +138,35 @@ def test_manual_rollback_reuses_persistent_paths_and_requires_existing_image() -
|
||||
rollback = (ROOT / "deploy" / "unraid" / "rollback-dockerman-container.sh").read_text(encoding="utf-8")
|
||||
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert "geointel-all-in-one:previous" in rollback
|
||||
assert "Backup manifest lacks an immutable rollback image ID" in rollback
|
||||
assert 'get("image_id", "")' in rollback
|
||||
assert 'docker image inspect "$GEOINTEL_ROLLBACK_IMAGE"' in rollback
|
||||
assert 'GEOINTEL_IMAGE="$GEOINTEL_ROLLBACK_IMAGE"' in rollback
|
||||
assert "restore-predeploy-database.sh" in rollback
|
||||
assert "--confirm-production-database-restore" in rollback
|
||||
assert "Image-only rollback" in rollback
|
||||
assert '-v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data"' in run_script
|
||||
assert '-v "${GEOINTEL_STORAGE_PATH}:/app/storage"' in run_script
|
||||
|
||||
|
||||
def test_same_revision_redeploy_rolls_back_by_backup_bound_image_id() -> None:
|
||||
release = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||
rollback = (ROOT / "deploy" / "unraid" / "rollback-dockerman-container.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
restore = (ROOT / "deploy" / "unraid" / "restore-predeploy-database.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
backup = (ROOT / "scripts" / "backup_release_state.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert "current_image_id" in release
|
||||
assert "release_image_id" not in release
|
||||
assert '"rollback_image_tag": ${ROLLBACK_IMAGE_TAG@Q} or None' in backup
|
||||
assert 'if [ -z "$RESTORE_IMAGE" ]; then\n RESTORE_IMAGE="$BACKUP_IMAGE_ID"' in restore
|
||||
assert 'GEOINTEL_ROLLBACK_IMAGE="$(python3 - "$BACKUP_DIR/manifest.json"' in rollback
|
||||
assert "geointel-all-in-one:previous" not in release + rollback + restore
|
||||
|
||||
|
||||
def test_readiness_checks_all_release_shell_entrypoints() -> None:
|
||||
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||
|
||||
@@ -107,6 +178,7 @@ def test_readiness_checks_all_release_shell_entrypoints() -> None:
|
||||
"deploy/unraid/run-dockerman-container.sh",
|
||||
"deploy/unraid/deploy-release.sh",
|
||||
"deploy/unraid/rollback-dockerman-container.sh",
|
||||
"deploy/unraid/restore-predeploy-database.sh",
|
||||
):
|
||||
assert f"bash -n {path}" in readiness
|
||||
|
||||
|
||||
Reference in New Issue
Block a user