underpost 2.90.4 → 2.95.1

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 (41) hide show
  1. package/.github/workflows/pwa-microservices-template-page.cd.yml +5 -4
  2. package/.github/workflows/release.cd.yml +7 -7
  3. package/README.md +7 -8
  4. package/bin/build.js +6 -1
  5. package/bin/deploy.js +2 -196
  6. package/cli.md +154 -80
  7. package/manifests/deployment/dd-default-development/deployment.yaml +4 -4
  8. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  9. package/package.json +1 -1
  10. package/scripts/disk-clean.sh +216 -0
  11. package/scripts/rocky-setup.sh +1 -0
  12. package/scripts/ssh-cluster-info.sh +4 -3
  13. package/src/cli/cluster.js +1 -1
  14. package/src/cli/db.js +1143 -201
  15. package/src/cli/deploy.js +93 -24
  16. package/src/cli/env.js +2 -2
  17. package/src/cli/image.js +198 -133
  18. package/src/cli/index.js +111 -44
  19. package/src/cli/lxd.js +73 -74
  20. package/src/cli/monitor.js +20 -9
  21. package/src/cli/repository.js +212 -5
  22. package/src/cli/run.js +207 -74
  23. package/src/cli/ssh.js +642 -14
  24. package/src/client/components/core/CommonJs.js +0 -1
  25. package/src/db/mongo/MongooseDB.js +5 -1
  26. package/src/index.js +1 -1
  27. package/src/monitor.js +11 -1
  28. package/src/server/backup.js +1 -1
  29. package/src/server/conf.js +1 -1
  30. package/src/server/dns.js +242 -1
  31. package/src/server/process.js +6 -1
  32. package/src/server/start.js +2 -0
  33. package/scripts/snap-clean.sh +0 -26
  34. package/src/client/public/default/plantuml/client-conf.svg +0 -1
  35. package/src/client/public/default/plantuml/client-schema.svg +0 -1
  36. package/src/client/public/default/plantuml/cron-conf.svg +0 -1
  37. package/src/client/public/default/plantuml/cron-schema.svg +0 -1
  38. package/src/client/public/default/plantuml/server-conf.svg +0 -1
  39. package/src/client/public/default/plantuml/server-schema.svg +0 -1
  40. package/src/client/public/default/plantuml/ssr-conf.svg +0 -1
  41. package/src/client/public/default/plantuml/ssr-schema.svg +0 -1
@@ -31,7 +31,11 @@ class MongooseDBService {
31
31
  logger.info('MongooseDB connect', { host, name, uri });
32
32
  return await mongoose
33
33
  .createConnection(uri, {
34
- // Options like useNewUrlParser and useUnifiedTopology are often set here.
34
+ serverSelectionTimeoutMS: 5000,
35
+ // readPreference: 'primary',
36
+ // directConnection: true,
37
+ // useNewUrlParser: true,
38
+ // useUnifiedTopology: true,
35
39
  })
36
40
  .asPromise();
37
41
  }
