Démarrage de la pile par un simple docker compose up
La pile de test de la branche ne démarrait pas : « Connexion à la base impossible ». Trois causes cumulées, toutes corrigées. 1. PTA_DB_PORT et DB_PORT étaient passés à 9306. Ces variables désignent le port joint À L'INTÉRIEUR du réseau Docker, où MariaDB écoute sur 3306, et non un port publié sur l'hôte : la connexion était refusée. Elles reviennent à 3306, et le port hôte 9306 est désormais réellement publié par le service db (sur la boucle locale), pour un client SQL externe. 2. Le service db-migrate manquait sur la branche. Le schéma d'initialisation ne contient aucune des tables et vues d'accès V7.8 : sur un volume neuf, l'application refusait de démarrer. db-migrate applique maintenant tous les database/migration_*.sql, dont celle du tableau de bord V7.9 — plus aucune étape SQL manuelle. 3. Le défaut de PTA_DB_PASS (change_me) différait de celui de MARIADB_PASSWORD (pta). Sans fichier .env — et ce répertoire n'en a pas, il est ignoré par git — l'authentification échouait. Les deux défauts sont alignés. Le bandeau du tableau de bord ne prétend plus que « tous les lieux ont des horaires attendus » quand aucun lieu n'est enregistré. Vérifié : volume détruit puis « docker compose up -d --build » seul suffit à obtenir les onze pages en 200, l'atterrissage sur le tableau de bord et les deux migrations appliquées ; un second up les ignore ; la base répond sur 127.0.0.1:9306 ; la pile principale sur 8080 est restée intacte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
19
CLAUDE.md
19
CLAUDE.md
@@ -40,8 +40,23 @@ find . -name '*.php' -not -path './.git/*' -exec php -l {} \;
|
|||||||
```
|
```
|
||||||
|
|
||||||
Le schéma `database/pta_prod_schema_mariadb.sql` n'est joué **qu'à la création initiale du volume**
|
Le schéma `database/pta_prod_schema_mariadb.sql` n'est joué **qu'à la création initiale du volume**
|
||||||
MariaDB (`docker-entrypoint-initdb.d`). Sur une base existante, toute évolution de schéma doit être
|
MariaDB (`docker-entrypoint-initdb.d`) : il ne contient que le socle métier, et **aucune** des tables
|
||||||
appliquée manuellement en SQL — il n'y a pas d'outil de migration.
|
ou vues ajoutées par les migrations.
|
||||||
|
|
||||||
|
Les fichiers `database/migration_*.sql` sont appliqués automatiquement au démarrage par le service
|
||||||
|
jetable `db-migrate` (`docker/db/apply-migrations.sh`), sur base neuve comme sur base existante.
|
||||||
|
Le service `php` attend sa sortie en succès : une migration en échec empêche l'application de
|
||||||
|
tourner sur une base incomplète, plutôt que de la laisser renvoyer une erreur 500 à l'accueil.
|
||||||
|
|
||||||
|
Le script tient un registre `schema_migration` (fichier + empreinte MD5) : un fichier déjà appliqué
|
||||||
|
et inchangé est ignoré, un fichier modifié est rejoué. **Toute migration doit donc rester
|
||||||
|
idempotente** (`CREATE TABLE IF NOT EXISTS`, `CREATE OR REPLACE VIEW`,
|
||||||
|
`INSERT … ON DUPLICATE KEY UPDATE`). Les fichiers sont pris dans l'ordre alphabétique : nommer une
|
||||||
|
migration `migration_v7_10_…` la ferait passer **avant** `migration_v7_9_…`.
|
||||||
|
|
||||||
|
`PTA_DB_PORT`, `DB_PORT` et `PMA_PORT` du compose sont les ports joints **à l'intérieur** du réseau
|
||||||
|
Docker, où MariaDB écoute sur 3306. Ce ne sont pas les ports publiés sur l'hôte (`APP_PORT`,
|
||||||
|
`PMA_PORT` de `.env`) : les modifier coupe l'accès à la base.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -70,22 +70,25 @@ L'endpoint `api/dashboard_summary.php` est gardé par `requireService()` : 403 p
|
|||||||
|
|
||||||
## Migration
|
## Migration
|
||||||
|
|
||||||
```sql
|
Aucun geste manuel : `database/migration_v7_9_tableau_de_bord.sql` est appliqué au démarrage par le
|
||||||
SOURCE database/migration_v7_9_tableau_de_bord.sql;
|
service `db-migrate`, qui joue tous les `database/migration_*.sql` dans l'ordre. Un simple
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Elle insère une seule ligne dans `acces_role_page`. **Elle doit être jouée en même temps que le
|
suffit, sur base neuve comme sur base existante. La migration insère une seule ligne dans
|
||||||
déploiement du code**, dans un sens comme dans l'autre :
|
`acces_role_page` et est idempotente (`ON DUPLICATE KEY UPDATE`).
|
||||||
|
|
||||||
- code déployé, migration non jouée → l'onglet n'apparaît pas, le Service Enfance atterrit comme
|
Si la migration n'était pas jouée, l'onglet n'apparaîtrait pas, le Service Enfance atterrirait comme
|
||||||
avant sur l'écran de saisie, et `dashboard.php` renvoie vers l'accueil. L'application reste
|
avant sur l'écran de saisie et `dashboard.php` renverrait vers l'accueil : l'application resterait
|
||||||
entièrement fonctionnelle, simplement sans tableau de bord ;
|
entièrement fonctionnelle, simplement sans tableau de bord.
|
||||||
- migration jouée, code non déployé → l'onglet apparaît et pointe vers un fichier inexistant.
|
|
||||||
|
|
||||||
Retour arrière :
|
Retour arrière :
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
DELETE FROM acces_role_page WHERE role = 'SERVICE_ENFANCE' AND page = 'dashboard';
|
DELETE FROM acces_role_page WHERE role = 'SERVICE_ENFANCE' AND page = 'dashboard';
|
||||||
|
DELETE FROM schema_migration WHERE fichier = 'migration_v7_9_tableau_de_bord.sql';
|
||||||
```
|
```
|
||||||
|
|
||||||
## Fichiers
|
## Fichiers
|
||||||
@@ -102,6 +105,8 @@ DELETE FROM acces_role_page WHERE role = 'SERVICE_ENFANCE' AND page = 'dashboard
|
|||||||
|
|
||||||
| Modifié | Raison |
|
| Modifié | Raison |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| `docker-compose.yml` | service `db-migrate`, port interne de la base rétabli à 3306, défaut de mot de passe aligné entre `db` et `php` |
|
||||||
|
| `docker/db/apply-migrations.sh` | application automatique et idempotente des migrations |
|
||||||
| `app/Core/Access.php` | `homeUrl()` vise le tableau de bord, avec repli si la migration manque |
|
| `app/Core/Access.php` | `homeUrl()` vise le tableau de bord, avec repli si la migration manque |
|
||||||
| `app/Controllers/PageController.php` | méthode `dashboard()` et sous-titre propre à chaque page |
|
| `app/Controllers/PageController.php` | méthode `dashboard()` et sous-titre propre à chaque page |
|
||||||
| `app/Views/layouts/header.php` | navigation regroupée, sous-titre contextuel |
|
| `app/Views/layouts/header.php` | navigation regroupée, sous-titre contextuel |
|
||||||
|
|||||||
@@ -151,9 +151,12 @@
|
|||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCoverage(coverage, week) {
|
function renderCoverage(coverage, week, perimetre) {
|
||||||
const gapCount = Number(coverage.gap_count || 0);
|
const gapCount = Number(coverage.gap_count || 0);
|
||||||
const unconfigured = Number(coverage.unconfigured_count || 0);
|
const unconfigured = Number(coverage.unconfigured_count || 0);
|
||||||
|
// Sans aucun lieu enregistré, « tous les minimums sont atteints » serait
|
||||||
|
// vrai mais trompeur : il n'y a simplement rien à contrôler.
|
||||||
|
const sansLieu = Number(perimetre?.structures || 0) === 0;
|
||||||
|
|
||||||
setTileTone(elements.coverageTile, gapCount);
|
setTileTone(elements.coverageTile, gapCount);
|
||||||
setTileTone(elements.setupTile, unconfigured);
|
setTileTone(elements.setupTile, unconfigured);
|
||||||
@@ -161,14 +164,14 @@
|
|||||||
if (elements.coverageCount) elements.coverageCount.textContent = String(gapCount);
|
if (elements.coverageCount) elements.coverageCount.textContent = String(gapCount);
|
||||||
if (elements.coverageDetail) {
|
if (elements.coverageDetail) {
|
||||||
elements.coverageDetail.textContent = gapCount === 0
|
elements.coverageDetail.textContent = gapCount === 0
|
||||||
? 'Tous les minimums sont atteints'
|
? (sansLieu ? 'Aucun lieu d’affectation enregistré' : 'Tous les minimums sont atteints')
|
||||||
: `${formatDuration(Number(coverage.gap_minutes || 0))} non couvertes · `
|
: `${formatDuration(Number(coverage.gap_minutes || 0))} non couvertes · `
|
||||||
+ `${coverage.structure_count} ${plural(coverage.structure_count, 'lieu concerné', 'lieux concernés')}`;
|
+ `${coverage.structure_count} ${plural(coverage.structure_count, 'lieu concerné', 'lieux concernés')}`;
|
||||||
}
|
}
|
||||||
if (elements.setupCount) elements.setupCount.textContent = String(unconfigured);
|
if (elements.setupCount) elements.setupCount.textContent = String(unconfigured);
|
||||||
if (elements.setupDetail) {
|
if (elements.setupDetail) {
|
||||||
elements.setupDetail.textContent = unconfigured === 0
|
elements.setupDetail.textContent = unconfigured === 0
|
||||||
? 'Tous les lieux ont des horaires attendus'
|
? (sansLieu ? 'Aucun lieu à paramétrer pour l’instant' : 'Tous les lieux ont des horaires attendus')
|
||||||
: 'Aucun minimum d’agents n’y est contrôlé';
|
: 'Aucun minimum d’agents n’y est contrôlé';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +219,9 @@
|
|||||||
if (elements.quotaCount) elements.quotaCount.textContent = String(overCount);
|
if (elements.quotaCount) elements.quotaCount.textContent = String(overCount);
|
||||||
if (elements.quotaDetail) {
|
if (elements.quotaDetail) {
|
||||||
elements.quotaDetail.textContent = overCount === 0
|
elements.quotaDetail.textContent = overCount === 0
|
||||||
? `Aucun dépassement sur ${quota.annee}`
|
? (Number(quota.agent_count || 0) === 0
|
||||||
|
? 'Aucun agent actif à suivre'
|
||||||
|
: `Aucun dépassement sur ${quota.annee}`)
|
||||||
: `Sur ${quota.agent_count} ${plural(quota.agent_count, 'agent suivi', 'agents suivis')} en ${quota.annee}`;
|
: `Sur ${quota.agent_count} ${plural(quota.agent_count, 'agent suivi', 'agents suivis')} en ${quota.annee}`;
|
||||||
}
|
}
|
||||||
if (elements.quotaTile) {
|
if (elements.quotaTile) {
|
||||||
@@ -301,7 +306,7 @@
|
|||||||
|
|
||||||
renderWeek(data.semaine);
|
renderWeek(data.semaine);
|
||||||
renderPending(data.validation || {});
|
renderPending(data.validation || {});
|
||||||
renderCoverage(data.couverture || {}, data.semaine);
|
renderCoverage(data.couverture || {}, data.semaine, data.perimetre);
|
||||||
renderQuota(data.quota || {});
|
renderQuota(data.quota || {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage(error.message, 'error');
|
setMessage(error.message, 'error');
|
||||||
|
|||||||
@@ -1,7 +1,36 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Pile de test de la branche V7.9.
|
||||||
|
# ============================================================================
|
||||||
|
# Noms de conteneurs, réseau, volume et ports publiés sont suffixés « test » :
|
||||||
|
# cette pile tourne EN PARALLÈLE de la pile principale (pta-db, pta-php, …)
|
||||||
|
# sans lui prendre ni ses conteneurs, ni ses données, ni ses ports.
|
||||||
|
#
|
||||||
|
# `docker compose up -d --build` suffit : le service db-migrate applique
|
||||||
|
# lui-même les migrations SQL, dont celle du tableau de bord V7.9.
|
||||||
|
#
|
||||||
|
# Ce répertoire n'a volontairement pas de fichier .env (il est ignoré par git) :
|
||||||
|
# les valeurs par défaut ci-dessous s'appliquent donc telles quelles, et les
|
||||||
|
# ports publiés (9080 / 9081 / 9306) ne peuvent pas entrer en conflit avec la
|
||||||
|
# pile principale (8080 / 8081). Si vous y copiez le .env de la pile principale,
|
||||||
|
# pensez à y changer APP_PORT et PMA_PORT.
|
||||||
|
#
|
||||||
|
# ATTENTION — deux familles de ports à ne pas confondre :
|
||||||
|
#
|
||||||
|
# · ports PUBLIÉS sur la machine hôte, dans les blocs `ports:` :
|
||||||
|
# 9080 application 9081 phpMyAdmin 9306 MariaDB
|
||||||
|
# Ceux-là se changent librement, ce sont eux qu'on tape dans un navigateur
|
||||||
|
# ou dans un client SQL.
|
||||||
|
#
|
||||||
|
# · ports JOINTS À L'INTÉRIEUR du réseau Docker, dans les blocs
|
||||||
|
# `environment:` (PTA_DB_PORT, DB_PORT, PMA_PORT) : MariaDB y écoute
|
||||||
|
# TOUJOURS sur 3306, quel que soit le port publié. Y mettre autre chose
|
||||||
|
# coupe l'accès à la base et l'application renvoie « Connexion à la base
|
||||||
|
# impossible ».
|
||||||
|
# ============================================================================
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: mariadb:11.4
|
image: mariadb:11.4
|
||||||
container_name: pta-db
|
container_name: pta-db-test
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
MARIADB_DATABASE: ${DB_NAME:-pta}
|
MARIADB_DATABASE: ${DB_NAME:-pta}
|
||||||
@@ -13,9 +42,14 @@ services:
|
|||||||
- --character-set-server=utf8mb4
|
- --character-set-server=utf8mb4
|
||||||
- --collation-server=utf8mb4_unicode_ci
|
- --collation-server=utf8mb4_unicode_ci
|
||||||
volumes:
|
volumes:
|
||||||
- mariadb_data:/var/lib/mysql
|
- mariadb_data_test:/var/lib/mysql
|
||||||
# Ce fichier n'est exécuté QUE lors de l'initialisation d'un volume vide.
|
# Ce fichier n'est exécuté QUE lors de l'initialisation d'un volume vide.
|
||||||
- ./database/pta_prod_schema_mariadb.sql:/docker-entrypoint-initdb.d/01-pta-schema.sql:ro
|
- ./database/pta_prod_schema_mariadb.sql:/docker-entrypoint-initdb.d/01-pta-schema.sql:ro
|
||||||
|
ports:
|
||||||
|
# Base joignable depuis la machine hôte pour un client SQL externe, sur la
|
||||||
|
# boucle locale uniquement. À l'intérieur du réseau Docker, les services
|
||||||
|
# continuent de joindre MariaDB sur son port natif 3306.
|
||||||
|
- "${DB_BIND:-127.0.0.1}:${DB_PORT_HOST:-9306}:3306"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -uroot -p\"$${MARIADB_ROOT_PASSWORD}\" --silent"]
|
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -uroot -p\"$${MARIADB_ROOT_PASSWORD}\" --silent"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -23,21 +57,54 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
start_period: 20s
|
start_period: 20s
|
||||||
networks:
|
networks:
|
||||||
- pta
|
- pta-test
|
||||||
|
|
||||||
|
# Service jetable : applique les migrations SQL puis s'arrête. Le service php
|
||||||
|
# ne démarre qu'après sa sortie en succès, si bien qu'une migration en échec
|
||||||
|
# empêche l'application de tourner sur une base incomplète.
|
||||||
|
db-migrate:
|
||||||
|
image: mariadb:11.4
|
||||||
|
container_name: pta-db-migrate-test
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
# Port joint À L'INTÉRIEUR du réseau Docker : MariaDB y écoute sur 3306.
|
||||||
|
# Le port publié sur l'hôte (9306) n'a pas cours ici.
|
||||||
|
DB_PORT: 3306
|
||||||
|
DB_NAME: ${DB_NAME:-pta}
|
||||||
|
# Les migrations créent tables et vues : elles passent par le compte
|
||||||
|
# d'administration, jamais par le compte applicatif.
|
||||||
|
DB_USER: root
|
||||||
|
DB_PASSWORD: ${DB_ROOT_PASSWORD:-pta}
|
||||||
|
TZ: ${TZ:-Europe/Paris}
|
||||||
|
volumes:
|
||||||
|
- ./database:/migrations:ro
|
||||||
|
- ./docker/db/apply-migrations.sh:/usr/local/bin/apply-migrations.sh:ro
|
||||||
|
entrypoint: ["/bin/sh", "/usr/local/bin/apply-migrations.sh"]
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- pta-test
|
||||||
|
|
||||||
php:
|
php:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
target: php
|
target: php
|
||||||
container_name: pta-php
|
container_name: pta-php-test
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
PTA_DB_HOST: db
|
PTA_DB_HOST: db
|
||||||
|
# Port joint À L'INTÉRIEUR du réseau Docker, où MariaDB écoute sur 3306.
|
||||||
|
# Ce n'est pas le port publié sur l'hôte : pour joindre la base depuis la
|
||||||
|
# machine, utiliser 127.0.0.1:9306, publié par le service db ci-dessus.
|
||||||
PTA_DB_PORT: 3306
|
PTA_DB_PORT: 3306
|
||||||
PTA_DB_NAME: ${DB_NAME:-pta}
|
PTA_DB_NAME: ${DB_NAME:-pta}
|
||||||
PTA_DB_USER: ${DB_USER:-pta}
|
PTA_DB_USER: ${DB_USER:-pta}
|
||||||
PTA_DB_PASS: ${DB_PASSWORD:-change_me}
|
# Le défaut doit rester identique à MARIADB_PASSWORD du service db :
|
||||||
|
# deux défauts différents rendent la pile inutilisable sans fichier .env.
|
||||||
|
PTA_DB_PASS: ${DB_PASSWORD:-pta}
|
||||||
# Aucun détail technique n'est renvoyé au navigateur tant que ce drapeau
|
# Aucun détail technique n'est renvoyé au navigateur tant que ce drapeau
|
||||||
# n'est pas explicitement passé à 1. Ne jamais l'activer en production.
|
# n'est pas explicitement passé à 1. Ne jamais l'activer en production.
|
||||||
PTA_DEBUG: ${PTA_DEBUG:-0}
|
PTA_DEBUG: ${PTA_DEBUG:-0}
|
||||||
@@ -45,29 +112,32 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
db-migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
networks:
|
networks:
|
||||||
- pta
|
- pta-test
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
target: nginx
|
target: nginx
|
||||||
container_name: pta-nginx
|
container_name: pta-nginx-test
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${APP_BIND:-0.0.0.0}:${APP_PORT:-8080}:80"
|
- "${APP_BIND:-0.0.0.0}:${APP_PORT:-9080}:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- php
|
- php
|
||||||
networks:
|
networks:
|
||||||
- pta
|
- pta-test
|
||||||
|
|
||||||
phpmyadmin:
|
phpmyadmin:
|
||||||
image: phpmyadmin:5.2.2
|
image: phpmyadmin:5.2.2
|
||||||
container_name: pta-phpmyadmin
|
container_name: pta-phpmyadmin-test
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
PMA_HOST: db
|
PMA_HOST: db
|
||||||
|
# Port interne au réseau Docker, jamais un port publié.
|
||||||
PMA_PORT: 3306
|
PMA_PORT: 3306
|
||||||
# PMA_USER / PMA_PASSWORD sont volontairement absents : leur présence fait
|
# PMA_USER / PMA_PASSWORD sont volontairement absents : leur présence fait
|
||||||
# basculer phpMyAdmin en auth_type=config, c'est-à-dire une session déjà
|
# basculer phpMyAdmin en auth_type=config, c'est-à-dire une session déjà
|
||||||
@@ -76,18 +146,19 @@ services:
|
|||||||
UPLOAD_LIMIT: 64M
|
UPLOAD_LIMIT: 64M
|
||||||
TZ: ${TZ:-Europe/Paris}
|
TZ: ${TZ:-Europe/Paris}
|
||||||
ports:
|
ports:
|
||||||
# phpMyAdmin n'écoute que sur la boucle locale. Pour y accéder à distance,
|
# phpMyAdmin n'écoute que sur la boucle locale : il donne un accès complet
|
||||||
# passer par un tunnel SSH (voir README_DOCKER.md).
|
# à la base. Pour y accéder à distance, passer par un tunnel SSH
|
||||||
- "${PMA_BIND:-127.0.0.1}:${PMA_PORT:-8081}:80"
|
# (voir README_DOCKER.md), plutôt que d'ouvrir ce port sur le réseau.
|
||||||
|
- "${PMA_BIND:-127.0.0.1}:${PMA_PORT:-9081}:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- pta
|
- pta-test
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
pta:
|
pta-test:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mariadb_data:
|
mariadb_data_test:
|
||||||
|
|||||||
153
docker/db/apply-migrations.sh
Executable file
153
docker/db/apply-migrations.sh
Executable file
@@ -0,0 +1,153 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ============================================================================
|
||||||
|
# PTA — application automatique des migrations SQL au démarrage de la stack
|
||||||
|
# ============================================================================
|
||||||
|
# Exécuté par le service jetable `db-migrate` du docker-compose, une fois que
|
||||||
|
# MariaDB est déclaré sain. Le service `php` attend la fin de ce script en
|
||||||
|
# succès : si une migration échoue, l'application ne démarre pas du tout.
|
||||||
|
#
|
||||||
|
# Le schéma initial (database/pta_prod_schema_mariadb.sql) reste joué par
|
||||||
|
# docker-entrypoint-initdb.d à la création du volume. Ce script ne traite que
|
||||||
|
# les fichiers `migration_*.sql`, dans l'ordre alphabétique, sur une base déjà
|
||||||
|
# existante comme sur une base fraîchement créée.
|
||||||
|
#
|
||||||
|
# Un registre `schema_migration` mémorise chaque fichier appliqué avec son
|
||||||
|
# empreinte. Un fichier déjà appliqué et inchangé est ignoré ; un fichier
|
||||||
|
# modifié est rejoué — les migrations PTA sont écrites idempotentes
|
||||||
|
# (CREATE TABLE IF NOT EXISTS, INSERT ... ON DUPLICATE KEY UPDATE,
|
||||||
|
# CREATE OR REPLACE VIEW), les rejouer est sans effet de bord.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DB_HOST="${DB_HOST:-db}"
|
||||||
|
DB_PORT="${DB_PORT:-3306}"
|
||||||
|
DB_NAME="${DB_NAME:-pta}"
|
||||||
|
DB_USER="${DB_USER:-root}"
|
||||||
|
MIGRATIONS_DIR="${MIGRATIONS_DIR:-/migrations}"
|
||||||
|
|
||||||
|
# Le mot de passe passe par MYSQL_PWD et jamais par la ligne de commande :
|
||||||
|
# un `ps` dans le conteneur ne doit pas le révéler.
|
||||||
|
MYSQL_PWD="${DB_PASSWORD:-}"
|
||||||
|
export MYSQL_PWD
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[pta-migrate] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
sql() {
|
||||||
|
# --ssl-verify-server-cert=0 : le client MariaDB 11.4 désactive de toute
|
||||||
|
# façon cette vérification quand le mot de passe ne vient pas de la ligne
|
||||||
|
# de commande, en émettant un avertissement trompeur parlant de « login
|
||||||
|
# sans mot de passe ». Le poser explicitement supprime ce bruit dans les
|
||||||
|
# journaux de déploiement sans rien changer au comportement réel : la
|
||||||
|
# liaison reste interne au réseau bridge du compose.
|
||||||
|
mariadb --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" \
|
||||||
|
--database="$DB_NAME" --default-character-set=utf8mb4 \
|
||||||
|
--ssl-verify-server-cert=0 "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Attente de la base
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Le compose garantit déjà `service_healthy`, mais le healthcheck interroge la
|
||||||
|
# boucle locale du conteneur : cette boucle couvre le court instant où le port
|
||||||
|
# n'accepte pas encore de connexion depuis le réseau.
|
||||||
|
attente=0
|
||||||
|
until sql -N -B -e 'SELECT 1' >/dev/null 2>&1; do
|
||||||
|
attente=$((attente + 1))
|
||||||
|
if [ "$attente" -ge 30 ]; then
|
||||||
|
log "ERREUR : base $DB_NAME injoignable sur $DB_HOST:$DB_PORT après 60 s."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Base pas encore joignable, nouvelle tentative dans 2 s ($attente/30)…"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
log "Base $DB_NAME joignable sur $DB_HOST:$DB_PORT."
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Registre des migrations appliquées
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
sql <<'SQL'
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_migration (
|
||||||
|
fichier VARCHAR(190) NOT NULL,
|
||||||
|
empreinte CHAR(32) NOT NULL,
|
||||||
|
applique_le DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY (fichier)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Application des migrations
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
applique=0
|
||||||
|
ignore=0
|
||||||
|
|
||||||
|
for fichier in "$MIGRATIONS_DIR"/migration_*.sql; do
|
||||||
|
# Le glob non résolu se présente tel quel quand aucun fichier ne correspond.
|
||||||
|
[ -f "$fichier" ] || continue
|
||||||
|
|
||||||
|
nom=$(basename "$fichier")
|
||||||
|
empreinte=$(md5sum < "$fichier" | cut -d' ' -f1)
|
||||||
|
connue=$(sql -N -B -e \
|
||||||
|
"SELECT empreinte FROM schema_migration WHERE fichier = '$nom'")
|
||||||
|
|
||||||
|
if [ "$connue" = "$empreinte" ]; then
|
||||||
|
log "$nom : déjà appliquée, ignorée."
|
||||||
|
ignore=$((ignore + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$connue" ]; then
|
||||||
|
log "$nom : contenu modifié depuis la dernière application, rejeu."
|
||||||
|
else
|
||||||
|
log "$nom : application…"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! sql < "$fichier"; then
|
||||||
|
log "ERREUR : échec de $nom. L'application ne sera pas démarrée."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sql -e "INSERT INTO schema_migration (fichier, empreinte)
|
||||||
|
VALUES ('$nom', '$empreinte')
|
||||||
|
ON DUPLICATE KEY UPDATE empreinte = VALUES(empreinte),
|
||||||
|
applique_le = CURRENT_TIMESTAMP"
|
||||||
|
|
||||||
|
log "$nom : appliquée."
|
||||||
|
applique=$((applique + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
log "Bilan : $applique migration(s) appliquée(s), $ignore ignorée(s)."
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Contrôle final : les vues de droits doivent exister
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Même contrôle que SchemaModel::assertAccessViews() côté PHP. Le faire ici
|
||||||
|
# évite de laisser démarrer une pile dont la page d'accueil renverrait une
|
||||||
|
# erreur 500 incompréhensible.
|
||||||
|
manquantes=$(sql -N -B -e "
|
||||||
|
SELECT GROUP_CONCAT(attendue.nom ORDER BY attendue.nom SEPARATOR ', ')
|
||||||
|
FROM (
|
||||||
|
SELECT 'v_acces_role_page' AS nom
|
||||||
|
UNION ALL SELECT 'v_acces_role_permission'
|
||||||
|
UNION ALL SELECT 'v_acces_identite_agent'
|
||||||
|
UNION ALL SELECT 'v_acces_identite_structure'
|
||||||
|
UNION ALL SELECT 'v_acces_agent_par_structure'
|
||||||
|
UNION ALL SELECT 'v_acces_perimetre_creneau'
|
||||||
|
UNION ALL SELECT 'v_acces_perimetre_planning'
|
||||||
|
) AS attendue
|
||||||
|
LEFT JOIN information_schema.VIEWS v
|
||||||
|
ON v.TABLE_SCHEMA = DATABASE()
|
||||||
|
AND v.TABLE_NAME = attendue.nom
|
||||||
|
WHERE v.TABLE_NAME IS NULL")
|
||||||
|
|
||||||
|
if [ -n "$manquantes" ] && [ "$manquantes" != "NULL" ]; then
|
||||||
|
log "ERREUR : vue(s) de contrôle d'accès manquante(s) : $manquantes"
|
||||||
|
log "La migration v7.8 n'a pas produit le résultat attendu."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Les sept vues de contrôle d'accès sont présentes. Base à jour."
|
||||||
Reference in New Issue
Block a user