underpost 3.2.9 → 3.2.10
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/.github/workflows/npmpkg.ci.yml +1 -0
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -0
- package/.vscode/settings.json +10 -5
- package/CHANGELOG.md +122 -1
- package/CLI-HELP.md +22 -7
- package/README.md +37 -8
- package/bin/build.js +26 -9
- package/bin/deploy.js +20 -21
- package/bin/file.js +31 -13
- package/bin/index.js +2 -1
- package/bin/vs.js +1 -1
- package/bump.config.js +26 -0
- package/conf.js +20 -4
- 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/manifests/deployment/dd-test-development/deployment.yaml +4 -2
- package/manifests/kind-config-dev.yaml +8 -0
- package/manifests/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/package.json +27 -12
- package/scripts/k3s-node-setup.sh +28 -9
- package/src/api/core/core.router.js +19 -14
- package/src/api/core/core.service.js +5 -5
- package/src/api/default/default.router.js +22 -18
- package/src/api/default/default.service.js +5 -5
- package/src/api/document/document.router.js +28 -23
- package/src/api/document/document.service.js +100 -23
- package/src/api/file/file.router.js +19 -13
- package/src/api/file/file.service.js +9 -7
- package/src/api/test/test.router.js +17 -12
- package/src/api/types.js +24 -0
- package/src/api/user/guest.service.js +5 -4
- package/src/api/user/user.router.js +297 -288
- package/src/api/user/user.service.js +100 -35
- package/src/cli/baremetal.js +20 -11
- package/src/cli/cluster.js +196 -55
- package/src/cli/db.js +59 -60
- package/src/cli/deploy.js +273 -159
- package/src/cli/fs.js +3 -1
- package/src/cli/index.js +16 -9
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +217 -135
- package/src/cli/release.js +289 -131
- package/src/cli/repository.js +58 -7
- package/src/cli/run.js +152 -25
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +4 -0
- package/src/client/components/core/PanelForm.js +56 -52
- package/src/client/components/core/Worker.js +162 -363
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +120 -20
- package/src/db/mongo/MongoBootstrap.js +587 -0
- package/src/db/mongo/MongooseDB.js +126 -22
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/wp/Wp.js +8 -5
- package/src/server/auth.js +2 -2
- package/src/server/client-build-docs.js +1 -1
- package/src/server/client-build.js +94 -129
- package/src/server/conf.js +20 -65
- package/src/server/process.js +180 -19
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +12 -4
- package/src/ws/IoInterface.js +16 -16
- package/src/ws/core/channels/core.ws.chat.js +11 -11
- package/src/ws/core/channels/core.ws.mailer.js +29 -29
- package/src/ws/core/channels/core.ws.stream.js +19 -19
- package/src/ws/core/core.ws.connection.js +8 -8
- package/src/ws/core/core.ws.server.js +6 -5
- package/src/ws/default/channels/default.ws.main.js +10 -10
- package/src/ws/default/default.ws.connection.js +4 -4
- package/src/ws/default/default.ws.server.js +4 -3
- package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
- package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
- /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
- /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
- /package/src/client/ssr/{pages → views}/Test.js +0 -0
package/src/cli/run.js
CHANGED
|
@@ -24,6 +24,7 @@ import { range, setPad, timer } from '../client/components/core/CommonJs.js';
|
|
|
24
24
|
import os from 'os';
|
|
25
25
|
import Underpost from '../index.js';
|
|
26
26
|
import dotenv from 'dotenv';
|
|
27
|
+
import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
|
|
27
28
|
|
|
28
29
|
const waitForPort = (port, host = '127.0.0.1', { maxAttempts = 30, interval = 2000 } = {}) =>
|
|
29
30
|
new Promise((resolve, reject) => {
|
|
@@ -97,6 +98,7 @@ const logger = loggerFactory(import.meta);
|
|
|
97
98
|
* @property {string} deployId - The deployment ID.
|
|
98
99
|
* @property {string} instanceId - The instance ID.
|
|
99
100
|
* @property {string} user - The user to run as.
|
|
101
|
+
* @property {string} group - The group to use.
|
|
100
102
|
* @property {string} pid - The process ID.
|
|
101
103
|
* @property {boolean} disablePrivateConfUpdate - Whether to disable private configuration updates.
|
|
102
104
|
* @property {string} monitorStatus - The monitor status option.
|
|
@@ -165,6 +167,7 @@ const DEFAULT_OPTION = {
|
|
|
165
167
|
deployId: '',
|
|
166
168
|
instanceId: '',
|
|
167
169
|
user: '',
|
|
170
|
+
group: '',
|
|
168
171
|
pid: '',
|
|
169
172
|
disablePrivateConfUpdate: false,
|
|
170
173
|
monitorStatus: '',
|
|
@@ -225,7 +228,7 @@ class UnderpostRun {
|
|
|
225
228
|
// Detect MongoDB primary pod using method
|
|
226
229
|
let primaryMongoHost = 'mongodb-0.mongodb-service';
|
|
227
230
|
try {
|
|
228
|
-
const primaryPodName =
|
|
231
|
+
const primaryPodName = MongoBootstrap.getPrimaryPodName({
|
|
229
232
|
namespace: options.namespace,
|
|
230
233
|
podName: 'mongodb-0',
|
|
231
234
|
});
|
|
@@ -505,14 +508,16 @@ class UnderpostRun {
|
|
|
505
508
|
},
|
|
506
509
|
/**
|
|
507
510
|
* @method docker-image
|
|
508
|
-
* @description Dispatches the Docker image CI workflow (`docker-image.ci.yml`)
|
|
509
|
-
*
|
|
511
|
+
* @description Dispatches the Docker image CI workflow (`docker-image[.<runtime>].ci.yml`) via `workflow_dispatch`.
|
|
512
|
+
* Repository resolution is delegated to `Underpost.repo.resolveInstanceRepo(path)`.
|
|
513
|
+
* @param {string} path - Optional runtime / workflow suffix (e.g. `cyberia-server`, `cyberia-client`).
|
|
510
514
|
* @param {Object} options - The default underpost runner options for customizing workflow
|
|
511
515
|
* @memberof UnderpostRun
|
|
512
516
|
*/
|
|
513
517
|
'docker-image': (path, options = DEFAULT_OPTION) => {
|
|
518
|
+
const repo = Underpost.repo.resolveInstanceRepo(path);
|
|
514
519
|
Underpost.repo.dispatchWorkflow({
|
|
515
|
-
repo
|
|
520
|
+
repo,
|
|
516
521
|
workflowFile: `docker-image${path ? `.${path}` : ''}.ci.yml`,
|
|
517
522
|
ref: 'master',
|
|
518
523
|
inputs: {},
|
|
@@ -536,16 +541,14 @@ class UnderpostRun {
|
|
|
536
541
|
* @memberof UnderpostRun
|
|
537
542
|
*/
|
|
538
543
|
pull: (path, options = DEFAULT_OPTION) => {
|
|
544
|
+
// shellExec is fail-fast by default — any non-zero exit throws and
|
|
545
|
+
// propagates up to the workflow step. No per-call flag required.
|
|
539
546
|
if (!fs.existsSync(`/home/dd`) || !fs.existsSync(`/home/dd/engine`)) {
|
|
540
547
|
fs.mkdirSync(`/home/dd`, { recursive: true });
|
|
541
|
-
shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, {
|
|
542
|
-
silent: true,
|
|
543
|
-
});
|
|
548
|
+
shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
|
|
544
549
|
} else {
|
|
545
550
|
shellExec(`underpost run clean`);
|
|
546
|
-
shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, {
|
|
547
|
-
silent: true,
|
|
548
|
-
});
|
|
551
|
+
shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
|
|
549
552
|
}
|
|
550
553
|
if (!fs.existsSync(`/home/dd/engine/engine-private`))
|
|
551
554
|
shellExec(`cd /home/dd/engine && underpost clone ${process.env.GITHUB_USERNAME}/engine-private`, {
|
|
@@ -554,9 +557,7 @@ class UnderpostRun {
|
|
|
554
557
|
else
|
|
555
558
|
shellExec(
|
|
556
559
|
`cd /home/dd/engine/engine-private && underpost pull . ${process.env.GITHUB_USERNAME}/engine-private`,
|
|
557
|
-
{
|
|
558
|
-
silent: true,
|
|
559
|
-
},
|
|
560
|
+
{ silent: true },
|
|
560
561
|
);
|
|
561
562
|
},
|
|
562
563
|
/**
|
|
@@ -643,6 +644,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
643
644
|
/**
|
|
644
645
|
* @method sync
|
|
645
646
|
* @description Cleans up, and then runs a deployment synchronization command (`underpost deploy --kubeadm --build-manifest --sync...`) using parameters parsed from `path` (deployId, replicas, versions, image, node).
|
|
647
|
+
*
|
|
648
|
+
* Forwards `--image-pull-policy <policy>` to the underlying `deploy --build-manifest` invocation when `options.imagePullPolicy` is set,
|
|
649
|
+
* which then plumbs through `buildManifest` and `deploymentYamlPartsFactory` to override the container `imagePullPolicy` in the generated
|
|
650
|
+
* `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
|
|
651
|
+
* `node bin run sync dd-core --kubeadm --image-pull-policy Always`
|
|
646
652
|
* @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing deploy parameters).
|
|
647
653
|
* @param {Object} options - The default underpost runner options for customizing workflow
|
|
648
654
|
* @memberof UnderpostRun
|
|
@@ -700,13 +706,14 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
700
706
|
|
|
701
707
|
const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
|
|
702
708
|
const pullBundleFlag = options.pullBundle ? ' --pull-bundle' : '';
|
|
709
|
+
const imagePullPolicyFlag = options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : '';
|
|
703
710
|
|
|
704
711
|
shellExec(
|
|
705
712
|
`${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
|
|
706
713
|
image ? ` --image ${image}` : ''
|
|
707
714
|
}${versions ? ` --versions ${versions}` : ''}${
|
|
708
715
|
options.namespace ? ` --namespace ${options.namespace}` : ''
|
|
709
|
-
}${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag} ${deployId} ${env}`,
|
|
716
|
+
}${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag} ${deployId} ${env}`,
|
|
710
717
|
);
|
|
711
718
|
|
|
712
719
|
if (isDeployRunnerContext(path, options)) {
|
|
@@ -717,7 +724,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
717
724
|
shellExec(
|
|
718
725
|
`${baseCommand} deploy${clusterFlag}${cmdString} --replicas ${replicas} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
|
|
719
726
|
options.namespace ? ` --namespace ${options.namespace}` : ''
|
|
720
|
-
}${timeoutFlags}${gitCleanFlag}`,
|
|
727
|
+
}${timeoutFlags}${gitCleanFlag}${imagePullPolicyFlag}`,
|
|
721
728
|
);
|
|
722
729
|
if (!targetTraffic)
|
|
723
730
|
targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
|
|
@@ -1023,6 +1030,9 @@ EOF
|
|
|
1023
1030
|
cmd: _cmd,
|
|
1024
1031
|
volumes: _volumes,
|
|
1025
1032
|
metadata: _metadata,
|
|
1033
|
+
lifecycle: _lifecycle,
|
|
1034
|
+
readinessProbe: _readinessProbe,
|
|
1035
|
+
livenessProbe: _livenessProbe,
|
|
1026
1036
|
} = instance;
|
|
1027
1037
|
if (id !== _id) continue;
|
|
1028
1038
|
const _deployId = `${deployId}-${_id}`;
|
|
@@ -1087,6 +1097,20 @@ EOF
|
|
|
1087
1097
|
),
|
|
1088
1098
|
);
|
|
1089
1099
|
|
|
1100
|
+
// Resolve env-scoped lifecycle/probe blocks: each can be either
|
|
1101
|
+
// { ...envObj } // shared shape
|
|
1102
|
+
// { development: {...}, production: {...} } // env-specific
|
|
1103
|
+
const pickEnv = (v) => (v && (v.development || v.production) ? v[env] : v);
|
|
1104
|
+
|
|
1105
|
+
// Convention: an instance config may place `imagePullPolicy` inside
|
|
1106
|
+
// the env-scoped lifecycle block (alongside postStart/preStop).
|
|
1107
|
+
// Extract it onto the container spec (where K8S expects it) and
|
|
1108
|
+
// strip it from the lifecycle hash so the rendered YAML stays valid.
|
|
1109
|
+
// CLI override (`--image-pull-policy`) wins over the conf value.
|
|
1110
|
+
const { lifecycle: lifecycleForManifest, imagePullPolicy: lifecycleImagePullPolicy } =
|
|
1111
|
+
Underpost.deploy.extractInstanceImagePullPolicy(pickEnv(_lifecycle));
|
|
1112
|
+
const instanceImagePullPolicy = options.imagePullPolicy || lifecycleImagePullPolicy;
|
|
1113
|
+
|
|
1090
1114
|
let deploymentYaml = `---
|
|
1091
1115
|
${Underpost.deploy
|
|
1092
1116
|
.deploymentYamlPartsFactory({
|
|
@@ -1099,6 +1123,11 @@ ${Underpost.deploy
|
|
|
1099
1123
|
namespace: options.namespace,
|
|
1100
1124
|
volumes: _volumes,
|
|
1101
1125
|
cmd: resolvedCmd,
|
|
1126
|
+
lifecycle: lifecycleForManifest,
|
|
1127
|
+
readinessProbe: pickEnv(_readinessProbe),
|
|
1128
|
+
livenessProbe: pickEnv(_livenessProbe),
|
|
1129
|
+
containerPort: _toPort,
|
|
1130
|
+
imagePullPolicy: instanceImagePullPolicy,
|
|
1102
1131
|
})
|
|
1103
1132
|
.replace('{{ports}}', buildKindPorts(_fromPort, _toPort))}
|
|
1104
1133
|
`;
|
|
@@ -1187,14 +1216,34 @@ EOF
|
|
|
1187
1216
|
volumes: _volumes,
|
|
1188
1217
|
metadata: _metadata,
|
|
1189
1218
|
runtime: _runtime,
|
|
1219
|
+
lifecycle: _lifecycle,
|
|
1220
|
+
readinessProbe: _readinessProbe,
|
|
1221
|
+
livenessProbe: _livenessProbe,
|
|
1190
1222
|
} = instance;
|
|
1191
1223
|
|
|
1192
|
-
// Resolve Dockerfile source
|
|
1193
|
-
|
|
1194
|
-
|
|
1224
|
+
// Resolve Dockerfile source. Dev/prod variant rules:
|
|
1225
|
+
// - When the instance defines a `runtime`, look under
|
|
1226
|
+
// `src/runtime/<runtime>/`. In `--dev` mode prefer `Dockerfile.dev`
|
|
1227
|
+
// when it exists, falling back to `Dockerfile`.
|
|
1228
|
+
// - When `runtime` is not set, look in the project root with the
|
|
1229
|
+
// same `.dev` → no-suffix precedence.
|
|
1230
|
+
// Dockerfile.dev is a full Dockerfile (not an overlay) — each runtime
|
|
1231
|
+
// owns the contract between its dev image and its prod image (debug
|
|
1232
|
+
// build flags, extra tooling, default ports, etc.).
|
|
1233
|
+
const dockerfileBase = _runtime ? `src/runtime/${_runtime}` : rootPath;
|
|
1234
|
+
const dockerfileCandidates = options.dev
|
|
1235
|
+
? [`${dockerfileBase}/Dockerfile.dev`, `${dockerfileBase}/Dockerfile`]
|
|
1236
|
+
: [`${dockerfileBase}/Dockerfile`];
|
|
1237
|
+
const dockerfileSourcePath = dockerfileCandidates.find((p) => fs.existsSync(p));
|
|
1238
|
+
if (dockerfileSourcePath) {
|
|
1239
|
+
if (options.dev && !dockerfileSourcePath.endsWith('.dev')) {
|
|
1240
|
+
logger.warn(
|
|
1241
|
+
`[instance-build-manifest] --dev requested but no Dockerfile.dev present; falling back to ${dockerfileSourcePath}`,
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1195
1244
|
fs.copyFileSync(dockerfileSourcePath, dockerfileManifestPath);
|
|
1196
1245
|
} else {
|
|
1197
|
-
logger.warn(`[instance-build-manifest] Dockerfile not found
|
|
1246
|
+
logger.warn(`[instance-build-manifest] Dockerfile not found; tried: ${dockerfileCandidates.join(', ')}`);
|
|
1198
1247
|
}
|
|
1199
1248
|
|
|
1200
1249
|
const _deployId = `${deployId}-${_id}`;
|
|
@@ -1239,6 +1288,17 @@ EOF
|
|
|
1239
1288
|
),
|
|
1240
1289
|
);
|
|
1241
1290
|
|
|
1291
|
+
// Env-aware lifecycle / probe selection. Each block may either be
|
|
1292
|
+
// a single object (shared across envs) or `{ development, production }`.
|
|
1293
|
+
const pickEnv = (v) => (v && (v.development || v.production) ? v[env] : v);
|
|
1294
|
+
|
|
1295
|
+
// Convention: an instance config may place `imagePullPolicy` inside
|
|
1296
|
+
// the env-scoped lifecycle block (alongside postStart/preStop).
|
|
1297
|
+
// Extract it onto the container spec and strip it from the lifecycle hash.
|
|
1298
|
+
const { lifecycle: lifecycleForManifest, imagePullPolicy: lifecycleImagePullPolicy } =
|
|
1299
|
+
Underpost.deploy.extractInstanceImagePullPolicy(pickEnv(_lifecycle));
|
|
1300
|
+
const instanceImagePullPolicy = options.imagePullPolicy || lifecycleImagePullPolicy;
|
|
1301
|
+
|
|
1242
1302
|
const deploymentYaml =
|
|
1243
1303
|
`---\n` +
|
|
1244
1304
|
Underpost.deploy
|
|
@@ -1252,6 +1312,11 @@ EOF
|
|
|
1252
1312
|
namespace: options.namespace,
|
|
1253
1313
|
volumes: _volumes,
|
|
1254
1314
|
cmd: resolvedCmd,
|
|
1315
|
+
lifecycle: lifecycleForManifest,
|
|
1316
|
+
readinessProbe: pickEnv(_readinessProbe),
|
|
1317
|
+
livenessProbe: pickEnv(_livenessProbe),
|
|
1318
|
+
containerPort: _toPort,
|
|
1319
|
+
imagePullPolicy: instanceImagePullPolicy,
|
|
1255
1320
|
})
|
|
1256
1321
|
.replace('{{ports}}', buildKindPorts(_fromPort, _toPort));
|
|
1257
1322
|
|
|
@@ -1330,9 +1395,9 @@ EOF`);
|
|
|
1330
1395
|
// crictl is in the kubernetes repo but excluded by default — install it explicitly
|
|
1331
1396
|
shellExec(`sudo yum install -y cri-tools --disableexcludes=kubernetes`);
|
|
1332
1397
|
// Ensure CRI-O uses systemd cgroup driver (matches kubelet default)
|
|
1333
|
-
shellExec(
|
|
1334
|
-
|
|
1335
|
-
);
|
|
1398
|
+
shellExec(`sudo sed -i 's/^#\?cgroup_manager =.*/cgroup_manager = "systemd"/' /etc/crio/crio.conf`, {
|
|
1399
|
+
silentOnError: true,
|
|
1400
|
+
});
|
|
1336
1401
|
shellExec(`sudo systemctl enable --now crio`);
|
|
1337
1402
|
logger.info('CRI-O installed and started.');
|
|
1338
1403
|
// Write crictl config so all crictl calls default to the CRI-O socket
|
|
@@ -2180,15 +2245,23 @@ EOF`);
|
|
|
2180
2245
|
* @memberof UnderpostRun
|
|
2181
2246
|
*/
|
|
2182
2247
|
kill: (path = '', options = DEFAULT_OPTION) => {
|
|
2183
|
-
if (options.pid)
|
|
2248
|
+
if (options.pid)
|
|
2249
|
+
return shellExec(`sudo kill -9 ${options.pid}`, {
|
|
2250
|
+
silentOnError: true,
|
|
2251
|
+
});
|
|
2184
2252
|
for (const _path of path.split(',')) {
|
|
2185
2253
|
if (_path.split('+')[1]) {
|
|
2186
2254
|
let [port, sumPortOffSet] = _path.split('+');
|
|
2187
2255
|
port = parseInt(port);
|
|
2188
2256
|
sumPortOffSet = parseInt(sumPortOffSet);
|
|
2189
2257
|
for (const sumPort of range(0, sumPortOffSet))
|
|
2190
|
-
shellExec(`sudo kill -9 $(lsof -t -i:${parseInt(port) + parseInt(sumPort)})
|
|
2191
|
-
|
|
2258
|
+
shellExec(`sudo kill -9 $(lsof -t -i:${parseInt(port) + parseInt(sumPort)})`, {
|
|
2259
|
+
silentOnError: true,
|
|
2260
|
+
});
|
|
2261
|
+
} else
|
|
2262
|
+
shellExec(`sudo kill -9 $(lsof -t -i:${_path})`, {
|
|
2263
|
+
silentOnError: true,
|
|
2264
|
+
});
|
|
2192
2265
|
}
|
|
2193
2266
|
},
|
|
2194
2267
|
/**
|
|
@@ -2545,6 +2618,60 @@ EOF`;
|
|
|
2545
2618
|
}
|
|
2546
2619
|
}
|
|
2547
2620
|
},
|
|
2621
|
+
|
|
2622
|
+
/**
|
|
2623
|
+
* @method setup-shared-dir
|
|
2624
|
+
* @description Run once for initial shared-directory setup. Creates the group, adds the user,
|
|
2625
|
+
* creates the directory, sets ownership, applies the SGID bit, and configures default ACLs so
|
|
2626
|
+
* all future files inside the directory automatically inherit group write permissions.
|
|
2627
|
+
* Use `reload-shared-dir` for subsequent permission repairs without recreating the group.
|
|
2628
|
+
* @param {string} path - Target directory to set up (defaults to `/home/dd/engine`).
|
|
2629
|
+
* Customise via the `path` argument or leave empty to use the default.
|
|
2630
|
+
* @param {Object} options - The default underpost runner options for customizing workflow.
|
|
2631
|
+
* Key fields: `options.user` (default `'admin'`), `options.group` (default `'engine-dev'`).
|
|
2632
|
+
* @memberof UnderpostRun
|
|
2633
|
+
*/
|
|
2634
|
+
'setup-shared-dir': (path = '/home/dd/engine', options = DEFAULT_OPTION) => {
|
|
2635
|
+
const dir = path || '/home/dd/engine';
|
|
2636
|
+
const user = options.user || 'admin';
|
|
2637
|
+
const group = options.group || 'engine-dev';
|
|
2638
|
+
|
|
2639
|
+
logger.info(`[setup-shared-dir] dir=${dir} user=${user} group=${group}`);
|
|
2640
|
+
|
|
2641
|
+
shellExec(`sudo groupadd ${group} 2>/dev/null || true`);
|
|
2642
|
+
shellExec(`sudo usermod -aG ${group} ${user}`);
|
|
2643
|
+
shellExec(`sudo mkdir -p ${dir}`);
|
|
2644
|
+
shellExec(`sudo chown -R ${user}:${group} ${dir}`);
|
|
2645
|
+
shellExec(`sudo chmod -R 2775 ${dir}`);
|
|
2646
|
+
shellExec(`sudo setfacl -d -m g:${group}:rwx ${dir}`);
|
|
2647
|
+
shellExec(`sudo setfacl -m g:${group}:rwx ${dir}`);
|
|
2648
|
+
|
|
2649
|
+
logger.info(`[setup-shared-dir] Shared directory setup complete: ${dir}`);
|
|
2650
|
+
},
|
|
2651
|
+
|
|
2652
|
+
/**
|
|
2653
|
+
* @method reload-shared-dir
|
|
2654
|
+
* @description Re-applies recursive permissions and ACLs to repair permission drift on an
|
|
2655
|
+
* already-configured shared directory. Does **not** recreate the group, add users, or modify
|
|
2656
|
+
* ownership. Use this after VS Code permission errors or when existing files lose group write
|
|
2657
|
+
* access due to tool or process interference.
|
|
2658
|
+
* @param {string} path - Target directory to repair (defaults to `/home/dd/engine`).
|
|
2659
|
+
* Customise via the `path` argument or leave empty to use the default.
|
|
2660
|
+
* @param {Object} options - The default underpost runner options for customizing workflow.
|
|
2661
|
+
* Key fields: `options.group` (default `'engine-dev'`).
|
|
2662
|
+
* @memberof UnderpostRun
|
|
2663
|
+
*/
|
|
2664
|
+
'reload-shared-dir': (path = '/home/dd/engine', options = DEFAULT_OPTION) => {
|
|
2665
|
+
const dir = path || '/home/dd/engine';
|
|
2666
|
+
const group = options.group || 'engine-dev';
|
|
2667
|
+
|
|
2668
|
+
logger.info(`[reload-shared-dir] dir=${dir} group=${group}`);
|
|
2669
|
+
|
|
2670
|
+
shellExec(`sudo chmod -R 2775 ${dir}`);
|
|
2671
|
+
shellExec(`sudo setfacl -R -m g:${group}:rwx ${dir}`);
|
|
2672
|
+
|
|
2673
|
+
logger.info(`[reload-shared-dir] Shared directory permissions reloaded: ${dir}`);
|
|
2674
|
+
},
|
|
2548
2675
|
};
|
|
2549
2676
|
|
|
2550
2677
|
static API = {
|
package/src/cli/test.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @namespace UnderpostTest
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import fs from 'fs-extra';
|
|
7
8
|
import { timer } from '../client/components/core/CommonJs.js';
|
|
8
9
|
import { MariaDB } from '../db/mariadb/MariaDB.js';
|
|
9
10
|
import { getNpmRootPath } from '../server/conf.js';
|
|
@@ -42,7 +43,13 @@ class UnderpostTest {
|
|
|
42
43
|
*/
|
|
43
44
|
run() {
|
|
44
45
|
actionInitLog();
|
|
45
|
-
|
|
46
|
+
const underpostTestPath = `${getNpmRootPath()}/underpost`;
|
|
47
|
+
if (fs.existsSync(underpostTestPath)) {
|
|
48
|
+
shellExec(`cd ${underpostTestPath} && npm run test`);
|
|
49
|
+
} else {
|
|
50
|
+
logger.warn(`Global underpost not found at ${underpostTestPath}, running local npm test instead`);
|
|
51
|
+
shellExec('npm test');
|
|
52
|
+
}
|
|
46
53
|
},
|
|
47
54
|
/**
|
|
48
55
|
* @method callback
|
|
@@ -148,8 +155,7 @@ class UnderpostTest {
|
|
|
148
155
|
const pods = Underpost.kubectl.get(podName, kindType);
|
|
149
156
|
let result = pods.find((p) => p.STATUS === status || (status === 'Running' && p.STATUS === 'Completed'));
|
|
150
157
|
logger.info(
|
|
151
|
-
`Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${
|
|
152
|
-
index + 1
|
|
158
|
+
`Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${index + 1
|
|
153
159
|
}/${maxAttempts}`,
|
|
154
160
|
pods[0] ? pods[0].STATUS : 'Not found kind object',
|
|
155
161
|
);
|
|
@@ -19,6 +19,12 @@ const DefaultTemplate = async () => {
|
|
|
19
19
|
});
|
|
20
20
|
});
|
|
21
21
|
return html`
|
|
22
|
+
<style>
|
|
23
|
+
.feature-icon {
|
|
24
|
+
font-size: 2.5rem;
|
|
25
|
+
margin-bottom: 1rem;
|
|
26
|
+
}
|
|
27
|
+
</style>
|
|
22
28
|
<div class="landing-container">
|
|
23
29
|
<div class="content-wrapper">
|
|
24
30
|
<h1 class="animated-text">
|
|
@@ -27,17 +33,17 @@ const DefaultTemplate = async () => {
|
|
|
27
33
|
</h1>
|
|
28
34
|
<div class="features">
|
|
29
35
|
<div class="feature-card">
|
|
30
|
-
<i class="icon"
|
|
36
|
+
<i class="fas fa-rocket feature-icon"></i>
|
|
31
37
|
<h3>Fast & Reliable</h3>
|
|
32
38
|
<p>Lightning-fast performance with 99.9% uptime</p>
|
|
33
39
|
</div>
|
|
34
40
|
<div class="feature-card">
|
|
35
|
-
<i class="icon"
|
|
41
|
+
<i class="fas fa-palette feature-icon"></i>
|
|
36
42
|
<h3>Beautiful UI</h3>
|
|
37
43
|
<p>Modern and intuitive user interface</p>
|
|
38
44
|
</div>
|
|
39
45
|
<div class="feature-card">
|
|
40
|
-
<i class="icon"
|
|
46
|
+
<i class="fas fa-bolt feature-icon"></i>
|
|
41
47
|
<h3>Powerful Features</h3>
|
|
42
48
|
<p>Everything you need in one place</p>
|
|
43
49
|
</div>
|
|
@@ -319,6 +319,10 @@ class Auth {
|
|
|
319
319
|
// Close any open login/signup modals
|
|
320
320
|
if (s(`.modal-log-in`)) s(`.btn-close-modal-log-in`).click();
|
|
321
321
|
if (s(`.modal-sign-up`)) s(`.btn-close-modal-sign-up`).click();
|
|
322
|
+
if (!s(`.main-body-btn-ui-open`).classList.contains('hide'))
|
|
323
|
+
s(`.main-body-btn-ui-open`).click();
|
|
324
|
+
if (!s(`.main-body-btn-ui-bar-custom-open`).classList.contains('hide'))
|
|
325
|
+
s(`.main-body-btn-ui-bar-custom-open`).click();
|
|
322
326
|
});
|
|
323
327
|
}
|
|
324
328
|
|
|
@@ -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
|
`,
|
|
@@ -332,16 +332,10 @@ class PanelForm {
|
|
|
332
332
|
}, 50);
|
|
333
333
|
},
|
|
334
334
|
initEdit: async function ({ data }) {
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
s(`.${fileFormData.id}`).inputFiles = null;
|
|
340
|
-
htmls(
|
|
341
|
-
`.file-name-render-${fileFormData.id}`,
|
|
342
|
-
`<div class="abs center"><i style="font-size: 25px" class="fa-solid fa-cloud"></i></div>`,
|
|
343
|
-
);
|
|
344
|
-
}
|
|
335
|
+
// Do NOT clear the file input here - the file should remain as-is when entering edit mode.
|
|
336
|
+
// If user wants to remove the file, they use the "clean file" button.
|
|
337
|
+
// If user wants to replace the file, they select a new file.
|
|
338
|
+
// Unconditionally clearing the file here would cause the server to receive fileId: null on save.
|
|
345
339
|
setTimeout(() => {
|
|
346
340
|
s(`.modal-${options.route}`).scrollTo({ top: 0, behavior: 'smooth' });
|
|
347
341
|
}, 50);
|
|
@@ -388,15 +382,15 @@ class PanelForm {
|
|
|
388
382
|
// It will be filtered from the tags array to keep visibility control separate from content tags
|
|
389
383
|
const tags = data.tags
|
|
390
384
|
? uniqueArray(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
385
|
+
data.tags
|
|
386
|
+
.replaceAll('/', ',')
|
|
387
|
+
.replaceAll('-', ',')
|
|
388
|
+
.replaceAll(' ', ',')
|
|
389
|
+
.split(',')
|
|
390
|
+
.map((t) => t.trim())
|
|
391
|
+
.filter((t) => t)
|
|
392
|
+
.concat(prefixTags),
|
|
393
|
+
)
|
|
400
394
|
: prefixTags;
|
|
401
395
|
let originObj, originFileObj, indexOriginObj;
|
|
402
396
|
if (editId) {
|
|
@@ -434,7 +428,17 @@ class PanelForm {
|
|
|
434
428
|
for (const file of inputFiles) {
|
|
435
429
|
indexFormDoc++;
|
|
436
430
|
let fileId = undefined; // Reset for each iteration - only set if user uploaded a file
|
|
431
|
+
// Track whether the file input was explicitly cleared (null) vs never had a file (undefined)
|
|
432
|
+
// In edit mode, null means user cleared the file - we need to tell server to remove it
|
|
433
|
+
const isFileCleared = data.fileId === null && editId;
|
|
437
434
|
await (async () => {
|
|
435
|
+
// When file is null and not the first iteration or not in edit mode, skip upload
|
|
436
|
+
if (!file && !isFileCleared) return;
|
|
437
|
+
// When user cleared file in edit mode, set fileId=null so server removes the reference
|
|
438
|
+
if (isFileCleared) {
|
|
439
|
+
fileId = null;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
438
442
|
const body = new FormData();
|
|
439
443
|
// Only append md file if it was created (has content)
|
|
440
444
|
if (md) body.append('md', md);
|
|
@@ -485,8 +489,8 @@ class PanelForm {
|
|
|
485
489
|
message: documentMessage,
|
|
486
490
|
data: documentData,
|
|
487
491
|
} = originObj && indexFormDoc === 0
|
|
488
|
-
|
|
489
|
-
|
|
492
|
+
? await DocumentService.put({ id: originObj._id, body })
|
|
493
|
+
: await DocumentService.post({
|
|
490
494
|
body,
|
|
491
495
|
});
|
|
492
496
|
const newDoc = {
|
|
@@ -514,12 +518,12 @@ class PanelForm {
|
|
|
514
518
|
fileId: {
|
|
515
519
|
fileBlob: file
|
|
516
520
|
? {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
521
|
+
data: {
|
|
522
|
+
data: await getDataFromInputFile(file),
|
|
523
|
+
},
|
|
524
|
+
mimetype: file.type,
|
|
525
|
+
name: file.name,
|
|
526
|
+
}
|
|
523
527
|
: undefined,
|
|
524
528
|
filePlain: undefined,
|
|
525
529
|
},
|
|
@@ -738,36 +742,36 @@ class PanelForm {
|
|
|
738
742
|
<div
|
|
739
743
|
class="in fll ssr-shimmer-search-box"
|
|
740
744
|
style="${renderCssAttr({
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
745
|
+
style: {
|
|
746
|
+
width: '80%',
|
|
747
|
+
height: '30px',
|
|
748
|
+
top: '-13px',
|
|
749
|
+
left: '10px',
|
|
750
|
+
},
|
|
751
|
+
})}"
|
|
748
752
|
></div>
|
|
749
753
|
</div>`,
|
|
750
754
|
createdAt: html`<div class="fl">
|
|
751
755
|
<div
|
|
752
756
|
class="in fll ssr-shimmer-search-box"
|
|
753
757
|
style="${renderCssAttr({
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
758
|
+
style: {
|
|
759
|
+
width: '50%',
|
|
760
|
+
height: '30px',
|
|
761
|
+
left: '-5px',
|
|
762
|
+
},
|
|
763
|
+
})}"
|
|
760
764
|
></div>
|
|
761
765
|
</div>`,
|
|
762
766
|
mdFileId: html`<div class="fl section-mp">
|
|
763
767
|
<div
|
|
764
768
|
class="in fll ssr-shimmer-search-box"
|
|
765
769
|
style="${renderCssAttr({
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
770
|
+
style: {
|
|
771
|
+
width: '80%',
|
|
772
|
+
height: '30px',
|
|
773
|
+
},
|
|
774
|
+
})}"
|
|
771
775
|
></div>
|
|
772
776
|
</div>`.repeat(random(2, 4)),
|
|
773
777
|
ssr: true,
|