embedded-postgres
Advanced tools
Comparing version 11.20.0-beta.8 to 11.20.0-beta.9
@@ -5,5 +5,5 @@ import { Client } from 'pg'; | ||
*/ | ||
interface PostgresOptions { | ||
export interface PostgresOptions { | ||
/** The location where the data should be persisted to. Defaults to: `./data/db` */ | ||
database_dir: string; | ||
databaseDir: string; | ||
/** The port where the Postgres database should be listening. Defaults to: | ||
@@ -18,6 +18,25 @@ * `5432` */ | ||
* Defaults to `password` */ | ||
auth_method: 'scram-sha-256' | 'password' | 'md5'; | ||
authMethod: 'scram-sha-256' | 'password' | 'md5'; | ||
/** Whether all data should be left in place when the database is shut down. | ||
* Defaults to true. */ | ||
persistent: boolean; | ||
/** Pass any additional flags to the initdb process. You can find all | ||
* available flags here: | ||
* https://www.postgresql.org/docs/current/app-initdb.html. Flags should be | ||
* passed as a string array, e.g. `["--debug"]` or `["--locale=en-GB"]` */ | ||
initdbFlags: string[]; | ||
/** Pass any additional flags to the postgres process. You can find all | ||
* available flags here: | ||
* https://www.postgresql.org/docs/current/app-postgres.html. Flags should | ||
* be passed as a string array, e.g. `["--debug"]` or `["--locale=en-GB"]` */ | ||
postgresFlags: string[]; | ||
/** | ||
* Postgres does not allow binaries to be run by root. In case you're | ||
* running in root-only enviroments, such as Docker containers, you may need | ||
* to create an extra user on your system in order to be able to call the binaries. | ||
* | ||
* NOTE: This WILL irreversibly modify your host system. The effects are | ||
* somewhat minor, but it's still recommend to only use this in Docker containers. | ||
*/ | ||
createPostgresUser: boolean; | ||
} | ||
@@ -32,2 +51,3 @@ /** | ||
private process?; | ||
private isRootUser; | ||
constructor(options?: Partial<PostgresOptions>); | ||
@@ -70,3 +90,11 @@ /** | ||
dropDatabase(name: string): Promise<void>; | ||
/** | ||
* Warn the user in case they're trying to run this library as a root user | ||
*/ | ||
private checkForRootUser; | ||
/** | ||
* Retrieve the uid and gid for a particular user | ||
*/ | ||
private getUidAndGid; | ||
} | ||
export = EmbeddedPostgres; |
@@ -5,19 +5,22 @@ "use strict"; | ||
}; | ||
const child_process_1 = require("child_process"); | ||
const path_1 = __importDefault(require("path")); | ||
const crypto_1 = __importDefault(require("crypto")); | ||
const promises_1 = __importDefault(require("fs/promises")); | ||
const os_1 = require("os"); | ||
const crypto_1 = __importDefault(require("crypto")); | ||
const child_process_1 = require("child_process"); | ||
const pg_1 = require("pg"); | ||
const async_exit_hook_1 = __importDefault(require("async-exit-hook")); | ||
const binary_1 = __importDefault(require("./binary")); | ||
const async_exit_hook_1 = __importDefault(require("async-exit-hook")); | ||
const { postgres, initdb } = (0, binary_1.default)(); | ||
// The default configuration options for the class | ||
const defaults = { | ||
database_dir: path_1.default.join(process.cwd(), 'data', 'db'), | ||
databaseDir: path_1.default.join(process.cwd(), 'data', 'db'), | ||
port: 5432, | ||
user: 'postgres', | ||
password: 'password', | ||
auth_method: 'password', | ||
authMethod: 'password', | ||
persistent: true, | ||
initdbFlags: [], | ||
postgresFlags: [], | ||
createPostgresUser: false, | ||
}; | ||
@@ -36,5 +39,15 @@ /** | ||
constructor(options = {}) { | ||
// Options were previously specified in snake_case rather than | ||
// camelCase. We still want to accept the old style of options. | ||
const legacyOptions = {}; | ||
if (options.database_dir) { | ||
legacyOptions.databaseDir = options.database_dir; | ||
} | ||
if (options.auth_method) { | ||
legacyOptions.authMethod = options.auth_method; | ||
} | ||
// Assign default options to options object | ||
this.options = Object.assign({}, defaults, options); | ||
this.options = Object.assign({}, defaults, legacyOptions, options); | ||
instances.add(this); | ||
this.isRootUser = (0, os_1.userInfo)().uid === 0; | ||
} | ||
@@ -48,2 +61,33 @@ /** | ||
async initialise() { | ||
// GUARD: Check that a postgres user is available | ||
await this.checkForRootUser(); | ||
// Optionally retrieve the uid and gid | ||
let permissionIds = await this.getUidAndGid() | ||
.catch(() => ({})); | ||
// GUARD: Check if we need to create users | ||
if (this.options.createPostgresUser | ||
&& !('uid' in permissionIds) | ||
&& !('gid' in permissionIds)) { | ||
try { | ||
// Create the group and user | ||
await execAsync('groupadd postgres'); | ||
await execAsync('useradd -g postgres postgres'); | ||
// Re-treieve the permission ids now the user exists | ||
permissionIds = await this.getUidAndGid(); | ||
} | ||
catch (err) { | ||
console.error(err); | ||
throw new Error('Failed to create and initialize a new user on this system.'); | ||
} | ||
} | ||
// GUARD: Ensure that the data directory is owned by the created user | ||
if (this.options.createPostgresUser) { | ||
if (!('uid' in permissionIds)) { | ||
throw new Error('Failed to retrieve the uid for the newly created user.'); | ||
} | ||
// Create the data directory and have the user own it, so we | ||
// don't get any permission errors | ||
await promises_1.default.mkdir(this.options.databaseDir, { recursive: true }); | ||
await promises_1.default.chown(this.options.databaseDir, permissionIds.uid, permissionIds.gid); | ||
} | ||
// Create a file on disk that contains the password in plaintext | ||
@@ -59,7 +103,8 @@ const randomId = crypto_1.default.randomBytes(6).readUIntLE(0, 6).toString(36); | ||
const process = (0, child_process_1.spawn)(initdb, [ | ||
`--pgdata=${this.options.database_dir}`, | ||
`--auth=${this.options.auth_method}`, | ||
`--pgdata=${this.options.databaseDir}`, | ||
`--auth=${this.options.authMethod}`, | ||
`--username=${this.options.user}`, | ||
`--pwfile=${passwordFile}`, | ||
], { stdio: 'inherit' }); | ||
...this.options.initdbFlags, | ||
], { stdio: 'inherit', ...permissionIds }); | ||
process.on('exit', (code) => { | ||
@@ -83,2 +128,7 @@ if (code === 0) { | ||
async start() { | ||
// Optionally retrieve the uid and gid | ||
const permissionIds = await this.getUidAndGid() | ||
.catch(() => { | ||
throw new Error('Postgres cannot run as a root user. embedded-postgres could not find a postgres user to run as instead. Consider using the `createPostgresUser` option.'); | ||
}); | ||
// Greedily make the file executable, in case it is not | ||
@@ -91,6 +141,7 @@ await promises_1.default.chmod(postgres, '755'); | ||
'-D', | ||
this.options.database_dir, | ||
this.options.databaseDir, | ||
'-p', | ||
this.options.port.toString(), | ||
]); | ||
...this.options.postgresFlags, | ||
], { ...permissionIds }); | ||
// Connect to stderr, as that is where the messages get sent | ||
@@ -132,3 +183,3 @@ (_a = this.process.stderr) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => { | ||
// Delete the data directory | ||
await promises_1.default.rm(this.options.database_dir, { recursive: true, force: true }); | ||
await promises_1.default.rm(this.options.databaseDir, { recursive: true, force: true }); | ||
} | ||
@@ -187,4 +238,53 @@ } | ||
} | ||
/** | ||
* Warn the user in case they're trying to run this library as a root user | ||
*/ | ||
async checkForRootUser() { | ||
// GUARD: Ensure that the user isn't root | ||
if (!this.isRootUser) { | ||
return; | ||
} | ||
// Attempt to retrieve the uid and gid for the postgres user. This check | ||
// will throw and error when the postgres user doesn't exist | ||
try { | ||
await this.getUidAndGid(); | ||
} | ||
catch (err) { | ||
// GUARD: No user exists, but check that a postgres user should be created | ||
if (!this.options.createPostgresUser) { | ||
throw new Error('You are running this script as root. Postgres does not support running as root. If you wish to continue, configure embedded-postgres to create a Postgres user by setting the `createPostgresUser` option to true.'); | ||
} | ||
} | ||
} | ||
/** | ||
* Retrieve the uid and gid for a particular user | ||
*/ | ||
async getUidAndGid(name = 'postgres') { | ||
if (!this.isRootUser) { | ||
return {}; | ||
} | ||
const [uid, gid] = await Promise.all([ | ||
execAsync(`id -u ${name}`).then(Number.parseInt), | ||
execAsync(`id -g ${name}`).then(Number.parseInt), | ||
]); | ||
return { uid, gid }; | ||
} | ||
} | ||
/** | ||
* A promisified version of the exec API that either throws on errors or returns | ||
* the string results from the executed command. | ||
*/ | ||
async function execAsync(command) { | ||
return new Promise((resolve, reject) => { | ||
(0, child_process_1.exec)(command, (error, stdout) => { | ||
if (error) { | ||
reject(error); | ||
} | ||
else { | ||
resolve(stdout); | ||
} | ||
}); | ||
}); | ||
} | ||
/** | ||
* This script should be called when a Node script is exited, so that we can | ||
@@ -191,0 +291,0 @@ * nicely shutdown all potentially started clusters, and we don't end up with |
{ | ||
"name": "embedded-postgres", | ||
"version": "11.20.0-beta.8", | ||
"version": "11.20.0-beta.9", | ||
"description": "A package to run an embedded Postgresql database right from NodeJS", | ||
@@ -9,3 +9,4 @@ "main": "./dist/index.js", | ||
"build": "tsc", | ||
"lint": "eslint ./src --ext ts" | ||
"lint": "eslint ./src --ext ts", | ||
"docs": "typedoc --plugin typedoc-plugin-markdown ./src/types.ts --theme markdown --excludePrivate" | ||
}, | ||
@@ -21,3 +22,3 @@ "keywords": [ | ||
"devDependencies": { | ||
"@embedded-postgres/symlink-reader": "^11.20.0-beta.8", | ||
"@embedded-postgres/symlink-reader": "^11.20.0-beta.9", | ||
"@types/async-exit-hook": "^2.0.0", | ||
@@ -29,10 +30,10 @@ "@types/pg": "^8.6.5", | ||
"optionalDependencies": { | ||
"@embedded-postgres/darwin-arm64": "^11.20.0-beta.8", | ||
"@embedded-postgres/darwin-x64": "^11.20.0-beta.8", | ||
"@embedded-postgres/linux-arm": "^11.20.0-beta.8", | ||
"@embedded-postgres/linux-arm64": "^11.20.0-beta.8", | ||
"@embedded-postgres/linux-ia32": "^11.20.0-beta.8", | ||
"@embedded-postgres/linux-ppc64": "^11.20.0-beta.8", | ||
"@embedded-postgres/linux-x64": "^11.20.0-beta.8", | ||
"@embedded-postgres/windows-x64": "^11.20.0-beta.8" | ||
"@embedded-postgres/darwin-arm64": "^11.20.0-beta.9", | ||
"@embedded-postgres/darwin-x64": "^11.20.0-beta.9", | ||
"@embedded-postgres/linux-arm": "^11.20.0-beta.9", | ||
"@embedded-postgres/linux-arm64": "^11.20.0-beta.9", | ||
"@embedded-postgres/linux-ia32": "^11.20.0-beta.9", | ||
"@embedded-postgres/linux-ppc64": "^11.20.0-beta.9", | ||
"@embedded-postgres/linux-x64": "^11.20.0-beta.9", | ||
"@embedded-postgres/windows-x64": "^11.20.0-beta.9" | ||
}, | ||
@@ -50,3 +51,3 @@ "dependencies": { | ||
], | ||
"gitHead": "34632a4940769ac5ce0c2b1e94add4cf73715452" | ||
"gitHead": "85d7084328cdde8feed11e7c27c31915f19ce550" | ||
} |
@@ -15,3 +15,3 @@ # embedded-postgres | ||
const pg = new EmbeddedPostgres({ | ||
data_dir: './data/db', | ||
databaseDir: './data/db', | ||
user: 'postgres', | ||
@@ -18,0 +18,0 @@ password: 'password', |
22328
9
457