underpost 3.0.3 → 3.1.0

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 (82) hide show
  1. package/{.env.production → .env.example} +20 -2
  2. package/.github/workflows/ghpkg.ci.yml +1 -1
  3. package/.github/workflows/gitlab.ci.yml +1 -1
  4. package/.github/workflows/npmpkg.ci.yml +22 -7
  5. package/.github/workflows/publish.ci.yml +5 -5
  6. package/.github/workflows/pwa-microservices-template-page.cd.yml +3 -3
  7. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  8. package/.github/workflows/release.cd.yml +3 -2
  9. package/.vscode/extensions.json +9 -8
  10. package/.vscode/settings.json +3 -2
  11. package/CHANGELOG.md +146 -1
  12. package/CLI-HELP.md +71 -52
  13. package/README.md +2 -2
  14. package/bin/build.js +4 -1
  15. package/bin/deploy.js +150 -208
  16. package/bin/file.js +2 -1
  17. package/bin/vs.js +3 -3
  18. package/conf.js +30 -13
  19. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  20. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  21. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  22. package/manifests/deployment/dd-test-development/deployment.yaml +52 -52
  23. package/manifests/deployment/dd-test-development/proxy.yaml +4 -4
  24. package/manifests/pv-pvc-dd.yaml +1 -1
  25. package/package.json +48 -43
  26. package/scripts/k3s-node-setup.sh +1 -1
  27. package/src/api/document/document.service.js +1 -1
  28. package/src/api/file/file.controller.js +3 -1
  29. package/src/api/file/file.service.js +28 -5
  30. package/src/api/user/user.router.js +10 -5
  31. package/src/api/user/user.service.js +7 -7
  32. package/src/cli/baremetal.js +6 -10
  33. package/src/cli/cloud-init.js +0 -3
  34. package/src/cli/db.js +54 -71
  35. package/src/cli/deploy.js +64 -12
  36. package/src/cli/env.js +4 -4
  37. package/src/cli/fs.js +0 -2
  38. package/src/cli/image.js +0 -3
  39. package/src/cli/index.js +27 -13
  40. package/src/cli/monitor.js +5 -6
  41. package/src/cli/repository.js +322 -35
  42. package/src/cli/run.js +118 -69
  43. package/src/cli/secrets.js +0 -3
  44. package/src/cli/ssh.js +1 -1
  45. package/src/client/components/core/AgGrid.js +20 -5
  46. package/src/client/components/core/Content.js +22 -3
  47. package/src/client/components/core/Docs.js +21 -4
  48. package/src/client/components/core/FileExplorer.js +71 -4
  49. package/src/client/components/core/Input.js +1 -1
  50. package/src/client/components/core/Modal.js +20 -6
  51. package/src/client/public/default/sitemap +3 -3
  52. package/src/client/public/test/sitemap +3 -3
  53. package/src/client.build.js +0 -3
  54. package/src/client.dev.js +0 -3
  55. package/src/db/DataBaseProvider.js +17 -2
  56. package/src/db/mariadb/MariaDB.js +14 -9
  57. package/src/db/mongo/MongooseDB.js +17 -1
  58. package/src/index.js +1 -1
  59. package/src/proxy.js +0 -3
  60. package/src/runtime/express/Express.js +7 -1
  61. package/src/runtime/lampp/Lampp.js +6 -13
  62. package/src/server/auth.js +6 -9
  63. package/src/server/backup.js +2 -3
  64. package/src/server/client-build-docs.js +178 -3
  65. package/src/server/client-build-live.js +9 -18
  66. package/src/server/client-build.js +175 -38
  67. package/src/server/client-dev-server.js +14 -13
  68. package/src/server/conf.js +357 -149
  69. package/src/server/cron.js +2 -1
  70. package/src/server/dns.js +28 -12
  71. package/src/server/downloader.js +0 -2
  72. package/src/server/logger.js +27 -9
  73. package/src/server/peer.js +0 -2
  74. package/src/server/process.js +1 -50
  75. package/src/server/proxy.js +4 -8
  76. package/src/server/runtime.js +5 -8
  77. package/src/server/ssr.js +0 -3
  78. package/src/server/start.js +5 -5
  79. package/src/server/tls.js +0 -2
  80. package/src/server.js +0 -4
  81. package/.env.development +0 -43
  82. package/.env.test +0 -43
