underpost 3.2.8 → 3.2.10

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 (92) hide show
  1. package/.github/workflows/npmpkg.ci.yml +1 -0
  2. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  3. package/.github/workflows/release.cd.yml +1 -0
  4. package/.vscode/settings.json +10 -5
  5. package/CHANGELOG.md +223 -2
  6. package/CLI-HELP.md +36 -7
  7. package/README.md +38 -9
  8. package/bin/build.js +27 -11
  9. package/bin/deploy.js +20 -21
  10. package/bin/file.js +32 -13
  11. package/bin/index.js +2 -1
  12. package/bin/vs.js +1 -1
  13. package/bump.config.js +26 -0
  14. package/conf.js +20 -4
  15. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  16. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
  17. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  18. package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
  19. package/manifests/kind-config-dev.yaml +8 -0
  20. package/manifests/mongodb/pv-pvc.yaml +44 -8
  21. package/manifests/mongodb/statefulset.yaml +55 -68
  22. package/package.json +40 -25
  23. package/scripts/k3s-node-setup.sh +30 -11
  24. package/scripts/nat-iptables.sh +103 -18
  25. package/src/api/core/core.router.js +19 -14
  26. package/src/api/core/core.service.js +5 -5
  27. package/src/api/default/default.router.js +22 -18
  28. package/src/api/default/default.service.js +5 -5
  29. package/src/api/document/document.router.js +28 -23
  30. package/src/api/document/document.service.js +100 -23
  31. package/src/api/file/file.router.js +19 -13
  32. package/src/api/file/file.service.js +9 -7
  33. package/src/api/test/test.router.js +17 -12
  34. package/src/api/types.js +24 -0
  35. package/src/api/user/guest.service.js +5 -4
  36. package/src/api/user/user.router.js +297 -288
  37. package/src/api/user/user.service.js +100 -35
  38. package/src/cli/baremetal.js +20 -11
  39. package/src/cli/cluster.js +243 -55
  40. package/src/cli/db.js +106 -62
  41. package/src/cli/deploy.js +297 -154
  42. package/src/cli/fs.js +19 -3
  43. package/src/cli/index.js +37 -9
  44. package/src/cli/ipfs.js +4 -6
  45. package/src/cli/kubectl.js +4 -1
  46. package/src/cli/lxd.js +217 -135
  47. package/src/cli/release.js +289 -131
  48. package/src/cli/repository.js +91 -34
  49. package/src/cli/run.js +297 -56
  50. package/src/cli/test.js +9 -3
  51. package/src/client/Default.index.js +9 -3
  52. package/src/client/components/core/Auth.js +19 -5
  53. package/src/client/components/core/Docs.js +6 -34
  54. package/src/client/components/core/FileExplorer.js +6 -6
  55. package/src/client/components/core/Modal.js +65 -2
  56. package/src/client/components/core/PanelForm.js +56 -52
  57. package/src/client/components/core/Recover.js +4 -4
  58. package/src/client/components/core/Worker.js +170 -350
  59. package/src/client/services/default/default.management.js +20 -25
  60. package/src/client/services/user/guest.service.js +10 -3
  61. package/src/client/sw/core.sw.js +174 -112
  62. package/src/db/DataBaseProvider.js +120 -20
  63. package/src/db/mongo/MongoBootstrap.js +587 -0
  64. package/src/db/mongo/MongooseDB.js +126 -22
  65. package/src/index.js +1 -1
  66. package/src/runtime/express/Express.js +2 -2
  67. package/src/runtime/wp/Wp.js +8 -5
  68. package/src/server/auth.js +2 -2
  69. package/src/server/client-build-docs.js +1 -1
  70. package/src/server/client-build.js +94 -129
  71. package/src/server/conf.js +20 -65
  72. package/src/server/data-query.js +32 -20
  73. package/src/server/dns.js +22 -0
  74. package/src/server/process.js +180 -19
  75. package/src/server/runtime.js +1 -1
  76. package/src/server/start.js +26 -7
  77. package/src/server/valkey.js +9 -2
  78. package/src/ws/IoInterface.js +16 -16
  79. package/src/ws/core/channels/core.ws.chat.js +11 -11
  80. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  81. package/src/ws/core/channels/core.ws.stream.js +19 -19
  82. package/src/ws/core/core.ws.connection.js +8 -8
  83. package/src/ws/core/core.ws.server.js +6 -5
  84. package/src/ws/default/channels/default.ws.main.js +10 -10
  85. package/src/ws/default/default.ws.connection.js +4 -4
  86. package/src/ws/default/default.ws.server.js +4 -3
  87. package/typedoc.json +10 -1
  88. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  89. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  90. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  91. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  92. /package/src/client/ssr/{pages → views}/Test.js +0 -0
package/src/cli/run.js CHANGED
@@ -24,6 +24,7 @@ import { range, setPad, timer } from '../client/components/core/CommonJs.js';
24
24
  import os from 'os';
25
25
  import Underpost from '../index.js';
26
26
  import dotenv from 'dotenv';
27
+ import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
27
28
 
