underpost 3.2.14 → 3.2.21

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.
@@ -10,11 +10,19 @@ import {
10
10
  loadConfServerJson,
11
11
  loadCronDeployEnv,
12
12
  etcHostFactory,
13
+ deployRangePortFactory,
13
14
  } from '../server/conf.js';
14
15
  import { loggerFactory } from '../server/logger.js';
15
16
  import { timer } from '../client/components/core/CommonJs.js';
17
+ import {
18
+ RUNTIME_STATUS,
19
+ INTERNAL_STATUS_PATH,
20
+ normalizeContainerStatus,
21
+ deployStatusPort,
22
+ } from '../server/runtime-status.js';
16
23
  import axios from 'axios';
17
24
  import fs from 'fs-extra';
25
+ import net from 'node:net';
18
26
  import { shellExec } from '../server/process.js';
19
27
  import Underpost from '../index.js';
20
28
 
@@ -328,26 +336,121 @@ class UnderpostMonitor {
328
336
  };
329
337
  },
330
338
  /**
331
- * Monitors the ready status of a deployment.
339
+ * Resolves a free ephemeral TCP port on the loopback interface, used as the
340
+ * local end of the `kubectl port-forward` tunnel so it never collides with
341
+ * host-local services.
342
+ * @returns {Promise<number>}
343
+ * @memberof UnderpostMonitor
344
+ */
345
+ findFreePort() {
346
+ return new Promise((resolve) => {
347
+ const srv = net.createServer();
348
+ srv.once('error', () => resolve(20000 + Math.floor(Math.random() * 20000)));
349
+ srv.listen(0, '127.0.0.1', () => {
350
+ const { port } = srv.address();
351
+ srv.close(() => resolve(port));
352
+ });
353
+ });
354
+ },
355
+ /**
356
+ * Resolves the deployment's internal status port (Phase-2 transport target).
357
+ *
358
+ * Canonical value is `fromPort - 1` from the deployment router — the exact
359
+ * port `buildManifest` injects into the pod (UNDERPOST_INTERNAL_PORT) and
360
+ * uses for the probes — so the tunnel target always matches the in-pod bind.
361
+ * `UNDERPOST_INTERNAL_PORT` overrides; ambient resolution is the last resort.
362
+ *
363
+ * @param {string} deployId
364
+ * @param {string} env
365
+ * @returns {Promise<number>}
366
+ * @memberof UnderpostMonitor
367
+ */
368
+ async deployInternalPort(deployId, env) {
369
+ const override = parseInt(process.env.UNDERPOST_INTERNAL_PORT);
370
+ if (!Number.isNaN(override)) return override;
371
+ try {
372
+ const router = await Underpost.deploy.routerFactory(deployId, env);
373
+ const { fromPort } = deployRangePortFactory(router);
374
+ if (Number.isFinite(fromPort) && fromPort > 0) return fromPort - 1;
375
+ } catch (_) {
376
+ /* fall through to ambient resolution */
377
+ }
378
+ return deployStatusPort(deployId, env) ?? 3000;
379
+ },
380
+ /**
381
+ * Reads Phase-2 runtime readiness from a single pod over HTTP, tunneling
382
+ * through `kubectl port-forward` to the in-pod internal status endpoint.
383
+ *
384
+ * Transport failures (port-forward down, connection refused, HTTP error)
385
+ * are reported as `{ ok: false }` and must never be interpreted as success
386
+ * by callers — they are retried, not promoted. A reachable endpoint returns
387
+ * `{ ok: true, status }` with the normalized runtime contract value.
388
+ *
389
+ * @param {string} podName
390
+ * @param {string} namespace
391
+ * @param {number} internalPort
392
+ * @returns {Promise<{ok: boolean, status?: (string|null), transportError?: string}>}
393
+ * @memberof UnderpostMonitor
394
+ */
395
+ async readRuntimeStatus(podName, namespace, internalPort) {
396
+ // The local side of the tunnel MUST be an ephemeral free port: pinning it
397
+ // to internalPort collides with any host-local service on that number
398
+ // (e.g. a dev runtime on the same machine as the cluster), which makes
399
+ // port-forward fail to bind and every read return a false transport error.
400
+ const override = parseInt(process.env.UNDERPOST_PF_LOCAL_PORT);
401
+ const localPort = Number.isNaN(override) ? await Underpost.monitor.findFreePort() : override;
402
+ const url = `http://127.0.0.1:${localPort}${INTERNAL_STATUS_PATH}`;
403
+ let portForward;
404
+ try {
405
+ // `exec` collapses the shell so the tracked child PID is the
406
+ // sudo/kubectl process, letting the SIGTERM teardown reach the tunnel.
407
+ portForward = shellExec(
408
+ `exec sudo kubectl port-forward pod/${podName} ${localPort}:${internalPort} -n ${namespace}`,
409
+ { async: true, silent: true, disableLog: true, silentOnError: true },
410
+ );
411
+ } catch (_) {
412
+ portForward = undefined;
413
+ }
414
+ try {
415
+ let lastError;
416
+ const attempts = parseInt(process.env.UNDERPOST_PF_ATTEMPTS) || 20;
417
+ for (let attempt = 0; attempt < attempts; attempt++) {
418
+ try {
419
+ const res = await axios.get(url, { timeout: 2500 });
420
+ const raw = res?.data?.status ?? null;
421
+ return { ok: true, status: normalizeContainerStatus(raw) ?? raw, payload: res.data };
422
+ } catch (error) {
423
+ lastError = error;
424
+ await timer(350);
425
+ }
426
+ }
427
+ return { ok: false, transportError: lastError?.code || lastError?.message || 'transport_failed' };
428
+ } finally {
429
+ if (portForward && typeof portForward.kill === 'function') {
430
+ try {
431
+ portForward.kill('SIGTERM');
432
+ } catch (_) {
433
+ /* tunnel already gone */
434
+ }
435
+ }
436
+ }
437
+ },
438
+ /**
439
+ * Monitors a deployment to terminal readiness using a deterministic
440
+ * two-phase state machine.
332
441
  *
333
- * Ready signal:
334
- * The orchestrator gate is the Kubernetes pod Ready condition. When the
335
- * container's `readinessProbe` succeeds, kubelet flips
336
- * `status.conditions[Ready]` to True and `checkDeploymentReadyStatus`
337
- * returns the pod in `readyPods`. This is the only required signal — see
338
- * `src/client/public/nexodev/docs/references/Deploy custom instance to K8S.md`.
442
+ * Phase 1 (Kubernetes): pod `Ready` condition via `checkDeploymentReadyStatus`.
443
+ * Phase 2 (Runtime): `running-deployment` from the in-pod internal
444
+ * status endpoint, read over HTTP (`readRuntimeStatus`).
339
445
  *
340
- * Container-status:
341
- * `underpost config get container-status` is read from each pod for both
342
- * the display column and as a second ready gate alongside the K8S Ready
343
- * condition. Both must be satisfied before the monitor exits:
344
- * 1. K8S readinessProbe (TCP socket) ensures the port is bound.
345
- * 2. container-status == `<deploy>-<env>-running-deployment` ensures
346
- * the application has completed its own startup sequence.
347
- * Early-abort on `error` container-status remains in effect: a failing
348
- * runtime keeps its pod alive (not Ready) with `container-status=error`,
349
- * so this `exec`-read surfaces the failure and the monitor aborts —
350
- * failing the CD runner instead of waiting out the full timeout.
446
+ * Contract:
447
+ * - Success requires BOTH phases; runtime readiness is never declared
448
+ * before Kubernetes readiness.
449
+ * - An explicit runtime `error` (or a fatal pod status) transitions
450
+ * immediately to `failed` (throw CD exit 1).
451
+ * - Transport failures never count as success and never advance state.
452
+ * - `timeout` is a distinct terminal state from `failed`.
453
+ * - Every transition emits a structured, secret-free event.
351
454
  *
352
455
  * @param {string} deployId - Deployment ID for which the ready status is being monitored.
353
456
  * @param {string} env - Environment for which the ready status is being monitored.
@@ -358,32 +461,29 @@ class UnderpostMonitor {
358
461
  * @memberof UnderpostMonitor
359
462
  */
