From f944fe4bbdfb55ebc6a30e450336cf8db1922b8e Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Sat, 8 Aug 2026 09:46:08 +0200 Subject: [PATCH] =?UTF-8?q?feat(postgres)=20=E2=80=94=20PostGIS,=20pos?= =?UTF-8?q?=C3=A9=20par=20le=20playbook=20et=20non=20par=20une=20image=20c?= =?UTF-8?q?ustom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kadans doit ranger le contour d'un quartier en vraie géométrie (geometry(MultiPolygon,4326), ST_Contains, index GiST). L'extension n'existait nulle part : mesuré sur pi2, `pg_available_extensions` ne rendait AUCUNE ligne `postgis%`. Arbitrage fondateur (2026-08-08) : on garde `postgres:16.3-alpine` et on pose l'extension par Ansible, comme le playbook pose déjà les bases et le rôle pgbouncer. Pas d'image custom. ⚠ POURQUOI LE RECALAGE DE CHEMINS N'EST PAS FACULTATIF — mesuré, arm64. `apk add postgis` SEUL réussit, et `CREATE EXTENSION postgis` échoue quand même : ERROR: extension "postgis" is not available DETAIL: Could not open extension control file "/usr/local/share/postgresql/extension/postgis.control" Le paquet Alpine vise la disposition d'Alpine (/usr/share/postgresql16, /usr/lib/postgresql16) ; l'image officielle compile le serveur dans /usr/local. Les fichiers sont là, le serveur regarde ailleurs. Après recalage : PostGIS 3.4 USE_GEOS=1 USE_PROJ=1, et un polygone lyonnais qui fait l'aller-retour ST_GeomFromText → ST_AsGeoJSON. Ne pas « simplifier » en un `apk add` nu : la simulation dit OK, l'installation dit OK, et l'extension reste inutilisable. ⚠ INSTALLATION PAR CONTENEUR, PAS PAR VOLUME. `apk add` écrit dans la couche inscriptible : recréer le conteneur efface PostGIS pendant que les données gardent leurs colonnes géométriques — toute requête spatiale casse jusqu'au prochain passage du playbook. D'où l'ordre (déploiement compose PUIS installation), l'idempotence, et surtout la tâche de vérification. La vérification ne se contente pas d'un code de retour : elle exige que la base rende USE_GEOS=1 ET un vrai Point GeoJSON avec son SRID. Un bouchon qui répondrait une chaîne vide passerait un simple `rc == 0` et ne prouverait rien — un playbook vert sur une extension absente ferait atterrir le symptôme dans Kadans, des jours plus tard, déguisé en bug applicatif. Vérifié sur le conteneur RÉEL sans le modifier : `apk add --simulate` résout postgis 3.4.2-r2, et `pg_config` y rend bien les deux chemins que les variables supposent. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01J4UE4AmX5PAMN6c6Q6Fey9 --- .../group_vars/postgres/postgres.yml | 43 +++++++++++++- .../factory/playbooks/setup/postgres.yml | 57 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/ansible/arcodange/factory/inventory/group_vars/postgres/postgres.yml b/ansible/arcodange/factory/inventory/group_vars/postgres/postgres.yml index 5874e31..ba2999d 100644 --- a/ansible/arcodange/factory/inventory/group_vars/postgres/postgres.yml +++ b/ansible/arcodange/factory/inventory/group_vars/postgres/postgres.yml @@ -22,4 +22,45 @@ postgres: pgbouncer: auth_user: &pgbouncer_auth pgbouncer_auth - auth_user_password: *pgbouncer_auth \ No newline at end of file + auth_user_password: *pgbouncer_auth + +# PostGIS — spatial types for the databases that ask for it. +# +# Kadans stores neighbourhood ("zone") outlines as real geometry, so it needs +# geometry(MultiPolygon,4326), ST_Contains and a GiST index. The image stays +# `postgres:16.3-alpine`: the extension is installed INTO the running container +# by setup/postgres.yml, exactly like the pgbouncer role and the app databases +# are created there. No custom image (founder's call, 2026-08-08). +# +# WARNING — this is a per-CONTAINER install, not a per-VOLUME one. `apk add` +# writes to the container's writable layer, so recreating the container (image +# change, `docker compose up --force-recreate`) REMOVES PostGIS while the data +# keeps its geometry columns — every spatial query then fails until this +# playbook runs again. The install task is therefore idempotent and runs after +# every compose deploy, and it is the reason `postgis_verifier` exists below: +# a silent absence would look like an application bug. +postgis: + # Only these databases get the extension. Adding one here is the whole change. + databases: + - kadans + # The Alpine package. Pinned to a MAJOR line, not a patch: postgis 3.x + # upgrades within a major are ABI-compatible with a given PostgreSQL major. + paquet: postgis + # ⚠ WHY THE COPY STEP EXISTS — measured on pi2 (arm64), 2026-08-08. + # `apk add postgis` alone SUCCEEDS and `CREATE EXTENSION postgis` still fails: + # + # ERROR: extension "postgis" is not available + # DETAIL: Could not open extension control file + # "/usr/local/share/postgresql/extension/postgis.control" + # + # Alpine's package targets Alpine's own PostgreSQL layout + # (/usr/share/postgresql16, /usr/lib/postgresql16), while the official + # `postgres:16-alpine` image builds the server into /usr/local. The files are + # on disk, the server looks elsewhere. Relocating them makes it work — proven + # in a throwaway container: PostGIS 3.4 USE_GEOS=1 USE_PROJ=1, and a Lyon + # polygon round-tripping through ST_GeomFromText/ST_AsGeoJSON. + # + # Do NOT "simplify" this to a bare `apk add`. An `apk add --simulate` reports + # OK, the install reports OK, and the extension is still unusable. + source_partagee: /usr/share/postgresql16/extension + source_lib: /usr/lib/postgresql16 \ No newline at end of file diff --git a/ansible/arcodange/factory/playbooks/setup/postgres.yml b/ansible/arcodange/factory/playbooks/setup/postgres.yml index a066c63..dce7d32 100644 --- a/ansible/arcodange/factory/playbooks/setup/postgres.yml +++ b/ansible/arcodange/factory/playbooks/setup/postgres.yml @@ -25,6 +25,63 @@ applications_databases: gitea: "{{ gitea_database }}" + # ── PostGIS ──────────────────────────────────────────────────────────── + # Installed INTO the running container rather than baked into a custom + # image (founder's call, 2026-08-08). See group_vars/postgres/postgres.yml + # for why the copy step is not optional, and for the durability caveat. + # + # Runs after the compose deploy above ON PURPOSE: if that task recreated + # the container, the writable layer is fresh and PostGIS is gone with it. + # This is what makes the pair (deploy, install) safe to replay. + - name: Install PostGIS into the Postgres container + ansible.builtin.shell: | + set -eu + docker exec {{ postgres_container_name }} sh -c ' + set -eu + apk add --no-cache {{ postgis.paquet }} >/dev/null + cp -r {{ postgis.source_partagee }}/* "$(pg_config --sharedir)/extension/" + cp -r {{ postgis.source_lib }}/*.so "$(pg_config --pkglibdir)/" + ' + # `apk add` is idempotent and the copies overwrite identical files, so a + # replay changes nothing observable. We do not pretend to detect that: + # claiming `changed_when: false` outright would hide a REAL first install. + register: postgis_installation + changed_when: "'Installing' in postgis_installation.stdout" + + - name: Enable PostGIS on the databases that need it + ansible.builtin.shell: | + docker exec {{ postgres_container_name }} \ + psql -U postgres -d {{ item }} -tAc 'CREATE EXTENSION IF NOT EXISTS postgis;' + loop: "{{ postgis.databases }}" + register: postgis_activation + changed_when: "'CREATE EXTENSION' in postgis_activation.stdout" + + # ⚠ THE HALF THAT MATTERS. Without it, a botched install leaves a green + # playbook and an application that fails at its first spatial query — the + # symptom would land in Kadans, days later, looking like an app bug. + # We ask the database itself, and we FAIL on anything unexpected. + - name: Verify PostGIS answers on every database + ansible.builtin.shell: | + docker exec {{ postgres_container_name }} psql -U postgres -d {{ item }} -tAc \ + "SELECT postgis_version() || ' | ' || ST_AsGeoJSON(ST_SetSRID(ST_Point(4.83, 45.76), 4326));" + loop: "{{ postgis.databases }}" + register: postgis_verifier + changed_when: false + # Not just "the command exited 0": a real geometry must come back with + # the SRID applied. A stub that answered an empty string would pass a + # bare rc check — and prove nothing. + failed_when: >- + postgis_verifier.rc != 0 + or 'USE_GEOS=1' not in postgis_verifier.stdout + or '"type":"Point"' not in postgis_verifier.stdout + + - name: Report the PostGIS version in use + ansible.builtin.debug: + msg: "PostGIS on {{ item.item }} → {{ item.stdout | trim }}" + loop: "{{ postgis_verifier.results }}" + loop_control: + label: "{{ item.item }}" + - name: Create auth_user for pgbouncer (connection pool component) ansible.builtin.shell: | docker exec -it {{ postgres_container_name }} psql -U postgres -d {{ database }} -tc "{{ pg_instruction.replace('$','\$') }}" -- 2.54.0