Checkpoint: Le stockage PDF devient un volume Docker externe explicitement nommé, avec nocopy. Le conteneur refuse de démarrer si /app/storage n’est pas un montage inscriptible, et son healthcheck contrôle aussi ce montage. Les tests, TypeScript et build ont été validés.
All checks were successful
Validation applicative / TypeScript, tests et build (push) Successful in 3m20s
All checks were successful
Validation applicative / TypeScript, tests et build (push) Successful in 3m20s
This commit is contained in:
@@ -63,10 +63,11 @@ COPY --from=builder /app/dist ./dist
|
|||||||
# Copy drizzle migrations
|
# Copy drizzle migrations
|
||||||
COPY drizzle/ ./drizzle/
|
COPY drizzle/ ./drizzle/
|
||||||
COPY drizzle.config.ts ./
|
COPY drizzle.config.ts ./
|
||||||
|
COPY scripts/ensure-storage-volume.sh ./scripts/ensure-storage-volume.sh
|
||||||
|
|
||||||
# Create uploads directory
|
# Create uploads directory and make the PDF volume guard executable.
|
||||||
RUN mkdir -p /app/uploads
|
RUN mkdir -p /app/uploads && chmod 755 /app/scripts/ensure-storage-volume.sh
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["node", "dist/index.js"]
|
CMD ["/app/scripts/ensure-storage-volume.sh"]
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ services:
|
|||||||
JWT_SECRET: demat-facturation-jwt-secret-recette-2026
|
JWT_SECRET: demat-facturation-jwt-secret-recette-2026
|
||||||
MISTRAL_API_KEY: ${MISTRAL_API_KEY:-}
|
MISTRAL_API_KEY: ${MISTRAL_API_KEY:-}
|
||||||
UPLOAD_DIR: /app/uploads
|
UPLOAD_DIR: /app/uploads
|
||||||
|
# Chemin unique utilisé par le serveur et vérifié au démarrage.
|
||||||
|
STORAGE_BASE_PATH: /app/storage
|
||||||
# Manus OAuth (optionnel en recette)
|
# Manus OAuth (optionnel en recette)
|
||||||
VITE_APP_ID: ${VITE_APP_ID:-demat-facturation-recette}
|
VITE_APP_ID: ${VITE_APP_ID:-demat-facturation-recette}
|
||||||
OAUTH_SERVER_URL: ${OAUTH_SERVER_URL:-}
|
OAUTH_SERVER_URL: ${OAUTH_SERVER_URL:-}
|
||||||
@@ -49,8 +51,12 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- demat_facturation_uploads:/app/uploads
|
- demat_facturation_uploads:/app/uploads
|
||||||
# Les factures source et leurs métadonnées sont servies sous /storage.
|
# Les factures source et leurs métadonnées sont servies sous /storage.
|
||||||
# Ce volume doit rester monté à chaque reconstruction du conteneur.
|
# Il est externe et n'est donc jamais recréé ni supprimé par Compose.
|
||||||
- demat_facturation_storage:/app/storage
|
- type: volume
|
||||||
|
source: demat_facturation_storage
|
||||||
|
target: /app/storage
|
||||||
|
volume:
|
||||||
|
nocopy: true
|
||||||
networks:
|
networks:
|
||||||
- demat-facturation-internal
|
- demat-facturation-internal
|
||||||
- web
|
- web
|
||||||
@@ -61,7 +67,7 @@ services:
|
|||||||
- "traefik.http.routers.demat-facturation.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.demat-facturation.tls.certresolver=letsencrypt"
|
||||||
- "traefik.http.services.demat-facturation.loadbalancer.server.port=3000"
|
- "traefik.http.services.demat-facturation.loadbalancer.server.port=3000"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
test: ["CMD-SHELL", "grep -qs ' /app/storage ' /proc/mounts && test -w /app/storage && curl -fsS http://localhost:3000/api/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -73,7 +79,8 @@ volumes:
|
|||||||
demat_facturation_uploads:
|
demat_facturation_uploads:
|
||||||
driver: local
|
driver: local
|
||||||
demat_facturation_storage:
|
demat_facturation_storage:
|
||||||
driver: local
|
external: true
|
||||||
|
name: ${STORAGE_VOLUME_NAME:-demat-facturation-dsi_demat_facturation_storage}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
demat-facturation-internal:
|
demat-facturation-internal:
|
||||||
|
|||||||
24
scripts/ensure-storage-volume.sh
Normal file
24
scripts/ensure-storage-volume.sh
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Refuse de lancer l'application si le répertoire PDF attendu n'est pas un
|
||||||
|
# volume Docker réellement monté. Cette barrière évite de démarrer avec un
|
||||||
|
# dossier de conteneur vide après une reconstruction.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
storage_path="${STORAGE_BASE_PATH:-/app/storage}"
|
||||||
|
|
||||||
|
if [ ! -d "$storage_path" ]; then
|
||||||
|
echo "[StorageGuard] Répertoire de stockage absent : $storage_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! awk -v expected="$storage_path" '$2 == expected { mounted = 1 } END { exit mounted ? 0 : 1 }' /proc/mounts; then
|
||||||
|
echo "[StorageGuard] Volume persistant non monté sur $storage_path ; démarrage annulé." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -w "$storage_path" ]; then
|
||||||
|
echo "[StorageGuard] Volume persistant non inscriptible : $storage_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec node dist/index.js
|
||||||
@@ -2,13 +2,28 @@ import { readFileSync } from "node:fs";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("routage Traefik", () => {
|
describe("routage et persistance de déploiement", () => {
|
||||||
it("utilise le domaine fourni par l’environnement avec la recette comme repli", () => {
|
it("utilise le domaine fourni par l’environnement avec la recette comme repli", () => {
|
||||||
const composeFile = readFileSync(join(process.cwd(), "docker-compose.yml"), "utf8");
|
const composeFile = readFileSync(join(process.cwd(), "docker-compose.yml"), "utf8");
|
||||||
|
|
||||||
expect(composeFile).toContain(
|
expect(composeFile).toContain(
|
||||||
"Host(`${APP_DOMAIN:-demat-facturation.recette.santinova-soft.org}`)"
|
"Host(`${APP_DOMAIN:-demat-facturation.recette.santinova-soft.org}`)"
|
||||||
);
|
);
|
||||||
expect(composeFile).toContain("demat_facturation_storage:/app/storage");
|
expect(composeFile).toContain("source: demat_facturation_storage");
|
||||||
|
expect(composeFile).toContain("target: /app/storage");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuse de recréer le volume PDF et contrôle son montage avant le démarrage", () => {
|
||||||
|
const composeFile = readFileSync(join(process.cwd(), "docker-compose.yml"), "utf8");
|
||||||
|
const dockerfile = readFileSync(join(process.cwd(), "Dockerfile"), "utf8");
|
||||||
|
const guardScript = readFileSync(join(process.cwd(), "scripts", "ensure-storage-volume.sh"), "utf8");
|
||||||
|
|
||||||
|
expect(composeFile).toContain("STORAGE_BASE_PATH: /app/storage");
|
||||||
|
expect(composeFile).toContain("external: true");
|
||||||
|
expect(composeFile).toContain("nocopy: true");
|
||||||
|
expect(composeFile).toContain("grep -qs ' /app/storage ' /proc/mounts");
|
||||||
|
expect(dockerfile).toContain('CMD ["/app/scripts/ensure-storage-volume.sh"]');
|
||||||
|
expect(guardScript).toContain("Volume persistant non monté");
|
||||||
|
expect(guardScript).toContain("/proc/mounts");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
5
todo.md
5
todo.md
@@ -900,3 +900,8 @@
|
|||||||
- [x] Corriger le composant PDF sans toucher aux factures ni aux volumes persistants
|
- [x] Corriger le composant PDF sans toucher aux factures ni aux volumes persistants
|
||||||
- [x] Vérifier l’ouverture d’un PDF en production
|
- [x] Vérifier l’ouverture d’un PDF en production
|
||||||
- [x] Promouvoir ADM.pareige et Olivier Pareige au rôle administrateur afin d’afficher les factures globales
|
- [x] Promouvoir ADM.pareige et Olivier Pareige au rôle administrateur afin d’afficher les factures globales
|
||||||
|
|
||||||
|
## Prévention du démontage des PDF après reconstruction
|
||||||
|
- [x] Auditer les montages de stockage et la chaîne de reconstruction en recette et production
|
||||||
|
- [x] Ajouter un contrôle bloquant avant reconstruction et une vérification après démarrage du volume `/app/storage`
|
||||||
|
- [ ] Vérifier les garde-fous sur les deux environnements sans modifier les données
|
||||||
|
|||||||
Reference in New Issue
Block a user