Harden immutable release deployment
This commit is contained in:
@@ -167,9 +167,11 @@ def test_frontend_uses_same_origin_api_proxy_by_default() -> None:
|
||||
def test_nginx_runtime_allows_real_gis_upload_payloads() -> None:
|
||||
frontend_nginx = (ROOT / "frontend" / "nginx.conf").read_text(encoding="utf-8")
|
||||
all_in_one_nginx = (ROOT / "deploy" / "unraid" / "nginx-all-in-one.conf").read_text(encoding="utf-8")
|
||||
start_script = (ROOT / "deploy" / "unraid" / "all-in-one-start.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert "client_max_body_size 250m;" in frontend_nginx
|
||||
assert "client_max_body_size 250m;" in all_in_one_nginx
|
||||
assert "client_max_body_size __GEOINTEL_MAX_UPLOAD_MB__m;" in all_in_one_nginx
|
||||
assert 'sed -i "s/__GEOINTEL_MAX_UPLOAD_MB__/${MAX_UPLOAD_MB}/g"' in start_script
|
||||
|
||||
|
||||
def test_nginx_runtime_allows_long_ai_and_qa_requests() -> None:
|
||||
@@ -314,11 +316,11 @@ def test_all_in_one_dockerfile_caches_dependencies_and_uses_cpu_torch_for_ai_run
|
||||
def test_unraid_deploy_passes_ai_build_arg_and_yolo_runtime_env() -> None:
|
||||
deploy_ps1 = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
||||
deploy_sh = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
||||
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")
|
||||
|
||||
assert 'DEPLOY_GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-}"' in deploy_sh
|
||||
assert 'docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI"' in deploy_sh
|
||||
assert 'docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI"' in deploy_ps1
|
||||
assert "--build-arg GEOINTEL_INSTALL_AI=" in release_script
|
||||
assert "DEPLOY_GEOINTEL_INSTALL_AI" in deploy_ps1
|
||||
assert "[string]$InstallAi" in deploy_ps1
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_build_identity_does_not_invalidate_dependency_layers() -> None:
|
||||
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
||||
|
||||
dependency_install = dockerfile.index("/usr/bin/python3.11 -m venv /opt/geointel/venv")
|
||||
source_copy = dockerfile.index("COPY backend/ /app/")
|
||||
build_identity = dockerfile.index("ARG GEOINTEL_BUILD_SHA=unknown")
|
||||
|
||||
assert build_identity > dependency_install
|
||||
assert build_identity > source_copy
|
||||
assert 'org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}"' in dockerfile
|
||||
assert 'org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}"' in dockerfile
|
||||
|
||||
|
||||
def test_release_deploy_preserves_immutable_and_previous_images() -> None:
|
||||
script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert 'GEOINTEL_RELEASE_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:${GEOINTEL_BUILD_SHA}"' in script
|
||||
assert 'GEOINTEL_PREVIOUS_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:previous"' in script
|
||||
assert 'docker tag "$current_image_id" "${GEOINTEL_IMAGE_REPOSITORY}:${current_revision}"' in script
|
||||
assert 'docker tag "$current_image_id" "$GEOINTEL_PREVIOUS_IMAGE"' in script
|
||||
assert "rollback_previous()" in script
|
||||
assert "Deployed immutable image" in script
|
||||
|
||||
|
||||
def test_runtime_configuration_is_validated_before_container_replacement() -> None:
|
||||
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
|
||||
|
||||
validation_index = run_script.index("validate_runtime_config")
|
||||
replacement_index = run_script.index("docker compose down")
|
||||
|
||||
assert validation_index < replacement_index
|
||||
assert "known-default PostGIS password" in run_script
|
||||
assert "GEOINTEL_MAX_UPLOAD_MB must be between 1 and 2048" in run_script
|
||||
assert 'docker image inspect "$GEOINTEL_IMAGE"' in run_script
|
||||
|
||||
|
||||
def test_fresh_install_smoke_is_isolated_and_cleans_only_its_temp_path() -> None:
|
||||
script = (ROOT / "scripts" / "verify_release_fresh_install.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert "mktemp -d" in script
|
||||
assert "geointel-fresh-smoke.*" in script
|
||||
assert "-p 127.0.0.1::80" in script
|
||||
assert "GEOINTEL_POSTGRES_PASSWORD=" in script
|
||||
assert "/health/ready" in script
|
||||
assert "/api/v1/system/capabilities" in script
|
||||
assert "docker exec" in script
|
||||
assert "python -m alembic heads" in script
|
||||
|
||||
|
||||
def test_manual_rollback_reuses_persistent_paths_and_requires_existing_image() -> None:
|
||||
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 'docker image inspect "$GEOINTEL_ROLLBACK_IMAGE"' in rollback
|
||||
assert 'GEOINTEL_IMAGE="$GEOINTEL_ROLLBACK_IMAGE"' 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_readiness_checks_all_release_shell_entrypoints() -> None:
|
||||
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||
|
||||
for path in (
|
||||
"scripts/deploy_tower.sh",
|
||||
"scripts/verify_release_fresh_install.sh",
|
||||
"deploy/unraid/all-in-one-start.sh",
|
||||
"deploy/unraid/run-dockerman-container.sh",
|
||||
"deploy/unraid/deploy-release.sh",
|
||||
"deploy/unraid/rollback-dockerman-container.sh",
|
||||
):
|
||||
assert f"bash -n {path}" in readiness
|
||||
@@ -36,13 +36,16 @@ def test_runtime_report_is_read_only_by_default_and_requires_confirmation() -> N
|
||||
|
||||
def test_all_in_one_deploy_embeds_immutable_build_identity() -> None:
|
||||
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
||||
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||
deploy_powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
||||
deploy_shell = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert "ARG GEOINTEL_BUILD_SHA=unknown" in dockerfile
|
||||
assert 'GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}"' in dockerfile
|
||||
assert 'GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}"' in dockerfile
|
||||
assert 'org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}"' in dockerfile
|
||||
assert 'GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"' in release_script
|
||||
assert "--build-arg GEOINTEL_BUILD_SHA=" in release_script
|
||||
assert "--build-arg GEOINTEL_BUILD_TIME=" in release_script
|
||||
for deploy_source in (deploy_powershell, deploy_shell):
|
||||
assert 'GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"' in deploy_source
|
||||
assert "--build-arg GEOINTEL_BUILD_SHA=" in deploy_source
|
||||
assert "--build-arg GEOINTEL_BUILD_TIME=" in deploy_source
|
||||
assert "bash deploy/unraid/deploy-release.sh" in deploy_source
|
||||
|
||||
@@ -524,8 +524,11 @@ def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
|
||||
def test_tower_deploy_waits_for_startup_migration_before_live_smoke() -> None:
|
||||
for relative_path in ("scripts/deploy_tower.ps1", "scripts/deploy_tower.sh"):
|
||||
script = (ROOT / relative_path).read_text(encoding="utf-8")
|
||||
wait_position = script.index("wait_for_geointel_health")
|
||||
invocation_position = script.index("\nwait_for_geointel_health", wait_position)
|
||||
smoke_position = script.index("LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh")
|
||||
assert "docker inspect --format" in script
|
||||
assert invocation_position < smoke_position
|
||||
assert "bash deploy/unraid/deploy-release.sh" in script
|
||||
|
||||
release_script = (ROOT / "deploy/unraid/deploy-release.sh").read_text(encoding="utf-8")
|
||||
wait_position = release_script.index("wait_for_geointel_health")
|
||||
invocation_position = release_script.index("\n wait_for_geointel_health", wait_position)
|
||||
smoke_position = release_script.index("LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh")
|
||||
assert "docker inspect --format" in release_script
|
||||
assert invocation_position < smoke_position
|
||||
|
||||
@@ -10,7 +10,7 @@ def test_unraid_template_documents_editable_runtime_settings() -> None:
|
||||
template = (ROOT / "deploy" / "unraid" / "geointel-unraid-template.xml").read_text(encoding="utf-8")
|
||||
|
||||
assert "<Name>geointel</Name>" in template
|
||||
assert "GeoIntel all-in-one runs the complete GeoIntel Kempen V1 stack in one Docker container" in template
|
||||
assert "Belgium and Belgian North Sea workbench" in template
|
||||
assert "<Repository>geointel-all-in-one:latest</Repository>" in template
|
||||
assert "<WebUI>http://[IP]:[PORT:80]/</WebUI>" in template
|
||||
assert "<Icon>http://192.168.10.150:1202/geointel-icon.png</Icon>" in template
|
||||
@@ -109,29 +109,35 @@ def test_unraid_all_in_one_runtime_starts_embedded_postgis_backend_and_nginx() -
|
||||
def test_tower_deploy_uses_single_container_unraid_compose() -> None:
|
||||
powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
||||
bash = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
||||
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||
|
||||
for script in (powershell, bash):
|
||||
assert "docker compose -f docker-compose.unraid.yml config" in script
|
||||
assert "--build-arg GEOINTEL_INSTALL_AI=" in script
|
||||
assert '--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA"' in script
|
||||
assert '--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME"' in script
|
||||
assert "-f deploy/unraid/Dockerfile.all-in-one" in script
|
||||
assert "-t geointel-all-in-one:latest" in script
|
||||
assert "docker compose -f docker-compose.unraid.yml build geointel" not in script
|
||||
assert "bash deploy/unraid/run-dockerman-container.sh" in script
|
||||
assert "LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh" in script
|
||||
assert "bash deploy/unraid/deploy-release.sh" in script
|
||||
|
||||
assert "docker compose -f docker-compose.unraid.yml config" in release_script
|
||||
assert "--build-arg GEOINTEL_INSTALL_AI=" in release_script
|
||||
assert '--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA"' in release_script
|
||||
assert '--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME"' in release_script
|
||||
assert "-f deploy/unraid/Dockerfile.all-in-one" in release_script
|
||||
assert '-t "$GEOINTEL_RELEASE_IMAGE"' in release_script
|
||||
assert '-t "${GEOINTEL_IMAGE_REPOSITORY}:latest"' in release_script
|
||||
assert 'GEOINTEL_IMAGE="$image" bash deploy/unraid/run-dockerman-container.sh' in release_script
|
||||
assert "LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh" in release_script
|
||||
|
||||
|
||||
def test_tower_deploy_build_uses_remote_env_ai_setting_by_default() -> None:
|
||||
powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
||||
bash = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
||||
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||
|
||||
for script in (powershell, bash):
|
||||
assert "if [ -f .env ]; then" in script
|
||||
assert ". ./.env" in script
|
||||
assert "DEPLOY_GEOINTEL_INSTALL_AI" in script
|
||||
assert 'GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"' in script
|
||||
assert 'docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI"' in script
|
||||
assert "bash deploy/unraid/deploy-release.sh" in script
|
||||
|
||||
assert "if [ -f .env ]; then" in release_script
|
||||
assert ". ./.env" in release_script
|
||||
assert 'GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"' in release_script
|
||||
assert "--build-arg GEOINTEL_INSTALL_AI=" in release_script
|
||||
|
||||
|
||||
def test_powershell_tower_deploy_streams_remote_script_to_bash() -> None:
|
||||
|
||||
Reference in New Issue
Block a user