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.
- package/CHANGELOG.md +57 -1
- package/CLI-HELP.md +97 -30
- package/README.md +2 -2
- package/bin/build.template.js +4 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/scripts/link-local-underpost-cli.sh +6 -0
- package/scripts/test-monitor.sh +86 -0
- package/src/cli/deploy.js +195 -46
- package/src/cli/env.js +1 -4
- package/src/cli/index.js +39 -0
- package/src/cli/monitor.js +184 -72
- package/src/cli/release.js +21 -6
- package/src/cli/repository.js +18 -4
- package/src/cli/run.js +22 -0
- package/src/db/mongo/MongooseDB.js +2 -1
- package/src/index.js +1 -1
- package/src/server/conf.js +33 -9
- package/src/server/runtime-status.js +235 -0
- package/src/server/start.js +11 -6
- package/test/deploy-monitor.test.js +108 -73
package/src/cli/monitor.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
334
|
-
*
|
|
335
|
-
*
|
|
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
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
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
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
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 =
|
|
513
|
+
const allPodsK8sReady = result.notReadyPods.length === 0;
|
|
514
|
+
if (allPodsK8sReady) emit('pod_ready');
|
|
416
515
|
|
|
417
|
-
|
|
418
|
-
|
|
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 =
|
|
540
|
+
const status = runtimeStatusCache.get(pod.NAME) || 'waiting for status';
|
|
424
541
|
const podStatus = pod.STATUS || 'Unknown';
|
|
425
|
-
const
|
|
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
|
-
//
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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`,
|
package/src/cli/release.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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`);
|
package/src/cli/repository.js
CHANGED
|
@@ -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
|
-
|
|
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} &&
|
|
625
|
-
shellExec(`cd ${destFolder} &&
|
|
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(`
|
|
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
|
|
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
package/src/server/conf.js
CHANGED
|
@@ -1086,13 +1086,9 @@ const buildPortProxyRouter = (
|
|
|
1086
1086
|
|
|
1087
1087
|
if (Object.keys(router).length === 0) return router;
|
|
1088
1088
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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###
|
|
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
|
};
|