diff --git a/backend/tests/test_kamal_deploy_config.py b/backend/tests/test_kamal_deploy_config.py index ba99aed..1fc21bf 100644 --- a/backend/tests/test_kamal_deploy_config.py +++ b/backend/tests/test_kamal_deploy_config.py @@ -49,3 +49,12 @@ def test_kamal_cron_role_and_crontab_are_wired(): assert "cron" in dockerfile assert "COPY config/crontab ./config/crontab" in dockerfile assert "start_cron.sh" in dockerfile + + +def test_kamal_accessory_directories_preserve_live_production_binds(): + base_config = _load_deploy_config("config/deploy.yml") + accessories = base_config["accessories"] + + assert accessories["elasticsearch"]["directories"] == ["esdata:/usr/share/elasticsearch/data"] + assert accessories["postgres"]["directories"] == ["pgdata:/var/lib/postgresql/data"] + assert accessories["redis"]["directories"] == ["redisdata:/data"] diff --git a/config/deploy.yml b/config/deploy.yml index e3fcd02..f741a48 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -98,7 +98,9 @@ accessories: # only listen on localhost port: "127.0.0.1:9200:9200" directories: - - /var/lib/opengeometadata-api/elasticsearch:/usr/share/elasticsearch/data + # Keep this relative source name: Kamal resolves it to the existing + # /home/ewlarson/ogm-api-elasticsearch/esdata production bind. + - esdata:/usr/share/elasticsearch/data postgres: image: postgres:15 @@ -115,11 +117,15 @@ accessories: files: - config/init.sql:/docker-entrypoint-initdb.d/setup.sql directories: - - /var/lib/opengeometadata-api/postgres:/var/lib/postgresql/data + # Keep this relative source name: Kamal resolves it to the existing + # /home/ewlarson/ogm-api-postgres/pgdata production bind. + - pgdata:/var/lib/postgresql/data redis: image: redis:7.2 roles: [web] cmd: sh -lc 'redis-server --appendonly yes --protected-mode yes --bind 0.0.0.0' directories: - - /var/lib/opengeometadata-api/redis:/data + # Keep this relative source name: Kamal resolves it to the existing + # /home/ewlarson/ogm-api-redis/redisdata production bind. + - redisdata:/data diff --git a/docs/deployment.md b/docs/deployment.md index 74fe790..9cee544 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -222,7 +222,8 @@ elasticsearch: - Single-node configuration - 2GB heap size - Security disabled (bound to localhost only) -- Data persisted at `/var/lib/opengeometadata-api/elasticsearch` on the host +- Kamal directory source `esdata`, currently resolved to + `/home/ewlarson/ogm-api-elasticsearch/esdata` on the production host ### PostgreSQL @@ -237,7 +238,8 @@ postgres: - Existing PostgreSQL 15 accessory used by the current production deployment - Initialized via `config/init.sql` - FAST vector embeddings are disabled in Kamal by default because the current production database image does not expose the `vector` extension -- Data persisted at `/var/lib/opengeometadata-api/postgres` on the host +- Kamal directory source `pgdata`, currently resolved to + `/home/ewlarson/ogm-api-postgres/pgdata` on the production host ### Redis @@ -251,7 +253,8 @@ redis: - Append-only file (AOF) persistence enabled - Current production accessory runs without Redis AUTH; the app should omit `REDIS_PASSWORD` unless/until the accessory is explicitly recreated with auth -- Data persisted at `/var/lib/opengeometadata-api/redis` on the host +- Kamal directory source `redisdata`, currently resolved to + `/home/ewlarson/ogm-api-redis/redisdata` on the production host ## Deployment Commands @@ -495,8 +498,9 @@ PostgreSQL is running; copying its live data directory is not: ssh ewlarson@ogm.geo4lib.app # Create a restricted backup directory and a custom-format dump -ogm_backup_dir=/var/backups/opengeometadata-api -sudo install -d -m 700 -o "$(id -un)" -g "$(id -gn)" "$ogm_backup_dir" +umask 077 +ogm_backup_dir="$HOME/ogm-api-backups" +install -d -m 700 "$ogm_backup_dir" docker exec ogm-api-postgres \ pg_dump --format=custom -U ogm_api_user btaa_ogm_api \ > "$ogm_backup_dir/postgres-$(date -u +%Y%m%dT%H%M%SZ).dump" @@ -514,7 +518,7 @@ record its identifier before a deployment or migration window. ```bash # Copy a verified custom-format backup to the server -scp postgres-YYYYMMDDTHHMMSSZ.dump ewlarson@ogm.geo4lib.app:/var/backups/opengeometadata-api/ +scp postgres-YYYYMMDDTHHMMSSZ.dump ewlarson@ogm.geo4lib.app:ogm-api-backups/ # SSH to server ssh ewlarson@ogm.geo4lib.app @@ -522,7 +526,7 @@ ssh ewlarson@ogm.geo4lib.app # Restore only during an approved recovery window docker exec -i ogm-api-postgres \ pg_restore --clean --if-exists -U ogm_api_user -d btaa_ogm_api \ - < /var/backups/opengeometadata-api/postgres-YYYYMMDDTHHMMSSZ.dump + < "$HOME/ogm-api-backups/postgres-YYYYMMDDTHHMMSSZ.dump" ``` ## Elasticsearch Operations @@ -678,15 +682,20 @@ For automated deployments via GitHub Actions or similar: ## Host Directories & Data Persistence -Kamal bind-mounts three persistent host directories. These paths are stable -production identity and must not change during a repository or image transfer: +Kamal bind-mounts three persistent host directories. The relative directory +sources in `config/deploy.yml` are stable production identity and must not +change during a repository or image transfer: -- `/var/lib/opengeometadata-api/elasticsearch`: Elasticsearch index data -- `/var/lib/opengeometadata-api/postgres`: PostgreSQL database files -- `/var/lib/opengeometadata-api/redis`: Redis append-only cache data +- `esdata` -> `/home/ewlarson/ogm-api-elasticsearch/esdata`: Elasticsearch index data +- `pgdata` -> `/home/ewlarson/ogm-api-postgres/pgdata`: PostgreSQL database files +- `redisdata` -> `/home/ewlarson/ogm-api-redis/redisdata`: Redis append-only cache data These directories persist across deployments and container restarts. They are -not Docker named volumes; `docker volume` commands do not back them up. +not Docker named volumes; `docker volume` commands do not back them up. Kamal +derives the resolved host paths from the deployment user, service, accessory, +and relative source name. Do not replace the relative sources with new absolute +paths unless the data is deliberately migrated in a separate maintenance +window. ### Backing Up Volumes @@ -695,7 +704,7 @@ not Docker named volumes; `docker volume` commands do not back them up. ssh ewlarson@ogm.geo4lib.app # Confirm the configured directories and mounts -sudo find /var/lib/opengeometadata-api -maxdepth 1 -type d -print +find "$HOME" -maxdepth 2 -type d -name 'ogm-api-*' -print docker inspect ogm-api-postgres --format '{{json .Mounts}}' ``` diff --git a/docs/repository_transfer.md b/docs/repository_transfer.md index 769c230..feaf0ec 100644 --- a/docs/repository_transfer.md +++ b/docs/repository_transfer.md @@ -60,13 +60,19 @@ The following values are intentionally frozen through the repository transfer: | Registry and login owner | `ghcr.io`, `ewlarson` | | Elasticsearch index | `opengeometadata_api` | | PostgreSQL database | `btaa_ogm_api` | -| Elasticsearch volume | `/var/lib/opengeometadata-api/elasticsearch` | -| PostgreSQL volume | `/var/lib/opengeometadata-api/postgres` | -| Redis volume | `/var/lib/opengeometadata-api/redis` | +| Elasticsearch directory source | `esdata` (resolves to `/home/ewlarson/ogm-api-elasticsearch/esdata`) | +| PostgreSQL directory source | `pgdata` (resolves to `/home/ewlarson/ogm-api-postgres/pgdata`) | +| Redis directory source | `redisdata` (resolves to `/home/ewlarson/ogm-api-redis/redisdata`) | The legacy PostgreSQL database name is persistent identity, not public project branding. Rename it only in a separate, backed-up database migration. +The relative Kamal directory source names are also persistent identity. Kamal +expands them under the deployment user's home directory using the service and +accessory names. Replacing them with a different absolute host path would make +an accessory reboot mount a different data directory, even though an ordinary +repository transfer does not restart the existing containers. + Run the local invariant checks before and after every preparation commit: ```bash diff --git a/docs/repository_transfer_rehearsal.md b/docs/repository_transfer_rehearsal.md index abbc2c6..893f11f 100644 --- a/docs/repository_transfer_rehearsal.md +++ b/docs/repository_transfer_rehearsal.md @@ -88,3 +88,22 @@ Before the actual transfer: 6. rewrite a fresh mirror of the frozen GitHub repository and require `make transfer-readiness-full` to pass in a checkout of the result; and 7. follow the transfer and rollback sequence in `repository_transfer.md`. + +## Change-window preflight correction + +The 2026-08-20 live preflight found that the running accessories use the +original Kamal-resolved bind paths under `/home/ewlarson/ogm-api-*`, while the +prepared configuration incorrectly named new `/var/lib/opengeometadata-api/*` +sources. No running container had adopted the new paths. The transfer was +paused before branch deletion or history rewriting, and the configuration was +corrected to preserve the original relative directory sources `esdata`, +`pgdata`, and `redisdata`. + +The same preflight confirmed that OpenGeoMetadata permits Actions for all +repositories, standard hosted runners are enabled, no repository in the +organization occupies the BTAA fork network, and the personal GHCR package is +private, remains personal-account scoped, and is not linked to this repository. +A current logical PostgreSQL dump completed successfully, its permissions were +restricted to the deployment user, and `pg_restore --list` validated its +catalog. The private change record holds the backup identifier, byte size, +checksum, running image digest, and exact live mount inspection output. diff --git a/scripts/verify_transfer_readiness.sh b/scripts/verify_transfer_readiness.sh index 6a8d851..dbc1bb1 100755 --- a/scripts/verify_transfer_readiness.sh +++ b/scripts/verify_transfer_readiness.sh @@ -62,9 +62,9 @@ expect_exact_line config/deploy.yml "image: $expected_image" 'production image i expect_exact_line config/deploy.yml ' host: ogm.geo4lib.app' 'production proxy hostname is unchanged' expect_exact_line config/deploy.yml ' ELASTICSEARCH_INDEX: opengeometadata_api' 'Elasticsearch index is unchanged' expect_exact_line config/deploy.yml ' POSTGRES_DB: btaa_ogm_api' 'PostgreSQL database is unchanged' -expect_exact_line config/deploy.yml ' - /var/lib/opengeometadata-api/elasticsearch:/usr/share/elasticsearch/data' 'Elasticsearch volume is unchanged' -expect_exact_line config/deploy.yml ' - /var/lib/opengeometadata-api/postgres:/var/lib/postgresql/data' 'PostgreSQL volume is unchanged' -expect_exact_line config/deploy.yml ' - /var/lib/opengeometadata-api/redis:/data' 'Redis volume is unchanged' +expect_exact_line config/deploy.yml ' - esdata:/usr/share/elasticsearch/data' 'Elasticsearch volume is unchanged' +expect_exact_line config/deploy.yml ' - pgdata:/var/lib/postgresql/data' 'PostgreSQL volume is unchanged' +expect_exact_line config/deploy.yml ' - redisdata:/data' 'Redis volume is unchanged' for role in web worker cron; do if grep -Eq "^ ${role}:$" config/deploy.yml; then