28
29
  const waitForPort = (port, host = '127.0.0.1', { maxAttempts = 30, interval = 2000 } = {}) =>
29
30
  new Promise((resolve, reject) => {
@@ -97,6 +98,7 @@ const logger = loggerFactory(import.meta);
97
98
  * @property {string} deployId - The deployment ID.
98
99
  * @property {string} instanceId - The instance ID.
99
100
  * @property {string} user - The user to run as.
101
+ * @property {string} group - The group to use.
100
102
  * @property {string} pid - The process ID.
101
103
  * @property {boolean} disablePrivateConfUpdate - Whether to disable private configuration updates.
102
104
  * @property {string} monitorStatus - The monitor status option.
@@ -110,6 +112,10 @@ const logger = loggerFactory(import.meta);
110
112
  * @property {string|Array<{ip: string, hostnames: string[]}>} hostAliases - Adds entries to the Pod /etc/hosts via Kubernetes hostAliases.
111
113
  * As a string (CLI): semicolon-separated entries of "ip=hostname1,hostname2" (e.g., "127.0.0.1=foo.local,bar.local;10.1.2.3=foo.remote").
112
114
  * As an array (programmatic): objects with `ip` and `hostnames` fields (e.g., [{ ip: "127.0.0.1", hostnames: ["foo.local"] }]).
115
+ * @property {boolean} gitClean - Whether to perform a `git clean` before running.
116
+ * @property {boolean} copy - Whether to copy the command to the clipboard instead of executing it.
117
+ * @property {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment (supported by: sync, template-deploy).
118
+ * @property {boolean} pullBundle - Whether to pull the bundle before running. Use together with --skip-full-build to skip the local build entirely (supported by: sync, template-deploy).
113
119
  * @memberof UnderpostRun
114
120
  */
115
121
  const DEFAULT_OPTION = {
@@ -161,6 +167,7 @@ const DEFAULT_OPTION = {
161
167
  deployId: '',
162
168
  instanceId: '',
163
169
  user: '',
170
+ group: '',
164
171
  pid: '',
165
172
  disablePrivateConfUpdate: false,
166
173
  monitorStatus: '',
@@ -174,6 +181,8 @@ const DEFAULT_OPTION = {
174
181
  hostAliases: '',
175
182
  gitClean: false,
176
183
  copy: false,
184
+ skipFullBuild: false,
185
+ pullBundle: false,
177
186
  };
178
187
 
179
188
  /**
@@ -219,7 +228,7 @@ class UnderpostRun {
219
228
  // Detect MongoDB primary pod using method
220
229
  let primaryMongoHost = 'mongodb-0.mongodb-service';
221
230
  try {
222
- const primaryPodName = Underpost.db.getMongoPrimaryPodName({
231
+ const primaryPodName = MongoBootstrap.getPrimaryPodName({
223
232
  namespace: options.namespace,
224
233
  podName: 'mongodb-0',
225
234
  });
@@ -435,6 +444,15 @@ class UnderpostRun {
435
444
  deployType = 'init';
436
445
  }
437
446
 
447
+ // If --build is set and path is a sync-engine-* target, push the pre-built client bundle
448
+ // to Cloudinary so the remote container can pull it instead of rebuilding from source.
449
+ if (options.build && deployConfId && deployConfId.startsWith('engine-')) {
450
+ const confName = deployConfId.replace(/^engine-/, '');
451
+ const pushDeployId = options.deployId || `dd-${confName}`;
452
+ logger.info(`[template-deploy] Running push-bundle for deployId: ${pushDeployId}`);
453
+ shellExec(`${baseCommand} run push-bundle --deploy-id ${pushDeployId}`);
454
+ }
455
+
438
456
  // Dispatch npmpkg CI workflow — this builds pwa-microservices-template first.
439
457
  // If deployConfId is set, npmpkg.ci.yml will dispatch the engine-<conf-id> CI
440
458
  // with sync=true after template build completes. The engine CI then dispatches
@@ -490,14 +508,16 @@ class UnderpostRun {
490
508
  },
491
509
  /**
492
510
  * @method docker-image
493
- * @description Dispatches the Docker image CI workflow (`docker-image.ci.yml`) for the `engine` repository via `workflow_dispatch`.
494
- * @param {string} path - The input value, identifier, or path for the operation.
511
+ * @description Dispatches the Docker image CI workflow (`docker-image[.<runtime>].ci.yml`) via `workflow_dispatch`.
512
+ * Repository resolution is delegated to `Underpost.repo.resolveInstanceRepo(path)`.
513
+ * @param {string} path - Optional runtime / workflow suffix (e.g. `cyberia-server`, `cyberia-client`).
495
514
  * @param {Object} options - The default underpost runner options for customizing workflow
496
515
  * @memberof UnderpostRun
497
516
  */
498
517
  'docker-image': (path, options = DEFAULT_OPTION) => {
518
+ const repo = Underpost.repo.resolveInstanceRepo(path);
499
519
  Underpost.repo.dispatchWorkflow({
500
- repo: `${process.env.GITHUB_USERNAME}/engine`,
520
+ repo,
501
521
  workflowFile: `docker-image${path ? `.${path}` : ''}.ci.yml`,
502
522
  ref: 'master',
503
523
  inputs: {},
@@ -521,16 +541,14 @@ class UnderpostRun {
521
541
  * @memberof UnderpostRun
522
542
  */
523
543
  pull: (path, options = DEFAULT_OPTION) => {
544
+ // shellExec is fail-fast by default — any non-zero exit throws and
545
+ // propagates up to the workflow step. No per-call flag required.
524
546
  if (!fs.existsSync(`/home/dd`) || !fs.existsSync(`/home/dd/engine`)) {
525
547
  fs.mkdirSync(`/home/dd`, { recursive: true });
526
- shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, {
527
- silent: true,
528
- });
548
+ shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
529
549
  } else {
530
550
  shellExec(`underpost run clean`);
531
- shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, {
532
- silent: true,
533
- });
551
+ shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
534
552
  }
535
553
  if (!fs.existsSync(`/home/dd/engine/engine-private`))
536
554
  shellExec(`cd /home/dd/engine && underpost clone ${process.env.GITHUB_USERNAME}/engine-private`, {
@@ -539,9 +557,7 @@ class UnderpostRun {
539
557
  else
540
558
  shellExec(
541
559
  `cd /home/dd/engine/engine-private && underpost pull . ${process.env.GITHUB_USERNAME}/engine-private`,
542
- {
543
- silent: true,
544
- },
560
+ { silent: true },
545
561
  );
546
562
  },
547
563
  /**
@@ -628,6 +644,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
628
644
  /**
629
645
  * @method sync
630
646
  * @description Cleans up, and then runs a deployment synchronization command (`underpost deploy --kubeadm --build-manifest --sync...`) using parameters parsed from `path` (deployId, replicas, versions, image, node).
647
+ *
648
+ * Forwards `--image-pull-policy <policy>` to the underlying `deploy --build-manifest` invocation when `options.imagePullPolicy` is set,
649
+ * which then plumbs through `buildManifest` and `deploymentYamlPartsFactory` to override the container `imagePullPolicy` in the generated
650
+ * `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
651
+ * `node bin run sync dd-core --kubeadm --image-pull-policy Always`
631
652
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing deploy parameters).
632
653
  * @param {Object} options - The default underpost runner options for customizing workflow
633
654
  * @memberof UnderpostRun
@@ -683,12 +704,16 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
683
704
  : '';
684
705
  const gitCleanFlag = options.gitClean ? ' --git-clean' : '';
685
706
 
707
+ const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
708
+ const pullBundleFlag = options.pullBundle ? ' --pull-bundle' : '';
709
+ const imagePullPolicyFlag = options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : '';
710
+
686
711
  shellExec(
687
712
  `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
688
713
  image ? ` --image ${image}` : ''
689
714
  }${versions ? ` --versions ${versions}` : ''}${
690
715
  options.namespace ? ` --namespace ${options.namespace}` : ''
691
- }${timeoutFlags}${cmdString}${gitCleanFlag} ${deployId} ${env}`,
716
+ }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag} ${deployId} ${env}`,
692
717
  );
693
718
 
694
719
  if (isDeployRunnerContext(path, options)) {
@@ -699,7 +724,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
699
724
  shellExec(
700
725
  `${baseCommand} deploy${clusterFlag}${cmdString} --replicas ${replicas} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
701
726
  options.namespace ? ` --namespace ${options.namespace}` : ''
702
- }${timeoutFlags}${gitCleanFlag}`,
727
+ }${timeoutFlags}${gitCleanFlag}${imagePullPolicyFlag}`,
703
728
  );
704
729
  if (!targetTraffic)
705
730
  targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
@@ -1005,6 +1030,9 @@ EOF
1005
1030
  cmd: _cmd,
1006
1031
  volumes: _volumes,
1007
1032
  metadata: _metadata,
1033
+ lifecycle: _lifecycle,
1034
+ readinessProbe: _readinessProbe,
1035
+ livenessProbe: _livenessProbe,
1008
1036
  } = instance;
1009
1037
  if (id !== _id) continue;
1010
1038
  const _deployId = `${deployId}-${_id}`;
@@ -1069,6 +1097,20 @@ EOF
1069
1097
  ),
1070
1098
  );
1071
1099
 
1100
+ // Resolve env-scoped lifecycle/probe blocks: each can be either
1101
+ // { ...envObj } // shared shape
1102
+ // { development: {...}, production: {...} } // env-specific
1103
+ const pickEnv = (v) => (v && (v.development || v.production) ? v[env] : v);
1104
+
1105
+ // Convention: an instance config may place `imagePullPolicy` inside
1106
+ // the env-scoped lifecycle block (alongside postStart/preStop).
1107
+ // Extract it onto the container spec (where K8S expects it) and
1108
+ // strip it from the lifecycle hash so the rendered YAML stays valid.
1109
+ // CLI override (`--image-pull-policy`) wins over the conf value.
1110
+ const { lifecycle: lifecycleForManifest, imagePullPolicy: lifecycleImagePullPolicy } =
1111
+ Underpost.deploy.extractInstanceImagePullPolicy(pickEnv(_lifecycle));
1112
+ const instanceImagePullPolicy = options.imagePullPolicy || lifecycleImagePullPolicy;
1113
+
1072
1114
  let deploymentYaml = `---
1073
1115
  ${Underpost.deploy
1074
1116
  .deploymentYamlPartsFactory({
@@ -1081,6 +1123,11 @@ ${Underpost.deploy
1081
1123
  namespace: options.namespace,
1082
1124
  volumes: _volumes,
1083
1125
  cmd: resolvedCmd,
1126
+ lifecycle: lifecycleForManifest,
1127
+ readinessProbe: pickEnv(_readinessProbe),
1128
+ livenessProbe: pickEnv(_livenessProbe),
1129
+ containerPort: _toPort,
1130
+ imagePullPolicy: instanceImagePullPolicy,
1084
1131
  })
1085
1132
  .replace('{{ports}}', buildKindPorts(_fromPort, _toPort))}
1086
1133
  `;
@@ -1169,14 +1216,34 @@ EOF
1169
1216
  volumes: _volumes,
1170
1217
  metadata: _metadata,
1171
1218
  runtime: _runtime,
1219
+ lifecycle: _lifecycle,
1220
+ readinessProbe: _readinessProbe,
1221
+ livenessProbe: _livenessProbe,
1172
1222
  } = instance;
1173
1223
 
1174
- // Resolve Dockerfile source: use runtime-specific path when instance defines a runtime.
1175
- const dockerfileSourcePath = _runtime ? `src/runtime/${_runtime}/Dockerfile` : `${rootPath}/Dockerfile`;
1176
- if (fs.existsSync(dockerfileSourcePath)) {
1224
+ // Resolve Dockerfile source. Dev/prod variant rules:
1225
+ // - When the instance defines a `runtime`, look under
1226
+ // `src/runtime/<runtime>/`. In `--dev` mode prefer `Dockerfile.dev`
1227
+ // when it exists, falling back to `Dockerfile`.
1228
+ // - When `runtime` is not set, look in the project root with the
1229
+ // same `.dev` → no-suffix precedence.
1230
+ // Dockerfile.dev is a full Dockerfile (not an overlay) — each runtime
1231
+ // owns the contract between its dev image and its prod image (debug
1232
+ // build flags, extra tooling, default ports, etc.).
1233
+ const dockerfileBase = _runtime ? `src/runtime/${_runtime}` : rootPath;
1234
+ const dockerfileCandidates = options.dev
1235
+ ? [`${dockerfileBase}/Dockerfile.dev`, `${dockerfileBase}/Dockerfile`]
1236
+ : [`${dockerfileBase}/Dockerfile`];
1237
+ const dockerfileSourcePath = dockerfileCandidates.find((p) => fs.existsSync(p));
1238
+ if (dockerfileSourcePath) {
1239
+ if (options.dev && !dockerfileSourcePath.endsWith('.dev')) {
1240
+ logger.warn(
1241
+ `[instance-build-manifest] --dev requested but no Dockerfile.dev present; falling back to ${dockerfileSourcePath}`,
1242
+ );
1243
+ }
1177
1244
  fs.copyFileSync(dockerfileSourcePath, dockerfileManifestPath);
1178
1245
  } else {
1179
- logger.warn(`[instance-build-manifest] Dockerfile not found at ${dockerfileSourcePath}`);
1246
+ logger.warn(`[instance-build-manifest] Dockerfile not found; tried: ${dockerfileCandidates.join(', ')}`);
1180
1247
  }
1181
1248
 
1182
1249
  const _deployId = `${deployId}-${_id}`;
@@ -1221,6 +1288,17 @@ EOF
1221
1288
  ),
1222
1289
  );
1223
1290
 
1291
+ // Env-aware lifecycle / probe selection. Each block may either be
1292
+ // a single object (shared across envs) or `{ development, production }`.
1293
+ const pickEnv = (v) => (v && (v.development || v.production) ? v[env] : v);
1294
+
1295
+ // Convention: an instance config may place `imagePullPolicy` inside
1296
+ // the env-scoped lifecycle block (alongside postStart/preStop).
1297
+ // Extract it onto the container spec and strip it from the lifecycle hash.
1298
+ const { lifecycle: lifecycleForManifest, imagePullPolicy: lifecycleImagePullPolicy } =
1299
+ Underpost.deploy.extractInstanceImagePullPolicy(pickEnv(_lifecycle));
1300
+ const instanceImagePullPolicy = options.imagePullPolicy || lifecycleImagePullPolicy;
1301
+
1224
1302
  const deploymentYaml =
1225
1303
  `---\n` +
1226
1304
  Underpost.deploy
@@ -1234,6 +1312,11 @@ EOF
1234
1312
  namespace: options.namespace,
1235
1313
  volumes: _volumes,
1236
1314
  cmd: resolvedCmd,
1315
+ lifecycle: lifecycleForManifest,
1316
+ readinessProbe: pickEnv(_readinessProbe),
1317
+ livenessProbe: pickEnv(_livenessProbe),
1318
+ containerPort: _toPort,
1319
+ imagePullPolicy: instanceImagePullPolicy,
1237
1320
  })
1238
1321
  .replace('{{ports}}', buildKindPorts(_fromPort, _toPort));
1239
1322
 
@@ -1288,6 +1371,44 @@ EOF
1288
1371
  shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh${options.dev ? ' --install-dev' : ``}`);
1289
1372
  },
1290
1373
 
1374
+ /**
1375
+ * @method install-crio
1376
+ * @description Installs and configures CRI-O as the container runtime for kubeadm clusters.
1377
+ * Adds the stable v1.33 CRI-O yum repository, installs the `cri-o` package, configures
1378
+ * the systemd cgroup driver, enables the `crio` service, and writes `/etc/crictl.yaml`
1379
+ * so that `crictl` targets the CRI-O socket by default.
1380
+ * @param {string} path - Unused.
1381
+ * @param {Object} options - The default underpost runner options for customizing workflow.
1382
+ * @memberof UnderpostRun
1383
+ */
1384
+ 'install-crio': (path, options = DEFAULT_OPTION) => {
1385
+ logger.info('Installing CRI-O...');
1386
+ shellExec(`cat <<EOF | sudo tee /etc/yum.repos.d/cri-o.repo
1387
+ [cri-o]
1388
+ name=CRI-O
1389
+ baseurl=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/v1.33/rpm/
1390
+ enabled=1
1391
+ gpgcheck=1
1392
+ gpgkey=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/v1.33/rpm/repodata/repomd.xml.key
1393
+ EOF`);
1394
+ shellExec(`sudo dnf -y install cri-o`);
1395
+ // crictl is in the kubernetes repo but excluded by default — install it explicitly
1396
+ shellExec(`sudo yum install -y cri-tools --disableexcludes=kubernetes`);
1397
+ // Ensure CRI-O uses systemd cgroup driver (matches kubelet default)
1398
+ shellExec(`sudo sed -i 's/^#\?cgroup_manager =.*/cgroup_manager = "systemd"/' /etc/crio/crio.conf`, {
1399
+ silentOnError: true,
1400
+ });
1401
+ shellExec(`sudo systemctl enable --now crio`);
1402
+ logger.info('CRI-O installed and started.');
1403
+ // Write crictl config so all crictl calls default to the CRI-O socket
1404
+ shellExec(`cat <<EOF | sudo tee /etc/crictl.yaml
1405
+ runtime-endpoint: unix:///var/run/crio/crio.sock
1406
+ image-endpoint: unix:///var/run/crio/crio.sock
1407
+ timeout: 10
1408
+ debug: false
1409
+ EOF`);
1410
+ },
1411
+
1291
1412
  /**
1292
1413
  * @method dd-container
1293
1414
  * @description Deploys a development or debug container tasks jobs, setting up necessary volumes and images, and running specified commands within the container.
@@ -1430,6 +1551,9 @@ EOF
1430
1551
  /**
1431
1552
  * @method promote
1432
1553
  * @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.router` for 'dd', or a specific ID).
1554
+ * When `--tls` is set, rebuilds the proxy manifest with `--cert` so the HTTPProxy includes
1555
+ * TLS config, deletes stale Certificate resources, then reapplies the proxy and secret.yaml
1556
+ * (cert-manager Certificate resources) for each affected deployment.
1433
1557
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string: `deployId,env,replicas`).
1434
1558
  * @param {Object} options - The default underpost runner options for customizing workflow
1435
1559
  * @memberof UnderpostRun
@@ -1438,11 +1562,34 @@ EOF
1438
1562
  let [inputDeployId, inputEnv, inputReplicas] = path.split(',');
1439
1563
  if (!inputEnv) inputEnv = 'production';
1440
1564
  if (!inputReplicas) inputReplicas = 1;
1565
+ // TODO: normalize: --tls maps to --cert for deploy.js isValidTLSContext compatibility
1566
+ if (options.tls) options.cert = true;
1567
+
1568
+ const applyCerts = (deployId, targetTraffic) => {
1569
+ if (!options.tls) return;
1570
+ // Rebuild proxy.yaml with --cert so the HTTPProxy includes TLS virtualhost config
1571
+ shellExec(
1572
+ `node bin deploy --build-manifest --cert --traffic ${targetTraffic} --replicas ${inputReplicas} --namespace ${options.namespace} ${deployId} ${inputEnv}`,
1573
+ );
1574
+ // Delete stale Certificate resources before reapplying
1575
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1576
+ if (fs.existsSync(confServerPath)) {
1577
+ for (const host of Object.keys(JSON.parse(fs.readFileSync(confServerPath, 'utf8'))))
1578
+ shellExec(`sudo kubectl delete Certificate ${host} -n ${options.namespace} --ignore-not-found`);
1579
+ }
1580
+ shellExec(
1581
+ `sudo kubectl apply -f ./engine-private/conf/${deployId}/build/${inputEnv}/proxy.yaml -n ${options.namespace}`,
1582
+ );
1583
+ const secretPath = `./engine-private/conf/${deployId}/build/${inputEnv}/secret.yaml`;
1584
+ if (fs.existsSync(secretPath)) shellExec(`kubectl apply -f ${secretPath} -n ${options.namespace}`);
1585
+ };
1586
+
1441
1587
  if (inputDeployId === 'dd') {
1442
1588
  for (const deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
1443
1589
  const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
1444
1590
  const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
1445
1591
  Underpost.deploy.switchTraffic(deployId, inputEnv, targetTraffic, inputReplicas, options.namespace, options);
1592
+ applyCerts(deployId, targetTraffic);
1446
1593
  }
1447
1594
  } else {
1448
1595
  const currentTraffic = Underpost.deploy.getCurrentTraffic(inputDeployId, { namespace: options.namespace });
@@ -1455,6 +1602,7 @@ EOF
1455
1602
  options.namespace,
1456
1603
  options,
1457
1604
  );
1605
+ applyCerts(inputDeployId, targetTraffic);
1458
1606
  }
1459
1607
  },
1460
1608
  /**
@@ -2097,15 +2245,23 @@ EOF
2097
2245
  * @memberof UnderpostRun
2098
2246
  */
2099
2247
  kill: (path = '', options = DEFAULT_OPTION) => {
2100
- if (options.pid) return shellExec(`sudo kill -9 ${options.pid}`);
2248
+ if (options.pid)
2249
+ return shellExec(`sudo kill -9 ${options.pid}`, {
2250
+ silentOnError: true,
2251
+ });
2101
2252
  for (const _path of path.split(',')) {
2102
2253
  if (_path.split('+')[1]) {
2103
2254
  let [port, sumPortOffSet] = _path.split('+');
2104
2255
  port = parseInt(port);
2105
2256
  sumPortOffSet = parseInt(sumPortOffSet);
2106
2257
  for (const sumPort of range(0, sumPortOffSet))
2107
- shellExec(`sudo kill -9 $(lsof -t -i:${parseInt(port) + parseInt(sumPort)})`);
2108
- } else shellExec(`sudo kill -9 $(lsof -t -i:${_path})`);
2258
+ shellExec(`sudo kill -9 $(lsof -t -i:${parseInt(port) + parseInt(sumPort)})`, {
2259
+ silentOnError: true,
2260
+ });
2261
+ } else
2262
+ shellExec(`sudo kill -9 $(lsof -t -i:${_path})`, {
2263
+ silentOnError: true,
2264
+ });
2109
2265
  }
2110
2266
  },
2111
2267
  /**
@@ -2371,9 +2527,11 @@ EOF`;
2371
2527
  /**
2372
2528
  * @method pull-bundle
2373
2529
  * @description Downloads split zip parts from file storage, merges and extracts them, and moves the result into the public directory.
2374
- * Steps: set cron env, download parts (omit-unzip), merge zip, unzip, remove zip, move to public/<host>.
2375
- * @param {string} path - Optional host name(s) used to locate zip(s) and as public destination(s) (e.g. 'underpost.net' or 'a.com,b.com').
2376
- * If omitted, hosts are loaded from `engine-private/conf/<deployId>/conf.server.json`.
2530
+ * Steps: set env, download parts (omit-unzip), merge zip, unzip, remove zip + parts, move to public/<host>[/path].
2531
+ * Iterates over every non-singleReplica, non-redirect, non-disabledRebuild route in conf.server.json
2532
+ * so that multi-path deployments are handled correctly.
2533
+ * @param {string} path - Optional comma-separated host name(s) to restrict processing (e.g. 'underpost.net' or 'a.com,b.com').
2534
+ * If omitted, all hosts from `engine-private/conf/<deployId>/conf.server.json` are used.
2377
2535
  * @param {Object} options - The default underpost runner options for customizing workflow.
2378
2536
  * @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
2379
2537
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
@@ -2384,21 +2542,16 @@ EOF`;
2384
2542
  const env = options.dev ? 'development' : 'production';
2385
2543
  const deployId = options.deployId || 'dd-default';
2386
2544
  const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
2387
- const hosts = path
2545
+ const confServer = fs.existsSync(confServerPath) ? loadConfServerJson(confServerPath) : {};
2546
+ const hostsArg = path
2388
2547
  ? path
2389
2548
  .split(',')
2390
2549
  .map((h) => h.trim())
2391
2550
  .filter(Boolean)
2392
- : fs.existsSync(confServerPath)
2393
- ? Object.keys(loadConfServerJson(confServerPath))
2394
- : [];
2551
+ : Object.keys(confServer);
2395
2552
 
2396
- if (hosts.length === 0) {
2397
- logger.error('pull-bundle: no hosts resolved', {
2398
- deployId,
2399
- path,
2400
- confServerPath,
2401
- });
2553
+ if (hostsArg.length === 0) {
2554
+ logger.error('pull-bundle: no hosts resolved', { deployId, path, confServerPath });
2402
2555
  return;
2403
2556
  }
2404
2557
 
@@ -2407,30 +2560,118 @@ EOF`;
2407
2560
  shellExec(
2408
2561
  `${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
2409
2562
  );
2410
- for (const host of hosts) {
2411
- const zipPath = `build/${host}-.zip`;
2412
- const hasZip = fs.existsSync(zipPath);
2413
- const hasParts =
2414
- fs.existsSync('./build') &&
2415
- fs
2416
- .readdirSync('./build')
2417
- .some((name) => name.startsWith(`${host}-.zip.part`) || name.startsWith(`${host}-.zip-part`));
2418
-
2419
- if (!hasZip && !hasParts) {
2420
- logger.warn(`Bundle not found for host '${host}'. Skipping host.`, {
2421
- zipPath,
2422
- deployId,
2423
- });
2424
- continue;
2425
- }
2426
2563
 
2427
- if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
2428
- shellExec(`${baseCommand} client --unzip ${zipPath}`);
2429
- shellExec(`sudo rm -rf ${zipPath}`);
2430
- if (fs.existsSync(`public/${host}`)) shellExec(`sudo rm -rf public/${host}`);
2431
- shellExec(`sudo mv build/${host} public/${host}`);
2564
+ for (const host of hostsArg) {
2565
+ // Gather all routes for this host; fall back to root '/' when host is not in confServer
2566
+ // (e.g. when hosts were provided explicitly via the path argument).
2567
+ const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
2568
+
2569
+ for (const routePath of routePaths) {
2570
+ const routeConf = confServer[host] ? confServer[host][routePath] || {} : {};
2571
+ // Skip routes that are not built by buildClient (mirrors buildClient skip conditions)
2572
+ if (routeConf.singleReplica || routeConf.redirect || routeConf.disabledRebuild) continue;
2573
+
2574
+ // buildClient names the zip as "<host>-<path-no-slashes>.zip"
2575
+ // e.g. host="underpost.net", path="/" → buildId="underpost.net-", zip="build/underpost.net-.zip"
2576
+ // e.g. host="app.net", path="/admin" → buildId="app.net-admin", zip="build/app.net-admin.zip"
2577
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2578
+ const zipPath = `build/${buildId}.zip`;
2579
+ const buildDir = './build';
2580
+ const hasZip = fs.existsSync(zipPath);
2581
+ const hasParts =
2582
+ fs.existsSync(buildDir) &&
2583
+ fs
2584
+ .readdirSync(buildDir)
2585
+ .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
2586
+
2587
+ if (!hasZip && !hasParts) {
2588
+ logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
2589
+ continue;
2590
+ }
2591
+
2592
+ if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
2593
+ shellExec(`${baseCommand} client --unzip ${zipPath}`);
2594
+ shellExec(`sudo rm -rf ${zipPath}`);
2595
+
2596
+ // Clean up downloaded part wrapper zips left by --omit-unzip pull
2597
+ if (fs.existsSync(buildDir)) {
2598
+ fs.readdirSync(buildDir)
2599
+ .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
2600
+ .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
2601
+ }
2602
+
2603
+ // unzipClientBuild extracts to buildId with trailing '-' stripped
2604
+ // e.g. "build/underpost.net-" → "build/underpost.net"
2605
+ // e.g. "build/app.net-admin" → "build/app.net-admin" (no trailing dash, no change)
2606
+ const extractedDir = `build/${buildId.replace(/-$/, '')}`;
2607
+ if (!fs.existsSync(extractedDir)) {
2608
+ logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
2609
+ continue;
2610
+ }
2611
+
2612
+ // Destination mirrors the public directory layout used by the server
2613
+ const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
2614
+ if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
2615
+ // Ensure parent directory exists for sub-paths
2616
+ if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
2617
+ shellExec(`sudo mv ${extractedDir} ${publicDestPath}`);
2618
+ }
2432
2619
  }
2433
2620
  },
2621
+
2622
+ /**
2623
+ * @method setup-shared-dir
2624
+ * @description Run once for initial shared-directory setup. Creates the group, adds the user,
2625
+ * creates the directory, sets ownership, applies the SGID bit, and configures default ACLs so
2626
+ * all future files inside the directory automatically inherit group write permissions.
2627
+ * Use `reload-shared-dir` for subsequent permission repairs without recreating the group.
2628
+ * @param {string} path - Target directory to set up (defaults to `/home/dd/engine`).
2629
+ * Customise via the `path` argument or leave empty to use the default.
2630
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2631
+ * Key fields: `options.user` (default `'admin'`), `options.group` (default `'engine-dev'`).
2632
+ * @memberof UnderpostRun
2633
+ */
2634
+ 'setup-shared-dir': (path = '/home/dd/engine', options = DEFAULT_OPTION) => {
2635
+ const dir = path || '/home/dd/engine';
2636
+ const user = options.user || 'admin';
2637
+ const group = options.group || 'engine-dev';
2638
+
2639
+ logger.info(`[setup-shared-dir] dir=${dir} user=${user} group=${group}`);
2640
+
2641
+ shellExec(`sudo groupadd ${group} 2>/dev/null || true`);
2642
+ shellExec(`sudo usermod -aG ${group} ${user}`);
2643
+ shellExec(`sudo mkdir -p ${dir}`);
2644
+ shellExec(`sudo chown -R ${user}:${group} ${dir}`);
2645
+ shellExec(`sudo chmod -R 2775 ${dir}`);
2646
+ shellExec(`sudo setfacl -d -m g:${group}:rwx ${dir}`);
2647
+ shellExec(`sudo setfacl -m g:${group}:rwx ${dir}`);
2648
+
2649
+ logger.info(`[setup-shared-dir] Shared directory setup complete: ${dir}`);
2650
+ },
2651
+
2652
+ /**
2653
+ * @method reload-shared-dir
2654
+ * @description Re-applies recursive permissions and ACLs to repair permission drift on an
2655
+ * already-configured shared directory. Does **not** recreate the group, add users, or modify
2656
+ * ownership. Use this after VS Code permission errors or when existing files lose group write
2657
+ * access due to tool or process interference.
2658
+ * @param {string} path - Target directory to repair (defaults to `/home/dd/engine`).
2659
+ * Customise via the `path` argument or leave empty to use the default.
2660
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2661
+ * Key fields: `options.group` (default `'engine-dev'`).
2662
+ * @memberof UnderpostRun
2663
+ */
2664
+ 'reload-shared-dir': (path = '/home/dd/engine', options = DEFAULT_OPTION) => {
2665
+ const dir = path || '/home/dd/engine';
2666
+ const group = options.group || 'engine-dev';
2667
+
2668
+ logger.info(`[reload-shared-dir] dir=${dir} group=${group}`);
2669
+
2670
+ shellExec(`sudo chmod -R 2775 ${dir}`);
2671
+ shellExec(`sudo setfacl -R -m g:${group}:rwx ${dir}`);
2672
+
2673
+ logger.info(`[reload-shared-dir] Shared directory permissions reloaded: ${dir}`);
2674
+ },
2434
2675
  };
2435
2676
 
2436
2677
  static API = {
package/src/cli/test.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * @namespace UnderpostTest
5
5
  */
6
6
 
7
+ import fs from 'fs-extra';
7
8
  import { timer } from '../client/components/core/CommonJs.js';
8
9
  import { MariaDB } from '../db/mariadb/MariaDB.js';
9
10
  import { getNpmRootPath } from '../server/conf.js';
@@ -42,7 +43,13 @@ class UnderpostTest {
42
43
  */
43
44
  run() {
44
45
  actionInitLog();
45
- shellExec(`cd ${getNpmRootPath()}/underpost && npm run test`);
46
+ const underpostTestPath = `${getNpmRootPath()}/underpost`;
47
+ if (fs.existsSync(underpostTestPath)) {
48
+ shellExec(`cd ${underpostTestPath} && npm run test`);
49
+ } else {
50
+ logger.warn(`Global underpost not found at ${underpostTestPath}, running local npm test instead`);
51
+ shellExec('npm test');
52
+ }
46
53
  },
47
54
  /**
48
55
  * @method callback
@@ -148,8 +155,7 @@ class UnderpostTest {
148
155
  const pods = Underpost.kubectl.get(podName, kindType);
149
156
  let result = pods.find((p) => p.STATUS === status || (status === 'Running' && p.STATUS === 'Completed'));
150
157
  logger.info(
151
- `Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${
152
- index + 1
158
+ `Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${index + 1
153
159
  }/${maxAttempts}`,
154
160
  pods[0] ? pods[0].STATUS : 'Not found kind object',
155
161
  );
@@ -19,6 +19,12 @@ const DefaultTemplate = async () => {
19
19
  });
20
20
  });
21
21
  return html`
22
+ <style>
23
+ .feature-icon {
24
+ font-size: 2.5rem;
25
+ margin-bottom: 1rem;
26
+ }
27
+ </style>
22
28
  <div class="landing-container">
23
29
  <div class="content-wrapper">
24
30
  <h1 class="animated-text">
@@ -27,17 +33,17 @@ const DefaultTemplate = async () => {
27
33
  </h1>
28
34
  <div class="features">
29
35
  <div class="feature-card">
30
- <i class="icon">🚀</i>
36
+ <i class="fas fa-rocket feature-icon"></i>
31
37
  <h3>Fast &amp; Reliable</h3>
32
38
  <p>Lightning-fast performance with 99.9% uptime</p>
33
39
  </div>
34
40
  <div class="feature-card">
35
- <i class="icon">🎨</i>
41
+ <i class="fas fa-palette feature-icon"></i>
36
42
  <h3>Beautiful UI</h3>
37
43
  <p>Modern and intuitive user interface</p>
38
44
  </div>
39
45
  <div class="feature-card">
40
- <i class="icon">⚡</i>
46
+ <i class="fas fa-bolt feature-icon"></i>
41
47
  <h3>Powerful Features</h3>
42
48
  <p>Everything you need in one place</p>
43
49
  </div>