@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.
- package/bootstrap/services/control/Dockerfile +1 -4
- package/bootstrap/services/notification/Dockerfile +1 -4
- package/bootstrap/services/page/Dockerfile +1 -4
- package/bootstrap/services/portal/Dockerfile +1 -4
- package/bootstrap/services/user/Dockerfile +1 -4
- package/package.json +2 -1
- package/src/config.js +8 -4
- package/src/dev/commands.js +10 -2
- package/src/dev/dev.service.js +228 -129
- package/src/services/docker.js +188 -0
- package/templates/service/Dockerfile.hbs +1 -4
|
@@ -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 ./
|
|
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 ./
|
|
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 ./
|
|
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 ./
|
|
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 ./
|
|
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.
|
|
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
|
-
|
|
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 = {
|
package/src/dev/commands.js
CHANGED
|
@@ -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('-
|
|
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);
|
package/src/dev/dev.service.js
CHANGED
|
@@ -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({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
78
|
+
// create and up containers
|
|
58
79
|
const dockerComposePath = path.join(gnarHiddenDir, "docker-compose.dev.yml");
|
|
59
|
-
const dockerCompose = await
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
322
|
+
const provisionerTag = `ge-${config.environment}-${config.namespace}-provisioner`;
|
|
323
|
+
|
|
324
|
+
if (build) {
|
|
325
|
+
await buildImage({
|
|
263
326
|
context: directories.provisioner,
|
|
264
|
-
dockerfile:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
341
|
+
provisionerBinds.push(`${gnarEngineCliConfig.coreDevPath}:${gnarEngineCliConfig.corePath}`);
|
|
278
342
|
}
|
|
279
343
|
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
368
|
+
attach: attachAll,
|
|
369
|
+
network: networkName
|
|
370
|
+
});
|
|
295
371
|
|
|
296
|
-
//
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
485
|
+
ports: {
|
|
486
|
+
[mysqlPortsCounter]: mysqlPortsCounter
|
|
487
|
+
},
|
|
488
|
+
binds: [
|
|
395
489
|
`${gnarHiddenDir}/data/${host}-data:/var/lib/mysql`
|
|
396
490
|
],
|
|
397
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
437
|
-
|
|
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 ./
|
|
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"]
|