Почему: 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, как на основе; повторный прогон на уже мигрированной БД — то же.
133 lines
5.9 KiB
JavaScript
133 lines
5.9 KiB
JavaScript
#!/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();
|