@gnar-engine/cli 1.0.4 → 1.0.5

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.
Files changed (119) hide show
  1. package/bootstrap/deploy.localdev.yml +30 -3
  2. package/bootstrap/secrets.localdev.yml +15 -4
  3. package/bootstrap/services/control/src/config.js +4 -0
  4. package/bootstrap/services/page/Dockerfile +23 -0
  5. package/bootstrap/services/page/package.json +16 -0
  6. package/bootstrap/services/page/src/app.js +50 -0
  7. package/bootstrap/services/page/src/commands/block.handler.js +94 -0
  8. package/bootstrap/services/page/src/commands/page.handler.js +167 -0
  9. package/bootstrap/services/page/src/config.js +62 -0
  10. package/bootstrap/services/page/src/controllers/block.http.controller.js +87 -0
  11. package/bootstrap/services/page/src/controllers/message.controller.js +51 -0
  12. package/bootstrap/services/page/src/controllers/page.http.controller.js +89 -0
  13. package/bootstrap/services/page/src/policies/block.policy.js +50 -0
  14. package/bootstrap/services/page/src/policies/page.policy.js +49 -0
  15. package/bootstrap/services/page/src/schema/page.schema.js +139 -0
  16. package/bootstrap/services/page/src/services/block.service.js +83 -0
  17. package/bootstrap/services/page/src/services/page.service.js +83 -0
  18. package/bootstrap/services/portal/Dockerfile +20 -0
  19. package/bootstrap/services/portal/README.md +73 -0
  20. package/bootstrap/services/portal/index.html +13 -0
  21. package/bootstrap/services/portal/nginx.conf +5 -0
  22. package/bootstrap/services/portal/package.json +33 -0
  23. package/bootstrap/services/portal/public/vite.svg +1 -0
  24. package/bootstrap/services/portal/react-router.config.js +7 -0
  25. package/bootstrap/services/portal/src/App.jsx +16 -0
  26. package/bootstrap/services/portal/src/assets/gnar-engine-white-logo.svg +9 -0
  27. package/bootstrap/services/portal/src/assets/icon-agent.svg +6 -0
  28. package/bootstrap/services/portal/src/assets/icon-cog.svg +4 -0
  29. package/bootstrap/services/portal/src/assets/icon-delete.svg +3 -0
  30. package/bootstrap/services/portal/src/assets/icon-home.svg +3 -0
  31. package/bootstrap/services/portal/src/assets/icon-padlock.svg +3 -0
  32. package/bootstrap/services/portal/src/assets/icon-page.svg +6 -0
  33. package/bootstrap/services/portal/src/assets/icon-reports.svg +3 -0
  34. package/bootstrap/services/portal/src/assets/icon-user.svg +3 -0
  35. package/bootstrap/services/portal/src/assets/icon-users.svg +3 -0
  36. package/bootstrap/services/portal/src/assets/login-green-rad-back-1.jpg +0 -0
  37. package/bootstrap/services/portal/src/assets/react.svg +1 -0
  38. package/bootstrap/services/portal/src/components/CrudList/CrudList.jsx +85 -0
  39. package/bootstrap/services/portal/src/components/CrudList/CrudList.less +59 -0
  40. package/bootstrap/services/portal/src/components/CustomSelect/CustomSelect.jsx +81 -0
  41. package/bootstrap/services/portal/src/components/CustomSelect/CustomSelect.less +0 -0
  42. package/bootstrap/services/portal/src/components/LoginForm/LoginForm.jsx +58 -0
  43. package/bootstrap/services/portal/src/components/PageBlockSwitch/PageBlockSwitch.jsx +129 -0
  44. package/bootstrap/services/portal/src/components/Sidebar/Sidebar.jsx +33 -0
  45. package/bootstrap/services/portal/src/components/Sidebar/Sidebar.less +37 -0
  46. package/bootstrap/services/portal/src/components/Topbar/Topbar.jsx +19 -0
  47. package/bootstrap/services/portal/src/components/Topbar/Topbar.less +22 -0
  48. package/bootstrap/services/portal/src/components/UserInfo/UserInfo.jsx +33 -0
  49. package/bootstrap/services/portal/src/components/UserInfo/UserInfo.less +21 -0
  50. package/bootstrap/services/portal/src/css/style.css +711 -0
  51. package/bootstrap/services/portal/src/data/pages.data.js +10 -0
  52. package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.jsx +65 -0
  53. package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.less +102 -0
  54. package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.jsx +115 -0
  55. package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.less +43 -0
  56. package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.jsx +124 -0
  57. package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.less +0 -0
  58. package/bootstrap/services/portal/src/elements/Repeater/Repeater.jsx +52 -0
  59. package/bootstrap/services/portal/src/elements/Repeater/Repeater.less +70 -0
  60. package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.jsx +18 -0
  61. package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.less +37 -0
  62. package/bootstrap/services/portal/src/elements/SaveButton/SaveButton.jsx +45 -0
  63. package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.jsx +63 -0
  64. package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.less +23 -0
  65. package/bootstrap/services/portal/src/elements/TextInput/TextInput.jsx +17 -0
  66. package/bootstrap/services/portal/src/layouts/Card/Card.jsx +15 -0
  67. package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.jsx +29 -0
  68. package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.less +49 -0
  69. package/bootstrap/services/portal/src/main.jsx +51 -0
  70. package/bootstrap/services/portal/src/pages/BlockSinglePage/BlockSinglePage.jsx +277 -0
  71. package/bootstrap/services/portal/src/pages/BlocksPage/BlocksPage.jsx +23 -0
  72. package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.jsx +11 -0
  73. package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.less +0 -0
  74. package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.jsx +21 -0
  75. package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.less +51 -0
  76. package/bootstrap/services/portal/src/pages/PageSinglePage/PageSinglePage.jsx +338 -0
  77. package/bootstrap/services/portal/src/pages/PagesPage/PagesPage.jsx +23 -0
  78. package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.jsx +9 -0
  79. package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.less +0 -0
  80. package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.jsx +25 -0
  81. package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.less +0 -0
  82. package/bootstrap/services/portal/src/services/block.js +28 -0
  83. package/bootstrap/services/portal/src/services/client.js +67 -0
  84. package/bootstrap/services/portal/src/services/gravatar.js +14 -0
  85. package/bootstrap/services/portal/src/services/page.js +28 -0
  86. package/bootstrap/services/portal/src/services/storage.js +62 -0
  87. package/bootstrap/services/portal/src/services/user.js +41 -0
  88. package/bootstrap/services/portal/src/slices/authSlice.js +101 -0
  89. package/bootstrap/services/portal/src/store/configureStore.js +10 -0
  90. package/bootstrap/services/portal/src/style/cards.less +57 -0
  91. package/bootstrap/services/portal/src/style/global.less +204 -0
  92. package/bootstrap/services/portal/src/style/icons.less +21 -0
  93. package/bootstrap/services/portal/src/style/inputs.less +52 -0
  94. package/bootstrap/services/portal/src/style/main.less +28 -0
  95. package/bootstrap/services/portal/src/utils/utils.js +9 -0
  96. package/bootstrap/services/portal/vite.config.js +12 -0
  97. package/bootstrap/services/user/src/app.js +6 -1
  98. package/bootstrap/services/user/src/commands/user.handler.js +0 -3
  99. package/bootstrap/services/user/src/config.js +5 -1
  100. package/bootstrap/services/user/src/policies/user.policy.js +3 -1
  101. package/bootstrap/services/user/src/tests/commands/user.test.js +22 -0
  102. package/install-from-clone.sh +30 -0
  103. package/package.json +1 -1
  104. package/src/cli.js +8 -0
  105. package/src/dev/commands.js +10 -2
  106. package/src/dev/dev.service.js +147 -60
  107. package/src/provisioner/Dockerfile +27 -0
  108. package/src/provisioner/package.json +19 -0
  109. package/src/provisioner/src/app.js +56 -0
  110. package/src/provisioner/src/services/mongodb.js +58 -0
  111. package/src/provisioner/src/services/mysql.js +51 -0
  112. package/src/provisioner/src/services/secrets.js +84 -0
  113. package/src/scaffolder/commands.js +1 -1
  114. package/src/scaffolder/scaffolder.handler.js +40 -15
  115. package/templates/service/src/app.js.hbs +12 -1
  116. package/templates/service/src/commands/{{serviceName}}.handler.js.hbs +1 -1
  117. package/templates/service/src/mongodb.config.js.hbs +5 -1
  118. package/templates/service/src/mysql.config.js.hbs +4 -0
  119. package/bootstrap/services/user/src/tests/user.test.js +0 -126
