underpost 3.2.21 → 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/CHANGELOG.md +38 -1
- package/CLI-HELP.md +3 -1
- package/README.md +2 -2
- package/bin/build.js +12 -1
- 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/test-monitor.sh +242 -78
- package/src/cli/deploy.js +5 -8
- package/src/cli/index.js +8 -0
- package/src/cli/monitor.js +114 -29
- package/src/cli/repository.js +3 -0
- package/src/cli/run.js +22 -4
- package/src/client/components/core/PanelForm.js +44 -44
- package/src/index.js +1 -1
- package/src/server/conf.js +58 -9
- package/src/server/ipfs-client.js +5 -3
- package/src/server/start.js +17 -5
- package/test/deploy-monitor.test.js +33 -5
package/src/cli/monitor.js
CHANGED
|
@@ -378,34 +378,77 @@ class UnderpostMonitor {
|
|
|
378
378
|
return deployStatusPort(deployId, env) ?? 3000;
|
|
379
379
|
},
|
|
380
380
|
/**
|
|
381
|
-
* Reads Phase-2 runtime
|
|
382
|
-
* through `kubectl port-forward` to the in-pod internal status endpoint.
|
|
381
|
+
* Reads Phase-2 runtime status from a single pod using the selected transport.
|
|
383
382
|
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
383
|
+
* - `exec` (default): `kubectl exec … underpost config get container-status`
|
|
384
|
+
* reads the env-file value. Synchronous, no background process — required
|
|
385
|
+
* for custom instances (cyberia-server/client) and the safe choice for
|
|
386
|
+
* CI/SSH. See `Deploy custom instance to K8S.md`.
|
|
387
|
+
* - `http`: port-forward to the in-pod `/_internal/status` endpoint served
|
|
388
|
+
* by the `underpost start` launcher (dd-* runtime deploys). Opt-in.
|
|
389
|
+
*
|
|
390
|
+
* Transport failures are reported as `{ ok: false }` and must never be read
|
|
391
|
+
* as success — they are retried, not promoted.
|
|
388
392
|
*
|
|
389
393
|
* @param {string} podName
|
|
390
394
|
* @param {string} namespace
|
|
391
395
|
* @param {number} internalPort
|
|
396
|
+
* @param {('http'|'exec')} [transport='exec']
|
|
392
397
|
* @returns {Promise<{ok: boolean, status?: (string|null), transportError?: string}>}
|
|
393
398
|
* @memberof UnderpostMonitor
|
|
394
399
|
*/
|
|
395
|
-
async readRuntimeStatus(podName, namespace, internalPort) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
+
async readRuntimeStatus(podName, namespace, internalPort, transport = 'exec') {
|
|
401
|
+
return transport === 'exec'
|
|
402
|
+
? Underpost.monitor.readRuntimeStatusViaExec(podName, namespace)
|
|
403
|
+
: Underpost.monitor.readRuntimeStatusViaHttp(podName, namespace, internalPort);
|
|
404
|
+
},
|
|
405
|
+
/**
|
|
406
|
+
* Phase-2 read over `kubectl exec` (env-file transport). Works for any pod
|
|
407
|
+
* whose image bakes the underpost CLI — notably custom instances that stamp
|
|
408
|
+
* `container-status` from `lifecycle.postStart`/`preStop` hooks.
|
|
409
|
+
* @param {string} podName
|
|
410
|
+
* @param {string} namespace
|
|
411
|
+
* @returns {{ok: boolean, status?: (string|null), transportError?: string}}
|
|
412
|
+
* @memberof UnderpostMonitor
|
|
413
|
+
*/
|
|
414
|
+
readRuntimeStatusViaExec(podName, namespace) {
|
|
415
|
+
try {
|
|
416
|
+
const raw = shellExec(
|
|
417
|
+
`sudo kubectl exec ${podName} -n ${namespace} -- sh -c 'underpost config get container-status --plain'`,
|
|
418
|
+
{ silent: true, disableLog: true, stdout: true, silentOnError: true },
|
|
419
|
+
);
|
|
420
|
+
const status = normalizeContainerStatus(raw ? raw.toString().trim() : '');
|
|
421
|
+
return status === undefined ? { ok: false, transportError: 'empty_status' } : { ok: true, status };
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return { ok: false, transportError: error?.code || error?.message || 'exec_failed' };
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
/**
|
|
427
|
+
* Phase-2 read over `kubectl port-forward` + HTTP `/_internal/status`.
|
|
428
|
+
*
|
|
429
|
+
* The local side of the tunnel MUST be an ephemeral free port: pinning it to
|
|
430
|
+
* internalPort collides with any host-local service on that number (e.g. a
|
|
431
|
+
* dev runtime on the same machine as the cluster), making port-forward fail
|
|
432
|
+
* to bind and every read return a false transport error.
|
|
433
|
+
*
|
|
434
|
+
* @param {string} podName
|
|
435
|
+
* @param {string} namespace
|
|
436
|
+
* @param {number} internalPort
|
|
437
|
+
* @returns {Promise<{ok: boolean, status?: (string|null), transportError?: string}>}
|
|
438
|
+
* @memberof UnderpostMonitor
|
|
439
|
+
*/
|
|
440
|
+
async readRuntimeStatusViaHttp(podName, namespace, internalPort) {
|
|
400
441
|
const override = parseInt(process.env.UNDERPOST_PF_LOCAL_PORT);
|
|
401
442
|
const localPort = Number.isNaN(override) ? await Underpost.monitor.findFreePort() : override;
|
|
402
443
|
const url = `http://127.0.0.1:${localPort}${INTERNAL_STATUS_PATH}`;
|
|
403
444
|
let portForward;
|
|
404
445
|
try {
|
|
405
|
-
// `exec`
|
|
406
|
-
//
|
|
446
|
+
// `exec` makes the tracked child the sudo/kubectl process (so kill
|
|
447
|
+
// reaches it); stdio is redirected to /dev/null so the tunnel never
|
|
448
|
+
// inherits — and therefore never holds open — a CI/SSH session's pipes,
|
|
449
|
+
// which would hang the job after a successful deploy.
|
|
407
450
|
portForward = shellExec(
|
|
408
|
-
`exec sudo kubectl port-forward pod/${podName} ${localPort}:${internalPort} -n ${namespace}`,
|
|
451
|
+
`exec sudo kubectl port-forward pod/${podName} ${localPort}:${internalPort} -n ${namespace} </dev/null >/dev/null 2>&1`,
|
|
409
452
|
{ async: true, silent: true, disableLog: true, silentOnError: true },
|
|
410
453
|
);
|
|
411
454
|
} catch (_) {
|
|
@@ -440,12 +483,26 @@ class UnderpostMonitor {
|
|
|
440
483
|
* two-phase state machine.
|
|
441
484
|
*
|
|
442
485
|
* Phase 1 (Kubernetes): pod `Ready` condition via `checkDeploymentReadyStatus`.
|
|
443
|
-
* Phase 2 (Runtime): `
|
|
444
|
-
*
|
|
486
|
+
* Phase 2 (Runtime): `container-status`, read via the selected transport.
|
|
487
|
+
*
|
|
488
|
+
* Two deployment shapes are supported via `options`:
|
|
489
|
+
* - `runtime` gate (default, dd-* deploys): the `underpost start` launcher
|
|
490
|
+
* stamps `running-deployment`. Success requires K8S Ready AND every pod
|
|
491
|
+
* reporting `running-deployment`.
|
|
492
|
+
* - `kubernetes` gate (custom instances, e.g. cyberia): the runtime is a
|
|
493
|
+
* bare binary; K8S `readinessProbe` (TCP) IS the running signal and
|
|
494
|
+
* `container-status` is stamped to `initializing`/`stopping` by lifecycle
|
|
495
|
+
* hooks. Success requires K8S Ready; the status read is used only for
|
|
496
|
+
* fast `error` detection and display.
|
|
497
|
+
*
|
|
498
|
+
* Phase-2 transport defaults to `exec` (`kubectl exec`, no background
|
|
499
|
+
* process). The `http` transport (`kubectl port-forward` → `/_internal/status`)
|
|
500
|
+
* is opt-in via `options.statusTransport='http'` or
|
|
501
|
+
* `UNDERPOST_STATUS_TRANSPORT=http`; it must not be used in CI/SSH sessions
|
|
502
|
+
* where a stray tunnel can hang the job.
|
|
445
503
|
*
|
|
446
|
-
* Contract:
|
|
447
|
-
* -
|
|
448
|
-
* before Kubernetes readiness.
|
|
504
|
+
* Contract (both shapes):
|
|
505
|
+
* - Runtime readiness is never declared before Kubernetes readiness.
|
|
449
506
|
* - An explicit runtime `error` (or a fatal pod status) transitions
|
|
450
507
|
* immediately to `failed` (throw → CD exit 1).
|
|
451
508
|
* - Transport failures never count as success and never advance state.
|
|
@@ -457,16 +514,27 @@ class UnderpostMonitor {
|
|
|
457
514
|
* @param {string} targetTraffic - Target traffic status for the deployment.
|
|
458
515
|
* @param {Array<string>} ignorePods - List of pod names to ignore.
|
|
459
516
|
* @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
|
|
517
|
+
* @param {object} [options] - Monitoring shape.
|
|
518
|
+
* @param {('runtime'|'kubernetes')} [options.readyGate='runtime'] - Running-signal owner.
|
|
519
|
+
* @param {('http'|'exec')} [options.statusTransport='http'] - Phase-2 read transport.
|
|
460
520
|
* @returns {object} - Object containing the ready status of the deployment.
|
|
461
521
|
* @memberof UnderpostMonitor
|
|
462
522
|
*/
|
|
463
|
-
async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
|
|
523
|
+
async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default', options = {}) {
|
|
464
524
|
const delayMs = parseInt(process.env.UNDERPOST_MONITOR_DELAY_MS) || 1000;
|
|
465
525
|
const maxIterations = parseInt(process.env.UNDERPOST_MONITOR_MAX_ITERATIONS) || 3000;
|
|
466
526
|
const deploymentId = `${deployId}-${env}-${targetTraffic}`;
|
|
467
527
|
const tag = `[${deploymentId}]`;
|
|
468
528
|
const expectedStatus = RUNTIME_STATUS.RUNNING;
|
|
469
|
-
const
|
|
529
|
+
const readyGate = options.readyGate === 'kubernetes' ? 'kubernetes' : 'runtime';
|
|
530
|
+
// Default to `exec`: a single synchronous `kubectl exec` read leaves no
|
|
531
|
+
// background process behind. The `http` transport spawns `kubectl
|
|
532
|
+
// port-forward` children that, if orphaned, inherit a CI/SSH session's
|
|
533
|
+
// stdio and hang the job after a successful deploy — opt in explicitly.
|
|
534
|
+
const statusTransport =
|
|
535
|
+
(options.statusTransport || process.env.UNDERPOST_STATUS_TRANSPORT) === 'http' ? 'http' : 'exec';
|
|
536
|
+
const internalPort =
|
|
537
|
+
statusTransport === 'http' ? await Underpost.monitor.deployInternalPort(deployId, env) : null;
|
|
470
538
|
const podErrorStates = ['error', 'crashloopbackoff', 'oomkilled', 'imagepullbackoff', 'errimagepull'];
|
|
471
539
|
|
|
472
540
|
const emit = (state, status) =>
|
|
@@ -478,7 +546,15 @@ class UnderpostMonitor {
|
|
|
478
546
|
timestamp: new Date().toISOString(),
|
|
479
547
|
});
|
|
480
548
|
|
|
481
|
-
logger.info('Deployment init', {
|
|
549
|
+
logger.info('Deployment init', {
|
|
550
|
+
deployId,
|
|
551
|
+
env,
|
|
552
|
+
targetTraffic,
|
|
553
|
+
namespace,
|
|
554
|
+
internalPort,
|
|
555
|
+
readyGate,
|
|
556
|
+
statusTransport,
|
|
557
|
+
});
|
|
482
558
|
emit('pending');
|
|
483
559
|
|
|
484
560
|
const runtimeStatusCache = new Map();
|
|
@@ -513,12 +589,12 @@ class UnderpostMonitor {
|
|
|
513
589
|
const allPodsK8sReady = result.notReadyPods.length === 0;
|
|
514
590
|
if (allPodsK8sReady) emit('pod_ready');
|
|
515
591
|
|
|
516
|
-
// Phase 2: runtime
|
|
517
|
-
// advance state nor count as success; explicit `error` is terminal.
|
|
592
|
+
// Phase 2: runtime status via the selected transport. Transport failures
|
|
593
|
+
// neither advance state nor count as success; explicit `error` is terminal.
|
|
518
594
|
let allRuntimeRead = true;
|
|
519
595
|
for (const pod of allPods) {
|
|
520
596
|
if (!pod?.NAME) continue;
|
|
521
|
-
const read = await Underpost.monitor.readRuntimeStatus(pod.NAME, namespace, internalPort);
|
|
597
|
+
const read = await Underpost.monitor.readRuntimeStatus(pod.NAME, namespace, internalPort, statusTransport);
|
|
522
598
|
if (!read.ok) {
|
|
523
599
|
allRuntimeRead = false;
|
|
524
600
|
emit('runtime_booting', `transport:${read.transportError}`);
|
|
@@ -526,6 +602,9 @@ class UnderpostMonitor {
|
|
|
526
602
|
}
|
|
527
603
|
const status = read.status;
|
|
528
604
|
if (status === RUNTIME_STATUS.ERROR) throw new Error(`Pod ${pod.NAME} reported runtime status=error`);
|
|
605
|
+
// Regression (advanced → empty/build) means a pod restarted. Under the
|
|
606
|
+
// kubernetes gate the runtime never advances past `initializing`, so
|
|
607
|
+
// only treat a drop to empty/build as a regression there.
|
|
529
608
|
if (advancedPods.has(pod.NAME) && (!status || status === RUNTIME_STATUS.BUILD))
|
|
530
609
|
throw new Error(`Pod ${pod.NAME} runtime status regressed (${status ?? 'empty'}) — pod likely restarted`);
|
|
531
610
|
if (status && status !== RUNTIME_STATUS.BUILD) advancedPods.add(pod.NAME);
|
|
@@ -533,8 +612,13 @@ class UnderpostMonitor {
|
|
|
533
612
|
emit('runtime_booting', status);
|
|
534
613
|
}
|
|
535
614
|
|
|
615
|
+
// Under the kubernetes gate the readinessProbe is the running signal, so
|
|
616
|
+
// K8S Ready alone confirms Phase 2; the status read above is kept only
|
|
617
|
+
// for `error` fast-fail and display.
|
|
536
618
|
const allRuntimeReady =
|
|
537
|
-
|
|
619
|
+
readyGate === 'kubernetes'
|
|
620
|
+
? true
|
|
621
|
+
: allRuntimeRead && allPods.every((pod) => runtimeStatusCache.get(pod.NAME) === expectedStatus);
|
|
538
622
|
|
|
539
623
|
for (const pod of allPods) {
|
|
540
624
|
const status = runtimeStatusCache.get(pod.NAME) || 'waiting for status';
|
|
@@ -550,11 +634,12 @@ class UnderpostMonitor {
|
|
|
550
634
|
);
|
|
551
635
|
}
|
|
552
636
|
|
|
553
|
-
// Terminal success requires
|
|
637
|
+
// Terminal success requires both phases. runtime_ready cannot precede
|
|
554
638
|
// Kubernetes readiness.
|
|
555
639
|
if (allPodsK8sReady && allRuntimeReady) {
|
|
556
|
-
|
|
557
|
-
|
|
640
|
+
const readySignal = readyGate === 'kubernetes' ? 'K8S readinessProbe' : `runtime ${expectedStatus}`;
|
|
641
|
+
emit('runtime_ready', readyGate === 'kubernetes' ? 'k8s-ready' : expectedStatus);
|
|
642
|
+
logger.info(`${tag} | Deployment ready (K8S Ready + ${readySignal})`);
|
|
558
643
|
return result;
|
|
559
644
|
}
|
|
560
645
|
|
package/src/cli/repository.js
CHANGED
|
@@ -8,6 +8,7 @@ import dotenv from 'dotenv';
|
|
|
8
8
|
import { commitData } from '../client/components/core/CommonJs.js';
|
|
9
9
|
import { pbcopy, shellCd, shellExec } from '../server/process.js';
|
|
10
10
|
import { actionInitLog, loggerFactory } from '../server/logger.js';
|
|
11
|
+
import path from 'path';
|
|
11
12
|
import fs from 'fs-extra';
|
|
12
13
|
import {
|
|
13
14
|
getNpmRootPath,
|
|
@@ -1738,6 +1739,8 @@ Prevent build private config repo.`,
|
|
|
1738
1739
|
if (!fs.existsSync(repoPath)) {
|
|
1739
1740
|
shellExec(`cd .. && underpost clone ${gitUri}`, { silent: true });
|
|
1740
1741
|
} else {
|
|
1742
|
+
const repoAbsPath = path.resolve(repoPath);
|
|
1743
|
+
shellExec(`git config --global --add safe.directory '${repoAbsPath}'`);
|
|
1741
1744
|
shellExec(`cd ${repoPath} && git checkout . && git clean -f -d && underpost pull . ${gitUri}`, {
|
|
1742
1745
|
silent: true,
|
|
1743
1746
|
});
|
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' : ''} --
|
|
228
|
+
`${baseCommand} cluster${options.dev ? ' --dev' : ''} --mongodb --service-host ${mongoHosts.join(
|
|
227
229
|
',',
|
|
228
230
|
)} --pull-image`,
|
|
229
231
|
);
|
|
@@ -1007,8 +1009,19 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1007
1009
|
// pathRewritePolicy,
|
|
1008
1010
|
});
|
|
1009
1011
|
if (options.tls) {
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
+
}
|
|
1012
1025
|
}
|
|
1013
1026
|
// console.log(proxyYaml);
|
|
1014
1027
|
shellExec(`kubectl delete HTTPProxy ${_host} --namespace ${options.namespace} --ignore-not-found`);
|
|
@@ -1077,6 +1090,7 @@ EOF
|
|
|
1077
1090
|
// Examples images:
|
|
1078
1091
|
// `underpost/underpost-engine:${Underpost.version}`
|
|
1079
1092
|
// `localhost/rockylinux9-underpost:${Underpost.version}`
|
|
1093
|
+
if (options.imageName) _image = options.imageName;
|
|
1080
1094
|
if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
|
|
1081
1095
|
|
|
1082
1096
|
if (_image && !_image.startsWith('localhost'))
|
|
@@ -1172,12 +1186,16 @@ EOF
|
|
|
1172
1186
|
`,
|
|
1173
1187
|
{ disableLog: true },
|
|
1174
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`.
|
|
1175
1192
|
const { ready, readyPods } = await Underpost.monitor.monitorReadyRunner(
|
|
1176
1193
|
_deployId,
|
|
1177
1194
|
env,
|
|
1178
1195
|
targetTraffic,
|
|
1179
1196
|
ignorePods,
|
|
1180
1197
|
options.namespace,
|
|
1198
|
+
{ readyGate: 'kubernetes', statusTransport: 'exec' },
|
|
1181
1199
|
);
|
|
1182
1200
|
|
|
1183
1201
|
if (!ready) {
|
|
@@ -1187,7 +1205,7 @@ EOF
|
|
|
1187
1205
|
shellExec(
|
|
1188
1206
|
`${baseCommand} run${baseClusterCommand} --namespace ${options.namespace}` +
|
|
1189
1207
|
`${options.nodeName ? ` --node-name ${options.nodeName}` : ''}` +
|
|
1190
|
-
`${options.tls ? ` --tls` : ''}` +
|
|
1208
|
+
`${options.tls ? ` --tls ${options.test ? '--test' : ''}` : ''}` +
|
|
1191
1209
|
` instance-promote '${path}'`,
|
|
1192
1210
|
);
|
|
1193
1211
|
}
|
|
@@ -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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
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
|
-
|
|
493
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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,
|
package/src/index.js
CHANGED
package/src/server/conf.js
CHANGED
|
@@ -1856,6 +1856,7 @@ const syncPrivateConf = (deployId, extraPaths = []) => {
|
|
|
1856
1856
|
if (!fs.existsSync(privateRepoPath)) {
|
|
1857
1857
|
shellExec(`cd .. && underpost clone ${privateGitUri}`, { silent: true });
|
|
1858
1858
|
} else {
|
|
1859
|
+
shellExec(`git config --global --add safe.directory '${dir.resolve(privateRepoPath)}'`);
|
|
1859
1860
|
shellExec(`cd ${privateRepoPath} && git checkout . && git clean -f -d && underpost pull . ${privateGitUri}`, {
|
|
1860
1861
|
silent: true,
|
|
1861
1862
|
});
|
|
@@ -1941,15 +1942,9 @@ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-te
|
|
|
1941
1942
|
)
|
|
1942
1943
|
).filter((p) => !p.startsWith('.git'));
|
|
1943
1944
|
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
} else {
|
|
1948
|
-
shellExec(`cd ${toPath} && git reset && git checkout . && git clean -f -d`);
|
|
1949
|
-
shellExec(`node bin pull ${toPath} ${githubUsername}/pwa-microservices-template`);
|
|
1950
|
-
shellExec(`sudo rm -rf ${toPath}/engine-private`);
|
|
1951
|
-
shellExec(`sudo rm -rf ${toPath}/logs`);
|
|
1952
|
-
}
|
|
1945
|
+
fs.removeSync(`${githubUsername}/pwa-microservices-template`);
|
|
1946
|
+
shellExec(`cd .. && node engine/bin clone ${githubUsername}/pwa-microservices-template`);
|
|
1947
|
+
|
|
1953
1948
|
shellExec(`cd ${toPath} && git config core.filemode false`);
|
|
1954
1949
|
|
|
1955
1950
|
for (const copyPath of sourceFiles) {
|
|
@@ -2052,6 +2047,59 @@ git add .`);
|
|
|
2052
2047
|
}
|
|
2053
2048
|
};
|
|
2054
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
|
+
|
|
2055
2103
|
export {
|
|
2056
2104
|
Config,
|
|
2057
2105
|
loadConf,
|
|
@@ -2104,4 +2152,5 @@ export {
|
|
|
2104
2152
|
syncDeployIdSources,
|
|
2105
2153
|
buildTemplate,
|
|
2106
2154
|
updatePrivateTemplateRepo,
|
|
2155
|
+
updatePrivateEngineTestRepo,
|
|
2107
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 ||
|
|
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
|
// ─────────────────────────────────────────────────────────
|
package/src/server/start.js
CHANGED
|
@@ -152,10 +152,16 @@ class UnderpostStartUp {
|
|
|
152
152
|
// observable through every lifecycle phase, including build and init. Bind
|
|
153
153
|
// the deployment-resolved port so it always matches the monitor's target.
|
|
154
154
|
startInternalStatusServer(deployStatusPort(deployId, env));
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
try {
|
|
156
|
+
setRuntimeStatus(deployId, env, RUNTIME_STATUS.BUILD);
|
|
157
|
+
if (options.build === true) await Underpost.start.build(deployId, env, options);
|
|
158
|
+
setRuntimeStatus(deployId, env, RUNTIME_STATUS.INIT);
|
|
159
|
+
if (options.run === true) await Underpost.start.run(deployId, env, options);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error('Deployment build/init failed', { deployId, env, message: error?.message });
|
|
162
|
+
setRuntimeStatus(deployId, env, RUNTIME_STATUS.ERROR);
|
|
163
|
+
if (!Underpost.env.isInsideContainer()) throw error;
|
|
164
|
+
}
|
|
159
165
|
},
|
|
160
166
|
/**
|
|
161
167
|
* Run itc-scripts and builds client bundle.
|
|
@@ -167,6 +173,8 @@ class UnderpostStartUp {
|
|
|
167
173
|
* @param {boolean} options.skipFullBuild - Whether to skip building the full client bundle.
|
|
168
174
|
* @param {boolean} options.pullBundle - When true, download pre-built client bundle from Cloudinary via pull-bundle (must be pushed first with push-bundle).
|
|
169
175
|
* This flag is independent of skipFullBuild: it can be combined with skipFullBuild or used alone.
|
|
176
|
+
* @param {boolean} options.privateTestRepo - When true, clone `engine-test-<id>` (the private test source repo
|
|
177
|
+
* published by `node bin/build <deployId> --update-private`) instead of the production `engine-<id>` repo.
|
|
170
178
|
* @memberof UnderpostStartUp
|
|
171
179
|
*/
|
|
172
180
|
async build(
|
|
@@ -175,7 +183,11 @@ class UnderpostStartUp {
|
|
|
175
183
|
options = { underpostQuicklyInstall: false, skipPullBase: false, skipFullBuild: false, pullBundle: false },
|
|
176
184
|
) {
|
|
177
185
|
const buildBasePath = `/home/dd`;
|
|
178
|
-
|
|
186
|
+
// `--private-test-repo` clones the isolated test source repo published by
|
|
187
|
+
// `node bin/build <deployId> --update-private`, instead of the production one.
|
|
188
|
+
const repoName = options?.privateTestRepo
|
|
189
|
+
? `engine-test-${deployId.split('-')[1]}`
|
|
190
|
+
: `engine-${deployId.split('-')[1]}`;
|
|
179
191
|
if (!options.skipPullBase) {
|
|
180
192
|
shellExec(`cd ${buildBasePath} && underpost clone ${process.env.GITHUB_USERNAME}/${repoName}`);
|
|
181
193
|
shellExec(`mkdir -p ${buildBasePath}/engine`);
|