360
463
  async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
361
- const delayMs = 1000;
362
- const maxIterations = 3000;
464
+ const delayMs = parseInt(process.env.UNDERPOST_MONITOR_DELAY_MS) || 1000;
465
+ const maxIterations = parseInt(process.env.UNDERPOST_MONITOR_MAX_ITERATIONS) || 3000;
363
466
  const deploymentId = `${deployId}-${env}-${targetTraffic}`;
364
- const expectedContainerStatus = `${deployId}-${env}-running-deployment`;
365
467
  const tag = `[${deploymentId}]`;
366
- const containerStatusDefault = 'waiting for status';
367
-
368
- logger.info('Deployment init', { deployId, env, targetTraffic, namespace });
369
-
370
- const podStatusCache = new Map();
468
+ const expectedStatus = RUNTIME_STATUS.RUNNING;
469
+ const internalPort = await Underpost.monitor.deployInternalPort(deployId, env);
470
+ const podErrorStates = ['error', 'crashloopbackoff', 'oomkilled', 'imagepullbackoff', 'errimagepull'];
471
+
472
+ const emit = (state, status) =>
473
+ logger.info('deploy-monitor', {
474
+ deployId: deploymentId,
475
+ phase: state.startsWith('runtime') ? 'runtime' : 'kubernetes',
476
+ state,
477
+ status: status ?? null,
478
+ timestamp: new Date().toISOString(),
479
+ });
480
+
481
+ logger.info('Deployment init', { deployId, env, targetTraffic, namespace, internalPort });
482
+ emit('pending');
483
+
484
+ const runtimeStatusCache = new Map();
371
485
  const advancedPods = new Set();
