underpost 3.2.14 → 3.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli/run.js CHANGED
@@ -120,6 +120,7 @@ const logger = loggerFactory(import.meta);
120
120
  * @property {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment (supported by: sync, template-deploy).
121
121
  * @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).
122
122
  * @property {boolean} remove - Whether to remove/teardown resources instead of creating them (e.g. delete-expose for k3s proxy devices in dev-cluster).
123
+ * @property {boolean} test - Whether to enable test/generic-purpose mode (e.g. use self-signed TLS instead of cert-manager).
123
124
  * @memberof UnderpostRun
124
125
  */
125
126
  const DEFAULT_OPTION = {
@@ -188,6 +189,7 @@ const DEFAULT_OPTION = {
188
189
  skipFullBuild: false,
189
190
  pullBundle: false,
190
191
  remove: false,
192
+ test: false,
191
193
  };
192
194
 
193
195
  /**
@@ -223,7 +225,7 @@ class UnderpostRun {
223
225
  shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''}`);
224
226
 
225
227
  shellExec(
226
- `${baseCommand} cluster${options.dev ? ' --dev' : ''} --mongodb4 --service-host ${mongoHosts.join(
228
+ `${baseCommand} cluster${options.dev ? ' --dev' : ''} --mongodb --service-host ${mongoHosts.join(
227
229
  ',',
228
230
  )} --pull-image`,
229
231
  );
@@ -265,6 +267,16 @@ class UnderpostRun {
265
267
  logger.info(hostListenResult.renderHosts);
266
268
  },
267
269
 
270
+ /**
271
+ * @method etc-hosts
272
+ * @description Modifies the `/etc/hosts` file to add entries for local access to services,
273
+ * based on the provided path input.
274
+ * @param {string} path - The input value, identifier, or path for the operation (used to specify the entries to add to /etc/hosts).
275
+ */
276
+ 'etc-hosts': (path = '', options = DEFAULT_OPTION) => {
277
+ etcHostFactory(path.split(','));
278
+ },
279
+
268
280
  /**
269
281
  * @method ipfs-expose
270
282
  * @description Exposes IPFS Cluster services on specified ports for local access.
@@ -997,8 +1009,19 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
997
1009
  // pathRewritePolicy,
998
1010
  });
999
1011
  if (options.tls) {
1000
- shellExec(`sudo kubectl delete Certificate ${_host} -n ${options.namespace} --ignore-not-found`);
1001
- proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host: _host });
1012
+ if (options.test) {
1013
+ const sslDir = `./engine-private/ssl/${_host}`;
1014
+ const nameSafe = _host.replace(/[^a-zA-Z0-9_.-]/g, '_');
1015
+ fs.mkdirpSync(sslDir);
1016
+ shellExec(`bash ./scripts/ssl.sh "${sslDir}" "${_host}"`);
1017
+ shellExec(`kubectl delete secret ${_host} -n ${options.namespace} --ignore-not-found`);
1018
+ shellExec(
1019
+ `kubectl create secret tls ${_host} --cert="${sslDir}/${nameSafe}.pem" --key="${sslDir}/${nameSafe}-key.pem" -n ${options.namespace}`,
1020
+ );
1021
+ } else {
1022
+ shellExec(`sudo kubectl delete Certificate ${_host} -n ${options.namespace} --ignore-not-found`);
1023
+ proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host: _host });
1024
+ }
1002
1025
  }
1003
1026
  // console.log(proxyYaml);
1004
1027
  shellExec(`kubectl delete HTTPProxy ${_host} --namespace ${options.namespace} --ignore-not-found`);
@@ -1067,6 +1090,7 @@ EOF
1067
1090
  // Examples images:
1068
1091
  // `underpost/underpost-engine:${Underpost.version}`
1069
1092
  // `localhost/rockylinux9-underpost:${Underpost.version}`
1093
+ if (options.imageName) _image = options.imageName;
1070
1094
  if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
1071
1095
 
1072
1096
  if (_image && !_image.startsWith('localhost'))
@@ -1162,12 +1186,16 @@ EOF
1162
1186
  `,
1163
1187
  { disableLog: true },
1164
1188
  );
1189
+ // Custom instances run a bare binary (no `underpost start` / internal
1190
+ // HTTP endpoint): Kubernetes readiness is the running signal and
1191
+ // container-status is read via exec. See `Deploy custom instance to K8S.md`.
1165
1192
  const { ready, readyPods } = await Underpost.monitor.monitorReadyRunner(
1166
1193
  _deployId,
1167
1194
  env,
1168
1195
  targetTraffic,
1169
1196
  ignorePods,
1170
1197
  options.namespace,
1198
+ { readyGate: 'kubernetes', statusTransport: 'exec' },
1171
1199
  );
1172
1200
 
1173
1201
  if (!ready) {
@@ -1177,7 +1205,7 @@ EOF
1177
1205
  shellExec(
1178
1206
  `${baseCommand} run${baseClusterCommand} --namespace ${options.namespace}` +
1179
1207
  `${options.nodeName ? ` --node-name ${options.nodeName}` : ''}` +
1180
- `${options.tls ? ` --tls` : ''}` +
1208
+ `${options.tls ? ` --tls ${options.test ? '--test' : ''}` : ''}` +
1181
1209
  ` instance-promote '${path}'`,
1182
1210
  );
1183
1211
  }
@@ -2685,6 +2713,18 @@ EOF`;
2685
2713
  }
2686
2714
  },
2687
2715
 
2716
+ /**
2717
+ * @method build-cluster-deployment-manifests
2718
+ * @description Builds deployment manifests for both production and development environments using `node bin deploy --build-manifest`, syncing them, and setting replicas to 1 for the `dd` deployment.
2719
+ * @param {string} path - Unused.
2720
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2721
+ * @memberof UnderpostRun
2722
+ */
2723
+ 'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
2724
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
2725
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production --cert`);
2726
+ },
2727
+
2688
2728
  /**
2689
2729
  * @method monitor-ui
2690
2730
  * @description Installs and enables the Cockpit KVM Dashboard (cockpit, cockpit-machines, libvirt)
@@ -73,7 +73,7 @@ class PanelForm {
73
73
  parentIdModal: undefined,
74
74
  route: 'home',
75
75
  htmlFormHeader: async () => '',
76
- firsUpdateEvent: async () => { },
76
+ firsUpdateEvent: async () => {},
77
77
  share: {
78
78
  copyLink: false,
79
79
  copySourceMd: false,
@@ -196,12 +196,12 @@ class PanelForm {
196
196
  <img
197
197
  class="abs center"
198
198
  style="${renderCssAttr({
199
- style: {
200
- width: '100px',
201
- height: '100px',
202
- opacity: 0.2,
203
- },
204
- })}"
199
+ style: {
200
+ width: '100px',
201
+ height: '100px',
202
+ opacity: 0.2,
203
+ },
204
+ })}"
205
205
  src="${defaultUrlImage}"
206
206
  />
207
207
  `,
@@ -382,15 +382,15 @@ class PanelForm {
382
382
  // It will be filtered from the tags array to keep visibility control separate from content tags
383
383
  const tags = data.tags
384
384
  ? uniqueArray(
385
- data.tags
386
- .replaceAll('/', ',')
387
- .replaceAll('-', ',')
388
- .replaceAll(' ', ',')
389
- .split(',')
390
- .map((t) => t.trim())
391
- .filter((t) => t)
392
- .concat(prefixTags),
393
- )
385
+ data.tags
386
+ .replaceAll('/', ',')
387
+ .replaceAll('-', ',')
388
+ .replaceAll(' ', ',')
389
+ .split(',')
390
+ .map((t) => t.trim())
391
+ .filter((t) => t)
392
+ .concat(prefixTags),
393
+ )
394
394
  : prefixTags;
395
395
  let originObj, originFileObj, indexOriginObj;
396
396
  if (editId) {
@@ -432,8 +432,8 @@ class PanelForm {
432
432
  // In edit mode, null means user cleared the file - we need to tell server to remove it
433
433
  const isFileCleared = data.fileId === null && editId;
434
434
  await (async () => {
435
- // When file is null and not the first iteration or not in edit mode, skip upload
436
- if (!file && !isFileCleared) return;
435
+ // When file is null, no markdown content, and not clearing a file, skip upload
436
+ if (!file && !isFileCleared && !hasMdContent) return;
437
437
  // When user cleared file in edit mode, set fileId=null so server removes the reference
438
438
  if (isFileCleared) {
439
439
  fileId = null;
@@ -489,8 +489,8 @@ class PanelForm {
489
489
  message: documentMessage,
490
490
  data: documentData,
491
491
  } = originObj && indexFormDoc === 0
492
- ? await DocumentService.put({ id: originObj._id, body })
493
- : await DocumentService.post({
492
+ ? await DocumentService.put({ id: originObj._id, body })
493
+ : await DocumentService.post({
494
494
  body,
495
495
  });
496
496
  const newDoc = {
@@ -518,12 +518,12 @@ class PanelForm {
518
518
  fileId: {
519
519
  fileBlob: file
520
520
  ? {
521
- data: {
522
- data: await getDataFromInputFile(file),
523
- },
524
- mimetype: file.type,
525
- name: file.name,
526
- }
521
+ data: {
522
+ data: await getDataFromInputFile(file),
523
+ },
524
+ mimetype: file.type,
525
+ name: file.name,
526
+ }
527
527
  : undefined,
528
528
  filePlain: undefined,
529
529
  },
@@ -742,36 +742,36 @@ class PanelForm {
742
742
  <div
743
743
  class="in fll ssr-shimmer-search-box"
744
744
  style="${renderCssAttr({
745
- style: {
746
- width: '80%',
747
- height: '30px',
748
- top: '-13px',
749
- left: '10px',
750
- },
751
- })}"
745
+ style: {
746
+ width: '80%',
747
+ height: '30px',
748
+ top: '-13px',
749
+ left: '10px',
750
+ },
751
+ })}"
752
752
  ></div>
753
753
  </div>`,
754
754
  createdAt: html`<div class="fl">
755
755
  <div
756
756
  class="in fll ssr-shimmer-search-box"
757
757
  style="${renderCssAttr({
758
- style: {
759
- width: '50%',
760
- height: '30px',
761
- left: '-5px',
762
- },
763
- })}"
758
+ style: {
759
+ width: '50%',
760
+ height: '30px',
761
+ left: '-5px',
762
+ },
763
+ })}"
764
764
  ></div>
