underpost 3.1.3 → 3.2.2
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/.env.example +0 -2
- package/.github/workflows/ghpkg.ci.yml +4 -4
- package/.github/workflows/npmpkg.ci.yml +28 -11
- package/.github/workflows/publish.ci.yml +6 -0
- package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -5
- package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
- package/.github/workflows/release.cd.yml +13 -8
- package/CHANGELOG.md +396 -1
- package/CLI-HELP.md +53 -6
- package/Dockerfile +4 -2
- package/README.md +3 -2
- package/bin/build.js +18 -12
- package/bin/deploy.js +177 -124
- package/bin/file.js +3 -0
- package/conf.js +3 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +88 -74
- package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/nodemon.json +1 -1
- package/package.json +22 -15
- package/scripts/rhel-grpc-setup.sh +56 -0
- package/src/api/file/file.ref.json +18 -0
- package/src/api/user/user.service.js +8 -7
- package/src/cli/cluster.js +7 -7
- package/src/cli/db.js +726 -825
- package/src/cli/deploy.js +151 -93
- package/src/cli/env.js +19 -0
- package/src/cli/fs.js +5 -2
- package/src/cli/index.js +45 -2
- package/src/cli/kubectl.js +211 -0
- package/src/cli/release.js +284 -0
- package/src/cli/repository.js +434 -75
- package/src/cli/run.js +189 -34
- package/src/cli/secrets.js +73 -0
- package/src/cli/test.js +3 -3
- package/src/client/Default.index.js +3 -4
- package/src/client/components/core/AppStore.js +69 -0
- package/src/client/components/core/CalendarCore.js +2 -2
- package/src/client/components/core/DropDown.js +137 -17
- package/src/client/components/core/Keyboard.js +2 -2
- package/src/client/components/core/LogIn.js +2 -2
- package/src/client/components/core/LogOut.js +2 -2
- package/src/client/components/core/Modal.js +0 -1
- package/src/client/components/core/Panel.js +0 -1
- package/src/client/components/core/PanelForm.js +19 -19
- package/src/client/components/core/SocketIo.js +82 -29
- package/src/client/components/core/SocketIoHandler.js +75 -0
- package/src/client/components/core/Stream.js +143 -95
- package/src/client/components/core/Webhook.js +40 -7
- package/src/client/components/default/AppStoreDefault.js +5 -0
- package/src/client/components/default/LogInDefault.js +3 -3
- package/src/client/components/default/LogOutDefault.js +2 -2
- package/src/client/components/default/MenuDefault.js +5 -5
- package/src/client/components/default/SocketIoDefault.js +3 -51
- package/src/client/services/core/core.service.js +20 -8
- package/src/client/services/user/user.management.js +2 -2
- package/src/index.js +24 -1
- package/src/runtime/express/Dockerfile +4 -0
- package/src/runtime/express/Express.js +18 -1
- package/src/runtime/lampp/Dockerfile +13 -2
- package/src/runtime/lampp/Lampp.js +27 -4
- package/src/runtime/wp/Dockerfile +68 -0
- package/src/runtime/wp/Wp.js +639 -0
- package/src/server/auth.js +24 -1
- package/src/server/backup.js +57 -23
- package/src/server/client-build-docs.js +9 -2
- package/src/server/client-build.js +31 -31
- package/src/server/client-formatted.js +109 -57
- package/src/server/cron.js +23 -18
- package/src/server/ipfs-client.js +24 -1
- package/src/server/peer.js +8 -0
- package/src/server/runtime.js +25 -1
- package/src/server/start.js +3 -2
- package/src/ws/IoInterface.js +1 -10
- package/src/ws/IoServer.js +14 -33
- package/src/ws/core/channels/core.ws.chat.js +65 -20
- package/src/ws/core/channels/core.ws.mailer.js +113 -32
- package/src/ws/core/channels/core.ws.stream.js +90 -31
- package/src/ws/core/core.ws.connection.js +12 -33
- package/src/ws/core/core.ws.emit.js +10 -26
- package/src/ws/core/core.ws.server.js +25 -58
- package/src/ws/default/channels/default.ws.main.js +53 -12
- package/src/ws/default/default.ws.connection.js +26 -13
- package/src/ws/default/default.ws.server.js +30 -12
- package/src/client/components/default/ElementsDefault.js +0 -38
- package/src/ws/core/management/core.ws.chat.js +0 -8
- package/src/ws/core/management/core.ws.mailer.js +0 -16
- package/src/ws/core/management/core.ws.stream.js +0 -8
- package/src/ws/default/management/default.ws.main.js +0 -8
package/src/cli/run.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* @namespace UnderpostRun
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { daemonProcess, getTerminalPid, shellCd, shellExec } from '../server/process.js';
|
|
7
|
+
import { daemonProcess, getTerminalPid, pbcopy, shellCd, shellExec } from '../server/process.js';
|
|
8
|
+
import crypto from 'crypto';
|
|
8
9
|
import {
|
|
9
10
|
awaitDeployMonitor,
|
|
10
11
|
buildKindPorts,
|
|
@@ -17,12 +18,31 @@ import {
|
|
|
17
18
|
import { actionInitLog, loggerFactory } from '../server/logger.js';
|
|
18
19
|
|
|
19
20
|
import fs from 'fs-extra';
|
|
21
|
+
import net from 'net';
|
|
20
22
|
import { range, setPad, timer } from '../client/components/core/CommonJs.js';
|
|
21
23
|
|
|
22
24
|
import os from 'os';
|
|
23
25
|
import Underpost from '../index.js';
|
|
24
26
|
import dotenv from 'dotenv';
|
|
25
27
|
|
|
28
|
+
const waitForPort = (port, host = '127.0.0.1', { maxAttempts = 30, interval = 2000 } = {}) =>
|
|
29
|
+
new Promise((resolve, reject) => {
|
|
30
|
+
let attempts = 0;
|
|
31
|
+
const tryConnect = () => {
|
|
32
|
+
attempts++;
|
|
33
|
+
const socket = net.createConnection({ port, host }, () => {
|
|
34
|
+
socket.destroy();
|
|
35
|
+
resolve();
|
|
36
|
+
});
|
|
37
|
+
socket.on('error', () => {
|
|
38
|
+
socket.destroy();
|
|
39
|
+
if (attempts >= maxAttempts) return reject(new Error(`Port ${port} not ready after ${maxAttempts} attempts`));
|
|
40
|
+
setTimeout(tryConnect, interval);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
tryConnect();
|
|
44
|
+
});
|
|
45
|
+
|
|
26
46
|
const logger = loggerFactory(import.meta);
|
|
27
47
|
|
|
28
48
|
/**
|
|
@@ -154,6 +174,8 @@ const DEFAULT_OPTION = {
|
|
|
154
174
|
createJobNow: false,
|
|
155
175
|
fromNCommit: 0,
|
|
156
176
|
hostAliases: '',
|
|
177
|
+
gitClean: false,
|
|
178
|
+
copy: false,
|
|
157
179
|
};
|
|
158
180
|
|
|
159
181
|
/**
|
|
@@ -246,8 +268,15 @@ class UnderpostRun {
|
|
|
246
268
|
const ports = '6379,27017';
|
|
247
269
|
shellExec(`node bin run kill '${ports}'`);
|
|
248
270
|
shellExec(`node bin run dev-cluster --dev --expose --namespace ${options.namespace}`, { async: true });
|
|
249
|
-
|
|
250
|
-
|
|
271
|
+
logger.info('Waiting for port-forward services to be ready...');
|
|
272
|
+
try {
|
|
273
|
+
await Promise.all([waitForPort(27017), waitForPort(6379)]);
|
|
274
|
+
logger.info('Port-forward services are ready');
|
|
275
|
+
} catch (err) {
|
|
276
|
+
logger.error('Port-forward services failed to become ready', { error: err.message });
|
|
277
|
+
shellExec(`node bin run kill '${ports}'`);
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
251
280
|
shellExec(`node bin metadata --generate ${path}`);
|
|
252
281
|
shellExec(`node bin db --dev --clean-fs-collection dd`);
|
|
253
282
|
shellExec(`node bin run kill '${ports}'`);
|
|
@@ -373,8 +402,12 @@ class UnderpostRun {
|
|
|
373
402
|
}
|
|
374
403
|
shellExec(`${baseCommand} run pull`);
|
|
375
404
|
|
|
376
|
-
// Capture last N commit messages for propagation
|
|
377
|
-
|
|
405
|
+
// Capture last N commit messages for propagation.
|
|
406
|
+
// When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
|
|
407
|
+
const fromN =
|
|
408
|
+
options.fromNCommit && parseInt(options.fromNCommit) > 0
|
|
409
|
+
? parseInt(options.fromNCommit)
|
|
410
|
+
: Underpost.repo.getUnpushedCount('.').count;
|
|
378
411
|
const message = shellExec(`node bin cmt --changelog ${fromN} --changelog-no-hash`, {
|
|
379
412
|
silent: true,
|
|
380
413
|
stdout: true,
|
|
@@ -423,6 +456,40 @@ class UnderpostRun {
|
|
|
423
456
|
});
|
|
424
457
|
},
|
|
425
458
|
|
|
459
|
+
/**
|
|
460
|
+
* @method template-deploy-local
|
|
461
|
+
* @description Similar to `template-deploy` but runs the workflow locally without dispatching GitHub Actions. It pulls the latest changes, pushes to GitHub, builds the template, and optionally triggers a local release with CI push.
|
|
462
|
+
* @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
|
|
463
|
+
* @param {Object} options - The default underpost runner options for customizing workflow
|
|
464
|
+
* @memberof UnderpostRun
|
|
465
|
+
*/
|
|
466
|
+
'template-deploy-local': async (path, options = DEFAULT_OPTION) => {
|
|
467
|
+
const baseCommand = options.dev ? 'node bin' : 'underpost';
|
|
468
|
+
shellExec(`npm run security:secrets`);
|
|
469
|
+
const reportPath = './gitleaks-report.json';
|
|
470
|
+
if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
|
|
471
|
+
logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
shellExec(`${baseCommand} run pull`);
|
|
475
|
+
|
|
476
|
+
// Capture last N commit messages from the engine repo.
|
|
477
|
+
// When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
|
|
478
|
+
const fromN =
|
|
479
|
+
options.fromNCommit && parseInt(options.fromNCommit) > 0
|
|
480
|
+
? parseInt(options.fromNCommit)
|
|
481
|
+
: Underpost.repo.getUnpushedCount('.').count;
|
|
482
|
+
const rawMessage = shellExec(`node bin cmt --changelog ${fromN} --changelog-no-hash`, {
|
|
483
|
+
silent: true,
|
|
484
|
+
stdout: true,
|
|
485
|
+
}).trim();
|
|
486
|
+
const sanitizedMessage = Underpost.repo.sanitizeChangelogMessage(rawMessage);
|
|
487
|
+
|
|
488
|
+
const { triggerCmd } = path
|
|
489
|
+
? await Underpost.release.ci(path, sanitizedMessage, options)
|
|
490
|
+
: await Underpost.release.pwa(sanitizedMessage, options);
|
|
491
|
+
pbcopy(triggerCmd + ' && cd /home/dd/engine');
|
|
492
|
+
},
|
|
426
493
|
/**
|
|
427
494
|
* @method template-deploy-image
|
|
428
495
|
* @description Dispatches the Docker image CI workflow for the `engine` repository.
|
|
@@ -438,6 +505,21 @@ class UnderpostRun {
|
|
|
438
505
|
inputs: {},
|
|
439
506
|
});
|
|
440
507
|
},
|
|
508
|
+
/**
|
|
509
|
+
* @method docker-image
|
|
510
|
+
* @description Dispatches the Docker image CI workflow (`docker-image.ci.yml`) for the `engine` repository via `workflow_dispatch`.
|
|
511
|
+
* @param {string} path - The input value, identifier, or path for the operation.
|
|
512
|
+
* @param {Object} options - The default underpost runner options for customizing workflow
|
|
513
|
+
* @memberof UnderpostRun
|
|
514
|
+
*/
|
|
515
|
+
'docker-image': (path, options = DEFAULT_OPTION) => {
|
|
516
|
+
Underpost.repo.dispatchWorkflow({
|
|
517
|
+
repo: `${process.env.GITHUB_USERNAME}/engine`,
|
|
518
|
+
workflowFile: 'docker-image.ci.yml',
|
|
519
|
+
ref: 'master',
|
|
520
|
+
inputs: {},
|
|
521
|
+
});
|
|
522
|
+
},
|
|
441
523
|
/**
|
|
442
524
|
* @method clean
|
|
443
525
|
* @description Changes directory to the provided path (defaulting to `/home/dd/engine`) and runs `node bin/deploy clean-core-repo`.
|
|
@@ -605,20 +687,22 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
605
687
|
const cmdString = options.cmd
|
|
606
688
|
? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
|
|
607
689
|
: '';
|
|
690
|
+
const clusterFlag = options.k3s ? ' --k3s' : options.kind ? ' --kind' : ' --kubeadm';
|
|
691
|
+
const gitCleanFlag = options.gitClean ? ' --git-clean' : '';
|
|
608
692
|
|
|
609
693
|
shellExec(
|
|
610
|
-
`${baseCommand} deploy --
|
|
694
|
+
`${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
|
|
611
695
|
image ? ` --image ${image}` : ''
|
|
612
696
|
}${versions ? ` --versions ${versions}` : ''}${
|
|
613
697
|
options.namespace ? ` --namespace ${options.namespace}` : ''
|
|
614
|
-
}${timeoutFlags}${cmdString} ${deployId} ${env}`,
|
|
698
|
+
}${timeoutFlags}${cmdString}${gitCleanFlag} ${deployId} ${env}`,
|
|
615
699
|
);
|
|
616
700
|
|
|
617
701
|
if (isDeployRunnerContext(path, options)) {
|
|
618
702
|
shellExec(
|
|
619
|
-
`${baseCommand} deploy
|
|
703
|
+
`${baseCommand} deploy${clusterFlag}${cmdString} --replicas ${replicas} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
|
|
620
704
|
options.namespace ? ` --namespace ${options.namespace}` : ''
|
|
621
|
-
}${timeoutFlags}`,
|
|
705
|
+
}${timeoutFlags}${gitCleanFlag}`,
|
|
622
706
|
);
|
|
623
707
|
if (!targetTraffic)
|
|
624
708
|
targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
|
|
@@ -830,6 +914,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
830
914
|
const confInstances = JSON.parse(
|
|
831
915
|
fs.readFileSync(`./engine-private/conf/${deployId}/conf.instances.json`, 'utf8'),
|
|
832
916
|
);
|
|
917
|
+
let promotedTraffic = '';
|
|
833
918
|
for (const instance of confInstances) {
|
|
834
919
|
let {
|
|
835
920
|
id: _id,
|
|
@@ -849,6 +934,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
849
934
|
namespace: options.namespace,
|
|
850
935
|
});
|
|
851
936
|
const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
|
|
937
|
+
promotedTraffic = targetTraffic;
|
|
852
938
|
let proxyYaml =
|
|
853
939
|
Underpost.deploy.baseProxyYamlFactory({ host: _host, env: options.tls ? 'production' : env, options }) +
|
|
854
940
|
Underpost.deploy.deploymentYamlServiceFactory({
|
|
@@ -874,6 +960,18 @@ EOF
|
|
|
874
960
|
{ disableLog: true },
|
|
875
961
|
);
|
|
876
962
|
}
|
|
963
|
+
// Refresh the gRPC service to ensure it points to the parent deploy's current traffic.
|
|
964
|
+
if (promotedTraffic) {
|
|
965
|
+
const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
|
|
966
|
+
const grpcServicePath = Underpost.deploy.buildGrpcServiceManifest({
|
|
967
|
+
deployId,
|
|
968
|
+
env,
|
|
969
|
+
confServer: loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
|
|
970
|
+
namespace: options.namespace,
|
|
971
|
+
traffic: [parentTraffic],
|
|
972
|
+
});
|
|
973
|
+
if (grpcServicePath) shellExec(`kubectl apply -f ${grpcServicePath} -n ${options.namespace}`);
|
|
974
|
+
}
|
|
877
975
|
},
|
|
878
976
|
|
|
879
977
|
/**
|
|
@@ -913,12 +1011,12 @@ EOF
|
|
|
913
1011
|
// `localhost/rockylinux9-underpost:${Underpost.version}`
|
|
914
1012
|
if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
|
|
915
1013
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}
|
|
1014
|
+
Underpost.image.pullDockerHubImage({
|
|
1015
|
+
dockerhubImage: _image,
|
|
1016
|
+
kind: options.kind || (!options.nodeName && !options.kubeadm && !options.k3s),
|
|
1017
|
+
kubeadm: options.nodeName || options.kubeadm,
|
|
1018
|
+
k3s: options.k3s,
|
|
1019
|
+
});
|
|
922
1020
|
|
|
923
1021
|
const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
|
|
924
1022
|
hostTest: _host,
|
|
@@ -927,7 +1025,7 @@ EOF
|
|
|
927
1025
|
|
|
928
1026
|
const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
|
|
929
1027
|
const podId = `${_deployId}-${env}-${targetTraffic}`;
|
|
930
|
-
const ignorePods = Underpost.
|
|
1028
|
+
const ignorePods = Underpost.kubectl.get(podId, 'pods', options.namespace).map((p) => p.NAME);
|
|
931
1029
|
Underpost.deploy.configMap(env, options.namespace);
|
|
932
1030
|
shellExec(`kubectl delete service ${podId}-service --namespace ${options.namespace} --ignore-not-found`);
|
|
933
1031
|
shellExec(`kubectl delete deployment ${podId} --namespace ${options.namespace} --ignore-not-found`);
|
|
@@ -939,7 +1037,30 @@ EOF
|
|
|
939
1037
|
env,
|
|
940
1038
|
version: targetTraffic,
|
|
941
1039
|
nodeName: options.nodeName,
|
|
1040
|
+
clusterContext: options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind',
|
|
1041
|
+
gitClean: options.gitClean || false,
|
|
942
1042
|
});
|
|
1043
|
+
// Regenerate the parent deploy's gRPC ClusterIP service pointing to the
|
|
1044
|
+
// parent's current traffic colour and apply it before the instance pod starts so
|
|
1045
|
+
// DNS is resolvable the moment the pod boots.
|
|
1046
|
+
const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
|
|
1047
|
+
const grpcServicePath = Underpost.deploy.buildGrpcServiceManifest({
|
|
1048
|
+
deployId,
|
|
1049
|
+
env,
|
|
1050
|
+
confServer: loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
|
|
1051
|
+
namespace: options.namespace,
|
|
1052
|
+
traffic: [targetTraffic],
|
|
1053
|
+
host: _host,
|
|
1054
|
+
});
|
|
1055
|
+
if (grpcServicePath) shellExec(`kubectl apply -f ${grpcServicePath} -n ${options.namespace}`);
|
|
1056
|
+
|
|
1057
|
+
const resolvedCmd = _cmd[env].map((c) =>
|
|
1058
|
+
c.replaceAll(
|
|
1059
|
+
'{{grpc-service-dns}}',
|
|
1060
|
+
`${deployId}-grpc-service-${env}-${parentTraffic}.${options.namespace || 'default'}.svc.cluster.local:50051`,
|
|
1061
|
+
),
|
|
1062
|
+
);
|
|
1063
|
+
|
|
943
1064
|
let deploymentYaml = `---
|
|
944
1065
|
${Underpost.deploy
|
|
945
1066
|
.deploymentYamlPartsFactory({
|
|
@@ -951,7 +1072,7 @@ ${Underpost.deploy
|
|
|
951
1072
|
image: _image,
|
|
952
1073
|
namespace: options.namespace,
|
|
953
1074
|
volumes: _volumes,
|
|
954
|
-
cmd:
|
|
1075
|
+
cmd: resolvedCmd,
|
|
955
1076
|
})
|
|
956
1077
|
.replace('{{ports}}', buildKindPorts(_fromPort, _toPort))}
|
|
957
1078
|
`;
|
|
@@ -997,7 +1118,7 @@ EOF
|
|
|
997
1118
|
* @memberof UnderpostRun
|
|
998
1119
|
*/
|
|
999
1120
|
'ls-deployments': async (path, options = DEFAULT_OPTION) => {
|
|
1000
|
-
console.table(await Underpost.
|
|
1121
|
+
console.table(await Underpost.kubectl.get(path, 'deployments', options.namespace));
|
|
1001
1122
|
},
|
|
1002
1123
|
|
|
1003
1124
|
/**
|
|
@@ -1091,15 +1212,13 @@ EOF
|
|
|
1091
1212
|
'db-client': async (path, options = DEFAULT_OPTION) => {
|
|
1092
1213
|
const { underpostRoot } = options;
|
|
1093
1214
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
}
|
|
1101
|
-
// For kubeadm/k3s, ensure it's available for containerd
|
|
1102
|
-
shellExec(`sudo crictl pull ${image}`);
|
|
1215
|
+
Underpost.image.pullDockerHubImage({
|
|
1216
|
+
dockerhubImage: 'adminer',
|
|
1217
|
+
version: '4.7.6-standalone',
|
|
1218
|
+
kind: options.kind,
|
|
1219
|
+
kubeadm: options.kubeadm,
|
|
1220
|
+
k3s: options.k3s,
|
|
1221
|
+
});
|
|
1103
1222
|
|
|
1104
1223
|
shellExec(`kubectl delete deployment adminer -n ${options.namespace} --ignore-not-found`);
|
|
1105
1224
|
shellExec(`kubectl apply -k ${underpostRoot}/manifests/deployment/adminer/. -n ${options.namespace}`);
|
|
@@ -1662,7 +1781,7 @@ EOF
|
|
|
1662
1781
|
: [
|
|
1663
1782
|
`npm install -g npm@11.2.0`,
|
|
1664
1783
|
`npm install -g underpost`,
|
|
1665
|
-
`${baseCommand} secret underpost --create-from-
|
|
1784
|
+
`${baseCommand} secret underpost --create-from-env`,
|
|
1666
1785
|
`${baseCommand} start --build --run ${deployId} ${env}`,
|
|
1667
1786
|
];
|
|
1668
1787
|
shellExec(`node bin run sync${baseClusterCommand} --deploy-id-cron-jobs none dd-test --cmd "${cmd}"`);
|
|
@@ -1693,7 +1812,7 @@ EOF
|
|
|
1693
1812
|
|
|
1694
1813
|
const { close } = await (async () => {
|
|
1695
1814
|
const checkAwaitPath = '/await';
|
|
1696
|
-
while (!Underpost.
|
|
1815
|
+
while (!Underpost.kubectl.existsFile({ podName, path: checkAwaitPath })) {
|
|
1697
1816
|
logger.info('monitor', checkAwaitPath);
|
|
1698
1817
|
await timer(1000);
|
|
1699
1818
|
}
|
|
@@ -1720,7 +1839,7 @@ EOF
|
|
|
1720
1839
|
logger.info('monitor', checkPath);
|
|
1721
1840
|
{
|
|
1722
1841
|
const checkAwaitPath = `/home/dd/docs${checkPath}`;
|
|
1723
|
-
while (!Underpost.
|
|
1842
|
+
while (!Underpost.kubectl.existsFile({ podName, path: checkAwaitPath })) {
|
|
1724
1843
|
logger.info('waiting for', checkAwaitPath);
|
|
1725
1844
|
await timer(1000);
|
|
1726
1845
|
}
|
|
@@ -1786,7 +1905,8 @@ EOF
|
|
|
1786
1905
|
|
|
1787
1906
|
shellCd(dir);
|
|
1788
1907
|
|
|
1789
|
-
|
|
1908
|
+
Underpost.repo.initLocalRepo({ path: dir });
|
|
1909
|
+
shellExec(`git add . && git commit -m "Base implementation"`);
|
|
1790
1910
|
shellExec(`chmod +x ./replace_params.sh`);
|
|
1791
1911
|
shellExec(`chmod +x ./build.sh`);
|
|
1792
1912
|
|
|
@@ -1831,11 +1951,46 @@ EOF
|
|
|
1831
1951
|
* @param {Object} options - The default underpost runner options for customizing workflow
|
|
1832
1952
|
* @memberof UnderpostRun
|
|
1833
1953
|
*/
|
|
1954
|
+
/**
|
|
1955
|
+
* @method generate-pass
|
|
1956
|
+
* @description Generates a cryptographically secure random password that satisfies all validatePassword
|
|
1957
|
+
* constraints (lowercase, uppercase, digit, special char, min 8 chars). Logs the plain password
|
|
1958
|
+
* to the console or, when `--copy` is set, copies it to the clipboard via pbcopy.
|
|
1959
|
+
* @param {string} path - Optional password length (default: 16).
|
|
1960
|
+
* @param {Object} options - The default underpost runner options for customizing workflow.
|
|
1961
|
+
* @param {boolean} options.copy - When true, copies to clipboard instead of logging.
|
|
1962
|
+
* @memberof UnderpostRun
|
|
1963
|
+
*/
|
|
1964
|
+
'generate-pass': (path, options = DEFAULT_OPTION) => {
|
|
1965
|
+
const length = path && parseInt(path) > 0 ? parseInt(path) : 16;
|
|
1966
|
+
const lower = 'abcdefghijklmnopqrstuvwxyz';
|
|
1967
|
+
const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
1968
|
+
const digits = '0123456789';
|
|
1969
|
+
const special = '@#$%^&*()_+';
|
|
1970
|
+
const all = lower + upper + digits + special;
|
|
1971
|
+
const buf = crypto.randomBytes(length + 4);
|
|
1972
|
+
// Guarantee at least one character from each required class
|
|
1973
|
+
const chars = [
|
|
1974
|
+
lower[buf[0] % lower.length],
|
|
1975
|
+
upper[buf[1] % upper.length],
|
|
1976
|
+
digits[buf[2] % digits.length],
|
|
1977
|
+
special[buf[3] % special.length],
|
|
1978
|
+
];
|
|
1979
|
+
for (let i = 4; i < length; i++) chars.push(all[buf[i] % all.length]);
|
|
1980
|
+
// Fisher-Yates shuffle using an independent random buffer
|
|
1981
|
+
const shuf = crypto.randomBytes(length);
|
|
1982
|
+
for (let i = chars.length - 1; i > 0; i--) {
|
|
1983
|
+
const j = shuf[i % shuf.length] % (i + 1);
|
|
1984
|
+
[chars[i], chars[j]] = [chars[j], chars[i]];
|
|
1985
|
+
}
|
|
1986
|
+
const password = chars.join('');
|
|
1987
|
+
if (options.copy) pbcopy(password);
|
|
1988
|
+
else console.log(password);
|
|
1989
|
+
},
|
|
1990
|
+
|
|
1834
1991
|
secret: (path, options = DEFAULT_OPTION) => {
|
|
1835
1992
|
const secretPath = path ? path : `/home/dd/engine/engine-private/conf/dd-cron/.env.production`;
|
|
1836
|
-
const command =
|
|
1837
|
-
? `node bin secret underpost --create-from-file ${secretPath}`
|
|
1838
|
-
: `underpost secret underpost --create-from-file ${secretPath}`;
|
|
1993
|
+
const command = `node bin secret underpost --create-from-file ${secretPath}`;
|
|
1839
1994
|
shellExec(command);
|
|
1840
1995
|
},
|
|
1841
1996
|
/**
|
package/src/cli/secrets.js
CHANGED
|
@@ -8,6 +8,10 @@ import { shellExec } from '../server/process.js';
|
|
|
8
8
|
import fs from 'fs-extra';
|
|
9
9
|
import dotenv from 'dotenv';
|
|
10
10
|
import Underpost from '../index.js';
|
|
11
|
+
import { loadConf } from '../server/conf.js';
|
|
12
|
+
import { loggerFactory } from '../server/logger.js';
|
|
13
|
+
|
|
14
|
+
const logger = loggerFactory(import.meta);
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* @class UnderpostSecret
|
|
@@ -23,11 +27,80 @@ class UnderpostSecret {
|
|
|
23
27
|
*/
|
|
24
28
|
underpost: {
|
|
25
29
|
createFromEnvFile(envPath) {
|
|
30
|
+
Underpost.env.clean();
|
|
26
31
|
const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
27
32
|
for (const key of Object.keys(envObj)) {
|
|
28
33
|
Underpost.env.set(key, envObj[key]);
|
|
29
34
|
}
|
|
30
35
|
},
|
|
36
|
+
/** Reads application secrets from process.env (injected via envFrom: secretRef)
|
|
37
|
+
* and writes them to the underpost .env file, filtering out known system and
|
|
38
|
+
* Kubernetes-injected environment variables. Replaces the fragile shell-based
|
|
39
|
+
* `printenv | grep -vE` pattern with a maintainable Node.js blocklist.
|
|
40
|
+
*/
|
|
41
|
+
createFromContainerEnv() {
|
|
42
|
+
Underpost.env.clean();
|
|
43
|
+
const systemKeys = new Set([
|
|
44
|
+
'HOME',
|
|
45
|
+
'HOSTNAME',
|
|
46
|
+
'PATH',
|
|
47
|
+
'TERM',
|
|
48
|
+
'SHLVL',
|
|
49
|
+
'PWD',
|
|
50
|
+
'_',
|
|
51
|
+
'LANG',
|
|
52
|
+
'LANGUAGE',
|
|
53
|
+
'LC_ALL',
|
|
54
|
+
'container',
|
|
55
|
+
'SHELL',
|
|
56
|
+
'USER',
|
|
57
|
+
'LOGNAME',
|
|
58
|
+
'MAIL',
|
|
59
|
+
'OLDPWD',
|
|
60
|
+
'LESSOPEN',
|
|
61
|
+
'LESSCLOSE',
|
|
62
|
+
'LS_COLORS',
|
|
63
|
+
'DISPLAY',
|
|
64
|
+
'COLORTERM',
|
|
65
|
+
'EDITOR',
|
|
66
|
+
'VISUAL',
|
|
67
|
+
'TERM_PROGRAM',
|
|
68
|
+
'TERM_PROGRAM_VERSION',
|
|
69
|
+
'SSH_AUTH_SOCK',
|
|
70
|
+
'SSH_CLIENT',
|
|
71
|
+
'SSH_CONNECTION',
|
|
72
|
+
'SSH_TTY',
|
|
73
|
+
'XDG_SESSION_ID',
|
|
74
|
+
'XDG_RUNTIME_DIR',
|
|
75
|
+
'XDG_DATA_DIRS',
|
|
76
|
+
'XDG_CONFIG_DIRS',
|
|
77
|
+
'DBUS_SESSION_BUS_ADDRESS',
|
|
78
|
+
'GPG_AGENT_INFO',
|
|
79
|
+
'WINDOWID',
|
|
80
|
+
'DESKTOP_SESSION',
|
|
81
|
+
'SESSION_MANAGER',
|
|
82
|
+
'XAUTHORITY',
|
|
83
|
+
'WAYLAND_DISPLAY',
|
|
84
|
+
'which_declare',
|
|
85
|
+
]);
|
|
86
|
+
const systemKeyPrefixes = ['KUBERNETES_', 'npm_', 'NODE_'];
|
|
87
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
88
|
+
if (systemKeys.has(key)) continue;
|
|
89
|
+
if (systemKeyPrefixes.some((prefix) => key.startsWith(prefix))) continue;
|
|
90
|
+
Underpost.env.set(key, value);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Removes all filesystem traces of secrets after deployment startup.
|
|
97
|
+
* Centralizes the defense-in-depth cleanup performed
|
|
98
|
+
* @memberof UnderpostSecret
|
|
99
|
+
*/
|
|
100
|
+
globalSecretClean() {
|
|
101
|
+
loadConf('clean');
|
|
102
|
+
Underpost.repo.cleanupPrivateEngineRepo();
|
|
103
|
+
Underpost.env.clean();
|
|
31
104
|
},
|
|
32
105
|
};
|
|
33
106
|
}
|
package/src/cli/test.js
CHANGED
|
@@ -81,7 +81,7 @@ class UnderpostTest {
|
|
|
81
81
|
return await Underpost.test.statusMonitor(options.podName, options.podStatus, options.kindType);
|
|
82
82
|
|
|
83
83
|
if (options.sh === true || options.logs === true) {
|
|
84
|
-
const [pod] = Underpost.
|
|
84
|
+
const [pod] = Underpost.kubectl.get(deployList);
|
|
85
85
|
if (pod) {
|
|
86
86
|
if (options.sh) return pbcopy(`sudo kubectl exec -it ${pod.NAME} -- sh`);
|
|
87
87
|
if (options.logs) return shellExec(`sudo kubectl logs -f ${pod.NAME}`);
|
|
@@ -115,7 +115,7 @@ class UnderpostTest {
|
|
|
115
115
|
break;
|
|
116
116
|
}
|
|
117
117
|
else {
|
|
118
|
-
const pods = Underpost.
|
|
118
|
+
const pods = Underpost.kubectl.get(deployId);
|
|
119
119
|
if (pods.length > 0)
|
|
120
120
|
for (const deployData of pods) {
|
|
121
121
|
const { NAME } = deployData;
|
|
@@ -145,7 +145,7 @@ class UnderpostTest {
|
|
|
145
145
|
logger.info(`Loading instance`, { podName, status, kindType, deltaMs, maxAttempts });
|
|
146
146
|
const _monitor = async () => {
|
|
147
147
|
await timer(deltaMs);
|
|
148
|
-
const pods = Underpost.
|
|
148
|
+
const pods = Underpost.kubectl.get(podName, kindType);
|
|
149
149
|
let result = pods.find((p) => p.STATUS === status || (status === 'Running' && p.STATUS === 'Completed'));
|
|
150
150
|
logger.info(
|
|
151
151
|
`Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${
|
|
@@ -11,10 +11,9 @@ import { RouterDefault } from './components/default/RoutesDefault.js';
|
|
|
11
11
|
import { TranslateDefault } from './components/default/TranslateDefault.js';
|
|
12
12
|
import { Worker } from './components/core/Worker.js';
|
|
13
13
|
import { Keyboard } from './components/core/Keyboard.js';
|
|
14
|
-
import { DefaultParams } from './components/default/CommonDefault.js';
|
|
15
14
|
import { SocketIo } from './components/core/SocketIo.js';
|
|
16
15
|
import { SocketIoDefault } from './components/default/SocketIoDefault.js';
|
|
17
|
-
import {
|
|
16
|
+
import { AppStoreDefault } from './components/default/AppStoreDefault.js';
|
|
18
17
|
import { CssDefaultDark, CssDefaultLight } from './components/default/CssDefault.js';
|
|
19
18
|
import { EventsUI } from './components/core/EventsUI.js';
|
|
20
19
|
import { Modal } from './components/core/Modal.js';
|
|
@@ -71,14 +70,14 @@ window.onload = () =>
|
|
|
71
70
|
await Responsive.Init();
|
|
72
71
|
await MenuDefault.Render({ htmlMainBody });
|
|
73
72
|
await SocketIo.Init({
|
|
74
|
-
channels:
|
|
73
|
+
channels: AppStoreDefault.Data,
|
|
75
74
|
path: `/`,
|
|
76
75
|
});
|
|
77
76
|
await SocketIoDefault.Init();
|
|
78
77
|
await LogInDefault();
|
|
79
78
|
await LogOutDefault();
|
|
80
79
|
await SignUpDefault();
|
|
81
|
-
await Keyboard.Init(
|
|
80
|
+
await Keyboard.Init();
|
|
82
81
|
await Modal.RenderSeoSanitizer();
|
|
83
82
|
},
|
|
84
83
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core per-app state store for WebSocket channel data.
|
|
3
|
+
*
|
|
4
|
+
* @module client/core/AppStore
|
|
5
|
+
* @namespace AppStore
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @class AppStore
|
|
10
|
+
* @classdesc Per-app singleton state store for WebSocket channel data and authenticated user state.
|
|
11
|
+
*
|
|
12
|
+
* Usage: `AppStoreX.Data.user.main.model.user` — the authenticated user object.
|
|
13
|
+
* `AppStoreX.Data` keys (`chat`, `mailer`, `stream`, etc.) — channel definitions for `SocketIo.Init`.
|
|
14
|
+
* @memberof AppStore
|
|
15
|
+
*/
|
|
16
|
+
class AppStore {
|
|
17
|
+
/**
|
|
18
|
+
* Channel data map, keyed by channel name (e.g. `user`, `chat`, `mailer`).
|
|
19
|
+
* The `user` channel always contains `{ main: { model: { user: { _id: '' } } } }`.
|
|
20
|
+
*
|
|
21
|
+
* @type {Object.<string, Object>}
|
|
22
|
+
*/
|
|
23
|
+
Data;
|
|
24
|
+
|
|
25
|
+
/** @private @type {function(): Object} */
|
|
26
|
+
#initialStateFactory;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a new AppStore instance.
|
|
30
|
+
*
|
|
31
|
+
* @param {function(): Object} initialStateFactory - Factory function returning the initial data shape.
|
|
32
|
+
* Must return at least `{ user: { main: { model: { user: { _id: '' } } } } }`.
|
|
33
|
+
*/
|
|
34
|
+
constructor(initialStateFactory) {
|
|
35
|
+
this.#initialStateFactory = initialStateFactory;
|
|
36
|
+
this.Data = initialStateFactory();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resets `Data` to its initial state.
|
|
41
|
+
*
|
|
42
|
+
* @returns {void}
|
|
43
|
+
*/
|
|
44
|
+
reset() {
|
|
45
|
+
this.Data = this.#initialStateFactory();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates an AppStore with the standard channel layout.
|
|
50
|
+
* Always includes `user`, `chat`, and `mailer` channels.
|
|
51
|
+
*
|
|
52
|
+
* @static
|
|
53
|
+
* @param {...string} extraChannels - Additional channel names (e.g. `'stream'`).
|
|
54
|
+
* @returns {AppStore}
|
|
55
|
+
*/
|
|
56
|
+
static create(...extraChannels) {
|
|
57
|
+
return new AppStore(() => {
|
|
58
|
+
const state = {
|
|
59
|
+
user: { main: { model: { user: { _id: '' } } } },
|
|
60
|
+
chat: {},
|
|
61
|
+
mailer: {},
|
|
62
|
+
};
|
|
63
|
+
for (const ch of extraChannels) state[ch] = {};
|
|
64
|
+
return state;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { AppStore };
|
|
@@ -25,7 +25,7 @@ const eventDateFactory = (event) =>
|
|
|
25
25
|
const CalendarCore = {
|
|
26
26
|
RenderStyle: async function () {},
|
|
27
27
|
Data: {},
|
|
28
|
-
Render: async function (options = { idModal: '',
|
|
28
|
+
Render: async function (options = { idModal: '', appStore: {}, hiddenDates: [] }) {
|
|
29
29
|
this.Data[options.idModal] = {
|
|
30
30
|
data: [],
|
|
31
31
|
originData: [],
|
|
@@ -49,7 +49,7 @@ const CalendarCore = {
|
|
|
49
49
|
this.Data[options.idModal].filesData = [];
|
|
50
50
|
this.Data[options.idModal].originData = newInstance(resultData);
|
|
51
51
|
this.Data[options.idModal].data = resultData.map((o) => {
|
|
52
|
-
if (o.creatorUserId && options.
|
|
52
|
+
if (o.creatorUserId && options.appStore.Data.user.main.model.user._id === o.creatorUserId) o.tools = true;
|
|
53
53
|
o.id = o._id;
|
|
54
54
|
|
|
55
55
|
this.Data[options.idModal].filesData.push({});
|