underpost 3.2.5 → 3.2.9

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 (144) hide show
  1. package/.github/workflows/release.cd.yml +1 -2
  2. package/CHANGELOG.md +351 -1
  3. package/CLI-HELP.md +40 -13
  4. package/Dockerfile +0 -4
  5. package/README.md +4 -4
  6. package/bin/build.js +14 -5
  7. package/bin/deploy.js +570 -1
  8. package/bin/file.js +6 -0
  9. package/conf.js +11 -2
  10. package/jsconfig.json +1 -1
  11. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  12. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
  13. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  14. package/manifests/deployment/dd-test-development/deployment.yaml +136 -66
  15. package/manifests/deployment/dd-test-development/proxy.yaml +41 -5
  16. package/package.json +24 -15
  17. package/scripts/k3s-node-setup.sh +2 -2
  18. package/scripts/nat-iptables.sh +103 -18
  19. package/src/api/core/core.controller.js +10 -10
  20. package/src/api/core/core.service.js +10 -10
  21. package/src/api/default/default.controller.js +10 -10
  22. package/src/api/default/default.service.js +10 -10
  23. package/src/api/document/document.controller.js +12 -12
  24. package/src/api/document/document.model.js +10 -16
  25. package/src/api/file/file.controller.js +8 -8
  26. package/src/api/file/file.model.js +10 -10
  27. package/src/api/file/file.service.js +36 -36
  28. package/src/api/test/test.controller.js +8 -8
  29. package/src/api/test/test.service.js +8 -8
  30. package/src/api/user/guest.service.js +99 -0
  31. package/src/api/user/user.controller.js +6 -6
  32. package/src/api/user/user.model.js +8 -13
  33. package/src/api/user/user.service.js +3 -20
  34. package/src/cli/cluster.js +61 -14
  35. package/src/cli/db.js +47 -2
  36. package/src/cli/deploy.js +67 -35
  37. package/src/cli/fs.js +79 -8
  38. package/src/cli/image.js +43 -1
  39. package/src/cli/index.js +26 -1
  40. package/src/cli/release.js +57 -1
  41. package/src/cli/repository.js +69 -31
  42. package/src/cli/run.js +415 -36
  43. package/src/cli/ssh.js +1 -1
  44. package/src/cli/static.js +43 -115
  45. package/src/client/Default.index.js +21 -33
  46. package/src/client/components/core/404.js +4 -4
  47. package/src/client/components/core/500.js +4 -4
  48. package/src/client/components/core/Account.js +73 -60
  49. package/src/client/components/core/AgGrid.js +23 -33
  50. package/src/client/components/core/Alert.js +12 -13
  51. package/src/client/components/core/AppStore.js +1 -1
  52. package/src/client/components/core/Auth.js +35 -37
  53. package/src/client/components/core/Badge.js +7 -13
  54. package/src/client/components/core/BtnIcon.js +15 -17
  55. package/src/client/components/core/CalendarCore.js +42 -63
  56. package/src/client/components/core/Chat.js +13 -15
  57. package/src/client/components/core/ClientEvents.js +87 -0
  58. package/src/client/components/core/ColorPaletteElement.js +309 -0
  59. package/src/client/components/core/Content.js +17 -14
  60. package/src/client/components/core/Css.js +15 -71
  61. package/src/client/components/core/CssCore.js +12 -16
  62. package/src/client/components/core/D3Chart.js +4 -4
  63. package/src/client/components/core/Docs.js +64 -91
  64. package/src/client/components/core/DropDown.js +69 -91
  65. package/src/client/components/core/EventBus.js +92 -0
  66. package/src/client/components/core/EventsUI.js +14 -17
  67. package/src/client/components/core/FileExplorer.js +96 -228
  68. package/src/client/components/core/FullScreen.js +47 -75
  69. package/src/client/components/core/Input.js +24 -69
  70. package/src/client/components/core/Keyboard.js +25 -18
  71. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  72. package/src/client/components/core/LoadingAnimation.js +25 -31
  73. package/src/client/components/core/LogIn.js +41 -41
  74. package/src/client/components/core/LogOut.js +23 -14
  75. package/src/client/components/core/Modal.js +462 -178
  76. package/src/client/components/core/NotificationManager.js +14 -18
  77. package/src/client/components/core/Panel.js +54 -50
  78. package/src/client/components/core/PanelForm.js +25 -125
  79. package/src/client/components/core/Polyhedron.js +110 -214
  80. package/src/client/components/core/PublicProfile.js +39 -32
  81. package/src/client/components/core/Recover.js +48 -44
  82. package/src/client/components/core/Responsive.js +88 -32
  83. package/src/client/components/core/RichText.js +9 -18
  84. package/src/client/components/core/Router.js +24 -3
  85. package/src/client/components/core/SearchBox.js +37 -37
  86. package/src/client/components/core/SignUp.js +39 -30
  87. package/src/client/components/core/SocketIo.js +31 -2
  88. package/src/client/components/core/SocketIoHandler.js +6 -6
  89. package/src/client/components/core/ToggleSwitch.js +8 -20
  90. package/src/client/components/core/ToolTip.js +5 -17
  91. package/src/client/components/core/Translate.js +56 -59
  92. package/src/client/components/core/Validator.js +26 -16
  93. package/src/client/components/core/Wallet.js +15 -26
  94. package/src/client/components/core/Worker.js +163 -27
  95. package/src/client/components/core/windowGetDimensions.js +7 -7
  96. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +87 -87
  97. package/src/client/components/default/CssDefault.js +12 -12
  98. package/src/client/components/default/LogInDefault.js +6 -4
  99. package/src/client/components/default/LogOutDefault.js +6 -4
  100. package/src/client/components/default/RouterDefault.js +47 -0
  101. package/src/client/components/default/SettingsDefault.js +4 -4
  102. package/src/client/components/default/SignUpDefault.js +6 -4
  103. package/src/client/components/default/TranslateDefault.js +3 -3
  104. package/src/client/services/core/core.service.js +17 -49
  105. package/src/client/services/default/default.management.js +159 -267
  106. package/src/client/services/default/default.service.js +10 -16
  107. package/src/client/services/document/document.service.js +14 -19
  108. package/src/client/services/file/file.service.js +8 -13
  109. package/src/client/services/test/test.service.js +8 -13
  110. package/src/client/services/user/guest.service.js +86 -0
  111. package/src/client/services/user/user.management.js +5 -5
  112. package/src/client/services/user/user.service.js +14 -20
  113. package/src/client/ssr/body/404.js +3 -3
  114. package/src/client/ssr/body/500.js +3 -3
  115. package/src/client/ssr/body/CacheControl.js +5 -2
  116. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  117. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  118. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  119. package/src/client/ssr/offline/Maintenance.js +12 -11
  120. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  121. package/src/client/ssr/pages/Test.js +2 -2
  122. package/src/client/sw/core.sw.js +212 -0
  123. package/src/index.js +1 -1
  124. package/src/runtime/express/Dockerfile +4 -4
  125. package/src/runtime/lampp/Dockerfile +8 -7
  126. package/src/runtime/wp/Dockerfile +11 -17
  127. package/src/server/client-build-docs.js +45 -46
  128. package/src/server/client-build.js +334 -60
  129. package/src/server/client-formatted.js +47 -16
  130. package/src/server/conf.js +5 -4
  131. package/src/server/data-query.js +32 -20
  132. package/src/server/dns.js +22 -0
  133. package/src/server/ipfs-client.js +232 -91
  134. package/src/server/process.js +13 -27
  135. package/src/server/start.js +17 -3
  136. package/src/server/valkey.js +141 -235
  137. package/tsconfig.docs.json +15 -0
  138. package/typedoc.json +29 -0
  139. package/jsdoc.json +0 -52
  140. package/src/client/components/core/ColorPalette.js +0 -5267
  141. package/src/client/components/core/JoyStick.js +0 -80
  142. package/src/client/components/default/RoutesDefault.js +0 -49
  143. package/src/client/sw/default.sw.js +0 -127
  144. package/src/client/sw/template.sw.js +0 -84
