Skip to content

Commit e4dc3b0

Browse files
authored
Merge pull request #975 from constructive-io/feat/pgpm-docker-include-services
feat(pgpm): add --include flag for additional Docker services (minio)
2 parents 79cd3e6 + 46961a2 commit e4dc3b0

2 files changed

Lines changed: 195 additions & 12 deletions

File tree

pgpm/cli/src/commands/docker.ts

Lines changed: 194 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,41 @@ Docker Command:
66
77
pgpm docker <subcommand> [OPTIONS]
88
9-
Manage PostgreSQL Docker containers for local development.
9+
Manage Docker containers for local development.
10+
PostgreSQL is always started by default. Additional services can be
11+
included with the --include flag.
1012
1113
Subcommands:
12-
start Start PostgreSQL container
13-
stop Stop PostgreSQL container
14+
start Start containers
15+
stop Stop containers
16+
ls List available services and their status
1417
15-
Options:
16-
--help, -h Show this help message
18+
PostgreSQL Options:
1719
--name <name> Container name (default: postgres)
1820
--image <image> Docker image (default: constructiveio/postgres-plus:18)
1921
--port <port> Host port mapping (default: 5432)
2022
--user <user> PostgreSQL user (default: postgres)
2123
--password <pass> PostgreSQL password (default: password)
2224
--shm-size <size> Shared memory size for container (default: 2g)
23-
--recreate Remove and recreate container on start
25+
26+
General Options:
27+
--help, -h Show this help message
28+
--recreate Remove and recreate containers on start
29+
--include <svc> Include additional service (can be repeated)
30+
31+
Available Additional Services:
32+
minio MinIO S3-compatible object storage (port 9000)
2433
2534
Examples:
26-
pgpm docker start Start default PostgreSQL container
35+
pgpm docker start Start PostgreSQL only
36+
pgpm docker start --include minio Start PostgreSQL + MinIO
2737
pgpm docker start --port 5433 Start on custom port
2838
pgpm docker start --shm-size 4g Start with 4GB shared memory
29-
pgpm docker start --recreate Remove and recreate container
30-
pgpm docker stop Stop PostgreSQL container
39+
pgpm docker start --recreate Remove and recreate containers
40+
pgpm docker start --recreate --include minio Recreate PostgreSQL + MinIO
41+
pgpm docker stop Stop PostgreSQL
42+
pgpm docker stop --include minio Stop PostgreSQL + MinIO
43+
pgpm docker ls List services and status
3144
`;
3245

3346
interface DockerRunOptions {
@@ -40,6 +53,39 @@ interface DockerRunOptions {
4053
recreate?: boolean;
4154
}
4255

56+
interface PortMapping {
57+
host: number;
58+
container: number;
59+
}
60+
61+
interface VolumeMapping {
62+
name: string;
63+
containerPath: string;
64+
}
65+
66+
interface ServiceDefinition {
67+
name: string;
68+
image: string;
69+
ports: PortMapping[];
70+
env: Record<string, string>;
71+
command?: string[];
72+
volumes?: VolumeMapping[];
73+
}
74+
75+
const ADDITIONAL_SERVICES: Record<string, ServiceDefinition> = {
76+
minio: {
77+
name: 'minio',
78+
image: 'minio/minio',
79+
ports: [{ host: 9000, container: 9000 }],
80+
env: {
81+
MINIO_ACCESS_KEY: 'minioadmin',
82+
MINIO_SECRET_KEY: 'minioadmin',
83+
},
84+
command: ['server', '/data'],
85+
volumes: [{ name: 'minio-data', containerPath: '/data' }],
86+
},
87+
};
88+
4389
interface SpawnResult {
4490
code: number;
4591
stdout: string;
@@ -196,6 +242,131 @@ async function stopContainer(name: string): Promise<void> {
196242
}
197243
}
198244

245+
async function startService(service: ServiceDefinition, recreate: boolean): Promise<void> {
246+
const { name, image, ports, env: serviceEnv, command } = service;
247+
248+
const exists = await containerExists(name);
249+
const running = await isContainerRunning(name);
250+
251+
if (running === true) {
252+
console.log(`✅ Container "${name}" is already running`);
253+
return;
254+
}
255+
256+
if (recreate && exists) {
257+
console.log(`🗑️ Removing existing container "${name}"...`);
258+
const removeResult = await run('docker', ['rm', '-f', name], { stdio: 'inherit' });
259+
if (removeResult.code !== 0) {
260+
await cliExitWithError(`Failed to remove container "${name}"`);
261+
return;
262+
}
263+
}
264+
265+
if (exists && running === false) {
266+
console.log(`🔄 Starting existing container "${name}"...`);
267+
const startResult = await run('docker', ['start', name], { stdio: 'inherit' });
268+
if (startResult.code === 0) {
269+
console.log(`✅ Container "${name}" started successfully`);
270+
} else {
271+
await cliExitWithError(`Failed to start container "${name}"`);
272+
}
273+
return;
274+
}
275+
276+
console.log(`🚀 Creating and starting new container "${name}"...`);
277+
const runArgs = [
278+
'run',
279+
'-d',
280+
'--name', name,
281+
];
282+
283+
for (const [key, value] of Object.entries(serviceEnv)) {
284+
runArgs.push('-e', `${key}=${value}`);
285+
}
286+
287+
for (const portMapping of ports) {
288+
runArgs.push('-p', `${portMapping.host}:${portMapping.container}`);
289+
}
290+
291+
if (service.volumes) {
292+
for (const vol of service.volumes) {
293+
runArgs.push('-v', `${vol.name}:${vol.containerPath}`);
294+
}
295+
}
296+
297+
runArgs.push(image);
298+
299+
if (command) {
300+
runArgs.push(...command);
301+
}
302+
303+
const runResult = await run('docker', runArgs, { stdio: 'inherit' });
304+
if (runResult.code === 0) {
305+
console.log(`✅ Container "${name}" created and started successfully`);
306+
const portInfo = ports.map(p => `localhost:${p.host}`).join(', ');
307+
console.log(`📌 ${name} is available at ${portInfo}`);
308+
} else {
309+
const portInfo = ports.map(p => String(p.host)).join(', ');
310+
await cliExitWithError(`Failed to create container "${name}". Check if port ${portInfo} is already in use.`);
311+
}
312+
}
313+
314+
async function stopService(service: ServiceDefinition): Promise<void> {
315+
await stopContainer(service.name);
316+
}
317+
318+
function parseInclude(args: Partial<Record<string, any>>): string[] {
319+
const include = args.include;
320+
if (!include) return [];
321+
if (Array.isArray(include)) return include as string[];
322+
if (typeof include === 'string') return [include];
323+
return [];
324+
}
325+
326+
function resolveIncludedServices(includeNames: string[]): ServiceDefinition[] {
327+
const services: ServiceDefinition[] = [];
328+
for (const name of includeNames) {
329+
const service = ADDITIONAL_SERVICES[name];
330+
if (!service) {
331+
console.warn(`⚠️ Unknown service: "${name}". Available: ${Object.keys(ADDITIONAL_SERVICES).join(', ')}`);
332+
} else {
333+
services.push(service);
334+
}
335+
}
336+
return services;
337+
}
338+
339+
async function listServices(): Promise<void> {
340+
const dockerAvailable = await checkDockerAvailable();
341+
342+
console.log('\nAvailable services:\n');
343+
console.log(' Primary:');
344+
345+
if (dockerAvailable) {
346+
const pgRunning = await isContainerRunning('postgres');
347+
const pgStatus = pgRunning === true ? '\x1b[32mrunning\x1b[0m' : pgRunning === false ? '\x1b[33mstopped\x1b[0m' : '\x1b[90mnot created\x1b[0m';
348+
console.log(` postgres constructiveio/postgres-plus:18 ${pgStatus}`);
349+
} else {
350+
console.log(' postgres constructiveio/postgres-plus:18 \x1b[90m(docker not available)\x1b[0m');
351+
}
352+
353+
console.log('\n Additional (use --include <name>):');
354+
355+
for (const [key, service] of Object.entries(ADDITIONAL_SERVICES)) {
356+
if (dockerAvailable) {
357+
const running = await isContainerRunning(service.name);
358+
const status = running === true ? '\x1b[32mrunning\x1b[0m' : running === false ? '\x1b[33mstopped\x1b[0m' : '\x1b[90mnot created\x1b[0m';
359+
const portInfo = service.ports.map(p => String(p.host)).join(', ');
360+
console.log(` ${key.padEnd(12)}${service.image.padEnd(36)}${status} port ${portInfo}`);
361+
} else {
362+
const portInfo = service.ports.map(p => String(p.host)).join(', ');
363+
console.log(` ${key.padEnd(12)}${service.image.padEnd(36)}\x1b[90m(docker not available)\x1b[0m port ${portInfo}`);
364+
}
365+
}
366+
367+
console.log('');
368+
}
369+
199370
export default async (
200371
argv: Partial<Record<string, any>>,
201372
_prompter: Inquirerer,
@@ -211,7 +382,7 @@ export default async (
211382

212383
if (!subcommand) {
213384
console.log(dockerUsageText);
214-
await cliExitWithError('No subcommand provided. Use "start" or "stop".');
385+
await cliExitWithError('No subcommand provided. Use "start", "stop", or "ls".');
215386
return;
216387
}
217388
const name = (args.name as string) || 'postgres';
@@ -221,18 +392,30 @@ export default async (
221392
const password = (args.password as string) || 'password';
222393
const shmSize = (args['shm-size'] as string) || (args.shmSize as string) || '2g';
223394
const recreate = args.recreate === true;
395+
const includeNames = parseInclude(args);
396+
const includedServices = resolveIncludedServices(includeNames);
224397

225398
switch (subcommand) {
226399
case 'start':
227400
await startContainer({ name, image, port, user, password, shmSize, recreate });
401+
for (const service of includedServices) {
402+
await startService(service, recreate);
403+
}
228404
break;
229405

230406
case 'stop':
231407
await stopContainer(name);
408+
for (const service of includedServices) {
409+
await stopService(service);
410+
}
411+
break;
412+
413+
case 'ls':
414+
await listServices();
232415
break;
233416

234417
default:
235418
console.log(dockerUsageText);
236-
await cliExitWithError(`Unknown subcommand: ${subcommand}. Use "start" or "stop".`);
419+
await cliExitWithError(`Unknown subcommand: ${subcommand}. Use "start", "stop", or "ls".`);
237420
}
238421
};

pgpm/cli/src/utils/display.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const usageText = `
3939
deps Show change dependencies
4040
4141
Development Tools:
42-
docker Manage PostgreSQL Docker containers (start/stop)
42+
docker Manage Docker containers (start/stop/ls, --include for additional services)
4343
env Manage PostgreSQL environment variables
4444
test-packages Run integration tests on workspace packages
4545

0 commit comments

Comments
 (0)