Create separate roles and auto-create plugin databases

This would create a role per plugin (with a predictable pattern) before
creating a database owned by that role. This removes the need to
manually provision databases and users.
This commit is contained in:
Joel Low
2020-09-30 18:01:48 +08:00
parent 37ae2bad74
commit b28c98a597
7 changed files with 171 additions and 83 deletions
@@ -27,9 +27,9 @@ describe('database connection', () => {
]);
describe(createDatabaseClient, () => {
it('returns a postgres connection', () => {
it('returns a postgres connection', async () => {
expect(
createDatabaseClient(
await createDatabaseClient(
createConfig({
client: 'pg',
connection: {
@@ -43,9 +43,9 @@ describe('database connection', () => {
).toBeTruthy();
});
it('returns an sqlite connection', () => {
it('returns an sqlite connection', async () => {
expect(
createDatabaseClient(
await createDatabaseClient(
createConfig({
client: 'sqlite3',
connection: ':memory:',
@@ -55,24 +55,25 @@ describe('database connection', () => {
});
it('tries to create a mysql connection as a passthrough', () => {
expect(() =>
createDatabaseClient(
createConfig({
client: 'mysql',
connection: {
host: '127.0.0.1',
user: 'foo',
password: 'bar',
database: 'dbname',
},
}),
),
expect(
async () =>
await createDatabaseClient(
createConfig({
client: 'mysql',
connection: {
host: '127.0.0.1',
user: 'foo',
password: 'bar',
database: 'dbname',
},
}),
),
).toThrowError(/Cannot find module 'mysql'/);
});
it('accepts overrides', () => {
it('accepts overrides', async () => {
expect(
createDatabaseClient(
await createDatabaseClient(
createConfig({
client: 'pg',
connection: {
@@ -92,22 +93,24 @@ describe('database connection', () => {
});
it('throws an error without a client', () => {
expect(() =>
createDatabaseClient(
createConfig({
connection: '',
}),
),
expect(
async () =>
await createDatabaseClient(
createConfig({
connection: '',
}),
),
).toThrowError();
});
it('throws an error without a connection', () => {
expect(() =>
createDatabaseClient(
createConfig({
client: 'pg',
}),
),
expect(
async () =>
await createDatabaseClient(
createConfig({
client: 'pg',
}),
),
).toThrowError();
});
});
@@ -28,7 +28,7 @@ type DatabaseClient = 'pg' | 'sqlite3' | string;
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function createDatabaseClient(
export async function createDatabaseClient(
dbConfig: Config,
overrides?: Partial<knex.Config>,
) {
@@ -164,9 +164,9 @@ describe('postgres', () => {
});
describe(createPgDatabaseClient, () => {
it('creates a postgres knex instance', () => {
it('creates a postgres knex instance', async () => {
expect(
createPgDatabaseClient(
await createPgDatabaseClient(
createConfig({
host: 'acme',
user: 'foo',
@@ -177,13 +177,14 @@ describe('postgres', () => {
).toBeTruthy();
});
it('attempts to read an ssl cert', () => {
expect(() =>
createPgDatabaseClient(
createConfig(
'postgresql://postgres:pass@localhost:5432/dbname?sslrootcert=/path/to/file',
it('attempts to read an ssl cert', async () => {
expect(
async () =>
await createPgDatabaseClient(
createConfig(
'postgresql://postgres:pass@localhost:5432/dbname?sslrootcert=/path/to/file',
),
),
),
).toThrowError(/no such file or directory/);
});
});
@@ -15,26 +15,41 @@
*/
import knex, { PgConnectionConfig } from 'knex';
import { Config } from '@backstage/config';
import { cloneDeep } from 'lodash';
import { Config, JsonValue } from '@backstage/config';
import { mergeDatabaseConfig } from './config';
/**
* Creates a knex postgres database connection
* Creates a knex Postgres database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function createPgDatabaseClient(
export async function createPgDatabaseClient(
dbConfig: Config,
overrides?: knex.Config,
) {
const knexConfig = buildPgDatabaseConfig(dbConfig, overrides);
const baseConfig = buildPgDatabaseConfig(dbConfig, overrides);
let knexConfig = baseConfig;
// Bootstrap the missing database.
if (!!baseConfig?.connection.database) {
const knexAdminConfig = buildPgDatabaseAdminConfig(cloneDeep(baseConfig));
const admin = knex(knexAdminConfig);
await ensurePgDatabase(admin, baseConfig.connection.database);
knexConfig = buildPgPluginConfig(
cloneDeep(baseConfig),
baseConfig.connection.database,
);
}
const database = knex(knexConfig);
return database;
}
/**
* Builds a knex postgres database connection
* Builds a knex Postgres database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
@@ -44,7 +59,7 @@ export function buildPgDatabaseConfig(
overrides?: knex.Config,
) {
return mergeDatabaseConfig(
dbConfig.get(),
cloneDeep(dbConfig.get()),
{
connection: getPgConnectionConfig(dbConfig, !!overrides),
useNullAsDefault: true,
@@ -54,10 +69,35 @@ export function buildPgDatabaseConfig(
}
/**
* Gets the postgres connection config
* Builds a knex Postgres database connection for database creation
*
* @param dbConfig The database config
* @param parseConnectionString Flag to explictly control connection string parsing
*/
function buildPgDatabaseAdminConfig(dbConfig: JsonValue) {
return mergeDatabaseConfig(dbConfig, {
connection: {
database: 'postgres',
},
});
}
/**
* Builds a knex Postgres database connection for plugin consumption
*
* @param dbConfig The database config
* @param database The database granted to the plugin
*/
function buildPgPluginConfig(dbConfig: JsonValue, database: string) {
return mergeDatabaseConfig(dbConfig, {
connection: roleCredentials(database),
});
}
/**
* Gets the Postgres connection config
*
* @param dbConfig The database config
* @param parseConnectionString Flag to explicitly control connection string parsing
*/
export function getPgConnectionConfig(
dbConfig: Config,
@@ -77,6 +117,41 @@ export function getPgConnectionConfig(
: connection;
}
/**
* Creates the missing Postgres database if it does not exist
*
* @param admin The administrative database connection, defaulting to the `postgres` database
* @param database The name of the database to create
*/
async function ensurePgDatabase(admin: knex, database: string) {
const result = await admin
.from('pg_database')
.where('datname', database)
.count<Record<string, { count: string }>>();
if (parseInt(result[0].count, 10) > 0) {
return;
}
const owner = roleCredentials(database);
await admin.raw(`CREATE ROLE ?? WITH LOGIN PASSWORD '${owner.password}'`, [
owner.user,
]);
await admin.raw(`CREATE DATABASE ?? OWNER ??`, [database, owner.user]);
}
/**
* Creates credentials to own the given database
*
* @param database The name of the database to create a role for
*/
function roleCredentials(database: string) {
return {
user: database,
password: database,
};
}
/**
* Parses a connection string using pg-connection-string
*