@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.
- package/bootstrap/deploy.localdev.yml +30 -3
- package/bootstrap/secrets.localdev.yml +15 -4
- package/bootstrap/services/control/src/config.js +4 -0
- package/bootstrap/services/page/Dockerfile +23 -0
- package/bootstrap/services/page/package.json +16 -0
- package/bootstrap/services/page/src/app.js +50 -0
- package/bootstrap/services/page/src/commands/block.handler.js +94 -0
- package/bootstrap/services/page/src/commands/page.handler.js +167 -0
- package/bootstrap/services/page/src/config.js +62 -0
- package/bootstrap/services/page/src/controllers/block.http.controller.js +87 -0
- package/bootstrap/services/page/src/controllers/message.controller.js +51 -0
- package/bootstrap/services/page/src/controllers/page.http.controller.js +89 -0
- package/bootstrap/services/page/src/policies/block.policy.js +50 -0
- package/bootstrap/services/page/src/policies/page.policy.js +49 -0
- package/bootstrap/services/page/src/schema/page.schema.js +139 -0
- package/bootstrap/services/page/src/services/block.service.js +83 -0
- package/bootstrap/services/page/src/services/page.service.js +83 -0
- package/bootstrap/services/portal/Dockerfile +20 -0
- package/bootstrap/services/portal/README.md +73 -0
- package/bootstrap/services/portal/index.html +13 -0
- package/bootstrap/services/portal/nginx.conf +5 -0
- package/bootstrap/services/portal/package.json +33 -0
- package/bootstrap/services/portal/public/vite.svg +1 -0
- package/bootstrap/services/portal/react-router.config.js +7 -0
- package/bootstrap/services/portal/src/App.jsx +16 -0
- package/bootstrap/services/portal/src/assets/gnar-engine-white-logo.svg +9 -0
- package/bootstrap/services/portal/src/assets/icon-agent.svg +6 -0
- package/bootstrap/services/portal/src/assets/icon-cog.svg +4 -0
- package/bootstrap/services/portal/src/assets/icon-delete.svg +3 -0
- package/bootstrap/services/portal/src/assets/icon-home.svg +3 -0
- package/bootstrap/services/portal/src/assets/icon-padlock.svg +3 -0
- package/bootstrap/services/portal/src/assets/icon-page.svg +6 -0
- package/bootstrap/services/portal/src/assets/icon-reports.svg +3 -0
- package/bootstrap/services/portal/src/assets/icon-user.svg +3 -0
- package/bootstrap/services/portal/src/assets/icon-users.svg +3 -0
- package/bootstrap/services/portal/src/assets/login-green-rad-back-1.jpg +0 -0
- package/bootstrap/services/portal/src/assets/react.svg +1 -0
- package/bootstrap/services/portal/src/components/CrudList/CrudList.jsx +85 -0
- package/bootstrap/services/portal/src/components/CrudList/CrudList.less +59 -0
- package/bootstrap/services/portal/src/components/CustomSelect/CustomSelect.jsx +81 -0
- package/bootstrap/services/portal/src/components/CustomSelect/CustomSelect.less +0 -0
- package/bootstrap/services/portal/src/components/LoginForm/LoginForm.jsx +58 -0
- package/bootstrap/services/portal/src/components/PageBlockSwitch/PageBlockSwitch.jsx +129 -0
- package/bootstrap/services/portal/src/components/Sidebar/Sidebar.jsx +33 -0
- package/bootstrap/services/portal/src/components/Sidebar/Sidebar.less +37 -0
- package/bootstrap/services/portal/src/components/Topbar/Topbar.jsx +19 -0
- package/bootstrap/services/portal/src/components/Topbar/Topbar.less +22 -0
- package/bootstrap/services/portal/src/components/UserInfo/UserInfo.jsx +33 -0
- package/bootstrap/services/portal/src/components/UserInfo/UserInfo.less +21 -0
- package/bootstrap/services/portal/src/css/style.css +711 -0
- package/bootstrap/services/portal/src/data/pages.data.js +10 -0
- package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.jsx +65 -0
- package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.less +102 -0
- package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.jsx +115 -0
- package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.less +43 -0
- package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.jsx +124 -0
- package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.less +0 -0
- package/bootstrap/services/portal/src/elements/Repeater/Repeater.jsx +52 -0
- package/bootstrap/services/portal/src/elements/Repeater/Repeater.less +70 -0
- package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.jsx +18 -0
- package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.less +37 -0
- package/bootstrap/services/portal/src/elements/SaveButton/SaveButton.jsx +45 -0
- package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.jsx +63 -0
- package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.less +23 -0
- package/bootstrap/services/portal/src/elements/TextInput/TextInput.jsx +17 -0
- package/bootstrap/services/portal/src/layouts/Card/Card.jsx +15 -0
- package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.jsx +29 -0
- package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.less +49 -0
- package/bootstrap/services/portal/src/main.jsx +51 -0
- package/bootstrap/services/portal/src/pages/BlockSinglePage/BlockSinglePage.jsx +277 -0
- package/bootstrap/services/portal/src/pages/BlocksPage/BlocksPage.jsx +23 -0
- package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.jsx +11 -0
- package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.less +0 -0
- package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.jsx +21 -0
- package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.less +51 -0
- package/bootstrap/services/portal/src/pages/PageSinglePage/PageSinglePage.jsx +338 -0
- package/bootstrap/services/portal/src/pages/PagesPage/PagesPage.jsx +23 -0
- package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.jsx +9 -0
- package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.less +0 -0
- package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.jsx +25 -0
- package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.less +0 -0
- package/bootstrap/services/portal/src/services/block.js +28 -0
- package/bootstrap/services/portal/src/services/client.js +67 -0
- package/bootstrap/services/portal/src/services/gravatar.js +14 -0
- package/bootstrap/services/portal/src/services/page.js +28 -0
- package/bootstrap/services/portal/src/services/storage.js +62 -0
- package/bootstrap/services/portal/src/services/user.js +41 -0
- package/bootstrap/services/portal/src/slices/authSlice.js +101 -0
- package/bootstrap/services/portal/src/store/configureStore.js +10 -0
- package/bootstrap/services/portal/src/style/cards.less +57 -0
- package/bootstrap/services/portal/src/style/global.less +204 -0
- package/bootstrap/services/portal/src/style/icons.less +21 -0
- package/bootstrap/services/portal/src/style/inputs.less +52 -0
- package/bootstrap/services/portal/src/style/main.less +28 -0
- package/bootstrap/services/portal/src/utils/utils.js +9 -0
- package/bootstrap/services/portal/vite.config.js +12 -0
- package/bootstrap/services/user/src/app.js +6 -1
- package/bootstrap/services/user/src/commands/user.handler.js +0 -3
- package/bootstrap/services/user/src/config.js +5 -1
- package/bootstrap/services/user/src/policies/user.policy.js +3 -1
- package/bootstrap/services/user/src/tests/commands/user.test.js +22 -0
- package/install-from-clone.sh +30 -0
- package/package.json +1 -1
- package/src/cli.js +8 -0
- package/src/dev/commands.js +10 -2
- package/src/dev/dev.service.js +147 -60
- package/src/provisioner/Dockerfile +27 -0
- package/src/provisioner/package.json +19 -0
- package/src/provisioner/src/app.js +56 -0
- package/src/provisioner/src/services/mongodb.js +58 -0
- package/src/provisioner/src/services/mysql.js +51 -0
- package/src/provisioner/src/services/secrets.js +84 -0
- package/src/scaffolder/commands.js +1 -1
- package/src/scaffolder/scaffolder.handler.js +40 -15
- package/templates/service/src/app.js.hbs +12 -1
- package/templates/service/src/commands/{{serviceName}}.handler.js.hbs +1 -1
- package/templates/service/src/mongodb.config.js.hbs +5 -1
- package/templates/service/src/mysql.config.js.hbs +4 -0
- package/bootstrap/services/user/src/tests/user.test.js +0 -126
package/src/dev/dev.service.js
CHANGED
|
@@ -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}
|
|
146
|
+
* @param {string} serviceConfDir
|
|
147
|
+
* @param {string} projectDir
|
|
132
148
|
*/
|
|
133
|
-
export async function createDynamicNginxConf({ config,
|
|
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
|
|
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
|
-
//
|
|
346
|
+
// check if mysql service required
|
|
257
347
|
if (
|
|
258
348
|
serviceEnvVars.MYSQL_HOST &&
|
|
259
|
-
|
|
260
|
-
serviceEnvVars.MYSQL_USER &&
|
|
261
|
-
serviceEnvVars.MYSQL_PASSWORD &&
|
|
262
|
-
serviceEnvVars.MYSQL_RANDOM_ROOT_PASSWORD
|
|
349
|
+
secrets.provision?.MYSQL_ROOT_PASSWORD
|
|
263
350
|
) {
|
|
264
|
-
|
|
265
|
-
|
|
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:
|
|
273
|
-
|
|
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/${
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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:
|
|
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/${
|
|
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:
|
|
122
|
+
serviceName: service,
|
|
123
123
|
projectDir: activeProfile.profile.PROJECT_DIR
|
|
124
124
|
});
|
|
125
125
|
} catch (error) {
|