Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ program
console.log(` Version: ${config.spec.version}`);
console.log(` Port: ${config.spec.network.port}`);
console.log(` Database: ${config.spec.database.name}`);
console.log(` User: ${config.spec.database.owner}`);
console.log(` Password: ${chalk.yellow(config.spec.database.password || 'N/A')}`);
console.log(` Data Directory: ${config.spec.storage.dataDirectory}`);

console.log();
Expand Down Expand Up @@ -133,6 +135,9 @@ program
console.log();
console.log(chalk.gray('Connection information:'));
console.log(chalk.gray(` psql -h ${config.spec.network.bindAddress} -p ${config.spec.network.port} -U ${config.spec.database.owner} -d ${config.spec.database.name}`));
if (config.spec.database.password) {
console.log(chalk.gray(` Password: ${config.spec.database.password}`));
}
}

} catch (error) {
Expand Down Expand Up @@ -274,22 +279,32 @@ program

if (options.format === 'uri') {
const host = config.spec.network.bindAddress === '0.0.0.0' ? 'localhost' : config.spec.network.bindAddress;
const uri = `postgresql://${config.spec.database.owner}@${host}:${config.spec.network.port}/${config.spec.database.name}`;
const userPassword = config.spec.database.password ?
`${config.spec.database.owner}:${config.spec.database.password}` :
config.spec.database.owner;
const uri = `postgresql://${userPassword}@${host}:${config.spec.network.port}/${config.spec.database.name}`;
console.log(uri);
} else if (options.format === 'env') {
const host = config.spec.network.bindAddress === '0.0.0.0' ? 'localhost' : config.spec.network.bindAddress;
console.log(`PGHOST=${host}`);
console.log(`PGPORT=${config.spec.network.port}`);
console.log(`PGDATABASE=${config.spec.database.name}`);
console.log(`PGUSER=${config.spec.database.owner}`);
if (config.spec.database.password) {
console.log(`PGPASSWORD=${config.spec.database.password}`);
}
} else if (options.format === 'json') {
const host = config.spec.network.bindAddress === '0.0.0.0' ? 'localhost' : config.spec.network.bindAddress;
const userPassword = config.spec.database.password ?
`${config.spec.database.owner}:${config.spec.database.password}` :
config.spec.database.owner;
const connectionInfo = {
host,
port: config.spec.network.port,
database: config.spec.database.name,
user: config.spec.database.owner,
uri: `postgresql://${config.spec.database.owner}@${host}:${config.spec.network.port}/${config.spec.database.name}`
password: config.spec.database.password,
uri: `postgresql://${userPassword}@${host}:${config.spec.network.port}/${config.spec.database.name}`
};
console.log(formatAsJson(connectionInfo));
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/config/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export class ConfigManager {
version,
network: {
port: defaultPort,
bindAddress: '127.0.0.1',
bindAddress: '0.0.0.0',
maxConnections: 100,
},
storage: {
Expand All @@ -210,7 +210,7 @@ export class ConfigManager {
},
authentication: {
method: 'md5',
allowedHosts: ['127.0.0.1/32', '::1/128'],
allowedHosts: ['127.0.0.1/32', '::1/128', '0.0.0.0/0'],
},
audit: {
enabled: false,
Expand Down
1 change: 1 addition & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface PostgreSQLInstanceConfig {
database: {
name: string;
owner: string;
password?: string;
encoding: string;
locale: string;
timezone: string;
Expand Down
92 changes: 92 additions & 0 deletions src/instance/manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { spawn, exec } from 'child_process';
import { promisify } from 'util';
import { randomBytes } from 'crypto';
import { access, mkdir, writeFile, readFile, readdir, rmdir, unlink, stat } from 'fs/promises';
import { join, dirname } from 'path';
import { ConfigManager } from '../config/manager.js';
Expand Down Expand Up @@ -57,6 +58,9 @@ export class InstanceManager {
// Create socket directory after initdb to avoid conflicts
await this.createSocketDirectory(config);

// Create database and user with password
await this.createDatabaseAndUser(config);

// Generate configuration files
await this.generateConfigFiles(config);

Expand Down Expand Up @@ -196,6 +200,94 @@ export class InstanceManager {
console.log(` Logs: ${config.spec.storage.logDirectory}`);
}

private async createDatabaseAndUser(config: PostgreSQLInstanceConfig): Promise<void> {
// Generate a secure password for the database user
const password = this.generateSecurePassword();
config.spec.database.password = password;

// Start PostgreSQL temporarily to create database and user
const postgresPath = await this.findPostgreSQLBinary('postgres', config.spec.version);
const psqlPath = await this.findPostgreSQLBinary('psql', config.spec.version);

// Start PostgreSQL in background
const tempProcess = spawn(postgresPath, [
'-D', config.spec.storage.dataDirectory,
'-p', config.spec.network.port.toString(),
'-c', 'listen_addresses=127.0.0.1',
], {
detached: false,
stdio: 'ignore',
});

try {
// Wait for PostgreSQL to start
await this.waitForPostgreSQLReady(config.spec.network.port, config.spec.version);

// Create the database
await execAsync(`${psqlPath} -h 127.0.0.1 -p ${config.spec.network.port} -U postgres -d postgres -c "CREATE DATABASE \\"${config.spec.database.name}\\""`);

// Create the user with password
await execAsync(`${psqlPath} -h 127.0.0.1 -p ${config.spec.network.port} -U postgres -d postgres -c "CREATE USER \\"${config.spec.database.owner}\\" WITH PASSWORD '${password}'"`);

// Grant privileges to the user on the database
await execAsync(`${psqlPath} -h 127.0.0.1 -p ${config.spec.network.port} -U postgres -d postgres -c "GRANT ALL PRIVILEGES ON DATABASE \\"${config.spec.database.name}\\" TO \\"${config.spec.database.owner}\\""`);

// Grant the user permission to create schemas in the database
await execAsync(`${psqlPath} -h 127.0.0.1 -p ${config.spec.network.port} -U postgres -d "${config.spec.database.name}" -c "GRANT CREATE ON SCHEMA public TO \\"${config.spec.database.owner}\\""`);

} catch (error) {
throw new Error(`Failed to create database and user: ${error}`);
} finally {
// Stop the temporary PostgreSQL process
if (tempProcess && tempProcess.pid) {
try {
process.kill(tempProcess.pid, 'SIGTERM');
// Wait for process to exit
await new Promise(resolve => setTimeout(resolve, 2000));
} catch {
// If SIGTERM doesn't work, try SIGKILL
try {
process.kill(tempProcess.pid, 'SIGKILL');
} catch {
// Process already dead, ignore
}
}
}
}
}

private generateSecurePassword(length: number = 16): string {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let password = '';

// Use crypto.randomBytes for cryptographically secure random generation
const bytes = randomBytes(length);

for (let i = 0; i < length; i++) {
password += characters[bytes[i] % characters.length];
}

return password;
}

private async waitForPostgreSQLReady(port: number, version: string, maxAttempts: number = 30): Promise<void> {
const psqlPath = await this.findPostgreSQLBinary('psql', version);

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await execAsync(`${psqlPath} -h 127.0.0.1 -p ${port} -U postgres -d postgres -c "SELECT 1"`, {
timeout: 2000
});
return; // Connection successful
} catch {
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, 1000));
}
}

throw new Error(`PostgreSQL did not become ready after ${maxAttempts} attempts`);
}

private async createInstanceDirectories(config: PostgreSQLInstanceConfig): Promise<void> {
const directories = [
config.spec.storage.dataDirectory,
Expand Down
Loading