@geekmidas/cli 1.10.7 → 1.10.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +44 -1
- package/dist/{bundler-NpfYPBUo.cjs → bundler-Bm3Az_sv.cjs} +2 -2
- package/dist/{bundler-NpfYPBUo.cjs.map → bundler-Bm3Az_sv.cjs.map} +1 -1
- package/dist/{bundler-DQYjKFPm.mjs → bundler-kk_XJTRp.mjs} +2 -2
- package/dist/{bundler-DQYjKFPm.mjs.map → bundler-kk_XJTRp.mjs.map} +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/{fullstack-secrets-ca0Kyrvt.mjs → fullstack-secrets-C2lbdbLZ.mjs} +15 -1
- package/dist/fullstack-secrets-C2lbdbLZ.mjs.map +1 -0
- package/dist/{fullstack-secrets-BctGaE4E.cjs → fullstack-secrets-CtWIYuI0.cjs} +15 -1
- package/dist/fullstack-secrets-CtWIYuI0.cjs.map +1 -0
- package/dist/{index-9tjTQjFt.d.mts → index-BdJZKXCJ.d.cts} +4 -2
- package/dist/index-BdJZKXCJ.d.cts.map +1 -0
- package/dist/{index-VOKKO-lm.d.cts → index-DB9VbcCD.d.mts} +4 -2
- package/dist/index-DB9VbcCD.d.mts.map +1 -0
- package/dist/index.cjs +177 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +177 -61
- package/dist/index.mjs.map +1 -1
- package/dist/openapi-BYxAWwok.cjs.map +1 -1
- package/dist/openapi-DenF-okj.mjs.map +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/{reconcile-C5OyCA7V.mjs → reconcile-BnM6FA6g.mjs} +2 -2
- package/dist/{reconcile-C5OyCA7V.mjs.map → reconcile-BnM6FA6g.mjs.map} +1 -1
- package/dist/{reconcile-TEBsryVn.cjs → reconcile-D6u4HSg8.cjs} +2 -2
- package/dist/{reconcile-TEBsryVn.cjs.map → reconcile-D6u4HSg8.cjs.map} +1 -1
- package/dist/{storage-DmCbr6DI.mjs → storage-B7H2PPCS.mjs} +8 -1
- package/dist/{storage-DmCbr6DI.mjs.map → storage-B7H2PPCS.mjs.map} +1 -1
- package/dist/{storage-Dx_jZbq6.mjs → storage-C1FNm2EP.mjs} +1 -1
- package/dist/{storage-CoCNe0Pt.cjs → storage-Cs13jkJ9.cjs} +8 -1
- package/dist/{storage-CoCNe0Pt.cjs.map → storage-Cs13jkJ9.cjs.map} +1 -1
- package/dist/{storage-C7pmBq1u.cjs → storage-D6BGLgWf.cjs} +1 -1
- package/dist/{sync-6FoT41G3.mjs → sync-CyGe5f1I.mjs} +1 -1
- package/dist/{sync-CbeKrnQV.mjs → sync-CzXruMzP.mjs} +2 -2
- package/dist/{sync-CbeKrnQV.mjs.map → sync-CzXruMzP.mjs.map} +1 -1
- package/dist/sync-DLlwsrBs.cjs +4 -0
- package/dist/{sync-DdkKaHqP.cjs → sync-oCqELfeA.cjs} +2 -2
- package/dist/{sync-DdkKaHqP.cjs.map → sync-oCqELfeA.cjs.map} +1 -1
- package/dist/{types-C7QJJl9f.d.cts → types-D4MLWXSL.d.cts} +2 -2
- package/dist/{types-C7QJJl9f.d.cts.map → types-D4MLWXSL.d.cts.map} +1 -1
- package/dist/{types-Iqsq_FIG.d.mts → types-DwpLq_fp.d.mts} +2 -2
- package/dist/{types-Iqsq_FIG.d.mts.map → types-DwpLq_fp.d.mts.map} +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace-4SP3Gx4Y.cjs.map +1 -1
- package/dist/workspace-D4z4A4cq.mjs.map +1 -1
- package/package.json +5 -5
- package/src/dev/__tests__/index.spec.ts +142 -0
- package/src/dev/index.ts +67 -33
- package/src/docker/__tests__/compose.spec.ts +151 -2
- package/src/docker/compose.ts +105 -8
- package/src/init/generators/docker.ts +3 -1
- package/src/init/index.ts +1 -0
- package/src/init/versions.ts +1 -1
- package/src/secrets/__tests__/generator.spec.ts +68 -0
- package/src/secrets/__tests__/storage.spec.ts +30 -0
- package/src/secrets/generator.ts +18 -0
- package/src/secrets/index.ts +9 -0
- package/src/secrets/storage.ts +7 -0
- package/src/secrets/types.ts +4 -0
- package/src/setup/index.ts +1 -0
- package/src/test/__tests__/index.spec.ts +115 -0
- package/src/test/index.ts +41 -21
- package/src/types.ts +1 -1
- package/src/workspace/types.ts +2 -0
- package/dist/fullstack-secrets-BctGaE4E.cjs.map +0 -1
- package/dist/fullstack-secrets-ca0Kyrvt.mjs.map +0 -1
- package/dist/index-9tjTQjFt.d.mts.map +0 -1
- package/dist/index-VOKKO-lm.d.cts.map +0 -1
- package/dist/sync-RsnjXYwG.cjs +0 -4
package/src/docker/compose.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const DEFAULT_SERVICE_IMAGES: Record<ComposeServiceName, string> = {
|
|
|
13
13
|
postgres: 'postgres',
|
|
14
14
|
redis: 'redis',
|
|
15
15
|
rabbitmq: 'rabbitmq',
|
|
16
|
+
minio: 'minio/minio',
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
/** Default Docker image versions for services */
|
|
@@ -20,6 +21,7 @@ export const DEFAULT_SERVICE_VERSIONS: Record<ComposeServiceName, string> = {
|
|
|
20
21
|
postgres: '18-alpine',
|
|
21
22
|
redis: '7-alpine',
|
|
22
23
|
rabbitmq: '3-management-alpine',
|
|
24
|
+
minio: 'latest',
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export interface ComposeOptions {
|
|
@@ -87,7 +89,9 @@ export function generateDockerCompose(options: ComposeOptions): string {
|
|
|
87
89
|
|
|
88
90
|
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
|
|
89
91
|
|
|
90
|
-
let yaml =
|
|
92
|
+
let yaml = `# Use "gkm dev" or "gkm test" to start services.
|
|
93
|
+
# Running "docker compose up" directly will not inject secrets or resolve ports.
|
|
94
|
+
version: '3.8'
|
|
91
95
|
|
|
92
96
|
services:
|
|
93
97
|
api:
|
|
@@ -119,6 +123,16 @@ services:
|
|
|
119
123
|
`;
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
if (serviceMap.has('minio')) {
|
|
127
|
+
yaml += ` - S3_ENDPOINT=\${S3_ENDPOINT:-http://minio:9000}
|
|
128
|
+
- S3_ACCESS_KEY_ID=\${MINIO_ACCESS_KEY:-app}
|
|
129
|
+
- S3_SECRET_ACCESS_KEY=\${MINIO_SECRET_KEY:-app}
|
|
130
|
+
- S3_BUCKET=\${MINIO_BUCKET:-app}
|
|
131
|
+
- S3_REGION=\${S3_REGION:-eu-west-1}
|
|
132
|
+
- S3_FORCE_PATH_STYLE=true
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
|
|
122
136
|
yaml += ` healthcheck:
|
|
123
137
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
|
|
124
138
|
interval: 30s
|
|
@@ -208,6 +222,32 @@ services:
|
|
|
208
222
|
`;
|
|
209
223
|
}
|
|
210
224
|
|
|
225
|
+
const minioImage = serviceMap.get('minio');
|
|
226
|
+
if (minioImage) {
|
|
227
|
+
yaml += `
|
|
228
|
+
minio:
|
|
229
|
+
image: ${minioImage}
|
|
230
|
+
container_name: minio
|
|
231
|
+
restart: unless-stopped
|
|
232
|
+
entrypoint: sh
|
|
233
|
+
command: -c 'mkdir -p /data/\${MINIO_BUCKET:-app} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
|
|
234
|
+
environment:
|
|
235
|
+
MINIO_ROOT_USER: \${MINIO_ACCESS_KEY:-app}
|
|
236
|
+
MINIO_ROOT_PASSWORD: \${MINIO_SECRET_KEY:-app}
|
|
237
|
+
ports:
|
|
238
|
+
- "9001:9001" # Console UI
|
|
239
|
+
volumes:
|
|
240
|
+
- minio_data:/data
|
|
241
|
+
healthcheck:
|
|
242
|
+
test: ["CMD", "mc", "ready", "local"]
|
|
243
|
+
interval: 10s
|
|
244
|
+
timeout: 5s
|
|
245
|
+
retries: 5
|
|
246
|
+
networks:
|
|
247
|
+
- app-network
|
|
248
|
+
`;
|
|
249
|
+
}
|
|
250
|
+
|
|
211
251
|
// Add volumes
|
|
212
252
|
yaml += `
|
|
213
253
|
volumes:
|
|
@@ -228,6 +268,11 @@ volumes:
|
|
|
228
268
|
`;
|
|
229
269
|
}
|
|
230
270
|
|
|
271
|
+
if (serviceMap.has('minio')) {
|
|
272
|
+
yaml += ` minio_data:
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
275
|
+
|
|
231
276
|
// Add networks
|
|
232
277
|
yaml += `
|
|
233
278
|
networks:
|
|
@@ -248,7 +293,9 @@ export function generateMinimalDockerCompose(
|
|
|
248
293
|
|
|
249
294
|
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
|
|
250
295
|
|
|
251
|
-
return
|
|
296
|
+
return `# Use "gkm dev" or "gkm test" to start services.
|
|
297
|
+
# Running "docker compose up" directly will not inject secrets or resolve ports.
|
|
298
|
+
version: '3.8'
|
|
252
299
|
|
|
253
300
|
services:
|
|
254
301
|
api:
|
|
@@ -301,13 +348,16 @@ export function generateWorkspaceCompose(
|
|
|
301
348
|
const hasPostgres = services.db !== undefined && services.db !== false;
|
|
302
349
|
const hasRedis = services.cache !== undefined && services.cache !== false;
|
|
303
350
|
const hasMail = services.mail !== undefined && services.mail !== false;
|
|
351
|
+
const hasMinio = services.storage !== undefined && services.storage !== false;
|
|
304
352
|
|
|
305
353
|
// Get image versions from config
|
|
306
354
|
const postgresImage = getInfraServiceImage('postgres', services.db);
|
|
307
355
|
const redisImage = getInfraServiceImage('redis', services.cache);
|
|
356
|
+
const minioImage = getInfraServiceImage('minio', services.storage);
|
|
308
357
|
|
|
309
358
|
let yaml = `# Docker Compose for ${workspace.name} workspace
|
|
310
|
-
#
|
|
359
|
+
# Use "gkm dev" or "gkm test" to start services.
|
|
360
|
+
# Running "docker compose up" directly will not inject secrets or resolve ports.
|
|
311
361
|
|
|
312
362
|
services:
|
|
313
363
|
`;
|
|
@@ -318,6 +368,7 @@ services:
|
|
|
318
368
|
registry,
|
|
319
369
|
hasPostgres,
|
|
320
370
|
hasRedis,
|
|
371
|
+
hasMinio,
|
|
321
372
|
});
|
|
322
373
|
}
|
|
323
374
|
|
|
@@ -376,6 +427,31 @@ services:
|
|
|
376
427
|
`;
|
|
377
428
|
}
|
|
378
429
|
|
|
430
|
+
if (hasMinio) {
|
|
431
|
+
yaml += `
|
|
432
|
+
minio:
|
|
433
|
+
image: ${minioImage}
|
|
434
|
+
container_name: ${workspace.name}-minio
|
|
435
|
+
restart: unless-stopped
|
|
436
|
+
entrypoint: sh
|
|
437
|
+
command: -c 'mkdir -p /data/\${MINIO_BUCKET:-app} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
|
|
438
|
+
environment:
|
|
439
|
+
MINIO_ROOT_USER: \${MINIO_ACCESS_KEY:-app}
|
|
440
|
+
MINIO_ROOT_PASSWORD: \${MINIO_SECRET_KEY:-app}
|
|
441
|
+
ports:
|
|
442
|
+
- "9001:9001" # Console UI
|
|
443
|
+
volumes:
|
|
444
|
+
- minio_data:/data
|
|
445
|
+
healthcheck:
|
|
446
|
+
test: ["CMD", "mc", "ready", "local"]
|
|
447
|
+
interval: 10s
|
|
448
|
+
timeout: 5s
|
|
449
|
+
retries: 5
|
|
450
|
+
networks:
|
|
451
|
+
- workspace-network
|
|
452
|
+
`;
|
|
453
|
+
}
|
|
454
|
+
|
|
379
455
|
// Add volumes section
|
|
380
456
|
yaml += `
|
|
381
457
|
volumes:
|
|
@@ -391,6 +467,11 @@ volumes:
|
|
|
391
467
|
`;
|
|
392
468
|
}
|
|
393
469
|
|
|
470
|
+
if (hasMinio) {
|
|
471
|
+
yaml += ` minio_data:
|
|
472
|
+
`;
|
|
473
|
+
}
|
|
474
|
+
|
|
394
475
|
// Add networks section
|
|
395
476
|
yaml += `
|
|
396
477
|
networks:
|
|
@@ -405,12 +486,13 @@ networks:
|
|
|
405
486
|
* Get infrastructure service image with version.
|
|
406
487
|
*/
|
|
407
488
|
function getInfraServiceImage(
|
|
408
|
-
serviceName: 'postgres' | 'redis',
|
|
489
|
+
serviceName: 'postgres' | 'redis' | 'minio',
|
|
409
490
|
config: boolean | { version?: string; image?: string } | undefined,
|
|
410
491
|
): string {
|
|
411
|
-
const defaults: Record<'postgres' | 'redis', string> = {
|
|
492
|
+
const defaults: Record<'postgres' | 'redis' | 'minio', string> = {
|
|
412
493
|
postgres: 'postgres:18-alpine',
|
|
413
494
|
redis: 'redis:7-alpine',
|
|
495
|
+
minio: 'minio/minio:latest',
|
|
414
496
|
};
|
|
415
497
|
|
|
416
498
|
if (!config || config === true) {
|
|
@@ -422,8 +504,12 @@ function getInfraServiceImage(
|
|
|
422
504
|
return config.image;
|
|
423
505
|
}
|
|
424
506
|
if (config.version) {
|
|
425
|
-
const
|
|
426
|
-
|
|
507
|
+
const baseImages: Record<'postgres' | 'redis' | 'minio', string> = {
|
|
508
|
+
postgres: 'postgres',
|
|
509
|
+
redis: 'redis',
|
|
510
|
+
minio: 'minio/minio',
|
|
511
|
+
};
|
|
512
|
+
return `${baseImages[serviceName]}:${config.version}`;
|
|
427
513
|
}
|
|
428
514
|
}
|
|
429
515
|
|
|
@@ -441,9 +527,10 @@ function generateAppService(
|
|
|
441
527
|
registry?: string;
|
|
442
528
|
hasPostgres: boolean;
|
|
443
529
|
hasRedis: boolean;
|
|
530
|
+
hasMinio: boolean;
|
|
444
531
|
},
|
|
445
532
|
): string {
|
|
446
|
-
const { registry, hasPostgres, hasRedis } = options;
|
|
533
|
+
const { registry, hasPostgres, hasRedis, hasMinio } = options;
|
|
447
534
|
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
|
|
448
535
|
|
|
449
536
|
// Health check path - frontends use /, backends use /health
|
|
@@ -485,6 +572,15 @@ function generateAppService(
|
|
|
485
572
|
}
|
|
486
573
|
if (hasRedis) {
|
|
487
574
|
yaml += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
if (hasMinio) {
|
|
578
|
+
yaml += ` - S3_ENDPOINT=\${S3_ENDPOINT:-http://minio:9000}
|
|
579
|
+
- S3_ACCESS_KEY_ID=\${MINIO_ACCESS_KEY:-app}
|
|
580
|
+
- S3_SECRET_ACCESS_KEY=\${MINIO_SECRET_KEY:-app}
|
|
581
|
+
- S3_BUCKET=\${MINIO_BUCKET:-app}
|
|
582
|
+
- S3_REGION=\${S3_REGION:-eu-west-1}
|
|
583
|
+
- S3_FORCE_PATH_STYLE=true
|
|
488
584
|
`;
|
|
489
585
|
}
|
|
490
586
|
}
|
|
@@ -501,6 +597,7 @@ function generateAppService(
|
|
|
501
597
|
if (app.type === 'backend') {
|
|
502
598
|
if (hasPostgres) dependencies.push('postgres');
|
|
503
599
|
if (hasRedis) dependencies.push('redis');
|
|
600
|
+
if (hasMinio) dependencies.push('minio');
|
|
504
601
|
}
|
|
505
602
|
|
|
506
603
|
if (dependencies.length > 0) {
|
|
@@ -161,7 +161,9 @@ export function generateDockerFiles(
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
// Build docker-compose.yml
|
|
164
|
-
let dockerCompose =
|
|
164
|
+
let dockerCompose = `# Use "gkm dev" or "gkm test" to start services.
|
|
165
|
+
# Running "docker compose up" directly will not inject secrets or resolve ports.
|
|
166
|
+
services:
|
|
165
167
|
${services.join('\n\n')}
|
|
166
168
|
`;
|
|
167
169
|
|
package/src/init/index.ts
CHANGED
|
@@ -330,6 +330,7 @@ export async function initCommand(
|
|
|
330
330
|
const secretServices: ComposeServiceName[] = [];
|
|
331
331
|
if (services.db) secretServices.push('postgres');
|
|
332
332
|
if (services.cache) secretServices.push('redis');
|
|
333
|
+
if (services.storage) secretServices.push('minio');
|
|
333
334
|
|
|
334
335
|
const devSecrets = createStageSecrets('development', secretServices, {
|
|
335
336
|
projectName: name,
|
package/src/init/versions.ts
CHANGED
|
@@ -45,7 +45,7 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
45
45
|
'@geekmidas/storage': '~2.0.0',
|
|
46
46
|
'@geekmidas/studio': '~1.0.0',
|
|
47
47
|
'@geekmidas/telescope': '~1.0.0',
|
|
48
|
-
'@geekmidas/testkit': '~1.0.
|
|
48
|
+
'@geekmidas/testkit': '~1.0.5',
|
|
49
49
|
'@geekmidas/cli': CLI_VERSION,
|
|
50
50
|
} as const;
|
|
51
51
|
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
createStageSecrets,
|
|
4
4
|
generateConnectionUrls,
|
|
5
|
+
generateMinioEndpoint,
|
|
5
6
|
generatePostgresUrl,
|
|
6
7
|
generateRabbitmqUrl,
|
|
7
8
|
generateRedisUrl,
|
|
@@ -65,6 +66,16 @@ describe('generateServiceCredentials', () => {
|
|
|
65
66
|
expect(creds.vhost).toBe('/');
|
|
66
67
|
expect(creds.password).toHaveLength(32);
|
|
67
68
|
});
|
|
69
|
+
|
|
70
|
+
it('should generate minio credentials with defaults', () => {
|
|
71
|
+
const creds = generateServiceCredentials('minio');
|
|
72
|
+
|
|
73
|
+
expect(creds.host).toBe('localhost');
|
|
74
|
+
expect(creds.port).toBe(9000);
|
|
75
|
+
expect(creds.username).toBe('app');
|
|
76
|
+
expect(creds.bucket).toBe('app');
|
|
77
|
+
expect(creds.password).toHaveLength(32);
|
|
78
|
+
});
|
|
68
79
|
});
|
|
69
80
|
|
|
70
81
|
describe('generateServicesCredentials', () => {
|
|
@@ -142,6 +153,33 @@ describe('generateRedisUrl', () => {
|
|
|
142
153
|
});
|
|
143
154
|
});
|
|
144
155
|
|
|
156
|
+
describe('generateMinioEndpoint', () => {
|
|
157
|
+
it('should generate valid minio endpoint URL', () => {
|
|
158
|
+
const creds: ServiceCredentials = {
|
|
159
|
+
host: 'localhost',
|
|
160
|
+
port: 9000,
|
|
161
|
+
username: 'app',
|
|
162
|
+
password: 'secret123',
|
|
163
|
+
bucket: 'my-bucket',
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const url = generateMinioEndpoint(creds);
|
|
167
|
+
expect(url).toBe('http://localhost:9000');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should use custom host and port', () => {
|
|
171
|
+
const creds: ServiceCredentials = {
|
|
172
|
+
host: 'minio.example.com',
|
|
173
|
+
port: 9090,
|
|
174
|
+
username: 'app',
|
|
175
|
+
password: 'secret',
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const url = generateMinioEndpoint(creds);
|
|
179
|
+
expect(url).toBe('http://minio.example.com:9090');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
145
183
|
describe('generateRabbitmqUrl', () => {
|
|
146
184
|
it('should generate valid rabbitmq URL with default vhost', () => {
|
|
147
185
|
const creds: ServiceCredentials = {
|
|
@@ -209,11 +247,34 @@ describe('generateConnectionUrls', () => {
|
|
|
209
247
|
password: 'rmq-pass',
|
|
210
248
|
vhost: '/',
|
|
211
249
|
},
|
|
250
|
+
minio: {
|
|
251
|
+
host: 'minio',
|
|
252
|
+
port: 9000,
|
|
253
|
+
username: 'app',
|
|
254
|
+
password: 'minio-pass',
|
|
255
|
+
bucket: 'app',
|
|
256
|
+
},
|
|
212
257
|
});
|
|
213
258
|
|
|
214
259
|
expect(urls.DATABASE_URL).toBeDefined();
|
|
215
260
|
expect(urls.REDIS_URL).toBeDefined();
|
|
216
261
|
expect(urls.RABBITMQ_URL).toBeDefined();
|
|
262
|
+
expect(urls.S3_ENDPOINT).toBe('http://minio:9000');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should generate S3_ENDPOINT for minio', () => {
|
|
266
|
+
const urls = generateConnectionUrls({
|
|
267
|
+
minio: {
|
|
268
|
+
host: 'localhost',
|
|
269
|
+
port: 9000,
|
|
270
|
+
username: 'app',
|
|
271
|
+
password: 'secret',
|
|
272
|
+
bucket: 'my-bucket',
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(urls.S3_ENDPOINT).toBe('http://localhost:9000');
|
|
277
|
+
expect(urls.DATABASE_URL).toBeUndefined();
|
|
217
278
|
});
|
|
218
279
|
});
|
|
219
280
|
|
|
@@ -246,6 +307,13 @@ describe('createStageSecrets', () => {
|
|
|
246
307
|
expect(secrets.urls.REDIS_URL).toBeDefined();
|
|
247
308
|
expect(secrets.urls.RABBITMQ_URL).toBeDefined();
|
|
248
309
|
});
|
|
310
|
+
|
|
311
|
+
it('should generate S3_ENDPOINT for minio', () => {
|
|
312
|
+
const secrets = createStageSecrets('production', ['minio']);
|
|
313
|
+
|
|
314
|
+
expect(secrets.services.minio).toBeDefined();
|
|
315
|
+
expect(secrets.urls.S3_ENDPOINT).toBe('http://localhost:9000');
|
|
316
|
+
});
|
|
249
317
|
});
|
|
250
318
|
|
|
251
319
|
describe('rotateServicePassword', () => {
|
|
@@ -338,6 +338,36 @@ describe('toEmbeddableSecrets', () => {
|
|
|
338
338
|
expect(embeddable.RABBITMQ_VHOST).toBe('/myapp');
|
|
339
339
|
});
|
|
340
340
|
|
|
341
|
+
it('should include minio service credentials', () => {
|
|
342
|
+
const secrets: StageSecrets = {
|
|
343
|
+
stage: 'production',
|
|
344
|
+
createdAt: new Date().toISOString(),
|
|
345
|
+
updatedAt: new Date().toISOString(),
|
|
346
|
+
services: {
|
|
347
|
+
minio: {
|
|
348
|
+
host: 'localhost',
|
|
349
|
+
port: 9000,
|
|
350
|
+
username: 'myaccesskey',
|
|
351
|
+
password: 'mysecretkey',
|
|
352
|
+
bucket: 'my-bucket',
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
urls: {
|
|
356
|
+
S3_ENDPOINT: 'http://localhost:9000',
|
|
357
|
+
},
|
|
358
|
+
custom: {},
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const embeddable = toEmbeddableSecrets(secrets);
|
|
362
|
+
|
|
363
|
+
expect(embeddable.S3_ENDPOINT).toBe('http://localhost:9000');
|
|
364
|
+
expect(embeddable.S3_ACCESS_KEY_ID).toBe('myaccesskey');
|
|
365
|
+
expect(embeddable.S3_SECRET_ACCESS_KEY).toBe('mysecretkey');
|
|
366
|
+
expect(embeddable.S3_BUCKET).toBe('my-bucket');
|
|
367
|
+
expect(embeddable.S3_REGION).toBe('eu-west-1');
|
|
368
|
+
expect(embeddable.S3_FORCE_PATH_STYLE).toBe('true');
|
|
369
|
+
});
|
|
370
|
+
|
|
341
371
|
it('should handle all services and custom secrets together', () => {
|
|
342
372
|
const secrets: StageSecrets = {
|
|
343
373
|
stage: 'production',
|
package/src/secrets/generator.ts
CHANGED
|
@@ -34,6 +34,12 @@ const SERVICE_DEFAULTS: Record<
|
|
|
34
34
|
username: 'app',
|
|
35
35
|
vhost: '/',
|
|
36
36
|
},
|
|
37
|
+
minio: {
|
|
38
|
+
host: 'localhost',
|
|
39
|
+
port: 9000,
|
|
40
|
+
username: 'app',
|
|
41
|
+
bucket: 'app',
|
|
42
|
+
},
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
/**
|
|
@@ -89,6 +95,14 @@ export function generateRabbitmqUrl(creds: ServiceCredentials): string {
|
|
|
89
95
|
return `amqp://${username}:${encodeURIComponent(password)}@${host}:${port}/${encodedVhost}`;
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Generate endpoint URL for MinIO (S3-compatible).
|
|
100
|
+
*/
|
|
101
|
+
export function generateMinioEndpoint(creds: ServiceCredentials): string {
|
|
102
|
+
const { host, port } = creds;
|
|
103
|
+
return `http://${host}:${port}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
/**
|
|
93
107
|
* Generate connection URLs from service credentials.
|
|
94
108
|
*/
|
|
@@ -109,6 +123,10 @@ export function generateConnectionUrls(
|
|
|
109
123
|
urls.RABBITMQ_URL = generateRabbitmqUrl(services.rabbitmq);
|
|
110
124
|
}
|
|
111
125
|
|
|
126
|
+
if (services.minio) {
|
|
127
|
+
urls.S3_ENDPOINT = generateMinioEndpoint(services.minio);
|
|
128
|
+
}
|
|
129
|
+
|
|
112
130
|
return urls;
|
|
113
131
|
}
|
|
114
132
|
|
package/src/secrets/index.ts
CHANGED
|
@@ -129,6 +129,9 @@ export async function secretsInitCommand(
|
|
|
129
129
|
if (secrets.urls.RABBITMQ_URL) {
|
|
130
130
|
logger.log(` RABBITMQ_URL: ${maskUrl(secrets.urls.RABBITMQ_URL)}`);
|
|
131
131
|
}
|
|
132
|
+
if (secrets.urls.S3_ENDPOINT) {
|
|
133
|
+
logger.log(` S3_ENDPOINT: ${secrets.urls.S3_ENDPOINT}`);
|
|
134
|
+
}
|
|
132
135
|
|
|
133
136
|
if (Object.keys(secrets.custom).length > 0) {
|
|
134
137
|
logger.log(`\n Custom secrets: ${Object.keys(secrets.custom).length}`);
|
|
@@ -234,6 +237,9 @@ export async function secretsShowCommand(
|
|
|
234
237
|
if (creds.vhost) {
|
|
235
238
|
logger.log(` vhost: ${creds.vhost}`);
|
|
236
239
|
}
|
|
240
|
+
if (creds.bucket) {
|
|
241
|
+
logger.log(` bucket: ${creds.bucket}`);
|
|
242
|
+
}
|
|
237
243
|
}
|
|
238
244
|
}
|
|
239
245
|
|
|
@@ -254,6 +260,9 @@ export async function secretsShowCommand(
|
|
|
254
260
|
` RABBITMQ_URL: ${reveal ? secrets.urls.RABBITMQ_URL : maskUrl(secrets.urls.RABBITMQ_URL)}`,
|
|
255
261
|
);
|
|
256
262
|
}
|
|
263
|
+
if (secrets.urls.S3_ENDPOINT) {
|
|
264
|
+
logger.log(` S3_ENDPOINT: ${secrets.urls.S3_ENDPOINT}`);
|
|
265
|
+
}
|
|
257
266
|
|
|
258
267
|
// Show custom secrets
|
|
259
268
|
const customKeys = Object.keys(secrets.custom);
|
package/src/secrets/storage.ts
CHANGED
|
@@ -208,6 +208,13 @@ export function toEmbeddableSecrets(secrets: StageSecrets): EmbeddableSecrets {
|
|
|
208
208
|
RABBITMQ_PORT: String(secrets.services.rabbitmq.port),
|
|
209
209
|
RABBITMQ_VHOST: secrets.services.rabbitmq.vhost ?? '/',
|
|
210
210
|
}),
|
|
211
|
+
...(secrets.services.minio && {
|
|
212
|
+
S3_ACCESS_KEY_ID: secrets.services.minio.username,
|
|
213
|
+
S3_SECRET_ACCESS_KEY: secrets.services.minio.password,
|
|
214
|
+
S3_BUCKET: secrets.services.minio.bucket ?? 'app',
|
|
215
|
+
S3_REGION: 'eu-west-1',
|
|
216
|
+
S3_FORCE_PATH_STYLE: 'true',
|
|
217
|
+
}),
|
|
211
218
|
};
|
|
212
219
|
}
|
|
213
220
|
|
package/src/secrets/types.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface ServiceCredentials {
|
|
|
10
10
|
database?: string;
|
|
11
11
|
/** Virtual host (for rabbitmq) */
|
|
12
12
|
vhost?: string;
|
|
13
|
+
/** Bucket name (for minio) */
|
|
14
|
+
bucket?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/** Stage secrets configuration */
|
|
@@ -25,12 +27,14 @@ export interface StageSecrets {
|
|
|
25
27
|
postgres?: ServiceCredentials;
|
|
26
28
|
redis?: ServiceCredentials;
|
|
27
29
|
rabbitmq?: ServiceCredentials;
|
|
30
|
+
minio?: ServiceCredentials;
|
|
28
31
|
};
|
|
29
32
|
/** Generated connection URLs */
|
|
30
33
|
urls: {
|
|
31
34
|
DATABASE_URL?: string;
|
|
32
35
|
REDIS_URL?: string;
|
|
33
36
|
RABBITMQ_URL?: string;
|
|
37
|
+
S3_ENDPOINT?: string;
|
|
34
38
|
};
|
|
35
39
|
/** Custom user-defined secrets */
|
|
36
40
|
custom: Record<string, string>;
|
package/src/setup/index.ts
CHANGED
|
@@ -194,6 +194,7 @@ async function generateFreshSecrets(
|
|
|
194
194
|
const serviceNames: ComposeServiceName[] = [];
|
|
195
195
|
if (workspace.services.db) serviceNames.push('postgres');
|
|
196
196
|
if (workspace.services.cache) serviceNames.push('redis');
|
|
197
|
+
if (workspace.services.storage) serviceNames.push('minio');
|
|
197
198
|
|
|
198
199
|
// Create base secrets with service credentials
|
|
199
200
|
const secrets = createStageSecrets(stage, serviceNames, {
|
|
@@ -18,9 +18,11 @@ import {
|
|
|
18
18
|
vi,
|
|
19
19
|
} from 'vitest';
|
|
20
20
|
import {
|
|
21
|
+
buildDockerComposeEnv,
|
|
21
22
|
createCredentialsPreload,
|
|
22
23
|
loadPortState,
|
|
23
24
|
parseComposePortMappings,
|
|
25
|
+
resolveServicePorts,
|
|
24
26
|
rewriteUrlsWithPorts,
|
|
25
27
|
savePortState,
|
|
26
28
|
} from '../../dev/index';
|
|
@@ -388,3 +390,116 @@ services:
|
|
|
388
390
|
);
|
|
389
391
|
});
|
|
390
392
|
});
|
|
393
|
+
|
|
394
|
+
describe('test command Docker startup pipeline', () => {
|
|
395
|
+
let testDir: string;
|
|
396
|
+
|
|
397
|
+
beforeAll(() => {
|
|
398
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
399
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
afterAll(() => {
|
|
403
|
+
vi.restoreAllMocks();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
beforeEach(() => {
|
|
407
|
+
testDir = join(tmpdir(), `gkm-test-docker-${Date.now()}`);
|
|
408
|
+
mkdirSync(testDir, { recursive: true });
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
afterEach(() => {
|
|
412
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should build docker compose env with secrets so credentials are interpolated', () => {
|
|
416
|
+
const secretsEnv = {
|
|
417
|
+
POSTGRES_USER: 'app',
|
|
418
|
+
POSTGRES_PASSWORD: 'supersecret',
|
|
419
|
+
POSTGRES_DB: 'myapp',
|
|
420
|
+
};
|
|
421
|
+
const portEnv = { POSTGRES_HOST_PORT: '5434' };
|
|
422
|
+
|
|
423
|
+
// This is the env that gets passed to `docker compose up -d`
|
|
424
|
+
const env = buildDockerComposeEnv(secretsEnv, portEnv);
|
|
425
|
+
|
|
426
|
+
// Secrets are available so ${POSTGRES_USER:-postgres} resolves correctly
|
|
427
|
+
expect(env.POSTGRES_USER).toBe('app');
|
|
428
|
+
expect(env.POSTGRES_PASSWORD).toBe('supersecret');
|
|
429
|
+
expect(env.POSTGRES_DB).toBe('myapp');
|
|
430
|
+
expect(env.POSTGRES_HOST_PORT).toBe('5434');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('should resolve ports, rewrite URLs and hostnames, then append _test', async () => {
|
|
434
|
+
writeFileSync(
|
|
435
|
+
join(testDir, 'docker-compose.yml'),
|
|
436
|
+
`
|
|
437
|
+
services:
|
|
438
|
+
postgres:
|
|
439
|
+
image: postgres:18
|
|
440
|
+
ports:
|
|
441
|
+
- '\${POSTGRES_HOST_PORT:-5432}:5432'
|
|
442
|
+
redis:
|
|
443
|
+
image: redis:7
|
|
444
|
+
ports:
|
|
445
|
+
- '\${REDIS_HOST_PORT:-6379}:6379'
|
|
446
|
+
`,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// Simulate secrets loaded from encrypted store (Docker hostnames)
|
|
450
|
+
const secretsEnv: Record<string, string> = {
|
|
451
|
+
DATABASE_URL: 'postgresql://app:supersecret@postgres:5432/myapp',
|
|
452
|
+
REDIS_URL: 'redis://:redispass@redis:6379',
|
|
453
|
+
POSTGRES_USER: 'app',
|
|
454
|
+
POSTGRES_PASSWORD: 'supersecret',
|
|
455
|
+
POSTGRES_DB: 'myapp',
|
|
456
|
+
POSTGRES_HOST: 'postgres',
|
|
457
|
+
POSTGRES_PORT: '5432',
|
|
458
|
+
REDIS_PASSWORD: 'redispass',
|
|
459
|
+
REDIS_HOST: 'redis',
|
|
460
|
+
REDIS_PORT: '6379',
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// Step 1: Resolve ports
|
|
464
|
+
const resolvedPorts = await resolveServicePorts(testDir);
|
|
465
|
+
|
|
466
|
+
// Step 2: Build env for docker compose (secrets + ports)
|
|
467
|
+
const dockerEnv = buildDockerComposeEnv(
|
|
468
|
+
secretsEnv,
|
|
469
|
+
resolvedPorts.dockerEnv,
|
|
470
|
+
);
|
|
471
|
+
expect(dockerEnv.POSTGRES_USER).toBe('app');
|
|
472
|
+
expect(dockerEnv.POSTGRES_PASSWORD).toBe('supersecret');
|
|
473
|
+
|
|
474
|
+
// Step 3: Rewrite URLs with ports and hostnames
|
|
475
|
+
const rewritten = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
|
|
476
|
+
|
|
477
|
+
// Step 4: Apply test suffix
|
|
478
|
+
const final = rewriteDatabaseUrlForTests(rewritten);
|
|
479
|
+
|
|
480
|
+
// Hostnames should be rewritten to localhost
|
|
481
|
+
expect(final.POSTGRES_HOST).toBe('localhost');
|
|
482
|
+
expect(final.REDIS_HOST).toBe('localhost');
|
|
483
|
+
|
|
484
|
+
// DATABASE_URL should have localhost and _test suffix
|
|
485
|
+
expect(final.DATABASE_URL).toContain('@localhost:');
|
|
486
|
+
expect(final.DATABASE_URL).toMatch(/\/myapp_test$/);
|
|
487
|
+
|
|
488
|
+
// REDIS_URL should have localhost
|
|
489
|
+
expect(final.REDIS_URL).toContain('@localhost:');
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('should not default to postgres:postgres when secrets are provided', () => {
|
|
493
|
+
const secretsEnv = {
|
|
494
|
+
POSTGRES_USER: 'myapp',
|
|
495
|
+
POSTGRES_PASSWORD: 'strongpass123',
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const env = buildDockerComposeEnv(secretsEnv, {});
|
|
499
|
+
|
|
500
|
+
// The key assertion: secrets are in env so Docker Compose
|
|
501
|
+
// ${POSTGRES_USER:-postgres} resolves to 'myapp', not 'postgres'
|
|
502
|
+
expect(env.POSTGRES_USER).toBe('myapp');
|
|
503
|
+
expect(env.POSTGRES_PASSWORD).toBe('strongpass123');
|
|
504
|
+
});
|
|
505
|
+
});
|