@gnar-engine/cli 1.0.6 → 1.0.8

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.
@@ -8,7 +8,7 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/control/package*.json ./
11
+ COPY ./package*.json ./
12
12
 
13
13
  # Install nodemon
14
14
  RUN npm install -g nodemon
@@ -18,6 +18,3 @@ RUN npm install
18
18
 
19
19
  # Expose the port the service will run on
20
20
  EXPOSE 3000
21
-
22
- # Start the application
23
- CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
@@ -8,7 +8,7 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/notification/package*.json ./
11
+ COPY ./package*.json ./
12
12
 
13
13
  # Install nodemon
14
14
  RUN npm install -g nodemon
@@ -18,6 +18,3 @@ RUN npm install
18
18
 
19
19
  # Expose the port the service will run on
20
20
  EXPOSE 3000
21
-
22
- # Start the application
23
- CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
@@ -8,7 +8,7 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/page/package*.json ./
11
+ COPY ./package*.json ./
12
12
 
13
13
  # Install nodemon
14
14
  RUN npm install -g nodemon
@@ -18,6 +18,3 @@ RUN npm install
18
18
 
19
19
  # Expose the port the service will run on
20
20
  EXPOSE 3000
21
-
22
- # Start the application
23
- CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
@@ -8,13 +8,10 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/portal/ ./
11
+ COPY ./ ./
12
12
 
13
13
  # Install app dependencies
14
14
  RUN npm install
15
15
 
16
16
  # Expose the port the service will run on
17
17
  EXPOSE 5173
18
-
19
- # Start the application
20
- CMD ["npm", "run", "start:dev"]
@@ -8,7 +8,7 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/user/package*.json ./
11
+ COPY ./package*.json ./
12
12
 
13
13
  # Install nodemon
14
14
  RUN npm install -g nodemon
@@ -18,6 +18,3 @@ RUN npm install
18
18
 
19
19
  # Expose the port the service will run on
20
20
  EXPOSE 3000
21
-
22
- # Start the application
23
- CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gnar-engine/cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Gnar Engine Development Framework CLI: Project bootstrap, scaffolder & control plane.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  "handlebars": "^4.7.8",
31
31
  "inquirer": "^12.5.2",
32
32
  "js-yaml": "^4.1.0",
33
+ "tar-fs": "^3.1.1",
33
34
  "uuid": "^11.1.0"
34
35
  }
35
36
  }
package/src/config.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
2
6
 
3
7
  export const gnarEngineCliConfig = {
4
8
 
5
- /**
6
- * The path the Gnar Engine service core should be found in the service containers
7
- */
8
- corePath: '/usr/gnar_engine/app/node_modules/@gnar-engine/core'
9
+ // The path the Gnar Engine service core should be found in the service containers
10
+ corePath: '/usr/gnar_engine/app/node_modules/@gnar-engine/core',
9
11
 
12
+ // The path to the core source code on the host machine (core dev mode)
13
+ coreDevPath: path.join(__dirname, '../../core'),
10
14
  }
11
15
 
