Files
doktainer-upstream/docker-entrypoint.sh

345 lines
8.3 KiB
Bash

#!/bin/sh
set -eu
node <<'NODE'
const net = require("net");
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("DATABASE_URL is not set.");
process.exit(1);
}
const url = new URL(databaseUrl);
const host = url.hostname;
const port = Number(url.port || 5432);
const timeoutMs = 90_000;
const intervalMs = 2_000;
const startedAt = Date.now();
function check() {
const socket = net.createConnection({ host, port });
socket.once("connect", () => {
socket.end();
console.log(`Database is reachable at ${host}:${port}.`);
process.exit(0);
});
socket.once("error", (error) => {
socket.destroy();
if (Date.now() - startedAt >= timeoutMs) {
console.error(`Database is not reachable at ${host}:${port}: ${error.message}`);
process.exit(1);
}
console.log(`Waiting for database at ${host}:${port}...`);
setTimeout(check, intervalMs);
});
}
check();
NODE
if [ -d /app/prisma/migrations ] && [ "$(find /app/prisma/migrations -mindepth 1 -maxdepth 1 | wc -l)" -gt 0 ]; then
db_action="$(node <<'NODE'
const { PrismaClient } = require("@prisma/client");
const { readdirSync } = require("fs");
const prisma = new PrismaClient();
async function main() {
const tableRows = await prisma.$queryRaw`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
`;
const enumRows = await prisma.$queryRaw`
SELECT typname
FROM pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = 'public'
AND t.typtype = 'e'
`;
const tables = new Set(tableRows.map((row) => row.table_name));
const hasMigrationTable = tables.has("_prisma_migrations");
const appTables = [...tables].filter(
(table) => table !== "_prisma_migrations"
);
const hasEnums = enumRows.length > 0;
let failedMigrations = [];
let migrationCount = 0;
let appliedMigrationNames = new Set();
if (hasMigrationTable) {
failedMigrations = await prisma.$queryRaw`
SELECT migration_name
FROM "_prisma_migrations"
WHERE finished_at IS NULL
AND rolled_back_at IS NULL
`;
const migrationCountResult = await prisma.$queryRaw`
SELECT COUNT(*)::int AS count
FROM "_prisma_migrations"
`;
migrationCount = migrationCountResult[0].count;
const appliedMigrationRows = await prisma.$queryRaw`
SELECT migration_name
FROM "_prisma_migrations"
WHERE finished_at IS NOT NULL
AND rolled_back_at IS NULL
`;
appliedMigrationNames = new Set(
appliedMigrationRows.map((row) => row.migration_name)
);
}
const migrationNames = readdirSync("/app/prisma/migrations", {
withFileTypes: true,
})
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name);
const pendingMigrationNames = migrationNames.filter(
(migrationName) => !appliedMigrationNames.has(migrationName)
);
/**
* STATE #1
* Fresh database
*/
if (
appTables.length === 0 &&
!hasMigrationTable &&
!hasEnums
) {
console.error(
"Fresh database detected. Applying migrations."
);
console.log("migrate");
return;
}
/**
* STATE #2
* Existing schema without Prisma migration history
*/
if (
appTables.length > 0 &&
!hasMigrationTable
) {
console.error(
"Application tables exist but Prisma migration history table is missing."
);
console.error(
"Database state is inconsistent."
);
console.log("blocked");
return;
}
/**
* STATE #3
* Prisma migration history exists
*/
if (hasMigrationTable) {
if (
migrationCount === 0 &&
(
appTables.length > 0 ||
hasEnums
)
) {
console.error(
"Database contains schema objects but Prisma migration history is empty."
);
console.error(
"Database state is inconsistent."
);
console.error(
"Do not delete records from _prisma_migrations manually."
);
console.log("blocked");
return;
}
if (failedMigrations.length > 0) {
console.error(
`Failed Prisma migrations detected: ${failedMigrations
.map((row) => row.migration_name)
.join(", ")}`
);
console.error(
"Resolve the failed migration before starting Doktainer."
);
console.log("blocked");
return;
}
if (pendingMigrationNames.length > 0) {
const idColumnIntegrity = await prisma.$queryRaw`
SELECT
table_class.relname AS "tableName",
attribute.attnotnull AS "isNotNull",
EXISTS (
SELECT 1
FROM pg_index i
WHERE i.indrelid = table_class.oid
AND i.indisunique
AND i.indisvalid
AND i.indisready
AND i.indnkeyatts = 1
AND i.indkey[0] = attribute.attnum
) AS "hasUniqueId"
FROM pg_class table_class
JOIN pg_namespace namespace
ON namespace.oid = table_class.relnamespace
JOIN pg_attribute attribute
ON attribute.attrelid = table_class.oid
WHERE namespace.nspname = 'public'
AND table_class.relkind IN ('r', 'p')
AND table_class.relname <> '_prisma_migrations'
AND attribute.attname = 'id'
AND NOT attribute.attisdropped
ORDER BY table_class.relname
`;
const invalidIdColumns = idColumnIntegrity.filter(
(row) => !row.hasUniqueId || !row.isNotNull
);
if (invalidIdColumns.length > 0) {
console.error(
`Database integrity check failed before applying pending migrations: ${pendingMigrationNames.join(", ")}.`
);
for (const invalidColumn of invalidIdColumns) {
const quotedTableName = `"${invalidColumn.tableName.replace(/"/g, '""')}"`;
const [counts] = await prisma.$queryRawUnsafe(`
SELECT
COUNT(*) FILTER (WHERE "id" IS NULL)::int AS "nullIds",
(COUNT(*) - COUNT(DISTINCT "id"))::int AS "duplicateIds"
FROM ${quotedTableName}
`);
console.error(
`${invalidColumn.tableName}.id must be NOT NULL and have a valid single-column PRIMARY KEY or UNIQUE index (duplicate rows: ${counts.duplicateIds}, null IDs: ${counts.nullIds}).`
);
}
console.error(
"Restore missing database constraints and resolve duplicate IDs before applying migrations."
);
console.log("blocked");
return;
}
}
console.error(
"Prisma migration history detected. Applying pending migrations."
);
console.log("migrate");
return;
}
/**
* STATE #4
* Partial schema / inconsistent state
*/
console.error("");
console.error(
"Database appears to be partially initialized."
);
console.error(
"Found Prisma objects without a valid schema state."
);
console.error("");
console.error(
"This may happen when:"
);
console.error(
"- a migration failed previously"
);
console.error(
"- the database was partially restored"
);
console.error(
"- schema objects were created manually"
);
console.error("");
console.error(
"Doktainer will not automatically modify the database."
);
console.error(
"Manual intervention is required."
);
console.log("blocked");
}
main()
.catch((error) => {
console.error(error);
console.log("blocked");
})
.finally(async () => {
await prisma.$disconnect();
});
NODE
)"
case "$db_action" in
migrate)
npx prisma migrate deploy
;;
push)
npx prisma db push
;;
blocked)
exit 1
;;
*)
echo "Unknown database setup action: $db_action" >&2
exit 1
;;
esac
else
npx prisma db push
fi
node dist/server/index.js &
api_pid=$!
cleanup() {
kill "$api_pid" 2>/dev/null || true
}
trap cleanup INT TERM EXIT
exec node_modules/.bin/next start -p 3000 -H 0.0.0.0