Harden immutable release deployment
This commit is contained in:
@@ -69,6 +69,11 @@
|
||||
- Kept map rectangle and full-area selection available for coverage inspection
|
||||
when a selected national theme has not yet been materialized, without
|
||||
querying an unrelated active dataset.
|
||||
- Hardened the all-in-one release path with commit-SHA image tags, OCI build
|
||||
labels, previous-image preservation, automatic/manual rollback, an isolated
|
||||
fresh-install smoke and dependency-cache-safe build metadata.
|
||||
- Made production startup reject known-default PostGIS passwords and apply the
|
||||
configured upload limit consistently to nginx and FastAPI.
|
||||
|
||||
## Sprint 240 Operational forest, agriculture, nature and soil themes (2026-07-17)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -12,13 +12,9 @@ ARG GEOINTEL_INSTALL_AI=false
|
||||
ARG GEOINTEL_TORCH_INDEX_URL=https://download.pytorch.org/whl/cpu
|
||||
ARG GEOINTEL_TORCH_VERSION=2.13.0
|
||||
ARG GEOINTEL_TORCHVISION_VERSION=0.28.0
|
||||
ARG GEOINTEL_BUILD_SHA=unknown
|
||||
ARG GEOINTEL_BUILD_TIME=unknown
|
||||
|
||||
ENV GEOINTEL_ENV=production \
|
||||
GEOINTEL_API_PREFIX=/api/v1 \
|
||||
GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}" \
|
||||
GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}" \
|
||||
GEOINTEL_STORAGE_ROOT=/app/storage \
|
||||
STORAGE_ROOT=/app/storage \
|
||||
GEOINTEL_ALL_IN_ONE=1 \
|
||||
@@ -154,6 +150,17 @@ RUN chmod +x /usr/local/bin/geointel-all-in-one-start \
|
||||
/app/scripts/run_background_corpus_split_matrix.sh \
|
||||
/app/scripts/run_split_background_promotion_workflow.sh
|
||||
|
||||
ARG GEOINTEL_BUILD_SHA=unknown
|
||||
ARG GEOINTEL_BUILD_TIME=unknown
|
||||
|
||||
ENV GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}" \
|
||||
GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}"
|
||||
|
||||
LABEL org.opencontainers.image.title="GeoIntel" \
|
||||
org.opencontainers.image.description="GeoIntel workbench for Belgium and the Belgian North Sea" \
|
||||
org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}" \
|
||||
org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}"
|
||||
|
||||
VOLUME ["/var/lib/postgresql/data", "/app/storage"]
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
+60
-7
@@ -72,11 +72,19 @@ cd /mnt/user/appdata/geointel
|
||||
cp deploy/unraid/geointel.env.example .env
|
||||
nano .env
|
||||
docker compose -f docker-compose.unraid.yml config
|
||||
docker build --build-arg GEOINTEL_INSTALL_AI=${GEOINTEL_INSTALL_AI:-false} -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest .
|
||||
bash deploy/unraid/run-dockerman-container.sh
|
||||
bash deploy/unraid/deploy-release.sh
|
||||
```
|
||||
|
||||
The repository deploy scripts run the same flow automatically. They validate the Compose reference, build the image with the `GEOINTEL_INSTALL_AI` build arg, install the DockerMan template/icon, remove any old Compose-owned `geointel` container, preserve/migrate the PostGIS data path and start the final container with DockerMan labels.
|
||||
Set `GEOINTEL_POSTGRES_PASSWORD` to a unique value before that first start.
|
||||
Production startup fails before replacing the active container when the
|
||||
password is empty or one of the documented defaults.
|
||||
|
||||
The repository deploy scripts run the same flow automatically. They validate
|
||||
the Compose reference, preserve the current image as
|
||||
`geointel-all-in-one:previous`, build an immutable commit-SHA tag plus `latest`,
|
||||
install the DockerMan metadata and start the SHA-tagged image. A failed start,
|
||||
live migration smoke or browser/API smoke automatically attempts the previous
|
||||
image without changing the configured PostGIS or storage paths.
|
||||
|
||||
`scripts/deploy_tower.sh` and `scripts/deploy_tower.ps1` source the remote
|
||||
`.env` before building the image. That means `GEOINTEL_INSTALL_AI=true` in
|
||||
@@ -99,8 +107,9 @@ libraries needed for Ultralytics imports; it still never downloads model weights
|
||||
The documented CPU runtime installs pinned `torch==2.13.0` and
|
||||
`torchvision==0.28.0` from `https://download.pytorch.org/whl/cpu`, avoiding the
|
||||
unused CUDA runtime wheels included by the general Linux package index. The
|
||||
Dockerfile copies dependency metadata before backend source, so normal code-only
|
||||
redeploys can reuse the expensive dependency layer.
|
||||
Dockerfile copies dependency metadata before backend source and applies
|
||||
commit/build metadata only after the heavy file layers. Normal code-only
|
||||
redeploys therefore reuse the apt, GIS and optional PyTorch dependency layers.
|
||||
`YOLO_CONFIG_DIR` defaults to `/app/storage/ultralytics`, a writable persistent
|
||||
path, so Ultralytics settings do not fall back to root user config directories.
|
||||
|
||||
@@ -173,8 +182,7 @@ GEOINTEL_CORS_ORIGINS=http://localhost:1203,http://127.0.0.1:1203,http://192.168
|
||||
Apply:
|
||||
|
||||
```bash
|
||||
docker build --build-arg GEOINTEL_INSTALL_AI=${GEOINTEL_INSTALL_AI:-false} -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest .
|
||||
bash deploy/unraid/run-dockerman-container.sh
|
||||
bash deploy/unraid/deploy-release.sh
|
||||
```
|
||||
|
||||
## Persistent paths
|
||||
@@ -226,10 +234,55 @@ curl http://127.0.0.1:1202/api/v1/assistant/models
|
||||
cd /mnt/user/appdata/geointel
|
||||
git fetch origin main
|
||||
git reset --hard origin/main
|
||||
bash deploy/unraid/deploy-release.sh
|
||||
```
|
||||
|
||||
The equivalent low-level build remains available for debugging:
|
||||
|
||||
```bash
|
||||
docker build --build-arg GEOINTEL_INSTALL_AI=${GEOINTEL_INSTALL_AI:-false} -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest .
|
||||
bash deploy/unraid/run-dockerman-container.sh
|
||||
```
|
||||
|
||||
## Release identity, fresh install and rollback
|
||||
|
||||
Inspect the running immutable revision and retained images:
|
||||
|
||||
```bash
|
||||
docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' geointel
|
||||
docker image ls geointel-all-in-one
|
||||
```
|
||||
|
||||
Run a fresh install in isolated temporary PostGIS, storage and model paths. The
|
||||
script binds only a random loopback port and removes its own container and
|
||||
temporary directory:
|
||||
|
||||
```bash
|
||||
bash scripts/verify_release_fresh_install.sh geointel-all-in-one:latest
|
||||
```
|
||||
|
||||
Return to the image that was active immediately before the latest deployment:
|
||||
|
||||
```bash
|
||||
bash deploy/unraid/rollback-dockerman-container.sh
|
||||
```
|
||||
|
||||
For an older retained commit, select its immutable tag explicitly:
|
||||
|
||||
```bash
|
||||
GEOINTEL_ROLLBACK_IMAGE=geointel-all-in-one:<commit-sha> \
|
||||
bash deploy/unraid/rollback-dockerman-container.sh
|
||||
```
|
||||
|
||||
Rollback reuses the configured PostGIS and storage mounts and never runs an
|
||||
Alembic downgrade. If a future release has a backward-incompatible migration,
|
||||
restore its verified pre-release backup instead of forcing an older app
|
||||
against a newer schema.
|
||||
|
||||
The configured upload limit is shared by FastAPI and the generated nginx
|
||||
runtime configuration. Values outside `1..2048` MiB are rejected before the
|
||||
active application is replaced.
|
||||
|
||||
## Safe cleanup
|
||||
|
||||
Safe cache cleanup if Docker build cache fills the Unraid Docker image:
|
||||
|
||||
@@ -13,6 +13,27 @@ export YOLO_MODELS_DIR="${YOLO_MODELS_DIR:-/app/models}"
|
||||
export YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-$STORAGE_ROOT/ultralytics}"
|
||||
export GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP="${GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP:-true}"
|
||||
|
||||
case "$MAX_UPLOAD_MB" in
|
||||
''|*[!0-9]*)
|
||||
echo "GEOINTEL_MAX_UPLOAD_MB must be a whole number between 1 and 2048." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
if [ "$MAX_UPLOAD_MB" -lt 1 ] || [ "$MAX_UPLOAD_MB" -gt 2048 ]; then
|
||||
echo "GEOINTEL_MAX_UPLOAD_MB must be between 1 and 2048." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
case "${GEOINTEL_ENV:-production}:${POSTGRES_PASSWORD}" in
|
||||
production:|production:geointel|production:postgres|production:password|production:changeme|production:change-me-before-shared-use)
|
||||
echo "Refusing to start production with an empty or known-default PostGIS password." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
sed -i "s/__GEOINTEL_MAX_UPLOAD_MB__/${MAX_UPLOAD_MB}/g" /etc/nginx/conf.d/default.conf
|
||||
nginx -t
|
||||
|
||||
mkdir -p "$PGDATA" "$STORAGE_ROOT" "$YOLO_CONFIG_DIR" /run/nginx /var/log/nginx
|
||||
chown -R postgres:postgres "$PGDATA"
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
. ./.env
|
||||
set +a
|
||||
fi
|
||||
|
||||
if [ -n "${DEPLOY_GEOINTEL_INSTALL_AI:-}" ]; then
|
||||
GEOINTEL_INSTALL_AI="$DEPLOY_GEOINTEL_INSTALL_AI"
|
||||
fi
|
||||
|
||||
GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"
|
||||
GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"
|
||||
GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
GEOINTEL_IMAGE_REPOSITORY="${GEOINTEL_IMAGE_REPOSITORY:-geointel-all-in-one}"
|
||||
GEOINTEL_RELEASE_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:${GEOINTEL_BUILD_SHA}"
|
||||
GEOINTEL_PREVIOUS_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:previous"
|
||||
FRONTEND_URL="${FRONTEND_URL:-http://127.0.0.1:${GEOINTEL_FRONTEND_PORT:-1202}}"
|
||||
|
||||
wait_for_geointel_health() {
|
||||
local status=""
|
||||
for attempt in $(seq 1 90); do
|
||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)"
|
||||
if [ "$status" = "healthy" ]; then
|
||||
echo "GeoIntel container is healthy after attempt ${attempt}."
|
||||
return 0
|
||||
fi
|
||||
if [ "$status" = "unhealthy" ] || [ "$status" = "exited" ] || [ "$status" = "dead" ]; then
|
||||
echo "GeoIntel container entered terminal state: ${status}" >&2
|
||||
docker logs --tail 120 geointel >&2 || true
|
||||
return 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "GeoIntel container did not become healthy (last state: ${status:-missing})." >&2
|
||||
docker logs --tail 120 geointel >&2 || true
|
||||
return 1
|
||||
}
|
||||
|
||||
start_image() {
|
||||
local image="$1"
|
||||
GEOINTEL_IMAGE="$image" bash deploy/unraid/run-dockerman-container.sh
|
||||
wait_for_geointel_health
|
||||
}
|
||||
|
||||
rollback_previous() {
|
||||
if ! docker image inspect "$GEOINTEL_PREVIOUS_IMAGE" >/dev/null 2>&1; then
|
||||
echo "Automatic rollback unavailable: ${GEOINTEL_PREVIOUS_IMAGE} does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
echo "Rolling back to ${GEOINTEL_PREVIOUS_IMAGE}..."
|
||||
start_image "$GEOINTEL_PREVIOUS_IMAGE"
|
||||
}
|
||||
|
||||
docker compose -f docker-compose.unraid.yml config >/dev/null
|
||||
|
||||
current_image_id="$(docker inspect --format '{{.Image}}' geointel 2>/dev/null || true)"
|
||||
if [ -n "$current_image_id" ] && docker image inspect "$current_image_id" >/dev/null 2>&1; then
|
||||
current_revision="$(
|
||||
docker image inspect \
|
||||
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \
|
||||
"$current_image_id" 2>/dev/null || true
|
||||
)"
|
||||
if [ -n "$current_revision" ] && [ "$current_revision" != "<no value>" ] && [ "$current_revision" != "unknown" ]; then
|
||||
docker tag "$current_image_id" "${GEOINTEL_IMAGE_REPOSITORY}:${current_revision}"
|
||||
fi
|
||||
docker tag "$current_image_id" "$GEOINTEL_PREVIOUS_IMAGE"
|
||||
fi
|
||||
|
||||
docker build \
|
||||
--build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \
|
||||
--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \
|
||||
--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \
|
||||
-f deploy/unraid/Dockerfile.all-in-one \
|
||||
-t "$GEOINTEL_RELEASE_IMAGE" \
|
||||
-t "${GEOINTEL_IMAGE_REPOSITORY}:latest" \
|
||||
.
|
||||
|
||||
if ! start_image "$GEOINTEL_RELEASE_IMAGE"; then
|
||||
rollback_previous || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -x scripts/live_migration_smoke.sh ]; then
|
||||
if ! LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh; then
|
||||
rollback_previous || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -x scripts/verify_browser_runtime.sh ]; then
|
||||
if ! bash scripts/verify_browser_runtime.sh "$FRONTEND_URL"; then
|
||||
rollback_previous || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Deployed immutable image ${GEOINTEL_RELEASE_IMAGE}."
|
||||
docker image inspect \
|
||||
--format 'revision={{index .Config.Labels "org.opencontainers.image.revision"}} created={{index .Config.Labels "org.opencontainers.image.created"}}' \
|
||||
"$GEOINTEL_RELEASE_IMAGE"
|
||||
@@ -7,8 +7,8 @@
|
||||
<Shell>bash</Shell>
|
||||
<Privileged>false</Privileged>
|
||||
<Support>http://192.168.10.150:1202</Support>
|
||||
<Project>GeoIntel Kempen</Project>
|
||||
<Overview>GeoIntel all-in-one runs the complete GeoIntel Kempen V1 stack in one Docker container: embedded PostGIS, FastAPI backend, nginx frontend and MapLibre UI. Use docker-compose.unraid.yml or this template so the web port, storage path and database path can be edited from Unraid.</Overview>
|
||||
<Project>GeoIntel Belgium and North Sea</Project>
|
||||
<Overview>GeoIntel all-in-one runs the Belgium and Belgian North Sea workbench in one Docker container: embedded PostGIS, FastAPI backend, nginx frontend and MapLibre UI. Use docker-compose.unraid.yml or this template so the web port, storage path and database path can be edited from Unraid.</Overview>
|
||||
<Category>Productivity: Tools: GIS:</Category>
|
||||
<WebUI>http://[IP]:[PORT:80]/</WebUI>
|
||||
<TemplateURL>deploy/unraid/geointel-unraid-template.xml</TemplateURL>
|
||||
@@ -29,7 +29,7 @@
|
||||
<Config Name="Postgres User" Target="GEOINTEL_POSTGRES_USER" Default="geointel" Mode="" Description="Embedded PostGIS database user." Type="Variable" Display="advanced" Required="true" Mask="false">geointel</Config>
|
||||
<Config Name="Postgres Password" Target="GEOINTEL_POSTGRES_PASSWORD" Default="change-me-before-shared-use" Mode="" Description="Embedded PostGIS database password. Change before shared use." Type="Variable" Display="advanced" Required="true" Mask="true">change-me-before-shared-use</Config>
|
||||
<Config Name="CORS Origins" Target="GEOINTEL_CORS_ORIGINS" Default="http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202" Mode="" Description="Comma-separated browser origins allowed to call the backend directly." Type="Variable" Display="advanced" Required="false" Mask="false">http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202</Config>
|
||||
<Config Name="Max Upload MB" Target="GEOINTEL_MAX_UPLOAD_MB" Default="500" Mode="" Description="Maximum upload size in MiB enforced by the backend settings." Type="Variable" Display="advanced" Required="true" Mask="false">500</Config>
|
||||
<Config Name="Max Upload MB" Target="GEOINTEL_MAX_UPLOAD_MB" Default="500" Mode="" Description="Maximum upload size in MiB enforced consistently by nginx and the backend (1-2048)." Type="Variable" Display="advanced" Required="true" Mask="false">500</Config>
|
||||
<Config Name="Official Orthophoto Acquisition" Target="ORTHOPHOTO_ENABLED" Default="true" Mode="" Description="Allow explicit bounded map selections to request the official Digitaal Vlaanderen orthophoto WMS." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
|
||||
<Config Name="Orthophoto WMS URL" Target="ORTHOPHOTO_WMS_URL" Default="https://geo.api.vlaanderen.be/OMWRGBMRVL/wms" Mode="" Description="Official Digitaal Vlaanderen most-recent winter orthophoto WMS endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geo.api.vlaanderen.be/OMWRGBMRVL/wms</Config>
|
||||
<Config Name="Orthophoto Resolution (m)" Target="ORTHOPHOTO_RESOLUTION_M" Default="1.0" Mode="" Description="Requested analysis sampling in metres per pixel. Keep at 1.0 for the active building model profile." Type="Variable" Display="advanced" Required="true" Mask="false">1.0</Config>
|
||||
@@ -63,7 +63,7 @@
|
||||
<Config Name="MDK Probe Timeout Seconds" Target="MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS" Default="20" Mode="" Description="Maximum wait for one read-only MDK GetCapabilities request." Type="Variable" Display="advanced" Required="true" Mask="false">20</Config>
|
||||
<Config Name="Official Thematic Raster Acquisition" Target="THEMATIC_RASTER_ENABLED" Default="true" Mode="" Description="Allow bounded official Departement Omgeving rasters for space, population, accessibility and services." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
|
||||
<Config Name="Thematic Raster WCS URL" Target="THEMATIC_RASTER_WCS_URL" Default="https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs" Mode="" Description="Official public MercatorNet WCS endpoint. Product identifiers remain server allowlisted." Type="Variable" Display="advanced" Required="true" Mask="false">https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs</Config>
|
||||
<Config Name="Thematic Raster Maximum Side (m)" Target="THEMATIC_RASTER_MAX_SIDE_M" Default="60000" Mode="" Description="Maximum side length for one allowlisted thematic raster scope; supports the complete Kempen work area while WCS transfers remain tiled." Type="Variable" Display="advanced" Required="true" Mask="false">60000</Config>
|
||||
<Config Name="Thematic Raster Maximum Side (m)" Target="THEMATIC_RASTER_MAX_SIDE_M" Default="60000" Mode="" Description="Maximum side length for one bounded allowlisted thematic raster request; larger national selections must remain partitioned." Type="Variable" Display="advanced" Required="true" Mask="false">60000</Config>
|
||||
<Config Name="Thematic Raster Maximum Cells" Target="THEMATIC_RASTER_MAX_PIXELS" Default="30000000" Mode="" Description="Maximum raster cells per allowlisted thematic acquisition or selection analysis." Type="Variable" Display="advanced" Required="true" Mask="false">30000000</Config>
|
||||
<Config Name="Local Ollama Assistant" Target="OLLAMA_ENABLED" Default="true" Mode="" Description="Enable the source-grounded GeoIntel assistant backed by Ollama on the Unraid host." Type="Variable" Display="always" Required="true" Mask="false">true</Config>
|
||||
<Config Name="Ollama Base URL" Target="OLLAMA_BASE_URL" Default="http://host.docker.internal:11434" Mode="" Description="Ollama API reachable from the container. The deployment maps host.docker.internal to the Unraid host gateway." Type="Variable" Display="always" Required="true" Mask="false">http://host.docker.internal:11434</Config>
|
||||
|
||||
@@ -14,6 +14,8 @@ GEOINTEL_MODELS_PATH=/mnt/user/appdata/geointel/models
|
||||
GEOINTEL_POSTGIS_DATA_PATH=/mnt/user/appdata/geointel/postgres-data
|
||||
|
||||
# Internal embedded PostGIS settings. The database is not published to the LAN.
|
||||
# Replace the placeholder with a unique secret before the first start. Production
|
||||
# startup rejects empty and known-default passwords.
|
||||
GEOINTEL_POSTGRES_DB=geointel
|
||||
GEOINTEL_POSTGRES_USER=geointel
|
||||
GEOINTEL_POSTGRES_PASSWORD=change-me-before-shared-use
|
||||
@@ -21,7 +23,7 @@ GEOINTEL_POSTGRES_PASSWORD=change-me-before-shared-use
|
||||
# Browser origins allowed when directly calling the backend API.
|
||||
GEOINTEL_CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202
|
||||
|
||||
# Upload guard in MiB.
|
||||
# Upload guard in MiB. The same 1-2048 limit is applied by nginx and FastAPI.
|
||||
GEOINTEL_MAX_UPLOAD_MB=500
|
||||
|
||||
# Explicit, bounded acquisition from the official Digitaal Vlaanderen WMS.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
client_max_body_size 250m;
|
||||
client_max_body_size __GEOINTEL_MAX_UPLOAD_MB__m;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
GEOINTEL_ROLLBACK_IMAGE="${GEOINTEL_ROLLBACK_IMAGE:-geointel-all-in-one:previous}"
|
||||
|
||||
if ! docker image inspect "$GEOINTEL_ROLLBACK_IMAGE" >/dev/null 2>&1; then
|
||||
echo "Rollback image does not exist: ${GEOINTEL_ROLLBACK_IMAGE}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "Starting rollback image ${GEOINTEL_ROLLBACK_IMAGE} without changing persistent volumes..."
|
||||
GEOINTEL_IMAGE="$GEOINTEL_ROLLBACK_IMAGE" bash deploy/unraid/run-dockerman-container.sh
|
||||
|
||||
for attempt in $(seq 1 90); do
|
||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)"
|
||||
if [ "$status" = "healthy" ]; then
|
||||
LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh
|
||||
echo "Rollback completed with healthy image ${GEOINTEL_ROLLBACK_IMAGE}."
|
||||
exit 0
|
||||
fi
|
||||
if [ "$status" = "unhealthy" ] || [ "$status" = "exited" ] || [ "$status" = "dead" ]; then
|
||||
docker logs --tail 120 geointel >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Rollback container did not become healthy." >&2
|
||||
docker logs --tail 120 geointel >&2 || true
|
||||
exit 1
|
||||
@@ -12,12 +12,13 @@ if [ -f .env ]; then
|
||||
fi
|
||||
|
||||
GEOINTEL_FRONTEND_PORT="${GEOINTEL_FRONTEND_PORT:-1202}"
|
||||
GEOINTEL_IMAGE="${GEOINTEL_IMAGE:-geointel-all-in-one:latest}"
|
||||
GEOINTEL_STORAGE_PATH="${GEOINTEL_STORAGE_PATH:-/mnt/user/appdata/geointel/storage}"
|
||||
GEOINTEL_MODELS_PATH="${GEOINTEL_MODELS_PATH:-/mnt/user/appdata/geointel/models}"
|
||||
GEOINTEL_POSTGIS_DATA_PATH="${GEOINTEL_POSTGIS_DATA_PATH:-/mnt/user/appdata/geointel/postgres-data}"
|
||||
GEOINTEL_POSTGRES_DB="${GEOINTEL_POSTGRES_DB:-geointel}"
|
||||
GEOINTEL_POSTGRES_USER="${GEOINTEL_POSTGRES_USER:-geointel}"
|
||||
GEOINTEL_POSTGRES_PASSWORD="${GEOINTEL_POSTGRES_PASSWORD:-geointel}"
|
||||
GEOINTEL_POSTGRES_PASSWORD="${GEOINTEL_POSTGRES_PASSWORD:-}"
|
||||
GEOINTEL_CORS_ORIGINS="${GEOINTEL_CORS_ORIGINS:-http://localhost:${GEOINTEL_FRONTEND_PORT},http://127.0.0.1:${GEOINTEL_FRONTEND_PORT},http://192.168.10.150:${GEOINTEL_FRONTEND_PORT}}"
|
||||
GEOINTEL_MAX_UPLOAD_MB="${GEOINTEL_MAX_UPLOAD_MB:-500}"
|
||||
ORTHOPHOTO_ENABLED="${ORTHOPHOTO_ENABLED:-true}"
|
||||
@@ -101,6 +102,39 @@ OLLAMA_TIMEOUT_SECONDS="${OLLAMA_TIMEOUT_SECONDS:-120}"
|
||||
OLLAMA_MAX_OUTPUT_TOKENS="${OLLAMA_MAX_OUTPUT_TOKENS:-1200}"
|
||||
OLLAMA_CONTEXT_TOKENS="${OLLAMA_CONTEXT_TOKENS:-16384}"
|
||||
|
||||
validate_runtime_config() {
|
||||
case "$GEOINTEL_FRONTEND_PORT" in
|
||||
''|*[!0-9]*)
|
||||
echo "GEOINTEL_FRONTEND_PORT must be a whole number." >&2
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
if [ "$GEOINTEL_FRONTEND_PORT" -lt 1 ] || [ "$GEOINTEL_FRONTEND_PORT" -gt 65535 ]; then
|
||||
echo "GEOINTEL_FRONTEND_PORT must be between 1 and 65535." >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
case "$GEOINTEL_MAX_UPLOAD_MB" in
|
||||
''|*[!0-9]*)
|
||||
echo "GEOINTEL_MAX_UPLOAD_MB must be a whole number." >&2
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
if [ "$GEOINTEL_MAX_UPLOAD_MB" -lt 1 ] || [ "$GEOINTEL_MAX_UPLOAD_MB" -gt 2048 ]; then
|
||||
echo "GEOINTEL_MAX_UPLOAD_MB must be between 1 and 2048." >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
case "$GEOINTEL_POSTGRES_PASSWORD" in
|
||||
''|geointel|postgres|password|changeme|change-me-before-shared-use)
|
||||
echo "Refusing deployment with an empty or known-default PostGIS password." >&2
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
|
||||
docker image inspect "$GEOINTEL_IMAGE" >/dev/null
|
||||
}
|
||||
|
||||
install_dockerman_metadata() {
|
||||
if [ -d /boot/config/plugins/dockerMan ]; then
|
||||
mkdir -p /boot/config/plugins/dockerMan/templates-user /boot/config/plugins/dockerMan/images
|
||||
@@ -125,6 +159,7 @@ migrate_compose_volume_if_needed() {
|
||||
cp -a "${compose_volume_path}/." "$GEOINTEL_POSTGIS_DATA_PATH/"
|
||||
}
|
||||
|
||||
validate_runtime_config
|
||||
install_dockerman_metadata
|
||||
|
||||
docker compose down --remove-orphans || true
|
||||
@@ -233,6 +268,6 @@ docker run -d \
|
||||
-v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data" \
|
||||
-v "${GEOINTEL_STORAGE_PATH}:/app/storage" \
|
||||
-v "${GEOINTEL_MODELS_PATH}:/app/models" \
|
||||
geointel-all-in-one:latest
|
||||
"$GEOINTEL_IMAGE"
|
||||
|
||||
docker ps --filter name=geointel
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
run reused the same project and dataset identifiers. Browser acceptance then
|
||||
exposed and closed a map guard that had prevented coverage-only selection
|
||||
when the chosen theme was honestly unavailable.
|
||||
- RC-5 deployment hardening started by centralizing Tower release execution,
|
||||
moving build identity behind expensive dependency layers, retaining
|
||||
immutable commit images plus a `previous` rollback target, adding automatic
|
||||
rollback and an isolated fresh-install smoke, and failing closed on default
|
||||
database secrets or inconsistent upload limits.
|
||||
|
||||
- Froze the RC geography as all Belgian land plus the separately labelled
|
||||
territorial sea, EEZ and continental shelf.
|
||||
|
||||
@@ -34,59 +34,9 @@ git reset --hard "origin/$REMOTE_BRANCH"
|
||||
git branch -M "$REMOTE_BRANCH"
|
||||
chmod +x scripts/*.sh backend/docker_start.sh deploy/unraid/*.sh || true
|
||||
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
. ./.env
|
||||
set +a
|
||||
fi
|
||||
|
||||
if [ -n "${DEPLOY_GEOINTEL_INSTALL_AI:-}" ]; then
|
||||
GEOINTEL_INSTALL_AI="$DEPLOY_GEOINTEL_INSTALL_AI"
|
||||
fi
|
||||
GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"
|
||||
GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"
|
||||
GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
docker compose -f docker-compose.unraid.yml config >/dev/null
|
||||
docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \
|
||||
--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \
|
||||
--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \
|
||||
-f deploy/unraid/Dockerfile.all-in-one \
|
||||
-t geointel-all-in-one:latest \
|
||||
.
|
||||
bash deploy/unraid/run-dockerman-container.sh
|
||||
|
||||
wait_for_geointel_health() {
|
||||
local status=''
|
||||
for attempt in $(seq 1 90); do
|
||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)"
|
||||
if [ "$status" = 'healthy' ]; then
|
||||
echo "GeoIntel container is healthy after attempt $attempt."
|
||||
return 0
|
||||
fi
|
||||
if [ "$status" = 'unhealthy' ] || [ "$status" = 'exited' ] || [ "$status" = 'dead' ]; then
|
||||
echo "GeoIntel container entered terminal state: $status" >&2
|
||||
docker logs --tail 120 geointel >&2 || true
|
||||
return 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "GeoIntel container did not become healthy (last state: ${status:-missing})." >&2
|
||||
docker logs --tail 120 geointel >&2 || true
|
||||
return 1
|
||||
}
|
||||
|
||||
# Container startup owns the migration. Waiting here prevents the validation
|
||||
# smoke from racing a concurrent `alembic upgrade head` against the same DB.
|
||||
wait_for_geointel_health
|
||||
|
||||
if [ -x scripts/live_migration_smoke.sh ]; then
|
||||
LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh
|
||||
fi
|
||||
|
||||
if [ -x scripts/verify_browser_runtime.sh ]; then
|
||||
bash scripts/verify_browser_runtime.sh "$FRONTEND_URL"
|
||||
fi
|
||||
FRONTEND_URL="$FRONTEND_URL" \
|
||||
DEPLOY_GEOINTEL_INSTALL_AI="${DEPLOY_GEOINTEL_INSTALL_AI:-}" \
|
||||
bash deploy/unraid/deploy-release.sh
|
||||
'@
|
||||
|
||||
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
||||
|
||||
+3
-54
@@ -35,58 +35,7 @@ git fetch origin "$REMOTE_BRANCH"
|
||||
git checkout -B "$REMOTE_BRANCH" "origin/$REMOTE_BRANCH"
|
||||
chmod +x scripts/*.sh backend/docker_start.sh deploy/unraid/*.sh || true
|
||||
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
. ./.env
|
||||
set +a
|
||||
fi
|
||||
|
||||
if [ -n "${DEPLOY_GEOINTEL_INSTALL_AI:-}" ]; then
|
||||
GEOINTEL_INSTALL_AI="$DEPLOY_GEOINTEL_INSTALL_AI"
|
||||
fi
|
||||
GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"
|
||||
GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"
|
||||
GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
docker compose -f docker-compose.unraid.yml config >/dev/null
|
||||
docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \
|
||||
--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \
|
||||
--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \
|
||||
-f deploy/unraid/Dockerfile.all-in-one \
|
||||
-t geointel-all-in-one:latest \
|
||||
.
|
||||
bash deploy/unraid/run-dockerman-container.sh
|
||||
|
||||
wait_for_geointel_health() {
|
||||
local status=''
|
||||
for attempt in $(seq 1 90); do
|
||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)"
|
||||
if [[ "$status" == 'healthy' ]]; then
|
||||
echo "GeoIntel container is healthy after attempt $attempt."
|
||||
return 0
|
||||
fi
|
||||
if [[ "$status" == 'unhealthy' || "$status" == 'exited' || "$status" == 'dead' ]]; then
|
||||
echo "GeoIntel container entered terminal state: $status" >&2
|
||||
docker logs --tail 120 geointel >&2 || true
|
||||
return 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "GeoIntel container did not become healthy (last state: ${status:-missing})." >&2
|
||||
docker logs --tail 120 geointel >&2 || true
|
||||
return 1
|
||||
}
|
||||
|
||||
# Container startup owns the migration. Waiting here prevents the validation
|
||||
# smoke from racing a concurrent `alembic upgrade head` against the same DB.
|
||||
wait_for_geointel_health
|
||||
|
||||
if [[ -x scripts/live_migration_smoke.sh ]]; then
|
||||
LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh
|
||||
fi
|
||||
|
||||
if [[ -x scripts/verify_browser_runtime.sh ]]; then
|
||||
bash scripts/verify_browser_runtime.sh "$FRONTEND_URL"
|
||||
fi
|
||||
FRONTEND_URL="$FRONTEND_URL" \
|
||||
DEPLOY_GEOINTEL_INSTALL_AI="${DEPLOY_GEOINTEL_INSTALL_AI:-}" \
|
||||
bash deploy/unraid/deploy-release.sh
|
||||
REMOTE_SCRIPT
|
||||
|
||||
@@ -106,6 +106,12 @@ ${PYTHON_BIN} -m compileall backend/app
|
||||
(cd frontend && npm run typecheck)
|
||||
(cd frontend && npm run build)
|
||||
bash -n scripts/live_migration_smoke.sh
|
||||
bash -n scripts/deploy_tower.sh
|
||||
bash -n scripts/verify_release_fresh_install.sh
|
||||
bash -n deploy/unraid/all-in-one-start.sh
|
||||
bash -n deploy/unraid/run-dockerman-container.sh
|
||||
bash -n deploy/unraid/deploy-release.sh
|
||||
bash -n deploy/unraid/rollback-dockerman-container.sh
|
||||
bash -n scripts/verify_browser_runtime.sh
|
||||
bash -n scripts/verify_demo_export_workflow.sh
|
||||
bash -n scripts/verify_demo_raster_workflow.sh
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE="${1:-geointel-all-in-one:latest}"
|
||||
NAME="geointel-fresh-smoke-$(date +%s)-$$"
|
||||
ROOT="$(mktemp -d "${TMPDIR:-/tmp}/geointel-fresh-smoke.XXXXXX")"
|
||||
PASSWORD="fresh-smoke-$(cat /proc/sys/kernel/random/uuid)"
|
||||
|
||||
cleanup() {
|
||||
docker rm -f "$NAME" >/dev/null 2>&1 || true
|
||||
case "$ROOT" in
|
||||
"${TMPDIR:-/tmp}"/geointel-fresh-smoke.*)
|
||||
rm -rf -- "$ROOT"
|
||||
;;
|
||||
*)
|
||||
echo "Refusing to remove unexpected smoke path: ${ROOT}" >&2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
docker image inspect "$IMAGE" >/dev/null
|
||||
mkdir -p "$ROOT/postgres-data" "$ROOT/storage" "$ROOT/models"
|
||||
|
||||
docker run -d \
|
||||
--name "$NAME" \
|
||||
-p 127.0.0.1::80 \
|
||||
-e GEOINTEL_POSTGRES_DB=geointel_fresh_smoke \
|
||||
-e GEOINTEL_POSTGRES_USER=geointel_fresh_smoke \
|
||||
-e GEOINTEL_POSTGRES_PASSWORD="$PASSWORD" \
|
||||
-e GEOINTEL_MAX_UPLOAD_MB=500 \
|
||||
-e OLLAMA_ENABLED=false \
|
||||
-v "$ROOT/postgres-data:/var/lib/postgresql/data" \
|
||||
-v "$ROOT/storage:/app/storage" \
|
||||
-v "$ROOT/models:/app/models" \
|
||||
"$IMAGE" >/dev/null
|
||||
|
||||
for attempt in $(seq 1 90); do
|
||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$NAME" 2>/dev/null || true)"
|
||||
if [ "$status" = "healthy" ]; then
|
||||
break
|
||||
fi
|
||||
if [ "$status" = "unhealthy" ] || [ "$status" = "exited" ] || [ "$status" = "dead" ]; then
|
||||
docker logs --tail 120 "$NAME" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ "${status:-}" != "healthy" ]; then
|
||||
echo "Fresh-install container did not become healthy." >&2
|
||||
docker logs --tail 120 "$NAME" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
host_port="$(docker port "$NAME" 80/tcp | awk -F: 'NR == 1 {print $NF}')"
|
||||
curl -fsS "http://127.0.0.1:${host_port}/health/ready" >/dev/null
|
||||
curl -fsS "http://127.0.0.1:${host_port}/api/v1/system/capabilities" >/dev/null
|
||||
docker exec "$NAME" python -m alembic heads
|
||||
|
||||
echo "Fresh install passed for ${IMAGE} on isolated storage ${ROOT}."
|
||||
Reference in New Issue
Block a user