krona-staff/scripts/test/provision-pg.mjs
grok aa7f5f8bbd test(e2e): готовые роли через E2E_MIGRATOR_URL/E2E_APP_URL
Почему: TASK-GROK-3 п.1 — на общем кластере (параллельные прогоны приёмки, чужой стенд)
пересоздание ролей krona_migrator/krona_staff_app со случайными паролями ломает соседей.
Если заданы оба URL, global-setup не вызывает provision и не требует E2E_PG_ADMIN_URL;
по умолчанию поведение прежнее. provision-pg получил --roles keep: пересоздать только БД,
не меняя паролей (нужно accept-branch для отдельной БД на ветку).
ctx.adminDatabaseUrl (E2E_DB_ADMIN_URL или E2E_PG_ADMIN_URL + БД стенда) — только для PILOT-REHOME.
Проверено: run-e2e с E2E_MIGRATOR_URL/E2E_APP_URL без E2E_PG_ADMIN_URL — 45 failed | 9 passed,
как на основе; повторный прогон на уже мигрированной БД — то же.
2026-09-26 16:01:19 +07:00

133 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// Тестовый стенд PostgreSQL, повторяющий роли pilot, но без суперпользователя-мигратора:
// * bootstrap-админ (E2E_PG_ADMIN_URL) используется ТОЛЬКО здесь, как DBA/entrypoint образа;
// * krona_migrator — NOSUPERUSER NOCREATEDB NOCREATEROLE, владелец БД и схемы public;
// * krona_staff_app — NOSUPERUSER NOBYPASSRLS NOINHERIT, права выдают миграции 0003+.
// Пароли генерируются crypto.randomBytes и пишутся только в env-файл с правами 0600.
// Печатаются имена ролей/БД, но не пароли.
import { randomBytes } from 'node:crypto';
import { writeFileSync } from 'node:fs';
import pg from 'pg';
const args = new Map();
for (let i = 2; i < process.argv.length; i += 2) args.set(process.argv[i], process.argv[i + 1]);
const adminUrl = process.env.E2E_PG_ADMIN_URL;
const envFile = args.get('--out');
const database = args.get('--db') ?? 'krona_staff_e2e';
// --roles keep: роли уже созданы (например, предыдущим вызовом accept-branch) — пароли НЕ меняются,
// только пересоздаётся БД. Нужно, чтобы параллельные/последовательные стенды не ломали друг другу пароли.
const keepRoles = args.get('--roles') === 'keep';
if (!adminUrl || (!envFile && !keepRoles)) {
console.error(
'Usage: E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --out <env-path> [--db name]\n' +
' E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --roles keep --db name',
);
process.exit(2);
}
if (!/^[a-z_][a-z0-9_]{0,62}$/.test(database)) throw new Error('Invalid database name');
export async function provision() {
const migratorPassword = randomBytes(24).toString('hex');
const appPassword = randomBytes(24).toString('hex');
const admin = new pg.Client({
connectionString: adminUrl,
application_name: 'krona-e2e-provision',
});
await admin.connect();
try {
if (keepRoles) {
const existing = await admin.query(
`SELECT count(*)::int AS n FROM pg_roles WHERE rolname IN ('krona_migrator','krona_staff_app')`,
);
if (existing.rows[0].n !== 2)
throw new Error('--roles keep: роли krona_migrator/krona_staff_app не созданы');
} else {
await admin.query(`
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'krona_migrator') THEN
CREATE ROLE krona_migrator LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'krona_staff_app') THEN
CREATE ROLE krona_staff_app LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS;
END IF;
END $$`);
const alterMigrator = await admin.query(
"SELECT format('ALTER ROLE krona_migrator WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS', $1::text) AS sql",
[migratorPassword],
);
await admin.query(alterMigrator.rows[0].sql);
const alterApp = await admin.query(
"SELECT format('ALTER ROLE krona_staff_app WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS', $1::text) AS sql",
[appPassword],
);
await admin.query(alterApp.rows[0].sql);
}
await admin.query(
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`,
[database],
);
await admin.query(`DROP DATABASE IF EXISTS ${database}`);
await admin.query(
`CREATE DATABASE ${database} OWNER krona_migrator ENCODING 'UTF8' TEMPLATE template0`,
);
} finally {
await admin.end();
}
const dbAdminUrl = new URL(adminUrl);
dbAdminUrl.pathname = `/${database}`;
const dbAdmin = new pg.Client({ connectionString: dbAdminUrl.toString() });
await dbAdmin.connect();
try {
await dbAdmin.query('ALTER SCHEMA public OWNER TO krona_migrator');
await dbAdmin.query('REVOKE CREATE ON SCHEMA public FROM PUBLIC');
await dbAdmin.query(`REVOKE ALL ON DATABASE ${database} FROM PUBLIC`);
await dbAdmin.query(`GRANT CONNECT ON DATABASE ${database} TO krona_staff_app`);
} finally {
await dbAdmin.end();
}
if (keepRoles) {
console.log(`recreated database=${database} (roles kept, passwords unchanged)`);
return;
}
const base = new URL(adminUrl);
const url = (user, password) => {
const u = new URL(base.toString());
u.username = user;
u.password = password;
u.pathname = `/${database}`;
return u.toString();
};
const migratorUrl = url('krona_migrator', migratorPassword);
const appUrl = url('krona_staff_app', appPassword);
const lines = [
`DATABASE_MIGRATION_URL=${migratorUrl}`,
`DATABASE_TEST_MIGRATION_URL=${migratorUrl}`,
`DATABASE_TEST_URL=${appUrl}`,
`DATABASE_URL=${appUrl}`,
`POSTGRES_APP_PASSWORD=${appPassword}`,
'',
];
writeFileSync(envFile, lines.join('\n'), { mode: 0o600 });
// Проверяем атрибуты ролей и печатаем их (без паролей) как доказательство.
const check = new pg.Client({ connectionString: migratorUrl });
await check.connect();
try {
const roles = await check.query(
`SELECT rolname, rolsuper, rolcreaterole, rolcreatedb, rolbypassrls
FROM pg_roles WHERE rolname IN ('krona_migrator','krona_staff_app') ORDER BY rolname`,
);
const version = await check.query('SHOW server_version');
console.log(
`provisioned database=${database} server_version=${version.rows[0].server_version}`,
);
for (const r of roles.rows) {
console.log(
`role=${r.rolname} super=${r.rolsuper} createrole=${r.rolcreaterole} createdb=${r.rolcreatedb} bypassrls=${r.rolbypassrls}`,
);
}
} finally {
await check.end();
}
}
await provision();