765
765
  </div>`,
766
766
  mdFileId: html`<div class="fl section-mp">
767
767
  <div
768
768
  class="in fll ssr-shimmer-search-box"
769
769
  style="${renderCssAttr({
770
- style: {
771
- width: '80%',
772
- height: '30px',
773
- },
774
- })}"
770
+ style: {
771
+ width: '80%',
772
+ height: '30px',
773
+ },
774
+ })}"
775
775
  ></div>
776
776
  </div>`.repeat(random(2, 4)),
777
777
  ssr: true,
@@ -86,7 +86,8 @@ class MongooseDBService {
86
86
 
87
87
  const user = config.user || process.env.DB_USER || '';
88
88
  const password = config.password || process.env.DB_PASSWORD || '';
89
- const directConnection = hosts.length === 1;
89
+ const hasExplicitReplicaSet = !!(config.replicaSet || process.env.DB_REPLICA_SET);
90
+ const directConnection = hosts.length === 1 && !hasExplicitReplicaSet;
90
91
  const replicaSet = directConnection
91
92
  ? ''
92
93
  : config.replicaSet || process.env.DB_REPLICA_SET || MONGODB_DEFAULT_REPLICA_SET;
package/src/index.js CHANGED
@@ -44,7 +44,7 @@ class Underpost {
44
44
  * @type {String}
45
45
  * @memberof Underpost
46
46
  */
47
- static version = 'v3.2.14';
47
+ static version = 'v3.2.22';
48
48
 
49
49
  /**
50
50
  * Required Node.js major version
@@ -1086,13 +1086,9 @@ const buildPortProxyRouter = (
1086
1086
 
1087
1087
  if (Object.keys(router).length === 0) return router;
1088
1088
 
1089
- if (options.devProxyContext === true && process.env.NODE_ENV === 'development') {
1090
- const confDevApiServer = JSON.parse(
1091
- fs.readFileSync(
1092
- `./engine-private/conf/${process.argv[3]}/conf.server.dev.${process.argv[4]}-dev-api.json`,
1093
- 'utf8',
1094
- ),
1095
- );
1089
+ const devApiConfPath = `./engine-private/conf/${process.argv[3]}/conf.server.dev.${process.argv[4]}-dev-api.json`;
1090
+ if (options.devProxyContext === true && process.env.NODE_ENV === 'development' && fs.existsSync(devApiConfPath)) {
1091
+ const confDevApiServer = JSON.parse(fs.readFileSync(devApiConfPath, 'utf8'));
1096
1092
  let devApiHosts = [];
1097
1093
  let origins = [];
1098
1094
  for (const _host of Object.keys(confDevApiServer))
@@ -1524,11 +1520,12 @@ const buildCliDoc = (program, oldVersion, newVersion) => {
1524
1520
  if (name === 'help') continue;
1525
1521
  const cmdHelp = parseHelp(help(name));
1526
1522
  details +=
1527
- `\n### \`underpost ${name}\`\n\n` +
1523
+ `\n### underpost ${name}\n\n` +
1528
1524
  (cmdHelp.description ? `${cmdHelp.description.replace(/\s+/g, ' ')}\n\n` : '') +