package/src/cli/db.js CHANGED
@@ -6,15 +6,30 @@
6
6
  * Supports MariaDB and MongoDB with import/export capabilities, Git integration, and multi-pod operations.
7
7
  */
8
8
 
9
- import { mergeFile, splitFileFactory } from '../server/conf.js';
9
+ import { mergeFile, splitFileFactory, loadConfServerJson, resolveConfSecrets } from '../server/conf.js';
10
10
  import { loggerFactory } from '../server/logger.js';
11
11
  import { shellExec } from '../server/process.js';
12
12
  import fs from 'fs-extra';
13
13
  import { DataBaseProvider } from '../db/DataBaseProvider.js';
14
- import { loadReplicas, pathPortAssignmentFactory } from '../server/conf.js';
14
+ import { loadReplicas, pathPortAssignmentFactory, loadCronDeployEnv } from '../server/conf.js';
15
15
  import Underpost from '../index.js';
16
16
  const logger = loggerFactory(import.meta);
17
17
 
18
+ /**
19
+ * Redacts credentials from shell command strings before logging.
20
+ * Masks passwords in `-p<password>`, `--password=<password>`, and `-P <password>` patterns.
21
+ * @param {string} cmd - The raw command string.
22
+ * @memberof UnderpostDB
23
+ * @returns {string} The command with credentials replaced by `***`.
24
+ */
25
+ const sanitizeCommand = (cmd) => {
26
+ if (typeof cmd !== 'string') return cmd;
27
+ return cmd
28
+ .replace(/-p['"]?[^\s'"]+/g, '-p***')
29
+ .replace(/--password=['"]?[^\s'"]+/g, '--password=***')
30
+ .replace(/-P\s+['"]?[^\s'"]+/g, '-P ***');
31
+ };
32
+
18
33
  /**
19
34
  * Constants for database operations
20
35
  * @constant {number} MAX_BACKUP_RETENTION - Maximum number of backups to retain
@@ -133,10 +148,10 @@ class UnderpostDB {
133
148
  const { context = '' } = options;
134
149
 
135
150
  try {
136
- logger.info(`Executing kubectl command`, { command, context });
137
- return shellExec(command, { stdout: true });
151
+ logger.info(`Executing kubectl command`, { command: sanitizeCommand(command), context });
152
+ return shellExec(command, { stdout: true, disableLog: true });
138
153
  } catch (error) {
139
- logger.error(`kubectl command failed`, { command, error: error.message, context });
154
+ logger.error(`kubectl command failed`, { command: sanitizeCommand(command), error: error.message, context });
140
155
  throw error;
141
156
  }
142
157
  },
@@ -200,11 +215,30 @@ class UnderpostDB {
200
215
  const kubectlCmd = `sudo kubectl exec -n ${namespace} -i ${podName} -- sh -c "${command}"`;
201
216
  return Underpost.db._executeKubectl(kubectlCmd, { context: `exec in pod ${podName}` });
202
217
  } catch (error) {
203
- logger.error('Failed to execute command in pod', { podName, command, error: error.message });
218
+ logger.error('Failed to execute command in pod', {
219
+ podName,
220
+ command: sanitizeCommand(command),
221
+ error: error.message,
222
+ });
204
223
  throw error;
205
224
  }
206
225
  },
207
226
 
227
+ /**
228
+ * Helper: Resolves the latest backup timestamp from an existing backup directory.
229
+ * Scans the directory for numeric (epoch) sub-folders and returns the most recent one.
230
+ * @method _getLatestBackupTimestamp
231
+ * @memberof UnderpostDB
232
+ * @param {string} backupDir - Path to the host-folder backup directory.
233
+ * @return {string|null} The latest timestamp string, or null if none found.
234
+ */
235
+ _getLatestBackupTimestamp(backupDir) {
236
+ if (!fs.existsSync(backupDir)) return null;
237
+ const entries = fs.readdirSync(backupDir).filter((e) => /^\d+$/.test(e));
238
+ if (entries.length === 0) return null;
239
+ return entries.sort((a, b) => parseInt(b) - parseInt(a))[0];
240
+ },
241
+
208
242
  /**
209
243
  * Helper: Manages Git repository for backups.
210
244
  * @method _manageGitRepo
@@ -275,55 +309,6 @@ class UnderpostDB {
275
309
  }
276
310
  },
277
311
 
278
- /**
279
- * Helper: Manages backup timestamps and cleanup.
280
- * @method _manageBackupTimestamps
281
- * @memberof UnderpostDB
282
- * @param {string} backupPath - Backup directory path.
283
- * @param {number} newTimestamp - New backup timestamp.
284
- * @param {boolean} shouldCleanup - Whether to cleanup old backups.
285
- * @return {Object} Backup info with current and removed timestamps.
286
- */
287
- _manageBackupTimestamps(backupPath, newTimestamp, shouldCleanup) {
288
- try {
289
- if (!fs.existsSync(backupPath)) {
290
- fs.mkdirSync(backupPath, { recursive: true });
291
- }
292
-
293
- // Delete empty folders
294
- shellExec(`cd ${backupPath} && find . -type d -empty -delete`);
295
-
296
- const times = fs.readdirSync(backupPath);
297
- const validTimes = times.map((t) => parseInt(t)).filter((t) => !isNaN(t));
298
-
299
- const currentBackupTimestamp = validTimes.length > 0 ? Math.max(...validTimes) : null;
300
- const removeBackupTimestamp = validTimes.length > 0 ? Math.min(...validTimes) : null;
301
-
302
- // Cleanup old backups if we have too many
303
- if (shouldCleanup && validTimes.length >= MAX_BACKUP_RETENTION && removeBackupTimestamp) {
304
- const removeDir = `${backupPath}/${removeBackupTimestamp}`;
305
- logger.info('Removing old backup', { path: removeDir });
306
- fs.removeSync(removeDir);
307
- }
308
-
309
- // Create new backup directory
310
- if (shouldCleanup) {
311
- const newBackupDir = `${backupPath}/${newTimestamp}`;
312
- logger.info('Creating new backup directory', { path: newBackupDir });
313
- fs.mkdirSync(newBackupDir, { recursive: true });
314
- }
315
-
316
- return {
317
- current: currentBackupTimestamp,
318
- removed: removeBackupTimestamp,
319
- count: validTimes.length,
320
- };
321
- } catch (error) {
322
- logger.error('Error managing backup timestamps', { backupPath, error: error.message });
323
- return { current: null, removed: null, count: 0 };
324
- }
325
- },
326
-
327
312
  /**
328
313
  * Helper: Performs MariaDB import operation.
329
314
  * @method _importMariaDB
@@ -624,7 +609,7 @@ class UnderpostDB {
624
609
  logger.info('Getting MariaDB table statistics', { podName, dbName });
625
610
 
626
611
  const command = `sudo kubectl exec -n ${namespace} -i ${podName} -- mariadb -u ${user} -p${password} ${dbName} -e "SELECT TABLE_NAME as 'table', TABLE_ROWS as 'count' FROM information_schema.TABLES WHERE TABLE_SCHEMA = '${dbName}' ORDER BY TABLE_NAME;" --skip-column-names --batch`;
627
- const output = shellExec(command, { stdout: true, silent: true });
612
+ const output = shellExec(command, { stdout: true, silent: true, disableLog: true });
628
613
 
629
614
  if (!output || output.trim() === '') {
630
615
  logger.warn('No tables found or empty output');
@@ -788,6 +773,7 @@ class UnderpostDB {
788
773
  kind: false,
789
774
  },
790
775
  ) {
776
+ loadCronDeployEnv();
791
777
  const newBackupTimestamp = new Date().getTime();
792
778
  const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
793
779
 
@@ -844,7 +830,7 @@ class UnderpostDB {
844
830
  continue;
845
831
  }
846
832
 
847
- const confServer = JSON.parse(fs.readFileSync(confServerPath, 'utf8'));
833
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
848
834
 
849
835
  // Build database configuration map
850
836
  for (const host of Object.keys(confServer)) {
@@ -943,16 +929,11 @@ class UnderpostDB {
943
929
 
944
930
  logger.info('Processing database', { hostFolder, provider, dbName, deployId });
945
931
 
946
- const backUpPath = `../${repoName}/${hostFolder}`;
947
- const backupInfo = Underpost.db._manageBackupTimestamps(
948
- backUpPath,
949
- newBackupTimestamp,
950
- options.export === true,
951
- );
932
+ const latestBackupTimestamp = Underpost.db._getLatestBackupTimestamp(`../${repoName}/${hostFolder}`);
952
933
 
953
- dbs[provider][dbName].currentBackupTimestamp = backupInfo.current;
934
+ dbs[provider][dbName].currentBackupTimestamp = latestBackupTimestamp;
954
935
 
955
- const currentTimestamp = backupInfo.current || newBackupTimestamp;
936
+ const currentTimestamp = latestBackupTimestamp || newBackupTimestamp;
956
937
  const sqlContainerPath = `/home/${dbName}.sql`;
957
938
  const fromPartsPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}-parths.json`;
958
939
  const toSqlPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}.sql`;
@@ -1149,6 +1130,7 @@ class UnderpostDB {
1149
1130
  host = process.env.DEFAULT_DEPLOY_HOST,
1150
1131
  path = process.env.DEFAULT_DEPLOY_PATH,
1151
1132
  ) {
1133
+ loadCronDeployEnv();
1152
1134
  deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
1153
1135
  host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
1154
1136
  path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
@@ -1171,7 +1153,7 @@ class UnderpostDB {
1171
1153
  throw new Error(`Server configuration not found: ${confServerPath}`);
1172
1154
  }
1173
1155
 
1174
- const { db } = JSON.parse(fs.readFileSync(confServerPath, 'utf8'))[host][path];
1156
+ const { db } = loadConfServerJson(confServerPath, { resolve: true })[host][path];
1175
1157
 
1176
1158
  try {
1177
1159
  await DataBaseProvider.load({ apis: ['instance', 'cron'], host, path, db });
@@ -1194,7 +1176,7 @@ class UnderpostDB {
1194
1176
  continue;
1195
1177
  }
1196
1178
 
1197
- const confServer = loadReplicas(deployId, JSON.parse(fs.readFileSync(confServerPath, 'utf8')));
1179
+ const confServer = loadReplicas(deployId, loadConfServerJson(confServerPath, { resolve: true }));
1198
1180
  const router = await Underpost.deploy.routerFactory(deployId, env);
1199
1181
  const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
1200
1182
 
@@ -1257,7 +1239,7 @@ class UnderpostDB {
1257
1239
  }
1258
1240
  }
1259
1241
  } catch (error) {
1260
- logger.error('Failed to create instance metadata', { error: error.message, stack: error.stack });
1242
+ logger.error('Failed to create instance metadata', { error: error.message });
1261
1243
  throw error;
1262
1244
  }
1263
1245
 
@@ -1297,7 +1279,7 @@ class UnderpostDB {
1297
1279
  await new Cron(body).save();
1298
1280
  }
1299
1281
  } catch (error) {
1300
- logger.error('Failed to create cron metadata', { error: error.message, stack: error.stack });
1282
+ logger.error('Failed to create cron metadata', { error: error.message });
1301
1283
  }
1302
1284
 
1303
1285
  await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
@@ -1325,6 +1307,7 @@ class UnderpostDB {
1325
1307
  dryRun: false,
1326
1308
  },
1327
1309
  ) {
1310
+ loadCronDeployEnv();
1328
1311
  if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
1329
1312
 
1330
1313
  logger.info('Starting File collection cleanup', { deployList, options });
@@ -1359,7 +1342,7 @@ class UnderpostDB {
1359
1342
  continue;
1360
1343
  }
1361
1344
 
1362
- const confServer = JSON.parse(fs.readFileSync(confServerPath, 'utf8'));
1345
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
1363
1346
 
1364
1347
  // Process each host+path combination
1365
1348
  for (const host of Object.keys(confServer)) {
@@ -1499,7 +1482,6 @@ class UnderpostDB {
1499
1482
  host,
1500
1483
  path,
1501
1484
  error: error.message,
1502
- stack: error.stack,
1503
1485
  });
1504
1486
  }
1505
1487
  }
@@ -1553,6 +1535,7 @@ class UnderpostDB {
1553
1535
  crons: false,
1554
1536
  },
1555
1537
  ) {
1538
+ loadCronDeployEnv();
1556
1539
  deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
1557
1540
  host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
1558
1541
  path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
package/src/cli/deploy.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  Config,
12
12
  deployRangePortFactory,
13
13
  getDataDeploy,
14
+ loadConfServerJson,
14
15
  loadReplicas,
15
16
  pathPortAssignmentFactory,
16
17
  } from '../server/conf.js';
@@ -132,7 +133,7 @@ class UnderpostDeploy {
132
133
  `npm install -g npm@11.2.0`,
133
134
  `npm install -g underpost`,
134
135
  `underpost secret underpost --create-from-file /etc/config/.env.${env}`,
135
- `underpost start --build --run --underpost-quickly-install ${deployId} ${env}`,
136
+ `underpost start --build --run ${deployId} ${env}`,
136
137
  ];
137
138
  const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
138
139
  if (!volumes)
@@ -230,7 +231,7 @@ spec:
230
231
  if (!deployId) continue;
231
232
  const confServer = loadReplicas(
232
233
  deployId,
233
- JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8')),
234
+ loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
234
235
  );
235
236
  const router = await Underpost.deploy.routerFactory(deployId, env);
236
237
  const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
@@ -259,6 +260,27 @@ ${Underpost.deploy
259
260
  }
260
261
  fs.writeFileSync(`./engine-private/conf/${deployId}/build/${env}/deployment.yaml`, deploymentYamlParts, 'utf8');
261
262
 
263
+ const confVolume = fs.existsSync(`./engine-private/conf/${deployId}/conf.volume.json`)
264
+ ? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
265
+ : [];
266
+ if (confVolume.length > 0) {
267
+ let volumeYaml = '';
268
+ for (const deploymentVersion of deploymentVersions) {
269
+ for (const volume of confVolume) {
270
+ if (!volume.claimName) continue;
271
+ const pvcId = `${volume.claimName}-${deployId}-${env}-${deploymentVersion}`;
272
+ const pvId = pvcId.replace(/^pvc-/, 'pv-');
273
+ const hostPath = `/home/dd/engine/volume/${pvId}`;
274
+ volumeYaml += `---\n${Underpost.deploy.persistentVolumeFactory({
275
+ pvcId,
276
+ namespace: options.namespace,
277
+ hostPath,
278
+ })}\n`;
279
+ }
280
+ }
281
+ fs.writeFileSync(`./engine-private/conf/${deployId}/build/${env}/pv-pvc.yaml`, volumeYaml, 'utf8');
282
+ }
283
+
262
284
  let proxyYaml = '';
263
285
  let secretYaml = '';
264
286
  const customServices = fs.existsSync(`./engine-private/conf/${deployId}/conf.services.json`)
@@ -335,7 +357,7 @@ ${Underpost.deploy
335
357
  const yamlPath = `./engine-private/conf/${deployId}/build/${env}/secret.yaml`;
336
358
  fs.writeFileSync(yamlPath, secretYaml, 'utf8');
337
359
  } else {
338
- const deploymentsFiles = ['Dockerfile', 'proxy.yaml', 'deployment.yaml'];
360
+ const deploymentsFiles = ['Dockerfile', 'proxy.yaml', 'deployment.yaml', 'pv-pvc.yaml'];
339
361
  for (const file of deploymentsFiles) {
340
362
  if (fs.existsSync(`./engine-private/conf/${deployId}/build/${env}/${file}`)) {
341
363
  fs.copyFileSync(
@@ -385,7 +407,7 @@ spec:
385
407
  // kubectl get deploy,sts,svc,configmap,secret -n default -o yaml --export > default.yaml
386
408
  const hostTest = options?.hostTest
387
409
  ? options.hostTest
388
- : Object.keys(JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8')))[0];
410
+ : Object.keys(loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`))[0];
389
411
  const info = shellExec(`sudo kubectl get HTTPProxy/${hostTest} -n ${options.namespace} -o yaml`, {
390
412
  silent: true,
391
413
  stdout: true,
@@ -550,7 +572,7 @@ EOF`);
550
572
  if (!(options.versions && typeof options.versions === 'string')) options.versions = 'blue,green';
551
573
  if (!options.replicas) options.replicas = 1;
552
574
  if (options.sync)
553
- getDataDeploy({
575
+ await getDataDeploy({
554
576
  buildSingleReplica: true,
555
577
  });
556
578
  if (options.buildManifest === true) await Underpost.deploy.buildManifest(deployList, env, options);
@@ -591,7 +613,7 @@ EOF`);
591
613
  continue;
592
614
  }
593
615
 
594
- const confServer = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8'));
616
+ const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
595
617
  const confVolume = fs.existsSync(`./engine-private/conf/${deployId}/conf.volume.json`)
596
618
  ? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
597
619
  : [];
@@ -844,6 +866,7 @@ EOF`);
844
866
  ${Underpost.deploy.persistentVolumeFactory({
845
867
  hostPath: rootVolumeHostPath,
846
868
  pvcId,
869
+ namespace,
847
870
  })}
848
871
  EOF
849
872
  `);
@@ -914,15 +937,44 @@ EOF
914
937
  * @param {object} options - Options for the persistent volume and claim creation.
915
938
  * @param {string} options.hostPath - Host path for the persistent volume.
916
939
  * @param {string} options.pvcId - Persistent volume claim ID.
940
+ * @param {string} [options.namespace='default'] - Kubernetes namespace for the PVC claimRef.
917
941
  * @returns {string} - YAML configuration for the persistent volume and claim.
918
942
  * @memberof UnderpostDeploy
919
943
  */
920
- persistentVolumeFactory({ hostPath, pvcId }) {
921
- return fs
922
- .readFileSync(`./manifests/pv-pvc-dd.yaml`, 'utf8')
923
- .replace('/home/dd', hostPath)
924
- .replace('pv-dd', pvcId.replace('pvc-', 'pv-'))
925
- .replace('pvc-dd', pvcId);
944
+ persistentVolumeFactory({ hostPath, pvcId, namespace = 'default' }) {
945
+ const pvId = pvcId.replace(/^pvc-/, 'pv-');
946
+ return `apiVersion: v1
947
+ kind: PersistentVolume
948
+ metadata:
949
+ name: ${pvId}
950
+ spec:
951
+ capacity:
952
+ storage: 5Gi
953
+ accessModes:
954
+ - ReadWriteOnce
955
+ persistentVolumeReclaimPolicy: Retain
956
+ storageClassName: manual
957
+ claimRef:
958
+ apiVersion: v1
959
+ kind: PersistentVolumeClaim
960
+ name: ${pvcId}
961
+ namespace: ${namespace}
962
+ hostPath:
963
+ path: ${hostPath}
964
+ type: DirectoryOrCreate
965
+ ---
966
+ apiVersion: v1
967
+ kind: PersistentVolumeClaim
968
+ metadata:
969
+ name: ${pvcId}
970
+ spec:
971
+ accessModes:
972
+ - ReadWriteOnce
973
+ storageClassName: manual
974
+ volumeName: ${pvId}
975
+ resources:
976
+ requests:
977
+ storage: 5Gi`;
926
978
  },
927
979
 
928
980
  /**
package/src/cli/env.js CHANGED
@@ -10,12 +10,10 @@ import { loggerFactory } from '../server/logger.js';
10
10
  import dotenv from 'dotenv';
11
11
  import { pbcopy } from '../server/process.js';
12
12
 
13
- dotenv.config();
14
-
15
13
  const logger = loggerFactory(import.meta);
16
14
 
17
15
  /**
18
- * @class UnderpostEnv
16
+ * @class UnderpostRootEnv
19
17
  * @description Manages the environment variables of the underpost root.
20
18
  * @memberof UnderpostEnv
21
19
  */
@@ -41,7 +39,9 @@ class UnderpostRootEnv {
41
39
  if (options.build) {
42
40
  const deployIdList = options.deployId
43
41
  ? [options.deployId]
44
- : fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',');
42
+ : fs.existsSync(`./engine-private/deploy/dd.router`)
43
+ ? fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')
44
+ : [DEFAULT_DEPLOY_ID];
45
45
  for (const deployId of deployIdList)
46
46
  for (const envFile of ['test', 'development', 'production'])
47
47
  _set(`./engine-private/conf/${deployId}/.env.${envFile}`, key, value);
package/src/cli/fs.js CHANGED
@@ -6,14 +6,12 @@
6
6
 
7
7
  import { v2 as cloudinary } from 'cloudinary';
8
8
  import { loggerFactory } from '../server/logger.js';
9
- import dotenv from 'dotenv';
10
9
  import AdmZip from 'adm-zip';
11
10
  import * as dir from 'path';
12
11
  import fs from 'fs-extra';
13
12
  import Downloader from '../server/downloader.js';
14
13
  import { shellExec } from '../server/process.js';
15
14
  import Underpost from '../index.js';
16
- dotenv.config();
17
15
 
18
16
  const logger = loggerFactory(import.meta);
19
17
 
package/src/cli/image.js CHANGED
@@ -5,14 +5,11 @@
5
5
  */
6
6
 
7
7
  import fs from 'fs-extra';
8
- import dotenv from 'dotenv';
9
8
  import { loggerFactory } from '../server/logger.js';
10
9
  import Underpost from '../index.js';
11
10
  import { getNpmRootPath, getUnderpostRootPath } from '../server/conf.js';
12
11
  import { shellExec } from '../server/process.js';
13
12
 
14
- dotenv.config();
15
-
16
13
  const logger = loggerFactory(import.meta);
17
14
 
18
15
  /**
package/src/cli/index.js CHANGED
@@ -7,11 +7,10 @@ import { commitData } from '../client/components/core/CommonJs.js';
7
7
 
8
8
  import Underpost from '../index.js';
9
9
 
10
- const underpostRootPath = getUnderpostRootPath();
10
+ const underpostGlobalEnv = `${getUnderpostRootPath()}/.env`;
11
11
 
12
- fs.existsSync(`${underpostRootPath}/.env`)
13
- ? dotenv.config({ path: `${underpostRootPath}/.env`, override: true })
14
- : dotenv.config();
12
+ if (fs.existsSync(underpostGlobalEnv)) dotenv.config({ path: underpostGlobalEnv, override: true });
13
+ else dotenv.config();
15
14
 
16
15
  const program = new Command();
17
16
 
@@ -27,6 +26,10 @@ program
27
26
  .option('--build', 'Build the deployment to pwa-microservices-template (requires --deploy-id)')
28
27
  .option('--clean-template', 'Clean the build directory (pwa-microservices-template)')
29
28
  .option('--sync-conf', 'Sync configuration to private repositories (requires --deploy-id)')
29
+ .option(
30
+ '--sync-start',
31
+ "Sync start scripts in deploy ID package.json with root package.json (use 'dd' as --deploy-id to sync all dd.router)",
32
+ )
30
33
  .option('--purge', 'Remove deploy ID conf and all related repositories (requires --deploy-id)')
31
34
  .option('--dev', 'Sets the development cli context')
32
35
  .option('--default-conf', 'Create default deploy ID conf env files')
@@ -34,6 +37,17 @@ program
34
37
  .description('Initializes a new Underpost project, service, or configuration.')
35
38
  .action(Underpost.repo.new);
36
39
 
40
+ program
41
+ .command('client')
42
+ .argument('[deploy-id]', 'The deployment ID to build.', 'dd-default')
43
+ .argument('[sub-conf]', 'The sub-configuration for the build.', '')
44
+ .argument('[host]', 'Comma-separated hosts to filter the build.', '')
45
+ .argument('[path]', 'Comma-separated paths to filter the build.', '')
46
+ .option('--sync-env-port', 'Sync environment port assignments across all deploy IDs')
47
+ .option('--single-replica', 'Build single replica folders instead of full client')
48
+ .description('Builds client assets, single replicas, and/or syncs environment ports.')
49
+ .action(Underpost.repo.client);
50
+
37
51
  program
38
52
  .command('start')
39
53
  .argument('<deploy-id>', 'The unique identifier for the deployment configuration.')
@@ -51,7 +65,7 @@ program
51
65
  .command('clone')
52
66
  .argument(`<uri>`, 'The URI of the GitHub repository (e.g., "username/repository").')
53
67
  .option('--bare', 'Performs a bare clone, downloading only the .git files.')
54
- .option('-g8', 'Uses the g8 repository extension for cloning.')
68
+ .option('--g8', 'Uses the g8 repository extension for cloning.')
55
69
  .description('Clones a specified GitHub repository into the current directory.')
56
70
  .action(Underpost.repo.clone);
57
71
 
@@ -60,7 +74,7 @@ program
60
74
  .argument('<path>', 'The absolute or relative directory path where the repository is located.')
61
75
  .argument(`<uri>`, 'The URI of the GitHub repository (e.g., "username/repository").')
62
76
  .description('Pulls the latest changes from a specified GitHub repository.')
63
- .option('-g8', 'Uses the g8 repository extension for pulling.')
77
+ .option('--g8', 'Uses the g8 repository extension for pulling.')
64
78
  .action(Underpost.repo.pull);
65
79
 
66
80
  program
@@ -90,6 +104,7 @@ program
90
104
  '--changelog-no-hash',
91
105
  'Excludes commit hashes from the generated changelog entries (used with --changelog-build).',
92
106
  )
107
+ .option('-b', 'Shows the current Git branch name.')
93
108
  .description('Manages commits to a GitHub repository, supporting various commit types and options.')
94
109
  .action(Underpost.repo.commit);
95
110
 
@@ -98,7 +113,7 @@ program
98
113
  .argument('<path>', 'The absolute or relative directory path of the repository.')
99
114
  .argument(`<uri>`, 'The URI of the GitHub repository (e.g., "username/repository").')
100
115
  .option('-f', 'Forces the push, overwriting the remote repository history.')
101
- .option('-g8', 'Uses the g8 repository extension for pushing.')
116
+ .option('--g8', 'Uses the g8 repository extension for pushing.')
102
117
  .description('Pushes committed changes from a local repository to a remote GitHub repository.')
103
118
  .action(Underpost.repo.push);
104
119
 
@@ -112,11 +127,11 @@ program
112
127
  .argument('[subConf]', 'Optional: The sub configuration to set.')
113
128
  .description('Sets environment variables and configurations related to a specific deployment ID.')
114
129
  .action((deployId, env, subConf) => {
115
- if (fs.existsSync(`./engine-private/conf/${deployId}/.env.${env}`))
116
- dotenv.config({ path: `./engine-private/conf/${deployId}/.env.${env}`, override: true });
117
- else if (deployId === 'root') {
118
- deployId = Underpost.env.get('DEPLOY_ID');
119
- } else dotenv.config({ path: `./.env`, override: true });
130
+ if (deployId === 'root') {
131
+ const underpostRootDeployId = Underpost.env.get('DEPLOY_ID');
132
+ if (underpostRootDeployId) deployId = underpostRootDeployId;
133
+ }
134
+ if (env) process.env.NODE_ENV = env;
120
135
  loadConf(deployId, subConf);
121
136
  });
122
137
 
@@ -556,7 +571,6 @@ program
556
571
  .option('--force', 'Forces operation, overriding any warnings or conflicts.')
557
572
  .option('--tls', 'Enables TLS for the runner execution.')
558
573
  .option('--reset', 'Resets the runner state before execution.')
559
- .option('--terminal', 'Enables terminal mode for interactive script execution.')
560
574
  .option('--dev-proxy-port-offset <port-offset>', 'Sets a custom port offset for development proxy.')
561
575
  .option('--host-network', 'Enables host network mode for the runner execution.')
562
576
  .option('--requests-memory <requests-memory>', 'Requests memory limit for the runner execution.')
@@ -4,7 +4,7 @@
4
4
  * @namespace UnderpostMonitor
5
5
  */
6
6
 
7
- import { loadReplicas, pathPortAssignmentFactory } from '../server/conf.js';
7
+ import { loadReplicas, pathPortAssignmentFactory, loadConfServerJson, loadCronDeployEnv } from '../server/conf.js';
8
8
  import { loggerFactory } from '../server/logger.js';
9
9
  import axios from 'axios';
10
10
  import fs from 'fs-extra';
@@ -71,6 +71,7 @@ class UnderpostMonitor {
71
71
  commanderOptions,
72
72
  auxRouter,
73
73
  ) {
74
+ loadCronDeployEnv();
74
75
  if (!options.namespace) options.namespace = 'default';
75
76
  if (!options.replicas) options.replicas = '1';
76
77
  if (deployId === 'dd' && fs.existsSync(`./engine-private/deploy/dd.router`)) {
@@ -100,7 +101,7 @@ class UnderpostMonitor {
100
101
 
101
102
  const confServer = loadReplicas(
102
103
  deployId,
103
- JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8')),
104
+ loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
104
105
  );
105
106
 
106
107
  const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
@@ -127,7 +128,7 @@ class UnderpostMonitor {
127
128
 
128
129
  const switchTraffic = (targetTraffic) => {
129
130
  const nextTraffic = targetTraffic ?? (traffic === 'blue' ? 'green' : 'blue');
130
- // Delegate traffic switching to centralized deploy implementation so behavior is consistent
131
+ // Delegate traffic switching to deploy implementation so behavior is consistent
131
132
  Underpost.deploy.switchTraffic(deployId, env, nextTraffic, options.replicas, options.namespace, options);
132
133
  // Keep local traffic in sync with the environment
133
134
  traffic = nextTraffic;
@@ -168,9 +169,7 @@ class UnderpostMonitor {
168
169
  switch (options.type) {
169
170
  case 'blue-green':
170
171
  default: {
171
- const confServer = JSON.parse(
172
- fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8'),
173
- );
172
+ const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
174
173
 
175
174
  const namespace = options.namespace;
176
175
  Underpost.deploy.configMap(env, namespace);