372
486
 
373
- const readContainerStatus = (podName) => {
374
- try {
375
- const raw = shellExec(
376
- `sudo kubectl exec ${podName} -n ${namespace} -- sh -c 'underpost config get container-status --plain'`,
377
- { silent: true, disableLog: true, stdout: true, silentOnError: true },
378
- );
379
- const val = raw ? raw.toString().trim() : '';
380
- return val && val !== 'undefined' ? val : containerStatusDefault;
381
- } catch (_) {
382
- // exec failed (e.g. pod not yet running) — preserve last known value
383
- return podStatusCache.get(podName) || containerStatusDefault;
384
- }
385
- };
386
-
387
487
  for (let i = 0; i < maxIterations; i++) {
388
488
  const result = await Underpost.monitor.checkDeploymentReadyStatus(
389
489
  deployId,
@@ -392,39 +492,54 @@ class UnderpostMonitor {
392
492
  ignorePods,
393
493
  namespace,
394
494
  );
395
-
396
495
  const allPods = [...result.readyPods, ...result.notReadyPods];
397
496
 
497
+ if (allPods.length === 0) {
498
+ emit('pending');
499
+ await timer(delayMs);
500
+ continue;
501
+ }
502
+ emit('pod_scheduled');
503
+
504
+ // Phase 1 fatal: a Kubernetes-level pod failure is terminal (failed,
505
+ // not timeout) — fail the CD runner immediately instead of waiting out
506
+ // the full window.
398
507
  for (const pod of allPods) {
399
- if (!pod?.NAME) continue;
400
508
  const podStatus = (pod.STATUS || '').toLowerCase().trim();
401
- if (
402
- ['error', 'crashloopbackoff', 'oomkilled', 'imagepullbackoff', 'errimagepull'].find((s) =>
403
- podStatus.match(s),
404
- )
405
- )
509
+ if (podErrorStates.find((s) => podStatus.includes(s)))
406
510
  throw new Error(`Pod ${pod.NAME} has error pod status: ${pod.STATUS}`);
407
- const status = readContainerStatus(pod.NAME);
408
- if (status === 'error') throw new Error(`Pod ${pod.NAME} has error container-status`);
409
- if (advancedPods.has(pod.NAME) && status === containerStatusDefault)
410
- throw new Error(`Pod ${pod.NAME} container-status regressed to default — pod likely restarted`);
411
- if (status !== containerStatusDefault) advancedPods.add(pod.NAME);
412
- podStatusCache.set(pod.NAME, status);
413
511
  }
414
512
 
415
- const allPodsK8sReady = allPods.length > 0 && result.notReadyPods.length === 0;
513
+ const allPodsK8sReady = result.notReadyPods.length === 0;
514
+ if (allPodsK8sReady) emit('pod_ready');
416
515
 
417
- const allPodsStatusReady =
418
- allPods.length > 0 && allPods.every((pod) => podStatusCache.get(pod.NAME) === expectedContainerStatus);
516
+ // Phase 2: runtime readiness over HTTP. Transport failures neither
517
+ // advance state nor count as success; explicit `error` is terminal.
518
+ let allRuntimeRead = true;
519
+ for (const pod of allPods) {
520
+ if (!pod?.NAME) continue;
521
+ const read = await Underpost.monitor.readRuntimeStatus(pod.NAME, namespace, internalPort);
522
+ if (!read.ok) {
523
+ allRuntimeRead = false;
524
+ emit('runtime_booting', `transport:${read.transportError}`);
525
+ continue;
526
+ }
527
+ const status = read.status;
528
+ if (status === RUNTIME_STATUS.ERROR) throw new Error(`Pod ${pod.NAME} reported runtime status=error`);
529
+ if (advancedPods.has(pod.NAME) && (!status || status === RUNTIME_STATUS.BUILD))
530
+ throw new Error(`Pod ${pod.NAME} runtime status regressed (${status ?? 'empty'}) — pod likely restarted`);
531
+ if (status && status !== RUNTIME_STATUS.BUILD) advancedPods.add(pod.NAME);
532
+ runtimeStatusCache.set(pod.NAME, status);
533
+ emit('runtime_booting', status);
534
+ }
535
+
536
+ const allRuntimeReady =
537
+ allRuntimeRead && allPods.every((pod) => runtimeStatusCache.get(pod.NAME) === expectedStatus);
419
538
 
420
- // Print snapshot for every pod — annotate when container-status hasn't caught
421
- // up to the K8S Ready condition yet.
422
539
  for (const pod of allPods) {
423
- const status = podStatusCache.get(pod.NAME) || containerStatusDefault;
540
+ const status = runtimeStatusCache.get(pod.NAME) || 'waiting for status';
424
541
  const podStatus = pod.STATUS || 'Unknown';
425
- const statusMatchesExpected = status === expectedContainerStatus;
426
- const statusDisplay = statusMatchesExpected ? status : `${status} (pending)`;
427
-
542
+ const statusDisplay = status === expectedStatus ? status : `${status} (pending)`;
428
543
  console.log(
429
544
  'Target pod:',
430
545
  pod.NAME[pod.NAME.includes('green') ? 'bgGreen' : 'bgBlue'].bold.black,
@@ -435,22 +550,19 @@ class UnderpostMonitor {
435
550
  );
436
551
  }
437
552
 
438
- // Both K8S readinessProbe AND container-status must be satisfied before
439
- // declaring the deployment ready. The TCP probe ensures the port is bound;
440
- // container-status == running-deployment ensures the application has
441
- // completed its own startup sequence so traffic is not switched prematurely.
442
- if (allPodsK8sReady && allPodsStatusReady) {
443
- logger.info(`${tag} | All pods Ready (K8S readinessProbe satisfied)`);
553
+ // Terminal success requires BOTH phases. runtime_ready cannot precede
554
+ // Kubernetes readiness.
555
+ if (allPodsK8sReady && allRuntimeReady) {
556
+ emit('runtime_ready', expectedStatus);
557
+ logger.info(`${tag} | Deployment ready (K8S Ready + runtime ${expectedStatus})`);
444
558
  return result;
445
559
  }
446
560
 
447
561
  await timer(delayMs);
448
-
449
- if ((i + 1) % 10 === 0) {
450
- logger.info(`${tag} | In progress... iteration ${i + 1}`);
451
- }
562
+ if ((i + 1) % 10 === 0) logger.info(`${tag} | In progress... iteration ${i + 1}`);
452
563
  }
453
564
 
565
+ emit('timeout');
454
566
  logger.error(`${tag} | Deployment timeout after ${maxIterations} iterations`);
455
567
  throw new Error(
456
568
  `monitorReadyRunner timeout: ${deploymentId} did not become Ready within ${maxIterations}*${delayMs}ms`,
@@ -264,18 +264,29 @@ const ISOLATED_ENV = 'env -i HOME="$HOME" PATH="$PATH" USER="$USER" LOGNAME="$LO
264
264
  *
265
265
  * @returns {boolean} true when the template started cleanly, false otherwise.
266
266
  */
267
- async function buildAndTestTemplate() {
267
+ async function buildAndTestTemplate(opts = {}) {
268
268
  killDevServers();
269
269
  Underpost.repo.clean({ paths: ['/home/dd/engine', '/home/dd/engine/engine-private '] });
270
270
  shellExec(`node bin pull . ${process.env.GITHUB_USERNAME}/engine`);
271
+ fs.removeSync(TEMPLATE_PATH);
271
272
  shellExec(`npm run build:template`);
272
273
  shellExec(`node bin run shared-dir ${TEMPLATE_PATH}`);
273
274
 
275
+ const upsertEnvVar = (content, key, value) => {
276
+ const re = new RegExp(`^(${key}=).*`, 'm');
277
+ if (re.test(content)) return content.replace(re, `$1${value}`);
278
+ return `${content.trimEnd()}\n${key}=${value}\n`;
279
+ };
280
+
274
281
  const dhcpHostIp = Dns.getLocalIPv4Address();
275
282
  logger.info(`DHCP host IP for template test: ${dhcpHostIp}`);
276
283
  let envContent = fs.readFileSync(`${TEMPLATE_PATH}/.env.example`, 'utf8');
277
284
  if (dhcpHostIp) envContent = envContent.replace(/127\.0\.0\.1/g, dhcpHostIp);
278
- envContent = envContent.replace(/^ENABLE_FILE_LOGS=.*/m, 'ENABLE_FILE_LOGS=true');
285
+ envContent = upsertEnvVar(envContent, 'ENABLE_FILE_LOGS', 'true');
286
+ if (opts.mongoHost) envContent = upsertEnvVar(envContent, 'DB_HOST', opts.mongoHost);
287
+ if (opts.mongoUser) envContent = upsertEnvVar(envContent, 'DB_USER', opts.mongoUser);
288
+ if (opts.mongoPassword) envContent = upsertEnvVar(envContent, 'DB_PASSWORD', opts.mongoPassword);
289
+ if (opts.valkeyHost) envContent = upsertEnvVar(envContent, 'VALKEY_HOST', opts.valkeyHost);
279
290
  // fs.writeFileSync(`${TEMPLATE_PATH}/.env`, envContent, 'utf8');
280
291
  fs.writeFileSync(`${TEMPLATE_PATH}/.env.example`, envContent, 'utf8');
281
292
  shellExec(`cd ${TEMPLATE_PATH} && npm install`);
@@ -337,7 +348,12 @@ class UnderpostRelease {
337
348
  *
338
349
  * @method build
339
350
  * @param {string} [newVersion] - The new version string to set. Defaults to current version if not provided.
340
- * @param {{dryRun?: boolean}} [options] - Commander options. `--dry-run` previews changes.
351
+ * @param {{dryRun?: boolean, mongoHost?: string, mongoUser?: string, mongoPassword?: string, valkeyHost?: string}} [options] - Commander options.
352
+ * `--dry-run` previews changes without writing files.
353
+ * `--mongo-host` overrides `DB_HOST` in the template `.env.example` smoke test.
354
+ * `--mongo-user` overrides `DB_USER` in the template `.env.example` smoke test.
355
+ * `--mongo-password` overrides `DB_PASSWORD` in the template `.env.example` smoke test.
356
+ * `--valkey-host` overrides `VALKEY_HOST` in the template `.env.example` smoke test.
341
357
  * @memberof UnderpostRelease
342
358
  */
343
359
  async build(newVersion, options = {}) {
@@ -352,7 +368,7 @@ class UnderpostRelease {
352
368
  logger.info(`Release build — bumping ${version} → ${newVersion}${dryRun ? ' (dry-run)' : ''}`);
353
369
 
354
370
  if (!dryRun) {
355
- const templateOk = await buildAndTestTemplate();
371
+ const templateOk = await buildAndTestTemplate(options);
356
372
  if (!templateOk) return;
357
373
  }
358
374
 
@@ -390,8 +406,7 @@ class UnderpostRelease {
390
406
  shellExec(`node bin/deploy cli-docs ${version} ${newVersion}`);
391
407
  shellExec(`node bin/deploy update-dependencies`);
392
408
  shellExec(`node bin/build dd`);
393
- shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production`);
394
- shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
409
+ shellExec(`node bin run build-cluster-deployment-manifests`);
395
410
  shellExec(`node bin new --default-conf --conf-workflow-id template`);
396
411
  shellExec(`sudo rm -rf ./engine-private/conf/dd-default`);
397
412
  shellExec(`node bin new --deploy-id dd-default`);
@@ -133,10 +133,21 @@ class UnderpostRepository {
133
133
  p: undefined,
134
134
  bc: '',
135
135
  isRemoteRepo: '',
136
+ hasChanges: false,
136
137
  },
137
138
  ) {
138
139
  if (!repoPath) repoPath = '.';
139
140
 
141
+ if (options.hasChanges) {
142
+ const status = shellExec(`cd ${repoPath} && git status --porcelain`, {
143
+ stdout: true,
144
+ silent: true,
145
+ disableLog: true,
146
+ }).trim();
147
+ process.stdout.write(status ? '1' : '');
148
+ return;
149
+ }
150
+
140
151
  if (options.isRemoteRepo) {
141
152
  const accessible = Underpost.repo.isRemoteRepo(options.isRemoteRepo);
142
153
  console.log(accessible);
@@ -608,7 +619,8 @@ class UnderpostRepository {
608
619
  const npmRoot = getNpmRootPath();
609
620
  const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
610
621
  const destFolder = `./${projectName}`;
611
- logger.info('build app', { destFolder });
622
+ const deployId = projectName.startsWith('dd-') ? projectName : `dd-${projectName}`;
623
+ logger.info('build app', { destFolder, deployId });
612
624
  if (fs.existsSync(destFolder)) fs.removeSync(destFolder);
613
625
  fs.mkdirSync(destFolder, { recursive: true });
614
626
  if (!options.dev) {
@@ -621,8 +633,9 @@ class UnderpostRepository {
621
633
  UnderpostRepository.API.initLocalRepo({ path: destFolder });
622
634
  shellExec(`cd ${destFolder} && git add . && git commit -m "Base template implementation"`);
623
635
  }
624
- shellExec(`cd ${destFolder} && npm run build`);
625
- shellExec(`cd ${destFolder} && npm run dev`);
636
+ shellExec(`cd ${destFolder} && node bin new --deploy-id ${deployId} --default-conf`);
637
+ shellExec(`cd ${destFolder} && node bin client ${deployId}`);
638
+ shellExec(`cd ${destFolder} && DEPLOY_ID=${deployId} npm run dev`);
626
639
  }
627
640
  return resolve(true);
628
641
  } catch (error) {
@@ -1380,8 +1393,9 @@ Prevent build private config repo.`,
1380
1393
  const gitEmail = process.env.GITHUB_EMAIL || `development@underpost.net`;
1381
1394
 
1382
1395
  if (!fs.existsSync(`${repoPath}/.git`)) {
1383
- shellExec(`cd "${repoPath}" && git init`);
1396
+ shellExec(`mkdir -p "${repoPath}" && git init "${repoPath}"`);
1384
1397
  }
1398
+
1385
1399
  shellExec(`cd "${repoPath}" && git config user.name '${gitUsername}'`);
1386
1400
  shellExec(`cd "${repoPath}" && git config user.email '${gitEmail}'`);
1387
1401
  shellExec(`cd "${repoPath}" && git config core.filemode false`);
package/src/cli/run.js CHANGED
@@ -265,6 +265,16 @@ class UnderpostRun {
265
265
  logger.info(hostListenResult.renderHosts);
266
266
  },
267
267
 
268
+ /**
269
+ * @method etc-hosts
270
+ * @description Modifies the `/etc/hosts` file to add entries for local access to services,
271
+ * based on the provided path input.
272
+ * @param {string} path - The input value, identifier, or path for the operation (used to specify the entries to add to /etc/hosts).
273
+ */
274
+ 'etc-hosts': (path = '', options = DEFAULT_OPTION) => {
275
+ etcHostFactory(path.split(','));
276
+ },
277
+
268
278
  /**
269
279
  * @method ipfs-expose
270
280
  * @description Exposes IPFS Cluster services on specified ports for local access.
@@ -2685,6 +2695,18 @@ EOF`;
2685
2695
  }
2686
2696
  },
2687
2697
 
2698
+ /**
2699
+ * @method build-cluster-deployment-manifests
2700
+ * @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.
2701
+ * @param {string} path - Unused.
2702
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2703
+ * @memberof UnderpostRun
2704
+ */
2705
+ 'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
2706
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
2707
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production --cert`);
2708
+ },
2709
+
2688
2710
  /**
2689
2711
  * @method monitor-ui
2690
2712
  * @description Installs and enables the Cockpit KVM Dashboard (cockpit, cockpit-machines, libvirt)
@@ -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.21';
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);
@@ -2029,6 +2026,32 @@ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-te
2029
2026
  );
2030
2027
  };
2031
2028
 
2029
+ const updatePrivateTemplateRepo = async () => {
2030
+ const templatePath = '/home/dd/pwa-microservices-template';
2031
+ shellExec(`sudo rm -rf ${templatePath}
2032
+ cd /home/dd/engine && npm run build:template
2033
+ cd /home/dd
2034
+ underpost clone --bare underpostnet/pwa-microservices-template-private
2035
+ sudo rm -rf ${templatePath}/.git
2036
+ mv ./pwa-microservices-template-private.git ${templatePath}/.git
2037
+ cd ${templatePath}
2038
+ npm install --omit=dev --ignore-scripts
2039
+ git init
2040
+ git config user.name 'underpostnet'
2041
+ git config user.email 'development@underpost.net'
2042
+ git add .`);
2043
+ const hasChanges = shellExec(`node bin cmt ${templatePath} --has-changes`, {
2044
+ stdout: true,
2045
+ silent: true,
2046
+ disableLog: true,
2047
+ }).trim();
2048
+ if (hasChanges === '1') {
2049
+ shellExec(
2050
+ `cd ${templatePath} && git commit -m 'Update template' && underpost push . underpostnet/pwa-microservices-template-private`,
2051
+ );
2052
+ }
2053
+ };
2054
+
2032
2055
  export {
2033
2056
  Config,
2034
2057
  loadConf,
@@ -2080,4 +2103,5 @@ export {
2080
2103
  syncPrivateConf,
2081
2104
  syncDeployIdSources,
2082
2105
  buildTemplate,
2106
+ updatePrivateTemplateRepo,
2083
2107
  };