feat(quick-260909-dgj): Migrationsverbindung von Laufzeitverbindung trennen (Task 2/4)
- apps/api/scripts/migrate-and-start.sh: TESSERA_MIGRATE_DATABASE_URL fuer den Migrationsschritt, DATABASE_URL bleibt unveraendert fuer den Laufzeitschritt; leer/nicht gesetzt = bisheriges Verhalten - Dockerfile kopiert scripts/ und ruft das Skript als CMD auf; exec statt &&-Verkettung, damit Signale den Node-Prozess erreichen - docker-compose.yml/.prod.yml reichen TESSERA_MIGRATE_DATABASE_URL durch (leerer Vorgabewert); docker-compose.dev.yml/.ci.yml unveraendert - .env.example erklaert beide Variablen mit Platzhaltern, Umstellung bleibt auskommentiert - 5 Tests in start-script.spec.ts (rot vor dem Skript, jetzt gruen) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
This commit is contained in:
@@ -2,6 +2,15 @@ DB_PASSWORD=your_db_password_here
|
||||
DATABASE_URL=postgresql://tessera:your_db_password_here@db:5432/tessera
|
||||
NODE_ENV=development
|
||||
|
||||
# DATABASE_URL ist die Verbindung, mit der die laufende Anwendung arbeitet.
|
||||
# TESSERA_MIGRATE_DATABASE_URL ist die separate Verbindung, mit der beim
|
||||
# Containerstart NUR "prisma migrate deploy" laeuft. Bleibt sie leer, laeuft
|
||||
# alles wie bisher — beide Schritte nutzen DATABASE_URL. Die getrennte
|
||||
# Belegung ist erst sinnvoll, wenn docs/mandantentrennung-datenbankrolle.md
|
||||
# vollstaendig abgearbeitet ist; vorher wuerde eine ungeprüfte Umstellung die
|
||||
# Anwendung von ihren eigenen Daten aussperren.
|
||||
# TESSERA_MIGRATE_DATABASE_URL=postgresql://tessera:your_db_password_here@db:5432/tessera
|
||||
|
||||
# Encrypts stored credentials (LDAP bind password, calendar and mailbox logins).
|
||||
# Required - the stack refuses to start without it.
|
||||
# Generate one with: openssl rand -hex 32
|
||||
|
||||
+2
-1
@@ -33,6 +33,7 @@ COPY --from=builder /app/apps/api/dist ./apps/api/dist
|
||||
COPY --from=builder /app/node_modules/.pnpm/@prisma+client@6.19.3_prisma@6.19.3_typescript@5.9.3__typescript@5.9.3/node_modules/.prisma ./node_modules/.pnpm/@prisma+client@6.19.3_prisma@6.19.3_typescript@5.9.3__typescript@5.9.3/node_modules/.prisma
|
||||
COPY --from=builder /app/apps/api/prisma ./apps/api/prisma
|
||||
COPY --from=builder /app/packages/shared/src ./packages/shared/src
|
||||
COPY apps/api/scripts ./apps/api/scripts
|
||||
USER nestjs
|
||||
EXPOSE 3001
|
||||
CMD ["sh", "-c", "apps/api/node_modules/.bin/prisma migrate deploy --schema apps/api/prisma/schema.prisma && node apps/api/dist/main.js"]
|
||||
CMD ["sh", "apps/api/scripts/migrate-and-start.sh"]
|
||||
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/sh
|
||||
# WINDOWS #18 — trennt die Verbindung des Migrationsschritts von der
|
||||
# Verbindung des Laufzeitschritts (T-DGJ-03).
|
||||
#
|
||||
# `prisma migrate deploy` braucht die Rechte des Tabelleneigentuemers, um DDL
|
||||
# auszufuehren. Die Anwendung soll genau diese Rechte NICHT haben — sonst
|
||||
# waere die Rollentrennung aus Migration 20260909130000_rls_app_role wieder
|
||||
# verloren. Ohne getrennte Verbindungen fuer Migration und Laufzeit ist eine
|
||||
# Rollentrennung deshalb nicht moeglich.
|
||||
#
|
||||
# Vorgabe (ohne gesetztes TESSERA_MIGRATE_DATABASE_URL): beide Schritte
|
||||
# nutzen DATABASE_URL — exakt der bisherige Ablauf, unveraendert fuer
|
||||
# lokale Entwicklung, CI und den Server, bis
|
||||
# docs/mandantentrennung-datenbankrolle.md abgearbeitet ist.
|
||||
set -e
|
||||
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "FEHLER: DATABASE_URL ist nicht gesetzt." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# In sh ersetzt ${VAR:-fallback} auch eine gesetzte, aber leere Variable
|
||||
# durch den Fallback — Compose reicht nicht belegte Variablen als leere
|
||||
# Zeichenketten weiter, das soll wie "nicht gesetzt" behandelt werden.
|
||||
MIGRATE_URL="${TESSERA_MIGRATE_DATABASE_URL:-$DATABASE_URL}"
|
||||
|
||||
if [ -n "$TESSERA_MIGRATE_DATABASE_URL" ]; then
|
||||
MIGRATE_SOURCE="TESSERA_MIGRATE_DATABASE_URL"
|
||||
else
|
||||
MIGRATE_SOURCE="DATABASE_URL"
|
||||
fi
|
||||
RUNTIME_SOURCE="DATABASE_URL"
|
||||
|
||||
if [ "$1" = "--print-plan" ]; then
|
||||
# Nur Variablennamen ausgeben, niemals Werte — diese Betriebsart existiert
|
||||
# allein, damit die CI das Verhalten ohne Datenbank pruefen kann.
|
||||
echo "Migrationsschritt verwendet: $MIGRATE_SOURCE"
|
||||
echo "Laufzeitschritt verwendet: $RUNTIME_SOURCE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# DATABASE_URL nur fuer diesen einen Aufruf auf die Migrationsverbindung
|
||||
# setzen (vorangestellte Zuweisung, kein export) — der nachfolgende exec
|
||||
# erbt die unveraenderte DATABASE_URL aus der Umgebung.
|
||||
DATABASE_URL="$MIGRATE_URL" apps/api/node_modules/.bin/prisma migrate deploy --schema apps/api/prisma/schema.prisma
|
||||
|
||||
# exec statt Aufruf, damit Signale (z. B. SIGTERM bei docker stop) den
|
||||
# Node-Prozess erreichen. Die bisherige CMD-Zeile hatte dieses Problem
|
||||
# ebenfalls und loeste es nicht; hier wird es nebenbei mitbehoben.
|
||||
exec node apps/api/dist/main.js
|
||||
@@ -0,0 +1,96 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* Prueft apps/api/scripts/migrate-and-start.sh ausschliesslich ueber die
|
||||
* Ausgabebetriebsart --print-plan — es wird weder Prisma noch Node
|
||||
* aufgerufen, keine Datenbankverbindung aufgebaut (Task 2, WINDOWS #18).
|
||||
*/
|
||||
|
||||
const SCRIPT_PATH = join(__dirname, '../../scripts/migrate-and-start.sh');
|
||||
const SECRET_MARKER = 'tessera-test-secret-marker-9f3c1e';
|
||||
|
||||
function runScript(env: Record<string, string>): { stdout: string; status: number } {
|
||||
try {
|
||||
const stdout = execFileSync('sh', [SCRIPT_PATH, '--print-plan'], {
|
||||
env,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
return { stdout, status: 0 };
|
||||
} catch (err: any) {
|
||||
return { stdout: (err.stdout ?? '').toString(), status: err.status ?? 1 };
|
||||
}
|
||||
}
|
||||
|
||||
describe('migrate-and-start.sh --print-plan', () => {
|
||||
it('Test 1 (Rueckwaertsvertraeglichkeit): ohne TESSERA_MIGRATE_DATABASE_URL melden beide Schritte DATABASE_URL', () => {
|
||||
const { stdout, status } = runScript({
|
||||
PATH: process.env.PATH ?? '',
|
||||
DATABASE_URL: `postgresql://x:${SECRET_MARKER}@db:5432/tessera`,
|
||||
});
|
||||
|
||||
expect(status).toBe(0);
|
||||
expect(stdout).toContain('DATABASE_URL');
|
||||
// Kein Wert, nur der Variablenname darf erscheinen
|
||||
expect(stdout).not.toContain(SECRET_MARKER);
|
||||
// beide Zeilen (Migration + Laufzeit) nennen DATABASE_URL als Quelle
|
||||
const dbUrlMentions = (stdout.match(/DATABASE_URL/g) ?? []).length;
|
||||
expect(dbUrlMentions).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('Test 2 (Trennung): mit gesetztem TESSERA_MIGRATE_DATABASE_URL nennt der Migrationsschritt die Migrationsvariable, der Laufzeitschritt bleibt bei DATABASE_URL', () => {
|
||||
const { stdout, status } = runScript({
|
||||
PATH: process.env.PATH ?? '',
|
||||
DATABASE_URL: `postgresql://app:${SECRET_MARKER}@db:5432/tessera`,
|
||||
TESSERA_MIGRATE_DATABASE_URL: `postgresql://owner:${SECRET_MARKER}@db:5432/tessera`,
|
||||
});
|
||||
|
||||
expect(status).toBe(0);
|
||||
expect(stdout).toContain('TESSERA_MIGRATE_DATABASE_URL');
|
||||
expect(stdout).toContain('DATABASE_URL');
|
||||
expect(stdout).not.toContain(SECRET_MARKER);
|
||||
});
|
||||
|
||||
it('Test 3 (leer zaehlt als nicht gesetzt): TESSERA_MIGRATE_DATABASE_URL="" verhaelt sich wie Test 1', () => {
|
||||
const { stdout, status } = runScript({
|
||||
PATH: process.env.PATH ?? '',
|
||||
DATABASE_URL: `postgresql://x:${SECRET_MARKER}@db:5432/tessera`,
|
||||
TESSERA_MIGRATE_DATABASE_URL: '',
|
||||
});
|
||||
|
||||
expect(status).toBe(0);
|
||||
expect(stdout).not.toContain('TESSERA_MIGRATE_DATABASE_URL wird verwendet');
|
||||
const dbUrlMentions = (stdout.match(/DATABASE_URL/g) ?? []).length;
|
||||
expect(dbUrlMentions).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('Test 4 (kein Geheimnisabfluss): keiner der beiden Verbindungswerte erscheint in der Ausgabe', () => {
|
||||
const { stdout } = runScript({
|
||||
PATH: process.env.PATH ?? '',
|
||||
DATABASE_URL: `postgresql://app:${SECRET_MARKER}@db:5432/tessera`,
|
||||
TESSERA_MIGRATE_DATABASE_URL: `postgresql://owner:${SECRET_MARKER}b@db:5432/tessera`,
|
||||
});
|
||||
|
||||
expect(stdout).not.toContain(SECRET_MARKER);
|
||||
});
|
||||
|
||||
it('Test 5 (fehlende Angabe): ohne DATABASE_URL bricht das Skript mit Ende-Code ungleich 0 ab und nennt den Variablennamen', () => {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let status = 0;
|
||||
try {
|
||||
stdout = execFileSync('sh', [SCRIPT_PATH, '--print-plan'], {
|
||||
env: { PATH: process.env.PATH ?? '' },
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
} catch (err: any) {
|
||||
status = err.status ?? 1;
|
||||
stdout = (err.stdout ?? '').toString();
|
||||
stderr = (err.stderr ?? '').toString();
|
||||
}
|
||||
|
||||
expect(status).not.toBe(0);
|
||||
expect(stdout + stderr).toContain('DATABASE_URL');
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,7 @@ services:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
TESSERA_MIGRATE_DATABASE_URL: ${TESSERA_MIGRATE_DATABASE_URL:-}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
TESSERA_ADMIN_USER: ${TESSERA_ADMIN_USER:-admin}
|
||||
TESSERA_ADMIN_EMAIL: ${TESSERA_ADMIN_EMAIL}
|
||||
|
||||
@@ -31,6 +31,7 @@ services:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL:-postgresql://tessera:tessera_dev@db:5432/tessera}
|
||||
TESSERA_MIGRATE_DATABASE_URL: ${TESSERA_MIGRATE_DATABASE_URL:-}
|
||||
JWT_SECRET: ${JWT_SECRET:-tessera-dev-jwt-secret-change-in-production}
|
||||
TESSERA_ADMIN_USER: ${TESSERA_ADMIN_USER:-admin}
|
||||
TESSERA_ADMIN_EMAIL: ${TESSERA_ADMIN_EMAIL:-admin@tessera.local}
|
||||
|
||||
Reference in New Issue
Block a user