package/src/cli/run.js CHANGED
@@ -93,7 +93,6 @@ const logger = loggerFactory(import.meta);
93
93
  * @property {boolean} kubeadm - Whether to run in kubeadm mode.
94
94
  * @property {boolean} kind - Whether to run in kind mode.
95
95
  * @property {boolean} k3s - Whether to run in k3s mode.
96
- * @property {string} logType - The type of log to generate.
97
96
  * @property {string} hosts - The hosts to use.
98
97
  * @property {string} deployId - The deployment ID.
99
98
  * @property {string} instanceId - The instance ID.
@@ -111,6 +110,10 @@ const logger = loggerFactory(import.meta);
111
110
  * @property {string|Array<{ip: string, hostnames: string[]}>} hostAliases - Adds entries to the Pod /etc/hosts via Kubernetes hostAliases.
112
111
  * 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").
113
112
  * As an array (programmatic): objects with `ip` and `hostnames` fields (e.g., [{ ip: "127.0.0.1", hostnames: ["foo.local"] }]).
113
+ * @property {boolean} gitClean - Whether to perform a `git clean` before running.
114
+ * @property {boolean} copy - Whether to copy the command to the clipboard instead of executing it.
115
+ * @property {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment (supported by: sync, template-deploy).
116
+ * @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).
114
117
  * @memberof UnderpostRun
115
118
  */