@@ -5,6 +5,7 @@ import fs from "fs/promises";
5
5
  import path from "path";
6
6
  import yaml from "js-yaml";
7
7
  import { gnarEngineCliConfig } from "../config.js";
8
+ import { directories } from "../cli.js";
8
9
 
9
10
  const docker = new Docker();
10
11
 
@@ -18,8 +19,11 @@ const docker = new Docker();
18
19
  * @param {boolean} [options.build=false] - Whether to re-build images
19
20
  * @param {boolean} [options.detached=false] - Whether to run containers in background
20
21
  * @param {boolean} [options.coreDev=false] - Whether to run in core development mode (requires access to core source)
22
+ * @param {boolean} [options.test=false] - Whether to run tests with ephemeral databases
23
+ * @param {string} [options.testService=''] - The service to run tests for (only applicable if test=true)
24
+ * @param {boolean} [options.removeOrphans=true] - Whether to remove orphaned containers
21
25
  */
22
- export async function up({ projectDir, build = false, detached = false, coreDev = false }) {
26
+ export async function up({ projectDir, build = false, detached = false, coreDev = false, test = false, testService = '', removeOrphans = true }) {
23
27
 
24
28
  // core dev
25
29
  if (coreDev) {
@@ -40,8 +44,13 @@ export async function up({ projectDir, build = false, detached = false, coreDev
40
44
 
41
45
  // create nginx.conf dynamically from configPath
42
46
  const nginxConfPath = path.join(gnarHiddenDir, "nginx", "nginx.conf");
47
+ const serviceConfDir = path.join(gnarHiddenDir, "nginx", "service_conf")
48
+ await fs.mkdir(serviceConfDir, { recursive: true });
49
+
43
50
  const nginxConf = await createDynamicNginxConf({
44
- config: parsedConfig.config
51
+ config: parsedConfig.config,
52
+ projectDir: projectDir,
53
+ serviceConfDir: serviceConfDir
45
54
  });
46
55
  await fs.writeFile(nginxConfPath, nginxConf);
47
56
 
@@ -52,7 +61,9 @@ export async function up({ projectDir, build = false, detached = false, coreDev
52
61
  secrets: parsedSecrets,
53
62
  gnarHiddenDir: gnarHiddenDir,
54
63
  projectDir: projectDir,
55
- coreDev: coreDev
64
+ coreDev: coreDev,
65
+ test: test,
66
+ testService: testService
56
67
  });
57
68
  await fs.writeFile(dockerComposePath, yaml.dump(dockerCompose));
58
69
 
@@ -67,6 +78,10 @@ export async function up({ projectDir, build = false, detached = false, coreDev
67
78
  args.push("-d");
68
79
  }
69
80
 
81
+ if (removeOrphans) {
82
+ args.push("--remove-orphans")
83
+ }
84
+
70
85
  const processRef = spawn(
71
86
  "docker-compose",
72
87
  args,
@@ -128,9 +143,10 @@ export async function down({ projectDir, allContainers = false }) {
128
143
  * Create dynamic nginx.conf file for running application locally
129
144
  *
130
145
  * @param {object} config
131
- * @param {string} outputPath - where to write nginx.conf
146
+ * @param {string} serviceConfDir
147
+ * @param {string} projectDir
132
148
  */
133
- export async function createDynamicNginxConf({ config, outputPath }) {
149
+ export async function createDynamicNginxConf({ config, serviceConfDir, projectDir }) {
134
150
  // Start with the static parts of nginx.conf
135
151
  let nginxConf = `
136
152
  events { worker_connections 1024; }
@@ -139,10 +155,29 @@ export async function createDynamicNginxConf({ config, outputPath }) {
139
155
  server {
140
156
  listen 80;
141
157
  server_name ${config.namespace};
158
+ include /etc/nginx/service_conf/*.conf;
159
+
142
160
  `;
143
161
 
144
162
  // Loop over each service
145
163
  for (const service of config.services || []) {
164
+ // Check if override is present and add conf to service_conf dir
165
+ const serviceDir = path.join(projectDir, 'services', service.name);
166
+
167
+ if (await fs.stat(serviceDir).then(() => true).catch(() => false)) {
168
+ const overridePath = path.join(serviceDir, 'nginx.conf');
169
+ if (await fs.stat(overridePath).then(() => true).catch(() => false)) {
170
+ const overrideConf = await fs.readFile(overridePath, 'utf8');
171
+
172
+ // write to service_conf directory
173
+ const serviceConfPath = path.join(serviceConfDir, `${service.name}.conf`);
174
+ await fs.writeFile(serviceConfPath, overrideConf);
175
+
176
+ continue;
177
+ }
178
+ }
179
+
180
+ // Otherwise create generic conf block
146
181
  const serviceName = service.name;
147
182
  const paths = service.listener_rules?.paths || [];
148
183
  const containerPort = service.ports && service.ports.length > 0 ? service.ports[0].split(':')[1] : '3000';
@@ -179,12 +214,56 @@ export async function createDynamicNginxConf({ config, outputPath }) {
179
214
  * @param {string} gnarHiddenDir
180
215
  * @param {string} projectDir
181
216
  * @param {boolean} coreDev - Whether to volume mount the core source code
217
+ * @param {boolean} test - Whether to run tests with ephemeral databases
218
+ * @param {string} testService - The service to run tests for (only applicable if test=true)
219
+ * @param {boolean} attachAll - Whether to attach to all containers' stdio (otherwise databases and message queue are detached)
182
220
  */
183
- async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, projectDir, coreDev = false }) {
221
+ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, projectDir, coreDev = false, test = false, testService, attachAll = false }) {
184
222
  let mysqlPortsCounter = 3306;
185
223
  let mongoPortsCounter = 27017;
224
+ let mysqlHostsRequired = [];
225
+ let mongoHostsRequired = [];
186
226
  const services = {};
187
-
227
+
228
+ // test mode env var adjustments
229
+ for (const svc of config.services) {
230
+ if (test) {
231
+ if (secrets.services?.[svc.name]?.MYSQL_HOST) {
232
+ secrets.services[svc.name].MYSQL_HOST = 'db-mysql-test';
233
+ }
234
+
235
+ if (secrets.services?.[svc.name]) {
236
+ secrets.services[svc.name].NODE_ENV = 'test';
237
+
238
+ if (testService && svc.name === testService) {
239
+ secrets.services[svc.name].RUN_TESTS = 'true';
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ // provision the provisioner service
246
+ services['provisioner'] = {
247
+ container_name: `ge-${config.environment}-${config.namespace}-provisioner`,
248
+ image: `ge-${config.environment}-${config.namespace}-provisioner`,
249
+ build: {
250
+ context: directories.provisioner,
251
+ dockerfile: `./Dockerfile`
252
+ },
253
+ environment: {
254
+ PROVISIONER_SECRETS: JSON.stringify(secrets)
255
+ },
256
+ volumes: [
257
+ `${directories.provisioner}/src:/usr/gnar_engine/app/src`
258
+ ],
259
+ restart: 'no',
260
+ attach: attachAll
261
+ }
262
+
263
+ if (coreDev) {
264
+ services['provisioner'].volumes.push(`../../../core/:${gnarEngineCliConfig.corePath}`);
265
+ }
266
+
188
267
  // nginx
189
268
  services['nginx'] = {
190
269
  image: 'nginx:latest',
@@ -194,9 +273,11 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
194
273
  "443:443"
195
274
  ],
196
275
  volumes: [
197
- `${gnarHiddenDir}/nginx/nginx.conf:/etc/nginx/nginx.conf`
276
+ `${gnarHiddenDir}/nginx/nginx.conf:/etc/nginx/nginx.conf`,
277
+ `${gnarHiddenDir}/nginx/service_conf:/etc/nginx/service_conf`
198
278
  ],
199
- restart: 'always'
279
+ restart: 'always',
280
+ attach: attachAll
200
281
  }
201
282
 
202
283
  // rabbit
@@ -211,7 +292,8 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
211
292
  RABBITMQ_DEFAULT_USER: secrets.global.RABBITMQ_USER || '',
212
293
  RABBITMQ_DEFAULT_PASS: secrets.global.RABBITMQ_PASS || ''
213
294
  },
214
- restart: 'always'
295
+ restart: 'always',
296
+ attach: attachAll
215
297
  }
216
298
 
217
299
  // services
@@ -220,7 +302,7 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
220
302
  // env variables
221
303
  const serviceEnvVars = secrets.services?.[svc.name] || {};
222
304
  const localisedServiceEnvVars = {};
223
-
305
+
224
306
  for (const [key, value] of Object.entries(serviceEnvVars)) {
225
307
  localisedServiceEnvVars[svc.name.toUpperCase() + '_' + key] = value;
226
308
  }
@@ -230,6 +312,14 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
230
312
  ...(localisedServiceEnvVars || {})
231
313
  };
232
314
 
315
+ // test mode adjustments
316
+ if (test) {
317
+ if (svc.depends_on && svc.depends_on.includes('db-mysql')) {
318
+ svc.depends_on = svc.depends_on.filter(d => d !== 'db-mysql');
319
+ svc.depends_on.push('db-mysql-test');
320
+ }
321
+ }
322
+
233
323
  // service block
234
324
  services[`${svc.name}-service`] = {
235
325
  container_name: `ge-${config.environment}-${config.namespace}-${svc.name}`,
@@ -248,69 +338,67 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
248
338
  restart: 'always'
249
339
  };
250
340
 
251
- // add the core source code mount if in coreDeve mode
341
+ // add the core source code mount if in coreDev mode
252
342
  if (coreDev) {
253
343
  services[`${svc.name}-service`].volumes.push(`../../../core/:${gnarEngineCliConfig.corePath}`);
254
344
  }
255
345
 
256
- // add a mysql instance if required
346
+ // check if mysql service required
257
347
  if (
258
348
  serviceEnvVars.MYSQL_HOST &&
259
- serviceEnvVars.MYSQL_DATABASE &&
260
- serviceEnvVars.MYSQL_USER &&
261
- serviceEnvVars.MYSQL_PASSWORD &&
262
- serviceEnvVars.MYSQL_RANDOM_ROOT_PASSWORD
349
+ secrets.provision?.MYSQL_ROOT_PASSWORD
263
350
  ) {
264
- services[`${svc.name}-db`] = {
265
- container_name: `ge-${config.environment}-${config.namespace}-${svc.name}-db`,
351
+ mysqlHostsRequired.push(serviceEnvVars.MYSQL_HOST);
352
+ }
353
+
354
+ // add a mongodb instance if required
355
+ if (
356
+ serviceEnvVars.MONGO_HOST &&
357
+ secrets.provision?.MONGO_ROOT_PASSWORD
358
+ ) {
359
+ mongoHostsRequired.push(serviceEnvVars.MONGO_HOST);
360
+ }
361
+ }
362
+
363
+ // add mysql if required
364
+ if (mysqlHostsRequired.length > 0) {
365
+ for (const host of mysqlHostsRequired) {
366
+ if (services[host]) {
367
+ continue;
368
+ }
369
+
370
+ services[host] = {
371
+ container_name: `ge-${config.environment}-${config.namespace}-${host}`,
266
372
  image: 'mysql',
267
373
  ports: [
268
374
  `${mysqlPortsCounter}:${mysqlPortsCounter}`
269
375
  ],
270
376
  restart: 'always',
271
377
  environment: {
272
- MYSQL_HOST: serviceEnvVars.MYSQL_HOST,
273
- MYSQL_DATABASE: serviceEnvVars.MYSQL_DATABASE,
274
- MYSQL_USER: serviceEnvVars.MYSQL_USER,
275
- MYSQL_PASSWORD: serviceEnvVars.MYSQL_PASSWORD,
276
- MYSQL_RANDOM_ROOT_PASSWORD: serviceEnvVars.MYSQL_RANDOM_ROOT_PASSWORD,
378
+ MYSQL_HOST: host,
379
+ MYSQL_ROOT_PASSWORD: secrets.provision.MYSQL_ROOT_PASSWORD
277
380
  },
278
381
  volumes: [
279
- `${gnarHiddenDir}/data/${svc.name}-db-data:/var/lib/mysql`
280
- ]
281
- };
382
+ `${gnarHiddenDir}/data/${host}-data:/var/lib/mysql`
383
+ ],
384
+ attach: attachAll
385
+ };
282
386
 
283
- // increment mysql port for next service as required
284
387
  mysqlPortsCounter++;
285
388
  }
286
389
 
287
- // add a mongodb instance if required
288
- if (
289
- serviceEnvVars.MONGO_HOST &&
290
- serviceEnvVars.MONGO_ROOT_PASSWORD &&
291
- serviceEnvVars.MONGO_DATABASE &&
292
- serviceEnvVars.MONGO_USER &&
293
- serviceEnvVars.MONGO_PASSWORD
294
- ) {
295
- // add mongo init scripts to hidden dir
296
- fs.mkdir(path.join(gnarHiddenDir, 'mongo-init-scripts'), { recursive: true });
297
- const mongoInitScript = `
298
- db = db.getSiblingDB("invoice_db");
299
- db.createUser({
300
- user: "${serviceEnvVars.MONGO_USER}",
301
- pwd: "${serviceEnvVars.MONGO_PASSWORD}",
302
- roles: [{ role: "readWrite", db: "${serviceEnvVars.MONGO_DATABASE}" }]
303
- });
304
-
305
- print("Created user ${serviceEnvVars.MONGO_USER} with access to database ${serviceEnvVars.MONGO_DATABASE}");
306
- `;
307
- await fs.writeFile(path.join(gnarHiddenDir, 'mongo-init-scripts', `${svc.name}-init.js`), mongoInitScript);
390
+ services['provisioner'].depends_on = [...new Set(mysqlHostsRequired)];
391
+ }
392
+
393
+ // add mongo hosts if required
394
+ if (mongoHostsRequired.length > 0) {
395
+ for (const host of mongoHostsRequired) {
396
+ if (services[host]) {
397
+ continue;
398
+ }
308
399
 
309
- // create mongo service
310
- const mongoUrl = `mongodb://${serviceEnvVars.MONGO_USER}:${serviceEnvVars.MONGO_PASSWORD}@${serviceEnvVars.MONGO_HOST}:27017/${serviceEnvVars.MONGO_DATABASE}`;
311
-
312
- services[`${svc.name}-db`] = {
313
- container_name: `ge-${config.environment}-${config.namespace}-${svc.name}-mongo`,
400
+ services[host] = {
401
+ container_name: `ge-${config.environment}-${config.namespace}-${host}`,
314
402
  image: 'mongo:latest',
315
403
  ports: [
316
404
  `${mongoPortsCounter}:27017`
@@ -318,15 +406,13 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
318
406
  restart: 'always',
319
407
  environment: {
320
408
  MONGO_INITDB_ROOT_USERNAME: 'root',
321
- MONGO_INITDB_ROOT_PASSWORD: serviceEnvVars.MONGO_ROOT_PASSWORD,
322
- MONGO_INITDB_DATABASE: serviceEnvVars.MONGO_DATABASE,
323
- DB_USER: serviceEnvVars.MONGO_USER,
324
- DB_PASSWORD: serviceEnvVars.MONGO_PASSWORD,
409
+ MONGO_INITDB_ROOT_PASSWORD: secrets.provision.MONGO_ROOT_PASSWORD
325
410
  },
326
411
  volumes: [
327
- `${gnarHiddenDir}/data/${svc.name}-mongo-data:/data/db`,
412
+ `${gnarHiddenDir}/data/${host}-data:/data/db`,
328
413
  './mongo-init-scripts:/docker-entrypoint-initdb.d'
329
- ]
414
+ ],
415
+ attach: attachAll
330
416
  };
331
417
 
332
418
  // increment mongo port for next service as required
@@ -346,3 +432,4 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
346
432
  async function assertGnarEngineHiddenDir(gnarHiddenDir) {
347
433
  await fs.mkdir(gnarHiddenDir, { recursive: true });
348
434
  }
435
+
@@ -0,0 +1,27 @@
1
+ # Dockerfile for the ephemeral provisioning service
2
+ #
3
+ # This service is responsible for asserting the databases
4
+ # & database users, or other single runtime provisioning tasks.
5
+
6
+ FROM node:20-alpine
7
+
8
+ # Set the working directory
9
+ WORKDIR /usr/gnar_engine/app
10
+
11
+ # Define a global env var
12
+ ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
13
+
14
+ # Copy package.json and package-lock.json
15
+ COPY ./package*.json ./
16
+
17
+ # Copy source
18
+ COPY ./src ./src
19
+
20
+ # Install nodemon
21
+ RUN npm install -g nodemon
22
+
23
+ # Install app dependencies
24
+ RUN npm install
25
+
26
+ # Start the application
27
+ CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "gnar_engine_user",
3
+ "version": "1.0.0",
4
+ "description": "Gnar Engine - User Service",
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "node ./src/app.js",
8
+ "start:dev": "nodemon --watch ./src ./src/app.js",
9
+ "test": "jest --watchAll --verbose"
10
+ },
11
+ "author": "Gnar Software Ltd.",
12
+ "license": "ISC",
13
+ "dependencies": {
14
+ "@gnar-engine/core": "^1.0.1",
15
+ "dotenv": "^16.4.7",
16
+ "mongodb": "^5.9.2",
17
+ "mysql2": "^3.12.0"
18
+ }
19
+ }
@@ -0,0 +1,56 @@
1
+ import { mysqlService } from './services/mysql.js';
2
+ import { mongoService } from './services/mongodb.js';
3
+ import { secrets } from './services/secrets.js';
4
+
5
+ /**
6
+ * Initialise service
7
+ */
8
+ export const initService = async () => {
9
+
10
+ console.log('G n a r E n g i n e | Provisioner provisioning databases...');
11
+
12
+ let provisionerSecrets;
13
+
14
+ // get all secrets
15
+ try {
16
+ provisionerSecrets = JSON.parse(process.env.PROVISIONER_SECRETS);
17
+ } catch (error) {
18
+ console.error('Error parsing provisioner secrets', error);
19
+ return;
20
+ }
21
+
22
+ // collate databases to provision from secrets
23
+ const mysqlDatabases = secrets.collateMysqlDatabases(provisionerSecrets);
24
+ const mongoDatabases = secrets.collateMongoDatabases(provisionerSecrets);
25
+
26
+ // provision mysql databases
27
+ if (mysqlDatabases) {
28
+ for (const [key, value] of Object.entries(mysqlDatabases)) {
29
+ mysqlService.provisionDatabase({
30
+ host: value.host,
31
+ database: value.database,
32
+ user: value.user,
33
+ password: value.password,
34
+ rootPassword: provisionerSecrets.provision.MYSQL_ROOT_PASSWORD
35
+ });
36
+ }
37
+ } else {
38
+ console.log('No MySQL databases to provision.');
39
+ }
40
+
41
+ if (mongoDatabases) {
42
+ for (const [key, value] of Object.entries(mongoDatabases)) {
43
+ mongoService.provisionDatabase({
44
+ host: value.host,
45
+ database: value.database,
46
+ user: value.user,
47
+ password: value.password,
48
+ rootPassword: provisionerSecrets.provision.MONGO_ROOT_PASSWORD
49
+ })
50
+ }
51
+ } else {
52
+ console.log('No MongoDB databases to provision.');
53
+ }
54
+ }
55
+
56
+ initService();
@@ -0,0 +1,58 @@
1
+ import { MongoClient } from 'mongodb';
2
+
3
+ const retryInterval = 5000;
4
+ const maxRetries = 5;
5
+
6
+ let db;
7
+
8
+ export const mongoService = {
9
+
10
+ /**
11
+ * Provision database and users
12
+ *
13
+ * @param {Object} params - The parameters object
14
+ * @param {string} host - The database host
15
+ * @param {string} database - The database name
16
+ * @param {string} user - The database user
17
+ * @param {string} password - The database user password
18
+ * @param {string} rootPassword - The root user password
19
+ * @param {number} [port=27017] - The database port
20
+ */
21
+ provisionDatabase: async ({host, database, user, password, rootPassword, port = 27017}) => {
22
+
23
+ const connectionUrl = `mongodb://root:${rootPassword}@${host}:${port}/admin`;
24
+ let retries = 0;
25
+
26
+ while (retries < maxRetries) {
27
+ try {
28
+ const dbClient = await MongoClient.connect(connectionUrl);
29
+ db = dbClient.db(database);
30
+
31
+ const existingUsers = await db.command({ usersInfo: 1 });
32
+
33
+ if (!existingUsers.users.some(u => u.user === user)) {
34
+ await db.command({
35
+ createUser: user,
36
+ pwd: password,
37
+ roles: [{ role: "readWrite", db: database }]
38
+ });
39
+ }
40
+
41
+ console.log(`Successfully provisioned MongoDB database: ${database} and user: ${user}`);
42
+ await dbClient.close();
43
+ return;
44
+
45
+ } catch (error) {
46
+ console.error(`Failed provisioning Mongo database "${database}" for user "${user}" ": ${error.message}`);
47
+ retries++;
48
+
49
+ if (retries >= maxRetries) {
50
+ console.error(`Max retries reached. Could not provision database "${database}".`);
51
+ return;
52
+ }
53
+
54
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
55
+ }
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,51 @@
1
+ import mysql from 'mysql2/promise';
2
+
3
+ const retryInterval = 5000;
4
+ const maxRetries = 5;
5
+
6
+ export const mysqlService = {
7
+
8
+ /**
9
+ * Provision database and users
10
+ *
11
+ * @param {Object} params - The parameters object
12
+ * @param {string} host - The database host
13
+ * @param {string} database - The database name
14
+ * @param {string} user - The database user
15
+ * @param {string} password - The database user password
16
+ * @param {string} rootPassword - The root user password
17
+ */
18
+ provisionDatabase: async ({host, database, user, password, rootPassword}) => {
19
+ let retries = 0;
20
+
21
+ while (retries < maxRetries) {
22
+ try {
23
+ const conn = await mysql.createConnection({
24
+ host: host || 'db-mysql',
25
+ user: 'root',
26
+ password: rootPassword
27
+ });
28
+
29
+ await conn.query(`CREATE DATABASE IF NOT EXISTS ${mysql.escapeId(database)};`);
30
+ await conn.query(`CREATE USER IF NOT EXISTS ${mysql.escape(user)}@'%' IDENTIFIED BY ${mysql.escape(password)};`);
31
+ await conn.query(`GRANT ALL PRIVILEGES ON ${mysql.escapeId(database)}.* TO ${mysql.escape(user)}@'%';`);
32
+ await conn.query(`FLUSH PRIVILEGES;`);
33
+
34
+ console.log(`Successfully provisioned MySQL database: ${database} and user: ${user}`);
35
+ await conn.end();
36
+ return;
37
+
38
+ } catch (error) {
39
+ console.error(`Failed provisioning MySQL database "${database}" for user "${user}" ": ${error.message}`);
40
+ retries++;
41
+
42
+ if (retries >= maxRetries) {
43
+ console.error(`Max retries reached. Could not provision database "${database}".`);
44
+ return;
45
+ }
46
+
47
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,84 @@
1
+
2
+
3
+ export const secrets = {
4
+
5
+ /**
6
+ * Collate MySQL databases from provisioner secrets
7
+ *
8
+ * @param {Object} provisionerSecrets - The provisioner secrets object
9
+ * @returns {Object} - Collated MySQL databases
10
+ */
11
+ collateMysqlDatabases: (provisionerSecrets) => {
12
+
13
+ const mysqlDatabases = {};
14
+
15
+ for (const [serviceKey, service] of Object.entries(provisionerSecrets.services)) {
16
+ for (const key of Object.keys(service)) {
17
+ if (key.startsWith('MYSQL_')) {
18
+ mysqlDatabases[serviceKey] = true;
19
+ break;
20
+ }
21
+ }
22
+ }
23
+
24
+ for (const [key, value] of Object.entries(mysqlDatabases)) {
25
+ try {
26
+ mysqlDatabases[key] = {};
27
+ mysqlDatabases[key].host = provisionerSecrets.services[key].MYSQL_HOST;
28
+ mysqlDatabases[key].database = provisionerSecrets.services[key].MYSQL_DATABASE;
29
+ mysqlDatabases[key].user = provisionerSecrets.services[key].MYSQL_USER;
30
+ mysqlDatabases[key].password = provisionerSecrets.services[key].MYSQL_PASSWORD;
31
+ } catch (error) {
32
+ console.error(`Missing database credentials for ${key} service. Please include: MYSQL_DATABASE, MYSQL_USER, and MYSQL_PASSWORD.`);
33
+ return;
34
+ }
35
+ }
36
+
37
+ if (!provisionerSecrets.provision.MYSQL_ROOT_PASSWORD) {
38
+ console.error('Missing MYSQL_ROOT_PASSWORD in provisioner secrets. Cannot provision databases.');
39
+ return;
40
+ }
41
+
42
+ return mysqlDatabases;
43
+ },
44
+
45
+ /**
46
+ * Collate MongoDB databases from provisioner secrets
47
+ *
48
+ * @param {Object} provisionerSecrets - The provisioner secrets object
49
+ * @returns {Object} - Collated MongoDB databases
50
+ */
51
+ collateMongoDatabases: (provisionerSecrets) => {
52
+
53
+ const mongoDatabases = {};
54
+
55
+ for (const [serviceKey, service] of Object.entries(provisionerSecrets.services)) {
56
+ for (const key of Object.keys(service)) {
57
+ if (key.startsWith('MONGO_')) {
58
+ mongoDatabases[serviceKey] = true;
59
+ break;
60
+ }
61
+ }
62
+ }
63
+
64
+ for (const [key, value] of Object.entries(mongoDatabases)) {
65
+ try {
66
+ mongoDatabases[key] = {};
67
+ mongoDatabases[key].host = provisionerSecrets.services[key].MONGO_HOST;
68
+ mongoDatabases[key].database = provisionerSecrets.services[key].MONGO_DATABASE;
69
+ mongoDatabases[key].user = provisionerSecrets.services[key].MONGO_USER;
70
+ mongoDatabases[key].password = provisionerSecrets.services[key].MONGO_PASSWORD;
71
+ } catch (error) {
72
+ console.error(`Missing database credentials for ${key} service. Please include: MONGO_DATABASE, MONGO_USER, and MONGO_PASSWORD.`);
73
+ return;
74
+ }
75
+ }
76
+
77
+ if (!provisionerSecrets.provision.MONGO_ROOT_PASSWORD) {
78
+ console.error('Missing MONGO_ROOT_PASSWORD in provisioner secrets. Cannot provision databases.');
79
+ return;
80
+ }
81
+
82
+ return mongoDatabases;
83
+ }
84
+ }
@@ -119,7 +119,7 @@ export const registerScaffolderCommands = (program) => {
119
119
  console.log('Creating new service in... ' + activeProfile.profile.PROJECT_DIR);
120
120
 
121
121
  scaffolder.createNewFrontEndService({
122
- serviceName: options.service,
122
+ serviceName: service,
123
123
  projectDir: activeProfile.profile.PROJECT_DIR
124
124
  });
125
125
  } catch (error) {