@gnar-engine/cli 1.0.4 → 1.0.6
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 +44 -3
- package/bootstrap/secrets.localdev.yml +20 -5
- package/bootstrap/services/control/src/config.js +4 -0
- package/bootstrap/services/notification/Dockerfile +2 -2
- package/bootstrap/services/notification/package.json +14 -32
- package/bootstrap/services/notification/src/app.js +50 -48
- package/bootstrap/services/notification/src/commands/notification.handler.js +96 -0
- package/bootstrap/services/notification/src/config.js +55 -12
- package/bootstrap/services/notification/src/controllers/http.controller.js +87 -0
- package/bootstrap/services/notification/src/controllers/message.controller.js +39 -70
- package/bootstrap/services/notification/src/db/migrations/01-init.js +50 -0
- package/bootstrap/services/notification/src/db/migrations/02-notification-service-init.js +23 -0
- package/bootstrap/services/notification/src/policies/notification.policy.js +49 -0
- package/bootstrap/services/notification/src/schema/notification.schema.js +17 -0
- package/bootstrap/services/notification/src/services/notification.service.js +32 -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/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 +70 -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 +35 -21
- 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 +31 -0
- package/install-from-clone.sh +30 -0
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/config.js +8 -0
- package/src/dev/commands.js +11 -3
- package/src/dev/dev.service.js +164 -64
- package/src/helpers/helpers.js +24 -0
- package/src/profiles/command.js +41 -0
- package/src/profiles/profiles.client.js +23 -0
- 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 +58 -2
- package/src/scaffolder/scaffolder.handler.js +164 -72
- package/templates/entity/src/commands/{{entityName}}.handler.js.hbs +94 -0
- package/templates/entity/src/controllers/{{entityName}}.http.controller.js.hbs +87 -0
- package/templates/entity/src/mysql.db/migrations/03-{{entityName}}-entity-init.js.hbs +23 -0
- package/templates/entity/src/policies/{{entityName}}.policy.js.hbs +49 -0
- package/templates/entity/src/schema/{{entityName}}.schema.js.hbs +17 -0
- package/templates/entity/src/services/mongodb.{{entityName}}.service.js.hbs +70 -0
- package/templates/entity/src/services/mysql.{{entityName}}.service.js.hbs +27 -0
- 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/notification/Dockerfile.prod +0 -37
- package/bootstrap/services/notification/README.md +0 -3
- package/bootstrap/services/notification/src/commands/command-bus.js +0 -20
- package/bootstrap/services/notification/src/commands/handlers/control.handler.js +0 -18
- package/bootstrap/services/notification/src/commands/handlers/notification.handler.js +0 -157
- package/bootstrap/services/notification/src/services/logger.service.js +0 -16
- package/bootstrap/services/notification/src/services/ses.service.js +0 -23
- package/bootstrap/services/notification/src/templates/admin-order-recieved.hbs +0 -136
- package/bootstrap/services/notification/src/templates/admin-subscription-failed.hbs +0 -87
- package/bootstrap/services/notification/src/templates/customer-order-recieved.hbs +0 -132
- package/bootstrap/services/notification/src/templates/customer-subscription-failed.hbs +0 -77
- package/bootstrap/services/user/src/tests/user.test.js +0 -126
- /package/bootstrap/services/{notification/src/tests/notification.test.js → portal/src/components/CustomSelect/CustomSelect.less} +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
TARGET_DIR="$HOME/.gnarengine"
|
|
5
|
+
CLI_DIR="$(pwd)"
|
|
6
|
+
|
|
7
|
+
echo "Installing Gnar Engine CLI from local source for user $USER..."
|
|
8
|
+
|
|
9
|
+
mkdir -p "$TARGET_DIR"
|
|
10
|
+
|
|
11
|
+
# Install dependencies
|
|
12
|
+
cd "$CLI_DIR"
|
|
13
|
+
npm install
|
|
14
|
+
|
|
15
|
+
# Link CLI to custom global folder
|
|
16
|
+
npm link
|
|
17
|
+
|
|
18
|
+
# Bin path for npm link
|
|
19
|
+
BIN_PATH="$TARGET_DIR/bin"
|
|
20
|
+
|
|
21
|
+
# Add to shell PATH if not already present
|
|
22
|
+
for SHELLRC in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
|
23
|
+
[ -f "$SHELLRC" ] || continue
|
|
24
|
+
if ! grep -q "$BIN_PATH" "$SHELLRC"; then
|
|
25
|
+
echo "export PATH=\"$BIN_PATH:\$PATH\"" >>"$SHELLRC"
|
|
26
|
+
echo "Added $BIN_PATH to PATH in $SHELLRC"
|
|
27
|
+
fi
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
echo "Gnar Engine CLI installed! Restart your terminal and run 'gnar --help'"
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { registerDevCommands } from './dev/commands.js';
|
|
|
6
6
|
import { registerControlCommands } from './control/commands.js';
|
|
7
7
|
import { registerScaffolderCommands } from './scaffolder/commands.js';
|
|
8
8
|
import { registerAgentCommands } from './agent/commands.js';
|
|
9
|
+
import path from 'path';
|
|
9
10
|
|
|
10
11
|
// Create a new program
|
|
11
12
|
const program = new Command();
|
|
@@ -28,3 +29,4 @@ G n a r E n g i n e - A powerful, AI ready microservice framework for modern a
|
|
|
28
29
|
|
|
29
30
|
// Parse CLI input
|
|
30
31
|
program.parse(process.argv);
|
|
32
|
+
|
package/src/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
|
|
2
3
|
export const gnarEngineCliConfig = {
|
|
3
4
|
|
|
@@ -7,3 +8,10 @@ export const gnarEngineCliConfig = {
|
|
|
7
8
|
corePath: '/usr/gnar_engine/app/node_modules/@gnar-engine/core'
|
|
8
9
|
|
|
9
10
|
}
|
|
11
|
+
|
|
12
|
+
export const directories = {
|
|
13
|
+
scaffolderServiceTemplates: path.join(import.meta.dirname, '../templates/service'),
|
|
14
|
+
scaffolderEntityTemplates: path.join(import.meta.dirname, '../templates/entity'),
|
|
15
|
+
bootstrap: path.join(import.meta.dirname, '../bootstrap'),
|
|
16
|
+
provisioner: path.join(import.meta.dirname, './provisioner')
|
|
17
|
+
};
|
package/src/dev/commands.js
CHANGED
|
@@ -10,8 +10,10 @@ export function registerDevCommands(program) {
|
|
|
10
10
|
devCmd
|
|
11
11
|
.command('up')
|
|
12
12
|
.description('🛠️ Up Development Containers')
|
|
13
|
-
.option('-b, --build', '
|
|
13
|
+
.option('-b, --build', 'Build without cache')
|
|
14
14
|
.option('-d, --detach', 'Run containers in background')
|
|
15
|
+
.option('-t --test', 'Run the tests with ephemeral databases')
|
|
16
|
+
.option('--test-service <service>', 'Run the tests for the specified service with ephemeral databases (e.g. --test-service user)')
|
|
15
17
|
.addOption(new Option('--core-dev').hideHelp())
|
|
16
18
|
.action(async (options) => {
|
|
17
19
|
let response = {};
|
|
@@ -27,13 +29,19 @@ export function registerDevCommands(program) {
|
|
|
27
29
|
// Change to the active profile directory
|
|
28
30
|
const projectDir = activeProfile.PROJECT_DIR;
|
|
29
31
|
|
|
32
|
+
if (options.testService) {
|
|
33
|
+
options.test = true;
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
try {
|
|
31
37
|
up({
|
|
32
38
|
projectDir: projectDir,
|
|
33
39
|
build: options.build || false,
|
|
34
40
|
detach: options.detach || false,
|
|
35
|
-
coreDev: options.coreDev || false
|
|
36
|
-
|
|
41
|
+
coreDev: options.coreDev || false,
|
|
42
|
+
test: options.test || false,
|
|
43
|
+
testService: options.testService || ''
|
|
44
|
+
});
|
|
37
45
|
} catch (err) {
|
|
38
46
|
console.error("❌ Error running containers:", err.message);
|
|
39
47
|
process.exit(1);
|
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 "../config.js";
|
|
8
9
|
|
|
9
10
|
const docker = new Docker();
|
|
10
11
|
|
|
@@ -16,10 +17,13 @@ const docker = new Docker();
|
|
|
16
17
|
* @param {object} options
|
|
17
18
|
* @param {string} options.projectDir - The project directory
|
|
18
19
|
* @param {boolean} [options.build=false] - Whether to re-build images
|
|
19
|
-
* @param {boolean} [options.
|
|
20
|
+
* @param {boolean} [options.detach=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,
|
|
26
|
+
export async function up({ projectDir, build = false, detach = 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
|
|
|
@@ -63,10 +74,14 @@ export async function up({ projectDir, build = false, detached = false, coreDev
|
|
|
63
74
|
args.push("--build");
|
|
64
75
|
}
|
|
65
76
|
|
|
66
|
-
if (
|
|
77
|
+
if (detach) {
|
|
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,
|
|
@@ -96,11 +111,11 @@ export async function up({ projectDir, build = false, detached = false, coreDev
|
|
|
96
111
|
*/
|
|
97
112
|
export async function down({ projectDir, allContainers = false }) {
|
|
98
113
|
// list all containers
|
|
99
|
-
|
|
114
|
+
let containers = await docker.listContainers();
|
|
100
115
|
|
|
101
116
|
// filter containers by image name
|
|
102
117
|
if (!allContainers) {
|
|
103
|
-
|
|
118
|
+
containers = containers.filter(c => c.Image.includes("ge-localdev"));
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
if (containers.length === 0) {
|
|
@@ -122,15 +137,28 @@ export async function down({ projectDir, allContainers = false }) {
|
|
|
122
137
|
});
|
|
123
138
|
})
|
|
124
139
|
);
|
|
140
|
+
|
|
141
|
+
// remove each container
|
|
142
|
+
await Promise.all(
|
|
143
|
+
containers.map(c => {
|
|
144
|
+
const container = docker.getContainer(c.Id);
|
|
145
|
+
return container.remove({ force: true }).catch(err => {
|
|
146
|
+
console.error(`Failed to remove ${c.Names[0]}: ${err.message}`);
|
|
147
|
+
});
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
console.log('Containers stopped and removed.');
|
|
125
152
|
}
|
|
126
153
|
|
|
127
154
|
/**
|
|
128
155
|
* Create dynamic nginx.conf file for running application locally
|
|
129
156
|
*
|
|
130
157
|
* @param {object} config
|
|
131
|
-
* @param {string}
|
|
158
|
+
* @param {string} serviceConfDir
|
|
159
|
+
* @param {string} projectDir
|
|
132
160
|
*/
|
|
133
|
-
export async function createDynamicNginxConf({ config,
|
|
161
|
+
export async function createDynamicNginxConf({ config, serviceConfDir, projectDir }) {
|
|
134
162
|
// Start with the static parts of nginx.conf
|
|
135
163
|
let nginxConf = `
|
|
136
164
|
events { worker_connections 1024; }
|
|
@@ -139,10 +167,29 @@ export async function createDynamicNginxConf({ config, outputPath }) {
|
|
|
139
167
|
server {
|
|
140
168
|
listen 80;
|
|
141
169
|
server_name ${config.namespace};
|
|
170
|
+
include /etc/nginx/service_conf/*.conf;
|
|
171
|
+
|
|
142
172
|
`;
|
|
143
173
|
|
|
144
174
|
// Loop over each service
|
|
145
175
|
for (const service of config.services || []) {
|
|
176
|
+
// Check if override is present and add conf to service_conf dir
|
|
177
|
+
const serviceDir = path.join(projectDir, 'services', service.name);
|
|
178
|
+
|
|
179
|
+
if (await fs.stat(serviceDir).then(() => true).catch(() => false)) {
|
|
180
|
+
const overridePath = path.join(serviceDir, 'nginx.conf');
|
|
181
|
+
if (await fs.stat(overridePath).then(() => true).catch(() => false)) {
|
|
182
|
+
const overrideConf = await fs.readFile(overridePath, 'utf8');
|
|
183
|
+
|
|
184
|
+
// write to service_conf directory
|
|
185
|
+
const serviceConfPath = path.join(serviceConfDir, `${service.name}.conf`);
|
|
186
|
+
await fs.writeFile(serviceConfPath, overrideConf);
|
|
187
|
+
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Otherwise create generic conf block
|
|
146
193
|
const serviceName = service.name;
|
|
147
194
|
const paths = service.listener_rules?.paths || [];
|
|
148
195
|
const containerPort = service.ports && service.ports.length > 0 ? service.ports[0].split(':')[1] : '3000';
|
|
@@ -179,12 +226,57 @@ export async function createDynamicNginxConf({ config, outputPath }) {
|
|
|
179
226
|
* @param {string} gnarHiddenDir
|
|
180
227
|
* @param {string} projectDir
|
|
181
228
|
* @param {boolean} coreDev - Whether to volume mount the core source code
|
|
229
|
+
* @param {boolean} test - Whether to run tests with ephemeral databases
|
|
230
|
+
* @param {string} testService - The service to run tests for (only applicable if test=true)
|
|
231
|
+
* @param {boolean} attachAll - Whether to attach to all containers' stdio (otherwise databases and message queue are detached)
|
|
182
232
|
*/
|
|
183
|
-
async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, projectDir, coreDev = false }) {
|
|
233
|
+
async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, projectDir, coreDev = false, test = false, testService, attachAll = false }) {
|
|
184
234
|
let mysqlPortsCounter = 3306;
|
|
185
235
|
let mongoPortsCounter = 27017;
|
|
236
|
+
let mysqlHostsRequired = [];
|
|
237
|
+
let mongoHostsRequired = [];
|
|
186
238
|
const services = {};
|
|
187
|
-
|
|
239
|
+
|
|
240
|
+
// test mode env var adjustments
|
|
241
|
+
for (const svc of config.services) {
|
|
242
|
+
if (test) {
|
|
243
|
+
if (secrets.services?.[svc.name]?.MYSQL_HOST) {
|
|
244
|
+
secrets.services[svc.name].MYSQL_HOST = 'db-mysql-test';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (secrets.services?.[svc.name]) {
|
|
248
|
+
secrets.services[svc.name].NODE_ENV = 'test';
|
|
249
|
+
|
|
250
|
+
console.log(testService, svc.name);
|
|
251
|
+
if (testService && svc.name === testService) {
|
|
252
|
+
secrets.services[svc.name].RUN_TESTS = 'true';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// provision the provisioner service
|
|
259
|
+
services['provisioner'] = {
|
|
260
|
+
container_name: `ge-${config.environment}-${config.namespace}-provisioner`,
|
|
261
|
+
image: `ge-${config.environment}-${config.namespace}-provisioner`,
|
|
262
|
+
build: {
|
|
263
|
+
context: directories.provisioner,
|
|
264
|
+
dockerfile: `./Dockerfile`
|
|
265
|
+
},
|
|
266
|
+
environment: {
|
|
267
|
+
PROVISIONER_SECRETS: JSON.stringify(secrets)
|
|
268
|
+
},
|
|
269
|
+
volumes: [
|
|
270
|
+
`${directories.provisioner}/src:/usr/gnar_engine/app/src`
|
|
271
|
+
],
|
|
272
|
+
restart: 'no',
|
|
273
|
+
attach: attachAll
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (coreDev) {
|
|
277
|
+
services['provisioner'].volumes.push(`../../../core/:${gnarEngineCliConfig.corePath}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
188
280
|
// nginx
|
|
189
281
|
services['nginx'] = {
|
|
190
282
|
image: 'nginx:latest',
|
|
@@ -194,9 +286,11 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
|
|
|
194
286
|
"443:443"
|
|
195
287
|
],
|
|
196
288
|
volumes: [
|
|
197
|
-
`${gnarHiddenDir}/nginx/nginx.conf:/etc/nginx/nginx.conf
|
|
289
|
+
`${gnarHiddenDir}/nginx/nginx.conf:/etc/nginx/nginx.conf`,
|
|
290
|
+
`${gnarHiddenDir}/nginx/service_conf:/etc/nginx/service_conf`
|
|
198
291
|
],
|
|
199
|
-
restart: 'always'
|
|
292
|
+
restart: 'always',
|
|
293
|
+
attach: attachAll
|
|
200
294
|
}
|
|
201
295
|
|
|
202
296
|
// rabbit
|
|
@@ -211,7 +305,8 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
|
|
|
211
305
|
RABBITMQ_DEFAULT_USER: secrets.global.RABBITMQ_USER || '',
|
|
212
306
|
RABBITMQ_DEFAULT_PASS: secrets.global.RABBITMQ_PASS || ''
|
|
213
307
|
},
|
|
214
|
-
restart: 'always'
|
|
308
|
+
restart: 'always',
|
|
309
|
+
attach: attachAll
|
|
215
310
|
}
|
|
216
311
|
|
|
217
312
|
// services
|
|
@@ -220,7 +315,7 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
|
|
|
220
315
|
// env variables
|
|
221
316
|
const serviceEnvVars = secrets.services?.[svc.name] || {};
|
|
222
317
|
const localisedServiceEnvVars = {};
|
|
223
|
-
|
|
318
|
+
|
|
224
319
|
for (const [key, value] of Object.entries(serviceEnvVars)) {
|
|
225
320
|
localisedServiceEnvVars[svc.name.toUpperCase() + '_' + key] = value;
|
|
226
321
|
}
|
|
@@ -230,6 +325,14 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
|
|
|
230
325
|
...(localisedServiceEnvVars || {})
|
|
231
326
|
};
|
|
232
327
|
|
|
328
|
+
// test mode adjustments
|
|
329
|
+
if (test) {
|
|
330
|
+
if (svc.depends_on && svc.depends_on.includes('db-mysql')) {
|
|
331
|
+
svc.depends_on = svc.depends_on.filter(d => d !== 'db-mysql');
|
|
332
|
+
svc.depends_on.push('db-mysql-test');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
233
336
|
// service block
|
|
234
337
|
services[`${svc.name}-service`] = {
|
|
235
338
|
container_name: `ge-${config.environment}-${config.namespace}-${svc.name}`,
|
|
@@ -248,69 +351,67 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
|
|
|
248
351
|
restart: 'always'
|
|
249
352
|
};
|
|
250
353
|
|
|
251
|
-
// add the core source code mount if in
|
|
354
|
+
// add the core source code mount if in coreDev mode
|
|
252
355
|
if (coreDev) {
|
|
253
356
|
services[`${svc.name}-service`].volumes.push(`../../../core/:${gnarEngineCliConfig.corePath}`);
|
|
254
357
|
}
|
|
255
358
|
|
|
256
|
-
//
|
|
359
|
+
// check if mysql service required
|
|
257
360
|
if (
|
|
258
361
|
serviceEnvVars.MYSQL_HOST &&
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
serviceEnvVars.
|
|
262
|
-
|
|
362
|
+
secrets.provision?.MYSQL_ROOT_PASSWORD
|
|
363
|
+
) {
|
|
364
|
+
mysqlHostsRequired.push(serviceEnvVars.MYSQL_HOST);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// add a mongodb instance if required
|
|
368
|
+
if (
|
|
369
|
+
serviceEnvVars.MONGO_HOST &&
|
|
370
|
+
secrets.provision?.MONGO_ROOT_PASSWORD
|
|
263
371
|
) {
|
|
264
|
-
|
|
265
|
-
|
|
372
|
+
mongoHostsRequired.push(serviceEnvVars.MONGO_HOST);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// add mysql if required
|
|
377
|
+
if (mysqlHostsRequired.length > 0) {
|
|
378
|
+
for (const host of mysqlHostsRequired) {
|
|
379
|
+
if (services[host]) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
services[host] = {
|
|
384
|
+
container_name: `ge-${config.environment}-${config.namespace}-${host}`,
|
|
266
385
|
image: 'mysql',
|
|
267
386
|
ports: [
|
|
268
387
|
`${mysqlPortsCounter}:${mysqlPortsCounter}`
|
|
269
388
|
],
|
|
270
389
|
restart: 'always',
|
|
271
390
|
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,
|
|
391
|
+
MYSQL_HOST: host,
|
|
392
|
+
MYSQL_ROOT_PASSWORD: secrets.provision.MYSQL_ROOT_PASSWORD
|
|
277
393
|
},
|
|
278
394
|
volumes: [
|
|
279
|
-
`${gnarHiddenDir}/data/${
|
|
280
|
-
]
|
|
281
|
-
|
|
395
|
+
`${gnarHiddenDir}/data/${host}-data:/var/lib/mysql`
|
|
396
|
+
],
|
|
397
|
+
attach: attachAll
|
|
398
|
+
};
|
|
282
399
|
|
|
283
|
-
// increment mysql port for next service as required
|
|
284
400
|
mysqlPortsCounter++;
|
|
285
401
|
}
|
|
286
402
|
|
|
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);
|
|
403
|
+
services['provisioner'].depends_on = [...new Set(mysqlHostsRequired)];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// add mongo hosts if required
|
|
407
|
+
if (mongoHostsRequired.length > 0) {
|
|
408
|
+
for (const host of mongoHostsRequired) {
|
|
409
|
+
if (services[host]) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
308
412
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
services[`${svc.name}-db`] = {
|
|
313
|
-
container_name: `ge-${config.environment}-${config.namespace}-${svc.name}-mongo`,
|
|
413
|
+
services[host] = {
|
|
414
|
+
container_name: `ge-${config.environment}-${config.namespace}-${host}`,
|
|
314
415
|
image: 'mongo:latest',
|
|
315
416
|
ports: [
|
|
316
417
|
`${mongoPortsCounter}:27017`
|
|
@@ -318,15 +419,13 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
|
|
|
318
419
|
restart: 'always',
|
|
319
420
|
environment: {
|
|
320
421
|
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,
|
|
422
|
+
MONGO_INITDB_ROOT_PASSWORD: secrets.provision.MONGO_ROOT_PASSWORD
|
|
325
423
|
},
|
|
326
424
|
volumes: [
|
|
327
|
-
`${gnarHiddenDir}/data/${
|
|
425
|
+
`${gnarHiddenDir}/data/${host}-data:/data/db`,
|
|
328
426
|
'./mongo-init-scripts:/docker-entrypoint-initdb.d'
|
|
329
|
-
]
|
|
427
|
+
],
|
|
428
|
+
attach: attachAll
|
|
330
429
|
};
|
|
331
430
|
|
|
332
431
|
// increment mongo port for next service as required
|
|
@@ -346,3 +445,4 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
|
|
|
346
445
|
async function assertGnarEngineHiddenDir(gnarHiddenDir) {
|
|
347
446
|
await fs.mkdir(gnarHiddenDir, { recursive: true });
|
|
348
447
|
}
|
|
448
|
+
|
package/src/helpers/helpers.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
1
4
|
|
|
2
5
|
/**
|
|
3
6
|
* CLI helper functions
|
|
@@ -59,5 +62,26 @@ export const helpers = {
|
|
|
59
62
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
60
63
|
}
|
|
61
64
|
return result;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
getDbTypeFromSecrets: async (serviceName, projectDir) => {
|
|
68
|
+
let dbType;
|
|
69
|
+
const secretsPath = path.join(projectDir, "secrets.localdev.yml");
|
|
70
|
+
const parsedSecrets = yaml.load(await fs.readFile(secretsPath, "utf8"));
|
|
71
|
+
const serviceSecrets = parsedSecrets.services[serviceName.toLowerCase()];
|
|
72
|
+
|
|
73
|
+
Object.keys(serviceSecrets).forEach(key => {
|
|
74
|
+
if (key.toLowerCase().includes('host')) {
|
|
75
|
+
const host = serviceSecrets[key].toLowerCase();
|
|
76
|
+
|
|
77
|
+
if (host.includes('mongo')) {
|
|
78
|
+
dbType = 'mongodb';
|
|
79
|
+
} else if (host.includes('mysql')) {
|
|
80
|
+
dbType = 'mysql';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return dbType;
|
|
62
86
|
}
|
|
63
87
|
}
|
package/src/profiles/command.js
CHANGED
|
@@ -166,5 +166,46 @@ export function registerProfileCommand(program) {
|
|
|
166
166
|
});
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
+
// delete profile
|
|
170
|
+
profile
|
|
171
|
+
.command('delete <profileName>')
|
|
172
|
+
.description('Delete an existing profile')
|
|
173
|
+
.action(async (profileName) => {
|
|
174
|
+
const config = profiles.getAllProfiles();
|
|
175
|
+
const activeProfileName = config.activeProfile;
|
|
176
|
+
|
|
177
|
+
if (activeProfileName === profileName) {
|
|
178
|
+
console.error(`Cannot delete active profile "${profileName}". Please set another profile as active first.`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!config.profiles[profileName]) {
|
|
183
|
+
console.error(`Profile "${profileName}" not found.`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// confirm deletion with user
|
|
189
|
+
const confirmation = await inquirer.prompt([
|
|
190
|
+
{
|
|
191
|
+
type: 'confirm',
|
|
192
|
+
name: 'confirmDelete',
|
|
193
|
+
message: `Are you sure you want to delete profile "${profileName}"?`,
|
|
194
|
+
default: false,
|
|
195
|
+
},
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
if (!confirmation.confirmDelete) {
|
|
199
|
+
console.log('❌ Deletion cancelled.');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
profiles.deleteProfile({ profileName });
|
|
204
|
+
console.log(`✅ Profile "${profileName}" deleted successfully.`);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error(error.message);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
169
210
|
program.addCommand(profile);
|
|
170
211
|
}
|
|
@@ -90,6 +90,29 @@ export const profiles = {
|
|
|
90
90
|
this.saveProfiles(allProfiles);
|
|
91
91
|
},
|
|
92
92
|
|
|
93
|
+
deleteProfile: function ({ profileName }) {
|
|
94
|
+
if (!profileName) {
|
|
95
|
+
throw new Error('Invalid profile name');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const allProfiles = this.getAllProfiles().profiles || {};
|
|
99
|
+
|
|
100
|
+
if (!allProfiles[profileName]) {
|
|
101
|
+
throw new Error(`Profile "${profileName}" not found`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const activeProfileName = this.getActiveProfile()?.name;
|
|
105
|
+
|
|
106
|
+
if (activeProfileName === profileName) {
|
|
107
|
+
throw new Error(`Cannot delete active profile "${profileName}". Please set another profile as active first.`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Prompt user to confirm deletion in the console
|
|
111
|
+
delete allProfiles[profileName];
|
|
112
|
+
|
|
113
|
+
this.saveProfiles(allProfiles);
|
|
114
|
+
},
|
|
115
|
+
|
|
93
116
|
saveProfiles: function (profilesObj) {
|
|
94
117
|
const dir = path.dirname(this.configPath);
|
|
95
118
|
if (!fs.existsSync(dir)) {
|
|
@@ -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
|
+
}
|