1529
1525
  `**Usage:** \`${cmdHelp.usage}\`\n` +
1530
1526
  detailSection(cmdHelp.sections, 'Arguments', ['Argument', 'Description']) +
1531
- detailSection(cmdHelp.sections, 'Options', ['Option', 'Description']);
1527
+ detailSection(cmdHelp.sections, 'Options', ['Option', 'Description']) +
1528
+ `\n---\n`;
1532
1529
  }
1533
1530
 
1534
1531
  const md = `${index}${details}`.replaceAll(oldVersion, newVersion);
@@ -1859,6 +1856,7 @@ const syncPrivateConf = (deployId, extraPaths = []) => {
1859
1856
  if (!fs.existsSync(privateRepoPath)) {
1860
1857
  shellExec(`cd .. && underpost clone ${privateGitUri}`, { silent: true });
1861
1858
  } else {
1859
+ shellExec(`git config --global --add safe.directory '${dir.resolve(privateRepoPath)}'`);
1862
1860
  shellExec(`cd ${privateRepoPath} && git checkout . && git clean -f -d && underpost pull . ${privateGitUri}`, {
1863
1861
  silent: true,
1864
1862
  });
@@ -1944,15 +1942,9 @@ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-te
1944
1942
  )
1945
1943
  ).filter((p) => !p.startsWith('.git'));