package/src/index.js CHANGED
@@ -36,7 +36,7 @@ class Underpost {
36
36
  * @type {String}
37
37
  * @memberof Underpost
38
38
  */
39
- static version = 'v2.90.4';
39
+ static version = 'v2.95.1';
40
40
  /**
41
41
  * Repository cli API
42
42
  * @static
package/src/monitor.js CHANGED
@@ -19,6 +19,16 @@ const logger = loggerFactory(import.meta);
19
19
 
20
20
  await logger.setUpInfo();
21
21
 
22
- UnderpostMonitor.API.callback(process.argv[2], process.argv[3], { type: 'blue-green', sync: true });
22
+ const deployId = process.argv[2];
23
+ const env = process.argv[3] || 'production';
24
+ const replicas = process.argv[4] || '1';
25
+ const namespace = process.argv[5] || 'default';
26
+
27
+ UnderpostMonitor.API.callback(deployId, env, {
28
+ type: 'blue-green',
29
+ sync: true,
30
+ replicas,
31
+ namespace,
32
+ });
23
33
 
24
34
  ProcessController.init(logger);
@@ -86,7 +86,7 @@ class BackUp {
86
86
  ` && underpost cmt . backup cron-job '${new Date().toLocaleDateString()}'` +
87
87
  ` && underpost push . ${process.env.GITHUB_USERNAME}/cron-backups`,
88
88
  {
89
- disableLog: true,
89
+ silent: true,
90
90
  },
91
91
  );
92
92
  }
@@ -124,7 +124,7 @@ const Config = {
124
124
  fs.readFileSync(`./.github/workflows/engine-test.ci.yml`, 'utf8').replaceAll('test', deployId.split('dd-')[1]),
125
125
  'utf8',
126
126
  );
127
- shellExec(`node bin/deploy update-default-conf ${deployId}`);
127
+ shellExec(`node bin new --default-conf --deploy-id ${deployId}`);
128
128
 
129
129
  if (!fs.existsSync(`./engine-private/deploy/dd.router`))
130
130
  fs.writeFileSync(`./engine-private/deploy/dd.router`, '', 'utf8');
package/src/server/dns.js CHANGED
@@ -12,7 +12,7 @@ import { loggerFactory } from './logger.js';
12
12
  import UnderpostRootEnv from '../cli/env.js';
13
13
  import dns from 'node:dns';
14
14
  import os from 'node:os';
15
- import { shellExec } from './process.js';
15
+ import { shellExec, pbcopy } from './process.js';
16
16
 
17
17
  dotenv.config();
18
18
 
@@ -100,6 +100,160 @@ class Dns {
100
100
  return ipv4.address;
101
101
  }
102
102
 
103
+ /**
104
+ * Setup nftables tables and chains if they don't exist.
105
+ * @static
106
+ * @memberof DnsManager
107
+ */
108
+ static setupNftables() {
109
+ shellExec(`sudo nft add table inet filter 2>/dev/null || true`, { silent: true });
110
+ shellExec(
111
+ `sudo nft add chain inet filter input '{ type filter hook input priority 0; policy accept; }' 2>/dev/null || true`,
112
+ { silent: true },
113
+ );
114
+ shellExec(
115
+ `sudo nft add chain inet filter output '{ type filter hook output priority 0; policy accept; }' 2>/dev/null || true`,
116
+ { silent: true },
117
+ );
118
+ shellExec(
119
+ `sudo nft add chain inet filter forward '{ type filter hook forward priority 0; policy accept; }' 2>/dev/null || true`,
120
+ { silent: true },
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Bans an IP address from ingress traffic.
126
+ * @static
127
+ * @memberof DnsManager
128
+ * @param {string} ip - The IP address to ban.
129
+ */
130
+ static banIngress(ip) {
131
+ Dns.setupNftables();
132
+ if (!validator.isIP(ip)) {
133
+ logger.error(`Invalid IP address: ${ip}`);
134
+ return;
135
+ }
136
+ shellExec(`sudo nft add rule inet filter input ip saddr ${ip} counter drop`, { silent: true });
137
+ logger.info(`Banned ingress for IP: ${ip}`);
138
+ }
139
+
140
+ /**
141
+ * Bans an IP address from egress traffic.
142
+ * @static
143
+ * @memberof DnsManager
144
+ * @param {string} ip - The IP address to ban.
145
+ */
146
+ static banEgress(ip) {
147
+ Dns.setupNftables();
148
+ if (!validator.isIP(ip)) {
149
+ logger.error(`Invalid IP address: ${ip}`);
150
+ return;
151
+ }
152
+ shellExec(`sudo nft add rule inet filter output ip daddr ${ip} counter drop`, { silent: true });
153
+ shellExec(`sudo nft add rule inet filter forward ip daddr ${ip} counter drop`, { silent: true });
154
+ logger.info(`Banned egress for IP: ${ip}`);
155
+ }
156
+
157
+ /**
158
+ * Helper to get nftables rule handles for a specific IP and chain.
159
+ * @static
160
+ * @memberof DnsManager
161
+ * @param {string} chain - The chain name (input, output, forward).
162
+ * @param {string} ip - The IP address.
163
+ * @param {string} type - The type (saddr or daddr).
164
+ * @returns {string[]} Array of handles.
165
+ */
166
+ static getNftHandles(chain, ip, type) {
167
+ const output = shellExec(`sudo nft -a list chain inet filter ${chain}`, { stdout: true, silent: true });
168
+ const lines = output.split('\n');
169
+ const handles = [];
170
+ // Regex to match IP and handle. Note: output format depends on nft version but usually contains "handle <id>" at end.
171
+ // Example: ip saddr 1.2.3.4 counter packets 0 bytes 0 drop # handle 5
172
+ const regex = new RegExp(`ip ${type} ${ip} .* handle (\\d+)`);
173
+ for (const line of lines) {
174
+ const match = line.match(regex);
175
+ if (match) {
176
+ handles.push(match[1]);
177
+ }
178
+ }
179
+ return handles;
180
+ }
181
+
182
+ /**
183
+ * Unbans an IP address from ingress traffic.
184
+ * @static
185
+ * @memberof DnsManager
186
+ * @param {string} ip - The IP address to unban.
187
+ */
188
+ static unbanIngress(ip) {
189
+ const handles = Dns.getNftHandles('input', ip, 'saddr');
190
+ for (const handle of handles) {
191
+ shellExec(`sudo nft delete rule inet filter input handle ${handle}`, { silent: true });
192
+ }
193
+ logger.info(`Unbanned ingress for IP: ${ip}`);
194
+ }
195
+
196
+ /**
197
+ * Unbans an IP address from egress traffic.
198
+ * @static
199
+ * @memberof DnsManager
200
+ * @param {string} ip - The IP address to unban.
201
+ */
202
+ static unbanEgress(ip) {
203
+ const outputHandles = Dns.getNftHandles('output', ip, 'daddr');
204
+ for (const handle of outputHandles) {
205
+ shellExec(`sudo nft delete rule inet filter output handle ${handle}`, { silent: true });
206
+ }
207
+ const forwardHandles = Dns.getNftHandles('forward', ip, 'daddr');
208
+ for (const handle of forwardHandles) {
209
+ shellExec(`sudo nft delete rule inet filter forward handle ${handle}`, { silent: true });
210
+ }
211
+ logger.info(`Unbanned egress for IP: ${ip}`);
212
+ }
213
+
214
+ /**
215
+ * Lists all banned ingress IPs.
216
+ * @static
217
+ * @memberof DnsManager
218
+ */
219
+ static listBannedIngress() {
220
+ const output = shellExec(`sudo nft list chain inet filter input`, { stdout: true, silent: true });
221
+ console.log(output);
222
+ }
223
+
224
+ /**
225
+ * Lists all banned egress IPs.
226
+ * @static
227
+ * @memberof DnsManager
228
+ */
229
+ static listBannedEgress() {
230
+ console.log('--- Output Chain ---');
231
+ console.log(shellExec(`sudo nft list chain inet filter output`, { stdout: true, silent: true }));
232
+ console.log('--- Forward Chain ---');
233
+ console.log(shellExec(`sudo nft list chain inet filter forward`, { stdout: true, silent: true }));
234
+ }
235
+
236
+ /**
237
+ * Clears all banned ingress IPs.
238
+ * @static
239
+ * @memberof DnsManager
240
+ */
241
+ static clearBannedIngress() {
242
+ shellExec(`sudo nft flush chain inet filter input`, { silent: true });
243
+ logger.info('Cleared all ingress bans.');
244
+ }
245
+
246
+ /**
247
+ * Clears all banned egress IPs.
248
+ * @static
249
+ * @memberof DnsManager
250
+ */
251
+ static clearBannedEgress() {
252
+ shellExec(`sudo nft flush chain inet filter output`, { silent: true });
253
+ shellExec(`sudo nft flush chain inet filter forward`, { silent: true });
254
+ logger.info('Cleared all egress bans.');
255
+ }
256
+
103
257
  /**
104
258
  * Performs the dynamic DNS update logic.
105
259
  * It checks if the public IP has changed and, if so, updates the configured DNS records.
@@ -232,6 +386,93 @@ class Dns {
232
386
  // Add other DNS provider update functions here
233
387
  },
234
388
  };
389
+
390
+ /**
391
+ * Dispatcher for IP ban/unban/list/clear operations based on CLI options.
392
+ * @static
393
+ * @memberof DnsManager
394
+ * @param {string} [ips=''] Comma-separated string of IPs to process.
395
+ * @param {object} options - Options indicating which action to perform.
396
+ * @property {boolean} [options.banIngressAdd=false] - Ban IPs from ingress.
397
+ * @property {boolean} [options.banIngressRemove=false] - Unban IPs from ingress.
398
+ * @property {boolean} [options.banIngressList=false] - List banned ingress IPs.
399
+ * @property {boolean} [options.banIngressClear=false] - Clear all banned ingress IPs.
400
+ * @property {boolean} [options.banEgressAdd=false] - Ban IPs from egress.
401
+ * @property {boolean} [options.banEgressRemove=false] - Unban IPs from egress.
402
+ * @property {boolean} [options.banEgressList=false] - List banned egress IPs.
403
+ * @property {boolean} [options.banEgressClear=false] - Clear all banned egress IPs.
404
+ * @property {boolean} [options.banBothAdd=false] - Ban IPs from both ingress and egress.
405
+ * @property {boolean} [options.banBothRemove=false] - Unban IPs from both ingress and egress.
406
+ * @property {boolean} [options.copy=false] - Copy the public IP to clipboard.
407
+ * @return {Promise<string|void>} The public IP if no ban/unban action is taken.
408
+ */
409
+ static async ipDispatcher(
410
+ ips = '',
411
+ options = {
412
+ banIngressAdd: false,
413
+ banIngressRemove: false,
414
+ banIngressList: false,
415
+ banIngressClear: false,
416
+ banEgressAdd: false,
417
+ banEgressRemove: false,
418
+ banEgressList: false,
419
+ banEgressClear: false,
420
+ banBothAdd: false,
421
+ banBothRemove: false,
422
+ copy: false,
423
+ },
424
+ ) {
425
+ const ipList = ips
426
+ ? ips
427
+ .split(',')
428
+ .map((i) => i.trim())
429
+ .filter(Boolean)
430
+ : [];
431
+
432
+ if (options.banIngressAdd) {
433
+ return ipList.forEach((ip) => Dns.banIngress(ip));
434
+ }
435
+ if (options.banIngressRemove) {
436
+ return ipList.forEach((ip) => Dns.unbanIngress(ip));
437
+ }
438
+ if (options.banIngressList) {
439
+ return Dns.listBannedIngress();
440
+ }
441
+ if (options.banIngressClear) {
442
+ return Dns.clearBannedIngress();
443
+ }
444
+
445
+ if (options.banEgressAdd) {
446
+ return ipList.forEach((ip) => Dns.banEgress(ip));
447
+ }
448
+ if (options.banEgressRemove) {
449
+ return ipList.forEach((ip) => Dns.unbanEgress(ip));
450
+ }
451
+ if (options.banEgressList) {
452
+ return Dns.listBannedEgress();
453
+ }
454
+ if (options.banEgressClear) {
455
+ return Dns.clearBannedEgress();
456
+ }
457
+
458
+ if (options.banBothAdd) {
459
+ return ipList.forEach((ip) => {
460
+ Dns.banIngress(ip);
461
+ Dns.banEgress(ip);
462
+ });
463
+ }
464
+ if (options.banBothRemove) {
465
+ return ipList.forEach((ip) => {
466
+ Dns.unbanIngress(ip);
467
+ Dns.unbanEgress(ip);
468
+ });
469
+ }
470
+
471
+ const ip = await Dns.getPublicIp();
472
+ if (options.copy) return pbcopy(ip);
473
+ console.log(ip);
474
+ return ip;
475
+ }
235
476
  }
236
477
 
237
478
  /**
@@ -96,10 +96,15 @@ const ProcessController = {
96
96
  * @param {boolean} [options.async=false] - Run command asynchronously.
97
97
  * @param {boolean} [options.stdout=false] - Return stdout content (string) instead of shelljs result object.
98
98
  * @param {boolean} [options.disableLog=false] - Prevent logging of the command.
99
+ * @param {Function} [options.callback=null] - Callback function for asynchronous execution.
99
100
  * @returns {string|shelljs.ShellString} The result of the shell command (string if `stdout: true`, otherwise a ShellString object).
100
101
  */
101
- const shellExec = (cmd, options = { silent: false, async: false, stdout: false, disableLog: false }) => {
102
+ const shellExec = (
103
+ cmd,
104
+ options = { silent: false, async: false, stdout: false, disableLog: false, callback: null },
105
+ ) => {
102
106
  if (!options.disableLog) logger.info(`cmd`, cmd);
107
+ if (options.callback) return shell.exec(cmd, options, options.callback);
103
108
  return options.stdout ? shell.exec(cmd, options).stdout : shell.exec(cmd, options);
104
109
  };
105
110
 
@@ -119,7 +119,9 @@ class UnderpostStartUp {
119
119
  * @param {boolean} options.run - Whether to run the deployment.
120
120
  */
121
121
  async callback(deployId = 'dd-default', env = 'development', options = { build: false, run: false }) {
122
+ UnderpostRootEnv.API.set('container-status', `${deployId}-${env}-build-deployment`);
122
123
  if (options.build === true) await UnderpostStartUp.API.build(deployId, env);
124
+ UnderpostRootEnv.API.set('container-status', `${deployId}-${env}-initializing-deployment`);
123
125
  if (options.run === true) await UnderpostStartUp.API.run(deployId, env);
124
126
  },
125
127
  /**
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env bash
2
- # cleanup-snap.sh
3
- # Remove all disabled snap revisions to free up disk space.
4
-
5
- set -euo pipefail
6
-
7
- # Ensure we’re running as root
8
- if [[ $EUID -ne 0 ]]; then
9
- echo "Please run this script with sudo or as root."
10
- exit 1
11
- fi
12
-
13
- echo "Gathering list of snaps with disabled revisions..."
14
- snap list --all \
15
- | awk '/disabled/ {print $1, $3}' \
16
- | while read -r pkg rev; do
17
- echo " -> Removing $pkg (revision $rev)..."
18
- snap remove "$pkg" --revision="$rev"
19
- done
20
-
21
- echo "Cleanup complete."
22
- echo
23
- echo "Tip: Limit how many revisions Snap retains by setting:"
24
- echo " sudo snap set system refresh.retain=2"
25
- echo "Then apply with:"
26
- echo " sudo snap refresh"