116
119
  const DEFAULT_OPTION = {
@@ -158,7 +161,6 @@ const DEFAULT_OPTION = {
158
161
  kubeadm: false,
159
162
  kind: false,
160
163
  k3s: false,
161
- logType: '',
162
164
  hosts: '',
163
165
  deployId: '',
164
166
  instanceId: '',
@@ -176,6 +178,8 @@ const DEFAULT_OPTION = {
176
178
  hostAliases: '',
177
179
  gitClean: false,
178
180
  copy: false,
181
+ skipFullBuild: false,
182
+ pullBundle: false,
179
183
  };
180
184
 
181
185
  /**
@@ -437,6 +441,15 @@ class UnderpostRun {
437
441
  deployType = 'init';
438
442
  }
439
443
 
444
+ // If --build is set and path is a sync-engine-* target, push the pre-built client bundle
445
+ // to Cloudinary so the remote container can pull it instead of rebuilding from source.
446
+ if (options.build && deployConfId && deployConfId.startsWith('engine-')) {
447
+ const confName = deployConfId.replace(/^engine-/, '');
448
+ const pushDeployId = options.deployId || `dd-${confName}`;
449
+ logger.info(`[template-deploy] Running push-bundle for deployId: ${pushDeployId}`);
450
+ shellExec(`${baseCommand} run push-bundle --deploy-id ${pushDeployId}`);
451
+ }
452
+
440
453
  // Dispatch npmpkg CI workflow — this builds pwa-microservices-template first.
441
454
  // If deployConfId is set, npmpkg.ci.yml will dispatch the engine-<conf-id> CI
442
455
  // with sync=true after template build completes. The engine CI then dispatches
@@ -490,21 +503,6 @@ class UnderpostRun {
490
503
  : await Underpost.release.pwa(sanitizedMessage, options);
491
504
  pbcopy(triggerCmd + ' && cd /home/dd/engine');
492
505
  },
493
- /**
494
- * @method template-deploy-image
495
- * @description Dispatches the Docker image CI workflow for the `engine` repository.
496
- * @param {string} path - The input value, identifier, or path for the operation.
497
- * @param {Object} options - The default underpost runner options for customizing workflow
498
- * @memberof UnderpostRun
499
- */
500
- 'template-deploy-image': (path, options = DEFAULT_OPTION) => {
501
- Underpost.repo.dispatchWorkflow({
502
- repo: `${process.env.GITHUB_USERNAME}/engine`,
503
- workflowFile: 'docker-image.ci.yml',
504
- ref: 'master',
505
- inputs: {},
506
- });
507
- },
508
506
  /**
509
507
  * @method docker-image
510
508
  * @description Dispatches the Docker image CI workflow (`docker-image.ci.yml`) for the `engine` repository via `workflow_dispatch`.
@@ -515,7 +513,7 @@ class UnderpostRun {
515
513
  'docker-image': (path, options = DEFAULT_OPTION) => {
516
514
  Underpost.repo.dispatchWorkflow({
517
515
  repo: `${process.env.GITHUB_USERNAME}/engine`,
518
- workflowFile: 'docker-image.ci.yml',
516
+ workflowFile: `docker-image${path ? `.${path}` : ''}.ci.yml`,
519
517
  ref: 'master',
520
518
  inputs: {},
521
519
  });
@@ -652,7 +650,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
652
650
  sync: async (path, options = DEFAULT_OPTION) => {
653
651
  // Dev usage: node bin run --dev --build sync dd-default
654
652
  const env = options.dev ? 'development' : 'production';
655
- const baseCommand = options.dev ? 'node bin' : 'underpost';
653
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
656
654
  const baseClusterCommand = options.dev ? ' --dev' : '';
657
655
  const clusterFlag = options.k3s ? ' --k3s' : options.kind ? ' --kind' : ' --kubeadm';
658
656
  const defaultPath = [
@@ -669,6 +667,15 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
669
667
  image = image ? image : defaultPath[3];
670
668
  node = node ? node : defaultPath[4];
671
669
  shellExec(`${baseCommand} cluster --ns-use ${options.namespace}`);
670
+
671
+ if (image && !image.startsWith('localhost'))
672
+ Underpost.image.pullDockerHubImage({
673
+ dockerhubImage: image,
674
+ kind: options.kind || (!options.nodeName && !options.kubeadm && !options.k3s),
675
+ kubeadm: options.nodeName || options.kubeadm,
676
+ k3s: options.k3s,
677
+ });
678
+
672
679
  if (isDeployRunnerContext(path, options)) {
673
680
  if (!options.disablePrivateConfUpdate) {
674
681
  const { validVersion } = Underpost.repo.privateConfUpdate(deployId);
@@ -691,12 +698,15 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
691
698
  : '';
692
699
  const gitCleanFlag = options.gitClean ? ' --git-clean' : '';
693
700
 
701
+ const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
702
+ const pullBundleFlag = options.pullBundle ? ' --pull-bundle' : '';
703
+
694
704
  shellExec(
695
705
  `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
696
706
  image ? ` --image ${image}` : ''
697
707
  }${versions ? ` --versions ${versions}` : ''}${
698
708
  options.namespace ? ` --namespace ${options.namespace}` : ''
699
- }${timeoutFlags}${cmdString}${gitCleanFlag} ${deployId} ${env}`,
709
+ }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag} ${deployId} ${env}`,
700
710
  );
701
711
 
702
712
  if (isDeployRunnerContext(path, options)) {
@@ -928,12 +938,17 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
928
938
  image: _image,
929
939
  fromPort: _fromPort,
930
940
  toPort: _toPort,
941
+ fromDebugPort: _fromDebugPort,
942
+ toDebugPort: _toDebugPort,
931
943
  cmd: _cmd,
932
944
  volumes: _volumes,
933
945
  metadata: _metadata,
934
946
  } = instance;
935
947
  if (id !== _id) continue;
936
948
  const _deployId = `${deployId}-${_id}`;
949
+ // Use debug ports in development when defined, fall back to production ports.
950
+ if (env === 'development' && _fromDebugPort) _fromPort = _fromDebugPort;
951
+ if (env === 'development' && _toDebugPort) _toPort = _toDebugPort;
937
952
  const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
938
953
  hostTest: _host,
939
954
  namespace: options.namespace,
@@ -1003,12 +1018,17 @@ EOF
1003
1018
  image: _image,
1004
1019
  fromPort: _fromPort,
1005
1020
  toPort: _toPort,
1021
+ fromDebugPort: _fromDebugPort,
1022
+ toDebugPort: _toDebugPort,
1006
1023
  cmd: _cmd,
1007
1024
  volumes: _volumes,
1008
1025
  metadata: _metadata,
1009
1026
  } = instance;
1010
1027
  if (id !== _id) continue;
1011
1028
  const _deployId = `${deployId}-${_id}`;
1029
+ // Use debug ports in development when defined, fall back to production ports.
1030
+ if (env === 'development' && _fromDebugPort) _fromPort = _fromDebugPort;
1031
+ if (env === 'development' && _toDebugPort) _toPort = _toDebugPort;
1012
1032
  etcHosts.push(_host);
1013
1033
  if (options.expose) continue;
1014
1034
  // Examples images:
@@ -1016,12 +1036,13 @@ EOF
1016
1036
  // `localhost/rockylinux9-underpost:${Underpost.version}`
1017
1037
  if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
1018
1038
 
1019
- Underpost.image.pullDockerHubImage({
1020
- dockerhubImage: _image,
1021
- kind: options.kind || (!options.nodeName && !options.kubeadm && !options.k3s),
1022
- kubeadm: options.nodeName || options.kubeadm,
1023
- k3s: options.k3s,
1024
- });
1039
+ if (_image && !_image.startsWith('localhost'))
1040
+ Underpost.image.pullDockerHubImage({
1041
+ dockerhubImage: _image,
1042
+ kind: options.kind || (!options.nodeName && !options.kubeadm && !options.k3s),
1043
+ kubeadm: options.nodeName || options.kubeadm,
1044
+ k3s: options.k3s,
1045
+ });
1025
1046
 
1026
1047
  const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
1027
1048
  hostTest: _host,
@@ -1095,7 +1116,6 @@ EOF
1095
1116
  targetTraffic,
1096
1117
  ignorePods,
1097
1118
  options.namespace,
1098
- options.logType,
1099
1119
  );
1100
1120
 
1101
1121
  if (!ready) {
@@ -1115,6 +1135,153 @@ EOF
1115
1135
  }
1116
1136
  },
1117
1137
 
1138
+ /**
1139
+ * @method instance-build-manifest
1140
+ * @description Builds a Kubernetes Deployment + Service manifest for a specific instance entry
1141
+ * from `conf.instances.json` and writes it to a file.
1142
+ * Traffic colour is automatically chosen as the opposite of the current live colour (blue/green),
1143
+ * defaulting to `blue` when no deployment is running yet.
1144
+ *
1145
+ * If `--build` is supplied the image is built from the project Dockerfile and loaded into the
1146
+ * cluster before the manifest is written (kind by default; `--kubeadm` / `--k3s` override).
1147
+ *
1148
+ * @param {string} path - Comma-separated: `deployId,instanceId[,projectPath]`.
1149
+ * `projectPath` is the root directory that contains the `Dockerfile` (e.g. `./cyberia-client`).
1150
+ * Artifacts are written to `<projectPath>/manifests/<env>/Dockerfile` and
1151
+ * `<projectPath>/manifests/<env>/deployment.yaml`.
1152
+ * In production, files are also copied to `<projectPath>/Dockerfile` and
1153
+ * `<projectPath>/deployment.yaml`.
1154
+ * @param {Object} options - The default underpost runner options for customizing workflow
1155
+ * @memberof UnderpostRun
1156
+ */
1157
+ 'instance-build-manifest': (path, options = DEFAULT_OPTION) => {
1158
+ const env = options.dev ? 'development' : 'production';
1159
+ let [deployId, id, projectPath] = path.split(',');
1160
+ const rootPath = projectPath ? projectPath : '.';
1161
+ const envManifestPath = `${rootPath}/manifests/deployments/${id}-${env}`;
1162
+ const outputPath = `${envManifestPath}/deployment.yaml`;
1163
+ const dockerfileManifestPath = `${envManifestPath}/Dockerfile`;
1164
+
1165
+ fs.mkdirpSync(envManifestPath);
1166
+
1167
+ const confInstances = JSON.parse(
1168
+ fs.readFileSync(`./engine-private/conf/${deployId}/conf.instances.json`, 'utf8'),
1169
+ );
1170
+
1171
+ const instance = confInstances.find((i) => i.id === id);
1172
+ if (!instance) {
1173
+ logger.error(`Instance with id '${id}' not found in conf.instances.json for deployId '${deployId}'`);
1174
+ return;
1175
+ }
1176
+
1177
+ let {
1178
+ id: _id,
1179
+ host: _host,
1180
+ path: _path,
1181
+ image: _image,
1182
+ fromPort: _fromPort,
1183
+ toPort: _toPort,
1184
+ fromDebugPort: _fromDebugPort,
1185
+ toDebugPort: _toDebugPort,
1186
+ cmd: _cmd,
1187
+ volumes: _volumes,
1188
+ metadata: _metadata,
1189
+ runtime: _runtime,
1190
+ } = instance;
1191
+
1192
+ // Resolve Dockerfile source: use runtime-specific path when instance defines a runtime.
1193
+ const dockerfileSourcePath = _runtime ? `src/runtime/${_runtime}/Dockerfile` : `${rootPath}/Dockerfile`;
1194
+ if (fs.existsSync(dockerfileSourcePath)) {
1195
+ fs.copyFileSync(dockerfileSourcePath, dockerfileManifestPath);
1196
+ } else {
1197
+ logger.warn(`[instance-build-manifest] Dockerfile not found at ${dockerfileSourcePath}`);
1198
+ }
1199
+
1200
+ const _deployId = `${deployId}-${_id}`;
1201
+ if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
1202
+ // Use debug ports in development when defined, fall back to production ports.
1203
+ if (env === 'development' && _fromDebugPort) _fromPort = _fromDebugPort;
1204
+ if (env === 'development' && _toDebugPort) _toPort = _toDebugPort;
1205
+
1206
+ // Build image from projectPath Dockerfile and load into cluster when --build is set.
1207
+ if (options.build && projectPath) {
1208
+ const isKind = !options.kubeadm && !options.k3s;
1209
+ Underpost.image.build({
1210
+ path: projectPath,
1211
+ imageName: _image,
1212
+ podmanSave: true,
1213
+ imagePath: projectPath,
1214
+ kind: isKind,
1215
+ kubeadm: !!options.kubeadm,
1216
+ k3s: !!options.k3s,
1217
+ reset: !!options.reset,
1218
+ dev: options.dev,
1219
+ });
1220
+ logger.info(`[instance-build-manifest] Image built and loaded`, {
1221
+ image: _image,
1222
+ cluster: isKind ? 'kind' : options.kubeadm ? 'kubeadm' : 'k3s',
1223
+ });
1224
+ }
1225
+
1226
+ // Determine target traffic: opposite of current, or 'blue' if nothing is running yet.
1227
+ const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
1228
+ hostTest: _host,
1229
+ namespace: options.namespace,
1230
+ });
1231
+ const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
1232
+
1233
+ // Resolve {{grpc-service-dns}} using the parent deploy's current (or default) traffic.
1234
+ const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
1235
+ const resolvedCmd = _cmd[env].map((c) =>
1236
+ c.replaceAll(
1237
+ '{{grpc-service-dns}}',
1238
+ `${deployId}-grpc-service-${env}-${parentTraffic}.${options.namespace || 'default'}.svc.cluster.local:50051`,
1239
+ ),
1240
+ );
1241
+
1242
+ const deploymentYaml =
1243
+ `---\n` +
1244
+ Underpost.deploy
1245
+ .deploymentYamlPartsFactory({
1246
+ deployId: _deployId,
1247
+ env,
1248
+ suffix: targetTraffic,
1249
+ resources: Underpost.deploy.resourcesFactory(options),
1250
+ replicas: options.replicas,
1251
+ image: _image,
1252
+ namespace: options.namespace,
1253
+ volumes: _volumes,
1254
+ cmd: resolvedCmd,
1255
+ })
1256
+ .replace('{{ports}}', buildKindPorts(_fromPort, _toPort));
1257
+
1258
+ fs.writeFileSync(outputPath, deploymentYaml, 'utf8');
1259
+ logger.info(`[instance-build-manifest] Manifest written to ${outputPath}`, {
1260
+ deployId: _deployId,
1261
+ env,
1262
+ traffic: targetTraffic,
1263
+ image: _image,
1264
+ });
1265
+
1266
+ if (env === 'production') {
1267
+ if (fs.existsSync(dockerfileManifestPath)) {
1268
+ fs.copyFileSync(dockerfileManifestPath, `${rootPath}/Dockerfile`);
1269
+ }
1270
+ fs.copyFileSync(outputPath, `${rootPath}/deployment.yaml`);
1271
+ logger.info('[instance-build-manifest] Production artifacts copied to project root', {
1272
+ rootPath,
1273
+ dockerfile: `${rootPath}/Dockerfile`,
1274
+ deployment: `${rootPath}/deployment.yaml`,
1275
+ });
1276
+ const ciSrc = `./.github/workflows/docker-image.${_runtime}.ci.yml`;
1277
+ if (fs.existsSync(ciSrc)) {
1278
+ if (!fs.existsSync(`${rootPath}/.github/workflows`)) fs.mkdirpSync(`${rootPath}/.github/workflows`);
1279
+ fs.copyFileSync(ciSrc, `${rootPath}/.github/workflows/docker-image.${_runtime}.ci.yml`);
1280
+ logger.info(`[instance-build-manifest] CI workflow copied`, { src: ciSrc });
1281
+ }
1282
+ }
1283
+ },
1284
+
1118
1285
  /**
1119
1286
  * @method ls-deployments
1120
1287
  * @description Retrieves and logs a table of Kubernetes deployments using `Underpost.deploy.get`.
@@ -1139,6 +1306,44 @@ EOF
1139
1306
  shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh${options.dev ? ' --install-dev' : ``}`);
1140
1307
  },
1141
1308
 
1309
+ /**
1310
+ * @method install-crio
1311
+ * @description Installs and configures CRI-O as the container runtime for kubeadm clusters.
1312
+ * Adds the stable v1.33 CRI-O yum repository, installs the `cri-o` package, configures
1313
+ * the systemd cgroup driver, enables the `crio` service, and writes `/etc/crictl.yaml`
1314
+ * so that `crictl` targets the CRI-O socket by default.
1315
+ * @param {string} path - Unused.
1316
+ * @param {Object} options - The default underpost runner options for customizing workflow.
1317
+ * @memberof UnderpostRun
1318
+ */
1319
+ 'install-crio': (path, options = DEFAULT_OPTION) => {
1320
+ logger.info('Installing CRI-O...');
1321
+ shellExec(`cat <<EOF | sudo tee /etc/yum.repos.d/cri-o.repo
1322
+ [cri-o]
1323
+ name=CRI-O
1324
+ baseurl=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/v1.33/rpm/
1325
+ enabled=1
1326
+ gpgcheck=1
1327
+ gpgkey=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/v1.33/rpm/repodata/repomd.xml.key
1328
+ EOF`);
1329
+ shellExec(`sudo dnf -y install cri-o`);
1330
+ // crictl is in the kubernetes repo but excluded by default — install it explicitly
1331
+ shellExec(`sudo yum install -y cri-tools --disableexcludes=kubernetes`);
1332
+ // Ensure CRI-O uses systemd cgroup driver (matches kubelet default)
1333
+ shellExec(
1334
+ `sudo sed -i 's/^#\?cgroup_manager =.*/cgroup_manager = "systemd"/' /etc/crio/crio.conf 2>/dev/null || true`,
1335
+ );
1336
+ shellExec(`sudo systemctl enable --now crio`);
1337
+ logger.info('CRI-O installed and started.');
1338
+ // Write crictl config so all crictl calls default to the CRI-O socket
1339
+ shellExec(`cat <<EOF | sudo tee /etc/crictl.yaml
1340
+ runtime-endpoint: unix:///var/run/crio/crio.sock
1341
+ image-endpoint: unix:///var/run/crio/crio.sock
1342
+ timeout: 10
1343
+ debug: false
1344
+ EOF`);
1345
+ },
1346
+
1142
1347
  /**
1143
1348
  * @method dd-container
1144
1349
  * @description Deploys a development or debug container tasks jobs, setting up necessary volumes and images, and running specified commands within the container.
@@ -1281,6 +1486,9 @@ EOF
1281
1486
  /**
1282
1487
  * @method promote
1283
1488
  * @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.router` for 'dd', or a specific ID).
1489
+ * When `--tls` is set, rebuilds the proxy manifest with `--cert` so the HTTPProxy includes
1490
+ * TLS config, deletes stale Certificate resources, then reapplies the proxy and secret.yaml
1491
+ * (cert-manager Certificate resources) for each affected deployment.
1284
1492
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string: `deployId,env,replicas`).
1285
1493
  * @param {Object} options - The default underpost runner options for customizing workflow
1286
1494
  * @memberof UnderpostRun
@@ -1289,11 +1497,34 @@ EOF
1289
1497
  let [inputDeployId, inputEnv, inputReplicas] = path.split(',');
1290
1498
  if (!inputEnv) inputEnv = 'production';
1291
1499
  if (!inputReplicas) inputReplicas = 1;
1500
+ // TODO: normalize: --tls maps to --cert for deploy.js isValidTLSContext compatibility
1501
+ if (options.tls) options.cert = true;
1502
+
1503
+ const applyCerts = (deployId, targetTraffic) => {
1504
+ if (!options.tls) return;
1505
+ // Rebuild proxy.yaml with --cert so the HTTPProxy includes TLS virtualhost config
1506
+ shellExec(
1507
+ `node bin deploy --build-manifest --cert --traffic ${targetTraffic} --replicas ${inputReplicas} --namespace ${options.namespace} ${deployId} ${inputEnv}`,
1508
+ );
1509
+ // Delete stale Certificate resources before reapplying
1510
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1511
+ if (fs.existsSync(confServerPath)) {
1512
+ for (const host of Object.keys(JSON.parse(fs.readFileSync(confServerPath, 'utf8'))))
1513
+ shellExec(`sudo kubectl delete Certificate ${host} -n ${options.namespace} --ignore-not-found`);
1514
+ }
1515
+ shellExec(
1516
+ `sudo kubectl apply -f ./engine-private/conf/${deployId}/build/${inputEnv}/proxy.yaml -n ${options.namespace}`,
1517
+ );
1518
+ const secretPath = `./engine-private/conf/${deployId}/build/${inputEnv}/secret.yaml`;
1519
+ if (fs.existsSync(secretPath)) shellExec(`kubectl apply -f ${secretPath} -n ${options.namespace}`);
1520
+ };
1521
+
1292
1522
  if (inputDeployId === 'dd') {
1293
1523
  for (const deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
1294
1524
  const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
1295
1525
  const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
1296
1526
  Underpost.deploy.switchTraffic(deployId, inputEnv, targetTraffic, inputReplicas, options.namespace, options);
1527
+ applyCerts(deployId, targetTraffic);
1297
1528
  }
1298
1529
  } else {
1299
1530
  const currentTraffic = Underpost.deploy.getCurrentTraffic(inputDeployId, { namespace: options.namespace });
@@ -1306,6 +1537,7 @@ EOF
1306
1537
  options.namespace,
1307
1538
  options,
1308
1539
  );
1540
+ applyCerts(inputDeployId, targetTraffic);
1309
1541
  }
1310
1542
  },
1311
1543
  /**
@@ -1920,6 +2152,16 @@ EOF
1920
2152
 
1921
2153
  shellCd('/home/dd/engine');
1922
2154
  },
2155
+ /**
2156
+ * @method pull-rocky-image
2157
+ * @description Pulls the base `rockylinux:9` image from Docker Hub via Podman.
2158
+ * @param {string} path - The input value, identifier, or path for the operation.
2159
+ * @param {Object} options - The default underpost runner options for customizing workflow
2160
+ * @memberof UnderpostRun
2161
+ */
2162
+ 'pull-rocky-image': (path, options = DEFAULT_OPTION) => {
2163
+ shellExec(`sudo podman pull docker.io/library/rockylinux:9`);
2164
+ },
1923
2165
  /**
1924
2166
  * @method rmi
1925
2167
  * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
@@ -1949,13 +2191,6 @@ EOF
1949
2191
  } else shellExec(`sudo kill -9 $(lsof -t -i:${_path})`);
1950
2192
  }
1951
2193
  },
1952
- /**
1953
- * @method secret
1954
- * @description Creates an Underpost secret named 'underpost' from a file, defaulting to `/home/dd/engine/engine-private/conf/dd-cron/.env.production` if no path is provided.
1955
- * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
1956
- * @param {Object} options - The default underpost runner options for customizing workflow
1957
- * @memberof UnderpostRun
1958
- */
1959
2194
  /**
1960
2195
  * @method generate-pass
1961
2196
  * @description Generates a cryptographically secure random password that satisfies all validatePassword
@@ -1992,10 +2227,16 @@ EOF
1992
2227
  if (options.copy) pbcopy(password);
1993
2228
  else console.log(password);
1994
2229
  },
1995
-
2230
+ /**
2231
+ * @method secret
2232
+ * @description Creates an Underpost secret named 'underpost' from a file, defaulting to `/home/dd/engine/engine-private/conf/dd-cron/.env.production` if no path is provided.
2233
+ * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
2234
+ * @param {Object} options - The default underpost runner options for customizing workflow
2235
+ * @memberof UnderpostRun
2236
+ */
1996
2237
  secret: (path, options = DEFAULT_OPTION) => {
1997
2238
  const secretPath = path ? path : `/home/dd/engine/engine-private/conf/dd-cron/.env.production`;
1998
- const command = `node bin secret underpost --create-from-file ${secretPath}`;
2239
+ const command = `${options.dev ? 'node bin' : 'underpost'} secret underpost --create-from-file ${secretPath}`;
1999
2240
  shellExec(command);
2000
2241
  },