1946
1944
 
1947
- // Clone the template from 0 if missing; otherwise reset it to a clean pristine checkout.
1948
- if (!fs.existsSync(toPath)) {
1949
- shellExec(`cd .. && node engine/bin clone ${githubUsername}/pwa-microservices-template`);
1950
- } else {
1951
- shellExec(`cd ${toPath} && git reset && git checkout . && git clean -f -d`);
1952
- shellExec(`node bin pull ${toPath} ${githubUsername}/pwa-microservices-template`);
1953
- shellExec(`sudo rm -rf ${toPath}/engine-private`);
1954
- shellExec(`sudo rm -rf ${toPath}/logs`);
1955
- }
1945
+ fs.removeSync(`${githubUsername}/pwa-microservices-template`);
1946
+ shellExec(`cd .. && node engine/bin clone ${githubUsername}/pwa-microservices-template`);
1947
+
1956
1948
  shellExec(`cd ${toPath} && git config core.filemode false`);
1957
1949
 
1958
1950
  for (const copyPath of sourceFiles) {
@@ -2029,6 +2021,85 @@ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-te
2029
2021
  );
2030
2022
  };
2031
2023
 
2024
+ const updatePrivateTemplateRepo = async () => {
2025
+ const templatePath = '/home/dd/pwa-microservices-template';
2026
+ shellExec(`sudo rm -rf ${templatePath}
2027
+ cd /home/dd/engine && npm run build:template
2028
+ cd /home/dd
2029
+ underpost clone --bare underpostnet/pwa-microservices-template-private
2030
+ sudo rm -rf ${templatePath}/.git
2031
+ mv ./pwa-microservices-template-private.git ${templatePath}/.git
2032
+ cd ${templatePath}
2033
+ npm install --omit=dev --ignore-scripts
2034
+ git init
2035
+ git config user.name 'underpostnet'
2036
+ git config user.email 'development@underpost.net'
2037
+ git add .`);
2038
+ const hasChanges = shellExec(`node bin cmt ${templatePath} --has-changes`, {
2039
+ stdout: true,
2040
+ silent: true,
2041
+ disableLog: true,
2042
+ }).trim();
2043
+ if (hasChanges === '1') {
2044
+ shellExec(
2045
+ `cd ${templatePath} && git commit -m 'Update template' && underpost push . underpostnet/pwa-microservices-template-private`,
2046
+ );
2047
+ }
2048
+ };
2049
+
2050
+ /**
2051
+ * @method updatePrivateEngineTestRepo
2052
+ * @description Publishes a deploy id's freshly assembled template to its private
2053
+ * **test** source repo `engine-test-<idPart>` (separate from the production
2054
+ * `engine-<idPart>`). A pod started with `underpost start --build --private-test-repo`
2055
+ * clones this repo, so work-in-progress engine source can be tested end to end
2056
+ * without touching the production source. Mirrors {@link updatePrivateTemplateRepo}
2057
+ * but per-deploy-id and against the test repo.
2058
+ *
2059
+ * Assumes the deploy id template has already been assembled at the template path
2060
+ * (run `node bin/build <deployId>` first, or use `node bin/build <deployId> --update-private`).
2061
+ * @param {string} deployId - Concrete deploy id (e.g. `dd-core`).
2062
+ * @returns {Promise<void>}
2063
+ * @memberof ServerConfBuilder
2064
+ */
2065
+ const updatePrivateEngineTestRepo = async (deployId) => {
2066
+ const username = process.env.GITHUB_USERNAME || 'underpostnet';
2067
+ const repoName = `engine-test-${deployId.split('-')[1]}`;
2068
+ const templatePath = '/home/dd/pwa-microservices-template';
2069
+ if (!fs.existsSync(templatePath))
2070
+ throw new Error(`updatePrivateEngineTestRepo: assemble the template first (node bin/build ${deployId})`);
2071
+
2072
+ // Detach the assembled working tree from any engine-build git history.
2073
+ shellExec(`sudo rm -rf ${templatePath}/.git`);
2074
+
2075
+ // Adopt the test repo's existing history when present (so the push is a delta);
2076
+ // otherwise publish a fresh history on first push.
2077
+ shellExec(`cd /home/dd && sudo rm -rf ./${repoName}.git && underpost clone --bare ${username}/${repoName}`, {
2078
+ silent: true,
2079
+ disableLog: true,
2080
+ silentOnError: true,
2081
+ });
2082
+ if (fs.existsSync(`/home/dd/${repoName}.git`)) shellExec(`mv /home/dd/${repoName}.git ${templatePath}/.git`);
2083
+
2084
+ // `git init` converts the moved bare repo into a normal work-tree repo (bare
2085
+ // clones have no work tree, so `git add` would fail), and bootstraps a fresh
2086
+ // repo on first publish. Idempotent — mirrors updatePrivateTemplateRepo.
2087
+ shellExec(`cd ${templatePath}
2088
+ git init
2089
+ git config user.name '${username}'
2090
+ git config user.email 'development@underpost.net'
2091
+ git add .`);
2092
+
2093
+ const hasChanges = shellExec(`node bin cmt ${templatePath} --has-changes`, {
2094
+ stdout: true,
2095
+ silent: true,
2096
+ disableLog: true,
2097
+ }).trim();
2098
+ if (hasChanges === '1')
2099
+ shellExec(`cd ${templatePath} && git commit -m 'Update ${repoName}' && underpost push . ${username}/${repoName}`);
2100
+ else logger.info('No changes to publish', { repoName });
2101
+ };
2102
+
2032
2103
  export {
2033
2104
  Config,
2034
2105
  loadConf,
@@ -2080,4 +2151,6 @@ export {
2080
2151
  syncPrivateConf,
2081
2152
  syncDeployIdSources,
2082
2153
  buildTemplate,
2154
+ updatePrivateTemplateRepo,
2155
+ updatePrivateEngineTestRepo,
2083
2156
  };
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import stringify from 'fast-json-stable-stringify';
14
14
  import { loggerFactory } from './logger.js';
15
+ import Underpost from '../index.js';
15
16
  const logger = loggerFactory(import.meta);
16
17
  const DEFAULT_IPFS_HTTP_TIMEOUT_MS = Number(process.env.IPFS_HTTP_TIMEOUT_MS || 10000);
17
18
  const getRequestTimeoutMs = (kind = 'kubo') => {
@@ -46,21 +47,22 @@ const fetchWithTimeout = async (url, options = {}, { kind = 'kubo', label = url
46
47
  * @returns {string}
47
48
  */
48
49
  const getIpfsApiUrl = () =>
49
- process.env.IPFS_API_URL || `http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:5001`;
50
+ process.env.IPFS_API_URL ||
51
+ `http://${process.env.NODE_ENV === 'development' && !Underpost.env.isInsideContainer() ? 'localhost' : 'ipfs-cluster'}:5001`;
50
52
  /**
51
53
  * Base URL of the IPFS Cluster REST API (port 9094).
52
54
  * @returns {string}
53
55
  */
54
56
  const getClusterApiUrl = () =>
55
57
  process.env.IPFS_CLUSTER_API_URL ||
56
- `http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:9094`;
58
+ `http://${process.env.NODE_ENV === 'development' && !Underpost.env.isInsideContainer() ? 'localhost' : 'ipfs-cluster'}:9094`;
57
59
  /**
58
60
  * Base URL of the IPFS HTTP Gateway (port 8080).
59
61
  * @returns {string}
60
62
  */
61
63
  const getGatewayUrl = () =>
62
64
  process.env.IPFS_GATEWAY_URL ||
63
- `http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:8080`;
65
+ `http://${process.env.NODE_ENV === 'development' && !Underpost.env.isInsideContainer() ? 'localhost' : 'ipfs-cluster'}:8080`;
64
66
  // ─────────────────────────────────────────────────────────
65
67
  // Core: add content
66
68
  // ─────────────────────────────────────────────────────────