12
16
  export const directories = {
@@ -12,9 +12,13 @@ export function registerDevCommands(program) {
12
12
  .description('🛠️ Up Development Containers')
13
13
  .option('-b, --build', 'Build without cache')
14
14
  .option('-d, --detach', 'Run containers in background')
15
- .option('-t --test', 'Run the tests with ephemeral databases')
15
+ .option('-a, --attach-all', 'Attach all services including database and message queues for debugging')
16
+ .option('-t --test', 'Run all tests with ephemeral databases *NOT IMPLEMENTED')
16
17
  .option('--test-service <service>', 'Run the tests for the specified service with ephemeral databases (e.g. --test-service user)')
18
+ .option('-reset-databases, --reset-databases', 'Drop all service databases, re-running all migrations and seeders *NOT IMPLEMENTED')
19
+ .option('--reset-database <service>', 'Drop the specified service database, re-running all migrations and seeders (e.g. --reset-database user)')
17
20
  .addOption(new Option('--core-dev').hideHelp())
21
+ .addOption(new Option('--bootstrap-dev').hideHelp())
18
22
  .action(async (options) => {
19
23
  let response = {};
20
24
 
@@ -39,8 +43,12 @@ export function registerDevCommands(program) {
39
43
  build: options.build || false,
40
44
  detach: options.detach || false,
41
45
  coreDev: options.coreDev || false,
46
+ bootstrapDev: options.bootstrapDev || false,
42
47
  test: options.test || false,
43
- testService: options.testService || ''
48
+ testService: options.testService || '',
49
+ resetDatabases: options.resetDatabases || false,
50
+ resetDatabase: options.resetDatabase || '',
51
+ attachAll: options.attachAll || false
44
52
  });
45
53
  } catch (err) {
46
54
  console.error("❌ Error running containers:", err.message);
@@ -3,11 +3,16 @@ import Docker from "dockerode";
3
3
  import process from "process";
4
4
  import fs from "fs/promises";
5
5
  import path from "path";
6
+ import { fileURLToPath } from 'url';
6
7
  import yaml from "js-yaml";
7
8
  import { gnarEngineCliConfig } from "../config.js";
9
+ import { buildImage, createContainer, createBridgeNetwork } from "../services/docker.js";
8
10
  import { directories } from "../config.js";
11
+ import { exec } from 'child_process';
12
+ import { promisify } from 'util';
9
13
 
10
14
  const docker = new Docker();
15
+ const execAsync = promisify(exec);
11
16
 
12
17
  /**
13
18
  * Start the application locally
@@ -19,14 +24,30 @@ const docker = new Docker();
19
24
  * @param {boolean} [options.build=false] - Whether to re-build images
20
25
  * @param {boolean} [options.detach=false] - Whether to run containers in background
21
26
  * @param {boolean} [options.coreDev=false] - Whether to run in core development mode (requires access to core source)
27
+ * @param {boolean} [options.bootstrapDev=false] - Whether to set the cli/src/bootstrap directory as the project directory
22
28
  * @param {boolean} [options.test=false] - Whether to run tests with ephemeral databases
23
29
  * @param {string} [options.testService=''] - The service to run tests for (only applicable if test=true)
30
+ * @param {boolean} [options.resetDatabases=false] - Whether to drop all service databases, re-running all migrations and seeders
31
+ * @param {string} [options.resetDatabase=''] - The service database to drop, re-running all migrations and seeders
24
32
  * @param {boolean} [options.removeOrphans=true] - Whether to remove orphaned containers
33
+ * @param {boolean} [options.attachAll=false] - Attach all services including database and message queues for debugging
25
34
  */
26
- export async function up({ projectDir, build = false, detach = false, coreDev = false, test = false, testService = '', removeOrphans = true }) {
27
-
28
- // core dev
29
- if (coreDev) {
35
+ export async function up({
36
+ projectDir,
37
+ build = false,
38
+ detach = false,
39
+ coreDev = false,
40
+ bootstrapDev = false,
41
+ test = false,
42
+ testService = '',
43
+ resetDatabases = false,
44
+ resetDatabase = '',
45
+ removeOrphans = true,
46
+ attachAll = false
47
+ }) {
48
+
49
+ // bootstrap dev
50
+ if (bootstrapDev) {
30
51
  const fileDir = path.dirname(new URL(import.meta.url).pathname);
31
52
  projectDir = path.resolve(fileDir, "../../bootstrap/");
32
53
  }
@@ -54,52 +75,22 @@ export async function up({ projectDir, build = false, detach = false, coreDev =
54
75
  });
55
76
  await fs.writeFile(nginxConfPath, nginxConf);
56
77
 
57
- // create docker-compose.yml dynamically from parsed config and secrets
78
+ // create and up containers
58
79
  const dockerComposePath = path.join(gnarHiddenDir, "docker-compose.dev.yml");
59
- const dockerCompose = await createDynamicDockerCompose({
80
+ const dockerCompose = await buildAndUpContainers({
60
81
  config: parsedConfig.config,
61
82
  secrets: parsedSecrets,
62
83
  gnarHiddenDir: gnarHiddenDir,
63
84
  projectDir: projectDir,
64
85
  coreDev: coreDev,
86
+ bootstrapDev: bootstrapDev,
65
87
  test: test,
66
- testService: testService
67
- });
68
- await fs.writeFile(dockerComposePath, yaml.dump(dockerCompose));
69
-
70
- // up docker-compose
71
- const args = ["-f", dockerComposePath, "up"];
72
-
73
- if (build) {
74
- args.push("--build");
75
- }
76
-
77
- if (detach) {
78
- args.push("-d");
79
- }
80
-
81
- if (removeOrphans) {
82
- args.push("--remove-orphans")
83
- }
84
-
85
- const processRef = spawn(
86
- "docker-compose",
87
- args,
88
- {
89
- cwd: projectDir,
90
- stdio: "inherit",
91
- shell: "/bin/sh"
92
- }
93
- );
94
-
95
- // handle exit
96
- const exitCode = await new Promise((resolve) => {
97
- processRef.on("close", resolve);
88
+ testService: testService,
89
+ resetDatabases: resetDatabases,
90
+ resetDatabase: resetDatabase,
91
+ attachAll: attachAll,
92
+ build: build,
98
93
  });
99
-
100
- if (exitCode !== 0) {
101
- throw new Error(`docker-compose up exited with code ${exitCode}`);
102
- }
103
94
  }
104
95
 
105
96
  /**
@@ -138,6 +129,15 @@ export async function down({ projectDir, allContainers = false }) {
138
129
  })
139
130
  );
140
131
 
132
+ // // remove bound mounts
133
+ // await Promise.all(
134
+ // containers.map(async c => {
135
+ // const container = docker.getContainer(c.Id);
136
+ // const containerInfo = await container.inspect();
137
+ // await removeBindMounts({ containerInfo: containerInfo });
138
+ // })
139
+ // );
140
+
141
141
  // remove each container
142
142
  await Promise.all(
143
143
  containers.map(c => {
@@ -151,6 +151,26 @@ export async function down({ projectDir, allContainers = false }) {
151
151
  console.log('Containers stopped and removed.');
152
152
  }
153
153
 
154
+ /**
155
+ * Remove bind mounts
156
+ *
157
+ * @param {object} containerInfo
158
+ */
159
+ export async function removeBindMounts({containerInfo}) {
160
+
161
+ const binds = containerInfo.HostConfig.Binds || [];
162
+
163
+ for (const bind of binds) {
164
+ const hostPath = bind.split(':')[0];
165
+ try {
166
+ await execAsync(`sudo umount ${hostPath}`);
167
+ console.log(`Unmounted ${hostPath}`);
168
+ } catch (err) {
169
+ console.warn(`Failed to unmount ${hostPath}: ${err.message}`);
170
+ }
171
+ }
172
+ }
173
+
154
174
  /**
155
175
  * Create dynamic nginx.conf file for running application locally
156
176
  *
@@ -226,19 +246,41 @@ export async function createDynamicNginxConf({ config, serviceConfDir, projectDi
226
246
  * @param {string} gnarHiddenDir
227
247
  * @param {string} projectDir
228
248
  * @param {boolean} coreDev - Whether to volume mount the core source code
249
+ * @param {boolean} bootstrapDev - Whether to set the cli/src/bootstrap directory as the project directory
250
+ * @param {boolean} build - Whether to re-build images
229
251
  * @param {boolean} test - Whether to run tests with ephemeral databases
230
252
  * @param {string} testService - The service to run tests for (only applicable if test=true)
253
+ * @param {boolean} resetDatabases - Whether to drop all service databases, re-running all migrations and seeders
254
+ * @
231
255
  * @param {boolean} attachAll - Whether to attach to all containers' stdio (otherwise databases and message queue are detached)
232
256
  */
233
- async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, projectDir, coreDev = false, test = false, testService, attachAll = false }) {
257
+ async function buildAndUpContainers({
258
+ config,
259
+ secrets,
260
+ gnarHiddenDir,
261
+ projectDir,
262
+ coreDev = false,
263
+ bootstrapDev = false,
264
+ build = false,
265
+ test = false,
266
+ testService,
267
+ resetDatabases = false,
268
+ resetDatabase = '',
269
+ attachAll = false
270
+ }) {
271
+
234
272
  let mysqlPortsCounter = 3306;
235
273
  let mongoPortsCounter = 27017;
236
274
  let mysqlHostsRequired = [];
237
275
  let mongoHostsRequired = [];
238
276
  const services = {};
239
277
 
240
- // test mode env var adjustments
278
+ console.log('======== g n a r e n g i n e ========');
279
+ console.log('⛏️ Starting development environment...');
280
+
281
+ // env var adjustments
241
282
  for (const svc of config.services) {
283
+ // Tests
242
284
  if (test) {
243
285
  if (secrets.services?.[svc.name]?.MYSQL_HOST) {
244
286
  secrets.services[svc.name].MYSQL_HOST = 'db-mysql-test';
@@ -247,71 +289,117 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
247
289
  if (secrets.services?.[svc.name]) {
248
290
  secrets.services[svc.name].NODE_ENV = 'test';
249
291
 
250
- console.log(testService, svc.name);
251
292
  if (testService && svc.name === testService) {
252
293
  secrets.services[svc.name].RUN_TESTS = 'true';
253
294
  }
254
295
  }
255
296
  }
297
+
298
+ // Reset all databases
299
+ if (resetDatabases) {
300
+ if (secrets.services?.[svc.name]) {
301
+ secrets.services[svc.name].RESET_DATABASE = true;
302
+ }
303
+ }
304
+
305
+ // Reset specific database
306
+ else if (resetDatabase) {
307
+ if (svc.name === resetDatabase) {
308
+ if (secrets.services?.[svc.name]) {
309
+ secrets.services[svc.name].RESET_DATABASE = true;
310
+ }
311
+ }
312
+ }
256
313
  }
257
314
 
315
+ // create bridge network
316
+ const networkName = `ge-${config.environment}-${config.namespace}`;
317
+ createBridgeNetwork({
318
+ name: networkName
319
+ })
320
+
258
321
  // provision the provisioner service
259
- services['provisioner'] = {
260
- container_name: `ge-${config.environment}-${config.namespace}-provisioner`,
261
- image: `ge-${config.environment}-${config.namespace}-provisioner`,
262
- build: {
322
+ const provisionerTag = `ge-${config.environment}-${config.namespace}-provisioner`;
323
+
324
+ if (build) {
325
+ await buildImage({
263
326
  context: directories.provisioner,
264
- dockerfile: `./Dockerfile`
265
- },
266
- environment: {
267
- PROVISIONER_SECRETS: JSON.stringify(secrets)
268
- },
269
- volumes: [
270
- `${directories.provisioner}/src:/usr/gnar_engine/app/src`
271
- ],
272
- restart: 'no',
273
- attach: attachAll
327
+ dockerfile: 'Dockerfile',
328
+ imageTag: provisionerTag,
329
+ nocache: false
330
+ });
274
331
  }
275
332
 
333
+ const __filename = fileURLToPath(import.meta.url);
334
+ const __dirname = path.dirname(__filename);
335
+
336
+ const provisionerBinds = [
337
+ `${path.resolve(__dirname, '../provisioner', 'src')}:/usr/gnar_engine/app/src`
338
+ ];
339
+
276
340
  if (coreDev) {
277
- services['provisioner'].volumes.push(`../../../core/:${gnarEngineCliConfig.corePath}`);
341
+ provisionerBinds.push(`${gnarEngineCliConfig.coreDevPath}:${gnarEngineCliConfig.corePath}`);
278
342
  }
279
343
 
280
- // nginx
281
- services['nginx'] = {
344
+ const provisioner = await createContainer({
345
+ name: provisionerTag,
346
+ image: provisionerTag,
347
+ env: {
348
+ PROVISIONER_SECRETS: JSON.stringify(secrets)
349
+ },
350
+ ports: {},
351
+ binds: provisionerBinds,
352
+ restart: 'no',
353
+ attach: attachAll,
354
+ network: networkName
355
+ });
356
+ services[provisionerTag] = provisioner;
357
+
358
+ // Nginx
359
+ const nginxName = `ge-${config.environment}-${config.namespace}-nginx`;
360
+ services[nginxName] = await createContainer({
361
+ name: nginxName,
282
362
  image: 'nginx:latest',
283
- container_name: `ge-${config.environment}-${config.namespace}-nginx`,
284
- ports: [
285
- "80:80",
286
- "443:443"
287
- ],
288
- volumes: [
363
+ ports: { 80: 80, 443: 443 },
364
+ binds: [
289
365
  `${gnarHiddenDir}/nginx/nginx.conf:/etc/nginx/nginx.conf`,
290
366
  `${gnarHiddenDir}/nginx/service_conf:/etc/nginx/service_conf`
291
367
  ],
292
- restart: 'always',
293
- attach: attachAll
294
- }
368
+ attach: attachAll,
369
+ network: networkName
370
+ });
295
371
 
296
- // rabbit
297
- services['rabbitmq'] = {
372
+ // Rabbit MQ
373
+ const rabbitMqName = `ge-${config.environment}-${config.namespace}-rabbitmq`;
374
+ services[rabbitMqName] = await createContainer({
375
+ name: rabbitMqName,
298
376
  image: 'rabbitmq:management',
299
- container_name: `ge-${config.environment}-${config.namespace}-rabbitmq`,
300
- ports: [
301
- "5672:5672",
302
- "15672:15672"
303
- ],
304
- environment: {
377
+ env: {
305
378
  RABBITMQ_DEFAULT_USER: secrets.global.RABBITMQ_USER || '',
306
379
  RABBITMQ_DEFAULT_PASS: secrets.global.RABBITMQ_PASS || ''
307
380
  },
308
- restart: 'always',
309
- attach: attachAll
310
- }
381
+ ports: { 5672: 5672, 15672: 15672 },
382
+ binds: [],
383
+ attach: attachAll,
384
+ network: networkName,
385
+ aliases: ['rabbitmq']
386
+ });
311
387
 
312
388
  // services
313
389
  for (const svc of config.services) {
314
390
 
391
+ // build service image
392
+ const svcTag = `ge-${config.environment}-${config.namespace}-${svc.name}`;
393
+
394
+ if (build) {
395
+ await buildImage({
396
+ context: path.resolve(projectDir, 'services', svc.name),
397
+ dockerfile: 'Dockerfile',
398
+ imageTag: svcTag,
399
+ nocache: false
400
+ });
401
+ }
402
+
315
403
  // env variables
316
404
  const serviceEnvVars = secrets.services?.[svc.name] || {};
317
405
  const localisedServiceEnvVars = {};
@@ -333,29 +421,35 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
333
421
  }
334
422
  }
335
423
 
336
- // service block
337
- services[`${svc.name}-service`] = {
338
- container_name: `ge-${config.environment}-${config.namespace}-${svc.name}`,
339
- image: `ge-${config.environment}-${config.namespace}-${svc.name}`,
340
- build: {
341
- context: projectDir,
342
- dockerfile: `./services/${svc.name}/Dockerfile`
343
- },
344
- command: svc.command || [],
345
- environment: env,
346
- ports: svc.ports || [],
347
- depends_on: svc.depends_on || [],
348
- volumes: [
349
- `${projectDir}/services/${svc.name}/src:/usr/gnar_engine/app/src`
350
- ],
351
- restart: 'always'
352
- };
353
-
354
424
  // add the core source code mount if in coreDev mode
425
+ const serviceVolumes = [
426
+ `${path.resolve(projectDir, 'services', svc.name, 'src')}:/usr/gnar_engine/app/src`
427
+ ];
428
+
355
429
  if (coreDev) {
356
- services[`${svc.name}-service`].volumes.push(`../../../core/:${gnarEngineCliConfig.corePath}`);
430
+ serviceVolumes.push(`${gnarEngineCliConfig.coreDevPath}:${gnarEngineCliConfig.corePath}`);
431
+ }
432
+
433
+ // split from "port:port" to { port: port }
434
+ const ports = {};
435
+ for (const portMapping of svc.ports || []) {
436
+ const [hostPort, containerPort] = portMapping.split(':').map(p => parseInt(p, 10));
437
+ ports[containerPort] = hostPort;
357
438
  }
358
439
 
440
+ services[svcTag] = await createContainer({
441
+ name: svcTag,
442
+ image: svcTag,
443
+ command: svc.command || [],
444
+ env: env,
445
+ ports: ports,
446
+ binds: serviceVolumes,
447
+ restart: 'no',
448
+ attach: svc.detach ? !svc.detach : true,
449
+ network: networkName,
450
+ aliases: [`${svc.name}-service`]
451
+ });
452
+
359
453
  // check if mysql service required
360
454
  if (
361
455
  serviceEnvVars.MYSQL_HOST &&
@@ -380,29 +474,30 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
380
474
  continue;
381
475
  }
382
476
 
383
- services[host] = {
384
- container_name: `ge-${config.environment}-${config.namespace}-${host}`,
477
+ const mysqlContainerName = `ge-${config.environment}-${config.namespace}-${host}`;
478
+ services[mysqlContainerName] = await createContainer({
479
+ name: `ge-${config.environment}-${config.namespace}-${host}`,
385
480
  image: 'mysql',
386
- ports: [
387
- `${mysqlPortsCounter}:${mysqlPortsCounter}`
388
- ],
389
- restart: 'always',
390
- environment: {
481
+ env: {
391
482
  MYSQL_HOST: host,
392
483
  MYSQL_ROOT_PASSWORD: secrets.provision.MYSQL_ROOT_PASSWORD
393
484
  },
394
- volumes: [
485
+ ports: {
486
+ [mysqlPortsCounter]: mysqlPortsCounter
487
+ },
488
+ binds: [
395
489
  `${gnarHiddenDir}/data/${host}-data:/var/lib/mysql`
396
490
  ],
397
- attach: attachAll
398
- };
491
+ restart: 'always',
492
+ attach: attachAll,
493
+ network: networkName,
494
+ aliases: [host]
495
+ });
399
496
 
400
497
  mysqlPortsCounter++;
401
498
  }
402
-
403
- services['provisioner'].depends_on = [...new Set(mysqlHostsRequired)];
404
499
  }
405
-
500
+
406
501
  // add mongo hosts if required
407
502
  if (mongoHostsRequired.length > 0) {
408
503
  for (const host of mongoHostsRequired) {
@@ -410,33 +505,37 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
410
505
  continue;
411
506
  }
412
507
 
413
- services[host] = {
414
- container_name: `ge-${config.environment}-${config.namespace}-${host}`,
508
+ const mongoContainerName = `ge-${config.environment}-${config.namespace}-${host}`;
509
+ services[mongoContainerName] = await createContainer({
510
+ name: `ge-${config.environment}-${config.namespace}-${host}`,
415
511
  image: 'mongo:latest',
416
- ports: [
417
- `${mongoPortsCounter}:27017`
418
- ],
419
- restart: 'always',
420
- environment: {
512
+ env: {
421
513
  MONGO_INITDB_ROOT_USERNAME: 'root',
422
514
  MONGO_INITDB_ROOT_PASSWORD: secrets.provision.MONGO_ROOT_PASSWORD
423
515
  },
424
- volumes: [
516
+ ports: {
517
+ [mongoPortsCounter]: 27017
518
+ },
519
+ binds: [
425
520
  `${gnarHiddenDir}/data/${host}-data:/data/db`,
426
- './mongo-init-scripts:/docker-entrypoint-initdb.d'
521
+ //'./mongo-init-scripts:/docker-entrypoint-initdb.d'
427
522
  ],
428
- attach: attachAll
429
- };
523
+ restart: 'always',
524
+ attach: attachAll,
525
+ network: networkName,
526
+ aliases: [host]
527
+ });
430
528
 
431
529
  // increment mongo port for next service as required
432
530
  mongoPortsCounter++;
433
531
  }
434
532
  }
435
533
 
436
- return {
437
- version: "3.9",
438
- services
439
- }
534
+ // start the containers
535
+ Object.keys(services).forEach(async (key) => {
536
+ const container = services[key];
537
+ container.start();
538
+ });
440
539
  }
441
540
 
442
541
  /**
@@ -0,0 +1,188 @@
1
+ import Docker from 'dockerode';
2
+ import { Writable } from 'stream';
3
+ import path from 'path';
4
+ import tar from 'tar-fs';
5
+ import fs from 'fs';
6
+
7
+ const docker = new Docker();
8
+
9
+ /**
10
+ * Build Docker image
11
+ *
12
+ * @param {Object} options
13
+ * @param {string} options.context - The build context directory
14
+ * @param {string} options.dockerfile - The Dockerfile path relative to the context
15
+ * @param {string} options.imageTag - The tag to assign to the built image
16
+ * @param {boolean} [options.nocache=true] - Whether to build without cache
17
+ */
18
+ export async function buildImage({ context, dockerfile, imageTag, nocache = true }) {
19
+
20
+ console.log('Building image...', imageTag);
21
+
22
+ // Create a tar stream of the full context folder
23
+ const tarStream = tar.pack(context);
24
+
25
+ const stream = await docker.buildImage(tarStream, {
26
+ t: imageTag,
27
+ dockerfile,
28
+ nocache
29
+ });
30
+
31
+ await new Promise((resolve, reject) => {
32
+ docker.modem.followProgress(
33
+ stream,
34
+ (err, res) => {
35
+ if (err) return reject(err);
36
+ resolve(res);
37
+ },
38
+ (event) => {
39
+ if (event.stream) process.stdout.write(event.stream);
40
+
41
+ // Catch BuildKit-style errors
42
+ if (event.error) return reject(new Error(event.error));
43
+ if (event.errorDetail && event.errorDetail.message) {
44
+ return reject(new Error(event.errorDetail.message));
45
+ }
46
+ }
47
+ );
48
+ });
49
+
50
+ console.log('Built image:', imageTag);
51
+ }
52
+
53
+ /**
54
+ * Create docker container
55
+ *
56
+ * @param {Object} options
57
+ * @param {string} options.name - Container name
58
+ * @param {string} options.image - Docker image to use
59
+ * @param {Array} [options.command] - Command to run in the container
60
+ * @param {Object} [options.env] - Environment variables
61
+ * @param {Object} [options.ports] - Port mappings (containerPort: hostPort)
62
+ * @param {Array} [options.binds] - Volume bindings
63
+ * @param {string} [options.restart] - Restart policy
64
+ * @param {boolean} [options.attach] - Whether to attach to container output
65
+ * @param {string} [options.network] - Network name
66
+ * @returns {Promise<Docker.Container>} - The started container
67
+ */
68
+ export async function createContainer({ name, image, command = [], env = {}, ports = {}, binds = [], restart = 'always', attach = true, network, aliases = [] }) {
69
+
70
+ // remove container first
71
+ try {
72
+ const existingContainer = docker.getContainer(name);
73
+ await existingContainer.inspect();
74
+ await existingContainer.remove({ force: true });
75
+ } catch (err) {
76
+ // Container does not exist, ignore
77
+ }
78
+
79
+ // create container
80
+ const containerConfig = {
81
+ name,
82
+ Image: image,
83
+ Cmd: command,
84
+ Env: Object.entries(env).map(([k, v]) => `${k}=${v}`),
85
+ HostConfig: {
86
+ RestartPolicy: { Name: restart },
87
+ Binds: binds,
88
+ PortBindings: Object.fromEntries(
89
+ Object.entries(ports).map(([cPort, hPort]) => [
90
+ `${cPort}/tcp`,
91
+ [{ HostPort: String(hPort) }]
92
+ ])
93
+ ),
94
+ },
95
+ ExposedPorts: Object.fromEntries(
96
+ Object.keys(ports).map(p => [`${p}/tcp`, {}])
97
+ )
98
+ };
99
+
100
+ const container = await docker.createContainer(containerConfig);
101
+
102
+ // Attach logs before starting the container
103
+ if (attach) {
104
+ const stream = await container.attach({ stream: true, stdout: true, stderr: true, logs: true });
105
+ const stdoutStream = createPrefixStream(name, process.stdout);
106
+ const stderrStream = createPrefixStream(name, process.stderr);
107
+
108
+ container.modem.demuxStream(stream, stdoutStream, stderrStream);
109
+ }
110
+
111
+ await docker.getNetwork(network).connect({
112
+ Container: container.id,
113
+ EndpointConfig: {
114
+ Aliases: aliases
115
+ }
116
+ });
117
+
118
+ return container;
119
+ }
120
+
121
+ /**
122
+ * Create network
123
+ *
124
+ * @param {String} name - Network name
125
+ */
126
+ export async function createBridgeNetwork({ name }) {
127
+ try {
128
+ await docker.createNetwork({
129
+ Name: name,
130
+ Driver: 'bridge',
131
+ CheckDuplicate: true
132
+ });
133
+ } catch (err) {
134
+ if (err.statusCode === 409) {
135
+ // network already exists, ignore
136
+ return;
137
+ }
138
+ console.error(err);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Tansform stream
144
+ *
145
+ * @param {String} name - Container name
146
+ * @returns {Transform} - Transform stream
147
+ */
148
+ function createPrefixStream(name, targetStream) {
149
+ const color = colorForName(name);
150
+ const labelWidth = 38;
151
+ name = '[' + name + ']';
152
+ const paddedName = name.padEnd(labelWidth, ' ');
153
+
154
+ return new Writable({
155
+ write(chunk, encoding, callback) {
156
+ const lines = chunk.toString().split(/\r?\n/);
157
+ for (const line of lines) {
158
+ if (line.trim()) {
159
+ targetStream.write(`${color}${paddedName}${RESET} ${line}\n`);
160
+ }
161
+ }
162
+ callback();
163
+ }
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Colours for container name
169
+ */
170
+ const COLORS = [
171
+ '\x1b[31m', // red
172
+ '\x1b[32m', // green
173
+ '\x1b[33m', // yellow
174
+ '\x1b[34m', // blue
175
+ '\x1b[35m', // magenta
176
+ '\x1b[36m', // cyan
177
+ '\x1b[37m', // white
178
+ ];
179
+
180
+ const RESET = '\x1b[0m';
181
+
182
+ function colorForName(name) {
183
+ let hash = 0;
184
+ for (let i = 0; i < name.length; i++) {
185
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
186
+ }
187
+ return COLORS[Math.abs(hash) % COLORS.length];
188
+ }
@@ -8,7 +8,7 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/{{serviceName}}/package*.json ./
11
+ COPY ./package*.json ./
12
12
 
13
13
  # Install nodemon
14
14
  RUN npm install -g nodemon
@@ -18,6 +18,3 @@ RUN npm install
18
18
 
19
19
  # Expose the port the service will run on
20
20
  EXPOSE 3000
21
-
22
- # Start the application
23
- CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]