2001
2242
  /**
@@ -2166,6 +2407,144 @@ EOF`;
2166
2407
  if (options.logs) shellExec(`kubectl logs -f ${podName} -n ${namespace}`, { async: true });
2167
2408
  }
2168
2409
  },
2410
+
2411
+ /**
2412
+ * @method push-bundle
2413
+ * @description Builds the client zip for the specified deployment, splits it into parts, and uploads to file storage.
2414
+ * Steps: set env, build+split zip, switch to cron env, upload parts to storage.
2415
+ * @param {string} path - Optional `fsPath.splitOption` string.
2416
+ * Examples: `build` (default split 8), `build.16` (split 16 MB), `build.none-split` (no split flag).
2417
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2418
+ * @param {string} [options.deployId] - Override deploy ID.
2419
+ * @param {boolean} [options.dev] - Use development environment; defaults to production.
2420
+ * @memberof UnderpostRun
2421
+ */
2422
+ 'push-bundle': (path = '', options = DEFAULT_OPTION) => {
2423
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
2424
+ const env = options.dev ? 'development' : 'production';
2425
+ const deployId = options.deployId || 'dd-default';
2426
+ const pathParts = (path || '').split('.');
2427
+ const fsPath = (pathParts[0] || '').trim() || 'build';
2428
+ const splitOption = (pathParts[1] || '').trim();
2429
+
2430
+ let splitFlag = '--split 8';
2431
+ if (splitOption) {
2432
+ if (splitOption === 'none-split') {
2433
+ splitFlag = '';
2434
+ } else {
2435
+ const splitMb = Number(splitOption);
2436
+ if (Number.isFinite(splitMb) && splitMb > 0) {
2437
+ splitFlag = `--split ${splitMb}`;
2438
+ } else {
2439
+ logger.warn('push-bundle: invalid split option, using default split 8', {
2440
+ path,
2441
+ splitOption,
2442
+ });
2443
+ }
2444
+ }
2445
+ }
2446
+
2447
+ shellExec(`${baseCommand} env ${deployId} ${env}`);
2448
+ shellExec(`${baseCommand} client ${deployId} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
2449
+ shellExec(
2450
+ `${baseCommand} fs ${fsPath} --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --force`,
2451
+ );
2452
+ },
2453
+
2454
+ /**
2455
+ * @method pull-bundle
2456
+ * @description Downloads split zip parts from file storage, merges and extracts them, and moves the result into the public directory.
2457
+ * Steps: set env, download parts (omit-unzip), merge zip, unzip, remove zip + parts, move to public/<host>[/path].
2458
+ * Iterates over every non-singleReplica, non-redirect, non-disabledRebuild route in conf.server.json
2459
+ * so that multi-path deployments are handled correctly.
2460
+ * @param {string} path - Optional comma-separated host name(s) to restrict processing (e.g. 'underpost.net' or 'a.com,b.com').
2461
+ * If omitted, all hosts from `engine-private/conf/<deployId>/conf.server.json` are used.
2462
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2463
+ * @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
2464
+ * @param {boolean} [options.dev] - Use development environment; defaults to production.
2465
+ * @memberof UnderpostRun
2466
+ */
2467
+ 'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
2468
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
2469
+ const env = options.dev ? 'development' : 'production';
2470
+ const deployId = options.deployId || 'dd-default';
2471
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
2472
+ const confServer = fs.existsSync(confServerPath) ? loadConfServerJson(confServerPath) : {};
2473
+ const hostsArg = path
2474
+ ? path
2475
+ .split(',')
2476
+ .map((h) => h.trim())
2477
+ .filter(Boolean)
2478
+ : Object.keys(confServer);
2479
+
2480
+ if (hostsArg.length === 0) {
2481
+ logger.error('pull-bundle: no hosts resolved', { deployId, path, confServerPath });
2482
+ return;
2483
+ }
2484
+
2485
+ shellExec(`${baseCommand} env ${deployId} ${env}`);
2486
+ if (!fs.existsSync('./build')) fs.mkdirSync('./build', { recursive: true });
2487
+ shellExec(
2488
+ `${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
2489
+ );
2490
+
2491
+ for (const host of hostsArg) {
2492
+ // Gather all routes for this host; fall back to root '/' when host is not in confServer
2493
+ // (e.g. when hosts were provided explicitly via the path argument).
2494
+ const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
2495
+
2496
+ for (const routePath of routePaths) {
2497
+ const routeConf = confServer[host] ? confServer[host][routePath] || {} : {};
2498
+ // Skip routes that are not built by buildClient (mirrors buildClient skip conditions)
2499
+ if (routeConf.singleReplica || routeConf.redirect || routeConf.disabledRebuild) continue;
2500
+
2501
+ // buildClient names the zip as "<host>-<path-no-slashes>.zip"
2502
+ // e.g. host="underpost.net", path="/" → buildId="underpost.net-", zip="build/underpost.net-.zip"
2503
+ // e.g. host="app.net", path="/admin" → buildId="app.net-admin", zip="build/app.net-admin.zip"
2504
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2505
+ const zipPath = `build/${buildId}.zip`;
2506
+ const buildDir = './build';
2507
+ const hasZip = fs.existsSync(zipPath);
2508
+ const hasParts =
2509
+ fs.existsSync(buildDir) &&
2510
+ fs
2511
+ .readdirSync(buildDir)
2512
+ .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
2513
+
2514
+ if (!hasZip && !hasParts) {
2515
+ logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
2516
+ continue;
2517
+ }
2518
+
2519
+ if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
2520
+ shellExec(`${baseCommand} client --unzip ${zipPath}`);
2521
+ shellExec(`sudo rm -rf ${zipPath}`);
2522
+
2523
+ // Clean up downloaded part wrapper zips left by --omit-unzip pull
2524
+ if (fs.existsSync(buildDir)) {
2525
+ fs.readdirSync(buildDir)
2526
+ .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
2527
+ .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
2528
+ }
2529
+
2530
+ // unzipClientBuild extracts to buildId with trailing '-' stripped
2531
+ // e.g. "build/underpost.net-" → "build/underpost.net"
2532
+ // e.g. "build/app.net-admin" → "build/app.net-admin" (no trailing dash, no change)
2533
+ const extractedDir = `build/${buildId.replace(/-$/, '')}`;
2534
+ if (!fs.existsSync(extractedDir)) {
2535
+ logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
2536
+ continue;
2537
+ }
2538
+
2539
+ // Destination mirrors the public directory layout used by the server
2540
+ const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
2541
+ if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
2542
+ // Ensure parent directory exists for sub-paths
2543
+ if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
2544
+ shellExec(`sudo mv ${extractedDir} ${publicDestPath}`);
2545
+ }
2546
+ }
2547
+ },
2169
2548
  };
2170
2549
 
2171
2550
  static API = {
package/src/cli/ssh.js CHANGED
@@ -315,7 +315,7 @@ EOF`);
315
315
  console.log(`group_name : password_x : GID(Internal Group ID) : user_list`.blue);
316
316
  console.log(filter ? groupsOut.replaceAll(filter, filter.red) : groupsOut);
317
317
  console.log('Users'.bold.blue);
318
- console.log(`usuario : x : UID : GID : GECOS : home_dir : shell`.blue);
318
+ console.log(`user : x : UID : GID : GECOS : home_dir : shell`.blue);
319
319
  console.log(filter ? usersOut.replaceAll(filter, filter.red) : usersOut);
320
320
  }
321
321